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); } }