# ADR 0009: LDtk gridSize convention

**Date:** 2026-05-20
**Phase:** 07 (Workflow Smoke + Convention Locks, Plan 07-01)

[doc->REQ-MAP-02]

## Status

**Accepted** — re-evaluation gate: any Phase 8 operator-machine evidence that
4 px entity-snap visibly drifts vs legacy GML. Absent that, locked through v1.2.

## Context

Phase 8 onward authors all REBNO room content as LDtk `.ldtk` files. LDtk
requires a `gridSize` setting per layer type (IntGrid, Tile, Entity). The
research default in `.planning/research/v1.1/STACK.md` is a **split**:
IntGrid + Tile layers at `gridSize = 4` (snap floor-paint to a fine grid),
Entity layers at `gridSize = 1` (pixel-accurate entity placement).

REBNO's authoritative floor tile pitch is **44 × 40 px**, sourced from
`extracted/client-5-8/sprites/0023-Tile1/meta.json` (`width: 44`,
`height: 40`) and locked in `CLAUDE.md` §"Extracted Constants" as
load-bearing for every renderer, collision grid, atlas slicer, and test-room
generator. This pitch is not negotiable — the entire extracted-asset
pipeline assumes it.

The operator's authoring preference, captured during Phase 7 planning
(`.planning/phases/07-workflow-smoke-convention-locks/07-CONTEXT.md` D-01),
is a **single mental grid across all layer types**. Two different gridSize
values across IntGrid/Tile vs Entity layers force the operator to context-
switch between "snap-to-4-px-fine-grid" and "pixel-accurate" modes mid-edit
on the same room — a recipe for placement drift and authoring fatigue.
Originally-extracted BNO entities (NPCs, teleporters, messageboards,
platforms) are all tile-aligned in the legacy GML, so the pixel-accurate
benefit that the research default's `gridSize = 1` Entity layer buys does
not match what the source content actually needs.

## Decision

Three sub-decisions, implementing D-01, D-02, D-03 from
`07-CONTEXT.md` §"gridSize ADR":

1. **`gridSize = 4` unified across IntGrid, Tile, and Entity layers** (D-01).
   This deliberately diverges from the research-default 4/1 split. Every
   layer type in every `.ldtk` file MUST be authored with `gridSize: 4`.
   Single mental grid; no per-layer asymmetry to remember.

2. **44 × 40 floor pitch maps to 11 × 10 cells in the editor** (D-02). Each
   LDtk visual cell = 4 px square; one BNO floor tile = an 11 × 10 block of
   cells. Inline arithmetic:

   ```
   44 / 4 = 11 cells wide
   40 / 4 = 10 cells tall
   ```

   Mental gear-shift acknowledged: the LDtk cell counter never lines up
   with "tile count" in BNO terms. Operators count cells in 11-wide /
   10-tall multiples when laying out floor tiles.

3. **Entity placement snaps to 4 px, not pixel-accurate** (D-03). Acceptable
   because legacy BNO entities are tile-aligned in GML — 4 px tolerance is
   well below visible drift on the 640 × 480 viewport (ADR 0008). Phaser
   sprite-origin offsets (e.g. NaviStandD `bboxBottom = 46` vs sprite-rect
   `height = 48`) are still applied at runtime by
   `apps/client/src/render/legacy-origin.ts` per CLAUDE.md §"Coordinate
   Conventions (D-63 — Plan 06.4)". `gridSize` controls **editor snapping
   only** — not runtime placement, not collision bounds, not animation
   pivots.

## Consequences

### Positive

- Single mental grid across IntGrid / Tile / Entity layers simplifies
  operator authoring; no context-switch between fine-grid-snap and
  pixel-accurate modes mid-edit.
- Uniform 4 px snap behavior across every layer type — operators learn one
  rule, apply it everywhere.
- No per-layer asymmetry to remember when round-tripping rooms through
  `tools/room-converter` (Phase 9 ldtk-import path).

### Negative

- Entity 4 px snap is a deliberate tradeoff vs the research-default
  pixel-accurate placement. Accepted per D-03 because legacy BNO entities
  are tile-aligned and 4 px tolerance is well below visible drift on the
  640 × 480 viewport. If Phase 8 operator-machine evidence shows visible
  entity drift vs legacy GML, this ADR's Status re-evaluation gate fires.

### Neutral

- 11 × 10 cells per floor tile means the editor's cell-counter never lines
  up with "tile count" in BNO terms. Operators mentally gear-shift between
  "BNO tiles" (44 × 40 px units) and "LDtk cells" (4 × 4 px units) when
  reading layouts. Documented here so the gear-shift is explicit, not a
  surprise.

## Alternatives considered

- **4 / 1 split (research default; IntGrid + Tile = 4, Entity = 1)** —
  Rejected: forces two mental grids on the operator within a single room
  file; the entity pixel-accuracy benefit is unnecessary because legacy BNO
  entities are tile-aligned in the source GML.
- **1 / 1 (pixel-accurate everywhere)** — Rejected: loses snap-to-tile
  benefit on IntGrid and Tile layers; makes floor-tile painting tedious
  (operator must hit exact pixel offsets to align with the 44 × 40 pitch).
- **44 / 40 (one cell = one BNO floor tile)** — Rejected: LDtk gridSize
  must be uniform per layer (a single integer, not a width × height pair),
  and `44 ≠ 40`. Forcing this would require non-square cells with no path
  to entity sub-tile placement, and LDtk's data model doesn't support it.

## References

- `extracted/client-5-8/sprites/0023-Tile1/meta.json` — 44 × 40 floor tile
  source-of-truth (`width: 44`, `height: 40`).
- `CLAUDE.md` §"Extracted Constants" — 44 × 40 floor pitch lock; load-bearing.
- `CLAUDE.md` §"Coordinate Conventions (D-63 — Plan 06.4)" — runtime sprite
  origin offsets unaffected by editor `gridSize`.
- `docs/adr/0008-canvas-base-resolution.md` — sibling canvas-base-resolution
  ADR (640 × 480 viewport lock); together with this ADR fixes the editor-
  grid + render-resolution invariants for Phase 8+ authoring.
- `.planning/phases/07-workflow-smoke-convention-locks/07-CONTEXT.md` §
  "gridSize ADR" — D-01, D-02, D-03, D-04 (decisions this ADR absorbs).
- `apps/client/src/render/legacy-origin.ts` — canonical runtime origin
  helper; unaffected by editor gridSize choice.
