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/ * **[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 * **[Cheat]** Add cheat option to manually manage REDFOR's TGOs
* **[UX]** Buy/Replace TGOs for free before the campaign has started * **[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 ## Fixes
* **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task * **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import random
import uuid import uuid
from collections.abc import Iterator from collections.abc import Iterator
from datetime import datetime, timedelta 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 @property
def available_callsigns(self) -> List[str]: def available_callsigns(self) -> List[str]:
callsigns = set() callsigns = set()

View File

@ -6,7 +6,7 @@ from typing import Type
from game.ato.flightplans.ibuilder import IBuilder from game.ato.flightplans.ibuilder import IBuilder
from game.ato.flightplans.patrolling import PatrollingFlightPlan, PatrollingLayout from game.ato.flightplans.patrolling import PatrollingFlightPlan, PatrollingLayout
from game.ato.flightplans.waypointbuilder import WaypointBuilder 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]): class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
@ -70,10 +70,7 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
builder = WaypointBuilder(self.flight) builder = WaypointBuilder(self.flight)
if self.flight.unit_type.patrol_altitude is not None: altitude = builder.get_patrol_altitude
altitude = self.flight.unit_type.patrol_altitude
else:
altitude = feet(25000)
racetrack = builder.race_track(racetrack_start, racetrack_end, 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 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) 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 [ if self.flight.is_hercules or self.flight.departure.cptype in [
ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP, ControlPointType.LHA_GROUP,
@ -134,7 +133,7 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
self._generate_ctld_pickup(), self._generate_ctld_pickup(),
) )
) )
pickup.alt = heli_alt pickup.alt = altitude
pickup_position = pickup.position pickup_position = pickup.position
ingress = builder.ingress( ingress = builder.ingress(
@ -160,8 +159,6 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
drop_pos = tgt.position.point_from_heading(heading, 1200) drop_pos = tgt.position.point_from_heading(heading, 1200)
drop_off_zone = MissionTarget("Dropoff zone", drop_pos) drop_off_zone = MissionTarget("Dropoff zone", drop_pos)
dz = builder.dropoff_zone(drop_off_zone) if self.flight.is_helo else None dz = builder.dropoff_zone(drop_off_zone) if self.flight.is_helo else None
if dz:
dz.alt = heli_alt
return AirAssaultLayout( return AirAssaultLayout(
departure=builder.takeoff(self.flight.departure), departure=builder.takeoff(self.flight.departure),

View File

@ -8,7 +8,7 @@ from typing import Optional
from typing import TYPE_CHECKING, Type from typing import TYPE_CHECKING, Type
from game.theater.missiontarget import MissionTarget 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 ._common_ctld import generate_random_ctld_point
from .ibuilder import IBuilder from .ibuilder import IBuilder
from .planningerror import PlanningError from .planningerror import PlanningError
@ -133,12 +133,11 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
"Cannot plan transport mission for flight with no cargo." "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) builder = WaypointBuilder(self.flight)
altitude = builder.get_cruise_altitude
altitude_is_agl = self.flight.is_helo
pickup_ascent = None pickup_ascent = None
pickup_descent = None pickup_descent = None
pickup = None pickup = None

View File

@ -1,11 +1,10 @@
from __future__ import annotations from __future__ import annotations
import random
from datetime import timedelta from datetime import timedelta
from typing import Type from typing import Type
from game.theater import FrontLine from game.theater import FrontLine
from game.utils import Distance, Speed, feet from game.utils import Distance, Speed
from .capbuilder import CapBuilder from .capbuilder import CapBuilder
from .invalidobjectivelocation import InvalidObjectiveLocation from .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan, PatrollingLayout 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) 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) builder = WaypointBuilder(self.flight)
patrol_alt = builder.get_patrol_altitude
start, end = builder.race_track(start_pos, end_pos, patrol_alt) start, end = builder.race_track(start_pos, end_pos, patrol_alt)
return PatrollingLayout( return PatrollingLayout(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ from typing import Type
from dcs import Point from dcs import Point
from game.utils import Distance, Heading, feet, meters from game.utils import Distance, Heading, meters
from .ibuilder import IBuilder from .ibuilder import IBuilder
from .patrolling import PatrollingLayout from .patrolling import PatrollingLayout
from .refuelingflightplan import RefuelingFlightPlan from .refuelingflightplan import RefuelingFlightPlan
@ -98,11 +98,7 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
builder = WaypointBuilder(self.flight) builder = WaypointBuilder(self.flight)
tanker_type = self.flight.unit_type altitude = builder.get_patrol_altitude
if tanker_type.patrol_altitude is not None:
altitude = tanker_type.patrol_altitude
else:
altitude = feet(21000)
racetrack = builder.race_track(racetrack_start, racetrack_end, 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_position = self.flight.state.estimate_position()
current_altitude, altitude_reference = self.flight.state.estimate_altitude() current_altitude, altitude_reference = self.flight.state.estimate_altitude()
builder = WaypointBuilder(self.flight)
altitude_is_agl = self.flight.is_helo altitude_is_agl = self.flight.is_helo
altitude = ( altitude = (
feet(self.coalition.game.settings.heli_cruise_alt_agl) feet(self.coalition.game.settings.heli_cruise_alt_agl)
if altitude_is_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( abort_point = builder.nav(
current_position, current_altitude, altitude_reference == "RADIO" current_position, current_altitude, altitude_reference == "RADIO"
) )

View File

@ -105,20 +105,20 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
) )
builder = WaypointBuilder(self.flight) 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()) hold = builder.hold(self._hold_point())
return SweepLayout( return SweepLayout(
departure=builder.takeoff(self.flight.departure), departure=builder.takeoff(self.flight.departure),
hold=hold, hold=hold,
nav_to=builder.nav_path( nav_to=builder.nav_path(hold.position, start.position, altitude),
hold.position, start.position, self.doctrine.ingress_altitude
),
nav_from=builder.nav_path( nav_from=builder.nav_path(
end.position, end.position,
self.flight.arrival.position, self.flight.arrival.position,
self.doctrine.ingress_altitude, altitude,
), ),
sweep_start=start, sweep_start=start,
sweep_end=end, sweep_end=end,

View File

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

View File

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

View File

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

View File

@ -26,8 +26,6 @@ class Doctrine:
strike: bool strike: bool
antiship: bool antiship: bool
rendezvous_altitude: Distance
#: The minimum distance between the departure airfield and the hold point. #: The minimum distance between the departure airfield and the hold point.
hold_distance: Distance hold_distance: Distance
@ -46,11 +44,14 @@ class Doctrine:
#: target. #: target.
min_ingress_distance: Distance min_ingress_distance: Distance
ingress_altitude: Distance
min_patrol_altitude: Distance min_patrol_altitude: Distance
max_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. #: The duration that CAP flights will remain on-station.
cap_duration: timedelta cap_duration: timedelta
@ -97,16 +98,17 @@ MODERN_DOCTRINE = Doctrine(
sead=True, sead=True,
strike=True, strike=True,
antiship=True, antiship=True,
rendezvous_altitude=feet(25000),
hold_distance=nautical_miles(25), hold_distance=nautical_miles(25),
push_distance=nautical_miles(20), push_distance=nautical_miles(20),
join_distance=nautical_miles(20), join_distance=nautical_miles(20),
max_ingress_distance=nautical_miles(45), max_ingress_distance=nautical_miles(45),
min_ingress_distance=nautical_miles(10), min_ingress_distance=nautical_miles(10),
ingress_altitude=feet(20000),
min_patrol_altitude=feet(15000), min_patrol_altitude=feet(15000),
max_patrol_altitude=feet(33000), 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_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(15), cap_min_track_length=nautical_miles(15),
cap_max_track_length=nautical_miles(40), cap_max_track_length=nautical_miles(40),
@ -140,16 +142,17 @@ COLDWAR_DOCTRINE = Doctrine(
sead=True, sead=True,
strike=True, strike=True,
antiship=True, antiship=True,
rendezvous_altitude=feet(22000),
hold_distance=nautical_miles(15), hold_distance=nautical_miles(15),
push_distance=nautical_miles(10), push_distance=nautical_miles(10),
join_distance=nautical_miles(10), join_distance=nautical_miles(10),
max_ingress_distance=nautical_miles(30), max_ingress_distance=nautical_miles(30),
min_ingress_distance=nautical_miles(10), min_ingress_distance=nautical_miles(10),
ingress_altitude=feet(18000),
min_patrol_altitude=feet(10000), min_patrol_altitude=feet(10000),
max_patrol_altitude=feet(24000), 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_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(12), cap_min_track_length=nautical_miles(12),
cap_max_track_length=nautical_miles(24), cap_max_track_length=nautical_miles(24),
@ -186,13 +189,14 @@ WWII_DOCTRINE = Doctrine(
hold_distance=nautical_miles(10), hold_distance=nautical_miles(10),
push_distance=nautical_miles(5), push_distance=nautical_miles(5),
join_distance=nautical_miles(5), join_distance=nautical_miles(5),
rendezvous_altitude=feet(10000),
max_ingress_distance=nautical_miles(7), max_ingress_distance=nautical_miles(7),
min_ingress_distance=nautical_miles(5), min_ingress_distance=nautical_miles(5),
ingress_altitude=feet(8000),
min_patrol_altitude=feet(4000), min_patrol_altitude=feet(4000),
max_patrol_altitude=feet(15000), 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_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(8), cap_min_track_length=nautical_miles(8),
cap_max_track_length=nautical_miles(18), 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) @dataclass(frozen=True)
class FuelConsumption: class FuelConsumption:
#: The estimated taxi fuel requirement, in pounds. #: The estimated taxi fuel requirement, in pounds.
@ -182,6 +197,9 @@ class AircraftType(UnitType[Type[FlyingType]]):
patrol_altitude: Optional[Distance] patrol_altitude: Optional[Distance]
patrol_speed: Optional[Speed] 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- #: The maximum range between the origin airfield and the target for which the auto-
#: planner will consider this aircraft usable for a mission. #: planner will consider this aircraft usable for a mission.
max_mission_range: Distance max_mission_range: Distance
@ -245,33 +263,13 @@ class AircraftType(UnitType[Type[FlyingType]]):
def max_speed(self) -> Speed: def max_speed(self) -> Speed:
return kph(self.dcs_unit_type.max_speed) return kph(self.dcs_unit_type.max_speed)
@property @cached_property
def preferred_patrol_altitude(self) -> Distance: def preferred_patrol_altitude(self) -> Distance:
if self.patrol_altitude is not None: if self.patrol_altitude:
return self.patrol_altitude return self.patrol_altitude
else: else:
# Estimate based on max speed. # TODO: somehow make the upper and lower limit configurable
# Aircaft with max speed 600 kph will prefer patrol at 10 000 ft return self.preferred_altitude(10, 33, "patrol")
# 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),
)
def preferred_patrol_speed(self, altitude: Distance) -> Speed: def preferred_patrol_speed(self, altitude: Distance) -> Speed:
"""Preferred true airspeed when patrolling""" """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) 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: def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
from game.radio.radios import ChannelInUseError, kHz from game.radio.radios import ChannelInUseError, kHz
@ -442,6 +480,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
radio_config = RadioConfig.from_data(data.get("radios", {})) radio_config = RadioConfig.from_data(data.get("radios", {}))
patrol_config = PatrolConfig.from_data(data.get("patrol", {})) patrol_config = PatrolConfig.from_data(data.get("patrol", {}))
altitudes_config = AltitudesConfig.from_data(data.get("altitudes", {}))
try: try:
mission_range = nautical_miles(int(data["max_range"])) 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), max_group_size=data.get("max_group_size", aircraft.group_size_max),
patrol_altitude=patrol_config.altitude, patrol_altitude=patrol_config.altitude,
patrol_speed=patrol_config.speed, patrol_speed=patrol_config.speed,
cruise_altitude=altitudes_config.cruise,
combat_altitude=altitudes_config.combat,
max_mission_range=mission_range, max_mission_range=mission_range,
fuel_consumption=fuel_consumption, fuel_consumption=fuel_consumption,
default_livery=data.get("default_livery"), default_livery=data.get("default_livery"),

View File

@ -52,7 +52,7 @@ class Migrator:
continue continue
found = False found = False
for d in doctrines: 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 c.faction.doctrine = d
found = True found = True
break break

View File

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

View File

@ -297,8 +297,18 @@ class Settings:
page=CAMPAIGN_DOCTRINE_PAGE, page=CAMPAIGN_DOCTRINE_PAGE,
section=GENERAL_SECTION, section=GENERAL_SECTION,
default=False, 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: int = bounded_int_option(
"Airbase threat range (nmi)", "Airbase threat range (nmi)",
page=CAMPAIGN_DOCTRINE_PAGE, page=CAMPAIGN_DOCTRINE_PAGE,

View File

@ -86,7 +86,7 @@ class QFlightWaypointList(QTableView):
self.model.setItem(row, 0, QWaypointItem(waypoint, row)) 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 = QStandardItem(f"{altitude}")
altitude_item.setEditable(True) altitude_item.setEditable(True)
self.model.setItem(row, 1, altitude_item) self.model.setItem(row, 1, altitude_item)