Debug menu, reorganized game scene
This commit is contained in:
389
docs/superpowers/plans/2026-04-05-f1-debug-menu.md
Normal file
389
docs/superpowers/plans/2026-04-05-f1-debug-menu.md
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
# F1 Debug Menu Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add an F1-toggled debug overlay with scene swapping and a command console that works regardless of the active scene.
|
||||||
|
|
||||||
|
**Architecture:** A new `game.tscn` root scene hosts a `DebugMenu` CanvasLayer and an `ActiveSceneContainer` node. Scenes are loaded as children of the container. The command console uses a command pattern with explicit registration and an Expression eval fallback.
|
||||||
|
|
||||||
|
**Tech Stack:** Godot 4.6, GDScript
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-04-05-f1-debug-menu-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: ConsoleCommand Base Class
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `resources/resource_definitions/console_command.gd`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the base class**
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name ConsoleCommand extends RefCounted
|
||||||
|
|
||||||
|
func get_command_name() -> String:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
func get_help_text() -> String:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
func run(args: Array, context: Dictionary) -> String:
|
||||||
|
return ""
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: HelpCommand
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `resources/console_commands/help_command.gd`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the command**
|
||||||
|
|
||||||
|
`context["commands"]` will be the `Array[ConsoleCommand]` from DebugMenu.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name HelpCommand extends ConsoleCommand
|
||||||
|
|
||||||
|
func get_command_name() -> String:
|
||||||
|
return "help"
|
||||||
|
|
||||||
|
func get_help_text() -> String:
|
||||||
|
return "Lists all available commands"
|
||||||
|
|
||||||
|
func run(args: Array, context: Dictionary) -> String:
|
||||||
|
var commands: Array = context["commands"]
|
||||||
|
var lines: PackedStringArray = []
|
||||||
|
for command: ConsoleCommand in commands:
|
||||||
|
lines.append("%s - %s" % [command.get_command_name(), command.get_help_text()])
|
||||||
|
lines.append("Any other input is evaluated as a GDScript expression.")
|
||||||
|
return "\n".join(lines)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: ListScenesCommand
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `resources/console_commands/list_scenes_command.gd`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the command**
|
||||||
|
|
||||||
|
`context["scene_registry"]` will be the scene registry array from DebugMenu.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name ListScenesCommand extends ConsoleCommand
|
||||||
|
|
||||||
|
func get_command_name() -> String:
|
||||||
|
return "list_scenes"
|
||||||
|
|
||||||
|
func get_help_text() -> String:
|
||||||
|
return "Lists available scenes for swapping"
|
||||||
|
|
||||||
|
func run(args: Array, context: Dictionary) -> String:
|
||||||
|
var registry: Array = context["scene_registry"]
|
||||||
|
var lines: PackedStringArray = []
|
||||||
|
for entry: Dictionary in registry:
|
||||||
|
lines.append(entry["name"])
|
||||||
|
return "\n".join(lines)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: SwapCommand
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `resources/console_commands/swap_command.gd`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the command**
|
||||||
|
|
||||||
|
`context["debug_menu"]` will be the DebugMenu node reference, which has the `swap_scene()` method.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name SwapCommand extends ConsoleCommand
|
||||||
|
|
||||||
|
func get_command_name() -> String:
|
||||||
|
return "swap"
|
||||||
|
|
||||||
|
func get_help_text() -> String:
|
||||||
|
return "swap <name> - Swap to a scene by name (use list_scenes to see options)"
|
||||||
|
|
||||||
|
func run(args: Array, context: Dictionary) -> String:
|
||||||
|
if args.size() == 0:
|
||||||
|
return "Usage: swap <scene name>"
|
||||||
|
|
||||||
|
var search_name := " ".join(args).to_lower()
|
||||||
|
var registry: Array = context["scene_registry"]
|
||||||
|
for entry: Dictionary in registry:
|
||||||
|
if entry["name"].to_lower() == search_name:
|
||||||
|
var debug_menu: Node = context["debug_menu"]
|
||||||
|
debug_menu.call_deferred("swap_scene", entry)
|
||||||
|
return "Swapping to: %s" % entry["name"]
|
||||||
|
|
||||||
|
return "Scene not found: %s" % search_name
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: DebugMenu Script
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `nodes/debug_menu.gd`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the script**
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name DebugMenu extends CanvasLayer
|
||||||
|
|
||||||
|
signal scene_swapped
|
||||||
|
signal close_requested
|
||||||
|
|
||||||
|
var active_scene_container: Node
|
||||||
|
|
||||||
|
var scene_registry: Array = [
|
||||||
|
{ "name": "Strategy Phase", "path": "res://scenes/strategy_phase.tscn" },
|
||||||
|
{ "name": "Main Menu", "path": "res://scenes/main_menu.tscn" },
|
||||||
|
{ "name": "Visual Novel", "path": "res://scenes/vn_scene.tscn" },
|
||||||
|
{ "name": "Dialogue", "path": "res://scenes/dialogue_scene.tscn" },
|
||||||
|
]
|
||||||
|
|
||||||
|
var commands: Array[ConsoleCommand] = []
|
||||||
|
|
||||||
|
@onready var panel: PanelContainer = %Panel
|
||||||
|
@onready var scene_list: VBoxContainer = %SceneList
|
||||||
|
@onready var command_input: LineEdit = %CommandInput
|
||||||
|
@onready var result_label: Label = %ResultLabel
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
commands = [
|
||||||
|
HelpCommand.new(),
|
||||||
|
ListScenesCommand.new(),
|
||||||
|
SwapCommand.new(),
|
||||||
|
]
|
||||||
|
_build_scene_buttons()
|
||||||
|
command_input.text_submitted.connect(_on_command_submitted)
|
||||||
|
result_label.text = ""
|
||||||
|
|
||||||
|
func _build_scene_buttons() -> void:
|
||||||
|
for child in scene_list.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
for entry: Dictionary in scene_registry:
|
||||||
|
var button := Button.new()
|
||||||
|
button.text = entry["name"]
|
||||||
|
button.pressed.connect(swap_scene.bind(entry))
|
||||||
|
scene_list.add_child(button)
|
||||||
|
|
||||||
|
func swap_scene(entry: Dictionary) -> void:
|
||||||
|
for child in active_scene_container.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
var scene: PackedScene = load(entry["path"])
|
||||||
|
var instance := scene.instantiate()
|
||||||
|
active_scene_container.add_child(instance)
|
||||||
|
if entry.has("setup"):
|
||||||
|
_apply_setup(entry["setup"], instance)
|
||||||
|
close_requested.emit()
|
||||||
|
|
||||||
|
func _apply_setup(setup_key: String, scene_instance: Node) -> void:
|
||||||
|
match setup_key:
|
||||||
|
_:
|
||||||
|
push_warning("Unknown setup key: %s" % setup_key)
|
||||||
|
|
||||||
|
func _on_command_submitted(text: String) -> void:
|
||||||
|
command_input.text = ""
|
||||||
|
if text.strip_edges().is_empty():
|
||||||
|
return
|
||||||
|
var result := _execute_command(text.strip_edges())
|
||||||
|
_show_result(result)
|
||||||
|
|
||||||
|
func _execute_command(input: String) -> String:
|
||||||
|
var parts := input.split(" ", false)
|
||||||
|
var command_name := parts[0]
|
||||||
|
var args: Array = []
|
||||||
|
if parts.size() > 1:
|
||||||
|
args = Array(parts.slice(1))
|
||||||
|
|
||||||
|
var context := {
|
||||||
|
"commands": commands,
|
||||||
|
"scene_registry": scene_registry,
|
||||||
|
"debug_menu": self,
|
||||||
|
"active_scene_container": active_scene_container,
|
||||||
|
}
|
||||||
|
|
||||||
|
for command: ConsoleCommand in commands:
|
||||||
|
if command.get_command_name() == command_name:
|
||||||
|
return command.run(args, context)
|
||||||
|
|
||||||
|
return _eval_expression(input)
|
||||||
|
|
||||||
|
func _eval_expression(input: String) -> String:
|
||||||
|
var expression := Expression.new()
|
||||||
|
var error := expression.parse(input)
|
||||||
|
if error != OK:
|
||||||
|
return "Parse error: %s" % expression.get_error_text()
|
||||||
|
|
||||||
|
var active_scene: Node = null
|
||||||
|
if active_scene_container.get_child_count() > 0:
|
||||||
|
active_scene = active_scene_container.get_child(0)
|
||||||
|
|
||||||
|
var result = expression.execute([], active_scene)
|
||||||
|
if expression.has_execute_failed():
|
||||||
|
return "Error: %s" % expression.get_error_text()
|
||||||
|
|
||||||
|
return str(result)
|
||||||
|
|
||||||
|
func _show_result(text: String) -> void:
|
||||||
|
result_label.text = text
|
||||||
|
var tween := create_tween()
|
||||||
|
tween.tween_interval(2.0)
|
||||||
|
tween.tween_callback(func(): result_label.text = "")
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: DebugMenu Scene (Prefab)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `prefabs/debug_menu.tscn`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Build the scene in the Godot editor**
|
||||||
|
|
||||||
|
Create `prefabs/debug_menu.tscn` with this node tree:
|
||||||
|
|
||||||
|
```
|
||||||
|
DebugMenu (CanvasLayer, layer = 100, script = res://nodes/debug_menu.gd)
|
||||||
|
└── Panel (PanelContainer, unique name ✓)
|
||||||
|
├── anchors: full rect (anchor_left=0, anchor_top=0, anchor_right=1, anchor_bottom=1)
|
||||||
|
└── MarginContainer
|
||||||
|
└── VBoxContainer
|
||||||
|
├── Label (text = "Debug Menu", horizontal_alignment = CENTER)
|
||||||
|
├── HSeparator
|
||||||
|
├── Label (text = "Scenes:")
|
||||||
|
├── SceneList (VBoxContainer, unique name ✓)
|
||||||
|
├── HSeparator
|
||||||
|
├── Label (text = "Console:")
|
||||||
|
├── CommandInput (LineEdit, unique name ✓, placeholder_text = "Enter command or expression...")
|
||||||
|
└── ResultLabel (Label, unique name ✓, text = "")
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure to:
|
||||||
|
- Set `DebugMenu` CanvasLayer `layer` property to `100`
|
||||||
|
- Attach `res://nodes/debug_menu.gd` as the script
|
||||||
|
- Use "Access as Unique Name" (%) on: `Panel`, `SceneList`, `CommandInput`, `ResultLabel`
|
||||||
|
- Set `Panel`'s layout to "Full Rect" so it covers the viewport
|
||||||
|
- Set `CommandInput.placeholder_text` to `"Enter command or expression..."`
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Game Script
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `nodes/game.gd`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the script**
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name Game extends Node
|
||||||
|
|
||||||
|
@onready var debug_menu: DebugMenu = $DebugMenu
|
||||||
|
@onready var active_scene_container: Node = $ActiveSceneContainer
|
||||||
|
|
||||||
|
var _default_scene: PackedScene = preload("res://scenes/strategy_phase.tscn")
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
debug_menu.active_scene_container = active_scene_container
|
||||||
|
debug_menu.close_requested.connect(_close_debug_menu)
|
||||||
|
debug_menu.visible = false
|
||||||
|
var instance := _default_scene.instantiate()
|
||||||
|
active_scene_container.add_child(instance)
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event.is_action_pressed("debug_toggle"):
|
||||||
|
_toggle_debug_menu()
|
||||||
|
|
||||||
|
func _toggle_debug_menu() -> void:
|
||||||
|
if debug_menu.visible:
|
||||||
|
_close_debug_menu()
|
||||||
|
else:
|
||||||
|
_open_debug_menu()
|
||||||
|
|
||||||
|
func _open_debug_menu() -> void:
|
||||||
|
debug_menu.visible = true
|
||||||
|
active_scene_container.process_mode = Node.PROCESS_MODE_DISABLED
|
||||||
|
|
||||||
|
func _close_debug_menu() -> void:
|
||||||
|
debug_menu.visible = false
|
||||||
|
active_scene_container.process_mode = Node.PROCESS_MODE_INHERIT
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Game Scene and Project Config
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `scenes/game.tscn`
|
||||||
|
- Modify: `project.godot`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Register the F1 input action**
|
||||||
|
|
||||||
|
In Godot editor: Project > Project Settings > Input Map. Add a new action `debug_toggle` mapped to the F1 key.
|
||||||
|
|
||||||
|
Alternatively, add this to `project.godot` under a new `[input]` section:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[input]
|
||||||
|
|
||||||
|
debug_toggle={
|
||||||
|
"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)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(Physical keycode `4194332` is F1 in Godot.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build the game scene in the Godot editor**
|
||||||
|
|
||||||
|
Create `scenes/game.tscn` with this tree:
|
||||||
|
|
||||||
|
```
|
||||||
|
Game (Node, script = res://nodes/game.gd, process_mode = PROCESS_MODE_ALWAYS)
|
||||||
|
├── DebugMenu (instance of res://prefabs/debug_menu.tscn)
|
||||||
|
└── ActiveSceneContainer (Node)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Set `Game`'s `process_mode` to `PROCESS_MODE_ALWAYS` in the inspector.
|
||||||
|
- Instance `prefabs/debug_menu.tscn` as a child named `DebugMenu`.
|
||||||
|
- Add a plain `Node` child named `ActiveSceneContainer`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update project.godot main scene**
|
||||||
|
|
||||||
|
Change `run/main_scene` in `project.godot` to point to `scenes/game.tscn` (use Project > Project Settings > General > Application > Run > Main Scene in the editor, or update the UID reference manually).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Smoke test**
|
||||||
|
|
||||||
|
Run the project. Expected:
|
||||||
|
1. Strategy phase loads as before inside ActiveSceneContainer.
|
||||||
|
2. Press F1 — debug menu overlay appears, game underneath freezes.
|
||||||
|
3. Press F1 again — debug menu hides, game resumes.
|
||||||
|
4. Click a scene button — scene swaps, debug menu closes.
|
||||||
|
5. Type `help` in command input, press Enter — lists commands in result label.
|
||||||
|
6. Type `list_scenes` — shows scene names.
|
||||||
|
7. Type `swap main menu` — swaps to main menu scene.
|
||||||
|
8. Type an expression like `2 + 2` — shows `4`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
145
docs/superpowers/specs/2026-04-05-f1-debug-menu-design.md
Normal file
145
docs/superpowers/specs/2026-04-05-f1-debug-menu-design.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# F1 Debug Menu Design
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
An F1-toggled debug menu overlay that works regardless of the current scene. Introduces a new root scene (`game.tscn`) that hosts both the debug menu and the active game scene as children. The game pauses underneath while the debug menu is open.
|
||||||
|
|
||||||
|
## Scene Tree Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Game (Node, process_mode = PROCESS_MODE_ALWAYS)
|
||||||
|
├── DebugMenu (CanvasLayer, layer 100)
|
||||||
|
│ ├── Panel (PanelContainer)
|
||||||
|
│ │ ├── SceneList (VBoxContainer of Buttons)
|
||||||
|
│ │ └── CommandInput (LineEdit)
|
||||||
|
│ └── ResultLabel (Label, brief flash for command output)
|
||||||
|
└── ActiveSceneContainer (Node)
|
||||||
|
└── <currently loaded scene>
|
||||||
|
```
|
||||||
|
|
||||||
|
- `Game` is the new main scene, replacing `strategy_phase.tscn` as the entry point in `project.godot`.
|
||||||
|
- `DebugMenu` uses a high `CanvasLayer` (layer 100) so it always renders above the active scene.
|
||||||
|
- `ActiveSceneContainer` is a plain `Node` whose child gets swapped when changing scenes.
|
||||||
|
- `Game` has `process_mode = PROCESS_MODE_ALWAYS` so it can receive F1 input even while the tree is paused.
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
### Game (`game.gd`)
|
||||||
|
|
||||||
|
Thin shell only:
|
||||||
|
|
||||||
|
- F1 input toggles `DebugMenu` visibility.
|
||||||
|
- When debug menu opens: sets `ActiveSceneContainer.process_mode = PROCESS_MODE_DISABLED`.
|
||||||
|
- When debug menu closes: sets `ActiveSceneContainer.process_mode = PROCESS_MODE_INHERIT`.
|
||||||
|
- Loads the default scene (strategy_phase) into `ActiveSceneContainer` at startup.
|
||||||
|
- Passes a reference to `ActiveSceneContainer` to `DebugMenu` at `_ready()`.
|
||||||
|
|
||||||
|
### DebugMenu (`debug_menu.gd`)
|
||||||
|
|
||||||
|
Owns all debug functionality:
|
||||||
|
|
||||||
|
- **Scene registry**: hardcoded array of scene entries.
|
||||||
|
- **Scene swapping**: frees old child of `ActiveSceneContainer`, instantiates new scene, applies setup if present.
|
||||||
|
- **Command console**: parses input, dispatches to registered commands or falls back to expression eval.
|
||||||
|
- **Command registration**: holds an explicit `Array[ConsoleCommand]` populated at `_ready()`.
|
||||||
|
|
||||||
|
## Scene Swapping
|
||||||
|
|
||||||
|
### Registry Format
|
||||||
|
|
||||||
|
A hardcoded array in `DebugMenu`:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var scene_registry: Array = [
|
||||||
|
{ "name": "Strategy Phase", "path": "res://scenes/strategy_phase.tscn" },
|
||||||
|
{ "name": "Strategy Phase (Test Data)", "path": "res://scenes/strategy_phase.tscn", "setup": "test_data" },
|
||||||
|
{ "name": "Main Menu", "path": "res://scenes/main_menu.tscn" },
|
||||||
|
{ "name": "Visual Novel", "path": "res://scenes/vn_scene.tscn" },
|
||||||
|
{ "name": "Dialogue", "path": "res://scenes/dialogue_scene.tscn" },
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Swap Flow
|
||||||
|
|
||||||
|
1. Free the current child of `ActiveSceneContainer`.
|
||||||
|
2. Instantiate the new scene from `path` and add as child.
|
||||||
|
3. If the entry has a `"setup"` key, call a setup method on `DebugMenu` that applies that configuration to the new scene instance.
|
||||||
|
4. Close the debug menu and signal `Game` to unpause.
|
||||||
|
|
||||||
|
### Setup Hooks
|
||||||
|
|
||||||
|
Setup functions live in `DebugMenu`, dispatched by matching the `"setup"` string key. This keeps test configurations self-contained within the debug system.
|
||||||
|
|
||||||
|
## Command Console
|
||||||
|
|
||||||
|
### Input
|
||||||
|
|
||||||
|
A `LineEdit` at the bottom of the debug panel. On Enter:
|
||||||
|
|
||||||
|
1. Split input text — first token is the command name, rest are arguments.
|
||||||
|
2. Check against registered commands by matching `get_command_name()`.
|
||||||
|
3. If found, call `run()` with args and context.
|
||||||
|
4. If no match, fall back to Godot's `Expression` class (evaluated with the active scene as base instance).
|
||||||
|
5. Show the result (or error) in `ResultLabel`, then hide it after 2 seconds using a `Timer` or `create_tween()`.
|
||||||
|
|
||||||
|
### Command Pattern
|
||||||
|
|
||||||
|
Base class:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name ConsoleCommand
|
||||||
|
|
||||||
|
func get_command_name() -> String:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
func get_help_text() -> String:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
func run(args: Array, context: Dictionary) -> String:
|
||||||
|
return ""
|
||||||
|
```
|
||||||
|
|
||||||
|
Each command extends `ConsoleCommand` and overrides these three methods. The `context` dictionary provides access to the active scene container, debug menu reference, and other shared state without coupling commands to specific nodes.
|
||||||
|
|
||||||
|
Commands are explicitly registered in an `Array[ConsoleCommand]` in `DebugMenu._ready()`.
|
||||||
|
|
||||||
|
### Starter Commands
|
||||||
|
|
||||||
|
- **`help`** — lists all registered commands with their help text.
|
||||||
|
- **`swap <name>`** — scene swap by name (matches against registry entry names).
|
||||||
|
- **`list_scenes`** — prints available scene registry entries.
|
||||||
|
|
||||||
|
### Expression Eval Fallback
|
||||||
|
|
||||||
|
Uses Godot's `Expression` class. The expression executes with the current active scene as the base instance, allowing direct access to nodes and properties in the loaded scene. Errors are displayed in `ResultLabel`.
|
||||||
|
|
||||||
|
## File Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
nodes/
|
||||||
|
├── game.gd # Thin shell
|
||||||
|
├── debug_menu.gd # Scene registry, command console, swap logic
|
||||||
|
|
||||||
|
resources/resource_definitions/
|
||||||
|
├── console_command.gd # Base class
|
||||||
|
|
||||||
|
resources/console_commands/
|
||||||
|
├── swap_command.gd
|
||||||
|
├── help_command.gd
|
||||||
|
├── list_scenes_command.gd
|
||||||
|
|
||||||
|
scenes/
|
||||||
|
├── game.tscn # New root scene (new main_scene in project.godot)
|
||||||
|
|
||||||
|
prefabs/
|
||||||
|
├── debug_menu.tscn # CanvasLayer with panel, buttons, input, result label
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing scenes are unchanged — they are loaded as children of `ActiveSceneContainer` instead of being the root.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
- **No autoload**: The debug menu is a UI node, not a headless service. The `XXXServer` autoload pattern is reserved for game services like a future `SceneManagerServer`.
|
||||||
|
- **Explicit command registration**: Commands are listed in an array rather than auto-discovered from a folder. Simpler and more readable for a small number of commands. Can be swapped to auto-discovery later if needed.
|
||||||
|
- **Single-line output**: `ResultLabel` shows one result at a time with a brief display. No scrollable history for now.
|
||||||
|
- **Hardcoded scene list**: Entries can carry setup data/context, which auto-discovery from `.tscn` files wouldn't support.
|
||||||
105
nodes/debug_menu.gd
Normal file
105
nodes/debug_menu.gd
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
class_name DebugMenu extends CanvasLayer
|
||||||
|
|
||||||
|
signal close_requested
|
||||||
|
|
||||||
|
var active_scene_container: Node
|
||||||
|
|
||||||
|
var scene_registry: Array = [
|
||||||
|
{ "name": "Battle Test", "path": "res://scenes/strategy_phase.tscn" },
|
||||||
|
{ "name": "Main Menu", "path": "res://scenes/main_menu.tscn" },
|
||||||
|
{ "name": "Dialogue Test", "path": "res://scenes/dialogue_scene.tscn" },
|
||||||
|
]
|
||||||
|
|
||||||
|
var commands: Array[ConsoleCommand] = []
|
||||||
|
var _result_tween: Tween
|
||||||
|
|
||||||
|
@onready var panel: PanelContainer = %Panel
|
||||||
|
@onready var scene_list: VBoxContainer = %SceneList
|
||||||
|
@onready var command_input: LineEdit = %CommandInput
|
||||||
|
@onready var result_label: Label = %ResultLabel
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
commands = [
|
||||||
|
HelpCommand.new(),
|
||||||
|
ListScenesCommand.new(),
|
||||||
|
SwapCommand.new(),
|
||||||
|
]
|
||||||
|
_build_scene_buttons()
|
||||||
|
command_input.text_submitted.connect(_on_command_submitted)
|
||||||
|
result_label.text = ""
|
||||||
|
|
||||||
|
func _build_scene_buttons() -> void:
|
||||||
|
for child in scene_list.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
for entry: Dictionary in scene_registry:
|
||||||
|
var button := Button.new()
|
||||||
|
button.text = entry["name"]
|
||||||
|
button.pressed.connect(swap_scene.bind(entry))
|
||||||
|
scene_list.add_child(button)
|
||||||
|
|
||||||
|
func swap_scene(entry: Dictionary) -> void:
|
||||||
|
for child in active_scene_container.get_children():
|
||||||
|
active_scene_container.remove_child(child)
|
||||||
|
child.queue_free()
|
||||||
|
var scene: PackedScene = load(entry["path"])
|
||||||
|
var instance := scene.instantiate()
|
||||||
|
active_scene_container.add_child(instance)
|
||||||
|
if entry.has("setup"):
|
||||||
|
_apply_setup(entry["setup"], instance)
|
||||||
|
close_requested.emit()
|
||||||
|
|
||||||
|
func _apply_setup(setup_key: String, scene_instance: Node) -> void:
|
||||||
|
match setup_key:
|
||||||
|
_:
|
||||||
|
push_warning("Unknown setup key: %s" % setup_key)
|
||||||
|
|
||||||
|
func _on_command_submitted(text: String) -> void:
|
||||||
|
command_input.text = ""
|
||||||
|
if text.strip_edges().is_empty():
|
||||||
|
return
|
||||||
|
var result := _execute_command(text.strip_edges())
|
||||||
|
_show_result(result)
|
||||||
|
|
||||||
|
func _execute_command(input: String) -> String:
|
||||||
|
var parts := input.split(" ", false)
|
||||||
|
var command_name := parts[0]
|
||||||
|
var args: Array = []
|
||||||
|
if parts.size() > 1:
|
||||||
|
args = Array(parts.slice(1))
|
||||||
|
|
||||||
|
var context := {
|
||||||
|
"commands": commands,
|
||||||
|
"scene_registry": scene_registry,
|
||||||
|
"debug_menu": self,
|
||||||
|
"active_scene_container": active_scene_container,
|
||||||
|
}
|
||||||
|
|
||||||
|
for command: ConsoleCommand in commands:
|
||||||
|
if command.get_command_name() == command_name:
|
||||||
|
return command.run(args, context)
|
||||||
|
|
||||||
|
return _eval_expression(input)
|
||||||
|
|
||||||
|
func _eval_expression(input: String) -> String:
|
||||||
|
var expression := Expression.new()
|
||||||
|
var error := expression.parse(input)
|
||||||
|
if error != OK:
|
||||||
|
return "Parse error: %s" % expression.get_error_text()
|
||||||
|
|
||||||
|
var active_scene: Node = null
|
||||||
|
if active_scene_container.get_child_count() > 0:
|
||||||
|
active_scene = active_scene_container.get_child(0)
|
||||||
|
|
||||||
|
var result = expression.execute([], active_scene)
|
||||||
|
if expression.has_execute_failed():
|
||||||
|
return "Error: %s" % expression.get_error_text()
|
||||||
|
|
||||||
|
return str(result)
|
||||||
|
|
||||||
|
func _show_result(text: String) -> void:
|
||||||
|
result_label.text = text
|
||||||
|
if _result_tween and _result_tween.is_valid():
|
||||||
|
_result_tween.kill()
|
||||||
|
_result_tween = create_tween()
|
||||||
|
_result_tween.tween_interval(2.0)
|
||||||
|
_result_tween.tween_callback(func(): result_label.text = "")
|
||||||
1
nodes/debug_menu.gd.uid
Normal file
1
nodes/debug_menu.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c64yr8xvkb5cw
|
||||||
31
nodes/game.gd
Normal file
31
nodes/game.gd
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
class_name Game extends Node
|
||||||
|
|
||||||
|
@onready var debug_menu: DebugMenu = $DebugMenu
|
||||||
|
@onready var active_scene_container: Node = $ActiveSceneContainer
|
||||||
|
|
||||||
|
var _default_scene: PackedScene = preload("res://scenes/main_menu.tscn")
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
debug_menu.active_scene_container = active_scene_container
|
||||||
|
debug_menu.close_requested.connect(_close_debug_menu)
|
||||||
|
debug_menu.visible = false
|
||||||
|
var instance := _default_scene.instantiate()
|
||||||
|
active_scene_container.add_child(instance)
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event.is_action_pressed("debug_toggle"):
|
||||||
|
_toggle_debug_menu()
|
||||||
|
|
||||||
|
func _toggle_debug_menu() -> void:
|
||||||
|
if debug_menu.visible:
|
||||||
|
_close_debug_menu()
|
||||||
|
else:
|
||||||
|
_open_debug_menu()
|
||||||
|
|
||||||
|
func _open_debug_menu() -> void:
|
||||||
|
debug_menu.visible = true
|
||||||
|
active_scene_container.process_mode = Node.PROCESS_MODE_DISABLED
|
||||||
|
|
||||||
|
func _close_debug_menu() -> void:
|
||||||
|
debug_menu.visible = false
|
||||||
|
active_scene_container.process_mode = Node.PROCESS_MODE_INHERIT
|
||||||
1
nodes/game.gd.uid
Normal file
1
nodes/game.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ifv6cww6fk6c
|
||||||
59
prefabs/debug_menu.tscn
Normal file
59
prefabs/debug_menu.tscn
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
[gd_scene format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://nodes/debug_menu.gd" id="1_script"]
|
||||||
|
|
||||||
|
[node name="DebugMenu" type="CanvasLayer"]
|
||||||
|
layer = 100
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="Panel" type="PanelContainer" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="MarginContainer" type="MarginContainer" parent="Panel"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/margin_left = 12
|
||||||
|
theme_override_constants/margin_top = 12
|
||||||
|
theme_override_constants/margin_right = 12
|
||||||
|
theme_override_constants/margin_bottom = 12
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Debug Menu"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="HSeparator" type="HSeparator" parent="Panel/MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ScenesLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Scenes:"
|
||||||
|
|
||||||
|
[node name="SceneList" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="HSeparator2" type="HSeparator" parent="Panel/MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ConsoleLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Console:"
|
||||||
|
|
||||||
|
[node name="CommandInput" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Enter command or expression..."
|
||||||
|
|
||||||
|
[node name="ResultLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = ""
|
||||||
|
autowrap_mode = 3
|
||||||
@@ -11,7 +11,7 @@ config_version=5
|
|||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Dungeon Lords"
|
config/name="Dungeon Lords"
|
||||||
run/main_scene="uid://c35md0oc82je2"
|
run/main_scene="res://scenes/game.tscn"
|
||||||
config/features=PackedStringArray("4.6", "Mobile")
|
config/features=PackedStringArray("4.6", "Mobile")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
@@ -20,6 +20,14 @@ config/icon="res://icon.svg"
|
|||||||
window/size/viewport_width=800
|
window/size/viewport_width=800
|
||||||
window/size/viewport_height=600
|
window/size/viewport_height=600
|
||||||
|
|
||||||
|
[input]
|
||||||
|
|
||||||
|
debug_toggle={
|
||||||
|
"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]
|
[physics]
|
||||||
|
|
||||||
3d/physics_engine="Jolt Physics"
|
3d/physics_engine="Jolt Physics"
|
||||||
|
|||||||
15
resources/console_commands/help_command.gd
Normal file
15
resources/console_commands/help_command.gd
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
class_name HelpCommand extends ConsoleCommand
|
||||||
|
|
||||||
|
func get_command_name() -> String:
|
||||||
|
return "help"
|
||||||
|
|
||||||
|
func get_help_text() -> String:
|
||||||
|
return "Lists all available commands"
|
||||||
|
|
||||||
|
func run(args: Array, context: Dictionary) -> String:
|
||||||
|
var commands: Array = context["commands"]
|
||||||
|
var lines: PackedStringArray = []
|
||||||
|
for command: ConsoleCommand in commands:
|
||||||
|
lines.append("%s - %s" % [command.get_command_name(), command.get_help_text()])
|
||||||
|
lines.append("Any other input is evaluated as a GDScript expression.")
|
||||||
|
return "\n".join(lines)
|
||||||
1
resources/console_commands/help_command.gd.uid
Normal file
1
resources/console_commands/help_command.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bvat5xgudptct
|
||||||
14
resources/console_commands/list_scenes_command.gd
Normal file
14
resources/console_commands/list_scenes_command.gd
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
class_name ListScenesCommand extends ConsoleCommand
|
||||||
|
|
||||||
|
func get_command_name() -> String:
|
||||||
|
return "list_scenes"
|
||||||
|
|
||||||
|
func get_help_text() -> String:
|
||||||
|
return "Lists available scenes for swapping"
|
||||||
|
|
||||||
|
func run(args: Array, context: Dictionary) -> String:
|
||||||
|
var registry: Array = context["scene_registry"]
|
||||||
|
var lines: PackedStringArray = []
|
||||||
|
for entry: Dictionary in registry:
|
||||||
|
lines.append(entry["name"])
|
||||||
|
return "\n".join(lines)
|
||||||
1
resources/console_commands/list_scenes_command.gd.uid
Normal file
1
resources/console_commands/list_scenes_command.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b51b3np7lxd3v
|
||||||
21
resources/console_commands/swap_command.gd
Normal file
21
resources/console_commands/swap_command.gd
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
class_name SwapCommand extends ConsoleCommand
|
||||||
|
|
||||||
|
func get_command_name() -> String:
|
||||||
|
return "swap"
|
||||||
|
|
||||||
|
func get_help_text() -> String:
|
||||||
|
return "swap <name> - Swap to a scene by name (use list_scenes to see options)"
|
||||||
|
|
||||||
|
func run(args: Array, context: Dictionary) -> String:
|
||||||
|
if args.size() == 0:
|
||||||
|
return "Usage: swap <scene name>"
|
||||||
|
|
||||||
|
var search_name := " ".join(args).to_lower()
|
||||||
|
var registry: Array = context["scene_registry"]
|
||||||
|
for entry: Dictionary in registry:
|
||||||
|
if entry["name"].to_lower() == search_name:
|
||||||
|
var debug_menu: Node = context["debug_menu"]
|
||||||
|
debug_menu.swap_scene(entry)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return "Scene not found: %s" % search_name
|
||||||
1
resources/console_commands/swap_command.gd.uid
Normal file
1
resources/console_commands/swap_command.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b56j4uyjiaku1
|
||||||
10
resources/resource_definitions/console_command.gd
Normal file
10
resources/resource_definitions/console_command.gd
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class_name ConsoleCommand extends RefCounted
|
||||||
|
|
||||||
|
func get_command_name() -> String:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
func get_help_text() -> String:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
func run(args: Array, context: Dictionary) -> String:
|
||||||
|
return ""
|
||||||
1
resources/resource_definitions/console_command.gd.uid
Normal file
1
resources/resource_definitions/console_command.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b2kk8l3kumxpr
|
||||||
12
scenes/game.tscn
Normal file
12
scenes/game.tscn
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[gd_scene format=3 uid="uid://gfrxev22t0bc"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://ifv6cww6fk6c" path="res://nodes/game.gd" id="1_script"]
|
||||||
|
[ext_resource type="PackedScene" path="res://prefabs/debug_menu.tscn" id="2_debug_menu"]
|
||||||
|
|
||||||
|
[node name="Game" type="Node" unique_id=906681388]
|
||||||
|
process_mode = 1
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="DebugMenu" parent="." unique_id=486799252 instance=ExtResource("2_debug_menu")]
|
||||||
|
|
||||||
|
[node name="ActiveSceneContainer" type="Node" parent="." unique_id=2133680613]
|
||||||
Reference in New Issue
Block a user