diff --git a/Autoloads/EventBus.cs b/Autoloads/EventBus.cs new file mode 100644 index 0000000..9b03718 --- /dev/null +++ b/Autoloads/EventBus.cs @@ -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); +} \ No newline at end of file diff --git a/Autoloads/EventBus.cs.uid b/Autoloads/EventBus.cs.uid new file mode 100644 index 0000000..2cab6b9 --- /dev/null +++ b/Autoloads/EventBus.cs.uid @@ -0,0 +1 @@ +uid://bb2yq5wggdw3w diff --git a/Autoloads/GameManager.cs b/Autoloads/GameManager.cs index 09aeeb3..f449190 100644 --- a/Autoloads/GameManager.cs +++ b/Autoloads/GameManager.cs @@ -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 _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("/root/SpeedRunManager"); + _eventBus = GetNode("/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() }, { "unlocked_levels", new Array() {0}}, - { "unlocked_skills", new Array() } + { "unlocked_skills", new Array() }, + { "statistics", new Godot.Collections.Dictionary()} }; } @@ -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("/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("/root/SaveSystem").SaveGame(); diff --git a/Autoloads/GhostManager.cs b/Autoloads/GhostManager.cs new file mode 100644 index 0000000..931227a --- /dev/null +++ b/Autoloads/GhostManager.cs @@ -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 _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(); + 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 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(); + 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"]; + } +} \ No newline at end of file diff --git a/Autoloads/GhostManager.cs.uid b/Autoloads/GhostManager.cs.uid new file mode 100644 index 0000000..2daece3 --- /dev/null +++ b/Autoloads/GhostManager.cs.uid @@ -0,0 +1 @@ +uid://cgmuod4p2hg5h diff --git a/Autoloads/SpeedRunManager.cs b/Autoloads/SpeedRunManager.cs new file mode 100644 index 0000000..3dd7676 --- /dev/null +++ b/Autoloads/SpeedRunManager.cs @@ -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 _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}"; + } +} \ No newline at end of file diff --git a/Autoloads/SpeedRunManager.cs.uid b/Autoloads/SpeedRunManager.cs.uid new file mode 100644 index 0000000..a2ea121 --- /dev/null +++ b/Autoloads/SpeedRunManager.cs.uid @@ -0,0 +1 @@ +uid://c6ohe36xw1h21 diff --git a/Autoloads/StatisticsManager.cs b/Autoloads/StatisticsManager.cs new file mode 100644 index 0000000..a9c9ccf --- /dev/null +++ b/Autoloads/StatisticsManager.cs @@ -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 _stats = new(); + + public override void _Ready() + { + _gameManager = GetNode("/root/GameManager"); + _achievementManager = GetNode("/root/AchievementManager"); + LoadStatistics(); + } + + 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)) + { + _stats[statName] = (int)currentValue + amount; + } + else + { + _stats[statName] = amount; + } + GD.Print($"Stat '{statName}' updated to: {_stats[statName]}"); + CheckAchievementsForStat(statName); + } + + /// + /// Gets the value of a statistic. + /// + public Variant GetStat(string statName, Variant defaultValue = default) + { + return _stats.TryGetValue(statName, out var value) ? value : defaultValue; + } + + /// + /// Checks if the updated stat meets the criteria for any achievements. + /// + 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; + } + } +} \ No newline at end of file diff --git a/Autoloads/StatisticsManager.cs.uid b/Autoloads/StatisticsManager.cs.uid new file mode 100644 index 0000000..50b21dd --- /dev/null +++ b/Autoloads/StatisticsManager.cs.uid @@ -0,0 +1 @@ +uid://c5p3l2mhkw0p4 diff --git a/objects/entities/ghost_player.tscn b/objects/entities/ghost_player.tscn new file mode 100644 index 0000000..ae3f13a --- /dev/null +++ b/objects/entities/ghost_player.tscn @@ -0,0 +1,39 @@ +[gd_scene load_steps=7 format=3 uid="uid://gknrmek1jmjx"] + +[ext_resource type="Shader" uid="uid://bs4xvm4qkurpr" path="res://shaders/hit_flash.tres" id="13_uybbp"] +[ext_resource type="Texture2D" uid="uid://0l454rfplmqg" path="res://sprites/MrBrick_base-sheet.png" id="14_4rwar"] +[ext_resource type="Texture2D" uid="uid://jl1gwqchhpdc" path="res://sprites/left_eye.png" id="15_qkwlh"] +[ext_resource type="Texture2D" uid="uid://iiawtnwmeny3" path="res://sprites/right_eye.png" id="16_kt5il"] +[ext_resource type="Texture2D" uid="uid://dhkwyv6ayb5qb" path="res://sprites/flying_ship.png" id="17_i5nnv"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_xoue7"] +shader = ExtResource("13_uybbp") +shader_parameter/enabled = false +shader_parameter/tint = Color(1, 1, 1, 1) + +[node name="Brick Player" type="Node2D"] + +[node name="Graphics" type="Node2D" parent="."] +modulate = Color(1, 1, 1, 0.443137) + +[node name="Root" type="Node2D" parent="Graphics"] + +[node name="Base" type="Sprite2D" parent="Graphics/Root"] +material = SubResource("ShaderMaterial_xoue7") +texture = ExtResource("14_4rwar") +hframes = 5 + +[node name="Left Eye" type="Sprite2D" parent="Graphics/Root"] +position = Vector2(-7, -6) +texture = ExtResource("15_qkwlh") +hframes = 2 + +[node name="Right Eye" type="Sprite2D" parent="Graphics/Root"] +position = Vector2(6, -5) +texture = ExtResource("16_kt5il") +hframes = 2 + +[node name="Ship" type="Sprite2D" parent="Graphics"] +visible = false +position = Vector2(1, 7) +texture = ExtResource("17_i5nnv") diff --git a/objects/ghost_manager.tscn b/objects/ghost_manager.tscn new file mode 100644 index 0000000..d20faa2 --- /dev/null +++ b/objects/ghost_manager.tscn @@ -0,0 +1,8 @@ +[gd_scene load_steps=3 format=3 uid="uid://ckeu2eddl5b3m"] + +[ext_resource type="Script" uid="uid://cgmuod4p2hg5h" path="res://Autoloads/GhostManager.cs" id="1_u0u02"] +[ext_resource type="PackedScene" uid="uid://gknrmek1jmjx" path="res://objects/entities/ghost_player.tscn" id="2_jnk6u"] + +[node name="GhostManager" type="Node"] +script = ExtResource("1_u0u02") +GhostPlayerScene = ExtResource("2_jnk6u") diff --git a/objects/ui/speed_run_hud.tscn b/objects/ui/speed_run_hud.tscn new file mode 100644 index 0000000..2cbdd54 --- /dev/null +++ b/objects/ui/speed_run_hud.tscn @@ -0,0 +1,26 @@ +[gd_scene load_steps=2 format=3 uid="uid://kh85xqo6j848"] + +[ext_resource type="Script" uid="uid://0jfdx0hufs55" path="res://scripts/UI/SpeedRunHud.cs" id="1_uwqm0"] + +[node name="SpeedRunHud" type="Control" node_paths=PackedStringArray("_timerLabel")] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_uwqm0") +_timerLabel = NodePath("Label") +metadata/_custom_type_script = "uid://0jfdx0hufs55" + +[node name="Label" type="Label" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +text = "00:00:00" +horizontal_alignment = 1 +vertical_alignment = 1 +uppercase = true diff --git a/project.godot b/project.godot index 63c6772..9d7d4b5 100644 --- a/project.godot +++ b/project.godot @@ -42,6 +42,10 @@ SteamManager="*res://Autoloads/SteamManager.cs" AchievementManager="*res://objects/achievement_manager.tscn" SkillManager="*res://objects/skill_manager.tscn" FloatingTextManager="*res://objects/floating_text_manager.tscn" +EventBus="*res://Autoloads/EventBus.cs" +StatisticsManager="*res://Autoloads/StatisticsManager.cs" +SpeedRunManager="res://Autoloads/SpeedRunManager.cs" +GhostManager="res://objects/ghost_manager.tscn" [debug] @@ -124,7 +128,6 @@ jump={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":11,"pressure":0.0,"pressed":true,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":true,"script":null) -, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null) ] } up={ diff --git a/scenes/level_village_1.tscn b/scenes/level_village_1.tscn index 41e290c..c3dcc34 100644 --- a/scenes/level_village_1.tscn +++ b/scenes/level_village_1.tscn @@ -1,10 +1,11 @@ -[gd_scene load_steps=23 format=4 uid="uid://bol7g83v2accs"] +[gd_scene load_steps=26 format=4 uid="uid://bol7g83v2accs"] [ext_resource type="PackedScene" uid="uid://bqi5s710xb1ju" path="res://objects/entities/brick_player.tscn" id="1_dnj2y"] [ext_resource type="PackedScene" uid="uid://cawlpch2lk3a2" path="res://objects/level/world_environment.tscn" id="2_1vw1j"] [ext_resource type="PackedScene" uid="uid://6foggu31cu14" path="res://objects/level/ui_layer.tscn" id="3_4fsls"] [ext_resource type="PackedScene" uid="uid://cywsu7yrtjdog" path="res://objects/level/global_light.tscn" id="4_mc58c"] [ext_resource type="Resource" uid="uid://cqtalsov2bkpo" path="res://resources/levels/village/village_1.tres" id="4_onnch"] +[ext_resource type="PackedScene" uid="uid://kh85xqo6j848" path="res://objects/ui/speed_run_hud.tscn" id="5_chnw1"] [ext_resource type="PackedScene" uid="uid://cb0mnye1ki5a6" path="res://objects/level/camera_2d.tscn" id="5_sskgn"] [ext_resource type="Script" uid="uid://d23haq52m7ulv" path="res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_2d.gd" id="6_18aqg"] [ext_resource type="Script" uid="uid://ccfft4b8rwgbo" path="res://addons/phantom_camera/scripts/resources/tween_resource.gd" id="7_80vn0"] @@ -16,6 +17,8 @@ [ext_resource type="PackedScene" uid="uid://bqom4cm7r18db" path="res://objects/entities/killzone.tscn" id="16_bxal3"] [ext_resource type="PackedScene" uid="uid://12jnkdygpxwc" path="res://objects/entities/exit_level.tscn" id="16_chnw1"] [ext_resource type="PackedScene" uid="uid://b4pdt1gv2ymyi" path="res://objects/tooltip.tscn" id="18_4bhfj"] +[ext_resource type="Script" uid="uid://dkqeys6lsf04v" path="res://scripts/Events/GhostEventHandler.cs" id="18_p7e1u"] +[ext_resource type="Script" uid="uid://bysxvcvt00t04" path="res://scripts/Events/SpeedRunEventHandler.cs" id="19_84xgs"] [sub_resource type="Gradient" id="Gradient_qb72p"] colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 0) @@ -83,6 +86,17 @@ SettingsControl = NodePath("../Settings menu") InputSettingsControl = NodePath("../Input Settings") AudioSettingsControl = NodePath("../Audio settings") +[node name="SpeedRunHud" parent="UI Layer" instance=ExtResource("5_chnw1")] +anchors_preset = 1 +anchor_left = 1.0 +anchor_bottom = 0.0 +offset_left = -40.0 +offset_top = 38.0 +offset_right = -40.0 +offset_bottom = 38.0 +grow_horizontal = 0 +grow_vertical = 1 + [node name="Global Light" parent="." instance=ExtResource("4_mc58c")] [node name="Camera2D" parent="." instance=ExtResource("5_sskgn")] @@ -145,6 +159,14 @@ Text = "LEVEL_1_TOOLTIP_3" [node name="Killzone" parent="." instance=ExtResource("16_bxal3")] position = Vector2(704, 337) +[node name="GhostEventHandler" type="Node" parent="."] +script = ExtResource("18_p7e1u") +metadata/_custom_type_script = "uid://dkqeys6lsf04v" + +[node name="SpeedRunEventHandler" type="Node" parent="."] +script = ExtResource("19_84xgs") +metadata/_custom_type_script = "uid://bysxvcvt00t04" + [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/Events/GhostEventHandler.cs b/scripts/Events/GhostEventHandler.cs new file mode 100644 index 0000000..6a0aab2 --- /dev/null +++ b/scripts/Events/GhostEventHandler.cs @@ -0,0 +1,31 @@ +using Godot; +using Mr.BrickAdventures.Autoloads; + +namespace Mr.BrickAdventures.scripts.Events; + +[GlobalClass] +public partial class GhostEventHandler : Node +{ + private GhostManager _ghostManager; + + public override void _Ready() + { + _ghostManager = GetNode("/root/GhostManager"); + var eventBus = GetNode("/root/EventBus"); + + eventBus.LevelStarted += OnLevelStarted; + eventBus.LevelCompleted += OnLevelCompleted; + } + + private void OnLevelStarted(int levelIndex, Node currentScene) + { + GD.Print($"GhostEventHandler: Level {levelIndex} started."); + _ghostManager.StartRecording(levelIndex); + _ghostManager.SpawnGhostPlayer(levelIndex, currentScene); + } + + private void OnLevelCompleted(int levelIndex, Node currentScene, double completionTime) + { + _ghostManager.StopRecording(true, completionTime); + } +} \ No newline at end of file diff --git a/scripts/Events/GhostEventHandler.cs.uid b/scripts/Events/GhostEventHandler.cs.uid new file mode 100644 index 0000000..1fafad6 --- /dev/null +++ b/scripts/Events/GhostEventHandler.cs.uid @@ -0,0 +1 @@ +uid://dkqeys6lsf04v diff --git a/scripts/Events/SpeedRunEventHandler.cs b/scripts/Events/SpeedRunEventHandler.cs new file mode 100644 index 0000000..93acbd2 --- /dev/null +++ b/scripts/Events/SpeedRunEventHandler.cs @@ -0,0 +1,23 @@ +using Godot; +using Mr.BrickAdventures.Autoloads; + +namespace Mr.BrickAdventures.scripts.Events; + +[GlobalClass] +public partial class SpeedRunEventHandler : Node +{ + private SpeedRunManager _speedRunManager; + + public override void _Ready() + { + _speedRunManager = GetNode("/root/SpeedRunManager"); + var eventBus = GetNode("/root/EventBus"); + + eventBus.LevelCompleted += OnLevelCompleted; + } + + private void OnLevelCompleted(int levelIndex, Node currentScene, double completionTime) + { + _speedRunManager.Split(); + } +} \ No newline at end of file diff --git a/scripts/Events/SpeedRunEventHandler.cs.uid b/scripts/Events/SpeedRunEventHandler.cs.uid new file mode 100644 index 0000000..443aa0c --- /dev/null +++ b/scripts/Events/SpeedRunEventHandler.cs.uid @@ -0,0 +1 @@ +uid://bysxvcvt00t04 diff --git a/scripts/GhostFrame.cs b/scripts/GhostFrame.cs new file mode 100644 index 0000000..7307f2c --- /dev/null +++ b/scripts/GhostFrame.cs @@ -0,0 +1,9 @@ +using Godot; + +namespace Mr.BrickAdventures.scripts; + +public partial class GhostFrame : GodotObject +{ + public double Timestamp { get; set; } + public Vector2 Position { get; set; } +} \ No newline at end of file diff --git a/scripts/GhostFrame.cs.uid b/scripts/GhostFrame.cs.uid new file mode 100644 index 0000000..328469d --- /dev/null +++ b/scripts/GhostFrame.cs.uid @@ -0,0 +1 @@ +uid://d2vth3yveucti diff --git a/scripts/GhostPlayer.cs b/scripts/GhostPlayer.cs new file mode 100644 index 0000000..df5bb1a --- /dev/null +++ b/scripts/GhostPlayer.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Godot; + +namespace Mr.BrickAdventures.scripts; + +[GlobalClass] +public partial class GhostPlayer : Node2D +{ + private List _playbackData = []; + private double _startTime = 0; + private int _currentFrameIndex = 0; + private bool _isPlaying = false; + + public void StartPlayback(List playbackData) + { + _playbackData = playbackData; + _startTime = Time.GetTicksMsec() / 1000.0; + _currentFrameIndex = 0; + _isPlaying = true; + SetProcess(true); + } + + public override void _PhysicsProcess(double delta) + { + if (!_isPlaying || _playbackData.Count == 0) return; + + var currentTime = (Time.GetTicksMsec() / 1000.0) - _startTime; + + while (_currentFrameIndex + 1 < _playbackData.Count && _playbackData[_currentFrameIndex + 1].Timestamp <= currentTime) + { + _currentFrameIndex++; + } + + if (_currentFrameIndex + 1 >= _playbackData.Count) + { + GlobalPosition = _playbackData[_currentFrameIndex].Position; + _isPlaying = false; + QueueFree(); + return; + } + + var frameA = _playbackData[_currentFrameIndex]; + var frameB = _playbackData[_currentFrameIndex + 1]; + var t = (currentTime - frameA.Timestamp) / (frameB.Timestamp - frameA.Timestamp); + + GlobalPosition = frameA.Position.Lerp(frameB.Position, (float)t); + } +} \ No newline at end of file diff --git a/scripts/GhostPlayer.cs.uid b/scripts/GhostPlayer.cs.uid new file mode 100644 index 0000000..76aa9f3 --- /dev/null +++ b/scripts/GhostPlayer.cs.uid @@ -0,0 +1 @@ +uid://cr4bfcpo27e7t diff --git a/scripts/UI/SpeedRunHud.cs b/scripts/UI/SpeedRunHud.cs new file mode 100644 index 0000000..8110f22 --- /dev/null +++ b/scripts/UI/SpeedRunHud.cs @@ -0,0 +1,26 @@ +using Godot; +using Mr.BrickAdventures.Autoloads; + +namespace Mr.BrickAdventures.scripts.UI; + +[GlobalClass] +public partial class SpeedRunHud : Control +{ + [Export] private Label _timerLabel; + + private SpeedRunManager _speedRunManager; + + public override void _Ready() + { + _speedRunManager = GetNode("/root/SpeedRunManager"); + + _speedRunManager.TimeUpdated += OnTimerUpdated; + + Visible = _speedRunManager.IsVisible; + } + + private void OnTimerUpdated(double totalTime, double levelTime) + { + _timerLabel.Text = SpeedRunManager.FormatTime(totalTime); + } +} \ No newline at end of file diff --git a/scripts/UI/SpeedRunHud.cs.uid b/scripts/UI/SpeedRunHud.cs.uid new file mode 100644 index 0000000..c848497 --- /dev/null +++ b/scripts/UI/SpeedRunHud.cs.uid @@ -0,0 +1 @@ +uid://0jfdx0hufs55