mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Plan waypoint TOTs.
Also fixes the CAP racetracks so the AI actually stays on station. Waypoint TOT assignment happens at mission generation time for the sake of the UI. It's a bit messy since we have the late-initialized field in FlightWaypoint, but on the other hand we don't have to reset every extant waypoint whenever the player adjusts the mission's TOT. If we want to clean this up a bit more, we could have two distinct types for waypoints: one for the planning stage and one with the resolved TOTs. We already do some thing like this with Flight vs FlightData. Future improvements: * Estimate the group's ground speed so we don't need such wide margins of error. * Delay takeoff to cut loiter fuel cost. * Plan mission TOT based on the aircraft in the package and their travel times to the objective. * Tune target area time prediction. Flights often don't need to travel all the way to the target point, and probably won't be doing it slowly, so the current planning causes a lot of extra time spent in enemy territory. * Per-flight TOT offsets from the package to allow a sweep to arrive before the rest, etc.
This commit is contained in:
parent
7abe32be5c
commit
b5e5a3b2da
578
gen/aircraft.py
578
gen/aircraft.py
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@ -28,7 +30,7 @@ from dcs.planes import (
|
|||||||
SpitfireLFMkIXCW,
|
SpitfireLFMkIXCW,
|
||||||
Su_33,
|
Su_33,
|
||||||
)
|
)
|
||||||
from dcs.point import PointAction
|
from dcs.point import MovingPoint, PointAction
|
||||||
from dcs.task import (
|
from dcs.task import (
|
||||||
AntishipStrike,
|
AntishipStrike,
|
||||||
AttackGroup,
|
AttackGroup,
|
||||||
@ -40,8 +42,6 @@ from dcs.task import (
|
|||||||
EngageTargets,
|
EngageTargets,
|
||||||
Escort,
|
Escort,
|
||||||
GroundAttack,
|
GroundAttack,
|
||||||
MainTask,
|
|
||||||
NoTask,
|
|
||||||
OptROE,
|
OptROE,
|
||||||
OptRTBOnBingoFuel,
|
OptRTBOnBingoFuel,
|
||||||
OptRTBOnOutOfAmmo,
|
OptRTBOnOutOfAmmo,
|
||||||
@ -64,10 +64,10 @@ from dcs.unittype import FlyingType, UnitType
|
|||||||
from game import db
|
from game import db
|
||||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
from game.data.cap_capabilities_db import GUNFIGHTERS
|
||||||
from game.settings import Settings
|
from game.settings import Settings
|
||||||
from game.utils import nm_to_meter
|
from game.utils import meter_to_nm, nm_to_meter
|
||||||
from gen.airfields import RunwayData
|
from gen.airfields import RunwayData
|
||||||
from gen.airsupportgen import AirSupport
|
from gen.airsupportgen import AirSupport
|
||||||
from gen.ato import AirTaskingOrder
|
from gen.ato import AirTaskingOrder, Package
|
||||||
from gen.callsigns import create_group_callsign_from_unit
|
from gen.callsigns import create_group_callsign_from_unit
|
||||||
from gen.flights.flight import (
|
from gen.flights.flight import (
|
||||||
Flight,
|
Flight,
|
||||||
@ -76,9 +76,10 @@ from gen.flights.flight import (
|
|||||||
FlightWaypointType,
|
FlightWaypointType,
|
||||||
)
|
)
|
||||||
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
||||||
|
from theater import MissionTarget, TheaterGroundObject
|
||||||
from theater.controlpoint import ControlPoint, ControlPointType
|
from theater.controlpoint import ControlPoint, ControlPointType
|
||||||
from .naming import namegen
|
|
||||||
from .conflictgen import Conflict
|
from .conflictgen import Conflict
|
||||||
|
from .naming import namegen
|
||||||
|
|
||||||
WARM_START_HELI_AIRSPEED = 120
|
WARM_START_HELI_AIRSPEED = 120
|
||||||
WARM_START_HELI_ALT = 500
|
WARM_START_HELI_ALT = 500
|
||||||
@ -532,6 +533,148 @@ AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
|
|||||||
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
|
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def race_track_start(self) -> Optional[int]:
|
||||||
|
cap_types = (FlightType.BARCAP, FlightType.CAP)
|
||||||
|
if self.package.primary_task in cap_types:
|
||||||
|
# CAP flights don't have hold points, and we don't calculate takeoff
|
||||||
|
# times yet or adjust the TOT based on when the flight can arrive,
|
||||||
|
# so if we set a TOT that gives the flight a lot of extra time it
|
||||||
|
# will just fly to the start point slowly, possibly slowly enough to
|
||||||
|
# stall and crash. Just don't set a TOT for these points and let the
|
||||||
|
# CAP get on station ASAP.
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return self.ingress
|
||||||
|
|
||||||
|
@property
|
||||||
|
def race_track_end(self) -> int:
|
||||||
|
cap_types = (FlightType.BARCAP, FlightType.CAP)
|
||||||
|
if self.package.primary_task in cap_types:
|
||||||
|
return self.target + CAP_DURATION * 60
|
||||||
|
else:
|
||||||
|
return self.egress
|
||||||
|
|
||||||
|
def push_time(self, flight: Flight, hold_point: Point) -> int:
|
||||||
|
assert self.package.waypoints is not None
|
||||||
|
return self.join - self.travel_time(
|
||||||
|
hold_point,
|
||||||
|
self.package.waypoints.join,
|
||||||
|
self.flight_ground_speed(flight)
|
||||||
|
)
|
||||||
|
|
||||||
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[int]:
|
||||||
|
target_types = (
|
||||||
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
|
FlightWaypointType.TARGET_POINT,
|
||||||
|
FlightWaypointType.TARGET_SHIP,
|
||||||
|
)
|
||||||
|
|
||||||
|
ingress_types = (
|
||||||
|
FlightWaypointType.INGRESS_CAS,
|
||||||
|
FlightWaypointType.INGRESS_SEAD,
|
||||||
|
FlightWaypointType.INGRESS_STRIKE,
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
return None
|
||||||
|
|
||||||
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
|
||||||
|
flight: Flight) -> Optional[int]:
|
||||||
|
if waypoint.waypoint_type == FlightWaypointType.LOITER:
|
||||||
|
return self.push_time(flight, Point(waypoint.x, waypoint.y))
|
||||||
|
elif waypoint.waypoint_type == FlightWaypointType.PATROL:
|
||||||
|
return self.race_track_end
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_package(cls, package: Package) -> PackageWaypointTiming:
|
||||||
|
assert package.time_over_target is not None
|
||||||
|
assert package.waypoints is not None
|
||||||
|
|
||||||
|
group_ground_speed = cls.package_ground_speed(package)
|
||||||
|
|
||||||
|
ingress = package.time_over_target - cls.travel_time(
|
||||||
|
package.waypoints.ingress,
|
||||||
|
package.target.position,
|
||||||
|
group_ground_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
join = ingress - cls.travel_time(
|
||||||
|
package.waypoints.join,
|
||||||
|
package.waypoints.ingress,
|
||||||
|
group_ground_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
egress = package.time_over_target + cls.travel_time(
|
||||||
|
package.target.position,
|
||||||
|
package.waypoints.egress,
|
||||||
|
group_ground_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
split = egress + cls.travel_time(
|
||||||
|
package.waypoints.egress,
|
||||||
|
package.waypoints.split,
|
||||||
|
group_ground_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls(package, join, ingress, egress, split)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def package_ground_speed(cls, package: Package) -> int:
|
||||||
|
speeds = []
|
||||||
|
for flight in package.flights:
|
||||||
|
speeds.append(cls.flight_ground_speed(flight))
|
||||||
|
return min(speeds) # knots
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def flight_ground_speed(_flight: Flight) -> int:
|
||||||
|
# TODO: Gather data so this is useful.
|
||||||
|
return 400 # knots
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def travel_time(a: Point, b: Point, speed: float) -> int:
|
||||||
|
error_factor = 1.1
|
||||||
|
distance = meter_to_nm(a.distance_to_point(b))
|
||||||
|
hours = distance / speed
|
||||||
|
seconds = hours * 3600
|
||||||
|
return int(seconds * error_factor)
|
||||||
|
|
||||||
|
|
||||||
class AircraftConflictGenerator:
|
class AircraftConflictGenerator:
|
||||||
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
|
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
|
||||||
game, radio_registry: RadioRegistry):
|
game, radio_registry: RadioRegistry):
|
||||||
@ -634,7 +777,8 @@ class AircraftConflictGenerator:
|
|||||||
arrival=departure_runway,
|
arrival=departure_runway,
|
||||||
# TODO: Support for divert airfields.
|
# TODO: Support for divert airfields.
|
||||||
divert=None,
|
divert=None,
|
||||||
waypoints=[first_point] + flight.points,
|
# Waypoints are added later, after they've had their TOTs set.
|
||||||
|
waypoints=[],
|
||||||
intra_flight_channel=channel
|
intra_flight_channel=channel
|
||||||
))
|
))
|
||||||
|
|
||||||
@ -817,6 +961,7 @@ class AircraftConflictGenerator:
|
|||||||
self.clear_parking_slots()
|
self.clear_parking_slots()
|
||||||
|
|
||||||
for package in ato.packages:
|
for package in ato.packages:
|
||||||
|
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:
|
||||||
@ -825,8 +970,7 @@ class AircraftConflictGenerator:
|
|||||||
logging.info(f"Generating flight: {flight.unit_type}")
|
logging.info(f"Generating flight: {flight.unit_type}")
|
||||||
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, flight, flight.flight_type,
|
self.setup_flight_group(group, flight, timing, dynamic_runways)
|
||||||
dynamic_runways)
|
|
||||||
self.setup_group_activation_trigger(flight, group)
|
self.setup_group_activation_trigger(flight, group)
|
||||||
|
|
||||||
def setup_group_activation_trigger(self, flight, group):
|
def setup_group_activation_trigger(self, flight, group):
|
||||||
@ -936,114 +1080,229 @@ class AircraftConflictGenerator:
|
|||||||
flight.group = group
|
flight.group = group
|
||||||
return group
|
return group
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def setup_flight_group(self, group, flight, flight_type,
|
def configure_behavior(
|
||||||
dynamic_runways: Dict[str, RunwayData]):
|
group: FlyingGroup,
|
||||||
|
react_on_threat: Optional[OptReactOnThreat.Values] = None,
|
||||||
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP, FlightType.INTERCEPTION]:
|
roe: Optional[OptROE.Values] = None,
|
||||||
group.task = CAP.name
|
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
|
||||||
self._setup_group(group, CAP, flight, dynamic_runways)
|
restrict_jettison: Optional[bool] = None) -> None:
|
||||||
# group.points[0].tasks.clear()
|
|
||||||
group.points[0].tasks.clear()
|
group.points[0].tasks.clear()
|
||||||
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50), targets=[Targets.All.Air]))
|
if react_on_threat is not None:
|
||||||
# group.tasks.append(EngageTargets(max_distance=nm_to_meter(120), targets=[Targets.All.Air]))
|
group.points[0].tasks.append(OptReactOnThreat(react_on_threat))
|
||||||
if flight.unit_type not in GUNFIGHTERS:
|
if roe is not None:
|
||||||
group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.AAM))
|
group.points[0].tasks.append(OptROE(roe))
|
||||||
else:
|
if restrict_jettison is not None:
|
||||||
group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.Cannon))
|
group.points[0].tasks.append(OptRestrictJettison(restrict_jettison))
|
||||||
|
if rtb_winchester is not None:
|
||||||
elif flight_type in [FlightType.CAS, FlightType.BAI]:
|
group.points[0].tasks.append(OptRTBOnOutOfAmmo(rtb_winchester))
|
||||||
group.task = CAS.name
|
|
||||||
self._setup_group(group, CAS, flight, dynamic_runways)
|
|
||||||
group.points[0].tasks.clear()
|
|
||||||
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(10), targets=[Targets.All.GroundUnits.GroundVehicles]))
|
|
||||||
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
|
|
||||||
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFireWeaponFree))
|
|
||||||
group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.Unguided))
|
|
||||||
group.points[0].tasks.append(OptRestrictJettison(True))
|
|
||||||
elif flight_type in [FlightType.SEAD, FlightType.DEAD]:
|
|
||||||
group.task = SEAD.name
|
|
||||||
self._setup_group(group, SEAD, flight, dynamic_runways)
|
|
||||||
group.points[0].tasks.clear()
|
|
||||||
group.points[0].tasks.append(NoTask())
|
|
||||||
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
|
|
||||||
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
|
|
||||||
group.points[0].tasks.append(OptRestrictJettison(True))
|
|
||||||
group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.ASM))
|
|
||||||
elif flight_type in [FlightType.STRIKE]:
|
|
||||||
group.task = PinpointStrike.name
|
|
||||||
self._setup_group(group, GroundAttack, flight, dynamic_runways)
|
|
||||||
group.points[0].tasks.clear()
|
|
||||||
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
|
|
||||||
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
|
|
||||||
group.points[0].tasks.append(OptRestrictJettison(True))
|
|
||||||
elif flight_type in [FlightType.ANTISHIP]:
|
|
||||||
group.task = AntishipStrike.name
|
|
||||||
self._setup_group(group, AntishipStrike, flight, dynamic_runways)
|
|
||||||
group.points[0].tasks.clear()
|
|
||||||
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
|
|
||||||
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
|
|
||||||
group.points[0].tasks.append(OptRestrictJettison(True))
|
|
||||||
elif flight_type == FlightType.ESCORT:
|
|
||||||
group.task = Escort.name
|
|
||||||
self._setup_group(group, Escort, flight, dynamic_runways)
|
|
||||||
# TODO: Cleanup duplication...
|
|
||||||
group.points[0].tasks.clear()
|
|
||||||
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
|
|
||||||
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
|
|
||||||
group.points[0].tasks.append(OptRestrictJettison(True))
|
|
||||||
|
|
||||||
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
|
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
|
||||||
group.points[0].tasks.append(OptRestrictAfterburner(True))
|
group.points[0].tasks.append(OptRestrictAfterburner(True))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
|
||||||
if hasattr(flight.unit_type, 'eplrs'):
|
if hasattr(flight.unit_type, 'eplrs'):
|
||||||
if flight.unit_type.eplrs:
|
if flight.unit_type.eplrs:
|
||||||
group.points[0].tasks.append(EPLRS(group.id))
|
group.points[0].tasks.append(EPLRS(group.id))
|
||||||
|
|
||||||
for i, point in enumerate(flight.points):
|
def configure_cap(self, group: FlyingGroup, flight: Flight,
|
||||||
if not point.only_for_player or (point.only_for_player and flight.client_count > 0):
|
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||||
pt = group.add_waypoint(Point(point.x, point.y), point.alt)
|
group.task = CAP.name
|
||||||
if point.waypoint_type == FlightWaypointType.PATROL_TRACK:
|
self._setup_group(group, CAP, flight, dynamic_runways)
|
||||||
action = ControlledTask(OrbitAction(altitude=pt.alt, pattern=OrbitAction.OrbitPattern.RaceTrack))
|
|
||||||
action.stop_after_duration(CAP_DURATION * 60)
|
|
||||||
#for tgt in point.targets:
|
|
||||||
# if hasattr(tgt, "position"):
|
|
||||||
# engagetgt = EngageTargetsInZone(tgt.position, radius=CAP_DEFAULT_ENGAGE_DISTANCE, targets=[Targets.All.Air])
|
|
||||||
# pt.tasks.append(engagetgt)
|
|
||||||
elif point.waypoint_type == FlightWaypointType.LANDING_POINT:
|
|
||||||
pt.type = "Land"
|
|
||||||
pt.action = PointAction.Landing
|
|
||||||
elif point.waypoint_type == FlightWaypointType.INGRESS_STRIKE:
|
|
||||||
|
|
||||||
if group.units[0].unit_type == B_17G:
|
if flight.unit_type not in GUNFIGHTERS:
|
||||||
if len(point.targets) > 0:
|
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
|
||||||
bcenter = Point(0,0)
|
|
||||||
for j, t in enumerate(point.targets):
|
|
||||||
bcenter.x += t.position.x
|
|
||||||
bcenter.y += t.position.y
|
|
||||||
bcenter.x = bcenter.x / len(point.targets)
|
|
||||||
bcenter.y = bcenter.y / len(point.targets)
|
|
||||||
bombing = Bombing(bcenter)
|
|
||||||
bombing.params["expend"] = "All"
|
|
||||||
bombing.params["attackQtyLimit"] = False
|
|
||||||
bombing.params["directionEnabled"] = False
|
|
||||||
bombing.params["altitudeEnabled"] = False
|
|
||||||
bombing.params["weaponType"] = 2032
|
|
||||||
bombing.params["groupAttack"] = True
|
|
||||||
pt.tasks.append(bombing)
|
|
||||||
else:
|
else:
|
||||||
for j, t in enumerate(point.targets):
|
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
||||||
print(t.position)
|
|
||||||
pt.tasks.append(Bombing(t.position))
|
|
||||||
if group.units[0].unit_type == JF_17 and j < 4:
|
|
||||||
group.add_nav_target_point(t.position, "PP" + str(j + 1))
|
|
||||||
if group.units[0].unit_type == F_14B and j == 0:
|
|
||||||
group.add_nav_target_point(t.position, "ST")
|
|
||||||
if group.units[0].unit_type == AJS37 and j < 9:
|
|
||||||
group.add_nav_target_point(t.position, "M" + str(j + 1))
|
|
||||||
elif point.waypoint_type == FlightWaypointType.INGRESS_SEAD:
|
|
||||||
|
|
||||||
tgroup = self.m.find_group(point.targetGroup.group_identifier)
|
self.configure_behavior(group, rtb_winchester=ammo_type)
|
||||||
|
|
||||||
|
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50),
|
||||||
|
targets=[Targets.All.Air]))
|
||||||
|
|
||||||
|
def configure_cas(self, group: FlyingGroup, flight: Flight,
|
||||||
|
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||||
|
group.task = CAS.name
|
||||||
|
self._setup_group(group, CAS, flight, dynamic_runways)
|
||||||
|
self.configure_behavior(
|
||||||
|
group,
|
||||||
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||||
|
roe=OptROE.Values.OpenFireWeaponFree,
|
||||||
|
rtb_winchester=OptRTBOnOutOfAmmo.Values.Unguided,
|
||||||
|
restrict_jettison=True)
|
||||||
|
group.points[0].tasks.append(
|
||||||
|
EngageTargets(max_distance=nm_to_meter(10),
|
||||||
|
targets=[Targets.All.GroundUnits.GroundVehicles])
|
||||||
|
)
|
||||||
|
|
||||||
|
def configure_sead(self, group: FlyingGroup, flight: Flight,
|
||||||
|
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||||
|
group.task = SEAD.name
|
||||||
|
self._setup_group(group, SEAD, flight, dynamic_runways)
|
||||||
|
self.configure_behavior(
|
||||||
|
group,
|
||||||
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||||
|
roe=OptROE.Values.OpenFire,
|
||||||
|
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
|
||||||
|
restrict_jettison=True)
|
||||||
|
|
||||||
|
def configure_strike(self, group: FlyingGroup, flight: Flight,
|
||||||
|
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||||
|
group.task = PinpointStrike.name
|
||||||
|
self._setup_group(group, GroundAttack, flight, dynamic_runways)
|
||||||
|
self.configure_behavior(
|
||||||
|
group,
|
||||||
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||||
|
roe=OptROE.Values.OpenFire,
|
||||||
|
restrict_jettison=True)
|
||||||
|
|
||||||
|
def configure_anti_ship(self, group: FlyingGroup, flight: Flight,
|
||||||
|
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||||
|
group.task = AntishipStrike.name
|
||||||
|
self._setup_group(group, AntishipStrike, flight, dynamic_runways)
|
||||||
|
self.configure_behavior(
|
||||||
|
group,
|
||||||
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||||
|
roe=OptROE.Values.OpenFire,
|
||||||
|
restrict_jettison=True)
|
||||||
|
|
||||||
|
def configure_escort(self, group: FlyingGroup, flight: Flight,
|
||||||
|
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||||
|
group.task = Escort.name
|
||||||
|
self._setup_group(group, Escort, flight, dynamic_runways)
|
||||||
|
self.configure_behavior(group, roe=OptROE.Values.OpenFire,
|
||||||
|
restrict_jettison=True)
|
||||||
|
|
||||||
|
def configure_unknown_task(self, group: FlyingGroup,
|
||||||
|
flight: Flight) -> None:
|
||||||
|
logging.error(f"Unhandled flight type: {flight.flight_type.name}")
|
||||||
|
self.configure_behavior(group)
|
||||||
|
|
||||||
|
def setup_flight_group(self, group: FlyingGroup, flight: Flight,
|
||||||
|
timing: PackageWaypointTiming,
|
||||||
|
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||||
|
flight_type = flight.flight_type
|
||||||
|
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP,
|
||||||
|
FlightType.INTERCEPTION]:
|
||||||
|
self.configure_cap(group, flight, dynamic_runways)
|
||||||
|
elif flight_type in [FlightType.CAS, FlightType.BAI]:
|
||||||
|
self.configure_cas(group, flight, dynamic_runways)
|
||||||
|
elif flight_type in [FlightType.SEAD, FlightType.DEAD]:
|
||||||
|
self.configure_sead(group, flight, dynamic_runways)
|
||||||
|
elif flight_type in [FlightType.STRIKE]:
|
||||||
|
self.configure_strike(group, flight, dynamic_runways)
|
||||||
|
elif flight_type in [FlightType.ANTISHIP]:
|
||||||
|
self.configure_anti_ship(group, flight, dynamic_runways)
|
||||||
|
elif flight_type == FlightType.ESCORT:
|
||||||
|
self.configure_escort(group, flight, dynamic_runways)
|
||||||
|
else:
|
||||||
|
self.configure_unknown_task(group, flight)
|
||||||
|
|
||||||
|
self.configure_eplrs(group, flight)
|
||||||
|
|
||||||
|
for waypoint in flight.points:
|
||||||
|
waypoint.tot = None
|
||||||
|
|
||||||
|
for point in flight.points:
|
||||||
|
if point.only_for_player and not flight.client_count:
|
||||||
|
continue
|
||||||
|
|
||||||
|
PydcsWaypointBuilder.for_waypoint(
|
||||||
|
point, group, flight, timing, self.m
|
||||||
|
).build()
|
||||||
|
|
||||||
|
# Set here rather than when the FlightData is created so they waypoints
|
||||||
|
# have their TOTs set.
|
||||||
|
self.flights[-1].waypoints = flight.points
|
||||||
|
self._setup_custom_payload(flight, group)
|
||||||
|
|
||||||
|
|
||||||
|
class PydcsWaypointBuilder:
|
||||||
|
def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup,
|
||||||
|
flight: Flight, timing: PackageWaypointTiming,
|
||||||
|
mission: Mission) -> None:
|
||||||
|
self.waypoint = waypoint
|
||||||
|
self.group = group
|
||||||
|
self.flight = flight
|
||||||
|
self.timing = timing
|
||||||
|
self.mission = mission
|
||||||
|
|
||||||
|
def build(self) -> MovingPoint:
|
||||||
|
waypoint = self.group.add_waypoint(
|
||||||
|
Point(self.waypoint.x, self.waypoint.y), self.waypoint.alt)
|
||||||
|
|
||||||
|
waypoint.alt_type = self.waypoint.alt_type
|
||||||
|
waypoint.name = String(self.waypoint.name)
|
||||||
|
return waypoint
|
||||||
|
|
||||||
|
def set_waypoint_tot(self, waypoint: MovingPoint, tot: int) -> None:
|
||||||
|
self.waypoint.tot = tot
|
||||||
|
waypoint.ETA = tot
|
||||||
|
waypoint.ETA_locked = True
|
||||||
|
waypoint.speed_locked = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_waypoint(cls, waypoint: FlightWaypoint,
|
||||||
|
group: FlyingGroup,
|
||||||
|
flight: Flight,
|
||||||
|
timing: PackageWaypointTiming,
|
||||||
|
mission: Mission) -> PydcsWaypointBuilder:
|
||||||
|
builders = {
|
||||||
|
FlightWaypointType.EGRESS: EgressPointBuilder,
|
||||||
|
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
|
||||||
|
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
|
||||||
|
FlightWaypointType.JOIN: JoinPointBuilder,
|
||||||
|
FlightWaypointType.LANDING_POINT: LandingPointBuilder,
|
||||||
|
FlightWaypointType.LOITER: HoldPointBuilder,
|
||||||
|
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)
|
||||||
|
return builder(waypoint, group, flight, timing, mission)
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultWaypointBuilder(PydcsWaypointBuilder):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HoldPointBuilder(PydcsWaypointBuilder):
|
||||||
|
def build(self) -> MovingPoint:
|
||||||
|
waypoint = super().build()
|
||||||
|
loiter = ControlledTask(OrbitAction(
|
||||||
|
altitude=waypoint.alt,
|
||||||
|
pattern=OrbitAction.OrbitPattern.Circle
|
||||||
|
))
|
||||||
|
loiter.stop_after_time(
|
||||||
|
self.timing.push_time(self.flight, waypoint.position))
|
||||||
|
waypoint.add_task(loiter)
|
||||||
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
|
class EgressPointBuilder(PydcsWaypointBuilder):
|
||||||
|
def build(self) -> MovingPoint:
|
||||||
|
waypoint = super().build()
|
||||||
|
self.set_waypoint_tot(waypoint, self.timing.egress)
|
||||||
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
|
class IngressBuilder(PydcsWaypointBuilder):
|
||||||
|
def build(self) -> MovingPoint:
|
||||||
|
waypoint = super().build()
|
||||||
|
self.set_waypoint_tot(waypoint, self.timing.ingress)
|
||||||
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
|
class SeadIngressBuilder(IngressBuilder):
|
||||||
|
def build(self) -> MovingPoint:
|
||||||
|
waypoint = super().build()
|
||||||
|
|
||||||
|
target_group = self.waypoint.targetGroup
|
||||||
|
if isinstance(target_group, TheaterGroundObject):
|
||||||
|
tgroup = self.mission.find_group(target_group.group_identifier)
|
||||||
if tgroup is not None:
|
if tgroup is not None:
|
||||||
task = AttackGroup(tgroup.id)
|
task = AttackGroup(tgroup.id)
|
||||||
task.params["expend"] = "All"
|
task.params["expend"] = "All"
|
||||||
@ -1052,18 +1311,103 @@ class AircraftConflictGenerator:
|
|||||||
task.params["altitudeEnabled"] = False
|
task.params["altitudeEnabled"] = False
|
||||||
task.params["weaponType"] = 268402702 # Guided Weapons
|
task.params["weaponType"] = 268402702 # Guided Weapons
|
||||||
task.params["groupAttack"] = True
|
task.params["groupAttack"] = True
|
||||||
pt.tasks.append(task)
|
waypoint.tasks.append(task)
|
||||||
|
|
||||||
for j, t in enumerate(point.targets):
|
for i, t in enumerate(self.waypoint.targets):
|
||||||
if group.units[0].unit_type == JF_17 and j < 4:
|
if self.group.units[0].unit_type == JF_17 and i < 4:
|
||||||
group.add_nav_target_point(t.position, "PP" + str(j + 1))
|
self.group.add_nav_target_point(t.position, "PP" + str(i + 1))
|
||||||
if group.units[0].unit_type == F_14B and j == 0:
|
if self.group.units[0].unit_type == F_14B and i == 0:
|
||||||
group.add_nav_target_point(t.position, "ST")
|
self.group.add_nav_target_point(t.position, "ST")
|
||||||
if group.units[0].unit_type == AJS37 and j < 9:
|
if self.group.units[0].unit_type == AJS37 and i < 9:
|
||||||
group.add_nav_target_point(t.position, "M" + str(j + 1))
|
self.group.add_nav_target_point(t.position, "M" + str(i + 1))
|
||||||
|
return waypoint
|
||||||
|
|
||||||
if pt is not None:
|
|
||||||
pt.alt_type = point.alt_type
|
|
||||||
pt.name = String(point.name)
|
|
||||||
|
|
||||||
self._setup_custom_payload(flight, group)
|
class StrikeIngressBuilder(IngressBuilder):
|
||||||
|
def build(self) -> MovingPoint:
|
||||||
|
if self.group.units[0].unit_type == B_17G:
|
||||||
|
return self.build_bombing()
|
||||||
|
else:
|
||||||
|
return self.build_strike()
|
||||||
|
|
||||||
|
def build_bombing(self) -> MovingPoint:
|
||||||
|
waypoint = super().build()
|
||||||
|
|
||||||
|
targets = self.waypoint.targets
|
||||||
|
if not targets:
|
||||||
|
return waypoint
|
||||||
|
|
||||||
|
center = Point(0, 0)
|
||||||
|
for target in targets:
|
||||||
|
center.x += target.position.x
|
||||||
|
center.y += target.position.y
|
||||||
|
center.x /= len(targets)
|
||||||
|
center.y /= len(targets)
|
||||||
|
bombing = Bombing(center)
|
||||||
|
bombing.params["expend"] = "All"
|
||||||
|
bombing.params["attackQtyLimit"] = False
|
||||||
|
bombing.params["directionEnabled"] = False
|
||||||
|
bombing.params["altitudeEnabled"] = False
|
||||||
|
bombing.params["weaponType"] = 2032
|
||||||
|
bombing.params["groupAttack"] = True
|
||||||
|
waypoint.tasks.append(bombing)
|
||||||
|
return waypoint
|
||||||
|
|
||||||
|
def build_strike(self) -> MovingPoint:
|
||||||
|
waypoint = super().build()
|
||||||
|
|
||||||
|
for i, t in enumerate(self.waypoint.targets):
|
||||||
|
waypoint.tasks.append(Bombing(t.position))
|
||||||
|
if self.group.units[0].unit_type == JF_17 and i < 4:
|
||||||
|
self.group.add_nav_target_point(t.position, "PP" + str(i + 1))
|
||||||
|
if self.group.units[0].unit_type == F_14B and i == 0:
|
||||||
|
self.group.add_nav_target_point(t.position, "ST")
|
||||||
|
if self.group.units[0].unit_type == AJS37 and i < 9:
|
||||||
|
self.group.add_nav_target_point(t.position, "M" + str(i + 1))
|
||||||
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
|
class JoinPointBuilder(PydcsWaypointBuilder):
|
||||||
|
def build(self) -> MovingPoint:
|
||||||
|
waypoint = super().build()
|
||||||
|
self.set_waypoint_tot(waypoint, self.timing.join)
|
||||||
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
|
class LandingPointBuilder(PydcsWaypointBuilder):
|
||||||
|
def build(self) -> MovingPoint:
|
||||||
|
waypoint = super().build()
|
||||||
|
waypoint.type = "Land"
|
||||||
|
waypoint.action = PointAction.Landing
|
||||||
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
|
class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||||
|
def build(self) -> MovingPoint:
|
||||||
|
waypoint = super().build()
|
||||||
|
|
||||||
|
racetrack = ControlledTask(OrbitAction(
|
||||||
|
altitude=waypoint.alt,
|
||||||
|
pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||||
|
))
|
||||||
|
|
||||||
|
start = self.timing.race_track_start
|
||||||
|
if start is not None:
|
||||||
|
self.set_waypoint_tot(waypoint, start)
|
||||||
|
racetrack.stop_after_time(self.timing.race_track_end)
|
||||||
|
waypoint.add_task(racetrack)
|
||||||
|
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
|
||||||
|
|||||||
26
gen/ato.py
26
gen/ato.py
@ -8,14 +8,15 @@ example, the package to strike an enemy airfield may contain an escort flight,
|
|||||||
a SEAD flight, and the strike aircraft themselves. CAP packages may contain only
|
a SEAD flight, and the strike aircraft themselves. CAP packages may contain only
|
||||||
the single CAP flight.
|
the single CAP flight.
|
||||||
"""
|
"""
|
||||||
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
import logging
|
from typing import Dict, List, Optional
|
||||||
from typing import Dict, Iterator, List, Optional
|
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from .flights.flight import Flight, FlightType
|
|
||||||
from theater.missiontarget import MissionTarget
|
from theater.missiontarget import MissionTarget
|
||||||
|
from .flights.flight import Flight, FlightType
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -29,6 +30,14 @@ class Task:
|
|||||||
location: str
|
location: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PackageWaypoints:
|
||||||
|
join: Point
|
||||||
|
ingress: Point
|
||||||
|
egress: Point
|
||||||
|
split: Point
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Package:
|
class Package:
|
||||||
"""A mission package."""
|
"""A mission package."""
|
||||||
@ -42,10 +51,10 @@ class Package:
|
|||||||
|
|
||||||
delay: int = field(default=0)
|
delay: int = field(default=0)
|
||||||
|
|
||||||
join_point: Optional[Point] = field(default=None, init=False, hash=False)
|
#: Desired TOT measured in seconds from mission start.
|
||||||
split_point: Optional[Point] = field(default=None, init=False, hash=False)
|
time_over_target: Optional[int] = field(default=None)
|
||||||
ingress_point: Optional[Point] = field(default=None, init=False, hash=False)
|
|
||||||
egress_point: Optional[Point] = field(default=None, init=False, hash=False)
|
waypoints: Optional[PackageWaypoints] = field(default=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."""
|
||||||
@ -55,8 +64,7 @@ class Package:
|
|||||||
"""Removes a flight from the package."""
|
"""Removes a flight from the package."""
|
||||||
self.flights.remove(flight)
|
self.flights.remove(flight)
|
||||||
if not self.flights:
|
if not self.flights:
|
||||||
self.ingress_point = None
|
self.waypoints = None
|
||||||
self.egress_point = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def primary_task(self) -> Optional[FlightType]:
|
def primary_task(self) -> Optional[FlightType]:
|
||||||
|
|||||||
@ -12,7 +12,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
|
from gen import Conflict, PackageWaypointTiming
|
||||||
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,
|
||||||
@ -496,8 +496,15 @@ class CoalitionMissionPlanner:
|
|||||||
latest=90,
|
latest=90,
|
||||||
margin=5
|
margin=5
|
||||||
)
|
)
|
||||||
for package in non_dca_packages:
|
for package in self.ato.packages:
|
||||||
|
if package.primary_task in dca_types:
|
||||||
|
# All CAP missions should be on station in 15-25 minutes.
|
||||||
|
package.time_over_target = (20 + random.randint(-5, 5)) * 60
|
||||||
|
else:
|
||||||
|
# But other packages should be spread out a bit.
|
||||||
package.delay = next(start_time)
|
package.delay = next(start_time)
|
||||||
|
# TODO: Compute TOT based on package.
|
||||||
|
package.time_over_target = (package.delay + 40) * 60
|
||||||
for flight in package.flights:
|
for flight in package.flights:
|
||||||
flight.scheduled_in = package.delay
|
flight.scheduled_in = package.delay
|
||||||
|
|
||||||
|
|||||||
@ -49,6 +49,7 @@ class FlightWaypointType(Enum):
|
|||||||
CUSTOM = 15 # User waypoint (no specific behaviour)
|
CUSTOM = 15 # User waypoint (no specific behaviour)
|
||||||
JOIN = 16
|
JOIN = 16
|
||||||
SPLIT = 17
|
SPLIT = 17
|
||||||
|
LOITER = 18
|
||||||
|
|
||||||
|
|
||||||
class PredefinedWaypointCategory(Enum):
|
class PredefinedWaypointCategory(Enum):
|
||||||
@ -66,6 +67,15 @@ class FlightWaypoint:
|
|||||||
|
|
||||||
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
|
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
|
||||||
alt: int = 0) -> None:
|
alt: int = 0) -> None:
|
||||||
|
"""Creates a flight waypoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
waypoint_type: The waypoint type.
|
||||||
|
x: X cooidinate of the waypoint.
|
||||||
|
y: Y coordinate of the waypoint.
|
||||||
|
alt: Altitude of the waypoint. By default this is AGL, but it can be
|
||||||
|
changed to MSL by setting alt_type to "RADIO".
|
||||||
|
"""
|
||||||
self.waypoint_type = waypoint_type
|
self.waypoint_type = waypoint_type
|
||||||
self.x = x
|
self.x = x
|
||||||
self.y = y
|
self.y = y
|
||||||
@ -81,6 +91,12 @@ class FlightWaypoint:
|
|||||||
self.only_for_player = False
|
self.only_for_player = False
|
||||||
self.data = None
|
self.data = None
|
||||||
|
|
||||||
|
# This is 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
|
||||||
|
# to waypoint times whenever the player alters the package TOT or the
|
||||||
|
# flight's offset in the UI.
|
||||||
|
self.tot: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_pydcs(cls, point: MovingPoint,
|
def from_pydcs(cls, point: MovingPoint,
|
||||||
|
|||||||
@ -16,10 +16,10 @@ from dcs.unit import Unit
|
|||||||
|
|
||||||
from game.data.doctrine import Doctrine, MODERN_DOCTRINE
|
from game.data.doctrine import Doctrine, MODERN_DOCTRINE
|
||||||
from game.utils import nm_to_meter
|
from game.utils import nm_to_meter
|
||||||
from gen.ato import Package
|
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, FlightWaypointType
|
from .flight import Flight, FlightType, FlightWaypoint
|
||||||
from .waypointbuilder import WaypointBuilder
|
from .waypointbuilder import WaypointBuilder
|
||||||
from ..conflictgen import Conflict
|
from ..conflictgen import Conflict
|
||||||
|
|
||||||
@ -55,7 +55,8 @@ class FlightPlanBuilder:
|
|||||||
"""Creates a default flight plan for the given mission."""
|
"""Creates a default flight plan for the given mission."""
|
||||||
if flight not in self.package.flights:
|
if flight not in self.package.flights:
|
||||||
raise RuntimeError("Flight must be a part of the package")
|
raise RuntimeError("Flight must be a part of the package")
|
||||||
self.generate_missing_package_waypoints()
|
if self.package.waypoints is None:
|
||||||
|
self.regenerate_package_waypoints()
|
||||||
|
|
||||||
# TODO: Flesh out mission types.
|
# TODO: Flesh out mission types.
|
||||||
try:
|
try:
|
||||||
@ -105,15 +106,18 @@ class FlightPlanBuilder:
|
|||||||
except InvalidObjectiveLocation as ex:
|
except InvalidObjectiveLocation as ex:
|
||||||
logging.error(f"Could not create flight plan: {ex}")
|
logging.error(f"Could not create flight plan: {ex}")
|
||||||
|
|
||||||
def generate_missing_package_waypoints(self) -> None:
|
def regenerate_package_waypoints(self) -> None:
|
||||||
if self.package.ingress_point is None:
|
ingress_point = self._ingress_point()
|
||||||
self.package.ingress_point = self._ingress_point()
|
egress_point = self._egress_point()
|
||||||
if self.package.egress_point is None:
|
join_point = self._join_point(ingress_point)
|
||||||
self.package.egress_point = self._egress_point()
|
split_point = self._split_point(egress_point)
|
||||||
if self.package.join_point is None:
|
|
||||||
self.package.join_point = self._join_point()
|
self.package.waypoints = PackageWaypoints(
|
||||||
if self.package.split_point is None:
|
join_point,
|
||||||
self.package.split_point = self._split_point()
|
ingress_point,
|
||||||
|
egress_point,
|
||||||
|
split_point,
|
||||||
|
)
|
||||||
|
|
||||||
def generate_strike(self, flight: Flight) -> None:
|
def generate_strike(self, flight: Flight) -> None:
|
||||||
"""Generates a strike flight plan.
|
"""Generates a strike flight plan.
|
||||||
@ -121,6 +125,7 @@ class FlightPlanBuilder:
|
|||||||
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.
|
||||||
@ -129,8 +134,9 @@ class FlightPlanBuilder:
|
|||||||
|
|
||||||
builder = WaypointBuilder(self.doctrine)
|
builder = WaypointBuilder(self.doctrine)
|
||||||
builder.ascent(flight.from_cp)
|
builder.ascent(flight.from_cp)
|
||||||
builder.join(self.package.join_point)
|
builder.hold(self._hold_point(flight))
|
||||||
builder.ingress_strike(self.package.ingress_point, location)
|
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?
|
||||||
@ -157,8 +163,8 @@ class FlightPlanBuilder:
|
|||||||
location
|
location
|
||||||
)
|
)
|
||||||
|
|
||||||
builder.egress(self.package.egress_point, location)
|
builder.egress(self.package.waypoints.egress, location)
|
||||||
builder.split(self.package.split_point)
|
builder.split(self.package.waypoints.split)
|
||||||
builder.rtb(flight.from_cp)
|
builder.rtb(flight.from_cp)
|
||||||
|
|
||||||
flight.points = builder.build()
|
flight.points = builder.build()
|
||||||
@ -215,6 +221,7 @@ class FlightPlanBuilder:
|
|||||||
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):
|
||||||
@ -246,7 +253,10 @@ class FlightPlanBuilder:
|
|||||||
# Create points
|
# Create points
|
||||||
builder = WaypointBuilder(self.doctrine)
|
builder = WaypointBuilder(self.doctrine)
|
||||||
builder.ascent(flight.from_cp)
|
builder.ascent(flight.from_cp)
|
||||||
|
builder.hold(self._hold_point(flight))
|
||||||
|
builder.join(self.package.waypoints.join)
|
||||||
builder.race_track(orbit0p, orbit1p, patrol_alt)
|
builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||||
|
builder.split(self.package.waypoints.split)
|
||||||
builder.rtb(flight.from_cp)
|
builder.rtb(flight.from_cp)
|
||||||
flight.points = builder.build()
|
flight.points = builder.build()
|
||||||
|
|
||||||
@ -258,6 +268,7 @@ class FlightPlanBuilder:
|
|||||||
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):
|
||||||
@ -268,21 +279,15 @@ class FlightPlanBuilder:
|
|||||||
|
|
||||||
builder = WaypointBuilder(self.doctrine)
|
builder = WaypointBuilder(self.doctrine)
|
||||||
builder.ascent(flight.from_cp)
|
builder.ascent(flight.from_cp)
|
||||||
builder.join(self.package.join_point)
|
builder.hold(self._hold_point(flight))
|
||||||
builder.ingress_sead(self.package.ingress_point, location)
|
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:
|
if custom_targets:
|
||||||
for target in custom_targets:
|
for target in custom_targets:
|
||||||
point = FlightWaypoint(
|
|
||||||
FlightWaypointType.TARGET_POINT,
|
|
||||||
target.position.x,
|
|
||||||
target.position.y,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
point.alt_type = "RADIO"
|
|
||||||
if flight.flight_type == FlightType.DEAD:
|
if flight.flight_type == FlightType.DEAD:
|
||||||
builder.dead_point(target, location.name, location)
|
builder.dead_point(target, location.name, location)
|
||||||
else:
|
else:
|
||||||
@ -293,17 +298,22 @@ class FlightPlanBuilder:
|
|||||||
else:
|
else:
|
||||||
builder.sead_area(location)
|
builder.sead_area(location)
|
||||||
|
|
||||||
builder.egress(self.package.egress_point, location)
|
builder.egress(self.package.waypoints.egress, location)
|
||||||
builder.split(self.package.split_point)
|
builder.split(self.package.waypoints.split)
|
||||||
builder.rtb(flight.from_cp)
|
builder.rtb(flight.from_cp)
|
||||||
|
|
||||||
flight.points = builder.build()
|
flight.points = builder.build()
|
||||||
|
|
||||||
|
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:
|
def generate_escort(self, flight: Flight) -> None:
|
||||||
# TODO: Decide common waypoints for the package ahead of time.
|
assert self.package.waypoints is not None
|
||||||
# Packages should determine some common points like push, ingress,
|
|
||||||
# egress, and split points ahead of time so they can be shared by all
|
|
||||||
# flights.
|
|
||||||
|
|
||||||
patrol_alt = random.randint(
|
patrol_alt = random.randint(
|
||||||
self.doctrine.min_patrol_altitude,
|
self.doctrine.min_patrol_altitude,
|
||||||
@ -312,13 +322,11 @@ class FlightPlanBuilder:
|
|||||||
|
|
||||||
builder = WaypointBuilder(self.doctrine)
|
builder = WaypointBuilder(self.doctrine)
|
||||||
builder.ascent(flight.from_cp)
|
builder.ascent(flight.from_cp)
|
||||||
builder.join(self.package.join_point)
|
builder.hold(self._hold_point(flight))
|
||||||
builder.race_track(
|
builder.join(self.package.waypoints.join)
|
||||||
self.package.ingress_point,
|
builder.race_track(self.package.waypoints.ingress,
|
||||||
self.package.egress_point,
|
self.package.waypoints.egress, patrol_alt)
|
||||||
patrol_alt
|
builder.split(self.package.waypoints.split)
|
||||||
)
|
|
||||||
builder.split(self.package.split_point)
|
|
||||||
builder.rtb(flight.from_cp)
|
builder.rtb(flight.from_cp)
|
||||||
|
|
||||||
flight.points = builder.build()
|
flight.points = builder.build()
|
||||||
@ -329,6 +337,7 @@ class FlightPlanBuilder:
|
|||||||
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):
|
||||||
@ -346,11 +355,12 @@ class FlightPlanBuilder:
|
|||||||
|
|
||||||
builder = WaypointBuilder(self.doctrine)
|
builder = WaypointBuilder(self.doctrine)
|
||||||
builder.ascent(flight.from_cp, is_helo)
|
builder.ascent(flight.from_cp, is_helo)
|
||||||
builder.join(self.package.join_point)
|
builder.hold(self._hold_point(flight))
|
||||||
|
builder.join(self.package.waypoints.join)
|
||||||
builder.ingress_cas(ingress, location)
|
builder.ingress_cas(ingress, location)
|
||||||
builder.cas(center, cap_alt)
|
builder.cas(center, cap_alt)
|
||||||
builder.egress(egress, location)
|
builder.egress(egress, location)
|
||||||
builder.split(self.package.split_point)
|
builder.split(self.package.waypoints.split)
|
||||||
builder.rtb(flight.from_cp, is_helo)
|
builder.rtb(flight.from_cp, is_helo)
|
||||||
|
|
||||||
flight.points = builder.build()
|
flight.points = builder.build()
|
||||||
@ -386,16 +396,12 @@ class FlightPlanBuilder:
|
|||||||
builder.land(arrival)
|
builder.land(arrival)
|
||||||
return builder.build()[0]
|
return builder.build()[0]
|
||||||
|
|
||||||
def _join_point(self) -> Point:
|
def _join_point(self, ingress_point: Point) -> Point:
|
||||||
ingress_point = self.package.ingress_point
|
|
||||||
assert ingress_point is not None
|
|
||||||
heading = self._heading_to_package_airfield(ingress_point)
|
heading = self._heading_to_package_airfield(ingress_point)
|
||||||
return ingress_point.point_from_heading(heading,
|
return ingress_point.point_from_heading(heading,
|
||||||
-self.doctrine.join_distance)
|
-self.doctrine.join_distance)
|
||||||
|
|
||||||
def _split_point(self) -> Point:
|
def _split_point(self, egress_point: Point) -> Point:
|
||||||
egress_point = self.package.egress_point
|
|
||||||
assert egress_point is not None
|
|
||||||
heading = self._heading_to_package_airfield(egress_point)
|
heading = self._heading_to_package_airfield(egress_point)
|
||||||
return egress_point.point_from_heading(heading,
|
return egress_point.point_from_heading(heading,
|
||||||
-self.doctrine.split_distance)
|
-self.doctrine.split_distance)
|
||||||
|
|||||||
@ -25,6 +25,7 @@ class WaypointBuilder:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
departure: Departure airfield or carrier.
|
departure: Departure airfield or carrier.
|
||||||
|
is_helo: True if the flight is a helicopter.
|
||||||
"""
|
"""
|
||||||
# TODO: Pick runway based on wind direction.
|
# TODO: Pick runway based on wind direction.
|
||||||
heading = departure.heading
|
heading = departure.heading
|
||||||
@ -48,6 +49,7 @@ class WaypointBuilder:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
arrival: Arrival airfield or carrier.
|
arrival: Arrival airfield or carrier.
|
||||||
|
is_helo: True if the flight is a helicopter.
|
||||||
"""
|
"""
|
||||||
# TODO: Pick runway based on wind direction.
|
# TODO: Pick runway based on wind direction.
|
||||||
# ControlPoint.heading is the departure heading.
|
# ControlPoint.heading is the departure heading.
|
||||||
@ -86,6 +88,18 @@ class WaypointBuilder:
|
|||||||
waypoint.pretty_name = "Land"
|
waypoint.pretty_name = "Land"
|
||||||
self.waypoints.append(waypoint)
|
self.waypoints.append(waypoint)
|
||||||
|
|
||||||
|
def hold(self, position: Point) -> None:
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.LOITER,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
self.doctrine.rendezvous_altitude
|
||||||
|
)
|
||||||
|
waypoint.pretty_name = "Hold"
|
||||||
|
waypoint.description = "Wait until push time"
|
||||||
|
waypoint.name = "HOLD"
|
||||||
|
self.waypoints.append(waypoint)
|
||||||
|
|
||||||
def join(self, position: Point) -> None:
|
def join(self, position: Point) -> None:
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.JOIN,
|
FlightWaypointType.JOIN,
|
||||||
@ -293,6 +307,7 @@ class WaypointBuilder:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
arrival: Arrival airfield or carrier.
|
arrival: Arrival airfield or carrier.
|
||||||
|
is_helo: True if the flight is a helicopter.
|
||||||
"""
|
"""
|
||||||
self.descent(arrival, is_helo)
|
self.descent(arrival, is_helo)
|
||||||
self.land(arrival)
|
self.land(arrival)
|
||||||
|
|||||||
@ -24,6 +24,7 @@ aircraft will be able to see the enemy's kneeboard for the same airframe.
|
|||||||
"""
|
"""
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
@ -106,7 +107,8 @@ class NumberedWaypoint:
|
|||||||
|
|
||||||
|
|
||||||
class FlightPlanBuilder:
|
class FlightPlanBuilder:
|
||||||
def __init__(self) -> None:
|
def __init__(self, start_time: datetime.datetime) -> None:
|
||||||
|
self.start_time = start_time
|
||||||
self.rows: List[List[str]] = []
|
self.rows: List[List[str]] = []
|
||||||
self.target_points: List[NumberedWaypoint] = []
|
self.target_points: List[NumberedWaypoint] = []
|
||||||
|
|
||||||
@ -133,16 +135,24 @@ class FlightPlanBuilder:
|
|||||||
self.rows.append([
|
self.rows.append([
|
||||||
f"{first_waypoint_num}-{last_waypoint_num}",
|
f"{first_waypoint_num}-{last_waypoint_num}",
|
||||||
"Target points",
|
"Target points",
|
||||||
"0"
|
"0",
|
||||||
|
self._format_time(self.target_points[0].waypoint.tot),
|
||||||
])
|
])
|
||||||
|
|
||||||
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
|
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
|
||||||
self.rows.append([
|
self.rows.append([
|
||||||
str(waypoint.number),
|
str(waypoint.number),
|
||||||
waypoint.waypoint.pretty_name,
|
waypoint.waypoint.pretty_name,
|
||||||
str(int(units.meters_to_feet(waypoint.waypoint.alt)))
|
str(int(units.meters_to_feet(waypoint.waypoint.alt))),
|
||||||
|
self._format_time(waypoint.waypoint.tot),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
def _format_time(self, time: Optional[int]) -> str:
|
||||||
|
if time is None:
|
||||||
|
return ""
|
||||||
|
local_time = self.start_time + datetime.timedelta(seconds=time)
|
||||||
|
return local_time.strftime(f"%H:%M:%S LOCAL")
|
||||||
|
|
||||||
def build(self) -> List[List[str]]:
|
def build(self) -> List[List[str]]:
|
||||||
return self.rows
|
return self.rows
|
||||||
|
|
||||||
@ -151,12 +161,13 @@ class BriefingPage(KneeboardPage):
|
|||||||
"""A kneeboard page containing briefing information."""
|
"""A kneeboard page containing briefing information."""
|
||||||
def __init__(self, flight: FlightData, comms: List[CommInfo],
|
def __init__(self, flight: FlightData, comms: List[CommInfo],
|
||||||
awacs: List[AwacsInfo], tankers: List[TankerInfo],
|
awacs: List[AwacsInfo], tankers: List[TankerInfo],
|
||||||
jtacs: List[JtacInfo]) -> None:
|
jtacs: List[JtacInfo], start_time: datetime.datetime) -> None:
|
||||||
self.flight = flight
|
self.flight = flight
|
||||||
self.comms = list(comms)
|
self.comms = list(comms)
|
||||||
self.awacs = awacs
|
self.awacs = awacs
|
||||||
self.tankers = tankers
|
self.tankers = tankers
|
||||||
self.jtacs = jtacs
|
self.jtacs = jtacs
|
||||||
|
self.start_time = start_time
|
||||||
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
|
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
|
||||||
|
|
||||||
def write(self, path: Path) -> None:
|
def write(self, path: Path) -> None:
|
||||||
@ -172,11 +183,11 @@ class BriefingPage(KneeboardPage):
|
|||||||
], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"])
|
], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"])
|
||||||
|
|
||||||
writer.heading("Flight Plan")
|
writer.heading("Flight Plan")
|
||||||
flight_plan_builder = FlightPlanBuilder()
|
flight_plan_builder = FlightPlanBuilder(self.start_time)
|
||||||
for num, waypoint in enumerate(self.flight.waypoints):
|
for num, waypoint in enumerate(self.flight.waypoints):
|
||||||
flight_plan_builder.add_waypoint(num, waypoint)
|
flight_plan_builder.add_waypoint(num, waypoint)
|
||||||
writer.table(flight_plan_builder.build(),
|
writer.table(flight_plan_builder.build(),
|
||||||
headers=["STPT", "Action", "Alt"])
|
headers=["STPT", "Action", "Alt", "TOT"])
|
||||||
|
|
||||||
writer.heading("Comm Ladder")
|
writer.heading("Comm Ladder")
|
||||||
comms = []
|
comms = []
|
||||||
@ -297,6 +308,11 @@ class KneeboardGenerator(MissionInfoGenerator):
|
|||||||
"""Returns a list of kneeboard pages for the given flight."""
|
"""Returns a list of kneeboard pages for the given flight."""
|
||||||
return [
|
return [
|
||||||
BriefingPage(
|
BriefingPage(
|
||||||
flight, self.comms, self.awacs, self.tankers, self.jtacs
|
flight,
|
||||||
|
self.comms,
|
||||||
|
self.awacs,
|
||||||
|
self.tankers,
|
||||||
|
self.jtacs,
|
||||||
|
self.mission.start_time
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
"""Qt data models for game objects."""
|
"""Qt data models for game objects."""
|
||||||
|
import datetime
|
||||||
|
from enum import auto, IntEnum
|
||||||
from typing import Any, Callable, Dict, Iterator, TypeVar, Optional
|
from typing import Any, Callable, Dict, Iterator, TypeVar, Optional
|
||||||
|
|
||||||
from PySide2.QtCore import (
|
from PySide2.QtCore import (
|
||||||
@ -156,6 +158,9 @@ 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:
|
||||||
|
self.package.time_over_target = tot
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_target(self) -> MissionTarget:
|
def mission_target(self) -> MissionTarget:
|
||||||
"""Returns the mission target of the package."""
|
"""Returns the mission target of the package."""
|
||||||
@ -178,6 +183,8 @@ class PackageModel(QAbstractListModel):
|
|||||||
class AtoModel(QAbstractListModel):
|
class AtoModel(QAbstractListModel):
|
||||||
"""The model for an AirTaskingOrder."""
|
"""The model for an AirTaskingOrder."""
|
||||||
|
|
||||||
|
PackageRole = Qt.UserRole
|
||||||
|
|
||||||
def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None:
|
def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.game = game
|
self.game = game
|
||||||
@ -190,9 +197,11 @@ class AtoModel(QAbstractListModel):
|
|||||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return None
|
return None
|
||||||
if role == Qt.DisplayRole:
|
|
||||||
package = self.ato.packages[index.row()]
|
package = self.ato.packages[index.row()]
|
||||||
|
if role == Qt.DisplayRole:
|
||||||
return f"{package.package_description} {package.target.name}"
|
return f"{package.package_description} {package.target.name}"
|
||||||
|
elif role == AtoModel.PackageRole:
|
||||||
|
return package
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def add_package(self, package: Package) -> None:
|
def add_package(self, package: Package) -> None:
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
"""Widgets for displaying air tasking orders."""
|
"""Widgets for displaying air tasking orders."""
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from contextlib import contextmanager
|
||||||
|
from typing import ContextManager, Optional
|
||||||
|
|
||||||
from PySide2.QtCore import QItemSelectionModel, QModelIndex, QSize, Qt
|
from PySide2.QtCore import (
|
||||||
|
QItemSelectionModel,
|
||||||
|
QModelIndex,
|
||||||
|
QSize,
|
||||||
|
Qt,
|
||||||
|
)
|
||||||
|
from PySide2.QtGui import QFont, QFontMetrics, QPainter
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
@ -10,13 +18,13 @@ from PySide2.QtWidgets import (
|
|||||||
QListView,
|
QListView,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QSplitter,
|
QSplitter,
|
||||||
QVBoxLayout,
|
QStyleOptionViewItem, QStyledItemDelegate, QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
|
||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
|
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
||||||
|
|
||||||
|
|
||||||
class QFlightList(QListView):
|
class QFlightList(QListView):
|
||||||
@ -138,6 +146,65 @@ class QFlightPanel(QGroupBox):
|
|||||||
GameUpdateSignal.get_instance().redraw_flight_paths()
|
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def painter_context(painter: QPainter) -> ContextManager[None]:
|
||||||
|
try:
|
||||||
|
painter.save()
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
painter.restore()
|
||||||
|
|
||||||
|
|
||||||
|
class PackageDelegate(QStyledItemDelegate):
|
||||||
|
FONT_SIZE = 12
|
||||||
|
HMARGIN = 4
|
||||||
|
VMARGIN = 4
|
||||||
|
|
||||||
|
def get_font(self, option: QStyleOptionViewItem) -> QFont:
|
||||||
|
font = QFont(option.font)
|
||||||
|
font.setPointSize(self.FONT_SIZE)
|
||||||
|
return font
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def package(index: QModelIndex) -> Package:
|
||||||
|
return index.data(AtoModel.PackageRole)
|
||||||
|
|
||||||
|
def left_text(self, index: QModelIndex) -> str:
|
||||||
|
package = self.package(index)
|
||||||
|
return f"{package.package_description} {package.target.name}"
|
||||||
|
|
||||||
|
def right_text(self, index: QModelIndex) -> str:
|
||||||
|
package = self.package(index)
|
||||||
|
if package.time_over_target is None:
|
||||||
|
return ""
|
||||||
|
tot = datetime.timedelta(seconds=package.time_over_target)
|
||||||
|
return f"TOT T+{tot}"
|
||||||
|
|
||||||
|
def paint(self, painter: QPainter, option: QStyleOptionViewItem,
|
||||||
|
index: QModelIndex) -> None:
|
||||||
|
# Draw the list item with all the default selection styling, but with an
|
||||||
|
# invalid index so text formatting is left to us.
|
||||||
|
super().paint(painter, option, QModelIndex())
|
||||||
|
|
||||||
|
rect = option.rect.adjusted(self.HMARGIN, self.VMARGIN, -self.HMARGIN,
|
||||||
|
-self.VMARGIN)
|
||||||
|
|
||||||
|
with painter_context(painter):
|
||||||
|
painter.setFont(self.get_font(option))
|
||||||
|
|
||||||
|
painter.drawText(rect, Qt.AlignLeft, self.left_text(index))
|
||||||
|
line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2)
|
||||||
|
painter.drawText(line2, Qt.AlignLeft, self.right_text(index))
|
||||||
|
|
||||||
|
def sizeHint(self, option: QStyleOptionViewItem,
|
||||||
|
index: QModelIndex) -> QSize:
|
||||||
|
metrics = QFontMetrics(self.get_font(option))
|
||||||
|
left = metrics.size(0, self.left_text(index))
|
||||||
|
right = metrics.size(0, self.right_text(index))
|
||||||
|
return QSize(max(left.width(), right.width()) + 2 * self.HMARGIN,
|
||||||
|
left.height() + right.height() + 2 * self.VMARGIN)
|
||||||
|
|
||||||
|
|
||||||
class QPackageList(QListView):
|
class QPackageList(QListView):
|
||||||
"""List view for displaying the packages of an ATO."""
|
"""List view for displaying the packages of an ATO."""
|
||||||
|
|
||||||
@ -145,6 +212,7 @@ class QPackageList(QListView):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.ato_model = model
|
self.ato_model = model
|
||||||
self.setModel(model)
|
self.setModel(model)
|
||||||
|
self.setItemDelegate(PackageDelegate())
|
||||||
self.setIconSize(QSize(91, 24))
|
self.setIconSize(QSize(91, 24))
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||||
|
|
||||||
|
|||||||
@ -2,12 +2,13 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PySide2.QtCore import QItemSelection, Signal
|
from PySide2.QtCore import QItemSelection, QTime, Signal
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QDialog,
|
QDialog,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QLabel,
|
QLabel,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
|
QTimeEdit,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -56,14 +57,39 @@ class QPackageDialog(QDialog):
|
|||||||
self.summary_row = QHBoxLayout()
|
self.summary_row = QHBoxLayout()
|
||||||
self.layout.addLayout(self.summary_row)
|
self.layout.addLayout(self.summary_row)
|
||||||
|
|
||||||
|
self.package_type_column = QHBoxLayout()
|
||||||
|
self.summary_row.addLayout(self.package_type_column)
|
||||||
|
|
||||||
self.package_type_label = QLabel("Package Type:")
|
self.package_type_label = QLabel("Package Type:")
|
||||||
self.package_type_text = QLabel(self.package_model.description)
|
self.package_type_text = QLabel(self.package_model.description)
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
self.package_changed.connect(lambda: self.package_type_text.setText(
|
self.package_changed.connect(lambda: self.package_type_text.setText(
|
||||||
self.package_model.description
|
self.package_model.description
|
||||||
))
|
))
|
||||||
self.summary_row.addWidget(self.package_type_label)
|
self.package_type_column.addWidget(self.package_type_label)
|
||||||
self.summary_row.addWidget(self.package_type_text)
|
self.package_type_column.addWidget(self.package_type_text)
|
||||||
|
|
||||||
|
self.summary_row.addStretch(1)
|
||||||
|
|
||||||
|
self.tot_column = QHBoxLayout()
|
||||||
|
self.summary_row.addLayout(self.tot_column)
|
||||||
|
|
||||||
|
self.tot_label = QLabel("Time Over Target:")
|
||||||
|
self.tot_column.addWidget(self.tot_label)
|
||||||
|
|
||||||
|
if self.package_model.package.time_over_target is None:
|
||||||
|
time = None
|
||||||
|
else:
|
||||||
|
delay = self.package_model.package.time_over_target
|
||||||
|
hours = delay // 3600
|
||||||
|
minutes = delay // 60 % 60
|
||||||
|
seconds = delay % 60
|
||||||
|
time = QTime(hours, minutes, seconds)
|
||||||
|
|
||||||
|
self.tot_spinner = QTimeEdit(time)
|
||||||
|
self.tot_spinner.setMinimumTime(QTime(0, 0))
|
||||||
|
self.tot_spinner.setDisplayFormat("T+hh:mm:ss")
|
||||||
|
self.tot_column.addWidget(self.tot_spinner)
|
||||||
|
|
||||||
self.package_view = QFlightList(self.package_model)
|
self.package_view = QFlightList(self.package_model)
|
||||||
self.package_view.selectionModel().selectionChanged.connect(
|
self.package_view.selectionModel().selectionChanged.connect(
|
||||||
@ -90,8 +116,10 @@ class QPackageDialog(QDialog):
|
|||||||
|
|
||||||
self.finished.connect(self.on_close)
|
self.finished.connect(self.on_close)
|
||||||
|
|
||||||
@staticmethod
|
def on_close(self, _result) -> None:
|
||||||
def on_close(_result) -> None:
|
time = self.tot_spinner.time()
|
||||||
|
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
|
||||||
|
self.package_model.update_tot(seconds)
|
||||||
GameUpdateSignal.get_instance().redraw_flight_paths()
|
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||||
|
|
||||||
def on_selection_changed(self, selected: QItemSelection,
|
def on_selection_changed(self, selected: QItemSelection,
|
||||||
|
|||||||
@ -1,26 +1,33 @@
|
|||||||
|
import datetime
|
||||||
|
import itertools
|
||||||
|
|
||||||
from PySide2.QtCore import QItemSelectionModel, QPoint
|
from PySide2.QtCore import QItemSelectionModel, QPoint
|
||||||
from PySide2.QtGui import QStandardItemModel, QStandardItem
|
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||||
from PySide2.QtWidgets import QTableView, QHeaderView
|
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.flights.flight import Flight, FlightWaypoint
|
from gen.flights.flight import Flight, FlightWaypoint
|
||||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import QWaypointItem
|
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import \
|
||||||
|
QWaypointItem
|
||||||
|
|
||||||
|
|
||||||
class QFlightWaypointList(QTableView):
|
class QFlightWaypointList(QTableView):
|
||||||
|
|
||||||
def __init__(self, flight: Flight):
|
def __init__(self, package: Package, flight: Flight):
|
||||||
super(QFlightWaypointList, self).__init__()
|
super().__init__()
|
||||||
|
self.package = package
|
||||||
|
self.flight = flight
|
||||||
|
|
||||||
self.model = QStandardItemModel(self)
|
self.model = QStandardItemModel(self)
|
||||||
self.setModel(self.model)
|
self.setModel(self.model)
|
||||||
self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||||
self.model.setHorizontalHeaderLabels(["Name", "Alt"])
|
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
|
||||||
|
|
||||||
header = self.horizontalHeader()
|
header = self.horizontalHeader()
|
||||||
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
||||||
|
|
||||||
self.flight = flight
|
|
||||||
|
|
||||||
if len(self.flight.points) > 0:
|
if len(self.flight.points) > 0:
|
||||||
self.selectedPoint = self.flight.points[0]
|
self.selectedPoint = self.flight.points[0]
|
||||||
self.update_list()
|
self.update_list()
|
||||||
@ -33,18 +40,49 @@ class QFlightWaypointList(QTableView):
|
|||||||
|
|
||||||
def update_list(self):
|
def update_list(self):
|
||||||
self.model.clear()
|
self.model.clear()
|
||||||
self.model.setHorizontalHeaderLabels(["Name", "Alt"])
|
|
||||||
takeoff = FlightWaypoint(self.flight.from_cp.position.x, self.flight.from_cp.position.y, 0)
|
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
|
||||||
|
# we need to add that waypoint manually.
|
||||||
|
takeoff = FlightWaypoint(self.flight.from_cp.position.x,
|
||||||
|
self.flight.from_cp.position.y, 0)
|
||||||
takeoff.description = "Take Off"
|
takeoff.description = "Take Off"
|
||||||
takeoff.name = takeoff.pretty_name = "Take Off from " + self.flight.from_cp.name
|
takeoff.name = takeoff.pretty_name = "Take Off from " + self.flight.from_cp.name
|
||||||
self.model.appendRow(QWaypointItem(takeoff, 0))
|
takeoff.alt_type = "RADIO"
|
||||||
item = QStandardItem("0 feet AGL")
|
|
||||||
item.setEditable(False)
|
waypoints = itertools.chain([takeoff], self.flight.points)
|
||||||
self.model.setItem(0, 1, item)
|
for row, waypoint in enumerate(waypoints):
|
||||||
for i, point in enumerate(self.flight.points):
|
self.add_waypoint_row(row, waypoint, timing)
|
||||||
|
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)),
|
||||||
|
QItemSelectionModel.Select)
|
||||||
|
|
||||||
|
def add_waypoint_row(self, row: int, waypoint: FlightWaypoint,
|
||||||
|
timing: PackageWaypointTiming) -> None:
|
||||||
self.model.insertRow(self.model.rowCount())
|
self.model.insertRow(self.model.rowCount())
|
||||||
self.model.setItem(self.model.rowCount()-1, 0, QWaypointItem(point, i + 1))
|
|
||||||
item = QStandardItem(str(meter_to_feet(point.alt)) + " ft " + str(["AGL" if point.alt_type == "RADIO" else "MSL"][0]))
|
self.model.setItem(row, 0, QWaypointItem(waypoint, row))
|
||||||
item.setEditable(False)
|
|
||||||
self.model.setItem(self.model.rowCount()-1, 1, item)
|
altitude = meter_to_feet(waypoint.alt)
|
||||||
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)), QItemSelectionModel.Select)
|
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
|
||||||
|
altitude_item = QStandardItem(f"{altitude} ft {altitude_type}")
|
||||||
|
altitude_item.setEditable(False)
|
||||||
|
self.model.setItem(row, 1, altitude_item)
|
||||||
|
|
||||||
|
tot = self.tot_text(waypoint, timing)
|
||||||
|
tot_item = QStandardItem(tot)
|
||||||
|
tot_item.setEditable(False)
|
||||||
|
self.model.setItem(row, 2, tot_item)
|
||||||
|
|
||||||
|
def tot_text(self, waypoint: FlightWaypoint,
|
||||||
|
timing: PackageWaypointTiming) -> str:
|
||||||
|
prefix = ""
|
||||||
|
time = timing.tot_for_waypoint(waypoint)
|
||||||
|
if time is None:
|
||||||
|
prefix = "Depart "
|
||||||
|
time = timing.depart_time_for_waypoint(waypoint, self.flight)
|
||||||
|
if time is None:
|
||||||
|
return ""
|
||||||
|
return f"{prefix}T+{datetime.timedelta(seconds=time)}"
|
||||||
|
|||||||
@ -44,7 +44,8 @@ class QFlightWaypointTab(QFrame):
|
|||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
layout = QGridLayout()
|
layout = QGridLayout()
|
||||||
|
|
||||||
self.flight_waypoint_list = QFlightWaypointList(self.flight)
|
self.flight_waypoint_list = QFlightWaypointList(self.package,
|
||||||
|
self.flight)
|
||||||
layout.addWidget(self.flight_waypoint_list, 0, 0)
|
layout.addWidget(self.flight_waypoint_list, 0, 0)
|
||||||
|
|
||||||
rlayout = QVBoxLayout()
|
rlayout = QVBoxLayout()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user