Move TGOs out of MapModel.

This commit is contained in:
Dan Albert 2022-03-03 17:10:12 -08:00
parent d0ad554e14
commit c5c596dc2f
19 changed files with 186 additions and 186 deletions

View File

@ -4,10 +4,11 @@ from .database import Database
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato import Flight from game.ato import Flight
from game.theater import FrontLine from game.theater import FrontLine, TheaterGroundObject
class GameDb: class GameDb:
def __init__(self) -> None: def __init__(self) -> None:
self.flights: Database[Flight] = Database() self.flights: Database[Flight] = Database()
self.front_lines: Database[FrontLine] = Database() self.front_lines: Database[FrontLine] = Database()
self.tgos: Database[TheaterGroundObject] = Database()

View File

@ -278,6 +278,8 @@ class Game:
for control_point in self.theater.controlpoints: for control_point in self.theater.controlpoints:
control_point.initialize_turn_0() control_point.initialize_turn_0()
for tgo in control_point.connected_objectives:
self.db.tgos.add(tgo.id, tgo)
self.blue.preinit_turn_0() self.blue.preinit_turn_0()
self.red.preinit_turn_0() self.red.preinit_turn_0()

View File

@ -9,7 +9,7 @@ from . import (
frontlines, frontlines,
mapzones, mapzones,
navmesh, navmesh,
packagedialog, qt,
supplyroutes, supplyroutes,
tgos, tgos,
waypoints, waypoints,
@ -29,7 +29,7 @@ app.include_router(flights.router)
app.include_router(frontlines.router) app.include_router(frontlines.router)
app.include_router(mapzones.router) app.include_router(mapzones.router)
app.include_router(navmesh.router) app.include_router(navmesh.router)
app.include_router(packagedialog.router) app.include_router(qt.router)
app.include_router(supplyroutes.router) app.include_router(supplyroutes.router)
app.include_router(tgos.router) app.include_router(tgos.router)
app.include_router(waypoints.router) app.include_router(waypoints.router)

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Callable, TYPE_CHECKING from typing import Callable, TYPE_CHECKING
from game.theater import MissionTarget from game.theater import MissionTarget, TheaterGroundObject
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -28,8 +28,13 @@ class GameContext:
class QtCallbacks: class QtCallbacks:
def __init__(self, create_new_package: Callable[[MissionTarget], None]) -> None: def __init__(
self,
create_new_package: Callable[[MissionTarget], None],
show_tgo_info: Callable[[TheaterGroundObject], None],
) -> None:
self.create_new_package = create_new_package self.create_new_package = create_new_package
self.show_tgo_info = show_tgo_info
class QtContext: class QtContext:

View File

@ -31,6 +31,7 @@ class GameUpdateEventsJs(BaseModel):
new_front_lines: list[FrontLineJs] new_front_lines: list[FrontLineJs]
updated_front_lines: set[UUID] updated_front_lines: set[UUID]
deleted_front_lines: set[UUID] deleted_front_lines: set[UUID]
updated_tgos: set[UUID]
@classmethod @classmethod
def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs: def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs:
@ -62,4 +63,5 @@ class GameUpdateEventsJs(BaseModel):
], ],
updated_front_lines=events.updated_front_lines, updated_front_lines=events.updated_front_lines,
deleted_front_lines=events.deleted_front_lines, deleted_front_lines=events.deleted_front_lines,
updated_tgos=events.updated_tgos,
) )

View File

@ -1,18 +0,0 @@
from uuid import UUID
from fastapi import APIRouter, Depends
from game import Game
from ..dependencies import GameContext, QtCallbacks, QtContext
router: APIRouter = APIRouter(prefix="/package-dialog")
@router.post("/front-line/{front_line_id}")
def new_front_line_package(
front_line_id: UUID,
game: Game = Depends(GameContext.get),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
front_line = game.db.front_lines.get(front_line_id)
qt.create_new_package(front_line)

35
game/server/qt/routes.py Normal file
View File

@ -0,0 +1,35 @@
from uuid import UUID
from fastapi import APIRouter, Depends
from game import Game
from ..dependencies import GameContext, QtCallbacks, QtContext
router: APIRouter = APIRouter(prefix="/qt")
@router.post("/create-package/front-line/{front_line_id}")
def new_front_line_package(
front_line_id: UUID,
game: Game = Depends(GameContext.get),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
qt.create_new_package(game.db.front_lines.get(front_line_id))
@router.post("/create-package/tgo/{tgo_id}")
def new_tgo_package(
tgo_id: UUID,
game: Game = Depends(GameContext.get),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
qt.create_new_package(game.db.tgos.get(tgo_id))
@router.post("/info/tgo/{tgo_id}")
def show_tgo_info(
tgo_id: UUID,
game: Game = Depends(GameContext.get),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
qt.show_tgo_info(game.db.tgos.get(tgo_id))

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from uuid import UUID
from pydantic import BaseModel from pydantic import BaseModel
from game.server.leaflet import LeafletPoint from game.server.leaflet import LeafletPoint
@ -7,16 +9,17 @@ from game.theater import TheaterGroundObject
class TgoJs(BaseModel): class TgoJs(BaseModel):
id: UUID
name: str name: str
control_point_name: str control_point_name: str
category: str category: str
blue: bool blue: bool
position: LeafletPoint position: LeafletPoint
units: list[str] units: list[str] # TODO: Event stream
threat_ranges: list[float] threat_ranges: list[float] # TODO: Event stream
detection_ranges: list[float] detection_ranges: list[float] # TODO: Event stream
dead: bool dead: bool # TODO: Event stream
sidc: str sidc: str # TODO: Event stream
@staticmethod @staticmethod
def for_tgo(tgo: TheaterGroundObject) -> TgoJs: def for_tgo(tgo: TheaterGroundObject) -> TgoJs:
@ -29,6 +32,7 @@ class TgoJs(BaseModel):
tgo.detection_range(group).meters for group in tgo.groups tgo.detection_range(group).meters for group in tgo.groups
] ]
return TgoJs( return TgoJs(
id=tgo.id,
name=tgo.name, name=tgo.name,
control_point_name=tgo.control_point.name, control_point_name=tgo.control_point.name,
category=tgo.category, category=tgo.category,

View File

@ -1,3 +1,5 @@
from uuid import UUID
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from game import Game from game import Game
@ -15,3 +17,8 @@ def list_tgos(game: Game = Depends(GameContext.get)) -> list[TgoJs]:
if not tgo.is_control_point: if not tgo.is_control_point:
tgos.append(TgoJs.for_tgo(tgo)) tgos.append(TgoJs.for_tgo(tgo))
return tgos return tgos
@router.get("/{tgo_id}")
def get_tgo(tgo_id: UUID, game: Game = Depends(GameContext.get)) -> TgoJs:
return TgoJs.for_tgo(game.db.tgos.get(tgo_id))

View File

@ -9,7 +9,7 @@ from dcs import Point
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato import Flight, Package from game.ato import Flight, Package
from game.sim.combat import FrozenCombat from game.sim.combat import FrozenCombat
from game.theater import FrontLine from game.theater import FrontLine, TheaterGroundObject
@dataclass @dataclass
@ -30,6 +30,7 @@ class GameUpdateEvents:
new_front_lines: set[FrontLine] = field(default_factory=set) new_front_lines: set[FrontLine] = field(default_factory=set)
updated_front_lines: set[UUID] = field(default_factory=set) updated_front_lines: set[UUID] = field(default_factory=set)
deleted_front_lines: set[UUID] = field(default_factory=set) deleted_front_lines: set[UUID] = field(default_factory=set)
updated_tgos: set[UUID] = field(default_factory=set)
shutting_down: bool = False shutting_down: bool = False
@property @property
@ -111,6 +112,10 @@ class GameUpdateEvents:
self.deleted_front_lines.add(front_line.id) self.deleted_front_lines.add(front_line.id)
return self return self
def update_tgo(self, tgo: TheaterGroundObject) -> GameUpdateEvents:
self.updated_tgos.add(tgo.id)
return self
def shut_down(self) -> GameUpdateEvents: def shut_down(self) -> GameUpdateEvents:
self.shutting_down = True self.shutting_down = True
return self return self

View File

@ -30,7 +30,7 @@ class MissionResultsProcessor:
self.commit_convoy_losses(debriefing) self.commit_convoy_losses(debriefing)
self.commit_cargo_ship_losses(debriefing) self.commit_cargo_ship_losses(debriefing)
self.commit_airlift_losses(debriefing) self.commit_airlift_losses(debriefing)
self.commit_ground_losses(debriefing) self.commit_ground_losses(debriefing, events)
self.commit_damaged_runways(debriefing) self.commit_damaged_runways(debriefing)
self.commit_captures(debriefing, events) self.commit_captures(debriefing, events)
self.commit_front_line_battle_impact(debriefing, events) self.commit_front_line_battle_impact(debriefing, events)
@ -131,11 +131,11 @@ class MissionResultsProcessor:
) )
@staticmethod @staticmethod
def commit_ground_losses(debriefing: Debriefing) -> None: def commit_ground_losses(debriefing: Debriefing, events: GameUpdateEvents) -> None:
for ground_object_loss in debriefing.ground_object_losses: for ground_object_loss in debriefing.ground_object_losses:
ground_object_loss.theater_unit.kill() ground_object_loss.theater_unit.kill(events)
for scenery_object_loss in debriefing.scenery_object_losses: for scenery_object_loss in debriefing.scenery_object_losses:
scenery_object_loss.ground_unit.kill() scenery_object_loss.ground_unit.kill(events)
@staticmethod @staticmethod
def commit_damaged_runways(debriefing: Debriefing) -> None: def commit_damaged_runways(debriefing: Debriefing) -> None:

View File

@ -730,11 +730,11 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
for squadron in self.squadrons: for squadron in self.squadrons:
self._retreat_squadron(game, squadron) self._retreat_squadron(game, squadron)
def depopulate_uncapturable_tgos(self) -> None: def depopulate_uncapturable_tgos(self, events: GameUpdateEvents) -> None:
# TODO Rework this. # TODO Rework this.
for tgo in self.connected_objectives: for tgo in self.connected_objectives:
if not tgo.capturable: if not tgo.capturable:
tgo.clear() tgo.clear(events)
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None: def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None:
@ -742,7 +742,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self.ground_unit_orders.refund_all(self.coalition) self.ground_unit_orders.refund_all(self.coalition)
self.retreat_ground_units(game) self.retreat_ground_units(game)
self.retreat_air_units(game) self.retreat_air_units(game)
self.depopulate_uncapturable_tgos() self.depopulate_uncapturable_tgos(events)
self._coalition = new_coalition self._coalition = new_coalition
self.base.set_strength_to_minimum() self.base.set_strength_to_minimum()

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import itertools import itertools
import uuid
from abc import ABC from abc import ABC
from typing import Iterator, List, Optional, TYPE_CHECKING from typing import Iterator, List, Optional, TYPE_CHECKING
@ -22,6 +23,7 @@ from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
from ..utils import Distance, Heading, meters from ..utils import Distance, Heading, meters
if TYPE_CHECKING: if TYPE_CHECKING:
from game.sim import GameUpdateEvents
from .theatergroup import TheaterUnit, TheaterGroup from .theatergroup import TheaterUnit, TheaterGroup
from .controlpoint import ControlPoint from .controlpoint import ControlPoint
from ..ato.flighttype import FlightType from ..ato.flighttype import FlightType
@ -62,6 +64,7 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC):
sea_object: bool, sea_object: bool,
) -> None: ) -> None:
super().__init__(name, position) super().__init__(name, position)
self.id = uuid.uuid4()
self.category = category self.category = category
self.heading = heading self.heading = heading
self.control_point = control_point self.control_point = control_point
@ -212,8 +215,9 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC):
def mark_locations(self) -> Iterator[Point]: def mark_locations(self) -> Iterator[Point]:
yield self.position yield self.position
def clear(self) -> None: def clear(self, events: GameUpdateEvents) -> None:
self.groups = [] self.groups = []
events.update_tgo(self)
@property @property
def capturable(self) -> bool: def capturable(self) -> bool:

View File

@ -1,22 +1,20 @@
from __future__ import annotations from __future__ import annotations
import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, Any, TYPE_CHECKING, Type from typing import Any, Optional, TYPE_CHECKING, Type
from dcs.triggers import TriggerZone from dcs.triggers import TriggerZone
from dcs.unittype import VehicleType, ShipType, StaticType from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType, VehicleType
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType from game.dcs.shipunittype import ShipUnitType
from game.dcs.unittype import UnitType from game.dcs.unittype import UnitType
from dcs.unittype import UnitType as DcsUnitType
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
from game.utils import Heading from game.utils import Heading
if TYPE_CHECKING: if TYPE_CHECKING:
from game.layout.layout import LayoutUnit, TgoLayoutGroup from game.layout.layout import LayoutUnit
from game.sim import GameUpdateEvents
from game.theater import TheaterGroundObject from game.theater import TheaterGroundObject
@ -58,8 +56,9 @@ class TheaterUnit:
# None for not available StaticTypes # None for not available StaticTypes
return None return None
def kill(self) -> None: def kill(self, events: GameUpdateEvents) -> None:
self.alive = False self.alive = False
events.update_tgo(self.ground_object)
@property @property
def unit_name(self) -> str: def unit_name(self) -> str:

View File

@ -1,90 +0,0 @@
from __future__ import annotations
from typing import List, Optional
from PySide2.QtCore import Property, QObject, Signal, Slot
from game import Game
from game.server.leaflet import LeafletLatLon
from game.theater import TheaterGroundObject
from qt_ui.dialogs import Dialog
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
class GroundObjectJs(QObject):
nameChanged = Signal()
controlPointNameChanged = Signal()
sidcChanged = Signal()
unitsChanged = Signal()
blueChanged = Signal()
positionChanged = Signal()
samThreatRangesChanged = Signal()
samDetectionRangesChanged = Signal()
categoryChanged = Signal()
deadChanged = Signal()
def __init__(self, tgo: TheaterGroundObject, game: Game) -> None:
super().__init__()
self.tgo = tgo
self.game = game
self.theater = game.theater
self.dialog: Optional[QGroundObjectMenu] = None
@Slot()
def showInfoDialog(self) -> None:
if self.dialog is None:
self.dialog = QGroundObjectMenu(
None,
self.tgo,
self.tgo.control_point,
self.game,
)
self.dialog.show()
@Slot()
def showPackageDialog(self) -> None:
Dialog.open_new_package_dialog(self.tgo)
@Property(str, notify=nameChanged)
def name(self) -> str:
return self.tgo.name
@Property(str, notify=controlPointNameChanged)
def controlPointName(self) -> str:
return self.tgo.control_point.name
@Property(str, notify=sidcChanged)
def sidc(self) -> str:
return str(self.tgo.sidc())
@Property(str, notify=categoryChanged)
def category(self) -> str:
return self.tgo.category
@Property(list, notify=unitsChanged)
def units(self) -> List[str]:
return [unit.display_name for unit in self.tgo.units]
@Property(bool, notify=blueChanged)
def blue(self) -> bool:
return self.tgo.control_point.captured
@Property(list, notify=positionChanged)
def position(self) -> LeafletLatLon:
return self.tgo.position.latlng().as_list()
@Property(bool, notify=deadChanged)
def dead(self) -> bool:
return not any(g.alive_units > 0 for g in self.tgo.groups)
@Property(list, notify=samThreatRangesChanged)
def samThreatRanges(self) -> List[float]:
if not self.tgo.might_have_aa:
return []
return [self.tgo.threat_range(group).meters for group in self.tgo.groups]
@Property(list, notify=samDetectionRangesChanged)
def samDetectionRanges(self) -> List[float]:
if not self.tgo.might_have_aa:
return []
return [self.tgo.detection_range(group).meters for group in self.tgo.groups]

View File

@ -15,7 +15,6 @@ from game.theater import (
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from .controlpointjs import ControlPointJs from .controlpointjs import ControlPointJs
from .groundobjectjs import GroundObjectJs
from .supplyroutejs import SupplyRouteJs from .supplyroutejs import SupplyRouteJs
@ -41,7 +40,6 @@ class MapModel(QObject):
apiKeyChanged = Signal(str) apiKeyChanged = Signal(str)
mapCenterChanged = Signal(list) mapCenterChanged = Signal(list)
controlPointsChanged = Signal() controlPointsChanged = Signal()
groundObjectsChanged = Signal()
supplyRoutesChanged = Signal() supplyRoutesChanged = Signal()
mapReset = Signal() mapReset = Signal()
@ -50,7 +48,6 @@ class MapModel(QObject):
self.game_model = game_model self.game_model = game_model
self._map_center = LatLng(0, 0) self._map_center = LatLng(0, 0)
self._control_points = [] self._control_points = []
self._ground_objects = []
self._supply_routes = [] self._supply_routes = []
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load) GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
@ -59,7 +56,6 @@ class MapModel(QObject):
def clear(self) -> None: def clear(self) -> None:
self._control_points = [] self._control_points = []
self._supply_routes = [] self._supply_routes = []
self._ground_objects = []
self.cleared.emit() self.cleared.emit()
def reset(self) -> None: def reset(self) -> None:
@ -68,7 +64,6 @@ class MapModel(QObject):
return return
with logged_duration("Map reset"): with logged_duration("Map reset"):
self.reset_control_points() self.reset_control_points()
self.reset_ground_objects()
self.reset_routes() self.reset_routes()
self.mapReset.emit() self.mapReset.emit()
@ -99,27 +94,6 @@ class MapModel(QObject):
def controlPoints(self) -> List[ControlPointJs]: def controlPoints(self) -> List[ControlPointJs]:
return self._control_points return self._control_points
def reset_ground_objects(self) -> None:
seen = set()
self._ground_objects = []
for cp in self.game.theater.controlpoints:
for tgo in cp.ground_objects:
if tgo.name in seen:
continue
seen.add(tgo.name)
if tgo.is_control_point:
# TGOs that are the CP (CV groups) are an implementation quirk that
# we don't need to expose to the UI.
continue
self._ground_objects.append(GroundObjectJs(tgo, self.game))
self.groundObjectsChanged.emit()
@Property(list, notify=groundObjectsChanged)
def groundObjects(self) -> List[GroundObjectJs]:
return self._ground_objects
def reset_routes(self) -> None: def reset_routes(self) -> None:
seen = set() seen = set()
self._supply_routes = [] self._supply_routes = []

View File

@ -24,7 +24,7 @@ from game.layout import LAYOUTS
from game.server import EventStream, GameContext from game.server import EventStream, GameContext
from game.server.dependencies import QtCallbacks, QtContext from game.server.dependencies import QtCallbacks, QtContext
from game.server.security import ApiKeyManager from game.server.security import ApiKeyManager
from game.theater import MissionTarget from game.theater import MissionTarget, TheaterGroundObject
from qt_ui import liberation_install from qt_ui import liberation_install
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel from qt_ui.models import GameModel
@ -36,6 +36,7 @@ from qt_ui.widgets.ato import QAirTaskingOrderPanel
from qt_ui.widgets.map.QLiberationMap import QLiberationMap from qt_ui.widgets.map.QLiberationMap import QLiberationMap
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QDebriefingWindow import QDebriefingWindow from qt_ui.windows.QDebriefingWindow import QDebriefingWindow
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from qt_ui.windows.infos.QInfoPanel import QInfoPanel from qt_ui.windows.infos.QInfoPanel import QInfoPanel
from qt_ui.windows.logs.QLogsWindow import QLogsWindow from qt_ui.windows.logs.QLogsWindow import QLogsWindow
from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard
@ -49,6 +50,7 @@ from qt_ui.windows.stats.QStatsWindow import QStatsWindow
class QLiberationWindow(QMainWindow): class QLiberationWindow(QMainWindow):
new_package_signal = Signal(MissionTarget) new_package_signal = Signal(MissionTarget)
tgo_info_signal = Signal(TheaterGroundObject)
def __init__(self, game: Optional[Game], new_map: bool) -> None: def __init__(self, game: Optional[Game], new_map: bool) -> None:
super().__init__() super().__init__()
@ -63,8 +65,12 @@ class QLiberationWindow(QMainWindow):
self.new_package_signal.connect( self.new_package_signal.connect(
lambda target: Dialog.open_new_package_dialog(target, self) lambda target: Dialog.open_new_package_dialog(target, self)
) )
self.tgo_info_signal.connect(self.open_tgo_info_dialog)
QtContext.set_callbacks( QtContext.set_callbacks(
QtCallbacks(lambda target: self.new_package_signal.emit(target)) QtCallbacks(
lambda target: self.new_package_signal.emit(target),
lambda tgo: self.tgo_info_signal.emit(tgo),
)
) )
Dialog.set_game(self.game_model) Dialog.set_game(self.game_model)
self.ato_panel = QAirTaskingOrderPanel(self.game_model) self.ato_panel = QAirTaskingOrderPanel(self.game_model)
@ -437,6 +443,9 @@ class QLiberationWindow(QMainWindow):
self.debriefing = QDebriefingWindow(debrief) self.debriefing = QDebriefingWindow(debrief)
self.debriefing.show() self.debriefing.show()
def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None:
QGroundObjectMenu(self, tgo, tgo.control_point, self.game).show()
def _qsettings(self) -> QSettings: def _qsettings(self) -> QSettings:
return QSettings("DCS Liberation", "Qt UI") return QSettings("DCS Liberation", "Qt UI")

View File

@ -253,7 +253,6 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
game.cleared.connect(clearAllLayers); game.cleared.connect(clearAllLayers);
game.mapCenterChanged.connect(recenterMap); game.mapCenterChanged.connect(recenterMap);
game.controlPointsChanged.connect(drawControlPoints); game.controlPointsChanged.connect(drawControlPoints);
game.groundObjectsChanged.connect(drawGroundObjects);
game.supplyRoutesChanged.connect(drawSupplyRoutes); game.supplyRoutesChanged.connect(drawSupplyRoutes);
game.mapReset.connect(drawAircraft); game.mapReset.connect(drawAircraft);
}); });
@ -320,6 +319,10 @@ function handleStreamedEvents(events) {
for (const id of events.deleted_front_lines) { for (const id of events.deleted_front_lines) {
FrontLine.popId(id).clear(); FrontLine.popId(id).clear();
} }
for (const id of events.updated_tgos) {
TheaterGroundObject.withId(id).update();
}
} }
function recenterMap(center) { function recenterMap(center) {
@ -523,6 +526,39 @@ function drawControlPoints() {
class TheaterGroundObject { class TheaterGroundObject {
constructor(tgo) { constructor(tgo) {
this.tgo = tgo; this.tgo = tgo;
this.marker = null;
this.threatCircles = [];
this.detectionCircles = [];
TheaterGroundObject.register(this);
}
static registered = [];
static register(tgo) {
TheaterGroundObject.registered[tgo.tgo.id] = tgo;
}
static withId(id) {
return TheaterGroundObject.registered[id];
}
showInfoDialog() {
postJson(`/qt/info/tgo/${this.tgo.id}`);
}
showPackageDialog() {
postJson(`/qt/create-package/tgo/${this.tgo.id}`);
}
update() {
getJson(`/tgos/${this.tgo.id}`).then((tgo) => {
// Clear explicitly before replacing the TGO in case (though this
// shouldn't happen) the replacement data changes the layer this TGO is
// drawn on.
this.clear();
this.tgo = tgo;
this.draw();
});
} }
icon() { icon() {
@ -550,27 +586,50 @@ class TheaterGroundObject {
const threatColor = this.tgo.blue ? Colors.Blue : Colors.Red; const threatColor = this.tgo.blue ? Colors.Blue : Colors.Red;
const detectionColor = this.tgo.blue ? "#bb89ff" : "#eee17b"; const detectionColor = this.tgo.blue ? "#bb89ff" : "#eee17b";
this.tgo.samDetectionRanges.forEach((range) => { this.tgo.detection_ranges.forEach((range) => {
L.circle(this.tgo.position, { this.detectionCircles.push(
radius: range, L.circle(this.tgo.position, {
color: detectionColor, radius: range,
fill: false, color: detectionColor,
weight: 1, fill: false,
interactive: false, weight: 1,
}).addTo(detectionLayer); interactive: false,
}).addTo(detectionLayer)
);
}); });
this.tgo.samThreatRanges.forEach((range) => { this.tgo.threat_ranges.forEach((range) => {
L.circle(this.tgo.position, { this.threatCircles.push(
radius: range, L.circle(this.tgo.position, {
color: threatColor, radius: range,
fill: false, color: threatColor,
weight: 2, fill: false,
interactive: false, weight: 2,
}).addTo(threatLayer); interactive: false,
}).addTo(threatLayer)
);
}); });
} }
clear() {
const detectionLayer = this.tgo.blue
? blueSamDetectionLayer
: redSamDetectionLayer;
const threatLayer = this.tgo.blue ? blueSamThreatLayer : redSamThreatLayer;
if (this.marker) {
this.marker.removeFrom(this.layer());
}
for (const circle of this.threatCircles) {
circle.removeFrom(threatLayer);
}
for (const circle of this.detectionCircles) {
circle.removeFrom(detectionLayer);
}
}
draw() { draw() {
if (!this.tgo.blue && this.tgo.dead) { if (!this.tgo.blue && this.tgo.dead) {
// Don't bother drawing dead opfor TGOs. Blue is worth showing because // Don't bother drawing dead opfor TGOs. Blue is worth showing because
@ -579,14 +638,14 @@ class TheaterGroundObject {
return; return;
} }
L.marker(this.tgo.position, { icon: this.icon() }) this.marker = L.marker(this.tgo.position, { icon: this.icon() })
.bindTooltip( .bindTooltip(
`${this.tgo.name} (${ `${this.tgo.name} (${
this.tgo.controlPointName this.tgo.control_point_name
})<br />${this.tgo.units.join("<br />")}` })<br />${this.tgo.units.join("<br />")}`
) )
.on("click", () => this.tgo.showInfoDialog()) .on("click", () => this.showInfoDialog())
.on("contextmenu", () => this.tgo.showPackageDialog()) .on("contextmenu", () => this.showPackageDialog())
.addTo(this.layer()); .addTo(this.layer());
this.drawSamThreats(); this.drawSamThreats();
} }
@ -601,8 +660,10 @@ function drawGroundObjects() {
redSamDetectionLayer.clearLayers(); redSamDetectionLayer.clearLayers();
blueSamThreatLayer.clearLayers(); blueSamThreatLayer.clearLayers();
redSamThreatLayer.clearLayers(); redSamThreatLayer.clearLayers();
game.groundObjects.forEach((tgo) => { getJson("/tgos").then((tgos) => {
new TheaterGroundObject(tgo).draw(); for (const tgo of tgos) {
new TheaterGroundObject(tgo).draw();
}
}); });
} }
@ -683,7 +744,7 @@ class FrontLine {
} }
openNewPackageDialog() { openNewPackageDialog() {
postJson(`/package-dialog/front-line/${this.id}`); postJson(`/qt/create-package/front-line/${this.id}`);
} }
} }