Move frozen combat handling out of MapModel.

This commit is contained in:
Dan Albert 2022-02-19 13:50:59 -08:00
parent 2168143fea
commit 09457d8aab
17 changed files with 140 additions and 136 deletions

View File

@ -4,6 +4,7 @@ import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, List, Optional, TYPE_CHECKING from typing import Any, List, Optional, TYPE_CHECKING
from dcs import Point
from dcs.planes import C_101CC, C_101EB, Su_33 from dcs.planes import C_101CC, C_101EB, Su_33
from gen.flights.loadouts import Loadout from gen.flights.loadouts import Loadout
@ -125,6 +126,9 @@ class Flight:
def points(self) -> List[FlightWaypoint]: def points(self) -> List[FlightWaypoint]:
return self.flight_plan.waypoints[1:] return self.flight_plan.waypoints[1:]
def position(self) -> Point:
return self.state.estimate_position()
def resize(self, new_size: int) -> None: def resize(self, new_size: int) -> None:
self.squadron.claim_inventory(new_size - self.count) self.squadron.claim_inventory(new_size - self.count)
self.roster.resize(new_size) self.roster.resize(new_size)

View File

@ -0,0 +1,10 @@
from abc import ABC
from dcs import Point
from game.ato.flightstate import FlightState
class AtDeparture(FlightState, ABC):
def estimate_position(self) -> Point:
return self.flight.departure.position

View File

@ -3,6 +3,8 @@ from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dcs import Point
from .flightstate import FlightState from .flightstate import FlightState
from ..starttype import StartType from ..starttype import StartType
@ -20,6 +22,9 @@ class Completed(FlightState):
def is_waiting_for_start(self) -> bool: def is_waiting_for_start(self) -> bool:
return False return False
def estimate_position(self) -> Point:
return self.flight.arrival.position
@property @property
def spawn_type(self) -> StartType: def spawn_type(self) -> StartType:
# TODO: May want to do something different to make these uncontrolled? # TODO: May want to do something different to make these uncontrolled?

View File

@ -4,6 +4,8 @@ from abc import ABC, abstractmethod
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from dcs import Point
from game.ato.starttype import StartType from game.ato.starttype import StartType
if TYPE_CHECKING: if TYPE_CHECKING:
@ -44,6 +46,10 @@ class FlightState(ABC):
def is_waiting_for_start(self) -> bool: def is_waiting_for_start(self) -> bool:
... ...
@abstractmethod
def estimate_position(self) -> Point:
...
@property @property
@abstractmethod @abstractmethod
def spawn_type(self) -> StartType: def spawn_type(self) -> StartType:

View File

@ -4,7 +4,7 @@ import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .flightstate import FlightState from .atdeparture import AtDeparture
from .taxi import Taxi from .taxi import Taxi
from ..starttype import StartType from ..starttype import StartType
@ -14,7 +14,7 @@ if TYPE_CHECKING:
from game.sim.gameupdateevents import GameUpdateEvents from game.sim.gameupdateevents import GameUpdateEvents
class StartUp(FlightState): class StartUp(AtDeparture):
def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None: def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None:
super().__init__(flight, settings) super().__init__(flight, settings)
self.completion_time = now + flight.flight_plan.estimate_startup() self.completion_time = now + flight.flight_plan.estimate_startup()

View File

@ -4,7 +4,7 @@ import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .flightstate import FlightState from .atdeparture import AtDeparture
from .navigating import Navigating from .navigating import Navigating
from ..starttype import StartType from ..starttype import StartType
from ...utils import LBS_TO_KG from ...utils import LBS_TO_KG
@ -15,7 +15,7 @@ if TYPE_CHECKING:
from game.sim.gameupdateevents import GameUpdateEvents from game.sim.gameupdateevents import GameUpdateEvents
class Takeoff(FlightState): class Takeoff(AtDeparture):
def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None: def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None:
super().__init__(flight, settings) super().__init__(flight, settings)
# TODO: Not accounted for in FlightPlan, can cause discrepancy without loiter. # TODO: Not accounted for in FlightPlan, can cause discrepancy without loiter.

View File

