Basic implementation of road based transfers.

This adds the models and UIs for creating ground unit transfer orders.
Most of the feature is still missing:

* The AI doesn't do them.
* Transfers can move across the whole map in one turn.
* Transfers between disconnected bases are allowed.
* Transfers are not modeled in the simulation, so they can't be
  interdicted.

https://github.com/Khopa/dcs_liberation/issues/824
This commit is contained in:
Dan Albert 2021-02-15 12:49:36 -08:00
parent b65d178cf1
commit e9ff554f39
12 changed files with 702 additions and 30 deletions

View File

@ -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,

View File

@ -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()

76
game/transfers.py Normal file
View File

@ -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."
)

13
qt_ui/delegate_helpers.py Normal file
View File

@ -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()

View File

@ -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())

View File

@ -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()

View File

@ -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

View File

@ -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())

View File

@ -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"<b>{self.quantity}</b>")
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(
"<b>"
+ db.unit_get_expanded_info(
self.game_model.game.player_country, unit_type, "name"
)
+ "</b>"
)
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()

View File

@ -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))

View File

@ -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: <b>${:.2f}M</b>"
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

View File

@ -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 = {}