Implement OCA strike missions.

https://github.com/Khopa/dcs_liberation/issues/349
This commit is contained in:
Dan Albert 2020-11-23 17:58:53 -08:00
parent 6e0af7c144
commit b0317055e7
7 changed files with 98 additions and 14 deletions

View File

@ -409,8 +409,8 @@ class Airfield(ControlPoint):
] ]
else: else:
yield from [ yield from [
FlightType.OCA_STRIKE,
FlightType.RUNWAY_ATTACK, FlightType.RUNWAY_ATTACK,
# TODO: FlightType.OCA_STRIKE
] ]
yield from super().mission_types(for_player) yield from super().mission_types(for_player)

View File

@ -1175,8 +1175,18 @@ class AircraftConflictGenerator:
self, group: FlyingGroup, package: Package, flight: Flight, self, group: FlyingGroup, package: Package, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = RunwayAttack.name group.task = RunwayAttack.name
self._setup_group(group, RunwayAttack, package, flight, self._setup_group(group, RunwayAttack, package, flight, dynamic_runways)
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( self.configure_behavior(
group, group,
react_on_threat=OptReactOnThreat.Values.EvadeFire, react_on_threat=OptReactOnThreat.Values.EvadeFire,
@ -1223,6 +1233,8 @@ class AircraftConflictGenerator:
elif flight_type == FlightType.RUNWAY_ATTACK: elif flight_type == FlightType.RUNWAY_ATTACK:
self.configure_runway_attack(group, package, flight, self.configure_runway_attack(group, package, flight,
dynamic_runways) dynamic_runways)
elif flight_type == FlightType.OCA_STRIKE:
self.configure_oca_strike(group, package, flight, dynamic_runways)
else: else:
self.configure_unknown_task(group, flight) self.configure_unknown_task(group, flight)
@ -1340,6 +1352,9 @@ class PydcsWaypointBuilder:
waypoint = self.group.add_waypoint( waypoint = self.group.add_waypoint(
Point(self.waypoint.x, self.waypoint.y), self.waypoint.alt) 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.alt_type = self.waypoint.alt_type
waypoint.name = String(self.waypoint.name) waypoint.name = String(self.waypoint.name)
tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint) tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint)
@ -1362,14 +1377,15 @@ class PydcsWaypointBuilder:
FlightWaypointType.INGRESS_BAI: BaiIngressBuilder, FlightWaypointType.INGRESS_BAI: BaiIngressBuilder,
FlightWaypointType.INGRESS_CAS: CasIngressBuilder, FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder, FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,
FlightWaypointType.INGRESS_OCA_STRIKE: OcaStrikeIngressBuilder,
FlightWaypointType.INGRESS_RUNWAY_BOMBING: RunwayBombingIngressBuilder, FlightWaypointType.INGRESS_RUNWAY_BOMBING: RunwayBombingIngressBuilder,
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder, FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder, FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
FlightWaypointType.INGRESS_SWEEP: SweepIngressBuilder,
FlightWaypointType.JOIN: JoinPointBuilder, FlightWaypointType.JOIN: JoinPointBuilder,
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)
@ -1498,6 +1514,32 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
return waypoint 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): class RunwayBombingIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()

View File

@ -147,6 +147,7 @@ class Package:
FlightType.CAS, FlightType.CAS,
FlightType.STRIKE, FlightType.STRIKE,
FlightType.ANTISHIP, FlightType.ANTISHIP,
FlightType.OCA_STRIKE,
FlightType.RUNWAY_ATTACK, FlightType.RUNWAY_ATTACK,
FlightType.BAI, FlightType.BAI,
FlightType.DEAD, FlightType.DEAD,

View File

@ -162,6 +162,8 @@ class AircraftAllocator:
return CAS_PREFERRED return CAS_PREFERRED
elif task in (FlightType.DEAD, FlightType.SEAD): elif task in (FlightType.DEAD, FlightType.SEAD):
return SEAD_PREFERRED return SEAD_PREFERRED
elif task == FlightType.OCA_STRIKE:
return CAS_PREFERRED
elif task == FlightType.RUNWAY_ATTACK: elif task == FlightType.RUNWAY_ATTACK:
return RUNWAY_ATTACK_PREFERRED return RUNWAY_ATTACK_PREFERRED
elif task == FlightType.STRIKE: elif task == FlightType.STRIKE:
@ -184,6 +186,8 @@ class AircraftAllocator:
return CAS_CAPABLE return CAS_CAPABLE
elif task in (FlightType.DEAD, FlightType.SEAD): elif task in (FlightType.DEAD, FlightType.SEAD):
return SEAD_CAPABLE return SEAD_CAPABLE
elif task == FlightType.OCA_STRIKE:
return CAS_CAPABLE
elif task == FlightType.RUNWAY_ATTACK: elif task == FlightType.RUNWAY_ATTACK:
return RUNWAY_ATTACK_CAPABLE return RUNWAY_ATTACK_CAPABLE
elif task == FlightType.STRIKE: elif task == FlightType.STRIKE:

View File

