Split flight plan layout into a separate class.

During package planning we don't care about the details of the flight
plan, just the layout (to check if the layout is threatened and we need
escorts). Splitting these will allow us to reduce the amount of work
that must be done in each loop of the planning phase, potentially
caching attempted flight plans between loops.
This commit is contained in:
Dan Albert 2022-03-11 16:00:48 -08:00
parent fa8c0d9660
commit 769fe12159
30 changed files with 510 additions and 594 deletions

View File

@ -27,6 +27,7 @@ if TYPE_CHECKING:
from game.squadrons import Squadron, Pilot
from game.theater import ControlPoint, MissionTarget
from game.transfers import TransferOrder
from .flightplans.flightplan import FlightPlan
from .flighttype import FlightType
from .flightwaypoint import FlightWaypoint
from .package import Package
@ -84,10 +85,11 @@ class Flight(SidcDescribable):
# Will be replaced with a more appropriate FlightPlan by
# FlightPlanBuilder, but an empty flight plan the flight begins with an
# empty flight plan.
from game.ato.flightplans.flightplan import FlightPlan
from .flightplans.custom import CustomFlightPlan
from .flightplans.custom import CustomFlightPlan, CustomLayout
self.flight_plan: FlightPlan = CustomFlightPlan(self, [])
self.flight_plan: FlightPlan[Any] = CustomFlightPlan(
self, CustomLayout(custom_waypoints=[])
)
def __getstate__(self) -> dict[str, Any]:
state = self.__dict__.copy()
@ -196,9 +198,8 @@ class Flight(SidcDescribable):
def abort(self) -> None:
from .flightplans.rtb import RtbFlightPlan
self.flight_plan = RtbFlightPlan.builder_type()(
self, self.coalition.game.theater
).build()
layout = RtbFlightPlan.builder_type()(self, self.coalition.game.theater).build()
self.flight_plan = RtbFlightPlan(self, layout)
self.set_state(
Navigating(

View File

@ -4,17 +4,15 @@ from datetime import timedelta
from typing import Type
from game.ato.flightplans.ibuilder import IBuilder
from game.ato.flightplans.patrolling import PatrollingFlightPlan
from game.ato.flightplans.patrolling import PatrollingFlightPlan, PatrollingLayout
from game.ato.flightplans.waypointbuilder import WaypointBuilder
from game.utils import Heading, feet, knots, meters, nautical_miles
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
class Builder(IBuilder):
def build(self) -> AewcFlightPlan:
def build(self) -> PatrollingLayout:
racetrack_half_distance = nautical_miles(30).meters
patrol_duration = timedelta(hours=4)
location = self.package.target
closest_boundary = self.threat_zones.closest_boundary(location.position)
@ -52,15 +50,9 @@ class Builder(IBuilder):
else:
altitude = feet(25000)
if self.flight.unit_type.preferred_patrol_speed(altitude) is not None:
speed = self.flight.unit_type.preferred_patrol_speed(altitude)
else:
speed = knots(390)
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
return AewcFlightPlan(
flight=self.flight,
return PatrollingLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position, racetrack_start, altitude
@ -73,15 +65,27 @@ class Builder(IBuilder):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
patrol_duration=patrol_duration,
patrol_speed=speed,
# 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.
engagement_distance=meters(0),
)
class AewcFlightPlan(PatrollingFlightPlan):
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

View File

@ -1,13 +1,14 @@
from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, Type
from game.utils import feet
from .ibuilder import IBuilder
from .planningerror import PlanningError
from .standard import StandardFlightPlan
from .standard import StandardFlightPlan, StandardLayout
from .waypointbuilder import WaypointBuilder
if TYPE_CHECKING:
@ -16,7 +17,7 @@ if TYPE_CHECKING:
class Builder(IBuilder):
def build(self) -> AirliftFlightPlan:
def build(self) -> AirliftLayout:
cargo = self.flight.cargo
if cargo is None:
raise PlanningError(
@ -39,8 +40,7 @@ class Builder(IBuilder):
altitude_is_agl,
)
return AirliftFlightPlan(
flight=self.flight,
return AirliftLayout(
departure=builder.takeoff(self.flight.departure),
nav_to_pickup=nav_to_pickup,
pickup=pickup,
@ -63,30 +63,13 @@ class Builder(IBuilder):
)
class AirliftFlightPlan(StandardFlightPlan):
def __init__(
self,
flight: Flight,
departure: FlightWaypoint,
nav_to_pickup: list[FlightWaypoint],
pickup: FlightWaypoint | None,
nav_to_drop_off: list[FlightWaypoint],
drop_off: FlightWaypoint,
nav_to_home: list[FlightWaypoint],
arrival: FlightWaypoint,
divert: FlightWaypoint | None,
bullseye: FlightWaypoint,
) -> None:
super().__init__(flight, departure, arrival, divert, bullseye)
self.nav_to_pickup = nav_to_pickup
self.pickup = pickup
self.nav_to_drop_off = nav_to_drop_off
self.drop_off = drop_off
self.nav_to_home = nav_to_home
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@dataclass(frozen=True)
class AirliftLayout(StandardLayout):
nav_to_pickup: list[FlightWaypoint]
pickup: FlightWaypoint | None
nav_to_drop_off: list[FlightWaypoint]
drop_off: FlightWaypoint
nav_to_home: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
@ -101,9 +84,18 @@ class AirliftFlightPlan(StandardFlightPlan):
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.drop_off
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

View File

@ -4,7 +4,11 @@ from typing import Type
from game.theater import NavalControlPoint
from game.theater.theatergroundobject import NavalGroundObject
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
from .formationattack import (
FormationAttackBuilder,
FormationAttackFlightPlan,
FormationAttackLayout,
)
from .invalidobjectivelocation import InvalidObjectiveLocation
from .waypointbuilder import StrikeTarget
from ..flightwaypointtype import FlightWaypointType
@ -16,8 +20,8 @@ class AntiShipFlightPlan(FormationAttackFlightPlan):
return Builder
class Builder(FormationAttackBuilder[AntiShipFlightPlan]):
def build(self) -> FormationAttackFlightPlan:
class Builder(FormationAttackBuilder):
def build(self) -> FormationAttackLayout:
location = self.package.target
from game.transfers import CargoShip
@ -31,7 +35,7 @@ class Builder(FormationAttackBuilder[AntiShipFlightPlan]):
else:
raise InvalidObjectiveLocation(self.flight.flight_type, location)
return self._build(AntiShipFlightPlan, FlightWaypointType.INGRESS_BAI, targets)
return self._build(FlightWaypointType.INGRESS_BAI, targets)
@staticmethod
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]:

View File

@ -3,7 +3,11 @@ from __future__ import annotations
from typing import Type
from game.theater.theatergroundobject import TheaterGroundObject
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
from .formationattack import (
FormationAttackBuilder,
FormationAttackFlightPlan,
FormationAttackLayout,
)
from .invalidobjectivelocation import InvalidObjectiveLocation
from .waypointbuilder import StrikeTarget
from ..flightwaypointtype import FlightWaypointType
@ -15,8 +19,8 @@ class BaiFlightPlan(FormationAttackFlightPlan):
return Builder
class Builder(FormationAttackBuilder[BaiFlightPlan]):
def build(self) -> FormationAttackFlightPlan:
class Builder(FormationAttackBuilder):
def build(self) -> FormationAttackLayout:
location = self.package.target
from game.transfers import Convoy
@ -33,4 +37,4 @@ class Builder(FormationAttackBuilder[BaiFlightPlan]):
else:
raise InvalidObjectiveLocation(self.flight.flight_type, location)
return self._build(BaiFlightPlan, FlightWaypointType.INGRESS_BAI, targets)
return self._build(FlightWaypointType.INGRESS_BAI, targets)

View File

@ -1,18 +1,19 @@
from __future__ import annotations
import random
from datetime import timedelta
from typing import Type
from game.theater import FrontLine
from game.utils import feet
from game.utils import Distance, Speed, feet
from .capbuilder import CapBuilder
from .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan
from .patrolling import PatrollingFlightPlan, PatrollingLayout
from .waypointbuilder import WaypointBuilder
class Builder(CapBuilder):
def build(self) -> BarCapFlightPlan:
def build(self) -> PatrollingLayout:
location = self.package.target
if isinstance(location, FrontLine):
@ -27,16 +28,10 @@ class Builder(CapBuilder):
min(self.doctrine.max_patrol_altitude, randomized_alt),
)
patrol_speed = self.flight.unit_type.preferred_patrol_speed(patrol_alt)
builder = WaypointBuilder(self.flight, self.coalition)
start, end = builder.race_track(start_pos, end_pos, patrol_alt)
return BarCapFlightPlan(
flight=self.flight,
patrol_duration=self.doctrine.cap_duration,
patrol_speed=patrol_speed,
engagement_distance=self.doctrine.cap_engagement_range,
return PatrollingLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position, start.position, patrol_alt
@ -52,7 +47,21 @@ class Builder(CapBuilder):
)
class BarCapFlightPlan(PatrollingFlightPlan):
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

View File

@ -1,24 +1,24 @@
from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, Type
from game.theater import FrontLine
from game.utils import Distance, Speed, meters
from game.utils import Distance, Speed, kph, meters
from .ibuilder import IBuilder
from .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan
from .patrolling import PatrollingFlightPlan, PatrollingLayout
from .waypointbuilder import WaypointBuilder
from ..flightwaypointtype import FlightWaypointType
if TYPE_CHECKING:
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint
class Builder(IBuilder):
def build(self) -> CasFlightPlan:
def build(self) -> CasLayout:
location = self.package.target
if not isinstance(location, FrontLine):
@ -41,24 +41,13 @@ class Builder(IBuilder):
builder = WaypointBuilder(self.flight, self.coalition)
# 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
is_helo = self.flight.unit_type.dcs_unit_type.helicopter
ingress_egress_altitude = (
self.doctrine.ingress_altitude if not is_helo else meters(50)
)
patrol_speed = self.flight.unit_type.preferred_patrol_speed(
ingress_egress_altitude
)
use_agl_ingress_egress = is_helo
from game.missiongenerator.frontlineconflictdescription import FRONTLINE_LENGTH
return CasFlightPlan(
flight=self.flight,
patrol_duration=self.doctrine.cas_duration,
patrol_speed=patrol_speed,
return CasLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position,
@ -75,7 +64,6 @@ class Builder(IBuilder):
patrol_start=builder.ingress(
FlightWaypointType.INGRESS_CAS, ingress, location
),
engagement_distance=meters(FRONTLINE_LENGTH) / 2,
target=builder.cas(center),
patrol_end=builder.egress(egress, location),
arrival=builder.land(self.flight.arrival),
@ -84,42 +72,9 @@ class Builder(IBuilder):
)
class CasFlightPlan(PatrollingFlightPlan):
def __init__(
self,
flight: Flight,
departure: FlightWaypoint,
arrival: FlightWaypoint,
divert: FlightWaypoint | None,
bullseye: FlightWaypoint,
nav_to: list[FlightWaypoint],
nav_from: list[FlightWaypoint],
patrol_start: FlightWaypoint,
patrol_end: FlightWaypoint,
patrol_duration: timedelta,
patrol_speed: Speed,
engagement_distance: Distance,
target: FlightWaypoint,
) -> None:
super().__init__(
flight,
departure,
arrival,
divert,
bullseye,
nav_to,
nav_from,
patrol_start,
patrol_end,
patrol_duration,
patrol_speed,
engagement_distance,
)
self.target = target
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@dataclass(frozen=True)
class CasLayout(PatrollingLayout):
target: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
@ -133,12 +88,35 @@ class CasFlightPlan(PatrollingFlightPlan):
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.patrol_start, self.target, self.patrol_end}
return {self.layout.patrol_start, self.layout.target, self.layout.patrol_end}
def request_escort_at(self) -> FlightWaypoint | None:
return self.patrol_start
return self.layout.patrol_start
def dismiss_escort_at(self) -> FlightWaypoint | None:
return self.patrol_end
return self.layout.patrol_end

