Event System

What it is

Events are world-space hold-to-charge zones placed at hub centers and scattered across the level when a run starts. Standing inside an event’s ring fills a charge meter; when the meter completes, the event dispatches a type-specific reward (XP-orb magnet pulse, level up, weapon chest, artifact upgrade, hull regen station, comet shower, enemy pulse, gravity vortex, horde, forager, cascade, starpower). Events are the primary mid-run beat-shape — they pull the player off the main loop of “shoot enemies, vacuum orbs” into a short stand-and-hold mini-encounter with a discrete payoff.

The default pool weights toward levelup (38%) and magnet (14%); reward-heavy types (weapon, artifact, starpower, cascade) are rarer. Pool definition lives in buildDefaultEventPool() in engine/world/events.ts. Each run’s pool can override the default via RunDefinition.node.events (EventPoolConfig.pool: string[]), and the engine substitutes the default when the pool array is empty.

How it gates

Events are gated by two mechanisms — one lifetime, one per-run.

Lifetime unlock (EVENT_KILL_THRESHOLD = 25). Defined in data/run-config.ts. When the player has accumulated 25 lifetime kills, context.eventsUnlocked and context.starsUnlocked flip to true during run assembly. Before that threshold, the metagame builds runs with eventsUnlocked: false and no events seed — events are part of the post-tutorial layer, not the first-impressions layer. The threshold is also reused for starsUnlocked, which gates the star-rewards system.

Per-run completion counter. Every time an event reaches the done phase, game.tracking.eventsCompleted is incremented (see EventManager.update() in engine/world/events.ts). The counter flows into result.progression.eventsCompleted, which feeds the run-summary screen, the credit economy (data/economy.ts), the run-progression service, and challenge conditions of type events_completed.

The user-task framing of “events fire at hubs/spokes when 25 kills accumulate mid-run” describes the lifetime unlock; events themselves are placed at world-generation time, not spawned in response to in-run kill counts.

How events spawn

Placement is deterministic per seed and runs three passes inside EventSpawner.seedHubs():

  1. One event per hub center. Each hub in LevelData.generation.hubs (or the legacy world.hubs fallback) gets one event at the exact hub center. Terrain overlapping the event radius + 80 px is removed and the terrain chunk index rebuilt.
  2. Hub extras. Each hub places floor(densityMult) + Bernoulli(frac) additional events inside its clear radius. Each extra retries up to 6 angles, rejecting placements that are too close (< 500 px) to an existing event or sitting inside terrain.
  3. Level-wide scatter. A jittered-grid pass over the level disc (600 px cells, 75% attempt rate) fills the gaps between hubs. Same rejection rules.

_seededHubs tracks which hubs have already seeded — completed or failed events do not respawn on revisit.

Event lifecycle

Events run a four-state machine inside EventManager.update():

  • idle — placed but never entered.
  • active — player has entered the radius at least once. Charge fills while inside (base 6 s, with +10% rate per second of continuous occupancy); drains at 0.15 / s while outside. Exit timer fails the event after 8 s outside (5 s warning).
  • done — charge reaches the threshold. Reward dispatches in bridge.ts, completion VFX plays for 2.0 s, eventsCompleted increments, an event_complete signal fires at the event position with the event type.
  • failed — exit timer exceeded. 1.0 s failure flash, then the event is spliced (non-hub) or reverts to idle with a full state reset (hub-tagged).

Hub-tagged events have two special behaviors: they never fail (exit timer is reset to 0 each frame while active), and on done they persist as permanent beacons rather than being spliced. The completing phase is reserved in the enum but never assigned in the runtime.

Type-keyed reward dispatch

The charge mechanic is identical across all types — only the reward dispatch in bridge.ts differs. Highlights:

  • magnet — global XP-orb vacuum; every orb flips to exponential lock-on.
  • levelup — player gains one level; fractional XP within the band is preserved.
  • weapon — weapon chest at event center; full-slot pickup grants +1 level to every equipped weapon.
  • artifact — queues an artifact upgrade; falls back to a weapon chest if the player owns < 2 artifacts or all owned are legendary.
  • regen — 30 s healing zone, 1% HP/s while inside. Filtered out of the pool until the ship has taken hull damage in the current run.
  • horde / comet_shower / pulse / vortex — combat sub-events (rare-tier charger pack, comet fragment ring, 600 px damage burst, Magnetar gravity pull).
  • forager — ring of 8 XP orbs at event center.
  • cascade — boss-kill loot piñata reused as an event reward.
  • starpower — 15 s Star Power exclusive state; stacks with existing timers.

The Datacore and Bacta Tank artifacts both listen on the event_complete signal and add extra payouts (random ship upgrades + heal + invuln, or hull/shield restore + damage buff).

What it does NOT do

  • Does not spawn events mid-run in response to kill counts — EVENT_KILL_THRESHOLD only controls the per-account lifetime unlock that flips context.eventsUnlocked at run-assembly time, after which events seed during world generation.
  • Does not respawn events at a hub once that hub has been seeded; the _seededHubs set is checked before every seed call.
  • Does not garbage-collect events by distance or time after seeding; the legacy decayTimer field exists on each event but is no longer ticked.
  • Does not gate event completion by type at the charge layer; only the post-completion reward dispatch differs.
  • Does not extend charge time for higher-value reward types; every type uses the same 6 s base.

Sources

  • src/starship-survivors/engine/world/events.tsEventManager, buildDefaultEventPool, createEvent, phase machine, type list.
  • src/starship-survivors/engine/world/event-spawner.ts — hub seeding + level-wide scatter placement.
  • src/starship-survivors/data/run-config.tsEVENT_KILL_THRESHOLD = 25, EventPoolConfig, context.eventsUnlocked / starsUnlocked / eventTier.
  • src/starship-survivors/services/assembleRunService.ts — populates eventsUnlocked / starsUnlocked from lifetime stats.
  • src/starship-survivors/data/economy.ts, services/runProgressionService.ts, data/challenges.tseventsCompleted flows into credits, run telemetry, and challenge conditions.