using System.Numerics; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; using AcDream.Core.Net.Messages; using AcDream.Core.Spells; namespace AcDream.App.Tests.UI.Layout; public sealed class ItemAppraisalTextFormatterTests { [Fact] public void WeaponAndMagic_AreProjectedInRetailOrderWithDatDescriptions() { var obj = new ClientObject { ObjectId = 0x50000001u, Name = "Flaming Sword", Type = ItemType.MeleeWeapon, ValidLocations = EquipMask.MeleeWeapon, Value = 12_500, Burden = 450, Workmanship = 8.4f, }; var properties = new PropertyBundle(); properties.Ints[353u] = 2; properties.Ints[19u] = 12_500; properties.Ints[5u] = 450; properties.Ints[204u] = 4; properties.Ints[106u] = 300; properties.Ints[107u] = 250; properties.Ints[108u] = 500; properties.Ints[117u] = 20; properties.Floats[29u] = 1.05d; properties.Strings[14u] = "Use this item to attack."; properties.Strings[16u] = "A finely balanced enchanted blade."; SpellMetadata flame = Spell(101u, "Flame Bolt VI", "Hurls a bolt of flame."); SpellMetadata armor = Spell(102u, "Impenetrability VI", "Increases the target's armor."); AppraiseInfoParser.Parsed appraisal = Parsed( properties, spells: [101u, 0x8000_0066u], weapon: new AppraiseInfoParser.WeaponProfile( DamageType: 1u, WeaponTime: 30u, WeaponSkill: 44u, Damage: 40u, DamageVariance: 0.25d, DamageMod: 1d, WeaponLength: 1d, MaxVelocity: 0d, WeaponOffense: 1.15d, MaxVelocityEstimated: 0u)); string report = ItemAppraisalTextFormatter.Build( obj, appraisal, id => id switch { 101u => flame, 102u => armor, _ => null, }); Assert.Contains("Value: 12,500", report); Assert.Contains("Workmanship: Utterly flawless (8.4)", report); Assert.Contains("Skill: Heavy Weapons (Sword)", report); Assert.Contains("Damage: 30 - 40, Slashing", report); Assert.Contains("Elemental Damage Bonus: 4, Slashing.", report); Assert.Contains("Speed: Fast (30)", report); Assert.Contains("Bonus to Attack Skill: +15%.", report); Assert.Contains("Bonus to Melee Defense: +5%.", report); Assert.Contains("Spells: Flame Bolt VI, Impenetrability VI", report); Assert.Contains("Spellcraft: 300.", report); Assert.Contains("Mana: 250 / 500.", report); Assert.Contains("Mana Cost: 20.", report); Assert.Contains("Spell Descriptions:", report); Assert.Contains("Flame Bolt VI\nHurls a bolt of flame.", report); Assert.Contains("Enchantments:", report); Assert.Contains("Impenetrability VI\nIncreases the target's armor.", report); Assert.True( report.IndexOf("Skill:", StringComparison.Ordinal) < report.IndexOf("Spells:", StringComparison.Ordinal)); Assert.True( report.IndexOf("Spells:", StringComparison.Ordinal) < report.IndexOf("Spell Descriptions:", StringComparison.Ordinal)); Assert.EndsWith("A finely balanced enchanted blade.", report); } [Fact] public void NpcLooksLikeObjectCreature_UsesRetailUnknownsAndSuppressesNegativeCapacity() { var obj = new ClientObject { ObjectId = 0x50000005u, Name = "Black Phyntos Hive", Type = ItemType.Creature, ItemsCapacity = -1, ContainersCapacity = -1, }; var properties = new PropertyBundle(); properties.Strings[16u] = "A hollowed out tree trunk that has a Phyntos Wasp Hive in it."; ItemAppraisalReport report = ItemAppraisalTextFormatter.BuildReport( obj, Parsed(properties), _ => null); Assert.Equal( "Value: ???\nBurden: Unknown\n\n" + "A hollowed out tree trunk that has a Phyntos Wasp Hive in it.", report.ToString()); Assert.DoesNotContain("Can hold", report.ToString()); Assert.Collection( report.Fragments, value => { Assert.Equal(ItemAppraisalSeparator.None, value.Separator); Assert.Equal("Value: ???", value.Text); }, burden => { Assert.Equal(ItemAppraisalSeparator.Line, burden.Separator); Assert.Equal("Burden: Unknown", burden.Text); }, description => { Assert.Equal(ItemAppraisalSeparator.Paragraph, description.Separator); Assert.Equal(properties.Strings[16u], description.Text); }); } [Fact] public void HookWithHookProfile_SuppressesHooksOwnCapacity() { var obj = new ClientObject { ObjectId = 0x50000015u, Name = "Weapon Hook", Type = ItemType.Container, ItemsCapacity = 24, ContainersCapacity = 1, HookItemTypes = (uint)ItemType.Weapon, HookType = 4u, }; ItemAppraisalReport report = ItemAppraisalTextFormatter.BuildReport( obj, Parsed( new PropertyBundle(), hook: new AppraiseInfoParser.HookProfile( Flags: 0u, ValidLocations: 0u, AmmoType: 0u)), _ => null); Assert.DoesNotContain("Can hold", report.ToString()); } [Fact] public void OrdinaryContainer_PresentsCapacityAsRetailParagraph() { var obj = new ClientObject { ObjectId = 0x50000006u, Name = "Backpack", Type = ItemType.Container, ItemsCapacity = 24, ContainersCapacity = 1, }; string report = ItemAppraisalTextFormatter.Build( obj, Parsed(new PropertyBundle()), _ => null); Assert.Equal( "Value: ???\nBurden: Unknown\n\n" + "Can hold up to 24 items and 1 containers.", report); } [Fact] public void BookCapacity_UsesRetailUsedThenTotalPropertyOrder() { var obj = new ClientObject { ObjectId = 0x5000000Bu, Name = "Book", Type = ItemType.Writable, }; var properties = new PropertyBundle(); properties.Ints[174u] = 3; properties.Ints[175u] = 10; string report = ItemAppraisalTextFormatter.Build( obj, Parsed(properties), _ => null); Assert.EndsWith("3 of 10 pages full.", report); } [Theory] [InlineData((ushort)0x0000, ItemAppraisalFontStyle.Detrimental)] [InlineData((ushort)0x0008, ItemAppraisalFontStyle.Beneficial)] public void WeaponDamage_UsesRetailEnchantmentFontSelection( ushort highHalf, ItemAppraisalFontStyle expected) { var obj = new ClientObject { ObjectId = 0x50000007u, Name = "Sword", Type = ItemType.MeleeWeapon, ValidLocations = EquipMask.MeleeWeapon, }; AppraiseInfoParser.Parsed parsed = Parsed( new PropertyBundle(), weapon: new AppraiseInfoParser.WeaponProfile( DamageType: 1u, WeaponTime: 30u, WeaponSkill: 44u, Damage: 40u, DamageVariance: 0d, DamageMod: 1d, WeaponLength: 1d, MaxVelocity: 0d, WeaponOffense: 1d, MaxVelocityEstimated: 0u), weaponEnchantments: ((ushort)0x0008, highHalf)); ItemAppraisalReport report = ItemAppraisalTextFormatter.BuildReport( obj, parsed, _ => null); ItemAppraisalFragment damage = Assert.Single( report.Fragments, fragment => fragment.Text.StartsWith( "Damage:", StringComparison.Ordinal)); Assert.Equal(expected, damage.Style); } [Fact] public void TextLayout_PreservesRetailPaletteAcrossParagraphs() { Vector4 normal = Vector4.One; var beneficial = new Vector4(0f, 1f, 0f, 1f); var detrimental = new Vector4(1f, 0f, 0f, 1f); var text = new UiText { Width = 1_000f, FontColorPalette = [normal, beneficial, detrimental], }; var report = new ItemAppraisalReport( [ new ItemAppraisalFragment( "Normal", ItemAppraisalSeparator.None, ItemAppraisalFontStyle.Normal), new ItemAppraisalFragment( "High", ItemAppraisalSeparator.Line, ItemAppraisalFontStyle.Beneficial), new ItemAppraisalFragment( "Low", ItemAppraisalSeparator.Paragraph, ItemAppraisalFontStyle.Detrimental), ]); IReadOnlyList lines = ItemAppraisalTextLayout.Shape( text, report); Assert.Equal(["Normal", "High", "", "Low"], lines.Select(line => line.Text)); Assert.Equal(normal, lines[0].Color); Assert.Equal(beneficial, lines[1].Color); Assert.Equal(detrimental, lines[3].Color); } [Fact] public void LockInformation_UsesRetailPresenceRulesWordingAndParagraphs() { var obj = new ClientObject { ObjectId = 0x50000009u, Name = "Locked Chest", Type = ItemType.Container, }; var properties = new PropertyBundle(); properties.Bools[3u] = true; properties.Ints[38u] = 212; properties.Ints[173u] = 42; ItemAppraisalReport report = ItemAppraisalTextFormatter.BuildReport( obj, Parsed(properties), _ => null); Assert.Contains( report.Fragments, fragment => fragment.Text == "Locked" && fragment.Separator == ItemAppraisalSeparator.Paragraph); Assert.Contains( report.Fragments, fragment => fragment.Text == "The lock looks difficult to pick (Resistance 212)." && fragment.Separator == ItemAppraisalSeparator.Paragraph); } [Fact] public void MissingLockedFlag_ReportsLockpickBonusInsteadOfInventingLockState() { var obj = new ClientObject { ObjectId = 0x5000000Au, Name = "Lockpick", Type = ItemType.Misc, }; var properties = new PropertyBundle(); properties.Ints[38u] = -7; string report = ItemAppraisalTextFormatter.Build( obj, Parsed(properties), _ => null); Assert.Contains("Bonus to Lockpick Skill: -7", report); Assert.DoesNotContain("Locked", report); Assert.DoesNotContain("Unlocked", report); } [Fact] public void ArmorRequirementsAndUses_UseRetailLabelsAndResistanceBands() { var obj = new ClientObject { ObjectId = 0x50000002u, Name = "Platemail Hauberk", Type = ItemType.Armor, ValidLocations = EquipMask.ChestArmor, Structure = 8, MaxStructure = 10, }; var properties = new PropertyBundle(); properties.Ints[28u] = 200; properties.Ints[158u] = 1; properties.Ints[159u] = 6; properties.Ints[160u] = 250; properties.Ints[324u] = 4; properties.Ints[369u] = 50; properties.Ints[370u] = 3; properties.Bools[69u] = false; properties.Strings[25u] = "Tester"; AppraiseInfoParser.Parsed appraisal = Parsed( properties, armor: new AppraiseInfoParser.ArmorProfile( SlashingProtection: 1.2f, PiercingProtection: 0.8f, BludgeoningProtection: 0.4f, ColdProtection: 0f, FireProtection: 1.6f, AcidProtection: 2f, NetherProtection: 1f, LightningProtection: 0.2f)); string report = ItemAppraisalTextFormatter.Build( obj, appraisal, _ => null); Assert.Contains("Ratings: Dam +3", report); Assert.Contains("Armor Level: 200", report); Assert.Contains("Slashing: Above Average (240)", report); Assert.Contains("Piercing: Average (160)", report); Assert.Contains("Bludgeoning: Below Average (80)", report); Assert.Contains("Cold: None (0)", report); Assert.Contains("Fire: Excellent (320)", report); Assert.Contains("Acid: Unparalleled (400)", report); Assert.Contains("Electric: Poor (40)", report); Assert.Contains("Wield requires Viamontian", report); Assert.Contains("Wield requires Melee Defense 250", report); Assert.Contains("Use requires level 50.", report); Assert.Contains("Number of uses remaining: 8", report); Assert.Contains("Created by Tester.", report); Assert.Contains("This item cannot be sold.", report); } [Fact] public void Launcher_UsesDamageBonusModifierAndAmmunitionWording() { var obj = new ClientObject { ObjectId = 0x50000003u, Name = "Longbow", Type = ItemType.MissileWeapon, ValidLocations = EquipMask.MissileWeapon, AmmoType = 1, }; AppraiseInfoParser.Parsed appraisal = Parsed( new PropertyBundle(), weapon: new AppraiseInfoParser.WeaponProfile( DamageType: 2u, WeaponTime: 45u, WeaponSkill: 47u, Damage: 12u, DamageVariance: 0d, DamageMod: 1.13d, WeaponLength: 1d, MaxVelocity: 50d, WeaponOffense: 1d, MaxVelocityEstimated: 1u)); string report = ItemAppraisalTextFormatter.Build( obj, appraisal, _ => null); Assert.Contains("Skill: Missile Weapons", report); Assert.Contains("Damage Bonus: 12", report); Assert.DoesNotContain("Damage Bonus: 12, Piercing", report); Assert.Contains("Damage Modifier: +13%.", report); Assert.Contains("Speed: Average (45)", report); Assert.Contains("Range: 85 yds. (based on STRENGTH 100)", report); Assert.Contains("Uses arrows as ammunition.", report); } [Fact] public void SpecialProperties_AreCombinedUsingRetailNames() { var obj = new ClientObject { ObjectId = 0x50000004u, Name = "Imbued Katar", Type = ItemType.MeleeWeapon, }; var properties = new PropertyBundle(); properties.Ints[279u] = 3; properties.Ints[292u] = 4; properties.Ints[179u] = 0x0000_0481; properties.Ints[114u] = 1; properties.Ints[33u] = 1; properties.Floats[167u] = 90d; properties.Bools[99u] = true; properties.Bools[100u] = true; string report = ItemAppraisalTextFormatter.Build( obj, Parsed(properties), _ => null); Assert.Contains("You can only carry 3 of these items.", report); Assert.Contains("Cooldown When Used: 1m 30s ", report); Assert.Contains("Cleave: 4 enemies in front arc.", report); Assert.Contains( "Properties: Critical Strike, Cold Rending, +1 Melee Defense, Attuned, Bonded, Ivoryable, Dyeable", report); Assert.Contains("This item cannot be further imbued.", report); } private static AppraiseInfoParser.Parsed Parsed( PropertyBundle properties, uint[]? spells = null, AppraiseInfoParser.WeaponProfile? weapon = null, AppraiseInfoParser.ArmorProfile? armor = null, AppraiseInfoParser.HookProfile? hook = null, (ushort Highlight, ushort Color)? weaponEnchantments = null) => new( Guid: 0x50000001u, Flags: AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable, Success: true, Properties: properties, SpellBook: spells ?? [], ArmorProfile: armor, CreatureProfile: null, WeaponProfile: weapon, HookProfile: hook, ArmorLevels: null, ArmorEnchantments: null, WeaponEnchantments: weaponEnchantments, ResistEnchantments: null); private static SpellMetadata Spell( uint id, string name, string description) => new( SpellId: id, Name: name, School: "War Magic", Family: id, IconId: 0u, SpellWords: string.Empty, Duration: 0f, ManaCost: 10, IsDebuff: false, IsFellowship: false, Description: description, SortKey: 0, Difficulty: 0, Flags: 0u, Generation: 6, IsFastWindup: false, IsOffensive: true, IsUntargeted: false, Speed: 0f, CasterEffect: 0u, TargetEffect: 0u, TargetMask: 0u, SpellType: 0); }