dcs_liberation/game/data/doctrine.py
zhexu14 5b858886c0
Doctrine cleanup (#3318)
This PR:

- Refactors the doctrine class to have a bit more structure, in
anticipation of adding more elements to Doctrine.
- Moves previously hard coded helo-specific altitudes into the Doctrine
class, aligning a bunch of altitudes ~200ft in the process.
- Refactors ingress_altitude to combat_altitude to clarify that the
altitude is applied to multiple waypoint types, not just the ingress
altitude.
2024-01-01 13:31:26 -08:00

225 lines
7.6 KiB
Python

from __future__ import annotations
from pathlib import Path
import yaml
from typing import Any, ClassVar
from dataclasses import dataclass
from datetime import timedelta
from game.data.units import UnitClass
from game.utils import Distance, feet, nautical_miles
@dataclass
class GroundUnitProcurementRatios:
ratios: dict[UnitClass, float]
def for_unit_class(self, unit_class: UnitClass) -> float:
try:
return self.ratios[unit_class] / sum(self.ratios.values())
except KeyError:
return 0.0
@staticmethod
def from_dict(data: dict[str, float]) -> GroundUnitProcurementRatios:
unit_class_enum_from_name = {unit.value: unit for unit in UnitClass}
r = {}
for unit_class in data:
if unit_class not in unit_class_enum_from_name:
raise ValueError(f"Could not find unit type {unit_class}")
r[unit_class_enum_from_name[unit_class]] = float(data[unit_class])
return GroundUnitProcurementRatios(r)
@dataclass
class Helicopter:
#: The altitude used for combat section of a flight, overrides the base combat_altitude parameter for helos
combat_altitude: Distance
#: The altitude used for forming up a pacakge. Overrides the base rendezvous_altitude parameter for helos
rendezvous_altitude: Distance
#: Altitude of the nav points (cruise section) of air assault missions.
air_assault_nav_altitude: Distance
@staticmethod
def from_dict(data: dict[str, Any]) -> Helicopter:
return Helicopter(
combat_altitude=feet(data["combat_altitude_ft_agl"]),
rendezvous_altitude=feet(data["rendezvous_altitude_ft_agl"]),
air_assault_nav_altitude=feet(data["air_assault_nav_altitude_ft_agl"]),
)
@dataclass
class Cas:
#: The duration that CAP flights will remain on-station.
duration: timedelta
@staticmethod
def from_dict(data: dict[str, Any]) -> Cas:
return Cas(duration=timedelta(minutes=data["duration_minutes"]))
@dataclass
class Sweep:
#: Length of the sweep / patrol leg
distance: Distance
@staticmethod
def from_dict(data: dict[str, Any]) -> Sweep:
return Sweep(
distance=nautical_miles(data["distance_nm"]),
)
@dataclass
class Cap:
#: The duration that CAP flights will remain on-station.
duration: timedelta
#: The minimum length of the CAP race track.
min_track_length: Distance
#: The maximum length of the CAP race track.
max_track_length: Distance
#: The minimum distance between the defended position and the *end* of the
#: CAP race track.
min_distance_from_cp: Distance
#: The maximum distance between the defended position and the *end* of the
#: CAP race track.
max_distance_from_cp: Distance
#: The engagement range of CAP flights. Any enemy aircraft within this range
#: of the CAP's current position will be engaged by the CAP.
engagement_range: Distance
#: Defines the range of altitudes CAP racetracks are planned at.
min_patrol_altitude: Distance
max_patrol_altitude: Distance
@staticmethod
def from_dict(data: dict[str, Any]) -> Cap:
return Cap(
duration=timedelta(minutes=data["duration_minutes"]),
min_track_length=nautical_miles(data["min_track_length_nm"]),
max_track_length=nautical_miles(data["max_track_length_nm"]),
min_distance_from_cp=nautical_miles(data["min_distance_from_cp_nm"]),
max_distance_from_cp=nautical_miles(data["max_distance_from_cp_nm"]),
engagement_range=nautical_miles(data["engagement_range_nm"]),
min_patrol_altitude=feet(data["min_patrol_altitude_ft_msl"]),
max_patrol_altitude=feet(data["max_patrol_altitude_ft_msl"]),
)
@dataclass(frozen=True)
class Doctrine:
#: Name of the doctrine, used to assign a doctrine in a faction.
name: str
#: The minimum distance between the departure airfield and the hold point.
hold_distance: Distance
#: The minimum distance between the hold point and the join point.
push_distance: Distance
#: The distance between the join point and the ingress point. Only used for the
#: fallback flight plan layout (when the departure airfield is near a threat zone).
join_distance: Distance
#: The maximum distance between the ingress point (beginning of the attack) and
#: target.
max_ingress_distance: Distance
#: The minimum distance between the ingress point (beginning of the attack) and
#: target.
min_ingress_distance: Distance
#: The altitude used for combat section of a flight.
combat_altitude: Distance
#: The altitude used for forming up a pacakge.
rendezvous_altitude: Distance
#: Defines prioritization of ground unit purchases.
ground_unit_procurement_ratios: GroundUnitProcurementRatios
#: Helicopter specific doctrines.
helicopter: Helicopter
#: Doctrine for CAS missions.
cas: Cas
#: Doctrine for CAP missions.
cap: Cap
#: Doctrine for Fighter Sweep missions.
sweep: Sweep
_by_name: ClassVar[dict[str, Doctrine]] = {}
_loaded: ClassVar[bool] = False
def resolve_combat_altitude(self, is_helo: bool = False) -> Distance:
if is_helo:
return self.helicopter.combat_altitude
return self.combat_altitude
def resolve_rendezvous_altitude(self, is_helo: bool = False) -> Distance:
if is_helo:
return self.helicopter.rendezvous_altitude
return self.rendezvous_altitude
@classmethod
def register(cls, doctrine: Doctrine) -> None:
if doctrine.name in cls._by_name:
duplicate = cls._by_name[doctrine.name]
raise ValueError(f"Doctrine {doctrine.name} is already loaded")
cls._by_name[doctrine.name] = doctrine
@classmethod
def named(cls, name: str) -> Doctrine:
if not cls._loaded:
cls.load_all()
return cls._by_name[name]
@classmethod
def all_doctrines(cls) -> list[Doctrine]:
if not cls._loaded:
cls.load_all()
return list(cls._by_name.values())
@classmethod
def load_all(cls) -> None:
if cls._loaded:
return
for doctrine_file_path in Path("resources/doctrines").glob("**/*.yaml"):
with doctrine_file_path.open(encoding="utf8") as doctrine_file:
data = yaml.safe_load(doctrine_file)
cls.register(
Doctrine(
name=data["name"],
rendezvous_altitude=feet(data["rendezvous_altitude_ft_msl"]),
hold_distance=nautical_miles(data["hold_distance_nm"]),
push_distance=nautical_miles(data["push_distance_nm"]),
join_distance=nautical_miles(data["join_distance_nm"]),
max_ingress_distance=nautical_miles(
data["max_ingress_distance_nm"]
),
min_ingress_distance=nautical_miles(
data["min_ingress_distance_nm"]
),
combat_altitude=feet(data["combat_altitude_ft_msl"]),
ground_unit_procurement_ratios=GroundUnitProcurementRatios.from_dict(
data["ground_unit_procurement_ratios"]
),
helicopter=Helicopter.from_dict(data["helicopter"]),
cas=Cas.from_dict(data["cas"]),
cap=Cap.from_dict(data["cap"]),
sweep=Sweep.from_dict(data["sweep"]),
)
)
cls._loaded = True