diff --git a/Autoloads/AchievementManager.cs b/Autoloads/AchievementManager.cs index 2b13f76..9a1daa5 100644 --- a/Autoloads/AchievementManager.cs +++ b/Autoloads/AchievementManager.cs @@ -77,6 +77,7 @@ public partial class AchievementManager : Node // 1. Mark as unlocked unlockedIds.Add(achievementId); GD.Print($"Achievement Unlocked: {achievement.DisplayName}"); + EventBus.EmitAchievementUnlocked(achievementId); // 2. Show the UI popup if (AchievementPopupScene != null) diff --git a/Autoloads/ConsoleManager.cs b/Autoloads/ConsoleManager.cs index 06b9298..a96c30f 100644 --- a/Autoloads/ConsoleManager.cs +++ b/Autoloads/ConsoleManager.cs @@ -1,11 +1,11 @@ using Godot; -using Mr.BrickAdventures; using Mr.BrickAdventures.scripts.components; namespace Mr.BrickAdventures.Autoloads; public partial class ConsoleManager : Node { + private GameStateStore Store => GameStateStore.Instance; private GameManager GameManager => GameManager.Instance; private AchievementManager AchievementManager => AchievementManager.Instance; private SkillManager _skillManager; @@ -18,114 +18,103 @@ public partial class ConsoleManager : Node private void AddCoinsCommand(int amount) { - GameManager.AddCoins(amount); + if (Store == null) return; + Store.Player.Coins += amount; + EventBus.EmitCoinsChanged(Store.GetTotalCoins()); } private void SetCoinsCommand(int amount) { - GameManager.SetCoins(amount); + if (Store == null) return; + Store.Player.Coins = Mathf.Max(0, amount); + EventBus.EmitCoinsChanged(Store.GetTotalCoins()); } private void SetLivesCommand(int amount) { - GameManager.SetLives(amount); + if (Store == null) return; + Store.Player.Lives = amount; + EventBus.EmitLivesChanged(amount); } private void AddLivesCommand(int amount) { - GameManager.AddLives(amount); + Store?.AddLives(amount); } private void SetHealthCommand(float amount) { - var playerHealthComponent = GameManager.Player.GetNode("HealthComponent"); + var playerHealthComponent = GameManager?.Player?.GetNode("HealthComponent"); if (playerHealthComponent != null) - { playerHealthComponent.Health = amount; - } } private void ResetSessionCommand() { - GameManager.ResetCurrentSessionState(); + Store?.ResetSession(); } private void UnlockSkillCommand(string skillName) { - if (!GetSkillManagement()) return; + if (!EnsureSkillManagement()) return; var skill = _skillManager.GetSkillByName(skillName); - if (skill == null) - { - return; - } + if (skill == null) return; - GameManager.UnlockSkill(skill); + Store?.UnlockSkillPermanently(skill); _skillManager.ActivateSkill(skill); _skillUnlockerComponent.EmitSignal(SkillUnlockerComponent.SignalName.SkillUnlocked, skill); } - private bool GetSkillManagement() - { - var player = GameManager.Player; - if (player == null || !IsInstanceValid(player)) - { - return false; - } - - _skillUnlockerComponent ??= player.GetNode("SkillUnlockerComponent"); - - if (_skillManager != null && _skillUnlockerComponent != null) return true; - - return false; - - } - private void UnlockAllSkillsCommand() { - if (!GetSkillManagement()) return; - + if (!EnsureSkillManagement()) return; _skillUnlockerComponent.UnlockAllSkills(); } private void RemoveSkillCommand(string skillName) { - if (!GetSkillManagement()) return; + if (!EnsureSkillManagement()) return; var skill = _skillManager.GetSkillByName(skillName); - if (skill == null) - { - return; - } + if (skill == null) return; - GameManager.RemoveSkill(skill.Name); + Store?.RemoveUnlockedSkill(skill.Name); _skillManager.DeactivateSkill(skill); } private void RemoveAllSkillsCommand() { - if (!GetSkillManagement()) return; + if (!EnsureSkillManagement()) return; foreach (var skill in _skillManager.AvailableSkills) { - GameManager.RemoveSkill(skill.Name); + Store?.RemoveUnlockedSkill(skill.Name); _skillManager.DeactivateSkill(skill); } } private void GoToNextLevelCommand() { - GameManager.OnLevelComplete(); + GameManager?.OnLevelComplete(); } private void UnlockAchievementCommand(string achievementId) { - AchievementManager.UnlockAchievement(achievementId); + AchievementManager?.UnlockAchievement(achievementId); } private void ResetAchievementCommand(string achievementId) { - AchievementManager.LockAchievement(achievementId); + AchievementManager?.LockAchievement(achievementId); } -} \ No newline at end of file + private bool EnsureSkillManagement() + { + var player = GameManager?.Player; + if (player == null || !IsInstanceValid(player)) return false; + + _skillUnlockerComponent ??= player.GetNode("SkillUnlockerComponent"); + return _skillManager != null && _skillUnlockerComponent != null; + } +} diff --git a/Autoloads/EventBus.cs b/Autoloads/EventBus.cs index de97ee8..bb24e52 100644 --- a/Autoloads/EventBus.cs +++ b/Autoloads/EventBus.cs @@ -140,6 +140,15 @@ public partial class EventBus : Node #endregion + #region Achievement Events + + [Signal] public delegate void AchievementUnlockedEventHandler(string achievementId); + + public static void EmitAchievementUnlocked(string achievementId) + => Instance?.EmitSignal(SignalName.AchievementUnlocked, achievementId); + + #endregion + #region State Change Events [Signal] public delegate void CoinsChangedEventHandler(int totalCoins); diff --git a/Autoloads/GameManager.cs b/Autoloads/GameManager.cs index 51e40a7..15b0dcf 100644 --- a/Autoloads/GameManager.cs +++ b/Autoloads/GameManager.cs @@ -1,14 +1,12 @@ using Godot; using Godot.Collections; using Mr.BrickAdventures.scripts.components; -using Mr.BrickAdventures.scripts.Resources; -using Mr.BrickAdventures.scripts.State; namespace Mr.BrickAdventures.Autoloads; /// -/// Game orchestrator - handles scene management and game flow. -/// State is delegated to GameStateStore for better separation of concerns. +/// Game orchestrator - handles scene transitions and game flow. +/// State lives in GameStateStore; query it directly for reads/writes. /// public partial class GameManager : Node { @@ -45,150 +43,8 @@ public partial class GameManager : Node player.TreeExiting += () => { if (_player == player) _player = null; }; } - #region Coin Operations - - /// - /// Adds coins permanently to the player's saved total (Store.Player.Coins). - /// Use this for out-of-gameplay grants (e.g. console commands, rewards). - /// During active gameplay, coins collected in a level go through - /// and are only committed on level completion. - /// - public void AddCoins(int amount) - { - if (Store != null) - { - Store.Player.Coins += amount; - EventBus.EmitCoinsChanged(Store.GetTotalCoins()); - } - } - - public void SetCoins(int amount) - { - if (Store != null) - { - Store.Player.Coins = Mathf.Max(0, amount); - EventBus.EmitCoinsChanged(Store.GetTotalCoins()); - } - } - - public int GetCoins() => Store?.GetTotalCoins() ?? 0; - - public void RemoveCoins(int amount) => Store?.RemoveCoins(amount); - - #endregion - - #region Lives Operations - - public void AddLives(int amount) => Store?.AddLives(amount); - public void RemoveLives(int amount) => Store?.RemoveLife(); - public void SetLives(int amount) - { - if (Store != null) - { - Store.Player.Lives = amount; - EventBus.EmitLivesChanged(amount); - } - } - public int GetLives() => Store?.Player.Lives ?? 0; - - #endregion - - #region Skill Operations - - public bool IsSkillUnlocked(SkillData skill) => Store?.IsSkillUnlocked(skill) ?? false; - - public void UnlockSkill(SkillData skill) - { - if (Store != null && !Store.IsSkillUnlocked(skill)) - { - Store.Player.UnlockedSkills.Add(skill); - } - } - - public void RemoveSkill(string skillName) - { - if (Store == null) return; - var skills = Store.Player.UnlockedSkills; - for (int i = 0; i < skills.Count; i++) - { - if (skills[i].Name == skillName) - { - skills.RemoveAt(i); - break; - } - } - } - - public void UnlockSkills(Array skills) - { - foreach (var s in skills) - UnlockSkill(s); - } - - public Array GetUnlockedSkills() - { - if (Store == null) return new Array(); - - var skills = Store.GetAllUnlockedSkills(); - var result = new Array(); - foreach (var s in skills) result.Add(s); - return result; - } - - #endregion - - #region Level Operations - - public void UnlockLevel(int levelIndex) => Store?.UnlockLevel(levelIndex); - - public void MarkLevelComplete(int levelIndex) => Store?.MarkLevelComplete(levelIndex); - - public void TryToGoToNextLevel() - { - if (Store == null) return; - - var next = Store.Session.CurrentLevel + 1; - if (next < LevelScenes.Count && Store.IsLevelUnlocked(next)) - { - Store.Session.CurrentLevel = next; - GetTree().ChangeSceneToPacked(LevelScenes[next]); - EventBus.EmitLevelStarted(next, GetTree().CurrentScene); - } - } - - #endregion - - #region State Reset - - public void ResetPlayerState() => Store?.ResetAll(); - - public void ResetCurrentSessionState() => Store?.ResetSession(); - - #endregion - #region Game Flow - public void RestartGame() - { - Store?.ResetAll(); - GetTree().ChangeSceneToPacked(LevelScenes[0]); - SaveSystem.Instance.SaveGame(); - } - - public void QuitGame() => GetTree().Quit(); - - public void PauseGame() - { - Engine.TimeScale = 0; - EventBus.EmitGamePaused(); - } - - public void ResumeGame() - { - Engine.TimeScale = 1; - EventBus.EmitGameResumed(); - } - public void StartNewGame() { Store?.ResetAll(); @@ -208,7 +64,7 @@ public partial class GameManager : Node return; } - var idx = Store?.Session.CurrentLevel ?? 0; + var idx = Store?.Player.CurrentLevel ?? 0; if (idx < LevelScenes.Count) { GetTree().ChangeSceneToPacked(LevelScenes[idx]); @@ -220,11 +76,18 @@ public partial class GameManager : Node } } + public void RestartGame() + { + Store?.ResetAll(); + GetTree().ChangeSceneToPacked(LevelScenes[0]); + SaveSystem.Instance.SaveGame(); + } + public void OnLevelComplete() { if (Store == null) return; - var levelIndex = Store.Session.CurrentLevel; + var levelIndex = Store.Player.CurrentLevel; Store.MarkLevelComplete(levelIndex); Store.CommitSessionCoins(); Store.CommitSessionSkills(); @@ -237,6 +100,33 @@ public partial class GameManager : Node SaveSystem.Instance.SaveGame(); } + public void TryToGoToNextLevel() + { + if (Store == null) return; + + var next = Store.Player.CurrentLevel + 1; + if (next < LevelScenes.Count && Store.IsLevelUnlocked(next)) + { + Store.Player.CurrentLevel = next; + GetTree().ChangeSceneToPacked(LevelScenes[next]); + EventBus.EmitLevelStarted(next, GetTree().CurrentScene); + } + } + + 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 @@ -249,4 +139,4 @@ public partial class GameManager : Node } #endregion -} \ No newline at end of file +} diff --git a/Autoloads/GameStateStore.cs b/Autoloads/GameStateStore.cs index 6279a95..5830abb 100644 --- a/Autoloads/GameStateStore.cs +++ b/Autoloads/GameStateStore.cs @@ -143,7 +143,7 @@ public partial class GameStateStore : Node } /// - /// Unlocks a skill in the session. + /// Unlocks a skill in the session (lost on death, committed on level complete). /// public void UnlockSkillInSession(SkillData skill) { @@ -151,6 +151,30 @@ public partial class GameStateStore : Node Session.SkillsUnlocked.Add(skill); } + /// + /// Permanently unlocks a skill directly in player state (bypasses session, e.g. console commands). + /// + public void UnlockSkillPermanently(SkillData skill) + { + if (!Player.UnlockedSkills.Contains(skill)) + Player.UnlockedSkills.Add(skill); + } + + /// + /// Removes a permanently unlocked skill from player state by name. + /// + public void RemoveUnlockedSkill(string skillName) + { + for (int i = 0; i < Player.UnlockedSkills.Count; i++) + { + if (Player.UnlockedSkills[i].Name == skillName) + { + Player.UnlockedSkills.RemoveAt(i); + return; + } + } + } + /// /// Commits session skills to player state. /// @@ -179,6 +203,26 @@ public partial class GameStateStore : Node #endregion + #region Statistics Operations + + public void IncrementStat(string name, int amount = 1) + { + if (Player.Statistics.TryGetValue(name, out var current)) + Player.Statistics[name] = current + amount; + else + Player.Statistics[name] = amount; + } + + public void SetStat(string name, int value) => Player.Statistics[name] = value; + + public int GetStat(string name) => + Player.Statistics.TryGetValue(name, out var value) ? value : 0; + + public System.Collections.Generic.Dictionary GetAllStats() => + new(Player.Statistics); + + #endregion + #region Reset Operations /// diff --git a/Autoloads/GhostManager.cs b/Autoloads/GhostManager.cs index 1ca2478..1cfe58a 100644 --- a/Autoloads/GhostManager.cs +++ b/Autoloads/GhostManager.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; +using System.Text.Json; using Godot; -using Godot.Collections; using Mr.BrickAdventures.scripts; +using Mr.BrickAdventures.scripts.State; namespace Mr.BrickAdventures.Autoloads; @@ -24,14 +25,14 @@ public partial class GhostManager : Node public void StartRecording(int levelIndex) { if (!IsPlaybackEnabled) return; - + _currentLevelIndex = levelIndex; _currentRecording.Clear(); _startTime = Time.GetTicksMsec() / 1000.0; IsRecording = true; GD.Print("Ghost recording started."); } - + public void StopRecording(bool levelCompleted, double finalTime) { if (!IsRecording) return; @@ -48,23 +49,22 @@ public partial class GhostManager : Node } _currentRecording.Clear(); } - + public void RecordFrame(Vector2 position) { if (!IsRecording) return; - var frame = new GhostFrame + _currentRecording.Add(new GhostFrame { Timestamp = (Time.GetTicksMsec() / 1000.0) - _startTime, Position = position - }; - _currentRecording.Add(frame); + }); } - + public void SpawnGhostPlayer(int levelIndex, Node parent) { if (!IsPlaybackEnabled || GhostPlayerScene == null) return; - + var ghostData = LoadGhostData(levelIndex); if (ghostData.Count > 0) { @@ -74,44 +74,63 @@ public partial class GhostManager : Node GD.Print($"Ghost player spawned for level {levelIndex}."); } } - + private void SaveGhostData(int levelIndex, double time) { - var path = $"user://ghost_level_{levelIndex}.dat"; - using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write); - - var dataToSave = new Godot.Collections.Dictionary + var path = $"user://ghost_level_{levelIndex}.json"; + var saveData = new GhostSaveData { BestTime = time }; + foreach (var frame in _currentRecording) + saveData.Frames.Add(new GhostFrameDto { Timestamp = frame.Timestamp, X = frame.Position.X, Y = frame.Position.Y }); + + try { - { "time", time }, - { "frames", _currentRecording.ToArray() } - }; - file.StoreVar(dataToSave); + var json = JsonSerializer.Serialize(saveData, SaveSystem.JsonOptions); + using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write); + file.StoreString(json); + } + catch (System.Exception e) + { + GD.PrintErr($"GhostManager: Failed to save ghost data: {e.Message}"); + } } - + private List LoadGhostData(int levelIndex) { - var path = $"user://ghost_level_{levelIndex}.dat"; + var path = $"user://ghost_level_{levelIndex}.json"; if (!FileAccess.FileExists(path)) return []; - using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); - var savedData = (Dictionary)file.GetVar(); - var framesArray = (Array)savedData["frames"]; - - var frames = new List(); - foreach (var obj in framesArray) + try { - frames.Add((GhostFrame)obj); + using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); + var saveData = JsonSerializer.Deserialize(file.GetAsText(), SaveSystem.JsonOptions); + if (saveData == null) return []; + + var frames = new List(); + foreach (var dto in saveData.Frames) + frames.Add(new GhostFrame { Timestamp = dto.Timestamp, Position = new Vector2(dto.X, dto.Y) }); + return frames; + } + catch (System.Exception e) + { + GD.PrintErr($"GhostManager: Failed to load ghost data: {e.Message}"); + return []; } - return frames; } - + private double LoadBestTime(int levelIndex) { - var path = $"user://ghost_level_{levelIndex}.dat"; + var path = $"user://ghost_level_{levelIndex}.json"; if (!FileAccess.FileExists(path)) return double.MaxValue; - using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); - var data = (Dictionary)file.GetVar(); - return (double)data["time"]; + try + { + using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); + var saveData = JsonSerializer.Deserialize(file.GetAsText(), SaveSystem.JsonOptions); + return saveData?.BestTime ?? double.MaxValue; + } + catch + { + return double.MaxValue; + } } -} \ No newline at end of file +} diff --git a/Autoloads/SaveSystem.cs b/Autoloads/SaveSystem.cs index bc890e4..a9ec281 100644 --- a/Autoloads/SaveSystem.cs +++ b/Autoloads/SaveSystem.cs @@ -16,7 +16,7 @@ public partial class SaveSystem : Node public static SaveSystem Instance { get; private set; } - private static readonly JsonSerializerOptions JsonOptions = new() + internal static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase @@ -47,7 +47,7 @@ public partial class SaveSystem : Node Version = Version, Coins = store.Player.Coins, Lives = store.Player.Lives, - CurrentLevel = store.Session.CurrentLevel, + CurrentLevel = store.Player.CurrentLevel, CompletedLevels = [.. store.Player.CompletedLevels], UnlockedLevels = new List(store.Player.UnlockedLevels), UnlockedSkillNames = GetSkillNames(store.Player.UnlockedSkills), @@ -105,7 +105,7 @@ public partial class SaveSystem : Node // Apply loaded state store.Player.Coins = saveData.Coins; store.Player.Lives = saveData.Lives; - store.Session.CurrentLevel = saveData.CurrentLevel; + store.Player.CurrentLevel = saveData.CurrentLevel; store.Player.CompletedLevels = saveData.CompletedLevels ?? new List(); store.Player.UnlockedLevels = saveData.UnlockedLevels ?? new List { 0 }; diff --git a/Autoloads/SkillManager.cs b/Autoloads/SkillManager.cs index 49afed9..82bf84b 100644 --- a/Autoloads/SkillManager.cs +++ b/Autoloads/SkillManager.cs @@ -11,7 +11,7 @@ namespace Mr.BrickAdventures.Autoloads; public partial class SkillManager : Node { private PlayerController _player; - private GameManager GameManager => GameManager.Instance; + private GameStateStore Store => GameStateStore.Instance; [Export] public Array AvailableSkills { get; set; } = []; @@ -154,18 +154,14 @@ public partial class SkillManager : Node public void ApplyUnlockedSkills() { if (_player == null || !IsInstanceValid(_player)) return; - if (GameManager == null) return; + if (Store == null) return; foreach (var sd in AvailableSkills) { - if (GameManager.IsSkillUnlocked(sd)) - { + if (Store.IsSkillUnlocked(sd)) CallDeferred(MethodName.AddSkill, sd); - } else - { RemoveSkill(sd.Name); - } } } diff --git a/Autoloads/StatisticsManager.cs b/Autoloads/StatisticsManager.cs deleted file mode 100644 index 079e058..0000000 --- a/Autoloads/StatisticsManager.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Collections.Generic; -using Godot; -using Mr.BrickAdventures.scripts.State; - -namespace Mr.BrickAdventures.Autoloads; - -/// -/// Manages game statistics using GameStateStore. -/// -public partial class StatisticsManager : Node -{ - public static StatisticsManager Instance { get; private set; } - - public override void _Ready() => Instance = this; - public override void _ExitTree() { if (Instance == this) Instance = null; } - - /// - /// Gets the statistics dictionary from the store. - /// - private Dictionary GetStats() - { - return GameStateStore.Instance?.Player.Statistics ?? new Dictionary(); - } - - /// - /// Increases a numerical statistic by a given amount. - /// - public void IncrementStat(string statName, int amount = 1) - { - var stats = GetStats(); - if (stats.TryGetValue(statName, out var currentValue)) - { - stats[statName] = currentValue + amount; - } - else - { - stats[statName] = amount; - } - } - - /// - /// Sets a statistic to a specific value. - /// - public void SetStat(string statName, int value) - { - var stats = GetStats(); - stats[statName] = value; - } - - /// - /// Gets the value of a statistic. - /// - public int GetStat(string statName) - { - var stats = GetStats(); - return stats.TryGetValue(statName, out var value) ? value : 0; - } - - /// - /// Gets a copy of all statistics. - /// - public Dictionary GetAllStats() - { - return new Dictionary(GetStats()); - } -} \ No newline at end of file diff --git a/Autoloads/StatisticsManager.cs.uid b/Autoloads/StatisticsManager.cs.uid deleted file mode 100644 index 50b21dd..0000000 --- a/Autoloads/StatisticsManager.cs.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c5p3l2mhkw0p4 diff --git a/project.godot b/project.godot index 27a68eb..71cd161 100644 --- a/project.godot +++ b/project.godot @@ -46,12 +46,10 @@ SteamManager="*res://Autoloads/SteamManager.cs" AchievementManager="*res://objects/achievement_manager.tscn" SkillManager="*res://objects/skill_manager.tscn" FloatingTextManager="*res://objects/floating_text_manager.tscn" -StatisticsManager="*res://Autoloads/StatisticsManager.cs" SpeedRunManager="res://Autoloads/SpeedRunManager.cs" GhostManager="res://objects/ghost_manager.tscn" StatisticsEventHandler="*res://scripts/Events/StatisticsEventHandler.cs" CoinStateHandler="*res://scripts/Events/CoinStateHandler.cs" -LevelStateHandler="*res://scripts/Events/LevelStateHandler.cs" LivesStateHandler="*res://scripts/Events/LivesStateHandler.cs" SkillCollectHandler="*res://scripts/Events/SkillCollectHandler.cs" GameStateStore="*res://Autoloads/GameStateStore.cs" diff --git a/scripts/Events/GhostEventHandler.cs b/scripts/Events/GhostEventHandler.cs index f068b77..351dd2d 100644 --- a/scripts/Events/GhostEventHandler.cs +++ b/scripts/Events/GhostEventHandler.cs @@ -15,8 +15,11 @@ public partial class GhostEventHandler : Node public override void _ExitTree() { - EventBus.Instance.LevelStarted -= OnLevelStarted; - EventBus.Instance.LevelCompleted -= OnLevelCompleted; + if (EventBus.Instance != null) + { + EventBus.Instance.LevelStarted -= OnLevelStarted; + EventBus.Instance.LevelCompleted -= OnLevelCompleted; + } } private void OnLevelStarted(int levelIndex, Node currentScene) diff --git a/scripts/Events/LevelStateHandler.cs b/scripts/Events/LevelStateHandler.cs deleted file mode 100644 index a79fd06..0000000 --- a/scripts/Events/LevelStateHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Godot; -using Mr.BrickAdventures.Autoloads; - -namespace Mr.BrickAdventures.scripts.Events; - -/// -/// Handles level completion events and updates GameStateStore. -/// -public partial class LevelStateHandler : Node -{ - public override void _Ready() - { - EventBus.Instance.LevelCompleted += OnLevelCompleted; - } - - public override void _ExitTree() - { - if (EventBus.Instance != null) - { - EventBus.Instance.LevelCompleted -= OnLevelCompleted; - } - } - - private void OnLevelCompleted(int levelIndex, Node currentScene, double completionTime) - { - // State mutations (commit coins/skills, reset session) are handled by GameManager.OnLevelComplete - // before this event fires. This handler is reserved for future level-specific side-effects. - } -} diff --git a/scripts/Events/LevelStateHandler.cs.uid b/scripts/Events/LevelStateHandler.cs.uid deleted file mode 100644 index 4cb8409..0000000 --- a/scripts/Events/LevelStateHandler.cs.uid +++ /dev/null @@ -1 +0,0 @@ -uid://gx5vn7viphv diff --git a/scripts/Events/SkillCollectHandler.cs b/scripts/Events/SkillCollectHandler.cs index 9e2c6a2..27728be 100644 --- a/scripts/Events/SkillCollectHandler.cs +++ b/scripts/Events/SkillCollectHandler.cs @@ -33,7 +33,6 @@ public partial class SkillCollectHandler : Node GameStateStore.Instance?.UnlockSkillInSession(skill); // Immediately activate the skill for the player - skill.Level = 1; SkillManager?.AddSkill(skill); // Emit skill unlocked event for UI/achievements diff --git a/scripts/Events/SpeedRunEventHandler.cs b/scripts/Events/SpeedRunEventHandler.cs index 9064a54..ce27557 100644 --- a/scripts/Events/SpeedRunEventHandler.cs +++ b/scripts/Events/SpeedRunEventHandler.cs @@ -13,7 +13,8 @@ public partial class SpeedRunEventHandler : Node public override void _ExitTree() { - EventBus.Instance.LevelCompleted -= OnLevelCompleted; + if (EventBus.Instance != null) + EventBus.Instance.LevelCompleted -= OnLevelCompleted; } private void OnLevelCompleted(int levelIndex, Node currentScene, double completionTime) diff --git a/scripts/Events/StatisticsEventHandler.cs b/scripts/Events/StatisticsEventHandler.cs index 66b311f..00d196d 100644 --- a/scripts/Events/StatisticsEventHandler.cs +++ b/scripts/Events/StatisticsEventHandler.cs @@ -1,19 +1,16 @@ using Godot; using Mr.BrickAdventures.Autoloads; +using Mr.BrickAdventures.scripts.State; namespace Mr.BrickAdventures.scripts.Events; /// /// Handles game events and updates statistics accordingly. -/// Listens to EventBus signals and increments relevant stats. /// public partial class StatisticsEventHandler : Node { - private StatisticsManager StatisticsManager => StatisticsManager.Instance; - public override void _Ready() { - // Subscribe to events EventBus.Instance.CoinCollected += OnCoinCollected; EventBus.Instance.EnemyDefeated += OnEnemyDefeated; EventBus.Instance.PlayerDied += OnPlayerDied; @@ -33,27 +30,17 @@ public partial class StatisticsEventHandler : Node } private void OnCoinCollected(int amount, Vector2 position) - { - StatisticsManager.IncrementStat("coins_collected", amount); - } + => GameStateStore.Instance?.IncrementStat(StatNames.CoinsCollected, amount); private void OnEnemyDefeated(Node enemy, Vector2 position) - { - StatisticsManager.IncrementStat("enemies_defeated"); - } + => GameStateStore.Instance?.IncrementStat(StatNames.EnemiesDefeated); private void OnPlayerDied(Vector2 position) - { - StatisticsManager.IncrementStat("deaths"); - } + => GameStateStore.Instance?.IncrementStat(StatNames.Deaths); private void OnLevelCompleted(int levelIndex, Node currentScene, double completionTime) - { - StatisticsManager.IncrementStat("levels_completed"); - } + => GameStateStore.Instance?.IncrementStat(StatNames.LevelsCompleted); private void OnChildRescued(Vector2 position) - { - StatisticsManager.IncrementStat("children_rescued"); - } + => GameStateStore.Instance?.IncrementStat(StatNames.ChildrenRescued); } diff --git a/scripts/State/GhostSaveData.cs b/scripts/State/GhostSaveData.cs new file mode 100644 index 0000000..958bbb1 --- /dev/null +++ b/scripts/State/GhostSaveData.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Mr.BrickAdventures.scripts.State; + +public class GhostSaveData +{ + public double BestTime { get; set; } + public List Frames { get; set; } = new(); +} + +public class GhostFrameDto +{ + public double Timestamp { get; set; } + public float X { get; set; } + public float Y { get; set; } +} diff --git a/scripts/State/GhostSaveData.cs.uid b/scripts/State/GhostSaveData.cs.uid new file mode 100644 index 0000000..3fa332e --- /dev/null +++ b/scripts/State/GhostSaveData.cs.uid @@ -0,0 +1 @@ +uid://drp68lkok8if3 diff --git a/scripts/State/PlayerState.cs b/scripts/State/PlayerState.cs index 6680b12..f1a884d 100644 --- a/scripts/State/PlayerState.cs +++ b/scripts/State/PlayerState.cs @@ -11,6 +11,11 @@ public class PlayerState { private const int DefaultLives = 3; + /// + /// The level index the player is currently on. Persisted across sessions. + /// + public int CurrentLevel { get; set; } + /// /// Saved coins (not including current session). /// @@ -56,6 +61,7 @@ public class PlayerState /// public void Reset() { + CurrentLevel = 0; Coins = 0; Lives = DefaultLives; CompletedLevels.Clear(); diff --git a/scripts/State/SessionState.cs b/scripts/State/SessionState.cs index a9d3578..de251dc 100644 --- a/scripts/State/SessionState.cs +++ b/scripts/State/SessionState.cs @@ -9,11 +9,6 @@ namespace Mr.BrickAdventures.scripts.State; /// public class SessionState { - /// - /// Current level index being played. - /// - public int CurrentLevel { get; set; } - /// /// Coins collected during this session (not yet saved). /// @@ -29,7 +24,6 @@ public class SessionState /// public static SessionState CreateDefault() => new() { - CurrentLevel = 0, CoinsCollected = 0, SkillsUnlocked = new List() }; @@ -44,11 +38,10 @@ public class SessionState } /// - /// Resets completely including level. + /// Resets all session state. /// public void ResetAll() { - CurrentLevel = 0; CoinsCollected = 0; SkillsUnlocked.Clear(); } diff --git a/scripts/State/StatNames.cs b/scripts/State/StatNames.cs new file mode 100644 index 0000000..2e3968a --- /dev/null +++ b/scripts/State/StatNames.cs @@ -0,0 +1,10 @@ +namespace Mr.BrickAdventures.scripts.State; + +public static class StatNames +{ + public const string CoinsCollected = "coins_collected"; + public const string EnemiesDefeated = "enemies_defeated"; + public const string Deaths = "deaths"; + public const string LevelsCompleted = "levels_completed"; + public const string ChildrenRescued = "children_rescued"; +} diff --git a/scripts/State/StatNames.cs.uid b/scripts/State/StatNames.cs.uid new file mode 100644 index 0000000..490ead5 --- /dev/null +++ b/scripts/State/StatNames.cs.uid @@ -0,0 +1 @@ +uid://dtqjh4v5w41ni diff --git a/scripts/UI/GameOverScreen.cs b/scripts/UI/GameOverScreen.cs index c9e4eed..a0ddd01 100644 --- a/scripts/UI/GameOverScreen.cs +++ b/scripts/UI/GameOverScreen.cs @@ -11,29 +11,26 @@ public partial class GameOverScreen : Control [Export] public Button MainMenuButton { get; set; } [Export] public PackedScene MainMenuScene { get; set; } - private GameManager _gameManager; - public override void _Ready() { - _gameManager = GameManager.Instance; RestartButton.Pressed += OnRestartClicked; MainMenuButton.Pressed += OnMainMenuClicked; } private void OnMainMenuClicked() { - _gameManager.ResetPlayerState(); + GameStateStore.Instance?.ResetAll(); GetTree().ChangeSceneToPacked(MainMenuScene); } private void OnRestartClicked() { - _gameManager.RestartGame(); + GameManager.Instance?.RestartGame(); } public void OnPlayerDeath() { - if (_gameManager == null || _gameManager.GetLives() != 0) return; + if (GameStateStore.Instance?.Player.Lives != 0) return; GameOverPanel.Show(); } diff --git a/scripts/UI/Hud.cs b/scripts/UI/Hud.cs index 1aaf331..11c627d 100644 --- a/scripts/UI/Hud.cs +++ b/scripts/UI/Hud.cs @@ -12,12 +12,7 @@ public partial class Hud : Control [Export] public ProgressBar HealthBar { get; set; } [Export] public Label LivesLabel { get; set; } - private GameManager _gameManager; - - public override void _Ready() - { - _gameManager = GameManager.Instance; - } + private GameStateStore Store => GameStateStore.Instance; public override void _Process(double delta) { @@ -28,12 +23,12 @@ public partial class Hud : Control private void SetCoinsLabel() { - CoinsLabel.Text = Tr("COINS_LABEL") + ": " + _gameManager.GetCoins(); + CoinsLabel.Text = Tr("COINS_LABEL") + ": " + (Store?.GetTotalCoins() ?? 0); } private void SetLivesLabel() { - LivesLabel.Text = Tr("LIVES_LABEL") + ": " + _gameManager.GetLives(); + LivesLabel.Text = Tr("LIVES_LABEL") + ": " + (Store?.Player.Lives ?? 0); } private void SetHealthBar() diff --git a/scripts/UI/Marketplace.cs b/scripts/UI/Marketplace.cs index 7e8bcae..d5585b1 100644 --- a/scripts/UI/Marketplace.cs +++ b/scripts/UI/Marketplace.cs @@ -18,7 +18,7 @@ public partial class Marketplace : Control [Export] public PackedScene MarketplaceButtonScene { get; set; } [Export] public PackedScene SkillButtonScene { get; set; } - private GameManager GameManager => GameManager.Instance; + private GameStateStore Store => GameStateStore.Instance; private SkillManager SkillManager => SkillManager.Instance; private readonly List