PURPOSE

Stand-in-zone event runtime. Each GameEvent is a world-space circular zone the player enters to charge up a reward. All event types share one hold-to-charge state machine; the type field determines only which reward the bridge dispatches on completion. EventManager.update() runs every frame from bridge.ts and returns the list of events that completed this tick.

OWNS

  • EventType union — twelve event kinds: magnet, levelup, weapon, artifact, wheel, starpower, regen, cascade, forager, horde, comet_shower, pulse, vortex.
  • EventPhase union — idle, active, completing, done, failed.
  • GameEvent interface — id, type, position, radius, phase, charge progress, decay/exit timers, mandala VFX phase, completion flash, proximity, optional hubId and zoneConstraint.
  • buildDefaultEventPool() — returns a 72-entry weighted array (levelup 27, magnet 10, weapon 6, regen 5, artifact 4, forager 4, horde 3, comet_shower 3, pulse 3, vortex 3, starpower 2, cascade 2) for uniform sampling.
  • createEvent(type, x, y, rng) — factory; assigns id from monotonic counter, sets per-type radius, rolls decay timer in [25, 40) seconds.
  • EventManager.update(events, ship, world, game, dt) — frame tick; advances state machines, removes finished events, fires Sentry first-tick snapshot, returns completed events.
  • EventManager._updateCharge(...) — per-event hold-to-charge logic.
  • EventManager.reset() — resets the id counter and first-tick log flag.
  • Module-private state: nextEventId counter, _firstTickLogged flag.
  • Per-type radius constants: EVENT_RADIUS_TIGHT = 110 (starpower, regen), EVENT_RADIUS_SMALL = 130 (levelup), EVENT_RADIUS_LARGE = 220 (all others). Charge time is CFG.EVENT_CHARGE_TIME * 1.5.

READS FROM

  • ShipStateship.x, ship.y for proximity and zone test.
  • WorldStateworld.seed and (world as any).biomeId for the first-tick Sentry tag.
  • GameStategame.tracking.eventsCompleted counter; incremented on completion.
  • CFG.EVENT_CHARGE_TIME from ../core/config.
  • Math.random() for spark VFX scheduling (non-deterministic; gameplay charge progression uses dt only).
  • Seeded rng parameter in createEvent for decay timer and initial mandala phase.

PUSHES TO

  • Mutates each GameEvent in place: phase, charged, initiated, exitTimer, _exitWarning, _insideDuration, _mandalaPhase, proximity, completeFlash.
  • Splices completed/failed non-hub events from the events array after completeFlash drains.
  • Particles.add(...) — green-gold spark VFX around the zone perimeter while charging past 10 percent.
  • Particles.burst(x, y, 25, 'spark', {r:80, g:255, b:120}, 100) — completion burst at the event center.
  • game.tracking.eventsCompleted — incremented per completion.
  • Sentry.captureMessage('[EventManager] first-tick snapshot', ...) — single info-level event per run with totals, alive count, hub-tagged count, done/failed counts, ship position, biome id, and seed.
  • Returns GameEvent[] of newly completed events to the caller; the bridge fires the event_complete signal from that list.

DOES NOT

  • Does not fire Sig.event_complete itself. The signal is fired from bridge.ts for each entry in the returned completedEvents array, with cev.x, cev.y, and cev.type as payload.
  • Does not dispatch any rewards. Reward resolution (level-up, weapon box, magnet sweep, cascade drop, forager scan, horde spawn, pulse shockwave, vortex pull, comet shower, regen, starpower, artifact grant) lives in the bridge’s completedEvents loop.
  • Does not place events in the world. World generation and EventSpawner own grid placement and distance-based garbage collection.
  • Does not run decay garbage collection. The decayTimer field is set but never decremented here; cleanup is delegated to EventSpawner.
  • Does not handle full-slot weapon events specially — the chest stays a weapon event and the bridge grants +1 level to every equipped weapon on pickup.
  • Does not gate availability by player state. The regen event is filtered upstream (until hull damage exists).

Signals

  • Outgoing (indirect): event_complete — fired by bridge.ts for each completed event returned from update(). Payload: num1=0, num2=0, f1=cev.x, f2=cev.y, tag=cev.type || ''. The f1/f2 coordinates let event_complete-listening artifacts (registered in engine/world/artifacts.ts) opt into signal-centered placement.
  • Incoming: none — events.ts does not listen to any signals.
  • Listeners on event_complete: legacy artifact handler in engine/world/artifacts.ts (Sig.on('event_complete', _onEventComplete, 50)), plus EffectEngine-routed artifact effects. Used in scenario tests as { type: 'fire_signal', signal: 'event_complete' }.

Entry points

  • buildDefaultEventPool(): EventType[] — weighted sampling pool for spawner selection.
  • createEvent(type, x, y, rng): GameEvent — factory.
  • EventManager.update(events, ship, world, game, dt): GameEvent[] — frame tick; called from bridge.ts (gameplay) and testing/stress-tests.ts.
  • EventManager.reset(): void — call on new run to zero the id counter and re-arm the first-tick Sentry snapshot.

Pattern notes

  • Lifecycle phases: idle (placed, untouched) → active (player entered, initiated=true) → done (charge filled) or failed (left for exitTimeMax = 8s). completing is declared in the union but the runtime transitions straight from active to done. After done/failed, completeFlash (set to 2.0s on done, 1.0s on fail) drains and the event is spliced out.
  • Accelerating charge: while inside the zone, _insideDuration accumulates and chargeAccel = 1 + _insideDuration * 0.10 multiplies dt added to charged. Charge rewards sustained presence.
  • Outside drain: leaving an active event drains charged at 0.15 per second, resets _insideDuration, and accumulates exitTimer. Warning flag flips at exitTimeMax - 5s. Fails at exitTimeMax.
  • Hub-lantern events (ev.hubId set): never spliced. Failed hub events revert to idle (charge, initiated, exitTimer, exitWarning all cleared). Completed hub events keep their done phase, play the flash, then persist as permanent beacons. Hub events drain charge while the player is away but never fail (exitTimer is held at 0).
  • Per-type radius (constants above): tight for starpower/regen, small for the common levelup filler, large for premium rewards and combat sub-events so the rings read as the big opportunity.
  • Reward type vs runtime: all events share the same state machine and VFX. The type field is opaque to events.ts — it travels through update() unchanged and is consumed by the bridge’s switch on cev.type (level_up, weapon_box, magnet_sweep, cascade, forager, horde, pulse, vortex, comet_shower, regen, starpower, artifact).
  • Proximity: proximity = max(0, 1 - dist / (radius * 4)). Drives rendering glow falloff out to four radii from center.
  • Determinism: createEvent advances rng once after computing the radius (kept as a no-op call so seeds generated before the per-type size change still produce the same downstream sequence), then again for the decay timer. Spark scheduling uses Math.random() and is intentionally non-deterministic VFX.
  • Diagnostics: first call to update() per run emits a Sentry info event tagged system=event-manager, stage=first-tick with totals/alive/done/failed/hubTagged counts, ship position, biome id, and seed. Guarded by _firstTickLogged, cleared in reset().
  • Decay field unused here: EVENT_DECAY_RANGE = [25, 40] writes decayTimer for downstream consumers; in-loop comment confirms decay has been removed from this module in favor of EventSpawner distance-based GC.