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
WaveShapeunion andWAVE_FNSlookup table that resolves a shape name to its function. - The
SquashPatterninterface (wave,freq,amp,axis, optionalphaseOffset) andSquashResultinterface (sx,sy). - The
ENEMY_PATTERNStable keyed bytypeId(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_COOLandSHIP_PATTERN_HOTthat bracket the heat lerp. - The damage-impulse constants
DMG_SQUASH_DUR = 0.15andDMG_SQUASH_AMP = 0.08. - A module-scoped scratch object
_dmgSquashScratchreused across allgetDamageSquashcalls to avoid per-frame GC pressure in combat.
READS FROM
- Caller-provided
time(seconds),typeId/heat, andentityIdarguments — no globals, no game state imports. - The
ENEMY_PATTERNSand 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. getDamageSquashreturns a shared scratch reference — callers must readsx/syimmediately 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/getShipSquashstill allocate fresh result objects. - Does not apply the scale itself — callers wrap
ctx.scale(sx, sy)or passsx/syinto draw helpers. - Does not compensate for non-volume-preservation at large amplitudes; the
(1 + d) / (1 - d)approximation is only exact for smalld. - Does not clamp
entityIdortime; 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 onfrac(t).wavePulse(t)—tanh(sin(t * 2π) * 3), square-ish smoothed throb.- Phase stagger constant: golden-ratio offset
0.618033988749895multiplied byentityIdto break visual lockstep across same-type enemies. - Heat-to-curve mapping for ship:
t = sqrt(heat / 100)with heat clamped to[0, 1];freqandamplerp betweenSHIP_PATTERN_COOLandSHIP_PATTERN_HOTalongt. - 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 upENEMY_PATTERNS[typeId](falls back toDEFAULT_ENEMY_PATTERN) and callscomputeSquash. Used by enemy draw paths inengine/rendering/draw.ts.getShipSquash(time: number, heat?: number): SquashResult— builds a blended sine pattern fromSHIP_PATTERN_COOL/SHIP_PATTERN_HOTvia the sqrt-heat curve, then callscomputeSquashwithentityId = 0. Used by ship draw path inengine/rendering/draw.ts.getDamageSquash(dmgSquashT: number): { sx, sy }— one-shot impulse for enemy hit reactions, called fromengine/bridge.tsagainst each enemy’s_dmgSquashTcountdown.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 be1 / (1 + d)but the difference is negligible at the configured amplitudes (~5% maximum). - Enemy pattern keys follow the
archetype_rarityconvention (orb_common,charger_legendary, etc.) matching the v2 enemytypeIdscheme. - 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 fromentityId * 0.618…. PassingentityId = 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.