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:
Dan Albert 2022-08-10 18:22:28 -07:00
parent 3dd0e4a66f
commit 4521053804
27 changed files with 573 additions and 498 deletions

View File

@ -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(

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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]:

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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."""

View File

@ -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()

View File

@ -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,

View File

@ -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

View File

@ -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())

View File

@ -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())

View File

@ -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())

View 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)

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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())