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
|
|
@ -54,8 +54,15 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
private static readonly uint[] CombatIndicatorIds =
|
||||
{ 0x10000192u, 0x10000193u, 0x10000194u, 0x10000195u };
|
||||
|
||||
private const uint CharacterButtonId = 0x10000199u;
|
||||
private const uint InventoryButtonId = 0x100001B1u;
|
||||
private const string ButtonClosedState = "Normal";
|
||||
private const string ButtonOpenState = "Highlight";
|
||||
|
||||
private readonly UiItemList?[] _slots = new UiItemList?[SlotIds.Length];
|
||||
private readonly UiElement?[] _combatIndicators = new UiElement?[CombatIndicatorIds.Length];
|
||||
private readonly UiButton? _characterButton;
|
||||
private readonly UiButton? _inventoryButton;
|
||||
private readonly ClientObjectTable _repo;
|
||||
private readonly Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> _shortcuts;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex
|
||||
|
|
@ -64,6 +71,7 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
private bool _storeLoaded;
|
||||
private readonly Action<uint, uint>? _sendAddShortcut; // (index, objectGuid)
|
||||
private readonly Action<uint>? _sendRemoveShortcut; // (index)
|
||||
private readonly ItemInteractionController? _itemInteraction;
|
||||
|
||||
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
|
||||
// Read from LayoutDesc 0x21000037, element 0x1000034A under composite 0x10000346.
|
||||
|
|
@ -87,6 +95,7 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
uint[]? peaceDigits,
|
||||
uint[]? warDigits,
|
||||
uint[]? emptyDigits,
|
||||
ItemInteractionController? itemInteraction = null,
|
||||
Action<uint, uint>? sendAddShortcut = null,
|
||||
Action<uint>? sendRemoveShortcut = null)
|
||||
{
|
||||
|
|
@ -97,6 +106,7 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
_peaceDigits = peaceDigits;
|
||||
_warDigits = warDigits;
|
||||
_emptyDigits = emptyDigits;
|
||||
_itemInteraction = itemInteraction;
|
||||
_sendAddShortcut = sendAddShortcut;
|
||||
_sendRemoveShortcut = sendRemoveShortcut;
|
||||
|
||||
|
|
@ -120,6 +130,9 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
for (int i = 0; i < CombatIndicatorIds.Length; i++)
|
||||
_combatIndicators[i] = layout.FindElement(CombatIndicatorIds[i]);
|
||||
|
||||
_characterButton = layout.FindElement(CharacterButtonId) as UiButton;
|
||||
_inventoryButton = layout.FindElement(InventoryButtonId) as UiButton;
|
||||
|
||||
// Hide target-object meters + stack slider (gmToolbarUI::PostInit).
|
||||
foreach (var id in HiddenIds)
|
||||
if (layout.FindElement(id) is { } e) e.Visible = false;
|
||||
|
|
@ -201,16 +214,44 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
uint[]? peaceDigits = null,
|
||||
uint[]? warDigits = null,
|
||||
uint[]? emptyDigits = null,
|
||||
ItemInteractionController? itemInteraction = null,
|
||||
Action<uint, uint>? sendAddShortcut = null,
|
||||
Action<uint>? sendRemoveShortcut = null)
|
||||
{
|
||||
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
||||
peaceDigits, warDigits, emptyDigits,
|
||||
peaceDigits, warDigits, emptyDigits, itemInteraction,
|
||||
sendAddShortcut, sendRemoveShortcut);
|
||||
c.Populate();
|
||||
return c;
|
||||
}
|
||||
|
||||
/// <summary>Wire the retail status-bar launch buttons to top-level window toggles.</summary>
|
||||
public void BindWindowToggles(Action? toggleInventory, Action? toggleCharacter)
|
||||
{
|
||||
if (_inventoryButton is not null)
|
||||
{
|
||||
if (_itemInteraction is not null)
|
||||
_inventoryButton.UseTargetGuidProvider = () => _itemInteraction.PlayerGuid;
|
||||
_inventoryButton.OnClick = () =>
|
||||
{
|
||||
if (_itemInteraction?.AcquireSelfTarget() == true) return;
|
||||
toggleInventory?.Invoke();
|
||||
};
|
||||
}
|
||||
if (_characterButton is not null)
|
||||
_characterButton.OnClick = toggleCharacter;
|
||||
}
|
||||
|
||||
public void SetInventoryOpen(bool open) => SetButtonOpen(_inventoryButton, open);
|
||||
|
||||
public void SetCharacterOpen(bool open) => SetButtonOpen(_characterButton, open);
|
||||
|
||||
private static void SetButtonOpen(UiButton? button, bool open)
|
||||
{
|
||||
if (button is not null)
|
||||
button.ActiveState = open ? ButtonOpenState : ButtonClosedState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Port of <c>gmToolbarUI::UpdateFromPlayerDesc</c>: clear all slots, then bind
|
||||
/// each shortcut entry that has a resolved item in the repository.
|
||||
|
|
@ -320,7 +361,12 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
list.Cell.Clicked = () =>
|
||||
{
|
||||
if (list.Cell.ItemId != 0)
|
||||
_useItem(list.Cell.ItemId);
|
||||
{
|
||||
if (_itemInteraction is not null)
|
||||
_itemInteraction.ActivateItem(list.Cell.ItemId);
|
||||
else
|
||||
_useItem(list.Cell.ItemId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue