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
|
|
@ -18,6 +18,8 @@ public class ToolbarControllerTests
|
|||
|
||||
// The four mutually-exclusive combat-mode indicator element ids (must match ToolbarController's list).
|
||||
private static readonly uint[] CombatIds = { 0x10000192u, 0x10000193u, 0x10000194u, 0x10000195u };
|
||||
private const uint CharacterButtonId = 0x10000199u;
|
||||
private const uint InventoryButtonId = 0x100001B1u;
|
||||
|
||||
private static (ImportedLayout layout, Dictionary<uint, UiItemList> slots,
|
||||
Dictionary<uint, UiElement> indicators) FakeToolbar()
|
||||
|
|
@ -34,6 +36,8 @@ public class ToolbarControllerTests
|
|||
var e = new UiPanel { Visible = true };
|
||||
dict[id] = e; indicators[id] = e; root.AddChild(e);
|
||||
}
|
||||
AddButton(CharacterButtonId);
|
||||
AddButton(InventoryButtonId);
|
||||
return (new ImportedLayout(root, dict), slots, indicators);
|
||||
|
||||
void AddSlot(uint id)
|
||||
|
|
@ -41,6 +45,23 @@ public class ToolbarControllerTests
|
|||
var list = new UiItemList(_ => (0u, 0, 0)) { Width = 32, Height = 32 };
|
||||
dict[id] = list; slots[id] = list; root.AddChild(list);
|
||||
}
|
||||
|
||||
void AddButton(uint id)
|
||||
{
|
||||
var info = new ElementInfo
|
||||
{
|
||||
Id = id,
|
||||
Type = 1,
|
||||
Width = 32,
|
||||
Height = 32,
|
||||
DefaultStateName = "Normal",
|
||||
};
|
||||
info.StateMedia["Normal"] = (0x1u, 1);
|
||||
info.StateMedia["Highlight"] = (0x2u, 1);
|
||||
var button = new UiButton(info, _ => (0u, 0, 0)) { Width = 32, Height = 32 };
|
||||
dict[id] = button;
|
||||
root.AddChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -95,6 +116,93 @@ public class ToolbarControllerTests
|
|||
Assert.Equal(0x5001u, used);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WindowToggleButtons_clickCallbacks_fire()
|
||||
{
|
||||
var (layout, _, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
int inventoryClicks = 0;
|
||||
int characterClicks = 0;
|
||||
|
||||
var ctrl = ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||
ctrl.BindWindowToggles(
|
||||
toggleInventory: () => inventoryClicks++,
|
||||
toggleCharacter: () => characterClicks++);
|
||||
|
||||
((UiButton)layout.FindElement(InventoryButtonId)!).OnEvent(new UiEvent(0u, null, UiEventType.Click));
|
||||
((UiButton)layout.FindElement(CharacterButtonId)!).OnEvent(new UiEvent(0u, null, UiEventType.Click));
|
||||
|
||||
Assert.Equal(1, inventoryClicks);
|
||||
Assert.Equal(1, characterClicks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InventoryButton_whenTargetModeActive_targetsPlayerInsteadOfTogglingInventory()
|
||||
{
|
||||
const uint player = 0x50000001u;
|
||||
const uint pack = 0x50000010u;
|
||||
const uint kit = 0x50000A01u;
|
||||
var (layout, _, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature });
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = pack, Type = ItemType.Container, ItemsCapacity = 24 });
|
||||
repo.MoveItem(pack, player, 0);
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = kit, Type = ItemType.Misc, Useability = 0x000A0008u });
|
||||
repo.MoveItem(kit, pack, 0);
|
||||
var useWithTarget = new List<(uint Source, uint Target)>();
|
||||
var interaction = new ItemInteractionController(
|
||||
repo,
|
||||
playerGuid: () => player,
|
||||
sendUse: null,
|
||||
sendUseWithTarget: (source, target) => useWithTarget.Add((source, target)),
|
||||
sendWield: null,
|
||||
sendDrop: null,
|
||||
nowMs: () => 1_000);
|
||||
int inventoryClicks = 0;
|
||||
|
||||
var ctrl = ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u,
|
||||
useItem: _ => { },
|
||||
itemInteraction: interaction);
|
||||
ctrl.BindWindowToggles(
|
||||
toggleInventory: () => inventoryClicks++,
|
||||
toggleCharacter: () => { });
|
||||
interaction.ActivateItem(kit);
|
||||
|
||||
((UiButton)layout.FindElement(InventoryButtonId)!).OnEvent(new UiEvent(0u, null, UiEventType.Click));
|
||||
|
||||
Assert.Equal(0, inventoryClicks);
|
||||
Assert.Equal(new[] { (kit, player) }, useWithTarget);
|
||||
Assert.False(interaction.IsTargetModeActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WindowButtonState_highlightsWhenOpen()
|
||||
{
|
||||
var (layout, _, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
var ctrl = ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||
var inventoryButton = (UiButton)layout.FindElement(InventoryButtonId)!;
|
||||
var characterButton = (UiButton)layout.FindElement(CharacterButtonId)!;
|
||||
|
||||
ctrl.SetInventoryOpen(true);
|
||||
ctrl.SetCharacterOpen(true);
|
||||
|
||||
Assert.Equal("Highlight", inventoryButton.ActiveState);
|
||||
Assert.Equal("Highlight", characterButton.ActiveState);
|
||||
|
||||
ctrl.SetInventoryOpen(false);
|
||||
ctrl.SetCharacterOpen(false);
|
||||
|
||||
Assert.Equal("Normal", inventoryButton.ActiveState);
|
||||
Assert.Equal("Normal", characterButton.ActiveState);
|
||||
}
|
||||
|
||||
// ── C1: combat-mode indicator tests ─────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue