---
phase: 06-client-rebuild-mvp-gate-cli-08-hard-milestone
plan: "14"
subsystem: asset-pipeline, room-converter, server, client-renderer
tags: [mvp-room, atlas, sprites, camera-follow, phaser-primitive, SRV-13, D-28, D-29, D-35]
dependency_graph:
  requires: [06-08-SUMMARY.md, 06-13-SUMMARY.md]
  provides: [atlas-mvp, mvp-room layout, SRV-13 extended schema, camera follow, __rebno hook]
  affects: [06-15 (e2e selector update), 06-16 (nametag + 8-direction sprites)]
tech_stack:
  added: [tools/room-converter mvp-room subcommand, vitest for room-converter]
  patterns: [Phaser Rectangle primitive for wall border, BNO hBorder/vBorder → Phaser deadzone, canonical JSON (sorted keys)]
key_files:
  created:
    - tools/room-converter/src/mvp-room.ts
    - tools/room-converter/test/mvp-room.test.ts
    - apps/server/rooms/mvp-room/000.json
    - apps/server/rooms/mvp-room/000.sig
    - .planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-14-SCHEMA-DISCOVERY.md
    - .planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-14-ASSET-MANIFEST.md
  modified:
    - tools/asset-pipeline/src/bootstrap.ts
    - tools/asset-pipeline/cli.ts
    - tools/asset-pipeline/test/bootstrap.test.ts
    - tools/room-converter/cli.ts
    - tools/room-converter/package.json
    - tools/scripts/lint-room-layout.mjs
    - packages/protocol/src/intents.ts
    - apps/server/src/RebnoRoom.ts
    - apps/client/src/render/RoomRenderer.ts
    - apps/client/src/scenes/GameScene.ts
    - assets/source/sprites/ (17 .png + .json sprite files)
    - package.json (root)
    - .gitignore
decisions:
  - "Wall border rendered via Phaser Rectangle primitive (#2C3E50) — no extracted 44×40 wall sprite exists (0024-TSide1=44×4, 0025-VBorder=1×40, 0026-HBorder=44×1 are thin trims)"
  - "Synthetic 20×20 MVP room (880×800 px) replaces planned BNCentral conversion per D-29 scope reduction — BNCentral defers to Phase 7 PAR-03"
  - "Atlas key = 'atlas-mvp' loaded by BootScene pipeline-manifest path; GameScene does not re-load"
  - "layoutSchema in @rebno/protocol split into legacyLayoutSchema + newLayoutSchema union to accept both SRV-13 shapes"
  - "tools/room-converter extended (not replaced by new tools/room-builder/) per revision fix BLOCKER 1"
  - "Camera deadzone = (32, 32) derived from BNO hBorder=304/vBorder=224: dzW = max(0, 640-608)=32, dzH = max(0, 480-448)=32"
metrics:
  duration: "~3h"
  completed_date: "2026-05-10"
  tasks_completed: 3
  files_changed: 22
---

# Phase 6 Plan 14: Schema Discovery + Asset Bootstrap + MVP Room + Client Renderer Summary

Synthetic 20×20 MVP room (880×800 px, 44×40 D-28 floor tile, Phaser-primitive wall border) + 16 Navi sprites bootstrapped + client camera-follow + window.__rebno test hook delivered. UAT Finding #4 (D-27) closed with D-29 scope reduction.

[doc->REQ-AST-01] [doc->REQ-CLI-07]

---

## What Was Built

### (a) Schema Discovery (Task 0)

`06-14-SCHEMA-DISCOVERY.md` pins actual extracted JSON field names from read meta.json files:
- Sprite meta: `width` / `height` / `originX` / `originY` (NOT `frame_w` / `origin_x`)
- Room meta: `views[0].hBorder=304`, `views[0].vBorder=224`, `views[0].viewW=640`, `views[0].viewH=480`
- Wall sprite resolution: 0024-TSide1 (44×4), 0025-VBorder (1×40), 0026-HBorder (44×1) — all thin trims, none are full-tile (44×40). **Wall border = Phaser Rectangle primitive, color #2C3E50.**

### (b) Atlas-mvp Dimensions + Sprite Count

Atlas key: `atlas-mvp` (packed by `tools/asset-pipeline/src/build.ts`).

