From a749d97b99a5b492a9a2c90b7f53a2ddca55dc12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20St=C4=99pie=C5=84?= Date: Sat, 22 Aug 2026 19:40:12 +0200 Subject: [PATCH] feat: add in-game debug console UI --- project.godot | 5 ++ .../Console/Godot/ConsoleController.cs | 83 +++++++++++++++++++ .../Console/Godot/ConsoleController.cs.uid | 1 + .../Console/Godot/ConsoleController.tscn | 71 ++++++++++++++++ src/Godot/Bootstrap/GameBootstrap.cs | 19 +++++ src/Godot/GamePause.cs | 29 +++++++ src/Godot/GamePause.cs.uid | 1 + src/Godot/Mappings/InputAction.cs | 4 +- src/Scenes/GameWorld.tscn | 6 +- 9 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 src/Features/Console/Godot/ConsoleController.cs create mode 100644 src/Features/Console/Godot/ConsoleController.cs.uid create mode 100644 src/Features/Console/Godot/ConsoleController.tscn create mode 100644 src/Godot/GamePause.cs create mode 100644 src/Godot/GamePause.cs.uid diff --git a/project.godot b/project.godot index 4b81689..71e7e24 100644 --- a/project.godot +++ b/project.godot @@ -56,6 +56,11 @@ 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":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) ] } +toggle_console={ +"deadzone": 0.2, +"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":4194332,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} [physics] diff --git a/src/Features/Console/Godot/ConsoleController.cs b/src/Features/Console/Godot/ConsoleController.cs new file mode 100644 index 0000000..57a0d17 --- /dev/null +++ b/src/Features/Console/Godot/ConsoleController.cs @@ -0,0 +1,83 @@ +using Game.Features.Console.Application; +using Game.Features.Console.Contracts; +using Game.Features.Console.Infrastructure; +using Game.Godot; +using Game.Godot.Extensions; +using Game.Godot.Mappings; +using Godot; + +namespace Game.Features.Console.Godot; + +public partial class ConsoleController : CanvasLayer +{ + [Export] public LineEdit CommandInput; + [Export] public RichTextLabel OutputLog; + + private CommandExecute _commandExecute = new(new CommandRegistry()); + private MouseCapture.Memento _mouseMemento; + + private void Toggle() + { + Visible = !Visible; + + if (Visible) + { + GamePause.Pause(); + _mouseMemento = MouseCapture.SaveCaptureMode(); + CommandInput.GrabFocus(); + } + else + { + GamePause.Resume(); + _mouseMemento.RestoreCaptureMode(); + } + } + + private void AppendLine(string line) + { + OutputLog.AppendText(line + "\n"); + } + + public void UseCommandRegistry(ICommandRegistry registry) + { + _commandExecute = new CommandExecute(registry); + } + + private void OnInputSubmitted(string text) + { + CommandInput.Clear(); + CommandInput.ReleaseFocus(); + CommandInput.GrabFocus(); + + if (text.Length == 0) return; + + AppendLine($"> {text}"); + + CommandResult result = _commandExecute.Execute(text); + if (result.ClearOutput) + { + OutputLog.Clear(); + return; + } + + if (result.Message.Length > 0) + { + AppendLine(result.Message); + } + } + + public override void _Ready() + { + this.AssertRequiredExports(); + Visible = false; + GamePause.SetIgnoresPause(this, true); + } + + public override void _Input(InputEvent @event) + { + if (!@event.IsActionPressed(InputAction.ToggleConsole.ActionName())) return; + + Toggle(); + GetViewport().SetInputAsHandled(); + } +} diff --git a/src/Features/Console/Godot/ConsoleController.cs.uid b/src/Features/Console/Godot/ConsoleController.cs.uid new file mode 100644 index 0000000..bfa30af --- /dev/null +++ b/src/Features/Console/Godot/ConsoleController.cs.uid @@ -0,0 +1 @@ +uid://ypsdpciag6r6 diff --git a/src/Features/Console/Godot/ConsoleController.tscn b/src/Features/Console/Godot/ConsoleController.tscn new file mode 100644 index 0000000..82e03c1 --- /dev/null +++ b/src/Features/Console/Godot/ConsoleController.tscn @@ -0,0 +1,71 @@ +[gd_scene format=3 uid="uid://bk0771dvonxh8"] + +[ext_resource type="Script" uid="uid://ypsdpciag6r6" path="res://src/Features/Console/Godot/ConsoleController.cs" id="1_script"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_console"] +content_margin_left = 8.0 +content_margin_top = 8.0 +content_margin_right = 8.0 +content_margin_bottom = 8.0 +bg_color = Color(0, 0, 0, 0.85) + +[sub_resource type="SystemFont" id="SystemFont_terminal"] +font_names = PackedStringArray("Consolas", "Cascadia Mono", "Courier New") + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_input"] +content_margin_left = 0.0 +content_margin_top = 12.0 +content_margin_right = 0.0 +content_margin_bottom = 0.0 +bg_color = Color(0, 0, 0, 0) + +[node name="ConsoleController" type="CanvasLayer" unique_id=1711862908 node_paths=PackedStringArray("CommandInput", "OutputLog")] +layer = 100 +script = ExtResource("1_script") +CommandInput = NodePath("Panel/Container/InputRow/CommandInput") +OutputLog = NodePath("Panel/Container/OutputLog") + +[node name="Panel" type="PanelContainer" parent="." unique_id=515760683] +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 260.0 +theme_override_styles/panel = SubResource("StyleBoxFlat_console") + +[node name="Container" type="VBoxContainer" parent="Panel" unique_id=37696848] +layout_mode = 2 +theme_override_constants/separation = 0 + +[node name="OutputLog" type="RichTextLabel" parent="Panel/Container" unique_id=1318894518] +custom_minimum_size = Vector2(0, 220) +layout_mode = 2 +size_flags_vertical = 3 +theme_override_fonts/normal_font = SubResource("SystemFont_terminal") +theme_override_fonts/bold_font = SubResource("SystemFont_terminal") +theme_override_fonts/italics_font = SubResource("SystemFont_terminal") +theme_override_fonts/mono_font = SubResource("SystemFont_terminal") +scroll_following = true + +[node name="InputRow" type="HBoxContainer" parent="Panel/Container" unique_id=1183500371] +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="PromptMargin" type="MarginContainer" parent="Panel/Container/InputRow" unique_id=1356750045] +layout_mode = 2 +theme_override_constants/margin_top = 12 + +[node name="PromptLabel" type="Label" parent="Panel/Container/InputRow/PromptMargin" unique_id=429447266] +layout_mode = 2 +theme_override_fonts/font = SubResource("SystemFont_terminal") +text = ">" + +[node name="CommandInput" type="LineEdit" parent="Panel/Container/InputRow" unique_id=209466737] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_fonts/font = SubResource("SystemFont_terminal") +theme_override_styles/normal = SubResource("StyleBoxFlat_input") +theme_override_styles/read_only = SubResource("StyleBoxFlat_input") +theme_override_styles/focus = SubResource("StyleBoxFlat_input") +keep_editing_on_text_submit = true +caret_blink = true + +[connection signal="text_submitted" from="Panel/Container/InputRow/CommandInput" to="." method="OnInputSubmitted"] diff --git a/src/Godot/Bootstrap/GameBootstrap.cs b/src/Godot/Bootstrap/GameBootstrap.cs index c2e8755..35d3897 100644 --- a/src/Godot/Bootstrap/GameBootstrap.cs +++ b/src/Godot/Bootstrap/GameBootstrap.cs @@ -1,4 +1,8 @@ +using Game.Features.Console.Application.Commands; +using Game.Features.Console.Godot; +using Game.Features.Console.Infrastructure; using Game.Godot; +using Game.Godot.Extensions; using Game.Godot.Mappings; using Godot; @@ -6,9 +10,24 @@ namespace Game.Godot.Bootstrap; public partial class GameBootstrap : Node { + [Export] public ConsoleController Console; + + private void RegisterConsoleCommands() + { + CommandRegistry registry = new(); + registry.Register(new HelpCommand(registry)); + registry.Register(new EchoCommand()); + registry.Register(new ClearCommand()); + + Console.UseCommandRegistry(registry); + } + public override void _Ready() { + this.AssertRequiredExports(); + MouseCapture.Capture(); + RegisterConsoleCommands(); } public override void _UnhandledInput(InputEvent @event) diff --git a/src/Godot/GamePause.cs b/src/Godot/GamePause.cs new file mode 100644 index 0000000..62cb150 --- /dev/null +++ b/src/Godot/GamePause.cs @@ -0,0 +1,29 @@ +using Godot; + +namespace Game.Godot; + +public static class GamePause +{ + private static SceneTree Tree => (SceneTree)Engine.GetMainLoop(); + + public static bool IsPaused => Tree.Paused; + + public static void Pause() => Tree.Paused = true; + + public static void Resume() => Tree.Paused = false; + + public static void Toggle() + { + if (IsPaused) + { + Resume(); + } + else + { + Pause(); + } + } + + public static void SetIgnoresPause(Node node, bool value) => + node.ProcessMode = value ? Node.ProcessModeEnum.Always : Node.ProcessModeEnum.Inherit; +} diff --git a/src/Godot/GamePause.cs.uid b/src/Godot/GamePause.cs.uid new file mode 100644 index 0000000..1f27192 --- /dev/null +++ b/src/Godot/GamePause.cs.uid @@ -0,0 +1 @@ +uid://i7fuhrb208r8 diff --git a/src/Godot/Mappings/InputAction.cs b/src/Godot/Mappings/InputAction.cs index 90bd096..f9a2dd4 100644 --- a/src/Godot/Mappings/InputAction.cs +++ b/src/Godot/Mappings/InputAction.cs @@ -9,7 +9,8 @@ public enum InputAction MoveForward, MoveBack, Jump, - Cancel + Cancel, + ToggleConsole } public static class InputActionExtensions @@ -25,6 +26,7 @@ public static class InputActionExtensions InputAction.MoveBack => "move_back", InputAction.Jump => "jump", InputAction.Cancel => "ui_cancel", + InputAction.ToggleConsole => "toggle_console", _ => throw new ArgumentOutOfRangeException(nameof(action), action, null) }; } diff --git a/src/Scenes/GameWorld.tscn b/src/Scenes/GameWorld.tscn index 38259ce..ffd0670 100644 --- a/src/Scenes/GameWorld.tscn +++ b/src/Scenes/GameWorld.tscn @@ -4,6 +4,7 @@ [ext_resource type="PackedScene" uid="uid://curuqmbndymi6" path="res://src/Scenes/FloorCeilingHr42.tscn" id="2_1kpcd"] [ext_resource type="PackedScene" uid="uid://bka1dmgdi5btd" path="res://src/Features/Player/Godot/PlayerNode.tscn" id="2_mvtit"] [ext_resource type="Script" uid="uid://drctu5y21ey2y" path="res://src/Godot/Bootstrap/GameBootstrap.cs" id="3_bootstrap"] +[ext_resource type="PackedScene" path="res://src/Features/Console/Godot/ConsoleController.tscn" id="4_console"] [sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_epykj"] panorama = ExtResource("1_g4n7o") @@ -16,8 +17,9 @@ background_mode = 2 sky = SubResource("Sky_1kpcd") glow_enabled = true -[node name="GameWorld" type="Node" unique_id=222644234] +[node name="GameWorld" type="Node" unique_id=222644234 node_paths=PackedStringArray("Console")] script = ExtResource("3_bootstrap") +Console = NodePath("Console") [node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1669211403] transform = Transform3D(-0.8660254, -0.43301278, 0.25, 0, 0.49999997, 0.86602545, -0.50000006, 0.75, -0.43301266, 0, 0, 0) @@ -29,6 +31,8 @@ environment = SubResource("Environment_mvtit") [node name="Player" parent="." unique_id=26823676 instance=ExtResource("2_mvtit")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +[node name="Console" parent="." instance=ExtResource("4_console")] + [node name="Entities" type="Node3D" parent="." unique_id=851492709] [node name="floor_ceiling_hr_46" parent="Entities" unique_id=33949496 instance=ExtResource("2_1kpcd")]