diff --git a/game/db.py b/game/db.py index 809a43b1..c091a399 100644 --- a/game/db.py +++ b/game/db.py @@ -1442,11 +1442,11 @@ def unit_task(unit: UnitType) -> Optional[Task]: return None -def find_unittype(for_task: Task, country_name: str) -> List[Type[UnitType]]: +def find_unittype(for_task: Type[MainTask], country_name: str) -> List[Type[UnitType]]: return [x for x in UNIT_BY_TASK[for_task] if x in FACTIONS[country_name].units] -MANPADS: List[VehicleType] = [ +MANPADS: List[Type[VehicleType]] = [ AirDefence.MANPADS_SA_18_Igla_Grouse, AirDefence.MANPADS_SA_18_Igla_S_Grouse, AirDefence.MANPADS_Stinger, diff --git a/game/game.py b/game/game.py index 22598504..9dcf5006 100644 --- a/game/game.py +++ b/game/game.py @@ -34,6 +34,7 @@ from .settings import Settings from .theater import ConflictTheater, ControlPoint, TheaterGroundObject from game.theater.theatergroundobject import MissileSiteGroundObject from .threatzones import ThreatZones +from .transfers import PendingTransfers from .unitmap import UnitMap from .weather import Conditions, TimeOfDay @@ -121,6 +122,8 @@ class Game: self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints) + self._transfers = PendingTransfers() + self.sanitize_sides() self.on_load() @@ -151,6 +154,14 @@ class Game: # Regenerate any state that was not persisted. self.on_load() + @property + def transfers(self) -> PendingTransfers: + try: + return self._transfers + except AttributeError: + self._transfers = PendingTransfers() + return self._transfers + def generate_conditions(self) -> Conditions: return Conditions.generate( self.theater, self.current_day, self.current_turn_time_of_day, self.settings @@ -264,6 +275,8 @@ class Game: for control_point in self.theater.controlpoints: control_point.process_turn(self) + self.transfers.complete_transfers() + self.process_enemy_income() self.process_player_income() diff --git a/game/transfers.py b/game/transfers.py new file mode 100644 index 00000000..cc2e7665 --- /dev/null +++ b/game/transfers.py @@ -0,0 +1,76 @@ +import logging +from dataclasses import dataclass +from typing import Dict, List, Type + +from dcs.unittype import VehicleType +from game.theater import ControlPoint + + +@dataclass +class TransferOrder: + """The base type of all transfer orders. + + A transfer order can transfer multiple units of multiple types. + """ + + #: The location the units are transferring from. + origin: ControlPoint + + #: The location the units are transferring to. + destination: ControlPoint + + #: True if the transfer order belongs to the player. + player: bool + + +@dataclass +class RoadTransferOrder(TransferOrder): + """A transfer order that moves units by road.""" + + #: The units being transferred. + units: Dict[Type[VehicleType], int] + + +class PendingTransfers: + def __init__(self) -> None: + self.pending_transfers: List[RoadTransferOrder] = [] + + @property + def pending_transfer_count(self) -> int: + return len(self.pending_transfers) + + def transfer_at_index(self, index: int) -> RoadTransferOrder: + 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 cancel_transfer(self, transfer: RoadTransferOrder) -> None: + self.pending_transfers.remove(transfer) + transfer.origin.base.commision_units(transfer.units) + + def complete_transfers(self) -> None: + for transfer in self.pending_transfers: + self.complete_transfer(transfer) + self.pending_transfers.clear() + + @staticmethod + def complete_transfer(transfer: RoadTransferOrder) -> None: + if transfer.player == transfer.destination.captured: + logging.info( + f"Units transferred from {transfer.origin.name} to " + f"{transfer.destination.name}" + ) + transfer.destination.base.commision_units(transfer.units) + elif transfer.player == transfer.origin.captured: + logging.info( + f"{transfer.destination.name} was captured. Transferring units are " + f"returning to {transfer.origin.name}" + ) + transfer.origin.base.commision_units(transfer.units) + else: + logging.info( + f"Both {transfer.origin.name} and {transfer.destination.name} were " + "captured. Units were surrounded and captured during transfer." + ) diff --git a/qt_ui/delegate_helpers.py b/qt_ui/delegate_helpers.py new file mode 100644 index 00000000..0c437310 --- /dev/null +++ b/qt_ui/delegate_helpers.py @@ -0,0 +1,13 @@ +from contextlib import contextmanager +from typing import ContextManager + +from PySide2.QtGui import QPainter + + +@contextmanager +def painter_context(painter: QPainter) -> ContextManager[None]: + try: + painter.save() + yield + finally: + painter.restore() diff --git a/qt_ui/models.py b/qt_ui/models.py index cdc594d6..bd7d2bc8 100644 --- a/qt_ui/models.py +++ b/qt_ui/models.py @@ -1,4 +1,6 @@ """Qt data models for game objects.""" +from __future__ import annotations + import datetime from typing import Any, Callable, Dict, Iterator, Optional, TypeVar @@ -12,11 +14,12 @@ 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 gen.ato import AirTaskingOrder, Package from gen.flights.flight import Flight from gen.flights.traveltime import TotEstimator from qt_ui.uiconstants import AIRCRAFT_ICONS -from game.theater.missiontarget import MissionTarget class DeletableChildModelManager: @@ -285,6 +288,63 @@ class AtoModel(QAbstractListModel): yield self.package_models.acquire(package) +class TransferModel(QAbstractListModel): + """The model for a ground unit transfer.""" + + TransferRole = Qt.UserRole + + def __init__(self, game_model: GameModel) -> None: + super().__init__() + self.game_model = game_model + + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + return self.game_model.game.transfers.pending_transfer_count + + def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any: + if not index.isValid(): + return None + transfer = self.transfer_at_index(index) + if role == Qt.DisplayRole: + return self.text_for_transfer(transfer) + if role == Qt.DecorationRole: + return self.icon_for_transfer(transfer) + elif role == TransferModel.TransferRole: + return transfer + return None + + @staticmethod + def text_for_transfer(transfer: RoadTransferOrder) -> str: + """Returns the text that should be displayed for the transfer.""" + count = sum(transfer.units.values()) + origin = transfer.origin.name + destination = transfer.destination.name + return f"Transfer of {count} units from {origin} to {destination}" + + @staticmethod + def icon_for_transfer(_transfer: RoadTransferOrder) -> Optional[QIcon]: + """Returns the icon that should be displayed for the transfer.""" + return None + + def new_transfer(self, transfer: RoadTransferOrder) -> None: + """Updates the game with the new unit transfer.""" + self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) + # TODO: Needs to regenerate base inventory tab. + self.game_model.game.transfers.new_transfer(transfer) + self.endInsertRows() + + 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()) + # 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: + """Returns the transfer located at the given index.""" + return self.game_model.game.transfers.transfer_at_index(index.row()) + + class GameModel: """A model for the Game object. @@ -294,6 +354,7 @@ class GameModel: def __init__(self, game: Optional[Game]) -> None: 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()) diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index 68834d28..f43657f7 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -4,6 +4,7 @@ from datetime import timedelta from typing import List, Optional from PySide2.QtWidgets import ( + QDialog, QFrame, QGroupBox, QHBoxLayout, @@ -22,6 +23,7 @@ from qt_ui.widgets.QFactionsInfos import QFactionsInfos from qt_ui.widgets.QIntelBox import QIntelBox from qt_ui.widgets.clientslots import MaxPlayerCount from qt_ui.windows.GameUpdateSignal import GameUpdateSignal +from qt_ui.windows.PendingTransfersDialog import PendingTransfersDialog from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow from qt_ui.windows.stats.QStatsWindow import QStatsWindow @@ -32,6 +34,8 @@ class QTopPanel(QFrame): def __init__(self, game_model: GameModel): super(QTopPanel, self).__init__() self.game_model = game_model + self.dialog: Optional[QDialog] = None + self.setMaximumHeight(70) self.init_ui() GameUpdateSignal.get_instance().gameupdated.connect(self.setGame) @@ -61,6 +65,11 @@ class QTopPanel(QFrame): self.factionsInfos = QFactionsInfos(self.game) + self.transfers = QPushButton("Transfers") + self.transfers.setDisabled(True) + self.transfers.setProperty("style", "btn-primary") + self.transfers.clicked.connect(self.open_transfers) + self.settings = QPushButton("Settings") self.settings.setDisabled(True) self.settings.setIcon(CONST.ICONS["Settings"]) @@ -77,6 +86,7 @@ class QTopPanel(QFrame): self.buttonBox = QGroupBox("Misc") self.buttonBoxLayout = QHBoxLayout() + self.buttonBoxLayout.addWidget(self.transfers) self.buttonBoxLayout.addWidget(self.settings) self.buttonBoxLayout.addWidget(self.statistics) self.buttonBox.setLayout(self.buttonBoxLayout) @@ -106,6 +116,7 @@ class QTopPanel(QFrame): if game is None: return + self.transfers.setEnabled(True) self.settings.setEnabled(True) self.statistics.setEnabled(True) @@ -121,13 +132,17 @@ class QTopPanel(QFrame): else: self.proceedButton.setEnabled(True) + def open_transfers(self): + self.dialog = PendingTransfersDialog(self.game_model) + self.dialog.show() + def openSettings(self): - self.subwindow = QSettingsWindow(self.game) - self.subwindow.show() + self.dialog = QSettingsWindow(self.game) + self.dialog.show() def openStatisticsWindow(self): - self.subwindow = QStatsWindow(self.game) - self.subwindow.show() + self.dialog = QStatsWindow(self.game) + self.dialog.show() def passTurn(self): start = timeit.default_timer() diff --git a/qt_ui/widgets/ato.py b/qt_ui/widgets/ato.py index fa5e7072..93b170bb 100644 --- a/qt_ui/widgets/ato.py +++ b/qt_ui/widgets/ato.py @@ -1,7 +1,6 @@ """Widgets for displaying air tasking orders.""" import logging -from contextlib import contextmanager -from typing import ContextManager, Optional +from typing import Optional from PySide2.QtCore import ( QItemSelectionModel, @@ -32,11 +31,11 @@ from PySide2.QtWidgets import ( QVBoxLayout, ) -from game import db from gen.ato import Package from gen.flights.flight import Flight from gen.flights.traveltime import TotEstimator from qt_ui.windows.GameUpdateSignal import GameUpdateSignal +from ..delegate_helpers import painter_context from ..models import AtoModel, GameModel, NullListModel, PackageModel @@ -312,15 +311,6 @@ class QFlightPanel(QGroupBox): self.flight_list.delete_flight(index) -@contextmanager -def painter_context(painter: QPainter) -> ContextManager[None]: - try: - painter.save() - yield - finally: - painter.restore() - - class PackageDelegate(QStyledItemDelegate): FONT_SIZE = 12 HMARGIN = 4 diff --git a/qt_ui/windows/PendingTransfersDialog.py b/qt_ui/windows/PendingTransfersDialog.py new file mode 100644 index 00000000..6892debf --- /dev/null +++ b/qt_ui/windows/PendingTransfersDialog.py @@ -0,0 +1,189 @@ +from typing import Optional + +from PySide2.QtCore import ( + QItemSelection, + QItemSelectionModel, + QModelIndex, + QSize, + Qt, +) +from PySide2.QtGui import QContextMenuEvent, QFont, QFontMetrics, QIcon, QPainter +from PySide2.QtWidgets import ( + QAbstractItemView, + QAction, + QDialog, + QHBoxLayout, + QListView, + QMenu, + QPushButton, + QStyle, + QStyleOptionViewItem, + QStyledItemDelegate, + QVBoxLayout, +) + +from game.transfers import RoadTransferOrder +from qt_ui.delegate_helpers import painter_context +from qt_ui.models import GameModel, TransferModel + + +class TransferDelegate(QStyledItemDelegate): + FONT_SIZE = 10 + HMARGIN = 4 + VMARGIN = 4 + + def __init__(self, transfer_model: TransferModel) -> None: + super().__init__() + self.transfer_model = transfer_model + + def get_font(self, option: QStyleOptionViewItem) -> QFont: + font = QFont(option.font) + font.setPointSize(self.FONT_SIZE) + return font + + @staticmethod + def transfer(index: QModelIndex) -> RoadTransferOrder: + 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) + return f"Currently at {transfer.origin}. Arrives at destination in 1 turn." + + def paint( + self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex + ) -> None: + # Draw the list item with all the default selection styling, but with an + # invalid index so text formatting is left to us. + super().paint(painter, option, QModelIndex()) + + rect = option.rect.adjusted( + self.HMARGIN, self.VMARGIN, -self.HMARGIN, -self.VMARGIN + ) + + with painter_context(painter): + painter.setFont(self.get_font(option)) + + icon: Optional[QIcon] = index.data(Qt.DecorationRole) + if icon is not None: + icon.paint( + painter, + rect, + Qt.AlignLeft | Qt.AlignVCenter, + self.icon_mode(option), + self.icon_state(option), + ) + + rect = rect.adjusted(self.icon_size(option).width() + self.HMARGIN, 0, 0, 0) + painter.drawText(rect, Qt.AlignLeft, self.first_row_text(index)) + line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2) + painter.drawText(line2, Qt.AlignLeft, self.second_row_text(index)) + + @staticmethod + def icon_mode(option: QStyleOptionViewItem) -> QIcon.Mode: + if not (option.state & QStyle.State_Enabled): + return QIcon.Disabled + elif option.state & QStyle.State_Selected: + return QIcon.Selected + elif option.state & QStyle.State_Active: + return QIcon.Active + return QIcon.Normal + + @staticmethod + def icon_state(option: QStyleOptionViewItem) -> QIcon.State: + return QIcon.On if option.state & QStyle.State_Open else QIcon.Off + + @staticmethod + def icon_size(option: QStyleOptionViewItem) -> QSize: + icon_size: Optional[QSize] = option.decorationSize + if icon_size is None: + return QSize(0, 0) + else: + return icon_size + + def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize: + left = self.icon_size(option).width() + self.HMARGIN + metrics = QFontMetrics(self.get_font(option)) + first = metrics.size(0, self.first_row_text(index)) + second = metrics.size(0, self.second_row_text(index)) + text_width = max(first.width(), second.width()) + return QSize( + left + text_width + 2 * self.HMARGIN, + first.height() + second.height() + 2 * self.VMARGIN, + ) + + +class PendingTransfersList(QListView): + """List view for displaying the pending unit transfers.""" + + def __init__(self, transfer_model: TransferModel) -> None: + super().__init__() + self.transfer_model = transfer_model + + self.setItemDelegate(TransferDelegate(self.transfer_model)) + self.setModel(self.transfer_model) + self.selectionModel().setCurrentIndex( + self.transfer_model.index(0, 0, QModelIndex()), QItemSelectionModel.Select + ) + + # self.setIconSize(QSize(91, 24)) + self.setSelectionBehavior(QAbstractItemView.SelectItems) + + def contextMenuEvent(self, event: QContextMenuEvent) -> None: + index = self.indexAt(event.pos()) + + menu = QMenu("Menu") + + delete_action = QAction("Cancel") + delete_action.triggered.connect(lambda: self.cancel_transfer(index)) + menu.addAction(delete_action) + + menu.exec_(event.globalPos()) + + def cancel_transfer(self, index: QModelIndex) -> None: + """Cancels the given transfer order.""" + self.transfer_model.cancel_transfer_at_index(index) + + +class PendingTransfersDialog(QDialog): + """Dialog window showing all scheduled transfers for the player.""" + + def __init__(self, game_model: GameModel, parent=None) -> None: + super().__init__(parent) + self.transfer_model = game_model.transfer_model + + self.setMinimumSize(1000, 440) + self.setWindowTitle(f"Pending Transfers") + # TODO: self.setWindowIcon() + + layout = QVBoxLayout() + self.setLayout(layout) + + self.transfer_list = PendingTransfersList(self.transfer_model) + self.transfer_list.selectionModel().selectionChanged.connect( + self.on_selection_changed + ) + layout.addWidget(self.transfer_list) + + button_layout = QHBoxLayout() + layout.addLayout(button_layout) + + button_layout.addStretch() + + self.cancel_button = QPushButton("Cancel Transfer") + self.cancel_button.setProperty("style", "btn-danger") + self.cancel_button.clicked.connect(self.on_cancel_transfer) + self.cancel_button.setEnabled(self.transfer_model.rowCount() > 0) + button_layout.addWidget(self.cancel_button) + + def on_cancel_transfer(self) -> None: + """Cancels the selected transfer order.""" + self.transfer_model.cancel_transfer_at_index(self.transfer_list.currentIndex()) + + def on_selection_changed( + self, selected: QItemSelection, _deselected: QItemSelection + ) -> None: + """Updates the state of the delete button.""" + self.cancel_button.setEnabled(not selected.empty()) diff --git a/qt_ui/windows/basemenu/NewUnitTransferDialog.py b/qt_ui/windows/basemenu/NewUnitTransferDialog.py new file mode 100644 index 00000000..4df54d31 --- /dev/null +++ b/qt_ui/windows/basemenu/NewUnitTransferDialog.py @@ -0,0 +1,310 @@ +from __future__ import annotations + +import logging +from collections import defaultdict +from typing import Callable, Dict, Type + +from PySide2.QtCore import Qt +from PySide2.QtWidgets import ( + QComboBox, + QDialog, + QFrame, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QPushButton, + QScrollArea, + QSizePolicy, + QSpacerItem, + QVBoxLayout, + QWidget, +) +from dcs.task import PinpointStrike +from dcs.unittype import UnitType + +from game import db +from game.theater import ControlPoint +from game.transfers import RoadTransferOrder +from qt_ui.models import GameModel +from qt_ui.widgets.QLabeledWidget import QLabeledWidget + + +class TransferDestinationComboBox(QComboBox): + def __init__(self, game_model: GameModel, origin: ControlPoint) -> None: + super().__init__() + + for cp in game_model.game.theater.controlpoints: + if cp != origin and cp.captured and not cp.is_global: + 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) + + +class TransferDestinationPanel(QVBoxLayout): + def __init__(self, label: str, origin: ControlPoint, game_model: GameModel) -> None: + super().__init__() + + self.source_combo_box = TransferDestinationComboBox(game_model, origin) + self.addLayout(QLabeledWidget(label, self.source_combo_box)) + + @property + def changed(self): + return self.source_combo_box.currentIndexChanged + + @property + def current(self) -> ControlPoint: + return self.source_combo_box.currentData() + + +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, game_model: GameModel) -> None: + super().__init__() + self.cp = cp + 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 + + 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 + + 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.dest_panel = TransferDestinationPanel("Destination:", origin, game_model) + self.dest_panel.changed.connect(self.on_destination_changed) + layout.addLayout(self.dest_panel) + + self.transfer_panel = ScrollingUnitTransferGrid(origin, game_model) + 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 on_destination_changed(self, index: int) -> 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, 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 + + self.game_model.transfer_model.new_transfer( + RoadTransferOrder( + player=True, + origin=self.transfer_panel.cp, + destination=self.dest_panel.current, + units=transfers, + ) + ) + self.close() diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index a4793b97..ec6f03dc 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -19,6 +19,7 @@ from qt_ui.uiconstants import EVENT_ICONS from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.basemenu.QBaseMenuTabs import QBaseMenuTabs from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour +from qt_ui.windows.basemenu.NewUnitTransferDialog import NewUnitTransferDialog class QBaseMenu2(QDialog): @@ -88,6 +89,11 @@ class QBaseMenu2(QDialog): runway_attack_button.setProperty("style", "btn-danger") runway_attack_button.clicked.connect(self.new_package) + if self.cp.captured and not self.cp.is_global: + transfer_button = QPushButton("Transfer Units") + bottom_row.addWidget(transfer_button) + transfer_button.clicked.connect(self.open_transfer_dialog) + self.budget_display = QLabel( QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.budget) ) @@ -180,5 +186,8 @@ class QBaseMenu2(QDialog): def new_package(self) -> None: Dialog.open_new_package_dialog(self.cp, parent=self.window()) + def open_transfer_dialog(self) -> None: + NewUnitTransferDialog(self.game_model, self.cp, parent=self.window()).show() + def update_budget(self, game: Game) -> None: self.budget_display.setText(QRecruitBehaviour.BUDGET_FORMAT.format(game.budget)) diff --git a/qt_ui/windows/basemenu/QRecruitBehaviour.py b/qt_ui/windows/basemenu/QRecruitBehaviour.py index 97ef9b2a..1e429895 100644 --- a/qt_ui/windows/basemenu/QRecruitBehaviour.py +++ b/qt_ui/windows/basemenu/QRecruitBehaviour.py @@ -1,17 +1,22 @@ import logging -from typing import Type +from typing import Callable, Set, Type from PySide2.QtCore import Qt from PySide2.QtWidgets import ( + QFrame, + QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLayout, QPushButton, + QScrollArea, QSizePolicy, QSpacerItem, + QVBoxLayout, + QWidget, ) -from dcs.unittype import UnitType +from dcs.unittype import FlyingType, UnitType from game import db from game.event import UnitsDeliveryEvent @@ -27,13 +32,11 @@ class QRecruitBehaviour: existing_units_labels = None bought_amount_labels = None maximum_units = -1 - recruitable_types = [] BUDGET_FORMAT = "Available Budget: ${:.2f}M" def __init__(self) -> None: self.bought_amount_labels = {} self.existing_units_labels = {} - self.recruitable_types = [] self.update_available_budget() @property @@ -195,9 +198,3 @@ class QRecruitBehaviour: Set the maximum number of units that can be bought """ self.maximum_units = maximum_units - - def set_recruitable_types(self, recruitables_types): - """ - Set the maximum number of units that can be bought - """ - self.recruitables_types = recruitables_types diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index 53f6cf69..970e1c6c 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -34,7 +34,6 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): # Determine maximum number of aircrafts that can be bought self.set_maximum_units(self.cp.total_aircraft_parking) - self.set_recruitable_types([CAP, CAS]) self.bought_amount_labels = {} self.existing_units_labels = {}