mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Move frozen combat handling out of MapModel.
This commit is contained in:
parent
2168143fea
commit
09457d8aab
@ -4,6 +4,7 @@ import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, List, Optional, TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
from dcs.planes import C_101CC, C_101EB, Su_33
|
||||
|
||||
from gen.flights.loadouts import Loadout
|
||||
@ -125,6 +126,9 @@ class Flight:
|
||||
def points(self) -> List[FlightWaypoint]:
|
||||
return self.flight_plan.waypoints[1:]
|
||||
|
||||
def position(self) -> Point:
|
||||
return self.state.estimate_position()
|
||||
|
||||
def resize(self, new_size: int) -> None:
|
||||
self.squadron.claim_inventory(new_size - self.count)
|
||||
self.roster.resize(new_size)
|
||||
|
||||
10
game/ato/flightstate/atdeparture.py
Normal file
10
game/ato/flightstate/atdeparture.py
Normal 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
|
||||
@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from .flightstate import FlightState
|
||||
from ..starttype import StartType
|
||||
|
||||
@ -20,6 +22,9 @@ class Completed(FlightState):
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
return False
|
||||
|
||||
def estimate_position(self) -> Point:
|
||||
return self.flight.arrival.position
|
||||
|
||||
@property
|
||||
def spawn_type(self) -> StartType:
|
||||
# TODO: May want to do something different to make these uncontrolled?
|
||||
|
||||
@ -4,6 +4,8 @@ from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from game.ato.starttype import StartType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -44,6 +46,10 @@ class FlightState(ABC):
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def estimate_position(self) -> Point:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def spawn_type(self) -> StartType:
|
||||
|
||||
@ -4,7 +4,7 @@ import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .flightstate import FlightState
|
||||
from .atdeparture import AtDeparture
|
||||
from .taxi import Taxi
|
||||
from ..starttype import StartType
|
||||
|
||||
@ -14,7 +14,7 @@ if TYPE_CHECKING:
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class StartUp(FlightState):
|
||||
class StartUp(AtDeparture):
|
||||
def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None:
|
||||
super().__init__(flight, settings)
|
||||
self.completion_time = now + flight.flight_plan.estimate_startup()
|
||||
|
||||
@ -4,7 +4,7 @@ import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .flightstate import FlightState
|
||||
from .atdeparture import AtDeparture
|
||||
from .navigating import Navigating
|
||||
from ..starttype import StartType
|
||||
from ...utils import LBS_TO_KG
|
||||
@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class Takeoff(FlightState):
|
||||
class Takeoff(AtDeparture):
|
||||
def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None:
|
||||
super().__init__(flight, settings)
|
||||
# TODO: Not accounted for in FlightPlan, can cause discrepancy without loiter.
|
||||
|
||||
@ -4,7 +4,7 @@ import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .flightstate import FlightState
|
||||
from .atdeparture import AtDeparture
|
||||
from .takeoff import Takeoff
|
||||
from ..starttype import StartType
|
||||
|
||||
@ -14,7 +14,7 @@ if TYPE_CHECKING:
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class Taxi(FlightState):
|
||||
class Taxi(AtDeparture):
|
||||
def __init__(self, flight: Flight, settings: Settings, now: datetime) -> None:
|
||||
super().__init__(flight, settings)
|
||||
self.completion_time = now + flight.flight_plan.estimate_ground_ops()
|
||||
|
||||
@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from .flightstate import FlightState
|
||||
from ..starttype import StartType
|
||||
@ -21,6 +23,9 @@ class Uninitialized(FlightState):
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
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
|
||||
def spawn_type(self) -> StartType:
|
||||
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
|
||||
|
||||
@ -4,6 +4,7 @@ from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.ato.starttype import StartType
|
||||
from .atdeparture import AtDeparture
|
||||
from .flightstate import FlightState
|
||||
from .navigating import Navigating
|
||||
from .startup import StartUp
|
||||
@ -16,7 +17,7 @@ if TYPE_CHECKING:
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class WaitingForStart(FlightState):
|
||||
class WaitingForStart(AtDeparture):
|
||||
def __init__(
|
||||
self,
|
||||
flight: Flight,
|
||||
|
||||
0
game/server/combat/__init__.py
Normal file
0
game/server/combat/__init__.py
Normal file
49
game/server/combat/models.py
Normal file
49
game/server/combat/models.py
Normal 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__}")
|
||||
@ -5,6 +5,7 @@ from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from game.server.combat.models import FrozenCombatJs
|
||||
from game.server.leaflet import LeafletLatLon
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -14,6 +15,8 @@ if TYPE_CHECKING:
|
||||
|
||||
class GameUpdateEventsJs(BaseModel):
|
||||
updated_flights: dict[UUID, LeafletLatLon]
|
||||
new_combats: list[FrozenCombatJs] = []
|
||||
updated_combats: list[FrozenCombatJs] = []
|
||||
|
||||
@classmethod
|
||||
def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs:
|
||||
@ -21,5 +24,12 @@ class GameUpdateEventsJs(BaseModel):
|
||||
updated_flights={
|
||||
f[0].id: game.theater.point_to_ll(f[1]).as_list()
|
||||
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
|
||||
],
|
||||
)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterator
|
||||
from typing import TYPE_CHECKING
|
||||
@ -11,6 +12,9 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class FrozenCombat(ABC):
|
||||
def __init__(self) -> None:
|
||||
self.id = uuid.uuid4()
|
||||
|
||||
@abstractmethod
|
||||
def because(self) -> str:
|
||||
...
|
||||
|
||||
@ -16,7 +16,6 @@ from PySide2.QtWebEngineWidgets import (
|
||||
|
||||
from game import Game
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.simcontroller import SimController
|
||||
from .model import MapModel
|
||||
|
||||
|
||||
@ -37,13 +36,11 @@ class LoggingWebPage(QWebEnginePage):
|
||||
|
||||
|
||||
class QLiberationMap(QWebEngineView):
|
||||
def __init__(
|
||||
self, game_model: GameModel, sim_controller: SimController, parent
|
||||
) -> None:
|
||||
def __init__(self, game_model: GameModel, parent) -> None:
|
||||
super().__init__(parent)
|
||||
self.game_model = game_model
|
||||
self.setMinimumSize(800, 600)
|
||||
self.map_model = MapModel(game_model, sim_controller)
|
||||
self.map_model = MapModel(game_model)
|
||||
|
||||
self.channel = QWebChannel()
|
||||
self.channel.registerObject("game", self.map_model)
|
||||
|
||||
@ -10,26 +10,17 @@ from game import Game
|
||||
from game.ato.airtaaskingorder import AirTaskingOrder
|
||||
from game.profiling import logged_duration
|
||||
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 (
|
||||
ConflictTheater,
|
||||
)
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.simcontroller import SimController
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from .aircombatjs import AirCombatJs
|
||||
from .controlpointjs import ControlPointJs
|
||||
from .flightjs import FlightJs
|
||||
from .frontlinejs import FrontLineJs
|
||||
from .groundobjectjs import GroundObjectJs
|
||||
from .ipcombatjs import IpCombatJs
|
||||
from .mapzonesjs import MapZonesJs
|
||||
from .navmeshjs import NavMeshJs
|
||||
from .samcombatjs import SamCombatJs
|
||||
from .supplyroutejs import SupplyRouteJs
|
||||
from .threatzonecontainerjs import ThreatZoneContainerJs
|
||||
from .threatzonesjs import ThreatZonesJs
|
||||
@ -70,7 +61,7 @@ class MapModel(QObject):
|
||||
ipCombatsChanged = Signal()
|
||||
selectedFlightChanged = Signal(str)
|
||||
|
||||
def __init__(self, game_model: GameModel, sim_controller: SimController) -> None:
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
self.game_model = game_model
|
||||
self._map_center = [0, 0]
|
||||
@ -86,9 +77,6 @@ class MapModel(QObject):
|
||||
self._map_zones = MapZonesJs([], [], [])
|
||||
self._unculled_zones = []
|
||||
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().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.on_package_change
|
||||
),
|
||||
sim_controller.sim_update.connect(self.on_sim_update)
|
||||
self.reset()
|
||||
|
||||
def clear(self) -> None:
|
||||
@ -119,17 +106,8 @@ class MapModel(QObject):
|
||||
self._navmeshes = NavMeshJs([], [])
|
||||
self._map_zones = MapZonesJs([], [], [])
|
||||
self._unculled_zones = []
|
||||
self._air_combats = []
|
||||
self._sam_combats = []
|
||||
self._ip_combats = []
|
||||
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:
|
||||
self.deselect_current_flight()
|
||||
# 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_map_zones()
|
||||
self.reset_unculled_zones()
|
||||
self.reset_combats()
|
||||
|
||||
def on_game_load(self, game: Optional[Game]) -> None:
|
||||
if game is not None:
|
||||
@ -364,79 +341,6 @@ class MapModel(QObject):
|
||||
def unculledZones(self) -> list[UnculledZone]:
|
||||
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
|
||||
def game(self) -> Game:
|
||||
if self.game_model.game is None:
|
||||
|
||||
@ -56,7 +56,7 @@ class QLiberationWindow(QMainWindow):
|
||||
Dialog.set_game(self.game_model)
|
||||
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
|
||||
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.updateWindowTitle()
|
||||
|
||||
@ -368,9 +368,6 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
|
||||
game.navmeshesChanged.connect(drawNavmeshes);
|
||||
game.mapZonesChanged.connect(drawMapZones);
|
||||
game.unculledZonesChanged.connect(drawUnculledZones);
|
||||
game.airCombatsChanged.connect(drawCombat);
|
||||
game.samCombatsChanged.connect(drawCombat);
|
||||
game.ipCombatsChanged.connect(drawCombat);
|
||||
game.selectedFlightChanged.connect(updateSelectedFlight);
|
||||
});
|
||||
|
||||
@ -378,6 +375,12 @@ function handleStreamedEvents(events) {
|
||||
for (const [flightId, position] of Object.entries(events.updated_flights)) {
|
||||
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) {
|
||||
@ -1290,32 +1293,39 @@ function drawHoldZones(id) {
|
||||
});
|
||||
}
|
||||
|
||||
function drawCombat() {
|
||||
combatLayer.clearLayers();
|
||||
var COMBATS = {};
|
||||
|
||||
for (const airCombat of game.airCombats) {
|
||||
function redrawCombat(combat) {
|
||||
if (combat.id in COMBATS) {
|
||||
for (layer in COMBATS[combat.id]) {
|
||||
layer.removeFrom(combatLayer);
|
||||
}
|
||||
}
|
||||
|
||||
const layers = [];
|
||||
|
||||
if (combat.footprint) {
|
||||
layers.push(
|
||||
L.polygon(airCombat.footprint, {
|
||||
color: Colors.Red,
|
||||
interactive: false,
|
||||
fillOpacity: 0.2,
|
||||
}).addTo(combatLayer);
|
||||
}).addTo(combatLayer)
|
||||
);
|
||||
}
|
||||
|
||||
for (const samCombat of game.samCombats) {
|
||||
for (const airDefense of samCombat.airDefenses) {
|
||||
L.polyline([samCombat.flight.position, airDefense.position], {
|
||||
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);
|
||||
}).addTo(combatLayer)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const ipCombat of game.ipCombats) {
|
||||
L.polyline([ipCombat.flight.position, ipCombat.flight.target], {
|
||||
color: Colors.Red,
|
||||
interactive: false,
|
||||
}).addTo(combatLayer);
|
||||
}
|
||||
COMBATS[combat.id] = layers;
|
||||
}
|
||||
|
||||
function drawInitialMap() {
|
||||
@ -1329,7 +1339,6 @@ function drawInitialMap() {
|
||||
drawNavmeshes();
|
||||
drawMapZones();
|
||||
drawUnculledZones();
|
||||
drawCombat();
|
||||
}
|
||||
|
||||
function clearAllLayers() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user