from __future__ import annotations import logging from collections import defaultdict from dataclasses import dataclass from typing import Callable, Dict, Type from PySide2.QtCore import Qt from PySide2.QtWidgets import ( QCheckBox, QComboBox, QDialog, QFrame, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QMessageBox, QPushButton, QScrollArea, QSizePolicy, QSpacerItem, QVBoxLayout, QWidget, ) from dcs.task import PinpointStrike 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 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 class TransferDestinationComboBox(QComboBox): def __init__(self, origin: ControlPoint) -> None: super().__init__() for cp in SupplyRoute.for_control_point(origin): if cp != origin and cp.captured: self.addItem(cp.name, cp) self.model().sort(0) self.setCurrentIndex(0) class UnitTransferList(QFrame): def __init__(self, cp: ControlPoint, game_model: GameModel): super().__init__(self) self.cp = cp self.game_model = game_model self.bought_amount_labels = {} self.existing_units_labels = {} main_layout = QVBoxLayout() self.setLayout(main_layout) scroll_content = QWidget() task_box_layout = QGridLayout() scroll_content.setLayout(task_box_layout) units_column = sorted( cp.base.armor, key=lambda u: db.unit_get_expanded_info( self.game_model.game.player_country, u, "name" ), ) count = 0 for count, unit_type in enumerate(units_column): self.add_purchase_row(unit_type, task_box_layout, count) stretch = QVBoxLayout() stretch.addStretch() task_box_layout.addLayout(stretch, count, 0) scroll_content.setLayout(task_box_layout) scroll = QScrollArea() scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) scroll.setWidgetResizable(True) scroll.setWidget(scroll_content) 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, origin: ControlPoint, airlift_capacity: AirliftCapacity) -> None: super().__init__() self.source_combo_box = TransferDestinationComboBox(origin) self.addLayout(QLabeledWidget("Destination:", self.source_combo_box)) self.airlift = QCheckBox() self.airlift.toggled.connect(self.set_airlift) self.addLayout(QLabeledWidget("Airlift (WIP):", 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): return self.source_combo_box.currentIndexChanged @property def current(self) -> ControlPoint: return self.source_combo_box.currentData() def set_airlift(self, value: bool) -> None: pass class TransferControls(QGroupBox): def __init__( self, increase_text: str, on_increase: Callable[[TransferControls], None], decrease_text: str, on_decrease: Callable[[TransferControls], None], initial_amount: int = 0, disabled: bool = False, ) -> None: super().__init__() self.quantity = initial_amount self.setProperty("style", "buy-box") self.setMaximumHeight(36) self.setMinimumHeight(36) layout = QHBoxLayout() self.setLayout(layout) decrease = QPushButton(decrease_text) decrease.setProperty("style", "btn-sell") decrease.setDisabled(disabled) decrease.setMinimumSize(16, 16) decrease.setMaximumSize(16, 16) decrease.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) decrease.clicked.connect(lambda: on_decrease(self)) layout.addWidget(decrease) self.count_label = QLabel() self.count_label.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) ) self.set_quantity(initial_amount) layout.addWidget(self.count_label) increase = QPushButton(increase_text) increase.setProperty("style", "btn-buy") increase.setDisabled(disabled) increase.setMinimumSize(16, 16) increase.setMaximumSize(16, 16) increase.clicked.connect(lambda: on_increase(self)) increase.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) layout.addWidget(increase) def set_quantity(self, quantity: int) -> None: self.quantity = quantity self.count_label.setText(f"{self.quantity}") class ScrollingUnitTransferGrid(QFrame): def __init__( self, cp: ControlPoint, airlift: bool, airlift_capacity: AirliftCapacity, 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) main_layout = QVBoxLayout() scroll_content = QWidget() task_box_layout = QGridLayout() unit_types = set( db.find_unittype(PinpointStrike, self.game_model.game.player_name) ) sorted_units = sorted( {u for u in unit_types if self.cp.base.total_units_of_type(u)}, key=lambda u: db.unit_get_expanded_info( self.game_model.game.player_country, u, "name" ), ) for row, unit_type in enumerate(sorted_units): self.add_unit_row(unit_type, task_box_layout, row) stretch = QVBoxLayout() stretch.addStretch() task_box_layout.addLayout(stretch, task_box_layout.count(), 0) scroll_content.setLayout(task_box_layout) scroll = QScrollArea() scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) scroll.setWidgetResizable(True) scroll.setWidget(scroll_content) main_layout.addWidget(scroll) self.setLayout(main_layout) def add_unit_row( self, unit_type: Type[UnitType], layout: QGridLayout, row: int, ) -> None: exist = QGroupBox() exist.setProperty("style", "buy-box") exist.setMaximumHeight(36) exist.setMinimumHeight(36) origin_inventory_layout = QHBoxLayout() exist.setLayout(origin_inventory_layout) origin_inventory = self.cp.base.total_units_of_type(unit_type) unit_name = QLabel( "" + db.unit_get_expanded_info( self.game_model.game.player_country, unit_type, "name" ) + "" ) unit_name.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) ) origin_inventory_label = QLabel(str(origin_inventory)) origin_inventory_label.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) ) def increase(controls: TransferControls): nonlocal origin_inventory nonlocal origin_inventory_label 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]) origin_inventory_label.setText(str(origin_inventory)) def decrease(controls: TransferControls): nonlocal origin_inventory nonlocal origin_inventory_label 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]) origin_inventory_label.setText(str(origin_inventory)) transfer_controls = TransferControls("->", increase, "<-", decrease) origin_inventory_layout.addWidget(unit_name) origin_inventory_layout.addItem( QSpacerItem(20, 0, QSizePolicy.Minimum, QSizePolicy.Minimum) ) origin_inventory_layout.addWidget(origin_inventory_label) origin_inventory_layout.addItem( QSpacerItem(20, 0, QSizePolicy.Minimum, QSizePolicy.Minimum) ) layout.addWidget(exist, row, 1) layout.addWidget(transfer_controls, row, 2) class NewUnitTransferDialog(QDialog): def __init__( self, game_model: GameModel, origin: ControlPoint, parent=None, ) -> None: super().__init__(parent) self.origin = origin self.setWindowTitle(f"New unit transfer from {origin.name}") self.game_model = game_model layout = QVBoxLayout() self.setLayout(layout) self.airlift_capacity = AirliftCapacity.to_control_point(game_model.game) self.dest_panel = TransferOptionsPanel(origin, self.airlift_capacity) self.dest_panel.changed.connect(self.rebuild_transfers) layout.addLayout(self.dest_panel) self.transfer_panel = ScrollingUnitTransferGrid( origin, airlift=False, airlift_capacity=self.airlift_capacity, game_model=game_model, ) self.dest_panel.airlift.toggled.connect(self.rebuild_transfers) layout.addWidget(self.transfer_panel) self.submit_button = QPushButton("Create Transfer Order", parent=self) self.submit_button.clicked.connect(self.on_submit) 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: transfers = {} for unit_type, count in self.transfer_panel.transfers.items(): if not count: continue logging.info( f"Transferring {count} {unit_type.id} from " f"{self.transfer_panel.cp.name} to {self.dest_panel.current.name}" ) transfers[unit_type] = count 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)