# ADR 0001: Client engine choice

**Date:** 2026-05-02
**Phase:** 02 close

## Status

**Accepted** — locked at end of Phase 2 (CDOC-04). Re-evaluation gate: Phase 6
start (CLI-01) AND Phase 7 retro (post-CLI-08, pre-Phase-7-PR-01) per the
Phaser 4 migration window discussed in the Decision section below.

## Context

Phase 1 extracted 854 sprites, 12 backgrounds, 198 scripts, 320 objects, and
16 rooms from `legacy/open-source-release/BN Online Client 5-8.gmd` into
`extracted/client-5-8/` (8307 files, 83 MB) per CLAUDE.md hard rule #6
(extract → document → rewrite). Phase 2 then documented every client engine
subsystem under `docs/extracted-engine/` (11 hand-authored subsystem MDs +
SUBSYSTEM-MAP.json + asset catalog) and produced a feature-vs-engine matrix
(`docs/extracted-engine/MATRIX.md`, rendered from `MATRIX-rows.json`) scoring
three candidate 2D engines: Phaser 3.90.0 ("Tsugumi"), Phaser 4.1.0 ("Salusa"),
and PixiJS 8.18.1 (with `@pixi/tilemap@5.0.2` + `howler@2.2.4` if chosen).

Key engine surfaces from CDOC-01 (per `docs/extracted-engine/README.md`):

- **Sprite rendering with origin / bounding-box / collision-mask** — heavy
  usage; every visible object carries `sprite_index` + `mask_index`.
- **Per-key keyboard event handling** — heavy; movement on object 0042-player
  uses Keyboard-N event slots.
- **Per-object Collision events with target object filter** — heavy; walls and
  walkability on player Step.gml drive the Hexport state machine.
- **Tile-background drawing** — rooms use tile sheets via GM's
  background-as-tileset model.
- **Custom-font text rendering** — UI-heavy; action 523/525 resolved in plan
  02-02 to "OCRA" + "Fixedsys" font setters.
- **Per-instance integer depth ordering** — covers the entire display list.
- **Room model with view config + room transitions** — 16 rooms catalogued;
  CLI-08 needs only one room for the MVP gate.
- **No embedded audio in the client `.gmd`** — Plan 02-05 confirmed zero
  `sound_play` / `playmidi` / `sound_load` call sites across all 198 scripts;
  no `extracted/client-5-8/sounds/` directory. Phase 6 ships silent.
- **DOM-overlay vs canvas UI compatibility** — chat HUD, Main_Menu,
  Online_Lobby, Settings_Menu, Online_Command_Screen all need either canvas
  text rendering or DOM overlays; per ui-and-menus.md both engines support
  this pattern equivalently.

See `docs/extracted-engine/README.md` for the full subsystem index and
`docs/extracted-engine/MATRIX.md` for the per-row engine scoring (21 rows,
total weight budget 72).

## Decision

We will use **Phaser 3.90.0** for the Phase 6 MVP and Phase 7 parity client.

Weighted totals (per `docs/extracted-engine/MATRIX-rows.json`, rendered in
MATRIX.md):

- Phaser 3.90: **286** weighted points
- Phaser 4.1: **286** weighted points
- PixiJS 8.18: **201** weighted points

Rationale, by MATRIX row:

- **MX-RENDER-01** (sprite rendering with origin/bbox/mask, weight 5): Phaser
  3 native (4) via `Phaser.GameObjects.Sprite.setOrigin` + Arcade/Matter mask
  matching; PixiJS manual (2) — Pixi.Sprite has anchor (~origin) but no
  built-in physics.
- **MX-COLLIDE-01** (per-object Collision events, weight 5): Phaser 3 native
  (4) via `Phaser.Physics.Arcade.collider(spriteA, spriteB, callback)` — a 1:1
  GM Collision-event analog; PixiJS manual (2) — no physics layer at all.
- **MX-SCENE-01** (room model + view + view-following, weight 5): Phaser 3
  native (4) via `Phaser.Scene` + `cameras.main.startFollow(target)`; PixiJS
  manual (2) — no Scene/Room model, must build a Container hierarchy + manual
  camera math + custom scene loader.
- **MX-INPUT-01** (per-key keyboard event handling, weight 5): Phaser 3 native
  (4) via `Phaser.Input.Keyboard.on('keydown-W')` + `addKey().isDown` polling;
  PixiJS manual (2) — Pixi has no input layer at all.
- **MX-AUDIO-01** (audio playback, weight 1): Phase 2 plan 02-05 confirmed no
  embedded audio in client-5-8; weight is correspondingly low and does not
  drive the decision.
- **MX-UI-01** (DOM-overlay UI, weight 4): both engines native (4) — chat HUD
  as a textarea/div overlay on the canvas works identically in either.

