mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +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:
@@ -26,6 +26,7 @@ from game.ato.starttype import StartType
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.dcs.aircrafttype import FuelConsumption
|
||||
from game.flightplan import HoldZoneGeometry, IpZoneGeometry, JoinZoneGeometry
|
||||
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ConflictTheater,
|
||||
@@ -51,7 +52,6 @@ if TYPE_CHECKING:
|
||||
from game.ato.package import Package
|
||||
from game.coalition import Coalition
|
||||
from game.threatzones import ThreatZones
|
||||
from game.transfers import Convoy
|
||||
|
||||
|
||||
INGRESS_TYPES = {
|
||||
@@ -362,6 +362,7 @@ class LoiterFlightPlan(FlightPlan):
|
||||
class FormationFlightPlan(LoiterFlightPlan):
|
||||
join: FlightWaypoint
|
||||
split: FlightWaypoint
|
||||
refuel: FlightWaypoint
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
raise NotImplementedError
|
||||
@@ -555,6 +556,7 @@ class CasFlightPlan(PatrollingFlightPlan):
|
||||
@dataclass(frozen=True)
|
||||
class TarCapFlightPlan(PatrollingFlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
refuel: Optional[FlightWaypoint]
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
bullseye: FlightWaypoint
|
||||
@@ -567,6 +569,8 @@ class TarCapFlightPlan(PatrollingFlightPlan):
|
||||
self.patrol_start,
|
||||
self.patrol_end,
|
||||
]
|
||||
if self.refuel is not None:
|
||||
yield self.refuel
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
@@ -624,6 +628,8 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
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.land
|
||||
if self.divert is not None:
|
||||
@@ -697,8 +703,20 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
|
||||
@property
|
||||
def split_time(self) -> timedelta:
|
||||
travel_time = self.travel_time_between_waypoints(self.ingress, self.split)
|
||||
return self.ingress_time + travel_time
|
||||
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:
|
||||
@@ -722,6 +740,7 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
nav_to: List[FlightWaypoint]
|
||||
sweep_start: FlightWaypoint
|
||||
sweep_end: FlightWaypoint
|
||||
refuel: FlightWaypoint
|
||||
nav_from: List[FlightWaypoint]
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
@@ -734,6 +753,8 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
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.land
|
||||
if self.divert is not None:
|
||||
@@ -835,6 +856,10 @@ class RefuelingFlightPlan(PatrollingFlightPlan):
|
||||
divert: Optional[FlightWaypoint]
|
||||
bullseye: FlightWaypoint
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
yield from self.nav_to
|
||||
@@ -847,6 +872,52 @@ class RefuelingFlightPlan(PatrollingFlightPlan):
|
||||
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)
|
||||
class AirliftFlightPlan(FlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
@@ -1043,11 +1114,24 @@ class FlightPlanBuilder:
|
||||
elif task == FlightType.TRANSPORT:
|
||||
return self.generate_transport(flight)
|
||||
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:
|
||||
return self.generate_ferry(flight)
|
||||
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:
|
||||
from game.ato.packagewaypoints import PackageWaypoints
|
||||
|
||||
@@ -1067,6 +1151,12 @@ class FlightPlanBuilder:
|
||||
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?
|
||||
@@ -1074,6 +1164,7 @@ class FlightPlanBuilder:
|
||||
WaypointBuilder.perturb(join_point),
|
||||
ingress_point,
|
||||
WaypointBuilder.perturb(join_point),
|
||||
refuel_point,
|
||||
)
|
||||
|
||||
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
|
||||
@@ -1241,7 +1332,7 @@ class FlightPlanBuilder:
|
||||
)
|
||||
|
||||
def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
|
||||
"""Generate a BARCAP flight at a given location.
|
||||
"""Generate a FighterSweep flight at a given location.
|
||||
|
||||
Args:
|
||||
flight: The flight to generate the flight plan for.
|
||||
@@ -1260,6 +1351,11 @@ class FlightPlanBuilder:
|
||||
|
||||
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(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
@@ -1275,6 +1371,7 @@ class FlightPlanBuilder:
|
||||
),
|
||||
sweep_start=start,
|
||||
sweep_end=end,
|
||||
refuel=refuel,
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
@@ -1482,6 +1579,12 @@ class FlightPlanBuilder:
|
||||
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(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
@@ -1498,6 +1601,7 @@ class FlightPlanBuilder:
|
||||
nav_from=builder.nav_path(orbit1p, flight.arrival.position, patrol_alt),
|
||||
patrol_start=start,
|
||||
patrol_end=end,
|
||||
refuel=refuel,
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
@@ -1611,6 +1715,9 @@ class FlightPlanBuilder:
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
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 StrikeFlightPlan(
|
||||
package=self.package,
|
||||
@@ -1625,6 +1732,7 @@ class FlightPlanBuilder:
|
||||
ingress=ingress,
|
||||
targets=[target],
|
||||
split=split,
|
||||
refuel=refuel,
|
||||
nav_from=builder.nav_path(
|
||||
split.position, flight.arrival.position, self.doctrine.ingress_altitude
|
||||
),
|
||||
@@ -1703,6 +1811,11 @@ class FlightPlanBuilder:
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
closest_boundary = self.threat_zones.closest_boundary(location.position)
|
||||
@@ -1725,11 +1838,10 @@ class FlightPlanBuilder:
|
||||
orbit_heading.degrees, orbit_distance.meters
|
||||
)
|
||||
|
||||
racetrack_half_distance = Distance.from_nautical_miles(20).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
|
||||
)
|
||||
@@ -1764,7 +1876,74 @@ class FlightPlanBuilder:
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
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,
|
||||
# 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.
|
||||
@@ -1843,6 +2022,9 @@ class FlightPlanBuilder:
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
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 StrikeFlightPlan(
|
||||
package=self.package,
|
||||
@@ -1859,6 +2041,7 @@ class FlightPlanBuilder:
|
||||
),
|
||||
targets=target_waypoints,
|
||||
split=split,
|
||||
refuel=refuel,
|
||||
nav_from=builder.nav_path(
|
||||
split.position, flight.arrival.position, self.doctrine.ingress_altitude
|
||||
),
|
||||
|
||||
@@ -199,6 +199,20 @@ class WaypointBuilder:
|
||||
waypoint.name = "JOIN"
|
||||
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:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.SPLIT,
|
||||
|
||||
Reference in New Issue
Block a user