mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Reorganize flight planning.
Previously we were trying to make every potential flight plan look just like a strike mission's flight plan. This led to a lot of special case behavior in several places that was causing us to misplan TOTs. I've reorganized this such that there's now an explicit `FlightPlan` class, and any specialized behavior is handled by the subclasses. I've also taken the opportunity to alter the behavior of CAS and front-line CAP missions. These no longer involve the usual formation waypoints. Instead the CAP will aim to be on station at the time that the CAS mission reaches its ingress point, and leave at its egress time. Both flights fly directly to the point with a start time configured for a rendezvous. It might be worth adding hold points back to every flight plan just to ensure that non-formation flights don't end up with a very low speed enroute to the target if they perform ground ops quicker than expected.
This commit is contained in:
parent
d94c57afd6
commit
88b9ed29ba
@ -1,4 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from game.utils import nm_to_meter, feet_to_meter
|
from game.utils import nm_to_meter, feet_to_meter
|
||||||
|
|
||||||
@ -25,11 +26,14 @@ class Doctrine:
|
|||||||
max_patrol_altitude: int
|
max_patrol_altitude: int
|
||||||
pattern_altitude: int
|
pattern_altitude: int
|
||||||
|
|
||||||
|
cap_duration: timedelta
|
||||||
cap_min_track_length: int
|
cap_min_track_length: int
|
||||||
cap_max_track_length: int
|
cap_max_track_length: int
|
||||||
cap_min_distance_from_cp: int
|
cap_min_distance_from_cp: int
|
||||||
cap_max_distance_from_cp: int
|
cap_max_distance_from_cp: int
|
||||||
|
|
||||||
|
cas_duration: timedelta
|
||||||
|
|
||||||
|
|
||||||
MODERN_DOCTRINE = Doctrine(
|
MODERN_DOCTRINE = Doctrine(
|
||||||
cap=True,
|
cap=True,
|
||||||
@ -48,10 +52,12 @@ MODERN_DOCTRINE = Doctrine(
|
|||||||
min_patrol_altitude=feet_to_meter(15000),
|
min_patrol_altitude=feet_to_meter(15000),
|
||||||
max_patrol_altitude=feet_to_meter(33000),
|
max_patrol_altitude=feet_to_meter(33000),
|
||||||
pattern_altitude=feet_to_meter(5000),
|
pattern_altitude=feet_to_meter(5000),
|
||||||
|
cap_duration=timedelta(minutes=30),
|
||||||
cap_min_track_length=nm_to_meter(15),
|
cap_min_track_length=nm_to_meter(15),
|
||||||
cap_max_track_length=nm_to_meter(40),
|
cap_max_track_length=nm_to_meter(40),
|
||||||
cap_min_distance_from_cp=nm_to_meter(10),
|
cap_min_distance_from_cp=nm_to_meter(10),
|
||||||
cap_max_distance_from_cp=nm_to_meter(40),
|
cap_max_distance_from_cp=nm_to_meter(40),
|
||||||
|
cas_duration=timedelta(minutes=30),
|
||||||
)
|
)
|
||||||
|
|
||||||
COLDWAR_DOCTRINE = Doctrine(
|
COLDWAR_DOCTRINE = Doctrine(
|
||||||
@ -71,10 +77,12 @@ COLDWAR_DOCTRINE = Doctrine(
|
|||||||
min_patrol_altitude=feet_to_meter(10000),
|
min_patrol_altitude=feet_to_meter(10000),
|
||||||
max_patrol_altitude=feet_to_meter(24000),
|
max_patrol_altitude=feet_to_meter(24000),
|
||||||
pattern_altitude=feet_to_meter(5000),
|
pattern_altitude=feet_to_meter(5000),
|
||||||
|
cap_duration=timedelta(minutes=30),
|
||||||
cap_min_track_length=nm_to_meter(12),
|
cap_min_track_length=nm_to_meter(12),
|
||||||
cap_max_track_length=nm_to_meter(24),
|
cap_max_track_length=nm_to_meter(24),
|
||||||
cap_min_distance_from_cp=nm_to_meter(8),
|
cap_min_distance_from_cp=nm_to_meter(8),
|
||||||
cap_max_distance_from_cp=nm_to_meter(25),
|
cap_max_distance_from_cp=nm_to_meter(25),
|
||||||
|
cas_duration=timedelta(minutes=30),
|
||||||
)
|
)
|
||||||
|
|
||||||
WWII_DOCTRINE = Doctrine(
|
WWII_DOCTRINE = Doctrine(
|
||||||
@ -94,8 +102,10 @@ WWII_DOCTRINE = Doctrine(
|
|||||||
min_patrol_altitude=feet_to_meter(4000),
|
min_patrol_altitude=feet_to_meter(4000),
|
||||||
max_patrol_altitude=feet_to_meter(15000),
|
max_patrol_altitude=feet_to_meter(15000),
|
||||||
pattern_altitude=feet_to_meter(5000),
|
pattern_altitude=feet_to_meter(5000),
|
||||||
|
cap_duration=timedelta(minutes=30),
|
||||||
cap_min_track_length=nm_to_meter(8),
|
cap_min_track_length=nm_to_meter(8),
|
||||||
cap_max_track_length=nm_to_meter(18),
|
cap_max_track_length=nm_to_meter(18),
|
||||||
cap_min_distance_from_cp=nm_to_meter(0),
|
cap_min_distance_from_cp=nm_to_meter(0),
|
||||||
cap_max_distance_from_cp=nm_to_meter(5),
|
cap_max_distance_from_cp=nm_to_meter(5),
|
||||||
|
cas_duration=timedelta(minutes=30),
|
||||||
)
|
)
|
||||||
|
|||||||
139
gen/aircraft.py
139
gen/aircraft.py
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Dict, List, Optional, Type, Union
|
from typing import Dict, List, Optional, Type, Union
|
||||||
|
|
||||||
from dcs import helicopters
|
from dcs import helicopters
|
||||||
@ -11,7 +12,6 @@ from dcs.condition import CoalitionHasAirdrome, TimeAfter
|
|||||||
from dcs.country import Country
|
from dcs.country import Country
|
||||||
from dcs.flyingunit import FlyingUnit
|
from dcs.flyingunit import FlyingUnit
|
||||||
from dcs.helicopters import UH_1H, helicopter_map
|
from dcs.helicopters import UH_1H, helicopter_map
|
||||||
from dcs.mapping import Point
|
|
||||||
from dcs.mission import Mission, StartType
|
from dcs.mission import Mission, StartType
|
||||||
from dcs.planes import (
|
from dcs.planes import (
|
||||||
AJS37,
|
AJS37,
|
||||||
@ -81,7 +81,12 @@ from dcs.mapping import Point
|
|||||||
from theater import TheaterGroundObject
|
from theater import TheaterGroundObject
|
||||||
from theater.controlpoint import ControlPoint, ControlPointType
|
from theater.controlpoint import ControlPoint, ControlPointType
|
||||||
from .conflictgen import Conflict
|
from .conflictgen import Conflict
|
||||||
from .flights.traveltime import PackageWaypointTiming, TotEstimator
|
from .flights.flightplan import (
|
||||||
|
CasFlightPlan,
|
||||||
|
FormationFlightPlan,
|
||||||
|
PatrollingFlightPlan,
|
||||||
|
)
|
||||||
|
from .flights.traveltime import TotEstimator
|
||||||
from .naming import namegen
|
from .naming import namegen
|
||||||
from .runways import RunwayAssigner
|
from .runways import RunwayAssigner
|
||||||
|
|
||||||
@ -785,7 +790,6 @@ class AircraftConflictGenerator:
|
|||||||
for package in ato.packages:
|
for package in ato.packages:
|
||||||
if not package.flights:
|
if not package.flights:
|
||||||
continue
|
continue
|
||||||
timing = PackageWaypointTiming.for_package(package)
|
|
||||||
for flight in package.flights:
|
for flight in package.flights:
|
||||||
culled = self.game.position_culled(flight.from_cp.position)
|
culled = self.game.position_culled(flight.from_cp.position)
|
||||||
if flight.client_count == 0 and culled:
|
if flight.client_count == 0 and culled:
|
||||||
@ -795,10 +799,10 @@ class AircraftConflictGenerator:
|
|||||||
group = self.generate_planned_flight(flight.from_cp, country,
|
group = self.generate_planned_flight(flight.from_cp, country,
|
||||||
flight)
|
flight)
|
||||||
self.setup_flight_group(group, package, flight, dynamic_runways)
|
self.setup_flight_group(group, package, flight, dynamic_runways)
|
||||||
self.create_waypoints(group, package, flight, timing)
|
self.create_waypoints(group, package, flight)
|
||||||
|
|
||||||
def set_activation_time(self, flight: Flight, group: FlyingGroup,
|
def set_activation_time(self, flight: Flight, group: FlyingGroup,
|
||||||
delay: int) -> None:
|
delay: timedelta) -> None:
|
||||||
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
||||||
# mission editor. Waypoint times will be relative to the group
|
# mission editor. Waypoint times will be relative to the group
|
||||||
# activation time rather than in absolute local time. A flight delayed
|
# activation time rather than in absolute local time. A flight delayed
|
||||||
@ -808,20 +812,22 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
activation_trigger = TriggerOnce(
|
activation_trigger = TriggerOnce(
|
||||||
Event.NoEvent, f"FlightLateActivationTrigger{group.id}")
|
Event.NoEvent, f"FlightLateActivationTrigger{group.id}")
|
||||||
activation_trigger.add_condition(TimeAfter(seconds=delay))
|
activation_trigger.add_condition(
|
||||||
|
TimeAfter(seconds=int(delay.total_seconds())))
|
||||||
|
|
||||||
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
|
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
|
||||||
activation_trigger.add_action(ActivateGroup(group.id))
|
activation_trigger.add_action(ActivateGroup(group.id))
|
||||||
self.m.triggerrules.triggers.append(activation_trigger)
|
self.m.triggerrules.triggers.append(activation_trigger)
|
||||||
|
|
||||||
def set_startup_time(self, flight: Flight, group: FlyingGroup,
|
def set_startup_time(self, flight: Flight, group: FlyingGroup,
|
||||||
delay: int) -> None:
|
delay: timedelta) -> None:
|
||||||
# Uncontrolled causes the AI unit to spawn, but not begin startup.
|
# Uncontrolled causes the AI unit to spawn, but not begin startup.
|
||||||
group.uncontrolled = True
|
group.uncontrolled = True
|
||||||
|
|
||||||
activation_trigger = TriggerOnce(Event.NoEvent,
|
activation_trigger = TriggerOnce(Event.NoEvent,
|
||||||
f"FlightStartTrigger{group.id}")
|
f"FlightStartTrigger{group.id}")
|
||||||
activation_trigger.add_condition(TimeAfter(seconds=delay))
|
activation_trigger.add_condition(
|
||||||
|
TimeAfter(seconds=int(delay.total_seconds())))
|
||||||
|
|
||||||
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
|
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
|
||||||
group.add_trigger_action(StartCommand())
|
group.add_trigger_action(StartCommand())
|
||||||
@ -882,7 +888,6 @@ class AircraftConflictGenerator:
|
|||||||
at=cp.position)
|
at=cp.position)
|
||||||
group.points[0].alt = 1500
|
group.points[0].alt = 1500
|
||||||
|
|
||||||
flight.group = group
|
|
||||||
return group
|
return group
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -1012,8 +1017,8 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
self.configure_eplrs(group, flight)
|
self.configure_eplrs(group, flight)
|
||||||
|
|
||||||
def create_waypoints(self, group: FlyingGroup, package: Package,
|
def create_waypoints(
|
||||||
flight: Flight, timing: PackageWaypointTiming) -> None:
|
self, group: FlyingGroup, package: Package, flight: Flight) -> None:
|
||||||
|
|
||||||
for waypoint in flight.points:
|
for waypoint in flight.points:
|
||||||
waypoint.tot = None
|
waypoint.tot = None
|
||||||
@ -1030,7 +1035,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
for idx, point in enumerate(filtered_points):
|
for idx, point in enumerate(filtered_points):
|
||||||
PydcsWaypointBuilder.for_waypoint(
|
PydcsWaypointBuilder.for_waypoint(
|
||||||
point, group, flight, timing, self.m
|
point, group, package, flight, self.m
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
# Set here rather than when the FlightData is created so they waypoints
|
# Set here rather than when the FlightData is created so they waypoints
|
||||||
@ -1043,7 +1048,7 @@ class AircraftConflictGenerator:
|
|||||||
estimator = TotEstimator(package)
|
estimator = TotEstimator(package)
|
||||||
start_time = estimator.mission_start_time(flight)
|
start_time = estimator.mission_start_time(flight)
|
||||||
|
|
||||||
if start_time > 0:
|
if start_time.total_seconds() > 0:
|
||||||
if self.should_activate_late(flight):
|
if self.should_activate_late(flight):
|
||||||
# Late activation causes the aircraft to not be spawned until
|
# Late activation causes the aircraft to not be spawned until
|
||||||
# triggered.
|
# triggered.
|
||||||
@ -1085,12 +1090,12 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
class PydcsWaypointBuilder:
|
class PydcsWaypointBuilder:
|
||||||
def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup,
|
def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup,
|
||||||
flight: Flight, timing: PackageWaypointTiming,
|
package: Package, flight: Flight,
|
||||||
mission: Mission) -> None:
|
mission: Mission) -> None:
|
||||||
self.waypoint = waypoint
|
self.waypoint = waypoint
|
||||||
self.group = group
|
self.group = group
|
||||||
|
self.package = package
|
||||||
self.flight = flight
|
self.flight = flight
|
||||||
self.timing = timing
|
|
||||||
self.mission = mission
|
self.mission = mission
|
||||||
|
|
||||||
def build(self) -> MovingPoint:
|
def build(self) -> MovingPoint:
|
||||||
@ -1099,35 +1104,32 @@ class PydcsWaypointBuilder:
|
|||||||
|
|
||||||
waypoint.alt_type = self.waypoint.alt_type
|
waypoint.alt_type = self.waypoint.alt_type
|
||||||
waypoint.name = String(self.waypoint.name)
|
waypoint.name = String(self.waypoint.name)
|
||||||
|
tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint)
|
||||||
|
if tot is not None:
|
||||||
|
self.set_waypoint_tot(waypoint, tot)
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
def set_waypoint_tot(self, waypoint: MovingPoint, tot: int) -> None:
|
def set_waypoint_tot(self, waypoint: MovingPoint, tot: timedelta) -> None:
|
||||||
self.waypoint.tot = tot
|
self.waypoint.tot = tot
|
||||||
waypoint.ETA = tot
|
waypoint.ETA = int(tot.total_seconds())
|
||||||
waypoint.ETA_locked = True
|
waypoint.ETA_locked = True
|
||||||
waypoint.speed_locked = False
|
waypoint.speed_locked = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_waypoint(cls, waypoint: FlightWaypoint, group: FlyingGroup,
|
def for_waypoint(cls, waypoint: FlightWaypoint, group: FlyingGroup,
|
||||||
flight: Flight, timing: PackageWaypointTiming,
|
package: Package, flight: Flight,
|
||||||
mission: Mission) -> PydcsWaypointBuilder:
|
mission: Mission) -> PydcsWaypointBuilder:
|
||||||
builders = {
|
builders = {
|
||||||
FlightWaypointType.EGRESS: EgressPointBuilder,
|
|
||||||
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
|
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
|
||||||
FlightWaypointType.INGRESS_ESCORT: IngressBuilder,
|
|
||||||
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
|
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
|
||||||
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
|
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
|
||||||
FlightWaypointType.JOIN: JoinPointBuilder,
|
FlightWaypointType.JOIN: JoinPointBuilder,
|
||||||
FlightWaypointType.LANDING_POINT: LandingPointBuilder,
|
FlightWaypointType.LANDING_POINT: LandingPointBuilder,
|
||||||
FlightWaypointType.LOITER: HoldPointBuilder,
|
FlightWaypointType.LOITER: HoldPointBuilder,
|
||||||
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
|
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
|
||||||
FlightWaypointType.SPLIT: SplitPointBuilder,
|
|
||||||
FlightWaypointType.TARGET_GROUP_LOC: TargetPointBuilder,
|
|
||||||
FlightWaypointType.TARGET_POINT: TargetPointBuilder,
|
|
||||||
FlightWaypointType.TARGET_SHIP: TargetPointBuilder,
|
|
||||||
}
|
}
|
||||||
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
||||||
return builder(waypoint, group, flight, timing, mission)
|
return builder(waypoint, group, package, flight, mission)
|
||||||
|
|
||||||
|
|
||||||
class DefaultWaypointBuilder(PydcsWaypointBuilder):
|
class DefaultWaypointBuilder(PydcsWaypointBuilder):
|
||||||
@ -1141,32 +1143,35 @@ class HoldPointBuilder(PydcsWaypointBuilder):
|
|||||||
altitude=waypoint.alt,
|
altitude=waypoint.alt,
|
||||||
pattern=OrbitAction.OrbitPattern.Circle
|
pattern=OrbitAction.OrbitPattern.Circle
|
||||||
))
|
))
|
||||||
push_time = self.timing.push_time(self.flight, self.waypoint)
|
if not isinstance(self.flight.flight_plan, FormationFlightPlan):
|
||||||
|
flight_plan_type = self.flight.flight_plan.__class__.__name__
|
||||||
|
logging.error(
|
||||||
|
f"Cannot configure hold for for {self.flight} because "
|
||||||
|
f"{flight_plan_type} does not define a push time. AI will push "
|
||||||
|
"immediately and may flight unsuitable speeds."
|
||||||
|
)
|
||||||
|
return waypoint
|
||||||
|
push_time = self.flight.flight_plan.push_time
|
||||||
self.waypoint.departure_time = push_time
|
self.waypoint.departure_time = push_time
|
||||||
loiter.stop_after_time(push_time)
|
loiter.stop_after_time(int(push_time.total_seconds()))
|
||||||
waypoint.add_task(loiter)
|
waypoint.add_task(loiter)
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
class EgressPointBuilder(PydcsWaypointBuilder):
|
class CasIngressBuilder(PydcsWaypointBuilder):
|
||||||
def build(self) -> MovingPoint:
|
def build(self) -> MovingPoint:
|
||||||
waypoint = super().build()
|
waypoint = super().build()
|
||||||
self.set_waypoint_tot(waypoint, self.timing.egress)
|
if isinstance(self.flight.flight_plan, CasFlightPlan):
|
||||||
return waypoint
|
waypoint.add_task(EngageTargetsInZone(
|
||||||
|
position=self.flight.flight_plan.target,
|
||||||
|
radius=FRONTLINE_LENGTH / 2,
|
||||||
class IngressBuilder(PydcsWaypointBuilder):
|
targets=[
|
||||||
def build(self) -> MovingPoint:
|
Targets.All.GroundUnits.GroundVehicles,
|
||||||
waypoint = super().build()
|
Targets.All.GroundUnits.AirDefence.AAA,
|
||||||
self.set_waypoint_tot(waypoint, self.timing.ingress)
|
Targets.All.GroundUnits.Infantry,
|
||||||
return waypoint
|
])
|
||||||
|
)
|
||||||
|
else:
|
||||||
class CasIngressBuilder(IngressBuilder):
|
|
||||||
def build(self) -> MovingPoint:
|
|
||||||
waypoint = super().build()
|
|
||||||
cas_waypoint = self.flight.waypoint_with_type((FlightWaypointType.CAS,))
|
|
||||||
if cas_waypoint is None:
|
|
||||||
logging.error(
|
logging.error(
|
||||||
"No CAS waypoint found. Falling back to search and engage")
|
"No CAS waypoint found. Falling back to search and engage")
|
||||||
waypoint.add_task(EngageTargets(
|
waypoint.add_task(EngageTargets(
|
||||||
@ -1177,25 +1182,15 @@ class CasIngressBuilder(IngressBuilder):
|
|||||||
Targets.All.GroundUnits.Infantry,
|
Targets.All.GroundUnits.Infantry,
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
waypoint.add_task(EngageTargetsInZone(
|
|
||||||
position=cas_waypoint.position,
|
|
||||||
radius=FRONTLINE_LENGTH / 2,
|
|
||||||
targets=[
|
|
||||||
Targets.All.GroundUnits.GroundVehicles,
|
|
||||||
Targets.All.GroundUnits.AirDefence.AAA,
|
|
||||||
Targets.All.GroundUnits.Infantry,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
waypoint.add_task(OptROE(OptROE.Values.OpenFireWeaponFree))
|
waypoint.add_task(OptROE(OptROE.Values.OpenFireWeaponFree))
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
class SeadIngressBuilder(IngressBuilder):
|
class SeadIngressBuilder(PydcsWaypointBuilder):
|
||||||
def build(self) -> MovingPoint:
|
def build(self) -> MovingPoint:
|
||||||
waypoint = super().build()
|
waypoint = super().build()
|
||||||
|
|
||||||
target_group = self.waypoint.targetGroup
|
target_group = self.package.target
|
||||||
if isinstance(target_group, TheaterGroundObject):
|
if isinstance(target_group, TheaterGroundObject):
|
||||||
tgroup = self.mission.find_group(target_group.group_identifier)
|
tgroup = self.mission.find_group(target_group.group_identifier)
|
||||||
if tgroup is not None:
|
if tgroup is not None:
|
||||||
@ -1218,7 +1213,7 @@ class SeadIngressBuilder(IngressBuilder):
|
|||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
class StrikeIngressBuilder(IngressBuilder):
|
class StrikeIngressBuilder(PydcsWaypointBuilder):
|
||||||
def build(self) -> MovingPoint:
|
def build(self) -> MovingPoint:
|
||||||
if self.group.units[0].unit_type == B_17G:
|
if self.group.units[0].unit_type == B_17G:
|
||||||
return self.build_bombing()
|
return self.build_bombing()
|
||||||
@ -1265,7 +1260,6 @@ class StrikeIngressBuilder(IngressBuilder):
|
|||||||
class JoinPointBuilder(PydcsWaypointBuilder):
|
class JoinPointBuilder(PydcsWaypointBuilder):
|
||||||
def build(self) -> MovingPoint:
|
def build(self) -> MovingPoint:
|
||||||
waypoint = super().build()
|
waypoint = super().build()
|
||||||
self.set_waypoint_tot(waypoint, self.timing.join)
|
|
||||||
if self.flight.flight_type == FlightType.ESCORT:
|
if self.flight.flight_type == FlightType.ESCORT:
|
||||||
self.configure_escort_tasks(waypoint)
|
self.configure_escort_tasks(waypoint)
|
||||||
return waypoint
|
return waypoint
|
||||||
@ -1321,27 +1315,20 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
|||||||
def build(self) -> MovingPoint:
|
def build(self) -> MovingPoint:
|
||||||
waypoint = super().build()
|
waypoint = super().build()
|
||||||
|
|
||||||
|
if not isinstance(self.flight.flight_plan, PatrollingFlightPlan):
|
||||||
|
flight_plan_type = self.flight.flight_plan.__class__.__name__
|
||||||
|
logging.error(
|
||||||
|
f"Cannot create race track for {self.flight} because "
|
||||||
|
f"{flight_plan_type} does not define a patrol.")
|
||||||
|
return waypoint
|
||||||
|
|
||||||
racetrack = ControlledTask(OrbitAction(
|
racetrack = ControlledTask(OrbitAction(
|
||||||
altitude=waypoint.alt,
|
altitude=waypoint.alt,
|
||||||
pattern=OrbitAction.OrbitPattern.RaceTrack
|
pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||||
))
|
))
|
||||||
|
self.set_waypoint_tot(
|
||||||
self.set_waypoint_tot(waypoint,
|
waypoint, self.flight.flight_plan.patrol_start_time)
|
||||||
self.timing.race_track_start(self.flight))
|
racetrack.stop_after_time(
|
||||||
racetrack.stop_after_time(self.timing.race_track_end(self.flight))
|
int(self.flight.flight_plan.patrol_end_time.total_seconds()))
|
||||||
waypoint.add_task(racetrack)
|
waypoint.add_task(racetrack)
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
class SplitPointBuilder(PydcsWaypointBuilder):
|
|
||||||
def build(self) -> MovingPoint:
|
|
||||||
waypoint = super().build()
|
|
||||||
self.set_waypoint_tot(waypoint, self.timing.split)
|
|
||||||
return waypoint
|
|
||||||
|
|
||||||
|
|
||||||
class TargetPointBuilder(PydcsWaypointBuilder):
|
|
||||||
def build(self) -> MovingPoint:
|
|
||||||
waypoint = super().build()
|
|
||||||
self.set_waypoint_tot(waypoint, self.timing.target)
|
|
||||||
return waypoint
|
|
||||||
|
|||||||
62
gen/ato.py
62
gen/ato.py
@ -11,12 +11,14 @@ the single CAP flight.
|
|||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
|
|
||||||
from theater.missiontarget import MissionTarget
|
from theater.missiontarget import MissionTarget
|
||||||
from .flights.flight import Flight, FlightType
|
from .flights.flight import Flight, FlightType
|
||||||
|
from .flights.flightplan import FormationFlightPlan
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -51,11 +53,67 @@ class Package:
|
|||||||
|
|
||||||
delay: int = field(default=0)
|
delay: int = field(default=0)
|
||||||
|
|
||||||
#: Desired TOT measured in seconds from mission start.
|
#: Desired TOT as an offset from mission start.
|
||||||
time_over_target: int = field(default=0)
|
time_over_target: timedelta = field(default=timedelta())
|
||||||
|
|
||||||
waypoints: Optional[PackageWaypoints] = field(default=None)
|
waypoints: Optional[PackageWaypoints] = field(default=None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def formation_speed(self) -> Optional[int]:
|
||||||
|
"""The speed of the package when in formation.
|
||||||
|
|
||||||
|
If none of the flights in the package will join a formation, this
|
||||||
|
returns None. This is nto uncommon, since only strike-like (strike,
|
||||||
|
DEAD, anti-ship, BAI, etc.) flights and their escorts fly in formation.
|
||||||
|
Others (CAP and CAS, currently) will coordinate in target timing but
|
||||||
|
fly their own path to the target.
|
||||||
|
"""
|
||||||
|
speeds = []
|
||||||
|
for flight in self.flights:
|
||||||
|
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||||
|
speeds.append(flight.flight_plan.best_flight_formation_speed)
|
||||||
|
if not speeds:
|
||||||
|
return None
|
||||||
|
return min(speeds)
|
||||||
|
|
||||||
|
# TODO: Should depend on the type of escort.
|
||||||
|
# SEAD might be able to leave before CAP.
|
||||||
|
@property
|
||||||
|
def escort_start_time(self) -> Optional[timedelta]:
|
||||||
|
times = []
|
||||||
|
for flight in self.flights:
|
||||||
|
waypoint = flight.flight_plan.request_escort_at()
|
||||||
|
if waypoint is None:
|
||||||
|
continue
|
||||||
|
tot = flight.flight_plan.tot_for_waypoint(waypoint)
|
||||||
|
if tot is None:
|
||||||
|
logging.error(
|
||||||
|
f"{flight} requested escort at {waypoint} but that "
|
||||||
|
"waypoint has no TOT. It may not be escorted.")
|
||||||
|
continue
|
||||||
|
times.append(tot)
|
||||||
|
if times:
|
||||||
|
return min(times)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def escort_end_time(self) -> Optional[timedelta]:
|
||||||
|
times = []
|
||||||
|
for flight in self.flights:
|
||||||
|
waypoint = flight.flight_plan.dismiss_escort_at()
|
||||||
|
if waypoint is None:
|
||||||
|
continue
|
||||||
|
tot = flight.flight_plan.tot_for_waypoint(waypoint)
|
||||||
|
if tot is None:
|
||||||
|
logging.error(
|
||||||
|
f"{flight} dismissed escort at {waypoint} but that "
|
||||||
|
"waypoint has no TOT. It may not be escorted.")
|
||||||
|
continue
|
||||||
|
times.append(tot)
|
||||||
|
if times:
|
||||||
|
return max(times)
|
||||||
|
return None
|
||||||
|
|
||||||
def add_flight(self, flight: Flight) -> None:
|
def add_flight(self, flight: Flight) -> None:
|
||||||
"""Adds a flight to the package."""
|
"""Adds a flight to the package."""
|
||||||
self.flights.append(flight)
|
self.flights.append(flight)
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import random
|
|
||||||
import operator
|
import operator
|
||||||
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple, Type
|
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple, Type
|
||||||
|
|
||||||
from dcs.unittype import FlyingType, UnitType
|
from dcs.unittype import FlyingType, UnitType
|
||||||
@ -12,7 +13,7 @@ from game import db
|
|||||||
from game.data.radar_db import UNITS_WITH_RADAR
|
from game.data.radar_db import UNITS_WITH_RADAR
|
||||||
from game.infos.information import Information
|
from game.infos.information import Information
|
||||||
from game.utils import nm_to_meter
|
from game.utils import nm_to_meter
|
||||||
from gen import Conflict, PackageWaypointTiming
|
from gen import Conflict
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
from gen.flights.ai_flight_planner_db import (
|
from gen.flights.ai_flight_planner_db import (
|
||||||
CAP_CAPABLE,
|
CAP_CAPABLE,
|
||||||
@ -483,11 +484,11 @@ class CoalitionMissionPlanner:
|
|||||||
|
|
||||||
def stagger_missions(self) -> None:
|
def stagger_missions(self) -> None:
|
||||||
def start_time_generator(count: int, earliest: int, latest: int,
|
def start_time_generator(count: int, earliest: int, latest: int,
|
||||||
margin: int) -> Iterator[int]:
|
margin: int) -> Iterator[timedelta]:
|
||||||
interval = latest // count
|
interval = latest // count
|
||||||
for time in range(earliest, latest, interval):
|
for time in range(earliest, latest, interval):
|
||||||
error = random.randint(-margin, margin)
|
error = random.randint(-margin, margin)
|
||||||
yield max(0, time + error)
|
yield timedelta(minutes=max(0, time + error))
|
||||||
|
|
||||||
dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION)
|
dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION)
|
||||||
|
|
||||||
@ -512,7 +513,7 @@ class CoalitionMissionPlanner:
|
|||||||
# airfields to hit grounded aircraft, since they're more likely
|
# airfields to hit grounded aircraft, since they're more likely
|
||||||
# to be present. Runway and air started aircraft will be
|
# to be present. Runway and air started aircraft will be
|
||||||
# delayed until their takeoff time by AirConflictGenerator.
|
# delayed until their takeoff time by AirConflictGenerator.
|
||||||
package.time_over_target = next(start_time) * 60 + tot
|
package.time_over_target = next(start_time) + tot
|
||||||
|
|
||||||
def message(self, title, text) -> None:
|
def message(self, title, text) -> None:
|
||||||
"""Emits a planning message to the player.
|
"""Emits a planning message to the player.
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, Iterable, List, Optional, TYPE_CHECKING
|
from typing import Dict, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.point import MovingPoint, PointAction
|
from dcs.point import MovingPoint, PointAction
|
||||||
@ -12,6 +13,7 @@ from theater.controlpoint import ControlPoint, MissionTarget
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
|
from gen.flights.flightplan import FlightPlan
|
||||||
|
|
||||||
|
|
||||||
class FlightType(Enum):
|
class FlightType(Enum):
|
||||||
@ -60,17 +62,6 @@ class FlightWaypointType(Enum):
|
|||||||
INGRESS_ESCORT = 19
|
INGRESS_ESCORT = 19
|
||||||
|
|
||||||
|
|
||||||
class PredefinedWaypointCategory(Enum):
|
|
||||||
NOT_PREDEFINED = 0
|
|
||||||
ALLY_CP = 1
|
|
||||||
ENEMY_CP = 2
|
|
||||||
FRONTLINE = 3
|
|
||||||
ENEMY_BUILDING = 4
|
|
||||||
ENEMY_UNIT = 5
|
|
||||||
ALLY_BUILDING = 6
|
|
||||||
ALLY_UNIT = 7
|
|
||||||
|
|
||||||
|
|
||||||
class FlightWaypoint:
|
class FlightWaypoint:
|
||||||
|
|
||||||
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
|
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
|
||||||
@ -92,19 +83,16 @@ class FlightWaypoint:
|
|||||||
self.name = ""
|
self.name = ""
|
||||||
self.description = ""
|
self.description = ""
|
||||||
self.targets: List[MissionTarget] = []
|
self.targets: List[MissionTarget] = []
|
||||||
self.targetGroup: Optional[MissionTarget] = None
|
|
||||||
self.obj_name = ""
|
self.obj_name = ""
|
||||||
self.pretty_name = ""
|
self.pretty_name = ""
|
||||||
self.category: PredefinedWaypointCategory = PredefinedWaypointCategory.NOT_PREDEFINED
|
|
||||||
self.only_for_player = False
|
self.only_for_player = False
|
||||||
self.data = None
|
|
||||||
|
|
||||||
# These are set very late by the air conflict generator (part of mission
|
# These are set very late by the air conflict generator (part of mission
|
||||||
# generation). We do it late so that we don't need to propagate changes
|
# generation). We do it late so that we don't need to propagate changes
|
||||||
# to waypoint times whenever the player alters the package TOT or the
|
# to waypoint times whenever the player alters the package TOT or the
|
||||||
# flight's offset in the UI.
|
# flight's offset in the UI.
|
||||||
self.tot: Optional[int] = None
|
self.tot: Optional[timedelta] = None
|
||||||
self.departure_time: Optional[int] = None
|
self.departure_time: Optional[timedelta] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def position(self) -> Point:
|
def position(self) -> Point:
|
||||||
@ -138,11 +126,6 @@ class FlightWaypoint:
|
|||||||
|
|
||||||
|
|
||||||
class Flight:
|
class Flight:
|
||||||
count: int = 0
|
|
||||||
client_count: int = 0
|
|
||||||
use_custom_loadout = False
|
|
||||||
preset_loadout_name = ""
|
|
||||||
group = False # Contains DCS Mission group data after mission has been generated
|
|
||||||
|
|
||||||
def __init__(self, package: Package, unit_type: UnitType, count: int,
|
def __init__(self, package: Package, unit_type: UnitType, count: int,
|
||||||
from_cp: ControlPoint, flight_type: FlightType,
|
from_cp: ControlPoint, flight_type: FlightType,
|
||||||
@ -152,7 +135,7 @@ class Flight:
|
|||||||
self.count = count
|
self.count = count
|
||||||
self.from_cp = from_cp
|
self.from_cp = from_cp
|
||||||
self.flight_type = flight_type
|
self.flight_type = flight_type
|
||||||
self.points: List[FlightWaypoint] = []
|
# TODO: Replace with FlightPlan.
|
||||||
self.targets: List[MissionTarget] = []
|
self.targets: List[MissionTarget] = []
|
||||||
self.loadout: Dict[str, str] = {}
|
self.loadout: Dict[str, str] = {}
|
||||||
self.start_type = start_type
|
self.start_type = start_type
|
||||||
@ -161,15 +144,23 @@ class Flight:
|
|||||||
# mission's TOT and the other flights in the package. Takeoff time is
|
# mission's TOT and the other flights in the package. Takeoff time is
|
||||||
# determined by AirConflictGenerator.
|
# determined by AirConflictGenerator.
|
||||||
self.scheduled_in = 0
|
self.scheduled_in = 0
|
||||||
|
self.use_custom_loadout = False
|
||||||
|
self.client_count = 0
|
||||||
|
|
||||||
|
# Will be replaced with a more appropriate FlightPlan by
|
||||||
|
# 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=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def points(self) -> List[FlightWaypoint]:
|
||||||
|
return self.flight_plan.waypoints[1:]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
|
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
|
||||||
+ " (" + str(len(self.points)) + " wpt)"
|
+ " (" + str(len(self.points)) + " wpt)"
|
||||||
|
|
||||||
def waypoint_with_type(
|
|
||||||
self,
|
|
||||||
types: Iterable[FlightWaypointType]) -> Optional[FlightWaypoint]:
|
|
||||||
for waypoint in self.points:
|
|
||||||
if waypoint.waypoint_type in types:
|
|
||||||
return waypoint
|
|
||||||
return None
|
|
||||||
|
|||||||
@ -7,27 +7,43 @@ generating the waypoints for the mission.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from functools import cached_property
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from typing import List, Optional, TYPE_CHECKING
|
from dataclasses import dataclass
|
||||||
|
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.unit import Unit
|
from dcs.unit import Unit
|
||||||
|
|
||||||
from game.data.doctrine import Doctrine, MODERN_DOCTRINE
|
from game.data.doctrine import Doctrine
|
||||||
from game.utils import nm_to_meter
|
from game.utils import nm_to_meter
|
||||||
from gen.ato import Package, PackageWaypoints
|
|
||||||
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
|
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
|
||||||
from .closestairfields import ObjectiveDistanceCache
|
from .closestairfields import ObjectiveDistanceCache
|
||||||
from .flight import Flight, FlightType, FlightWaypoint
|
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||||
from .waypointbuilder import WaypointBuilder
|
from .traveltime import GroundSpeed, TravelTime
|
||||||
|
from .waypointbuilder import StrikeTarget, WaypointBuilder
|
||||||
from ..conflictgen import Conflict
|
from ..conflictgen import Conflict
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
|
from gen.ato import Package
|
||||||
|
|
||||||
|
|
||||||
class InvalidObjectiveLocation(RuntimeError):
|
INGRESS_TYPES = {
|
||||||
|
FlightWaypointType.INGRESS_CAS,
|
||||||
|
FlightWaypointType.INGRESS_ESCORT,
|
||||||
|
FlightWaypointType.INGRESS_SEAD,
|
||||||
|
FlightWaypointType.INGRESS_STRIKE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PlanningError(RuntimeError):
|
||||||
|
"""Raised when the flight planner was unable to create a flight plan."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidObjectiveLocation(PlanningError):
|
||||||
"""Raised when the objective location is invalid for the mission type."""
|
"""Raised when the objective location is invalid for the mission type."""
|
||||||
def __init__(self, task: FlightType, location: MissionTarget) -> None:
|
def __init__(self, task: FlightType, location: MissionTarget) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@ -35,10 +51,473 @@ class InvalidObjectiveLocation(RuntimeError):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PackageWaypointTiming:
|
||||||
|
#: The package being scheduled.
|
||||||
|
package: Package
|
||||||
|
|
||||||
|
#: The package join time.
|
||||||
|
join: timedelta
|
||||||
|
|
||||||
|
#: The ingress waypoint TOT.
|
||||||
|
ingress: timedelta
|
||||||
|
|
||||||
|
#: The egress waypoint TOT.
|
||||||
|
egress: timedelta
|
||||||
|
|
||||||
|
#: The package split time.
|
||||||
|
split: timedelta
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target(self) -> timedelta:
|
||||||
|
"""The package time over target."""
|
||||||
|
return self.package.time_over_target
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_package(cls, package: Package) -> Optional[PackageWaypointTiming]:
|
||||||
|
"""Computes and returns the timings for package formation waypoints.
|
||||||
|
|
||||||
|
Package waypoint timing depends on the composition of the package.
|
||||||
|
Whenever flights are added or removed they must be recomputed since the
|
||||||
|
mission speed may have changed.
|
||||||
|
|
||||||
|
If the package contains no flights with formation flight plans, this
|
||||||
|
returns None.
|
||||||
|
"""
|
||||||
|
assert package.waypoints is not None
|
||||||
|
|
||||||
|
if not package.flights:
|
||||||
|
raise ValueError("Cannot plan TOT for package with no flights")
|
||||||
|
|
||||||
|
group_ground_speed = package.formation_speed
|
||||||
|
if group_ground_speed is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ingress = package.time_over_target - TravelTime.between_points(
|
||||||
|
package.waypoints.ingress,
|
||||||
|
package.target.position,
|
||||||
|
group_ground_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
join = ingress - TravelTime.between_points(
|
||||||
|
package.waypoints.join,
|
||||||
|
package.waypoints.ingress,
|
||||||
|
group_ground_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
egress = package.time_over_target + TravelTime.between_points(
|
||||||
|
package.target.position,
|
||||||
|
package.waypoints.egress,
|
||||||
|
group_ground_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
split = egress + TravelTime.between_points(
|
||||||
|
package.waypoints.egress,
|
||||||
|
package.waypoints.split,
|
||||||
|
group_ground_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls(package, join, ingress, egress, split)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FlightPlan:
|
||||||
|
package: Package
|
||||||
|
flight: Flight
|
||||||
|
|
||||||
|
@property
|
||||||
|
def waypoints(self) -> List[FlightWaypoint]:
|
||||||
|
"""A list of all waypoints in the flight plan, in order."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def edges(self) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]:
|
||||||
|
"""A list of all paths between waypoints, in order."""
|
||||||
|
return zip(self.waypoints, self.waypoints[1:])
|
||||||
|
|
||||||
|
def best_speed_between_waypoints(self, a: FlightWaypoint,
|
||||||
|
b: FlightWaypoint) -> int:
|
||||||
|
"""Desired ground speed between points a and b."""
|
||||||
|
factor = 1.0
|
||||||
|
if b.waypoint_type == FlightWaypointType.ASCEND_POINT:
|
||||||
|
# Flights that start airborne already have some altitude and a good
|
||||||
|
# amount of speed.
|
||||||
|
factor = 0.5
|
||||||
|
# TODO: Adjust if AGL.
|
||||||
|
# We don't have an exact heightmap, but we should probably be performing
|
||||||
|
# *some* adjustment for NTTR since the minimum altitude of the map is
|
||||||
|
# near 2000 ft MSL.
|
||||||
|
return int(
|
||||||
|
GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor)
|
||||||
|
|
||||||
|
def speed_between_waypoints(self, a: FlightWaypoint,
|
||||||
|
b: FlightWaypoint) -> int:
|
||||||
|
return self.best_speed_between_waypoints(a, b)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tot_waypoint(self) -> Optional[FlightWaypoint]:
|
||||||
|
"""The waypoint that is associated with the package TOT, or None.
|
||||||
|
|
||||||
|
Note that the only flight plans that should have no target waypoints are
|
||||||
|
user-planned missions without any useful waypoints and flight plans that
|
||||||
|
failed to generate. Nevertheless, we have to defend against it.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# Not cached because changes to the package might alter the formation speed.
|
||||||
|
@property
|
||||||
|
def travel_time_to_target(self) -> Optional[timedelta]:
|
||||||
|
"""The estimated time between the first waypoint and the target."""
|
||||||
|
if self.tot_waypoint is None:
|
||||||
|
return None
|
||||||
|
return self._travel_time_to_waypoint(self.tot_waypoint)
|
||||||
|
|
||||||
|
def _travel_time_to_waypoint(
|
||||||
|
self, destination: FlightWaypoint) -> timedelta:
|
||||||
|
total = timedelta()
|
||||||
|
for previous_waypoint, waypoint in self.edges:
|
||||||
|
total += TravelTime.between_points(
|
||||||
|
previous_waypoint.position, waypoint.position,
|
||||||
|
self.speed_between_waypoints(previous_waypoint, waypoint))
|
||||||
|
if waypoint == destination:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise PlanningError(
|
||||||
|
f"Did not find destination waypoint {destination} in "
|
||||||
|
f"waypoints for {self.flight}")
|
||||||
|
return total
|
||||||
|
|
||||||
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def depart_time_for_waypoint(
|
||||||
|
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def request_escort_at(self) -> Optional[FlightWaypoint]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FormationFlightPlan(FlightPlan):
|
||||||
|
hold: FlightWaypoint
|
||||||
|
join: FlightWaypoint
|
||||||
|
split: FlightWaypoint
|
||||||
|
|
||||||
|
@property
|
||||||
|
def package_timing(self) -> PackageWaypointTiming:
|
||||||
|
timing = PackageWaypointTiming.for_package(self.package)
|
||||||
|
# Should be able to create a PackageWaypointTiming for any package with
|
||||||
|
# a FormationFlightPlan flight.
|
||||||
|
assert timing is not None
|
||||||
|
return timing
|
||||||
|
|
||||||
|
@property
|
||||||
|
def waypoints(self) -> List[FlightWaypoint]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def package_speed_waypoints(self) -> Set[FlightWaypoint]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tot_waypoint(self) -> Optional[FlightWaypoint]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def request_escort_at(self) -> Optional[FlightWaypoint]:
|
||||||
|
return self.join
|
||||||
|
|
||||||
|
def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
|
||||||
|
return self.split
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def best_flight_formation_speed(self) -> int:
|
||||||
|
"""The best speed this flight is capable at all formation waypoints.
|
||||||
|
|
||||||
|
To ease coordination with other flights, we aim to have a single mission
|
||||||
|
speed used by the formation for all waypoints. As such, this function
|
||||||
|
returns the highest ground speed that the flight is capable of flying at
|
||||||
|
all of its formation waypoints.
|
||||||
|
"""
|
||||||
|
speeds = []
|
||||||
|
for previous_waypoint, waypoint in self.edges:
|
||||||
|
if waypoint in self.package_speed_waypoints:
|
||||||
|
speeds.append(self.best_speed_between_waypoints(
|
||||||
|
previous_waypoint, waypoint))
|
||||||
|
return min(speeds)
|
||||||
|
|
||||||
|
def speed_between_waypoints(self, a: FlightWaypoint,
|
||||||
|
b: FlightWaypoint) -> int:
|
||||||
|
if b in self.package_speed_waypoints:
|
||||||
|
# Should be impossible, as any package with at least one
|
||||||
|
# FormationFlightPlan flight needs a formation speed.
|
||||||
|
assert self.package.formation_speed is not None
|
||||||
|
return self.package.formation_speed
|
||||||
|
return super().speed_between_waypoints(a, b)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def travel_time_to_rendezvous(self) -> timedelta:
|
||||||
|
"""The estimated time between the first waypoint and the join point."""
|
||||||
|
return self._travel_time_to_waypoint(self.join)
|
||||||
|
|
||||||
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||||
|
target_types = (
|
||||||
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
|
FlightWaypointType.TARGET_POINT,
|
||||||
|
FlightWaypointType.TARGET_SHIP,
|
||||||
|
)
|
||||||
|
|
||||||
|
if waypoint.waypoint_type == FlightWaypointType.JOIN:
|
||||||
|
return self.package_timing.join
|
||||||
|
elif waypoint.waypoint_type in INGRESS_TYPES:
|
||||||
|
return self.package_timing.ingress
|
||||||
|
elif waypoint.waypoint_type in target_types:
|
||||||
|
return self.package_timing.target
|
||||||
|
elif waypoint.waypoint_type == FlightWaypointType.EGRESS:
|
||||||
|
return self.package_timing.egress
|
||||||
|
elif waypoint.waypoint_type == FlightWaypointType.SPLIT:
|
||||||
|
return self.package_timing.split
|
||||||
|
return None
|
||||||
|
|
||||||
|
def depart_time_for_waypoint(
|
||||||
|
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||||
|
if waypoint == self.hold:
|
||||||
|
return self.push_time
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def push_time(self) -> timedelta:
|
||||||
|
return self.package_timing.join - TravelTime.between_points(
|
||||||
|
self.hold.position,
|
||||||
|
self.join.position,
|
||||||
|
GroundSpeed.for_flight(self.flight, self.hold.alt)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PatrollingFlightPlan(FlightPlan):
|
||||||
|
patrol_start: FlightWaypoint
|
||||||
|
patrol_end: FlightWaypoint
|
||||||
|
|
||||||
|
#: Maximum time to remain on station.
|
||||||
|
patrol_duration: timedelta
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patrol_start_time(self) -> timedelta:
|
||||||
|
return self.package.time_over_target
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patrol_end_time(self) -> timedelta:
|
||||||
|
# TODO: This is currently wrong for CAS.
|
||||||
|
# CAS missions end when they're winchester or bingo. We need to
|
||||||
|
# configure push tasks for the escorts rather than relying on timing.
|
||||||
|
return self.patrol_start_time + self.patrol_duration
|
||||||
|
|
||||||
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||||
|
if waypoint == self.patrol_start:
|
||||||
|
return self.patrol_start_time
|
||||||
|
return None
|
||||||
|
|
||||||
|
def depart_time_for_waypoint(
|
||||||
|
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||||
|
if waypoint == self.patrol_end:
|
||||||
|
return self.patrol_end_time
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def waypoints(self) -> List[FlightWaypoint]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def package_speed_waypoints(self) -> Set[FlightWaypoint]:
|
||||||
|
return {self.patrol_start, self.patrol_end}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tot_waypoint(self) -> Optional[FlightWaypoint]:
|
||||||
|
return self.patrol_start
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BarCapFlightPlan(PatrollingFlightPlan):
|
||||||
|
takeoff: FlightWaypoint
|
||||||
|
ascent: FlightWaypoint
|
||||||
|
descent: FlightWaypoint
|
||||||
|
land: FlightWaypoint
|
||||||
|
|
||||||
|
@property
|
||||||
|
def waypoints(self) -> List[FlightWaypoint]:
|
||||||
|
return [
|
||||||
|
self.takeoff,
|
||||||
|
self.ascent,
|
||||||
|
self.patrol_start,
|
||||||
|
self.patrol_end,
|
||||||
|
self.descent,
|
||||||
|
self.land,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CasFlightPlan(PatrollingFlightPlan):
|
||||||
|
takeoff: FlightWaypoint
|
||||||
|
ascent: FlightWaypoint
|
||||||
|
target: FlightWaypoint
|
||||||
|
descent: FlightWaypoint
|
||||||
|
land: FlightWaypoint
|
||||||
|
|
||||||
|
@property
|
||||||
|
def waypoints(self) -> List[FlightWaypoint]:
|
||||||
|
return [
|
||||||
|
self.takeoff,
|
||||||
|
self.ascent,
|
||||||
|
self.patrol_start,
|
||||||
|
self.target,
|
||||||
|
self.patrol_end,
|
||||||
|
self.descent,
|
||||||
|
self.land,
|
||||||
|
]
|
||||||
|
|
||||||
|
def request_escort_at(self) -> Optional[FlightWaypoint]:
|
||||||
|
return self.patrol_start
|
||||||
|
|
||||||
|
def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
|
||||||
|
return self.patrol_end
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FrontLineCapFlightPlan(PatrollingFlightPlan):
|
||||||
|
takeoff: FlightWaypoint
|
||||||
|
ascent: FlightWaypoint
|
||||||
|
descent: FlightWaypoint
|
||||||
|
land: FlightWaypoint
|
||||||
|
|
||||||
|
@property
|
||||||
|
def waypoints(self) -> List[FlightWaypoint]:
|
||||||
|
return [
|
||||||
|
self.takeoff,
|
||||||
|
self.ascent,
|
||||||
|
self.patrol_start,
|
||||||
|
self.patrol_end,
|
||||||
|
self.descent,
|
||||||
|
self.land,
|
||||||
|
]
|
||||||
|
|
||||||
|
def depart_time_for_waypoint(
|
||||||
|
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||||
|
if waypoint == self.patrol_end:
|
||||||
|
timing = PackageWaypointTiming.for_package(self.package)
|
||||||
|
if timing is None:
|
||||||
|
# If the player for some reason planned a TAR
|
||||||
|
patrol_duration = self.patrol_duration
|
||||||
|
else:
|
||||||
|
patrol_duration = timing.egress
|
||||||
|
return self.package.time_over_target + patrol_duration
|
||||||
|
return super().depart_time_for_waypoint(waypoint)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patrol_start_time(self) -> timedelta:
|
||||||
|
start = self.package.escort_start_time
|
||||||
|
if start is not None:
|
||||||
|
return start
|
||||||
|
return super().patrol_start_time
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patrol_end_time(self) -> timedelta:
|
||||||
|
end = self.package.escort_end_time
|
||||||
|
if end is not None:
|
||||||
|
return end
|
||||||
|
return super().patrol_end_time
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class StrikeFlightPlan(FormationFlightPlan):
|
||||||
|
takeoff: FlightWaypoint
|
||||||
|
ascent: FlightWaypoint
|
||||||
|
hold: FlightWaypoint
|
||||||
|
join: FlightWaypoint
|
||||||
|
ingress: FlightWaypoint
|
||||||
|
targets: List[FlightWaypoint]
|
||||||
|
egress: FlightWaypoint
|
||||||
|
split: FlightWaypoint
|
||||||
|
descent: FlightWaypoint
|
||||||
|
land: FlightWaypoint
|
||||||
|
|
||||||
|
@property
|
||||||
|
def waypoints(self) -> List[FlightWaypoint]:
|
||||||
|
return [
|
||||||
|
self.takeoff,
|
||||||
|
self.ascent,
|
||||||
|
self.hold,
|
||||||
|
self.join,
|
||||||
|
self.ingress
|
||||||
|
] + self.targets + [
|
||||||
|
self.egress,
|
||||||
|
self.split,
|
||||||
|
self.descent,
|
||||||
|
self.land,
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def package_speed_waypoints(self) -> Set[FlightWaypoint]:
|
||||||
|
return {
|
||||||
|
self.ingress,
|
||||||
|
self.egress,
|
||||||
|
self.split,
|
||||||
|
} | set(self.targets)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tot_waypoint(self) -> Optional[FlightWaypoint]:
|
||||||
|
return self.targets[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mission_speed(self) -> int:
|
||||||
|
return GroundSpeed.for_flight(self.flight, self.ingress.alt)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CustomFlightPlan(FlightPlan):
|
||||||
|
custom_waypoints: List[FlightWaypoint]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def waypoints(self) -> List[FlightWaypoint]:
|
||||||
|
return self.custom_waypoints
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tot_waypoint(self) -> Optional[FlightWaypoint]:
|
||||||
|
target_types = (
|
||||||
|
FlightWaypointType.PATROL_TRACK,
|
||||||
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
|
FlightWaypointType.TARGET_POINT,
|
||||||
|
FlightWaypointType.TARGET_SHIP,
|
||||||
|
)
|
||||||
|
for waypoint in self.waypoints:
|
||||||
|
if waypoint in target_types:
|
||||||
|
return waypoint
|
||||||
|
return None
|
||||||
|
|
||||||
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||||
|
if waypoint == self.tot_waypoint:
|
||||||
|
return self.package.time_over_target
|
||||||
|
return None
|
||||||
|
|
||||||
|
def depart_time_for_waypoint(
|
||||||
|
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class FlightPlanBuilder:
|
class FlightPlanBuilder:
|
||||||
"""Generates flight plans for flights."""
|
"""Generates flight plans for flights."""
|
||||||
|
|
||||||
def __init__(self, game: Game, package: Package, is_player: bool) -> None:
|
def __init__(self, game: Game, package: Package, is_player: bool) -> None:
|
||||||
|
# TODO: Plan similar altitudes for the in-country leg of the mission.
|
||||||
|
# Waypoint altitudes for a given flight *shouldn't* differ too much
|
||||||
|
# between the join and split points, so we don't need speeds for each
|
||||||
|
# leg individually since they should all be fairly similar. This doesn't
|
||||||
|
# hold too well right now since nothing is stopping each waypoint from
|
||||||
|
# jumping 20k feet each time, but that's a huge waste of energy we
|
||||||
|
# should be avoiding anyway.
|
||||||
self.game = game
|
self.game = game
|
||||||
self.package = package
|
self.package = package
|
||||||
self.is_player = is_player
|
self.is_player = is_player
|
||||||
@ -58,53 +537,38 @@ class FlightPlanBuilder:
|
|||||||
if self.package.waypoints is None:
|
if self.package.waypoints is None:
|
||||||
self.regenerate_package_waypoints()
|
self.regenerate_package_waypoints()
|
||||||
|
|
||||||
# TODO: Flesh out mission types.
|
|
||||||
try:
|
try:
|
||||||
|
flight_plan = self.generate_flight_plan(flight, custom_targets)
|
||||||
|
except PlanningError:
|
||||||
|
logging.exception(f"Could not create flight plan")
|
||||||
|
return
|
||||||
|
flight.flight_plan = flight_plan
|
||||||
|
|
||||||
|
def generate_flight_plan(
|
||||||
|
self, flight: Flight,
|
||||||
|
custom_targets: Optional[List[Unit]]) -> FlightPlan:
|
||||||
|
# TODO: Flesh out mission types.
|
||||||
task = flight.flight_type
|
task = flight.flight_type
|
||||||
if task == FlightType.ANTISHIP:
|
if task == FlightType.BARCAP:
|
||||||
logging.error(
|
return self.generate_barcap(flight)
|
||||||
"Anti-ship flight plan generation not implemented"
|
|
||||||
)
|
|
||||||
elif task == FlightType.BAI:
|
|
||||||
logging.error("BAI flight plan generation not implemented")
|
|
||||||
elif task == FlightType.BARCAP:
|
|
||||||
self.generate_barcap(flight)
|
|
||||||
elif task == FlightType.CAS:
|
elif task == FlightType.CAS:
|
||||||
self.generate_cas(flight)
|
return self.generate_cas(flight)
|
||||||
elif task == FlightType.DEAD:
|
elif task == FlightType.DEAD:
|
||||||
self.generate_sead(flight, custom_targets)
|
return self.generate_sead(flight, custom_targets)
|
||||||
elif task == FlightType.ELINT:
|
|
||||||
logging.error("ELINT flight plan generation not implemented")
|
|
||||||
elif task == FlightType.ESCORT:
|
elif task == FlightType.ESCORT:
|
||||||
self.generate_escort(flight)
|
return self.generate_escort(flight)
|
||||||
elif task == FlightType.EVAC:
|
|
||||||
logging.error("Evac flight plan generation not implemented")
|
|
||||||
elif task == FlightType.EWAR:
|
|
||||||
logging.error("EWar flight plan generation not implemented")
|
|
||||||
elif task == FlightType.INTERCEPTION:
|
|
||||||
logging.error(
|
|
||||||
"Intercept flight plan generation not implemented"
|
|
||||||
)
|
|
||||||
elif task == FlightType.LOGISTICS:
|
|
||||||
logging.error(
|
|
||||||
"Logistics flight plan generation not implemented"
|
|
||||||
)
|
|
||||||
elif task == FlightType.RECON:
|
|
||||||
logging.error("Recon flight plan generation not implemented")
|
|
||||||
elif task == FlightType.SEAD:
|
elif task == FlightType.SEAD:
|
||||||
self.generate_sead(flight, custom_targets)
|
return self.generate_sead(flight, custom_targets)
|
||||||
elif task == FlightType.STRIKE:
|
elif task == FlightType.STRIKE:
|
||||||
self.generate_strike(flight)
|
return self.generate_strike(flight)
|
||||||
elif task == FlightType.TARCAP:
|
elif task == FlightType.TARCAP:
|
||||||
self.generate_frontline_cap(flight)
|
return self.generate_frontline_cap(flight)
|
||||||
elif task == FlightType.TROOP_TRANSPORT:
|
elif task == FlightType.TROOP_TRANSPORT:
|
||||||
logging.error(
|
logging.error(
|
||||||
"Troop transport flight plan generation not implemented"
|
"Troop transport flight plan generation not implemented"
|
||||||
)
|
)
|
||||||
else:
|
raise PlanningError(
|
||||||
logging.error(f"Unsupported task type: {task.name}")
|
f"{task.name} flight plan generation not implemented")
|
||||||
except InvalidObjectiveLocation:
|
|
||||||
logging.exception(f"Could not create flight plan")
|
|
||||||
|
|
||||||
def regenerate_package_waypoints(self) -> None:
|
def regenerate_package_waypoints(self) -> None:
|
||||||
ingress_point = self._ingress_point()
|
ingress_point = self._ingress_point()
|
||||||
@ -112,6 +576,7 @@ class FlightPlanBuilder:
|
|||||||
join_point = self._join_point(ingress_point)
|
join_point = self._join_point(ingress_point)
|
||||||
split_point = self._split_point(egress_point)
|
split_point = self._split_point(egress_point)
|
||||||
|
|
||||||
|
from gen.ato import PackageWaypoints
|
||||||
self.package.waypoints = PackageWaypoints(
|
self.package.waypoints = PackageWaypoints(
|
||||||
join_point,
|
join_point,
|
||||||
ingress_point,
|
ingress_point,
|
||||||
@ -119,31 +584,25 @@ class FlightPlanBuilder:
|
|||||||
split_point,
|
split_point,
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_strike(self, flight: Flight) -> None:
|
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
|
||||||
"""Generates a strike flight plan.
|
"""Generates a strike flight plan.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
flight: The flight to generate the flight plan for.
|
flight: The flight to generate the flight plan for.
|
||||||
"""
|
"""
|
||||||
assert self.package.waypoints is not None
|
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
# TODO: Support airfield strikes.
|
# TODO: Support airfield strikes.
|
||||||
if not isinstance(location, TheaterGroundObject):
|
if not isinstance(location, TheaterGroundObject):
|
||||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||||
|
|
||||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
targets: List[StrikeTarget] = []
|
||||||
builder.ascent(flight.from_cp)
|
|
||||||
builder.hold(self._hold_point(flight))
|
|
||||||
builder.join(self.package.waypoints.join)
|
|
||||||
builder.ingress_strike(self.package.waypoints.ingress, location)
|
|
||||||
|
|
||||||
if len(location.groups) > 0 and location.dcs_identifier == "AA":
|
if len(location.groups) > 0 and location.dcs_identifier == "AA":
|
||||||
# TODO: Replace with DEAD?
|
# TODO: Replace with DEAD?
|
||||||
# Strike missions on SEAD targets target units.
|
# Strike missions on SEAD targets target units.
|
||||||
for g in location.groups:
|
for g in location.groups:
|
||||||
for j, u in enumerate(g.units):
|
for j, u in enumerate(g.units):
|
||||||
builder.strike_point(u, f"{u.type} #{j}", location)
|
targets.append(StrikeTarget(f"{u.type} #{j}", u))
|
||||||
else:
|
else:
|
||||||
# TODO: Does this actually happen?
|
# TODO: Does this actually happen?
|
||||||
# ConflictTheater is built with the belief that multiple ground
|
# ConflictTheater is built with the belief that multiple ground
|
||||||
@ -157,15 +616,11 @@ class FlightPlanBuilder:
|
|||||||
if building.is_dead:
|
if building.is_dead:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
builder.strike_point(building, building.category, location)
|
targets.append(StrikeTarget(building.category, building))
|
||||||
|
|
||||||
builder.egress(self.package.waypoints.egress, location)
|
return self.strike_flightplan(flight, location, targets)
|
||||||
builder.split(self.package.waypoints.split)
|
|
||||||
builder.rtb(flight.from_cp)
|
|
||||||
|
|
||||||
flight.points = builder.build()
|
def generate_barcap(self, flight: Flight) -> BarCapFlightPlan:
|
||||||
|
|
||||||
def generate_barcap(self, flight: Flight) -> None:
|
|
||||||
"""Generate a BARCAP flight at a given location.
|
"""Generate a BARCAP flight at a given location.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -191,8 +646,7 @@ class FlightPlanBuilder:
|
|||||||
closest_airfield = airfield
|
closest_airfield = airfield
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
logging.error("Could not find any enemy airfields")
|
raise PlanningError("Could not find any enemy airfields")
|
||||||
return
|
|
||||||
|
|
||||||
heading = location.position.heading_between_point(
|
heading = location.position.heading_between_point(
|
||||||
closest_airfield.position
|
closest_airfield.position
|
||||||
@ -219,18 +673,27 @@ class FlightPlanBuilder:
|
|||||||
start = end.point_from_heading(heading - 180, diameter)
|
start = end.point_from_heading(heading - 180, diameter)
|
||||||
|
|
||||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||||
builder.ascent(flight.from_cp)
|
start, end = builder.race_track(start, end, patrol_alt)
|
||||||
builder.race_track(start, end, patrol_alt)
|
descent, land = builder.rtb(flight.from_cp)
|
||||||
builder.rtb(flight.from_cp)
|
|
||||||
flight.points = builder.build()
|
|
||||||
|
|
||||||
def generate_frontline_cap(self, flight: Flight) -> None:
|
return BarCapFlightPlan(
|
||||||
|
package=self.package,
|
||||||
|
flight=flight,
|
||||||
|
patrol_duration=self.doctrine.cap_duration,
|
||||||
|
takeoff=builder.takeoff(flight.from_cp),
|
||||||
|
ascent=builder.ascent(flight.from_cp),
|
||||||
|
patrol_start=start,
|
||||||
|
patrol_end=end,
|
||||||
|
descent=descent,
|
||||||
|
land=land
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_frontline_cap(self, flight: Flight) -> FrontLineCapFlightPlan:
|
||||||
"""Generate a CAP flight plan for the given front line.
|
"""Generate a CAP flight plan for the given front line.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
flight: The flight to generate the flight plan for.
|
flight: The flight to generate the flight plan for.
|
||||||
"""
|
"""
|
||||||
assert self.package.waypoints is not None
|
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
if not isinstance(location, FrontLine):
|
if not isinstance(location, FrontLine):
|
||||||
@ -261,87 +724,79 @@ class FlightPlanBuilder:
|
|||||||
|
|
||||||
# Create points
|
# Create points
|
||||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||||
builder.ascent(flight.from_cp)
|
|
||||||
builder.hold(self._hold_point(flight))
|
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||||
builder.join(self.package.waypoints.join)
|
descent, land = builder.rtb(flight.from_cp)
|
||||||
builder.race_track(orbit0p, orbit1p, patrol_alt)
|
return FrontLineCapFlightPlan(
|
||||||
builder.split(self.package.waypoints.split)
|
package=self.package,
|
||||||
builder.rtb(flight.from_cp)
|
flight=flight,
|
||||||
flight.points = builder.build()
|
# Note that this duration only has an effect if there are no
|
||||||
|
# flights in the package that have requested escort. If the package
|
||||||
|
# requests an escort the CAP flight will remain on station for the
|
||||||
|
# duration of the escorted mission, or until it is winchester/bingo.
|
||||||
|
patrol_duration=self.doctrine.cap_duration,
|
||||||
|
takeoff=builder.takeoff(flight.from_cp),
|
||||||
|
ascent=builder.ascent(flight.from_cp),
|
||||||
|
patrol_start=start,
|
||||||
|
patrol_end=end,
|
||||||
|
descent=descent,
|
||||||
|
land=land
|
||||||
|
)
|
||||||
|
|
||||||
def generate_sead(self, flight: Flight,
|
def generate_sead(self, flight: Flight,
|
||||||
custom_targets: Optional[List[Unit]]) -> None:
|
custom_targets: Optional[List[Unit]]) -> StrikeFlightPlan:
|
||||||
"""Generate a SEAD/DEAD flight at a given location.
|
"""Generate a SEAD/DEAD 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.
|
||||||
custom_targets: Specific radar equipped units selected by the user.
|
custom_targets: Specific radar equipped units selected by the user.
|
||||||
"""
|
"""
|
||||||
assert self.package.waypoints is not None
|
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
if not isinstance(location, TheaterGroundObject):
|
if not isinstance(location, TheaterGroundObject):
|
||||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||||
|
|
||||||
if custom_targets is None:
|
|
||||||
custom_targets = []
|
|
||||||
|
|
||||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
|
||||||
builder.ascent(flight.from_cp)
|
|
||||||
builder.hold(self._hold_point(flight))
|
|
||||||
builder.join(self.package.waypoints.join)
|
|
||||||
builder.ingress_sead(self.package.waypoints.ingress, location)
|
|
||||||
|
|
||||||
# TODO: Unify these.
|
# TODO: Unify these.
|
||||||
# There doesn't seem to be any reason to treat the UI fragged missions
|
# There doesn't seem to be any reason to treat the UI fragged missions
|
||||||
# different from the automatic missions.
|
# different from the automatic missions.
|
||||||
if custom_targets:
|
targets: Optional[List[StrikeTarget]] = None
|
||||||
|
if custom_targets is not None:
|
||||||
|
targets = []
|
||||||
for target in custom_targets:
|
for target in custom_targets:
|
||||||
if flight.flight_type == FlightType.DEAD:
|
targets.append(StrikeTarget(location.name, target))
|
||||||
builder.dead_point(target, location.name, location)
|
|
||||||
else:
|
|
||||||
builder.sead_point(target, location.name, location)
|
|
||||||
else:
|
|
||||||
if flight.flight_type == FlightType.DEAD:
|
|
||||||
builder.dead_area(location)
|
|
||||||
else:
|
|
||||||
builder.sead_area(location)
|
|
||||||
|
|
||||||
builder.egress(self.package.waypoints.egress, location)
|
return self.strike_flightplan(flight, location, targets)
|
||||||
builder.split(self.package.waypoints.split)
|
|
||||||
builder.rtb(flight.from_cp)
|
|
||||||
|
|
||||||
flight.points = builder.build()
|
def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
|
||||||
|
|
||||||
def _hold_point(self, flight: Flight) -> Point:
|
|
||||||
heading = flight.from_cp.position.heading_between_point(
|
|
||||||
self.package.target.position
|
|
||||||
)
|
|
||||||
return flight.from_cp.position.point_from_heading(
|
|
||||||
heading, nm_to_meter(15)
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_escort(self, flight: Flight) -> None:
|
|
||||||
assert self.package.waypoints is not None
|
assert self.package.waypoints is not None
|
||||||
|
|
||||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||||
builder.ascent(flight.from_cp)
|
ingress, target, egress = builder.escort(
|
||||||
builder.hold(self._hold_point(flight))
|
self.package.waypoints.ingress, self.package.target,
|
||||||
builder.join(self.package.waypoints.join)
|
self.package.waypoints.egress)
|
||||||
builder.escort(self.package.waypoints.ingress,
|
descent, land = builder.rtb(flight.from_cp)
|
||||||
self.package.target, self.package.waypoints.egress)
|
|
||||||
builder.split(self.package.waypoints.split)
|
|
||||||
builder.rtb(flight.from_cp)
|
|
||||||
|
|
||||||
flight.points = builder.build()
|
return StrikeFlightPlan(
|
||||||
|
package=self.package,
|
||||||
|
flight=flight,
|
||||||
|
takeoff=builder.takeoff(flight.from_cp),
|
||||||
|
ascent=builder.ascent(flight.from_cp),
|
||||||
|
hold=builder.hold(self._hold_point(flight)),
|
||||||
|
join=builder.join(self.package.waypoints.join),
|
||||||
|
ingress=ingress,
|
||||||
|
targets=[target],
|
||||||
|
egress=egress,
|
||||||
|
split=builder.split(self.package.waypoints.split),
|
||||||
|
descent=descent,
|
||||||
|
land=land
|
||||||
|
)
|
||||||
|
|
||||||
def generate_cas(self, flight: Flight) -> None:
|
def generate_cas(self, flight: Flight) -> CasFlightPlan:
|
||||||
"""Generate a CAS flight plan for the given target.
|
"""Generate a CAS flight plan for the given target.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
flight: The flight to generate the flight plan for.
|
flight: The flight to generate the flight plan for.
|
||||||
"""
|
"""
|
||||||
assert self.package.waypoints is not None
|
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
if not isinstance(location, FrontLine):
|
if not isinstance(location, FrontLine):
|
||||||
@ -355,16 +810,48 @@ class FlightPlanBuilder:
|
|||||||
egress = ingress.point_from_heading(heading, distance)
|
egress = ingress.point_from_heading(heading, distance)
|
||||||
|
|
||||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||||
builder.ascent(flight.from_cp)
|
descent, land = builder.rtb(flight.from_cp)
|
||||||
builder.hold(self._hold_point(flight))
|
|
||||||
builder.join(self.package.waypoints.join)
|
|
||||||
builder.ingress_cas(ingress, location)
|
|
||||||
builder.cas(center)
|
|
||||||
builder.egress(egress, location)
|
|
||||||
builder.split(self.package.waypoints.split)
|
|
||||||
builder.rtb(flight.from_cp)
|
|
||||||
|
|
||||||
flight.points = builder.build()
|
return CasFlightPlan(
|
||||||
|
package=self.package,
|
||||||
|
flight=flight,
|
||||||
|
patrol_duration=self.doctrine.cas_duration,
|
||||||
|
takeoff=builder.takeoff(flight.from_cp),
|
||||||
|
ascent=builder.ascent(flight.from_cp),
|
||||||
|
patrol_start=builder.ingress_cas(ingress, location),
|
||||||
|
target=builder.cas(center),
|
||||||
|
patrol_end=builder.egress(egress, location),
|
||||||
|
descent=descent,
|
||||||
|
land=land
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def target_waypoint(flight: Flight, builder: WaypointBuilder,
|
||||||
|
target: StrikeTarget) -> FlightWaypoint:
|
||||||
|
if flight.flight_type == FlightType.DEAD:
|
||||||
|
return builder.dead_point(target)
|
||||||
|
elif flight.flight_type == FlightType.SEAD:
|
||||||
|
return builder.sead_point(target)
|
||||||
|
else:
|
||||||
|
return builder.strike_point(target)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def target_area_waypoint(flight: Flight, location: MissionTarget,
|
||||||
|
builder: WaypointBuilder) -> FlightWaypoint:
|
||||||
|
if flight.flight_type == FlightType.DEAD:
|
||||||
|
return builder.dead_area(location)
|
||||||
|
elif flight.flight_type == FlightType.SEAD:
|
||||||
|
return builder.sead_area(location)
|
||||||
|
else:
|
||||||
|
return builder.strike_area(location)
|
||||||
|
|
||||||
|
def _hold_point(self, flight: Flight) -> Point:
|
||||||
|
heading = flight.from_cp.position.heading_between_point(
|
||||||
|
self.package.target.position
|
||||||
|
)
|
||||||
|
return flight.from_cp.position.point_from_heading(
|
||||||
|
heading, nm_to_meter(15)
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: Make a model for the waypoint builder and use that in the UI.
|
# TODO: Make a model for the waypoint builder and use that in the UI.
|
||||||
def generate_ascend_point(self, flight: Flight,
|
def generate_ascend_point(self, flight: Flight,
|
||||||
@ -376,8 +863,7 @@ class FlightPlanBuilder:
|
|||||||
departure: Departure airfield or carrier.
|
departure: Departure airfield or carrier.
|
||||||
"""
|
"""
|
||||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||||
builder.ascent(departure)
|
return builder.ascent(departure)
|
||||||
return builder.build()[0]
|
|
||||||
|
|
||||||
def generate_descend_point(self, flight: Flight,
|
def generate_descend_point(self, flight: Flight,
|
||||||
arrival: ControlPoint) -> FlightWaypoint:
|
arrival: ControlPoint) -> FlightWaypoint:
|
||||||
@ -388,8 +874,7 @@ class FlightPlanBuilder:
|
|||||||
arrival: Arrival airfield or carrier.
|
arrival: Arrival airfield or carrier.
|
||||||
"""
|
"""
|
||||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||||
builder.descent(arrival)
|
return builder.descent(arrival)
|
||||||
return builder.build()[0]
|
|
||||||
|
|
||||||
def generate_rtb_waypoint(self, flight: Flight,
|
def generate_rtb_waypoint(self, flight: Flight,
|
||||||
arrival: ControlPoint) -> FlightWaypoint:
|
arrival: ControlPoint) -> FlightWaypoint:
|
||||||
@ -400,8 +885,46 @@ class FlightPlanBuilder:
|
|||||||
arrival: Arrival airfield or carrier.
|
arrival: Arrival airfield or carrier.
|
||||||
"""
|
"""
|
||||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||||
builder.land(arrival)
|
return builder.land(arrival)
|
||||||
return builder.build()[0]
|
|
||||||
|
def strike_flightplan(
|
||||||
|
self, flight: Flight, location: TheaterGroundObject,
|
||||||
|
targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan:
|
||||||
|
assert self.package.waypoints is not None
|
||||||
|
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine,
|
||||||
|
targets)
|
||||||
|
sead_types = {FlightType.DEAD, FlightType.SEAD}
|
||||||
|
if flight.flight_type in sead_types:
|
||||||
|
ingress = builder.ingress_sead(self.package.waypoints.ingress,
|
||||||
|
location)
|
||||||
|
else:
|
||||||
|
ingress = builder.ingress_strike(self.package.waypoints.ingress,
|
||||||
|
location)
|
||||||
|
|
||||||
|
target_waypoints: List[FlightWaypoint] = []
|
||||||
|
if targets is not None:
|
||||||
|
for target in targets:
|
||||||
|
target_waypoints.append(
|
||||||
|
self.target_waypoint(flight, builder, target))
|
||||||
|
else:
|
||||||
|
target_waypoints.append(
|
||||||
|
self.target_area_waypoint(flight, location, builder))
|
||||||
|
|
||||||
|
descent, land = builder.rtb(flight.from_cp)
|
||||||
|
return StrikeFlightPlan(
|
||||||
|
package=self.package,
|
||||||
|
flight=flight,
|
||||||
|
takeoff=builder.takeoff(flight.from_cp),
|
||||||
|
ascent=builder.ascent(flight.from_cp),
|
||||||
|
hold=builder.hold(self._hold_point(flight)),
|
||||||
|
join=builder.join(self.package.waypoints.join),
|
||||||
|
ingress=ingress,
|
||||||
|
targets=target_waypoints,
|
||||||
|
egress=builder.egress(self.package.waypoints.egress, location),
|
||||||
|
split=builder.split(self.package.waypoints.split),
|
||||||
|
descent=descent,
|
||||||
|
land=land
|
||||||
|
)
|
||||||
|
|
||||||
def _join_point(self, ingress_point: Point) -> Point:
|
def _join_point(self, ingress_point: Point) -> Point:
|
||||||
heading = self._heading_to_package_airfield(ingress_point)
|
heading = self._heading_to_package_airfield(ingress_point)
|
||||||
|
|||||||
@ -2,61 +2,20 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from dataclasses import dataclass
|
from datetime import timedelta
|
||||||
from typing import Iterable, Optional
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.unittype import FlyingType
|
from dcs.unittype import FlyingType
|
||||||
|
|
||||||
from game.utils import meter_to_nm
|
from game.utils import meter_to_nm
|
||||||
|
from gen.flights.flight import Flight
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
from gen.flights.flight import (
|
|
||||||
Flight,
|
|
||||||
FlightType,
|
|
||||||
FlightWaypoint,
|
|
||||||
FlightWaypointType,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
CAP_DURATION = 30 # Minutes
|
|
||||||
|
|
||||||
INGRESS_TYPES = {
|
|
||||||
FlightWaypointType.INGRESS_CAS,
|
|
||||||
FlightWaypointType.INGRESS_ESCORT,
|
|
||||||
FlightWaypointType.INGRESS_SEAD,
|
|
||||||
FlightWaypointType.INGRESS_STRIKE,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class GroundSpeed:
|
class GroundSpeed:
|
||||||
@staticmethod
|
|
||||||
def mission_speed(package: Package) -> int:
|
|
||||||
speeds = set()
|
|
||||||
for flight in package.flights:
|
|
||||||
# Find a waypoint that matches the mission start waypoint and use
|
|
||||||
# that for the altitude of the mission. That may not be true for the
|
|
||||||
# whole mission, but it's probably good enough for now.
|
|
||||||
waypoint = flight.waypoint_with_type({
|
|
||||||
FlightWaypointType.INGRESS_CAS,
|
|
||||||
FlightWaypointType.INGRESS_ESCORT,
|
|
||||||
FlightWaypointType.INGRESS_SEAD,
|
|
||||||
FlightWaypointType.INGRESS_STRIKE,
|
|
||||||
FlightWaypointType.PATROL_TRACK,
|
|
||||||
})
|
|
||||||
if waypoint is None:
|
|
||||||
logging.error(f"Could not find ingress point for {flight}.")
|
|
||||||
if flight.points:
|
|
||||||
logging.warning(
|
|
||||||
"Using first waypoint for mission altitude.")
|
|
||||||
waypoint = flight.points[0]
|
|
||||||
else:
|
|
||||||
logging.warning(
|
|
||||||
"Flight has no waypoints. Assuming mission altitude "
|
|
||||||
"of 25000 feet.")
|
|
||||||
waypoint = FlightWaypoint(FlightWaypointType.NAV, 0, 0,
|
|
||||||
25000)
|
|
||||||
speeds.add(GroundSpeed.for_flight(flight, waypoint.alt))
|
|
||||||
return min(speeds)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_flight(cls, flight: Flight, altitude: int) -> int:
|
def for_flight(cls, flight: Flight, altitude: int) -> int:
|
||||||
@ -121,52 +80,53 @@ class GroundSpeed:
|
|||||||
|
|
||||||
class TravelTime:
|
class TravelTime:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def between_points(a: Point, b: Point, speed: float) -> int:
|
def between_points(a: Point, b: Point, speed: float) -> timedelta:
|
||||||
error_factor = 1.1
|
error_factor = 1.1
|
||||||
distance = meter_to_nm(a.distance_to_point(b))
|
distance = meter_to_nm(a.distance_to_point(b))
|
||||||
hours = distance / speed
|
return timedelta(hours=distance / speed * error_factor)
|
||||||
seconds = hours * 3600
|
|
||||||
return int(seconds * error_factor)
|
|
||||||
|
|
||||||
|
|
||||||
class TotEstimator:
|
class TotEstimator:
|
||||||
# An extra five minutes given as wiggle room. Expected to be spent at the
|
# An extra five minutes given as wiggle room. Expected to be spent at the
|
||||||
# hold point performing any last minute configuration.
|
# hold point performing any last minute configuration.
|
||||||
HOLD_TIME = 5 * 60
|
HOLD_TIME = timedelta(minutes=5)
|
||||||
|
|
||||||
def __init__(self, package: Package) -> None:
|
def __init__(self, package: Package) -> None:
|
||||||
self.package = package
|
self.package = package
|
||||||
self.timing = PackageWaypointTiming.for_package(package)
|
|
||||||
|
|
||||||
def mission_start_time(self, flight: Flight) -> int:
|
def mission_start_time(self, flight: Flight) -> timedelta:
|
||||||
takeoff_time = self.takeoff_time_for_flight(flight)
|
takeoff_time = self.takeoff_time_for_flight(flight)
|
||||||
startup_time = self.estimate_startup(flight)
|
startup_time = self.estimate_startup(flight)
|
||||||
ground_ops_time = self.estimate_ground_ops(flight)
|
ground_ops_time = self.estimate_ground_ops(flight)
|
||||||
return takeoff_time - startup_time - ground_ops_time
|
return takeoff_time - startup_time - ground_ops_time
|
||||||
|
|
||||||
def takeoff_time_for_flight(self, flight: Flight) -> int:
|
def takeoff_time_for_flight(self, flight: Flight) -> timedelta:
|
||||||
stop_types = {FlightWaypointType.JOIN, FlightWaypointType.PATROL_TRACK}
|
travel_time = self.travel_time_to_rendezvous_or_target(flight)
|
||||||
travel_time = self.estimate_waypoints_to_target(flight, stop_types)
|
|
||||||
if travel_time is None:
|
if travel_time is None:
|
||||||
logging.warning("Found no join point or patrol point. Cannot "
|
logging.warning("Found no join point or patrol point. Cannot "
|
||||||
f"estimate takeoff time takeoff time for {flight}")
|
f"estimate takeoff time takeoff time for {flight}")
|
||||||
# Takeoff immediately.
|
# Takeoff immediately.
|
||||||
return 0
|
return timedelta()
|
||||||
|
|
||||||
# BARCAP flights do not coordinate with the rest of the package on join
|
from gen.flights.flightplan import FormationFlightPlan
|
||||||
# or ingress points.
|
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||||
if flight.flight_type == FlightType.BARCAP:
|
tot = flight.flight_plan.tot_for_waypoint(
|
||||||
start_time = self.timing.race_track_start(flight)
|
flight.flight_plan.join)
|
||||||
|
if tot is None:
|
||||||
|
logging.warning(
|
||||||
|
"Could not determine the TOT of the join point. Takeoff "
|
||||||
|
f"time for {flight} will be immediate.")
|
||||||
|
return timedelta()
|
||||||
else:
|
else:
|
||||||
start_time = self.timing.join
|
tot = self.package.time_over_target
|
||||||
return start_time - travel_time - self.HOLD_TIME
|
return tot - travel_time - self.HOLD_TIME
|
||||||
|
|
||||||
def earliest_tot(self) -> int:
|
def earliest_tot(self) -> timedelta:
|
||||||
return max((
|
return max((
|
||||||
self.earliest_tot_for_flight(f) for f in self.package.flights
|
self.earliest_tot_for_flight(f) for f in self.package.flights
|
||||||
)) + self.HOLD_TIME
|
)) + self.HOLD_TIME
|
||||||
|
|
||||||
def earliest_tot_for_flight(self, flight: Flight) -> int:
|
def earliest_tot_for_flight(self, flight: Flight) -> timedelta:
|
||||||
"""Estimate fastest time from mission start to the target position.
|
"""Estimate fastest time from mission start to the target position.
|
||||||
|
|
||||||
For BARCAP flights, this is time to race track start. This ensures that
|
For BARCAP flights, this is time to race track start. This ensures that
|
||||||
@ -182,211 +142,47 @@ class TotEstimator:
|
|||||||
The earliest possible TOT for the given flight in seconds. Returns 0
|
The earliest possible TOT for the given flight in seconds. Returns 0
|
||||||
if an ingress point cannot be found.
|
if an ingress point cannot be found.
|
||||||
"""
|
"""
|
||||||
if flight.flight_type == FlightType.BARCAP:
|
time_to_target = self.travel_time_to_target(flight)
|
||||||
time_to_target = self.estimate_waypoints_to_target(flight, {
|
|
||||||
FlightWaypointType.PATROL_TRACK
|
|
||||||
})
|
|
||||||
if time_to_target is None:
|
if time_to_target is None:
|
||||||
logging.warning(
|
logging.warning(f"Cannot estimate TOT for {flight}")
|
||||||
f"Found no race track. Cannot estimate TOT for {flight}")
|
|
||||||
# Return 0 so this flight's travel time does not affect the rest
|
# Return 0 so this flight's travel time does not affect the rest
|
||||||
# of the package.
|
# of the package.
|
||||||
return 0
|
return timedelta()
|
||||||
else:
|
startup = self.estimate_startup(flight)
|
||||||
time_to_ingress = self.estimate_waypoints_to_target(
|
ground_ops = self.estimate_ground_ops(flight)
|
||||||
flight, INGRESS_TYPES
|
return startup + ground_ops + time_to_target
|
||||||
)
|
|
||||||
if time_to_ingress is None:
|
|
||||||
logging.warning(
|
|
||||||
f"Found no ingress types. Cannot estimate TOT for {flight}")
|
|
||||||
# Return 0 so this flight's travel time does not affect the rest
|
|
||||||
# of the package.
|
|
||||||
return 0
|
|
||||||
|
|
||||||
assert self.package.waypoints is not None
|
|
||||||
time_to_target = time_to_ingress + TravelTime.between_points(
|
|
||||||
self.package.waypoints.ingress, self.package.target.position,
|
|
||||||
GroundSpeed.mission_speed(self.package))
|
|
||||||
return sum([
|
|
||||||
self.estimate_startup(flight),
|
|
||||||
self.estimate_ground_ops(flight),
|
|
||||||
time_to_target,
|
|
||||||
])
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def estimate_startup(flight: Flight) -> int:
|
def estimate_startup(flight: Flight) -> timedelta:
|
||||||
if flight.start_type == "Cold":
|
if flight.start_type == "Cold":
|
||||||
if flight.client_count:
|
if flight.client_count:
|
||||||
return 10 * 60
|
return timedelta(minutes=10)
|
||||||
else:
|
else:
|
||||||
# The AI doesn't seem to have a real startup procedure.
|
# The AI doesn't seem to have a real startup procedure.
|
||||||
return 2 * 60
|
return timedelta(minutes=2)
|
||||||
return 0
|
return timedelta()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def estimate_ground_ops(flight: Flight) -> int:
|
def estimate_ground_ops(flight: Flight) -> timedelta:
|
||||||
if flight.start_type in ("Runway", "In Flight"):
|
if flight.start_type in ("Runway", "In Flight"):
|
||||||
return 0
|
return timedelta()
|
||||||
if flight.from_cp.is_fleet:
|
if flight.from_cp.is_fleet:
|
||||||
return 2 * 60
|
return timedelta(minutes=2)
|
||||||
else:
|
else:
|
||||||
return 5 * 60
|
return timedelta(minutes=5)
|
||||||
|
|
||||||
def estimate_waypoints_to_target(
|
|
||||||
self, flight: Flight,
|
|
||||||
stop_types: Iterable[FlightWaypointType]) -> Optional[int]:
|
|
||||||
total = 0
|
|
||||||
# TODO: This is AGL. We want MSL.
|
|
||||||
previous_altitude = 0
|
|
||||||
previous_position = flight.from_cp.position
|
|
||||||
for waypoint in flight.points:
|
|
||||||
position = Point(waypoint.x, waypoint.y)
|
|
||||||
total += TravelTime.between_points(
|
|
||||||
previous_position, position,
|
|
||||||
self.speed_to_waypoint(flight, waypoint, previous_altitude)
|
|
||||||
)
|
|
||||||
previous_position = position
|
|
||||||
previous_altitude = waypoint.alt
|
|
||||||
if waypoint.waypoint_type in stop_types:
|
|
||||||
return total
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def travel_time_to_target(flight: Flight) -> Optional[timedelta]:
|
||||||
|
if flight.flight_plan is None:
|
||||||
return None
|
return None
|
||||||
|
return flight.flight_plan.travel_time_to_target
|
||||||
|
|
||||||
def speed_to_waypoint(self, flight: Flight, waypoint: FlightWaypoint,
|
@staticmethod
|
||||||
from_altitude: int) -> int:
|
def travel_time_to_rendezvous_or_target(
|
||||||
# TODO: Adjust if AGL.
|
flight: Flight) -> Optional[timedelta]:
|
||||||
# We don't have an exact heightmap, but we should probably be performing
|
if flight.flight_plan is None:
|
||||||
# *some* adjustment for NTTR since the minimum altitude of the map is
|
|
||||||
# near 2000 ft MSL.
|
|
||||||
alt_for_speed = min(from_altitude, waypoint.alt)
|
|
||||||
pre_join = (FlightWaypointType.LOITER, FlightWaypointType.JOIN)
|
|
||||||
if waypoint.waypoint_type == FlightWaypointType.ASCEND_POINT:
|
|
||||||
# Flights that start airborne already have some altitude and a good
|
|
||||||
# amount of speed.
|
|
||||||
factor = 1.0 if flight.start_type == "In Flight" else 0.5
|
|
||||||
return int(GroundSpeed.for_flight(flight, alt_for_speed) * factor)
|
|
||||||
elif waypoint.waypoint_type in pre_join:
|
|
||||||
return GroundSpeed.for_flight(flight, alt_for_speed)
|
|
||||||
return GroundSpeed.mission_speed(self.package)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class PackageWaypointTiming:
|
|
||||||
#: The package being scheduled.
|
|
||||||
package: Package
|
|
||||||
|
|
||||||
#: The package join time.
|
|
||||||
join: int
|
|
||||||
|
|
||||||
#: The ingress waypoint TOT.
|
|
||||||
ingress: int
|
|
||||||
|
|
||||||
#: The egress waypoint TOT.
|
|
||||||
egress: int
|
|
||||||
|
|
||||||
#: The package split time.
|
|
||||||
split: int
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target(self) -> int:
|
|
||||||
"""The package time over target."""
|
|
||||||
assert self.package.time_over_target is not None
|
|
||||||
return self.package.time_over_target
|
|
||||||
|
|
||||||
def race_track_start(self, flight: Flight) -> int:
|
|
||||||
if flight.flight_type == FlightType.BARCAP:
|
|
||||||
return self.target
|
|
||||||
else:
|
|
||||||
# The only other type that (currently) uses race tracks is TARCAP,
|
|
||||||
# which is sort of in need of cleanup. TARCAP is only valid on front
|
|
||||||
# lines and they participate in join points and patrol between the
|
|
||||||
# ingress and egress points rather than on a race track actually
|
|
||||||
# pointed at the enemy.
|
|
||||||
return self.ingress
|
|
||||||
|
|
||||||
def race_track_end(self, flight: Flight) -> int:
|
|
||||||
if flight.flight_type == FlightType.BARCAP:
|
|
||||||
return self.target + CAP_DURATION * 60
|
|
||||||
else:
|
|
||||||
# For TARCAP. See the explanation in race_track_start.
|
|
||||||
return self.egress
|
|
||||||
|
|
||||||
def push_time(self, flight: Flight, hold_point: FlightWaypoint) -> int:
|
|
||||||
assert self.package.waypoints is not None
|
|
||||||
return self.join - TravelTime.between_points(
|
|
||||||
Point(hold_point.x, hold_point.y),
|
|
||||||
self.package.waypoints.join,
|
|
||||||
GroundSpeed.for_flight(flight, hold_point.alt)
|
|
||||||
)
|
|
||||||
|
|
||||||
def tot_for_waypoint(self, flight: Flight,
|
|
||||||
waypoint: FlightWaypoint) -> Optional[int]:
|
|
||||||
target_types = (
|
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
|
||||||
FlightWaypointType.TARGET_POINT,
|
|
||||||
FlightWaypointType.TARGET_SHIP,
|
|
||||||
)
|
|
||||||
|
|
||||||
if waypoint.waypoint_type == FlightWaypointType.JOIN:
|
|
||||||
return self.join
|
|
||||||
elif waypoint.waypoint_type in INGRESS_TYPES:
|
|
||||||
return self.ingress
|
|
||||||
elif waypoint.waypoint_type in target_types:
|
|
||||||
return self.target
|
|
||||||
elif waypoint.waypoint_type == FlightWaypointType.EGRESS:
|
|
||||||
return self.egress
|
|
||||||
elif waypoint.waypoint_type == FlightWaypointType.SPLIT:
|
|
||||||
return self.split
|
|
||||||
elif waypoint.waypoint_type == FlightWaypointType.PATROL_TRACK:
|
|
||||||
return self.race_track_start(flight)
|
|
||||||
return None
|
return None
|
||||||
|
from gen.flights.flightplan import FormationFlightPlan
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
|
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||||
flight: Flight) -> Optional[int]:
|
return flight.flight_plan.travel_time_to_rendezvous
|
||||||
if waypoint.waypoint_type == FlightWaypointType.LOITER:
|
return flight.flight_plan.travel_time_to_target
|
||||||
return self.push_time(flight, waypoint)
|
|
||||||
elif waypoint.waypoint_type == FlightWaypointType.PATROL:
|
|
||||||
return self.race_track_end(flight)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def for_package(cls, package: Package) -> PackageWaypointTiming:
|
|
||||||
assert package.waypoints is not None
|
|
||||||
|
|
||||||
# TODO: Plan similar altitudes for the in-country leg of the mission.
|
|
||||||
# Waypoint altitudes for a given flight *shouldn't* differ too much
|
|
||||||
# between the join and split points, so we don't need speeds for each
|
|
||||||
# leg individually since they should all be fairly similar. This doesn't
|
|
||||||
# hold too well right now since nothing is stopping each waypoint from
|
|
||||||
# jumping 20k feet each time, but that's a huge waste of energy we
|
|
||||||
# should be avoiding anyway.
|
|
||||||
if not package.flights:
|
|
||||||
raise ValueError("Cannot plan TOT for package with no flights")
|
|
||||||
|
|
||||||
group_ground_speed = GroundSpeed.mission_speed(package)
|
|
||||||
|
|
||||||
ingress = package.time_over_target - TravelTime.between_points(
|
|
||||||
package.waypoints.ingress,
|
|
||||||
package.target.position,
|
|
||||||
group_ground_speed
|
|
||||||
)
|
|
||||||
|
|
||||||
join = ingress - TravelTime.between_points(
|
|
||||||
package.waypoints.join,
|
|
||||||
package.waypoints.ingress,
|
|
||||||
group_ground_speed
|
|
||||||
)
|
|
||||||
|
|
||||||
egress = package.time_over_target + TravelTime.between_points(
|
|
||||||
package.target.position,
|
|
||||||
package.waypoints.egress,
|
|
||||||
group_ground_speed
|
|
||||||
)
|
|
||||||
|
|
||||||
split = egress + TravelTime.between_points(
|
|
||||||
package.waypoints.egress,
|
|
||||||
package.waypoints.split,
|
|
||||||
group_ground_speed
|
|
||||||
)
|
|
||||||
|
|
||||||
return cls(package, join, ingress, egress, split)
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import List, Optional, Union
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Optional, Tuple, Union
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.unit import Unit
|
from dcs.unit import Unit
|
||||||
@ -13,23 +14,50 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
|
|||||||
from ..runways import RunwayAssigner
|
from ..runways import RunwayAssigner
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class StrikeTarget:
|
||||||
|
name: str
|
||||||
|
target: Union[TheaterGroundObject, Unit]
|
||||||
|
|
||||||
|
|
||||||
class WaypointBuilder:
|
class WaypointBuilder:
|
||||||
def __init__(self, conditions: Conditions, flight: Flight,
|
def __init__(self, conditions: Conditions, flight: Flight,
|
||||||
doctrine: Doctrine) -> None:
|
doctrine: Doctrine,
|
||||||
|
targets: Optional[List[StrikeTarget]] = None) -> None:
|
||||||
self.conditions = conditions
|
self.conditions = conditions
|
||||||
self.flight = flight
|
self.flight = flight
|
||||||
self.doctrine = doctrine
|
self.doctrine = doctrine
|
||||||
self.waypoints: List[FlightWaypoint] = []
|
self.targets = targets
|
||||||
self.ingress_point: Optional[FlightWaypoint] = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_helo(self) -> bool:
|
def is_helo(self) -> bool:
|
||||||
return getattr(self.flight.unit_type, "helicopter", False)
|
return getattr(self.flight.unit_type, "helicopter", False)
|
||||||
|
|
||||||
def build(self) -> List[FlightWaypoint]:
|
@staticmethod
|
||||||
return self.waypoints
|
def takeoff(departure: ControlPoint) -> FlightWaypoint:
|
||||||
|
"""Create takeoff waypoint for the given arrival airfield or carrier.
|
||||||
|
|
||||||
def ascent(self, departure: ControlPoint) -> None:
|
Note that the takeoff waypoint will automatically be created by pydcs
|
||||||
|
when we create the group, but creating our own before generation makes
|
||||||
|
the planning code simpler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
departure: Departure airfield or carrier.
|
||||||
|
"""
|
||||||
|
position = departure.position
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.TAKEOFF,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
waypoint.name = "TAKEOFF"
|
||||||
|
waypoint.alt_type = "RADIO"
|
||||||
|
waypoint.description = "Takeoff"
|
||||||
|
waypoint.pretty_name = "Takeoff"
|
||||||
|
return waypoint
|
||||||
|
|
||||||
|
def ascent(self, departure: ControlPoint) -> FlightWaypoint:
|
||||||
"""Create ascent waypoint for the given departure airfield or carrier.
|
"""Create ascent waypoint for the given departure airfield or carrier.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -49,9 +77,9 @@ class WaypointBuilder:
|
|||||||
waypoint.alt_type = "RADIO"
|
waypoint.alt_type = "RADIO"
|
||||||
waypoint.description = "Ascend"
|
waypoint.description = "Ascend"
|
||||||
waypoint.pretty_name = "Ascend"
|
waypoint.pretty_name = "Ascend"
|
||||||
self.waypoints.append(waypoint)
|
return waypoint
|
||||||
|
|
||||||
def descent(self, arrival: ControlPoint) -> None:
|
def descent(self, arrival: ControlPoint) -> FlightWaypoint:
|
||||||
"""Create descent waypoint for the given arrival airfield or carrier.
|
"""Create descent waypoint for the given arrival airfield or carrier.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -73,9 +101,10 @@ class WaypointBuilder:
|
|||||||
waypoint.alt_type = "RADIO"
|
waypoint.alt_type = "RADIO"
|
||||||
waypoint.description = "Descend to pattern altitude"
|
waypoint.description = "Descend to pattern altitude"
|
||||||
waypoint.pretty_name = "Descend"
|
waypoint.pretty_name = "Descend"
|
||||||
self.waypoints.append(waypoint)
|
return waypoint
|
||||||
|
|
||||||
def land(self, arrival: ControlPoint) -> None:
|
@staticmethod
|
||||||
|
def land(arrival: ControlPoint) -> FlightWaypoint:
|
||||||
"""Create descent waypoint for the given arrival airfield or carrier.
|
"""Create descent waypoint for the given arrival airfield or carrier.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -92,9 +121,9 @@ class WaypointBuilder:
|
|||||||
waypoint.alt_type = "RADIO"
|
waypoint.alt_type = "RADIO"
|
||||||
waypoint.description = "Land"
|
waypoint.description = "Land"
|
||||||
waypoint.pretty_name = "Land"
|
waypoint.pretty_name = "Land"
|
||||||
self.waypoints.append(waypoint)
|
return waypoint
|
||||||
|
|
||||||
def hold(self, position: Point) -> None:
|
def hold(self, position: Point) -> FlightWaypoint:
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.LOITER,
|
FlightWaypointType.LOITER,
|
||||||
position.x,
|
position.x,
|
||||||
@ -104,9 +133,9 @@ class WaypointBuilder:
|
|||||||
waypoint.pretty_name = "Hold"
|
waypoint.pretty_name = "Hold"
|
||||||
waypoint.description = "Wait until push time"
|
waypoint.description = "Wait until push time"
|
||||||
waypoint.name = "HOLD"
|
waypoint.name = "HOLD"
|
||||||
self.waypoints.append(waypoint)
|
return waypoint
|
||||||
|
|
||||||
def join(self, position: Point) -> None:
|
def join(self, position: Point) -> FlightWaypoint:
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.JOIN,
|
FlightWaypointType.JOIN,
|
||||||
position.x,
|
position.x,
|
||||||
@ -116,9 +145,9 @@ class WaypointBuilder:
|
|||||||
waypoint.pretty_name = "Join"
|
waypoint.pretty_name = "Join"
|
||||||
waypoint.description = "Rendezvous with package"
|
waypoint.description = "Rendezvous with package"
|
||||||
waypoint.name = "JOIN"
|
waypoint.name = "JOIN"
|
||||||
self.waypoints.append(waypoint)
|
return waypoint
|
||||||
|
|
||||||
def split(self, position: Point) -> None:
|
def split(self, position: Point) -> FlightWaypoint:
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.SPLIT,
|
FlightWaypointType.SPLIT,
|
||||||
position.x,
|
position.x,
|
||||||
@ -128,25 +157,30 @@ class WaypointBuilder:
|
|||||||
waypoint.pretty_name = "Split"
|
waypoint.pretty_name = "Split"
|
||||||
waypoint.description = "Depart from package"
|
waypoint.description = "Depart from package"
|
||||||
waypoint.name = "SPLIT"
|
waypoint.name = "SPLIT"
|
||||||
self.waypoints.append(waypoint)
|
return waypoint
|
||||||
|
|
||||||
def ingress_cas(self, position: Point, objective: MissionTarget) -> None:
|
def ingress_cas(self, position: Point,
|
||||||
self._ingress(FlightWaypointType.INGRESS_CAS, position, objective)
|
objective: MissionTarget) -> FlightWaypoint:
|
||||||
|
return self._ingress(FlightWaypointType.INGRESS_CAS, position,
|
||||||
|
objective)
|
||||||
|
|
||||||
def ingress_escort(self, position: Point, objective: MissionTarget) -> None:
|
def ingress_escort(self, position: Point,
|
||||||
self._ingress(FlightWaypointType.INGRESS_ESCORT, position, objective)
|
objective: MissionTarget) -> FlightWaypoint:
|
||||||
|
return self._ingress(FlightWaypointType.INGRESS_ESCORT, position,
|
||||||
|
objective)
|
||||||
|
|
||||||
def ingress_sead(self, position: Point, objective: MissionTarget) -> None:
|
def ingress_sead(self, position: Point,
|
||||||
self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective)
|
objective: MissionTarget) -> FlightWaypoint:
|
||||||
|
return self._ingress(FlightWaypointType.INGRESS_SEAD, position,
|
||||||
|
objective)
|
||||||
|
|
||||||
def ingress_strike(self, position: Point, objective: MissionTarget) -> None:
|
def ingress_strike(self, position: Point,
|
||||||
self._ingress(FlightWaypointType.INGRESS_STRIKE, position, objective)
|
objective: MissionTarget) -> FlightWaypoint:
|
||||||
|
return self._ingress(FlightWaypointType.INGRESS_STRIKE, position,
|
||||||
|
objective)
|
||||||
|
|
||||||
def _ingress(self, ingress_type: FlightWaypointType, position: Point,
|
def _ingress(self, ingress_type: FlightWaypointType, position: Point,
|
||||||
objective: MissionTarget) -> None:
|
objective: MissionTarget) -> FlightWaypoint:
|
||||||
if self.ingress_point is not None:
|
|
||||||
raise RuntimeError("A flight plan can have only one ingress point.")
|
|
||||||
|
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
ingress_type,
|
ingress_type,
|
||||||
position.x,
|
position.x,
|
||||||
@ -156,10 +190,11 @@ class WaypointBuilder:
|
|||||||
waypoint.pretty_name = "INGRESS on " + objective.name
|
waypoint.pretty_name = "INGRESS on " + objective.name
|
||||||
waypoint.description = "INGRESS on " + objective.name
|
waypoint.description = "INGRESS on " + objective.name
|
||||||
waypoint.name = "INGRESS"
|
waypoint.name = "INGRESS"
|
||||||
self.waypoints.append(waypoint)
|
# TODO: This seems wrong, but it's what was there before.
|
||||||
self.ingress_point = waypoint
|
waypoint.targets.append(objective)
|
||||||
|
return waypoint
|
||||||
|
|
||||||
def egress(self, position: Point, target: MissionTarget) -> None:
|
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.EGRESS,
|
FlightWaypointType.EGRESS,
|
||||||
position.x,
|
position.x,
|
||||||
@ -169,68 +204,45 @@ class WaypointBuilder:
|
|||||||
waypoint.pretty_name = "EGRESS from " + target.name
|
waypoint.pretty_name = "EGRESS from " + target.name
|
||||||
waypoint.description = "EGRESS from " + target.name
|
waypoint.description = "EGRESS from " + target.name
|
||||||
waypoint.name = "EGRESS"
|
waypoint.name = "EGRESS"
|
||||||
self.waypoints.append(waypoint)
|
return waypoint
|
||||||
|
|
||||||
def dead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
def dead_point(self, target: StrikeTarget) -> FlightWaypoint:
|
||||||
location: MissionTarget) -> None:
|
return self._target_point(target, f"STRIKE {target.name}")
|
||||||
self._target_point(target, name, f"STRIKE {name}", location)
|
|
||||||
# TODO: Seems fishy.
|
|
||||||
if self.ingress_point is not None:
|
|
||||||
self.ingress_point.targetGroup = location
|
|
||||||
|
|
||||||
def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
def sead_point(self, target: StrikeTarget) -> FlightWaypoint:
|
||||||
location: MissionTarget) -> None:
|
return self._target_point(target, f"STRIKE {target.name}")
|
||||||
self._target_point(target, name, f"STRIKE {name}", location)
|
|
||||||
# TODO: Seems fishy.
|
|
||||||
if self.ingress_point is not None:
|
|
||||||
self.ingress_point.targetGroup = location
|
|
||||||
|
|
||||||
def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
def strike_point(self, target: StrikeTarget) -> FlightWaypoint:
|
||||||
location: MissionTarget) -> None:
|
return self._target_point(target, f"STRIKE {target.name}")
|
||||||
self._target_point(target, name, f"STRIKE {name}", location)
|
|
||||||
|
|
||||||
def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
|
||||||
description: str, location: MissionTarget) -> None:
|
|
||||||
if self.ingress_point is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
"An ingress point must be added before target points."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _target_point(target: StrikeTarget, description: str) -> FlightWaypoint:
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.TARGET_POINT,
|
FlightWaypointType.TARGET_POINT,
|
||||||
target.position.x,
|
target.target.position.x,
|
||||||
target.position.y,
|
target.target.position.y,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
waypoint.description = description
|
waypoint.description = description
|
||||||
waypoint.pretty_name = description
|
waypoint.pretty_name = description
|
||||||
waypoint.name = name
|
waypoint.name = target.name
|
||||||
# The target waypoints are only for the player's benefit. AI tasks for
|
# The target waypoints are only for the player's benefit. AI tasks for
|
||||||
# the target are set on the ingress point so they begin their attack
|
# the target are set on the ingress point so they begin their attack
|
||||||
# *before* reaching the target.
|
# *before* reaching the target.
|
||||||
waypoint.only_for_player = True
|
waypoint.only_for_player = True
|
||||||
self.waypoints.append(waypoint)
|
return waypoint
|
||||||
# TODO: This seems wrong, but it's what was there before.
|
|
||||||
self.ingress_point.targets.append(location)
|
|
||||||
|
|
||||||
def sead_area(self, target: MissionTarget) -> None:
|
def strike_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||||
self._target_area(f"SEAD on {target.name}", target)
|
return self._target_area(f"STRIKE {target.name}", target)
|
||||||
# TODO: Seems fishy.
|
|
||||||
if self.ingress_point is not None:
|
|
||||||
self.ingress_point.targetGroup = target
|
|
||||||
|
|
||||||
def dead_area(self, target: MissionTarget) -> None:
|
def sead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||||
self._target_area(f"DEAD on {target.name}", target)
|
return self._target_area(f"SEAD on {target.name}", target)
|
||||||
# TODO: Seems fishy.
|
|
||||||
if self.ingress_point is not None:
|
|
||||||
self.ingress_point.targetGroup = target
|
|
||||||
|
|
||||||
def _target_area(self, name: str, location: MissionTarget) -> None:
|
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||||
if self.ingress_point is None:
|
return self._target_area(f"DEAD on {target.name}", target)
|
||||||
raise RuntimeError(
|
|
||||||
"An ingress point must be added before target points."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _target_area(name: str, location: MissionTarget) -> FlightWaypoint:
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
location.position.x,
|
location.position.x,
|
||||||
@ -244,11 +256,9 @@ class WaypointBuilder:
|
|||||||
# the target are set on the ingress point so they begin their attack
|
# the target are set on the ingress point so they begin their attack
|
||||||
# *before* reaching the target.
|
# *before* reaching the target.
|
||||||
waypoint.only_for_player = True
|
waypoint.only_for_player = True
|
||||||
self.waypoints.append(waypoint)
|
return waypoint
|
||||||
# TODO: This seems wrong, but it's what was there before.
|
|
||||||
self.ingress_point.targets.append(location)
|
|
||||||
|
|
||||||
def cas(self, position: Point) -> None:
|
def cas(self, position: Point) -> FlightWaypoint:
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.CAS,
|
FlightWaypointType.CAS,
|
||||||
position.x,
|
position.x,
|
||||||
@ -259,9 +269,10 @@ class WaypointBuilder:
|
|||||||
waypoint.description = "Provide CAS"
|
waypoint.description = "Provide CAS"
|
||||||
waypoint.name = "CAS"
|
waypoint.name = "CAS"
|
||||||
waypoint.pretty_name = "CAS"
|
waypoint.pretty_name = "CAS"
|
||||||
self.waypoints.append(waypoint)
|
return waypoint
|
||||||
|
|
||||||
def race_track_start(self, position: Point, altitude: int) -> None:
|
@staticmethod
|
||||||
|
def race_track_start(position: Point, altitude: int) -> FlightWaypoint:
|
||||||
"""Creates a racetrack start waypoint.
|
"""Creates a racetrack start waypoint.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -277,9 +288,10 @@ class WaypointBuilder:
|
|||||||
waypoint.name = "RACETRACK START"
|
waypoint.name = "RACETRACK START"
|
||||||
waypoint.description = "Orbit between this point and the next point"
|
waypoint.description = "Orbit between this point and the next point"
|
||||||
waypoint.pretty_name = "Race-track start"
|
waypoint.pretty_name = "Race-track start"
|
||||||
self.waypoints.append(waypoint)
|
return waypoint
|
||||||
|
|
||||||
def race_track_end(self, position: Point, altitude: int) -> None:
|
@staticmethod
|
||||||
|
def race_track_end(position: Point, altitude: int) -> FlightWaypoint:
|
||||||
"""Creates a racetrack end waypoint.
|
"""Creates a racetrack end waypoint.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -295,9 +307,10 @@ class WaypointBuilder:
|
|||||||
waypoint.name = "RACETRACK END"
|
waypoint.name = "RACETRACK END"
|
||||||
waypoint.description = "Orbit between this point and the previous point"
|
waypoint.description = "Orbit between this point and the previous point"
|
||||||
waypoint.pretty_name = "Race-track end"
|
waypoint.pretty_name = "Race-track end"
|
||||||
self.waypoints.append(waypoint)
|
return waypoint
|
||||||
|
|
||||||
def race_track(self, start: Point, end: Point, altitude: int) -> None:
|
def race_track(self, start: Point, end: Point,
|
||||||
|
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||||
"""Creates two waypoint for a racetrack orbit.
|
"""Creates two waypoint for a racetrack orbit.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -305,20 +318,20 @@ class WaypointBuilder:
|
|||||||
end: The ending racetrack waypoint.
|
end: The ending racetrack waypoint.
|
||||||
altitude: The racetrack altitude.
|
altitude: The racetrack altitude.
|
||||||
"""
|
"""
|
||||||
self.race_track_start(start, altitude)
|
return (self.race_track_start(start, altitude),
|
||||||
self.race_track_end(end, altitude)
|
self.race_track_end(end, altitude))
|
||||||
|
|
||||||
def rtb(self, arrival: ControlPoint) -> None:
|
def rtb(self,
|
||||||
|
arrival: ControlPoint) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||||
"""Creates descent ant landing waypoints for the given control point.
|
"""Creates descent ant landing waypoints for the given control point.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
arrival: Arrival airfield or carrier.
|
arrival: Arrival airfield or carrier.
|
||||||
"""
|
"""
|
||||||
self.descent(arrival)
|
return self.descent(arrival), self.land(arrival)
|
||||||
self.land(arrival)
|
|
||||||
|
|
||||||
def escort(self, ingress: Point, target: MissionTarget,
|
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
|
||||||
egress: Point) -> None:
|
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
|
||||||
"""Creates the waypoints needed to escort the package.
|
"""Creates the waypoints needed to escort the package.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -332,7 +345,8 @@ class WaypointBuilder:
|
|||||||
# description in gen.aircraft.JoinPointBuilder), so instead we give
|
# description in gen.aircraft.JoinPointBuilder), so instead we give
|
||||||
# the escort flights a flight plan including the ingress point, target
|
# the escort flights a flight plan including the ingress point, target
|
||||||
# area, and egress point.
|
# area, and egress point.
|
||||||
self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
|
ingress = self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
|
||||||
|
target)
|
||||||
|
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
@ -343,6 +357,6 @@ class WaypointBuilder:
|
|||||||
waypoint.name = "TARGET"
|
waypoint.name = "TARGET"
|
||||||
waypoint.description = "Escort the package"
|
waypoint.description = "Escort the package"
|
||||||
waypoint.pretty_name = "Target area"
|
waypoint.pretty_name = "Target area"
|
||||||
self.waypoints.append(waypoint)
|
|
||||||
|
|
||||||
self.egress(egress, target)
|
egress = self.egress(egress, target)
|
||||||
|
return ingress, waypoint, egress
|
||||||
|
|||||||
@ -157,10 +157,10 @@ class FlightPlanBuilder:
|
|||||||
self._format_time(waypoint.waypoint.departure_time),
|
self._format_time(waypoint.waypoint.departure_time),
|
||||||
])
|
])
|
||||||
|
|
||||||
def _format_time(self, time: Optional[int]) -> str:
|
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
|
||||||
if time is None:
|
if time is None:
|
||||||
return ""
|
return ""
|
||||||
local_time = self.start_time + datetime.timedelta(seconds=time)
|
local_time = self.start_time + time
|
||||||
return local_time.strftime(f"%H:%M:%S")
|
return local_time.strftime(f"%H:%M:%S")
|
||||||
|
|
||||||
def _waypoint_distance(self, waypoint: FlightWaypoint) -> str:
|
def _waypoint_distance(self, waypoint: FlightWaypoint) -> str:
|
||||||
@ -189,8 +189,8 @@ class FlightPlanBuilder:
|
|||||||
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
|
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
|
||||||
waypoint.position
|
waypoint.position
|
||||||
))
|
))
|
||||||
duration = (waypoint.tot - last_time) / 3600
|
duration = waypoint.tot - last_time
|
||||||
return f"{int(distance / duration)} kt"
|
return f"{int(distance / duration.total_seconds())} kt"
|
||||||
|
|
||||||
def build(self) -> List[List[str]]:
|
def build(self) -> List[List[str]]:
|
||||||
return self.rows
|
return self.rows
|
||||||
|
|||||||
@ -162,7 +162,7 @@ class PackageModel(QAbstractListModel):
|
|||||||
"""Returns the flight located at the given index."""
|
"""Returns the flight located at the given index."""
|
||||||
return self.package.flights[index.row()]
|
return self.package.flights[index.row()]
|
||||||
|
|
||||||
def update_tot(self, tot: int) -> None:
|
def update_tot(self, tot: datetime.timedelta) -> None:
|
||||||
self.package.time_over_target = tot
|
self.package.time_over_target = tot
|
||||||
self.layoutChanged.emit()
|
self.layoutChanged.emit()
|
||||||
|
|
||||||
|
|||||||
@ -126,7 +126,7 @@ class QTopPanel(QFrame):
|
|||||||
continue
|
continue
|
||||||
estimator = TotEstimator(package)
|
estimator = TotEstimator(package)
|
||||||
for flight in package.flights:
|
for flight in package.flights:
|
||||||
if estimator.mission_start_time(flight) < 0:
|
if estimator.mission_start_time(flight).total_seconds() < 0:
|
||||||
packages.append(package)
|
packages.append(package)
|
||||||
break
|
break
|
||||||
return packages
|
return packages
|
||||||
|
|||||||
@ -64,7 +64,8 @@ class FlightDelegate(QStyledItemDelegate):
|
|||||||
count = flight.count
|
count = flight.count
|
||||||
name = db.unit_type_name(flight.unit_type)
|
name = db.unit_type_name(flight.unit_type)
|
||||||
estimator = TotEstimator(self.package)
|
estimator = TotEstimator(self.package)
|
||||||
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
delay = datetime.timedelta(
|
||||||
|
seconds=int(estimator.mission_start_time(flight).total_seconds()))
|
||||||
return f"[{task}] {count} x {name} in {delay}"
|
return f"[{task}] {count} x {name} in {delay}"
|
||||||
|
|
||||||
def second_row_text(self, index: QModelIndex) -> str:
|
def second_row_text(self, index: QModelIndex) -> str:
|
||||||
@ -328,10 +329,9 @@ class PackageDelegate(QStyledItemDelegate):
|
|||||||
|
|
||||||
def right_text(self, index: QModelIndex) -> str:
|
def right_text(self, index: QModelIndex) -> str:
|
||||||
package = self.package(index)
|
package = self.package(index)
|
||||||
if package.time_over_target is None:
|
delay = datetime.timedelta(
|
||||||
return ""
|
seconds=int(package.time_over_target.total_seconds()))
|
||||||
tot = datetime.timedelta(seconds=package.time_over_target)
|
return f"TOT T+{delay}"
|
||||||
return f"TOT T+{tot}"
|
|
||||||
|
|
||||||
def paint(self, painter: QPainter, option: QStyleOptionViewItem,
|
def paint(self, painter: QPainter, option: QStyleOptionViewItem,
|
||||||
index: QModelIndex) -> None:
|
index: QModelIndex) -> None:
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
from PySide2.QtCore import QSortFilterProxyModel, Qt, QModelIndex
|
|
||||||
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||||
from PySide2.QtWidgets import QComboBox, QCompleter
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from gen import Conflict, FlightWaypointType
|
from gen import Conflict, FlightWaypointType
|
||||||
from gen.flights.flight import FlightWaypoint, PredefinedWaypointCategory
|
from gen.flights.flight import FlightWaypoint
|
||||||
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
|
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
|
||||||
from theater import ControlPointType
|
from theater import ControlPointType
|
||||||
|
|
||||||
@ -66,8 +65,6 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
|||||||
wpt.alt_type = "RADIO"
|
wpt.alt_type = "RADIO"
|
||||||
wpt.pretty_name = wpt.name
|
wpt.pretty_name = wpt.name
|
||||||
wpt.description = "Frontline"
|
wpt.description = "Frontline"
|
||||||
wpt.data = [cp, ecp]
|
|
||||||
wpt.category = PredefinedWaypointCategory.FRONTLINE
|
|
||||||
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
||||||
|
|
||||||
if self.include_targets:
|
if self.include_targets:
|
||||||
@ -86,13 +83,10 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
|||||||
wpt.pretty_name = wpt.name
|
wpt.pretty_name = wpt.name
|
||||||
wpt.obj_name = ground_object.obj_name
|
wpt.obj_name = ground_object.obj_name
|
||||||
wpt.targets.append(ground_object)
|
wpt.targets.append(ground_object)
|
||||||
wpt.data = ground_object
|
|
||||||
if cp.captured:
|
if cp.captured:
|
||||||
wpt.description = "Friendly Building"
|
wpt.description = "Friendly Building"
|
||||||
wpt.category = PredefinedWaypointCategory.ALLY_BUILDING
|
|
||||||
else:
|
else:
|
||||||
wpt.description = "Enemy Building"
|
wpt.description = "Enemy Building"
|
||||||
wpt.category = PredefinedWaypointCategory.ENEMY_BUILDING
|
|
||||||
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
||||||
|
|
||||||
if self.include_units:
|
if self.include_units:
|
||||||
@ -112,15 +106,12 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
|||||||
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j)
|
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j)
|
||||||
wpt.pretty_name = wpt.name
|
wpt.pretty_name = wpt.name
|
||||||
wpt.targets.append(u)
|
wpt.targets.append(u)
|
||||||
wpt.data = u
|
|
||||||
wpt.obj_name = ground_object.obj_name
|
wpt.obj_name = ground_object.obj_name
|
||||||
wpt.waypoint_type = FlightWaypointType.CUSTOM
|
wpt.waypoint_type = FlightWaypointType.CUSTOM
|
||||||
if cp.captured:
|
if cp.captured:
|
||||||
wpt.description = "Friendly unit : " + u.type
|
wpt.description = "Friendly unit : " + u.type
|
||||||
wpt.category = PredefinedWaypointCategory.ALLY_UNIT
|
|
||||||
else:
|
else:
|
||||||
wpt.description = "Enemy unit : " + u.type
|
wpt.description = "Enemy unit : " + u.type
|
||||||
wpt.category = PredefinedWaypointCategory.ENEMY_UNIT
|
|
||||||
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
||||||
|
|
||||||
if self.include_airbases:
|
if self.include_airbases:
|
||||||
@ -134,13 +125,10 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
|||||||
)
|
)
|
||||||
wpt.alt_type = "RADIO"
|
wpt.alt_type = "RADIO"
|
||||||
wpt.name = cp.name
|
wpt.name = cp.name
|
||||||
wpt.data = cp
|
|
||||||
if cp.captured:
|
if cp.captured:
|
||||||
wpt.description = "Position of " + cp.name + " [Friendly Airbase]"
|
wpt.description = "Position of " + cp.name + " [Friendly Airbase]"
|
||||||
wpt.category = PredefinedWaypointCategory.ALLY_CP
|
|
||||||
else:
|
else:
|
||||||
wpt.description = "Position of " + cp.name + " [Enemy Airbase]"
|
wpt.description = "Position of " + cp.name + " [Enemy Airbase]"
|
||||||
wpt.category = PredefinedWaypointCategory.ENEMY_CP
|
|
||||||
|
|
||||||
if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
|
if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
|
||||||
wpt.pretty_name = cp.name + " (Aircraft Carrier Group)"
|
wpt.pretty_name = cp.name + " (Aircraft Carrier Group)"
|
||||||
|
|||||||
@ -30,9 +30,9 @@ from game.data.aaa_db import AAA_UNITS
|
|||||||
from game.data.radar_db import UNITS_WITH_RADAR
|
from game.data.radar_db import UNITS_WITH_RADAR
|
||||||
from game.utils import meter_to_feet
|
from game.utils import meter_to_feet
|
||||||
from game.weather import TimeOfDay
|
from game.weather import TimeOfDay
|
||||||
from gen import Conflict, PackageWaypointTiming
|
from gen import Conflict
|
||||||
from gen.ato import Package
|
|
||||||
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
|
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
|
||||||
|
from gen.flights.flightplan import FlightPlan
|
||||||
from qt_ui.displayoptions import DisplayOptions
|
from qt_ui.displayoptions import DisplayOptions
|
||||||
from qt_ui.models import GameModel
|
from qt_ui.models import GameModel
|
||||||
from qt_ui.widgets.map.QFrontLine import QFrontLine
|
from qt_ui.widgets.map.QFrontLine import QFrontLine
|
||||||
@ -294,11 +294,10 @@ class QLiberationMap(QGraphicsView):
|
|||||||
selected = (p_idx, f_idx) == self.selected_flight
|
selected = (p_idx, f_idx) == self.selected_flight
|
||||||
if DisplayOptions.flight_paths.only_selected and not selected:
|
if DisplayOptions.flight_paths.only_selected and not selected:
|
||||||
continue
|
continue
|
||||||
self.draw_flight_plan(scene, package_model.package, flight,
|
self.draw_flight_plan(scene, flight, selected)
|
||||||
selected)
|
|
||||||
|
|
||||||
def draw_flight_plan(self, scene: QGraphicsScene, package: Package,
|
def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight,
|
||||||
flight: Flight, selected: bool) -> None:
|
selected: bool) -> None:
|
||||||
is_player = flight.from_cp.captured
|
is_player = flight.from_cp.captured
|
||||||
pos = self._transform_point(flight.from_cp.position)
|
pos = self._transform_point(flight.from_cp.position)
|
||||||
|
|
||||||
@ -310,7 +309,7 @@ class QLiberationMap(QGraphicsView):
|
|||||||
FlightWaypointType.TARGET_POINT,
|
FlightWaypointType.TARGET_POINT,
|
||||||
FlightWaypointType.TARGET_SHIP,
|
FlightWaypointType.TARGET_SHIP,
|
||||||
)
|
)
|
||||||
for idx, point in enumerate(flight.points):
|
for idx, point in enumerate(flight.flight_plan.waypoints[1:]):
|
||||||
new_pos = self._transform_point(Point(point.x, point.y))
|
new_pos = self._transform_point(Point(point.x, point.y))
|
||||||
self.draw_flight_path(scene, prev_pos, new_pos, is_player,
|
self.draw_flight_path(scene, prev_pos, new_pos, is_player,
|
||||||
selected)
|
selected)
|
||||||
@ -321,8 +320,8 @@ class QLiberationMap(QGraphicsView):
|
|||||||
# Don't draw dozens of targets over each other.
|
# Don't draw dozens of targets over each other.
|
||||||
continue
|
continue
|
||||||
drew_target = True
|
drew_target = True
|
||||||
self.draw_waypoint_info(scene, idx + 1, point, new_pos, package,
|
self.draw_waypoint_info(scene, idx + 1, point, new_pos,
|
||||||
flight)
|
flight.flight_plan)
|
||||||
prev_pos = tuple(new_pos)
|
prev_pos = tuple(new_pos)
|
||||||
self.draw_flight_path(scene, prev_pos, pos, is_player, selected)
|
self.draw_flight_path(scene, prev_pos, pos, is_player, selected)
|
||||||
|
|
||||||
@ -337,21 +336,21 @@ class QLiberationMap(QGraphicsView):
|
|||||||
|
|
||||||
def draw_waypoint_info(self, scene: QGraphicsScene, number: int,
|
def draw_waypoint_info(self, scene: QGraphicsScene, number: int,
|
||||||
waypoint: FlightWaypoint, position: Tuple[int, int],
|
waypoint: FlightWaypoint, position: Tuple[int, int],
|
||||||
package: Package, flight: Flight) -> None:
|
flight_plan: FlightPlan) -> None:
|
||||||
timing = PackageWaypointTiming.for_package(package)
|
|
||||||
|
|
||||||
altitude = meter_to_feet(waypoint.alt)
|
altitude = meter_to_feet(waypoint.alt)
|
||||||
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
|
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
|
||||||
|
|
||||||
prefix = "TOT"
|
prefix = "TOT"
|
||||||
time = timing.tot_for_waypoint(flight, waypoint)
|
time = flight_plan.tot_for_waypoint(waypoint)
|
||||||
if time is None:
|
if time is None:
|
||||||
prefix = "Depart"
|
prefix = "Depart"
|
||||||
time = timing.depart_time_for_waypoint(waypoint, flight)
|
time = flight_plan.depart_time_for_waypoint(waypoint)
|
||||||
if time is None:
|
if time is None:
|
||||||
tot = ""
|
tot = ""
|
||||||
else:
|
else:
|
||||||
tot = f"{prefix} T+{datetime.timedelta(seconds=time)}"
|
time = datetime.timedelta(seconds=int(time.total_seconds()))
|
||||||
|
tot = f"{prefix} T+{time}"
|
||||||
|
|
||||||
pen = QPen(QColor("black"), 0.3)
|
pen = QPen(QColor("black"), 0.3)
|
||||||
brush = QColor("white")
|
brush = QColor("white")
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"""Dialogs for creating and editing ATO packages."""
|
"""Dialogs for creating and editing ATO packages."""
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PySide2.QtCore import QItemSelection, QTime, Signal
|
from PySide2.QtCore import QItemSelection, QTime, Signal
|
||||||
@ -118,7 +119,7 @@ class QPackageDialog(QDialog):
|
|||||||
return self.game_model.game
|
return self.game_model.game
|
||||||
|
|
||||||
def tot_qtime(self) -> QTime:
|
def tot_qtime(self) -> QTime:
|
||||||
delay = self.package_model.package.time_over_target
|
delay = int(self.package_model.package.time_over_target.total_seconds())
|
||||||
hours = delay // 3600
|
hours = delay // 3600
|
||||||
minutes = delay // 60 % 60
|
minutes = delay // 60 % 60
|
||||||
seconds = delay % 60
|
seconds = delay % 60
|
||||||
@ -137,11 +138,11 @@ class QPackageDialog(QDialog):
|
|||||||
def save_tot(self) -> None:
|
def save_tot(self) -> None:
|
||||||
time = self.tot_spinner.time()
|
time = self.tot_spinner.time()
|
||||||
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
|
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
|
||||||
self.package_model.update_tot(seconds)
|
self.package_model.update_tot(timedelta(seconds=seconds))
|
||||||
|
|
||||||
def reset_tot(self) -> None:
|
def reset_tot(self) -> None:
|
||||||
if not list(self.package_model.flights):
|
if not list(self.package_model.flights):
|
||||||
self.package_model.update_tot(0)
|
self.package_model.update_tot(timedelta())
|
||||||
else:
|
else:
|
||||||
self.package_model.update_tot(
|
self.package_model.update_tot(
|
||||||
TotEstimator(self.package_model.package).earliest_tot())
|
TotEstimator(self.package_model.package).earliest_tot())
|
||||||
|
|||||||
@ -19,7 +19,8 @@ class QFlightDepartureDisplay(QGroupBox):
|
|||||||
layout.addLayout(departure_row)
|
layout.addLayout(departure_row)
|
||||||
|
|
||||||
estimator = TotEstimator(package)
|
estimator = TotEstimator(package)
|
||||||
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
delay = datetime.timedelta(
|
||||||
|
seconds=int(estimator.mission_start_time(flight).total_seconds()))
|
||||||
|
|
||||||
departure_row.addWidget(QLabel(
|
departure_row.addWidget(QLabel(
|
||||||
f"Departing from <b>{flight.from_cp.name}</b>"
|
f"Departing from <b>{flight.from_cp.name}</b>"
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import datetime
|
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from PySide2.QtCore import QItemSelectionModel, QPoint
|
from PySide2.QtCore import QItemSelectionModel, QPoint
|
||||||
@ -6,7 +5,6 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel
|
|||||||
from PySide2.QtWidgets import QHeaderView, QTableView
|
from PySide2.QtWidgets import QHeaderView, QTableView
|
||||||
|
|
||||||
from game.utils import meter_to_feet
|
from game.utils import meter_to_feet
|
||||||
from gen.aircraft import PackageWaypointTiming
|
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
from gen.flights.flight import Flight, FlightWaypoint
|
from gen.flights.flight import Flight, FlightWaypoint
|
||||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import \
|
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import \
|
||||||
@ -43,8 +41,6 @@ class QFlightWaypointList(QTableView):
|
|||||||
|
|
||||||
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
|
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
|
||||||
|
|
||||||
timing = PackageWaypointTiming.for_package(self.package)
|
|
||||||
|
|
||||||
# The first waypoint is set up by pydcs at mission generation time, so
|
# The first waypoint is set up by pydcs at mission generation time, so
|
||||||
# we need to add that waypoint manually.
|
# we need to add that waypoint manually.
|
||||||
takeoff = FlightWaypoint(self.flight.from_cp.position.x,
|
takeoff = FlightWaypoint(self.flight.from_cp.position.x,
|
||||||
@ -55,13 +51,12 @@ class QFlightWaypointList(QTableView):
|
|||||||
|
|
||||||
waypoints = itertools.chain([takeoff], self.flight.points)
|
waypoints = itertools.chain([takeoff], self.flight.points)
|
||||||
for row, waypoint in enumerate(waypoints):
|
for row, waypoint in enumerate(waypoints):
|
||||||
self.add_waypoint_row(row, self.flight, waypoint, timing)
|
self.add_waypoint_row(row, self.flight, waypoint)
|
||||||
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)),
|
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)),
|
||||||
QItemSelectionModel.Select)
|
QItemSelectionModel.Select)
|
||||||
|
|
||||||
def add_waypoint_row(self, row: int, flight: Flight,
|
def add_waypoint_row(self, row: int, flight: Flight,
|
||||||
waypoint: FlightWaypoint,
|
waypoint: FlightWaypoint) -> None:
|
||||||
timing: PackageWaypointTiming) -> None:
|
|
||||||
self.model.insertRow(self.model.rowCount())
|
self.model.insertRow(self.model.rowCount())
|
||||||
|
|
||||||
self.model.setItem(row, 0, QWaypointItem(waypoint, row))
|
self.model.setItem(row, 0, QWaypointItem(waypoint, row))
|
||||||
@ -72,18 +67,18 @@ class QFlightWaypointList(QTableView):
|
|||||||
altitude_item.setEditable(False)
|
altitude_item.setEditable(False)
|
||||||
self.model.setItem(row, 1, altitude_item)
|
self.model.setItem(row, 1, altitude_item)
|
||||||
|
|
||||||
tot = self.tot_text(flight, waypoint, timing)
|
tot = self.tot_text(flight, waypoint)
|
||||||
tot_item = QStandardItem(tot)
|
tot_item = QStandardItem(tot)
|
||||||
tot_item.setEditable(False)
|
tot_item.setEditable(False)
|
||||||
self.model.setItem(row, 2, tot_item)
|
self.model.setItem(row, 2, tot_item)
|
||||||
|
|
||||||
def tot_text(self, flight: Flight, waypoint: FlightWaypoint,
|
@staticmethod
|
||||||
timing: PackageWaypointTiming) -> str:
|
def tot_text(flight: Flight, waypoint: FlightWaypoint) -> str:
|
||||||
prefix = ""
|
prefix = ""
|
||||||
time = timing.tot_for_waypoint(flight, waypoint)
|
time = flight.flight_plan.tot_for_waypoint(waypoint)
|
||||||
if time is None:
|
if time is None:
|
||||||
prefix = "Depart "
|
prefix = "Depart "
|
||||||
time = timing.depart_time_for_waypoint(waypoint, self.flight)
|
time = flight.flight_plan.depart_time_for_waypoint(waypoint)
|
||||||
if time is None:
|
if time is None:
|
||||||
return ""
|
return ""
|
||||||
return f"{prefix}T+{datetime.timedelta(seconds=time)}"
|
return f"{prefix}T+{time}"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user