Dan Albert 87441b8939 Formalize waypoint actions.
Create a WaypointAction class that defines the actions taken at a
waypoint. These will often map one-to-one with DCS waypoint actions but
can also be higher level and generate multiple actions. Once everything
has migrated all waypoint-type-specific behaviors of
PydcsWaypointBuilder will be gone, and it'll be easier to keep the sim
behaviors in sync with the mission generator behaviors.

For now only hold has been migrated. This is actually probably the most
complicated action we have (starting with this may have been a mistake,
but it did find all the rough edges quickly) since it affects waypoint
timings and flight position during simulation. That part isn't handled
as neatly as I'd like because the FlightState still has to special case
LOITER points to avoid simulating the wrong waypoint position. At some
point we should probably start tracking real positions in FlightState,
and when we do that will be solved.
2023-08-13 12:43:59 -07:00

94 lines
3.1 KiB
Python

from __future__ import annotations
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from dcs import Point
from game.ato.flightstate import InFlight
from game.ato.starttype import StartType
from game.utils import Distance, LBS_TO_KG, Speed, meters
if TYPE_CHECKING:
from game.sim.gameupdateevents import GameUpdateEvents
def lerp(v0: float, v1: float, t: float) -> float:
return (1 - t) * v0 + t * v1
class Navigating(InFlight):
def on_game_tick(
self, events: GameUpdateEvents, time: datetime, duration: timedelta
) -> None:
super().on_game_tick(events, time, duration)
# If the parent tick caused this waypoint to become inactive don't update the
# position based on our now invalid state.
if not self.current_waypoint_elapsed:
events.update_flight_position(self.flight, self.estimate_position())
def progress(self) -> float:
return (
self.elapsed_time.total_seconds()
/ self.total_time_to_next_waypoint.total_seconds()
)
def estimate_position(self) -> Point:
return self.current_waypoint.position.lerp(
self.next_waypoint.position, self.progress()
)
def estimate_altitude(self) -> tuple[Distance, str]:
# This does not behave well when one of the waypoints is AGL and the other is
# MSL. We can't really avoid that problem though. We don't know where the ground
# is, so conversions between them are impossible, and we do need to use AGL
# altitudes for takeoff and landing waypoints (even if we had the runway
# elevation, we don't have elevation for FARPs).
return (
meters(
lerp(
self.current_waypoint.alt.meters,
self.next_waypoint.alt.meters,
self.progress(),
)
),
self.current_waypoint.alt_type,
)
def estimate_speed(self) -> Speed:
return self.flight.flight_plan.speed_between_waypoints(
self.current_waypoint, self.next_waypoint
)
def estimate_fuel(self) -> float:
initial_fuel = self.estimate_fuel_at_current_waypoint()
ppm = self.flight.flight_plan.fuel_rate_to_between_points(
self.current_waypoint, self.next_waypoint
)
if ppm is None:
return initial_fuel
position = self.estimate_position()
distance = meters(self.current_waypoint.position.distance_to_point(position))
consumption = distance.nautical_miles * ppm * LBS_TO_KG
return initial_fuel - consumption
@property
def is_waiting_for_start(self) -> bool:
return False
@property
def spawn_type(self) -> StartType:
return StartType.IN_FLIGHT
@property
def description(self) -> str:
if (action := self.current_action) is not None:
return action.describe()
if self.has_aborted:
abort = "(Aborted) "
else:
abort = ""
return f"{abort}Flying to {self.next_waypoint.name}"