Compare commits
1 Commits
master
...
flat-jeans
| Author | SHA1 | Date | |
|---|---|---|---|
| 39c0887e22 |
24
.opencode/package-lock.json
generated
24
.opencode/package-lock.json
generated
@@ -5,7 +5,7 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/plugin": "1.14.29"
|
"@opencode-ai/plugin": "1.14.27"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||||
@@ -87,13 +87,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@opencode-ai/plugin": {
|
"node_modules/@opencode-ai/plugin": {
|
||||||
"version": "1.14.29",
|
"version": "1.14.27",
|
||||||
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.14.29.tgz",
|
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.14.27.tgz",
|
||||||
"integrity": "sha512-GwmBg7dajawma/6tzpoK/JMbcRcUTg27XHlnxZOFWG85WDz4M67hxFYnvE+BnJj9k7tTLXTfSR+pgfcdqbUDAg==",
|
"integrity": "sha512-8J8JkrInF5oWaR2rsLuwQktdF9Yq3xoyA8B42/B8Te74/q4rqOt7YzWK2I/ZSxvKA/Ct+iQ8f2OeUrpQ2INgSw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "1.14.29",
|
"@opencode-ai/sdk": "1.14.27",
|
||||||
"effect": "4.0.0-beta.57",
|
"effect": "4.0.0-beta.48",
|
||||||
"zod": "4.1.8"
|
"zod": "4.1.8"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -110,9 +110,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@opencode-ai/sdk": {
|
"node_modules/@opencode-ai/sdk": {
|
||||||
"version": "1.14.29",
|
"version": "1.14.27",
|
||||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.14.29.tgz",
|
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.14.27.tgz",
|
||||||
"integrity": "sha512-y6wNTlHhgfwLdp01EwdnMFVxUS1FLgz7MZh7H3+jROG2v02GqGDy/gPH3ME0kI+sqQ4qSlk/9AJ+YbKAruPaZw==",
|
"integrity": "sha512-mpzDFDAGi+8wKEcm4aP0HVtS56rN/q2hVs8Ai6JziPu7NuTMddfFoEvddArYsgkRWUfHL5ypZc1mDmAMEiO1vg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "7.0.6"
|
"cross-spawn": "7.0.6"
|
||||||
@@ -149,9 +149,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/effect": {
|
"node_modules/effect": {
|
||||||
"version": "4.0.0-beta.57",
|
"version": "4.0.0-beta.48",
|
||||||
"resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.57.tgz",
|
"resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz",
|
||||||
"integrity": "sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g==",
|
"integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.1.0",
|
"@standard-schema/spec": "^1.1.0",
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
---
|
|
||||||
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)
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: gitea-tea
|
name: gitea-tea
|
||||||
description: Use tea CLI to create, manage, and checkout Gitea pull requests. Use this when opening a PR, managing PRs, or checking out PRs on the gitea remote (gitea.story-basking.ts.net).
|
description: Use tea CLI to create, manage, and checkout Gitea pull requests. Use this when opening a PR, managing PRs, or checking out PRs on the Gitea remote (gitea.story-basking.ts.net).
|
||||||
---
|
---
|
||||||
|
|
||||||
# Gitea Tea CLI Skill
|
# Gitea Tea CLI Skill
|
||||||
@@ -10,27 +10,36 @@ This skill covers using `tea` (Gitea's official CLI) for pull request workflows
|
|||||||
## When to Use This Skill
|
## When to Use This Skill
|
||||||
|
|
||||||
Use this skill when you need to:
|
Use this skill when you need to:
|
||||||
- Create a PR from a working branch to master on the gitea remote
|
- Create a PR from a working branch to master on the Gitea remote
|
||||||
- Open, list, or view PRs
|
- Open, list, or view PRs
|
||||||
- Checkout a PR locally for review or iteration
|
- Checkout a PR locally for review or iteration
|
||||||
- Manage PR state (close, reopen, merge)
|
- Manage PR state (close, reopen, merge)
|
||||||
|
|
||||||
## Project Setup
|
## Project Setup
|
||||||
|
|
||||||
The gitea remote is `gitea.story-basking.ts.net` with repo slug `notid/integreat`. The default push remote is **gitea**, NOT origin and NOT deploy.
|
The git remote is `origin` pointing to `git@gitea:notid/ai-game-2.git`. The repo slug is `notid/ai-game-2`.
|
||||||
|
|
||||||
In this project's environment:
|
In this project's environment:
|
||||||
- Gitea login is pre-configured for `gitea.story-basking.ts.net`
|
- Gitea login is pre-configured for `gitea.story-basking.ts.net`
|
||||||
- Repo slug: `notid/integreat`
|
- Repo slug: `notid/ai-game-2`
|
||||||
- Target branch for PRs: `master`
|
- Target branch for PRs: `master`
|
||||||
- The git remote named `gitea` points to this instance
|
- The git remote named `origin` points to this instance
|
||||||
|
|
||||||
|
## Key Flags
|
||||||
|
|
||||||
|
All tea subcommands support these flags for repo and auth context:
|
||||||
|
- `-r notid/ai-game-2` - Override repo slug (required when auto-discovery fails, e.g. in worktrees)
|
||||||
|
- `-R origin` - Discover Gitea login from a specific git remote
|
||||||
|
- `-l <username>` - Use a different Gitea login
|
||||||
|
|
||||||
|
In practice, you usually need just `-r notid/ai-game-2` on the subcommand you're running.
|
||||||
|
|
||||||
## Creating a PR
|
## Creating a PR
|
||||||
|
|
||||||
Use `tea pulls create` to open a PR from the current branch to master. Always specify `-r notid/integreat -b master`:
|
Use `tea pulls create` to open a PR from the current branch to master:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tea pulls create -r notid/integreat -b master --title "Title" --description "Body"
|
tea pulls create -r notid/ai-game-2 -b master -t "Title" -d "Body"
|
||||||
```
|
```
|
||||||
|
|
||||||
Common flags:
|
Common flags:
|
||||||
@@ -43,7 +52,7 @@ Common flags:
|
|||||||
**Writing a multiline description:**
|
**Writing a multiline description:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tea pulls create -r notid/integreat -b master \
|
tea pulls create -r notid/ai-game-2 -b master \
|
||||||
-t "feat: add feature" \
|
-t "feat: add feature" \
|
||||||
-d "$(cat <<'EOF'
|
-d "$(cat <<'EOF'
|
||||||
## Summary
|
## Summary
|
||||||
@@ -58,22 +67,22 @@ Or write the body to a temp file first and reference it.
|
|||||||
## Listing PRs
|
## Listing PRs
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tea pulls list -r notid/integreat # List open PRs
|
tea pulls list -r notid/ai-game-2 # List open PRs
|
||||||
tea pulls list -r notid/integreat --state all # All PRs
|
tea pulls list -r notid/ai-game-2 --state all # All PRs
|
||||||
tea pulls list -r notid/integreat --limit 10 -o simple # Limit output, simple format
|
tea pulls list -r notid/ai-game-2 --limit 10 -o simple # Limit output, simple format
|
||||||
```
|
```
|
||||||
|
|
||||||
## Opening a PR in Browser
|
## Opening a PR in Browser
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tea open pr <number> -r notid/integreat
|
tea open pr <number> -r notid/ai-game-2
|
||||||
tea open pr create -r notid/integreat # Open web UI to create a PR
|
tea open pr create -r notid/ai-game-2 # Open web UI to create a PR
|
||||||
```
|
```
|
||||||
|
|
||||||
## Checking Out a PR Locally
|
## Checking Out a PR Locally
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tea pulls checkout <number> -r notid/integreat
|
tea pulls checkout <number> -r notid/ai-game-2
|
||||||
```
|
```
|
||||||
|
|
||||||
This fetches and checks out the PR branch locally.
|
This fetches and checks out the PR branch locally.
|
||||||
@@ -82,46 +91,61 @@ This fetches and checks out the PR branch locally.
|
|||||||
|
|
||||||
**Close a PR:**
|
**Close a PR:**
|
||||||
```bash
|
```bash
|
||||||
tea pulls close <number> -r notid/integreat --confirm
|
tea pulls close <number> -r notid/ai-game-2 --confirm
|
||||||
```
|
```
|
||||||
|
|
||||||
**Reopen a closed PR:**
|
**Reopen a closed PR:**
|
||||||
```bash
|
```bash
|
||||||
tea pulls reopen <number> -r notid/integreat --confirm
|
tea pulls reopen <number> -r notid/ai-game-2 --confirm
|
||||||
```
|
```
|
||||||
|
|
||||||
**Merge a PR:**
|
**Merge a PR:**
|
||||||
```bash
|
```bash
|
||||||
tea pulls merge <number> -r notid/integreat --confirm
|
tea pulls merge <number> -r notid/ai-game-2 --confirm
|
||||||
```
|
```
|
||||||
|
|
||||||
**Edit a PR (title, description, etc.):**
|
**Edit a PR (title, description, etc.):**
|
||||||
```bash
|
```bash
|
||||||
tea pulls edit <number> -r notid/integreat --title "New title" --description "New body"
|
tea pulls edit <number> -r notid/ai-game-2 -t "New title" -d "New body"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Full PR Creation Workflow
|
## Full PR Creation Workflow
|
||||||
|
|
||||||
1. Ensure the branch is pushed to gitea:
|
1. Commit all changes on your branch:
|
||||||
```bash
|
```bash
|
||||||
git push gitea <branch-name>
|
git add . && git commit -m "describe the change"
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Create the PR with tea:
|
2. Push the branch to origin:
|
||||||
```bash
|
```bash
|
||||||
tea pulls create -r notid/integreat -b master \
|
git push origin <branch-name>
|
||||||
--title "feat: description of change" \
|
|
||||||
--description "Detailed PR body here"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Open the PR in browser to verify:
|
3. Create the PR with tea:
|
||||||
```bash
|
```bash
|
||||||
tea open pr <number> -r notid/integreat
|
tea pulls create -r notid/ai-game-2 -b master \
|
||||||
|
-t "feat: description of change" \
|
||||||
|
-d "Detailed PR body here"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
4. Open the PR in browser to verify:
|
||||||
|
```bash
|
||||||
|
tea open pr <number> -r notid/ai-game-2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Worktree Gotcha
|
||||||
|
|
||||||
|
When running from a git worktree, tea may fail to auto-discover the repo. Always pass `-r notid/ai-game-2` explicitly in that case:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tea pulls list -r notid/ai-game-2 # Works in worktrees
|
||||||
|
tea pulls create -r notid/ai-game-2 ... # Works in worktrees
|
||||||
|
```
|
||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
|
|
||||||
- Always use `-r notid/integreat` to specify the repo explicitly
|
- Always use `-r notid/ai-game-2` to specify the repo explicitly, especially in worktrees
|
||||||
- Use `-b master` to set the target branch (default may differ)
|
- Use `-b master` to set the target branch (default may differ)
|
||||||
- The `--confirm` flag is required for destructive actions (close, merge)
|
- The `--confirm` flag is required for destructive actions (close, merge)
|
||||||
- Use `-o simple`, `-o json`, `-o table`, etc. to control output format
|
- Use `-o simple`, `-o json`, `-o table`, etc. to control output format
|
||||||
|
- `tea whoami` verifies your authentication before running PR commands
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
---
|
|
||||||
name: kq4-room-navigator
|
|
||||||
description: Navigate between KQ4 rooms using the Godot MCP server. BFS pathfinds through room transition graph, then the agent navigates McpInteractionServer (port 9090). Use when planning navigation between any two rooms or simulating player movement through the game world at runtime. Trigger phrases: "navigate from", "go to room", "path from X to Y", "walk to", "how do I get to room", "room navigation".
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
# KQ4 Room Navigator
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This skill finds shortest paths between rooms and can execute them through the running game using Godot's MCP server. It combines offline `.tscn` file parsing for graph construction with runtime interaction via McpInteractionServer (TCP port 9090).
|
|
||||||
|
|
||||||
The room transition system uses TransitionPiece nodes: each has an `exit_node_name` (destination), a polygon, and connects rooms typically bidirectionally. Clicking a TransitionPiece triggers a 3-step animation (walk → fade/swap scenes → walk to entrance in new room).
|
|
||||||
|
|
||||||
## When to Use
|
|
||||||
|
|
||||||
- Planning multi-room navigation paths
|
|
||||||
- Verifying room connectivity
|
|
||||||
- Debugging why a path can't be traversed
|
|
||||||
- Automating room transitions during testing
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
```
|
|
||||||
.scenes/*.tscn files → adjacency graph (96 rooms, 233 exits) → BFS pathfinding → use MCP for execution
|
|
||||||
```
|
|
||||||
|
|
||||||
Room identification at runtime:
|
|
||||||
|
|
||||||
- Game node tree: `/root/Node2D/SceneViewport/background` (always named "background")
|
|
||||||
- Room identity comes from `background.get_script().resource_path` (e.g., `res://scenes/kq4_010_forest_path/kq4_010_forest_path.gd`)
|
|
||||||
- TransitionPiece children have `.target` UIDs and `.label` strings for cross-referencing
|
|
||||||
|
|
||||||
### Getting the current room name at runtime
|
|
||||||
|
|
||||||
The canonical way is via `MainGame.get_current_room_name()`:
|
|
||||||
|
|
||||||
Use `godot_game_call_method` to call it directly on the node path:
|
|
||||||
```json
|
|
||||||
Call method get_current_room_name on /root/Node2D
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns a string like `"kq4_010_forest_path"`. The implementation extracts the filename from the scene script's resource path (`script_path.trim_suffix(".gd").get_file()`), NOT the node name (which is always `"background"`).
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Step 1 — Make a plan:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python tools/kq4_room_navigator.py --from kq4_003_fountain_pool --to kq4_011_enchanted_grove
|
|
||||||
```
|
|
||||||
|
|
||||||
Output:
|
|
||||||
|
|
||||||
```
|
|
||||||
Path: kq4_003_fountain_pool → kq4_011_enchanted_grove (3 steps)
|
|
||||||
1. Click 'kq4_004_ogres_cottage' in kq4_003_fountain_pool → kq4_004_ogres_cottage [Ogre's Cottage] (click at: 1874, 714)
|
|
||||||
2. Click 'kq4_010_forest_path' in kq4_004_ogres_cottage → kq4_010_forest_path [Forest Path] (click at: 1042, 1078)
|
|
||||||
3. Click 'kq4_011_enchanted_grove' in kq4_010_forest_path → kq4_011_enchanted_grove [Enchanted Grove] (click at: 1898, 610)
|
|
||||||
```
|
|
||||||
|
|
||||||
The click coordinates are not used — the `exit_node_name` is what matters.
|
|
||||||
|
|
||||||
### Step 2 — Launch and navigate
|
|
||||||
|
|
||||||
1. Use the Godot MCP to start the game (`godot_run_project`)
|
|
||||||
2. Poll room name to verify starting room:
|
|
||||||
```
|
|
||||||
get_tree().root.get_node('Node2D').get_current_room_name()
|
|
||||||
```
|
|
||||||
3. For each step, call **`mock_interact(0)`** on the TransitionPiece node (see exact method name below)
|
|
||||||
4. Wait and poll until `get_current_room_name()` returns the expected destination
|
|
||||||
5. Repeat for next step
|
|
||||||
|
|
||||||
## Detailed Navigation Protocol
|
|
||||||
|
|
||||||
### Identify current room
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "eval", "params": {"code": "get_tree().root.get_node('Node2D').get_current_room_name()"}}
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns `"kq4_0xx_room_name"`, confirming both MCP connectivity and which room you're in.
|
|
||||||
|
|
||||||
### Discover available exits from current room
|
|
||||||
|
|
||||||
Use the `set-piece` group to find all interactive polygons (TransitionPieces are automatically added):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"command": "get_nodes_in_group", "params": {"group": "set-piece"}}
|
|
||||||
```
|
|
||||||
|
|
||||||
Filter results for node names starting with `kq4_0` — these are transition exits.
|
|
||||||
|
|
||||||
### Trigger a room transition
|
|
||||||
|
|
||||||
**Critical: The method name is `mock_interact`, NOT `mock_interaction`.**
|
|
||||||
|
|
||||||
Use `godot_game_call_method` to call it directly on the node path:
|
|
||||||
|
|
||||||
```
|
|
||||||
Call method mock_interact(0) on /root/Node2D/SceneViewport/background/<exit_node_name>
|
|
||||||
```
|
|
||||||
|
|
||||||
Argument `0` = ActionState.Action.WALK (1=LOOK, 2=TOUCH, 3=TALK).
|
|
||||||
|
|
||||||
### Waiting — MCP busy protocol
|
|
||||||
|
|
||||||
**IMPORTANT**: Walk animations block the McpInteractionServer for ~30 seconds. During this time:
|
|
||||||
- `eval`, `wait`, `call_method` all fail with "timed out after 30s"
|
|
||||||
- The server has a built-in `_busy` flag auto-reset after ~30s ("_busy flag stuck for 30.Xs, force-resetting")
|
|
||||||
|
|
||||||
**Robust polling pattern**:
|
|
||||||
1. Call `mock_interact(0)` on the target node (returns immediately, animation starts)
|
|
||||||
2. Try `wait(frames=30)` then poll room name
|
|
||||||
3. If both timeout (~30s elapsed), retry the room name poll once more
|
|
||||||
4. The server will auto-reset and the next poll will succeed
|
|
||||||
|
|
||||||
Expected timing per transition: **~45-75 seconds total** (walk ~15s, MCP block ~30s, fade-in ~5s, confirm ~2s)
|
|
||||||
|
|
||||||
### Finding transitions by script handler
|
|
||||||
|
|
||||||
In room `.gd` files, handlers like `_on_ogres_cottage_interacted()` reference the TransitionPiece node (`$kq4_004_ogres_cottage.default_script(self)`). The handler's parameter name matches the node name you need to call `mock_interact(0)` on.
|
|
||||||
|
|
||||||
## Room Graph Structure
|
|
||||||
|
|
||||||
The graph currently has:
|
|
||||||
|
|
||||||
- **96 rooms** across 23 connected components
|
|
||||||
- Largest component: 36 rooms (starting from room 3 Fountain Pool)
|
|
||||||
- Second largest: 17 rooms (room 1 Beach area)
|
|
||||||
- Two additional 36-room components that are disconnected due to UID mismatches
|
|
||||||
|
|
||||||
Use `python scripts/build_room_graph.py --from A --to B` to find a path. If "no path" is returned but rooms feel like they should connect, the cause is likely a stale UID (see Common Issues).
|
|
||||||
|
|
||||||
### UID mismatch example
|
|
||||||
|
|
||||||
Room `kq4_018_cemetery` has UID `uid://b3fjmiaribbrl`, but nearby rooms point to it with stale UIDs (`uid://35amqvpjgpf2x`). Runtime navigation works fine (Transitions use file paths), but the graph can't find the connection.
|
|
||||||
|
|
||||||
## Common Issues
|
|
||||||
|
|
||||||
| Problem | Diagnosis | Fix |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `mock_interaction` method not found | Wrong method name | Use **`mock_interact(0)`** — no trailing "tion" |
|
|
||||||
| MCP commands time out during transition | Walk animation blocks server (~30s) | Wait and retry; server auto-resets after ~30s |
|
|
||||||
| "No path" between adjacent rooms at graph level | Target UID in source `.tscn` doesn't match destination room's `.uid` file | Runtime still works. Fix the target uid in the source tscn or update the destination room's .uid |
|
|
||||||
| `get_current_room_name()` returns `"background"` | Old implementation used `scene.name` | Updated to use `script_path.trim_suffix(".gd").get_file()` |
|
|
||||||
|
|
||||||
## Verified Navigation Example
|
|
||||||
|
|
||||||
Successfully tested navigating from kq4_003_fountain_pool to kq4_018_cemetery via 5 transitions:
|
|
||||||
|
|
||||||
```
|
|
||||||
kq4_003_fountain_pool → mock_interact(kq4_004_ogres_cottage) → kq4_004_ogres_cottage ✓
|
|
||||||
kq4_004_ogres_cottage → mock_interact(kq4_010_forest_path) → kq4_010_forest_path ✓
|
|
||||||
kq4_010_forest_path → mock_interact(kq4_011_enchanted_grove) → kq4_011_enchanted_grove ✓
|
|
||||||
kq4_011_enchanted_grove → mock_interact(kq4_017_spooky_house_exterior) → kq4_017_spooky_house_exterior ✓
|
|
||||||
kq4_017_spooky_house_exterior → mock_interact(kq4_018_cemetery) → kq4_018_cemetery ✓
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Reference
|
|
||||||
|
|
||||||
### build_room_graph.py module
|
|
||||||
|
|
||||||
```python
|
|
||||||
from pathlib import Path
|
|
||||||
from scripts.build_room_graph import build_graph, find_path, NavigationStep
|
|
||||||
|
|
||||||
graph = build_graph(Path('scenes')) # RoomInfo dict keyed by room name
|
|
||||||
steps = find_path(graph, "kq4_003_fountain_pool", "kq4_018_cemetery") # List[NavigationStep] or None
|
|
||||||
step.from_room # Source room name
|
|
||||||
step.exit_node_name # TransitionPiece node to call mock_interact(0) on
|
|
||||||
step.to_room # Destination room name
|
|
||||||
step.label # Human-readable label (e.g., "Ogre's Cottage")
|
|
||||||
```
|
|
||||||
|
|
||||||
### kq4_room_navigator.py CLI
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python tools/kq4_room_navigator.py --from ROOM --to ROOM
|
|
||||||
# For graph summary:
|
|
||||||
python tools/kq4_room_navigator.py summary
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
| --- | --- |
|
|
||||||
| `MainGame.gd` | Root game node; `get_current_room_name()` returns room from script path |
|
|
||||||
| `tools/kq4_room_navigator.py` | CLI combining graph + BFS + MCP navigation |
|
|
||||||
| `scripts/build_room_graph.py` | Room adjacency graph builder + BFS pathfinding |
|
|
||||||
| `TransitionPiece.gd` | Exit nodes; `mock_interact(action)` triggers default_script() |
|
|
||||||
| `SetPiece_.gd` | Base interactive polygon class (class_name SetPiece) |
|
|
||||||
| `.opencode/skills/kq4-room-creator/SKILL.md` | Related: creating new rooms with transitions |
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"setup": [
|
|
||||||
"git submodule update\ncd godot-mcp\nnpm install\nnpm run build\ncd .."
|
|
||||||
],
|
|
||||||
"teardown": [],
|
|
||||||
"run": []
|
|
||||||
}
|
|
||||||
@@ -10,14 +10,6 @@ var is_cursor_locked: bool = false # When true, hourglass is shown and cursor c
|
|||||||
func get_scene() -> Scene:
|
func get_scene() -> Scene:
|
||||||
return $SceneViewport/background
|
return $SceneViewport/background
|
||||||
|
|
||||||
func get_current_room_name() -> String:
|
|
||||||
var scene = get_scene()
|
|
||||||
if scene and scene.get_script():
|
|
||||||
var script_path = scene.get_script().resource_path
|
|
||||||
if script_path.begins_with("res://scenes/"):
|
|
||||||
return script_path.trim_suffix(".gd").get_file()
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# Called when the node enters the scene tree for the first time.
|
# Called when the node enters the scene tree for the first time.
|
||||||
func _ready():
|
func _ready():
|
||||||
# get_scene().connect("transitioned", Callable($SceneViewport/label, "_on_transitioned"))
|
# get_scene().connect("transitioned", Callable($SceneViewport/label, "_on_transitioned"))
|
||||||
|
|||||||
38
SetPiece_.gd
38
SetPiece_.gd
@@ -41,44 +41,6 @@ signal exited(lab)
|
|||||||
points_resource = value
|
points_resource = value
|
||||||
_update_polygon_from_resource()
|
_update_polygon_from_resource()
|
||||||
|
|
||||||
func sample_polygon_point() -> Vector2:
|
|
||||||
if polygon.is_empty():
|
|
||||||
return position
|
|
||||||
var sum_x = 0.0
|
|
||||||
var sum_y = 0.0
|
|
||||||
for p in polygon:
|
|
||||||
sum_x += p.x
|
|
||||||
sum_y += p.y
|
|
||||||
return to_global(Vector2(sum_x / polygon.size(), sum_y / polygon.size()))
|
|
||||||
|
|
||||||
func get_screen_click_point() -> Vector2i:
|
|
||||||
var world_pt = sample_polygon_point()
|
|
||||||
var vp = get_viewport()
|
|
||||||
var screen_pt = vp.get_canvas_transform() * world_pt
|
|
||||||
return Vector2i(int(screen_pt.x), int(screen_pt.y))
|
|
||||||
|
|
||||||
func mock_interact(action = 0) -> void:
|
|
||||||
action = int(action)
|
|
||||||
var script_builder = get_node_or_null("/root/Node2D/GameScript")
|
|
||||||
if script_builder and script_builder.current_script and not script_builder.current_script.can_interrupt:
|
|
||||||
return
|
|
||||||
if interacted.get_connections().size() > 0:
|
|
||||||
emit_signal("interacted")
|
|
||||||
return
|
|
||||||
match action:
|
|
||||||
ActionState.Action.WALK:
|
|
||||||
if walked.get_connections().size() > 0:
|
|
||||||
emit_signal("walked")
|
|
||||||
ActionState.Action.LOOK:
|
|
||||||
if looked.get_connections().size() > 0:
|
|
||||||
emit_signal("looked")
|
|
||||||
ActionState.Action.TOUCH:
|
|
||||||
if touched.get_connections().size() > 0:
|
|
||||||
emit_signal("touched")
|
|
||||||
ActionState.Action.TALK:
|
|
||||||
if talked.get_connections().size() > 0:
|
|
||||||
emit_signal("talked")
|
|
||||||
|
|
||||||
func _input(event):
|
func _input(event):
|
||||||
if not Engine.is_editor_hint():
|
if not Engine.is_editor_hint():
|
||||||
if event is InputEventMouseButton and Input.is_action_just_released("interact"):
|
if event is InputEventMouseButton and Input.is_action_just_released("interact"):
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
## Code Review: PR 3 - "Stealth Cymbal"
|
|
||||||
|
|
||||||
### Scope
|
|
||||||
- **Branch:** stealth-cymbal
|
|
||||||
- **Base:** master
|
|
||||||
- **Files Changed:** 86 files (~1576 insertions, ~230 deletions)
|
|
||||||
- **Key Changes:** KQ4 Room Navigator skill, UID repair tools, MCP interaction server, room scene updates
|
|
||||||
|
|
||||||
### Review Team
|
|
||||||
- ✅ **ce-correctness-reviewer** - Logic errors, edge cases, state bugs
|
|
||||||
- ✅ **ce-testing-reviewer** - Coverage gaps, weak assertions, brittle tests
|
|
||||||
- ✅ **ce-maintainability-reviewer** - Coupling, complexity, naming, dead code
|
|
||||||
- ✅ **ce-project-standards-reviewer** - CLAUDE.md and AGENTS.md compliance
|
|
||||||
- ✅ **ce-agent-native-reviewer** - Agent-accessible design patterns
|
|
||||||
- ✅ **ce-learnings-researcher** - Past issues in docs/solutions/
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P1 -- High
|
|
||||||
|
|
||||||
| # | File | Issue | Reviewer | Confidence | Route |
|
|
||||||
|---|------|------|----------|------------|-------|
|
|
||||||
| 1 | `scenes/kq4_004_ogres_cottage/kq4_004_ogres_cottage.tscn:46` | UID mismatch - target UID for kq4_028_mine_entrance points to non-existent room | correctness | 75% | manual → downstream-resolver |
|
|
||||||
| 2 | `scripts/mcp_interaction_server.gd` | No runtime tests for 80+ TCP command handlers (171KB script) | testing | 50% | manual → downstream-resolver |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P2 -- Moderate
|
|
||||||
|
|
||||||
| # | File | Issue | Reviewer | Confidence | Route |
|
|
||||||
|---|------|------|----------|------------|-------|
|
|
||||||
| 3 | `scenes/kq4_098_transitional_room/kq4_098_transitional_room.tscn` | Missing .uid companion file | correctness | 75% | manual → review-fixer |
|
|
||||||
| 4 | `scripts/build_room_graph.py:184` | `_resolve_target_room` returns None without explicit handling in callers | correctness | 75% | safe_auto → review-fixer |
|
|
||||||
| 5 | `tools/repair_uids.py:322` | Code duplication in fix_stale_target with inconsistent error handling | correctness | 75% | safe_auto → review-fixer |
|
|
||||||
| 6 | `tools/repair_uids.py` | No tests for file mutation operations (UID sync, target replacement) | testing | 75% | manual → downstream-resolver |
|
|
||||||
| 7 | `scripts/build_room_graph.py` | No tests for BFS edge cases (disconnected components, malformed UIDs) | testing | 75% | manual → downstream-resolver |
|
|
||||||
| 8 | `MainGame.gd`, `SetPiece_.gd` | No tests for signal emission logic and cursor action routing | testing | 50% | manual → downstream-resolver |
|
|
||||||
| 9 | `SetPiece_.gd:mock_interact()` | Unnecessary indirection - duplicates `_input()` logic | maintainability | 75% | gated_auto → downstream-resolver |
|
|
||||||
| 10 | `SetPiece_.gd` | Unused `entered/exited` signals with unclear `lab` param | maintainability | 75% | gated_auto → downstream-resolver |
|
|
||||||
| 11 | `tools/kq4_room_navigator.py` | sys.path manipulation creates coupling | maintainability | 50% | gated_auto → downstream-resolver |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P3 -- Low
|
|
||||||
|
|
||||||
| # | File | Issue | Reviewer | Confidence | Route |
|
|
||||||
|---|------|------|----------|------------|-------|
|
|
||||||
| 12 | `MainGame.gd:13` | `get_current_room_name()` returns empty string on failure - silent failure mode | correctness | 50% | advisory → downstream-resolver |
|
|
||||||
| 13 | `scripts/mcp_interaction_server.gd:42` | Fixed 30-second timeout could mask real hangs | correctness | 50% | advisory → downstream-resolver |
|
|
||||||
| 14 | `SetPiece_.gd:60` | `mock_interact()` doesn't validate target room exists | correctness | 50% | advisory → downstream-resolver |
|
|
||||||
| 15 | `tools/kq4_room_navigator.py` | No tests for CLI tool | testing | 75% | advisory → human |
|
|
||||||
| 16 | `MainGame.gd` | Placeholder comment in `_ready()` - dead code | maintainability | 100% | advisory → human |
|
|
||||||
| 17 | `MainGame.gd` | Commented-out code with typo - dead code | maintainability | 100% | advisory → human |
|
|
||||||
| 18 | `scripts/mcp_interaction_server.gd` | Potentially incomplete implementation | maintainability | 50% | advisory → human |
|
|
||||||
| 19 | `.opencode/skills/kq4-room-navigator/SKILL.md:38-41` | Code block formatting changed from JSON to plain text | project-standards | 75% | advisory → human |
|
|
||||||
| 20 | `tools/kq4_room_navigator.py` | Dataclass overhead for Mismatch in CLI tool | maintainability | 50% | advisory → human |
|
|
||||||
| 21 | `.opencode/skills/kq4-room-navigator/SKILL.md` | Missing explicit retry guidance for `_busy` protocol | agent-native | 75% | advisory → human |
|
|
||||||
| 22 | `.opencode/skills/kq4-room-navigator/SKILL.md` | `mock_interact` vs `mock_interaction` typo in error messages | agent-native | 75% | safe_auto → review-fixer |
|
|
||||||
| 23 | `.opencode/skills/kq4-room-navigator/SKILL.md` | `--navigate` flag documented but not implemented in CLI | agent-native | 50% | gated_auto → downstream-resolver |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirements Completeness
|
|
||||||
**No plan document found** for this PR. The PR title "Stealth Cymbal" is ambiguous and doesn't reference a specific plan.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Pre-existing Issues
|
|
||||||
None identified in this review.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Learnings & Past Solutions
|
|
||||||
**No institutional learnings found.** The `docs/solutions/` directory does not exist in this repository. Consider using `/ce-compound` skill after significant work to document:
|
|
||||||
- UID generation patterns (make_uid.py + .uid files)
|
|
||||||
- Navigation/pathfinding setup (NavigationServer2D)
|
|
||||||
- MCP server integration learnings
|
|
||||||
- Room transition wiring patterns
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Agent-Native Gaps
|
|
||||||
1. **Offline-Only Graph Discovery** - Agents can't dynamically discover room connectivity at runtime
|
|
||||||
2. **Manual Verification** - Navigation completion requires polling instead of signals
|
|
||||||
3. **No Path Validation** - Agents can attempt invalid navigation without guardrails
|
|
||||||
4. **Incomplete CLI** - The `--navigate` flag is documented but not implemented
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Residual Risks
|
|
||||||
1. **UID mismatch risk** - Room graph connectivity depends on UID consistency across all .tscn and .uid files. If UIDs drift from manual edits, pathfinding will fail even though runtime transitions work.
|
|
||||||
- **Mitigation:** Run `tools/repair_uids.py --fix` before each significant room update. Consider integrating UID validation into CI pipeline.
|
|
||||||
|
|
||||||
2. **Timeout risk** - The MCP server's `_busy` flag uses a fixed 30-second timeout. If navigation animations or other operations take longer, commands may be prematurely reset.
|
|
||||||
- **Mitigation:** Monitor `_busy` timeout warnings in logs. Consider making configurable or implementing per-command timeout overrides.
|
|
||||||
|
|
||||||
3. **Arbitrary code execution** - MCP eval command allows arbitrary code execution without guardrails.
|
|
||||||
|
|
||||||
4. **Heuristic risk** - UID auto-resolution in repair_uids.py may produce incorrect fixes based on heuristics.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Testing Gaps
|
|
||||||
1. **No automated tests for UID consistency** across room transitions
|
|
||||||
2. **No tests for pathfinding edge cases** (disconnected rooms, single-room graphs, self-loops)
|
|
||||||
3. **No tests for MCP server command timeout** behavior under load
|
|
||||||
4. **No validation** that all TransitionPiece targets point to existing rooms
|
|
||||||
5. **No unit tests** for UID mismatch detection in repair_uids.py
|
|
||||||
6. **No tests** for BFS pathfinding edge cases in build_room_graph.py
|
|
||||||
7. **No tests** for SetPiece signal emission logic
|
|
||||||
8. **No tests** for busy-state timeout recovery in MCP server
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Coverage
|
|
||||||
- **Findings:** 23 total (2 P1, 9 P2, 12 P3)
|
|
||||||
- **Safe-auto (fixable now):** 3 findings
|
|
||||||
- **Manual (needs handoff):** 4 findings
|
|
||||||
- **Gated-auto (needs review):** 4 findings
|
|
||||||
- **Advisory (report-only):** 12 findings
|
|
||||||
- **Failed reviewers:** 0 of 6
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Verdict
|
|
||||||
**Ready with fixes**
|
|
||||||
|
|
||||||
**Fix order:**
|
|
||||||
1. **Critical:** Fix UID mismatch in kq4_004_ogres_cottage.tscn (finding #1) - this breaks navigation
|
|
||||||
2. **High:** Add missing .uid file for kq4_098_transitional_room (finding #3)
|
|
||||||
3. **Medium:** Fix None handling in build_room_graph.py (finding #4)
|
|
||||||
4. **Medium:** Consolidate code duplication in repair_uids.py (finding #5)
|
|
||||||
5. **Low:** Address dead code and formatting issues (findings #12-23)
|
|
||||||
|
|
||||||
**Before merge:** Run `tools/repair_uids.py --fix` to synchronize all UIDs and verify the UID mismatch is resolved.
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[resource _type="ItemDefinition" uid="uid://dovkj7b9xqwpp"]
|
{
|
||||||
|
"item_id": "splash",
|
||||||
id = "splash"
|
"name": "Splash",
|
||||||
name = "Splash"
|
"icon": "res://splash.png",
|
||||||
combination_category = "potion"
|
"combination_category": "potion"
|
||||||
icon = null
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"godot": {
|
"playwright": {
|
||||||
"type": "local",
|
"type": "local",
|
||||||
"command": [
|
"command": [
|
||||||
"node",
|
"npx",
|
||||||
"./godot-mcp/build/index.js"
|
"@playwright/mcp@latest",
|
||||||
|
"--executable-path",
|
||||||
|
"/snap/bin/chromium",
|
||||||
|
"--isolated"
|
||||||
],
|
],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_001_beach"
|
appear_at_node = "kq4_001_beach"
|
||||||
target = "uid://dxs1tr5yvmoba"
|
target = "uid://1489d4oh9twtu"
|
||||||
label = "Meadow"
|
label = "Meadow"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_002_meadow" index="0"]
|
[node name="entrance" parent="kq4_002_meadow" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://dlg6010ym2uw4
|
uid://1rwfkejhz94hp
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-79, 729, -86, 873, 261, 904, 249, 694)
|
polygon = PackedVector2Array(-79, 729, -86, 873, 261, 904, 249, 694)
|
||||||
appear_at_node = "kq4_002_meadow"
|
appear_at_node = "kq4_002_meadow"
|
||||||
target = "uid://dyk4rcqsk3aed"
|
target = "uid://151dbn9bybiwx"
|
||||||
label = "Fountain Pool"
|
label = "Fountain Pool"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_003_fountain_pool" index="0"]
|
[node name="entrance" parent="kq4_003_fountain_pool" index="0"]
|
||||||
@@ -71,7 +71,7 @@ position = Vector2(227, 790)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_002_meadow"
|
appear_at_node = "kq4_002_meadow"
|
||||||
target = "uid://bncm0jvaibkv"
|
target = "uid://bncmzju9ibkv"
|
||||||
label = "Back of Fisherman's Shack"
|
label = "Back of Fisherman's Shack"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_008_back_of_fishermans_shack" index="0"]
|
[node name="entrance" parent="kq4_008_back_of_fishermans_shack" index="0"]
|
||||||
@@ -83,7 +83,7 @@ position = Vector2(151, 615)
|
|||||||
[node name="kq4_001_beach" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
[node name="kq4_001_beach" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_002_meadow"
|
appear_at_node = "kq4_002_meadow"
|
||||||
target = "uid://dlg6010ym2uw4"
|
target = "uid://1rwfkejhz94hp"
|
||||||
label = "Beach"
|
label = "Beach"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_001_beach" index="0"]
|
[node name="entrance" parent="kq4_001_beach" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://dxs1tr5yvmoba
|
uid://1489d4oh9twtu
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(-150, 100)
|
position = Vector2(-150, 100)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_003_fountain_pool"
|
appear_at_node = "kq4_003_fountain_pool"
|
||||||
target = "uid://dxs1tr5yvmoba"
|
target = "uid://1489d4oh9twtu"
|
||||||
label = "Meadow"
|
label = "Meadow"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_002_meadow" index="0"]
|
[node name="entrance" parent="kq4_002_meadow" index="0"]
|
||||||
@@ -75,7 +75,7 @@ position = Vector2(66, 71)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_003_fountain_pool"
|
appear_at_node = "kq4_003_fountain_pool"
|
||||||
target = "uid://da4h2ljrt02ie"
|
target = "uid://1hkplw2a78b1y"
|
||||||
label = "Shady Wooded Forest"
|
label = "Shady Wooded Forest"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_009_shady_wooded_area" index="0"]
|
[node name="entrance" parent="kq4_009_shady_wooded_area" index="0"]
|
||||||
@@ -89,7 +89,7 @@ position = Vector2(500, 100)
|
|||||||
color = Color(1, 1, 1, 1)
|
color = Color(1, 1, 1, 1)
|
||||||
polygon = PackedVector2Array(349, 278, 398, 417, 593, 411, 650, 277)
|
polygon = PackedVector2Array(349, 278, 398, 417, 593, 411, 650, 277)
|
||||||
appear_at_node = "kq4_003_fountain_pool"
|
appear_at_node = "kq4_003_fountain_pool"
|
||||||
target = "uid://c8aq5g1juqdam"
|
target = "uid://1fpyosj18xls7"
|
||||||
label = "Forest Path"
|
label = "Forest Path"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_027_forest_path" index="0"]
|
[node name="entrance" parent="kq4_027_forest_path" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://dyk4rcqsk3aed
|
uid://151dbn9bybiwx
|
||||||
@@ -43,7 +43,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_004_ogres_cottage"
|
appear_at_node = "kq4_004_ogres_cottage"
|
||||||
target = "uid://qkcwifq2lcam"
|
target = "uid://qkcwifq2lb9m"
|
||||||
label = "Mine Entrance"
|
label = "Mine Entrance"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_028_mine_entrance" index="0"]
|
[node name="entrance" parent="kq4_028_mine_entrance" index="0"]
|
||||||
@@ -57,7 +57,7 @@ position = Vector2(5, 888)
|
|||||||
color = Color(1, 1, 1, 1)
|
color = Color(1, 1, 1, 1)
|
||||||
polygon = PackedVector2Array(-85, -86, -65, 128, 264, 167, 260, -78)
|
polygon = PackedVector2Array(-85, -86, -65, 128, 264, 167, 260, -78)
|
||||||
appear_at_node = "kq4_004_ogres_cottage"
|
appear_at_node = "kq4_004_ogres_cottage"
|
||||||
target = "uid://dyk4rcqsk3aed"
|
target = "uid://151dbn9bybiwx"
|
||||||
label = "Fountain Pool"
|
label = "Fountain Pool"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_003_fountain_pool" index="0"]
|
[node name="entrance" parent="kq4_003_fountain_pool" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://dhie3qsi5333g
|
uid://1nxmm3b1kcdm1
|
||||||
@@ -46,7 +46,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_005_forest_grove"
|
appear_at_node = "kq4_005_forest_grove"
|
||||||
target = "uid://dlyrp8twdxb4g"
|
target = "uid://1sfzaldfq5kn1"
|
||||||
label = "Dense Forest"
|
label = "Dense Forest"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_029_dense_forest" index="0"]
|
[node name="entrance" parent="kq4_029_dense_forest" index="0"]
|
||||||
@@ -84,7 +84,7 @@ position = Vector2(151, 615)
|
|||||||
[node name="kq4_004_ogres_cottage" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
[node name="kq4_004_ogres_cottage" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_005_forest_grove"
|
appear_at_node = "kq4_005_forest_grove"
|
||||||
target = "uid://dhie3qsi5333g"
|
target = "uid://1nxmm3b1kcdm1"
|
||||||
label = "Ogre's Cottage"
|
label = "Ogre's Cottage"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_004_ogres_cottage" index="0"]
|
[node name="entrance" parent="kq4_004_ogres_cottage" index="0"]
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_007_fishermans_shack"
|
appear_at_node = "kq4_007_fishermans_shack"
|
||||||
target = "uid://dlg6010ym2uw4"
|
target = "uid://1rwfkejhz94hp"
|
||||||
label = "Beach"
|
label = "Beach"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_001_beach" index="0"]
|
[node name="entrance" parent="kq4_001_beach" index="0"]
|
||||||
@@ -59,7 +59,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_007_fishermans_shack"
|
appear_at_node = "kq4_007_fishermans_shack"
|
||||||
target = "uid://bncm0jvaibkv"
|
target = "uid://bncmzju9ibkv"
|
||||||
label = "Back of Fisherman's Shack"
|
label = "Back of Fisherman's Shack"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_008_back_of_fishermans_shack" index="0"]
|
[node name="entrance" parent="kq4_008_back_of_fishermans_shack" index="0"]
|
||||||
@@ -72,7 +72,7 @@ position = Vector2(293, 554)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_007_fishermans_shack"
|
appear_at_node = "kq4_007_fishermans_shack"
|
||||||
target = "uid://d4a2d0rfqnmmo"
|
target = "uid://2bqawc9w4uu59"
|
||||||
label = "Beach"
|
label = "Beach"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_013_beach" index="0"]
|
[node name="entrance" parent="kq4_013_beach" index="0"]
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ z_index = 4
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_008_back_of_fishermans_shack"
|
appear_at_node = "kq4_008_back_of_fishermans_shack"
|
||||||
target = "uid://da4h2ljrt02ie"
|
target = "uid://1hkplw2a78b1y"
|
||||||
label = "Shady Wooded Area"
|
label = "Shady Wooded Area"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_009_shady_wooded_area" index="0"]
|
[node name="entrance" parent="kq4_009_shady_wooded_area" index="0"]
|
||||||
@@ -59,7 +59,7 @@ z_index = 2
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-631, 454, -539, 610, 376, 658, 769, 610, 748, 475, 348, 381, -594, 362)
|
polygon = PackedVector2Array(-631, 454, -539, 610, 376, 658, 769, 610, 748, 475, 348, 381, -594, 362)
|
||||||
appear_at_node = "kq4_008_back_of_fishermans_shack"
|
appear_at_node = "kq4_008_back_of_fishermans_shack"
|
||||||
target = "uid://dxs1tr5yvmoba"
|
target = "uid://1489d4oh9twtu"
|
||||||
label = "Meadow"
|
label = "Meadow"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_002_meadow" index="0"]
|
[node name="entrance" parent="kq4_002_meadow" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://bncm0jvaibkv
|
uid://bncmzju9ibkv
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_009_shady_wooded_area"
|
appear_at_node = "kq4_009_shady_wooded_area"
|
||||||
target = "uid://dyk4rcqsk3aed"
|
target = "uid://151dbn9bybiwx"
|
||||||
label = "Fountain Pool"
|
label = "Fountain Pool"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_003_fountain_pool" index="0"]
|
[node name="entrance" parent="kq4_003_fountain_pool" index="0"]
|
||||||
@@ -58,7 +58,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_009_shady_wooded_area"
|
appear_at_node = "kq4_009_shady_wooded_area"
|
||||||
target = "uid://bsog5s257pres"
|
target = "uid://3ujj97iw54vo5"
|
||||||
label = "Forest Path"
|
label = "Forest Path"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_010_forest_path" index="0"]
|
[node name="entrance" parent="kq4_010_forest_path" index="0"]
|
||||||
@@ -71,7 +71,7 @@ position = Vector2(293, 554)
|
|||||||
position = Vector2(-200, 100)
|
position = Vector2(-200, 100)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_009_shady_wooded_area"
|
appear_at_node = "kq4_009_shady_wooded_area"
|
||||||
target = "uid://bncm0jvaibkv"
|
target = "uid://bncmzju9ibkv"
|
||||||
label = "Back of Fisherman's Shack"
|
label = "Back of Fisherman's Shack"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_008_back_of_fishermans_shack" index="0"]
|
[node name="entrance" parent="kq4_008_back_of_fishermans_shack" index="0"]
|
||||||
@@ -84,7 +84,7 @@ position = Vector2(100, 480)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_009_shady_wooded_area"
|
appear_at_node = "kq4_009_shady_wooded_area"
|
||||||
target = "uid://xk6xu65nm620"
|
target = "uid://2zga29mwl2ced"
|
||||||
label = "Frog Pond"
|
label = "Frog Pond"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_015_frog_pond" index="0"]
|
[node name="entrance" parent="kq4_015_frog_pond" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://da4h2ljrt02ie
|
uid://1hkplw2a78b1y
|
||||||
@@ -44,7 +44,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_010_forest_path"
|
appear_at_node = "kq4_010_forest_path"
|
||||||
target = "uid://dyk4rcqsk3aed"
|
target = "uid://151dbn9bybiwx"
|
||||||
label = "Fountain Pool"
|
label = "Fountain Pool"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_003_fountain_pool" index="0"]
|
[node name="entrance" parent="kq4_003_fountain_pool" index="0"]
|
||||||
@@ -57,7 +57,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_010_forest_path"
|
appear_at_node = "kq4_010_forest_path"
|
||||||
target = "uid://dhie3qsi5333g"
|
target = "uid://1nxmm3b1kcdm1"
|
||||||
label = "Ogre's Cottage"
|
label = "Ogre's Cottage"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_004_ogres_cottage" index="0"]
|
[node name="entrance" parent="kq4_004_ogres_cottage" index="0"]
|
||||||
@@ -83,7 +83,7 @@ position = Vector2(293, 554)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_010_forest_path"
|
appear_at_node = "kq4_010_forest_path"
|
||||||
target = "uid://5gygr0s1n433"
|
target = "uid://27b2k6gky3afg"
|
||||||
label = "Graveyard"
|
label = "Graveyard"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_016_graveyard" index="0"]
|
[node name="entrance" parent="kq4_016_graveyard" index="0"]
|
||||||
@@ -96,7 +96,7 @@ position = Vector2(151, 615)
|
|||||||
position = Vector2(-150, 100)
|
position = Vector2(-150, 100)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_010_forest_path"
|
appear_at_node = "kq4_010_forest_path"
|
||||||
target = "uid://da4h2ljrt02ie"
|
target = "uid://1hkplw2a78b1y"
|
||||||
label = "Shady Wooded Area"
|
label = "Shady Wooded Area"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_009_shady_wooded_area" index="0"]
|
[node name="entrance" parent="kq4_009_shady_wooded_area" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://bsog5s257pres
|
uid://3ujj97iw54vo5
|
||||||
@@ -69,7 +69,7 @@ position = Vector2(293, 554)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_011_enchanted_grove"
|
appear_at_node = "kq4_011_enchanted_grove"
|
||||||
target = "uid://dek2gdmwnmgsl"
|
target = "uid://1kz9yo5f1tpc6"
|
||||||
label = "Spooky House Exterior"
|
label = "Spooky House Exterior"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_017_spooky_house_exterior" index="0"]
|
[node name="entrance" parent="kq4_017_spooky_house_exterior" index="0"]
|
||||||
@@ -81,7 +81,7 @@ position = Vector2(151, 615)
|
|||||||
[node name="kq4_010_forest_path" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
[node name="kq4_010_forest_path" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_011_enchanted_grove"
|
appear_at_node = "kq4_011_enchanted_grove"
|
||||||
target = "uid://bsog5s257pres"
|
target = "uid://3ujj97iw54vo5"
|
||||||
label = "Forest Path"
|
label = "Forest Path"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_010_forest_path" index="0"]
|
[node name="entrance" parent="kq4_010_forest_path" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://d4a2d0rfqnmmo
|
uid://2bqawc9w4uu59
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_014_green_meadow"
|
appear_at_node = "kq4_014_green_meadow"
|
||||||
target = "uid://bncm0jvaibkv"
|
target = "uid://bncmzju9ibkv"
|
||||||
label = "Back of Fisherman's Shack"
|
label = "Back of Fisherman's Shack"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_008_back_of_fishermans_shack" index="0"]
|
[node name="entrance" parent="kq4_008_back_of_fishermans_shack" index="0"]
|
||||||
@@ -59,7 +59,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_014_green_meadow"
|
appear_at_node = "kq4_014_green_meadow"
|
||||||
target = "uid://xk6xu65nm620"
|
target = "uid://2zga29mwl2ced"
|
||||||
label = "Frog Pond"
|
label = "Frog Pond"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_015_frog_pond" index="0"]
|
[node name="entrance" parent="kq4_015_frog_pond" index="0"]
|
||||||
@@ -72,7 +72,7 @@ position = Vector2(293, 554)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_014_green_meadow"
|
appear_at_node = "kq4_014_green_meadow"
|
||||||
target = "uid://w4xpm5qeo45d"
|
target = "uid://2yy1t1lic39gp"
|
||||||
label = "Meadow"
|
label = "Meadow"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_020_meadow" index="0"]
|
[node name="entrance" parent="kq4_020_meadow" index="0"]
|
||||||
@@ -84,7 +84,7 @@ position = Vector2(151, 615)
|
|||||||
[node name="kq4_013_beach" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
[node name="kq4_013_beach" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_014_green_meadow"
|
appear_at_node = "kq4_014_green_meadow"
|
||||||
target = "uid://d4a2d0rfqnmmo"
|
target = "uid://2bqawc9w4uu59"
|
||||||
label = "Beach"
|
label = "Beach"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_013_beach" index="0"]
|
[node name="entrance" parent="kq4_013_beach" index="0"]
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_015_frog_pond"
|
appear_at_node = "kq4_015_frog_pond"
|
||||||
target = "uid://da4h2ljrt02ie"
|
target = "uid://1hkplw2a78b1y"
|
||||||
label = "Shady Wooded Area"
|
label = "Shady Wooded Area"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_009_shady_wooded_area" index="0"]
|
[node name="entrance" parent="kq4_009_shady_wooded_area" index="0"]
|
||||||
@@ -58,7 +58,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_015_frog_pond"
|
appear_at_node = "kq4_015_frog_pond"
|
||||||
target = "uid://5gygr0s1n433"
|
target = "uid://27b2k6gky3afg"
|
||||||
label = "Graveyard"
|
label = "Graveyard"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_016_graveyard" index="0"]
|
[node name="entrance" parent="kq4_016_graveyard" index="0"]
|
||||||
@@ -84,7 +84,7 @@ position = Vector2(300, 554)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_015_frog_pond"
|
appear_at_node = "kq4_015_frog_pond"
|
||||||
target = "uid://bs3fll3ml3ffy"
|
target = "uid://3uxipzjekijqc"
|
||||||
label = "Bridge over Stream"
|
label = "Bridge over Stream"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_021_bridge_over_stream" index="0"]
|
[node name="entrance" parent="kq4_021_bridge_over_stream" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://xk6xu65nm620
|
uid://2zga29mwl2ced
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(-200, 100)
|
position = Vector2(-200, 100)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_016_graveyard"
|
appear_at_node = "kq4_016_graveyard"
|
||||||
target = "uid://xk6xu65nm620"
|
target = "uid://2zga29mwl2ced"
|
||||||
label = "Frog Pond"
|
label = "Frog Pond"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_015_frog_pond" index="0"]
|
[node name="entrance" parent="kq4_015_frog_pond" index="0"]
|
||||||
@@ -57,7 +57,7 @@ position = Vector2(100, 480)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_016_graveyard"
|
appear_at_node = "kq4_016_graveyard"
|
||||||
target = "uid://bsog5s257pres"
|
target = "uid://3ujj97iw54vo5"
|
||||||
label = "Forest Path"
|
label = "Forest Path"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_010_forest_path" index="0"]
|
[node name="entrance" parent="kq4_010_forest_path" index="0"]
|
||||||
@@ -70,7 +70,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_016_graveyard"
|
appear_at_node = "kq4_016_graveyard"
|
||||||
target = "uid://bmv1tox6p3h1x"
|
target = "uid://3oq4x3exoimdb"
|
||||||
label = "Gnomes Cottage"
|
label = "Gnomes Cottage"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_022_gnomes_cottage" index="0"]
|
[node name="entrance" parent="kq4_022_gnomes_cottage" index="0"]
|
||||||
@@ -83,7 +83,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_016_graveyard"
|
appear_at_node = "kq4_016_graveyard"
|
||||||
target = "uid://dek2gdmwnmgsl"
|
target = "uid://1kz9yo5f1tpc6"
|
||||||
label = "Spooky House Exterior"
|
label = "Spooky House Exterior"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_017_spooky_house_exterior" index="0"]
|
[node name="entrance" parent="kq4_017_spooky_house_exterior" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://5gygr0s1n433
|
uid://27b2k6gky3afg
|
||||||
@@ -56,7 +56,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_017_spooky_house_exterior"
|
appear_at_node = "kq4_017_spooky_house_exterior"
|
||||||
target = "uid://b3fjmiaribbrl"
|
target = "uid://35amqvpjgpf2x"
|
||||||
label = "Cemetery"
|
label = "Cemetery"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_018_cemetery" index="0"]
|
[node name="entrance" parent="kq4_018_cemetery" index="0"]
|
||||||
@@ -81,7 +81,7 @@ position = Vector2(151, 615)
|
|||||||
[node name="kq4_016_graveyard" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
[node name="kq4_016_graveyard" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_017_spooky_house_exterior"
|
appear_at_node = "kq4_017_spooky_house_exterior"
|
||||||
target = "uid://5gygr0s1n433"
|
target = "uid://27b2k6gky3afg"
|
||||||
label = "Graveyard"
|
label = "Graveyard"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_016_graveyard" index="0"]
|
[node name="entrance" parent="kq4_016_graveyard" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://dek2gdmwnmgsl
|
uid://1kz9yo5f1tpc6
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ position = Vector2(151, 615)
|
|||||||
[node name="kq4_017_spooky_house_exterior" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
[node name="kq4_017_spooky_house_exterior" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_018_cemetery"
|
appear_at_node = "kq4_018_cemetery"
|
||||||
target = "uid://dek2gdmwnmgsl"
|
target = "uid://1kz9yo5f1tpc6"
|
||||||
label = "Spooky House Exterior"
|
label = "Spooky House Exterior"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_017_spooky_house_exterior" index="0"]
|
[node name="entrance" parent="kq4_017_spooky_house_exterior" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://b3fjmiaribbrl
|
uid://35amqvpjgpf2x
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ navigation_polygon = SubResource("NavigationPolygon_mt6rs")
|
|||||||
[node name="kq4_020_meadow" parent="." index="5" instance=ExtResource("4_8xjvi")]
|
[node name="kq4_020_meadow" parent="." index="5" instance=ExtResource("4_8xjvi")]
|
||||||
polygon = PackedVector2Array(1821, 506, 1756, 1110, 2122, 1104, 2067, 511)
|
polygon = PackedVector2Array(1821, 506, 1756, 1110, 2122, 1104, 2067, 511)
|
||||||
appear_at_node = "kq4_019_coastal_cliffs"
|
appear_at_node = "kq4_019_coastal_cliffs"
|
||||||
target = "uid://w4xpm5qeo45d"
|
target = "uid://2yy1t1lic39gp"
|
||||||
label = "Meadow"
|
label = "Meadow"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_020_meadow" index="0"]
|
[node name="entrance" parent="kq4_020_meadow" index="0"]
|
||||||
@@ -46,7 +46,7 @@ position = Vector2(1538, 1274)
|
|||||||
[node name="kq4_013_beach" parent="." index="7" instance=ExtResource("4_8xjvi")]
|
[node name="kq4_013_beach" parent="." index="7" instance=ExtResource("4_8xjvi")]
|
||||||
polygon = PackedVector2Array(1200, 100, 1300, 200, 1500, 200, 1600, 100)
|
polygon = PackedVector2Array(1200, 100, 1300, 200, 1500, 200, 1600, 100)
|
||||||
appear_at_node = "kq4_019_coastal_cliffs"
|
appear_at_node = "kq4_019_coastal_cliffs"
|
||||||
target = "uid://d4a2d0rfqnmmo"
|
target = "uid://2bqawc9w4uu59"
|
||||||
label = "Beach"
|
label = "Beach"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_013_beach" index="0"]
|
[node name="entrance" parent="kq4_013_beach" index="0"]
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_020_meadow"
|
appear_at_node = "kq4_020_meadow"
|
||||||
target = "uid://bs3fll3ml3ffy"
|
target = "uid://3uxipzjekijqc"
|
||||||
label = "Bridge over Stream"
|
label = "Bridge over Stream"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_021_bridge_over_stream" index="0"]
|
[node name="entrance" parent="kq4_021_bridge_over_stream" index="0"]
|
||||||
@@ -84,7 +84,7 @@ position = Vector2(-64, 534)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_020_meadow"
|
appear_at_node = "kq4_020_meadow"
|
||||||
target = "uid://dtay26ehvtu2m"
|
target = "uid://10p7miv0a14l7"
|
||||||
label = "River Meadow"
|
label = "River Meadow"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_026_river_meadow" index="0"]
|
[node name="entrance" parent="kq4_026_river_meadow" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://w4xpm5qeo45d
|
uid://2yy1t1lic39gp
|
||||||
@@ -44,7 +44,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_021_bridge_over_stream"
|
appear_at_node = "kq4_021_bridge_over_stream"
|
||||||
target = "uid://xk6xu65nm620"
|
target = "uid://2zga29mwl2ced"
|
||||||
label = "Frog Pond"
|
label = "Frog Pond"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_015_frog_pond" index="0"]
|
[node name="entrance" parent="kq4_015_frog_pond" index="0"]
|
||||||
@@ -57,7 +57,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_021_bridge_over_stream"
|
appear_at_node = "kq4_021_bridge_over_stream"
|
||||||
target = "uid://bmv1tox6p3h1x"
|
target = "uid://3oq4x3exoimdb"
|
||||||
label = "Gnome's Cottage"
|
label = "Gnome's Cottage"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_022_gnomes_cottage" index="0"]
|
[node name="entrance" parent="kq4_022_gnomes_cottage" index="0"]
|
||||||
@@ -70,7 +70,7 @@ position = Vector2(293, 554)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_021_bridge_over_stream"
|
appear_at_node = "kq4_021_bridge_over_stream"
|
||||||
target = "uid://c8aq5g1juqdam"
|
target = "uid://1fpyosj18xls7"
|
||||||
label = "Forest Path"
|
label = "Forest Path"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_027_forest_path" index="0"]
|
[node name="entrance" parent="kq4_027_forest_path" index="0"]
|
||||||
@@ -83,7 +83,7 @@ position = Vector2(151, 615)
|
|||||||
position = Vector2(-200, 74)
|
position = Vector2(-200, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_021_bridge_over_stream"
|
appear_at_node = "kq4_021_bridge_over_stream"
|
||||||
target = "uid://w4xpm5qeo45d"
|
target = "uid://2yy1t1lic39gp"
|
||||||
label = "Meadow"
|
label = "Meadow"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_020_meadow" index="0"]
|
[node name="entrance" parent="kq4_020_meadow" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://bs3fll3ml3ffy
|
uid://3uxipzjekijqc
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_022_gnomes_cottage"
|
appear_at_node = "kq4_022_gnomes_cottage"
|
||||||
target = "uid://5gygr0s1n433"
|
target = "uid://27b2k6gky3afg"
|
||||||
label = "Graveyard"
|
label = "Graveyard"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_016_graveyard" index="0"]
|
[node name="entrance" parent="kq4_016_graveyard" index="0"]
|
||||||
@@ -56,7 +56,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_022_gnomes_cottage"
|
appear_at_node = "kq4_022_gnomes_cottage"
|
||||||
target = "uid://qkcwifq2lcam"
|
target = "uid://qkcwifq2lb9m"
|
||||||
label = "South Exit"
|
label = "South Exit"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_028_mine_entrance" index="0"]
|
[node name="entrance" parent="kq4_028_mine_entrance" index="0"]
|
||||||
@@ -81,7 +81,7 @@ position = Vector2(293, 554)
|
|||||||
[node name="kq4_021_bridge_over_stream" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
[node name="kq4_021_bridge_over_stream" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_022_gnomes_cottage"
|
appear_at_node = "kq4_022_gnomes_cottage"
|
||||||
target = "uid://bs3fll3ml3ffy"
|
target = "uid://3uxipzjekijqc"
|
||||||
label = "West Exit"
|
label = "West Exit"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_021_bridge_over_stream" index="0"]
|
[node name="entrance" parent="kq4_021_bridge_over_stream" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://bmv1tox6p3h1x
|
uid://3oq4x3exoimdb
|
||||||
@@ -43,7 +43,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_023_forest_path_with_cottage"
|
appear_at_node = "kq4_023_forest_path_with_cottage"
|
||||||
target = "uid://dek2gdmwnmgsl"
|
target = "uid://1kz9yo5f1tpc6"
|
||||||
label = "Spooky House Exterior"
|
label = "Spooky House Exterior"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_017_spooky_house_exterior" index="0"]
|
[node name="entrance" parent="kq4_017_spooky_house_exterior" index="0"]
|
||||||
@@ -69,7 +69,7 @@ position = Vector2(293, 554)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_023_forest_path_with_cottage"
|
appear_at_node = "kq4_023_forest_path_with_cottage"
|
||||||
target = "uid://dlyrp8twdxb4g"
|
target = "uid://1sfzaldfq5kn1"
|
||||||
label = "Dense Forest"
|
label = "Dense Forest"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_029_dense_forest" index="0"]
|
[node name="entrance" parent="kq4_029_dense_forest" index="0"]
|
||||||
@@ -81,7 +81,7 @@ position = Vector2(151, 615)
|
|||||||
[node name="kq4_022_gnomes_cottage" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
[node name="kq4_022_gnomes_cottage" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_023_forest_path_with_cottage"
|
appear_at_node = "kq4_023_forest_path_with_cottage"
|
||||||
target = "uid://bmv1tox6p3h1x"
|
target = "uid://3oq4x3exoimdb"
|
||||||
label = "Gnome's Cottage"
|
label = "Gnome's Cottage"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_022_gnomes_cottage" index="0"]
|
[node name="entrance" parent="kq4_022_gnomes_cottage" index="0"]
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_024_waterfall_and_pool"
|
appear_at_node = "kq4_024_waterfall_and_pool"
|
||||||
target = "uid://b3fjmiaribbrl"
|
target = "uid://35amqvpjgpf2x"
|
||||||
label = "Cemetery"
|
label = "Cemetery"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_018_cemetery" index="0"]
|
[node name="entrance" parent="kq4_018_cemetery" index="0"]
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ position = Vector2(1535, 302)
|
|||||||
position = Vector2(1800, 400)
|
position = Vector2(1800, 400)
|
||||||
polygon = PackedVector2Array(1700, 300, 1900, 300, 1900, 500, 1700, 500)
|
polygon = PackedVector2Array(1700, 300, 1900, 300, 1900, 500, 1700, 500)
|
||||||
appear_at_node = "kq4_025_beach_at_river_delta"
|
appear_at_node = "kq4_025_beach_at_river_delta"
|
||||||
target = "uid://dtay26ehvtu2m"
|
target = "uid://10p7miv0a14l7"
|
||||||
label = "River Meadow"
|
label = "River Meadow"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_026_river_meadow" index="0"]
|
[node name="entrance" parent="kq4_026_river_meadow" index="0"]
|
||||||
@@ -91,7 +91,7 @@ position = Vector2(50, 350)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_025_beach_at_river_delta"
|
appear_at_node = "kq4_025_beach_at_river_delta"
|
||||||
target = "uid://dlg6010ym2uw4"
|
target = "uid://1rwfkejhz94hp"
|
||||||
label = "Beach"
|
label = "Beach"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_001_beach" index="0"]
|
[node name="entrance" parent="kq4_001_beach" index="0"]
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_026_river_meadow"
|
appear_at_node = "kq4_026_river_meadow"
|
||||||
target = "uid://w4xpm5qeo45d"
|
target = "uid://2yy1t1lic39gp"
|
||||||
label = "Meadow"
|
label = "Meadow"
|
||||||
priority = 1
|
priority = 1
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_026_river_meadow"
|
appear_at_node = "kq4_026_river_meadow"
|
||||||
target = "uid://c8aq5g1juqdam"
|
target = "uid://1fpyosj18xls7"
|
||||||
label = "Forest Path"
|
label = "Forest Path"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_027_forest_path" index="0"]
|
[node name="entrance" parent="kq4_027_forest_path" index="0"]
|
||||||
@@ -68,7 +68,7 @@ position = Vector2(293, 554)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_026_river_meadow"
|
appear_at_node = "kq4_026_river_meadow"
|
||||||
target = "uid://dxs1tr5yvmoba"
|
target = "uid://1489d4oh9twtu"
|
||||||
label = "Meadow"
|
label = "Meadow"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_002_meadow" index="0"]
|
[node name="entrance" parent="kq4_002_meadow" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://dtay26ehvtu2m
|
uid://10p7miv0a14l7
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_027_forest_path"
|
appear_at_node = "kq4_027_forest_path"
|
||||||
target = "uid://bs3fll3ml3ffy"
|
target = "uid://3uxipzjekijqc"
|
||||||
label = "Bridge over Stream"
|
label = "Bridge over Stream"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_021_bridge_over_stream" index="0"]
|
[node name="entrance" parent="kq4_021_bridge_over_stream" index="0"]
|
||||||
@@ -56,7 +56,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_027_forest_path"
|
appear_at_node = "kq4_027_forest_path"
|
||||||
target = "uid://qkcwifq2lcam"
|
target = "uid://qkcwifq2lb9m"
|
||||||
label = "Mine Entrance"
|
label = "Mine Entrance"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_028_mine_entrance" index="0"]
|
[node name="entrance" parent="kq4_028_mine_entrance" index="0"]
|
||||||
@@ -69,7 +69,7 @@ position = Vector2(293, 554)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_027_forest_path"
|
appear_at_node = "kq4_027_forest_path"
|
||||||
target = "uid://dyk4rcqsk3aed"
|
target = "uid://151dbn9bybiwx"
|
||||||
label = "Fountain Pool"
|
label = "Fountain Pool"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_003_fountain_pool" index="0"]
|
[node name="entrance" parent="kq4_003_fountain_pool" index="0"]
|
||||||
@@ -81,7 +81,7 @@ position = Vector2(151, 615)
|
|||||||
[node name="kq4_026_river_meadow" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
[node name="kq4_026_river_meadow" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_027_forest_path"
|
appear_at_node = "kq4_027_forest_path"
|
||||||
target = "uid://dtay26ehvtu2m"
|
target = "uid://10p7miv0a14l7"
|
||||||
label = "River Meadow"
|
label = "River Meadow"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_026_river_meadow" index="0"]
|
[node name="entrance" parent="kq4_026_river_meadow" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://c8aq5g1juqdam
|
uid://1fpyosj18xls7
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_028_mine_entrance"
|
appear_at_node = "kq4_028_mine_entrance"
|
||||||
target = "uid://bmv1tox6p3h1x"
|
target = "uid://3oq4x3exoimdb"
|
||||||
label = "Gnome's Cottage"
|
label = "Gnome's Cottage"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_022_gnomes_cottage" index="0"]
|
[node name="entrance" parent="kq4_022_gnomes_cottage" index="0"]
|
||||||
@@ -56,7 +56,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_028_mine_entrance"
|
appear_at_node = "kq4_028_mine_entrance"
|
||||||
target = "uid://dlyrp8twdxb4g"
|
target = "uid://1sfzaldfq5kn1"
|
||||||
label = "Dense Forest"
|
label = "Dense Forest"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_029_dense_forest" index="0"]
|
[node name="entrance" parent="kq4_029_dense_forest" index="0"]
|
||||||
@@ -69,7 +69,7 @@ position = Vector2(293, 554)
|
|||||||
position = Vector2(910, 542)
|
position = Vector2(910, 542)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_028_mine_entrance"
|
appear_at_node = "kq4_028_mine_entrance"
|
||||||
target = "uid://dhie3qsi5333g"
|
target = "uid://1nxmm3b1kcdm1"
|
||||||
label = "Ogre's Cottage"
|
label = "Ogre's Cottage"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_004_ogres_cottage" index="0"]
|
[node name="entrance" parent="kq4_004_ogres_cottage" index="0"]
|
||||||
@@ -81,7 +81,7 @@ position = Vector2(151, 615)
|
|||||||
[node name="kq4_027_forest_path" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
[node name="kq4_027_forest_path" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_028_mine_entrance"
|
appear_at_node = "kq4_028_mine_entrance"
|
||||||
target = "uid://c8aq5g1juqdam"
|
target = "uid://1fpyosj18xls7"
|
||||||
label = "Forest Path"
|
label = "Forest Path"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_027_forest_path" index="0"]
|
[node name="entrance" parent="kq4_027_forest_path" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://qkcwifq2lcam
|
uid://qkcwifq2lb9m
|
||||||
@@ -81,7 +81,7 @@ position = Vector2(151, 615)
|
|||||||
[node name="kq4_028_mine_entrance" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
[node name="kq4_028_mine_entrance" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_029_dense_forest"
|
appear_at_node = "kq4_029_dense_forest"
|
||||||
target = "uid://qkcwifq2lcam"
|
target = "uid://qkcwifq2lb9m"
|
||||||
label = "Mine Entrance"
|
label = "Mine Entrance"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_028_mine_entrance" index="0"]
|
[node name="entrance" parent="kq4_028_mine_entrance" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://dlyrp8twdxb4g
|
uid://1sfzaldfq5kn1
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ position = Vector2(293, 554)
|
|||||||
[node name="kq4_029_dense_forest" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
[node name="kq4_029_dense_forest" parent="." unique_id=1117747814 instance=ExtResource("4_abc")]
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_030_mountain_pass"
|
appear_at_node = "kq4_030_mountain_pass"
|
||||||
target = "uid://dlyrp8twdxb4g"
|
target = "uid://1sfzaldfq5kn1"
|
||||||
label = "Dense Forest"
|
label = "Dense Forest"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_029_dense_forest" index="0"]
|
[node name="entrance" parent="kq4_029_dense_forest" index="0"]
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_031_open_ocean"
|
appear_at_node = "kq4_031_open_ocean"
|
||||||
target = "uid://dlg6010ym2uw4"
|
target = "uid://1rwfkejhz94hp"
|
||||||
label = "Beach"
|
label = "Beach"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_001_beach" index="0"]
|
[node name="entrance" parent="kq4_001_beach" index="0"]
|
||||||
@@ -82,7 +82,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(1500, 200)
|
position = Vector2(1500, 200)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_031_open_ocean"
|
appear_at_node = "kq4_031_open_ocean"
|
||||||
target = "uid://d4a2d0rfqnmmo"
|
target = "uid://2bqawc9w4uu59"
|
||||||
label = "Beach"
|
label = "Beach"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_013_beach" index="0"]
|
[node name="entrance" parent="kq4_013_beach" index="0"]
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
uid://2f7c49hpkducc
|
|
||||||
@@ -41,7 +41,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(910, -213)
|
position = Vector2(910, -213)
|
||||||
polygon = PackedVector2Array(-703, 472, -696, 1033, -462, 953, -471, 562)
|
polygon = PackedVector2Array(-703, 472, -696, 1033, -462, 953, -471, 562)
|
||||||
appear_at_node = "kq4_049_ogres_cottage"
|
appear_at_node = "kq4_049_ogres_cottage"
|
||||||
target = "uid://dhie3qsi5333g"
|
target = "uid://1nxmm3b1kcdm1"
|
||||||
label = "Ogre's Cottage Exterior"
|
label = "Ogre's Cottage Exterior"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_004_ogres_cottage_exterior" index="0"]
|
[node name="entrance" parent="kq4_004_ogres_cottage_exterior" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://c5h5n8dreoa8k
|
uid://1cxd7kvarvjr5
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ position = Vector2(194, 819)
|
|||||||
position = Vector2(1766, 74)
|
position = Vector2(1766, 74)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_051_ogres_closet"
|
appear_at_node = "kq4_051_ogres_closet"
|
||||||
target = "uid://c5h5n8dreoa8k"
|
target = "uid://1cxd7kvarvjr5"
|
||||||
label = "Ogre's Cottage"
|
label = "Ogre's Cottage"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_049_ogres_cottage" index="0"]
|
[node name="entrance" parent="kq4_049_ogres_cottage" index="0"]
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ position = Vector2(200, 900)
|
|||||||
position = Vector2(1766, 600)
|
position = Vector2(1766, 600)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_053_seven_dwarfs_bedroom"
|
appear_at_node = "kq4_053_seven_dwarfs_bedroom"
|
||||||
target = "uid://bmum2mjox5dwl"
|
target = "uid://3opp6zygwkh7x"
|
||||||
label = "054 Seven Dwarfs Cottage"
|
label = "054 Seven Dwarfs Cottage"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_054_seven_dwarfs_cottage" index="0"]
|
[node name="entrance" parent="kq4_054_seven_dwarfs_cottage" index="0"]
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ position = Vector2(188, 628)
|
|||||||
position = Vector2(801, 597)
|
position = Vector2(801, 597)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_054_seven_dwarfs_cottage"
|
appear_at_node = "kq4_054_seven_dwarfs_cottage"
|
||||||
target = "uid://bmv1tox6p3h1x"
|
target = "uid://3oq4x3exoimdb"
|
||||||
label = "022 Gnomes Cottage"
|
label = "022 Gnomes Cottage"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_022_gnomes_cottage" index="0"]
|
[node name="entrance" parent="kq4_022_gnomes_cottage" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://bmum2mjox5dwl
|
uid://3opp6zygwkh7x
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ position = Vector2(174, 519)
|
|||||||
position = Vector2(50, 600)
|
position = Vector2(50, 600)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_055_seven_dwarfs_diamond_mine"
|
appear_at_node = "kq4_055_seven_dwarfs_diamond_mine"
|
||||||
target = "uid://qkcwifq2lcam"
|
target = "uid://qkcwifq2lb9m"
|
||||||
label = "028 Mine Entrance"
|
label = "028 Mine Entrance"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_028_mine_entrance" index="0"]
|
[node name="entrance" parent="kq4_028_mine_entrance" index="0"]
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ position = Vector2(200, 900)
|
|||||||
position = Vector2(1452, 159)
|
position = Vector2(1452, 159)
|
||||||
polygon = PackedVector2Array(62, 224, 50, 647, 277, 662, 295, 181)
|
polygon = PackedVector2Array(62, 224, 50, 647, 277, 662, 295, 181)
|
||||||
appear_at_node = "kq4_059_baby_nursery"
|
appear_at_node = "kq4_059_baby_nursery"
|
||||||
target = "uid://b5eo5ndws4tin"
|
target = "uid://368r91sorjxs0"
|
||||||
label = "062 Bedroom"
|
label = "062 Bedroom"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_062_bedroom" index="0"]
|
[node name="entrance" parent="kq4_062_bedroom" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://dyhaubm3vhano
|
uid://15wiem5l9oi69
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ position = Vector2(200, 900)
|
|||||||
position = Vector2(967, 527)
|
position = Vector2(967, 527)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_060_bedroom"
|
appear_at_node = "kq4_060_bedroom"
|
||||||
target = "uid://bfgygdasrhic6"
|
target = "uid://3hb2kqpkpvmnj"
|
||||||
label = "068 The Foyer"
|
label = "068 The Foyer"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_068_the_foyer" index="0"]
|
[node name="entrance" parent="kq4_068_the_foyer" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://bd2plmdre4f
|
uid://abd2plmdre4f
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ position = Vector2(200, 900)
|
|||||||
position = Vector2(877, 491)
|
position = Vector2(877, 491)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_062_bedroom"
|
appear_at_node = "kq4_062_bedroom"
|
||||||
target = "uid://bfgygdasrhic6"
|
target = "uid://3hb2kqpkpvmnj"
|
||||||
label = "068 The Foyer"
|
label = "068 The Foyer"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_068_the_foyer" index="0"]
|
[node name="entrance" parent="kq4_068_the_foyer" index="0"]
|
||||||
@@ -53,7 +53,7 @@ position = Vector2(130, 684)
|
|||||||
position = Vector2(212, -39)
|
position = Vector2(212, -39)
|
||||||
polygon = PackedVector2Array(46, 377, 108, 814, 214, 811, 221, 419)
|
polygon = PackedVector2Array(46, 377, 108, 814, 214, 811, 221, 419)
|
||||||
appear_at_node = "kq4_062_bedroom"
|
appear_at_node = "kq4_062_bedroom"
|
||||||
target = "uid://dyhaubm3vhano"
|
target = "uid://15wiem5l9oi69"
|
||||||
label = "059 Baby Nursery"
|
label = "059 Baby Nursery"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_059_baby_nursery" index="0"]
|
[node name="entrance" parent="kq4_059_baby_nursery" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://b5eo5ndws4tin
|
uid://368r91sorjxs0
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ position = Vector2(200, 900)
|
|||||||
position = Vector2(351, -41)
|
position = Vector2(351, -41)
|
||||||
polygon = PackedVector2Array(-108, 454, -102, 844, 78, 811, 108, 429)
|
polygon = PackedVector2Array(-108, 454, -102, 844, 78, 811, 108, 429)
|
||||||
appear_at_node = "kq4_064_old_dining_room"
|
appear_at_node = "kq4_064_old_dining_room"
|
||||||
target = "uid://bfgygdasrhic6"
|
target = "uid://3hb2kqpkpvmnj"
|
||||||
label = "068 The Foyer"
|
label = "068 The Foyer"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_068_the_foyer" index="0"]
|
[node name="entrance" parent="kq4_068_the_foyer" index="0"]
|
||||||
@@ -54,7 +54,7 @@ position = Vector2(-97, 787)
|
|||||||
position = Vector2(1217, -17)
|
position = Vector2(1217, -17)
|
||||||
polygon = PackedVector2Array(-49, 358, -18, 700, 179, 703, 181, 351)
|
polygon = PackedVector2Array(-49, 358, -18, 700, 179, 703, 181, 351)
|
||||||
appear_at_node = "kq4_064_old_dining_room"
|
appear_at_node = "kq4_064_old_dining_room"
|
||||||
target = "uid://7r5j24tcpshw"
|
target = "uid://3am8ohkla4wr9"
|
||||||
label = "065 Old Kitchen"
|
label = "065 Old Kitchen"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_065_old_kitchen" index="0"]
|
[node name="entrance" parent="kq4_065_old_kitchen" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://w5po4qisklh8
|
uid://2y0sti59qypsl
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ position = Vector2(200, 900)
|
|||||||
position = Vector2(1103, 522)
|
position = Vector2(1103, 522)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_065_old_kitchen"
|
appear_at_node = "kq4_065_old_kitchen"
|
||||||
target = "uid://w5po4qisklh8"
|
target = "uid://2y0sti59qypsl"
|
||||||
label = "064 Old Dining Room"
|
label = "064 Old Dining Room"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_064_old_dining_room" index="0"]
|
[node name="entrance" parent="kq4_064_old_dining_room" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://7r5j24tcpshw
|
uid://3am8ohkla4wr9
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ position = Vector2(200, 900)
|
|||||||
position = Vector2(50, 600)
|
position = Vector2(50, 600)
|
||||||
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
polygon = PackedVector2Array(-108, 454, -87, 649, 376, 658, 348, 381)
|
||||||
appear_at_node = "kq4_067_the_parlor"
|
appear_at_node = "kq4_067_the_parlor"
|
||||||
target = "uid://bfgygdasrhic6"
|
target = "uid://3hb2kqpkpvmnj"
|
||||||
label = "068 The Foyer"
|
label = "068 The Foyer"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_068_the_foyer" index="0"]
|
[node name="entrance" parent="kq4_068_the_foyer" index="0"]
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ position = Vector2(7, 747)
|
|||||||
position = Vector2(910, 200)
|
position = Vector2(910, 200)
|
||||||
polygon = PackedVector2Array(-396, -99, -479, 209, -147, 228, -161, -61)
|
polygon = PackedVector2Array(-396, -99, -479, 209, -147, 228, -161, -61)
|
||||||
appear_at_node = "kq4_068_the_foyer"
|
appear_at_node = "kq4_068_the_foyer"
|
||||||
target = "uid://bd2plmdre4f"
|
target = "uid://abd2plmdre4f"
|
||||||
label = "060 Bedroom"
|
label = "060 Bedroom"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_060_bedroom" index="0"]
|
[node name="entrance" parent="kq4_060_bedroom" index="0"]
|
||||||
@@ -81,7 +81,7 @@ position = Vector2(-311, 176)
|
|||||||
position = Vector2(973, -346)
|
position = Vector2(973, -346)
|
||||||
polygon = PackedVector2Array(58, 451, 108, 830, 393, 832, 348, 381)
|
polygon = PackedVector2Array(58, 451, 108, 830, 393, 832, 348, 381)
|
||||||
appear_at_node = "kq4_068_the_foyer"
|
appear_at_node = "kq4_068_the_foyer"
|
||||||
target = "uid://b5eo5ndws4tin"
|
target = "uid://368r91sorjxs0"
|
||||||
label = "062 Bedroom"
|
label = "062 Bedroom"
|
||||||
metadata/_edit_lock_ = true
|
metadata/_edit_lock_ = true
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ position = Vector2(207, 737)
|
|||||||
position = Vector2(50, 600)
|
position = Vector2(50, 600)
|
||||||
polygon = PackedVector2Array(1448, -51, 1404, 401, 1680, 390, 1668, 85)
|
polygon = PackedVector2Array(1448, -51, 1404, 401, 1680, 390, 1668, 85)
|
||||||
appear_at_node = "kq4_068_the_foyer"
|
appear_at_node = "kq4_068_the_foyer"
|
||||||
target = "uid://w5po4qisklh8"
|
target = "uid://2y0sti59qypsl"
|
||||||
label = "064 Old Dining Room"
|
label = "064 Old Dining Room"
|
||||||
|
|
||||||
[node name="entrance" parent="kq4_064_old_dining_room" index="0"]
|
[node name="entrance" parent="kq4_064_old_dining_room" index="0"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://bfgygdasrhic6
|
uid://3hb2kqpkpvmnj
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
extends Node2D
|
||||||
|
class_name TransitionalRoom
|
||||||
|
|
||||||
|
@onready var ego: Node2D = $"../ego"
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _on_room_looked() -> void:
|
||||||
|
pass
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bg2in3fw1di73
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
[gd_scene format=3 uid="uid://3uw3e4ki6p2md"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scenes/kq4_099_transitional_room/kq4_099_transitional_room.gd" id="1_script"]
|
||||||
|
[ext_resource type="Script" uid="uid://xmphq3i0wbg3" path="res://ScalePoint_.gd" id="3_scale"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://c4vc1wx7k6cw" path="res://TransitionPiece.tscn" id="4_transition"]
|
||||||
|
|
||||||
|
[sub_resource type="NavigationPolygon" id="NavigationPolygon_main"]
|
||||||
|
vertices = PackedVector2Array(100, 800, 1800, 800, 1800, 1200, 100, 1200)
|
||||||
|
polygons = Array[PackedInt32Array]([PackedInt32Array(0, 1, 2, 3)])
|
||||||
|
outlines = Array[PackedVector2Array]([PackedVector2Array(100, 800, 1800, 800, 1800, 1200, 100, 1200)])
|
||||||
|
|
||||||
|
[node name="background" type="Node2D"]
|
||||||
|
y_sort_enabled = true
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="StartScalePoint" type="Node2D" parent="."]
|
||||||
|
position = Vector2(200, 900)
|
||||||
|
script = ExtResource("3_scale")
|
||||||
|
target_scale = 0.3
|
||||||
|
|
||||||
|
[node name="EndScalePoint" type="Node2D" parent="."]
|
||||||
|
position = Vector2(1800, 1100)
|
||||||
|
script = ExtResource("3_scale")
|
||||||
|
target_scale = 0.4
|
||||||
|
|
||||||
|
[node name="pathfind" type="NavigationRegion2D" parent="."]
|
||||||
|
navigation_polygon = SubResource("NavigationPolygon_main")
|
||||||
|
|
||||||
|
[node name="default-starting-point" type="Node2D" parent="."]
|
||||||
|
position = Vector2(200, 900)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://3uw3e4ki6p2md
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bk8q2nad4pu44
|
||||||
40
scenes/kq4_099_transitional_room/pic_99_visual.png.import
Normal file
40
scenes/kq4_099_transitional_room/pic_99_visual.png.import
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://3vitc6ejvwys2"
|
||||||
|
path="res://.godot/imported/pic_99_visual.png-PLACEHOLDER.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://scenes/kq4_099_transitional_room/pic_99_visual.png"
|
||||||
|
dest_files=["res://.godot/imported/pic_99_visual.png-PLACEHOLDER.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Builds a queryable room adjacency graph from .tscn files with BFS pathfinding.
|
|
||||||
|
|
||||||
Parses all scene files to extract TransitionPiece exits, builds an adjacency
|
|
||||||
graph, and provides shortest-path queries between any two rooms.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python scripts/build_room_graph.py # print full graph
|
|
||||||
python scripts/build_room_graph.py --from kq4_001_beach --to kq4_092_lolottes_throne_room
|
|
||||||
|
|
||||||
Can also be imported as a module for use by other tools.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TransitionInfo:
|
|
||||||
"""A single room exit / TransitionPiece."""
|
|
||||||
|
|
||||||
exit_node_name: str # Node name in source room's scene tree (e.g., "kq4_010_forest_path")
|
|
||||||
target_uid: str # UID of destination .tscn file
|
|
||||||
appear_at_node: str # Node name in destination room where player appears
|
|
||||||
label: str # Human-readable label (e.g., "Forest Path")
|
|
||||||
polygon: list[tuple[float, float]] # Polygon vertices relative to transition node position
|
|
||||||
position: tuple[float, float] = (0.0, 0.0) # Node position in parent space
|
|
||||||
scale: tuple[float, float] = (1.0, 1.0) # Node scale factors
|
|
||||||
|
|
||||||
def viewport_centroid(self) -> tuple[float, float]:
|
|
||||||
"""Compute the polygon centroid transformed to viewport coordinates."""
|
|
||||||
if not self.polygon:
|
|
||||||
return self.position
|
|
||||||
cx = sum(p[0] for p in self.polygon) / len(self.polygon)
|
|
||||||
cy = sum(p[1] for p in self.polygon) / len(self.polygon)
|
|
||||||
return (
|
|
||||||
self.scale[0] * (self.position[0] + cx),
|
|
||||||
self.scale[1] * (self.position[1] + cy),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RoomInfo:
|
|
||||||
"""Metadata for a single room."""
|
|
||||||
|
|
||||||
name: str # e.g., "kq4_004_ogres_cottage"
|
|
||||||
uid: Optional[str] # Scene UID from .tscn header
|
|
||||||
path: Path # Absolute path to .tscn file
|
|
||||||
transitions: list[TransitionInfo] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class NavigationStep:
|
|
||||||
"""One step in a navigation plan (source room → destination room)."""
|
|
||||||
|
|
||||||
from_room: str # Source room name
|
|
||||||
exit_node_name: str # TransitionPiece node name to click on
|
|
||||||
to_room: str # Destination room name
|
|
||||||
label: str # Human-readable label
|
|
||||||
polygon: list[tuple[float, float]]
|
|
||||||
position: tuple[float, float] = (0.0, 0.0)
|
|
||||||
scale: tuple[float, float] = (1.0, 1.0)
|
|
||||||
|
|
||||||
def viewport_centroid(self) -> tuple[float, float]:
|
|
||||||
"""Compute the polygon centroid transformed to viewport coordinates."""
|
|
||||||
if not self.polygon:
|
|
||||||
return self.position
|
|
||||||
cx = sum(p[0] for p in self.polygon) / len(self.polygon)
|
|
||||||
cy = sum(p[1] for p in self.polygon) / len(self.polygon)
|
|
||||||
return (
|
|
||||||
self.scale[0] * (self.position[0] + cx),
|
|
||||||
self.scale[1] * (self.position[1] + cy),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def find_uid_files(root: Path) -> dict[str, Path]:
|
|
||||||
"""Build a mapping of UID → file path from all .uid files."""
|
|
||||||
uid_map = {}
|
|
||||||
for uid_file in root.rglob("*.uid"):
|
|
||||||
content = uid_file.read_text().strip()
|
|
||||||
if content.startswith("uid://"):
|
|
||||||
resource_path = uid_file.with_suffix("")
|
|
||||||
uid_map[content] = resource_path
|
|
||||||
return uid_map
|
|
||||||
|
|
||||||
|
|
||||||
def parse_transitions(body: str, room_name: str) -> list[TransitionInfo]:
|
|
||||||
"""Extract TransitionPiece transitions from a .tscn file body."""
|
|
||||||
transitions = []
|
|
||||||
|
|
||||||
transition_pattern = re.compile(
|
|
||||||
r'\[node name="([^"]+)"[^\]]*instance=ExtResource\([^\)]+\)\]\s*\n'
|
|
||||||
r"(([^\[]+(?:\n|$))*?)(?=\[|$)",
|
|
||||||
re.MULTILINE,
|
|
||||||
)
|
|
||||||
|
|
||||||
for match in transition_pattern.finditer(body):
|
|
||||||
node_name = match.group(1)
|
|
||||||
body_text = match.group(2)
|
|
||||||
|
|
||||||
target_match = re.search(r'^target = "([^"]+)"', body_text, re.MULTILINE)
|
|
||||||
appear_match = re.search(
|
|
||||||
r'^(?:appear_at_node = "([^"]+)"|appear_at_node = &"([^"]+)")',
|
|
||||||
body_text,
|
|
||||||
re.MULTILINE,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not (target_match and appear_match):
|
|
||||||
continue
|
|
||||||
|
|
||||||
target_uid = target_match.group(1)
|
|
||||||
appear_at = appear_match.group(1) or appear_match.group(2)
|
|
||||||
|
|
||||||
label_match = re.search(r'^label = "([^"]+)"', body_text, re.MULTILINE)
|
|
||||||
label = label_match.group(1) if label_match else node_name
|
|
||||||
|
|
||||||
# Parse position (defaults to 0, 0)
|
|
||||||
pos_match = re.search(r"^position\s*=\s*Vector2?\(([^)]+)\)", body_text, re.MULTILINE)
|
|
||||||
if pos_match:
|
|
||||||
pos_nums = [float(x.strip()) for x in pos_match.group(1).split(",")]
|
|
||||||
node_position = (pos_nums[0], pos_nums[1])
|
|
||||||
else:
|
|
||||||
node_position = (0.0, 0.0)
|
|
||||||
|
|
||||||
# Parse scale (defaults to 1, 1)
|
|
||||||
scale_match = re.search(r"^scale\s*=\s*Vector2?\(([^)]+)\)", body_text, re.MULTILINE)
|
|
||||||
if scale_match:
|
|
||||||
scale_nums = [float(x.strip()) for x in scale_match.group(1).split(",")]
|
|
||||||
node_scale = (scale_nums[0], scale_nums[1])
|
|
||||||
else:
|
|
||||||
node_scale = (1.0, 1.0)
|
|
||||||
|
|
||||||
polygon_str_match = re.search(
|
|
||||||
r"^polygon = PackedVector2Array\((.+?)\)",
|
|
||||||
body_text,
|
|
||||||
re.MULTILINE,
|
|
||||||
)
|
|
||||||
polygon: list[tuple[float, float]] = []
|
|
||||||
if polygon_str_match:
|
|
||||||
nums = re.findall(r"[-+]?[0-9]*\.?[0-9]+", polygon_str_match.group(1))
|
|
||||||
floats = [float(n) for n in nums]
|
|
||||||
polygon = [(floats[i], floats[i + 1]) for i in range(0, len(floats), 2)]
|
|
||||||
|
|
||||||
transitions.append(
|
|
||||||
TransitionInfo(
|
|
||||||
exit_node_name=node_name,
|
|
||||||
target_uid=target_uid,
|
|
||||||
appear_at_node=appear_at,
|
|
||||||
label=label,
|
|
||||||
polygon=polygon,
|
|
||||||
position=node_position,
|
|
||||||
scale=node_scale,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return transitions
|
|
||||||
|
|
||||||
|
|
||||||
def build_graph(scenes_dir: Path) -> dict[str, RoomInfo]:
|
|
||||||
"""Parse all room .tscn files and return a dict keyed by room name."""
|
|
||||||
scene_files = sorted(scenes_dir.glob("kq4_*/kq4_*.tscn"))
|
|
||||||
|
|
||||||
graph: dict[str, RoomInfo] = {}
|
|
||||||
for sf in scene_files:
|
|
||||||
if "placeholder_template" in str(sf):
|
|
||||||
continue
|
|
||||||
|
|
||||||
content = sf.read_text()
|
|
||||||
uid_match = re.search(r'\[gd_scene[^\]]*uid="([^"]+)"', content)
|
|
||||||
room_uid = uid_match.group(1) if uid_match else None
|
|
||||||
|
|
||||||
room_name = sf.stem
|
|
||||||
transitions = parse_transitions(content, room_name)
|
|
||||||
|
|
||||||
graph[room_name] = RoomInfo(
|
|
||||||
name=room_name,
|
|
||||||
uid=room_uid,
|
|
||||||
path=sf,
|
|
||||||
transitions=transitions,
|
|
||||||
)
|
|
||||||
|
|
||||||
return graph
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_target_room(trans: TransitionInfo, graph: dict[str, RoomInfo]) -> Optional[str]:
|
|
||||||
"""Given a transition and the room graph, find which room it targets."""
|
|
||||||
for room_name, room_info in graph.items():
|
|
||||||
if room_info.uid == trans.target_uid:
|
|
||||||
return room_name
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def find_path(
|
|
||||||
graph: dict[str, RoomInfo], start_room: str, end_room: str
|
|
||||||
) -> Optional[list[NavigationStep]]:
|
|
||||||
"""BFS from start to end. Returns ordered list of steps or None."""
|
|
||||||
if start_room not in graph:
|
|
||||||
raise ValueError(f"Start room not found in graph: {start_room}")
|
|
||||||
if end_room not in graph:
|
|
||||||
raise ValueError(f"End room not found in graph: {end_room}")
|
|
||||||
|
|
||||||
if start_room == end_room:
|
|
||||||
return []
|
|
||||||
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
adj: dict[str, list[tuple[str, TransitionInfo]]] = {}
|
|
||||||
for rname, rinfo in graph.items():
|
|
||||||
adj[rname] = []
|
|
||||||
for t in rinfo.transitions:
|
|
||||||
target = _resolve_target_room(t, graph)
|
|
||||||
if target:
|
|
||||||
adj[rname].append((target, t))
|
|
||||||
|
|
||||||
visited = {start_room}
|
|
||||||
queue = deque([(start_room, [])])
|
|
||||||
|
|
||||||
while queue:
|
|
||||||
current, path_steps = queue.popleft()
|
|
||||||
for neighbor, trans in adj.get(current, []):
|
|
||||||
if neighbor in visited:
|
|
||||||
continue
|
|
||||||
|
|
||||||
step = NavigationStep(
|
|
||||||
from_room=current,
|
|
||||||
exit_node_name=trans.exit_node_name,
|
|
||||||
to_room=neighbor,
|
|
||||||
label=trans.label,
|
|
||||||
polygon=trans.polygon,
|
|
||||||
position=trans.position,
|
|
||||||
scale=trans.scale,
|
|
||||||
)
|
|
||||||
new_steps = path_steps + [step]
|
|
||||||
|
|
||||||
if neighbor == end_room:
|
|
||||||
return new_steps
|
|
||||||
|
|
||||||
visited.add(neighbor)
|
|
||||||
queue.append((neighbor, new_steps))
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_connected_components(graph: dict[str, RoomInfo]) -> list[set[str]]:
|
|
||||||
"""Return list of connected components in the room graph."""
|
|
||||||
adj: dict[str, set[str]] = {r: set() for r in graph}
|
|
||||||
for rname, rinfo in graph.items():
|
|
||||||
for t in rinfo.transitions:
|
|
||||||
target = _resolve_target_room(t, graph)
|
|
||||||
if target:
|
|
||||||
adj[rname].add(target)
|
|
||||||
adj[target].add(rname)
|
|
||||||
|
|
||||||
visited = set()
|
|
||||||
components = []
|
|
||||||
|
|
||||||
for room in graph:
|
|
||||||
if room in visited:
|
|
||||||
continue
|
|
||||||
component = set()
|
|
||||||
stack = [room]
|
|
||||||
while stack:
|
|
||||||
current = stack.pop()
|
|
||||||
if current in visited:
|
|
||||||
continue
|
|
||||||
visited.add(current)
|
|
||||||
component.add(current)
|
|
||||||
stack.extend(adj[current] - visited)
|
|
||||||
components.append(component)
|
|
||||||
|
|
||||||
return sorted(components, key=len, reverse=True)
|
|
||||||
|
|
||||||
|
|
||||||
def print_graph_summary(graph: dict[str, RoomInfo]) -> None:
|
|
||||||
"""Print a human-readable summary of the room graph."""
|
|
||||||
total_rooms = len(graph)
|
|
||||||
total_exits = sum(len(r.transitions) for r in graph.values())
|
|
||||||
components = get_connected_components(graph)
|
|
||||||
|
|
||||||
print(f"Rooms: {total_rooms}")
|
|
||||||
print(f"Total transition exits: {total_exits}")
|
|
||||||
print(f"Connected components: {len(components)}")
|
|
||||||
|
|
||||||
if len(components) > 1:
|
|
||||||
for i, comp in enumerate(components):
|
|
||||||
print(f" Component {i + 1} ({len(comp)} rooms): {', '.join(sorted(comp)[:6])}{'...' if len(comp) > 6 else ''}")
|
|
||||||
else:
|
|
||||||
print(" All rooms connected!")
|
|
||||||
|
|
||||||
|
|
||||||
def print_room(graph: dict[str, RoomInfo], room_name: str) -> None:
|
|
||||||
"""Print exits for a specific room."""
|
|
||||||
if room_name not in graph:
|
|
||||||
print(f"Room not found: {room_name}")
|
|
||||||
return
|
|
||||||
|
|
||||||
room = graph[room_name]
|
|
||||||
print(f"Room: {room_name} (uid: {room.uid or 'unknown'})")
|
|
||||||
print(f" Exits ({len(room.transitions)}):")
|
|
||||||
for t in room.transitions:
|
|
||||||
target_room = _resolve_target_room(t, graph)
|
|
||||||
target_str = target_room or t.target_uid[:12]
|
|
||||||
print(f" {t.exit_node_name} → {target_str} [{t.label}] (polygon: {len(t.polygon)} pts)")
|
|
||||||
|
|
||||||
|
|
||||||
def find_and_print_path(
|
|
||||||
graph: dict[str, RoomInfo], start_room: str, end_room: str
|
|
||||||
) -> None:
|
|
||||||
"""Find and print a path between two rooms."""
|
|
||||||
steps = find_path(graph, start_room, end_room)
|
|
||||||
|
|
||||||
if not steps:
|
|
||||||
print(f"No path from {start_room} to {end_room}")
|
|
||||||
components = get_connected_components(graph)
|
|
||||||
for comp in components:
|
|
||||||
if start_room in comp:
|
|
||||||
print(f" {start_room} is in component with {len(comp)} rooms")
|
|
||||||
break
|
|
||||||
for comp in components:
|
|
||||||
if end_room in comp:
|
|
||||||
print(f" {end_room} is in component with {len(comp)} rooms")
|
|
||||||
break
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"Path from {start_room} → {end_room} ({len(steps)} step{'s' if len(steps) != 1 else ''}):")
|
|
||||||
for i, step in enumerate(steps):
|
|
||||||
coord_str = ""
|
|
||||||
cx, cy = step.viewport_centroid()
|
|
||||||
coord_str = f" (click at: {cx:.0f}, {cy:.0f})"
|
|
||||||
print(
|
|
||||||
f" {i + 1}. Click '{step.exit_node_name}' "
|
|
||||||
f"in {step.from_room} "
|
|
||||||
f"→ {step.to_room} [{step.label}]{coord_str}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="KQ4 room graph builder and BFS pathfinder")
|
|
||||||
parser.add_argument("--from", dest="from_room", help="Starting room name (e.g., kq4_001_beach)")
|
|
||||||
parser.add_argument("--to", dest="to_room", help="Destination room name")
|
|
||||||
parser.add_argument("--room", help="Show exits for a specific room")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
root = Path(__file__).resolve().parent.parent
|
|
||||||
scenes_dir = root / "scenes"
|
|
||||||
|
|
||||||
if not scenes_dir.exists():
|
|
||||||
print(f"ERROR: Scenes directory not found at {scenes_dir}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
graph = build_graph(scenes_dir)
|
|
||||||
|
|
||||||
if args.room:
|
|
||||||
print_room(graph, args.room)
|
|
||||||
elif args.from_room and args.to_room:
|
|
||||||
find_and_print_path(graph, args.from_room, args.to_room)
|
|
||||||
else:
|
|
||||||
print_graph_summary(graph)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -14,18 +14,9 @@ const BUSY_TIMEOUT: float = 30.0
|
|||||||
var _key_map: Dictionary
|
var _key_map: Dictionary
|
||||||
var _held_keys: Dictionary = {}
|
var _held_keys: Dictionary = {}
|
||||||
|
|
||||||
var _debugger_safe: bool = false
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
# Ensure MCP server keeps processing even when game is paused
|
||||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
if EngineDebugger.is_active():
|
|
||||||
EngineDebugger.send_message("core:set_skip_breakpoints", [true])
|
|
||||||
EngineDebugger.send_message("core:set_ignore_error_breaks", [true])
|
|
||||||
_debugger_safe = EngineDebugger.is_skipping_breakpoints()
|
|
||||||
if _debugger_safe:
|
|
||||||
print("McpInteractionServer: Debugger breakpoints safely disabled")
|
|
||||||
else:
|
|
||||||
push_warning("McpInteractionServer: Could not disable debugger breakpoints (LocalDebugger?). Eval with invalid code may hang the game.")
|
|
||||||
_init_key_map()
|
_init_key_map()
|
||||||
_server = TCPServer.new()
|
_server = TCPServer.new()
|
||||||
var err: int = _server.listen(PORT, "127.0.0.1")
|
var err: int = _server.listen(PORT, "127.0.0.1")
|
||||||
@@ -553,41 +544,8 @@ func _cmd_eval(params: Dictionary) -> void:
|
|||||||
_send_response({"error": "No code provided"})
|
_send_response({"error": "No code provided"})
|
||||||
return
|
return
|
||||||
|
|
||||||
var script_source: String = _build_eval_script(code)
|
# Wrap user code in a function so we can capture the return value
|
||||||
|
var script_source: String = """extends Node
|
||||||
if EngineDebugger.is_active() and not _debugger_safe:
|
|
||||||
var ext_validation: Dictionary = _validate_script_external(script_source)
|
|
||||||
if not ext_validation.get("valid", false):
|
|
||||||
_send_response({"error": "Script validation failed: %s" % ext_validation.get("error", "unknown error")})
|
|
||||||
return
|
|
||||||
|
|
||||||
var validation: Dictionary = _validate_script_source(script_source)
|
|
||||||
if not validation.get("valid", false):
|
|
||||||
_send_response({"error": "Script validation failed: %s" % validation.get("error", "unknown error"), "error_code": validation.get("error_code", -1)})
|
|
||||||
return
|
|
||||||
|
|
||||||
var script: GDScript = GDScript.new()
|
|
||||||
script.source_code = script_source
|
|
||||||
var err: int = script.reload()
|
|
||||||
if err != OK:
|
|
||||||
_send_response({"error": "Failed to compile GDScript (error %d). Check syntax." % err})
|
|
||||||
return
|
|
||||||
|
|
||||||
var temp_node: Node = Node.new()
|
|
||||||
temp_node.set_script(script)
|
|
||||||
temp_node.process_mode = Node.PROCESS_MODE_ALWAYS
|
|
||||||
add_child(temp_node)
|
|
||||||
|
|
||||||
var result: Variant = null
|
|
||||||
if temp_node.has_method("execute"):
|
|
||||||
result = await temp_node.execute()
|
|
||||||
|
|
||||||
temp_node.queue_free()
|
|
||||||
_send_response({"success": true, "result": _variant_to_json(result)})
|
|
||||||
|
|
||||||
|
|
||||||
func _build_eval_script(code: String) -> String:
|
|
||||||
return """extends Node
|
|
||||||
|
|
||||||
func execute():
|
func execute():
|
||||||
var __result = null
|
var __result = null
|
||||||
@@ -598,55 +556,25 @@ func _run():
|
|||||||
%s
|
%s
|
||||||
""" % [_indent_code(code)]
|
""" % [_indent_code(code)]
|
||||||
|
|
||||||
|
var script: GDScript = GDScript.new()
|
||||||
func _validate_script_source(source: String) -> Dictionary:
|
script.source_code = script_source
|
||||||
var test_script: GDScript = GDScript.new()
|
var err: int = script.reload()
|
||||||
test_script.source_code = source
|
|
||||||
var err: int = test_script.reload()
|
|
||||||
if err != OK:
|
if err != OK:
|
||||||
var error_name: String = ""
|
_send_response({"error": "Failed to compile GDScript (error %d). Check syntax." % err})
|
||||||
match err:
|
return
|
||||||
ERR_PARSE_ERROR:
|
|
||||||
error_name = "Parse error"
|
|
||||||
ERR_COMPILATION_FAILED:
|
|
||||||
error_name = "Compilation failed"
|
|
||||||
_:
|
|
||||||
error_name = "Error code %d" % err
|
|
||||||
return {"valid": false, "error": error_name, "error_code": err}
|
|
||||||
return {"valid": true}
|
|
||||||
|
|
||||||
|
var temp_node: Node = Node.new()
|
||||||
|
temp_node.set_script(script)
|
||||||
|
# Allow eval to work even when game is paused
|
||||||
|
temp_node.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
add_child(temp_node)
|
||||||
|
|
||||||
func _validate_script_external(source: String) -> Dictionary:
|
var result: Variant = null
|
||||||
var temp_path: String = "user://_mcp_eval_validate_%d.gd" % Time.get_ticks_msec()
|
if temp_node.has_method("execute"):
|
||||||
var file: FileAccess = FileAccess.open(temp_path, FileAccess.WRITE)
|
result = await temp_node.execute()
|
||||||
if file == null:
|
|
||||||
return {"valid": true}
|
|
||||||
file.store_string(source)
|
|
||||||
file.close()
|
|
||||||
|
|
||||||
var godot_path: String = OS.get_executable_path()
|
temp_node.queue_free()
|
||||||
var project_path: String = ProjectSettings.globalize_path("res://")
|
_send_response({"success": true, "result": _variant_to_json(result)})
|
||||||
var global_temp: String = ProjectSettings.globalize_path(temp_path)
|
|
||||||
var output: Array = []
|
|
||||||
var exit_code: int = OS.execute(godot_path, [
|
|
||||||
"--headless",
|
|
||||||
"--check-only",
|
|
||||||
"--script", global_temp,
|
|
||||||
"--path", project_path
|
|
||||||
], output)
|
|
||||||
|
|
||||||
DirAccess.remove_absolute(global_temp)
|
|
||||||
|
|
||||||
if exit_code != 0:
|
|
||||||
var error_detail: String = ""
|
|
||||||
for line in output:
|
|
||||||
error_detail += line + "\n"
|
|
||||||
error_detail = error_detail.strip_edges()
|
|
||||||
var msg: String = "Syntax error in eval code (external validation)"
|
|
||||||
if not error_detail.is_empty():
|
|
||||||
msg += ": " + error_detail
|
|
||||||
return {"valid": false, "error": msg}
|
|
||||||
return {"valid": true}
|
|
||||||
|
|
||||||
|
|
||||||
func _indent_code(code: String) -> String:
|
func _indent_code(code: String) -> String:
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""KQ4 Room Navigator — BFS pathfinding + MCP runtime navigation.
|
|
||||||
|
|
||||||
Uses file parsing to build a room adjacency graph, finds shortest paths via BFS,
|
|
||||||
then optionally uses the Godot MCP server (port 9090) to execute the navigation
|
|
||||||
step by step with click coordinates computed from TransitionPiece polygons.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
# Just print the path plan:
|
|
||||||
python tools/kq4_room_navigator.py --from kq4_001_beach --to kq4_092_lolottes_throne_room
|
|
||||||
|
|
||||||
# Navigate with MCP (game must be running):
|
|
||||||
python tools/kq4_room_navigator.py --from kq4_003_fountain_pool --to kq4_010_forest_path --navigate
|
|
||||||
|
|
||||||
# Just print graph summary:
|
|
||||||
python tools/kq4_room_navigator.py --summary
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Add project root to path for imports
|
|
||||||
_project_root = Path(__file__).resolve().parent.parent
|
|
||||||
sys.path.insert(0, str(_project_root))
|
|
||||||
sys.path.insert(0, str(_project_root / "tools"))
|
|
||||||
sys.path.insert(0, str(_project_root / "scripts"))
|
|
||||||
|
|
||||||
from build_room_graph import (
|
|
||||||
NavigationStep,
|
|
||||||
build_graph,
|
|
||||||
find_path,
|
|
||||||
find_and_print_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
BACKGROOM_PATH = "/root/Node2D/SceneViewport/background"
|
|
||||||
|
|
||||||
|
|
||||||
class NavigationError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="KQ4 Room Navigator — plan and execute room-to-room navigation"
|
|
||||||
)
|
|
||||||
parser.add_argument("--from", dest="from_room", help="Starting room name (e.g., kq4_003_fountain_pool)")
|
|
||||||
parser.add_argument("--to", dest="to_room", help="Destination room name")
|
|
||||||
parser.add_argument("--summary", action="store_true", help="Print room graph summary")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
scenes_dir = _project_root / "scenes"
|
|
||||||
if not scenes_dir.exists():
|
|
||||||
print(f"ERROR: Scenes directory not found at {scenes_dir}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
graph = build_graph(scenes_dir)
|
|
||||||
|
|
||||||
if args.summary:
|
|
||||||
from build_room_graph import print_graph_summary
|
|
||||||
|
|
||||||
print_graph_summary(graph)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not (args.from_room and args.to_room):
|
|
||||||
parser.print_help()
|
|
||||||
return
|
|
||||||
|
|
||||||
steps = find_path(graph, args.from_room, args.to_room)
|
|
||||||
if steps is None:
|
|
||||||
from build_room_graph import find_and_print_path
|
|
||||||
|
|
||||||
find_and_print_path(graph, args.from_room, args.to_room)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Print path plan
|
|
||||||
print(f"\nPath: {args.from_room} → {args.to_room} ({len(steps)} steps)\n")
|
|
||||||
for i, step in enumerate(steps):
|
|
||||||
cx, cy = step.viewport_centroid()
|
|
||||||
coord_note = f" (click at: {cx:.0f}, {cy:.0f})" if step.polygon else ""
|
|
||||||
print(
|
|
||||||
f" {i + 1}. Click '{step.exit_node_name}' "
|
|
||||||
f"in {step.from_room} → {step.to_room} [{step.label}]{coord_note}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Scans and repairs UID mismatches across all KQ4 room scenes.
|
|
||||||
|
|
||||||
Fixes two types of drift:
|
|
||||||
1. .tscn.headers have a different UID than the .uid companion file
|
|
||||||
2. Source rooms reference stale target UIDs from .uid companions instead of
|
|
||||||
the current .tscn header UID
|
|
||||||
|
|
||||||
After repair, check_transitions.py and build_room_graph.py report consistent results.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
# Detect (show what's wrong)
|
|
||||||
python tools/repair_uids.py --detect
|
|
||||||
|
|
||||||
# Dry run (show what would change)
|
|
||||||
python tools/repair_uids.py --fix --dry-run
|
|
||||||
|
|
||||||
# Apply fixes
|
|
||||||
python tools/repair_uids.py --fix
|
|
||||||
|
|
||||||
# Fix a specific room only
|
|
||||||
python tools/repair_uids.py --fix --room kq4_018_cemetery
|
|
||||||
|
|
||||||
# Verbose output
|
|
||||||
python tools/repair_uids.py --detect -v
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
def generate_godot_uid() -> str:
|
|
||||||
"""Generate a fresh Godot-compatible UID (base36 encoded 64-bit random)."""
|
|
||||||
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
||||||
|
|
||||||
def to_base36(num: int) -> str:
|
|
||||||
if num == 0:
|
|
||||||
return "0"
|
|
||||||
chars = []
|
|
||||||
while num:
|
|
||||||
num, rem = divmod(num, 36)
|
|
||||||
chars.append(ALPHABET[rem])
|
|
||||||
return "".join(reversed(chars))
|
|
||||||
|
|
||||||
import secrets
|
|
||||||
value = secrets.randbits(64)
|
|
||||||
return f"uid://{to_base36(value)}"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RoomData:
|
|
||||||
"""Parsed data for a single room scene."""
|
|
||||||
name: str
|
|
||||||
path: Path
|
|
||||||
header_uid: Optional[str] # uid from .tscn [gd_scene ...] line
|
|
||||||
companion_uid: Optional[str] # uid from .uid file (may be stale or missing)
|
|
||||||
target_uids: list[dict] # [{"exit_node": str, "target_uid": str}, ...]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Mismatch:
|
|
||||||
"""A single detected UID problem."""
|
|
||||||
category: str # "companion_mismatch" | "missing_companion" | "stale_target"
|
|
||||||
room_name: str
|
|
||||||
detail: str
|
|
||||||
fixable: bool = True
|
|
||||||
fix_uid: Optional[str] = None
|
|
||||||
source_file: Optional[Path] = None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_all_rooms(scenes_dir: Path) -> dict[str, RoomData]:
|
|
||||||
"""Parse every kq4_* .tscn scene and return data keyed by room name."""
|
|
||||||
rooms: dict[str, RoomData] = {}
|
|
||||||
|
|
||||||
for tscn in sorted(scenes_dir.glob("kq4_*/kq4_*.tscn")):
|
|
||||||
if "placeholder" in str(tscn):
|
|
||||||
continue
|
|
||||||
|
|
||||||
content = tscn.read_text()
|
|
||||||
|
|
||||||
# Extract header UID
|
|
||||||
header_match = re.search(r'\[gd_scene[^\]]*uid="([^"]+)"', content)
|
|
||||||
header_uid = header_match.group(1) if header_match else None
|
|
||||||
|
|
||||||
# Extract companion .uid file
|
|
||||||
uid_file = Path(str(tscn) + ".uid")
|
|
||||||
companion_uid = uid_file.read_text().strip() if uid_file.exists() else None
|
|
||||||
|
|
||||||
# Extract target UIDs from TransitionPiece nodes
|
|
||||||
target_uids = []
|
|
||||||
transition_pattern = re.compile(
|
|
||||||
r'\[node name="([^"]+)"[^\]]*instance=ExtResource\([^\)]+\)\]\s*\n'
|
|
||||||
r"(([^\[]+(?:\n|$))*)(?=\[|$)",
|
|
||||||
re.MULTILINE,
|
|
||||||
)
|
|
||||||
for match in transition_pattern.finditer(content):
|
|
||||||
node_name = match.group(1)
|
|
||||||
body = match.group(2)
|
|
||||||
target_match = re.search(r'^target = "([^"]+)"', body, re.MULTILINE)
|
|
||||||
if target_match:
|
|
||||||
target_uids.append({
|
|
||||||
"exit_node": node_name,
|
|
||||||
"target_uid": target_match.group(1),
|
|
||||||
})
|
|
||||||
|
|
||||||
rooms[tscn.stem] = RoomData(
|
|
||||||
name=tscn.stem, path=tscn,
|
|
||||||
header_uid=header_uid, companion_uid=companion_uid, target_uids=target_uids,
|
|
||||||
)
|
|
||||||
|
|
||||||
return rooms
|
|
||||||
|
|
||||||
|
|
||||||
def detect_mismatches(rooms: dict[str, RoomData]) -> list[Mismatch]:
|
|
||||||
"""Find all UID problems and return actionable mismatch records."""
|
|
||||||
mismatches: list[Mismatch] = []
|
|
||||||
|
|
||||||
# Build lookup: companion_uid -> room_name (for stale-target detection)
|
|
||||||
companion_to_room: dict[str, str] = {}
|
|
||||||
for rdata in rooms.values():
|
|
||||||
if rdata.companion_uid:
|
|
||||||
companion_to_room[rdata.companion_uid] = rdata.name
|
|
||||||
|
|
||||||
for rname, rd in rooms.items():
|
|
||||||
# 1. Companion doesn't match header (both exist but differ)
|
|
||||||
if rd.header_uid and rd.companion_uid and rd.header_uid != rd.companion_uid:
|
|
||||||
mismatches.append(Mismatch(
|
|
||||||
category="companion_mismatch",
|
|
||||||
room_name=rname,
|
|
||||||
detail=f".uid={rd.companion_uid} .tscn header={rd.header_uid}",
|
|
||||||
fix_uid=rd.header_uid, source_file=Path(str(rd.path) + ".uid"),
|
|
||||||
))
|
|
||||||
|
|
||||||
# 2. Missing companion file
|
|
||||||
if rd.header_uid and not rd.companion_uid:
|
|
||||||
mismatches.append(Mismatch(
|
|
||||||
category="missing_companion",
|
|
||||||
room_name=rname,
|
|
||||||
detail=f"No .uid file for header UID {rd.header_uid}",
|
|
||||||
fix_uid=rd.header_uid, source_file=Path(str(rd.path) + ".uid"),
|
|
||||||
))
|
|
||||||
|
|
||||||
# 3. Stale target references
|
|
||||||
for tgt in rd.target_uids:
|
|
||||||
uid = tgt["target_uid"]
|
|
||||||
if not uid.startswith("uid://"):
|
|
||||||
continue
|
|
||||||
# Target exists in some header -> check companion matches
|
|
||||||
target_room = None
|
|
||||||
for other_name, other_rd in rooms.items():
|
|
||||||
if other_rd.header_uid == uid:
|
|
||||||
target_room = other_name
|
|
||||||
break
|
|
||||||
|
|
||||||
if target_room is None:
|
|
||||||
# Check if the stale UID matches a companion that diverged from header
|
|
||||||
mapped_companion = companion_to_room.get(uid)
|
|
||||||
if mapped_companion and mapped_companion != rooms[mapped_companion].name or \
|
|
||||||
(mapped_companion and rooms[mapped_companion].companion_uid == uid):
|
|
||||||
# Stale companion lookup — the target room exists but source
|
|
||||||
# references its OLD companion UID instead of current header
|
|
||||||
correct_uid = rooms[mapped_companion].header_uid
|
|
||||||
if uid != correct_uid:
|
|
||||||
mismatches.append(Mismatch(
|
|
||||||
category="stale_target",
|
|
||||||
room_name=rname,
|
|
||||||
detail=f"{tgt['exit_node']} -> {uid} (correct: {correct_uid} for {mapped_companion})",
|
|
||||||
fix_uid=correct_uid, source_file=rd.path,
|
|
||||||
))
|
|
||||||
elif target_room is None and uid not in companion_to_room:
|
|
||||||
# Totally orphaned UID — try to auto-resolve via exit node name
|
|
||||||
suggested = suggest_target_from_node(tgt["exit_node"], rooms)
|
|
||||||
if suggested:
|
|
||||||
sugg_uid = rooms[suggested].header_uid
|
|
||||||
mismatches.append(Mismatch(
|
|
||||||
category="stale_target",
|
|
||||||
room_name=rname,
|
|
||||||
detail=f"{tgt['exit_node']} -> {uid} (not found, likely → {suggested}: {sugg_uid})",
|
|
||||||
fix_uid=sugg_uid, source_file=rd.path,
|
|
||||||
))
|
|
||||||
|
|
||||||
return mismatches
|
|
||||||
|
|
||||||
|
|
||||||
def suggest_target_from_node(exit_node_name: str, rooms: dict[str, RoomData]) -> Optional[str]:
|
|
||||||
"""Try to guess target room from exit node naming convention.
|
|
||||||
|
|
||||||
Exit nodes are typically named after their destination (e.g., 'kq4_018_cemetery').
|
|
||||||
"""
|
|
||||||
# Direct match
|
|
||||||
if exit_node_name in rooms:
|
|
||||||
return exit_node_name
|
|
||||||
|
|
||||||
# kq4_XXX_room_node -> kq4_XXX_room (strip trailing node-like suffix)
|
|
||||||
for rname in rooms:
|
|
||||||
if exit_node_name.startswith(rname):
|
|
||||||
return rname
|
|
||||||
if rname in exit_node_name:
|
|
||||||
return rname
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def fix_companion_mismatch(room: RoomData, new_uid: str, dry_run: bool) -> str:
|
|
||||||
"""Write the header UID into the .uid companion file."""
|
|
||||||
uid_path = Path(str(room.path) + ".uid")
|
|
||||||
content = f"{new_uid}\n"
|
|
||||||
|
|
||||||
if not dry_run:
|
|
||||||
uid_path.write_text(content)
|
|
||||||
return f"Synchronizing {room.name}: .uid → {new_uid}"
|
|
||||||
|
|
||||||
|
|
||||||
def fix_missing_companion(room: RoomData, uid: str, dry_run: bool) -> str:
|
|
||||||
"""Create a new .uid companion file with the header UID."""
|
|
||||||
uid_path = Path(str(room.path) + ".uid")
|
|
||||||
content = f"{uid}\n"
|
|
||||||
|
|
||||||
if not dry_run:
|
|
||||||
uid_path.write_text(content)
|
|
||||||
return f"Creating {room.name}: .uid ← {uid} (from .tscn header)"
|
|
||||||
|
|
||||||
|
|
||||||
def fix_stale_target(source_room: RoomData, exit_node: str, old_uid: str, new_uid: str, dry_run: bool) -> str:
|
|
||||||
"""Replace a stale target UID in the source .tscn file."""
|
|
||||||
content = source_room.path.read_text()
|
|
||||||
if not dry_run:
|
|
||||||
content = content.replace(
|
|
||||||
f'target = "{old_uid}"',
|
|
||||||
f'target = "{new_uid}"',
|
|
||||||
)
|
|
||||||
source_room.path.write_text(content)
|
|
||||||
return f"Updating {source_room.name}/{exit_node}: target {old_uid} → {new_uid}"
|
|
||||||
|
|
||||||
|
|
||||||
def run_detect(rooms: dict[str, RoomData], verbose: bool) -> list[Mismatch]:
|
|
||||||
"""Detect and display all mismatches. Returns list for further processing."""
|
|
||||||
mismatches = detect_mismatches(rooms)
|
|
||||||
|
|
||||||
if not mismatches:
|
|
||||||
print("No UID mismatches found. All rooms are consistent.")
|
|
||||||
return mismatches
|
|
||||||
|
|
||||||
# Group by category
|
|
||||||
by_category: dict[str, list[Mismatch]] = {}
|
|
||||||
for m in mismatches:
|
|
||||||
by_category.setdefault(m.category, []).append(m)
|
|
||||||
|
|
||||||
total = len(mismatches)
|
|
||||||
print(f"Found {total} UID issues across {len(rooms)} rooms:\n")
|
|
||||||
|
|
||||||
if "companion_mismatch" in by_category:
|
|
||||||
msgs = by_category["companion_mismatch"]
|
|
||||||
print(f" [{len(msgs)}] Companion files out of sync with .tscn headers:")
|
|
||||||
for m in msgs[:10]:
|
|
||||||
print(f" {m.room_name}: {m.detail}")
|
|
||||||
if len(msgs) > 10:
|
|
||||||
print(f" ... and {len(msgs) - 10} more")
|
|
||||||
|
|
||||||
if "missing_companion" in by_category:
|
|
||||||
msgs = by_category["missing_companion"]
|
|
||||||
print(f" [{len(msgs)}] Missing .uid companion files:")
|
|
||||||
for m in msgs:
|
|
||||||
print(f" {m.room_name}: {m.detail}")
|
|
||||||
|
|
||||||
if "stale_target" in by_category:
|
|
||||||
msgs = by_category["stale_target"]
|
|
||||||
print(f" [{len(msgs)}] Stale target UIDs in source rooms:")
|
|
||||||
for m in (msgs[:15] if verbose else msgs[:5]):
|
|
||||||
print(f" {m.room_name}: {m.detail}")
|
|
||||||
if len(msgs) > 5 and not verbose:
|
|
||||||
print(f" ... and {len(msgs) - 5} more (use -v for full list)")
|
|
||||||
|
|
||||||
return mismatches
|
|
||||||
|
|
||||||
|
|
||||||
def run_fix(rooms: dict[str, RoomData], dry_run: bool, room_filter: Optional[str]) -> int:
|
|
||||||
"""Detect and fix all mismatches. Returns count of fixes applied."""
|
|
||||||
mismatches = detect_mismatches(rooms)
|
|
||||||
|
|
||||||
if not mismatches:
|
|
||||||
print("No UID mismatches to fix.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if room_filter:
|
|
||||||
mismatches = [m for m in mismatches if m.room_name == room_filter]
|
|
||||||
if not mismatches:
|
|
||||||
print(f"No issues found for {room_filter}.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
mode = "(DRY RUN) " if dry_run else ""
|
|
||||||
applied = 0
|
|
||||||
|
|
||||||
for m in mismatches:
|
|
||||||
if not m.fixable or not m.fix_uid:
|
|
||||||
continue
|
|
||||||
|
|
||||||
rd = rooms.get(m.room_name)
|
|
||||||
if not rd:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if m.category == "companion_mismatch" and m.source_file:
|
|
||||||
msg = fix_companion_mismatch(rd, m.fix_uid, dry_run)
|
|
||||||
print(f"{mode}✓ {msg}")
|
|
||||||
applied += 1
|
|
||||||
|
|
||||||
elif m.category == "missing_companion":
|
|
||||||
# Check if room needs a new UID generated
|
|
||||||
uid_to_use = m.fix_uid or rd.header_uid
|
|
||||||
if not uid_to_use:
|
|
||||||
uid_to_use = generate_godot_uid()
|
|
||||||
if not dry_run:
|
|
||||||
# Also update the .tscn header with the new UID
|
|
||||||
content = rd.path.read_text()
|
|
||||||
if rd.header_uid:
|
|
||||||
content = content.replace(f'uid="{rd.header_uid}"', f'uid="{uid_to_use}"')
|
|
||||||
else:
|
|
||||||
content = content.replace('[gd_scene format=3', f'[gd_scene format=3 uid="{uid_to_use}"')
|
|
||||||
rd.path.write_text(content)
|
|
||||||
msg = fix_missing_companion(rd, uid_to_use, dry_run)
|
|
||||||
print(f"{mode}✓ {msg}")
|
|
||||||
applied += 1
|
|
||||||
|
|
||||||
elif m.category == "stale_target":
|
|
||||||
# Extract old UID from detail message
|
|
||||||
detail_parts = m.detail.split(" -> ")
|
|
||||||
if len(detail_parts) >= 2:
|
|
||||||
exit_node = detail_parts[0].split()[-1]
|
|
||||||
old_uid = detail_parts[1]
|
|
||||||
new_uid = m.fix_uid
|
|
||||||
msg = fix_stale_target(rd, exit_node, old_uid or "", new_uid, dry_run)
|
|
||||||
|
|
||||||
# Handle case where target UID doesn't exist in any companion file
|
|
||||||
if "not found" in m.detail.lower():
|
|
||||||
# Extract via regex for more robust parsing
|
|
||||||
import re
|
|
||||||
uid_match = re.search(r"-> (uid://\S+)", m.detail)
|
|
||||||
if uid_match:
|
|
||||||
old_uid = uid_match.group(1)
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
|
|
||||||
exit_node_match = re.search(r"(kq4_\S+) -> ", m.detail)
|
|
||||||
exit_node = exit_node_match.group(1) if exit_node_match else "unknown"
|
|
||||||
|
|
||||||
msg = fix_stale_target(rd, exit_node, old_uid, new_uid, dry_run)
|
|
||||||
|
|
||||||
print(f"{mode}✓ {msg}")
|
|
||||||
applied += 1
|
|
||||||
|
|
||||||
return applied
|
|
||||||
|
|
||||||
|
|
||||||
def run_verify() -> bool:
|
|
||||||
"""Run check_transitions.py and build_room_graph.py to confirm fixes."""
|
|
||||||
root = Path(__file__).resolve().parent.parent
|
|
||||||
check_script = root / "scripts" / "check_transitions.py"
|
|
||||||
graph_script = root / "scripts" / "build_room_graph.py"
|
|
||||||
|
|
||||||
print("\n--- Post-fix verification ---\n")
|
|
||||||
all_clean = True
|
|
||||||
|
|
||||||
if check_script.exists():
|
|
||||||
import subprocess
|
|
||||||
print("$ python scripts/check_transitions.py")
|
|
||||||
result = subprocess.run(["python", str(check_script)], cwd=str(root), capture_output=True, text=True)
|
|
||||||
lines = [l for l in result.stdout.strip().split("\n") if "ERROR:" in l]
|
|
||||||
if lines:
|
|
||||||
all_clean = False
|
|
||||||
print(f" Still has {len(lines)} error(s):")
|
|
||||||
for l in lines[:10]:
|
|
||||||
print(f" {l.strip()}")
|
|
||||||
if len(lines) > 10:
|
|
||||||
print(f" ... and {len(lines) - 10} more")
|
|
||||||
else:
|
|
||||||
OK_count = sum(1 for l in result.stdout.split("\n") if "OK:" in l)
|
|
||||||
print(f" All {OK_count} transitions valid! ✓\n")
|
|
||||||
else:
|
|
||||||
print(f"[skip] check_transitions.py not found at {check_script}\n")
|
|
||||||
|
|
||||||
if graph_script.exists():
|
|
||||||
import subprocess
|
|
||||||
print("$ python scripts/build_room_graph.py")
|
|
||||||
result = subprocess.run(["python", str(graph_script)], cwd=str(root), capture_output=True, text=True)
|
|
||||||
output = result.stdout.strip().split("\n")
|
|
||||||
for line in output[:8]:
|
|
||||||
print(f" {line}")
|
|
||||||
|
|
||||||
# Check if all rooms are connected or note component issues
|
|
||||||
for l in output:
|
|
||||||
if "component" in l.lower():
|
|
||||||
if "all rooms connected" not in l.lower():
|
|
||||||
comp_count = re.search(r"(\d+)", l)
|
|
||||||
if comp_count and int(comp_count.group(1)) > 1:
|
|
||||||
print(f" Note: {l.strip()} (some rooms disconnected by missing transitions)")
|
|
||||||
else:
|
|
||||||
print(f"[skip] build_room_graph.py not found at {graph_script}\n")
|
|
||||||
|
|
||||||
return all_clean
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
import argparse
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Detect and repair UID mismatches in KQ4 room scenes",
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
epilog=__doc__,
|
|
||||||
)
|
|
||||||
parser.add_argument("--detect", action="store_true", help="Only detect mismatches")
|
|
||||||
parser.add_argument("--fix", action="store_true", help="Apply fixes for found mismatches")
|
|
||||||
parser.add_argument("--dry-run", action="store_true", help="Show what would change without writing")
|
|
||||||
parser.add_argument("--room", help="Limit to a specific room name (e.g., kq4_018_cemetery)")
|
|
||||||
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
root = Path(__file__).resolve().parent.parent
|
|
||||||
scenes_dir = root / "scenes"
|
|
||||||
|
|
||||||
if not scenes_dir.exists():
|
|
||||||
print(f"ERROR: Scenes directory not found at {scenes_dir}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
rooms = parse_all_rooms(scenes_dir)
|
|
||||||
|
|
||||||
if args.detect and not args.fix:
|
|
||||||
mismatches = run_detect(rooms, args.verbose)
|
|
||||||
if mismatches:
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
if args.fix:
|
|
||||||
applied = run_fix(rooms, args.dry_run, args.room)
|
|
||||||
print(f"\nApplied {applied} fix(es)" + (" (dry run — no changes made)" if args.dry_run else ""))
|
|
||||||
if not args.dry_run and applied > 0:
|
|
||||||
try:
|
|
||||||
clean = run_verify()
|
|
||||||
if not clean:
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[verification warning] {e}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Reference in New Issue
Block a user