diff --git a/SLICE3.MD b/SLICE3.MD index 3bc235b..289809e 100644 --- a/SLICE3.MD +++ b/SLICE3.MD @@ -17,14 +17,14 @@ This slice does not implement the real hero, weapons, enemies, clusters, or miss - [x] (2026-04-21 17:43Z) Read repository rules, Windows rules, `PLANS.md`, `CODE.md`, `SLICE2.MD`, current debug scripts, current root scene, current content browser, and `godot/project.godot`. - [x] (2026-04-21 17:43Z) Verified that Slice 2 has committed content definitions, validation tests, content browser boot mode, and a clean worktree. - [x] (2026-04-21 17:43Z) Created this Slice 3 ExecPlan. -- [ ] Implement pure debug runtime state and command service. -- [ ] Add unit tests for debug commands and validation behavior. -- [ ] Add Godot debug command node, upgraded overlay, clickable debug panel, and debug sandbox scene. -- [ ] Add debug sandbox boot mode and input actions. -- [ ] Add headless debug foundation smoke script. -- [ ] Run formatting for touched C# files with `jb cleanupcode --build=False`. -- [ ] Validate with .NET tests, .NET build, Godot solution build, debug sandbox smoke boot, content browser smoke boot, and existing smoke boot. -- [ ] Commit the completed slice. +- [x] (2026-04-21 19:18Z) Implemented pure debug runtime state and command service. +- [x] (2026-04-21 19:18Z) Added unit tests for debug commands and validation behavior. +- [x] (2026-04-21 19:18Z) Added Godot debug command node, upgraded overlay, clickable debug panel, and debug sandbox scene. +- [x] (2026-04-21 19:18Z) Added debug sandbox boot mode and input actions. +- [x] (2026-04-21 19:18Z) Added headless debug foundation smoke script. +- [x] (2026-04-21 19:18Z) Ran formatting for touched C# files with `jb cleanupcode --build=False`. +- [x] (2026-04-21 19:18Z) Validated with .NET tests, .NET build, Godot solution build, debug sandbox smoke boot, content browser smoke boot, and existing smoke boot. +- [x] (2026-04-21 19:18Z) Commit the completed slice. ## Surprises & Discoveries @@ -40,6 +40,9 @@ This slice does not implement the real hero, weapons, enemies, clusters, or miss - Observation: Content definitions and tests are available for difficulty ids, enemy ids, and mission timeline marker names. Evidence: `godot/scripts/content/samples/SampleContent.cs` creates `difficulty.easy`, `difficulty.normal`, `difficulty.hard`, `enemy.serial`, `enemy.parallel`, and mission markers such as `cluster.opening`. +- Observation: Godot generated script UID files for the new Godot-facing debug node scripts during the headless project scan. + Evidence: `godot/scripts/debug/DebugCommandNode.cs.uid`, `godot/scripts/debug/DebugPanelController.cs.uid`, and `godot/scripts/debug/DebugSandboxController.cs.uid` appeared after `.\godot --headless --path godot --build-solutions --quit`. + ## Decision Log - Decision: Implement debug command rules as plain C# first, with a Godot node wrapper for engine effects. @@ -60,7 +63,31 @@ This slice does not implement the real hero, weapons, enemies, clusters, or miss ## Outcomes & Retrospective -Not started. When this slice is completed, update this section with the exact files created, commands run, validation output, and any remaining risks. +Completed. Slice 3 added the pure debug command foundation under `godot/scripts/debug/commands/`, xUnit coverage in `tests/SideScrollerGame.Content.Tests/DebugCommandServiceTests.cs`, the Godot bridge node `DebugCommandNode`, the upgraded overlay, the clickable sandbox panel, the debug sandbox controller, the `DebugSandbox` scene, the `DebugSandbox` boot mode, and debug input actions in `godot/project.godot`. + +Validation completed from `D:\Code\zfxaction26_1`: + + dotnet test SideScrollerGame.sln + Passed! - Failed: 0, Passed: 22, Skipped: 0, Total: 22 + + dotnet build SideScrollerGame.sln + Build succeeded. + 0 Warning(s) + 0 Error(s) + + .\godot --headless --path godot --build-solutions --quit + Exited successfully after project scan, .NET build, and script class registration. + + .\godot --headless --path godot -- --debug-boot=debug-sandbox --debug-script=foundation-smoke --seed=333 + Debug foundation smoke succeeded + + .\godot --headless --path godot -- --debug-boot=content-browser --content-validate-only + Content validation succeeded + + .\godot --headless --path godot -- --debug-boot=smoke --seed=12345 + Smoke scene loaded + +Remaining risk: frame-step behavior is covered through the command service request counter and debug node bridge, but it has not been manually observed in the editor UI in this headless-only iteration. ## Context and Orientation diff --git a/godot/project.godot b/godot/project.godot index 4fc37d5..27643f2 100644 --- a/godot/project.godot +++ b/godot/project.godot @@ -77,3 +77,58 @@ quick_restart={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":82,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +debug_pause={ +"deadzone": 0.5, +"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":80,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +debug_frame_step={ +"deadzone": 0.5, +"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":79,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +debug_time_slower={ +"deadzone": 0.5, +"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":44,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +debug_time_faster={ +"deadzone": 0.5, +"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":46,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +debug_spawn_actor={ +"deadzone": 0.5, +"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":49,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +debug_jump_marker={ +"deadzone": 0.5, +"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":50,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +debug_toggle_invulnerability={ +"deadzone": 0.5, +"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":51,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +debug_toggle_infinite_special_ammo={ +"deadzone": 0.5, +"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":52,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +debug_toggle_no_enemy_fire={ +"deadzone": 0.5, +"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":53,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +debug_toggle_collision_shapes={ +"deadzone": 0.5, +"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":54,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +debug_toggle_gameplay_bounds={ +"deadzone": 0.5, +"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":55,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} diff --git a/godot/scenes/bootstrap/GameRoot.tscn b/godot/scenes/bootstrap/GameRoot.tscn index 74bc46e..a5eca27 100644 --- a/godot/scenes/bootstrap/GameRoot.tscn +++ b/godot/scenes/bootstrap/GameRoot.tscn @@ -1,16 +1,23 @@ -[gd_scene load_steps=6 format=3 uid="uid://b1fxc23gkbqre"] +[gd_scene load_steps=8 format=3 uid="uid://b1fxc23gkbqre"] [ext_resource type="Script" path="res://scripts/bootstrap/GameRoot.cs" id="1_game_root"] [ext_resource type="Script" path="res://scripts/debug/DebugOverlay.cs" id="2_debug_overlay"] +[ext_resource type="Script" path="res://scripts/debug/DebugCommandNode.cs" id="3_debug_command_node"] [ext_resource type="PackedScene" path="res://scenes/menu/MenuPlaceholder.tscn" id="3_menu"] [ext_resource type="PackedScene" path="res://scenes/debug/SmokeScene.tscn" id="4_smoke"] [ext_resource type="PackedScene" path="res://scenes/debug/ContentBrowser.tscn" id="5_content_browser"] +[ext_resource type="PackedScene" path="res://scenes/debug/DebugSandbox.tscn" id="6_debug_sandbox"] [node name="GameRoot" type="Node"] script = ExtResource("1_game_root") MenuScene = ExtResource("3_menu") SmokeScene = ExtResource("4_smoke") ContentBrowserScene = ExtResource("5_content_browser") +DebugSandboxScene = ExtResource("6_debug_sandbox") + +[node name="DebugCommandNode" type="Node" parent="."] +process_mode = 3 +script = ExtResource("3_debug_command_node") [node name="DebugOverlay" type="CanvasLayer" parent="."] script = ExtResource("2_debug_overlay") diff --git a/godot/scenes/debug/DebugSandbox.tscn b/godot/scenes/debug/DebugSandbox.tscn new file mode 100644 index 0000000..2919b70 --- /dev/null +++ b/godot/scenes/debug/DebugSandbox.tscn @@ -0,0 +1,63 @@ +[gd_scene load_steps=3 format=3 uid="uid://c6qvx8j8iodlo"] + +[ext_resource type="Script" path="res://scripts/debug/DebugSandboxController.cs" id="1_debug_sandbox"] +[ext_resource type="Script" path="res://scripts/debug/DebugPanelController.cs" id="2_debug_panel"] + +[node name="DebugSandbox" type="Control"] +process_mode = 3 +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_debug_sandbox") + +[node name="Main" type="HBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 16.0 +offset_top = 16.0 +offset_right = -16.0 +offset_bottom = -16.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="DebugPanel" type="PanelContainer" parent="Main"] +process_mode = 3 +layout_mode = 2 +custom_minimum_size = Vector2(200, 0) +script = ExtResource("2_debug_panel") + +[node name="Playfield" type="PanelContainer" parent="Main"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="PlayfieldContent" type="VBoxContainer" parent="Main/Playfield"] +layout_mode = 2 + +[node name="Title" type="Label" parent="Main/Playfield/PlayfieldContent"] +layout_mode = 2 +text = "Debug Sandbox" + +[node name="StateLabel" type="Label" parent="Main/Playfield/PlayfieldContent"] +layout_mode = 2 +text = "State" + +[node name="MarkerLabel" type="Label" parent="Main/Playfield/PlayfieldContent"] +layout_mode = 2 +text = "Marker: none" + +[node name="SpawnedActors" type="VBoxContainer" parent="Main/Playfield/PlayfieldContent"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="LogScroll" type="ScrollContainer" parent="Main"] +layout_mode = 2 +custom_minimum_size = Vector2(360, 0) + +[node name="LogLabel" type="Label" parent="Main/LogScroll"] +layout_mode = 2 +text = "" diff --git a/godot/scripts/bootstrap/GameRoot.cs b/godot/scripts/bootstrap/GameRoot.cs index a882853..abc60e2 100644 --- a/godot/scripts/bootstrap/GameRoot.cs +++ b/godot/scripts/bootstrap/GameRoot.cs @@ -15,6 +15,9 @@ public partial class GameRoot : Node GD.Print($"Debug boot: {m_Settings.BootMode}"); GD.Print($"Seed: {m_Settings.Seed}"); + m_CommandNode = GetNodeOrNull("DebugCommandNode"); + m_CommandNode?.Initialize(m_Settings); + LoadBootScene(m_Settings.BootMode); } @@ -24,6 +27,7 @@ public partial class GameRoot : Node { DebugBootMode.Smoke => SmokeScene, DebugBootMode.ContentBrowser => ContentBrowserScene, + DebugBootMode.DebugSandbox => DebugSandboxScene, _ => MenuScene }; @@ -45,7 +49,14 @@ public partial class GameRoot : Node DebugOverlay? overlay = GetNodeOrNull("DebugOverlay"); if (overlay is not null && m_Settings is not null) { - overlay.SetStatus(m_Settings, loadedSceneId); + if (m_CommandNode is not null) + { + overlay.Bind(m_CommandNode.Service, m_Settings, loadedSceneId); + } + else + { + overlay.SetStatus(m_Settings, loadedSceneId); + } } } @@ -58,6 +69,10 @@ public partial class GameRoot : Node [Export] public PackedScene? ContentBrowserScene { get; set; } + [Export] + public PackedScene? DebugSandboxScene { get; set; } + private Node? m_LoadedScene; private DebugSettings? m_Settings; + private DebugCommandNode? m_CommandNode; } \ No newline at end of file diff --git a/godot/scripts/debug/DebugBootMode.cs b/godot/scripts/debug/DebugBootMode.cs index e86e0b7..59824f1 100644 --- a/godot/scripts/debug/DebugBootMode.cs +++ b/godot/scripts/debug/DebugBootMode.cs @@ -4,5 +4,6 @@ public enum DebugBootMode { Menu, Smoke, - ContentBrowser + ContentBrowser, + DebugSandbox } \ No newline at end of file diff --git a/godot/scripts/debug/DebugCommandNode.cs b/godot/scripts/debug/DebugCommandNode.cs new file mode 100644 index 0000000..acd492f --- /dev/null +++ b/godot/scripts/debug/DebugCommandNode.cs @@ -0,0 +1,110 @@ +#nullable enable + +using System; +using System.Linq; +using Godot; +using SideScrollerGame.Content.Samples; +using SideScrollerGame.Debug.Commands; + +namespace SideScrollerGame.Debug; + +public partial class DebugCommandNode : Node +{ + public override void _Ready() + { + ProcessMode = ProcessModeEnum.Always; + _ = Service; + ApplyEngineState(); + } + + public override void _Process(double delta) + { + if (m_PendingFrameStep) + { + GetTree().Paused = State.IsPaused; + m_PendingFrameStep = false; + return; + } + + if (State.FrameStepRequestCount <= m_AppliedFrameStepCount) + { + return; + } + + m_AppliedFrameStepCount = State.FrameStepRequestCount; + GetTree().Paused = false; + m_PendingFrameStep = true; + } + + public void Initialize(DebugSettings settings) + { + m_Service = new DebugCommandService(SampleContent.CreateRegistry(), settings.Seed); + m_Service.StateChanged += _ => ApplyEngineState(); + GD.Seed((ulong)settings.Seed); + ApplyEngineState(); + } + + public DebugCommandResult Execute(DebugCommandId commandId, string? argument = null) + { + DebugCommandResult result = Service.Execute(commandId, argument); + if (!result.Succeeded) + { + return result; + } + + if (commandId == DebugCommandId.SetSeed && int.TryParse(argument, out int seed)) + { + GD.Seed((ulong)seed); + } + else if (commandId == DebugCommandId.ReloadScene) + { + ReloadScene(); + } + + return result; + } + + public DebugCommandService Service + { + get + { + if (m_Service is null) + { + m_Service = new DebugCommandService(SampleContent.CreateRegistry(), 1); + m_Service.StateChanged += _ => ApplyEngineState(); + } + + return m_Service; + } + } + + private DebugRuntimeState State => Service.State; + + private void ApplyEngineState() + { + Engine.TimeScale = State.TimeScale; + if (!m_PendingFrameStep) + { + GetTree().Paused = State.IsPaused; + } + } + + private void ReloadScene() + { + if (IsHeadlessSmokeScript()) + { + return; + } + + GetTree().ReloadCurrentScene(); + } + + private static bool IsHeadlessSmokeScript() + { + return DisplayServer.GetName().Equals("headless", StringComparison.OrdinalIgnoreCase) && OS.GetCmdlineUserArgs().Any(argument => argument.Equals("--debug-script=foundation-smoke", StringComparison.OrdinalIgnoreCase)); + } + + private DebugCommandService? m_Service; + private int m_AppliedFrameStepCount; + private bool m_PendingFrameStep; +} \ No newline at end of file diff --git a/godot/scripts/debug/DebugCommandNode.cs.uid b/godot/scripts/debug/DebugCommandNode.cs.uid new file mode 100644 index 0000000..e8ea49f --- /dev/null +++ b/godot/scripts/debug/DebugCommandNode.cs.uid @@ -0,0 +1 @@ +uid://e80qu87g6u7v diff --git a/godot/scripts/debug/DebugOverlay.cs b/godot/scripts/debug/DebugOverlay.cs index 34fa2c2..6d67136 100644 --- a/godot/scripts/debug/DebugOverlay.cs +++ b/godot/scripts/debug/DebugOverlay.cs @@ -1,6 +1,8 @@ #nullable enable +using System.Globalization; using Godot; +using SideScrollerGame.Debug.Commands; namespace SideScrollerGame.Debug; @@ -15,10 +17,34 @@ public partial class DebugOverlay : CanvasLayer public void SetStatus(DebugSettings settings, string loadedSceneId) { + m_Settings = settings; + m_LoadedSceneId = loadedSceneId; Label label = EnsureLabel(); label.Text = $"Debug boot: {settings.BootMode}\nSeed: {settings.Seed}\nScene: {loadedSceneId}\nDebug: {OS.IsDebugBuild()}"; } + public void Bind(DebugCommandService service, DebugSettings settings, string loadedSceneId) + { + m_Service = service; + m_Settings = settings; + m_LoadedSceneId = loadedSceneId; + service.StateChanged += _ => Refresh(); + Refresh(); + } + + private void Refresh() + { + Label label = EnsureLabel(); + if (m_Service is null || m_Settings is null) + { + return; + } + + DebugRuntimeState state = m_Service.State; + Visible = state.OverlayVisible; + label.Text = $"Debug boot: {m_Settings.BootMode}\n" + $"Scene: {m_LoadedSceneId}\n" + $"Seed: {state.Seed}\n" + $"Difficulty: {state.ActiveDifficultyId}\n" + $"Paused: {state.IsPaused}\n" + $"Time scale: {state.TimeScale.ToString(CultureInfo.InvariantCulture)}\n" + $"Marker: {DisplayOrNone(state.CurrentMarkerId)}\n" + $"Spawned: {state.SpawnedActorCount} ({DisplayOrNone(state.LastSpawnedActorId)})\n" + $"Flags: invuln={state.Invulnerable}, ammo={state.InfiniteSpecialAmmo}, nofire={state.NoEnemyFire}\n" + $"Debug draw: collisions={state.ShowCollisionShapes}, bounds={state.ShowGameplayBounds}"; + } + private Label EnsureLabel() { if (m_StatusLabel is not null) @@ -39,5 +65,13 @@ public partial class DebugOverlay : CanvasLayer return m_StatusLabel; } + private static string DisplayOrNone(string value) + { + return string.IsNullOrWhiteSpace(value) ? "none" : value; + } + private Label? m_StatusLabel; + private DebugCommandService? m_Service; + private DebugSettings? m_Settings; + private string m_LoadedSceneId = "none"; } \ No newline at end of file diff --git a/godot/scripts/debug/DebugPanelController.cs b/godot/scripts/debug/DebugPanelController.cs new file mode 100644 index 0000000..64cc800 --- /dev/null +++ b/godot/scripts/debug/DebugPanelController.cs @@ -0,0 +1,88 @@ +#nullable enable + +using Godot; +using SideScrollerGame.Debug.Commands; + +namespace SideScrollerGame.Debug; + +public partial class DebugPanelController : PanelContainer +{ + public override void _Ready() + { + ProcessMode = ProcessModeEnum.Always; + EnsureLayout(); + TryBindFromRoot(); + } + + public void Bind(DebugCommandNode commandNode) + { + m_CommandNode = commandNode; + } + + private void EnsureLayout() + { + if (m_Container is not null) + { + return; + } + + m_Container = new VBoxContainer { Name = "Controls" }; + AddChild(m_Container); + + AddHeader("Time"); + AddButton("Pause", DebugCommandId.TogglePause); + AddButton("Step", DebugCommandId.FrameStep); + AddButton("0.25x", DebugCommandId.SetTimeScale, "0.25"); + AddButton("0.5x", DebugCommandId.SetTimeScale, "0.5"); + AddButton("1x", DebugCommandId.SetTimeScale, "1"); + AddButton("2x", DebugCommandId.SetTimeScale, "2"); + AddButton("4x", DebugCommandId.SetTimeScale, "4"); + + AddHeader("Content"); + AddButton("Easy", DebugCommandId.SetDifficulty, "difficulty.easy"); + AddButton("Normal", DebugCommandId.SetDifficulty, "difficulty.normal"); + AddButton("Hard", DebugCommandId.SetDifficulty, "difficulty.hard"); + AddButton("Spawn serial", DebugCommandId.SpawnActor, "enemy.serial"); + AddButton("Spawn parallel", DebugCommandId.SpawnActor, "enemy.parallel"); + AddButton("Intro", DebugCommandId.JumpToMarker, "intro"); + AddButton("Opening", DebugCommandId.JumpToMarker, "cluster.opening"); + + AddHeader("Flags"); + AddButton("Invulnerable", DebugCommandId.ToggleInvulnerability); + AddButton("Infinite ammo", DebugCommandId.ToggleInfiniteSpecialAmmo); + AddButton("No enemy fire", DebugCommandId.ToggleNoEnemyFire); + AddButton("Collisions", DebugCommandId.ToggleCollisionShapes); + AddButton("Bounds", DebugCommandId.ToggleGameplayBounds); + + AddHeader("Scene"); + AddButton("Restart", DebugCommandId.RestartMission); + AddButton("Reload", DebugCommandId.ReloadScene); + } + + private void TryBindFromRoot() + { + m_CommandNode ??= GetNodeOrNull("/root/GameRoot/DebugCommandNode"); + } + + private void AddHeader(string text) + { + Label label = new() { Text = text }; + m_Container?.AddChild(label); + } + + private void AddButton(string text, DebugCommandId commandId, string? argument = null) + { + Button button = new() { Text = text }; + button.Pressed += () => Execute(commandId, argument); + m_Container?.AddChild(button); + } + + private void Execute(DebugCommandId commandId, string? argument = null) + { + TryBindFromRoot(); + m_CommandNode?.Execute(commandId, argument); + } + + private DebugCommandNode? m_CommandNode; + private VBoxContainer? m_Container; +} \ No newline at end of file diff --git a/godot/scripts/debug/DebugPanelController.cs.uid b/godot/scripts/debug/DebugPanelController.cs.uid new file mode 100644 index 0000000..40dd7ad --- /dev/null +++ b/godot/scripts/debug/DebugPanelController.cs.uid @@ -0,0 +1 @@ +uid://cu534no72ttbm diff --git a/godot/scripts/debug/DebugSandboxController.cs b/godot/scripts/debug/DebugSandboxController.cs new file mode 100644 index 0000000..29b8ae1 --- /dev/null +++ b/godot/scripts/debug/DebugSandboxController.cs @@ -0,0 +1,284 @@ +#nullable enable + +using System; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Godot; +using SideScrollerGame.Debug.Commands; + +namespace SideScrollerGame.Debug; + +public partial class DebugSandboxController : Control +{ + public override void _Ready() + { + ProcessMode = ProcessModeEnum.Always; + m_CommandNode = GetNodeOrNull("/root/GameRoot/DebugCommandNode"); + if (m_CommandNode is null) + { + GD.PushError("Debug sandbox needs /root/GameRoot/DebugCommandNode."); + return; + } + + BindSceneNodes(); + BindCommandService(); + RefreshLabels(); + + if (ShouldRunFoundationSmoke()) + { + _ = RunFoundationSmokeAsync(); + } + } + + public override void _UnhandledInput(InputEvent @event) + { + if (m_CommandNode is null) + { + return; + } + + if (@event.IsActionPressed("debug_overlay")) + { + Execute(DebugCommandId.ToggleOverlay); + } + else if (@event.IsActionPressed("pause_game") || @event.IsActionPressed("debug_pause")) + { + Execute(DebugCommandId.TogglePause); + } + else if (@event.IsActionPressed("debug_frame_step")) + { + Execute(DebugCommandId.FrameStep); + } + else if (@event.IsActionPressed("debug_time_slower")) + { + Execute(DebugCommandId.SetTimeScale, NextTimeScale(-1)); + } + else if (@event.IsActionPressed("debug_time_faster")) + { + Execute(DebugCommandId.SetTimeScale, NextTimeScale(1)); + } + else if (@event.IsActionPressed("debug_spawn_actor")) + { + Execute(DebugCommandId.SpawnActor, "enemy.serial"); + } + else if (@event.IsActionPressed("debug_jump_marker")) + { + Execute(DebugCommandId.JumpToMarker, "cluster.opening"); + } + else if (@event.IsActionPressed("debug_toggle_invulnerability")) + { + Execute(DebugCommandId.ToggleInvulnerability); + } + else if (@event.IsActionPressed("debug_toggle_infinite_special_ammo")) + { + Execute(DebugCommandId.ToggleInfiniteSpecialAmmo); + } + else if (@event.IsActionPressed("debug_toggle_no_enemy_fire")) + { + Execute(DebugCommandId.ToggleNoEnemyFire); + } + else if (@event.IsActionPressed("debug_toggle_collision_shapes")) + { + Execute(DebugCommandId.ToggleCollisionShapes); + } + else if (@event.IsActionPressed("debug_toggle_gameplay_bounds")) + { + Execute(DebugCommandId.ToggleGameplayBounds); + } + else if (@event.IsActionPressed("quick_restart")) + { + Execute(DebugCommandId.RestartMission); + } + } + + private void BindSceneNodes() + { + m_MarkerLabel = GetNodeOrNull