BLOBBO/lanes/LANE-B-paint.md
type-two 969f602dd4 Foundation: vite+ts+three+rapier scaffold, frozen contracts, lane docs A-E
Fixed-step world runtime, event bus wire protocol between lanes, integration
seam (game.ts) with placeholder ball. Lanes A/B/C own disjoint dirs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:55:43 +10:00

3.1 KiB
Raw Blame History

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.tsPaintSkin 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.tsPaintCannon: 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.tsBuffSystem (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.