This commit is contained in:
Dan Albert
2021-02-12 19:58:30 -08:00
parent 053663bd76
commit a47bef1f13
222 changed files with 8434 additions and 4461 deletions

View File

@@ -109,22 +109,25 @@ class ProposedMission:
flights: List[ProposedFlight]
def __str__(self) -> str:
flights = ', '.join([str(f) for f in self.flights])
flights = ", ".join([str(f) for f in self.flights])
return f"{self.location.name}: {flights}"
class AircraftAllocator:
"""Finds suitable aircraft for proposed missions."""
def __init__(self, closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool) -> None:
def __init__(
self,
closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool,
) -> None:
self.closest_airfields = closest_airfields
self.global_inventory = global_inventory
self.is_player = is_player
def find_aircraft_for_flight(
self, flight: ProposedFlight
self, flight: ProposedFlight
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
"""Finds aircraft suitable for the given mission.
@@ -144,12 +147,12 @@ class AircraftAllocator:
on subsequent calls. If the found aircraft are not used, the caller is
responsible for returning them to the inventory.
"""
return self.find_aircraft_of_type(
flight, aircraft_for_task(flight.task)
)
return self.find_aircraft_of_type(flight, aircraft_for_task(flight.task))
def find_aircraft_of_type(
self, flight: ProposedFlight, types: List[Type[FlyingType]],
self,
flight: ProposedFlight,
types: List[Type[FlyingType]],
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
airfields_in_range = self.closest_airfields.airfields_within(
flight.max_distance
@@ -171,18 +174,22 @@ class AircraftAllocator:
class PackageBuilder:
"""Builds a Package for the flights it receives."""
def __init__(self, location: MissionTarget,
closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool,
package_country: str,
start_type: str) -> None:
def __init__(
self,
location: MissionTarget,
closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool,
package_country: str,
start_type: str,
) -> None:
self.closest_airfields = closest_airfields
self.is_player = is_player
self.package_country = package_country
self.package = Package(location)
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
is_player)
self.allocator = AircraftAllocator(
closest_airfields, global_inventory, is_player
)
self.global_inventory = global_inventory
self.start_type = start_type
@@ -203,14 +210,23 @@ class PackageBuilder:
else:
start_type = self.start_type
flight = Flight(self.package, self.package_country, aircraft, plan.num_aircraft, plan.task,
start_type, departure=airfield, arrival=airfield,
divert=self.find_divert_field(aircraft, airfield))
flight = Flight(
self.package,
self.package_country,
aircraft,
plan.num_aircraft,
plan.task,
start_type,
departure=airfield,
arrival=airfield,
divert=self.find_divert_field(aircraft, airfield),
)
self.package.add_flight(flight)
return True
def find_divert_field(self, aircraft: Type[FlyingType],
arrival: ControlPoint) -> Optional[ControlPoint]:
def find_divert_field(
self, aircraft: Type[FlyingType], arrival: ControlPoint
) -> Optional[ControlPoint]:
divert_limit = nautical_miles(150)
for airfield in self.closest_airfields.airfields_within(divert_limit):
if airfield.captured != self.is_player:
@@ -323,8 +339,8 @@ class ObjectiveFinder:
return self._targets_by_range(self.enemy_ships())
def _targets_by_range(
self,
targets: Iterable[MissionTarget]) -> Iterator[MissionTarget]:
self, targets: Iterable[MissionTarget]
) -> Iterator[MissionTarget]:
target_ranges: List[Tuple[MissionTarget, int]] = []
for target in targets:
ranges: List[int] = []
@@ -430,8 +446,9 @@ class ObjectiveFinder:
def friendly_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all friendly control points."""
return (c for c in self.game.theater.controlpoints if
c.is_friendly(self.is_player))
return (
c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player)
)
def farthest_friendly_control_point(self) -> ControlPoint:
"""
@@ -451,8 +468,11 @@ class ObjectiveFinder:
def enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all enemy control points."""
return (c for c in self.game.theater.controlpoints if
not c.is_friendly(self.is_player))
return (
c
for c in self.game.theater.controlpoints
if not c.is_friendly(self.is_player)
)
def all_possible_targets(self) -> Iterator[MissionTarget]:
"""Iterates over all possible mission targets in the theater.
@@ -524,33 +544,46 @@ class CoalitionMissionPlanner:
eliminated this turn.
"""
#Find farthest, friendly CP for AEWC
# Find farthest, friendly CP for AEWC
cp = self.objective_finder.farthest_friendly_control_point()
yield ProposedMission(cp, [
ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)
])
yield ProposedMission(
cp, [ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)]
)
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
for cp in self.objective_finder.vulnerable_control_points():
# Plan three rounds of CAP to give ~90 minutes coverage. Spacing
# these out appropriately is done in stagger_missions.
yield ProposedMission(cp, [
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
])
yield ProposedMission(cp, [
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
])
yield ProposedMission(cp, [
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
])
yield ProposedMission(
cp,
[
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
],
)
yield ProposedMission(
cp,
[
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
],
)
yield ProposedMission(
cp,
[
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
],
)
# Find front lines, plan CAS.
for front_line in self.objective_finder.front_lines():
yield ProposedMission(front_line, [
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE,
EscortType.AirToAir),
])
yield ProposedMission(
front_line,
[
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
ProposedFlight(
FlightType.TARCAP, 2, self.MAX_CAP_RANGE, EscortType.AirToAir
),
],
)
def propose_missions(self) -> Iterator[ProposedMission]:
"""Identifies and iterates over potential mission in priority order."""
@@ -561,30 +594,46 @@ class CoalitionMissionPlanner:
# Find enemy SAM sites with ranges that extend to within 50 nmi of
# friendly CPs, front, lines, or objects, plan DEAD.
for sam in self.objective_finder.threatening_sams():
yield ProposedMission(sam, [
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE,
EscortType.AirToAir),
])
yield ProposedMission(
sam,
[
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.AirToAir
),
],
)
for group in self.objective_finder.threatening_ships():
yield ProposedMission(group, [
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_ANTISHIP_RANGE,
EscortType.AirToAir),
])
yield ProposedMission(
group,
[
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT,
2,
self.MAX_ANTISHIP_RANGE,
EscortType.AirToAir,
),
],
)
for group in self.objective_finder.threatening_vehicle_groups():
yield ProposedMission(group, [
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_BAI_RANGE,
EscortType.AirToAir),
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE,
EscortType.Sead),
])
yield ProposedMission(
group,
[
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
),
ProposedFlight(
FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead
),
],
)
for target in self.objective_finder.oca_targets(min_aircraft=20):
flights = [
@@ -593,27 +642,37 @@ class CoalitionMissionPlanner:
if self.game.settings.default_start_type == "Cold":
# Only schedule if the default start type is Cold. If the player
# has set anything else there are no targets to hit.
flights.append(ProposedFlight(FlightType.OCA_AIRCRAFT, 2,
self.MAX_OCA_RANGE))
flights.extend([
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE,
EscortType.AirToAir),
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE,
EscortType.Sead),
])
flights.append(
ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE)
)
flights.extend(
[
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_OCA_RANGE, EscortType.AirToAir
),
ProposedFlight(
FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead
),
]
)
yield ProposedMission(target, flights)
# Plan strike missions.
for target in self.objective_finder.strike_targets():
yield ProposedMission(target, [
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE,
EscortType.AirToAir),
ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE,
EscortType.Sead),
])
yield ProposedMission(
target,
[
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, EscortType.AirToAir
),
ProposedFlight(
FlightType.SEAD, 2, self.MAX_STRIKE_RANGE, EscortType.Sead
),
],
)
def plan_missions(self) -> None:
"""Identifies and plans mission for the turn."""
@@ -628,19 +687,23 @@ class CoalitionMissionPlanner:
for cp in self.objective_finder.friendly_control_points():
inventory = self.game.aircraft_inventory.for_control_point(cp)
for aircraft, available in inventory.all_aircraft:
self.message("Unused aircraft",
f"{available} {aircraft.id} from {cp}")
self.message("Unused aircraft", f"{available} {aircraft.id} from {cp}")
def plan_flight(self, mission: ProposedMission, flight: ProposedFlight,
builder: PackageBuilder, missing_types: Set[FlightType],
for_reserves: bool) -> None:
def plan_flight(
self,
mission: ProposedMission,
flight: ProposedFlight,
builder: PackageBuilder,
missing_types: Set[FlightType],
for_reserves: bool,
) -> None:
if not builder.plan_flight(flight):
missing_types.add(flight.task)
purchase_order = AircraftProcurementRequest(
near=mission.location,
range=flight.max_distance,
task_capability=flight.task,
number=flight.num_aircraft
number=flight.num_aircraft,
)
if for_reserves:
# Reserves are planned for critical missions, so prioritize
@@ -650,26 +713,28 @@ class CoalitionMissionPlanner:
self.procurement_requests.append(purchase_order)
def scrub_mission_missing_aircraft(
self, mission: ProposedMission, builder: PackageBuilder,
missing_types: Set[FlightType],
not_attempted: Iterable[ProposedFlight],
reserves: bool) -> None:
self,
mission: ProposedMission,
builder: PackageBuilder,
missing_types: Set[FlightType],
not_attempted: Iterable[ProposedFlight],
reserves: bool,
) -> None:
# Try to plan the rest of the mission just so we can count the missing
# types to buy.
for flight in not_attempted:
self.plan_flight(mission, flight, builder, missing_types, reserves)
missing_types_str = ", ".join(
sorted([t.name for t in missing_types]))
missing_types_str = ", ".join(sorted([t.name for t in missing_types]))
builder.release_planned_aircraft()
desc = "reserve aircraft" if reserves else "aircraft"
self.message(
"Insufficient aircraft",
f"Not enough {desc} in range for {mission.location.name} "
f"capable of: {missing_types_str}")
f"capable of: {missing_types_str}",
)
def check_needed_escorts(
self, builder: PackageBuilder) -> Dict[EscortType, bool]:
def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]:
threats = defaultdict(bool)
for flight in builder.package.flights:
if self.threat_zones.threatened_by_aircraft(flight):
@@ -678,8 +743,7 @@ class CoalitionMissionPlanner:
threats[EscortType.Sead] = True
return threats
def plan_mission(self, mission: ProposedMission,
reserves: bool = False) -> None:
def plan_mission(self, mission: ProposedMission, reserves: bool = False) -> None:
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
if self.is_player:
@@ -693,7 +757,7 @@ class CoalitionMissionPlanner:
self.game.aircraft_inventory,
self.is_player,
package_country,
self.game.settings.default_start_type
self.game.settings.default_start_type,
)
# Attempt to plan all the main elements of the mission first. Escorts
@@ -707,12 +771,12 @@ class CoalitionMissionPlanner:
# If the package does not need escorts they may be pruned.
escorts.append(proposed_flight)
continue
self.plan_flight(mission, proposed_flight, builder, missing_types,
reserves)
self.plan_flight(mission, proposed_flight, builder, missing_types, reserves)
if missing_types:
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
escorts, reserves)
self.scrub_mission_missing_aircraft(
mission, builder, missing_types, escorts, reserves
)
return
# Create flight plans for the main flights of the package so we can
@@ -721,8 +785,9 @@ class CoalitionMissionPlanner:
# flights that will rendezvous with their package will be affected by
# the other flights in the package. Escorts will not be able to
# contribute to this.
flight_plan_builder = FlightPlanBuilder(self.game, builder.package,
self.is_player)
flight_plan_builder = FlightPlanBuilder(
self.game, builder.package, self.is_player
)
for flight in builder.package.flights:
flight_plan_builder.populate_flight_plan(flight)
@@ -732,14 +797,14 @@ class CoalitionMissionPlanner:
# impossible.
assert escort.escort_type is not None
if needed_escorts[escort.escort_type]:
self.plan_flight(mission, escort, builder, missing_types,
reserves)
self.plan_flight(mission, escort, builder, missing_types, reserves)
# Check again for unavailable aircraft. If the escort was required and
# none were found, scrub the mission.
if missing_types:
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
escorts, reserves)
self.scrub_mission_missing_aircraft(
mission, builder, missing_types, escorts, reserves
)
return
if reserves:
@@ -756,8 +821,9 @@ class CoalitionMissionPlanner:
self.ato.add_package(package)
def stagger_missions(self) -> None:
def start_time_generator(count: int, earliest: int, latest: int,
margin: int) -> Iterator[timedelta]:
def start_time_generator(
count: int, earliest: int, latest: int, margin: int
) -> Iterator[timedelta]:
interval = (latest - earliest) // count
for time in range(earliest, latest, interval):
error = random.randint(-margin, margin)
@@ -768,17 +834,13 @@ class CoalitionMissionPlanner:
FlightType.TARCAP,
}
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(
timedelta
)
non_dca_packages = [p for p in self.ato.packages if
p.primary_task not in dca_types]
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta)
non_dca_packages = [
p for p in self.ato.packages if p.primary_task not in dca_types
]
start_time = start_time_generator(
count=len(non_dca_packages),
earliest=5,
latest=90,
margin=5
count=len(non_dca_packages), earliest=5, latest=90, margin=5
)
for package in self.ato.packages:
tot = TotEstimator(package).earliest_tot()
@@ -795,8 +857,7 @@ class CoalitionMissionPlanner:
departure_time = package.mission_departure_time
# Should be impossible for CAPs
if departure_time is None:
logging.error(
f"Could not determine mission end time for {package}")
logging.error(f"Could not determine mission end time for {package}")
continue
previous_cap_end_time[package.target] = departure_time
else:
@@ -815,8 +876,6 @@ class CoalitionMissionPlanner:
message to the info panel.
"""
if self.is_player:
self.game.informations.append(
Information(title, text, self.game.turn)
)
self.game.informations.append(Information(title, text, self.game.turn))
else:
logging.info(f"{title}: {text}")

View File

@@ -370,11 +370,7 @@ TRANSPORT_CAPABLE = [
UH_1H,
]
DRONES = [
MQ_9_Reaper,
RQ_1A_Predator,
WingLoong_I
]
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
AEWC_CAPABLE = [
E_3A,

View File

@@ -12,8 +12,9 @@ if TYPE_CHECKING:
class ClosestAirfields:
"""Precalculates which control points are closes to the given target."""
def __init__(self, target: MissionTarget,
all_control_points: List[ControlPoint]) -> None:
def __init__(
self, target: MissionTarget, all_control_points: List[ControlPoint]
) -> None:
self.target = target
# This cache is configured once on load, so it's important that it is
# complete and deterministic to avoid different behaviors across loads.
@@ -52,9 +53,7 @@ class ObjectiveDistanceCache:
@classmethod
def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields:
if cls.theater is None:
raise RuntimeError(
"Call ObjectiveDistanceCache.set_theater before using"
)
raise RuntimeError("Call ObjectiveDistanceCache.set_theater before using")
if location.name not in cls.closest_airfields:
cls.closest_airfields[location.name] = ClosestAirfields(

View File

@@ -28,6 +28,7 @@ class FlightType(Enum):
each flight and thus a part of the ATO, so changing these values will break
save compat.
"""
TARCAP = "TARCAP"
BARCAP = "BARCAP"
CAS = "CAS"
@@ -48,22 +49,22 @@ class FlightType(Enum):
class FlightWaypointType(Enum):
TAKEOFF = 0 # Take off point
ASCEND_POINT = 1 # Ascension point after take off
PATROL = 2 # Patrol point
PATROL_TRACK = 3 # Patrol race track
NAV = 4 # Nav point
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
INGRESS_CAS = 7 # Ingress cas (should start CAS task)
CAS = 8 # Should do CAS there
EGRESS = 9 # Should stop attack
DESCENT_POINT = 10 # Should start descending to pattern alt
LANDING_POINT = 11 # Should land there
TARGET_POINT = 12 # A target building or static object, position
TARGET_GROUP_LOC = 13 # A target group approximate location
TARGET_SHIP = 14 # A target ship known location
CUSTOM = 15 # User waypoint (no specific behaviour)
TAKEOFF = 0 # Take off point
ASCEND_POINT = 1 # Ascension point after take off
PATROL = 2 # Patrol point
PATROL_TRACK = 3 # Patrol race track
NAV = 4 # Nav point
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
INGRESS_CAS = 7 # Ingress cas (should start CAS task)
CAS = 8 # Should do CAS there
EGRESS = 9 # Should stop attack
DESCENT_POINT = 10 # Should start descending to pattern alt
LANDING_POINT = 11 # Should land there
TARGET_POINT = 12 # A target building or static object, position
TARGET_GROUP_LOC = 13 # A target group approximate location
TARGET_SHIP = 14 # A target ship known location
CUSTOM = 15 # User waypoint (no specific behaviour)
JOIN = 16
SPLIT = 17
LOITER = 18
@@ -77,9 +78,13 @@ class FlightWaypointType(Enum):
class FlightWaypoint:
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
alt: Distance = meters(0)) -> None:
def __init__(
self,
waypoint_type: FlightWaypointType,
x: float,
y: float,
alt: Distance = meters(0),
) -> None:
"""Creates a flight waypoint.
Args:
@@ -117,10 +122,13 @@ class FlightWaypoint:
return Point(self.x, self.y)
@classmethod
def from_pydcs(cls, point: MovingPoint,
from_cp: ControlPoint) -> "FlightWaypoint":
waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x,
point.position.y, meters(point.alt))
def from_pydcs(cls, point: MovingPoint, from_cp: ControlPoint) -> "FlightWaypoint":
waypoint = FlightWaypoint(
FlightWaypointType.NAV,
point.position.x,
point.position.y,
meters(point.alt),
)
waypoint.alt_type = point.alt_type
# Other actions exist... but none of them *should* be the first
# waypoint for a flight.
@@ -144,12 +152,19 @@ class FlightWaypoint:
class Flight:
def __init__(self, package: Package, country: str, unit_type: Type[FlyingType],
count: int, flight_type: FlightType, start_type: str,
departure: ControlPoint, arrival: ControlPoint,
divert: Optional[ControlPoint],
custom_name: Optional[str] = None) -> None:
def __init__(
self,
package: Package,
country: str,
unit_type: Type[FlyingType],
count: int,
flight_type: FlightType,
start_type: str,
departure: ControlPoint,
arrival: ControlPoint,
divert: Optional[ControlPoint],
custom_name: Optional[str] = None,
) -> None:
self.package = package
self.country = country
self.unit_type = unit_type
@@ -170,10 +185,9 @@ class Flight:
# FlightPlanBuilder, but an empty flight plan the flight begins with an
# empty flight plan.
from gen.flights.flightplan import CustomFlightPlan
self.flight_plan: FlightPlan = CustomFlightPlan(
package=package,
flight=self,
custom_waypoints=[]
package=package, flight=self, custom_waypoints=[]
)
@property
@@ -191,7 +205,7 @@ class Flight:
return f"[{self.flight_type}] {self.count} x {name}"
def __str__(self):
name = db.unit_get_expanded_info(self.country, self.unit_type, 'name')
name = db.unit_get_expanded_info(self.country, self.unit_type, "name")
if self.custom_name:
return f"{self.custom_name} {self.count} x {name}"
return f"[{self.flight_type}] {self.count} x {name}"

View File

@@ -15,12 +15,7 @@ from datetime import timedelta
from functools import cached_property
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
from dcs.planes import (
E_3A,
E_2C,
A_50,
KJ_2000
)
from dcs.planes import E_3A, E_2C, A_50, KJ_2000
from dcs.mapping import Point
from dcs.unit import Unit
@@ -82,7 +77,7 @@ class FlightPlan:
raise NotImplementedError
def edges(
self, until: Optional[FlightWaypoint] = None
self, until: Optional[FlightWaypoint] = None
) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]:
"""A list of all paths between waypoints, in order."""
waypoints = self.waypoints
@@ -93,8 +88,9 @@ class FlightPlan:
return zip(self.waypoints[:last_index], self.waypoints[1:last_index])
def best_speed_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> Speed:
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:
@@ -115,8 +111,7 @@ class FlightPlan:
# 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:
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
return self.best_speed_between_waypoints(a, b)
@property
@@ -131,8 +126,7 @@ class FlightPlan:
@cached_property
def bingo_fuel(self) -> int:
"""Bingo fuel value for the FlightPlan
"""
"""Bingo fuel value for the FlightPlan"""
distance_to_arrival = self.max_distance_from(self.flight.arrival)
bingo = 1000.0 # Minimum Emergency Fuel
@@ -149,8 +143,7 @@ class FlightPlan:
@cached_property
def joker_fuel(self) -> int:
"""Joker fuel value for the FlightPlan
"""
"""Joker fuel value for the FlightPlan"""
return self.bingo_fuel + 1000
def max_distance_from(self, cp: ControlPoint) -> Distance:
@@ -159,8 +152,9 @@ class FlightPlan:
"""
if not self.waypoints:
return meters(0)
return max([meters(cp.position.distance_to_point(w.position)) for w in
self.waypoints])
return max(
[meters(cp.position.distance_to_point(w.position)) for w in self.waypoints]
)
@property
def tot_offset(self) -> timedelta:
@@ -171,30 +165,30 @@ class FlightPlan:
"""
return timedelta()
def _travel_time_to_waypoint(
self, destination: FlightWaypoint) -> 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}")
f"waypoints for {self.flight}"
)
for previous_waypoint, waypoint in self.edges(until=destination):
total += self.travel_time_between_waypoints(previous_waypoint,
waypoint)
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 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) -> Optional[timedelta]:
raise NotImplementedError
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
raise NotImplementedError
def request_escort_at(self) -> Optional[FlightWaypoint]:
@@ -219,8 +213,7 @@ class FlightPlan:
if takeoff_time is None:
return None
start_time = (takeoff_time - self.estimate_startup() -
self.estimate_ground_ops())
start_time = takeoff_time - self.estimate_startup() - self.estimate_ground_ops()
# In case FP math has given us some barely below zero time, round to
# zero.
@@ -276,14 +269,14 @@ class LoiterFlightPlan(FlightPlan):
def push_time(self) -> timedelta:
raise NotImplementedError
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.push_time
return None
def travel_time_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> timedelta:
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
@@ -328,12 +321,12 @@ class FormationFlightPlan(LoiterFlightPlan):
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))
speeds.append(
self.best_speed_between_waypoints(previous_waypoint, waypoint)
)
return min(speeds)
def speed_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> Speed:
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.
@@ -366,7 +359,7 @@ class FormationFlightPlan(LoiterFlightPlan):
return self.join_time - TravelTime.between_points(
self.hold.position,
self.join.position,
GroundSpeed.for_flight(self.flight, self.hold.alt)
GroundSpeed.for_flight(self.flight, self.hold.alt),
)
@property
@@ -406,8 +399,7 @@ class PatrollingFlightPlan(FlightPlan):
return self.patrol_start_time
return None
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.patrol_end:
return self.patrol_end_time
return None
@@ -497,8 +489,7 @@ class TarCapFlightPlan(PatrollingFlightPlan):
def tot_offset(self) -> timedelta:
return -self.lead_time
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.patrol_end:
return self.patrol_end_time
return super().depart_time_for_waypoint(waypoint)
@@ -549,13 +540,12 @@ class StrikeFlightPlan(FormationFlightPlan):
@property
def package_speed_waypoints(self) -> Set[FlightWaypoint]:
return {
self.ingress,
self.egress,
self.split,
} | set(self.targets)
self.ingress,
self.egress,
self.split,
} | set(self.targets)
def speed_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> Speed:
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:
@@ -571,10 +561,12 @@ class StrikeFlightPlan(FormationFlightPlan):
@property
def target_area_waypoint(self) -> FlightWaypoint:
return FlightWaypoint(FlightWaypointType.TARGET_GROUP_LOC,
self.package.target.position.x,
self.package.target.position.y,
meters(0))
return FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
self.package.target.position.x,
self.package.target.position.y,
meters(0),
)
@property
def travel_time_to_target(self) -> timedelta:
@@ -588,14 +580,15 @@ class StrikeFlightPlan(FormationFlightPlan):
# 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)
previous_waypoint, self.target_area_waypoint
)
break
total += self.travel_time_between_waypoints(previous_waypoint,
waypoint)
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}")
f"waypoints for {self.flight}"
)
return total
@property
@@ -604,28 +597,28 @@ class StrikeFlightPlan(FormationFlightPlan):
@property
def join_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(
self.join, self.ingress)
travel_time = self.travel_time_between_waypoints(self.join, self.ingress)
return self.ingress_time - travel_time
@property
def split_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(
self.egress, self.split)
travel_time = self.travel_time_between_waypoints(self.egress, self.split)
return self.egress_time + travel_time
@property
def ingress_time(self) -> timedelta:
tot = self.package.time_over_target
travel_time = self.travel_time_between_waypoints(
self.ingress, self.target_area_waypoint)
self.ingress, self.target_area_waypoint
)
return tot - travel_time
@property
def egress_time(self) -> timedelta:
tot = self.package.time_over_target
travel_time = self.travel_time_between_waypoints(
self.target_area_waypoint, self.egress)
self.target_area_waypoint, self.egress
)
return tot + travel_time
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
@@ -671,7 +664,8 @@ class SweepFlightPlan(LoiterFlightPlan):
@property
def sweep_start_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(
self.sweep_start, self.sweep_end)
self.sweep_start, self.sweep_end
)
return self.sweep_end_time - travel_time
@property
@@ -685,8 +679,7 @@ class SweepFlightPlan(LoiterFlightPlan):
return self.sweep_end_time
return None
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.push_time
return None
@@ -696,7 +689,7 @@ class SweepFlightPlan(LoiterFlightPlan):
return self.sweep_end_time - TravelTime.between_points(
self.hold.position,
self.sweep_end.position,
GroundSpeed.for_flight(self.flight, self.hold.alt)
GroundSpeed.for_flight(self.flight, self.hold.alt),
)
def mission_departure_time(self) -> timedelta:
@@ -763,8 +756,7 @@ class CustomFlightPlan(FlightPlan):
return self.package.time_over_target
return None
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
return None
@property
@@ -794,9 +786,11 @@ class FlightPlanBuilder:
self.threat_zones = self.game.threat_zone_for(not self.is_player)
def populate_flight_plan(
self, flight: Flight,
# TODO: Custom targets should be an attribute of the flight.
custom_targets: Optional[List[Unit]] = None) -> None:
self,
flight: Flight,
# TODO: Custom targets should be an attribute of the flight.
custom_targets: Optional[List[Unit]] = None,
) -> 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")
@@ -805,8 +799,8 @@ class FlightPlanBuilder:
flight.flight_plan = self.generate_flight_plan(flight, custom_targets)
def generate_flight_plan(
self, flight: Flight,
custom_targets: Optional[List[Unit]]) -> FlightPlan:
self, flight: Flight, custom_targets: Optional[List[Unit]]
) -> FlightPlan:
# TODO: Flesh out mission types.
task = flight.flight_type
if task == FlightType.ANTISHIP:
@@ -835,8 +829,7 @@ class FlightPlanBuilder:
return self.generate_tarcap(flight)
elif task == FlightType.AEWC:
return self.generate_aewc(flight)
raise PlanningError(
f"{task} flight plan generation not implemented")
raise PlanningError(f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None:
# The simple case is where the target is greater than the ingress
@@ -873,6 +866,7 @@ class FlightPlanBuilder:
# | | | |
# +--------------+ +---------------+
from gen.ato import PackageWaypoints
target = self.package.target.position
join_point = self.preferred_join_point()
@@ -906,10 +900,9 @@ class FlightPlanBuilder:
def legacy_package_waypoints_impl(self) -> None:
from gen.ato import PackageWaypoints
ingress_point = self._ingress_point(
self._target_heading_to_package_airfield())
egress_point = self._egress_point(
self._target_heading_to_package_airfield())
ingress_point = self._ingress_point(self._target_heading_to_package_airfield())
egress_point = self._egress_point(self._target_heading_to_package_airfield())
join_point = self._rendezvous_point(ingress_point)
split_point = self._rendezvous_point(egress_point)
self.package.waypoints = PackageWaypoints(
@@ -921,7 +914,8 @@ class FlightPlanBuilder:
def preferred_join_point(self) -> Optional[Point]:
path = self.game.navmesh_for(self.is_player).shortest_path(
self.package_airfield().position, self.package.target.position)
self.package_airfield().position, self.package.target.position
)
for point in reversed(path):
if not self.threat_zones.threatened(point):
return point
@@ -961,16 +955,16 @@ class FlightPlanBuilder:
targets.append(StrikeTarget(building.category, building))
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_STRIKE,
targets)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_STRIKE, targets
)
def generate_aewc(self, flight: Flight) -> AwacsFlightPlan:
"""Generate a AWACS flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
start = self.aewc_orbit(location)
@@ -994,10 +988,12 @@ class FlightPlanBuilder:
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, start.position,
patrol_alt),
nav_from=builder.nav_path(start.position, flight.arrival.position,
patrol_alt),
nav_to=builder.nav_path(
flight.departure.position, start.position, patrol_alt
),
nav_from=builder.nav_path(
start.position, flight.arrival.position, patrol_alt
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
hold=start,
@@ -1017,11 +1013,11 @@ class FlightPlanBuilder:
targets: List[StrikeTarget] = []
for group in location.groups:
targets.append(
StrikeTarget(f"{group.name} at {location.name}", group))
targets.append(StrikeTarget(f"{group.name} at {location.name}", group))
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_BAI, targets)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_BAI, targets
)
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
"""Generates an anti-ship flight plan.
@@ -1043,11 +1039,11 @@ class FlightPlanBuilder:
targets: List[StrikeTarget] = []
for group in location.groups:
targets.append(
StrikeTarget(f"{group.name} at {location.name}", group))
targets.append(StrikeTarget(f"{group.name} at {location.name}", group))
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_BAI, targets)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_BAI, targets
)
def generate_barcap(self, flight: Flight) -> BarCapFlightPlan:
"""Generate a BARCAP flight at a given location.
@@ -1061,10 +1057,12 @@ class FlightPlanBuilder:
raise InvalidObjectiveLocation(flight.flight_type, location)
start, end = self.racetrack_for_objective(location, barcap=True)
patrol_alt = meters(random.randint(
int(self.doctrine.min_patrol_altitude.meters),
int(self.doctrine.max_patrol_altitude.meters)
))
patrol_alt = meters(
random.randint(
int(self.doctrine.min_patrol_altitude.meters),
int(self.doctrine.max_patrol_altitude.meters),
)
)
builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.race_track(start, end, patrol_alt)
@@ -1075,14 +1073,16 @@ class FlightPlanBuilder:
patrol_duration=self.doctrine.cap_duration,
engagement_distance=self.doctrine.cap_engagement_range,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, start.position,
patrol_alt),
nav_from=builder.nav_path(end.position, flight.arrival.position,
patrol_alt),
nav_to=builder.nav_path(
flight.departure.position, start.position, patrol_alt
),
nav_from=builder.nav_path(
end.position, flight.arrival.position, patrol_alt
),
patrol_start=start,
patrol_end=end,
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
divert=builder.divert(flight.divert),
)
def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
@@ -1095,12 +1095,10 @@ class FlightPlanBuilder:
target = self.package.target.position
heading = self.package.waypoints.join.heading_between_point(target)
start = target.point_from_heading(heading,
-self.doctrine.sweep_distance.meters)
start = target.point_from_heading(heading, -self.doctrine.sweep_distance.meters)
builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.sweep(start, target,
self.doctrine.ingress_altitude)
start, end = builder.sweep(start, target, self.doctrine.ingress_altitude)
hold = builder.hold(self._hold_point(flight))
@@ -1111,18 +1109,21 @@ class FlightPlanBuilder:
takeoff=builder.takeoff(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, flight.arrival.position,
self.doctrine.ingress_altitude),
nav_to=builder.nav_path(
hold.position, start.position, self.doctrine.ingress_altitude
),
nav_from=builder.nav_path(
end.position, flight.arrival.position, self.doctrine.ingress_altitude
),
sweep_start=start,
sweep_end=end,
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
divert=builder.divert(flight.divert),
)
def racetrack_for_objective(self, location: MissionTarget,
barcap: bool) -> Tuple[Point, Point]:
def 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*
@@ -1135,20 +1136,21 @@ class FlightPlanBuilder:
else:
raise PlanningError("Could not find any enemy airfields")
heading = location.position.heading_between_point(
closest_airfield.position
)
heading = location.position.heading_between_point(closest_airfield.position)
position = ShapelyPoint(self.package.target.position.x,
self.package.target.position.y)
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)
distance_to_no_fly = (
meters(position.distance(self.threat_zones.all))
- self.doctrine.cap_engagement_range
- nautical_miles(5)
)
else:
# Other race tracks (TARCAPs, currently) just try to keep some
# distance from the nearest enemy airbase, but since they are by
@@ -1158,22 +1160,24 @@ class FlightPlanBuilder:
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
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)
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,
random.randint(int(min_cap_distance.meters),
int(max_cap_distance.meters))
random.randint(int(min_cap_distance.meters), int(max_cap_distance.meters)),
)
diameter = random.randint(
int(self.doctrine.cap_min_track_length.meters),
int(self.doctrine.cap_max_track_length.meters)
int(self.doctrine.cap_max_track_length.meters),
)
start = end.point_from_heading(heading - 180, diameter)
return start, end
@@ -1185,13 +1189,11 @@ class FlightPlanBuilder:
# Place this either over the target or as close as possible outside the
# threat zone: https://github.com/Khopa/dcs_liberation/issues/842.
heading = location.position.heading_between_point(closest_airfield.position)
return location.position.point_from_heading(
heading,
5000
)
return location.position.point_from_heading(heading, 5000)
def racetrack_for_frontline(self, origin: Point,
front_line: FrontLine) -> Tuple[Point, Point]:
def racetrack_for_frontline(
self, origin: Point, front_line: FrontLine
) -> Tuple[Point, Point]:
ally_cp, enemy_cp = front_line.control_points
# Find targets waypoints
@@ -1200,8 +1202,10 @@ class FlightPlanBuilder:
)
center = ingress.point_from_heading(heading, distance / 2)
orbit_center = center.point_from_heading(
heading - 90, random.randint(int(nautical_miles(6).meters),
int(nautical_miles(15).meters))
heading - 90,
random.randint(
int(nautical_miles(6).meters), int(nautical_miles(15).meters)
),
)
combat_width = distance / 2
@@ -1227,18 +1231,21 @@ class FlightPlanBuilder:
location = self.package.target
patrol_alt = meters(
random.randint(int(self.doctrine.min_patrol_altitude.meters),
int(self.doctrine.max_patrol_altitude.meters)))
random.randint(
int(self.doctrine.min_patrol_altitude.meters),
int(self.doctrine.max_patrol_altitude.meters),
)
)
# Create points
builder = WaypointBuilder(flight, self.game, self.is_player)
if isinstance(location, FrontLine):
orbit0p, orbit1p = self.racetrack_for_frontline(
flight.departure.position, location)
flight.departure.position, location
)
else:
orbit0p, orbit1p = self.racetrack_for_objective(location,
barcap=False)
orbit0p, orbit1p = self.racetrack_for_objective(location, barcap=False)
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
return TarCapFlightPlan(
@@ -1252,18 +1259,17 @@ class FlightPlanBuilder:
patrol_duration=self.doctrine.cap_duration,
engagement_distance=self.doctrine.cap_engagement_range,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, orbit0p,
patrol_alt),
nav_from=builder.nav_path(orbit1p, flight.arrival.position,
patrol_alt),
nav_to=builder.nav_path(flight.departure.position, orbit0p, patrol_alt),
nav_from=builder.nav_path(orbit1p, flight.arrival.position, patrol_alt),
patrol_start=start,
patrol_end=end,
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
divert=builder.divert(flight.divert),
)
def generate_dead(self, flight: Flight,
custom_targets: Optional[List[Unit]]) -> StrikeFlightPlan:
def generate_dead(
self, flight: Flight, custom_targets: Optional[List[Unit]]
) -> StrikeFlightPlan:
"""Generate a DEAD flight at a given location.
Args:
@@ -1276,7 +1282,8 @@ class FlightPlanBuilder:
is_sam = isinstance(location, SamGroundObject)
if not is_ewr and not is_sam:
logging.exception(
f"Invalid Objective Location for DEAD flight {flight=} at {location=}")
f"Invalid Objective Location for DEAD flight {flight=} at {location=}"
)
raise InvalidObjectiveLocation(flight.flight_type, location)
# TODO: Unify these.
@@ -1288,8 +1295,9 @@ class FlightPlanBuilder:
for target in custom_targets:
targets.append(StrikeTarget(location.name, target))
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_DEAD, targets)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_DEAD, targets
)
def generate_oca_strike(self, flight: Flight) -> StrikeFlightPlan:
"""Generate an OCA Strike flight plan at a given location.
@@ -1302,11 +1310,13 @@ class FlightPlanBuilder:
if not isinstance(location, Airfield):
logging.exception(
f"Invalid Objective Location for OCA Strike flight "
f"{flight=} at {location=}.")
f"{flight=} at {location=}."
)
raise InvalidObjectiveLocation(flight.flight_type, location)
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_OCA_AIRCRAFT)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_OCA_AIRCRAFT
)
def generate_runway_attack(self, flight: Flight) -> StrikeFlightPlan:
"""Generate a runway attack flight plan at a given location.
@@ -1319,14 +1329,17 @@ class FlightPlanBuilder:
if not isinstance(location, Airfield):
logging.exception(
f"Invalid Objective Location for runway bombing flight "
f"{flight=} at {location=}.")
f"{flight=} at {location=}."
)
raise InvalidObjectiveLocation(flight.flight_type, location)
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_OCA_RUNWAY)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_OCA_RUNWAY
)
def generate_sead(self, flight: Flight,
custom_targets: Optional[List[Unit]]) -> StrikeFlightPlan:
def generate_sead(
self, flight: Flight, custom_targets: Optional[List[Unit]]
) -> StrikeFlightPlan:
"""Generate a SEAD flight at a given location.
Args:
@@ -1344,16 +1357,19 @@ class FlightPlanBuilder:
for target in custom_targets:
targets.append(StrikeTarget(location.name, target))
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_SEAD, targets)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_SEAD, targets
)
def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
assert self.package.waypoints is not None
builder = WaypointBuilder(flight, self.game, self.is_player)
ingress, target, egress = builder.escort(
self.package.waypoints.ingress, self.package.target,
self.package.waypoints.egress)
self.package.waypoints.ingress,
self.package.target,
self.package.waypoints.egress,
)
hold = builder.hold(self._hold_point(flight))
join = builder.join(self.package.waypoints.join)
split = builder.split(self.package.waypoints.split)
@@ -1364,17 +1380,19 @@ class FlightPlanBuilder:
takeoff=builder.takeoff(flight.departure),
hold=hold,
hold_duration=timedelta(minutes=5),
nav_to=builder.nav_path(hold.position, join.position,
self.doctrine.ingress_altitude),
nav_to=builder.nav_path(
hold.position, join.position, self.doctrine.ingress_altitude
),
join=join,
ingress=ingress,
targets=[target],
egress=egress,
split=split,
nav_from=builder.nav_path(split.position, flight.arrival.position,
self.doctrine.ingress_altitude),
nav_from=builder.nav_path(
split.position, flight.arrival.position, self.doctrine.ingress_altitude
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
divert=builder.divert(flight.divert),
)
def generate_cas(self, flight: Flight) -> CasFlightPlan:
@@ -1389,8 +1407,7 @@ class FlightPlanBuilder:
raise InvalidObjectiveLocation(flight.flight_type, location)
ingress, heading, distance = Conflict.frontline_vector(
location.control_points[0], location.control_points[1],
self.game.theater
location.control_points[0], location.control_points[1], self.game.theater
)
center = ingress.point_from_heading(heading, distance / 2)
egress = ingress.point_from_heading(heading, distance)
@@ -1407,22 +1424,26 @@ class FlightPlanBuilder:
flight=flight,
patrol_duration=self.doctrine.cas_duration,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, ingress,
self.doctrine.ingress_altitude),
nav_from=builder.nav_path(egress, flight.arrival.position,
self.doctrine.ingress_altitude),
patrol_start=builder.ingress(FlightWaypointType.INGRESS_CAS,
ingress, location),
nav_to=builder.nav_path(
flight.departure.position, ingress, self.doctrine.ingress_altitude
),
nav_from=builder.nav_path(
egress, flight.arrival.position, self.doctrine.ingress_altitude
),
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),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
divert=builder.divert(flight.divert),
)
@staticmethod
def target_waypoint(flight: Flight, builder: WaypointBuilder,
target: StrikeTarget) -> FlightWaypoint:
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:
@@ -1433,8 +1454,9 @@ class FlightPlanBuilder:
return builder.strike_point(target)
@staticmethod
def target_area_waypoint(flight: Flight, location: MissionTarget,
builder: WaypointBuilder) -> FlightWaypoint:
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:
@@ -1455,12 +1477,14 @@ class FlightPlanBuilder:
# If the origin airfield is closer to the target than the join
# point, plan the hold point such that it retreats from the origin
# airfield.
return join.point_from_heading(target.heading_between_point(origin),
self.doctrine.push_distance.meters)
return join.point_from_heading(
target.heading_between_point(origin), self.doctrine.push_distance.meters
)
heading_to_join = origin.heading_between_point(join)
hold_point = origin.point_from_heading(
heading_to_join, self.doctrine.push_distance.meters)
heading_to_join, self.doctrine.push_distance.meters
)
hold_distance = meters(hold_point.distance_to_point(join))
if hold_distance >= self.doctrine.push_distance:
# Hold point is between the origin airfield and the join point and
@@ -1474,26 +1498,27 @@ class FlightPlanBuilder:
# properly.
origin_to_join = origin.distance_to_point(join)
cos_theta = (
(self.doctrine.hold_distance.meters ** 2 +
origin_to_join ** 2 -
self.doctrine.join_distance.meters ** 2) /
(2 * self.doctrine.hold_distance.meters * origin_to_join)
)
self.doctrine.hold_distance.meters ** 2
+ origin_to_join ** 2
- self.doctrine.join_distance.meters ** 2
) / (2 * self.doctrine.hold_distance.meters * origin_to_join)
try:
theta = math.acos(cos_theta)
except ValueError:
# No solution that maintains hold and join distances. Extend the
# hold point away from the target.
return origin.point_from_heading(
target.heading_between_point(origin),
self.doctrine.hold_distance.meters)
target.heading_between_point(origin), self.doctrine.hold_distance.meters
)
return origin.point_from_heading(heading_to_join - theta,
self.doctrine.hold_distance.meters)
return origin.point_from_heading(
heading_to_join - theta, self.doctrine.hold_distance.meters
)
# TODO: Make a model for the waypoint builder and use that in the UI.
def generate_rtb_waypoint(self, flight: Flight,
arrival: ControlPoint) -> FlightWaypoint:
def generate_rtb_waypoint(
self, flight: Flight, arrival: ControlPoint
) -> FlightWaypoint:
"""Generate RTB landing point.
Args:
@@ -1504,20 +1529,23 @@ class FlightPlanBuilder:
return builder.land(arrival)
def strike_flightplan(
self, flight: Flight, location: MissionTarget,
ingress_type: FlightWaypointType,
targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan:
self,
flight: Flight,
location: MissionTarget,
ingress_type: FlightWaypointType,
targets: Optional[List[StrikeTarget]] = None,
) -> StrikeFlightPlan:
assert self.package.waypoints is not None
builder = WaypointBuilder(flight, self.game, self.is_player, targets)
target_waypoints: List[FlightWaypoint] = []
if targets is not None:
for target in targets:
target_waypoints.append(
self.target_waypoint(flight, builder, target))
target_waypoints.append(self.target_waypoint(flight, builder, target))
else:
target_waypoints.append(
self.target_area_waypoint(flight, location, builder))
self.target_area_waypoint(flight, location, builder)
)
hold = builder.hold(self._hold_point(flight))
join = builder.join(self.package.waypoints.join)
@@ -1529,32 +1557,38 @@ class FlightPlanBuilder:
takeoff=builder.takeoff(flight.departure),
hold=hold,
hold_duration=timedelta(minutes=5),
nav_to=builder.nav_path(hold.position, join.position,
self.doctrine.ingress_altitude),
nav_to=builder.nav_path(
hold.position, join.position, self.doctrine.ingress_altitude
),
join=join,
ingress=builder.ingress(ingress_type,
self.package.waypoints.ingress, location),
ingress=builder.ingress(
ingress_type, self.package.waypoints.ingress, location
),
targets=target_waypoints,
egress=builder.egress(self.package.waypoints.egress, location),
split=split,
nav_from=builder.nav_path(split.position, flight.arrival.position,
self.doctrine.ingress_altitude),
nav_from=builder.nav_path(
split.position, flight.arrival.position, self.doctrine.ingress_altitude
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
divert=builder.divert(flight.divert),
)
def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:
"""Creates a rendezvous point that retreats from the origin airfield."""
return attack_transition.point_from_heading(
self.package.target.position.heading_between_point(
self.package_airfield().position),
self.doctrine.join_distance.meters)
self.package_airfield().position
),
self.doctrine.join_distance.meters,
)
def _advancing_rendezvous_point(self, attack_transition: Point) -> Point:
"""Creates a rendezvous point that advances toward the target."""
heading = self._heading_to_package_airfield(attack_transition)
return attack_transition.point_from_heading(
heading, -self.doctrine.join_distance.meters)
heading, -self.doctrine.join_distance.meters
)
def _rendezvous_should_retreat(self, attack_transition: Point) -> bool:
transition_target_distance = attack_transition.distance_to_point(
@@ -1609,13 +1643,9 @@ class FlightPlanBuilder:
# 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
)
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 RuntimeError(
"Could not find any airfield assigned to this package"
)
raise RuntimeError("Could not find any airfield assigned to this package")

