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