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.
This commit is contained in:
zhexu14 2024-01-02 08:31:26 +11:00 committed by GitHub
parent c695e7724a
commit 5b858886c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 213 additions and 163 deletions

View File

@ -88,7 +88,7 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
raise PlanningError("Air assault is only usable by helicopters")
assert self.package.waypoints is not None
altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude
altitude = self.doctrine.helicopter.air_assault_nav_altitude
altitude_is_agl = self.flight.is_helo
builder = WaypointBuilder(self.flight, self.coalition)

View File

@ -19,7 +19,7 @@ class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
@property
def patrol_duration(self) -> timedelta:
return self.flight.coalition.doctrine.cap_duration
return self.flight.coalition.doctrine.cap.duration
@property
def patrol_speed(self) -> Speed:
@ -29,7 +29,7 @@ class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
@property
def engagement_distance(self) -> Distance:
return self.flight.coalition.doctrine.cap_engagement_range
return self.flight.coalition.doctrine.cap.engagement_range
class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
@ -44,8 +44,8 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
patrol_alt = max(
self.doctrine.min_patrol_altitude,
min(self.doctrine.max_patrol_altitude, randomized_alt),
self.doctrine.cap.min_patrol_altitude,
min(self.doctrine.cap.max_patrol_altitude, randomized_alt),
)
builder = WaypointBuilder(self.flight, self.coalition)

View File

@ -90,10 +90,10 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
# buffer.
distance_to_no_fly = (
meters(position.distance(self.threat_zones.all))
- self.doctrine.cap_engagement_range
- self.doctrine.cap.engagement_range
- nautical_miles(5)
)
max_track_length = self.doctrine.cap_max_track_length
max_track_length = self.doctrine.cap.max_track_length
else:
# Other race tracks (TARCAPs, currently) just try to keep some
# distance from the nearest enemy airbase, but since they are by
@ -108,15 +108,15 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
# TARCAPs fly short racetracks because they need to react faster.
max_track_length = self.doctrine.cap_min_track_length + 0.3 * (
self.doctrine.cap_max_track_length - self.doctrine.cap_min_track_length
max_track_length = self.doctrine.cap.min_track_length + 0.3 * (
self.doctrine.cap.max_track_length - self.doctrine.cap.min_track_length
)
min_cap_distance = min(
self.doctrine.cap_min_distance_from_cp, distance_to_no_fly
self.doctrine.cap.min_distance_from_cp, distance_to_no_fly
)
max_cap_distance = min(
self.doctrine.cap_max_distance_from_cp, distance_to_no_fly
self.doctrine.cap.max_distance_from_cp, distance_to_no_fly
)
end = location.position.point_from_heading(
@ -125,7 +125,7 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
)
track_length = random.randint(
int(self.doctrine.cap_min_track_length.meters),
int(self.doctrine.cap.min_track_length.meters),
int(max_track_length.meters),
)
start = end.point_from_heading(heading.opposite.degrees, track_length)

View File

@ -44,7 +44,7 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
@property
def patrol_duration(self) -> timedelta:
return self.flight.coalition.doctrine.cas_duration
return self.flight.coalition.doctrine.cas.duration
@property
def patrol_speed(self) -> Speed:
@ -96,7 +96,7 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
builder = WaypointBuilder(self.flight, self.coalition)
is_helo = self.flight.unit_type.dcs_unit_type.helicopter
patrol_altitude = self.doctrine.ingress_altitude if not is_helo else meters(50)
patrol_altitude = self.doctrine.resolve_combat_altitude(is_helo)
use_agl_patrol_altitude = is_helo
ip_solver = IpSolver(

View File

@ -33,7 +33,7 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
departure=builder.takeoff(self.flight.departure),
hold=hold,
nav_to=builder.nav_path(
hold.position, join.position, self.doctrine.ingress_altitude
hold.position, join.position, self.doctrine.combat_altitude
),
join=join,
ingress=ingress,
@ -43,7 +43,7 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
nav_from=builder.nav_path(
refuel.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
self.doctrine.combat_altitude,
),
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),

View File

@ -163,7 +163,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
departure=builder.takeoff(self.flight.departure),
hold=hold,
nav_to=builder.nav_path(
hold.position, join.position, self.doctrine.ingress_altitude
hold.position, join.position, self.doctrine.combat_altitude
),
join=join,
ingress=ingress,
@ -173,7 +173,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
nav_from=builder.nav_path(
refuel.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
self.doctrine.combat_altitude,
),
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),

View File

@ -114,11 +114,11 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
self.package.waypoints.join.heading_between_point(target)
)
start_pos = target.point_from_heading(
heading.degrees, -self.doctrine.sweep_distance.meters
heading.degrees, -self.doctrine.sweep.distance.meters
)
builder = WaypointBuilder(self.flight, self.coalition)
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
start, end = builder.sweep(start_pos, target, self.doctrine.combat_altitude)
hold = builder.hold(self._hold_point())
@ -126,12 +126,12 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
departure=builder.takeoff(self.flight.departure),
hold=hold,
nav_to=builder.nav_path(
hold.position, start.position, self.doctrine.ingress_altitude
hold.position, start.position, self.doctrine.combat_altitude
),
nav_from=builder.nav_path(
end.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
self.doctrine.combat_altitude,
),
sweep_start=start,
sweep_end=end,

View File

@ -40,7 +40,7 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
# flights in the package that have requested escort. If the package
# requests an escort the CAP self.flight will remain on station for the
# duration of the escorted mission, or until it is winchester/bingo.
return self.flight.coalition.doctrine.cap_duration
return self.flight.coalition.doctrine.cap.duration
@property
def patrol_speed(self) -> Speed:
@ -50,7 +50,7 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
@property
def engagement_distance(self) -> Distance:
return self.flight.coalition.doctrine.cap_engagement_range
return self.flight.coalition.doctrine.cap.engagement_range
@staticmethod
def builder_type() -> Type[Builder]:
@ -90,8 +90,8 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
patrol_alt = max(
self.doctrine.min_patrol_altitude,
min(self.doctrine.max_patrol_altitude, randomized_alt),
self.doctrine.cap.min_patrol_altitude,
min(self.doctrine.cap.max_patrol_altitude, randomized_alt),
)
builder = WaypointBuilder(self.flight, self.coalition)

View File

@ -72,7 +72,7 @@ class WaypointBuilder:
"NAV",
FlightWaypointType.NAV,
position,
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
self.doctrine.resolve_rendezvous_altitude(self.is_helo),
description="Enter theater",
pretty_name="Enter theater",
)
@ -99,7 +99,7 @@ class WaypointBuilder:
"NAV",
FlightWaypointType.NAV,
position,
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
self.doctrine.resolve_rendezvous_altitude(self.is_helo),
description="Exit theater",
pretty_name="Exit theater",
)
@ -127,10 +127,7 @@ class WaypointBuilder:
position = divert.position
altitude_type: AltitudeReference
if isinstance(divert, OffMapSpawn):
if self.is_helo:
altitude = meters(500)
else:
altitude = self.doctrine.rendezvous_altitude
altitude = self.doctrine.resolve_rendezvous_altitude(self.is_helo)
altitude_type = "BARO"
else:
altitude = meters(0)
@ -168,10 +165,7 @@ class WaypointBuilder:
"HOLD",
FlightWaypointType.LOITER,
position,
# Bug: DCS only accepts MSL altitudes for the orbit task and 500 meters is
# below the ground for most if not all of NTTR (and lots of places in other
# maps).
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
self.doctrine.resolve_rendezvous_altitude(self.is_helo),
alt_type,
description="Wait until push time",
pretty_name="Hold",
@ -186,7 +180,7 @@ class WaypointBuilder:
"JOIN",
FlightWaypointType.JOIN,
position,
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
self.doctrine.resolve_combat_altitude(self.is_helo),
alt_type,
description="Rendezvous with package",
pretty_name="Join",
@ -201,7 +195,7 @@ class WaypointBuilder:
"REFUEL",
FlightWaypointType.REFUEL,
position,
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
self.doctrine.resolve_combat_altitude(self.is_helo),
alt_type,
description="Refuel from tanker",
pretty_name="Refuel",
@ -229,7 +223,7 @@ class WaypointBuilder:
"SPLIT",
FlightWaypointType.SPLIT,
position,
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
self.doctrine.resolve_combat_altitude(self.is_helo),
alt_type,
description="Depart from package",
pretty_name="Split",
@ -249,7 +243,7 @@ class WaypointBuilder:
"INGRESS",
ingress_type,
position,
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
self.doctrine.resolve_combat_altitude(self.is_helo),
alt_type,
description=f"INGRESS on {objective.name}",
pretty_name=f"INGRESS on {objective.name}",
@ -294,7 +288,7 @@ class WaypointBuilder:
f"SEAD on {target.name}",
target,
flyover=True,
altitude=self.doctrine.ingress_altitude,
altitude=self.doctrine.combat_altitude,
alt_type="BARO",
)
@ -484,7 +478,7 @@ class WaypointBuilder:
"TARGET",
FlightWaypointType.TARGET_GROUP_LOC,
target.position,
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
self.doctrine.resolve_combat_altitude(self.is_helo),
alt_type,
description="Escort the package",
pretty_name="Target area",

