mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
This is the first step in a larger project to add play/pause buttons to the Liberation UI so the mission can be generated at any point. docs/design/turnless.md describes the plan. This adds an option to fast forward the turn to first contact before generating the mission. None of that is reflected in the UI (for now), but the miz will be generated with many flights in the air. For now "first contact" means as soon as any flight reaches its IP. I'll follow up to add threat checking so that air-to-air combat also triggers this, as will entering a SAM's threat zone. This also includes an option to halt fast-forward whenever a player flight reaches a certain mission-prep phase. This can be used to avoid fast forwarding past the player's startup time, taxi time, or takeoff time. By default this option is disabled so player aircraft may start in the air (possibly even at their IP if they're the first mission to reach IP). Fuel states do not currently account for distance traveled during fast forward. That will come later. https://github.com/dcs-liberation/dcs_liberation/issues/1681
116 lines
4.0 KiB
Python
116 lines
4.0 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import math
|
|
from datetime import timedelta
|
|
from typing import TYPE_CHECKING
|
|
|
|
from dcs.mapping import Point
|
|
|
|
from game.utils import (
|
|
Distance,
|
|
SPEED_OF_SOUND_AT_SEA_LEVEL,
|
|
Speed,
|
|
mach,
|
|
meters,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from game.ato.flight import Flight
|
|
from game.ato.package import Package
|
|
|
|
|
|
class GroundSpeed:
|
|
@classmethod
|
|
def for_flight(cls, flight: Flight, altitude: Distance) -> Speed:
|
|
# 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.
|
|
|
|
# DCS's max speed is in kph at 0 MSL.
|
|
max_speed = flight.unit_type.max_speed
|
|
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
|
|
# Aircraft is supersonic. Limit to mach 0.85 to conserve fuel and
|
|
# account for heavily loaded jets.
|
|
return mach(0.85, altitude)
|
|
|
|
# For subsonic aircraft, assume the aircraft can reasonably perform at
|
|
# 80% of its maximum, and that it can maintain the same mach at altitude
|
|
# as it can at sea level. This probably isn't great assumption, but
|
|
# might. be sufficient given the wiggle room. We can come up with
|
|
# another heuristic if needed.
|
|
cruise_mach = max_speed.mach() * 0.85
|
|
return mach(cruise_mach, altitude)
|
|
|
|
|
|
class TravelTime:
|
|
@staticmethod
|
|
def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
|
|
error_factor = 1.05
|
|
distance = meters(a.distance_to_point(b))
|
|
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
|
|
|
|
|
|
# TODO: Most if not all of this should move into FlightPlan.
|
|
class TotEstimator:
|
|
def __init__(self, package: Package) -> None:
|
|
self.package = package
|
|
|
|
@staticmethod
|
|
def mission_start_time(flight: Flight) -> timedelta:
|
|
startup_time = flight.flight_plan.startup_time()
|
|
if startup_time is None:
|
|
# Could not determine takeoff time, probably due to a custom flight
|
|
# plan. Start immediately.
|
|
return timedelta()
|
|
return startup_time
|
|
|
|
def earliest_tot(self) -> timedelta:
|
|
if not self.package.flights:
|
|
return timedelta(0)
|
|
|
|
earliest_tot = max(
|
|
(self.earliest_tot_for_flight(f) for f in self.package.flights)
|
|
)
|
|
|
|
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
|
|
# and they're not interesting from a mission planning perspective so we
|
|
# don't want them in the UI.
|
|
#
|
|
# Round up so we don't get negative start times.
|
|
return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
|
|
|
|
@staticmethod
|
|
def earliest_tot_for_flight(flight: Flight) -> timedelta:
|
|
"""Estimate fastest time from mission start to the target position.
|
|
|
|
For BARCAP flights, this is time to race track start. This ensures that
|
|
they are on station at the same time any other package members reach
|
|
their ingress point.
|
|
|
|
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.
|
|
"""
|
|
# Clear the TOT, calculate the startup time. Negating the result gives
|
|
# the earliest possible start time.
|
|
orig_tot = flight.package.time_over_target
|
|
try:
|
|
flight.package.time_over_target = timedelta()
|
|
time = flight.flight_plan.startup_time()
|
|
finally:
|
|
flight.package.time_over_target = orig_tot
|
|
|
|
if time is None:
|
|
logging.warning(f"Cannot estimate TOT for {flight}")
|
|
# Return 0 so this flight's travel time does not affect the rest
|
|
# of the package.
|
|
return timedelta()
|
|
return -time
|