@ -4,7 +4,7 @@ import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .flightstate import FlightState from .atdeparture import AtDeparture
from .takeoff import Takeoff from .takeoff import Takeoff
from ..starttype import StartType from ..starttype import StartType
@ -14,7 +14,7 @@ if TYPE_CHECKING:
from game.sim.gameupdateevents import GameUpdateEvents from game.sim.gameupdateevents import GameUpdateEvents
class Taxi(FlightState): class Taxi(AtDeparture):
def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None: def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None:
super().__init__(flight, settings) super().__init__(flight, settings)
self.completion_time = now + flight.flight_plan.estimate_ground_ops() self.completion_time = now + flight.flight_plan.estimate_ground_ops()

View File

@ -3,6 +3,8 @@ from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dcs import Point
from gen.flights.traveltime import TotEstimator from gen.flights.traveltime import TotEstimator
from .flightstate import FlightState from .flightstate import FlightState
from ..starttype import StartType from ..starttype import StartType
@ -21,6 +23,9 @@ class Uninitialized(FlightState):
def is_waiting_for_start(self) -> bool: def is_waiting_for_start(self) -> bool:
raise RuntimeError("Attempted to simulate flight that is not fully initialized") raise RuntimeError("Attempted to simulate flight that is not fully initialized")
def estimate_position(self) -> Point:
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
@property @property
def spawn_type(self) -> StartType: def spawn_type(self) -> StartType:
raise RuntimeError("Attempted to simulate flight that is not fully initialized") raise RuntimeError("Attempted to simulate flight that is not fully initialized")

View File

@ -4,6 +4,7 @@ from datetime import datetime, timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from game.ato.starttype import StartType from game.ato.starttype import StartType
from .atdeparture import AtDeparture
from .flightstate import FlightState from .flightstate import FlightState
from .navigating import Navigating from .navigating import Navigating
from .startup import StartUp from .startup import StartUp
@ -16,7 +17,7 @@ if TYPE_CHECKING:
from game.sim.gameupdateevents import GameUpdateEvents from game.sim.gameupdateevents import GameUpdateEvents
class WaitingForStart(FlightState): class WaitingForStart(AtDeparture):
def __init__( def __init__(
self, self,
flight: Flight, flight: Flight,

View File

View File

@ -0,0 +1,49 @@
from __future__ import annotations
from uuid import UUID
from pydantic import BaseModel
from game.server.leaflet import LeafletLatLon, LeafletPoly, ShapelyUtil
from game.sim.combat import FrozenCombat
from game.sim.combat.aircombat import AirCombat
from game.sim.combat.atip import AtIp
from game.sim.combat.defendingsam import DefendingSam
from game.theater import ConflictTheater
class FrozenCombatJs(BaseModel):
id: UUID
flight_position: LeafletLatLon | None
target_positions: list[LeafletLatLon] | None
footprint: list[LeafletPoly] | None
@staticmethod
def for_combat(combat: FrozenCombat, theater: ConflictTheater) -> FrozenCombatJs:
if isinstance(combat, AirCombat):
return FrozenCombatJs(
id=combat.id,
flight_position=None,
target_positions=None,
footprint=ShapelyUtil.polys_to_leaflet(combat.footprint, theater),
)
if isinstance(combat, AtIp):
return FrozenCombatJs(
id=combat.id,
flight_position=theater.point_to_ll(combat.flight.position()).as_list(),
target_positions=[
theater.point_to_ll(combat.flight.package.target.position).as_list()
],
footprint=None,
)
if isinstance(combat, DefendingSam):
return FrozenCombatJs(
id=combat.id,
flight_position=theater.point_to_ll(combat.flight.position()).as_list(),
target_positions=[
theater.point_to_ll(sam.position).as_list()
for sam in combat.air_defenses
],
footprint=None,
)
raise NotImplementedError(f"Unhandled FrozenCombat type: {combat.__class__}")

View File

@ -5,6 +5,7 @@ from uuid import UUID
from pydantic import BaseModel from pydantic import BaseModel
from game.server.combat.models import FrozenCombatJs
from game.server.leaflet import LeafletLatLon from game.server.leaflet import LeafletLatLon
if TYPE_CHECKING: if TYPE_CHECKING:
@ -14,6 +15,8 @@ if TYPE_CHECKING:
class GameUpdateEventsJs(BaseModel): class GameUpdateEventsJs(BaseModel):
updated_flights: dict[UUID, LeafletLatLon] updated_flights: dict[UUID, LeafletLatLon]
new_combats: list[FrozenCombatJs] = []
updated_combats: list[FrozenCombatJs] = []
@classmethod @classmethod
def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs: def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs:
@ -21,5 +24,12 @@ class GameUpdateEventsJs(BaseModel):
updated_flights={ updated_flights={
f[0].id: game.theater.point_to_ll(f[1]).as_list() f[0].id: game.theater.point_to_ll(f[1]).as_list()
for f in events.updated_flights for f in events.updated_flights
} },
new_combats=[
FrozenCombatJs.for_combat(c, game.theater) for c in events.new_combats
],
updated_combats=[
FrozenCombatJs.for_combat(c, game.theater)
for c in events.updated_combats
],
) )

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import uuid
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Iterator from collections.abc import Iterator
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -11,6 +12,9 @@ if TYPE_CHECKING:
class FrozenCombat(ABC): class FrozenCombat(ABC):
def __init__(self) -> None:
self.id = uuid.uuid4()
@abstractmethod @abstractmethod
def because(self) -> str: def because(self) -> str:
... ...

