Generate settings pages automatically.

This adds metadata to settings fields that can be used to automatically
generate the settings window. For now I have replaced the Difficulty
page. Will follow up to replace the others.
This commit is contained in:
Dan Albert 2021-09-12 16:44:01 -07:00
parent 39fae9effc
commit 7bec4c62f7
13 changed files with 518 additions and 329 deletions

View File

@ -1,119 +0,0 @@
from dataclasses import dataclass, field
from datetime import timedelta
from enum import Enum, unique
from typing import Dict, Optional, Any
from dcs.forcedoptions import ForcedOptions
@unique
class AutoAtoBehavior(Enum):
Disabled = "Disabled"
Never = "Never assign player pilots"
Default = "No preference"
Prefer = "Prefer player pilots"
@dataclass
class Settings:
version: Optional[str] = None
# Difficulty settings
# AI Difficulty
player_skill: str = "Good"
enemy_skill: str = "Average"
enemy_vehicle_skill: str = "Average"
player_income_multiplier: float = 1.0
enemy_income_multiplier: float = 1.0
invulnerable_player_pilots: bool = True
# Mission Difficulty
night_disabled: bool = False
manpads: bool = True
# Mission Restrictions
labels: str = "Full"
map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All
external_views_allowed: bool = True
battle_damage_assessment: Optional[bool] = None
# Campaign management
# General
restrict_weapons_by_date: bool = False
disable_legacy_aewc: bool = True
disable_legacy_tanker: bool = True
# Pilots and Squadrons
ai_pilot_levelling: bool = True
#: Feature flag for squadron limits.
enable_squadron_pilot_limits: bool = False
#: 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
#: The number of pilots a squadron can replace per turn.
squadron_replenishment_rate: int = 4
# 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
reserves_procurement_target: int = 10
# Mission Generator
# Gameplay
supercarrier: bool = False
generate_marks: bool = True
generate_dark_kneeboard: bool = False
never_delay_player_flights: bool = False
default_start_type: str = "Cold"
# Mission specific
desired_player_mission_duration: timedelta = timedelta(minutes=60)
# Performance
perf_smoke_gen: bool = True
perf_smoke_spacing = 1600
perf_red_alert_state: bool = True
perf_artillery: bool = True
perf_moving_units: bool = True
perf_infantry: bool = True
perf_destroyed_units: bool = True
# Performance culling
perf_culling: bool = False
perf_culling_distance: int = 100
perf_do_not_cull_carrier = True
# Cheating
show_red_ato: bool = False
enable_frontline_cheats: bool = False
enable_base_capture_cheat: bool = False
# LUA Plugins system
plugins: Dict[str, bool] = field(default_factory=dict)
only_player_takeoff: bool = True # Legacy parameter do not use
@staticmethod
def plugin_settings_key(identifier: str) -> str:
return f"plugins.{identifier}"
def initialize_plugin_option(self, identifier: str, default_value: bool) -> None:
try:
self.plugin_option(identifier)
except KeyError:
self.set_plugin_option(identifier, default_value)
def plugin_option(self, identifier: str) -> bool:
return self.plugins[self.plugin_settings_key(identifier)]
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
self.plugins[self.plugin_settings_key(identifier)] = enabled
def __setstate__(self, state: dict[str, Any]) -> None:
# __setstate__ is called with the dict of the object being unpickled. We
# can provide save compatibility for new settings options (which
# normally would not be present in the unpickled object) by creating a
# new settings object, updating it with the unpickled state, and
# updating our dict with that.
new_state = Settings().__dict__
new_state.update(state)
self.__dict__.update(new_state)

View File

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

View File

@ -0,0 +1,22 @@
from dataclasses import dataclass, field
from typing import Any, Optional
from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
@dataclass(frozen=True)
class BooleanOption(OptionDescription):
pass
def boolean_option(
text: str,
page: str,
section: str,
detail: Optional[str] = None,
**kwargs: Any,
) -> bool:
return field(
metadata={SETTING_DESCRIPTION_KEY: BooleanOption(page, section, text, detail)},
**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 BoundedFloatOption(OptionDescription):
min: float
max: float
divisor: int
def bounded_float_option(
text: str,
page: str,
section: str,
min: float,
max: float,
divisor: int,
detail: Optional[str] = None,
**kwargs: Any,
) -> float:
return field(
metadata={
SETTING_DESCRIPTION_KEY: BoundedFloatOption(
page, section, text, detail, min, max, divisor
)
},
**kwargs,
)

View File

@ -0,0 +1,41 @@
from dataclasses import dataclass, field
from typing import Any, Generic, Iterable, Mapping, Optional, TypeVar, Union
from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
ValueT = TypeVar("ValueT")
@dataclass(frozen=True)
class ChoicesOption(OptionDescription, Generic[ValueT]):
choices: dict[str, ValueT]
def text_for_value(self, value: ValueT) -> str:
for text, _value in self.choices.items():
if value == _value:
return text
raise ValueError(f"{self} does not contain {value}")
def choices_option(
text: str,
page: str,
section: str,
choices: Union[Iterable[str], Mapping[str, ValueT]],
detail: Optional[str] = None,
**kwargs: Any,
) -> ValueT:
if not isinstance(choices, Mapping):
choices = {c: c for c in choices}
return field(
metadata={
SETTING_DESCRIPTION_KEY: ChoicesOption(
page,
section,
text,
detail,
dict(choices),
)
},
**kwargs,
)

View File

@ -0,0 +1,13 @@
from dataclasses import dataclass
from typing import Optional
SETTING_DESCRIPTION_KEY = "DCS_LIBERATION_SETTING_DESCRIPTION_KEY"
@dataclass(frozen=True)
class OptionDescription:
page: str
section: str
text: str
detail: Optional[str]

247
game/settings/settings.py Normal file
View File

@ -0,0 +1,247 @@
from collections import Iterator
from dataclasses import Field, dataclass, field, fields
from datetime import timedelta
from enum import Enum, unique
from typing import Any, Dict, Optional
from dcs.forcedoptions import ForcedOptions
from .booleanoption import boolean_option
from .boundedfloatoption import bounded_float_option
from .choicesoption import choices_option
from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
from .skilloption import skill_option
@unique
class AutoAtoBehavior(Enum):
Disabled = "Disabled"
Never = "Never assign player pilots"
Default = "No preference"
Prefer = "Prefer player pilots"
DIFFICULTY_PAGE = "Difficulty"
AI_DIFFICULTY_SECTION = "AI Difficulty"
MISSION_DIFFICULTY_SECTION = "Mission Difficulty"
MISSION_RESTRICTIONS_SECTION = "Mission Restrictions"
@dataclass
class Settings:
version: Optional[str] = None
# Difficulty settings
# AI Difficulty
player_skill: str = skill_option(
"Player coalition skill",
page=DIFFICULTY_PAGE,
section=AI_DIFFICULTY_SECTION,
default="Good",
)
enemy_skill: str = skill_option(
"Enemy coalition skill",
page=DIFFICULTY_PAGE,
section=AI_DIFFICULTY_SECTION,
default="Average",
)
enemy_vehicle_skill: str = skill_option(
"Enemy AA and vehicles skill",
page=DIFFICULTY_PAGE,
section=AI_DIFFICULTY_SECTION,
default="Average",
)
player_income_multiplier: float = bounded_float_option(
"Player income multiplier",
page=DIFFICULTY_PAGE,
section=AI_DIFFICULTY_SECTION,
min=0,
max=5,
divisor=10,
default=1.0,
)
enemy_income_multiplier: float = bounded_float_option(
"Enemy income multiplier",
page=DIFFICULTY_PAGE,
section=AI_DIFFICULTY_SECTION,
min=0,
max=5,
divisor=10,
default=1.0,
)
invulnerable_player_pilots: bool = boolean_option(
"Player pilots cannot be killed",
page=DIFFICULTY_PAGE,
section=AI_DIFFICULTY_SECTION,
detail=(
"Aircraft are vulnerable, but the player's pilot will be returned to the "
"squadron at the end of the mission"
),
default=True,
)
# Mission Difficulty
manpads: bool = boolean_option(
"Manpads on frontlines",
page=DIFFICULTY_PAGE,
section=MISSION_DIFFICULTY_SECTION,
default=True,
)
night_disabled: bool = boolean_option(
"No night missions",
page=DIFFICULTY_PAGE,
section=MISSION_DIFFICULTY_SECTION,
default=False,
)
# Mission Restrictions
labels: str = choices_option(
"In game labels",
page=DIFFICULTY_PAGE,
section=MISSION_RESTRICTIONS_SECTION,
choices=["Full", "Abbreviated", "Dot Only", "Neutral Dot", "Off"],
default="Full",
)
map_coalition_visibility: ForcedOptions.Views = choices_option(
"Map visibility options",
page=DIFFICULTY_PAGE,
section=MISSION_RESTRICTIONS_SECTION,
choices={
"All": ForcedOptions.Views.All,
"Fog of war": ForcedOptions.Views.Allies,
"Allies only": ForcedOptions.Views.OnlyAllies,
"Own aircraft only": ForcedOptions.Views.MyAircraft,
"Map only": ForcedOptions.Views.OnlyMap,
},
default=ForcedOptions.Views.All,
)
external_views_allowed: bool = boolean_option(
"Allow external views",
DIFFICULTY_PAGE,
MISSION_RESTRICTIONS_SECTION,
default=True,
)
battle_damage_assessment: Optional[bool] = choices_option(
"Battle damage assessment",
page=DIFFICULTY_PAGE,
section=MISSION_RESTRICTIONS_SECTION,
choices={"Player preference": None, "Enforced on": True, "Enforced off": False},
default=None,
)
# Campaign management
# General
restrict_weapons_by_date: bool = False
disable_legacy_aewc: bool = True
disable_legacy_tanker: bool = True
# Pilots and Squadrons
ai_pilot_levelling: bool = True
#: Feature flag for squadron limits.
enable_squadron_pilot_limits: bool = False
#: 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
#: The number of pilots a squadron can replace per turn.
squadron_replenishment_rate: int = 4
# 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
reserves_procurement_target: int = 10
# Mission Generator
# Gameplay
supercarrier: bool = False
generate_marks: bool = True
generate_dark_kneeboard: bool = False
never_delay_player_flights: bool = False
default_start_type: str = "Cold"
# Mission specific
desired_player_mission_duration: timedelta = timedelta(minutes=60)
# Performance
perf_smoke_gen: bool = True
perf_smoke_spacing = 1600
perf_red_alert_state: bool = True
perf_artillery: bool = True
perf_moving_units: bool = True
perf_infantry: bool = True
perf_destroyed_units: bool = True
# Performance culling
perf_culling: bool = False
perf_culling_distance: int = 100
perf_do_not_cull_carrier = True
# Cheating
show_red_ato: bool = False
enable_frontline_cheats: bool = False
enable_base_capture_cheat: bool = False
# LUA Plugins system
plugins: Dict[str, bool] = field(default_factory=dict)
only_player_takeoff: bool = True # Legacy parameter do not use
@staticmethod
def plugin_settings_key(identifier: str) -> str:
return f"plugins.{identifier}"
def initialize_plugin_option(self, identifier: str, default_value: bool) -> None:
try:
self.plugin_option(identifier)
except KeyError:
self.set_plugin_option(identifier, default_value)
def plugin_option(self, identifier: str) -> bool:
return self.plugins[self.plugin_settings_key(identifier)]
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
self.plugins[self.plugin_settings_key(identifier)] = enabled
def __setstate__(self, state: dict[str, Any]) -> None:
# __setstate__ is called with the dict of the object being unpickled. We
# can provide save compatibility for new settings options (which
# normally would not be present in the unpickled object) by creating a
# new settings object, updating it with the unpickled state, and
# updating our dict with that.
new_state = Settings().__dict__
new_state.update(state)
self.__dict__.update(new_state)
@classmethod
def _field_description(cls, settings_field: Field[Any]) -> OptionDescription:
return settings_field.metadata[SETTING_DESCRIPTION_KEY]
@classmethod
def pages(cls) -> Iterator[str]:
seen: set[str] = set()
for settings_field in cls._user_fields():
description = cls._field_description(settings_field)
if description.page not in seen:
yield description.page
seen.add(description.page)
@classmethod
def sections(cls, page: str) -> Iterator[str]:
seen: set[str] = set()
for settings_field in cls._user_fields():
description = cls._field_description(settings_field)
if description.page == page and description.section not in seen:
yield description.section
seen.add(description.section)
@classmethod
def fields(cls, page: str, section: str) -> Iterator[tuple[str, OptionDescription]]:
for settings_field in cls._user_fields():
description = cls._field_description(settings_field)
if description.page == page and description.section == section:
yield settings_field.name, description
@classmethod
def _user_fields(cls) -> Iterator[Field[Any]]:
for settings_field in fields(cls):
if SETTING_DESCRIPTION_KEY in settings_field.metadata:
yield settings_field

View File

@ -0,0 +1,20 @@
from typing import Any, Optional
from .choicesoption import choices_option
def skill_option(
text: str,
page: str,
section: str,
detail: Optional[str] = None,
**kwargs: Any,
) -> str:
return choices_option(
text,
page,
section,
["Average", "Good", "High", "Excellent"],
detail=detail,
**kwargs,
)

View File

@ -100,6 +100,7 @@ def load_icons():
ICONS["Missile"] = QPixmap(
"./resources/ui/misc/" + get_theme_icons() + "/missile.png"
)
ICONS["Difficulty"] = ICONS["Missile"]
ICONS["Cheat"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/cheat.png")
ICONS["Plugins"] = QPixmap(
"./resources/ui/misc/" + get_theme_icons() + "/plugins.png"

View File

@ -3,21 +3,27 @@ from typing import Optional
from PySide2.QtWidgets import QSpinBox
class TenthsSpinner(QSpinBox):
class FloatSpinner(QSpinBox):
def __init__(
self,
minimum: Optional[int] = None,
maximum: Optional[int] = None,
initial: Optional[int] = None,
divisor: int,
minimum: Optional[float] = None,
maximum: Optional[float] = None,
initial: Optional[float] = None,
) -> None:
super().__init__()
self.divisor = divisor
if minimum is not None:
self.setMinimum(minimum)
self.setMinimum(int(minimum * divisor))
if maximum is not None:
self.setMaximum(maximum)
self.setMaximum(int(maximum * divisor))
if initial is not None:
self.setValue(initial)
self.setValue(int(initial * divisor))
def textFromValue(self, val: int) -> str:
return f"X {val / 10:.1f}"
return f"X {val / self.divisor:.1f}"
@property
def real_value(self) -> float:
return self.value() / self.divisor

View File

@ -1,30 +1,33 @@
from datetime import timedelta
from typing import Optional
from PySide2 import QtWidgets
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QGridLayout, QLabel, QSlider
from typing import Optional
from qt_ui.widgets.floatspinners import TenthsSpinner
from datetime import timedelta
from PySide2.QtWidgets import QSlider, QHBoxLayout
from qt_ui.widgets.floatspinners import FloatSpinner
class TenthsSpinSlider(QGridLayout):
def __init__(self, label: str, minimum: int, maximum: int, initial: int) -> None:
class FloatSpinSlider(QHBoxLayout):
def __init__(
self, minimum: float, maximum: float, initial: float, divisor: int
) -> None:
super().__init__()
self.addWidget(QLabel(label), 0, 0)
slider = QSlider(Qt.Horizontal)
slider.setMinimum(minimum)
slider.setMaximum(maximum)
slider.setMinimum(int(minimum * divisor))
slider.setMaximum(int(maximum * divisor))
slider.setValue(initial)
self.spinner = TenthsSpinner(minimum, maximum, initial)
self.spinner = FloatSpinner(divisor, minimum, maximum, initial)
slider.valueChanged.connect(lambda x: self.spinner.setValue(x))
self.spinner.valueChanged.connect(lambda x: slider.setValue(x))
self.addWidget(slider, 1, 0)
self.addWidget(self.spinner, 1, 1)
self.addWidget(slider)
self.addWidget(self.spinner)
@property
def value(self) -> float:
return self.spinner.value() / 10
return self.spinner.real_value
class TimeInputs(QtWidgets.QGridLayout):

View File

@ -15,7 +15,7 @@ from game.settings import Settings
from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSettings
from game.factions.faction import Faction
from qt_ui.widgets.QLiberationCalendar import QLiberationCalendar
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs, CurrencySpinner
from qt_ui.widgets.spinsliders import FloatSpinSlider, TimeInputs, CurrencySpinner
from qt_ui.windows.AirWingConfigurationDialog import AirWingConfigurationDialog
from qt_ui.windows.newgame.QCampaignList import QCampaignList
@ -472,11 +472,12 @@ class DifficultyAndAutomationOptions(QtWidgets.QWizardPage):
economy_layout = QtWidgets.QVBoxLayout()
economy_group.setLayout(economy_layout)
player_income = TenthsSpinSlider("Player income multiplier", 0, 50, 10)
# TODO: Put labels back.
player_income = FloatSpinSlider(0, 5, 1, divisor=10)
self.registerField("player_income_multiplier", player_income.spinner)
economy_layout.addLayout(player_income)
enemy_income = TenthsSpinSlider("Enemy income multiplier", 0, 50, 10)
enemy_income = FloatSpinSlider(0, 5, 1, divisor=10)
self.registerField("enemy_income_multiplier", enemy_income.spinner)
economy_layout.addLayout(enemy_income)

View File

@ -1,4 +1,5 @@
import logging
import textwrap
from typing import Callable
from PySide2.QtCore import QItemSelectionModel, QPoint, QSize, Qt
@ -18,13 +19,19 @@ from PySide2.QtWidgets import (
QVBoxLayout,
QWidget,
)
from dcs.forcedoptions import ForcedOptions
import qt_ui.uiconstants as CONST
from game.game import Game
from game.settings import Settings, AutoAtoBehavior
from game.settings import (
Settings,
AutoAtoBehavior,
OptionDescription,
BooleanOption,
ChoicesOption,
BoundedFloatOption,
)
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs
from qt_ui.widgets.spinsliders import FloatSpinSlider, TimeInputs
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine
from qt_ui.windows.settings.plugins import PluginOptionsPage, PluginsPage
@ -321,6 +328,86 @@ class StartTypeComboBox(QComboBox):
self.settings.default_start_type = value
class AutoSettingsLayout(QGridLayout):
def __init__(self, page: str, section: str, settings: Settings) -> None:
super().__init__()
self.settings = settings
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)
elif isinstance(description, ChoicesOption):
self.add_combobox_for(row, name, description)
elif isinstance(description, BoundedFloatOption):
self.add_float_spin_slider_for(row, name, description)
else:
raise TypeError(f"Unhandled option type: {description}")
def add_label(self, row: int, description: OptionDescription) -> None:
text = description.text
if description.detail is not None:
wrapped = "<br />".join(textwrap.wrap(description.detail, width=55))
text += f"<br /><strong>{wrapped}</strong>"
label = QLabel(text)
self.addWidget(label, row, 0)
def add_checkbox_for(self, row: int, name: str) -> None:
def on_toggle(value: bool) -> None:
self.settings.__dict__[name] = value
checkbox = QCheckBox()
checkbox.setChecked(self.settings.__dict__[name])
checkbox.toggled.connect(on_toggle)
self.addWidget(checkbox, row, 1, Qt.AlignRight)
def add_combobox_for(self, row: int, name: str, description: ChoicesOption) -> None:
combobox = QComboBox()
def on_changed(index: int) -> None:
self.settings.__dict__[name] = combobox.itemData(index)
for text, value in description.choices.items():
combobox.addItem(text, value)
combobox.setCurrentText(
description.text_for_value(self.settings.__dict__[name])
)
combobox.currentIndexChanged.connect(on_changed)
self.addWidget(combobox, row, 1, Qt.AlignRight)
def add_float_spin_slider_for(
self, row: int, name: str, description: BoundedFloatOption
) -> None:
spinner = FloatSpinSlider(
description.min,
description.max,
self.settings.__dict__[name],
divisor=description.divisor,
)
self.addLayout(spinner, row, 1, Qt.AlignRight)
class AutoSettingsGroup(QGroupBox):
def __init__(self, page: str, section: str, settings: Settings) -> None:
super().__init__(section)
self.setLayout(AutoSettingsLayout(page, section, settings))
class AutoSettingsPageLayout(QVBoxLayout):
def __init__(self, page: str, settings: Settings) -> None:
super().__init__()
self.setAlignment(Qt.AlignTop)
for section in Settings.sections(page):
self.addWidget(AutoSettingsGroup(page, section, settings))
class AutoSettingsPage(QWidget):
def __init__(self, page: str, settings: Settings) -> None:
super().__init__()
self.setLayout(AutoSettingsPageLayout(page, settings))
class QSettingsWindow(QDialog):
def __init__(self, game: Game):
super().__init__()
@ -330,6 +417,10 @@ class QSettingsWindow(QDialog):
self.pluginsOptionsPage = None
self.campaign_management_page = QWidget()
self.pages: dict[str, AutoSettingsPage] = {}
for page in Settings.pages():
self.pages[page] = AutoSettingsPage(page, game.settings)
self.setModal(True)
self.setWindowTitle("Settings")
self.setWindowIcon(CONST.ICONS["Settings"])
@ -349,13 +440,16 @@ class QSettingsWindow(QDialog):
self.categoryList.setIconSize(QSize(32, 32))
self.initDifficultyLayout()
difficulty = QStandardItem("Difficulty")
difficulty.setIcon(CONST.ICONS["Missile"])
difficulty.setEditable(False)
difficulty.setSelectable(True)
self.categoryModel.appendRow(difficulty)
self.right_layout.addWidget(self.difficultyPage)
for name, page in self.pages.items():
page_item = QStandardItem(name)
if name in CONST.ICONS:
page_item.setIcon(CONST.ICONS[name])
else:
page_item.setIcon(CONST.ICONS["Generator"])
page_item.setEditable(False)
page_item.setSelectable(True)
self.categoryModel.appendRow(page_item)
self.right_layout.addWidget(page)
self.init_campaign_management_layout()
campaign_management = QStandardItem("Campaign Management")
@ -414,182 +508,6 @@ class QSettingsWindow(QDialog):
def init(self):
pass
def initDifficultyLayout(self):
self.difficultyPage = QWidget()
self.difficultyLayout = QVBoxLayout()
self.difficultyLayout.setAlignment(Qt.AlignTop)
self.difficultyPage.setLayout(self.difficultyLayout)
# DCS AI difficulty settings
self.aiDifficultySettings = QGroupBox("AI Difficulty")
self.aiDifficultyLayout = QGridLayout()
self.playerCoalitionSkill = QComboBox()
self.enemyCoalitionSkill = QComboBox()
self.enemyAASkill = QComboBox()
for skill in CONST.SKILL_OPTIONS:
self.playerCoalitionSkill.addItem(skill)
self.enemyCoalitionSkill.addItem(skill)
self.enemyAASkill.addItem(skill)
self.playerCoalitionSkill.setCurrentIndex(
CONST.SKILL_OPTIONS.index(self.game.settings.player_skill)
)
self.enemyCoalitionSkill.setCurrentIndex(
CONST.SKILL_OPTIONS.index(self.game.settings.enemy_skill)
)
self.enemyAASkill.setCurrentIndex(
CONST.SKILL_OPTIONS.index(self.game.settings.enemy_vehicle_skill)
)
self.player_income = TenthsSpinSlider(
"Player income multiplier",
0,
50,
int(self.game.settings.player_income_multiplier * 10),
)
self.player_income.spinner.valueChanged.connect(self.applySettings)
self.enemy_income = TenthsSpinSlider(
"Enemy income multiplier",
0,
50,
int(self.game.settings.enemy_income_multiplier * 10),
)
self.enemy_income.spinner.valueChanged.connect(self.applySettings)
self.playerCoalitionSkill.currentIndexChanged.connect(self.applySettings)
self.enemyCoalitionSkill.currentIndexChanged.connect(self.applySettings)
self.enemyAASkill.currentIndexChanged.connect(self.applySettings)
# Mission generation settings related to difficulty
self.missionSettings = QGroupBox("Mission Difficulty")
self.missionLayout = QGridLayout()
self.manpads = QCheckBox()
self.manpads.setChecked(self.game.settings.manpads)
self.manpads.toggled.connect(self.applySettings)
self.noNightMission = QCheckBox()
self.noNightMission.setChecked(self.game.settings.night_disabled)
self.noNightMission.toggled.connect(self.applySettings)
# DCS Mission options
self.missionRestrictionsSettings = QGroupBox("Mission Restrictions")
self.missionRestrictionsLayout = QGridLayout()
self.difficultyLabel = QComboBox()
[self.difficultyLabel.addItem(t) for t in CONST.LABELS_OPTIONS]
self.difficultyLabel.setCurrentIndex(
CONST.LABELS_OPTIONS.index(self.game.settings.labels)
)
self.difficultyLabel.currentIndexChanged.connect(self.applySettings)
self.mapVisibiitySelection = QComboBox()
self.mapVisibiitySelection.addItem("All", ForcedOptions.Views.All)
if self.game.settings.map_coalition_visibility == ForcedOptions.Views.All:
self.mapVisibiitySelection.setCurrentIndex(0)
self.mapVisibiitySelection.addItem("Fog of War", ForcedOptions.Views.Allies)
if self.game.settings.map_coalition_visibility == ForcedOptions.Views.Allies:
self.mapVisibiitySelection.setCurrentIndex(1)
self.mapVisibiitySelection.addItem(
"Allies Only", ForcedOptions.Views.OnlyAllies
)
if (
self.game.settings.map_coalition_visibility
== ForcedOptions.Views.OnlyAllies
):
self.mapVisibiitySelection.setCurrentIndex(2)
self.mapVisibiitySelection.addItem(
"Own Aircraft Only", ForcedOptions.Views.MyAircraft
)
if (
self.game.settings.map_coalition_visibility
== ForcedOptions.Views.MyAircraft
):
self.mapVisibiitySelection.setCurrentIndex(3)
self.mapVisibiitySelection.addItem("Map Only", ForcedOptions.Views.OnlyMap)
if self.game.settings.map_coalition_visibility == ForcedOptions.Views.OnlyMap:
self.mapVisibiitySelection.setCurrentIndex(4)
self.mapVisibiitySelection.currentIndexChanged.connect(self.applySettings)
self.ext_views = QCheckBox()
self.ext_views.setChecked(self.game.settings.external_views_allowed)
self.ext_views.toggled.connect(self.applySettings)
self.battleDamageAssessment = QComboBox()
self.battleDamageAssessment.addItem("Player preference", None)
self.battleDamageAssessment.addItem("Enforced on", True)
self.battleDamageAssessment.addItem("Enforced off", False)
if self.game.settings.battle_damage_assessment is None:
self.battleDamageAssessment.setCurrentIndex(0)
elif self.game.settings.battle_damage_assessment is True:
self.battleDamageAssessment.setCurrentIndex(1)
else:
self.battleDamageAssessment.setCurrentIndex(2)
self.battleDamageAssessment.currentIndexChanged.connect(self.applySettings)
def set_invulnerable_player_pilots(checked: bool) -> None:
self.game.settings.invulnerable_player_pilots = checked
invulnerable_player_pilots_label = QLabel(
"Player pilots cannot be killed<br />"
"<strong>Aircraft are vulnerable, but the player's pilot will be<br />"
"returned to the squadron at the end of the mission</strong>"
)
invulnerable_player_pilots_checkbox = QCheckBox()
invulnerable_player_pilots_checkbox.setChecked(
self.game.settings.invulnerable_player_pilots
)
invulnerable_player_pilots_checkbox.toggled.connect(
set_invulnerable_player_pilots
)
self.aiDifficultyLayout.addWidget(QLabel("Player coalition skill"), 0, 0)
self.aiDifficultyLayout.addWidget(
self.playerCoalitionSkill, 0, 1, Qt.AlignRight
)
self.aiDifficultyLayout.addWidget(QLabel("Enemy coalition skill"), 1, 0)
self.aiDifficultyLayout.addWidget(self.enemyCoalitionSkill, 1, 1, Qt.AlignRight)
self.aiDifficultyLayout.addWidget(QLabel("Enemy AA and vehicles skill"), 2, 0)
self.aiDifficultyLayout.addWidget(self.enemyAASkill, 2, 1, Qt.AlignRight)
self.aiDifficultyLayout.addLayout(self.player_income, 3, 0)
self.aiDifficultyLayout.addLayout(self.enemy_income, 4, 0)
self.aiDifficultyLayout.addWidget(invulnerable_player_pilots_label, 5, 0)
self.aiDifficultyLayout.addWidget(
invulnerable_player_pilots_checkbox, 5, 1, Qt.AlignRight
)
self.aiDifficultySettings.setLayout(self.aiDifficultyLayout)
self.difficultyLayout.addWidget(self.aiDifficultySettings)
self.missionLayout.addWidget(QLabel("Manpads on frontlines"), 0, 0)
self.missionLayout.addWidget(self.manpads, 0, 1, Qt.AlignRight)
self.missionLayout.addWidget(QLabel("No night missions"), 1, 0)
self.missionLayout.addWidget(self.noNightMission, 1, 1, Qt.AlignRight)
self.missionSettings.setLayout(self.missionLayout)
self.difficultyLayout.addWidget(self.missionSettings)
self.missionRestrictionsLayout.addWidget(QLabel("In Game Labels"), 0, 0)
self.missionRestrictionsLayout.addWidget(
self.difficultyLabel, 0, 1, Qt.AlignRight
)
self.missionRestrictionsLayout.addWidget(QLabel("Map visibility options"), 1, 0)
self.missionRestrictionsLayout.addWidget(
self.mapVisibiitySelection, 1, 1, Qt.AlignRight
)
self.missionRestrictionsLayout.addWidget(QLabel("Allow external views"), 2, 0)
self.missionRestrictionsLayout.addWidget(self.ext_views, 2, 1, Qt.AlignRight)
self.missionRestrictionsLayout.addWidget(
QLabel("Battle damage assessment"), 3, 0
)
self.missionRestrictionsLayout.addWidget(
self.battleDamageAssessment, 3, 1, Qt.AlignRight
)
self.missionRestrictionsSettings.setLayout(self.missionRestrictionsLayout)
self.difficultyLayout.addWidget(self.missionRestrictionsSettings)
def init_campaign_management_layout(self) -> None:
campaign_layout = QVBoxLayout()
campaign_layout.setAlignment(Qt.AlignTop)