using AcDream.App.Spells; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; using AcDream.Core.Selection; using AcDream.Core.Spells; namespace AcDream.App.Tests.UI.Layout; public sealed class SpellcastingUiControllerTests { private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); [Fact] public void ImportedFixture_UsesRetailEmptyCells_ShortcutDigits_AndCompleteCastButton() { ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadCombatInfos(), NoTex, datFont: null); ApplyAnchors(layout.Root); var spellbook = new Spellbook(); spellbook.OnSpellLearned(42u, 1f); spellbook.SetFavorite(0, 0, 42u); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" }); uint[] regular = [101u, 102u, 103u, 104u, 105u, 106u, 107u, 108u, 109u]; uint[] ghosted = [201u, 202u, 203u, 204u, 205u, 206u, 207u, 208u, 209u]; uint[] empty = [301u, 302u, 303u, 304u, 305u, 306u, 307u, 308u, 309u]; var graphics = new UiShortcutSlotGraphics(regular, ghosted, empty, 0x060074CFu); using SpellcastingUiController controller = Bind( layout, spellbook, objects, _ => { }, shortcutGraphics: graphics)!; UiElement group = layout.FindElement(0x100000AAu)!; UiItemList list = Descendants(group).OfType().First(); Assert.True(list.SingleRow); Assert.True(list.FillVisibleEmptySlots); Assert.Equal(439f, list.Width); Assert.Equal(13, list.GetNumUIItems()); UiCatalogSlot favorite = Assert.IsType(list.GetItem(0)); Assert.Equal(42u, favorite.EntryId); Assert.Equal(0, favorite.ShortcutNum); Assert.Same(regular, favorite.ActiveDigitArray()); for (int i = 1; i < 9; i++) { UiCatalogSlot slot = Assert.IsType(list.GetItem(i)); Assert.True(slot.IsEmptySlot); Assert.Equal(i, slot.ShortcutNum); Assert.Same(empty, slot.ActiveDigitArray()); } for (int i = 9; i < 13; i++) Assert.Equal(-1, list.GetItem(i)!.ShortcutNum); var cast = Assert.IsType( layout.FindElement(SpellcastingUiController.CastButtonId)); Assert.Equal((525f, 75f), (cast.Left, cast.Width)); Assert.Equal(3, cast.FaceSegmentCount); Assert.Equal( [ new UiPixelRect(0, 0, 31, 31), new UiPixelRect(32, 0, 42, 31), new UiPixelRect(43, 0, 74, 31), ], cast.FaceSegmentRectsForTest()); } [Fact] public void ImportedFixture_BindsAllTabs_AndTracksEquippedEndowment() { ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadCombatInfos(), NoTex, datFont: null); var spellbook = new Spellbook(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" }); var used = new List(); using SpellcastingUiController? controller = Bind( layout, spellbook, objects, used.Add); Assert.True(controller is not null, DescribeBinding(layout)); objects.AddOrUpdate(new ClientObject { ObjectId = 2u, Name = "Orb", Type = ItemType.Caster, WielderId = 1u, CurrentlyEquippedLocation = EquipMask.Held, SpellId = 2670u, IconId = 0x06001234u, }); controller.Tick(); UiElement host = Assert.IsAssignableFrom( layout.FindElement(SpellcastingUiController.EndowmentId)); Assert.True(host.Visible); UiCatalogSlot slot = Assert.IsType(host.Children[^1]); Assert.Equal(2u, slot.EntryId); Assert.Equal(2670u, slot.CatalogIconTexture); Assert.Equal(2u, slot.CatalogOverlayTexture); slot.OnEvent(new UiEvent(0, slot, UiEventType.Click)); var cast = Assert.IsType( layout.FindElement(SpellcastingUiController.CastButtonId)); cast.OnEvent(new UiEvent(0, cast, UiEventType.Click)); Assert.Equal([2u], used); } [Fact] public void FavoriteDrop_IgnoresForeignInventoryPayload() { ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadCombatInfos(), NoTex, datFont: null); var spellbook = new Spellbook(); spellbook.OnSpellLearned(42u, 1f); spellbook.SetFavorite(0, 0, 42u); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" }); using SpellcastingUiController? controller = Bind( layout, spellbook, objects, _ => { }); Assert.True(controller is not null, DescribeBinding(layout)); controller.Tick(); UiElement group = Assert.IsAssignableFrom(layout.FindElement(0x100000AAu)); UiItemList list = Descendants(group).OfType().First(); UiCatalogSlot slot = Assert.IsType(list.GetItem(0)); var foreign = new ItemDragPayload(9u, ItemDragSource.Inventory, 0, new UiItemSlot(), null); bool handled = slot.OnEvent(new UiEvent( 0, slot, UiEventType.DropReleased, Payload: foreign)); Assert.True(handled); Assert.Equal([42u], spellbook.GetFavorites(0)); } [Fact] public void SpellbookShortcutDrop_AddsToOpenTabWithoutRemovingLearnedSpell() { ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadCombatInfos(), NoTex, datFont: null); var spellbook = new Spellbook(); spellbook.OnSpellLearned(42u, 1f); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" }); var added = new List<(int Tab, int Position, uint Spell)>(); using SpellcastingUiController controller = Bind( layout, spellbook, objects, _ => { }, (tab, position, spell) => added.Add((tab, position, spell)))!; UiElement group = Assert.IsAssignableFrom(layout.FindElement(0x100000AAu)); UiItemList list = Descendants(group).OfType().First(); bool handled = list.OnEvent(new UiEvent( 0, list, UiEventType.DropReleased, Data1: 5, Payload: new SpellbookShortcutDragPayload(42u))); Assert.True(handled); Assert.Equal([(0, 0, 42u)], added); Assert.Equal([42u], spellbook.GetFavorites(0)); Assert.True(spellbook.Knows(42u)); } [Fact] public void UnrelatedObjectUpdate_DoesNotRecreateFavoriteSlots() { ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadCombatInfos(), NoTex, datFont: null); var spellbook = new Spellbook(); spellbook.OnSpellLearned(42u, 1f); spellbook.SetFavorite(0, 0, 42u); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" }); using SpellcastingUiController controller = Bind( layout, spellbook, objects, _ => { })!; UiElement group = layout.FindElement(0x100000AAu)!; UiItemList list = Descendants(group).OfType().First(); UiItemSlot before = list.GetItem(0)!; objects.AddOrUpdate(new ClientObject { ObjectId = 99u, Name = "Rock" }); controller.Tick(); Assert.Same(before, list.GetItem(0)); } [Fact] public void ObjectTableClear_RemovesEquippedEndowmentAndCannotUseStaleGuid() { ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadCombatInfos(), NoTex, datFont: null); var spellbook = new Spellbook(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" }); objects.AddOrUpdate(new ClientObject { ObjectId = 2u, Name = "Orb", Type = ItemType.Caster, WielderId = 1u, CurrentlyEquippedLocation = EquipMask.Held, SpellId = 2670u, }); var used = new List(); using SpellcastingUiController controller = Bind( layout, spellbook, objects, used.Add)!; UiElement host = layout.FindElement(SpellcastingUiController.EndowmentId)!; UiCatalogSlot slot = Assert.IsType(host.Children[^1]); slot.OnEvent(new UiEvent(0, slot, UiEventType.Click)); objects.Clear(); controller.Tick(); Assert.False(host.Visible); Assert.Equal(0u, slot.EntryId); slot.OnEvent(new UiEvent(0, slot, UiEventType.DoubleClick)); Assert.Empty(used); } private static SpellcastingUiController? Bind( ImportedLayout layout, Spellbook spellbook, ClientObjectTable objects, Action useItem, Action? addFavorite = null, UiShortcutSlotGraphics? shortcutGraphics = null) { var casting = new SpellCastingController( spellbook, () => null, () => 1u, () => { }, _ => { }, (_, _) => { }, _ => { }); return SpellcastingUiController.Bind( layout, spellbook, casting, objects, () => 1u, spellId => spellId, item => item.ObjectId, useItem, new SelectionState(), addFavorite ?? ((_, _, _) => { }), (_, _) => { }, shortcutGraphics); } private static void ApplyAnchors(UiElement parent) { foreach (UiElement child in parent.Children) { child.ApplyAnchor(parent.Width, parent.Height); ApplyAnchors(child); } } private static IEnumerable Descendants(UiElement root) { foreach (UiElement child in root.Children) { yield return child; foreach (UiElement nested in Descendants(child)) yield return nested; } } private static string DescribeBinding(ImportedLayout layout) { uint[] ids = [ SpellcastingUiController.CastButtonId, SpellcastingUiController.EndowmentId, 0x100000A3u, 0x100000AAu, 0x100005C2u, 0x100005C3u, ]; return string.Join(", ", ids.Select(id => $"{id:X8}={layout.FindElement(id)?.GetType().Name ?? "missing"}")); } }