From a618f00662faaf943e0d5eb0895f126029aca3ff Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 21 Oct 2021 19:15:47 -0700 Subject: [PATCH] Port the campaign management page to auto. Also fixes the oversight in the previous commit where float options were not saved when changed. --- game/settings/__init__.py | 1 + game/settings/booleanoption.py | 9 +- game/settings/boundedfloatoption.py | 2 + game/settings/boundedintoption.py | 31 +++ game/settings/choicesoption.py | 2 + game/settings/settings.py | 146 +++++++++++-- game/settings/skilloption.py | 2 + qt_ui/uiconstants.py | 1 + qt_ui/windows/settings/QSettingsWindow.py | 247 +++------------------- 9 files changed, 212 insertions(+), 229 deletions(-) create mode 100644 game/settings/boundedintoption.py 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()