mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
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:
parent
fa8c0d9660
commit
769fe12159
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = (
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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] = []
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user