Configurable cruise & combat altitude + randomized offsets (phase 1)

This commit is contained in:
Raffson 2024-03-04 00:00:08 +01:00
parent 7158a5e60d
commit 9303e1cb9e
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
23 changed files with 190 additions and 166 deletions

View File

@ -44,6 +44,8 @@
* **[Options]** Add option (so it can be disabled when fixed in DCS) to force air-starts (except for the slots that work) at Nevatim due to https://forum.dcs.world/topic/335545-29-nevatim-ramp-starts-still-bugged/
* **[Cheat]** Add cheat option to manually manage REDFOR's TGOs
* **[UX]** Buy/Replace TGOs for free before the campaign has started
* **[Data]** Ability to define "cruise" & "combat" altitudes for airplanes
* **[Options]** Option to randomize altitudes for flights with airplanes
## Fixes
* **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import random
import uuid
from collections.abc import Iterator
from datetime import datetime, timedelta
@ -123,6 +124,11 @@ class Flight(
)
)
# altitude offset for planes
offset_factor = self.coalition.game.settings.max_plane_altitude_offset
offset_factor = random.randint(0, offset_factor)
self.plane_altitude_offset = 1000 * offset_factor * random.choice([-1, 1])
@property
def available_callsigns(self) -> List[str]:
callsigns = set()

View File

@ -6,7 +6,7 @@ from typing import Type
from game.ato.flightplans.ibuilder import IBuilder
from game.ato.flightplans.patrolling import PatrollingFlightPlan, PatrollingLayout
from game.ato.flightplans.waypointbuilder import WaypointBuilder
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
from game.utils import Distance, Heading, Speed, knots, meters, nautical_miles
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
@ -70,10 +70,7 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
builder = WaypointBuilder(self.flight)
if self.flight.unit_type.patrol_altitude is not None:
altitude = self.flight.unit_type.patrol_altitude
else:
altitude = feet(25000)
altitude = builder.get_patrol_altitude
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)

View File

@ -112,12 +112,11 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
)
assert self.package.waypoints is not None
heli_alt = feet(self.coalition.game.settings.heli_cruise_alt_agl)
altitude = heli_alt if self.flight.is_helo else self.doctrine.ingress_altitude
altitude_is_agl = self.flight.is_helo
builder = WaypointBuilder(self.flight)
altitude = builder.get_cruise_altitude
altitude_is_agl = self.flight.is_helo
if self.flight.is_hercules or self.flight.departure.cptype in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
@ -134,7 +133,7 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
self._generate_ctld_pickup(),
)
)
pickup.alt = heli_alt
pickup.alt = altitude
pickup_position = pickup.position
ingress = builder.ingress(
@ -160,8 +159,6 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
drop_pos = tgt.position.point_from_heading(heading, 1200)
drop_off_zone = MissionTarget("Dropoff zone", drop_pos)
dz = builder.dropoff_zone(drop_off_zone) if self.flight.is_helo else None
if dz:
dz.alt = heli_alt
return AirAssaultLayout(
departure=builder.takeoff(self.flight.departure),

View File

@ -8,7 +8,7 @@ from typing import Optional
from typing import TYPE_CHECKING, Type
from game.theater.missiontarget import MissionTarget
from game.utils import feet, Distance
from game.utils import Distance
from ._common_ctld import generate_random_ctld_point
from .ibuilder import IBuilder
from .planningerror import PlanningError
@ -133,12 +133,11 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
"Cannot plan transport mission for flight with no cargo."
)
heli_alt = feet(self.coalition.game.settings.heli_cruise_alt_agl)
altitude = heli_alt if self.flight.is_helo else self.doctrine.ingress_altitude
altitude_is_agl = self.flight.is_helo
builder = WaypointBuilder(self.flight)
altitude = builder.get_cruise_altitude
altitude_is_agl = self.flight.is_helo
pickup_ascent = None
pickup_descent = None
pickup = None

View File

@ -1,11 +1,10 @@
from __future__ import annotations
import random
from datetime import timedelta
from typing import Type
from game.theater import FrontLine
from game.utils import Distance, Speed, feet
from game.utils import Distance, Speed
from .capbuilder import CapBuilder
from .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan, PatrollingLayout
@ -41,14 +40,9 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
start_pos, end_pos = self.cap_racetrack_for_objective(location, barcap=True)
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),
)
builder = WaypointBuilder(self.flight)
patrol_alt = builder.get_patrol_altitude
start, end = builder.race_track(start_pos, end_pos, patrol_alt)
return PatrollingLayout(

View File

@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Type
from game.theater import FrontLine
from game.utils import Distance, Speed, kph, dcs_to_shapely_point
from game.utils import feet, nautical_miles
from game.utils import nautical_miles
from .ibuilder import IBuilder
from .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan, PatrollingLayout
@ -105,11 +105,7 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
builder = WaypointBuilder(self.flight)
is_helo = self.flight.unit_type.dcs_unit_type.helicopter
ingress_egress_altitude = (
self.doctrine.ingress_altitude
if not is_helo
else feet(self.coalition.game.settings.heli_combat_alt_agl)
)
ingress_egress_altitude = builder.get_combat_altitude
use_agl_patrol_altitude = is_helo
ip_solver = IpSolver(

View File

@ -11,7 +11,6 @@ from .formationattack import (
)
from .waypointbuilder import WaypointBuilder
from .. import FlightType
from ...utils import feet
class EscortFlightPlan(FormationAttackFlightPlan):
@ -43,12 +42,9 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
split = builder.split(self._get_split())
ingress_alt = self.doctrine.ingress_altitude
is_helo = builder.flight.is_helo
heli_alt = feet(self.coalition.game.settings.heli_combat_alt_agl)
initial = builder.escort_hold(
target.position if is_helo else self.package.waypoints.initial,
min(heli_alt, ingress_alt) if is_helo else ingress_alt,
)
pf = self.package.primary_flight
@ -69,9 +65,6 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
if layout.drop_off:
initial = builder.escort_hold(
layout.drop_off.position,
min(feet(200), ingress_alt)
if builder.flight.is_helo
else ingress_alt,
)
refuel = self._build_refuel(builder)
@ -80,13 +73,13 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
nav_to = builder.nav_path(
hold.position if hold else departure.position,
join.position,
self.doctrine.ingress_altitude,
builder.get_cruise_altitude,
)
nav_from = builder.nav_path(
refuel.position if refuel else split.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
builder.get_cruise_altitude,
)
return FormationAttackLayout(

View File

@ -61,14 +61,14 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
f"{self.flight.departure}"
)
builder = WaypointBuilder(self.flight)
altitude_is_agl = self.flight.is_helo
altitude = (
feet(self.coalition.game.settings.heli_cruise_alt_agl)
if altitude_is_agl
else self.flight.unit_type.preferred_patrol_altitude
else builder.get_patrol_altitude
)
builder = WaypointBuilder(self.flight)
return FerryLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(

View File

@ -11,7 +11,7 @@ from dcs import Point
from game.flightplan import HoldZoneGeometry
from game.theater import MissionTarget
from game.utils import Speed, meters, nautical_miles, feet
from game.utils import Speed, meters, nautical_miles
from .flightplan import FlightPlan
from .formation import FormationFlightPlan, FormationLayout
from .ibuilder import IBuilder
@ -210,14 +210,10 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
if self.flight.flight_type == FlightType.STRIKE:
hdg = self.package.target.position.heading_between_point(ingress.position)
pos = ingress.position.point_from_heading(hdg, nautical_miles(10).meters)
lineup = builder.nav(pos, self.flight.coalition.doctrine.ingress_altitude)
lineup = builder.nav(pos, builder.get_combat_altitude)
is_helo = self.flight.is_helo
ingress_egress_altitude = (
self.doctrine.ingress_altitude
if not is_helo
else feet(self.coalition.game.settings.heli_combat_alt_agl)
)
ingress_egress_altitude = builder.get_combat_altitude
use_agl_ingress_egress = is_helo
return FormationAttackLayout(

View File

@ -25,6 +25,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
def __init__(self, flight: Flight) -> None:
self.flight = flight
self._flight_plan: FlightPlanT | None = None
self.settings = self.flight.coalition.game.settings
def get_or_build(self) -> FlightPlanT:
if self._flight_plan is None:

View File

@ -5,7 +5,7 @@ from typing import Type
from dcs import Point
from game.utils import Distance, Heading, feet, meters
from game.utils import Distance, Heading, meters
from .ibuilder import IBuilder
from .patrolling import PatrollingLayout
from .refuelingflightplan import RefuelingFlightPlan
@ -98,11 +98,7 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
builder = WaypointBuilder(self.flight)
tanker_type = self.flight.unit_type
if tanker_type.patrol_altitude is not None:
altitude = tanker_type.patrol_altitude
else:
altitude = feet(21000)
altitude = builder.get_patrol_altitude
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)

View File

@ -66,13 +66,13 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
current_position = self.flight.state.estimate_position()
current_altitude, altitude_reference = self.flight.state.estimate_altitude()
builder = WaypointBuilder(self.flight)
altitude_is_agl = self.flight.is_helo
altitude = (
feet(self.coalition.game.settings.heli_cruise_alt_agl)
if altitude_is_agl
else self.flight.unit_type.preferred_patrol_altitude
else builder.get_patrol_altitude
)
builder = WaypointBuilder(self.flight)
abort_point = builder.nav(
current_position, current_altitude, altitude_reference == "RADIO"
)

View File

@ -105,20 +105,20 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
)
builder = WaypointBuilder(self.flight)
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
altitude = builder.get_patrol_altitude
start, end = builder.sweep(start_pos, target, altitude)
hold = builder.hold(self._hold_point())
return SweepLayout(
departure=builder.takeoff(self.flight.departure),
hold=hold,
nav_to=builder.nav_path(
hold.position, start.position, self.doctrine.ingress_altitude
),
nav_to=builder.nav_path(hold.position, start.position, altitude),
nav_from=builder.nav_path(
end.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
altitude,
),
sweep_start=start,
sweep_end=end,

View File

@ -1,12 +1,11 @@
from __future__ import annotations
import random
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Type
from game.utils import Distance, Speed, feet
from game.utils import Distance, Speed
from .capbuilder import CapBuilder
from .patrolling import PatrollingFlightPlan, PatrollingLayout
from .waypointbuilder import WaypointBuilder
@ -96,14 +95,9 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
def layout(self) -> TarCapLayout:
location = self.package.target
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),
)
builder = WaypointBuilder(self.flight)
patrol_alt = builder.get_patrol_altitude
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import timedelta
from typing import Type
from game.utils import Heading, feet, meters, nautical_miles
from game.utils import Heading, meters, nautical_miles
from .ibuilder import IBuilder
from .patrolling import PatrollingLayout
from .refuelingflightplan import RefuelingFlightPlan
@ -58,11 +58,7 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
builder = WaypointBuilder(self.flight)
tanker_type = self.flight.unit_type
if tanker_type.patrol_altitude is not None:
altitude = tanker_type.patrol_altitude
else:
altitude = feet(21000)
altitude = builder.get_patrol_altitude
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)

