BLOBBO/src/contracts.ts
type-two d4d41208f8 M0 playable slice: lanes integrated, machine chain staged, hidden-tab hardening
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>
2026-07-17 20:50:16 +10:00

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' {}