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