feat: level progression moves to the new domain (slice 10b, step 1)
narrowest safe step: only level identity and progression move. Coins, lives, skills and death stay entirely on GameStateStore and behave as before, so nothing is tracked in two places. GameManager addresses levels by stable LevelId via the catalogue; LevelScenes is gone, so reordering levels no longer invalidates saves. Every level entry goes through EnterLevel, which fixes LevelStarted never firing on the first level or after Continue - ghost recording and splits have been dead outside mid-campaign. GoTo announces after a frame, once the incoming scene is actually current. ChangeSceneToPacked is deferred, so the old code handed GhostEventHandler the scene being torn down. FinishLevel takes LevelTime? - GetCurrentLevelTime can legitimately be 0 and inventing a value is exactly the bad data LevelTime refuses. Continue distinguishes absent from corrupt and refuses to overwrite a corrupt save. Statistics move to the Profile. 69 tests.
This commit is contained in:
@@ -1,17 +1,27 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using Mr.BrickAdventures.Progression;
|
||||
using Mr.BrickAdventures.scripts.components;
|
||||
|
||||
namespace Mr.BrickAdventures.Autoloads;
|
||||
|
||||
/// <summary>
|
||||
/// Game orchestrator - handles scene transitions and game flow.
|
||||
/// State lives in GameStateStore; query it directly for reads/writes.
|
||||
/// Game orchestrator: scene transitions and game flow.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Which Level the player is on now belongs to <see cref="ProgressionRoot"/>, addressed by a stable
|
||||
/// <see cref="LevelId"/> and resolved to a scene by the level catalogue. The old
|
||||
/// <c>Array<PackedScene> LevelScenes</c> is gone: it made a save's meaning depend on the order
|
||||
/// of an inspector array, so reordering levels silently invalidated every save.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Transitional: coins, lives and skills are still owned by <c>GameStateStore</c> and persisted by
|
||||
/// <c>SaveSystem</c>. Only level progression has moved. The two are kept apart deliberately — nothing
|
||||
/// is tracked in both places at once.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public partial class GameManager : Node
|
||||
{
|
||||
[Export] public Array<PackedScene> LevelScenes { get; set; } = [];
|
||||
|
||||
public PlayerController Player
|
||||
{
|
||||
get => GetPlayer();
|
||||
@@ -21,6 +31,7 @@ public partial class GameManager : Node
|
||||
private PlayerController _player;
|
||||
private SpeedRunManager SpeedRunManager => SpeedRunManager.Instance;
|
||||
private GameStateStore Store => GameStateStore.Instance;
|
||||
private ProgressionRoot Progression => ProgressionRoot.Instance;
|
||||
|
||||
public static GameManager Instance { get; private set; }
|
||||
|
||||
@@ -47,70 +58,122 @@ public partial class GameManager : Node
|
||||
|
||||
public void StartNewGame()
|
||||
{
|
||||
if (Progression == null) return;
|
||||
|
||||
Store?.ResetAll();
|
||||
Progression.StartPlaythrough(ProgressionRoot.SoleSlot);
|
||||
SpeedRunManager?.StartTimer();
|
||||
GetTree().ChangeSceneToPacked(LevelScenes[0]);
|
||||
SaveSystem.Instance.SaveGame();
|
||||
EventBus.EmitGameStarted();
|
||||
|
||||
GoTo(Progression.Campaign.First);
|
||||
}
|
||||
|
||||
public void ContinueGame()
|
||||
{
|
||||
var save = SaveSystem.Instance;
|
||||
if (!save.LoadGame())
|
||||
if (Progression == null) return;
|
||||
|
||||
// Coins and skills still come from the old save; level progression from the new one.
|
||||
if (!SaveSystem.Instance.LoadGame())
|
||||
GD.PrintErr("GameManager: no legacy save to continue; starting fresh.");
|
||||
|
||||
if (!Progression.LoadPlaythrough(ProgressionRoot.SoleSlot))
|
||||
{
|
||||
GD.PrintErr("Failed to load game. Starting a new game instead.");
|
||||
var why = Progression.PeekPlaythrough(ProgressionRoot.SoleSlot);
|
||||
if (why == LoadOutcome.Corrupt)
|
||||
{
|
||||
// Deliberately not overwritten: the file is left where it is so it can be recovered.
|
||||
GD.PushError("Saved progress could not be read. It has been left untouched.");
|
||||
return;
|
||||
}
|
||||
|
||||
StartNewGame();
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = Store?.Player.CurrentLevel ?? 0;
|
||||
if (idx < LevelScenes.Count)
|
||||
{
|
||||
GetTree().ChangeSceneToPacked(LevelScenes[idx]);
|
||||
EventBus.EmitGameContinued();
|
||||
}
|
||||
else
|
||||
{
|
||||
GD.PrintErr("No levels unlocked to continue.");
|
||||
}
|
||||
SpeedRunManager?.StartTimer();
|
||||
EventBus.EmitGameContinued();
|
||||
|
||||
GoTo(Progression.CurrentLevel ?? Progression.Campaign.First);
|
||||
}
|
||||
|
||||
public void RestartGame()
|
||||
{
|
||||
if (Progression == null) return;
|
||||
|
||||
Store?.ResetAll();
|
||||
GetTree().ChangeSceneToPacked(LevelScenes[0]);
|
||||
Progression.StartPlaythrough(ProgressionRoot.SoleSlot);
|
||||
SpeedRunManager?.StartTimer();
|
||||
SaveSystem.Instance.SaveGame();
|
||||
|
||||
GoTo(Progression.Campaign.First);
|
||||
}
|
||||
|
||||
public void OnLevelComplete()
|
||||
{
|
||||
if (Store == null) return;
|
||||
if (Progression?.CurrentLevel is not { } finished) return;
|
||||
|
||||
var levelIndex = Store.Player.CurrentLevel;
|
||||
Store.MarkLevelComplete(levelIndex);
|
||||
Store.CommitSessionCoins();
|
||||
Store.CommitSessionSkills();
|
||||
var seconds = SpeedRunManager?.GetCurrentLevelTime() ?? 0.0;
|
||||
LevelTime? time = seconds > 0 ? LevelTime.Of(seconds) : null;
|
||||
|
||||
var completionTime = SpeedRunManager?.GetCurrentLevelTime() ?? 0.0;
|
||||
EventBus.EmitLevelCompleted(levelIndex, GetTree().CurrentScene, completionTime);
|
||||
// Old path: coins and skills earned this attempt.
|
||||
Store?.CommitSessionCoins();
|
||||
Store?.CommitSessionSkills();
|
||||
|
||||
Store.ResetSession();
|
||||
TryToGoToNextLevel();
|
||||
EventBus.EmitLevelCompleted(IndexOf(finished), GetTree().CurrentScene, seconds);
|
||||
|
||||
// New path: banks, marks cleared, and advances campaign position if this was a first clear.
|
||||
Progression.FinishLevel(time);
|
||||
Store?.ResetSession();
|
||||
SaveSystem.Instance.SaveGame();
|
||||
|
||||
if (Progression.CurrentLevel is { } next && !next.Equals(finished))
|
||||
GoTo(next);
|
||||
}
|
||||
|
||||
public void TryToGoToNextLevel()
|
||||
/// <summary>
|
||||
/// Loads a Level's scene, then announces the entry once the new scene is actually current.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The announcement is deliberately after a frame. <c>ChangeSceneToPacked</c> is deferred, so at
|
||||
/// the moment it returns <c>CurrentScene</c> is still the outgoing scene — which is why the old
|
||||
/// code handed <c>GhostEventHandler</c> the scene being torn down and ghosts were spawned into it.
|
||||
/// </remarks>
|
||||
private async void GoTo(LevelId level)
|
||||
{
|
||||
if (Store == null) return;
|
||||
|
||||
var next = Store.Player.CurrentLevel + 1;
|
||||
if (next < LevelScenes.Count && Store.IsLevelUnlocked(next))
|
||||
if (!Progression.CanEnter(level))
|
||||
{
|
||||
Store.Player.CurrentLevel = next;
|
||||
GetTree().ChangeSceneToPacked(LevelScenes[next]);
|
||||
EventBus.EmitLevelStarted(next, GetTree().CurrentScene);
|
||||
GD.PushError($"Level '{level}' is not reachable in this playthrough.");
|
||||
return;
|
||||
}
|
||||
|
||||
var scene = Progression.Catalogue.SceneFor(level);
|
||||
if (scene == null)
|
||||
{
|
||||
GD.PushError($"Level '{level}' has no loadable scene.");
|
||||
return;
|
||||
}
|
||||
|
||||
GetTree().ChangeSceneToPacked(scene);
|
||||
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
|
||||
|
||||
Progression.EnterLevel(level);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Position in the catalogue, for the signals that are still index-based because GhostManager
|
||||
/// names its files <c>ghost_level_{index}.json</c>.
|
||||
/// </summary>
|
||||
private int IndexOf(LevelId level)
|
||||
{
|
||||
var levels = Progression?.Catalogue?.Levels;
|
||||
if (levels == null) return -1;
|
||||
|
||||
for (var i = 0; i < levels.Count; i++)
|
||||
if (levels[i]?.Id == level.Value)
|
||||
return i;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void PauseGame()
|
||||
|
||||
Reference in New Issue
Block a user