/** * Lane B (Paint) public surface. Integration (game.ts) imports from here. */ import * as THREE from 'three' import type { PaintColor, World } from '../contracts' import { PaintSkin } from './skin' export { PaintSkin } from './skin' export type { PaintSkinOptions } from './skin' export { PaintCannon, solveBallisticVelocity } from './cannon' export type { PaintCannonConfig } from './cannon' export { BuffSystem } from './buffs' export type { BuffTarget } from './buffs' export { PaintHUD } from './hud' export { COLOR_ORDER, colorIndex, countBuckets, coverageReportFromCounts, computeModifiers, buffStrength, superColor, dominantColor, ACTIVATE, SUPER, BOUNCE_JUMP, hasMeteor, hasZap, } from './coverage-math' export interface PaintEventTarget { /** The paintable body — its world position is used for proximity gating. */ mesh: THREE.Object3D paint: PaintSkin } /** * Wire the inbound paint protocol from Lane C onto a blob's skin: * 'paint:request-splat' {point, color, radius} — bucket dumps, sprays * 'paint:request-cleanse' {fraction} — bubble arches, rinses * request-splat is proximity-gated to the blob (ground decals are V1), and a * matched splat re-emits 'paint:splatted' {target:'blob'} so downstream listeners * (VFX, audio, netcode) see a single canonical splat event. Returns an unsub fn. */ export function installPaint(world: World, target: PaintEventTarget): () => void { const center = new THREE.Vector3() const scale = new THREE.Vector3() const worldRadius = (): number => { target.mesh.getWorldScale(scale) return target.paint.boundingRadius * Math.max(scale.x, scale.y, scale.z) } const offSplat = world.events.on( 'paint:request-splat', (p: { point: THREE.Vector3; color: PaintColor; radius: number }) => { if (!p?.point) return target.mesh.getWorldPosition(center) if (center.distanceTo(p.point) <= worldRadius() + p.radius) { target.paint.splatAtPoint(p.point, p.color, p.radius) world.events.emit('paint:splatted', { color: p.color, target: 'blob' }) } }, ) const offCleanse = world.events.on( 'paint:request-cleanse', (p: { fraction: number }) => { target.paint.cleanse(p?.fraction ?? 1) }, ) return () => { offSplat() offCleanse() } }