Hoist pure render helpers out of TileLayer into a shared module (Pixi cutover prerequisite) #1

Closed
opened 2026-07-05 09:13:30 +00:00 by beasty · 1 comment
Owner

Context

The PixiJS spike (40ab476, route /game/pixi) validated the renderer verdict in docs/ENGINE_FRONTEND.md: 60fps at whole-map zoom-out where the DOM renderer stalls. To stay decoupled from the then-in-flux components/map/TileLayer.tsx, the spike had to reimplement the pure tile-visual logic in lib/render/pixi/tileVisual.ts. That duplication is now the single biggest risk to the cutover: any change to fog/fence/road/tile rules must currently be made twice, and the two renderers will silently drift apart.

The organic-village rewiring of TileLayer.tsx (25571c4) has settled, so this refactor is unblocked.

Task

Extract every pure, renderer-agnostic decision out of components/map/TileLayer.tsx / components/map/constants.ts into a shared lib/ module (suggestion: lib/game/tileVisuals.ts — pure, no React, no Pixi, no DB) and make both renderers import it:

  • Tile sprite selection — terrain type/overlay → sprite key (incl. the water recolor, grass base underlay rule for standalone trees, stump rule via isChoppedStumpTile).
  • FogisExplored and the 4-level brightness dim (computeFogDim), including the claimed-village reveal (+1 halo) semantics.
  • Fence — segment → sprite mapping (FENCE_X / FENCE_Y / gate) and the edge-seating offsets used to place rails on their tile edge (these came from fencePerimeter in lib/game/villageArea.ts; the offsets currently live in the render layer).
  • RoadsroadSpriteFor(mask) and the per-chunk neighbour pass (computeRoadSprites) that builds the masks.

Constraints

  • Pure functions only: (tile, neighbours, villageArea, …) → {spriteKey, tint/brightness, offsets}-shaped outputs. No JSX, no Pixi types.
  • components/map/TileLayer.tsx and lib/render/pixi/tileVisual.ts become thin consumers; delete the spike's duplicated logic.
  • Zero visual change in either renderer. Verify in-browser at close/mid/far zoom on both /game and /game/pixi (screenshots), and on /dev/fit (the fence/path/stump test-fitting cases live there).
  • Unit-test the extracted module directly in tests/unit/game/ (sprite selection per terrain/overlay combination, fog level boundaries, fence mask → sprite, road mask table). Boundary tests for every threshold, per the testing contract in CLAUDE.md.

Acceptance criteria

  • One shared module owns all pure tile-visual decisions; grep shows no duplicated fog/fence/road/sprite logic under lib/render/pixi/.
  • /game, /game/pixi, and /dev/fit are pixel-equivalent to before (screenshot comparison at 3 zooms).
  • New unit tests cover the extracted module; full suite + typecheck green.

Blocks #2.

## Context The PixiJS spike (`40ab476`, route `/game/pixi`) validated the renderer verdict in `docs/ENGINE_FRONTEND.md`: 60fps at whole-map zoom-out where the DOM renderer stalls. To stay decoupled from the then-in-flux `components/map/TileLayer.tsx`, the spike had to **reimplement** the pure tile-visual logic in `lib/render/pixi/tileVisual.ts`. That duplication is now the single biggest risk to the cutover: any change to fog/fence/road/tile rules must currently be made twice, and the two renderers will silently drift apart. The organic-village rewiring of `TileLayer.tsx` (`25571c4`) has settled, so this refactor is unblocked. ## Task Extract every pure, renderer-agnostic decision out of `components/map/TileLayer.tsx` / `components/map/constants.ts` into a shared `lib/` module (suggestion: `lib/game/tileVisuals.ts` — pure, no React, no Pixi, no DB) and make **both** renderers import it: - **Tile sprite selection** — terrain type/overlay → sprite key (incl. the water recolor, grass base underlay rule for standalone trees, stump rule via `isChoppedStumpTile`). - **Fog** — `isExplored` and the 4-level brightness dim (`computeFogDim`), including the claimed-village reveal (+1 halo) semantics. - **Fence** — segment → sprite mapping (FENCE_X / FENCE_Y / gate) and the edge-seating offsets used to place rails on their tile edge (these came from `fencePerimeter` in `lib/game/villageArea.ts`; the offsets currently live in the render layer). - **Roads** — `roadSpriteFor(mask)` and the per-chunk neighbour pass (`computeRoadSprites`) that builds the masks. ## Constraints - Pure functions only: `(tile, neighbours, villageArea, …) → {spriteKey, tint/brightness, offsets}`-shaped outputs. No JSX, no Pixi types. - `components/map/TileLayer.tsx` and `lib/render/pixi/tileVisual.ts` become thin consumers; delete the spike's duplicated logic. - Zero visual change in either renderer. Verify in-browser at close/mid/far zoom on both `/game` and `/game/pixi` (screenshots), and on `/dev/fit` (the fence/path/stump test-fitting cases live there). - Unit-test the extracted module directly in `tests/unit/game/` (sprite selection per terrain/overlay combination, fog level boundaries, fence mask → sprite, road mask table). Boundary tests for every threshold, per the testing contract in CLAUDE.md. ## Acceptance criteria - [ ] One shared module owns all pure tile-visual decisions; grep shows no duplicated fog/fence/road/sprite logic under `lib/render/pixi/`. - [ ] `/game`, `/game/pixi`, and `/dev/fit` are pixel-equivalent to before (screenshot comparison at 3 zooms). - [ ] New unit tests cover the extracted module; full suite + typecheck green. Blocks #2.
Author
Owner

Closed after the Rust/Bevy cutover. The DOM/Pixi extraction described here is superseded by the native Bevy renderer.

Closed after the Rust/Bevy cutover. The DOM/Pixi extraction described here is superseded by the native Bevy renderer.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
beasty/cat_idler#1
No description provided.