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;
EventBus.EmitLevelCompleted(IndexOf(finished), GetTree().CurrentScene, seconds);
// Banks the attempt's coins and skills, marks the level cleared, and advances campaign
// position if this was a First Clear.
Progression.FinishLevel(time);
// Still needed for lives, which move with the death path in the next commit.
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
}