fix(ui): retail-faithful use-target compat gate + paperdoll doll self-target

Visual gate on the item-interaction slice found two target-use bugs:
the valid-target bullseye showed over inventory ITEMS (a coat is not a
valid healing-kit target) while the paperdoll doll showed blocked (it
should mean SELF).

- TargetCompatible is now a faithful port of
  ItemHolder::IsTargetCompatibleWithTargetingObject (0x00588070):
  a location constraint applies only when the least-limited target-use
  bit (ItemUses::GetLeastLimitedTargetUse 0x004fcd50, new
  ItemUseability.LeastLimitedTargetUse) is Contained/Wielded; the
  player as target requires the Self bit (IsUseable_SelfTarget
  0x004fcd30); and EVERY target passes retail's kind gate
  source._targetType & target->InqType(). The previous hand-rolled
  arms (Remote => accept anything, invented Viewed/Contained accepts,
  self path skipping the kind gate) were the yellow-over-items bug.
  Retail's tradeState refusal is skipped (no trade state yet).
- The paperdoll doll viewport now carries UseTargetGuidProvider =
  player and Clicked -> AcquireSelfTarget (UiViewport gains an opt-in
  Clicked handler), so hovering the doll resolves the self cursor and
  clicking it applies the armed kit to yourself.
- AcquireTarget logs one [use-target] line (wire useability, TargetType
  mask, target kind, decision) so live refusals are diagnosable from
  the launch log.
- Test fixtures updated from the guessed kit useability 0x000A0008 to
  the real USEABLE_SOURCE_CONTAINED_TARGET_REMOTE_OR_SELF (0x00220008,
  ACE Usable.SourceContainedTargetRemoteOrSelf) + TargetType masks;
  new coverage pins the kind gate, the Self-bit requirement, the
  missing-mask-matches-nothing shape, and the Contained location rule.

Full suite green (3,294).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-03 10:09:13 +02:00
parent d3cab1ab10
commit 047410ccc9
8 changed files with 190 additions and 21 deletions

View file

@ -254,6 +254,25 @@ public static class ItemUseability
public static uint TargetFlags(uint useability)
=> (useability & TargetMask) >> 16;
/// <summary>
/// Retail <c>ItemUses::GetLeastLimitedTargetUse</c> (0x004fcd50): the most
/// permissive target-use bit present, priority Remote &gt; Viewed &gt;
/// Contained &gt; Wielded &gt; Self &gt; ObjSelf. The target-compat gate
/// (<c>ItemHolder::IsTargetCompatibleWithTargetingObject</c> 0x00588070)
/// only enforces a location constraint when the BEST available bit is
/// Contained or Wielded.
/// </summary>
public static uint LeastLimitedTargetUse(uint useability)
{
uint t = TargetFlags(useability);
if ((t & Remote) != 0) return Remote;
if ((t & Viewed) != 0) return Viewed;
if ((t & Contained) != 0) return Contained;
if ((t & Wielded) != 0) return Wielded;
if ((t & Self) != 0) return Self;
return t & ObjSelf;
}
public static bool IsDirectUseable(uint useability)
{
uint source = SourceFlags(useability);