refactor: enhance GameStateStore integration and improve skill management

This commit is contained in:
2026-03-19 02:33:07 +01:00
parent 3e36e48e97
commit eeefca4d4e
31 changed files with 260 additions and 419 deletions

View File

@@ -77,6 +77,7 @@ public partial class AchievementManager : Node
// 1. Mark as unlocked // 1. Mark as unlocked
unlockedIds.Add(achievementId); unlockedIds.Add(achievementId);
GD.Print($"Achievement Unlocked: {achievement.DisplayName}"); GD.Print($"Achievement Unlocked: {achievement.DisplayName}");
EventBus.EmitAchievementUnlocked(achievementId);
// 2. Show the UI popup // 2. Show the UI popup
if (AchievementPopupScene != null) if (AchievementPopupScene != null)

View File

@@ -1,11 +1,11 @@
using Godot; using Godot;
using Mr.BrickAdventures;
using Mr.BrickAdventures.scripts.components; using Mr.BrickAdventures.scripts.components;
namespace Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.Autoloads;
public partial class ConsoleManager : Node public partial class ConsoleManager : Node
{ {
private GameStateStore Store => GameStateStore.Instance;
private GameManager GameManager => GameManager.Instance; private GameManager GameManager => GameManager.Instance;
private AchievementManager AchievementManager => AchievementManager.Instance; private AchievementManager AchievementManager => AchievementManager.Instance;
private SkillManager _skillManager; private SkillManager _skillManager;
@@ -18,114 +18,103 @@ public partial class ConsoleManager : Node
private void AddCoinsCommand(int amount) private void AddCoinsCommand(int amount)
{ {
GameManager.AddCoins(amount); if (Store == null) return;
Store.Player.Coins += amount;
EventBus.EmitCoinsChanged(Store.GetTotalCoins());
} }
private void SetCoinsCommand(int amount) private void SetCoinsCommand(int amount)
{ {
GameManager.SetCoins(amount); if (Store == null) return;
Store.Player.Coins = Mathf.Max(0, amount);
EventBus.EmitCoinsChanged(Store.GetTotalCoins());
} }
private void SetLivesCommand(int amount) private void SetLivesCommand(int amount)
{ {
GameManager.SetLives(amount); if (Store == null) return;
Store.Player.Lives = amount;
EventBus.EmitLivesChanged(amount);
} }
private void AddLivesCommand(int amount) private void AddLivesCommand(int amount)
{ {
GameManager.AddLives(amount); Store?.AddLives(amount);
} }
private void SetHealthCommand(float amount) private void SetHealthCommand(float amount)
{ {
var playerHealthComponent = GameManager.Player.GetNode<HealthComponent>("HealthComponent"); var playerHealthComponent = GameManager?.Player?.GetNode<HealthComponent>("HealthComponent");
if (playerHealthComponent != null) if (playerHealthComponent != null)
{
playerHealthComponent.Health = amount; playerHealthComponent.Health = amount;
} }
}
private void ResetSessionCommand() private void ResetSessionCommand()
{ {
GameManager.ResetCurrentSessionState(); Store?.ResetSession();
} }
private void UnlockSkillCommand(string skillName) private void UnlockSkillCommand(string skillName)
{ {
if (!GetSkillManagement()) return; if (!EnsureSkillManagement()) return;
var skill = _skillManager.GetSkillByName(skillName); var skill = _skillManager.GetSkillByName(skillName);
if (skill == null) if (skill == null) return;
{
return;
}
GameManager.UnlockSkill(skill); Store?.UnlockSkillPermanently(skill);
_skillManager.ActivateSkill(skill); _skillManager.ActivateSkill(skill);
_skillUnlockerComponent.EmitSignal(SkillUnlockerComponent.SignalName.SkillUnlocked, skill); _skillUnlockerComponent.EmitSignal(SkillUnlockerComponent.SignalName.SkillUnlocked, skill);
} }
private bool GetSkillManagement()
{
var player = GameManager.Player;
if (player == null || !IsInstanceValid(player))
{
return false;
}
_skillUnlockerComponent ??= player.GetNode<SkillUnlockerComponent>("SkillUnlockerComponent");
if (_skillManager != null && _skillUnlockerComponent != null) return true;
return false;
}
private void UnlockAllSkillsCommand() private void UnlockAllSkillsCommand()
{ {
if (!GetSkillManagement()) return; if (!EnsureSkillManagement()) return;
_skillUnlockerComponent.UnlockAllSkills(); _skillUnlockerComponent.UnlockAllSkills();
} }
private void RemoveSkillCommand(string skillName) private void RemoveSkillCommand(string skillName)
{ {
if (!GetSkillManagement()) return; if (!EnsureSkillManagement()) return;
var skill = _skillManager.GetSkillByName(skillName); var skill = _skillManager.GetSkillByName(skillName);
if (skill == null) if (skill == null) return;
{
return;
}
GameManager.RemoveSkill(skill.Name); Store?.RemoveUnlockedSkill(skill.Name);
_skillManager.DeactivateSkill(skill); _skillManager.DeactivateSkill(skill);
} }
private void RemoveAllSkillsCommand() private void RemoveAllSkillsCommand()
{ {
if (!GetSkillManagement()) return; if (!EnsureSkillManagement()) return;
foreach (var skill in _skillManager.AvailableSkills) foreach (var skill in _skillManager.AvailableSkills)
{ {
GameManager.RemoveSkill(skill.Name); Store?.RemoveUnlockedSkill(skill.Name);
_skillManager.DeactivateSkill(skill); _skillManager.DeactivateSkill(skill);
} }
} }
private void GoToNextLevelCommand() private void GoToNextLevelCommand()
{ {
GameManager.OnLevelComplete(); GameManager?.OnLevelComplete();
} }
private void UnlockAchievementCommand(string achievementId) private void UnlockAchievementCommand(string achievementId)
{ {
AchievementManager.UnlockAchievement(achievementId); AchievementManager?.UnlockAchievement(achievementId);
} }
private void ResetAchievementCommand(string achievementId) private void ResetAchievementCommand(string achievementId)
{ {
AchievementManager.LockAchievement(achievementId); AchievementManager?.LockAchievement(achievementId);
} }
private bool EnsureSkillManagement()
{
var player = GameManager?.Player;
if (player == null || !IsInstanceValid(player)) return false;
_skillUnlockerComponent ??= player.GetNode<SkillUnlockerComponent>("SkillUnlockerComponent");
return _skillManager != null && _skillUnlockerComponent != null;
}
} }

