using AcDream.Plugin.Abstractions; using AcDream.Plugins.MossTank; namespace AcDream.Plugins.MossTank.Tests; /// /// The mana-upkeep loop: convert stamina to mana when mana is low, restore /// stamina with Revitalize when that leaves stamina too low to convert. /// public class VitalPlanTests { private static readonly VitalSettings Default = new(); private sealed class Character : ICharacterInfo { public bool IsInWorld => true; public uint ObjectId => 1u; public uint CurrentHealth { get; init; } public uint MaxHealth { get; init; } = 100; public uint CurrentStamina { get; init; } public uint MaxStamina { get; init; } = 100; public uint CurrentMana { get; init; } public uint MaxMana { get; init; } = 100; public IReadOnlyList Skills { get; init; } = []; public IReadOnlyList Attributes { get; init; } = []; public IReadOnlyList ActiveEnchantments { get; init; } = []; public bool TryGetSkill(uint skillId, out PluginSkillInfo skill) { skill = default; return false; } } private static PluginSpellInfo Spell( uint id, string name, int tier, int difficulty = 50) => new(id, name, Family: 89, tier, difficulty, ManaCost: 0, DurationSeconds: 0f, School: 33u, Description: string.Empty, IsSelfTargeted: true, IsBeneficial: true); [Fact] public void ConvertsStaminaToManaWhenManaIsLowAndStaminaIsAvailable() { var character = new Character { CurrentMana = 10, CurrentStamina = 90 }; Assert.Equal(VitalAction.StaminaToMana, VitalPlan.Decide(character, Default)); } [Fact] public void RevitalizesWhenBothManaAndStaminaAreLow() { // Converting here would drain what little stamina is left, so stamina // has to come back first. var character = new Character { CurrentMana = 10, CurrentStamina = 10 }; Assert.Equal(VitalAction.Revitalize, VitalPlan.Decide(character, Default)); } [Fact] public void DoesNothingWhenManaIsHealthy() { var character = new Character { CurrentMana = 95, CurrentStamina = 20 }; Assert.Equal(VitalAction.None, VitalPlan.Decide(character, Default)); } [Fact] public void DoesNothingWhenVitalsAreUnknown() { // Unpublished vitals read as zero; treating that as "empty" would cast // on a character that is perfectly fine. var character = new Character { MaxMana = 0, MaxStamina = 0 }; Assert.Equal(VitalAction.None, VitalPlan.Decide(character, Default)); } [Fact] public void PicksTheStrongestCastableConversionByName() { // Family cannot identify these: retail puts Stamina to Health and // Stamina to Mana in the SAME family, so only the name distinguishes // them and a family-based pick would convert into the wrong vital. var known = new[] { Spell(1, "Stamina to Mana Self I", tier: 1), Spell(2, "Stamina to Mana Self VI", tier: 6), Spell(3, "Stamina to Health Self VII", tier: 7), }; var levels = new Dictionary { [33u] = 300 }; Assert.True(VitalPlan.TryFind( known, VitalPlan.StaminaToManaStem, levels, 10, out PluginSpellInfo pick)); Assert.Equal(2u, pick.SpellId); } [Fact] public void SkipsConversionTiersTheSkillCannotCarry() { var known = new[] { Spell(1, "Stamina to Mana Self I", tier: 1, difficulty: 50), Spell(2, "Stamina to Mana Self VII", tier: 7, difficulty: 400), }; var levels = new Dictionary { [33u] = 100 }; Assert.True(VitalPlan.TryFind( known, VitalPlan.StaminaToManaStem, levels, 10, out PluginSpellInfo pick)); Assert.Equal(1u, pick.SpellId); } [Fact] public void DoesNothingWhenVitalUpkeepIsTurnedOff() { // Converting stamina mid-buff surprises people, so it has to be a // setting rather than a behaviour. var character = new Character { CurrentMana = 1, CurrentStamina = 100 }; var off = new VitalSettings { Enabled = false }; Assert.Equal(VitalAction.None, VitalPlan.Decide(character, off)); } [Fact] public void ReportsNoConversionWhenTheCharacterKnowsNone() { var known = new[] { Spell(1, "Strength Self I", tier: 1) }; Assert.False(VitalPlan.TryFind( known, VitalPlan.StaminaToManaStem, new Dictionary(), 10, out _)); } }