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.flight.squadron.return_pilot(current_pilot)
|
||||||
self.members[index].pilot = 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:
|
def clear(self) -> None:
|
||||||
self.flight.squadron.return_pilots(
|
self.flight.squadron.return_pilots(
|
||||||
[p for p in self.iter_pilots() if p is not None]
|
[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.
|
#: True if the mission ended. If False, the mission exited abnormally.
|
||||||
mission_ended: bool
|
mission_ended: bool
|
||||||
|
|
||||||
|
#: Simulation time since mission start in seconds
|
||||||
|
simulation_time_seconds: float
|
||||||
|
|
||||||
#: Names of aircraft units that were killed during the mission.
|
#: Names of aircraft units that were killed during the mission.
|
||||||
killed_aircraft: List[str]
|
killed_aircraft: List[str]
|
||||||
|
|
||||||
@ -248,6 +251,7 @@ class StateData:
|
|||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
mission_ended=data["mission_ended"],
|
mission_ended=data["mission_ended"],
|
||||||
|
simulation_time_seconds=data["simulation_time_seconds"],
|
||||||
killed_aircraft=killed_aircraft,
|
killed_aircraft=killed_aircraft,
|
||||||
killed_ground_units=killed_ground_units,
|
killed_ground_units=killed_ground_units,
|
||||||
destroyed_statics=data["destroyed_objects_positions"],
|
destroyed_statics=data["destroyed_objects_positions"],
|
||||||
|
|||||||
@ -355,6 +355,16 @@ class Settings:
|
|||||||
"your game after aborting take off."
|
"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_stop_condition: FastForwardStopCondition = choices_option(
|
||||||
"Fast forward until",
|
"Fast forward until",
|
||||||
page=MISSION_GENERATOR_PAGE,
|
page=MISSION_GENERATOR_PAGE,
|
||||||
|
|||||||
@ -28,9 +28,17 @@ class AircraftSimulation:
|
|||||||
self.set_initial_flight_states()
|
self.set_initial_flight_states()
|
||||||
|
|
||||||
def on_game_tick(
|
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:
|
) -> 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(
|
logging.error(
|
||||||
"Cannot resume simulation because aircraft are in combat and "
|
"Cannot resume simulation because aircraft are in combat and "
|
||||||
"auto-resolve is disabled"
|
"auto-resolve is disabled"
|
||||||
@ -45,7 +53,7 @@ class AircraftSimulation:
|
|||||||
duration,
|
duration,
|
||||||
self.results,
|
self.results,
|
||||||
events,
|
events,
|
||||||
self.game.settings.combat_resolution_method,
|
combat_resolution_method,
|
||||||
):
|
):
|
||||||
events.end_combat(combat)
|
events.end_combat(combat)
|
||||||
else:
|
else:
|
||||||
@ -61,11 +69,22 @@ class AircraftSimulation:
|
|||||||
|
|
||||||
# After updating all combat states, check for halts.
|
# After updating all combat states, check for halts.
|
||||||
for flight in self.iter_flights():
|
for flight in self.iter_flights():
|
||||||
if flight.should_halt_sim():
|
if flight.should_halt_sim() and not force_continue:
|
||||||
events.complete_simulation()
|
events.complete_simulation()
|
||||||
return
|
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()
|
events.complete_simulation()
|
||||||
|
|
||||||
def set_initial_flight_states(self) -> None:
|
def set_initial_flight_states(self) -> None:
|
||||||
@ -90,10 +109,9 @@ class AircraftSimulation:
|
|||||||
for package in packages:
|
for package in packages:
|
||||||
yield from package.flights
|
yield from package.flights
|
||||||
|
|
||||||
def _auto_resolve_combat(self) -> bool:
|
def _auto_resolve_combat(
|
||||||
return (
|
self, combat_resolution_method: CombatResolutionMethod, force_continue: bool
|
||||||
self.game.settings.fast_forward_stop_condition
|
) -> bool:
|
||||||
!= FastForwardStopCondition.DISABLED
|
if force_continue:
|
||||||
and self.game.settings.combat_resolution_method
|
return True
|
||||||
!= CombatResolutionMethod.PAUSE
|
return combat_resolution_method != CombatResolutionMethod.PAUSE
|
||||||
)
|
|
||||||
|
|||||||
@ -100,7 +100,11 @@ class GameLoop:
|
|||||||
if not self.started:
|
if not self.started:
|
||||||
raise RuntimeError("Attempted to tick game loop before initialization")
|
raise RuntimeError("Attempted to tick game loop before initialization")
|
||||||
try:
|
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
|
self.completed = self.events.simulation_complete
|
||||||
if not suppress_events:
|
if not suppress_events:
|
||||||
self.send_update(rate_limit=True)
|
self.send_update(rate_limit=True)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from game.ground_forces.combat_stance import CombatStance
|
|||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint
|
||||||
from .gameupdateevents import GameUpdateEvents
|
from .gameupdateevents import GameUpdateEvents
|
||||||
from ..ato.airtaaskingorder import AirTaskingOrder
|
from ..ato.airtaaskingorder import AirTaskingOrder
|
||||||
|
from ..ato.flightstate.atdeparture import AtDeparture
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..game import Game
|
from ..game import Game
|
||||||
@ -57,6 +58,25 @@ class MissionResultsProcessor:
|
|||||||
logging.info(f"{aircraft} destroyed from {squadron}")
|
logging.info(f"{aircraft} destroyed from {squadron}")
|
||||||
squadron.owned_aircraft -= 1
|
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
|
@staticmethod
|
||||||
def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
|
def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
|
||||||
for package in ato.packages:
|
for package in ato.packages:
|
||||||
@ -148,10 +168,21 @@ class MissionResultsProcessor:
|
|||||||
iads_network.update_network(events)
|
iads_network.update_network(events)
|
||||||
return
|
return
|
||||||
|
|
||||||
@staticmethod
|
def commit_damaged_runways(self, debriefing: Debriefing) -> None:
|
||||||
def commit_damaged_runways(debriefing: Debriefing) -> None:
|
|
||||||
for damaged_runway in debriefing.damaged_runways:
|
for damaged_runway in debriefing.damaged_runways:
|
||||||
damaged_runway.damage_runway()
|
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:
|
def commit_captures(self, debriefing: Debriefing, events: GameUpdateEvents) -> None:
|
||||||
for captured in debriefing.base_captures:
|
for captured in debriefing.base_captures:
|
||||||
|
|||||||
@ -7,6 +7,7 @@ from typing import Optional, TYPE_CHECKING
|
|||||||
|
|
||||||
from game.debriefing import Debriefing
|
from game.debriefing import Debriefing
|
||||||
from game.missiongenerator import MissionGenerator
|
from game.missiongenerator import MissionGenerator
|
||||||
|
from game.settings.settings import FastForwardStopCondition, CombatResolutionMethod
|
||||||
from game.unitmap import UnitMap
|
from game.unitmap import UnitMap
|
||||||
from .aircraftsimulation import AircraftSimulation
|
from .aircraftsimulation import AircraftSimulation
|
||||||
from .missionresultsprocessor import MissionResultsProcessor
|
from .missionresultsprocessor import MissionResultsProcessor
|
||||||
@ -37,12 +38,25 @@ class MissionSimulation:
|
|||||||
self.time = self.game.simulation_time
|
self.time = self.game.simulation_time
|
||||||
self.aircraft_simulation.begin_simulation()
|
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.time += TICK
|
||||||
self.game.simulation_time = self.time
|
self.game.simulation_time = self.time
|
||||||
if self.completed:
|
if self.completed:
|
||||||
raise RuntimeError("Simulation already 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
|
self.completed = events.simulation_complete
|
||||||
return events
|
return events
|
||||||
|
|
||||||
@ -76,6 +90,18 @@ class MissionSimulation:
|
|||||||
|
|
||||||
self.game.save_last_turn_state()
|
self.game.save_last_turn_state()
|
||||||
MissionResultsProcessor(self.game).commit(debriefing, events)
|
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:
|
def finish(self) -> None:
|
||||||
self.unit_map = None
|
self.unit_map = None
|
||||||
|
|||||||
@ -50,10 +50,7 @@ class QTopPanel(QFrame):
|
|||||||
self.conditionsWidget = QConditionsWidget(sim_controller)
|
self.conditionsWidget = QConditionsWidget(sim_controller)
|
||||||
self.budgetBox = QBudgetBox(self.game)
|
self.budgetBox = QBudgetBox(self.game)
|
||||||
|
|
||||||
pass_turn_text = "Pass Turn"
|
self.passTurnButton = QPushButton(self._pass_turn_button_text(self.game))
|
||||||
if not self.game or self.game.turn == 0:
|
|
||||||
pass_turn_text = "Begin Campaign"
|
|
||||||
self.passTurnButton = QPushButton(pass_turn_text)
|
|
||||||
self.passTurnButton.setIcon(CONST.ICONS["PassTurn"])
|
self.passTurnButton.setIcon(CONST.ICONS["PassTurn"])
|
||||||
self.passTurnButton.setProperty("style", "btn-primary")
|
self.passTurnButton.setProperty("style", "btn-primary")
|
||||||
self.passTurnButton.clicked.connect(self.passTurn)
|
self.passTurnButton.clicked.connect(self.passTurn)
|
||||||
@ -120,11 +117,9 @@ class QTopPanel(QFrame):
|
|||||||
self.factionsInfos.setGame(game)
|
self.factionsInfos.setGame(game)
|
||||||
|
|
||||||
self.passTurnButton.setEnabled(True)
|
self.passTurnButton.setEnabled(True)
|
||||||
if game and game.turn > 0:
|
self.passTurnButton.setText(self._pass_turn_button_text(game))
|
||||||
self.passTurnButton.setText("Pass Turn")
|
|
||||||
|
|
||||||
if game and game.turn == 0:
|
if game and game.turn == 0:
|
||||||
self.passTurnButton.setText("Begin Campaign")
|
|
||||||
self.proceedButton.setEnabled(False)
|
self.proceedButton.setEnabled(False)
|
||||||
else:
|
else:
|
||||||
self.proceedButton.setEnabled(True)
|
self.proceedButton.setEnabled(True)
|
||||||
@ -283,3 +278,11 @@ class QTopPanel(QFrame):
|
|||||||
|
|
||||||
def budget_update(self, game: Game):
|
def budget_update(self, game: Game):
|
||||||
self.budgetBox.setGame(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:
|
if state is not TurnState.CONTINUE:
|
||||||
GameOverDialog(won=state is TurnState.WIN, parent=self).exec()
|
GameOverDialog(won=state is TurnState.WIN, parent=self).exec()
|
||||||
else:
|
else:
|
||||||
self.game.pass_turn()
|
if not self.game.settings.turnless_mode:
|
||||||
|
self.game.pass_turn()
|
||||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||||
|
|
||||||
def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None:
|
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 = mist.Logger:new("DCSLiberation", "info")
|
||||||
logger:info("Check that json.lua is loaded : json = "..tostring(json))
|
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
|
crash_events = {} -- killed aircraft will be added via S_EVENT_CRASH event
|
||||||
dead_events = {} -- killed units will be added via S_EVENT_DEAD 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
|
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,
|
["destroyed_objects_positions"] = destroyed_objects_positions,
|
||||||
["killed_ground_units"] = killed_ground_units,
|
["killed_ground_units"] = killed_ground_units,
|
||||||
["unit_hit_point_updates"] = unit_hit_point_updates,
|
["unit_hit_point_updates"] = unit_hit_point_updates,
|
||||||
|
["simulation_time_seconds"] = simulation_time_seconds
|
||||||
}
|
}
|
||||||
if not json then
|
if not json then
|
||||||
local message = string.format("Unable to save DCS Liberation state to %s, JSON library is not loaded !", _debriefing_file_location)
|
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 = {}
|
activeWeapons = {}
|
||||||
local function onEvent(event)
|
local function onEvent(event)
|
||||||
|
|
||||||
|
simulation_time_seconds = event.time
|
||||||
|
|
||||||
if event.id == world.event.S_EVENT_CRASH and event.initiator then
|
if event.id == world.event.S_EVENT_CRASH and event.initiator then
|
||||||
crash_events[#crash_events + 1] = event.initiator.getName(event.initiator)
|
crash_events[#crash_events + 1] = event.initiator.getName(event.initiator)
|
||||||
write_state()
|
write_state()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user