Support display of dead flights.

This commit is contained in:
Dan Albert 2022-03-07 19:34:51 -08:00
parent 053a1287c9
commit 005090fbcd
9 changed files with 58 additions and 31 deletions

View File

@ -10,6 +10,7 @@ from dcs.planes import C_101CC, C_101EB, Su_33
from game.savecompat import has_save_compat_for from game.savecompat import has_save_compat_for
from .flightroster import FlightRoster from .flightroster import FlightRoster
from .flightstate import FlightState, Uninitialized from .flightstate import FlightState, Uninitialized
from .flightstate.killed import Killed
from .loadouts import Loadout from .loadouts import Loadout
from ..sidc import ( from ..sidc import (
AirEntity, AirEntity,
@ -117,7 +118,7 @@ class Flight(SidcDescribable):
@property @property
def sidc_status(self) -> Status: def sidc_status(self) -> Status:
return Status.PRESENT return Status.PRESENT if self.alive else Status.PRESENT_DESTROYED
@property @property
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
@ -202,7 +203,11 @@ class Flight(SidcDescribable):
def should_halt_sim(self) -> bool: def should_halt_sim(self) -> bool:
return self.state.should_halt_sim() return self.state.should_halt_sim()
def kill(self, results: SimulationResults) -> None: @property
def alive(self) -> bool:
return self.state.alive
def kill(self, results: SimulationResults, events: GameUpdateEvents) -> None:
# This is a bit messy while we're in transition from turn-based to turnless # This is a bit messy while we're in transition from turn-based to turnless
# because we want the simulation to have minimal impact on the save game while # because we want the simulation to have minimal impact on the save game while
# turns exist so that loading a game is essentially a way to reset the # turns exist so that loading a game is essentially a way to reset the
@ -211,17 +216,17 @@ class Flight(SidcDescribable):
# the UI to reflect that aircraft were lost and avoid generating those flights # the UI to reflect that aircraft were lost and avoid generating those flights
# when the mission is generated. # when the mission is generated.
# #
# For now we do this by removing the flight from the ATO and logging the kill in # For now we do this by logging the kill in the SimulationResults, which is
# the SimulationResults, which is similar to the Debriefing. If a flight is # similar to the Debriefing. We also set the flight's state to Killed, which
# killed and the player saves and reloads, those pilots/aircraft will be # will prevent it from being spawned in the mission and updates the SIDC.
# unusable until the next turn, but otherwise will survive. # This does leave an opportunity for players to cheat since the UI won't stop
# # them from cancelling a dead flight, returning the aircraft to the pool. Not a
# This is going to be extremely temporary since the solution for other killable # big deal for now.
# game objects (killed SAMs, sinking carriers, bombed out runways) will not be
# so easily worked around.
# TODO: Support partial kills. # TODO: Support partial kills.
# TODO: Remove empty packages from the ATO? self.set_state(
self.package.remove_flight(self) Killed(self.state.estimate_position(), self, self.squadron.settings)
)
events.update_flight(self)
for pilot in self.roster.pilots: for pilot in self.roster.pilots:
if pilot is not None: if pilot is not None:
results.kill_pilot(self, pilot) results.kill_pilot(self, pilot)

View File

@ -20,6 +20,10 @@ class FlightState(ABC):
self.flight = flight self.flight = flight
self.settings = settings self.settings = settings
@property
def alive(self) -> bool:
return True
@abstractmethod @abstractmethod
def on_game_tick( def on_game_tick(
self, events: GameUpdateEvents, time: datetime, duration: timedelta self, events: GameUpdateEvents, time: datetime, duration: timedelta

View File

@ -5,14 +5,26 @@ from typing import TYPE_CHECKING
from dcs import Point from dcs import Point
from game.settings import Settings
from .flightstate import FlightState from .flightstate import FlightState
from ..starttype import StartType from ..starttype import StartType
if TYPE_CHECKING: if TYPE_CHECKING:
from .. import Flight
from game.sim.gameupdateevents import GameUpdateEvents from game.sim.gameupdateevents import GameUpdateEvents
class Completed(FlightState): class Killed(FlightState):
def __init__(
self, last_position: Point, flight: Flight, settings: Settings
) -> None:
super().__init__(flight, settings)
self.last_position = last_position
@property
def alive(self) -> bool:
return False
def on_game_tick( def on_game_tick(
self, events: GameUpdateEvents, time: datetime, duration: timedelta self, events: GameUpdateEvents, time: datetime, duration: timedelta
) -> None: ) -> None:
@ -23,12 +35,11 @@ class Completed(FlightState):
return False return False
def estimate_position(self) -> Point: def estimate_position(self) -> Point:
return self.flight.arrival.position return self.last_position
@property @property
def spawn_type(self) -> StartType: def spawn_type(self) -> StartType:
# TODO: May want to do something different to make these uncontrolled? raise RuntimeError("Attempted to spawn a dead flight")
return StartType.COLD
@property @property
def description(self) -> str: def description(self) -> str:

View File

@ -105,11 +105,12 @@ class AircraftGenerator:
if not package.flights: if not package.flights:
continue continue
for flight in package.flights: for flight in package.flights:
logging.info(f"Generating flight: {flight.unit_type}") if flight.alive:
group = self.create_and_configure_flight( logging.info(f"Generating flight: {flight.unit_type}")
flight, country, dynamic_runways group = self.create_and_configure_flight(
) flight, country, dynamic_runways
self.unit_map.add_aircraft(group, flight) )
self.unit_map.add_aircraft(group, flight)
def spawn_unused_aircraft( def spawn_unused_aircraft(
self, player_country: Country, enemy_country: Country self, player_country: Country, enemy_country: Country

View File

@ -49,7 +49,7 @@ class AircraftSimulation:
still_active = [] still_active = []
for combat in self.combats: for combat in self.combats:
if combat.on_game_tick(duration, self.results): if combat.on_game_tick(duration, self.results, events):
events.end_combat(combat) events.end_combat(combat)
else: else:
still_active.append(combat) still_active.append(combat)

View File

@ -10,6 +10,7 @@ from shapely.ops import unary_union
from game.ato.flightstate import InCombat, InFlight from game.ato.flightstate import InCombat, InFlight
from game.utils import dcs_to_shapely_point from game.utils import dcs_to_shapely_point
from .joinablecombat import JoinableCombat from .joinablecombat import JoinableCombat
from .. import GameUpdateEvents
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato import Flight from game.ato import Flight
@ -60,7 +61,7 @@ class AirCombat(JoinableCombat):
def describe(self) -> str: def describe(self) -> str:
return f"in air-to-air combat" return f"in air-to-air combat"
def resolve(self, results: SimulationResults) -> None: def resolve(self, results: SimulationResults, events: GameUpdateEvents) -> None:
blue = [] blue = []
red = [] red = []
for flight in self.flights: for flight in self.flights:
@ -87,11 +88,11 @@ class AirCombat(JoinableCombat):
logging.debug(f"{self} auto-resolved as red victory") logging.debug(f"{self} auto-resolved as red victory")
for flight in loser: for flight in loser:
flight.kill(results) flight.kill(results, events)
for flight in winner: for flight in winner:
assert isinstance(flight.state, InCombat) assert isinstance(flight.state, InCombat)
if random.random() / flight.count >= 0.5: if random.random() / flight.count >= 0.5:
flight.kill(results) flight.kill(results, events)
else: else:
flight.state.exit_combat() flight.state.exit_combat()

View File

@ -6,6 +6,7 @@ from datetime import timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .frozencombat import FrozenCombat from .frozencombat import FrozenCombat
from .. import GameUpdateEvents
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato import Flight from game.ato import Flight
@ -26,7 +27,7 @@ class AtIp(FrozenCombat):
def iter_flights(self) -> Iterator[Flight]: def iter_flights(self) -> Iterator[Flight]:
yield self.flight yield self.flight
def resolve(self, results: SimulationResults) -> None: def resolve(self, results: SimulationResults, events: GameUpdateEvents) -> None:
logging.debug( logging.debug(
f"{self.flight} attack on {self.flight.package.target} auto-resolved with " f"{self.flight} attack on {self.flight.package.target} auto-resolved with "
"mission failure but no losses" "mission failure but no losses"

View File

@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
from game.ato.flightstate import InCombat from game.ato.flightstate import InCombat
from .frozencombat import FrozenCombat from .frozencombat import FrozenCombat
from .. import GameUpdateEvents
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato import Flight from game.ato import Flight
@ -36,11 +37,11 @@ class DefendingSam(FrozenCombat):
def iter_flights(self) -> Iterator[Flight]: def iter_flights(self) -> Iterator[Flight]:
yield self.flight yield self.flight
def resolve(self, results: SimulationResults) -> None: def resolve(self, results: SimulationResults, events: GameUpdateEvents) -> None:
assert isinstance(self.flight.state, InCombat) assert isinstance(self.flight.state, InCombat)
if random.random() / self.flight.count >= 0.5: if random.random() / self.flight.count >= 0.5:
logging.debug(f"Air defense combat auto-resolved with {self.flight} lost") logging.debug(f"Air defense combat auto-resolved with {self.flight} lost")
self.flight.kill(results) self.flight.kill(results, events)
else: else:
logging.debug( logging.debug(
f"Air defense combat auto-resolved with {self.flight} surviving" f"Air defense combat auto-resolved with {self.flight} surviving"

View File

@ -7,6 +7,7 @@ from datetime import timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from game.ato.flightstate import InCombat, InFlight from game.ato.flightstate import InCombat, InFlight
from .. import GameUpdateEvents
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato import Flight from game.ato import Flight
@ -19,15 +20,17 @@ class FrozenCombat(ABC):
self.freeze_duration = freeze_duration self.freeze_duration = freeze_duration
self.elapsed_time = timedelta() self.elapsed_time = timedelta()
def on_game_tick(self, duration: timedelta, results: SimulationResults) -> bool: def on_game_tick(
self, duration: timedelta, results: SimulationResults, events: GameUpdateEvents
) -> bool:
self.elapsed_time += duration self.elapsed_time += duration
if self.elapsed_time >= self.freeze_duration: if self.elapsed_time >= self.freeze_duration:
self.resolve(results) self.resolve(results, events)
return True return True
return False return False
@abstractmethod @abstractmethod
def resolve(self, results: SimulationResults) -> None: def resolve(self, results: SimulationResults, events: GameUpdateEvents) -> None:
... ...
@abstractmethod @abstractmethod