// The glassie run, pure half (docs/SCENARIOS.md): carry a wire rack of glasses // through a nightclub crowd without redecorating the floor. Wobble physics and // breakage rules live here so the feel is testable; the scene only reports // bumps and steering. export const RACK_GLASSES = 6; /** Wobble at or past this breaks a glass (and the wobble sheds with it). */ export const BREAK_AT = 1; /** A break dumps you back here — shaken, not reset. */ const AFTER_BREAK = 0.45; /** Passive settle per second when you walk smoothly. */ const SETTLE_PER_S = 0.35; /** One crowd bump is most of the way to trouble on a settled rack. */ const BUMP_WOBBLE = 0.55; /** Full-lock steering reversal adds this much per second. */ const TURN_WOBBLE_PER_S = 1.4; /** In-game minutes between racks — the bar never stops producing empties. */ export const RACK_RESPAWN_MIN = 22; export interface RackState { glasses: number; wobble: number; } export const freshRack = (): RackState => ({ glasses: RACK_GLASSES, wobble: 0 }); /** * Advance the rack one frame. `turnSharpness` is 0..1 (how hard the carry * velocity swung this frame); `bumped` is a de-bounced crowd contact. * Returns the new state plus how many glasses broke THIS frame (0 or 1 — a * frame only ever breaks one; the wobble shed makes chains possible but earned). */ export function stepRack( rack: RackState, dtMs: number, bumped: boolean, turnSharpness: number, ): { rack: RackState; broke: number } { const dt = Math.max(0, dtMs) / 1000; let wobble = rack.wobble - SETTLE_PER_S * dt; wobble += Math.max(0, Math.min(1, turnSharpness)) * TURN_WOBBLE_PER_S * dt; if (bumped) wobble += BUMP_WOBBLE; wobble = Math.max(0, wobble); if (wobble >= BREAK_AT && rack.glasses > 0) { return { rack: { glasses: rack.glasses - 1, wobble: AFTER_BREAK }, broke: 1 }; } return { rack: { glasses: rack.glasses, wobble }, broke: 0 }; } /** 0..1 steering harshness from consecutive frame velocities. */ export function turnSharpness( prevVx: number, prevVy: number, vx: number, vy: number, maxSpeed: number, ): number { if (maxSpeed <= 0) return 0; const dx = vx - prevVx; const dy = vy - prevVy; return Math.min(1, Math.hypot(dx, dy) / (maxSpeed * 2)); }