Let the TheaterCommander manage front line stance.

This improves the AI behavior by choosing the stances non-randomly:

* Breakthrough will be used if the base is expected to be capturable and
  the coalition outnumbers the enemy by 20%.
* Elimination will be used if the coalition has at least as many units
  as the enemy.
* Defensive will be used if the coalition has at least half as many
  units as the enemy.
* Retreat will be used if the coalition is significantly outnumbers.

This also exposes the option to the player.
This commit is contained in:
Dan Albert 2021-07-12 20:31:38 -07:00
parent 575aca5886
commit 0a416ab758
35 changed files with 361 additions and 57 deletions

View File

@ -5,6 +5,7 @@ Saves from 3.x are not compatible with 5.0.
## Features/Improvements
* **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions.
* **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI.
## Fixes

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from collections import Iterator
from dataclasses import dataclass
from game.theater import ConflictTheater
from game.theater import ConflictTheater, ControlPoint
from game.theater.theatergroundobject import VehicleGroupGroundObject
from game.utils import meters, nautical_miles
@ -53,9 +53,7 @@ class Garrisons:
continue
for garrison in garrisons:
# Not sure what distance DCS uses, but assuming it's about 2NM since
# that's roughly the distance of the circle on the map.
if meters(garrison.distance_to(cp)) < nautical_miles(2):
if meters(garrison.distance_to(cp)) < ControlPoint.CAPTURE_DISTANCE:
blocking.append(garrison)
else:
defending.append(garrison)

View File

@ -0,0 +1,19 @@
from collections import Iterator
from dataclasses import dataclass
from game.commander.tasks.compound.destroyenemygroundunits import (
DestroyEnemyGroundUnits,
)
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
from game.theater import FrontLine
@dataclass(frozen=True)
class CaptureBase(CompoundTask[TheaterState]):
front_line: FrontLine
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [BreakthroughAttack(self.front_line, state.player)]
yield [DestroyEnemyGroundUnits(self.front_line)]

View File

@ -0,0 +1,13 @@
from collections import Iterator
from dataclasses import dataclass
from game.commander.tasks.compound.capturebase import CaptureBase
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
@dataclass(frozen=True)
class CaptureBases(CompoundTask[TheaterState]):
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
for front in state.active_front_lines:
yield [CaptureBase(front)]

View File

@ -0,0 +1,19 @@
from collections import Iterator
from dataclasses import dataclass
from game.commander.tasks.primitive.cas import PlanCas
from game.commander.tasks.primitive.defensivestance import DefensiveStance
from game.commander.tasks.primitive.retreatstance import RetreatStance
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
from game.theater import FrontLine
@dataclass(frozen=True)
class DefendBase(CompoundTask[TheaterState]):
front_line: FrontLine
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [DefensiveStance(self.front_line, state.player)]
yield [RetreatStance(self.front_line, state.player)]
yield [PlanCas(self.front_line)]

View File

@ -0,0 +1,13 @@
from collections import Iterator
from dataclasses import dataclass
from game.commander.tasks.compound.defendbase import DefendBase
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
@dataclass(frozen=True)
class DefendBases(CompoundTask[TheaterState]):
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
for front in state.active_front_lines:
yield [DefendBase(front)]

View File

@ -0,0 +1,17 @@
from collections import Iterator
from dataclasses import dataclass
from game.commander.tasks.primitive.cas import PlanCas
from game.commander.tasks.primitive.eliminationattack import EliminationAttack
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
from game.theater import FrontLine
@dataclass(frozen=True)
class DestroyEnemyGroundUnits(CompoundTask[TheaterState]):
front_line: FrontLine
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [EliminationAttack(self.front_line, state.player)]
yield [PlanCas(self.front_line)]

View File

@ -1,19 +1,19 @@
from collections import Iterator
from dataclasses import dataclass
from game.commander.tasks.compound.aewcsupport import PlanAewcSupport
from game.commander.tasks.compound.attackairinfrastructure import (
AttackAirInfrastructure,
)
from game.commander.tasks.compound.attackbuildings import AttackBuildings
from game.commander.tasks.compound.attackgarrisons import AttackGarrisons
from game.commander.tasks.compound.capturebases import CaptureBases
from game.commander.tasks.compound.defendbases import DefendBases
from game.commander.tasks.compound.degradeiads import DegradeIads
from game.commander.tasks.compound.frontlinedefense import FrontLineDefense
from game.commander.tasks.compound.interdictreinforcements import (
InterdictReinforcements,
)
from game.commander.tasks.compound.protectairspace import ProtectAirSpace
from game.commander.tasks.compound.refuelingsupport import PlanRefuelingSupport
from game.commander.tasks.compound.theatersupport import TheaterSupport
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
@ -23,10 +23,10 @@ class PlanNextAction(CompoundTask[TheaterState]):
aircraft_cold_start: bool
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [PlanAewcSupport()]
yield [PlanRefuelingSupport()]
yield [TheaterSupport()]
yield [ProtectAirSpace()]
yield [FrontLineDefense()]
yield [CaptureBases()]
yield [DefendBases()]
yield [InterdictReinforcements()]
yield [AttackGarrisons()]
yield [AttackAirInfrastructure(self.aircraft_cold_start)]

View File

@ -0,0 +1,14 @@
from collections import Iterator
from dataclasses import dataclass
from game.commander.tasks.compound.aewcsupport import PlanAewcSupport
from game.commander.tasks.compound.refuelingsupport import PlanRefuelingSupport
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
@dataclass(frozen=True)
class TheaterSupport(CompoundTask[TheaterState]):
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [PlanAewcSupport()]
yield [PlanRefuelingSupport()]

View File

@ -0,0 +1,75 @@
from __future__ import annotations
import math
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
from game.commander.theaterstate import TheaterState
from game.profiling import MultiEventTracer
from game.theater import FrontLine
from gen.ground_forces.combat_stance import CombatStance
if TYPE_CHECKING:
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
class FrontLineStanceTask(TheaterCommanderTask, ABC):
def __init__(self, front_line: FrontLine, player: bool) -> None:
self.front_line = front_line
self.friendly_cp = self.front_line.control_point_friendly_to(player)
self.enemy_cp = self.front_line.control_point_hostile_to(player)
@property
@abstractmethod
def stance(self) -> CombatStance:
...
@staticmethod
def management_allowed(state: TheaterState) -> bool:
return not state.player or state.stance_automation_enabled
def better_stance_already_set(self, state: TheaterState) -> bool:
current_stance = state.front_line_stances[self.front_line]
if current_stance is None:
return False
preference = (
CombatStance.RETREAT,
CombatStance.DEFENSIVE,
CombatStance.AMBUSH,
CombatStance.AGGRESSIVE,
CombatStance.ELIMINATION,
CombatStance.BREAKTHROUGH,
)
current_rating = preference.index(current_stance)
new_rating = preference.index(self.stance)
return current_rating >= new_rating
@property
@abstractmethod
def have_sufficient_front_line_advantage(self) -> bool:
...
@property
def ground_force_balance(self) -> float:
# TODO: Planned CAS missions should reduce the expected opposing force size.
friendly_forces = self.friendly_cp.deployable_front_line_units
enemy_forces = self.enemy_cp.deployable_front_line_units
if enemy_forces == 0:
return math.inf
return friendly_forces / enemy_forces
def preconditions_met(self, state: TheaterState) -> bool:
if not self.management_allowed(state):
return False
if self.better_stance_already_set(state):
return False
return self.have_sufficient_front_line_advantage
def apply_effects(self, state: TheaterState) -> None:
state.front_line_stances[self.front_line] = self.stance
def execute(
self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer
) -> None:
self.friendly_cp.stances[self.enemy_cp.id] = self.stance

View File

@ -40,6 +40,9 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
def __post_init__(self) -> None:
self.flights = []
def preconditions_met(self, state: TheaterState) -> bool:
return not state.player or state.ato_automation_enabled
def execute(
self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer
) -> None:

View File

@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
@dataclass
class PlanAewc(PackagePlanningTask[MissionTarget]):
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
return self.target in state.aewc_targets
def apply_effects(self, state: TheaterState) -> None:

View File

@ -13,6 +13,8 @@ from gen.flights.flight import FlightType
@dataclass
class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if self.target not in state.threatening_air_defenses:
return False
return self.target_area_preconditions_met(state, ignore_iads=True)

View File

@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
@dataclass
class PlanAntiShipping(PackagePlanningTask[CargoShip]):
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if self.target not in state.enemy_shipping:
return False
return self.target_area_preconditions_met(state)