View File

@@ -140,6 +140,15 @@ public partial class EventBus : Node
#endregion #endregion
#region Achievement Events
[Signal] public delegate void AchievementUnlockedEventHandler(string achievementId);
public static void EmitAchievementUnlocked(string achievementId)
=> Instance?.EmitSignal(SignalName.AchievementUnlocked, achievementId);
#endregion
#region State Change Events #region State Change Events
[Signal] public delegate void CoinsChangedEventHandler(int totalCoins); [Signal] public delegate void CoinsChangedEventHandler(int totalCoins);

View File

@@ -1,14 +1,12 @@
using Godot; using Godot;
using Godot.Collections; using Godot.Collections;
using Mr.BrickAdventures.scripts.components; using Mr.BrickAdventures.scripts.components;
using Mr.BrickAdventures.scripts.Resources;
using Mr.BrickAdventures.scripts.State;
namespace Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.Autoloads;
/// <summary> /// <summary>
/// Game orchestrator - handles scene management and game flow. /// Game orchestrator - handles scene transitions and game flow.
/// State is delegated to GameStateStore for better separation of concerns. /// State lives in GameStateStore; query it directly for reads/writes.
/// </summary> /// </summary>
public partial class GameManager : Node public partial class GameManager : Node
{ {
@@ -45,150 +43,8 @@ public partial class GameManager : Node
player.TreeExiting += () => { if (_player == player) _player = null; }; player.TreeExiting += () => { if (_player == player) _player = null; };
} }
#region Coin Operations
/// <summary>
/// Adds coins permanently to the player's saved total (Store.Player.Coins).
/// Use this for out-of-gameplay grants (e.g. console commands, rewards).
/// During active gameplay, coins collected in a level go through
/// <see cref="GameStateStore.AddSessionCoins"/> and are only committed on level completion.
/// </summary>
public void AddCoins(int amount)
{
if (Store != null)
{
Store.Player.Coins += amount;
EventBus.EmitCoinsChanged(Store.GetTotalCoins());
}
}
public void SetCoins(int amount)
{
if (Store != null)
{
Store.Player.Coins = Mathf.Max(0, amount);
EventBus.EmitCoinsChanged(Store.GetTotalCoins());
}
}
public int GetCoins() => Store?.GetTotalCoins() ?? 0;
public void RemoveCoins(int amount) => Store?.RemoveCoins(amount);
#endregion
#region Lives Operations
public void AddLives(int amount) => Store?.AddLives(amount);
public void RemoveLives(int amount) => Store?.RemoveLife();
public void SetLives(int amount)
{
if (Store != null)
{
Store.Player.Lives = amount;
EventBus.EmitLivesChanged(amount);
}
}
public int GetLives() => Store?.Player.Lives ?? 0;
#endregion
#region Skill Operations
public bool IsSkillUnlocked(SkillData skill) => Store?.IsSkillUnlocked(skill) ?? false;
public void UnlockSkill(SkillData skill)
{
if (Store != null && !Store.IsSkillUnlocked(skill))
{
Store.Player.UnlockedSkills.Add(skill);
}
}
public void RemoveSkill(string skillName)
{
if (Store == null) return;
var skills = Store.Player.UnlockedSkills;
for (int i = 0; i < skills.Count; i++)
{
if (skills[i].Name == skillName)
{
skills.RemoveAt(i);
break;
}
}
}
public void UnlockSkills(Array<SkillData> skills)
{
foreach (var s in skills)
UnlockSkill(s);
}
public Array<SkillData> GetUnlockedSkills()
{
if (Store == null) return new Array<SkillData>();
var skills = Store.GetAllUnlockedSkills();
var result = new Array<SkillData>();
foreach (var s in skills) result.Add(s);
return result;
}
#endregion
#region Level Operations
public void UnlockLevel(int levelIndex) => Store?.UnlockLevel(levelIndex);
public void MarkLevelComplete(int levelIndex) => Store?.MarkLevelComplete(levelIndex);
public void TryToGoToNextLevel()
{
if (Store == null) return;
var next = Store.Session.CurrentLevel + 1;
if (next < LevelScenes.Count && Store.IsLevelUnlocked(next))
{
Store.Session.CurrentLevel = next;
GetTree().ChangeSceneToPacked(LevelScenes[next]);
EventBus.EmitLevelStarted(next, GetTree().CurrentScene);
}
}
#endregion
#region State Reset
public void ResetPlayerState() => Store?.ResetAll();
public void ResetCurrentSessionState() => Store?.ResetSession();
#endregion
#region Game Flow #region Game Flow
public void RestartGame()
{
Store?.ResetAll();
GetTree().ChangeSceneToPacked(LevelScenes[0]);
SaveSystem.Instance.SaveGame();
}
public void QuitGame() => GetTree().Quit();
public void PauseGame()
{
Engine.TimeScale = 0;
EventBus.EmitGamePaused();
}
public void ResumeGame()
{
Engine.TimeScale = 1;
EventBus.EmitGameResumed();
}
public void StartNewGame() public void StartNewGame()
{ {
Store?.ResetAll(); Store?.ResetAll();
@@ -208,7 +64,7 @@ public partial class GameManager : Node
return; return;
} }
var idx = Store?.Session.CurrentLevel ?? 0; var idx = Store?.Player.CurrentLevel ?? 0;
if (idx < LevelScenes.Count) if (idx < LevelScenes.Count)
{ {
GetTree().ChangeSceneToPacked(LevelScenes[idx]); GetTree().ChangeSceneToPacked(LevelScenes[idx]);
@@ -220,11 +76,18 @@ public partial class GameManager : Node
} }
} }
public void RestartGame()
{
Store?.ResetAll();
GetTree().ChangeSceneToPacked(LevelScenes[0]);
SaveSystem.Instance.SaveGame();
}
public void OnLevelComplete() public void OnLevelComplete()
{ {
if (Store == null) return; if (Store == null) return;
var levelIndex = Store.Session.CurrentLevel; var levelIndex = Store.Player.CurrentLevel;
Store.MarkLevelComplete(levelIndex); Store.MarkLevelComplete(levelIndex);
Store.CommitSessionCoins(); Store.CommitSessionCoins();
Store.CommitSessionSkills(); Store.CommitSessionSkills();
@@ -237,6 +100,33 @@ public partial class GameManager : Node
SaveSystem.Instance.SaveGame(); SaveSystem.Instance.SaveGame();
} }
public void TryToGoToNextLevel()
{
if (Store == null) return;
var next = Store.Player.CurrentLevel + 1;
if (next < LevelScenes.Count && Store.IsLevelUnlocked(next))
{
Store.Player.CurrentLevel = next;
GetTree().ChangeSceneToPacked(LevelScenes[next]);
EventBus.EmitLevelStarted(next, GetTree().CurrentScene);
}
}
public void PauseGame()
{
Engine.TimeScale = 0;
EventBus.EmitGamePaused();
}
public void ResumeGame()
{
Engine.TimeScale = 1;
EventBus.EmitGameResumed();
}
public void QuitGame() => GetTree().Quit();
#endregion #endregion
#region Player Lookup #region Player Lookup

View File

@@ -143,7 +143,7 @@ public partial class GameStateStore : Node
} }
/// <summary> /// <summary>
/// Unlocks a skill in the session. /// Unlocks a skill in the session (lost on death, committed on level complete).
/// </summary> /// </summary>
public void UnlockSkillInSession(SkillData skill) public void UnlockSkillInSession(SkillData skill)
{ {
@@ -151,6 +151,30 @@ public partial class GameStateStore : Node
Session.SkillsUnlocked.Add(skill); Session.SkillsUnlocked.Add(skill);
} }
/// <summary>
/// Permanently unlocks a skill directly in player state (bypasses session, e.g. console commands).
/// </summary>
public void UnlockSkillPermanently(SkillData skill)
{
if (!Player.UnlockedSkills.Contains(skill))
Player.UnlockedSkills.Add(skill);
}
/// <summary>
/// Removes a permanently unlocked skill from player state by name.
/// </summary>
public void RemoveUnlockedSkill(string skillName)
{
for (int i = 0; i < Player.UnlockedSkills.Count; i++)
{
if (Player.UnlockedSkills[i].Name == skillName)
{
Player.UnlockedSkills.RemoveAt(i);
return;
}
}
}
/// <summary> /// <summary>
/// Commits session skills to player state. /// Commits session skills to player state.
/// </summary> /// </summary>
@@ -179,6 +203,26 @@ public partial class GameStateStore : Node
#endregion #endregion
#region Statistics Operations
public void IncrementStat(string name, int amount = 1)
{
if (Player.Statistics.TryGetValue(name, out var current))
Player.Statistics[name] = current + amount;
else
Player.Statistics[name] = amount;
}
public void SetStat(string name, int value) => Player.Statistics[name] = value;
public int GetStat(string name) =>
Player.Statistics.TryGetValue(name, out var value) ? value : 0;
public System.Collections.Generic.Dictionary<string, int> GetAllStats() =>
new(Player.Statistics);
#endregion
#region Reset Operations #region Reset Operations
/// <summary> /// <summary>

View File

@@ -1,7 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json;
using Godot; using Godot;
using Godot.Collections;
using Mr.BrickAdventures.scripts; using Mr.BrickAdventures.scripts;
using Mr.BrickAdventures.scripts.State;
namespace Mr.BrickAdventures.Autoloads; namespace Mr.BrickAdventures.Autoloads;
@@ -53,12 +54,11 @@ public partial class GhostManager : Node
{ {
if (!IsRecording) return; if (!IsRecording) return;
var frame = new GhostFrame _currentRecording.Add(new GhostFrame
{ {
Timestamp = (Time.GetTicksMsec() / 1000.0) - _startTime, Timestamp = (Time.GetTicksMsec() / 1000.0) - _startTime,
Position = position Position = position
}; });
_currentRecording.Add(frame);
} }
public void SpawnGhostPlayer(int levelIndex, Node parent) public void SpawnGhostPlayer(int levelIndex, Node parent)
@@ -77,41 +77,60 @@ public partial class GhostManager : Node
private void SaveGhostData(int levelIndex, double time) private void SaveGhostData(int levelIndex, double time)
{ {
var path = $"user://ghost_level_{levelIndex}.dat"; var path = $"user://ghost_level_{levelIndex}.json";
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write); var saveData = new GhostSaveData { BestTime = time };
foreach (var frame in _currentRecording)
saveData.Frames.Add(new GhostFrameDto { Timestamp = frame.Timestamp, X = frame.Position.X, Y = frame.Position.Y });
var dataToSave = new Godot.Collections.Dictionary try
{ {
{ "time", time }, var json = JsonSerializer.Serialize(saveData, SaveSystem.JsonOptions);
{ "frames", _currentRecording.ToArray() } using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
}; file.StoreString(json);
file.StoreVar(dataToSave); }
catch (System.Exception e)
{
GD.PrintErr($"GhostManager: Failed to save ghost data: {e.Message}");
}
} }
private List<GhostFrame> LoadGhostData(int levelIndex) private List<GhostFrame> LoadGhostData(int levelIndex)
{ {
var path = $"user://ghost_level_{levelIndex}.dat"; var path = $"user://ghost_level_{levelIndex}.json";
if (!FileAccess.FileExists(path)) return []; if (!FileAccess.FileExists(path)) return [];
try
{
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
var savedData = (Dictionary)file.GetVar(); var saveData = JsonSerializer.Deserialize<GhostSaveData>(file.GetAsText(), SaveSystem.JsonOptions);
var framesArray = (Array)savedData["frames"]; if (saveData == null) return [];
var frames = new List<GhostFrame>(); var frames = new List<GhostFrame>();
foreach (var obj in framesArray) foreach (var dto in saveData.Frames)
{ frames.Add(new GhostFrame { Timestamp = dto.Timestamp, Position = new Vector2(dto.X, dto.Y) });
frames.Add((GhostFrame)obj);
}
return frames; return frames;
} }
catch (System.Exception e)
{
GD.PrintErr($"GhostManager: Failed to load ghost data: {e.Message}");
return [];
}
}
private double LoadBestTime(int levelIndex) private double LoadBestTime(int levelIndex)
{ {
var path = $"user://ghost_level_{levelIndex}.dat"; var path = $"user://ghost_level_{levelIndex}.json";
if (!FileAccess.FileExists(path)) return double.MaxValue; if (!FileAccess.FileExists(path)) return double.MaxValue;
try
{
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
var data = (Dictionary)file.GetVar(); var saveData = JsonSerializer.Deserialize<GhostSaveData>(file.GetAsText(), SaveSystem.JsonOptions);
return (double)data["time"]; return saveData?.BestTime ?? double.MaxValue;
}
catch
{
return double.MaxValue;
}
} }
} }

View File

