mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Move FlightPlan instantiation into the builder.
I'm working on moving the builder to be owned by the Flight, which will simplify callers that need to create (or recreate) flight plans for a flight.
This commit is contained in:
parent
3dd0e4a66f
commit
4521053804
@ -194,8 +194,9 @@ class Flight(SidcDescribable):
|
||||
def abort(self) -> None:
|
||||
from .flightplans.rtb import RtbFlightPlan
|
||||
|
||||
layout = RtbFlightPlan.builder_type()(self, self.coalition.game.theater).build()
|
||||
self.flight_plan = RtbFlightPlan(self, layout)
|
||||
self.flight_plan = RtbFlightPlan.builder_type()(
|
||||
self, self.coalition.game.theater
|
||||
).build()
|
||||
|
||||
self.set_state(
|
||||
Navigating(
|
||||
|
||||
@ -9,8 +9,31 @@ from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
||||
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
|
||||
|
||||
|
||||
class Builder(IBuilder):
|
||||
def build(self) -> PatrollingLayout:
|
||||
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
return timedelta(hours=4)
|
||||
|
||||
@property
|
||||
def patrol_speed(self) -> Speed:
|
||||
altitude = self.layout.patrol_start.alt
|
||||
if self.flight.unit_type.preferred_patrol_speed(altitude) is not None:
|
||||
return self.flight.unit_type.preferred_patrol_speed(altitude)
|
||||
return knots(390)
|
||||
|
||||
@property
|
||||
def engagement_distance(self) -> Distance:
|
||||
# TODO: Factor out a common base of the combat and non-combat race-tracks.
|
||||
# No harm in setting this, but we ought to clean up a bit.
|
||||
return meters(0)
|
||||
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
|
||||
def layout(self) -> PatrollingLayout:
|
||||
racetrack_half_distance = nautical_miles(30).meters
|
||||
|
||||
location = self.package.target
|
||||
@ -67,25 +90,5 @@ class Builder(IBuilder):
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
|
||||
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
return timedelta(hours=4)
|
||||
|
||||
@property
|
||||
def patrol_speed(self) -> Speed:
|
||||
altitude = self.layout.patrol_start.alt
|
||||
if self.flight.unit_type.preferred_patrol_speed(altitude) is not None:
|
||||
return self.flight.unit_type.preferred_patrol_speed(altitude)
|
||||
return knots(390)
|
||||
|
||||
@property
|
||||
def engagement_distance(self) -> Distance:
|
||||
# TODO: Factor out a common base of the combat and non-combat race-tracks.
|
||||
# No harm in setting this, but we ought to clean up a bit.
|
||||
return meters(0)
|
||||
|
||||
@staticmethod
|
||||
def builder_type() -> Type[IBuilder]:
|
||||
return Builder
|
||||
def build(self) -> AewcFlightPlan:
|
||||
return AewcFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -2,7 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Iterator, Type
|
||||
from typing import Iterator, TYPE_CHECKING, Type
|
||||
|
||||
from game.ato.flightplans.airlift import AirliftLayout
|
||||
from game.ato.flightplans.standard import StandardFlightPlan
|
||||
from game.theater.controlpoint import ControlPointType
|
||||
@ -16,8 +17,57 @@ if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
class Builder(IBuilder):
|
||||
def build(self) -> AirAssaultLayout:
|
||||
@dataclass(frozen=True)
|
||||
class AirAssaultLayout(AirliftLayout):
|
||||
target: FlightWaypoint
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to_pickup
|
||||
if self.pickup:
|
||||
yield self.pickup
|
||||
yield from self.nav_to_drop_off
|
||||
yield self.drop_off
|
||||
yield self.target
|
||||
yield from self.nav_to_home
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
|
||||
def __init__(self, flight: Flight, layout: AirAssaultLayout) -> None:
|
||||
super().__init__(flight, layout)
|
||||
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint | None:
|
||||
return self.layout.drop_off
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
if waypoint == self.tot_waypoint:
|
||||
return self.tot
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def engagement_distance(self) -> Distance:
|
||||
# The radius of the WaypointZone created at the target location
|
||||
return meters(2500)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
|
||||
|
||||
class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
def layout(self) -> AirAssaultLayout:
|
||||
|
||||
altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude
|
||||
altitude_is_agl = self.flight.is_helo
|
||||
@ -78,51 +128,5 @@ class Builder(IBuilder):
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirAssaultLayout(AirliftLayout):
|
||||
target: FlightWaypoint
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to_pickup
|
||||
if self.pickup:
|
||||
yield self.pickup
|
||||
yield from self.nav_to_drop_off
|
||||
yield self.drop_off
|
||||
yield self.target
|
||||
yield from self.nav_to_home
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
|
||||
def __init__(self, flight: Flight, layout: AirAssaultLayout) -> None:
|
||||
super().__init__(flight, layout)
|
||||
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint | None:
|
||||
return self.layout.drop_off
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
if waypoint == self.tot_waypoint:
|
||||
return self.tot
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def engagement_distance(self) -> Distance:
|
||||
# The radius of the WaypointZone created at the target location
|
||||
return meters(2500)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
def build(self) -> AirAssaultFlightPlan:
|
||||
return AirAssaultFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -4,8 +4,8 @@ from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Type
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.utils import feet
|
||||
from .ibuilder import IBuilder
|
||||
from .planningerror import PlanningError
|
||||
@ -17,8 +17,58 @@ if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
class Builder(IBuilder):
|
||||
def build(self) -> AirliftLayout:
|
||||
@dataclass(frozen=True)
|
||||
class AirliftLayout(StandardLayout):
|
||||
nav_to_pickup: list[FlightWaypoint]
|
||||
pickup: FlightWaypoint | None
|
||||
nav_to_drop_off: list[FlightWaypoint]
|
||||
drop_off: FlightWaypoint
|
||||
stopover: FlightWaypoint | None
|
||||
nav_to_home: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to_pickup
|
||||
if self.pickup is not None:
|
||||
yield self.pickup
|
||||
yield from self.nav_to_drop_off
|
||||
yield self.drop_off
|
||||
if self.stopover is not None:
|
||||
yield self.stopover
|
||||
yield from self.nav_to_home
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
|
||||
def __init__(self, flight: Flight, layout: AirliftLayout) -> None:
|
||||
super().__init__(flight, layout)
|
||||
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint | None:
|
||||
return self.layout.drop_off
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
# TOT planning isn't really useful for transports. They're behind the front
|
||||
# lines so no need to wait for escorts or for other missions to complete.
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
|
||||
|
||||
class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
def layout(self) -> AirliftLayout:
|
||||
cargo = self.flight.cargo
|
||||
if cargo is None:
|
||||
raise PlanningError(
|
||||
@ -97,52 +147,5 @@ class Builder(IBuilder):
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirliftLayout(StandardLayout):
|
||||
nav_to_pickup: list[FlightWaypoint]
|
||||
pickup: FlightWaypoint | None
|
||||
nav_to_drop_off: list[FlightWaypoint]
|
||||
drop_off: FlightWaypoint
|
||||
stopover: FlightWaypoint | None
|
||||
nav_to_home: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to_pickup
|
||||
if self.pickup is not None:
|
||||
yield self.pickup
|
||||
yield from self.nav_to_drop_off
|
||||
yield self.drop_off
|
||||
if self.stopover is not None:
|
||||
yield self.stopover
|
||||
yield from self.nav_to_home
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
|
||||
def __init__(self, flight: Flight, layout: AirliftLayout) -> None:
|
||||
super().__init__(flight, layout)
|
||||
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint | None:
|
||||
return self.layout.drop_off
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
# TOT planning isn't really useful for transports. They're behind the front
|
||||
# lines so no need to wait for escorts or for other missions to complete.
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
def build(self) -> AirliftFlightPlan:
|
||||
return AirliftFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -20,8 +20,8 @@ class AntiShipFlightPlan(FormationAttackFlightPlan):
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder):
|
||||
def build(self) -> FormationAttackLayout:
|
||||
class Builder(FormationAttackBuilder[AntiShipFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
location = self.package.target
|
||||
|
||||
from game.transfers import CargoShip
|
||||
@ -40,3 +40,6 @@ class Builder(FormationAttackBuilder):
|
||||
@staticmethod
|
||||
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]:
|
||||
return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups]
|
||||
|
||||
def build(self) -> AntiShipFlightPlan:
|
||||
return AntiShipFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -19,8 +19,8 @@ class BaiFlightPlan(FormationAttackFlightPlan):
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder):
|
||||
def build(self) -> FormationAttackLayout:
|
||||
class Builder(FormationAttackBuilder[BaiFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
location = self.package.target
|
||||
|
||||
from game.transfers import Convoy
|
||||
@ -38,3 +38,6 @@ class Builder(FormationAttackBuilder):
|
||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||
|
||||
return self._build(FlightWaypointType.INGRESS_BAI, targets)
|
||||
|
||||
def build(self) -> BaiFlightPlan:
|
||||
return BaiFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -12,8 +12,28 @@ from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
|
||||
|
||||
class Builder(CapBuilder):
|
||||
def build(self) -> PatrollingLayout:
|
||||
class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
return self.flight.coalition.doctrine.cap_duration
|
||||
|
||||
@property
|
||||
def patrol_speed(self) -> Speed:
|
||||
return self.flight.unit_type.preferred_patrol_speed(
|
||||
self.layout.patrol_start.alt
|
||||
)
|
||||
|
||||
@property
|
||||
def engagement_distance(self) -> Distance:
|
||||
return self.flight.coalition.doctrine.cap_engagement_range
|
||||
|
||||
|
||||
class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
|
||||
def layout(self) -> PatrollingLayout:
|
||||
location = self.package.target
|
||||
|
||||
if isinstance(location, FrontLine):
|
||||
@ -46,22 +66,5 @@ class Builder(CapBuilder):
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
|
||||
class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
return self.flight.coalition.doctrine.cap_duration
|
||||
|
||||
@property
|
||||
def patrol_speed(self) -> Speed:
|
||||
return self.flight.unit_type.preferred_patrol_speed(
|
||||
self.layout.patrol_start.alt
|
||||
)
|
||||
|
||||
@property
|
||||
def engagement_distance(self) -> Distance:
|
||||
return self.flight.coalition.doctrine.cap_engagement_range
|
||||
def build(self) -> BarCapFlightPlan:
|
||||
return BarCapFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -2,12 +2,14 @@ from __future__ import annotations
|
||||
|
||||
import random
|
||||
from abc import ABC
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any, TYPE_CHECKING, TypeVar
|
||||
|
||||
from dcs import Point
|
||||
from shapely.geometry import Point as ShapelyPoint
|
||||
|
||||
from game.utils import Heading, meters, nautical_miles
|
||||
from .flightplan import FlightPlan
|
||||
from .patrolling import PatrollingLayout
|
||||
from ..closestairfields import ObjectiveDistanceCache
|
||||
from ..flightplans.ibuilder import IBuilder
|
||||
from ..flightplans.planningerror import PlanningError
|
||||
@ -15,8 +17,11 @@ from ..flightplans.planningerror import PlanningError
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import MissionTarget
|
||||
|
||||
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[Any])
|
||||
LayoutT = TypeVar("LayoutT", bound=PatrollingLayout)
|
||||
|
||||
class CapBuilder(IBuilder, ABC):
|
||||
|
||||
class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
def cap_racetrack_for_objective(
|
||||
self, location: MissionTarget, barcap: bool
|
||||
) -> tuple[Point, Point]:
|
||||
|
||||
@ -17,8 +17,58 @@ if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
class Builder(IBuilder):
|
||||
def build(self) -> CasLayout:
|
||||
@dataclass(frozen=True)
|
||||
class CasLayout(PatrollingLayout):
|
||||
target: FlightWaypoint
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to
|
||||
yield self.patrol_start
|
||||
yield self.target
|
||||
yield self.patrol_end
|
||||
yield from self.nav_from
|
||||
yield self.departure
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
class CasFlightPlan(PatrollingFlightPlan[CasLayout]):
|
||||
@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.patrol_start, self.layout.target, self.layout.patrol_end}
|
||||
|
||||
def request_escort_at(self) -> FlightWaypoint | None:
|
||||
return self.layout.patrol_start
|
||||
|
||||
def dismiss_escort_at(self) -> FlightWaypoint | None:
|
||||
return self.layout.patrol_end
|
||||
|
||||
|
||||
class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
||||
def layout(self) -> CasLayout:
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, FrontLine):
|
||||
@ -71,52 +121,5 @@ class Builder(IBuilder):
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CasLayout(PatrollingLayout):
|
||||
target: FlightWaypoint
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to
|
||||
yield self.patrol_start
|
||||
yield self.target
|
||||
yield self.patrol_end
|
||||
yield from self.nav_from
|
||||
yield self.departure
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
class CasFlightPlan(PatrollingFlightPlan[CasLayout]):
|
||||
@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.patrol_start, self.layout.target, self.layout.patrol_end}
|
||||
|
||||
def request_escort_at(self) -> FlightWaypoint | None:
|
||||
return self.layout.patrol_start
|
||||
|
||||
def dismiss_escort_at(self) -> FlightWaypoint | None:
|
||||
return self.layout.patrol_end
|
||||
def build(self) -> CasFlightPlan:
|
||||
return CasFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -13,11 +13,6 @@ if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
class Builder(IBuilder):
|
||||
def build(self) -> CustomLayout:
|
||||
return CustomLayout([])
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CustomLayout(Layout):
|
||||
custom_waypoints: list[FlightWaypoint]
|
||||
@ -55,3 +50,11 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
|
||||
|
||||
class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
|
||||
def layout(self) -> CustomLayout:
|
||||
return CustomLayout([])
|
||||
|
||||
def build(self) -> CustomFlightPlan:
|
||||
return CustomFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -22,8 +22,8 @@ class DeadFlightPlan(FormationAttackFlightPlan):
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder):
|
||||
def build(self) -> FormationAttackLayout:
|
||||
class Builder(FormationAttackBuilder[DeadFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
location = self.package.target
|
||||
|
||||
is_ewr = isinstance(location, EwrGroundObject)
|
||||
@ -36,3 +36,6 @@ class Builder(FormationAttackBuilder):
|
||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||
|
||||
return self._build(FlightWaypointType.INGRESS_DEAD)
|
||||
|
||||
def build(self) -> DeadFlightPlan:
|
||||
return DeadFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -16,8 +16,8 @@ class EscortFlightPlan(FormationAttackFlightPlan):
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder):
|
||||
def build(self) -> FormationAttackLayout:
|
||||
class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
@ -51,3 +51,6 @@ class Builder(FormationAttackBuilder):
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
def build(self) -> EscortFlightPlan:
|
||||
return EscortFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -15,36 +15,6 @@ if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
class Builder(IBuilder):
|
||||
def build(self) -> FerryLayout:
|
||||
if self.flight.departure == self.flight.arrival:
|
||||
raise PlanningError(
|
||||
f"Cannot plan ferry self.flight: departure and arrival are both "
|
||||
f"{self.flight.departure}"
|
||||
)
|
||||
|
||||
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
|
||||
altitude = (
|
||||
feet(1500)
|
||||
if altitude_is_agl
|
||||
else self.flight.unit_type.preferred_patrol_altitude
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
return FerryLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to_destination=builder.nav_path(
|
||||
self.flight.departure.position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FerryLayout(StandardLayout):
|
||||
nav_to_destination: list[FlightWaypoint]
|
||||
@ -78,3 +48,36 @@ class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
|
||||
|
||||
class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
|
||||
def layout(self) -> FerryLayout:
|
||||
if self.flight.departure == self.flight.arrival:
|
||||
raise PlanningError(
|
||||
f"Cannot plan ferry self.flight: departure and arrival are both "
|
||||
f"{self.flight.departure}"
|
||||
)
|
||||
|
||||
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
|
||||
altitude = (
|
||||
feet(1500)
|
||||
if altitude_is_agl
|
||||
else self.flight.unit_type.preferred_patrol_altitude
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
return FerryLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to_destination=builder.nav_path(
|
||||
self.flight.departure.position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
def build(self) -> FerryFlightPlan:
|
||||
return FerryFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -8,15 +8,14 @@ generating the waypoints for the mission.
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from abc import ABC, abstractmethod
|
||||
from abc import ABC
|
||||
from collections.abc import Iterator
|
||||
from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from typing import Any, Generic, TYPE_CHECKING, Type, TypeGuard, TypeVar
|
||||
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
|
||||
|
||||
from game.typeguard import self_type_guard
|
||||
from game.utils import Distance, Speed, meters
|
||||
from .ibuilder import IBuilder
|
||||
from .planningerror import PlanningError
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ..starttype import StartType
|
||||
@ -64,11 +63,6 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
def package(self) -> Package:
|
||||
return self.flight.package
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def builder_type() -> Type[IBuilder]:
|
||||
...
|
||||
|
||||
@property
|
||||
def waypoints(self) -> list[FlightWaypoint]:
|
||||
"""A list of all waypoints in the flight plan, in order."""
|
||||
|
||||
@ -14,6 +14,7 @@ from .dead import DeadFlightPlan
|
||||
from .escort import EscortFlightPlan
|
||||
from .ferry import FerryFlightPlan
|
||||
from .flightplan import FlightPlan
|
||||
from .ibuilder import IBuilder
|
||||
from .ocaaircraft import OcaAircraftFlightPlan
|
||||
from .ocarunway import OcaRunwayFlightPlan
|
||||
from .packagerefueling import PackageRefuelingFlightPlan
|
||||
@ -72,42 +73,39 @@ class FlightPlanBuilder:
|
||||
f"{flight.departure} to {flight.package.target}"
|
||||
) from ex
|
||||
|
||||
def plan_type(self, flight: Flight) -> Type[FlightPlan[Any]]:
|
||||
plan_type: Type[FlightPlan[Any]]
|
||||
def builder_type(self, flight: Flight) -> Type[IBuilder[Any, Any]]:
|
||||
if flight.flight_type is FlightType.REFUELING:
|
||||
if self.package.target.is_friendly(self.is_player) or isinstance(
|
||||
self.package.target, FrontLine
|
||||
):
|
||||
return TheaterRefuelingFlightPlan
|
||||
return PackageRefuelingFlightPlan
|
||||
return TheaterRefuelingFlightPlan.builder_type()
|
||||
return PackageRefuelingFlightPlan.builder_type()
|
||||
|
||||
plan_dict: dict[FlightType, Type[FlightPlan[Any]]] = {
|
||||
FlightType.ANTISHIP: AntiShipFlightPlan,
|
||||
FlightType.BAI: BaiFlightPlan,
|
||||
FlightType.BARCAP: BarCapFlightPlan,
|
||||
FlightType.CAS: CasFlightPlan,
|
||||
FlightType.DEAD: DeadFlightPlan,
|
||||
FlightType.ESCORT: EscortFlightPlan,
|
||||
FlightType.OCA_AIRCRAFT: OcaAircraftFlightPlan,
|
||||
FlightType.OCA_RUNWAY: OcaRunwayFlightPlan,
|
||||
FlightType.SEAD: SeadFlightPlan,
|
||||
FlightType.SEAD_ESCORT: EscortFlightPlan,
|
||||
FlightType.STRIKE: StrikeFlightPlan,
|
||||
FlightType.SWEEP: SweepFlightPlan,
|
||||
FlightType.TARCAP: TarCapFlightPlan,
|
||||
FlightType.AEWC: AewcFlightPlan,
|
||||
FlightType.TRANSPORT: AirliftFlightPlan,
|
||||
FlightType.FERRY: FerryFlightPlan,
|
||||
FlightType.AIR_ASSAULT: AirAssaultFlightPlan,
|
||||
builder_dict: dict[FlightType, Type[IBuilder[Any, Any]]] = {
|
||||
FlightType.ANTISHIP: AntiShipFlightPlan.builder_type(),
|
||||
FlightType.BAI: BaiFlightPlan.builder_type(),
|
||||
FlightType.BARCAP: BarCapFlightPlan.builder_type(),
|
||||
FlightType.CAS: CasFlightPlan.builder_type(),
|
||||
FlightType.DEAD: DeadFlightPlan.builder_type(),
|
||||
FlightType.ESCORT: EscortFlightPlan.builder_type(),
|
||||
FlightType.OCA_AIRCRAFT: OcaAircraftFlightPlan.builder_type(),
|
||||
FlightType.OCA_RUNWAY: OcaRunwayFlightPlan.builder_type(),
|
||||
FlightType.SEAD: SeadFlightPlan.builder_type(),
|
||||
FlightType.SEAD_ESCORT: EscortFlightPlan.builder_type(),
|
||||
FlightType.STRIKE: StrikeFlightPlan.builder_type(),
|
||||
FlightType.SWEEP: SweepFlightPlan.builder_type(),
|
||||
FlightType.TARCAP: TarCapFlightPlan.builder_type(),
|
||||
FlightType.AEWC: AewcFlightPlan.builder_type(),
|
||||
FlightType.TRANSPORT: AirliftFlightPlan.builder_type(),
|
||||
FlightType.FERRY: FerryFlightPlan.builder_type(),
|
||||
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
|
||||
}
|
||||
try:
|
||||
return plan_dict[flight.flight_type]
|
||||
return builder_dict[flight.flight_type]
|
||||
except KeyError as ex:
|
||||
raise PlanningError(
|
||||
f"{flight.flight_type} flight plan generation not implemented"
|
||||
) from ex
|
||||
|
||||
def generate_flight_plan(self, flight: Flight) -> FlightPlan[Any]:
|
||||
plan_type = self.plan_type(flight)
|
||||
layout = plan_type.builder_type()(flight, self.theater).build()
|
||||
return plan_type(flight, layout)
|
||||
return self.builder_type(flight)(flight, self.theater).build()
|
||||
|
||||
@ -11,6 +11,7 @@ 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 .planningerror import PlanningError
|
||||
@ -151,10 +152,11 @@ class FormationAttackLayout(FormationLayout):
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[FormationAttackLayout])
|
||||
LayoutT = TypeVar("LayoutT", bound=FormationAttackLayout)
|
||||
|
||||
|
||||
class FormationAttackBuilder(IBuilder, ABC):
|
||||
class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
def _build(
|
||||
self,
|
||||
ingress_type: FlightWaypointType,
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any, Generic, TYPE_CHECKING, TypeVar
|
||||
|
||||
from .flightplan import FlightPlan, Layout
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
@ -10,16 +12,23 @@ if TYPE_CHECKING:
|
||||
from game.threatzones import ThreatZones
|
||||
from ..flight import Flight
|
||||
from ..package import Package
|
||||
from .flightplan import Layout
|
||||
|
||||
|
||||
class IBuilder(ABC):
|
||||
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[Any])
|
||||
LayoutT = TypeVar("LayoutT", bound=Layout)
|
||||
|
||||
|
||||
class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
||||
def __init__(self, flight: Flight, theater: ConflictTheater) -> None:
|
||||
self.flight = flight
|
||||
self.theater = theater
|
||||
|
||||
@abstractmethod
|
||||
def build(self) -> Layout:
|
||||
def layout(self) -> LayoutT:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def build(self) -> FlightPlanT:
|
||||
...
|
||||
|
||||
@property
|
||||
|
||||
@ -19,8 +19,8 @@ class OcaAircraftFlightPlan(FormationAttackFlightPlan):
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder):
|
||||
def build(self) -> FormationAttackLayout:
|
||||
class Builder(FormationAttackBuilder[OcaAircraftFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, Airfield):
|
||||
@ -31,3 +31,6 @@ class Builder(FormationAttackBuilder):
|
||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||
|
||||
return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT)
|
||||
|
||||
def build(self) -> OcaAircraftFlightPlan:
|
||||
return OcaAircraftFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -19,8 +19,8 @@ class OcaRunwayFlightPlan(FormationAttackFlightPlan):
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder):
|
||||
def build(self) -> FormationAttackLayout:
|
||||
class Builder(FormationAttackBuilder[OcaRunwayFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, Airfield):
|
||||
@ -31,3 +31,6 @@ class Builder(FormationAttackBuilder):
|
||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||
|
||||
return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY)
|
||||
|
||||
def build(self) -> OcaRunwayFlightPlan:
|
||||
return OcaRunwayFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -6,65 +6,15 @@ from typing import Type
|
||||
from dcs import Point
|
||||
|
||||
from game.utils import Distance, Heading, feet, meters
|
||||
from .ibuilder import IBuilder
|
||||
from .patrolling import PatrollingLayout
|
||||
from .theaterrefueling import (
|
||||
Builder as TheaterRefuelingBuilder,
|
||||
TheaterRefuelingFlightPlan,
|
||||
)
|
||||
from .refuelingflightplan import RefuelingFlightPlan
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
|
||||
class Builder(TheaterRefuelingBuilder):
|
||||
def build(self) -> PatrollingLayout:
|
||||
package_waypoints = self.package.waypoints
|
||||
assert package_waypoints is not None
|
||||
|
||||
racetrack_half_distance = Distance.from_nautical_miles(20).meters
|
||||
|
||||
racetrack_center = package_waypoints.refuel
|
||||
|
||||
split_heading = Heading.from_degrees(
|
||||
racetrack_center.heading_between_point(package_waypoints.split)
|
||||
)
|
||||
home_heading = split_heading.opposite
|
||||
|
||||
racetrack_start = racetrack_center.point_from_heading(
|
||||
split_heading.degrees, racetrack_half_distance
|
||||
)
|
||||
|
||||
racetrack_end = racetrack_center.point_from_heading(
|
||||
home_heading.degrees, racetrack_half_distance
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
|
||||
tanker_type = self.flight.unit_type
|
||||
if tanker_type.patrol_altitude is not None:
|
||||
altitude = tanker_type.patrol_altitude
|
||||
else:
|
||||
altitude = feet(21000)
|
||||
|
||||
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
||||
|
||||
return PatrollingLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
self.flight.departure.position, racetrack_start, altitude
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
racetrack_end, self.flight.arrival.position, altitude
|
||||
),
|
||||
patrol_start=racetrack[0],
|
||||
patrol_end=racetrack[1],
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
|
||||
class PackageRefuelingFlightPlan(TheaterRefuelingFlightPlan):
|
||||
class PackageRefuelingFlightPlan(RefuelingFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
@ -122,3 +72,54 @@ class PackageRefuelingFlightPlan(TheaterRefuelingFlightPlan):
|
||||
+ delay_split_to_refuel
|
||||
- timedelta(minutes=1.5)
|
||||
)
|
||||
|
||||
|
||||
class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
|
||||
def layout(self) -> PatrollingLayout:
|
||||
package_waypoints = self.package.waypoints
|
||||
assert package_waypoints is not None
|
||||
|
||||
racetrack_half_distance = Distance.from_nautical_miles(20).meters
|
||||
|
||||
racetrack_center = package_waypoints.refuel
|
||||
|
||||
split_heading = Heading.from_degrees(
|
||||
racetrack_center.heading_between_point(package_waypoints.split)
|
||||
)
|
||||
home_heading = split_heading.opposite
|
||||
|
||||
racetrack_start = racetrack_center.point_from_heading(
|
||||
split_heading.degrees, racetrack_half_distance
|
||||
)
|
||||
|
||||
racetrack_end = racetrack_center.point_from_heading(
|
||||
home_heading.degrees, racetrack_half_distance
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
|
||||
tanker_type = self.flight.unit_type
|
||||
if tanker_type.patrol_altitude is not None:
|
||||
altitude = tanker_type.patrol_altitude
|
||||
else:
|
||||
altitude = feet(21000)
|
||||
|
||||
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
||||
|
||||
return PatrollingLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
self.flight.departure.position, racetrack_start, altitude
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
racetrack_end, self.flight.arrival.position, altitude
|
||||
),
|
||||
patrol_start=racetrack[0],
|
||||
patrol_end=racetrack[1],
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
def build(self) -> PackageRefuelingFlightPlan:
|
||||
return PackageRefuelingFlightPlan(self.flight, self.layout())
|
||||
|
||||
20
game/ato/flightplans/refuelingflightplan.py
Normal file
20
game/ato/flightplans/refuelingflightplan.py
Normal file
@ -0,0 +1,20 @@
|
||||
from abc import ABC
|
||||
|
||||
from game.utils import Distance, Speed, knots, meters
|
||||
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||
|
||||
|
||||
class RefuelingFlightPlan(PatrollingFlightPlan[PatrollingLayout], ABC):
|
||||
@property
|
||||
def patrol_speed(self) -> Speed:
|
||||
# TODO: Could use self.flight.unit_type.preferred_patrol_speed(altitude).
|
||||
if self.flight.unit_type.patrol_speed is not None:
|
||||
return self.flight.unit_type.patrol_speed
|
||||
# ~280 knots IAS at 21000.
|
||||
return knots(400)
|
||||
|
||||
@property
|
||||
def engagement_distance(self) -> Distance:
|
||||
# TODO: Factor out a common base of the combat and non-combat race-tracks.
|
||||
# No harm in setting this, but we ought to clean up a bit.
|
||||
return meters(0)
|
||||
@ -15,42 +15,6 @@ if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
class Builder(IBuilder):
|
||||
def build(self) -> RtbLayout:
|
||||
if not isinstance(self.flight.state, InFlight):
|
||||
raise RuntimeError(f"Cannot abort {self} because it is not in flight")
|
||||
|
||||
current_position = self.flight.state.estimate_position()
|
||||
current_altitude, altitude_reference = self.flight.state.estimate_altitude()
|
||||
|
||||
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
|
||||
altitude = (
|
||||
feet(1500)
|
||||
if altitude_is_agl
|
||||
else self.flight.unit_type.preferred_patrol_altitude
|
||||
)
|
||||
builder = WaypointBuilder(self.flight, self.flight.coalition)
|
||||
abort_point = builder.nav(
|
||||
current_position, current_altitude, altitude_reference == "RADIO"
|
||||
)
|
||||
abort_point.name = "ABORT AND RTB"
|
||||
abort_point.pretty_name = "Abort and RTB"
|
||||
abort_point.description = "Abort mission and return to base"
|
||||
return RtbLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
abort_location=abort_point,
|
||||
nav_to_destination=builder.nav_path(
|
||||
current_position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RtbLayout(StandardLayout):
|
||||
abort_location: FlightWaypoint
|
||||
@ -88,3 +52,42 @@ class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return timedelta()
|
||||
|
||||
|
||||
class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
||||
def layout(self) -> RtbLayout:
|
||||
if not isinstance(self.flight.state, InFlight):
|
||||
raise RuntimeError(f"Cannot abort {self} because it is not in flight")
|
||||
|
||||
current_position = self.flight.state.estimate_position()
|
||||
current_altitude, altitude_reference = self.flight.state.estimate_altitude()
|
||||
|
||||
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
|
||||
altitude = (
|
||||
feet(1500)
|
||||
if altitude_is_agl
|
||||
else self.flight.unit_type.preferred_patrol_altitude
|
||||
)
|
||||
builder = WaypointBuilder(self.flight, self.flight.coalition)
|
||||
abort_point = builder.nav(
|
||||
current_position, current_altitude, altitude_reference == "RADIO"
|
||||
)
|
||||
abort_point.name = "ABORT AND RTB"
|
||||
abort_point.pretty_name = "Abort and RTB"
|
||||
abort_point.description = "Abort mission and return to base"
|
||||
return RtbLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
abort_location=abort_point,
|
||||
nav_to_destination=builder.nav_path(
|
||||
current_position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
def build(self) -> RtbFlightPlan:
|
||||
return RtbFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -25,6 +25,9 @@ class SeadFlightPlan(FormationAttackFlightPlan):
|
||||
return timedelta(minutes=1)
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder):
|
||||
def build(self) -> FormationAttackLayout:
|
||||
class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
return self._build(FlightWaypointType.INGRESS_SEAD)
|
||||
|
||||
def build(self) -> SeadFlightPlan:
|
||||
return SeadFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -19,8 +19,8 @@ class StrikeFlightPlan(FormationAttackFlightPlan):
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder):
|
||||
def build(self) -> FormationAttackLayout:
|
||||
class Builder(FormationAttackBuilder[StrikeFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, TheaterGroundObject):
|
||||
@ -31,3 +31,6 @@ class Builder(FormationAttackBuilder):
|
||||
targets.append(StrikeTarget(f"{unit.type.id} #{idx}", unit))
|
||||
|
||||
return self._build(FlightWaypointType.INGRESS_STRIKE, targets)
|
||||
|
||||
def build(self) -> StrikeFlightPlan:
|
||||
return StrikeFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -17,57 +17,6 @@ if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
class Builder(IBuilder):
|
||||
def build(self) -> SweepLayout:
|
||||
assert self.package.waypoints is not None
|
||||
target = self.package.target.position
|
||||
heading = Heading.from_degrees(
|
||||
self.package.waypoints.join.heading_between_point(target)
|
||||
)
|
||||
start_pos = target.point_from_heading(
|
||||
heading.degrees, -self.doctrine.sweep_distance.meters
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
|
||||
|
||||
hold = builder.hold(self._hold_point())
|
||||
|
||||
refuel = None
|
||||
|
||||
if self.package.waypoints is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
|
||||
return SweepLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
hold=hold,
|
||||
nav_to=builder.nav_path(
|
||||
hold.position, start.position, self.doctrine.ingress_altitude
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
end.position,
|
||||
self.flight.arrival.position,
|
||||
self.doctrine.ingress_altitude,
|
||||
),
|
||||
sweep_start=start,
|
||||
sweep_end=end,
|
||||
refuel=refuel,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SweepLayout(LoiterLayout):
|
||||
nav_to: list[FlightWaypoint]
|
||||
@ -145,3 +94,57 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.sweep_end_time
|
||||
|
||||
|
||||
class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
||||
def layout(self) -> SweepLayout:
|
||||
assert self.package.waypoints is not None
|
||||
target = self.package.target.position
|
||||
heading = Heading.from_degrees(
|
||||
self.package.waypoints.join.heading_between_point(target)
|
||||
)
|
||||
start_pos = target.point_from_heading(
|
||||
heading.degrees, -self.doctrine.sweep_distance.meters
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
|
||||
|
||||
hold = builder.hold(self._hold_point())
|
||||
|
||||
refuel = None
|
||||
|
||||
if self.package.waypoints is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
|
||||
return SweepLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
hold=hold,
|
||||
nav_to=builder.nav_path(
|
||||
hold.position, start.position, self.doctrine.ingress_altitude
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
end.position,
|
||||
self.flight.arrival.position,
|
||||
self.doctrine.ingress_altitude,
|
||||
),
|
||||
sweep_start=start,
|
||||
sweep_end=end,
|
||||
refuel=refuel,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
def build(self) -> SweepFlightPlan:
|
||||
return SweepFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -15,44 +15,6 @@ if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
class Builder(CapBuilder):
|
||||
def build(self) -> TarCapLayout:
|
||||
location = self.package.target
|
||||
|
||||
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||
patrol_alt = max(
|
||||
self.doctrine.min_patrol_altitude,
|
||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
|
||||
|
||||
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||
|
||||
refuel = None
|
||||
|
||||
if self.package.waypoints is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
|
||||
return TarCapLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
self.flight.departure.position, orbit0p, patrol_alt
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
orbit1p, self.flight.arrival.position, patrol_alt
|
||||
),
|
||||
patrol_start=start,
|
||||
patrol_end=end,
|
||||
refuel=refuel,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TarCapLayout(PatrollingLayout):
|
||||
refuel: FlightWaypoint | None
|
||||
@ -124,3 +86,44 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
||||
if end is not None:
|
||||
return end
|
||||
return super().patrol_end_time
|
||||
|
||||
|
||||
class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
|
||||
def layout(self) -> TarCapLayout:
|
||||
location = self.package.target
|
||||
|
||||
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||
patrol_alt = max(
|
||||
self.doctrine.min_patrol_altitude,
|
||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
|
||||
|
||||
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||
|
||||
refuel = None
|
||||
|
||||
if self.package.waypoints is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
|
||||
return TarCapLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
self.flight.departure.position, orbit0p, patrol_alt
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
orbit1p, self.flight.arrival.position, patrol_alt
|
||||
),
|
||||
patrol_start=start,
|
||||
patrol_end=end,
|
||||
refuel=refuel,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
def build(self) -> TarCapFlightPlan:
|
||||
return TarCapFlightPlan(self.flight, self.layout())
|
||||
|
||||
@ -3,14 +3,25 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
from typing import Type
|
||||
|
||||
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
|
||||
from game.utils import Heading, feet, meters, nautical_miles
|
||||
from .ibuilder import IBuilder
|
||||
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||
from .patrolling import PatrollingLayout
|
||||
from .refuelingflightplan import RefuelingFlightPlan
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
|
||||
|
||||
class Builder(IBuilder):
|
||||
def build(self) -> PatrollingLayout:
|
||||
class TheaterRefuelingFlightPlan(RefuelingFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
return timedelta(hours=1)
|
||||
|
||||
|
||||
class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
|
||||
def layout(self) -> PatrollingLayout:
|
||||
racetrack_half_distance = nautical_miles(20).meters
|
||||
|
||||
location = self.package.target
|
||||
@ -68,26 +79,5 @@ class Builder(IBuilder):
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
|
||||
class TheaterRefuelingFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
return timedelta(hours=1)
|
||||
|
||||
@property
|
||||
def patrol_speed(self) -> Speed:
|
||||
# TODO: Could use self.flight.unit_type.preferred_patrol_speed(altitude).
|
||||
if self.flight.unit_type.patrol_speed is not None:
|
||||
return self.flight.unit_type.patrol_speed
|
||||
# ~280 knots IAS at 21000.
|
||||
return knots(400)
|
||||
|
||||
@property
|
||||
def engagement_distance(self) -> Distance:
|
||||
# TODO: Factor out a common base of the combat and non-combat race-tracks.
|
||||
# No harm in setting this, but we ought to clean up a bit.
|
||||
return meters(0)
|
||||
def build(self) -> TheaterRefuelingFlightPlan:
|
||||
return TheaterRefuelingFlightPlan(self.flight, self.layout())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user