The weighted-total spread (286 vs 201 = 85 points / ~30 % of PixiJS's score)
is driven by four heavy-weight rows where Phaser 3 is `native` and PixiJS is
`manual`: MX-RENDER-01, MX-INPUT-01, MX-COLLIDE-01, MX-SCENE-01. Together
those four rows alone contribute (4-2)×(5+5+5+5) = 40 of the 85-point gap.

**Phaser 4.1 considered.** `npm view phaser dist-tags` returned `latest:
4.1.0` on 2026-05-02 — Phaser 4 is GA (not beta) and architecturally cleaner
than v3 (rendering pipeline rewritten). MATRIX.md scores Phaser 4 identically
to Phaser 3 on all 21 rows because v4 added APIs without removing existing
ones (per the official migration guide). **Rejected for the Phase 6 MVP**
because:

1. STACK.md (researched 2026-05-01) flags v4 as "still settling" with an
   explicit decision-gate language: "If starting after ~Q3 2026 once the v4
   ecosystem (plugins, tutorials, AI scaffolding) catches up." 2026-05-02 is
   five months ahead of that gate.
2. CLI-08 ship pressure (the Phase 6 hard milestone — two players move + chat
   on a deployed server) favours the safer choice with the more mature
   ecosystem (community plugins, tutorials, AI scaffolding context).
3. The migration cost from v3 → v4 if v4 ecosystem matures by Phase 7 retro
   is bounded — per the official Phaser 3 → 4 Migration Guide, "biggest
   release ever, migration mostly automatic but still recent." Sprite / Origin
   / Tilemap / Scene / Animations / Input APIs are preserved.

**Re-evaluation gate:** Phase 6 start AND Phase 7 retro. If either gate
surfaces a v4-specific advantage that v3 cannot match, migration is feasible
per the migration guide. Phase 6 commits to v3 patterns; Phase 7 retro
re-scores the matrix and may flip.

**D-13 hard-knockout audit:** zero rows in MATRIX-rows.json have weight ≥ 4
graded `hard` for Phaser 3.90. The hard-knockout rule does not fire; the
ADR therefore follows the weighted-total winner without override.

## Consequences

- **Phase 6 (CLI-01, CLI-04, CLI-06)** commits to Phaser 3.90 patterns:
  `pixelArt: true` config (per CLI-06 integer-pixel rendering requirement),
  `roundPixels: true` camera, `Phaser.GameObjects.Sprite` for movement +
  animation, `Phaser.Tilemaps` for room rendering.
- **Pin:** `pnpm add phaser@3.90.0` in `apps/client` (first commit in CLI-01).
- **AST-01 (Phase 6/7)** targets the Phaser atlas JSON format. The
  asset-pipeline tool reads `extracted/client-5-8/sprites/**/frames/img_NNN.bmp`
  and emits Phaser-compatible atlas JSON + PNG sheets per BMP→PNG conversion.
- **AST-02 (Phase 7 MIDI→OGG)** targets `Phaser.Sound.WebAudio` loader.
  (Note: low engine-decision weight per the audio finding above; Phase 6
  ships silent and AST-02 is a Phase 7 deliverable when audio assets surface.)
- **AST-03 (Phase 7 fonts)** targets Phaser BitmapText format — sprite-strip
  atlases for "OCRA" and "Fixedsys" fonts referenced by action 523/525.
- **PixiJS deferred.** If Phase 6 surfaces a Phaser 3 hard-knockout missed by
  this MATRIX (i.e. a CDOC-01 feature where Phaser 3 cannot achieve parity
  without invasive workaround), fall back to PixiJS 8.18.1 +
  `@pixi/tilemap@5.0.2` + `howler@2.2.4` per D-13 hard-knockout rule. The
  fallback cost is bounded: Pixi covers 7 of 21 rows at the same `native`
  grade as Phaser; the 4-row scene-physics-input-collision gap is the only
  significant migration burden.
- **Phaser 4 migration window opens at Phase 7 retro.** If by then the v4
  ecosystem (plugins, tutorials, AI scaffolding) has caught up to v3 maturity,
  apps/client can migrate per the official Phaser 3 → 4 Migration Guide.
  Re-evaluation timing: post-CLI-08, pre-Phase-7-PR-01. Migration cost is one
  apps/client refactor.
- **CLAUDE.md hard rule #6 satisfied.** Phase 2 closes the "document" stage
  with this ADR locking the engine choice; Phase 6 unlocks the "rewrite"
  stage with a concrete pin.

## References

- `docs/extracted-engine/MATRIX.md` (decisive rows: MX-RENDER-01,
  MX-COLLIDE-01, MX-SCENE-01, MX-INPUT-01, MX-AUDIO-01, MX-UI-01)
- `docs/extracted-engine/MATRIX-rows.json` (canonical scoring data — sole
  source of truth; MATRIX.md is rendered from this)
- `docs/extracted-engine/README.md` (subsystem index)
- `.planning/research/STACK.md` §"Phaser 3 → Phaser 4" (the "still settling"
  decision-gate language)
- [Phaser 3 → 4 Migration Guide](https://phaser.io/news/2026/04/migrating-from-phaser-3-to-phaser-4-what-you-need-to-know)
  — "biggest release ever, migration mostly automatic but still recent"
- `CLAUDE.md` hard rule #6 (extract → document → rewrite — this ADR closes
  the "document" stage and unlocks "rewrite" Phase 6)
