Add EventBus, SpeedRunManager, and GhostManager; implement ghost recording and playback features
This commit is contained in:
9
Autoloads/EventBus.cs
Normal file
9
Autoloads/EventBus.cs
Normal 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);
|
||||
}
|
1
Autoloads/EventBus.cs.uid
Normal file
1
Autoloads/EventBus.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bb2yq5wggdw3w
|
@@ -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
112
Autoloads/GhostManager.cs
Normal 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"];
|
||||
}
|
||||
}
|
1
Autoloads/GhostManager.cs.uid
Normal file
1
Autoloads/GhostManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cgmuod4p2hg5h
|
62
Autoloads/SpeedRunManager.cs
Normal file
62
Autoloads/SpeedRunManager.cs
Normal 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}";
|
||||
}
|
||||
}
|
1
Autoloads/SpeedRunManager.cs.uid
Normal file
1
Autoloads/SpeedRunManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c6ohe36xw1h21
|
78
Autoloads/StatisticsManager.cs
Normal file
78
Autoloads/StatisticsManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
1
Autoloads/StatisticsManager.cs.uid
Normal file
1
Autoloads/StatisticsManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c5p3l2mhkw0p4
|
Reference in New Issue
Block a user