acdream/tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs
Erik 89e6b207f8 refactor(runtime): close canonical gameplay ownership
Unify the toolbar shortcut manager with Runtime inventory state, route retail-ordered shortcut and spellbook command effects through the canonical owners, and make retained controllers borrow those exact instances. Remove the item-interaction transaction fallback and add graphical/no-window parity plus failure-safe terminal ownership-ledger coverage.

Co-authored-by: Codex <codex@openai.com>
2026-07-26 09:48:51 +02:00

933 lines
34 KiB
C#

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<uint>();
var inscriptions = new List<(uint ObjectId, string Text)>();
var messages = new List<string>();
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<UiText>(
layout.FindElement(AppraisalUiController.TitleId));
Assert.Equal("Atlan Weapon", Assert.Single(title.LinesProvider()).Text);
UiText itemText = Assert.IsType<UiText>(
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<UiScrollbar>(
layout.FindElement(AppraisalUiController.ItemScrollbarId));
Assert.Same(itemText.Scroll, scrollbar.Model);
UiField inscription = Assert.IsType<UiField>(
layout.FindElement(AppraisalUiController.InscriptionTextId));
Assert.Contains("Remember the fallen", inscription.Text);
Assert.True(inscription.Editable);
UiText signature = Assert.IsType<UiText>(
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<uint, string> { [77u] = "Reed Shark Hide" },
new CreatureDisplayNameResolver(new Dictionary<uint, string>()));
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<UiText>(
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<uint>();
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<UiText>(
layout.FindElement(0x1000014Au));
UiText levelLabel = Assert.IsType<UiText>(
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<uint, string> { [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<UiViewport>(
layout.FindElement(AppraisalUiController.CreatureViewportId));
UiItemList background = Assert.Single(
host.Children.OfType<UiItemList>());
UiItemList list = Assert.Single(
creaturePanel.Children.OfType<UiItemList>(),
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<UiTemplateListSlot>(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<UiItemList>(),
candidate => candidate.Top == extraHost.Top);
Assert.Equal(3, extra.GetNumUIItems());
UiTemplateListSlot rating =
Assert.IsType<UiTemplateListSlot>(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<uint>();
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<UiField>(
layout.FindElement(AppraisalUiController.InscriptionTextId));
UiText signature = Assert.IsType<UiText>(
layout.FindElement(AppraisalUiController.SignatureTextId));
Assert.True(field.Editable);
Assert.Equal("<Inscribe here>", 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<UiField>(
layout.FindElement(AppraisalUiController.InscriptionTextId));
UiText signature = Assert.IsType<UiText>(
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("<Inscribe here>", 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<string>();
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<UiField>(
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<UiField>(
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<string>();
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<UiField>(
layout.FindElement(AppraisalUiController.InscriptionTextId));
UiText signature = Assert.IsType<UiText>(
layout.FindElement(AppraisalUiController.SignatureTextId));
Assert.False(field.Visible);
Assert.False(signature.Visible);
UiDatElement background = Assert.IsType<UiDatElement>(
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<string>();
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<UiField>(
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<uint>();
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<uint>();
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<UiTextureElement>());
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<UiDatElement>()
.Select(cell => cell.RuntimeImageTexture!.Value));
}
private static AppraisalUiController? Bind(
ImportedLayout layout,
ClientObjectTable objects,
ItemInteractionController interaction,
CombatState combat,
List<(uint ObjectId, string Text)> inscriptions,
List<string> messages,
Action show,
Action close,
CreatureAppraisalRowTemplateFactory? creatureRows = null,
CreatureDisplayNameResolver? creatureNames = null,
SelectionState? selection = null,
RetailAppraisalNameResolver? itemNames = null,
Spellbook? spellbook = null,
Func<uint, uint>? resolveSpellIcon = null,
Func<uint, uint>? resolveComponentIcon = null,
Func<uint, IReadOnlyList<SpellExamineComponent>>? spellComponents = null,
Func<MagicSchool, uint>? 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<uint> 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<UiText>(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],
};
}