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>
55 lines
3.1 KiB
Markdown
55 lines
3.1 KiB
Markdown
# 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.
|