Implement manual squadron transfers.

Lightly tested but seems to work fine.

https://github.com/dcs-liberation/dcs_liberation/issues/1145
This commit is contained in:
Dan Albert 2021-08-28 18:09:33 -07:00
parent cd15de6d42
commit c2e5cba061
7 changed files with 148 additions and 43 deletions

View File

@ -53,6 +53,9 @@ class Squadron:
settings: Settings = field(hash=False, compare=False)
location: ControlPoint
destination: Optional[ControlPoint] = field(
init=False, hash=False, compare=False, default=None
)
owned_aircraft: int = field(init=False, hash=False, compare=False, default=0)
untasked_aircraft: int = field(init=False, hash=False, compare=False, default=0)
@ -168,6 +171,8 @@ class Squadron:
self._recruit_pilots(self.settings.squadron_pilot_limit)
def end_turn(self) -> None:
if self.destination is not None:
self.relocate_to(self.destination)
self.replenish_lost_pilots()
self.deliver_orders()
@ -280,6 +285,8 @@ class Squadron:
def relocate_to(self, destination: ControlPoint) -> None:
self.location = destination
if self.location == self.destination:
self.destination = None
def cancel_overflow_orders(self) -> None:
if self.pending_deliveries <= 0:
@ -297,6 +304,42 @@ class Squadron:
def max_fulfillable_aircraft(self) -> int:
return max(self.number_of_available_pilots, self.untasked_aircraft)
@property
def expected_size_next_turn(self) -> int:
return self.owned_aircraft + self.pending_deliveries
def plan_relocation(self, destination: ControlPoint) -> None:
if destination == self.location:
logging.warning(
f"Attempted to plan relocation of {self} to current location "
f"{destination}. Ignoring."
)
return
if destination == self.destination:
logging.warning(
f"Attempted to plan relocation of {self} to current destination "
f"{destination}. Ignoring."
)
return
if self.expected_size_next_turn >= destination.unclaimed_parking():
raise RuntimeError(f"Not enough parking for {self} at {destination}.")
if not destination.can_operate(self.aircraft):
raise RuntimeError(f"{self} cannot operate at {destination}.")
self.destination = destination
def cancel_relocation(self) -> None:
if self.destination is None:
logging.warning(
f"Attempted to cancel relocation of squadron with no transfer order. "
"Ignoring."
)
return
if self.expected_size_next_turn >= self.location.unclaimed_parking():
raise RuntimeError(f"Not enough parking for {self} at {self.location}.")
self.destination = None
@classmethod
def create_from(
cls,

View File

@ -728,13 +728,19 @@ class ControlPoint(MissionTarget, ABC):
def allocated_aircraft(self) -> AircraftAllocations:
present: dict[AircraftType, int] = defaultdict(int)
on_order: dict[AircraftType, int] = defaultdict(int)
transferring: dict[AircraftType, int] = defaultdict(int)
for squadron in self.squadrons:
present[squadron.aircraft] += squadron.owned_aircraft
# TODO: Only if this is the squadron destination, not location.
if squadron.destination is None:
on_order[squadron.aircraft] += squadron.pending_deliveries
else:
transferring[squadron.aircraft] -= squadron.owned_aircraft
for squadron in self.coalition.air_wing.iter_squadrons():
if squadron.destination == self:
on_order[squadron.aircraft] += squadron.pending_deliveries
transferring[squadron.aircraft] += squadron.owned_aircraft
# TODO: Implement squadron transfers.
return AircraftAllocations(present, on_order, transferring={})
return AircraftAllocations(present, on_order, transferring)
def allocated_ground_units(
self, transfers: PendingTransfers

17
qt_ui/errorreporter.py Normal file
View File

@ -0,0 +1,17 @@
import logging
from collections import Iterator
from contextlib import contextmanager
from typing import Type
from PySide2.QtWidgets import QDialog, QMessageBox
@contextmanager
def report_errors(
title: str, parent: QDialog, error_type: Type[Exception] = Exception
) -> Iterator[None]:
try:
yield
except error_type as ex:
logging.exception(title)
QMessageBox().critical(parent, title, str(ex), QMessageBox.Ok)

View File

@ -13,6 +13,7 @@ from PySide2.QtWidgets import QApplication, QSplashScreen
from dcs.payloads import PayloadDirectories
from game import Game, VERSION, persistency
from game.campaignloader.campaign import Campaign
from game.data.weapons import WeaponGroup, Pylon, Weapon
from game.db import FACTIONS
from game.dcs.aircrafttype import AircraftType
@ -27,7 +28,6 @@ from qt_ui import (
)
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QLiberationWindow import QLiberationWindow
from game.campaignloader.campaign import Campaign
from qt_ui.windows.newgame.QNewGameWizard import DEFAULT_BUDGET
from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
QLiberationFirstStartWindow,

View File

@ -10,7 +10,6 @@ from PySide2.QtCore import (
)
from PySide2.QtGui import QStandardItemModel, QStandardItem, QIcon
from PySide2.QtWidgets import (
QAbstractItemView,
QDialog,
QListView,
QVBoxLayout,
@ -32,38 +31,7 @@ from game.dcs.aircrafttype import AircraftType
from game.squadrons import AirWing, Pilot, Squadron
from game.theater import ControlPoint, ConflictTheater
from gen.flights.flight import FlightType
from qt_ui.models import AirWingModel, SquadronModel
from qt_ui.uiconstants import AIRCRAFT_ICONS
from qt_ui.windows.AirWingDialog import SquadronDelegate
from qt_ui.windows.SquadronDialog import SquadronDialog
class SquadronList(QListView):
"""List view for displaying the air wing's squadrons."""
def __init__(self, air_wing_model: AirWingModel) -> None:
super().__init__()
self.air_wing_model = air_wing_model
self.dialog: Optional[SquadronDialog] = None
self.setIconSize(QSize(91, 24))
self.setItemDelegate(SquadronDelegate(self.air_wing_model))
self.setModel(self.air_wing_model)
self.selectionModel().setCurrentIndex(
self.air_wing_model.index(0, 0, QModelIndex()), QItemSelectionModel.Select
)
# self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.doubleClicked.connect(self.on_double_click)
def on_double_click(self, index: QModelIndex) -> None:
if not index.isValid():
return
self.dialog = SquadronDialog(
SquadronModel(self.air_wing_model.squadron_at_index(index)), self
)
self.dialog.show()
class AllowedMissionTypeControls(QVBoxLayout):

View File

@ -17,6 +17,7 @@ from PySide2.QtWidgets import (
)
from game.squadrons import Squadron
from game.theater import ConflictTheater
from gen.flights.flight import Flight
from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.models import GameModel, AirWingModel, SquadronModel
@ -56,9 +57,10 @@ class SquadronDelegate(TwoColumnRowDelegate):
class SquadronList(QListView):
"""List view for displaying the air wing's squadrons."""
def __init__(self, air_wing_model: AirWingModel) -> None:
def __init__(self, air_wing_model: AirWingModel, theater: ConflictTheater) -> None:
super().__init__()
self.air_wing_model = air_wing_model
self.theater = theater
self.dialog: Optional[SquadronDialog] = None
self.setIconSize(QSize(91, 24))
@ -76,7 +78,9 @@ class SquadronList(QListView):
if not index.isValid():
return
self.dialog = SquadronDialog(
SquadronModel(self.air_wing_model.squadron_at_index(index)), self
SquadronModel(self.air_wing_model.squadron_at_index(index)),
self.theater,
self,
)
self.dialog.show()
@ -194,7 +198,10 @@ class AirWingTabs(QTabWidget):
def __init__(self, game_model: GameModel) -> None:
super().__init__()
self.addTab(SquadronList(game_model.blue_air_wing_model), "Squadrons")
self.addTab(
SquadronList(game_model.blue_air_wing_model, game_model.game.theater),
"Squadrons",
)
self.addTab(AirInventoryView(game_model), "Inventory")

View File

@ -1,5 +1,5 @@
import logging
from typing import Callable
from typing import Callable, Iterator, Optional
from PySide2.QtCore import (
QItemSelectionModel,
@ -16,11 +16,14 @@ from PySide2.QtWidgets import (
QHBoxLayout,
QLabel,
QCheckBox,
QComboBox,
)
from game.squadrons import Pilot
from game.squadrons import Pilot, Squadron
from game.theater import ControlPoint, ConflictTheater
from gen.flights.flight import FlightType
from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.errorreporter import report_errors
from qt_ui.models import SquadronModel
@ -90,10 +93,50 @@ class AutoAssignedTaskControls(QVBoxLayout):
self.squadron_model.set_auto_assignable(task, checked)
class SquadronDestinationComboBox(QComboBox):
def __init__(self, squadron: Squadron, theater: ConflictTheater) -> None:
super().__init__()
self.squadron = squadron
self.theater = theater
room = squadron.location.unclaimed_parking()
self.addItem(
f"Remain at {squadron.location} (room for {room} more aircraft)", None
)
selected_index: Optional[int] = None
for idx, destination in enumerate(sorted(self.iter_destinations(), key=str), 1):
if destination == squadron.destination:
selected_index = idx
room = destination.unclaimed_parking()
self.addItem(
f"Transfer to {destination} (room for {room} more aircraft)",
destination,
)
if squadron.destination is None:
selected_index = 0
if selected_index is not None:
self.setCurrentIndex(selected_index)
def iter_destinations(self) -> Iterator[ControlPoint]:
size = self.squadron.expected_size_next_turn
for control_point in self.theater.control_points_for(self.squadron.player):
if control_point == self:
continue
if not control_point.can_operate(self.squadron.aircraft):
continue
if control_point.unclaimed_parking() < size:
continue
yield control_point
class SquadronDialog(QDialog):
"""Dialog window showing a squadron."""
def __init__(self, squadron_model: SquadronModel, parent) -> None:
def __init__(
self, squadron_model: SquadronModel, theater: ConflictTheater, parent
) -> None:
super().__init__(parent)
self.squadron_model = squadron_model
@ -117,6 +160,15 @@ class SquadronDialog(QDialog):
columns.addWidget(self.pilot_list)
button_panel = QHBoxLayout()
self.transfer_destination = SquadronDestinationComboBox(
squadron_model.squadron, theater
)
self.transfer_destination.currentIndexChanged.connect(
self.on_destination_changed
)
button_panel.addWidget(self.transfer_destination)
button_panel.addStretch()
layout.addLayout(button_panel)
@ -132,6 +184,18 @@ class SquadronDialog(QDialog):
self.toggle_leave_button.clicked.connect(self.toggle_leave)
button_panel.addWidget(self.toggle_leave_button, alignment=Qt.AlignRight)
@property
def squadron(self) -> Squadron:
return self.squadron_model.squadron
def on_destination_changed(self, index: int) -> None:
with report_errors("Could not change squadron destination", self):
destination = self.transfer_destination.itemData(index)
if destination is None:
self.squadron.cancel_relocation()
else:
self.squadron.plan_relocation(destination)
def check_disabled_button_states(
self, button: QPushButton, index: QModelIndex
) -> bool: