From 56abd0bb7fea57189fa8083a027f89942ef0134c Mon Sep 17 00:00:00 2001 From: Schneefl0cke <60181177+Schneefl0cke@users.noreply.github.com> Date: Tue, 11 May 2021 12:13:15 +0200 Subject: [PATCH] Add option for setting desired mission length. --- changelog.md | 1 + game/settings.py | 4 ++ gen/flights/ai_flight_planner.py | 32 ++++------- qt_ui/widgets/spinsliders.py | 69 ++++++++++++++++++++++- qt_ui/windows/newgame/QNewGameWizard.py | 35 +++++------- qt_ui/windows/settings/QSettingsWindow.py | 17 +++++- 6 files changed, 114 insertions(+), 44 deletions(-) diff --git a/changelog.md b/changelog.md index 7859c58e..af6ca83c 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ Saves from 2.5 are not compatible with 3.0. * **[Campaign]** Ground units can now be transferred by road, airlift, and cargo ship. See https://github.com/dcs-liberation/dcs_liberation/wiki/Unit-Transfers for more information. * **[Campaign]** Ground units can no longer be sold. To move units to a new location, transfer them. * **[Campaign]** Ground units must now be recruited at a base with a factory and transferred to their destination. When buying units in the UI, the purchase will automatically be fulfilled at the closest factory, and a transfer will be created on the next turn. +* **[Campaign AI]** Every 30 minutes the AI will plan a CAP, so players can customize their mission better. * **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present. * **[Modding]** Campaigns now choose locations for factories to spawn. * **[Modding]** Can now install custom factions to /Liberation/Factions instead of the Liberation install directory. diff --git a/game/settings.py b/game/settings.py index 7d7bc0d1..192d080f 100644 --- a/game/settings.py +++ b/game/settings.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +from datetime import timedelta from typing import Dict, Optional from dcs.forcedoptions import ForcedOptions @@ -25,6 +26,9 @@ class Settings: default_start_type: str = "Cold" + # Mission specific + desired_player_mission_duration: timedelta = timedelta(minutes=90) + # Campaign management automate_runway_repair: bool = False automate_front_line_reinforcements: bool = False diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index f2177782..868ae8ac 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -583,26 +583,18 @@ class CoalitionMissionPlanner: # Find friendly CPs within 100 nmi from an enemy airfield, plan CAP. for cp in self.objective_finder.vulnerable_control_points(): - # Plan three rounds of CAP to give ~90 minutes coverage. Spacing - # these out appropriately is done in stagger_missions. - yield ProposedMission( - cp, - [ - ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), - ], - ) - yield ProposedMission( - cp, - [ - ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), - ], - ) - yield ProposedMission( - cp, - [ - ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), - ], - ) + # Plan CAP in such a way, that it is established during the whole desired mission length + for _ in range( + 0, + int(self.game.settings.desired_player_mission_duration.total_seconds()), + int(self.faction.doctrine.cap_duration.total_seconds()), + ): + yield ProposedMission( + cp, + [ + ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), + ], + ) # Find front lines, plan CAS. for front_line in self.objective_finder.front_lines(): diff --git a/qt_ui/widgets/spinsliders.py b/qt_ui/widgets/spinsliders.py index 90623a6d..93d3017e 100644 --- a/qt_ui/widgets/spinsliders.py +++ b/qt_ui/widgets/spinsliders.py @@ -1,7 +1,9 @@ +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 class TenthsSpinSlider(QGridLayout): @@ -23,3 +25,68 @@ class TenthsSpinSlider(QGridLayout): @property def value(self) -> float: return self.spinner.value() / 10 + + +class TimeInputs(QtWidgets.QGridLayout): + def __init__(self, label: str, initial: timedelta) -> None: + super().__init__() + self.addWidget(QtWidgets.QLabel(label), 0, 0) + + minimum_minutes = 30 + maximum_minutes = 150 + initial_minutes = int(initial.total_seconds() / 60) + + slider = QtWidgets.QSlider(Qt.Horizontal) + slider.setMinimum(minimum_minutes) + slider.setMaximum(maximum_minutes) + slider.setValue(initial_minutes) + self.spinner = TimeSpinner(minimum_minutes, maximum_minutes, initial_minutes) + 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) + + @property + def value(self) -> timedelta: + return timedelta(minutes=self.spinner.value()) + + +class TimeSpinner(QtWidgets.QSpinBox): + def __init__( + self, + minimum: Optional[int] = None, + maximum: Optional[int] = None, + initial: Optional[int] = None, + ) -> None: + super().__init__() + + if minimum is not None: + self.setMinimum(minimum) + if maximum is not None: + self.setMaximum(maximum) + if initial is not None: + self.setValue(initial) + + def textFromValue(self, val: int) -> str: + return f"{val} minutes" + + +class CurrencySpinner(QtWidgets.QSpinBox): + def __init__( + self, + minimum: Optional[int] = None, + maximum: Optional[int] = None, + initial: Optional[int] = None, + ) -> None: + super().__init__() + + if minimum is not None: + self.setMinimum(minimum) + if maximum is not None: + self.setMaximum(maximum) + if initial is not None: + self.setValue(initial) + + def textFromValue(self, val: int) -> str: + return f"${val}" diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index 87e34ab9..ecd64056 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -7,12 +7,13 @@ from PySide2 import QtGui, QtWidgets from PySide2.QtCore import QItemSelectionModel, QPoint, Qt, QDate from PySide2.QtWidgets import QVBoxLayout, QTextEdit, QLabel from jinja2 import Environment, FileSystemLoader, select_autoescape +from datetime import timedelta from game import db from game.settings import Settings from game.theater.start_generator import GameGenerator, GeneratorSettings from qt_ui.widgets.QLiberationCalendar import QLiberationCalendar -from qt_ui.widgets.spinsliders import TenthsSpinSlider +from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs, CurrencySpinner from qt_ui.windows.newgame.QCampaignList import ( Campaign, QCampaignList, @@ -33,8 +34,8 @@ jinja_env = Environment( lstrip_blocks=True, ) - DEFAULT_BUDGET = 2000 +DEFAULT_MISSION_LENGTH: timedelta = timedelta(minutes=90) class NewGameWizard(QtWidgets.QWizard): @@ -85,6 +86,9 @@ class NewGameWizard(QtWidgets.QWizard): automate_front_line_reinforcements=self.field( "automate_front_line_purchases" ), + desired_player_mission_duration=timedelta( + minutes=self.field("desired_player_mission_duration") + ), automate_aircraft_reinforcements=self.field("automate_aircraft_purchases"), supercarrier=self.field("supercarrier"), enable_new_ground_unit_recruitment=self.field( @@ -428,26 +432,6 @@ class TheaterConfiguration(QtWidgets.QWizardPage): self.setLayout(layout) -class CurrencySpinner(QtWidgets.QSpinBox): - def __init__( - self, - minimum: Optional[int] = None, - maximum: Optional[int] = None, - initial: Optional[int] = None, - ) -> None: - super().__init__() - - if minimum is not None: - self.setMinimum(minimum) - if maximum is not None: - self.setMaximum(maximum) - if initial is not None: - self.setValue(initial) - - def textFromValue(self, val: int) -> str: - return f"${val}" - - class BudgetInputs(QtWidgets.QGridLayout): def __init__(self, label: str) -> None: super().__init__() @@ -565,6 +549,12 @@ class GeneratorOptions(QtWidgets.QWizardPage): self.registerField("no_player_navy", no_player_navy) no_enemy_navy = QtWidgets.QCheckBox() self.registerField("no_enemy_navy", no_enemy_navy) + desired_player_mission_duration = TimeInputs( + "Desired mission duration", DEFAULT_MISSION_LENGTH + ) + self.registerField( + "desired_player_mission_duration", desired_player_mission_duration.spinner + ) generatorLayout = QtWidgets.QGridLayout() generatorLayout.addWidget(QtWidgets.QLabel("No Aircraft Carriers"), 1, 0) @@ -577,6 +567,7 @@ class GeneratorOptions(QtWidgets.QWizardPage): generatorLayout.addWidget(no_player_navy, 4, 1) generatorLayout.addWidget(QtWidgets.QLabel("No Enemy Navy"), 5, 0) generatorLayout.addWidget(no_enemy_navy, 5, 1) + generatorLayout.addLayout(desired_player_mission_duration, 6, 0) generatorSettingsGroup.setLayout(generatorLayout) mlayout = QVBoxLayout() diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py index 8af7df3d..c7e13e7d 100644 --- a/qt_ui/windows/settings/QSettingsWindow.py +++ b/qt_ui/windows/settings/QSettingsWindow.py @@ -26,7 +26,7 @@ from game.game import Game from game.infos.information import Information from game.settings import Settings from qt_ui.widgets.QLabeledWidget import QLabeledWidget -from qt_ui.widgets.spinsliders import TenthsSpinSlider +from qt_ui.widgets.spinsliders import TenthsSpinSlider, 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 @@ -471,6 +471,14 @@ class QSettingsWindow(QDialog): "spawned immediately. AI wingmen may begin startup immediately." ) + self.desired_player_mission_duration = TimeInputs( + "Desired mission duration", + self.game.settings.desired_player_mission_duration, + ) + self.desired_player_mission_duration.spinner.valueChanged.connect( + self.applySettings + ) + self.gameplayLayout.addWidget(QLabel("Use Supercarrier Module"), 0, 0) self.gameplayLayout.addWidget(self.supercarrier, 0, 1, Qt.AlignRight) self.gameplayLayout.addWidget(QLabel("Put Objective Markers on Map"), 1, 0) @@ -483,6 +491,9 @@ class QSettingsWindow(QDialog): ) self.gameplayLayout.addWidget(dark_kneeboard_label, 2, 0) self.gameplayLayout.addWidget(self.generate_dark_kneeboard, 2, 1, Qt.AlignRight) + self.gameplayLayout.addLayout( + self.desired_player_mission_duration, 5, 0, Qt.AlignRight + ) spawn_players_immediately_tooltip = ( "Always spawns player aircraft immediately, even if their start time is " @@ -695,6 +706,10 @@ class QSettingsWindow(QDialog): self.generate_dark_kneeboard.isChecked() ) + self.game.settings.desired_player_mission_duration = ( + self.desired_player_mission_duration.value + ) + self.game.settings.perf_red_alert_state = self.red_alert.isChecked() self.game.settings.perf_smoke_gen = self.smoke.isChecked() self.game.settings.perf_smoke_spacing = self.smoke_spacing.value()