Add fighter sweep tasks.

Fighter sweeps arrive at the target ahead of the rest of the package
(currently a fixed 5 minute lead) to clear out enemy fighters and then
RTB.

Fixes https://github.com/Khopa/dcs_liberation/issues/348
This commit is contained in:
Dan Albert 2020-11-15 23:12:38 -08:00
parent e60166dc89
commit d369ce8847
9 changed files with 237 additions and 21 deletions

View File

@ -1,3 +1,8 @@
# 2.3.0
# Features/Improvements
* **[Flight Planner]** Added fighter sweep missions.
# 2.2.1 # 2.2.1
# Features/Improvements # Features/Improvements

View File

@ -36,6 +36,8 @@ class Doctrine:
cas_duration: timedelta cas_duration: timedelta
sweep_distance: int
MODERN_DOCTRINE = Doctrine( MODERN_DOCTRINE = Doctrine(
cap=True, cap=True,
@ -62,6 +64,7 @@ MODERN_DOCTRINE = Doctrine(
cap_min_distance_from_cp=nm_to_meter(10), cap_min_distance_from_cp=nm_to_meter(10),
cap_max_distance_from_cp=nm_to_meter(40), cap_max_distance_from_cp=nm_to_meter(40),
cas_duration=timedelta(minutes=30), cas_duration=timedelta(minutes=30),
sweep_distance=nm_to_meter(60),
) )
COLDWAR_DOCTRINE = Doctrine( COLDWAR_DOCTRINE = Doctrine(
@ -89,6 +92,7 @@ COLDWAR_DOCTRINE = Doctrine(
cap_min_distance_from_cp=nm_to_meter(8), cap_min_distance_from_cp=nm_to_meter(8),
cap_max_distance_from_cp=nm_to_meter(25), cap_max_distance_from_cp=nm_to_meter(25),
cas_duration=timedelta(minutes=30), cas_duration=timedelta(minutes=30),
sweep_distance=nm_to_meter(40),
) )
WWII_DOCTRINE = Doctrine( WWII_DOCTRINE = Doctrine(
@ -116,4 +120,5 @@ WWII_DOCTRINE = Doctrine(
cap_min_distance_from_cp=nm_to_meter(0), cap_min_distance_from_cp=nm_to_meter(0),
cap_max_distance_from_cp=nm_to_meter(5), cap_max_distance_from_cp=nm_to_meter(5),
cas_duration=timedelta(minutes=30), cas_duration=timedelta(minutes=30),
sweep_distance=nm_to_meter(10),
) )

View File

@ -5,7 +5,7 @@ import random
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from functools import cached_property from functools import cached_property
from typing import Dict, List, Optional, Type, Union, TYPE_CHECKING from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union
from dcs import helicopters from dcs import helicopters
from dcs.action import AITaskPush, ActivateGroup from dcs.action import AITaskPush, ActivateGroup
@ -13,10 +13,12 @@ from dcs.condition import CoalitionHasAirdrome, TimeAfter
from dcs.country import Country from dcs.country import Country
from dcs.flyingunit import FlyingUnit from dcs.flyingunit import FlyingUnit
from dcs.helicopters import UH_1H, helicopter_map from dcs.helicopters import UH_1H, helicopter_map
from dcs.mapping import Point
from dcs.mission import Mission, StartType from dcs.mission import Mission, StartType
from dcs.planes import ( from dcs.planes import (
AJS37, AJS37,
B_17G, B_17G,
B_52H,
Bf_109K_4, Bf_109K_4,
FW_190A8, FW_190A8,
FW_190D9, FW_190D9,
@ -31,7 +33,8 @@ from dcs.planes import (
P_51D_30_NA, P_51D_30_NA,
SpitfireLFMkIX, SpitfireLFMkIX,
SpitfireLFMkIXCW, SpitfireLFMkIXCW,
Su_33, A_20G, Tu_22M3, B_52H, Su_33,
Tu_22M3,
) )
from dcs.point import MovingPoint, PointAction from dcs.point import MovingPoint, PointAction
from dcs.task import ( from dcs.task import (
@ -49,10 +52,8 @@ from dcs.task import (
OptRTBOnBingoFuel, OptRTBOnBingoFuel,
OptRTBOnOutOfAmmo, OptRTBOnOutOfAmmo,
OptReactOnThreat, OptReactOnThreat,
OptRestrictAfterburner,
OptRestrictJettison, OptRestrictJettison,
OrbitAction, OrbitAction,
PinpointStrike,
SEAD, SEAD,
StartCommand, StartCommand,
Targets, Targets,
@ -71,6 +72,7 @@ from game.utils import nm_to_meter
from gen.airsupportgen import AirSupport from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit from gen.callsigns import create_group_callsign_from_unit
from gen.conflictgen import FRONTLINE_LENGTH
from gen.flights.flight import ( from gen.flights.flight import (
Flight, Flight,
FlightType, FlightType,
@ -79,15 +81,14 @@ from gen.flights.flight import (
) )
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData from gen.runways import RunwayData
from gen.conflictgen import FRONTLINE_LENGTH
from dcs.mapping import Point
from theater import TheaterGroundObject from theater import TheaterGroundObject
from theater.controlpoint import ControlPoint, ControlPointType from theater.controlpoint import ControlPoint, ControlPointType
from .conflictgen import Conflict from .conflictgen import Conflict
from .flights.flightplan import ( from .flights.flightplan import (
CasFlightPlan, CasFlightPlan,
FormationFlightPlan, LoiterFlightPlan,
PatrollingFlightPlan, PatrollingFlightPlan,
SweepFlightPlan,
) )
from .flights.traveltime import TotEstimator from .flights.traveltime import TotEstimator
from .naming import namegen from .naming import namegen
@ -1035,9 +1036,6 @@ class AircraftConflictGenerator:
self.configure_behavior(group, rtb_winchester=ammo_type) self.configure_behavior(group, rtb_winchester=ammo_type)
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50),
targets=[Targets.All.Air]))
def configure_cas(self, group: FlyingGroup, package: Package, def configure_cas(self, group: FlyingGroup, package: Package,
flight: Flight, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
@ -1118,7 +1116,7 @@ class AircraftConflictGenerator:
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
flight_type = flight.flight_type flight_type = flight.flight_type
if flight_type in [FlightType.BARCAP, FlightType.TARCAP, if flight_type in [FlightType.BARCAP, FlightType.TARCAP,
FlightType.INTERCEPTION]: FlightType.INTERCEPTION, FlightType.SWEEP]:
self.configure_cap(group, package, flight, dynamic_runways) self.configure_cap(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.CAS, FlightType.BAI]: elif flight_type in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, package, flight, dynamic_runways) self.configure_cas(group, package, flight, dynamic_runways)
@ -1278,6 +1276,7 @@ class PydcsWaypointBuilder:
FlightWaypointType.LANDING_POINT: LandingPointBuilder, FlightWaypointType.LANDING_POINT: LandingPointBuilder,
FlightWaypointType.LOITER: HoldPointBuilder, FlightWaypointType.LOITER: HoldPointBuilder,
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder, FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
FlightWaypointType.INGRESS_SWEEP: SweepIngressBuilder,
} }
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder) builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
return builder(waypoint, group, package, flight, mission) return builder(waypoint, group, package, flight, mission)
@ -1314,7 +1313,7 @@ class HoldPointBuilder(PydcsWaypointBuilder):
altitude=waypoint.alt, altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.Circle pattern=OrbitAction.OrbitPattern.Circle
)) ))
if not isinstance(self.flight.flight_plan, FormationFlightPlan): if not isinstance(self.flight.flight_plan, LoiterFlightPlan):
flight_plan_type = self.flight.flight_plan.__class__.__name__ flight_plan_type = self.flight.flight_plan.__class__.__name__
logging.error( logging.error(
f"Cannot configure hold for for {self.flight} because " f"Cannot configure hold for for {self.flight} because "
@ -1458,6 +1457,23 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
return waypoint return waypoint
class SweepIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
if not isinstance(self.flight.flight_plan, SweepFlightPlan):
flight_plan_type = self.flight.flight_plan.__class__.__name__
logging.error(
f"Cannot create sweep for {self.flight} because "
f"{flight_plan_type} is not a sweep flight plan.")
return waypoint
waypoint.tasks.append(EngageTargets(max_distance=nm_to_meter(50),
targets=[Targets.All.Air]))
return waypoint
class JoinPointBuilder(PydcsWaypointBuilder): class JoinPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
@ -1532,4 +1548,14 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
racetrack.stop_after_time( racetrack.stop_after_time(
int(self.flight.flight_plan.patrol_end_time.total_seconds())) int(self.flight.flight_plan.patrol_end_time.total_seconds()))
waypoint.add_task(racetrack) waypoint.add_task(racetrack)
# TODO: Move the properties of this task into the flight plan?
# CAP is the only current user of this so it's not a big deal, but might
# be good to make this usable for things like BAI when we add that
# later.
cap_types = {FlightType.BARCAP, FlightType.TARCAP}
if self.flight.flight_type in cap_types:
waypoint.tasks.append(EngageTargets(max_distance=nm_to_meter(50),
targets=[Targets.All.Air]))
return waypoint return waypoint

View File

@ -159,6 +159,7 @@ class Package:
FlightType.TARCAP, FlightType.TARCAP,
FlightType.CAP, FlightType.CAP,
FlightType.BARCAP, FlightType.BARCAP,
FlightType.SWEEP,
FlightType.EWAR, FlightType.EWAR,
FlightType.ESCORT, FlightType.ESCORT,
] ]

