From b0317055e7db71fca59df97413c92bed513ee538 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Mon, 23 Nov 2020 17:58:53 -0800 Subject: [PATCH] Implement OCA strike missions. https://github.com/Khopa/dcs_liberation/issues/349 --- game/theater/controlpoint.py | 2 +- gen/aircraft.py | 48 ++++++++++++++++++++++++++++++-- gen/ato.py | 1 + gen/flights/ai_flight_planner.py | 4 +++ gen/flights/flight.py | 3 ++ gen/flights/flightplan.py | 36 +++++++++++++++++++----- gen/flights/waypointbuilder.py | 18 ++++++++++-- 7 files changed, 98 insertions(+), 14 deletions(-) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 6ec68c40..6a714acf 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -409,8 +409,8 @@ class Airfield(ControlPoint): ] else: yield from [ + FlightType.OCA_STRIKE, FlightType.RUNWAY_ATTACK, - # TODO: FlightType.OCA_STRIKE ] yield from super().mission_types(for_player) diff --git a/gen/aircraft.py b/gen/aircraft.py index a4c60623..14c9e33f 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1175,8 +1175,18 @@ class AircraftConflictGenerator: self, group: FlyingGroup, package: Package, flight: Flight, dynamic_runways: Dict[str, RunwayData]) -> None: group.task = RunwayAttack.name - self._setup_group(group, RunwayAttack, package, flight, - dynamic_runways) + self._setup_group(group, RunwayAttack, package, flight, dynamic_runways) + self.configure_behavior( + group, + react_on_threat=OptReactOnThreat.Values.EvadeFire, + roe=OptROE.Values.OpenFire, + restrict_jettison=True) + + def configure_oca_strike( + self, group: FlyingGroup, package: Package, flight: Flight, + dynamic_runways: Dict[str, RunwayData]) -> None: + group.task = CAS.name + self._setup_group(group, CAS, package, flight, dynamic_runways) self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, @@ -1223,6 +1233,8 @@ class AircraftConflictGenerator: elif flight_type == FlightType.RUNWAY_ATTACK: self.configure_runway_attack(group, package, flight, dynamic_runways) + elif flight_type == FlightType.OCA_STRIKE: + self.configure_oca_strike(group, package, flight, dynamic_runways) else: self.configure_unknown_task(group, flight) @@ -1340,6 +1352,9 @@ class PydcsWaypointBuilder: waypoint = self.group.add_waypoint( Point(self.waypoint.x, self.waypoint.y), self.waypoint.alt) + if self.waypoint.flyover: + waypoint.type = PointAction.FlyOverPoint.value + waypoint.alt_type = self.waypoint.alt_type waypoint.name = String(self.waypoint.name) tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint) @@ -1362,14 +1377,15 @@ class PydcsWaypointBuilder: FlightWaypointType.INGRESS_BAI: BaiIngressBuilder, FlightWaypointType.INGRESS_CAS: CasIngressBuilder, FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder, + FlightWaypointType.INGRESS_OCA_STRIKE: OcaStrikeIngressBuilder, FlightWaypointType.INGRESS_RUNWAY_BOMBING: RunwayBombingIngressBuilder, FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder, FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder, + FlightWaypointType.INGRESS_SWEEP: SweepIngressBuilder, FlightWaypointType.JOIN: JoinPointBuilder, FlightWaypointType.LANDING_POINT: LandingPointBuilder, FlightWaypointType.LOITER: HoldPointBuilder, FlightWaypointType.PATROL_TRACK: RaceTrackBuilder, - FlightWaypointType.INGRESS_SWEEP: SweepIngressBuilder, } builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder) return builder(waypoint, group, package, flight, mission) @@ -1498,6 +1514,32 @@ class DeadIngressBuilder(PydcsWaypointBuilder): return waypoint +class OcaStrikeIngressBuilder(PydcsWaypointBuilder): + def build(self) -> MovingPoint: + waypoint = super().build() + + target = self.package.target + if not isinstance(target, Airfield): + logging.error( + "Unexpected target type for OCA Strike mission: %s", + target.__class__.__name__) + return waypoint + + task = EngageTargetsInZone( + position=target.position, + # Al Dhafra is 4 nm across at most. Add a little wiggle room in case + # the airport position from DCS is not centered. + radius=nm_to_meter(3), + targets=[Targets.All.Air] + ) + task.params["attackQtyLimit"] = False + task.params["directionEnabled"] = False + task.params["altitudeEnabled"] = False + task.params["groupAttack"] = True + waypoint.tasks.append(task) + return waypoint + + class RunwayBombingIngressBuilder(PydcsWaypointBuilder): def build(self) -> MovingPoint: waypoint = super().build() diff --git a/gen/ato.py b/gen/ato.py index d5a658c8..f4c6df76 100644 --- a/gen/ato.py +++ b/gen/ato.py @@ -147,6 +147,7 @@ class Package: FlightType.CAS, FlightType.STRIKE, FlightType.ANTISHIP, + FlightType.OCA_STRIKE, FlightType.RUNWAY_ATTACK, FlightType.BAI, FlightType.DEAD, diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index bdc3b487..6a21758e 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -162,6 +162,8 @@ class AircraftAllocator: return CAS_PREFERRED elif task in (FlightType.DEAD, FlightType.SEAD): return SEAD_PREFERRED + elif task == FlightType.OCA_STRIKE: + return CAS_PREFERRED elif task == FlightType.RUNWAY_ATTACK: return RUNWAY_ATTACK_PREFERRED elif task == FlightType.STRIKE: @@ -184,6 +186,8 @@ class AircraftAllocator: return CAS_CAPABLE elif task in (FlightType.DEAD, FlightType.SEAD): return SEAD_CAPABLE + elif task == FlightType.OCA_STRIKE: + return CAS_CAPABLE elif task == FlightType.RUNWAY_ATTACK: return RUNWAY_ATTACK_CAPABLE elif task == FlightType.STRIKE: diff --git a/gen/flights/flight.py b/gen/flights/flight.py index f01632b5..f337ec7d 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -29,6 +29,7 @@ class FlightType(Enum): BAI = "BAI" SWEEP = "Fighter sweep" RUNWAY_ATTACK = "Runway attack" + OCA_STRIKE = "OCA Strike" def __str__(self) -> str: return self.value @@ -60,6 +61,7 @@ class FlightWaypointType(Enum): INGRESS_BAI = 22 DIVERT = 23 INGRESS_RUNWAY_BOMBING = 24 + INGRESS_OCA_STRIKE = 25 class FlightWaypoint: @@ -86,6 +88,7 @@ class FlightWaypoint: self.obj_name = "" self.pretty_name = "" self.only_for_player = False + self.flyover = False # These are set very late by the air conflict generator (part of mission # generation). We do it late so that we don't need to propagate changes diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index ce906b43..1a325834 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -39,7 +39,6 @@ if TYPE_CHECKING: from game import Game from gen.ato import Package - INGRESS_TYPES = { FlightWaypointType.INGRESS_CAS, FlightWaypointType.INGRESS_ESCORT, @@ -55,6 +54,7 @@ class PlanningError(RuntimeError): class InvalidObjectiveLocation(PlanningError): """Raised when the objective location is invalid for the mission type.""" + def __init__(self, task: FlightType, location: MissionTarget) -> None: super().__init__(f"{location.name} is not valid for {task} missions.") @@ -412,7 +412,7 @@ class StrikeFlightPlan(FormationFlightPlan): self.ingress ] yield from self.targets - yield from[ + yield from [ self.egress, self.split, self.land, @@ -423,9 +423,9 @@ class StrikeFlightPlan(FormationFlightPlan): @property def package_speed_waypoints(self) -> Set[FlightWaypoint]: return { - self.ingress, - self.egress, - self.split, + self.ingress, + self.egress, + self.split, } | set(self.targets) def speed_between_waypoints(self, a: FlightWaypoint, @@ -649,6 +649,8 @@ class FlightPlanBuilder: return self.generate_dead(flight, custom_targets) elif task == FlightType.ESCORT: return self.generate_escort(flight) + elif task == FlightType.OCA_STRIKE: + return self.generate_oca_strike(flight) elif task == FlightType.RUNWAY_ATTACK: return self.generate_runway_attack(flight) elif task == FlightType.SEAD: @@ -876,7 +878,7 @@ class FlightPlanBuilder: if combat_width < 35000: combat_width = 35000 - radius = combat_width*1.25 + radius = combat_width * 1.25 orbit0p = orbit_center.point_from_heading(heading, radius) orbit1p = orbit_center.point_from_heading(heading + 180, radius) @@ -931,7 +933,8 @@ class FlightPlanBuilder: is_ewr = isinstance(location, EwrGroundObject) is_sam = isinstance(location, SamGroundObject) if not is_ewr and not is_sam: - logging.exception(f"Invalid Objective Location for DEAD flight {flight=} at {location=}") + logging.exception( + f"Invalid Objective Location for DEAD flight {flight=} at {location=}") raise InvalidObjectiveLocation(flight.flight_type, location) # TODO: Unify these. @@ -946,6 +949,23 @@ class FlightPlanBuilder: return self.strike_flightplan(flight, location, FlightWaypointType.INGRESS_DEAD, targets) + def generate_oca_strike(self, flight: Flight) -> StrikeFlightPlan: + """Generate an OCA Strike flight plan at a given location. + + Args: + flight: The flight to generate the flight plan for. + """ + location = self.package.target + + if not isinstance(location, Airfield): + logging.exception( + f"Invalid Objective Location for OCA Strike flight " + f"{flight=} at {location=}.") + raise InvalidObjectiveLocation(flight.flight_type, location) + + return self.strike_flightplan(flight, location, + FlightWaypointType.INGRESS_OCA_STRIKE) + def generate_runway_attack(self, flight: Flight) -> StrikeFlightPlan: """Generate a runway attack flight plan at a given location. @@ -1059,6 +1079,8 @@ class FlightPlanBuilder: return builder.dead_area(location) elif flight.flight_type == FlightType.SEAD: return builder.sead_area(location) + elif flight.flight_type == FlightType.OCA_STRIKE: + return builder.oca_strike_area(location) else: return builder.strike_area(location) diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index e55672bf..e8bcce7d 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -239,8 +239,12 @@ class WaypointBuilder: def dead_area(self, target: MissionTarget) -> FlightWaypoint: return self._target_area(f"DEAD on {target.name}", target) + def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint: + return self._target_area(f"ATTACK {target.name}", target, flyover=True) + @staticmethod - def _target_area(name: str, location: MissionTarget) -> FlightWaypoint: + def _target_area(name: str, location: MissionTarget, + flyover: bool = False) -> FlightWaypoint: waypoint = FlightWaypoint( FlightWaypointType.TARGET_GROUP_LOC, location.position.x, @@ -251,10 +255,18 @@ class WaypointBuilder: waypoint.pretty_name = name waypoint.name = name waypoint.alt_type = "RADIO" - # The target waypoints are only for the player's benefit. AI tasks for + + # Most target waypoints are only for the player's benefit. AI tasks for # the target are set on the ingress point so they begin their attack # *before* reaching the target. - waypoint.only_for_player = True + # + # The exception is for flight plans that require passing over the + # target. For example, OCA strikes need to get close enough to detect + # the targets in their engagement zone or they will RTB immediately. + if flyover: + waypoint.flyover = True + else: + waypoint.only_for_player = True return waypoint def cas(self, position: Point) -> FlightWaypoint: