diff --git a/changelog.md b/changelog.md index 422c6e8a..a6a8021f 100644 --- a/changelog.md +++ b/changelog.md @@ -18,6 +18,7 @@ Saves from 6.x are not compatible with 7.0. * **[Modding]** Aircraft task capabilities and preferred aircraft for each task are now moddable in the aircraft unit yaml files. Each aircraft has a weight per task. Higher weights are given higher preference. * **[New Game Wizard]** Choices for some options will be remembered for the next new game. Not all settings will be preserved, as many are campaign dependent. * **[New Game Wizard]** Lua plugins can now be set while creating a new game. +* **[New Game Wizard]** Squadrons can be directly replaced with a preset during air wing configuration rather than needing to remove and create a new squadron. * **[Squadrons]** Squadron-specific mission capability lists no longer restrict players from assigning missions outside the squadron's preferences. ## Fixes diff --git a/qt_ui/windows/AirWingConfigurationDialog.py b/qt_ui/windows/AirWingConfigurationDialog.py index 0232ac53..3ef8f08e 100644 --- a/qt_ui/windows/AirWingConfigurationDialog.py +++ b/qt_ui/windows/AirWingConfigurationDialog.py @@ -35,27 +35,22 @@ from game.coalition import Coalition from game.dcs.aircrafttype import AircraftType from game.squadrons import AirWing, Pilot, Squadron from game.squadrons.squadrondef import SquadronDef -from game.theater import ConflictTheater, ControlPoint +from game.theater import ControlPoint from qt_ui.uiconstants import AIRCRAFT_ICONS, ICONS -class QMissionType: +class QMissionType(QCheckBox): def __init__( self, mission_type: FlightType, allowed: bool, auto_assignable: bool ) -> None: + super().__init__() self.flight_type = mission_type - self.auto_assignable_checkbox = QCheckBox() - self.auto_assignable_checkbox.setEnabled(allowed) - self.auto_assignable_checkbox.setChecked(auto_assignable) - - def update_auto_assignable(self, checked: bool) -> None: - self.auto_assignable_checkbox.setEnabled(checked) - if not checked: - self.auto_assignable_checkbox.setChecked(False) + self.setEnabled(allowed) + self.setChecked(auto_assignable) @property def auto_assignable(self) -> bool: - return self.auto_assignable_checkbox.isChecked() + return self.isChecked() class MissionTypeControls(QGridLayout): @@ -78,7 +73,7 @@ class MissionTypeControls(QGridLayout): self.mission_types.append(mission_type) self.addWidget(QLabel(task.value), i + 1, 0) - self.addWidget(mission_type.auto_assignable_checkbox, i + 1, 1) + self.addWidget(mission_type, i + 1, 1) @property def auto_assignable_mission_types(self) -> Iterator[FlightType]: @@ -86,6 +81,13 @@ class MissionTypeControls(QGridLayout): if mission_type.auto_assignable: yield mission_type.flight_type + def replace_squadron(self, squadron: Squadron) -> None: + self.squadron = squadron + for mission_type in self.mission_types: + mission_type.setChecked( + mission_type.flight_type in self.squadron.auto_assignable_mission_types + ) + class SquadronBaseSelector(QComboBox): """A combo box for selecting a squadrons home air base. @@ -127,8 +129,15 @@ class SquadronBaseSelector(QComboBox): class SquadronConfigurationBox(QGroupBox): remove_squadron_signal = Signal(Squadron) - def __init__(self, squadron: Squadron, theater: ConflictTheater) -> None: + def __init__( + self, + game: Game, + coalition: Coalition, + squadron: Squadron, + ) -> None: super().__init__() + self.game = game + self.coalition = coalition self.squadron = squadron columns = QHBoxLayout() @@ -158,7 +167,7 @@ class SquadronConfigurationBox(QGroupBox): left_column.addWidget(QLabel("Base:")) self.base_selector = SquadronBaseSelector( - theater.control_points_for(squadron.player), + game.theater.control_points_for(squadron.player), squadron.location, squadron.aircraft, ) @@ -174,20 +183,27 @@ class SquadronConfigurationBox(QGroupBox): ) left_column.addWidget(player_label) - players = [p for p in squadron.pilot_pool if p.player] - for player in players: - squadron.pilot_pool.remove(player) - if not squadron.player: - players = [] - self.player_list = QTextEdit("
".join(p.name for p in players)) + self.player_list = QTextEdit( + "
".join(p.name for p in self.claim_players_from_squadron()) + ) self.player_list.setAcceptRichText(False) self.player_list.setEnabled(squadron.player and squadron.aircraft.flyable) left_column.addWidget(self.player_list) + + button_row = QHBoxLayout() + left_column.addLayout(button_row) + left_column.addStretch() + delete_button = QPushButton("Remove Squadron") delete_button.setMaximumWidth(140) delete_button.clicked.connect(self.remove_from_squadron_config) - left_column.addWidget(delete_button) - left_column.addStretch() + button_row.addWidget(delete_button) + + replace_button = QPushButton("Replace with preset") + replace_button.setMaximumWidth(140) + replace_button.clicked.connect(self.replace_with_preset) + button_row.addWidget(replace_button) + button_row.addStretch() right_column = QVBoxLayout() self.mission_types = MissionTypeControls(squadron) @@ -195,9 +211,69 @@ class SquadronConfigurationBox(QGroupBox): right_column.addStretch() columns.addLayout(right_column) + def bind_data(self) -> None: + old_state = self.blockSignals(True) + try: + self.name_edit.setText(self.squadron.name) + self.nickname_edit.setText(self.squadron.nickname) + self.base_selector.setCurrentText(self.squadron.location.name) + self.player_list.setText( + "
".join(p.name for p in self.claim_players_from_squadron()) + ) + finally: + self.blockSignals(old_state) + def remove_from_squadron_config(self) -> None: self.remove_squadron_signal.emit(self.squadron) + def pick_replacement_squadron(self) -> Squadron | None: + popup = PresetSquadronSelector( + self.squadron.aircraft, + self.coalition.air_wing.squadron_defs, + ) + if popup.exec_() != QDialog.Accepted: + return None + + selected_def = popup.squadron_def_selector.currentData() + + self.squadron.coalition.air_wing.unclaim_squadron_def(self.squadron) + squadron = Squadron.create_from( + selected_def, + self.squadron.location, + self.coalition, + self.game, + ) + return squadron + + def claim_players_from_squadron(self) -> list[Pilot]: + if not self.squadron.player: + return [] + + players = [p for p in self.squadron.pilot_pool if p.player] + for player in players: + self.squadron.pilot_pool.remove(player) + return players + + def return_players_to_squadron(self) -> None: + if not self.squadron.player: + return + + 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 + + def replace_with_preset(self) -> None: + new_squadron = self.pick_replacement_squadron() + if new_squadron is None: + # The user canceled the dialog. + return + self.return_players_to_squadron() + self.squadron = new_squadron + self.bind_data() + self.mission_types.replace_squadron(self.squadron) + def reset_title(self) -> None: self.setTitle(f"{self.name_edit.text()} - {self.squadron.aircraft}") @@ -213,12 +289,8 @@ class SquadronConfigurationBox(QGroupBox): if base is None: raise RuntimeError("Base cannot be none") self.squadron.assign_to_base(base) + self.return_players_to_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 # Also update the auto assignable mission types self.squadron.set_auto_assignable_mission_types( set(self.mission_types.auto_assignable_mission_types) @@ -229,10 +301,16 @@ class SquadronConfigurationBox(QGroupBox): class SquadronConfigurationLayout(QVBoxLayout): config_changed = Signal(AircraftType) - def __init__(self, squadrons: list[Squadron], theater: ConflictTheater) -> None: + def __init__( + self, + game: Game, + coalition: Coalition, + squadrons: list[Squadron], + ) -> None: super().__init__() + self.game = game + self.coalition = coalition self.squadron_configs = [] - self.theater = theater for squadron in squadrons: self.add_squadron(squadron) @@ -253,7 +331,7 @@ class SquadronConfigurationLayout(QVBoxLayout): return def add_squadron(self, squadron: Squadron) -> None: - squadron_config = SquadronConfigurationBox(squadron, self.theater) + squadron_config = SquadronConfigurationBox(self.game, self.coalition, squadron) squadron_config.remove_squadron_signal.connect(self.remove_squadron) self.squadron_configs.append(squadron_config) self.addWidget(squadron_config) @@ -262,12 +340,14 @@ class SquadronConfigurationLayout(QVBoxLayout): class AircraftSquadronsPage(QWidget): remove_squadron_page = Signal(AircraftType) - def __init__(self, squadrons: list[Squadron], theater: ConflictTheater) -> None: + def __init__( + self, game: Game, coalition: Coalition, squadrons: list[Squadron] + ) -> None: super().__init__() layout = QVBoxLayout() self.setLayout(layout) - self.squadrons_config = SquadronConfigurationLayout(squadrons, theater) + self.squadrons_config = SquadronConfigurationLayout(game, coalition, squadrons) self.squadrons_config.config_changed.connect(self.on_squadron_config_changed) scrolling_widget = QWidget() @@ -295,14 +375,18 @@ class AircraftSquadronsPage(QWidget): class AircraftSquadronsPanel(QStackedLayout): page_removed = Signal(AircraftType) - def __init__(self, air_wing: AirWing, theater: ConflictTheater) -> None: + def __init__(self, game: Game, coalition: Coalition) -> None: super().__init__() - self.air_wing = air_wing - self.theater = theater + self.game = game + self.coalition = coalition self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {} for aircraft, squadrons in self.air_wing.squadrons.items(): self.new_page_for_type(aircraft, squadrons) + @property + def air_wing(self) -> AirWing: + return self.coalition.air_wing + def remove_page_for_type(self, aircraft_type: AircraftType): page = self.squadrons_pages[aircraft_type] self.removeWidget(page) @@ -314,7 +398,7 @@ class AircraftSquadronsPanel(QStackedLayout): def new_page_for_type( self, aircraft_type: AircraftType, squadrons: list[Squadron] ) -> None: - page = AircraftSquadronsPage(squadrons, self.theater) + page = AircraftSquadronsPage(self.game, self.coalition, squadrons) page.remove_squadron_page.connect(self.remove_page_for_type) self.addWidget(page) self.squadrons_pages[aircraft_type] = page @@ -417,7 +501,7 @@ class AirWingConfigurationTab(QWidget): add_button.clicked.connect(lambda state: self.add_squadron()) layout.addWidget(add_button, 2, 1, 1, 1) - self.squadrons_panel = AircraftSquadronsPanel(coalition.air_wing, game.theater) + self.squadrons_panel = AircraftSquadronsPanel(game, coalition) self.squadrons_panel.page_removed.connect(self.type_list.remove_aircraft_type) layout.addLayout(self.squadrons_panel, 1, 3, 2, 1) @@ -563,15 +647,18 @@ class SquadronDefSelector(QComboBox): self, squadron_defs: dict[AircraftType, list[SquadronDef]], aircraft: Optional[AircraftType], + allow_random: bool = True, ) -> None: super().__init__() self.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents) self.squadron_defs = squadron_defs + self.allow_random = allow_random self.set_aircraft_type(aircraft) def set_aircraft_type(self, aircraft: Optional[AircraftType]): self.clear() - self.addItem("None (Random)", None) + if self.allow_random: + self.addItem("None (Random)", None) if aircraft and aircraft in self.squadron_defs: for squadron_def in sorted( self.squadron_defs[aircraft], key=lambda squadron_def: squadron_def.name @@ -581,7 +668,7 @@ class SquadronDefSelector(QComboBox): if squadron_def.nickname: squadron_name += " (" + squadron_def.nickname + ")" self.addItem(squadron_name, squadron_def) - self.setCurrentText("None (Random)") + self.setCurrentIndex(0) class SquadronConfigPopup(QDialog): @@ -599,8 +686,6 @@ class SquadronConfigPopup(QDialog): self.column = QVBoxLayout() self.setLayout(self.column) - self.bases = bases - self.column.addWidget(QLabel("Aircraft:")) self.aircraft_type_selector = SquadronAircraftTypeSelector( types, selected_aircraft @@ -652,3 +737,36 @@ class SquadronConfigPopup(QDialog): ) self.update_accept_button() self.update() + + +class PresetSquadronSelector(QDialog): + def __init__( + self, + aircraft: AircraftType, + squadron_defs: dict[AircraftType, list[SquadronDef]], + ) -> None: + super().__init__() + + self.setWindowTitle(f"Choose preset squadron") + + self.column = QVBoxLayout() + self.setLayout(self.column) + + self.column.addWidget(QLabel("Preset:")) + self.squadron_def_selector = SquadronDefSelector( + squadron_defs, aircraft, allow_random=False + ) + self.column.addWidget(self.squadron_def_selector) + + self.column.addStretch() + + self.button_layout = QHBoxLayout() + self.column.addLayout(self.button_layout) + + self.accept_button = QPushButton("Accept") + self.accept_button.clicked.connect(lambda state: self.accept()) + self.button_layout.addWidget(self.accept_button) + + self.cancel_button = QPushButton("Cancel") + self.cancel_button.clicked.connect(lambda state: self.reject()) + self.button_layout.addWidget(self.cancel_button)