DRIFTLANDS co-op survival on drifting tides — studio progress

UX Spec: Inventory — Bag & Shared Storage

Status: Reviewed — APPROVED (/ux-review re-pass 2026-08-31: 0 blocking, 4 advisory — applied) · v0.3
Author: Jeroen + ux-designer
Last Updated: 2026-08-31
Journey Phase(s): unknown — no player journey map yet (see Open Questions)
Template: UX Spec
Variants: Bag (opened with Inventory anywhere) · Bag + Storage (opened with Interact at a storage chest)
Requirements input: inventory quick-spec Rules 1–7 + Tuning Knobs; harvesting UI Requirements (full-bag cue, no-op cue family OQ6); crafting Rule 12 / AC-13 / AC-33 (equip swap-in-place, maker's mark in tooltip + rack, aggregation ban); base-building Rule 12 (storage instantiated per chest); art bible §7.1–7.5; ui-hud quick-spec Rule 4 (Inventory row) + queue item ①
Patterns used: #3 Panel/Modal · #4 Focus & Nav · #5 Tooltip/Inspect · #6 Reason String · #7 Honest No-Op Cue · #12 Maker's-Mark Inspect · #13 Storage Who-Took-What · #17 Carry Meter · #19 Feedback Split · #21 Stack Grid (new — defined here)


Purpose & Player Need

The bag is where the loop's tension becomes a decision. Every harvest swing, every demolish refund, every craft lands here, and the tide makes the bag's fill level the one question that matters on a docked land: bank or push? The player arrives at this screen wanting to see what I'm carrying and decide what to do with it — check the fill, equip the thing Mara just made, empty the haul into the chest so the next trip starts clean. If this screen is slow, fiddly, or lies about what's in the chest, the co-op loop breaks in the most annoying way possible: two friends hauling the same timber, or a full bag discovered by a failed swing instead of a glance. The quick-spec names this the "chronically underestimated" risk; the ui-hud queue puts it first, before the first friend playtest.

What it is not: a management game. No weight, no sorting, no filtering, no favourites, no "made by" grouping (aggregation ban). Ten slots, two equipment slots, one chest at a time.


Player Context on Arrival


Navigation Position

This screen lives at: Gameplay → Bag (top-level, always reachable while alive) and Gameplay → Chest → Bag + Storage (context-dependent: within interaction range of a storage structure at home). Never reached from pause or the main menu; no inventory tab in Settings. Only one panel is open at a time (pattern #3): opening the bag while another panel is open replaces it via the panel stack.


Entry & Exit Points

Entry Source Trigger Player carries this context
Gameplay (anywhere, alive)Inventory press (Tab / North)Own CARRY (owner-predicted), equipped weapon/armor, current carry-meter fill
Gameplay at home, in range of a storage chestInteract press (E / West) with the chest as the nearest interactableOwn CARRY + that chest's host-confirmed contents + its live who-took-what feed (#13)
Gameplay (Downed)Inventory pressAn ordinary bag — Downed does not affect the bag (quick-spec Rule 8) and ADR-0015 suppresses only Strike/Dash/Demolish, so every verb here stays legal (ruled 2026-08-31; the earlier read-only variant invented a mechanic no GDD owns)
Exit Destination Trigger Notes
GameplayInventory press — closes the whole panel in both variants (chest half included; ADR-0007 §3 grants the bag its opening-key close)No state change on close; transfers already committed or already failed
GameplayMenu (Esc / Start) — closes via the panel stack, does not open pauseSame
GameplayUI.Cancel (East)Same
Gameplay (Bag variant)Walking out of chest interaction range (Bag + Storage only)Degrade, not close: the storage half closes; the bag half stays open as the Bag variant
GameplayTide soft-close deposit / death respawn / host disconnectPanel force-closes; CARRY untouched (tide AC-23, combat AC-28)
—Gamepad disconnect while openPanel stays open; focus stays where it is; prompt glyphs switch to KB/M (ADR-0007 §3)

No exit is one-way; no exit changes persistent state by itself (state changes are per-transfer host commits that already resolved).


Layout Specification

Information Hierarchy

Deliberately absent: sort buttons, filters, search, weight, categories, favourites, any per-crafter view — the ban list (quick-spec Rule 3/Pillar 4; aggregation ban).

Layout Zones

Arrangement: fixed-bag (ruled 2026-08-30) — the bag never moves; spatial memory holds across variants.

All px values at 100 % UI scale; multiply with the accessibility scale option (pattern-library scale convention).

Component Inventory

Zone Component Type Content Interactive Pattern
Z1Bag gridStack grid 5×2Own CARRY stacks: icon + count (count hidden when 1)Yes#21 Stack Grid (new — defined by this spec's Interaction Map)
Z1Equipment strip2 special slotsEquipped weapon / armor icons; wear read: the glanceable indicator is the HUD hotbar's frayed-corner accent (hud.md E3, 2026-08-31); the equipment strip here mirrors that same accent on its two slots, and the tooltip (#12) carries the worded stateYes#21 variant; swap per crafting Rule 12 / AC-33
Z1Capacity readText7/10 used slotsNomirrors #17
Z2Chest gridStack grid 8×5Chest stacks — host-confirmed only, never predictedYes#21 + #13 tags
Z2Chest headerText"Chest"No—
Z3TooltipTooltipname · tier · maker's mark · wear · count/maxStackNo#5 + #12
Z4Reason stringText"chest is full" / "bag is full" / "bag is full — deposit something first" (Small 13–14 px). Lifetime: persists until the next focus move or action, minimum 250 ms (#6) — a discrete-event string, not auto-dismissingNo#6

New pattern flagged: #21 Stack Grid — interaction rules defined here, to be lifted into interaction-patterns.md after this spec is approved (its reserved number).

ASCII Wireframe

Bag + Storage variant (Bag variant = Z1 alone, same position):

 gameplay visible & live ······································ HUD stays visible
┌─ Chest ────────────────────────────────┐  ┌─ Bag ──────────────────┐
│ ┌────┬────┬────┬────┬────┬────┬────┬────┐ │  │ ┌────┬────┬────┬────┬────┐ │
│ │timb│timb│fibr│    │sinw│    │    │    │ │  │ │timb│fibr│fibr│ ⚔︎* │    │ │
│ │ 20 │ 12 │ 20 │    │  4 │    │    │    │ │  │ │  7 │ 20 │  3 │    │    │ │
│ ├────┼────┼────┼────┼────┼────┼────┼────┤ │  │ ├────┼────┼────┼────┼────┤ │
│ │ −12 · Alex        (2 s tag, #13)     │ │  │ │    │    │    │    │    │ │
│ ├────┼────┼────┼────┼────┼────┼────┼────┤ │  │ └────┴────┴────┴────┴────┘ │
│ │ …4 more rows (row 2 shown above;     │ │  │  ⚔ Weapon   🛡 Armor        │
│ │  8×5 = 40 slots total)               │ │  │  ┌──────┐  ┌──────┐        │
│ └────┴────┴────┴────┴────┴────┴────┴────┘ │  │  │ axe  │  │cloth │        │
│                                        │  │  └──────┘  └──────┘        │
│                                        │  │              Bag  7/10     │
└────────────────────────────────────────┘  └────────────────────────────┘
   [reason-string line appears under the refusing panel]        * = the ONE focused element
   focused slot: 3 px outline + inset tick (─▸); tooltip anchors to it

Z1 is pixel-identical between variants; Z2 slides in/out left of it. Focus starts on the bag's first occupied slot.


States & Variants

State / Variant Trigger What Changes
Bag (default)InventoryZ1 only; Submit on gear = equip / swap
Bag + StorageInteract at a chestZ2 slides in; Submit = transfer in both grids; who-took-what tags live (#13)
Empty bagNew character / fully banked10 empty slots; capacity 0/10; no placeholder text (an empty grid is self-explanatory; first-run tutorialisation is deliberately absent)
Empty chestFresh chest40 empty slots; header unchanged
Full targetTransfer refused by CanAddNothing moves (all-or-nothing, quick-spec Rule 6); reason string "bag is full" / "chest is full" in the refusing panel's footer (#6); no sound beyond the ordinary click
Withdraw pendingSubmit on a chest stack, host verdict not yet in (~1 RTT)The source cell keeps its stack; further input on that stack is ignored until the outcome lands (no cue — one press = one transaction, never a queued pair); focus stays on the cell; other cells remain live
Race lossStack vanished before your click resolvedStack disappears when the host update lands; who-took-what tag shows who; your bag unchanged (#7 world-family — withdraws are never predicted)
Unequip unavailableBag full and an equipment slot focusedThe equipment slot is not actionable (verb hint blank); footer shows "bag is full — deposit something first" (#6). Never a refused action — nothing is attempted
DownedOpened, or player becomes Downed while openNo change — an ordinary bag (quick-spec Rule 8); the panel does not force-close
DegradeWalked out of chest rangeZ2 slides out; Z1 stays; focus jumps to the bag grid if it was in the chest
Chest demolished while openAnother player demolishes it (empty-first rule — it was empty)Same as Degrade
Loading—None: CARRY is local; chest contents are already-replicated state — the panel never waits on the network to open

Interaction Map

Mapping for KB/M (primary) and gamepad (partial, twin-stick) per technical-preferences; UI capture semantics per ADR-0007 §3. One verb per container, context-labelled (provisional, playtest item): Submit on a bag stack = equip (gear) when no chest is open, deposit when one is; Submit on a chest stack = withdraw; the footer hint names the active verb for the focused cell. Escape hatch stated: to equip while banking, step out of chest range (Degrade) — no second verb exists at MVP (OQ 2).

Component Action KB/M Gamepad Feedback Outcome
Any stackFocushover / arrowsUI.Navigate3 px outline + inset tick (#4); tooltip after 150 ms (#5)—
Bag stack (material)Transfer → chestclick / EnterUI.Submit (South)Predicted removal from bag; appears in chest on host confirm; click soundInventoryTransfer(bag→chest, stackRef)
Chest stackTransfer → bagclick / EnterUI.SubmitNot predicted: stays until host confirms, then moves; bag flash (#17)InventoryTransfer(chest→bag, stackRef)
Bag gear (no chest open)Equip / swapclick / EnterUI.SubmitTwo cases (ruled 2026-08-31): target equipment slot empty → the bag cell simply empties; target occupied → swap: the outgoing item lands in the exact bag slot the incoming item vacated (crafting AC-33 — the one non-fungible placement in the grid)equip command
Bag gear (chest open)Depositclick / EnterUI.SubmitAs material transfer; footer hint reads "deposit"InventoryTransfer
Equipment slotUnequip → bagclick / EnterUI.SubmitItem moves to the first free bag slot (empty-handed removal has no vacated slot to reuse). With a full bag the verb is not offered (see States) — never a refused attempt, so "equipment never requires bag slots" (quick-spec Rule 4) and crafting Rule 12 "free and placeless" hold: swap is always available; only empty-handed removal waits for roomunequip command
PanelCloseTab / EscNorth / East / Start100–150 ms fade (#3)—
Any cellSecondaryRMB (UI.RightClick)—No-op at MVP (reserved by OQ 2 for a possible equip verb); never a context menu—
Grid ↔ grid / stripFocus traversalarrowsUI.NavigateComplete adjacency set below—

Focus adjacency (complete — every edge defined; no wrap anywhere):

From Left Right Up Down
Bag cell (5×2)previous column; from column 1 with a chest open → chest column 8, same row; without a chest → stopnext column; from column 5 → stoprow 1 → stop; row 2 → row 1row 1 → row 2; row 2 → equipment strip (columns 1–2 → Weapon, 3–5 → Armor)
Weapon slotstop (with a chest open → chest column 8, row 5)Armorbag row 2, column 1stop
Armor slotWeaponstopbag row 2, column 5stop
Chest cell (8×5)previous column; column 1 → stopnext column; column 8 → bag column 1, same row (rows 1–2) / bag row 2 (rows 3–5)previous row; row 1 → stopnext row; row 5 → stop

Numbered order (the QA traversal sequence, not the entry point): Bag variant — bag 1…10 row-major, then Weapon (11), Armor (12). Bag + Storage — chest 1…40 row-major, then bag 1…10, then Weapon, Armor (matches #13). First focus is always the bag: first occupied bag slot (slot 1 if empty). Empty cells are focusable. Exactly one element is focused at all times.

Gutter asymmetry — intentional (ruled 2026-08-31): the bag has 2 rows, the chest 5, so crossing clamps: chest rows 3–5 → bag row 2, and Left from bag row 2 returns to chest row 2, not the row departed; Weapon.Left → chest row 5, whose Right lands on bag row 2. Nothing is trapped; it may read as odd in play — OQ 8 (a sticky last-chest-row is the fallback).

No drag, no split, no multi-select, no modifier-click at MVP (ruled 2026-08-30 — one press = one whole stack = one host transaction, transferBatchWindow 0). Focus starts on the bag's first occupied slot (slot 1 if empty).

Stack Grid (#21) rules distilled for the pattern library: fungible slots (a stack fills the first free slot) except the AC-33 swap, which reuses the vacated slot; the adjacency table above with no wrap; Submit = the container's single verb; icon + count (count hidden at 1); tagged stacks (gear) never merge; empty slot = outlined cell; no drag/split at MVP; input on a stack with a pending host verdict is ignored.


Events Fired

Player Action Event Payload Persistent?
Transfer either directionInventoryTransfer command on the host channel (ADR-0002 CommandKind.InventoryTransfer)direction, stackRef (itemId, count, instanceTag?), containerIdYes — CARRY / STORAGE tables dirty on commit (ADR-0004)
Equip / unequipequip command — owner-rail CARRY mutation + equipped-slot change (exact command shape → crafting runtime ADR / ADR-0010)instanceTag, slotYes — equipped instances persist in carries (ADR-0004 §1)
Open / close panel, focus moves, tooltipnone—Deliberate: pure presentation
Refused transfer / race lossnone from this client—The host's refusal is the event; silence is the honest no-op

No analytics events exist in this project (no telemetry at MVP).


Transitions & Animations


Data Requirements

Data Source System Read / Write Notes
Own CARRY stacks (ItemStack[], ≤ bagSlots)Inventory — ICarryInventory (owner rail)Read; write via commands onlyOwner-predicted for own mutations; the UI never edits stacks directly
Equipped weapon / armor (instanceTag)Inventory equipment slots (Combat owns hook signatures; Crafting owns definitions)Read; write via equip commandSwap-in-place is the command's rule, not the UI's
Chest contents (ItemStack[], ≤ storageSlots)Inventory — ISharedStorage (host rail), replicatedRead only, host-confirmedRepresentation → ADR-0010; the UI never predicts it
Who-took-what deltasInventory — confirmed InventoryTransfer outcomes broadcast from the drainRead, event-drivenTransient; never stored (no-contribution-comparison rule)
Item icon, name, tier, maxStackGameData IGameData.Items (ADR-0006)ReadStatic after boot
Maker's mark (crafter, forged cycle, tier), wear stateCrafting IInstanceMetadata via instanceTag; current cycleIndex from ITideClock for "N tides ago"ReadPer-item only (#12)
Interaction-range chest idBase Building / interact systemReadDrives the Bag ↔ Bag + Storage variant and the Degrade exit
bagSlots, storageSlots, transferBatchWindowGameData Configs.InventoryReadGrid dimensions derive from these — never a hardcoded 5×2 / 8×5

Architectural flag: the UI owns nothing. Every write is an ICommand on the host channel (ADR-0002) or an owner-rail equip; "CARRY predicted, STORAGE confirmed, withdraws not predicted" is the split ADR-0010 must implement.


Accessibility

Tier: Standard (design/accessibility-requirements.md).


Localization Considerations

Element English max Layout-critical Priority
Footer reason strings ("chest is full", "bag is full — deposit something first")≤ 32 chars at Small 13–14 px (≈ 7 px/char → 45 chars expanded ≈ 315 px < 380 px)Yes — one line under the 380 px panelHIGH
Footer verb hint ("Enter: deposit")≤ 20 charsYesHIGH
Chest header ("Chest")≤ 12 charsNoLow
Maker's mark line ("Mara — forged 4 tides ago — cloth")≤ 48 chars; pluralised templateExempt from the no-wrap rule — tooltip max width 320 px @100 %, ≤ 2 linesHIGH (plural rules)
Who-took-what tag ("− 12 · Alex")name ≤ 16 chars + deltaInline on a 64 px slot — truncate the name with an ellipsisMedium
Counts 7/10, 20numericLocale digits only; no separators needed (max 80)Low

Any element marked HIGH must survive 40 % text expansion without wrapping to a second line, except where a line cap is stated instead. Layout at 150 % UI scale: (380 + 24 + 560) × 1.5 = 1446 px ≤ 1920 — fits 1080p (AC 11).


Acceptance Criteria


Open Questions