mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Add escort tasks for the AI.
This commit is contained in:
parent
95f486870d
commit
fd969020af
@ -3,11 +3,11 @@ from __future__ import annotations
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Tuple, Type, Union
|
||||
from typing import Dict, List, Optional, Type, Union
|
||||
|
||||
from dcs import helicopters
|
||||
from dcs.action import AITaskPush, ActivateGroup, MessageToAll
|
||||
from dcs.condition import CoalitionHasAirdrome, PartOfCoalitionInZone, TimeAfter
|
||||
from dcs.action import AITaskPush, ActivateGroup
|
||||
from dcs.condition import CoalitionHasAirdrome, TimeAfter
|
||||
from dcs.country import Country
|
||||
from dcs.flyingunit import FlyingUnit
|
||||
from dcs.helicopters import UH_1H, helicopter_map
|
||||
@ -40,7 +40,6 @@ from dcs.task import (
|
||||
ControlledTask,
|
||||
EPLRS,
|
||||
EngageTargets,
|
||||
Escort,
|
||||
GroundAttack,
|
||||
OptROE,
|
||||
OptRTBOnBingoFuel,
|
||||
@ -55,10 +54,10 @@ from dcs.task import (
|
||||
Targets,
|
||||
Task,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||
from dcs.terrain.terrain import Airport
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
||||
from dcs.unitgroup import FlyingGroup, Group, ShipGroup, StaticGroup
|
||||
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
@ -548,7 +547,6 @@ class AircraftConflictGenerator:
|
||||
self.settings = settings
|
||||
self.conflict = conflict
|
||||
self.radio_registry = radio_registry
|
||||
self.escort_targets: List[Tuple[FlyingGroup, int]] = []
|
||||
self.flights: List[FlightData] = []
|
||||
|
||||
def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency:
|
||||
@ -633,10 +631,6 @@ class AircraftConflictGenerator:
|
||||
logging.warning(f"Unhandled departure control point: {cp.cptype}")
|
||||
departure_runway = fallback_runway
|
||||
|
||||
# The first waypoint is set automatically by pydcs, so it's not in our
|
||||
# list. Convert the pydcs MovingPoint to a FlightWaypoint so it shows up
|
||||
# in our FlightData.
|
||||
first_point = FlightWaypoint.from_pydcs(group.points[0], flight.from_cp)
|
||||
self.flights.append(FlightData(
|
||||
flight_type=flight.flight_type,
|
||||
units=group.units,
|
||||
@ -808,8 +802,8 @@ class AircraftConflictGenerator:
|
||||
logging.info(f"Generating flight: {flight.unit_type}")
|
||||
group = self.generate_planned_flight(flight.from_cp, country,
|
||||
flight)
|
||||
self.setup_flight_group(group, package, flight, timing,
|
||||
dynamic_runways)
|
||||
self.setup_flight_group(group, flight, dynamic_runways)
|
||||
self.create_waypoints(group, package, flight, timing)
|
||||
|
||||
def set_activation_time(self, flight: Flight, group: FlyingGroup,
|
||||
delay: int) -> None:
|
||||
@ -988,8 +982,11 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_escort(self, group: FlyingGroup, flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
group.task = Escort.name
|
||||
self._setup_group(group, Escort, flight, dynamic_runways)
|
||||
# Escort groups are actually given the CAP task so they can perform the
|
||||
# Search Then Engage task, which we have to use instead of the Escort
|
||||
# task for the reasons explained in JoinPointBuilder.
|
||||
group.task = CAP.name
|
||||
self._setup_group(group, CAP, flight, dynamic_runways)
|
||||
self.configure_behavior(group, roe=OptROE.Values.OpenFire,
|
||||
restrict_jettison=True)
|
||||
|
||||
@ -998,8 +995,7 @@ class AircraftConflictGenerator:
|
||||
logging.error(f"Unhandled flight type: {flight.flight_type.name}")
|
||||
self.configure_behavior(group)
|
||||
|
||||
def setup_flight_group(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight, timing: PackageWaypointTiming,
|
||||
def setup_flight_group(self, group: FlyingGroup, flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
flight_type = flight.flight_type
|
||||
if flight_type in [FlightType.BARCAP, FlightType.TARCAP,
|
||||
@ -1020,16 +1016,23 @@ class AircraftConflictGenerator:
|
||||
|
||||
self.configure_eplrs(group, flight)
|
||||
|
||||
def create_waypoints(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight, timing: PackageWaypointTiming) -> None:
|
||||
|
||||
for waypoint in flight.points:
|
||||
waypoint.tot = None
|
||||
|
||||
takeoff_point = FlightWaypoint.from_pydcs(group.points[0],
|
||||
flight.from_cp)
|
||||
self.set_takeoff_time(takeoff_point, package, flight, group)
|
||||
|
||||
filtered_points = []
|
||||
for point in flight.points:
|
||||
if point.only_for_player and not flight.client_count:
|
||||
continue
|
||||
filtered_points.append(point)
|
||||
|
||||
for idx, point in enumerate(filtered_points):
|
||||
PydcsWaypointBuilder.for_waypoint(
|
||||
point, group, flight, timing, self.m
|
||||
).build()
|
||||
@ -1114,6 +1117,8 @@ class PydcsWaypointBuilder:
|
||||
mission: Mission) -> PydcsWaypointBuilder:
|
||||
builders = {
|
||||
FlightWaypointType.EGRESS: EgressPointBuilder,
|
||||
FlightWaypointType.INGRESS_CAS: IngressBuilder,
|
||||
FlightWaypointType.INGRESS_ESCORT: IngressBuilder,
|
||||
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
|
||||
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
|
||||
FlightWaypointType.JOIN: JoinPointBuilder,
|
||||
@ -1236,8 +1241,48 @@ class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
self.set_waypoint_tot(waypoint, self.timing.join)
|
||||
if self.flight.flight_type == FlightType.ESCORT:
|
||||
self.configure_escort_tasks(waypoint)
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def configure_escort_tasks(waypoint: MovingPoint) -> 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
|
||||
# (such as attacking the mission target), AI escorts wander aimlessly
|
||||
# until the escorted group resumes its flight plan.
|
||||
#
|
||||
# As such, we instead use the Search Then Engage task, which is an
|
||||
# enroute task that causes the AI to follow their flight plan and engage
|
||||
# enemies of the set type within a certain distance. The downside to
|
||||
# this approach is that AI escorts are no longer related to the group
|
||||
# they are escorting, aside from the fact that they fly a similar flight
|
||||
# plan at the same time. With Escort, the escorts will follow the
|
||||
# escorted group out of the area. The strike element may or may not fly
|
||||
# directly over the target, and they may or may not require multiple
|
||||
# attack runs. For the escort flight we must just assume a flight plan
|
||||
# for the escort to fly. If the strike flight doesn't need to overfly
|
||||
# the target, the escorts are needlessly going in harms way. If the
|
||||
# strike flight needs multiple passes, the escorts may leave before the
|
||||
# escorted aircraft do.
|
||||
#
|
||||
# Another possible option would be to use Search Then Engage for join ->
|
||||
# ingress and egress -> split, but use a Search Then Engage in Zone task
|
||||
# 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
|
||||
waypoint.add_task(ControlledTask(EngageTargets(
|
||||
# TODO: From doctrine.
|
||||
max_distance=nm_to_meter(30),
|
||||
targets=[Targets.All.Air.Planes.Fighters]
|
||||
)))
|
||||
|
||||
# We could set this task to end at the split point. pydcs doesn't
|
||||
# currently support that task end condition though, and we don't really
|
||||
# need it.
|
||||
|
||||
|
||||
class LandingPointBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
|
||||
@ -52,6 +52,7 @@ class FlightWaypointType(Enum):
|
||||
JOIN = 16
|
||||
SPLIT = 17
|
||||
LOITER = 18
|
||||
INGRESS_ESCORT = 19
|
||||
|
||||
|
||||
class PredefinedWaypointCategory(Enum):
|
||||
|
||||
@ -132,7 +132,7 @@ class FlightPlanBuilder:
|
||||
if not isinstance(location, TheaterGroundObject):
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.hold(self._hold_point(flight))
|
||||
builder.join(self.package.waypoints.join)
|
||||
@ -222,7 +222,7 @@ class FlightPlanBuilder:
|
||||
)
|
||||
start = end.point_from_heading(heading - 180, diameter)
|
||||
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.race_track(start, end, patrol_alt)
|
||||
builder.rtb(flight.from_cp)
|
||||
@ -264,7 +264,7 @@ class FlightPlanBuilder:
|
||||
orbit1p = orbit_center.point_from_heading(heading + 180, radius)
|
||||
|
||||
# Create points
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.hold(self._hold_point(flight))
|
||||
builder.join(self.package.waypoints.join)
|
||||
@ -290,7 +290,7 @@ class FlightPlanBuilder:
|
||||
if custom_targets is None:
|
||||
custom_targets = []
|
||||
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.hold(self._hold_point(flight))
|
||||
builder.join(self.package.waypoints.join)
|
||||
@ -328,17 +328,12 @@ class FlightPlanBuilder:
|
||||
def generate_escort(self, flight: Flight) -> None:
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
patrol_alt = random.randint(
|
||||
self.doctrine.min_patrol_altitude,
|
||||
self.doctrine.max_patrol_altitude
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.hold(self._hold_point(flight))
|
||||
builder.join(self.package.waypoints.join)
|
||||
builder.race_track(self.package.waypoints.ingress,
|
||||
self.package.waypoints.egress, patrol_alt)
|
||||
builder.escort(self.package.waypoints.ingress,
|
||||
self.package.target, self.package.waypoints.egress)
|
||||
builder.split(self.package.waypoints.split)
|
||||
builder.rtb(flight.from_cp)
|
||||
|
||||
@ -366,7 +361,7 @@ class FlightPlanBuilder:
|
||||
center = ingress.point_from_heading(heading, distance / 2)
|
||||
egress = ingress.point_from_heading(heading, distance)
|
||||
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder.ascent(flight.from_cp, is_helo)
|
||||
builder.hold(self._hold_point(flight))
|
||||
builder.join(self.package.waypoints.join)
|
||||
@ -379,33 +374,39 @@ class FlightPlanBuilder:
|
||||
flight.points = builder.build()
|
||||
|
||||
# TODO: Make a model for the waypoint builder and use that in the UI.
|
||||
def generate_ascend_point(self, departure: ControlPoint) -> FlightWaypoint:
|
||||
def generate_ascend_point(self, flight: Flight,
|
||||
departure: ControlPoint) -> FlightWaypoint:
|
||||
"""Generate ascend point.
|
||||
|
||||
Args:
|
||||
flight: The flight to generate the descend point for.
|
||||
departure: Departure airfield or carrier.
|
||||
"""
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder.ascent(departure)
|
||||
return builder.build()[0]
|
||||
|
||||
def generate_descend_point(self, arrival: ControlPoint) -> FlightWaypoint:
|
||||
def generate_descend_point(self, flight: Flight,
|
||||
arrival: ControlPoint) -> FlightWaypoint:
|
||||
"""Generate approach/descend point.
|
||||
|
||||
Args:
|
||||
flight: The flight to generate the descend point for.
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder.descent(arrival)
|
||||
return builder.build()[0]
|
||||
|
||||
def generate_rtb_waypoint(self, arrival: ControlPoint) -> FlightWaypoint:
|
||||
def generate_rtb_waypoint(self, flight: Flight,
|
||||
arrival: ControlPoint) -> FlightWaypoint:
|
||||
"""Generate RTB landing point.
|
||||
|
||||
Args:
|
||||
flight: The flight to generate the landing waypoint for.
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder.land(arrival)
|
||||
return builder.build()[0]
|
||||
|
||||
|
||||
@ -22,12 +22,14 @@ CAP_DURATION = 30 # Minutes
|
||||
|
||||
INGRESS_TYPES = {
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_ESCORT,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
}
|
||||
|
||||
IP_TYPES = {
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_ESCORT,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
|
||||
@ -8,11 +8,12 @@ from dcs.unit import Unit
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.utils import nm_to_meter
|
||||
from theater import ControlPoint, MissionTarget, TheaterGroundObject
|
||||
from .flight import FlightWaypoint, FlightWaypointType
|
||||
from .flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
|
||||
|
||||
class WaypointBuilder:
|
||||
def __init__(self, doctrine: Doctrine) -> None:
|
||||
def __init__(self, flight: Flight, doctrine: Doctrine) -> None:
|
||||
self.flight = flight
|
||||
self.doctrine = doctrine
|
||||
self.waypoints: List[FlightWaypoint] = []
|
||||
self.ingress_point: Optional[FlightWaypoint] = None
|
||||
@ -127,6 +128,9 @@ class WaypointBuilder:
|
||||
def ingress_cas(self, position: Point, objective: MissionTarget) -> None:
|
||||
self._ingress(FlightWaypointType.INGRESS_CAS, position, objective)
|
||||
|
||||
def ingress_escort(self, position: Point, objective: MissionTarget) -> None:
|
||||
self._ingress(FlightWaypointType.INGRESS_ESCORT, position, objective)
|
||||
|
||||
def ingress_sead(self, position: Point, objective: MissionTarget) -> None:
|
||||
self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective)
|
||||
|
||||
@ -199,6 +203,9 @@ class WaypointBuilder:
|
||||
waypoint.description = description
|
||||
waypoint.pretty_name = description
|
||||
waypoint.name = name
|
||||
# The 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
|
||||
self.waypoints.append(waypoint)
|
||||
# TODO: This seems wrong, but it's what was there before.
|
||||
@ -231,6 +238,9 @@ class WaypointBuilder:
|
||||
waypoint.description = name
|
||||
waypoint.pretty_name = name
|
||||
waypoint.name = name
|
||||
# The 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
|
||||
self.waypoints.append(waypoint)
|
||||
# TODO: This seems wrong, but it's what was there before.
|
||||
@ -305,3 +315,33 @@ class WaypointBuilder:
|
||||
"""
|
||||
self.descent(arrival, is_helo)
|
||||
self.land(arrival)
|
||||
|
||||
def escort(self, ingress: Point, target: MissionTarget,
|
||||
egress: Point) -> None:
|
||||
"""Creates the waypoints needed to escort the package.
|
||||
|
||||
Args:
|
||||
ingress: The package ingress point.
|
||||
target: The mission target.
|
||||
egress: The package egress point.
|
||||
"""
|
||||
# This would preferably be no points at all, and instead the Escort task
|
||||
# would begin on the join point and end on the split point, however the
|
||||
# escort task does not appear to work properly (see the longer
|
||||
# description in gen.aircraft.JoinPointBuilder), so instead we give
|
||||
# the escort flights a flight plan including the ingress point, target
|
||||
# area, and egress point.
|
||||
self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
target.position.x,
|
||||
target.position.y,
|
||||
self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.name = "TARGET"
|
||||
waypoint.description = "Escort the package"
|
||||
waypoint.pretty_name = "Target area"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
self.egress(egress, target)
|
||||
|
||||
@ -111,19 +111,22 @@ class QFlightWaypointTab(QFrame):
|
||||
self.subwindow.show()
|
||||
|
||||
def on_ascend_waypoint(self):
|
||||
ascend = self.planner.generate_ascend_point(self.flight.from_cp)
|
||||
ascend = self.planner.generate_ascend_point(self.flight,
|
||||
self.flight.from_cp)
|
||||
self.flight.points.append(ascend)
|
||||
self.flight_waypoint_list.update_list()
|
||||
self.on_change()
|
||||
|
||||
def on_rtb_waypoint(self):
|
||||
rtb = self.planner.generate_rtb_waypoint(self.flight.from_cp)
|
||||
rtb = self.planner.generate_rtb_waypoint(self.flight,
|
||||
self.flight.from_cp)
|
||||
self.flight.points.append(rtb)
|
||||
self.flight_waypoint_list.update_list()
|
||||
self.on_change()
|
||||
|
||||
def on_descend_waypoint(self):
|
||||
descend = self.planner.generate_descend_point(self.flight.from_cp)
|
||||
descend = self.planner.generate_descend_point(self.flight,
|
||||
self.flight.from_cp)
|
||||
self.flight.points.append(descend)
|
||||
self.flight_waypoint_list.update_list()
|
||||
self.on_change()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user