# Phase 6: Client Rebuild — MVP Gate (CLI-08 HARD MILESTONE) - Context

**Gathered:** 2026-05-09
**Status:** Ready for planning

<domain>
## Phase Boundary

Stand up `apps/client` — a Vite-built TypeScript Phaser 3.90 bundle running in Chrome desktop — that connects to the deployed Fly.io server (`wss://staging.rebno.decidel.com/colyseus`, then `wss://rebno.decidel.com/colyseus`) over WSS, authenticates via Better-Auth, runs `packages/game-logic` `step()` for client-side prediction with server reconciliation, renders the `mvp-lobby` room with HiDPI nearest-neighbor + integer scaling, and lets two players in the same room see each other move and exchange chat — demonstrated end-to-end. **CLI-08 is THE go/no-go for the rebuild.**

In scope:
- **`apps/client/`** workspace package (Vite 8 + Phaser 3.90 + TypeScript strict). Per-env builds via `pnpm --filter @rebno/client build:staging|build:prod` (carries forward Phase 5 D-19); env files (`.env.staging`, `.env.prod`) hold `VITE_WSS_URL`, `VITE_HTTP_BASE`, `VITE_ROOM_SIGNING_PUBKEY`. **Same-Fly-app hosting** — Vite build outputs to `apps/server/public/` (or equivalent path served by Express); single domain per env eliminates CORS, allows `SameSite=Strict` cookies (CLI-01).
- **Login + session flow (CLI-02, CLI-03)** — hybrid Phaser scene + DOM-overlay form (DOM `<form>` rendered via `Phaser.GameObjects.DOMElement` over a retro Phaser backdrop). On boot, a `BootScene` calls `GET /api/auth/me`; valid session → ~500 ms spinner → transition to `GameScene`; missing/expired → focused login form. Errors surfaced inline (bad pw, `PROTOCOL_VERSION_MISMATCH`, `SERVER_DRAINING`). Force-reset users: server emits `s2c.force_password_change` after `onJoin`; client opens an in-room password-change overlay (movement frozen, no disconnect-reconnect cycle — Phase 4 D-07 carryover).
- **Movement + prediction (CLI-04, CLI-06)** — `GameScene` wraps `packages/game-logic` `step()` for client-side prediction. Reconciliation is **threshold-gated** — small divergence (< ~half-tile, planner picks concrete number from 44 × 40 px tile) lerps over ~100 ms while replaying unacked inputs; large divergence hard-snaps. Remote-player rendering uses **snapshot interpolation with extrapolation on stale (>200 ms) snapshots**, extrapolated by running deterministic `step()` locally for remote players using their broadcast input axes (see D-08 — wire schema expands to carry remote inputs). HiDPI: Phaser `Scale.FIT` with `autoRound: true` + `Phaser.Textures.FilterMode.NEAREST` everywhere. Camera follows local player.
- **Chat HUD (CLI-05)** — Minecraft-pattern: chat is a non-modal overlay (DOM div absolutely positioned over Phaser canvas). Default state shows last N messages fading out; **Enter or T** opens chat input + freezes movement keys; Enter sends, Esc cancels and releases focus. Remote-player nameplates rendered above sprite via Phaser text or DOM (planner picks). Phase 4 D-02 wire (`s2c.chat_msg`, `c2s.chat_send`) consumed unchanged.
- **Reconnect UX (CLI-09)** — top-banner notification ("Reconnecting…") on WS drop. Local input continues to be predicted but **world-mutating actions** (item acquisition, message-board posts, etc. — none in MVP scope but affordance baked in) are blocked while disconnected. Auto-retry every 1 s. Within 10 s grace → Colyseus `allowReconnection` resumes same session, server-authoritative position restored. After 10 s → silent re-auth from cookie. New session (sign-out + sign-in or full session expiry) → spawn at home portal, NOT last position. Banner clears on success; final failure shows "Disconnected — click to retry."
- **Asset pipeline (AST-01, CLI-07)** — `tools/asset-pipeline/` standalone Node CLI (Phase 1 D-17 pattern; outside the workspace). Two modes: (a) `bootstrap` — reads `extracted/client-5-8/sprites/<id>/<frame>.bmp` + `meta.json`, decodes via sharp, emits per-sprite Aseprite-export-compatible PNG strips + JSON sidecars at `assets/source/sprites/<sprite_id>.{png,json}` (one-shot; output committed so devs can edit `.aseprite` source files locally and re-export); (b) `build` — packs `assets/source/**` into a single multi-sprite atlas (`atlas-mvp.png` + Phaser-format `atlas-mvp.json`) plus a `background-mvp.png`, writes `tools/asset-pipeline/output/pipeline-manifest.json` with rich per-sprite metadata (frames, origin, hashes) referencing Vite-hashed paths from Vite's own `manifest.json` after build. Phaser loads via `this.load.atlas('mvp', …)`. MVP slice scope: NaviStandD/U/R + NaviRunD/U/R (player) + Tile1, TSide1, VBorder, HBorder (room walls/floor) + one background. Sourced from `extracted/client-5-8/sprites/0000-NaviStandD/` etc.
- **Room rendering** — `apps/server/rooms/mvp-lobby/000.json` arrives via `s2c.room_layout`, signature verified against `import.meta.env.VITE_ROOM_SIGNING_PUBKEY`, then rendered via Phaser `TilemapLayer` (or custom blit if planner finds Phaser tilemap awkward for the canonical layout JSON — see Claude's discretion). Hot-reload from server (Phase 4 D-10): on receiving a fresh `s2c.room_layout` for the current room, client re-builds the tilemap in place; player position preserved.
- **Wire / protocol coordination** — Phase 6 plan **expands `packages/protocol`** to broadcast remote players' last-input axes (see D-08). This bumps `PROTOCOL_VERSION` and adds an `input_axes` field to `PlayerState` schema; `apps/server` tick loop emits the held input vector per account. Coordinated re-deploy ritual per Phase 4 D-03 (server before client).
- **Per-env Vite bundles** (Phase 5 D-19 carryover) — `pnpm --filter @rebno/client build:staging` injects staging WSS URL + staging Ed25519 pubkey; `:prod` injects prod equivalents. Bundle ships zero static layout data per Phase 4 SRV-13.
- **CLI-08 verification harness** — Both: (a) **Playwright two-client smoke** in `pnpm verify:phase-6` + `.github/workflows/deploy-staging.yml` (extends Phase 5 D-11 soak) — spawns 2 headless Chromium contexts, logs in as 2 accounts, asserts move + chat round-trip + nameplates visible; gates merge to `main`. (b) **Manual UAT** recorded in `06-HUMAN-UAT.md` with screenshot/video evidence — milestone artifact for "we shipped CLI-08." Same UAT pattern as Phase 5.
- **Lints** — `lint-asset-pipeline.mjs` (manifest schema validation + atlas integrity), `lint-vite-env.mjs` (`.env.staging`/`.env.prod` carry required `VITE_*` keys, no secrets), wired into `pnpm verify:phase-6`.
- **CSP + HiDPI + Chrome-first runtime guarantees** — strict CSP headers via Express on `/`, `<canvas image-rendering: pixelated>` fallback, integer-scale snap on `window.resize`. Best-effort other browsers, blocking only Chrome desktop.

Out of scope (belongs in Phase 7 or later):
- AST-02 (MIDI→OGG via ffmpeg + vintage soundfont), AST-03 (BMP fonts → WOFF2), AST-04 (full content-hashed manifest spanning audio + fonts) — Phase 7. Phase 6 ships silent (no audio in `extracted/client-5-8/` per ADR 0001).
- Full room set + pixel-diff verification — Phase 7 PAR-03. Phase 6 ships exactly `mvp-lobby` through Phase 4 SRV-13 contract.
- Multi-room transitions, friends list + presence, whispers, account recovery, account-level settings — Phase 7 PAR-04/PAR-06.
- `.bnu` per-user transactional character migration — Phase 7 PAR-05.
- Modernized admin web UI — Phase 7 PAR-07.
- Full chat surface (whispers, channels, ignore/block, profanity wordlist, history rolling buffer) — Phase 7 PAR-04. Phase 6 ships only `chat_send`/`chat_msg` broadcast.
- Mobile / native ports — out of scope per PROJECT.md.
- Behavioural anti-cheat beyond authoritative-server model — out of scope per PROJECT.md.
- Pre-game lobby, character creation, settings menu, message-board reader — Phase 7.
- World-mutating action surfaces (item acquisition, message-board posts) — Phase 7. Phase 6 only bakes the **affordance** (block these while reconnecting); none ship in MVP scope.

</domain>

<decisions>
## Implementation Decisions

### Login + session UX (CLI-02, CLI-03)

- **D-01 (hybrid Phaser scene + DOM-overlay form):** `LoginScene` renders retro pixel-art backdrop (Phaser); login form is a DOM `<form>` mounted via `Phaser.GameObjects.DOMElement`. Browser password-manager + autofill + accessibility (screen readers, autofill, paste) all work. Errors render inline below the form (`PROTOCOL_VERSION_MISMATCH`, bad credentials, `SERVER_DRAINING`).
- **D-02 (autologin → ~500 ms spinner on login scene):** `BootScene` calls `GET /api/auth/me` on app boot. Valid session → show `LoginScene` with spinner + "Reconnecting as `<username>`…" for ~500 ms (visible feedback even on fast paths) → transition to `GameScene`. Invalid/expired → focused login form. User can hit Esc during the spinner to cancel and re-enter credentials. Frictionless steer (Phase 4 D-07) preserved without making the login screen "flash."
- **D-03 (in-room force-reset overlay):** On `s2c.force_password_change` after `onJoin`, `GameScene` opens a non-dismissible password-change overlay (DOM form): movement keys frozen, scene rendered behind, `POST /api/auth/change-password`, on success server clears `accounts.force_reset` (Phase 4 D-08), overlay closes, normal play resumes — **no disconnect-reconnect cycle**.

### Chat HUD (CLI-05)

- **D-04 (Minecraft-pattern non-modal overlay):** Chat is a DOM div absolutely positioned over the Phaser canvas. Default: latest N messages render bottom-left, fading out after a few seconds. **Enter or T** opens the chat input field + freezes movement keys (input field becomes focus-target so browser stops bubbling keystrokes to Phaser). **Enter** sends + closes; **Esc** cancels + closes; either way, movement keys re-engage. Visual indicator (e.g. cursor blink, distinct background) shows "chat-mode" active.
- **D-05 (remote-player nameplates):** Always-on text label above remote-player sprites showing their `username`. Form factor (Phaser canvas text vs DOM overlay) is planner's call — both work; canvas text is cheaper, DOM gives crisper anti-aliasing.

### Reconnect UX (CLI-09)

- **D-06 (top-banner reconnect):** WS drop → top-of-viewport yellow/orange banner: "Reconnecting…" with auto-retry every 1 s. Local prediction continues so the player's character stays responsive on screen. **World-mutating actions blocked** while disconnected (Phase 6 ships none, but the affordance is baked into the input/intent layer for Phase 7). Within Phase 4's 10 s grace → `allowReconnection` reuses session token; server position is authoritative on rejoin (no client-claim trust). Past 10 s → silent re-auth from cookie. Banner clears on success. Final failure (cookie also expired or repeated server unreachable) → "Disconnected — click to retry" clickable banner.
- **D-07 (no cross-session location persistence — home-portal spawn on fresh login):** **Refines Phase 4 D-14.** Position persists in `characters.x/y/room_id` only for grace-window reconnect; on new session (full sign-out + sign-in, or session-expiry-and-rejoin), server spawns the player at their home portal, ignoring `characters.x/y`. REBNO is exploration-driven; travel itself is the progression, so per-session reset is intentional. **Server-side change required** (Phase 4 D-14 amended): `onJoin` for a new Better-Auth session emits home-portal position, NOT the persisted `characters.x/y`. Anti-cheat trivially satisfied — server is authoritative; client never claims position.

### Movement prediction + reconciliation (CLI-04, CLI-06)

- **D-08 (schema expansion — server broadcasts remote-player input axes):** **Refines Phase 4 D-01.** `PlayerState` schema (`packages/protocol`) gains `input_axes` (last received c2s.input axes for the player). Server tick-loop populates from per-account stored input vector; `@colyseus/schema` delta-syncs. Bumps `PROTOCOL_VERSION`. Coordinated server-before-client deploy (Phase 4 D-03 ritual). Enables clients to run deterministic `packages/game-logic` `step()` for **remote players** during stale-snapshot extrapolation, so extrapolated positions stay collision-correct against the local signed-room layout.
- **D-09 (event-driven c2s.input — refines Phase 4 D-02):** Client sends `c2s.input` on **state change** (keydown/keyup transitions), not per server tick. Payload: `{ seq, axes: {x, y}, buttons_down, buttons_up, monotonic_at_ms }`. Server stores latest axes per account; tick loop reads stored value each step. Heartbeat-coupled refresh (every 15 s — Phase 4 D-22 heartbeat cadence) reaffirms held inputs to survive dropped keyup packets. Lowers wire traffic; lowers Phase 4 D-22 input rate-limiter need (planner may relax `input` budget from 25/s burst 35).
- **D-10 (threshold-gated reconciliation):** Local prediction error vs server snapshot:
  - `divergence < ~22 px` (≈ half a 44 × 40 tile, planner locks concrete number): smooth lerp toward server position over ~100 ms, replaying unacked inputs.
  - `divergence ≥ threshold`: hard snap to server position; no lerp.
  Avoids visible rubber-banding on big corrections while keeping micro-corrections invisible. Standard Source/Overwatch netcode pattern.
- **D-11 (snapshot interp + step()-driven extrapolation for remote players):** Remote players render at ~100 ms backbuffer, interpolating between received state snapshots. If snapshot stale > 200 ms: extrapolate by running `packages/game-logic` `step()` locally for that player using their broadcast `input_axes` (D-08), with collision against the local signed-room layout — bounded, never floats through walls. Cap extrapolation horizon at 250 ms; thereafter, freeze the sprite. On next valid snapshot, snap-or-lerp by D-10's threshold rule.
- **D-12 (HiDPI nearest-neighbor + integer scale):** Phaser `Scale.FIT` with `autoRound: true`; `Phaser.Textures.FilterMode.NEAREST` set on every loaded atlas; CSS `image-rendering: pixelated` as belt-and-suspenders. Snap to integer scale on `window.resize`. Closes CLI-06.

### Asset pipeline (AST-01, CLI-07)

- **D-13 (`tools/asset-pipeline/` standalone CLI):** New Phase 6 tool, parallel to `tools/extract-gmd`, `tools/asset-catalog`, `tools/protocol-doc`, `tools/save-format-doc`, `tools/room-converter`. **Outside the pnpm workspace** (Phase 1 D-17 / Phase 2 D-15 boundary preserved). Reused by Phase 7 AST-02..04 (audio + fonts + manifest expansion).
- **D-14 (two-mode pipeline — bootstrap + build):**
  - `pnpm asset-pipeline bootstrap` (one-shot): reads `extracted/client-5-8/sprites/<id>/<frame>.bmp` + companion `meta.json` (origin, frame_count) via sharp, emits per-sprite Aseprite-export-compatible PNG strips + JSON sidecars at `assets/source/sprites/<sprite_id>.{png,json}`. Output **committed** to git so devs can edit `.aseprite` source files locally, export PNG strips back to `assets/source/sprites/`, re-run `build`, and ship without re-running `bootstrap`.
  - `pnpm asset-pipeline build`: reads `assets/source/**`, packs into a single multi-sprite atlas (`atlas-mvp.png` + Phaser-format `atlas-mvp.json` with `{frames: {sprite_id_frame_n: {x, y, w, h}}, anchors: {sprite_id: {x, y}}}`) plus per-room `background-<room>.png`. Emits `tools/asset-pipeline/output/pipeline-manifest.json` with rich per-sprite metadata (sha256, frame_count, origin, atlas_ref).
- **D-15 (Aseprite-friendly source format):** `assets/source/sprites/<sprite_id>.png` is a horizontal-strip PNG (Aseprite's canonical export); `assets/source/sprites/<sprite_id>.json` carries `{frame_w, frame_h, frame_count, origin: {x, y}, sprite_id}`. Devs edit `.aseprite` files (kept in `assets/source/sprites/aseprite/<sprite_id>.aseprite`, gitignored or committed per repo size policy — planner picks), export to the canonical PNG strip + JSON via Aseprite CLI (`aseprite -b sprite.aseprite --sheet sprite.png --data sprite.json --format json-array`).
- **D-16 (hybrid manifest = Vite hashes + asset-pipeline indexes):** Vite handles file hashing + cache-busting (writes `dist/manifest.json` with content-hashed paths); `tools/asset-pipeline` post-processes `dist/manifest.json` into a richer `pipeline-manifest.json` indexing per-sprite metadata against Vite-hashed paths. Client fetches `pipeline-manifest.json` on `BootScene` startup, then loads atlas. Decouples Phase 7 AST-02..04 (audio + fonts) expansion from Vite internals.
- **D-17 (MVP atlas scope):** Sprites for `mvp-lobby`: NaviStandD/U/R + NaviRunD/U/R (player, ~6 sprites × frame counts) + tile sprites (Tile1, TSide1, VBorder, HBorder). One background image (sourced from the smallest navigable extracted background that fits `mvp-lobby` semantics — planner picks at plan time after `tools/room-converter` outputs are inspected). Atlas budget: 512 × 512 px target (overflow → 1024 × 1024 fallback).

### Hosting + CLI-08 verification

- **D-18 (same Fly app — `apps/server` serves static):** Vite build outputs to `apps/server/public/` (or `apps/server/dist/client/` — planner picks); Express adds `app.use(express.static(...))` mounted **after** `/api/auth/*` routes and **before** the Colyseus WS upgrade path. Single domain per env (`staging.rebno.decidel.com`, `rebno.decidel.com`); zero CORS; cookies `SameSite=Strict` viable. `.github/workflows/deploy-staging.yml` adds a client-build step (`pnpm --filter @rebno/client build:staging`) before the Docker build so the static bundle ships in the image. **Couples** client redeploy to server redeploy — acceptable at MVP scope; revisit at v2 if independent client release cadence becomes valuable.
- **D-19 (CLI-08 verification = Playwright merge-gate + manual UAT milestone):**
  - **Playwright two-client smoke** — `apps/client/test/cli-08.e2e.test.ts` spawns 2 headless Chromium contexts via `@playwright/test`, logs in as 2 accounts (seeded via `pnpm migrate:legacy-accounts` ritual on staging), navigates to `mvp-lobby`, drives WASD on each, asserts: (a) each client sees the other's nameplate + sprite, (b) chat round-trip (client A sends "hello", client B receives with sender=A), (c) reconnect grace works (kill ws on A, reopen, A's character resumes). Wired into `pnpm verify:phase-6` + `.github/workflows/deploy-staging.yml` post-deploy step (extends Phase 5 D-11 soak harness). Failure marks staging deploy red; gates merge to `main` (closes CLI-08).
  - **Manual UAT (`06-HUMAN-UAT.md`)** — operator opens 2 Chrome tabs against `staging.rebno.decidel.com`, repeats the Playwright scenario by hand, captures screenshot + screen-recording video. Logged as the milestone artifact ("we shipped CLI-08"). Mirrors Phase 5's HUMAN-UAT.md pattern.

### Carried forward from prior phases (locked, not re-decided)

Every prior `D-*` decision stands. Phase 6 explicitly relies on:
- **ADR 0001** (`docs/adr/0001-client-engine.md`) — Phaser 3.90 locked. No engine flip.
- **Phase 4 D-01..D-04** wire/state model (Colyseus Schema state-diff + msgpackr events + zod intents + `PROTOCOL_VERSION` first byte of `c2s.auth`). Phase 6 D-08 amends `PlayerState` to add `input_axes`; protocol bump.
- **Phase 4 D-05..D-08** auth flow (Better-Auth `/api/auth/sign-in/email` returns `session_token`; client opens `wss://server/colyseus?token=<bearer>`; cookie-backed silent reconnect; in-room force-reset overlay; `legacy_credentials_staging` → `accounts` rehash on first login).
- **Phase 4 D-10..D-13** room-layout signing + hot-reload (`s2c.room_layout`, Ed25519 verify against `VITE_ROOM_SIGNING_PUBKEY`, hot-swap on file change preserving player state). Phase 6 client implements verify + tilemap rebuild on hot-swap.
- **Phase 4 D-20** `packages/game-logic` purity (no `Date.now`, no `Math.random`, no I/O). Phase 6 wraps `step()` for client prediction without violating the lint.
- **Phase 4 D-22** rate limiter (token-bucket per `(account_id, msg_type)`). Phase 6 may relax `input` budget given D-09 event-driven cadence (planner decides).
- **Phase 5 D-02** domains (`staging.rebno.decidel.com`, `rebno.decidel.com`).
- **Phase 5 D-04** staging access (Fly proxy IP allowlist + `STAGING_INVITE_TOKEN` middleware). Phase 6 client must accept `?invite=<token>` in WSS connect URL when `import.meta.env.VITE_STAGING_MODE === '1'`.
- **Phase 5 D-19** Ed25519 keypair lifecycle — client embeds `VITE_ROOM_SIGNING_PUBKEY` extracted from prod machine via `fly ssh` per env at build time.
- **Phase 2 D-04** `mvp: yes|no` tagging in `docs/extracted-engine/` — Phase 6 cherry-picks `mvp:yes` features only; everything else is Phase 7.

### Claude's Discretion

User answered every surfaced gray area; deferred two specific sub-questions to best judgment (recorded above as decisions):
- **D-13** asset-pipeline tool home → standalone (`tools/asset-pipeline/`, Phase 1 D-17 pattern preserved).
- **D-16** manifest format → hybrid (Vite handles hashing, asset-pipeline emits richer pipeline-manifest.json).

Other discretion items inside selected areas:
- **D-05** remote-player nameplate form-factor (Phaser canvas text vs DOM overlay) — planner picks during plan time after a quick perf comparison.
- **D-10** divergence threshold concrete value (~22 px is the recommendation given 44 × 40 tile size; planner may pick 24 / 20 / 16 based on prediction-error histograms from Phase 5 staging soak data).
- **D-11** extrapolation cap horizon concrete value (~250 ms recommendation; planner may tune given measured RTT distribution).
- **D-15** `.aseprite` source storage location (committed under `assets/source/sprites/aseprite/` vs gitignored) — planner picks per repo size policy.
- **D-17** MVP background image — selected at plan time after `tools/room-converter` discovers `mvp-lobby` dimensions.
- **D-18** static-mount path (`apps/server/public/` vs `apps/server/dist/client/`) — planner picks at plan time.
- Room-rendering — Phaser `TilemapLayer` is the recommendation; planner may flip to custom blit if `TilemapLayer` proves awkward for the canonical layout JSON shape from Phase 4 D-09.
- Phase 6 plan **must** coordinate D-07 (server-side spawn semantics) + D-08 (schema expansion) + D-09 (event-driven inputs) into a small set of `apps/server` + `packages/protocol` patches landed before or alongside `apps/client`.

Override any decision in planning if research or codebase reading surfaces a conflict.

### Folded Todos

None — `gsd-sdk query todo.match-phase 6` returned 0 matches.

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents (gsd-phase-researcher, gsd-planner, gsd-pattern-mapper, gsd-executor) MUST read these before planning or implementing.**

### Project planning
- `.planning/PROJECT.md` — Stage 6 Active scope ("MVP gate: two players join the same room, see each other move, exchange chat messages — multiplayer over the new server"); core value statement
- `.planning/REQUIREMENTS.md` §"Client Rebuild — MVP (Stage 6)" — CLI-01..CLI-09 + AST-01 acceptance text
- `.planning/ROADMAP.md` §"Phase 6: Client Rebuild — MVP Gate (CLI-08 HARD MILESTONE)" — goal + 6 success criteria + hard-milestone language
- `.planning/STATE.md` — current position (Phase 05 complete; Phase 06 next)
- `.planning/research/STACK.md` — Phaser 3.90 + Vite 8 + version pins; Fly.io specifics
- `.planning/research/FEATURES.md` §"MVP-Critical (Stage 6 gate)" — defines `mvp:yes` cherry-pick scope
- `.planning/research/ARCHITECTURE.md` — three-pipeline topology; where `apps/client` sits relative to `packages/protocol` + `packages/game-logic`
- `.planning/research/PITFALLS.md` §B1 (server-authoritative — D-07 anti-cheat foundation), §C1 (HiDPI scale-mode pitfalls)
- `.planning/codebase/CONCERNS.md` — repo-private-through-Phase-7 reminder; client deploy stays internal until cleared

### Prior CONTEXT.md (decisions consumed verbatim)
- `.planning/phases/02-client-engine-documentation/02-CONTEXT.md` — D-04 `mvp` tagging convention
- `.planning/phases/04-server-rebuild-mvp/04-CONTEXT.md` — D-01..D-25 (wire/state, auth+Colyseus, room hot-reload, persistence, lints); Phase 6 D-07/D-08/D-09 amend D-14/D-01/D-02 respectively
- `.planning/phases/05-deploy/05-CONTEXT.md` — D-02 (region/domains), D-04 (staging invite token), D-19 (Ed25519 pubkey extract ritual + per-env Vite bundles)

### ADRs (locked decisions — Phase 6 consumes)
- `docs/adr/0001-client-engine.md` — Phaser 3.90 locked (Phase 6 is where it lands)
- `docs/adr/0002-persistence-layer.md` — SQLite + Litestream → Tigris (server-side; informs D-07 server-side spawn semantics)
- `docs/adr/0003-canonical-snapshot.md` — `legacy/servers/enlyzeam-current/` import source (Phase 4 already consumed; Phase 6 has nothing new here)
- `docs/adr/0004-room-hot-reload.md` — Phase 6 client implements verify + tilemap rebuild on `s2c.room_layout` hot-swap
- `docs/adr/0005-deploy-topology.md` — two Fly apps + region `lax` (Phase 6 client ships into the same apps per D-18)
- `docs/adr/0006-observability-stack.md` — OpenObserve self-hosted (Phase 6 client telemetry via OTel browser SDK is **deferred**; Phase 7 / v2 territory)

### Phase 1–5 outputs (consumed as input)
- **Extracted assets:** `extracted/client-5-8/sprites/<sprite_id>/` + `meta.json` — `tools/asset-pipeline bootstrap` source (D-14)
- `extracted/client-5-8/backgrounds/` — background-image source for D-17
- `extracted/client-5-8/rooms/` — `tools/room-converter` already consumed for `mvp-lobby` (Phase 4)
- **Server:** `apps/server/rooms/mvp-lobby/000.{json,sig}` — already seeded (Phase 4 D-12); Phase 6 client renders this layout
- **Server (extension required):** `apps/server/src/RebnoRoom.{ts,js}` — gains `input_axes` broadcast (D-08); `onJoin` adds home-portal-spawn-on-new-session logic (D-07); `onMessage('input')` switches from per-tick to event-driven storage (D-09)
- **Protocol:** `packages/protocol/src/state.ts` (or equivalent) — `PlayerState` schema gains `input_axes`; `PROTOCOL_VERSION` bumps (D-08)
- **Protocol:** `packages/protocol/src/intents.ts` — `c2s.input` zod schema updated to event-driven shape `{seq, axes, buttons_down, buttons_up, monotonic_at_ms}` (D-09)
- **Game-logic:** `packages/game-logic/src/step.ts` — pure `step(state, inputs, dt)` reused **as-is** for client-side prediction + remote-player extrapolation (D-11). No new game-logic in Phase 6.
- **Tools:** `tools/asset-catalog/output/index.json` — sprite metadata cross-referenced during D-14 bootstrap step
- **Tools:** `tools/room-converter/` — informs `mvp-lobby` JSON shape; Phase 6 doesn't call it at runtime
- **Verification:** `apps/server/test/authority.integ.test.ts` + `apps/server/test/reconnect.integ.test.ts` (Phase 4) — Playwright two-client harness pattern reused (D-19)
- **Verification:** `scripts/soak-staging.mjs` (Phase 5 D-11) — Phase 6 Playwright smoke extends this harness
- **CI:** `scripts/verify-phase-5.mjs` + `.github/workflows/deploy-staging.yml` (Phase 5) — Phase 6 mirrors shape for `verify-phase-6.mjs` + adds client-build step

### Reverse-engineering wiki + extracted-engine docs
- `docs/extracted-engine/README.md` — task-keyed jump table for engine subsystems
- `docs/extracted-engine/MATRIX.md` + `MATRIX-rows.json` — engine choice rationale (informs Phaser feature usage in Phase 6)
- `docs/extracted-engine/rendering.md` — `mvp:yes` rendering primitives Phase 6 must implement
- `docs/extracted-engine/input.md` — keyboard event handling on the original (informs Phase 6 input layer)
- `docs/extracted-engine/scene-room-model.md` — original room model; informs Phaser scene wiring + camera follow
- `docs/extracted-engine/animation.md` — sprite frame/animation conventions for atlas packing (D-14 sidecar JSON shape)
- `docs/extracted-engine/ui-and-menus.md` — chat HUD overlay pattern reference for D-04
- `docs/extracted-engine/asset-catalog/index.json` — sprite cross-reference for AST-01 MVP-scope picks
- `decomp/wiki/15-extraction-pipeline.md` — context for what `extracted/client-5-8/` carries
- `decomp/wiki/16-bno-bnb-notes.md` — context only (Phase 7 PAR-05 territory; Phase 6 doesn't touch `.bnu`)

### External implementation references
- Phaser 3.90 docs (`phaser.io/docs/3.90`) — Scene lifecycle, `Scale.FIT` + `autoRound`, `DOMElement`, `TilemapLayer`, `Tweens` for D-10 lerp, `Textures.FilterMode.NEAREST` for D-12
- Colyseus 0.17 client docs (`docs.colyseus.io/colyseus/getting-started/javascript-client/`) — `Client.joinOrCreate`, room `state.onChange`, message handlers, `client.room.send()`, reconnect tokens
- Vite 8 docs (`vitejs.dev/guide/`) — env modes, `import.meta.env`, manifest output, asset hashing, multi-env build via `--mode`
- msgpackr browser bundle (`github.com/kriszyp/msgpackr#browser`) — encoder/decoder reuse on client side
- zod docs — schema sharing between server (Phase 4) + client (Phase 6 reuses zod intents for type-safe send paths)
- @colyseus/schema docs — client-side `state.onChange` + `MapSchema.onAdd/onChange/onRemove` patterns for player join/leave/move
- Better-Auth docs (`better-auth.com`) — client SDK (`createAuthClient`) for `/api/auth/sign-in/email`, `signOut`, `me`, cookie semantics; `SameSite` config
- Playwright docs (`playwright.dev`) — `@playwright/test` multi-context patterns for D-19 two-client harness
- sharp docs (`sharp.pixelplumbing.com`) — BMP decode + PNG encode for D-14 bootstrap; lossless settings
- Aseprite CLI docs (`aseprite.org/docs/cli/`) — canonical export to PNG strip + JSON-array sidecar
- TexturePacker / `free-tex-packer` / sharp-bin-pack — atlas packing algorithm reference for D-14 build mode
- HiDPI canvas + integer-scale references (`webgl-fundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html`, Phaser `Scale.Manager`) — D-12

### What NOT to read in Phase 6
- `legacy/servers/{enlyzeam-archive,local-current}/` — non-canonical per ADR 0003
- `legacy/source-archive/` — older Client revisions, reference only
- `legacy/unity-project/`, `legacy/maya-project/` — abandoned remake attempts; not authoritative
- Modern decompilers (UTMT, Altar.NET) — incompatible with GM 5.3a (PITFALLS A1)
- LiteFS / Postgres docs — explicitly out of scope per ADR 0002
- Phaser 4 docs — explicitly rejected for MVP per ADR 0001 (Phase 7 retro re-evaluates)
- PixiJS docs — engine choice locked Phaser 3.90 per ADR 0001

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets

- **`packages/game-logic/src/step.ts`** (Phase 4 D-20) — pure `step(state, inputs, dt) → state`; runs in browser unmodified. Phase 6 wraps it twice: (a) for the local player's prediction loop, (b) for remote-player extrapolation when snapshots go stale (D-11). Lint-enforced purity (no `Date.now`, no `Math.random`, no I/O) means it's safe in a Phaser scene update without breaking determinism.
- **`packages/protocol/src/{state,intents,events}.ts`** (Phase 4) — Colyseus Schema classes, zod intents, msgpackr event types. Phase 6 imports browser-side; bumps `PROTOCOL_VERSION`; extends `PlayerState` (D-08) and `c2s.input` (D-09).
- **`apps/server/src/RebnoRoom.js`** + `apps/server/src/onMessageHandlers.js` (Phase 4) — already handle `c2s.input`, `c2s.chat_send`, `c2s.heartbeat`, `c2s.room_join`. Phase 6 adds `input_axes` broadcast (D-08), event-driven input semantics (D-09), home-portal-spawn-on-new-session in `onJoin` (D-07).
- **`apps/server/rooms/mvp-lobby/000.{json,sig}`** — already seeded (Phase 4 D-12). Phase 6 client consumes via `s2c.room_layout`.
- **`apps/server/src/health.ts`** (Phase 4 D-21) + Phase 5 Express boot — Phase 6 adds `app.use(express.static(...))` mount for the Vite client bundle (D-18).
- **`apps/server/src/auth-bootstrap.js`** + Better-Auth Express routes (Phase 4 D-05) — Phase 6 client SDK calls `/api/auth/sign-in/email`, `/api/auth/me`, `/api/auth/sign-out`, `/api/auth/change-password` with same-origin cookies.
- **`extracted/client-5-8/sprites/0000-NaviStandD/` etc.** + `meta.json` — `tools/asset-pipeline bootstrap` source for AST-01 (D-14). 854 sprites total; MVP scope ~10 picks.
- **`docs/extracted-engine/asset-catalog/index.json`** (Phase 2) — sprite metadata cross-reference; informs MVP-pick decisions (D-17).
- **`tools/extract-gmd/src/types.ts`** — canonical extraction types reused by `tools/asset-pipeline`.
- **`scripts/verify-phase-5.mjs`** + `.github/workflows/verify-phase-5.yml` (Phase 5) — Phase 6 mirrors shape for `verify-phase-6.mjs` (composite gate including Playwright two-client smoke + asset-pipeline lint + Vite env lint + trace:check).
- **`scripts/soak-staging.mjs`** (Phase 5 D-11) — Phase 6 Playwright smoke (D-19) extends harness; reuses staging-deploy infrastructure.

### Established Patterns

- **TS-everywhere** (PROJECT.md). `apps/client` is TypeScript-strict, Vite-built.
- **pnpm workspace** (Phase 4 D-18). `apps/client` becomes the second `apps/*` workspace member; existing `tools/*` stay standalone (Phase 1 D-17 / Phase 2 D-15 boundary preserved → `tools/asset-pipeline` follows suit per D-13).
- **Headless, CI-runnable** (Phase 1 D-01) — Phase 6 verification surface (Playwright headless Chromium, asset-pipeline lints, Vite env lint) runs on `ubuntu-latest` per Phase 5 precedent.
- **Reproducibility-first** (Phase 1 D-15) — `tools/asset-pipeline build` output deterministic (sorted keys in manifest, pinned PNG encoder settings via sharp).
- **Lint-as-forcing-function** (Phase 2/3/4/5 D-2X pattern) — `lint-asset-pipeline.mjs` (manifest schema + atlas integrity), `lint-vite-env.mjs` (`.env.staging`/`.env.prod` carry required `VITE_*` keys, no secrets), wired into `pnpm verify:phase-6`.
- **Mvp tagging** (Phase 2 D-04) — Phase 6 cherry-picks `mvp:yes` features only.
- **Anti-port reference** — Phase 6 ships **none** of the original `Ctrl+E` clipboard RCE / `Ctrl+Q` inspect / `Ctrl+O` codes admin model. Phase 7 PAR-07 fills the modernized admin UI; Phase 6 client has zero admin surface.
- **ADR-on-first-lock** — Phase 6 produces no new ADR; ADR 0001 already locks the engine, ADR 0004 already locks hot-reload. If D-07/D-08/D-09 prove load-bearing in Phase 7 retro, an ADR captures the contract then.

### Integration Points

- **Inputs:**
  - All prior phases' outputs (workspace + tools + extracted assets + signed-room layout + ADRs + research).
  - Phase 4 server runtime (extended with D-07/D-08/D-09 patches landed in Phase 6 plan).
  - Phase 5 deployed staging + prod Fly apps + per-env domains.
  - `legacy/open-source-release/BN Online Client 5-8.gmd` (already extracted; Phase 6 reads the extracted PNG/BMP outputs only).
- **Outputs:**
  - `apps/client/` — full Vite + Phaser + TS app (`src/scenes/`, `src/net/`, `src/prediction/`, `src/ui/`, `src/auth/`, `src/assets/`, `src/main.ts`, `index.html`, `vite.config.ts`, `package.json`)
  - `apps/client/.env.staging` + `apps/client/.env.prod`
  - `apps/client/test/cli-08.e2e.test.ts` — Playwright two-client smoke (D-19)
  - `tools/asset-pipeline/` — new standalone Node CLI (D-13/D-14/D-15/D-16)
  - `assets/source/sprites/<sprite_id>.{png,json}` — committed Aseprite-export source files (output of `bootstrap`)
  - `apps/client/dist/` (gitignored) — Vite build artefact; shipped via `apps/server/public/` mount per D-18
  - `apps/server/src/RebnoRoom.js` patch — `input_axes` broadcast (D-08), event-driven input storage (D-09), home-portal `onJoin` (D-07)
  - `packages/protocol/src/state.ts` patch — `PlayerState.input_axes`, `PROTOCOL_VERSION` bump (D-08)
  - `packages/protocol/src/intents.ts` patch — `c2s.input` event-driven shape (D-09)
  - `tools/scripts/lint-asset-pipeline.mjs` + `tools/scripts/lint-vite-env.mjs`
  - `scripts/verify-phase-6.mjs` composite gate
  - `.github/workflows/deploy-staging.yml` extension — client-build step + Playwright smoke
  - `06-HUMAN-UAT.md` — milestone artifact (D-19)
- **Downstream consumers:**
  - **Phase 7 PAR-01** (full client surface) — extends `apps/client/src/scenes/` with multi-room transitions, friends list, whispers, settings menu, message board reader; reuses `BootScene` + `LoginScene` + `GameScene` skeleton.
  - **Phase 7 PAR-03** (full room set) — uses `tools/room-converter` to populate every room; client tilemap rendering generalises from `mvp-lobby` to N rooms; D-04 hot-reload contract preserved.
  - **Phase 7 AST-02..04** (audio + fonts + content-hashed manifest) — extends `tools/asset-pipeline` per D-13/D-16 hybrid manifest.
  - **Phase 7 PAR-04** (full chat) — extends `apps/client/src/ui/chat/` from D-04 baseline (whispers, channels, ignore/block, profanity wordlist, history rolling buffer).
  - **Phase 7 PAR-05** (`.bnu` per-user character migration) — server-side; client unchanged at the protocol layer because home-portal spawn (D-07) means migrated character location is irrelevant on first login.
  - **Phase 7 PAR-07** (admin UI) — separate web UI; Phase 6 client surface has zero admin affordance.

</code_context>

<specifics>
## Specific Ideas

- **CLI-08 is THE go/no-go for the rebuild** (ROADMAP.md). Phase 6 plan writes the Playwright smoke FIRST (TDD shape), then implements `apps/client` against it; CI gate prevents regression while the rest of Phase 6 builds out (D-19).
- **Minecraft-pattern chat is the explicit user lock** (D-04). Enter or T toggles non-modal overlay; movement keys freeze on chat-mode; nameplate text always-on. Planner must NOT silently swap to a modal-popup chat UI.
- **Anti-cheat via authoritative server is sufficient** (D-07). Server is authoritative; client never claims position. Reconnect "accept new location securely" = server's last-known x/y for the same session; new session = home portal regardless of client state. No behavioural anti-cheat in v1 (out of scope per PROJECT.md).
- **Exploration is the progression model** (D-07). Cross-session location is intentionally NOT persisted — travel itself matters. This is the explicit user steer; Phase 4 D-14 (`characters.x/y` persisted on graceful-disconnect) is amended for Phase 6+.
- **Schema expansion is acceptable Phase 6 cost** (D-08). Adding `PlayerState.input_axes` + bumping `PROTOCOL_VERSION` is preferred over client-only collision-clipped extrapolation, because it lets `packages/game-logic` `step()` drive remote-player extrapolation deterministically against the local signed-room layout. Coordinated server-before-client deploy ritual (Phase 4 D-03).
- **Event-driven inputs** (D-09) — client sends on keydown/keyup transitions; server holds last-known axis vector. Saves bandwidth, simplifies server tick. Planner must add a heartbeat-coupled "still-pressed" reaffirmation message (every 15 s) to survive dropped keyup packets.
- **Threshold-gated reconciliation with concrete tile size** (D-10) — tiles are 44 × 40 px; threshold candidate ~22 px (≈ half-tile). Planner picks final number from prediction-error histograms collected during the Phase 5 30-min staging soak (D-11). Invisible micro-corrections, no rubber-banding on big corrections.
- **Aseprite-friendly asset source** (D-15) — devs edit `.aseprite` source files locally; pipeline reads PNG-strip + JSON-array exports (Aseprite's canonical CLI output). Keeps the 30+ year roadmap of "human dev edits a sprite" cheap.
- **Same-Fly-app hosting** (D-18) — couples client + server deploy intentionally. MVP simplicity wins; CORS-free; cookies stay `SameSite=Strict`. Revisit at v2 if independent client release cadence becomes valuable.
- **Playwright merge-gate + manual UAT milestone** (D-19) — CI keeps regression risk near zero; manual UAT provides the human "we shipped CLI-08" artifact + screen-recording video for posterity.
- **Phaser 4 retro window** (ADR 0001) — Phase 6 ships on Phaser 3.90; Phase 7 retro re-evaluates Phaser 4. Phase 6 plan must not silently jump to Phaser 4.

</specifics>

<deferred>
## Deferred Ideas

- **AST-02 (MIDI→OGG via ffmpeg + vintage soundfont) + AST-03 (BMP fonts → WOFF2) + AST-04 (full content-hashed manifest)** — Phase 7. Phase 6 ships silent (no audio in `extracted/client-5-8/` per ADR 0001) + uses Phaser default font. Hybrid manifest (D-16) is shaped to extend cleanly.
- **Multi-room transitions, friends list + presence, whispers, account recovery, account-level settings** — Phase 7 PAR-04 / PAR-06.
- **Full chat surface** (channels, ignore/block, profanity wordlist, history rolling buffer, whispers) — Phase 7 PAR-04. Phase 6 ships only `chat_send`/`chat_msg` broadcast on top of the D-04 HUD.
- **`.bnu` per-user transactional character migration** — Phase 7 PAR-05.
- **Modernized admin web UI** — Phase 7 PAR-07. Phase 6 client surface has zero admin affordance.
- **Pre-game lobby, character creation, settings menu, message-board reader** — Phase 7.
- **Mobile / native ports** — out of scope per PROJECT.md.
- **OTel browser SDK / client-side telemetry to OpenObserve** — v2 / Phase 7. Phase 6 logs to console only; server-side OTel (Phase 5 D-12..D-16) covers the wire side.
- **Independent client deploy cadence (separate Fly app for static client)** — v2; D-18 same-Fly-app coupling is acceptable at MVP scope.
- **Behavioural anti-cheat** — out of scope per PROJECT.md. Authoritative server (D-07) is sufficient for v1.
- **Pixel-diff verification of MVP room against original screenshots** — Phase 7 PAR-03. Phase 6 ships `mvp-lobby` only; pixel-perfect parity against original is Phase 7 territory.
- **OAuth/passkey providers, public username changes** — Phase 7 PAR-06 + v2.
- **Phaser 4 migration** — gated on ADR 0001 retro window (post-CLI-08, pre-Phase-7-PR-01).
- **Independent input cadence rate-limit budget reduction** — D-09 makes Phase 4 D-22's 25/s burst 35 input budget overkill; planner may relax during Phase 6 plan if it doesn't add friction.

### Reviewed Todos (not folded)

None — `gsd-sdk query todo.match-phase 6` returned 0 matches.

</deferred>

---

# Gap-Closure Addendum (2026-05-10)

**Trigger:** 06-HUMAN-UAT.md executed by operator decidel 2026-05-10. All 5 tests block CLI-08 closure. Per user decision, all five gap-closure items scoped to **Phase 6** — no Phase 7 advance until CLI-08 truly green.

**See:** `.planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-HUMAN-UAT.md ## Findings` for finding details + sequencing recommendation.

<gap_closure_decisions>

### Finding #1 — Login screen text invisible (high)

- **D-23 (small CSS fix; one plan):** LoginScene CSS enforces UI-SPEC color tokens — title + field labels switch from `#000` → `#FFFFFF` (or token foreground). No design discussion needed; UI-SPEC §LoginScene already locks tokens. Add a Playwright contrast assertion (axe-core or simple computed-style check) to prevent regression.

### Finding #5 — No logout affordance (medium)

- **D-24 (Esc menu — also fires on game-view click):** Add an in-GameScene **Esc menu** modal overlay (BNO-faithful command-screen UX feel). Menu options: **Logout** (clears Better-Auth session cookie + returns to LoginScene), **Resume** (closes overlay), **Settings** (placeholder, deferred to Phase 7). Trigger conditions: (a) operator presses Esc, (b) operator clicks the game canvas (single click). Movement keys frozen while overlay open. Logout flow: `POST /api/auth/sign-out` → clear cookie → `LoginScene.start()` (do NOT preserve invite token; user re-enters via fresh `?invite=` URL or saved bookmark). One plan.

### Finding #2 — Reconnect / cookie auto-login renders blank GameScene (critical)

- **D-25 (investigate via /gsd-debug FIRST, then plan from findings):** Root cause unknown. Hypothesis space large (HUD mount idempotency, scene re-entry init, Phaser DOMElement re-create, Better-Auth cookie path bypassing BootScene → GameScene init). Run `/gsd-debug` session to isolate before writing a fix plan. Output: a debug summary doc that the next plan consumes verbatim. Fix plan ships with two new Playwright e2e tests added to 06-08's `cli-08.e2e.test.ts` family: (a) cookie-auto-login → GameScene renders HUD + remotes + self, (b) WS-kill → reconnect → GameScene re-renders HUD + remotes + self.

### Finding #3 — Movement does not match BNO (critical)

- **D-26 (split into TWO plans; numerics first, sprite-state after #4):**
  - **Plan A (no asset dep):** Reverse numeric movement constants from extracted Client 5-8 GML and pin into `packages/game-logic`. Files to read (canonical refs below): `extracted/client-5-8/scripts/0285-player_update.gml`, `extracted/client-5-8/scripts/0347-iv_keyactions.gml`, `extracted/client-5-8/scripts/0355-pcode_mover.gml`, **plus the player object's GML events at `extracted/client-5-8/objects/0000-server/`** (⚠ in original BNO the player character object is named `server` — this is the canonical mover, NOT `0042-player` or any `navi*` script). Derive: walk speed (px/tick), key-buffering window, friction/accel if any, sub-pixel accumulator behavior, tick rate assumption. Update `PredictionEngine` + `step()` to use these constants. Add unit tests pinning the derived values (regression guard for any future tweak). **No asset dependency** — runs alongside #4 in parallel waves.
  - **Plan B (depends on Plan A + Finding #4):** 8-direction sprite-state machine — facing direction (D/U/L/R/DR/DL/UR/UL) + walk-cycle phase derived from velocity vector + cycle counter. Maps onto the player sprite atlases that #4 lands. Frame-perfect to BNO (consult `0000-server` events for facing-on-input semantics). Sequenced AFTER Plan A (numerics) AND #4 (atlases land).

### Finding #4 — No real BNO assets — first-pass (critical)

- **D-27 (player sprites + 1 room = BNCentral):** First-pass real assets =
  - **Player sprite set:** all 8-direction Stand + Run sprites — `extracted/client-5-8/sprites/0027-NaviRunD/`, `0028-NaviRunR/` *(R-facing Run; corrects original enumeration omission flagged by plan-checker 2026-05-10)*, `0029-NaviStandR/`, `0030-NaviStandU/`, `0031-NaviRunU/`, `0032-NaviRunL/`, `0033-NaviStandL/`, `0036-NaviStandDR/`, `0037-NaviRunDR/`, `0038-NaviStandUR/`, `0039-NaviRunUR/`, `0040-NaviStandUL/`, `0041-NaviRunUL/`, `0042-NaviStandDL/`, `0043-NaviRunDL/`, plus the existing `0000-NaviStandD/` (already on disk). Total: 16 sprites (1 already-on-disk + 15 new). Bridge through the existing `tools/asset-pipeline/` plumbing (06-08 built the lint + manifest schema; never piped real input). Add a `bootstrap` mode (per existing CONTEXT D-13 reference) that reads `extracted/client-5-8/sprites/<id>/frames/img_*.bmp` + `meta.json` → emits PNG strips + Aseprite-compatible JSON sidecars at `assets/source/sprites/<id>.{png,json}`. Then `build` mode packs into `atlas-mvp.{png,json}`. Phaser loads via existing PlayerRenderer (replaces blue rectangle).
  - **Room:** `extracted/client-5-8/rooms/0058-BNCentral/` — **the actual latest online lobby iteration** in original BNO (NOT `0004-Online_Lobby`, which is older). Bridge `instances.json` + `tiles.json` + `creation-code.gml` + `backgrounds.json` into a server-emittable room-layout JSON consumable by `apps/server/rooms/<room>/000.json` and the Phase 4 SRV-13 contract + Phase 6 D-12 hot-reload protocol. Replaces the synthetic `mvp-lobby` room used in 06-07. Tile sprites needed for BNCentral come along with the bootstrap (the room's `tiles.json` references sprite IDs already covered by extracted/sprites/).
  - **Replaces** the `mvp-lobby` placeholder. Server room registration updated to use `bn-central` (or planner's preferred slug); Playwright e2e (06-08 + new D-25 tests) updated to assert `data-room-id="bn-central"`.
  - **Nametag (user-supplied 2026-05-10):** original BNO renders player display name as a cyan text label always-on above the sprite head. First-pass: cyan (`#22D3EE` accent already in UI-SPEC, or exact BNO cyan if extractable from `0000-server` draw events / `scr_draw*` text-color calls — verify before locking). Anchored to sprite top-center, follows movement, visible for self + remotes. Belongs to player-render plan (06-16 sprite-state Plan B is the natural home since it owns the per-player render path), not a separate plan. Confirm exact font/size/outline against extracted draw code before commit.
  - First-pass scope deliberately excludes audio (Phase 7 AST-02), fonts (Phase 7 AST-03), and the other 9 extracted rooms (Phase 7 PAR-03).

### Sequencing (advisory — locked at /gsd-plan-phase 6 time)

| Wave | Plan | Finding | Depends on |
|------|------|---------|------------|
| 8 | 06-10: LoginScene contrast fix + Playwright assertion | #1 | — |
| 8 | 06-11: Esc menu logout (incl. game-view click) | #5 | — |
| 9 | 06-12: /gsd-debug session for reconnect/cookie blank-render | #2 | — |
| 9 | 06-13: Reverse movement numeric constants from extracted GML (Plan A) | #3a | — |
| 10 | 06-14: Asset-pipeline bootstrap → atlas-mvp + BNCentral room layout | #4 | — |
| 11 | 06-15: Reconnect/cookie blank-render fix + 2 e2e tests + stale-dispatcher-room fix | #2 | 06-12 (debug findings) |
| 11 | 06-16: 8-direction sprite-state machine (Plan B) | #3b | 06-13 + 06-14 |
| 12 | 06-17: Re-run 06-HUMAN-UAT.md, capture real `06-CLI-08-milestone.mp4` | all | 06-10..06-16 |

Wave 8 and 9 plans run in parallel within their wave (no shared files). Wave 10 (06-14) sequential — touches asset pipeline + server room registration. Wave 11: 06-15 then 06-16 (serial, shared apps/client/src/scenes/GameScene.ts — 06-15 owns the init/create/connect/cookie-path edits + InputDispatcher.setRoom; 06-16 owns the PredictionEngine sim-tick callback wiring. Parallel execution would merge-conflict on GameScene.ts; serialised via 06-16 depends_on: [06-13, 06-14, 06-15] — see revision fix WARNING 1, 2026-05-12). Wave 12 sequential (final UAT + close-out).

### Plan-tagging guidance for /gsd-plan-phase 6

- 06-10 → `[<doc>->REQ-CDOC-*]` + `[<impl/unit>->REQ-CLI-06]` (visual/contrast under HiDPI umbrella) — or expand REQ list if a fresh contrast REQ-ID added.
- 06-11 → `[impl->REQ-CLI-02]` + `[impl->REQ-CLI-03]` (logout is auth lifecycle).
- 06-12 + 06-15 → `[impl/int->REQ-CLI-09]` (reconnect grace) + `[impl->REQ-CLI-04]` (re-entry render). 06-15 also folds in the secondary defect surfaced by `/gsd-debug` (`GameScene.ts:238-252` `if (!this.inputDispatcher)` guard keeps stale `room` reference after silent_reauth; movement intents would `room.send` through closed old room). Fix: refresh dispatcher's room ref on every reconnect, not only on first connect. Add unit test: dispatcher `.room` matches `GameScene.room` after silent_reauth cycle.
- 06-13 + 06-16 → `[impl/unit->REQ-CLI-04]` (movement prediction; numerics + sprite-state).
- 06-14 → `[impl->REQ-AST-01]` + `[impl->REQ-CLI-07]` (room rendering); evaluate whether AST-02..04 stay Phase 7 or partially absorb here (planner call).
- 06-17 → `[doc->REQ-CLI-08]` (final milestone artifact).

</gap_closure_decisions>

<canonical_refs_addendum>

### Added 2026-05-10 (gap-closure)

- `.planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-HUMAN-UAT.md` — operator UAT findings; **MUST read** before planning gap-closure.
- `extracted/client-5-8/objects/0000-server/` — original BNO player character object (named `server`, NOT `player`); contains the canonical mover events for Finding #3.
- `extracted/client-5-8/scripts/0285-player_update.gml` — player tick/update logic.
- `extracted/client-5-8/scripts/0347-iv_keyactions.gml` — input-vector keyactions mapping.
- `extracted/client-5-8/scripts/0355-pcode_mover.gml` — mover pcode (per-tick movement application).
- `extracted/client-5-8/rooms/0058-BNCentral/` — first-pass room (the actual latest online lobby iteration, replaces synthetic `mvp-lobby`).
- `extracted/client-5-8/sprites/0000-NaviStandD/` through `extracted/client-5-8/sprites/0043-NaviRunDL/` — full 8-direction Stand+Run player sprite atlases (BMP frames + meta.json) + tiles + doors needed for BNCentral.
- `tools/asset-pipeline/` (existing 06-08 plumbing) — bootstrap mode addition needed; `build` mode already shipped.

</canonical_refs_addendum>

<gotchas_addendum>

### Added 2026-05-10 (gap-closure)

- **Player object naming:** BNO names the player character object `server` (`extracted/client-5-8/objects/0000-server/`), not `player`. `0042-player` is something else (likely lobby/session-related). Movement-fidelity work MUST consult `0000-server` events.
- **Room slug:** original BNO uses `BNCentral` (room id 58); the room `0004-Online_Lobby` (id 4) is older. Use `0058-BNCentral` for first-pass real-asset work.
- **Asset pipeline plumbing exists:** 06-08 shipped `tools/asset-pipeline/` (lint + manifest schema + build mode) but never piped real input. Don't rebuild — extend with `bootstrap` mode that consumes `extracted/client-5-8/sprites/<id>/frames/*.bmp`.
- **`mvp-lobby` was synthetic:** 06-07 GameScene + Playwright assertions reference `mvp-lobby` room id. Updating to `bn-central` requires coordinated changes in `apps/server/rooms/`, `apps/client/src/scenes/`, and 06-08's `cli-08.e2e.test.ts` selectors.

</gotchas_addendum>

---

*Gap-closure addendum captured: 2026-05-10*
*Operator: decidel*
*Next step: `/gsd-plan-phase 6` to scaffold plans 06-10..06-17 per sequencing table above.*

---

# Review-Driven Addendum (2026-05-11)

**Trigger:** `/gsd-review 6` produced `06-REVIEWS.md` (commit `32f7a46`) — Codex flagged 3 HIGH concerns against the 06-10..06-17 scaffold. User accepted findings and issued scope overrides during `/gsd-plan-phase 6 --reviews`.

<review_driven_decisions>

### D-28 — Locked floor tile dimensions: **44 × 40 px** (NOT 32×32)

The world floor tile in BN Online is **44 px wide × 40 px tall**. Source of truth: `extracted/client-5-8/sprites/0023-Tile1/meta.json` (`width: 44, height: 40, bboxRight: 43, bboxBottom: 39`). Companion `0024-TSide1` is 44×4 (same 44-px column pitch, thin edge trim).

**Locked in three places** so it cannot drift:
- Project `CLAUDE.md` → "Extracted Constants" table (always loaded by every agent)
- `docs/extracted-engine/scene-room-model.md` → "Canonical floor tile" section (canonical subsystem doc)
- This CONTEXT.md decision (this-phase planner + executor consume verbatim)

**Implications for Phase 6 planning:**
- A 20×20-tile MVP room = **880 × 800 px** (20·44 by 20·40).
- View/camera = **640 × 480** (~14.5 × 12 tiles — fractional, do not round).
- Atlas frame slicing for `Tile*` / `TSide*` / `HBorder` family uses 44×40 — read from `meta.json`, never hard-code.

### D-29 — 06-14 scope reduction: 20×20 synthetic MVP room, NOT full BNCentral

Codex HIGH findings #1 and #2 against 06-14:
1. Plan combines BMP bootstrap + atlas build + BNCentral conversion + signing + server registration + render + e2e in one wave — schema assumptions un-proven (`meta.json`/`tiles.json` field names).
2. Full BNCentral replacement = Phase-7-shaped work (collision, logic objects, backgrounds, spawn semantics).

**User override (2026-05-11):** Drop the BNCentral conversion from Phase 6. 06-14 scope becomes:
- **Asset-pipeline bootstrap (KEEP):** real player Navi sprites (the 16 listed in D-27) bootstrap → atlas-mvp.{png,json}. PlayerRenderer replaces blue rectangle.
- **Synthetic MVP room (REPLACES BNCentral conversion):** a **20×20-tile floor square = 880 × 800 px** using `0023-Tile1` (or equivalent floor sprite from extracted/sprites/) tiled in a uniform grid, with a 1-tile-thick wall border so the player has visible boundaries. Server-emitted as a small `apps/server/rooms/mvp-room/000.json` consumable by SRV-13 + D-12 hot-reload.
- **Player-following camera (MANDATORY):** the camera follows the player using BNO's view semantics (`hBorder: 304, vBorder: 224` from the BNCentral meta — pin these or derive equivalents). View viewport stays 640×480. Camera must visibly pan when the player walks toward the edge of the room — verified in e2e.
- **Schema discovery pre-task:** before any code, the plan runs a one-shot read of `extracted/client-5-8/sprites/0023-Tile1/meta.json` + `0027-NaviRunD/meta.json` and pins the actual JSON field names into the plan's `<read_first>` block. No assumed field names.

**BNCentral defers to Phase 7 PAR-03** (the other 14 rooms already deferred there — BNCentral joins them). The synthetic 20×20 room is sufficient to close CLI-08 (two players move + chat in same room over deployed server).

**Slug:** `mvp-room` (NOT `bn-central` — that name is reserved for the Phase 7 real-asset port). Update 06-08 e2e selectors + 06-15 D-25 e2e tests to `data-room-id="mvp-room"`.

### D-30 — 06-15 dependency: depends_on includes 06-14

Codex HIGH finding #3: 06-15 front matter listed only `[06-12]`, but its e2e tests run against GameScene's current room path which after Wave 10 is `mvp-room`. Encode `depends_on: [06-12, 06-14]` in 06-15 frontmatter, OR make the tests room-agnostic (assert against `data-room-id` attribute rather than `mvp-room` literal). Planner picks one and applies consistently.

### D-31 — 06-16 animation phase driven by simulation ticks, NOT renderer frames

Codex MEDIUM: `deriveFrame()` advancing per render call runs 2× on Phaser's 60 FPS vs BNO's 30 FPS room speed. Tie animation cycle advance to **simulation tick count** (`PredictionEngine` tick or `step()` accumulator), not Phaser's render frame callback. Cross-reference BNO `image_speed` from extracted player object draw events to determine frames-per-tick.

### D-32 — 06-13 movement-constant scope widened

Codex MEDIUM: speed/friction/accel alone may not produce BNO-faithful feel. Planner must include in 06-13's reverse-engineering pass:
- Diagonal-movement normalization (does BNO normalize diagonal speed or accept the 1.414× advantage?)
- Collision-step behavior (`move_contact_solid` or equivalent — pixel-by-pixel collision resolution)
- Keyboard-repeat / input-buffering window
- Sub-pixel accumulator vs. integer-pixel snap

If any of these are absent from `0000-server` events / `pcode_mover.gml`, document the absence in the plan's research notes and proceed with documented assumption.

### D-33 — 06-10 contrast test: verify selectors + actual ratio

Codex MEDIUM: test assumed `label[for="username"]` and `#EF4444 on #0A0E1A`. Planner must:
- Inspect actual LoginScene HTML markup before writing the selector (`label[for=...]` vs wrapping `<label>` vs aria-labelledby).
- Verify `#EF4444` (or whatever error-text color UI-SPEC actually locks) meets WCAG AA 4.5:1 against the background BEFORE locking it in the assertion. Use axe-core or a manual contrast calc.

### D-34 — 06-11 logout cookie assertion + click-to-menu guards

Codex MEDIUM (logout): Better-Auth session cookies are likely `HttpOnly` — `document.cookie` is the wrong source. Use `page.context().cookies()` and assert `expect(sessionCookies).toHaveLength(0)`, not `.every(c => !c.value)`.

Codex MEDIUM (click-to-menu UX risk): canvas single-click opening the Esc menu can interfere with future mouse interactions. The Esc-menu decision is user-locked (D-24) but the planner must add suppression guards: do NOT open menu while pointer-lock active, while chat input has focus, while ForceResetOverlay is up, while ReconnectBanner is up, or while the canvas click landed on an interactive sub-element (if/when those exist).

### D-35 — e2e canvas-pixel assertions: prefer deterministic test hooks

Codex MEDIUM: Phaser WebGL canvas-pixel sampling is fragile across drivers/headless. Planner adds (or extends) a dev/test-mode-guarded `window.__rebno` test hook exposing the minimum needed (`localFrame`, `loadedTextures`, room id, etc.) and asserts against that. One screenshot artifact per test is fine; pixel-equality is not.

### D-36 — 06-17 trace + verify gate order

Codex MEDIUM: 06-17 must run `pnpm trace:check` AND `pnpm verify:phase-6` AFTER updating the HUMAN-UAT.md doc and committing the milestone video, not only before. The post-update run is the actual close-out gate.

### D-37 — 06-17 milestone video → Git LFS by default

Codex LOW: even sub-50 MB `.mp4` bloats repo history. The 06-CLI-08-milestone.mp4 commit uses Git LFS (`git lfs track "*.mp4"` if not already). Verify `.gitattributes` covers it before committing.

### D-38 — trace tag IDs must be declared in `traceable-reqs.toml`

Codex LOW: `[<doc>->REQ-CDOC-04]` and `[<doc>->REQ-CDOC-*]` referenced in plan-tagging guidance must already exist as canonical IDs in `traceable-reqs.toml`. Planner verifies via `pnpm trace:list` before pinning tag references in plan frontmatter / acceptance criteria.

</review_driven_decisions>

<sequencing_unchanged>

Wave structure from 2026-05-10 addendum is unchanged — only **scope inside 06-14 shrinks** (synthetic 20×20 room replaces BNCentral conversion) and **dependency metadata in 06-15 expands** to include 06-14.

| Wave | Plan | Change vs 2026-05-10 |
|------|------|----------------------|
| 8 | 06-10 | + D-33 selector + contrast verification |
| 8 | 06-11 | + D-34 logout cookie + click-guard refinements |
| 9 | 06-12 | unchanged (debug session) |
| 9 | 06-13 | + D-32 widened scope (diagonal/collision/buffering) |
| 10 | 06-14 | **MAJOR: D-29 scope reduction — 20×20 synthetic room, not BNCentral; D-28 tile dim locked; schema-discovery pre-task added** |
| 11 | 06-15 | + D-30 depends_on includes 06-14 (or room-agnostic tests) |
| 11 | 06-16 | + D-31 animation tied to sim ticks; + D-35 test hooks |
| 12 | 06-17 | + D-36 gate order; + D-37 LFS for mp4; + D-38 trace IDs verified |

</sequencing_unchanged>

---

*Review-driven addendum captured: 2026-05-11*
*Operator: decidel*
*Source: `06-REVIEWS.md` (Codex review, commit 32f7a46) + user scope overrides during `/gsd-plan-phase 6 --reviews`*
*Next step: planner regenerates 06-10..06-17 incorporating D-28..D-38; tile dims (44×40) and the 20×20 synthetic room scope are LOAD-BEARING.*