View File

@ -1,35 +1,36 @@
from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, Type
from .flightplan import FlightPlan
from .flightplan import FlightPlan, Layout
from .ibuilder import IBuilder
from ..flightwaypointtype import FlightWaypointType
if TYPE_CHECKING:
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint
class Builder(IBuilder):
def build(self) -> CustomFlightPlan:
return CustomFlightPlan(self.flight, [])
def build(self) -> CustomLayout:
return CustomLayout([])
class CustomFlightPlan(FlightPlan):
def __init__(self, flight: Flight, waypoints: list[FlightWaypoint]) -> None:
super().__init__(flight)
self.custom_waypoints = waypoints
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@dataclass(frozen=True)
class CustomLayout(Layout):
custom_waypoints: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield from self.custom_waypoints
class CustomFlightPlan(FlightPlan[CustomLayout]):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def tot_waypoint(self) -> FlightWaypoint | None:
target_types = (

View File

@ -7,7 +7,11 @@ from game.theater.theatergroundobject import (
EwrGroundObject,
SamGroundObject,
)
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
from .formationattack import (
FormationAttackBuilder,
FormationAttackFlightPlan,
FormationAttackLayout,
)
from .invalidobjectivelocation import InvalidObjectiveLocation
from ..flightwaypointtype import FlightWaypointType
@ -18,8 +22,8 @@ class DeadFlightPlan(FormationAttackFlightPlan):
return Builder
class Builder(FormationAttackBuilder[DeadFlightPlan]):
def build(self) -> FormationAttackFlightPlan:
class Builder(FormationAttackBuilder):
def build(self) -> FormationAttackLayout:
location = self.package.target
is_ewr = isinstance(location, EwrGroundObject)
@ -31,4 +35,4 @@ class Builder(FormationAttackBuilder[DeadFlightPlan]):
)
raise InvalidObjectiveLocation(self.flight.flight_type, location)
return self._build(DeadFlightPlan, FlightWaypointType.INGRESS_DEAD)
return self._build(FlightWaypointType.INGRESS_DEAD)

