diff --git a/game/coalition.py b/game/coalition.py index 1922f3ce..01c1e2cb 100644 --- a/game/coalition.py +++ b/game/coalition.py @@ -150,6 +150,13 @@ class Coalition: # is handled correctly. self.transfers.perform_transfers() + def preinit_turn_0(self) -> None: + """Runs final Coalition initialization. + + Final initialization occurs before Game.initialize_turn runs for turn 0. + """ + self.air_wing.populate_for_turn_0() + def initialize_turn(self) -> None: """Processes coalition-specific turn initialization. diff --git a/game/game.py b/game/game.py index 6ce7b178..7125cc24 100644 --- a/game/game.py +++ b/game/game.py @@ -294,6 +294,8 @@ class Game: def begin_turn_0(self) -> None: """Initialization for the first turn of the game.""" self.turn = 0 + self.blue.preinit_turn_0() + self.red.preinit_turn_0() self.initialize_turn() def pass_turn(self, no_action: bool = False) -> None: diff --git a/game/squadrons.py b/game/squadrons.py index 3a23d4ea..45ebe7de 100644 --- a/game/squadrons.py +++ b/game/squadrons.py @@ -101,9 +101,6 @@ class Squadron: settings: Settings = field(hash=False, compare=False) def __post_init__(self) -> None: - if any(p.status is not PilotStatus.Active for p in self.pilot_pool): - raise ValueError("Squadrons can only be created with active pilots.") - self._recruit_pilots(self.settings.squadron_pilot_limit) self.auto_assignable_mission_types = set(self.mission_types) def __str__(self) -> str: @@ -181,6 +178,11 @@ class Squadron: self.current_roster.extend(new_pilots) self.available_pilots.extend(new_pilots) + def populate_for_turn_0(self) -> None: + if any(p.status is not PilotStatus.Active for p in self.pilot_pool): + raise ValueError("Squadrons can only be created with active pilots.") + self._recruit_pilots(self.settings.squadron_pilot_limit) + def replenish_lost_pilots(self) -> None: if not self.pilot_limits_enabled: return @@ -414,6 +416,10 @@ class AirWing: def squadron_at_index(self, index: int) -> Squadron: return list(self.iter_squadrons())[index] + def populate_for_turn_0(self) -> None: + for squadron in self.iter_squadrons(): + squadron.populate_for_turn_0() + def replenish(self) -> None: for squadron in self.iter_squadrons(): squadron.replenish_lost_pilots() diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 0bf85391..aee758e9 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -123,7 +123,6 @@ class GameGenerator: GroundObjectGenerator(game, self.generator_settings).generate() game.settings.version = VERSION - game.begin_turn_0() return game def prepare_theater(self) -> None: diff --git a/qt_ui/main.py b/qt_ui/main.py index 26c5cb48..ee614287 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -246,7 +246,9 @@ def create_game( high_digit_sams=False, ), ) - return generator.generate() + game = generator.generate() + game.begin_turn_0() + return game def lint_weapon_data() -> None: diff --git a/qt_ui/windows/AirWingConfigurationDialog.py b/qt_ui/windows/AirWingConfigurationDialog.py new file mode 100644 index 00000000..fb6bdc8b --- /dev/null +++ b/qt_ui/windows/AirWingConfigurationDialog.py @@ -0,0 +1,220 @@ +import itertools +import logging +from collections import defaultdict +from typing import Optional, Callable, Iterator + +from PySide2.QtCore import ( + QItemSelectionModel, + QModelIndex, + QSize, + Qt, +) +from PySide2.QtWidgets import ( + QAbstractItemView, + QDialog, + QListView, + QVBoxLayout, + QGroupBox, + QGridLayout, + QLabel, + QWidget, + QScrollArea, + QLineEdit, + QTextEdit, + QCheckBox, + QHBoxLayout, +) + +from game import Game +from game.squadrons import Squadron, AirWing, Pilot +from gen.flights.flight import FlightType +from qt_ui.models import AirWingModel, SquadronModel +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): + def __init__(self, squadron: Squadron) -> None: + super().__init__() + self.squadron = squadron + self.allowed_mission_types = set() + + self.addWidget(QLabel("Allowed mission types")) + + def make_callback(toggled_task: FlightType) -> Callable[[bool], None]: + def callback(checked: bool) -> None: + self.on_toggled(toggled_task, checked) + + return callback + + for task in FlightType: + enabled = task in squadron.mission_types + if enabled: + self.allowed_mission_types.add(task) + checkbox = QCheckBox(text=task.value) + checkbox.setChecked(enabled) + checkbox.toggled.connect(make_callback(task)) + self.addWidget(checkbox) + + self.addStretch() + + def on_toggled(self, task: FlightType, checked: bool) -> None: + if checked: + self.allowed_mission_types.add(task) + else: + self.allowed_mission_types.remove(task) + + +class SquadronConfigurationBox(QGroupBox): + def __init__(self, squadron: Squadron) -> None: + super().__init__() + self.setCheckable(True) + self.squadron = squadron + self.reset_title() + + columns = QHBoxLayout() + self.setLayout(columns) + + left_column = QVBoxLayout() + columns.addLayout(left_column) + + left_column.addWidget(QLabel("Name:")) + self.name_edit = QLineEdit(squadron.name) + self.name_edit.textChanged.connect(self.on_name_changed) + left_column.addWidget(self.name_edit) + + left_column.addWidget(QLabel("Nickname:")) + self.nickname_edit = QLineEdit(squadron.nickname) + self.nickname_edit.textChanged.connect(self.on_nickname_changed) + left_column.addWidget(self.nickname_edit) + + left_column.addWidget( + QLabel("Players (one per line, leave empty for an AI-only squadron):") + ) + players = [p for p in squadron.available_pilots if p.player] + for player in players: + squadron.available_pilots.remove(player) + self.player_list = QTextEdit("
".join(p.name for p in players)) + self.player_list.setAcceptRichText(False) + left_column.addWidget(self.player_list) + + left_column.addStretch() + + self.allowed_missions = AllowedMissionTypeControls(squadron) + columns.addLayout(self.allowed_missions) + + def on_name_changed(self, text: str) -> None: + self.squadron.name = text + self.reset_title() + + def on_nickname_changed(self, text: str) -> None: + self.squadron.nickname = text + + def reset_title(self) -> None: + self.setTitle(f"{self.squadron.name} - {self.squadron.aircraft}") + + def apply(self) -> Squadron: + player_names = self.player_list.toPlainText().splitlines() + # Prepend player pilots so they get set active first. + self.squadron.pilot_pool = [ + Pilot(n, player=True) for n in player_names + ] + self.squadron.pilot_pool + self.squadron.mission_types = tuple(self.allowed_missions.allowed_mission_types) + return self.squadron + + +class AirWingConfigurationLayout(QVBoxLayout): + def __init__(self, air_wing: AirWing) -> None: + super().__init__() + self.air_wing = air_wing + self.squadron_configs = [] + + doc_url = ( + "https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots" + ) + doc_label = QLabel( + "Use this opportunity to customize the squadrons available to your " + "coalition. This is your
" + "only opportunity to make changes.

