Three whole categories of buff were missing, for two different reasons, and
both were my errors.
**Protections and weapon auras were silently dropped by the description
parser.** They are self-targeted and were sitting in the spellbook the whole
time, but retail words them differently and the pattern only accepted
"Increases the caster's X by N":
Fire Protection Self -> "Reduces damage the caster takes from Fire by 9%."
Armor Self -> "Increases the caster's natural armor by 20 points."
Aura of Blood Drinker -> "Increases a weapon's damage value by 2 points."
So the weapon and wand buffs do exist as self-cast "Aura of" lines and are now
cast. Each category has its own toggle, matching VTank's separate
BuffProfile_Prots and BuffProfile_Banes.
The underlying flaw mattered more than the two missing patterns: anything
unmatched was DISCARDED. It now falls into an Other bucket (off by default)
instead, so nothing self-targeted is lost without a word. A test caught a
second instance immediately -- regeneration spells say "Restores..." and were
vanishing the same way.
**Banes were excluded because I misread a flag.** I took IsSelfTargeted as
"can be cast on you". It means "needs no selection". Retail's own bane text
says exactly how they work:
"Increases a shield or piece of armor's resistance to slashing damage by
10%. Target yourself to cast this spell on all of your equipped armor."
So banes ARE cast on the person, and the catalogue now includes every
beneficial non-untargeted spell rather than only flagged self-casts, leaving
EvaluateGate to decide what a given target accepts. Before casting anything
without the self flag, MossTank selects the player -- and restores whatever
was selected before the pass, so targeting yourself does not quietly steal
the selection.
They are matched on retail's "Target yourself..." sentence rather than on the
word "Bane", so the classification comes from what the spell says it does.
Solution builds clean; 14,450 tests pass on the standard hermetic lane filter,
0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
4.6 KiB
C#
123 lines
4.6 KiB
C#
using AcDream.Plugin.Abstractions;
|
|
using AcDream.Plugins.MossTank;
|
|
|
|
namespace AcDream.Plugins.MossTank.Tests;
|
|
|
|
/// <summary>
|
|
/// The mana-upkeep loop: convert stamina to mana when mana is low, restore
|
|
/// stamina with Revitalize when that leaves stamina too low to convert.
|
|
/// </summary>
|
|
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<PluginSkillInfo> Skills { get; init; } = [];
|
|
public IReadOnlyList<PluginAttributeInfo> Attributes { get; init; } = [];
|
|
public IReadOnlyList<PluginActiveEnchantment> 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<uint, uint> { [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<uint, uint> { [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<uint, uint>(), 10, out _));
|
|
}
|
|
}
|