View File

@ -16,7 +16,6 @@ from PySide2.QtWebEngineWidgets import (
from game import Game from game import Game
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.simcontroller import SimController
from .model import MapModel from .model import MapModel
@ -37,13 +36,11 @@ class LoggingWebPage(QWebEnginePage):
class QLiberationMap(QWebEngineView): class QLiberationMap(QWebEngineView):
def __init__( def __init__(self, game_model: GameModel, parent) -> None:
self, game_model: GameModel, sim_controller: SimController, parent
) -> None:
super().__init__(parent) super().__init__(parent)
self.game_model = game_model self.game_model = game_model
self.setMinimumSize(800, 600) self.setMinimumSize(800, 600)
self.map_model = MapModel(game_model, sim_controller) self.map_model = MapModel(game_model)
self.channel = QWebChannel() self.channel = QWebChannel()
self.channel.registerObject("game", self.map_model) self.channel.registerObject("game", self.map_model)

View File

@ -10,26 +10,17 @@ from game import Game
from game.ato.airtaaskingorder import AirTaskingOrder from game.ato.airtaaskingorder import AirTaskingOrder
from game.profiling import logged_duration from game.profiling import logged_duration
from game.server.leaflet import LeafletLatLon from game.server.leaflet import LeafletLatLon
from game.sim.combat import FrozenCombat
from game.sim.combat.aircombat import AirCombat
from game.sim.combat.atip import AtIp
from game.sim.combat.defendingsam import DefendingSam
from game.sim.gameupdateevents import GameUpdateEvents
from game.theater import ( from game.theater import (
ConflictTheater, ConflictTheater,
) )
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.simcontroller import SimController
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from .aircombatjs import AirCombatJs
from .controlpointjs import ControlPointJs from .controlpointjs import ControlPointJs
from .flightjs import FlightJs from .flightjs import FlightJs
from .frontlinejs import FrontLineJs from .frontlinejs import FrontLineJs
from .groundobjectjs import GroundObjectJs from .groundobjectjs import GroundObjectJs
from .ipcombatjs import IpCombatJs
from .mapzonesjs import MapZonesJs from .mapzonesjs import MapZonesJs
from .navmeshjs import NavMeshJs from .navmeshjs import NavMeshJs
from .samcombatjs import SamCombatJs
from .supplyroutejs import SupplyRouteJs from .supplyroutejs import SupplyRouteJs
from .threatzonecontainerjs import ThreatZoneContainerJs from .threatzonecontainerjs import ThreatZoneContainerJs
from .threatzonesjs import ThreatZonesJs from .threatzonesjs import ThreatZonesJs
@ -70,7 +61,7 @@ class MapModel(QObject):
ipCombatsChanged = Signal() ipCombatsChanged = Signal()
selectedFlightChanged = Signal(str) selectedFlightChanged = Signal(str)
def __init__(self, game_model: GameModel, sim_controller: SimController) -> None: def __init__(self, game_model: GameModel) -> None:
super().__init__() super().__init__()
self.game_model = game_model self.game_model = game_model
self._map_center = [0, 0] self._map_center = [0, 0]
@ -86,9 +77,6 @@ class MapModel(QObject):
self._map_zones = MapZonesJs([], [], []) self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = [] self._unculled_zones = []
self._selected_flight_index: Optional[Tuple[int, int]] = None self._selected_flight_index: Optional[Tuple[int, int]] = None
self._air_combats = []
self._sam_combats = []
self._ip_combats = []
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load) GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos) GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
@ -104,7 +92,6 @@ class MapModel(QObject):
self.game_model.ato_model_for(False).packages_changed.connect( self.game_model.ato_model_for(False).packages_changed.connect(
self.on_package_change self.on_package_change
), ),
sim_controller.sim_update.connect(self.on_sim_update)
self.reset() self.reset()
def clear(self) -> None: def clear(self) -> None:
@ -119,17 +106,8 @@ class MapModel(QObject):
self._navmeshes = NavMeshJs([], []) self._navmeshes = NavMeshJs([], [])
self._map_zones = MapZonesJs([], [], []) self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = [] self._unculled_zones = []
self._air_combats = []
self._sam_combats = []
self._ip_combats = []
self.cleared.emit() self.cleared.emit()
def on_sim_update(self, events: GameUpdateEvents) -> None:
for combat in events.new_combats:
self.on_add_combat(combat)
for combat in events.updated_combats:
self.on_combat_changed(combat)
def set_package_selection(self, index: int) -> None: def set_package_selection(self, index: int) -> None:
self.deselect_current_flight() self.deselect_current_flight()
# Optional[int] isn't a valid type for a Qt signal. None will be converted to # Optional[int] isn't a valid type for a Qt signal. None will be converted to
@ -199,7 +177,6 @@ class MapModel(QObject):
self.reset_navmeshes() self.reset_navmeshes()
self.reset_map_zones() self.reset_map_zones()
self.reset_unculled_zones() self.reset_unculled_zones()
self.reset_combats()
def on_game_load(self, game: Optional[Game]) -> None: def on_game_load(self, game: Optional[Game]) -> None:
if game is not None: if game is not None:
@ -364,79 +341,6 @@ class MapModel(QObject):
def unculledZones(self) -> list[UnculledZone]: def unculledZones(self) -> list[UnculledZone]:
return self._unculled_zones return self._unculled_zones
def reset_combats(self) -> None:
self._air_combats = []
self._sam_combats = []
self._ip_combats = []
self.airCombatsChanged.emit()
self.samCombatsChanged.emit()
self.ipCombatsChanged.emit()
def on_add_combat(self, combat: FrozenCombat) -> None:
if isinstance(combat, AirCombat):
self.add_air_combat(combat)
elif isinstance(combat, DefendingSam):
self.add_sam_combat(combat)
elif isinstance(combat, AtIp):
self.add_ip_combat(combat)
else:
logging.error(f"Unhandled FrozenCombat type: {combat.__class__}")
def add_air_combat(self, combat: AirCombat) -> None:
self._air_combats.append(AirCombatJs(combat, self.game.theater))
self.airCombatsChanged.emit()
def add_sam_combat(self, combat: DefendingSam) -> None:
self._sam_combats.append(SamCombatJs(combat, self.game_model))
self.samCombatsChanged.emit()
def add_ip_combat(self, combat: AtIp) -> None:
self._ip_combats.append(IpCombatJs(combat, self.game_model))
self.ipCombatsChanged.emit()
def on_combat_changed(self, combat: FrozenCombat) -> None:
if isinstance(combat, AirCombat):
self.refresh_air_combat(combat)
elif isinstance(combat, DefendingSam):
self.refresh_sam_combat(combat)
elif isinstance(combat, AtIp):
self.refresh_ip_combat(combat)
else:
logging.error(f"Unhandled FrozenCombat type: {combat.__class__}")
def refresh_air_combat(self, combat: AirCombat) -> None:
for js in self._air_combats:
if js.combat == combat:
js.refresh()
return
logging.error(f"Could not find existing combat model to update for {combat}")
def refresh_sam_combat(self, combat: DefendingSam) -> None:
for js in self._sam_combats:
if js.combat == combat:
js.refresh()
return
logging.error(f"Could not find existing combat model to update for {combat}")
def refresh_ip_combat(self, combat: AtIp) -> None:
for js in self._ip_combats:
if js.combat == combat:
js.refresh()
return
logging.error(f"Could not find existing combat model to update for {combat}")
@Property(list, notify=airCombatsChanged)
def airCombats(self) -> list[AirCombatJs]:
return self._air_combats
@Property(list, notify=samCombatsChanged)
def samCombats(self) -> list[SamCombatJs]:
return self._sam_combats
@Property(list, notify=ipCombatsChanged)
def ipCombats(self) -> list[IpCombatJs]:
return self._ip_combats
@property @property
def game(self) -> Game: def game(self) -> Game:
if self.game_model.game is None: if self.game_model.game is None:

