Show parking capacities in air wing config.

This does show the theoretical parking use of full squadrons even when
the new rules are not enabled. Since limits can be enabled manually
later in the game, it's still useful information, even if it's a bit
misleading.

https://github.com/dcs-liberation/dcs_liberation/issues/2910
This commit is contained in:
Dan Albert 2023-05-31 01:09:10 -07:00
parent 56f93c76eb
commit cb61dfccc4
3 changed files with 112 additions and 16 deletions

View File

@ -17,6 +17,7 @@ Saves from 7.0.0 are compatible with 7.1.0
* **[Mission Generation]** Added option to prevent scud and V2 sites from firing at the start of the mission.
* **[Mission Generation]** Added settings for controlling number of tactical commander, observer, JTAC, and game master slots.
* **[Mission Planning]** Per-flight TOT offsets can now be set in the flight details UI. This allows individual flights to be scheduled ahead of or behind the rest of the package.
* **[UI]** Parking capacity of each squadron's base is now shown during air wing configuration to avoid overcrowding bases when beginning the game with full squadrons.
## Fixes

View File

@ -5,7 +5,8 @@ import random
from collections.abc import Iterable
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, Sequence, TYPE_CHECKING
from typing import Optional, Sequence, TYPE_CHECKING, Any
from uuid import uuid4, UUID
from faker import Faker
@ -13,6 +14,7 @@ from game.ato import Flight, FlightType, Package
from game.settings import AutoAtoBehavior, Settings
from .pilot import Pilot, PilotStatus
from ..db.database import Database
from ..savecompat import has_save_compat_for
from ..utils import meters
if TYPE_CHECKING:
@ -26,6 +28,8 @@ if TYPE_CHECKING:
@dataclass
class Squadron:
id: UUID = field(init=False, default_factory=uuid4)
name: str
nickname: Optional[str]
country: str
@ -61,21 +65,24 @@ class Squadron:
untasked_aircraft: int = field(init=False, hash=False, compare=False, default=0)
pending_deliveries: int = field(init=False, hash=False, compare=False, default=0)
@has_save_compat_for(7)
def __setstate__(self, state: dict[str, Any]) -> None:
if "id" not in state:
state["id"] = uuid4()
self.__dict__.update(state)
def __str__(self) -> str:
if self.nickname is None:
return self.name
return f'{self.name} "{self.nickname}"'
def __hash__(self) -> int:
return hash(
(
self.name,
self.nickname,
self.country,
self.role,
self.aircraft,
)
)
return hash(self.id)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Squadron):
return False
return self.id == other.id
@property
def player(self) -> bool:

View File

