# Technology Stack — v1.1 Map Groundwork

**Project:** REBNO v1.1 — LDtk authoring + map-deploy pipeline + minimap UI
**Researched:** 2026-05-18
**Overall confidence:** HIGH for additions (3 verified npm packages + 1 vendored generated file); MEDIUM for the Phaser-LDtk loader path (because we will own it ourselves — no maintained third-party exists).

## Scope of this document

This is a **subsequent-milestone** STACK delta. 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 for re-research**. See `.planning/PROJECT.md` § Constraints.

Only the **new pieces required to land LDtk authoring + map-deploy + minimap** are listed below. Anything you don't see here means "reuse the v1.0 stack as-is."

The editor itself (LDtk 1.5.3, installed on the operator's machine — NOT npm) is decided in `.planning/notes/map-editor-decision.md` and is also out of re-evaluation scope.

---

## Recommended Stack Additions

### 1. LDtk authoring + client consumption

| Technology | Version | Purpose | Why |
|------------|---------|---------|-----|
| **LDtk** (desktop app) | **1.5.3** (latest stable, 2025-01-15; 1.5.4 milestone open but unreleased) | Author `.ldtk` projects on operator machine | Decided in map-editor-decision.md. NOT a runtime dependency. |
| **LdtkJson.ts** (QuickType-generated) | Generated from LDtk 1.5.3 JSON schema, vendored under `packages/protocol/src/ldtk/` | TypeScript types matching `.ldtk` JSON shape | LDtk's officially documented loading path. Always current, regenerable, zero runtime dependency. Pattern explicitly endorsed by ldtk.io/docs/game-dev/loading. |
| **quicktype** | **23.2.6** (dev-only, used once per LDtk-schema bump) | Regenerate `LdtkJson.ts` from `LdtkJson.schema.json` | Official LDtk-recommended type-gen tool. Run as `npx quicktype --lang ts --src-lang schema --top-level LdtkProject LdtkJson.schema.json -o LdtkJson.ts`. Output checked into repo; quicktype itself never ships. |
| **ajv** | **8.17.1** | Validate `.ldtk` files against the canonical JSON schema before commit + on load | Fastest JSON-schema validator in node; draft-2020-12 support; type-guard generation matches our zod-bridged TS pattern. Used in `packages/protocol` to gate roundtrip importer output. |
| **chokidar** | **4.x** (stay on 4 NOT 5 — see below) | Watch `apps/client/public/maps/*.ldtk` in Vite dev plugin + watch `packages/maps/` in publish CLI | Industry-standard file watcher. Vite already depends on chokidar 4 transitively; do NOT bump to chokidar 5 (Nov-2025 release, requires Node 20+ ESM-only — Vite 8 has not yet adopted, and matching Vite's pin avoids dual-instance watcher overhead). |

**What we explicitly do NOT add:**

- ❌ `ldtk-ts` (jprochazk) — **archived 2022-02-11**, last release supports LDtk 0.7.2, no 1.5+ support. Listed on ldtk.io/api but stale.
- ❌ `ldtk` npm package — same archived `ldtk-ts`, just the pre-2022 release name.
- ❌ `ldtk-lib` — community fork, no active maintenance, no track record at our scale.
- ❌ `phaser-ldtk-importer` (mobilex1122) — explicit alpha, "currently only maintained by one person" asking for help, no docs. We can read for inspiration but cannot adopt.
- ❌ `@excaliburjs/plugin-ldtk` — high-quality reference for the pattern (parse → tilemap → entity factories) but tightly bound to Excalibur's `TileMap` class; cannot use directly with Phaser. Useful to **read** for our own loader design.
- ❌ Any Tiled tooling (`phaser/tilemap` JSON loader, `@kayahr/tiled`, etc.) — LDtk decision is locked; even though LDtk can export TMX, that path loses LDtk-native features (typed entities, custom-field metadata, IntGrid values) which are load-bearing for our 5 sub-categories.

**Custom code we will own (NOT a library):**

- `apps/client/src/render/LdtkLoader.ts` — Phaser scene-side loader that takes a parsed `LdtkProject` (typed via `LdtkJson.ts`) and produces Phaser tilemaps, sprite groups, and entity instances. Budget: ~1 task in milestone plan per map-editor-decision.md. Pattern reference: Excalibur LDtk plugin source.

### 2. Legacy GameMaker rooms → comprehensive `.ldtk` project (programmatic authoring)

| Technology | Version | Purpose | Why |
|------------|---------|---------|-----|
| **(no new lib)** — programmatic `.ldtk` write via `LdtkJson.ts` types + `JSON.stringify` | — | Synthesize `.ldtk` projects from extracted GM5 `instances.json`/`tiles.json` | LDtk's JSON is the canonical format; round-tripping via the editor's own importer is unsafe (it loses fields). The QuickType types give us a typed builder. **Validate output with ajv against the LDtk schema before write.** |
| **zod** | **3.23.8** (already in client + room-converter) | Schema-validate the GM5-side **input** before mapping | Already in the repo; no need to add. The 5 sub-categories (static floor / animated floor / metadata-tile / interactive / entity) get one zod schema each, validating extracted-room inputs before LDtk-side construction. |
| **tsx** | **4.21.0** (already in room-converter) | CLI runner for the importer | Already in monorepo dev-deps. |
| **vitest** | **3.2.4** (already everywhere) | Golden-file tests: re-import 3-5 reference rooms (Bahoo, Noiya, BNCentral, BNCentral chunks if split) and snapshot the produced `.ldtk` | Already in monorepo. Standard pattern. |

**Where this code lives:**

Extend `tools/room-converter/` rather than spawning a new tool dir. The existing `tools/room-converter/` (inspected — has `cli.ts`, `src/{convert,mvp-room,sign,types}.ts`, Ed25519 signing keys at `keys/`) already owns "extracted GM5 room → canonical layout JSON + signed manifest" for the v1.0 server-side runtime. v1.1 adds a **separate output target**: extracted GM5 room → `.ldtk` project (operator-side authoring input). Two CLI subcommands, one tool:

- `room-converter convert` (existing) — emits server-runtime `CanonicalLayout` JSON
- `room-converter to-ldtk` (NEW) — emits authoring-side `.ldtk` project
- `room-converter from-ldtk` (NEW) — emits server-runtime `CanonicalLayout` JSON **from a `.ldtk` source-of-truth** (this is the eventual replacement for `convert` once authored maps supersede extracted ones)

**Critical**: `src/types.ts` is currently a redeclared-locally `CanonicalLayout` because `room-converter` is standalone-CLI (per Phase 1 D-18 — cannot import workspace packages). The new `.ldtk` types must follow the same rule: vendor `LdtkJson.ts` into the tool dir, do NOT import from `packages/protocol`. The QuickType regeneration script must write to **both** `packages/protocol/src/ldtk/LdtkJson.ts` (client consumption) AND `tools/room-converter/src/ldtk/LdtkJson.ts` (CLI consumption). Same source `.schema.json`, two destinations.

**What we explicitly do NOT add:**

- ❌ A new `tools/ldtk-importer/` directory — duplicates `room-converter`'s purpose, splits the GM5-room domain logic across two trees. **Extend the existing tool.**
- ❌ A new key/signing scheme for `.ldtk` files — `.ldtk` is operator-authored content with operator-side review (PR diff). The Ed25519 signing in `keys/` is for server-runtime hot-reload safety (ADR 0004); authored-source artifacts don't need it.
- ❌ `@deepnight/ldtk-importer` or similar — does not exist; LDtk has no official npm-side importer SDK.

### 3. Map publish + fast-deploy workflow (on top of Phase 06.5 symlink pattern)

| Technology | Version | Purpose | Why |
|------------|---------|---------|-----|
| **chokidar** | **4.x** (see above — shared) | Watch authored `.ldtk` files + asset-pipeline outputs; trigger rebuild of `maps/<sha>.tgz` | Standard pattern, already in v1.0 dev path via Vite. |
| **(no new lib)** — extend `scripts/client-release.sh` for `maps/`-only releases | — | Atomic symlink swap on Fly machine for maps-only updates | The existing Phase 06.5 script already implements the load-bearing pattern: SHA validation, tarball stage, content-addressed release dir, atomic `mv -T` swap, GC by `readlink`. **Generalize it** to accept a payload-type arg (`client`/`maps`) so the same hardened script handles both surfaces. |
| **better-sqlite3** + **Drizzle** | (already pinned in v1.0) | Server-side map-registry table (`map_releases(sha, kind, created_at, active)`) + map-load endpoint | Already in v1.0. **No new persistence layer.** Maps follow the same Litestream → Tigris snapshot path as character data. |
| **Ed25519** signing (Node `crypto.subtle` or `@noble/ed25519`) | Built-in OR **@noble/ed25519 2.x** if we adopt | Sign authored `.ldtk` bundles before publish — server verifies sig before hot-swap | **Reuse the existing `keys/rebno-room-signing.ed25519`** key + `tools/room-converter/src/sign.ts` helper. The ADR 0004 hot-reload contract from v1.0 server already requires signed room manifests; the same contract extends to `.ldtk` bundles. No new crypto lib needed if Node `crypto.sign('ed25519', ...)` suffices — it does (Node 22). |

**Architecture pattern (concrete):**

```
operator-machine                       fly-machine /data/
─────────────────                      ─────────────────
LDtk editor                           /data/maps/
   ↓ save                             ├── releases/<sha-1>/  ← extracted, signed
.ldtk file                            ├── releases/<sha-2>/  ← extracted, signed
   ↓ pnpm maps:publish                └── current → releases/<sha-2>  ← atomic mv -T
        (chokidar watch)
        + room-converter from-ldtk
        + ajv schema validation
        + ed25519 sign
        + tar -czf
        + flyctl sftp put
        + flyctl machine exec
                                      apps/server reads /data/maps/current/
                                      via STATIC_ASSETS_DIR-style resolver
                                      (NEW: MAPS_DIR resolver, same pattern)
```

This is **exactly** the Phase 06.5 client-asset pattern extended to a second payload directory. No new infrastructure primitives.

**What we explicitly do NOT add:**

- ❌ A second Fly volume — `/data` is already mounted, has headroom, and Litestream covers backup.
- ❌ A new content store (S3 direct, R2, etc.) — Tigris snapshots of SQLite + the `/data` volume cover persistence; we don't need a second blob store.
- ❌ A queue / message bus for publish events — single-operator publish at this scale doesn't justify it. Direct `flyctl machine exec` matches the LOCAL-DEPLOY runbook.
- ❌ A CI-side publish workflow — GH Actions storage is dead (`docs/deploy/LOCAL-DEPLOY.md`). Operator-local publish only, mirroring the existing deploy posture.
- ❌ HTTP file-upload endpoint on the game server — every authenticated client + every bug-in-auth becomes an attack surface for arbitrary `/data` writes. SFTP+exec via Fly's auth boundary is correct.

### 4. In-game minimap UI (inside Phaser 3.90)

| Technology | Version | Purpose | Why |
|------------|---------|---------|-----|
| **(no new lib)** — Phaser built-in second `Camera` + `Phaser.GameObjects.RenderTexture` | Bundled with phaser@3.90.0 | Minimap rendering | Phaser has first-class support for minimap-via-second-camera (`phaser.io/examples/v3/view/camera/minimap-camera`) and render-to-texture for masked / styled minimaps. No third-party plugin earns its weight here. |

**Design pattern (concrete, two options — pick at planning time):**

1. **Second Camera** (simplest): `scene.cameras.add(x, y, w, h, false, 'minimap')`, set `.setBackgroundColor`, `.setZoom(0.1)`, `.ignore(/* fullscreen HUD layer */)`, `.setBounds(0, 0, room.w, room.h)`. Renders the same game world at a different scale. Cheap, no asset duplication.
2. **Render Texture** (richer): `scene.add.renderTexture(x, y, w, h)`, draw downscaled tile thumbnails + player dot + POI markers, redraw on tick. Decouples minimap from world camera frustum (lets us show entire zone always, not just visible region) — closer to what `seeds/in-game-minimap.md` describes.

For zone-adjacency overworld (the "theoretical relative placement of zones" half of the seed): read the same `.ldtk` project's `worlds[].levels[].worldX/worldY` directly via `LdtkJson.ts` types, render to a `RenderTexture`. **The minimap consumes the same artifact as the editor** — no second data source.

**What we explicitly do NOT add:**

- ❌ `phaser3-rex-plugins` (Board / MiniBoard) — Rex's Board plugin is a **chess/grid simulation toolkit**, not a minimap. The "MiniBoard" name is misleading; it's a "small board for previewing pieces," not a minimap. Adding the ~1MB Rex bundle for a 30-line built-in solution is wrong.
- ❌ Any third-party HUD/overlay lib — Phaser's UI scene + DOM elements (already in v1.0 via `apps/client/src/ui/ChatHUD.ts`, `EscMenu.ts`) suffice.

---

## Alternatives Considered

| Category | Recommended | Alternative | Why Not |
|----------|-------------|-------------|---------|
| LDtk types | QuickType-generated `LdtkJson.ts` (vendored) | `ldtk-ts` (jprochazk) | Archived 2022, LDtk 0.7.2 max |
| LDtk types | QuickType-generated `LdtkJson.ts` (vendored) | `ldtk-lib`, `phaser-ldtk-importer` | Alpha / single-maintainer / no docs |
| LDtk loader | Custom `LdtkLoader.ts` (~1 task) | `@excaliburjs/plugin-ldtk` | Bound to Excalibur's TileMap; can't reuse with Phaser |
| Schema validation | ajv 8.17.1 | zod | zod is great for runtime TS schemas but does NOT validate against external draft-2020-12 JSON schemas; ajv is the right tool for "LDtk says this schema, does our file conform?" |
| Schema validation | ajv 8.17.1 | `@cfworker/json-schema` | Smaller but slower; ajv is the industry default |
| File watcher | chokidar 4 | chokidar 5 (Nov 2025) | Vite 8 pins chokidar 4 transitively; mismatched majors = two watcher instances |
| File watcher | chokidar 4 | `node:fs.watch` recursive | Cross-platform recursive support still incomplete on Linux as of Node 22; chokidar is the safe choice |
| Map publish CLI | extend `scripts/client-release.sh` | New `scripts/maps-release.sh` | DRY violation; release.sh is hardened (SHA validation, atomic swap, GC) — generalize, don't duplicate |
| Map signing | Reuse `keys/rebno-room-signing.ed25519` | New keypair | ADR 0004's signed-room-registry contract already covers authored map content |
| Minimap | Phaser built-in Camera/RenderTexture | `phaser3-rex-plugins` Board | Board is grid-sim, not minimap; wrong tool |
| Minimap | Phaser built-in Camera/RenderTexture | DOM/canvas overlay | No advantage over Phaser-native; loses access to game-world coordinates |

---

## Installation

```bash
# Dev-only, monorepo root
pnpm add -DwR ajv@8.17.1 chokidar@4

# Dev-only, regenerate-on-bump, NOT a dependency
# (run when LDtk app version bumps and ships a new schema)
pnpm dlx quicktype@23.2.6 \
  --lang ts --src-lang schema --top-level LdtkProject \
  packages/protocol/src/ldtk/LdtkJson.schema.json \
  -o packages/protocol/src/ldtk/LdtkJson.ts
# (then duplicate the output to tools/room-converter/src/ldtk/LdtkJson.ts —
# see the standalone-CLI carve-out reasoning above)

# Operator desktop (NOT npm — pin in docs/contrib/setup.md or equivalent)
# Download from https://ldtk.io/ — version 1.5.3 minimum, app autoupdate OK
```

**Workspace topology** (where these go):

- `packages/protocol/src/ldtk/` — new subdir, holds `LdtkJson.schema.json` (downloaded artifact) + `LdtkJson.ts` (QuickType output). Exported from `packages/protocol/index.ts` so client + server (if needed for server-side validation) can import.
- `tools/room-converter/src/ldtk/` — mirror of the above, vendored per standalone-CLI rule.
- `apps/client/src/render/LdtkLoader.ts` — new, the Phaser-side runtime loader.
- `apps/client/src/ui/Minimap.ts` — new, Phaser Scene + RenderTexture.
- `scripts/client-release.sh` — extend to accept `--kind {client,maps}` arg.
- `tools/room-converter/cli.ts` — add `to-ldtk` + `from-ldtk` subcommands.
- `packages/maps/` (NEW) — authored `.ldtk` source-of-truth files, plus their published-bundle build output. **Sibling to `packages/protocol`**.
- `apps/server/src/maps/` (NEW) — server-side reader that resolves `MAPS_DIR=/data/maps/current` (with dev-mode fallback to `packages/maps/`) and serves map bundles to authenticated clients.

---

## Versioning Discipline

| Artifact | Versioning rule |
|----------|-----------------|
| `LdtkJson.ts` (generated) | Regenerate on every LDtk app major+minor bump; commit the regeneration as a single PR with the schema diff; CI (when restored) runs ajv-validate against all `.ldtk` files. |
| `.ldtk` files | Committed under `packages/maps/`; LDtk auto-stamps `jsonVersion` in each file; ajv check enforces match with vendored schema. |
| Published map bundles | Content-addressed by git SHA at publish time, mirroring `client-assets/releases/<sha>/`. **No separate map versioning.** |
| Ed25519 signing key | **Reuse** the v1.0 `keys/rebno-room-signing.ed25519`. No rotation needed for v1.1; rotation policy is a future-milestone concern. |

---

## Integration Points with Existing Stack

| Existing surface | v1.1 touch |
|------------------|------------|
| `apps/client/src/render/RoomRenderer.ts` | Currently discriminates `LegacyRoomLayout` vs `NewRoomLayout` (Plan 06-14 SRV-13). Add a **third** branch for `LdtkRoomLayout` (or refactor to a stage-0 "any source → canonical IR → renderer" indirection if the third branch makes the discriminator unwieldy). **Decision deferred to planner.** |
| `packages/protocol/src/intents.ts` | `layoutSchema` union — add LDtk-derived shape (or: keep `layoutSchema` as the canonical IR and have the LDtk loader convert, which avoids leaking LDtk types onto the wire). **Prefer the latter** — wire stays LDtk-agnostic, future authoring-tool swaps don't break the protocol. |
| `tools/room-converter/` | Extend with `to-ldtk` + `from-ldtk` subcommands. Standalone-CLI rule (Phase 1 D-18) preserved — vendor types, no workspace imports. |
| `tools/asset-pipeline/` | Out of scope for v1.1; tile-atlas slicing remains driven by extracted `meta.json` (44×40 px constants). LDtk references the produced atlases by relative path; no atlas-pipeline changes needed. |
| `scripts/client-release.sh` | Generalize with `--kind {client,maps}` flag. Hardening (SHA validation, atomic swap, GC) is reused verbatim. |
| `docs/deploy/LOCAL-DEPLOY.md` | Add a "Publishing maps" section mirroring the client-only fast-path, but pointing at `/data/maps/`. |
| `apps/server/src/` | NEW `maps/` module: `MAPS_DIR` resolver, signed-bundle verification, `/v1/maps/manifest` + `/v1/maps/:sha/:level` endpoints. Mirrors STATIC_ASSETS_DIR pattern. |
| `apps/client/src/scenes/GameScene.ts` | Replaces synthesized-MVP-room load with `LdtkLoader.loadLevel('BNCentral')` (or whichever level the active zone resolves to). |
| `packages/db/schema/` (Drizzle) | NEW `map_releases` table — same Litestream → Tigris persistence story as v1.0; no new storage layer. |
| `traceable-reqs.toml` | Will need new REQ-MAP-XX entries when planner decomposes requirements; STACK.md is upstream of that. |

---

## Confidence Notes

- **HIGH** — All package versions (`ajv@8.17.1`, `chokidar@4`, `quicktype@23.2.6`, `phaser@3.90.0` reuse) verified against npm registry pages 2026-05-18.
- **HIGH** — LDtk 1.5.3 as latest stable verified against GitHub releases page 2026-05-18 (1.5.4 milestone open since Feb 2025, ~8% complete, not released).
- **HIGH** — `ldtk-ts` archival status confirmed (archived 2022-02-11, last release supports LDtk 0.7.2).
- **HIGH** — QuickType + LdtkJson.ts pattern is the official LDtk-documented loading path (verified against ldtk.io/docs/game-dev/loading and ldtk.io/api 2026-05-18).
- **MEDIUM** — "~1 task budget" for the custom Phaser LDtk loader (cite: map-editor-decision.md). Actual size will be refined by the planner; the Excalibur plugin source is the closest reference implementation but is Excalibur-bound.
- **MEDIUM** — Minimap-via-second-Camera vs. RenderTexture decision deferred to planner; both Phaser-native paths are well-documented and proven, but the "show entire zone always" requirement from the seed pushes toward RenderTexture.
- **LOW** — chokidar 4 vs 5 pin is currently defensive (avoid two watcher instances inside Vite 8). If Vite 8.x ever bumps to chokidar 5, follow it. Not load-bearing now.

---

## Sources

- LDtk decision rationale: `.planning/notes/map-editor-decision.md`
- LDtk releases: [github.com/deepnight/ldtk/releases](https://github.com/deepnight/ldtk/releases) (verified 2026-05-18)
- LDtk official loading docs: [ldtk.io/docs/game-dev/loading](https://ldtk.io/docs/game-dev/loading/) (verified 2026-05-18)
- LDtk API/bindings page: [ldtk.io/api](https://ldtk.io/api/) (verified 2026-05-18)
- ldtk-ts archival: [github.com/jprochazk/ldtk-ts](https://github.com/jprochazk/ldtk-ts) (archived 2022-02-11, verified 2026-05-18)
- Excalibur LDtk plugin (reference for our loader): [excaliburjs.com/docs/ldtk-plugin](https://excaliburjs.com/docs/ldtk-plugin/) (verified 2026-05-18)
- ajv: [npmjs.com/package/ajv](https://www.npmjs.com/package/ajv) — 8.17.1 (verified 2026-05-18)
- chokidar: [npmjs.com/package/chokidar](https://www.npmjs.com/package/chokidar) — 4.x stable, 5.0.0 released 2025-11-25 (intentionally not adopted, see notes)
- quicktype: [npmjs.com/package/quicktype](https://www.npmjs.com/package/quicktype) — 23.2.6 (verified 2026-05-18)
- Phaser minimap pattern: [phaser.io/examples/v3/view/camera/minimap-camera](https://phaser.io/examples/v3/view/camera/minimap-camera)
- Existing v1.0 deploy pattern (reused): `docs/deploy/LOCAL-DEPLOY.md`, `scripts/client-release.sh`
- Existing tool to extend (NOT duplicate): `tools/room-converter/{cli.ts, src/*, keys/}` (inspected 2026-05-18)
- Hot-reload signing contract (reused): ADR 0004 `docs/adr/0004-room-registry-hot-reload.md`
- 44×40 tile constant (load-bearing): `docs/extracted-engine/scene-room-model.md` + `extracted/client-5-8/sprites/0023-Tile1/meta.json`

[doc->REQ-DEP-04]