View File

@ -56,7 +56,7 @@ class QLiberationWindow(QMainWindow):
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)
self.liberation_map = QLiberationMap(self.game_model, self.sim_controller, self) self.liberation_map = QLiberationMap(self.game_model, self)
self.setGeometry(300, 100, 270, 100) self.setGeometry(300, 100, 270, 100)
self.updateWindowTitle() self.updateWindowTitle()

View File

@ -368,9 +368,6 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
game.navmeshesChanged.connect(drawNavmeshes); game.navmeshesChanged.connect(drawNavmeshes);
game.mapZonesChanged.connect(drawMapZones); game.mapZonesChanged.connect(drawMapZones);
game.unculledZonesChanged.connect(drawUnculledZones); game.unculledZonesChanged.connect(drawUnculledZones);
game.airCombatsChanged.connect(drawCombat);
game.samCombatsChanged.connect(drawCombat);
game.ipCombatsChanged.connect(drawCombat);
game.selectedFlightChanged.connect(updateSelectedFlight); game.selectedFlightChanged.connect(updateSelectedFlight);
}); });
@ -378,6 +375,12 @@ function handleStreamedEvents(events) {
for (const [flightId, position] of Object.entries(events.updated_flights)) { for (const [flightId, position] of Object.entries(events.updated_flights)) {
Flight.withId(flightId).drawAircraftLocation(position); Flight.withId(flightId).drawAircraftLocation(position);
} }
for (const combat of events.new_combats) {
redrawCombat(combat);
}
for (const combat of events.updated_combats) {
redrawCombat(combat);
}
} }
function recenterMap(center) { function recenterMap(center) {
@ -1290,32 +1293,39 @@ function drawHoldZones(id) {
}); });
} }
function drawCombat() { var COMBATS = {};
combatLayer.clearLayers();
for (const airCombat of game.airCombats) { function redrawCombat(combat) {
L.polygon(airCombat.footprint, { if (combat.id in COMBATS) {
color: Colors.Red, for (layer in COMBATS[combat.id]) {
interactive: false, layer.removeFrom(combatLayer);
fillOpacity: 0.2,
}).addTo(combatLayer);
}
for (const samCombat of game.samCombats) {
for (const airDefense of samCombat.airDefenses) {
L.polyline([samCombat.flight.position, airDefense.position], {
color: Colors.Red,
interactive: false,
}).addTo(combatLayer);
} }
} }
for (const ipCombat of game.ipCombats) { const layers = [];
L.polyline([ipCombat.flight.position, ipCombat.flight.target], {
color: Colors.Red, if (combat.footprint) {
interactive: false, layers.push(
}).addTo(combatLayer); L.polygon(airCombat.footprint, {
color: Colors.Red,
interactive: false,
fillOpacity: 0.2,
}).addTo(combatLayer)
);
} }
if (combat.flight_position) {
for (target_position of combat.target_positions) {
layers.push(
L.polyline([combat.flight_position, target_position], {
color: Colors.Red,
interactive: false,
}).addTo(combatLayer)
);
}
}
COMBATS[combat.id] = layers;
} }
function drawInitialMap() { function drawInitialMap() {
@ -1329,7 +1339,6 @@ function drawInitialMap() {
drawNavmeshes(); drawNavmeshes();
drawMapZones(); drawMapZones();
drawUnculledZones(); drawUnculledZones();
drawCombat();
} }
function clearAllLayers() { function clearAllLayers() {