namespace Aeshnidae.CombatWindow; /// /// The two places combat asks the curve, taken over so the slope and the clamps are /// ours. Both are prefixes that replace the original (return false) and are ACE's /// bodies with one line changed; when the mod is off they fall through to ACE. /// /// Why take over the callers rather than patch SkillCheck.GetSkillChance: that method /// is two lines and the JIT inlines it into every caller, so a patch on it is not /// reached by code compiled before the patch went in - which on a hot reload is all /// of it. GetEvadeChance and TryResistSpell are large enough never to be inlined. /// [HarmonyPatch] public static class Patches { /// DamageEvent.GetEvadeChance - melee and missile. [HarmonyPrefix] [HarmonyPatch(typeof(DamageEvent), nameof(DamageEvent.GetEvadeChance), new[] { typeof(Creature), typeof(Creature) })] public static bool PreGetEvadeChance(DamageEvent __instance, Creature attacker, Creature defender, ref float __result) { if (!Mod.Settings.Enabled) return true; try { __instance.AccuracyMod = attacker.GetAccuracyMod(__instance.Weapon); __instance.EffectiveAttackSkill = attacker.GetEffectiveAttackSkill(); __instance.EffectiveDefenseSkill = defender.GetEffectiveDefenseSkill(__instance.CombatType); var land = Mod.Settings.LandChance((int)__instance.EffectiveAttackSkill, (int)__instance.EffectiveDefenseSkill, spell: false); __result = (float)(1.0 - land); return false; } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] evade chance failed, falling back to ACE: {ex.Message}", ModManager.LogLevel.Warn); return true; } } /// WorldObject.TryResistSpell - ACE's body, with the roll on our curve. [HarmonyPrefix] [HarmonyPatch(typeof(WorldObject), nameof(WorldObject.TryResistSpell), new[] { typeof(WorldObject), typeof(Spell), typeof(WorldObject), typeof(bool) })] public static bool PreTryResistSpell(WorldObject __instance, WorldObject target, Spell spell, WorldObject itemCaster, bool projectileHit, ref bool __result) { if (!Mod.Settings.Enabled) return true; try { __result = TryResistSpell(__instance, target, spell, itemCaster, projectileHit); return false; } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] resist check failed, falling back to ACE: {ex.Message}", ModManager.LogLevel.Warn); return true; } } private static bool TryResistSpell(WorldObject self, WorldObject target, Spell spell, WorldObject? itemCaster, bool projectileHit) { // fix hermetic void? if (!spell.IsResistable && spell.Category != SpellCategory.ManaConversionModLowering || spell.IsSelfTargeted) return false; if (spell.MetaSpellType == SpellType.Dispel && spell.Align == DispelType.Negative && !PropertyManager.GetBool("allow_negative_dispel_resist").Item) return false; if (spell.NumProjectiles > 0 && !projectileHit) return false; if (itemCaster != null && Cloak.IsCloak(itemCaster)) return false; uint magicSkill = 0; var caster = itemCaster ?? self; var casterCreature = caster as Creature; if (casterCreature != null) magicSkill = casterCreature.GetCreatureSkill(spell.School).Current; else if (caster.ItemSpellcraft != null) magicSkill = (uint)caster.ItemSpellcraft; else if (caster.Wielder is Creature wielder) magicSkill = wielder.GetCreatureSkill(spell.School).Current; // only creatures can resist spells? if (target is not Creature targetCreature) return false; var difficulty = targetCreature.GetEffectiveMagicDefense(); // The one changed line: ACE's MagicDefenseCheck on retail's 0.03 slope becomes // our curve with its floor and ceiling. var chance = Mod.Settings.LandChance((int)magicSkill, (int)difficulty, spell: true); var rng = ThreadSafeRandom.Next(0.0f, 1.0f); var resistChance = (float)(1.0 - chance); var resisted = chance <= rng; var player = self as Player; var targetPlayer = target as Player; if (targetPlayer != null) { if (targetPlayer.Invincible) resisted = true; if (targetPlayer.UnderLifestoneProtection) { targetPlayer.HandleLifestoneProtection(); resisted = true; } } if (caster == target) resisted = false; if (resisted) { if (player != null) { player.SendChatMessage(targetCreature, $"{targetCreature.Name} resists your spell", ChatMessageType.Magic); player.Session.Network.EnqueueSend(new GameMessageSound(player.Guid, Sound.ResistSpell, 1.0f)); } if (targetPlayer != null) { targetPlayer.SendChatMessage(self, $"You resist the spell cast by {self.Name}", ChatMessageType.Magic); targetPlayer.Session.Network.EnqueueSend(new GameMessageSound(targetPlayer.Guid, Sound.ResistSpell, 1.0f)); if (casterCreature != null) targetPlayer.SetCurrentAttacker(casterCreature); Proficiency.OnSuccessUse(targetPlayer, targetPlayer.GetCreatureSkill(Skill.MagicDefense), magicSkill); } if (self is Creature creature) targetCreature.EmoteManager.OnResistSpell(creature); } if (player != null && player.DebugDamage.HasFlag(Creature.DebugDamageType.Attacker)) WorldObject.ShowResistInfo(player, self, target, spell, magicSkill, difficulty, resistChance, resisted); if (targetCreature.DebugDamage.HasFlag(Creature.DebugDamageType.Defender)) WorldObject.ShowResistInfo(targetCreature, self, target, spell, magicSkill, difficulty, resistChance, resisted); return resisted; } }