mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Initial implementation of AEW&C missions.
Still a work in progress (the missions don't actually perform their task, just orbit). Currently: * AEW&C aircraft can be bought. * AEW&C missions can be planned at any control point and at front lines. * AEW&C will return after 4H or Bingo.
This commit is contained in:
committed by
Simon Clark
parent
4a0ccc4c2f
commit
e0501e46e3
@@ -12,8 +12,8 @@ from dcs.helicopters import (
|
||||
OH_58D,
|
||||
SA342L,
|
||||
SA342M,
|
||||
SH_60B,
|
||||
UH_1H,
|
||||
SH_60B
|
||||
)
|
||||
from dcs.planes import (
|
||||
AJS37,
|
||||
@@ -22,11 +22,14 @@ from dcs.planes import (
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
A_20G,
|
||||
A_50,
|
||||
B_17G,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Bf_109K_4,
|
||||
C_101CC,
|
||||
E_2C,
|
||||
E_3A,
|
||||
FA_18C_hornet,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
@@ -40,9 +43,11 @@ from dcs.planes import (
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
I_16,
|
||||
JF_17,
|
||||
J_11A,
|
||||
Ju_88A4,
|
||||
KJ_2000,
|
||||
L_39ZA,
|
||||
MQ_9_Reaper,
|
||||
M_2000C,
|
||||
@@ -83,18 +88,16 @@ from dcs.planes import (
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
WingLoong_I,
|
||||
I_16
|
||||
)
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from pydcs_extensions.f22a.f22a import F_22A
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_B, Rafale_M
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
|
||||
# All aircraft lists are in priority order. Aircraft higher in the list will be
|
||||
# preferred over those lower in the list.
|
||||
@@ -155,8 +158,8 @@ CAP_CAPABLE = [
|
||||
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
|
||||
CAS_CAPABLE = [
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
B_1B,
|
||||
A_10C,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
Su_25TM,
|
||||
@@ -373,6 +376,13 @@ DRONES = [
|
||||
WingLoong_I
|
||||
]
|
||||
|
||||
AEWC_CAPABLE = [
|
||||
E_3A,
|
||||
E_2C,
|
||||
A_50,
|
||||
KJ_2000,
|
||||
]
|
||||
|
||||
|
||||
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
@@ -396,6 +406,8 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
return STRIKE_CAPABLE
|
||||
elif task == FlightType.ESCORT:
|
||||
return CAP_CAPABLE
|
||||
elif task == FlightType.AEWC:
|
||||
return AEWC_CAPABLE
|
||||
else:
|
||||
logging.error(f"Unplannable flight type: {task}")
|
||||
return []
|
||||
|
||||
@@ -20,6 +20,14 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class FlightType(Enum):
|
||||
"""Enumeration of mission types.
|
||||
|
||||
The value of each enumeration is the name that will be shown in the UI.
|
||||
|
||||
These values are persisted to the save game as well since they are a part of
|
||||
each flight and thus a part of the ATO, so changing these values will break
|
||||
save compat.
|
||||
"""
|
||||
TARCAP = "TARCAP"
|
||||
BARCAP = "BARCAP"
|
||||
CAS = "CAS"
|
||||
@@ -33,6 +41,7 @@ class FlightType(Enum):
|
||||
SWEEP = "Fighter sweep"
|
||||
OCA_RUNWAY = "OCA/Runway"
|
||||
OCA_AIRCRAFT = "OCA/Aircraft"
|
||||
AEWC = "AEW&C"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
@@ -15,6 +15,13 @@ from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
|
||||
|
||||
from dcs.planes import (
|
||||
E_3A,
|
||||
E_2C,
|
||||
A_50,
|
||||
KJ_2000
|
||||
)
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from shapely.geometry import Point as ShapelyPoint
|
||||
@@ -29,7 +36,7 @@ from game.theater import (
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.theater.theatergroundobject import EwrGroundObject
|
||||
from game.utils import Distance, Speed, meters, nautical_miles
|
||||
from game.utils import Distance, Speed, feet, meters, nautical_miles
|
||||
from .closestairfields import ObjectiveDistanceCache
|
||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||
from .traveltime import GroundSpeed, TravelTime
|
||||
@@ -121,7 +128,7 @@ class FlightPlan:
|
||||
failed to generate. Nevertheless, we have to defend against it.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@cached_property
|
||||
def bingo_fuel(self) -> int:
|
||||
"""Bingo fuel value for the FlightPlan
|
||||
@@ -145,7 +152,7 @@ class FlightPlan:
|
||||
"""Joker fuel value for the FlightPlan
|
||||
"""
|
||||
return self.bingo_fuel + 1000
|
||||
|
||||
|
||||
def max_distance_from(self, cp: ControlPoint) -> Distance:
|
||||
"""Returns the farthest waypoint of the flight plan from a ControlPoint.
|
||||
:arg cp The ControlPoint to measure distance from.
|
||||
@@ -280,11 +287,11 @@ class LoiterFlightPlan(FlightPlan):
|
||||
travel_time = super().travel_time_between_waypoints(a, b)
|
||||
if a != self.hold:
|
||||
return travel_time
|
||||
try:
|
||||
return travel_time + self.hold_duration
|
||||
except AttributeError:
|
||||
# Save compat for 2.3.
|
||||
return travel_time + timedelta(minutes=5)
|
||||
return travel_time + self.hold_duration
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -542,10 +549,10 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
@property
|
||||
def package_speed_waypoints(self) -> Set[FlightWaypoint]:
|
||||
return {
|
||||
self.ingress,
|
||||
self.egress,
|
||||
self.split,
|
||||
} | set(self.targets)
|
||||
self.ingress,
|
||||
self.egress,
|
||||
self.split,
|
||||
} | set(self.targets)
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint,
|
||||
b: FlightWaypoint) -> Speed:
|
||||
@@ -696,6 +703,41 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
return self.sweep_end_time
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AwacsFlightPlan(LoiterFlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
nav_to: List[FlightWaypoint]
|
||||
nav_from: List[FlightWaypoint]
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
yield from self.nav_to
|
||||
yield self.hold
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||
if waypoint == self.hold:
|
||||
return self.package.time_over_target
|
||||
return None
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> Optional[FlightWaypoint]:
|
||||
return self.hold
|
||||
|
||||
@property
|
||||
def push_time(self) -> timedelta:
|
||||
return self.package.time_over_target + self.hold_duration
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.push_time
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CustomFlightPlan(FlightPlan):
|
||||
custom_waypoints: List[FlightWaypoint]
|
||||
@@ -791,6 +833,8 @@ class FlightPlanBuilder:
|
||||
return self.generate_sweep(flight)
|
||||
elif task == FlightType.TARCAP:
|
||||
return self.generate_tarcap(flight)
|
||||
elif task == FlightType.AEWC:
|
||||
return self.generate_aewc(flight)
|
||||
raise PlanningError(
|
||||
f"{task} flight plan generation not implemented")
|
||||
|
||||
@@ -921,6 +965,45 @@ class FlightPlanBuilder:
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
targets)
|
||||
|
||||
def generate_aewc(self, flight: Flight) -> AwacsFlightPlan:
|
||||
"""Generate a AWACS flight at a given location.
|
||||
|
||||
Args:
|
||||
flight: The flight to generate the flight plan for.
|
||||
"""
|
||||
location = self.package.target
|
||||
|
||||
start = self.aewc_orbit(location)
|
||||
|
||||
# As high as possible to maximize detection and on-station time.
|
||||
if flight.unit_type == E_2C:
|
||||
patrol_alt = feet(30000)
|
||||
elif flight.unit_type == E_3A:
|
||||
patrol_alt = feet(35000)
|
||||
elif flight.unit_type == A_50:
|
||||
patrol_alt = feet(33000)
|
||||
elif flight.unit_type == KJ_2000:
|
||||
patrol_alt = feet(40000)
|
||||
else:
|
||||
patrol_alt = feet(25000)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
start = builder.orbit(start, patrol_alt)
|
||||
|
||||
return AwacsFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(flight.departure.position, start.position,
|
||||
patrol_alt),
|
||||
nav_from=builder.nav_path(start.position, flight.arrival.position,
|
||||
patrol_alt),
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
hold=start,
|
||||
hold_duration=timedelta(hours=4),
|
||||
)
|
||||
|
||||
def generate_bai(self, flight: Flight) -> StrikeFlightPlan:
|
||||
"""Generates a BAI flight plan.
|
||||
|
||||
@@ -1095,6 +1178,18 @@ class FlightPlanBuilder:
|
||||
start = end.point_from_heading(heading - 180, diameter)
|
||||
return start, end
|
||||
|
||||
@staticmethod
|
||||
def aewc_orbit(location: MissionTarget) -> Point:
|
||||
closest_airfield = location
|
||||
# TODO: This is a heading to itself.
|
||||
# Place this either over the target or as close as possible outside the
|
||||
# threat zone: https://github.com/Khopa/dcs_liberation/issues/842.
|
||||
heading = location.position.heading_between_point(closest_airfield.position)
|
||||
return location.position.point_from_heading(
|
||||
heading,
|
||||
5000
|
||||
)
|
||||
|
||||
def racetrack_for_frontline(self, origin: Point,
|
||||
front_line: FrontLine) -> Tuple[Point, Point]:
|
||||
ally_cp, enemy_cp = front_line.control_points
|
||||
|
||||
@@ -366,6 +366,26 @@ class WaypointBuilder:
|
||||
return (self.race_track_start(start, altitude),
|
||||
self.race_track_end(end, altitude))
|
||||
|
||||
@staticmethod
|
||||
def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates an circular orbit point.
|
||||
|
||||
Args:
|
||||
start: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.LOITER,
|
||||
start.x,
|
||||
start.y,
|
||||
altitude
|
||||
)
|
||||
waypoint.name = "ORBIT"
|
||||
waypoint.description = "Anchor and hold at this point"
|
||||
waypoint.pretty_name = "Orbit"
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a sweep start waypoint.
|
||||
|
||||
Reference in New Issue
Block a user