" + "
" + "To accept your changes and continue, close this window.
" + "
" + "To remove a squadron from the game, uncheck the box in the title. New " + "squadrons cannot
" + "be added via the UI at this time. To add a custom squadron, see " + f'the wiki.' + ) + + doc_label.setOpenExternalLinks(True) + self.addWidget(doc_label) + for squadron in self.air_wing.iter_squadrons(): + squadron_config = SquadronConfigurationBox(squadron) + self.squadron_configs.append(squadron_config) + self.addWidget(squadron_config) + + def apply(self) -> None: + keep_squadrons = defaultdict(list) + for squadron_config in self.squadron_configs: + if squadron_config.isChecked(): + squadron = squadron_config.apply() + keep_squadrons[squadron.aircraft].append(squadron) + self.air_wing.squadrons = keep_squadrons + + +class AirWingConfigurationDialog(QDialog): + """Dialog window for air wing configuration.""" + + def __init__(self, game: Game, parent) -> None: + super().__init__(parent) + self.air_wing = game.blue.air_wing + + self.setMinimumSize(500, 800) + self.setWindowTitle(f"Air Wing Configuration") + # TODO: self.setWindowIcon() + + self.air_wing_config = AirWingConfigurationLayout(self.air_wing) + + scrolling_layout = QVBoxLayout() + scrolling_widget = QWidget() + scrolling_widget.setLayout(self.air_wing_config) + + scrolling_area = QScrollArea() + scrolling_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scrolling_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + scrolling_area.setWidgetResizable(True) + scrolling_area.setWidget(scrolling_widget) + + scrolling_layout.addWidget(scrolling_area) + self.setLayout(scrolling_layout) + + def reject(self) -> None: + self.air_wing_config.apply() + super().reject() diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index 264f73cf..b29a4806 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -15,6 +15,7 @@ from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSe from game.factions.faction import Faction from qt_ui.widgets.QLiberationCalendar import QLiberationCalendar from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs, CurrencySpinner +from qt_ui.windows.AirWingConfigurationDialog import AirWingConfigurationDialog from qt_ui.windows.newgame.QCampaignList import ( Campaign, QCampaignList, @@ -125,6 +126,10 @@ class NewGameWizard(QtWidgets.QWizard): ) self.generatedGame = generator.generate() + AirWingConfigurationDialog(self.generatedGame, self).exec_() + + self.generatedGame.begin_turn_0() + super(NewGameWizard, self).accept()