Add debug foundation
This commit is contained in:
45
SLICE3.MD
45
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) 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) 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.
|
- [x] (2026-04-21 17:43Z) Created this Slice 3 ExecPlan.
|
||||||
- [ ] Implement pure debug runtime state and command service.
|
- [x] (2026-04-21 19:18Z) Implemented pure debug runtime state and command service.
|
||||||
- [ ] Add unit tests for debug commands and validation behavior.
|
- [x] (2026-04-21 19:18Z) Added unit tests for debug commands and validation behavior.
|
||||||
- [ ] Add Godot debug command node, upgraded overlay, clickable debug panel, and debug sandbox scene.
|
- [x] (2026-04-21 19:18Z) Added Godot debug command node, upgraded overlay, clickable debug panel, and debug sandbox scene.
|
||||||
- [ ] Add debug sandbox boot mode and input actions.
|
- [x] (2026-04-21 19:18Z) Added debug sandbox boot mode and input actions.
|
||||||
- [ ] Add headless debug foundation smoke script.
|
- [x] (2026-04-21 19:18Z) Added headless debug foundation smoke script.
|
||||||
- [ ] Run formatting for touched C# files with `jb cleanupcode --build=False`.
|
- [x] (2026-04-21 19:18Z) Ran 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.
|
- [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.
|
||||||
- [ ] Commit the completed slice.
|
- [x] (2026-04-21 19:18Z) Commit the completed slice.
|
||||||
|
|
||||||
## Surprises & Discoveries
|
## 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.
|
- 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`.
|
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 Log
|
||||||
|
|
||||||
- Decision: Implement debug command rules as plain C# first, with a Godot node wrapper for engine effects.
|
- 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
|
## 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
|
## Context and Orientation
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
"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)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -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/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/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/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/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/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"]
|
[node name="GameRoot" type="Node"]
|
||||||
script = ExtResource("1_game_root")
|
script = ExtResource("1_game_root")
|
||||||
MenuScene = ExtResource("3_menu")
|
MenuScene = ExtResource("3_menu")
|
||||||
SmokeScene = ExtResource("4_smoke")
|
SmokeScene = ExtResource("4_smoke")
|
||||||
ContentBrowserScene = ExtResource("5_content_browser")
|
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="."]
|
[node name="DebugOverlay" type="CanvasLayer" parent="."]
|
||||||
script = ExtResource("2_debug_overlay")
|
script = ExtResource("2_debug_overlay")
|
||||||
|
|||||||
63
godot/scenes/debug/DebugSandbox.tscn
Normal file
63
godot/scenes/debug/DebugSandbox.tscn
Normal file
@@ -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 = ""
|
||||||
@@ -15,6 +15,9 @@ public partial class GameRoot : Node
|
|||||||
GD.Print($"Debug boot: {m_Settings.BootMode}");
|
GD.Print($"Debug boot: {m_Settings.BootMode}");
|
||||||
GD.Print($"Seed: {m_Settings.Seed}");
|
GD.Print($"Seed: {m_Settings.Seed}");
|
||||||
|
|
||||||
|
m_CommandNode = GetNodeOrNull<DebugCommandNode>("DebugCommandNode");
|
||||||
|
m_CommandNode?.Initialize(m_Settings);
|
||||||
|
|
||||||
LoadBootScene(m_Settings.BootMode);
|
LoadBootScene(m_Settings.BootMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,6 +27,7 @@ public partial class GameRoot : Node
|
|||||||
{
|
{
|
||||||
DebugBootMode.Smoke => SmokeScene,
|
DebugBootMode.Smoke => SmokeScene,
|
||||||
DebugBootMode.ContentBrowser => ContentBrowserScene,
|
DebugBootMode.ContentBrowser => ContentBrowserScene,
|
||||||
|
DebugBootMode.DebugSandbox => DebugSandboxScene,
|
||||||
_ => MenuScene
|
_ => MenuScene
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,10 +48,17 @@ public partial class GameRoot : Node
|
|||||||
|
|
||||||
DebugOverlay? overlay = GetNodeOrNull<DebugOverlay>("DebugOverlay");
|
DebugOverlay? overlay = GetNodeOrNull<DebugOverlay>("DebugOverlay");
|
||||||
if (overlay is not null && m_Settings is not null)
|
if (overlay is not null && m_Settings is not null)
|
||||||
|
{
|
||||||
|
if (m_CommandNode is not null)
|
||||||
|
{
|
||||||
|
overlay.Bind(m_CommandNode.Service, m_Settings, loadedSceneId);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
overlay.SetStatus(m_Settings, loadedSceneId);
|
overlay.SetStatus(m_Settings, loadedSceneId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Export]
|
[Export]
|
||||||
public PackedScene? MenuScene { get; set; }
|
public PackedScene? MenuScene { get; set; }
|
||||||
@@ -58,6 +69,10 @@ public partial class GameRoot : Node
|
|||||||
[Export]
|
[Export]
|
||||||
public PackedScene? ContentBrowserScene { get; set; }
|
public PackedScene? ContentBrowserScene { get; set; }
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
public PackedScene? DebugSandboxScene { get; set; }
|
||||||
|
|
||||||
private Node? m_LoadedScene;
|
private Node? m_LoadedScene;
|
||||||
private DebugSettings? m_Settings;
|
private DebugSettings? m_Settings;
|
||||||
|
private DebugCommandNode? m_CommandNode;
|
||||||
}
|
}
|
||||||
@@ -4,5 +4,6 @@ public enum DebugBootMode
|
|||||||
{
|
{
|
||||||
Menu,
|
Menu,
|
||||||
Smoke,
|
Smoke,
|
||||||
ContentBrowser
|
ContentBrowser,
|
||||||
|
DebugSandbox
|
||||||
}
|
}
|
||||||
110
godot/scripts/debug/DebugCommandNode.cs
Normal file
110
godot/scripts/debug/DebugCommandNode.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
1
godot/scripts/debug/DebugCommandNode.cs.uid
Normal file
1
godot/scripts/debug/DebugCommandNode.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://e80qu87g6u7v
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
using SideScrollerGame.Debug.Commands;
|
||||||
|
|
||||||
namespace SideScrollerGame.Debug;
|
namespace SideScrollerGame.Debug;
|
||||||
|
|
||||||
@@ -15,10 +17,34 @@ public partial class DebugOverlay : CanvasLayer
|
|||||||
|
|
||||||
public void SetStatus(DebugSettings settings, string loadedSceneId)
|
public void SetStatus(DebugSettings settings, string loadedSceneId)
|
||||||
{
|
{
|
||||||
|
m_Settings = settings;
|
||||||
|
m_LoadedSceneId = loadedSceneId;
|
||||||
Label label = EnsureLabel();
|
Label label = EnsureLabel();
|
||||||
label.Text = $"Debug boot: {settings.BootMode}\nSeed: {settings.Seed}\nScene: {loadedSceneId}\nDebug: {OS.IsDebugBuild()}";
|
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()
|
private Label EnsureLabel()
|
||||||
{
|
{
|
||||||
if (m_StatusLabel is not null)
|
if (m_StatusLabel is not null)
|
||||||
@@ -39,5 +65,13 @@ public partial class DebugOverlay : CanvasLayer
|
|||||||
return m_StatusLabel;
|
return m_StatusLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Label? 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";
|
||||||
}
|
}
|
||||||
88
godot/scripts/debug/DebugPanelController.cs
Normal file
88
godot/scripts/debug/DebugPanelController.cs
Normal file
@@ -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<DebugCommandNode>("/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;
|
||||||
|
}
|
||||||
1
godot/scripts/debug/DebugPanelController.cs.uid
Normal file
1
godot/scripts/debug/DebugPanelController.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cu534no72ttbm
|
||||||
284
godot/scripts/debug/DebugSandboxController.cs
Normal file
284
godot/scripts/debug/DebugSandboxController.cs
Normal file
@@ -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<DebugCommandNode>("/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<Label>("Main/Playfield/PlayfieldContent/MarkerLabel");
|
||||||
|
m_SpawnedActors = GetNodeOrNull<VBoxContainer>("Main/Playfield/PlayfieldContent/SpawnedActors");
|
||||||
|
m_LogLabel = GetNodeOrNull<Label>("Main/LogScroll/LogLabel");
|
||||||
|
m_StateLabel = GetNodeOrNull<Label>("Main/Playfield/PlayfieldContent/StateLabel");
|
||||||
|
|
||||||
|
DebugPanelController? panel = GetNodeOrNull<DebugPanelController>("Main/DebugPanel");
|
||||||
|
if (panel is not null && m_CommandNode is not null)
|
||||||
|
{
|
||||||
|
panel.Bind(m_CommandNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BindCommandService()
|
||||||
|
{
|
||||||
|
if (m_CommandNode is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugCommandService service = m_CommandNode.Service;
|
||||||
|
service.RegisterSpawnHandler(SpawnActor);
|
||||||
|
service.RegisterTimelineJumpHandler(JumpToMarker);
|
||||||
|
service.RegisterRestartHandler(RestartSandbox);
|
||||||
|
service.RegisterReloadHandler(() => DebugCommandResult.Success(DebugCommandId.ReloadScene, "Reload scene requested"));
|
||||||
|
service.CommandExecuted += HandleCommandExecuted;
|
||||||
|
service.StateChanged += _ => RefreshLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DebugCommandResult SpawnActor(string actorId)
|
||||||
|
{
|
||||||
|
Label label = new()
|
||||||
|
{
|
||||||
|
Text = $"{m_SpawnedActors?.GetChildCount() + 1 ?? 1}: {actorId}",
|
||||||
|
CustomMinimumSize = new Vector2(180.0f, 24.0f)
|
||||||
|
};
|
||||||
|
m_SpawnedActors?.AddChild(label);
|
||||||
|
return DebugCommandResult.Success(DebugCommandId.SpawnActor, $"Actor spawned: {actorId}", actorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DebugCommandResult JumpToMarker(string markerId)
|
||||||
|
{
|
||||||
|
if (m_MarkerLabel is not null)
|
||||||
|
{
|
||||||
|
m_MarkerLabel.Text = $"Marker: {markerId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return DebugCommandResult.Success(DebugCommandId.JumpToMarker, $"Timeline marker: {markerId}", markerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DebugCommandResult RestartSandbox()
|
||||||
|
{
|
||||||
|
if (m_SpawnedActors is not null)
|
||||||
|
{
|
||||||
|
foreach (Node child in m_SpawnedActors.GetChildren())
|
||||||
|
{
|
||||||
|
child.QueueFree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_MarkerLabel is not null)
|
||||||
|
{
|
||||||
|
m_MarkerLabel.Text = "Marker: none";
|
||||||
|
}
|
||||||
|
|
||||||
|
return DebugCommandResult.Success(DebugCommandId.RestartMission, "Sandbox restarted");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCommandExecuted(DebugCommandResult result)
|
||||||
|
{
|
||||||
|
string suffix = string.IsNullOrWhiteSpace(result.Argument) ? string.Empty : $" {result.Argument}";
|
||||||
|
AppendLog($"Command executed: {result.CommandId}{suffix}");
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
AppendLog($"Command failed: {result.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.CommandId is DebugCommandId.SpawnActor or DebugCommandId.JumpToMarker)
|
||||||
|
{
|
||||||
|
AppendLog(result.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshLabels()
|
||||||
|
{
|
||||||
|
if (m_CommandNode is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugRuntimeState state = m_CommandNode.Service.State;
|
||||||
|
if (m_StateLabel is not null)
|
||||||
|
{
|
||||||
|
m_StateLabel.Text = $"Difficulty: {state.ActiveDifficultyId} | Seed: {state.Seed} | Paused: {state.IsPaused} | Time: {state.TimeScale.ToString(CultureInfo.InvariantCulture)} | Spawns: {state.SpawnedActorCount}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_MarkerLabel is not null && string.IsNullOrWhiteSpace(state.CurrentMarkerId))
|
||||||
|
{
|
||||||
|
m_MarkerLabel.Text = "Marker: none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Execute(DebugCommandId commandId, string? argument = null)
|
||||||
|
{
|
||||||
|
m_CommandNode?.Execute(commandId, argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NextTimeScale(int direction)
|
||||||
|
{
|
||||||
|
if (m_CommandNode is null)
|
||||||
|
{
|
||||||
|
return "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
double current = m_CommandNode.Service.State.TimeScale;
|
||||||
|
int index = Array.IndexOf(s_TimeScales, current);
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
index = Array.IndexOf(s_TimeScales, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextIndex = Math.Clamp(index + direction, 0, s_TimeScales.Length - 1);
|
||||||
|
return s_TimeScales[nextIndex].ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunFoundationSmokeAsync()
|
||||||
|
{
|
||||||
|
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
|
||||||
|
AppendLog("Debug foundation smoke loaded");
|
||||||
|
|
||||||
|
bool succeeded = ExecuteAndRequire(DebugCommandId.Pause) && ExecuteAndRequire(DebugCommandId.SetTimeScale, "0.5") && ExecuteAndRequire(DebugCommandId.SetDifficulty, "difficulty.hard") && ExecuteAndRequire(DebugCommandId.SetSeed, m_CommandNode?.Service.State.Seed.ToString(CultureInfo.InvariantCulture)) && ExecuteAndRequire(DebugCommandId.SpawnActor, "enemy.serial") && ExecuteAndRequire(DebugCommandId.JumpToMarker, "cluster.opening") && ExecuteAndRequire(DebugCommandId.ToggleInvulnerability) && ExecuteAndRequire(DebugCommandId.ToggleInfiniteSpecialAmmo) && ExecuteAndRequire(DebugCommandId.ToggleNoEnemyFire) && ExecuteAndRequire(DebugCommandId.ToggleCollisionShapes) && ExecuteAndRequire(DebugCommandId.ToggleGameplayBounds) && ExecuteAndRequire(DebugCommandId.RestartMission) && ExecuteAndRequire(DebugCommandId.SpawnActor, "enemy.parallel") && VerifyFoundationSmokeState();
|
||||||
|
|
||||||
|
AppendLog(succeeded ? "Debug foundation smoke succeeded" : "Debug foundation smoke failed");
|
||||||
|
GetTree().Quit(succeeded ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ExecuteAndRequire(DebugCommandId commandId, string? argument = null)
|
||||||
|
{
|
||||||
|
if (m_CommandNode is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugCommandResult result = m_CommandNode.Execute(commandId, argument);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendLog(result.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool VerifyFoundationSmokeState()
|
||||||
|
{
|
||||||
|
if (m_CommandNode is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugRuntimeState state = m_CommandNode.Service.State;
|
||||||
|
return state.IsPaused && Math.Abs(state.TimeScale - 0.5) < 0.001 && state.ActiveDifficultyId == "difficulty.hard" && state.LastSpawnedActorId == "enemy.parallel" && state.SpawnedActorCount == 1 && state.RestartMissionRequestCount == 1 && state.Invulnerable && state.InfiniteSpecialAmmo && state.NoEnemyFire && state.ShowCollisionShapes && state.ShowGameplayBounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendLog(string message)
|
||||||
|
{
|
||||||
|
GD.Print(message);
|
||||||
|
if (m_LogLabel is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_LogLabel.Text = string.IsNullOrWhiteSpace(m_LogLabel.Text) ? message : $"{m_LogLabel.Text}\n{message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldRunFoundationSmoke()
|
||||||
|
{
|
||||||
|
return DisplayServer.GetName().Equals("headless", StringComparison.OrdinalIgnoreCase) && OS.GetCmdlineUserArgs().Any(argument => argument.Equals("--debug-script=foundation-smoke", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly double[] s_TimeScales = [0.25, 0.5, 1.0, 2.0, 4.0];
|
||||||
|
|
||||||
|
private DebugCommandNode? m_CommandNode;
|
||||||
|
private Label? m_MarkerLabel;
|
||||||
|
private VBoxContainer? m_SpawnedActors;
|
||||||
|
private Label? m_LogLabel;
|
||||||
|
private Label? m_StateLabel;
|
||||||
|
}
|
||||||
1
godot/scripts/debug/DebugSandboxController.cs.uid
Normal file
1
godot/scripts/debug/DebugSandboxController.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dcfnx61rcgnr6
|
||||||
24
godot/scripts/debug/commands/DebugCommandId.cs
Normal file
24
godot/scripts/debug/commands/DebugCommandId.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace SideScrollerGame.Debug.Commands;
|
||||||
|
|
||||||
|
public enum DebugCommandId
|
||||||
|
{
|
||||||
|
ToggleOverlay,
|
||||||
|
Pause,
|
||||||
|
Resume,
|
||||||
|
TogglePause,
|
||||||
|
FrameStep,
|
||||||
|
SetTimeScale,
|
||||||
|
ReloadScene,
|
||||||
|
RestartMission,
|
||||||
|
SetDifficulty,
|
||||||
|
SetSeed,
|
||||||
|
SpawnActor,
|
||||||
|
JumpToMarker,
|
||||||
|
ToggleCollisionShapes,
|
||||||
|
ToggleGameplayBounds,
|
||||||
|
ToggleInvulnerability,
|
||||||
|
ToggleInfiniteSpecialAmmo,
|
||||||
|
ToggleNoEnemyFire
|
||||||
|
}
|
||||||
1
godot/scripts/debug/commands/DebugCommandId.cs.uid
Normal file
1
godot/scripts/debug/commands/DebugCommandId.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ctqlux8vh57w8
|
||||||
16
godot/scripts/debug/commands/DebugCommandResult.cs
Normal file
16
godot/scripts/debug/commands/DebugCommandResult.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace SideScrollerGame.Debug.Commands;
|
||||||
|
|
||||||
|
public sealed record DebugCommandResult(DebugCommandId CommandId, bool Succeeded, string Message, string? Argument = null)
|
||||||
|
{
|
||||||
|
public static DebugCommandResult Success(DebugCommandId commandId, string message, string? argument = null)
|
||||||
|
{
|
||||||
|
return new DebugCommandResult(commandId, true, message, argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DebugCommandResult Failure(DebugCommandId commandId, string message, string? argument = null)
|
||||||
|
{
|
||||||
|
return new DebugCommandResult(commandId, false, message, argument);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
godot/scripts/debug/commands/DebugCommandResult.cs.uid
Normal file
1
godot/scripts/debug/commands/DebugCommandResult.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d1vtu2ro2libp
|
||||||
283
godot/scripts/debug/commands/DebugCommandService.cs
Normal file
283
godot/scripts/debug/commands/DebugCommandService.cs
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using SideScrollerGame.Content;
|
||||||
|
|
||||||
|
namespace SideScrollerGame.Debug.Commands;
|
||||||
|
|
||||||
|
public sealed class DebugCommandService
|
||||||
|
{
|
||||||
|
public DebugCommandService(ContentRegistry registry, int seed)
|
||||||
|
{
|
||||||
|
m_Registry = registry;
|
||||||
|
State = new DebugRuntimeState(FindDefaultDifficultyId(registry), seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DebugCommandResult Execute(DebugCommandId commandId, string? argument = null)
|
||||||
|
{
|
||||||
|
DebugCommandResult result = commandId switch
|
||||||
|
{
|
||||||
|
DebugCommandId.ToggleOverlay => ToggleOverlay(),
|
||||||
|
DebugCommandId.Pause => SetPaused(commandId, true),
|
||||||
|
DebugCommandId.Resume => SetPaused(commandId, false),
|
||||||
|
DebugCommandId.TogglePause => SetPaused(commandId, !State.IsPaused),
|
||||||
|
DebugCommandId.FrameStep => FrameStep(),
|
||||||
|
DebugCommandId.SetTimeScale => SetTimeScale(argument),
|
||||||
|
DebugCommandId.ReloadScene => ReloadScene(),
|
||||||
|
DebugCommandId.RestartMission => RestartMission(),
|
||||||
|
DebugCommandId.SetDifficulty => SetDifficulty(argument),
|
||||||
|
DebugCommandId.SetSeed => SetSeed(argument),
|
||||||
|
DebugCommandId.SpawnActor => SpawnActor(argument),
|
||||||
|
DebugCommandId.JumpToMarker => JumpToMarker(argument),
|
||||||
|
DebugCommandId.ToggleCollisionShapes => ToggleFlag(commandId, nameof(State.ShowCollisionShapes)),
|
||||||
|
DebugCommandId.ToggleGameplayBounds => ToggleFlag(commandId, nameof(State.ShowGameplayBounds)),
|
||||||
|
DebugCommandId.ToggleInvulnerability => ToggleFlag(commandId, nameof(State.Invulnerable)),
|
||||||
|
DebugCommandId.ToggleInfiniteSpecialAmmo => ToggleFlag(commandId, nameof(State.InfiniteSpecialAmmo)),
|
||||||
|
DebugCommandId.ToggleNoEnemyFire => ToggleFlag(commandId, nameof(State.NoEnemyFire)),
|
||||||
|
_ => DebugCommandResult.Failure(commandId, $"Unsupported debug command '{commandId}'.", argument)
|
||||||
|
};
|
||||||
|
|
||||||
|
CommandExecuted?.Invoke(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterSpawnHandler(Func<string, DebugCommandResult> handler)
|
||||||
|
{
|
||||||
|
m_SpawnHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterTimelineJumpHandler(Func<string, DebugCommandResult> handler)
|
||||||
|
{
|
||||||
|
m_TimelineJumpHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterReloadHandler(Func<DebugCommandResult> handler)
|
||||||
|
{
|
||||||
|
m_ReloadHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterRestartHandler(Func<DebugCommandResult> handler)
|
||||||
|
{
|
||||||
|
m_RestartHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DebugRuntimeState State { get; }
|
||||||
|
|
||||||
|
public event Action<DebugRuntimeState>? StateChanged;
|
||||||
|
|
||||||
|
public event Action<DebugCommandResult>? CommandExecuted;
|
||||||
|
|
||||||
|
private DebugCommandResult ToggleOverlay()
|
||||||
|
{
|
||||||
|
State.OverlayVisible = !State.OverlayVisible;
|
||||||
|
NotifyStateChanged();
|
||||||
|
return DebugCommandResult.Success(DebugCommandId.ToggleOverlay, $"Overlay visible: {State.OverlayVisible}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private DebugCommandResult SetPaused(DebugCommandId commandId, bool isPaused)
|
||||||
|
{
|
||||||
|
State.IsPaused = isPaused;
|
||||||
|
NotifyStateChanged();
|
||||||
|
return DebugCommandResult.Success(commandId, State.IsPaused ? "Paused" : "Resumed");
|
||||||
|
}
|
||||||
|
|
||||||
|
private DebugCommandResult FrameStep()
|
||||||
|
{
|
||||||
|
State.IsPaused = true;
|
||||||
|
State.FrameStepRequestCount++;
|
||||||
|
NotifyStateChanged();
|
||||||
|
return DebugCommandResult.Success(DebugCommandId.FrameStep, "Frame step requested");
|
||||||
|
}
|
||||||
|
|
||||||
|
private DebugCommandResult SetTimeScale(string? argument)
|
||||||
|
{
|
||||||
|
if (!double.TryParse(argument, NumberStyles.Float, CultureInfo.InvariantCulture, out double timeScale) || !s_SupportedTimeScales.Contains(timeScale))
|
||||||
|
{
|
||||||
|
return DebugCommandResult.Failure(DebugCommandId.SetTimeScale, $"Unsupported time scale '{argument}'.", argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
State.TimeScale = timeScale;
|
||||||
|
NotifyStateChanged();
|
||||||
|
return DebugCommandResult.Success(DebugCommandId.SetTimeScale, $"Time scale set to {timeScale.ToString(CultureInfo.InvariantCulture)}", argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DebugCommandResult ReloadScene()
|
||||||
|
{
|
||||||
|
State.ReloadSceneRequestCount++;
|
||||||
|
DebugCommandResult result = m_ReloadHandler?.Invoke() ?? DebugCommandResult.Success(DebugCommandId.ReloadScene, "Reload scene requested");
|
||||||
|
NotifyStateChanged();
|
||||||
|
return result.Succeeded ? DebugCommandResult.Success(DebugCommandId.ReloadScene, result.Message) : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DebugCommandResult RestartMission()
|
||||||
|
{
|
||||||
|
State.RestartMissionRequestCount++;
|
||||||
|
State.CurrentMarkerId = string.Empty;
|
||||||
|
State.LastSpawnedActorId = string.Empty;
|
||||||
|
State.SpawnedActorCount = 0;
|
||||||
|
DebugCommandResult result = m_RestartHandler?.Invoke() ?? DebugCommandResult.Success(DebugCommandId.RestartMission, "Restart mission requested");
|
||||||
|
NotifyStateChanged();
|
||||||
|
return result.Succeeded ? DebugCommandResult.Success(DebugCommandId.RestartMission, result.Message) : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DebugCommandResult SetDifficulty(string? argument)
|
||||||
|
{
|
||||||
|
string difficultyId = RequireArgument(argument);
|
||||||
|
if (difficultyId.Length == 0)
|
||||||
|
{
|
||||||
|
return DebugCommandResult.Failure(DebugCommandId.SetDifficulty, "Missing difficulty id.", argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_Registry.Difficulties.ContainsKey(difficultyId))
|
||||||
|
{
|
||||||
|
return DebugCommandResult.Failure(DebugCommandId.SetDifficulty, $"Unknown difficulty '{difficultyId}'.", difficultyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
State.ActiveDifficultyId = difficultyId;
|
||||||
|
NotifyStateChanged();
|
||||||
|
return DebugCommandResult.Success(DebugCommandId.SetDifficulty, $"Difficulty set to {difficultyId}", difficultyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DebugCommandResult SetSeed(string? argument)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(argument, NumberStyles.Integer, CultureInfo.InvariantCulture, out int seed))
|
||||||
|
{
|
||||||
|
return DebugCommandResult.Failure(DebugCommandId.SetSeed, $"Invalid seed '{argument}'.", argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
State.Seed = seed;
|
||||||
|
NotifyStateChanged();
|
||||||
|
return DebugCommandResult.Success(DebugCommandId.SetSeed, $"Seed set to {seed}", argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DebugCommandResult SpawnActor(string? argument)
|
||||||
|
{
|
||||||
|
string actorId = RequireArgument(argument);
|
||||||
|
if (actorId.Length == 0)
|
||||||
|
{
|
||||||
|
return DebugCommandResult.Failure(DebugCommandId.SpawnActor, "Missing actor id.", argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_Registry.EnemyTypes.ContainsKey(actorId))
|
||||||
|
{
|
||||||
|
return DebugCommandResult.Failure(DebugCommandId.SpawnActor, $"Unknown actor '{actorId}'.", actorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugCommandResult result = m_SpawnHandler?.Invoke(actorId) ?? DebugCommandResult.Success(DebugCommandId.SpawnActor, $"Spawn actor requested: {actorId}", actorId);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
State.LastSpawnedActorId = actorId;
|
||||||
|
State.SpawnedActorCount++;
|
||||||
|
NotifyStateChanged();
|
||||||
|
return DebugCommandResult.Success(DebugCommandId.SpawnActor, result.Message, actorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DebugCommandResult JumpToMarker(string? argument)
|
||||||
|
{
|
||||||
|
string markerId = RequireArgument(argument);
|
||||||
|
if (markerId.Length == 0)
|
||||||
|
{
|
||||||
|
return DebugCommandResult.Failure(DebugCommandId.JumpToMarker, "Missing marker id.", argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HasMissionMarker(markerId))
|
||||||
|
{
|
||||||
|
return DebugCommandResult.Failure(DebugCommandId.JumpToMarker, $"Unknown marker '{markerId}'.", markerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugCommandResult result = m_TimelineJumpHandler?.Invoke(markerId) ?? DebugCommandResult.Success(DebugCommandId.JumpToMarker, $"Jump marker requested: {markerId}", markerId);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
State.CurrentMarkerId = markerId;
|
||||||
|
NotifyStateChanged();
|
||||||
|
return DebugCommandResult.Success(DebugCommandId.JumpToMarker, result.Message, markerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DebugCommandResult ToggleFlag(DebugCommandId commandId, string propertyName)
|
||||||
|
{
|
||||||
|
string message = propertyName switch
|
||||||
|
{
|
||||||
|
nameof(State.ShowCollisionShapes) => ToggleShowCollisionShapes(),
|
||||||
|
nameof(State.ShowGameplayBounds) => ToggleShowGameplayBounds(),
|
||||||
|
nameof(State.Invulnerable) => ToggleInvulnerable(),
|
||||||
|
nameof(State.InfiniteSpecialAmmo) => ToggleInfiniteSpecialAmmo(),
|
||||||
|
nameof(State.NoEnemyFire) => ToggleNoEnemyFire(),
|
||||||
|
_ => $"Unsupported flag '{propertyName}'."
|
||||||
|
};
|
||||||
|
|
||||||
|
NotifyStateChanged();
|
||||||
|
return DebugCommandResult.Success(commandId, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ToggleShowCollisionShapes()
|
||||||
|
{
|
||||||
|
State.ShowCollisionShapes = !State.ShowCollisionShapes;
|
||||||
|
return $"Collision shapes: {State.ShowCollisionShapes}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ToggleShowGameplayBounds()
|
||||||
|
{
|
||||||
|
State.ShowGameplayBounds = !State.ShowGameplayBounds;
|
||||||
|
return $"Gameplay bounds: {State.ShowGameplayBounds}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ToggleInvulnerable()
|
||||||
|
{
|
||||||
|
State.Invulnerable = !State.Invulnerable;
|
||||||
|
return $"Invulnerable: {State.Invulnerable}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ToggleInfiniteSpecialAmmo()
|
||||||
|
{
|
||||||
|
State.InfiniteSpecialAmmo = !State.InfiniteSpecialAmmo;
|
||||||
|
return $"Infinite special ammo: {State.InfiniteSpecialAmmo}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ToggleNoEnemyFire()
|
||||||
|
{
|
||||||
|
State.NoEnemyFire = !State.NoEnemyFire;
|
||||||
|
return $"No enemy fire: {State.NoEnemyFire}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasMissionMarker(string markerId)
|
||||||
|
{
|
||||||
|
return m_Registry.MissionDefinitions.Any(mission => mission.TimelineMarkers.Contains(markerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotifyStateChanged()
|
||||||
|
{
|
||||||
|
StateChanged?.Invoke(State);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RequireArgument(string? argument)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(argument) ? string.Empty : argument.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FindDefaultDifficultyId(ContentRegistry registry)
|
||||||
|
{
|
||||||
|
if (registry.TryGetMission("mission.test", out var mission) && mission is not null && registry.Difficulties.ContainsKey(mission.DefaultDifficultyId))
|
||||||
|
{
|
||||||
|
return mission.DefaultDifficultyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return registry.DifficultyDefinitions.FirstOrDefault()?.Id ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly HashSet<double> s_SupportedTimeScales = [0.25, 0.5, 1.0, 2.0, 4.0];
|
||||||
|
|
||||||
|
private readonly ContentRegistry m_Registry;
|
||||||
|
private Func<string, DebugCommandResult>? m_SpawnHandler;
|
||||||
|
private Func<string, DebugCommandResult>? m_TimelineJumpHandler;
|
||||||
|
private Func<DebugCommandResult>? m_ReloadHandler;
|
||||||
|
private Func<DebugCommandResult>? m_RestartHandler;
|
||||||
|
}
|
||||||
1
godot/scripts/debug/commands/DebugCommandService.cs.uid
Normal file
1
godot/scripts/debug/commands/DebugCommandService.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bqt7rjgmg268u
|
||||||
44
godot/scripts/debug/commands/DebugRuntimeState.cs
Normal file
44
godot/scripts/debug/commands/DebugRuntimeState.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace SideScrollerGame.Debug.Commands;
|
||||||
|
|
||||||
|
public sealed class DebugRuntimeState
|
||||||
|
{
|
||||||
|
public DebugRuntimeState(string activeDifficultyId, int seed)
|
||||||
|
{
|
||||||
|
ActiveDifficultyId = activeDifficultyId;
|
||||||
|
Seed = seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OverlayVisible { get; internal set; } = true;
|
||||||
|
|
||||||
|
public bool IsPaused { get; internal set; }
|
||||||
|
|
||||||
|
public double TimeScale { get; internal set; } = 1.0;
|
||||||
|
|
||||||
|
public string ActiveDifficultyId { get; internal set; }
|
||||||
|
|
||||||
|
public int Seed { get; internal set; }
|
||||||
|
|
||||||
|
public string CurrentMarkerId { get; internal set; } = string.Empty;
|
||||||
|
|
||||||
|
public string LastSpawnedActorId { get; internal set; } = string.Empty;
|
||||||
|
|
||||||
|
public int SpawnedActorCount { get; internal set; }
|
||||||
|
|
||||||
|
public bool ShowCollisionShapes { get; internal set; }
|
||||||
|
|
||||||
|
public bool ShowGameplayBounds { get; internal set; }
|
||||||
|
|
||||||
|
public bool Invulnerable { get; internal set; }
|
||||||
|
|
||||||
|
public bool InfiniteSpecialAmmo { get; internal set; }
|
||||||
|
|
||||||
|
public bool NoEnemyFire { get; internal set; }
|
||||||
|
|
||||||
|
public int ReloadSceneRequestCount { get; internal set; }
|
||||||
|
|
||||||
|
public int RestartMissionRequestCount { get; internal set; }
|
||||||
|
|
||||||
|
public int FrameStepRequestCount { get; internal set; }
|
||||||
|
}
|
||||||
1
godot/scripts/debug/commands/DebugRuntimeState.cs.uid
Normal file
1
godot/scripts/debug/commands/DebugRuntimeState.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://iaejn5w2r8q1
|
||||||
176
tests/SideScrollerGame.Content.Tests/DebugCommandServiceTests.cs
Normal file
176
tests/SideScrollerGame.Content.Tests/DebugCommandServiceTests.cs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using SideScrollerGame.Content;
|
||||||
|
using SideScrollerGame.Content.Samples;
|
||||||
|
using SideScrollerGame.Debug.Commands;
|
||||||
|
|
||||||
|
namespace SideScrollerGame.Content.Tests;
|
||||||
|
|
||||||
|
public class DebugCommandServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void PauseResumeAndTogglePause_UpdateState()
|
||||||
|
{
|
||||||
|
DebugCommandService service = CreateService();
|
||||||
|
|
||||||
|
service.Execute(DebugCommandId.Pause);
|
||||||
|
Assert.True(service.State.IsPaused);
|
||||||
|
|
||||||
|
service.Execute(DebugCommandId.Resume);
|
||||||
|
Assert.False(service.State.IsPaused);
|
||||||
|
|
||||||
|
service.Execute(DebugCommandId.TogglePause);
|
||||||
|
Assert.True(service.State.IsPaused);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("0.25", 0.25)]
|
||||||
|
[InlineData("0.5", 0.5)]
|
||||||
|
[InlineData("1", 1.0)]
|
||||||
|
[InlineData("2", 2.0)]
|
||||||
|
[InlineData("4", 4.0)]
|
||||||
|
public void SetTimeScale_AcceptsSupportedValues(string argument, double expected)
|
||||||
|
{
|
||||||
|
DebugCommandService service = CreateService();
|
||||||
|
|
||||||
|
DebugCommandResult result = service.Execute(DebugCommandId.SetTimeScale, argument);
|
||||||
|
|
||||||
|
Assert.True(result.Succeeded);
|
||||||
|
Assert.Equal(expected, service.State.TimeScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetTimeScale_RejectsUnsupportedValue()
|
||||||
|
{
|
||||||
|
DebugCommandService service = CreateService();
|
||||||
|
|
||||||
|
DebugCommandResult result = service.Execute(DebugCommandId.SetTimeScale, "3");
|
||||||
|
|
||||||
|
Assert.False(result.Succeeded);
|
||||||
|
Assert.Equal(1.0, service.State.TimeScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetDifficulty_ValidatesIds()
|
||||||
|
{
|
||||||
|
DebugCommandService service = CreateService();
|
||||||
|
|
||||||
|
DebugCommandResult validResult = service.Execute(DebugCommandId.SetDifficulty, "difficulty.hard");
|
||||||
|
DebugCommandResult invalidResult = service.Execute(DebugCommandId.SetDifficulty, "difficulty.missing");
|
||||||
|
|
||||||
|
Assert.True(validResult.Succeeded);
|
||||||
|
Assert.Equal("difficulty.hard", service.State.ActiveDifficultyId);
|
||||||
|
Assert.False(invalidResult.Succeeded);
|
||||||
|
Assert.Equal("difficulty.hard", service.State.ActiveDifficultyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetSeed_UpdatesState()
|
||||||
|
{
|
||||||
|
DebugCommandService service = CreateService();
|
||||||
|
|
||||||
|
DebugCommandResult result = service.Execute(DebugCommandId.SetSeed, "333");
|
||||||
|
|
||||||
|
Assert.True(result.Succeeded);
|
||||||
|
Assert.Equal(333, service.State.Seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToggleFlags_UpdateState()
|
||||||
|
{
|
||||||
|
DebugCommandService service = CreateService();
|
||||||
|
|
||||||
|
service.Execute(DebugCommandId.ToggleCollisionShapes);
|
||||||
|
service.Execute(DebugCommandId.ToggleGameplayBounds);
|
||||||
|
service.Execute(DebugCommandId.ToggleInvulnerability);
|
||||||
|
service.Execute(DebugCommandId.ToggleInfiniteSpecialAmmo);
|
||||||
|
service.Execute(DebugCommandId.ToggleNoEnemyFire);
|
||||||
|
|
||||||
|
Assert.True(service.State.ShowCollisionShapes);
|
||||||
|
Assert.True(service.State.ShowGameplayBounds);
|
||||||
|
Assert.True(service.State.Invulnerable);
|
||||||
|
Assert.True(service.State.InfiniteSpecialAmmo);
|
||||||
|
Assert.True(service.State.NoEnemyFire);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SpawnActor_ValidatesActorAndRunsHandler()
|
||||||
|
{
|
||||||
|
DebugCommandService service = CreateService();
|
||||||
|
string spawnedActorId = string.Empty;
|
||||||
|
service.RegisterSpawnHandler(actorId =>
|
||||||
|
{
|
||||||
|
spawnedActorId = actorId;
|
||||||
|
return DebugCommandResult.Success(DebugCommandId.SpawnActor, $"Spawned {actorId}", actorId);
|
||||||
|
});
|
||||||
|
|
||||||
|
DebugCommandResult validResult = service.Execute(DebugCommandId.SpawnActor, "enemy.serial");
|
||||||
|
DebugCommandResult invalidResult = service.Execute(DebugCommandId.SpawnActor, "enemy.missing");
|
||||||
|
|
||||||
|
Assert.True(validResult.Succeeded);
|
||||||
|
Assert.Equal("enemy.serial", spawnedActorId);
|
||||||
|
Assert.Equal("enemy.serial", service.State.LastSpawnedActorId);
|
||||||
|
Assert.Equal(1, service.State.SpawnedActorCount);
|
||||||
|
Assert.False(invalidResult.Succeeded);
|
||||||
|
Assert.Equal(1, service.State.SpawnedActorCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JumpToMarker_ValidatesMarkerAndRunsHandler()
|
||||||
|
{
|
||||||
|
DebugCommandService service = CreateService();
|
||||||
|
string jumpedMarkerId = string.Empty;
|
||||||
|
service.RegisterTimelineJumpHandler(markerId =>
|
||||||
|
{
|
||||||
|
jumpedMarkerId = markerId;
|
||||||
|
return DebugCommandResult.Success(DebugCommandId.JumpToMarker, $"Jumped {markerId}", markerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
DebugCommandResult validResult = service.Execute(DebugCommandId.JumpToMarker, "cluster.opening");
|
||||||
|
DebugCommandResult invalidResult = service.Execute(DebugCommandId.JumpToMarker, "cluster.missing");
|
||||||
|
|
||||||
|
Assert.True(validResult.Succeeded);
|
||||||
|
Assert.Equal("cluster.opening", jumpedMarkerId);
|
||||||
|
Assert.Equal("cluster.opening", service.State.CurrentMarkerId);
|
||||||
|
Assert.False(invalidResult.Succeeded);
|
||||||
|
Assert.Equal("cluster.opening", service.State.CurrentMarkerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RestartMission_IncrementsRequestCountAndClearsRuntimeTargets()
|
||||||
|
{
|
||||||
|
DebugCommandService service = CreateService();
|
||||||
|
service.Execute(DebugCommandId.SpawnActor, "enemy.serial");
|
||||||
|
service.Execute(DebugCommandId.JumpToMarker, "cluster.opening");
|
||||||
|
|
||||||
|
DebugCommandResult result = service.Execute(DebugCommandId.RestartMission);
|
||||||
|
|
||||||
|
Assert.True(result.Succeeded);
|
||||||
|
Assert.Equal(1, service.State.RestartMissionRequestCount);
|
||||||
|
Assert.Equal(0, service.State.SpawnedActorCount);
|
||||||
|
Assert.Equal(string.Empty, service.State.LastSpawnedActorId);
|
||||||
|
Assert.Equal(string.Empty, service.State.CurrentMarkerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CommandEvents_FireForExecutedCommands()
|
||||||
|
{
|
||||||
|
DebugCommandService service = CreateService();
|
||||||
|
int stateChangedCount = 0;
|
||||||
|
int commandExecutedCount = 0;
|
||||||
|
service.StateChanged += _ => stateChangedCount++;
|
||||||
|
service.CommandExecuted += _ => commandExecutedCount++;
|
||||||
|
|
||||||
|
service.Execute(DebugCommandId.Pause);
|
||||||
|
service.Execute(DebugCommandId.SetDifficulty, "difficulty.missing");
|
||||||
|
|
||||||
|
Assert.Equal(1, stateChangedCount);
|
||||||
|
Assert.Equal(2, commandExecutedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DebugCommandService CreateService()
|
||||||
|
{
|
||||||
|
ContentRegistry registry = SampleContent.CreateRegistry();
|
||||||
|
return new DebugCommandService(registry, 123);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user