Separate combat as a distinct flight state.

Will be used later to simulate combat.

https://github.com/dcs-liberation/dcs_liberation/issues/1680
This commit is contained in:
Dan Albert 2021-11-07 12:17:05 -08:00
parent d9108a7ca6
commit ce4628b64f
11 changed files with 179 additions and 67 deletions

View File

@ -151,5 +151,10 @@ class Flight:
def on_game_tick(self, time: datetime, duration: timedelta) -> None: def on_game_tick(self, time: datetime, duration: timedelta) -> None:
self.state.on_game_tick(time, duration) self.state.on_game_tick(time, duration)
def should_halt_sim(self, enemy_aircraft_coverage: AircraftEngagementZones) -> bool: def check_for_combat(
return self.state.should_halt_sim(enemy_aircraft_coverage) 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()

View File

@ -22,7 +22,12 @@ class FlightState(ABC):
def on_game_tick(self, time: datetime, duration: timedelta) -> None: 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 return False
@property @property

View File

@ -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

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from abc import ABC, abstractmethod
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import TYPE_CHECKING 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.flightwaypoint import FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.starttype import StartType 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 from gen.flights.flightplan import LoiterFlightPlan
if TYPE_CHECKING: if TYPE_CHECKING:
@ -20,11 +21,7 @@ if TYPE_CHECKING:
from game.sim.aircraftengagementzones import AircraftEngagementZones from game.sim.aircraftengagementzones import AircraftEngagementZones
def lerp(v0: float, v1: float, t: float) -> float: class InFlight(FlightState, ABC):
return (1 - t) * v0 + t * v1
class InFlight(FlightState):
def __init__(self, flight: Flight, settings: Settings, waypoint_index: int) -> None: def __init__(self, flight: Flight, settings: Settings, waypoint_index: int) -> None:
super().__init__(flight, settings) super().__init__(flight, settings)
waypoints = self.flight.flight_plan.waypoints waypoints = self.flight.flight_plan.waypoints
@ -52,36 +49,17 @@ class InFlight(FlightState):
travel_time -= self.flight.flight_plan.hold_duration travel_time -= self.flight.flight_plan.hold_duration
return travel_time return travel_time
def progress(self) -> float: @abstractmethod
return (
self.elapsed_time.total_seconds()
/ self.total_time_to_next_waypoint.total_seconds()
)
def estimate_position(self) -> Point: 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]: 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: 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: def estimate_fuel_at_current_waypoint(self) -> float:
initial_fuel = super().estimate_fuel() initial_fuel = super().estimate_fuel()
@ -95,21 +73,10 @@ class InFlight(FlightState):
initial_fuel -= consumption * LBS_TO_KG initial_fuel -= consumption * LBS_TO_KG
return initial_fuel 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: def next_waypoint_state(self) -> FlightState:
from game.ato.flightstate.loiter import Loiter from .loiter import Loiter
from game.ato.flightstate.racetrack import RaceTrack from .racetrack import RaceTrack
from .navigating import Navigating
new_index = self.waypoint_index + 1 new_index = self.waypoint_index + 1
if self.next_waypoint.waypoint_type is FlightWaypointType.LANDING_POINT: 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) return RaceTrack(self.flight, self.settings, new_index)
if self.next_waypoint.waypoint_type is FlightWaypointType.LOITER: if self.next_waypoint.waypoint_type is FlightWaypointType.LOITER:
return Loiter(self.flight, self.settings, new_index) 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: def advance_to_next_waypoint(self) -> None:
self.flight.set_state(self.next_waypoint_state()) 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: if self.elapsed_time > self.total_time_to_next_waypoint:
self.advance_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 = { contact_types = {
FlightWaypointType.INGRESS_BAI, FlightWaypointType.INGRESS_BAI,
FlightWaypointType.INGRESS_CAS, FlightWaypointType.INGRESS_CAS,
@ -144,7 +115,7 @@ class InFlight(FlightState):
f"Interrupting simulation because {self.flight} has reached its " f"Interrupting simulation because {self.flight} has reached its "
"ingress point" "ingress point"
) )
return True self.flight.set_state(InCombat(self, "At IP"))
threat_zone = self.flight.squadron.coalition.opponent.threat_zone threat_zone = self.flight.squadron.coalition.opponent.threat_zone
if threat_zone.threatened_by_air_defense(self.estimate_position()): 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 " f"Interrupting simulation because {self.flight} has encountered enemy "
"air defenses" "air defenses"
) )
return True self.flight.set_state(InCombat(self, "In combat with enemy air defenses"))
if enemy_aircraft_coverage.covers(self.estimate_position()): if enemy_aircraft_coverage.covers(self.estimate_position()):
logging.info( logging.info(
f"Interrupting simulation because {self.flight} has encountered enemy " f"Interrupting simulation because {self.flight} has encountered enemy "
"air-to-air patrol" "air-to-air patrol"
) )
return True self.flight.set_state(
InCombat(self, "In combat with enemy air-to-air patrol")
return False )
@property @property
def is_waiting_for_start(self) -> bool: def is_waiting_for_start(self) -> bool:

