mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Clean up flight plan code.
Split the oversized file into one per plan type. This also moves the layout responsibility out of the oversized FlightPlanBuilder and into each flight plan type file.
This commit is contained in:
parent
c5fd3df235
commit
fa8c0d9660
@ -9,7 +9,7 @@ from dcs.planes import C_101CC, C_101EB, Su_33
|
||||
|
||||
from game.savecompat import has_save_compat_for
|
||||
from .flightroster import FlightRoster
|
||||
from .flightstate import FlightState, InFlight, Navigating, Uninitialized
|
||||
from .flightstate import FlightState, Navigating, Uninitialized
|
||||
from .flightstate.killed import Killed
|
||||
from .loadouts import Loadout
|
||||
from ..sidc import (
|
||||
@ -84,11 +84,10 @@ 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.flightplan import FlightPlan, CustomFlightPlan
|
||||
from game.ato.flightplans.flightplan import FlightPlan
|
||||
from .flightplans.custom import CustomFlightPlan
|
||||
|
||||
self.flight_plan: FlightPlan = CustomFlightPlan(
|
||||
package=package, flight=self, custom_waypoints=[]
|
||||
)
|
||||
self.flight_plan: FlightPlan = CustomFlightPlan(self, [])
|
||||
|
||||
def __getstate__(self) -> dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
@ -195,16 +194,11 @@ class Flight(SidcDescribable):
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
|
||||
def abort(self) -> None:
|
||||
from game.ato.flightplan import RtbFlightPlan
|
||||
from .flightplans.rtb import RtbFlightPlan
|
||||
|
||||
if not isinstance(self.state, InFlight):
|
||||
raise RuntimeError(f"Cannot abort {self} because it is not in flight")
|
||||
|
||||
altitude, altitude_reference = self.state.estimate_altitude()
|
||||
|
||||
self.flight_plan = RtbFlightPlan.create_for_abort(
|
||||
self, self.state.estimate_position(), altitude, altitude_reference
|
||||
)
|
||||
self.flight_plan = RtbFlightPlan.builder_type()(
|
||||
self, self.coalition.game.theater
|
||||
).build()
|
||||
|
||||
self.set_state(
|
||||
Navigating(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
0
game/ato/flightplans/__init__.py
Normal file
0
game/ato/flightplans/__init__.py
Normal file
87
game/ato/flightplans/aewc.py
Normal file
87
game/ato/flightplans/aewc.py
Normal file
@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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.waypointbuilder import WaypointBuilder
|
||||
from game.utils import Heading, feet, knots, meters, nautical_miles
|
||||
|
||||
|
||||
class Builder(IBuilder):
|
||||
def build(self) -> AewcFlightPlan:
|
||||
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)
|
||||
heading_to_threat_boundary = Heading.from_degrees(
|
||||
location.position.heading_between_point(closest_boundary)
|
||||
)
|
||||
distance_to_threat = meters(
|
||||
location.position.distance_to_point(closest_boundary)
|
||||
)
|
||||
orbit_heading = heading_to_threat_boundary
|
||||
|
||||
# Station 80nm outside the threat zone.
|
||||
threat_buffer = nautical_miles(80)
|
||||
if self.threat_zones.threatened(location.position):
|
||||
orbit_distance = distance_to_threat + threat_buffer
|
||||
else:
|
||||
orbit_distance = distance_to_threat - threat_buffer
|
||||
|
||||
racetrack_center = location.position.point_from_heading(
|
||||
orbit_heading.degrees, orbit_distance.meters
|
||||
)
|
||||
|
||||
racetrack_start = racetrack_center.point_from_heading(
|
||||
orbit_heading.right.degrees, racetrack_half_distance
|
||||
)
|
||||
|
||||
racetrack_end = racetrack_center.point_from_heading(
|
||||
orbit_heading.left.degrees, racetrack_half_distance
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
|
||||
if self.flight.unit_type.patrol_altitude is not None:
|
||||
altitude = self.flight.unit_type.patrol_altitude
|
||||
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,
|
||||
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(),
|
||||
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):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[IBuilder]:
|
||||
return Builder
|
||||
118
game/ato/flightplans/airlift.py
Normal file
118
game/ato/flightplans/airlift.py
Normal file
@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
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 .waypointbuilder import WaypointBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flight import Flight
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
class Builder(IBuilder):
|
||||
def build(self) -> AirliftFlightPlan:
|
||||
cargo = self.flight.cargo
|
||||
if cargo is None:
|
||||
raise PlanningError(
|
||||
"Cannot plan transport mission for flight with no cargo."
|
||||
)
|
||||
|
||||
altitude = feet(1500)
|
||||
altitude_is_agl = True
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
|
||||
pickup = None
|
||||
nav_to_pickup = []
|
||||
if cargo.origin != self.flight.departure:
|
||||
pickup = builder.pickup(cargo.origin)
|
||||
nav_to_pickup = builder.nav_path(
|
||||
self.flight.departure.position,
|
||||
cargo.origin.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
)
|
||||
|
||||
return AirliftFlightPlan(
|
||||
flight=self.flight,
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to_pickup=nav_to_pickup,
|
||||
pickup=pickup,
|
||||
nav_to_drop_off=builder.nav_path(
|
||||
cargo.origin.position,
|
||||
cargo.next_stop.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
drop_off=builder.drop_off(cargo.next_stop),
|
||||
nav_to_home=builder.nav_path(
|
||||
cargo.origin.position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
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 from self.nav_to_home
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint | None:
|
||||
return self.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
|
||||
38
game/ato/flightplans/antiship.py
Normal file
38
game/ato/flightplans/antiship.py
Normal file
@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Type
|
||||
|
||||
from game.theater import NavalControlPoint
|
||||
from game.theater.theatergroundobject import NavalGroundObject
|
||||
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from .waypointbuilder import StrikeTarget
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
|
||||
class AntiShipFlightPlan(FormationAttackFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[AntiShipFlightPlan]):
|
||||
def build(self) -> FormationAttackFlightPlan:
|
||||
location = self.package.target
|
||||
|
||||
from game.transfers import CargoShip
|
||||
|
||||
if isinstance(location, NavalControlPoint):
|
||||
targets = self.anti_ship_targets_for_tgo(location.find_main_tgo())
|
||||
elif isinstance(location, NavalGroundObject):
|
||||
targets = self.anti_ship_targets_for_tgo(location)
|
||||
elif isinstance(location, CargoShip):
|
||||
targets = [StrikeTarget(location.name, location)]
|
||||
else:
|
||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||
|
||||
return self._build(AntiShipFlightPlan, FlightWaypointType.INGRESS_BAI, targets)
|
||||
|
||||
@staticmethod
|
||||
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]:
|
||||
return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups]
|
||||
36
game/ato/flightplans/bai.py
Normal file
36
game/ato/flightplans/bai.py
Normal file
@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Type
|
||||
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from .waypointbuilder import StrikeTarget
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
|
||||
class BaiFlightPlan(FormationAttackFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[BaiFlightPlan]):
|
||||
def build(self) -> FormationAttackFlightPlan:
|
||||
location = self.package.target
|
||||
|
||||
from game.transfers import Convoy
|
||||
|
||||
targets: list[StrikeTarget] = []
|
||||
if isinstance(location, TheaterGroundObject):
|
||||
for group in location.groups:
|
||||
if group.units:
|
||||
targets.append(
|
||||
StrikeTarget(f"{group.group_name} at {location.name}", group)
|
||||
)
|
||||
elif isinstance(location, Convoy):
|
||||
targets.append(StrikeTarget(location.name, location))
|
||||
else:
|
||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||
|
||||
return self._build(BaiFlightPlan, FlightWaypointType.INGRESS_BAI, targets)
|
||||
58
game/ato/flightplans/barcap.py
Normal file
58
game/ato/flightplans/barcap.py
Normal file
@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Type
|
||||
|
||||
from game.theater import FrontLine
|
||||
from game.utils import feet
|
||||
from .capbuilder import CapBuilder
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from .patrolling import PatrollingFlightPlan
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
|
||||
|
||||
class Builder(CapBuilder):
|
||||
def build(self) -> BarCapFlightPlan:
|
||||
location = self.package.target
|
||||
|
||||
if isinstance(location, FrontLine):
|
||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||
|
||||
start_pos, end_pos = self.cap_racetrack_for_objective(location, barcap=True)
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
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,
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
self.flight.departure.position, start.position, patrol_alt
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
end.position, self.flight.arrival.position, patrol_alt
|
||||
),
|
||||
patrol_start=start,
|
||||
patrol_end=end,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
|
||||
class BarCapFlightPlan(PatrollingFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
88
game/ato/flightplans/capbuilder.py
Normal file
88
game/ato/flightplans/capbuilder.py
Normal file
@ -0,0 +1,88 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from abc import ABC
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
from shapely.geometry import Point as ShapelyPoint
|
||||
|
||||
from game.utils import Heading, meters, nautical_miles
|
||||
from ..closestairfields import ObjectiveDistanceCache
|
||||
from ..flightplans.ibuilder import IBuilder
|
||||
from ..flightplans.planningerror import PlanningError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import MissionTarget
|
||||
|
||||
|
||||
class CapBuilder(IBuilder, ABC):
|
||||
def cap_racetrack_for_objective(
|
||||
self, location: MissionTarget, barcap: bool
|
||||
) -> tuple[Point, Point]:
|
||||
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
for airfield in closest_cache.operational_airfields:
|
||||
# If the mission is a BARCAP of an enemy airfield, find the *next*
|
||||
# closest enemy airfield.
|
||||
if airfield == self.package.target:
|
||||
continue
|
||||
if airfield.captured != self.is_player:
|
||||
closest_airfield = airfield
|
||||
break
|
||||
else:
|
||||
raise PlanningError("Could not find any enemy airfields")
|
||||
|
||||
heading = Heading.from_degrees(
|
||||
location.position.heading_between_point(closest_airfield.position)
|
||||
)
|
||||
|
||||
position = ShapelyPoint(
|
||||
self.package.target.position.x, self.package.target.position.y
|
||||
)
|
||||
|
||||
if barcap:
|
||||
# BARCAPs should remain far enough back from the enemy that their
|
||||
# commit range does not enter the enemy's threat zone. Include a 5nm
|
||||
# buffer.
|
||||
distance_to_no_fly = (
|
||||
meters(position.distance(self.threat_zones.all))
|
||||
- self.doctrine.cap_engagement_range
|
||||
- nautical_miles(5)
|
||||
)
|
||||
max_track_length = self.doctrine.cap_max_track_length
|
||||
else:
|
||||
# Other race tracks (TARCAPs, currently) just try to keep some
|
||||
# distance from the nearest enemy airbase, but since they are by
|
||||
# definition in enemy territory they can't avoid the threat zone
|
||||
# without being useless.
|
||||
min_distance_from_enemy = nautical_miles(20)
|
||||
distance_to_airfield = meters(
|
||||
closest_airfield.position.distance_to_point(
|
||||
self.package.target.position
|
||||
)
|
||||
)
|
||||
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
|
||||
|
||||
# TARCAPs fly short racetracks because they need to react faster.
|
||||
max_track_length = self.doctrine.cap_min_track_length + 0.3 * (
|
||||
self.doctrine.cap_max_track_length - self.doctrine.cap_min_track_length
|
||||
)
|
||||
|
||||
min_cap_distance = min(
|
||||
self.doctrine.cap_min_distance_from_cp, distance_to_no_fly
|
||||
)
|
||||
max_cap_distance = min(
|
||||
self.doctrine.cap_max_distance_from_cp, distance_to_no_fly
|
||||
)
|
||||
|
||||
end = location.position.point_from_heading(
|
||||
heading.degrees,
|
||||
random.randint(int(min_cap_distance.meters), int(max_cap_distance.meters)),
|
||||
)
|
||||
|
||||
track_length = random.randint(
|
||||
int(self.doctrine.cap_min_track_length.meters),
|
||||
int(max_track_length.meters),
|
||||
)
|
||||
start = end.point_from_heading(heading.opposite.degrees, track_length)
|
||||
return start, end
|
||||
144
game/ato/flightplans/cas.py
Normal file
144
game/ato/flightplans/cas.py
Normal file
@ -0,0 +1,144 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.theater import FrontLine
|
||||
from game.utils import Distance, Speed, meters
|
||||
from .ibuilder import IBuilder
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from .patrolling import PatrollingFlightPlan
|
||||
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:
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, FrontLine):
|
||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||
|
||||
from game.missiongenerator.frontlineconflictdescription import (
|
||||
FrontLineConflictDescription,
|
||||
)
|
||||
|
||||
ingress, heading, distance = FrontLineConflictDescription.frontline_vector(
|
||||
location, self.theater
|
||||
)
|
||||
center = ingress.point_from_heading(heading.degrees, distance / 2)
|
||||
egress = ingress.point_from_heading(heading.degrees, distance)
|
||||
|
||||
ingress_distance = ingress.distance_to_point(self.flight.departure.position)
|
||||
egress_distance = egress.distance_to_point(self.flight.departure.position)
|
||||
if egress_distance < ingress_distance:
|
||||
ingress, egress = egress, ingress
|
||||
|
||||
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,
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
self.flight.departure.position,
|
||||
ingress,
|
||||
ingress_egress_altitude,
|
||||
use_agl_ingress_egress,
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
egress,
|
||||
self.flight.arrival.position,
|
||||
ingress_egress_altitude,
|
||||
use_agl_ingress_egress,
|
||||
),
|
||||
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),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
return {self.patrol_start, self.target, self.patrol_end}
|
||||
|
||||
def request_escort_at(self) -> FlightWaypoint | None:
|
||||
return self.patrol_start
|
||||
|
||||
def dismiss_escort_at(self) -> FlightWaypoint | None:
|
||||
return self.patrol_end
|
||||
56
game/ato/flightplans/custom.py
Normal file
56
game/ato/flightplans/custom.py
Normal file
@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from .flightplan import FlightPlan
|
||||
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, [])
|
||||
|
||||
|
||||
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
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield from self.custom_waypoints
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint | None:
|
||||
target_types = (
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
FlightWaypointType.TARGET_SHIP,
|
||||
)
|
||||
for waypoint in self.waypoints:
|
||||
if waypoint in target_types:
|
||||
return waypoint
|
||||
return None
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
if waypoint == self.tot_waypoint:
|
||||
return self.package.time_over_target
|
||||
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
|
||||
34
game/ato/flightplans/dead.py
Normal file
34
game/ato/flightplans/dead.py
Normal file
@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Type
|
||||
|
||||
from game.theater.theatergroundobject import (
|
||||
EwrGroundObject,
|
||||
SamGroundObject,
|
||||
)
|
||||
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
|
||||
class DeadFlightPlan(FormationAttackFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[DeadFlightPlan]):
|
||||
def build(self) -> FormationAttackFlightPlan:
|
||||
location = self.package.target
|
||||
|
||||
is_ewr = isinstance(location, EwrGroundObject)
|
||||
is_sam = isinstance(location, SamGroundObject)
|
||||
if not is_ewr and not is_sam:
|
||||
logging.exception(
|
||||
f"Invalid Objective Location for DEAD flight {self.flight=} at "
|
||||
f"{location=}"
|
||||
)
|
||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||
|
||||
return self._build(DeadFlightPlan, FlightWaypointType.INGRESS_DEAD)
|
||||
52
game/ato/flightplans/escort.py
Normal file
52
game/ato/flightplans/escort.py
Normal file
@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Type
|
||||
|
||||
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
|
||||
|
||||
class EscortFlightPlan(FormationAttackFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[EscortFlightPlan]):
|
||||
def build(self) -> FormationAttackFlightPlan:
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
ingress, target = builder.escort(
|
||||
self.package.waypoints.ingress, self.package.target
|
||||
)
|
||||
hold = builder.hold(self._hold_point())
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
refuel = None
|
||||
if self.package.waypoints.refuel is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
|
||||
return EscortFlightPlan(
|
||||
flight=self.flight,
|
||||
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
|
||||
),
|
||||
join=join,
|
||||
ingress=ingress,
|
||||
targets=[target],
|
||||
split=split,
|
||||
refuel=refuel,
|
||||
nav_from=builder.nav_path(
|
||||
split.position,
|
||||
self.flight.arrival.position,
|
||||
self.doctrine.ingress_altitude,
|
||||
),
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
88
game/ato/flightplans/ferry.py
Normal file
88
game/ato/flightplans/ferry.py
Normal file
@ -0,0 +1,88 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
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 .waypointbuilder import WaypointBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flight import Flight
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
class Builder(IBuilder):
|
||||
def build(self) -> FerryFlightPlan:
|
||||
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 FerryFlightPlan(
|
||||
flight=self.flight,
|
||||
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(),
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to_destination
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint | None:
|
||||
return self.arrival
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
# TOT planning isn't really useful for ferries. 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
|
||||
308
game/ato/flightplans/flightplan.py
Normal file
308
game/ato/flightplans/flightplan.py
Normal file
@ -0,0 +1,308 @@
|
||||
"""Flight plan generation.
|
||||
|
||||
Flights are first planned generically by either the player or by the
|
||||
MissionPlanner. Those only plan basic information like the objective, aircraft
|
||||
type, and the size of the flight. The FlightPlanBuilder is responsible for
|
||||
generating the waypoints for the mission.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
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 game.typeguard import self_type_guard
|
||||
from game.utils import Distance, Speed, meters
|
||||
from .ibuilder import IBuilder
|
||||
from .planningerror import PlanningError
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ..starttype import StartType
|
||||
from ..traveltime import GroundSpeed, TravelTime
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import FuelConsumption
|
||||
from game.theater import ControlPoint
|
||||
from ..flight import Flight
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
from ..package import Package
|
||||
from .formation import FormationFlightPlan
|
||||
from .loiter import LoiterFlightPlan
|
||||
from .patrolling import PatrollingFlightPlan
|
||||
|
||||
INGRESS_TYPES = {
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_ESCORT,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
FlightWaypointType.INGRESS_DEAD,
|
||||
}
|
||||
|
||||
|
||||
class FlightPlan(ABC):
|
||||
def __init__(self, flight: Flight) -> None:
|
||||
self.flight = flight
|
||||
|
||||
@property
|
||||
def package(self) -> Package:
|
||||
return self.flight.package
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def builder_type() -> Type[IBuilder]:
|
||||
...
|
||||
|
||||
@property
|
||||
def waypoints(self) -> list[FlightWaypoint]:
|
||||
"""A list of all waypoints in the flight plan, in order."""
|
||||
return list(self.iter_waypoints())
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
"""Iterates over all waypoints in the flight plan, in order."""
|
||||
raise NotImplementedError
|
||||
|
||||
def edges(
|
||||
self, until: FlightWaypoint | None = None
|
||||
) -> Iterator[tuple[FlightWaypoint, FlightWaypoint]]:
|
||||
"""A list of all paths between waypoints, in order."""
|
||||
waypoints = self.waypoints
|
||||
if until is None:
|
||||
last_index = len(waypoints)
|
||||
else:
|
||||
last_index = waypoints.index(until) + 1
|
||||
|
||||
return zip(self.waypoints[:last_index], self.waypoints[1:last_index])
|
||||
|
||||
def best_speed_between_waypoints(
|
||||
self, a: FlightWaypoint, b: FlightWaypoint
|
||||
) -> Speed:
|
||||
"""Desired ground speed between points a and b."""
|
||||
factor = 1.0
|
||||
if b.waypoint_type == FlightWaypointType.ASCEND_POINT:
|
||||
# Flights that start airborne already have some altitude and a good
|
||||
# amount of speed.
|
||||
factor = 0.5
|
||||
elif b.waypoint_type == FlightWaypointType.LOITER:
|
||||
# On the way to the hold point the AI won't climb unless they're in
|
||||
# formation, so slowing down the flight lead gives them more time to
|
||||
# form up and climb.
|
||||
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/dcs-wishlist-aa/7121300-ai-flights-will-not-climb-to-hold-point-because-wingman-not-joined
|
||||
#
|
||||
# Plus, it's a loiter point so there's no reason to hurry.
|
||||
factor = 0.75
|
||||
# TODO: Adjust if AGL.
|
||||
# We don't have an exact heightmap, but we should probably be performing
|
||||
# *some* adjustment for NTTR since the minimum altitude of the map is
|
||||
# near 2000 ft MSL.
|
||||
return GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
|
||||
return self.best_speed_between_waypoints(a, b)
|
||||
|
||||
@property
|
||||
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
return set()
|
||||
|
||||
def fuel_consumption_between_points(
|
||||
self, a: FlightWaypoint, b: FlightWaypoint
|
||||
) -> float | None:
|
||||
ppm = self.fuel_rate_to_between_points(a, b)
|
||||
if ppm is None:
|
||||
return None
|
||||
distance = meters(a.position.distance_to_point(b.position))
|
||||
return distance.nautical_miles * ppm
|
||||
|
||||
def fuel_rate_to_between_points(
|
||||
self, a: FlightWaypoint, b: FlightWaypoint
|
||||
) -> float | None:
|
||||
if self.flight.unit_type.fuel_consumption is None:
|
||||
return None
|
||||
if a.waypoint_type is FlightWaypointType.TAKEOFF:
|
||||
return self.flight.unit_type.fuel_consumption.climb
|
||||
if b in self.combat_speed_waypoints:
|
||||
return self.flight.unit_type.fuel_consumption.combat
|
||||
return self.flight.unit_type.fuel_consumption.cruise
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint | None:
|
||||
"""The waypoint that is associated with the package TOT, or None.
|
||||
|
||||
Note that the only flight plans that should have no target waypoints are
|
||||
user-planned missions without any useful waypoints and flight plans that
|
||||
failed to generate. Nevertheless, we have to defend against it.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def tot(self) -> timedelta:
|
||||
return self.package.time_over_target + self.tot_offset
|
||||
|
||||
@cached_property
|
||||
def bingo_fuel(self) -> int:
|
||||
"""Bingo fuel value for the FlightPlan"""
|
||||
if (fuel := self.flight.unit_type.fuel_consumption) is not None:
|
||||
return self._bingo_estimate(fuel)
|
||||
return self._legacy_bingo_estimate()
|
||||
|
||||
def _bingo_estimate(self, fuel: FuelConsumption) -> int:
|
||||
distance_to_arrival = self.max_distance_from(self.flight.arrival)
|
||||
fuel_consumed = fuel.cruise * distance_to_arrival.nautical_miles
|
||||
bingo = fuel_consumed + fuel.min_safe
|
||||
return math.ceil(bingo / 100) * 100
|
||||
|
||||
def _legacy_bingo_estimate(self) -> int:
|
||||
distance_to_arrival = self.max_distance_from(self.flight.arrival)
|
||||
|
||||
bingo = 1000.0 # Minimum Emergency Fuel
|
||||
bingo += 500 # Visual Traffic
|
||||
bingo += 15 * distance_to_arrival.nautical_miles
|
||||
|
||||
# TODO: Per aircraft tweaks.
|
||||
|
||||
if self.flight.divert is not None:
|
||||
max_divert_distance = self.max_distance_from(self.flight.divert)
|
||||
bingo += 10 * max_divert_distance.nautical_miles
|
||||
|
||||
return round(bingo / 100) * 100
|
||||
|
||||
@cached_property
|
||||
def joker_fuel(self) -> int:
|
||||
"""Joker fuel value for the FlightPlan"""
|
||||
return self.bingo_fuel + 1000
|
||||
|
||||
def max_distance_from(self, cp: ControlPoint) -> Distance:
|
||||
"""Returns the farthest waypoint of the flight plan from a ControlPoint.
|
||||
:arg cp The ControlPoint to measure distance from.
|
||||
"""
|
||||
if not self.waypoints:
|
||||
return meters(0)
|
||||
return max(
|
||||
[meters(cp.position.distance_to_point(w.position)) for w in self.waypoints]
|
||||
)
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
"""This flight's offset from the package's TOT.
|
||||
|
||||
Positive values represent later TOTs. An offset of -2 minutes is used
|
||||
for a flight that has a TOT 2 minutes before the rest of the package.
|
||||
"""
|
||||
return timedelta()
|
||||
|
||||
def _travel_time_to_waypoint(self, destination: FlightWaypoint) -> timedelta:
|
||||
total = timedelta()
|
||||
|
||||
if destination not in self.waypoints:
|
||||
raise PlanningError(
|
||||
f"Did not find destination waypoint {destination} in "
|
||||
f"waypoints for {self.flight}"
|
||||
)
|
||||
|
||||
for previous_waypoint, waypoint in self.edges(until=destination):
|
||||
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
|
||||
return total
|
||||
|
||||
def travel_time_between_waypoints(
|
||||
self, a: FlightWaypoint, b: FlightWaypoint
|
||||
) -> timedelta:
|
||||
return TravelTime.between_points(
|
||||
a.position, b.position, self.speed_between_waypoints(a, b)
|
||||
)
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
raise NotImplementedError
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
raise NotImplementedError
|
||||
|
||||
def request_escort_at(self) -> FlightWaypoint | None:
|
||||
return None
|
||||
|
||||
def dismiss_escort_at(self) -> FlightWaypoint | None:
|
||||
return None
|
||||
|
||||
def escorted_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
begin = self.request_escort_at()
|
||||
end = self.dismiss_escort_at()
|
||||
if begin is None or end is None:
|
||||
return
|
||||
escorting = False
|
||||
for waypoint in self.waypoints:
|
||||
if waypoint == begin:
|
||||
escorting = True
|
||||
if escorting:
|
||||
yield waypoint
|
||||
if waypoint == end:
|
||||
return
|
||||
|
||||
def takeoff_time(self) -> timedelta | None:
|
||||
tot_waypoint = self.tot_waypoint
|
||||
if tot_waypoint is None:
|
||||
return None
|
||||
return self.tot - self._travel_time_to_waypoint(tot_waypoint)
|
||||
|
||||
def startup_time(self) -> timedelta | None:
|
||||
takeoff_time = self.takeoff_time()
|
||||
if takeoff_time is None:
|
||||
return None
|
||||
|
||||
start_time: timedelta = (
|
||||
takeoff_time - self.estimate_startup() - self.estimate_ground_ops()
|
||||
)
|
||||
|
||||
# In case FP math has given us some barely below zero time, round to
|
||||
# zero.
|
||||
if math.isclose(start_time.total_seconds(), 0):
|
||||
start_time = timedelta()
|
||||
|
||||
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
|
||||
# and they're not interesting from a mission planning perspective so we
|
||||
# don't want them in the UI.
|
||||
#
|
||||
# Round down so *barely* above zero start times are just zero.
|
||||
start_time = timedelta(seconds=math.floor(start_time.total_seconds()))
|
||||
|
||||
# Feature request #1309: Carrier planes should start at +1s
|
||||
# This is a workaround to a DCS problem: some AI planes spawn on
|
||||
# the 'sixpack' when start_time is zero and cause a deadlock.
|
||||
# Workaround: force the start_time to 1 second for these planes.
|
||||
if self.flight.from_cp.is_fleet and start_time.total_seconds() == 0:
|
||||
start_time = timedelta(seconds=1)
|
||||
|
||||
return start_time
|
||||
|
||||
def estimate_startup(self) -> timedelta:
|
||||
if self.flight.start_type is StartType.COLD:
|
||||
if self.flight.client_count:
|
||||
return timedelta(minutes=10)
|
||||
else:
|
||||
# The AI doesn't seem to have a real startup procedure.
|
||||
return timedelta(minutes=2)
|
||||
return timedelta()
|
||||
|
||||
def estimate_ground_ops(self) -> timedelta:
|
||||
if self.flight.start_type in {StartType.RUNWAY, StartType.IN_FLIGHT}:
|
||||
return timedelta()
|
||||
if self.flight.from_cp.is_fleet:
|
||||
return timedelta(minutes=2)
|
||||
else:
|
||||
return timedelta(minutes=8)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
"""The time that the mission is complete and the flight RTBs."""
|
||||
raise NotImplementedError
|
||||
|
||||
@self_type_guard
|
||||
def is_loiter(self, flight_plan: FlightPlan) -> TypeGuard[LoiterFlightPlan]:
|
||||
return False
|
||||
|
||||
@self_type_guard
|
||||
def is_patrol(self, flight_plan: FlightPlan) -> TypeGuard[PatrollingFlightPlan]:
|
||||
return False
|
||||
|
||||
@self_type_guard
|
||||
def is_formation(self, flight_plan: FlightPlan) -> TypeGuard[FormationFlightPlan]:
|
||||
return False
|
||||
193
game/ato/flightplans/flightplanbuilder.py
Normal file
193
game/ato/flightplans/flightplanbuilder.py
Normal file
@ -0,0 +1,193 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.ato import FlightType
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.flightplan import IpZoneGeometry, JoinZoneGeometry
|
||||
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
|
||||
from .aewc import AewcFlightPlan
|
||||
from .airlift import AirliftFlightPlan
|
||||
from .antiship import AntiShipFlightPlan
|
||||
from .bai import BaiFlightPlan
|
||||
from .barcap import BarCapFlightPlan
|
||||
from .cas import CasFlightPlan
|
||||
from .dead import DeadFlightPlan
|
||||
from .escort import EscortFlightPlan
|
||||
from .ferry import FerryFlightPlan
|
||||
from .flightplan import FlightPlan
|
||||
from .ocaaircraft import OcaAircraftFlightPlan
|
||||
from .ocarunway import OcaRunwayFlightPlan
|
||||
from .packagerefueling import PackageRefuelingFlightPlan
|
||||
from .planningerror import PlanningError
|
||||
from .sead import SeadFlightPlan
|
||||
from .strike import StrikeFlightPlan
|
||||
from .sweep import SweepFlightPlan
|
||||
from .tarcap import TarCapFlightPlan
|
||||
from .theaterrefueling import TheaterRefuelingFlightPlan
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato import Flight, FlightWaypoint, Package
|
||||
from game.coalition import Coalition
|
||||
from game.theater import ConflictTheater, ControlPoint, FrontLine
|
||||
from game.threatzones import ThreatZones
|
||||
|
||||
|
||||
class FlightPlanBuilder:
|
||||
"""Generates flight plans for flights."""
|
||||
|
||||
def __init__(
|
||||
self, package: Package, coalition: Coalition, theater: ConflictTheater
|
||||
) -> None:
|
||||
# TODO: Plan similar altitudes for the in-country leg of the mission.
|
||||
# Waypoint altitudes for a given flight *shouldn't* differ too much
|
||||
# between the join and split points, so we don't need speeds for each
|
||||
# leg individually since they should all be fairly similar. This doesn't
|
||||
# hold too well right now since nothing is stopping each waypoint from
|
||||
# jumping 20k feet each time, but that's a huge waste of energy we
|
||||
# should be avoiding anyway.
|
||||
self.package = package
|
||||
self.coalition = coalition
|
||||
self.theater = theater
|
||||
|
||||
@property
|
||||
def is_player(self) -> bool:
|
||||
return self.coalition.player
|
||||
|
||||
@property
|
||||
def doctrine(self) -> Doctrine:
|
||||
return self.coalition.doctrine
|
||||
|
||||
@property
|
||||
def threat_zones(self) -> ThreatZones:
|
||||
return self.coalition.opponent.threat_zone
|
||||
|
||||
def populate_flight_plan(self, flight: Flight) -> None:
|
||||
"""Creates a default flight plan for the given mission."""
|
||||
if flight not in self.package.flights:
|
||||
raise RuntimeError("Flight must be a part of the package")
|
||||
|
||||
from game.navmesh import NavMeshError
|
||||
|
||||
try:
|
||||
if self.package.waypoints is None:
|
||||
self.regenerate_package_waypoints()
|
||||
flight.flight_plan = self.generate_flight_plan(flight)
|
||||
except NavMeshError as ex:
|
||||
color = "blue" if self.is_player else "red"
|
||||
raise PlanningError(
|
||||
f"Could not plan {color} {flight.flight_type.value} from "
|
||||
f"{flight.departure} to {flight.package.target}"
|
||||
) from ex
|
||||
|
||||
def plan_type(self, task: FlightType) -> Type[FlightPlan] | None:
|
||||
plan_type: Type[FlightPlan]
|
||||
if task == FlightType.REFUELING:
|
||||
if self.package.target.is_friendly(self.is_player) or isinstance(
|
||||
self.package.target, FrontLine
|
||||
):
|
||||
return TheaterRefuelingFlightPlan
|
||||
return PackageRefuelingFlightPlan
|
||||
|
||||
plan_dict: dict[FlightType, Type[FlightPlan]] = {
|
||||
FlightType.ANTISHIP: AntiShipFlightPlan,
|
||||
FlightType.BAI: BaiFlightPlan,
|
||||
FlightType.BARCAP: BarCapFlightPlan,
|
||||
FlightType.CAS: CasFlightPlan,
|
||||
FlightType.DEAD: DeadFlightPlan,
|
||||
FlightType.ESCORT: EscortFlightPlan,
|
||||
FlightType.OCA_AIRCRAFT: OcaAircraftFlightPlan,
|
||||
FlightType.OCA_RUNWAY: OcaRunwayFlightPlan,
|
||||
FlightType.SEAD: SeadFlightPlan,
|
||||
FlightType.SEAD_ESCORT: EscortFlightPlan,
|
||||
FlightType.STRIKE: StrikeFlightPlan,
|
||||
FlightType.SWEEP: SweepFlightPlan,
|
||||
FlightType.TARCAP: TarCapFlightPlan,
|
||||
FlightType.AEWC: AewcFlightPlan,
|
||||
FlightType.TRANSPORT: AirliftFlightPlan,
|
||||
FlightType.FERRY: FerryFlightPlan,
|
||||
}
|
||||
return plan_dict.get(task)
|
||||
|
||||
def generate_flight_plan(self, flight: Flight) -> FlightPlan:
|
||||
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()
|
||||
|
||||
def regenerate_flight_plans(self) -> None:
|
||||
new_flights: list[Flight] = []
|
||||
for old_flight in self.package.flights:
|
||||
old_flight.flight_plan = self.generate_flight_plan(old_flight)
|
||||
new_flights.append(old_flight)
|
||||
self.package.flights = new_flights
|
||||
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
from game.ato.packagewaypoints import PackageWaypoints
|
||||
|
||||
package_airfield = self.package_airfield()
|
||||
|
||||
# Start by picking the best IP for the attack.
|
||||
ingress_point = IpZoneGeometry(
|
||||
self.package.target.position,
|
||||
package_airfield.position,
|
||||
self.coalition,
|
||||
).find_best_ip()
|
||||
|
||||
join_point = JoinZoneGeometry(
|
||||
self.package.target.position,
|
||||
package_airfield.position,
|
||||
ingress_point,
|
||||
self.coalition,
|
||||
).find_best_join_point()
|
||||
|
||||
refuel_point = RefuelZoneGeometry(
|
||||
package_airfield.position,
|
||||
join_point,
|
||||
self.coalition,
|
||||
).find_best_refuel_point()
|
||||
|
||||
# And the split point based on the best route from the IP. Since that's no
|
||||
# different than the best route *to* the IP, this is the same as the join point.
|
||||
# TODO: Estimate attack completion point based on the IP and split from there?
|
||||
self.package.waypoints = PackageWaypoints(
|
||||
WaypointBuilder.perturb(join_point),
|
||||
ingress_point,
|
||||
WaypointBuilder.perturb(join_point),
|
||||
refuel_point,
|
||||
)
|
||||
|
||||
# TODO: Make a model for the waypoint builder and use that in the UI.
|
||||
def generate_rtb_waypoint(
|
||||
self, flight: Flight, arrival: ControlPoint
|
||||
) -> FlightWaypoint:
|
||||
"""Generate RTB landing point.
|
||||
|
||||
Args:
|
||||
flight: The flight to generate the landing waypoint for.
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
return builder.land(arrival)
|
||||
|
||||
def package_airfield(self) -> ControlPoint:
|
||||
# We'll always have a package, but if this is being planned via the UI
|
||||
# it could be the first flight in the package.
|
||||
if not self.package.flights:
|
||||
raise PlanningError(
|
||||
"Cannot determine source airfield for package with no flights"
|
||||
)
|
||||
|
||||
# The package airfield is either the flight's airfield (when there is no
|
||||
# package) or the closest airfield to the objective that is the
|
||||
# departure airfield for some flight in the package.
|
||||
cache = ObjectiveDistanceCache.get_closest_airfields(self.package.target)
|
||||
for airfield in cache.operational_airfields:
|
||||
for flight in self.package.flights:
|
||||
if flight.departure == airfield:
|
||||
return airfield
|
||||
raise PlanningError("Could not find any airfield assigned to this package")
|
||||
126
game/ato/flightplans/formation.py
Normal file
126
game/ato/flightplans/formation.py
Normal file
@ -0,0 +1,126 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING, TypeGuard
|
||||
|
||||
from game.typeguard import self_type_guard
|
||||
from game.utils import Speed
|
||||
from .flightplan import FlightPlan
|
||||
from .loiter import LoiterFlightPlan
|
||||
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
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def package_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
...
|
||||
|
||||
@property
|
||||
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
return self.package_speed_waypoints
|
||||
|
||||
def request_escort_at(self) -> FlightWaypoint | None:
|
||||
return self.join
|
||||
|
||||
def dismiss_escort_at(self) -> FlightWaypoint | None:
|
||||
return self.split
|
||||
|
||||
@cached_property
|
||||
def best_flight_formation_speed(self) -> Speed:
|
||||
"""The best speed this flight is capable at all formation waypoints.
|
||||
|
||||
To ease coordination with other flights, we aim to have a single mission
|
||||
speed used by the formation for all waypoints. As such, this function
|
||||
returns the highest ground speed that the flight is capable of flying at
|
||||
all of its formation waypoints.
|
||||
"""
|
||||
speeds = []
|
||||
for previous_waypoint, waypoint in self.edges():
|
||||
if waypoint in self.package_speed_waypoints:
|
||||
speeds.append(
|
||||
self.best_speed_between_waypoints(previous_waypoint, waypoint)
|
||||
)
|
||||
return min(speeds)
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
|
||||
if b in self.package_speed_waypoints:
|
||||
# Should be impossible, as any package with at least one
|
||||
# FormationFlightPlan flight needs a formation speed.
|
||||
assert self.package.formation_speed is not None
|
||||
return self.package.formation_speed
|
||||
return super().speed_between_waypoints(a, b)
|
||||
|
||||
@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)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def join_time(self) -> timedelta:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def split_time(self) -> timedelta:
|
||||
...
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
if waypoint == self.join:
|
||||
return self.join_time
|
||||
elif waypoint == self.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),
|
||||
)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.split_time
|
||||
|
||||
@self_type_guard
|
||||
def is_formation(self, flight_plan: FlightPlan) -> TypeGuard[FormationFlightPlan]:
|
||||
return True
|
||||
271
game/ato/flightplans/formationattack.py
Normal file
271
game/ato/flightplans/formationattack.py
Normal file
@ -0,0 +1,271 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from collections.abc import Iterator
|
||||
from datetime import timedelta
|
||||
from typing import Generic, TYPE_CHECKING, Type, 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 .ibuilder import IBuilder
|
||||
from .planningerror import PlanningError
|
||||
from .waypointbuilder import StrikeTarget, WaypointBuilder
|
||||
from .. import FlightType
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flight import Flight
|
||||
|
||||
|
||||
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 package_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
return {
|
||||
self.ingress,
|
||||
self.split,
|
||||
} | set(self.targets)
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
|
||||
# FlightWaypoint is only comparable by identity, so adding
|
||||
# target_area_waypoint to package_speed_waypoints is useless.
|
||||
if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
|
||||
# Should be impossible, as any package with at least one
|
||||
# FormationFlightPlan flight needs a formation speed.
|
||||
assert self.package.formation_speed is not None
|
||||
return self.package.formation_speed
|
||||
return super().speed_between_waypoints(a, b)
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.targets[0]
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
try:
|
||||
return -self.lead_time
|
||||
except AttributeError:
|
||||
return timedelta()
|
||||
|
||||
@property
|
||||
def target_area_waypoint(self) -> FlightWaypoint:
|
||||
return FlightWaypoint(
|
||||
"TARGET AREA",
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
self.package.target.position,
|
||||
meters(0),
|
||||
"RADIO",
|
||||
)
|
||||
|
||||
@property
|
||||
def travel_time_to_target(self) -> timedelta:
|
||||
"""The estimated time between the first waypoint and the target."""
|
||||
destination = self.tot_waypoint
|
||||
total = timedelta()
|
||||
for previous_waypoint, waypoint in self.edges():
|
||||
if waypoint == self.tot_waypoint:
|
||||
# For anything strike-like the TOT waypoint is the *flight's*
|
||||
# mission target, but to synchronize with the rest of the
|
||||
# package we need to use the travel time to the same position as
|
||||
# the others.
|
||||
total += self.travel_time_between_waypoints(
|
||||
previous_waypoint, self.target_area_waypoint
|
||||
)
|
||||
break
|
||||
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
|
||||
else:
|
||||
raise PlanningError(
|
||||
f"Did not find destination waypoint {destination} in "
|
||||
f"waypoints for {self.flight}"
|
||||
)
|
||||
return total
|
||||
|
||||
@property
|
||||
def join_time(self) -> timedelta:
|
||||
travel_time = self.travel_time_between_waypoints(self.join, self.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
|
||||
)
|
||||
travel_time_egress = self.travel_time_between_waypoints(
|
||||
self.target_area_waypoint, self.split
|
||||
)
|
||||
minutes_at_target = 0.75 * len(self.targets)
|
||||
timedelta_at_target = timedelta(minutes=minutes_at_target)
|
||||
return (
|
||||
self.ingress_time
|
||||
+ travel_time_ingress
|
||||
+ timedelta_at_target
|
||||
+ travel_time_egress
|
||||
)
|
||||
|
||||
@property
|
||||
def ingress_time(self) -> timedelta:
|
||||
tot = self.tot
|
||||
travel_time = self.travel_time_between_waypoints(
|
||||
self.ingress, self.target_area_waypoint
|
||||
)
|
||||
return tot - travel_time
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
if waypoint == self.ingress:
|
||||
return self.ingress_time
|
||||
elif waypoint in self.targets:
|
||||
return self.tot
|
||||
return super().tot_for_waypoint(waypoint)
|
||||
|
||||
|
||||
FlightPlanT = TypeVar("FlightPlanT", bound=FormationAttackFlightPlan)
|
||||
|
||||
|
||||
class FormationAttackBuilder(IBuilder, ABC, Generic[FlightPlanT]):
|
||||
def _build(
|
||||
self,
|
||||
plan_type: Type[FlightPlanT],
|
||||
ingress_type: FlightWaypointType,
|
||||
targets: list[StrikeTarget] | None = None,
|
||||
lead_time: timedelta = timedelta(),
|
||||
) -> FlightPlanT:
|
||||
assert self.package.waypoints is not None
|
||||
builder = WaypointBuilder(self.flight, self.coalition, targets)
|
||||
|
||||
target_waypoints: list[FlightWaypoint] = []
|
||||
if targets is not None:
|
||||
for target in targets:
|
||||
target_waypoints.append(
|
||||
self.target_waypoint(self.flight, builder, target)
|
||||
)
|
||||
else:
|
||||
target_waypoints.append(
|
||||
self.target_area_waypoint(
|
||||
self.flight, self.flight.package.target, builder
|
||||
)
|
||||
)
|
||||
|
||||
hold = builder.hold(self._hold_point())
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
refuel = None
|
||||
if self.package.waypoints.refuel is not None:
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
|
||||
return plan_type(
|
||||
flight=self.flight,
|
||||
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
|
||||
),
|
||||
join=join,
|
||||
ingress=builder.ingress(
|
||||
ingress_type, self.package.waypoints.ingress, self.package.target
|
||||
),
|
||||
targets=target_waypoints,
|
||||
split=split,
|
||||
refuel=refuel,
|
||||
nav_from=builder.nav_path(
|
||||
split.position,
|
||||
self.flight.arrival.position,
|
||||
self.doctrine.ingress_altitude,
|
||||
),
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
lead_time=lead_time,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def target_waypoint(
|
||||
flight: Flight, builder: WaypointBuilder, target: StrikeTarget
|
||||
) -> FlightWaypoint:
|
||||
if flight.flight_type in {FlightType.ANTISHIP, FlightType.BAI}:
|
||||
return builder.bai_group(target)
|
||||
elif flight.flight_type == FlightType.DEAD:
|
||||
return builder.dead_point(target)
|
||||
elif flight.flight_type == FlightType.SEAD:
|
||||
return builder.sead_point(target)
|
||||
else:
|
||||
return builder.strike_point(target)
|
||||
|
||||
@staticmethod
|
||||
def target_area_waypoint(
|
||||
flight: Flight, location: MissionTarget, builder: WaypointBuilder
|
||||
) -> FlightWaypoint:
|
||||
if flight.flight_type == FlightType.DEAD:
|
||||
return builder.dead_area(location)
|
||||
elif flight.flight_type == FlightType.SEAD:
|
||||
return builder.sead_area(location)
|
||||
elif flight.flight_type == FlightType.OCA_AIRCRAFT:
|
||||
return builder.oca_strike_area(location)
|
||||
else:
|
||||
return builder.strike_area(location)
|
||||
|
||||
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()
|
||||
43
game/ato/flightplans/ibuilder.py
Normal file
43
game/ato/flightplans/ibuilder.py
Normal file
@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater import ConflictTheater
|
||||
from game.threatzones import ThreatZones
|
||||
from ..flight import Flight
|
||||
from ..package import Package
|
||||
from .flightplan import FlightPlan
|
||||
|
||||
|
||||
class IBuilder(ABC):
|
||||
def __init__(self, flight: Flight, theater: ConflictTheater) -> None:
|
||||
self.flight = flight
|
||||
self.theater = theater
|
||||
|
||||
@abstractmethod
|
||||
def build(self) -> FlightPlan:
|
||||
...
|
||||
|
||||
@property
|
||||
def package(self) -> Package:
|
||||
return self.flight.package
|
||||
|
||||
@property
|
||||
def coalition(self) -> Coalition:
|
||||
return self.flight.coalition
|
||||
|
||||
@property
|
||||
def is_player(self) -> bool:
|
||||
return self.coalition.player
|
||||
|
||||
@property
|
||||
def doctrine(self) -> Doctrine:
|
||||
return self.coalition.doctrine
|
||||
|
||||
@property
|
||||
def threat_zones(self) -> ThreatZones:
|
||||
return self.coalition.opponent.threat_zone
|
||||
12
game/ato/flightplans/invalidobjectivelocation.py
Normal file
12
game/ato/flightplans/invalidobjectivelocation.py
Normal file
@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from game.ato import FlightType
|
||||
from game.ato.flightplans.planningerror import PlanningError
|
||||
from game.theater import MissionTarget
|
||||
|
||||
|
||||
class InvalidObjectiveLocation(PlanningError):
|
||||
"""Raised when the objective location is invalid for the mission type."""
|
||||
|
||||
def __init__(self, task: FlightType, location: MissionTarget) -> None:
|
||||
super().__init__(f"{location.name} is not valid for {task} missions.")
|
||||
55
game/ato/flightplans/loiter.py
Normal file
55
game/ato/flightplans/loiter.py
Normal file
@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, TypeGuard
|
||||
|
||||
from game.typeguard import self_type_guard
|
||||
from .flightplan import FlightPlan
|
||||
from .standard import StandardFlightPlan
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def push_time(self) -> timedelta:
|
||||
...
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
if waypoint == self.hold:
|
||||
return self.push_time
|
||||
return None
|
||||
|
||||
def travel_time_between_waypoints(
|
||||
self, a: FlightWaypoint, b: FlightWaypoint
|
||||
) -> timedelta:
|
||||
travel_time = super().travel_time_between_waypoints(a, b)
|
||||
if a != self.hold:
|
||||
return travel_time
|
||||
return travel_time + self.hold_duration
|
||||
|
||||
@self_type_guard
|
||||
def is_loiter(self, flight_plan: FlightPlan) -> TypeGuard[LoiterFlightPlan]:
|
||||
return True
|
||||
31
game/ato/flightplans/ocaaircraft.py
Normal file
31
game/ato/flightplans/ocaaircraft.py
Normal file
@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Type
|
||||
|
||||
from game.theater import Airfield
|
||||
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
|
||||
class OcaAircraftFlightPlan(FormationAttackFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[OcaAircraftFlightPlan]):
|
||||
def build(self) -> FormationAttackFlightPlan:
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, Airfield):
|
||||
logging.exception(
|
||||
f"Invalid Objective Location for OCA/Aircraft flight "
|
||||
f"{self.flight=} at {location=}."
|
||||
)
|
||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||
|
||||
return self._build(
|
||||
OcaAircraftFlightPlan, FlightWaypointType.INGRESS_OCA_AIRCRAFT
|
||||
)
|
||||
29
game/ato/flightplans/ocarunway.py
Normal file
29
game/ato/flightplans/ocarunway.py
Normal file
@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Type
|
||||
|
||||
from game.theater import Airfield
|
||||
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
|
||||
class OcaRunwayFlightPlan(FormationAttackFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[OcaRunwayFlightPlan]):
|
||||
def build(self) -> FormationAttackFlightPlan:
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, Airfield):
|
||||
logging.exception(
|
||||
f"Invalid Objective Location for OCA/Runway flight "
|
||||
f"{self.flight=} at {location=}."
|
||||
)
|
||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||
|
||||
return self._build(OcaRunwayFlightPlan, FlightWaypointType.INGRESS_OCA_RUNWAY)
|
||||
133
game/ato/flightplans/packagerefueling.py
Normal file
133
game/ato/flightplans/packagerefueling.py
Normal file
@ -0,0 +1,133 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Type
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from game.utils import Distance, Heading, feet, knots, meters
|
||||
from .theaterrefueling import (
|
||||
Builder as TheaterRefuelingBuilder,
|
||||
TheaterRefuelingFlightPlan,
|
||||
)
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
|
||||
class Builder(TheaterRefuelingBuilder):
|
||||
def build(self) -> PackageRefuelingFlightPlan:
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
# 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,
|
||||
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(),
|
||||
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 PackageRefuelingFlightPlan(TheaterRefuelingFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
def target_area_waypoint(self) -> FlightWaypoint:
|
||||
return FlightWaypoint(
|
||||
"TARGET AREA",
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
self.package.target.position,
|
||||
meters(0),
|
||||
"RADIO",
|
||||
)
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
altitude = self.flight.unit_type.patrol_altitude
|
||||
|
||||
if altitude is None:
|
||||
altitude = Distance.from_feet(20000)
|
||||
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
# Cheat in a FlightWaypoint for the split point.
|
||||
split: Point = self.package.waypoints.split
|
||||
split_waypoint: FlightWaypoint = FlightWaypoint(
|
||||
"SPLIT", FlightWaypointType.SPLIT, split, altitude
|
||||
)
|
||||
|
||||
# Cheat in a FlightWaypoint for the refuel point.
|
||||
refuel: Point = self.package.waypoints.refuel
|
||||
refuel_waypoint: FlightWaypoint = FlightWaypoint(
|
||||
"REFUEL", FlightWaypointType.REFUEL, refuel, altitude
|
||||
)
|
||||
|
||||
delay_target_to_split: timedelta = self.travel_time_between_waypoints(
|
||||
self.target_area_waypoint(), split_waypoint
|
||||
)
|
||||
delay_split_to_refuel: timedelta = self.travel_time_between_waypoints(
|
||||
split_waypoint, refuel_waypoint
|
||||
)
|
||||
|
||||
return (
|
||||
self.package.time_over_target
|
||||
+ delay_target_to_split
|
||||
+ delay_split_to_refuel
|
||||
- timedelta(minutes=1.5)
|
||||
)
|
||||
98
game/ato/flightplans/patrolling.py
Normal file
98
game/ato/flightplans/patrolling.py
Normal file
@ -0,0 +1,98 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from collections.abc import Iterator
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, TypeGuard
|
||||
|
||||
from game.ato.flightplans.standard import StandardFlightPlan
|
||||
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
|
||||
|
||||
# Maximum time to remain on station.
|
||||
self.patrol_duration = patrol_duration
|
||||
|
||||
# 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
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
|
||||
@property
|
||||
def patrol_end_time(self) -> timedelta:
|
||||
# TODO: This is currently wrong for CAS.
|
||||
# CAS missions end when they're winchester or bingo. We need to
|
||||
# configure push tasks for the escorts rather than relying on timing.
|
||||
return self.patrol_start_time + self.patrol_duration
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
if waypoint == self.patrol_start:
|
||||
return self.patrol_start_time
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
if waypoint == self.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}
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint | None:
|
||||
return self.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]:
|
||||
return True
|
||||
5
game/ato/flightplans/planningerror.py
Normal file
5
game/ato/flightplans/planningerror.py
Normal file
@ -0,0 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class PlanningError(RuntimeError):
|
||||
"""Raised when the flight planner was unable to create a flight plan."""
|
||||
99
game/ato/flightplans/rtb.py
Normal file
99
game/ato/flightplans/rtb.py
Normal file
@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.utils import feet
|
||||
from .ibuilder import IBuilder
|
||||
from .standard import StandardFlightPlan
|
||||
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:
|
||||
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 RtbFlightPlan(
|
||||
flight=self.flight,
|
||||
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(),
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield self.abort_location
|
||||
yield from self.nav_to_destination
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
@property
|
||||
def abort_index(self) -> int:
|
||||
return 1
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint | None:
|
||||
return None
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return timedelta()
|
||||
22
game/ato/flightplans/sead.py
Normal file
22
game/ato/flightplans/sead.py
Normal file
@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Type
|
||||
|
||||
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
|
||||
class SeadFlightPlan(FormationAttackFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[SeadFlightPlan]):
|
||||
def build(self) -> FormationAttackFlightPlan:
|
||||
return self._build(
|
||||
SeadFlightPlan,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
lead_time=timedelta(minutes=1),
|
||||
)
|
||||
33
game/ato/flightplans/standard.py
Normal file
33
game/ato/flightplans/standard.py
Normal file
@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.ato.flightplans.flightplan import FlightPlan
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flight import Flight
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
class StandardFlightPlan(FlightPlan, 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
|
||||
29
game/ato/flightplans/strike.py
Normal file
29
game/ato/flightplans/strike.py
Normal file
@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Type
|
||||
|
||||
from game.theater import TheaterGroundObject
|
||||
from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from .waypointbuilder import StrikeTarget
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
|
||||
|
||||
class StrikeFlightPlan(FormationAttackFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[StrikeFlightPlan]):
|
||||
def build(self) -> FormationAttackFlightPlan:
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, TheaterGroundObject):
|
||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||
|
||||
targets: list[StrikeTarget] = []
|
||||
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)
|
||||
169
game/ato/flightplans/sweep.py
Normal file
169
game/ato/flightplans/sweep.py
Normal file
@ -0,0 +1,169 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Iterator, TYPE_CHECKING, Type
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from game.utils import Heading
|
||||
from .ibuilder import IBuilder
|
||||
from .loiter import LoiterFlightPlan
|
||||
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:
|
||||
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 SweepFlightPlan(
|
||||
flight=self.flight,
|
||||
lead_time=timedelta(minutes=5),
|
||||
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
|
||||
),
|
||||
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()
|
||||
|
||||
|
||||
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
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield self.hold
|
||||
yield from self.nav_to
|
||||
yield self.sweep_start
|
||||
yield self.sweep_end
|
||||
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 combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
return {self.sweep_end}
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint | None:
|
||||
return self.sweep_end
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
return -self.lead_time
|
||||
|
||||
@property
|
||||
def sweep_start_time(self) -> timedelta:
|
||||
travel_time = self.travel_time_between_waypoints(
|
||||
self.sweep_start, self.sweep_end
|
||||
)
|
||||
return self.sweep_end_time - travel_time
|
||||
|
||||
@property
|
||||
def sweep_end_time(self) -> timedelta:
|
||||
return self.tot
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
if waypoint == self.sweep_start:
|
||||
return self.sweep_start_time
|
||||
if waypoint == self.sweep_end:
|
||||
return self.sweep_end_time
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
if waypoint == self.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),
|
||||
)
|
||||
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.sweep_end_time
|
||||
143
game/ato/flightplans/tarcap.py
Normal file
143
game/ato/flightplans/tarcap.py
Normal file
@ -0,0 +1,143 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from collections.abc import Iterator
|
||||
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 .waypointbuilder import WaypointBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flight import Flight
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
class Builder(CapBuilder):
|
||||
def build(self) -> TarCapFlightPlan:
|
||||
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),
|
||||
)
|
||||
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)
|
||||
|
||||
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 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,
|
||||
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(),
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to
|
||||
yield self.patrol_start
|
||||
yield self.patrol_end
|
||||
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 combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
return {self.patrol_start, self.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:
|
||||
return self.patrol_end_time
|
||||
return super().depart_time_for_waypoint(waypoint)
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
start = self.package.escort_start_time
|
||||
if start is not None:
|
||||
return start + self.tot_offset
|
||||
return self.tot
|
||||
|
||||
@property
|
||||
def patrol_end_time(self) -> timedelta:
|
||||
end = self.package.escort_end_time
|
||||
if end is not None:
|
||||
return end
|
||||
return super().patrol_end_time
|
||||
90
game/ato/flightplans/theaterrefueling.py
Normal file
90
game/ato/flightplans/theaterrefueling.py
Normal file
@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Type
|
||||
|
||||
from game.utils import Heading, feet, knots, meters, nautical_miles
|
||||
from .ibuilder import IBuilder
|
||||
from .patrolling import PatrollingFlightPlan
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
|
||||
|
||||
class Builder(IBuilder):
|
||||
def build(self) -> TheaterRefuelingFlightPlan:
|
||||
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)
|
||||
heading_to_threat_boundary = Heading.from_degrees(
|
||||
location.position.heading_between_point(closest_boundary)
|
||||
)
|
||||
distance_to_threat = meters(
|
||||
location.position.distance_to_point(closest_boundary)
|
||||
)
|
||||
orbit_heading = heading_to_threat_boundary
|
||||
|
||||
# Station 70nm outside the threat zone.
|
||||
threat_buffer = nautical_miles(70)
|
||||
if self.threat_zones.threatened(location.position):
|
||||
orbit_distance = distance_to_threat + threat_buffer
|
||||
else:
|
||||
orbit_distance = distance_to_threat - threat_buffer
|
||||
|
||||
racetrack_center = location.position.point_from_heading(
|
||||
orbit_heading.degrees, orbit_distance.meters
|
||||
)
|
||||
|
||||
racetrack_start = racetrack_center.point_from_heading(
|
||||
orbit_heading.right.degrees, racetrack_half_distance
|
||||
)
|
||||
|
||||
racetrack_end = racetrack_center.point_from_heading(
|
||||
orbit_heading.left.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)
|
||||
|
||||
# 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,
|
||||
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(),
|
||||
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):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
@ -14,6 +14,8 @@ from typing import (
|
||||
|
||||
from dcs.mapping import Point, Vector2
|
||||
|
||||
from game.ato.flightwaypoint import AltitudeReference, FlightWaypoint
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
MissionTarget,
|
||||
@ -22,14 +24,12 @@ from game.theater import (
|
||||
TheaterUnit,
|
||||
)
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from .flightwaypoint import AltitudeReference, FlightWaypoint
|
||||
from .flightwaypointtype import FlightWaypointType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
from game.transfers import MultiGroupTransport
|
||||
from game.theater.theatergroup import TheaterGroup
|
||||
from .flight import Flight
|
||||
from game.ato.flight import Flight
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -6,10 +6,10 @@ from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
from .flightplans.formation import FormationFlightPlan
|
||||
from game.db import Database
|
||||
from game.utils import Speed
|
||||
from .flight import Flight
|
||||
from .flightplan import FormationFlightPlan
|
||||
from .flighttype import FlightType
|
||||
from .packagewaypoints import PackageWaypoints
|
||||
from .traveltime import TotEstimator
|
||||
|
||||
@ -5,6 +5,8 @@ from collections import defaultdict
|
||||
from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING
|
||||
|
||||
from game.ato.airtaaskingorder import AirTaskingOrder
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.ato.package import Package
|
||||
from game.commander.missionproposals import EscortType, ProposedFlight, ProposedMission
|
||||
@ -17,8 +19,6 @@ from game.settings import Settings
|
||||
from game.squadrons import AirWing
|
||||
from game.theater import ConflictTheater
|
||||
from game.threatzones import ThreatZones
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from game.ato.flightplan import FlightPlanBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato import Flight
|
||||
|
||||
@ -23,7 +23,8 @@ from dcs.task import (
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
|
||||
from game.ato import Flight, FlightType
|
||||
from game.ato.flightplan import AwacsFlightPlan, RefuelingFlightPlan
|
||||
from game.ato.flightplans.aewc import AewcFlightPlan
|
||||
from game.ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
|
||||
|
||||
|
||||
class AircraftBehavior:
|
||||
@ -221,7 +222,7 @@ class AircraftBehavior:
|
||||
def configure_awacs(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
group.task = AWACS.name
|
||||
|
||||
if not isinstance(flight.flight_plan, AwacsFlightPlan):
|
||||
if not isinstance(flight.flight_plan, AewcFlightPlan):
|
||||
logging.error(
|
||||
f"Cannot configure AEW&C tasks for {flight} because it does not have "
|
||||
"an AEW&C flight plan."
|
||||
@ -242,7 +243,7 @@ class AircraftBehavior:
|
||||
def configure_refueling(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
group.task = Refueling.name
|
||||
|
||||
if not isinstance(flight.flight_plan, RefuelingFlightPlan):
|
||||
if not isinstance(flight.flight_plan, TheaterRefuelingFlightPlan):
|
||||
logging.error(
|
||||
f"Cannot configure racetrack refueling tasks for {flight} because it "
|
||||
"does not have an racetrack refueling flight plan."
|
||||
|
||||
@ -18,11 +18,12 @@ from game.radio.radios import RadioFrequency, RadioRegistry
|
||||
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
|
||||
from game.runways import RunwayData
|
||||
from game.squadrons import Pilot
|
||||
from game.ato.flightplan import AwacsFlightPlan, RefuelingFlightPlan
|
||||
from .aircraftbehavior import AircraftBehavior
|
||||
from .aircraftpainter import AircraftPainter
|
||||
from .flightdata import FlightData
|
||||
from .waypoints import WaypointGenerator
|
||||
from ...ato.flightplans.aewc import AewcFlightPlan
|
||||
from ...ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -128,7 +129,7 @@ class FlightGroupConfigurator:
|
||||
|
||||
def register_air_support(self, channel: RadioFrequency) -> None:
|
||||
callsign = callsign_for_support_unit(self.group)
|
||||
if isinstance(self.flight.flight_plan, AwacsFlightPlan):
|
||||
if isinstance(self.flight.flight_plan, AewcFlightPlan):
|
||||
self.air_support.awacs.append(
|
||||
AwacsInfo(
|
||||
group_name=str(self.group.name),
|
||||
@ -136,11 +137,11 @@ class FlightGroupConfigurator:
|
||||
freq=channel,
|
||||
depature_location=self.flight.departure.name,
|
||||
end_time=self.flight.flight_plan.mission_departure_time,
|
||||
start_time=self.flight.flight_plan.mission_start_time,
|
||||
start_time=self.flight.flight_plan.takeoff_time(),
|
||||
blue=self.flight.departure.captured,
|
||||
)
|
||||
)
|
||||
elif isinstance(self.flight.flight_plan, RefuelingFlightPlan):
|
||||
elif isinstance(self.flight.flight_plan, TheaterRefuelingFlightPlan):
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir)
|
||||
self.air_support.tankers.append(
|
||||
TankerInfo(
|
||||
|
||||
@ -3,8 +3,8 @@ import logging
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import EngageTargets, EngageTargetsInZone, Targets
|
||||
|
||||
from game.ato.flightplans.cas import CasFlightPlan
|
||||
from game.utils import nautical_miles
|
||||
from game.ato.flightplan import CasFlightPlan
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import logging
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import ControlledTask, OptFormation, OrbitAction
|
||||
|
||||
from game.ato.flightplan import LoiterFlightPlan
|
||||
from game.ato.flightplans.loiter import LoiterFlightPlan
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ from dcs.task import (
|
||||
)
|
||||
|
||||
from game.ato import FlightType
|
||||
from game.ato.flightplan import PatrollingFlightPlan
|
||||
from game.ato.flightplans.patrolling import PatrollingFlightPlan
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import logging
|
||||
|
||||
from dcs.point import MovingPoint
|
||||
|
||||
from game.ato.flightplan import PatrollingFlightPlan
|
||||
from game.ato.flightplans.patrolling import PatrollingFlightPlan
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
|
||||
@ -3,8 +3,8 @@ import logging
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import EngageTargets, OptFormation, Targets
|
||||
|
||||
from game.ato.flightplans.sweep import SweepFlightPlan
|
||||
from game.utils import nautical_miles
|
||||
from game.ato.flightplan import SweepFlightPlan
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
|
||||
@ -4,7 +4,8 @@ from fastapi import APIRouter, Depends
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint
|
||||
|
||||
from game import Game
|
||||
from game.ato.flightplan import CasFlightPlan, PatrollingFlightPlan
|
||||
from game.ato.flightplans.patrolling import PatrollingFlightPlan
|
||||
from game.ato.flightplans.cas import CasFlightPlan
|
||||
from game.server import GameContext
|
||||
from game.server.flights.models import FlightJs
|
||||
from game.server.leaflet import LeafletPoly, ShapelyUtil
|
||||
|
||||
@ -9,9 +9,9 @@ from typing import Optional, Sequence, TYPE_CHECKING
|
||||
from faker import Faker
|
||||
|
||||
from game.ato import Flight, FlightType, Package
|
||||
from game.ato.flightplan import FlightPlanBuilder
|
||||
from game.settings import AutoAtoBehavior, Settings
|
||||
from .pilot import Pilot, PilotStatus
|
||||
from ..ato.flightplans.flightplanbuilder import FlightPlanBuilder
|
||||
from ..db.database import Database
|
||||
from ..utils import meters
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ from dcs.mapping import Point
|
||||
from game.ato.ai_flight_planner_db import aircraft_for_task
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from game.ato.flight import Flight
|
||||
from game.ato.flightplan import FlightPlanBuilder
|
||||
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.ato.package import Package
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
|
||||
@ -16,7 +16,8 @@ from PySide2.QtWidgets import (
|
||||
)
|
||||
|
||||
from game.ato.flight import Flight
|
||||
from game.ato.flightplan import FlightPlanBuilder, PlanningError
|
||||
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
|
||||
from game.ato.flightplans.planningerror import PlanningError
|
||||
from game.ato.package import Package
|
||||
from game.game import Game
|
||||
from game.server import EventStream
|
||||
|
||||
@ -4,7 +4,8 @@ from PySide2.QtWidgets import QGroupBox, QLabel, QMessageBox, QVBoxLayout
|
||||
|
||||
from game import Game
|
||||
from game.ato.flight import Flight
|
||||
from game.ato.flightplan import FlightPlanBuilder, PlanningError
|
||||
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
|
||||
from game.ato.flightplans.planningerror import PlanningError
|
||||
from game.ato.traveltime import TotEstimator
|
||||
from qt_ui.models import PackageModel
|
||||
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||
|
||||
@ -12,17 +12,15 @@ from PySide2.QtWidgets import (
|
||||
)
|
||||
|
||||
from game import Game
|
||||
from game.ato.package import Package
|
||||
from game.ato.flight import Flight
|
||||
from game.ato.flightplans.custom import CustomFlightPlan
|
||||
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
|
||||
from game.ato.flightplans.formationattack import FormationAttackFlightPlan
|
||||
from game.ato.flightplans.planningerror import PlanningError
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.ato.flightwaypoint import FlightWaypoint
|
||||
from game.ato.flight import Flight
|
||||
from game.ato.flightplan import (
|
||||
CustomFlightPlan,
|
||||
FlightPlanBuilder,
|
||||
PlanningError,
|
||||
StrikeFlightPlan,
|
||||
)
|
||||
from game.ato.loadouts import Loadout
|
||||
from game.ato.package import Package
|
||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import (
|
||||
QFlightWaypointList,
|
||||
)
|
||||
@ -107,7 +105,7 @@ class QFlightWaypointTab(QFrame):
|
||||
# Need to degrade to a custom flight plan and remove the waypoint.
|
||||
# If the waypoint is a target waypoint and is not the last target
|
||||
# waypoint, we don't need to degrade.
|
||||
if isinstance(self.flight.flight_plan, StrikeFlightPlan):
|
||||
if isinstance(self.flight.flight_plan, FormationAttackFlightPlan):
|
||||
is_target = waypoint in self.flight.flight_plan.targets
|
||||
if is_target and len(self.flight.flight_plan.targets) > 1:
|
||||
self.flight.flight_plan.targets.remove(waypoint)
|
||||
@ -144,9 +142,8 @@ class QFlightWaypointTab(QFrame):
|
||||
def degrade_to_custom_flight_plan(self) -> None:
|
||||
if not isinstance(self.flight.flight_plan, CustomFlightPlan):
|
||||
self.flight.flight_plan = CustomFlightPlan(
|
||||
package=self.flight.package,
|
||||
flight=self.flight,
|
||||
custom_waypoints=self.flight.flight_plan.waypoints,
|
||||
waypoints=self.flight.flight_plan.waypoints,
|
||||
)
|
||||
|
||||
def confirm_recreate(self, task: FlightType) -> None:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user