Airlift support.

UI isn't finished. Bulk transfers where the player doesn't care what
aircraft get used work (though they're chosen with no thought at all),
but being able to plan your own airlift flight isn't here yet.

Cargo planes are not implemented yet.

No way to view the cargo of a flight (will come with the cargo flight
planning UI).

The airlift flight/package creation should probably be moved out of the
UI and into the game code.

AI doesn't use these yet.

https://github.com/Khopa/dcs_liberation/issues/825
This commit is contained in:
Dan Albert 2021-04-21 17:13:35 -07:00
parent 8e361a8776
commit 481f195725
12 changed files with 406 additions and 73 deletions

View File

@ -1174,6 +1174,7 @@ EXPANDED_TASK_PAYLOAD_OVERRIDE = {
"SWEEP": ("CAP HEAVY", "CAP"),
"OCA_RUNWAY": ("RUNWAY_ATTACK", "RUNWAY_STRIKE", "STRIKE"),
"OCA_AIRCRAFT": ("OCA", "CAS MAVERICK F", "CAS"),
"TRANSPORT": (),
}
PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {

View File

@ -122,7 +122,7 @@ class Game:
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
self.transfers = PendingTransfers()
self.transfers = PendingTransfers(self)
self.sanitize_sides()

View File

@ -3,16 +3,17 @@ from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass, field
from functools import singledispatchmethod
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Type
from dcs.unittype import VehicleType
if TYPE_CHECKING:
pass
from game import Game
from game.theater import ControlPoint, MissionTarget
from game.theater.supplyroutes import SupplyRoute
from gen.naming import namegen
from gen.flights.flight import FlightType
from gen.flights.flight import Flight, FlightType
@dataclass
@ -31,14 +32,18 @@ class TransferOrder:
#: True if the transfer order belongs to the player.
player: bool
#: 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 units being transferred.
units: Dict[Type[VehicleType], int]
#: The current position of the group being transferred. Groups move one control
#: point a turn through the supply line.
position: ControlPoint = field(init=False)
@ -53,6 +58,26 @@ class RoadTransferOrder(TransferOrder):
def next_stop(self) -> ControlPoint:
return self.path()[0]
@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}."
@dataclass
class AirliftOrder(TransferOrder):
"""A transfer order that moves units by cargo planes and helicopters."""
flight: Flight
@property
def description(self) -> str:
return "Airlift"
class Convoy(MissionTarget):
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
@ -159,27 +184,54 @@ class ConvoyMap:
class PendingTransfers:
def __init__(self) -> None:
def __init__(self, game: Game) -> None:
self.game = game
self.convoys = ConvoyMap()
self.pending_transfers: List[RoadTransferOrder] = []
self.pending_transfers: List[TransferOrder] = []
def __iter__(self) -> Iterator[RoadTransferOrder]:
def __iter__(self) -> Iterator[TransferOrder]:
yield from self.pending_transfers
@property
def pending_transfer_count(self) -> int:
return len(self.pending_transfers)
def transfer_at_index(self, index: int) -> RoadTransferOrder:
def transfer_at_index(self, index: int) -> TransferOrder:
return self.pending_transfers[index]
def new_transfer(self, transfer: RoadTransferOrder) -> None:
transfer.origin.base.commit_losses(transfer.units)
self.pending_transfers.append(transfer)
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 cancel_transfer(self, transfer: RoadTransferOrder) -> None:
def new_transfer(self, transfer: TransferOrder) -> None:
transfer.origin.base.commit_losses(transfer.units)
self.pending_transfers.append(transfer)
self.arrange_transport(transfer)
@singledispatchmethod
def cancel_transport(self, transfer) -> None:
pass
@cancel_transport.register
def _cancel_transport_air(self, transfer: AirliftOrder) -> None:
flight = transfer.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_transfer(self, transfer: TransferOrder) -> None:
self.cancel_transport(transfer)
self.pending_transfers.remove(transfer)
transfer.origin.base.commision_units(transfer.units)
@ -194,9 +246,27 @@ class PendingTransfers:
def rebuild_convoys(self) -> None:
self.convoys.disband_all()
for transfer in self.pending_transfers:
self.convoys.add(transfer)
self.arrange_transport(transfer)
def perform_transfer(self, transfer: RoadTransferOrder) -> bool:
@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:

View File

@ -8,6 +8,8 @@ example, the package to strike an enemy airfield may contain an escort flight,
a SEAD flight, and the strike aircraft themselves. CAP packages may contain only
the single CAP flight.
"""
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass, field
@ -172,6 +174,7 @@ class Package:
FlightType.OCA_RUNWAY,
FlightType.BAI,
FlightType.DEAD,
FlightType.TRANSPORT,
FlightType.SEAD,
FlightType.TARCAP,
FlightType.BARCAP,

View File

@ -1,6 +1,5 @@
from __future__ import annotations
from collections import defaultdict
from datetime import timedelta
from enum import Enum
from typing import Dict, List, Optional, TYPE_CHECKING, Type
@ -15,6 +14,7 @@ from game.theater.controlpoint import ControlPoint, MissionTarget
from game.utils import Distance, meters
if TYPE_CHECKING:
from game.transfers import AirliftOrder
from gen.ato import Package
from gen.flights.flightplan import FlightPlan
@ -43,6 +43,7 @@ class FlightType(Enum):
OCA_RUNWAY = "OCA/Runway"
OCA_AIRCRAFT = "OCA/Aircraft"
AEWC = "AEW&C"
TRANSPORT = "Transport"
def __str__(self) -> str:
return self.value
@ -75,6 +76,8 @@ class FlightWaypointType(Enum):
DIVERT = 23
INGRESS_OCA_RUNWAY = 24
INGRESS_OCA_AIRCRAFT = 25
PICKUP = 26
DROP_OFF = 27
class FlightWaypoint:
@ -164,6 +167,7 @@ class Flight:
arrival: ControlPoint,
divert: Optional[ControlPoint],
custom_name: Optional[str] = None,
cargo: Optional[AirliftOrder] = None,
) -> None:
self.package = package
self.country = country
@ -181,6 +185,9 @@ class Flight:
self.client_count = 0
self.custom_name = custom_name
# Only used by transport missions.
self.cargo = cargo
# Will be replaced with a more appropriate FlightPlan by
# FlightPlanBuilder, but an empty flight plan the flight begins with an
# empty flight plan.

View File

@ -31,7 +31,6 @@ from game.theater import (
TheaterGroundObject,
)
from game.theater.theatergroundobject import EwrGroundObject
from game.transfers import Convoy
from game.utils import Distance, Speed, feet, meters, nautical_miles
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
@ -42,6 +41,7 @@ from ..conflictgen import Conflict, FRONTLINE_LENGTH
if TYPE_CHECKING:
from game import Game
from gen.ato import Package
from game.transfers import Convoy
INGRESS_TYPES = {
FlightWaypointType.INGRESS_CAS,
@ -736,6 +736,46 @@ class AwacsFlightPlan(LoiterFlightPlan):
return self.push_time
@dataclass(frozen=True)
class AirliftFlightPlan(FlightPlan):
takeoff: FlightWaypoint
nav_to_pickup: List[FlightWaypoint]
pickup: Optional[FlightWaypoint]
nav_to_drop_off: List[FlightWaypoint]
drop_off: FlightWaypoint
nav_to_home: List[FlightWaypoint]
land: FlightWaypoint
divert: Optional[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to_pickup
if self.pickup:
yield self.pickup
yield from self.nav_to_drop_off
yield self.drop_off
yield from self.nav_to_home
yield self.land
if self.divert is not None:
yield self.divert
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
return self.drop_off
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
# TOT planning isn't really useful for transports. They're behind the front
# lines so no need to wait for escorts or for other missions to complete.
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
return None
@property
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target
@dataclass(frozen=True)
class CustomFlightPlan(FlightPlan):
custom_waypoints: List[FlightWaypoint]
@ -844,6 +884,8 @@ class FlightPlanBuilder:
return self.generate_tarcap(flight)
elif task == FlightType.AEWC:
return self.generate_aewc(flight)
elif task == FlightType.TRANSPORT:
return self.generate_transport(flight)
raise PlanningError(f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None:
@ -1023,6 +1065,8 @@ class FlightPlanBuilder:
"""
location = self.package.target
from game.transfers import Convoy
targets: List[StrikeTarget] = []
if isinstance(location, TheaterGroundObject):
for group in location.groups:
@ -1141,6 +1185,57 @@ class FlightPlanBuilder:
divert=builder.divert(flight.divert),
)
def generate_transport(self, flight: Flight) -> AirliftFlightPlan:
"""Generate an airlift flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
cargo = flight.cargo
if cargo is None:
raise PlanningError(
"Cannot plan transport mission for flight with no cargo."
)
altitude = feet(1500)
altitude_is_agl = True
builder = WaypointBuilder(flight, self.game, self.is_player)
pickup = None
nav_to_pickup = []
if cargo.origin != flight.departure:
pickup = builder.pickup(cargo.origin)
nav_to_pickup = builder.nav_path(
flight.departure.position,
cargo.origin.position,
altitude,
altitude_is_agl,
)
return AirliftFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.departure),
nav_to_pickup=nav_to_pickup,
pickup=pickup,
nav_to_drop_off=builder.nav_path(
cargo.origin.position,
cargo.destination.position,
altitude,
altitude_is_agl,
),
drop_off=builder.drop_off(cargo.destination),
nav_to_home=builder.nav_path(
cargo.origin.position,
flight.arrival.position,
altitude,
altitude_is_agl,
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
)
def racetrack_for_objective(
self, location: MissionTarget, barcap: bool
) -> Tuple[Point, Point]:

View File

@ -16,10 +16,9 @@ from dcs.mapping import Point
from dcs.unit import Unit
from dcs.unitgroup import Group, VehicleGroup
from game.transfers import Convoy
if TYPE_CHECKING:
from game import Game
from game.transfers import Convoy
from game.theater import (
ControlPoint,
@ -444,24 +443,69 @@ class WaypointBuilder:
return ingress, waypoint, egress
@staticmethod
def nav(position: Point, altitude: Distance) -> FlightWaypoint:
def pickup(control_point: ControlPoint) -> FlightWaypoint:
"""Creates a cargo pickup waypoint.
Args:
control_point: Pick up location.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PICKUP,
control_point.position.x,
control_point.position.y,
meters(0),
)
waypoint.alt_type = "RADIO"
waypoint.name = "PICKUP"
waypoint.description = f"Pick up cargo from {control_point}"
waypoint.pretty_name = "Pick up location"
return waypoint
@staticmethod
def drop_off(control_point: ControlPoint) -> FlightWaypoint:
"""Creates a cargo drop-off waypoint.
Args:
control_point: Drop-off location.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PICKUP,
control_point.position.x,
control_point.position.y,
meters(0),
)
waypoint.alt_type = "RADIO"
waypoint.name = "DROP OFF"
waypoint.description = f"Drop off cargo at {control_point}"
waypoint.pretty_name = "Drop off location"
return waypoint
@staticmethod
def nav(
position: Point, altitude: Distance, altitude_is_agl: bool = False
) -> FlightWaypoint:
"""Creates a navigation point.
Args:
position: Position of the waypoint.
altitude: Altitude of the waypoint.
altitude_is_agl: True for altitude is AGL. False if altitude is MSL.
"""
waypoint = FlightWaypoint(
FlightWaypointType.NAV, position.x, position.y, altitude
)
if altitude_is_agl:
waypoint.alt_type = "RADIO"
waypoint.name = "NAV"
waypoint.description = "NAV"
waypoint.pretty_name = "Nav"
return waypoint
def nav_path(self, a: Point, b: Point, altitude: Distance) -> List[FlightWaypoint]:
def nav_path(
self, a: Point, b: Point, altitude: Distance, altitude_is_agl: bool = False
) -> List[FlightWaypoint]:
path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
return [self.nav(self.perturb(p), altitude) for p in path]
return [self.nav(self.perturb(p), altitude, altitude_is_agl) for p in path]
def clean_nav_points(self, points: Iterable[Point]) -> Iterator[Point]:
# Examine a sliding window of three waypoints. `current` is the waypoint

View File

@ -15,7 +15,7 @@ from PySide2.QtGui import QIcon
from game import db
from game.game import Game
from game.theater.missiontarget import MissionTarget
from game.transfers import RoadTransferOrder
from game.transfers import TransferOrder
from gen.ato import AirTaskingOrder, Package
from gen.flights.flight import Flight
from gen.flights.traveltime import TotEstimator
@ -52,8 +52,13 @@ class DeletableChildModelManager:
ModelDict = Dict[DataType, ModelType]
def __init__(self, create_model: Callable[[DataType], ModelType]) -> None:
def __init__(
self,
create_model: Callable[[DataType, GameModel], ModelType],
game_model: GameModel,
) -> None:
self.create_model = create_model
self.game_model = game_model
self.models: DeletableChildModelManager.ModelDict = {}
def acquire(self, data: DataType) -> ModelType:
@ -64,7 +69,7 @@ class DeletableChildModelManager:
"""
if data in self.models:
return self.models[data]
model = self.create_model(data)
model = self.create_model(data, self.game_model)
self.models[data] = model
return model
@ -105,9 +110,10 @@ class PackageModel(QAbstractListModel):
tot_changed = Signal()
def __init__(self, package: Package) -> None:
def __init__(self, package: Package, game_model: GameModel) -> None:
super().__init__()
self.package = package
self.game_model = game_model
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return len(self.package.flights)
@ -154,14 +160,15 @@ class PackageModel(QAbstractListModel):
self.delete_flight(self.flight_at_index(index))
def delete_flight(self, flight: Flight) -> None:
"""Removes the given flight from the package.
If the flight is using claimed inventory, the caller is responsible for
returning that inventory.
"""
"""Removes the given flight from the package."""
index = self.package.flights.index(flight)
self.beginRemoveRows(QModelIndex(), index, index)
self.package.remove_flight(flight)
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)
self.endRemoveRows()
self.update_tot()
@ -210,11 +217,15 @@ class AtoModel(QAbstractListModel):
client_slots_changed = Signal()
def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None:
def __init__(self, game_model: GameModel, ato: AirTaskingOrder) -> None:
super().__init__()
self.game = game
self.game_model = game_model
self.ato = ato
self.package_models = DeletableChildModelManager(PackageModel)
self.package_models = DeletableChildModelManager(PackageModel, game_model)
@property
def game(self) -> Optional[Game]:
return self.game_model.game
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return len(self.ato.packages)
@ -249,6 +260,8 @@ class AtoModel(QAbstractListModel):
self.ato.remove_package(package)
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)
self.endRemoveRows()
# noinspection PyUnresolvedReferences
self.client_slots_changed.emit()
@ -257,20 +270,19 @@ class AtoModel(QAbstractListModel):
"""Returns the package at the given index."""
return self.ato.packages[index.row()]
def replace_from_game(self, game: Optional[Game], player: bool) -> None:
def replace_from_game(self, player: bool) -> None:
"""Updates the ATO object to match the updated game object.
If the game is None (as is the case when no game has been loaded), an
empty ATO will be used.
"""
self.beginResetModel()
self.game = game
self.package_models.clear()
if self.game is not None:
if player:
self.ato = game.blue_ato
self.ato = self.game.blue_ato
else:
self.ato = game.red_ato
self.ato = self.game.red_ato
else:
self.ato = AirTaskingOrder()
self.endResetModel()
@ -313,7 +325,7 @@ class TransferModel(QAbstractListModel):
return None
@staticmethod
def text_for_transfer(transfer: RoadTransferOrder) -> str:
def text_for_transfer(transfer: TransferOrder) -> str:
"""Returns the text that should be displayed for the transfer."""
count = sum(transfer.units.values())
origin = transfer.origin.name
@ -321,11 +333,11 @@ class TransferModel(QAbstractListModel):
return f"Transfer of {count} units from {origin} to {destination}"
@staticmethod
def icon_for_transfer(_transfer: RoadTransferOrder) -> Optional[QIcon]:
def icon_for_transfer(_transfer: TransferOrder) -> Optional[QIcon]:
"""Returns the icon that should be displayed for the transfer."""
return None
def new_transfer(self, transfer: RoadTransferOrder) -> None:
def new_transfer(self, transfer: TransferOrder) -> None:
"""Updates the game with the new unit transfer."""
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
# TODO: Needs to regenerate base inventory tab.
@ -334,13 +346,17 @@ class TransferModel(QAbstractListModel):
def cancel_transfer_at_index(self, index: QModelIndex) -> None:
"""Cancels the planned unit transfer at the given index."""
transfer = self.transfer_at_index(index)
self.beginRemoveRows(QModelIndex(), index.row(), index.row())
self.cancel_transfer(self.transfer_at_index(index))
def cancel_transfer(self, transfer: TransferOrder) -> None:
"""Cancels the planned unit transfer at the given index."""
index = self.game_model.game.transfers.index_of_transfer(transfer)
self.beginRemoveRows(QModelIndex(), index, index)
# TODO: Needs to regenerate base inventory tab.
self.game_model.game.transfers.cancel_transfer(transfer)
self.endRemoveRows()
def transfer_at_index(self, index: QModelIndex) -> RoadTransferOrder:
def transfer_at_index(self, index: QModelIndex) -> TransferOrder:
"""Returns the transfer located at the given index."""
return self.game_model.game.transfers.transfer_at_index(index.row())
@ -356,11 +372,11 @@ class GameModel:
self.game: Optional[Game] = game
self.transfer_model = TransferModel(self)
if self.game is None:
self.ato_model = AtoModel(self.game, AirTaskingOrder())
self.red_ato_model = AtoModel(self.game, AirTaskingOrder())
self.ato_model = AtoModel(self, AirTaskingOrder())
self.red_ato_model = AtoModel(self, AirTaskingOrder())
else:
self.ato_model = AtoModel(self.game, self.game.blue_ato)
self.red_ato_model = AtoModel(self.game, self.game.red_ato)
self.ato_model = AtoModel(self, self.game.blue_ato)
self.red_ato_model = AtoModel(self, self.game.red_ato)
def set(self, game: Optional[Game]) -> None:
"""Updates the managed Game object.
@ -371,5 +387,5 @@ class GameModel:
loaded.
"""
self.game = game
self.ato_model.replace_from_game(self.game, player=True)
self.red_ato_model.replace_from_game(self.game, player=False)
self.ato_model.replace_from_game(player=True)
self.red_ato_model.replace_from_game(player=False)

