Integration: blob+paint+machines composed in game.ts; cannons staged along the racing line (muzzle 26 — default 16 undershoots course distances); plate threshold above clean-blob mass so only painted blobs trip it (paint=weight= machine access); belt delivers riders under the bucket (shallow catch timed to teeter drift); arch moved to a detour lane. Cross-lane fixes found at integration, all one class — gameplay logic living on render-frame time, which stalls in hidden tabs: - telegraph windup -> fixed step (gates forces/dumps) - bucket tip/pour -> fixed step (gates the splat) - blob group sync -> fixed step in controller (paint proximity gate reads it) world.ts grew tick()/renderOnce() + a hidden-tab watchdog interval for this. Assets: mesh wave 1 (4 GLBs via farm: blobbo-base, toaster, spring boot, paint bucket) + mesh_pipeline.py with outdir scp fallback for the farm's unregistered-remote-outputs bug. Verified headless end-to-end via tick(): move/jump/squash, cannon hits on a moving blob, coverage->buffs (BURN 1.6x, super at 70%), mass 1.0->1.67 with paint, plate clean-reject + painted-trip, boot launch (peak y 3.9), belt-> bucket dump (23.6% purple), arch cleanse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
3.3 KiB
TypeScript
99 lines
3.3 KiB
TypeScript
/**
|
|
* FROZEN CONTRACTS — the seams between lanes A (blob/course), B (paint),
|
|
* C (machine). Lanes never edit this file; integration (game.ts) owns changes.
|
|
* Lanes communicate through these types and world.events ONLY.
|
|
*/
|
|
import type * as THREE from 'three'
|
|
import type RAPIER from '@dimforge/rapier3d-compat'
|
|
|
|
// ---- Paint palette (GDD §6) ------------------------------------------------
|
|
export type PaintColor =
|
|
| 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'purple' | 'pink'
|
|
|
|
export const PALETTE: Record<PaintColor, string> = {
|
|
red: '#FF3B30',
|
|
orange: '#FF9500',
|
|
yellow: '#FFD60A',
|
|
green: '#34C759',
|
|
blue: '#0A84FF',
|
|
purple: '#AF52DE',
|
|
pink: '#FF6EB4',
|
|
}
|
|
export const BASE_WHITE = '#F5F5F7'
|
|
|
|
/** Fractions of TOTAL body surface, 0..1. Unpainted = 1 - total. */
|
|
export interface CoverageReport {
|
|
total: number
|
|
byColor: Record<PaintColor, number>
|
|
}
|
|
|
|
// ---- Blob ------------------------------------------------------------------
|
|
/** Written by BuffSystem (lane B); read every frame by the controller (lane A). */
|
|
export interface BlobModifiers {
|
|
speedMul: number // 1 = base
|
|
jumpMul: number // 1 = base
|
|
massMul: number // 1 = base; paint = weight
|
|
grip: number // 0..1 extra effective friction
|
|
size: number // 1 = base uniform scale (visual + collider)
|
|
waterproof: boolean
|
|
glow: number // 0..1 super-state emissive pulse
|
|
}
|
|
|
|
export const defaultModifiers = (): BlobModifiers => ({
|
|
speedMul: 1, jumpMul: 1, massMul: 1, grip: 0, size: 1,
|
|
waterproof: false, glow: 0,
|
|
})
|
|
|
|
export interface PaintSkin {
|
|
applySplat(uv: THREE.Vector2, color: PaintColor, radius: number): void
|
|
splatAtPoint(worldPoint: THREE.Vector3, color: PaintColor, radius: number): void
|
|
coverage(): CoverageReport
|
|
cleanse(fraction: number): void
|
|
}
|
|
|
|
export interface Blob {
|
|
group: THREE.Group // root, added to scene
|
|
mesh: THREE.Mesh // UV-mapped paintable body mesh
|
|
body: RAPIER.RigidBody
|
|
modifiers: BlobModifiers
|
|
paint?: PaintSkin // attached during integration
|
|
}
|
|
|
|
// ---- Systems / world -------------------------------------------------------
|
|
export interface System {
|
|
/** Called at fixed 60Hz, after physics step. */
|
|
update(dt: number): void
|
|
}
|
|
|
|
export type EventHandler = (payload: any) => void
|
|
export interface Events {
|
|
on(name: string, fn: EventHandler): () => void
|
|
emit(name: string, payload?: any): void
|
|
}
|
|
|
|
export interface World {
|
|
scene: THREE.Scene
|
|
camera: THREE.PerspectiveCamera
|
|
renderer: THREE.WebGLRenderer
|
|
physics: RAPIER.World
|
|
rapier: typeof RAPIER
|
|
events: Events
|
|
blob?: Blob
|
|
addSystem(s: System): void
|
|
/** Per-render-frame hooks (visual-only work: cameras, jiggle, particles). */
|
|
onFrame(fn: (dt: number) => void): void
|
|
/** Manually advance the fixed-step sim (tests, headless verification). */
|
|
tick(steps?: number): void
|
|
/** Run frame hooks + render one frame (screenshots while backgrounded). */
|
|
renderOnce(): void
|
|
start(): void
|
|
}
|
|
|
|
// ---- Event names (the wire protocol between lanes) -------------------------
|
|
// 'machine:signal' {id: string}
|
|
// 'paint:request-splat' {point: THREE.Vector3, color: PaintColor, radius: number}
|
|
// 'paint:request-cleanse' {fraction: number}
|
|
// 'paint:splatted' {color: PaintColor, target: 'blob'|'world'}
|
|
// 'blob:landed' {impact: number}
|
|
// 'blob:jumped' {}
|