diff --git a/changelog.md b/changelog.md index 200ef74c..acb714e7 100644 --- a/changelog.md +++ b/changelog.md @@ -129,7 +129,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]** Both A-10C modules now use 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 1cc7c54e..7ed62fb8 100644 --- a/game/campaignloader/defaultsquadronassigner.py +++ b/game/campaignloader/defaultsquadronassigner.py @@ -42,7 +42,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 562a10f7..d3763a26 100644 --- a/game/settings/settings.py +++ b/game/settings/settings.py @@ -176,6 +176,18 @@ class Settings: "extremely incomplete so does not affect all weapons." ), ) + 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 8760db7d..8daedf15 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -31,6 +31,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 @@ -416,6 +417,7 @@ class Squadron: def create_from( cls, squadron_def: SquadronDef, + primary_task: FlightType, base: ControlPoint, coalition: Coalition, game: Game, @@ -428,6 +430,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 81687501..ccd074a2 100644 --- a/qt_ui/windows/AirWingConfigurationDialog.py +++ b/qt_ui/windows/AirWingConfigurationDialog.py @@ -38,6 +38,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): @@ -210,6 +211,10 @@ class SquadronConfigurationBox(QGroupBox): self.livery_selector = SquadronLiverySelector(squadron) left_column.addWidget(self.livery_selector) + 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), @@ -262,6 +267,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()) @@ -285,6 +291,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, @@ -331,6 +338,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") @@ -583,6 +594,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 @@ -594,7 +606,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 @@ -744,6 +756,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() @@ -774,6 +792,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) @@ -784,6 +803,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 cdeff5a2..1b2704cf 100644 --- a/qt_ui/windows/SquadronDialog.py +++ b/qt_ui/windows/SquadronDialog.py @@ -26,6 +26,7 @@ from game.theater import ConflictTheater, ControlPoint from qt_ui.delegates import TwoColumnRowDelegate from qt_ui.errorreporter import report_errors from qt_ui.models import AtoModel, SquadronModel +from qt_ui.widgets.combos.primarytaskselector import PrimaryTaskSelector class PilotDelegate(TwoColumnRowDelegate): @@ -161,8 +162,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( @@ -298,3 +311,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