From 610dd78ff1c5b248b379855b374bfd733bfca1e8 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 4 Mar 2026 22:47:43 +0100 Subject: [PATCH 01/10] feat(assets): author tile maps for Sova Transit District (#582, #583) Complete tile data for all five locations in the transit district: - The Terminal (44x28, z=1): logistics hub with supervisor office LOS window - The Last Shift (34x22, z=1): bar with corner booth and back room alley exit - Maintenance Corridors (44x6, z=0): restricted storage, hatch room, transition corridor - Gate Ground (40x32, z=1): aperture chamber, staging, customs lanes, concourse - Gate Gallery (32x10, z=2): Commission-only observation gallery Gate corridor split into two files (gate-ground, gate-gallery) because the Location struct supports only one tile_bounds per file. Updated district.yaml locations list with gate-ground and gate-gallery. Co-Authored-By: Claude Opus 4.6 --- .../sova/districts/transit/district.yaml | 2 + .../transit/locations/gate-gallery.yaml | 48 +++++++++++ .../transit/locations/gate-ground.yaml | 79 +++++++++++++++++++ .../locations/maintenance-corridors.yaml | 52 +++++++++++- .../transit/locations/the-last-shift.yaml | 65 ++++++++++++++- .../transit/locations/the-terminal.yaml | 77 +++++++++++++++++- 6 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/gate-gallery.yaml create mode 100644 content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/gate-ground.yaml 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..8b86aa56d 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 @@ -10,4 +10,6 @@ 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..6867c2dbb --- /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 +# 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: + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + - "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..f2abf6b1c --- /dev/null +++ b/content/campaigns/main/systems/krenn/stations/sova/districts/transit/locations/gate-ground.yaml @@ -0,0 +1,79 @@ +# 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: 31 + z: 1 + +# Legend: +# F = Floor (walkable) W = Wall (solid, blocks LOS) +# R = Restricted (access-tier gated) +# +# Layout (40 wide x 32 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-22: Freight customs (west, 20 tiles, lane walls) | ped customs (east, 17 tiles, lanes) +# Row 23: Wall with wide opening to concourse (cols 5-34 open) +# Rows 24-31: Gate concourse (full 38-tile width, 8 rows — public open space) +# +# Corridor width: gate concourse 8vt (D-093) +# Customs lanes: freight 2vt/lane with wall stubs, pedestrian 1vt/lane + +tiles: + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + - "WWWWWWWWWWWWWWWWRRRRRRRRWWWWWWWWWWWWWWWW" + - "WWWWWWWWWWWWWWWWRRRRRRRRWWWWWWWWWWWWWWWW" + - "WWWWWWWWWWWWWWWWRRRRRRRRWWWWWWWWWWWWWWWW" + - "WWWWWWWWWWWWWWWWRRRRRRRRWWWWWWWWWWWWWWWW" + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" + - "WWWWWWWWWWWWFWWWWWWWWWWWWWWWWWWWFWWWWWWW" + - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" + - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" + - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" + - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" + - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" + - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" + - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" + - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" + - "WWWWWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWWWWW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + +sightlines: + open: false + notes: > + Gate concourse (rows 24-31) is wide open with full sightlines. + Customs lanes have wall stubs creating partial occlusion between + lanes. Aperture chamber is fully walled and restricted. Cross-z + LOS from the observation gallery (z=2) into the concourse is + handled server-side; the gallery rail is a transparent low wall. 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..d8dcd4faa 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,52 @@ # 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: 43 + y_max: 5 + z: 0 + +# Legend: +# F = Floor (walkable) W = Wall (solid, blocks LOS) +# R = Restricted (access-tier gated) +# +# Layout (44 wide x 6 tall): +# Row 0: North wall +# Row 1: Restricted storage (west, 8 tiles) | wall | corridor (26) | wall | hatch room (east, 6) +# Row 2: Doors connect rooms (F in wall positions at cols 9 and 36) +# Row 3-4: Same as row 1 (walls between rooms) +# Row 5: South wall +# +# Corridor width: 2vt visible at rows 1-4 (internal height, standard maintenance) +# Transition corridor: 26 tiles (~26m at 1m/vt) between storage and hatch +# Restricted storage: R tiles for access restriction +# Maintenance hatch: F tiles (cross-z connection is future movement feature) + +tiles: + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + - "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WRRRRRRRRFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + +sightlines: + open: false + notes: > + Long straight corridor with limited concealment. Storage and hatch + rooms are separated by walls with single-tile doors. 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..8178c71e7 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,65 @@ # 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) +# V = Void (outside building) R = Restricted (access-tier gated) +# +# Layout (34 wide x 22 tall): +# Row 0: North exterior wall +# Rows 1-4: Main floor + corner booth (NE, cols 26-31, walled alcove) +# Rows 5-16: Open main floor (bar counter is furniture on F tiles) +# Row 17: Back room north wall with door (col 21 = 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" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "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 + gives staff full sightlines across the main floor. Back room is + partially occluded by wall at col 21. + +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..04a894690 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 22 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 22 (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" From 1db3677b3006fc57a5cfb9fe7ba63b209b5c17f4 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 4 Mar 2026 23:01:13 +0100 Subject: [PATCH 02/10] chore(meta): reframe worktree boundaries as team identity Replace worktree-centric language with team-centric framing across CLAUDE.md and skills. Agents now identify by $WORKTREE_TEAM env var instead of resolving git internals. This prevents agents from following .git pointers back to the main repo and crossing boundaries. - CLAUDE.md: rename section to "Team boundaries", reference $WORKTREE_TEAM - sprint-start: add TEAM BOUNDARY rule to agent spawn prompt - sprint-plan: replace "worktree-relative paths" with "relative paths only" - pr-review: replace "worktree" with "team directory", note cross-dir reading is a main-team privilege only Co-Authored-By: Claude Opus 4.6 --- .claude/skills/pr-review/SKILL.md | 18 +++---- .../pr-review/references/reviewer-profiles.md | 48 ++++++++++--------- .claude/skills/sprint-plan/SKILL.md | 11 ++--- .claude/skills/sprint-start/SKILL.md | 4 ++ CLAUDE.md | 13 ++--- 5 files changed, 51 insertions(+), 43 deletions(-) diff --git a/.claude/skills/pr-review/SKILL.md b/.claude/skills/pr-review/SKILL.md index c69a5c049..c0b9d9d74 100644 --- a/.claude/skills/pr-review/SKILL.md +++ b/.claude/skills/pr-review/SKILL.md @@ -63,35 +63,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`, @@ -99,8 +99,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 ec0618a48..891e14358 100644 --- a/.claude/skills/sprint-plan/SKILL.md +++ b/.claude/skills/sprint-plan/SKILL.md @@ -124,13 +124,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 541ccd0ec..fe613cc54 100644 --- a/.claude/skills/sprint-start/SKILL.md +++ b/.claude/skills/sprint-start/SKILL.md @@ -289,6 +289,10 @@ Task( Branch: `{team}` RULES: + - 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. - 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. diff --git a/CLAUDE.md b/CLAUDE.md index ae65dd053..7d92e3df6 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. ### Database From 749e16f379585fc1261834ce2d79561ef9d51d00 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 4 Mar 2026 23:01:26 +0100 Subject: [PATCH 03/10] chore(meta): update changelog Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bc06c37f..0d899f998 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). - Entity renderer migrated from ColorRect placeholders to Sprite2D with D-019 angle sprites — self_modulate for D-033 tinting, 8→4 octant direction mapping, feet-anchored y-sort (#540) ### 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 From 895a77ae68690d71db9143f1da4a00a21f53a877 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 4 Mar 2026 23:10:48 +0100 Subject: [PATCH 04/10] =?UTF-8?q?fix(assets):=20address=20PR=20#83=20revie?= =?UTF-8?q?w=20=E2=80=94=20tile=20map=20corrections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fixes: - Terminal: fix sightlines comment col 22 → col 28 (actual window position) - Last Shift: fix door comment col 21 → col 22, add spatial features (bar counter W-stubs cols 1-2, table clusters, card table, ticker mount) - Maintenance corridors: extend transition corridor from 26 to 40 tiles per D-093 spec (grid now 58x6) - Gate ground: expand customs zone from 8 to 10 rows per D-093 spec, correct freight (20 tiles west) / ped (10 tiles east) layout with 6-tile corridor between (grid now 40x34) - Gate gallery: add staircase entrance tile (col 1 row 0 = R) - District description: update from "Three social sites" to five locations Co-Authored-By: Claude Opus 4.6 --- .../sova/districts/transit/district.yaml | 5 ++- .../transit/locations/gate-gallery.yaml | 4 +- .../transit/locations/gate-ground.yaml | 40 ++++++++++--------- .../locations/maintenance-corridors.yaml | 32 ++++++++------- .../transit/locations/the-last-shift.yaml | 34 +++++++++------- .../transit/locations/the-terminal.yaml | 4 +- 6 files changed, 66 insertions(+), 53 deletions(-) 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 8b86aa56d..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,8 +4,9 @@ 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" 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 index 6867c2dbb..7c284bf5a 100644 --- 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 @@ -23,13 +23,13 @@ tile_bounds: # W = Wall (solid) # # Layout (32 wide x 10 tall): -# Row 0: North wall +# 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: - - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + - "WRWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" - "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW" - "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW" - "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW" 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 index f2abf6b1c..725c1fd43 100644 --- 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 @@ -15,25 +15,26 @@ tile_bounds: x_min: 0 y_min: 0 x_max: 39 - y_max: 31 + y_max: 33 z: 1 # Legend: # F = Floor (walkable) W = Wall (solid, blocks LOS) # R = Restricted (access-tier gated) # -# Layout (40 wide x 32 tall): +# 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-22: Freight customs (west, 20 tiles, lane walls) | ped customs (east, 17 tiles, lanes) -# Row 23: Wall with wide opening to concourse (cols 5-34 open) -# Rows 24-31: Gate concourse (full 38-tile width, 8 rows — public open space) +# 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) -# Customs lanes: freight 2vt/lane with wall stubs, pedestrian 1vt/lane tiles: - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" @@ -51,14 +52,16 @@ tiles: - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" - "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW" - "WWWWWWWWWWWWFWWWWWWWWWWWWWWWWWWWFWWWWWWW" - - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" - - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" - - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" - - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" - - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" - - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" - - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" - - "WFFFFFWFFFFFWFFFFFFFFWFFFFWFFFFWFFFFFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" + - "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW" - "WWWWWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWWWWW" - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" @@ -72,8 +75,9 @@ tiles: sightlines: open: false notes: > - Gate concourse (rows 24-31) is wide open with full sightlines. + Gate concourse (rows 26-33) is wide open with full sightlines. Customs lanes have wall stubs creating partial occlusion between - lanes. Aperture chamber is fully walled and restricted. Cross-z - LOS from the observation gallery (z=2) into the concourse is - handled server-side; the gallery rail is a transparent low wall. + 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 d8dcd4faa..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 @@ -16,7 +16,7 @@ description: > tile_bounds: x_min: 0 y_min: 0 - x_max: 43 + x_max: 57 y_max: 5 z: 0 @@ -24,29 +24,31 @@ tile_bounds: # F = Floor (walkable) W = Wall (solid, blocks LOS) # R = Restricted (access-tier gated) # -# Layout (44 wide x 6 tall): +# Layout (58 wide x 6 tall): # Row 0: North wall -# Row 1: Restricted storage (west, 8 tiles) | wall | corridor (26) | wall | hatch room (east, 6) -# Row 2: Doors connect rooms (F in wall positions at cols 9 and 36) +# 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, standard maintenance) -# Transition corridor: 26 tiles (~26m at 1m/vt) between storage and hatch +# 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: - - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" - - "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" - - "WRRRRRRRRFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" - - "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" - - "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" - - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" + - "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WRRRRRRRRFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" sightlines: open: false notes: > - Long straight corridor with limited concealment. Storage and hatch - rooms are separated by walls with single-tile doors. Mundane - appearance per D-093 G-08 — no visually suspicious tile arrangements. + 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 8178c71e7..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 @@ -20,13 +20,17 @@ tile_bounds: # Legend: # F = Floor (walkable) W = Wall (solid, blocks LOS) -# V = Void (outside building) R = Restricted (access-tier gated) # # Layout (34 wide x 22 tall): # Row 0: North exterior wall # Rows 1-4: Main floor + corner booth (NE, cols 26-31, walled alcove) -# Rows 5-16: Open main floor (bar counter is furniture on F tiles) -# Row 17: Back room north wall with door (col 21 = F) +# 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) @@ -36,17 +40,17 @@ tiles: - "WFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" - "WFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" - "WFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW" + - "WWWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WWWFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFW" + - "WWWFFFFFFFWFFFFFFFWFFFFFFWFFFFFFFW" + - "WWWFFFFFFFWFFFFFFFWFFFFFFFFFFFFFFW" + - "WWWFFFFFFFFFFFWFFFFFFFFFFFFFFFFFFW" + - "WWWFFFFFFFFFFFWFFFFFFFFFFFFFFFFFFW" - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFWFFFFFFFWFFFFFFFWFFFFFFFFW" + - "WFFFFFFFWFFFFFFFWFFFFFFFWFFFFFFFFW" - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" - - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" - - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" - - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" - - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" - - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" - - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" - - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" - - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" - - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" + - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWFW" - "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW" - "WFFFFFFFFFFFFFFFFFFFFWFWFFFFFFFFFW" - "WFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFW" @@ -59,7 +63,9 @@ sightlines: 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 - gives staff full sightlines across the main floor. Back room is - partially occluded by wall at col 21. + (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 04a894690..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 @@ -27,7 +27,7 @@ tile_bounds: # 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 22 is F (LOS gap into office) ** +# 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) @@ -70,7 +70,7 @@ tiles: sightlines: open: false notes: > - Supervisor office window faces main corridor at row 7, col 22 (F gap in + 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. From 1037ea2bf7f10abdda1c4f92eb7dd903ced4c509 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 4 Mar 2026 23:16:22 +0100 Subject: [PATCH 05/10] docs(meta): add testing preferences to CLAUDE.md Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 85ad78625..de4e61a1d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,6 +61,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) From f7852b93ace4022ac114e46202bbe3dac4bf790e Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 4 Mar 2026 23:27:06 +0100 Subject: [PATCH 06/10] chore(meta): release v0.1.23 Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 3 +++ project.yaml | 2 +- server/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb6ddffc9..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) 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] From a75d9f6c08dedbd439c454d96e1a7c648c746aef Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 4 Mar 2026 23:42:30 +0100 Subject: [PATCH 07/10] chore(meta): plan Sprint 24: Signal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 10 tickets across server (6), client (3), copy (1). Capstone sprint for v0.1 — everything converges on a full playthrough from main menu through storyteller activation. Closed stale epics: #38, #369, #455, #575, #596. Sprint goal: wire TriangleActivated into player-visible signal, thread character archetype through session lifecycle. Co-Authored-By: Claude Opus 4.6 --- docs/backups/settledreach.db.backup | Bin 598016 -> 598016 bytes docs/sprints/sprint-24/client.md | 149 +++++++++++++++++++++ docs/sprints/sprint-24/copy.md | 147 ++++++++++++++++++++ docs/sprints/sprint-24/joint.md | 120 +++++++++++++++++ docs/sprints/sprint-24/server.md | 199 ++++++++++++++++++++++++++++ 5 files changed, 615 insertions(+) create mode 100644 docs/sprints/sprint-24/client.md create mode 100644 docs/sprints/sprint-24/copy.md create mode 100644 docs/sprints/sprint-24/joint.md create mode 100644 docs/sprints/sprint-24/server.md diff --git a/docs/backups/settledreach.db.backup b/docs/backups/settledreach.db.backup index 69b281ff55e7d79c6b3bdf6d686130ae41dc7251..e47628a3e85c2e8f925474a603e071a518a6a3d0 100644 GIT binary patch delta 6371 zcmb7IYj7OZmG17_-P5CKX=Ln)Bn#tf1V+MojAq7`HGWC9jBUN;k!+HsY}%vtOyhaE zyGKS)>`3w?;$c$8#xzM$#CE`yN`M%=aTAtWw3`HqB;F*o`GG4HAnb!ATe~44g<;n$ zd+zO-(a0zXF}VAi+xPL^bI(2BIi8xkX=?7K^S^9kYfrw}wu&8l!OvHtZod11tLrd# z9sdYF%iqTvyvFb6$M`;IU*fmfoNHz);yA94i@0XG2^!-@Tr*t+jS{qlpa?;m9W?5i z*+kGVY1>H95I5|a*+9@BX$uoHKv1WH23<4j3F;?p9R&3ew2mOu$Ms+MUQf3NbtC42 zYxpj%E{`S=gHkApZbt)XE9yY2P&4`*@|;}apU744FpAJ+w>l8`vfT~@!fclVfib(K z2uIn?4g}8ZCIwU}~6Pkg=}Ru#Z~@>e*Yp!4`iA2Lj<>Al%;0Fte}Mvi9q>+=h42 z4ahp>Likh@dai~=KNn)|3+_KwRLPH5V)PH_dGr)~zlzpz=tp*dMK`h)yu0|HHO!%3 zpTz$C)_R2G&0{jE=ME^*Ly2^ZiOm{M1ig79+Ju6rsp{8NZ&ppALGL%bPkJBqK3H|N>Kj#0dZ()X zP&HvCu0!7qq8$wHMm~;E<~Eklh7t-_ZDw4sUa6tWzI`eBj|#r(1!g^XO~|@4&hZ=R z_|a0QEk(q>Rzio1h}%&@w|PaT8D?puvoz9KHkZC`DxqB^wB7pxa}V)WIwC+v>@0ol zDWPp8w6%n~y}x6EFt&}3wSTh`)n^b!zpz}#QH(>suv5p;W!Bzx0?vwd5!H+q<$CRZ zKZ#b#tltt&qxYA|&&X%w$K;3PDLE_OC6CFy@>=`D)94ELj9<__uX!$dp7EUZSe}PHM?I6Cv~a(0pJ$(E z)YI$P;t6@0gqU!LFd~G7Hkjh-8Hk=1zDbmFo!l5<+0)_=c5>7X4vYwh;ke!ONw@Db z)9pKuZr_1)`v3zWj&7g84vuc$fqitIUi#ccpF8Puhc)*hKWKmSH1A!`xj(Qg=J?m$ zeAN$_1~|8s@=WOH4q~36qc{7kpZzocp~bFeeb#SVgzAMcC#u=4KjJOKGYq@o!V-T6 zd-10{U(MEA!rS~Bv2vMwRsJ{2|299v%NOO-_6u+G18ik;C9ag@_vAO^Z_0Dl|NV-; z)$V$S|D~Ikf6N>O0y*k~xoA2Ae&a!PoT2Iys?O~uK15qb%UUCqpUlUGvi1>zqX|t- zncQ$$`%qc?U|IV>S$jWVUvb&KvQEAB`v2m8%%QbX!g9UGf5$ra2=8_Mt)xo{>2CYw z_xLlcZTx}%NU(6Pz;EV8skxm)9iw=JK8NXZh&~7DbAUek>9cTb_H!Tddt9jAmHUoM zSj(|4dr1bRxC_|-b53#*rR(K(+p{j=zMW`TTy49Cg|lpXTs$v+Q+!H%QhZ!IAs!RI zBu^g6U3G4ZDC4ynB^-bmW z+B|cj8`k>0&@j_b>t@=HY7Da?7|G~b%EWqm$8u+KiRdI38CM8e9>lpQalo&Zh1%qPmtb)p4w*##_vE z3%u}zzqOsRDbD=P)T8*Io=)PVqNQ+BO=U6gHFZr%#egXg(k2uWmTZMRq*7Wc7Vrnw zLD+)-N$wF^9}afdZCAuu-dE`aa1vnI8aLBEGr4NjO6!gP75`;vX;3JdPG%A+(3*Yz zs%Srd)iq{_?=xtSc^|I+yOsI9`EWJFp6NBS9-sYjMylg@B6ijaLt4#4rW3Mv7}874 zOVFK+z`lW`dxgvJidVb_r}HXXd6ds@s%Dll(@RNgzx}k-!#d-?enx7znWjsDb>a4q z6B&T2L`dh8`OS1x-RywR{>rnGzp(00zb)YvrBNNus4w1--{^3>-{-Uc^^CNy(0k`o zQe!RQ*zSa^{LcIa+OhLX`+fHPPe~Ppj$b(^tzKEy@xefT3thkd#5P|Y5DErwvDzx! zwe|<+q)WWBgCBlNx^eYix>&{rPQl{v}`Y`fT^lq$;U2?sHb?vUI)Y*8EnYB)P|7|9^8?N){)6OKK;4 z$zRrWo^hQA#^+pQ#vVFjC7JQ1cO|(n&S|mp7{;y>bB&_mpBxNYn? z=09C7rWHDr{!Dk~$H)mzD@?tQsbPabAI-5wKYfugx$;Ct!&u3h@wASE?@XOsi!*vU zcej?*%t@R~r_zw`XH|$ZDb>ISp;K|DZYLr@SZU0Z$Ev16kr3Bnahjgg&fj@jq=dr$ zz`~{wsapt>>5hDaun$jvzDU20(jUsGB;=G4g2)t$!MbD z(l8a>#0L}us6xan1=YXA6Y8nXMR+=kP1ogjgTrvstC)JYr(owgn$`A>Y7i}Rj9Q@z_%Q;r;%WGr+7dzzYR6bL5OaV?v?){4Mx z?Vn^9nZ7YULfxR9sbAsHqjvmM*oW292g3_Beej~E!HJ5)GM=Vpd6=vF!bju$I_+&Px(!D>PUbs~C-M?nqy z6%e^dTsvsudq5r**lbbv1H<_jq3A3M6o>vC#axkN(d80pQ0xNt&vy^>H&Z9X(X6f$ zl5r4pa&-t>Bc)`Fc-s6d6a(uPp$Kc{K-{Ny<_9Uqdl%l3;>9e}YMNkVlh@c4vW%!% zuI~HcI1SC%PZJfRoGWge4*}7*mdvSm4bJK@vbQvFfpq3C-uFN_u!v9i3kGk^575o* z_)MG08KvD6Pk>x{d_avW6IxnV64+EBJyH#DPMWTeD_^usDP$Fnpb(-0p-;Opn7^3b z(8y0h6o_Pw6d=>LnlTGuGe?q9@Z|`JsSzg!ZN^4MIg$cj z1D^-S8s4^m^5TpaiCu*pWO{jilrmn!_&~uR19B9MNwig7|5Ih9yY$JP9il(l}V!&BBN&DDCFsBldP$$2D!0h z)|Ld^@S`N&S4o+pKpjMC9~GE`V@)U| zQ6aM$=`7SK^J)lvbS+0Up9K2iCTZ*iH88M(d$vb*?HI&Fh+A=wQwq`f!??W(N3w8v zC-IRu*tE^W|!IU|w2t zW^?phfhxyT+jKPsX4NN~G01z1^U0C=Nk~p&uwz;q78h*B+qXqp4094v0@|7QbvlK@*HR;wd=twaSU?eQFS~^LOI9`_y{dJ3n9hQjt<8 delta 2169 zcmZvcYitx%6vyYzxifQTXSTbg_Bp~93gvD0L0^kd3N0-Z1VkkP1Zu6dT1#IL3o2zh zP!VeS2)z{vmSQ6|fJn7hP(zIv1SE>j)I>oP{UAzF5>YXFXBWGRad$u5|LnQ{^E>yP zI}HP48wSQ6{wfW{w||{B8qGW6#!kq>&-W>_s>Qx&cZxVc31ovPiiP4lC6HyUa>W8A zkQrLdRRS5-Do@Psdp9pvgDfcNQx-oWD)mqaCX}>CyMJt3Dv#ycQML{9P-{QR`gb`O zWrmiPL-W!NZy}WtCpxb<*Q7=Xu_z~ijFCdT;PcVz@u-u2hEWPVoQUkSHv!q2P9-3A zDnXq71B`>LoeCtQSvynVm2puvq+V0|^iHV^K82HT3=TpUw8K`YhYDB?cSAmpzYG~{ zEP^qJ-Xp?yn{+V52Z~D`tyo(o&WjXcr%!w4UBSmglOezy+g;klD z6Dej!iW%%1fHs8gWOWi0VdSR+D&&ahBt#!gQF|^tR@hsgA zny71Fpqq83(ghB91G%W=gm>usPUxl;(eQPu<4WkuxAKn@QpEDg)m1fX%W6t%YlR~> zAJB>sa1=+)6nwnRX|%xwDl3kGuWZ_* z%;v@`ksc@wR6B%NVOirwgRS%6@kqH@9_{ktQMz}kCp+2W^}F4EA3N>E{>Na3^c+*V za2cX?dvVn$?WESP9niWpsx@nkTD`VbTczEj&DV0YY1%|~TQ|QI?aA(Dnmk{^^vHF* zlKtL;9Z6!;d(1X~PpKB;3eLC;u{+LS3VI;3vA|+l)Hu$d;%R)H<}82&>Nta)bl?=0 z=~J;1LFN4)VeCW>DqX;nX~qQ{U~gT(3lWXKh$mA0TU^CDFXAmKw*QR`ZXb_W)=&5Y z5hjsCR2jmrvyX@HUKHCz8c02ApaNp$t$wg$_pdbqZ+w_c-DXMyCptwH1 zL&^sD3!Dd-aBLZ>>Bez+J*n zd$jvIySd!-J9&RLPj1qT{%Vpa4$sWUT9S%A)BGM*P)m+YiPk}ogdzcNM4t;q_8szR zy5tM+>xwzUDZ~hyW2~vWw3pEu+mlLm8irHQ4t( zWK}q2;eIk9AsAa*TC={i#w;?Y2V=9ht~S`y`-v?a+tEu>BV(UQH*E#wzyA907Xz1%~RZ~t#8 zcHTu1_X--pz1(6C)^{v7a~=FO{Upsc*rB5&NgigXK1$;0x5r76eOsM5D_E7%T4Jzc zuaUkmDUV9kB2J)xfXGhyhM+DIZt#W;*;^KwbAp?rUvL;qKS!cSWQsUCZIIlqO*Lm* zw{BA>KeiQvMhw|^jd*!k=gG9fMkD_A+o z5|m6tb4NiszYEv(?HkRxR@SnP7=tMWHFOHZvQlo*X_-i>h|&9@La4fuU%JM z(VB1O2h+Q{$_>iXC$jsmlJZ*^62^No|HmNI$RLyWmmvOSv)&O`Mf **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. From 2189b00c6ff9d6d1617c5409a1b74b6d74c3aaab Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 5 Mar 2026 08:40:57 +0100 Subject: [PATCH 08/10] fix(client): move fog blur to CPU pipeline, fix GL compat bilinear on RGBA8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces GPU 7×7/5×5 Gaussian blur (98 texture reads/px) with CPU-side Gaussian blur (sigma 2.0) + 4× bilinear upscale + RGBA8 convert in fog_state.gd. GL compatibility mode doesn't bilinear-filter R8 textures; RGBA8 at 4× resolution resolves this. Squared exp_fade at the explored/unexplored boundary keeps fog opaque near tile content edges, fixing the staircase artifact. Shader now does 2 texture reads per pixel. Co-Authored-By: Claude Opus 4.6 --- client/scripts/rendering/fog_shader.gd | 1 + client/shaders/fog.gdshader | 80 +++++++------------------- docs/architecture/fog-shader-spec.md | 68 ++++++++++++---------- 3 files changed, 59 insertions(+), 90 deletions(-) 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/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 From ac763fef97116b7356a1e059b47fee5b63e1b30c Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 5 Mar 2026 08:41:09 +0100 Subject: [PATCH 09/10] feat(engine): live server visual tests and gauntlet snapshot replay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add live server lifecycle to tests/run-visual (start/stop server per scenario, parse LISTENING:{port}). Add MessagePack snapshot replay to visual_capture.gd via Protocol.decode_snapshot() — exercises the full client pipeline from wire bytes to rendered fog. Three replay scenarios (hub_spawn, fog_theater, hub_after_movement) plus one live scenario (fog_live_hub). Add gen_gauntlet_fixtures.rs to produce .msgpack fixtures from the Gauntlet test world. Add max_diff_pct threshold to visual-diff. Makefile: add fixtures-gauntlet target, fix build-client double-import, preserve .godot cache in clean. Co-Authored-By: Claude Opus 4.6 --- Makefile | 10 +- client/tests/visual_capture.gd | 78 + client/tests/visual_capture.gd.uid | 1 + client/tests/visual_scenarios.gd | 11 + client/tests/visual_scenarios.gd.uid | 1 + server/tests/gen_gauntlet_fixtures.rs | 185 + tests/fixtures/gauntlet-t164-snapshot.json | 2895 ++++++++++++++++ tests/fixtures/gauntlet/fog_theater.json | 3029 +++++++++++++++++ tests/fixtures/gauntlet/fog_theater.msgpack | Bin 0 -> 20343 bytes .../fixtures/gauntlet/hub_after_movement.json | 1079 ++++++ .../gauntlet/hub_after_movement.msgpack | Bin 0 -> 7116 bytes tests/fixtures/gauntlet/hub_spawn.json | 1607 +++++++++ tests/fixtures/gauntlet/hub_spawn.msgpack | Bin 0 -> 10674 bytes tests/run-visual | 95 + tests/visual.json | 21 + tooling/visual-diff | 31 +- 16 files changed, 9034 insertions(+), 9 deletions(-) create mode 100644 client/tests/visual_capture.gd.uid create mode 100644 client/tests/visual_scenarios.gd.uid create mode 100644 server/tests/gen_gauntlet_fixtures.rs create mode 100644 tests/fixtures/gauntlet-t164-snapshot.json create mode 100644 tests/fixtures/gauntlet/fog_theater.json create mode 100644 tests/fixtures/gauntlet/fog_theater.msgpack create mode 100644 tests/fixtures/gauntlet/hub_after_movement.json create mode 100644 tests/fixtures/gauntlet/hub_after_movement.msgpack create mode 100644 tests/fixtures/gauntlet/hub_spawn.json create mode 100644 tests/fixtures/gauntlet/hub_spawn.msgpack 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/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/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 0000000000000000000000000000000000000000..1630a75e70ed067475944cd594e12baf05d20c51 GIT binary patch literal 20343 zcmb{4+ix7z9l-Go1bV-43E_^p1_GE6?)O}XAis1*@s9zUv~Dd zErqg5i~PEkI_=G0wK1D4gl?TKg!ew`CrxK_^kcV}>5l$qga32~b4eA}ep-}yn$K>G zP`6C#urjrnW#)Aot61C@Bj7f@lDPwrByneW@+85O%>&- zr0kz9LzdJmrkYC!t8eDTseH4Tu0pvqes%2^Baww|x4358Ek6DF79YJe=qdWX=YZ3qX|J~u-c=g$6R9UFL*faaVaQVJrb0?d0g%8e{xOB96t5aD~lxy!6 zd0>b4Y^l4Hn%GT^W}R)n)&0Zfj@{7C4~P=WLWyOe;+OS+S{6zy3ni9?ieJ`&VRL)@ zv`}KkD6ttRu^FiN&3I6421;xON^Ay7Yz8WRGaec?x5bx%5@qqr{L=V#p{lWRw^(Dt^e1sUf4pkWpgDC^2M|7&1x>86}2{5<^DC5Bbor*&qKT zqr~%%5^11B8YqzlN~D1jX`nw63L@P z@+gr!N+gepPyTroFG|FV67ix$yeJVbO2ms2@uEb$C=oA8#ETN~qC~u?_;_DX@uEb$ zC=oA8#ETN~qC~tX5id%_ixTmoM7$^wFG|FVijVh2l_5%Gh!Pp1M20AlAxdP35*eaI zhA5FCN@R!<8KUAdd`V@95*eaIhA5FCN@R!<8KOjnD3KvbWQYefO21=xX5^11z zO=IWp@}cgI{0GtZhs&sY+NrIJ@7?X<@oy&byN^dyswj~vN~DSssiH)xD3K~kq>2)$ zqC~1Fkt#~0ifW%K7q?FJsEQ6HqC<)3P$D{%hz=#9Ly726B07|a4ke;PwU3UATSs?H zMTZj6p+s~j5gkfIhZ51DM06+-9ZE!p649aBN5{pjqdTthL5X}&A|I5<2PN`BiF{BZ zAC$-kCGtVF&xea!=kuz{2PN`BiF{BZAC$-kCGtUud{81El*k9wJ|8Y_ozDps0!oB{ z5+R^O2q+N(N`!zCA)rJEsP-XnaqAGKT8wnZ2{)i!Cwe8c>p-uBcAe*ykQgV665~XP zaiZFflZ#uA^EI^ylvo6+{UW%y-6Ht6jn`X$oUM!7e+u5Y^`|)5VtM2A_z+y%?tMSW zLsCw2>{nigGU<&^@%r$CuMY#eX7lp7V0AMHqw~4e`{~w^VtZFP!fd+w@V)WrU}sJ& zCktsFxMDR+rbD)Q`GtwM-afuQDAS_k#IQXZ$x@Q?_gVNiM}++<^y=>DjW3?<6?t7I z{j^ty{=Y8t7R!?Rb{C4g$cov;@SDG+{lAr=SIp*|S61ZRLx)%Duv~wB_-AuP6((ki zqOR&Ns3s>TCr%z%3KMgzt3OfCCH2H0saae;F&$=#GEAQDFLH9)=uIl#Fz;8Fj{LdP z`E(HSKBuR3x80@s!}gunac0@y%dn87qx0A}4y-p{?Y&SJb&_@a9Ea9yy>#uiH`sY3 zS(`Vw&`*=`7VyEU0cWg*i_=-|VTJUaQEub!BC?e&vyy!n%}Rk4`!-dp$jgzXxgTer1)mtXw& zn=j`5&_8{4rpU75RF_ifaPi~bTrz5l6Y2&mqFOA&`|DwOP>g=+oJ;eHL+BPq6G^hp zHx85Qtd4iIv$9YW{fp}*J6(m)Ke#?9(r!kGr~grxX_C)!%v!JHn7Qh!k9uZ&>n?}p R&3(4~`+t7^>;E3w_dopP20H)% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..44294b99140f08d1c0d642450862f60fcd65d365 GIT binary patch literal 7116 zcmbW6NpBoQ7={}X!oGwp?2wSv*_?fIHYYJ8CISfoH(E8_<(bOVRE?^7YPNSaP>Kt+;WQe4g3V&Ze!snj!eB5PrE%&*Y`c|%*F3t(Wd^mbY5Arv6rc& zxqhZDS~AX5OMdrDD)MNyyo~KwT>dhHA91@OyzF1K&L~r#DWTYjO!h`PTF0F(^U?C_)CPa2b1x{A{=kP=rEIghEh+ zLQsT4P~i&qQ3^p33PBMHK@kc;5eh+tE8Jh?XTm>)poq^dC_+3aLOdu!JSajusBrOv zlz32tcu<6RP=t6;gm_Sdcu?Wu4;1<7@J~D_;(G#$hzk@E7bqewP()myh`2z7$91sC zhr;6mMT88B2pJR+GAJTsP(;X}h>$@MA%hAJ`4ANsC?YOUL|mYVxIhtcfg<7pMZ^Uv zJg&o(cu<6RP=t6;gm_Sdcu<6RP~qZ_P~t%m;z1GOK@s9X5#m7+;z5OrKT0VCMJNPC zC`8U+WY#AShx$P~ig({f_}3`L92qHRoFm3LNi_k;2tbwwO#h zkyPDDcOmIGhZZJoS!1=WcjV&lZD~?eIoQYESb@Lb=O89J@g+N^meeeAbKKy9&mXEh?+rDGxu7TZq_U_w1 zc;Min!$*!DJAUHisiD(n&Yn9zeBt7ykxxFod}Z{rId3}#4_`X$PGZqVIbU@}%QtU7 zeAA>d?aqx^t?fjNMk|wxx08k_)uHxkV+-GL^80By*|z0XG_Qhgm#4Z}9UY^IYuvkk?eFHd+Xw&r E2diG@xc~qF literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..602ad74bf5ddad1371604b0b081718f58210827a GIT binary patch literal 10674 zcmbW-$x|F<9LDiB#<(wW!+nn=WMN+ujiRCkw~Ko0>gj$NI@8nd)Z0D4J-8{w;ockP?cqrslZ>GT}1~&-0Me?{BZCxnNut+w|mW9*31<8|9#C z{5-CjA8tfJ!`m!A_3fNr{AGhb{MvkwndX<)rEyx`D3I@h+^o*jlVrP=1P$Z-xgg{p z*S@kYpWlBi%Y!sDtKSDn<<`Gn7N-j)&8=%Juk$g^W0U>z(?;vlh95`X%F^BGE1tK~ zxI2B757)faN}NW&e$#r$&4oCNOK}qCjpmGXi-C)_oJoS5A10fRYiqNqT}(G^DKl=N z^=k9GLL}zEBknu!i0K25__}n}gn6e&G`~$N|M!sX*0+}wStcgC=lywoWvO?i(a%rO z=55K5FD-t$BA}#s&8DVEU=Ix_Xiz~z3L5SjomPSXbqr!{Cb8BXj1Sc@iMFa^6m3<< zEZVA(P6-}WXOsv*LWC$GLX;38N{A38MA)Z92ofSh2@#@%2vI_WC@sR#?zekZ2_7T_ zj}n4M3BjX;;88;GC?WWBO7I{dc$5%4N(de$1dkGeM`^*2b^m*EUa1Qt)P)l2LJ4)D zgt|~dT_~Y0lvdYx_a$FYN(KofqlA)CLdhthWRy@cN+=m6l#CKe?pI0%2_>V1l2Jm* zD4}GOP%=s=86}jA(n_A_{`Y@CnFdIh21=L)N|**pm|LJ5OH34=ljgF*>|LJ5N!Ri*(FrhyWsffA;H5~hI?rhyWsffA;H z5~hI?rZJ{W10+lXB}@Y)OamoM10_rYB}@Y)OamoMV_Yd2B$SL2N=6AKqlA)CLdhth zWRy@cN+@|kDH$Y`j1o#l2_>V1l2Jm*D4}GOQ1YZw7f7fJCDery>Ou*1p@h0nLS0iz z@E{?0ln^{h2p%N_j}n4M3Bg}ff(Hq~qlDm5LhvXdc$5(QB_%>o#|V#HT!A_!*ba3J zupR1{Upv$>zQgEFY64f{q%O^k3&Pfg-11IXcaB>-ziLyP*mB+M{$4X_#NnRrOlZp~ z2YH;*{e1nui@c-fa=$rK4dUVykVChyk{#d4Z5||k#3f#1@1+wvQ5>`$!24!3-qP%D zW?_)}ra1r0mT#338&*u@?;io)^Jb6TfBbE%~meeLx(-hAurcTTmSF^bZUU4UdeDjZaKYUA*+(`mqRf#oLEpOd}I5Z_U{xv5P);i@Djo9nJ?ubR0L< z_$jiwGe54IrJ5~1^S0wO<9@MT1QsOTp*uuxt(AzkTD3OXU3Vmv858xb*KF)3ah5Ot kk-IoZ%NziPjtj+X|GT=`dbH2I<@{e`jZ@{`(v6;f0Q0}tRR910 literal 0 HcmV?d00001 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: From fc24de61281758fab28d4716c7e8f216f3bc0900 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 5 Mar 2026 08:41:18 +0100 Subject: [PATCH 10/10] fix(client): test harness tile types, bug report screenshot, time_display maxf TestHarness: remove deprecated tiles/visible_positions keys, add tile type (floor/wall/door) to visible_tiles, expand radius to 5. Bug report dialog: capture viewport screenshot before showing overlay, save as screenshot.png in report bundle. time_display: use maxf() instead of max() to match float argument types. Co-Authored-By: Claude Opus 4.6 --- client/scripts/protocol/test_harness.gd | 27 +++++++------------ .../tests/test_entanglement_sprint22.gd.uid | 1 + client/tests/test_fog_sprint22.gd.uid | 1 + client/ui/bug_report_dialog.gd | 17 +++++++++++- client/ui/time_display.gd | 2 +- 5 files changed, 29 insertions(+), 19 deletions(-) create mode 100644 client/tests/test_entanglement_sprint22.gd.uid create mode 100644 client/tests/test_fog_sprint22.gd.uid 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/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/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