diff --git a/changelog.md b/changelog.md index 17465710..f12e9098 100644 --- a/changelog.md +++ b/changelog.md @@ -47,6 +47,7 @@ * **[Options]** Renamed Maximum frontline length -> Maximum frontline width. * **[Squadrons]** Add livery selector in Squadron Dialog, allowing you to change the livery during the campaign. * **[New Game Wizard]** Automatically invert factions when 'Invert Map' is selected. +* **[Flight Plans]** Added "SEAD Sweep" flight plan, which basically reintroduces the legacy "SEAD Escort" flight plan where the flight will engage whatever it can find without actually escorting the primary flight. ## Fixes diff --git a/game/ato/flightplans/flightplanbuildertypes.py b/game/ato/flightplans/flightplanbuildertypes.py index cbe9a9e6..5047e45c 100644 --- a/game/ato/flightplans/flightplanbuildertypes.py +++ b/game/ato/flightplans/flightplanbuildertypes.py @@ -19,6 +19,7 @@ from .ocarunway import OcaRunwayFlightPlan from .packagerefueling import PackageRefuelingFlightPlan from .planningerror import PlanningError from .sead import SeadFlightPlan +from .seadsweep import SeadSweepFlightPlan from .strike import StrikeFlightPlan from .sweep import SweepFlightPlan from .tarcap import TarCapFlightPlan @@ -51,6 +52,7 @@ class FlightPlanBuilderTypes: FlightType.OCA_RUNWAY: OcaRunwayFlightPlan.builder_type(), FlightType.SEAD: SeadFlightPlan.builder_type(), FlightType.SEAD_ESCORT: EscortFlightPlan.builder_type(), + FlightType.SEAD_SWEEP: SeadSweepFlightPlan.builder_type(), FlightType.STRIKE: StrikeFlightPlan.builder_type(), FlightType.SWEEP: SweepFlightPlan.builder_type(), FlightType.TARCAP: TarCapFlightPlan.builder_type(), diff --git a/game/ato/flightplans/formationattack.py b/game/ato/flightplans/formationattack.py index c37fbd9a..b55eee26 100644 --- a/game/ato/flightplans/formationattack.py +++ b/game/ato/flightplans/formationattack.py @@ -182,6 +182,8 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC): initial = None if ingress_type == FlightWaypointType.INGRESS_SEAD: initial = builder.sead_search(self.package.target) + elif ingress_type == FlightWaypointType.INGRESS_SEAD_SWEEP: + initial = builder.sead_sweep(self.package.target) return FormationAttackLayout( departure=builder.takeoff(self.flight.departure), @@ -213,7 +215,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC): return builder.bai_group(target) elif flight.flight_type == FlightType.DEAD: return builder.dead_point(target) - elif flight.flight_type == FlightType.SEAD: + elif flight.flight_type in {FlightType.SEAD, FlightType.SEAD_SWEEP}: return builder.sead_point(target) else: return builder.strike_point(target) diff --git a/game/ato/flightplans/seadsweep.py b/game/ato/flightplans/seadsweep.py new file mode 100644 index 00000000..d5dcab95 --- /dev/null +++ b/game/ato/flightplans/seadsweep.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from datetime import timedelta +from typing import Type + +from .formationattack import ( + FormationAttackBuilder, + FormationAttackFlightPlan, + FormationAttackLayout, +) +from ..flightwaypointtype import FlightWaypointType + + +class SeadSweepFlightPlan(FormationAttackFlightPlan): + @staticmethod + def builder_type() -> Type[Builder]: + return Builder + + def default_tot_offset(self) -> timedelta: + return -timedelta(minutes=2) + + +class Builder(FormationAttackBuilder[SeadSweepFlightPlan, FormationAttackLayout]): + def layout(self) -> FormationAttackLayout: + return self._build(FlightWaypointType.INGRESS_SEAD_SWEEP) + + def build(self) -> SeadSweepFlightPlan: + return SeadSweepFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/waypointbuilder.py b/game/ato/flightplans/waypointbuilder.py index 2a55f7d0..bea1a294 100644 --- a/game/ato/flightplans/waypointbuilder.py +++ b/game/ato/flightplans/waypointbuilder.py @@ -423,6 +423,32 @@ class WaypointBuilder: ) def sead_search(self, target: MissionTarget) -> FlightWaypoint: + hold = self._sead_search_point(target) + + return FlightWaypoint( + "SEAD Search", + FlightWaypointType.NAV, + hold, + self.doctrine.ingress_altitude, + alt_type="BARO", + description="Anchor and search from this point", + pretty_name="SEAD Search", + ) + + def sead_sweep(self, target: MissionTarget) -> FlightWaypoint: + hold = self._sead_search_point(target) + + return FlightWaypoint( + "SEAD Sweep", + FlightWaypointType.NAV, + hold, + self.doctrine.ingress_altitude, + alt_type="BARO", + description="Anchor and search from this point", + pretty_name="SEAD Sweep", + ) + + def _sead_search_point(self, target: MissionTarget) -> Point: """Creates custom waypoint for AI SEAD flights to avoid having them fly all the way to the SAM site. Args: @@ -437,16 +463,7 @@ class WaypointBuilder: hold = target.position.point_from_heading( hdg, min(threat_range, ingress2tgt_dist * 0.95) ) - - return FlightWaypoint( - "SEAD Search", - FlightWaypointType.INGRESS_SEAD, - hold, - self.doctrine.ingress_altitude, - alt_type="BARO", - description="Anchor and search from this point", - pretty_name="SEAD Search", - ) + return hold @staticmethod def escort_hold(start: Point, altitude: Distance) -> FlightWaypoint: diff --git a/game/ato/flighttype.py b/game/ato/flighttype.py index 18216ea3..dad305ee 100644 --- a/game/ato/flighttype.py +++ b/game/ato/flighttype.py @@ -57,6 +57,7 @@ class FlightType(Enum): REFUELING = "Refueling" FERRY = "Ferry" AIR_ASSAULT = "Air Assault" + SEAD_SWEEP = "SEAD Sweep" # Reintroduce legacy "engage-whatever-you-can-find" SEAD def __str__(self) -> str: return self.value @@ -91,6 +92,7 @@ class FlightType(Enum): FlightType.OCA_AIRCRAFT, FlightType.SEAD_ESCORT, FlightType.AIR_ASSAULT, + FlightType.SEAD_SWEEP, } @property @@ -110,6 +112,7 @@ class FlightType(Enum): FlightType.REFUELING: AirEntity.TANKER, FlightType.SEAD: AirEntity.SUPPRESSION_OF_ENEMY_AIR_DEFENCE, FlightType.SEAD_ESCORT: AirEntity.SUPPRESSION_OF_ENEMY_AIR_DEFENCE, + FlightType.SEAD_SWEEP: AirEntity.SUPPRESSION_OF_ENEMY_AIR_DEFENCE, FlightType.STRIKE: AirEntity.ATTACK_STRIKE, FlightType.SWEEP: AirEntity.FIGHTER, FlightType.TARCAP: AirEntity.FIGHTER, diff --git a/game/ato/flightwaypointtype.py b/game/ato/flightwaypointtype.py index 5a201674..f9913e8a 100644 --- a/game/ato/flightwaypointtype.py +++ b/game/ato/flightwaypointtype.py @@ -50,3 +50,4 @@ class FlightWaypointType(IntEnum): CARGO_STOP = 30 # Stopover landing point using the LandingReFuAr waypoint type INGRESS_AIR_ASSAULT = 31 INGRESS_ANTI_SHIP = 32 + INGRESS_SEAD_SWEEP = 33 diff --git a/game/ato/loadouts.py b/game/ato/loadouts.py index b184cc66..d8a04d1c 100644 --- a/game/ato/loadouts.py +++ b/game/ato/loadouts.py @@ -184,6 +184,9 @@ class Loadout: # A SEAD escort typically does not need a different loadout than a regular # SEAD flight, so fall back to SEAD if needed. loadout_names[FlightType.SEAD_ESCORT].extend(loadout_names[FlightType.SEAD]) + loadout_names[FlightType.SEAD_SWEEP].extend( + loadout_names[FlightType.SEAD_ESCORT] + ) # Sweep and escort can fall back to TARCAP. loadout_names[FlightType.ESCORT].extend(loadout_names[FlightType.TARCAP]) loadout_names[FlightType.SWEEP].extend(loadout_names[FlightType.TARCAP]) diff --git a/game/dcs/aircrafttype.py b/game/dcs/aircrafttype.py index 87d6866b..f2ecab72 100644 --- a/game/dcs/aircrafttype.py +++ b/game/dcs/aircrafttype.py @@ -459,6 +459,9 @@ class AircraftType(UnitType[Type[FlyingType]]): for task_name, priority in data.get("tasks", {}).items(): task_priorities[FlightType(task_name)] = priority + if FlightType.SEAD in task_priorities: + task_priorities[FlightType.SEAD_SWEEP] = task_priorities[FlightType.SEAD] + for variant in data.get("variants", [aircraft.id]): yield AircraftType( dcs_unit_type=aircraft, diff --git a/game/missiongenerator/aircraft/aircraftbehavior.py b/game/missiongenerator/aircraft/aircraftbehavior.py index ad5c09ee..8ba6e486 100644 --- a/game/missiongenerator/aircraft/aircraftbehavior.py +++ b/game/missiongenerator/aircraft/aircraftbehavior.py @@ -56,7 +56,7 @@ class AircraftBehavior: self.configure_cas(group, flight) elif self.task == FlightType.DEAD: self.configure_dead(group, flight) - elif self.task == FlightType.SEAD: + elif self.task in [FlightType.SEAD, FlightType.SEAD_SWEEP]: self.configure_sead(group, flight) elif self.task == FlightType.SEAD_ESCORT: self.configure_sead_escort(group, flight) diff --git a/game/missiongenerator/aircraft/waypoints/seadsweepingress.py b/game/missiongenerator/aircraft/waypoints/seadsweepingress.py new file mode 100644 index 00000000..a1eccc1e --- /dev/null +++ b/game/missiongenerator/aircraft/waypoints/seadsweepingress.py @@ -0,0 +1,27 @@ +from dcs.point import MovingPoint +from dcs.task import ( + OptECMUsing, + ControlledTask, + EngageTargets, + Targets, +) + +from game.utils import nautical_miles +from .pydcswaypointbuilder import PydcsWaypointBuilder + + +class SeadSweepIngressBuilder(PydcsWaypointBuilder): + def add_tasks(self, waypoint: MovingPoint) -> None: + # Preemptively use ECM to better avoid getting swatted. + ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfDetectedLockByRadar) + waypoint.tasks.append(ecm_option) + + waypoint.add_task( + ControlledTask( + EngageTargets( + # TODO: From doctrine. + max_distance=int(nautical_miles(30).meters), + targets=[Targets.All.GroundUnits.AirDefence.AAA.SAMRelated], + ) + ) + ) diff --git a/game/missiongenerator/aircraft/waypoints/waypointgenerator.py b/game/missiongenerator/aircraft/waypoints/waypointgenerator.py index 7aeef5bb..4b052f65 100644 --- a/game/missiongenerator/aircraft/waypoints/waypointgenerator.py +++ b/game/missiongenerator/aircraft/waypoints/waypointgenerator.py @@ -37,6 +37,7 @@ from .racetrack import RaceTrackBuilder from .racetrackend import RaceTrackEndBuilder from .refuel import RefuelPointBuilder from .seadingress import SeadIngressBuilder +from .seadsweepingress import SeadSweepIngressBuilder from .splitpoint import SplitPointBuilder from .strikeingress import StrikeIngressBuilder from .sweepingress import SweepIngressBuilder @@ -126,6 +127,7 @@ class WaypointGenerator: FlightWaypointType.INGRESS_OCA_AIRCRAFT: OcaAircraftIngressBuilder, FlightWaypointType.INGRESS_OCA_RUNWAY: OcaRunwayIngressBuilder, FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder, + FlightWaypointType.INGRESS_SEAD_SWEEP: SeadSweepIngressBuilder, FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder, FlightWaypointType.INGRESS_SWEEP: SweepIngressBuilder, FlightWaypointType.JOIN: JoinPointBuilder, diff --git a/game/theater/missiontarget.py b/game/theater/missiontarget.py index fb48d6f0..6350a6fb 100644 --- a/game/theater/missiontarget.py +++ b/game/theater/missiontarget.py @@ -38,6 +38,7 @@ class MissionTarget: FlightType.ESCORT, FlightType.TARCAP, FlightType.SEAD_ESCORT, + FlightType.SEAD_SWEEP, FlightType.SWEEP, # TODO: FlightType.ELINT, # TODO: FlightType.EWAR,