not-tonight/src/scenes/floor/glassie.ts

64 lines
2.2 KiB
TypeScript

// 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));
}