diff --git a/game/debriefing.py b/game/debriefing.py index 857c4143..88c9f8ae 100644 --- a/game/debriefing.py +++ b/game/debriefing.py @@ -24,7 +24,7 @@ from game import db from game.theater import Airfield, ControlPoint from game.transfers import CargoShip from game.unitmap import ( - AirliftUnit, + AirliftUnits, Building, ConvoyUnit, FrontLineUnit, @@ -75,8 +75,8 @@ class GroundLosses: player_cargo_ships: List[CargoShip] = field(default_factory=list) enemy_cargo_ships: List[CargoShip] = field(default_factory=list) - player_airlifts: List[AirliftUnit] = field(default_factory=list) - enemy_airlifts: List[AirliftUnit] = field(default_factory=list) + player_airlifts: List[AirliftUnits] = field(default_factory=list) + enemy_airlifts: List[AirliftUnits] = field(default_factory=list) player_ground_objects: List[GroundObjectUnit] = field(default_factory=list) enemy_ground_objects: List[GroundObjectUnit] = field(default_factory=list) @@ -160,7 +160,7 @@ class Debriefing: yield from self.ground_losses.enemy_cargo_ships @property - def airlift_losses(self) -> Iterator[AirliftUnit]: + def airlift_losses(self) -> Iterator[AirliftUnits]: yield from self.ground_losses.player_airlifts yield from self.ground_losses.enemy_airlifts @@ -220,7 +220,8 @@ class Debriefing: else: losses = self.ground_losses.enemy_airlifts for loss in losses: - losses_by_type[loss.unit_type] += 1 + for unit_type in loss.cargo: + losses_by_type[unit_type] += 1 return losses_by_type def building_losses_by_type(self, player: bool) -> Dict[str, int]: diff --git a/game/event/event.py b/game/event/event.py index 7498a5cf..ae1c3a4a 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -202,19 +202,17 @@ class Event: @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) + for unit_type in loss.cargo: + try: + transfer.kill_unit(unit_type) + logging.info(f"{unit_type} destroyed in {airlift_name}") + except KeyError: + logging.exception( + f"Found killed {unit_type} in {airlift_name} but that airlift " + "has none available." + ) @staticmethod def commit_ground_object_losses(debriefing: Debriefing) -> None: diff --git a/game/transfers.py b/game/transfers.py index ec623265..988c1e84 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import math from collections import defaultdict from dataclasses import dataclass, field from functools import singledispatchmethod @@ -89,10 +90,9 @@ class TransferOrder: self.units.clear() def kill_unit(self, unit_type: Type[VehicleType]) -> None: - if unit_type in self.units: - self.units[unit_type] -= 1 - return - raise KeyError + if unit_type not in self.units or not self.units[unit_type]: + raise KeyError(f"{self.destination} has no {unit_type} remaining") + self.units[unit_type] -= 1 @property def size(self) -> int: @@ -254,11 +254,13 @@ class AirliftPlanner: self, squadron: Squadron, inventory: ControlPointAircraftInventory ) -> int: available = inventory.available(squadron.aircraft) - # 4 is the max flight size in DCS. - flight_size = min(self.transfer.size, available, 4) + capacity_each = 1 if squadron.aircraft.helicopter else 2 + required = math.ceil(self.transfer.size / capacity_each) + flight_size = min(required, available, squadron.aircraft.group_size_max) + capacity = flight_size * capacity_each - if flight_size < self.transfer.size: - transfer = self.game.transfers.split_transfer(self.transfer, flight_size) + if capacity < self.transfer.size: + transfer = self.game.transfers.split_transfer(self.transfer, capacity) else: transfer = self.transfer diff --git a/game/unitmap.py b/game/unitmap.py index bcf457ca..c1778091 100644 --- a/game/unitmap.py +++ b/game/unitmap.py @@ -1,4 +1,6 @@ """Maps generated units back to their Liberation types.""" +import itertools +import math from dataclasses import dataclass from typing import Dict, Optional, Type @@ -40,8 +42,8 @@ class ConvoyUnit: @dataclass(frozen=True) -class AirliftUnit: - unit_type: Type[VehicleType] +class AirliftUnits: + cargo: tuple[Type[VehicleType], ...] transfer: TransferOrder @@ -59,7 +61,7 @@ class UnitMap: self.buildings: Dict[str, Building] = {} self.convoys: Dict[str, ConvoyUnit] = {} self.cargo_ships: Dict[str, CargoShip] = {} - self.airlifts: Dict[str, AirliftUnit] = {} + self.airlifts: Dict[str, AirliftUnits] = {} def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None: for pilot, unit in zip(flight.roster.pilots, group.units): @@ -177,15 +179,26 @@ class UnitMap: return self.cargo_ships.get(name, None) def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> None: - for transport, cargo_type in zip(group.units, transfer.iter_units()): + capacity_each = math.ceil(transfer.size / len(group.units)) + for idx, transport in enumerate(group.units): + # Slice the units in groups based on the capacity of each unit. Cargo is + # assigned arbitrarily to units in the order of the group. The last unit in + # the group will receive a partial load if there is not enough cargo to fill + # every transport. + base_idx = idx * capacity_each + cargo = tuple( + itertools.islice( + transfer.iter_units(), base_idx, base_idx + capacity_each + ) + ) # 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}") - self.airlifts[name] = AirliftUnit(cargo_type, transfer) + self.airlifts[name] = AirliftUnits(cargo, transfer) - def airlift_unit(self, name: str) -> Optional[AirliftUnit]: + def airlift_unit(self, name: str) -> Optional[AirliftUnits]: return self.airlifts.get(name, None) def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None: diff --git a/qt_ui/windows/QWaitingForMissionResultWindow.py b/qt_ui/windows/QWaitingForMissionResultWindow.py index b4addd4d..f4ce44fb 100644 --- a/qt_ui/windows/QWaitingForMissionResultWindow.py +++ b/qt_ui/windows/QWaitingForMissionResultWindow.py @@ -133,10 +133,10 @@ class QWaitingForMissionResultWindow(QDialog): self.setLayout(self.layout) @staticmethod - def add_update_row(description: str, count: Sized, layout: QGridLayout) -> None: + def add_update_row(description: str, count: int, layout: QGridLayout) -> None: row = layout.rowCount() layout.addWidget(QLabel(f"{description}"), row, 0) - layout.addWidget(QLabel(f"{len(count)}"), row, 1) + layout.addWidget(QLabel(f"{count}"), row, 1) def updateLayout(self, debriefing: Debriefing) -> None: updateBox = QGroupBox("Mission status") @@ -145,34 +145,36 @@ class QWaitingForMissionResultWindow(QDialog): self.debriefing = debriefing self.add_update_row( - "Aircraft destroyed", list(debriefing.air_losses.losses), update_layout + "Aircraft destroyed", len(list(debriefing.air_losses.losses)), update_layout ) self.add_update_row( "Front line units destroyed", - list(debriefing.front_line_losses), + len(list(debriefing.front_line_losses)), update_layout, ) self.add_update_row( - "Convoy units destroyed", list(debriefing.convoy_losses), update_layout + "Convoy units destroyed", len(list(debriefing.convoy_losses)), update_layout ) self.add_update_row( "Shipping cargo destroyed", - list(debriefing.cargo_ship_losses), + len(list(debriefing.cargo_ship_losses)), update_layout, ) self.add_update_row( - "Airlift cargo destroyed", list(debriefing.airlift_losses), update_layout + "Airlift cargo destroyed", + sum(len(loss.cargo) for loss in debriefing.airlift_losses), + update_layout, ) self.add_update_row( "Ground units lost at objective areas", - list(debriefing.ground_object_losses), + len(list(debriefing.ground_object_losses)), update_layout, ) self.add_update_row( - "Buildings destroyed", list(debriefing.building_losses), update_layout + "Buildings destroyed", len(list(debriefing.building_losses)), update_layout ) self.add_update_row( - "Base capture events", debriefing.base_captures, update_layout + "Base capture events", len(debriefing.base_captures), update_layout ) # Clear previous content of the window diff --git a/resources/campaigns/inherent_resolve.miz b/resources/campaigns/inherent_resolve.miz index 3c16bace..d7d34c19 100644 Binary files a/resources/campaigns/inherent_resolve.miz and b/resources/campaigns/inherent_resolve.miz differ