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()