Add EventBus, SpeedRunManager, and GhostManager; implement ghost recording and playback features

This commit is contained in:
2025-09-13 03:30:15 +02:00
parent 46553a351a
commit dfc9201f62
24 changed files with 526 additions and 4 deletions

9
Autoloads/EventBus.cs Normal file
View File

@@ -0,0 +1,9 @@
using Godot;
namespace Mr.BrickAdventures.Autoloads;
public partial class EventBus : Node
{
[Signal] public delegate void LevelStartedEventHandler(int levelIndex, Node currentScene);
[Signal] public delegate void LevelCompletedEventHandler(int levelIndex, Node currentScene, double completionTime);
}

View File

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

View File

@@ -1,9 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using Godot;
using Godot.Collections;
using Mr.BrickAdventures.scripts.components;
using Mr.BrickAdventures.scripts.Resources;
using Double = System.Double;
namespace Mr.BrickAdventures.Autoloads;
@@ -18,6 +18,8 @@ public partial class GameManager : Node
private List<Node> _sceneNodes = [];
private PlayerController _player;
private SpeedRunManager _speedRunManager;
private EventBus _eventBus;
[Export]
public Dictionary PlayerState { get; set; } = new()
@@ -50,6 +52,12 @@ public partial class GameManager : Node
_sceneNodes.Clear();
}
public override void _Ready()
{
_speedRunManager = GetNode<SpeedRunManager>("/root/SpeedRunManager");
_eventBus = GetNode<EventBus>("/root/EventBus");
}
private void OnNodeAdded(Node node)
{
_sceneNodes.Add(node);
@@ -133,7 +141,8 @@ public partial class GameManager : Node
{ "current_level", 0 },
{ "completed_levels", new Array<int>() },
{ "unlocked_levels", new Array<int>() {0}},
{ "unlocked_skills", new Array<SkillData>() }
{ "unlocked_skills", new Array<SkillData>() },
{ "statistics", new Godot.Collections.Dictionary<string, Variant>()}
};
}
@@ -151,6 +160,7 @@ public partial class GameManager : Node
{
PlayerState["current_level"] = next;
GetTree().ChangeSceneToPacked(LevelScenes[next]);
_eventBus.EmitSignal(EventBus.SignalName.LevelStarted, next, GetTree().CurrentScene);
}
}
@@ -187,6 +197,9 @@ public partial class GameManager : Node
{
ResetPlayerState();
ResetCurrentSessionState();
_speedRunManager?.StartTimer();
GetTree().ChangeSceneToPacked(LevelScenes[0]);
GetNode<SaveSystem>("/root/SaveSystem").SaveGame();
}
@@ -212,10 +225,14 @@ public partial class GameManager : Node
{
var levelIndex = (int)PlayerState["current_level"];
MarkLevelComplete(levelIndex);
AddCoins((int)CurrentSessionState["coins_collected"]);
foreach (var s in (Array)CurrentSessionState["skills_unlocked"])
UnlockSkill((SkillData)s);
var completionTime = _speedRunManager?.GetCurrentLevelTime() ?? 0.0;
_eventBus.EmitSignal(EventBus.SignalName.LevelCompleted, levelIndex, GetTree().CurrentScene, completionTime);
ResetCurrentSessionState();
TryToGoToNextLevel();
GetNode<SaveSystem>("/root/SaveSystem").SaveGame();

112
Autoloads/GhostManager.cs Normal file
View File

@@ -0,0 +1,112 @@
using System.Collections.Generic;
using Godot;
using Godot.Collections;
using Mr.BrickAdventures.scripts;
namespace Mr.BrickAdventures.Autoloads;
public partial class GhostManager : Node
{
[Export] private PackedScene GhostPlayerScene { get; set; }
public bool IsRecording { get; private set; } = false;
public bool IsPlaybackEnabled { get; private set; } = true;
private List<GhostFrame> _currentRecording = [];
private double _startTime = 0.0;
private int _currentLevelIndex = -1;
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;
IsRecording = false;
if (levelCompleted)
{
var bestTime = LoadBestTime(_currentLevelIndex);
if (finalTime < bestTime)
{
SaveGhostData(_currentLevelIndex, finalTime);
GD.Print($"New best ghost saved for level {_currentLevelIndex}. Time: {finalTime}");
}
}
_currentRecording.Clear();
}
public void RecordFrame(Vector2 position)
{
if (!IsRecording) return;
var frame = 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)
{
var ghostPlayer = GhostPlayerScene.Instantiate<GhostPlayer>();
parent.AddChild(ghostPlayer);
ghostPlayer.StartPlayback(ghostData);
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
{
{ "time", time },
{ "frames", _currentRecording.ToArray() }
};
file.StoreVar(dataToSave);
}
private List<GhostFrame> LoadGhostData(int levelIndex)
{
var path = $"user://ghost_level_{levelIndex}.dat";
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)
{
frames.Add((GhostFrame)obj);
}
return frames;
}
private double LoadBestTime(int levelIndex)
{
var path = $"user://ghost_level_{levelIndex}.dat";
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"];
}
}

