feat(ui): D.2b item interaction + retail cursors + live character sheet
Lands the codex-worktree D.2b stream plus the extraction the 2026-07-02 UI architecture review mandated before commit: - ItemInteractionController: single owner of double-click use/equip/ container-open, targeted-use mode (health kits), drag-out drop; toolbar shortcut drags don't drop the real item. ItemEquipRules for multi-slot (coat) coverage via equip masks. - Cursor phase: CursorFeedbackController (semantic priority chain: drag > resize > window-move > target-mode > text) + RetailCursorCatalog (enums 0x27/0x28/0x29, hotspot 14,14; ClientUISystem::UpdateCursorState 0x00564630) resolved through the portal EnumIDMap chain by RetailCursorResolver; RetailCursorManager applies dat cursor art to the OS cursor. Register row AP-72 covers the OS standard-cursor fallback. - Character window goes live: CharacterSheetProvider owns sheet assembly, XP-curve/raise-cost math and the raise flow — extracted out of GameWindow per Code Structure Rule 1 instead of committing the ~430-line feature body there. Optimistic XP/credit debits go through eventful store APIs (new ClientObjectTable.UpdateInt64Property + LocalPlayerState.DebitIntProperty/DebitInt64Property) instead of raw property-dictionary writes; register row AP-73 covers the still-missing raise ledger (#163). - RetailWindowFrame: the shared nine-slice window mount recipe; the character window uses it, remaining windows migrate via #164. - Status-bar buttons toggle inventory/character windows; retail row-major backpack ordering; WorldSession.SendUseWithTarget + raise/train sends. GameWindow shrinks 14,214 -> 13,877 lines despite the new features; the sheet/raise logic is unit-tested in CharacterSheetProviderTests instead of trapped in the god object. Build green; full suite 3,286 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e3fc7ac5ba
commit
b7dc91a053
74 changed files with 6669 additions and 238 deletions
|
|
@ -117,9 +117,23 @@ public sealed class PropertyBundle
|
|||
public Dictionary<uint, uint> InstanceIds { get; } = new();
|
||||
|
||||
public int GetInt (uint k, int def = 0) => Ints.TryGetValue(k, out var v) ? v : def;
|
||||
public long GetInt64 (uint k, long def = 0) => Int64s.TryGetValue(k, out var v) ? v : def;
|
||||
public bool GetBool (uint k, bool def = false) => Bools.TryGetValue(k, out var v) ? v : def;
|
||||
public double GetFloat (uint k, double def = 0) => Floats.TryGetValue(k, out var v) ? v : def;
|
||||
public string GetString(uint k, string def = "") => Strings.TryGetValue(k, out var v) ? v : def;
|
||||
|
||||
public PropertyBundle Clone()
|
||||
{
|
||||
var copy = new PropertyBundle();
|
||||
foreach (var kv in Ints) copy.Ints[kv.Key] = kv.Value;
|
||||
foreach (var kv in Int64s) copy.Int64s[kv.Key] = kv.Value;
|
||||
foreach (var kv in Bools) copy.Bools[kv.Key] = kv.Value;
|
||||
foreach (var kv in Floats) copy.Floats[kv.Key] = kv.Value;
|
||||
foreach (var kv in Strings) copy.Strings[kv.Key] = kv.Value;
|
||||
foreach (var kv in DataIds) copy.DataIds[kv.Key] = kv.Value;
|
||||
foreach (var kv in InstanceIds) copy.InstanceIds[kv.Key] = kv.Value;
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -150,12 +164,20 @@ public sealed class ClientObject
|
|||
public int Value { get; set; } // pyreals
|
||||
public uint ContainerId { get; set; } // parent container ObjectId, or 0
|
||||
public int ContainerSlot { get; set; } = -1;
|
||||
/// <summary>
|
||||
/// Retail <c>ContentProfile.m_uContainerProperties</c> hint from
|
||||
/// PlayerDescription/ViewContents: 0 = loose item list, nonzero = container list.
|
||||
/// Kept separate from <see cref="Type"/> because CreateObject owns real item data.
|
||||
/// </summary>
|
||||
public uint ContainerTypeHint { get; set; }
|
||||
public bool Attuned { get; set; }
|
||||
public bool Bonded { get; set; }
|
||||
public uint WielderId { get; set; } // PropertyInstanceId.Wielder; 0 = not wielded
|
||||
public int ItemsCapacity { get; set; } // main-pack slots (containers)
|
||||
public int ContainersCapacity{ get; set; } // side-pack slots (containers)
|
||||
public uint Priority { get; set; } // ClothingPriority / CoverageMask layer order
|
||||
public uint? Useability { get; set; } // ITEM_USEABLE from PublicWeenieDesc
|
||||
public uint? TargetType { get; set; } // ITEM_TYPE mask for targeted-use compatibility
|
||||
public int Structure { get; set; } // charges/uses remaining
|
||||
public int MaxStructure { get; set; }
|
||||
public float Workmanship { get; set; } // 0..10 (fractional on the wire)
|
||||
|
|
@ -193,7 +215,52 @@ public readonly record struct WeenieData(
|
|||
int? ContainersCapacity,
|
||||
int? Structure,
|
||||
int? MaxStructure,
|
||||
float? Workmanship);
|
||||
float? Workmanship,
|
||||
uint? Useability = null,
|
||||
uint? TargetType = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retail ITEM_USEABLE helpers (acclient.h:6478, ItemUses::* at 0x004fccd0).
|
||||
/// Low 16 bits describe where the source may be used from; high 16 bits
|
||||
/// describe where the acquired target may be.
|
||||
/// </summary>
|
||||
public static class ItemUseability
|
||||
{
|
||||
public const uint Undef = 0x0u;
|
||||
public const uint No = 0x1u;
|
||||
public const uint Self = 0x2u;
|
||||
public const uint Wielded = 0x4u;
|
||||
public const uint Contained = 0x8u;
|
||||
public const uint Viewed = 0x10u;
|
||||
public const uint Remote = 0x20u;
|
||||
public const uint NeverWalk = 0x40u;
|
||||
public const uint ObjSelf = 0x80u;
|
||||
|
||||
public const uint SourceMask = 0x0000FFFFu;
|
||||
public const uint TargetMask = 0xFFFF0000u;
|
||||
|
||||
public static bool IsTargeted(uint useability)
|
||||
=> (useability & TargetMask) != 0;
|
||||
|
||||
public static bool AllowsSelfTarget(uint useability)
|
||||
=> ((useability >> 16) & Self) != 0;
|
||||
|
||||
public static bool AllowsObjectSelfTarget(uint useability)
|
||||
=> ((useability >> 16) & ObjSelf) != 0;
|
||||
|
||||
public static uint SourceFlags(uint useability)
|
||||
=> useability & SourceMask;
|
||||
|
||||
public static uint TargetFlags(uint useability)
|
||||
=> (useability & TargetMask) >> 16;
|
||||
|
||||
public static bool IsDirectUseable(uint useability)
|
||||
{
|
||||
uint source = SourceFlags(useability);
|
||||
return !IsTargeted(useability)
|
||||
&& (source & ~(No | NeverWalk)) != Undef;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Container = inventory pack. Hierarchy is strictly 2-deep: character
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue