using AcDream.Plugin.Abstractions; using AcDream.Plugins.MossTank; namespace AcDream.Plugins.MossTank.Tests; /// /// The buff policy decides what gets cast, so it is the part worth pinning. It /// is a pure function of (buff lines, character state) precisely so these tests /// need no host and no session. /// public class BuffPlanTests { private const uint LifeMagicSkill = 33u; private const uint CreatureEnchantmentSkill = 31u; private static readonly BuffSettings Default = new(); private static PluginSpellInfo Spell( uint id, uint family, int tier, string description, int difficulty = 50, int mana = 10, uint school = CreatureEnchantmentSkill, float duration = 1800f) => new(id, $"spell-{id}", family, tier, difficulty, mana, duration, school, description, IsSelfTargeted: true, IsBeneficial: true); private static PluginSkillInfo Skill( uint id, string name, PluginSkillTraining training, uint level = 300) => new(id, name, training, level); private static PluginAttributeInfo Attribute(int kind, string name) => new(kind, name, 200); private static List Lines(params PluginSpellInfo[] spells) => BuffProfile.Build(spells); // ── BuffProfile: deriving the target from retail's own description ──── [Theory] [InlineData("Increases the caster's Life Magic skill by 10 points.", BuffTargetKind.Skill, "Life Magic")] [InlineData("Increases the caster's Strength by 10 points.", BuffTargetKind.Attribute, "Strength")] // The trap that would break any name-matching scheme: the spell line called // Willpower raises the attribute named Self. [InlineData("Increases the caster's Self by 10 points.", BuffTargetKind.Attribute, "Self")] // Retail's spell text and skill table disagree on this skill's name. [InlineData("Increases the caster's Assess Monster skill by 10 points.", BuffTargetKind.Skill, "Assess Creature")] public void ParsesBuffTargetFromDescription( string description, BuffTargetKind expectedKind, string expectedTarget) { Assert.True(BuffProfile.TryParseTarget(description, out var kind, out string target)); Assert.Equal(expectedKind, kind); Assert.Equal(expectedTarget, target); } [Fact] public void IgnoresSpellsWhoseDescriptionSaysNothingAboutAStat() { Assert.False(BuffProfile.TryParseTarget( "Drains one-half of the caster's Stamina and gives 90% of that to his/her Mana.", out _, out _)); } [Fact] public void ExcludesInstantaneousSpellsFromBuffLines() { // Vital transfers have no duration and share families across unrelated // lines, so treating them as buffs would be wrong twice over. var instant = Spell(1, family: 89, tier: 1, "Increases the caster's Strength by 10 points.", duration: 0f); Assert.Empty(BuffProfile.Build(new[] { instant })); } // ── BuffPlan: what actually gets cast ──────────────────────────────── [Fact] public void BuffsTrainedSkillsAndSkipsUntrainedOnes() { var lines = Lines( Spell(1, 47, 1, "Increases the caster's Life Magic skill by 10 points."), Spell(2, 71, 1, "Increases the caster's Leadership skill by 10 points.")); var plan = BuffPlan.Build( lines, new[] { Skill(LifeMagicSkill, "Life Magic", PluginSkillTraining.Specialized), Skill(35, "Leadership", PluginSkillTraining.Untrained), }, Array.Empty(), Array.Empty(), Default); Assert.Single(plan); Assert.Equal(1u, plan[0].SpellId); } [Fact] public void BuffsEveryAttribute() { var lines = Lines( Spell(1, 1, 1, "Increases the caster's Strength by 10 points."), Spell(2, 11, 1, "Increases the caster's Self by 10 points.")); var plan = BuffPlan.Build( lines, Array.Empty(), new[] { Attribute(0, "Strength"), Attribute(5, "Self") }, Array.Empty(), Default); Assert.Equal(2, plan.Count); } [Fact] public void PicksTheStrongestTierTheCastingSkillCanCarry() { // Skill 300; tiers at difficulty 400 and 200. With the default excess // of 10, only the 200 tier is reliable. var lines = Lines( Spell(1, 47, 7, "Increases the caster's Life Magic skill by 10 points.", difficulty: 400, school: LifeMagicSkill), Spell(2, 47, 4, "Increases the caster's Life Magic skill by 10 points.", difficulty: 200, school: LifeMagicSkill)); var plan = BuffPlan.Build( lines, new[] { Skill(LifeMagicSkill, "Life Magic", PluginSkillTraining.Trained, 300) }, Array.Empty(), Array.Empty(), Default); Assert.Single(plan); Assert.Equal(2u, plan[0].SpellId); } [Fact] public void SkipsAFamilyAlreadyInForceAtAnEqualTierWithTimeLeft() { var lines = Lines( Spell(1, 47, 4, "Increases the caster's Life Magic skill by 10 points.", difficulty: 100, school: LifeMagicSkill)); var plan = BuffPlan.Build( lines, new[] { Skill(LifeMagicSkill, "Life Magic", PluginSkillTraining.Trained) }, Array.Empty(), new[] { new PluginActiveEnchantment(1, 47, 4, 900) }, Default); Assert.Empty(plan); } [Fact] public void RecastsWhenTheBuffInForceIsWeaker() { var lines = Lines( Spell(2, 47, 7, "Increases the caster's Life Magic skill by 10 points.", difficulty: 100, school: LifeMagicSkill)); var plan = BuffPlan.Build( lines, new[] { Skill(LifeMagicSkill, "Life Magic", PluginSkillTraining.Trained) }, Array.Empty(), new[] { new PluginActiveEnchantment(1, 47, 2, 900) }, Default); Assert.Single(plan); } [Fact] public void RefreshesBelowVirindiTanksFiveMinuteThreshold() { var lines = Lines( Spell(1, 47, 4, "Increases the caster's Life Magic skill by 10 points.", difficulty: 100, school: LifeMagicSkill)); var skills = new[] { Skill(LifeMagicSkill, "Life Magic", PluginSkillTraining.Trained) }; var comfortable = BuffPlan.Build(lines, skills, Array.Empty(), new[] { new PluginActiveEnchantment(1, 47, 4, 301) }, Default); var expiring = BuffPlan.Build(lines, skills, Array.Empty(), new[] { new PluginActiveEnchantment(1, 47, 4, 299) }, Default); Assert.Empty(comfortable); Assert.Single(expiring); } [Fact] public void OrdersCheapestFirstSoAPartialPassLandsMoreBuffs() { var lines = Lines( Spell(1, 1, 1, "Increases the caster's Strength by 10 points.", mana: 500), Spell(2, 3, 1, "Increases the caster's Endurance by 10 points.", mana: 5), Spell(3, 5, 1, "Increases the caster's Quickness by 10 points.", mana: 50)); var plan = BuffPlan.Build( lines, Array.Empty(), new[] { Attribute(0, "Strength"), Attribute(1, "Endurance"), Attribute(2, "Quickness") }, Array.Empty(), Default); Assert.Equal(new uint[] { 2, 3, 1 }, plan.Select(s => s.SpellId).ToArray()); } [Fact] public void EmptySpellbookProducesNoPlan() { Assert.Empty(BuffPlan.Build( Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Default)); } }