219 lines
7.5 KiB
C#
219 lines
7.5 KiB
C#
using AcDream.Plugin.Abstractions;
|
|
|
|
namespace AcDream.Plugins.MossTank.Tests;
|
|
|
|
public sealed class DebuffSchedulerTests
|
|
{
|
|
[Theory]
|
|
[InlineData("Fester Other VII", 1 << 0, 0)]
|
|
[InlineData("Broadside of a Barn", 1 << 1, 0)]
|
|
[InlineData("Gravity Well", 1 << 2, 0)]
|
|
[InlineData("Incantation of Imperil Other", 1 << 3, 0)]
|
|
[InlineData("Magic Yield Other VI", 1 << 4, 0)]
|
|
[InlineData("Incantation of Fire Vulnerability Other", 1 << 5, 5)]
|
|
[InlineData("Acid Lure VI", 1 << 5, 6)]
|
|
[InlineData("Incantation of Blade Lure", 1 << 5, 1)]
|
|
[InlineData("Incantation of Bludgeon Lure", 1 << 5, 3)]
|
|
[InlineData("Incantation of Frost Lure", 1 << 5, 4)]
|
|
[InlineData("Incantation of Flame Lure", 1 << 5, 5)]
|
|
[InlineData("Incantation of Lightning Lure", 1 << 5, 7)]
|
|
[InlineData("Incantation of Piercing Lure", 1 << 5, 2)]
|
|
[InlineData("Weakening Curse VII", 1 << 9, 0)]
|
|
[InlineData("Festering Curse VII", 1 << 10, 0)]
|
|
[InlineData("Corruption VII", 1 << 11, 0)]
|
|
[InlineData("Destructive Curse VII", 1 << 12, 0)]
|
|
[InlineData("Corrosion VII", 1 << 13, 0)]
|
|
public void ClassifierMapsEveryVtankMonsterDebuffColumn(
|
|
string name,
|
|
int expectedFlag,
|
|
int expectedDamage)
|
|
{
|
|
Assert.True(DebuffSpellCatalog.TryClassify(
|
|
Spell(1, name),
|
|
out DebuffIdentity identity,
|
|
out _));
|
|
Assert.Equal((MonsterActionFlags)expectedFlag, identity.Flag);
|
|
Assert.Equal((MonsterDamageType)expectedDamage, identity.DamageType);
|
|
}
|
|
|
|
[Fact]
|
|
public void DamageAndExtraVulnerabilityBothProduceCandidates()
|
|
{
|
|
DebuffSpellCatalog catalog = DebuffSpellCatalog.Build(
|
|
[
|
|
Spell(1, "Incantation of Fire Vulnerability Other"),
|
|
Spell(2, "Incantation of Acid Vulnerability Other"),
|
|
]);
|
|
var actions = new MonsterRuleActions
|
|
{
|
|
Flags = MonsterActionFlags.Vulnerability,
|
|
DamageType = MonsterDamageType.Fire,
|
|
ExtraVulnerability = MonsterDamageType.Acid,
|
|
};
|
|
|
|
IReadOnlyList<DebuffChoice> choices = catalog.Candidates(
|
|
actions,
|
|
DebuffSelectionMethod.SpellLevel,
|
|
new Character(),
|
|
static (_, _) => true);
|
|
|
|
Assert.Equal(2, choices.Count);
|
|
Assert.Contains(choices, choice =>
|
|
choice.Identity.DamageType == MonsterDamageType.Fire);
|
|
Assert.Contains(choices, choice =>
|
|
choice.Identity.DamageType == MonsterDamageType.Acid);
|
|
}
|
|
|
|
[Fact]
|
|
public void LureBladeItemSpellIsNotAClassicVulnerabilityLure()
|
|
{
|
|
Assert.False(DebuffSpellCatalog.TryClassify(
|
|
Spell(1, "Incantation of Lure Blade"),
|
|
out _,
|
|
out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void SkillPreferenceUsesBuffedSchoolSkillBeforeSpellTier()
|
|
{
|
|
DebuffSpellCatalog catalog = DebuffSpellCatalog.Build(
|
|
[
|
|
Spell(1, "Imperil Other VII", tier: 7, school: 33),
|
|
Spell(2, "Weakening Curse VI", tier: 6, school: 43),
|
|
]);
|
|
var actions = new MonsterRuleActions
|
|
{
|
|
Flags = MonsterActionFlags.Imperil | MonsterActionFlags.WeakeningCurse,
|
|
};
|
|
var character = new Character(
|
|
[
|
|
new PluginSkillInfo(33, "Life Magic", PluginSkillTraining.Trained, 300),
|
|
new PluginSkillInfo(43, "Void Magic", PluginSkillTraining.Trained, 420),
|
|
]);
|
|
|
|
IReadOnlyList<DebuffChoice> choices = catalog.Candidates(
|
|
actions,
|
|
DebuffSelectionMethod.Skill,
|
|
character,
|
|
static (_, _) => true);
|
|
|
|
Assert.Equal(2u, choices[0].Spell.SpellId);
|
|
}
|
|
|
|
[Fact]
|
|
public void TrackerWaitsForMatchingSuccessfulServerReceipt()
|
|
{
|
|
var tracker = new DebuffTracker();
|
|
var identity = new DebuffIdentity(
|
|
MonsterActionFlags.Imperil,
|
|
MonsterDamageType.Auto);
|
|
PluginSpellInfo spell = Spell(10, "Imperil Other VII", duration: 60);
|
|
tracker.Begin(99, identity, spell, now: 10, completionRevision: 4);
|
|
|
|
Assert.False(tracker.Observe(
|
|
new PluginCastCompletion(5, 11, 99, 0), 12).Completed);
|
|
Assert.True(tracker.HasPending);
|
|
|
|
DebuffCompletion failed = tracker.Observe(
|
|
new PluginCastCompletion(6, 10, 99, 0x0402), 13);
|
|
Assert.True(failed.Completed);
|
|
Assert.False(failed.Succeeded);
|
|
Assert.True(tracker.IsDue(99, identity, spell, 13, 5));
|
|
|
|
tracker.Begin(99, identity, spell, 14, completionRevision: 6);
|
|
DebuffCompletion succeeded = tracker.Observe(
|
|
new PluginCastCompletion(7, 10, 99, 0), 15);
|
|
Assert.True(succeeded.Succeeded);
|
|
Assert.False(tracker.IsDue(99, identity, spell, 69, 5));
|
|
Assert.True(tracker.IsDue(99, identity, spell, 70, 5));
|
|
}
|
|
|
|
[Fact]
|
|
public void DamageOverTimeDoesNotUsePrecastWindow()
|
|
{
|
|
var tracker = new DebuffTracker();
|
|
var identity = new DebuffIdentity(
|
|
MonsterActionFlags.Corrosion,
|
|
MonsterDamageType.Auto);
|
|
PluginSpellInfo spell = Spell(
|
|
10,
|
|
"Corrosion VII",
|
|
duration: 60) with { IsDamageOverTime = true };
|
|
tracker.Begin(99, identity, spell, 0, 0);
|
|
tracker.Observe(new PluginCastCompletion(1, 10, 99, 0), 1);
|
|
|
|
Assert.False(tracker.IsDue(99, identity, spell, 60, 20));
|
|
Assert.True(tracker.IsDue(99, identity, spell, 61, 20));
|
|
}
|
|
|
|
[Fact]
|
|
public void FakeImperilCreatesVtankThreeThousandSecondLocalMarker()
|
|
{
|
|
var tracker = new DebuffTracker();
|
|
var identity = new DebuffIdentity(
|
|
MonsterActionFlags.Imperil,
|
|
MonsterDamageType.Auto);
|
|
PluginSpellInfo learned = Spell(
|
|
10,
|
|
"Imperil Other VII",
|
|
tier: 7,
|
|
duration: 60);
|
|
|
|
tracker.RecordFakeImperil(99u, now: 10d);
|
|
|
|
Assert.False(tracker.IsDue(99u, identity, learned, 3009d, 0d));
|
|
Assert.True(tracker.IsDue(99u, identity, learned, 3010d, 0d));
|
|
}
|
|
|
|
private static PluginSpellInfo Spell(
|
|
uint id,
|
|
string name,
|
|
int tier = 8,
|
|
uint school = 33,
|
|
float duration = 30) => new(
|
|
id,
|
|
name,
|
|
Family: id,
|
|
Tier: tier,
|
|
Difficulty: 300,
|
|
ManaCost: 20,
|
|
DurationSeconds: duration,
|
|
School: school,
|
|
Description: string.Empty,
|
|
IsSelfTargeted: false,
|
|
IsBeneficial: false)
|
|
{
|
|
IsDebuff = true,
|
|
IsOffensive = true,
|
|
};
|
|
|
|
private sealed class Character(
|
|
IReadOnlyList<PluginSkillInfo>? skills = null) : ICharacterInfo
|
|
{
|
|
public bool IsInWorld => true;
|
|
public uint ObjectId => 1;
|
|
public uint CurrentHealth => 100;
|
|
public uint MaxHealth => 100;
|
|
public uint CurrentStamina => 100;
|
|
public uint MaxStamina => 100;
|
|
public uint CurrentMana => 100;
|
|
public uint MaxMana => 100;
|
|
public IReadOnlyList<PluginSkillInfo> Skills { get; } = skills ?? [];
|
|
public IReadOnlyList<PluginAttributeInfo> Attributes => [];
|
|
public IReadOnlyList<PluginActiveEnchantment> ActiveEnchantments => [];
|
|
|
|
public bool TryGetSkill(uint skillId, out PluginSkillInfo skill)
|
|
{
|
|
foreach (PluginSkillInfo candidate in Skills)
|
|
{
|
|
if (candidate.SkillId == skillId)
|
|
{
|
|
skill = candidate;
|
|
return true;
|
|
}
|
|
}
|
|
skill = default;
|
|
return false;
|
|
}
|
|
}
|
|
}
|