Pickup pool

World pickups (XP orbs, magnet bursts, regen, weapon-chest, artifact-box) are not stored in a single shared pool — each type owns its own pool module for cache locality and zero-GC steady state. engine/world/pickups.ts is a thin adapter that delegates the legacy PickupSystem surface to the columnar XP-orb pool in engine/world/xp-orbs.ts. Other pickup-like entities (crates, weapon-chest reward boxes, artifact boxes, magnet/regen drops) come from their own pool modules (crates.ts, event-driven spawns from event-spawner.ts, etc.) and from boss / event reward dispatch on the bridge.

Per-pickup ownership

  • XP orbsengine/world/xp-orbs.ts. Capacity 1024, columnar Float32Array storage. Owns spawn, magnet pull, exponential lock-on, on-screen / off-screen merge, draw, cull. Active orbs live in indices [0, _count); removal is swap-with-last.
  • Magnet bursts — fired as world events through PickupSystem.triggerGlobalMagnet, which calls xpOrbs.triggerGlobalMagnet() to flip every active orb into exponential lock-on. No standalone “magnet pickup” entity — the trigger is the effect.
  • Crates / debrisengine/world/crates.ts. Pool size 80, sector-aware off-screen spawn, never spawned or culled while on-screen. Drops a fraction (3–9%) of the current level’s XP bar split across orbs spawned by xpOrbs.spawn on break.
  • Weapon-chest / artifact-box pickups — these are not world pool entities. They are reward dispatch outcomes routed through engine/bridge.ts and rendered by the reward-cinematic pipeline (engine/rendering/reward-cinematics/weapon-chest.ts, artifact-glitch.ts).

Collection routing

All pickup collection flows through the bridge for reward dispatch:

  • XP orbs collect via radius check (shouldCollect: magnetRange + ship.radius), then call XpAccum.collect(amount) + Juice.fire('xp_pickup') and increment game.xp. Level-up checks fire downstream in leveling.ts.
  • Crate breaks spawn orbs at the crate position with randomized launch velocity, deferring all consequence to the orb pool.
  • Reward boxes (weapon-chest, artifact-box) are not collected by ship overlap — they enter via the cinematic state machine after a boss / event trigger.

Hitbox / sprite / edge-arrow

  • Hitbox is implicit: each pool owns its own collision radius (orbs: per-orb _radius; crates: COLLISION_RADIUS = 36). Magnet pull uses ship.magnetRange + ship.radius.
  • Sprite is per-pool: orbs use the green glow tile from the atlas; crates use the 📦 emoji baked into a fallback atlas region at 96px and drawn at 80px world size with a heavy Y-offset drop shadow.
  • Edge-arrow indicator (off-screen arrow pointing at world pickups) is not part of the pickup pool surface — it’s a HUD-side effect drawn by engine/rendering/hud.ts based on entity positions outside the viewport.

Why no shared pool

Each type has different lifetime, render cost, and update logic — XP orbs run a 20 Hz merge pass and exponential lock-on, crates never tick while off-screen, reward boxes are one-shot cinematic objects. Forcing them into one heterogeneous pool would waste cache lines and complicate the per-frame branch hot paths. The thin PickupSystem adapter exists only so legacy call sites (boss death, magnet events, dev tooling, tests) keep working.