Beacon System

Hubs are the only places in the world where events spawn. Every hub is a dormant beacon — dark by default, with one or more charge events anchored to its center. The player drives the run forward by visiting hubs, completing the events inside them, and lighting up the map.

What a beacon is

A beacon is just a hub treated as a one-shot quest target. Each hub on the generated map carries a hubId and a radius hub.r. Events tagged with that hubId use the full hub radius as their charge zone (engine/world/events.ts, “Hub events use full hub radius as the charge zone”), so the player can roam the hub freely while charging. There is no separate “beacon entity” — the hub itself is the beacon, and its center is the lantern.

The activation loop

  1. Player enters hub. Events tagged with the hub start charging while the player is inside the hub circle. Charge accelerates by +10% per second spent inside (engine/world/events.ts).
  2. Player stands at center. A separate lantern fill at hub.x, hub.y ticks up while the player is inside the inner activation circlehub.r * 0.35 (ACTIVATION_RADIUS_FRAC = 0.35 in engine/world/illumination.ts).
  3. Fill completes. Default fill time is 4 seconds (DEFAULT_FILL_SECONDS = 4); LevelConfig.illuminationSpeed scales this between near-instant (0) and ~5s (1). Leaving the inner circle drains the meter at DRAIN_RATE = 0.5 per second.
  4. Hub illuminates. system.illuminationMap.set(hub.id, true) flips. The hub is now permanently lit for the run, and system.hubsIlluminated increments.

What illumination changes

The moment a hub lights up, onHubIlluminated(hubId, hub) (engine/bridge-hub-illumination.ts) fires and cascades four effects:

  • Dark-only events get purged. Every event in world.events with hubId === hubId and zoneConstraint === 'dark_only' is either spliced out (if idle/failed) or marked failed (if active). The completed event keeps its done flash animation untouched.
  • Zone classification flips. The hub’s cells in the zone grid switch from hub_dark to hub_lit (engine/world/zone-classifier.ts). Connected spokes also light up — isSpokeLit returns true if either endpoint hub is illuminated.
  • Enemies flee. Every enemy within hub.r * 2 of the hub center gets a radial push: force = 300 * (1 - dist / (hub.r * 2)). Closer enemies get shoved harder.
  • Counter increments. world._hubsIlluminated ticks up. The director reads this as a run-progression input.

Spawn-pool consequence

Illumination drops the spawn rate floor at lit hubs and their connected spokes. Dark zones keep the higher hostile spawn pressure; lit zones become friendlier travel corridors. illuminated_only events become eligible to spawn at lit hubs, replacing the cleared dark_only set with a different rotation tuned for a player who has already cleared the area.

Permanence

Illumination is run-permanent. Once illuminationMap.get(hub.id) === true, nothing in the run resets it. The system header is explicit: “Illumination is permanent — once lit, always lit. This is the core progression metric of every run.” A new run rebuilds the level data and the map starts dark again.

Why this shape

Hubs as event beacons gives every run the same structural rhythm:

  • Dark map → directed exploration. Hostile spawn rates and dark_only events push the player to find the next hub.
  • Hub clear → safe pocket. Completing the lantern collapses the local threat and clears the dark-only event clutter.
  • Lit network grows. Each beacon connects through spokes, so successive clears thread safer paths back to old hubs.

There is no separate “quest log” or “event picker.” The map IS the quest log; the beacons are the steps.

  • engine/world/illumination.ts — lantern state, fill tick, setIllumination, getIlluminatedHubIds
  • engine/bridge-hub-illumination.tsonHubIlluminated cascade
  • engine/world/events.tshubId, zoneConstraint, hub-event charge logic
  • engine/world/zone-classifier.tshub_dark / hub_lit / spoke_dark / spoke_lit resolution
  • engine/world/event-spawner.ts — spawn-radius window around the ship