View File

@@ -23,7 +23,6 @@ if TYPE_CHECKING:
class GroundSpeed:
@classmethod
def for_flight(cls, flight: Flight, altitude: Distance) -> Speed:
if not issubclass(flight.unit_type, FlyingType):
@@ -55,13 +54,11 @@ class TravelTime:
def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
error_factor = 1.1
distance = meters(a.distance_to_point(b))
return timedelta(
hours=distance.nautical_miles / speed.knots * error_factor)
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
# TODO: Most if not all of this should move into FlightPlan.
class TotEstimator:
def __init__(self, package: Package) -> None:
self.package = package
@@ -75,9 +72,9 @@ class TotEstimator:
return startup_time
def earliest_tot(self) -> timedelta:
earliest_tot = max((
self.earliest_tot_for_flight(f) for f in self.package.flights
))
earliest_tot = max(
(self.earliest_tot_for_flight(f) for f in self.package.flights)
)
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we

View File

@@ -36,8 +36,13 @@ class StrikeTarget:
class WaypointBuilder:
def __init__(self, flight: Flight, game: Game, player: bool,
targets: Optional[List[StrikeTarget]] = None) -> None:
def __init__(
self,
flight: Flight,
game: Game,
player: bool,
targets: Optional[List[StrikeTarget]] = None,
) -> None:
self.flight = flight
self.conditions = game.conditions
self.doctrine = game.faction_for(player).doctrine
@@ -65,9 +70,7 @@ class WaypointBuilder:
FlightWaypointType.NAV,
position.x,
position.y,
meters(
500
) if self.is_helo else self.doctrine.rendezvous_altitude
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
)
waypoint.name = "NAV"
waypoint.alt_type = "BARO"
@@ -75,10 +78,7 @@ class WaypointBuilder:
waypoint.pretty_name = "Enter theater"
else:
waypoint = FlightWaypoint(
FlightWaypointType.TAKEOFF,
position.x,
position.y,
meters(0)
FlightWaypointType.TAKEOFF, position.x, position.y, meters(0)
)
waypoint.name = "TAKEOFF"
waypoint.alt_type = "RADIO"
@@ -98,9 +98,7 @@ class WaypointBuilder:
FlightWaypointType.NAV,
position.x,
position.y,
meters(
500
) if self.is_helo else self.doctrine.rendezvous_altitude
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
)
waypoint.name = "NAV"
waypoint.alt_type = "BARO"
@@ -108,10 +106,7 @@ class WaypointBuilder:
waypoint.pretty_name = "Exit theater"
else:
waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT,
position.x,
position.y,
meters(0)
FlightWaypointType.LANDING_POINT, position.x, position.y, meters(0)
)
waypoint.name = "LANDING"
waypoint.alt_type = "RADIO"
@@ -119,8 +114,7 @@ class WaypointBuilder:
waypoint.pretty_name = "Land"
return waypoint
def divert(self,
divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
def divert(self, divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
"""Create divert waypoint for the given arrival airfield or carrier.
Args:
@@ -141,10 +135,7 @@ class WaypointBuilder:
altitude_type = "RADIO"
waypoint = FlightWaypoint(
FlightWaypointType.DIVERT,
position.x,
position.y,
altitude
FlightWaypointType.DIVERT, position.x, position.y, altitude
)
waypoint.alt_type = altitude_type
waypoint.name = "DIVERT"
@@ -158,9 +149,7 @@ class WaypointBuilder:
FlightWaypointType.LOITER,
position.x,
position.y,
meters(
500
) if self.is_helo else self.doctrine.rendezvous_altitude
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
)
waypoint.pretty_name = "Hold"
waypoint.description = "Wait until push time"
@@ -172,9 +161,7 @@ class WaypointBuilder:
FlightWaypointType.JOIN,
position.x,
position.y,
meters(
80
) if self.is_helo else self.doctrine.ingress_altitude
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
)
if self.is_helo:
waypoint.alt_type = "RADIO"
@@ -188,9 +175,7 @@ class WaypointBuilder:
FlightWaypointType.SPLIT,
position.x,
position.y,
meters(
80
) if self.is_helo else self.doctrine.ingress_altitude
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
)
if self.is_helo:
waypoint.alt_type = "RADIO"
@@ -199,15 +184,17 @@ class WaypointBuilder:
waypoint.name = "SPLIT"
return waypoint
def ingress(self, ingress_type: FlightWaypointType, position: Point,
objective: MissionTarget) -> FlightWaypoint:
def ingress(
self,
ingress_type: FlightWaypointType,
position: Point,
objective: MissionTarget,
) -> FlightWaypoint:
waypoint = FlightWaypoint(
ingress_type,
position.x,
position.y,
meters(
50
) if self.is_helo else self.doctrine.ingress_altitude
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
)
if self.is_helo:
waypoint.alt_type = "RADIO"
@@ -223,9 +210,7 @@ class WaypointBuilder:
FlightWaypointType.EGRESS,
position.x,
position.y,
meters(
50
) if self.is_helo else self.doctrine.ingress_altitude
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
)
if self.is_helo:
waypoint.alt_type = "RADIO"
@@ -252,7 +237,7 @@ class WaypointBuilder:
FlightWaypointType.TARGET_POINT,
target.target.position.x,
target.target.position.y,
meters(0)
meters(0),
)
waypoint.description = description
waypoint.pretty_name = description
@@ -277,13 +262,14 @@ class WaypointBuilder:
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
@staticmethod
def _target_area(name: str, location: MissionTarget,
flyover: bool = False) -> FlightWaypoint:
def _target_area(
name: str, location: MissionTarget, flyover: bool = False
) -> FlightWaypoint:
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
location.position.x,
location.position.y,
meters(0)
meters(0),
)
waypoint.description = name
waypoint.pretty_name = name
@@ -308,7 +294,7 @@ class WaypointBuilder:
FlightWaypointType.CAS,
position.x,
position.y,
meters(50) if self.is_helo else meters(1000)
meters(50) if self.is_helo else meters(1000),
)
waypoint.alt_type = "RADIO"
waypoint.description = "Provide CAS"
@@ -325,10 +311,7 @@ class WaypointBuilder:
altitude: Altitude of the racetrack.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PATROL_TRACK,
position.x,
position.y,
altitude
FlightWaypointType.PATROL_TRACK, position.x, position.y, altitude
)
waypoint.name = "RACETRACK START"
waypoint.description = "Orbit between this point and the next point"
@@ -344,18 +327,16 @@ class WaypointBuilder:
altitude: Altitude of the racetrack.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PATROL,
position.x,
position.y,
altitude
FlightWaypointType.PATROL, position.x, position.y, altitude
)
waypoint.name = "RACETRACK END"
waypoint.description = "Orbit between this point and the previous point"
waypoint.pretty_name = "Race-track end"
return waypoint
def race_track(self, start: Point, end: Point,
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
def race_track(
self, start: Point, end: Point, altitude: Distance
) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a racetrack orbit.
Args:
@@ -363,8 +344,10 @@ class WaypointBuilder:
end: The ending racetrack waypoint.
altitude: The racetrack altitude.
"""
return (self.race_track_start(start, altitude),
self.race_track_end(end, altitude))
return (
self.race_track_start(start, altitude),
self.race_track_end(end, altitude),
)
@staticmethod
def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
@@ -375,12 +358,7 @@ class WaypointBuilder:
altitude: Altitude of the racetrack.
"""
waypoint = FlightWaypoint(
FlightWaypointType.LOITER,
start.x,
start.y,
altitude
)
waypoint = FlightWaypoint(FlightWaypointType.LOITER, start.x, start.y, altitude)
waypoint.name = "ORBIT"
waypoint.description = "Anchor and hold at this point"
waypoint.pretty_name = "Orbit"
@@ -395,10 +373,7 @@ class WaypointBuilder:
altitude: Altitude of the sweep in meters.
"""
waypoint = FlightWaypoint(
FlightWaypointType.INGRESS_SWEEP,
position.x,
position.y,
altitude
FlightWaypointType.INGRESS_SWEEP, position.x, position.y, altitude
)
waypoint.name = "SWEEP START"
waypoint.description = "Proceed to the target and engage enemy aircraft"
@@ -414,18 +389,16 @@ class WaypointBuilder:
altitude: Altitude of the sweep in meters.
"""
waypoint = FlightWaypoint(
FlightWaypointType.EGRESS,
position.x,
position.y,
altitude
FlightWaypointType.EGRESS, position.x, position.y, altitude
)
waypoint.name = "SWEEP END"
waypoint.description = "End of sweep"
waypoint.pretty_name = "Sweep end"
return waypoint
def sweep(self, start: Point, end: Point,
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
def sweep(
self, start: Point, end: Point, altitude: Distance
) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a racetrack orbit.
Args:
@@ -433,11 +406,11 @@ class WaypointBuilder:
end: The end of the sweep.
altitude: The sweep altitude.
"""
return (self.sweep_start(start, altitude),
self.sweep_end(end, altitude))
return (self.sweep_start(start, altitude), self.sweep_end(end, altitude))
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
def escort(
self, ingress: Point, target: MissionTarget, egress: Point
) -> Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
"""Creates the waypoints needed to escort the package.
Args:
@@ -451,16 +424,13 @@ class WaypointBuilder:
# description in gen.aircraft.JoinPointBuilder), so instead we give
# the escort flights a flight plan including the ingress point, target
# area, and egress point.
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
target)
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
target.position.x,
target.position.y,
meters(
50
) if self.is_helo else self.doctrine.ingress_altitude
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
)
if self.is_helo:
waypoint.alt_type = "RADIO"
@@ -480,18 +450,14 @@ class WaypointBuilder:
altitude: Altitude of the waypoint.
"""
waypoint = FlightWaypoint(
FlightWaypointType.NAV,
position.x,
position.y,
altitude
FlightWaypointType.NAV, position.x, position.y, altitude
)
waypoint.name = "NAV"
waypoint.description = "NAV"
waypoint.pretty_name = "Nav"
return waypoint
def nav_path(self, a: Point, b: Point,
altitude: Distance) -> List[FlightWaypoint]:
def nav_path(self, a: Point, b: Point, altitude: Distance) -> List[FlightWaypoint]:
path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
return [self.nav(self.perturb(p), altitude) for p in path]
@@ -518,10 +484,8 @@ class WaypointBuilder:
previous = current
current = nxt
def nav_point_prunable(self, previous: Point, current: Point,
nxt: Point) -> bool:
previous_threatened = self.threat_zones.path_threatened(previous,
current)
def nav_point_prunable(self, previous: Point, current: Point, nxt: Point) -> bool:
previous_threatened = self.threat_zones.path_threatened(previous, current)
next_threatened = self.threat_zones.path_threatened(current, nxt)
pruned_threatened = self.threat_zones.path_threatened(previous, nxt)
previous_distance = meters(previous.distance_to_point(current))