using AcDream.Plugin.Abstractions; using AcDream.Plugins.MossTank; namespace AcDream.Plugins.MossTank.Tests; /// /// The buff policy is the part of MossTank that decides what gets cast, so it /// is the part worth pinning. It is a pure function of (known buffs, active /// enchantments) precisely so these tests need no host and no session. /// public class BuffPlanTests { private const double Refresh = 60.0; private static PluginSpellInfo Spell( uint id, uint family, int tier, int mana = 10) => new(id, $"spell-{id}", family, tier, Difficulty: 100, ManaCost: mana, DurationSeconds: 1800f, IsSelfTargeted: true, IsBeneficial: true); private static PluginActiveEnchantment Active( uint id, uint family, int tier, double seconds) => new(id, family, tier, seconds); [Fact] public void WithNothingActive_CastsTheStrongestTierPerFamily() { var known = new[] { Spell(1, family: 10, tier: 1), Spell(2, family: 10, tier: 7), Spell(3, family: 20, tier: 4), }; var plan = BuffPlan.Build(known, Array.Empty(), Refresh); Assert.Equal(2, plan.Count); Assert.Contains(plan, s => s.SpellId == 2); // tier 7 beat tier 1 Assert.Contains(plan, s => s.SpellId == 3); Assert.DoesNotContain(plan, s => s.SpellId == 1); } [Fact] public void SkipsFamiliesAlreadyInForceAtTheSameTier() { var known = new[] { Spell(2, family: 10, tier: 7) }; var active = new[] { Active(2, family: 10, tier: 7, seconds: 900) }; Assert.Empty(BuffPlan.Build(known, active, Refresh)); } [Fact] public void RecastsWhenAStrongerTierIsKnownThanTheOneInForce() { // The whole point of tracking tier rather than mere presence: a // level-1 buff in force must not block casting the level-7 one. var known = new[] { Spell(2, family: 10, tier: 7) }; var active = new[] { Active(1, family: 10, tier: 1, seconds: 900) }; var plan = BuffPlan.Build(known, active, Refresh); Assert.Single(plan); Assert.Equal(2u, plan[0].SpellId); } [Fact] public void RefreshesABuffThatIsAboutToExpire() { var known = new[] { Spell(2, family: 10, tier: 7) }; var active = new[] { Active(2, family: 10, tier: 7, seconds: 5) }; var plan = BuffPlan.Build(known, active, Refresh); Assert.Single(plan); Assert.Equal(2u, plan[0].SpellId); } [Fact] public void TreatsFamilyZeroSpellsIndividually() { // Family 0 is retail's "does not stack" bucket. Collapsing it by family // would silently drop every such buff but one, and they are unrelated // spells that all need casting. var known = new[] { Spell(101, family: 0, tier: 1), Spell(102, family: 0, tier: 1), Spell(103, family: 0, tier: 1), }; var plan = BuffPlan.Build(known, Array.Empty(), Refresh); Assert.Equal(3, plan.Count); } [Fact] public void SkipsAnIndividuallyActiveFamilyZeroSpell() { var known = new[] { Spell(101, family: 0, tier: 1), Spell(102, family: 0, tier: 1) }; var active = new[] { Active(101, family: 0, tier: 1, seconds: 900) }; var plan = BuffPlan.Build(known, active, Refresh); Assert.Single(plan); Assert.Equal(102u, plan[0].SpellId); } [Fact] public void OrdersCheapestFirstSoAPartialPassLandsMoreBuffs() { var known = new[] { Spell(1, family: 10, tier: 1, mana: 500), Spell(2, family: 20, tier: 1, mana: 5), Spell(3, family: 30, tier: 1, mana: 50), }; var plan = BuffPlan.Build(known, Array.Empty(), Refresh); Assert.Equal(new uint[] { 2, 3, 1 }, plan.Select(s => s.SpellId).ToArray()); } [Fact] public void IsStableAcrossRepeatedBuilds() { // The tick loop rebuilds the plan every pass; an unstable order would // make it re-cast the same spell while starving another. var known = new[] { Spell(1, family: 10, tier: 1, mana: 20), Spell(2, family: 20, tier: 1, mana: 20), Spell(3, family: 30, tier: 1, mana: 20), }; var first = BuffPlan.Build(known, Array.Empty(), Refresh); var second = BuffPlan.Build(known, Array.Empty(), Refresh); Assert.Equal( first.Select(s => s.SpellId).ToArray(), second.Select(s => s.SpellId).ToArray()); } [Fact] public void EmptySpellbookProducesNoPlan() { Assert.Empty(BuffPlan.Build( Array.Empty(), Array.Empty(), Refresh)); } }