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.
This commit is contained in:
Dan Albert 2023-05-04 22:34:23 -07:00 committed by Raffson
parent e95a6bf759
commit f719e1cfe7
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
8 changed files with 116 additions and 4 deletions

View File

@ -129,7 +129,8 @@ Saves from 6.x are not compatible with 7.0.
* **[Engine]** Support for DCS 2.8.3.37556. * **[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. * **[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]** 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]** 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). * **[Mission Generation]** Both A-10C modules now use separate radios for inter- and intra-flight comms (similar to other modern aircraft).

View File

@ -42,7 +42,11 @@ class DefaultSquadronAssigner:
continue continue
squadron = Squadron.create_from( 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.set_auto_assignable_mission_types(
squadron_config.auto_assignable squadron_config.auto_assignable

View File

@ -176,6 +176,18 @@ class Settings:
"extremely incomplete so does not affect all weapons." "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 # Pilots and Squadrons
ai_pilot_levelling: bool = boolean_option( ai_pilot_levelling: bool = boolean_option(
"Allow AI pilot leveling", "Allow AI pilot leveling",

View File

@ -23,6 +23,7 @@ class AirWing:
self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list) self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
self.squadron_defs = SquadronDefLoader(game, faction).load() self.squadron_defs = SquadronDefLoader(game, faction).load()
self.squadron_def_generator = SquadronDefGenerator(faction) self.squadron_def_generator = SquadronDefGenerator(faction)
self.settings = game.settings
def unclaim_squadron_def(self, squadron: Squadron) -> None: def unclaim_squadron_def(self, squadron: Squadron) -> None:
if squadron.aircraft in self.squadron_defs: if squadron.aircraft in self.squadron_defs:
@ -66,6 +67,17 @@ class AirWing:
key=lambda s: best_aircraft.index(s.aircraft), 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 return ordered
def best_squadron_for( def best_squadron_for(

View File

@ -31,6 +31,7 @@ class Squadron:
role: str role: str
aircraft: AircraftType aircraft: AircraftType
livery: Optional[str] livery: Optional[str]
primary_task: FlightType
auto_assignable_mission_types: set[FlightType] auto_assignable_mission_types: set[FlightType]
operating_bases: OperatingBases operating_bases: OperatingBases
female_pilot_percentage: int female_pilot_percentage: int
@ -416,6 +417,7 @@ class Squadron:
def create_from( def create_from(
cls, cls,
squadron_def: SquadronDef, squadron_def: SquadronDef,
primary_task: FlightType,
base: ControlPoint, base: ControlPoint,
coalition: Coalition, coalition: Coalition,
game: Game, game: Game,
@ -428,6 +430,7 @@ class Squadron:
squadron_def.role, squadron_def.role,
squadron_def.aircraft, squadron_def.aircraft,
squadron_def.livery, squadron_def.livery,
primary_task,
squadron_def.auto_assignable_mission_types, squadron_def.auto_assignable_mission_types,
squadron_def.operating_bases, squadron_def.operating_bases,
squadron_def.female_pilot_percentage, squadron_def.female_pilot_percentage,

View File

@ -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()

View File

@ -38,6 +38,7 @@ from game.squadrons import AirWing, Pilot, Squadron
from game.squadrons.squadrondef import SquadronDef from game.squadrons.squadrondef import SquadronDef
from game.theater import ControlPoint from game.theater import ControlPoint
from qt_ui.uiconstants import AIRCRAFT_ICONS, ICONS from qt_ui.uiconstants import AIRCRAFT_ICONS, ICONS
from qt_ui.widgets.combos.primarytaskselector import PrimaryTaskSelector
class QMissionType(QCheckBox): class QMissionType(QCheckBox):
@ -210,6 +211,10 @@ class SquadronConfigurationBox(QGroupBox):
self.livery_selector = SquadronLiverySelector(squadron) self.livery_selector = SquadronLiverySelector(squadron)
left_column.addWidget(self.livery_selector) 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:")) left_column.addWidget(QLabel("Base:"))
self.base_selector = SquadronBaseSelector( self.base_selector = SquadronBaseSelector(
game.theater.control_points_for(squadron.player), game.theater.control_points_for(squadron.player),
@ -262,6 +267,7 @@ class SquadronConfigurationBox(QGroupBox):
try: try:
self.name_edit.setText(self.squadron.name) self.name_edit.setText(self.squadron.name)
self.nickname_edit.setText(self.squadron.nickname) 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.base_selector.setCurrentText(self.squadron.location.name)
self.player_list.setText( self.player_list.setText(
"<br />".join(p.name for p in self.claim_players_from_squadron()) "<br />".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) self.squadron.coalition.air_wing.unclaim_squadron_def(self.squadron)
squadron = Squadron.create_from( squadron = Squadron.create_from(
selected_def, selected_def,
self.squadron.primary_task,
self.squadron.location, self.squadron.location,
self.coalition, self.coalition,
self.game, self.game,
@ -331,6 +338,10 @@ class SquadronConfigurationBox(QGroupBox):
def apply(self) -> Squadron: def apply(self) -> Squadron:
self.squadron.name = self.name_edit.text() self.squadron.name = self.name_edit.text()
self.squadron.nickname = self.nickname_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() base = self.base_selector.currentData()
if base is None: if base is None:
raise RuntimeError("Base cannot be none") raise RuntimeError("Base cannot be none")
@ -583,6 +594,7 @@ class AirWingConfigurationTab(QWidget):
selected_type = popup.aircraft_type_selector.currentData() selected_type = popup.aircraft_type_selector.currentData()
selected_base = popup.squadron_base_selector.currentData() selected_base = popup.squadron_base_selector.currentData()
selected_task = popup.primary_task_selector.selected_task
selected_def = popup.squadron_def_selector.currentData() selected_def = popup.squadron_def_selector.currentData()
# Let user choose the preset or generate one # Let user choose the preset or generate one
@ -594,7 +606,7 @@ class AirWingConfigurationTab(QWidget):
) )
squadron = Squadron.create_from( squadron = Squadron.create_from(
squadron_def, selected_base, self.coalition, self.game squadron_def, selected_task, selected_base, self.coalition, self.game
) )
# Add Squadron # Add Squadron
@ -744,6 +756,12 @@ class SquadronConfigPopup(QDialog):
) )
self.column.addWidget(self.aircraft_type_selector) 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.column.addWidget(QLabel("Base:"))
self.squadron_base_selector = SquadronBaseSelector( self.squadron_base_selector = SquadronBaseSelector(
bases, None, self.aircraft_type_selector.currentData() bases, None, self.aircraft_type_selector.currentData()
@ -774,6 +792,7 @@ class SquadronConfigPopup(QDialog):
enabled = ( enabled = (
self.aircraft_type_selector.currentData() is not None self.aircraft_type_selector.currentData() is not None
and self.squadron_base_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) self.accept_button.setEnabled(enabled)
@ -784,6 +803,9 @@ class SquadronConfigPopup(QDialog):
self.squadron_def_selector.set_aircraft_type( self.squadron_def_selector.set_aircraft_type(
self.aircraft_type_selector.currentData() self.aircraft_type_selector.currentData()
) )
self.primary_task_selector.set_aircraft(
self.aircraft_type_selector.currentData()
)
self.update_accept_button() self.update_accept_button()
self.update() self.update()

View File

@ -26,6 +26,7 @@ from game.theater import ConflictTheater, ControlPoint
from qt_ui.delegates import TwoColumnRowDelegate from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.errorreporter import report_errors from qt_ui.errorreporter import report_errors
from qt_ui.models import AtoModel, SquadronModel from qt_ui.models import AtoModel, SquadronModel
from qt_ui.widgets.combos.primarytaskselector import PrimaryTaskSelector
class PilotDelegate(TwoColumnRowDelegate): class PilotDelegate(TwoColumnRowDelegate):
@ -161,8 +162,20 @@ class SquadronDialog(QDialog):
columns = QHBoxLayout() columns = QHBoxLayout()
layout.addLayout(columns) 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) 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 = PilotList(squadron_model)
self.pilot_list.selectionModel().selectionChanged.connect( self.pilot_list.selectionModel().selectionChanged.connect(
@ -298,3 +311,9 @@ class SquadronDialog(QDialog):
index = selected.indexes()[0] index = selected.indexes()[0]
self.reset_ai_toggle_state(index) self.reset_ai_toggle_state(index)
self.reset_leave_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