diff --git a/changelog.md b/changelog.md index 178296a5..88413c68 100644 --- a/changelog.md +++ b/changelog.md @@ -51,6 +51,7 @@ * **[Capture Logic]** Release all parking slots when an airbase is captured * **[Modding]** Swedish Military Assets Pack air defence presets are now correctly removed from the faction when the mod is disabled. * **[Mission Generation]** Naval aircraft not always returning to carrier +* **[Mission Generation]** AI AirLift aircraft crashing into terrain due to insufficient waypoints # Retribution v1.2.1 (hotfix) diff --git a/game/ato/flightplans/airlift.py b/game/ato/flightplans/airlift.py index 1cc15d84..106ded52 100644 --- a/game/ato/flightplans/airlift.py +++ b/game/ato/flightplans/airlift.py @@ -1,5 +1,6 @@ from __future__ import annotations +import random from collections.abc import Iterator from dataclasses import dataclass from datetime import datetime @@ -7,7 +8,7 @@ from typing import Optional from typing import TYPE_CHECKING, Type from game.theater.missiontarget import MissionTarget -from game.utils import feet +from game.utils import feet, Distance from ._common_ctld import generate_random_ctld_point from .ibuilder import IBuilder from .planningerror import PlanningError @@ -23,13 +24,17 @@ if TYPE_CHECKING: @dataclass class AirliftLayout(StandardLayout): + pickup_ascent: FlightWaypoint | None + pickup_descent: FlightWaypoint | None # There will not be a pickup waypoint when the pickup airfield is the departure # airfield for cargo planes, as the cargo is pre-loaded. Helicopters will still pick # up the cargo near the airfield. pickup: FlightWaypoint | None # pickup_zone will be used for player flights to create the CTLD stuff ctld_pickup_zone: FlightWaypoint | None + drop_off_ascent: FlightWaypoint | None nav_to_drop_off: list[FlightWaypoint] + drop_off_descent: FlightWaypoint | None # There will not be a drop-off waypoint when the drop-off airfield and the arrival # airfield is the same for a cargo plane, as planes will land to unload and we don't # want a double landing. Helicopters will still drop their cargo near the airfield @@ -37,6 +42,8 @@ class AirliftLayout(StandardLayout): drop_off: FlightWaypoint | None # drop_off_zone will be used for player flights to create the CTLD stuff ctld_drop_off_zone: FlightWaypoint | None + return_ascent: FlightWaypoint | None + return_descent: FlightWaypoint | None def add_waypoint( self, wpt: FlightWaypoint, next_wpt: Optional[FlightWaypoint] @@ -60,17 +67,29 @@ class AirliftLayout(StandardLayout): def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.departure + if self.pickup_ascent is not None: + yield self.pickup_ascent yield from self.nav_to + if self.pickup_descent is not None: + yield self.pickup_descent if self.pickup is not None: yield self.pickup if self.ctld_pickup_zone is not None: yield self.ctld_pickup_zone + if self.drop_off_ascent is not None: + yield self.drop_off_ascent yield from self.nav_to_drop_off + if self.drop_off_descent is not None: + yield self.drop_off_descent if self.drop_off is not None: yield self.drop_off + if self.return_ascent is not None: + yield self.return_ascent if self.ctld_drop_off_zone is not None: yield self.ctld_drop_off_zone yield from self.nav_from + if self.return_descent is not None: + yield self.return_descent yield self.arrival if self.divert is not None: yield self.divert @@ -121,15 +140,47 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]): builder = WaypointBuilder(self.flight) + pickup_ascent = None + pickup_descent = None pickup = None + drop_off_ascent = None + drop_off_descent = None drop_off = None pickup_zone = None drop_off_zone = None if cargo.origin != self.flight.departure: pickup = builder.cargo_stop(cargo.origin) + pickup_ascent = self._create_ascent_or_descent( + builder, + self.flight.departure.position, + cargo.origin.position, + altitude, + altitude_is_agl, + ) + pickup_descent = self._create_ascent_or_descent( + builder, + cargo.origin.position, + self.flight.departure.position, + altitude, + altitude_is_agl, + ) if cargo.next_stop != self.flight.arrival: drop_off = builder.cargo_stop(cargo.next_stop) + drop_off_ascent = self._create_ascent_or_descent( + builder, + cargo.origin.position, + cargo.next_stop.position, + altitude, + altitude_is_agl, + ) + drop_off_descent = self._create_ascent_or_descent( + builder, + cargo.next_stop.position, + cargo.origin.position, + altitude, + altitude_is_agl, + ) if self.flight.is_helo: # Create CTLD Zones for Helo flights @@ -150,25 +201,50 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]): altitude_is_agl, ) + return_ascent = self._create_ascent_or_descent( + builder, + cargo.next_stop.position + if cargo.next_stop != self.flight.arrival + else cargo.origin.position, + self.flight.arrival.position, + altitude, + altitude_is_agl, + ) + return_descent = self._create_ascent_or_descent( + builder, + self.flight.arrival.position, + cargo.next_stop.position + if cargo.next_stop != self.flight.arrival + else cargo.origin.position, + altitude, + altitude_is_agl, + ) + return AirliftLayout( departure=builder.takeoff(self.flight.departure), + pickup_ascent=pickup_ascent, nav_to=nav_to_pickup, + pickup_descent=pickup_descent, pickup=pickup, ctld_pickup_zone=pickup_zone, + drop_off_ascent=drop_off_ascent, nav_to_drop_off=builder.nav_path( cargo.origin.position, cargo.next_stop.position, altitude, altitude_is_agl, ), + drop_off_descent=drop_off_descent, drop_off=drop_off, ctld_drop_off_zone=drop_off_zone, + return_ascent=return_ascent, nav_from=builder.nav_path( cargo.origin.position, self.flight.arrival.position, altitude, altitude_is_agl, ), + return_descent=return_descent, arrival=builder.land(self.flight.arrival), divert=builder.divert(self.flight.divert), bullseye=builder.bullseye(), @@ -188,3 +264,18 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]): if cargo and cargo.transport and isinstance(cargo.transport.destination, CTLD): return generate_random_ctld_point(cargo.transport.destination) raise RuntimeError("Could not generate CTLD dropoff") + + @staticmethod + def _create_ascent_or_descent( + builder: WaypointBuilder, + start: Point, + end: Point, + alt: Distance, + agl: bool, + ) -> FlightWaypoint: + distance = start.distance_to_point(end) + rdistance = 1000 if agl else min(distance / 10, 20000) + heading = round(start.heading_between_point(end)) + rheading = random.randint(heading - 30, heading + 30) % 360 + pos = start.point_from_heading(float(rheading), rdistance) + return builder.nav(pos, alt, agl) diff --git a/game/ato/flightplans/waypointbuilder.py b/game/ato/flightplans/waypointbuilder.py index f60962e9..ba63601b 100644 --- a/game/ato/flightplans/waypointbuilder.py +++ b/game/ato/flightplans/waypointbuilder.py @@ -15,7 +15,6 @@ from typing import ( from dcs.mapping import Point, Vector2 -from game.ato.flightplans._common_ctld import generate_random_ctld_point from game.ato.flightwaypoint import AltitudeReference, FlightWaypoint from game.ato.flightwaypointtype import FlightWaypointType from game.theater import ( @@ -25,7 +24,6 @@ from game.theater import ( TheaterGroundObject, TheaterUnit, ) -from game.theater.interfaces.CTLD import CTLD from game.utils import Distance, meters, nautical_miles, feet if TYPE_CHECKING: @@ -635,14 +633,10 @@ class WaypointBuilder: """Creates a cargo stop waypoint. This waypoint is used by AirLift as a landing and stopover waypoint """ - if isinstance(control_point, CTLD) and control_point.ctld_zones: - pos = generate_random_ctld_point(control_point) - else: - pos = control_point.position return FlightWaypoint( "CARGOSTOP", FlightWaypointType.CARGO_STOP, - pos, + control_point.position, meters(0), "RADIO", description=f"Stop for cargo at {control_point.name}",