View File

@ -204,7 +204,6 @@ class QFlightList(QListView):
)
def delete_flight(self, index: QModelIndex) -> None:
self.game_model.game.aircraft_inventory.return_from_flight(self.selected_item)
self.package_model.delete_flight_at_index(index)
GameUpdateSignal.get_instance().redraw_flight_paths()

View File

@ -22,8 +22,7 @@ from PySide2.QtWidgets import (
QVBoxLayout,
)
from game.theater.supplyroutes import SupplyRoute
from game.transfers import RoadTransferOrder
from game.transfers import TransferOrder
from qt_ui.delegate_helpers import painter_context
from qt_ui.models import GameModel, TransferModel
@ -43,20 +42,14 @@ class TransferDelegate(QStyledItemDelegate):
return font
@staticmethod
def transfer(index: QModelIndex) -> RoadTransferOrder:
def transfer(index: QModelIndex) -> TransferOrder:
return index.data(TransferModel.TransferRole)
def first_row_text(self, index: QModelIndex) -> str:
return self.transfer_model.data(index, Qt.DisplayRole)
def second_row_text(self, index: QModelIndex) -> str:
transfer = self.transfer(index)
path = transfer.path()
if len(path) == 1:
turns = "1 turn"
else:
turns = f"{len(path)} turns"
return f"Currently at {transfer.position}. Arrives at destination in {turns}."
return self.transfer(index).description
def paint(
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex

View File

@ -15,6 +15,7 @@ from PySide2.QtWidgets import (
QGroupBox,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QScrollArea,
QSizePolicy,
@ -23,12 +24,16 @@ from PySide2.QtWidgets import (
QWidget,
)
from dcs.task import PinpointStrike
from dcs.unittype import UnitType
from dcs.unittype import FlyingType, UnitType, VehicleType
from game import Game, db
from game.inventory import ControlPointAircraftInventory
from game.theater import ControlPoint, SupplyRoute
from game.transfers import RoadTransferOrder
from qt_ui.models import GameModel
from game.transfers import AirliftOrder, RoadTransferOrder
from gen.ato import Package
from gen.flights.flight import Flight, FlightType
from gen.flights.flightplan import FlightPlanBuilder, PlanningError
from qt_ui.models import GameModel, PackageModel
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
@ -111,7 +116,7 @@ class TransferOptionsPanel(QVBoxLayout):
self.addLayout(QLabeledWidget("Destination:", self.source_combo_box))
self.airlift = QCheckBox()
self.airlift.toggled.connect(self.set_airlift)
self.addLayout(QLabeledWidget("Airlift (non-functional):", self.airlift))
self.addLayout(QLabeledWidget("Airlift (WIP):", self.airlift))
self.addWidget(
QLabel(
f"{airlift_capacity.total} airlift capacity "
@ -363,12 +368,112 @@ class NewUnitTransferDialog(QDialog):
)
transfers[unit_type] = count
self.game_model.transfer_model.new_transfer(
RoadTransferOrder(
if self.dest_panel.airlift.isChecked():
self.create_package_for_airlift(
self.transfer_panel.cp,
self.dest_panel.current,
transfers,
)
else:
transfer = RoadTransferOrder(
player=True,
origin=self.transfer_panel.cp,
destination=self.dest_panel.current,
units=transfers,
)
)
self.game_model.transfer_model.new_transfer(transfer)
self.close()
@staticmethod
def take_units(
units: Dict[Type[VehicleType], int], count: int
) -> Dict[Type[VehicleType], int]:
taken = {}
for unit_type, remaining in units.items():
take = min(remaining, count)
count -= take
units[unit_type] -= take
taken[unit_type] = take
if not count:
break
return taken
def create_airlift_flight(
self,
game: Game,
package_model: PackageModel,
unit_type: Type[FlyingType],
inventory: ControlPointAircraftInventory,
needed_capacity: int,
pickup: ControlPoint,
drop_off: ControlPoint,
units: Dict[Type[VehicleType], int],
) -> int:
available = inventory.available(unit_type)
# 4 is the max flight size in DCS.
flight_size = min(needed_capacity, available, 4)
flight = Flight(
package_model.package,
game.player_country,
unit_type,
flight_size,
FlightType.TRANSPORT,
game.settings.default_start_type,
departure=inventory.control_point,
arrival=inventory.control_point,
divert=None,
)
transfer = AirliftOrder(
player=True,
origin=pickup,
destination=drop_off,
units=self.take_units(units, flight_size),
flight=flight,
)
flight.cargo = transfer
package_model.add_flight(flight)
planner = FlightPlanBuilder(game, package_model.package, is_player=True)
try:
planner.populate_flight_plan(flight)
except PlanningError as ex:
package_model.delete_flight(flight)
logging.exception("Could not create flight")
QMessageBox.critical(
self, "Could not create flight", str(ex), QMessageBox.Ok
)
game.aircraft_inventory.claim_for_flight(flight)
self.game_model.transfer_model.new_transfer(transfer)
return flight_size
def create_package_for_airlift(
self,
pickup: ControlPoint,
drop_off: ControlPoint,
units: Dict[Type[VehicleType], int],
) -> None:
package = Package(target=drop_off, auto_asap=True)
package_model = PackageModel(package, self.game_model)
needed_capacity = sum(c for c in units.values())
game = self.game_model.game
for cp in game.theater.player_points():
inventory = game.aircraft_inventory.for_control_point(cp)
for unit_type, available in inventory.all_aircraft:
if unit_type.helicopter:
while available and needed_capacity:
flight_size = self.create_airlift_flight(
self.game_model.game,
package_model,
unit_type,
inventory,
needed_capacity,
pickup,
drop_off,
units,
)
available -= flight_size
needed_capacity -= flight_size
package_model.update_tot()
self.game_model.ato_model.add_package(package)

View File

@ -185,7 +185,6 @@ class QPackageDialog(QDialog):
try:
planner.populate_flight_plan(flight)
except PlanningError as ex:
self.game.aircraft_inventory.return_from_flight(flight)
self.package_model.delete_flight(flight)
logging.exception("Could not create flight")
QMessageBox.critical(
@ -201,7 +200,6 @@ class QPackageDialog(QDialog):
if flight is None:
logging.error(f"Cannot delete flight when no flight is selected.")
return
self.game.aircraft_inventory.return_from_flight(flight)
self.package_model.delete_flight(flight)
# noinspection PyUnresolvedReferences
self.package_changed.emit()
@ -216,7 +214,9 @@ class QNewPackageDialog(QPackageDialog):
def __init__(
self, game_model: GameModel, model: AtoModel, target: MissionTarget, parent=None
) -> None:
super().__init__(game_model, PackageModel(Package(target)), parent=parent)
super().__init__(
game_model, PackageModel(Package(target), game_model), parent=parent
)
self.ato_model = model
self.save_button = QPushButton("Save")