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:
Erik 2026-06-22 15:54:54 +02:00
parent 077586a0f0
commit a45c421bd1
6 changed files with 112 additions and 1 deletions

View file

@ -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 &gt; 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)

View file

@ -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 &amp;&amp; itemsCapacity &gt; 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