wave-telegraph.ts

PURPOSE

Fires a brief audio plus screen-tint plus edge-vignette HUD cue 1.5 seconds before a “big wave” spawn so the player can read incoming pressure rather than being suddenly enveloped. The cue is a forewarning, not a boss arrival — intensity is capped well below the boss-fight tint range so the player palette distinguishes “incoming swarm” from “boss spawning.”

OWNS

  • WAVE_TELEGRAPH_LEAD — constant lead time in seconds before the wave burst spawns. Set to 1.5.
  • BIG_WAVE_THRESHOLD — minimum projected waveSize (enemies in the burst) required to trigger the telegraph. Set to 10. Wave sizes from the spawner range 4–16 over a run, so only the upper half of waves telegraph.
  • TELEGRAPH_COLOR — warm red #FF4030, chosen to read as “danger” without overlapping the boss palette.
  • intensityFor(waveSize) — pure mapping from projected waveSize to tint intensity. waveSize 100.20; waveSize 160.45. Linearly interpolated, clamped at the top end.
  • fireWaveTelegraph(waveSize) — the public entry point. No-ops if waveSize < BIG_WAVE_THRESHOLD. Triggers the three cue layers (screen tint, edge vignette, audio sting) and records telemetry.
  • _kit — lazy singleton VfxLayerKit instance. Initialized on the first fireWaveTelegraph call, never torn down in production.
  • _resetForTests() — test-only hook that clears the singleton.

READS FROM

  • ../vfx/boss-layerscreateVfxLayerKit and the VfxLayerKit type. The kit exposes the screenTintPulse and edgeVignette primitives that this module composes.
  • ../audio/micro-sfxMicroSfx.play('wave_incoming') for the warning sting. The wave_incoming recipe is defined inside micro-sfx.ts.
  • ../telemetry/collectortelemetry.recordDirectorPhase for cloud telemetry of telegraph cadence.

PUSHES TO

  • VFX layer kit (screenTintPulse, edgeVignette) — produces the two visual layers that the existing tickBossVfxLayers / renderBossVfxLayersAdditive pipeline ticks and renders every frame. This module does not run its own update loop.
  • MicroSfx queue — enqueues the wave_incoming audio cue once per fire.
  • telemetry.recordDirectorPhase('wave_telegraph', waveSize) — emits a phase event tagged with the projected wave size so the cloud telemetry dashboard can correlate telegraph cadence with player damage-taken and kite-distance metrics.

DOES NOT

  • Does not spawn enemies. Spawning is owned by engine/enemies/spawner.ts. This module is presentation only.
  • Does not gate against repeat fires. The caller is responsible for the _telegraphFired flag inside the spawner wave-burst block so multiple ticks during the 1.5s lead window do not stack cues.
  • Does not run an update loop or hold per-frame state. All decay is handled by the VFX layer kit.
  • Does not depend on the boss subsystem — it only reuses the boss VFX layer primitives because they are compositor-agnostic.
  • Does not render the cue itself. Rendering is done by the shared boss-vfx render pass.
  • Does not produce world entities, projectiles, or hitboxes.
  • Does not adjust gameplay parameters (spawn count, enemy stats, damage). Read-only with respect to game state.

Signals

  • Audio: MicroSfx.play('wave_incoming') — single warm warning sting per fire.
  • Visual: screenTintPulse(TELEGRAPH_COLOR, intensity, WAVE_TELEGRAPH_LEAD, 'easeOutCubic') — full-screen red tint that decays over the full 1.5s lead window with an ease-out-cubic curve.
  • Visual: edgeVignette(TELEGRAPH_COLOR, intensity * 1.4, WAVE_TELEGRAPH_LEAD, 'easeOutCubic') — peripheral edge arrows/vignette pulse, scaled to 1.4× the tint intensity. The intent is for peripheral vision to sell “they’re coming” while the central tint stays readable.
  • Telemetry: telemetry.recordDirectorPhase('wave_telegraph', waveSize) — director-phase event with the projected wave size as payload.

Entry points

  • fireWaveTelegraph(waveSize: number): void — called from engine/enemies/spawner.ts inside the wave-burst block, exactly once per wave, gated by a _telegraphFired flag in the spawner.
  • intensityFor(waveSize: number): number — exported for reuse and unit testing of the intensity curve.
  • _resetForTests(): void — test harness only; resets the lazy _kit singleton.

Pattern notes

  • Lazy singleton kit. _kit is null until the first fireWaveTelegraph call. Avoids touching the boss VFX layer array at module load and lets tests reset state.
  • Big-wave gating in the module, not the caller. fireWaveTelegraph returns early if waveSize < BIG_WAVE_THRESHOLD, so callers can always invoke it and let this module decide. The caller still owns the per-wave fire-once flag because that is wave-lifecycle state, not telegraph state.
  • Intensity curve is small and capped. Range is 0.20 to 0.45 — deliberately below the boss tint range so the player learns the palette distinction. The linear shape 0.20 + min(1, (waveSize - 10) / 6) * 0.25 means it saturates at waveSize = 16, the spawner’s upper bound.
  • Edge vignette is louder than the tint. intensity * 1.4 on the vignette versus intensity on the full-screen tint. Periphery does the “incoming” work; the center stays low enough not to obscure aiming.
  • Reuses boss VFX primitives. Both layers are compositor-agnostic and tick through the existing tickBossVfxLayers / renderBossVfxLayersAdditive pipeline — no new render path was added for this feature.
  • Color choice. #FF4030 is warm red, picked to read as a danger warning without colliding with the boss color palette so cues stay distinguishable.
  • Telemetry is opt-in only for analytics. The recordDirectorPhase call is the only data flow out of the module beyond visual/audio side effects. Removing it would not change gameplay or presentation.
  • Idempotency is a caller contract. This module will happily fire repeatedly if called repeatedly; the spawner’s _telegraphFired gate is load-bearing for cue stacking.