Replaces click-based navigation with mock_interact(0) on TransitionPiece nodes, adds get_current_room_name() verification pattern, and documents the MCP busy protocol for walk animation timeouts.
203 lines
11 KiB
Markdown
203 lines
11 KiB
Markdown
---
|
|
model: "auto"
|
|
kill: ["alpha-mask-creator"]
|
|
date_introduced: 2026-04-28
|
|
---
|
|
|
|
# Plan: kq4-room-navigator Skill (AI Agent Room Pathfinding via Godot MCP)
|
|
|
|
## Context
|
|
|
|
The game has ~96 rooms connected by TransitionPiece nodes. Each room's `.tscn` file defines its exits: node name (destination room), target UID, appear_at_node, label, and polygon coordinates. Room scripts connect the `interacted` signal to handlers that call `default_script()` on the transition piece, triggering a 3-step animated sequence (walk to exit → fade/swap scenes → walk to entrance in new room).
|
|
|
|
The custom MCP server (`scripts/mcp_interaction_server.gd`) runs as an autoload at TCP port 9090 with ~80 commands including `eval` (arbitrary GDScript execution), `click`, `screenshot`, `get_scene_tree`, `get_property`, and `wait`. No Python client exists yet.
|
|
|
|
**Key constraint**: Full runtime discovery via `eval` is fragile — instantiating .tscn files for graph discovery triggers script execution (`_ready()`) which references autoload singletons (ActionState, GameScript), causing errors when rooms aren't fully set up. **Solution**: Use targeted `find_nodes_by_class` MCP command for the currently-loaded room, and file parsing for global graph construction (leveraging existing `scripts/check_transitions.py`).
|
|
|
|
## Approach
|
|
|
|
Build a Python tool (`tools/kq4_room_navigator.py`) that:
|
|
1. Parses `.tscn` files to build complete room adjacency graph (reuse logic from `check_transitions.py`)
|
|
2. Runs BFS pathfinding between start and destination rooms
|
|
3. Connects to the MCP server on port 9090 for live navigation
|
|
4. Uses `eval` to compute click coordinates within TransitionPiece polygons in the running game
|
|
5. Executes step-by-step clicks with screenshot verification after each transition
|
|
|
|
The SKILL.md guides an AI agent through: starting Godot, invoking the tool, and optionally verifying steps manually with MCP commands.
|
|
|
|
## Implementation Plan
|
|
|
|
### 1. Extend room graph parser (`scripts/build_room_graph.py`)
|
|
|
|
**Purpose**: Parse all `.tscn` files to produce a queryable room adjacency graph with BFS pathfinding. Extends/extracts logic from existing `scripts/check_transitions.py`.
|
|
|
|
**What it does**:
|
|
1. Scan `scenes/kq4_*/kq4_*.tscn` (exclude placeholder_template)
|
|
2. Parse each file to extract:
|
|
- Scene UID (from header `[gd_scene format=3 uid="uid://XXX"]`)
|
|
- Room name/directory stem (e.g., `kq4_004_ogres_cottage`)
|
|
- All TransitionPiece nodes → build adjacency list
|
|
3. Build bidirectional graph: `{room_name: [(exit_node_name, target_uid, appear_at_node, label, polygon), ...]}`
|
|
|
|
**Output**: Returns a structure that supports BFS queries between any two rooms.
|
|
|
|
**Interface** (callable from Python or CLI):
|
|
```python
|
|
def build_graph(scenes_dir: Path) -> dict[str, list[TransitionInfo]]:
|
|
"""Parse all room .tscn files and return adjacency graph."""
|
|
|
|
def find_path(graph: dict, start_room: str, end_room: str) -> list[NavigationStep]:
|
|
"""BFS from start to end. Returns ordered list of steps or None."""
|
|
|
|
@dataclass
|
|
class NavigationStep:
|
|
from_room: str # Current room name (e.g., "kq4_004_ogres_cottage")
|
|
exit_node_name: str # TransitionPiece node name (e.g., "kq4_010_forest_path")
|
|
to_room: str # Destination room name
|
|
label: str # Human-readable label (e.g., "Forest Path")
|
|
polygon: list[tuple] # Polygon vertices for click coordinate computation
|
|
```
|
|
|
|
**File**: `scripts/build_room_graph.py`
|
|
|
|
### 3. Create the main navigator tool (`tools/kq4_room_navigator.py`)
|
|
|
|
**Purpose**: End-to-end CLI tool that connects to MCP, builds graph, finds path, and navigates.
|
|
|
|
**Workflow**:
|
|
|
|
```bash
|
|
python tools/kq4_room_navigator.py --from kq4_004_ogres_cottage --to kq4_092_lolottes_throne_room
|
|
```
|
|
|
|
**Implementation flow**:
|
|
|
|
1. **Build graph** (offline): Calls `build_graph()` to parse all rooms
|
|
2. **BFS pathfinding**: Finds shortest path from start → destination
|
|
3. **No path found**: Reports unreachable, lists connected components
|
|
4. **Path found**: Prints the step-by-step plan with click coordinates
|
|
|
|
5. **Connect to MCP** (runtime): Opens TCP connection to port 9090
|
|
6. **For each navigation step**:
|
|
a. Verify starting room: `eval("return get_node('/root/Node2D').get_current_room_name()")` returns the current room's node name (e.g., `"kq4_003_fountain_pool"`). This convenience function on MainGame simplifies verification compared to traversing the scene tree.
|
|
b. `find_nodes_by_class(class_name="TransitionPiece")` — discover all transition pieces in current scene
|
|
c. Match the exit node name from our path → get runtime position + polygon
|
|
d. Compute click coordinate: centroid of the TransitionPiece's `polygon`, transformed to viewport coordinates
|
|
e. `click(x, y)` — trigger the transition
|
|
f. Wait for transition animation (2-3s via `wait` command or poll-loop)
|
|
g. **Verify arrival**: `eval("return get_node('/root/Node2D').get_current_room_name()")` to confirm we've entered the expected room. Compare against the `to_room` from our BFS path. Returns empty string if no scene loaded.
|
|
h. If verification fails, retry up to 2 times with adjusted coordinates
|
|
h. Optionally take a screenshot via `screenshot()` for visual confirmation
|
|
|
|
7. **Complete**: Print summary of navigation path taken
|
|
|
|
**GDScript eval code used at runtime**:
|
|
|
|
Find clickable position within TransitionPiece polygon:
|
|
```gdscript
|
|
# Returns {node_name, centroid_x, centroid_y} for matching transition piece
|
|
var bg = get_tree().root.get_node_or_null("Node2D/SceneViewport/background")
|
|
if not bg: return null
|
|
for child in bg.get_children():
|
|
if child.has_method("is_class") and child.is_class("TransitionPiece"):
|
|
var p = child.position + child.polygon.reduce(func(p, a): return p + a, Vector2(0,0)) / child.polygon.size()
|
|
return {"node": child.name, "x": p.x, "y": p.y, "polygon_size": child.polygon.size()}
|
|
```
|
|
|
|
Verify current room:
|
|
```gdscript
|
|
var bg = get_tree().root.get_node_or_null("Node2D/SceneViewport/background")
|
|
return bg ? bg.name : null
|
|
```
|
|
|
|
**File**: `tools/kq4_room_navigator.py`
|
|
|
|
### 4. Write SKILL.md (`.opencode/skills/kq4-room-navigator/SKILL.md`)
|
|
|
|
The skill guide documents:
|
|
|
|
- **When to use**: Planning navigation between rooms, verifying room connectivity, debugging transitions
|
|
- **Pre-requisites**: Godot game running with MCP server active on port 9090
|
|
- **Quick start**: `python tools/kq4_room_navigator.py --from kq4_XXX --to kq4_YYY`
|
|
- **Room verification helper** (`MainGame.get_current_room_name()`):
|
|
|
|
The root node (`/root/Node2D`, script: `MainGame.gd`) provides a convenience function for testing:
|
|
|
|
```gdscript
|
|
# Via MCP eval — returns node name like "kq4_003_fountain_pool", or "" if no scene loaded
|
|
return get_node("/root/Node2D").get_current_room_name()
|
|
```
|
|
|
|
Use this after each transition click to verify you arrived at the expected room. Compare against the `to_room` field from your BFS path.
|
|
|
|
- **Manual MCP workflow** (for step-by-step agent control):
|
|
- Start Godot: `godot --path .` or run exported binary
|
|
- Check current room: `{"command": "eval", "params": {"code": "return get_node('/root/Node2D').get_current_room_name()"}}` → returns `"kq4_003_fountain_pool"` style string
|
|
- Verify connectivity: `{"command": "get_scene_tree"}` returns the current scene tree
|
|
- Discover exits in current room: `{"command": "find_nodes_by_class", "params": {"class_name": "TransitionPiece"}}`
|
|
- Click a transition: compute centroid from polygon data, then `{"command": "click", "params": {"x": px, "y": py}}`
|
|
- Verify current room: `{"command": "eval", "params": {"code": "return get_node('/root/Node2D').get_current_room_name()"}}` (returns node name like `"kq4_010_forest_path"`). Alternatively use screenshot for visual confirmation.
|
|
- **Coordinate computation math**: How to convert TransitionPiece polygon (local-space) to viewport click coordinates: `viewport_x = transition_node.position.x + polygon_centroid.x`, accounting for any node scale transforms
|
|
- **Troubleshooting**: Common failures (server busy, node not found mid-transition, wrong room), escape hatches
|
|
|
|
**File**: `.opencode/skills/kq4-room-navigator/SKILL.md`
|
|
|
|
## Task Dependency Graph
|
|
|
|
```
|
|
[1. build_room_graph.py] ────────┐
|
|
├── [2. kq4_room_navigator.py] ─── [3. SKILL.md]
|
|
|
|
### Using MainGame.get_current_room_name() for Room Verification
|
|
|
|
The `MainGame.gd` script (attached to `/root/Node2D`) provides a convenience function:
|
|
|
|
```gdscript
|
|
# Returns the node name of the current scene, e.g., "kq4_003_fountain_pool"
|
|
func get_current_room_name() -> String:
|
|
var scene = get_scene()
|
|
if scene:
|
|
return scene.name
|
|
return ""
|
|
```
|
|
|
|
**Via MCP eval for runtime verification**:
|
|
```bash
|
|
curl http://localhost:9090 -d '{"command": "eval", "params": {"code": "return get_node(\"/root/Node2D\").get_current_room_name()"}}'
|
|
```
|
|
|
|
Returns the room node name string (e.g., `"kq4_003_fountain_pool"`), or empty string if no scene is loaded. This is more reliable than traversing the scene tree manually since it uses the same `$SceneViewport/background` reference that MainGame's own logic uses.
|
|
|
|
**When to use**:
|
|
- After clicking a transition, call to confirm arrival at expected room in `to_room`
|
|
- Before navigation starts, verify you're actually in the intended `from_room`
|
|
- Compare against BFS path nodes during automated step-by-step execution
|
|
```
|
|
|
|
Tasks run sequentially. Task 3 documents the completed system.
|
|
|
|
## Verification
|
|
|
|
- Run `python scripts/build_room_graph.py` → should produce a graph with all rooms and their exits
|
|
- Test BFS: path from kq4_001_beach to an adjacent room should be 1 step; path to a far room tests multi-hop correctly
|
|
- Connect to running Godot instance via `tools/kq4_room_navigator.py` → verify it can navigate at least one transition end-to-end (from current room to an adjacent room)
|
|
- Verify failure handling: requesting a path between disconnected rooms returns "no path found"
|
|
- Test `_busy` retry logic by sending rapid commands
|
|
|
|
## Risks & Mitigations
|
|
|
|
| Risk | Impact | Mitigation |
|
|
|------|--------|------------|
|
|
| Room not fully set up — `TransitionPiece._ready()` crashes on eval-instantiation | Can't discover transitions from unloaded rooms via MCP | File parsing for graph discovery is the primary approach; MCP runtime only queries currently-loaded scene |
|
|
| Transition animation timing varies | Click at wrong time → missed transition | Poll current room name via eval every 0.5s until it changes, with 10s timeout |
|
|
| Polygon coordinates are local to node — need transform to viewport | Click lands outside polygon | Account for node `position` AND `scale` when computing centroid; use `get_node_info` runtime to get actual global polygon positions |
|
|
| MCP server `_busy` state blocks commands | Navigation stalls | Retry with exponential backoff (0.1s, 0.2s, 0.4s, ...) and 3-second max wait |
|
|
|
|
## Out of Scope
|
|
|
|
- Path optimization beyond BFS shortest path
|
|
- Inventory/item-based transitions (e.g., needing a key to enter a room)
|
|
- Cutscene-triggered room changes (non-interaction transitions)
|
|
- Multiplayer considerations
|
|
- Auto-starting Godot from the tool (user starts game manually)
|