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

55 lines
3.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.