mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
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:
parent
b65d178cf1
commit
e9ff554f39
@ -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,
|
||||
|
||||
13
game/game.py
13
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()
|
||||
|
||||
76
game/transfers.py
Normal file
76
game/transfers.py
Normal 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
13
qt_ui/delegate_helpers.py
Normal 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()
|
||||
@ -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())
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
189
qt_ui/windows/PendingTransfersDialog.py
Normal file
189
qt_ui/windows/PendingTransfersDialog.py
Normal 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())
|
||||
310
qt_ui/windows/basemenu/NewUnitTransferDialog.py
Normal file
310
qt_ui/windows/basemenu/NewUnitTransferDialog.py
Normal 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()
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user