using System.Numerics; using AcDream.App.Spells; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Core.Net.Messages; using AcDream.Core.Selection; using AcDream.Core.Spells; namespace AcDream.App.Tests.UI.Layout; public sealed class AppraisalUiControllerTests { private const uint ObjectId = 0x50000001u; private static (uint, int, int) NoTexture(uint _) => (0u, 0, 0); [Fact] public void ItemResponse_UsesAuthoredItemSubviewTitleAndScrollbars() { ImportedLayout layout = FixtureLoader.LoadExamination(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Atlan Weapon", Type = ItemType.MeleeWeapon, Value = 1250, Burden = 350, ContainerId = 0x50000002u, PublicWeenieBitfield = (uint)PublicWeenieFlags.Inscribable, }); var sent = new List(); var inscriptions = new List<(uint ObjectId, string Text)>(); var messages = new List(); using var interaction = NewInteraction(objects, sent); var combat = new CombatState(); int shown = 0; int closed = 0; using AppraisalUiController controller = Bind( layout, objects, interaction, combat, inscriptions, messages, () => shown++, () => closed++)!; Assert.True(interaction.ExamineSelectedOrEnterMode(ObjectId)); var properties = new PropertyBundle(); properties.Ints[19u] = 1_250; properties.Ints[5u] = 350; properties.Strings[16u] = "A finely balanced weapon."; properties.Strings[7u] = "Remember the fallen."; properties.Strings[8u] = "Tester"; Assert.True(controller.Apply(Parsed(properties))); Assert.Equal(1, shown); Assert.Equal(AppraisalView.Item, controller.ActiveView); Assert.Equal(0, interaction.BusyCount); UiText title = Assert.IsType( layout.FindElement(AppraisalUiController.TitleId)); Assert.Equal("Atlan Weapon", Assert.Single(title.LinesProvider()).Text); UiText itemText = Assert.IsType( layout.FindElement(AppraisalUiController.ItemTextId)); Assert.Equal(VJustify.Top, itemText.VerticalJustify); Assert.Equal( [ new Vector4(1f, 1f, 1f, 1f), new Vector4(0f, 1f, 0f, 1f), new Vector4(1f, 0f, 0f, 1f), ], itemText.FontColorPalette); string report = string.Join('\n', itemText.LinesProvider().Select(line => line.Text)); Assert.Contains("Value: 1,250", report); Assert.Contains("Burden: 350", report); Assert.Contains("A finely balanced weapon.", report); UiScrollbar scrollbar = Assert.IsType( layout.FindElement(AppraisalUiController.ItemScrollbarId)); Assert.Same(itemText.Scroll, scrollbar.Model); UiField inscription = Assert.IsType( layout.FindElement(AppraisalUiController.InscriptionTextId)); Assert.Contains("Remember the fallen", inscription.Text); Assert.True(inscription.Editable); UiText signature = Assert.IsType( layout.FindElement(AppraisalUiController.SignatureTextId)); Assert.Equal("--Tester", Assert.Single(signature.LinesProvider()).Text); Assert.Empty(inscriptions); Assert.Empty(messages); ((UiButton)layout.FindElement(AppraisalUiController.CloseId)!).OnClick!.Invoke(); Assert.Equal(1, closed); } [Fact] public void ItemResponse_TitleUsesRetailMaterialDecoratedAppropriateName() { ImportedLayout layout = FixtureLoader.LoadExamination(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Steel Toed Boots", Type = ItemType.Clothing, MaterialType = 77u, }); using var interaction = NewInteraction(objects, []); var names = new RetailAppraisalNameResolver( new Dictionary { [77u] = "Reed Shark Hide" }, new CreatureDisplayNameResolver(new Dictionary())); using AppraisalUiController controller = Bind( layout, objects, interaction, new CombatState(), [], [], () => { }, () => { }, itemNames: names)!; interaction.ExamineSelectedOrEnterMode(ObjectId); Assert.True(controller.Apply(Parsed(new PropertyBundle()))); UiText title = Assert.IsType( layout.FindElement(AppraisalUiController.TitleId)); Assert.Equal( "Reed Shark Hide Steel Toed Boots", Assert.Single(title.LinesProvider()).Text); } [Fact] public void CreatureResponse_SelectsCreatureSubviewAndRefreshesInCombatWithoutBusy() { ImportedLayout layout = FixtureLoader.LoadExamination(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Drudge Slinker", Type = ItemType.Creature, }); var sent = new List(); using var interaction = NewInteraction(objects, sent); var combat = new CombatState(); int shown = 0; using AppraisalUiController controller = Bind( layout, objects, interaction, combat, [], [], () => shown++, () => { })!; interaction.ExamineSelectedOrEnterMode(ObjectId); var properties = new PropertyBundle(); properties.Ints[25u] = 12; var creature = new AppraiseInfoParser.CreatureProfile( Flags: 0, Health: 80, HealthMax: 100, Strength: null, Endurance: null, Quickness: null, Coordination: null, Focus: null, Self: null, Stamina: null, Mana: null, StaminaMax: null, ManaMax: null, AttributeHighlights: null, AttributeColors: null); Assert.True(controller.Apply(Parsed(properties, creature))); Assert.Equal(AppraisalView.Creature, controller.ActiveView); Assert.Equal(1, shown); controller.OnShown(); Assert.False(layout.FindElement(AppraisalUiController.ItemPanelId)!.Visible); Assert.True(layout.FindElement(AppraisalUiController.CreaturePanelId)!.Visible); combat.SetCombatMode(CombatMode.Melee); controller.Tick(0.74); Assert.Single(sent); controller.Tick(0.01); Assert.Equal(new[] { ObjectId, ObjectId }, sent); Assert.Equal(0, interaction.BusyCount); controller.OnHidden(); controller.Tick(0.75); Assert.Equal(new[] { ObjectId, ObjectId }, sent); Assert.True(controller.Apply(Parsed(properties, creature))); Assert.Equal(1, shown); } [Fact] public void CreatureResponse_UsesRetailHeaderAndNineOrderedTemplateRows() { ImportedLayout layout = FixtureLoader.LoadExamination(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Specter", Type = ItemType.Creature, }); using var interaction = NewInteraction(objects, []); UiText characterLabel = Assert.IsType( layout.FindElement(0x1000014Au)); UiText levelLabel = Assert.IsType( layout.FindElement(0x1000014Bu)); characterLabel.LinesProvider = () => [new UiText.Line("Character", characterLabel.DefaultColor)]; levelLabel.LinesProvider = () => [new UiText.Line("Level", levelLabel.DefaultColor)]; var templates = new CreatureAppraisalRowTemplateFactory( FixtureLoader.LoadExaminationRowTemplateInfos(), NoTexture, defaultFont: null); using AppraisalUiController controller = Bind( layout, objects, interaction, new CombatState(), [], [], () => { }, () => { }, templates, new CreatureDisplayNameResolver( new Dictionary { [77u] = "Ghost" }))!; interaction.ExamineSelectedOrEnterMode(ObjectId); var properties = new PropertyBundle(); properties.Ints[2u] = 77; properties.Ints[25u] = 80; properties.Ints[0x133u] = 35; properties.Ints[0x13Au] = 4; var profile = new AppraiseInfoParser.CreatureProfile( Flags: 0x08u, Health: 295u, HealthMax: 295u, Strength: 120u, Endurance: 190u, Quickness: 190u, Coordination: 190u, Focus: 330u, Self: 350u, Stamina: 190u, Mana: 550u, StaminaMax: 190u, ManaMax: 550u, AttributeHighlights: (ushort)0, AttributeColors: (ushort)0); Assert.True(controller.Apply(Parsed(properties, profile))); Assert.Equal( "Character", Assert.Single(characterLabel.LinesProvider()).Text); Assert.Equal("Level", Assert.Single(levelLabel.LinesProvider()).Text); Assert.Equal( "80", Assert.Single(((UiText)layout.FindElement( AppraisalUiController.CreatureLevelValueId)!) .LinesProvider()).Text); Assert.Equal( "Ghost", Assert.Single(((UiText)layout.FindElement( AppraisalUiController.CreatureDisplayNameId)!) .LinesProvider()).Text); UiElement host = layout.FindElement( AppraisalUiController.CreatureStatsListId)!; UiElement creaturePanel = layout.FindElement( AppraisalUiController.CreaturePanelId)!; UiViewport viewport = Assert.IsType( layout.FindElement(AppraisalUiController.CreatureViewportId)); UiItemList background = Assert.Single( host.Children.OfType()); UiItemList list = Assert.Single( creaturePanel.Children.OfType(), candidate => candidate.Top == host.Top); Assert.Equal(0f, background.Left); Assert.Equal(CreatureAppraisalLayeredList.TextInset, list.Left); Assert.True(host.ZOrder < viewport.ZOrder); Assert.True(viewport.ZOrder < list.ZOrder); Assert.Same(background.Scroll, list.Scroll); Assert.Equal(9, list.GetNumUIItems()); string[] labels = new string[9]; string[] values = new string[9]; for (int index = 0; index < 9; index++) { UiTemplateListSlot row = Assert.IsType(list.GetItem(index)); labels[index] = Assert.Single( ((UiText)row.Content.FindElement( CreatureAppraisalRowTemplateFactory.LabelId)!) .LinesProvider()).Text; values[index] = Assert.Single( ((UiText)row.Content.FindElement( CreatureAppraisalRowTemplateFactory.ValueId)!) .LinesProvider()).Text; } Assert.Equal( [ "Strength", "Endurance", "Coordination", "Quickness", "Focus", "Self", "Health", "Stamina", "Mana", ], labels); Assert.Equal( [ "120", "190", "190", "190", "330", "350", "295/295 (100 %)", "190/190", "550/550", ], values); UiElement extraHost = layout.FindElement( AppraisalUiController.CreatureExtraListId)!; UiItemList extra = Assert.Single( creaturePanel.Children.OfType(), candidate => candidate.Top == extraHost.Top); Assert.Equal(3, extra.GetNumUIItems()); UiTemplateListSlot rating = Assert.IsType(extra.GetItem(1)); Assert.Equal( "Dmg/CritDmg", Assert.Single(((UiText)rating.Content.FindElement( CreatureAppraisalRowTemplateFactory.LabelId)!) .LinesProvider()).Text); Assert.Equal( "Rating: 35/4", Assert.Single(((UiText)rating.Content.FindElement( CreatureAppraisalRowTemplateFactory.ValueId)!) .LinesProvider()).Text); Assert.True(extraHost.ZOrder < viewport.ZOrder); Assert.True(viewport.ZOrder < extra.ZOrder); } [Fact] public void VisibleExaminationFollowsSelectionAcrossSubviewsAndClosesOnClear() { const uint otherObjectId = 0x50000003u; ImportedLayout layout = FixtureLoader.LoadExamination(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Drudge", Type = ItemType.Creature, }); objects.AddOrUpdate(new ClientObject { ObjectId = otherObjectId, Name = "Sword", Type = ItemType.MeleeWeapon, }); var selection = new SelectionState(); var sent = new List(); int closed = 0; using var interaction = NewInteraction(objects, sent); using AppraisalUiController controller = Bind( layout, objects, interaction, new CombatState(), [], [], () => { }, () => closed++, selection: selection)!; selection.Select(ObjectId, SelectionChangeSource.World); interaction.ExamineSelectedOrEnterMode(ObjectId); Assert.True(controller.Apply(Parsed( new PropertyBundle(), new AppraiseInfoParser.CreatureProfile( Flags: 0u, Health: 1u, HealthMax: 1u, Strength: null, Endurance: null, Quickness: null, Coordination: null, Focus: null, Self: null, Stamina: null, Mana: null, StaminaMax: null, ManaMax: null, AttributeHighlights: null, AttributeColors: null)))); controller.OnShown(); selection.Select(otherObjectId, SelectionChangeSource.Inventory); Assert.Equal(new[] { ObjectId, otherObjectId }, sent); Assert.True(controller.Apply(Parsed( new PropertyBundle(), guid: otherObjectId))); Assert.Equal(AppraisalView.Item, controller.ActiveView); selection.Clear(SelectionChangeSource.World); Assert.Equal(1, closed); controller.OnHidden(); selection.Select(ObjectId, SelectionChangeSource.World); Assert.Equal(new[] { ObjectId, otherObjectId }, sent); } [Fact] public void ResponseForNeitherPendingNorCurrent_IsIgnored() { ImportedLayout layout = FixtureLoader.LoadExamination(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Item" }); using var interaction = NewInteraction(objects, []); using AppraisalUiController controller = Bind( layout, objects, interaction, new CombatState(), [], [], () => throw new InvalidOperationException("must not show"), () => { })!; Assert.False(controller.Apply(Parsed(new PropertyBundle()))); Assert.Equal(0u, controller.CurrentObjectId); } [Fact] public void EmptyOwnedInscription_FocusEditAndBlur_SendsRetailTransactionOnce() { ImportedLayout layout = FixtureLoader.LoadExamination(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Sword", Type = ItemType.MeleeWeapon, ContainerId = 0x50000002u, PublicWeenieBitfield = (uint)PublicWeenieFlags.Inscribable, }); var inscriptions = new List<(uint ObjectId, string Text)>(); using var interaction = NewInteraction(objects, []); using AppraisalUiController controller = Bind( layout, objects, interaction, new CombatState(), inscriptions, [], () => { }, () => { })!; interaction.ExamineSelectedOrEnterMode(ObjectId); Assert.True(controller.Apply(Parsed(new PropertyBundle()))); UiField field = Assert.IsType( layout.FindElement(AppraisalUiController.InscriptionTextId)); UiText signature = Assert.IsType( layout.FindElement(AppraisalUiController.SignatureTextId)); Assert.True(field.Editable); Assert.Equal("", field.Text); field.OnEvent(new UiEvent(field.EventId, field, UiEventType.FocusGained)); Assert.Equal(string.Empty, field.Text); Assert.Equal("--Tester", Assert.Single(signature.LinesProvider()).Text); foreach (char c in "For glory") field.InsertChar(c); field.OnEvent(new UiEvent(field.EventId, field, UiEventType.FocusLost)); Assert.Equal( new[] { (ObjectId, "For glory") }, inscriptions); Assert.Equal("--Tester", Assert.Single(signature.LinesProvider()).Text); field.OnEvent(new UiEvent(field.EventId, field, UiEventType.FocusGained)); field.OnEvent(new UiEvent(field.EventId, field, UiEventType.FocusLost)); Assert.Single(inscriptions); } [Fact] public void ExistingInscription_ClearOnBlur_SendsEmptyAndRestoresPlaceholder() { ImportedLayout layout = FixtureLoader.LoadExamination(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Sword", Type = ItemType.MeleeWeapon, ContainerId = 0x50000002u, PublicWeenieBitfield = (uint)PublicWeenieFlags.Inscribable, }); var inscriptions = new List<(uint ObjectId, string Text)>(); using var interaction = NewInteraction(objects, []); using AppraisalUiController controller = Bind( layout, objects, interaction, new CombatState(), inscriptions, [], () => { }, () => { })!; interaction.ExamineSelectedOrEnterMode(ObjectId); var properties = new PropertyBundle(); properties.Strings[7u] = "Old words"; properties.Strings[8u] = "Tester"; Assert.True(controller.Apply(Parsed(properties))); UiField field = Assert.IsType( layout.FindElement(AppraisalUiController.InscriptionTextId)); UiText signature = Assert.IsType( layout.FindElement(AppraisalUiController.SignatureTextId)); field.SetText(string.Empty); field.OnEvent(new UiEvent(field.EventId, field, UiEventType.FocusLost)); Assert.Equal(new[] { (ObjectId, string.Empty) }, inscriptions); Assert.Equal("", field.Text); Assert.Equal(string.Empty, Assert.Single(signature.LinesProvider()).Text); } [Fact] public void InscriptionPermissionMessages_MatchRetail() { ImportedLayout otherScribeLayout = FixtureLoader.LoadExamination(); var otherScribeObjects = new ClientObjectTable(); otherScribeObjects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Sword", ContainerId = 0x50000002u, PublicWeenieBitfield = (uint)PublicWeenieFlags.Inscribable, }); var messages = new List(); using var otherInteraction = NewInteraction(otherScribeObjects, []); using AppraisalUiController otherController = Bind( otherScribeLayout, otherScribeObjects, otherInteraction, new CombatState(), [], messages, () => { }, () => { })!; otherInteraction.ExamineSelectedOrEnterMode(ObjectId); var otherProperties = new PropertyBundle(); otherProperties.Strings[7u] = "Hands off"; otherProperties.Strings[8u] = "Other"; Assert.True(otherController.Apply(Parsed(otherProperties))); UiField otherField = Assert.IsType( otherScribeLayout.FindElement(AppraisalUiController.InscriptionTextId)); Assert.False(otherField.Editable); otherField.OnEvent(new UiEvent( otherField.EventId, otherField, UiEventType.Click)); Assert.Equal("Only Other can change the inscription", Assert.Single(messages)); ImportedLayout unownedLayout = FixtureLoader.LoadExamination(); var unownedObjects = new ClientObjectTable(); unownedObjects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Sword", ContainerId = 0x60000000u, PublicWeenieBitfield = (uint)PublicWeenieFlags.Inscribable, }); messages.Clear(); using var unownedInteraction = NewInteraction(unownedObjects, []); using AppraisalUiController unownedController = Bind( unownedLayout, unownedObjects, unownedInteraction, new CombatState(), [], messages, () => { }, () => { })!; unownedInteraction.ExamineSelectedOrEnterMode(ObjectId); Assert.True(unownedController.Apply(Parsed(new PropertyBundle()))); UiField unownedField = Assert.IsType( unownedLayout.FindElement(AppraisalUiController.InscriptionTextId)); unownedField.OnEvent(new UiEvent( unownedField.EventId, unownedField, UiEventType.Click)); Assert.Equal( "Item must be in your inventory to inscribe.", Assert.Single(messages)); } [Fact] public void NonInscribableItem_HidesEditorAndBackgroundReportsRetailMessage() { ImportedLayout layout = FixtureLoader.LoadExamination(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Component", ContainerId = 0x50000002u, }); var messages = new List(); using var interaction = NewInteraction(objects, []); using AppraisalUiController controller = Bind( layout, objects, interaction, new CombatState(), [], messages, () => { }, () => { })!; interaction.ExamineSelectedOrEnterMode(ObjectId); Assert.True(controller.Apply(Parsed(new PropertyBundle()))); UiField field = Assert.IsType( layout.FindElement(AppraisalUiController.InscriptionTextId)); UiText signature = Assert.IsType( layout.FindElement(AppraisalUiController.SignatureTextId)); Assert.False(field.Visible); Assert.False(signature.Visible); UiDatElement background = Assert.IsType( layout.FindElement(AppraisalUiController.InscriptionBackgroundId)); background.OnClick!.Invoke(); Assert.Equal("This item is not inscribable.", Assert.Single(messages)); } [Fact] public void HookProfileControlsVisibilityButPublicFlagStillControlsEditing() { ImportedLayout layout = FixtureLoader.LoadExamination(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Hooked Decoration", ContainerId = 0x50000002u, HookItemTypes = (uint)ItemType.Misc, HookType = 1u, }); var messages = new List(); using var interaction = NewInteraction(objects, []); using AppraisalUiController controller = Bind( layout, objects, interaction, new CombatState(), [], messages, () => { }, () => { })!; interaction.ExamineSelectedOrEnterMode(ObjectId); AppraiseInfoParser.Parsed appraisal = Parsed(new PropertyBundle()) with { Flags = AppraiseInfoParser.IdentifyResponseFlags.HookProfile, HookProfile = new AppraiseInfoParser.HookProfile( Flags: 1u, ValidLocations: 0u, AmmoType: 0u), }; Assert.True(controller.Apply(appraisal)); UiField field = Assert.IsType( layout.FindElement(AppraisalUiController.InscriptionTextId)); Assert.True(field.Visible); Assert.False(field.Editable); field.OnEvent(new UiEvent(field.EventId, field, UiEventType.Click)); Assert.Equal("This item is not inscribable.", Assert.Single(messages)); } [Fact] public void Examination_IsAnIndependentFloatyWindow_NotSharedMainPanelContent() { Assert.False( RetailPanelCatalog.TryGetPanelId(WindowNames.Examination, out _)); Assert.DoesNotContain( RetailPanelCatalog.MountedPanels, mounted => mounted.WindowName == WindowNames.Examination); } [Fact] public void SpellExamination_UsesAuthoredLocalSubviewWithoutSelectingOrAppraisingSpellId() { ImportedLayout layout = FixtureLoader.LoadExamination(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Selected Drudge", Type = ItemType.Creature, }); var sent = new List(); using var interaction = NewInteraction(objects, sent); var selection = new SelectionState(); selection.Select(ObjectId, SelectionChangeSource.World); SpellMetadata metadata = ExaminedSpell(); var spellbook = new Spellbook(SpellTable.Create([metadata])); var components = new[] { new SpellExamineComponent( 10u, new SpellComponentDescriptor(100u, "Lead Scarab", 0u, 0x06000010u), Owned: true), new SpellExamineComponent( 11u, new SpellComponentDescriptor(101u, "Malar Herb", 1u, 0x06000011u), Owned: false), }; var componentTemplate = new SpellExamineComponentTemplateFactory( new ElementInfo { Id = SpellExamineComponentTemplateFactory.TemplateId, Type = 3, Width = 20, Height = 20, }, NoTexture, defaultFont: null); var resolvedComponentIcons = new List(); int shown = 0; using AppraisalUiController controller = Bind( layout, objects, interaction, new CombatState(), [], [], () => shown++, () => { }, selection: selection, spellbook: spellbook, resolveSpellIcon: id => id + 1_000u, resolveComponentIcon: did => { resolvedComponentIcons.Add(did); return did + 2_000u; }, spellComponents: _ => components, magicSkill: _ => 200u, spellComponentTemplates: componentTemplate)!; Assert.True(interaction.ExamineSelectedOrEnterMode(ObjectId)); Assert.Equal(1, interaction.BusyCount); Assert.True(controller.ExamineSpell(metadata.SpellId)); Assert.Equal(AppraisalView.Spell, controller.ActiveView); Assert.Equal(1, shown); Assert.Equal(ObjectId, selection.SelectedObjectId); Assert.Equal(new uint[] { ObjectId, 0u }, sent); Assert.Equal(0, interaction.BusyCount); Assert.Equal(0u, controller.CurrentObjectId); Assert.True(layout.FindElement(AppraisalUiController.SpellPanelId)!.Visible); Assert.False(layout.FindElement(AppraisalUiController.ItemPanelId)!.Visible); Assert.False(layout.FindElement(AppraisalUiController.CreaturePanelId)!.Visible); AssertSpellText( layout, AppraisalUiController.TitleId, "Incantation of Test"); AssertSpellText( layout, AppraisalUiController.SpellSchoolTextId, "School: War Magic"); AssertSpellText( layout, AppraisalUiController.SpellManaTextId, "Mana: 50 + 14 per target"); AssertSpellText( layout, AppraisalUiController.SpellDurationTextId, "Duration: 1 min."); AssertSpellText( layout, AppraisalUiController.SpellRangeTextId, "Range: 82.0 yds."); string display = string.Join( '\n', ((UiText)layout.FindElement( AppraisalUiController.SpellDisplayTextId)!) .LinesProvider() .Select(line => line.Text)); Assert.Contains("A projected retail spell.", display); Assert.Contains("COMPONENTS:", display); Assert.Contains("Lead Scarab", display); Assert.Contains("Malar Herb", display); UiElement iconHost = layout.FindElement( AppraisalUiController.SpellIconId)!; UiTextureElement icon = Assert.Single( iconHost.Children.OfType()); Assert.Equal(metadata.SpellId + 1_000u, icon.Texture); Assert.Equal( new uint[] { 0x06000010u, 0x06000011u }, resolvedComponentIcons); UiElement formula = layout.FindElement( AppraisalUiController.SpellFormulaListId)!; Assert.Equal( new uint[] { 0x06000010u + 2_000u, 0x06000011u + 2_000u }, formula.Children .OfType() .Select(cell => cell.RuntimeImageTexture!.Value)); } private static AppraisalUiController? Bind( ImportedLayout layout, ClientObjectTable objects, ItemInteractionController interaction, CombatState combat, List<(uint ObjectId, string Text)> inscriptions, List messages, Action show, Action close, CreatureAppraisalRowTemplateFactory? creatureRows = null, CreatureDisplayNameResolver? creatureNames = null, SelectionState? selection = null, RetailAppraisalNameResolver? itemNames = null, Spellbook? spellbook = null, Func? resolveSpellIcon = null, Func? resolveComponentIcon = null, Func>? spellComponents = null, Func? magicSkill = null, SpellExamineComponentTemplateFactory? spellComponentTemplates = null) => AppraisalUiController.Bind( layout, objects, interaction, selection ?? new SelectionState(), combat, spellbook ?? new Spellbook(), () => "Tester", (objectId, text) => inscriptions.Add((objectId, text)), messages.Add, show, close, creatureRows, creatureNames, itemNames, resolveSpellIcon, resolveComponentIcon, spellComponents, magicSkill, spellComponentTemplates); private static ItemInteractionController NewInteraction( ClientObjectTable objects, List sent) => new( objects, new InventoryTransactionState(objects), playerGuid: () => 0x50000002u, sendUse: null, sendUseWithTarget: null, sendWield: null, sendDrop: null, sendExamine: sent.Add); private static AppraiseInfoParser.Parsed Parsed( PropertyBundle properties, AppraiseInfoParser.CreatureProfile? creature = null, uint guid = ObjectId) => new( Guid: guid, Flags: creature is null ? AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable : AppraiseInfoParser.IdentifyResponseFlags.CreatureProfile, Success: true, Properties: properties, SpellBook: [], ArmorProfile: null, CreatureProfile: creature, WeaponProfile: null, HookProfile: null, ArmorLevels: null, ArmorEnchantments: null, WeaponEnchantments: null, ResistEnchantments: null); private static void AssertSpellText( ImportedLayout layout, uint elementId, string expected) { UiText text = Assert.IsType(layout.FindElement(elementId)); Assert.Equal( expected, string.Join('\n', text.LinesProvider().Select(line => line.Text))); } private static SpellMetadata ExaminedSpell() => new( SpellId: 42u, Name: "Incantation of Test", School: "War Magic", Family: 1u, IconId: 0x06001234u, SpellWords: "Malar Aether", Duration: 90f, ManaCost: 50, IsDebuff: false, IsFellowship: false, Description: "A projected retail spell.", SortKey: 0, Difficulty: 0, Flags: 0u, Generation: 6, IsFastWindup: false, IsOffensive: true, IsUntargeted: false, Speed: 0f, CasterEffect: 0u, TargetEffect: 0u, TargetMask: 0u, SpellType: 0) { SchoolId = MagicSchool.WarMagic, BaseRangeConstant = 40f, BaseRangeModifier = 0.25f, ManaModifier = 14u, FormulaComponents = [10u, 11u], }; }