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