# ADR 0008: Client canvas base resolution

**Date:** 2026-05-10
**Phase:** 06 (Plan 06-04, retroactive lock)

[doc->REQ-CLI-06]

## Status

**Accepted** — locks the Phaser internal render resolution to **640×480 (4:3)**
for the entire rebuild. Re-evaluation gate: Phase 7 PAR-02 (pixel-diff room
imports). Changing the base resolution requires re-cropping every imported
sprite and re-running pixel-diff verification.

## Context

Phase 6 plan 06-04 (BootScene + LoginScene + GameScene) had to pick a
Phaser `Scale.FIT` internal render resolution before any sprites or rooms
were rendered. RESEARCH.md left this open: "planner picks; matches
mvp-lobby:1000×1000 minus margin OR a smaller pixel-art base". The plan's
first draft used **800×600** as a placeholder, never formally locked.

The original BN Online (GameMaker 5.3a) extracted-room metadata
(`extracted/client-5-8/rooms/0058-BNCentral/meta.json`) declares the camera
viewport as:

```json
"viewW": 640, "viewH": 480, "portW": 640, "portH": 480
```

Every BNO room (BNCentral 8000×6400, Bahoo 7200×5600, Schweisstar 7200×5600,
…) was authored against the same 640×480 viewport. Sprite atlas pivots,
NPC framing, room transition triggers, parallax layers, and on-screen UI
chrome (chat HUD anchor offsets, area-name text positioning) all assume
640×480.

A different base resolution forces re-authoring of every imported asset
in Phase 7 PAR-02 (pixel-diff room verification) — even at the same 4:3
aspect ratio, 800×600 stretches sprites by 1.25× and shifts pivots
sub-pixel.

## Decision

Lock Phaser canvas internal render resolution at **640×480** (4:3, matching
original BNO `viewW`/`viewH`).

`apps/client/src/main.ts` Phaser config:

```ts
scale: {
  mode: Phaser.Scale.FIT,
  autoCenter: Phaser.Scale.CENTER_BOTH,
  width: 640,
  height: 480,
  autoRound: true,
  zoom: Phaser.Scale.MAX_ZOOM,
}
```

`pixelArt: true`, `roundPixels: true`, `<canvas image-rendering: pixelated>`
fallback in `apps/client/index.html`.

`Phaser.Scale.MAX_ZOOM` + `autoRound: true` snaps to the largest integer
zoom that fits the viewport (1×, 2×, 3×, 4×). At 1080p the canvas displays
at 2.25× → snaps to 2× (1280×960 actual pixels). At 1440p → 3× (1920×1440).
`window.addEventListener('resize', () => game.scale.refresh())` re-snaps on
viewport change.

### DOM overlays render at viewport DPR, never canvas zoom

`apps/client/index.html` mounts `#dom-overlay` as a **sibling** of
`#game-root` with `position: fixed; inset: 0` — not inside the Phaser DOM
container. ChatHUD, ReconnectBanner, ForceResetOverlay, and login form all
mount under `#dom-overlay`. Consequence: when the canvas zooms 2×/3×/4×,
chat text stays at native browser pixel density (fine-detail antialiased
text), gaining horizontal room before wrapping rather than scaling
proportionally with the pixel art. Phase 6 plan 06-07 carries a hard
mount-point invariant test (`chat-hud test 0`) locking this; any component
that needs canvas-locked pixel grid renders inside the Phaser scene as a
canvas Text/Image.

## Consequences

**Positive:**
- Asset pipeline AST-01 atlases (plan 06-02 lands a 216×240 atlas under a
  512² budget) carry over to Phase 7 PAR-02 pixel-diff with no re-cropping.
- Original BNO room screenshots become a 1:1 reference for visual parity.
- Integer zoom (1×–4×) on common viewport sizes — no sub-pixel sprite
  smearing on retina or 125% Windows DPI scaling.

**Negative:**
- 640×480 leaves ~37% horizontal whitespace on a 1920×1080 monitor at 2×.
  Acceptable for an MMO chat-and-walk surface; mitigated by DOM chat HUD
  spanning the bottom strip outside the canvas viewport.
- Original BNO ran 30 Hz; the rebuild now uses the same 30 Hz server
  simulation and patch cadence while rendering at 60 Hz. Render rate is
  decoupled from base resolution and is governed by ADR 0007 (input +
  extrapolation). No interaction.

## Alternatives considered

- **800×600** — Phase 6 plan 06-04 first draft. Rejected: forces 1.25×
  sprite re-crop in Phase 7 PAR-02; no rationale tying it to extracted
  room data.
- **1000×1000** — match `mvp-lobby` room dimensions directly. Rejected:
  not 4:3; mvp-lobby is a synthetic room (plan 04-12), not authored
  against any extracted asset.
- **Dynamic** (resize internal render to viewport, no integer zoom).
  Rejected: breaks `pixelArt: true` invariant; sprite art smears at
  non-integer zoom.

## Triggered by

Operator question 2026-05-10 mid-execution of plan 06-04: aspect ratio /
base resolution had never been formally locked. Plan 06-04 was patched in
flight (commit `f7acf74 fix(06-04): lock canvas to 640×480 (BNO original
viewport)`); this ADR records the decision retroactively.
