Port the campaign management page to auto.

Also fixes the oversight in the previous commit where float options were
not saved when changed.
This commit is contained in:
Dan Albert 2021-10-21 19:15:47 -07:00
parent 7bec4c62f7
commit a618f00662
9 changed files with 212 additions and 229 deletions

View File

@ -1,5 +1,6 @@
from .booleanoption import BooleanOption from .booleanoption import BooleanOption
from .boundedfloatoption import BoundedFloatOption from .boundedfloatoption import BoundedFloatOption
from .boundedintoption import BoundedIntOption
from .choicesoption import ChoicesOption from .choicesoption import ChoicesOption
from .optiondescription import OptionDescription from .optiondescription import OptionDescription
from .settings import AutoAtoBehavior, Settings from .settings import AutoAtoBehavior, Settings

View File

@ -6,17 +6,22 @@ from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
@dataclass(frozen=True) @dataclass(frozen=True)
class BooleanOption(OptionDescription): class BooleanOption(OptionDescription):
pass invert: bool
def boolean_option( def boolean_option(
text: str, text: str,
page: str, page: str,
section: str, section: str,
default: bool,
invert: bool = False,
detail: Optional[str] = None, detail: Optional[str] = None,
**kwargs: Any, **kwargs: Any,
) -> bool: ) -> bool:
return field( return field(
metadata={SETTING_DESCRIPTION_KEY: BooleanOption(page, section, text, detail)}, metadata={
SETTING_DESCRIPTION_KEY: BooleanOption(page, section, text, detail, invert)
},
default=default,
**kwargs, **kwargs,
) )

View File

@ -15,6 +15,7 @@ def bounded_float_option(
text: str, text: str,
page: str, page: str,
section: str, section: str,
default: float,
min: float, min: float,
max: float, max: float,
divisor: int, divisor: int,
@ -27,5 +28,6 @@ def bounded_float_option(
page, section, text, detail, min, max, divisor page, section, text, detail, min, max, divisor
) )
}, },
default=default,
**kwargs, **kwargs,
) )

View File

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

View File

@ -21,6 +21,7 @@ def choices_option(
text: str, text: str,
page: str, page: str,
section: str, section: str,
default: ValueT,
choices: Union[Iterable[str], Mapping[str, ValueT]], choices: Union[Iterable[str], Mapping[str, ValueT]],
detail: Optional[str] = None, detail: Optional[str] = None,
**kwargs: Any, **kwargs: Any,
@ -37,5 +38,6 @@ def choices_option(
dict(choices), dict(choices),
) )
}, },
default=default,
**kwargs, **kwargs,
) )

View File

@ -8,6 +8,7 @@ from dcs.forcedoptions import ForcedOptions
from .booleanoption import boolean_option from .booleanoption import boolean_option
from .boundedfloatoption import bounded_float_option from .boundedfloatoption import bounded_float_option
from .boundedintoption import bounded_int_option
from .choicesoption import choices_option from .choicesoption import choices_option
from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
from .skilloption import skill_option from .skilloption import skill_option
@ -27,6 +28,12 @@ AI_DIFFICULTY_SECTION = "AI Difficulty"
MISSION_DIFFICULTY_SECTION = "Mission Difficulty" MISSION_DIFFICULTY_SECTION = "Mission Difficulty"
MISSION_RESTRICTIONS_SECTION = "Mission Restrictions" 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 @dataclass
class Settings: class Settings:
@ -130,27 +137,140 @@ class Settings:
# Campaign management # Campaign management
# General # General
restrict_weapons_by_date: bool = False restrict_weapons_by_date: bool = boolean_option(
disable_legacy_aewc: bool = True "Restrict weapons by date (WIP)",
disable_legacy_tanker: bool = True 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 # 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. #: 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 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 #: 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 #: squadron will not be removed if the limit is lowered and pilots will not be
#: immediately created if the limit is raised. #: 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. #: 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 # HQ Automation
automate_runway_repair: bool = False automate_runway_repair: bool = boolean_option(
automate_front_line_reinforcements: bool = False "Automate runway repairs",
automate_aircraft_reinforcements: bool = False CAMPAIGN_MANAGEMENT_PAGE,
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default HQ_AUTOMATION_SECTION,
auto_ato_player_missions_asap: bool = True default=False,
automate_front_line_stance: bool = True )
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 reserves_procurement_target: int = 10
# Mission Generator # Mission Generator

View File

@ -7,6 +7,7 @@ def skill_option(
text: str, text: str,
page: str, page: str,
section: str, section: str,
default: str,
detail: Optional[str] = None, detail: Optional[str] = None,
**kwargs: Any, **kwargs: Any,
) -> str: ) -> str:
@ -14,6 +15,7 @@ def skill_option(
text, text,
page, page,
section, section,
default,
["Average", "Good", "High", "Excellent"], ["Average", "Good", "High", "Excellent"],
detail=detail, detail=detail,
**kwargs, **kwargs,

View File

@ -78,6 +78,7 @@ def load_icons():
ICONS["Money"] = QPixmap( ICONS["Money"] = QPixmap(
"./resources/ui/misc/" + get_theme_icons() + "/money_icon.png" "./resources/ui/misc/" + get_theme_icons() + "/money_icon.png"
) )
ICONS["Campaign Management"] = ICONS["Money"]
ICONS["PassTurn"] = QPixmap( ICONS["PassTurn"] = QPixmap(
"./resources/ui/misc/" + get_theme_icons() + "/hourglass.png" "./resources/ui/misc/" + get_theme_icons() + "/hourglass.png"
) )

View File

@ -23,12 +23,12 @@ from PySide2.QtWidgets import (
import qt_ui.uiconstants as CONST import qt_ui.uiconstants as CONST
from game.game import Game from game.game import Game
from game.settings import ( from game.settings import (
Settings,
AutoAtoBehavior,
OptionDescription,
BooleanOption, BooleanOption,
ChoicesOption,
BoundedFloatOption, BoundedFloatOption,
BoundedIntOption,
ChoicesOption,
OptionDescription,
Settings,
) )
from qt_ui.widgets.QLabeledWidget import QLabeledWidget from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.widgets.spinsliders import FloatSpinSlider, TimeInputs from qt_ui.widgets.spinsliders import FloatSpinSlider, TimeInputs
@ -81,128 +81,6 @@ class CheatSettingsBox(QGroupBox):
return self.base_capture_cheat_checkbox.isChecked() 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<br>"
"<strong>Aircraft auto-purchase is directed by the auto-planner,<br />"
"so disabling auto-planning disables auto-purchase.</strong>"
),
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): class PilotSettingsBox(QGroupBox):
def __init__(self, game: Game) -> None: def __init__(self, game: Game) -> None:
super().__init__("Pilots and Squadrons") super().__init__("Pilots and Squadrons")
@ -336,11 +214,13 @@ class AutoSettingsLayout(QGridLayout):
for row, (name, description) in enumerate(Settings.fields(page, section)): for row, (name, description) in enumerate(Settings.fields(page, section)):
self.add_label(row, description) self.add_label(row, description)
if isinstance(description, BooleanOption): if isinstance(description, BooleanOption):
self.add_checkbox_for(row, name) self.add_checkbox_for(row, name, description)
elif isinstance(description, ChoicesOption): elif isinstance(description, ChoicesOption):
self.add_combobox_for(row, name, description) self.add_combobox_for(row, name, description)
elif isinstance(description, BoundedFloatOption): elif isinstance(description, BoundedFloatOption):
self.add_float_spin_slider_for(row, name, description) self.add_float_spin_slider_for(row, name, description)
elif isinstance(description, BoundedIntOption):
self.add_spinner_for(row, name, description)
else: else:
raise TypeError(f"Unhandled option type: {description}") raise TypeError(f"Unhandled option type: {description}")
@ -352,12 +232,17 @@ class AutoSettingsLayout(QGridLayout):
label = QLabel(text) label = QLabel(text)
self.addWidget(label, row, 0) 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: def on_toggle(value: bool) -> None:
if description.invert:
value = not value
self.settings.__dict__[name] = value self.settings.__dict__[name] = value
checkbox = QCheckBox() 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) checkbox.toggled.connect(on_toggle)
self.addWidget(checkbox, row, 1, Qt.AlignRight) self.addWidget(checkbox, row, 1, Qt.AlignRight)
@ -384,8 +269,27 @@ class AutoSettingsLayout(QGridLayout):
self.settings.__dict__[name], self.settings.__dict__[name],
divisor=description.divisor, 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) 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): class AutoSettingsGroup(QGroupBox):
def __init__(self, page: str, section: str, settings: Settings) -> None: def __init__(self, page: str, section: str, settings: Settings) -> None:
@ -415,7 +319,6 @@ class QSettingsWindow(QDialog):
self.game = game self.game = game
self.pluginsPage = None self.pluginsPage = None
self.pluginsOptionsPage = None self.pluginsOptionsPage = None
self.campaign_management_page = QWidget()
self.pages: dict[str, AutoSettingsPage] = {} self.pages: dict[str, AutoSettingsPage] = {}
for page in Settings.pages(): for page in Settings.pages():
@ -451,14 +354,6 @@ class QSettingsWindow(QDialog):
self.categoryModel.appendRow(page_item) self.categoryModel.appendRow(page_item)
self.right_layout.addWidget(page) 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() self.initGeneratorLayout()
generator = QStandardItem("Mission Generator") generator = QStandardItem("Mission Generator")
generator.setIcon(CONST.ICONS["Generator"]) generator.setIcon(CONST.ICONS["Generator"])
@ -508,82 +403,6 @@ class QSettingsWindow(QDialog):
def init(self): def init(self):
pass 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): def initGeneratorLayout(self):
self.generatorPage = QWidget() self.generatorPage = QWidget()
self.generatorLayout = QVBoxLayout() self.generatorLayout = QVBoxLayout()