View File

@ -1,9 +1,12 @@
from __future__ import annotations
from datetime import timedelta
from typing import Type
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
from .formationattack import (
FormationAttackBuilder,
FormationAttackFlightPlan,
FormationAttackLayout,
)
from .waypointbuilder import WaypointBuilder
@ -13,8 +16,8 @@ class EscortFlightPlan(FormationAttackFlightPlan):
return Builder
class Builder(FormationAttackBuilder[EscortFlightPlan]):
def build(self) -> FormationAttackFlightPlan:
class Builder(FormationAttackBuilder):
def build(self) -> FormationAttackLayout:
assert self.package.waypoints is not None
builder = WaypointBuilder(self.flight, self.coalition)
@ -28,11 +31,9 @@ class Builder(FormationAttackBuilder[EscortFlightPlan]):
if self.package.waypoints.refuel is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
return EscortFlightPlan(
flight=self.flight,
return FormationAttackLayout(
departure=builder.takeoff(self.flight.departure),
hold=hold,
hold_duration=timedelta(minutes=5),
nav_to=builder.nav_path(
hold.position, join.position, self.doctrine.ingress_altitude
),

View File

@ -1,22 +1,22 @@
from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, Type
from game.utils import feet
from .ibuilder import IBuilder
from .planningerror import PlanningError
from .standard import StandardFlightPlan
from .standard import StandardFlightPlan, StandardLayout
from .waypointbuilder import WaypointBuilder
if TYPE_CHECKING:
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint
class Builder(IBuilder):
def build(self) -> FerryFlightPlan:
def build(self) -> FerryLayout:
if self.flight.departure == self.flight.arrival:
raise PlanningError(
f"Cannot plan ferry self.flight: departure and arrival are both "
@ -31,8 +31,7 @@ class Builder(IBuilder):
)
builder = WaypointBuilder(self.flight, self.coalition)
return FerryFlightPlan(
flight=self.flight,
return FerryLayout(
departure=builder.takeoff(self.flight.departure),
nav_to_destination=builder.nav_path(
self.flight.departure.position,
@ -46,22 +45,9 @@ class Builder(IBuilder):
)
class FerryFlightPlan(StandardFlightPlan):
def __init__(
self,
flight: Flight,
departure: FlightWaypoint,
arrival: FlightWaypoint,
divert: FlightWaypoint | None,
bullseye: FlightWaypoint,
nav_to_destination: list[FlightWaypoint],
) -> None:
super().__init__(flight, departure, arrival, divert, bullseye)
self.nav_to_destination = nav_to_destination
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@dataclass(frozen=True)
class FerryLayout(StandardLayout):
nav_to_destination: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
@ -71,9 +57,15 @@ class FerryFlightPlan(StandardFlightPlan):
yield self.divert
yield self.bullseye
class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def tot_waypoint(self) -> FlightWaypoint | None:
return self.arrival
return self.layout.arrival
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
# TOT planning isn't really useful for ferries. They're behind the front

View File

@ -12,7 +12,7 @@ from abc import ABC, abstractmethod
from collections.abc import Iterator
from datetime import timedelta
from functools import cached_property
from typing import TYPE_CHECKING, Type, TypeGuard
from typing import Any, Generic, TYPE_CHECKING, Type, TypeGuard, TypeVar
from game.typeguard import self_type_guard
from game.utils import Distance, Speed, meters
@ -41,9 +41,24 @@ INGRESS_TYPES = {
}
class FlightPlan(ABC):
def __init__(self, flight: Flight) -> None:
class Layout(ABC):
@property
def waypoints(self) -> list[FlightWaypoint]:
"""A list of all waypoints in the flight plan, in order."""
return list(self.iter_waypoints())
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
"""Iterates over all waypoints in the flight plan, in order."""
raise NotImplementedError
LayoutT = TypeVar("LayoutT", bound=Layout)
class FlightPlan(ABC, Generic[LayoutT]):
def __init__(self, flight: Flight, layout: LayoutT) -> None:
self.flight = flight
self.layout = layout
@property
def package(self) -> Package:
@ -61,7 +76,7 @@ class FlightPlan(ABC):
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
"""Iterates over all waypoints in the flight plan, in order."""
raise NotImplementedError
yield from self.layout.iter_waypoints()
def edges(
self, until: FlightWaypoint | None = None
@ -296,13 +311,17 @@ class FlightPlan(ABC):
raise NotImplementedError
@self_type_guard
def is_loiter(self, flight_plan: FlightPlan) -> TypeGuard[LoiterFlightPlan]:
def is_loiter(self, flight_plan: FlightPlan[Any]) -> TypeGuard[LoiterFlightPlan]:
return False
@self_type_guard
def is_patrol(self, flight_plan: FlightPlan) -> TypeGuard[PatrollingFlightPlan]:
def is_patrol(
self, flight_plan: FlightPlan[Any]
) -> TypeGuard[PatrollingFlightPlan[Any]]:
return False
@self_type_guard
def is_formation(self, flight_plan: FlightPlan) -> TypeGuard[FormationFlightPlan]:
def is_formation(
self, flight_plan: FlightPlan[Any]
) -> TypeGuard[FormationFlightPlan]:
return False

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Type
from typing import Any, TYPE_CHECKING, Type
from game.ato import FlightType
from game.ato.closestairfields import ObjectiveDistanceCache
@ -82,8 +82,8 @@ class FlightPlanBuilder:
f"{flight.departure} to {flight.package.target}"
) from ex
def plan_type(self, task: FlightType) -> Type[FlightPlan] | None:
plan_type: Type[FlightPlan]
def plan_type(self, task: FlightType) -> Type[FlightPlan[Any]] | None:
plan_type: Type[FlightPlan[Any]]
if task == FlightType.REFUELING:
if self.package.target.is_friendly(self.is_player) or isinstance(
self.package.target, FrontLine
@ -91,7 +91,7 @@ class FlightPlanBuilder:
return TheaterRefuelingFlightPlan
return PackageRefuelingFlightPlan
plan_dict: dict[FlightType, Type[FlightPlan]] = {
plan_dict: dict[FlightType, Type[FlightPlan[Any]]] = {
FlightType.ANTISHIP: AntiShipFlightPlan,
FlightType.BAI: BaiFlightPlan,
FlightType.BARCAP: BarCapFlightPlan,
@ -111,13 +111,14 @@ class FlightPlanBuilder:
}
return plan_dict.get(task)
def generate_flight_plan(self, flight: Flight) -> FlightPlan:
def generate_flight_plan(self, flight: Flight) -> FlightPlan[Any]:
plan_type = self.plan_type(flight.flight_type)
if plan_type is None:
raise PlanningError(
f"{flight.flight_type} flight plan generation not implemented"
)
return plan_type.builder_type()(flight, self.theater).build()
layout = plan_type.builder_type()(flight, self.theater).build()
return plan_type(flight, layout)
def regenerate_flight_plans(self) -> None:
new_flights: list[Flight] = []

View File

@ -1,52 +1,31 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import timedelta
from functools import cached_property
from typing import TYPE_CHECKING, TypeGuard
from typing import Any, TYPE_CHECKING, TypeGuard
from game.typeguard import self_type_guard
from game.utils import Speed
from .flightplan import FlightPlan
from .loiter import LoiterFlightPlan
from .loiter import LoiterFlightPlan, LoiterLayout
from ..traveltime import GroundSpeed, TravelTime
if TYPE_CHECKING:
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint
class FormationFlightPlan(LoiterFlightPlan, ABC):
def __init__(
self,
flight: Flight,
departure: FlightWaypoint,
arrival: FlightWaypoint,
divert: FlightWaypoint | None,
bullseye: FlightWaypoint,
nav_to: list[FlightWaypoint],
nav_from: list[FlightWaypoint],
hold: FlightWaypoint,
hold_duration: timedelta,
join: FlightWaypoint,
split: FlightWaypoint,
refuel: FlightWaypoint,
) -> None:
super().__init__(
flight,
departure,
arrival,
divert,
bullseye,
nav_to,
nav_from,
hold,
hold_duration,
)
self.join = join
self.split = split
self.refuel = refuel
@dataclass(frozen=True)
class FormationLayout(LoiterLayout, ABC):
nav_to: list[FlightWaypoint]
join: FlightWaypoint
split: FlightWaypoint
refuel: FlightWaypoint
nav_from: list[FlightWaypoint]
class FormationFlightPlan(LoiterFlightPlan, ABC):
@property
@abstractmethod
def package_speed_waypoints(self) -> set[FlightWaypoint]:
@ -57,10 +36,10 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
return self.package_speed_waypoints
def request_escort_at(self) -> FlightWaypoint | None:
return self.join
return self.layout.join
def dismiss_escort_at(self) -> FlightWaypoint | None:
return self.split
return self.layout.split
@cached_property
def best_flight_formation_speed(self) -> Speed:
@ -90,7 +69,7 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
@property
def travel_time_to_rendezvous(self) -> timedelta:
"""The estimated time between the first waypoint and the join point."""
return self._travel_time_to_waypoint(self.join)
return self._travel_time_to_waypoint(self.layout.join)
@property
@abstractmethod
@ -103,18 +82,18 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
...
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.join:
if waypoint == self.layout.join:
return self.join_time
elif waypoint == self.split:
elif waypoint == self.layout.split:
return self.split_time
return None
@property
def push_time(self) -> timedelta:
return self.join_time - TravelTime.between_points(
self.hold.position,
self.join.position,
GroundSpeed.for_flight(self.flight, self.hold.alt),
self.layout.hold.position,
self.layout.join.position,
GroundSpeed.for_flight(self.flight, self.layout.hold.alt),
)
@property
@ -122,5 +101,7 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
return self.split_time
@self_type_guard
def is_formation(self, flight_plan: FlightPlan) -> TypeGuard[FormationFlightPlan]:
def is_formation(
self, flight_plan: FlightPlan[Any]
) -> TypeGuard[FormationFlightPlan]:
return True

View File

@ -2,15 +2,16 @@ from __future__ import annotations
from abc import ABC
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from typing import Generic, TYPE_CHECKING, Type, TypeVar
from typing import TYPE_CHECKING, TypeVar
from dcs import Point
from game.flightplan import HoldZoneGeometry
from game.theater import MissionTarget
from game.utils import Speed, meters
from .formation import FormationFlightPlan
from .formation import FormationFlightPlan, FormationLayout
from .ibuilder import IBuilder
from .planningerror import PlanningError
from .waypointbuilder import StrikeTarget, WaypointBuilder
@ -23,64 +24,16 @@ if TYPE_CHECKING:
class FormationAttackFlightPlan(FormationFlightPlan, ABC):
def __init__(
self,
flight: Flight,
departure: FlightWaypoint,
arrival: FlightWaypoint,
divert: FlightWaypoint | None,
bullseye: FlightWaypoint,
nav_to: list[FlightWaypoint],
nav_from: list[FlightWaypoint],
hold: FlightWaypoint,
hold_duration: timedelta,
join: FlightWaypoint,
split: FlightWaypoint,
refuel: FlightWaypoint,
ingress: FlightWaypoint,
targets: list[FlightWaypoint],
lead_time: timedelta = timedelta(),
) -> None:
super().__init__(
flight,
departure,
arrival,
divert,
bullseye,
nav_to,
nav_from,
hold,
hold_duration,
join,
split,
refuel,
)
self.ingress = ingress
self.targets = targets
self.lead_time = lead_time
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield self.hold
yield from self.nav_to
yield self.join
yield self.ingress
yield from self.targets
yield self.split
if self.refuel is not None:
yield self.refuel
yield from self.nav_from
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
@property
def lead_time(self) -> timedelta:
return timedelta()
@property
def package_speed_waypoints(self) -> set[FlightWaypoint]:
return {
self.ingress,
self.split,
} | set(self.targets)
self.layout.ingress,
self.layout.split,
} | set(self.layout.targets)
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
# FlightWaypoint is only comparable by identity, so adding
@ -94,7 +47,7 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
@property
def tot_waypoint(self) -> FlightWaypoint:
return self.targets[0]
return self.layout.targets[0]
@property
def tot_offset(self) -> timedelta:
@ -138,18 +91,20 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
@property
def join_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(self.join, self.ingress)
travel_time = self.travel_time_between_waypoints(
self.layout.join, self.layout.ingress
)
return self.ingress_time - travel_time
@property
def split_time(self) -> timedelta:
travel_time_ingress = self.travel_time_between_waypoints(
self.ingress, self.target_area_waypoint
self.layout.ingress, self.target_area_waypoint
)
travel_time_egress = self.travel_time_between_waypoints(
self.target_area_waypoint, self.split
self.target_area_waypoint, self.layout.split
)
minutes_at_target = 0.75 * len(self.targets)
minutes_at_target = 0.75 * len(self.layout.targets)
timedelta_at_target = timedelta(minutes=minutes_at_target)
return (
self.ingress_time
@ -162,29 +117,49 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
def ingress_time(self) -> timedelta:
tot = self.tot
travel_time = self.travel_time_between_waypoints(
self.ingress, self.target_area_waypoint
self.layout.ingress, self.target_area_waypoint
)
return tot - travel_time
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.ingress:
if waypoint == self.layout.ingress:
return self.ingress_time
elif waypoint in self.targets:
elif waypoint in self.layout.targets:
return self.tot
return super().tot_for_waypoint(waypoint)
FlightPlanT = TypeVar("FlightPlanT", bound=FormationAttackFlightPlan)
@dataclass(frozen=True)
class FormationAttackLayout(FormationLayout):
ingress: FlightWaypoint
targets: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield self.hold
yield from self.nav_to
yield self.join
yield self.ingress
yield from self.targets
yield self.split
if self.refuel is not None:
yield self.refuel
yield from self.nav_from
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
class FormationAttackBuilder(IBuilder, ABC, Generic[FlightPlanT]):
LayoutT = TypeVar("LayoutT", bound=FormationAttackLayout)
class FormationAttackBuilder(IBuilder, ABC):
def _build(
self,
plan_type: Type[FlightPlanT],
ingress_type: FlightWaypointType,
targets: list[StrikeTarget] | None = None,
lead_time: timedelta = timedelta(),
) -> FlightPlanT:
) -> FormationAttackLayout:
assert self.package.waypoints is not None
builder = WaypointBuilder(self.flight, self.coalition, targets)
@ -208,11 +183,9 @@ class FormationAttackBuilder(IBuilder, ABC, Generic[FlightPlanT]):
if self.package.waypoints.refuel is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
return plan_type(
flight=self.flight,
return FormationAttackLayout(
departure=builder.takeoff(self.flight.departure),
hold=hold,
hold_duration=timedelta(minutes=5),
nav_to=builder.nav_path(
hold.position, join.position, self.doctrine.ingress_altitude
),
@ -231,7 +204,6 @@ class FormationAttackBuilder(IBuilder, ABC, Generic[FlightPlanT]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
lead_time=lead_time,
)
@staticmethod

View File

@ -10,7 +10,7 @@ if TYPE_CHECKING:
from game.threatzones import ThreatZones
from ..flight import Flight
from ..package import Package
from .flightplan import FlightPlan
from .flightplan import Layout
class IBuilder(ABC):
@ -19,7 +19,7 @@ class IBuilder(ABC):
self.theater = theater
@abstractmethod
def build(self) -> FlightPlan:
def build(self) -> Layout:
...
@property

View File

@ -1,36 +1,27 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, TypeGuard
from typing import Any, TYPE_CHECKING, TypeGuard
from game.typeguard import self_type_guard
from .flightplan import FlightPlan
from .standard import StandardFlightPlan
from .standard import StandardFlightPlan, StandardLayout
if TYPE_CHECKING:
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint
class LoiterFlightPlan(StandardFlightPlan, ABC):
def __init__(
self,
flight: Flight,
departure: FlightWaypoint,
arrival: FlightWaypoint,
divert: FlightWaypoint | None,
bullseye: FlightWaypoint,
nav_to: list[FlightWaypoint],
nav_from: list[FlightWaypoint],
hold: FlightWaypoint,
hold_duration: timedelta,
) -> None:
super().__init__(flight, departure, arrival, divert, bullseye)
self.nav_to = nav_to
self.nav_from = nav_from
self.hold = hold
self.hold_duration = hold_duration
@dataclass(frozen=True)
class LoiterLayout(StandardLayout, ABC):
hold: FlightWaypoint
class LoiterFlightPlan(StandardFlightPlan[Any], ABC):
@property
def hold_duration(self) -> timedelta:
return timedelta(minutes=5)
@property
@abstractmethod
@ -38,7 +29,7 @@ class LoiterFlightPlan(StandardFlightPlan, ABC):
...
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.hold:
if waypoint == self.layout.hold:
return self.push_time
return None
@ -46,10 +37,10 @@ class LoiterFlightPlan(StandardFlightPlan, ABC):
self, a: FlightWaypoint, b: FlightWaypoint
) -> timedelta:
travel_time = super().travel_time_between_waypoints(a, b)
if a != self.hold:
if a != self.layout.hold:
return travel_time
return travel_time + self.hold_duration
@self_type_guard
def is_loiter(self, flight_plan: FlightPlan) -> TypeGuard[LoiterFlightPlan]:
def is_loiter(self, flight_plan: FlightPlan[Any]) -> TypeGuard[LoiterFlightPlan]:
return True

View File

@ -4,7 +4,11 @@ import logging
from typing import Type
from game.theater import Airfield
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
from .formationattack import (
FormationAttackBuilder,
FormationAttackFlightPlan,
FormationAttackLayout,
)
from .invalidobjectivelocation import InvalidObjectiveLocation
from ..flightwaypointtype import FlightWaypointType
@ -15,8 +19,8 @@ class OcaAircraftFlightPlan(FormationAttackFlightPlan):
return Builder
class Builder(FormationAttackBuilder[OcaAircraftFlightPlan]):
def build(self) -> FormationAttackFlightPlan:
class Builder(FormationAttackBuilder):
def build(self) -> FormationAttackLayout:
location = self.package.target
if not isinstance(location, Airfield):
@ -26,6 +30,4 @@ class Builder(FormationAttackBuilder[OcaAircraftFlightPlan]):
)
raise InvalidObjectiveLocation(self.flight.flight_type, location)
return self._build(
OcaAircraftFlightPlan, FlightWaypointType.INGRESS_OCA_AIRCRAFT
)
return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT)

View File

@ -4,7 +4,11 @@ import logging
from typing import Type
from game.theater import Airfield
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
from .formationattack import (
FormationAttackBuilder,
FormationAttackFlightPlan,
FormationAttackLayout,
)
from .invalidobjectivelocation import InvalidObjectiveLocation
from ..flightwaypointtype import FlightWaypointType
@ -15,8 +19,8 @@ class OcaRunwayFlightPlan(FormationAttackFlightPlan):
return Builder
class Builder(FormationAttackBuilder[OcaRunwayFlightPlan]):
def build(self) -> FormationAttackFlightPlan:
class Builder(FormationAttackBuilder):
def build(self) -> FormationAttackLayout:
location = self.package.target
if not isinstance(location, Airfield):
@ -26,4 +30,4 @@ class Builder(FormationAttackBuilder[OcaRunwayFlightPlan]):
)
raise InvalidObjectiveLocation(self.flight.flight_type, location)
return self._build(OcaRunwayFlightPlan, FlightWaypointType.INGRESS_OCA_RUNWAY)
return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY)

View File

@ -5,7 +5,8 @@ from typing import Type
from dcs import Point
from game.utils import Distance, Heading, feet, knots, meters
from game.utils import Distance, Heading, feet, meters
from .patrolling import PatrollingLayout
from .theaterrefueling import (
Builder as TheaterRefuelingBuilder,
TheaterRefuelingFlightPlan,
@ -16,18 +17,11 @@ from ..flightwaypointtype import FlightWaypointType
class Builder(TheaterRefuelingBuilder):
def build(self) -> PackageRefuelingFlightPlan:
def build(self) -> PatrollingLayout:
package_waypoints = self.package.waypoints
assert package_waypoints is not None
racetrack_half_distance = Distance.from_nautical_miles(20).meters
# TODO: Only consider aircraft that can refuel with this tanker type.
refuel_time_minutes = 5
for self.flight in self.package.flights:
flight_size = self.flight.roster.max_size
refuel_time_minutes = refuel_time_minutes + 4 * flight_size + 1
patrol_duration = timedelta(minutes=refuel_time_minutes)
racetrack_center = package_waypoints.refuel
@ -52,17 +46,9 @@ class Builder(TheaterRefuelingBuilder):
else:
altitude = feet(21000)
# TODO: Could use self.flight.unit_type.preferred_patrol_speed(altitude).
if tanker_type.patrol_speed is not None:
speed = tanker_type.patrol_speed
else:
# ~280 knots IAS at 21000.
speed = knots(400)
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
return PackageRefuelingFlightPlan(
flight=self.flight,
return PatrollingLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position, racetrack_start, altitude
@ -75,11 +61,6 @@ class Builder(TheaterRefuelingBuilder):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
patrol_duration=patrol_duration,
patrol_speed=speed,
# 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.
engagement_distance=meters(0),
)
@ -88,6 +69,16 @@ class PackageRefuelingFlightPlan(TheaterRefuelingFlightPlan):
def builder_type() -> Type[Builder]:
return Builder
@property
def patrol_duration(self) -> timedelta:
# TODO: Only consider aircraft that can refuel with this tanker type.
refuel_time_minutes = 5
for self.flight in self.package.flights:
flight_size = self.flight.roster.max_size
refuel_time_minutes = refuel_time_minutes + 4 * flight_size + 1
return timedelta(minutes=refuel_time_minutes)
def target_area_waypoint(self) -> FlightWaypoint:
return FlightWaypoint(
"TARGET AREA",

View File

@ -1,53 +1,63 @@
from __future__ import annotations
from abc import ABC
from abc import ABC, abstractmethod
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, TypeGuard
from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar
from game.ato.flightplans.standard import StandardFlightPlan
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
from game.typeguard import self_type_guard
from game.utils import Distance, Speed
if TYPE_CHECKING:
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint
from .flightplan import FlightPlan
class PatrollingFlightPlan(StandardFlightPlan, ABC):
def __init__(
self,
flight: Flight,
departure: FlightWaypoint,
arrival: FlightWaypoint,
divert: FlightWaypoint | None,
bullseye: FlightWaypoint,
nav_to: list[FlightWaypoint],
nav_from: list[FlightWaypoint],
patrol_start: FlightWaypoint,
patrol_end: FlightWaypoint,
patrol_duration: timedelta,
patrol_speed: Speed,
engagement_distance: Distance,
) -> None:
super().__init__(flight, departure, arrival, divert, bullseye)
self.nav_to = nav_to
self.nav_from = nav_from
self.patrol_start = patrol_start
self.patrol_end = patrol_end
@dataclass(frozen=True)
class PatrollingLayout(StandardLayout):
nav_to: list[FlightWaypoint]
patrol_start: FlightWaypoint
patrol_end: FlightWaypoint
nav_from: list[FlightWaypoint]
# Maximum time to remain on station.
self.patrol_duration = patrol_duration
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to
yield self.patrol_start
yield self.patrol_end
yield from self.nav_from
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
# Racetrack speed TAS.
self.patrol_speed = patrol_speed
# The engagement range of any Search Then Engage task, or the radius of a
# Search Then Engage in Zone task. Any enemies of the appropriate type for
# this mission within this range of the flight's current position (or the
# center of the zone) will be engaged by the flight.
self.engagement_distance = engagement_distance
LayoutT = TypeVar("LayoutT", bound=PatrollingLayout)
class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC):
@property
@abstractmethod
def patrol_duration(self) -> timedelta:
"""Maximum time to remain on station."""
@property
@abstractmethod
def patrol_speed(self) -> Speed:
"""Racetrack speed TAS."""
@property
@abstractmethod
def engagement_distance(self) -> Distance:
"""The maximum engagement distance.
The engagement range of any Search Then Engage task, or the radius of a Search
Then Engage in Zone task. Any enemies of the appropriate type for this mission
within this range of the flight's current position (or the center of the zone)
will be engaged by the flight.
"""
@property
def patrol_start_time(self) -> timedelta:
@ -61,38 +71,29 @@ class PatrollingFlightPlan(StandardFlightPlan, ABC):
return self.patrol_start_time + self.patrol_duration
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.patrol_start:
if waypoint == self.layout.patrol_start:
return self.patrol_start_time
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.patrol_end:
if waypoint == self.layout.patrol_end:
return self.patrol_end_time
return None
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to
yield self.patrol_start
yield self.patrol_end
yield from self.nav_from
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
@property
def package_speed_waypoints(self) -> set[FlightWaypoint]:
return {self.patrol_start, self.patrol_end}
return {self.layout.patrol_start, self.layout.patrol_end}
@property
def tot_waypoint(self) -> FlightWaypoint | None:
return self.patrol_start
return self.layout.patrol_start
@property
def mission_departure_time(self) -> timedelta:
return self.patrol_end_time
@self_type_guard
def is_patrol(self, flight_plan: FlightPlan) -> TypeGuard[PatrollingFlightPlan]:
def is_patrol(
self, flight_plan: FlightPlan[Any]
) -> TypeGuard[PatrollingFlightPlan[Any]]:
return True

View File

@ -1,22 +1,22 @@
from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, Type
from game.utils import feet
from .ibuilder import IBuilder
from .standard import StandardFlightPlan
from .standard import StandardFlightPlan, StandardLayout
from .waypointbuilder import WaypointBuilder
from ..flightstate import InFlight
if TYPE_CHECKING:
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint
class Builder(IBuilder):
def build(self) -> RtbFlightPlan:
def build(self) -> RtbLayout:
if not isinstance(self.flight.state, InFlight):
raise RuntimeError(f"Cannot abort {self} because it is not in flight")
@ -36,8 +36,7 @@ class Builder(IBuilder):
abort_point.name = "ABORT AND RTB"
abort_point.pretty_name = "Abort and RTB"
abort_point.description = "Abort mission and return to base"
return RtbFlightPlan(
flight=self.flight,
return RtbLayout(
departure=builder.takeoff(self.flight.departure),
abort_location=abort_point,
nav_to_destination=builder.nav_path(
@ -52,24 +51,10 @@ class Builder(IBuilder):
)
class RtbFlightPlan(StandardFlightPlan):
def __init__(
self,
flight: Flight,
departure: FlightWaypoint,
arrival: FlightWaypoint,
divert: FlightWaypoint | None,
bullseye: FlightWaypoint,
abort_location: FlightWaypoint,
nav_to_destination: list[FlightWaypoint],
) -> None:
super().__init__(flight, departure, arrival, divert, bullseye)
self.abort_location = abort_location
self.nav_to_destination = nav_to_destination
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@dataclass(frozen=True)
class RtbLayout(StandardLayout):
abort_location: FlightWaypoint
nav_to_destination: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
@ -80,6 +65,12 @@ class RtbFlightPlan(StandardFlightPlan):
yield self.divert
yield self.bullseye
class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def abort_index(self) -> int:
return 1

View File

@ -3,20 +3,28 @@ from __future__ import annotations
from datetime import timedelta
from typing import Type
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
from .formationattack import (
FormationAttackBuilder,
FormationAttackFlightPlan,
FormationAttackLayout,
)
from .. import Flight
from ..flightwaypointtype import FlightWaypointType
class SeadFlightPlan(FormationAttackFlightPlan):
def __init__(self, flight: Flight, layout: FormationAttackLayout) -> None:
super().__init__(flight, layout)
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def lead_time(self) -> timedelta:
return timedelta(minutes=1)
class Builder(FormationAttackBuilder[SeadFlightPlan]):
def build(self) -> FormationAttackFlightPlan:
return self._build(
SeadFlightPlan,
FlightWaypointType.INGRESS_SEAD,
lead_time=timedelta(minutes=1),
)
class Builder(FormationAttackBuilder):
def build(self) -> FormationAttackLayout:
return self._build(FlightWaypointType.INGRESS_SEAD)

View File

@ -1,33 +1,30 @@
from __future__ import annotations
from abc import ABC
from typing import TYPE_CHECKING
from dataclasses import dataclass
from typing import TYPE_CHECKING, TypeVar
from game.ato.flightplans.flightplan import FlightPlan
from game.ato.flightplans.flightplan import FlightPlan, Layout
if TYPE_CHECKING:
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint
class StandardFlightPlan(FlightPlan, ABC):
@dataclass(frozen=True)
class StandardLayout(Layout, ABC):
departure: FlightWaypoint
arrival: FlightWaypoint
divert: FlightWaypoint | None
bullseye: FlightWaypoint
LayoutT = TypeVar("LayoutT", bound=StandardLayout)
class StandardFlightPlan(FlightPlan[LayoutT], ABC):
"""Base type for all non-custom flight plans.
We can't reason about custom flight plans so they get special treatment, but all
others are guaranteed to have certain properties like departure and arrival points,
potentially a divert field, and a bullseye
"""
def __init__(
self,
flight: Flight,
departure: FlightWaypoint,
arrival: FlightWaypoint,
divert: FlightWaypoint | None,
bullseye: FlightWaypoint,
) -> None:
super().__init__(flight)
self.departure = departure
self.arrival = arrival
self.divert = divert
self.bullseye = bullseye

View File

@ -3,7 +3,11 @@ from __future__ import annotations
from typing import Type
from game.theater import TheaterGroundObject
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
from .formationattack import (
FormationAttackBuilder,
FormationAttackFlightPlan,
FormationAttackLayout,
)
from .invalidobjectivelocation import InvalidObjectiveLocation
from .waypointbuilder import StrikeTarget
from ..flightwaypointtype import FlightWaypointType
@ -15,8 +19,8 @@ class StrikeFlightPlan(FormationAttackFlightPlan):
return Builder
class Builder(FormationAttackBuilder[StrikeFlightPlan]):
def build(self) -> FormationAttackFlightPlan:
class Builder(FormationAttackBuilder):
def build(self) -> FormationAttackLayout:
location = self.package.target
if not isinstance(location, TheaterGroundObject):
@ -26,4 +30,4 @@ class Builder(FormationAttackBuilder[StrikeFlightPlan]):
for idx, unit in enumerate(location.strike_targets):
targets.append(StrikeTarget(f"{unit.type.id} #{idx}", unit))
return self._build(StrikeFlightPlan, FlightWaypointType.INGRESS_STRIKE, targets)
return self._build(FlightWaypointType.INGRESS_STRIKE, targets)

View File

@ -1,5 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from typing import Iterator, TYPE_CHECKING, Type
@ -7,18 +8,17 @@ from dcs import Point
from game.utils import Heading
from .ibuilder import IBuilder
from .loiter import LoiterFlightPlan
from .loiter import LoiterFlightPlan, LoiterLayout
from .waypointbuilder import WaypointBuilder
from ..traveltime import GroundSpeed, TravelTime
from ...flightplan import HoldZoneGeometry
if TYPE_CHECKING:
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint
class Builder(IBuilder):
def build(self) -> SweepFlightPlan:
def build(self) -> SweepLayout:
assert self.package.waypoints is not None
target = self.package.target.position
heading = Heading.from_degrees(
@ -38,12 +38,9 @@ class Builder(IBuilder):
if self.package.waypoints is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
return SweepFlightPlan(
flight=self.flight,
lead_time=timedelta(minutes=5),
return SweepLayout(
departure=builder.takeoff(self.flight.departure),
hold=hold,
hold_duration=timedelta(minutes=5),
nav_to=builder.nav_path(
hold.position, start.position, self.doctrine.ingress_altitude
),
@ -71,42 +68,13 @@ class Builder(IBuilder):
).find_best_hold_point()
class SweepFlightPlan(LoiterFlightPlan):
def __init__(
self,
flight: Flight,
departure: FlightWaypoint,
arrival: FlightWaypoint,
divert: FlightWaypoint | None,
bullseye: FlightWaypoint,
nav_to: list[FlightWaypoint],
nav_from: list[FlightWaypoint],
hold: FlightWaypoint,
hold_duration: timedelta,
sweep_start: FlightWaypoint,
sweep_end: FlightWaypoint,
refuel: FlightWaypoint,
lead_time: timedelta,
) -> None:
super().__init__(
flight,
departure,
arrival,
divert,
bullseye,
nav_to,
nav_from,
hold,
hold_duration,
)
self.sweep_start = sweep_start
self.sweep_end = sweep_end
self.refuel = refuel
self.lead_time = lead_time
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@dataclass(frozen=True)
class SweepLayout(LoiterLayout):
nav_to: list[FlightWaypoint]
sweep_start: FlightWaypoint
sweep_end: FlightWaypoint
refuel: FlightWaypoint | None
nav_from: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
@ -122,13 +90,23 @@ class SweepFlightPlan(LoiterFlightPlan):
yield self.divert
yield self.bullseye
class SweepFlightPlan(LoiterFlightPlan):
@property
def lead_time(self) -> timedelta:
return timedelta(minutes=5)
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
return {self.sweep_end}
return {self.layout.sweep_end}
@property
def tot_waypoint(self) -> FlightWaypoint | None:
return self.sweep_end
return self.layout.sweep_end
@property
def tot_offset(self) -> timedelta:
@ -137,7 +115,7 @@ class SweepFlightPlan(LoiterFlightPlan):
@property
def sweep_start_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(
self.sweep_start, self.sweep_end
self.layout.sweep_start, self.layout.sweep_end
)
return self.sweep_end_time - travel_time
@ -146,23 +124,23 @@ class SweepFlightPlan(LoiterFlightPlan):
return self.tot
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.sweep_start:
if waypoint == self.layout.sweep_start:
return self.sweep_start_time
if waypoint == self.sweep_end:
if waypoint == self.layout.sweep_end:
return self.sweep_end_time
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.hold:
if waypoint == self.layout.hold:
return self.push_time
return None
@property
def push_time(self) -> timedelta:
return self.sweep_end_time - TravelTime.between_points(
self.hold.position,
self.sweep_end.position,
GroundSpeed.for_flight(self.flight, self.hold.alt),
self.layout.hold.position,
self.layout.sweep_end.position,
GroundSpeed.for_flight(self.flight, self.layout.hold.alt),
)
def mission_departure_time(self) -> timedelta:

View File

@ -2,21 +2,21 @@ from __future__ import annotations
import random
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, Type
from game.utils import Distance, Speed, feet
from .capbuilder import CapBuilder
from .patrolling import PatrollingFlightPlan
from .patrolling import PatrollingFlightPlan, PatrollingLayout
from .waypointbuilder import WaypointBuilder
if TYPE_CHECKING:
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint
class Builder(CapBuilder):
def build(self) -> TarCapFlightPlan:
def build(self) -> TarCapLayout:
location = self.package.target
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
@ -25,7 +25,6 @@ class Builder(CapBuilder):
self.doctrine.min_patrol_altitude,
min(self.doctrine.max_patrol_altitude, randomized_alt),
)
patrol_speed = self.flight.unit_type.preferred_patrol_speed(patrol_alt)
builder = WaypointBuilder(self.flight, self.coalition)
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
@ -37,16 +36,7 @@ class Builder(CapBuilder):
if self.package.waypoints is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
return TarCapFlightPlan(
flight=self.flight,
lead_time=timedelta(minutes=2),
# Note that this duration only has an effect if there are no
# flights in the package that have requested escort. If the package
# requests an escort the CAP self.flight will remain on station for the
# duration of the escorted mission, or until it is winchester/bingo.
patrol_duration=self.doctrine.cap_duration,
patrol_speed=patrol_speed,
engagement_distance=self.doctrine.cap_engagement_range,
return TarCapLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position, orbit0p, patrol_alt
@ -63,44 +53,9 @@ class Builder(CapBuilder):
)
class TarCapFlightPlan(PatrollingFlightPlan):
def __init__(
self,
flight: Flight,
departure: FlightWaypoint,
arrival: FlightWaypoint,
divert: FlightWaypoint | None,
bullseye: FlightWaypoint,
nav_to: list[FlightWaypoint],
nav_from: list[FlightWaypoint],
patrol_start: FlightWaypoint,
patrol_end: FlightWaypoint,
patrol_duration: timedelta,
patrol_speed: Speed,
engagement_distance: Distance,
refuel: FlightWaypoint | None,
lead_time: timedelta,
) -> None:
super().__init__(
flight,
departure,
arrival,
divert,
bullseye,
nav_to,
nav_from,
patrol_start,
patrol_end,
patrol_duration,
patrol_speed,
engagement_distance,
)
self.refuel = refuel
self.lead_time = lead_time
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@dataclass(frozen=True)
class TarCapLayout(PatrollingLayout):
refuel: FlightWaypoint | None
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
@ -115,16 +70,44 @@ class TarCapFlightPlan(PatrollingFlightPlan):
yield self.divert
yield self.bullseye
class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
@property
def lead_time(self) -> timedelta:
return timedelta(minutes=2)
@property
def patrol_duration(self) -> timedelta:
# Note that this duration only has an effect if there are no
# flights in the package that have requested escort. If the package
# requests an escort the CAP self.flight will remain on station for the
# duration of the escorted mission, or until it is winchester/bingo.
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
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
return {self.patrol_start, self.patrol_end}
return {self.layout.patrol_start, self.layout.patrol_end}
@property
def tot_offset(self) -> timedelta:
return -self.lead_time
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.patrol_end:
if waypoint == self.layout.patrol_end:
return self.patrol_end_time
return super().depart_time_for_waypoint(waypoint)

View File

@ -3,18 +3,16 @@ from __future__ import annotations
from datetime import timedelta
from typing import Type
from game.utils import Heading, feet, knots, meters, nautical_miles
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
from .ibuilder import IBuilder
from .patrolling import PatrollingFlightPlan
from .patrolling import PatrollingFlightPlan, PatrollingLayout
from .waypointbuilder import WaypointBuilder
class Builder(IBuilder):
def build(self) -> TheaterRefuelingFlightPlan:
def build(self) -> PatrollingLayout:
racetrack_half_distance = nautical_miles(20).meters
patrol_duration = timedelta(hours=1)
location = self.package.target
closest_boundary = self.threat_zones.closest_boundary(location.position)
@ -53,17 +51,9 @@ class Builder(IBuilder):
else:
altitude = feet(21000)
# TODO: Could use self.flight.unit_type.preferred_patrol_speed(altitude).
if tanker_type.patrol_speed is not None:
speed = tanker_type.patrol_speed
else:
# ~280 knots IAS at 21000.
speed = knots(400)
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
return TheaterRefuelingFlightPlan(
flight=self.flight,
return PatrollingLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position, racetrack_start, altitude
@ -76,15 +66,28 @@ class Builder(IBuilder):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
patrol_duration=patrol_duration,
patrol_speed=speed,
# 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.
engagement_distance=meters(0),
)
class TheaterRefuelingFlightPlan(PatrollingFlightPlan):
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)

View File

@ -13,7 +13,7 @@ class CasIngressBuilder(PydcsWaypointBuilder):
if isinstance(self.flight.flight_plan, CasFlightPlan):
waypoint.add_task(
EngageTargetsInZone(
position=self.flight.flight_plan.target.position,
position=self.flight.flight_plan.layout.target.position,
radius=int(self.flight.flight_plan.engagement_distance.meters),
targets=[
Targets.All.GroundUnits.GroundVehicles,

View File

@ -4,8 +4,8 @@ from fastapi import APIRouter, Depends
from shapely.geometry import LineString, Point as ShapelyPoint
from game import Game
from game.ato.flightplans.patrolling import PatrollingFlightPlan
from game.ato.flightplans.cas import CasFlightPlan
from game.ato.flightplans.patrolling import PatrollingFlightPlan
from game.server import GameContext
from game.server.flights.models import FlightJs
from game.server.leaflet import LeafletPoly, ShapelyUtil
@ -41,10 +41,10 @@ def commit_boundary(
flight = game.db.flights.get(flight_id)
if not isinstance(flight.flight_plan, PatrollingFlightPlan):
return []
start = flight.flight_plan.patrol_start
end = flight.flight_plan.patrol_end
start = flight.flight_plan.layout.patrol_start
end = flight.flight_plan.layout.patrol_end
if isinstance(flight.flight_plan, CasFlightPlan):
center = flight.flight_plan.target.position
center = flight.flight_plan.layout.target.position
commit_center = ShapelyPoint(center.x, center.y)
else:
commit_center = LineString(