Dan Albert 3862ec1b2e Convert escort request to a waypoint property.
Another step in reducing the rigidity of FlightPlan and making it
testable.

There is one intentional behavior change here: escort flights no longer
request escorts. That actually has a very minimal effect because these
properties are only used for two things: determining if a package needs
escorts or not, and determining when the TARCAP should show up and
leave. Since escorts won't have been in the package when the first part
happens anyway, that has no effect. The only change is that TARCAP won't
show up earlier or stay later just because of a TOT offset for an escort
flight.
2023-09-11 22:25:58 -07:00

164 lines
5.7 KiB
Python

from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, Type
from game.theater import FrontLine
from game.utils import Distance, Speed, kph, meters, dcs_to_shapely_point
from .ibuilder import IBuilder
from .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan, PatrollingLayout
from .uizonedisplay import UiZone, UiZoneDisplay
from .waypointbuilder import WaypointBuilder
from ..flightwaypointtype import FlightWaypointType
from ...flightplan.ipsolver import IpSolver
from ...persistence.paths import waypoint_debug_directory
if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
@dataclass(frozen=True)
class CasLayout(PatrollingLayout):
ingress: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to
yield self.ingress
yield self.patrol_start
yield self.patrol_end
yield from self.nav_from
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def patrol_duration(self) -> timedelta:
return self.flight.coalition.doctrine.cas_duration
@property
def patrol_speed(self) -> Speed:
# 2021-08-02: patrol_speed will currently have no effect because
# CAS doesn't use OrbitAction. But all PatrollingFlightPlan are expected
# to have patrol_speed
return kph(0)
@property
def engagement_distance(self) -> Distance:
from game.missiongenerator.frontlineconflictdescription import FRONTLINE_LENGTH
return meters(FRONTLINE_LENGTH) / 2
@property
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
return {self.layout.ingress, self.layout.patrol_start, self.layout.patrol_end}
def ui_zone(self) -> UiZone:
midpoint = (
self.layout.patrol_start.position + self.layout.patrol_end.position
) / 2
return UiZone(
[midpoint],
self.engagement_distance,
)
class Builder(IBuilder[CasFlightPlan, CasLayout]):
def layout(self, dump_debug_info: bool) -> CasLayout:
location = self.package.target
if not isinstance(location, FrontLine):
raise InvalidObjectiveLocation(self.flight.flight_type, location)
from game.missiongenerator.frontlineconflictdescription import (
FrontLineConflictDescription,
)
bounds = FrontLineConflictDescription.frontline_bounds(location, self.theater)
patrol_start = bounds.left_position
patrol_end = bounds.right_position
start_distance = patrol_start.distance_to_point(self.flight.departure.position)
end_distance = patrol_end.distance_to_point(self.flight.departure.position)
if end_distance < start_distance:
patrol_start, patrol_end = patrol_end, patrol_start
builder = WaypointBuilder(self.flight, self.coalition)
is_helo = self.flight.unit_type.dcs_unit_type.helicopter
patrol_altitude = self.doctrine.ingress_altitude if not is_helo else meters(50)
use_agl_patrol_altitude = is_helo
ip_solver = IpSolver(
dcs_to_shapely_point(self.flight.departure.position),
dcs_to_shapely_point(patrol_start),
self.doctrine,
self.threat_zones.all,
)
ip_solver.set_debug_properties(
waypoint_debug_directory() / "IP", self.theater.terrain
)
ingress_point_shapely = ip_solver.solve()
if dump_debug_info:
ip_solver.dump_debug_info()
ingress_point = patrol_start.new_in_same_map(
ingress_point_shapely.x, ingress_point_shapely.y
)
patrol_start_waypoint = builder.nav(
patrol_start, patrol_altitude, use_agl_patrol_altitude
)
patrol_start_waypoint.name = "FLOT START"
patrol_start_waypoint.pretty_name = "FLOT start"
patrol_start_waypoint.description = "FLOT boundary"
patrol_start_waypoint.wants_escort = True
patrol_end_waypoint = builder.nav(
patrol_end, patrol_altitude, use_agl_patrol_altitude
)
patrol_end_waypoint.name = "FLOT END"
patrol_end_waypoint.pretty_name = "FLOT end"
patrol_end_waypoint.description = "FLOT boundary"
patrol_end_waypoint.wants_escort = True
ingress = builder.ingress(
FlightWaypointType.INGRESS_CAS, ingress_point, location
)
ingress.description = f"Ingress to provide CAS at {location}"
return CasLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position,
ingress_point,
patrol_altitude,
use_agl_patrol_altitude,
),
nav_from=builder.nav_path(
patrol_end,
self.flight.arrival.position,
patrol_altitude,
use_agl_patrol_altitude,
),
ingress=ingress,
patrol_start=patrol_start_waypoint,
patrol_end=patrol_end_waypoint,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
def build(self, dump_debug_info: bool = False) -> CasFlightPlan:
return CasFlightPlan(self.flight, self.layout(dump_debug_info))