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())):