View File

@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
from dcs import Point from dcs import Point
from game.ato.flightstate import FlightState, InFlight from game.ato.flightstate import FlightState, InFlight
from game.ato.flightstate.navigating import Navigating
from game.utils import Distance, Speed from game.utils import Distance, Speed
from gen.flights.flightplan import LoiterFlightPlan from gen.flights.flightplan import LoiterFlightPlan
@ -36,7 +37,7 @@ class Loiter(InFlight):
def next_waypoint_state(self) -> FlightState: def next_waypoint_state(self) -> FlightState:
# Do not automatically advance to the next waypoint. Just proceed from the # Do not automatically advance to the next waypoint. Just proceed from the
# current one with the normal flying state. # 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: def travel_time_between_waypoints(self) -> timedelta:
return self.hold_duration return self.hold_duration

View File

@ -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}"

View File

@ -11,7 +11,6 @@ from ..starttype import StartType
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato.flight import Flight from game.ato.flight import Flight
from game.settings import Settings from game.settings import Settings
from game.sim.aircraftengagementzones import AircraftEngagementZones
class StartUp(FlightState): class StartUp(FlightState):
@ -32,7 +31,7 @@ class StartUp(FlightState):
def spawn_type(self) -> StartType: def spawn_type(self) -> StartType:
return StartType.COLD return StartType.COLD
def should_halt_sim(self, enemy_aircraft_coverage: AircraftEngagementZones) -> bool: def should_halt_sim(self) -> bool:
if ( if (
self.flight.client_count > 0 self.flight.client_count > 0
and self.settings.player_mission_interrupts_sim_at is StartType.COLD and self.settings.player_mission_interrupts_sim_at is StartType.COLD

View File

@ -5,14 +5,13 @@ from datetime import datetime, timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .flightstate import FlightState from .flightstate import FlightState
from .inflight import InFlight from .navigating import Navigating
from ..starttype import StartType from ..starttype import StartType
from ...utils import LBS_TO_KG from ...utils import LBS_TO_KG
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato.flight import Flight from game.ato.flight import Flight
from game.settings import Settings from game.settings import Settings
from game.sim.aircraftengagementzones import AircraftEngagementZones
class Takeoff(FlightState): class Takeoff(FlightState):
@ -24,7 +23,7 @@ class Takeoff(FlightState):
def on_game_tick(self, time: datetime, duration: timedelta) -> None: def on_game_tick(self, time: datetime, duration: timedelta) -> None:
if time < self.completion_time: if time < self.completion_time:
return 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 @property
def is_waiting_for_start(self) -> bool: def is_waiting_for_start(self) -> bool:
@ -40,7 +39,7 @@ class Takeoff(FlightState):
return initial_fuel return initial_fuel
return initial_fuel - self.flight.unit_type.fuel_consumption.taxi * LBS_TO_KG 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 ( if (
self.flight.client_count > 0 self.flight.client_count > 0
and self.settings.player_mission_interrupts_sim_at is StartType.RUNWAY and self.settings.player_mission_interrupts_sim_at is StartType.RUNWAY

View File

@ -11,7 +11,6 @@ from ..starttype import StartType
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato.flight import Flight from game.ato.flight import Flight
from game.settings import Settings from game.settings import Settings
from game.sim.aircraftengagementzones import AircraftEngagementZones
class Taxi(FlightState): class Taxi(FlightState):
@ -32,7 +31,7 @@ class Taxi(FlightState):
def spawn_type(self) -> StartType: def spawn_type(self) -> StartType:
return StartType.WARM return StartType.WARM
def should_halt_sim(self, enemy_aircraft_coverage: AircraftEngagementZones) -> bool: def should_halt_sim(self) -> bool:
if ( if (
self.flight.client_count > 0 self.flight.client_count > 0
and self.settings.player_mission_interrupts_sim_at is StartType.WARM and self.settings.player_mission_interrupts_sim_at is StartType.WARM

View File

@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
from game.ato.starttype import StartType from game.ato.starttype import StartType
from .flightstate import FlightState from .flightstate import FlightState
from .inflight import InFlight from .navigating import Navigating
from .startup import StartUp from .startup import StartUp
from .takeoff import Takeoff from .takeoff import Takeoff
from .taxi import Taxi from .taxi import Taxi
@ -41,7 +41,7 @@ class WaitingForStart(FlightState):
elif self.start_type is StartType.RUNWAY: elif self.start_type is StartType.RUNWAY:
new_state = Takeoff(self.flight, self.settings, time) new_state = Takeoff(self.flight, self.settings, time)
else: 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) self.flight.set_state(new_state)
@property @property

View File

@ -40,7 +40,11 @@ class AircraftSimulation:
blue_a2a = AircraftEngagementZones.from_ato(self.game.blue.ato) blue_a2a = AircraftEngagementZones.from_ato(self.game.blue.ato)
red_a2a = AircraftEngagementZones.from_ato(self.game.red.ato) red_a2a = AircraftEngagementZones.from_ato(self.game.red.ato)
for flight in self.iter_flights(): 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 True
return False return False