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
unlockedIds.Add(achievementId);
GD.Print($"Achievement Unlocked: {achievement.DisplayName}");
EventBus.EmitAchievementUnlocked(achievementId);
// 2. Show the UI popup
if (AchievementPopupScene != null)

View File

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

View File

@@ -1,14 +1,12 @@
using Godot;
using Godot.Collections;
using Mr.BrickAdventures.scripts.components;
using Mr.BrickAdventures.scripts.Resources;
using Mr.BrickAdventures.scripts.State;
namespace Mr.BrickAdventures.Autoloads;
/// <summary>
/// Game orchestrator - handles scene management and game flow.
/// State is delegated to GameStateStore for better separation of concerns.
/// Game orchestrator - handles scene transitions and game flow.
/// State lives in GameStateStore; query it directly for reads/writes.
/// </summary>
public partial class GameManager : Node
{
@@ -45,150 +43,8 @@ public partial class GameManager : Node
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
public void RestartGame()
{
Store?.ResetAll();
GetTree().ChangeSceneToPacked(LevelScenes[0]);
SaveSystem.Instance.SaveGame();
}
public void QuitGame() => GetTree().Quit();
public void PauseGame()
{
Engine.TimeScale = 0;
EventBus.EmitGamePaused();
}
public void ResumeGame()
{
Engine.TimeScale = 1;
EventBus.EmitGameResumed();
}
public void StartNewGame()
{
Store?.ResetAll();
@@ -208,7 +64,7 @@ public partial class GameManager : Node
return;
}
var idx = Store?.Session.CurrentLevel ?? 0;
var idx = Store?.Player.CurrentLevel ?? 0;
if (idx < LevelScenes.Count)
{
GetTree().ChangeSceneToPacked(LevelScenes[idx]);
@@ -220,11 +76,18 @@ public partial class GameManager : Node
}
}
public void RestartGame()
{
Store?.ResetAll();
GetTree().ChangeSceneToPacked(LevelScenes[0]);
SaveSystem.Instance.SaveGame();
}
public void OnLevelComplete()
{
if (Store == null) return;
var levelIndex = Store.Session.CurrentLevel;
var levelIndex = Store.Player.CurrentLevel;
Store.MarkLevelComplete(levelIndex);
Store.CommitSessionCoins();
Store.CommitSessionSkills();
@@ -237,6 +100,33 @@ public partial class GameManager : Node
SaveSystem.Instance.SaveGame();
}
public void TryToGoToNextLevel()
{
if (Store == null) return;
var next = Store.Player.CurrentLevel + 1;
if (next < LevelScenes.Count && Store.IsLevelUnlocked(next))
{
Store.Player.CurrentLevel = next;
GetTree().ChangeSceneToPacked(LevelScenes[next]);
EventBus.EmitLevelStarted(next, GetTree().CurrentScene);
}
}
public void PauseGame()
{
Engine.TimeScale = 0;
EventBus.EmitGamePaused();
}
public void ResumeGame()
{
Engine.TimeScale = 1;
EventBus.EmitGameResumed();
}
public void QuitGame() => GetTree().Quit();
#endregion
#region Player Lookup
@@ -249,4 +139,4 @@ public partial class GameManager : Node
}
#endregion
}
}

View File

@@ -143,7 +143,7 @@ public partial class GameStateStore : Node
}
/// <summary>
/// Unlocks a skill in the session.
/// Unlocks a skill in the session (lost on death, committed on level complete).
/// </summary>
public void UnlockSkillInSession(SkillData skill)
{
@@ -151,6 +151,30 @@ public partial class GameStateStore : Node
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>
/// Commits session skills to player state.
/// </summary>
@@ -179,6 +203,26 @@ public partial class GameStateStore : Node
#endregion
#region Statistics Operations
public void IncrementStat(string name, int amount = 1)
{
if (Player.Statistics.TryGetValue(name, out var current))
Player.Statistics[name] = current + amount;
else
Player.Statistics[name] = amount;
}
public void SetStat(string name, int value) => Player.Statistics[name] = value;
public int GetStat(string name) =>
Player.Statistics.TryGetValue(name, out var value) ? value : 0;
public System.Collections.Generic.Dictionary<string, int> GetAllStats() =>
new(Player.Statistics);
#endregion
#region Reset Operations
/// <summary>

View File

@@ -1,7 +1,8 @@
using System.Collections.Generic;
using System.Text.Json;
using Godot;
using Godot.Collections;
using Mr.BrickAdventures.scripts;
using Mr.BrickAdventures.scripts.State;
namespace Mr.BrickAdventures.Autoloads;
@@ -24,14 +25,14 @@ public partial class GhostManager : Node
public void StartRecording(int levelIndex)
{
if (!IsPlaybackEnabled) return;
_currentLevelIndex = levelIndex;
_currentRecording.Clear();
_startTime = Time.GetTicksMsec() / 1000.0;
IsRecording = true;
GD.Print("Ghost recording started.");
}
public void StopRecording(bool levelCompleted, double finalTime)
{
if (!IsRecording) return;
@@ -48,23 +49,22 @@ public partial class GhostManager : Node
}
_currentRecording.Clear();
}
public void RecordFrame(Vector2 position)
{
if (!IsRecording) return;
var frame = new GhostFrame
_currentRecording.Add(new GhostFrame
{
Timestamp = (Time.GetTicksMsec() / 1000.0) - _startTime,
Position = position
};
_currentRecording.Add(frame);
});
}
public void SpawnGhostPlayer(int levelIndex, Node parent)
{
if (!IsPlaybackEnabled || GhostPlayerScene == null) return;
var ghostData = LoadGhostData(levelIndex);
if (ghostData.Count > 0)
{
@@ -74,44 +74,63 @@ public partial class GhostManager : Node
GD.Print($"Ghost player spawned for level {levelIndex}.");
}
}
private void SaveGhostData(int levelIndex, double time)
{
var path = $"user://ghost_level_{levelIndex}.dat";
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
var dataToSave = new Godot.Collections.Dictionary
var path = $"user://ghost_level_{levelIndex}.json";
var saveData = new GhostSaveData { BestTime = time };
foreach (var frame in _currentRecording)
saveData.Frames.Add(new GhostFrameDto { Timestamp = frame.Timestamp, X = frame.Position.X, Y = frame.Position.Y });
try
{
{ "time", time },
{ "frames", _currentRecording.ToArray() }
};
file.StoreVar(dataToSave);
var json = JsonSerializer.Serialize(saveData, SaveSystem.JsonOptions);
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
file.StoreString(json);
}
catch (System.Exception e)
{
GD.PrintErr($"GhostManager: Failed to save ghost data: {e.Message}");
}
}
private List<GhostFrame> LoadGhostData(int levelIndex)
{
var path = $"user://ghost_level_{levelIndex}.dat";
var path = $"user://ghost_level_{levelIndex}.json";
if (!FileAccess.FileExists(path)) return [];
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
var savedData = (Dictionary)file.GetVar();
var framesArray = (Array)savedData["frames"];
var frames = new List<GhostFrame>();
foreach (var obj in framesArray)
try
{
frames.Add((GhostFrame)obj);
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
var saveData = JsonSerializer.Deserialize<GhostSaveData>(file.GetAsText(), SaveSystem.JsonOptions);
if (saveData == null) return [];
var frames = new List<GhostFrame>();
foreach (var dto in saveData.Frames)
frames.Add(new GhostFrame { Timestamp = dto.Timestamp, Position = new Vector2(dto.X, dto.Y) });
return frames;
}
catch (System.Exception e)
{
GD.PrintErr($"GhostManager: Failed to load ghost data: {e.Message}");
return [];
}
return frames;
}
private double LoadBestTime(int levelIndex)
{
var path = $"user://ghost_level_{levelIndex}.dat";
var path = $"user://ghost_level_{levelIndex}.json";
if (!FileAccess.FileExists(path)) return double.MaxValue;
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
var data = (Dictionary)file.GetVar();
return (double)data["time"];
try
{
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
var saveData = JsonSerializer.Deserialize<GhostSaveData>(file.GetAsText(), SaveSystem.JsonOptions);
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; }
private static readonly JsonSerializerOptions JsonOptions = new()
internal static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
@@ -47,7 +47,7 @@ public partial class SaveSystem : Node
Version = Version,
Coins = store.Player.Coins,
Lives = store.Player.Lives,
CurrentLevel = store.Session.CurrentLevel,
CurrentLevel = store.Player.CurrentLevel,
CompletedLevels = [.. store.Player.CompletedLevels],
UnlockedLevels = new List<int>(store.Player.UnlockedLevels),
UnlockedSkillNames = GetSkillNames(store.Player.UnlockedSkills),
@@ -105,7 +105,7 @@ public partial class SaveSystem : Node
// Apply loaded state
store.Player.Coins = saveData.Coins;
store.Player.Lives = saveData.Lives;
store.Session.CurrentLevel = saveData.CurrentLevel;
store.Player.CurrentLevel = saveData.CurrentLevel;
store.Player.CompletedLevels = saveData.CompletedLevels ?? new List<int>();
store.Player.UnlockedLevels = saveData.UnlockedLevels ?? new List<int> { 0 };

View File

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

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