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));
}
}