# Project Research Summary — v1.1 Map Groundwork

**Project:** REBNO — Rebuild BN Online
**Milestone:** v1.1 Map Groundwork (LDtk authoring + map-deploy + in-game minimap)
**Domain:** 2D web MMO map-authoring toolchain + fast-deploy + in-game minimap, layered onto the live Phase 06.7 v1.0 stack
**Researched:** 2026-05-18
**Confidence:** HIGH overall

---

## Executive Summary

v1.1 is an **additive content-tooling milestone** on top of a validated v1.0 stack — not a rewrite. The editor (LDtk 1.5.3) was pre-locked in `.planning/notes/map-editor-decision.md`; research confirms the surrounding ecosystem with three small npm additions (`ajv@8.17.1`, `chokidar@4`, `quicktype@23.2.6` dev-only) plus one piece of custom code we own outright (a Phaser-side `LdtkLoader` against QuickType-generated `LdtkJson.ts` types, since no maintained third-party Phaser LDtk plugin exists — `ldtk-ts` was archived 2022). Everything else reuses v1.0 primitives: the `tools/room-converter` Ed25519-signing CLI is extended (not forked), the Phase 06.5 `/data/<channel>/current` symlink-swap pattern is replicated on a second channel `/data/maps/current`, and a new pure-data `packages/map-loader` package mirrors the `@rebno/game-logic` discipline so the same parser runs in converter, client, and (in the future Phase 06.8) server.

**Recommended approach:** decompose the milestone into 5–6 phases ordered by dependency-respecting risk-front-loading: (1) workflow smoke-test that locks the BNCentral chunking + gridSize conventions *before* any conversion happens, (2) `packages/map-loader` pure-data parser + Phaser `LdtkLoader` + hand-authored `TestBed_001`, (3) `tools/room-converter ldtk-import` extension with `__legacyCreationCode` verbatim preservation, (4) `/data/maps/current` symlink-swap deploy pipeline (replicating `client-release.sh` verbatim), (5) `MinimapHUD` overlay container inside GameScene (RenderTexture-based, NOT a second camera that would conflict with ADR 0008's Scale.NONE invariant), then (6) one real imported zone end-to-end as the milestone-exit HUMAN-UAT gate. Server stays **map-agnostic** in v1.1 — LDtk parsing never reaches the server; that work is deferred to Phase 06.8 anti-cheat. This preserves the Ed25519 manifest-sig security boundary established in ADR 0004.

**Key risks and mitigations:** (a) **D-63 origin-helper bypass** — a naïve loader will read LDtk's `__pivot` field and feed it to `setOrigin()`, silently breaking alignment with player-attached effects; mitigated by routing all entity placements through `apps/client/src/render/legacy-origin.ts` and extending the existing HARD gate. (b) **Atlas ↔ LDtk tileset drift** — two source-of-truth chains exist (atlas regen vs `.ldtk` tile-coord snapshot); mitigated by a `tilesetSourceHash` custom field asserted at loader init. (c) **Phase 06.5 symlink-swap gap repeat** on the maps axis — naïve `rsync` to `/data/maps/current/` reintroduces the exact failure mode of the resolved `fly-fullpath-deploy-data-symlink-gap` memory; mitigated by generalizing `scripts/client-release.sh` with a `--kind {client,maps}` flag rather than authoring a new script. (d) **BNCentral authoring lag** triggering premature "LDtk doesn't work for us" — mitigated by smoke-testing the chunked workflow on a synthetic 8000×6400 zone *before* converting any real zone. (e) **Minimap O(N) per-frame iteration** echoing the `per-tick-log-freeze-pattern` memory — mitigated by event-driven Colyseus subscriptions + a RenderTexture static layer baked once per layout swap. (f) **Per-instance `creationCode` data loss** — mitigated by always preserving the GML verbatim in a `__legacyCreationCode` opaque string field; *no* typed parsing in v1.1 (explicit anti-feature).

---

## Key Findings

### Recommended Stack

See `.planning/research/v1.1/STACK.md` for full version pins, alternatives matrix, and integration-point inventory.

v1.1 is a **stack delta**, not a re-evaluation. The validated v1.0 stack (Node 22, Colyseus 0.17.42, Phaser 3.90.0, Vite 8.0.11, msgpackr 1.11.10, Drizzle, Better-Auth, better-sqlite3, Litestream → Tigris, Fly.io single machine, pnpm workspaces) is frozen and out of scope. v1.1 adds three runtime/dev libraries, one regeneration tool, one piece of custom code we own outright, plus an authored-content editor that lives only on the operator's desktop.

**Core stack additions (new in v1.1):**

- **LDtk 1.5.3** (desktop app, operator-machine only, NOT a runtime dependency) — authoring editor, pre-locked in `map-editor-decision.md`
- **QuickType-generated `LdtkJson.ts`** (vendored, regenerated via `quicktype@23.2.6` dev-only on LDtk schema bumps) — official LDtk-documented loading path; the `ldtk-ts` npm package is archived since 2022
- **`ajv@8.17.1`** — validates `.ldtk` files against the canonical JSON schema at commit + load time
- **`chokidar@4`** (NOT 5 — Vite 8 transitively pins 4; mismatch creates dual watcher instances) — file-watch for Vite dev HMR plugin + publish CLI
- **Custom `apps/client/src/render/LdtkLoader.ts`** (we own this, ~1 task per editor-decision) — Phaser scene-side loader; no maintained third-party Phaser LDtk plugin exists
- **No new persistence layer, no new crypto lib, no new transport.** Map releases reuse SQLite + Drizzle + Litestream + Ed25519 (`keys/rebno-room-signing.ed25519`) verbatim.

**Topology additions:**
- `packages/map-loader/` — new pure-data package (mirrors `@rebno/game-logic` discipline: no Phaser, no fs, no DOM)
- `packages/protocol/src/ldtk/` + `tools/room-converter/src/ldtk/` — vendored `LdtkJson.ts` in both locations (standalone-CLI rule from Phase 1 D-18 requires the duplication)
- `maps/` — new top-level dir for `.ldtk` authoring source (sibling to `decomp/`, `extracted/`, `legacy/`; deliberately NOT under `apps/client/public/` because that would defeat the content/code split)
- `apps/server/src/maps-dir.ts` — env-driven `MAPS_DIR` resolver mirroring `static-assets.ts`
- `scripts/client-release.sh` — generalized with `--kind {client,maps}` flag (do NOT duplicate the script)

### Expected Features

See `.planning/research/v1.1/FEATURES.md` for the full four-area landscape + dependency graph + REQ-MAP-* candidates.

**Must have (table stakes, all P1):**
- Phaser-side `LdtkLoader` proven against hand-authored `TestBed_001` (20×20 BORDERED tiles, 880×800 px per the 44×40 extracted constant — replaces the v1.0 synthetic MVP room)
- `gridSize` convention ADR (recommended: gridSize=4 for IntGrid/Tile floor layers, gridSize=1 for Entity layers — accommodates 44×40 non-square pitch without scaling)
- Vite HMR plugin watching `.ldtk` files → Phaser scene reload (~1-day task, same pattern as Excalibur's official LDtk plugin)
- `tools/room-converter` extended with `ldtk-import` subcommand emitting signed `rooms/<id>/<rev>.{json,sig}` from `.ldtk` sources; verbatim `__legacyCreationCode` preservation; per-zone `conversion-report.json` listing all non-cell-aligned source instances
- `/data/maps/current` symlink-swap deploy via generalized `client-release.sh --kind maps` (atomic `mv -T`, releases retention, `MAPS_DIR` env resolver)
- `MinimapHUD` overlay in GameScene with player marker + current-zone pre-baked PNG geometry layer (mirrors ChatHUD lifecycle pattern)
- One real imported zone end-to-end (recommend `Online_Lobby` — smallest world room; `Prairie_Flats` acceptable backup)
- v1.0 synthetic MVP room code path removed (one source of room truth = LDtk)
- HUMAN-UAT round on deployed staging — two players walk + chat on LDtk-loaded room (same gate shape as CLI-08)

**Should have (differentiators, P2 — promote to P1 if scope allows):**
- Multi-zone overworld minimap using LDtk multi-worlds `worldX/worldY` adjacency — this is **the headline LDtk capability** that made it win over Tiled; zero re-architecture cost since minimap consumes the same `.ldtk` artifact
- Peer-player markers + POI markers on minimap (Colyseus state stream already broadcasts player positions; zero server load)
- Auto-tile rule generation for DYNAMIC tile families (Floes-of-Ghennam ice/water/snow etc.)
- Bake-step human-readable map diff (entity count delta, tileset changes) in commit message + console
- HEXPORTER/WARP/HEXSLING-specific `creationCode` parsing (the ONE place narrow GML parsing is warranted — runtime-load-bearing)
- All ~16 zones imported (full BNO catalogue)

**Defer (v2+, explicit anti-features):**
- Typed parsing of arbitrary `creationCode` GML into LDtk fields (multi-week semantics-replication project; belongs in runtime layer)
- Bidirectional round-trip (`.ldtk` → re-write `extracted/`) — creates competing sources of truth
- Fog-of-war / visited-area persistence (requires new schema + Litestream replication + explicit gameplay rule decision)
- Live in-game map editing — reimplements LDtk inside the game
- Server-side per-tick map mutation API — competes with file-based git review
- Database-backed `.ldtk` storage — opaque to LDtk editor on re-open
- Click-to-fast-travel from minimap — implicates server-side travel rules
- Map hot-push to connected clients without reload — conflicts with client-authoritative-movement carve-out
- Auto-deploy on `.ldtk` commit — CI is currently out of service per `LOCAL-DEPLOY.md`
- Replicating GameMaker per-instance `depth` as `F_Int` — wedges GM mental model into LDtk; use layer stacking instead

### Architecture Approach

See `.planning/research/v1.1/ARCHITECTURE.md` for the full integration-point inventory + new file/dir map + 9-task build order + 5 anti-patterns.

**Integration strategy: additive, not invasive.** v1.0 already has every load-bearing primitive v1.1 needs — `tools/room-converter` (CLI + Ed25519 signing + atomic write + numeric `nextRev`), `apps/server/src/RoomRegistry` (fs.watch + sig-verify + zod parse + broadcast), `apps/client/src/render/RoomRenderer` (two-shape layout discriminator), `scripts/client-release.sh` (atomic `mv -T` symlink swap), `apps/server/src/static-assets.ts` (env-driven resolver with bundled-fallback). v1.1 adds a **third layout shape** (LDtk-derived) to the `layoutSchema` union, a **new symlink-swap channel** for maps independent of the client-asset channel, and **two new client-only components** (LDtk JSON runtime loader + minimap overlay).

**Major architecture decisions:**

1. **`maps/` is top-level** (sibling to `decomp/`, `extracted/`, `legacy/`) — mirrors content vs. code split. NOT `apps/client/public/maps/` (Vite would bundle into image, defeating Phase 06.5 split). NOT `packages/maps/` (`.ldtk` is content, not importable TS).
2. **Extend `tools/room-converter`** with `ldtk-import` subcommand — Ed25519 + atomic-write + signing key is non-trivial and already battle-tested. NO `tools/ldtk-import/` fork.
3. **New `packages/map-loader` pure-data package** — three downstream consumers (converter, client, future server-side anti-cheat) need the same parser; inlining in `apps/client/src/render/` forces a future copy + drift.
4. **Server stays map-agnostic in v1.1** — movement is client-authoritative (06.7 carve-out); server has no `walkable_grid` validation reason yet. Phase 06.8 lifts `deriveWalkable(tiles[])` from `map-loader` as a 5-line wire-up.
5. **New `/data/maps/current` symlink swap, independent of client bundle** — editing a map should NOT require a Vite rebuild (~30-60s); independent swap gives map-only deploys at ~5s.
6. **`MinimapHUD` = overlay container in GameScene** (NOT a separate Phaser Scene) — mirrors `ChatHUD.ts` lifecycle pattern; RenderTexture-based, NOT a second `cameras.add(...)` which would interfere with ADR 0008's Scale.NONE + manual `setZoom` invariant.
7. **Build order:** workflow smoke → map-loader scaffold → ldtk-import subcommand → smoke-test layout E2E → protocol schema extension → maps-release.sh pipeline → MinimapHUD → BNCentral chunking → remaining zones.

**Three reusable patterns:**
- Pattern A: Three-shape schema union with passthrough derivation
- Pattern B: Pure-data package (no Phaser/fs/DOM) mirroring `@rebno/game-logic`
- Pattern C: Independent symlink-swap channels (`/data/<channel>/current`)

### Critical Pitfalls

See `.planning/research/v1.1/PITFALLS.md` for the full 10-item catalog.

Top 5 driving roadmap shape:

1. **D-63 origin-helper bypass in the custom LDtk loader** — LDtk's `__pivot` is editor-cosmetic, not runtime guidance. Loader places entities at `pxX/pxY` then routes through `legacy-origin.ts`; floor tiles use `setOrigin(0,0)`. Loader unit test asserts pivot fields are discarded. Extend D-63 HARD-gate to forbid `__pivot` reads outside the loader.
2. **Atlas ↔ LDtk tileset hash drift** — `tilesetSourceHash` custom field asserted at loader-init time as a hard error. Single `pnpm asset:atlas-regen-then-relink` script. Map + atlas deploy as one atomic release directory.
3. **Phase 06.5 symlink-swap gap recurs on maps axis** — generalize `client-release.sh --kind {client,maps}`. Reuse `keys/rebno-room-signing.ed25519` verbatim. Mid-session map swap kicks affected players to lobby with toast.
4. **BNCentral 8000×6400 authoring lag** — smoke-test GridVania chunked workflow on synthetic 8000×6400 zone BEFORE converting any real zone. Pre-define chunk grid (4×4 or 2×2 chunks of ≤2000×1600 px). Pin LDtk version in `tools/ldtk-version.txt`.
5. **Minimap O(N) per-frame iteration** — RenderTexture static layer baked ONCE per layout swap; dynamic layer subscribes to Colyseus add/remove/change events. Dev-mode perf assert: minimap update path <0.5 ms steady-state.

Additional:
6. Per-instance `creationCode` GML — preserve verbatim in `__legacyCreationCode: F_Text`; per-objectId mapping table grows iteratively.
7. Multi-worlds JSON schema break — loader MUST hard-assert on `worlds[]` presence.
8. Minimap second-camera approach conflicts with ADR 0008 Scale.NONE invariant — RenderTexture is the ONLY correct path.

---

## Implications for Roadmap

Based on combined research, **6 phases** recommended for v1.1. Phase numbering starts at **Phase 7** per PROJECT.md.

### Phase 7.1 — Workflow Smoke + Convention Locks (research-flagged)

Front-load risk on early-HARD-gate decisions. ADR for gridSize convention (gridSize=4 for IntGrid/Tile, gridSize=1 for Entity). ADR for BNCentral GridVania chunk grid. `tools/ldtk-version.txt` pin. `pnpm preflight` script substituting missing CI. `tilesetSourceHash` custom-field convention documented.

### Phase 7.2 — Map-Loader Package + Phaser LdtkLoader + TestBed_001 (research-flagged)

`packages/map-loader/` with QuickType-generated `LdtkJson.ts`, `parseLdtk()`, `levelToCanonicalLayout()`, `deriveWalkable()`, IntGrid auto-tile rule evaluator, entity-mapping skeleton. `apps/client/src/render/LdtkLoader.ts` routing through `legacy-origin.ts`. Hand-authored `TestBed_001.ldtk` (20×20 BORDERED, 880×800 px). `packages/protocol/src/intents.ts` extended with `ldtkLayoutSchema` union. RoomRenderer third-shape branch. v1.0 synthetic MVP room removed. HUMAN-UAT round.

### Phase 7.3 — Room-Converter ldtk-import Extension (research-flagged)

`tools/room-converter` extended with `ldtk-import <ldtk-file> <world-name>`. Vendor `LdtkJson.ts` into `tools/room-converter/src/ldtk/` (D-18 standalone-CLI rule). Verbatim `__legacyCreationCode` preservation. Per-objectId mapping table; unmapped → generic `LegacyInstance` flagged in conversion-report. `conversion-report.json` sidecar. `room-converter verify` accepts LDtk-derived rooms. Deterministic round-trip via `hash(roomId, instanceId)` UIDs.

### Phase 7.4 — Map-Deploy Pipeline

`scripts/client-release.sh --kind {client,maps}` generalized. `apps/server/src/maps-dir.ts` mirroring `static-assets.ts`. `MAPS_DIR=/data/maps/current` wired in `fly.staging.toml`. `RoomRegistry` boots on both `ROOMS_DIR` + `MAPS_DIR`. `/data/maps/releases/<sha>/` + atomic `mv -T` symlink, last N=5 retention. Vite HMR plugin watching `.ldtk` files. `/health/maps` endpoint. `docs/deploy/MAPS-DEPLOY.md` runbook.

### Phase 7.5 — MinimapHUD (RenderTexture-based)

`apps/client/src/ui/MinimapHUD.ts` overlay container in GameScene. RenderTexture static layer baked on `s2c.room_layout` change. Local + remote player dots via Colyseus state subscriptions. M-key toggle + localStorage persistence. Main camera `ignore()`s the minimap container. Dev-mode perf assert <0.5 ms. (P2 if scope allows) Multi-zone overworld view reading `worlds[].levels[].worldX/worldY`.

### Phase 7.6 — First Real Imported Zone + Milestone-Exit HUMAN-UAT

Start with `Online_Lobby` (smallest world room) — NEVER BNCentral first. HUMAN-UAT mirrors CLI-08 round: two real accounts, two browsers, movement + chat survive the LDtk-loaded room. `traceable-reqs.toml` updated with all REQ-MAP-XX entries; `pnpm trace:check` passes. Milestone-exit retrospective.

### Phase Ordering Rationale

7.1 front-loads risk. 7.2 precedes 7.3 (loader works against TestBed_001 before importer). 7.3 precedes 7.4 (deploy needs signed `.json` output). 7.4 precedes 7.5 (minimap needs a deployed Level to exercise bake-on-swap). 7.5 before 7.6 (overworld minimap most valuable on real imported zone). 7.6 last (HUMAN-UAT against real data is milestone-exit gate).

### Research Flags

Phases 7.1, 7.2, 7.3 need deeper `/gsd:plan-phase --research-phase N` at plan time.
Phases 7.4, 7.5, 7.6 follow standard or in-repo patterns.

---

## Confidence Assessment

| Area | Confidence | Notes |
|------|------------|-------|
| Stack | HIGH | All package versions verified vs npm 2026-05-18; LDtk 1.5.3 verified vs GitHub releases; QuickType+LdtkJson.ts is the official documented pattern. |
| Features | MEDIUM-HIGH | LDtk + Phaser patterns HIGH (official docs); converter strategies MEDIUM (extrapolated from Tiled/Unity importer analogies); fog-of-war MEDIUM (correctly deferred). |
| Architecture | HIGH | Integration-point identification based on direct v1.0 source inspection (12+ files). MEDIUM only for the converter-extend-vs-fork decision (recommendation: extend). |
| Pitfalls | HIGH | Items grounded in existing code (D-63, 44×40, 06.5, ADR 0008) HIGH; LDtk-specific verified vs GitHub issues + community loaders. |

**Overall confidence: HIGH**

### Gaps to Resolve (12 open questions consolidated)

1. **gridSize=4 vs gridSize=44+10%-Y-scaling vs Entity-only placement** — recommended gridSize=4 + gridSize=1 (entity); validate experimentally. **Resolve at: Phase 7.1 plan.**
2. **RoomRenderer third-branch vs unify-with-renderNew** — depends on shape divergence. **Resolve at: Phase 7.2 plan.**
3. **REQUIRED_LAYOUT_KEYS check in room-converter** — relax D-18 or vendor third local schema. **Resolve at: Phase 7.3 plan.**
4. **RoomRegistry multi-dir scan** — two instances vs constructor accepts dirs[]. **Resolve at: Phase 7.4 plan.**
5. **Minimap RenderTexture resolution per zone** — 1 px/tile vs 4×4 px/tile vs configurable. **Resolve at: Phase 7.5 plan.**
6. **Multi-world overworld minimap promote-to-P1** — gate on prior-phase margin. **Resolve at: Phase 7.5 plan.**
7. **First-zone choice** — `Online_Lobby` recommended, `Prairie_Flats` backup, NEVER BNCentral. **Resolve at: Phase 7.6 plan.**
8. **HEXPORTER/WARP `creationCode` parsing scope** — v1.1 P2 or v1.2. **Resolve at: requirements-definition or Phase 7.3 plan.**
9. **CI restoration as v1.1 in-scope or backlog** — manual `pnpm preflight` substitutes. **Resolve at: requirements-definition.**
10. **D-63 audit script `tooling/no-inline-origin.ts`** — pull forward in v1.1. **Resolve at: requirements-definition.**
11. **`__legacyCreationCode` field name + lint rule** — STRING ARCHIVE ONLY, never `eval()`. **Resolve at: Phase 7.3 plan.**
12. **`MinimapHUD` reveal policy for HIDDEN tiles** — v1.1 render only currently-visible-to-player. **Resolve at: Phase 7.5 plan.**

---

## Sources

### Primary (HIGH confidence)

- LDtk official docs — Loading LDtk, JSON overview, World layout, Auto-layer rules, Entity fields, QuickType integration (ldtk.io/docs/...)
- LDtk multi-worlds — 1.3.0 multi-worlds preview devlog (deepnight.itch.io), wiki (github.com/deepnight/ldtk/wiki)
- LDtk perf issues — deepnight/ldtk#1029, #1073, #985, #1006
- npm package verification (2026-05-18): ajv 8.17.1, chokidar 4.x vs 5.0.0, quicktype 23.2.6
- Phaser docs — minimap camera example, Camera ignore, RenderTexture, Tilemap culling
- Vite — HMR API, hotUpdate plugin hook, Custom-language plugin tutorial
- REBNO repo (direct inspection): CLAUDE.md, .planning/PROJECT.md, .planning/notes/map-editor-decision.md, apps/server/src/{RoomRegistry,layout-derive,static-assets}.ts, apps/client/src/render/{RoomRenderer,RoomCollision,legacy-origin}.ts, apps/client/src/scenes/BootScene.ts, apps/client/src/ui/ChatHUD.ts, tools/room-converter/{cli.ts,src/{convert,types,sign}.ts}, packages/protocol/src/intents.ts, scripts/client-release.sh, apps/server/rooms/mvp-room/000.json, docs/extracted-engine/scene-room-model.md, docs/LEGACY_FEATURE_REFERENCE.md, docs/deploy/LOCAL-DEPLOY.md, extracted/client-5-8/rooms/0058-BNCentral/*, extracted/client-5-8/sprites/0023-Tile1/meta.json

### Secondary (MEDIUM)

- Community LDtk loaders (none official for Phaser): mobilex1122/phaser-ldtk-importer (alpha), Madour/LDtkLoader (C++), Excalibur LDtk plugin
- ldtk-ts archival (2022-02-11, max LDtk 0.7.2)
- Phaser animated tiles plugins; Vite caching pitfalls (Vite #15372)
- ADR 0001/0002/0003/0004/0008 (in-repo)

### Tertiary (LOW)

- LDtk multi-worlds long-term schema stability (experimental)
- Tile pitch gridSize=4 strategy for non-square 44×40 (needs Phase 7.1 experimental confirmation)
- chokidar 4 vs 5 long-term posture
- Operator-machine memory: fly-fullpath-deploy-data-symlink-gap.md, per-tick-log-freeze-pattern.md

---
*Research completed: 2026-05-18*
*Ready for roadmap: yes*

[doc->REQ-DEP-04]
