fix(ui): port exact paperdoll body selection

Resolve and sample retail's authored nine-color paperdoll click map, preserve local click coordinates, and select the stable highest-priority worn item with the player fallback. Keep targeted-use body clicks routed to self and pin both synthetic and live-DAT conformance.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 11:56:26 +02:00
parent 2644d1d527
commit 0b74d19475
16 changed files with 490 additions and 32 deletions

View file

@ -40,11 +40,19 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
private bool _selected;
private bool _hotClicking;
private bool _suppressNextClick;
private int _pointerX;
private int _pointerY;
private double _nextHotClickTime = double.NaN;
/// <summary>Optional click handler. Wired by the controller (e.g. chat Submit, ToggleMaximize).</summary>
public Action? OnClick { get; set; }
/// <summary>
/// Optional position-aware click handler. Coordinates are local pixels in this button,
/// matching retail's <c>UIElementMessageInfo.ptWindow</c> paperdoll hit-test input.
/// </summary>
public Action<int, int>? OnClickAt { get; set; }
/// <summary>
/// Optional item-drop target callbacks. Retail uses ordinary buttons as drop surfaces
/// in a few panels (notably gmToolbarUI's inventory button), so this belongs on the
@ -265,12 +273,15 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
UpdateVisualState();
return true;
case UiEventType.MouseDown:
_pointerX = e.Data1;
_pointerY = e.Data2;
_pointerOver = ContainsLocal(e.Data1, e.Data2);
_pressed = true;
UpdateVisualState();
if (HotClickEnabled && Enabled)
{
OnClick?.Invoke();
OnClickAt?.Invoke(_pointerX, _pointerY);
_hotClicking = true;
_nextHotClickTime = double.NaN;
}
@ -278,12 +289,16 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
case UiEventType.MouseMove:
if (_pressed)
{
_pointerX = e.Data1;
_pointerY = e.Data2;
_pointerOver = ContainsLocal(e.Data1, e.Data2);
UpdateVisualState();
return true;
}
return false;
case UiEventType.MouseUp:
_pointerX = e.Data1;
_pointerY = e.Data2;
_pointerOver = ContainsLocal(e.Data1, e.Data2);
_suppressNextClick = _hotClicking && _pointerOver;
_hotClicking = false;
@ -301,7 +316,8 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
return true;
}
OnClick?.Invoke();
return OnClick is not null;
OnClickAt?.Invoke(e.Data1, e.Data2);
return OnClick is not null || OnClickAt is not null;
case UiEventType.DragEnter:
_itemDragAcceptance = e.Payload is ItemDragPayload payload
? OnItemDragOver?.Invoke(payload) ?? ItemDragAcceptance.None
@ -348,6 +364,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
if (_pointerOver && nowSeconds >= _nextHotClickTime)
{
OnClick?.Invoke();
OnClickAt?.Invoke(_pointerX, _pointerY);
_nextHotClickTime += HotClickRepeatInterval;
}
}