View File

@ -158,7 +158,7 @@ class TheaterState(WorldState["TheaterState"]):
# Plan enough rounds of CAP that the target has coverage over the expected
# mission duration.
mission_duration = game.settings.desired_player_mission_duration.total_seconds()
barcap_duration = coalition.doctrine.cap_duration.total_seconds()
barcap_duration = coalition.doctrine.cap.duration.total_seconds()
barcap_rounds = math.ceil(mission_duration / barcap_duration)
refueling_targets: list[MissionTarget] = []

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from pathlib import Path
import yaml
from typing import ClassVar
from typing import Any, ClassVar
from dataclasses import dataclass
from datetime import timedelta
@ -32,18 +32,94 @@ class GroundUnitProcurementRatios:
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
cas: bool
cap: bool
sead: bool
strike: bool
antiship: bool
rendezvous_altitude: Distance
#: The minimum distance between the departure airfield and the hold point.
hold_distance: Distance
@ -62,42 +138,40 @@ class Doctrine:
#: target.
min_ingress_distance: Distance
ingress_altitude: Distance
#: The altitude used for combat section of a flight.
combat_altitude: Distance
min_patrol_altitude: Distance
max_patrol_altitude: Distance
pattern_altitude: Distance
#: The duration that CAP flights will remain on-station.
cap_duration: timedelta
#: The minimum length of the CAP race track.
cap_min_track_length: Distance
#: The maximum length of the CAP race track.
cap_max_track_length: Distance
#: The minimum distance between the defended position and the *end* of the
#: CAP race track.
cap_min_distance_from_cp: Distance
#: The maximum distance between the defended position and the *end* of the
#: CAP race track.
cap_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.
cap_engagement_range: Distance
cas_duration: timedelta
sweep_distance: 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:
@ -127,11 +201,6 @@ class Doctrine:
cls.register(
Doctrine(
name=data["name"],
cap=data["cap"],
cas=data["cas"],
sead=data["sead"],
strike=data["strike"],
antiship=data["antiship"],
rendezvous_altitude=feet(data["rendezvous_altitude_ft_msl"]),
hold_distance=nautical_miles(data["hold_distance_nm"]),
push_distance=nautical_miles(data["push_distance_nm"]),
@ -142,31 +211,14 @@ class Doctrine:
min_ingress_distance=nautical_miles(
data["min_ingress_distance_nm"]
),
ingress_altitude=feet(data["ingress_altitude_ft_msl"]),
min_patrol_altitude=feet(data["min_patrol_altitude_ft_msl"]),
max_patrol_altitude=feet(data["max_patrol_altitude_ft_msl"]),
pattern_altitude=feet(data["pattern_altitude_ft_msl"]),
cap_duration=timedelta(minutes=data["cap_duration_minutes"]),
cap_min_track_length=nautical_miles(
data["cap_min_track_length_nm"]
),
cap_max_track_length=nautical_miles(
data["cap_max_track_length_nm"]
),
cap_min_distance_from_cp=nautical_miles(
data["cap_min_distance_from_cp_nm"]
),
cap_max_distance_from_cp=nautical_miles(
data["cap_max_distance_from_cp_nm"]
),
cap_engagement_range=nautical_miles(
data["cap_engagement_range_nm"]
),
cas_duration=timedelta(minutes=data["cas_duration_minutes"]),
sweep_distance=nautical_miles(data["sweep_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

View File

@ -165,7 +165,7 @@ class ThreatZones:
cls, doctrine: Doctrine, control_point: ControlPoint
) -> Distance:
cap_threat_range = (
doctrine.cap_max_distance_from_cp + doctrine.cap_engagement_range
doctrine.cap.max_distance_from_cp + doctrine.cap.engagement_range
)
opposing_airfield = cls.closest_enemy_airbase(
control_point, cap_threat_range * 2

View File

@ -1,27 +1,24 @@
name: coldwar
cap: true
cas: true
sead: true
strike: true
antiship: true
rendezvous_altitude_ft_msl: 22000
hold_distance_nm: 15
push_distance_nm: 10
join_distance_nm: 10
max_ingress_distance_nm: 30
min_ingress_distance_nm: 10
ingress_altitude_ft_msl: 18000
min_patrol_altitude_ft_msl: 10000
max_patrol_altitude_ft_msl: 24000
pattern_altitude_ft_msl: 5000
cap_duration_minutes: 30
cap_min_track_length_nm: 12
cap_max_track_length_nm: 24
cap_min_distance_from_cp_nm: 8
cap_max_distance_from_cp_nm: 25
cap_engagement_range_nm: 35
cas_duration_minutes: 30
sweep_distance_nm: 40
rendezvous_altitude_ft_msl: 22000
combat_altitude_ft_msl: 18000
cap:
duration_minutes: 30
min_track_length_nm: 12
max_track_length_nm: 24
min_distance_from_cp_nm: 8
max_distance_from_cp_nm: 25
engagement_range_nm: 35
min_patrol_altitude_ft_msl: 10000
max_patrol_altitude_ft_msl: 24000
cas:
duration_minutes: 30
sweep:
distance_nm: 40
ground_unit_procurement_ratios:
Tank: 4
ATGM: 2
@ -30,3 +27,8 @@ ground_unit_procurement_ratios:
Artillery: 1
SHORAD: 2
Recon: 1
helicopter:
combat_altitude_ft_agl: 200
rendezvous_altitude_ft_agl: 1500
air_assault_nav_altitude_ft_agl: 1500

View File

@ -1,27 +1,24 @@
name: modern
cap: true
cas: true
sead: true
strike: true
antiship: true
rendezvous_altitude_ft_msl: 25000
hold_distance_nm: 25
push_distance_nm: 20
join_distance_nm: 20
max_ingress_distance_nm: 45
min_ingress_distance_nm: 10
ingress_altitude_ft_msl: 20000
min_patrol_altitude_ft_msl: 15000
max_patrol_altitude_ft_msl: 33000
pattern_altitude_ft_msl: 5000
cap_duration_minutes: 30
cap_min_track_length_nm: 15
cap_max_track_length_nm: 40
cap_min_distance_from_cp_nm: 10
cap_max_distance_from_cp_nm: 40
cap_engagement_range_nm: 50
cas_duration_minutes: 30
sweep_distance_nm: 60
rendezvous_altitude_ft_msl: 25000
combat_altitude_ft_msl: 20000
cap:
duration_minutes: 30
min_track_length_nm: 15
max_track_length_nm: 40
min_distance_from_cp_nm: 10
max_distance_from_cp_nm: 40
engagement_range_nm: 50
min_patrol_altitude_ft_msl: 15000
max_patrol_altitude_ft_msl: 33000
cas:
duration_minutes: 30
sweep:
distance_nm: 60
ground_unit_procurement_ratios:
Tank: 3
ATGM: 2
@ -30,3 +27,7 @@ ground_unit_procurement_ratios:
Artillery: 1
SHORAD: 2
Recon: 1
helicopter:
combat_altitude_ft_agl: 200
rendezvous_altitude_ft_agl: 1500
air_assault_nav_altitude_ft_agl: 1500

View File

@ -1,27 +1,24 @@
name: ww2
cap: true
cas: true
sead: false
strike: true
antiship: true
hold_distance_nm: 10
push_distance_nm: 5
join_distance_nm: 5
rendezvous_altitude_ft_msl: 10000
max_ingress_distance_nm: 7
min_ingress_distance_nm: 5
ingress_altitude_ft_msl: 8000
min_patrol_altitude_ft_msl: 4000
max_patrol_altitude_ft_msl: 15000
pattern_altitude_ft_msl: 5000
cap_duration_minutes: 30
cap_min_track_length_nm: 8
cap_max_track_length_nm: 18
cap_min_distance_from_cp_nm: 0
cap_max_distance_from_cp_nm: 5
cap_engagement_range_nm: 20
cas_duration_minutes: 30
sweep_distance_nm: 10
rendezvous_altitude_ft_msl: 10000
combat_altitude_ft_msl: 8000
cap:
duration_minutes: 30
min_track_length_nm: 8
max_track_length_nm: 18
min_distance_from_cp_nm: 0
max_distance_from_cp_nm: 5
engagement_range_nm: 20
min_patrol_altitude_ft_msl: 4000
max_patrol_altitude_ft_msl: 15000
cas:
duration_minutes: 30
sweep:
distance_nm: 10
ground_unit_procurement_ratios:
Tank: 3
ATGM: 3
@ -29,3 +26,7 @@ ground_unit_procurement_ratios:
Artillery: 1
SHORAD: 3
Recon: 1
helicopter:
combat_altitude_ft_agl: 200
rendezvous_altitude_ft_agl: 1500
air_assault_nav_altitude_ft_agl: 1500