diff --git a/game/debriefing.py b/game/debriefing.py index 3e6b4689..1ccafaca 100644 --- a/game/debriefing.py +++ b/game/debriefing.py @@ -22,7 +22,14 @@ from dcs.unittype import FlyingType, UnitType from game import db from game.theater import Airfield, ControlPoint -from game.unitmap import Building, ConvoyUnit, FrontLineUnit, GroundObjectUnit, UnitMap +from game.unitmap import ( + AirliftUnit, + Building, + ConvoyUnit, + FrontLineUnit, + GroundObjectUnit, + UnitMap, +) from gen.flights.flight import Flight if TYPE_CHECKING: @@ -63,6 +70,9 @@ class GroundLosses: player_convoy: List[ConvoyUnit] = field(default_factory=list) enemy_convoy: List[ConvoyUnit] = field(default_factory=list) + player_airlifts: List[AirliftUnit] = field(default_factory=list) + enemy_airlifts: List[AirliftUnit] = field(default_factory=list) + player_ground_objects: List[GroundObjectUnit] = field(default_factory=list) enemy_ground_objects: List[GroundObjectUnit] = field(default_factory=list) @@ -128,6 +138,11 @@ class Debriefing: yield from self.ground_losses.player_convoy yield from self.ground_losses.enemy_convoy + @property + def airlift_losses(self) -> Iterator[AirliftUnit]: + yield from self.ground_losses.player_airlifts + yield from self.ground_losses.enemy_airlifts + @property def ground_object_losses(self) -> Iterator[GroundObjectUnit]: yield from self.ground_losses.player_ground_objects @@ -166,6 +181,16 @@ class Debriefing: losses_by_type[loss.unit_type] += 1 return losses_by_type + def airlift_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]: + losses_by_type: Dict[Type[UnitType], int] = defaultdict(int) + if player: + losses = self.ground_losses.player_airlifts + else: + losses = self.ground_losses.enemy_airlifts + for loss in losses: + losses_by_type[loss.unit_type] += 1 + return losses_by_type + def building_losses_by_type(self, player: bool) -> Dict[str, int]: losses_by_type: Dict[str, int] = defaultdict(int) if player: @@ -250,6 +275,15 @@ class Debriefing: "have no effect. This may be normal behavior." ) + for unit_name in self.state_data.killed_aircraft: + airlift_unit = self.unit_map.airlift_unit(unit_name) + if airlift_unit is not None: + if airlift_unit.transfer.player: + losses.player_airlifts.append(airlift_unit) + else: + losses.enemy_airlifts.append(airlift_unit) + continue + return losses @property diff --git a/game/event/event.py b/game/event/event.py index c2b77aa2..beebd54a 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -172,6 +172,23 @@ class Event: logging.info(f"{unit_type} destroyed in {convoy_name}") convoy.kill_unit(unit_type) + @staticmethod + def commit_airlift_losses(debriefing: Debriefing) -> None: + for loss in debriefing.airlift_losses: + unit_type = loss.unit_type + transfer = loss.transfer + available = loss.transfer.units.get(unit_type, 0) + airlift_name = f"airlift from {transfer.origin} to {transfer.destination}" + if available <= 0: + logging.error( + f"Found killed {unit_type} in {airlift_name} but that airlift has " + "none available." + ) + continue + + logging.info(f"{unit_type} destroyed in {airlift_name}") + transfer.kill_unit(unit_type) + @staticmethod def commit_ground_object_losses(debriefing: Debriefing) -> None: for loss in debriefing.ground_object_losses: @@ -205,6 +222,7 @@ class Event: self.commit_air_losses(debriefing) self.commit_front_line_losses(debriefing) self.commit_convoy_losses(debriefing) + self.commit_airlift_losses(debriefing) self.commit_ground_object_losses(debriefing) self.commit_building_losses(debriefing) self.commit_damaged_runways(debriefing) diff --git a/game/transfers.py b/game/transfers.py index 64e2f830..0819baf1 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -78,6 +78,17 @@ class AirliftOrder(TransferOrder): def description(self) -> str: return "Airlift" + def iter_units(self) -> Iterator[Type[VehicleType]]: + for unit_type, count in self.units.items(): + for _ in range(count): + yield unit_type + + def kill_unit(self, unit_type: Type[VehicleType]) -> None: + if unit_type in self.units: + self.units[unit_type] -= 1 + return + raise KeyError + class Convoy(MissionTarget): def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None: diff --git a/game/unitmap.py b/game/unitmap.py index 520e80f9..322320b7 100644 --- a/game/unitmap.py +++ b/game/unitmap.py @@ -9,7 +9,7 @@ from dcs.unittype import VehicleType from game import db from game.theater import Airfield, ControlPoint, TheaterGroundObject from game.theater.theatergroundobject import BuildingGroundObject -from game.transfers import Convoy, RoadTransferOrder +from game.transfers import AirliftOrder, Convoy from gen.flights.flight import Flight @@ -32,6 +32,12 @@ class ConvoyUnit: convoy: Convoy +@dataclass(frozen=True) +class AirliftUnit: + unit_type: Type[VehicleType] + transfer: AirliftOrder + + @dataclass(frozen=True) class Building: ground_object: BuildingGroundObject @@ -45,6 +51,7 @@ class UnitMap: self.ground_object_units: Dict[str, GroundObjectUnit] = {} self.buildings: Dict[str, Building] = {} self.convoys: Dict[str, ConvoyUnit] = {} + self.airlifts: Dict[str, AirliftUnit] = {} def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None: for unit in group.units: @@ -54,6 +61,8 @@ class UnitMap: if name in self.aircraft: raise RuntimeError(f"Duplicate unit name: {name}") self.aircraft[name] = flight + if flight.cargo is not None: + self.add_airlift_units(group, flight.cargo) def flight(self, unit_name: str) -> Optional[Flight]: return self.aircraft.get(unit_name, None) @@ -140,6 +149,21 @@ class UnitMap: def convoy_unit(self, name: str) -> Optional[ConvoyUnit]: return self.convoys.get(name, None) + def add_airlift_units(self, group: FlyingGroup, airlift: AirliftOrder) -> None: + for transport, cargo_type in zip(group.units, airlift.iter_units()): + # The actual name is a String (the pydcs translatable string), which + # doesn't define __eq__. + name = str(transport.name) + if name in self.airlifts: + raise RuntimeError(f"Duplicate airlift unit: {name}") + unit_type = db.unit_type_from_name(transport.type) + if unit_type is None: + raise RuntimeError(f"Unknown unit type: {transport.type}") + self.airlifts[name] = AirliftUnit(cargo_type, airlift) + + def airlift_unit(self, name: str) -> Optional[AirliftUnit]: + return self.airlifts.get(name, None) + def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None: # The actual name is a String (the pydcs translatable string), which # doesn't define __eq__. diff --git a/qt_ui/windows/QDebriefingWindow.py b/qt_ui/windows/QDebriefingWindow.py index 4b62c397..7308b0e6 100644 --- a/qt_ui/windows/QDebriefingWindow.py +++ b/qt_ui/windows/QDebriefingWindow.py @@ -83,6 +83,17 @@ class QDebriefingWindow(QDialog): except AttributeError: logging.exception(f"Issue adding {unit_type} to debriefing information") + airlift_losses = self.debriefing.airlift_losses_by_type(player=True) + for unit_type, count in airlift_losses.items(): + try: + lostUnitsLayout.addWidget( + QLabel(f"{db.unit_type_name(unit_type)} from airlift"), row, 0 + ) + lostUnitsLayout.addWidget(QLabel(str(count)), row, 1) + row += 1 + except AttributeError: + logging.exception(f"Issue adding {unit_type} to debriefing information") + building_losses = self.debriefing.building_losses_by_type(player=True) for building, count in building_losses.items(): try: @@ -135,6 +146,17 @@ class QDebriefingWindow(QDialog): except AttributeError: logging.exception(f"Issue adding {unit_type} to debriefing information") + airlift_losses = self.debriefing.airlift_losses_by_type(player=False) + for unit_type, count in airlift_losses.items(): + try: + lostUnitsLayout.addWidget( + QLabel(f"{db.unit_type_name(unit_type)} from airlift"), row, 0 + ) + lostUnitsLayout.addWidget(QLabel(str(count)), row, 1) + row += 1 + except AttributeError: + logging.exception(f"Issue adding {unit_type} to debriefing information") + building_losses = self.debriefing.building_losses_by_type(player=False) for building, count in building_losses.items(): try: diff --git a/qt_ui/windows/QWaitingForMissionResultWindow.py b/qt_ui/windows/QWaitingForMissionResultWindow.py index 3b06e877..bd7f6163 100644 --- a/qt_ui/windows/QWaitingForMissionResultWindow.py +++ b/qt_ui/windows/QWaitingForMissionResultWindow.py @@ -151,16 +151,19 @@ class QWaitingForMissionResultWindow(QDialog): updateLayout.addWidget(QLabel("Convoy units destroyed"), 2, 0) updateLayout.addWidget(QLabel(str(len(list(debriefing.convoy_losses)))), 2, 1) - updateLayout.addWidget(QLabel("Other ground units destroyed"), 3, 0) + updateLayout.addWidget(QLabel("Airlift cargo destroyed"), 3, 0) + updateLayout.addWidget(QLabel(str(len(list(debriefing.airlift_losses)))), 3, 1) + + updateLayout.addWidget(QLabel("Other ground units destroyed"), 4, 0) updateLayout.addWidget( - QLabel(str(len(list(debriefing.ground_object_losses)))), 3, 1 + QLabel(str(len(list(debriefing.ground_object_losses)))), 4, 1 ) - updateLayout.addWidget(QLabel("Buildings destroyed"), 4, 0) - updateLayout.addWidget(QLabel(str(len(list(debriefing.building_losses)))), 4, 1) + updateLayout.addWidget(QLabel("Buildings destroyed"), 5, 0) + updateLayout.addWidget(QLabel(str(len(list(debriefing.building_losses)))), 5, 1) - updateLayout.addWidget(QLabel("Base Capture Events"), 5, 0) - updateLayout.addWidget(QLabel(str(len(debriefing.base_capture_events))), 5, 1) + updateLayout.addWidget(QLabel("Base Capture Events"), 6, 0) + updateLayout.addWidget(QLabel(str(len(debriefing.base_capture_events))), 6, 1) # Clear previous content of the window for i in reversed(range(self.gridLayout.count())):