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_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 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) { var casting = new SpellCastingController( spellbook, () => null, () => 1u, () => { }, _ => { }, (_, _) => { }, _ => { }); return SpellcastingUiController.Bind( layout, spellbook, casting, objects, () => 1u, spellId => spellId, item => item.ObjectId, useItem, new SelectionState(), (_, _, _) => { }, (_, _) => { }); } 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"}")); } }