Dan Albert a2630fc75f
Add a real CAS ingress point.
Putting the ingress point directly on one end of the FLOT means that AI
flights won't start searching and engaging targets until they reach that
point. If the front line has advanced toward the flight's departure
airfield, it might overfly targets on its way to the IP.

Instead, place an IP for CAS the same way we place any other IP. The AI
will fly to that and start searching from there.

This also:

* Removes the midpoint waypoint, since it didn't serve any real purpose
* Names the FLOT boundary waypoints for what they actually are

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2231.
2023-10-07 18:00:07 +02:00

181 lines
6.2 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from typing import Iterator, TYPE_CHECKING, Type
from game.theater.controlpoint import ControlPointType
from game.theater.missiontarget import MissionTarget
from game.utils import Distance, feet, meters
from ._common_ctld import generate_random_ctld_point
from .formationattack import (
FormationAttackLayout,
FormationAttackBuilder,
FormationAttackFlightPlan,
)
from .planningerror import PlanningError
from .uizonedisplay import UiZone, UiZoneDisplay
from .waypointbuilder import WaypointBuilder
from ..flightwaypoint import FlightWaypointType
from ...theater.interfaces.CTLD import CTLD
if TYPE_CHECKING:
from dcs import Point
from ..flightwaypoint import FlightWaypoint
@dataclass
class AirAssaultLayout(FormationAttackLayout):
# The pickup point is optional because we don't always need to load the cargo. When
# departing from a carrier, LHA, or off-map spawn, the cargo is pre-loaded.
pickup: FlightWaypoint | None = None
drop_off: FlightWaypoint | None = None
# This is an implementation detail used by CTLD. The aircraft will not go to this
# waypoint. It is used by CTLD as the destination for unloaded troops.
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
if self.pickup is not None:
yield self.pickup
yield from self.nav_to
yield self.ingress
if self.drop_off is not None:
yield self.drop_off
yield self.targets[0]
yield from self.nav_from
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def is_airassault(self) -> bool:
return True
@property
def tot_waypoint(self) -> FlightWaypoint:
if self.flight.is_helo and self.layout.drop_off is not None:
return self.layout.drop_off
return self.layout.targets[0]
@property
def ingress_time(self) -> timedelta:
tot = self.tot
travel_time = self.travel_time_between_waypoints(
self.layout.ingress, self.tot_waypoint
)
return tot - travel_time
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint is self.tot_waypoint:
return self.tot
elif waypoint is self.layout.ingress:
return self.ingress_time
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
return None
@property
def ctld_target_zone_radius(self) -> Distance:
return meters(2500)
@property
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target
def ui_zone(self) -> UiZone:
return UiZone(
[self.layout.targets[0].position],
self.ctld_target_zone_radius,
)
class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
def layout(self) -> AirAssaultLayout:
if not self.flight.is_helo and not self.flight.is_hercules:
raise PlanningError(
"Air assault is only usable by helicopters and Anubis' C-130 mod"
)
assert self.package.waypoints is not None
heli_alt = feet(self.coalition.game.settings.heli_cruise_alt_agl)
altitude = heli_alt if self.flight.is_helo else self.doctrine.ingress_altitude
altitude_is_agl = self.flight.is_helo
builder = WaypointBuilder(self.flight)
if self.flight.is_hercules or self.flight.departure.cptype in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
ControlPointType.OFF_MAP,
]:
# Off_Map spawns will be preloaded
# Carrier operations load the logistics directly from the carrier
pickup = None
pickup_position = self.flight.departure.position
else:
pickup = builder.pickup_zone(
MissionTarget(
"Pickup Zone",
self._generate_ctld_pickup(),
)
)
pickup_position = pickup.position
assault_area = builder.assault_area(self.package.target)
heading = self.package.target.position.heading_between_point(pickup_position)
if self.flight.is_hercules:
assault_area.only_for_player = False
assault_area.alt = feet(1000)
# TODO: define CTLD dropoff zones in campaign miz?
drop_off_zone = MissionTarget(
"Dropoff zone",
self.package.target.position.point_from_heading(heading, 1200),
)
dz = builder.dropoff_zone(drop_off_zone) if self.flight.is_helo else None
return AirAssaultLayout(
departure=builder.takeoff(self.flight.departure),
pickup=pickup,
nav_to=builder.nav_path(
pickup_position,
self.package.waypoints.ingress,
altitude,
altitude_is_agl,
),
ingress=builder.ingress(
FlightWaypointType.INGRESS_AIR_ASSAULT,
self.package.waypoints.ingress,
self.package.target,
),
drop_off=dz,
targets=[assault_area],
nav_from=builder.nav_path(
drop_off_zone.position,
self.flight.arrival.position,
altitude,
altitude_is_agl,
),
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
hold=None,
join=builder.join(pickup_position),
split=builder.split(self.package.waypoints.split),
refuel=None,
)
def build(self, dump_debug_info: bool = False) -> AirAssaultFlightPlan:
return AirAssaultFlightPlan(self.flight, self.layout())
def _generate_ctld_pickup(self) -> Point:
assert isinstance(self.flight.departure, CTLD)
return generate_random_ctld_point(self.flight.departure)