# Lane B — Paint

**Owns:** `src/paint/`, `demos/lane-b.html`, `src/demo/lane-b.ts`.
**Never touches:** `src/contracts.ts`, `src/world.ts`, `src/game.ts`, `src/main.ts`,
`package.json`, other lanes' dirs. No new deps — three + rapier only.

## Mission
The core invention: paint lands on the blob's body, live per-colour coverage %
is computed, and coverage drives buffs (GDD §5.1, §6). MVP colours: RED, GREEN,
BLUE (design for all 7 — palette is in contracts).

## Deliverables
1. `src/paint/skin.ts` — `PaintSkin` class, attachable to ANY mesh with clean
   UVs (implements the `PaintSkin` interface in contracts):
   - Offscreen canvas (256×256) as the blob's colour mask → THREE.CanvasTexture
     as the material map over the white base.
   - `applySplat(uv, color, radius)` stamps a soft-edged irregular splat
     (quantize stored pixels to exact palette colours so counting is exact).
   - `splatAtPoint(mesh, worldPoint, color, radius)` — raycast/closest-face →
     UV → applySplat. Handle UV seams tolerantly (a stamp near seam may wrap).
   - `coverage(): CoverageReport` — recount every ≤250ms (cached between):
     bucket pixels by palette colour; fractions of TOTAL surface (unpainted
     counts in denominator).
   - `cleanse(fraction)` — erase that fraction of painted pixels (random holes,
     looks scrubbed), 1 = full clean.
2. `src/paint/cannon.ts` — `PaintCannon`: placed emitter that fires paint-glob
   projectiles (small coloured spheres, gravity arc, Rapier sensor or manual
   sweep). On hit: blob → `splatAtPoint`; anything else → despawn (ground
   decals are V1). Fire on interval AND on event `machine:signal {id}` matching
   its configured trigger id. Emits `paint:splatted {color, target}`.
3. `src/paint/buffs.ts` — `BuffSystem` (a `System`): every frame maps the
   blob's `coverage()` → `blob.modifiers` per GDD §6, MVP subset:
   - threshold 20%, strength scales linearly 20%→70%;
   - RED `speedMul` up to 1.6 + ember particle trail (cheap sprite points);
   - GREEN `grip` up to 1.0;
   - BLUE `waterproof=true` + slick trail flag (`slippery` decal V1 — flag ok);
   - total coverage → `massMul` 1.0→1.8 (paint = weight, GDD §5.2);
   - ≥70% one colour = super: modifier at max + `glow` (emissive pulse).
4. HUD `src/paint/hud.ts` — tiny DOM overlay: per-colour % bars + active buffs.
5. Demo `demos/lane-b.html` + `src/demo/lane-b.ts`: a plain rolling test ball
   (your own stub — do NOT import Lane A) circling between two cannons (red,
   green) + a cleanse key (C). HUD live. Prove: paint accumulates where hit,
   %s are sane, buffs kick in at 20%, super at 70%, cleanse works.

## Acceptance
- `npm run build` passes; demo shows visible splats accumulating AT hit
  locations, live HUD %, buff activation visibly changes the test ball
  (speed/trail/glow), cleanse visibly scrubs.
- Coverage recount ≤250ms cadence, no per-frame full readback (perf).
- No edits outside owned paths.

## Contracts (frozen — read `src/contracts.ts` first)
Listen: `machine:signal`, `paint:request-splat {point, color, radius}` (Lane C's
bucket dumps emit this). Emit: `paint:splatted`. Palette: `PALETTE` in contracts.
