diff --git a/game/ato/flight.py b/game/ato/flight.py index f35da57d..67fec518 100644 --- a/game/ato/flight.py +++ b/game/ato/flight.py @@ -10,6 +10,7 @@ from dcs.planes import C_101CC, C_101EB, Su_33 from game.savecompat import has_save_compat_for from .flightroster import FlightRoster from .flightstate import FlightState, Uninitialized +from .flightstate.killed import Killed from .loadouts import Loadout from ..sidc import ( AirEntity, @@ -117,7 +118,7 @@ class Flight(SidcDescribable): @property def sidc_status(self) -> Status: - return Status.PRESENT + return Status.PRESENT if self.alive else Status.PRESENT_DESTROYED @property def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: @@ -202,7 +203,11 @@ class Flight(SidcDescribable): def should_halt_sim(self) -> bool: 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 # 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 @@ -211,17 +216,17 @@ class Flight(SidcDescribable): # the UI to reflect that aircraft were lost and avoid generating those flights # when the mission is generated. # - # For now we do this by removing the flight from the ATO and logging the kill in - # the SimulationResults, which is similar to the Debriefing. If a flight is - # killed and the player saves and reloads, those pilots/aircraft will be - # unusable until the next turn, but otherwise will survive. - # - # This is going to be extremely temporary since the solution for other killable - # game objects (killed SAMs, sinking carriers, bombed out runways) will not be - # so easily worked around. + # For now we do this by logging the kill in the SimulationResults, which is + # similar to the Debriefing. We also set the flight's state to Killed, which + # will prevent it from being spawned in the mission and updates the SIDC. + # 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 + # big deal for now. # TODO: Support partial kills. - # TODO: Remove empty packages from the ATO? - self.package.remove_flight(self) + self.set_state( + Killed(self.state.estimate_position(), self, self.squadron.settings) + ) + events.update_flight(self) for pilot in self.roster.pilots: if pilot is not None: results.kill_pilot(self, pilot) diff --git a/game/ato/flightstate/flightstate.py b/game/ato/flightstate/flightstate.py index 5fe45c76..47013d5d 100644 --- a/game/ato/flightstate/flightstate.py +++ b/game/ato/flightstate/flightstate.py @@ -20,6 +20,10 @@ class FlightState(ABC): self.flight = flight self.settings = settings + @property + def alive(self) -> bool: + return True + @abstractmethod def on_game_tick( self, events: GameUpdateEvents, time: datetime, duration: timedelta diff --git a/game/ato/flightstate/killed.py b/game/ato/flightstate/killed.py index fffb5f67..f9061793 100644 --- a/game/ato/flightstate/killed.py +++ b/game/ato/flightstate/killed.py @@ -5,14 +5,26 @@ from typing import TYPE_CHECKING from dcs import Point +from game.settings import Settings from .flightstate import FlightState from ..starttype import StartType if TYPE_CHECKING: + from .. import Flight 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( self, events: GameUpdateEvents, time: datetime, duration: timedelta ) -> None: @@ -23,12 +35,11 @@ class Completed(FlightState): return False def estimate_position(self) -> Point: - return self.flight.arrival.position + return self.last_position @property def spawn_type(self) -> StartType: - # TODO: May want to do something different to make these uncontrolled? - return StartType.COLD + raise RuntimeError("Attempted to spawn a dead flight") @property def description(self) -> str: diff --git a/game/missiongenerator/aircraft/aircraftgenerator.py b/game/missiongenerator/aircraft/aircraftgenerator.py index 9daa68cd..6868f981 100644 --- a/game/missiongenerator/aircraft/aircraftgenerator.py +++ b/game/missiongenerator/aircraft/aircraftgenerator.py @@ -105,11 +105,12 @@ class AircraftGenerator: if not package.flights: continue for flight in package.flights: - logging.info(f"Generating flight: {flight.unit_type}") - group = self.create_and_configure_flight( - flight, country, dynamic_runways - ) - self.unit_map.add_aircraft(group, flight) + if flight.alive: + logging.info(f"Generating flight: {flight.unit_type}") + group = self.create_and_configure_flight( + flight, country, dynamic_runways + ) + self.unit_map.add_aircraft(group, flight) def spawn_unused_aircraft( self, player_country: Country, enemy_country: Country diff --git a/game/sim/aircraftsimulation.py b/game/sim/aircraftsimulation.py index aa2416c6..ee1ced2d 100644 --- a/game/sim/aircraftsimulation.py +++ b/game/sim/aircraftsimulation.py @@ -49,7 +49,7 @@ class AircraftSimulation: still_active = [] 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) else: still_active.append(combat) diff --git a/game/sim/combat/aircombat.py b/game/sim/combat/aircombat.py index 1b3800a1..e1a93228 100644 --- a/game/sim/combat/aircombat.py +++ b/game/sim/combat/aircombat.py @@ -10,6 +10,7 @@ from shapely.ops import unary_union from game.ato.flightstate import InCombat, InFlight from game.utils import dcs_to_shapely_point from .joinablecombat import JoinableCombat +from .. import GameUpdateEvents if TYPE_CHECKING: from game.ato import Flight @@ -60,7 +61,7 @@ class AirCombat(JoinableCombat): def describe(self) -> str: return f"in air-to-air combat" - def resolve(self, results: SimulationResults) -> None: + def resolve(self, results: SimulationResults, events: GameUpdateEvents) -> None: blue = [] red = [] for flight in self.flights: @@ -87,11 +88,11 @@ class AirCombat(JoinableCombat): logging.debug(f"{self} auto-resolved as red victory") for flight in loser: - flight.kill(results) + flight.kill(results, events) for flight in winner: assert isinstance(flight.state, InCombat) if random.random() / flight.count >= 0.5: - flight.kill(results) + flight.kill(results, events) else: flight.state.exit_combat() diff --git a/game/sim/combat/atip.py b/game/sim/combat/atip.py index fd34480e..17b6bd67 100644 --- a/game/sim/combat/atip.py +++ b/game/sim/combat/atip.py @@ -6,6 +6,7 @@ from datetime import timedelta from typing import TYPE_CHECKING from .frozencombat import FrozenCombat +from .. import GameUpdateEvents if TYPE_CHECKING: from game.ato import Flight @@ -26,7 +27,7 @@ class AtIp(FrozenCombat): def iter_flights(self) -> Iterator[Flight]: yield self.flight - def resolve(self, results: SimulationResults) -> None: + def resolve(self, results: SimulationResults, events: GameUpdateEvents) -> None: logging.debug( f"{self.flight} attack on {self.flight.package.target} auto-resolved with " "mission failure but no losses" diff --git a/game/sim/combat/defendingsam.py b/game/sim/combat/defendingsam.py index 4cc2ad1a..0ae0619a 100644 --- a/game/sim/combat/defendingsam.py +++ b/game/sim/combat/defendingsam.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING from game.ato.flightstate import InCombat from .frozencombat import FrozenCombat +from .. import GameUpdateEvents if TYPE_CHECKING: from game.ato import Flight @@ -36,11 +37,11 @@ class DefendingSam(FrozenCombat): def iter_flights(self) -> Iterator[Flight]: yield self.flight - def resolve(self, results: SimulationResults) -> None: + def resolve(self, results: SimulationResults, events: GameUpdateEvents) -> None: assert isinstance(self.flight.state, InCombat) if random.random() / self.flight.count >= 0.5: logging.debug(f"Air defense combat auto-resolved with {self.flight} lost") - self.flight.kill(results) + self.flight.kill(results, events) else: logging.debug( f"Air defense combat auto-resolved with {self.flight} surviving" diff --git a/game/sim/combat/frozencombat.py b/game/sim/combat/frozencombat.py index 09601191..51502dcb 100644 --- a/game/sim/combat/frozencombat.py +++ b/game/sim/combat/frozencombat.py @@ -7,6 +7,7 @@ from datetime import timedelta from typing import TYPE_CHECKING from game.ato.flightstate import InCombat, InFlight +from .. import GameUpdateEvents if TYPE_CHECKING: from game.ato import Flight @@ -19,15 +20,17 @@ class FrozenCombat(ABC): self.freeze_duration = freeze_duration 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 if self.elapsed_time >= self.freeze_duration: - self.resolve(results) + self.resolve(results, events) return True return False @abstractmethod - def resolve(self, results: SimulationResults) -> None: + def resolve(self, results: SimulationResults, events: GameUpdateEvents) -> None: ... @abstractmethod