diff --git a/game/db/gamedb.py b/game/db/gamedb.py index f9e92f1e..8bebdd04 100644 --- a/game/db/gamedb.py +++ b/game/db/gamedb.py @@ -4,8 +4,10 @@ from .database import Database if TYPE_CHECKING: from game.ato import Flight + from game.theater import FrontLine class GameDb: def __init__(self) -> None: self.flights: Database[Flight] = Database() + self.front_lines: Database[FrontLine] = Database() diff --git a/game/game.py b/game/game.py index 734b3ff1..f5d1714e 100644 --- a/game/game.py +++ b/game/game.py @@ -222,7 +222,7 @@ class Game: # reset when we're done. self.compute_threat_zones(GameUpdateEvents()) - def finish_turn(self, skipped: bool = False) -> None: + def finish_turn(self, events: GameUpdateEvents, skipped: bool = False) -> None: """Finalizes the current turn and advances to the next turn. This handles the turn-end portion of passing a turn. Initialization of the next @@ -265,6 +265,9 @@ class Game: if not skipped: for cp in self.theater.player_points(): + for front_line in cp.front_lines.values(): + front_line.update_position() + events.update_front_line(front_line) cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY) self.conditions = self.generate_conditions() @@ -293,11 +296,12 @@ class Game: from .server import EventStream from .sim import GameUpdateEvents + events = GameUpdateEvents() + logging.info("Pass turn") with logged_duration("Turn finalization"): - self.finish_turn(no_action) + self.finish_turn(events, no_action) - events = GameUpdateEvents() with logged_duration("Turn initialization"): self.initialize_turn(events) diff --git a/game/server/app.py b/game/server/app.py index 549ad01d..9c26cc8a 100644 --- a/game/server/app.py +++ b/game/server/app.py @@ -9,6 +9,7 @@ from . import ( frontlines, mapzones, navmesh, + packagedialog, supplyroutes, tgos, waypoints, @@ -28,6 +29,7 @@ app.include_router(flights.router) app.include_router(frontlines.router) app.include_router(mapzones.router) app.include_router(navmesh.router) +app.include_router(packagedialog.router) app.include_router(supplyroutes.router) app.include_router(tgos.router) app.include_router(waypoints.router) diff --git a/game/server/dependencies.py b/game/server/dependencies.py index 0440d44f..f7e5ca55 100644 --- a/game/server/dependencies.py +++ b/game/server/dependencies.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import Callable, TYPE_CHECKING + +from game.theater import MissionTarget if TYPE_CHECKING: from game import Game @@ -23,3 +25,22 @@ class GameContext: @classmethod def get_model(cls) -> GameModel: return cls._game_model + + +class QtCallbacks: + def __init__(self, create_new_package: Callable[[MissionTarget], None]) -> None: + self.create_new_package = create_new_package + + +class QtContext: + _callbacks: QtCallbacks + + @classmethod + def set_callbacks(cls, callbacks: QtCallbacks) -> None: + cls._callbacks = callbacks + + @classmethod + def get(cls) -> QtCallbacks: + if cls._callbacks is None: + raise RuntimeError("QtContext has no callbacks set") + return cls._callbacks diff --git a/game/server/eventstream/models.py b/game/server/eventstream/models.py index a2cabd38..5a6c4db7 100644 --- a/game/server/eventstream/models.py +++ b/game/server/eventstream/models.py @@ -7,6 +7,7 @@ from pydantic import BaseModel from game.server.combat.models import FrozenCombatJs from game.server.flights.models import FlightJs +from game.server.frontlines.models import FrontLineJs from game.server.leaflet import LeafletLatLon if TYPE_CHECKING: @@ -27,6 +28,9 @@ class GameUpdateEventsJs(BaseModel): deleted_flights: set[UUID] selected_flight: UUID | None deselected_flight: bool + new_front_lines: list[FrontLineJs] + updated_front_lines: set[UUID] + deleted_front_lines: set[UUID] @classmethod def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs: @@ -53,4 +57,9 @@ class GameUpdateEventsJs(BaseModel): deleted_flights=events.deleted_flights, selected_flight=events.selected_flight, deselected_flight=events.deselected_flight, + new_front_lines=[ + FrontLineJs.for_front_line(f) for f in events.new_front_lines + ], + updated_front_lines=events.updated_front_lines, + deleted_front_lines=events.deleted_front_lines, ) diff --git a/game/server/frontlines/models.py b/game/server/frontlines/models.py index 6a9d85f5..73290537 100644 --- a/game/server/frontlines/models.py +++ b/game/server/frontlines/models.py @@ -1,9 +1,24 @@ from __future__ import annotations +from uuid import UUID + from pydantic import BaseModel from game.server.leaflet import LeafletPoint +from game.theater import FrontLine +from game.utils import nautical_miles class FrontLineJs(BaseModel): + id: UUID extents: list[LeafletPoint] + + @staticmethod + def for_front_line(front_line: FrontLine) -> FrontLineJs: + a = front_line.position.point_from_heading( + front_line.attack_heading.right.degrees, nautical_miles(2).meters + ) + b = front_line.position.point_from_heading( + front_line.attack_heading.left.degrees, nautical_miles(2).meters + ) + return FrontLineJs(id=front_line.id, extents=[a.latlng(), b.latlng()]) diff --git a/game/server/frontlines/routes.py b/game/server/frontlines/routes.py index f0e70da5..9d141b00 100644 --- a/game/server/frontlines/routes.py +++ b/game/server/frontlines/routes.py @@ -1,7 +1,8 @@ +from uuid import UUID + from fastapi import APIRouter, Depends from game import Game -from game.utils import nautical_miles from .models import FrontLineJs from ..dependencies import GameContext @@ -10,13 +11,11 @@ router: APIRouter = APIRouter(prefix="/front-lines") @router.get("/") def list_front_lines(game: Game = Depends(GameContext.get)) -> list[FrontLineJs]: - front_lines = [] - for front_line in game.theater.conflicts(): - a = front_line.position.point_from_heading( - front_line.attack_heading.right.degrees, nautical_miles(2).meters - ) - b = front_line.position.point_from_heading( - front_line.attack_heading.left.degrees, nautical_miles(2).meters - ) - front_lines.append(FrontLineJs(extents=[a.latlng(), b.latlng()])) - return front_lines + return [FrontLineJs.for_front_line(f) for f in game.theater.conflicts()] + + +@router.get("/{front_line_id}") +def get_front_line( + front_line_id: UUID, game: Game = Depends(GameContext.get) +) -> FrontLineJs: + return FrontLineJs.for_front_line(game.db.front_lines.get(front_line_id)) diff --git a/game/server/packagedialog/__init__.py b/game/server/packagedialog/__init__.py new file mode 100644 index 00000000..3a27ef1c --- /dev/null +++ b/game/server/packagedialog/__init__.py @@ -0,0 +1 @@ +from .routes import router diff --git a/game/server/packagedialog/routes.py b/game/server/packagedialog/routes.py new file mode 100644 index 00000000..ec718389 --- /dev/null +++ b/game/server/packagedialog/routes.py @@ -0,0 +1,18 @@ +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) diff --git a/game/sim/gameloop.py b/game/sim/gameloop.py index 78ae2ef4..7d965800 100644 --- a/game/sim/gameloop.py +++ b/game/sim/gameloop.py @@ -68,8 +68,9 @@ class GameLoop: def complete_with_results(self, debriefing: Debriefing) -> None: self.pause() - self.sim.process_results(debriefing) + self.sim.process_results(debriefing, self.events) self.completed = True + self.send_update(rate_limit=False) def send_update(self, rate_limit: bool) -> None: # We don't skip empty events because we still want the tick in the Qt part of diff --git a/game/sim/gameupdateevents.py b/game/sim/gameupdateevents.py index 83f4d372..0f2099bc 100644 --- a/game/sim/gameupdateevents.py +++ b/game/sim/gameupdateevents.py @@ -9,6 +9,7 @@ from dcs import Point if TYPE_CHECKING: from game.ato import Flight, Package from game.sim.combat import FrozenCombat + from game.theater import FrontLine @dataclass @@ -26,6 +27,9 @@ class GameUpdateEvents: deleted_flights: set[UUID] = field(default_factory=set) selected_flight: UUID | None = None deselected_flight: bool = False + new_front_lines: set[FrontLine] = field(default_factory=set) + updated_front_lines: set[UUID] = field(default_factory=set) + deleted_front_lines: set[UUID] = field(default_factory=set) shutting_down: bool = False @property @@ -95,6 +99,18 @@ class GameUpdateEvents: self.selected_flight = None return self + def new_front_line(self, front_line: FrontLine) -> GameUpdateEvents: + self.new_front_lines.add(front_line) + return self + + def update_front_line(self, front_line: FrontLine) -> GameUpdateEvents: + self.updated_front_lines.add(front_line.id) + return self + + def delete_front_line(self, front_line: FrontLine) -> GameUpdateEvents: + self.deleted_front_lines.add(front_line.id) + return self + def shut_down(self) -> GameUpdateEvents: self.shutting_down = True return self diff --git a/game/sim/missionresultsprocessor.py b/game/sim/missionresultsprocessor.py index 397c5972..82a1ecd7 100644 --- a/game/sim/missionresultsprocessor.py +++ b/game/sim/missionresultsprocessor.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING from game.debriefing import Debriefing from game.ground_forces.combat_stance import CombatStance from game.theater import ControlPoint +from .gameupdateevents import GameUpdateEvents from ..ato.airtaaskingorder import AirTaskingOrder if TYPE_CHECKING: @@ -21,7 +22,7 @@ class MissionResultsProcessor: def __init__(self, game: Game) -> None: self.game = game - def commit(self, debriefing: Debriefing) -> None: + def commit(self, debriefing: Debriefing, events: GameUpdateEvents) -> None: logging.info("Committing mission results") self.commit_air_losses(debriefing) self.commit_pilot_experience() @@ -31,8 +32,8 @@ class MissionResultsProcessor: self.commit_airlift_losses(debriefing) self.commit_ground_losses(debriefing) self.commit_damaged_runways(debriefing) - self.commit_captures(debriefing) - self.commit_front_line_battle_impact(debriefing) + self.commit_captures(debriefing, events) + self.commit_front_line_battle_impact(debriefing, events) self.record_carcasses(debriefing) def commit_air_losses(self, debriefing: Debriefing) -> None: @@ -141,7 +142,7 @@ class MissionResultsProcessor: for damaged_runway in debriefing.damaged_runways: damaged_runway.damage_runway() - def commit_captures(self, debriefing: Debriefing) -> None: + def commit_captures(self, debriefing: Debriefing, events: GameUpdateEvents) -> None: for captured in debriefing.base_captures: try: if captured.captured_by_player: @@ -155,7 +156,9 @@ class MissionResultsProcessor: f"The enemy took control of {captured.control_point}.", ) - captured.control_point.capture(self.game, captured.captured_by_player) + captured.control_point.capture( + self.game, events, captured.captured_by_player + ) logging.info(f"Will run redeploy for {captured.control_point}") self.redeploy_units(captured.control_point) except Exception: @@ -165,10 +168,16 @@ class MissionResultsProcessor: for destroyed_unit in debriefing.state_data.destroyed_statics: self.game.add_destroyed_units(destroyed_unit) - def commit_front_line_battle_impact(self, debriefing: Debriefing) -> None: + def commit_front_line_battle_impact( + self, debriefing: Debriefing, events: GameUpdateEvents + ) -> None: for cp in self.game.theater.player_points(): enemy_cps = [e for e in cp.connected_points if not e.captured] for enemy_cp in enemy_cps: + front_line = cp.front_line_with(enemy_cp) + front_line.update_position() + events.update_front_line(front_line) + print( "Compute frontline progression for : " + cp.name diff --git a/game/sim/missionsimulation.py b/game/sim/missionsimulation.py index 5868bc68..c96e7846 100644 --- a/game/sim/missionsimulation.py +++ b/game/sim/missionsimulation.py @@ -66,14 +66,14 @@ class MissionSimulation: debriefing.merge_simulation_results(self.aircraft_simulation.results) return debriefing - def process_results(self, debriefing: Debriefing) -> None: + def process_results(self, debriefing: Debriefing, events: GameUpdateEvents) -> None: if self.unit_map is None: raise RuntimeError( "Simulation has no unit map. Results processing began before a mission " "was generated." ) - MissionResultsProcessor(self.game).commit(debriefing) + MissionResultsProcessor(self.game).commit(debriefing, events) def finish(self) -> None: self.unit_map = None diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 621d0ac0..e58e7815 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -52,6 +52,7 @@ from .theatergroundobject import ( from .theatergroup import TheaterUnit from ..ato.starttype import StartType from ..data.units import UnitClass +from ..db import Database from ..dcs.aircrafttype import AircraftType from ..dcs.groundunittype import GroundUnitType from ..utils import nautical_miles @@ -59,10 +60,11 @@ from ..weather import Conditions if TYPE_CHECKING: from game import Game - from ..ato.flighttype import FlightType + from game.ato.flighttype import FlightType + from game.coalition import Coalition + from game.sim import GameUpdateEvents from game.squadrons.squadron import Squadron - from ..coalition import Coalition - from ..transfers import PendingTransfers + from game.transfers import PendingTransfers from .conflicttheater import ConflictTheater FREE_FRONTLINE_UNIT_SUPPLY: int = 15 @@ -326,6 +328,9 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): self.target_position: Optional[Point] = None + # Initialized late because ControlPoints are constructed before the game is. + self._front_line_db: Database[FrontLine] | None = None + def __repr__(self) -> str: return f"<{self.__class__}: {self.name}>" @@ -338,31 +343,50 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): def finish_init(self, game: Game) -> None: assert self._coalition is None self._coalition = game.coalition_for(self.starts_blue) + assert self._front_line_db is None + self._front_line_db = game.db.front_lines def initialize_turn_0(self) -> None: - self._recreate_front_lines() + # We don't need to send events for turn 0. The UI isn't up yet, and it'll fetch + # the entire game state when it comes up. + from game.sim import GameUpdateEvents - def _recreate_front_lines(self) -> None: - self._clear_front_lines() + self._create_missing_front_lines(GameUpdateEvents()) + + @property + def front_line_db(self) -> Database[FrontLine]: + assert self._front_line_db is not None + return self._front_line_db + + def _create_missing_front_lines(self, events: GameUpdateEvents) -> None: for connection in self.convoy_routes.keys(): if not connection.front_line_active_with( self ) and not connection.is_friendly_to(self): - self._create_front_line_with(connection) + self._create_front_line_with(connection, events) - def _create_front_line_with(self, connection: ControlPoint) -> None: + def _create_front_line_with( + self, connection: ControlPoint, events: GameUpdateEvents + ) -> None: blue, red = FrontLine.sort_control_points(self, connection) front = FrontLine(blue, red) self.front_lines[connection] = front connection.front_lines[self] = front + self.front_line_db.add(front.id, front) + events.new_front_line(front) - def _remove_front_line_with(self, connection: ControlPoint) -> None: + def _remove_front_line_with( + self, connection: ControlPoint, events: GameUpdateEvents + ) -> None: + front = self.front_lines[connection] del self.front_lines[connection] del connection.front_lines[self] + self.front_line_db.remove(front.id) + events.delete_front_line(front) - def _clear_front_lines(self) -> None: + def _clear_front_lines(self, events: GameUpdateEvents) -> None: for opponent in list(self.front_lines.keys()): - self._remove_front_line_with(opponent) + self._remove_front_line_with(opponent, events) @property def has_frontline(self) -> bool: @@ -713,7 +737,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): tgo.clear() # TODO: Should be Airbase specific. - def capture(self, game: Game, for_player: bool) -> None: + def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None: new_coalition = game.coalition_for(for_player) self.ground_unit_orders.refund_all(self.coalition) self.retreat_ground_units(game) @@ -722,7 +746,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): self._coalition = new_coalition self.base.set_strength_to_minimum() - self._recreate_front_lines() + self._clear_front_lines(events) + self._create_missing_front_lines(events) @property def required_aircraft_start_type(self) -> Optional[StartType]: @@ -1127,7 +1152,7 @@ class Carrier(NavalControlPoint): FlightType.REFUELING, ] - def capture(self, game: Game, for_player: bool) -> None: + def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None: raise RuntimeError("Carriers cannot be captured") @property @@ -1161,7 +1186,7 @@ class Lha(NavalControlPoint): def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.AMPHIBIOUS_ASSAULT_SHIP_GENERAL - def capture(self, game: Game, for_player: bool) -> None: + def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None: raise RuntimeError("LHAs cannot be captured") @property @@ -1198,7 +1223,7 @@ class OffMapSpawn(ControlPoint): def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: return SymbolSet.LAND_INSTALLATIONS, LandInstallationEntity.AIPORT_AIR_BASE - def capture(self, game: Game, for_player: bool) -> None: + def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None: raise RuntimeError("Off map control points cannot be captured") def mission_types(self, for_player: bool) -> Iterator[FlightType]: diff --git a/game/theater/frontline.py b/game/theater/frontline.py index a17030c9..9c0e1ae7 100644 --- a/game/theater/frontline.py +++ b/game/theater/frontline.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import uuid from dataclasses import dataclass from typing import Any, Iterator, List, TYPE_CHECKING, Tuple @@ -49,6 +50,7 @@ class FrontLine(MissionTarget): blue_point: ControlPoint, red_point: ControlPoint, ) -> None: + self.id = uuid.uuid4() self.blue_cp = blue_point self.red_cp = red_point try: @@ -67,8 +69,7 @@ class FrontLine(MissionTarget): FrontLineSegment(a, b) for a, b in pairwise(route) ] super().__init__( - f"Front line {blue_point}/{red_point}", - self.point_from_a(self._position_distance), + f"Front line {blue_point}/{red_point}", self._compute_position() ) def __eq__(self, other: Any) -> bool: @@ -79,10 +80,11 @@ class FrontLine(MissionTarget): def __hash__(self) -> int: return hash((self.blue_cp, self.red_cp)) - def __setstate__(self, state: dict[str, Any]) -> None: - self.__dict__.update(state) - if not hasattr(self, "position"): - self.position = self.point_from_a(self._position_distance) + def _compute_position(self) -> Point: + return self.point_from_a(self._position_distance) + + def update_position(self) -> None: + self.position = self._compute_position() def control_point_friendly_to(self, player: bool) -> ControlPoint: if player: diff --git a/qt_ui/widgets/map/model/frontlinejs.py b/qt_ui/widgets/map/model/frontlinejs.py deleted file mode 100644 index 3a5656ca..00000000 --- a/qt_ui/widgets/map/model/frontlinejs.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -from typing import List - -from PySide2.QtCore import Property, QObject, Signal, Slot - -from game.server.leaflet import LeafletLatLon -from game.theater import FrontLine -from game.utils import nautical_miles -from qt_ui.dialogs import Dialog - - -class FrontLineJs(QObject): - extentsChanged = Signal() - - def __init__(self, front_line: FrontLine) -> None: - super().__init__() - self.front_line = front_line - - @Property(list, notify=extentsChanged) - def extents(self) -> List[LeafletLatLon]: - a = self.front_line.position.point_from_heading( - self.front_line.attack_heading.right.degrees, nautical_miles(2).meters - ).latlng() - b = self.front_line.position.point_from_heading( - self.front_line.attack_heading.left.degrees, nautical_miles(2).meters - ).latlng() - return [a.as_list(), b.as_list()] - - @Slot() - def showPackageDialog(self) -> None: - Dialog.open_new_package_dialog(self.front_line) diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 142da325..33cf0134 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -3,7 +3,7 @@ import traceback import webbrowser from typing import Optional -from PySide2.QtCore import QSettings, Qt +from PySide2.QtCore import QSettings, Qt, Signal from PySide2.QtGui import QCloseEvent, QIcon from PySide2.QtWidgets import ( QAction, @@ -22,7 +22,9 @@ from game import Game, VERSION, persistency from game.debriefing import Debriefing from game.layout import LAYOUTS from game.server import EventStream, GameContext +from game.server.dependencies import QtCallbacks, QtContext from game.server.security import ApiKeyManager +from game.theater import MissionTarget from qt_ui import liberation_install from qt_ui.dialogs import Dialog from qt_ui.models import GameModel @@ -46,6 +48,8 @@ from qt_ui.windows.stats.QStatsWindow import QStatsWindow class QLiberationWindow(QMainWindow): + new_package_signal = Signal(MissionTarget) + def __init__(self, game: Optional[Game], new_map: bool) -> None: super().__init__() @@ -56,6 +60,12 @@ class QLiberationWindow(QMainWindow): self.sim_controller.sim_update.connect(EventStream.put_nowait) self.game_model = GameModel(game, self.sim_controller) GameContext.set_model(self.game_model) + self.new_package_signal.connect( + lambda target: Dialog.open_new_package_dialog(target, self) + ) + QtContext.set_callbacks( + QtCallbacks(lambda target: self.new_package_signal.emit(target)) + ) Dialog.set_game(self.game_model) self.ato_panel = QAirTaskingOrderPanel(self.game_model) self.info_panel = QInfoPanel(self.game) diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index 9cc55945..b6d7b343 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -124,10 +124,10 @@ class QBaseMenu2(QDialog): return self.game_model.game.settings.enable_base_capture_cheat def cheat_capture(self) -> None: - self.cp.capture(self.game_model.game, for_player=not self.cp.captured) + events = GameUpdateEvents() + self.cp.capture(self.game_model.game, events, for_player=not self.cp.captured) # Reinitialized ground planners and the like. The ATO needs to be reset because # missions planned against the flipped base are no longer valid. - events = GameUpdateEvents() self.game_model.game.initialize_turn(events) EventStream.put_nowait(events) GameUpdateSignal.get_instance().updateGame(self.game_model.game) diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py index 18c55d07..cda39d20 100644 --- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py +++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py @@ -57,8 +57,10 @@ class QGroundForcesStrategy(QGroupBox): amount *= -1 self.cp.base.affect_strength(amount) enemy_point.base.affect_strength(-amount) + front_line = self.cp.front_line_with(enemy_point) + front_line.update_position() + events = GameUpdateEvents().update_front_line(front_line) # Clear the ATO to replan missions affected by the front line. - events = GameUpdateEvents() self.game.initialize_turn(events) EventStream.put_nowait(events) GameUpdateSignal.get_instance().updateGame(self.game) diff --git a/resources/ui/map/map.js b/resources/ui/map/map.js index e46abfe3..475d9a33 100644 --- a/resources/ui/map/map.js +++ b/resources/ui/map/map.js @@ -308,6 +308,18 @@ function handleStreamedEvents(events) { for (const flightId of events.deleted_flights) { Flight.popId(flightId).clear(); } + + for (const frontLine of events.new_front_lines) { + new FrontLine(frontLine).draw(); + } + + for (const id of events.updated_front_lines) { + FrontLine.withId(id).update(); + } + + for (const id of events.deleted_front_lines) { + FrontLine.popId(id).clear(); + } } function recenterMap(center) { @@ -622,15 +634,64 @@ function drawSupplyRoutes() { }); } +class FrontLine { + static registered = {}; + + constructor(frontLine) { + this.id = frontLine.id; + this.extents = frontLine.extents; + this.line = null; + FrontLine.register(this); + } + + static register(frontLine) { + FrontLine.registered[frontLine.id] = frontLine; + } + + static unregister(id) { + delete FrontLine.registered[id]; + } + + static withId(id) { + return FrontLine.registered[id]; + } + + static popId(id) { + const front = FrontLine.withId(id); + FrontLine.unregister(id); + return front; + } + + update() { + getJson(`/front-lines/${this.id}`).then((frontLine) => { + this.extents = frontLine.extents; + this.draw(); + }); + } + + clear() { + if (this.line != null) { + this.line.removeFrom(frontLinesLayer); + } + } + + draw() { + this.clear(); + this.line = L.polyline(this.extents, { weight: 8, color: "#fe7d0a" }) + .on("contextmenu", () => this.openNewPackageDialog()) + .addTo(frontLinesLayer); + } + + openNewPackageDialog() { + postJson(`/package-dialog/front-line/${this.id}`); + } +} + function drawFrontLines() { frontLinesLayer.clearLayers(); getJson("/front-lines").then((frontLines) => { for (const front of frontLines) { - L.polyline(front.extents, { weight: 8, color: "#fe7d0a" }) - .on("contextmenu", function () { - front.showPackageDialog(); - }) - .addTo(frontLinesLayer); + new FrontLine(front).draw(); } }); }