acdream/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs
Erik b7dc91a053 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>
2026-07-03 09:18:43 +02:00

182 lines
6 KiB
C#

using AcDream.App.UI;
namespace AcDream.App.Tests.UI;
public class UiItemSlotTests
{
[Fact]
public void IsLeafWidget()
=> Assert.True(new UiItemSlot().ConsumesDatChildren);
[Fact]
public void DefaultEmptySprite_isToolbarBorder()
=> Assert.Equal(0x060074CFu, new UiItemSlot().EmptySprite);
[Fact]
public void Empty_whenNoItem()
{
var s = new UiItemSlot();
Assert.Equal(0u, s.ItemId);
Assert.Equal(0u, s.IconTexture);
}
[Fact]
public void SetItem_setsIdAndTexture()
{
var s = new UiItemSlot();
s.SetItem(0x5001u, 0x99u);
Assert.Equal(0x5001u, s.ItemId);
Assert.Equal(0x99u, s.IconTexture);
}
[Fact]
public void Clear_afterSetItem_resetsToEmpty()
{
var s = new UiItemSlot();
s.SetItem(0x5001u, 0x99u);
s.Clear();
Assert.Equal(0u, s.ItemId);
Assert.Equal(0u, s.IconTexture);
}
[Fact]
public void DoubleClick_invokesDoubleClicked()
{
var s = new UiItemSlot();
bool fired = false;
s.DoubleClicked = () => fired = true;
s.OnEvent(new UiEvent(0u, s, UiEventType.DoubleClick));
Assert.True(fired);
}
// ── Shortcut number tests ────────────────────────────────────────────────
// Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
[Fact]
public void ShortcutNum_defaultIsMinusOne()
{
var s = new UiItemSlot();
Assert.Equal(-1, s.ShortcutNum);
}
[Fact]
public void ShortcutPeace_defaultIsTrue()
{
var s = new UiItemSlot();
Assert.True(s.ShortcutPeace);
}
[Fact]
public void SetShortcutNum_setsIndexAndPeace()
{
var s = new UiItemSlot();
s.SetShortcutNum(3, peace: false);
Assert.Equal(3, s.ShortcutNum);
Assert.False(s.ShortcutPeace);
}
[Fact]
public void SetShortcutNum_peaceTrue()
{
var s = new UiItemSlot();
s.SetShortcutNum(0, peace: true);
Assert.Equal(0, s.ShortcutNum);
Assert.True(s.ShortcutPeace);
}
[Fact]
public void ClearShortcutNum_setsMinusOne()
{
var s = new UiItemSlot();
s.SetShortcutNum(5, peace: true);
s.ClearShortcutNum();
Assert.Equal(-1, s.ShortcutNum);
}
// ── ActiveDigitArray occupancy gating (decomp UIElement_UIItem::SetShortcutNum:229481) ──
private static readonly uint[] Peace = { 0x10u, 0x11u, 0x12u };
private static readonly uint[] War = { 0x20u, 0x21u, 0x22u };
private static readonly uint[] Empty = { 0x30u, 0x31u, 0x32u };
/// <summary>
/// When ItemId == 0 (empty slot), ActiveDigitArray returns EmptyDigits regardless
/// of ShortcutPeace. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) —
/// else branch when m_elem_Icon->m_state == 0x1000001c (empty).
/// </summary>
[Fact]
public void ActiveDigitArray_emptySlot_returnsEmptyDigits()
{
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
s.SetShortcutNum(0, peace: true);
// ItemId == 0 → EmptyDigits
Assert.Same(Empty, s.ActiveDigitArray());
}
[Fact]
public void ActiveDigitArray_emptySlot_warStance_stillReturnsEmptyDigits()
{
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
s.SetShortcutNum(0, peace: false);
// ItemId == 0 → EmptyDigits regardless of stance
Assert.Same(Empty, s.ActiveDigitArray());
}
/// <summary>
/// When ItemId != 0 (occupied), ActiveDigitArray returns PeaceDigits or WarDigits
/// depending on ShortcutPeace. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481/229493).
/// </summary>
[Fact]
public void ActiveDigitArray_occupiedSlot_peaceStance_returnsPeaceDigits()
{
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
s.SetItem(0x5001u, 0x99u);
s.SetShortcutNum(0, peace: true);
Assert.Same(Peace, s.ActiveDigitArray());
}
[Fact]
public void ActiveDigitArray_occupiedSlot_warStance_returnsWarDigits()
{
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
s.SetItem(0x5001u, 0x99u);
s.SetShortcutNum(0, peace: false);
Assert.Same(War, s.ActiveDigitArray());
}
[Fact]
public void ActiveDigitArray_emptySlot_nullEmptyDigits_returnsNull()
{
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = null };
s.SetShortcutNum(0, peace: true);
Assert.Null(s.ActiveDigitArray());
}
// ── Container-switching indicator overlays (decomp SetOpenContainerState 0x004e1200 /
// SetSelectedState 0x004e1240) ───────────────────────────────────────────────────────
[Fact]
public void Selected_defaultsFalse() => Assert.False(new UiItemSlot().Selected);
[Fact]
public void IsOpenContainer_defaultsFalse() => Assert.False(new UiItemSlot().IsOpenContainer);
[Fact]
public void SelectedSprite_default_isGreenYellowSquare()
=> Assert.Equal(0x06004D21u, new UiItemSlot().SelectedSprite);
[Fact]
public void OpenContainerSprite_default_isTriangle()
=> Assert.Equal(0x06005D9Cu, new UiItemSlot().OpenContainerSprite);
// ── Container capacity bar (decomp UpdateCapacityDisplay 0x004e16e0, element 0x10000347) ──
[Fact]
public void CapacityFill_defaultsHidden() => Assert.Equal(-1f, new UiItemSlot().CapacityFill);
[Fact]
public void CapacityBackSprite_default() => Assert.Equal(0x06004D22u, new UiItemSlot().CapacityBackSprite);
[Fact]
public void CapacityFrontSprite_default() => Assert.Equal(0x06004D23u, new UiItemSlot().CapacityFrontSprite);
}