mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
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.
218 lines
7.4 KiB
Python
218 lines
7.4 KiB
Python
from __future__ import annotations
|
|
|
|
from abc import ABC
|
|
from collections.abc import Iterator
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timedelta
|
|
from typing import TYPE_CHECKING, TypeVar
|
|
|
|
from dcs import Point
|
|
|
|
from game.flightplan import HoldZoneGeometry
|
|
from game.theater import MissionTarget
|
|
from game.utils import Speed, meters
|
|
from .flightplan import FlightPlan
|
|
from .formation import FormationFlightPlan, FormationLayout
|
|
from .ibuilder import IBuilder
|
|
from .waypointbuilder import StrikeTarget, WaypointBuilder
|
|
from .. import FlightType
|
|
from ..flightwaypoint import FlightWaypoint
|
|
from ..flightwaypointtype import FlightWaypointType
|
|
|
|
if TYPE_CHECKING:
|
|
from ..flight import Flight
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FormationAttackLayout(FormationLayout):
|
|
ingress: FlightWaypoint
|
|
targets: list[FlightWaypoint]
|
|
|
|
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
|
yield self.departure
|
|
yield self.hold
|
|
yield from self.nav_to
|
|
yield self.join
|
|
yield self.ingress
|
|
yield from self.targets
|
|
yield self.split
|
|
if self.refuel is not None:
|
|
yield self.refuel
|
|
yield from self.nav_from
|
|
yield self.arrival
|
|
if self.divert is not None:
|
|
yield self.divert
|
|
yield self.bullseye
|
|
|
|
|
|
class FormationAttackFlightPlan(FormationFlightPlan[FormationAttackLayout], ABC):
|
|
@property
|
|
def package_speed_waypoints(self) -> set[FlightWaypoint]:
|
|
return {
|
|
self.layout.ingress,
|
|
self.layout.split,
|
|
} | set(self.layout.targets)
|
|
|
|
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
|
|
# FlightWaypoint is only comparable by identity, so adding
|
|
# target_area_waypoint to package_speed_waypoints is useless.
|
|
if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
|
|
# Should be impossible, as any package with at least one
|
|
# FormationFlightPlan flight needs a formation speed.
|
|
assert self.package.formation_speed is not None
|
|
return self.package.formation_speed
|
|
return super().speed_between_waypoints(a, b)
|
|
|
|
@property
|
|
def tot_waypoint(self) -> FlightWaypoint:
|
|
return self.layout.targets[0]
|
|
|
|
@property
|
|
def target_area_waypoint(self) -> FlightWaypoint:
|
|
return FlightWaypoint(
|
|
"TARGET AREA",
|
|
FlightWaypointType.TARGET_GROUP_LOC,
|
|
self.package.target.position,
|
|
meters(0),
|
|
"RADIO",
|
|
)
|
|
|
|
@property
|
|
def join_time(self) -> datetime:
|
|
travel_time = self.total_time_between_waypoints(
|
|
self.layout.join, self.layout.ingress
|
|
)
|
|
return self.ingress_time - travel_time
|
|
|
|
@property
|
|
def split_time(self) -> datetime:
|
|
travel_time_ingress = self.total_time_between_waypoints(
|
|
self.layout.ingress, self.target_area_waypoint
|
|
)
|
|
travel_time_egress = self.total_time_between_waypoints(
|
|
self.target_area_waypoint, self.layout.split
|
|
)
|
|
minutes_at_target = 0.75 * len(self.layout.targets)
|
|
timedelta_at_target = timedelta(minutes=minutes_at_target)
|
|
return (
|
|
self.ingress_time
|
|
+ travel_time_ingress
|
|
+ timedelta_at_target
|
|
+ travel_time_egress
|
|
)
|
|
|
|
@property
|
|
def ingress_time(self) -> datetime:
|
|
tot = self.tot
|
|
travel_time = self.total_time_between_waypoints(
|
|
self.layout.ingress, self.target_area_waypoint
|
|
)
|
|
return tot - travel_time
|
|
|
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
|
if waypoint == self.layout.ingress:
|
|
return self.ingress_time
|
|
elif waypoint in self.layout.targets:
|
|
return self.tot
|
|
return super().tot_for_waypoint(waypoint)
|
|
|
|
|
|
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[FormationAttackLayout])
|
|
LayoutT = TypeVar("LayoutT", bound=FormationAttackLayout)
|
|
|
|
|
|
class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|
def _build(
|
|
self,
|
|
ingress_type: FlightWaypointType,
|
|
targets: list[StrikeTarget] | None = None,
|
|
) -> FormationAttackLayout:
|
|
assert self.package.waypoints is not None
|
|
builder = WaypointBuilder(self.flight, self.coalition, targets)
|
|
|
|
target_waypoints: list[FlightWaypoint] = []
|
|
if targets is not None:
|
|
for target in targets:
|
|
target_waypoints.append(
|
|
self.target_waypoint(self.flight, builder, target)
|
|
)
|
|
else:
|
|
target_waypoints.append(
|
|
self.target_area_waypoint(
|
|
self.flight, self.flight.package.target, builder
|
|
)
|
|
)
|
|
|
|
hold = builder.hold(self._hold_point())
|
|
join = builder.join(self.package.waypoints.join)
|
|
join.wants_escort = True
|
|
|
|
ingress = builder.ingress(
|
|
ingress_type, self.package.waypoints.ingress, self.package.target
|
|
)
|
|
ingress.wants_escort = True
|
|
|
|
for target_waypoint in target_waypoints:
|
|
target_waypoint.wants_escort = True
|
|
|
|
split = builder.split(self.package.waypoints.split)
|
|
split.wants_escort = True
|
|
refuel = builder.refuel(self.package.waypoints.refuel)
|
|
|
|
return FormationAttackLayout(
|
|
departure=builder.takeoff(self.flight.departure),
|
|
hold=hold,
|
|
nav_to=builder.nav_path(
|
|
hold.position, join.position, self.doctrine.ingress_altitude
|
|
),
|
|
join=join,
|
|
ingress=ingress,
|
|
targets=target_waypoints,
|
|
split=split,
|
|
refuel=refuel,
|
|
nav_from=builder.nav_path(
|
|
refuel.position,
|
|
self.flight.arrival.position,
|
|
self.doctrine.ingress_altitude,
|
|
),
|
|
arrival=builder.land(self.flight.arrival),
|
|
divert=builder.divert(self.flight.divert),
|
|
bullseye=builder.bullseye(),
|
|
)
|
|
|
|
@staticmethod
|
|
def target_waypoint(
|
|
flight: Flight, builder: WaypointBuilder, target: StrikeTarget
|
|
) -> FlightWaypoint:
|
|
if flight.flight_type in {FlightType.ANTISHIP, FlightType.BAI}:
|
|
return builder.bai_group(target)
|
|
elif flight.flight_type == FlightType.DEAD:
|
|
return builder.dead_point(target)
|
|
elif flight.flight_type == FlightType.SEAD:
|
|
return builder.sead_point(target)
|
|
else:
|
|
return builder.strike_point(target)
|
|
|
|
@staticmethod
|
|
def target_area_waypoint(
|
|
flight: Flight, location: MissionTarget, builder: WaypointBuilder
|
|
) -> FlightWaypoint:
|
|
if flight.flight_type == FlightType.DEAD:
|
|
return builder.dead_area(location)
|
|
elif flight.flight_type == FlightType.SEAD:
|
|
return builder.sead_area(location)
|
|
elif flight.flight_type == FlightType.OCA_AIRCRAFT:
|
|
return builder.oca_strike_area(location)
|
|
else:
|
|
return builder.strike_area(location)
|
|
|
|
def _hold_point(self) -> Point:
|
|
assert self.package.waypoints is not None
|
|
origin = self.flight.departure.position
|
|
target = self.package.target.position
|
|
join = self.package.waypoints.join
|
|
ip = self.package.waypoints.ingress
|
|
return HoldZoneGeometry(
|
|
target, origin, ip, join, self.coalition, self.theater
|
|
).find_best_hold_point()
|