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 projectedwaveSize(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 projectedwaveSizeto tint intensity.waveSize 10→0.20;waveSize 16→0.45. Linearly interpolated, clamped at the top end.fireWaveTelegraph(waveSize)— the public entry point. No-ops ifwaveSize < BIG_WAVE_THRESHOLD. Triggers the three cue layers (screen tint, edge vignette, audio sting) and records telemetry._kit— lazy singletonVfxLayerKitinstance. Initialized on the firstfireWaveTelegraphcall, never torn down in production._resetForTests()— test-only hook that clears the singleton.
READS FROM
../vfx/boss-layers—createVfxLayerKitand theVfxLayerKittype. The kit exposes thescreenTintPulseandedgeVignetteprimitives that this module composes.../audio/micro-sfx—MicroSfx.play('wave_incoming')for the warning sting. Thewave_incomingrecipe is defined insidemicro-sfx.ts.../telemetry/collector—telemetry.recordDirectorPhasefor cloud telemetry of telegraph cadence.
PUSHES TO
- VFX layer kit (
screenTintPulse,edgeVignette) — produces the two visual layers that the existingtickBossVfxLayers/renderBossVfxLayersAdditivepipeline ticks and renders every frame. This module does not run its own update loop. MicroSfxqueue — enqueues thewave_incomingaudio 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
_telegraphFiredflag 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 fromengine/enemies/spawner.tsinside the wave-burst block, exactly once per wave, gated by a_telegraphFiredflag 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_kitsingleton.
Pattern notes
- Lazy singleton kit.
_kitisnulluntil the firstfireWaveTelegraphcall. Avoids touching the boss VFX layer array at module load and lets tests reset state. - Big-wave gating in the module, not the caller.
fireWaveTelegraphreturns early ifwaveSize < 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.20to0.45— deliberately below the boss tint range so the player learns the palette distinction. The linear shape0.20 + min(1, (waveSize - 10) / 6) * 0.25means it saturates atwaveSize = 16, the spawner’s upper bound. - Edge vignette is louder than the tint.
intensity * 1.4on the vignette versusintensityon 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/renderBossVfxLayersAdditivepipeline — no new render path was added for this feature. - Color choice.
#FF4030is 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
recordDirectorPhasecall 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
_telegraphFiredgate is load-bearing for cue stacking.