# Phase 06.1: gap-closure D-39..D-46 (UAT 2026-05-11) - Context

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

<domain>
## Phase Boundary

Close the 8 UAT findings from 2026-05-11 (D-39..D-46) AND ship the BNO-canonical MVP visuals/gameplay surfaced during this discussion: animated `bkdraw` scrolling background, `depth_set` z-sort, `TSide1` bottom-edge sides, `NaviMask` feet-bbox collision, walkable-region edge-block. After 06.1, re-run `06-HUMAN-UAT.md` Test 2 (CLI-08 milestone) and re-evaluate Tests 1/3/4/5.

**In scope:**
- D-39 alpha-keying in asset-pipeline (BMP→PNG)
- D-40 floor-tile rendering in `RoomRenderer` (root-cause investigation + fix)
- D-41/D-42 sim-tick-driven animation (local + remote callsite contract)
- D-43 movement smoothness (falls out of D-41/D-42 + camera deadzone)
- D-44 single-speed run + `Shift`-stand input model
- D-45 nametag font/color/position/shadow
- D-46 camera follow + unbounded + roundPixels
- MVP background — port `0051-bkdraw` with BNCentral params
- `depth_set` port — per-step player depth + per-create tile/side depth
- `0024-TSide1` bottom-edge sides
- `NaviMask` 18×7 feet-bbox collision + per-axis sub-pixel collision
- Walkable-region edge-block (modern replacement for per-tile-object borders)
- Asset-pipeline bootstrap extended to backgrounds (`0064-BKA1` 55-frame atlas)
- Atlas + `pipeline-manifest.json` regeneration committed by the fixing plan

**Out of scope (deferred to Phase 7):**
- Fall-off-edge logic + Digital_Abyss transition + `duo-net` recovery
- Ice tiles, movement tiles (`mtile1*`), Hexport, sliding sprites
- Per-tile-object border spawning (legacy heavy mechanism — replaced by walkable-region mask)
- Multi-room transitions, friends list, ignore/block, channels, profanity wordlist
- Full chat surface beyond MVP `chat_send` / `chat_msg`
- All sprites and rooms beyond the mvp-lobby slice + BKA1 + Navi*

</domain>

<decisions>
## Implementation Decisions

### D-39 — transparency keying (asset-pipeline BMP→PNG)

- **D6.1-01:** Two-path pipeline. **Bootstrap path** (legacy BMP→PNG): canonical GameMaker rule — read meta.json `transparent` flag; when `true`, sample top-left pixel of frame 0 as key color and replace exact RGB matches with `alpha=0`; when `false`, emit fully opaque. **Build path** (Aseprite export PNG→atlas): trust the embedded alpha channel as-is. Two distinct codepaths in `tools/asset-pipeline/src/bootstrap.ts` vs `build.ts`.
- **D6.1-02:** Exact RGB match in bootstrap (BMPs are 8-bit paletted, no anti-alias fringe). No tolerance band.
- **D6.1-03:** Regression guard = unit test per sprite-with-`transparent:true`: assert (a) output PNG has alpha channel, (b) zero pixels match the sentinel RGB at full alpha. Runs in `pnpm test`.
- **D6.1-04:** The plan re-runs `pnpm asset-pipeline bootstrap` + `build` and commits regenerated `assets/source/sprites/**`, `pipeline-manifest.json`, and atlas PNG in the same plan. CI picks up via existing deploy-staging workflow.

### D-44 / D-41 / D-42 / D-43 — speed + animation + input

