mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Estimate TOTs for packages.
We estimate the longest possible time from mission start to TOT for all flights in a package and use that to set the TOT (plus any delay used to stagger flights). This both cuts down on loiter time for shorter flights and ensures that long flights will make it to the target in time. This is also used to compute the start time for the AI, so the explicit delay option is no longer needed.
This commit is contained in:
parent
d414c00b74
commit
974b6590d8
405
gen/aircraft.py
405
gen/aircraft.py
@ -57,14 +57,14 @@ from dcs.task import (
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import Event, TriggerOnce
|
||||
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
||||
from dcs.unitgroup import FlyingGroup, Group, ShipGroup, StaticGroup
|
||||
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 meter_to_nm, nm_to_meter
|
||||
from game.utils import nm_to_meter
|
||||
from gen.airfields import RunwayData
|
||||
from gen.airsupportgen import AirSupport
|
||||
from gen.ato import AirTaskingOrder, Package
|
||||
@ -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 import TheaterGroundObject
|
||||
from theater.controlpoint import ControlPoint, ControlPointType
|
||||
from .conflictgen import Conflict
|
||||
from .flights.traveltime import PackageWaypointTiming, TotEstimator
|
||||
from .naming import namegen
|
||||
|
||||
WARM_START_HELI_AIRSPEED = 120
|
||||
@ -86,8 +87,6 @@ WARM_START_HELI_ALT = 500
|
||||
WARM_START_ALTITUDE = 3000
|
||||
WARM_START_AIRSPEED = 550
|
||||
|
||||
CAP_DURATION = 30 # minutes
|
||||
|
||||
RTB_ALTITUDE = 800
|
||||
RTB_DISTANCE = 5000
|
||||
HELI_ALT = 500
|
||||
@ -217,7 +216,7 @@ class FlightData:
|
||||
#: True if this flight belongs to the player's coalition.
|
||||
friendly: bool
|
||||
|
||||
#: Number of minutes after mission start the flight is set to depart.
|
||||
#: Number of seconds after mission start the flight is set to depart.
|
||||
departure_delay: int
|
||||
|
||||
#: Arrival airport.
|
||||
@ -533,148 +532,6 @@ 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):
|
||||
@ -702,8 +559,13 @@ class AircraftConflictGenerator:
|
||||
except KeyError:
|
||||
return get_fallback_channel(airframe)
|
||||
|
||||
def _start_type(self) -> StartType:
|
||||
return self.settings.cold_start and StartType.Cold or StartType.Warm
|
||||
@staticmethod
|
||||
def _start_type(start_type: str) -> StartType:
|
||||
if start_type == "Runway":
|
||||
return StartType.Runway
|
||||
elif start_type == "Cold":
|
||||
return StartType.Cold
|
||||
return StartType.Warm
|
||||
|
||||
def _setup_group(self, group: FlyingGroup, for_task: Type[Task],
|
||||
flight: Flight, dynamic_runways: Dict[str, RunwayData]):
|
||||
@ -808,15 +670,10 @@ class AircraftConflictGenerator:
|
||||
return runways[0]
|
||||
|
||||
def _generate_at_airport(self, name: str, side: Country,
|
||||
unit_type: FlyingType, count: int,
|
||||
client_count: int,
|
||||
airport: Optional[Airport] = None,
|
||||
start_type=None) -> FlyingGroup:
|
||||
unit_type: FlyingType, count: int, start_type: str,
|
||||
airport: Optional[Airport] = None) -> FlyingGroup:
|
||||
assert count > 0
|
||||
|
||||
if start_type is None:
|
||||
start_type = self._start_type()
|
||||
|
||||
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
|
||||
return self.m.flight_group_from_airport(
|
||||
country=side,
|
||||
@ -824,11 +681,11 @@ class AircraftConflictGenerator:
|
||||
aircraft_type=unit_type,
|
||||
airport=airport,
|
||||
maintask=None,
|
||||
start_type=start_type,
|
||||
start_type=self._start_type(start_type),
|
||||
group_size=count,
|
||||
parking_slots=None)
|
||||
|
||||
def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: Point) -> FlyingGroup:
|
||||
def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, at: Point) -> FlyingGroup:
|
||||
assert count > 0
|
||||
|
||||
if unit_type in helicopters.helicopter_map.values():
|
||||
@ -850,21 +707,16 @@ class AircraftConflictGenerator:
|
||||
altitude=alt,
|
||||
speed=speed,
|
||||
maintask=None,
|
||||
start_type=self._start_type(),
|
||||
group_size=count)
|
||||
|
||||
group.points[0].alt_type = "RADIO"
|
||||
return group
|
||||
|
||||
def _generate_at_group(self, name: str, side: Country,
|
||||
unit_type: FlyingType, count: int, client_count: int,
|
||||
at: Union[ShipGroup, StaticGroup],
|
||||
start_type=None) -> FlyingGroup:
|
||||
unit_type: FlyingType, count: int, start_type: str,
|
||||
at: Union[ShipGroup, StaticGroup]) -> FlyingGroup:
|
||||
assert count > 0
|
||||
|
||||
if start_type is None:
|
||||
start_type = self._start_type()
|
||||
|
||||
logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
|
||||
return self.m.flight_group_from_unit(
|
||||
country=side,
|
||||
@ -872,34 +724,9 @@ class AircraftConflictGenerator:
|
||||
aircraft_type=unit_type,
|
||||
pad_group=at,
|
||||
maintask=None,
|
||||
start_type=start_type,
|
||||
start_type=self._start_type(start_type),
|
||||
group_size=count)
|
||||
|
||||
def _generate_group(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: db.StartingPosition):
|
||||
if isinstance(at, Point):
|
||||
return self._generate_inflight(name, side, unit_type, count, client_count, at)
|
||||
elif isinstance(at, Group):
|
||||
takeoff_ban = unit_type in db.CARRIER_TAKEOFF_BAN
|
||||
ai_ban = client_count == 0 and self.settings.only_player_takeoff
|
||||
|
||||
if not takeoff_ban and not ai_ban:
|
||||
return self._generate_at_group(name, side, unit_type, count, client_count, at)
|
||||
else:
|
||||
return self._generate_inflight(name, side, unit_type, count, client_count, at.position)
|
||||
elif isinstance(at, Airport):
|
||||
takeoff_ban = unit_type in db.TAKEOFF_BAN
|
||||
ai_ban = client_count == 0 and self.settings.only_player_takeoff
|
||||
|
||||
if not takeoff_ban and not ai_ban:
|
||||
try:
|
||||
return self._generate_at_airport(name, side, unit_type, count, client_count, at)
|
||||
except NoParkingSlotError:
|
||||
logging.info("No parking slot found at " + at.name + ", switching to air start.")
|
||||
pass
|
||||
return self._generate_inflight(name, side, unit_type, count, client_count, at.position)
|
||||
else:
|
||||
assert False
|
||||
|
||||
def _add_radio_waypoint(self, group: FlyingGroup, position, altitude: int, airspeed: int = 600):
|
||||
point = group.add_waypoint(position, altitude, airspeed)
|
||||
point.alt_type = "RADIO"
|
||||
@ -970,110 +797,91 @@ 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, timing, dynamic_runways)
|
||||
self.setup_group_activation_trigger(flight, group)
|
||||
self.setup_flight_group(group, package, flight, timing,
|
||||
dynamic_runways)
|
||||
|
||||
def setup_group_activation_trigger(self, flight, group):
|
||||
if flight.scheduled_in > 0 and flight.client_count == 0:
|
||||
def set_activation_time(self, flight: Flight, group: FlyingGroup,
|
||||
delay: int) -> None:
|
||||
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
||||
# mission editor. Waypoint times will be relative to the group
|
||||
# activation time rather than in absolute local time. A flight delayed
|
||||
# until 09:10 when the overall mission start time is 09:00, with a join
|
||||
# time of 09:30 will show the join time as 00:30, not 09:30.
|
||||
group.late_activation = True
|
||||
|
||||
if flight.start_type != "In Flight" and flight.from_cp.cptype not in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]:
|
||||
group.late_activation = False
|
||||
group.uncontrolled = True
|
||||
activation_trigger = TriggerOnce(
|
||||
Event.NoEvent, f"FlightLateActivationTrigger{group.id}")
|
||||
activation_trigger.add_condition(TimeAfter(seconds=delay))
|
||||
|
||||
activation_trigger = TriggerOnce(Event.NoEvent, "FlightStartTrigger" + str(group.id))
|
||||
activation_trigger.add_condition(TimeAfter(seconds=flight.scheduled_in * 60))
|
||||
if (flight.from_cp.cptype == ControlPointType.AIRBASE):
|
||||
if flight.from_cp.captured:
|
||||
activation_trigger.add_condition(
|
||||
CoalitionHasAirdrome(self.game.get_player_coalition_id(), flight.from_cp.id))
|
||||
else:
|
||||
activation_trigger.add_condition(
|
||||
CoalitionHasAirdrome(self.game.get_enemy_coalition_id(), flight.from_cp.id))
|
||||
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
|
||||
activation_trigger.add_action(ActivateGroup(group.id))
|
||||
self.m.triggerrules.triggers.append(activation_trigger)
|
||||
|
||||
if flight.flight_type == FlightType.INTERCEPTION:
|
||||
self.setup_interceptor_triggers(group, flight, activation_trigger)
|
||||
def set_startup_time(self, flight: Flight, group: FlyingGroup,
|
||||
delay: int) -> None:
|
||||
# Uncontrolled causes the AI unit to spawn, but not begin startup.
|
||||
group.uncontrolled = True
|
||||
|
||||
group.add_trigger_action(StartCommand())
|
||||
activation_trigger.add_action(AITaskPush(group.id, len(group.tasks)))
|
||||
activation_trigger = TriggerOnce(Event.NoEvent,
|
||||
f"FlightStartTrigger{group.id}")
|
||||
activation_trigger.add_condition(TimeAfter(seconds=delay))
|
||||
|
||||
self.m.triggerrules.triggers.append(activation_trigger)
|
||||
else:
|
||||
group.late_activation = True
|
||||
activation_trigger = TriggerOnce(Event.NoEvent, "FlightLateActivationTrigger" + str(group.id))
|
||||
activation_trigger.add_condition(TimeAfter(seconds=flight.scheduled_in*60))
|
||||
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
|
||||
group.add_trigger_action(StartCommand())
|
||||
activation_trigger.add_action(AITaskPush(group.id, len(group.tasks)))
|
||||
self.m.triggerrules.triggers.append(activation_trigger)
|
||||
|
||||
if(flight.from_cp.cptype == ControlPointType.AIRBASE):
|
||||
if flight.from_cp.captured:
|
||||
activation_trigger.add_condition(CoalitionHasAirdrome(self.game.get_player_coalition_id(), flight.from_cp.id))
|
||||
else:
|
||||
activation_trigger.add_condition(CoalitionHasAirdrome(self.game.get_enemy_coalition_id(), flight.from_cp.id))
|
||||
def prevent_spawn_at_hostile_airbase(self, flight: Flight,
|
||||
trigger: TriggerRule) -> None:
|
||||
# Prevent delayed flights from spawning at airbases if they were
|
||||
# captured before they've spawned.
|
||||
if flight.from_cp.cptype != ControlPointType.AIRBASE:
|
||||
return
|
||||
|
||||
if flight.flight_type == FlightType.INTERCEPTION:
|
||||
self.setup_interceptor_triggers(group, flight, activation_trigger)
|
||||
|
||||
activation_trigger.add_action(ActivateGroup(group.id))
|
||||
self.m.triggerrules.triggers.append(activation_trigger)
|
||||
|
||||
def setup_interceptor_triggers(self, group, flight, activation_trigger):
|
||||
|
||||
detection_zone = self.m.triggers.add_triggerzone(flight.from_cp.position, radius=25000, hidden=False, name="ITZ")
|
||||
if flight.from_cp.captured:
|
||||
activation_trigger.add_condition(PartOfCoalitionInZone(self.game.get_enemy_color(), detection_zone.id)) # TODO : support unit type in part of coalition
|
||||
activation_trigger.add_action(MessageToAll(String("WARNING : Enemy aircraft have been detected in the vicinity of " + flight.from_cp.name + ". Interceptors are taking off."), 20))
|
||||
coalition = self.game.get_player_coalition_id()
|
||||
else:
|
||||
activation_trigger.add_condition(PartOfCoalitionInZone(self.game.get_player_color(), detection_zone.id))
|
||||
activation_trigger.add_action(MessageToAll(String("WARNING : We have detected that enemy aircraft are scrambling for an interception on " + flight.from_cp.name + " airbase."), 20))
|
||||
coalition = self.game.get_enemy_coalition_id()
|
||||
|
||||
trigger.add_condition(
|
||||
CoalitionHasAirdrome(coalition, flight.from_cp.id))
|
||||
|
||||
def generate_planned_flight(self, cp, country, flight:Flight):
|
||||
try:
|
||||
if flight.client_count == 0 and self.game.settings.perf_ai_parking_start:
|
||||
flight.start_type = "Cold"
|
||||
|
||||
if flight.start_type == "In Flight":
|
||||
group = self._generate_group(
|
||||
group = self._generate_inflight(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
client_count=0,
|
||||
at=cp.position)
|
||||
elif cp.is_fleet:
|
||||
group_name = cp.get_carrier_group_name()
|
||||
group = self._generate_at_group(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
start_type=flight.start_type,
|
||||
at=self.m.find_group(group_name))
|
||||
else:
|
||||
st = StartType.Runway
|
||||
if flight.start_type == "Cold":
|
||||
st = StartType.Cold
|
||||
elif flight.start_type == "Warm":
|
||||
st = StartType.Warm
|
||||
|
||||
if cp.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]:
|
||||
group_name = cp.get_carrier_group_name()
|
||||
group = self._generate_at_group(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
client_count=0,
|
||||
at=self.m.find_group(group_name),
|
||||
start_type=st)
|
||||
else:
|
||||
group = self._generate_at_airport(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
client_count=0,
|
||||
airport=cp.airport,
|
||||
start_type=st)
|
||||
group = self._generate_at_airport(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
start_type=flight.start_type,
|
||||
airport=cp.airport)
|
||||
except Exception as e:
|
||||
# Generated when there is no place on Runway or on Parking Slots
|
||||
logging.error(e)
|
||||
logging.warning("No room on runway or parking slots. Starting from the air.")
|
||||
flight.start_type = "In Flight"
|
||||
group = self._generate_group(
|
||||
group = self._generate_inflight(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
client_count=0,
|
||||
at=cp.position)
|
||||
group.points[0].alt = 1500
|
||||
|
||||
@ -1179,8 +987,8 @@ class AircraftConflictGenerator:
|
||||
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,
|
||||
def setup_flight_group(self, group: FlyingGroup, package: Package,
|
||||
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,
|
||||
@ -1204,6 +1012,9 @@ class AircraftConflictGenerator:
|
||||
for waypoint in flight.points:
|
||||
waypoint.tot = None
|
||||
|
||||
takeoff_point = FlightWaypoint.from_pydcs(group.points[0],
|
||||
flight.from_cp)
|
||||
self.set_takeoff_time(takeoff_point, package, flight, group)
|
||||
for point in flight.points:
|
||||
if point.only_for_player and not flight.client_count:
|
||||
continue
|
||||
@ -1214,9 +1025,53 @@ class AircraftConflictGenerator:
|
||||
|
||||
# Set here rather than when the FlightData is created so they waypoints
|
||||
# have their TOTs set.
|
||||
self.flights[-1].waypoints = flight.points
|
||||
self.flights[-1].waypoints = [takeoff_point] + flight.points
|
||||
self._setup_custom_payload(flight, group)
|
||||
|
||||
def set_takeoff_time(self, waypoint: FlightWaypoint, package: Package,
|
||||
flight: Flight, group: FlyingGroup) -> None:
|
||||
estimator = TotEstimator(package)
|
||||
start_time = estimator.mission_start_time(flight)
|
||||
|
||||
if start_time > 0:
|
||||
if self.should_activate_late(flight):
|
||||
# Late activation causes the aircraft to not be spawned until
|
||||
# triggered.
|
||||
self.set_activation_time(flight, group, start_time)
|
||||
elif flight.start_type == "Cold":
|
||||
# Setting the start time causes the AI to wait until the
|
||||
# specified time to begin their startup sequence.
|
||||
self.set_startup_time(flight, group, start_time)
|
||||
|
||||
# And setting *our* waypoint TOT causes the takeoff time to show up in
|
||||
# the player's kneeboard.
|
||||
waypoint.tot = estimator.takeoff_time_for_flight(flight)
|
||||
|
||||
@staticmethod
|
||||
def should_activate_late(flight: Flight) -> bool:
|
||||
if flight.client_count:
|
||||
# Never delay players. Note that cold start player flights with
|
||||
# AI members will still be marked as uncontrolled until the start
|
||||
# trigger fires to postpone engine start.
|
||||
#
|
||||
# Player flights that start on the runway or in the air will start
|
||||
# immediately, and AI flight members will not be delayed.
|
||||
return False
|
||||
|
||||
if flight.start_type != "Cold":
|
||||
# Avoid spawning aircraft in the air or on the runway until it's
|
||||
# time for their mission. Also avoid burning through gas spawning
|
||||
# hot aircraft hours before their takeoff time.
|
||||
return True
|
||||
|
||||
if flight.from_cp.is_fleet:
|
||||
# Carrier spawns will crowd the carrier deck, especially without
|
||||
# super carrier.
|
||||
# TODO: Is there enough parking on the supercarrier?
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class PydcsWaypointBuilder:
|
||||
def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup,
|
||||
@ -1243,10 +1098,8 @@ class PydcsWaypointBuilder:
|
||||
waypoint.speed_locked = False
|
||||
|
||||
@classmethod
|
||||
def for_waypoint(cls, waypoint: FlightWaypoint,
|
||||
group: FlyingGroup,
|
||||
flight: Flight,
|
||||
timing: PackageWaypointTiming,
|
||||
def for_waypoint(cls, waypoint: FlightWaypoint, group: FlyingGroup,
|
||||
flight: Flight, timing: PackageWaypointTiming,
|
||||
mission: Mission) -> PydcsWaypointBuilder:
|
||||
builders = {
|
||||
FlightWaypointType.EGRESS: EgressPointBuilder,
|
||||
@ -1391,9 +1244,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||
))
|
||||
|
||||
start = self.timing.race_track_start
|
||||
if start is not None:
|
||||
self.set_waypoint_tot(waypoint, start)
|
||||
self.set_waypoint_tot(waypoint, self.timing.race_track_start)
|
||||
racetrack.stop_after_time(self.timing.race_track_end)
|
||||
waypoint.add_task(racetrack)
|
||||
return waypoint
|
||||
|
||||
@ -52,7 +52,7 @@ class Package:
|
||||
delay: int = field(default=0)
|
||||
|
||||
#: Desired TOT measured in seconds from mission start.
|
||||
time_over_target: Optional[int] = field(default=None)
|
||||
time_over_target: int = field(default=0)
|
||||
|
||||
waypoints: Optional[PackageWaypoints] = field(default=None)
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import datetime
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
@ -116,9 +117,10 @@ class BriefingGenerator(MissionInfoGenerator):
|
||||
assert not flight.client_units
|
||||
aircraft = flight.aircraft_type
|
||||
flight_unit_name = db.unit_type_name(aircraft)
|
||||
delay = datetime.timedelta(seconds=flight.departure_delay)
|
||||
self.description += (
|
||||
f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, "
|
||||
f"departing in {flight.departure_delay} minutes\n"
|
||||
f"departing in {delay}\n"
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
|
||||
@ -33,6 +33,7 @@ from gen.flights.flight import (
|
||||
FlightType,
|
||||
)
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from theater import (
|
||||
ControlPoint,
|
||||
FrontLine,
|
||||
@ -185,11 +186,13 @@ class PackageBuilder:
|
||||
def __init__(self, location: MissionTarget,
|
||||
closest_airfields: ClosestAirfields,
|
||||
global_inventory: GlobalAircraftInventory,
|
||||
is_player: bool) -> None:
|
||||
is_player: bool,
|
||||
start_type: str) -> None:
|
||||
self.package = Package(location)
|
||||
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
|
||||
is_player)
|
||||
self.global_inventory = global_inventory
|
||||
self.start_type = start_type
|
||||
|
||||
def plan_flight(self, plan: ProposedFlight) -> bool:
|
||||
"""Allocates aircraft for the given flight and adds them to the package.
|
||||
@ -203,7 +206,8 @@ class PackageBuilder:
|
||||
if assignment is None:
|
||||
return False
|
||||
airfield, aircraft = assignment
|
||||
flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task)
|
||||
flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task,
|
||||
self.start_type)
|
||||
self.package.add_flight(flight)
|
||||
return True
|
||||
|
||||
@ -444,11 +448,18 @@ class CoalitionMissionPlanner:
|
||||
|
||||
def plan_mission(self, mission: ProposedMission) -> None:
|
||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||
|
||||
if self.game.settings.perf_ai_parking_start:
|
||||
start_type = "Cold"
|
||||
else:
|
||||
start_type = "Warm"
|
||||
|
||||
builder = PackageBuilder(
|
||||
mission.location,
|
||||
self.objective_finder.closest_airfields_to(mission.location),
|
||||
self.game.aircraft_inventory,
|
||||
self.is_player
|
||||
self.is_player,
|
||||
start_type
|
||||
)
|
||||
|
||||
missing_types: Set[FlightType] = set()
|
||||
@ -497,16 +508,18 @@ class CoalitionMissionPlanner:
|
||||
margin=5
|
||||
)
|
||||
for package in self.ato.packages:
|
||||
tot = TotEstimator(package).earliest_tot()
|
||||
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
|
||||
# All CAP missions should be on station ASAP.
|
||||
package.time_over_target = tot
|
||||
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
|
||||
# But other packages should be spread out a bit. Note that take
|
||||
# times are delayed, but all aircraft will become active at
|
||||
# mission start. This makes it more worthwhile to attack enemy
|
||||
# airfields to hit grounded aircraft, since they're more likely
|
||||
# to be present. Runway and air started aircraft will be
|
||||
# delayed until their takeoff time by AirConflictGenerator.
|
||||
package.time_over_target = next(start_time) * 60 + tot
|
||||
|
||||
def message(self, title, text) -> None:
|
||||
"""Emits a planning message to the player.
|
||||
|
||||
@ -97,7 +97,6 @@ class FlightWaypoint:
|
||||
# flight's offset in the UI.
|
||||
self.tot: Optional[int] = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_pydcs(cls, point: MovingPoint,
|
||||
from_cp: ControlPoint) -> "FlightWaypoint":
|
||||
@ -130,13 +129,10 @@ class Flight:
|
||||
client_count: int = 0
|
||||
use_custom_loadout = False
|
||||
preset_loadout_name = ""
|
||||
start_type = "Runway"
|
||||
group = False # Contains DCS Mission group data after mission has been generated
|
||||
|
||||
# How long before this flight should take off
|
||||
scheduled_in = 0
|
||||
|
||||
def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint, flight_type: FlightType):
|
||||
def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint,
|
||||
flight_type: FlightType, start_type: str) -> None:
|
||||
self.unit_type = unit_type
|
||||
self.count = count
|
||||
self.from_cp = from_cp
|
||||
@ -144,11 +140,16 @@ class Flight:
|
||||
self.points: List[FlightWaypoint] = []
|
||||
self.targets: List[MissionTarget] = []
|
||||
self.loadout: Dict[str, str] = {}
|
||||
self.start_type = "Runway"
|
||||
self.start_type = start_type
|
||||
# Late activation delay in seconds from mission start. This is not
|
||||
# the same as the flight's takeoff time. Takeoff time depends on the
|
||||
# mission's TOT and the other flights in the package. Takeoff time is
|
||||
# determined by AirConflictGenerator.
|
||||
self.scheduled_in = 0
|
||||
|
||||
def __repr__(self):
|
||||
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
|
||||
+ " in " + str(self.scheduled_in) + " minutes (" + str(len(self.points)) + " wpt)"
|
||||
+ " (" + str(len(self.points)) + " wpt)"
|
||||
|
||||
|
||||
# Test
|
||||
@ -157,6 +158,6 @@ if __name__ == '__main__':
|
||||
from theater import ControlPoint, Point, List
|
||||
|
||||
from_cp = ControlPoint(0, "AA", Point(0, 0), Point(0, 0), [], 0, 0)
|
||||
f = Flight(A_10C(), 4, from_cp, FlightType.CAS)
|
||||
f = Flight(A_10C(), 4, from_cp, FlightType.CAS, "Cold")
|
||||
f.scheduled_in = 50
|
||||
print(f)
|
||||
|
||||
285
gen/flights/traveltime.py
Normal file
285
gen/flights/traveltime.py
Normal file
@ -0,0 +1,285 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
from game.utils import meter_to_nm
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import (
|
||||
Flight,
|
||||
FlightType,
|
||||
FlightWaypoint,
|
||||
FlightWaypointType,
|
||||
)
|
||||
|
||||
|
||||
CAP_DURATION = 30 # Minutes
|
||||
CAP_TYPES = (FlightType.BARCAP, FlightType.CAP)
|
||||
|
||||
|
||||
class GroundSpeed:
|
||||
@classmethod
|
||||
def for_package(cls, package: Package) -> int:
|
||||
speeds = []
|
||||
for flight in package.flights:
|
||||
speeds.append(cls.for_flight(flight))
|
||||
return min(speeds) # knots
|
||||
|
||||
@staticmethod
|
||||
def for_flight(_flight: Flight) -> int:
|
||||
# TODO: Gather data so this is useful.
|
||||
# TODO: Expose both a cruise speed and target speed.
|
||||
# The cruise speed can be used for ascent, hold, join, and RTB to save
|
||||
# on fuel, but mission speed will be fast enough to keep the flight
|
||||
# safer.
|
||||
return 400 # knots
|
||||
|
||||
|
||||
class TravelTime:
|
||||
@staticmethod
|
||||
def between_points(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 TotEstimator:
|
||||
# An extra five minutes given as wiggle room. Expected to be spent at the
|
||||
# hold point performing any last minute configuration.
|
||||
HOLD_TIME = 5 * 60
|
||||
|
||||
def __init__(self, package: Package) -> None:
|
||||
self.package = package
|
||||
self.timing = PackageWaypointTiming.for_package(package)
|
||||
|
||||
def mission_start_time(self, flight: Flight) -> int:
|
||||
takeoff_time = self.takeoff_time_for_flight(flight)
|
||||
startup_time = self.estimate_startup(flight)
|
||||
ground_ops_time = self.estimate_ground_ops(flight)
|
||||
return takeoff_time - startup_time - ground_ops_time
|
||||
|
||||
def takeoff_time_for_flight(self, flight: Flight) -> int:
|
||||
stop_types = {FlightWaypointType.JOIN, FlightWaypointType.PATROL_TRACK}
|
||||
travel_time = self.estimate_waypoints_to_target(flight, stop_types)
|
||||
if travel_time is None:
|
||||
logging.warning("Found no join point or patrol point. Cannot "
|
||||
f"estimate takeoff time takeoff time for {flight}")
|
||||
# Takeoff immediately.
|
||||
return 0
|
||||
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
start_time = self.timing.race_track_start
|
||||
else:
|
||||
start_time = self.timing.join
|
||||
return start_time - travel_time - self.HOLD_TIME
|
||||
|
||||
def earliest_tot(self) -> int:
|
||||
return max((
|
||||
self.earliest_tot_for_flight(f) for f in self.package.flights
|
||||
)) + self.HOLD_TIME
|
||||
|
||||
def earliest_tot_for_flight(self, flight: Flight) -> int:
|
||||
"""Estimate fastest time from mission start to the target position.
|
||||
|
||||
For CAP missions, this is time to race track start.
|
||||
|
||||
For other mission types this is the time to the mission target.
|
||||
|
||||
Args:
|
||||
flight: The flight to get the earliest TOT time for.
|
||||
|
||||
Returns:
|
||||
The earliest possible TOT for the given flight in seconds. Returns 0
|
||||
if an ingress point cannot be found.
|
||||
"""
|
||||
stop_types = {
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
}
|
||||
time_to_ingress = self.estimate_waypoints_to_target(flight, stop_types)
|
||||
if time_to_ingress is None:
|
||||
logging.warning(
|
||||
f"Found no ingress types. Cannot estimate TOT for {flight}")
|
||||
# Return 0 so this flight's travel time does not affect the rest of
|
||||
# the package.
|
||||
return 0
|
||||
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
# The racetrack start *is* the target. The package target is the
|
||||
# protected objective.
|
||||
time_to_target = 0
|
||||
else:
|
||||
assert self.package.waypoints is not None
|
||||
time_to_target = TravelTime.between_points(
|
||||
self.package.waypoints.ingress, self.package.target.position,
|
||||
GroundSpeed.for_package(self.package))
|
||||
return sum([
|
||||
self.estimate_startup(flight),
|
||||
self.estimate_ground_ops(flight),
|
||||
time_to_ingress,
|
||||
time_to_target,
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def estimate_startup(flight: Flight) -> int:
|
||||
if flight.start_type == "Cold":
|
||||
return 10 * 60
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def estimate_ground_ops(flight: Flight) -> int:
|
||||
if flight.start_type in ("Runway", "In Flight"):
|
||||
return 0
|
||||
if flight.from_cp.is_fleet:
|
||||
return 2 * 60
|
||||
else:
|
||||
return 5 * 60
|
||||
|
||||
def estimate_waypoints_to_target(
|
||||
self, flight: Flight,
|
||||
stop_types: Iterable[FlightWaypointType]) -> Optional[int]:
|
||||
total = 0
|
||||
previous_position = flight.from_cp.position
|
||||
for waypoint in flight.points:
|
||||
position = Point(waypoint.x, waypoint.y)
|
||||
total += TravelTime.between_points(
|
||||
previous_position, position,
|
||||
self.speed_to_waypoint(flight, waypoint)
|
||||
)
|
||||
previous_position = position
|
||||
if waypoint.waypoint_type in stop_types:
|
||||
return total
|
||||
|
||||
return None
|
||||
|
||||
def speed_to_waypoint(self, flight: Flight,
|
||||
waypoint: FlightWaypoint) -> int:
|
||||
pre_join = (FlightWaypointType.LOITER, FlightWaypointType.JOIN)
|
||||
if waypoint.waypoint_type == FlightWaypointType.ASCEND_POINT:
|
||||
# Flights that start airborne already have some altitude and a good
|
||||
# amount of speed.
|
||||
factor = 1.0 if flight.start_type == "In Flight" else 0.5
|
||||
return int(GroundSpeed.for_flight(flight) * factor)
|
||||
elif waypoint.waypoint_type in pre_join:
|
||||
return GroundSpeed.for_flight(flight)
|
||||
return GroundSpeed.for_package(self.package)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackageWaypointTiming:
|
||||
#: The package being scheduled.
|
||||
package: Package
|
||||
|
||||
#: The package join time.
|
||||
join: int
|
||||
|
||||
#: The ingress waypoint TOT.
|
||||
ingress: int
|
||||
|
||||
#: The egress waypoint TOT.
|
||||
egress: int
|
||||
|
||||
#: The package split time.
|
||||
split: int
|
||||
|
||||
@property
|
||||
def target(self) -> int:
|
||||
"""The package time over target."""
|
||||
assert self.package.time_over_target is not None
|
||||
return self.package.time_over_target
|
||||
|
||||
@property
|
||||
def race_track_start(self) -> int:
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
return self.package.time_over_target
|
||||
else:
|
||||
return self.ingress
|
||||
|
||||
@property
|
||||
def race_track_end(self) -> int:
|
||||
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 - TravelTime.between_points(
|
||||
hold_point,
|
||||
self.package.waypoints.join,
|
||||
GroundSpeed.for_flight(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.waypoints is not None
|
||||
|
||||
group_ground_speed = GroundSpeed.for_package(package)
|
||||
|
||||
ingress = package.time_over_target - TravelTime.between_points(
|
||||
package.waypoints.ingress,
|
||||
package.target.position,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
join = ingress - TravelTime.between_points(
|
||||
package.waypoints.join,
|
||||
package.waypoints.ingress,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
egress = package.time_over_target + TravelTime.between_points(
|
||||
package.target.position,
|
||||
package.waypoints.egress,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
split = egress + TravelTime.between_points(
|
||||
package.waypoints.egress,
|
||||
package.waypoints.split,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
return cls(package, join, ingress, egress, split)
|
||||
@ -1,7 +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 typing import Any, Callable, Dict, Iterator, Optional, TypeVar
|
||||
|
||||
from PySide2.QtCore import (
|
||||
QAbstractListModel,
|
||||
@ -15,6 +14,7 @@ from game import db
|
||||
from game.game import Game
|
||||
from gen.ato import AirTaskingOrder, Package
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.uiconstants import AIRCRAFT_ICONS
|
||||
from theater.missiontarget import MissionTarget
|
||||
|
||||
@ -119,15 +119,15 @@ class PackageModel(QAbstractListModel):
|
||||
return flight
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def text_for_flight(flight: Flight) -> str:
|
||||
def text_for_flight(self, flight: Flight) -> str:
|
||||
"""Returns the text that should be displayed for the flight."""
|
||||
task = flight.flight_type.name
|
||||
count = flight.count
|
||||
name = db.unit_type_name(flight.unit_type)
|
||||
delay = flight.scheduled_in
|
||||
estimator = TotEstimator(self.package)
|
||||
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
||||
origin = flight.from_cp.name
|
||||
return f"[{task}] {count} x {name} from {origin} in {delay} minutes"
|
||||
return f"[{task}] {count} x {name} from {origin} in {delay}"
|
||||
|
||||
@staticmethod
|
||||
def icon_for_flight(flight: Flight) -> Optional[QIcon]:
|
||||
|
||||
@ -147,6 +147,8 @@ class QTopPanel(QFrame):
|
||||
if not self.ato_has_clients() and not self.confirm_no_client_launch():
|
||||
return
|
||||
|
||||
# TODO: Verify no negative start times.
|
||||
|
||||
# TODO: Refactor this nonsense.
|
||||
game_event = None
|
||||
for event in self.game.events:
|
||||
|
||||
@ -24,6 +24,7 @@ from PySide2.QtWidgets import (
|
||||
from game import db
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
||||
|
||||
@ -33,6 +34,10 @@ class FlightDelegate(QStyledItemDelegate):
|
||||
HMARGIN = 4
|
||||
VMARGIN = 4
|
||||
|
||||
def __init__(self, package: Package) -> None:
|
||||
super().__init__()
|
||||
self.package = package
|
||||
|
||||
def get_font(self, option: QStyleOptionViewItem) -> QFont:
|
||||
font = QFont(option.font)
|
||||
font.setPointSize(self.FONT_SIZE)
|
||||
@ -47,8 +52,9 @@ class FlightDelegate(QStyledItemDelegate):
|
||||
task = flight.flight_type.name
|
||||
count = flight.count
|
||||
name = db.unit_type_name(flight.unit_type)
|
||||
delay = flight.scheduled_in
|
||||
return f"[{task}] {count} x {name} in {delay} minutes"
|
||||
estimator = TotEstimator(self.package)
|
||||
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
||||
return f"[{task}] {count} x {name} in {delay}"
|
||||
|
||||
def second_row_text(self, index: QModelIndex) -> str:
|
||||
flight = self.flight(index)
|
||||
@ -128,7 +134,8 @@ class QFlightList(QListView):
|
||||
super().__init__()
|
||||
self.package_model = model
|
||||
self.set_package(model)
|
||||
self.setItemDelegate(FlightDelegate())
|
||||
if model is not None:
|
||||
self.setItemDelegate(FlightDelegate(model.package))
|
||||
self.setIconSize(QSize(91, 24))
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||
|
||||
@ -138,6 +145,7 @@ class QFlightList(QListView):
|
||||
self.disconnect_model()
|
||||
else:
|
||||
self.package_model = model
|
||||
self.setItemDelegate(FlightDelegate(model.package))
|
||||
self.setModel(model)
|
||||
# noinspection PyUnresolvedReferences
|
||||
model.deleted.connect(self.disconnect_model)
|
||||
|
||||
@ -21,7 +21,8 @@ from game import Game, db
|
||||
from game.data.aaa_db import AAA_UNITS
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from game.utils import meter_to_feet
|
||||
from gen import Conflict, Package, PackageWaypointTiming
|
||||
from gen import Conflict, PackageWaypointTiming
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
from qt_ui.displayoptions import DisplayOptions
|
||||
from qt_ui.models import GameModel
|
||||
|
||||
@ -1,26 +1,28 @@
|
||||
import datetime
|
||||
|
||||
from PySide2.QtGui import QStandardItem, QIcon
|
||||
|
||||
from game import db
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.uiconstants import AIRCRAFT_ICONS
|
||||
|
||||
|
||||
# TODO: Replace with QFlightList.
|
||||
class QFlightItem(QStandardItem):
|
||||
|
||||
def __init__(self, flight:Flight):
|
||||
def __init__(self, package: Package, flight: Flight):
|
||||
super(QFlightItem, self).__init__()
|
||||
self.package = package
|
||||
self.flight = flight
|
||||
|
||||
if db.unit_type_name(self.flight.unit_type).replace("/", " ") in AIRCRAFT_ICONS.keys():
|
||||
icon = QIcon((AIRCRAFT_ICONS[db.unit_type_name(self.flight.unit_type)]))
|
||||
self.setIcon(icon)
|
||||
self.setEditable(False)
|
||||
estimator = TotEstimator(self.package)
|
||||
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
||||
self.setText("["+str(self.flight.flight_type.name[:6])+"] "
|
||||
+ str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type)
|
||||
+ " in " + str(self.flight.scheduled_in) + " minutes")
|
||||
|
||||
def update(self, flight):
|
||||
self.flight = flight
|
||||
self.setText("[" + str(self.flight.flight_type.name[:6]) + "] "
|
||||
+ str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type)
|
||||
+ " in " + str(self.flight.scheduled_in) + " minutes")
|
||||
+ " in " + str(delay))
|
||||
|
||||
@ -116,11 +116,14 @@ class QPackageDialog(QDialog):
|
||||
|
||||
self.finished.connect(self.on_close)
|
||||
|
||||
def on_close(self, _result) -> None:
|
||||
@staticmethod
|
||||
def on_close(_result) -> None:
|
||||
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||
|
||||
def save_tot(self) -> 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,
|
||||
_deselected: QItemSelection) -> None:
|
||||
@ -182,6 +185,7 @@ class QNewPackageDialog(QPackageDialog):
|
||||
Empty packages may be created. They can be modified later, and will have
|
||||
no effect if empty when the mission is generated.
|
||||
"""
|
||||
self.save_tot()
|
||||
self.ato_model.add_package(self.package_model.package)
|
||||
for flight in self.package_model.package.flights:
|
||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||
@ -227,6 +231,7 @@ class QEditPackageDialog(QPackageDialog):
|
||||
|
||||
def on_done(self) -> None:
|
||||
"""Closes the window."""
|
||||
self.save_tot()
|
||||
self.close()
|
||||
|
||||
def on_delete(self) -> None:
|
||||
|
||||
@ -90,7 +90,11 @@ class QFlightCreator(QDialog):
|
||||
origin = self.airfield_selector.currentData()
|
||||
size = self.flight_size_spinner.value()
|
||||
|
||||
flight = Flight(aircraft, size, origin, task)
|
||||
if self.game.settings.perf_ai_parking_start:
|
||||
start_type = "Cold"
|
||||
else:
|
||||
start_type = "Warm"
|
||||
flight = Flight(aircraft, size, origin, task, start_type)
|
||||
flight.scheduled_in = self.package.delay
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox
|
||||
|
||||
|
||||
# TODO: Remove?
|
||||
class QFlightDepartureEditor(QGroupBox):
|
||||
|
||||
def __init__(self, flight):
|
||||
@ -15,7 +16,7 @@ class QFlightDepartureEditor(QGroupBox):
|
||||
self.departure_delta = QSpinBox(self)
|
||||
self.departure_delta.setMinimum(0)
|
||||
self.departure_delta.setMaximum(120)
|
||||
self.departure_delta.setValue(self.flight.scheduled_in)
|
||||
self.departure_delta.setValue(self.flight.scheduled_in // 60)
|
||||
self.departure_delta.valueChanged.connect(self.change_scheduled)
|
||||
|
||||
layout.addWidget(self.depart_from)
|
||||
@ -27,4 +28,4 @@ class QFlightDepartureEditor(QGroupBox):
|
||||
self.changed = self.departure_delta.valueChanged
|
||||
|
||||
def change_scheduled(self):
|
||||
self.flight.scheduled_in = int(self.departure_delta.value())
|
||||
self.flight.scheduled_in = int(self.departure_delta.value() * 60)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user