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. Two
existing tests updated for the now-padded count (divergence AP-52: 7-slot
fallback when capacity is absent — register row added in the wrap-up commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-21 20:33:03 +02:00
parent fad807587d
commit 4112a53683
2 changed files with 42 additions and 7 deletions

View file

@ -33,7 +33,9 @@ public sealed class InventoryController
// 3D-items grid: 192x96 → 6 cols x 3 rows of the 32x32 UIItem cell (template 0x21000037).
private const int ContentsColumns = 6;
private const float CellPx = 32f;
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
private readonly ClientObjectTable _objects;
private readonly Func<uint> _playerGuid;
@ -73,8 +75,8 @@ public sealed class InventoryController
if (_contentsGrid is not null)
{
_contentsGrid.Columns = ContentsColumns;
_contentsGrid.CellWidth = CellPx;
_contentsGrid.CellHeight = CellPx;
_contentsGrid.CellWidth = ContentsCellPx;
_contentsGrid.CellHeight = ContentsCellPx;
}
// Bind the gutter scrollbar to the contents grid's scroll model (the factory built
@ -94,8 +96,8 @@ public sealed class InventoryController
if (_containerList is not null)
{
_containerList.Columns = 1;
_containerList.CellWidth = CellPx;
_containerList.CellHeight = CellPx;
_containerList.CellWidth = BackpackCellPx;
_containerList.CellHeight = BackpackCellPx;
}
// Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4).
@ -177,6 +179,21 @@ public sealed class InventoryController
list.AddItem(cell);
}
// Side-bag column: pad with empty slot frames up to the player's container capacity
// (clamped to the 7-slot column) so it reads like retail (bags on top, empty frames
// below) rather than one lone cell. Falls back to the full 7 slots when capacity is
// unknown (divergence AP-52).
if (_containerList is not null)
{
int capacity = _objects.Get(p)?.ContainersCapacity ?? 0;
int bags = _containerList.GetNumUIItems();
int slots = capacity > 0 ? capacity : SideBagSlots;
slots = Math.Max(slots, bags);
slots = Math.Min(slots, SideBagSlots);
while (_containerList.GetNumUIItems() < slots)
_containerList.AddItem(new UiItemSlot { SpriteResolve = _containerList.SpriteResolve });
}
// Main-pack cell: the player's own container. Icon = placeholder until a backpack
// RenderSurface DID is pinned (spec §3 / divergence row). Bind the guid so a future
// click can select it.

View file

@ -83,8 +83,9 @@ public class InventoryControllerTests
Assert.Equal(2, grid.GetNumUIItems()); // 2 loose
Assert.Equal(0xAu, grid.GetItem(0)!.ItemId);
Assert.Equal(0xBu, grid.GetItem(1)!.ItemId);
Assert.Equal(1, containers.GetNumUIItems()); // 1 side bag
Assert.Equal(7, containers.GetNumUIItems()); // 1 side bag + 6 empty (no capacity → 7-slot column)
Assert.Equal(0xCu, containers.GetItem(0)!.ItemId);
Assert.Equal(0u, containers.GetItem(1)!.ItemId); // padded empty frame
}
[Fact]
@ -102,7 +103,8 @@ public class InventoryControllerTests
Assert.Equal(1, grid.GetNumUIItems()); // only the loose item
Assert.Equal(0xAu, grid.GetItem(0)!.ItemId);
Assert.Equal(0, containers.GetNumUIItems()); // equipped item is not a side bag either
Assert.Equal(7, containers.GetNumUIItems()); // 7 empty slots (no bags; the equipped item isn't here)
Assert.Equal(0u, containers.GetItem(0)!.ItemId);
}
[Fact]
@ -203,6 +205,22 @@ public class InventoryControllerTests
Assert.Same(grid.Scroll, bar.Model); // the bar drives the grid's scroll
}
[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
}
// Reads the text of the UiText caption child attached by the controller.
private static string CaptionText(UiElement host)
{