using System.Reflection.Emit;
namespace Aeshnidae.Healing;
///
/// The one rule: a heal is its retail amount times Factor - max(1, the target's maximum of the
/// vital / Settings' reference for it), capped at MaxFactor when that is set. Harm and drain are
/// never touched (they are negative boosts, or not boosts at all).
///
/// The hooks (every body wrapped - a hot path must never throw into ACE):
///
/// Healer.DoSkillCheck (prefix) a kit's success is a skill check against missing vital x 2;
/// the missing vital is divided by the factor, so a large pool
/// heals with retail odds instead of failing nearly always
/// Healer.GetHealAmount (postfix) the kit's amount x factor, capped at the missing vital; the
/// stamina it costs stays the retail amount's
/// WorldObject.HandleCastSpell_Boost (prefix/finalizer) a positive Boost spell (Heal, Revitalize)
/// marks its target and vital; (transpiler) its UpdateVitalDelta
/// calls go through BoostVitalDelta, which multiplies that one
/// positive delta, once
///
[HarmonyPatch]
public static class Scaling
{
public static long KitsScaled, SpellsScaled;
private static double Reference(PropertyAttribute2nd vital) => vital switch
{
PropertyAttribute2nd.Health or PropertyAttribute2nd.MaxHealth => Mod.Settings.ReferenceHealth,
PropertyAttribute2nd.Stamina or PropertyAttribute2nd.MaxStamina => Mod.Settings.ReferenceStamina,
PropertyAttribute2nd.Mana or PropertyAttribute2nd.MaxMana => Mod.Settings.ReferenceMana,
_ => 0,
};
private static bool IsMana(PropertyAttribute2nd vital) =>
vital is PropertyAttribute2nd.Mana or PropertyAttribute2nd.MaxMana;
// GetCreatureVital takes only Health/Stamina/Mana (a Max* id logs an error and returns null),
// while a CreatureVital calls itself MaxHealth/MaxStamina/MaxMana.
private static PropertyAttribute2nd Pool(PropertyAttribute2nd vital) => vital switch
{
PropertyAttribute2nd.MaxHealth => PropertyAttribute2nd.Health,
PropertyAttribute2nd.MaxStamina => PropertyAttribute2nd.Stamina,
PropertyAttribute2nd.MaxMana => PropertyAttribute2nd.Mana,
_ => vital,
};
/// How much a heal of this vital on this creature is multiplied by (1 = retail).
public static double Factor(Creature? target, PropertyAttribute2nd vital)
{
if (target is null || !Mod.Settings.Enabled || (IsMana(vital) && !Mod.Settings.ScaleMana))
return 1;
var reference = Reference(vital);
if (reference <= 0 || Pool(vital) is not (PropertyAttribute2nd.Health or PropertyAttribute2nd.Stamina or PropertyAttribute2nd.Mana))
return 1;
var max = target.GetCreatureVital(Pool(vital))?.MaxValue ?? 0;
var factor = Math.Max(1.0, max / reference);
return Mod.Settings.MaxFactor > 0 ? Math.Min(factor, Mod.Settings.MaxFactor) : factor;
}
// ------------------------------------------------------------ kits
[HarmonyPrefix]
[HarmonyPatch(typeof(Healer), nameof(Healer.DoSkillCheck))]
public static void PreSkillCheck(Healer __instance, Player target, ref uint missingVital)
{
try
{
if (!Mod.Settings.ScaleKits)
return;
var factor = Factor(target, __instance.BoosterEnum);
if (factor > 1)
missingVital = (uint)Math.Ceiling(missingVital / factor);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] kit skill check scaling failed: {ex.Message}", ModManager.LogLevel.Warn);
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(Healer), nameof(Healer.GetHealAmount))]
public static void PostHealAmount(Healer __instance, Player target, uint missingVital, ref uint __result)
{
try
{
if (!Mod.Settings.ScaleKits || __result == 0)
return;
var factor = Factor(target, __instance.BoosterEnum);
if (factor <= 1)
return;
// Capped at the missing vital, but never below what retail would have said (a healing
// rating can already carry retail's figure past the missing vital).
__result = Math.Max(__result, (uint)Math.Min(missingVital, Math.Round(__result * factor)));
KitsScaled++;
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] kit heal scaling failed: {ex.Message}", ModManager.LogLevel.Warn);
}
}
// ------------------------------------------------------------ spells
[ThreadStatic] private static (Creature Target, PropertyAttribute2nd Vital, double Factor)? _pending;
private static PropertyAttribute2nd? VitalOf(DamageType type) => type switch
{
DamageType.Health => PropertyAttribute2nd.MaxHealth,
DamageType.Stamina => PropertyAttribute2nd.MaxStamina,
DamageType.Mana => PropertyAttribute2nd.MaxMana,
_ => null,
};
[HarmonyPrefix]
[HarmonyPatch(typeof(WorldObject), "HandleCastSpell_Boost")]
public static void PreBoost(WorldObject __instance, Spell spell, Creature targetCreature)
{
_pending = null;
try
{
if (!Mod.Settings.Enabled || !Mod.Settings.ScaleSpells || spell is null || targetCreature is null)
return;
// A heal: both ends of the range positive. Harm (negative) is left exactly as it is.
if (spell.Boost <= 0 || spell.MaxBoost <= 0)
return;
if (VitalOf(spell.VitalDamageType) is not { } vital)
return;
// A heal on a monster or NPC (its own, or a player's) stays retail unless asked otherwise.
if (targetCreature is not Player && !Mod.Settings.ScaleMonsterHeals)
return;
var factor = Factor(targetCreature, vital);
if (factor > 1)
_pending = (targetCreature, vital, factor);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] heal spell scaling failed: {ex.Message}", ModManager.LogLevel.Warn);
}
}
[HarmonyFinalizer]
[HarmonyPatch(typeof(WorldObject), "HandleCastSpell_Boost")]
public static void FinalBoost() => _pending = null;
// The heal's own UpdateVitalDelta calls are rewritten to go through BoostVitalDelta. A prefix on
// Creature.UpdateVitalDelta (the first version) stopped firing after a server restart: the method
// is three lines, and the JIT inlined it into HandleCastSpell_Boost's compiled body, where a patch
// on it never runs (staging, 2026-09-30: self-test "scaled 0 of 12" with the patch attached).
private static readonly MethodInfo UpdateVitalDeltaInt =
AccessTools.Method(typeof(Creature), nameof(Creature.UpdateVitalDelta), new[] { typeof(CreatureVital), typeof(int) });
/// How many calls the transpiler rewrote (ACE has three: mana, stamina, health).
public static int RewrittenCalls;
[HarmonyTranspiler]
[HarmonyPatch(typeof(WorldObject), "HandleCastSpell_Boost")]
public static IEnumerable TranspileBoost(IEnumerable instructions)
{
var rewritten = 0;
var target = AccessTools.Method(typeof(Scaling), nameof(BoostVitalDelta));
foreach (var instruction in instructions)
{
if ((instruction.opcode == OpCodes.Call || instruction.opcode == OpCodes.Callvirt) &&
instruction.operand is MethodInfo method && method == UpdateVitalDeltaInt)
{
// Same stack: the creature, the vital, the delta in; the change out.
instruction.opcode = OpCodes.Call;
instruction.operand = target;
rewritten++;
}
yield return instruction;
}
RewrittenCalls = rewritten;
}
public static int BoostVitalDelta(Creature creature, CreatureVital vital, int delta)
{
if (_pending is { } p && delta > 0 && ReferenceEquals(creature, p.Target) && vital?.Vital == p.Vital)
{
_pending = null; // once: the spell's own delta, nothing after it
try
{
delta = (int)Math.Min(int.MaxValue, Math.Round(delta * p.Factor));
SpellsScaled++;
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] heal spell delta failed: {ex.Message}", ModManager.LogLevel.Warn);
}
}
return creature.UpdateVitalDelta(vital, delta);
}
}