mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Ability to plan tanker recovery for strike like flights (#1729)
* Initial refueling definitions. * Adding refuel definitions. * Initial functionality changes * Regenerate package when adding refueling flight. * Recursively change package waypoint. * Fix mypy errors. * Regenerate flight plans when tanker is added to package. * Give tanker better starting position on package recovery. * Add TOT calculation for refueling waypoint. * Timing changes to Strike split point and Refueling start time. * Add correct waypoint builder for refuel in tarcap and sweep. Remove restrict afterburner on refuel point. * Always generate a refuel point for a package. * Less arbitrary altitude in Refuel track start time calculation. * Refueling waypoint no longer optional. * Fix mypy gen error. * Better discrimination of which tanker flight plan to make. * Remove refuel tot calculations. * Remove package regeneration on tanker flight addition.
This commit is contained in:
parent
94f65d8f70
commit
532ac261ff
@ -5,6 +5,7 @@ Saves from 5.x are not compatible with 6.0.
|
|||||||
## Features/Improvements
|
## Features/Improvements
|
||||||
|
|
||||||
* **[Mission Generation]** Added an option to fast-forward mission generation until the point of first contact (WIP).
|
* **[Mission Generation]** Added an option to fast-forward mission generation until the point of first contact (WIP).
|
||||||
|
* **[Flight Planning]** Added the ability to plan tankers for recovery on package flights. AI does not plan.
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
|
|||||||
@ -45,3 +45,4 @@ class FlightWaypointType(Enum):
|
|||||||
PICKUP = 26
|
PICKUP = 26
|
||||||
DROP_OFF = 27
|
DROP_OFF = 27
|
||||||
BULLSEYE = 28
|
BULLSEYE = 28
|
||||||
|
REFUEL = 29 # Should look for nearby tanker to refuel from.
|
||||||
|
|||||||
@ -48,7 +48,7 @@ class Package:
|
|||||||
"""The speed of the package when in formation.
|
"""The speed of the package when in formation.
|
||||||
|
|
||||||
If none of the flights in the package will join a formation, this
|
If none of the flights in the package will join a formation, this
|
||||||
returns None. This is nto uncommon, since only strike-like (strike,
|
returns None. This is not uncommon, since only strike-like (strike,
|
||||||
DEAD, anti-ship, BAI, etc.) flights and their escorts fly in formation.
|
DEAD, anti-ship, BAI, etc.) flights and their escorts fly in formation.
|
||||||
Others (CAP and CAS, currently) will coordinate in target timing but
|
Others (CAP and CAS, currently) will coordinate in target timing but
|
||||||
fly their own path to the target.
|
fly their own path to the target.
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
|
|
||||||
@ -8,3 +9,4 @@ class PackageWaypoints:
|
|||||||
join: Point
|
join: Point
|
||||||
ingress: Point
|
ingress: Point
|
||||||
split: Point
|
split: Point
|
||||||
|
refuel: Point
|
||||||
|
|||||||
27
game/flightplan/refuelzonegeometry.py
Normal file
27
game/flightplan/refuelzonegeometry.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from dcs import Point
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game.coalition import Coalition
|
||||||
|
|
||||||
|
|
||||||
|
class RefuelZoneGeometry:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
package_home: Point,
|
||||||
|
join: Point,
|
||||||
|
coalition: Coalition,
|
||||||
|
) -> None:
|
||||||
|
self.package_home = package_home
|
||||||
|
self.join = join
|
||||||
|
self.coalition = coalition
|
||||||
|
|
||||||
|
def find_best_refuel_point(self) -> Point:
|
||||||
|
# Do simple at first.
|
||||||
|
# TODO: Consider threats.
|
||||||
|
distance = 0.75 * self.package_home.distance_to_point(self.join)
|
||||||
|
heading = self.package_home.heading_between_point(self.join)
|
||||||
|
return self.package_home.point_from_heading(heading, distance)
|
||||||
9
game/missiongenerator/aircraft/waypoints/refuel.py
Normal file
9
game/missiongenerator/aircraft/waypoints/refuel.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from dcs.point import MovingPoint
|
||||||
|
from dcs.task import RefuelingTaskAction
|
||||||
|
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||||
|
|
||||||
|
|
||||||
|
class RefuelPointBuilder(PydcsWaypointBuilder):
|
||||||
|
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||||
|
waypoint.add_task(RefuelingTaskAction())
|
||||||
|
return super().add_tasks(waypoint)
|
||||||
@ -32,6 +32,7 @@ from .ocarunwayingress import OcaRunwayIngressBuilder
|
|||||||
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
|
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
|
||||||
from .racetrack import RaceTrackBuilder
|
from .racetrack import RaceTrackBuilder
|
||||||
from .racetrackend import RaceTrackEndBuilder
|
from .racetrackend import RaceTrackEndBuilder
|
||||||
|
from .refuel import RefuelPointBuilder
|
||||||
from .seadingress import SeadIngressBuilder
|
from .seadingress import SeadIngressBuilder
|
||||||
from .strikeingress import StrikeIngressBuilder
|
from .strikeingress import StrikeIngressBuilder
|
||||||
from .sweepingress import SweepIngressBuilder
|
from .sweepingress import SweepIngressBuilder
|
||||||
@ -130,6 +131,7 @@ class WaypointGenerator:
|
|||||||
FlightWaypointType.PATROL: RaceTrackEndBuilder,
|
FlightWaypointType.PATROL: RaceTrackEndBuilder,
|
||||||
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
|
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
|
||||||
FlightWaypointType.PICKUP: CargoStopBuilder,
|
FlightWaypointType.PICKUP: CargoStopBuilder,
|
||||||
|
FlightWaypointType.REFUEL: RefuelPointBuilder,
|
||||||
}
|
}
|
||||||
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
||||||
return builder(
|
return builder(
|
||||||
|
|||||||
@ -906,11 +906,12 @@ class Airfield(ControlPoint):
|
|||||||
if self.is_friendly(for_player):
|
if self.is_friendly(for_player):
|
||||||
yield from [
|
yield from [
|
||||||
FlightType.AEWC,
|
FlightType.AEWC,
|
||||||
FlightType.REFUELING,
|
|
||||||
# TODO: FlightType.INTERCEPTION
|
# TODO: FlightType.INTERCEPTION
|
||||||
# TODO: FlightType.LOGISTICS
|
# TODO: FlightType.LOGISTICS
|
||||||
]
|
]
|
||||||
|
|
||||||
|
yield FlightType.REFUELING
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_aircraft_parking(self) -> int:
|
def total_aircraft_parking(self) -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -132,6 +132,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
|
|||||||
yield from [
|
yield from [
|
||||||
FlightType.STRIKE,
|
FlightType.STRIKE,
|
||||||
FlightType.BAI,
|
FlightType.BAI,
|
||||||
|
FlightType.REFUELING,
|
||||||
]
|
]
|
||||||
yield from super().mission_types(for_player)
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,7 @@ from game.ato.starttype import StartType
|
|||||||
from game.data.doctrine import Doctrine
|
from game.data.doctrine import Doctrine
|
||||||
from game.dcs.aircrafttype import FuelConsumption
|
from game.dcs.aircrafttype import FuelConsumption
|
||||||
from game.flightplan import HoldZoneGeometry, IpZoneGeometry, JoinZoneGeometry
|
from game.flightplan import HoldZoneGeometry, IpZoneGeometry, JoinZoneGeometry
|
||||||
|
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
|
||||||
from game.theater import (
|
from game.theater import (
|
||||||
Airfield,
|
Airfield,
|
||||||
ConflictTheater,
|
ConflictTheater,
|
||||||
@ -51,7 +52,6 @@ if TYPE_CHECKING:
|
|||||||
from game.ato.package import Package
|
from game.ato.package import Package
|
||||||
from game.coalition import Coalition
|
from game.coalition import Coalition
|
||||||
from game.threatzones import ThreatZones
|
from game.threatzones import ThreatZones
|
||||||
from game.transfers import Convoy
|
|
||||||
|
|
||||||
|
|
||||||
INGRESS_TYPES = {
|
INGRESS_TYPES = {
|
||||||
@ -362,6 +362,7 @@ class LoiterFlightPlan(FlightPlan):
|
|||||||
class FormationFlightPlan(LoiterFlightPlan):
|
class FormationFlightPlan(LoiterFlightPlan):
|
||||||
join: FlightWaypoint
|
join: FlightWaypoint
|
||||||
split: FlightWaypoint
|
split: FlightWaypoint
|
||||||
|
refuel: FlightWaypoint
|
||||||
|
|
||||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -555,6 +556,7 @@ class CasFlightPlan(PatrollingFlightPlan):
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class TarCapFlightPlan(PatrollingFlightPlan):
|
class TarCapFlightPlan(PatrollingFlightPlan):
|
||||||
takeoff: FlightWaypoint
|
takeoff: FlightWaypoint
|
||||||
|
refuel: Optional[FlightWaypoint]
|
||||||
land: FlightWaypoint
|
land: FlightWaypoint
|
||||||
divert: Optional[FlightWaypoint]
|
divert: Optional[FlightWaypoint]
|
||||||
bullseye: FlightWaypoint
|
bullseye: FlightWaypoint
|
||||||
@ -567,6 +569,8 @@ class TarCapFlightPlan(PatrollingFlightPlan):
|
|||||||
self.patrol_start,
|
self.patrol_start,
|
||||||
self.patrol_end,
|
self.patrol_end,
|
||||||
]
|
]
|
||||||
|
if self.refuel is not None:
|
||||||
|
yield self.refuel
|
||||||
yield from self.nav_from
|
yield from self.nav_from
|
||||||
yield self.land
|
yield self.land
|
||||||
if self.divert is not None:
|
if self.divert is not None:
|
||||||
@ -624,6 +628,8 @@ class StrikeFlightPlan(FormationFlightPlan):
|
|||||||
yield self.ingress
|
yield self.ingress
|
||||||
yield from self.targets
|
yield from self.targets
|
||||||
yield self.split
|
yield self.split
|
||||||
|
if self.refuel is not None:
|
||||||
|
yield self.refuel
|
||||||
yield from self.nav_from
|
yield from self.nav_from
|
||||||
yield self.land
|
yield self.land
|
||||||
if self.divert is not None:
|
if self.divert is not None:
|
||||||
@ -697,8 +703,20 @@ class StrikeFlightPlan(FormationFlightPlan):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def split_time(self) -> timedelta:
|
def split_time(self) -> timedelta:
|
||||||
travel_time = self.travel_time_between_waypoints(self.ingress, self.split)
|
travel_time_ingress = self.travel_time_between_waypoints(
|
||||||
return self.ingress_time + travel_time
|
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
|
@property
|
||||||
def ingress_time(self) -> timedelta:
|
def ingress_time(self) -> timedelta:
|
||||||
@ -722,6 +740,7 @@ class SweepFlightPlan(LoiterFlightPlan):
|
|||||||
nav_to: List[FlightWaypoint]
|
nav_to: List[FlightWaypoint]
|
||||||
sweep_start: FlightWaypoint
|
sweep_start: FlightWaypoint
|
||||||
sweep_end: FlightWaypoint
|
sweep_end: FlightWaypoint
|
||||||
|
refuel: FlightWaypoint
|
||||||
nav_from: List[FlightWaypoint]
|
nav_from: List[FlightWaypoint]
|
||||||
land: FlightWaypoint
|
land: FlightWaypoint
|
||||||
divert: Optional[FlightWaypoint]
|
divert: Optional[FlightWaypoint]
|
||||||
@ -734,6 +753,8 @@ class SweepFlightPlan(LoiterFlightPlan):
|
|||||||
yield from self.nav_to
|
yield from self.nav_to
|
||||||
yield self.sweep_start
|
yield self.sweep_start
|
||||||
yield self.sweep_end
|
yield self.sweep_end
|
||||||
|
if self.refuel is not None:
|
||||||
|
yield self.refuel
|
||||||
yield from self.nav_from
|
yield from self.nav_from
|
||||||
yield self.land
|
yield self.land
|
||||||
if self.divert is not None:
|
if self.divert is not None:
|
||||||
@ -835,6 +856,10 @@ class RefuelingFlightPlan(PatrollingFlightPlan):
|
|||||||
divert: Optional[FlightWaypoint]
|
divert: Optional[FlightWaypoint]
|
||||||
bullseye: FlightWaypoint
|
bullseye: FlightWaypoint
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patrol_start_time(self) -> timedelta:
|
||||||
|
return self.package.time_over_target
|
||||||
|
|
||||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||||
yield self.takeoff
|
yield self.takeoff
|
||||||
yield from self.nav_to
|
yield from self.nav_to
|
||||||
@ -847,6 +872,52 @@ class RefuelingFlightPlan(PatrollingFlightPlan):
|
|||||||
yield self.bullseye
|
yield self.bullseye
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PackageRefuelingFlightPlan(RefuelingFlightPlan):
|
||||||
|
def target_area_waypoint(self) -> FlightWaypoint:
|
||||||
|
return FlightWaypoint(
|
||||||
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
|
self.package.target.position.x,
|
||||||
|
self.package.target.position.y,
|
||||||
|
meters(0),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patrol_start_time(self) -> timedelta:
|
||||||
|
altitude: Optional[Distance] = 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(
|
||||||
|
FlightWaypointType.SPLIT, split.x, split.y, altitude
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cheat in a FlightWaypoint for the refuel point.
|
||||||
|
refuel: Point = self.package.waypoints.refuel
|
||||||
|
refuel_waypoint: FlightWaypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.REFUEL, refuel.x, refuel.y, 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)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AirliftFlightPlan(FlightPlan):
|
class AirliftFlightPlan(FlightPlan):
|
||||||
takeoff: FlightWaypoint
|
takeoff: FlightWaypoint
|
||||||
@ -1043,11 +1114,24 @@ class FlightPlanBuilder:
|
|||||||
elif task == FlightType.TRANSPORT:
|
elif task == FlightType.TRANSPORT:
|
||||||
return self.generate_transport(flight)
|
return self.generate_transport(flight)
|
||||||
elif task == FlightType.REFUELING:
|
elif task == FlightType.REFUELING:
|
||||||
return self.generate_refueling_racetrack(flight)
|
if self.package.target.is_friendly(self.is_player) or isinstance(
|
||||||
|
self.package.target, FrontLine
|
||||||
|
):
|
||||||
|
return self.generate_refueling_racetrack(flight)
|
||||||
|
else:
|
||||||
|
return self.generate_refueling_package_support(flight)
|
||||||
elif task == FlightType.FERRY:
|
elif task == FlightType.FERRY:
|
||||||
return self.generate_ferry(flight)
|
return self.generate_ferry(flight)
|
||||||
raise PlanningError(f"{task} flight plan generation not implemented")
|
raise PlanningError(f"{task} flight plan generation not implemented")
|
||||||
|
|
||||||
|
def regenerate_flight_plans(self) -> None:
|
||||||
|
new_flights: list[Flight] = []
|
||||||
|
for old_flight in self.package.flights:
|
||||||
|
# TODO: Don't lose custom targets here.
|
||||||
|
old_flight.flight_plan = self.generate_flight_plan(old_flight, None)
|
||||||
|
new_flights.append(old_flight)
|
||||||
|
self.package.flights = new_flights
|
||||||
|
|
||||||
def regenerate_package_waypoints(self) -> None:
|
def regenerate_package_waypoints(self) -> None:
|
||||||
from game.ato.packagewaypoints import PackageWaypoints
|
from game.ato.packagewaypoints import PackageWaypoints
|
||||||
|
|
||||||
@ -1067,6 +1151,12 @@ class FlightPlanBuilder:
|
|||||||
self.coalition,
|
self.coalition,
|
||||||
).find_best_join_point()
|
).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
|
# 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.
|
# 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?
|
# TODO: Estimate attack completion point based on the IP and split from there?
|
||||||
@ -1074,6 +1164,7 @@ class FlightPlanBuilder:
|
|||||||
WaypointBuilder.perturb(join_point),
|
WaypointBuilder.perturb(join_point),
|
||||||
ingress_point,
|
ingress_point,
|
||||||
WaypointBuilder.perturb(join_point),
|
WaypointBuilder.perturb(join_point),
|
||||||
|
refuel_point,
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
|
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
|
||||||
@ -1241,7 +1332,7 @@ class FlightPlanBuilder:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
|
def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
|
||||||
"""Generate a BARCAP flight at a given location.
|
"""Generate a FighterSweep flight at a given location.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
flight: The flight to generate the flight plan for.
|
flight: The flight to generate the flight plan for.
|
||||||
@ -1260,6 +1351,11 @@ class FlightPlanBuilder:
|
|||||||
|
|
||||||
hold = builder.hold(self._hold_point(flight))
|
hold = builder.hold(self._hold_point(flight))
|
||||||
|
|
||||||
|
refuel = None
|
||||||
|
|
||||||
|
if self.package.waypoints is not None:
|
||||||
|
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||||
|
|
||||||
return SweepFlightPlan(
|
return SweepFlightPlan(
|
||||||
package=self.package,
|
package=self.package,
|
||||||
flight=flight,
|
flight=flight,
|
||||||
@ -1275,6 +1371,7 @@ class FlightPlanBuilder:
|
|||||||
),
|
),
|
||||||
sweep_start=start,
|
sweep_start=start,
|
||||||
sweep_end=end,
|
sweep_end=end,
|
||||||
|
refuel=refuel,
|
||||||
land=builder.land(flight.arrival),
|
land=builder.land(flight.arrival),
|
||||||
divert=builder.divert(flight.divert),
|
divert=builder.divert(flight.divert),
|
||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
@ -1482,6 +1579,12 @@ class FlightPlanBuilder:
|
|||||||
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
|
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
|
||||||
|
|
||||||
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
|
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(
|
return TarCapFlightPlan(
|
||||||
package=self.package,
|
package=self.package,
|
||||||
flight=flight,
|
flight=flight,
|
||||||
@ -1498,6 +1601,7 @@ class FlightPlanBuilder:
|
|||||||
nav_from=builder.nav_path(orbit1p, flight.arrival.position, patrol_alt),
|
nav_from=builder.nav_path(orbit1p, flight.arrival.position, patrol_alt),
|
||||||
patrol_start=start,
|
patrol_start=start,
|
||||||
patrol_end=end,
|
patrol_end=end,
|
||||||
|
refuel=refuel,
|
||||||
land=builder.land(flight.arrival),
|
land=builder.land(flight.arrival),
|
||||||
divert=builder.divert(flight.divert),
|
divert=builder.divert(flight.divert),
|
||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
@ -1611,6 +1715,9 @@ class FlightPlanBuilder:
|
|||||||
hold = builder.hold(self._hold_point(flight))
|
hold = builder.hold(self._hold_point(flight))
|
||||||
join = builder.join(self.package.waypoints.join)
|
join = builder.join(self.package.waypoints.join)
|
||||||
split = builder.split(self.package.waypoints.split)
|
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 StrikeFlightPlan(
|
return StrikeFlightPlan(
|
||||||
package=self.package,
|
package=self.package,
|
||||||
@ -1625,6 +1732,7 @@ class FlightPlanBuilder:
|
|||||||
ingress=ingress,
|
ingress=ingress,
|
||||||
targets=[target],
|
targets=[target],
|
||||||
split=split,
|
split=split,
|
||||||
|
refuel=refuel,
|
||||||
nav_from=builder.nav_path(
|
nav_from=builder.nav_path(
|
||||||
split.position, flight.arrival.position, self.doctrine.ingress_altitude
|
split.position, flight.arrival.position, self.doctrine.ingress_altitude
|
||||||
),
|
),
|
||||||
@ -1703,6 +1811,11 @@ class FlightPlanBuilder:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def generate_refueling_racetrack(self, flight: Flight) -> RefuelingFlightPlan:
|
def generate_refueling_racetrack(self, flight: Flight) -> RefuelingFlightPlan:
|
||||||
|
|
||||||
|
racetrack_half_distance = Distance.from_nautical_miles(20).meters
|
||||||
|
|
||||||
|
patrol_duration = timedelta(hours=1)
|
||||||
|
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
closest_boundary = self.threat_zones.closest_boundary(location.position)
|
closest_boundary = self.threat_zones.closest_boundary(location.position)
|
||||||
@ -1725,11 +1838,10 @@ class FlightPlanBuilder:
|
|||||||
orbit_heading.degrees, orbit_distance.meters
|
orbit_heading.degrees, orbit_distance.meters
|
||||||
)
|
)
|
||||||
|
|
||||||
racetrack_half_distance = Distance.from_nautical_miles(20).meters
|
|
||||||
|
|
||||||
racetrack_start = racetrack_center.point_from_heading(
|
racetrack_start = racetrack_center.point_from_heading(
|
||||||
orbit_heading.right.degrees, racetrack_half_distance
|
orbit_heading.right.degrees, racetrack_half_distance
|
||||||
)
|
)
|
||||||
|
|
||||||
racetrack_end = racetrack_center.point_from_heading(
|
racetrack_end = racetrack_center.point_from_heading(
|
||||||
orbit_heading.left.degrees, racetrack_half_distance
|
orbit_heading.left.degrees, racetrack_half_distance
|
||||||
)
|
)
|
||||||
@ -1764,7 +1876,74 @@ class FlightPlanBuilder:
|
|||||||
land=builder.land(flight.arrival),
|
land=builder.land(flight.arrival),
|
||||||
divert=builder.divert(flight.divert),
|
divert=builder.divert(flight.divert),
|
||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
patrol_duration=timedelta(hours=1),
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_refueling_package_support(
|
||||||
|
self, flight: Flight
|
||||||
|
) -> 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 flight in self.package.flights:
|
||||||
|
flight_size = 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(flight, self.coalition)
|
||||||
|
|
||||||
|
tanker_type = flight.unit_type
|
||||||
|
if tanker_type.patrol_altitude is not None:
|
||||||
|
altitude = tanker_type.patrol_altitude
|
||||||
|
else:
|
||||||
|
altitude = feet(21000)
|
||||||
|
|
||||||
|
# TODO: Could use flight.unit_type.preferred_patrol_speed(altitude) instead.
|
||||||
|
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(
|
||||||
|
package=self.package,
|
||||||
|
flight=flight,
|
||||||
|
takeoff=builder.takeoff(flight.departure),
|
||||||
|
nav_to=builder.nav_path(
|
||||||
|
flight.departure.position, racetrack_start, altitude
|
||||||
|
),
|
||||||
|
nav_from=builder.nav_path(racetrack_end, flight.arrival.position, altitude),
|
||||||
|
patrol_start=racetrack[0],
|
||||||
|
patrol_end=racetrack[1],
|
||||||
|
land=builder.land(flight.arrival),
|
||||||
|
divert=builder.divert(flight.divert),
|
||||||
|
bullseye=builder.bullseye(),
|
||||||
|
patrol_duration=patrol_duration,
|
||||||
patrol_speed=speed,
|
patrol_speed=speed,
|
||||||
# TODO: Factor out a common base of the combat and non-combat race-tracks.
|
# 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.
|
# No harm in setting this, but we ought to clean up a bit.
|
||||||
@ -1843,6 +2022,9 @@ class FlightPlanBuilder:
|
|||||||
hold = builder.hold(self._hold_point(flight))
|
hold = builder.hold(self._hold_point(flight))
|
||||||
join = builder.join(self.package.waypoints.join)
|
join = builder.join(self.package.waypoints.join)
|
||||||
split = builder.split(self.package.waypoints.split)
|
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 StrikeFlightPlan(
|
return StrikeFlightPlan(
|
||||||
package=self.package,
|
package=self.package,
|
||||||
@ -1859,6 +2041,7 @@ class FlightPlanBuilder:
|
|||||||
),
|
),
|
||||||
targets=target_waypoints,
|
targets=target_waypoints,
|
||||||
split=split,
|
split=split,
|
||||||
|
refuel=refuel,
|
||||||
nav_from=builder.nav_path(
|
nav_from=builder.nav_path(
|
||||||
split.position, flight.arrival.position, self.doctrine.ingress_altitude
|
split.position, flight.arrival.position, self.doctrine.ingress_altitude
|
||||||
),
|
),
|
||||||
|
|||||||
@ -199,6 +199,20 @@ class WaypointBuilder:
|
|||||||
waypoint.name = "JOIN"
|
waypoint.name = "JOIN"
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
|
def refuel(self, position: Point) -> FlightWaypoint:
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.REFUEL,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
|
)
|
||||||
|
if self.is_helo:
|
||||||
|
waypoint.alt_type = "RADIO"
|
||||||
|
waypoint.pretty_name = "Refuel"
|
||||||
|
waypoint.description = "Refuel from tanker"
|
||||||
|
waypoint.name = "REFUEL"
|
||||||
|
return waypoint
|
||||||
|
|
||||||
def split(self, position: Point) -> FlightWaypoint:
|
def split(self, position: Point) -> FlightWaypoint:
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.SPLIT,
|
FlightWaypointType.SPLIT,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user