@ -1,3 +1,4 @@
from collections import defaultdict
from typing import Iterable, Iterator, Optional
from PySide6.QtCore import (
@ -150,6 +151,41 @@ class SquadronSizeSpinner(QSpinBox):
# return size
class AirWingConfigParkingTracker(QWidget):
allocation_changed = Signal()
def __init__(self, squadrons: Iterable[Squadron]) -> None:
super().__init__()
self.by_cp: dict[ControlPoint, set[Squadron]] = defaultdict(set)
for squadron in squadrons:
self.add_squadron(squadron)
def add_squadron(self, squadron: Squadron) -> None:
self.by_cp[squadron.location].add(squadron)
self.signal_change()
def remove_squadron(self, squadron: Squadron) -> None:
self.by_cp[squadron.location].remove(squadron)
self.signal_change()
def relocate_squadron(
self,
squadron: Squadron,
prior_location: ControlPoint,
new_location: ControlPoint,
) -> None:
self.by_cp[prior_location].remove(squadron)
self.by_cp[new_location].add(squadron)
squadron.relocate_to(new_location)
self.signal_change()
def used_parking_at(self, control_point: ControlPoint) -> int:
return sum(s.max_size for s in self.by_cp[control_point])
def signal_change(self) -> None:
self.allocation_changed.emit()
class SquadronConfigurationBox(QGroupBox):
remove_squadron_signal = Signal(Squadron)
@ -158,11 +194,13 @@ class SquadronConfigurationBox(QGroupBox):
game: Game,
coalition: Coalition,
squadron: Squadron,
parking_tracker: AirWingConfigParkingTracker,
) -> None:
super().__init__()
self.game = game
self.coalition = coalition
self.squadron = squadron
self.parking_tracker = parking_tracker
columns = QHBoxLayout()
self.setLayout(columns)
@ -200,6 +238,7 @@ class SquadronConfigurationBox(QGroupBox):
left_column.addLayout(size_column)
size_column.addWidget(QLabel("Max size:"))
self.max_size_selector = SquadronSizeSpinner(self.squadron.max_size, self)
self.max_size_selector.valueChanged.connect(self.update_max_size)
size_column.addWidget(self.max_size_selector)
task_column = QVBoxLayout()
@ -214,8 +253,14 @@ class SquadronConfigurationBox(QGroupBox):
squadron.location,
squadron.aircraft,
)
self.base_selector.currentIndexChanged.connect(self.relocate_squadron)
left_column.addWidget(self.base_selector)
self.parking_label = QLabel()
self.update_parking_label()
self.parking_tracker.allocation_changed.connect(self.update_parking_label)
left_column.addWidget(self.parking_label)
if not squadron.player and squadron.aircraft.flyable:
player_label = QLabel("Player slots not available for opfor")
elif not squadron.aircraft.flyable:
@ -266,9 +311,26 @@ class SquadronConfigurationBox(QGroupBox):
self.player_list.setText(
"<br />".join(p.name for p in self.claim_players_from_squadron())
)
self.update_parking_label()
finally:
self.blockSignals(old_state)
def update_parking_label(self) -> None:
self.parking_label.setText(
f"{self.parking_tracker.used_parking_at(self.squadron.location)}/"
f"{self.squadron.location.total_aircraft_parking}"
)
def update_max_size(self) -> None:
self.squadron.max_size = self.max_size_selector.value()
self.parking_tracker.signal_change()
def relocate_squadron(self) -> None:
location = self.base_selector.currentData()
self.parking_tracker.relocate_squadron(
self.squadron, self.squadron.location, location
)
def remove_from_squadron_config(self) -> None:
self.remove_squadron_signal.emit(self.squadron)
@ -321,6 +383,7 @@ class SquadronConfigurationBox(QGroupBox):
self.squadron = new_squadron
self.bind_data()
self.mission_types.replace_squadron(self.squadron)
self.parking_tracker.signal_change()
def reset_title(self) -> None:
self.setTitle(f"{self.name_edit.text()} - {self.squadron.aircraft}")
@ -361,11 +424,13 @@ class SquadronConfigurationLayout(QVBoxLayout):
game: Game,
coalition: Coalition,
squadrons: list[Squadron],
parking_tracker: AirWingConfigParkingTracker,
) -> None:
super().__init__()
self.game = game
self.coalition = coalition
self.squadron_configs = []
self.parking_tracker = parking_tracker
for squadron in squadrons:
self.add_squadron(squadron)
@ -376,6 +441,7 @@ class SquadronConfigurationLayout(QVBoxLayout):
return keep_squadrons
def remove_squadron(self, squadron: Squadron) -> None:
self.parking_tracker.remove_squadron(squadron)
for squadron_config in self.squadron_configs:
if squadron_config.squadron == squadron:
squadron_config.deleteLater()
@ -386,23 +452,32 @@ class SquadronConfigurationLayout(QVBoxLayout):
return
def add_squadron(self, squadron: Squadron) -> None:
squadron_config = SquadronConfigurationBox(self.game, self.coalition, squadron)
squadron_config = SquadronConfigurationBox(
self.game, self.coalition, squadron, self.parking_tracker
)
squadron_config.remove_squadron_signal.connect(self.remove_squadron)
self.squadron_configs.append(squadron_config)
self.addWidget(squadron_config)
self.parking_tracker.add_squadron(squadron)
class AircraftSquadronsPage(QWidget):
remove_squadron_page = Signal(AircraftType)
def __init__(
self, game: Game, coalition: Coalition, squadrons: list[Squadron]
self,
game: Game,
coalition: Coalition,
squadrons: list[Squadron],
parking_tracker: AirWingConfigParkingTracker,
) -> None:
super().__init__()
layout = QVBoxLayout()
self.setLayout(layout)
self.squadrons_config = SquadronConfigurationLayout(game, coalition, squadrons)
self.squadrons_config = SquadronConfigurationLayout(
game, coalition, squadrons, parking_tracker
)
self.squadrons_config.config_changed.connect(self.on_squadron_config_changed)
scrolling_widget = QWidget()
@ -430,10 +505,16 @@ class AircraftSquadronsPage(QWidget):
class AircraftSquadronsPanel(QStackedLayout):
page_removed = Signal(AircraftType)
def __init__(self, game: Game, coalition: Coalition) -> None:
def __init__(
self,
game: Game,
coalition: Coalition,
parking_tracker: AirWingConfigParkingTracker,
) -> None:
super().__init__()
self.game = game
self.coalition = coalition
self.parking_tracker = parking_tracker
self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {}
for aircraft, squadrons in self.air_wing.squadrons.items():
self.new_page_for_type(aircraft, squadrons)
@ -453,7 +534,9 @@ class AircraftSquadronsPanel(QStackedLayout):
def new_page_for_type(
self, aircraft_type: AircraftType, squadrons: list[Squadron]
) -> None:
page = AircraftSquadronsPage(self.game, self.coalition, squadrons)
page = AircraftSquadronsPage(
self.game, self.coalition, squadrons, self.parking_tracker
)
page.remove_squadron_page.connect(self.remove_page_for_type)
self.addWidget(page)
self.squadrons_pages[aircraft_type] = page
@ -547,6 +630,9 @@ class AirWingConfigurationTab(QWidget):
self.setLayout(layout)
self.game = game
self.coalition = coalition
self.parking_tracker = AirWingConfigParkingTracker(
coalition.air_wing.iter_squadrons()
)
self.type_list = AircraftTypeList(coalition.air_wing)
@ -556,7 +642,9 @@ class AirWingConfigurationTab(QWidget):
add_button.clicked.connect(lambda state: self.add_squadron())
layout.addWidget(add_button, 2, 1, 1, 1)
self.squadrons_panel = AircraftSquadronsPanel(game, coalition)
self.squadrons_panel = AircraftSquadronsPanel(
game, coalition, self.parking_tracker
)
self.squadrons_panel.page_removed.connect(self.type_list.remove_aircraft_type)
layout.addLayout(self.squadrons_panel, 1, 3, 2, 1)