@@ -16,7 +16,7 @@ public partial class SaveSystem : Node
public static SaveSystem Instance { get; private set; } public static SaveSystem Instance { get; private set; }
private static readonly JsonSerializerOptions JsonOptions = new() internal static readonly JsonSerializerOptions JsonOptions = new()
{ {
WriteIndented = true, WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase PropertyNamingPolicy = JsonNamingPolicy.CamelCase
@@ -47,7 +47,7 @@ public partial class SaveSystem : Node
Version = Version, Version = Version,
Coins = store.Player.Coins, Coins = store.Player.Coins,
Lives = store.Player.Lives, Lives = store.Player.Lives,
CurrentLevel = store.Session.CurrentLevel, CurrentLevel = store.Player.CurrentLevel,
CompletedLevels = [.. store.Player.CompletedLevels], CompletedLevels = [.. store.Player.CompletedLevels],
UnlockedLevels = new List<int>(store.Player.UnlockedLevels), UnlockedLevels = new List<int>(store.Player.UnlockedLevels),
UnlockedSkillNames = GetSkillNames(store.Player.UnlockedSkills), UnlockedSkillNames = GetSkillNames(store.Player.UnlockedSkills),
@@ -105,7 +105,7 @@ public partial class SaveSystem : Node
// Apply loaded state // Apply loaded state
store.Player.Coins = saveData.Coins; store.Player.Coins = saveData.Coins;
store.Player.Lives = saveData.Lives; store.Player.Lives = saveData.Lives;
store.Session.CurrentLevel = saveData.CurrentLevel; store.Player.CurrentLevel = saveData.CurrentLevel;
store.Player.CompletedLevels = saveData.CompletedLevels ?? new List<int>(); store.Player.CompletedLevels = saveData.CompletedLevels ?? new List<int>();
store.Player.UnlockedLevels = saveData.UnlockedLevels ?? new List<int> { 0 }; store.Player.UnlockedLevels = saveData.UnlockedLevels ?? new List<int> { 0 };

View File

@@ -11,7 +11,7 @@ namespace Mr.BrickAdventures.Autoloads;
public partial class SkillManager : Node public partial class SkillManager : Node
{ {
private PlayerController _player; private PlayerController _player;
private GameManager GameManager => GameManager.Instance; private GameStateStore Store => GameStateStore.Instance;
[Export] public Array<SkillData> AvailableSkills { get; set; } = []; [Export] public Array<SkillData> AvailableSkills { get; set; } = [];
@@ -154,20 +154,16 @@ public partial class SkillManager : Node
public void ApplyUnlockedSkills() public void ApplyUnlockedSkills()
{ {
if (_player == null || !IsInstanceValid(_player)) return; if (_player == null || !IsInstanceValid(_player)) return;
if (GameManager == null) return; if (Store == null) return;
foreach (var sd in AvailableSkills) foreach (var sd in AvailableSkills)
{ {
if (GameManager.IsSkillUnlocked(sd)) if (Store.IsSkillUnlocked(sd))
{
CallDeferred(MethodName.AddSkill, sd); CallDeferred(MethodName.AddSkill, sd);
}
else else
{
RemoveSkill(sd.Name); RemoveSkill(sd.Name);
} }
} }
}
public SkillData GetSkillByName(string skillName) public SkillData GetSkillByName(string skillName)
{ {

View File

@@ -1,66 +0,0 @@
using System.Collections.Generic;
using Godot;
using Mr.BrickAdventures.scripts.State;
namespace Mr.BrickAdventures.Autoloads;
/// <summary>
/// Manages game statistics using GameStateStore.
/// </summary>
public partial class StatisticsManager : Node
{
public static StatisticsManager Instance { get; private set; }
public override void _Ready() => Instance = this;
public override void _ExitTree() { if (Instance == this) Instance = null; }
/// <summary>
/// Gets the statistics dictionary from the store.
/// </summary>
private Dictionary<string, int> GetStats()
{
return GameStateStore.Instance?.Player.Statistics ?? new Dictionary<string, int>();
}
/// <summary>
/// Increases a numerical statistic by a given amount.
/// </summary>
public void IncrementStat(string statName, int amount = 1)
{
var stats = GetStats();
if (stats.TryGetValue(statName, out var currentValue))
{
stats[statName] = currentValue + amount;
}
else
{
stats[statName] = amount;
}
}
/// <summary>
/// Sets a statistic to a specific value.
/// </summary>
public void SetStat(string statName, int value)
{
var stats = GetStats();
stats[statName] = value;
}
/// <summary>
/// Gets the value of a statistic.
/// </summary>
public int GetStat(string statName)
{
var stats = GetStats();
return stats.TryGetValue(statName, out var value) ? value : 0;
}
/// <summary>
/// Gets a copy of all statistics.
/// </summary>
public Dictionary<string, int> GetAllStats()
{
return new Dictionary<string, int>(GetStats());
}
}

View File

@@ -1 +0,0 @@
uid://c5p3l2mhkw0p4

View File

@@ -46,12 +46,10 @@ SteamManager="*res://Autoloads/SteamManager.cs"
AchievementManager="*res://objects/achievement_manager.tscn" AchievementManager="*res://objects/achievement_manager.tscn"
SkillManager="*res://objects/skill_manager.tscn" SkillManager="*res://objects/skill_manager.tscn"
FloatingTextManager="*res://objects/floating_text_manager.tscn" FloatingTextManager="*res://objects/floating_text_manager.tscn"
StatisticsManager="*res://Autoloads/StatisticsManager.cs"
SpeedRunManager="res://Autoloads/SpeedRunManager.cs" SpeedRunManager="res://Autoloads/SpeedRunManager.cs"
GhostManager="res://objects/ghost_manager.tscn" GhostManager="res://objects/ghost_manager.tscn"
StatisticsEventHandler="*res://scripts/Events/StatisticsEventHandler.cs" StatisticsEventHandler="*res://scripts/Events/StatisticsEventHandler.cs"
CoinStateHandler="*res://scripts/Events/CoinStateHandler.cs" CoinStateHandler="*res://scripts/Events/CoinStateHandler.cs"
LevelStateHandler="*res://scripts/Events/LevelStateHandler.cs"
LivesStateHandler="*res://scripts/Events/LivesStateHandler.cs" LivesStateHandler="*res://scripts/Events/LivesStateHandler.cs"
SkillCollectHandler="*res://scripts/Events/SkillCollectHandler.cs" SkillCollectHandler="*res://scripts/Events/SkillCollectHandler.cs"
GameStateStore="*res://Autoloads/GameStateStore.cs" GameStateStore="*res://Autoloads/GameStateStore.cs"

View File

@@ -14,10 +14,13 @@ public partial class GhostEventHandler : Node
} }
public override void _ExitTree() public override void _ExitTree()
{
if (EventBus.Instance != null)
{ {
EventBus.Instance.LevelStarted -= OnLevelStarted; EventBus.Instance.LevelStarted -= OnLevelStarted;
EventBus.Instance.LevelCompleted -= OnLevelCompleted; EventBus.Instance.LevelCompleted -= OnLevelCompleted;
} }
}
private void OnLevelStarted(int levelIndex, Node currentScene) private void OnLevelStarted(int levelIndex, Node currentScene)
{ {

View File

@@ -1,29 +0,0 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
namespace Mr.BrickAdventures.scripts.Events;
/// <summary>
/// Handles level completion events and updates GameStateStore.
/// </summary>
public partial class LevelStateHandler : Node
{
public override void _Ready()
{
EventBus.Instance.LevelCompleted += OnLevelCompleted;
}
public override void _ExitTree()
{
if (EventBus.Instance != null)
{
EventBus.Instance.LevelCompleted -= OnLevelCompleted;
}
}
private void OnLevelCompleted(int levelIndex, Node currentScene, double completionTime)
{
// State mutations (commit coins/skills, reset session) are handled by GameManager.OnLevelComplete
// before this event fires. This handler is reserved for future level-specific side-effects.
}
}

View File

@@ -1 +0,0 @@
uid://gx5vn7viphv

View File

@@ -33,7 +33,6 @@ public partial class SkillCollectHandler : Node
GameStateStore.Instance?.UnlockSkillInSession(skill); GameStateStore.Instance?.UnlockSkillInSession(skill);
// Immediately activate the skill for the player // Immediately activate the skill for the player
skill.Level = 1;
SkillManager?.AddSkill(skill); SkillManager?.AddSkill(skill);
// Emit skill unlocked event for UI/achievements // Emit skill unlocked event for UI/achievements

View File

@@ -13,6 +13,7 @@ public partial class SpeedRunEventHandler : Node
public override void _ExitTree() public override void _ExitTree()
{ {
if (EventBus.Instance != null)
EventBus.Instance.LevelCompleted -= OnLevelCompleted; EventBus.Instance.LevelCompleted -= OnLevelCompleted;
} }

View File

@@ -1,19 +1,16 @@
using Godot; using Godot;
using Mr.BrickAdventures.Autoloads; using Mr.BrickAdventures.Autoloads;
using Mr.BrickAdventures.scripts.State;
namespace Mr.BrickAdventures.scripts.Events; namespace Mr.BrickAdventures.scripts.Events;
/// <summary> /// <summary>
/// Handles game events and updates statistics accordingly. /// Handles game events and updates statistics accordingly.
/// Listens to EventBus signals and increments relevant stats.
/// </summary> /// </summary>
public partial class StatisticsEventHandler : Node public partial class StatisticsEventHandler : Node
{ {
private StatisticsManager StatisticsManager => StatisticsManager.Instance;
public override void _Ready() public override void _Ready()
{ {
// Subscribe to events
EventBus.Instance.CoinCollected += OnCoinCollected; EventBus.Instance.CoinCollected += OnCoinCollected;
EventBus.Instance.EnemyDefeated += OnEnemyDefeated; EventBus.Instance.EnemyDefeated += OnEnemyDefeated;
EventBus.Instance.PlayerDied += OnPlayerDied; EventBus.Instance.PlayerDied += OnPlayerDied;
@@ -33,27 +30,17 @@ public partial class StatisticsEventHandler : Node
} }
private void OnCoinCollected(int amount, Vector2 position) private void OnCoinCollected(int amount, Vector2 position)
{ => GameStateStore.Instance?.IncrementStat(StatNames.CoinsCollected, amount);
StatisticsManager.IncrementStat("coins_collected", amount);
}
private void OnEnemyDefeated(Node enemy, Vector2 position) private void OnEnemyDefeated(Node enemy, Vector2 position)
{ => GameStateStore.Instance?.IncrementStat(StatNames.EnemiesDefeated);
StatisticsManager.IncrementStat("enemies_defeated");
}
private void OnPlayerDied(Vector2 position) private void OnPlayerDied(Vector2 position)
{ => GameStateStore.Instance?.IncrementStat(StatNames.Deaths);
StatisticsManager.IncrementStat("deaths");
}
private void OnLevelCompleted(int levelIndex, Node currentScene, double completionTime) private void OnLevelCompleted(int levelIndex, Node currentScene, double completionTime)
{ => GameStateStore.Instance?.IncrementStat(StatNames.LevelsCompleted);
StatisticsManager.IncrementStat("levels_completed");
}
private void OnChildRescued(Vector2 position) private void OnChildRescued(Vector2 position)
{ => GameStateStore.Instance?.IncrementStat(StatNames.ChildrenRescued);
StatisticsManager.IncrementStat("children_rescued");
}
} }

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace Mr.BrickAdventures.scripts.State;
public class GhostSaveData
{
public double BestTime { get; set; }
public List<GhostFrameDto> Frames { get; set; } = new();
}
public class GhostFrameDto
{
public double Timestamp { get; set; }
public float X { get; set; }
public float Y { get; set; }
}

View File

@@ -0,0 +1 @@
uid://drp68lkok8if3

View File

@@ -11,6 +11,11 @@ public class PlayerState
{ {
private const int DefaultLives = 3; private const int DefaultLives = 3;
/// <summary>
/// The level index the player is currently on. Persisted across sessions.
/// </summary>
public int CurrentLevel { get; set; }
/// <summary> /// <summary>
/// Saved coins (not including current session). /// Saved coins (not including current session).
/// </summary> /// </summary>
@@ -56,6 +61,7 @@ public class PlayerState
/// </summary> /// </summary>
public void Reset() public void Reset()
{ {
CurrentLevel = 0;
Coins = 0; Coins = 0;
Lives = DefaultLives; Lives = DefaultLives;
CompletedLevels.Clear(); CompletedLevels.Clear();

View File

@@ -9,11 +9,6 @@ namespace Mr.BrickAdventures.scripts.State;
/// </summary> /// </summary>
public class SessionState public class SessionState
{ {
/// <summary>
/// Current level index being played.
/// </summary>
public int CurrentLevel { get; set; }
/// <summary> /// <summary>
/// Coins collected during this session (not yet saved). /// Coins collected during this session (not yet saved).
/// </summary> /// </summary>
@@ -29,7 +24,6 @@ public class SessionState
/// </summary> /// </summary>
public static SessionState CreateDefault() => new() public static SessionState CreateDefault() => new()
{ {
CurrentLevel = 0,
CoinsCollected = 0, CoinsCollected = 0,
SkillsUnlocked = new List<SkillData>() SkillsUnlocked = new List<SkillData>()
}; };
@@ -44,11 +38,10 @@ public class SessionState
} }
/// <summary> /// <summary>
/// Resets completely including level. /// Resets all session state.
/// </summary> /// </summary>
public void ResetAll() public void ResetAll()
{ {
CurrentLevel = 0;
CoinsCollected = 0; CoinsCollected = 0;
SkillsUnlocked.Clear(); SkillsUnlocked.Clear();
} }

View File

@@ -0,0 +1,10 @@
namespace Mr.BrickAdventures.scripts.State;
public static class StatNames
{
public const string CoinsCollected = "coins_collected";
public const string EnemiesDefeated = "enemies_defeated";
public const string Deaths = "deaths";
public const string LevelsCompleted = "levels_completed";
public const string ChildrenRescued = "children_rescued";
}

View File

@@ -0,0 +1 @@
uid://dtqjh4v5w41ni

View File

@@ -11,29 +11,26 @@ public partial class GameOverScreen : Control
[Export] public Button MainMenuButton { get; set; } [Export] public Button MainMenuButton { get; set; }
[Export] public PackedScene MainMenuScene { get; set; } [Export] public PackedScene MainMenuScene { get; set; }
private GameManager _gameManager;
public override void _Ready() public override void _Ready()
{ {
_gameManager = GameManager.Instance;
RestartButton.Pressed += OnRestartClicked; RestartButton.Pressed += OnRestartClicked;
MainMenuButton.Pressed += OnMainMenuClicked; MainMenuButton.Pressed += OnMainMenuClicked;
} }
private void OnMainMenuClicked() private void OnMainMenuClicked()
{ {
_gameManager.ResetPlayerState(); GameStateStore.Instance?.ResetAll();
GetTree().ChangeSceneToPacked(MainMenuScene); GetTree().ChangeSceneToPacked(MainMenuScene);
} }
private void OnRestartClicked() private void OnRestartClicked()
{ {
_gameManager.RestartGame(); GameManager.Instance?.RestartGame();
} }
public void OnPlayerDeath() public void OnPlayerDeath()
{ {
if (_gameManager == null || _gameManager.GetLives() != 0) return; if (GameStateStore.Instance?.Player.Lives != 0) return;
GameOverPanel.Show(); GameOverPanel.Show();
} }

View File

@@ -12,12 +12,7 @@ public partial class Hud : Control
[Export] public ProgressBar HealthBar { get; set; } [Export] public ProgressBar HealthBar { get; set; }
[Export] public Label LivesLabel { get; set; } [Export] public Label LivesLabel { get; set; }
private GameManager _gameManager; private GameStateStore Store => GameStateStore.Instance;
public override void _Ready()
{
_gameManager = GameManager.Instance;
}
public override void _Process(double delta) public override void _Process(double delta)
{ {
@@ -28,12 +23,12 @@ public partial class Hud : Control
private void SetCoinsLabel() private void SetCoinsLabel()
{ {
CoinsLabel.Text = Tr("COINS_LABEL") + ": " + _gameManager.GetCoins(); CoinsLabel.Text = Tr("COINS_LABEL") + ": " + (Store?.GetTotalCoins() ?? 0);
} }
private void SetLivesLabel() private void SetLivesLabel()
{ {
LivesLabel.Text = Tr("LIVES_LABEL") + ": " + _gameManager.GetLives(); LivesLabel.Text = Tr("LIVES_LABEL") + ": " + (Store?.Player.Lives ?? 0);
} }
private void SetHealthBar() private void SetHealthBar()

View File

@@ -18,7 +18,7 @@ public partial class Marketplace : Control
[Export] public PackedScene MarketplaceButtonScene { get; set; } [Export] public PackedScene MarketplaceButtonScene { get; set; }
[Export] public PackedScene SkillButtonScene { get; set; } [Export] public PackedScene SkillButtonScene { get; set; }
private GameManager GameManager => GameManager.Instance; private GameStateStore Store => GameStateStore.Instance;
private SkillManager SkillManager => SkillManager.Instance; private SkillManager SkillManager => SkillManager.Instance;
private readonly List<Button> _unlockButtons = []; private readonly List<Button> _unlockButtons = [];
private readonly List<SkillButton> _skillButtons = []; private readonly List<SkillButton> _skillButtons = [];
@@ -33,7 +33,7 @@ public partial class Marketplace : Control
foreach (var skill in skillsToUnlock) CreateUpgradeButton(skill); foreach (var skill in skillsToUnlock) CreateUpgradeButton(skill);
var unlockedSkills = GameManager.GetUnlockedSkills(); var unlockedSkills = Store.GetAllUnlockedSkills();
foreach (var skill in unlockedSkills) CreateSkillButton(skill); foreach (var skill in unlockedSkills) CreateSkillButton(skill);
SkillUnlockerComponent.SkillUnlocked += OnSkillUnlocked; SkillUnlockerComponent.SkillUnlocked += OnSkillUnlocked;
@@ -113,7 +113,7 @@ public partial class Marketplace : Control
private void OnUpgradeButtonPressed(SkillData skill) private void OnUpgradeButtonPressed(SkillData skill)
{ {
if (GameManager.IsSkillUnlocked(skill)) if (Store.IsSkillUnlocked(skill))
{ {
if (skill.Level < skill.MaxLevel) if (skill.Level < skill.MaxLevel)
{ {

View File

@@ -13,14 +13,12 @@ public partial class MarketplaceButton : Button
[Export] public Texture2D LockedSkillIcon { get; set; } [Export] public Texture2D LockedSkillIcon { get; set; }
[Export] public Container SkillLevelContainer { get; set; } [Export] public Container SkillLevelContainer { get; set; }
private GameManager _gameManager;
private SkillUnlockerComponent _skillUnlockerComponent; private SkillUnlockerComponent _skillUnlockerComponent;
private SkillManager _skillManager; private SkillManager _skillManager;
public override void _Ready() public override void _Ready()
{ {
_gameManager = GameManager.Instance; var player = GameManager.Instance?.Player;
var player = _gameManager.Player;
if (player == null) return; if (player == null) return;
_skillUnlockerComponent = player.GetNodeOrNull<SkillUnlockerComponent>("SkillUnlockerComponent"); _skillUnlockerComponent = player.GetNodeOrNull<SkillUnlockerComponent>("SkillUnlockerComponent");
@@ -59,7 +57,7 @@ public partial class MarketplaceButton : Button
return; return;
} }
var isUnlocked = _gameManager.IsSkillUnlocked(Data); var isUnlocked = GameStateStore.Instance?.IsSkillUnlocked(Data) ?? false;
for (var i = 0; i < SkillLevelContainer.GetChildCount(); i++) for (var i = 0; i < SkillLevelContainer.GetChildCount(); i++)
{ {

View File

@@ -53,7 +53,7 @@ public partial class PauseMenu : Control
private void OnMainMenuPressed() private void OnMainMenuPressed()
{ {
GameManager.ResumeGame(); GameManager.ResumeGame();
GameManager.ResetCurrentSessionState(); GameStateStore.Instance?.ResetSession();
GetTree().ChangeSceneToPacked(MainMenuScene); GetTree().ChangeSceneToPacked(MainMenuScene);
} }

View File

@@ -12,13 +12,11 @@ public partial class BrickShieldSkillComponent : SkillComponentBase
[Export] public PackedScene ShieldScene { get; set; } [Export] public PackedScene ShieldScene { get; set; }
private Node2D _shieldInstance; private Node2D _shieldInstance;
private GameManager _gameManager;
private SkillManager _skillManager; private SkillManager _skillManager;
private HealthComponent _shieldHealth; private HealthComponent _shieldHealth;
public override void _Ready() public override void _Ready()
{ {
_gameManager = GameManager.Instance;
_skillManager = SkillManager.Instance; _skillManager = SkillManager.Instance;
} }
@@ -55,9 +53,9 @@ public partial class BrickShieldSkillComponent : SkillComponentBase
private void OnShieldDestroyed() private void OnShieldDestroyed()
{ {
if (_gameManager != null && Data != null && _skillManager != null) if (Data != null && _skillManager != null)
{ {
_gameManager.RemoveSkill(Data.Name); GameStateStore.Instance?.RemoveUnlockedSkill(Data.Name);
_skillManager.RemoveSkill(Data.Name); _skillManager.RemoveSkill(Data.Name);
} }
_shieldInstance = null; _shieldInstance = null;

View File

@@ -1,7 +1,6 @@
using Godot; using Godot;
using Mr.BrickAdventures.Autoloads; using Mr.BrickAdventures.Autoloads;
using Mr.BrickAdventures.scripts.interfaces; using Mr.BrickAdventures.scripts.interfaces;
using Mr.BrickAdventures.scripts.State;
namespace Mr.BrickAdventures.scripts.components; namespace Mr.BrickAdventures.scripts.components;
@@ -28,8 +27,6 @@ public partial class ExitDoorComponent : Area2D, IUnlockable
EmitSignalExitTriggered(); EmitSignalExitTriggered();
AchievementManager.Instance?.UnlockAchievement(AchievementId); AchievementManager.Instance?.UnlockAchievement(AchievementId);
var currentLevel = GameStateStore.Instance?.Session.CurrentLevel ?? 0;
GameManager.Instance?.UnlockLevel(currentLevel + 1);
CallDeferred(nameof(GoToNextLevel)); CallDeferred(nameof(GoToNextLevel));
} }

View File

@@ -1,10 +1,8 @@
using Godot; using Godot;
using Godot.Collections; using Godot.Collections;
using Mr.BrickAdventures;
using Mr.BrickAdventures.Autoloads; using Mr.BrickAdventures.Autoloads;
using Mr.BrickAdventures.scripts.interfaces; using Mr.BrickAdventures.scripts.interfaces;
using Mr.BrickAdventures.scripts.Resources; using Mr.BrickAdventures.scripts.Resources;
using Mr.BrickAdventures.scripts.State;
namespace Mr.BrickAdventures.scripts.components; namespace Mr.BrickAdventures.scripts.components;
@@ -16,30 +14,24 @@ public partial class SkillUnlockerComponent : Node
[Signal] [Signal]
public delegate void SkillUnlockedEventHandler(SkillData skill); public delegate void SkillUnlockedEventHandler(SkillData skill);
private GameManager _gameManager; private GameStateStore Store => GameStateStore.Instance;
public override void _Ready() public override void _Ready()
{ {
_gameManager = GameManager.Instance;
SkillManager = SkillManager.Instance; SkillManager = SkillManager.Instance;
} }
private bool HasEnoughCoins(int amount) private bool HasEnoughCoins(int amount) => (Store?.GetTotalCoins() ?? 0) >= amount;
{
return _gameManager != null && _gameManager.GetCoins() >= amount;
}
public bool TryUnlockSkill(SkillData skill) public bool TryUnlockSkill(SkillData skill)
{ {
if (_gameManager == null) return false; if (Store == null) return false;
if (_gameManager.IsSkillUnlocked(skill)) return false; if (Store.IsSkillUnlocked(skill)) return false;
if (!HasEnoughCoins(skill.Upgrades[0].Cost)) return false; if (!HasEnoughCoins(skill.Upgrades[0].Cost)) return false;
skill.Level = 1; skill.Level = 1;
_gameManager.RemoveCoins(skill.Upgrades[0].Cost); Store.RemoveCoins(skill.Upgrades[0].Cost);
Store.UnlockSkillInSession(skill);
// Add to session state via GameStateStore
GameStateStore.Instance?.UnlockSkillInSession(skill);
SkillManager.AddSkill(skill); SkillManager.AddSkill(skill);
EmitSignalSkillUnlocked(skill); EmitSignalSkillUnlocked(skill);
@@ -52,30 +44,28 @@ public partial class SkillUnlockerComponent : Node
foreach (var skill in availableSkills) foreach (var skill in availableSkills)
{ {
Store?.UnlockSkillPermanently(skill);
EmitSignalSkillUnlocked(skill); EmitSignalSkillUnlocked(skill);
} }
_gameManager.UnlockSkills(availableSkills);
SkillManager.ApplyUnlockedSkills(); SkillManager.ApplyUnlockedSkills();
} }
public bool TryUpgradeSkill(SkillData skill) public bool TryUpgradeSkill(SkillData skill)
{ {
if (_gameManager == null) return false; if (Store == null) return false;
if (!_gameManager.IsSkillUnlocked(skill)) return false; if (!Store.IsSkillUnlocked(skill)) return false;
if (skill.Level >= skill.MaxLevel) return false; if (skill.Level >= skill.MaxLevel) return false;
if (!HasEnoughCoins(skill.Upgrades[skill.Level].Cost)) return false; if (!HasEnoughCoins(skill.Upgrades[skill.Level].Cost)) return false;
_gameManager.RemoveCoins(skill.Upgrades[skill.Level].Cost); Store.RemoveCoins(skill.Upgrades[skill.Level].Cost);
skill.Level++; skill.Level++;
if (SkillManager.ActiveComponents.TryGetValue(skill.Name, out Variant componentVariant)) if (SkillManager.ActiveComponents.TryGetValue(skill.Name, out Variant componentVariant))
{ {
var component = componentVariant.AsGodotObject(); var component = componentVariant.AsGodotObject();
if (component is ISkill skillInstance) if (component is ISkill skillInstance)
{
skillInstance.ApplyUpgrade(skill.Upgrades[skill.Level - 1]); skillInstance.ApplyUpgrade(skill.Upgrades[skill.Level - 1]);
} }
}
EmitSignalSkillUnlocked(skill); EmitSignalSkillUnlocked(skill);
return true; return true;
} }