diff --git a/changelog.md b/changelog.md index b0f8e3c9..7f3a60aa 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ Saves from 2.5 are not compatible with 3.0. * **[Campaign AI]** Fix purchase of aircraft by priority (the faction's list was being used as the priority list rather than the game's). * **[Flight Planner]** AI strike flight plans now include the correct target actions for building groups. * **[Flight Planner]** Flight plans now include bullseye waypoints. +* **[Flight Planner]** Differentiated SEAD and SEAD escort. SEAD is tasked with suppressing the package target, SEAD escort is tasked with protecting the package from all SAMs along its route. * **[Kneeboard]** ATC table overflow alleviated by wrapping long airfield names and splitting ATC frequency and channel into separate rows. * **[UI]** Added new web based map UI. This is mostly functional but many of the old display options are a WIP. Revert to the old map with --old-map. * **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present. diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 341df02e..ad9f4ec1 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -1100,7 +1100,7 @@ class Fob(ControlPoint): FlightType.STRIKE, FlightType.SWEEP, FlightType.ESCORT, - FlightType.SEAD, + FlightType.SEAD_ESCORT, ] @property diff --git a/game/theater/missiontarget.py b/game/theater/missiontarget.py index 197de586..ea426603 100644 --- a/game/theater/missiontarget.py +++ b/game/theater/missiontarget.py @@ -37,7 +37,7 @@ class MissionTarget: yield from [ FlightType.ESCORT, FlightType.TARCAP, - FlightType.SEAD, + FlightType.SEAD_ESCORT, FlightType.SWEEP, # TODO: FlightType.ELINT, # TODO: FlightType.EWAR, diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index 18131a42..2ff05114 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -452,6 +452,7 @@ class SamGroundObject(BaseDefenseGroundObject): if not self.is_friendly(for_player): yield FlightType.DEAD + yield FlightType.SEAD yield from super().mission_types(for_player) @property diff --git a/gen/aircraft.py b/gen/aircraft.py index b170c5c2..c7b7a17a 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -67,6 +67,7 @@ from dcs.task import ( Targets, Transport, WeaponType, + TargetType, ) from dcs.terrain.terrain import Airport, NoParkingSlotError from dcs.triggers import Event, TriggerOnce, TriggerRule @@ -1379,6 +1380,22 @@ class AircraftConflictGenerator: group, roe=OptROE.Values.OpenFire, restrict_jettison=True ) + def configure_sead_escort( + self, + group: FlyingGroup, + package: Package, + flight: Flight, + dynamic_runways: Dict[str, RunwayData], + ) -> None: + group.task = SEAD.name + self._setup_group(group, package, flight, dynamic_runways) + self.configure_behavior( + group, + roe=OptROE.Values.OpenFire, + rtb_winchester=OptRTBOnOutOfAmmo.Values.Guided, + restrict_jettison=True, + ) + def configure_transport( self, group: FlyingGroup, @@ -1423,6 +1440,8 @@ class AircraftConflictGenerator: self.configure_dead(group, package, flight, dynamic_runways) elif flight_type == FlightType.SEAD: self.configure_sead(group, package, flight, dynamic_runways) + elif flight_type == FlightType.SEAD_ESCORT: + self.configure_sead_escort(group, package, flight, dynamic_runways) elif flight_type == FlightType.STRIKE: self.configure_strike(group, package, flight, dynamic_runways) elif flight_type == FlightType.ANTISHIP: @@ -1802,18 +1821,16 @@ class SeadIngressBuilder(PydcsWaypointBuilder): if isinstance(target_group, TheaterGroundObject): tgroup = self.mission.find_group(target_group.group_name) if tgroup is not None: - waypoint.add_task( - EngageTargetsInZone( - position=tgroup.position, - radius=int(nautical_miles(30).meters), - targets=[ - Targets.All.GroundUnits.AirDefence, - ], - ) - ) + task = AttackGroup(tgroup.id, weapon_type=WeaponType.Guided) + task.params["expend"] = "All" + task.params["attackQtyLimit"] = False + task.params["directionEnabled"] = False + task.params["altitudeEnabled"] = False + task.params["groupAttack"] = True + waypoint.tasks.append(task) else: logging.error( - f"Could not find group for DEAD mission {target_group.group_name}" + f"Could not find group for SEAD mission {target_group.group_name}" ) self.register_special_waypoints(self.waypoint.targets) return waypoint @@ -1889,11 +1906,23 @@ class JoinPointBuilder(PydcsWaypointBuilder): def build(self) -> MovingPoint: waypoint = super().build() if self.flight.flight_type == FlightType.ESCORT: - self.configure_escort_tasks(waypoint) + self.configure_escort_tasks( + waypoint, + [ + Targets.All.Air.Planes.Fighters, + Targets.All.Air.Planes.MultiroleFighters, + ], + ) + elif self.flight.flight_type == FlightType.SEAD_ESCORT: + self.configure_escort_tasks( + waypoint, [Targets.All.GroundUnits.AirDefence.AAA.SAMRelated] + ) return waypoint @staticmethod - def configure_escort_tasks(waypoint: MovingPoint) -> None: + def configure_escort_tasks( + waypoint: MovingPoint, target_types: List[Type[TargetType]] + ) -> None: # Ideally we would use the escort mission type and escort task to have # the AI automatically but the AI only escorts AI flights while they are # traveling between waypoints. When an AI flight performs an attack @@ -1919,16 +1948,13 @@ class JoinPointBuilder(PydcsWaypointBuilder): # for the target area that is set to end on a flag flip that occurs when # the strike aircraft finish their attack task. # - # https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/250183-task-follow-and-escort-temporarily-aborted + # https://forums.eagle.ru/topic/251798-options-for-alternate-ai-escort-behavior waypoint.add_task( ControlledTask( EngageTargets( # TODO: From doctrine. max_distance=int(nautical_miles(30).meters), - targets=[ - Targets.All.Air.Planes.Fighters, - Targets.All.Air.Planes.MultiroleFighters, - ], + targets=target_types, ) ) ) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 2cd658d5..f8bb3905 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -635,6 +635,7 @@ class CoalitionMissionPlanner: sam, [ ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE), + ProposedFlight(FlightType.SEAD, 2, self.MAX_SEAD_RANGE), # TODO: Max escort range. ProposedFlight( FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.AirToAir @@ -665,7 +666,7 @@ class CoalitionMissionPlanner: FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir ), ProposedFlight( - FlightType.SEAD, 2, self.MAX_BAI_RANGE, EscortType.Sead + FlightType.SEAD_ESCORT, 2, self.MAX_BAI_RANGE, EscortType.Sead ), ], ) @@ -680,7 +681,7 @@ class CoalitionMissionPlanner: FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir ), ProposedFlight( - FlightType.SEAD, 2, self.MAX_BAI_RANGE, EscortType.Sead + FlightType.SEAD_ESCORT, 2, self.MAX_BAI_RANGE, EscortType.Sead ), ], ) @@ -710,7 +711,7 @@ class CoalitionMissionPlanner: FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir ), ProposedFlight( - FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead + FlightType.SEAD_ESCORT, 2, self.MAX_OCA_RANGE, EscortType.Sead ), ], ) @@ -732,7 +733,7 @@ class CoalitionMissionPlanner: FlightType.ESCORT, 2, self.MAX_OCA_RANGE, EscortType.AirToAir ), ProposedFlight( - FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead + FlightType.SEAD_ESCORT, 2, self.MAX_OCA_RANGE, EscortType.Sead ), ] ) @@ -749,7 +750,10 @@ class CoalitionMissionPlanner: FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, EscortType.AirToAir ), ProposedFlight( - FlightType.SEAD, 2, self.MAX_STRIKE_RANGE, EscortType.Sead + FlightType.SEAD_ESCORT, + 2, + self.MAX_STRIKE_RANGE, + EscortType.Sead, ), ], ) diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py index 730d4722..2c476b14 100644 --- a/gen/flights/ai_flight_planner_db.py +++ b/gen/flights/ai_flight_planner_db.py @@ -405,6 +405,8 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]: return CAS_CAPABLE elif task == FlightType.SEAD: return SEAD_CAPABLE + elif task == FlightType.SEAD_ESCORT: + return SEAD_CAPABLE elif task == FlightType.DEAD: return DEAD_CAPABLE elif task == FlightType.OCA_AIRCRAFT: diff --git a/gen/flights/flight.py b/gen/flights/flight.py index d5f95862..b9ea8bce 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -28,6 +28,27 @@ class FlightType(Enum): 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. + + When adding new mission types to this list, you will also need to update: + + * flightplan.py: Add waypoint population in generate_flight_plan. Add a new flight + plan type if necessary, though most are a subclass of StrikeFlightPlan. + * aircraft.py: Add a configuration method and call it in setup_flight_group. This is + responsible for configuring waypoint 0 actions like setting ROE, threat reaction, + and mission abort parameters (winchester, bingo, etc). + * Implementations of MissionTarget.mission_types: A mission type can only be planned + against compatible targets. The mission_types method of each target class defines + which missions may target it. + * ai_flight_planner_db.py: Add the new mission type to aircraft_for_task that + returns the list of compatible aircraft in order of preference. + + You may also need to update: + + * flight.py: Add a new waypoint type if necessary. Most mission types will need + these, as aircraft.py uses the ingress point type to specialize AI tasks, and non- + strike-like missions will need more specialized control. + * ai_flight_planner.py: Use the new mission type in propose_missions so the AI will + plan the new mission type. """ TARCAP = "TARCAP" @@ -45,12 +66,27 @@ class FlightType(Enum): OCA_AIRCRAFT = "OCA/Aircraft" AEWC = "AEW&C" TRANSPORT = "Transport" + SEAD_ESCORT = "SEAD Escort" def __str__(self) -> str: return self.value class FlightWaypointType(Enum): + """Enumeration of waypoint types. + + The value of the enum has no meaning but should remain stable to prevent breaking + save game compatibility. + + When adding a new waypoint type, you will also need to update: + + * waypointbuilder.py: Add a builder to simplify construction of the new waypoint + type unless the new waypoint type will be a parameter to an existing builder + method (such as how escort ingress waypoints work). + * aircraft.py: Associate AI actions with the new waypoint type by subclassing + PydcsWaypointBuilder and using it in PydcsWaypointBuilder.for_waypoint. + """ + TAKEOFF = 0 # Take off point ASCEND_POINT = 1 # Ascension point after take off PATROL = 2 # Patrol point diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 14bc6601..5e6bc886 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -890,6 +890,8 @@ class FlightPlanBuilder: return self.generate_runway_attack(flight) elif task == FlightType.SEAD: return self.generate_sead(flight, custom_targets) + elif task == FlightType.SEAD_ESCORT: + return self.generate_escort(flight) elif task == FlightType.STRIKE: return self.generate_strike(flight) elif task == FlightType.SWEEP: diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index 670cdc01..f8380897 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -423,7 +423,10 @@ class WaypointBuilder: return self.sweep_start(start, altitude), self.sweep_end(end, altitude) def escort( - self, ingress: Point, target: MissionTarget, egress: Point + self, + ingress: Point, + target: MissionTarget, + egress: Point, ) -> Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]: """Creates the waypoints needed to escort the package.