From bfe951939d0960b875c0635b0f79eb4a3ae94170 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 1 Feb 2026 11:47:40 +0000 Subject: [PATCH] refactor (#6) Reviewed-on: https://git.gabrielkaszewski.dev/GKaszewski/przygody-pana-cegly/pulls/6 --- Autoloads/AchievementManager.cs | 60 ++-- Autoloads/ConsoleManager.cs | 19 +- Autoloads/EventBus.cs | 146 +++++++++ Autoloads/FloatingTextManager.cs | 24 +- Autoloads/GameManager.cs | 286 +++++++++--------- Autoloads/GameStateStore.cs | 199 ++++++++++++ Autoloads/GameStateStore.cs.uid | 1 + Autoloads/SaveSystem.cs | 189 ++++++++++-- Autoloads/SkillManager.cs | 47 +-- Autoloads/StatisticsManager.cs | 74 ++--- objects/entities/brick_player.tscn | 6 +- .../entities/double_jump_skill_pickup.tscn | 34 +++ objects/entities/skill_pickup.tscn | 32 ++ .../x_ray_vision_skill_component.tscn | 2 +- objects/skill_manager.tscn | 7 +- project.godot | 8 +- .../collectables/double_jump_pickup.tres | 10 + resources/skills/xray_vision.tres | 31 -- scenes/level_village_3.tscn | 8 +- scripts/Constants.cs | 25 ++ scripts/Constants.cs.uid | 1 + scripts/Events/CoinStateHandler.cs | 29 ++ scripts/Events/CoinStateHandler.cs.uid | 1 + scripts/Events/GhostEventHandler.cs | 12 +- scripts/Events/LevelStateHandler.cs | 39 +++ scripts/Events/LevelStateHandler.cs.uid | 1 + scripts/Events/LivesStateHandler.cs | 29 ++ scripts/Events/LivesStateHandler.cs.uid | 1 + scripts/Events/SkillCollectHandler.cs | 43 +++ scripts/Events/SkillCollectHandler.cs.uid | 1 + scripts/Events/SpeedRunEventHandler.cs | 8 +- scripts/Events/StatisticsEventHandler.cs | 62 ++++ scripts/Events/StatisticsEventHandler.cs.uid | 1 + scripts/Resources/CollectableResource.cs | 5 + scripts/Resources/CollectableType.cs | 1 + scripts/Resources/SkillData.cs | 3 +- scripts/State/PlayerState.cs | 73 +++++ scripts/State/PlayerState.cs.uid | 1 + scripts/State/SessionState.cs | 55 ++++ scripts/State/SessionState.cs.uid | 1 + scripts/UI/AudioSettings.cs | 19 +- scripts/UI/ChargeProgressBar.cs | 19 +- scripts/UI/Credits.cs | 3 +- scripts/UI/DeathScreen.cs | 52 +++- scripts/UI/GameOverScreen.cs | 9 +- scripts/UI/Hud.cs | 3 +- scripts/UI/MainMenu.cs | 9 +- scripts/UI/Marketplace.cs | 56 ++-- scripts/UI/MarketplaceButton.cs | 17 +- scripts/UI/PauseMenu.cs | 5 +- scripts/UI/SettingsMenu.cs | 11 +- scripts/UI/SpeedRunHud.cs | 9 +- .../components/BrickArmorSkillComponent.cs | 25 +- .../components/BrickShieldSkillComponent.cs | 35 +-- scripts/components/BrickThrowComponent.cs | 40 +-- scripts/components/CollectableComponent.cs | 16 +- .../components/DoubleJumpSkillComponent.cs | 29 +- scripts/components/EnemyDeathComponent.cs | 9 +- scripts/components/ExitDoorComponent.cs | 20 +- .../components/GroundPoundSkillComponent.cs | 39 ++- scripts/components/HealthComponent.cs | 26 +- scripts/components/LeverComponent.cs | 13 +- scripts/components/MagneticSkillComponent.cs | 49 ++- scripts/components/PlayerController.cs | 43 +-- scripts/components/PlayerDeathComponent.cs | 11 +- scripts/components/ScoreComponent.cs | 43 --- scripts/components/ScoreComponent.cs.uid | 1 - scripts/components/SkillComponentBase.cs | 29 ++ scripts/components/SkillComponentBase.cs.uid | 1 + scripts/components/SkillUnlockerComponent.cs | 11 +- .../components/XRayVisionSkillComponent.cs | 17 +- 71 files changed, 1583 insertions(+), 661 deletions(-) create mode 100644 Autoloads/GameStateStore.cs create mode 100644 Autoloads/GameStateStore.cs.uid create mode 100644 objects/entities/double_jump_skill_pickup.tscn create mode 100644 objects/entities/skill_pickup.tscn create mode 100644 resources/collectables/double_jump_pickup.tres delete mode 100644 resources/skills/xray_vision.tres create mode 100644 scripts/Constants.cs create mode 100644 scripts/Constants.cs.uid create mode 100644 scripts/Events/CoinStateHandler.cs create mode 100644 scripts/Events/CoinStateHandler.cs.uid create mode 100644 scripts/Events/LevelStateHandler.cs create mode 100644 scripts/Events/LevelStateHandler.cs.uid create mode 100644 scripts/Events/LivesStateHandler.cs create mode 100644 scripts/Events/LivesStateHandler.cs.uid create mode 100644 scripts/Events/SkillCollectHandler.cs create mode 100644 scripts/Events/SkillCollectHandler.cs.uid create mode 100644 scripts/Events/StatisticsEventHandler.cs create mode 100644 scripts/Events/StatisticsEventHandler.cs.uid create mode 100644 scripts/State/PlayerState.cs create mode 100644 scripts/State/PlayerState.cs.uid create mode 100644 scripts/State/SessionState.cs create mode 100644 scripts/State/SessionState.cs.uid delete mode 100644 scripts/components/ScoreComponent.cs delete mode 100644 scripts/components/ScoreComponent.cs.uid create mode 100644 scripts/components/SkillComponentBase.cs create mode 100644 scripts/components/SkillComponentBase.cs.uid diff --git a/Autoloads/AchievementManager.cs b/Autoloads/AchievementManager.cs index 5b275e9..bf400cd 100644 --- a/Autoloads/AchievementManager.cs +++ b/Autoloads/AchievementManager.cs @@ -1,23 +1,22 @@ using Godot; -using Godot.Collections; using Mr.BrickAdventures.scripts.Resources; +using Mr.BrickAdventures.scripts.State; namespace Mr.BrickAdventures.Autoloads; +/// +/// Manages achievements using GameStateStore. +/// public partial class AchievementManager : Node { [Export] private string AchievementsFolderPath = "res://achievements/"; [Export] private PackedScene AchievementPopupScene { get; set; } - + private System.Collections.Generic.Dictionary _achievements = new(); - private Array _unlockedAchievementIds = []; - private GameManager _gameManager; - + public override void _Ready() { - _gameManager = GetNode("/root/GameManager"); LoadAchievementsFromFolder(); - LoadUnlockedAchievements(); } private void LoadAchievementsFromFolder() @@ -44,7 +43,15 @@ public partial class AchievementManager : Node fileName = dir.GetNext(); } } - + + /// + /// Gets the list of unlocked achievement IDs from the store. + /// + private System.Collections.Generic.List GetUnlockedIds() + { + return GameStateStore.Instance?.Player.UnlockedAchievements ?? new System.Collections.Generic.List(); + } + public void UnlockAchievement(string achievementId) { if (!_achievements.TryGetValue(achievementId, out var achievement)) @@ -53,13 +60,14 @@ public partial class AchievementManager : Node return; } - if (_unlockedAchievementIds.Contains(achievementId)) + var unlockedIds = GetUnlockedIds(); + if (unlockedIds.Contains(achievementId)) { return; // Already unlocked } - // 1. Mark as unlocked internally - _unlockedAchievementIds.Add(achievementId); + // 1. Mark as unlocked + unlockedIds.Add(achievementId); GD.Print($"Achievement Unlocked: {achievement.DisplayName}"); // 2. Show the UI popup @@ -75,31 +83,19 @@ public partial class AchievementManager : Node { SteamManager.UnlockAchievement(achievement.Id); } - - // 4. Save progress - SaveUnlockedAchievements(); - } - - public void LockAchievement(string achievementId) - { - if (_unlockedAchievementIds.Contains(achievementId)) - { - _unlockedAchievementIds.Remove(achievementId); - SaveUnlockedAchievements(); - } - } - - private void SaveUnlockedAchievements() - { - _gameManager.PlayerState["unlocked_achievements"] = _unlockedAchievementIds; - // You might want to trigger a save game here, depending on your SaveSystem } - private void LoadUnlockedAchievements() + public void LockAchievement(string achievementId) { - if (_gameManager.PlayerState.TryGetValue("unlocked_achievements", out var unlocked)) + var unlockedIds = GetUnlockedIds(); + if (unlockedIds.Contains(achievementId)) { - _unlockedAchievementIds = (Array)unlocked; + unlockedIds.Remove(achievementId); } } + + public bool IsAchievementUnlocked(string achievementId) + { + return GetUnlockedIds().Contains(achievementId); + } } \ No newline at end of file diff --git a/Autoloads/ConsoleManager.cs b/Autoloads/ConsoleManager.cs index bd7ef9c..af11c8e 100644 --- a/Autoloads/ConsoleManager.cs +++ b/Autoloads/ConsoleManager.cs @@ -1,4 +1,5 @@ using Godot; +using Mr.BrickAdventures; using Mr.BrickAdventures.scripts.components; namespace Mr.BrickAdventures.Autoloads; @@ -12,9 +13,9 @@ public partial class ConsoleManager : Node public override void _Ready() { - _gameManager = GetNode("/root/GameManager"); - _achievementManager = GetNode("/root/AchievementManager"); - _skillManager = GetNode("/root/SkillManager"); + _gameManager = GameManager.Instance; + _achievementManager = GetNode(Constants.AchievementManagerPath); + _skillManager = SkillManager.Instance; } private void AddCoinsCommand(int amount) @@ -88,7 +89,7 @@ public partial class ConsoleManager : Node _skillUnlockerComponent.UnlockAllSkills(); } - + private void RemoveSkillCommand(string skillName) { if (!GetSkillManagement()) return; @@ -102,28 +103,28 @@ public partial class ConsoleManager : Node _gameManager.RemoveSkill(skill.Name); _skillManager.DeactivateSkill(skill); } - + private void RemoveAllSkillsCommand() { if (!GetSkillManagement()) return; - + foreach (var skill in _skillManager.AvailableSkills) { _gameManager.RemoveSkill(skill.Name); _skillManager.DeactivateSkill(skill); } } - + private void GoToNextLevelCommand() { _gameManager.OnLevelComplete(); } - + private void UnlockAchievementCommand(string achievementId) { _achievementManager.UnlockAchievement(achievementId); } - + private void ResetAchievementCommand(string achievementId) { _achievementManager.LockAchievement(achievementId); diff --git a/Autoloads/EventBus.cs b/Autoloads/EventBus.cs index 9b03718..de97ee8 100644 --- a/Autoloads/EventBus.cs +++ b/Autoloads/EventBus.cs @@ -1,9 +1,155 @@ using Godot; +using Mr.BrickAdventures.scripts.components; +using Mr.BrickAdventures.scripts.Resources; namespace Mr.BrickAdventures.Autoloads; +/// +/// Global event bus for decoupled communication between game systems. +/// Use the static Instance property for easy access from anywhere. +/// public partial class EventBus : Node { + /// + /// Singleton instance. Available after the autoload is initialized. + /// + public static EventBus Instance { get; private set; } + + public override void _Ready() + { + Instance = this; + } + + public override void _ExitTree() + { + if (Instance == this) + Instance = null; + } + + #region Level Events + [Signal] public delegate void LevelStartedEventHandler(int levelIndex, Node currentScene); [Signal] public delegate void LevelCompletedEventHandler(int levelIndex, Node currentScene, double completionTime); + [Signal] public delegate void LevelRestartedEventHandler(int levelIndex); + + public static void EmitLevelStarted(int levelIndex, Node currentScene) + => Instance?.EmitSignal(SignalName.LevelStarted, levelIndex, currentScene); + + public static void EmitLevelCompleted(int levelIndex, Node currentScene, double completionTime) + => Instance?.EmitSignal(SignalName.LevelCompleted, levelIndex, currentScene, completionTime); + + public static void EmitLevelRestarted(int levelIndex) + => Instance?.EmitSignal(SignalName.LevelRestarted, levelIndex); + + #endregion + + #region Player Events + + [Signal] public delegate void PlayerSpawnedEventHandler(PlayerController player); + [Signal] public delegate void PlayerDiedEventHandler(Vector2 position); + [Signal] public delegate void PlayerDamagedEventHandler(float damage, float remainingHealth, Vector2 position); + [Signal] public delegate void PlayerHealedEventHandler(float amount, float newHealth, Vector2 position); + + public static void EmitPlayerSpawned(PlayerController player) + => Instance?.EmitSignal(SignalName.PlayerSpawned, player); + + public static void EmitPlayerDied(Vector2 position) + => Instance?.EmitSignal(SignalName.PlayerDied, position); + + public static void EmitPlayerDamaged(float damage, float remainingHealth, Vector2 position) + => Instance?.EmitSignal(SignalName.PlayerDamaged, damage, remainingHealth, position); + + public static void EmitPlayerHealed(float amount, float newHealth, Vector2 position) + => Instance?.EmitSignal(SignalName.PlayerHealed, amount, newHealth, position); + + #endregion + + #region Combat Events + + [Signal] public delegate void EnemyDefeatedEventHandler(Node enemy, Vector2 position); + [Signal] public delegate void EnemyDamagedEventHandler(Node enemy, float damage, Vector2 position); + + public static void EmitEnemyDefeated(Node enemy, Vector2 position) + => Instance?.EmitSignal(SignalName.EnemyDefeated, enemy, position); + + public static void EmitEnemyDamaged(Node enemy, float damage, Vector2 position) + => Instance?.EmitSignal(SignalName.EnemyDamaged, enemy, damage, position); + + #endregion + + #region Collection Events + + [Signal] public delegate void CoinCollectedEventHandler(int amount, Vector2 position); + [Signal] public delegate void ItemCollectedEventHandler(CollectableType itemType, float amount, Vector2 position); + [Signal] public delegate void ChildRescuedEventHandler(Vector2 position); + [Signal] public delegate void SkillCollectedEventHandler(SkillData skill, Vector2 position); + + public static void EmitCoinCollected(int amount, Vector2 position) + => Instance?.EmitSignal(SignalName.CoinCollected, amount, position); + + public static void EmitItemCollected(CollectableType itemType, float amount, Vector2 position) + => Instance?.EmitSignal(SignalName.ItemCollected, (int)itemType, amount, position); + + public static void EmitChildRescued(Vector2 position) + => Instance?.EmitSignal(SignalName.ChildRescued, position); + + public static void EmitSkillCollected(SkillData skill, Vector2 position) + => Instance?.EmitSignal(SignalName.SkillCollected, skill, position); + + #endregion + + #region Skill Events + + [Signal] public delegate void SkillUnlockedEventHandler(string skillName, int level); + [Signal] public delegate void SkillActivatedEventHandler(string skillName); + [Signal] public delegate void SkillDeactivatedEventHandler(string skillName); + + public static void EmitSkillUnlocked(string skillName, int level = 1) + => Instance?.EmitSignal(SignalName.SkillUnlocked, skillName, level); + + public static void EmitSkillActivated(string skillName) + => Instance?.EmitSignal(SignalName.SkillActivated, skillName); + + public static void EmitSkillDeactivated(string skillName) + => Instance?.EmitSignal(SignalName.SkillDeactivated, skillName); + + #endregion + + #region Game State Events + + [Signal] public delegate void GamePausedEventHandler(); + [Signal] public delegate void GameResumedEventHandler(); + [Signal] public delegate void GameSavedEventHandler(); + [Signal] public delegate void GameStartedEventHandler(); + [Signal] public delegate void GameContinuedEventHandler(); + + public static void EmitGamePaused() + => Instance?.EmitSignal(SignalName.GamePaused); + + public static void EmitGameResumed() + => Instance?.EmitSignal(SignalName.GameResumed); + + public static void EmitGameSaved() + => Instance?.EmitSignal(SignalName.GameSaved); + + public static void EmitGameStarted() + => Instance?.EmitSignal(SignalName.GameStarted); + + public static void EmitGameContinued() + => Instance?.EmitSignal(SignalName.GameContinued); + + #endregion + + #region State Change Events + + [Signal] public delegate void CoinsChangedEventHandler(int totalCoins); + [Signal] public delegate void LivesChangedEventHandler(int lives); + + public static void EmitCoinsChanged(int totalCoins) + => Instance?.EmitSignal(SignalName.CoinsChanged, totalCoins); + + public static void EmitLivesChanged(int lives) + => Instance?.EmitSignal(SignalName.LivesChanged, lives); + + #endregion } \ No newline at end of file diff --git a/Autoloads/FloatingTextManager.cs b/Autoloads/FloatingTextManager.cs index 5db0bb9..3f475cd 100644 --- a/Autoloads/FloatingTextManager.cs +++ b/Autoloads/FloatingTextManager.cs @@ -7,22 +7,34 @@ namespace Mr.BrickAdventures.Autoloads; public partial class FloatingTextManager : Node { [Export] public PackedScene FloatingTextScene { get; set; } - + [ExportGroup("Colors")] [Export] public Color DamageColor { get; set; } = new Color("#b21030"); // Red [Export] public Color HealColor { get; set; } = new Color("#71f341"); // Green [Export] public Color CoinColor { get; set; } = new Color("#ebd320"); // Gold [Export] public Color MessageColor { get; set; } = new Color("#ffffff"); // White - + + public static FloatingTextManager Instance { get; private set; } + + public override void _Ready() + { + Instance = this; + } + + public override void _ExitTree() + { + if (Instance == this) Instance = null; + } + public void ShowDamage(float amount, Vector2 position) { var text = Mathf.Round(amount * 100f).ToString(CultureInfo.InvariantCulture); CreateFloatingText(text, position, DamageColor); } - + public void ShowHeal(float amount, Vector2 position) { - var text = $"+{Mathf.Round(amount)}"; + var text = $"+{Mathf.Round(amount * 100f).ToString(CultureInfo.InvariantCulture)}"; CreateFloatingText(text, position, HealColor); } @@ -31,12 +43,12 @@ public partial class FloatingTextManager : Node var text = $"+{amount}"; CreateFloatingText(text, position, CoinColor); } - + public void ShowMessage(string message, Vector2 position) { CreateFloatingText(message, position, MessageColor); } - + private void CreateFloatingText(string text, Vector2 position, Color color) { if (FloatingTextScene == null) diff --git a/Autoloads/GameManager.cs b/Autoloads/GameManager.cs index f449190..73ceb8d 100644 --- a/Autoloads/GameManager.cs +++ b/Autoloads/GameManager.cs @@ -3,41 +3,34 @@ using Godot; using Godot.Collections; using Mr.BrickAdventures.scripts.components; using Mr.BrickAdventures.scripts.Resources; -using Double = System.Double; +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. +/// public partial class GameManager : Node { [Export] public Array LevelScenes { get; set; } = []; - - public PlayerController Player { + + public PlayerController Player + { get => GetPlayer(); private set => _player = value; } - + private List _sceneNodes = []; private PlayerController _player; private SpeedRunManager _speedRunManager; - private EventBus _eventBus; - - [Export] - public Dictionary PlayerState { get; set; } = new() - { - { "coins", 0 }, - { "lives", 3 }, - { "current_level", 0 }, - { "completed_levels", new Array() }, - { "unlocked_levels", new Array() {0}}, - { "unlocked_skills", new Array() } - }; - - [Export] - public Dictionary CurrentSessionState { get; private set; } = new() - { - { "coins_collected", 0 }, - { "skills_unlocked", new Array() } - }; + + /// + /// Lazy accessor for GameStateStore - avoids initialization order issues. + /// + private GameStateStore Store => GameStateStore.Instance; + + public static GameManager Instance { get; private set; } public override void _EnterTree() { @@ -47,6 +40,7 @@ public partial class GameManager : Node public override void _ExitTree() { + if (Instance == this) Instance = null; GetTree().NodeAdded -= OnNodeAdded; GetTree().NodeRemoved -= OnNodeRemoved; _sceneNodes.Clear(); @@ -54,15 +48,15 @@ public partial class GameManager : Node public override void _Ready() { - _speedRunManager = GetNode("/root/SpeedRunManager"); - _eventBus = GetNode("/root/EventBus"); + Instance = this; + _speedRunManager = GetNode(Constants.SpeedRunManagerPath); } private void OnNodeAdded(Node node) { _sceneNodes.Add(node); } - + private void OnNodeRemoved(Node node) { _sceneNodes.Remove(node); @@ -72,57 +66,71 @@ public partial class GameManager : Node } } + #region Coin Operations + public void AddCoins(int amount) { - PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"] + amount); - } - - public void SetCoins(int amount) => PlayerState["coins"] = Mathf.Max(0, amount); - - public int GetCoins() => (int)PlayerState["coins"] + (int)CurrentSessionState["coins_collected"]; - - public void RemoveCoins(int amount) - { - var sessionCoins = (int)CurrentSessionState["coins_collected"]; - if (amount <= sessionCoins) + if (Store != null) { - CurrentSessionState["coins_collected"] = sessionCoins - amount; + Store.Player.Coins += amount; + EventBus.EmitCoinsChanged(Store.GetTotalCoins()); } - else - { - var remaining = amount - sessionCoins; - CurrentSessionState["coins_collected"] = 0; - PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"] - remaining); - } - PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"]); } - - public void AddLives(int amount) => PlayerState["lives"] = (int)PlayerState["lives"] + amount; - public void RemoveLives(int amount) => PlayerState["lives"] = (int)PlayerState["lives"] - amount; - public void SetLives(int amount) => PlayerState["lives"] = amount; - public int GetLives() => (int)PlayerState["lives"]; - - public bool IsSkillUnlocked(SkillData skill) + + public void SetCoins(int amount) { - return ((Array)PlayerState["unlocked_skills"]).Contains(skill) - || ((Array)CurrentSessionState["skills_unlocked"]).Contains(skill); + 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 (!IsSkillUnlocked(skill)) - ((Array)PlayerState["unlocked_skills"]).Add(skill); + if (Store != null && !Store.IsSkillUnlocked(skill)) + { + Store.Player.UnlockedSkills.Add(skill); + } } public void RemoveSkill(string skillName) { - var arr = (Array)PlayerState["unlocked_skills"]; - foreach (SkillData s in arr) + if (Store == null) return; + var skills = Store.Player.UnlockedSkills; + for (int i = 0; i < skills.Count; i++) { - if (s.Name != skillName) continue; - - arr.Remove(s); - break; + if (skills[i].Name == skillName) + { + skills.RemoveAt(i); + break; + } } } @@ -132,81 +140,82 @@ public partial class GameManager : Node UnlockSkill(s); } - public void ResetPlayerState() + public Array GetUnlockedSkills() { - PlayerState = new Dictionary - { - { "coins", 0 }, - { "lives", 3 }, - { "current_level", 0 }, - { "completed_levels", new Array() }, - { "unlocked_levels", new Array() {0}}, - { "unlocked_skills", new Array() }, - { "statistics", new Godot.Collections.Dictionary()} - }; - } - - public void UnlockLevel(int levelIndex) - { - var unlocked = (Array)PlayerState["unlocked_levels"]; - if (!unlocked.Contains(levelIndex)) unlocked.Add(levelIndex); + 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() { - var next = (int)PlayerState["current_level"] + 1; - var unlocked = (Array)PlayerState["unlocked_levels"]; - if (next < LevelScenes.Count && unlocked.Contains(next)) + if (Store == null) return; + + var next = Store.Session.CurrentLevel + 1; + if (next < LevelScenes.Count && Store.IsLevelUnlocked(next)) { - PlayerState["current_level"] = next; + Store.Session.CurrentLevel = next; GetTree().ChangeSceneToPacked(LevelScenes[next]); - _eventBus.EmitSignal(EventBus.SignalName.LevelStarted, next, GetTree().CurrentScene); + EventBus.EmitLevelStarted(next, GetTree().CurrentScene); } } - - public void MarkLevelComplete(int levelIndex) - { - UnlockLevel(levelIndex + 1); - var completed = (Array)PlayerState["completed_levels"]; - if (!completed.Contains(levelIndex)) completed.Add(levelIndex); - } - public void ResetCurrentSessionState() - { - CurrentSessionState = new Dictionary - { - { "coins_collected", 0 }, - { "skills_unlocked", new Array() } - }; - } - + #endregion + + #region State Reset + + public void ResetPlayerState() => Store?.ResetAll(); + + public void ResetCurrentSessionState() => Store?.ResetSession(); + + #endregion + + #region Game Flow + public void RestartGame() { - ResetPlayerState(); - ResetCurrentSessionState(); + Store?.ResetAll(); GetTree().ChangeSceneToPacked(LevelScenes[0]); - GetNode("/root/SaveSystem").SaveGame(); + GetNode(Constants.SaveSystemPath).SaveGame(); } - + public void QuitGame() => GetTree().Quit(); - public void PauseGame() => Engine.TimeScale = 0; - public void ResumeGame() => Engine.TimeScale = 1; - + public void PauseGame() + { + Engine.TimeScale = 0; + EventBus.EmitGamePaused(); + } + + public void ResumeGame() + { + Engine.TimeScale = 1; + EventBus.EmitGameResumed(); + } + public void StartNewGame() { - ResetPlayerState(); - ResetCurrentSessionState(); - + Store?.ResetAll(); _speedRunManager?.StartTimer(); - GetTree().ChangeSceneToPacked(LevelScenes[0]); - GetNode("/root/SaveSystem").SaveGame(); + GetNode(Constants.SaveSystemPath).SaveGame(); + EventBus.EmitGameStarted(); } public void ContinueGame() { - var save = GetNode("/root/SaveSystem"); + var save = GetNode(Constants.SaveSystemPath); if (!save.LoadGame()) { GD.PrintErr("Failed to load game. Starting a new game instead."); @@ -214,57 +223,56 @@ public partial class GameManager : Node return; } - var idx = (int)PlayerState["current_level"]; + var idx = Store?.Session.CurrentLevel ?? 0; if (idx < LevelScenes.Count) + { GetTree().ChangeSceneToPacked(LevelScenes[idx]); + EventBus.EmitGameContinued(); + } else + { GD.PrintErr("No levels unlocked to continue."); + } } - + public void OnLevelComplete() { - var levelIndex = (int)PlayerState["current_level"]; - MarkLevelComplete(levelIndex); - - AddCoins((int)CurrentSessionState["coins_collected"]); - foreach (var s in (Array)CurrentSessionState["skills_unlocked"]) - UnlockSkill((SkillData)s); + if (Store == null) return; + + var levelIndex = Store.Session.CurrentLevel; + Store.MarkLevelComplete(levelIndex); + Store.CommitSessionCoins(); + Store.CommitSessionSkills(); var completionTime = _speedRunManager?.GetCurrentLevelTime() ?? 0.0; - _eventBus.EmitSignal(EventBus.SignalName.LevelCompleted, levelIndex, GetTree().CurrentScene, completionTime); - - ResetCurrentSessionState(); + EventBus.EmitLevelCompleted(levelIndex, GetTree().CurrentScene, completionTime); + + Store.ResetSession(); TryToGoToNextLevel(); - GetNode("/root/SaveSystem").SaveGame(); + GetNode(Constants.SaveSystemPath).SaveGame(); } - public Array GetUnlockedSkills() - { - var unlocked = (Array)PlayerState["unlocked_skills"]; - var session = (Array)CurrentSessionState["skills_unlocked"]; - if (session!.Count == 0) return unlocked; - if (unlocked!.Count == 0) return session; - var joined = new Array(); - joined.AddRange(unlocked); - joined.AddRange(session); - return joined; - } + #endregion + + #region Player Lookup public PlayerController GetPlayer() { if (_player != null && IsInstanceValid(_player)) return _player; - + _player = null; foreach (var node in _sceneNodes) { if (node is not PlayerController player) continue; - + _player = player; return _player; } - + GD.PrintErr("PlayerController not found in the scene tree."); return null; } + + #endregion } \ No newline at end of file diff --git a/Autoloads/GameStateStore.cs b/Autoloads/GameStateStore.cs new file mode 100644 index 0000000..6279a95 --- /dev/null +++ b/Autoloads/GameStateStore.cs @@ -0,0 +1,199 @@ +using Godot; +using System.Collections.Generic; +using Mr.BrickAdventures.scripts.Resources; +using Mr.BrickAdventures.scripts.State; + +namespace Mr.BrickAdventures.Autoloads; + +/// +/// Central store for game state - single source of truth. +/// Use the static Instance property for easy access. +/// +public partial class GameStateStore : Node +{ + /// + /// Singleton instance. + /// + public static GameStateStore Instance { get; private set; } + + /// + /// Persistent player state (saved to disk). + /// + public PlayerState Player { get; set; } = new(); + + /// + /// Current session state (transient, reset on death/level complete). + /// + public SessionState Session { get; set; } = new(); + + public override void _Ready() + { + Instance = this; + } + + public override void _ExitTree() + { + if (Instance == this) + Instance = null; + } + + #region Coin Operations + + /// + /// Gets total coins (saved + session). + /// + public int GetTotalCoins() => Player.Coins + Session.CoinsCollected; + + /// + /// Adds coins to the session (not saved until level complete). + /// + public void AddSessionCoins(int amount) + { + Session.CoinsCollected += amount; + EventBus.EmitCoinsChanged(GetTotalCoins()); + } + + /// + /// Commits session coins to player state. + /// + public void CommitSessionCoins() + { + Player.Coins += Session.CoinsCollected; + Session.CoinsCollected = 0; + } + + /// + /// Removes coins, first from session then from saved. + /// + public void RemoveCoins(int amount) + { + if (amount <= Session.CoinsCollected) + { + Session.CoinsCollected -= amount; + } + else + { + var remaining = amount - Session.CoinsCollected; + Session.CoinsCollected = 0; + Player.Coins = Mathf.Max(0, Player.Coins - remaining); + } + EventBus.EmitCoinsChanged(GetTotalCoins()); + } + + #endregion + + #region Lives Operations + + /// + /// Decrements lives by 1. + /// + public void RemoveLife() + { + Player.Lives = Mathf.Max(0, Player.Lives - 1); + EventBus.EmitLivesChanged(Player.Lives); + } + + /// + /// Adds lives. + /// + public void AddLives(int amount) + { + Player.Lives += amount; + EventBus.EmitLivesChanged(Player.Lives); + } + + #endregion + + #region Level Operations + + /// + /// Unlocks a level for access. + /// + public void UnlockLevel(int levelIndex) + { + if (!Player.UnlockedLevels.Contains(levelIndex)) + Player.UnlockedLevels.Add(levelIndex); + } + + /// + /// Marks a level as completed and unlocks the next. + /// + public void MarkLevelComplete(int levelIndex) + { + if (!Player.CompletedLevels.Contains(levelIndex)) + Player.CompletedLevels.Add(levelIndex); + UnlockLevel(levelIndex + 1); + } + + /// + /// Checks if a level is unlocked. + /// + public bool IsLevelUnlocked(int levelIndex) => Player.UnlockedLevels.Contains(levelIndex); + + #endregion + + #region Skill Operations + + /// + /// Checks if a skill is unlocked (saved or session). + /// + public bool IsSkillUnlocked(SkillData skill) + { + return Player.UnlockedSkills.Contains(skill) || Session.SkillsUnlocked.Contains(skill); + } + + /// + /// Unlocks a skill in the session. + /// + public void UnlockSkillInSession(SkillData skill) + { + if (!IsSkillUnlocked(skill)) + Session.SkillsUnlocked.Add(skill); + } + + /// + /// Commits session skills to player state. + /// + public void CommitSessionSkills() + { + foreach (var skill in Session.SkillsUnlocked) + { + if (!Player.UnlockedSkills.Contains(skill)) + Player.UnlockedSkills.Add(skill); + } + Session.SkillsUnlocked.Clear(); + } + + /// + /// Gets all unlocked skills from player persistence and current session. + /// + public List GetAllUnlockedSkills() + { + var result = new List(Player.UnlockedSkills); + foreach (var skill in Session.SkillsUnlocked) + { + if (!result.Contains(skill)) result.Add(skill); + } + return result; + } + + #endregion + + #region Reset Operations + + /// + /// Resets only the session state. + /// + public void ResetSession() => Session.Reset(); + + /// + /// Resets everything to defaults. + /// + public void ResetAll() + { + Player.Reset(); + Session.ResetAll(); + } + + #endregion +} diff --git a/Autoloads/GameStateStore.cs.uid b/Autoloads/GameStateStore.cs.uid new file mode 100644 index 0000000..f216d4d --- /dev/null +++ b/Autoloads/GameStateStore.cs.uid @@ -0,0 +1 @@ +uid://bwrhkipwecytk diff --git a/Autoloads/SaveSystem.cs b/Autoloads/SaveSystem.cs index d5ce18a..f0d36a4 100644 --- a/Autoloads/SaveSystem.cs +++ b/Autoloads/SaveSystem.cs @@ -1,61 +1,192 @@ +using System.Collections.Generic; +using System.Text.Json; using Godot; -using Godot.Collections; using Mr.BrickAdventures.scripts.Resources; +using Mr.BrickAdventures.scripts.State; namespace Mr.BrickAdventures.Autoloads; +/// +/// Save system that serializes state to JSON using DTOs. +/// public partial class SaveSystem : Node { - [Export] public string SavePath { get; set; } = "user://savegame.save"; - [Export] public int Version { get; set; } = 1; + [Export] public string SavePath { get; set; } = "user://savegame.json"; + [Export] public int Version { get; set; } = 2; - private GameManager _gameManager; + public static SaveSystem Instance { get; private set; } + + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; public override void _Ready() { - _gameManager = GetNode("/root/GameManager"); + Instance = this; + } + + public override void _ExitTree() + { + if (Instance == this) Instance = null; } public void SaveGame() { - var saveData = new Dictionary + var store = GameStateStore.Instance; + if (store == null) { - { "player_state", _gameManager.PlayerState}, - { "version", Version} + GD.PrintErr("SaveSystem: GameStateStore not available."); + return; + } + + // Convert to DTO (only serializable data) + var saveData = new SaveDataDto + { + Version = Version, + Coins = store.Player.Coins, + Lives = store.Player.Lives, + CurrentLevel = store.Session.CurrentLevel, + CompletedLevels = [.. store.Player.CompletedLevels], + UnlockedLevels = new List(store.Player.UnlockedLevels), + UnlockedSkillNames = GetSkillNames(store.Player.UnlockedSkills), + UnlockedAchievements = new List(store.Player.UnlockedAchievements), + Statistics = new Dictionary(store.Player.Statistics) }; - using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Write); - file.StoreVar(saveData); - GD.Print("Game state saved to: ", SavePath); + try + { + var json = JsonSerializer.Serialize(saveData, JsonOptions); + using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Write); + file.StoreString(json); + GD.Print("Game saved to: ", SavePath); + EventBus.EmitGameSaved(); + } + catch (System.Exception e) + { + GD.PrintErr($"SaveSystem: Failed to save game: {e.Message}"); + } } public bool LoadGame() { if (!FileAccess.FileExists(SavePath)) - return false; - - using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Read); - var saveDataObj = (Dictionary)file.GetVar(); - - if (saveDataObj.ContainsKey("version") && (int)saveDataObj["version"] != Version) { - GD.Print($"Save file version mismatch. Expected: {Version}, Found: {saveDataObj["version"]}"); + GD.Print("SaveSystem: No save file found."); return false; } - GD.Print("Game state loaded from: ", SavePath); - GD.Print("Player state: ", saveDataObj["player_state"]); - _gameManager.PlayerState = (Dictionary)saveDataObj["player_state"]; - - var skills = new Array(); - foreach (var skill in (Array)_gameManager.PlayerState["unlocked_skills"]) + try { - skills.Add(skill); + using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Read); + var json = file.GetAsText(); + var saveData = JsonSerializer.Deserialize(json, JsonOptions); + + if (saveData == null) + { + GD.PrintErr("SaveSystem: Failed to deserialize save data."); + return false; + } + + if (saveData.Version != Version) + { + GD.PrintErr($"SaveSystem: Version mismatch. Expected {Version}, found {saveData.Version}"); + return false; + } + + var store = GameStateStore.Instance; + if (store == null) + { + GD.PrintErr("SaveSystem: GameStateStore not available."); + return false; + } + + // Apply loaded state + store.Player.Coins = saveData.Coins; + store.Player.Lives = saveData.Lives; + store.Session.CurrentLevel = saveData.CurrentLevel; + + store.Player.CompletedLevels = saveData.CompletedLevels ?? new List(); + store.Player.UnlockedLevels = saveData.UnlockedLevels ?? new List { 0 }; + store.Player.UnlockedAchievements = saveData.UnlockedAchievements ?? new List(); + store.Player.Statistics = saveData.Statistics ?? new Dictionary(); + + // Reload skills by name from SkillManager + store.Player.UnlockedSkills = LoadSkillsByName(saveData.UnlockedSkillNames); + + GD.Print("Game loaded from: ", SavePath); + return true; + } + catch (System.Exception e) + { + GD.PrintErr($"SaveSystem: Failed to load game: {e.Message}"); + return false; } - - _gameManager.UnlockSkills(skills); - return true; } - + + private static List GetSkillNames(List skills) + { + var names = new List(); + foreach (var skill in skills) + { + if (skill != null) + names.Add(skill.Name); + } + return names; + } + + private List LoadSkillsByName(List skillNames) + { + var skills = new List(); + if (skillNames == null) return skills; + + var skillManager = GetNodeOrNull(Constants.SkillManagerPath); + if (skillManager == null) + { + GD.PrintErr("SaveSystem: SkillManager not available to resolve skill names."); + return skills; + } + + foreach (var name in skillNames) + { + var skill = skillManager.GetSkillByName(name); + if (skill != null) + { + skills.Add(skill); + } + else + { + GD.PrintErr($"SaveSystem: Skill '{name}' not found in SkillManager."); + } + } + return skills; + } + public bool CheckSaveExists() => FileAccess.FileExists(SavePath); + + public void DeleteSave() + { + if (FileAccess.FileExists(SavePath)) + { + DirAccess.RemoveAbsolute(ProjectSettings.GlobalizePath(SavePath)); + GD.Print("Save file deleted."); + } + } +} + +/// +/// Serializable DTO for save data - no Godot types. +/// +public class SaveDataDto +{ + public int Version { get; set; } + public int Coins { get; set; } + public int Lives { get; set; } + public int CurrentLevel { get; set; } + public List CompletedLevels { get; set; } + public List UnlockedLevels { get; set; } + public List UnlockedSkillNames { get; set; } + public List UnlockedAchievements { get; set; } + public Dictionary Statistics { get; set; } } \ No newline at end of file diff --git a/Autoloads/SkillManager.cs b/Autoloads/SkillManager.cs index d499789..abaf4be 100644 --- a/Autoloads/SkillManager.cs +++ b/Autoloads/SkillManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Godot; using Godot.Collections; +using Mr.BrickAdventures; using Mr.BrickAdventures.scripts.components; using Mr.BrickAdventures.scripts.interfaces; using Mr.BrickAdventures.scripts.Resources; @@ -14,19 +15,27 @@ public partial class SkillManager : Node private PlayerController _player; [Export] public Array AvailableSkills { get; set; } = []; - + public Dictionary ActiveComponents { get; private set; } = new(); + public static SkillManager Instance { get; private set; } + [Signal] public delegate void ActiveThrowSkillChangedEventHandler(BrickThrowComponent throwComponent); [Signal] public delegate void SkillRemovedEventHandler(SkillData skillData); - + public override void _Ready() { - _gameManager = GetNode("/root/GameManager"); + Instance = this; + _gameManager = GetNode(Constants.GameManagerPath); } - + + public override void _ExitTree() + { + if (Instance == this) Instance = null; + } + /// /// Called by the PlayerController from its _Ready method to register itself with the manager. /// @@ -39,7 +48,7 @@ public partial class SkillManager : Node { UnregisterPlayer(); } - + _player = player; if (_player != null) { @@ -61,7 +70,7 @@ public partial class SkillManager : Node } _player = null; } - + public void AddSkill(SkillData skillData) { // Ensure a valid player is registered before adding a skill. @@ -70,7 +79,7 @@ public partial class SkillManager : Node GD.Print("SkillManager: Player not available to add skill."); return; } - + if (ActiveComponents.ContainsKey(skillData.Name)) return; @@ -98,9 +107,9 @@ public partial class SkillManager : Node if (instance is ISkill skill) { // Initialize the skill with the registered player instance. - skill.Initialize(_player, skillData); + skill.Initialize(_player, skillData); skill.Activate(); - } + } else { GD.PrintErr($"Skill scene for '{skillData.Name}' does not implement ISkill!"); @@ -111,18 +120,18 @@ public partial class SkillManager : Node // Add the skill node as a child of the player. _player.AddChild(instance); ActiveComponents[skillData.Name] = instance; - + if (instance is BrickThrowComponent btc) { - EmitSignalActiveThrowSkillChanged(btc); + EmitSignalActiveThrowSkillChanged(btc); } } - + public void RemoveSkill(string skillName) { if (!ActiveComponents.TryGetValue(skillName, out var component)) return; - + if (component.AsGodotObject() is BrickThrowComponent) { EmitSignalActiveThrowSkillChanged(null); @@ -133,7 +142,7 @@ public partial class SkillManager : Node { skill.Deactivate(); } - + if (IsInstanceValid(inst)) inst.QueueFree(); @@ -142,7 +151,6 @@ public partial class SkillManager : Node { if (s.Name == skillName) { - s.IsActive = false; break; } } @@ -150,7 +158,7 @@ public partial class SkillManager : Node var sd = GetSkillByName(skillName); if (sd != null) EmitSignalSkillRemoved(sd); } - + private void RemoveAllActiveSkills() { // Create a copy of keys to avoid modification during iteration @@ -187,12 +195,16 @@ public partial class SkillManager : Node return null; } + public bool IsSkillActive(SkillData skill) + { + return skill != null && ActiveComponents.ContainsKey(skill.Name); + } + public void ActivateSkill(SkillData skill) { if (!ActiveComponents.ContainsKey(skill.Name)) { AddSkill(skill); - skill.IsActive = true; } } @@ -201,7 +213,6 @@ public partial class SkillManager : Node if (ActiveComponents.ContainsKey(skill.Name)) { RemoveSkill(skill.Name); - skill.IsActive = false; } } diff --git a/Autoloads/StatisticsManager.cs b/Autoloads/StatisticsManager.cs index a9c9ccf..c2669f3 100644 --- a/Autoloads/StatisticsManager.cs +++ b/Autoloads/StatisticsManager.cs @@ -1,78 +1,62 @@ +using System.Collections.Generic; using Godot; -using Godot.Collections; +using Mr.BrickAdventures.scripts.State; namespace Mr.BrickAdventures.Autoloads; +/// +/// Manages game statistics using GameStateStore. +/// public partial class StatisticsManager : Node { - private GameManager _gameManager; - private AchievementManager _achievementManager; - private Dictionary _stats = new(); - public override void _Ready() + /// + /// Gets the statistics dictionary from the store. + /// + private Dictionary GetStats() { - _gameManager = GetNode("/root/GameManager"); - _achievementManager = GetNode("/root/AchievementManager"); - LoadStatistics(); + return GameStateStore.Instance?.Player.Statistics ?? new Dictionary(); } - private void LoadStatistics() - { - if (_gameManager.PlayerState.TryGetValue("statistics", out var statsObj)) - { - _stats = (Dictionary)statsObj; - } - else - { - _stats = new Dictionary(); - _gameManager.PlayerState["statistics"] = _stats; - } - } - /// /// Increases a numerical statistic by a given amount. /// public void IncrementStat(string statName, int amount = 1) { - if (_stats.TryGetValue(statName, out var currentValue)) + var stats = GetStats(); + if (stats.TryGetValue(statName, out var currentValue)) { - _stats[statName] = (int)currentValue + amount; + stats[statName] = currentValue + amount; } else { - _stats[statName] = amount; + stats[statName] = amount; } - GD.Print($"Stat '{statName}' updated to: {_stats[statName]}"); - CheckAchievementsForStat(statName); + } + + /// + /// 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 Variant GetStat(string statName, Variant defaultValue = default) + public int GetStat(string statName) { - return _stats.TryGetValue(statName, out var value) ? value : defaultValue; + var stats = GetStats(); + return stats.TryGetValue(statName, out var value) ? value : 0; } /// - /// Checks if the updated stat meets the criteria for any achievements. + /// Gets a copy of all statistics. /// - private void CheckAchievementsForStat(string statName) + public Dictionary GetAllStats() { - switch (statName) - { - case "enemies_defeated": - if ((int)GetStat(statName, 0) >= 100) - { - _achievementManager.UnlockAchievement("slayer_100_enemies"); - } - break; - case "jumps_made": - if ((int)GetStat(statName, 0) >= 1000) - { - _achievementManager.UnlockAchievement("super_jumper"); - } - break; - } + return new Dictionary(GetStats()); } } \ No newline at end of file diff --git a/objects/entities/brick_player.tscn b/objects/entities/brick_player.tscn index eceaf7f..0ff0d6c 100644 --- a/objects/entities/brick_player.tscn +++ b/objects/entities/brick_player.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=58 format=3 uid="uid://bqi5s710xb1ju"] +[gd_scene load_steps=57 format=3 uid="uid://bqi5s710xb1ju"] [ext_resource type="Script" uid="uid://csel4s0e4g5uf" path="res://scripts/components/PlayerController.cs" id="1_yysbb"] [ext_resource type="Shader" uid="uid://bs4xvm4qkurpr" path="res://shaders/hit_flash.tres" id="2_lgb3u"] @@ -19,7 +19,6 @@ [ext_resource type="PackedScene" uid="uid://dre1vit1m4d2n" path="res://objects/movement_abilities/grid_movement_ability.tscn" id="8_xuhvf"] [ext_resource type="Script" uid="uid://dy78ak8eykw6e" path="res://scripts/components/FlipComponent.cs" id="9_yysbb"] [ext_resource type="Script" uid="uid://mnjg3p0aw1ow" path="res://scripts/components/CanPickUpComponent.cs" id="10_yysbb"] -[ext_resource type="Script" uid="uid://ccqb8kd5m0eh7" path="res://scripts/components/ScoreComponent.cs" id="11_o1ihh"] [ext_resource type="Script" uid="uid://dgb8bqcri7nsj" path="res://scripts/components/HealthComponent.cs" id="12_ur2y5"] [ext_resource type="Script" uid="uid://byw1legrv1ep2" path="res://scripts/components/PlayerDeathComponent.cs" id="13_7til7"] [ext_resource type="Script" uid="uid://cecelixl41t3j" path="res://scripts/components/InvulnerabilityComponent.cs" id="15_xuhvf"] @@ -175,9 +174,6 @@ shape = SubResource("RectangleShape2D_vad0t") [node name="CanPickUpComponent" type="Node" parent="."] script = ExtResource("10_yysbb") -[node name="ScoreComponent" type="Node" parent="."] -script = ExtResource("11_o1ihh") - [node name="HealthComponent" type="Node2D" parent="." node_paths=PackedStringArray("HurtSfx", "HealSfx")] script = ExtResource("12_ur2y5") HurtSfx = NodePath("../sfx_hurt") diff --git a/objects/entities/double_jump_skill_pickup.tscn b/objects/entities/double_jump_skill_pickup.tscn new file mode 100644 index 0000000..4ad4764 --- /dev/null +++ b/objects/entities/double_jump_skill_pickup.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=6 format=3 uid="uid://dk2cu8qs7odib"] + +[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_214vd"] +[ext_resource type="Script" uid="uid://r4jybneigfcn" path="res://scripts/components/CollectableComponent.cs" id="2_h7fi3"] +[ext_resource type="Script" uid="uid://bjln6jb1sigx2" path="res://scripts/components/FadeAwayComponent.cs" id="3_b687r"] +[ext_resource type="Resource" path="res://resources/collectables/double_jump_pickup.tres" id="3_h7fi3"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_pickup"] +radius = 12.0 + +[node name="SkillPickup" type="Area2D" groups=["Collectables"]] +collision_layer = 2 +collision_mask = 4 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("CircleShape2D_pickup") + +[node name="Sprite2D" type="Sprite2D" parent="."] +texture = ExtResource("1_214vd") +hframes = 12 +vframes = 12 +frame = 24 + +[node name="CollectableComponent" type="Node" parent="." node_paths=PackedStringArray("Area2D", "CollisionShape")] +script = ExtResource("2_h7fi3") +Area2D = NodePath("..") +CollisionShape = NodePath("../CollisionShape2D") +Data = ExtResource("3_h7fi3") + +[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "Area")] +script = ExtResource("3_b687r") +Sprite = NodePath("../Sprite2D") +FadeDuration = 0.5 +Area = NodePath("..") diff --git a/objects/entities/skill_pickup.tscn b/objects/entities/skill_pickup.tscn new file mode 100644 index 0000000..724bbcb --- /dev/null +++ b/objects/entities/skill_pickup.tscn @@ -0,0 +1,32 @@ +[gd_scene load_steps=5 format=3 uid="uid://0idmnkwids1r"] + +[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_sprite"] +[ext_resource type="Script" uid="uid://r4jybneigfcn" path="res://scripts/components/CollectableComponent.cs" id="2_collectable"] +[ext_resource type="Script" uid="uid://bjln6jb1sigx2" path="res://scripts/components/FadeAwayComponent.cs" id="3_fadeaway"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_pickup"] +radius = 12.0 + +[node name="SkillPickup" type="Area2D" groups=["Collectables"]] +collision_layer = 2 +collision_mask = 4 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("CircleShape2D_pickup") + +[node name="Sprite2D" type="Sprite2D" parent="."] +texture = ExtResource("1_sprite") +hframes = 12 +vframes = 12 +frame = 24 + +[node name="CollectableComponent" type="Node" parent="." node_paths=PackedStringArray("Area2D", "CollisionShape")] +script = ExtResource("2_collectable") +Area2D = NodePath("..") +CollisionShape = NodePath("../CollisionShape2D") + +[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "Area")] +script = ExtResource("3_fadeaway") +Sprite = NodePath("../Sprite2D") +FadeDuration = 0.5 +Area = NodePath("..") diff --git a/objects/player_skills/x_ray_vision_skill_component.tscn b/objects/player_skills/x_ray_vision_skill_component.tscn index d940b89..7febbcf 100644 --- a/objects/player_skills/x_ray_vision_skill_component.tscn +++ b/objects/player_skills/x_ray_vision_skill_component.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=2 format=3 uid="uid://dtxkjif7prm70"] +[gd_scene load_steps=2 format=3 uid="uid://bimyb8suadq3u"] [ext_resource type="Script" uid="uid://dl7vthhurirwc" path="res://scripts/components/XRayVisionSkillComponent.cs" id="1_ebn6n"] diff --git a/objects/skill_manager.tscn b/objects/skill_manager.tscn index 62f779f..3f5097e 100644 --- a/objects/skill_manager.tscn +++ b/objects/skill_manager.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=13 format=3 uid="uid://bi6v7u17vg1ww"] +[gd_scene load_steps=12 format=3 uid="uid://bi6v7u17vg1ww"] [ext_resource type="Script" uid="uid://dru77vj07e18s" path="res://Autoloads/SkillManager.cs" id="1_31033"] [ext_resource type="Script" uid="uid://d4crrfmbgxnqf" path="res://scripts/Resources/SkillData.cs" id="2_87da4"] @@ -6,13 +6,12 @@ [ext_resource type="Resource" uid="uid://cdp8sex36vdq2" path="res://resources/skills/explosive_brick.tres" id="4_53vnv"] [ext_resource type="Resource" uid="uid://cr5lo4h8wm0jc" path="res://resources/skills/fire_brick.tres" id="5_77gav"] [ext_resource type="Resource" uid="uid://ceakv6oqob6m7" path="res://resources/skills/ice_brick.tres" id="6_gib8v"] -[ext_resource type="Resource" uid="uid://d3bjre2etov1n" path="res://resources/skills/magnetic.tres" id="7_6wy8o"] [ext_resource type="Resource" uid="uid://bxsgq8703qx4u" path="res://resources/skills/double_jump.tres" id="8_87da4"] [ext_resource type="Resource" uid="uid://cseilsspimw1n" path="res://resources/skills/ground_pound_skill.tres" id="9_77gav"] -[ext_resource type="Resource" uid="uid://c5dj06l86winx" path="res://resources/skills/xray_vision.tres" id="10_gib8v"] +[ext_resource type="Resource" uid="uid://d3bjre2etov1n" path="res://resources/skills/magnetic.tres" id="10_gib8v"] [ext_resource type="Resource" uid="uid://d12defdtmlk0u" path="res://resources/skills/brick_shield.tres" id="11_6wy8o"] [ext_resource type="Resource" uid="uid://dghnl301o1aiy" path="res://resources/skills/brick_armor.tres" id="12_gib8v"] [node name="SkillManager" type="Node"] script = ExtResource("1_31033") -AvailableSkills = Array[ExtResource("2_87da4")]([ExtResource("3_shjvi"), ExtResource("4_53vnv"), ExtResource("5_77gav"), ExtResource("6_gib8v"), ExtResource("7_6wy8o"), ExtResource("8_87da4"), ExtResource("9_77gav"), ExtResource("10_gib8v"), ExtResource("11_6wy8o"), ExtResource("12_gib8v")]) +AvailableSkills = Array[ExtResource("2_87da4")]([ExtResource("3_shjvi"), ExtResource("4_53vnv"), ExtResource("5_77gav"), ExtResource("6_gib8v"), ExtResource("10_gib8v"), ExtResource("8_87da4"), ExtResource("9_77gav"), ExtResource("11_6wy8o"), ExtResource("12_gib8v")]) diff --git a/project.godot b/project.godot index d8a3535..7c0a8ca 100644 --- a/project.godot +++ b/project.godot @@ -29,7 +29,6 @@ config/icon="uid://jix7wdn0isr3" [autoload] -GameManager="*res://objects/game_manager.tscn" PhantomCameraManager="*res://addons/phantom_camera/scripts/managers/phantom_camera_manager.gd" AudioController="*res://objects/audio_controller.tscn" UIManager="*res://Autoloads/UIManager.cs" @@ -46,6 +45,13 @@ EventBus="*res://Autoloads/EventBus.cs" 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" +GameManager="*res://objects/game_manager.tscn" [debug] diff --git a/resources/collectables/double_jump_pickup.tres b/resources/collectables/double_jump_pickup.tres new file mode 100644 index 0000000..b2a9b65 --- /dev/null +++ b/resources/collectables/double_jump_pickup.tres @@ -0,0 +1,10 @@ +[gd_resource type="Resource" script_class="CollectableResource" load_steps=3 format=3] + +[ext_resource type="Script" path="res://scripts/Resources/CollectableResource.cs" id="1_script"] +[ext_resource type="Resource" uid="uid://bxsgq8703qx4u" path="res://resources/skills/double_jump.tres" id="2_skill"] + +[resource] +script = ExtResource("1_script") +Amount = 0.0 +Type = 3 +Skill = ExtResource("2_skill") diff --git a/resources/skills/xray_vision.tres b/resources/skills/xray_vision.tres deleted file mode 100644 index 7806a6e..0000000 --- a/resources/skills/xray_vision.tres +++ /dev/null @@ -1,31 +0,0 @@ -[gd_resource type="Resource" script_class="SkillData" load_steps=6 format=3 uid="uid://c5dj06l86winx"] - -[ext_resource type="PackedScene" uid="uid://dtxkjif7prm70" path="res://objects/player_skills/x_ray_vision_skill_component.tscn" id="1_ax2d8"] -[ext_resource type="Script" uid="uid://d4crrfmbgxnqf" path="res://scripts/Resources/SkillData.cs" id="1_g8qe3"] -[ext_resource type="Script" uid="uid://dwb0e05pewcsn" path="res://scripts/Resources/SkillUpgrade.cs" id="2_o726x"] - -[sub_resource type="Resource" id="Resource_72ltj"] -script = ExtResource("2_o726x") -Cost = 200 -Description = "" -Properties = Dictionary[String, Variant]({ -"duration": 5.0 -}) -metadata/_custom_type_script = "uid://dwb0e05pewcsn" - -[sub_resource type="Resource" id="Resource_2kdfi"] -script = ExtResource("2_o726x") -Cost = 275 -Description = "" -Properties = Dictionary[String, Variant]({ -"duration": 10.0 -}) -metadata/_custom_type_script = "uid://dwb0e05pewcsn" - -[resource] -script = ExtResource("1_g8qe3") -Name = "XRAY_VISION" -Description = "XRAY_VISION_DESCRIPTION" -Node = ExtResource("1_ax2d8") -Upgrades = Array[ExtResource("2_o726x")]([SubResource("Resource_72ltj"), SubResource("Resource_2kdfi")]) -metadata/_custom_type_script = "uid://d4crrfmbgxnqf" diff --git a/scenes/level_village_3.tscn b/scenes/level_village_3.tscn index da67bb4..323c5ad 100644 --- a/scenes/level_village_3.tscn +++ b/scenes/level_village_3.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=33 format=4 uid="uid://h60obxmju6mo"] +[gd_scene load_steps=34 format=4 uid="uid://h60obxmju6mo"] [ext_resource type="PackedScene" uid="uid://dyp4i4ru2j2jh" path="res://objects/fxs/explosion_fx.tscn" id="1_p30ax"] [ext_resource type="PackedScene" uid="uid://dx80ivlvuuew4" path="res://objects/fxs/fire_fx.tscn" id="2_a7yjf"] @@ -23,6 +23,7 @@ [ext_resource type="PackedScene" uid="uid://bqom4cm7r18db" path="res://objects/entities/killzone.tscn" id="21_p30ax"] [ext_resource type="PackedScene" uid="uid://12jnkdygpxwc" path="res://objects/entities/exit_level.tscn" id="22_a7yjf"] [ext_resource type="PackedScene" uid="uid://t6h2ra7kjyq" path="res://objects/entities/small_heal_potion.tscn" id="23_m6h4x"] +[ext_resource type="PackedScene" uid="uid://dk2cu8qs7odib" path="res://objects/entities/double_jump_skill_pickup.tscn" id="24_6fdf4"] [sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_j7bvy"] texture = ExtResource("7_uvxky") @@ -300,7 +301,7 @@ z_index = 5 position = Vector2(903, -118) metadata/_edit_group_ = true -[node name="HitParticles" parent="Brick Player" index="24"] +[node name="HitParticles" parent="Brick Player" index="23"] process_material = SubResource("ParticleProcessMaterial_lgb3u") [node name="Camera2D" parent="." instance=ExtResource("12_qhkyq")] @@ -364,6 +365,9 @@ position = Vector2(1359, -42) [node name="Killzone" parent="." instance=ExtResource("21_p30ax")] position = Vector2(2456, 815) +[node name="SkillPickup" parent="." instance=ExtResource("24_6fdf4")] +position = Vector2(1136, -109) + [connection signal="Death" from="Brick Player/HealthComponent" to="UI Layer/DeathScreen" method="OnPlayerDeath"] [connection signal="Death" from="Brick Player/HealthComponent" to="UI Layer/GameOverScreen" method="OnPlayerDeath"] diff --git a/scripts/Constants.cs b/scripts/Constants.cs new file mode 100644 index 0000000..b660e7b --- /dev/null +++ b/scripts/Constants.cs @@ -0,0 +1,25 @@ +namespace Mr.BrickAdventures; + +/// +/// Constants for autoload paths and other commonly used values. +/// +public static class Constants +{ + // Autoload paths + public const string EventBusPath = "/root/EventBus"; + public const string GameManagerPath = "/root/GameManager"; + public const string GameStateStorePath = "/root/GameStateStore"; + public const string SaveSystemPath = "/root/SaveSystem"; + public const string SpeedRunManagerPath = "/root/SpeedRunManager"; + public const string GhostManagerPath = "/root/GhostManager"; + public const string AchievementManagerPath = "/root/AchievementManager"; + public const string StatisticsManagerPath = "/root/StatisticsManager"; + public const string SkillManagerPath = "/root/SkillManager"; + public const string FloatingTextManagerPath = "/root/FloatingTextManager"; + public const string UIManagerPath = "/root/UIManager"; + public const string ConsoleManagerPath = "/root/ConsoleManager"; + public const string ConfigFileHandlerPath = "/root/ConfigFileHandler"; + + // Group names + public const string CoinsGroup = "coins"; +} diff --git a/scripts/Constants.cs.uid b/scripts/Constants.cs.uid new file mode 100644 index 0000000..b78e143 --- /dev/null +++ b/scripts/Constants.cs.uid @@ -0,0 +1 @@ +uid://bn7o3n3bomvrd diff --git a/scripts/Events/CoinStateHandler.cs b/scripts/Events/CoinStateHandler.cs new file mode 100644 index 0000000..dc6b9b1 --- /dev/null +++ b/scripts/Events/CoinStateHandler.cs @@ -0,0 +1,29 @@ +using Godot; +using Mr.BrickAdventures.Autoloads; + +namespace Mr.BrickAdventures.scripts.Events; + +/// +/// Handles coin collection events and updates the GameStateStore. +/// Replaces the manual coin logic in GameManager. +/// +public partial class CoinStateHandler : Node +{ + public override void _Ready() + { + EventBus.Instance.CoinCollected += OnCoinCollected; + } + + public override void _ExitTree() + { + if (EventBus.Instance != null) + { + EventBus.Instance.CoinCollected -= OnCoinCollected; + } + } + + private void OnCoinCollected(int amount, Vector2 position) + { + GameStateStore.Instance?.AddSessionCoins(amount); + } +} diff --git a/scripts/Events/CoinStateHandler.cs.uid b/scripts/Events/CoinStateHandler.cs.uid new file mode 100644 index 0000000..550fa58 --- /dev/null +++ b/scripts/Events/CoinStateHandler.cs.uid @@ -0,0 +1 @@ +uid://1qg3q53kkh0k diff --git a/scripts/Events/GhostEventHandler.cs b/scripts/Events/GhostEventHandler.cs index 6a0aab2..3fe6e5e 100644 --- a/scripts/Events/GhostEventHandler.cs +++ b/scripts/Events/GhostEventHandler.cs @@ -1,4 +1,5 @@ using Godot; +using Mr.BrickAdventures; using Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.scripts.Events; @@ -10,11 +11,10 @@ public partial class GhostEventHandler : Node public override void _Ready() { - _ghostManager = GetNode("/root/GhostManager"); - var eventBus = GetNode("/root/EventBus"); - - eventBus.LevelStarted += OnLevelStarted; - eventBus.LevelCompleted += OnLevelCompleted; + _ghostManager = GetNode(Constants.GhostManagerPath); + + EventBus.Instance.LevelStarted += OnLevelStarted; + EventBus.Instance.LevelCompleted += OnLevelCompleted; } private void OnLevelStarted(int levelIndex, Node currentScene) @@ -23,7 +23,7 @@ public partial class GhostEventHandler : Node _ghostManager.StartRecording(levelIndex); _ghostManager.SpawnGhostPlayer(levelIndex, currentScene); } - + private void OnLevelCompleted(int levelIndex, Node currentScene, double completionTime) { _ghostManager.StopRecording(true, completionTime); diff --git a/scripts/Events/LevelStateHandler.cs b/scripts/Events/LevelStateHandler.cs new file mode 100644 index 0000000..5925e6f --- /dev/null +++ b/scripts/Events/LevelStateHandler.cs @@ -0,0 +1,39 @@ +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) + { + var store = GameStateStore.Instance; + if (store == null) return; + + // Mark level complete and unlock next + store.MarkLevelComplete(levelIndex); + + // Commit session data to persistent state + store.CommitSessionCoins(); + store.CommitSessionSkills(); + + // Reset session for next level + store.ResetSession(); + } +} diff --git a/scripts/Events/LevelStateHandler.cs.uid b/scripts/Events/LevelStateHandler.cs.uid new file mode 100644 index 0000000..4cb8409 --- /dev/null +++ b/scripts/Events/LevelStateHandler.cs.uid @@ -0,0 +1 @@ +uid://gx5vn7viphv diff --git a/scripts/Events/LivesStateHandler.cs b/scripts/Events/LivesStateHandler.cs new file mode 100644 index 0000000..77179b1 --- /dev/null +++ b/scripts/Events/LivesStateHandler.cs @@ -0,0 +1,29 @@ +using Godot; +using Mr.BrickAdventures.Autoloads; + +namespace Mr.BrickAdventures.scripts.Events; + +/// +/// Handles player death events and updates lives in GameStateStore. +/// +public partial class LivesStateHandler : Node +{ + public override void _Ready() + { + EventBus.Instance.PlayerDied += OnPlayerDied; + } + + public override void _ExitTree() + { + if (EventBus.Instance != null) + { + EventBus.Instance.PlayerDied -= OnPlayerDied; + } + } + + private void OnPlayerDied(Vector2 position) + { + GameStateStore.Instance?.RemoveLife(); + GameStateStore.Instance?.ResetSession(); + } +} diff --git a/scripts/Events/LivesStateHandler.cs.uid b/scripts/Events/LivesStateHandler.cs.uid new file mode 100644 index 0000000..ade5217 --- /dev/null +++ b/scripts/Events/LivesStateHandler.cs.uid @@ -0,0 +1 @@ +uid://b4ocg7g8vmtvp diff --git a/scripts/Events/SkillCollectHandler.cs b/scripts/Events/SkillCollectHandler.cs new file mode 100644 index 0000000..3c60911 --- /dev/null +++ b/scripts/Events/SkillCollectHandler.cs @@ -0,0 +1,43 @@ +using Godot; +using Mr.BrickAdventures.Autoloads; +using Mr.BrickAdventures.scripts.Resources; + +namespace Mr.BrickAdventures.scripts.Events; + +/// +/// Handles skill collection events and unlocks skills via GameStateStore. +/// Skills are immediately activated but only persisted on level complete. +/// +public partial class SkillCollectHandler : Node +{ + private SkillManager _skillManager; + + public override void _Ready() + { + _skillManager = SkillManager.Instance; + EventBus.Instance.SkillCollected += OnSkillCollected; + } + + public override void _ExitTree() + { + if (EventBus.Instance != null) + { + EventBus.Instance.SkillCollected -= OnSkillCollected; + } + } + + private void OnSkillCollected(SkillData skill, Vector2 position) + { + if (skill == null) return; + + // Unlock in session (will be committed on level complete, lost on death) + GameStateStore.Instance?.UnlockSkillInSession(skill); + + // Immediately activate the skill for the player + skill.Level = 1; + _skillManager?.AddSkill(skill); + + // Emit skill unlocked event for UI/achievements + EventBus.EmitSkillUnlocked(skill.Name, skill.Level); + } +} diff --git a/scripts/Events/SkillCollectHandler.cs.uid b/scripts/Events/SkillCollectHandler.cs.uid new file mode 100644 index 0000000..a9cecdb --- /dev/null +++ b/scripts/Events/SkillCollectHandler.cs.uid @@ -0,0 +1 @@ +uid://c1po4hjvqbslm diff --git a/scripts/Events/SpeedRunEventHandler.cs b/scripts/Events/SpeedRunEventHandler.cs index 93acbd2..babe841 100644 --- a/scripts/Events/SpeedRunEventHandler.cs +++ b/scripts/Events/SpeedRunEventHandler.cs @@ -1,4 +1,5 @@ using Godot; +using Mr.BrickAdventures; using Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.scripts.Events; @@ -10,10 +11,9 @@ public partial class SpeedRunEventHandler : Node public override void _Ready() { - _speedRunManager = GetNode("/root/SpeedRunManager"); - var eventBus = GetNode("/root/EventBus"); - - eventBus.LevelCompleted += OnLevelCompleted; + _speedRunManager = GetNode(Constants.SpeedRunManagerPath); + + 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 new file mode 100644 index 0000000..420d6b9 --- /dev/null +++ b/scripts/Events/StatisticsEventHandler.cs @@ -0,0 +1,62 @@ +using Godot; +using Mr.BrickAdventures; +using Mr.BrickAdventures.Autoloads; + +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; + + public override void _Ready() + { + _statisticsManager = GetNode(Constants.StatisticsManagerPath); + + // Subscribe to events + EventBus.Instance.CoinCollected += OnCoinCollected; + EventBus.Instance.EnemyDefeated += OnEnemyDefeated; + EventBus.Instance.PlayerDied += OnPlayerDied; + EventBus.Instance.LevelCompleted += OnLevelCompleted; + EventBus.Instance.ChildRescued += OnChildRescued; + } + + public override void _ExitTree() + { + if (EventBus.Instance == null) return; + + EventBus.Instance.CoinCollected -= OnCoinCollected; + EventBus.Instance.EnemyDefeated -= OnEnemyDefeated; + EventBus.Instance.PlayerDied -= OnPlayerDied; + EventBus.Instance.LevelCompleted -= OnLevelCompleted; + EventBus.Instance.ChildRescued -= OnChildRescued; + } + + private void OnCoinCollected(int amount, Vector2 position) + { + _statisticsManager.IncrementStat("coins_collected", amount); + } + + private void OnEnemyDefeated(Node enemy, Vector2 position) + { + _statisticsManager.IncrementStat("enemies_defeated"); + } + + private void OnPlayerDied(Vector2 position) + { + _statisticsManager.IncrementStat("deaths"); + } + + private void OnLevelCompleted(int levelIndex, Node currentScene, double completionTime) + { + _statisticsManager.IncrementStat("levels_completed"); + } + + private void OnChildRescued(Vector2 position) + { + _statisticsManager.IncrementStat("children_rescued"); + } +} diff --git a/scripts/Events/StatisticsEventHandler.cs.uid b/scripts/Events/StatisticsEventHandler.cs.uid new file mode 100644 index 0000000..11d654b --- /dev/null +++ b/scripts/Events/StatisticsEventHandler.cs.uid @@ -0,0 +1 @@ +uid://l68tjau3k6bw diff --git a/scripts/Resources/CollectableResource.cs b/scripts/Resources/CollectableResource.cs index 96cb03a..6f5bad7 100644 --- a/scripts/Resources/CollectableResource.cs +++ b/scripts/Resources/CollectableResource.cs @@ -6,4 +6,9 @@ public partial class CollectableResource : Resource { [Export] public float Amount { get; set; } = 0.0f; [Export] public CollectableType Type { get; set; } + + /// + /// The skill to unlock when collected. Only used when Type is Skill. + /// + [Export] public SkillData Skill { get; set; } } \ No newline at end of file diff --git a/scripts/Resources/CollectableType.cs b/scripts/Resources/CollectableType.cs index 840b8b9..133195e 100644 --- a/scripts/Resources/CollectableType.cs +++ b/scripts/Resources/CollectableType.cs @@ -5,4 +5,5 @@ public enum CollectableType Coin, Kid, Health, + Skill, } \ No newline at end of file diff --git a/scripts/Resources/SkillData.cs b/scripts/Resources/SkillData.cs index 0f06e13..660aba0 100644 --- a/scripts/Resources/SkillData.cs +++ b/scripts/Resources/SkillData.cs @@ -10,11 +10,10 @@ public partial class SkillData : Resource [Export] public string Name { get; set; } = "New Skill"; [Export] public string Description { get; set; } = "New Skill"; [Export] public Texture2D Icon { get; set; } - [Export] public bool IsActive { get; set; } = false; [Export] public int Level { get; set; } = 1; [Export] public SkillType Type { get; set; } = SkillType.Throw; [Export] public PackedScene Node { get; set; } [Export] public Array Upgrades { get; set; } = []; - + public int MaxLevel => Upgrades.Count; } \ No newline at end of file diff --git a/scripts/State/PlayerState.cs b/scripts/State/PlayerState.cs new file mode 100644 index 0000000..8388217 --- /dev/null +++ b/scripts/State/PlayerState.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using Mr.BrickAdventures.scripts.Resources; + +namespace Mr.BrickAdventures.scripts.State; + +/// +/// Persistent player data that survives across sessions. +/// This is a POCO (Plain Old C# Object) for predictable state management. +/// +public class PlayerState +{ + /// + /// Saved coins (not including current session). + /// + public int Coins { get; set; } + + /// + /// Remaining lives. + /// + public int Lives { get; set; } = 3; + + /// + /// Indices of completed levels. + /// + public List CompletedLevels { get; set; } = new(); + + /// + /// Indices of levels the player can access. + /// + public List UnlockedLevels { get; set; } = new() { 0 }; + + /// + /// Skills the player has permanently unlocked. + /// + public List UnlockedSkills { get; set; } = new(); + + /// + /// Statistics dictionary for tracking game stats. + /// + public Dictionary Statistics { get; set; } = new(); + + /// + /// IDs of unlocked achievements. + /// + public List UnlockedAchievements { get; set; } = new(); + + /// + /// Creates a fresh default player state. + /// + public static PlayerState CreateDefault() => new() + { + Coins = 0, + Lives = 3, + CompletedLevels = new List(), + UnlockedLevels = new List { 0 }, + UnlockedSkills = new List(), + Statistics = new Dictionary() + }; + + /// + /// Resets this state to default values. + /// + public void Reset() + { + Coins = 0; + Lives = 3; + CompletedLevels.Clear(); + UnlockedLevels.Clear(); + UnlockedLevels.Add(0); + UnlockedSkills.Clear(); + Statistics.Clear(); + } +} diff --git a/scripts/State/PlayerState.cs.uid b/scripts/State/PlayerState.cs.uid new file mode 100644 index 0000000..348078b --- /dev/null +++ b/scripts/State/PlayerState.cs.uid @@ -0,0 +1 @@ +uid://gtr1e60jq7iv diff --git a/scripts/State/SessionState.cs b/scripts/State/SessionState.cs new file mode 100644 index 0000000..a9d3578 --- /dev/null +++ b/scripts/State/SessionState.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using Mr.BrickAdventures.scripts.Resources; + +namespace Mr.BrickAdventures.scripts.State; + +/// +/// Data for the current gameplay session. +/// Reset when player dies or completes a level. +/// +public class SessionState +{ + /// + /// Current level index being played. + /// + public int CurrentLevel { get; set; } + + /// + /// Coins collected during this session (not yet saved). + /// + public int CoinsCollected { get; set; } + + /// + /// Skills unlocked during this session (not yet saved). + /// + public List SkillsUnlocked { get; set; } = new(); + + /// + /// Creates a fresh session state. + /// + public static SessionState CreateDefault() => new() + { + CurrentLevel = 0, + CoinsCollected = 0, + SkillsUnlocked = new List() + }; + + /// + /// Resets session state to defaults. + /// + public void Reset() + { + CoinsCollected = 0; + SkillsUnlocked.Clear(); + } + + /// + /// Resets completely including level. + /// + public void ResetAll() + { + CurrentLevel = 0; + CoinsCollected = 0; + SkillsUnlocked.Clear(); + } +} diff --git a/scripts/State/SessionState.cs.uid b/scripts/State/SessionState.cs.uid new file mode 100644 index 0000000..5b23b3e --- /dev/null +++ b/scripts/State/SessionState.cs.uid @@ -0,0 +1 @@ +uid://chqsdleqrnl7b diff --git a/scripts/UI/AudioSettings.cs b/scripts/UI/AudioSettings.cs index b9acc6f..96eeebb 100644 --- a/scripts/UI/AudioSettings.cs +++ b/scripts/UI/AudioSettings.cs @@ -1,4 +1,5 @@ using Godot; +using Mr.BrickAdventures; using Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.scripts.UI; @@ -10,19 +11,19 @@ public partial class AudioSettings : Control [Export] public Slider SfxVolumeSlider { get; set; } [Export] public Control AudioSettingsControl { get; set; } [Export] public float MuteThreshold { get; set; } = -20f; - + private UIManager _uiManager; private ConfigFileHandler _configFileHandler; public override void _Ready() { - _uiManager = GetNode("/root/UIManager"); - _configFileHandler = GetNode("/root/ConfigFileHandler"); + _uiManager = GetNode(Constants.UIManagerPath); + _configFileHandler = GetNode(Constants.ConfigFileHandlerPath); Initialize(); MasterVolumeSlider.ValueChanged += OnMasterVolumeChanged; MusicVolumeSlider.ValueChanged += OnMusicVolumeChanged; SfxVolumeSlider.ValueChanged += OnSfxVolumeChanged; - + LoadSettings(); } @@ -35,7 +36,7 @@ public partial class AudioSettings : Control { if (!@event.IsActionReleased("ui_cancel")) return; if (!_uiManager.IsScreenOnTop(AudioSettingsControl)) return; - + SaveSettings(); _uiManager.PopScreen(); } @@ -64,12 +65,12 @@ public partial class AudioSettings : Control MasterVolumeSlider.Value = volumeDb; MasterVolumeSlider.MinValue = MuteThreshold; MasterVolumeSlider.MaxValue = 0f; - + var musicVolumeDb = AudioServer.GetBusVolumeDb(AudioServer.GetBusIndex("music")); MusicVolumeSlider.Value = musicVolumeDb; MusicVolumeSlider.MinValue = MuteThreshold; MusicVolumeSlider.MaxValue = 0f; - + var sfxVolumeDb = AudioServer.GetBusVolumeDb(AudioServer.GetBusIndex("sfx")); SfxVolumeSlider.Value = sfxVolumeDb; SfxVolumeSlider.MinValue = MuteThreshold; @@ -95,12 +96,12 @@ public partial class AudioSettings : Control { var settingsConfig = _configFileHandler.SettingsConfig; if (!settingsConfig.HasSection("audio_settings")) return; - + var masterVolume = (float)settingsConfig.GetValue("audio_settings", "master_volume", MasterVolumeSlider.Value); var musicVolume = (float)settingsConfig.GetValue("audio_settings", "music_volume", MusicVolumeSlider.Value); var sfxVolume = (float)settingsConfig.GetValue("audio_settings", "sfx_volume", SfxVolumeSlider.Value); var muteThreshold = (float)settingsConfig.GetValue("audio_settings", "mute_threshold", MuteThreshold); - + MasterVolumeSlider.Value = masterVolume; MusicVolumeSlider.Value = musicVolume; SfxVolumeSlider.Value = sfxVolume; diff --git a/scripts/UI/ChargeProgressBar.cs b/scripts/UI/ChargeProgressBar.cs index 91654ef..286295f 100644 --- a/scripts/UI/ChargeProgressBar.cs +++ b/scripts/UI/ChargeProgressBar.cs @@ -1,4 +1,5 @@ using Godot; +using Mr.BrickAdventures; using Mr.BrickAdventures.Autoloads; using Mr.BrickAdventures.scripts.components; using Mr.BrickAdventures.scripts.Resources; @@ -17,15 +18,15 @@ public partial class ChargeProgressBar : ProgressBar { ProgressBar.Hide(); - _skillManager = GetNodeOrNull("/root/SkillManager"); + _skillManager = GetNodeOrNull(Constants.SkillManagerPath); if (_skillManager == null) { GD.PrintErr("ChargeProgressBar: SkillManager autoload not found."); return; } - + _skillManager.ActiveThrowSkillChanged += OnActiveThrowSkillChanged; - + SetupDependencies(); } @@ -43,7 +44,7 @@ public partial class ChargeProgressBar : ProgressBar OnOwnerExiting(); if (throwComponent == null || !IsInstanceValid(throwComponent)) return; - + _throwComponent = throwComponent; _throwComponent.TreeExiting += OnOwnerExiting; SetupDependencies(); @@ -60,7 +61,7 @@ public partial class ChargeProgressBar : ProgressBar } _throwComponent = null; } - + private void SetupDependencies() { @@ -68,7 +69,7 @@ public partial class ChargeProgressBar : ProgressBar { return; } - + if (_throwComponent.ThrowInputBehavior is ChargeThrowInputResource throwInput) { _throwInput = throwInput; @@ -77,7 +78,7 @@ public partial class ChargeProgressBar : ProgressBar { _throwInput = null; } - + if (_throwInput == null) { return; @@ -88,9 +89,9 @@ public partial class ChargeProgressBar : ProgressBar ProgressBar.Hide(); return; } - + SetupProgressBar(); - + _throwInput.ChargeStarted += OnChargeStarted; _throwInput.ChargeStopped += OnChargeStopped; _throwInput.ChargeUpdated += OnChargeUpdated; diff --git a/scripts/UI/Credits.cs b/scripts/UI/Credits.cs index b0d2d0f..d26b114 100644 --- a/scripts/UI/Credits.cs +++ b/scripts/UI/Credits.cs @@ -1,4 +1,5 @@ using Godot; +using Mr.BrickAdventures; using Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.scripts.UI; @@ -9,7 +10,7 @@ public partial class Credits : Control public override void _Ready() { - _uiManager = GetNode("/root/UIManager"); + _uiManager = GetNode(Constants.UIManagerPath); } public override void _UnhandledInput(InputEvent @event) diff --git a/scripts/UI/DeathScreen.cs b/scripts/UI/DeathScreen.cs index ce03d99..80b56ca 100644 --- a/scripts/UI/DeathScreen.cs +++ b/scripts/UI/DeathScreen.cs @@ -1,6 +1,8 @@ using Godot; +using Mr.BrickAdventures; using Mr.BrickAdventures.Autoloads; using Mr.BrickAdventures.scripts.Resources; +using Mr.BrickAdventures.scripts.State; namespace Mr.BrickAdventures.scripts.UI; @@ -12,27 +14,44 @@ public partial class DeathScreen : Control [Export] public Label LivesLeftLabel { get; set; } [Export] public float TimeoutTime { get; set; } = 2.0f; [Export] public Godot.Collections.Array NodesToDisable { get; set; } = new(); - + private GameManager _gameManager; private Timer _timer; public override void _Ready() { - _gameManager = GetNode("/root/GameManager"); - SetLabels(); + _gameManager = GameManager.Instance; + + // Subscribe to lives changed event for reactive updates + EventBus.Instance.LivesChanged += OnLivesChanged; } - + + public override void _ExitTree() + { + if (EventBus.Instance != null) + { + EventBus.Instance.LivesChanged -= OnLivesChanged; + } + } + + private void OnLivesChanged(int lives) + { + // Update the label when lives change + LivesLeftLabel.Text = $" x {lives}"; + } + private void SetLabels() { - if (_gameManager == null) return; - if (CurrentLevel != null) { CurrentLevelLabel.Text = CurrentLevel.LevelName; } - LivesLeftLabel.Text = $" x {_gameManager.GetLives()}"; + + // Read current lives from store + var lives = GameStateStore.Instance?.Player.Lives ?? 0; + LivesLeftLabel.Text = $" x {lives}"; } - + private void SetupTimer() { _timer = new Timer(); @@ -42,31 +61,32 @@ public partial class DeathScreen : Control AddChild(_timer); _timer.Start(); } - + private void ToggleNodes() { foreach (var node in NodesToDisable) { - node.ProcessMode = node.ProcessMode == ProcessModeEnum.Disabled - ? ProcessModeEnum.Inherit + node.ProcessMode = node.ProcessMode == ProcessModeEnum.Disabled + ? ProcessModeEnum.Inherit : ProcessModeEnum.Disabled; } } - + public void OnPlayerDeath() { if (_gameManager == null) return; - + ToggleNodes(); SetLabels(); Show(); SetupTimer(); } - + private void OnTimeout() { - if (_gameManager == null || _gameManager.GetLives() == 0) return; - + var lives = GameStateStore.Instance?.Player.Lives ?? 0; + if (lives == 0) return; + GetTree().ReloadCurrentScene(); } } \ No newline at end of file diff --git a/scripts/UI/GameOverScreen.cs b/scripts/UI/GameOverScreen.cs index 6cb17a1..c9e4eed 100644 --- a/scripts/UI/GameOverScreen.cs +++ b/scripts/UI/GameOverScreen.cs @@ -1,4 +1,5 @@ using Godot; +using Mr.BrickAdventures; using Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.scripts.UI; @@ -9,14 +10,14 @@ public partial class GameOverScreen : Control [Export] public Button RestartButton { get; set; } [Export] public Button MainMenuButton { get; set; } [Export] public PackedScene MainMenuScene { get; set; } - + private GameManager _gameManager; public override void _Ready() { - _gameManager = GetNode("/root/GameManager"); + _gameManager = GameManager.Instance; RestartButton.Pressed += OnRestartClicked; - MainMenuButton.Pressed += OnMainMenuClicked; + MainMenuButton.Pressed += OnMainMenuClicked; } private void OnMainMenuClicked() @@ -33,7 +34,7 @@ public partial class GameOverScreen : Control public void OnPlayerDeath() { if (_gameManager == null || _gameManager.GetLives() != 0) return; - + GameOverPanel.Show(); } } \ No newline at end of file diff --git a/scripts/UI/Hud.cs b/scripts/UI/Hud.cs index 9f584ce..1aaf331 100644 --- a/scripts/UI/Hud.cs +++ b/scripts/UI/Hud.cs @@ -1,4 +1,5 @@ using Godot; +using Mr.BrickAdventures; using Mr.BrickAdventures.Autoloads; using Mr.BrickAdventures.scripts.components; @@ -15,7 +16,7 @@ public partial class Hud : Control public override void _Ready() { - _gameManager = GetNode("/root/GameManager"); + _gameManager = GameManager.Instance; } public override void _Process(double delta) diff --git a/scripts/UI/MainMenu.cs b/scripts/UI/MainMenu.cs index b3aa727..4b9c161 100644 --- a/scripts/UI/MainMenu.cs +++ b/scripts/UI/MainMenu.cs @@ -1,4 +1,5 @@ using Godot; +using Mr.BrickAdventures; using Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.scripts.UI; @@ -14,16 +15,16 @@ public partial class MainMenu : Control [Export] public Label VersionLabel { get; set; } [Export] public Control SettingsControl { get; set; } [Export] public Control CreditsControl { get; set; } - + private SaveSystem _saveSystem; private GameManager _gameManager; private UIManager _uiManager; public override void _Ready() { - _saveSystem = GetNode("/root/SaveSystem"); - _gameManager = GetNode("/root/GameManager"); - _uiManager = GetNode("/root/UIManager"); + _saveSystem = SaveSystem.Instance; + _gameManager = GameManager.Instance; + _uiManager = GetNode(Constants.UIManagerPath); NewGameButton.Pressed += OnNewGamePressed; ContinueButton.Pressed += OnContinuePressed; diff --git a/scripts/UI/Marketplace.cs b/scripts/UI/Marketplace.cs index 4b801c7..1e91681 100644 --- a/scripts/UI/Marketplace.cs +++ b/scripts/UI/Marketplace.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Godot; using Godot.Collections; +using Mr.BrickAdventures; using Mr.BrickAdventures.Autoloads; using Mr.BrickAdventures.scripts.components; using Mr.BrickAdventures.scripts.Resources; @@ -17,7 +18,7 @@ public partial class Marketplace : Control [Export] public Array ComponentsToDisable { get; set; } = []; [Export] public PackedScene MarketplaceButtonScene { get; set; } [Export] public PackedScene SkillButtonScene { get; set; } - + private GameManager _gameManager; private SkillManager _skillManager; private readonly List