From 4112a536835c934b76de0424cc2b77f219bed258 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 21 Jun 2026 20:33:03 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20D.2b=20inventory=20finish=20?= =?UTF-8?q?=E2=80=94=20side-bag=20column=20slots=20(36px=20pitch=20+=20emp?= =?UTF-8?q?ties)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../UI/Layout/InventoryController.cs | 27 +++++++++++++++---- .../UI/Layout/InventoryControllerTests.cs | 22 +++++++++++++-- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/AcDream.App/UI/Layout/InventoryController.cs b/src/AcDream.App/UI/Layout/InventoryController.cs index 9aa6ac45..01de0aea 100644 --- a/src/AcDream.App/UI/Layout/InventoryController.cs +++ b/src/AcDream.App/UI/Layout/InventoryController.cs @@ -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 _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. diff --git a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs index 53844cd5..0fa5d270 100644 --- a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs @@ -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) {