View File

@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
@dataclass
class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]):
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if self.target not in state.enemy_garrisons:
return False
return self.target_area_preconditions_met(state)

View File

@ -19,6 +19,8 @@ class PlanBarcap(TheaterCommanderTask):
target: ControlPoint
def preconditions_met(self, state: TheaterState) -> bool:
if state.player and not state.ato_automation_enabled:
return False
return self.target in state.vulnerable_control_points
def apply_effects(self, state: TheaterState) -> None:

View File

@ -0,0 +1,37 @@
from __future__ import annotations
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
from game.commander.theaterstate import TheaterState
from game.theater import ControlPoint
from game.theater.theatergroundobject import VehicleGroupGroundObject
from game.utils import meters
from gen.ground_forces.combat_stance import CombatStance
class BreakthroughAttack(FrontLineStanceTask):
@property
def stance(self) -> CombatStance:
return CombatStance.BREAKTHROUGH
@property
def have_sufficient_front_line_advantage(self) -> bool:
return self.ground_force_balance >= 1.2
@property
def opposing_garrisons_eliminated(self) -> bool:
# TODO: Should operate on TheaterState to account for BAIs planned this turn.
for tgo in self.enemy_cp.ground_objects:
if not isinstance(tgo, VehicleGroupGroundObject):
continue
if meters(tgo.distance_to(self.enemy_cp)) < ControlPoint.CAPTURE_DISTANCE:
return False
return True
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
return self.opposing_garrisons_eliminated
def apply_effects(self, state: TheaterState) -> None:
super().apply_effects(state)
state.active_front_lines.remove(self.front_line)

View File

@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
@dataclass
class PlanCas(PackagePlanningTask[FrontLine]):
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
return self.target in state.vulnerable_front_lines
def apply_effects(self, state: TheaterState) -> None:

View File

@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
@dataclass
class PlanConvoyInterdiction(PackagePlanningTask[Convoy]):
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if self.target not in state.enemy_convoys:
return False
return self.target_area_preconditions_met(state)

View File