Total: **17 sprites** (16 Navi + 1 floor tile):
- 0000-NaviStandD (1 frame), 0027-NaviRunD (2), 0028-NaviRunR (2), 0029-NaviStandR (1), 0030-NaviStandU (1), 0031-NaviRunU (2), 0032-NaviRunL (2), 0033-NaviStandL (1)
- 0036-NaviStandDR (1), 0037-NaviRunDR (2), 0038-NaviStandUR (1), 0039-NaviRunUR (2), 0040-NaviStandUL (1), 0041-NaviRunUL (2), 0042-NaviStandDL (1), 0043-NaviRunDL (2)
- 0023-Tile1 (1 frame, 44×40 px)

Atlas PNG dimensions: see `tools/asset-pipeline/output/atlas-mvp.json`.

No wall sprite in atlas — wall border is Phaser Rectangle.

### (c) MVP Room Layout

| Parameter | Value |
|-----------|-------|
| room_id | `mvp-room` |
| Grid | 20 × 20 tiles |
| World | 880 × 800 px |
| Tile dimensions | 44 × 40 px (D-28 locked) |
| Interior floor tiles | 324 (18×18, x∈[1..18], y∈[1..18]) |
| Wall border | Phaser primitive (#2C3E50), thickness_tiles=1, 4 edges |
| home_portal spawn | (440, 400) — room center |
| Background | #000000 |
| Camera hBorder | 304 px (from BNCentral views[0].hBorder) |
| Camera vBorder | 224 px (from BNCentral views[0].vBorder) |
| Viewport | 640 × 480 px |
| Phaser deadzone | (32, 32) = (640-2×304, 480-2×224) |

Generated at: `apps/server/rooms/mvp-room/000.json` + `000.sig` (signed with project-root Ed25519 key, passes `lint:room-layout`).

### (d) BNCentral Deferred

BNCentral (0058-BNCentral, 8000×6400 px world) is **NOT shipped here**. The 2026-05-10 scope of "convert BNCentral end-to-end" was reduced per D-29 because collision/logic-object/background semantics are Phase-7-shaped. BNCentral and the other 14 extracted rooms defer to Phase 7 PAR-03. Faithful wall art also defers (Phase 7 PAR-03).

### (e) window.__rebno Hook Fields (D-35)

Exposed in `GameScene.update()` gated by `import.meta.env.DEV || import.meta.env.MODE === 'test'`:

| Field | Type | Description |
|-------|------|-------------|
| `window.__rebno.roomId` | string | Current room ID (`'mvp-room'`) |
| `window.__rebno.cameraScrollX` | number | `cameras.main.scrollX` in world coords |
| `window.__rebno.cameraScrollY` | number | `cameras.main.scrollY` in world coords |
| `window.__rebno.localPlayerX` | number | Local player sprite X (predicted position) |
| `window.__rebno.localPlayerY` | number | Local player sprite Y |
| `window.__rebno.loadedTextures` | string[] | Phaser texture keys present (`textures.getTextureKeys()`) |

`localFrame` (current sprite frame key) is added in 06-16 when the 8-direction sprite-state machine lands.

### (f) Tooling: EXISTING tools/room-converter Extended

`tools/room-converter` was extended (NOT replaced by a new `tools/room-builder/` directory — revision fix BLOCKER 1). New `mvp-room` subcommand added alongside existing `convert` / `edit` / `verify`.

Root package.json entries follow the established `cd tools/<x> && pnpm <script>` pattern:
- `"room-converter:mvp-room": "cd tools/room-converter && pnpm mvp-room"`
- `"room-converter:test": "cd tools/room-converter && pnpm test"`

`pnpm --filter` is **never used** — pnpm-workspace.yaml excludes `tools/*`.

### (g) Pointers to Downstream Plans

- **06-15**: E2E selector update (`data-room-id="mvp-room"`), camera-pan assertion using `window.__rebno.cameraScrollX` hook
- **06-16**: D-27a cyan nametag above player, 8-direction Navi sprite-state wiring (`localFrame` added to `__rebno`)

### (h) Trace Tag Inventory

| Tag | Stage | Files |
|-----|-------|-------|
| `[impl->REQ-AST-01]` | impl | tools/asset-pipeline/src/bootstrap.ts, tools/room-converter/src/mvp-room.ts, apps/client/src/scenes/GameScene.ts, apps/client/src/render/RoomRenderer.ts |
| `[impl->REQ-CLI-07]` | impl | tools/room-converter/src/mvp-room.ts, tools/room-converter/cli.ts, apps/server/src/RebnoRoom.ts, apps/client/src/render/RoomRenderer.ts, apps/client/src/scenes/GameScene.ts |
| `[unit->REQ-AST-01]` | unit | tools/asset-pipeline/test/bootstrap.test.ts, tools/room-converter/test/mvp-room.test.ts |
| `[doc->REQ-AST-01]` | doc | 06-14-SCHEMA-DISCOVERY.md, 06-14-ASSET-MANIFEST.md |
| `[doc->REQ-CLI-07]` | doc | 06-14-SCHEMA-DISCOVERY.md, 06-14-ASSET-MANIFEST.md |

Full `pnpm trace:check` gate runs in 06-17 post-update close-out (D-36).

---

## Commits

| Hash | Task | Description |
|------|------|-------------|
| `48ff008` | Task 0+1 | Schema discovery doc + asset-pipeline bootstrap for 16 Navi + Tile1 |
| `7d25d4e` | Task 2 | Synthetic 20×20 MVP room builder + server wiring |
| `37d8199` | Task 3 | Client atlas render, RoomRenderer dual-schema, camera follow, __rebno hook |

---

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] Room signed with wrong key on first run**
- **Found during:** Task 2 run step
- **Issue:** `pnpm mvp-room` from `tools/room-converter/` CWD resolves the default key path `./keys/rebno-room-signing.ed25519` relative to CWD, generating a local key pair separate from the project-root `keys/`. The generated 000.sig failed `lint:room-layout` because lint reads the project-root public key.
- **Fix:** Re-ran `pnpm mvp-room --key ../../keys/rebno-room-signing.ed25519 --out ../../apps/server/rooms`; added `.gitignore` entry for `tools/room-converter/keys/`; root `room-converter:mvp-room` script uses project-relative paths.
- **Files modified:** `apps/server/rooms/mvp-room/000.json`, `000.sig`, `.gitignore`

**2. [Rule 2 - Missing feature] Atlas frame suffix `_000` vs plan's `_0`**
- **Found during:** Task 3 integration check
- **Issue:** Plan spec used `'0000-NaviStandD_0'` but the actual atlas emits `_000` (3-digit padding). PlayerRenderer already used `'0000-NaviStandD_000'` correctly from a prior plan; no change needed.
- **Fix:** No code change — verified existing convention is correct.

**3. [Rule 2 - Missing feature] GameScene already loads atlas via BootScene pipeline-manifest**
- **Found during:** Task 3 (plan said add `this.load.atlas('mvp', ...)` to GameScene.preload())
- **Issue:** BootScene.create() already calls `fetchPipelineManifest()` → `queueAtlasLoads()` → `setNearestFilterAll()` for all atlases before GameScene starts. Adding a redundant atlas load in GameScene.preload() would double-load the texture.
- **Fix:** Did NOT add a redundant preload in GameScene. The atlas `'atlas-mvp'` is already present in the texture cache when GameScene.create() runs.

**4. [Rule 1 - Bug] RoomLayout interface in RoomRenderer was legacy-only**
- **Found during:** Task 3 (client type check)
- **Issue:** Existing `RoomLayout` export was typed as the legacy shape only. Adding new shape required splitting into `LegacyRoomLayout | NewRoomLayout` union and adding discriminator function.
- **Fix:** Rewrote RoomRenderer.ts with union type + `isNewLayout()` discriminant + `renderNew()` / `renderLegacy()` split. All 106 client tests pass.

---

## Known Stubs

None. All plan deliverables are wired with real data:
- Atlas: 17 real sprites bootstrapped from extracted BMP frames
- Room: generated from real tile dimensions (44×40)
- Server: `MVP_ROOM_ID='mvp-room'`, registry reads real 000.json
- Client: renders real atlas frames; camera follow uses real layout.viewport params

---

## Self-Check

**Created files verified:**

- `apps/server/rooms/mvp-room/000.json` — exists, lint:room-layout OK
- `apps/server/rooms/mvp-room/000.sig` — exists
- `tools/room-converter/src/mvp-room.ts` — exists
- `tools/room-converter/test/mvp-room.test.ts` — exists, 11 tests pass
- `.planning/phases/.../06-14-SCHEMA-DISCOVERY.md` — exists
- `.planning/phases/.../06-14-ASSET-MANIFEST.md` — exists

**Commits verified:**
- `48ff008` — Task 0+1
- `7d25d4e` — Task 2
- `37d8199` — Task 3

**Typechecks:** @rebno/client, @rebno/server, @rebno/protocol — all pass

## Self-Check: PASSED
