mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
9a7dfc55e3
2
.gitignore
vendored
2
.gitignore
vendored
@ -16,7 +16,7 @@ tests/**
|
||||
/liberation_preferences.json
|
||||
/state.json
|
||||
|
||||
logs/liberation.log
|
||||
logs/
|
||||
|
||||
qt_ui/logs/liberation.log
|
||||
|
||||
|
||||
596
gen/aircraft.py
596
gen/aircraft.py
@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
@ -28,7 +30,7 @@ from dcs.planes import (
|
||||
SpitfireLFMkIXCW,
|
||||
Su_33,
|
||||
)
|
||||
from dcs.point import PointAction
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.task import (
|
||||
AntishipStrike,
|
||||
AttackGroup,
|
||||
@ -40,8 +42,6 @@ from dcs.task import (
|
||||
EngageTargets,
|
||||
Escort,
|
||||
GroundAttack,
|
||||
MainTask,
|
||||
NoTask,
|
||||
OptROE,
|
||||
OptRTBOnBingoFuel,
|
||||
OptRTBOnOutOfAmmo,
|
||||
@ -64,10 +64,10 @@ from dcs.unittype import FlyingType, UnitType
|
||||
from game import db
|
||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
||||
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.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.flights.flight import (
|
||||
Flight,
|
||||
@ -76,9 +76,10 @@ from gen.flights.flight import (
|
||||
FlightWaypointType,
|
||||
)
|
||||
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
||||
from theater import MissionTarget, TheaterGroundObject
|
||||
from theater.controlpoint import ControlPoint, ControlPointType
|
||||
from .naming import namegen
|
||||
from .conflictgen import Conflict
|
||||
from .naming import namegen
|
||||
|
||||
WARM_START_HELI_AIRSPEED = 120
|
||||
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"]
|
||||
|
||||
|
||||
@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:
|
||||
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
|
||||
game, radio_registry: RadioRegistry):
|
||||
@ -634,7 +777,8 @@ class AircraftConflictGenerator:
|
||||
arrival=departure_runway,
|
||||
# TODO: Support for divert airfields.
|
||||
divert=None,
|
||||
waypoints=[first_point] + flight.points,
|
||||
# Waypoints are added later, after they've had their TOTs set.
|
||||
waypoints=[],
|
||||
intra_flight_channel=channel
|
||||
))
|
||||
|
||||
@ -817,6 +961,7 @@ class AircraftConflictGenerator:
|
||||
self.clear_parking_slots()
|
||||
|
||||
for package in ato.packages:
|
||||
timing = PackageWaypointTiming.for_package(package)
|
||||
for flight in package.flights:
|
||||
culled = self.game.position_culled(flight.from_cp.position)
|
||||
if flight.client_count == 0 and culled:
|
||||
@ -825,8 +970,7 @@ class AircraftConflictGenerator:
|
||||
logging.info(f"Generating flight: {flight.unit_type}")
|
||||
group = self.generate_planned_flight(flight.from_cp, country,
|
||||
flight)
|
||||
self.setup_flight_group(group, flight, flight.flight_type,
|
||||
dynamic_runways)
|
||||
self.setup_flight_group(group, flight, timing, dynamic_runways)
|
||||
self.setup_group_activation_trigger(flight, group)
|
||||
|
||||
def setup_group_activation_trigger(self, flight, group):
|
||||
@ -936,134 +1080,334 @@ class AircraftConflictGenerator:
|
||||
flight.group = group
|
||||
return group
|
||||
|
||||
|
||||
def setup_flight_group(self, group, flight, flight_type,
|
||||
dynamic_runways: Dict[str, RunwayData]):
|
||||
|
||||
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP, FlightType.INTERCEPTION]:
|
||||
group.task = CAP.name
|
||||
self._setup_group(group, CAP, flight, dynamic_runways)
|
||||
# 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]))
|
||||
# group.tasks.append(EngageTargets(max_distance=nm_to_meter(120), targets=[Targets.All.Air]))
|
||||
if flight.unit_type not in GUNFIGHTERS:
|
||||
group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.AAM))
|
||||
else:
|
||||
group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.Cannon))
|
||||
|
||||
elif flight_type in [FlightType.CAS, FlightType.BAI]:
|
||||
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))
|
||||
@staticmethod
|
||||
def configure_behavior(
|
||||
group: FlyingGroup,
|
||||
react_on_threat: Optional[OptReactOnThreat.Values] = None,
|
||||
roe: Optional[OptROE.Values] = None,
|
||||
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
|
||||
restrict_jettison: Optional[bool] = None) -> None:
|
||||
group.points[0].tasks.clear()
|
||||
if react_on_threat is not None:
|
||||
group.points[0].tasks.append(OptReactOnThreat(react_on_threat))
|
||||
if roe is not None:
|
||||
group.points[0].tasks.append(OptROE(roe))
|
||||
if restrict_jettison is not None:
|
||||
group.points[0].tasks.append(OptRestrictJettison(restrict_jettison))
|
||||
if rtb_winchester is not None:
|
||||
group.points[0].tasks.append(OptRTBOnOutOfAmmo(rtb_winchester))
|
||||
|
||||
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
|
||||
group.points[0].tasks.append(OptRestrictAfterburner(True))
|
||||
|
||||
@staticmethod
|
||||
def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
|
||||
if hasattr(flight.unit_type, 'eplrs'):
|
||||
if flight.unit_type.eplrs:
|
||||
group.points[0].tasks.append(EPLRS(group.id))
|
||||
|
||||
for i, point in enumerate(flight.points):
|
||||
if not point.only_for_player or (point.only_for_player and flight.client_count > 0):
|
||||
pt = group.add_waypoint(Point(point.x, point.y), point.alt)
|
||||
if point.waypoint_type == FlightWaypointType.PATROL_TRACK:
|
||||
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:
|
||||
def configure_cap(self, group: FlyingGroup, flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
group.task = CAP.name
|
||||
self._setup_group(group, CAP, flight, dynamic_runways)
|
||||
|
||||
if group.units[0].unit_type == B_17G:
|
||||
if len(point.targets) > 0:
|
||||
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:
|
||||
for j, t in enumerate(point.targets):
|
||||
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:
|
||||
if flight.unit_type not in GUNFIGHTERS:
|
||||
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
|
||||
else:
|
||||
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
||||
|
||||
tgroup = self.m.find_group(point.targetGroup.group_identifier)
|
||||
if tgroup is not None:
|
||||
task = AttackGroup(tgroup.id)
|
||||
task.params["expend"] = "All"
|
||||
task.params["attackQtyLimit"] = False
|
||||
task.params["directionEnabled"] = False
|
||||
task.params["altitudeEnabled"] = False
|
||||
task.params["weaponType"] = 268402702 # Guided Weapons
|
||||
task.params["groupAttack"] = True
|
||||
pt.tasks.append(task)
|
||||
self.configure_behavior(group, rtb_winchester=ammo_type)
|
||||
|
||||
for j, t in enumerate(point.targets):
|
||||
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))
|
||||
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50),
|
||||
targets=[Targets.All.Air]))
|
||||
|
||||
if pt is not None:
|
||||
pt.alt_type = point.alt_type
|
||||
pt.name = String(point.name)
|
||||
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:
|
||||
task = AttackGroup(tgroup.id)
|
||||
task.params["expend"] = "All"
|
||||
task.params["attackQtyLimit"] = False
|
||||
task.params["directionEnabled"] = False
|
||||
task.params["altitudeEnabled"] = False
|
||||
task.params["weaponType"] = 268402702 # Guided Weapons
|
||||
task.params["groupAttack"] = True
|
||||
waypoint.tasks.append(task)
|
||||
|
||||
for i, t in enumerate(self.waypoint.targets):
|
||||
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 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
|
||||
the single CAP flight.
|
||||
"""
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from typing import Dict, Iterator, List, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
from .flights.flight import Flight, FlightType
|
||||
|
||||
from theater.missiontarget import MissionTarget
|
||||
from .flights.flight import Flight, FlightType
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -29,6 +30,14 @@ class Task:
|
||||
location: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackageWaypoints:
|
||||
join: Point
|
||||
ingress: Point
|
||||
egress: Point
|
||||
split: Point
|
||||
|
||||
|
||||
@dataclass
|
||||
class Package:
|
||||
"""A mission package."""
|
||||
@ -42,10 +51,10 @@ class Package:
|
||||
|
||||
delay: int = field(default=0)
|
||||
|
||||
join_point: Optional[Point] = field(default=None, init=False, hash=False)
|
||||
split_point: Optional[Point] = field(default=None, init=False, hash=False)
|
||||
ingress_point: Optional[Point] = field(default=None, init=False, hash=False)
|
||||
egress_point: Optional[Point] = field(default=None, init=False, hash=False)
|
||||
#: Desired TOT measured in seconds from mission start.
|
||||
time_over_target: Optional[int] = field(default=None)
|
||||
|
||||
waypoints: Optional[PackageWaypoints] = field(default=None)
|
||||
|
||||
def add_flight(self, flight: Flight) -> None:
|
||||
"""Adds a flight to the package."""
|
||||
@ -55,8 +64,7 @@ class Package:
|
||||
"""Removes a flight from the package."""
|
||||
self.flights.remove(flight)
|
||||
if not self.flights:
|
||||
self.ingress_point = None
|
||||
self.egress_point = None
|
||||
self.waypoints = None
|
||||
|
||||
@property
|
||||
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.infos.information import Information
|
||||
from game.utils import nm_to_meter
|
||||
from gen import Conflict
|
||||
from gen import Conflict, PackageWaypointTiming
|
||||
from gen.ato import Package
|
||||
from gen.flights.ai_flight_planner_db import (
|
||||
CAP_CAPABLE,
|
||||
@ -496,10 +496,17 @@ class CoalitionMissionPlanner:
|
||||
latest=90,
|
||||
margin=5
|
||||
)
|
||||
for package in non_dca_packages:
|
||||
package.delay = next(start_time)
|
||||
for flight in package.flights:
|
||||
flight.scheduled_in = package.delay
|
||||
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)
|
||||
# TODO: Compute TOT based on package.
|
||||
package.time_over_target = (package.delay + 40) * 60
|
||||
for flight in package.flights:
|
||||
flight.scheduled_in = package.delay
|
||||
|
||||
def message(self, title, text) -> None:
|
||||
"""Emits a planning message to the player.
|
||||
|
||||
@ -49,6 +49,7 @@ class FlightWaypointType(Enum):
|
||||
CUSTOM = 15 # User waypoint (no specific behaviour)
|
||||
JOIN = 16
|
||||
SPLIT = 17
|
||||
LOITER = 18
|
||||
|
||||
|
||||
class PredefinedWaypointCategory(Enum):
|
||||
@ -66,6 +67,15 @@ class FlightWaypoint:
|
||||
|
||||
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
|
||||
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.x = x
|
||||
self.y = y
|
||||
@ -81,6 +91,12 @@ class FlightWaypoint:
|
||||
self.only_for_player = False
|
||||
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
|
||||
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.utils import nm_to_meter
|
||||
from gen.ato import Package
|
||||
from gen.ato import Package, PackageWaypoints
|
||||
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
|
||||
from .closestairfields import ObjectiveDistanceCache
|
||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||
from .flight import Flight, FlightType, FlightWaypoint
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..conflictgen import Conflict
|
||||
|
||||
@ -55,7 +55,8 @@ class FlightPlanBuilder:
|
||||
"""Creates a default flight plan for the given mission."""
|
||||
if flight not in self.package.flights:
|
||||
raise RuntimeError("Flight must be a part of the package")
|
||||
self.generate_missing_package_waypoints()
|
||||
if self.package.waypoints is None:
|
||||
self.regenerate_package_waypoints()
|
||||
|
||||
# TODO: Flesh out mission types.
|
||||
try:
|
||||
@ -105,15 +106,18 @@ class FlightPlanBuilder:
|
||||
except InvalidObjectiveLocation as ex:
|
||||
logging.error(f"Could not create flight plan: {ex}")
|
||||
|
||||
def generate_missing_package_waypoints(self) -> None:
|
||||
if self.package.ingress_point is None:
|
||||
self.package.ingress_point = self._ingress_point()
|
||||
if self.package.egress_point is None:
|
||||
self.package.egress_point = self._egress_point()
|
||||
if self.package.join_point is None:
|
||||
self.package.join_point = self._join_point()
|
||||
if self.package.split_point is None:
|
||||
self.package.split_point = self._split_point()
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
ingress_point = self._ingress_point()
|
||||
egress_point = self._egress_point()
|
||||
join_point = self._join_point(ingress_point)
|
||||
split_point = self._split_point(egress_point)
|
||||
|
||||
self.package.waypoints = PackageWaypoints(
|
||||
join_point,
|
||||
ingress_point,
|
||||
egress_point,
|
||||
split_point,
|
||||
)
|
||||
|
||||
def generate_strike(self, flight: Flight) -> None:
|
||||
"""Generates a strike flight plan.
|
||||
@ -121,6 +125,7 @@ class FlightPlanBuilder:
|
||||
Args:
|
||||
flight: The flight to generate the flight plan for.
|
||||
"""
|
||||
assert self.package.waypoints is not None
|
||||
location = self.package.target
|
||||
|
||||
# TODO: Support airfield strikes.
|
||||
@ -129,8 +134,9 @@ class FlightPlanBuilder:
|
||||
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.join(self.package.join_point)
|
||||
builder.ingress_strike(self.package.ingress_point, location)
|
||||
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":
|
||||
# TODO: Replace with DEAD?
|
||||
@ -157,8 +163,8 @@ class FlightPlanBuilder:
|
||||
location
|
||||
)
|
||||
|
||||
builder.egress(self.package.egress_point, location)
|
||||
builder.split(self.package.split_point)
|
||||
builder.egress(self.package.waypoints.egress, location)
|
||||
builder.split(self.package.waypoints.split)
|
||||
builder.rtb(flight.from_cp)
|
||||
|
||||
flight.points = builder.build()
|
||||
@ -215,6 +221,7 @@ class FlightPlanBuilder:
|
||||
Args:
|
||||
flight: The flight to generate the flight plan for.
|
||||
"""
|
||||
assert self.package.waypoints is not None
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, FrontLine):
|
||||
@ -246,7 +253,10 @@ class FlightPlanBuilder:
|
||||
# Create points
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
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.split(self.package.waypoints.split)
|
||||
builder.rtb(flight.from_cp)
|
||||
flight.points = builder.build()
|
||||
|
||||
@ -258,6 +268,7 @@ class FlightPlanBuilder:
|
||||
flight: The flight to generate the flight plan for.
|
||||
custom_targets: Specific radar equipped units selected by the user.
|
||||
"""
|
||||
assert self.package.waypoints is not None
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, TheaterGroundObject):
|
||||
@ -268,21 +279,15 @@ class FlightPlanBuilder:
|
||||
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.join(self.package.join_point)
|
||||
builder.ingress_sead(self.package.ingress_point, location)
|
||||
builder.hold(self._hold_point(flight))
|
||||
builder.join(self.package.waypoints.join)
|
||||
builder.ingress_sead(self.package.waypoints.ingress, location)
|
||||
|
||||
# TODO: Unify these.
|
||||
# There doesn't seem to be any reason to treat the UI fragged missions
|
||||
# different from the automatic missions.
|
||||
if 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:
|
||||
builder.dead_point(target, location.name, location)
|
||||
else:
|
||||
@ -293,17 +298,22 @@ class FlightPlanBuilder:
|
||||
else:
|
||||
builder.sead_area(location)
|
||||
|
||||
builder.egress(self.package.egress_point, location)
|
||||
builder.split(self.package.split_point)
|
||||
builder.egress(self.package.waypoints.egress, location)
|
||||
builder.split(self.package.waypoints.split)
|
||||
builder.rtb(flight.from_cp)
|
||||
|
||||
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:
|
||||
# TODO: Decide common waypoints for the package ahead of time.
|
||||
# Packages should determine some common points like push, ingress,
|
||||
# egress, and split points ahead of time so they can be shared by all
|
||||
# flights.
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
patrol_alt = random.randint(
|
||||
self.doctrine.min_patrol_altitude,
|
||||
@ -312,13 +322,11 @@ class FlightPlanBuilder:
|
||||
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.join(self.package.join_point)
|
||||
builder.race_track(
|
||||
self.package.ingress_point,
|
||||
self.package.egress_point,
|
||||
patrol_alt
|
||||
)
|
||||
builder.split(self.package.split_point)
|
||||
builder.hold(self._hold_point(flight))
|
||||
builder.join(self.package.waypoints.join)
|
||||
builder.race_track(self.package.waypoints.ingress,
|
||||
self.package.waypoints.egress, patrol_alt)
|
||||
builder.split(self.package.waypoints.split)
|
||||
builder.rtb(flight.from_cp)
|
||||
|
||||
flight.points = builder.build()
|
||||
@ -329,6 +337,7 @@ class FlightPlanBuilder:
|
||||
Args:
|
||||
flight: The flight to generate the flight plan for.
|
||||
"""
|
||||
assert self.package.waypoints is not None
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, FrontLine):
|
||||
@ -346,11 +355,12 @@ class FlightPlanBuilder:
|
||||
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
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.cas(center, cap_alt)
|
||||
builder.egress(egress, location)
|
||||
builder.split(self.package.split_point)
|
||||
builder.split(self.package.waypoints.split)
|
||||
builder.rtb(flight.from_cp, is_helo)
|
||||
|
||||
flight.points = builder.build()
|
||||
@ -386,16 +396,12 @@ class FlightPlanBuilder:
|
||||
builder.land(arrival)
|
||||
return builder.build()[0]
|
||||
|
||||
def _join_point(self) -> Point:
|
||||
ingress_point = self.package.ingress_point
|
||||
assert ingress_point is not None
|
||||
def _join_point(self, ingress_point: Point) -> Point:
|
||||
heading = self._heading_to_package_airfield(ingress_point)
|
||||
return ingress_point.point_from_heading(heading,
|
||||
-self.doctrine.join_distance)
|
||||
|
||||
def _split_point(self) -> Point:
|
||||
egress_point = self.package.egress_point
|
||||
assert egress_point is not None
|
||||
def _split_point(self, egress_point: Point) -> Point:
|
||||
heading = self._heading_to_package_airfield(egress_point)
|
||||
return egress_point.point_from_heading(heading,
|
||||
-self.doctrine.split_distance)
|
||||
|
||||
@ -25,6 +25,7 @@ class WaypointBuilder:
|
||||
|
||||
Args:
|
||||
departure: Departure airfield or carrier.
|
||||
is_helo: True if the flight is a helicopter.
|
||||
"""
|
||||
# TODO: Pick runway based on wind direction.
|
||||
heading = departure.heading
|
||||
@ -48,6 +49,7 @@ class WaypointBuilder:
|
||||
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
is_helo: True if the flight is a helicopter.
|
||||
"""
|
||||
# TODO: Pick runway based on wind direction.
|
||||
# ControlPoint.heading is the departure heading.
|
||||
@ -64,7 +66,7 @@ class WaypointBuilder:
|
||||
waypoint.name = "DESCEND"
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Descend to pattern altitude"
|
||||
waypoint.pretty_name = "Ascend"
|
||||
waypoint.pretty_name = "Descend"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
def land(self, arrival: ControlPoint) -> None:
|
||||
@ -86,6 +88,18 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "Land"
|
||||
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:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.JOIN,
|
||||
@ -170,7 +184,7 @@ class WaypointBuilder:
|
||||
location)
|
||||
|
||||
def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||
description: str, location: MissionTarget) -> None:
|
||||
description: str, location: MissionTarget) -> None:
|
||||
if self.ingress_point is None:
|
||||
raise RuntimeError(
|
||||
"An ingress point must be added before target points."
|
||||
@ -293,6 +307,7 @@ class WaypointBuilder:
|
||||
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
is_helo: True if the flight is a helicopter.
|
||||
"""
|
||||
self.descent(arrival, is_helo)
|
||||
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 dataclasses import dataclass
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
@ -106,7 +107,8 @@ class NumberedWaypoint:
|
||||
|
||||
|
||||
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.target_points: List[NumberedWaypoint] = []
|
||||
|
||||
@ -133,16 +135,24 @@ class FlightPlanBuilder:
|
||||
self.rows.append([
|
||||
f"{first_waypoint_num}-{last_waypoint_num}",
|
||||
"Target points",
|
||||
"0"
|
||||
"0",
|
||||
self._format_time(self.target_points[0].waypoint.tot),
|
||||
])
|
||||
|
||||
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
|
||||
self.rows.append([
|
||||
str(waypoint.number),
|
||||
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]]:
|
||||
return self.rows
|
||||
|
||||
@ -151,12 +161,13 @@ class BriefingPage(KneeboardPage):
|
||||
"""A kneeboard page containing briefing information."""
|
||||
def __init__(self, flight: FlightData, comms: List[CommInfo],
|
||||
awacs: List[AwacsInfo], tankers: List[TankerInfo],
|
||||
jtacs: List[JtacInfo]) -> None:
|
||||
jtacs: List[JtacInfo], start_time: datetime.datetime) -> None:
|
||||
self.flight = flight
|
||||
self.comms = list(comms)
|
||||
self.awacs = awacs
|
||||
self.tankers = tankers
|
||||
self.jtacs = jtacs
|
||||
self.start_time = start_time
|
||||
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
@ -172,11 +183,11 @@ class BriefingPage(KneeboardPage):
|
||||
], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"])
|
||||
|
||||
writer.heading("Flight Plan")
|
||||
flight_plan_builder = FlightPlanBuilder()
|
||||
flight_plan_builder = FlightPlanBuilder(self.start_time)
|
||||
for num, waypoint in enumerate(self.flight.waypoints):
|
||||
flight_plan_builder.add_waypoint(num, waypoint)
|
||||
writer.table(flight_plan_builder.build(),
|
||||
headers=["STPT", "Action", "Alt"])
|
||||
headers=["STPT", "Action", "Alt", "TOT"])
|
||||
|
||||
writer.heading("Comm Ladder")
|
||||
comms = []
|
||||
@ -297,6 +308,11 @@ class KneeboardGenerator(MissionInfoGenerator):
|
||||
"""Returns a list of kneeboard pages for the given flight."""
|
||||
return [
|
||||
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."""
|
||||
import datetime
|
||||
from enum import auto, IntEnum
|
||||
from typing import Any, Callable, Dict, Iterator, TypeVar, Optional
|
||||
|
||||
from PySide2.QtCore import (
|
||||
@ -156,6 +158,9 @@ class PackageModel(QAbstractListModel):
|
||||
"""Returns the flight located at the given index."""
|
||||
return self.package.flights[index.row()]
|
||||
|
||||
def update_tot(self, tot: int) -> None:
|
||||
self.package.time_over_target = tot
|
||||
|
||||
@property
|
||||
def mission_target(self) -> MissionTarget:
|
||||
"""Returns the mission target of the package."""
|
||||
@ -178,6 +183,8 @@ class PackageModel(QAbstractListModel):
|
||||
class AtoModel(QAbstractListModel):
|
||||
"""The model for an AirTaskingOrder."""
|
||||
|
||||
PackageRole = Qt.UserRole
|
||||
|
||||
def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None:
|
||||
super().__init__()
|
||||
self.game = game
|
||||
@ -190,9 +197,11 @@ class AtoModel(QAbstractListModel):
|
||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||
if not index.isValid():
|
||||
return None
|
||||
package = self.ato.packages[index.row()]
|
||||
if role == Qt.DisplayRole:
|
||||
package = self.ato.packages[index.row()]
|
||||
return f"{package.package_description} {package.target.name}"
|
||||
elif role == AtoModel.PackageRole:
|
||||
return package
|
||||
return None
|
||||
|
||||
def add_package(self, package: Package) -> None:
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtWidgets import QFrame, QGroupBox, QHBoxLayout, QPushButton
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
)
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game
|
||||
@ -9,9 +15,10 @@ from qt_ui.widgets.QBudgetBox import QBudgetBox
|
||||
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
|
||||
from qt_ui.widgets.QTurnCounter import QTurnCounter
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.QWaitingForMissionResultWindow import \
|
||||
QWaitingForMissionResultWindow
|
||||
from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow
|
||||
from qt_ui.windows.stats.QStatsWindow import QStatsWindow
|
||||
from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow
|
||||
|
||||
|
||||
class QTopPanel(QFrame):
|
||||
@ -101,8 +108,36 @@ class QTopPanel(QFrame):
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
self.proceedButton.setEnabled(True)
|
||||
|
||||
def ato_has_clients(self) -> bool:
|
||||
for package in self.game.blue_ato.packages:
|
||||
for flight in package.flights:
|
||||
if flight.client_count > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def confirm_no_client_launch(self) -> bool:
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"Continue without client slots?",
|
||||
("No client slots have been created for players. Continuing will "
|
||||
"allow the AI to perform the mission, but players will be unable "
|
||||
"to participate.<br />"
|
||||
"<br />"
|
||||
"To add client slots for players, select a package from the "
|
||||
"Packages panel on the left of the main window, and then a flight "
|
||||
"from the Flights panel below the Packages panel. The edit button "
|
||||
"below the Flights panel will allow you to edit the number of "
|
||||
"client slots in the flight. Each client slot allows one player."),
|
||||
QMessageBox.No,
|
||||
QMessageBox.Yes
|
||||
)
|
||||
return result == QMessageBox.Yes
|
||||
|
||||
def launch_mission(self):
|
||||
"""Finishes planning and waits for mission completion."""
|
||||
if not self.ato_has_clients() and not self.confirm_no_client_launch():
|
||||
return
|
||||
|
||||
# TODO: Refactor this nonsense.
|
||||
game_event = None
|
||||
for event in self.game.events:
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
"""Widgets for displaying air tasking orders."""
|
||||
import datetime
|
||||
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 (
|
||||
QAbstractItemView,
|
||||
QGroupBox,
|
||||
@ -10,13 +18,13 @@ from PySide2.QtWidgets import (
|
||||
QListView,
|
||||
QPushButton,
|
||||
QSplitter,
|
||||
QVBoxLayout,
|
||||
QStyleOptionViewItem, QStyledItemDelegate, QVBoxLayout,
|
||||
)
|
||||
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
||||
|
||||
|
||||
class QFlightList(QListView):
|
||||
@ -138,6 +146,65 @@ class QFlightPanel(QGroupBox):
|
||||
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):
|
||||
"""List view for displaying the packages of an ATO."""
|
||||
|
||||
@ -145,6 +212,7 @@ class QPackageList(QListView):
|
||||
super().__init__()
|
||||
self.ato_model = model
|
||||
self.setModel(model)
|
||||
self.setItemDelegate(PackageDelegate())
|
||||
self.setIconSize(QSize(91, 24))
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||
|
||||
|
||||
@ -2,12 +2,13 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import QItemSelection, Signal
|
||||
from PySide2.QtCore import QItemSelection, QTime, Signal
|
||||
from PySide2.QtWidgets import (
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QTimeEdit,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
@ -56,14 +57,39 @@ class QPackageDialog(QDialog):
|
||||
self.summary_row = QHBoxLayout()
|
||||
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_text = QLabel(self.package_model.description)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_changed.connect(lambda: self.package_type_text.setText(
|
||||
self.package_model.description
|
||||
))
|
||||
self.summary_row.addWidget(self.package_type_label)
|
||||
self.summary_row.addWidget(self.package_type_text)
|
||||
self.package_type_column.addWidget(self.package_type_label)
|
||||
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.selectionModel().selectionChanged.connect(
|
||||
@ -90,8 +116,10 @@ class QPackageDialog(QDialog):
|
||||
|
||||
self.finished.connect(self.on_close)
|
||||
|
||||
@staticmethod
|
||||
def on_close(_result) -> None:
|
||||
def on_close(self, _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()
|
||||
|
||||
def on_selection_changed(self, selected: QItemSelection,
|
||||
|
||||
@ -1,26 +1,33 @@
|
||||
import datetime
|
||||
import itertools
|
||||
|
||||
from PySide2.QtCore import QItemSelectionModel, QPoint
|
||||
from PySide2.QtGui import QStandardItemModel, QStandardItem
|
||||
from PySide2.QtWidgets import QTableView, QHeaderView
|
||||
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide2.QtWidgets import QHeaderView, QTableView
|
||||
|
||||
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 qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import QWaypointItem
|
||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import \
|
||||
QWaypointItem
|
||||
|
||||
|
||||
class QFlightWaypointList(QTableView):
|
||||
|
||||
def __init__(self, flight: Flight):
|
||||
super(QFlightWaypointList, self).__init__()
|
||||
def __init__(self, package: Package, flight: Flight):
|
||||
super().__init__()
|
||||
self.package = package
|
||||
self.flight = flight
|
||||
|
||||
self.model = QStandardItemModel(self)
|
||||
self.setModel(self.model)
|
||||
self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
self.model.setHorizontalHeaderLabels(["Name", "Alt"])
|
||||
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
|
||||
|
||||
header = self.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
||||
|
||||
self.flight = flight
|
||||
|
||||
if len(self.flight.points) > 0:
|
||||
self.selectedPoint = self.flight.points[0]
|
||||
self.update_list()
|
||||
@ -33,18 +40,49 @@ class QFlightWaypointList(QTableView):
|
||||
|
||||
def update_list(self):
|
||||
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.name = takeoff.pretty_name = "Take Off from " + self.flight.from_cp.name
|
||||
self.model.appendRow(QWaypointItem(takeoff, 0))
|
||||
item = QStandardItem("0 feet AGL")
|
||||
item.setEditable(False)
|
||||
self.model.setItem(0, 1, item)
|
||||
for i, point in enumerate(self.flight.points):
|
||||
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]))
|
||||
item.setEditable(False)
|
||||
self.model.setItem(self.model.rowCount()-1, 1, item)
|
||||
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)), QItemSelectionModel.Select)
|
||||
takeoff.alt_type = "RADIO"
|
||||
|
||||
waypoints = itertools.chain([takeoff], self.flight.points)
|
||||
for row, waypoint in enumerate(waypoints):
|
||||
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.setItem(row, 0, QWaypointItem(waypoint, row))
|
||||
|
||||
altitude = meter_to_feet(waypoint.alt)
|
||||
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):
|
||||
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)
|
||||
|
||||
rlayout = QVBoxLayout()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user