Skip to content

Loot & Inventory Redesign — Design Doc

Status: Proposal / pre-implementation. No code changed yet. Scope: the in-raid looting + inventory loop and how it connects to stealth play and meta power. Lens: casual mobile first (short sessions, touch, low micromanagement, dopamine-forward, forgiving). Companion reading: docs/research/arc-raiders-looting-inventory-research.html (the reference-mechanics deep dive) and PROJECT_CONTEXT.md (design pillars).


1. Why this doc exists

Looting is currently the weakest part of the game. Three concrete symptoms, all structural rather than cosmetic:

  1. Looting doesn't feel rewarding.
  2. You can't manage the bag during a run — it fills with trash you can't drop, sort, or use, and once full you're locked out of new loot until the run ends.
  3. Powerful items trivialize the game — once you've extracted strong gear there's no in-run counter-pressure, so every subsequent raid is a stomp.

This doc audits the current system, maps the Arc Raiders reference patterns onto our gaps, and lays out the pillars we should build — each with why it matters for a casual player, concrete variants, a recommendation, and the data/config it implies. It ends with a phased rollout so we can ship value incrementally without a balance upheaval.


2. Current state — how the system actually works today

Traced from game.js / config.js:

Stage Implementation File anchors
Loot creation openChest() instantly rolls 2–3 randomLoot items + a possible bonus epic + potions/cells + gold + blueprints, all auto-collected at once. Mobs/bots drop items via mkLoot. openChest, mkLoot, randomLoot
The bag G.bag is a flat array, hard-capped at CFG.player.bagCap (18). collect() rejects new loot when full ("BAG FULL"). No drop / sort / use / equip. Write-only until the run ends. collect, G.bag
Power Frozen at newRun() by computeStats(save.loadout). Found gear is inert during the raid — it cannot affect stats until equipped in the lobby on a later run. newRun, computeStats
In-run usables Only potions / cells — plain integer counters, not bag items. usePotion, useCell
Banking On extract: every bag item → save.stash; gold/blueprints → save. On death: bag lost, stash & equipped gear safe. No protected slot. endRun
Value lootValue(it) is a derived stat-sum, only surfaced on the result screen. The player can't judge an item mid-run. lootValue

One-line summary: loot is a deferred, invisible, auto-collected number dump poured into a write-only bag, with zero in-run agency and zero in-run impact.

2.1 Root-cause map (symptom → cause)

  • Not rewarding ← auto-vacuum (no agency) · all loot is the same kind (nothing to recognize or triage) · payoff is invisible and deferred to the result screen (you never feel stronger).
  • Can't manage ← the bag exposes no verbs; scarcity (cap 18) exists without the drop/swap/use decisions that would make scarcity into gameplay. The cap is a wall, not a choice.
  • Power trivializes ← stats are locked pre-run from stash gear, so the loop is extract good gear → stomp next run; nothing in-run pushes back and loot can't create interesting mid-run power decisions. The "no gear loss" twist also removed the genre's core tension and nothing replaced it inside the raid.

3. Design philosophy for a casual extraction looter

The reference doc's own Mobile Framework is the north star. Distilled for our decisions:

  • Triage at a glance, not a spreadsheet. Rarity color + a clear "sell-only" marker so a player knows keep/drop in <1s. No stat math required mid-run.
  • Tactile, one-tap verbs. Tap to open the bag, tap to drop, long-press to protect. Minimize taps; never force grid Tetris on a phone.
  • Felt rewards, immediately. A casual player should feel the dopamine this run — "I found a better sword, I'm stronger now" — not read a number on the end screen.
  • Forgiving by default, risky by choice. A guaranteed-keep slot softens the sting of a lost run; greed is opt-in, not a hard wall.
  • The bag stays smaller than the world. Density > capacity creates the choosing. But the ceiling must come with verbs, or it's just frustration.
  • Protect the home stash absolutely; risk the run bag completely. The non-negotiable wall. A casual player will happily re-attempt a lost run but churns forever if a bad session erases their collection.

