397 lines
14 KiB
C#
397 lines
14 KiB
C#
using AcDream.Plugin.Abstractions;
|
|
|
|
namespace AcDream.Plugins.MossTank.Tests;
|
|
|
|
public sealed class VitalRechargeTests
|
|
{
|
|
[Fact]
|
|
public void RetailDefaultsAreExactNineThresholds()
|
|
{
|
|
var settings = new VitalSettings();
|
|
|
|
Assert.Equal(0.75, settings.NormalHealth);
|
|
Assert.Equal(0.50, settings.NormalStamina);
|
|
Assert.Equal(0.50, settings.NormalMana);
|
|
Assert.Equal(0.01, settings.NoTargetHealth);
|
|
Assert.Equal(0.01, settings.NoTargetStamina);
|
|
Assert.Equal(0.01, settings.NoTargetMana);
|
|
Assert.Equal(0.20, settings.HelperHealth);
|
|
Assert.Equal(0.01, settings.HelperStamina);
|
|
Assert.Equal(0.01, settings.HelperMana);
|
|
}
|
|
|
|
[Fact]
|
|
public void NoTargetTopoffExtendsRatherThanReplacesCombatThreshold()
|
|
{
|
|
var settings = new VitalSettings { NoTargetHealth = 0.90 };
|
|
var surface = new Surface { CurrentHealth = 80 };
|
|
|
|
Assert.Null(VitalPlan.DecideNeed(surface, settings, noTarget: false));
|
|
Assert.Equal(
|
|
VitalKind.Health,
|
|
VitalPlan.DecideNeed(surface, settings, noTarget: true));
|
|
}
|
|
|
|
[Fact]
|
|
public void MagicHealthHandlerOrderMatchesOfficialDefaultTable()
|
|
{
|
|
Assert.Equal(
|
|
[
|
|
VitalRechargeMethod.StaminaToHealth,
|
|
VitalRechargeMethod.ManaToHealth,
|
|
VitalRechargeMethod.RegularSpell,
|
|
VitalRechargeMethod.Food,
|
|
VitalRechargeMethod.Kit,
|
|
],
|
|
VitalRechargePlanner.Handlers(VitalKind.Health, true, 15));
|
|
Assert.Equal(
|
|
[
|
|
VitalRechargeMethod.Kit,
|
|
VitalRechargeMethod.StaminaToHealth,
|
|
VitalRechargeMethod.ManaToHealth,
|
|
VitalRechargeMethod.RegularSpell,
|
|
VitalRechargeMethod.Food,
|
|
],
|
|
VitalRechargePlanner.Handlers(VitalKind.Health, true, 16));
|
|
}
|
|
|
|
[Fact]
|
|
public void NonMagicHealthConversionExistsOnlyInOfficialEmergencyBand()
|
|
{
|
|
Assert.Equal(
|
|
[
|
|
VitalRechargeMethod.Food,
|
|
VitalRechargeMethod.Kit,
|
|
VitalRechargeMethod.StaminaToHealth,
|
|
VitalRechargeMethod.RegularSpell,
|
|
],
|
|
VitalRechargePlanner.Handlers(VitalKind.Health, false, 10));
|
|
Assert.Equal(
|
|
[
|
|
VitalRechargeMethod.Food,
|
|
VitalRechargeMethod.Kit,
|
|
VitalRechargeMethod.RegularSpell,
|
|
],
|
|
VitalRechargePlanner.Handlers(VitalKind.Health, false, 15));
|
|
Assert.Equal(
|
|
[
|
|
VitalRechargeMethod.Kit,
|
|
VitalRechargeMethod.Food,
|
|
VitalRechargeMethod.RegularSpell,
|
|
],
|
|
VitalRechargePlanner.Handlers(VitalKind.Health, false, 16));
|
|
}
|
|
|
|
[Fact]
|
|
public void RechargeHandlerSetCanOverrideOneStanceVitalBand()
|
|
{
|
|
const string profile =
|
|
"combat-health-critical=Regular Spell > Kit Recharge; "
|
|
+ "magic-mana-normal=Recharge With Food > Regular Spell";
|
|
|
|
Assert.Equal(
|
|
[VitalRechargeMethod.RegularSpell, VitalRechargeMethod.Kit],
|
|
VitalRechargePlanner.Handlers(
|
|
VitalKind.Health,
|
|
false,
|
|
5,
|
|
profile));
|
|
Assert.Equal(
|
|
[VitalRechargeMethod.Food, VitalRechargeMethod.RegularSpell],
|
|
VitalRechargePlanner.Handlers(
|
|
VitalKind.Mana,
|
|
true,
|
|
50,
|
|
profile));
|
|
}
|
|
|
|
[Fact]
|
|
public void RechargeBoostAdjustmentTemporarilyRaisesNeed()
|
|
{
|
|
var surface = new Surface { CurrentHealth = 80 };
|
|
var settings = new VitalSettings { NormalHealth = 0.75 };
|
|
|
|
Assert.Null(VitalPlan.DecideNeed(surface, settings, noTarget: false));
|
|
Assert.Equal(
|
|
VitalKind.Health,
|
|
VitalPlan.DecideNeed(
|
|
surface,
|
|
settings,
|
|
noTarget: false,
|
|
healthCurrentAdjustment: 40));
|
|
}
|
|
|
|
[Fact]
|
|
public void IncantationHealthAliasParticipatesInRegularSpellHandler()
|
|
{
|
|
var surface = new Surface
|
|
{
|
|
Mode = PluginCombatMode.Magic,
|
|
CurrentHealth = 50,
|
|
Spells =
|
|
[
|
|
Spell(
|
|
(uint)SpellId.AdjaSIntervention,
|
|
"Adja's Intervention",
|
|
1u,
|
|
400),
|
|
],
|
|
};
|
|
|
|
Assert.True(VitalRechargePlanner.TryPlan(
|
|
VitalKind.Health,
|
|
surface,
|
|
new VitalSettings { MinimumHealKitSuccessChance = 100 },
|
|
new CombatSettings(),
|
|
out VitalRechargeChoice choice));
|
|
Assert.Equal((uint)SpellId.AdjaSIntervention, choice.SpellId);
|
|
}
|
|
|
|
[Fact]
|
|
public void MagicModeUsesProfiledViableKitBeforeRegularHealAboveEmergencyBand()
|
|
{
|
|
var surface = new Surface
|
|
{
|
|
Mode = PluginCombatMode.Magic,
|
|
CurrentHealth = 50,
|
|
Skills = [Skill(21u, 400u)],
|
|
Items = [Kit(10u, "Plentiful Healing Kit", booster: 2)],
|
|
};
|
|
var combat = new CombatSettings();
|
|
combat.ConsumableNames.Add("Plentiful Healing Kit");
|
|
surface.Spells = [Spell(100u, "Heal Self VII", family: 1u, quality: 300)];
|
|
|
|
Assert.True(VitalRechargePlanner.TryPlan(
|
|
VitalKind.Health,
|
|
surface,
|
|
new VitalSettings(),
|
|
combat,
|
|
out VitalRechargeChoice choice));
|
|
Assert.Equal(VitalRechargeSourceKind.Kit, choice.SourceKind);
|
|
Assert.Equal(10u, choice.ItemObjectId);
|
|
}
|
|
|
|
[Fact]
|
|
public void EmergencyMagicHealthPrefersStaminaConversionBeforeKit()
|
|
{
|
|
var surface = new Surface
|
|
{
|
|
Mode = PluginCombatMode.Magic,
|
|
CurrentHealth = 10,
|
|
Skills = [Skill(21u, 400u), Skill(33u, 400u)],
|
|
Items = [Kit(10u, "Plentiful Healing Kit", booster: 2)],
|
|
Spells =
|
|
[
|
|
Spell(101u, "Stamina to Health Self VII", 5u, 300),
|
|
Spell(102u, "Heal Self VII", 1u, 300),
|
|
],
|
|
};
|
|
var combat = new CombatSettings();
|
|
combat.ConsumableNames.Add("Plentiful Healing Kit");
|
|
|
|
Assert.True(VitalRechargePlanner.TryPlan(
|
|
VitalKind.Health,
|
|
surface,
|
|
new VitalSettings(),
|
|
combat,
|
|
out VitalRechargeChoice choice));
|
|
Assert.Equal(VitalRechargeSourceKind.LearnedSpell, choice.SourceKind);
|
|
Assert.Equal(101u, choice.SpellId);
|
|
}
|
|
|
|
[Fact]
|
|
public void DirectLearnedSpellWinsFinalQualityTieAgainstCasterItem()
|
|
{
|
|
PluginSpellInfo learned = Spell(101u, "Heal Self VII", 1u, 300);
|
|
PluginSpellInfo itemSpell = Spell(102u, "Heal Self VII", 1u, 300);
|
|
var surface = new Surface
|
|
{
|
|
Mode = PluginCombatMode.Magic,
|
|
CurrentHealth = 50,
|
|
Spells = [learned],
|
|
Lookup = [itemSpell],
|
|
Items = [Caster(20u, "Healing Lens", itemSpell.SpellId)],
|
|
};
|
|
var combat = new CombatSettings();
|
|
combat.CombatItemNames.Add("Healing Lens");
|
|
|
|
Assert.True(VitalRechargePlanner.TryPlan(
|
|
VitalKind.Health,
|
|
surface,
|
|
new VitalSettings { MinimumHealKitSuccessChance = 100 },
|
|
combat,
|
|
out VitalRechargeChoice choice));
|
|
Assert.Equal(VitalRechargeSourceKind.LearnedSpell, choice.SourceKind);
|
|
Assert.Equal(learned.SpellId, choice.SpellId);
|
|
}
|
|
|
|
[Fact]
|
|
public void HelperChoosesLowestInRangeFellowAndStrongestFamilySpell()
|
|
{
|
|
PluginSpellInfo basis = Spell(
|
|
(uint)SpellId.AdjaSGift,
|
|
"Adja's Gift",
|
|
900u,
|
|
100);
|
|
PluginSpellInfo strong = Spell(300u, "Adja's Grace", 900u, 350);
|
|
var surface = new Surface
|
|
{
|
|
Mode = PluginCombatMode.Magic,
|
|
Spells = [strong],
|
|
Lookup = [basis],
|
|
InFellowship = true,
|
|
Fellows =
|
|
[
|
|
Fellow(70u, "Near", health: 19, distance: 10f),
|
|
Fellow(71u, "Lowest", health: 5, distance: 20f),
|
|
Fellow(72u, "Out of range", health: 1, distance: 100f),
|
|
],
|
|
};
|
|
|
|
Assert.True(VitalRechargePlanner.TryPlanHelper(
|
|
surface,
|
|
new VitalSettings(),
|
|
out VitalRechargeChoice choice));
|
|
Assert.Equal(VitalKind.Health, choice.Vital);
|
|
Assert.Equal(71u, choice.TargetObjectId);
|
|
Assert.Equal(strong.SpellId, choice.SpellId);
|
|
}
|
|
|
|
[Fact]
|
|
public void HealKitChanceUsesRetailLogisticDifficultyFormula()
|
|
{
|
|
var surface = new Surface { CurrentHealth = 50 };
|
|
double chance = VitalRechargePlanner.HealKitChance(
|
|
90u,
|
|
10,
|
|
surface,
|
|
VitalKind.Health,
|
|
PluginCombatMode.Peace);
|
|
|
|
Assert.Equal(0.5d, chance, precision: 10);
|
|
}
|
|
|
|
private static PluginSkillInfo Skill(uint id, uint current) =>
|
|
new(id, string.Empty, PluginSkillTraining.Trained, current);
|
|
|
|
private static PluginSpellInfo Spell(
|
|
uint id,
|
|
string name,
|
|
uint family,
|
|
int quality) =>
|
|
new(id, name, family, 7, quality, 10, 0f, 33u, string.Empty, true, true);
|
|
|
|
private static PluginInventoryItem Kit(
|
|
uint id,
|
|
string name,
|
|
int booster) => Item(id, name) with
|
|
{
|
|
BoosterVital = booster,
|
|
BoostValue = 0,
|
|
HealKitModifier = 1.2,
|
|
};
|
|
|
|
private static PluginInventoryItem Caster(
|
|
uint id,
|
|
string name,
|
|
uint spellId) => new(
|
|
id, 1u, name, 0x8000u, 1u, 0u, 0u, 0u, 0u, 0u, 0u,
|
|
1, 0, 0, spellId, 0, 0, 0u, false, 0d, 0, 0, 0, 0d,
|
|
0, 0, 0);
|
|
|
|
private static PluginInventoryItem Item(uint id, string name) => new(
|
|
id, 1u, name, 0x80u, 1u, 0u, 0u, 0u, 0u, 0u, 0u,
|
|
1, 1, 1, 0u, 0, 0, 0u, false, 0d, 0, 0, 0, 0d,
|
|
21, 0, 0);
|
|
|
|
private static PluginFellowMember Fellow(
|
|
uint id,
|
|
string name,
|
|
uint health,
|
|
float distance) => new(
|
|
id, name, health, 100u, 100u, 100u, 100u, 100u, distance);
|
|
|
|
private sealed class Surface :
|
|
IAutomationSurface,
|
|
ICharacterInfo,
|
|
ISpellCatalog,
|
|
IMagicCommands,
|
|
IPluginChat,
|
|
ICombatAutomation,
|
|
IItemAutomation,
|
|
IFellowshipAutomation
|
|
{
|
|
public bool IsAvailable => true;
|
|
public ICharacterInfo Character => this;
|
|
public ISpellCatalog SpellsCatalog => this;
|
|
ISpellCatalog IAutomationSurface.Spells => this;
|
|
public IMagicCommands Magic => this;
|
|
public IPluginChat Chat => this;
|
|
public ICombatAutomation Combat => this;
|
|
public IItemAutomation ItemsAutomation => this;
|
|
IItemAutomation IAutomationSurface.Items => this;
|
|
public IFellowshipAutomation Fellowship => this;
|
|
public bool IsInWorld => true;
|
|
public uint ObjectId => 1u;
|
|
public uint CurrentHealth { get; init; } = 100u;
|
|
public uint MaxHealth { get; init; } = 100u;
|
|
public uint CurrentStamina { get; init; } = 100u;
|
|
public uint MaxStamina { get; init; } = 100u;
|
|
public uint CurrentMana { get; init; } = 100u;
|
|
public uint MaxMana { get; init; } = 100u;
|
|
public IReadOnlyList<PluginSkillInfo> Skills { get; init; } = [];
|
|
public IReadOnlyList<PluginAttributeInfo> Attributes => [];
|
|
public IReadOnlyList<PluginActiveEnchantment> ActiveEnchantments => [];
|
|
public IReadOnlyList<PluginSpellInfo> Spells { get; set; } = [];
|
|
public IReadOnlyList<PluginSpellInfo> Lookup { get; init; } = [];
|
|
public IReadOnlyList<PluginSpellInfo> KnownSelfBuffs => Spells;
|
|
public IReadOnlyList<PluginInventoryItem> Items { get; init; } = [];
|
|
public bool InFellowship { get; init; }
|
|
public IReadOnlyList<PluginFellowMember> Fellows { get; init; } = [];
|
|
public PluginCombatMode Mode { get; init; } = PluginCombatMode.Peace;
|
|
public PluginCombatSnapshot Snapshot => new(0u, Mode, default, 0f, 0f,
|
|
false, false, false, false);
|
|
bool IItemAutomation.IsAvailable => true;
|
|
bool IFellowshipAutomation.IsInFellowship => InFellowship;
|
|
public bool IsCasting => false;
|
|
|
|
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;
|
|
}
|
|
|
|
public bool TryGet(uint spellId, out PluginSpellInfo info)
|
|
{
|
|
foreach (PluginSpellInfo candidate in Spells.Concat(Lookup))
|
|
{
|
|
if (candidate.SpellId == spellId)
|
|
{
|
|
info = candidate;
|
|
return true;
|
|
}
|
|
}
|
|
info = default;
|
|
return false;
|
|
}
|
|
|
|
public IReadOnlyList<PluginInventoryItem> CaptureOwnedItems() => Items;
|
|
public IReadOnlyList<PluginFellowMember> CaptureMembers() => Fellows;
|
|
public IReadOnlyList<PluginCombatTarget> CaptureHostileTargets(float maximumDistance) => [];
|
|
public PluginCombatCommandResult EnterDefaultMode() => new(PluginCombatCommandStatus.AlreadyReady);
|
|
public PluginCombatCommandResult BeginPhysicalAttack(uint targetObjectId, PluginAttackHeight height, float power) => new(PluginCombatCommandStatus.Started);
|
|
public PluginCombatCommandResult ReleasePhysicalAttack() => new(PluginCombatCommandStatus.Released);
|
|
public PluginCombatCommandResult AbortPhysicalAttack() => new(PluginCombatCommandStatus.Stopped);
|
|
public PluginCastGate EvaluateGate(uint spellId) => PluginCastGate.Ready;
|
|
public bool Cast(uint spellId) => true;
|
|
public void PostSystemMessage(string text) { }
|
|
}
|
|
}
|