@ -13,6 +13,8 @@ from gen.flights.flight import FlightType
@dataclass
class PlanDead(PackagePlanningTask[IadsGroundObject]):
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if (
self.target not in state.threatening_air_defenses
and self.target not in state.detecting_air_defenses

View File

@ -0,0 +1,14 @@
from __future__ import annotations
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
from gen.ground_forces.combat_stance import CombatStance
class DefensiveStance(FrontLineStanceTask):
@property
def stance(self) -> CombatStance:
return CombatStance.DEFENSIVE
@property
def have_sufficient_front_line_advantage(self) -> bool:
return self.ground_force_balance >= 0.5

View File

@ -0,0 +1,14 @@
from __future__ import annotations
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
from gen.ground_forces.combat_stance import CombatStance
class EliminationAttack(FrontLineStanceTask):
@property
def stance(self) -> CombatStance:
return CombatStance.ELIMINATION
@property
def have_sufficient_front_line_advantage(self) -> bool:
return self.ground_force_balance >= 1.0

View File

@ -14,6 +14,8 @@ class PlanOcaStrike(PackagePlanningTask[ControlPoint]):
aircraft_cold_start: bool
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if self.target not in state.oca_targets:
return False
return self.target_area_preconditions_met(state)

View File

@ -12,6 +12,8 @@ from gen.flights.flight import FlightType
@dataclass
class PlanRefueling(PackagePlanningTask[MissionTarget]):
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
return self.target in state.refueling_targets
def apply_effects(self, state: TheaterState) -> None:

View File

@ -0,0 +1,14 @@
from __future__ import annotations
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
from gen.ground_forces.combat_stance import CombatStance
class RetreatStance(FrontLineStanceTask):
@property
def stance(self) -> CombatStance:
return CombatStance.RETREAT
@property
def have_sufficient_front_line_advantage(self) -> bool:
return True

View File

@ -13,6 +13,8 @@ from gen.flights.flight import FlightType
@dataclass
class PlanStrike(PackagePlanningTask[TheaterGroundObject[Any]]):
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if self.target not in state.strike_targets:
return False
return self.target_area_preconditions_met(state)

View File

@ -3,12 +3,13 @@ from __future__ import annotations
import dataclasses
import itertools
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Union
from typing import TYPE_CHECKING, Any, Union, Optional
from game.commander.garrisons import Garrisons
from game.commander.objectivefinder import ObjectiveFinder
from game.data.doctrine import Doctrine
from game.htn import WorldState
from game.settings import AutoAtoBehavior
from game.theater import ControlPoint, FrontLine, MissionTarget
from game.theater.theatergroundobject import (
TheaterGroundObject,
@ -17,6 +18,7 @@ from game.theater.theatergroundobject import (
)
from game.threatzones import ThreatZones
from game.transfers import Convoy, CargoShip
from gen.ground_forces.combat_stance import CombatStance
if TYPE_CHECKING:
from game import Game
@ -24,7 +26,12 @@ if TYPE_CHECKING:
@dataclass
class TheaterState(WorldState["TheaterState"]):
player: bool
stance_automation_enabled: bool
ato_automation_enabled: bool
vulnerable_control_points: list[ControlPoint]
active_front_lines: list[FrontLine]
front_line_stances: dict[FrontLine, Optional[CombatStance]]
vulnerable_front_lines: list[FrontLine]
aewc_targets: list[MissionTarget]
refueling_targets: list[MissionTarget]
@ -69,7 +76,12 @@ class TheaterState(WorldState["TheaterState"]):
# Do not use copy.deepcopy. Copying every TGO, control point, etc is absurdly
# expensive.
return TheaterState(
player=self.player,
stance_automation_enabled=self.stance_automation_enabled,
ato_automation_enabled=self.ato_automation_enabled,
vulnerable_control_points=list(self.vulnerable_control_points),
active_front_lines=list(self.active_front_lines),
front_line_stances=dict(self.front_line_stances),
vulnerable_front_lines=list(self.vulnerable_front_lines),
aewc_targets=list(self.aewc_targets),
refueling_targets=list(self.refueling_targets),
@ -96,8 +108,15 @@ class TheaterState(WorldState["TheaterState"]):
@classmethod
def from_game(cls, game: Game, player: bool) -> TheaterState:
finder = ObjectiveFinder(game, player)
auto_stance = game.settings.automate_front_line_stance
auto_ato = game.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled
return TheaterState(
player=player,
stance_automation_enabled=auto_stance,
ato_automation_enabled=auto_ato,
vulnerable_control_points=list(finder.vulnerable_control_points()),
active_front_lines=list(finder.front_lines()),
front_line_stances={f: None for f in finder.front_lines()},
vulnerable_front_lines=list(finder.front_lines()),
aewc_targets=[finder.farthest_friendly_control_point()],
refueling_targets=[finder.closest_friendly_control_point()],

View File

@ -24,6 +24,7 @@ from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner import GroundPlanner
from . import persistency
from .commander import TheaterCommander
from .debriefing import Debriefing
from .event.event import Event
from .event.frontlineattack import FrontlineAttackEvent
@ -32,7 +33,7 @@ from .income import Income
from .infos.information import Information
from .navmesh import NavMesh
from .procurement import AircraftProcurementRequest, ProcurementAi
from .profiling import logged_duration
from .profiling import logged_duration, MultiEventTracer
from .settings import Settings, AutoAtoBehavior
from .squadrons import AirWing
from .theater import ConflictTheater, ControlPoint
@ -504,13 +505,15 @@ class Game:
with logged_duration("Transport planning"):
self.transfers.plan_transports()
if not player or (
player and self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled
):
color = "Blue" if player else "Red"
with logged_duration(f"{color} mission planning"):
with MultiEventTracer() as tracer:
mission_planner = CoalitionMissionPlanner(self, player)
mission_planner.plan_missions()
with tracer.trace(f"{color} mission planning"):
with tracer.trace(f"{color} mission identification"):
commander = TheaterCommander(self, player)
commander.plan_missions(mission_planner, tracer)
with tracer.trace(f"{color} mission fulfillment"):
mission_planner.fulfill_missions()
self.plan_procurement_for(player)

View File

@ -397,6 +397,7 @@ class Operation:
player_gp,
enemy_gp,
player_cp.stances[enemy_cp.id],
enemy_cp.stances[player_cp.id],
cls.unit_map,
)
ground_conflict_gen.generate()

View File

@ -55,6 +55,7 @@ class Settings:
automate_runway_repair: bool = False
automate_front_line_reinforcements: bool = False
automate_aircraft_reinforcements: bool = False
automate_front_line_stance: bool = True
restrict_weapons_by_date: bool = False
disable_legacy_aewc: bool = True
disable_legacy_tanker: bool = True

View File

@ -271,6 +271,9 @@ class ControlPointStatus(IntEnum):
class ControlPoint(MissionTarget, ABC):
# Not sure what distance DCS uses, but assuming it's about 2NM since that's roughly
# the distance of the circle on the map.
CAPTURE_DISTANCE = nautical_miles(2)
position = None # type: Point
name = None # type: str
@ -727,6 +730,10 @@ class ControlPoint(MissionTarget, ABC):
return self.captured != other.captured
@property
def deployable_front_line_units(self) -> int:
return min(self.frontline_unit_count_limit, self.base.total_armor)
@property
def frontline_unit_count_limit(self) -> int:
return (

View File

@ -71,15 +71,26 @@ class FrontLine(MissionTarget):
self.point_from_a(self._position_distance),
)
def __eq__(self, other: Any) -> bool:
if not isinstance(other, FrontLine):
return False
return (self.blue_cp, self.red_cp) == (other.blue_cp, other.red_cp)
def __hash__(self) -> int:
return hash((self.blue_cp, self.red_cp))
def __setstate__(self, state: dict[str, Any]) -> None:
self.__dict__.update(state)
if not hasattr(self, "position"):
self.position = self.point_from_a(self._position_distance)
def control_point_hostile_to(self, player: bool) -> ControlPoint:
def control_point_friendly_to(self, player: bool) -> ControlPoint:
if player:
return self.red_cp
return self.blue_cp
return self.red_cp
def control_point_hostile_to(self, player: bool) -> ControlPoint:
return self.control_point_friendly_to(not player)
def is_friendly(self, to_player: bool) -> bool:
"""Returns True if the objective is in friendly territory."""

View File

@ -86,43 +86,19 @@ class GroundConflictGenerator:
player_planned_combat_groups: List[CombatGroup],
enemy_planned_combat_groups: List[CombatGroup],
player_stance: CombatStance,
enemy_stance: CombatStance,
unit_map: UnitMap,
) -> None:
self.mission = mission
self.conflict = conflict
self.enemy_planned_combat_groups = enemy_planned_combat_groups
self.player_planned_combat_groups = player_planned_combat_groups
self.player_stance = CombatStance(player_stance)
self.enemy_stance = self._enemy_stance()
self.player_stance = player_stance
self.enemy_stance = enemy_stance
self.game = game
self.unit_map = unit_map
self.jtacs: List[JtacInfo] = []
def _enemy_stance(self) -> CombatStance:
"""Picks the enemy stance according to the number of planned groups on the frontline for each side"""
if len(self.enemy_planned_combat_groups) > len(
self.player_planned_combat_groups
):
return random.choice(
[
CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE,
CombatStance.ELIMINATION,
CombatStance.BREAKTHROUGH,
]
)
else:
return random.choice(
[
CombatStance.DEFENSIVE,
CombatStance.DEFENSIVE,
CombatStance.DEFENSIVE,
CombatStance.AMBUSH,
CombatStance.AGGRESSIVE,
]
)
def generate(self) -> None:
position = Conflict.frontline_position(
self.conflict.front_line, self.game.theater

View File

@ -247,14 +247,9 @@ class CoalitionMissionPlanner:
"""
return self.game.air_wing_for(self.is_player).can_auto_plan(mission_type)
def plan_missions(self) -> None:
def fulfill_missions(self) -> None:
"""Identifies and plans mission for the turn."""
player = "Blue" if self.is_player else "Red"
with logged_duration(f"{player} mission identification and fulfillment"):
with MultiEventTracer() as tracer:
commander = TheaterCommander(self.game, self.is_player)
commander.plan_missions(self, tracer)
with logged_duration(f"{player} mission scheduling"):
self.stagger_missions()

View File

@ -101,7 +101,7 @@ class HqAutomationSettingsBox(QGroupBox):
front_line = QCheckBox()
front_line.setChecked(self.game.settings.automate_front_line_reinforcements)
front_line.toggled.connect(self.set_front_line_automation)
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)
@ -147,12 +147,30 @@ class HqAutomationSettingsBox(QGroupBox):
)
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_automation(self, value: bool) -> None:
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