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