Camera shake stack

Camera.shake(amp, dur) requests a screen shake. amp is amplitude in world-space pixels; dur is duration in seconds. Implementation lives in engine/rendering/camera.ts.

Single slot, strongest wins

The shake state is a single slot (_shakeAmp, _shakeT, _shakeDur), not a list. A new call only replaces the in-flight shake if amp * dur exceeds the remaining strength _shakeAmp * _shakeT. Weaker shakes during a stronger one are dropped; a much stronger shake overrides whatever was decaying. This is the “stack” — strongest active request wins, the rest are ignored.

Decay

Linear decay over dur. Each frame, _shakeT ticks down by dt, and the current magnitude is _shakeAmp * (_shakeT / _shakeDur). Per-frame offset is ±mag random on each axis, applied to camera.x/y after the position lerp, then subtracted next frame so the anchor doesn’t drift.

Tick domain

Sim dt — affected by timeDilation. During slowmo the shake decays slower, matching the rest of the world.

Callers

SiteCallNotes
Shield break (combat/damage.ts:1037)Camera.shake(11, 0.45)Fixed amplitude when shields drop to zero
Shield hit (combat/damage.ts:933)Camera.shake(4 + shieldFrac*5, 0.18 + shieldFrac*0.12)Scales with shield fraction lost
Hull hit (combat/damage.ts:1129)Camera.shake(6 + hullFracForFx*8, 0.25 + hullFracForFx*0.20)Scales with hull fraction lost
Boss death (Doomsayer) (data/bosses/doomsayer.ts:478)Camera.shake(24, 0.8)Largest one-shot in the game
Enemy-impact telegraph (engine/bridge.ts:2457)Camera.shake(amp, 0.15)Caller computes amp
Pluggable hitShake on bullets (combat/collision-resolver.ts:739)Camera.shake(def.hitShake.amp, def.hitShake.dur)Per-weapon authoring
Pluggable fireShake on weapons (weapons/weapons.ts:2080)Camera.shake(def.fireShake.amp, def.fireShake.dur)Per-weapon authoring
Heat overlimit (physics/movement.ts:388)Camera.shake(heatAmp * over, 0.08)Short duration, refreshed every frame while overheated — acts as a continuous source
Hard-brake stop (physics/movement.ts:380)Camera.shake(stopAmp, 0.2)Wall-impact feel

Heat shake is continuous, not stacking

The heat overlimit shake at movement.ts:388 uses a 0.08s duration but is re-issued every frame while the ship is over the heat limit. Because each new call is the strongest-recent request, it keeps refreshing the slot and reads as a continuous tremor. Any one-shot shake that exceeds it in amp * dur will still override.

Zoom pulse is separate

Camera.zoomPulse(factor, dur, easing?) is a sibling system with the same strongest-wins rule (compared by |1 - factor| * dur) but operates on camera.zoom multiplicatively, decays on wall-dt (independent of timeDilation), and supports easing curves. See _zoomPulse* state in engine/rendering/camera.ts.