PURPOSE

Owns the active camera: ship-follow positioning, smoothed zoom, transient screen-shake, transient zoom-pulse, and world↔screen coordinate conversion. Exports a single Camera object used by gameplay, rendering, weapons, and combat code.

OWNS

  • Module-scoped screen-shake state: _shakeAmp, _shakeT, _shakeDur, _shakeLastX, _shakeLastY.
  • Module-scoped zoom-pulse state: _zoomPulseFactor, _zoomPulseT, _zoomPulseDur, _zoomPulseEasing, _zoomPulseLastDelta.
  • The per-frame mutation of the shared camera state object (position, zoom, target position, target zoom).
  • Linear-decay shake math and four easing curves for zoom-pulse decay (linear, easeOutCubic, easeOutQuart, easeInOutCubic).
  • Coordinate-conversion helpers (toS, toSx, toSy, toW).

READS FROM

  • game.phase from ../core — only follows ship when phase is playing or birth.
  • ship.x, ship.y, ship.heat, ship.warpT from ../core — follow target, heat-zoom input, warp-puddle zoom multiplier.
  • W, H from ../core — canvas width/height for centering in toS/toW.
  • CFG.HEAT_ZOOM_S, CFG.HEAT_ZOOM_A, CFG.BASE_ZOOM, CFG.CAMERA_LERP from ../core — heat-zoom curve start, heat-zoom amplitude, base zoom level, position-lerp rate.
  • lerp from ../core/utils.

PUSHES TO

  • The shared camera state object: writes camera.x, camera.y, camera.zoom, camera.targetX, camera.targetY, camera.targetZoom each call to update.

DOES NOT

  • Does not draw anything — purely state and math.
  • Does not own the camera state object itself (created by makeCamera in engine/core/state.ts).
  • Does not handle boss-state camera overrides. Comment explicitly forbids re-introducing boss-state camera coupling; bosses are disabled and the camera stays pure player-follow with heat-zoom for the full level.
  • Does not clamp camera to room/arena bounds (room clamping lives in engine/boss/boss-room.ts, applied after Camera.update).
  • Does not allocate in toSx/toSy (the single-axis variants are zero-alloc for tight loops); toS and toW return a fresh {x, y} object.

Signals

  • Camera.shake(amp, dur) — Triggers screen-shake. Amplitude in pixels (world-space), duration in seconds. Stronger-shake-wins replacement: incoming shake is ignored if amp × dur is not greater than the current _shakeAmp × _shakeT. Resets _shakeT to dur.
  • Camera.zoomPulse(factor, dur, easing?) — Triggers a one-shot multiplicative zoom pulse. factor < 1 pulls back, factor > 1 punches in. Decays back to 1.0 over dur seconds using the chosen easing curve (default easeOutCubic). Stronger-pulse-wins replacement by |1 - factor| × dur. Ticks on wall-dt — independent of game timeDilation.
  • Camera.update(dt) — Per-frame: removes last frame’s shake offset, sets targetX/Y/Zoom from ship state during playing/birth, lerps camera toward target, applies new shake offset, applies new zoom-pulse delta. Must be called once per frame before any toS calls.
  • Camera.toS(wx, wy) — World → screen, returns {x, y}.
  • Camera.toSx(wx) / Camera.toSy(wy) — World → screen, single axis, no allocation.
  • Camera.toW(sx, sy) — Screen → world, returns {x, y}.

Entry points

  • engine/core/loop.ts calls Camera.update(simDt) once per simulation step.
  • engine/physics/movement.ts also calls Camera.update(dt) and fires Camera.shake for over-heat thrust and one-shot stop-shake.
  • engine/combat/damage.ts fires Camera.shake on player hits, hull damage, and shield breaks.
  • engine/combat/collision-resolver.ts fires Camera.shake via per-enemy def.hitShake.
  • engine/weapons/weapons.ts fires Camera.shake via per-weapon def.fireShake.
  • engine/bridge.ts fires Camera.shake for event-driven shakes.
  • engine/vfx/boss-layers.ts fires Camera.zoomPulse for boss-layer transitions.
  • data/bosses/doomsayer.ts fires Camera.shake(24, 0.8).
  • Rendering, VFX, world, and HUD modules consume Camera.toS/toSx/toSy/toW for coordinate conversion.

Pattern notes

  • Anchor cleanup. Both shake and zoom-pulse store the last-applied delta and subtract it at the top of the next update before re-lerping. Keeps the camera anchor and camera.zoom undisturbed by transient effects.
  • Follow offset. During playing/birth, targetY = ship.y + 60 / camera.zoom — camera sits 60 px south of ship center (in screen pixels, zoom-corrected), so ship appears higher on screen.
  • Heat zoom. Piecewise: below HEAT_ZOOM_S (0.75) heat fraction, zoom stays at BASE_ZOOM. Above, linearly ramps up by HEAT_ZOOM_A (0.12) across the remaining heat range.
  • Warp zoom. When ship.warpT > 0, target zoom is multiplied by 1 + 0.2 * ship.warpT. Multiplicative so it stacks cleanly with heat zoom.
  • Lerp rates. Position uses CFG.CAMERA_LERP (6). Zoom uses a hardcoded rate of 4.
  • Shake decay. Linear from _shakeAmp at t = dur down to 0 at t = 0. Random per-frame offset in ±mag on both axes.
  • Stronger-wins replacement. Both shake and zoomPulse compare remaining strength (amp × t) of any in-flight effect against the new request’s full strength (amp × dur). A weaker incoming effect cannot interrupt a stronger active one.
  • dt sources. Camera position/zoom lerp and shake decay tick on simDt (called from loop.ts). Zoom-pulse decay ticks on the same dt passed in but documented as “wall-dt only” — see callers for which dt they pass.