feat(D.2b): Slice 2 — paperdoll armor/non-armor partition + Slots toggle state

Adds ArmorSlotElementIds (the exact 9 ids gmPaperDollUI::ListenToElementMessage
flips per decomp 175674-175706) and PaperdollViewState (SlotView/DollVisible/
ArmorSlotsVisible/Toggle) as a nested public class on PaperdollController.

Wires ApplyView() into the constructor so the Slots button (0x100005BE) drives
armor-slot Visible and the doll viewport Visible on every click. Initial state
is doll-view (armor slots hidden). The doll viewport element (0x100001D5) is
bound as UiElement? so this slice stays independent of Slice 3's IUiViewportRenderer
seam; _armorSlots collects whichever of the 9 ids were found in the live layout.

Three new unit tests verify the id set, the default state, and the round-trip
toggle against the public PaperdollViewState surface only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-23 08:57:50 +02:00
parent 1c7f20fd08
commit c5604ff6ad
2 changed files with 97 additions and 0 deletions

View file

@ -14,6 +14,32 @@ namespace AcDream.App.UI.Layout;
/// </summary>
public sealed class PaperdollController : IItemListDragHandler
{
// ── Slots-toggle public surface ───────────────────────────────────────────────────────────────
/// <summary>
/// The 9 armor-slot element-ids whose Visible state the Slots button (0x100005BE) toggles.
/// Doll-view: hidden. Slot-view: shown. Source: gmPaperDollUI::ListenToElementMessage decomp
/// 175674-175706 — these are the only 9 ids that element flips; the other ~12 equip slots
/// (jewelry, weapon, wrists, feet…) are NON-armor and remain visible in both views.
/// </summary>
public static readonly uint[] ArmorSlotElementIds =
{
0x100005abu, 0x100005acu, 0x100005adu, 0x100005aeu, 0x100005afu,
0x100005b0u, 0x100005b1u, 0x100005b2u, 0x100005b3u,
};
/// <summary>
/// View-state model for the Slots toggle (pure logic, no UI coupling).
/// Default is doll-view (SlotView == false): the 3-D character is visible,
/// the 9 armor slots are hidden. Calling Toggle() alternates between views.
/// </summary>
public sealed class PaperdollViewState
{
public bool SlotView { get; private set; } // false = doll-view (default)
public bool DollVisible => !SlotView;
public bool ArmorSlotsVisible => SlotView;
public void Toggle() => SlotView = !SlotView;
}
// WEAPON_READY_SLOT_LOC (acclient.h:3235): the weapon-hand doll slot accepts any wieldable weapon.
private const EquipMask WeaponSlotMask =
EquipMask.MeleeWeapon | EquipMask.MissileWeapon | EquipMask.Held | EquipMask.TwoHanded;
@ -51,6 +77,11 @@ public sealed class PaperdollController : IItemListDragHandler
private readonly Action<uint, uint>? _sendWield; // (itemGuid, equipMask) → GetAndWieldItem 0x001A
private readonly List<(EquipMask Mask, UiItemList List)> _slots = new();
// ── Slots-toggle state ────────────────────────────────────────────────────────────────────────
private readonly PaperdollViewState _viewState = new();
private readonly List<UiItemList> _armorSlots = new();
private UiElement? _dollViewport; // UiViewport wired in Slice 3; UiElement? keeps this task independent
private PaperdollController(
ImportedLayout layout, ClientObjectTable objects, Func<uint> playerGuid,
Func<ItemType, uint, uint, uint, uint, uint> iconIds, Action<uint, uint>? sendWield,
@ -80,6 +111,17 @@ public sealed class PaperdollController : IItemListDragHandler
_objects.ObjectRemoved += OnObjectChanged;
_objects.ObjectUpdated += OnObjectChanged;
// ── Slots-toggle wiring ───────────────────────────────────────────────────────────────────
foreach (var id in ArmorSlotElementIds)
if (layout.FindElement(id) is UiItemList armor) _armorSlots.Add(armor);
_dollViewport = layout.FindElement(0x100001D5u); // doll viewport (may be null until Slice 3)
if (layout.FindElement(0x100005BEu) is UiButton slotsBtn)
slotsBtn.OnClick = () => { _viewState.Toggle(); ApplyView(); };
ApplyView(); // initial state = doll-view (armor slots hidden)
Populate();
}
@ -161,6 +203,17 @@ public sealed class PaperdollController : IItemListDragHandler
_sendWield?.Invoke(payload.ObjId, (uint)wieldMask);
}
/// <summary>
/// Pushes the current <see cref="PaperdollViewState"/> onto the live widgets:
/// hides/shows the doll viewport and each armor slot according to the toggle.
/// Called once at construction (doll-view default) and on every button click.
/// </summary>
private void ApplyView()
{
if (_dollViewport is not null) _dollViewport.Visible = _viewState.DollVisible;
foreach (var a in _armorSlots) a.Visible = _viewState.ArmorSlotsVisible;
}
/// <summary>Detach event handlers (idempotent).</summary>
public void Dispose()
{