mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
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:
parent
d6c84e362f
commit
d80f7ebf3b
@ -20,11 +20,14 @@ if TYPE_CHECKING:
|
|||||||
from game.inventory import ControlPointAircraftInventory
|
from game.inventory import ControlPointAircraftInventory
|
||||||
|
|
||||||
|
|
||||||
# TODO: Remove base classes.
|
class Transport:
|
||||||
# Eventually we'll want multi-mode transfers (convoy from factory to port, onto a ship,
|
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||||
# then airlifted to the final destination, etc). To do this we'll need to make the
|
raise NotImplementedError
|
||||||
# transfer *order* represent the full journey and let classes like Convoy handle the
|
|
||||||
# individual hops.
|
def description(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TransferOrder:
|
class TransferOrder:
|
||||||
"""The base type of all transfer orders.
|
"""The base type of all transfer orders.
|
||||||
@ -38,59 +41,30 @@ class TransferOrder:
|
|||||||
#: The location the units are transferring to.
|
#: The location the units are transferring to.
|
||||||
destination: ControlPoint
|
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.
|
#: True if the transfer order belongs to the player.
|
||||||
player: bool
|
player: bool = field(init=False)
|
||||||
|
|
||||||
#: The units being transferred.
|
#: The units being transferred.
|
||||||
units: Dict[Type[VehicleType], int]
|
units: Dict[Type[VehicleType], int]
|
||||||
|
|
||||||
@property
|
transport: Optional[Transport] = field(default=None)
|
||||||
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)
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
self.position = self.origin
|
self.position = self.origin
|
||||||
|
self.player = self.origin.is_friendly(to_player=True)
|
||||||
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]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self) -> str:
|
def description(self) -> str:
|
||||||
path = self.path()
|
if self.transport is None:
|
||||||
if len(path) == 1:
|
return "No transports available"
|
||||||
turns = "1 turn"
|
return self.transport.description()
|
||||||
else:
|
|
||||||
turns = f"{len(path)} turns"
|
|
||||||
return f"Currently at {self.position}. Arrives at destination in {turns}."
|
|
||||||
|
|
||||||
|
def kill_all(self) -> None:
|
||||||
@dataclass
|
self.units.clear()
|
||||||
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_unit(self, unit_type: Type[VehicleType]) -> None:
|
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
|
||||||
if unit_type in self.units:
|
if unit_type in self.units:
|
||||||
@ -98,54 +72,102 @@ class AirliftOrder(TransferOrder):
|
|||||||
return
|
return
|
||||||
raise KeyError
|
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:
|
class AirliftPlanner:
|
||||||
def __init__(
|
def __init__(self, game: Game, transfer: TransferOrder) -> None:
|
||||||
self,
|
|
||||||
game: Game,
|
|
||||||
pickup: ControlPoint,
|
|
||||||
drop_off: ControlPoint,
|
|
||||||
units: Dict[Type[VehicleType], int],
|
|
||||||
) -> None:
|
|
||||||
self.game = game
|
self.game = game
|
||||||
self.pickup = pickup
|
self.transfer = transfer
|
||||||
self.drop_off = drop_off
|
self.for_player = transfer.destination.captured
|
||||||
self.units = units
|
self.package = Package(target=transfer.destination, auto_asap=True)
|
||||||
self.for_player = drop_off.captured
|
|
||||||
self.package = Package(target=drop_off, 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():
|
for cp in self.game.theater.player_points():
|
||||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||||
for unit_type, available in inventory.all_aircraft:
|
for unit_type, available in inventory.all_aircraft:
|
||||||
if unit_type.helicopter:
|
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)
|
flight_size = self.create_airlift_flight(unit_type, inventory)
|
||||||
available -= flight_size
|
available -= flight_size
|
||||||
self.game.ato_for(self.for_player).add_package(self.package)
|
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(
|
def create_airlift_flight(
|
||||||
self, unit_type: Type[FlyingType], inventory: ControlPointAircraftInventory
|
self, unit_type: Type[FlyingType], inventory: ControlPointAircraftInventory
|
||||||
) -> int:
|
) -> int:
|
||||||
available = inventory.available(unit_type)
|
available = inventory.available(unit_type)
|
||||||
# 4 is the max flight size in DCS.
|
# 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(
|
flight = Flight(
|
||||||
self.package,
|
self.package,
|
||||||
self.game.player_country,
|
self.game.player_country,
|
||||||
@ -156,31 +178,25 @@ class AirliftPlanner:
|
|||||||
departure=inventory.control_point,
|
departure=inventory.control_point,
|
||||||
arrival=inventory.control_point,
|
arrival=inventory.control_point,
|
||||||
divert=None,
|
divert=None,
|
||||||
|
cargo=transfer,
|
||||||
)
|
)
|
||||||
|
|
||||||
transfer = AirliftOrder(
|
transport = Airlift(transfer, flight)
|
||||||
player=True,
|
transfer.transport = transport
|
||||||
origin=self.pickup,
|
|
||||||
destination=self.drop_off,
|
|
||||||
units=self.take_units(flight_size),
|
|
||||||
flight=flight,
|
|
||||||
)
|
|
||||||
flight.cargo = transfer
|
|
||||||
|
|
||||||
self.package.add_flight(flight)
|
self.package.add_flight(flight)
|
||||||
planner = FlightPlanBuilder(self.game, self.package, self.for_player)
|
planner = FlightPlanBuilder(self.game, self.package, self.for_player)
|
||||||
planner.populate_flight_plan(flight)
|
planner.populate_flight_plan(flight)
|
||||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||||
self.game.transfers.new_transfer(transfer)
|
|
||||||
return flight_size
|
return flight_size
|
||||||
|
|
||||||
|
|
||||||
class Convoy(MissionTarget):
|
class Convoy(MissionTarget, Transport):
|
||||||
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
|
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
|
||||||
super().__init__(namegen.next_convoy_name(), origin.position)
|
super().__init__(namegen.next_convoy_name(), origin.position)
|
||||||
self.origin = origin
|
self.origin = origin
|
||||||
self.destination = destination
|
self.destination = destination
|
||||||
self.transfers: List[RoadTransferOrder] = []
|
self.transfers: List[TransferOrder] = []
|
||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
if self.is_friendly(for_player):
|
if self.is_friendly(for_player):
|
||||||
@ -192,17 +208,20 @@ class Convoy(MissionTarget):
|
|||||||
def is_friendly(self, to_player: bool) -> bool:
|
def is_friendly(self, to_player: bool) -> bool:
|
||||||
return self.origin.captured
|
return self.origin.captured
|
||||||
|
|
||||||
def add_units(self, transfer: RoadTransferOrder) -> None:
|
def add_units(self, transfer: TransferOrder) -> None:
|
||||||
self.transfers.append(transfer)
|
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)
|
self.transfers.remove(transfer)
|
||||||
|
|
||||||
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
|
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
|
||||||
for transfer in self.transfers:
|
for transfer in self.transfers:
|
||||||
if unit_type in transfer.units:
|
try:
|
||||||
transfer.units[unit_type] -= 1
|
transfer.kill_unit(unit_type)
|
||||||
return
|
return
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
raise KeyError
|
raise KeyError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -221,6 +240,12 @@ class Convoy(MissionTarget):
|
|||||||
def player_owned(self) -> bool:
|
def player_owned(self) -> bool:
|
||||||
return self.origin.captured
|
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:
|
class ConvoyMap:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@ -255,18 +280,21 @@ class ConvoyMap:
|
|||||||
def disband_convoy(self, convoy: Convoy) -> None:
|
def disband_convoy(self, convoy: Convoy) -> None:
|
||||||
del self.convoys[convoy.origin][convoy.destination]
|
del self.convoys[convoy.origin][convoy.destination]
|
||||||
|
|
||||||
def add(self, transfer: RoadTransferOrder) -> None:
|
@staticmethod
|
||||||
next_stop = transfer.next_stop()
|
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)
|
self.find_or_create_convoy(transfer.position, next_stop).add_units(transfer)
|
||||||
|
|
||||||
def remove(self, transfer: RoadTransferOrder) -> None:
|
def remove(self, convoy: Convoy, transfer: TransferOrder) -> 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
|
|
||||||
convoy.remove_units(transfer)
|
convoy.remove_units(transfer)
|
||||||
if not convoy.transfers:
|
if not convoy.transfers:
|
||||||
self.disband_convoy(convoy)
|
self.disband_convoy(convoy)
|
||||||
@ -298,43 +326,63 @@ class PendingTransfers:
|
|||||||
def index_of_transfer(self, transfer: TransferOrder) -> int:
|
def index_of_transfer(self, transfer: TransferOrder) -> int:
|
||||||
return self.pending_transfers.index(transfer)
|
return self.pending_transfers.index(transfer)
|
||||||
|
|
||||||
# TODO: Move airlift arrangements here?
|
def arrange_transport(self, transfer: TransferOrder) -> None:
|
||||||
@singledispatchmethod
|
supply_route = SupplyRoute.for_control_point(transfer.position)
|
||||||
def arrange_transport(self, transfer) -> None:
|
if transfer.destination in supply_route:
|
||||||
pass
|
|
||||||
|
|
||||||
@arrange_transport.register
|
|
||||||
def _arrange_transport_road(self, transfer: RoadTransferOrder) -> None:
|
|
||||||
self.convoys.add(transfer)
|
self.convoys.add(transfer)
|
||||||
|
else:
|
||||||
|
AirliftPlanner(self.game, transfer).create_package_for_airlift()
|
||||||
|
|
||||||
def new_transfer(self, transfer: TransferOrder) -> None:
|
def new_transfer(self, transfer: TransferOrder) -> None:
|
||||||
transfer.origin.base.commit_losses(transfer.units)
|
transfer.origin.base.commit_losses(transfer.units)
|
||||||
self.pending_transfers.append(transfer)
|
self.pending_transfers.append(transfer)
|
||||||
self.arrange_transport(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
|
@singledispatchmethod
|
||||||
def cancel_transport(self, transfer) -> None:
|
def cancel_transport(self, transfer: TransferOrder, transport) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@cancel_transport.register
|
@cancel_transport.register
|
||||||
def _cancel_transport_air(self, transfer: AirliftOrder) -> None:
|
def _cancel_transport_air(
|
||||||
flight = transfer.flight
|
self, _transfer: TransferOrder, transport: Airlift
|
||||||
|
) -> None:
|
||||||
|
flight = transport.flight
|
||||||
flight.package.remove_flight(flight)
|
flight.package.remove_flight(flight)
|
||||||
self.game.aircraft_inventory.return_from_flight(flight)
|
self.game.aircraft_inventory.return_from_flight(flight)
|
||||||
|
|
||||||
@cancel_transport.register
|
def _cancel_transport_convoy(
|
||||||
def _cancel_transport_road(self, transfer: RoadTransferOrder) -> None:
|
self, transfer: TransferOrder, transport: Convoy
|
||||||
self.convoys.remove(transfer)
|
) -> None:
|
||||||
|
self.convoys.remove(transport, transfer)
|
||||||
|
|
||||||
def cancel_transfer(self, transfer: TransferOrder) -> None:
|
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)
|
self.pending_transfers.remove(transfer)
|
||||||
transfer.origin.base.commision_units(transfer.units)
|
transfer.origin.base.commision_units(transfer.units)
|
||||||
|
|
||||||
def perform_transfers(self) -> None:
|
def perform_transfers(self) -> None:
|
||||||
incomplete = []
|
incomplete = []
|
||||||
for transfer in self.pending_transfers:
|
for transfer in self.pending_transfers:
|
||||||
if not self.perform_transfer(transfer):
|
transfer.proceed()
|
||||||
|
if not transfer.completed:
|
||||||
incomplete.append(transfer)
|
incomplete.append(transfer)
|
||||||
self.pending_transfers = incomplete
|
self.pending_transfers = incomplete
|
||||||
self.rebuild_convoys()
|
self.rebuild_convoys()
|
||||||
@ -343,80 +391,3 @@ class PendingTransfers:
|
|||||||
self.convoys.disband_all()
|
self.convoys.disband_all()
|
||||||
for transfer in self.pending_transfers:
|
for transfer in self.pending_transfers:
|
||||||
self.arrange_transport(transfer)
|
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."
|
|
||||||
)
|
|
||||||
|
|||||||
@ -8,11 +8,9 @@ from typing import Dict, Optional, TYPE_CHECKING, Type
|
|||||||
from dcs.unittype import UnitType, VehicleType
|
from dcs.unittype import UnitType, VehicleType
|
||||||
|
|
||||||
from game.theater import ControlPoint, SupplyRoute
|
from game.theater import ControlPoint, SupplyRoute
|
||||||
from gen.ato import Package
|
|
||||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||||
from gen.flights.flight import Flight
|
|
||||||
from .db import PRICES
|
from .db import PRICES
|
||||||
from .transfers import AirliftOrder, AirliftPlanner, RoadTransferOrder
|
from .transfers import TransferOrder
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .game import Game
|
from .game import Game
|
||||||
@ -111,44 +109,14 @@ class PendingUnitDeliveries:
|
|||||||
ground_unit_source.control_point.base.commision_units(
|
ground_unit_source.control_point.base.commision_units(
|
||||||
units_needing_transfer
|
units_needing_transfer
|
||||||
)
|
)
|
||||||
if ground_unit_source.requires_airlift:
|
self.create_transfer(
|
||||||
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
|
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]
|
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
|
||||||
) -> None:
|
) -> None:
|
||||||
planner = AirliftPlanner(game, source, self.destination, units)
|
game.transfers.new_transfer(TransferOrder(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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def find_ground_unit_source(self, game: Game) -> Optional[GroundUnitSource]:
|
def find_ground_unit_source(self, game: Game) -> Optional[GroundUnitSource]:
|
||||||
# This is running *after* the turn counter has been incremented, so this is the
|
# This is running *after* the turn counter has been incremented, so this is the
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from dcs.unittype import VehicleType
|
|||||||
from game import db
|
from game import db
|
||||||
from game.theater import Airfield, ControlPoint, TheaterGroundObject
|
from game.theater import Airfield, ControlPoint, TheaterGroundObject
|
||||||
from game.theater.theatergroundobject import BuildingGroundObject
|
from game.theater.theatergroundobject import BuildingGroundObject
|
||||||
from game.transfers import AirliftOrder, Convoy
|
from game.transfers import Convoy, TransferOrder
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
|
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ class ConvoyUnit:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AirliftUnit:
|
class AirliftUnit:
|
||||||
unit_type: Type[VehicleType]
|
unit_type: Type[VehicleType]
|
||||||
transfer: AirliftOrder
|
transfer: TransferOrder
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -149,17 +149,14 @@ class UnitMap:
|
|||||||
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
|
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
|
||||||
return self.convoys.get(name, None)
|
return self.convoys.get(name, None)
|
||||||
|
|
||||||
def add_airlift_units(self, group: FlyingGroup, airlift: AirliftOrder) -> None:
|
def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> None:
|
||||||
for transport, cargo_type in zip(group.units, airlift.iter_units()):
|
for transport, cargo_type in zip(group.units, transfer.iter_units()):
|
||||||
# The actual name is a String (the pydcs translatable string), which
|
# The actual name is a String (the pydcs translatable string), which
|
||||||
# doesn't define __eq__.
|
# doesn't define __eq__.
|
||||||
name = str(transport.name)
|
name = str(transport.name)
|
||||||
if name in self.airlifts:
|
if name in self.airlifts:
|
||||||
raise RuntimeError(f"Duplicate airlift unit: {name}")
|
raise RuntimeError(f"Duplicate airlift unit: {name}")
|
||||||
unit_type = db.unit_type_from_name(transport.type)
|
self.airlifts[name] = AirliftUnit(cargo_type, transfer)
|
||||||
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]:
|
def airlift_unit(self, name: str) -> Optional[AirliftUnit]:
|
||||||
return self.airlifts.get(name, None)
|
return self.airlifts.get(name, None)
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union
|
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.point import MovingPoint, PointAction
|
||||||
from dcs.task import (
|
from dcs.task import (
|
||||||
AWACS,
|
AWACS,
|
||||||
|
AWACSTaskAction,
|
||||||
AntishipStrike,
|
AntishipStrike,
|
||||||
AttackGroup,
|
AttackGroup,
|
||||||
Bombing,
|
Bombing,
|
||||||
@ -66,13 +67,12 @@ from dcs.task import (
|
|||||||
StartCommand,
|
StartCommand,
|
||||||
Targets,
|
Targets,
|
||||||
Task,
|
Task,
|
||||||
|
Transport,
|
||||||
WeaponType,
|
WeaponType,
|
||||||
AWACSTaskAction,
|
|
||||||
SetFrequencyCommand,
|
|
||||||
)
|
)
|
||||||
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||||
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
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 dcs.unittype import FlyingType, UnitType
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
@ -88,7 +88,7 @@ from game.theater.controlpoint import (
|
|||||||
OffMapSpawn,
|
OffMapSpawn,
|
||||||
)
|
)
|
||||||
from game.theater.theatergroundobject import TheaterGroundObject
|
from game.theater.theatergroundobject import TheaterGroundObject
|
||||||
from game.transfers import Convoy, RoadTransferOrder
|
from game.transfers import Convoy
|
||||||
from game.unitmap import UnitMap
|
from game.unitmap import UnitMap
|
||||||
from game.utils import Distance, meters, nautical_miles
|
from game.utils import Distance, meters, nautical_miles
|
||||||
from gen.ato import AirTaskingOrder, Package
|
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.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
||||||
from gen.runways import RunwayData
|
from gen.runways import RunwayData
|
||||||
|
from .airsupportgen import AirSupport, AwacsInfo
|
||||||
|
from .callsigns import callsign_for_support_unit
|
||||||
from .flights.flightplan import (
|
from .flights.flightplan import (
|
||||||
|
AwacsFlightPlan,
|
||||||
CasFlightPlan,
|
CasFlightPlan,
|
||||||
LoiterFlightPlan,
|
LoiterFlightPlan,
|
||||||
PatrollingFlightPlan,
|
PatrollingFlightPlan,
|
||||||
SweepFlightPlan,
|
SweepFlightPlan,
|
||||||
AwacsFlightPlan,
|
|
||||||
)
|
)
|
||||||
from .flights.traveltime import GroundSpeed, TotEstimator
|
from .flights.traveltime import GroundSpeed, TotEstimator
|
||||||
from .naming import namegen
|
from .naming import namegen
|
||||||
from .airsupportgen import AirSupport, AwacsInfo
|
|
||||||
from .callsigns import callsign_for_support_unit
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
@ -1421,6 +1421,25 @@ class AircraftConflictGenerator:
|
|||||||
group, roe=OptROE.Values.OpenFire, restrict_jettison=True
|
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:
|
def configure_unknown_task(self, group: FlyingGroup, flight: Flight) -> None:
|
||||||
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
||||||
self.configure_behavior(group)
|
self.configure_behavior(group)
|
||||||
@ -1459,6 +1478,8 @@ class AircraftConflictGenerator:
|
|||||||
self.configure_runway_attack(group, package, flight, dynamic_runways)
|
self.configure_runway_attack(group, package, flight, dynamic_runways)
|
||||||
elif flight_type == FlightType.OCA_AIRCRAFT:
|
elif flight_type == FlightType.OCA_AIRCRAFT:
|
||||||
self.configure_oca_strike(group, package, flight, dynamic_runways)
|
self.configure_oca_strike(group, package, flight, dynamic_runways)
|
||||||
|
elif flight_type == FlightType.TRANSPORT:
|
||||||
|
self.configure_transport(group, package, flight, dynamic_runways)
|
||||||
else:
|
else:
|
||||||
self.configure_unknown_task(group, flight)
|
self.configure_unknown_task(group, flight)
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from dcs.unit import Vehicle
|
|||||||
from dcs.unitgroup import VehicleGroup
|
from dcs.unitgroup import VehicleGroup
|
||||||
from dcs.unittype import VehicleType
|
from dcs.unittype import VehicleType
|
||||||
|
|
||||||
from game.transfers import Convoy, RoadTransferOrder
|
from game.transfers import Convoy
|
||||||
from game.unitmap import UnitMap
|
from game.unitmap import UnitMap
|
||||||
from game.utils import kph
|
from game.utils import kph
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from game.theater.controlpoint import ControlPoint, MissionTarget
|
|||||||
from game.utils import Distance, meters
|
from game.utils import Distance, meters
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.transfers import AirliftOrder
|
from game.transfers import Airlift, TransferOrder
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
from gen.flights.flightplan import FlightPlan
|
from gen.flights.flightplan import FlightPlan
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ class Flight:
|
|||||||
arrival: ControlPoint,
|
arrival: ControlPoint,
|
||||||
divert: Optional[ControlPoint],
|
divert: Optional[ControlPoint],
|
||||||
custom_name: Optional[str] = None,
|
custom_name: Optional[str] = None,
|
||||||
cargo: Optional[AirliftOrder] = None,
|
cargo: Optional[TransferOrder] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.package = package
|
self.package = package
|
||||||
self.country = country
|
self.country = country
|
||||||
|
|||||||
@ -163,12 +163,10 @@ class PackageModel(QAbstractListModel):
|
|||||||
"""Removes the given flight from the package."""
|
"""Removes the given flight from the package."""
|
||||||
index = self.package.flights.index(flight)
|
index = self.package.flights.index(flight)
|
||||||
self.beginRemoveRows(QModelIndex(), index, index)
|
self.beginRemoveRows(QModelIndex(), index, index)
|
||||||
if flight.cargo is None:
|
if flight.cargo is not None:
|
||||||
|
flight.cargo.transport = None
|
||||||
self.game_model.game.aircraft_inventory.return_from_flight(flight)
|
self.game_model.game.aircraft_inventory.return_from_flight(flight)
|
||||||
self.package.remove_flight(flight)
|
self.package.remove_flight(flight)
|
||||||
else:
|
|
||||||
# Deleted transfers will clean up after themselves.
|
|
||||||
self.game_model.transfer_model.cancel_transfer(flight.cargo)
|
|
||||||
self.endRemoveRows()
|
self.endRemoveRows()
|
||||||
self.update_tot()
|
self.update_tot()
|
||||||
|
|
||||||
@ -261,7 +259,7 @@ class AtoModel(QAbstractListModel):
|
|||||||
for flight in package.flights:
|
for flight in package.flights:
|
||||||
self.game.aircraft_inventory.return_from_flight(flight)
|
self.game.aircraft_inventory.return_from_flight(flight)
|
||||||
if flight.cargo is not None:
|
if flight.cargo is not None:
|
||||||
self.game_model.transfer_model.cancel_transfer(flight.cargo)
|
flight.cargo.transport = None
|
||||||
self.endRemoveRows()
|
self.endRemoveRows()
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
self.client_slots_changed.emit()
|
self.client_slots_changed.emit()
|
||||||
|
|||||||
@ -39,12 +39,12 @@ from shapely.geometry import (
|
|||||||
import qt_ui.uiconstants as CONST
|
import qt_ui.uiconstants as CONST
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.navmesh import NavMesh
|
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.conflicttheater import FrontLine, ReferencePoint
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
)
|
)
|
||||||
from game.transfers import Convoy, RoadTransferOrder
|
from game.transfers import Convoy
|
||||||
from game.utils import Distance, meters, nautical_miles
|
from game.utils import Distance, meters, nautical_miles
|
||||||
from game.weather import TimeOfDay
|
from game.weather import TimeOfDay
|
||||||
from gen import Conflict, Package
|
from gen import Conflict, Package
|
||||||
@ -55,12 +55,10 @@ from gen.flights.flight import (
|
|||||||
FlightWaypointType,
|
FlightWaypointType,
|
||||||
)
|
)
|
||||||
from gen.flights.flightplan import (
|
from gen.flights.flightplan import (
|
||||||
BarCapFlightPlan,
|
|
||||||
FlightPlan,
|
FlightPlan,
|
||||||
FlightPlanBuilder,
|
FlightPlanBuilder,
|
||||||
InvalidObjectiveLocation,
|
InvalidObjectiveLocation,
|
||||||
PatrollingFlightPlan,
|
PatrollingFlightPlan,
|
||||||
TarCapFlightPlan,
|
|
||||||
)
|
)
|
||||||
from gen.flights.traveltime import TotEstimator
|
from gen.flights.traveltime import TotEstimator
|
||||||
from qt_ui.displayoptions import DisplayOptions, ThreatZoneOptions
|
from qt_ui.displayoptions import DisplayOptions, ThreatZoneOptions
|
||||||
@ -865,33 +863,6 @@ class QLiberationMap(QGraphicsView):
|
|||||||
if DisplayOptions.lines:
|
if DisplayOptions.lines:
|
||||||
self.draw_supply_route_between(cp, connected)
|
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:
|
def draw_supply_route_between(self, a: ControlPoint, b: ControlPoint) -> None:
|
||||||
scene = self.scene()
|
scene = self.scene()
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@ from dcs.unittype import UnitType
|
|||||||
|
|
||||||
from game import Game, db
|
from game import Game, db
|
||||||
from game.theater import ControlPoint, SupplyRoute
|
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.models import GameModel
|
||||||
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||||
|
|
||||||
@ -89,50 +89,12 @@ class UnitTransferList(QFrame):
|
|||||||
main_layout.addWidget(scroll)
|
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):
|
class TransferOptionsPanel(QVBoxLayout):
|
||||||
def __init__(
|
def __init__(self, game: Game, origin: ControlPoint) -> None:
|
||||||
self,
|
|
||||||
game: Game,
|
|
||||||
origin: ControlPoint,
|
|
||||||
airlift_capacity: AirliftCapacity,
|
|
||||||
airlift_required: bool,
|
|
||||||
) -> None:
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.source_combo_box = TransferDestinationComboBox(game, origin)
|
self.source_combo_box = TransferDestinationComboBox(game, origin)
|
||||||
self.addLayout(QLabeledWidget("Destination:", self.source_combo_box))
|
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
|
@property
|
||||||
def changed(self):
|
def changed(self):
|
||||||
@ -194,17 +156,9 @@ class TransferControls(QGroupBox):
|
|||||||
|
|
||||||
|
|
||||||
class ScrollingUnitTransferGrid(QFrame):
|
class ScrollingUnitTransferGrid(QFrame):
|
||||||
def __init__(
|
def __init__(self, cp: ControlPoint, game_model: GameModel) -> None:
|
||||||
self,
|
|
||||||
cp: ControlPoint,
|
|
||||||
airlift: bool,
|
|
||||||
airlift_capacity: AirliftCapacity,
|
|
||||||
game_model: GameModel,
|
|
||||||
) -> None:
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.cp = cp
|
self.cp = cp
|
||||||
self.airlift = airlift
|
|
||||||
self.remaining_capacity = airlift_capacity.total
|
|
||||||
self.game_model = game_model
|
self.game_model = game_model
|
||||||
self.transfers: Dict[Type[UnitType, int]] = defaultdict(int)
|
self.transfers: Dict[Type[UnitType, int]] = defaultdict(int)
|
||||||
|
|
||||||
@ -274,11 +228,6 @@ class ScrollingUnitTransferGrid(QFrame):
|
|||||||
if not origin_inventory:
|
if not origin_inventory:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.airlift:
|
|
||||||
if not self.remaining_capacity:
|
|
||||||
return
|
|
||||||
self.remaining_capacity -= 1
|
|
||||||
|
|
||||||
self.transfers[unit_type] += 1
|
self.transfers[unit_type] += 1
|
||||||
origin_inventory -= 1
|
origin_inventory -= 1
|
||||||
controls.set_quantity(self.transfers[unit_type])
|
controls.set_quantity(self.transfers[unit_type])
|
||||||
@ -290,9 +239,6 @@ class ScrollingUnitTransferGrid(QFrame):
|
|||||||
if not controls.quantity:
|
if not controls.quantity:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.airlift:
|
|
||||||
self.remaining_capacity += 1
|
|
||||||
|
|
||||||
self.transfers[unit_type] -= 1
|
self.transfers[unit_type] -= 1
|
||||||
origin_inventory += 1
|
origin_inventory += 1
|
||||||
controls.set_quantity(self.transfers[unit_type])
|
controls.set_quantity(self.transfers[unit_type])
|
||||||
@ -329,21 +275,10 @@ class NewUnitTransferDialog(QDialog):
|
|||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
self.airlift_capacity = AirliftCapacity.to_control_point(game_model.game)
|
self.dest_panel = TransferOptionsPanel(game_model.game, origin)
|
||||||
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)
|
|
||||||
layout.addLayout(self.dest_panel)
|
layout.addLayout(self.dest_panel)
|
||||||
|
|
||||||
self.transfer_panel = ScrollingUnitTransferGrid(
|
self.transfer_panel = ScrollingUnitTransferGrid(origin, game_model)
|
||||||
origin,
|
|
||||||
airlift=airlift_required,
|
|
||||||
airlift_capacity=self.airlift_capacity,
|
|
||||||
game_model=game_model,
|
|
||||||
)
|
|
||||||
self.dest_panel.airlift.toggled.connect(self.rebuild_transfers)
|
|
||||||
layout.addWidget(self.transfer_panel)
|
layout.addWidget(self.transfer_panel)
|
||||||
|
|
||||||
self.submit_button = QPushButton("Create Transfer Order", parent=self)
|
self.submit_button = QPushButton("Create Transfer Order", parent=self)
|
||||||
@ -351,31 +286,8 @@ class NewUnitTransferDialog(QDialog):
|
|||||||
self.submit_button.setProperty("style", "start-button")
|
self.submit_button.setProperty("style", "start-button")
|
||||||
layout.addWidget(self.submit_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:
|
def on_submit(self) -> None:
|
||||||
destination = self.dest_panel.current
|
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 = {}
|
transfers = {}
|
||||||
for unit_type, count in self.transfer_panel.transfers.items():
|
for unit_type, count in self.transfer_panel.transfers.items():
|
||||||
if not count:
|
if not count:
|
||||||
@ -387,17 +299,7 @@ class NewUnitTransferDialog(QDialog):
|
|||||||
)
|
)
|
||||||
transfers[unit_type] = count
|
transfers[unit_type] = count
|
||||||
|
|
||||||
if self.dest_panel.airlift.isChecked():
|
transfer = TransferOrder(
|
||||||
planner = AirliftPlanner(
|
|
||||||
self.game_model.game,
|
|
||||||
self.origin,
|
|
||||||
destination,
|
|
||||||
transfers,
|
|
||||||
)
|
|
||||||
planner.create_package_for_airlift()
|
|
||||||
else:
|
|
||||||
transfer = RoadTransferOrder(
|
|
||||||
player=True,
|
|
||||||
origin=self.origin,
|
origin=self.origin,
|
||||||
destination=destination,
|
destination=destination,
|
||||||
units=transfers,
|
units=transfers,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user