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
179
tests/AcDream.App.Tests/UI/RetailUiAutomationProbeTests.cs
Normal file
179
tests/AcDream.App.Tests/UI/RetailUiAutomationProbeTests.cs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Testing;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
public sealed class RetailUiAutomationProbeTests
|
||||
{
|
||||
private sealed class SpyHandler : IItemListDragHandler
|
||||
{
|
||||
public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastDrop;
|
||||
public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastLift;
|
||||
|
||||
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload)
|
||||
=> LastLift = (sourceList, sourceCell, payload);
|
||||
|
||||
public bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||
=> true;
|
||||
|
||||
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||
=> LastDrop = (targetList, targetCell, payload);
|
||||
}
|
||||
|
||||
private static (UiRoot root, UiItemList source, UiItemList target, SpyHandler handler, ClientObjectTable objects)
|
||||
RootWithTwoItemLists()
|
||||
{
|
||||
var root = new UiRoot { Width = 240, Height = 120 };
|
||||
var objects = new ClientObjectTable();
|
||||
|
||||
var source = new UiItemList(_ => (1u, 1, 1))
|
||||
{
|
||||
DatElementId = 0x10000010u,
|
||||
Left = 10,
|
||||
Top = 10,
|
||||
Width = 32,
|
||||
Height = 32,
|
||||
};
|
||||
source.Cell.SlotIndex = 0;
|
||||
source.Cell.SourceKind = ItemDragSource.Inventory;
|
||||
source.Cell.SetItem(0x5001u, 0x99u);
|
||||
|
||||
var target = new UiItemList(_ => (1u, 1, 1))
|
||||
{
|
||||
DatElementId = 0x10000020u,
|
||||
Left = 70,
|
||||
Top = 10,
|
||||
Width = 32,
|
||||
Height = 32,
|
||||
};
|
||||
target.Cell.SlotIndex = 1;
|
||||
|
||||
var handler = new SpyHandler();
|
||||
target.RegisterDragHandler(handler);
|
||||
|
||||
root.AddChild(source);
|
||||
root.AddChild(target);
|
||||
return (root, source, target, handler, objects);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Snapshot_listsDatElementsAndItemSlots()
|
||||
{
|
||||
var (root, _, _, _, objects) = RootWithTwoItemLists();
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
|
||||
var rows = probe.Snapshot();
|
||||
|
||||
Assert.Contains(rows, r => r.DatElementId == 0x10000010u && r.TypeName == nameof(UiItemList));
|
||||
var itemRow = Assert.Single(rows, r => r.ItemId == 0x5001u);
|
||||
Assert.Equal(ItemDragSource.Inventory, itemRow.SourceKind);
|
||||
Assert.Equal(0, itemRow.SlotIndex);
|
||||
Assert.True(itemRow.Width > 0f);
|
||||
Assert.True(itemRow.Height > 0f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoubleClickItem_routesThroughUiRootDoubleClick()
|
||||
{
|
||||
var (root, source, _, _, objects) = RootWithTwoItemLists();
|
||||
bool doubleClicked = false;
|
||||
source.Cell.DoubleClicked = () => doubleClicked = true;
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
|
||||
Assert.True(probe.DoubleClickItem(0x5001u, ItemDragSource.Inventory));
|
||||
|
||||
Assert.True(doubleClicked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DragItemToElement_deliversDropReleaseToTargetCell()
|
||||
{
|
||||
var (root, _, target, handler, objects) = RootWithTwoItemLists();
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
|
||||
Assert.True(probe.DragItemToElement(0x5001u, 0x10000020u, ItemDragSource.Inventory));
|
||||
|
||||
Assert.NotNull(handler.LastDrop);
|
||||
Assert.Same(target, handler.LastDrop!.Value.list);
|
||||
Assert.Same(target.Cell, handler.LastDrop.Value.cell);
|
||||
Assert.Equal(0x5001u, handler.LastDrop.Value.payload.ObjId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DragItemOutside_raisesRootOutsideUiEvent()
|
||||
{
|
||||
var (root, _, _, _, objects) = RootWithTwoItemLists();
|
||||
object? payload = null;
|
||||
(int x, int y) release = default;
|
||||
root.DragReleasedOutsideUi += (p, x, y) =>
|
||||
{
|
||||
payload = p;
|
||||
release = (x, y);
|
||||
};
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
|
||||
Assert.True(probe.DragItemOutside(0x5001u, 220, 100, ItemDragSource.Inventory));
|
||||
|
||||
var itemPayload = Assert.IsType<ItemDragPayload>(payload);
|
||||
Assert.Equal(0x5001u, itemPayload.ObjId);
|
||||
Assert.Equal((220, 100), release);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssertItem_checksObjectTableState()
|
||||
{
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = 0x5001u,
|
||||
ContainerId = 0x7001u,
|
||||
ContainerSlot = 3,
|
||||
CurrentlyEquippedLocation = EquipMask.ChestArmor | EquipMask.UpperArmArmor,
|
||||
});
|
||||
var probe = new RetailUiAutomationProbe(new UiRoot(), objects);
|
||||
|
||||
var ok = probe.AssertItem(
|
||||
0x5001u,
|
||||
equippedLocation: EquipMask.ChestArmor | EquipMask.UpperArmArmor,
|
||||
containerId: 0x7001u,
|
||||
slot: 3);
|
||||
var bad = probe.AssertItem(0x5001u, slot: 4);
|
||||
|
||||
Assert.True(ok.Success);
|
||||
Assert.False(bad.Success);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptRunner_waitItemThenDoubleClick_executesThroughProbe()
|
||||
{
|
||||
var (root, source, _, _, objects) = RootWithTwoItemLists();
|
||||
bool doubleClicked = false;
|
||||
source.Cell.DoubleClicked = () => doubleClicked = true;
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
var logs = new List<string>();
|
||||
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ui-probe.txt");
|
||||
File.WriteAllLines(path, new[]
|
||||
{
|
||||
"wait item 0x5001 inventory 100",
|
||||
"doubleclick item 0x5001 inventory",
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var runner = new RetailUiAutomationScriptRunner(probe, path, dumpOnStart: false, logs.Add);
|
||||
|
||||
runner.Tick(0.016);
|
||||
|
||||
Assert.True(doubleClicked);
|
||||
Assert.True(runner.Completed);
|
||||
Assert.Contains(logs, line => line.Contains("UI probe script complete"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue