Move front lines out of MapModel.

This commit is contained in:
Dan Albert 2022-03-03 01:52:53 -08:00
parent 89b987fc87
commit ccb510fe47
20 changed files with 252 additions and 87 deletions

View File

@ -4,8 +4,10 @@ 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
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()

View File

@ -222,7 +222,7 @@ class Game:
# reset when we're done. # reset when we're done.
self.compute_threat_zones(GameUpdateEvents()) 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. """Finalizes the current turn and advances to the next turn.
This handles the turn-end portion of passing a turn. Initialization of the next This handles the turn-end portion of passing a turn. Initialization of the next
@ -265,6 +265,9 @@ class Game:
if not skipped: if not skipped:
for cp in self.theater.player_points(): 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) cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
self.conditions = self.generate_conditions() self.conditions = self.generate_conditions()
@ -293,11 +296,12 @@ class Game:
from .server import EventStream from .server import EventStream
from .sim import GameUpdateEvents from .sim import GameUpdateEvents
events = GameUpdateEvents()
logging.info("Pass turn") logging.info("Pass turn")
with logged_duration("Turn finalization"): with logged_duration("Turn finalization"):
self.finish_turn(no_action) self.finish_turn(events, no_action)
events = GameUpdateEvents()
with logged_duration("Turn initialization"): with logged_duration("Turn initialization"):
self.initialize_turn(events) self.initialize_turn(events)

View File

@ -9,6 +9,7 @@ from . import (
frontlines, frontlines,
mapzones, mapzones,
navmesh, navmesh,
packagedialog,
supplyroutes, supplyroutes,
tgos, tgos,
waypoints, waypoints,
@ -28,6 +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(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

@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import Callable, TYPE_CHECKING
from game.theater import MissionTarget
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -23,3 +25,22 @@ class GameContext:
@classmethod @classmethod
def get_model(cls) -> GameModel: def get_model(cls) -> GameModel:
return cls._game_model 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

View File

@ -7,6 +7,7 @@ from pydantic import BaseModel
from game.server.combat.models import FrozenCombatJs from game.server.combat.models import FrozenCombatJs
from game.server.flights.models import FlightJs from game.server.flights.models import FlightJs
from game.server.frontlines.models import FrontLineJs
from game.server.leaflet import LeafletLatLon from game.server.leaflet import LeafletLatLon
if TYPE_CHECKING: if TYPE_CHECKING:
@ -27,6 +28,9 @@ class GameUpdateEventsJs(BaseModel):
deleted_flights: set[UUID] deleted_flights: set[UUID]
selected_flight: UUID | None selected_flight: UUID | None
deselected_flight: bool deselected_flight: bool
new_front_lines: list[FrontLineJs]
updated_front_lines: set[UUID]
deleted_front_lines: set[UUID]
@classmethod @classmethod
def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs: def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs:
@ -53,4 +57,9 @@ class GameUpdateEventsJs(BaseModel):
deleted_flights=events.deleted_flights, deleted_flights=events.deleted_flights,
selected_flight=events.selected_flight, selected_flight=events.selected_flight,
deselected_flight=events.deselected_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,
) )

View File

@ -1,9 +1,24 @@
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
from game.theater import FrontLine
from game.utils import nautical_miles
class FrontLineJs(BaseModel): class FrontLineJs(BaseModel):
id: UUID
extents: list[LeafletPoint] 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()])

View File

@ -1,7 +1,8 @@
from uuid import UUID
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from game import Game from game import Game
from game.utils import nautical_miles
from .models import FrontLineJs from .models import FrontLineJs
from ..dependencies import GameContext from ..dependencies import GameContext
@ -10,13 +11,11 @@ router: APIRouter = APIRouter(prefix="/front-lines")
@router.get("/") @router.get("/")
def list_front_lines(game: Game = Depends(GameContext.get)) -> list[FrontLineJs]: def list_front_lines(game: Game = Depends(GameContext.get)) -> list[FrontLineJs]:
front_lines = [] return [FrontLineJs.for_front_line(f) for f in game.theater.conflicts()]
for front_line in game.theater.conflicts():
a = front_line.position.point_from_heading(
front_line.attack_heading.right.degrees, nautical_miles(2).meters @router.get("/{front_line_id}")
) def get_front_line(
b = front_line.position.point_from_heading( front_line_id: UUID, game: Game = Depends(GameContext.get)
front_line.attack_heading.left.degrees, nautical_miles(2).meters ) -> FrontLineJs:
) return FrontLineJs.for_front_line(game.db.front_lines.get(front_line_id))
front_lines.append(FrontLineJs(extents=[a.latlng(), b.latlng()]))
return front_lines

View File

@ -0,0 +1 @@
from .routes import router

View File

@ -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)

View File

@ -68,8 +68,9 @@ class GameLoop:
def complete_with_results(self, debriefing: Debriefing) -> None: def complete_with_results(self, debriefing: Debriefing) -> None:
self.pause() self.pause()
self.sim.process_results(debriefing) self.sim.process_results(debriefing, self.events)
self.completed = True self.completed = True
self.send_update(rate_limit=False)
def send_update(self, rate_limit: bool) -> None: 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 # We don't skip empty events because we still want the tick in the Qt part of

View File

@ -9,6 +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
@dataclass @dataclass
@ -26,6 +27,9 @@ class GameUpdateEvents:
deleted_flights: set[UUID] = field(default_factory=set) deleted_flights: set[UUID] = field(default_factory=set)
selected_flight: UUID | None = None selected_flight: UUID | None = None
deselected_flight: bool = False 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 shutting_down: bool = False
@property @property
@ -95,6 +99,18 @@ class GameUpdateEvents:
self.selected_flight = None self.selected_flight = None
return self 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: def shut_down(self) -> GameUpdateEvents:
self.shutting_down = True self.shutting_down = True
return self return self

View File

@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
from game.debriefing import Debriefing from game.debriefing import Debriefing
from game.ground_forces.combat_stance import CombatStance from game.ground_forces.combat_stance import CombatStance
from game.theater import ControlPoint from game.theater import ControlPoint
from .gameupdateevents import GameUpdateEvents
from ..ato.airtaaskingorder import AirTaskingOrder from ..ato.airtaaskingorder import AirTaskingOrder
if TYPE_CHECKING: if TYPE_CHECKING:
@ -21,7 +22,7 @@ class MissionResultsProcessor:
def __init__(self, game: Game) -> None: def __init__(self, game: Game) -> None:
self.game = game self.game = game
def commit(self, debriefing: Debriefing) -> None: def commit(self, debriefing: Debriefing, events: GameUpdateEvents) -> None:
logging.info("Committing mission results") logging.info("Committing mission results")
self.commit_air_losses(debriefing) self.commit_air_losses(debriefing)
self.commit_pilot_experience() self.commit_pilot_experience()
@ -31,8 +32,8 @@ class MissionResultsProcessor:
self.commit_airlift_losses(debriefing) self.commit_airlift_losses(debriefing)
self.commit_ground_losses(debriefing) self.commit_ground_losses(debriefing)
self.commit_damaged_runways(debriefing) self.commit_damaged_runways(debriefing)
self.commit_captures(debriefing) self.commit_captures(debriefing, events)
self.commit_front_line_battle_impact(debriefing) self.commit_front_line_battle_impact(debriefing, events)
self.record_carcasses(debriefing) self.record_carcasses(debriefing)
def commit_air_losses(self, debriefing: Debriefing) -> None: def commit_air_losses(self, debriefing: Debriefing) -> None:
@ -141,7 +142,7 @@ class MissionResultsProcessor:
for damaged_runway in debriefing.damaged_runways: for damaged_runway in debriefing.damaged_runways:
damaged_runway.damage_runway() 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: for captured in debriefing.base_captures:
try: try:
if captured.captured_by_player: if captured.captured_by_player:
@ -155,7 +156,9 @@ class MissionResultsProcessor:
f"The enemy took control of {captured.control_point}.", 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}") logging.info(f"Will run redeploy for {captured.control_point}")
self.redeploy_units(captured.control_point) self.redeploy_units(captured.control_point)
except Exception: except Exception:
@ -165,10 +168,16 @@ class MissionResultsProcessor:
for destroyed_unit in debriefing.state_data.destroyed_statics: for destroyed_unit in debriefing.state_data.destroyed_statics:
self.game.add_destroyed_units(destroyed_unit) 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(): for cp in self.game.theater.player_points():
enemy_cps = [e for e in cp.connected_points if not e.captured] enemy_cps = [e for e in cp.connected_points if not e.captured]
for enemy_cp in enemy_cps: 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( print(
"Compute frontline progression for : " "Compute frontline progression for : "
+ cp.name + cp.name

View File

@ -66,14 +66,14 @@ class MissionSimulation:
debriefing.merge_simulation_results(self.aircraft_simulation.results) debriefing.merge_simulation_results(self.aircraft_simulation.results)
return debriefing return debriefing
def process_results(self, debriefing: Debriefing) -> None: def process_results(self, debriefing: Debriefing, events: GameUpdateEvents) -> None:
if self.unit_map is None: if self.unit_map is None:
raise RuntimeError( raise RuntimeError(
"Simulation has no unit map. Results processing began before a mission " "Simulation has no unit map. Results processing began before a mission "
"was generated." "was generated."
) )
MissionResultsProcessor(self.game).commit(debriefing) MissionResultsProcessor(self.game).commit(debriefing, events)
def finish(self) -> None: def finish(self) -> None:
self.unit_map = None self.unit_map = None

View File

@ -52,6 +52,7 @@ from .theatergroundobject import (
from .theatergroup import TheaterUnit from .theatergroup import TheaterUnit
from ..ato.starttype import StartType from ..ato.starttype import StartType
from ..data.units import UnitClass from ..data.units import UnitClass
from ..db import Database
from ..dcs.aircrafttype import AircraftType from ..dcs.aircrafttype import AircraftType
from ..dcs.groundunittype import GroundUnitType from ..dcs.groundunittype import GroundUnitType
from ..utils import nautical_miles from ..utils import nautical_miles
@ -59,10 +60,11 @@ from ..weather import Conditions
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game 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 game.squadrons.squadron import Squadron
from ..coalition import Coalition from game.transfers import PendingTransfers
from ..transfers import PendingTransfers
from .conflicttheater import ConflictTheater from .conflicttheater import ConflictTheater
FREE_FRONTLINE_UNIT_SUPPLY: int = 15 FREE_FRONTLINE_UNIT_SUPPLY: int = 15
@ -326,6 +328,9 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self.target_position: Optional[Point] = None 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: def __repr__(self) -> str:
return f"<{self.__class__}: {self.name}>" return f"<{self.__class__}: {self.name}>"
@ -338,31 +343,50 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
def finish_init(self, game: Game) -> None: def finish_init(self, game: Game) -> None:
assert self._coalition is None assert self._coalition is None
self._coalition = game.coalition_for(self.starts_blue) 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: 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._create_missing_front_lines(GameUpdateEvents())
self._clear_front_lines()
@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(): for connection in self.convoy_routes.keys():
if not connection.front_line_active_with( if not connection.front_line_active_with(
self self
) and not connection.is_friendly_to(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) blue, red = FrontLine.sort_control_points(self, connection)
front = FrontLine(blue, red) front = FrontLine(blue, red)
self.front_lines[connection] = front self.front_lines[connection] = front
connection.front_lines[self] = 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 self.front_lines[connection]
del connection.front_lines[self] 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()): for opponent in list(self.front_lines.keys()):
self._remove_front_line_with(opponent) self._remove_front_line_with(opponent, events)
@property @property
def has_frontline(self) -> bool: def has_frontline(self) -> bool:
@ -713,7 +737,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
tgo.clear() tgo.clear()
# TODO: Should be Airbase specific. # 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) new_coalition = game.coalition_for(for_player)
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)
@ -722,7 +746,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self._coalition = new_coalition self._coalition = new_coalition
self.base.set_strength_to_minimum() self.base.set_strength_to_minimum()
self._recreate_front_lines() self._clear_front_lines(events)
self._create_missing_front_lines(events)
@property @property
def required_aircraft_start_type(self) -> Optional[StartType]: def required_aircraft_start_type(self) -> Optional[StartType]:
@ -1127,7 +1152,7 @@ class Carrier(NavalControlPoint):
FlightType.REFUELING, 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") raise RuntimeError("Carriers cannot be captured")
@property @property
@ -1161,7 +1186,7 @@ class Lha(NavalControlPoint):
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.AMPHIBIOUS_ASSAULT_SHIP_GENERAL 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") raise RuntimeError("LHAs cannot be captured")
@property @property
@ -1198,7 +1223,7 @@ class OffMapSpawn(ControlPoint):
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
return SymbolSet.LAND_INSTALLATIONS, LandInstallationEntity.AIPORT_AIR_BASE 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") raise RuntimeError("Off map control points cannot be captured")
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import uuid
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Iterator, List, TYPE_CHECKING, Tuple from typing import Any, Iterator, List, TYPE_CHECKING, Tuple
@ -49,6 +50,7 @@ class FrontLine(MissionTarget):
blue_point: ControlPoint, blue_point: ControlPoint,
red_point: ControlPoint, red_point: ControlPoint,
) -> None: ) -> None:
self.id = uuid.uuid4()
self.blue_cp = blue_point self.blue_cp = blue_point
self.red_cp = red_point self.red_cp = red_point
try: try:
@ -67,8 +69,7 @@ class FrontLine(MissionTarget):
FrontLineSegment(a, b) for a, b in pairwise(route) FrontLineSegment(a, b) for a, b in pairwise(route)
] ]
super().__init__( super().__init__(
f"Front line {blue_point}/{red_point}", f"Front line {blue_point}/{red_point}", self._compute_position()
self.point_from_a(self._position_distance),
) )
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
@ -79,10 +80,11 @@ class FrontLine(MissionTarget):
def __hash__(self) -> int: def __hash__(self) -> int:
return hash((self.blue_cp, self.red_cp)) return hash((self.blue_cp, self.red_cp))
def __setstate__(self, state: dict[str, Any]) -> None: def _compute_position(self) -> Point:
self.__dict__.update(state) return self.point_from_a(self._position_distance)
if not hasattr(self, "position"):
self.position = 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: def control_point_friendly_to(self, player: bool) -> ControlPoint:
if player: if player:

View File

@ -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)

View File

@ -3,7 +3,7 @@ import traceback
import webbrowser import webbrowser
from typing import Optional 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.QtGui import QCloseEvent, QIcon
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QAction, QAction,
@ -22,7 +22,9 @@ from game import Game, VERSION, persistency
from game.debriefing import Debriefing from game.debriefing import Debriefing
from game.layout import LAYOUTS 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.security import ApiKeyManager from game.server.security import ApiKeyManager
from game.theater import MissionTarget
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
@ -46,6 +48,8 @@ from qt_ui.windows.stats.QStatsWindow import QStatsWindow
class QLiberationWindow(QMainWindow): class QLiberationWindow(QMainWindow):
new_package_signal = Signal(MissionTarget)
def __init__(self, game: Optional[Game], new_map: bool) -> None: def __init__(self, game: Optional[Game], new_map: bool) -> None:
super().__init__() super().__init__()
@ -56,6 +60,12 @@ class QLiberationWindow(QMainWindow):
self.sim_controller.sim_update.connect(EventStream.put_nowait) self.sim_controller.sim_update.connect(EventStream.put_nowait)
self.game_model = GameModel(game, self.sim_controller) self.game_model = GameModel(game, self.sim_controller)
GameContext.set_model(self.game_model) 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) Dialog.set_game(self.game_model)
self.ato_panel = QAirTaskingOrderPanel(self.game_model) self.ato_panel = QAirTaskingOrderPanel(self.game_model)
self.info_panel = QInfoPanel(self.game) self.info_panel = QInfoPanel(self.game)

View File

@ -124,10 +124,10 @@ class QBaseMenu2(QDialog):
return self.game_model.game.settings.enable_base_capture_cheat return self.game_model.game.settings.enable_base_capture_cheat
def cheat_capture(self) -> None: 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 # Reinitialized ground planners and the like. The ATO needs to be reset because
# missions planned against the flipped base are no longer valid. # missions planned against the flipped base are no longer valid.
events = GameUpdateEvents()
self.game_model.game.initialize_turn(events) self.game_model.game.initialize_turn(events)
EventStream.put_nowait(events) EventStream.put_nowait(events)
GameUpdateSignal.get_instance().updateGame(self.game_model.game) GameUpdateSignal.get_instance().updateGame(self.game_model.game)

View File

@ -57,8 +57,10 @@ class QGroundForcesStrategy(QGroupBox):
amount *= -1 amount *= -1
self.cp.base.affect_strength(amount) self.cp.base.affect_strength(amount)
enemy_point.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. # Clear the ATO to replan missions affected by the front line.
events = GameUpdateEvents()
self.game.initialize_turn(events) self.game.initialize_turn(events)
EventStream.put_nowait(events) EventStream.put_nowait(events)
GameUpdateSignal.get_instance().updateGame(self.game) GameUpdateSignal.get_instance().updateGame(self.game)

View File

@ -308,6 +308,18 @@ function handleStreamedEvents(events) {
for (const flightId of events.deleted_flights) { for (const flightId of events.deleted_flights) {
Flight.popId(flightId).clear(); 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) { 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() { function drawFrontLines() {
frontLinesLayer.clearLayers(); frontLinesLayer.clearLayers();
getJson("/front-lines").then((frontLines) => { getJson("/front-lines").then((frontLines) => {
for (const front of frontLines) { for (const front of frontLines) {
L.polyline(front.extents, { weight: 8, color: "#fe7d0a" }) new FrontLine(front).draw();
.on("contextmenu", function () {
front.showPackageDialog();
})
.addTo(frontLinesLayer);
} }
}); });
} }