From 7f94b34277633a5af45ad1a0cd255699bebf5fe1 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 4 May 2023 22:34:23 -0700 Subject: [PATCH] Add an option to prefer primary tasked aircraft. We're still using mostly the same aircraft selection as we have before we added squadrons: the closest aircraft is the best choice. This adds an option to obey the primary task set by the campaign designer (can be overridden by players), even if the squadron is farther away than one that is capable of it as a secondary task. I don't expect this option to live very long. I'm making it optional for now to give people a chance to test it, but it'll either replace the old selection strategy or will be removed. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1892. --- changelog.md | 3 +- .../campaignloader/defaultsquadronassigner.py | 6 ++- game/settings/settings.py | 12 ++++++ game/squadrons/airwing.py | 12 ++++++ game/squadrons/squadron.py | 3 ++ qt_ui/widgets/combos/primarytaskselector.py | 39 +++++++++++++++++++ qt_ui/windows/AirWingConfigurationDialog.py | 24 +++++++++++- qt_ui/windows/SquadronDialog.py | 21 +++++++++- 8 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 qt_ui/widgets/combos/primarytaskselector.py diff --git a/changelog.md b/changelog.md index a6a8021f..e3748a4e 100644 --- a/changelog.md +++ b/changelog.md @@ -6,7 +6,8 @@ Saves from 6.x are not compatible with 7.0. * **[Engine]** Support for DCS 2.8.3.37556. * **[Engine]** Saved games are now a zip file of save assets for easier bug reporting. The new extension is .liberation.zip. Drag and drop that file into bug reports. -* **[Flight Planning]** Package TOT and composition can be modified after advancing time in Liberation. +* **[Campaign AI]** Added an option to instruct the campaign AI to prefer fulfilling missions with squadrons which have a matching primary task. Previously distance from target held a stronger influence than task preference. Primary tasks for squadrons are set by campaign designers but are user-configurable. +* **[Flight Planning]** Package TOT and composition can be modified after advancing time in Liberation. * **[Mission Generation]** Units on the front line are now hidden on MFDs. * **[Mission Generation]** Preset radio channels will now be configured for both A-10C modules. * **[Mission Generation]** The A-10C II now uses separate radios for inter- and intra-flight comms (similar to other modern aircraft). diff --git a/game/campaignloader/defaultsquadronassigner.py b/game/campaignloader/defaultsquadronassigner.py index 8323ce5b..5a3c25eb 100644 --- a/game/campaignloader/defaultsquadronassigner.py +++ b/game/campaignloader/defaultsquadronassigner.py @@ -43,7 +43,11 @@ class DefaultSquadronAssigner: continue squadron = Squadron.create_from( - squadron_def, control_point, self.coalition, self.game + squadron_def, + squadron_config.primary, + control_point, + self.coalition, + self.game, ) squadron.set_auto_assignable_mission_types( squadron_config.auto_assignable diff --git a/game/settings/settings.py b/game/settings/settings.py index 54b4195b..d49c6cd1 100644 --- a/game/settings/settings.py +++ b/game/settings/settings.py @@ -195,6 +195,18 @@ class Settings: "future release." ), ) + prefer_squadrons_with_matching_primary_task: bool = boolean_option( + "Prefer squadrons with matching primary task when planning missions", + page=CAMPAIGN_MANAGEMENT_PAGE, + section=GENERAL_SECTION, + default=False, + detail=( + "If checked, squadrons with a primary task matching the mission will be " + "preferred even if there is a closer squadron capable of the mission as a" + "secondary task. Expect longer flights, but squadrons will be more often " + "assigned to their primary task." + ), + ) # Pilots and Squadrons ai_pilot_levelling: bool = boolean_option( "Allow AI pilot leveling", diff --git a/game/squadrons/airwing.py b/game/squadrons/airwing.py index c8948a66..ac1f8430 100644 --- a/game/squadrons/airwing.py +++ b/game/squadrons/airwing.py @@ -23,6 +23,7 @@ class AirWing: self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list) self.squadron_defs = SquadronDefLoader(game, faction).load() self.squadron_def_generator = SquadronDefGenerator(faction) + self.settings = game.settings def unclaim_squadron_def(self, squadron: Squadron) -> None: if squadron.aircraft in self.squadron_defs: @@ -66,6 +67,17 @@ class AirWing: key=lambda s: best_aircraft.index(s.aircraft), ) ) + + if self.settings.prefer_squadrons_with_matching_primary_task: + return sorted( + ordered, + key=lambda s: ( + # This looks like the opposite of what we want because False sorts + # before True. + s.primary_task != task, + s.location.distance_to(location), + ), + ) return ordered def best_squadron_for( diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index 9618fa81..9334d7f6 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -32,6 +32,7 @@ class Squadron: role: str aircraft: AircraftType livery: Optional[str] + primary_task: FlightType auto_assignable_mission_types: set[FlightType] operating_bases: OperatingBases female_pilot_percentage: int @@ -422,6 +423,7 @@ class Squadron: def create_from( cls, squadron_def: SquadronDef, + primary_task: FlightType, base: ControlPoint, coalition: Coalition, game: Game, @@ -434,6 +436,7 @@ class Squadron: squadron_def.role, squadron_def.aircraft, squadron_def.livery, + primary_task, squadron_def.auto_assignable_mission_types, squadron_def.operating_bases, squadron_def.female_pilot_percentage, diff --git a/qt_ui/widgets/combos/primarytaskselector.py b/qt_ui/widgets/combos/primarytaskselector.py new file mode 100644 index 00000000..2f39b0cb --- /dev/null +++ b/qt_ui/widgets/combos/primarytaskselector.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from PySide6.QtWidgets import QComboBox + +from game.ato import FlightType +from game.dcs.aircrafttype import AircraftType +from game.squadrons import Squadron + + +class PrimaryTaskSelector(QComboBox): + def __init__(self, aircraft: AircraftType | None) -> None: + super().__init__() + self.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents) + self.set_aircraft(aircraft) + + @staticmethod + def for_squadron(squadron: Squadron) -> PrimaryTaskSelector: + selector = PrimaryTaskSelector(squadron.aircraft) + selector.setCurrentText(squadron.primary_task.value) + return selector + + def set_aircraft(self, aircraft: AircraftType | None) -> None: + self.clear() + if aircraft is None: + self.addItem("Select aircraft type first", None) + self.setEnabled(False) + self.update() + return + + self.setEnabled(True) + for task in aircraft.iter_task_capabilities(): + self.addItem(task.value, task) + self.model().sort(0) + self.setEnabled(True) + self.update() + + @property + def selected_task(self) -> FlightType | None: + return self.currentData() diff --git a/qt_ui/windows/AirWingConfigurationDialog.py b/qt_ui/windows/AirWingConfigurationDialog.py index 3ef8f08e..f65c8eeb 100644 --- a/qt_ui/windows/AirWingConfigurationDialog.py +++ b/qt_ui/windows/AirWingConfigurationDialog.py @@ -37,6 +37,7 @@ from game.squadrons import AirWing, Pilot, Squadron from game.squadrons.squadrondef import SquadronDef from game.theater import ControlPoint from qt_ui.uiconstants import AIRCRAFT_ICONS, ICONS +from qt_ui.widgets.combos.primarytaskselector import PrimaryTaskSelector class QMissionType(QCheckBox): @@ -165,6 +166,10 @@ class SquadronConfigurationBox(QGroupBox): reroll_nickname_button.clicked.connect(self.reroll_nickname) nickname_edit_layout.addWidget(reroll_nickname_button, 1, 1, Qt.AlignTop) + left_column.addWidget(QLabel("Primary task:")) + self.primary_task_selector = PrimaryTaskSelector.for_squadron(self.squadron) + left_column.addWidget(self.primary_task_selector) + left_column.addWidget(QLabel("Base:")) self.base_selector = SquadronBaseSelector( game.theater.control_points_for(squadron.player), @@ -216,6 +221,7 @@ class SquadronConfigurationBox(QGroupBox): try: self.name_edit.setText(self.squadron.name) self.nickname_edit.setText(self.squadron.nickname) + self.primary_task_selector.setCurrentText(self.squadron.primary_task.value) self.base_selector.setCurrentText(self.squadron.location.name) self.player_list.setText( "
".join(p.name for p in self.claim_players_from_squadron()) @@ -239,6 +245,7 @@ class SquadronConfigurationBox(QGroupBox): self.squadron.coalition.air_wing.unclaim_squadron_def(self.squadron) squadron = Squadron.create_from( selected_def, + self.squadron.primary_task, self.squadron.location, self.coalition, self.game, @@ -285,6 +292,10 @@ class SquadronConfigurationBox(QGroupBox): def apply(self) -> Squadron: self.squadron.name = self.name_edit.text() self.squadron.nickname = self.nickname_edit.text() + if (primary_task := self.primary_task_selector.selected_task) is not None: + self.squadron.primary_task = primary_task + else: + raise RuntimeError("Primary task cannot be none") base = self.base_selector.currentData() if base is None: raise RuntimeError("Base cannot be none") @@ -534,6 +545,7 @@ class AirWingConfigurationTab(QWidget): selected_type = popup.aircraft_type_selector.currentData() selected_base = popup.squadron_base_selector.currentData() + selected_task = popup.primary_task_selector.selected_task selected_def = popup.squadron_def_selector.currentData() # Let user choose the preset or generate one @@ -545,7 +557,7 @@ class AirWingConfigurationTab(QWidget): ) squadron = Squadron.create_from( - squadron_def, selected_base, self.coalition, self.game + squadron_def, selected_task, selected_base, self.coalition, self.game ) # Add Squadron @@ -695,6 +707,12 @@ class SquadronConfigPopup(QDialog): ) self.column.addWidget(self.aircraft_type_selector) + self.column.addWidget(QLabel("Primary task:")) + self.primary_task_selector = PrimaryTaskSelector( + self.aircraft_type_selector.currentData() + ) + self.column.addWidget(self.primary_task_selector) + self.column.addWidget(QLabel("Base:")) self.squadron_base_selector = SquadronBaseSelector( bases, None, self.aircraft_type_selector.currentData() @@ -725,6 +743,7 @@ class SquadronConfigPopup(QDialog): enabled = ( self.aircraft_type_selector.currentData() is not None and self.squadron_base_selector.currentData() is not None + and self.primary_task_selector.selected_task is not None ) self.accept_button.setEnabled(enabled) @@ -735,6 +754,9 @@ class SquadronConfigPopup(QDialog): self.squadron_def_selector.set_aircraft_type( self.aircraft_type_selector.currentData() ) + self.primary_task_selector.set_aircraft( + self.aircraft_type_selector.currentData() + ) self.update_accept_button() self.update() diff --git a/qt_ui/windows/SquadronDialog.py b/qt_ui/windows/SquadronDialog.py index 206660f2..b8104e07 100644 --- a/qt_ui/windows/SquadronDialog.py +++ b/qt_ui/windows/SquadronDialog.py @@ -21,6 +21,7 @@ from qt_ui.delegates import TwoColumnRowDelegate from qt_ui.errorreporter import report_errors from qt_ui.models import AtoModel, SquadronModel from qt_ui.simcontroller import SimController +from qt_ui.widgets.combos.primarytaskselector import PrimaryTaskSelector class PilotDelegate(TwoColumnRowDelegate): @@ -154,8 +155,20 @@ class SquadronDialog(QDialog): columns = QHBoxLayout() layout.addLayout(columns) + left_column = QVBoxLayout() + columns.addLayout(left_column) + + left_column.addWidget(QLabel("Primary task")) + self.primary_task_selector = PrimaryTaskSelector.for_squadron( + self.squadron_model.squadron + ) + self.primary_task_selector.currentIndexChanged.connect( + self.on_task_index_changed + ) + left_column.addWidget(self.primary_task_selector) + auto_assigned_tasks = AutoAssignedTaskControls(squadron_model) - columns.addLayout(auto_assigned_tasks) + left_column.addLayout(auto_assigned_tasks) self.pilot_list = PilotList(squadron_model) self.pilot_list.selectionModel().selectionChanged.connect( @@ -261,3 +274,9 @@ class SquadronDialog(QDialog): index = selected.indexes()[0] self.reset_ai_toggle_state(index) self.reset_leave_toggle_state(index) + + def on_task_index_changed(self, index: int) -> None: + task = self.primary_task_selector.itemData(index) + if task is None: + raise RuntimeError("Selected task cannot be None") + self.squadron.primary_task = task