- **D6.1-05:** Single-speed run. Rename `WALK_SPEED_PX_PER_TICK = 3` → `RUN_SPEED_PX_PER_TICK = 5` in `packages/game-logic/src/constants.ts`. Drop walk entirely. Citation: `extracted/client-5-8/objects/0000-server/events/KeyPress-82.gml:5-12`.
- **D6.1-06:** Animation rate = canonical formula `image_speed = curspeed / 10`. Store `curspeed` as a per-entity value (default 5, but reservable for SRV-14 moving platforms / conveyors which can override). `SpriteStateMachine` advances frame index by `curspeed / 10` per sim-tick. Citation: `0000-server/events/Other-7.gml:4`.
- **D6.1-07:** Sim-tick callback is the single animation-advance contract for both local and remote sprites. `PlayerRenderer.onSimulationTickLocal` and `onSimulationTickRemote` both fire once per 30 Hz sim-tick (NOT per Phaser 60 Hz render frame). Closes D-41 (local stuck — was missing the tick) and D-42 (remote too fast — was on render-frame). D-31 lock is re-enforced.
- **D6.1-08:** D-43 (choppy movement) falls out of D-41/D-42 fixes + D-46 camera fix. No separate plan needed; verify in UAT re-run.
- **D6.1-09:** Input model. Drop the legacy `Ctrl+R` walk/run toggle entirely. Hold-`Shift` + direction = face-without-moving (replaces legacy `Alt`-held stand semantics — operator preference). Plain WASD or arrows = run at `RUN_SPEED_PX_PER_TICK`. Wire in `InputDispatcher`.

### D-45 — nametag (Fixedsys)

- **D6.1-10:** Font = **Fixedsys Excelsior** WOFF2 via CSS `@font-face` (free Fixedsys recreation). Loaded from `apps/client/public/assets/fonts/`. Renders through Phaser canvas text. Fallback `monospace` if WOFF2 fails.
- **D6.1-11:** Color rule = local cyan / remote white. Local-player nametag drawn in `c_aqua` (#00FFFF); remote-player nametags in `c_white` (#FFFFFF). Citations: `0000-server/events/Draw.dnd.json` (local: `font_color = c_aqua`) and `0042-player/events/Draw.dnd.json` (remote: `font_color = c_white`).
- **D6.1-12:** Size + shadow: 9-10 px, 1 px solid-black drop-shadow at offset `(1, 1)`. Deviation from pure BNO (no shadow originally) — operator-requested for legibility on light tiles.
- **D6.1-13:** Position math = exact BNO: `text_x = sprite.x + (sprite_width/2) - (string_width(name)/2); text_y = sprite.y - 16`. Anchor baseline 16 px above sprite top.

### D-46 — camera

- **D6.1-14:** Camera deadzone = **32×32 px** per BNCentral view semantics (`hBorder=304, vBorder=224`, `viewW=640, viewH=480` → deadzone = `viewW - 2·hBorder = 32`, `viewH - 2·vBorder = 32`). `cameras.main.setDeadzone(32, 32)`. Instant follow (`hSpeed=-1, vSpeed=-1` → no smoothing lag). Citation: `extracted/client-5-8/rooms/0058-BNCentral/meta.json` views[0].
- **D6.1-15:** Root-cause fix: defer `cameras.main.startFollow(localSprite, true)` until **after** local `PlayerRenderer` creates the sprite (await sprite-ready signal). Re-bind in 06-15's cookie-resume self-heal path when the sprite is recreated. Add a Playwright assertion: after 200 ms of held movement input, `camera.scrollX` or `scrollY` must change.
- **D6.1-16:** Camera bounds = **unbounded**. NO `setBounds()` call. The world should never feel like it has "edges." Combined with the bkdraw scrolling background (D6.1-17..19), the player's view past the room edge shows infinite background tile (no void). `cameras.main.setRoundPixels(true)` required for HiDPI nearest-neighbor.

### MVP background — port `0051-bkdraw`

- **D6.1-17:** Port the `0051-bkdraw` object behavior to a Phaser renderer (`apps/client/src/render/BackgroundRenderer.ts`). Reads sprite `0064-BKA1` (32×32, **55-frame** animation, `transparent: false`). Tiles across the full viewport (plus one tile of bleed in each direction) every render-tick.
- **D6.1-18:** Scroll = BNCentral instance creation code params: `dxspeed = 0.25, dyspeed = -1`. Per-sim-tick: `dxoff += dxspeed`, `dyoff += dyspeed`. Wrap when `|dxoff| >= sprite_width` (or `sprite_height` for dy). Citation: `extracted/client-5-8/rooms/0058-BNCentral/instances.json` (instanceId 128992) + `0051-bkdraw/events/Step.gml`.
- **D6.1-19:** Frame animation = `image_speed = 0.25` per BKA1 sprite. Frame index advances 0.25 per sim-tick (7.5 fps at 30 ticks/s). Wraps `bgframes`. Citation: `0051-bkdraw/events/Create.gml:2`.
- **D6.1-20:** Asset-pipeline bootstrap extension: extend `bootstrap.ts` to also process backgrounds — sprite `0064-BKA1` enters the atlas as 55 frames, alpha-untouched (`transparent: false` → no keying).

### depth_set — z-sort port

- **D6.1-21:** Port `extracted/client-5-8/scripts/0354-depth_set.gml` formula. For single-floor MVP (drop `floor`), Phaser depth = `-(1000 * layer - (y + yOffset))` (sign-flipped because Phaser higher-depth = on top, GameMaker lower-depth = on top).
- **D6.1-22:** `PlayerRenderer.onSimulationTick(Local|Remote)` calls `sprite.setDepth(...)` each tick with `layer = 0, yOffset = 43`. Citation: `0000-server/events/Step.gml:5` (`depth_set(0, 43)`).
- **D6.1-23:** `RoomRenderer` sets tile depth at create with `layer = 2, yOffset = 0`; tile-side depth with `layer = 3, yOffset = 0`. Citations: `0020-borderedtile/events/Create.gml:2` (`depth_set(2, 0)`), `0021-tside1/events/Create.gml:2` (`depth_set(3, 0)`).
- **D6.1-24:** Layer/yOffset values live inline in each renderer file (no shared registry) for MVP. Migrate to a `depth-registry.ts` in Phase 7 when more entity types ship.

### TSide1 — bottom-edge tile sides

- **D6.1-25:** Port bottom-edge sides only (top/left/right border sprites are commented out in 5-8 GML — do NOT reactivate). For each floor tile whose bottom-neighbor cell is NOT a floor tile, `RoomRenderer` places a `0024-TSide1` sprite (44×4 px) at `(tile.x, tile.y + 40)`. Citations: `0020-borderedtile/events/Create.gml:3` (`tileborder(tside1)`) + the tileborder script body (`extracted/client-5-8/scripts/[tileborder].gml`).
- **D6.1-26:** TSide1 sprite enters the atlas via asset-pipeline bootstrap (same alpha rule — `transparent: false` so no keying needed).

### NaviMask — collision bbox

- **D6.1-27:** Player collision bbox = NaviMask `(left=9, top=40, right=26, bottom=46)` — an 18×7 feet rectangle at sprite bottom-center. Replace any current full-sprite bbox usage in `packages/game-logic/src/step.ts`. Add unit test pinning the bbox values to `0034-NaviMask/meta.json`. Citation: `extracted/client-5-8/sprites/0034-NaviMask/meta.json` + `0000-server/events/Step.gml:228+` (movement collision uses `x+9, y+39, x+26, y+47` derived from NaviMask + 1 px cushion).

### Per-axis sub-pixel collision + walkable-region edge-block

- **D6.1-28:** Port BNO's per-axis 1-px sub-step movement loop. For each pixel of intended movement: check x±1 against the walkable region using NaviMask bbox; advance or block. Then same for y. Yields wall-slide along diagonals at floor-region corners. Citation: `0000-server/events/Step.gml:224-242`.
- **D6.1-29:** Replace BNO's per-tile-spawned border objects (legacy heavy mechanism) with a modern derived **walkable-region mask**. At room load, `RoomRenderer` (or a new `RoomCollision.ts`) scans the tile grid and computes the union of floor-tile rectangles. Per-axis sub-step collision checks NaviMask feet bbox against this union mask. When player tries to walk past the edge of the walkable region, movement on that axis is blocked. Falls / Digital_Abyss / dafalls counter deferred to Phase 7.

### Test coverage strategy

- **D6.1-30:** Mixed coverage:
  - **Unit tests:** asset-pipeline alpha-key (D-39), `RUN_SPEED_PX_PER_TICK=5` + Shift-stand input semantics (D-44), `SpriteStateMachine` frames-per-tick = curspeed/10 (D-41/D-42), `0354-depth_set` formula port, `BackgroundRenderer` scroll/wrap math, `NaviMask` bbox values, walkable-region edge-block.
  - **Playwright e2e:** camera.scrollX/Y changes after movement input (D-46), nametag visible above sprite with correct color rule (D-45), floor tiles rendered with bottom-edge sides on outer cells (D-40 + D6.1-25), local + remote anim rate visually consistent (D-41/D-42 smoke).
  - **Manual UAT re-run:** `06-HUMAN-UAT.md` Tests 1–5 on staging, final gate. Capture `06.1-CLI-08-milestone.mp4` if green.

### Claude's Discretion

- Wave / plan sequencing — planner picks. Likely waves: (1) asset-pipeline bootstrap + atlas regen (D-39, BKA1, TSide1) and game-logic constants (D-44 RUN=5, NaviMask bbox, per-axis collision, walkable-region) in parallel — both no shared files; (2) PlayerRenderer + SpriteStateMachine (D-41/D-42, depth-on-player), Nameplate (D-45), BackgroundRenderer (bkdraw), RoomRenderer (D-40 + tile depth + TSide1) in parallel; (3) GameScene wire-up (camera D-46 + InputDispatcher Shift-stand D-44); (4) Playwright + UAT re-run.
- D-40 root-cause investigation depth — planner picks: read-only spike vs binary-search in execute. UAT lists three candidate causes (Ed25519 verify, schema dual-union, atlas frame key lookup) — pick the cheapest to verify first.
- Background scroll formula details (bleed-tile count, integer-vs-float offset, fractional offset rendering) — planner picks. Lock against the per-sim-tick advance.
- Aseprite round-trip semantics for D-39 — bootstrap output PNG must be Aseprite-openable; planner verifies via fixture or assertion.
- Phaser depth math sign — confirm Phaser ordering at implementation time; flip the formula if wrong.
- `c_aqua` / `c_white` Phaser color constants — planner picks `0x00FFFF` / `0xFFFFFF` or named CSS color.
- Background renders past room edges — planner picks: (a) draw bkdraw before world, no clip; (b) negative-z bkdraw under everything; (c) full-viewport background layer with `scrollFactor` math.

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### UAT source + Phase 6 prior context
- `.planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-HUMAN-UAT.md` §"Re-Run 2026-05-11" — D-39..D-46 source, severity, suggested fix areas
- `.planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-CONTEXT.md` — Phase 6 locked decisions (D-31 sim-tick lock, D-08 input_axes, D-10/D-11 prediction/reconcile/extrapolation, D-05 nameplates)
- `.planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-PATTERNS.md` — existing renderer patterns
- `.planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-RESEARCH.md` — Phaser 3.90 patterns, Colyseus prediction

### Movement, animation, input (D-44, D-41, D-42, D-43)
- `extracted/client-5-8/objects/0000-server/events/Create.gml` — `if(global.curspeed == 0) global.curspeed = 3; image_speed = 1;`
- `extracted/client-5-8/objects/0000-server/events/Step.gml` — `depth_set(0, 43)`, full collision math at lines 224+
- `extracted/client-5-8/objects/0000-server/events/Other-7.gml` — `image_speed = global.curspeed/10` (canonical anim formula)
- `extracted/client-5-8/objects/0000-server/events/KeyPress-82.gml` — Ctrl+R toggle (drop in rebno); curspeed=5 = canonical run value
- `extracted/client-5-8/objects/0000-server/events/Keyboard-37.gml`, `Keyboard-38.gml`, `Keyboard-39.gml`, `Keyboard-40.gml` — direction input + `vk_alt`-held = stand-in-place (rebno replaces with `vk_shift`)
- `packages/game-logic/src/constants.ts` — current `WALK_SPEED_PX_PER_TICK=3`; rename to `RUN_SPEED_PX_PER_TICK=5`
- `packages/game-logic/src/step.ts` — current step() impl; update to NaviMask bbox + per-axis sub-pixel collision

### Nametag (D-45)
- `extracted/client-5-8/objects/0000-server/events/Draw.dnd.json` — local nametag: `font_name="fixedsys", font_color=c_aqua, draw_text(x+(sprite_width/2)-(string_width(global.playername)/2), y-16, global.playername)`
- `extracted/client-5-8/objects/0042-player/events/Draw.dnd.json` — remote nametag: `font_name="fixedsys", font_color=c_white`, same position math
- `apps/client/src/render/Nameplate.ts` — existing file; rewrite per D6.1-10..13

### Camera (D-46)
- `extracted/client-5-8/rooms/0058-BNCentral/meta.json` — views[0]: `viewW=640, viewH=480, hBorder=304, vBorder=224, hSpeed=-1, vSpeed=-1, objectFollowing=0` — deadzone derivation
- `apps/client/src/scenes/GameScene.ts` — existing camera wiring; defer startFollow per D6.1-15

### Background (port `0051-bkdraw`)
- `extracted/client-5-8/objects/0051-bkdraw/events/Create.gml` — `if(image_speed == 1) image_speed = 0.25; if(dscale == 0) dscale = 1;`
- `extracted/client-5-8/objects/0051-bkdraw/events/Draw.gml` — tile-across-viewport math with `dxoff, dyoff`
- `extracted/client-5-8/objects/0051-bkdraw/events/Step.gml` — `dxoff += dxspeed` wrap when `> sprite_width*dscale`; `bgframe += bgspeed` wrap
- `extracted/client-5-8/objects/0051-bkdraw/meta.json` — `spriteId: 64` (BKA1 default)
- `extracted/client-5-8/sprites/0064-BKA1/meta.json` — `width=32, height=32, transparent=false`
- `extracted/client-5-8/sprites/0064-BKA1/frames/` — 55 frames (`img_000.bmp` … `img_054.bmp`)
- `extracted/client-5-8/rooms/0058-BNCentral/instances.json` — first instance (objectId 51 = bkdraw, x=88, y=6160, `creationCode: "dyspeed = -1; dxspeed = 0.25;"`)
- `extracted/client-5-8/rooms/0058-BNCentral/backgrounds.json` — room-level bg config disabled in favor of bkdraw object

### Depth (z-sort)
- `extracted/client-5-8/scripts/0354-depth_set.gml` — canonical formula `depth = round(((10000 * -floor) + (1000 * layer)) - (y + yOffset))`
- `extracted/client-5-8/objects/0020-borderedtile/events/Create.gml` — `depth_set(2, 0); tileborder(tside1);`
- `extracted/client-5-8/objects/0021-tside1/events/Create.gml` — `depth_set(3, 0);`

### Tile sides (TSide1)
- `extracted/client-5-8/sprites/0024-TSide1/meta.json` — `width=44, height=4, transparent=false`
- `extracted/client-5-8/scripts/[tileborder].gml` — body in earlier grep result: `if((!tbottomcheck(tile1) ...) && argument0 != -1) instance_create(x, y+40, argument0);` (find exact file under `extracted/client-5-8/scripts/` named `*tileborder*`)

### Collision (NaviMask)
- `extracted/client-5-8/sprites/0034-NaviMask/meta.json` — `width=36, height=48, bboxLeft=9, bboxTop=40, bboxRight=26, bboxBottom=46, transparent=true`

### Asset pipeline (D-39, BKA1 ingestion, TSide1 ingestion)
- `tools/asset-pipeline/src/bootstrap.ts` — current BMP→PNG path; extend per D6.1-01..04 + add backgrounds support per D6.1-20
- `tools/asset-pipeline/src/build.ts` — Aseprite-PNG→atlas path; ensure trusts embedded alpha
- `apps/client/public/pipeline-manifest.json` — committed manifest; regenerated by plan
- `apps/client/public/assets/` — committed atlas + manifest output

### Renderer + scene
- `apps/client/src/render/PlayerRenderer.ts` — sim-tick callsite, anim advance, depth setter
- `apps/client/src/render/SpriteStateMachine.ts` — frame-per-tick math
- `apps/client/src/render/RoomRenderer.ts` — floor tiles + sides + per-tile depth + walkable-region derivation
- `apps/client/src/render/Nameplate.ts` — font/color/pos
- `apps/client/src/scenes/GameScene.ts` — camera follow + InputDispatcher
- `apps/client/src/net/InputDispatcher.ts` — Shift-stand semantics

### Project rules + constants
- `CLAUDE.md` — hard rules (server-authoritative, extract→document→rewrite, 44×40 tile pitch); constants table
- `.planning/codebase/CONCERNS.md` — risks ahead of planning
- `decomp/wiki/00-overview.md` — extraction conventions; cross-reference if any uncertainty about extracted GML semantics

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `packages/game-logic/src/step.ts` — pure `step(state, inputs, dt) → state`; lint-enforced purity (no `Date.now`, no I/O); runs in Node + browser identically (Phase 4 D-20)
- `packages/game-logic/src/constants.ts` — current `WALK_SPEED_PX_PER_TICK=3` + diagonal `/√2` rule + extensive GML citations; well-structured for rename to `RUN_SPEED=5`
- `apps/client/src/render/PlayerRenderer.ts` + `SpriteStateMachine.ts` — 06-16 introduced these; sim-tick wiring exists but D-31 lock regressed (D-41/D-42); the callsite contract is the actual fix
- `tools/asset-pipeline/src/bootstrap.ts` — 06-14 introduced; sharp pipeline already runs; extend with alpha-key path + background ingestion
- `apps/client/src/render/RoomRenderer.ts` — 06-14 + 06-15 introduced; needs root-cause for D-40 (no tiles rendered) + extension for TSide1 + tile depth
- `apps/client/src/scenes/GameScene.ts` — 06-15 + 06-17 wired camera + cookie-resume; D-46 = race against sprite create
- `apps/client/src/render/Nameplate.ts` — 06-07 introduced; rewrite per D-45 spec
- `apps/server/public/atlas-mvp.png`, `atlas-mvp.json`, `pipeline-manifest.json` — generated artifacts committed to apps/server/public/; regenerated by plan
- CSP hotfix `6dc1f81` already allows `blob:` img-src — atlas blob loads fine

### Established Patterns
- **Sim-tick callsite contract (D-31):** Animation + depth + render-position updates all fire from the 30 Hz simulation tick, NOT from Phaser's 60 Hz render loop. Re-enforce in D6.1-07 + D6.1-22.
- **Extract → document → rewrite:** Every constant (RUN=5, NaviMask bbox, depth formula, bkdraw params) is cited to extracted GML and committed alongside a unit test pinning it. CLAUDE.md hard rule #6.
- **Phase 4 D-08 schema:** Server broadcasts `input_axes` for remote players; client uses them for extrapolation. No protocol change needed here.
- **Phase 4 SRV-13 room wire format:** room layouts live on server, signed with Ed25519, hot-reload via `RoomRegistry`. mvp-lobby is already signed and shipped — D-40 root cause may be client-side verify, schema, or atlas key lookup (UAT lists all three).

### Integration Points
- `packages/game-logic/step.ts` (pure deterministic) used by both `apps/server/src/RebnoRoom.ts` (server tick) AND `apps/client/src/sim/PredictionEngine.ts` (client prediction). All collision-bbox + speed changes must apply identically in both. NaviMask bbox is per-entity data; pass into step() not hardcoded.
- `apps/client/src/net/InputDispatcher.ts` wires WASD/arrows + Shift-stand → c2s.input intents. Server already accepts the same axes (server doesn't care about Shift, just receives `(ax, ay)` axes).
- Asset-pipeline bootstrap output → committed to `assets/source/sprites/` AND `apps/server/public/`. Plan regenerates both.
- Phaser 3.90 camera API: `setDeadzone(w, h)`, `setRoundPixels(true)`, NO `setBounds()` (per D6.1-16), `startFollow(target, roundPixels)`.

</code_context>

<specifics>
## Specific Ideas

- Operator referenced the UAT-2 video file at `.planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/uat-test-1-2.mp4` as ground-truth for current broken state.
- Operator clarification 2026-05-11 (in UAT D-44 row): "rebno is single-speed, no Ctrl+R toggle. Drop walk entirely."
- Operator clarification 2026-05-11 (discussion): "MVP also needs a background. BNO backgrounds are animated, tiled, and usually have some slow directional scrolling to them. you should check for the canonical details. BNCentral's background is the one to use for MVP."
- Operator clarification 2026-05-11: "the background draw event might not be in the room files, but tied to an object within the room" — directly led to discovery of `0051-bkdraw`.
- Operator preference: `Shift` replaces `Alt` as the stand-in-place modifier (modern muscle memory).
- Operator preference: world should never feel like it has "edges" — unbounded camera + background renders past room edges.
- "Saber Mage" operator-supplied screenshot referenced in UAT D-45 row defines target nametag visual identity (small cyan pixel-font, above sprite head, with shadow).

</specifics>

<deferred>
## Deferred Ideas

### Phase 7
- Fall-off-edge logic: `tcollide` end-of-step check, `JoinOut` transition, `Digital_Abyss` routing, `global.dafalls` counter, duo-net recovery (Step.gml lines 165–205)
- Ice tiles (`icetile`), movement tiles (`mtile1*`, `mtile3*`), Hexport/HexportIn/HexportOut, JokerShell, slidedir sprite handoff (Step.gml lines 54–95)
- All other rooms beyond mvp-lobby (PAR-03)
- Top/left/right tile borders (commented out in 5-8 GML) — leave commented
- Multi-floor depth (`floor != 0` in `depth_set`) — single-floor for MVP
- `depth-registry.ts` consolidation when more entity types ship (D6.1-24)
- Sprite_index handoff for 8-direction Run/Stand sprite variants (NaviRunUL/UR/DL/DR + matching Stand variants) — current MVP may only ship subset
- Per-tile-object border-spawning legacy mechanism — staying replaced by walkable-region mask (D6.1-29)
- HexportMask + NaviCol secondary collision masks (Step.gml refs them; out of scope without Hexport)
- Aseprite round-trip ergonomics (verify `assets/source/sprites/*.png` opens in Aseprite without artifacts after the D-39 fix) — confirm in plan but don't gate on full Aseprite UX

### Note for Phase 7 backlog
The per-tile-object border mechanism in legacy BNO was replaced in Phase 6.1 by a walkable-region mask derived at room load. When Phase 7 imports the full room set, the same mask-derivation path generalizes — no per-tile object spawn needed. PAR-03 (full room import) should use the walkable-region path, not resurrect the legacy `tileborder()` script.

</deferred>

---

*Phase: 06.1-gap-closure-d-39-d-46-uat-2026-05-11*
*Context gathered: 2026-05-11*