Casual guardrails (what we deliberately avoid for now): spatial/Tetris grids; a hard bag-full lockout; punishing weight micromanagement (see §7, deferred); any system that needs the player to do arithmetic during combat.


4. Reference patterns we're pulling from

From arc-raiders-looting-inventory-research.html, the portable spine (their L-## tags):

  • L-01 — Inventory as an equippable pre-run choice, not a number that only grows (capacity vs. protection vs. utility trade-off).
  • L-02 — One protected "Safe Pocket" slot turns binary win/lose into a dial. The single biggest anti-frustration lever, and the most casual-friendly idea in the whole system.
  • L-03 — Make the bag smaller than the world. Forced prioritization is the gameplay.
  • L-04 — Two constraints (slots + weight). Weight taxes mobility instead of blocking. (Deferred — see §7.)
  • L-06 — Death is clean: total run-loot loss, untouchable stash, no recovery. Legible stakes.
  • §04 — Loot variety + audio: distinct sources/types each with a risk profile; looting is loud and exposed.

5. The pillars

Three pillars, in priority order for casual impact. Weight is a deferred fourth (§7).

Pillar A — Runtime inventory management (the core ask)

Why it matters (casual): the #1 and #2 complaints both live here. Right now the bag is a black box that fills with junk and then locks you out — the opposite of satisfying. Giving the player agency over their haul (see it, sort it, drop the trash, protect the gem) is what converts "carrying loot" into "playing with loot." The Safe Pocket specifically is the anti-frustration valve that lets us keep death meaningful without making it feel cheap — vital on mobile where a wiped run otherwise churns players.

Variants

  • A1 · Tap-to-manage bag + 1 Safe Pocket (recommended, low effort).
  • Tapping the bag HUD opens a simple grid: each item shows its rarity color and lootValue. Verbs: Drop (frees the slot, spills the item on the ground so it's recoverable / contestable), Sort by value, and long-press → Safe Pocket.
  • Safe Pocket = 1 slot whose item survives death and auto-extracts (L-02). The recurring "is this the keeper?" decision.
  • Bag-full becomes a soft prompt ("drop your lowest-value item?") instead of a hard rejection — keeps the friction, removes the dead-end.
  • A2 · Add the weight constraintdeferred to §7.
  • A3 · Spatial Tetris grid — explicitly rejected for casual (violates "minimize micromanagement").

Data / config implied by A1 - item.safe (bool) — is this the Safe-Pocket item. - G.safePocket — the single protected slot (separate from G.bag). - CFG.player.safeSlots (default 1) — future-proofed for 2–3 via meta progression. - endRun: on death, bank G.safePocket (if any) to save.stash before discarding G.bag. - UI: reuse the existing loot-reveal / inventory styles; add a bag overlay + long-press handler.


Pillar B — Make looting rewarding

Why it matters (casual): even with a manageable bag, looting is flat if every chest spews the same anonymous gear and the reward is invisible until the end. Casual players are driven by recognition (ooh, a gold drop!) and immediate payoff. This pillar adds variety to recognize and power you feel now.

Variants

  • B1 · Loot types, not just gear (recommended, medium effort). Split drops into a few instantly-readable categories instead of "all gear": | Type | Role | On-screen tell | |---|---|---| | Gear | equip / stash (the build loop) | slot icon + rarity color | | Valuables | pure sell → gold | 💎 "sell-only" diamond marker | | Materials | stack → blueprints / crafting | small stack count | | Consumables | potions / cells / (future) grenades | usable in-run | | Keys / quest | unlock vault / objectives | 🔑, Safe-Pocket-worthy | Chests spawn their contents as grabbable items on/near the chest rather than auto-vacuuming, so each pickup is a deliberate, exposed action. Triage replaces the number-dump. (Reference §02/§04.)

  • B2 · Container variety + audio discovery (medium). Distinct sources with risk profiles: quick crates (fast, poor) · locked caches (loud, rich — the "ticking" audio cache) · mob husks (combat→loot) · rival packs (biggest score). Each loot grab pings makeNoise, so how and where you loot is itself the stealth decision. Leans on the existing risk-gradient map (rich core, poor rim).

  • B3 · Field-equip (recommended, the dopamine lever). Let the player equip a found gear piece mid-run — only while safe (in a bush / out of combat), costing a short sustained-presence channel like opening a chest. Stats update live. A great drop becomes a felt power spike and a risk choice ("stop to gear up vs. keep moving while exposed"). Optionally, run-found gear is at-risk until extracted (lost on death like the rest of the haul), which quietly restores extraction stakes the "no gear loss" twist removed.

Why B1+B3 together: B1 makes the moment of finding legible and varied; B3 makes the found item matter immediately. Together they answer "doesn't feel rewarding" from both ends — recognition and payoff.

Data / config implied - item.type ("gear" | "valuable" | "material" | "consumable" | "key"), item.sell (gold value for valuables), item.stack (materials). - mkLoot / randomLoot extended to roll a type, not just a gear piece. - CFG.loot group: per-type drop weights by rarity/ring, fieldEquipTime (channel seconds), fieldEquipSafeOnly (bool), runGearAtRisk (bool). - openChest reworked to place grabbable items instead of auto-collecting.


Pillar C — Keep power from trivializing the game

Why it matters (casual): casual ≠ no challenge. If a geared player one-shots everything, the loot loop loses all tension and the stealth pillar becomes pointless. We need pressure that scales with the player without punishing newcomers — difficulty that follows power rather than gating it.

Variants

  • C1 · Enemy power-scaling (partially shipped). enemyScale already ramps creature HP/damage vs. the player's Power. Extend: also nudge spawn density/tier with power so a strong player meets more and nastier things, not just spongier ones.
  • C2 · Risk-gated rewards (recommended, mostly tuning). Strengthen the core/rim gradient so the best loot is only reachable through lethal zones. Power then changes which zone is the scary one — it never removes risk. Keeps "route over reflexes" relevant at every gear level.
  • C3 · Strong = loud / heavy (optional, ties to deferred weight). Heavier/stronger gear costs noise or speed, so brute-forcing carries a stealth tax.

6. How this reinforces the stealth pillar

Every pillar is designed to reward sneaky play, per PROJECT_CONTEXT.md pillar 1–2:

  • Looting is exposure. Grabbing items (B1) and cracking caches (B2) makes noise and takes time — a careful, well-positioned looter thrives; a greedy vacuumer gets caught.
  • Triage rewards patience. A Safe Pocket + drop verbs (A1) reward the player who thinks about what's worth the risk, not the one who grabs everything.
  • Field-equip rewards safety. B3 only works when you've made yourself safe (a bush, a cleared room) — stealth literally unlocks your power spikes.
  • Risk-gated rewards (C2) keep the rarest loot behind the stealth gauntlet of the core.

7. Deferred: the weight system (documented, not building yet)

Per direction, we are not implementing weight now — but it's the natural next constraint and we should keep the door open so we don't have to retrofit.

  • What it is (L-04): items carry weight; the bag is capped by slots and weight. Overweight = slower move/dodge (rides the new move system: baseMove/speedGain), never a hard block. A greedy haul makes you slow and catchable.
  • Why it's compelling later: it's the cleanest mechanical weld between greed and the stealth pillar — the more you carry, the easier you are to catch. It also adds a second, cheap optimization axis (light junk vs. few heavy valuables).
  • Why not now: for a first pass it adds cognitive load and balance surface that fights the casual lens. Ship agency + reward + variety first; layer weight once the bag is fun.
  • Keep-the-door-open hooks: include a weight field on items from the start (unused/0 for now), and reserve CFG.player.weightCap + CFG.move.overweightSlow in the config plan so enabling it later is data-only, not a refactor.

8. Proposed data shapes & config (consolidated)

// item (superset — gear keeps today's fields; new fields default harmlessly)
{
  type: "gear" | "valuable" | "material" | "consumable" | "key",
  slot, name, icon, rarity, level, stats,   // (gear, unchanged)
  sell: 0,        // valuables: gold on sell
  stack: 1,       // materials: stack size
  weight: 0,      // reserved for the deferred weight system (unused for now)
  safe: false,    // currently in the Safe Pocket
}

// run state additions
G.safePocket = null;   // single protected item (banked even on death)

// config additions
CFG.player.safeSlots = 1;        // Safe-Pocket capacity (future: 2–3 via meta)
CFG.loot = {                     // new group → "Enemies"/"Map" or a new "Loot" tab
  fieldEquipTime: 2.5,           // channel seconds to field-equip
  fieldEquipSafeOnly: true,      // only while in a bush / out of combat
  runGearAtRisk: true,           // run-found gear lost on death (extraction stakes)
  // per-type drop weights by ring/rarity live here too
};
// reserved for the deferred weight pass (do not wire yet):
// CFG.player.weightCap, CFG.move.overweightSlow

The tuning panel auto-builds from CONFIG, so any CFG.loot.* we add surfaces automatically; assign the new group to a tab in TUNE_TABS (likely a new Loot tab or fold into Map).


9. Phased rollout

  1. Phase 1 — Agency + safety (A1 + B1). ✅ SHIPPED. Openable triage bag (drop/sort), 1 Safe Pocket, loot split into types (gear/valuable/ material/consumable/coin) with rarity colors. Chests scatter grabbable ground items; the paused bag panel does drop/sort/protect. Fixed complaints #1 and #2.
  2. Phase 2 — Felt rewards + exposed looting (see §12/§13). ✅ SHIPPED. Non-pausing bottom-sheet container looting (rooted + vulnerable, damage-interrupt), origin-tag safety (equipped/stash gear kept on death, found loot forfeit), live loadout + recomputeLive, and a ~2s rooted channel field-equip with a ▲/▼ power-delta preview. Welds loot to stealth.
  3. Phase 3 — Containment (C1 + C2 tuning). Extend enemy scaling to density/tier; tighten risk-gated rewards so power never removes risk.
  4. Phase 4 — Weight (deferred). Turn on the reserved weight constraint once the bag is already fun; tune overweightSlow.

Each phase is independently shippable and testable in-browser.


10. Open questions / risks

  • Drop = ground spill vs. destroy? Spilling makes drops recoverable/contestable (richer, reuses G.items) but adds clutter; destroying is simpler. Leaning spill.
  • Field-equip & "no gear loss": if run-found gear is at-risk (B3 option), confirm it can't ever threaten stash/equipped gear — the §3 wall is non-negotiable.
  • Safe Pocket scope: 1 slot to start; expanding to 2–3 should be a meta reward (L-01 pack-as-choice), not a free default.
  • Economy retune: moving gold/valuables out of the auto-dump into pickup-able items will shift run income; the result-screen tally and stash sink need a pass.
  • Bag size: revisit bagCap (18) against "bag < world" once drop/triage exists — it may want to be smaller to make triage bite.

11. Unity-port notes

Each pillar is a small, data-driven system, not an architecture change — consistent with the "web build is a design vehicle" framing:

  • Loot types + drop tables → ScriptableObject loot tables (already the config.js intent).
  • Safe Pocket / bag → a simple inventory component with a protected-slot flag.
  • Field-equip → reuse the chest "sustained-presence channel" interaction.
  • All tunables stay in CONFIG groups that map 1:1 onto SO assets.

12. Phase 2 — decided spec (container looting + field-equip)

Decisions locked: A-i bottom-sheet looting · B-i channel field-equip · origin-tag safety. This supersedes the lighter Phase-1 ground-spill for containers (chests/caches/ corpses); ground-spill remains only for quick mob drops and player-discarded items.

12.1 Container looting — non-pausing bottom sheet (A-i)

  • Tapping a chest/cache/corpse opens a bottom sheet over the lower ~⅓ of the screen; the top ⅔ keeps rendering the world (threats, cones, your raider). The game does NOT pause.
  • You are rooted at the container while the sheet is open (can't move) — that's the exposure cost. A persistent ✕ Leave frees you to move instantly.
  • Verbs: tap an item = take one (instant) · Take All · gear shows an Equip action.
  • Graceful interrupt (mobile fairness): taking a hit flashes the sheet red; after a hit (or on alert) it can auto-close so you're never helplessly menu-locked. Auto-fire at visible enemies may continue while rooted (designer toggle).
  • Exposure reinforcement: opening/looting pings makeNoise → draws enemies. Loud, risky, on-reference.
  • Own-bag management stays a paused panel (the existing Phase-1 bag): housekeeping is calm; world looting is tense. This split is the casual/mid-core balance.

12.2 Field-equip — channel equip (B-i)

  • Gear in the sheet (or paused bag) has Equip → a ~CFG.loot.fieldEquipTime (~2s) channel while rooted/vulnerable; interrupted if you move or (optionally) take a hit.
  • On completion, the run's live loadout updates and stats recompute live. The displaced piece goes to the bag (origin rules below).
  • Felt reward + commitment under exposure — equipping is itself a stealth-gated power spike.

12.3 The no-gear-loss pillar — origin tags (REVISED — see §15)

Tag every item: origin: "stash" (your permanent gear) or "found" (looted this run). Field-equip = "use it now, keep it only if you extract": - Found gear is at-risk even when equipped — lost on death (loadout reverts to your start gear), kept only on extract (becomes permanent). - Displaced originals are reserved (G.reserved) — pulled out of the at-risk bag, non-droppable; restored to the loadout on death, banked to stash on extract. Your permanent gear is never lost. - Forfeit on death: found bag loot + found-equipped gear (not Safe-Pocketed).

The earlier "equipped found gear is kept on death" rule was an exploit (permanent upgrades by dying, + droppable displaced originals). Corrected in §15.

12.4 Implementation notes / risks

  • Live loadout + recompute. Today stats bake once in newRun from save.loadout. Phase 2 needs the run player to carry a live loadout and a recomputeLive() that updates atk/crit/speed/maxhp/shieldMax/power. Scale current HP/shield proportionally to the new max (don't refill) so equipping isn't a free heal.
  • Rooted state. Reuse the chest "sustained-presence" channel + a rooted/looting flag on the player that blocks movement input but not rendering/threat AI.
  • New non-pausing sheet UI distinct from the paused bag panel; bottom-anchored, big targets.
  • Containers hold inventories. Chests stop spilling on open; they hold their rolled items until taken (overflow stays put). Corpses (esp. rivals) expose their haul via the same sheet.
  • Config: CFG.loot.fieldEquipTime, fieldEquipInterruptOnHit (bool), lootRootNoise (noise radius while looting). Surfaces in the Loot tuning tab automatically.

13. Phase 2 — engineering build breakdown

Status: ✅ all steps shipped (step 1 plumbing, 2a container sheet, 2b channel field-equip). Ordered, independently testable units. File anchors are approximate (current game.js). Build in this order; each step leaves the game runnable.

Step 1 — Data plumbing + recomputeLive() (invisible, verifiable, unblocks 2a/2b)

1a. origin tag on every item. - mkItem (game.js ~91): add origin: "found" to the returned object (default for all generated loot — randomLoot, mkValuable/Material/Consumable/Coin inherit/set it). - newRun (~154): when building the run, deep-copy save.loadout and stamp each piece origin: "stash". Stash/loadout items that ride in with you are always "stash". - Rule it enforces: death banking keeps equipped + Safe Pocket + every origin:"stash" item (even if a swap displaced it into the bag); forfeits only origin:"found" bag items.

1b. Live run loadout. - newRun: add player.loadout = deepCopy(save.loadout) (stamped "stash"). The run mutates this, never save.loadout, until banking. - Keep player.power/atk/crit/speed/maxhp/shieldMax as today but sourced from a shared compute.

1c. Extract a shared derive helper + recomputeLive(p). - Pull the perk/set/derived block out of newRun (~176–183: bushSpeed, backstabBonus, mobDmgBonus, setMobDmg, setStealth, perks, sets) into applyLoadout(p, loadout). - recomputeLive(p): 1. const s = computeStats(p.loadout) (game.js:126) + the speed formula (CFG.move.baseMove + max(0, s.speed − baseSpeed) × speedGain). 2. Scale current pools, don't refill: p.hp = clamp(round(p.hp * s.hp / p.maxhp), 1, s.hp); p.shield = round(p.shield * s.shieldMax / p.shieldMax) (guard divide-by-zero on first call). 3. Assign maxhp/atk/crit/speed/shieldMax/power; call applyLoadout(p, p.loadout); refreshHud(). - Test: call recomputeLive right after newRun with the unchanged loadout → every stat must equal the newRun-baked values (proves the math is identical before any swap exists).

Config added (step 1): none required yet.

Step 2a — Container bottom-sheet + rooted looting + origin death-banking

2a-1. Chest holds an inventory; opening shows the sheet (not scatter). - openChest (game.js ~907): keep the roll, but store into c.loot = drops and do not push to G.items. Keep burst + makeNoise. Set c.opened = true. - The crack channel in interactObjects (~887–900) stays (loud, vulnerable unlock). On c.progress >= 1openLootSheet(c) instead of leaving scattered items.

2a-2. Rooted, non-pausing state. - Add p.rooted (or reuse p.interacting). While a sheet is open: block movement/path input in onTap + the movement section (~757), do not set pausedupdate()/render() keep running. Auto-fire while rooted: off by default (you're rummaging). - lootSheetOpen module flag mirrors bagOpen.

2a-3. #lootSheet UI (new, bottom-anchored — NOT a .screen). - index.html: a bottom sheet div (lower ~⅓), pointer-events only on the sheet so the world shows through the top. style.css: .loot-sheet { position:fixed; bottom:0; ... }. - openLootSheet(c) / renderLootSheet() / closeLootSheet() mirror the bag-panel functions. Cards = tap-to-take (calls collect, removes from c.loot), Take All, ✕ Leave. Gear cards show an Equip button (wired in 2b; inert stub in 2a). - Looting pings makeNoise(c.x, c.y, CFG.loot.lootRootNoise, "shot") on a cadence.

2a-4. Damage interrupt (mobile fairness). - damage() player branch (~1100): if lootSheetOpen and CFG.loot.lootInterruptOnHit → flash + closeLootSheet() (un-root).

2a-5. Origin-aware banking (endRun, ~2670). - On win: bank all (as today) and write p.loadoutsave.loadout (equipped persists). - On death: bank Safe Pocket + every origin:"stash" bag item; write p.loadoutsave.loadout (equipped — including found-then-equipped — is never lost); forfeit origin:"found" bag items. - Result screen: tag kept-vs-lost by origin.

Config added (2a): lootRootNoise (px), lootInterruptOnHit (bool). Test: open chest → sheet over lower third, world still live; take items; get hit → sheet closes; die with a found item in bag → lost; die with a stash item displaced in bag → kept.

Step 2b — Channel field-equip

2b-1. Equip action. - Sheet/bag gear card Equipp.equipping = { item, slot: item.slot, t: 0, dur: CFG.loot.fieldEquipTime, from } (from = "sheet"/"bag"). Requires rooted/standing still.

2b-2. Channel in update(). - Advance p.equipping.t while rooted and not moving; interrupt on move or (per CFG.loot.fieldEquipInterruptOnHit) on hit. Draw a ring/bar (reuse ringMeter). - On complete: displaced = p.loadout[slot]; p.loadout[slot] = item; push displaced → bag (origin preserved); remove item from its source; recomputeLive(p); clear p.equipping.

2b-3. Kept-on-death. Equipped items live in p.loadout → §2a-5 already keeps them. A found item, once equipped, is safe; a displaced stash item sits safe in the bag.

Config added (2b): fieldEquipTime (~2), fieldEquipInterruptOnHit (bool). Test: equip a found weapon → power jumps, current HP scales (no refill), old piece → bag; die afterward → the equipped found weapon is kept, persists into the lobby loadout.

Risk register

  • Recompute parity (step 1c) — must equal newRun; verify before building UI.
  • HP/shield scaling — guard divide-by-zero on the first recomputeLive; never refill.
  • Non-pausing input — ensure rooted blocks movement but the sheet's buttons still receive taps (pointer-events scoping) and the canvas tap handler ignores taps while a sheet is open.
  • Loadout persistence — writing p.loadoutsave.loadout on death is the mechanism for "equipped never lost"; double-check it can't ever drop a save.stash/equipped item.

14. Phase 2.5 — corpse containers + readable ground loot (decided)

Decisions: A1 tiered corpse-containers + B1+B2 labeled, deliberate-grab ground loot. Problem solved: a downed rival dumps ~6–7 scattered items (unmanageable), and loose loot is opaque + force-grabbed on walk-over. Reuses the Phase-2a loot sheet.

14.1 Tiered corpse-containers (A1)

  • On kill, if the victim is a rival, boss, or tier ≥ CFG.loot.corpseTier (elites), spawn a lootable corpse holding its drops as corpse.loot (built from the existing onKill roll — the bot's carried+haul, the boss's 4, an elite's roll). Tapping the corpse opens the same bottom sheet (openLootSheet) — rooted + vulnerable; the death noise already draws enemies, so looting a body in the open is the risk.
  • Tier-1 mobs drop a single labeled ground item (no sheet for one trash drop).
  • Corpses fold into G.items as { kind:"corpse", loot, name, icon, tier, x, y, r }: they render via a drawCorpse branch, are tappable (onTap), open on arrival (updateInteractions), are skipped by the pickup loop (no .item), and are removed once emptied (picked = true).
  • Gold/blueprints stay instant on kill (as today); the key stays a separate ground pickup.
  • Persist until looted; cleared at round end (G reset). Future: rivals loot bodies too.

14.2 Readable, deliberate ground loot (B1 + B2)

  • B1 labels: drawLootLabels() (overlay pass) draws a compact label — icon · name · rarity · value, plus a ▲/▼ powerDelta for gear — above each ground item (and a "Loot ×N" tag on corpses) within CFG.loot.labelRadius of the player and in vision. A1 keeps piles rare, so labels don't stack.
  • B2 deliberate grab: the pickup loop auto-grabs only instant types (coin/consumable — always wanted, no slot). Gear/valuable/material are grabbed only when the item is the player's interactTarget (you tapped it) and you're adjacent — fits tap-to-interact and stops junk auto-filling the bag. (Dropped items keep the armed step-off guard.)

14.3 Config (Loot tab, auto-surfaced)

corpseTier (min mob tier that leaves a body; rivals/boss always), labelRadius (px to show ground-loot labels).

14.4 Test criteria

  • Kill a rival → a corpse appears; tap → sheet with its full haul; emptied → corpse gone.
  • Kill a tier-1 mob → one labeled ground item; walk over it → NOT auto-taken; tap → grabbed.
  • A coin/consumable on the ground → auto-grabbed on contact.
  • Labels show name/value/▲pw near items and hide at range.

15. Field-equip exploit fix (V2 — shipped)

Exploit: the old rule persisted the run loadout on death, so field-equipping a found item made it permanent even if you died — optimal play was "grab the best per slot, equip, die, keep it free." Plus displaced originals went to the at-risk bag and were droppable ("I don't care, they're safe anyway") to free space for more loot.

Fix (V2): found-equipped gear is at-risk; the displaced original stays in the bag, taking a slot (so found gear has a real carry cost), locked (non-droppable) but re-equippable so a mistaken equip can be swapped back. - finishFieldEquip: the displaced piece → the bag (origin preserved). A origin:"stash" bag item is locked (shown with 🔒, no Drop/Protect) and can be Re-equipped to swap back; a displaced found piece is normal at-risk loot. - dropFromBag / protectItem reject origin:"stash" items. - endRun death: don't persist the loadout (so save.loadout keeps your originals — restored intact); the bag is discarded (the locked original is redundant with save.loadout); found loot + found-equipped gear forfeit; only Safe Pocket survives. - endRun extract: persist the run loadout (found-equipped → permanent, origin normalized to stash); the bag banks normally so the locked original → save.stash as a spare.

Result: equipping is "power now, yours only if you survive"; your gear is never lost or droppable and is always swap-back-able mid-run; both leaks closed. Verified headless: equip→displaced-locked, drop-rejected, re-equip swaps back, death restores the original, extract makes the found item permanent and stashes the original.