diff --git a/src/AcDream.App/UI/Layout/PaperdollController.cs b/src/AcDream.App/UI/Layout/PaperdollController.cs
index 41763f01..82c47758 100644
--- a/src/AcDream.App/UI/Layout/PaperdollController.cs
+++ b/src/AcDream.App/UI/Layout/PaperdollController.cs
@@ -14,6 +14,32 @@ namespace AcDream.App.UI.Layout;
///
public sealed class PaperdollController : IItemListDragHandler
{
+ // ── Slots-toggle public surface ───────────────────────────────────────────────────────────────
+ ///
+ /// 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.
+ ///
+ public static readonly uint[] ArmorSlotElementIds =
+ {
+ 0x100005abu, 0x100005acu, 0x100005adu, 0x100005aeu, 0x100005afu,
+ 0x100005b0u, 0x100005b1u, 0x100005b2u, 0x100005b3u,
+ };
+
+ ///
+ /// 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.
+ ///
+ 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? _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 _armorSlots = new();
+ private UiElement? _dollViewport; // UiViewport wired in Slice 3; UiElement? keeps this task independent
+
private PaperdollController(
ImportedLayout layout, ClientObjectTable objects, Func playerGuid,
Func iconIds, Action? 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);
}
+ ///
+ /// Pushes the current 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.
+ ///
+ private void ApplyView()
+ {
+ if (_dollViewport is not null) _dollViewport.Visible = _viewState.DollVisible;
+ foreach (var a in _armorSlots) a.Visible = _viewState.ArmorSlotsVisible;
+ }
+
/// Detach event handlers (idempotent).
public void Dispose()
{
diff --git a/tests/AcDream.App.Tests/UI/Layout/PaperdollToggleTests.cs b/tests/AcDream.App.Tests/UI/Layout/PaperdollToggleTests.cs
new file mode 100644
index 00000000..7241dd01
--- /dev/null
+++ b/tests/AcDream.App.Tests/UI/Layout/PaperdollToggleTests.cs
@@ -0,0 +1,44 @@
+using AcDream.App.UI.Layout;
+using Xunit;
+
+namespace AcDream.App.Tests.UI.Layout;
+
+public class PaperdollToggleTests
+{
+ // The 9 armor element-ids that gmPaperDollUI::ListenToElementMessage flips
+ // (decomp 175674-175706). Doll-view hides these; slot-view shows them.
+ private static readonly uint[] ArmorIds =
+ {
+ 0x100005abu, 0x100005acu, 0x100005adu, 0x100005aeu, 0x100005afu,
+ 0x100005b0u, 0x100005b1u, 0x100005b2u, 0x100005b3u,
+ };
+
+ [Fact]
+ public void ArmorSlotIds_match_the_decomp_nine()
+ {
+ Assert.Equal(ArmorIds, PaperdollController.ArmorSlotElementIds);
+ }
+
+ [Fact]
+ public void DollView_default_shows_doll_hides_armor()
+ {
+ var v = new PaperdollController.PaperdollViewState();
+ Assert.False(v.SlotView);
+ Assert.True(v.DollVisible);
+ Assert.False(v.ArmorSlotsVisible);
+ }
+
+ [Fact]
+ public void Toggle_to_slotview_hides_doll_shows_armor_and_back()
+ {
+ var v = new PaperdollController.PaperdollViewState();
+ v.Toggle();
+ Assert.True(v.SlotView);
+ Assert.False(v.DollVisible);
+ Assert.True(v.ArmorSlotsVisible);
+ v.Toggle();
+ Assert.False(v.SlotView);
+ Assert.True(v.DollVisible);
+ Assert.False(v.ArmorSlotsVisible);
+ }
+}