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 .boundedfloatoption import BoundedFloatOption
from .boundedintoption import BoundedIntOption
from .choicesoption import ChoicesOption
from .optiondescription import OptionDescription
from .settings import AutoAtoBehavior, Settings

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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