namespace Aeshnidae.BuffBot; /// /// One patch. WorldObject.TryCastSpell_WithRedirects is where a spell cast at a /// creature finds the items it is really for. For Impenetrability and the Banes, ACE /// (as retail) puts the spell on every enchantable piece of clothing and the shield /// when the caster is the wearer, and on selectable equipment alone - the shield - when /// it is anyone else, so a bot's seven Banes reached nothing but a shield. When the /// caster is one of the bots in Settings, the wearer's own rule is used. Everything /// else (weapon spells, negative item spells, resist checks) is ACE's. /// /// Since 2026-10-05 the same patch is where a Town Warden's town tier is applied: the /// warden's emote names all 68 spells at the top level and this decides, per spell, whether /// the town has paid for it to land and at what level (). /// [HarmonyPatch] public static class Patches { /// /// The NPC emote path is TryCastSpell_WithRedirects(spell, player, itemCaster: the /// bot itself). For an Impen/Bane on another creature ACE picks the targets with /// GetNonComponentTargetTypes, which keeps only SELECTABLE equipment - the shield - /// so armour never saw the spell. (The earlier patch sat one layer down, on /// TryCastItemEnchantment_WithRedirects, and bowed out whenever itemCaster was set - /// which the emote path always does. It never fired. 2026-09-21.) /// /// For a listed bot the targets are every enchantable piece of clothing and armour /// the player wears plus the shield, each cast exactly as ACE would cast at the one. /// [HarmonyPrefix] [HarmonyPatch(typeof(WorldObject), nameof(WorldObject.TryCastSpell_WithRedirects))] public static bool PreTryCastSpellWithRedirects(WorldObject __instance, Spell spell, WorldObject target, WorldObject? itemCaster, WorldObject? weapon, bool isWeaponSpell, bool fromProc, bool tryResist, ref bool __result) { try { if (spell is null || spell.IsHarmful || target is not Player wearer || !Mod.Settings.Bots.Contains(__instance.WeenieClassId)) return true; var verdict = Wardens.Decide(__instance, wearer, spell); if (!verdict.Allowed) { // Either the town has paid for nothing (say so, once per Use), or this tier // simply does not reach this spell (say nothing - 68 refusals is noise). if (verdict.Refusal is not null && Wardens.FirstInBurst(wearer, 0, message: true)) wearer.SendMessage(verdict.Refusal); __result = false; return false; } // The level the player is entitled to, whatever the emote names. var landed = Cast(__instance, wearer, Bands.At(spell, verdict.Level), itemCaster, weapon, isWeaponSpell, fromProc, tryResist); // Tier 10 adds the cantrip of each family the warden already casts, once per Use. if (verdict.Cantrip != 0 && Wardens.FirstInBurst(wearer, verdict.Cantrip, message: false)) { var cantrip = Bands.Strongest(verdict.Cantrip); if (cantrip is not null) Cast(__instance, wearer, cantrip, itemCaster, weapon, isWeaponSpell, fromProc, tryResist); } __result = landed; return false; } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] buff redirect failed, ACE's rule applies: {ex.Message}", ModManager.LogLevel.Warn); return true; } } /// /// One spell at the level decided. An Impen/Bane type goes on every enchantable piece /// the player wears; anything else is ACE's own cast at the player - not back through /// WithRedirects, which is the patched method. /// private static bool Cast(WorldObject caster, Player wearer, Spell spell, WorldObject? itemCaster, WorldObject? weapon, bool isWeaponSpell, bool fromProc, bool tryResist) { if (!spell.IsImpenBaneType) { caster.TryCastSpell(spell, wearer, itemCaster, weapon, isWeaponSpell, fromProc, tryResist); return true; } var items = wearer.EquippedObjects.Values .Where(i => (i.WeenieType == WeenieType.Clothing || i.IsShield) && i.IsEnchantable) .ToList(); foreach (var item in items) caster.TryCastSpell(spell, item, itemCaster, weapon, isWeaponSpell, fromProc, tryResist); if (items.Count == 0) wearer.SendMessage($"{caster.Name} finds nothing on you for {spell.Name}."); return items.Count > 0; } }