Support replacing squadrons in-place.

This commit is contained in:
Dan Albert 2023-05-04 20:34:29 -07:00 committed by Raffson
parent 7bbf5d59e4
commit e95a6bf759
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
2 changed files with 162 additions and 42 deletions

View File

@ -138,6 +138,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.
* **[Modding]** The `mission_types` field in squadron files has been removed. Squadron task capability is now determined by airframe, and the auto-assignable list has always been overridden by the campaign settings. * **[Modding]** The `mission_types` field in squadron files has been removed. Squadron task capability is now determined by airframe, and the auto-assignable list has always been overridden by the campaign settings.
* **[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.
* **[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.
## Fixes ## Fixes

View File

@ -36,27 +36,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):
@ -79,7 +74,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]:
@ -87,6 +82,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.
@ -168,8 +170,15 @@ class SquadronLiverySelector(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()
@ -203,7 +212,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,
) )
@ -220,20 +229,27 @@ class SquadronConfigurationBox(QGroupBox):
player_label = QLabel(text) player_label = QLabel(text)
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)
@ -241,9 +257,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) -> Optional[Squadron]:
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}")
@ -260,12 +336,8 @@ class SquadronConfigurationBox(QGroupBox):
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.squadron.livery = self.livery_selector.currentData() self.squadron.livery = self.livery_selector.currentData()
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)
@ -276,10 +348,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)
@ -300,7 +378,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)
@ -309,12 +387,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()
@ -342,14 +422,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)
@ -361,7 +445,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
@ -465,7 +549,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)
@ -484,7 +568,8 @@ class AirWingConfigurationTab(QWidget):
possible_aircrafts = [ possible_aircrafts = [
aircraft aircraft
for aircraft in self.coalition.faction.aircrafts for aircraft in self.coalition.faction.aircrafts
if any(base.can_operate(aircraft) for base in bases) if isinstance(aircraft, AircraftType)
and any(base.can_operate(aircraft) for base in bases)
] ]
popup = SquadronConfigPopup( popup = SquadronConfigPopup(
@ -611,14 +696,17 @@ 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(self.AdjustToContents) self.setSizeAdjustPolicy(self.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()
if self.allow_random:
self.addItem("None (Random)", None) 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(
@ -629,7 +717,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):
@ -647,8 +735,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
@ -700,3 +786,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)