docs(D.2b): inventory window finish (Stage 1) implementation plan
6 tasks: (1) UiItemList clip+scroll via the shared UiScrollable + whole-row clip; (2) mouse-wheel; (3) InventoryController binds the gutter scrollbar 0x100001C7 like ChatWindowController; (4) side-bag column 36px pitch + empty- slot padding to capacity; (5) backdrop coverage — screenshot-gated, primary fix is the clip; (6) verify + bookkeeping. TDD on the pure/controller logic (internals test-visible); backdrop is the visual gate. Grounded in the dat dumps + the existing scroll/scrollbar machinery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0fbb76b2e7
commit
7aba6d235c
1 changed files with 535 additions and 0 deletions
535
docs/superpowers/plans/2026-06-21-d2b-inventory-window-finish.md
Normal file
535
docs/superpowers/plans/2026-06-21-d2b-inventory-window-finish.md
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
# D.2b inventory window finish (Stage 1) Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make the inventory window match retail's 2D presentation — the contents grid clips + scrolls, the backdrop covers the whole window, and the side-bag column shows proper slots.
|
||||
|
||||
**Architecture:** Reuse the existing `UiScrollable` scroll model (the chat transcript + `UiScrollbar` already use it) by giving `UiItemList` a clip+scroll capability (cells positioned offset by `-ScrollY`, `cell.Visible` toggled by whole-row clip — the same whole-row approach `UiText` uses, since there is no GL scissor). `InventoryController` binds the dat gutter scrollbar `0x100001C7` to the grid's model exactly like `ChatWindowController` does, splits the cell pitch (contents 32px / backpack 36px), and pads the side-bag column with empty slots. The backdrop is mostly fixed for free by clipping the grid; any residual is a screenshot-gated render fix.
|
||||
|
||||
**Tech Stack:** C# / .NET 10, xUnit. `src/AcDream.App/UI/`. Spec: `docs/superpowers/specs/2026-06-21-d2b-inventory-window-finish-design.md`. Grounding (dat geometry): contents grid `0x100001C6` = 192×96 (6×3, 32px cells); gutter scrollbar `0x100001C7` = 16×96; side-bag column `0x100001CA` = 36×252 (7 slots of 36px); backdrop `0x100001D0` = 300×362 full-window.
|
||||
|
||||
**Reuse references (read before coding):**
|
||||
- `src/AcDream.App/UI/UiScrollable.cs` — the scroll model (ContentHeight/ViewHeight/ScrollY/ScrollByLines/etc.). Already unit-tested; do NOT re-test it.
|
||||
- `src/AcDream.App/UI/UiText.cs:117-247` — the whole-row clip (`y < top || y+lh > bottom → skip`, line 198) + the wheel handler (`OnEvent` Scroll case, line 239) to mirror.
|
||||
- `src/AcDream.App/UI/Layout/ChatWindowController.cs:44-49` (scrollbar sprite-id constants) + `:237-255` (the `FindElement → bar.Model = …Scroll + sprite ids` binding pattern).
|
||||
- `src/AcDream.App/UI/UiItemList.cs` + `src/AcDream.App/UI/Layout/InventoryController.cs` — the files being changed.
|
||||
- `tests/AcDream.App.Tests/UI/UiItemListTests.cs` + `.../UI/Layout/InventoryControllerTests.cs` — test style + the `BuildLayout`/`Bind`/`SeedContained` harness.
|
||||
|
||||
Every commit message MUST end with:
|
||||
`Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `UiItemList` — scroll model + scroll-aware clip layout
|
||||
|
||||
Give the grid a `UiScrollable` and make `LayoutCells` clip whole rows + offset by the scroll position. Fill mode (the toolbar single-cell, `CellWidth <= 0`) is unchanged.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/UI/UiItemList.cs`
|
||||
- Test: `tests/AcDream.App.Tests/UI/UiItemListScrollTests.cs` (create)
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Create `tests/AcDream.App.Tests/UI/UiItemListScrollTests.cs`:
|
||||
|
||||
```csharp
|
||||
using AcDream.App.UI;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
public sealed class UiItemListScrollTests
|
||||
{
|
||||
private static UiItemList Grid(int items)
|
||||
{
|
||||
var list = new UiItemList { Columns = 6, CellWidth = 32, CellHeight = 32, Width = 192, Height = 96 };
|
||||
list.Flush(); // drop the default single cell
|
||||
for (int i = 0; i < items; i++) list.AddItem(new UiItemSlot());
|
||||
list.LayoutCells(); // drive the scroll model + position cells
|
||||
return list;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RowCount_ceils_to_whole_rows()
|
||||
{
|
||||
Assert.Equal(0, UiItemList.RowCount(0, 6));
|
||||
Assert.Equal(1, UiItemList.RowCount(1, 6));
|
||||
Assert.Equal(7, UiItemList.RowCount(41, 6));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Grid_drives_scroll_model_from_content()
|
||||
{
|
||||
var list = Grid(30); // 5 rows × 32 = 160 content, 96 view
|
||||
Assert.Equal(160, list.Scroll.ContentHeight);
|
||||
Assert.Equal(96, list.Scroll.ViewHeight);
|
||||
Assert.Equal(64, list.Scroll.MaxScroll);
|
||||
Assert.True(list.Scroll.HasOverflow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void At_top_first_three_rows_visible_rest_clipped()
|
||||
{
|
||||
var list = Grid(30);
|
||||
Assert.True(list.GetItem(0)!.Visible); // row 0, top 0
|
||||
Assert.True(list.GetItem(12)!.Visible); // row 2, top 64 (+32 == 96)
|
||||
Assert.False(list.GetItem(18)!.Visible); // row 3, top 96 (off the bottom)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scrolled_one_row_shifts_window_and_clips_top_row()
|
||||
{
|
||||
var list = Grid(30);
|
||||
list.Scroll.SetScrollY(32);
|
||||
list.LayoutCells();
|
||||
Assert.False(list.GetItem(0)!.Visible); // row 0 scrolled above
|
||||
Assert.Equal(0f, list.GetItem(6)!.Top); // row 1 now at the view top
|
||||
Assert.True(list.GetItem(18)!.Visible); // row 3 now visible
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~UiItemListScrollTests"`
|
||||
Expected: FAIL — `UiItemList` has no `Scroll`, no `RowCount`, `LayoutCells` is private.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
In `src/AcDream.App/UI/UiItemList.cs`:
|
||||
|
||||
(a) add `using System;` if missing, and the scroll model field after `_cells`:
|
||||
|
||||
```csharp
|
||||
/// <summary>Vertical scroll model for grid mode (clip+scroll). Bound to the gutter
|
||||
/// UiScrollbar by the panel controller. Inert in fill mode (single toolbar cell).</summary>
|
||||
public UiScrollable Scroll { get; } = new();
|
||||
```
|
||||
|
||||
(b) add the row-count helper (next to `CellOffset`):
|
||||
|
||||
```csharp
|
||||
/// <summary>Whole rows needed for <paramref name="cellCount"/> cells in a grid of
|
||||
/// <paramref name="columns"/> columns.</summary>
|
||||
public static int RowCount(int cellCount, int columns)
|
||||
{
|
||||
int cols = columns < 1 ? 1 : columns;
|
||||
return (cellCount + cols - 1) / cols;
|
||||
}
|
||||
```
|
||||
|
||||
(c) replace the whole `private void LayoutCells()` body with this (and change `private` → `internal` so tests can drive it):
|
||||
|
||||
```csharp
|
||||
internal void LayoutCells()
|
||||
{
|
||||
if (CellWidth <= 0f)
|
||||
{
|
||||
// Fill mode (the toolbar single cell): size the one cell to the list.
|
||||
if (_cells.Count > 0)
|
||||
{
|
||||
var c = _cells[0];
|
||||
c.Left = 0; c.Top = 0; c.Width = Width; c.Height = Height; c.Visible = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int cols = Columns < 1 ? 1 : Columns;
|
||||
int cellH = (int)MathF.Round(CellHeight);
|
||||
|
||||
// Drive the shared scroll model from the current geometry, then re-clamp the
|
||||
// offset to the (possibly changed) max. ContentHeight/ViewHeight must be set
|
||||
// BEFORE reading ScrollY so the clamp uses the right max.
|
||||
Scroll.LineHeight = cellH > 0 ? cellH : 1;
|
||||
Scroll.ContentHeight = RowCount(_cells.Count, cols) * cellH;
|
||||
Scroll.ViewHeight = (int)MathF.Floor(Height);
|
||||
Scroll.SetScrollY(Scroll.ScrollY); // re-clamp to new max
|
||||
float scrollY = Scroll.ScrollY;
|
||||
|
||||
for (int i = 0; i < _cells.Count; i++)
|
||||
{
|
||||
int col = i % cols, row = i / cols;
|
||||
float top = row * CellHeight - scrollY;
|
||||
var cell = _cells[i];
|
||||
cell.Left = col * CellWidth;
|
||||
cell.Top = top;
|
||||
cell.Width = CellWidth;
|
||||
cell.Height = CellHeight;
|
||||
// Whole-row vertical clip (no scissor — mirrors UiText.cs:198). A row fully
|
||||
// inside [0, Height] draws; a partially-scrolled row is hidden.
|
||||
cell.Visible = top >= -0.5f && top + CellHeight <= Height + 0.5f;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~UiItemListScrollTests"`
|
||||
Expected: PASS (4 tests).
|
||||
|
||||
- [ ] **Step 5: Run the existing UiItemList tests (no regression)**
|
||||
|
||||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~UiItemList"`
|
||||
Expected: PASS (the scroll tests + the pre-existing `UiItemListTests`/`UiItemListGridTests`; fill mode + grid offsets unchanged for non-scrolled grids).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.App/UI/UiItemList.cs tests/AcDream.App.Tests/UI/UiItemListScrollTests.cs
|
||||
git commit -F - <<'EOF'
|
||||
feat(ui): D.2b inventory finish — UiItemList clip+scroll via UiScrollable
|
||||
|
||||
Grid mode now drives a shared UiScrollable from its content + clips whole rows
|
||||
to the panel (cells offset by -ScrollY, Visible toggled by whole-row clip,
|
||||
mirroring UiText). Fill mode (toolbar single cell) unchanged.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: `UiItemList` — mouse-wheel scroll
|
||||
|
||||
Wheel over the grid scrolls it, mirroring `UiText`'s Scroll handler.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/UI/UiItemList.cs`
|
||||
- Test: `tests/AcDream.App.Tests/UI/UiItemListScrollTests.cs` (append)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
First open `src/AcDream.App/UI/UiText.cs` around line 239 to confirm the `UiEvent` Scroll shape (`e.Data0` = wheel delta). Then append to `UiItemListScrollTests.cs`:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void Wheel_down_scrolls_toward_the_bottom()
|
||||
{
|
||||
var list = Grid(30); // max scroll 64, LineHeight 32
|
||||
// Silk wheel +Y = up/older; UiText negates Data0. Wheel DOWN (Data0 < 0) → +ScrollY.
|
||||
var e = new UiEvent { Type = UiEventType.Scroll, Data0 = -1f };
|
||||
bool handled = list.OnEvent(e);
|
||||
Assert.True(handled);
|
||||
Assert.Equal(32, list.Scroll.ScrollY); // one line (32px) down
|
||||
}
|
||||
```
|
||||
|
||||
(If `UiEvent`'s field types differ — e.g. `Data0` is `int` — match them; the construction style is the only thing to align, the assertion stands.)
|
||||
|
||||
- [ ] **Step 2: Run to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~UiItemListScrollTests.Wheel"`
|
||||
Expected: FAIL — `UiItemList` doesn't override `OnEvent` (wheel ignored, ScrollY stays 0).
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
In `src/AcDream.App/UI/UiItemList.cs` add an `OnEvent` override (mirror `UiText.cs:239`):
|
||||
|
||||
```csharp
|
||||
public override bool OnEvent(in UiEvent e)
|
||||
{
|
||||
if (e.Type == UiEventType.Scroll && CellWidth > 0f)
|
||||
{
|
||||
// Mirror UiText: Silk +Y wheel = up/older = decrease ScrollY; negate Data0.
|
||||
Scroll.ScrollByLines(-(int)e.Data0);
|
||||
return true;
|
||||
}
|
||||
return base.OnEvent(e);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~UiItemListScrollTests"`
|
||||
Expected: PASS (5 tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.App/UI/UiItemList.cs tests/AcDream.App.Tests/UI/UiItemListScrollTests.cs
|
||||
git commit -F - <<'EOF'
|
||||
feat(ui): D.2b inventory finish — UiItemList mouse-wheel scroll
|
||||
|
||||
OnEvent handles the Scroll wheel in grid mode (mirrors UiText's sign), driving
|
||||
the shared UiScrollable. The bound gutter scrollbar (Task 3) is the primary
|
||||
scroll affordance; the wheel is the convenience path.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
```
|
||||
|
||||
> NOTE for the implementer: the wheel only fires if a Scroll event reaches the list (directly hovered, or bubbled from a cell that doesn't consume Scroll). `UiItemSlot` does not handle Scroll, so it should bubble — but confirm at the visual gate (Task 5). If the wheel doesn't reach the list, the scrollbar (Task 3) still works; file a follow-up rather than reworking event dispatch here.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: `InventoryController` — bind the contents-grid gutter scrollbar
|
||||
|
||||
Bind `0x100001C7` (a factory-built Type-11 `UiScrollbar`) to the contents grid's scroll model, with the same sprite ids the chat scrollbar uses (both inherit base layout `0x2100003E`).
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/UI/Layout/InventoryController.cs`
|
||||
- Test: `tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs` (extend `BuildLayout` + add a test)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
In `InventoryControllerTests.cs`: (a) add the scrollbar id constant near the others (`private const uint ContentsScrollbar = 0x100001C7u;`); (b) extend `BuildLayout` to create + register + return a `UiScrollbar`. Change its signature/return tuple to append `UiScrollbar scrollbar`:
|
||||
|
||||
```csharp
|
||||
var scrollbar = new UiScrollbar { Width = 16, Height = 96 };
|
||||
// ... after the existing root.AddChild(...) calls:
|
||||
root.AddChild(scrollbar);
|
||||
// ... in the byId dictionary initializer add:
|
||||
// [ContentsScrollbar] = scrollbar,
|
||||
// ... and append scrollbar to the returned tuple.
|
||||
```
|
||||
|
||||
Then add the test:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void Contents_grid_scrollbar_binds_to_the_grid_scroll_model()
|
||||
{
|
||||
var (layout, grid, _, _, _, _, _, _, scrollbar) = BuildLayout();
|
||||
Bind(layout, new ClientObjectTable());
|
||||
Assert.Same(grid.Scroll, scrollbar.Model); // the bar drives the grid's scroll
|
||||
}
|
||||
```
|
||||
|
||||
(Update the other `BuildLayout()` call sites' tuple deconstruction to add a trailing `_` for the new element.)
|
||||
|
||||
- [ ] **Step 2: Run to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~InventoryControllerTests.Contents_grid_scrollbar"`
|
||||
Expected: FAIL — `scrollbar.Model` is null (controller doesn't bind it).
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
In `src/AcDream.App/UI/Layout/InventoryController.cs`:
|
||||
|
||||
(a) add the element-id + sprite-id constants (sprite ids copied from `ChatWindowController.cs:44-49` — both scrollbars inherit `0x2100003E`):
|
||||
|
||||
```csharp
|
||||
public const uint ContentsScrollbarId = 0x100001C7u; // gm3DItemsUI gutter scrollbar
|
||||
// Scrollbar chrome from base layout 0x2100003E (shared with the chat scrollbar).
|
||||
private const uint ScrollTrackSprite = 0x06004C5Fu;
|
||||
private const uint ScrollThumbSprite = 0x06004C63u;
|
||||
private const uint ScrollThumbTop = 0x06004C60u;
|
||||
private const uint ScrollThumbBot = 0x06004C66u;
|
||||
private const uint ScrollUpSprite = 0x06004C6Cu;
|
||||
private const uint ScrollDownSprite = 0x06004C69u;
|
||||
```
|
||||
|
||||
(b) in the constructor, after the `_contentsGrid` setup block (the `if (_contentsGrid is not null) { … }` around line 63-68), bind the scrollbar:
|
||||
|
||||
```csharp
|
||||
// Bind the gutter scrollbar to the contents grid's scroll model (the factory built
|
||||
// 0x100001C7 as a bare Type-11 UiScrollbar; wire it like ChatWindowController does).
|
||||
if (_contentsGrid is not null
|
||||
&& layout.FindElement(ContentsScrollbarId) is UiScrollbar bar)
|
||||
{
|
||||
bar.Model = _contentsGrid.Scroll;
|
||||
bar.SpriteResolve = _contentsGrid.SpriteResolve; // chrome resolve from the factory ctor
|
||||
bar.TrackSprite = ScrollTrackSprite;
|
||||
bar.ThumbSprite = ScrollThumbSprite;
|
||||
bar.ThumbTopSprite = ScrollThumbTop;
|
||||
bar.ThumbBotSprite = ScrollThumbBot;
|
||||
bar.UpSprite = ScrollUpSprite;
|
||||
bar.DownSprite = ScrollDownSprite;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~InventoryControllerTests"`
|
||||
Expected: PASS (the new test + all pre-existing InventoryController tests, incl. the B-Wire burden tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.App/UI/Layout/InventoryController.cs tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs
|
||||
git commit -F - <<'EOF'
|
||||
feat(ui): D.2b inventory finish — bind contents-grid gutter scrollbar
|
||||
|
||||
InventoryController binds 0x100001C7 (factory Type-11 UiScrollbar) to the
|
||||
contents grid's UiScrollable + the shared 0x2100003E scrollbar sprites,
|
||||
mirroring ChatWindowController. The grid now scrolls instead of overflowing.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: `InventoryController` — side-bag column (36px pitch + empty-slot padding)
|
||||
|
||||
The side-bag column should show the player's bags + empty slot frames up to capacity, at the correct 36px pitch.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/UI/Layout/InventoryController.cs`
|
||||
- Test: `tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs` (append)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Append to `InventoryControllerTests.cs`:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void Side_bag_column_pads_empty_slots_up_to_capacity()
|
||||
{
|
||||
var (layout, _, containers, _, _, _, _, _, _) = BuildLayout();
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = Player, ContainersCapacity = 3 });
|
||||
SeedContained(objects, 0xC, Player, slot: 0, type: ItemType.Container); // one side bag
|
||||
|
||||
Bind(layout, objects);
|
||||
|
||||
Assert.Equal(3, containers.GetNumUIItems()); // 1 bag + 2 empty = capacity 3
|
||||
Assert.Equal(0xCu, containers.GetItem(0)!.ItemId); // the bag
|
||||
Assert.Equal(0u, containers.GetItem(1)!.ItemId); // empty frame
|
||||
Assert.Equal(0u, containers.GetItem(2)!.ItemId); // empty frame
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~InventoryControllerTests.Side_bag_column"`
|
||||
Expected: FAIL — only 1 cell (the bag); no empty padding.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
In `src/AcDream.App/UI/Layout/InventoryController.cs`:
|
||||
|
||||
(a) split the cell pitch — replace the single `private const float CellPx = 32f;` with:
|
||||
|
||||
```csharp
|
||||
private const float ContentsCellPx = 32f; // gm3DItemsUI grid (192x96 = 6x3 of 32px)
|
||||
private const float BackpackCellPx = 36f; // gmBackpackUI column cells (0x100001C9/CA = 36px)
|
||||
private const int SideBagSlots = 7; // 0x100001CA is 36x252 = 7 slots
|
||||
```
|
||||
|
||||
Update the constructor's three cell-pitch assignments: `_contentsGrid` uses `ContentsCellPx` (was `CellPx`); `_containerList` and `_topContainer` use `BackpackCellPx`.
|
||||
|
||||
(b) in `Populate()`, after the `foreach (var guid in contents)` loop that adds side bags to `_containerList`, pad it with empty slots. Find where the loop ends (before the `_topContainer` block) and add:
|
||||
|
||||
```csharp
|
||||
// Side-bag column: pad with empty slot frames up to the player's container
|
||||
// capacity (clamped to the 7-slot column), so the column reads like retail
|
||||
// (bags on top, empty frames below) rather than one lone cell. Divergence AP-XX
|
||||
// if capacity is absent → default to the full 7-slot column.
|
||||
if (_containerList is not null)
|
||||
{
|
||||
int capacity = _objects.Get(p)?.ContainersCapacity ?? 0;
|
||||
int slots = capacity > 0 ? capacity : SideBagSlots;
|
||||
slots = Math.Clamp(slots, _containerList.GetNumUIItems(), SideBagSlots);
|
||||
while (_containerList.GetNumUIItems() < slots)
|
||||
_containerList.AddItem(new UiItemSlot { SpriteResolve = _containerList.SpriteResolve });
|
||||
}
|
||||
```
|
||||
|
||||
(Place this AFTER the `foreach` that adds bags and BEFORE the `if (_topContainer is not null)` block. The `_contentsGrid`/`_containerList` were already `Flush()`ed at the top of `Populate`, so the count here is exactly the bags added this pass.)
|
||||
|
||||
- [ ] **Step 4: Run to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~InventoryControllerTests"`
|
||||
Expected: PASS (all InventoryController tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.App/UI/Layout/InventoryController.cs tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs
|
||||
git commit -F - <<'EOF'
|
||||
feat(ui): D.2b inventory finish — side-bag column slots (36px pitch + empties)
|
||||
|
||||
The side-bag column (0x100001CA, 36x252 = 7 slots) pads empty slot frames up to
|
||||
the player's ContainersCapacity (clamped to 7), at the correct 36px pitch
|
||||
(split from the contents grid's 32px). Reads like retail's bag column.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Backdrop coverage — visual gate + conditional residual fix
|
||||
|
||||
The backdrop `0x100001D0` is already full-window (300×362); the tear is the unclipped grid overflowing below it, which Tasks 1+3 fix. This task verifies that at the running client and fixes any residual.
|
||||
|
||||
**Files:**
|
||||
- Modify (only if residual gap found): `src/AcDream.App/UI/UiDatElement.cs` (Alphablend draw) — exact change determined by the root cause.
|
||||
|
||||
- [ ] **Step 1: Build + launch**
|
||||
|
||||
Run: `dotnet build`
|
||||
Expected: Build succeeded.
|
||||
Then launch the client against ACE with `ACDREAM_RETAIL_UI=1` (per CLAUDE.md "Running the client"), get in-world with a full pack, press F12.
|
||||
|
||||
- [ ] **Step 2: Observe the backdrop**
|
||||
|
||||
With the grid now clipped to 3 rows + scrollable, check the dark backdrop covers the whole inventory window (no grass/world showing through anywhere) — compare to retail screenshot 2.
|
||||
|
||||
- [ ] **Step 3: If fully covered — done.** Record the observation; no code change. Skip to Task 6.
|
||||
|
||||
- [ ] **Step 4: If a residual gap remains — root-cause then fix.** Most likely candidate: `UiDatElement` draws the Alphablend backdrop sprite (`0x06004D0A`) at native size instead of stretched/tiled to the element's 300×362 bounds. Open `src/AcDream.App/UI/UiDatElement.cs`, find the Alphablend `DrawMode` draw path, and confirm whether it fills the element rect. If it draws at native sprite size, change it to fill the element bounds (stretch or tile, matching how the vitals chrome center fill tiles — see `UiNineSlicePanel`). Add a divergence row if the fill differs from retail's exact draw. Re-launch + re-verify. (Do not change the draw speculatively — only if Step 2 shows a gap.)
|
||||
|
||||
- [ ] **Step 5: Commit (only if a fix was made)**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.App/UI/UiDatElement.cs
|
||||
git commit -F - <<'EOF'
|
||||
fix(ui): D.2b inventory finish — backdrop fills the full window
|
||||
|
||||
<describe the root cause found at the visual gate>
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Full verification + bookkeeping
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/ISSUES.md` (close the contents-grid-overflow issue), `docs/architecture/retail-divergence-register.md` (add the side-bag-capacity-fallback row + any backdrop row), `claude-memory/project_d2b_retail_ui.md` + `MEMORY.md` (Stage 1 shipped entry).
|
||||
|
||||
- [ ] **Step 1: Full build + test**
|
||||
|
||||
Run: `dotnet build` then `dotnet test`
|
||||
Expected: all green (App count = the B-Wire baseline 534 + the new Stage-1 tests; Core/Core.Net/UI unchanged).
|
||||
|
||||
- [ ] **Step 2: Close the overflow issue + add divergence rows**
|
||||
|
||||
In `docs/ISSUES.md`: move the "Inventory 'Contents of Backpack' grid overflows (no scroll)" issue to DONE with the commit SHAs (Tasks 1-3 fixed it).
|
||||
In `docs/architecture/retail-divergence-register.md`: add a row for the side-bag slot count using the 7-slot fallback when `ContainersCapacity` is absent (replace the `AP-XX` placeholder in the Task-4 comment with the real id). Add a backdrop row only if Task 5 made a fill change that differs from retail.
|
||||
|
||||
- [ ] **Step 3: Update memory**
|
||||
|
||||
In `claude-memory/project_d2b_retail_ui.md`: add a Stage-1 SHIPPED entry (UiItemList clip+scroll via UiScrollable; gutter scrollbar 0x100001C7 bound like chat; side-bag column 36px + empty padding; backdrop coverage outcome). Update the `MEMORY.md` index line. Note Stage 2 (paperdoll) is next.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/ISSUES.md docs/architecture/retail-divergence-register.md
|
||||
git commit -F - <<'EOF'
|
||||
docs(D.2b): inventory window finish (Stage 1) shipped — close overflow issue
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Post-implementation: visual gate
|
||||
|
||||
After Task 6, Stage 1 is code-complete. The acceptance is visual (project model): open F12 with a full pack → scrollable 3-row grid with a working gutter scrollbar, solid backdrop edge-to-edge, side-bag column with bag(s) + empty frames — matching retail screenshot 2 minus the doll. Report the observation. Then Stage 2 (paperdoll) gets its own brainstorm → spec → plan.
|
||||
|
||||
---
|
||||
|
||||
## Self-review notes (coverage vs. spec)
|
||||
|
||||
- **A (contents-grid clip+scroll)** → Tasks 1 (clip layout) + 2 (wheel) + 3 (scrollbar binding). Reuses `UiScrollable` per decision §4.
|
||||
- **B (backdrop coverage)** → Task 5 (screenshot-gated; primary fix is A's clip per spec §B / decision §4).
|
||||
- **C (side-bag column)** → Task 4 (36px pitch + empty padding up to capacity per decision §4).
|
||||
- **Testing** → per-task unit tests (UiItemList scroll/wheel pure-ish via internal `LayoutCells`; controller scrollbar-bound + side-bag count via the `BuildLayout` harness); backdrop is visual-gate only per spec §5.
|
||||
- **Divergence** → side-bag capacity fallback row in Task 6; backdrop row only if Task 5 changes the draw.
|
||||
- **Out of scope** (paperdoll, B-Drag, side-bag scrollbar `0x100001CB`, stack overlays) — untouched.
|
||||
- Type consistency: `UiItemList.Scroll` (UiScrollable) / `RowCount` / internal `LayoutCells` defined in Task 1 and consumed in Tasks 2-3; `ContentsCellPx`/`BackpackCellPx`/`SideBagSlots` defined + used in Task 4; scrollbar `Model`/sprite setters match `UiScrollbar`'s public API.
|
||||
Loading…
Add table
Add a link
Reference in a new issue