mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
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:
parent
cd15de6d42
commit
c2e5cba061
@ -53,6 +53,9 @@ class Squadron:
|
|||||||
settings: Settings = field(hash=False, compare=False)
|
settings: Settings = field(hash=False, compare=False)
|
||||||
|
|
||||||
location: ControlPoint
|
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)
|
owned_aircraft: int = field(init=False, hash=False, compare=False, default=0)
|
||||||
untasked_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)
|
self._recruit_pilots(self.settings.squadron_pilot_limit)
|
||||||
|
|
||||||
def end_turn(self) -> None:
|
def end_turn(self) -> None:
|
||||||
|
if self.destination is not None:
|
||||||
|
self.relocate_to(self.destination)
|
||||||
self.replenish_lost_pilots()
|
self.replenish_lost_pilots()
|
||||||
self.deliver_orders()
|
self.deliver_orders()
|
||||||
|
|
||||||
@ -280,6 +285,8 @@ class Squadron:
|
|||||||
|
|
||||||
def relocate_to(self, destination: ControlPoint) -> None:
|
def relocate_to(self, destination: ControlPoint) -> None:
|
||||||
self.location = destination
|
self.location = destination
|
||||||
|
if self.location == self.destination:
|
||||||
|
self.destination = None
|
||||||
|
|
||||||
def cancel_overflow_orders(self) -> None:
|
def cancel_overflow_orders(self) -> None:
|
||||||
if self.pending_deliveries <= 0:
|
if self.pending_deliveries <= 0:
|
||||||
@ -297,6 +304,42 @@ class Squadron:
|
|||||||
def max_fulfillable_aircraft(self) -> int:
|
def max_fulfillable_aircraft(self) -> int:
|
||||||
return max(self.number_of_available_pilots, self.untasked_aircraft)
|
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
|
@classmethod
|
||||||
def create_from(
|
def create_from(
|
||||||
cls,
|
cls,
|
||||||
|
|||||||
@ -728,13 +728,19 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
def allocated_aircraft(self) -> AircraftAllocations:
|
def allocated_aircraft(self) -> AircraftAllocations:
|
||||||
present: dict[AircraftType, int] = defaultdict(int)
|
present: dict[AircraftType, int] = defaultdict(int)
|
||||||
on_order: dict[AircraftType, int] = defaultdict(int)
|
on_order: dict[AircraftType, int] = defaultdict(int)
|
||||||
|
transferring: dict[AircraftType, int] = defaultdict(int)
|
||||||
for squadron in self.squadrons:
|
for squadron in self.squadrons:
|
||||||
present[squadron.aircraft] += squadron.owned_aircraft
|
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
|
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(
|
def allocated_ground_units(
|
||||||
self, transfers: PendingTransfers
|
self, transfers: PendingTransfers
|
||||||
|
|||||||
17
qt_ui/errorreporter.py
Normal file
17
qt_ui/errorreporter.py
Normal 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)
|
||||||
@ -13,6 +13,7 @@ from PySide2.QtWidgets import QApplication, QSplashScreen
|
|||||||
from dcs.payloads import PayloadDirectories
|
from dcs.payloads import PayloadDirectories
|
||||||
|
|
||||||
from game import Game, VERSION, persistency
|
from game import Game, VERSION, persistency
|
||||||
|
from game.campaignloader.campaign import Campaign
|
||||||
from game.data.weapons import WeaponGroup, Pylon, Weapon
|
from game.data.weapons import WeaponGroup, Pylon, Weapon
|
||||||
from game.db import FACTIONS
|
from game.db import FACTIONS
|
||||||
from game.dcs.aircrafttype import AircraftType
|
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.GameUpdateSignal import GameUpdateSignal
|
||||||
from qt_ui.windows.QLiberationWindow import QLiberationWindow
|
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.newgame.QNewGameWizard import DEFAULT_BUDGET
|
||||||
from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
|
from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
|
||||||
QLiberationFirstStartWindow,
|
QLiberationFirstStartWindow,
|
||||||
|
|||||||
@ -10,7 +10,6 @@ from PySide2.QtCore import (
|
|||||||
)
|
)
|
||||||
from PySide2.QtGui import QStandardItemModel, QStandardItem, QIcon
|
from PySide2.QtGui import QStandardItemModel, QStandardItem, QIcon
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QAbstractItemView,
|
|
||||||
QDialog,
|
QDialog,
|
||||||
QListView,
|
QListView,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
@ -32,38 +31,7 @@ from game.dcs.aircrafttype import AircraftType
|
|||||||
from game.squadrons import AirWing, Pilot, Squadron
|
from game.squadrons import AirWing, Pilot, Squadron
|
||||||
from game.theater import ControlPoint, ConflictTheater
|
from game.theater import ControlPoint, ConflictTheater
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
from qt_ui.models import AirWingModel, SquadronModel
|
|
||||||
from qt_ui.uiconstants import AIRCRAFT_ICONS
|
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):
|
class AllowedMissionTypeControls(QVBoxLayout):
|
||||||
|
|||||||
@ -17,6 +17,7 @@ from PySide2.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from game.squadrons import Squadron
|
from game.squadrons import Squadron
|
||||||
|
from game.theater import ConflictTheater
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
from qt_ui.delegates import TwoColumnRowDelegate
|
from qt_ui.delegates import TwoColumnRowDelegate
|
||||||
from qt_ui.models import GameModel, AirWingModel, SquadronModel
|
from qt_ui.models import GameModel, AirWingModel, SquadronModel
|
||||||
@ -56,9 +57,10 @@ class SquadronDelegate(TwoColumnRowDelegate):
|
|||||||
class SquadronList(QListView):
|
class SquadronList(QListView):
|
||||||
"""List view for displaying the air wing's squadrons."""
|
"""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__()
|
super().__init__()
|
||||||
self.air_wing_model = air_wing_model
|
self.air_wing_model = air_wing_model
|
||||||
|
self.theater = theater
|
||||||
self.dialog: Optional[SquadronDialog] = None
|
self.dialog: Optional[SquadronDialog] = None
|
||||||
|
|
||||||
self.setIconSize(QSize(91, 24))
|
self.setIconSize(QSize(91, 24))
|
||||||
@ -76,7 +78,9 @@ class SquadronList(QListView):
|
|||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return
|
return
|
||||||
self.dialog = SquadronDialog(
|
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()
|
self.dialog.show()
|
||||||
|
|
||||||
@ -194,7 +198,10 @@ class AirWingTabs(QTabWidget):
|
|||||||
def __init__(self, game_model: GameModel) -> None:
|
def __init__(self, game_model: GameModel) -> None:
|
||||||
super().__init__()
|
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")
|
self.addTab(AirInventoryView(game_model), "Inventory")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Callable
|
from typing import Callable, Iterator, Optional
|
||||||
|
|
||||||
from PySide2.QtCore import (
|
from PySide2.QtCore import (
|
||||||
QItemSelectionModel,
|
QItemSelectionModel,
|
||||||
@ -16,11 +16,14 @@ from PySide2.QtWidgets import (
|
|||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QLabel,
|
QLabel,
|
||||||
QCheckBox,
|
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 gen.flights.flight import FlightType
|
||||||
from qt_ui.delegates import TwoColumnRowDelegate
|
from qt_ui.delegates import TwoColumnRowDelegate
|
||||||
|
from qt_ui.errorreporter import report_errors
|
||||||
from qt_ui.models import SquadronModel
|
from qt_ui.models import SquadronModel
|
||||||
|
|
||||||
|
|
||||||
@ -90,10 +93,50 @@ class AutoAssignedTaskControls(QVBoxLayout):
|
|||||||
self.squadron_model.set_auto_assignable(task, checked)
|
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):
|
class SquadronDialog(QDialog):
|
||||||
"""Dialog window showing a squadron."""
|
"""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)
|
super().__init__(parent)
|
||||||
self.squadron_model = squadron_model
|
self.squadron_model = squadron_model
|
||||||
|
|
||||||
@ -117,6 +160,15 @@ class SquadronDialog(QDialog):
|
|||||||
columns.addWidget(self.pilot_list)
|
columns.addWidget(self.pilot_list)
|
||||||
|
|
||||||
button_panel = QHBoxLayout()
|
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()
|
button_panel.addStretch()
|
||||||
layout.addLayout(button_panel)
|
layout.addLayout(button_panel)
|
||||||
|
|
||||||
@ -132,6 +184,18 @@ class SquadronDialog(QDialog):
|
|||||||
self.toggle_leave_button.clicked.connect(self.toggle_leave)
|
self.toggle_leave_button.clicked.connect(self.toggle_leave)
|
||||||
button_panel.addWidget(self.toggle_leave_button, alignment=Qt.AlignRight)
|
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(
|
def check_disabled_button_states(
|
||||||
self, button: QPushButton, index: QModelIndex
|
self, button: QPushButton, index: QModelIndex
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user