View File

@ -51,11 +51,34 @@ class WaypointBuilder:
self.navmesh = coalition.nav_mesh
self.targets = targets
self._bullseye = coalition.bullseye
self.settings = self.flight.coalition.game.settings
@property
def is_helo(self) -> bool:
return self.flight.is_helo
@property
def get_patrol_altitude(self) -> Distance:
return self.get_altitude(self.flight.unit_type.preferred_patrol_altitude)
@property
def get_cruise_altitude(self) -> Distance:
return self.get_altitude(self.flight.unit_type.preferred_cruise_altitude)
@property
def get_combat_altitude(self) -> Distance:
return self.get_altitude(self.flight.unit_type.preferred_combat_altitude)
def get_altitude(self, alt: Distance) -> Distance:
randomized_alt = feet(round(alt.feet + self.flight.plane_altitude_offset))
altitude = max(
self.doctrine.min_combat_altitude,
min(self.doctrine.max_combat_altitude, randomized_alt),
)
return (
feet(self.settings.heli_combat_alt_agl) if self.flight.is_helo else altitude
)
def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier.
@ -72,9 +95,7 @@ class WaypointBuilder:
"NAV",
FlightWaypointType.NAV,
position,
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
if self.is_helo
else self.doctrine.rendezvous_altitude,
self.get_cruise_altitude,
description="Enter theater",
pretty_name="Enter theater",
)
@ -101,9 +122,7 @@ class WaypointBuilder:
"NAV",
FlightWaypointType.NAV,
position,
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
if self.is_helo
else self.doctrine.rendezvous_altitude,
self.get_cruise_altitude,
description="Exit theater",
pretty_name="Exit theater",
)
@ -129,14 +148,10 @@ class WaypointBuilder:
return None
position = divert.position
altitude_type: AltitudeReference
altitude_type: AltitudeReference = "BARO"
if isinstance(divert, OffMapSpawn):
altitude = (
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
if self.is_helo
else self.doctrine.rendezvous_altitude
)
altitude_type = "BARO"
altitude = self.get_cruise_altitude
altitude_type = "RADIO" if self.is_helo else altitude_type
else:
altitude = meters(0)
altitude_type = "RADIO"
@ -173,9 +188,8 @@ class WaypointBuilder:
"HOLD",
FlightWaypointType.LOITER,
position,
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
if self.is_helo
else self.doctrine.ingress_altitude,
# TODO: dedicated altitude setting for holding
self.get_cruise_altitude if self.is_helo else self.get_combat_altitude,
alt_type,
description="Wait until push time",
pretty_name="Hold",
@ -190,9 +204,7 @@ class WaypointBuilder:
"JOIN",
FlightWaypointType.JOIN,
position,
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
if self.is_helo
else self.doctrine.ingress_altitude,
self.get_cruise_altitude,
alt_type,
description="Rendezvous with package",
pretty_name="Join",
@ -207,9 +219,7 @@ class WaypointBuilder:
"REFUEL",
FlightWaypointType.REFUEL,
position,
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
if self.is_helo
else self.doctrine.ingress_altitude,
self.get_cruise_altitude,
alt_type,
description="Refuel from tanker",
pretty_name="Refuel",
@ -224,9 +234,7 @@ class WaypointBuilder:
"SPLIT",
FlightWaypointType.SPLIT,
position,
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
if self.is_helo
else self.doctrine.ingress_altitude,
self.get_combat_altitude,
alt_type,
description="Depart from package",
pretty_name="Split",
@ -238,7 +246,7 @@ class WaypointBuilder:
position: Point,
objective: MissionTarget,
) -> FlightWaypoint:
alt = self.doctrine.ingress_altitude
alt = self.get_combat_altitude
alt_type: AltitudeReference = "BARO"
if self.is_helo or self.flight.is_hercules:
alt_type = "RADIO"
@ -272,9 +280,7 @@ class WaypointBuilder:
"EGRESS",
FlightWaypointType.EGRESS,
position,
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
if self.is_helo
else self.doctrine.ingress_altitude,
self.get_combat_altitude,
alt_type,
description=f"EGRESS from {target.name}",
pretty_name=f"EGRESS from {target.name}",
@ -315,7 +321,7 @@ class WaypointBuilder:
return self._target_area(
f"SEAD on {target.name}",
target,
altitude=self.doctrine.ingress_altitude,
altitude=self.get_combat_altitude,
alt_type="BARO",
)
@ -453,9 +459,7 @@ class WaypointBuilder:
"SEAD Search",
FlightWaypointType.NAV,
hold,
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
if self.is_helo
else self.doctrine.ingress_altitude,
self.get_combat_altitude,
alt_type="BARO",
description="Anchor and search from this point",
pretty_name="SEAD Search",
@ -468,10 +472,8 @@ class WaypointBuilder:
"SEAD Sweep",
FlightWaypointType.NAV,
hold,
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
if self.is_helo
else self.doctrine.ingress_altitude,
alt_type="BARO",
self.get_combat_altitude,
alt_type="BARO", # SEAD Sweep shouldn't be used for helicopters
description="Anchor and search from this point",
pretty_name="SEAD Sweep",
)
@ -499,13 +501,15 @@ class WaypointBuilder:
)
return hold
def escort_hold(self, start: Point, altitude: Distance) -> FlightWaypoint:
def escort_hold(self, start: Point) -> FlightWaypoint:
"""Creates custom waypoint for escort flights that need to hold.
Args:
start: Position of the waypoint.
altitude: Altitude of the holding pattern.
"""
altitude = self.get_combat_altitude
alt_type: Literal["BARO", "RADIO"] = "BARO"
if self.is_helo:
alt_type = "RADIO"
@ -592,9 +596,7 @@ class WaypointBuilder:
"TARGET",
FlightWaypointType.TARGET_GROUP_LOC,
target.position,
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
if self.is_helo
else self.doctrine.ingress_altitude,
self.get_combat_altitude,
alt_type,
description="Escort the package",
pretty_name="Target area",
@ -616,17 +618,19 @@ class WaypointBuilder:
pretty_name="Pick-up zone",
)
@staticmethod
def dropoff_zone(drop_off: MissionTarget) -> FlightWaypoint:
def dropoff_zone(self, drop_off: MissionTarget) -> FlightWaypoint:
"""Creates a dropoff landing zone waypoint
This waypoint is used to generate the Trigger Zone used for AirAssault and
AirLift using the CTLD plugin (see LogisticsGenerator)
"""
heli_alt = feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
altitude = heli_alt if self.flight.is_helo else meters(0)
return FlightWaypoint(
"DROPOFFZONE",
FlightWaypointType.DROPOFF_ZONE,
drop_off.position,
meters(0),
altitude,
"RADIO",
description=f"Drop off cargo at {drop_off.name}",
pretty_name="Drop-off zone",

View File

@ -26,8 +26,6 @@ class Doctrine:
strike: bool
antiship: bool
rendezvous_altitude: Distance
#: The minimum distance between the departure airfield and the hold point.
hold_distance: Distance
@ -46,11 +44,14 @@ class Doctrine:
#: target.
min_ingress_distance: Distance
ingress_altitude: Distance
min_patrol_altitude: Distance
max_patrol_altitude: Distance
pattern_altitude: Distance
min_cruise_altitude: Distance
max_cruise_altitude: Distance
min_combat_altitude: Distance
max_combat_altitude: Distance
#: The duration that CAP flights will remain on-station.
cap_duration: timedelta
@ -97,16 +98,17 @@ MODERN_DOCTRINE = Doctrine(
sead=True,
strike=True,
antiship=True,
rendezvous_altitude=feet(25000),
hold_distance=nautical_miles(25),
push_distance=nautical_miles(20),
join_distance=nautical_miles(20),
max_ingress_distance=nautical_miles(45),
min_ingress_distance=nautical_miles(10),
ingress_altitude=feet(20000),
min_patrol_altitude=feet(15000),
max_patrol_altitude=feet(33000),
pattern_altitude=feet(5000),
min_cruise_altitude=feet(20000),
max_cruise_altitude=feet(40000),
min_combat_altitude=feet(15000),
max_combat_altitude=feet(35000),
cap_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(15),
cap_max_track_length=nautical_miles(40),
@ -140,16 +142,17 @@ COLDWAR_DOCTRINE = Doctrine(
sead=True,
strike=True,
antiship=True,
rendezvous_altitude=feet(22000),
hold_distance=nautical_miles(15),
push_distance=nautical_miles(10),
join_distance=nautical_miles(10),
max_ingress_distance=nautical_miles(30),
min_ingress_distance=nautical_miles(10),
ingress_altitude=feet(18000),
min_patrol_altitude=feet(10000),
max_patrol_altitude=feet(24000),
pattern_altitude=feet(5000),
min_cruise_altitude=feet(20000),
max_cruise_altitude=feet(30000),
min_combat_altitude=feet(5000),
max_combat_altitude=feet(25000),
cap_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(12),
cap_max_track_length=nautical_miles(24),
@ -186,13 +189,14 @@ WWII_DOCTRINE = Doctrine(
hold_distance=nautical_miles(10),
push_distance=nautical_miles(5),
join_distance=nautical_miles(5),
rendezvous_altitude=feet(10000),
max_ingress_distance=nautical_miles(7),
min_ingress_distance=nautical_miles(5),
ingress_altitude=feet(8000),
min_patrol_altitude=feet(4000),
max_patrol_altitude=feet(15000),
pattern_altitude=feet(5000),
min_cruise_altitude=feet(5000),
max_cruise_altitude=feet(30000),
min_combat_altitude=feet(3000),
max_combat_altitude=feet(10000),
cap_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(8),
cap_max_track_length=nautical_miles(18),

View File

@ -132,6 +132,21 @@ class PatrolConfig:
)
@dataclass(frozen=True)
class AltitudesConfig:
cruise: Optional[Distance]
combat: Optional[Distance]
@classmethod
def from_data(cls, data: dict[str, Any]) -> AltitudesConfig:
cruise = data.get("cruise", None)
combat = data.get("combat", None)
return AltitudesConfig(
feet(cruise) if cruise is not None else None,
feet(combat) if combat is not None else None,
)
@dataclass(frozen=True)
class FuelConsumption:
#: The estimated taxi fuel requirement, in pounds.
@ -182,6 +197,9 @@ class AircraftType(UnitType[Type[FlyingType]]):
patrol_altitude: Optional[Distance]
patrol_speed: Optional[Speed]
cruise_altitude: Optional[Distance]
combat_altitude: Optional[Distance]
#: The maximum range between the origin airfield and the target for which the auto-
#: planner will consider this aircraft usable for a mission.
max_mission_range: Distance
@ -245,33 +263,13 @@ class AircraftType(UnitType[Type[FlyingType]]):
def max_speed(self) -> Speed:
return kph(self.dcs_unit_type.max_speed)
@property
@cached_property
def preferred_patrol_altitude(self) -> Distance:
if self.patrol_altitude is not None:
if self.patrol_altitude:
return self.patrol_altitude
else:
# Estimate based on max speed.
# Aircaft with max speed 600 kph will prefer patrol at 10 000 ft
# Aircraft with max speed 2800 kph will prefer pratrol at 33 000 ft
altitude_for_lowest_speed = feet(10 * 1000)
altitude_for_highest_speed = feet(33 * 1000)
lowest_speed = kph(600)
highest_speed = kph(2800)
factor = (self.max_speed - lowest_speed).kph / (
highest_speed - lowest_speed
).kph
altitude = (
altitude_for_lowest_speed
+ (altitude_for_highest_speed - altitude_for_lowest_speed) * factor
)
logging.debug(
f"Preferred patrol altitude for {self.dcs_unit_type.id}: {altitude.feet}"
)
rounded_altitude = feet(round(1000 * round(altitude.feet / 1000)))
return max(
altitude_for_lowest_speed,
min(altitude_for_highest_speed, rounded_altitude),
)
# TODO: somehow make the upper and lower limit configurable
return self.preferred_altitude(10, 33, "patrol")
def preferred_patrol_speed(self, altitude: Distance) -> Speed:
"""Preferred true airspeed when patrolling"""
@ -309,6 +307,46 @@ class AircraftType(UnitType[Type[FlyingType]]):
)
return min(Speed.from_mach(0.35, altitude), max_speed * 0.5)
@cached_property
def preferred_cruise_altitude(self) -> Distance:
if self.cruise_altitude:
return self.cruise_altitude
else:
# TODO: somehow make the upper and lower limit configurable
return self.preferred_altitude(20, 20, "cruise")
@cached_property
def preferred_combat_altitude(self) -> Distance:
if self.combat_altitude:
return self.combat_altitude
else:
# TODO: somehow make the upper and lower limit configurable
return self.preferred_altitude(20, 20, "combat")
def preferred_altitude(self, low: int, high: int, type: str) -> Distance:
# Estimate based on max speed.
# Aircraft with max speed 600 kph will prefer low
# Aircraft with max speed 2800 kph will prefer high
altitude_for_lowest_speed = feet(low * 1000)
altitude_for_highest_speed = feet(high * 1000)
lowest_speed = kph(600)
highest_speed = kph(2800)
factor = (self.max_speed - lowest_speed).kph / (
highest_speed - lowest_speed
).kph
altitude = (
altitude_for_lowest_speed
+ (altitude_for_highest_speed - altitude_for_lowest_speed) * factor
)
logging.debug(
f"Preferred {type} altitude for {self.dcs_unit_type.id}: {altitude.feet}"
)
rounded_altitude = feet(round(1000 * round(altitude.feet / 1000)))
return max(
altitude_for_lowest_speed,
min(altitude_for_highest_speed, rounded_altitude),
)
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
from game.radio.radios import ChannelInUseError, kHz
@ -442,6 +480,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
radio_config = RadioConfig.from_data(data.get("radios", {}))
patrol_config = PatrolConfig.from_data(data.get("patrol", {}))
altitudes_config = AltitudesConfig.from_data(data.get("altitudes", {}))
try:
mission_range = nautical_miles(int(data["max_range"]))
@ -510,6 +549,8 @@ class AircraftType(UnitType[Type[FlyingType]]):
max_group_size=data.get("max_group_size", aircraft.group_size_max),
patrol_altitude=patrol_config.altitude,
patrol_speed=patrol_config.speed,
cruise_altitude=altitudes_config.cruise,
combat_altitude=altitudes_config.combat,
max_mission_range=mission_range,
fuel_consumption=fuel_consumption,
default_livery=data.get("default_livery"),

View File

@ -52,7 +52,7 @@ class Migrator:
continue
found = False
for d in doctrines:
if c.faction.doctrine.rendezvous_altitude == d.rendezvous_altitude:
if c.faction.doctrine.max_patrol_altitude == d.max_patrol_altitude:
c.faction.doctrine = d
found = True
break

View File

@ -8,9 +8,7 @@ class LandingPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
if self.ai_despawn(waypoint):
waypoint.alt = round(
self.flight.coalition.doctrine.max_patrol_altitude.meters
)
waypoint.alt = round(self.flight.unit_type.preferred_patrol_altitude.meters)
waypoint.alt_type = "BARO"
else:
waypoint.type = "Land"

View File

@ -297,8 +297,18 @@ class Settings:
page=CAMPAIGN_DOCTRINE_PAGE,
section=GENERAL_SECTION,
default=False,
detail=("AI will jettison their fuel tanks as soon as they're empty."),
detail="AI will jettison their fuel tanks as soon as they're empty.",
)
max_plane_altitude_offset: int = bounded_int_option(
"Maximum randomized altitude offset (x1000 ft) for airplanes.",
page=CAMPAIGN_DOCTRINE_PAGE,
section=GENERAL_SECTION,
min=0,
max=5,
default=2,
detail="Creates a randomized altitude offset for airplanes.",
)
# Doctrine Distances Section
airbase_threat_range: int = bounded_int_option(
"Airbase threat range (nmi)",
page=CAMPAIGN_DOCTRINE_PAGE,

View File

@ -86,7 +86,7 @@ class QFlightWaypointList(QTableView):
self.model.setItem(row, 0, QWaypointItem(waypoint, row))
altitude = int(waypoint.alt.feet)
altitude = round(waypoint.alt.feet)
altitude_item = QStandardItem(f"{altitude}")
altitude_item.setEditable(True)
self.model.setItem(row, 1, altitude_item)