PURPOSE

Computes per-frame (sx, sy) scale factors that give ships, enemies, and damaged entities subtle oscillating squash/stretch motion. Each entity type is keyed to a wave shape, frequency, amplitude, and primary axis so that visually similar enemies feel mechanically distinct. The output is volume-preserving — when one axis expands, the other contracts by the inverse amount — so silhouettes stay readable at the configured ~2.5% baseline amplitude.

OWNS

  • Four wave-shape primitives normalised to [-1, 1]: waveSine, waveSawtooth, waveTriangle, wavePulse.
  • The WaveShape union and WAVE_FNS lookup table that resolves a shape name to its function.
  • The SquashPattern interface (wave, freq, amp, axis, optional phaseOffset) and SquashResult interface (sx, sy).
  • The ENEMY_PATTERNS table keyed by typeId (archetype_rarity) for orb, charger, shooter, and mortar tiers.
  • The fallback DEFAULT_ENEMY_PATTERN (sine, 1.2 Hz, ~1.75% amp, X axis).
  • The ship endpoints SHIP_PATTERN_COOL and SHIP_PATTERN_HOT that bracket the heat lerp.
  • The damage-impulse constants DMG_SQUASH_DUR = 0.15 and DMG_SQUASH_AMP = 0.08.
  • A module-scoped scratch object _dmgSquashScratch reused across all getDamageSquash calls to avoid per-frame GC pressure in combat.

READS FROM

  • Caller-provided time (seconds), typeId / heat, and entityId arguments — no globals, no game state imports.
  • The ENEMY_PATTERNS and ship pattern tables defined in this same file.

PUSHES TO

  • Returns SquashResult ({ sx, sy }) values consumed by the renderer; this module performs no canvas, store, or telemetry writes.
  • getDamageSquash returns a shared scratch reference — callers must read sx/sy immediately and must not retain the object.

DOES NOT

  • Does not import anything (no external dependencies, no project imports).
  • Does not mutate the input pattern objects.
  • Does not allocate per-call result objects in getDamageSquash (uses scratch); getEnemySquash / getShipSquash still allocate fresh result objects.
  • Does not apply the scale itself — callers wrap ctx.scale(sx, sy) or pass sx/sy into draw helpers.
  • Does not compensate for non-volume-preservation at large amplitudes; the (1 + d) / (1 - d) approximation is only exact for small d.
  • Does not clamp entityId or time; relies on caller to pass finite numbers.

Signals

  • waveSine(t)Math.sin(t * 2π), smooth oscillation.
  • waveSawtooth(t)frac(t) * 2 - 1, ramps 0→1 then snaps.
  • waveTriangle(t) — linear up/down piecewise on frac(t).
  • wavePulse(t)tanh(sin(t * 2π) * 3), square-ish smoothed throb.
  • Phase stagger constant: golden-ratio offset 0.618033988749895 multiplied by entityId to break visual lockstep across same-type enemies.
  • Heat-to-curve mapping for ship: t = sqrt(heat / 100) with heat clamped to [0, 1]; freq and amp lerp between SHIP_PATTERN_COOL and SHIP_PATTERN_HOT along t.
  • Damage pulse shape: sin((dmgSquashT / 0.15) * π) — peaks at midway through the 0.15 s window, returns identity scale when timer is <= 0.

Entry points

  • getEnemySquash(typeId: string, time: number, entityId?: number): SquashResult — looks up ENEMY_PATTERNS[typeId] (falls back to DEFAULT_ENEMY_PATTERN) and calls computeSquash. Used by enemy draw paths in engine/rendering/draw.ts.
  • getShipSquash(time: number, heat?: number): SquashResult — builds a blended sine pattern from SHIP_PATTERN_COOL / SHIP_PATTERN_HOT via the sqrt-heat curve, then calls computeSquash with entityId = 0. Used by ship draw path in engine/rendering/draw.ts.
  • getDamageSquash(dmgSquashT: number): { sx, sy } — one-shot impulse for enemy hit reactions, called from engine/bridge.ts against each enemy’s _dmgSquashT countdown.
  • computeSquash(pattern, time, entityId) — internal core that evaluates the wave function and produces volume-preserving scale factors.
  • Exported types: SquashResult.

Pattern notes

  • Volume-preserving deformation uses the approximation (1 + d) on the primary axis and (1 - d) on the secondary axis; exact reciprocal would be 1 / (1 + d) but the difference is negligible at the configured amplitudes (~5% maximum).
  • Enemy pattern keys follow the archetype_rarity convention (orb_common, charger_legendary, etc.) matching the v2 enemy typeId scheme.
  • Pattern grouping intent: orbs use slow pulse (breathing), chargers use fast sine (aggressive buzz), shooters use triangle (piston chug), mortars use slow sawtooth (heavy lurch).
  • Ship sqrt-heat curve ensures the ship is never visually static — even at zero heat it breathes at the COOL frequency and amplitude — while preventing jitter at maximum heat.
  • The damage-pulse scratch object is the only memory-allocation optimization in the module; the rationale comment in source notes it eliminated 96+ object allocations per frame in heavy combat. Callers must not store or queue the returned reference.
  • Phase offset has two sources: per-pattern phaseOffset (unused in the current tables but supported) and per-entity stagger from entityId * 0.618…. Passing entityId = 0 (the default for the ship) yields a deterministic phase.
  • All frequencies were tuned down approximately 40% from initial values for a more organic feel; see the comment above ENEMY_PATTERNS.