fix(D.2b): PaperdollController review fixes — player-scope Concerns, loop clarity

Code-quality review on Task 5:
- I1: Concerns was unscoped (CurrentlyEquippedLocation != None) → an NPC's
  wielded item (which also carries that wire field) triggered spurious full
  repaints. Narrowed to (WielderId==p || ContainerId==p), matching
  InventoryController; OnObjectMoved's from/to-player backstop still catches
  unwield-into-a-side-bag. Populate's own scope already prevented wrong data;
  this kills the wasted repaints.
- I2: replaced the dual-`index++` (assign-vs-skip) with a for-i loop;
  SlotIndex = SlotMap position (= the drag payload's SourceSlot on unwield).
- M1/M2: comment that the cell's SpriteResolve + the discrete-slot accept/
  reject ring (0x060011F9/F8, not the grid insert-arrow) are factory-provided.
- Added two behavioral tests: a live player wield repaints the slot
  (ObjectMoved → Concerns → Populate); an NPC's wielded item never appears on
  the doll (player-scoping).
- Synced the stale spec §4b/§6c/§8 to the Task-3 Option-1 reality (the
  optimistic wield is ContainerId-based and does NOT write WielderId).

App suite 580 passed / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-22 22:32:34 +02:00
parent 9f187c3e31
commit 3b8a39c49e
3 changed files with 54 additions and 15 deletions

View file

@ -57,14 +57,18 @@ public sealed class PaperdollController : IItemListDragHandler
{
_objects = objects; _playerGuid = playerGuid; _iconIds = iconIds; _sendWield = sendWield;
int index = 0;
foreach (var (element, mask) in SlotMap)
for (int i = 0; i < SlotMap.Length; i++)
{
if (layout.FindElement(element) is not UiItemList list) { index++; continue; }
var (element, mask) = SlotMap[i];
if (layout.FindElement(element) is not UiItemList list) continue;
list.RegisterDragHandler(this);
list.Cell.SourceKind = ItemDragSource.Equipment;
list.Cell.SlotIndex = index++; // a stable per-slot index (routing uses the list, not this)
list.Cell.EmptySprite = 0u; // TRANSPARENT empty slot (brainstorm decision; no doll behind it)
list.Cell.SlotIndex = i; // SlotMap position = this equipped item's drag-payload SourceSlot when dragged out to unwield
list.Cell.EmptySprite = 0u; // TRANSPARENT empty slot (brainstorm decision; no doll behind it in Slice 1)
// Cell.SpriteResolve + the default accept/reject sprites (ItemSlot_DragOver_Accept ring
// 0x060011F9 / reject circle 0x060011F8 — the discrete-slot frames, NOT the inventory grid's
// insert-arrow 0x060011F7) are already wired by DatWidgetFactory when it built the UiItemList;
// no need to re-set them here.
_slots.Add((mask, list));
}
@ -85,11 +89,16 @@ public sealed class PaperdollController : IItemListDragHandler
private void OnObjectMoved(ClientObject o, uint from, uint to)
{ if (Concerns(o) || from == _playerGuid() || to == _playerGuid()) Populate(); }
/// <summary>The object is (or just became / ceased to be) the player's equipped gear.</summary>
/// <summary>The object belongs to the player (wielded gear or pack contents) — so a change to it may
/// add/remove/repaint a doll slot. Player-scoped: an NPC's or vendor's wielded item (which also carries
/// CurrentlyEquippedLocation from the wire) must NOT trigger a repaint. A player-equipped item always
/// has WielderId==p (login, from CreateObject) or ContainerId==p (live/optimistic wield, set by
/// WieldItemOptimistic), so the equip-location need not be tested here; OnObjectMoved adds the from/to
/// player backstop for moves that transiently satisfy neither (e.g. unwield into a side bag).</summary>
private bool Concerns(ClientObject o)
{
uint p = _playerGuid();
return o.CurrentlyEquippedLocation != EquipMask.None || o.WielderId == p || o.ContainerId == p;
return o.WielderId == p || o.ContainerId == p;
}
public void Populate()