acdream/docs/superpowers/specs/2026-06-21-d2b-inventory-window-finish-design.md
Erik 0fbb76b2e7 docs(D.2b): inventory window finish (Stage 1) spec
Stage 1 of the "full retail inventory" arc (Stage 2 = paperdoll, separate
spec). Grounded in the dat dumps: contents grid 0x100001C6 is 192x96 (6x3) +
scrollbar 0x100001C7 (16x96); backdrop 0x100001D0 is full-window 300x362
(so the "torn" look is the unclipped grid overflowing below the frame —
fixed by clipping); side-bag column 0x100001CA is 36x252 (7 slots).
Components: (A) UiItemList clip+scroll reusing UiScrollable + bind the gutter
scrollbar like ChatWindowController; (B) backdrop coverage — primary fix is A,
residual gated on the post-A screenshot; (C) side-bag column pads empty slots
up to capacity at 36px pitch. Paperdoll + B-Drag out of scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:07:10 +02:00

172 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# D.2b inventory window finish (Stage 1) — design / spec
**Date:** 2026-06-21
**Phase:** D.2b inventory, **Stage 1 of the "full retail inventory" arc** (Stage 2 = paperdoll, separate spec).
**Branch:** `claude/hopeful-maxwell-214a12` (follows B-Wire, tip `7c006d1`).
**Predecessor:** B-Wire (inventory wire layer) — shipped + burden gate passed.
**Status:** approved design → write plan next.
This was triggered by a side-by-side: acdream's inventory vs. a retail client (`+Je`). The gaps the
user flagged — "scroll bar, background all the way, slots for backpacks" — are the 2D window finish.
The 3D paperdoll doll is the heavier, separable Stage 2.
---
## 1. Goal / non-goals
**Goal.** Make the inventory window match retail's 2D presentation:
1. The "Contents of Backpack" grid **clips to its panel and scrolls** (today it renders every row
unclipped, so a full pack overflows off-window — which is *also* why picked-up items "don't appear"
and why the backdrop looks torn at the bottom).
2. The dark **backdrop covers the whole window** (no grass showing through).
3. The **side-bag selector** renders as a proper slot column (bags + empty frames), like retail.
**Non-goals (deferred):**
- **Paperdoll** (3D character doll + per-slot equip silhouettes) → **Stage 2** (needs a Core→App
`UiViewport` render seam + `0x10000032` `UiItemSlot` registration). Research:
`docs/research/2026-06-16-equipment-paperdoll-deep-dive.md`.
- **Drag-to-drop / drag-from-inventory** → B-Drag.
- The **side-bag column's own scrollbar** (`0x100001CB`): 7 slots fit the 252px column exactly, so
no scroll is needed there; leave it inert.
---
## 2. Grounding (from the dat layout dumps — authoritative geometry)
Read from `.layout-dumps/inventory-0x21000023.txt`, `items3d-0x21000021.txt`, `backpack-0x21000022.txt`:
- **Outer frame `0x21000023`** (300×362): the **backdrop `0x100001D0`** is **X=0,Y=0, 300×362**
(full window), Type 3, Alphablend sprite `0x06004D0A`, ZLevel 100. Three sub-windows mount inside:
paperdoll `0x100001CD` (0,23 224×214), backpack `0x100001CE` (239,23 61×339), 3D-items
`0x100001CF` (0,237 234×120).
- **3D-items `0x21000021`** (234×120): **contents grid `0x100001C6` = X=15,Y=20, 192×96** (exactly
6 cols × 3 rows of 32px); **scrollbar `0x100001C7` = X=207,Y=20, 16×96**, Type 0 inheriting base
layout `0x2100003E` (the scrollbar base → resolves to a Type-11 `UiScrollbar` via the inheritance
chain), ZLevel 9 (just behind the grid's 10).
- **Backpack `0x21000022`** (61×339): main-pack cell `0x100001C9` (6,32 36×36); **side-bag column
`0x100001CA` = X=6,Y=73, 36×252** (= 7 slots of 36px), inherits the ItemList base `0x2100003D`;
side-bag scrollbar gutter `0x100001CB` (41,73 16×252) — unused (7 slots fit). Burden meter
`0x100001D9` + captions `0x100001D7/D8` (already wired by B-Controller).
**Key deduction:** the backdrop is geometrically full-window, so the "torn" look is the **unclipped
grid overflowing below Y=362** (41 items / 6 cols = 7 rows × 32 = 224px, drawn from abs Y≈257 down to
≈481, far past the frame's 362 bottom). Clipping the grid (component A) removes that overflow and the
torn-bottom backdrop with it.
---
## 3. Components
### A — Contents-grid clip + scroll (the core)
**A1. `UiItemList` gains a clip+scroll capability**, backed by the existing **`UiScrollable`** model
(the same one `UiText`/the chat transcript + `UiScrollbar` use — do not invent a second scroll model).
- The list clips cell drawing to its own rect (`0x100001C6` = 192×96 = 3 visible rows). Content
height = `ceil(count / Columns) × CellHeight`; view height = the list's `Height`.
- A scroll offset (whole rows, or pixels) shifts the cells up; cells fully outside the view are not
drawn (scissor-clip via `UiRenderContext`, mirroring the transcript's clip).
- Mouse-wheel over the list scrolls it (reuse the wheel→`UiScrollable` path the transcript uses).
- `UiScrollable` exposes the thumb-size + position ratios the bound `UiScrollbar` reads (already its
contract).
**A2. `InventoryController` binds the gutter scrollbar.** Resolve `0x100001C7` from the layout; it
is built by `DatWidgetFactory` as a Type-11 `UiScrollbar` (via its `0x2100003E` base). Bind it to
`_contentsGrid`'s `UiScrollable` — exactly as `ChatWindowController` binds its transcript scrollbar
(`ChatWindowController.cs:237-254`). If `0x100001C7` does not resolve to a `UiScrollbar` (inheritance
surprise), the plan's first step pins why before wiring.
**Acceptance:** with > 18 loose items the grid shows 3 rows + a working scrollbar; picked-up items are
reachable by scrolling; the grid no longer draws past the frame; the bottom backdrop is intact.
### B — Backdrop full coverage
The backdrop `0x100001D0` is already full-window (300×362). **Primary fix is A** (clipping the grid
removes the overflow that tears the bottom). After A lands, the implementer verifies at the visual
gate that the dark backing covers the whole window. **If a residual gap remains** (e.g. the Alphablend
backdrop sprite drawn at native size instead of filled to the element's 300×362 bounds, or a sub-window
region uncovered), root-cause it then — the candidate is `UiDatElement`'s Alphablend draw not
stretching/tiling to bounds. This is a **root-cause-then-fix** task gated on the post-A screenshot, not
a pre-committed code change (a render-coverage bug must be confirmed visually before fixing).
**Acceptance:** no world/grass shows through the inventory window anywhere (matches retail's solid
backing).
### C — Side-bag slot column
The side-bag column `0x100001CA` (36×252 = 7 slots) currently renders **only the actual side bags**
(via `InventoryController.Populate`'s `isBag → _containerList` branch), so a character with one bag
shows one lone cell. Retail shows the bag(s) **plus empty slot frames** filling the column.
- `InventoryController.Populate`: after adding the player's side bags to `_containerList`, **pad with
empty `UiItemSlot`s** up to the slot count. Slot count = the column capacity (252 / cellPitch = 7),
optionally clamped to the player's `ContainersCapacity` when known (> 0); default to the 7-slot
column height otherwise. Empty cells draw the empty-slot art already (the toolbar empty-slot path).
- **Cell pitch = 36** for `_containerList` + `_topContainer` (match `0x100001C9`/`0x100001CA`'s 36px
geometry), not the 32px used for the contents grid. (Today the controller sets `CellPx = 32` for all
three — split the contents-grid pitch (32) from the backpack-column pitch (36).)
**Acceptance:** the side-bag column shows the character's bag(s) in the top slot(s) + empty frames
below, a clean column like retail's.
---
## 4. Design decisions
- **Reuse `UiScrollable`, don't fork it.** It already models content/view/offset + thumb ratios and is
battle-tested by the chat transcript. `UiItemList` becomes a second consumer. (DRY; matches the
widget-generalization ethos.)
- **Scroll by whole rows vs. pixels:** scroll by **pixels** (smooth), clamped so the last row aligns to
the view bottom — matches the transcript's pixel scroll and avoids a janky row-snap. (Plan may choose
row-snap if the dat/retail behavior says so; default pixels.)
- **Backdrop fix is gated on the post-A screenshot** (decision §B) — don't pre-change the Alphablend
draw before confirming A didn't already fix it; avoids touching shared `UiDatElement` render for
every window without cause.
- **Side-bag slot count from `ContainersCapacity` when known, else 7** — the column is physically 7
slots; clamping to capacity matches retail (you see only as many slots as you can use) without a wire
dependency (capacity ships in the weenie header, already parsed).
---
## 5. Testing
- **`UiItemList` clip/scroll (unit, App.Tests):** content-height for N cells; the visible-cell index
range at scroll offset 0 and at max offset; wheel delta updates the offset (clamped); `UiScrollable`
thumb/position ratios for a known content/view. (No GL — assert the model + which cells are in-view.)
- **`InventoryController` (unit, App.Tests, `BuildLayout` harness):** the side-bag column renders
`bags + empties = slotCount` cells (filled first, then empty); the contents grid is bound to the
scrollbar (the `0x100001C7` widget's scroll model is the grid's). A >18-item grid reports content
height > view height (scrollable).
- **Backdrop:** visual gate only (no unit test).
- Full `dotnet build` + `dotnet test` green; existing inventory tests (incl. the B-Wire burden tests)
unbroken.
---
## 6. Divergence register
- If the side-bag slot count uses a fallback (7) when `ContainersCapacity` is absent, add a row noting
the approximation. If B's residual fix stretches the Alphablend backdrop in a way that differs from
retail's exact draw, add a row. Otherwise no new deviations (faithful 2D layout). Any row lands in the
same commit as its deviation (register rule).
---
## 7. Out of scope (explicit)
Paperdoll doll + equip-slot silhouettes (Stage 2); drag-to-drop / drag-from-inventory (B-Drag); the
side-bag column scrollbar `0x100001CB` (inert — 7 slots fit); contents-grid drag-reorder; stack-quantity
overlays.
---
## 8. Acceptance + visual gate
- [ ] Contents grid clips to 3 rows + scrolls (wheel + scrollbar); picked-up items reachable; no
overflow past the frame.
- [ ] Backdrop covers the whole window (no grass through), confirmed at the gate.
- [ ] Side-bag column shows bag(s) + empty slot frames (36px pitch).
- [ ] `dotnet build` + `dotnet test` green.
- [ ] **Visual gate:** open F12 with a full pack → matches retail screenshot 2 (minus the doll):
scrollable grid, solid backdrop, bag-slot column.
Then Stage 2 (paperdoll) gets its own brainstorm → spec → plan.