# 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 - 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 " 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**