using Godot; using Mr.BrickAdventures.Progression; using Mr.BrickAdventures.scripts.components; namespace Mr.BrickAdventures.Autoloads; /// /// Game orchestrator: scene transitions and game flow. /// /// /// /// Which Level the player is on now belongs to , addressed by a stable /// and resolved to a scene by the level catalogue. The old /// Array<PackedScene> LevelScenes is gone: it made a save's meaning depend on the order /// of an inspector array, so reordering levels silently invalidated every save. /// /// /// Transitional: coins, lives and skills are still owned by GameStateStore and persisted by /// SaveSystem. Only level progression has moved. The two are kept apart deliberately — nothing /// is tracked in both places at once. /// /// public partial class GameManager : Node { public PlayerController Player { get => GetPlayer(); private set => _player = value; } 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; } public override void _Ready() { Instance = this; EventBus.Instance.PlayerSpawned += OnPlayerSpawned; } public override void _ExitTree() { if (Instance == this) Instance = null; if (EventBus.Instance != null) EventBus.Instance.PlayerSpawned -= OnPlayerSpawned; } private void OnPlayerSpawned(PlayerController player) { _player = player; player.TreeExiting += () => { if (_player == player) _player = null; }; } #region Game Flow public void StartNewGame() { if (Progression == null) return; Store?.ResetAll(); Progression.StartPlaythrough(ProgressionRoot.SoleSlot); SpeedRunManager?.StartTimer(); SaveSystem.Instance.SaveGame(); EventBus.EmitGameStarted(); GoTo(Progression.Campaign.First); } public void ContinueGame() { 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)) { 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; } SpeedRunManager?.StartTimer(); EventBus.EmitGameContinued(); GoTo(Progression.CurrentLevel ?? Progression.Campaign.First); } public void RestartGame() { if (Progression == null) return; Store?.ResetAll(); Progression.StartPlaythrough(ProgressionRoot.SoleSlot); SpeedRunManager?.StartTimer(); SaveSystem.Instance.SaveGame(); GoTo(Progression.Campaign.First); } public void OnLevelComplete() { if (Progression?.CurrentLevel is not { } finished) return; var seconds = SpeedRunManager?.GetCurrentLevelTime() ?? 0.0; LevelTime? time = seconds > 0 ? LevelTime.Of(seconds) : null; // Old path: coins and skills earned this attempt. Store?.CommitSessionCoins(); Store?.CommitSessionSkills(); 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); } /// /// Loads a Level's scene, then announces the entry once the new scene is actually current. /// /// /// The announcement is deliberately after a frame. ChangeSceneToPacked is deferred, so at /// the moment it returns CurrentScene is still the outgoing scene — which is why the old /// code handed GhostEventHandler the scene being torn down and ghosts were spawned into it. /// private async void GoTo(LevelId level) { if (!Progression.CanEnter(level)) { 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); } /// /// Position in the catalogue, for the signals that are still index-based because GhostManager /// names its files ghost_level_{index}.json. /// 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() { Engine.TimeScale = 0; EventBus.EmitGamePaused(); } public void ResumeGame() { Engine.TimeScale = 1; EventBus.EmitGameResumed(); } public void QuitGame() => GetTree().Quit(); #endregion #region Player Lookup public PlayerController GetPlayer() { if (_player != null && IsInstanceValid(_player)) return _player; _player = null; return null; } #endregion }