diff --git a/game/settings/__init__.py b/game/settings/__init__.py
index f0e7c7c5..21fb5f7e 100644
--- a/game/settings/__init__.py
+++ b/game/settings/__init__.py
@@ -1,5 +1,6 @@
from .booleanoption import BooleanOption
from .boundedfloatoption import BoundedFloatOption
+from .boundedintoption import BoundedIntOption
from .choicesoption import ChoicesOption
from .optiondescription import OptionDescription
from .settings import AutoAtoBehavior, Settings
diff --git a/game/settings/booleanoption.py b/game/settings/booleanoption.py
index 9fdf6589..7f1ed4e1 100644
--- a/game/settings/booleanoption.py
+++ b/game/settings/booleanoption.py
@@ -6,17 +6,22 @@ from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
@dataclass(frozen=True)
class BooleanOption(OptionDescription):
- pass
+ invert: bool
def boolean_option(
text: str,
page: str,
section: str,
+ default: bool,
+ invert: bool = False,
detail: Optional[str] = None,
**kwargs: Any,
) -> bool:
return field(
- metadata={SETTING_DESCRIPTION_KEY: BooleanOption(page, section, text, detail)},
+ metadata={
+ SETTING_DESCRIPTION_KEY: BooleanOption(page, section, text, detail, invert)
+ },
+ default=default,
**kwargs,
)
diff --git a/game/settings/boundedfloatoption.py b/game/settings/boundedfloatoption.py
index 0e6e6ee2..d2e15de7 100644
--- a/game/settings/boundedfloatoption.py
+++ b/game/settings/boundedfloatoption.py
@@ -15,6 +15,7 @@ def bounded_float_option(
text: str,
page: str,
section: str,
+ default: float,
min: float,
max: float,
divisor: int,
@@ -27,5 +28,6 @@ def bounded_float_option(
page, section, text, detail, min, max, divisor
)
},
+ default=default,
**kwargs,
)
diff --git a/game/settings/boundedintoption.py b/game/settings/boundedintoption.py
new file mode 100644
index 00000000..4cc9901b
--- /dev/null
+++ b/game/settings/boundedintoption.py
@@ -0,0 +1,31 @@
+from dataclasses import dataclass, field
+from typing import Any, Optional
+
+from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
+
+
+@dataclass(frozen=True)
+class BoundedIntOption(OptionDescription):
+ min: int
+ max: int
+
+
+def bounded_int_option(
+ text: str,
+ page: str,
+ section: str,
+ default: int,
+ min: int,
+ max: int,
+ detail: Optional[str] = None,
+ **kwargs: Any,
+) -> int:
+ return field(
+ metadata={
+ SETTING_DESCRIPTION_KEY: BoundedIntOption(
+ page, section, text, detail, min, max
+ )
+ },
+ default=default,
+ **kwargs,
+ )
diff --git a/game/settings/choicesoption.py b/game/settings/choicesoption.py
index 9e6f590a..737ae265 100644
--- a/game/settings/choicesoption.py
+++ b/game/settings/choicesoption.py
@@ -21,6 +21,7 @@ def choices_option(
text: str,
page: str,
section: str,
+ default: ValueT,
choices: Union[Iterable[str], Mapping[str, ValueT]],
detail: Optional[str] = None,
**kwargs: Any,
@@ -37,5 +38,6 @@ def choices_option(
dict(choices),
)
},
+ default=default,
**kwargs,
)
diff --git a/game/settings/settings.py b/game/settings/settings.py
index 794ed58e..40050cad 100644
--- a/game/settings/settings.py
+++ b/game/settings/settings.py
@@ -8,6 +8,7 @@ from dcs.forcedoptions import ForcedOptions
from .booleanoption import boolean_option
from .boundedfloatoption import bounded_float_option
+from .boundedintoption import bounded_int_option
from .choicesoption import choices_option
from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
from .skilloption import skill_option
@@ -27,6 +28,12 @@ AI_DIFFICULTY_SECTION = "AI Difficulty"
MISSION_DIFFICULTY_SECTION = "Mission Difficulty"
MISSION_RESTRICTIONS_SECTION = "Mission Restrictions"
+CAMPAIGN_MANAGEMENT_PAGE = "Campaign Management"
+
+GENERAL_SECTION = "General"
+PILOTS_AND_SQUADRONS_SECTION = "Pilots and Squadrons"
+HQ_AUTOMATION_SECTION = "HQ Automation"
+
@dataclass
class Settings:
@@ -130,27 +137,140 @@ class Settings:
# Campaign management
# General
- restrict_weapons_by_date: bool = False
- disable_legacy_aewc: bool = True
- disable_legacy_tanker: bool = True
+ restrict_weapons_by_date: bool = boolean_option(
+ "Restrict weapons by date (WIP)",
+ page=CAMPAIGN_MANAGEMENT_PAGE,
+ section=GENERAL_SECTION,
+ default=False,
+ detail=(
+ "Restricts weapon availability based on the campaign date. Data is "
+ "extremely incomplete so does not affect all weapons."
+ ),
+ )
+ disable_legacy_aewc: bool = boolean_option(
+ "Spawn invulnerable, always-available AEW&C aircraft (deprecated)",
+ page=CAMPAIGN_MANAGEMENT_PAGE,
+ section=GENERAL_SECTION,
+ default=True,
+ invert=True,
+ detail=(
+ "If checked, an invulnerable friendly AEW&C aircraft that begins the "
+ "mission on station will be be spawned. This behavior will be removed in a "
+ "future release."
+ ),
+ )
+ disable_legacy_tanker: bool = boolean_option(
+ "Spawn invulnerable, always-available tanker aircraft (deprecated)",
+ page=CAMPAIGN_MANAGEMENT_PAGE,
+ section=GENERAL_SECTION,
+ default=True,
+ invert=True,
+ detail=(
+ "If checked, an invulnerable friendly tanker aircraft that begins the "
+ "mission on station will be be spawned. This behavior will be removed in a "
+ "future release."
+ ),
+ )
# Pilots and Squadrons
- ai_pilot_levelling: bool = True
+ ai_pilot_levelling: bool = boolean_option(
+ "Allow AI pilot leveling",
+ CAMPAIGN_MANAGEMENT_PAGE,
+ PILOTS_AND_SQUADRONS_SECTION,
+ default=True,
+ detail=(
+ "Set whether or not AI pilots will level up after completing a number of"
+ " sorties. Since pilot level affects the AI skill, you may wish to disable"
+ " this, lest you face an Ace!"
+ ),
+ )
#: Feature flag for squadron limits.
- enable_squadron_pilot_limits: bool = False
+ enable_squadron_pilot_limits: bool = boolean_option(
+ "Enable per-squadron pilot limits (WIP)",
+ CAMPAIGN_MANAGEMENT_PAGE,
+ PILOTS_AND_SQUADRONS_SECTION,
+ default=False,
+ detail=(
+ "If set, squadrons will be limited to a maximum number of pilots and dead "
+ "pilots will replenish at a fixed rate, each defined with the settings"
+ "below. Auto-purchase may buy aircraft for which there are no pilots"
+ "available, so this feature is still a work-in-progress."
+ ),
+ )
#: The maximum number of pilots a squadron can have at one time. Changing this after
#: the campaign has started will have no immediate effect; pilots already in the
#: squadron will not be removed if the limit is lowered and pilots will not be
#: immediately created if the limit is raised.
- squadron_pilot_limit: int = 12
+ squadron_pilot_limit: int = bounded_int_option(
+ "Maximum number of pilots per squadron",
+ CAMPAIGN_MANAGEMENT_PAGE,
+ PILOTS_AND_SQUADRONS_SECTION,
+ default=12,
+ min=12,
+ max=72,
+ detail=(
+ "Sets the maximum number of pilots a squadron may have active. "
+ "Changing this value will not have an immediate effect, but will alter "
+ "replenishment for future turns."
+ ),
+ )
#: The number of pilots a squadron can replace per turn.
- squadron_replenishment_rate: int = 4
+ squadron_replenishment_rate: int = bounded_int_option(
+ "Squadron pilot replenishment rate",
+ CAMPAIGN_MANAGEMENT_PAGE,
+ PILOTS_AND_SQUADRONS_SECTION,
+ default=4,
+ min=1,
+ max=20,
+ detail=(
+ "Sets the maximum number of pilots that will be recruited to each squadron "
+ "at the end of each turn. Squadrons will not recruit new pilots beyond the "
+ "pilot limit, but each squadron with room for more pilots will recruit "
+ "this many pilots each turn up to the limit."
+ ),
+ )
+
# HQ Automation
- automate_runway_repair: bool = False
- automate_front_line_reinforcements: bool = False
- automate_aircraft_reinforcements: bool = False
- auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default
- auto_ato_player_missions_asap: bool = True
- automate_front_line_stance: bool = True
+ automate_runway_repair: bool = boolean_option(
+ "Automate runway repairs",
+ CAMPAIGN_MANAGEMENT_PAGE,
+ HQ_AUTOMATION_SECTION,
+ default=False,
+ )
+ automate_front_line_reinforcements: bool = boolean_option(
+ "Automate front-line purchases",
+ CAMPAIGN_MANAGEMENT_PAGE,
+ HQ_AUTOMATION_SECTION,
+ default=False,
+ )
+ automate_aircraft_reinforcements: bool = boolean_option(
+ "Automate aircraft purchases",
+ CAMPAIGN_MANAGEMENT_PAGE,
+ HQ_AUTOMATION_SECTION,
+ default=False,
+ )
+ auto_ato_behavior: AutoAtoBehavior = choices_option(
+ "Automatic package planning behavior",
+ CAMPAIGN_MANAGEMENT_PAGE,
+ HQ_AUTOMATION_SECTION,
+ default=AutoAtoBehavior.Default,
+ choices={v.value: v for v in AutoAtoBehavior},
+ detail=(
+ "Aircraft auto-purchase is directed by the auto-planner, so disabling "
+ "auto-planning disables auto-purchase."
+ ),
+ )
+ auto_ato_player_missions_asap: bool = boolean_option(
+ "Automatically generated packages with players are scheduled ASAP",
+ CAMPAIGN_MANAGEMENT_PAGE,
+ HQ_AUTOMATION_SECTION,
+ default=True,
+ )
+ automate_front_line_stance: bool = boolean_option(
+ "Automatically manage front line stances",
+ CAMPAIGN_MANAGEMENT_PAGE,
+ HQ_AUTOMATION_SECTION,
+ default=True,
+ )
reserves_procurement_target: int = 10
# Mission Generator
diff --git a/game/settings/skilloption.py b/game/settings/skilloption.py
index f13473dc..59d26611 100644
--- a/game/settings/skilloption.py
+++ b/game/settings/skilloption.py
@@ -7,6 +7,7 @@ def skill_option(
text: str,
page: str,
section: str,
+ default: str,
detail: Optional[str] = None,
**kwargs: Any,
) -> str:
@@ -14,6 +15,7 @@ def skill_option(
text,
page,
section,
+ default,
["Average", "Good", "High", "Excellent"],
detail=detail,
**kwargs,
diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py
index f250216c..b935538f 100644
--- a/qt_ui/uiconstants.py
+++ b/qt_ui/uiconstants.py
@@ -78,6 +78,7 @@ def load_icons():
ICONS["Money"] = QPixmap(
"./resources/ui/misc/" + get_theme_icons() + "/money_icon.png"
)
+ ICONS["Campaign Management"] = ICONS["Money"]
ICONS["PassTurn"] = QPixmap(
"./resources/ui/misc/" + get_theme_icons() + "/hourglass.png"
)
diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py
index 3deb82d3..7adce5f7 100644
--- a/qt_ui/windows/settings/QSettingsWindow.py
+++ b/qt_ui/windows/settings/QSettingsWindow.py
@@ -23,12 +23,12 @@ from PySide2.QtWidgets import (
import qt_ui.uiconstants as CONST
from game.game import Game
from game.settings import (
- Settings,
- AutoAtoBehavior,
- OptionDescription,
BooleanOption,
- ChoicesOption,
BoundedFloatOption,
+ BoundedIntOption,
+ ChoicesOption,
+ OptionDescription,
+ Settings,
)
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.widgets.spinsliders import FloatSpinSlider, TimeInputs
@@ -81,128 +81,6 @@ class CheatSettingsBox(QGroupBox):
return self.base_capture_cheat_checkbox.isChecked()
-class AutoAtoBehaviorSelector(QComboBox):
- def __init__(self, default: AutoAtoBehavior) -> None:
- super().__init__()
-
- for behavior in AutoAtoBehavior:
- self.addItem(behavior.value, behavior)
- self.setCurrentText(default.value)
-
-
-class HqAutomationSettingsBox(QGroupBox):
- def __init__(self, game: Game) -> None:
- super().__init__("HQ Automation")
- self.game = game
-
- layout = QGridLayout()
- self.setLayout(layout)
-
- runway_repair = QCheckBox()
- runway_repair.setChecked(self.game.settings.automate_runway_repair)
- runway_repair.toggled.connect(self.set_runway_automation)
-
- layout.addWidget(QLabel("Automate runway repairs"), 0, 0)
- layout.addWidget(runway_repair, 0, 1, Qt.AlignRight)
-
- front_line = QCheckBox()
- front_line.setChecked(self.game.settings.automate_front_line_reinforcements)
- front_line.toggled.connect(self.set_front_line_reinforcement_automation)
-
- layout.addWidget(QLabel("Automate front-line purchases"), 1, 0)
- layout.addWidget(front_line, 1, 1, Qt.AlignRight)
-
- self.automate_aircraft_reinforcements = QCheckBox()
- self.automate_aircraft_reinforcements.setChecked(
- self.game.settings.automate_aircraft_reinforcements
- )
- self.automate_aircraft_reinforcements.toggled.connect(
- self.set_aircraft_automation
- )
-
- layout.addWidget(QLabel("Automate aircraft purchases"), 2, 0)
- layout.addWidget(self.automate_aircraft_reinforcements, 2, 1, Qt.AlignRight)
-
- self.auto_ato_behavior = AutoAtoBehaviorSelector(
- self.game.settings.auto_ato_behavior
- )
- self.auto_ato_behavior.currentIndexChanged.connect(self.set_auto_ato_behavior)
- layout.addWidget(
- QLabel(
- "Automatic package planning behavior
"
- "Aircraft auto-purchase is directed by the auto-planner,
"
- "so disabling auto-planning disables auto-purchase."
- ),
- 3,
- 0,
- )
- layout.addWidget(self.auto_ato_behavior, 3, 1)
-
- self.auto_ato_player_missions_asap = QCheckBox()
- self.auto_ato_player_missions_asap.setChecked(
- self.game.settings.auto_ato_player_missions_asap
- )
- self.auto_ato_player_missions_asap.toggled.connect(
- self.set_auto_ato_player_missions_asap
- )
-
- layout.addWidget(
- QLabel("Automatically generated packages with players are scheduled ASAP"),
- 4,
- 0,
- )
- layout.addWidget(self.auto_ato_player_missions_asap, 4, 1, Qt.AlignRight)
-
- self.automate_front_line_stance = QCheckBox()
- self.automate_front_line_stance.setChecked(
- self.game.settings.automate_front_line_stance
- )
- self.automate_front_line_stance.toggled.connect(
- self.set_front_line_stance_automation
- )
-
- layout.addWidget(
- QLabel("Automatically manage front line stances"),
- 5,
- 0,
- )
- layout.addWidget(self.automate_front_line_stance, 5, 1, Qt.AlignRight)
-
- def set_runway_automation(self, value: bool) -> None:
- self.game.settings.automate_runway_repair = value
-
- def set_front_line_reinforcement_automation(self, value: bool) -> None:
- self.game.settings.automate_front_line_reinforcements = value
-
- def set_front_line_stance_automation(self, value: bool) -> None:
- self.game.settings.automate_front_line_stance = value
-
- def set_aircraft_automation(self, value: bool) -> None:
- self.game.settings.automate_aircraft_reinforcements = value
-
- def set_auto_ato_behavior(self, index: int) -> None:
- behavior = self.auto_ato_behavior.itemData(index)
- self.game.settings.auto_ato_behavior = behavior
- if behavior in (AutoAtoBehavior.Disabled, AutoAtoBehavior.Never):
- self.auto_ato_player_missions_asap.setChecked(False)
- self.auto_ato_player_missions_asap.setEnabled(False)
- if behavior is AutoAtoBehavior.Disabled:
- self.automate_aircraft_reinforcements.setChecked(False)
- self.automate_aircraft_reinforcements.setEnabled(False)
- else:
- self.auto_ato_player_missions_asap.setEnabled(True)
- self.auto_ato_player_missions_asap.setChecked(
- self.game.settings.auto_ato_player_missions_asap
- )
- self.automate_aircraft_reinforcements.setEnabled(True)
- self.automate_aircraft_reinforcements.setChecked(
- self.game.settings.automate_aircraft_reinforcements
- )
-
- def set_auto_ato_player_missions_asap(self, value: bool) -> None:
- self.game.settings.auto_ato_player_missions_asap = value
-
-
class PilotSettingsBox(QGroupBox):
def __init__(self, game: Game) -> None:
super().__init__("Pilots and Squadrons")
@@ -336,11 +214,13 @@ class AutoSettingsLayout(QGridLayout):
for row, (name, description) in enumerate(Settings.fields(page, section)):
self.add_label(row, description)
if isinstance(description, BooleanOption):
- self.add_checkbox_for(row, name)
+ self.add_checkbox_for(row, name, description)
elif isinstance(description, ChoicesOption):
self.add_combobox_for(row, name, description)
elif isinstance(description, BoundedFloatOption):
self.add_float_spin_slider_for(row, name, description)
+ elif isinstance(description, BoundedIntOption):
+ self.add_spinner_for(row, name, description)
else:
raise TypeError(f"Unhandled option type: {description}")
@@ -352,12 +232,17 @@ class AutoSettingsLayout(QGridLayout):
label = QLabel(text)
self.addWidget(label, row, 0)
- def add_checkbox_for(self, row: int, name: str) -> None:
+ def add_checkbox_for(self, row: int, name: str, description: BooleanOption) -> None:
def on_toggle(value: bool) -> None:
+ if description.invert:
+ value = not value
self.settings.__dict__[name] = value
checkbox = QCheckBox()
- checkbox.setChecked(self.settings.__dict__[name])
+ value = self.settings.__dict__[name]
+ if description.invert:
+ value = not value
+ checkbox.setChecked(value)
checkbox.toggled.connect(on_toggle)
self.addWidget(checkbox, row, 1, Qt.AlignRight)
@@ -384,8 +269,27 @@ class AutoSettingsLayout(QGridLayout):
self.settings.__dict__[name],
divisor=description.divisor,
)
+
+ def on_changed() -> None:
+ self.settings.__dict__[name] = spinner.value
+
+ spinner.spinner.valueChanged.connect(on_changed)
self.addLayout(spinner, row, 1, Qt.AlignRight)
+ def add_spinner_for(
+ self, row: int, name: str, description: BoundedIntOption
+ ) -> None:
+ def on_changed(value: int) -> None:
+ self.settings.__dict__[name] = value
+
+ spinner = QSpinBox()
+ spinner.setMinimum(description.min)
+ spinner.setMaximum(description.max)
+ spinner.setValue(self.settings.__dict__[name])
+
+ spinner.valueChanged.connect(on_changed)
+ self.addWidget(spinner, row, 1, Qt.AlignRight)
+
class AutoSettingsGroup(QGroupBox):
def __init__(self, page: str, section: str, settings: Settings) -> None:
@@ -415,7 +319,6 @@ class QSettingsWindow(QDialog):
self.game = game
self.pluginsPage = None
self.pluginsOptionsPage = None
- self.campaign_management_page = QWidget()
self.pages: dict[str, AutoSettingsPage] = {}
for page in Settings.pages():
@@ -451,14 +354,6 @@ class QSettingsWindow(QDialog):
self.categoryModel.appendRow(page_item)
self.right_layout.addWidget(page)
- self.init_campaign_management_layout()
- campaign_management = QStandardItem("Campaign Management")
- campaign_management.setIcon(CONST.ICONS["Money"])
- campaign_management.setEditable(False)
- campaign_management.setSelectable(True)
- self.categoryModel.appendRow(campaign_management)
- self.right_layout.addWidget(self.campaign_management_page)
-
self.initGeneratorLayout()
generator = QStandardItem("Mission Generator")
generator.setIcon(CONST.ICONS["Generator"])
@@ -508,82 +403,6 @@ class QSettingsWindow(QDialog):
def init(self):
pass
- def init_campaign_management_layout(self) -> None:
- campaign_layout = QVBoxLayout()
- campaign_layout.setAlignment(Qt.AlignTop)
- self.campaign_management_page.setLayout(campaign_layout)
-
- general = QGroupBox("General")
- campaign_layout.addWidget(general)
-
- general_layout = QGridLayout()
- general.setLayout(general_layout)
-
- def set_restict_weapons_by_date(value: bool) -> None:
- self.game.settings.restrict_weapons_by_date = value
-
- restrict_weapons = QCheckBox()
- restrict_weapons.setChecked(self.game.settings.restrict_weapons_by_date)
- restrict_weapons.toggled.connect(set_restict_weapons_by_date)
-
- tooltip_text = (
- "Restricts weapon availability based on the campaign date. Data is "
- "extremely incomplete so does not affect all weapons."
- )
- restrict_weapons.setToolTip(tooltip_text)
- restrict_weapons_label = QLabel("Restrict weapons by date (WIP)")
- restrict_weapons_label.setToolTip(tooltip_text)
-
- general_layout.addWidget(restrict_weapons_label, 0, 0)
- general_layout.addWidget(restrict_weapons, 0, 1, Qt.AlignRight)
-
- def set_old_awec(value: bool) -> None:
- self.game.settings.disable_legacy_aewc = not value
-
- old_awac = QCheckBox()
- old_awac.setChecked(not self.game.settings.disable_legacy_aewc)
- old_awac.toggled.connect(set_old_awec)
-
- old_awec_info = (
- "If checked, an invulnerable friendly AEW&C aircraft that begins the "
- "mission on station will be be spawned. This behavior will be removed in a "
- "future release."
- )
-
- old_awac.setToolTip(old_awec_info)
- old_awac_label = QLabel(
- "Spawn invulnerable, always-available AEW&C aircraft (deprecated)"
- )
- old_awac_label.setToolTip(old_awec_info)
-
- general_layout.addWidget(old_awac_label, 1, 0)
- general_layout.addWidget(old_awac, 1, 1, Qt.AlignRight)
-
- def set_old_tanker(value: bool) -> None:
- self.game.settings.disable_legacy_tanker = not value
-
- old_tanker = QCheckBox()
- old_tanker.setChecked(not self.game.settings.disable_legacy_tanker)
- old_tanker.toggled.connect(set_old_tanker)
-
- old_tanker_info = (
- "If checked, an invulnerable friendly Tanker aircraft that begins the "
- "mission on station will be be spawned. This behavior will be removed in a "
- "future release."
- )
-
- old_tanker.setToolTip(old_tanker_info)
- old_tanker_label = QLabel(
- "Spawn invulnerable, always-available Tanker aircraft (deprecated)"
- )
- old_tanker_label.setToolTip(old_tanker_info)
-
- general_layout.addWidget(old_tanker_label, 2, 0)
- general_layout.addWidget(old_tanker, 2, 1, Qt.AlignRight)
-
- campaign_layout.addWidget(PilotSettingsBox(self.game))
- campaign_layout.addWidget(HqAutomationSettingsBox(self.game))
-
def initGeneratorLayout(self):
self.generatorPage = QWidget()
self.generatorLayout = QVBoxLayout()