View File

@ -38,6 +38,8 @@ class FlightType(Enum):
RECON = 15 RECON = 15
EWAR = 16 EWAR = 16
SWEEP = 17
class FlightWaypointType(Enum): class FlightWaypointType(Enum):
TAKEOFF = 0 # Take off point TAKEOFF = 0 # Take off point
@ -61,6 +63,7 @@ class FlightWaypointType(Enum):
LOITER = 18 LOITER = 18
INGRESS_ESCORT = 19 INGRESS_ESCORT = 19
INGRESS_DEAD = 20 INGRESS_DEAD = 20
INGRESS_SWEEP = 21
class FlightWaypoint: class FlightWaypoint:

View File

@ -105,6 +105,15 @@ class FlightPlan:
""" """
raise NotImplementedError raise NotImplementedError
@property
def tot_offset(self) -> timedelta:
"""This flight's offset from the package's TOT.
Positive values represent later TOTs. An offset of -2 minutes is used
for a flight that has a TOT 2 minutes before the rest of the package.
"""
return timedelta()
# Not cached because changes to the package might alter the formation speed. # Not cached because changes to the package might alter the formation speed.
@property @property
def travel_time_to_target(self) -> Optional[timedelta]: def travel_time_to_target(self) -> Optional[timedelta]:
@ -147,8 +156,33 @@ class FlightPlan:
@dataclass(frozen=True) @dataclass(frozen=True)
class FormationFlightPlan(FlightPlan): class LoiterFlightPlan(FlightPlan):
hold: FlightWaypoint hold: FlightWaypoint
@property
def waypoints(self) -> List[FlightWaypoint]:
raise NotImplementedError
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
raise NotImplementedError
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
raise NotImplementedError
@property
def push_time(self) -> timedelta:
raise NotImplementedError
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.push_time
return None
@dataclass(frozen=True)
class FormationFlightPlan(LoiterFlightPlan):
join: FlightWaypoint join: FlightWaypoint
split: FlightWaypoint split: FlightWaypoint
@ -215,12 +249,6 @@ class FormationFlightPlan(FlightPlan):
return self.split_time return self.split_time
return None return None
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.push_time
return None
@property @property
def push_time(self) -> timedelta: def push_time(self) -> timedelta:
return self.join_time - TravelTime.between_points( return self.join_time - TravelTime.between_points(
@ -461,6 +489,64 @@ class StrikeFlightPlan(FormationFlightPlan):
return super().tot_for_waypoint(waypoint) return super().tot_for_waypoint(waypoint)
@dataclass(frozen=True)
class SweepFlightPlan(LoiterFlightPlan):
takeoff: FlightWaypoint
sweep_start: FlightWaypoint
sweep_end: FlightWaypoint
land: FlightWaypoint
lead_time: timedelta
@property
def waypoints(self) -> List[FlightWaypoint]:
return [
self.takeoff,
self.hold,
self.sweep_start,
self.sweep_end,
self.land,
]
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
return self.sweep_end
@property
def tot_offset(self) -> timedelta:
return -self.lead_time
@property
def sweep_start_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(
self.sweep_start, self.sweep_end)
return self.sweep_end_time - travel_time
@property
def sweep_end_time(self) -> timedelta:
return self.package.time_over_target + self.tot_offset
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.sweep_start:
return self.sweep_start_time
if waypoint == self.sweep_end:
return self.sweep_end_time
return None
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.push_time
return None
@property
def push_time(self) -> timedelta:
return self.sweep_end_time - TravelTime.between_points(
self.hold.position,
self.sweep_end.position,
GroundSpeed.for_flight(self.flight, self.hold.alt)
)
@dataclass(frozen=True) @dataclass(frozen=True)
class CustomFlightPlan(FlightPlan): class CustomFlightPlan(FlightPlan):
custom_waypoints: List[FlightWaypoint] custom_waypoints: List[FlightWaypoint]
@ -546,6 +632,8 @@ class FlightPlanBuilder:
return self.generate_sead(flight, custom_targets) return self.generate_sead(flight, custom_targets)
elif task == FlightType.STRIKE: elif task == FlightType.STRIKE:
return self.generate_strike(flight) return self.generate_strike(flight)
elif task == FlightType.SWEEP:
return self.generate_sweep(flight)
elif task == FlightType.TARCAP: elif task == FlightType.TARCAP:
return self.generate_frontline_cap(flight) return self.generate_frontline_cap(flight)
elif task == FlightType.TROOP_TRANSPORT: elif task == FlightType.TROOP_TRANSPORT:
@ -671,6 +759,35 @@ class FlightPlanBuilder:
land=land land=land
) )
def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
"""Generate a BARCAP flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
target = self.package.target.position
heading = self._heading_to_package_airfield(target)
start = target.point_from_heading(heading,
-self.doctrine.sweep_distance)
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
descent, land = builder.rtb(flight.from_cp)
start, end = builder.sweep(start, target,
self.doctrine.ingress_altitude)
return SweepFlightPlan(
package=self.package,
flight=flight,
lead_time=timedelta(minutes=5),
takeoff=builder.takeoff(flight.from_cp),
hold=builder.hold(self._hold_point(flight)),
sweep_start=start,
sweep_end=end,
land=land
)
def generate_frontline_cap(self, flight: Flight) -> FrontLineCapFlightPlan: def generate_frontline_cap(self, flight: Flight) -> FrontLineCapFlightPlan:
"""Generate a CAP flight plan for the given front line. """Generate a CAP flight plan for the given front line.

View File

@ -128,7 +128,11 @@ class TotEstimator:
f"time for {flight} will be immediate.") f"time for {flight} will be immediate.")
return timedelta() return timedelta()
else: else:
tot = self.package.time_over_target tot_waypoint = flight.flight_plan.tot_waypoint
if tot_waypoint is None:
tot = self.package.time_over_target
else:
tot = flight.flight_plan.tot_for_waypoint(tot_waypoint)
return tot - travel_time - self.HOLD_TIME return tot - travel_time - self.HOLD_TIME
def earliest_tot(self) -> timedelta: def earliest_tot(self) -> timedelta:
@ -165,9 +169,13 @@ class TotEstimator:
# Return 0 so this flight's travel time does not affect the rest # Return 0 so this flight's travel time does not affect the rest
# of the package. # of the package.
return timedelta() return timedelta()
# Account for TOT offsets for the flight plan. An offset of -2 minutes
# means the flight's TOT is 2 minutes ahead of the package's so it needs
# an extra two minutes.
offset = -flight.flight_plan.tot_offset
startup = self.estimate_startup(flight) startup = self.estimate_startup(flight)
ground_ops = self.estimate_ground_ops(flight) ground_ops = self.estimate_ground_ops(flight)
return startup + ground_ops + time_to_target return startup + ground_ops + time_to_target + offset
@staticmethod @staticmethod
def estimate_startup(flight: Flight) -> timedelta: def estimate_startup(flight: Flight) -> timedelta:

View File

@ -326,6 +326,56 @@ class WaypointBuilder:
return (self.race_track_start(start, altitude), return (self.race_track_start(start, altitude),
self.race_track_end(end, altitude)) self.race_track_end(end, altitude))
@staticmethod
def sweep_start(position: Point, altitude: int) -> FlightWaypoint:
"""Creates a sweep start waypoint.
Args:
position: Position of the waypoint.
altitude: Altitude of the sweep in meters.
"""
waypoint = FlightWaypoint(
FlightWaypointType.INGRESS_SWEEP,
position.x,
position.y,
altitude
)
waypoint.name = "SWEEP START"
waypoint.description = "Proceed to the target and engage enemy aircraft"
waypoint.pretty_name = "Sweep start"
return waypoint
@staticmethod
def sweep_end(position: Point, altitude: int) -> FlightWaypoint:
"""Creates a sweep end waypoint.
Args:
position: Position of the waypoint.
altitude: Altitude of the sweep in meters.
"""
waypoint = FlightWaypoint(
FlightWaypointType.EGRESS,
position.x,
position.y,
altitude
)
waypoint.name = "SWEEP END"
waypoint.description = "End of sweep"
waypoint.pretty_name = "Sweep end"
return waypoint
def sweep(self, start: Point, end: Point,
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a racetrack orbit.
Args:
start: The beginning of the sweep.
end: The end of the sweep.
altitude: The sweep altitude.
"""
return (self.sweep_start(start, altitude),
self.sweep_end(end, altitude))
def rtb(self, def rtb(self,
arrival: ControlPoint) -> Tuple[FlightWaypoint, FlightWaypoint]: arrival: ControlPoint) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates descent ant landing waypoints for the given control point. """Creates descent ant landing waypoints for the given control point.

View File

@ -21,6 +21,7 @@ class QFlightTypeComboBox(QComboBox):
FlightType.ESCORT, FlightType.ESCORT,
FlightType.SEAD, FlightType.SEAD,
FlightType.DEAD, FlightType.DEAD,
FlightType.SWEEP,
# TODO: FlightType.ELINT, # TODO: FlightType.ELINT,
# TODO: FlightType.EWAR, # TODO: FlightType.EWAR,
# TODO: FlightType.RECON, # TODO: FlightType.RECON,