diff --git a/.claude/skills/pr-review/SKILL.md b/.claude/skills/pr-review/SKILL.md index 28276aeb0..df9fd1d26 100644 --- a/.claude/skills/pr-review/SKILL.md +++ b/.claude/skills/pr-review/SKILL.md @@ -74,35 +74,35 @@ If the diff is empty, report "No changes to review" and stop. Three-dot diff with pathspec exclusions is unreliable. Instead, either: 1. Use `git diff main...` (full diff) and filter in the prompt, or -2. Read source files directly from the branch worktree (see below). +2. Read source files directly from the team directory (see below). For large diffs (>1000 lines of source), provide **source files** rather than raw diff to reviewers — cleaner context, better reviews. -**IMPORTANT — use worktree paths for ALL agents.** This project uses git -worktrees. Each team branch is checked out at: +**IMPORTANT — use team directory paths for ALL agents.** Each team branch +is checked out in its own directory at: ``` /var/mnt/data/projects/settled-reach// ``` -For example, the `copy` branch lives at: +For example, the `copy` team directory is at: ``` /var/mnt/data/projects/settled-reach/copy/content/dialogue/... ``` **All reviewer agents** (regardless of Bash access) should read source files -from the worktree path using the Read tool. This is more reliable than +from the team directory using the Read tool. This is more reliable than `git show origin/:` because: - All agents have Read access (no Bash dependency) - Files are always the actual branch checkout (no stale cache) - No risk of accidentally reading from main's working directory When constructing reviewer prompts, tell agents to read files from the -worktree path. Example instruction for agents: +team directory. Example instruction for agents: ``` -Read the changed files from the branch worktree. The branch is checked +Read the changed files from the team directory. The branch is checked out at: /var/mnt/data/projects/settled-reach// For example, to read `content/dialogue/the-terminal/kael-davan.yaml`, @@ -110,8 +110,8 @@ use: /var/mnt/data/projects/settled-reach//content/dialogue/the-terminal ``` Also tell agents to read relevant `decisions/*.md` files from the same -worktree (they're identical to main, but using the worktree path keeps -agents grounded in the correct directory). +directory (they're identical to main, but using the team directory path +keeps agents grounded in the correct location). ### 4. Spawn reviewers in parallel diff --git a/.claude/skills/pr-review/references/reviewer-profiles.md b/.claude/skills/pr-review/references/reviewer-profiles.md index 8f9773fce..4ece25389 100644 --- a/.claude/skills/pr-review/references/reviewer-profiles.md +++ b/.claude/skills/pr-review/references/reviewer-profiles.md @@ -2,20 +2,24 @@ Use `model: sonnet` for all reviewers — sufficient for review, saves cost. -**All agents read from worktree paths.** Each branch is checked out at: +**All reviewer agents read from team directories.** Each team branch is +checked out in its own directory at: `/var/mnt/data/projects/settled-reach//` -Tell every reviewer agent to read source files from the worktree using the -Read tool. Include the worktree base path and a list of changed files in -every prompt. Do NOT rely on `git show` or paste file contents — agents -can read directly from the worktree. +Tell every reviewer agent to read source files from the team directory +using the Read tool. Include the directory path and a list of changed +files in every prompt. Do NOT rely on `git show` or paste file contents — +agents can read directly from the directory. + +Note: cross-directory reading is only permitted for review agents spawned +from the `main` team. Team agents must stay within their own directory. ## Code reviews (`server`, `client`, `ci`) **Hoshe (Code Quality)** - `subagent_type`: `hoshe`, `model`: `sonnet` -- Prompt: Provide the worktree path, list of changed files, and commit log. - Tell Hoshe to read source files from the worktree, then review for: +- Prompt: Provide the team directory path, list of changed files, and commit log. + Tell Hoshe to read source files from the team directory, then review for: - Correctness and bug risks - Error handling gaps - Test coverage (are new features tested?) @@ -25,8 +29,8 @@ can read directly from the worktree. **Tyre (Architecture)** - `subagent_type`: `tyre`, `model`: `sonnet` -- Prompt: Provide the worktree path, list of changed files, and commit log. - Tell Tyre to read the relevant `decisions/*.md` files from the worktree +- Prompt: Provide the team directory path, list of changed files, and commit log. + Tell Tyre to read the relevant `decisions/*.md` files from the team directory first, then review for: - Architectural consistency with project decisions - API/interface design quality @@ -38,8 +42,8 @@ can read directly from the worktree. **Hoshe (QA)** - `subagent_type`: `hoshe`, `model`: `sonnet` -- Prompt: Provide the worktree path, list of changed files, and commit log. - Tell Hoshe to read the changed files from the worktree, then review for: +- Prompt: Provide the team directory path, list of changed files, and commit log. + Tell Hoshe to read the changed files from the team directory, then review for: - Formatting consistency (markdown, file naming, frontmatter) - Broken references or links - Spelling and grammar @@ -48,9 +52,9 @@ can read directly from the worktree. **Paula (Narrative Depth)** - `subagent_type`: `paula`, `model`: `sonnet` -- Prompt: Provide the worktree path, list of changed files, commit log, and +- Prompt: Provide the team directory path, list of changed files, commit log, and list of relevant `decisions/*.md` files to read. Tell Paula to read all - files from the worktree using the Read tool, then review for: + files from the team directory using the Read tool, then review for: - Narrative quality and character voice consistency - Whether dialogue and monologue feel authentic to the characters - Consequences and stakes — do choices carry weight? @@ -59,9 +63,9 @@ can read directly from the worktree. **Miri (World Consistency)** - `subagent_type`: `miri`, `model`: `sonnet` -- Prompt: Provide the worktree path, list of changed files, commit log, and +- Prompt: Provide the team directory path, list of changed files, commit log, and list of relevant `decisions/*.md` files to read. Tell Miri to read all - files from the worktree using the Read tool, then review for: + files from the team directory using the Read tool, then review for: - Lore accuracy — do facts match established setting? - Internal consistency across files - IP originality — nothing should read as a copy from another franchise @@ -72,8 +76,8 @@ can read directly from the worktree. **Hoshe (QA)** - `subagent_type`: `hoshe`, `model`: `sonnet` -- Prompt: Provide the worktree path, list of changed files, and commit log. - Tell Hoshe to read the changed files from the worktree, then review for: +- Prompt: Provide the team directory path, list of changed files, and commit log. + Tell Hoshe to read the changed files from the team directory, then review for: - File format and naming conventions - Asset organization and directory structure - Missing or broken references in scene/resource files @@ -81,7 +85,7 @@ can read directly from the worktree. **Araminta (Art Direction)** - `subagent_type`: `araminta`, `model`: `sonnet` -- Prompt: Provide the worktree path, list of changed files, and commit log. +- Prompt: Provide the team directory path, list of changed files, and commit log. Tell Araminta to read the style guide and relevant design docs from the worktree first, then review for: - Visual consistency with the established style guide @@ -94,8 +98,8 @@ can read directly from the worktree. **Hoshe (QA)** - `subagent_type`: `hoshe`, `model`: `sonnet` -- Prompt: Provide the worktree path, list of changed files, and commit log. - Tell Hoshe to read the changed files from the worktree, then review for: +- Prompt: Provide the team directory path, list of changed files, and commit log. + Tell Hoshe to read the changed files from the team directory, then review for: - File format and naming conventions - Audio asset organization and directory structure - Missing or broken references @@ -103,8 +107,8 @@ can read directly from the worktree. **Ozzie (Player Experience)** - `subagent_type`: `ozzie`, `model`: `sonnet` -- Prompt: Provide the worktree path, list of changed files, and commit log. - Tell Ozzie to read all files from the worktree using the Read tool, then +- Prompt: Provide the team directory path, list of changed files, and commit log. + Tell Ozzie to read all files from the team directory using the Read tool, then review for: - Emotional impact — does the audio enhance the moment? - Atmosphere and tone — does it feel like the Commonwealth? diff --git a/.claude/skills/sprint-plan/SKILL.md b/.claude/skills/sprint-plan/SKILL.md index 1525992b1..767e76dc3 100644 --- a/.claude/skills/sprint-plan/SKILL.md +++ b/.claude/skills/sprint-plan/SKILL.md @@ -154,13 +154,12 @@ Create `docs/sprints/sprint-N/` and write one file per team. Read the template at `references/briefing-template.md` in this skill directory for the exact file structure. -**IMPORTANT — worktree-relative paths:** This project uses git worktrees. -Each team branch is checked out in its own worktree, and each worktree -contains the full repo (`server/`, `client/`, `docs/`, etc.). All file -paths in briefings must be relative to the worktree/git root. Example: +**IMPORTANT — relative paths only:** Each team works in its own directory +containing the full repo (`server/`, `client/`, `docs/`, etc.). All file +paths in briefings must be relative to the working directory. Example: `server/src/bridge/types.rs`, not `/absolute/path/to/server/src/...` or -paths that navigate outside the git root (`../sibling-worktree/...`). -Agents must stay within the git root they are running in. +paths that navigate outside (`../sibling-dir/...`). +Agents must stay within their team's working directory. Key requirements per file: - **server.md**: Carry-overs, new tickets, dependency chain, key decisions, notes diff --git a/.claude/skills/sprint-start/SKILL.md b/.claude/skills/sprint-start/SKILL.md index 6a800083d..6cc7aeb7f 100644 --- a/.claude/skills/sprint-start/SKILL.md +++ b/.claude/skills/sprint-start/SKILL.md @@ -290,6 +290,11 @@ Task( RULES (NON-NEGOTIABLE): + 0. TEAM BOUNDARY: Your team is `{team}` ($WORKTREE_TEAM). Stay + within the current working directory. Do NOT navigate to + parent or sibling directories. Do NOT follow .git pointers + to other directories. + 1. GIT: Do NOT run any git commands (commit, push, pull, merge, checkout, branch, stash, tag, etc.). All git operations are handled by the team lead. No exceptions. diff --git a/CHANGELOG.md b/CHANGELOG.md index c352e091c..1a584c7f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] +## [v0.1.23] — 2026-03-04 + ### Added - TileKind enum (Floor/Wall/Void/Restricted) on WalkabilityMap with per-tile type data alongside walkability (#576, D-094) - Location YAML tile format — hand-author tiles as string arrays (F/W/V/R characters), loaded into WalkabilityMap on production startup (#577) @@ -17,6 +19,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). - Debug console server — 10 DebugCommandKind variants (AdvanceTicks, SkipToContamination, TeleportToPosition, InspectNpc, ListTriangles, etc.) with DebugResponsePayload on ObserverSnapshot (#580) - Debug console client — tilde-toggle UI panel with command input, output log, settings toggle, and full DebugCommandKind dispatch via protocol v18 (#581) - Entity-bound dialogue speaker colors — NPC colors assigned by entity ID (not screen position) with per-conversation lifecycle and round-robin palette (#573) +- Sova Transit District tile maps — 5 locations authored: The Terminal (44×28), The Last Shift (34×22), Maintenance Corridors (58×6), Gate Ground (40×34), Gate Gallery (32×10) (#582, #583) ### Fixed - LOS boundary walls — 1-tile wall margin beyond vision cone included in visible_tiles as BoundaryWall sector, walls at fog edge now render instead of bleeding into fog (#584) @@ -191,6 +194,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). - run-ipc-benchmark dead --iterations flag removed (Rust compile-time constant governs rounds) ### Changed +- Team boundary framing — replaced worktree-centric language with `$WORKTREE_TEAM` env var identity across CLAUDE.md and skills (sprint-start, sprint-plan, pr-review) to prevent agents from following `.git` pointers across boundaries - CLAUDE.md compacted from 188 to 67 lines — CLI references, endpoints, and patterns moved to `.claude/rules/` - `/sprint-status` delegates to haiku subagent — keeps sweep JSON, template read, and PR list out of main context window - `sprint sweep` JSON trimmed — removed unused fields (`ok`, `sprint.status`, `priority`, `ticket_id`), shortened issue detail strings diff --git a/CLAUDE.md b/CLAUDE.md index 129576dce..0d161e484 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,14 +27,15 @@ See [docs/DEVOPS.md](docs/DEVOPS.md) for build, test, lint, and CI procedures. A ## Agent Instructions -### Worktree boundaries +### Team boundaries -This project uses **git worktrees** in a shared parent directory (`settled-reach/`). Each team branch (`server`, `client`, `copy`, `audio`, `visual`, `ci`) has its own worktree. The worktree root IS the git root. +**Your team identity is `$WORKTREE_TEAM`.** All work must stay within the current working directory. -- **All work must remain within the git root** unless explicitly instructed otherwise. -- All file paths are relative to the worktree root (e.g. `server/src/bridge/types.rs`). -- Do not navigate to or access sibling worktrees (`../client/`, `../copy/`, etc.) unless explicitly instructed. -- **Exception — stale git lock files:** Worktree index locks live in the shared `.git` directory (e.g. `main/.git/worktrees/copy/index.lock`). If a `git` command fails with `index.lock: File exists`, you may remove the lock file for **your own worktree only**. Never touch lock files belonging to other worktrees. +- All file paths are relative to the current working directory (e.g. `server/src/bridge/types.rs`). +- **Do NOT navigate to parent or sibling directories** (`../`, `../client/`, etc.) unless explicitly instructed. Do NOT use absolute paths to reach other team directories. +- **Do NOT write auto-memory files for other teams.** If `$WORKTREE_TEAM` is `server`, do not write to memory paths containing `client`, `main`, etc. +- For context: each team has its own directory via git worktrees, sharing a parent directory (`settled-reach/`). The `.git` file points to a shared git directory — do not follow it to determine your working root. +- **Exception — stale git lock files:** If a `git` command fails with `index.lock: File exists`, you may remove the lock file for **your own team only** (e.g. `main/.git/worktrees/$WORKTREE_TEAM/index.lock`). Never touch lock files belonging to other teams. - **Never chain git commands** in a single Bash call (e.g. `git add ... && git commit ...`). The shared `.git` directory means concurrent index access from the same terminal creates `index.lock` collisions. Always run `git add` and `git commit` as **separate sequential Bash calls**. ### Database @@ -61,6 +62,13 @@ The ticketing database (`settledreach.db`) lives in the **parent directory** sha | Doc search | `tooling/db/qdrant-search "query"` | `/docs-search` skill | | Doc index | `tooling/db/qdrant-index path/to/file.md` | `/docs-search` skill | +### Testing preferences + +- **Prefer live Gauntlet testing over mocks.** For visual tests and rendering verification, use the full client/server pipeline (`--test-mode` + `SR_LIVE=1`) instead of TestHarness mocks. The Gauntlet test world produces production-identical data. Mocks can mask rendering bugs by taking different code paths. +- **Gauntlet rooms are immutable.** Never modify existing rooms — new systems get new rooms. This ensures StableId determinism and fixture stability. +- Three test tiers: (1) Live server — highest fidelity, (2) MessagePack replay via `Protocol.decode_snapshot()` — for unreachable rooms, (3) TestHarness mock — for UI-only tests where fog data doesn't matter. +- `make fixtures-gauntlet` regenerates real server snapshot fixtures from the Gauntlet world. + ### File conventions - Decisions: domain files in `decisions/` (see `decisions/README.md` for index) - Decision IDs: `D-NNN` (confirmed), `Q-NNN` (open questions), `R-NNN` (rejected) diff --git a/Makefile b/Makefile index e8080ffe9..99471422b 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null) db-backup db-install validate-content content-ron check-fact-ids setup-hooks \ pre-pr pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures \ pre-pr-server pre-pr-client pre-pr-content \ - fixtures-client golden-diff golden-update \ + fixtures-client fixtures-gauntlet golden-diff golden-update \ checklist-validate checklist-generate \ perf-baseline debug-schedule \ test-ipc-fixtures test-ipc-protocol test-ipc-integration test-ipc-benchmark \ @@ -110,6 +110,9 @@ build-server: build-client: @test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; } + @# First import may error on theme/font loading before the import scan completes. + @# Run twice: first pass generates imports silently, second pass validates clean. + @$(GODOT) --headless --path client --import --quit 2>/dev/null || true $(GODOT) --headless --path client --import --quit # --- Run --- @@ -144,6 +147,9 @@ test-server: fixtures: cd server && cargo test --test gen_fixtures -- --ignored +fixtures-gauntlet: + cd server && cargo test --test gen_gauntlet_fixtures -- --ignored + fixtures-client: @test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; } @echo "Generating GDScript fixtures for Rust decoder..." @@ -347,5 +353,5 @@ content-ron: clean: cd server && cargo clean || true rm -rf .cache/* - rm -rf client/.godot/* client/reports + rm -rf client/reports @echo "Clean complete." diff --git a/client/scripts/protocol/test_harness.gd b/client/scripts/protocol/test_harness.gd index d75b6f8cf..f40055dc0 100644 --- a/client/scripts/protocol/test_harness.gd +++ b/client/scripts/protocol/test_harness.gd @@ -179,9 +179,7 @@ func snapshot() -> Dictionary: "player_stance": "Walk", "player_inventory": [], "entities": entities, - "tiles": _tiles(), "visible_tiles": _visible_tiles(), - "visible_positions": _visible_positions(), "nearby_interactions": nearby, "current_monologue": monologue, "current_dialogue": dialogue, @@ -229,7 +227,7 @@ func _visible_tiles() -> Array: var vtiles: Array = [] var px := player_pos.x var py := player_pos.y - var radius := 4 + var radius := 5 var room_x := 7 var room_y := 7 var room_w := 8 @@ -241,27 +239,22 @@ func _visible_tiles() -> Array: if dist <= radius: if x >= room_x and x < room_x + room_w and y >= room_y and y < room_y + room_h: var sector: String = "Forward" if y <= py else "Peripheral" - vtiles.append({"x": x, "y": y, "z": 0, "visibility": sector}) + vtiles.append({"x": x, "y": y, "z": 0, "visibility": sector, "type": _get_tile_type(x, y)}) return vtiles -func _visible_positions() -> Array: - var positions: Array = [] - var px := player_pos.x - var py := player_pos.y - var radius := 4 +func _get_tile_type(x: int, y: int) -> String: var room_x := 7 var room_y := 7 var room_w := 8 var room_h := 8 - - for x in range(px - radius, px + radius + 1): - for y in range(py - radius, py + radius + 1): - var dist := absf(x - px) + absf(y - py) - if dist <= radius: - if x >= room_x and x < room_x + room_w and y >= room_y and y < room_y + room_h: - positions.append({"x": x, "y": y}) - return positions + var is_edge := (x == room_x or x == room_x + room_w - 1 + or y == room_y or y == room_y + room_h - 1) + if is_edge: + if y == room_y + room_h - 1 and x == room_x + room_w / 2: + return "door" + return "wall" + return "floor" # -- Spatial helpers ----------------------------------------------------------- diff --git a/client/scripts/rendering/fog_shader.gd b/client/scripts/rendering/fog_shader.gd index 24b423fe0..3349f5d9c 100644 --- a/client/scripts/rendering/fog_shader.gd +++ b/client/scripts/rendering/fog_shader.gd @@ -32,6 +32,7 @@ func _ready() -> void: # Create seamless noise texture for fog animation var noise := FastNoiseLite.new() + noise.seed = 42 noise.noise_type = FastNoiseLite.TYPE_PERLIN noise.frequency = 0.03 var noise_tex := NoiseTexture2D.new() diff --git a/client/shaders/fog.gdshader b/client/shaders/fog.gdshader index 1505d3268..ac1c865b5 100644 --- a/client/shaders/fog.gdshader +++ b/client/shaders/fog.gdshader @@ -1,17 +1,21 @@ shader_type canvas_item; // D-059/D-015: 3-state fog shader (simplified from 5-layer by #569). -// State 1: Clear (forward cone) — transparent, soft Gaussian gradient edge (3-4 tile radius) +// State 1: Clear (forward cone) — transparent, soft gradient edge (6-8 tile radius) // State 2: Explored (out of cone) — light fog overlay, alpha 0.25-0.35, zone temperature tint, // 8-10s Perlin breathe. Art and information preserved, just "not fresh" (D-015). // State 3: Unexplored — solid near-black #12141a // D-033: Entity colors are NOT affected — they render above the fog overlay (z-layer 5). // D-046: Zone temperature tint from zone_tint_tex — warm=bar, cool=hub, neutral=corridor. // D-077: zone_tint_tex populated per-tile from server zone_id via fog_state.gd. +// +// Texture pipeline (fog_state.gd): binary 0/255 at 1× tile resolution → CPU Gaussian blur +// (sigma 2.0, 6-8 tile gradient) → Image.resize 4× bilinear upscale → RGBA8 convert. +// Textures arrive here with smooth sub-tile gradients — no GPU-side blur needed. -uniform sampler2D visibility_tex : filter_linear, repeat_disable; -uniform sampler2D exploration_tex : filter_linear, repeat_disable; -uniform sampler2D zone_tint_tex : filter_nearest, repeat_disable; // nearest: zones have hard boundaries (D-073) +uniform sampler2D visibility_tex : filter_linear, repeat_disable; // RGBA8, 4× tile resolution +uniform sampler2D exploration_tex : filter_linear, repeat_disable; // RGBA8, 4× tile resolution +uniform sampler2D zone_tint_tex : filter_nearest, repeat_disable; // D-073: hard zone boundaries uniform sampler2D noise_tex : filter_linear, repeat_enable; uniform vec2 rect_pos; // World-space position of the ColorRect (pixels) uniform vec2 rect_sz; // World-space size of the ColorRect (pixels) @@ -23,40 +27,6 @@ uniform bool debug_exploration = false; // When true, render raw exploration tex const vec3 UNEXPLORED_COLOR = vec3(0.071, 0.078, 0.102); // #12141a -// Soft gradient via 7x7 Gaussian blur on visibility (sigma 2.0). -// Spreads the cone boundary into a 3-4 tile radius gradient — no hard tile-stepped edges. -float sample_visibility(vec2 uv) { - vec2 t = 2.0 / map_size; - float sum = 0.0; - float weight = 0.0; - for (float dy = -3.0; dy <= 3.0; dy += 1.0) { - for (float dx = -3.0; dx <= 3.0; dx += 1.0) { - float w = exp(-(dx * dx + dy * dy) / 8.0); - vec2 sample_uv = clamp(uv + vec2(dx, dy) * t, vec2(0.0), vec2(1.0)); - sum += texture(visibility_tex, sample_uv).r * w; - weight += w; - } - } - return sum / weight; -} - -// Soft gradient on exploration boundary (5x5, sigma 1.5). -// Prevents hard tile-stepped staircase at explored/unexplored edge. -float sample_exploration(vec2 uv) { - vec2 t = 1.0 / map_size; - float sum = 0.0; - float weight = 0.0; - for (float dy = -2.0; dy <= 2.0; dy += 1.0) { - for (float dx = -2.0; dx <= 2.0; dx += 1.0) { - float w = exp(-(dx * dx + dy * dy) / 4.5); - vec2 sample_uv = clamp(uv + vec2(dx, dy) * t, vec2(0.0), vec2(1.0)); - sum += texture(exploration_tex, sample_uv).r * w; - weight += w; - } - } - return sum / weight; -} - void fragment() { vec2 world_px = rect_pos + UV * rect_sz; vec2 tile = world_px / tile_size; @@ -67,28 +37,15 @@ void fragment() { if (tex_uv.x < 0.0 || tex_uv.x > 1.0 || tex_uv.y < 0.0 || tex_uv.y > 1.0) { COLOR = vec4(UNEXPLORED_COLOR, 1.0); - // Debug mode: render raw exploration texture (bypass fog rendering). - // Green = EXP_VISIBLE (255), blue = EXP_EXPLORED (128), red = EXP_UNEXPLORED (0). + // Debug mode: render RAW exploration texture with filter_linear. + // If hardware bilinear works, should show smooth sub-tile gradients. } else if (debug_exploration) { - float explored_dbg = texture(exploration_tex, tex_uv).r; - if (explored_dbg > 0.9) { - COLOR = vec4(0.0, explored_dbg, 0.0, 0.8); // Green: currently visible - } else if (explored_dbg > 0.1) { - COLOR = vec4(0.0, 0.0, explored_dbg * 2.0, 0.8); // Blue: explored - } else { - COLOR = vec4(0.5, 0.0, 0.0, 0.8); // Red: unexplored - } + float explored_raw = texture(exploration_tex, tex_uv).r; + COLOR = vec4(vec3(explored_raw), 0.9); } else { - float vis_raw = texture(visibility_tex, tex_uv).r; - float vis = sample_visibility(tex_uv); - float explored_raw = texture(exploration_tex, tex_uv).r; - float explored = sample_exploration(tex_uv); - - // Prevent gradient bleed into never-explored tiles (use raw, unblurred value) - if (explored_raw < 0.01 && vis_raw < 0.01) { - vis = 0.0; - } + float vis = texture(visibility_tex, tex_uv).r; + float explored = texture(exploration_tex, tex_uv).r; if (explored < 0.01 && vis < 0.01) { // Unexplored: solid near-black — information zero @@ -106,8 +63,13 @@ void fragment() { float alpha = mix(fog_alpha, 0.0, clarity); vec3 color = mix(zone_tint, vec3(0.0), clarity); - // Soft edge between explored and unexplored (blurred to avoid staircase) - float exp_fade = smoothstep(0.0, 0.3, explored); + // Soft edge between explored and unexplored. + // The exploration texture is binary (explored-or-not) blurred over ~12 tiles. + // At the physical tile boundary, explored ≈ 0.5. Squaring the fade keeps + // fog nearly opaque there (97%), hiding tile-aligned content edges. + // Content appears gradually 4-6 tiles inside the explored area. + float exp_fade = smoothstep(0.3, 1.0, explored); + exp_fade *= exp_fade; // Steeper curve: fog stays opaque near content edge alpha = mix(1.0, alpha, exp_fade); color = mix(UNEXPLORED_COLOR, color, exp_fade); diff --git a/client/tests/test_entanglement_sprint22.gd.uid b/client/tests/test_entanglement_sprint22.gd.uid new file mode 100644 index 000000000..bad8ed8cd --- /dev/null +++ b/client/tests/test_entanglement_sprint22.gd.uid @@ -0,0 +1 @@ +uid://c1dnlbnxtgqqo diff --git a/client/tests/test_fog_sprint22.gd.uid b/client/tests/test_fog_sprint22.gd.uid new file mode 100644 index 000000000..90787ff09 --- /dev/null +++ b/client/tests/test_fog_sprint22.gd.uid @@ -0,0 +1 @@ +uid://bxhgo1e4rvfmi diff --git a/client/tests/visual_capture.gd b/client/tests/visual_capture.gd index 8e9f4550e..60c4bf6aa 100644 --- a/client/tests/visual_capture.gd +++ b/client/tests/visual_capture.gd @@ -24,6 +24,7 @@ var _flow: String = "" var _interval: float = 3.0 var _list_mode: bool = false var _config: Dictionary = {} +var _is_live: bool = false func _init(): @@ -73,6 +74,19 @@ func _run(): return var main_node = main_scene.instantiate() + + # Live mode: configure server port BEFORE main.gd._ready() calls connect_to_sim() + _is_live = OS.get_environment("SR_LIVE") == "1" + if _is_live: + var sim_bridge := root.get_node("/root/SimBridge") + var port_env := OS.get_environment("SR_PORT") + if port_env.is_empty(): + push_error("visual_capture: SR_LIVE=1 but SR_PORT not set") + quit(1) + return + sim_bridge.server_port = int(port_env) + print("visual_capture: live mode — server port %d" % sim_bridge.server_port) + root.add_child(main_node) # Wait for NoiseTexture2D async generation @@ -92,6 +106,36 @@ func _run(): for i in range(settle_count): await process_frame + # Live mode: wait for server connection and first snapshot + if _is_live: + var sim_bridge := root.get_node("/root/SimBridge") + var game_state := root.get_node("/root/GameState") + print("visual_capture: waiting for server connection...") + var max_frames := 300 # 5 seconds at 60fps + var waited := 0 + while sim_bridge.state != sim_bridge.ConnectionState.CONNECTED: + if sim_bridge.state == sim_bridge.ConnectionState.ERROR: + push_error("visual_capture: server connection failed") + quit(1) + return + await process_frame + waited += 1 + if waited >= max_frames: + push_error("visual_capture: connection timeout after %d frames" % waited) + quit(1) + return + print("visual_capture: connected after %d frames" % waited) + # Wait for first snapshot from server + waited = 0 + while game_state.current_tick == 0: + await process_frame + waited += 1 + if waited >= max_frames: + push_error("visual_capture: no snapshot after %d frames" % waited) + quit(1) + return + print("visual_capture: first snapshot tick=%d (%d frames)" % [game_state.current_tick, waited]) + if not _scenario.is_empty(): await _run_scenario(main_node) elif not _flow.is_empty(): @@ -123,6 +167,40 @@ func _run_scenario(_main_node: Node) -> void: # Post-tick setup (e.g. zone tint patching) _scenarios.post_setup(_scenario, root) + # Replay snapshot: inject a real server snapshot through the FULL client pipeline. + # Loads MessagePack bytes (exact wire format from server), decodes via Protocol.gd, + # then applies through GameState → FogState → shader — same path as live game. + var replay_path: String = scenario_cfg.get("replay_snapshot", "") + if not replay_path.is_empty(): + var project_root := ProjectSettings.globalize_path("res://") + var repo_root := project_root.rstrip("/").get_base_dir() + var abs_path := repo_root.path_join(replay_path) + var rf := FileAccess.open(abs_path, FileAccess.READ) + if rf == null: + push_error("visual_capture: cannot open replay snapshot %s" % abs_path) + quit(1) + return + var replay_bytes := rf.get_buffer(rf.get_length()) + rf.close() + # Decode through Protocol.decode_snapshot() — same as live IPC receive path. + # This exercises: msgpack decode → entity decode → tile_kind→type mapping → etc. + var replay_data: Variant = Protocol.decode_snapshot(replay_bytes) + if replay_data == null or not replay_data is Dictionary: + push_error("visual_capture: Protocol.decode_snapshot failed for %s" % abs_path) + quit(1) + return + print("visual_capture: replaying %s (%d bytes, tick=%s, %d tiles)" % [ + replay_path, replay_bytes.size(), + str(replay_data.get("tick", "?")), + replay_data.get("visible_tiles", []).size()]) + var game_state := root.get_node("/root/GameState") + var fog_state := root.get_node("/root/FogState") + game_state.apply_snapshot(replay_data) + fog_state.update_from_state() + # Extra frames for fog uniform propagation + for i in range(4): + await process_frame + # Extra frames for state propagation + viewport texture lag await process_frame await process_frame diff --git a/client/tests/visual_capture.gd.uid b/client/tests/visual_capture.gd.uid new file mode 100644 index 000000000..1719ae961 --- /dev/null +++ b/client/tests/visual_capture.gd.uid @@ -0,0 +1 @@ +uid://pd1qpgxiodig diff --git a/client/tests/visual_scenarios.gd b/client/tests/visual_scenarios.gd index 0732fe0f4..3ba3d001b 100644 --- a/client/tests/visual_scenarios.gd +++ b/client/tests/visual_scenarios.gd @@ -83,6 +83,17 @@ func apply_setup(scenario_name: String, tree_root: Node) -> bool: sim_bridge.harness.player_pos = Vector2i(11, 9) sim_bridge.harness.process_input("Interact") + "fog_live_replay", "fog_theater_replay", "fog_boundary_replay": + # Replay real server snapshots via MessagePack → Protocol.decode_snapshot(). + # Setup handled by visual_capture.gd (reads replay_snapshot from config). + pass + + "fog_live_hub": + # Live server connection — Hub spawn position. + # No setup needed: server starts in --test-mode with Gauntlet, + # player spawns at Hub (50,58). Captures real fog pipeline output. + pass + _: push_warning("VisualScenarios: unknown scenario '%s'" % scenario_name) return false diff --git a/client/tests/visual_scenarios.gd.uid b/client/tests/visual_scenarios.gd.uid new file mode 100644 index 000000000..ec6c64605 --- /dev/null +++ b/client/tests/visual_scenarios.gd.uid @@ -0,0 +1 @@ +uid://dna10a0ln5pd0 diff --git a/client/ui/bug_report_dialog.gd b/client/ui/bug_report_dialog.gd index 1936ee3ed..bffaf1913 100644 --- a/client/ui/bug_report_dialog.gd +++ b/client/ui/bug_report_dialog.gd @@ -11,6 +11,7 @@ extends Control ## - inputs.jsonl — last 60 ticks of PlayerInput (replay-compatible JSONL) ## - snapshots.jsonl — last 60 ticks of ObserverSnapshot (one JSON per line) ## - seed.txt — RNG seed for deterministic replay +## - screenshot.png — viewport capture taken before dialog opened ## ## Ring buffer: pre-allocated RING_SIZE arrays at startup. record_tick() is the ## public API for main.gd. _push_tick_inputs() / _push_tick_snapshot() are the @@ -36,6 +37,7 @@ const RING_SIZE := 60 var _line_edit: LineEdit = null var _active: bool = false +var _captured_screenshot: Image = null # #507: Pre-allocated ring buffers (no per-tick allocation after _ready). # Input ring: replay-format PlayerInput arrays, one per tick. @@ -201,6 +203,8 @@ func _get_filled_snapshot_count() -> int: func start_capture() -> void: if _active: return + # Capture screenshot BEFORE showing the dialog overlay + _captured_screenshot = get_viewport().get_texture().get_image() _active = true visible = true @@ -247,6 +251,8 @@ func _close() -> void: _line_edit.queue_free() _line_edit = null + _captured_screenshot = null + # Unpause the simulation SimBridge.send_input({ "action": InputMapper.Action.UNPAUSE, @@ -345,7 +351,16 @@ func _save_report(description: String) -> void: else: push_error("BugReport: failed to write %s" % seed_path) - print("BugReport: saved %d/6 files to %s (ring: %d ticks)" % [ + # 7. screenshot.png — viewport capture taken before dialog opened + if _captured_screenshot: + var screenshot_path := base_path + "/screenshot.png" + var img_err := _captured_screenshot.save_png(screenshot_path) + if img_err == OK: + files_saved += 1 + else: + push_error("BugReport: failed to write %s (error %d)" % [screenshot_path, img_err]) + + print("BugReport: saved %d/7 files to %s (ring: %d ticks)" % [ files_saved, base_path, _input_count]) diff --git a/client/ui/time_display.gd b/client/ui/time_display.gd index bf1fe44ee..71898f6ee 100644 --- a/client/ui/time_display.gd +++ b/client/ui/time_display.gd @@ -70,7 +70,7 @@ func _cache_geometry() -> void: _day_size = font.get_string_size(_day_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_META) _meta_h = font.get_string_size("A", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_META).y var meta_w := _phase_size.x + _day_size.x - var content_w := max(_time_size.x, meta_w) + var content_w := maxf(_time_size.x, meta_w) _box_w = content_w + PADDING.x * 2 _box_h = PADDING.y * 2 + _time_size.y + 3 + _meta_h diff --git a/content/campaigns/main/systems/krenn/stations/sova/districts/transit/district.yaml b/content/campaigns/main/systems/krenn/stations/sova/districts/transit/district.yaml index 4aefb6f45..878744af3 100644 --- a/content/campaigns/main/systems/krenn/stations/sova/districts/transit/district.yaml +++ b/content/campaigns/main/systems/krenn/stations/sova/districts/transit/district.yaml @@ -4,10 +4,13 @@ display_name: "Sova Transit District" description: > A 40-year-old prefab-modular-retrofitted freight logistics hub on Station Sova. - Three social sites: The Terminal (logistics hub), The Last Shift (bar), - and maintenance corridors. + Five locations: The Terminal (logistics hub), The Last Shift (bar), + maintenance corridors, gate corridor (ground level), and the observation + gallery (Commission-only, z=2 above the gate concourse). locations: - "the-terminal" - "the-last-shift" - "maintenance-corridors" + - "gate-ground" + - "gate-gallery" npc_count: 17 diff --git a/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/gate-gallery.yaml b/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/gate-gallery.yaml new file mode 100644 index 000000000..7c284bf5a --- /dev/null +++ b/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/gate-gallery.yaml @@ -0,0 +1,48 @@ +# Location: Gate Corridor — Observation Gallery +# Source: D-093 gate cluster zone spec +# Zone palette: surface #b8bec4 / fog tint #0a1222 +# z=2 (above gate concourse) + +canonical_id: "krenn.sova.transit.location.gate-gallery" +display_name: "Observation Gallery" +description: > + Commission-only observation gallery overlooking the gate concourse from + z=2. The gallery rail is a transparent low wall — occupants can see + down to the concourse below, but upward LOS from z=1 is blocked except + at the staircase. Access restricted to Commission personnel. + +tile_bounds: + x_min: 0 + y_min: 0 + x_max: 31 + y_max: 9 + z: 2 + +# Legend: +# R = Restricted (Commission-only access) +# W = Wall (solid) +# +# Layout (32 wide x 10 tall): +# Row 0: North wall with staircase entrance (col 1 = R, access point) +# Rows 1-8: Gallery floor (R tiles — restricted access) +# Row 9: South wall (gallery rail — transparent low wall, modeled as W; +# cross-z LOS is handled server-side) + +tiles: + - "WRWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + - "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW" + - "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW" + - "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW" + - "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW" + - "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW" + - "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW" + - "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW" + - "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW" + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + +sightlines: + open: true + notes: > + Gallery rail is transparent — full downward LOS to gate concourse (z=1). + Upward LOS from concourse to gallery is blocked except at staircase. + Cross-z LOS behavior handled by server shadowcasting system. diff --git a/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/gate-ground.yaml b/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/gate-ground.yaml new file mode 100644 index 000000000..725c1fd43 --- /dev/null +++ b/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/gate-ground.yaml @@ -0,0 +1,83 @@ +# Location: Gate Corridor — Ground Level +# Source: D-093 gate cluster zone spec +# Zone palette: surface #b8bec4 / fog tint #0a1222 + +canonical_id: "krenn.sova.transit.location.gate-ground" +display_name: "Gate Corridor" +description: > + The gate cluster handles all traffic between Station Sova and the horizon + gate network. From north to south: the restricted aperture chamber where + span gate transits occur, freight staging and passenger arrival halls, + customs lanes (freight and pedestrian), and the wide gate concourse + where arrivals disperse into the transit district. + +tile_bounds: + x_min: 0 + y_min: 0 + x_max: 39 + y_max: 33 + z: 1 + +# Legend: +# F = Floor (walkable) W = Wall (solid, blocks LOS) +# R = Restricted (access-tier gated) +# +# Layout (40 wide x 34 tall): +# Row 0: North wall +# Rows 1-4: Aperture chamber (centered 8 wide, R tiles — restricted) +# Row 5: Wall separator +# Rows 6-13: Freight staging (west, 24 tiles) | passenger arrival (east, 13 tiles) +# Row 14: Wall with door gaps (cols 12, 32) +# Rows 15-24: Freight customs (west, 20 tiles, 3 lanes at 6vt + wall stubs) +# | corridor (6 tiles) | ped customs (east, 10 tiles, 3 lanes) +# Row 25: Wall with wide opening to concourse (cols 5-34 open) +# Rows 26-33: Gate concourse (full 38-tile width, 8 rows — public open space) +# +# D-093 zone dimensions: freight customs 20x10, ped customs 12x10 (10 rows each) +# Corridor width: gate concourse 8vt (D-093) + +tiles: + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + - "WWWWWWWWWWWWWWWWRRRRRRRRWWWWWWWWWWWWWWWW" + - "WWWWWWWWWWWWWWWWRRRRRRRRWWWWWWWWWWWWWWWW" + - "WWWWWWWWWWWWWWWWRRRRRRRRWWWWWWWWWWWWWWWW" + - "WWWWWWWWWWWWWWWWRRRRRRRRWWWWWWWWWWWWWWWW" + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WWWWWWWWWWWWFWWWWWWWWWWWWWWWWWWWFWWWWWWW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WWWWWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWWWWW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + +sightlines: + open: false + notes: > + Gate concourse (rows 26-33) is wide open with full sightlines. + Customs lanes have wall stubs creating partial occlusion between + lanes. Freight customs (west, 20 tiles, 3 lanes at 6vt) is wider + than pedestrian customs (east, 10 tiles, 3 lanes). Aperture chamber + is fully walled and restricted. Cross-z LOS from the observation + gallery (z=2) into the concourse is handled server-side. diff --git a/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/maintenance-corridors.yaml b/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/maintenance-corridors.yaml index 74037a133..467625001 100644 --- a/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/maintenance-corridors.yaml +++ b/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/maintenance-corridors.yaml @@ -1,2 +1,54 @@ # Location: Maintenance Corridors (smuggling spaces) -# canonical_id: krenn.sova.transit.location.maintenance-corridors +# Source: D-093, #313 (Sprint 12 spatial layout) +# Zone palette: surface #4e5054 / fog tint #101214 +# z=0 (Era 1, below main structures) + +canonical_id: "krenn.sova.transit.location.maintenance-corridors" +display_name: "Maintenance Corridors" +description: > + Era 1 maintenance infrastructure beneath the transit district. A long + transition corridor connects the logistics hub to the bar district. + Restricted storage at the west end is shared with The Terminal above. + A maintenance hatch at the east end provides the cross-z connection + to the main level. Everything reads as mundane maintenance — the ring + operates here because nobody looks twice. + +tile_bounds: + x_min: 0 + y_min: 0 + x_max: 57 + y_max: 5 + z: 0 + +# Legend: +# F = Floor (walkable) W = Wall (solid, blocks LOS) +# R = Restricted (access-tier gated) +# +# Layout (58 wide x 6 tall): +# Row 0: North wall +# Row 1: Restricted storage (west, 8 tiles) | wall | corridor (40 tiles) | wall | hatch room (east, 6) +# Row 2: Doors connect rooms (F in wall positions at cols 9 and 50) +# Row 3-4: Same as row 1 (walls between rooms) +# Row 5: South wall +# +# Corridor width: 2vt visible at rows 1-4 (internal height, maintenance standard) +# Transition corridor: 40 tiles (~40m at 1m/vt) between storage and hatch per D-093 +# Restricted storage: R tiles for access restriction +# Maintenance hatch: F tiles (cross-z connection is future movement feature) +# Design: mundane maintenance appearance per D-093 G-08 + +tiles: + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + - "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WRRRRRRRRFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + +sightlines: + open: false + notes: > + Long straight corridor (40 tiles) with limited concealment. Storage and + hatch rooms are separated by walls with single-tile doors at row 2. + Mundane appearance per D-093 G-08 — no visually suspicious tile + arrangements. diff --git a/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/the-last-shift.yaml b/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/the-last-shift.yaml index cf07df16d..fd596ad81 100644 --- a/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/the-last-shift.yaml +++ b/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/the-last-shift.yaml @@ -1,2 +1,71 @@ # Location: The Last Shift (bar) -# canonical_id: krenn.sova.transit.location.the-last-shift +# Source: D-093, #312 (Sprint 12 spatial layout) +# Zone palette: surface #6b4018 / fog tint #200c04 + +canonical_id: "krenn.sova.transit.location.the-last-shift" +display_name: "The Last Shift" +description: > + A converted maintenance bay turned bar. The long counter runs along the + west wall with full sightlines across the room. A corner booth in the + northeast sees the bar, card table, entrance, and back room — prime + observation real estate. Scattered tables fill the main floor. A back + room with an alley exit provides the key traversal route for the ring. + +tile_bounds: + x_min: 0 + y_min: 0 + x_max: 33 + y_max: 21 + z: 1 + +# Legend: +# F = Floor (walkable) W = Wall (solid, blocks LOS) +# +# Layout (34 wide x 22 tall): +# Row 0: North exterior wall +# Rows 1-4: Main floor + corner booth (NE, cols 26-31, walled alcove) +# Row 5: Bar counter starts (cols 1-2 = W fixture) +# Rows 6-10: Bar counter (cols 1-2 W), tables (W stubs), card table (col 25) +# Row 11: Open transition floor +# Rows 12-14: Table clusters (W stubs at cols 8, 16, 24) +# Row 15: News ticker mount (col 31 = W stub) +# Row 16: Open floor +# Row 17: Back room north wall with door (col 22 = F) +# Rows 18-20: Main floor (west) + back room (east, cols 22-32) +# Row 21: South wall with alley exit gap (cols 29-32 = F) + +tiles: + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WWWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WWWFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFW" + - "WWWFFFFFFFWFFFFFFFWFFFFFFWFFFFFFFW" + - "WWWFFFFFFFWFFFFFFFWFFFFFFFFFFFFFFW" + - "WWWFFFFFFFFFFFWFFFFFFFFFFFFFFFFFFW" + - "WWWFFFFFFFFFFFWFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFWFFFFFFFWFFFFFFFWFFFFFFFFW" + - "WFFFFFFFWFFFFFFFWFFFFFFFWFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFWFWFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFW" + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWFFFFW" + +sightlines: + open: false + notes: > + Corner booth (NE alcove, rows 1-4) has LOS to bar counter, card table + area, main entrance, and back room door. Bar counter along west wall + (cols 1-2, rows 5-10) gives staff full sightlines across the main + floor. Back room is partially occluded by wall at col 21. Back room + door at col 22. Table W-stubs create partial cover but do not fully + block sightlines. + +social_site: "bar" diff --git a/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/the-terminal.yaml b/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/the-terminal.yaml index 7ea006283..42fc0ab54 100644 --- a/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/the-terminal.yaml +++ b/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/the-terminal.yaml @@ -1,2 +1,77 @@ # Location: The Terminal (logistics hub) -# canonical_id: krenn.sova.transit.location.the-terminal +# Source: D-093, #311 (Sprint 12 spatial layout) +# Zone palette: surface #7a8490 / fog tint #0d1520 + +canonical_id: "krenn.sova.transit.location.the-terminal" +display_name: "The Terminal" +description: > + Sova's freight logistics hub. Scanner bays filter incoming cargo at the + south entrance; a wide main corridor runs north through the manifest + processing floor. The break room sits in the northeast. The supervisor's + office overlooks the corridor through a large interior window. Restricted + storage in the northwest corner is where the ring hides re-tagged cargo. + +tile_bounds: + x_min: 0 + y_min: 0 + x_max: 43 + y_max: 27 + z: 1 + +# Legend: +# F = Floor (walkable) W = Wall (solid, blocks LOS) +# V = Void (outside building) R = Restricted (access-tier gated) +# +# Layout (44 wide x 28 tall): +# Rows 0: North exterior wall +# Rows 1-4: Restricted storage (NW, 11 tiles) + corridor + supervisor office (NE, 9 tiles) +# Row 5: Storage south wall; corridor + office continue +# Rows 6-8: Open corridor; supervisor office east side +# Row 7: ** Supervisor window — col 28 is F (LOS gap into office) ** +# Row 9: Supervisor office south wall +# Rows 10-11: Main corridor (full width chokepoint — high sightline value) +# Rows 12-17: Manifest processing (west, 14 tiles) + break room (east, 13 tiles) +# Row 18: Break room south wall; manifest open to corridor +# Rows 19-21: South corridor +# Row 22: Scanner bay north walls with entry gaps +# Rows 23-26: Scanner bay interiors (3 bays) +# Row 27: South exterior wall + +tiles: + - "VVVVVWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWVVVVV" + - "VVVVVWRRRRRRRRRRRWFFFFFFFFFFWFFFFFFFFFWVVVVV" + - "VVVVVWRRRRRRRRRRRWFFFFFFFFFFWFFFFFFFFFWVVVVV" + - "VVVVVWRRRRRRRRRRRWFFFFFFFFFFWFFFFFFFFFWVVVVV" + - "VVVVVWRRRRRRRRRRRWFFFFFFFFFFWFFFFFFFFFWVVVVV" + - "VVVVVWWWWWWWWWWWWWFFFFFFFFFFWFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFFFFFFFFFWWWWWWWWWWWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFWFFFWFFFFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFWFFFWFFFFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFWFFFWFFFFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFWFFFWFFFFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFWFFFWFFFFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFWFFFWFFFFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFFFFFWWWWWWWWWWWWWWWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWVVVVV" + - "VVVVVWWWWFFFFFFWWWWFFFFFFFFWWWWFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFWFFFFFFFFFFFWFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFWFFFFFFFFFFFWFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFWFFFFFFFFFFFWFFFFFFFFFFWVVVVV" + - "VVVVVWFFFFFFFFFWFFFFFFFFFFFWFFFFFFFFFFWVVVVV" + - "VVVVVWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWVVVVV" + +sightlines: + open: false + notes: > + Supervisor office window faces main corridor at row 7, col 28 (F gap in + wall line). Player standing in the corridor has direct LOS into the + office interior. This is the primary investigative discovery moment. + +social_site: "logistics-hub" diff --git a/docs/architecture/fog-shader-spec.md b/docs/architecture/fog-shader-spec.md index 3213eb205..0f4fec8c2 100644 --- a/docs/architecture/fog-shader-spec.md +++ b/docs/architecture/fog-shader-spec.md @@ -10,16 +10,23 @@ The fog system was simplified from the original 5-layer spec in Sprint 22 (#569) | Original D-059 Layer | Sprint 22 Status | Notes | |----------------------|------------------|-------| -| 1. Clear (forward cone) | Implemented | Soft Gaussian gradient (7x7, sigma 2.0) | -| 2. Light fog (cone gradient) | Simplified — merged into gradient | Peripheral sector removed from server (#569); Gaussian blur provides soft transition | -| 3. Deep fog (previously explored) | Implemented — EXP_EXPLORED | Alpha 0.55-0.70 with zone temperature tint (#563) | +| 1. Clear (forward cone) | Implemented | CPU Gaussian blur (sigma 2.0) → 4× bilinear upscale → RGBA8. Smooth sub-tile gradients. | +| 2. Light fog (cone gradient) | Simplified — merged into gradient | Peripheral sector removed from server (#569); CPU blur + bilinear upscale provides soft transition | +| 3. Deep fog (previously explored) | Simplified — D-015 light fog | Alpha 0.25-0.35 with zone temperature tint. Explored tiles show through light haze. | | 4. Unexplored + maps app | Deferred | v0.1.2+, requires mapped_tiles in ObserverSnapshot | | 5. Unexplored (no maps) | Implemented | Solid near-black #12141a | -**Alpha values (Sprint 22, #563):** -- Light fog zone (near cone gradient, vis 0.0-0.3): alpha 0.25-0.35, breathing ±0.05 (8-10s) -- Deep fog zone (EXP_EXPLORED, vis≈0): alpha 0.55-0.70, breathing ±0.075 (15-20s) -- Zone temperature tint active in deep fog zone (D-059/D-046/D-077) +**Texture pipeline (fog_state.gd):** +Binary 0/255 data at 1× tile resolution → CPU Gaussian blur (sigma 2.0, radius 4) +→ `Image.resize()` 4× bilinear upscale → `Image.convert()` RGBA8. +GL compat mode doesn't bilinear-filter R8 textures; RGBA8 at 4× resolves this. +Exploration data is binarized (0/128/255 → 0/255) before blur to avoid a +second gradient at the explored/visible boundary. + +**Alpha values (Sprint 22, D-015):** +- Light fog zone (explored, out of cone): alpha 0.25-0.35, breathing ±0.05 (8-10s) +- Zone temperature tint active in explored zone (D-059/D-046/D-077) +- Explored/unexplored boundary: squared fade keeps fog opaque at tile content edge ## Overview @@ -120,30 +127,27 @@ func update_from_state() -> void: File: `client/shaders/fog.gdshader` -The shader determines fog state per pixel based on the visibility and exploration textures. -Two explored sub-zones are distinguished by the blurred `vis` value (proximity to the forward cone): +The shader reads pre-smoothed RGBA8 textures (CPU blur + 4× bilinear upscale) +and determines fog state per pixel. No GPU-side blur — 2 texture reads per pixel. ```glsl -// Light fog noise — fast cycle (8-10s), subtle ±0.05 breathing -float noise_fast = texture(noise_tex, tile * 0.03 + vec2(time * 0.11, time * 0.07)).r; -// Deep fog noise — slow cycle (15-20s), more pronounced ±0.075 breathing -float noise_slow = texture(noise_tex, tile * 0.02 + vec2(time * 0.05, time * 0.035)).r; +// Fog noise — 8-10s breathe cycle, ±0.05 symmetric around baseline +float noise_val = texture(noise_tex, tile * 0.03 + vec2(time * 0.11, time * 0.07)).r; +float fog_alpha = 0.30 + (noise_val * 2.0 - 1.0) * 0.05; // 0.25-0.35 -// Light fog: alpha 0.25-0.35 (near cone gradient) -float light_fog_alpha = 0.30 + (noise_fast * 2.0 - 1.0) * 0.05; -// Deep fog: alpha 0.55-0.70 (far from cone, EXP_EXPLORED) -float deep_fog_alpha = 0.625 + (noise_slow * 2.0 - 1.0) * 0.075; - -// Blend deep <-> light fog by proximity to cone: -// vis=0 (far from cone) → deep_factor=1 → deep fog color + alpha -// vis=0.3 (cone gradient) → deep_factor=0 → light fog color + alpha -// vis=0.85+ (inside cone) → clarity=1 → transparent (clear) -float deep_factor = 1.0 - smoothstep(0.0, 0.30, vis); -float fog_alpha = mix(light_fog_alpha, deep_fog_alpha, deep_factor); - -// Zone temperature tint (D-059/D-046/D-077): deep fog color = zone tint +// Zone temperature tint (D-046/D-077): subtle warm/cool/neutral per zone vec3 zone_tint = texture(zone_tint_tex, tex_uv).rgb; -vec3 fog_color = mix(DARK_OVERLAY, zone_tint, deep_factor); + +// Clarity ramp: transparent inside cone, light fog at edges and beyond +float clarity = smoothstep(0.0, 0.85, vis); +float alpha = mix(fog_alpha, 0.0, clarity); +vec3 color = mix(zone_tint, vec3(0.0), clarity); + +// Explored/unexplored boundary: squared fade hides tile content edges +float exp_fade = smoothstep(0.3, 1.0, explored); +exp_fade *= exp_fade; // Steeper: fog stays opaque near content edge +alpha = mix(1.0, alpha, exp_fade); +color = mix(UNEXPLORED_COLOR, color, exp_fade); ``` **Zone temperature palette (D-046):** @@ -215,11 +219,13 @@ Per tick (in _process or on snapshot signal): | Component | Budget | Estimate | Notes | |-----------|--------|----------|-------| -| Visibility texture upload | 0.1ms | ~0.05ms | 400 pixels via set_pixel() | -| Exploration texture update | 0.1ms | ~0.05ms | Incremental — only changed tiles | -| Fragment shader (1080p) | 0.5ms | ~0.2ms | Single full-screen pass, simple math | +| CPU Gaussian blur (1×, 40×40) | 0.2ms | ~0.1ms | Separable, sigma=2.0, radius=4 (vis: 1 pass, exp: 2 passes) | +| Image.resize C++ (40→160) | 0.1ms | ~0.05ms | INTERPOLATE_BILINEAR, 2× textures | +| Image.convert R8→RGBA8 | 0.1ms | ~0.02ms | GL compat bilinear requires RGBA8 | +| Texture upload (RGBA8 160×160) | 0.2ms | ~0.1ms | 2× textures, ~200KB total | +| Fragment shader (1080p) | 0.2ms | ~0.05ms | 2 texture reads/px (was 98 with GPU blur) | | Fog entity sprites | 0.2ms | ~0.05ms | 0-15 sprites, trivial draw calls | -| **Total** | **<1ms** | **~0.35ms** | Well within D-059 budget | +| **Total** | **<1ms** | **~0.37ms** | Well within D-059 budget | ## Files to Create diff --git a/docs/backups/settledreach.db.backup b/docs/backups/settledreach.db.backup index 69b281ff5..e47628a3e 100644 Binary files a/docs/backups/settledreach.db.backup and b/docs/backups/settledreach.db.backup differ diff --git a/docs/sprints/sprint-24/client.md b/docs/sprints/sprint-24/client.md new file mode 100644 index 000000000..5cbb70896 --- /dev/null +++ b/docs/sprints/sprint-24/client.md @@ -0,0 +1,149 @@ +# Sprint 24: Signal — Client Tasks + +**Goal:** Wire the storyteller's activation event into player-visible consequences, thread character archetype through the full session lifecycle, and deliver the first unscripted end-to-end v0.1 playthrough — from main menu to triangle activation. + +**Branch:** `client` +**Agents:** Stig (UI/rendering), Hoshe (QA) + +> **This is the capstone sprint for v0.1.** Client work this sprint wires the player-facing signal +> that makes the simulation legible as a story: the character select screen, the triangle +> activation response, and the news ticker. All three must land before #593 (playthrough proof) +> can be filed as done. Sprint 25 is playtest. There is no Sprint 26 before v0.1 ships. + +--- + +## New Tickets + +| # | Title | Blocked by | +|---|-------|------------| +| #588 | Character archetype selection — client: character select screen before session start | #587 (server: archetype in StartupMessage) | +| #590 | Triangle activation consumer — client: react to triangle_crisis_events (monologue chime + urgent overlay) | #589 (server: tell escalation emitted) | +| #592 | News ticker — client: scrolling ticker HUD element in The Last Shift zone | #591 (server: ticker in snapshot) | + +Use `tooling/db/ticket show ` for full details. + +--- + +## Key Decisions + +- `decisions/scope.md` — D-027 (vertical slice success criteria — the 4 tests this sprint's work must satisfy), D-039 (wow moments — #1 Arrival: opening monologue; #5 News Ticker Gut-Punch: same ticker, opposite monologue reactions) +- `decisions/architecture.md` — D-020 (ObserverSnapshot is the only data crossing IPC; StartupMessage is the client→server init message; PROTOCOL_VERSION gates wire compatibility), D-042 (UI microcopy in `client/data/ui-strings.yaml` via UIStrings autoload) +- `decisions/content.md` — D-032 (separate monologue pools per character — client does not select the pool; the character string in StartupMessage drives server-side selection), D-074 (audio aesthetic — monologue chime = insert-tech: synthetic, precise, no reverb) +- `decisions/perception.md` — D-067 (recognition chime fires at onset of cognitive delay — `sfx_monologue_chime_urgent.ogg` is the correct asset for triangle activation), D-016 (internal monologue as perception bridge — client only displays what server sends; no client-side monologue logic) + +--- + +## Notes + +### #588 — Character archetype select screen + +**What exists:** `client/ui/main_menu.gd` (130 lines) — "New Game" button triggers `SessionManager.new_game()` which creates a save directory and seeds `GameState.world_seed`. It then loads `GAME_SCENE` directly, with no character selection step. `client/scripts/autoloads/session_manager.gd` — `new_game()` returns a `game_id` but does not record which archetype was chosen. `client/scripts/autoloads/game_state.gd` — check whether a `character_archetype` field already exists (likely not — add it). `CharacterArchetype` is a server-side enum; the client needs to record the chosen value as a string (`"smuggler"` or `"detective"`) and include it in `StartupMessage` sent over IPC. + +**What to deliver:** + +1. `GameState.character_archetype: String` — new field, default `"detective"`. Persisted alongside `world_seed` in the save directory (`user://saves//character.txt` or extend the existing seed file format). + +2. Character select scene — insert a step between "New Game" and loading `main.tscn`. This can be a new scene (`client/scenes/character_select.tscn`) or a modal panel within `main_menu.tscn`. Show two options: **Smuggler** and **Detective**. Each option shows the character name, a one-line role description, and a two-line tone description (see below). On selection, set `GameState.character_archetype`, then proceed to `main.tscn`. + + **Smuggler card:** + - Name: `Smuggler` + - Role: `Freight logistics worker — Sova Transit` + - Tone: `Insider access. Social camouflage. The ring is your daily life.` + + **Detective card:** + - Name: `Detective` + - Role: `Commission investigator — External assignment` + - Tone: `Institutional authority. Analytical lattice. You were sent here.` + + These strings belong in `client/data/ui-strings.yaml` (D-042), not hardcoded in GDScript. + +3. `Protocol.encode_startup_message()` update — add `character_archetype` to the StartupMessage dict before it is serialized. The server's `StartupMessage` struct now has `pub character_archetype: CharacterArchetype` (#587). Map client string `"smuggler"` → server enum variant `Smuggler`. In MessagePack/GDScript, this is just a string field added to the dict: `{ "world_seed": ..., "character_archetype": "Smuggler" }`. + +4. `Protocol.PROTOCOL_VERSION = 19` — bump to match server #587. The client must send the new version on handshake. This is a **hard coordination point** with Dudley — client and server PRs must land together or in the same merge window. A version mismatch will crash the connection on the handshake check. + +**UI constraints:** The character select screen must feel intentional, not an afterthought. Two full-width cards, dark background, character name in the sprint's color palette (consistent with main menu). No portraits (art is deferred). Cards are selectable via keyboard (left/right arrows) and mouse click. The selection is confirmed with Enter or a "Begin" button. ESC cancels back to the main menu without creating a save directory. + +**Non-obvious gotcha:** `SessionManager.new_game()` currently creates the save directory before any game scene loads. The character select step happens after `new_game()` creates the directory but before the game scene loads. `GameState.character_archetype` must be set before `SimBridge` sends the `StartupMessage` — which happens when `main.tscn` is ready and `SimBridge._ready()` connects to the server. Verify the ordering: `new_game()` → character select panel → user picks archetype → `GameState.character_archetype` set → `main.tscn` loads → `SimBridge._ready()` fires → `StartupMessage` includes archetype. + +**Blocked by:** #587 (server must define `character_archetype` field in `StartupMessage` before client serialization is finalized). + +--- + +### #590 — Triangle activation consumer + +**What exists:** `client/scripts/snapshot_event_router.gd` — routes snapshot fields to registered handlers. `client/scripts/main.gd` — registers handlers on `_router`. `client/scripts/autoloads/sim_bridge.gd` — `_on_snapshot_received()` decodes and emits snapshot. `client/scripts/protocol/protocol.gd` — `decode_snapshot()` returns a dict from the MessagePack bytes. The server snapshot wire type (`ObserverSnapshotWire`) has a `triangle_crisis_events: Vec` field (see `server/src/bridge/types.rs` line ~181). This field is present in the MessagePack output. **The client currently ignores it entirely** — there is no decode path for `triangle_crisis_events` in `protocol.gd` and no handler registered in `main.gd`. + +**What to deliver:** + +1. **Decode `triangle_crisis_events`** in `protocol.gd` `decode_snapshot()`. The field is an array of dicts, each with at minimum `{ "triangle_id": int }`. Add it to the returned snapshot dict as `"triangle_crisis_events": Array`. + +2. **Handle activation in `main.gd`** — register a handler that reads `triangle_crisis_events` from the snapshot. When the array is non-empty (at least one event), fire the urgent monologue chime: `AudioManager.play_one_shot(AudioManager.CHIME_RECOGNITION, AudioManager.BUS_UI_SOUNDS)` — wait, check the constant name. The correct asset is `sfx_monologue_chime_urgent.ogg` (D-038, D-067 "sharper variant for contradiction/anomaly"). `AudioManager` has `const CHIME_RECOGNITION := "sfx_monologue_chime"` — add `const CHIME_ACTIVATION := "sfx_monologue_chime_urgent"` if it doesn't exist, then call `AudioManager.play_one_shot(CHIME_ACTIVATION, BUS_UI_SOUNDS)`. + +3. **Deduplication** — the triangle activation is a one-shot event (v0.1 fires once per session per D-072/D-089). The client must not fire the chime on every subsequent tick that includes the event in the array. Track activated triangle IDs in a local `Set` in `main.gd`. If `triangle_id` is already in the set, skip. Add to set on first encounter. + +4. **No overlay UI** — the monologue chime is the client-side signal. The copy team (#597) authors the proximity monologue lines that fire when the player observes the activated NPC's `tell_state: RoutineDeviation`. The client does not need to render a special overlay or notification — the tell state on the entity and the subsequent proximity monologue are the visible consequence. Keep client reaction to: chime + deduplication tracking only. + +**Why no overlay:** D-039 wow moment #2 ("The Character's Eye") is about the monologue noticing something the player didn't. Adding a UI overlay would make it a notification, not a character observation. The feel is: you're wandering near Kael, suddenly you hear the chime — then the next monologue line is your character's internal voice noticing something is off. The server sends the `tell_state: RoutineDeviation` on the NPC entity; the client's entity renderer already renders this as visible entity data that can trigger `observe_npc` monologue. + +**Blocked by:** #589 (server must send non-empty `triangle_crisis_events` for client to handle). + +--- + +### #592 — News ticker HUD + +**What exists:** `client/ui/hud.gd` and `hud.tscn` — main HUD container. `client/ui/time_display.gd` — insert-style time display already wired via `_router.register_always(time_display.update_from_state)` in `main.gd`. No ticker node or script exists. Server snapshot will carry `current_ticker: Optional<{ id, text, category }>` when player is in the bar zone (#591). + +**What to deliver:** + +1. `client/ui/news_ticker.gd` + `news_ticker.tscn` — a horizontal scrolling text bar. Design: narrow strip (24–32px tall), anchored top of screen or bottom above the dialogue box, full width. Background: dark semi-transparent (`Color(0.05, 0.05, 0.07, 0.75)`). Text: scrolls left at a constant rate (~60px/sec). Text content: the `text` field from `current_ticker`. When `current_ticker` is `null` (player is not in bar zone), the ticker hides itself (`visible = false`). + +2. Wire in `main.gd` — add `@onready var news_ticker = $UILayer/NewsTicker` and register: `_router.register_always(news_ticker.update_from_state)`. Implement `news_ticker.update_from_state(snapshot: Dictionary)`: read `snapshot.get("current_ticker")`, update text if changed, show/hide based on null. + +3. **Insert overlay compatibility** — the ticker lives on `UILayer` (z-layer 7 per D-049). When the insert overlay is open (`GameState.insert_active = true`), the ticker should NOT be hidden — the news terminal is a real-world object the player can see while their insert is open. Do not call `set_insert_active` on the ticker. + +4. **Scrolling behavior** — the headline scrolls in from the right and exits left. When it exits, it restarts from the right with the same text (the server rotates the headline every 200 ticks; client just loops whatever it currently has). No crossfade, no fade-in. Pure marquee. + +**UI location:** Confirm with the sprint visual check that the ticker does not occlude the time display (top-right insert) or the monologue display (top-center). If there is a conflict, anchor the ticker at the bottom-center above the dialogue box, 4px margin above. + +**Blocked by:** #591 (server must send `current_ticker` field in snapshot before client has real data to render; before that, the ticker renders nothing and stays hidden). + +--- + +## Dependency Chain + +``` +#587 (server: archetype in StartupMessage) + └→ #588 (character select screen) ← start after #587 is merged + └→ PROTOCOL_VERSION 17→19 bump (coordinate with server) + +#589 (server: tell escalation) + └→ #590 (triangle activation consumer) ← start after #589 is merged + +#591 (server: ticker in snapshot) + └→ #592 (news ticker HUD) ← start after #591 is merged + +#588 + #590 + #592 → #593 (playthrough proof — server ticket) +``` + +All three client tickets are blocked on their respective server tickets. Start with `protocol.gd` decode additions speculatively (no server data yet — verify against `server/src/bridge/types.rs` for field names), then wire the handlers once server branches are merged to main. + +--- + +## PR Workflow + +```bash +tea pr create --repo jpmschweitzer/settled-reach --login schweitz \ + --title "feat(client): character archetype select screen" \ + --description "body" --base main --head client +``` + +--- + +## Sprint Completion (Client Criteria) + +1. From main menu, "New Game" → character select screen appears. Both cards render. Keyboard and mouse selection work. ESC cancels without creating a save directory. +2. Selected archetype is persisted in `GameState.character_archetype` and sent in `StartupMessage`. Server receives correct archetype (verify via debug console `status` — it should report the active archetype if Dudley adds it to the status response). +3. `PROTOCOL_VERSION = 19` — client matches server. Connection handshake succeeds. +4. When `triangle_crisis_events` is non-empty in snapshot, `sfx_monologue_chime_urgent.ogg` fires once. Does not re-fire on subsequent ticks. +5. News ticker visible and scrolling in The Last Shift zone. Hidden in all other zones. Text matches the server-sent headline. +6. `make test-client` green on client branch. No regressions in existing test suite. diff --git a/docs/sprints/sprint-24/copy.md b/docs/sprints/sprint-24/copy.md new file mode 100644 index 000000000..678e8c0c1 --- /dev/null +++ b/docs/sprints/sprint-24/copy.md @@ -0,0 +1,147 @@ +# Sprint 24: Signal — Copy Tasks + +**Goal:** Wire the storyteller's activation event into player-visible consequences, thread character archetype through the full session lifecycle, and deliver the first unscripted end-to-end v0.1 playthrough — from main menu to triangle activation. + +**Branch:** `copy` +**Agents:** Mellanie (dialogue), Paula (narrative), Gestalt (systems) + +> **This is the capstone sprint for v0.1.** The lines authored here are the player's first +> experience of the storyteller doing its job. When the triangle activates and Kael's behavior +> shifts, the character's internal voice is the signal. These lines carry D-039 wow moment #2 +> ("The Character's Eye") — the moment the monologue notices something the player didn't. +> Sprint 25 is playtest. Get these lines right. + +--- + +## New Tickets + +| # | Title | Blocked by | +|---|-------|------------| +| #597 | Triangle activation — copy: author 3–5 proximity monologue lines for TriangleActivated (per archetype) | — | + +Use `tooling/db/ticket show ` for full details. + +--- + +## Key Decisions + +- `decisions/scope.md` — D-039 (wow moment #2: "The Character's Eye" — monologue flags something the player didn't notice; urgent chime fires), D-027 (vertical slice criterion: the observe→notice→follow→discover sequence must emerge from systems, not scripts) +- `decisions/content.md` — D-032 (separate monologue pools per character — no shared lines between smuggler and detective), D-035 (tag taxonomy: `trigger: observe_npc`, `situation: [triangle_activated]`, `character`, `prerequisite` fields required), D-090 (PC voice registers — smuggler: contracted, street-cadenced, risk-calculating; detective: analytical, institutional, uncontracted) +- `decisions/content.md` — D-016 (monologue functions: perception bridge, atmosphere, diegetic hint, unreliable narrator — these lines are observation, not exposition), D-034 (THE FRIEND: Kael Davan is smuggler's FRIEND; Sera Venn is detective's FRIEND — these are the primary triangle anchor NPCs) +- `decisions/content.md` — D-024 (tell system — `RoutineDeviation` tell fires when NPC is off schedule; these lines should feel like the character noticing the deviation, not naming the conspiracy) + +--- + +## Open Questions to Resolve Early + +None blocking this ticket. The tag schema is fully specified (D-035). The character voice registers are documented (D-090). The NPC identities are confirmed (D-034). Author without waiting for server/client tickets to land — the content files are independent of the implementation. + +--- + +## Notes + +### #597 — Triangle activation proximity monologue lines + +**What exists:** The monologue pool system (`server/src/simulation/monologue.rs`) selects lines by `trigger`, `character`, `situation`, and `prerequisite`. The `observe_npc` trigger fires when the player is proximate to a specific NPC and has LOS to them. `RoutineDeviation` tell state is the server signal that a triangle anchor NPC is activated. The system can gate monologue lines on situation tags that correspond to game state — `triangle_activated` is a valid situation tag that the server can emit when `TriangleActivatedQueue` is non-empty. + +**What to deliver:** 3–5 monologue lines per character (smuggler and detective), placed in the correct pool files. + +**File locations:** +- Smuggler: `content/campaigns/main/systems/krenn/stations/sova/districts/transit/monologue/smuggler/the-terminal.yaml` +- Detective: `content/campaigns/main/systems/krenn/stations/sova/districts/transit/monologue/detective/the-terminal.yaml` + +(Kael and Torek are Terminal NPCs. Sera is also a Terminal presence. These lines fire when the player observes those NPCs post-activation while at The Terminal or maintenance corridors.) + +**Tag specification (D-035 required fields):** + +```yaml +- id: pc-smuggler_m_s_NNN # NNN = next available index in the smuggler pool + text: "..." + role: player_character + access: [public] + trust: surface + situation: [triangle_activated] + trigger: observe_npc + mood: [suspicious] # or [anxious] depending on line content + priority: 8 # higher than ambient lines; lower than opening hook (10) + cooldown: 9999 # fires once per activation per session + tags: [triangle-signal, tell-observation] + prerequisite: + npc_in_los: true # only fires when actively observing an NPC +``` + +**Smuggler lines — Kael Davan (triangle anchor: T1 hub-power)** + +The smuggler knows Kael. Kael is a colleague, possibly a friend. The lines must feel like noticing something personal about a familiar person, not flagging a conspiracy suspect. The smuggler's register (D-090): contracted, casual, risk-reading. + +Three beats to cover in 3–5 lines (one line per beat, no doubling): +1. Physical observation — Kael's behavior is off in a specific, observable way (posture, timing, direction). +2. Internal rationalization — the smuggler finds a mundane explanation first. This is deniable. +3. Doubt — the rationalization doesn't quite hold. The smuggler can't name what's wrong. The reader can. + +Example tone (do not use as final lines — author fresh): +- Beat 1: "Kael's on the main corridor. He doesn't usually work this route." (Too on-the-nose; soften) +- Beat 2: "Could just be a schedule swap. Voss does that sometimes." +- Beat 3: "...but Kael doesn't swap shifts." + +These are tone illustrations, not copy. Paula and Mellanie should author the actual lines. + +**Detective lines — Sera Venn and Torek Lintar (triangle anchor: T2 informant-question)** + +The detective is external — these are analytical observations about NPCs whose behavior creates a logical anomaly in the investigation pattern. Detective register (D-090): uncontracted, institutional framing, evidence-cataloguing internal voice. + +Three beats: +1. Pattern recognition — the detective logs a behavioral deviation as data. +2. Hypothesis formation — what does this deviation imply? (stated as a question, not a conclusion) +3. Procedural next step — the detective's internal instinct is to act, not just observe. + +Example tone: +- Beat 1: "Sera avoided eye contact with Torek again. Third occurrence in four observations." +- Beat 2: "If she knows something about his manifest discrepancy, why the silence?" +- Beat 3: "Worth a conversation. But not here." + +**What these lines must NOT do:** +- Name the conspiracy directly ("Kael is in the ring" / "Torek is covering evidence") +- Be omniscient — the character observes behavior, not motive +- Be too long — 1–2 sentences maximum per line; monologue is a flash of interiority +- Repeat vocabulary across lines — each line should use different sensory or cognitive entry + +**Verification:** Run the line previewer CLI to confirm schema compliance before committing: +```bash +tooling/db/sqlite-query 'SELECT * FROM ...' # check via line previewer if available +# Or: server/target/debug/line-previewer path/to/the-terminal.yaml +``` + +Confirm with `make validate-content` that the YAML parses without errors. + +--- + +## Dependency Chain + +``` +#597 (proximity monologue lines) — no blockers, author immediately + +#597 → #593 (playthrough proof — must land before sprint is closed) +``` + +Start authoring #597 immediately. It is unblocked. The lines need to be in the content files before the playthrough proof (#593) can be run. + +--- + +## PR Workflow + +```bash +tea pr create --repo jpmschweitzer/settled-reach --login schweitz \ + --title "feat(copy): triangle activation proximity monologue lines" \ + --description "body" --base main --head copy +``` + +--- + +## Sprint Completion (Copy Criteria) + +1. 3–5 smuggler monologue lines in `monologue/smuggler/the-terminal.yaml` with `situation: [triangle_activated]`, correct tag schema (D-035), `cooldown: 9999`, `trigger: observe_npc`. +2. 3–5 detective monologue lines in `monologue/detective/the-terminal.yaml` with the same schema. +3. `make validate-content` passes — YAML parses, required fields present, no FactId typos. +4. Voice consistency: smuggler lines are contracted, risk-reading; detective lines are analytical, institutional. Paula reviews before PR. +5. Lines do not name the conspiracy — they observe behavior and feel personal to the character. diff --git a/docs/sprints/sprint-24/joint.md b/docs/sprints/sprint-24/joint.md new file mode 100644 index 000000000..8150fb5b0 --- /dev/null +++ b/docs/sprints/sprint-24/joint.md @@ -0,0 +1,120 @@ +# Sprint 24: Signal — Joint / Integration + +**Goal:** Wire the storyteller's activation event into player-visible consequences, thread character archetype through the full session lifecycle, and deliver the first unscripted end-to-end v0.1 playthrough — from main menu to triangle activation. + +**Agents:** All implementation agents (Dudley, Tyre, Hoshe, Stig, Mellanie, Paula, Gestalt) + +> **This is the capstone sprint for v0.1.** Sprint 23 built the world. Sprint 24 makes it +> legible as a story. Sprint 25 is playtest. Every ticket in this sprint feeds into a single +> proof: a real human can boot the game, pick a character, play for 30 minutes, and experience +> the storyteller doing its job — without scripted interventions or debug shortcuts. + +--- + +## Pre-Sprint Actions + +These must happen at sprint start, before implementation tickets begin: + +| Action | Owner | Blocks | +|--------|-------|--------| +| Confirm `StartupMessage` wire format with `character_archetype` field | Dudley + Stig sync (day 1) | #588 — client serialization cannot be finalized until server field is published in `bridge/types.rs` | +| Confirm PROTOCOL_VERSION bump: 18 → 19 | Dudley + Stig | #587 server + #588 client must land together — version mismatch crashes handshake | +| Confirm `triangle_crisis_events` decode path for `TriangleCrisisEventWire` field names | Dudley + Stig sync (day 1) | #590 — client `protocol.gd` decode speculates from `bridge/types.rs`; confirm field names before merging | +| Confirm `current_ticker` snapshot field structure (`id`, `text`, `category`) | Dudley + Stig sync (day 1) | #592 — client ticker decode needs field names from server `bridge/types.rs` | + +--- + +## Cross-Team Dependencies + +| Dependency | Direction | Notes | +|------------|-----------|-------| +| `character_archetype` in StartupMessage | Server (#587) → Client (#588) | Client cannot send archetype until server defines the field. Stig reads `bridge/types.rs` for the field name before finalizing `Protocol.encode_startup_message()`. | +| PROTOCOL_VERSION 18 → 19 | Server (#587) ↔ Client (#588) | Both must land in the same merge window. A mismatch crashes the handshake. | +| `RoutineDeviation` tell in snapshot | Server (#589) → Client (#590) | Client registers `triangle_crisis_events` handler; server populates it after tell escalation is wired. | +| `current_ticker` in snapshot | Server (#591) → Client (#592) | Client ticker hides when field is absent (null). Client can be merged before server; it simply renders nothing until server sends data. | +| Triangle activation monologue lines | Copy (#597) → Server (#593) | Lines must be in YAML files before playthrough proof run. Copy is unblocked — merge first. | +| Playthrough proof | All tickets → Server (#593) | #593 is the convergence gate. Cannot be filed done until all upstream tickets are merged and CI is green across all branches. | + +--- + +## Housekeeping Done at Sprint Planning + +The following tickets were closed during planning as their work was already complete: + +| # | Reason | +|---|--------| +| #38 | Client-Server Integration epic — all 11 children done across Sprints 1–22 | +| #369 | v0.1 Content Scoping Workshop Outputs — all 39 children done | +| #455 | QA Strategy & Test Infrastructure — all 59 children done | +| #575 | LOS boundary bug — fixed by #584/#585 in Sprint 23 | +| #596 | Opening monologue content — fully authored in Sprint 12 (#299 smuggler, #300 detective) | + +--- + +## Sprint Completion Proof + +The sprint is done when **all of the following are observable in a live production session** (no `--gauntlet`, no `SR_TEST=1`, no forced debug shortcuts): + +1. **Character selection works.** From the main menu, click "New Game." A character select screen appears with two cards: Smuggler and Detective. Both keyboard and mouse selection work. ESC cancels without creating a save. Selecting a character proceeds to the game. + +2. **Opening monologue is character-correct.** The first monologue line that fires on session start matches the selected archetype. Smuggler opening: contracted, dock-worker voice, insider framing. Detective opening: analytical, institutional framing, uncontracted. Both fire within the first 5 seconds of game load. + +3. **Storyteller activates naturally.** Play as Smuggler. Walk to The Terminal. Observe Kael for ~3 game-minutes (no teleport, no debug shortcut). Eventually (after ~30 game-minutes real or via debug `contaminate`): the urgent monologue chime fires. The next time you approach Kael, a proximity monologue line fires from the `triangle_activated` situation pool. + +4. **Tell state is observable.** After activation, stand near Kael. The debug console `npc ` reports `tell_state: RoutineDeviation`. The entity renderer colors/indicators for Kael reflect the tell (future animation deferred — tell state emitted is sufficient for v0.1). + +5. **News ticker displays in The Last Shift.** Walk to The Last Shift bar. A scrolling text headline is visible in the HUD. Leaving the bar hides it. Returning shows a headline (possibly different, if 200 ticks have elapsed). + +6. **CI green across all branches.** `make ci` passes on `main`, `server`, `client`, `copy` branches. No regressions. + +--- + +## Test Plan Alignment (D-030) + +Sprint 24 is Phase 3 territory (D-030 Phase 3: CauseChain verification + divergent snapshots): + +- **#587/#588:** Protocol round-trip test — `StartupMessage { character_archetype: Smuggler }` survives serialize/deserialize. Phase 1 (data structure). +- **#589:** Unit test — after `TriangleActivatedQueue` is populated, triangle NPCs have `RoutineDeviation`; `derive_tell_state()` returns `RoutineDeviation`. Phase 1. +- **#591:** Unit test — `TickerPool` loads 30 headlines from YAML, rotation advances deterministically with `SimRng`. Phase 1. +- **#595:** Integration test — 1-tick simulation with `StartupMessage { character_archetype: Smuggler }` emits opening monologue line from smuggler pool (not detective pool). Phase 2. +- **#590:** Client test — mock snapshot with `triangle_crisis_events: [{ triangle_id: 1 }]` triggers `CHIME_ACTIVATION` play call once; second snapshot with same triangle_id does not retrigger. Phase 2. +- **#593:** End-to-end integration test — `test_v0_1_integration_playthrough` (server `tests/` directory). Boot server, send Smuggler startup message, advance to contamination via debug command, assert TriangleActivated and RoutineDeviation in golden snapshot. Phase 3. + +--- + +## Open Questions + +| ID | Question | Blocks | Action | +|----|----------|--------|--------| +| Q-052 | Storyteller hint delivery channels | — | Resolved at v0.1 scope this sprint. #589 implements channel 1 (behavioral tell escalation), #597 implements channel 7 (proximity monologue). Channels 2 (environmental change) and 4 (overheard NPC conversation) deferred to v0.2. File resolution note in `decisions/questions-content.md` after sprint. | + +--- + +## PR Merge Order + +To avoid conflicts on shared files (`bridge/types.rs`, `protocol.gd`, `PROTOCOL_VERSION`): + +1. Copy PR (#597) — no code dependencies; merge first. Lines must be in `main` before playthrough proof runs. +2. Server PRs (#589, #591, #594) — no client dependencies; merge in any order. These are independent. +3. **Server PR #587 (StartupMessage + archetype) + Client PR #588 (character select)** — must land together. Both bump `PROTOCOL_VERSION` to 19. Coordinate merge timing. +4. Client PR #590 (triangle consumer) — merge after server #589 is in main. +5. Client PR #592 (ticker HUD) — can merge before server #591 (renders nothing when field absent); merge after for clean integration test. +6. Server PR #595 (opening monologue gate) — merge after server #587 is in main. +7. Server PR #593 (playthrough proof) — last to merge. Requires all upstream PRs green. + +**Critical:** #587 and #588 share the PROTOCOL_VERSION bump. Do not merge one without the other. + +--- + +## v0.1 Readiness After This Sprint + +After Sprint 24 ships, the vertical slice (D-027) satisfies: + +| D-027 Criterion | Sprint 24 Delivery | Status | +|----------------|-------------------|--------| +| 30 minutes of daily-life breathing room before contamination | Contamination delay (Sprint 22 #254) + storyteller lifecycle (Sprint 23 #572) | Done — Sprint 22/23 | +| Both playthroughs feel fundamentally different | Character archetype select (#587/#588) + archetype-gated monologue (#595) + character voice (#597) | Done — this sprint | +| Player names an NPC they felt conflicted about | Kael/Sera content (Sprint 12–22) + tell escalation (#589) + proximity monologue (#597) | Done — this sprint | +| Observe→notice→follow→discover emerges from systems | Tell escalation (#589) + proximity monologue (#597) + existing dialogue/knowledge graph | Done — this sprint | + +Sprint 25 will address wow moments #3 (THE FRIEND's Contradiction) and #6 (The Quiet Moment) — the two that require deeper playtest-driven tuning. The core loop is complete after Sprint 24. diff --git a/docs/sprints/sprint-24/server.md b/docs/sprints/sprint-24/server.md new file mode 100644 index 000000000..48d72c07f --- /dev/null +++ b/docs/sprints/sprint-24/server.md @@ -0,0 +1,199 @@ +# Sprint 24: Signal — Server Tasks + +**Goal:** Wire the storyteller's activation event into player-visible consequences, thread character archetype through the full session lifecycle, and deliver the first unscripted end-to-end v0.1 playthrough — from main menu to triangle activation. + +**Branch:** `server` +**Agents:** Dudley (simulation), Tyre (architecture), Hoshe (QA) + +> **This is the capstone sprint for v0.1.** Everything server-side must converge on a production +> playthrough: correct character spawned, opening monologue fired, storyteller active, triangle +> escalation observable. Sprint 25 is playtest. There is no Sprint 26 before v0.1 ships. + +--- + +## New Tickets + +| # | Title | Blocked by | +|---|-------|------------| +| #587 | Character archetype — server: accept archetype in StartupMessage, spawn correct PC | — | +| #589 | Triangle activation consumer — server: behavioral tell escalation on TriangleActivated | — | +| #591 | News ticker — server: load ticker YAML, emit current headline in snapshot | — | +| #595 | Opening monologue trigger — server: emit opening lines at session start based on archetype | #587 | +| #593 | v0.1 playthrough proof: full session from main menu through storyteller activation | #587, #589, #591, #595 (+ client #588, #590, #592 + copy #597) | +| #594 | Tile data model design — produce D-record for extensible tile properties | — (design only, unblocks post-v0.1) | + +Use `tooling/db/ticket show ` for full details. + +--- + +## Key Decisions + +- `decisions/scope.md` — D-027 (vertical slice success criteria — the 4 tests the playthrough must satisfy), D-039 (6 wow moments — opening arrival, character's eye, FRIEND contradiction, divergence reveal, news ticker gut-punch, quiet moment) +- `decisions/content.md` — D-032 (separate monologue pools: `character` tag is a hard partition, not a filter), D-035 (tag taxonomy: `trigger`, `character`, `prerequisite` fields on monologue lines), D-023 (three-tier content model — storyteller activates Tier 1 modules based on engagement) +- `decisions/content.md` — D-024 (NPC 10-axis model — tell system axis 9; tells flow from simulation state not authored), D-036 (Sova Transit District — The Terminal, The Last Shift, news ticker content at `ticker/the-last-shift.yaml`) +- `decisions/architecture.md` — D-020 (StartupMessage is the only initialization crossing IPC; PROTOCOL_VERSION gates wire compatibility), D-041 (knowledge graph data model — KnowledgeConfidence confidence levels), D-010 principle 3 (no player identity baked into game loop — archetype is a configuration, not a special case) + +--- + +## Open Questions to Resolve Early + +- **Q-052: Storyteller hint delivery channels** — #589 implements channels 1 (behavioral tell escalation) and 7 (proximity monologue, via copy #597). Environmental change (channel 2) and overheard NPC conversation (channel 4) are deferred to Sprint 25. This sprint resolves Q-052 at v0.1 scope. No design discussion needed — the channel inventory is decided; implementation scope is constrained to what's achievable this sprint. + +--- + +## Notes + +### #587 — Character archetype in StartupMessage + +**What exists:** `StartupMessage` in `server/src/bridge/types.rs` has one field: `pub world_seed: u64`. The production startup path initializes all NPCs and spawns the player entity without any archetype selection — the player entity carries `CharacterArchetype::Detective` by default (see `server/src/simulation/monologue.rs` line ~174: `character: "detective".to_string()`). `CharacterArchetype` enum is already defined in `bridge/types.rs` with `Smuggler` and `Detective` variants (used by phase-2 verb filter in `server/src/perception/observer/mod.rs`). + +**What to deliver:** + +1. Add `pub character_archetype: CharacterArchetype` to `StartupMessage`. Default to `Detective` if absent during deserialization (backward-compatible via `#[serde(default)]`). + +2. In the production startup path (`server/src/main.rs` or `server/src/content/spawn.rs`), read `startup_msg.character_archetype` and insert the correct `CharacterArchetype` component on the player entity. The player entity is currently spawned without an archetype component — insert it here. + +3. In `server/src/simulation/monologue.rs`, read the `CharacterArchetype` component from the player entity at `MonologueState` initialization (or on first tick) and set `MonologueState.character` from it: `"smuggler"` or `"detective"`. This is the string that gates all monologue pool selection. + +4. Bump `PROTOCOL_VERSION` to 19. The new field in `StartupMessage` is a breaking change — old clients send a message that omits `character_archetype`, new server will default it correctly, but old servers receiving the new format will fail. Coordinate with Stig (#588) on timing. + +**Gotcha:** `CharacterArchetype` already serializes via serde. The existing `impl From for ObserverSnapshot` path in bridge tests validates round-trips — make sure the new `StartupMessage` test also covers the default case (`Smuggler` serializes and round-trips; missing field deserializes as `Detective`). + +**Unblocks:** #595 (opening monologue), #588 (client select screen can now send the archetype). + +--- + +### #589 — Triangle activation consumer: behavioral tell escalation + +**What exists:** `server/src/npc/tell_state.rs` — `TellCategory` enum (Nervous, Angry, Friendly, Guarded, RoutineDeviation) and `derive_tell_state()` function. Tell state is derived from NPC axis values each tick and emitted in `ObserverSnapshot.entities[].tell_state`. `RoutineDeviation` component exists. The storyteller emits `TriangleActivated { triangle_id, npc_entity }` into `TriangleActivatedQueue`. + +**What to deliver:** A system `escalate_tells_on_activation()` that: + +1. Drains `TriangleActivatedQueue` each tick (non-destructively — queue must still be readable by other consumers; use `events()` pattern or check if existing drain is appropriate). + +2. For each `TriangleActivatedEvent`, find the anchor NPC entity (`event.npc_entity`) and the 1-2 NPCs who are in the same triangle (via `TriangleState` query). These are the triangle's NPCs. + +3. Insert a `RoutineDeviation` component on all triangle NPCs. `RoutineDeviation` is the strongest tell category per `derive_tell_state()` priority order — it overrides Nervous/Angry/Guarded. This makes the triangle NPCs immediately observable as anomalous. + +4. The `RoutineDeviation` component should carry a `expires_at_tick: u64` field (if not already present) so it can be removed after a configurable window (suggest `TELL_ESCALATION_DURATION_TICKS = 300` = 30 game-minutes). Add a system to remove expired `RoutineDeviation` components. + +**Why this is the right channel:** D-024 axis 9 (tell system) is a simulation output, not authored content. The `RoutineDeviation` tell fires when the NPC is off their usual schedule — which is exactly true post-activation (the triangle is hot). No new content required. Client sees it as `tell_state: RoutineDeviation` on the visible entity. Copy (#597) authors the monologue lines the client fires when the player observes this tell. + +**Integration point:** `TriangleActivatedQueue` is in `server/src/storyteller/mod.rs`. The new system should live in `server/src/storyteller/` or `server/src/simulation/pressure.rs` — either is appropriate. Register in `StorytellerPlugin.build()`. + +**Test:** `cargo test` — verify that after `TriangleActivatedQueue` is populated with a test event, triangle NPCs have `RoutineDeviation` inserted and `derive_tell_state()` returns `RoutineDeviation` for them. + +--- + +### #591 — News ticker: server-side + +**What exists:** `content/campaigns/main/systems/krenn/stations/sova/districts/transit/ticker/the-last-shift.yaml` — 30 authored headlines (freight, politics, infrastructure, sports, commission, community categories). The YAML is fully authored (Sprint 12, #306). It is NOT loaded at runtime — the content loader (`server/src/content/loader.rs`) does not parse ticker files. `ObserverSnapshotWire` in `server/src/bridge/types.rs` has no ticker field. + +**What to deliver:** + +1. `TickerLine` struct: `id: String`, `text: String`, `category: String`. Add to `bridge/types.rs`. + +2. Add `current_ticker: Option` to `ObserverSnapshotWire`. Only populated when player is in The Last Shift zone (zone_id `"bar"` — check `server/src/simulation/zone.rs`). `None` in all other zones. + +3. `TickerPool` resource: loads `ticker/the-last-shift.yaml` at startup via the content loader. Holds all 30 headlines. Uses `SimRng` to advance to a new headline every `TICKER_ROTATION_TICKS = 200` ticks (20 game-minutes). Deterministic under D-010 — always use `SimRng`, never system randomness. + +4. In the observer snapshot system (`server/src/perception/observer/mod.rs`), read `TickerPool` and populate `current_ticker` when player zone is `"bar"`. + +**Gotcha:** The ticker rotation must use `SimRng` (the seeded bevy resource), not `rand::thread_rng()`. D-010 principle 4 — deterministic simulation. A ticker rotation driven by `thread_rng()` would produce different headlines on replay, breaking golden-file tests. + +**Content note:** The ticker content file at `ticker/the-last-shift.yaml` uses a `dual_lens` field per headline (separate monologue notes for smuggler vs detective perspective). These notes are for copy authoring reference only — do NOT include them in the `TickerLine` wire struct. The `text` field is what crosses the boundary; `dual_lens` is authoring metadata. + +--- + +### #595 — Opening monologue trigger + +**What exists:** `server/src/simulation/monologue.rs` — `MonologueState` has `pub character: String` (initialized as `"detective"`) and `pub enter_location_fired: bool`. The system `tick_monologue()` calls `select_monologue_line()` which filters pools by `pool.character != character` — so the character string gate already works. Opening monologue content is fully authored at: +- `content/campaigns/main/.../monologue/detective/opening.yaml` (12 lines, Sprint 12 #300) +- `content/campaigns/main/.../monologue/smuggler/opening.yaml` (Sprint 12 #299) + +Both files use `trigger: enter_location` and `situation: [arrival, shift_start]`. The `enter_location` trigger fires on the first tick (`MonologueState.enter_location_fired = false` → fires → sets `true`). + +**What to deliver:** This ticket is small because the infrastructure is almost complete. The only missing piece is that `MonologueState.character` is initialized as `"detective"` before the archetype is known. Fix: + +1. After #587 lands (archetype inserted on player entity), read `CharacterArchetype` in `MonologuePlugin.build()` or the first-tick setup system and set `MonologueState.character` correctly: `CharacterArchetype::Smuggler → "smuggler"`, `CharacterArchetype::Detective → "detective"`. + +2. Verify that `select_monologue_line()` with `trigger = "enter_location"` and `character = "smuggler"` correctly selects from `opening.yaml` in the smuggler subdirectory. Run the existing monologue integration test with a smuggler archetype — it should already pass once #587 sets the character string. + +3. Add a regression test: spawn two sessions (smuggler + detective), advance 1 tick each, assert different `character` on `MonologueState`. This verifies the archetype flows end-to-end. + +**Blocked by:** #587 (needs `CharacterArchetype` on player entity before this system can read it). + +--- + +### #594 — Tile data model design (D-record) + +**What exists:** Current tile format is single characters (`F/W/V/R`) in string arrays in location YAML files. This cannot represent per-tile properties (door access lists, container contents, damage state, visual variant, sound properties, trigger zones). Epic #586 tracks this. This ticket produces the design only — no migration, no implementation. + +**What to deliver:** A filed D-record in `decisions/architecture.md` via `tooling/db/decision claim D architecture "Tile data model — extensible per-tile properties"` before writing. The D-record must: + +1. Survey what tile-level data the game needs across systems (doors, containers, damage, visual variants, trigger zones, material properties). +2. Choose between: (a) tile palette/registry (tiles are typed by ID, properties on the type), (b) per-tile property bags (each tile can have arbitrary key-value), (c) ECS-style tile components (tiles are entities), or (d) hybrid. +3. Specify the YAML authoring format (human-writable, survives merge conflicts), the loader contract (how the server parses it), and the runtime representation (what ECS queries use). +4. Estimate migration effort for the 5 existing location YAMLs. + +**Scope:** This is design work only. No code changes. The D-record is the deliverable. Tyre should author it; Dudley reviews for implementation feasibility. File via the standard decision workflow (`decision claim` → edit `decisions/architecture.md` → commit with pre-commit hook running `decisions-sync`). + +--- + +### #593 — v0.1 playthrough proof + +**What exists:** By the time this ticket starts, all upstream tickets are merged: archetype in StartupMessage (#587), tell escalation (#589), ticker in snapshot (#591), opening monologue archetype-gated (#595), client character select (#588), client triangle consumer (#590), client ticker (#592), copy activation monologue lines (#597). + +**What to deliver:** This is an integration proof ticket, not an implementation ticket. Deliverable is a written test plan execution + green CI. + +1. **Full playthrough test (manual):** Boot server in production mode (no `--gauntlet`). Boot client in production mode (no `SR_TEST=1`). From main menu, click "New Game." Character select screen appears — select Smuggler. Game scene loads. Confirm: opening monologue fires (Smuggler voice). Walk to The Terminal. Observe Kael for ~3 game-minutes (check `EngagementRecord` via debug console `npc `). Use debug console `contaminate` to skip to contamination phase. Wait for activation pass — `triangles` command shows one triangle in Active phase. Walk back to Kael — `tell_state` is `RoutineDeviation`. Proximity monologue fires (from copy #597). Walk to The Last Shift — news ticker visible. CI green. + +2. **Automated integration test (server):** Add `test_v0_1_integration_playthrough` in `server/tests/`. Uses the existing test-client infrastructure (tooling/test-client): boot server, send `StartupMessage { world_seed: 12345, character_archetype: Smuggler }`, advance 1 tick, assert snapshot contains smuggler opening monologue, advance to tick 3000, send `SkipToContamination` debug command, advance 10 more ticks, assert `TriangleActivatedQueue` is non-empty in server state (via golden snapshot comparison). + +3. **PR merge coordination:** Server #593 and client #588/#590/#592 must all be green on their respective branches before this ticket can be filed as done. The playthrough proof is the collective gate for the sprint. + +**Gotcha — PROTOCOL_VERSION:** #587 bumps to 19. Client #588 must update `Protocol.PROTOCOL_VERSION` to 19 simultaneously. The version mismatch is a connection crash. Coordinate with Stig on merge timing. + +--- + +## Dependency Chain + +``` +#587 (archetype in StartupMessage) ─────────────────────────────────────────────────┐ + └→ #595 (opening monologue archetype-gated) │ + └→ #588 (client: character select screen) ← client work │ + ↓ +#589 (tell escalation on TriangleActivated) ─────────────────────────────────────── #593 + └→ #590 (client: triangle consumer) ← client work (v0.1 + playthrough +#591 (news ticker in snapshot) ─────────────────────────────────────────────────── proof) + └→ #592 (client: ticker HUD) ← client work │ + │ +#597 (copy: activation monologue lines) ← copy work ────────┘ + +#594 (tile data model D-record) ← standalone design track, no v0.1 dependency +``` + +Parallel server tracks: #587, #589, #591, #594 are all independent — start all simultaneously. #595 blocked on #587. + +--- + +## PR Workflow + +```bash +tea pr create --repo jpmschweitzer/settled-reach --login schweitz \ + --title "feat(server): character archetype in StartupMessage and monologue gating" \ + --description "body" --base main --head server +``` + +--- + +## Sprint Completion (Server Criteria) + +1. `cargo test` green on server branch. All existing tests pass. New tests for archetype round-trip and tell escalation pass. +2. `StartupMessage` with `character_archetype: Smuggler` produces a smuggler player entity with `MonologueState.character = "smuggler"` on tick 1. +3. After `TriangleActivated` fires, triangle NPCs have `RoutineDeviation` inserted and `tell_state: RoutineDeviation` appears in the snapshot. +4. Snapshot contains `current_ticker` (non-null headline) when player is in `"bar"` zone. +5. Opening monologue (enter_location) fires on tick 1 with lines from the correct character pool. +6. Integration test `test_v0_1_integration_playthrough` passes end-to-end. +7. `PROTOCOL_VERSION = 19` — matches client. diff --git a/project.yaml b/project.yaml index 2900d75fc..4f82ce093 100644 --- a/project.yaml +++ b/project.yaml @@ -1,5 +1,5 @@ name: The Settled Reach -version: 0.1.22 +version: 0.1.23 repository: settled-reach codename: commonwealth diff --git a/server/Cargo.toml b/server/Cargo.toml index 3306cc1a6..adcf0692e 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "settled-reach-server" -version = "0.1.22" +version = "0.1.23" edition = "2021" [dependencies] diff --git a/server/tests/gen_gauntlet_fixtures.rs b/server/tests/gen_gauntlet_fixtures.rs new file mode 100644 index 000000000..9daf1abe5 --- /dev/null +++ b/server/tests/gen_gauntlet_fixtures.rs @@ -0,0 +1,185 @@ +//! Generate snapshot fixtures from the Gauntlet test world (D-030 full pipeline). +//! +//! Runs the full Gauntlet simulation pipeline — same code path as --test-mode — +//! then serializes ObserverSnapshots to both MessagePack (wire format) and JSON +//! (human-readable debug) for client-side visual testing. +//! +//! The .msgpack files are the actual wire-format bytes the server sends over IPC. +//! Client visual tests load them via Protocol.decode_snapshot() → GameState.apply_snapshot(), +//! exercising the exact same pipeline as the live game. +//! +//! The .json files are for human inspection only. +//! +//! Run with: cargo test --test gen_gauntlet_fixtures -- --ignored +//! Or: make fixtures-gauntlet +//! +//! Output: tests/fixtures/gauntlet/*.{msgpack,json} (relative to repo root) + +use settled_reach_server::bridge::types::*; +use settled_reach_server::bridge::{BridgePlugin, SnapshotBuffer}; +use settled_reach_server::knowledge::KnowledgePlugin; +use settled_reach_server::npc::NpcPlugin; +use settled_reach_server::perception::vision_cone::Facing; +use settled_reach_server::simulation::movement::{PlayerCharacter, TilePosition}; +use settled_reach_server::simulation::rng::SimRng; +use settled_reach_server::simulation::SimulationPlugin; +use settled_reach_server::test_world; + +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use serde_json::Value; +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +const SEED: u64 = 42; +const FIXTURE_DIR: &str = "../tests/fixtures/gauntlet"; + +/// Build a deterministic Gauntlet simulation app. +/// Identical to what the server runs in --test-mode. +fn build_gauntlet(seed: u64) -> App { + let mut app = App::new(); + app.add_plugins(SimulationPlugin); + app.add_plugins(BridgePlugin); + app.add_plugins(KnowledgePlugin); + app.add_plugins(NpcPlugin); + app.insert_resource(SimRng::new(seed)); + + test_world::setup_gauntlet(&mut app); + + app +} + +/// Teleport the player to a specific position and facing. +/// Directly modifies ECS components — same effect as PlayerAction::Teleport +/// but without needing a target room action. +fn teleport_player(app: &mut App, pos: TilePosition, facing: Facing) { + let player = { + let mut query = app + .world_mut() + .query_filtered::>(); + query.single(app.world()).expect("player entity must exist") + }; + app.world_mut() + .entity_mut(player) + .insert((pos, facing)); +} + +/// Run N ticks, feeding inputs each tick, return the last snapshot. +fn run_ticks(app: &mut App, inputs: &[Vec]) -> ObserverSnapshot { + let mut last_snapshot: Option = None; + + for tick_inputs in inputs { + { + let mut queue = app + .world_mut() + .resource_mut::(); + for input in tick_inputs { + queue.push(input.clone()); + } + } + + app.update(); + + let buffer = app.world().resource::(); + if let Some(snapshot) = &buffer.snapshot { + last_snapshot = Some(snapshot.clone()); + } + } + + last_snapshot.expect("no snapshot produced") +} + +/// Recursively sort all object keys for deterministic JSON output. +fn sort_json_keys(value: &Value) -> Value { + match value { + Value::Object(map) => { + let sorted: BTreeMap = map + .iter() + .map(|(k, v)| (k.clone(), sort_json_keys(v))) + .collect(); + Value::Object(sorted.into_iter().collect()) + } + Value::Array(arr) => Value::Array(arr.iter().map(sort_json_keys).collect()), + other => other.clone(), + } +} + +fn write_fixture(name: &str, snapshot: &ObserverSnapshot) { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(FIXTURE_DIR); + fs::create_dir_all(&dir).expect("create fixture dir"); + + // MessagePack — exact wire-format bytes the server sends over IPC. + // Client visual tests load these via Protocol.decode_snapshot(). + let msgpack = rmp_serde::to_vec_named(snapshot).expect("serialize to MessagePack"); + let msgpack_path = dir.join(format!("{}.msgpack", name)); + fs::write(&msgpack_path, &msgpack).expect("write msgpack fixture"); + + // JSON — human-readable debug companion (not loaded by tests). + let value: Value = serde_json::to_value(snapshot).expect("serialize to JSON"); + let sorted = sort_json_keys(&value); + let json = serde_json::to_string_pretty(&sorted).expect("format JSON") + "\n"; + let json_path = dir.join(format!("{}.json", name)); + fs::write(&json_path, &json).expect("write json fixture"); + + eprintln!( + "Wrote {} ({} bytes msgpack, {} bytes json, {} visible_tiles, {} entities)", + name, + msgpack.len(), + json.len(), + snapshot.visible_tiles.len(), + snapshot.entities.len(), + ); +} + +#[test] +#[ignore] // Run manually: cargo test --test gen_gauntlet_fixtures -- --ignored +fn generate_gauntlet_snapshot_fixtures() { + // --- Hub (default spawn position) --- + // Player at (50, 58) facing North — tests fog rendering at the starting location. + // This is the exact state a new player sees on connect. + { + let mut app = build_gauntlet(SEED); + // Run 3 idle ticks to stabilize (cognitive delay, vision cone init) + let inputs: Vec> = vec![vec![], vec![], vec![]]; + let snapshot = run_ticks(&mut app, &inputs); + write_fixture("hub_spawn", &snapshot); + } + + // --- Fog Theater (observer position) --- + // Player at (56, 18) facing South — large open room with NPCs at varying distances. + // Tests visibility cone, fog layers, and distance-based fog rendering. + { + let mut app = build_gauntlet(SEED); + let fog_theater = test_world::constants::FOG_THEATER; + teleport_player(&mut app, fog_theater.observer, fog_theater.observer_facing); + let inputs: Vec> = vec![vec![], vec![], vec![]]; + let snapshot = run_ticks(&mut app, &inputs); + write_fixture("fog_theater", &snapshot); + } + + // --- Hub after movement (explored tiles + visible tiles differ) --- + // Player moves south from hub, creating a mix of explored-but-not-visible + // and currently-visible tiles — the exact fog boundary condition. + { + let mut app = build_gauntlet(SEED); + let inputs: Vec> = vec![ + vec![], + vec![PlayerInput { + tick: 1, + action: PlayerAction::MoveSouth, + }], + vec![PlayerInput { + tick: 2, + action: PlayerAction::MoveSouth, + }], + vec![PlayerInput { + tick: 3, + action: PlayerAction::MoveSouth, + }], + vec![], // idle — snapshot has explored + visible tiles that differ + ]; + let snapshot = run_ticks(&mut app, &inputs); + write_fixture("hub_after_movement", &snapshot); + } +} diff --git a/tests/fixtures/gauntlet-t164-snapshot.json b/tests/fixtures/gauntlet-t164-snapshot.json new file mode 100644 index 000000000..57a97cad8 --- /dev/null +++ b/tests/fixtures/gauntlet-t164-snapshot.json @@ -0,0 +1,2895 @@ +{ + "conversation_ended": [], + "conversation_events": [], + "current_dialogue": null, + "current_monologue": null, + "decode_errors": 0, + "dialogue_response": null, + "entities": [ + { + "entity_id": 0, + "kind": { + "data": null, + "variant": "Player" + }, + "observation": "Visible", + "relationship": "Known", + "visibility": "Forward", + "x": 16.5, + "y": 16.5, + "z": 0 + }, + { + "entity_id": 3, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 18.5, + "y": 14.5, + "z": 0 + }, + { + "entity_id": 4, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 5, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 6, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 7, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 8, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 9, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 10, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 11, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 12, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 13, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 14, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 15, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 16, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 17, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 18, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 19, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 20, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 21, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 22, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 23, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 24, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 25, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + }, + { + "entity_id": 26, + "kind": { + "data": null, + "variant": "Npc" + }, + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 0.5, + "y": 0.5, + "z": 0 + } + ], + "examine_result": null, + "game_time": { + "day": 0, + "day_phase": "Morning", + "tick_rate": "Paused", + "time_of_day": 16 + }, + "nearby_interactions": [ + { + "distance": 3, + "entity_id": 1, + "entity_type": "Npc", + "verbs": [ + { + "available": true, + "kind": "ExamineNpc", + "label": "Examine NPC", + "priority": 1 + } + ] + }, + { + "distance": 4, + "entity_id": 2, + "entity_type": "Npc", + "verbs": [ + { + "available": true, + "kind": "ExamineNpc", + "label": "Examine NPC", + "priority": 1 + } + ] + }, + { + "distance": 4, + "entity_id": 3, + "entity_type": "Npc", + "verbs": [ + { + "available": true, + "kind": "ExamineNpc", + "label": "Examine NPC", + "priority": 1 + } + ] + } + ], + "pending_recognitions": [], + "player_facing": "North", + "player_inventory": [], + "player_knowledge": null, + "player_stance": "Walk", + "poi_list": [], + "save_result": null, + "stationary_ticks": null, + "tick": 164, + "version": 17, + "visible_tiles": [ + { + "type": "wall", + "visibility": "Forward", + "x": -1, + "y": -1, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": -1, + "y": 0, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": -1, + "y": 1, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": -1, + "y": 2, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": -1, + "y": 3, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": -1, + "y": 4, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": -1, + "y": 5, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": -1, + "y": 6, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 0, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 0, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 0, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 0, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 0, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 0, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 0, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 0, + "y": 6, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 1, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 1, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 1, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 1, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 1, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 1, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 1, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 1, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 1, + "y": 7, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 2, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 2, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 2, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 2, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 2, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 2, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 2, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 2, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 2, + "y": 7, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 3, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 3, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 3, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 3, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 3, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 3, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 3, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 3, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 3, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 3, + "y": 8, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 4, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 4, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 4, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 4, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 4, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 4, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 4, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 4, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 4, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 4, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 4, + "y": 9, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 5, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 5, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 5, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 5, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 5, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 5, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 5, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 5, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 5, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 5, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 5, + "y": 9, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 6, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 6, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 6, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 6, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 6, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 6, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 6, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 6, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 6, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 6, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 6, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 6, + "y": 10, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 7, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 7, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 7, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 7, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 7, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 7, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 7, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 7, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 7, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 7, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 7, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 7, + "y": 10, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 8, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 8, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 8, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 8, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 8, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 8, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 8, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 8, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 8, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 8, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 8, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 8, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 8, + "y": 11, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 9, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 9, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 9, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 9, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 9, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 9, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 9, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 9, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 9, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 9, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 9, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 9, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 9, + "y": 11, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 10, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 10, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 10, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 10, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 10, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 10, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 10, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 10, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 10, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 10, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 10, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 10, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 10, + "y": 11, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 10, + "y": 12, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 11, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 11, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 12, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 11, + "y": 13, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 11, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 12, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 12, + "y": 13, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 13, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 13, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 13, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 13, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 13, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 13, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 13, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 13, + "y": 11, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 13, + "y": 12, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 13, + "y": 13, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 13, + "y": 14, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 14, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 14, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 14, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 14, + "y": 11, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 14, + "y": 12, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 14, + "y": 13, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 14, + "y": 14, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 15, + "y": 12, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 15, + "y": 13, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 15, + "y": 14, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 15, + "y": 15, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 16, + "y": 14, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 16, + "y": 15, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 16, + "y": 16, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 17, + "y": 12, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 17, + "y": 13, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 17, + "y": 14, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 17, + "y": 15, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 18, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 18, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 18, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 18, + "y": 11, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 18, + "y": 12, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 18, + "y": 13, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 18, + "y": 14, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 19, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 19, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 19, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 19, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 19, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 19, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 19, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 19, + "y": 11, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 19, + "y": 12, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 19, + "y": 13, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 19, + "y": 14, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 11, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 12, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 20, + "y": 13, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 21, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 11, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 12, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 21, + "y": 13, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 22, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 22, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 22, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 22, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 22, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 22, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 22, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 22, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 22, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 22, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 22, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 22, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 22, + "y": 11, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 22, + "y": 12, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 23, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 23, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 23, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 23, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 23, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 23, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 23, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 23, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 23, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 23, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 23, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 23, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 23, + "y": 11, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 24, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 24, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 24, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 24, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 24, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 24, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 24, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 24, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 24, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 24, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 24, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 24, + "y": 10, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 24, + "y": 11, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 25, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 25, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 25, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 25, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 25, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 25, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 25, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 25, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 25, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 25, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 25, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 25, + "y": 10, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 26, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 26, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 26, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 26, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 26, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 26, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 26, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 26, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 26, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 26, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 26, + "y": 9, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 26, + "y": 10, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 27, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 27, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 27, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 27, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 27, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 27, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 27, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 27, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 27, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 27, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 27, + "y": 9, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 28, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 28, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 28, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 28, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 28, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 28, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 28, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 28, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 28, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 28, + "y": 8, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 28, + "y": 9, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 29, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 29, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 29, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 29, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 29, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 29, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 29, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 29, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 29, + "y": 7, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 29, + "y": 8, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 30, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 30, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 30, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 30, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 30, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 30, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 30, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 30, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 30, + "y": 7, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 31, + "y": -1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 31, + "y": 0, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 31, + "y": 1, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 31, + "y": 2, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 31, + "y": 3, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 31, + "y": 4, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 31, + "y": 5, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 31, + "y": 6, + "z": 0 + }, + { + "type": "floor", + "visibility": "Forward", + "x": 31, + "y": 7, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 32, + "y": -1, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 32, + "y": 0, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 32, + "y": 1, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 32, + "y": 2, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 32, + "y": 3, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 32, + "y": 4, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 32, + "y": 5, + "z": 0 + }, + { + "type": "wall", + "visibility": "Forward", + "x": 32, + "y": 6, + "z": 0 + } + ], + "zone_id": null +} \ No newline at end of file diff --git a/tests/fixtures/gauntlet/fog_theater.json b/tests/fixtures/gauntlet/fog_theater.json new file mode 100644 index 000000000..89e2209bb --- /dev/null +++ b/tests/fixtures/gauntlet/fog_theater.json @@ -0,0 +1,3029 @@ +{ + "blocked_entities": [ + 1, + 2, + 3, + 4, + 6, + 7, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76 + ], + "character_pressure": { + "exposure": 0, + "institutional": 0, + "mood": null, + "relationship": 0, + "total": 0 + }, + "conversation_ended": [], + "conversation_events": [], + "current_monologue": { + "duration_seconds": 5.0, + "id": "recognition_02", + "text": "Those footsteps... I've heard that pattern before." + }, + "dialogue_response": null, + "entities": [ + { + "entity_id": 0, + "kind": "Player", + "observation": "Visible", + "relationship": "Known", + "visibility": "Forward", + "x": 56.5, + "y": 18.5, + "z": 0 + }, + { + "entity_id": 5, + "kind": "Npc", + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 56.5, + "y": 22.5, + "z": 0 + }, + { + "entity_id": 8, + "kind": "Npc", + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 68.5, + "y": 28.5, + "z": 0 + } + ], + "follow_state": null, + "game_time": { + "day": 0, + "day_phase": "Morning", + "tick_rate": "Full", + "time_of_day": 0 + }, + "nearby_interactions": [ + { + "contradicted": false, + "distance": 4, + "entity_id": 5, + "entity_type": "Npc", + "object_type": null, + "verbs": [ + { + "available": true, + "kind": "ExamineNpc", + "label": "Examine NPC", + "priority": 1 + } + ] + } + ], + "pending_recognitions": [ + { + "entity_id": 5, + "remaining_ticks": 4, + "total_delay_ticks": 6, + "x": 56.5, + "y": 22.5, + "z": 0 + }, + { + "entity_id": 8, + "remaining_ticks": 4, + "total_delay_ticks": 6, + "x": 68.5, + "y": 28.5, + "z": 0 + } + ], + "player_facing": "South", + "player_inventory": [], + "player_stance": "Walk", + "poi_list": [], + "rng_seed": 42, + "scan_events": [], + "sound_events": [], + "state_hash": 8686526522907766805, + "tick": 2, + "triangle_crisis_events": [], + "version": 17, + "visible_tiles": [ + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 36, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 36, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 36, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 37, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 37, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 37, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 37, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 38, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 38, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 38, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 38, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 39, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 39, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 39, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 39, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 39, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 40, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 40, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 40, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 40, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 40, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 41, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 41, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 41, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 41, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 41, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 41, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 42, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 43, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 44, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 45, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 46, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 47, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 48, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 49, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 22, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 50, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 21, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 22, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 51, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 21, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 22, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 52, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 20, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 21, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 22, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 53, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 20, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 21, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 22, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 54, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 19, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 20, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 21, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 22, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 55, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 18, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 19, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 20, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 21, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 22, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 56, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 19, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 20, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 21, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 22, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 57, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 20, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 21, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 22, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 58, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 20, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 21, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 22, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 59, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 60, + "y": 21, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 60, + "y": 22, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 60, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 60, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 60, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 60, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 60, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 60, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 60, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 60, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 60, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 60, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 61, + "y": 21, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 61, + "y": 22, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 61, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 61, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 61, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 61, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 61, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 61, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 61, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 61, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 61, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 61, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 62, + "y": 22, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 62, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 62, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 62, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 62, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 62, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 62, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 62, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 62, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 62, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 62, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 63, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 63, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 63, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 63, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 63, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 63, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 63, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 63, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 63, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 63, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 63, + "y": 33, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 63, + "y": 34, + "z": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 63, + "y": 35, + "z": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 64, + "y": 23, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 64, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 64, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 64, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 64, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 64, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 64, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 64, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 64, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 64, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 64, + "y": 33, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 64, + "y": 34, + "z": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 64, + "y": 35, + "z": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 65, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 65, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 65, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 65, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 65, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 65, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 65, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 65, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 65, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 65, + "y": 33, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 65, + "y": 34, + "z": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 65, + "y": 35, + "z": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 66, + "y": 24, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 66, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 66, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 66, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 66, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 66, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 66, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 66, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 66, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 66, + "y": 33, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 66, + "y": 34, + "z": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 66, + "y": 35, + "z": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 67, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 67, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 67, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 67, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 67, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 67, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 67, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 67, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 67, + "y": 33, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 67, + "y": 34, + "z": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 67, + "y": 35, + "z": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 68, + "y": 25, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 68, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 68, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 68, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 68, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 68, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 68, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 68, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 68, + "y": 33, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 68, + "y": 34, + "z": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 68, + "y": 35, + "z": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 69, + "y": 26, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 69, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 69, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 69, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 69, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 69, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 69, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 69, + "y": 33, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 69, + "y": 34, + "z": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 69, + "y": 35, + "z": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 70, + "y": 27, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 70, + "y": 28, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 70, + "y": 29, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 70, + "y": 30, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 70, + "y": 31, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 70, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 70, + "y": 33, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 70, + "y": 34, + "z": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 70, + "y": 35, + "z": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 71, + "y": 32, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 71, + "y": 33, + "z": 0, + "zone_id": 1 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 71, + "y": 34, + "z": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 71, + "y": 35, + "z": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 72, + "y": 33, + "z": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 72, + "y": 34, + "z": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 72, + "y": 35, + "z": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 73, + "y": 34, + "z": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 73, + "y": 35, + "z": 0 + } + ] +} diff --git a/tests/fixtures/gauntlet/fog_theater.msgpack b/tests/fixtures/gauntlet/fog_theater.msgpack new file mode 100644 index 000000000..1630a75e7 Binary files /dev/null and b/tests/fixtures/gauntlet/fog_theater.msgpack differ diff --git a/tests/fixtures/gauntlet/hub_after_movement.json b/tests/fixtures/gauntlet/hub_after_movement.json new file mode 100644 index 000000000..db60759b9 --- /dev/null +++ b/tests/fixtures/gauntlet/hub_after_movement.json @@ -0,0 +1,1079 @@ +{ + "blocked_entities": [ + 1, + 2, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76 + ], + "character_pressure": { + "exposure": 0, + "institutional": 0, + "mood": null, + "relationship": 0, + "total": 0 + }, + "conversation_ended": [], + "conversation_events": [], + "current_monologue": null, + "dialogue_response": null, + "entities": [ + { + "entity_id": 0, + "kind": "Player", + "observation": "Visible", + "relationship": "Known", + "visibility": "Forward", + "x": 50.5, + "y": 60.5, + "z": 0 + }, + { + "entity_id": 3, + "kind": "Object", + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 50.5, + "y": 68.5, + "z": 0 + } + ], + "follow_state": null, + "game_time": { + "day": 0, + "day_phase": "Morning", + "tick_rate": "Full", + "time_of_day": 0 + }, + "nearby_interactions": [], + "pending_recognitions": [ + { + "entity_id": 3, + "remaining_ticks": 3, + "total_delay_ticks": 6, + "x": 50.5, + "y": 68.5, + "z": 0 + } + ], + "player_facing": "South", + "player_inventory": [], + "player_stance": "Walk", + "poi_list": [], + "rng_seed": 42, + "scan_events": [], + "sound_events": [], + "state_hash": 15450425815579635895, + "tick": 4, + "triangle_crisis_events": [], + "version": 17, + "visible_tiles": [ + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 39, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 39, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 40, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 40, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 40, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 41, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 41, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 41, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 42, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 43, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 64, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 44, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 63, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 64, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 45, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 63, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 64, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 46, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 62, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 63, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 64, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 47, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 62, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 63, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 64, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 48, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 61, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 62, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 63, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 64, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 49, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 60, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 61, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 62, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 63, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 64, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 50, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 61, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 62, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 63, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 64, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 51, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 62, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 63, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 64, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 52, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 62, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 63, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 64, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 53, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 63, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 64, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 54, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 63, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 64, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 55, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 64, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 56, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 57, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 65, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 58, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 59, + "y": 68, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 60, + "y": 66, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 60, + "y": 67, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 60, + "y": 68, + "z": 0, + "zone_id": 0 + } + ] +} diff --git a/tests/fixtures/gauntlet/hub_after_movement.msgpack b/tests/fixtures/gauntlet/hub_after_movement.msgpack new file mode 100644 index 000000000..44294b991 Binary files /dev/null and b/tests/fixtures/gauntlet/hub_after_movement.msgpack differ diff --git a/tests/fixtures/gauntlet/hub_spawn.json b/tests/fixtures/gauntlet/hub_spawn.json new file mode 100644 index 000000000..dbae3424d --- /dev/null +++ b/tests/fixtures/gauntlet/hub_spawn.json @@ -0,0 +1,1607 @@ +{ + "blocked_entities": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76 + ], + "character_pressure": { + "exposure": 0, + "institutional": 0, + "mood": null, + "relationship": 0, + "total": 0 + }, + "conversation_ended": [], + "conversation_events": [], + "current_monologue": null, + "dialogue_response": null, + "entities": [ + { + "entity_id": 0, + "kind": "Player", + "observation": "Visible", + "relationship": "Known", + "visibility": "Forward", + "x": 50.5, + "y": 58.5, + "z": 0 + }, + { + "entity_id": 1, + "kind": "Object", + "observation": "Visible", + "relationship": "Unknown", + "visibility": "Forward", + "x": 50.5, + "y": 48.5, + "z": 0 + } + ], + "follow_state": null, + "game_time": { + "day": 0, + "day_phase": "Morning", + "tick_rate": "Full", + "time_of_day": 0 + }, + "nearby_interactions": [], + "pending_recognitions": [ + { + "entity_id": 1, + "remaining_ticks": 4, + "total_delay_ticks": 6, + "x": 50.5, + "y": 48.5, + "z": 0 + } + ], + "player_facing": "North", + "player_inventory": [], + "player_stance": "Walk", + "poi_list": [], + "rng_seed": 42, + "scan_events": [], + "sound_events": [], + "state_hash": 3925209675202732823, + "tick": 2, + "triangle_crisis_events": [], + "version": 17, + "visible_tiles": [ + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 39, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 39, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 39, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 39, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 39, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 40, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 40, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 40, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 40, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 40, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 40, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 41, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 41, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 41, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 41, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 41, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 41, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 42, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 42, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 43, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 43, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 44, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 44, + "y": 54, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 45, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 54, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 45, + "y": 55, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 46, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 54, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 46, + "y": 55, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 47, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 54, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 55, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 47, + "y": 56, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 48, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 54, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 55, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 48, + "y": 56, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 49, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 54, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 55, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 56, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 49, + "y": 57, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 50, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 54, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 55, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 56, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 57, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 50, + "y": 58, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 51, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 54, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 55, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 56, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 51, + "y": 57, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 52, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 54, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 55, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 52, + "y": 56, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 53, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 54, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 55, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 53, + "y": 56, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 54, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 54, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 54, + "y": 55, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 55, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 54, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 55, + "y": 55, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 56, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 56, + "y": 54, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 57, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 57, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 58, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 58, + "y": 53, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 59, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Floor", + "visibility": "Forward", + "x": 59, + "y": 52, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 60, + "y": 47, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 60, + "y": 48, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 60, + "y": 49, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 60, + "y": 50, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 60, + "y": 51, + "z": 0, + "zone_id": 0 + }, + { + "tile_kind": "Wall", + "visibility": "Forward", + "x": 60, + "y": 52, + "z": 0, + "zone_id": 0 + } + ] +} diff --git a/tests/fixtures/gauntlet/hub_spawn.msgpack b/tests/fixtures/gauntlet/hub_spawn.msgpack new file mode 100644 index 000000000..602ad74bf Binary files /dev/null and b/tests/fixtures/gauntlet/hub_spawn.msgpack differ diff --git a/tests/run-visual b/tests/run-visual index 24ee6fb32..74ed830a9 100755 --- a/tests/run-visual +++ b/tests/run-visual @@ -25,6 +25,14 @@ MODE="golden" # golden | screenshot | movie | update TARGET="" INTERVAL="" +# Server state for live scenarios +SERVER_BIN="" +SERVER_PID="" +SERVER_PORT="" + +# Cleanup server on exit +trap '[[ -n "${SERVER_PID:-}" ]] && kill "$SERVER_PID" 2>/dev/null; wait "$SERVER_PID" 2>/dev/null || true' EXIT + # -- Parse args ---------------------------------------------------------------- while [[ $# -gt 0 ]]; do @@ -102,12 +110,84 @@ xvfb_capture() { fi } +# -- Server lifecycle (live scenarios) ----------------------------------------- + +# Check if a scenario has "live": true in config +is_live_scenario() { + python3 -c " +import json, sys +c = json.load(open('${CONFIG}')) +s = c.get('scenarios', {}).get('${1}', {}) +sys.exit(0 if s.get('live') else 1) +" +} + +# Build server binary (once, cached) +ensure_server_built() { + if [[ -n "$SERVER_BIN" ]]; then return 0; fi + echo " Building server for live visual tests..." + (cd "$ROOT/server" && cargo build --bin settled-reach-server 2>&1) || { + echo "Error: server build failed" >&2 + return 1 + } + SERVER_BIN="$ROOT/server/target/debug/settled-reach-server" +} + +# Start server with --test-mode --port 0, parse LISTENING:{port} +start_server() { + local stdout_log + stdout_log=$(mktemp) + "$SERVER_BIN" --test-mode --port 0 >"$stdout_log" 2>/dev/null & + SERVER_PID=$! + + local attempts=0 + while [[ $attempts -lt 150 ]]; do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + echo " Error: server exited unexpectedly" >&2 + rm -f "$stdout_log" + SERVER_PID="" + return 1 + fi + if grep -q "^LISTENING:" "$stdout_log" 2>/dev/null; then + SERVER_PORT=$(sed -n 's/^LISTENING://p' "$stdout_log") + rm -f "$stdout_log" + echo " Server started: pid=$SERVER_PID port=$SERVER_PORT" + return 0 + fi + sleep 0.1 + attempts=$((attempts + 1)) + done + + echo " Error: no LISTENING signal after 15s" >&2 + kill "$SERVER_PID" 2>/dev/null || true + rm -f "$stdout_log" + SERVER_PID="" + return 1 +} + +# Stop server (called after each live capture; server may have exited on disconnect) +stop_server() { + if [[ -n "$SERVER_PID" ]]; then + kill "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" 2>/dev/null || true + SERVER_PID="" + SERVER_PORT="" + fi +} + # -- Screenshot mode ----------------------------------------------------------- if [[ "$MODE" == "screenshot" ]]; then mkdir -p "$CACHE_DIR" echo "Capturing scenario: $TARGET" + if is_live_scenario "$TARGET"; then + ensure_server_built || exit 2 + start_server || exit 2 + export SR_LIVE=1 SR_PORT="$SERVER_PORT" + fi godot_capture --scenario "$TARGET" "$CACHE_DIR" + stop_server + unset SR_LIVE SR_PORT 2>/dev/null || true PNG="$CACHE_DIR/$TARGET.png" if [[ -f "$PNG" ]]; then echo "Screenshot: $PNG ($(stat -c%s "$PNG" 2>/dev/null || stat -f%z "$PNG") bytes)" @@ -162,12 +242,27 @@ for scenario in "${SCENARIOS[@]}"; do TOTAL=$((TOTAL + 1)) echo "--- $scenario ---" + # Start server for live scenarios + IS_LIVE=false + if is_live_scenario "$scenario"; then + IS_LIVE=true + ensure_server_built || { FAILED=$((FAILED + 1)); continue; } + start_server || { FAILED=$((FAILED + 1)); continue; } + export SR_LIVE=1 SR_PORT="$SERVER_PORT" + fi + # Capture set +e CAPTURE_OUT=$(xvfb_capture --scenario "$scenario" "$CACHE_DIR" 2>&1) CAPTURE_RC=$? set -e + # Stop server after capture (server exits on client disconnect anyway) + if [[ "$IS_LIVE" == "true" ]]; then + unset SR_LIVE SR_PORT 2>/dev/null || true + stop_server + fi + CAPTURED="$CACHE_DIR/$scenario.png" if [[ $CAPTURE_RC -ne 0 ]] || [[ ! -f "$CAPTURED" ]]; then diff --git a/tests/visual.json b/tests/visual.json index be5f52007..4d7e92772 100644 --- a/tests/visual.json +++ b/tests/visual.json @@ -2,6 +2,7 @@ "resolution": [960, 540], "settle_frames": 30, "tolerance": 5, + "max_diff_pct": 0.5, "golden_dir": "client/tests/golden/visual", "scenarios": { "fog_3state": { @@ -47,6 +48,26 @@ "cursor_menu": { "ticks": 3, "description": "Cursor rendering over dialogue option" + }, + "fog_live_replay": { + "ticks": 1, + "description": "Replay real server hub snapshot — full pipeline fog test", + "replay_snapshot": "tests/fixtures/gauntlet/hub_spawn.msgpack" + }, + "fog_theater_replay": { + "ticks": 1, + "description": "Replay Fog Theater — large room with NPCs at varying distances", + "replay_snapshot": "tests/fixtures/gauntlet/fog_theater.msgpack" + }, + "fog_boundary_replay": { + "ticks": 1, + "description": "Replay hub after movement — explored/visible tile boundary", + "replay_snapshot": "tests/fixtures/gauntlet/hub_after_movement.msgpack" + }, + "fog_live_hub": { + "ticks": 10, + "live": true, + "description": "Live server: Hub spawn fog — real pipeline regression test" } }, "flows": { diff --git a/tooling/visual-diff b/tooling/visual-diff index c1a9c7fa0..8b1d913a1 100755 --- a/tooling/visual-diff +++ b/tooling/visual-diff @@ -239,18 +239,22 @@ def compare( # --------------------------------------------------------------------------- -def load_tolerance(config_path: Path | None) -> int: - """Read tolerance from config JSON, return DEFAULT_TOLERANCE on failure.""" +def load_config(config_path: Path | None) -> dict: + """Read visual test config, return dict with tolerance and max_diff_pct.""" if config_path is None: config_path = DEFAULT_CONFIG + defaults = {"tolerance": DEFAULT_TOLERANCE, "max_diff_pct": 0.0} if not config_path.exists(): - return DEFAULT_TOLERANCE + return defaults try: with open(config_path) as f: data = json.load(f) - return int(data.get("tolerance", DEFAULT_TOLERANCE)) + return { + "tolerance": int(data.get("tolerance", DEFAULT_TOLERANCE)), + "max_diff_pct": float(data.get("max_diff_pct", 0.0)), + } except (json.JSONDecodeError, ValueError, OSError): - return DEFAULT_TOLERANCE + return defaults # --------------------------------------------------------------------------- @@ -270,6 +274,12 @@ def main() -> int: default=None, help="Per-channel pixel tolerance (default: from config or 5)", ) + parser.add_argument( + "--max-diff-pct", + type=float, + default=None, + help="Max allowed diff percentage (default: from config or 0.0)", + ) parser.add_argument( "--diff-output", default=None, @@ -282,9 +292,11 @@ def main() -> int: ) args = parser.parse_args() - # Resolve tolerance: CLI > config > fallback + # Resolve settings: CLI > config > fallback config_path = Path(args.config) if args.config else None - tolerance = args.tolerance if args.tolerance is not None else load_tolerance(config_path) + cfg = load_config(config_path) + tolerance = args.tolerance if args.tolerance is not None else cfg["tolerance"] + max_diff_pct = args.max_diff_pct if args.max_diff_pct is not None else cfg["max_diff_pct"] # Read images try: @@ -322,6 +334,11 @@ def main() -> int: return 0 pct = diff_count / total * 100 + + if pct <= max_diff_pct: + print(f"PASS: {diff_count} of {total} pixels differ ({pct:.1f}%, within {max_diff_pct}% threshold)") + return 0 + print(f"FAIL: {diff_count} of {total} pixels differ ({pct:.1f}%)") if args.diff_output and diff_buf: