mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Allow player to continue playing after the end of a turn. (#3526)
This PR: Keeps track of time spent in mission Introduces a new "turnless mode" setting, which activates the following: - At the end of a mission, fast forwards through the time spent in the mission, skipping any combat (which has already been tracked through state.json) - Removes killed flights from the ATO - Does not start a new turn, instead allows the player to continue the current turn.
This commit is contained in:
parent
5676c40788
commit
d09a15a7f3
@ -69,6 +69,15 @@ class FlightMembers(IFlightRoster):
|
||||
self.flight.squadron.return_pilot(current_pilot)
|
||||
self.members[index].pilot = pilot
|
||||
|
||||
def remove_pilot(self, pilot: Pilot) -> None:
|
||||
for i, member in enumerate(self.members):
|
||||
if member.pilot is not None and member.pilot.name == pilot.name:
|
||||
self.members.pop(i)
|
||||
if (code := member.tgp_laser_code) is not None:
|
||||
code.release()
|
||||
return
|
||||
raise ValueError(f"Pilot {pilot.name} not a member")
|
||||
|
||||
def clear(self) -> None:
|
||||
self.flight.squadron.return_pilots(
|
||||
[p for p in self.iter_pilots() if p is not None]
|
||||
|
||||
@ -196,6 +196,9 @@ class StateData:
|
||||
#: True if the mission ended. If False, the mission exited abnormally.
|
||||
mission_ended: bool
|
||||
|
||||
#: Simulation time since mission start in seconds
|
||||
simulation_time_seconds: float
|
||||
|
||||
#: Names of aircraft units that were killed during the mission.
|
||||
killed_aircraft: List[str]
|
||||
|
||||
@ -248,6 +251,7 @@ class StateData:
|
||||
|
||||
return cls(
|
||||
mission_ended=data["mission_ended"],
|
||||
simulation_time_seconds=data["simulation_time_seconds"],
|
||||
killed_aircraft=killed_aircraft,
|
||||
killed_ground_units=killed_ground_units,
|
||||
destroyed_statics=data["destroyed_objects_positions"],
|
||||
|
||||
@ -355,6 +355,16 @@ class Settings:
|
||||
"your game after aborting take off."
|
||||
),
|
||||
)
|
||||
turnless_mode: bool = boolean_option(
|
||||
"Enable turnless mode (WIP)",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=GAMEPLAY_SECTION,
|
||||
default=False,
|
||||
detail=(
|
||||
"If enabled, turns do not end after mission completion. A new mission "
|
||||
"can be started picking up from where the previous mission ended."
|
||||
),
|
||||
)
|
||||
fast_forward_stop_condition: FastForwardStopCondition = choices_option(
|
||||
"Fast forward until",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
|
||||
@ -28,9 +28,17 @@ class AircraftSimulation:
|
||||
self.set_initial_flight_states()
|
||||
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
self,
|
||||
events: GameUpdateEvents,
|
||||
time: datetime,
|
||||
duration: timedelta,
|
||||
combat_resolution_method: CombatResolutionMethod,
|
||||
force_continue: bool,
|
||||
) -> None:
|
||||
if not self._auto_resolve_combat() and self.combats:
|
||||
if (
|
||||
not self._auto_resolve_combat(combat_resolution_method, force_continue)
|
||||
and self.combats
|
||||
):
|
||||
logging.error(
|
||||
"Cannot resume simulation because aircraft are in combat and "
|
||||
"auto-resolve is disabled"
|
||||
@ -45,7 +53,7 @@ class AircraftSimulation:
|
||||
duration,
|
||||
self.results,
|
||||
events,
|
||||
self.game.settings.combat_resolution_method,
|
||||
combat_resolution_method,
|
||||
):
|
||||
events.end_combat(combat)
|
||||
else:
|
||||
@ -61,11 +69,22 @@ class AircraftSimulation:
|
||||
|
||||
# After updating all combat states, check for halts.
|
||||
for flight in self.iter_flights():
|
||||
if flight.should_halt_sim():
|
||||
if flight.should_halt_sim() and not force_continue:
|
||||
events.complete_simulation()
|
||||
return
|
||||
|
||||
if not self._auto_resolve_combat() and self.combats:
|
||||
# Find completed flights, removing them from the ATO and returning aircraft
|
||||
# and pilots back to the squadron.
|
||||
for flight in self.iter_flights():
|
||||
if type(flight.state) == Completed:
|
||||
flight.package.remove_flight(flight)
|
||||
if len(flight.package.flights) == 0:
|
||||
flight.squadron.coalition.ato.remove_package(flight.package)
|
||||
|
||||
if (
|
||||
not self._auto_resolve_combat(combat_resolution_method, force_continue)
|
||||
and self.combats
|
||||
):
|
||||
events.complete_simulation()
|
||||
|
||||
def set_initial_flight_states(self) -> None:
|
||||
@ -90,10 +109,9 @@ class AircraftSimulation:
|
||||
for package in packages:
|
||||
yield from package.flights
|
||||
|
||||
def _auto_resolve_combat(self) -> bool:
|
||||
return (
|
||||
self.game.settings.fast_forward_stop_condition
|
||||
!= FastForwardStopCondition.DISABLED
|
||||
and self.game.settings.combat_resolution_method
|
||||
!= CombatResolutionMethod.PAUSE
|
||||
)
|
||||
def _auto_resolve_combat(
|
||||
self, combat_resolution_method: CombatResolutionMethod, force_continue: bool
|
||||
) -> bool:
|
||||
if force_continue:
|
||||
return True
|
||||
return combat_resolution_method != CombatResolutionMethod.PAUSE
|
||||
|
||||
@ -100,7 +100,11 @@ class GameLoop:
|
||||
if not self.started:
|
||||
raise RuntimeError("Attempted to tick game loop before initialization")
|
||||
try:
|
||||
self.sim.tick(self.events)
|
||||
self.sim.tick(
|
||||
self.events,
|
||||
self.game.settings.combat_resolution_method,
|
||||
force_continue=False,
|
||||
)
|
||||
self.completed = self.events.simulation_complete
|
||||
if not suppress_events:
|
||||
self.send_update(rate_limit=True)
|
||||
|
||||
@ -8,6 +8,7 @@ from game.ground_forces.combat_stance import CombatStance
|
||||
from game.theater import ControlPoint
|
||||
from .gameupdateevents import GameUpdateEvents
|
||||
from ..ato.airtaaskingorder import AirTaskingOrder
|
||||
from ..ato.flightstate.atdeparture import AtDeparture
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..game import Game
|
||||
@ -57,6 +58,25 @@ class MissionResultsProcessor:
|
||||
logging.info(f"{aircraft} destroyed from {squadron}")
|
||||
squadron.owned_aircraft -= 1
|
||||
|
||||
# Remove air losses from the flight. Remove the flight if all aircraft are lost.
|
||||
# Remove the package if the flight is the last flight in the package.
|
||||
# This logic is redundant if we are going to a new turn, since the whole ATO is
|
||||
# regenerated. However if we want to keep the ATO to continue a turn, this update
|
||||
# is necessary to make sure lost aircraft are removed from the ATO.
|
||||
if loss.pilot is not None:
|
||||
loss.flight.roster.remove_pilot(loss.pilot)
|
||||
if loss.flight.count == 0: # Last aircraft in the flight, remove the flight
|
||||
# If no flights in package, generally indicates that the loss is an aircraft
|
||||
# that is not assigned to a mission and is parked on the ground. There is no need
|
||||
# to remove the aircraft from the ATO as it was never in the ATO in the first place.
|
||||
if len(loss.flight.package.flights) == 0:
|
||||
continue
|
||||
loss.flight.package.remove_flight(loss.flight)
|
||||
if len(loss.flight.package.flights) == 0:
|
||||
loss.flight.squadron.coalition.ato.remove_package(
|
||||
loss.flight.package
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
|
||||
for package in ato.packages:
|
||||
@ -148,10 +168,21 @@ class MissionResultsProcessor:
|
||||
iads_network.update_network(events)
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def commit_damaged_runways(debriefing: Debriefing) -> None:
|
||||
def commit_damaged_runways(self, debriefing: Debriefing) -> None:
|
||||
for damaged_runway in debriefing.damaged_runways:
|
||||
damaged_runway.damage_runway()
|
||||
# Remove any flight in ATO scheduled to take off from the damaged runway.
|
||||
for coalition in self.game.coalitions:
|
||||
for package in coalition.ato.packages:
|
||||
for flight in package.flights:
|
||||
if flight.departure.name == damaged_runway.name and isinstance(
|
||||
flight.state, AtDeparture
|
||||
):
|
||||
flight.package.remove_flight(flight)
|
||||
if len(flight.package.flights) == 0:
|
||||
flight.squadron.coalition.ato.remove_package(
|
||||
flight.package
|
||||
)
|
||||
|
||||
def commit_captures(self, debriefing: Debriefing, events: GameUpdateEvents) -> None:
|
||||
for captured in debriefing.base_captures:
|
||||
|
||||
@ -7,6 +7,7 @@ from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.debriefing import Debriefing
|
||||
from game.missiongenerator import MissionGenerator
|
||||
from game.settings.settings import FastForwardStopCondition, CombatResolutionMethod
|
||||
from game.unitmap import UnitMap
|
||||
from .aircraftsimulation import AircraftSimulation
|
||||
from .missionresultsprocessor import MissionResultsProcessor
|
||||
@ -37,12 +38,25 @@ class MissionSimulation:
|
||||
self.time = self.game.simulation_time
|
||||
self.aircraft_simulation.begin_simulation()
|
||||
|
||||
def tick(self, events: GameUpdateEvents) -> GameUpdateEvents:
|
||||
def tick(
|
||||
self,
|
||||
events: GameUpdateEvents,
|
||||
combat_resolution_method: CombatResolutionMethod,
|
||||
force_continue: bool,
|
||||
) -> GameUpdateEvents:
|
||||
self.time += TICK
|
||||
self.game.simulation_time = self.time
|
||||
if self.completed:
|
||||
raise RuntimeError("Simulation already completed")
|
||||
self.aircraft_simulation.on_game_tick(events, self.time, TICK)
|
||||
if (
|
||||
self.game.settings.fast_forward_stop_condition
|
||||
== FastForwardStopCondition.DISABLED
|
||||
):
|
||||
events.complete_simulation()
|
||||
return events
|
||||
self.aircraft_simulation.on_game_tick(
|
||||
events, self.time, TICK, combat_resolution_method, force_continue
|
||||
)
|
||||
self.completed = events.simulation_complete
|
||||
return events
|
||||
|
||||
@ -76,6 +90,18 @@ class MissionSimulation:
|
||||
|
||||
self.game.save_last_turn_state()
|
||||
MissionResultsProcessor(self.game).commit(debriefing, events)
|
||||
if self.game.settings.turnless_mode:
|
||||
# Set completed to False to clear completion of any previous simulation tick.
|
||||
self.completed = False
|
||||
# If running in turnless mode, run sim to calculate planned positions of flights
|
||||
# for the duration of time the DCS mission ran.
|
||||
start_time = copy.deepcopy(self.time)
|
||||
while self.time < start_time + timedelta(
|
||||
seconds=int(debriefing.state_data.simulation_time_seconds)
|
||||
):
|
||||
# Always skip combat as we are processing results from DCS. Any combat has already
|
||||
# been resolved in-game
|
||||
self.tick(events, CombatResolutionMethod.SKIP, force_continue=True)
|
||||
|
||||
def finish(self) -> None:
|
||||
self.unit_map = None
|
||||
|
||||
@ -50,10 +50,7 @@ class QTopPanel(QFrame):
|
||||
self.conditionsWidget = QConditionsWidget(sim_controller)
|
||||
self.budgetBox = QBudgetBox(self.game)
|
||||
|
||||
pass_turn_text = "Pass Turn"
|
||||
if not self.game or self.game.turn == 0:
|
||||
pass_turn_text = "Begin Campaign"
|
||||
self.passTurnButton = QPushButton(pass_turn_text)
|
||||
self.passTurnButton = QPushButton(self._pass_turn_button_text(self.game))
|
||||
self.passTurnButton.setIcon(CONST.ICONS["PassTurn"])
|
||||
self.passTurnButton.setProperty("style", "btn-primary")
|
||||
self.passTurnButton.clicked.connect(self.passTurn)
|
||||
@ -120,11 +117,9 @@ class QTopPanel(QFrame):
|
||||
self.factionsInfos.setGame(game)
|
||||
|
||||
self.passTurnButton.setEnabled(True)
|
||||
if game and game.turn > 0:
|
||||
self.passTurnButton.setText("Pass Turn")
|
||||
self.passTurnButton.setText(self._pass_turn_button_text(game))
|
||||
|
||||
if game and game.turn == 0:
|
||||
self.passTurnButton.setText("Begin Campaign")
|
||||
self.proceedButton.setEnabled(False)
|
||||
else:
|
||||
self.proceedButton.setEnabled(True)
|
||||
@ -283,3 +278,11 @@ class QTopPanel(QFrame):
|
||||
|
||||
def budget_update(self, game: Game):
|
||||
self.budgetBox.setGame(game)
|
||||
|
||||
def _pass_turn_button_text(self, game: Game) -> None:
|
||||
if game and game.turn > 0:
|
||||
if game.settings.turnless_mode:
|
||||
return "End Turn"
|
||||
else:
|
||||
return "Pass Turn"
|
||||
return "Begin campaign"
|
||||
|
||||
@ -571,7 +571,8 @@ class QLiberationWindow(QMainWindow):
|
||||
if state is not TurnState.CONTINUE:
|
||||
GameOverDialog(won=state is TurnState.WIN, parent=self).exec()
|
||||
else:
|
||||
self.game.pass_turn()
|
||||
if not self.game.settings.turnless_mode:
|
||||
self.game.pass_turn()
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None:
|
||||
|
||||
@ -4,6 +4,7 @@ local WRITESTATE_SCHEDULE_IN_SECONDS = 60
|
||||
logger = mist.Logger:new("DCSLiberation", "info")
|
||||
logger:info("Check that json.lua is loaded : json = "..tostring(json))
|
||||
|
||||
simulation_time_seconds = 0
|
||||
crash_events = {} -- killed aircraft will be added via S_EVENT_CRASH event
|
||||
dead_events = {} -- killed units will be added via S_EVENT_DEAD event
|
||||
unit_lost_events = {} -- killed units will be added via S_EVENT_UNIT_LOST
|
||||
@ -43,6 +44,7 @@ function write_state()
|
||||
["destroyed_objects_positions"] = destroyed_objects_positions,
|
||||
["killed_ground_units"] = killed_ground_units,
|
||||
["unit_hit_point_updates"] = unit_hit_point_updates,
|
||||
["simulation_time_seconds"] = simulation_time_seconds
|
||||
}
|
||||
if not json then
|
||||
local message = string.format("Unable to save DCS Liberation state to %s, JSON library is not loaded !", _debriefing_file_location)
|
||||
@ -160,6 +162,9 @@ end
|
||||
|
||||
activeWeapons = {}
|
||||
local function onEvent(event)
|
||||
|
||||
simulation_time_seconds = event.time
|
||||
|
||||
if event.id == world.event.S_EVENT_CRASH and event.initiator then
|
||||
crash_events[#crash_events + 1] = event.initiator.getName(event.initiator)
|
||||
write_state()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user