Weapon Slot Management
The ship carries weapons in a single fixed-length inventory: ship.weapons, a flat array of weapon instances. Capacity is governed by game.weaponSlotsMax, which defaults to 4 (makeGameState() in engine/core/state.ts) and is overwritten at mission start by runDef.ship.weaponSlotCount from the RunDefinition (engine/bridge.ts). Every gameplay path that adds, upgrades, or chest-resolves a weapon reads from these two fields. There is no second-tier “loadout” structure: what the ship fires is exactly what lives in ship.weapons.
Inventory shape
Each entry in ship.weapons is a per-instance record, not a reference to the global weapon definition:
id— weapon definition key (joined toWEAPON_MAPfor stats and behavior).level— fractional weapon level (1–20). Weapon chests grant fractional increments; level-up upgrade cards grant integer levels.cooldown/cooldownMax— per-instance fire interval, derived fromdef.fireRateat level 1.fireTimer— phase offset within the current cycle. Set toMath.random() * cooldownwhen the weapon is added so a freshly equipped weapon never fires in lockstep with existing ones.defaultAngle— barrel angle from the weapon definition.
Each weapon ticks independently. There is no shared cooldown, no group fire order, and no per-slot configuration outside this record.
Equipping a new weapon
The only routes into ship.weapons are applyReward in engine/world/leveling.ts (case 'weapon', fired when the player picks a new-weapon card from a weapon chest) and the helper addWeapon in engine/weapons/weapons.ts. Both do the same thing: ship.weapons.push({...}). Push appends to the end of the array, so the first weapon added lands at index 0 and subsequent weapons fill 1, 2, 3 in order. There is no slot picker, no preferred index, and no gap-filling.
Equipping also bumps game.weaponsAcquired and appends to game.tracking.weaponsFound. Both feed run telemetry; neither gates further equips.
Capacity check
Before a weapon chest is spawned in the world (engine/bridge.ts, mission start and supply-level chest placement) the game tests ship.weapons.length < (game.weaponSlotsMax || 4). Chest sources are skipped entirely when the inventory is full. The enemy-drop path in engine/combat/damage.ts performs the same check (_dropSlotsFree) but the drop rate is currently pinned to 0, so chests do not drop from enemy deaths.
Slots full → auto-upgrade path
When the player collects a weapon chest, bridge.ts queues a weapon_box reward with a snapshot of slot-fullness at collect time (_slotsFullAtCollect). The snapshot exists because the live recheck at dispatch time used to race against a stale weapons.length and occasionally offer a choice screen when slots had just filled.
When the queued reward dispatches:
- Slots full at collect time → no choice screen. The game runs the
weapon_chest_autocinematic: every weapon inship.weaponshas its level incremented by 1 (capped at 20), the upgrade-show animation plays one icon per equipped weapon flying to its slot, and the level-up phase resolves without ever showing a card. - Slots free →
LevelingSystem.generateWeaponChoicesbuilds a 3-card pick of unowned weapons. If every non-legendary weapon in the unlocked pool is already owned,generateWeaponChoicesreturns an empty array and the sameweapon_chest_autocinematic fires as a fallback — the chest still resolves visibly.
This is the only path that adds a level to every weapon at once from a chest. Level-up reward cards (weapon_upgrade case in applyReward) only touch the single chosen weapon, with an optional sympathetic-resonance cascade from the artifact.
What is not implemented
- Manual swap. There is no path to remove a weapon, replace a weapon in a specific slot, or reorder the array. Once a weapon is pushed onto
ship.weaponsit stays for the run. - Slot targeting on equip. New weapons cannot be placed at a chosen index. They always append.
- Per-slot metadata beyond the instance record. There is no slot-cosmetic, slot-affinity, or slot-bonus system; index in the array is purely the order in which the weapon was acquired.
Level retention
Each weapon’s level and fireTimer live on the instance record, so they survive across all reward screens, mission events, and shooting-star resolutions for the run. Weapon level is independent across weapons — the Sympathetic Resonance artifact and the shooting-star weapons category are the only mechanics that propagate a level gain to multiple weapons in one operation.
Run end resets the entire ship via resetState() (engine/core/state.ts), so the inventory does not carry between missions. Loadout for the next run is rebuilt from runDef.ship.startingWeapons.