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