Magnet Pickup / Global Magnet Event

Map-wide XP vacuum — a single “magnet” trigger flips every active XP orb on the map into exponential lock-on toward the ship and a golden magnet icon flashes above the ship as visual confirmation. The “vacuum up the field” beat that clears anything the player let drift off-screen.

The trigger primitive

The only entry point is triggerGlobalMagnet() in engine/world/xp-orbs.ts. It is dead simple — it walks every active orb in the SoA pool and:

  • Sets the _collected bit (_flags[i] |= 2) on the orb, which the per-frame magnet update reads as “skip layer 1 / layer 2, go straight to layer 3.”
  • Resets the orb’s _magTime to 0 (so the exponential acceleration ramp restarts cleanly from this instant).
  • Zeroes the orb’s velocity (_vx, _vy → 0) so any leftover spawn-burst momentum cannot fight the lock-on.

After the flag flips, the existing per-orb update loop in xp-orbs.ts:325-335 handles the rest — layer 3 lock-on multiplies acceleration by 200 * 3.0^_magTime per second, capped at 95% of the remaining distance per frame, so the orb arrives at the ship within ~0.5-1 second regardless of how far away it spawned.

The pickups.ts PickupSystem.triggerGlobalMagnet(world) wrapper exists only as a thin adapter so legacy callers (boss death, magnet events, dev tooling, tests) don’t have to import the SoA module directly. It delegates to the same function.

What fires it

Three distinct callers in the codebase invoke triggerGlobalMagnet():

1. Magnet world event completion

The canonical case. In engine/bridge.ts:2586, when EventManager.update() returns a completed event whose cev.type === 'magnet', bridge calls xpOrbs.triggerGlobalMagnet() and sets game._magnetIconTimer = 1.5 (1.5 seconds of UI display). This is the world-event reward path that ships alongside levelup, weapon, and artifact event rewards — see wiki/gameplay/events.md for the full event reward pipeline.

The event itself is the “pickup” framing — the player walks through the magnet event marker (which behaves like every other completable world event: charge / defend / race / instant), and on completion the global magnet fires once.

2. Scrap Pile prop-break synergy (15% chance)

Defined in engine/world/props.ts:480. When the player breaks a scrap_pile prop, there is a SCRAP_MAGNET_CHANCE = 0.15 roll. On success, spawnScrapMagnetPulse() fires (props.ts:830):

  • Draws a grey magnet-pulse ring (10 dust particles travelling inward toward the ship from 60-90 px out) to sell the “buff lands on you” beat even when no orbs are nearby to vacuum.
  • Calls xpOrbs.triggerGlobalMagnet() — same primitive, same effect on the orb field.
  • Fires the extraction_done audio cue.
  • Records prop_break:scrap_magnet_pulse to telemetry with the live orb count.

Note: this synergy does not flash the magnet icon (it has no _magnetIconTimer write) — the in-world inward-particle ring is the feedback channel. Scrap Pile is the workhorse prop tier (densityMult 1.0), so the 15% rate keeps the proc feeling like a found bonus rather than a routine drop.

3. Boss death

The xp-orbs source comment (xp-orbs.ts:140) names “boss death” as a caller. The actual call site lives in the boss-death cinematic dispatch; the effect is identical — every active orb on the map locks onto the player as part of the boss-clear payoff alongside the standard chest / artifact drops.

The magnet icon UI

A single golden magnet icon flashes above the ship for 1.5 seconds whenever the magnet-event path completes. Rendering lives in engine/rendering/hud.ts:2451-2510:

  • Reads game._magnetIconTimer (game-state field defined in engine/core/types.ts:365, initialised in engine/core/state.ts:185).
  • Position: screen-center X, H * 0.32 Y (just above the ship in normal play).
  • Size: 28 px (scaled by uiScale).
  • Shape: drawn procedurally — black rectangular arms with red tips (classic horseshoe-magnet silhouette), white radial-gradient glow behind it.
  • Animation: 70 ms fade-in, hold at full alpha, 20% fade-out tail, multiplied by an 18 Hz sine flash for the “active suction” feel.

The timer decays in bridge.ts:3270-3273 (game._magnetIconTimer = Math.max(0, _magnetIconTimer - dt) per frame). When it hits 0 the HUD branch is skipped.

The icon is intentionally only wired to the magnet-event path. Scrap-pile pulses and boss-death pulses use their own in-world feedback (particle ring / boss-clear cinematic) so the icon stays a clean “an explicit magnet reward landed” signal.

Cross-references

  • engine/world/xp-orbs.ts — the primitive plus all three magnet layers (snap-on / base pull / exponential lock-on).
  • engine/world/pickups.ts — thin PickupSystem adapter.
  • engine/bridge.ts:2586 — magnet-event completion → primitive + icon timer.
  • engine/world/props.ts:830 — Scrap Pile synergy.
  • engine/rendering/hud.ts:2451 — magnet icon render.
  • wiki/gameplay/events.md — full event reward pipeline (where magnet sits alongside levelup / weapon / artifact).
  • wiki/gameplay/concepts/destructibles.md — Scrap Pile and the other prop-break synergies.