mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge branch 'dev' into pr/204
This commit is contained in:
@@ -275,8 +275,6 @@ class Flight(
|
||||
self.fuel = unit_type.fuel_max * 0.5
|
||||
elif unit_type == Hercules:
|
||||
self.fuel = unit_type.fuel_max * 0.75
|
||||
elif self.departure.cptype.name in ["FARP", "FOB"] and not self.is_helo:
|
||||
self.fuel = unit_type.fuel_max * 0.75
|
||||
|
||||
def any_member_has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
|
||||
return any(
|
||||
|
||||
@@ -75,13 +75,6 @@ class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
)
|
||||
return tot - travel_time
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint is self.tot_waypoint:
|
||||
return self.tot
|
||||
elif waypoint is self.layout.ingress:
|
||||
return self.ingress_time
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@@ -89,10 +82,6 @@ class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
def ctld_target_zone_radius(self) -> Distance:
|
||||
return meters(2500)
|
||||
|
||||
@property
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
34
game/ato/flightplans/armedrecon.py
Normal file
34
game/ato/flightplans/armedrecon.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Type
|
||||
|
||||
from .formationattack import (
|
||||
FormationAttackBuilder,
|
||||
FormationAttackFlightPlan,
|
||||
FormationAttackLayout,
|
||||
)
|
||||
from .uizonedisplay import UiZone, UiZoneDisplay
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ...utils import nautical_miles
|
||||
|
||||
|
||||
class ArmedReconFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
def ui_zone(self) -> UiZone:
|
||||
return UiZone(
|
||||
[self.tot_waypoint.position],
|
||||
nautical_miles(
|
||||
self.flight.coalition.game.settings.armed_recon_engagement_range_distance
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[ArmedReconFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
return self._build(FlightWaypointType.INGRESS_ARMED_RECON)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> ArmedReconFlightPlan:
|
||||
return ArmedReconFlightPlan(self.flight, self.layout())
|
||||
@@ -125,16 +125,12 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
||||
ingress_point_shapely.x, ingress_point_shapely.y
|
||||
)
|
||||
|
||||
patrol_start_waypoint = builder.nav(
|
||||
patrol_start, ingress_egress_altitude, use_agl_patrol_altitude
|
||||
)
|
||||
patrol_start_waypoint = builder.cas(patrol_start, ingress_egress_altitude)
|
||||
patrol_start_waypoint.name = "FLOT START"
|
||||
patrol_start_waypoint.pretty_name = "FLOT start"
|
||||
patrol_start_waypoint.description = "FLOT boundary"
|
||||
|
||||
patrol_end_waypoint = builder.nav(
|
||||
patrol_end, ingress_egress_altitude, use_agl_patrol_altitude
|
||||
)
|
||||
patrol_end_waypoint = builder.cas(patrol_end, ingress_egress_altitude)
|
||||
patrol_end_waypoint.name = "FLOT END"
|
||||
patrol_end_waypoint.pretty_name = "FLOT end"
|
||||
patrol_end_waypoint.description = "FLOT boundary"
|
||||
|
||||
@@ -11,6 +11,7 @@ from .formationattack import (
|
||||
)
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from .. import FlightType
|
||||
from ...utils import feet
|
||||
|
||||
|
||||
class EscortFlightPlan(FormationAttackFlightPlan):
|
||||
@@ -34,7 +35,7 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
hold = builder.hold(self._hold_point())
|
||||
|
||||
join_pos = (
|
||||
self.package.waypoints.ingress
|
||||
WaypointBuilder.perturb(self.package.waypoints.ingress, feet(500))
|
||||
if self.flight.is_helo
|
||||
else self.package.waypoints.join
|
||||
)
|
||||
@@ -59,8 +60,6 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
join = builder.join(ascent.position)
|
||||
if layout.pickup and layout.drop_off_ascent:
|
||||
join = builder.join(layout.drop_off_ascent.position)
|
||||
elif layout.pickup:
|
||||
join = builder.join(layout.pickup.position)
|
||||
split = builder.split(layout.arrival.position)
|
||||
if layout.drop_off:
|
||||
initial = builder.escort_hold(
|
||||
|
||||
@@ -105,19 +105,6 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
#
|
||||
# Plus, it's a loiter point so there's no reason to hurry.
|
||||
factor = 0.75
|
||||
elif (
|
||||
self.flight.is_helo
|
||||
and (
|
||||
a.waypoint_type == FlightWaypointType.JOIN
|
||||
or "INGRESS" in a.waypoint_type.name
|
||||
or a.waypoint_type == FlightWaypointType.CUSTOM
|
||||
)
|
||||
and self.package.primary_flight
|
||||
and not self.package.primary_flight.flight_plan.is_airassault
|
||||
):
|
||||
# Helicopter flights should be slowed down between JOIN & INGRESS
|
||||
# to allow the escort to keep up while engaging targets along the way.
|
||||
factor = 0.50
|
||||
# TODO: Adjust if AGL.
|
||||
# We don't have an exact heightmap, but we should probably be performing
|
||||
# *some* adjustment for NTTR since the minimum altitude of the map is
|
||||
@@ -268,7 +255,9 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
def estimate_startup(self) -> timedelta:
|
||||
if self.flight.start_type is StartType.COLD:
|
||||
if self.flight.client_count:
|
||||
return timedelta(minutes=10)
|
||||
return timedelta(
|
||||
minutes=self.flight.coalition.game.settings.player_startup_time
|
||||
)
|
||||
else:
|
||||
# The AI doesn't seem to have a real startup procedure.
|
||||
return timedelta(minutes=2)
|
||||
|
||||
@@ -7,6 +7,7 @@ from .aewc import AewcFlightPlan
|
||||
from .airassault import AirAssaultFlightPlan
|
||||
from .airlift import AirliftFlightPlan
|
||||
from .antiship import AntiShipFlightPlan
|
||||
from .armedrecon import ArmedReconFlightPlan
|
||||
from .bai import BaiFlightPlan
|
||||
from .barcap import BarCapFlightPlan
|
||||
from .cas import CasFlightPlan
|
||||
@@ -62,6 +63,7 @@ class FlightPlanBuilderTypes:
|
||||
FlightType.FERRY: FerryFlightPlan.builder_type(),
|
||||
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
|
||||
FlightType.PRETENSE_CARGO: PretenseCargoFlightPlan.builder_type(),
|
||||
FlightType.ARMED_RECON: ArmedReconFlightPlan.builder_type(),
|
||||
}
|
||||
try:
|
||||
return builder_dict[flight.flight_type]
|
||||
|
||||
@@ -64,8 +64,10 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
return min(speeds)
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
|
||||
if self.package.formation_speed and b in self.package_speed_waypoints:
|
||||
return self.package.formation_speed
|
||||
if (
|
||||
speed := self.package.formation_speed(self.flight.is_helo)
|
||||
) and b in self.package_speed_waypoints:
|
||||
return speed
|
||||
return super().speed_between_waypoints(a, b)
|
||||
|
||||
@property
|
||||
|
||||
@@ -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
|
||||
from game.utils import nautical_miles, Speed, feet
|
||||
from .flightplan import FlightPlan
|
||||
from .formation import FormationFlightPlan, FormationLayout
|
||||
from .ibuilder import IBuilder
|
||||
@@ -39,8 +39,9 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
|
||||
# Should be impossible, as any package with at least one
|
||||
# FormationFlightPlan flight needs a formation speed.
|
||||
assert self.package.formation_speed is not None
|
||||
return self.package.formation_speed
|
||||
speed = self.package.formation_speed(self.flight.is_helo)
|
||||
assert speed is not None
|
||||
return speed
|
||||
return super().speed_between_waypoints(a, b)
|
||||
|
||||
@property
|
||||
@@ -53,7 +54,7 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
"TARGET AREA",
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
self.package.target.position,
|
||||
meters(0),
|
||||
feet(0),
|
||||
"RADIO",
|
||||
)
|
||||
|
||||
@@ -192,6 +193,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
join_pos = self.package.waypoints.join
|
||||
if self.flight.is_helo:
|
||||
join_pos = self.package.waypoints.ingress
|
||||
join_pos = WaypointBuilder.perturb(join_pos, feet(500))
|
||||
join = builder.join(join_pos)
|
||||
split = builder.split(self._get_split())
|
||||
refuel = self._build_refuel(builder)
|
||||
@@ -286,6 +288,8 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
return builder.sead_area(location)
|
||||
elif flight.flight_type == FlightType.OCA_AIRCRAFT:
|
||||
return builder.oca_strike_area(location)
|
||||
elif flight.flight_type == FlightType.ARMED_RECON:
|
||||
return builder.armed_recon_area(location)
|
||||
else:
|
||||
return builder.strike_area(location)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
@@ -252,18 +253,9 @@ class WaypointBuilder:
|
||||
if ingress_type in [
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_OCA_AIRCRAFT,
|
||||
FlightWaypointType.INGRESS_ARMED_RECON,
|
||||
]:
|
||||
weather = self.flight.coalition.game.conditions.weather
|
||||
max_alt = feet(30000)
|
||||
if weather.clouds and (
|
||||
weather.clouds.preset
|
||||
and "overcast" in weather.clouds.preset.description.lower()
|
||||
or weather.clouds.density > 5
|
||||
):
|
||||
max_alt = meters(
|
||||
max(feet(500).meters, weather.clouds.base - feet(500).meters)
|
||||
)
|
||||
alt = min(alt, max_alt)
|
||||
alt = self._adjust_altitude_for_clouds(alt)
|
||||
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or self.flight.is_hercules:
|
||||
@@ -291,6 +283,19 @@ class WaypointBuilder:
|
||||
targets=objective.strike_targets,
|
||||
)
|
||||
|
||||
def _adjust_altitude_for_clouds(self, alt: Distance) -> Distance:
|
||||
weather = self.flight.coalition.game.conditions.weather
|
||||
max_alt = feet(math.inf)
|
||||
if weather.clouds and (
|
||||
weather.clouds.preset
|
||||
and "overcast" in weather.clouds.preset.description.lower()
|
||||
or weather.clouds.density > 5
|
||||
):
|
||||
max_alt = meters(
|
||||
max(feet(500).meters, weather.clouds.base - feet(500).meters)
|
||||
)
|
||||
return min(alt, max_alt)
|
||||
|
||||
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
|
||||
@@ -354,6 +359,21 @@ class WaypointBuilder:
|
||||
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"DEAD on {target.name}", target)
|
||||
|
||||
def armed_recon_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
# Force AI aircraft to fly towards target area
|
||||
alt = self.get_combat_altitude
|
||||
alt = self._adjust_altitude_for_clouds(alt)
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or alt.feet <= AGL_TRANSITION_ALT:
|
||||
alt_type = "RADIO"
|
||||
return self._target_area(
|
||||
f"ARMED RECON {target.name}",
|
||||
target,
|
||||
altitude=alt,
|
||||
alt_type=alt_type,
|
||||
flyover=True,
|
||||
)
|
||||
|
||||
def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
|
||||
|
||||
@@ -398,15 +418,14 @@ class WaypointBuilder:
|
||||
waypoint.only_for_player = True
|
||||
return waypoint
|
||||
|
||||
def cas(self, position: Point) -> FlightWaypoint:
|
||||
def cas(self, position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
weather = self.flight.coalition.game.conditions.weather
|
||||
max_alt = feet(30000)
|
||||
if weather.clouds and (
|
||||
weather.clouds.preset
|
||||
and "overcast" in weather.clouds.preset.description.lower()
|
||||
or weather.clouds.density > 5
|
||||
):
|
||||
max_alt = meters(
|
||||
altitude = meters(
|
||||
max(feet(500).meters, weather.clouds.base - feet(500).meters)
|
||||
)
|
||||
return FlightWaypoint(
|
||||
@@ -415,7 +434,7 @@ class WaypointBuilder:
|
||||
position,
|
||||
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
|
||||
if self.is_helo
|
||||
else min(meters(1000), max_alt),
|
||||
else max(meters(1000), altitude),
|
||||
"RADIO",
|
||||
description="Provide CAS",
|
||||
pretty_name="CAS",
|
||||
@@ -667,14 +686,13 @@ class WaypointBuilder:
|
||||
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)
|
||||
alt = self.get_combat_altitude if self.flight.is_helo else meters(0)
|
||||
|
||||
return FlightWaypoint(
|
||||
"DROPOFFZONE",
|
||||
FlightWaypointType.DROPOFF_ZONE,
|
||||
drop_off.position,
|
||||
altitude,
|
||||
alt,
|
||||
"RADIO",
|
||||
description=f"Drop off cargo at {drop_off.name}",
|
||||
pretty_name="Drop-off zone",
|
||||
@@ -772,8 +790,7 @@ class WaypointBuilder:
|
||||
return previous_threatened and next_threatened
|
||||
|
||||
@staticmethod
|
||||
def perturb(point: Point) -> Point:
|
||||
deviation = nautical_miles(1)
|
||||
def perturb(point: Point, deviation: Distance = nautical_miles(1)) -> Point:
|
||||
x_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
y_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
return point + Vector2(x_adj, y_adj)
|
||||
|
||||
@@ -58,9 +58,8 @@ class FlightType(Enum):
|
||||
FERRY = "Ferry"
|
||||
AIR_ASSAULT = "Air Assault"
|
||||
SEAD_SWEEP = "SEAD Sweep" # Reintroduce legacy "engage-whatever-you-can-find" SEAD
|
||||
PRETENSE_CARGO = (
|
||||
"Cargo Transport" # Flight type for Pretense campaign AI cargo planes
|
||||
)
|
||||
PRETENSE_CARGO = "Cargo Transport" # For Pretense campaign AI cargo planes
|
||||
ARMED_RECON = "Armed Recon"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@@ -96,6 +95,7 @@ class FlightType(Enum):
|
||||
FlightType.SEAD_ESCORT,
|
||||
FlightType.AIR_ASSAULT,
|
||||
FlightType.SEAD_SWEEP,
|
||||
FlightType.ARMED_RECON,
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -107,6 +107,7 @@ class FlightType(Enum):
|
||||
return {
|
||||
FlightType.AEWC: AirEntity.AIRBORNE_EARLY_WARNING,
|
||||
FlightType.ANTISHIP: AirEntity.ANTISURFACE_WARFARE,
|
||||
FlightType.ARMED_RECON: AirEntity.ATTACK_STRIKE,
|
||||
FlightType.BAI: AirEntity.ATTACK_STRIKE,
|
||||
FlightType.BARCAP: AirEntity.FIGHTER,
|
||||
FlightType.CAS: AirEntity.ATTACK_STRIKE,
|
||||
|
||||
@@ -51,3 +51,4 @@ class FlightWaypointType(IntEnum):
|
||||
INGRESS_AIR_ASSAULT = 31
|
||||
INGRESS_ANTI_SHIP = 32
|
||||
INGRESS_SEAD_SWEEP = 33
|
||||
INGRESS_ARMED_RECON = 34
|
||||
|
||||
@@ -208,6 +208,7 @@ class Loadout:
|
||||
loadout_names[FlightType.INTERCEPTION].extend(loadout_names[FlightType.BARCAP])
|
||||
# OCA/Aircraft falls back to BAI, which falls back to CAS.
|
||||
loadout_names[FlightType.BAI].extend(loadout_names[FlightType.CAS])
|
||||
loadout_names[FlightType.ARMED_RECON].extend(loadout_names[FlightType.CAS])
|
||||
loadout_names[FlightType.OCA_AIRCRAFT].extend(loadout_names[FlightType.BAI])
|
||||
# DEAD also falls back to BAI.
|
||||
loadout_names[FlightType.DEAD].extend(loadout_names[FlightType.BAI])
|
||||
|
||||
@@ -54,8 +54,7 @@ class Package(RadioFrequencyContainer):
|
||||
def has_players(self) -> bool:
|
||||
return any(flight.client_count for flight in self.flights)
|
||||
|
||||
@property
|
||||
def formation_speed(self) -> Optional[Speed]:
|
||||
def formation_speed(self, is_helo: bool) -> Optional[Speed]:
|
||||
"""The speed of the package when in formation.
|
||||
|
||||
If none of the flights in the package will join a formation, this
|
||||
@@ -66,7 +65,10 @@ class Package(RadioFrequencyContainer):
|
||||
"""
|
||||
speeds = []
|
||||
for flight in self.flights:
|
||||
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||
if (
|
||||
isinstance(flight.flight_plan, FormationFlightPlan)
|
||||
and flight.is_helo == is_helo
|
||||
):
|
||||
speeds.append(flight.flight_plan.best_flight_formation_speed)
|
||||
if not speeds:
|
||||
return None
|
||||
@@ -183,6 +185,7 @@ class Package(RadioFrequencyContainer):
|
||||
FlightType.SEAD_SWEEP,
|
||||
FlightType.TARCAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.ARMED_RECON,
|
||||
FlightType.AEWC,
|
||||
FlightType.FERRY,
|
||||
FlightType.REFUELING,
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.utils import Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, mach, meters
|
||||
from game.utils import Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, mach
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .flight import Flight
|
||||
@@ -30,8 +30,8 @@ class GroundSpeed:
|
||||
# as it can at sea level. This probably isn't great assumption, but
|
||||
# might. be sufficient given the wiggle room. We can come up with
|
||||
# another heuristic if needed.
|
||||
cruise_mach = max_speed.mach() * (0.60 if flight.is_helo else 0.85)
|
||||
return mach(cruise_mach, altitude if not flight.is_helo else meters(0))
|
||||
cruise_mach = max_speed.mach() * (0.7 if flight.is_helo else 0.85)
|
||||
return mach(cruise_mach, altitude)
|
||||
|
||||
|
||||
# TODO: Most if not all of this should move into FlightPlan.
|
||||
|
||||
Reference in New Issue
Block a user