mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Draw frozen combat on the map.
Very basic display. Draws the engagement footprint for air-to-air combat, a line from the flight to the target for IP, and lines from SAMs to their target for air defense. https://github.com/dcs-liberation/dcs_liberation/issues/1680
This commit is contained in:
parent
fb10a8d28e
commit
515efd0598
@ -18,14 +18,16 @@ from game.ato.flightstate import (
|
||||
from game.ato.starttype import StartType
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from .combat import CombatInitiator, FrozenCombat
|
||||
from .gameupdatecallbacks import GameUpdateCallbacks
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
class AircraftSimulation:
|
||||
def __init__(self, game: Game) -> None:
|
||||
def __init__(self, game: Game, callbacks: GameUpdateCallbacks) -> None:
|
||||
self.game = game
|
||||
self.callbacks = callbacks
|
||||
self.combats: list[FrozenCombat] = []
|
||||
|
||||
def begin_simulation(self) -> None:
|
||||
@ -38,7 +40,7 @@ class AircraftSimulation:
|
||||
|
||||
# Finish updating all flights before checking for combat so that the new
|
||||
# positions are used.
|
||||
CombatInitiator(self.game, self.combats).update_active_combats()
|
||||
CombatInitiator(self.game, self.combats, self.callbacks).update_active_combats()
|
||||
|
||||
# After updating all combat states, check for halts.
|
||||
for flight in self.iter_flights():
|
||||
|
||||
@ -12,6 +12,7 @@ from .atip import AtIp
|
||||
from .defendingsam import DefendingSam
|
||||
from .joinablecombat import JoinableCombat
|
||||
from .samengagementzones import SamEngagementZones
|
||||
from ..gameupdatecallbacks import GameUpdateCallbacks
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -20,9 +21,12 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class CombatInitiator:
|
||||
def __init__(self, game: Game, combats: list[FrozenCombat]) -> None:
|
||||
def __init__(
|
||||
self, game: Game, combats: list[FrozenCombat], callbacks: GameUpdateCallbacks
|
||||
) -> None:
|
||||
self.game = game
|
||||
self.combats = combats
|
||||
self.callbacks = callbacks
|
||||
|
||||
def update_active_combats(self) -> None:
|
||||
blue_a2a = AircraftEngagementZones.from_ato(self.game.blue.ato)
|
||||
@ -60,6 +64,7 @@ class CombatInitiator:
|
||||
logging.info(f"{flight} is joining existing combat {joined}")
|
||||
joined.join(flight)
|
||||
own_a2a.remove_flight(flight)
|
||||
self.callbacks.on_combat_changed(joined)
|
||||
elif (combat := self.check_flight_for_new_combat(flight, a2a, sam)) is not None:
|
||||
logging.info(f"Interrupting simulation because {combat.because()}")
|
||||
combat.update_flight_states()
|
||||
@ -70,6 +75,7 @@ class CombatInitiator:
|
||||
a2a.update_for_combat(combat)
|
||||
own_a2a.update_for_combat(combat)
|
||||
self.combats.append(combat)
|
||||
self.callbacks.on_add_combat(combat)
|
||||
|
||||
def check_flight_for_joined_combat(
|
||||
self, flight: Flight
|
||||
|
||||
@ -3,9 +3,10 @@ from __future__ import annotations
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Callable, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .gamelooptimer import GameLoopTimer
|
||||
from .gameupdatecallbacks import GameUpdateCallbacks
|
||||
from .missionsimulation import MissionSimulation, SimulationAlreadyCompletedError
|
||||
from .simspeedsetting import SimSpeedSetting
|
||||
|
||||
@ -15,11 +16,11 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class GameLoop:
|
||||
def __init__(self, game: Game, on_complete: Callable[[], None]) -> None:
|
||||
def __init__(self, game: Game, callbacks: GameUpdateCallbacks) -> None:
|
||||
self.game = game
|
||||
self.on_complete = on_complete
|
||||
self.callbacks = callbacks
|
||||
self.timer = GameLoopTimer(self.tick)
|
||||
self.sim = MissionSimulation(self.game)
|
||||
self.sim = MissionSimulation(self.game, self.callbacks)
|
||||
self.started = False
|
||||
self.completed = False
|
||||
|
||||
@ -75,6 +76,6 @@ class GameLoop:
|
||||
if self.completed:
|
||||
self.pause()
|
||||
logging.info(f"Simulation completed at {self.sim.time}")
|
||||
self.on_complete()
|
||||
self.callbacks.on_simulation_complete()
|
||||
except SimulationAlreadyCompletedError:
|
||||
logging.exception("Attempted to tick already completed sim")
|
||||
|
||||
17
game/sim/gameupdatecallbacks.py
Normal file
17
game/sim/gameupdatecallbacks.py
Normal file
@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.sim.combat import FrozenCombat
|
||||
|
||||
|
||||
# Ought to be frozen but mypy can't handle that:
|
||||
# https://github.com/python/mypy/issues/5485
|
||||
@dataclass
|
||||
class GameUpdateCallbacks:
|
||||
on_simulation_complete: Callable[[], None]
|
||||
on_add_combat: Callable[[FrozenCombat], None]
|
||||
on_combat_changed: Callable[[FrozenCombat], None]
|
||||
@ -9,6 +9,7 @@ from game.debriefing import Debriefing
|
||||
from game.missiongenerator import MissionGenerator
|
||||
from game.unitmap import UnitMap
|
||||
from .aircraftsimulation import AircraftSimulation
|
||||
from .gameupdatecallbacks import GameUpdateCallbacks
|
||||
from .missionresultsprocessor import MissionResultsProcessor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -24,10 +25,10 @@ class SimulationAlreadyCompletedError(RuntimeError):
|
||||
|
||||
|
||||
class MissionSimulation:
|
||||
def __init__(self, game: Game) -> None:
|
||||
def __init__(self, game: Game, callbacks: GameUpdateCallbacks) -> None:
|
||||
self.game = game
|
||||
self.unit_map: Optional[UnitMap] = None
|
||||
self.aircraft_simulation = AircraftSimulation(self.game)
|
||||
self.aircraft_simulation = AircraftSimulation(self.game, callbacks)
|
||||
self.completed = False
|
||||
self.time = self.game.conditions.start_time
|
||||
|
||||
|
||||
@ -8,7 +8,9 @@ from typing import Callable, Optional, TYPE_CHECKING
|
||||
from PySide2.QtCore import QObject, Signal
|
||||
|
||||
from game.polldebriefingfilethread import PollDebriefingFileThread
|
||||
from game.sim.combat import FrozenCombat
|
||||
from game.sim.gameloop import GameLoop
|
||||
from game.sim.gameupdatecallbacks import GameUpdateCallbacks
|
||||
from game.sim.simspeedsetting import SimSpeedSetting
|
||||
from qt_ui.simupdatethread import SimUpdateThread
|
||||
|
||||
@ -21,6 +23,8 @@ class SimController(QObject):
|
||||
sim_update = Signal()
|
||||
sim_speed_reset = Signal(SimSpeedSetting)
|
||||
simulation_complete = Signal()
|
||||
on_add_combat = Signal(FrozenCombat)
|
||||
on_combat_changed = Signal(FrozenCombat)
|
||||
|
||||
def __init__(self, game: Optional[Game]) -> None:
|
||||
super().__init__()
|
||||
@ -59,7 +63,14 @@ class SimController(QObject):
|
||||
self.game_loop.pause()
|
||||
self.game_loop = None
|
||||
if game is not None:
|
||||
self.game_loop = GameLoop(game, self.on_simulation_complete)
|
||||
self.game_loop = GameLoop(
|
||||
game,
|
||||
GameUpdateCallbacks(
|
||||
self.on_simulation_complete,
|
||||
self.on_add_combat.emit,
|
||||
self.on_combat_changed.emit,
|
||||
),
|
||||
)
|
||||
self.started = False
|
||||
|
||||
def set_simulation_speed(self, simulation_speed: SimSpeedSetting) -> None:
|
||||
|
||||
27
qt_ui/widgets/map/model/aircombatjs.py
Normal file
27
qt_ui/widgets/map/model/aircombatjs.py
Normal file
@ -0,0 +1,27 @@
|
||||
from PySide2.QtCore import Property, QObject, Signal
|
||||
|
||||
from game.sim.combat.aircombat import AirCombat
|
||||
from game.theater import ConflictTheater
|
||||
from .leaflet import LeafletPoly
|
||||
from .shapelyutil import ShapelyUtil
|
||||
|
||||
|
||||
class AirCombatJs(QObject):
|
||||
footprintChanged = Signal()
|
||||
|
||||
def __init__(self, combat: AirCombat, theater: ConflictTheater) -> None:
|
||||
super().__init__()
|
||||
self.combat = combat
|
||||
self.theater = theater
|
||||
self._footprint = self._make_footprint()
|
||||
|
||||
@Property(type=list, notify=footprintChanged)
|
||||
def footprint(self) -> list[LeafletPoly]:
|
||||
return self._footprint
|
||||
|
||||
def refresh(self) -> None:
|
||||
self._footprint = self._make_footprint()
|
||||
self.footprintChanged.emit()
|
||||
|
||||
def _make_footprint(self) -> list[LeafletPoly]:
|
||||
return ShapelyUtil.polys_to_leaflet(self.combat.footprint, self.theater)
|
||||
28
qt_ui/widgets/map/model/ipcombatjs.py
Normal file
28
qt_ui/widgets/map/model/ipcombatjs.py
Normal file
@ -0,0 +1,28 @@
|
||||
from PySide2.QtCore import Property, QObject, Signal
|
||||
|
||||
from game.sim.combat.atip import AtIp
|
||||
from qt_ui.models import GameModel
|
||||
from .flightjs import FlightJs
|
||||
|
||||
|
||||
class IpCombatJs(QObject):
|
||||
flightChanged = Signal()
|
||||
|
||||
def __init__(self, combat: AtIp, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
assert game_model.game is not None
|
||||
self.combat = combat
|
||||
self.theater = game_model.game.theater
|
||||
self._flight = FlightJs(
|
||||
combat.flight,
|
||||
selected=False,
|
||||
theater=game_model.game.theater,
|
||||
ato_model=game_model.ato_model_for(combat.flight.squadron.player),
|
||||
)
|
||||
|
||||
@Property(FlightJs, notify=flightChanged)
|
||||
def flight(self) -> FlightJs:
|
||||
return self._flight
|
||||
|
||||
def refresh(self) -> None:
|
||||
pass
|
||||
@ -9,22 +9,29 @@ from dcs import Point
|
||||
from game import Game
|
||||
from game.ato.airtaaskingorder import AirTaskingOrder
|
||||
from game.profiling import logged_duration
|
||||
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,
|
||||
)
|
||||
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 .holdzonesjs import HoldZonesJs
|
||||
from .ipcombatjs import IpCombatJs
|
||||
from .ipzonesjs import IpZonesJs
|
||||
from .joinzonesjs import JoinZonesJs
|
||||
from .leaflet import LeafletLatLon
|
||||
from .mapzonesjs import MapZonesJs
|
||||
from .navmeshjs import NavMeshJs
|
||||
from .samcombatjs import SamCombatJs
|
||||
from .supplyroutejs import SupplyRouteJs
|
||||
from .threatzonecontainerjs import ThreatZoneContainerJs
|
||||
from .threatzonesjs import ThreatZonesJs
|
||||
@ -63,6 +70,9 @@ class MapModel(QObject):
|
||||
ipZonesChanged = Signal()
|
||||
joinZonesChanged = Signal()
|
||||
holdZonesChanged = Signal()
|
||||
airCombatsChanged = Signal()
|
||||
samCombatsChanged = Signal()
|
||||
ipCombatsChanged = Signal()
|
||||
|
||||
def __init__(self, game_model: GameModel, sim_controller: SimController) -> None:
|
||||
super().__init__()
|
||||
@ -83,6 +93,10 @@ class MapModel(QObject):
|
||||
self._join_zones = JoinZonesJs.empty()
|
||||
self._hold_zones = HoldZonesJs.empty()
|
||||
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)
|
||||
GameUpdateSignal.get_instance().package_selection_changed.connect(
|
||||
@ -92,6 +106,8 @@ class MapModel(QObject):
|
||||
self.set_flight_selection
|
||||
)
|
||||
sim_controller.sim_update.connect(self.on_sim_update)
|
||||
sim_controller.on_add_combat.connect(self.on_add_combat)
|
||||
sim_controller.on_combat_changed.connect(self.on_combat_changed)
|
||||
self.reset()
|
||||
|
||||
def clear(self) -> None:
|
||||
@ -107,6 +123,9 @@ class MapModel(QObject):
|
||||
self._map_zones = MapZonesJs([], [], [])
|
||||
self._unculled_zones = []
|
||||
self._ip_zones = IpZonesJs.empty()
|
||||
self._air_combats = []
|
||||
self._sam_combats = []
|
||||
self._ip_combats = []
|
||||
self.cleared.emit()
|
||||
|
||||
def on_sim_update(self) -> None:
|
||||
@ -180,6 +199,7 @@ 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:
|
||||
@ -367,6 +387,79 @@ class MapModel(QObject):
|
||||
def holdZones(self) -> HoldZonesJs:
|
||||
return self._hold_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:
|
||||
|
||||
37
qt_ui/widgets/map/model/samcombatjs.py
Normal file
37
qt_ui/widgets/map/model/samcombatjs.py
Normal file
@ -0,0 +1,37 @@
|
||||
from PySide2.QtCore import Property, QObject, Signal
|
||||
|
||||
from game.sim.combat.defendingsam import DefendingSam
|
||||
from qt_ui.models import GameModel
|
||||
from .flightjs import FlightJs
|
||||
from .groundobjectjs import GroundObjectJs
|
||||
|
||||
|
||||
class SamCombatJs(QObject):
|
||||
flightChanged = Signal()
|
||||
airDefensesChanged = Signal()
|
||||
|
||||
def __init__(self, combat: DefendingSam, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
assert game_model.game is not None
|
||||
self.combat = combat
|
||||
self.theater = game_model.game.theater
|
||||
self._flight = FlightJs(
|
||||
combat.flight,
|
||||
selected=False,
|
||||
theater=game_model.game.theater,
|
||||
ato_model=game_model.ato_model_for(combat.flight.squadron.player),
|
||||
)
|
||||
self._air_defenses = [
|
||||
GroundObjectJs(tgo, game_model.game) for tgo in self.combat.air_defenses
|
||||
]
|
||||
|
||||
@Property(FlightJs, notify=flightChanged)
|
||||
def flight(self) -> FlightJs:
|
||||
return self._flight
|
||||
|
||||
@Property(list, notify=airDefensesChanged)
|
||||
def airDefenses(self) -> list[GroundObjectJs]:
|
||||
return self._air_defenses
|
||||
|
||||
def refresh(self) -> None:
|
||||
pass
|
||||
@ -206,6 +206,7 @@ const supplyRoutesLayer = L.layerGroup().addTo(map);
|
||||
const frontLinesLayer = L.layerGroup().addTo(map);
|
||||
const redSamThreatLayer = L.layerGroup().addTo(map);
|
||||
const blueFlightPlansLayer = L.layerGroup().addTo(map);
|
||||
const combatLayer = L.layerGroup().addTo(map);
|
||||
|
||||
// Added to map by the user via layer controls.
|
||||
const blueSamThreatLayer = L.layerGroup();
|
||||
@ -287,6 +288,7 @@ L.control
|
||||
"Units and locations": {
|
||||
"Control points": controlPointsLayer,
|
||||
Aircraft: aircraftLayer,
|
||||
"Active combat": combatLayer,
|
||||
"Air defenses": airDefensesLayer,
|
||||
Factories: factoriesLayer,
|
||||
Ships: shipsLayer,
|
||||
@ -351,6 +353,9 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
|
||||
game.ipZonesChanged.connect(drawIpZones);
|
||||
game.joinZonesChanged.connect(drawJoinZones);
|
||||
game.holdZonesChanged.connect(drawHoldZones);
|
||||
game.airCombatsChanged.connect(drawCombat);
|
||||
game.samCombatsChanged.connect(drawCombat);
|
||||
game.ipCombatsChanged.connect(drawCombat);
|
||||
});
|
||||
|
||||
function recenterMap(center) {
|
||||
@ -1226,6 +1231,34 @@ function drawHoldZones() {
|
||||
}
|
||||
}
|
||||
|
||||
function drawCombat() {
|
||||
combatLayer.clearLayers();
|
||||
|
||||
for (const airCombat of game.airCombats) {
|
||||
L.polygon(airCombat.footprint, {
|
||||
color: Colors.Red,
|
||||
interactive: false,
|
||||
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) {
|
||||
L.polyline([ipCombat.flight.position, ipCombat.flight.target], {
|
||||
color: Colors.Red,
|
||||
interactive: false,
|
||||
}).addTo(combatLayer);
|
||||
}
|
||||
}
|
||||
|
||||
function drawInitialMap() {
|
||||
recenterMap(game.mapCenter);
|
||||
drawControlPoints();
|
||||
@ -1240,6 +1273,7 @@ function drawInitialMap() {
|
||||
drawIpZones();
|
||||
drawJoinZones();
|
||||
drawHoldZones();
|
||||
drawCombat();
|
||||
}
|
||||
|
||||
function clearAllLayers() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user