Refactor transfers to support unfulfilled orders.

This gives a clean break between the transfer request and the type of
transport allocated to make way for transports that need to switch
types (to support driving to a port, then getting on a ship, to a train,
then back on the road, etc).

https://github.com/Khopa/dcs_liberation/issues/823
This commit is contained in:
Dan Albert 2021-04-23 18:40:31 -07:00
parent d6c84e362f
commit d80f7ebf3b
9 changed files with 227 additions and 399 deletions

View File

@ -20,11 +20,14 @@ if TYPE_CHECKING:
from game.inventory import ControlPointAircraftInventory
# TODO: Remove base classes.
# Eventually we'll want multi-mode transfers (convoy from factory to port, onto a ship,
# then airlifted to the final destination, etc). To do this we'll need to make the
# transfer *order* represent the full journey and let classes like Convoy handle the
# individual hops.
class Transport:
def find_escape_route(self) -> Optional[ControlPoint]:
raise NotImplementedError
def description(self) -> str:
raise NotImplementedError
@dataclass
class TransferOrder:
"""The base type of all transfer orders.
@ -38,59 +41,30 @@ class TransferOrder:
#: The location the units are transferring to.
destination: ControlPoint
#: The current position of the group being transferred. Groups may make multiple
#: stops and can switch transport modes before reaching their destination.
position: ControlPoint = field(init=False)
#: True if the transfer order belongs to the player.
player: bool
player: bool = field(init=False)
#: The units being transferred.
units: Dict[Type[VehicleType], int]
@property
def description(self) -> str:
raise NotImplementedError
@dataclass
class RoadTransferOrder(TransferOrder):
"""A transfer order that moves units by road."""
#: The current position of the group being transferred. Groups move one control
#: point a turn through the supply line.
position: ControlPoint = field(init=False)
transport: Optional[Transport] = field(default=None)
def __post_init__(self) -> None:
self.position = self.origin
def path(self) -> List[ControlPoint]:
supply_route = SupplyRoute.for_control_point(self.position)
return supply_route.shortest_path_between(self.position, self.destination)
def next_stop(self) -> ControlPoint:
return self.path()[0]
self.player = self.origin.is_friendly(to_player=True)
@property
def description(self) -> str:
path = self.path()
if len(path) == 1:
turns = "1 turn"
else:
turns = f"{len(path)} turns"
return f"Currently at {self.position}. Arrives at destination in {turns}."
if self.transport is None:
return "No transports available"
return self.transport.description()
@dataclass
class AirliftOrder(TransferOrder):
"""A transfer order that moves units by cargo planes and helicopters."""
flight: Flight
@property
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_all(self) -> None:
self.units.clear()
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
if unit_type in self.units:
@ -98,54 +72,102 @@ class AirliftOrder(TransferOrder):
return
raise KeyError
@property
def size(self) -> int:
return sum(c for c in self.units.values())
def iter_units(self) -> Iterator[Type[VehicleType]]:
for unit_type, count in self.units.items():
for _ in range(count):
yield unit_type
@property
def completed(self) -> bool:
return self.destination == self.position or not self.units
def disband_at(self, location: ControlPoint) -> None:
logging.info(f"Units halting at {location}.")
location.base.commision_units(self.units)
self.units.clear()
def proceed(self) -> None:
if self.transport is None:
return
if not self.destination.is_friendly(self.player):
logging.info(f"Transfer destination {self.destination} was captured.")
if self.position.is_friendly(self.player):
self.disband_at(self.position)
elif (escape_route := self.transport.find_escape_route()) is not None:
self.disband_at(escape_route)
else:
logging.info(
f"No escape route available. Units were surrounded and destroyed "
"during transfer."
)
self.kill_all()
return
self.position = self.destination
self.transport = None
if self.completed:
self.disband_at(self.position)
@dataclass
class Airlift(Transport):
"""A transfer order that moves units by cargo planes and helicopters."""
transfer: TransferOrder
flight: Flight
@property
def units(self) -> Dict[Type[VehicleType], int]:
return self.transfer.units
@property
def player_owned(self) -> bool:
return self.transfer.player
def find_escape_route(self) -> Optional[ControlPoint]:
# TODO: Move units to closest base.
return None
def description(self) -> str:
return f"Being airlifted by {self.flight}"
class AirliftPlanner:
def __init__(
self,
game: Game,
pickup: ControlPoint,
drop_off: ControlPoint,
units: Dict[Type[VehicleType], int],
) -> None:
def __init__(self, game: Game, transfer: TransferOrder) -> None:
self.game = game
self.pickup = pickup
self.drop_off = drop_off
self.units = units
self.for_player = drop_off.captured
self.package = Package(target=drop_off, auto_asap=True)
self.transfer = transfer
self.for_player = transfer.destination.captured
self.package = Package(target=transfer.destination, auto_asap=True)
def create_package_for_airlift(self) -> Dict[Type[VehicleType], int]:
def create_package_for_airlift(self) -> None:
for cp in self.game.theater.player_points():
inventory = self.game.aircraft_inventory.for_control_point(cp)
for unit_type, available in inventory.all_aircraft:
if unit_type.helicopter:
while available and self.needed_capacity:
while available and self.transfer.transport is None:
flight_size = self.create_airlift_flight(unit_type, inventory)
available -= flight_size
self.game.ato_for(self.for_player).add_package(self.package)
return self.units
def take_units(self, count: int) -> Dict[Type[VehicleType], int]:
taken = {}
for unit_type, remaining in self.units.items():
take = min(remaining, count)
count -= take
self.units[unit_type] -= take
taken[unit_type] = take
if not count:
break
return taken
@property
def needed_capacity(self) -> int:
return sum(c for c in self.units.values())
def create_airlift_flight(
self, unit_type: Type[FlyingType], inventory: ControlPointAircraftInventory
) -> int:
available = inventory.available(unit_type)
# 4 is the max flight size in DCS.
flight_size = min(self.needed_capacity, available, 4)
flight_size = min(self.transfer.size, available, 4)
if flight_size < self.transfer.size:
transfer = self.game.transfers.split_transfer(self.transfer, flight_size)
else:
transfer = self.transfer
flight = Flight(
self.package,
self.game.player_country,
@ -156,31 +178,25 @@ class AirliftPlanner:
departure=inventory.control_point,
arrival=inventory.control_point,
divert=None,
cargo=transfer,
)
transfer = AirliftOrder(
player=True,
origin=self.pickup,
destination=self.drop_off,
units=self.take_units(flight_size),
flight=flight,
)
flight.cargo = transfer
transport = Airlift(transfer, flight)
transfer.transport = transport
self.package.add_flight(flight)
planner = FlightPlanBuilder(self.game, self.package, self.for_player)
planner.populate_flight_plan(flight)
self.game.aircraft_inventory.claim_for_flight(flight)
self.game.transfers.new_transfer(transfer)
return flight_size
class Convoy(MissionTarget):
class Convoy(MissionTarget, Transport):
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
super().__init__(namegen.next_convoy_name(), origin.position)
self.origin = origin
self.destination = destination
self.transfers: List[RoadTransferOrder] = []
self.transfers: List[TransferOrder] = []
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
if self.is_friendly(for_player):
@ -192,17 +208,20 @@ class Convoy(MissionTarget):
def is_friendly(self, to_player: bool) -> bool:
return self.origin.captured
def add_units(self, transfer: RoadTransferOrder) -> None:
def add_units(self, transfer: TransferOrder) -> None:
self.transfers.append(transfer)
transfer.transport = self
def remove_units(self, transfer: RoadTransferOrder) -> None:
def remove_units(self, transfer: TransferOrder) -> None:
self.transfers.remove(transfer)
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
for transfer in self.transfers:
if unit_type in transfer.units:
transfer.units[unit_type] -= 1
try:
transfer.kill_unit(unit_type)
return
except KeyError:
pass
raise KeyError
@property
@ -221,6 +240,12 @@ class Convoy(MissionTarget):
def player_owned(self) -> bool:
return self.origin.captured
def find_escape_route(self) -> Optional[ControlPoint]:
return None
def description(self) -> str:
return f"In a convoy to {self.destination}"
class ConvoyMap:
def __init__(self) -> None:
@ -255,18 +280,21 @@ class ConvoyMap:
def disband_convoy(self, convoy: Convoy) -> None:
del self.convoys[convoy.origin][convoy.destination]
def add(self, transfer: RoadTransferOrder) -> None:
next_stop = transfer.next_stop()
@staticmethod
def path_for(transfer: TransferOrder) -> List[ControlPoint]:
supply_route = SupplyRoute.for_control_point(transfer.position)
return supply_route.shortest_path_between(
transfer.position, transfer.destination
)
def next_stop_for(self, transfer: TransferOrder) -> ControlPoint:
return self.path_for(transfer)[0]
def add(self, transfer: TransferOrder) -> None:
next_stop = self.next_stop_for(transfer)
self.find_or_create_convoy(transfer.position, next_stop).add_units(transfer)
def remove(self, transfer: RoadTransferOrder) -> None:
next_stop = transfer.next_stop()
convoy = self.find_convoy(transfer.position, next_stop)
if convoy is None:
logging.error(
f"Attempting to remove {transfer} from convoy but it is in no convoy."
)
return
def remove(self, convoy: Convoy, transfer: TransferOrder) -> None:
convoy.remove_units(transfer)
if not convoy.transfers:
self.disband_convoy(convoy)
@ -298,43 +326,63 @@ class PendingTransfers:
def index_of_transfer(self, transfer: TransferOrder) -> int:
return self.pending_transfers.index(transfer)
# TODO: Move airlift arrangements here?
@singledispatchmethod
def arrange_transport(self, transfer) -> None:
pass
@arrange_transport.register
def _arrange_transport_road(self, transfer: RoadTransferOrder) -> None:
self.convoys.add(transfer)
def arrange_transport(self, transfer: TransferOrder) -> None:
supply_route = SupplyRoute.for_control_point(transfer.position)
if transfer.destination in supply_route:
self.convoys.add(transfer)
else:
AirliftPlanner(self.game, transfer).create_package_for_airlift()
def new_transfer(self, transfer: TransferOrder) -> None:
transfer.origin.base.commit_losses(transfer.units)
self.pending_transfers.append(transfer)
self.arrange_transport(transfer)
def split_transfer(self, transfer: TransferOrder, size: int) -> TransferOrder:
"""Creates a smaller transfer that is a subset of the original."""
if transfer.size <= size:
raise ValueError
units = {}
for unit_type, remaining in transfer.units.items():
take = min(remaining, size)
size -= take
transfer.units[unit_type] -= take
units[unit_type] = take
if not size:
break
new_transfer = TransferOrder(transfer.origin, transfer.destination, units)
self.pending_transfers.append(new_transfer)
return new_transfer
@singledispatchmethod
def cancel_transport(self, transfer) -> None:
def cancel_transport(self, transfer: TransferOrder, transport) -> None:
pass
@cancel_transport.register
def _cancel_transport_air(self, transfer: AirliftOrder) -> None:
flight = transfer.flight
def _cancel_transport_air(
self, _transfer: TransferOrder, transport: Airlift
) -> None:
flight = transport.flight
flight.package.remove_flight(flight)
self.game.aircraft_inventory.return_from_flight(flight)
@cancel_transport.register
def _cancel_transport_road(self, transfer: RoadTransferOrder) -> None:
self.convoys.remove(transfer)
def _cancel_transport_convoy(
self, transfer: TransferOrder, transport: Convoy
) -> None:
self.convoys.remove(transport, transfer)
def cancel_transfer(self, transfer: TransferOrder) -> None:
self.cancel_transport(transfer)
if transfer.transport is not None:
self.cancel_transport(transfer, transfer.transport)
self.pending_transfers.remove(transfer)
transfer.origin.base.commision_units(transfer.units)
def perform_transfers(self) -> None:
incomplete = []
for transfer in self.pending_transfers:
if not self.perform_transfer(transfer):
transfer.proceed()
if not transfer.completed:
incomplete.append(transfer)
self.pending_transfers = incomplete
self.rebuild_convoys()
@ -343,80 +391,3 @@ class PendingTransfers:
self.convoys.disband_all()
for transfer in self.pending_transfers:
self.arrange_transport(transfer)
@singledispatchmethod
def perform_transfer(self, transfer) -> bool:
raise NotImplementedError
@perform_transfer.register
def _perform_transfer_air(self, transfer: AirliftOrder) -> bool:
if transfer.player != transfer.destination.captured:
logging.info(
f"Transfer destination {transfer.destination} was captured. Cancelling "
"transport."
)
transfer.origin.base.commision_units(transfer.units)
return True
transfer.destination.base.commision_units(transfer.units)
return True
@perform_transfer.register
def _perform_transfer_road(self, transfer: RoadTransferOrder) -> bool:
# TODO: Can be improved to use the convoy map.
# The convoy map already has a lot of the data that we're recomputing here.
if transfer.player != transfer.destination.captured:
logging.info(
f"Transfer destination {transfer.destination.name} was captured."
)
self.handle_route_interrupted(transfer)
return True
supply_route = SupplyRoute.for_control_point(transfer.destination)
if transfer.position not in supply_route:
logging.info(
f"Route from {transfer.position.name} to {transfer.destination.name} "
"was cut off."
)
self.handle_route_interrupted(transfer)
return True
path = transfer.path()
next_hop = path[0]
if next_hop == transfer.destination:
logging.info(
f"Units transferred from {transfer.origin.name} to "
f"{transfer.destination.name}"
)
transfer.destination.base.commision_units(transfer.units)
return True
logging.info(
f"Units transferring from {transfer.origin.name} to "
f"{transfer.destination.name} arrived at {next_hop.name}. {len(path) - 1} "
"turns remaining."
)
transfer.position = next_hop
return False
@staticmethod
def handle_route_interrupted(transfer: RoadTransferOrder):
# Halt the transfer in place if safe.
if transfer.player == transfer.position.captured:
logging.info(f"Transferring units are halting at {transfer.position.name}.")
transfer.position.base.commision_units(transfer.units)
return
# If the current position was captured attempt to divert to a neighboring
# friendly CP.
for connected in transfer.position.connected_points:
if connected.captured == transfer.player:
logging.info(f"Transferring units are re-routing to {connected.name}.")
connected.base.commision_units(transfer.units)
return
# If the units are cutoff they are destroyed.
logging.info(
f"Both {transfer.position.name} and {transfer.destination.name} were "
"captured. Units were surrounded and destroyed during transfer."
)

View File

@ -8,11 +8,9 @@ from typing import Dict, Optional, TYPE_CHECKING, Type
from dcs.unittype import UnitType, VehicleType
from game.theater import ControlPoint, SupplyRoute
from gen.ato import Package
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import Flight
from .db import PRICES
from .transfers import AirliftOrder, AirliftPlanner, RoadTransferOrder
from .transfers import TransferOrder
if TYPE_CHECKING:
from .game import Game
@ -111,44 +109,14 @@ class PendingUnitDeliveries:
ground_unit_source.control_point.base.commision_units(
units_needing_transfer
)
if ground_unit_source.requires_airlift:
self.create_air_transfer(
game, ground_unit_source.control_point, units_needing_transfer
)
else:
self.create_road_transfer(
game, ground_unit_source.control_point, units_needing_transfer
)
self.create_transfer(
game, ground_unit_source.control_point, units_needing_transfer
)
def create_air_transfer(
def create_transfer(
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
) -> None:
planner = AirliftPlanner(game, source, self.destination, units)
leftovers = planner.create_package_for_airlift()
if leftovers:
game.message(
f"No airlift capacity remaining for {self.destination}. "
"Remaining unit orders were refunded."
)
self.refund(game, leftovers)
source.base.commit_losses(leftovers)
def find_transport_for(
self,
origin: ControlPoint,
destination: ControlPoint,
units: Dict[Type[VehicleType], int],
) -> Optional[Flight]:
pass
def create_road_transfer(
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
) -> None:
game.transfers.new_transfer(
RoadTransferOrder(
source, self.destination, self.destination.captured, units
)
)
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
def find_ground_unit_source(self, game: Game) -> Optional[GroundUnitSource]:
# This is running *after* the turn counter has been incremented, so this is the

View File

@ -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 AirliftOrder, Convoy
from game.transfers import Convoy, TransferOrder
from gen.flights.flight import Flight
@ -35,7 +35,7 @@ class ConvoyUnit:
@dataclass(frozen=True)
class AirliftUnit:
unit_type: Type[VehicleType]
transfer: AirliftOrder
transfer: TransferOrder
@dataclass(frozen=True)
@ -149,17 +149,14 @@ 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()):
def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> None:
for transport, cargo_type in zip(group.units, transfer.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)
self.airlifts[name] = AirliftUnit(cargo_type, transfer)
def airlift_unit(self, name: str) -> Optional[AirliftUnit]:
return self.airlifts.get(name, None)

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import logging
import random
from dataclasses import dataclass, field
from dataclasses import dataclass
from datetime import timedelta
from functools import cached_property
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union
@ -42,6 +42,7 @@ from dcs.planes import (
from dcs.point import MovingPoint, PointAction
from dcs.task import (
AWACS,
AWACSTaskAction,
AntishipStrike,
AttackGroup,
Bombing,
@ -66,13 +67,12 @@ from dcs.task import (
StartCommand,
Targets,
Task,
Transport,
WeaponType,
AWACSTaskAction,
SetFrequencyCommand,
)
from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
from dcs.unittype import FlyingType, UnitType
from game import db
@ -88,7 +88,7 @@ from game.theater.controlpoint import (
OffMapSpawn,
)
from game.theater.theatergroundobject import TheaterGroundObject
from game.transfers import Convoy, RoadTransferOrder
from game.transfers import Convoy
from game.unitmap import UnitMap
from game.utils import Distance, meters, nautical_miles
from gen.ato import AirTaskingOrder, Package
@ -101,17 +101,17 @@ from gen.flights.flight import (
)
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData
from .airsupportgen import AirSupport, AwacsInfo
from .callsigns import callsign_for_support_unit
from .flights.flightplan import (
AwacsFlightPlan,
CasFlightPlan,
LoiterFlightPlan,
PatrollingFlightPlan,
SweepFlightPlan,
AwacsFlightPlan,
)
from .flights.traveltime import GroundSpeed, TotEstimator
from .naming import namegen
from .airsupportgen import AirSupport, AwacsInfo
from .callsigns import callsign_for_support_unit
if TYPE_CHECKING:
from game import Game
@ -1421,6 +1421,25 @@ class AircraftConflictGenerator:
group, roe=OptROE.Values.OpenFire, restrict_jettison=True
)
def configure_transport(
self,
group: FlyingGroup,
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
) -> None:
# Escort groups are actually given the CAP task so they can perform the
# Search Then Engage task, which we have to use instead of the Escort
# task for the reasons explained in JoinPointBuilder.
group.task = Transport.name
self._setup_group(group, Transport, package, flight, dynamic_runways)
self.configure_behavior(
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.WeaponHold,
restrict_jettison=True,
)
def configure_unknown_task(self, group: FlyingGroup, flight: Flight) -> None:
logging.error(f"Unhandled flight type: {flight.flight_type}")
self.configure_behavior(group)
@ -1459,6 +1478,8 @@ class AircraftConflictGenerator:
self.configure_runway_attack(group, package, flight, dynamic_runways)
elif flight_type == FlightType.OCA_AIRCRAFT:
self.configure_oca_strike(group, package, flight, dynamic_runways)
elif flight_type == FlightType.TRANSPORT:
self.configure_transport(group, package, flight, dynamic_runways)
else:
self.configure_unknown_task(group, flight)

View File

@ -10,7 +10,7 @@ from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup
from dcs.unittype import VehicleType
from game.transfers import Convoy, RoadTransferOrder
from game.transfers import Convoy
from game.unitmap import UnitMap
from game.utils import kph

View File

@ -14,7 +14,7 @@ from game.theater.controlpoint import ControlPoint, MissionTarget
from game.utils import Distance, meters
if TYPE_CHECKING:
from game.transfers import AirliftOrder
from game.transfers import Airlift, TransferOrder
from gen.ato import Package
from gen.flights.flightplan import FlightPlan
@ -167,7 +167,7 @@ class Flight:
arrival: ControlPoint,
divert: Optional[ControlPoint],
custom_name: Optional[str] = None,
cargo: Optional[AirliftOrder] = None,
cargo: Optional[TransferOrder] = None,
) -> None:
self.package = package
self.country = country

View File

@ -163,12 +163,10 @@ class PackageModel(QAbstractListModel):
"""Removes the given flight from the package."""
index = self.package.flights.index(flight)
self.beginRemoveRows(QModelIndex(), index, index)
if flight.cargo is None:
self.game_model.game.aircraft_inventory.return_from_flight(flight)
self.package.remove_flight(flight)
else:
# Deleted transfers will clean up after themselves.
self.game_model.transfer_model.cancel_transfer(flight.cargo)
if flight.cargo is not None:
flight.cargo.transport = None
self.game_model.game.aircraft_inventory.return_from_flight(flight)
self.package.remove_flight(flight)
self.endRemoveRows()
self.update_tot()
@ -261,7 +259,7 @@ class AtoModel(QAbstractListModel):
for flight in package.flights:
self.game.aircraft_inventory.return_from_flight(flight)
if flight.cargo is not None:
self.game_model.transfer_model.cancel_transfer(flight.cargo)
flight.cargo.transport = None
self.endRemoveRows()
# noinspection PyUnresolvedReferences
self.client_slots_changed.emit()

View File

@ -39,12 +39,12 @@ from shapely.geometry import (
import qt_ui.uiconstants as CONST
from game import Game
from game.navmesh import NavMesh
from game.theater import ControlPoint, Enum, SupplyRoute
from game.theater import ControlPoint, Enum
from game.theater.conflicttheater import FrontLine, ReferencePoint
from game.theater.theatergroundobject import (
TheaterGroundObject,
)
from game.transfers import Convoy, RoadTransferOrder
from game.transfers import Convoy
from game.utils import Distance, meters, nautical_miles
from game.weather import TimeOfDay
from gen import Conflict, Package
@ -55,12 +55,10 @@ from gen.flights.flight import (
FlightWaypointType,
)
from gen.flights.flightplan import (
BarCapFlightPlan,
FlightPlan,
FlightPlanBuilder,
InvalidObjectiveLocation,
PatrollingFlightPlan,
TarCapFlightPlan,
)
from gen.flights.traveltime import TotEstimator
from qt_ui.displayoptions import DisplayOptions, ThreatZoneOptions
@ -865,33 +863,6 @@ class QLiberationMap(QGraphicsView):
if DisplayOptions.lines:
self.draw_supply_route_between(cp, connected)
def _transfers_between(
self, a: ControlPoint, b: ControlPoint
) -> List[RoadTransferOrder]:
# We attempt to short circuit the expensive shortest path computation for the
# cases where there is never a transfer, but caching might be needed.
if a.captured != b.captured:
# Cannot transfer to enemy CPs.
return []
# This is only called for drawing lines between nodes and have rules out routes
# to enemy bases, so a and b are guaranteed to be in the same supply route.
supply_route = SupplyRoute.for_control_point(a)
transfers = []
points = {a, b}
for transfer in self.game.transfers:
# No possible route from our network to this transfer.
if transfer.position not in supply_route:
continue
# Anything left is a transfer within our supply route.
transfer_points = {transfer.position, transfer.next_stop()}
if points == transfer_points:
transfers.append(transfer)
return transfers
def draw_supply_route_between(self, a: ControlPoint, b: ControlPoint) -> None:
scene = self.scene()

View File

@ -28,7 +28,7 @@ from dcs.unittype import UnitType
from game import Game, db
from game.theater import ControlPoint, SupplyRoute
from game.transfers import AirliftPlanner, RoadTransferOrder
from game.transfers import TransferOrder
from qt_ui.models import GameModel
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
@ -89,50 +89,12 @@ class UnitTransferList(QFrame):
main_layout.addWidget(scroll)
@dataclass(frozen=True)
class AirliftCapacity:
helicopter: int
cargo_plane: int
@property
def total(self) -> int:
return self.helicopter + self.cargo_plane
@classmethod
def to_control_point(cls, game: Game) -> AirliftCapacity:
helo_capacity = 0
plane_capacity = 0
for cp in game.theater.player_points():
inventory = game.aircraft_inventory.for_control_point(cp)
for unit_type, count in inventory.all_aircraft:
if unit_type.helicopter:
helo_capacity += count
return AirliftCapacity(helicopter=helo_capacity, cargo_plane=plane_capacity)
class TransferOptionsPanel(QVBoxLayout):
def __init__(
self,
game: Game,
origin: ControlPoint,
airlift_capacity: AirliftCapacity,
airlift_required: bool,
) -> None:
def __init__(self, game: Game, origin: ControlPoint) -> None:
super().__init__()
self.source_combo_box = TransferDestinationComboBox(game, origin)
self.addLayout(QLabeledWidget("Destination:", self.source_combo_box))
self.airlift = QCheckBox()
self.airlift.setChecked(airlift_required)
self.airlift.setDisabled(airlift_required)
self.addLayout(QLabeledWidget("Airlift:", self.airlift))
self.addWidget(
QLabel(
f"{airlift_capacity.total} airlift capacity "
f"({airlift_capacity.cargo_plane} from cargo planes, "
f"{airlift_capacity.helicopter} from helicopters)"
)
)
@property
def changed(self):
@ -194,17 +156,9 @@ class TransferControls(QGroupBox):
class ScrollingUnitTransferGrid(QFrame):
def __init__(
self,
cp: ControlPoint,
airlift: bool,
airlift_capacity: AirliftCapacity,
game_model: GameModel,
) -> None:
def __init__(self, cp: ControlPoint, game_model: GameModel) -> None:
super().__init__()
self.cp = cp
self.airlift = airlift
self.remaining_capacity = airlift_capacity.total
self.game_model = game_model
self.transfers: Dict[Type[UnitType, int]] = defaultdict(int)
@ -274,11 +228,6 @@ class ScrollingUnitTransferGrid(QFrame):
if not origin_inventory:
return
if self.airlift:
if not self.remaining_capacity:
return
self.remaining_capacity -= 1
self.transfers[unit_type] += 1
origin_inventory -= 1
controls.set_quantity(self.transfers[unit_type])
@ -290,9 +239,6 @@ class ScrollingUnitTransferGrid(QFrame):
if not controls.quantity:
return
if self.airlift:
self.remaining_capacity += 1
self.transfers[unit_type] -= 1
origin_inventory += 1
controls.set_quantity(self.transfers[unit_type])
@ -329,21 +275,10 @@ class NewUnitTransferDialog(QDialog):
layout = QVBoxLayout()
self.setLayout(layout)
self.airlift_capacity = AirliftCapacity.to_control_point(game_model.game)
airlift_required = len(SupplyRoute.for_control_point(origin)) == 1
self.dest_panel = TransferOptionsPanel(
game_model.game, origin, self.airlift_capacity, airlift_required
)
self.dest_panel.changed.connect(self.rebuild_transfers)
self.dest_panel = TransferOptionsPanel(game_model.game, origin)
layout.addLayout(self.dest_panel)
self.transfer_panel = ScrollingUnitTransferGrid(
origin,
airlift=airlift_required,
airlift_capacity=self.airlift_capacity,
game_model=game_model,
)
self.dest_panel.airlift.toggled.connect(self.rebuild_transfers)
self.transfer_panel = ScrollingUnitTransferGrid(origin, game_model)
layout.addWidget(self.transfer_panel)
self.submit_button = QPushButton("Create Transfer Order", parent=self)
@ -351,31 +286,8 @@ class NewUnitTransferDialog(QDialog):
self.submit_button.setProperty("style", "start-button")
layout.addWidget(self.submit_button)
def rebuild_transfers(self) -> None:
# Rebuild the transfer panel to reset everything. It's easier to recreate the
# panel itself than to clear the grid layout in the panel.
self.layout().removeWidget(self.transfer_panel)
self.layout().removeWidget(self.submit_button)
self.transfer_panel = ScrollingUnitTransferGrid(
self.origin,
airlift=self.dest_panel.airlift.isChecked(),
airlift_capacity=self.airlift_capacity,
game_model=self.game_model,
)
self.layout().addWidget(self.transfer_panel)
self.layout().addWidget(self.submit_button)
def on_submit(self) -> None:
destination = self.dest_panel.current
supply_route = SupplyRoute.for_control_point(self.origin)
if not self.dest_panel.airlift.isChecked() and destination not in supply_route:
QMessageBox.critical(
self,
"Could not create transfer",
f"Transfers from {self.origin} to {destination} require airlift.",
QMessageBox.Ok,
)
return
transfers = {}
for unit_type, count in self.transfer_panel.transfers.items():
if not count:
@ -387,20 +299,10 @@ class NewUnitTransferDialog(QDialog):
)
transfers[unit_type] = count
if self.dest_panel.airlift.isChecked():
planner = AirliftPlanner(
self.game_model.game,
self.origin,
destination,
transfers,
)
planner.create_package_for_airlift()
else:
transfer = RoadTransferOrder(
player=True,
origin=self.origin,
destination=destination,
units=transfers,
)
self.game_model.transfer_model.new_transfer(transfer)
transfer = TransferOrder(
origin=self.origin,
destination=destination,
units=transfers,
)
self.game_model.transfer_model.new_transfer(transfer)
self.close()