Support replacing squadrons in-place.

This commit is contained in:
Dan Albert 2023-05-04 20:34:29 -07:00
parent c9e4b5eba4
commit e444761059
2 changed files with 160 additions and 41 deletions

View File

@ -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. * **[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]** 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]** 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. * **[Squadrons]** Squadron-specific mission capability lists no longer restrict players from assigning missions outside the squadron's preferences.
## Fixes ## Fixes

View File

@ -35,27 +35,22 @@ from game.coalition import Coalition
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.squadrons import AirWing, Pilot, Squadron from game.squadrons import AirWing, Pilot, Squadron
from game.squadrons.squadrondef import SquadronDef 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 from qt_ui.uiconstants import AIRCRAFT_ICONS, ICONS
class QMissionType: class QMissionType(QCheckBox):
def __init__( def __init__(
self, mission_type: FlightType, allowed: bool, auto_assignable: bool self, mission_type: FlightType, allowed: bool, auto_assignable: bool
) -> None: ) -> None:
super().__init__()
self.flight_type = mission_type self.flight_type = mission_type
self.auto_assignable_checkbox = QCheckBox() self.setEnabled(allowed)
self.auto_assignable_checkbox.setEnabled(allowed) self.setChecked(auto_assignable)
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)
@property @property
def auto_assignable(self) -> bool: def auto_assignable(self) -> bool:
return self.auto_assignable_checkbox.isChecked() return self.isChecked()
class MissionTypeControls(QGridLayout): class MissionTypeControls(QGridLayout):
@ -78,7 +73,7 @@ class MissionTypeControls(QGridLayout):
self.mission_types.append(mission_type) self.mission_types.append(mission_type)
self.addWidget(QLabel(task.value), i + 1, 0) 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 @property
def auto_assignable_mission_types(self) -> Iterator[FlightType]: def auto_assignable_mission_types(self) -> Iterator[FlightType]:
@ -86,6 +81,13 @@ class MissionTypeControls(QGridLayout):
if mission_type.auto_assignable: if mission_type.auto_assignable:
yield mission_type.flight_type 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): class SquadronBaseSelector(QComboBox):
"""A combo box for selecting a squadrons home air base. """A combo box for selecting a squadrons home air base.
@ -127,8 +129,15 @@ class SquadronBaseSelector(QComboBox):
class SquadronConfigurationBox(QGroupBox): class SquadronConfigurationBox(QGroupBox):
remove_squadron_signal = Signal(Squadron) 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__() super().__init__()
self.game = game
self.coalition = coalition
self.squadron = squadron self.squadron = squadron
columns = QHBoxLayout() columns = QHBoxLayout()
@ -158,7 +167,7 @@ class SquadronConfigurationBox(QGroupBox):
left_column.addWidget(QLabel("Base:")) left_column.addWidget(QLabel("Base:"))
self.base_selector = SquadronBaseSelector( self.base_selector = SquadronBaseSelector(
theater.control_points_for(squadron.player), game.theater.control_points_for(squadron.player),
squadron.location, squadron.location,
squadron.aircraft, squadron.aircraft,
) )
@ -174,20 +183,27 @@ class SquadronConfigurationBox(QGroupBox):
) )
left_column.addWidget(player_label) left_column.addWidget(player_label)
players = [p for p in squadron.pilot_pool if p.player] self.player_list = QTextEdit(
for player in players: "<br />".join(p.name for p in self.claim_players_from_squadron())
squadron.pilot_pool.remove(player) )
if not squadron.player:
players = []
self.player_list = QTextEdit("<br />".join(p.name for p in players))
self.player_list.setAcceptRichText(False) self.player_list.setAcceptRichText(False)
self.player_list.setEnabled(squadron.player and squadron.aircraft.flyable) self.player_list.setEnabled(squadron.player and squadron.aircraft.flyable)
left_column.addWidget(self.player_list) left_column.addWidget(self.player_list)
button_row = QHBoxLayout()
left_column.addLayout(button_row)
left_column.addStretch()
delete_button = QPushButton("Remove Squadron") delete_button = QPushButton("Remove Squadron")
delete_button.setMaximumWidth(140) delete_button.setMaximumWidth(140)
delete_button.clicked.connect(self.remove_from_squadron_config) delete_button.clicked.connect(self.remove_from_squadron_config)
left_column.addWidget(delete_button) button_row.addWidget(delete_button)
left_column.addStretch()
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() right_column = QVBoxLayout()
self.mission_types = MissionTypeControls(squadron) self.mission_types = MissionTypeControls(squadron)
@ -195,9 +211,69 @@ class SquadronConfigurationBox(QGroupBox):
right_column.addStretch() right_column.addStretch()
columns.addLayout(right_column) 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(
"<br />".join(p.name for p in self.claim_players_from_squadron())
)
finally:
self.blockSignals(old_state)
def remove_from_squadron_config(self) -> None: def remove_from_squadron_config(self) -> None:
self.remove_squadron_signal.emit(self.squadron) 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: def reset_title(self) -> None:
self.setTitle(f"{self.name_edit.text()} - {self.squadron.aircraft}") self.setTitle(f"{self.name_edit.text()} - {self.squadron.aircraft}")
@ -213,12 +289,8 @@ class SquadronConfigurationBox(QGroupBox):
if base is None: if base is None:
raise RuntimeError("Base cannot be none") raise RuntimeError("Base cannot be none")
self.squadron.assign_to_base(base) 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 # Also update the auto assignable mission types
self.squadron.set_auto_assignable_mission_types( self.squadron.set_auto_assignable_mission_types(
set(self.mission_types.auto_assignable_mission_types) set(self.mission_types.auto_assignable_mission_types)
@ -229,10 +301,16 @@ class SquadronConfigurationBox(QGroupBox):
class SquadronConfigurationLayout(QVBoxLayout): class SquadronConfigurationLayout(QVBoxLayout):
config_changed = Signal(AircraftType) 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__() super().__init__()
self.game = game
self.coalition = coalition
self.squadron_configs = [] self.squadron_configs = []
self.theater = theater
for squadron in squadrons: for squadron in squadrons:
self.add_squadron(squadron) self.add_squadron(squadron)
@ -253,7 +331,7 @@ class SquadronConfigurationLayout(QVBoxLayout):
return return
def add_squadron(self, squadron: Squadron) -> None: 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) squadron_config.remove_squadron_signal.connect(self.remove_squadron)
self.squadron_configs.append(squadron_config) self.squadron_configs.append(squadron_config)
self.addWidget(squadron_config) self.addWidget(squadron_config)
@ -262,12 +340,14 @@ class SquadronConfigurationLayout(QVBoxLayout):
class AircraftSquadronsPage(QWidget): class AircraftSquadronsPage(QWidget):
remove_squadron_page = Signal(AircraftType) 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__() super().__init__()
layout = QVBoxLayout() layout = QVBoxLayout()
self.setLayout(layout) 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) self.squadrons_config.config_changed.connect(self.on_squadron_config_changed)
scrolling_widget = QWidget() scrolling_widget = QWidget()
@ -295,14 +375,18 @@ class AircraftSquadronsPage(QWidget):
class AircraftSquadronsPanel(QStackedLayout): class AircraftSquadronsPanel(QStackedLayout):
page_removed = Signal(AircraftType) page_removed = Signal(AircraftType)
def __init__(self, air_wing: AirWing, theater: ConflictTheater) -> None: def __init__(self, game: Game, coalition: Coalition) -> None:
super().__init__() super().__init__()
self.air_wing = air_wing self.game = game
self.theater = theater self.coalition = coalition
self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {} self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {}
for aircraft, squadrons in self.air_wing.squadrons.items(): for aircraft, squadrons in self.air_wing.squadrons.items():
self.new_page_for_type(aircraft, squadrons) 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): def remove_page_for_type(self, aircraft_type: AircraftType):
page = self.squadrons_pages[aircraft_type] page = self.squadrons_pages[aircraft_type]
self.removeWidget(page) self.removeWidget(page)
@ -314,7 +398,7 @@ class AircraftSquadronsPanel(QStackedLayout):
def new_page_for_type( def new_page_for_type(
self, aircraft_type: AircraftType, squadrons: list[Squadron] self, aircraft_type: AircraftType, squadrons: list[Squadron]
) -> None: ) -> None:
page = AircraftSquadronsPage(squadrons, self.theater) page = AircraftSquadronsPage(self.game, self.coalition, squadrons)
page.remove_squadron_page.connect(self.remove_page_for_type) page.remove_squadron_page.connect(self.remove_page_for_type)
self.addWidget(page) self.addWidget(page)
self.squadrons_pages[aircraft_type] = page self.squadrons_pages[aircraft_type] = page
@ -417,7 +501,7 @@ class AirWingConfigurationTab(QWidget):
add_button.clicked.connect(lambda state: self.add_squadron()) add_button.clicked.connect(lambda state: self.add_squadron())
layout.addWidget(add_button, 2, 1, 1, 1) 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) self.squadrons_panel.page_removed.connect(self.type_list.remove_aircraft_type)
layout.addLayout(self.squadrons_panel, 1, 3, 2, 1) layout.addLayout(self.squadrons_panel, 1, 3, 2, 1)
@ -563,15 +647,18 @@ class SquadronDefSelector(QComboBox):
self, self,
squadron_defs: dict[AircraftType, list[SquadronDef]], squadron_defs: dict[AircraftType, list[SquadronDef]],
aircraft: Optional[AircraftType], aircraft: Optional[AircraftType],
allow_random: bool = True,
) -> None: ) -> None:
super().__init__() super().__init__()
self.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents) self.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents)
self.squadron_defs = squadron_defs self.squadron_defs = squadron_defs
self.allow_random = allow_random
self.set_aircraft_type(aircraft) self.set_aircraft_type(aircraft)
def set_aircraft_type(self, aircraft: Optional[AircraftType]): def set_aircraft_type(self, aircraft: Optional[AircraftType]):
self.clear() self.clear()
self.addItem("None (Random)", None) if self.allow_random:
self.addItem("None (Random)", None)
if aircraft and aircraft in self.squadron_defs: if aircraft and aircraft in self.squadron_defs:
for squadron_def in sorted( for squadron_def in sorted(
self.squadron_defs[aircraft], key=lambda squadron_def: squadron_def.name self.squadron_defs[aircraft], key=lambda squadron_def: squadron_def.name
@ -581,7 +668,7 @@ class SquadronDefSelector(QComboBox):
if squadron_def.nickname: if squadron_def.nickname:
squadron_name += " (" + squadron_def.nickname + ")" squadron_name += " (" + squadron_def.nickname + ")"
self.addItem(squadron_name, squadron_def) self.addItem(squadron_name, squadron_def)
self.setCurrentText("None (Random)") self.setCurrentIndex(0)
class SquadronConfigPopup(QDialog): class SquadronConfigPopup(QDialog):
@ -599,8 +686,6 @@ class SquadronConfigPopup(QDialog):
self.column = QVBoxLayout() self.column = QVBoxLayout()
self.setLayout(self.column) self.setLayout(self.column)
self.bases = bases
self.column.addWidget(QLabel("Aircraft:")) self.column.addWidget(QLabel("Aircraft:"))
self.aircraft_type_selector = SquadronAircraftTypeSelector( self.aircraft_type_selector = SquadronAircraftTypeSelector(
types, selected_aircraft types, selected_aircraft
@ -652,3 +737,36 @@ class SquadronConfigPopup(QDialog):
) )
self.update_accept_button() self.update_accept_button()
self.update() 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)