11 KiB
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
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.
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.
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.
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
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
DebugMenuCanvasLayerlayerproperty to100 -
Attach
res://nodes/debug_menu.gdas 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_textto"Enter command or expression..." -
Step 2: Commit
Task 7: Game Script
Files:
-
Create:
nodes/game.gd -
Step 1: Create the script
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:
[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'sprocess_modetoPROCESS_MODE_ALWAYSin the inspector. -
Instance
prefabs/debug_menu.tscnas a child namedDebugMenu. -
Add a plain
Nodechild namedActiveSceneContainer. -
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:
- Strategy phase loads as before inside ActiveSceneContainer.
- Press F1 — debug menu overlay appears, game underneath freezes.
- Press F1 again — debug menu hides, game resumes.
- Click a scene button — scene swaps, debug menu closes.
- Type
helpin command input, press Enter — lists commands in result label. - Type
list_scenes— shows scene names. - Type
swap main menu— swaps to main menu scene. - Type an expression like
2 + 2— shows4.
- Step 5: Commit