From ce4628b64ff959bc7d5b3c4bb6793569ea7702df Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 7 Nov 2021 12:17:05 -0800 Subject: [PATCH] Separate combat as a distinct flight state. Will be used later to simulate combat. https://github.com/dcs-liberation/dcs_liberation/issues/1680 --- game/ato/flight.py | 9 ++- game/ato/flightstate/flightstate.py | 7 ++- game/ato/flightstate/incombat.py | 56 ++++++++++++++++++ game/ato/flightstate/inflight.py | 75 ++++++++----------------- game/ato/flightstate/loiter.py | 3 +- game/ato/flightstate/navigating.py | 73 ++++++++++++++++++++++++ game/ato/flightstate/startup.py | 3 +- game/ato/flightstate/takeoff.py | 7 +-- game/ato/flightstate/taxi.py | 3 +- game/ato/flightstate/waitingforstart.py | 4 +- game/sim/aircraftsimulation.py | 6 +- 11 files changed, 179 insertions(+), 67 deletions(-) create mode 100644 game/ato/flightstate/incombat.py create mode 100644 game/ato/flightstate/navigating.py diff --git a/game/ato/flight.py b/game/ato/flight.py index c62dc3c9..ed20c265 100644 --- a/game/ato/flight.py +++ b/game/ato/flight.py @@ -151,5 +151,10 @@ class Flight: def on_game_tick(self, time: datetime, duration: timedelta) -> None: self.state.on_game_tick(time, duration) - def should_halt_sim(self, enemy_aircraft_coverage: AircraftEngagementZones) -> bool: - return self.state.should_halt_sim(enemy_aircraft_coverage) + def check_for_combat( + self, enemy_aircraft_coverage: AircraftEngagementZones + ) -> None: + self.state.check_for_combat(enemy_aircraft_coverage) + + def should_halt_sim(self) -> bool: + return self.state.should_halt_sim() diff --git a/game/ato/flightstate/flightstate.py b/game/ato/flightstate/flightstate.py index 60e83109..9f1a401b 100644 --- a/game/ato/flightstate/flightstate.py +++ b/game/ato/flightstate/flightstate.py @@ -22,7 +22,12 @@ class FlightState(ABC): def on_game_tick(self, time: datetime, duration: timedelta) -> None: ... - def should_halt_sim(self, enemy_aircraft_coverage: AircraftEngagementZones) -> bool: + def check_for_combat( + self, enemy_aircraft_coverage: AircraftEngagementZones + ) -> None: + pass + + def should_halt_sim(self) -> bool: return False @property diff --git a/game/ato/flightstate/incombat.py b/game/ato/flightstate/incombat.py new file mode 100644 index 00000000..53971019 --- /dev/null +++ b/game/ato/flightstate/incombat.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import TYPE_CHECKING + +from dcs import Point + +from game.utils import Distance, Speed +from .inflight import InFlight +from ..starttype import StartType + +if TYPE_CHECKING: + from game.sim.aircraftengagementzones import AircraftEngagementZones + + +class InCombat(InFlight): + def __init__(self, previous_state: InFlight, description: str) -> None: + super().__init__( + previous_state.flight, + previous_state.settings, + previous_state.waypoint_index, + ) + self.previous_state = previous_state + self._description = description + + def estimate_position(self) -> Point: + return self.previous_state.estimate_position() + + def estimate_altitude(self) -> tuple[Distance, str]: + return self.previous_state.estimate_altitude() + + def estimate_speed(self) -> Speed: + return self.previous_state.estimate_speed() + + def on_game_tick(self, time: datetime, duration: timedelta) -> None: + raise RuntimeError("Cannot simulate combat") + + @property + def is_waiting_for_start(self) -> bool: + return False + + def should_halt_sim(self) -> bool: + return True + + def check_for_combat( + self, enemy_aircraft_coverage: AircraftEngagementZones + ) -> None: + pass + + @property + def spawn_type(self) -> StartType: + return StartType.IN_FLIGHT + + @property + def description(self) -> str: + return self._description diff --git a/game/ato/flightstate/inflight.py b/game/ato/flightstate/inflight.py index 96985157..743f02ac 100644 --- a/game/ato/flightstate/inflight.py +++ b/game/ato/flightstate/inflight.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +from abc import ABC, abstractmethod from datetime import datetime, timedelta from typing import TYPE_CHECKING @@ -11,7 +12,7 @@ from game.ato.flightstate.flightstate import FlightState from game.ato.flightwaypoint import FlightWaypoint from game.ato.flightwaypointtype import FlightWaypointType from game.ato.starttype import StartType -from game.utils import Distance, LBS_TO_KG, Speed, meters, pairwise +from game.utils import Distance, LBS_TO_KG, Speed, pairwise from gen.flights.flightplan import LoiterFlightPlan if TYPE_CHECKING: @@ -20,11 +21,7 @@ if TYPE_CHECKING: from game.sim.aircraftengagementzones import AircraftEngagementZones -def lerp(v0: float, v1: float, t: float) -> float: - return (1 - t) * v0 + t * v1 - - -class InFlight(FlightState): +class InFlight(FlightState, ABC): def __init__(self, flight: Flight, settings: Settings, waypoint_index: int) -> None: super().__init__(flight, settings) waypoints = self.flight.flight_plan.waypoints @@ -52,36 +49,17 @@ class InFlight(FlightState): travel_time -= self.flight.flight_plan.hold_duration return travel_time - def progress(self) -> float: - return ( - self.elapsed_time.total_seconds() - / self.total_time_to_next_waypoint.total_seconds() - ) - + @abstractmethod def estimate_position(self) -> Point: - x0 = self.current_waypoint.position.x - y0 = self.current_waypoint.position.y - x1 = self.next_waypoint.position.x - y1 = self.next_waypoint.position.y - progress = self.progress() - return Point(lerp(x0, x1, progress), lerp(y0, y1, progress)) + ... + @abstractmethod def estimate_altitude(self) -> tuple[Distance, str]: - return ( - meters( - lerp( - self.current_waypoint.alt.meters, - self.next_waypoint.alt.meters, - self.progress(), - ) - ), - self.current_waypoint.alt_type, - ) + ... + @abstractmethod def estimate_speed(self) -> Speed: - return self.flight.flight_plan.speed_between_waypoints( - self.current_waypoint, self.next_waypoint - ) + ... def estimate_fuel_at_current_waypoint(self) -> float: initial_fuel = super().estimate_fuel() @@ -95,21 +73,10 @@ class InFlight(FlightState): initial_fuel -= consumption * LBS_TO_KG return initial_fuel - 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 - def next_waypoint_state(self) -> FlightState: - from game.ato.flightstate.loiter import Loiter - from game.ato.flightstate.racetrack import RaceTrack + from .loiter import Loiter + from .racetrack import RaceTrack + from .navigating import Navigating new_index = self.waypoint_index + 1 if self.next_waypoint.waypoint_type is FlightWaypointType.LANDING_POINT: @@ -118,7 +85,7 @@ class InFlight(FlightState): return RaceTrack(self.flight, self.settings, new_index) if self.next_waypoint.waypoint_type is FlightWaypointType.LOITER: return Loiter(self.flight, self.settings, new_index) - return InFlight(self.flight, self.settings, new_index) + return Navigating(self.flight, self.settings, new_index) def advance_to_next_waypoint(self) -> None: self.flight.set_state(self.next_waypoint_state()) @@ -128,7 +95,11 @@ class InFlight(FlightState): if self.elapsed_time > self.total_time_to_next_waypoint: self.advance_to_next_waypoint() - def should_halt_sim(self, enemy_aircraft_coverage: AircraftEngagementZones) -> bool: + def check_for_combat( + self, enemy_aircraft_coverage: AircraftEngagementZones + ) -> None: + from game.ato.flightstate.incombat import InCombat + contact_types = { FlightWaypointType.INGRESS_BAI, FlightWaypointType.INGRESS_CAS, @@ -144,7 +115,7 @@ class InFlight(FlightState): f"Interrupting simulation because {self.flight} has reached its " "ingress point" ) - return True + self.flight.set_state(InCombat(self, "At IP")) threat_zone = self.flight.squadron.coalition.opponent.threat_zone if threat_zone.threatened_by_air_defense(self.estimate_position()): @@ -152,16 +123,16 @@ class InFlight(FlightState): f"Interrupting simulation because {self.flight} has encountered enemy " "air defenses" ) - return True + self.flight.set_state(InCombat(self, "In combat with enemy air defenses")) if enemy_aircraft_coverage.covers(self.estimate_position()): logging.info( f"Interrupting simulation because {self.flight} has encountered enemy " "air-to-air patrol" ) - return True - - return False + self.flight.set_state( + InCombat(self, "In combat with enemy air-to-air patrol") + ) @property def is_waiting_for_start(self) -> bool: diff --git a/game/ato/flightstate/loiter.py b/game/ato/flightstate/loiter.py index 03301d24..63ecea53 100644 --- a/game/ato/flightstate/loiter.py +++ b/game/ato/flightstate/loiter.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING from dcs import Point from game.ato.flightstate import FlightState, InFlight +from game.ato.flightstate.navigating import Navigating from game.utils import Distance, Speed from gen.flights.flightplan import LoiterFlightPlan @@ -36,7 +37,7 @@ class Loiter(InFlight): def next_waypoint_state(self) -> FlightState: # Do not automatically advance to the next waypoint. Just proceed from the # current one with the normal flying state. - return InFlight(self.flight, self.settings, self.waypoint_index) + return Navigating(self.flight, self.settings, self.waypoint_index) def travel_time_between_waypoints(self) -> timedelta: return self.hold_duration diff --git a/game/ato/flightstate/navigating.py b/game/ato/flightstate/navigating.py new file mode 100644 index 00000000..85cf9970 --- /dev/null +++ b/game/ato/flightstate/navigating.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +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: + pass + + +def lerp(v0: float, v1: float, t: float) -> float: + return (1 - t) * v0 + t * v1 + + +class Navigating(InFlight): + def progress(self) -> float: + return ( + self.elapsed_time.total_seconds() + / self.total_time_to_next_waypoint.total_seconds() + ) + + def estimate_position(self) -> Point: + x0 = self.current_waypoint.position.x + y0 = self.current_waypoint.position.y + x1 = self.next_waypoint.position.x + y1 = self.next_waypoint.position.y + progress = self.progress() + return Point(lerp(x0, x1, progress), lerp(y0, y1, progress)) + + def estimate_altitude(self) -> tuple[Distance, str]: + 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: + return f"Flying to {self.next_waypoint.name}" diff --git a/game/ato/flightstate/startup.py b/game/ato/flightstate/startup.py index 6503b31c..4cc5a39e 100644 --- a/game/ato/flightstate/startup.py +++ b/game/ato/flightstate/startup.py @@ -11,7 +11,6 @@ from ..starttype import StartType if TYPE_CHECKING: from game.ato.flight import Flight from game.settings import Settings - from game.sim.aircraftengagementzones import AircraftEngagementZones class StartUp(FlightState): @@ -32,7 +31,7 @@ class StartUp(FlightState): def spawn_type(self) -> StartType: return StartType.COLD - def should_halt_sim(self, enemy_aircraft_coverage: AircraftEngagementZones) -> bool: + def should_halt_sim(self) -> bool: if ( self.flight.client_count > 0 and self.settings.player_mission_interrupts_sim_at is StartType.COLD diff --git a/game/ato/flightstate/takeoff.py b/game/ato/flightstate/takeoff.py index ee56a529..1ef8d22e 100644 --- a/game/ato/flightstate/takeoff.py +++ b/game/ato/flightstate/takeoff.py @@ -5,14 +5,13 @@ from datetime import datetime, timedelta from typing import TYPE_CHECKING from .flightstate import FlightState -from .inflight import InFlight +from .navigating import Navigating from ..starttype import StartType from ...utils import LBS_TO_KG if TYPE_CHECKING: from game.ato.flight import Flight from game.settings import Settings - from game.sim.aircraftengagementzones import AircraftEngagementZones class Takeoff(FlightState): @@ -24,7 +23,7 @@ class Takeoff(FlightState): def on_game_tick(self, time: datetime, duration: timedelta) -> None: if time < self.completion_time: return - self.flight.set_state(InFlight(self.flight, self.settings, waypoint_index=0)) + self.flight.set_state(Navigating(self.flight, self.settings, waypoint_index=0)) @property def is_waiting_for_start(self) -> bool: @@ -40,7 +39,7 @@ class Takeoff(FlightState): return initial_fuel return initial_fuel - self.flight.unit_type.fuel_consumption.taxi * LBS_TO_KG - def should_halt_sim(self, enemy_aircraft_coverage: AircraftEngagementZones) -> bool: + def should_halt_sim(self) -> bool: if ( self.flight.client_count > 0 and self.settings.player_mission_interrupts_sim_at is StartType.RUNWAY diff --git a/game/ato/flightstate/taxi.py b/game/ato/flightstate/taxi.py index e52cb84a..31c3da90 100644 --- a/game/ato/flightstate/taxi.py +++ b/game/ato/flightstate/taxi.py @@ -11,7 +11,6 @@ from ..starttype import StartType if TYPE_CHECKING: from game.ato.flight import Flight from game.settings import Settings - from game.sim.aircraftengagementzones import AircraftEngagementZones class Taxi(FlightState): @@ -32,7 +31,7 @@ class Taxi(FlightState): def spawn_type(self) -> StartType: return StartType.WARM - def should_halt_sim(self, enemy_aircraft_coverage: AircraftEngagementZones) -> bool: + def should_halt_sim(self) -> bool: if ( self.flight.client_count > 0 and self.settings.player_mission_interrupts_sim_at is StartType.WARM diff --git a/game/ato/flightstate/waitingforstart.py b/game/ato/flightstate/waitingforstart.py index 5f058baa..14e58a64 100644 --- a/game/ato/flightstate/waitingforstart.py +++ b/game/ato/flightstate/waitingforstart.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from game.ato.starttype import StartType from .flightstate import FlightState -from .inflight import InFlight +from .navigating import Navigating from .startup import StartUp from .takeoff import Takeoff from .taxi import Taxi @@ -41,7 +41,7 @@ class WaitingForStart(FlightState): elif self.start_type is StartType.RUNWAY: new_state = Takeoff(self.flight, self.settings, time) else: - new_state = InFlight(self.flight, self.settings, waypoint_index=0) + new_state = Navigating(self.flight, self.settings, waypoint_index=0) self.flight.set_state(new_state) @property diff --git a/game/sim/aircraftsimulation.py b/game/sim/aircraftsimulation.py index 9ce5e376..b78e1632 100644 --- a/game/sim/aircraftsimulation.py +++ b/game/sim/aircraftsimulation.py @@ -40,7 +40,11 @@ class AircraftSimulation: blue_a2a = AircraftEngagementZones.from_ato(self.game.blue.ato) red_a2a = AircraftEngagementZones.from_ato(self.game.red.ato) for flight in self.iter_flights(): - if flight.should_halt_sim(red_a2a if flight.squadron.player else blue_a2a): + flight.check_for_combat(red_a2a if flight.squadron.player else blue_a2a) + + # After updating all combat states, check for halts. + for flight in self.iter_flights(): + if flight.should_halt_sim(): return True return False