View File

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

View File

@@ -0,0 +1,62 @@
using System.Collections.Generic;
using Godot;
namespace Mr.BrickAdventures.Autoloads;
public partial class SpeedRunManager : Node
{
public bool IsRunning { get; private set; } = false;
public bool IsVisible { get; private set; } = true;
private double _startTime;
private double _levelStartTime;
private List<double> _splits = [];
[Signal] public delegate void TimeUpdatedEventHandler(double totalTime, double levelTime);
public override void _Process(double delta)
{
if (!IsRunning || !IsVisible) return;
EmitSignalTimeUpdated(GetCurrentTotalTime(), GetCurrentLevelTime());
}
public void StartTimer()
{
_startTime = Time.GetTicksMsec() / 1000.0;
_levelStartTime = _startTime;
_splits.Clear();
IsRunning = true;
GD.Print("Speedrun timer started.");
}
public void StopTimer()
{
if (!IsRunning) return;
IsRunning = false;
var finalTime = GetCurrentTotalTime();
GD.Print($"Speedrun finished. Final time: {FormatTime(finalTime)}");
// Save personal best if applicable
}
public void Split()
{
if (!IsRunning) return;
var now = Time.GetTicksMsec() / 1000.0;
var splitTime = now - _levelStartTime;
_splits.Add(splitTime);
_levelStartTime = now;
GD.Print($"Split recorded: {FormatTime(splitTime)}");
}
public double GetCurrentTotalTime() => IsRunning ? (Time.GetTicksMsec() / 1000.0) - _startTime : 0;
public double GetCurrentLevelTime() => IsRunning ? (Time.GetTicksMsec() / 1000.0) - _levelStartTime : 0;
public static string FormatTime(double time)
{
var span = System.TimeSpan.FromSeconds(time);
return $"{(int)span.TotalMinutes:00}:{span.Seconds:00}.{span.Milliseconds:000}";
}
}

View File

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

View File

@@ -0,0 +1,78 @@
using Godot;
using Godot.Collections;
namespace Mr.BrickAdventures.Autoloads;
public partial class StatisticsManager : Node
{
private GameManager _gameManager;
private AchievementManager _achievementManager;
private Dictionary<string, Variant> _stats = new();
public override void _Ready()
{
_gameManager = GetNode<GameManager>("/root/GameManager");
_achievementManager = GetNode<AchievementManager>("/root/AchievementManager");
LoadStatistics();
}
private void LoadStatistics()
{
if (_gameManager.PlayerState.TryGetValue("statistics", out var statsObj))
{
_stats = (Dictionary<string, Variant>)statsObj;
}
else
{
_stats = new Dictionary<string, Variant>();
_gameManager.PlayerState["statistics"] = _stats;
}
}
/// <summary>
/// Increases a numerical statistic by a given amount.
/// </summary>
public void IncrementStat(string statName, int amount = 1)
{
if (_stats.TryGetValue(statName, out var currentValue))
{
_stats[statName] = (int)currentValue + amount;
}
else
{
_stats[statName] = amount;
}
GD.Print($"Stat '{statName}' updated to: {_stats[statName]}");
CheckAchievementsForStat(statName);
}
/// <summary>
/// Gets the value of a statistic.
/// </summary>
public Variant GetStat(string statName, Variant defaultValue = default)
{
return _stats.TryGetValue(statName, out var value) ? value : defaultValue;
}
/// <summary>
/// Checks if the updated stat meets the criteria for any achievements.
/// </summary>
private void CheckAchievementsForStat(string statName)
{
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;
}
}
}

View File

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