docs(D.2b-B): B-Grid implementation plan
4-task TDD plan: UiItemList grid mode (CellOffset/Columns/cell pitch), the LayoutImporter.Resolve sub-window mount (ShouldMountBaseChildren + attach base subtree), GameWindow swap of the placeholder for the real Import(0x21000023), then full-suite regression guard + visual. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aa60615912
commit
132bf36daa
1 changed files with 390 additions and 0 deletions
390
docs/superpowers/plans/2026-06-20-d2b-inventory-grid-mount.md
Normal file
390
docs/superpowers/plans/2026-06-20-d2b-inventory-grid-mount.md
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
# D.2b B-Grid Implementation Plan (inventory sub-window mount + UiItemList grid)
|
||||
|
||||
> **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 `LayoutImporter.Import(0x21000023)` produce the full nested inventory frame and give `UiItemList` an N-cell grid, so F12 shows the real inventory window.
|
||||
|
||||
**Architecture:** A ~4-line mount in `LayoutImporter.Resolve` attaches a base element's resolved children to a childless, media-less inheritor (the three gmInventoryUI panels nest via the existing `BaseElement`+`BaseLayoutId` path). `UiItemList` gains a column-count + cell-pitch layout, with single-cell (toolbar) behavior preserved by defaults. `GameWindow` swaps the Sub-phase A placeholder for the real import.
|
||||
|
||||
**Tech Stack:** C# .NET 10, xUnit, the `AcDream.App.UI` toolkit + `LayoutImporter`, `DatCollection`.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-20-d2b-inventory-grid-mount-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **Modify** `src/AcDream.App/UI/UiItemList.cs` — add `Columns`/`CellWidth`/`CellHeight` + grid layout (`CellOffset` helper, `LayoutCells`).
|
||||
- **Modify** `src/AcDream.App/UI/Layout/LayoutImporter.cs` — `ShouldMountBaseChildren` predicate + the `Resolve` mount.
|
||||
- **Modify** `src/AcDream.App/Rendering/GameWindow.cs` — swap the placeholder for `Import(0x21000023)`.
|
||||
- **Create** `tests/AcDream.App.Tests/UI/UiItemListGridTests.cs` — grid layout tests.
|
||||
- **Create** `tests/AcDream.App.Tests/UI/LayoutImporterMountTests.cs` — mount predicate tests.
|
||||
|
||||
(`InternalsVisibleTo` from `AcDream.App` → `AcDream.App.Tests` is already configured, so `internal static` helpers are testable — see `UiItemSlot.DragAcceptVisual`.)
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `UiItemList` grid mode
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/UI/UiItemList.cs`
|
||||
- Test: `tests/AcDream.App.Tests/UI/UiItemListGridTests.cs` (create)
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Create `tests/AcDream.App.Tests/UI/UiItemListGridTests.cs`:
|
||||
|
||||
```csharp
|
||||
using AcDream.App.UI;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
public class UiItemListGridTests
|
||||
{
|
||||
[Fact]
|
||||
public void CellOffset_RowMajor()
|
||||
{
|
||||
Assert.Equal((0f, 0f), UiItemList.CellOffset(0, 3, 36, 36));
|
||||
Assert.Equal((72f, 0f), UiItemList.CellOffset(2, 3, 36, 36));
|
||||
Assert.Equal((36f, 36f), UiItemList.CellOffset(4, 3, 36, 36)); // col 1, row 1
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GridMode_PositionsCellsInColumns()
|
||||
{
|
||||
var list = new UiItemList { Columns = 3, CellWidth = 36, CellHeight = 36 };
|
||||
list.Flush(); // drop the ctor's default cell
|
||||
for (int i = 0; i < 7; i++) list.AddItem(new UiItemSlot());
|
||||
|
||||
var c4 = list.GetItem(4)!;
|
||||
Assert.Equal(36f, c4.Left);
|
||||
Assert.Equal(36f, c4.Top);
|
||||
Assert.Equal(36f, c4.Width);
|
||||
Assert.Equal(36f, c4.Height);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillMode_SizesSingleCellToList()
|
||||
{
|
||||
// CellWidth defaults to 0 = "fill the list" (single-cell toolbar legacy).
|
||||
var list = new UiItemList { Width = 36, Height = 36 };
|
||||
list.Flush();
|
||||
list.AddItem(new UiItemSlot());
|
||||
|
||||
var c = list.Cell;
|
||||
Assert.Equal(0f, c.Left);
|
||||
Assert.Equal(0f, c.Top);
|
||||
Assert.Equal(36f, c.Width);
|
||||
Assert.Equal(36f, c.Height);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~UiItemListGridTests"`
|
||||
Expected: FAIL — compile error, `'UiItemList' does not contain a definition for 'Columns'` / `'CellOffset'`.
|
||||
|
||||
- [ ] **Step 3: Implement grid mode**
|
||||
|
||||
In `src/AcDream.App/UI/UiItemList.cs`, replace the `AddItem` method and the `OnDraw` method:
|
||||
|
||||
```csharp
|
||||
public void AddItem(UiItemSlot cell)
|
||||
{
|
||||
cell.SpriteResolve ??= SpriteResolve;
|
||||
_cells.Add(cell);
|
||||
AddChild(cell);
|
||||
LayoutCells();
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
// The factory sets Width/Height AFTER construction, so re-layout each frame:
|
||||
// fill mode keeps the single toolbar cell sized to the list; grid mode tiles cells.
|
||||
LayoutCells();
|
||||
}
|
||||
```
|
||||
|
||||
Then add these members to the class (e.g. after the `Cell` property):
|
||||
|
||||
```csharp
|
||||
/// <summary>Grid columns (row-major). 1 = single column. Ignored in fill mode.</summary>
|
||||
public int Columns { get; set; } = 1;
|
||||
|
||||
/// <summary>Fixed cell width in grid mode. 0 = "fill the list" — the single cell sizes
|
||||
/// to the whole list (the toolbar single-slot legacy). Set >0 (with CellHeight) for a grid.</summary>
|
||||
public float CellWidth { get; set; }
|
||||
/// <summary>Fixed cell height in grid mode (pairs with CellWidth).</summary>
|
||||
public float CellHeight { get; set; }
|
||||
|
||||
/// <summary>Row-major pixel offset of cell <paramref name="index"/> in a grid of
|
||||
/// <paramref name="columns"/> columns at the given cell pitch.</summary>
|
||||
internal static (float x, float y) CellOffset(int index, int columns, float cellW, float cellH)
|
||||
{
|
||||
int col = index % columns, row = index / columns;
|
||||
return (col * cellW, row * cellH);
|
||||
}
|
||||
|
||||
/// <summary>Position every cell per the current mode: fill (CellWidth<=0) sizes the single
|
||||
/// cell to the list; grid (CellWidth>0) tiles cells row-major at the cell pitch.</summary>
|
||||
private void LayoutCells()
|
||||
{
|
||||
if (CellWidth <= 0f)
|
||||
{
|
||||
if (_cells.Count > 0)
|
||||
{
|
||||
var c = _cells[0];
|
||||
c.Left = 0; c.Top = 0; c.Width = Width; c.Height = Height;
|
||||
}
|
||||
return;
|
||||
}
|
||||
int cols = Columns < 1 ? 1 : Columns;
|
||||
for (int i = 0; i < _cells.Count; i++)
|
||||
{
|
||||
var (x, y) = CellOffset(i, cols, CellWidth, CellHeight);
|
||||
var cell = _cells[i];
|
||||
cell.Left = x; cell.Top = y; cell.Width = CellWidth; cell.Height = CellHeight;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~UiItemListGridTests"`
|
||||
Expected: PASS (3 tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.App/UI/UiItemList.cs tests/AcDream.App.Tests/UI/UiItemListGridTests.cs
|
||||
git commit -m "feat(ui): D.2b-B — UiItemList N-cell grid mode
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Sub-window mount in `LayoutImporter.Resolve`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/UI/Layout/LayoutImporter.cs` (add `ShouldMountBaseChildren`; restructure `Resolve`)
|
||||
- Test: `tests/AcDream.App.Tests/UI/LayoutImporterMountTests.cs` (create)
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Create `tests/AcDream.App.Tests/UI/LayoutImporterMountTests.cs`:
|
||||
|
||||
```csharp
|
||||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
public class LayoutImporterMountTests
|
||||
{
|
||||
[Fact]
|
||||
public void Mounts_ChildlessMediaLessInheritor_WithContentfulBase()
|
||||
=> Assert.True(LayoutImporter.ShouldMountBaseChildren(derivedChildCount: 0, derivedMediaCount: 0, baseChildCount: 5));
|
||||
|
||||
[Fact]
|
||||
public void DoesNotMount_WhenDerivedHasOwnMedia() // close button / title
|
||||
=> Assert.False(LayoutImporter.ShouldMountBaseChildren(0, 1, 5));
|
||||
|
||||
[Fact]
|
||||
public void DoesNotMount_WhenDerivedHasOwnChildren()
|
||||
=> Assert.False(LayoutImporter.ShouldMountBaseChildren(2, 0, 5));
|
||||
|
||||
[Fact]
|
||||
public void DoesNotMount_WhenBaseIsChildless() // vitals/chat/toolbar style prototypes
|
||||
=> Assert.False(LayoutImporter.ShouldMountBaseChildren(0, 0, 0));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~LayoutImporterMountTests"`
|
||||
Expected: FAIL — compile error, `'LayoutImporter' does not contain a definition for 'ShouldMountBaseChildren'`.
|
||||
|
||||
- [ ] **Step 3: Add the predicate**
|
||||
|
||||
In `src/AcDream.App/UI/Layout/LayoutImporter.cs`, add this method just above the private `Resolve` method (after the `Import` method, in the "Inheritance resolution" region):
|
||||
|
||||
```csharp
|
||||
/// <summary>True when a pure-container leaf should inherit its base's subtree (the
|
||||
/// gmInventoryUI sub-window mount): the derived element has no own children, no own
|
||||
/// state media, and the base resolved to ≥1 child. Inert for media-bearing inheritors
|
||||
/// (close button / title), elements with their own children, and childless style
|
||||
/// prototypes (vitals/chat/toolbar text).</summary>
|
||||
internal static bool ShouldMountBaseChildren(int derivedChildCount, int derivedMediaCount, int baseChildCount)
|
||||
=> derivedChildCount == 0 && derivedMediaCount == 0 && baseChildCount > 0;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Wire the mount into `Resolve`**
|
||||
|
||||
In `src/AcDream.App/UI/Layout/LayoutImporter.cs`, replace the entire body of the private `Resolve` method:
|
||||
|
||||
```csharp
|
||||
private static ElementInfo Resolve(
|
||||
DatCollection dats,
|
||||
ElementDesc d,
|
||||
HashSet<(uint layoutId, uint elementId)> baseChain)
|
||||
{
|
||||
// Read this element's own fields + media (no inheritance, no children yet).
|
||||
var self = ToInfo(d);
|
||||
var result = self;
|
||||
List<ElementInfo>? baseChildren = null;
|
||||
|
||||
// Apply BaseElement / BaseLayoutId inheritance if present.
|
||||
if (d.BaseElement != 0 && d.BaseLayoutId != 0
|
||||
&& baseChain.Add((d.BaseLayoutId, d.BaseElement)))
|
||||
{
|
||||
var baseLd = dats.Get<LayoutDesc>(d.BaseLayoutId);
|
||||
var baseDesc = baseLd is null ? null : FindDesc(baseLd, d.BaseElement);
|
||||
if (baseDesc is not null)
|
||||
{
|
||||
// Recurse the base chain (already guarded by the HashSet add above).
|
||||
var baseInfo = Resolve(dats, baseDesc, baseChain);
|
||||
// Derived fields override the base; children are attached below.
|
||||
result = ElementReader.Merge(baseInfo, self);
|
||||
baseChildren = baseInfo.Children; // capture for the sub-window mount
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve + attach children. Each child gets a FRESH base-chain set:
|
||||
// the cycle guard is per-element, not shared across siblings.
|
||||
foreach (var kv in d.Children)
|
||||
result.Children.Add(Resolve(dats, kv.Value, new HashSet<(uint, uint)>()));
|
||||
|
||||
// Sub-window mount: a pure-container leaf (no own children, no own media) that inherits
|
||||
// from a base WITH content attaches the base's subtree. Targets the gmInventoryUI panels
|
||||
// (0x100001CD/CE/CF — Type-0, media-less, childless, BaseLayoutId → a gm*UI window);
|
||||
// inert for media-bearing inheritors (close button/title) and childless style prototypes
|
||||
// (vitals/chat/toolbar). See ShouldMountBaseChildren + the B-Grid spec.
|
||||
if (baseChildren is not null
|
||||
&& ShouldMountBaseChildren(d.Children.Count, self.StateMedia.Count, baseChildren.Count))
|
||||
result.Children.AddRange(baseChildren);
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~LayoutImporterMountTests"`
|
||||
Expected: PASS (4 tests).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.App/UI/Layout/LayoutImporter.cs tests/AcDream.App.Tests/UI/LayoutImporterMountTests.cs
|
||||
git commit -m "feat(ui): D.2b-B — sub-window mount (inheritor attaches base subtree)
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Swap the placeholder for the real import (`GameWindow`)
|
||||
|
||||
No unit tests — GL-coupled, runs only with `ACDREAM_RETAIL_UI=1`; verified by build + the visual check in Task 4.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (the Sub-phase A placeholder block in `_options.RetailUi`)
|
||||
|
||||
- [ ] **Step 1: Replace the placeholder mount**
|
||||
|
||||
In `src/AcDream.App/Rendering/GameWindow.cs`, find the Sub-phase A placeholder block:
|
||||
|
||||
```csharp
|
||||
// Phase D.2b-A — placeholder inventory window. Starts HIDDEN; F12
|
||||
// (InputAction.ToggleInventoryPanel) reveals it via the UiRoot window
|
||||
// manager. Throwaway scaffolding: Sub-phase B replaces the body with the
|
||||
// real gmInventoryUI (0x21000023) nested layout, keeping the same name.
|
||||
var inventoryWindow = new AcDream.App.UI.UiNineSlicePanel(ResolveChrome)
|
||||
{
|
||||
Left = 220, Top = 120, Width = 320, Height = 400,
|
||||
Visible = false,
|
||||
};
|
||||
_uiHost.Root.AddChild(inventoryWindow);
|
||||
_uiHost.RegisterWindow(AcDream.App.UI.WindowNames.Inventory, inventoryWindow);
|
||||
Console.WriteLine("[D.2b-A] placeholder inventory window registered (F12 toggles).");
|
||||
```
|
||||
|
||||
Replace it with the real import:
|
||||
|
||||
```csharp
|
||||
// Phase D.2b-B — the real inventory window from LayoutDesc 0x21000023 (gmInventoryUI),
|
||||
// via the LayoutImporter. The sub-window mount pulls in the nested paperdoll/backpack/
|
||||
// 3D-items panels (Type-0 leaves inheriting BaseLayoutId 0x21000024/22/21). Starts
|
||||
// HIDDEN; F12 toggles it via the window manager. Position is the dat's own (X=500,Y=138).
|
||||
AcDream.App.UI.Layout.ImportedLayout? invLayout;
|
||||
lock (_datLock)
|
||||
invLayout = AcDream.App.UI.Layout.LayoutImporter.Import(
|
||||
_dats!, 0x21000023u, ResolveChrome, vitalsDatFont);
|
||||
if (invLayout is not null)
|
||||
{
|
||||
var inventoryWindow = invLayout.Root;
|
||||
inventoryWindow.Visible = false;
|
||||
inventoryWindow.Anchors = AcDream.App.UI.AnchorEdges.None; // user-positioned
|
||||
inventoryWindow.Draggable = true;
|
||||
_uiHost.Root.AddChild(inventoryWindow);
|
||||
_uiHost.RegisterWindow(AcDream.App.UI.WindowNames.Inventory, inventoryWindow);
|
||||
Console.WriteLine("[D.2b-B] retail inventory window from LayoutDesc importer (0x21000023).");
|
||||
}
|
||||
else Console.WriteLine("[D.2b-B] inventory: LayoutDesc 0x21000023 not found.");
|
||||
```
|
||||
|
||||
(`_dats`, `_datLock`, `ResolveChrome`, and `vitalsDatFont` are all in scope in this block — the vitals/chat imports above use the same.)
|
||||
|
||||
- [ ] **Step 2: Build to verify it compiles**
|
||||
|
||||
Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug`
|
||||
Expected: Build succeeded, 0 errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.App/Rendering/GameWindow.cs
|
||||
git commit -m "feat(ui): D.2b-B — F12 shows the real gmInventoryUI (0x21000023)
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Regression guard + full verification
|
||||
|
||||
- [ ] **Step 1: Run the full test suite (regression guard)**
|
||||
|
||||
Run: `dotnet test`
|
||||
Expected: PASS — full suite green (2757 baseline + 3 grid + 4 mount-predicate = ~2764). The existing `LayoutImporter` / vitals / chat / toolbar tests passing confirms the mount is inert for those windows (their bases are childless prototypes).
|
||||
|
||||
- [ ] **Step 2: Hand off for visual verification**
|
||||
|
||||
Launch with `ACDREAM_RETAIL_UI=1` (standard launch in CLAUDE.md) and confirm with the user:
|
||||
- F12 now shows the **real nested inventory frame** (outer chrome + backpack strip on the right + a 3D-items area + an empty paperdoll panel), not the blank placeholder.
|
||||
- F12 again hides it; clicking it raises it; it doesn't toggle while chat is focused (Sub-phase A behavior intact).
|
||||
- **Vitals, chat, and toolbar look unchanged** (the mount regression guard, visually).
|
||||
|
||||
Watch the launch log for `[D.2b-B] retail inventory window from LayoutDesc importer (0x21000023).` and the absence of importer errors.
|
||||
|
||||
Do NOT mark B-Grid done until the user confirms the visual.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**Spec coverage:**
|
||||
- Sub-window mount (spec §4) → Task 2 (`ShouldMountBaseChildren` + `Resolve`). ✓
|
||||
- `UiItemList` grid mode (spec §5) → Task 1. ✓
|
||||
- Placeholder → real import swap (spec §9) → Task 3. ✓
|
||||
- Grid + mount-predicate tests (spec §6) → Tasks 1–2. ✓
|
||||
- Regression guard, vitals/chat/toolbar unchanged (spec §6/§8) → Task 4 Step 1 (suite) + Step 2 (visual). ✓
|
||||
- No divergence row (spec §7) → nothing to add. ✓
|
||||
- Visual acceptance (spec §8) → Task 4 Step 2. ✓
|
||||
|
||||
**Placeholder scan:** No TBD/TODO/"handle edge cases"; every code step shows full code. ✓
|
||||
|
||||
**Type consistency:** `Columns`/`CellWidth`/`CellHeight`/`CellOffset`/`LayoutCells` (Task 1), `ShouldMountBaseChildren(int,int,int)` (Task 2 — same signature in predicate, test, and `Resolve` call site), `LayoutImporter.Import(_dats, 0x21000023u, ResolveChrome, vitalsDatFont)` (Task 3, matches the vitals/chat call shape). ✓
|
||||
Loading…
Add table
Add a link
Reference in a new issue