namespace Aeshnidae.Healing;
///
/// `healing selftest` (console, meant for staging): runs ACE's own heal-spell path on a creature that is
/// never placed in the world, with retail rules and then with a factor of exactly 3, and checks the
/// patches are attached. Heals must land at 3x retail's range, once per cast; harm must not change.
/// The kits cannot be driven without a player online - their patches are only shown attached.
///
public static class SelfTest
{
private const int Casts = 12;
private const double TestFactor = 3;
public static string Run(uint wcid)
{
var sb = new StringBuilder();
var saved = Mod.Settings;
Creature? subject = null;
try
{
foreach (var (label, method) in Targets())
{
var owners = method is null ? null : Harmony.GetPatchInfo(method)?.Owners;
sb.AppendLine($" {label}: {(owners?.Contains(Mod.HarmonyId) == true ? "patched" : "NOT PATCHED")}");
}
sb.AppendLine($" HandleCastSpell_Boost calls rewritten: {Scaling.RewrittenCalls} (ACE has 3)");
subject = WorldObjectFactory.CreateNewWorldObject(wcid) as Creature;
if (subject is null)
return sb.AppendLine($" wcid {wcid} is not a creature").ToString();
// A big pool, so a tripled heal is never cut short by the missing amount.
subject.Health.StartingValue = 3000;
subject.Stamina.StartingValue = 3000;
var boost = AccessTools.Method(typeof(WorldObject), "HandleCastSpell_Boost");
var boostResist = AccessTools.Method(typeof(WorldObject), "GetBoostResistanceType", new[] { typeof(DamageType) });
var drainResist = AccessTools.Method(typeof(WorldObject), "GetDrainResistanceType", new[] { typeof(DamageType) });
var retail = new Settings { Enabled = false };
var scaled = new Settings
{
Enabled = true, ScaleSpells = true, ScaleMonsterHeals = true,
ReferenceHealth = subject.Health.MaxValue / TestFactor,
ReferenceStamina = subject.Stamina.MaxValue / TestFactor,
};
var pass = true;
foreach (var id in new[] { SpellId.HealSelf6, SpellId.RevitalizeSelf6, SpellId.HarmSelf1 })
{
var spell = new Spell(id);
var heal = spell.Boost > 0;
var vital = spell.VitalDamageType == DamageType.Stamina ? subject.Stamina : subject.Health;
var resist = (ResistanceType)(heal ? boostResist : drainResist)!.Invoke(null, new object[] { spell.VitalDamageType })!;
var mod = subject.GetResistanceMod(resist);
var lo = Math.Round(Math.Min(spell.Boost, spell.MaxBoost) * mod);
var hi = Math.Round(Math.Max(spell.Boost, spell.MaxBoost) * mod);
if (heal) { lo *= TestFactor; hi *= TestFactor; }
Mod.Settings = retail;
var r = Cast(subject, vital, boost, spell, heal);
Mod.Settings = scaled;
var before = Scaling.SpellsScaled;
var s = Cast(subject, vital, boost, spell, heal);
var counted = Scaling.SpellsScaled - before;
Mod.Settings = saved;
var ok = s.All(d => d >= lo - 1 && d <= hi + 1) && counted == (heal ? Casts : 0);
pass &= ok;
sb.AppendLine($" {spell.Name}: retail {r.Min()}..{r.Max()}, with x{TestFactor} {s.Min()}..{s.Max()} " +
$"(expected {lo}..{hi}), scaled {counted} of {Casts} - {(ok ? "ok" : "WRONG")}");
}
sb.Insert(0, $"Healing self-test on {subject.Name} ({subject.Health.MaxValue} health, {subject.Stamina.MaxValue} stamina): {(pass ? "PASS" : "FAIL")}\n");
}
catch (Exception ex)
{
sb.AppendLine($" self-test threw: {ex.Message}");
}
finally
{
Mod.Settings = saved;
try { subject?.Destroy(); } catch { }
}
return sb.ToString();
}
// One cast at a time, from 1 point (a heal) or a full pool (harm), measuring the change.
private static List Cast(Creature subject, CreatureVital vital, MethodInfo boost, Spell spell, bool heal)
{
var deltas = new List();
for (var i = 0; i < Casts; i++)
{
subject.UpdateVital(vital, heal ? 1 : (int)vital.MaxValue);
var start = (int)vital.Current;
boost.Invoke(subject, new object[] { spell, subject });
deltas.Add((int)vital.Current - start);
}
return deltas;
}
private static IEnumerable<(string, MethodBase?)> Targets()
{
yield return ("Healer.DoSkillCheck", AccessTools.Method(typeof(Healer), nameof(Healer.DoSkillCheck)));
yield return ("Healer.GetHealAmount", AccessTools.Method(typeof(Healer), nameof(Healer.GetHealAmount)));
yield return ("WorldObject.HandleCastSpell_Boost", AccessTools.Method(typeof(WorldObject), "HandleCastSpell_Boost"));
}
}