feat(D.2b): per-container capacity bar on inventory cells
Faithful port of retail UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): each container cell (side bags + main pack) shows a vertical UIElement_Meter (element 0x10000347, back 0x06004D22 / fill 0x06004D23) filled to GetNumContainedItems / ItemsCapacity, clamped [0,1]; hidden for non-containers (CapacityFill=-1). Drawn procedurally on UiItemSlot like the triangle/square overlays (back full + front clipped bottom-up). Right-anchored flush to the cell edge (visual gate: the dat X=26 sat ~5px off the right edge). Visually confirmed 2026-06-22. Divergence AP-59; polish deferred to ISSUES #146 (exact rect/anchor, fill direction vs m_eDirection 0x6f, closed-bag lazy-load). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
077586a0f0
commit
a45c421bd1
6 changed files with 112 additions and 1 deletions
|
|
@ -46,6 +46,32 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #146 — D.2b inventory capacity-bar visual polish
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** LOW
|
||||
**Filed:** 2026-06-22
|
||||
|
||||
The per-side-bag / main-pack container **capacity fill bar** (faithful port of retail
|
||||
`UIElement_UIItem::UpdateCapacityDisplay 0x004e16e0`, element `0x10000347`) shipped + is
|
||||
visually confirmed (`src/AcDream.App/UI/UiItemSlot.cs` `CapacityFill`;
|
||||
`InventoryController.SetCapacityBar`). User: "looks good, we need to polish it a bit, but lets do
|
||||
that later." Get the specific polish list from the user when picked up. Candidate points (confirm
|
||||
against retail):
|
||||
|
||||
- **Exact bar rect / anchor** — currently right-anchored flush (`X = Width − barW`); the dat element
|
||||
rect is `X=26 Y=1 W=5 H=30` (a 5px right margin). Flush was the visual-gate call; reconcile with
|
||||
the dat once the desired look is pinned.
|
||||
- **Fill direction** — currently bottom-up (assumed); the dat `m_eDirection` (property `0x6f`) isn't
|
||||
read (cf. AP-50). Confirm bottom-up vs retail.
|
||||
- **Closed-bag fill** — a closed side bag reads empty until opened (contents aren't indexed until
|
||||
`ViewContents`). If retail shows real fill for closed bags, the per-container item counts must be
|
||||
pre-loaded at login (the lazy-load question — also the container-switching "open verification").
|
||||
|
||||
See divergence register **AP-59**.
|
||||
|
||||
---
|
||||
|
||||
## #145 — Inventory window panels occluded by the full-window backdrop (importer ignores ZLevel)
|
||||
|
||||
**Status:** DONE (2026-06-21 · `45a5cc5` + continuation `417b137`) — VISUALLY CONFIRMED. The first fix (`45a5cc5`) folded `ZLevel` into `ZOrder` and put the backdrop behind the **ZLevel-0 top-level** panels, which made the **paperdoll** (its base root is ZLevel 0) render — so it *looked* done. But the **mounted backpack/3D-items panels** inherited their sub-window root's **ZLevel 1000** (via `ElementReader.Merge`'s zero-wins-base rule), so the `ReadOrder − ZLevel·10000` fold sank them to ZOrder ≈ −10,000,000 — *behind* the backdrop (ZLevel 100 → ≈ −1,000,000). The Alphablend backdrop then **washed out** the backpack/3D-items captions + burden meter + item cells (only the bright paperdoll survived the wash). Surfaced once B-Controller populated those panels. Continuation fix `417b137`: the sub-window mount keeps each slot's **own** frame ZLevel (not the base root's 1000), so panels sit in front of the backdrop. Confirmed live (Burden 17% + vertical bar + "Contents of Backpack" + full item grid all render on the dark backdrop). Locked by `InventoryFrameImportProbe` (real-dat: each mounted panel's ZOrder > the backdrop's). DO-NOT-RETRY: a mounted sub-window slot must NOT inherit the base layout root's ZLevel. Per-slot paperdoll silhouettes (generic `UiDatElement` sprite — need `0x10000032` UiItemSlot + per-slot art) → Sub-phase C.
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| AP-55 | The toolbar's item slots keep the hardcoded `UiItemSlot.EmptySprite = 0x060074CF` rather than resolving their cell template via attribute `0x1000000e`. Correct in outcome (the toolbar's `0x1000000e` resolves to a generic prototype whose empty media is `0x060074CF`) but not dat-resolved. | `src/AcDream.App/UI/UiItemSlot.cs` (`EmptySprite` default); `src/AcDream.App/UI/Layout/ToolbarController.cs` | The inventory lists now port the retail resolver (`ItemListCellTemplate`); the toolbar was left on its hardcoded default to avoid regressing frozen, working art. Retire when the toolbar is routed through `ItemListCellTemplate`. | If a future toolbar layout's `0x1000000e` points at a non-generic prototype, the toolbar would show stale art. | `UIElement_ItemList::InternalCreateItem` 0x004e3570; catalog `0x21000037` |
|
||||
| AP-57 | The open-container triangle (`0x06005D9C`) + selected-item square (`0x06004D21`) are drawn as **procedural `UiItemSlot` overlays** keyed by controller state (`_openContainer`/`_selectedItem`), reproducing the retail keying (`item.itemID == openContainerId / selectedItemId` per `UpdateOpenContainerIndicator 0x004e3070` / `ItemList_SetSelectedItem 0x004e2fe0`) but not via the dat prototype's `m_elem_Icon_OpenContainer`/`m_elem_Icon_Selected` state elements + `SetOpenContainerState`/`SetSelectedState` calls. | `src/AcDream.App/UI/UiItemSlot.cs`; `src/AcDream.App/UI/Layout/InventoryController.cs` | The procedural overlay produces the correct visual result; the 36×36 container prototype (`0x1000033F`) lacks the square child, so the procedural overlay is the only way to show the square on a selected bag — the dat-state path retail uses would require the prototype to be richer than it is. | A cell that should show an indicator via a dat-state path retail uses but we don't would be missed; outcome currently matches retail for the open/selected indicator cases. | `UIElement_ItemList::UpdateOpenContainerIndicator` 0x004e3070; `ItemList_SetSelectedItem` 0x004e2fe0; `SetOpenContainerState` 0x004e1200; `SetSelectedState` 0x004e1240 |
|
||||
| AP-58 | Inventory item **selection is panel-local + visual-only** — clicking a grid item moves the green square but does not wire to the selected-object bar (`SelectedObjectController`) or to global 3D-world selection. | `src/AcDream.App/UI/Layout/InventoryController.cs` (`SelectItem`) | The selected-object bar + 3D-world selection wiring is deferred to the selection follow-up; the panel-local square is the correct first step. | Selecting an inventory item doesn't populate the selected-object strip as retail does — functional gap deferred. | `gmInventoryUI`/`gmBackpackUI` item-select → `ClientUISystem::SetSelectedObject`; `SelectedObjectController` |
|
||||
| AP-59 | The per-cell container **capacity bar** (retail `UIElement_UIItem::UpdateCapacityDisplay 0x004e16e0`, element `0x10000347`) is drawn as a **procedural `UiItemSlot` overlay** (track `0x06004D22` full + fill `0x06004D23` clipped bottom-up to `GetContents(guid).Count / ItemsCapacity`), not via a real dat `UIElement_Meter` child. **Right-anchored flush** (the dat rect X=26 in a 36px cell sat ~5px off the edge — flush per the visual gate) and **bottom-up fill assumed** (the dat `m_eDirection` 0x6f isn't read, cf. AP-50). A CLOSED side bag reads empty until opened (its contents aren't indexed until `ViewContents`) — faithful to retail's known-children count, divergent if retail pre-loads. | `src/AcDream.App/UI/UiItemSlot.cs` (`CapacityFill` draw); `src/AcDream.App/UI/Layout/InventoryController.cs` (`SetCapacityBar`) | UiItemSlot is a behavioral leaf that paints overlays procedurally (cf. AP-57); the meter sprites + fill formula are the faithful port. Right-anchor + bottom-up were visual-gate calls. Further visual polish is deferred — ISSUES #146. | Fill direction / exact bar rect could differ from retail's `m_eDirection` + dat X; closed-bag bars read empty if retail pre-loads container counts. | `UIElement_UIItem::UpdateCapacityDisplay` 0x004e16e0; element `0x10000347` (back `0x06004D22` / front `0x06004D23`); `GetNumContainedItems` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ public sealed class InventoryController
|
|||
var main = new UiItemSlot { SpriteResolve = _topContainer.SpriteResolve };
|
||||
main.SetItem(p, _iconIds(ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u));
|
||||
main.Clicked = () => OpenContainer(p);
|
||||
SetCapacityBar(main, p); // main-pack fullness (items / ItemsCapacity)
|
||||
_topContainer.AddItem(main);
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +262,7 @@ public sealed class InventoryController
|
|||
: _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
||||
var cell = new UiItemSlot { SpriteResolve = list.SpriteResolve };
|
||||
cell.SetItem(guid, tex);
|
||||
if (isContainer) cell.Clicked = () => OpenContainer(guid);
|
||||
if (isContainer) { cell.Clicked = () => OpenContainer(guid); SetCapacityBar(cell, guid); }
|
||||
else cell.Clicked = () => SelectItem(guid);
|
||||
list.AddItem(cell);
|
||||
}
|
||||
|
|
@ -269,6 +270,19 @@ public sealed class InventoryController
|
|||
private static void AddEmptyCell(UiItemList list)
|
||||
=> list.AddItem(new UiItemSlot { SpriteResolve = list.SpriteResolve });
|
||||
|
||||
/// <summary>Set the per-cell container capacity bar — retail UIElement_UIItem::UpdateCapacityDisplay
|
||||
/// (0x004e16e0): visible only for a container with itemsCapacity > 0; fill =
|
||||
/// GetNumContainedItems / itemsCapacity, clamped [0,1]. -1 hides the bar (non-container / unknown
|
||||
/// capacity). For a CLOSED side bag the contents aren't indexed until it's opened (ViewContents),
|
||||
/// so the bar reads empty until then — faithful to retail's known-children count.</summary>
|
||||
private void SetCapacityBar(UiItemSlot cell, uint containerGuid)
|
||||
{
|
||||
int cap = _objects.Get(containerGuid)?.ItemsCapacity ?? 0;
|
||||
if (cap <= 0) { cell.CapacityFill = -1f; return; }
|
||||
int n = _objects.GetContents(containerGuid).Count;
|
||||
cell.CapacityFill = Math.Clamp(n / (float)cap, 0f, 1f);
|
||||
}
|
||||
|
||||
/// <summary>Select an item (panel-wide green square) without changing the open container or
|
||||
/// touching the wire. Retail: UIElement_ItemList::ItemList_SetSelectedItem (0x004e2fe0).</summary>
|
||||
private void SelectItem(uint guid)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,16 @@ public sealed class UiItemSlot : UiElement
|
|||
/// on the 36×36 container cell too (whose prototype lacks the square child).</summary>
|
||||
public uint SelectedSprite { get; set; } = 0x06004D21u;
|
||||
|
||||
/// <summary>Container fullness [0..1], or -1 = hidden (the cell is not a container, or its
|
||||
/// itemsCapacity is unknown/0). Port of UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a
|
||||
/// per-cell vertical UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only when
|
||||
/// isContainer && itemsCapacity > 0, fill = numContainedItems / itemsCapacity (meter attr 0x69).</summary>
|
||||
public float CapacityFill { get; set; } = -1f;
|
||||
/// <summary>Capacity-bar track sprite (meter 0x10000347 DirectState).</summary>
|
||||
public uint CapacityBackSprite { get; set; } = 0x06004D22u;
|
||||
/// <summary>Capacity-bar fill sprite (meter 0x10000347 front child 0x00000002 DirectState).</summary>
|
||||
public uint CapacityFrontSprite { get; set; } = 0x06004D23u;
|
||||
|
||||
/// <summary>Accept/reject overlay state while a drag hovers this cell.</summary>
|
||||
public enum DragAcceptState { None, Accept, Reject }
|
||||
private DragAcceptState _dragAccept = DragAcceptState.None;
|
||||
|
|
@ -237,6 +247,33 @@ public sealed class UiItemSlot : UiElement
|
|||
}
|
||||
}
|
||||
|
||||
// Container capacity bar — UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a vertical
|
||||
// UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only for container cells
|
||||
// (CapacityFill >= 0). Track drawn full; fill clipped to the fraction from the BOTTOM (the
|
||||
// "how full" direction). Procedural — UiItemSlot is a behavioral leaf. Guard id != 0 first.
|
||||
if (CapacityFill >= 0f && SpriteResolve is not null)
|
||||
{
|
||||
const float by = 1f, bw = 5f, bh = 30f; // element 0x10000347 size (dat 5×30 at y=1)
|
||||
float bx = Width - bw; // flush to the cell's right edge (visual gate: the dat X=26 sat ~5px off the edge)
|
||||
if (CapacityBackSprite != 0)
|
||||
{
|
||||
var (bt, _, _) = SpriteResolve(CapacityBackSprite);
|
||||
if (bt != 0) ctx.DrawSprite(bt, bx, by, bw, bh, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
float f = Math.Clamp(CapacityFill, 0f, 1f);
|
||||
if (f > 0f && CapacityFrontSprite != 0)
|
||||
{
|
||||
var (ft, _, _) = SpriteResolve(CapacityFrontSprite);
|
||||
if (ft != 0)
|
||||
{
|
||||
// Bottom-up fill: draw the bottom f-fraction of the bar, sampling the matching
|
||||
// bottom slice of the front sprite (UV v from 1-f to 1).
|
||||
float fh = bh * f;
|
||||
ctx.DrawSprite(ft, bx, by + (bh - fh), bw, fh, 0f, 1f - f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open-container triangle (retail SetOpenContainerState 0x004e1200) + selected-item square
|
||||
// (retail SetSelectedState 0x004e1240). Drawn procedurally like the digit/drag overlays;
|
||||
// guard id != 0 BEFORE resolving (feedback_ui_resolve_zero_magenta). Triangle under the
|
||||
|
|
|
|||
|
|
@ -380,6 +380,29 @@ public class InventoryControllerTests
|
|||
Assert.Equal(0x0600127Eu, mainPackCall.Value.icon);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SideBagCell_capacityBar_reflectsContents()
|
||||
{
|
||||
var (layout, _, containers, _, _, _, _, _) = BuildLayout();
|
||||
var objects = new ClientObjectTable();
|
||||
SeedBag(objects, 0xC, slot: 0, itemsCapacity: 24); // a side bag (cap 24)
|
||||
for (uint i = 0; i < 6; i++) SeedContained(objects, 0xB0u + i, 0xC, slot: (int)i); // 6 items inside
|
||||
Bind(layout, objects);
|
||||
|
||||
// Retail UpdateCapacityDisplay: fill = contained / itemsCapacity = 6/24 = 0.25 (exact in float).
|
||||
Assert.Equal(0.25f, containers.GetItem(0)!.CapacityFill);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LooseGridItem_hasNoCapacityBar()
|
||||
{
|
||||
var (layout, grid, _, _, _, _, _, _) = BuildLayout();
|
||||
var objects = new ClientObjectTable();
|
||||
SeedContained(objects, 0xA, Player, slot: 0); // a loose, non-container item
|
||||
Bind(layout, objects);
|
||||
Assert.Equal(-1f, grid.GetItem(0)!.CapacityFill); // hidden — not a container
|
||||
}
|
||||
|
||||
// Reads the text of the UiText caption child attached by the controller.
|
||||
private static string CaptionText(UiElement host)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -157,4 +157,14 @@ public class UiItemSlotTests
|
|||
[Fact]
|
||||
public void OpenContainerSprite_default_isTriangle()
|
||||
=> Assert.Equal(0x06005D9Cu, new UiItemSlot().OpenContainerSprite);
|
||||
|
||||
// ── Container capacity bar (decomp UpdateCapacityDisplay 0x004e16e0, element 0x10000347) ──
|
||||
[Fact]
|
||||
public void CapacityFill_defaultsHidden() => Assert.Equal(-1f, new UiItemSlot().CapacityFill);
|
||||
|
||||
[Fact]
|
||||
public void CapacityBackSprite_default() => Assert.Equal(0x06004D22u, new UiItemSlot().CapacityBackSprite);
|
||||
|
||||
[Fact]
|
||||
public void CapacityFrontSprite_default() => Assert.Equal(0x06004D23u, new UiItemSlot().CapacityFrontSprite);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue