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>
172 lines
9.6 KiB
Markdown
172 lines
9.6 KiB
Markdown
# 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.
|