@ -29,6 +29,7 @@ class FlightType(Enum):
BAI = "BAI" BAI = "BAI"
SWEEP = "Fighter sweep" SWEEP = "Fighter sweep"
RUNWAY_ATTACK = "Runway attack" RUNWAY_ATTACK = "Runway attack"
OCA_STRIKE = "OCA Strike"
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value
@ -60,6 +61,7 @@ class FlightWaypointType(Enum):
INGRESS_BAI = 22 INGRESS_BAI = 22
DIVERT = 23 DIVERT = 23
INGRESS_RUNWAY_BOMBING = 24 INGRESS_RUNWAY_BOMBING = 24
INGRESS_OCA_STRIKE = 25
class FlightWaypoint: class FlightWaypoint:
@ -86,6 +88,7 @@ class FlightWaypoint:
self.obj_name = "" self.obj_name = ""
self.pretty_name = "" self.pretty_name = ""
self.only_for_player = False self.only_for_player = False
self.flyover = False
# These are set very late by the air conflict generator (part of mission # 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 # generation). We do it late so that we don't need to propagate changes

View File

@ -39,7 +39,6 @@ if TYPE_CHECKING:
from game import Game from game import Game
from gen.ato import Package from gen.ato import Package
INGRESS_TYPES = { INGRESS_TYPES = {
FlightWaypointType.INGRESS_CAS, FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_ESCORT, FlightWaypointType.INGRESS_ESCORT,
@ -55,6 +54,7 @@ class PlanningError(RuntimeError):
class InvalidObjectiveLocation(PlanningError): class InvalidObjectiveLocation(PlanningError):
"""Raised when the objective location is invalid for the mission type.""" """Raised when the objective location is invalid for the mission type."""
def __init__(self, task: FlightType, location: MissionTarget) -> None: def __init__(self, task: FlightType, location: MissionTarget) -> None:
super().__init__(f"{location.name} is not valid for {task} missions.") super().__init__(f"{location.name} is not valid for {task} missions.")
@ -412,7 +412,7 @@ class StrikeFlightPlan(FormationFlightPlan):
self.ingress self.ingress
] ]
yield from self.targets yield from self.targets
yield from[ yield from [
self.egress, self.egress,
self.split, self.split,
self.land, self.land,
@ -649,6 +649,8 @@ class FlightPlanBuilder:
return self.generate_dead(flight, custom_targets) return self.generate_dead(flight, custom_targets)
elif task == FlightType.ESCORT: elif task == FlightType.ESCORT:
return self.generate_escort(flight) return self.generate_escort(flight)
elif task == FlightType.OCA_STRIKE:
return self.generate_oca_strike(flight)
elif task == FlightType.RUNWAY_ATTACK: elif task == FlightType.RUNWAY_ATTACK:
return self.generate_runway_attack(flight) return self.generate_runway_attack(flight)
elif task == FlightType.SEAD: elif task == FlightType.SEAD:
@ -876,7 +878,7 @@ class FlightPlanBuilder:
if combat_width < 35000: if combat_width < 35000:
combat_width = 35000 combat_width = 35000
radius = combat_width*1.25 radius = combat_width * 1.25
orbit0p = orbit_center.point_from_heading(heading, radius) orbit0p = orbit_center.point_from_heading(heading, radius)
orbit1p = orbit_center.point_from_heading(heading + 180, radius) orbit1p = orbit_center.point_from_heading(heading + 180, radius)
@ -931,7 +933,8 @@ class FlightPlanBuilder:
is_ewr = isinstance(location, EwrGroundObject) is_ewr = isinstance(location, EwrGroundObject)
is_sam = isinstance(location, SamGroundObject) is_sam = isinstance(location, SamGroundObject)
if not is_ewr and not is_sam: 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) raise InvalidObjectiveLocation(flight.flight_type, location)
# TODO: Unify these. # TODO: Unify these.
@ -946,6 +949,23 @@ class FlightPlanBuilder:
return self.strike_flightplan(flight, location, return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_DEAD, targets) 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: def generate_runway_attack(self, flight: Flight) -> StrikeFlightPlan:
"""Generate a runway attack flight plan at a given location. """Generate a runway attack flight plan at a given location.
@ -1059,6 +1079,8 @@ class FlightPlanBuilder:
return builder.dead_area(location) return builder.dead_area(location)
elif flight.flight_type == FlightType.SEAD: elif flight.flight_type == FlightType.SEAD:
return builder.sead_area(location) return builder.sead_area(location)
elif flight.flight_type == FlightType.OCA_STRIKE:
return builder.oca_strike_area(location)
else: else:
return builder.strike_area(location) return builder.strike_area(location)

View File

@ -239,8 +239,12 @@ class WaypointBuilder:
def dead_area(self, target: MissionTarget) -> FlightWaypoint: def dead_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"DEAD on {target.name}", target) 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 @staticmethod
def _target_area(name: str, location: MissionTarget) -> FlightWaypoint: def _target_area(name: str, location: MissionTarget,
flyover: bool = False) -> FlightWaypoint:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
location.position.x, location.position.x,
@ -251,9 +255,17 @@ class WaypointBuilder:
waypoint.pretty_name = name waypoint.pretty_name = name
waypoint.name = name waypoint.name = name
waypoint.alt_type = "RADIO" 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 # the target are set on the ingress point so they begin their attack
# *before* reaching the target. # *before* reaching the target.
#
# 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 waypoint.only_for_player = True
return waypoint return waypoint