Paragon: cross-class talents + Warrior stance bypass + Mirror Image spellbook draw

Server-side cross-class wildcard pass for several talents that were
previously locked to a single SpellFamilyName, plus a server+client
Warrior stance bypass and a Paragon-aware Mirror Image rebuild that
mimics the owner's spellbook instead of stock Frostbolt/Fire Blast.

Talent expansions (Paragon owners only; stock classes unchanged):
- Cold Snap (11958): resets cooldown of any Frost-school spell.
- Nature's Swiftness (17116, 16188) + Predator's Swiftness (69369):
  instant-cast on any Nature-school spell.
- Vampiric Embrace (15286): leech-heals from any single-target
  Shadow-school spell.
- Fingers of Frost (44543/44545) + Frostbite (11071/12496/12497):
  proc from any Frost-school chill effect (DK Howling Blast / Icy
  Touch / Chains of Ice, Hunter Frost Trap, Shaman Frost Shock,
  cross-class chill auras via SPELL_AURA_MOD_DECREASE_SPEED).
- Maelstrom Weapon (53817): now also affects Mage Fireball (133),
  Frostbolt (116), and Arcane Blast (30451) at every rank, both
  for cast-time/cost spellmod and for stack consumption.

Warrior stance bypass:
- SpellInfo::CheckShapeshift returns SPELL_CAST_OK whenever a
  Paragon caster hits any Stances!=0 spell (no SpellFamilyName
  gate). Stock classes still see the regular form rules.
- Client side: patch-enUS-4.MPQ now zeroes Stances on every
  SPELLFAMILY_WARRIOR Spell.dbc row (105 spells) so the engine's
  pre-cast "Must be in Battle/Defensive/Berserker Stance" check
  no longer eats CMSG_CAST_SPELL packets for Paragons. Server
  bypass enforces the actual decision; stock Warriors still
  error mid-cast if they actually click while out of stance.
- patch-enUS-5.MPQ Lua tooltip post-processor recolors and
  appends "(Paragon: bypassed)" to "Requires *Stance*" lines on
  Warrior abilities, plus Paragon notes on Maelstrom Weapon and
  Mirror Image tooltips. Action-bar UseAction wrapper routes
  stance-gated Warrior spell clicks through CastSpellByName so
  the stance-zero DBC + server bypass actually run.

Mirror Image:
- npc_pet_mage_mirror_image rebuilds its spell list from the
  Paragon owner's spellbook on InitializeAI AND JustEngagedWith
  (the second pass + events.Reset clears any stale events the
  CasterAI base scheduler may have queued from stock 59637 /
  59638 entries before the rebuild ran).
- Curated filter keeps single-target damaging spells (instant,
  cast-time, or channeled) with a base cooldown <=10s, with the
  "damaging" definition expanded to include
  SPELL_EFFECT_TRIGGER_MISSILE and
  SPELL_AURA_PERIODIC_TRIGGER_SPELL so Arcane Missiles
  qualifies. Rejects passives, AoE, melee/ranged weapon strikes,
  item/reagent/stance/equip-gated, and lower spell ranks.
- UpdateAI picks a random spell from the curated list per cast
  and reschedules the next pick by the actually-cast spell's
  cast/channel duration + 750ms breather, so a 5s Arcane
  Missiles channel waits its full duration before re-rolling
  rather than visually looping across four images.

Helpers:
- Unit::IsParagonWildcardCaller / Unit::ParagonFamilyMatches
  used by Spell.cpp, SpellInfo.cpp, Player.cpp,
  SpellAuraEffects.cpp, and the spell scripts.
- SpellInfo::CheckShapeshift signature gains an optional caster
  pointer; all call sites updated.

SQL migrations under modules/mod-paragon/data/sql/db-world/updates/:
- 2026_05_11_01.sql  Vampiric Embrace spell_proc relax + script
                     gate (CheckProc enforces stock for non-Paragon).
- 2026_05_11_02.sql  Maelstrom Weapon spell_proc relax (initial,
                     superseded by _04 below for stack-consumption fix).
- 2026_05_11_03.sql  Fingers of Frost / Frostbite spell_proc relax
                     and spell_script_names binding.
- 2026_05_11_04.sql  Maelstrom Weapon spell_proc fixup: restore
                     SpellPhaseMask=1 (CAST) and AttributesMask=8
                     (REQ_SPELLMOD); previous _02 set 8/0 which
                     silently dropped every proc event.

Diagnostics from this debugging session demoted from LOG_INFO to
LOG_DEBUG (silent at default info level) so production logs stay
quiet but the probes remain available for reproducing future
regressions: pet_mage.cpp MirrorImage probe/kept/rebuild/init/
engage/cast lines and SpellInfo.cpp CheckShapeshift bypass line.

CLIENT-PATCHES.md updated to document the new Warrior stance DBC
patcher (_patch_spell_dbc_stances.py), the spell-tooltip post-
processor and stance UseAction wrapper in patch-enUS-5.MPQ, and
the Mirror Image / Maelstrom Weapon Paragon notes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Docker Build
2026-05-11 14:54:05 -04:00
parent a1c9172beb
commit e649402163
16 changed files with 874 additions and 39 deletions
@@ -0,0 +1,46 @@
-- mod-paragon: Vampiric Embrace (15286) cross-family wildcard.
--
-- Stock 3.3.5 spell_proc row for Vampiric Embrace gates by SpellFamilyName=6
-- (PRIEST) plus a Priest-Shadow-damage SpellFamilyMask. That blocks the proc
-- engine from ever calling the AuraScript's CheckProc when a Paragon casts a
-- non-Priest Shadow-school spell (e.g. Warlock Shadow Bolt, Death Knight
-- Death Coil, etc.), because IsAffected's familyFlags gate fails before
-- CheckProc runs even though the Phase A wildcard already loosens the
-- familyName equality test (see SpellInfo::IsAffected, listenerOwner overload).
--
-- We relax this row so:
-- * SchoolMask=32 (SHADOW) kept -- proc-engine still gates by school
-- * SpellTypeMask=1 (DAMAGE) kept -- only damage events trigger CheckProc
-- * SpellPhaseMask=2 (HIT) kept -- post-hit phase
-- * AttributesMask=2 (TRIGGERED) kept -- triggered-spell payloads still proc
-- * SpellFamilyName=0 wildcard -- IsAffected short-circuits to true
-- * SpellFamilyMask{0,1,2}=0 wildcard -- no flag-bit gating at this layer
--
-- Real filtering moves into spell_pri_vampiric_embrace::CheckProc, which
-- branches on IsParagonWildcardCaller(GetTarget()):
--
-- * For Paragon owners with `Paragon.WildcardFamilyMatching = 1`, accept any
-- single-target Shadow-school spell (Mind Sear / AoE shadow spells like
-- Seed of Corruption / Hellfire are filtered there via IsAffectingArea
-- and the existing Mind Sear bit-mask).
--
-- * For stock Priest owners (and for the non-wildcard runtime path), the
-- CheckProc re-enforces the EXACT original gate -- SpellFamilyName=6 plus
-- the original 0x0280A010 / 0x00002402 / 0x00000008 SpellFamilyMask bits
-- -- so behavior is byte-identical to before this change for any caster
-- that is not a Paragon.
--
-- Net effect: Paragon characters with VE learned now leech-heal off any
-- single-target Shadow spell they cast (Death Coil, Shadow Bolt, Searing
-- Pain, Drain Soul, etc.); stock Shadow Priests are unchanged.
DELETE FROM `spell_proc` WHERE `SpellId` = 15286;
INSERT INTO `spell_proc`
(`SpellId`, `SchoolMask`, `SpellFamilyName`,
`SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`,
`ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`,
`AttributesMask`, `DisableEffectsMask`, `ProcsPerMinute`,
`Chance`, `Cooldown`, `Charges`)
VALUES
(15286, 32, 0, 0, 0, 0, 0, 1, 2, 0, 2, 0, 0, 0, 0, 0);
@@ -0,0 +1,51 @@
-- mod-paragon: Maelstrom Weapon (53817) cross-family wildcard.
--
-- Stock 3.3.5 spell_proc row for Maelstrom Weapon gates by SpellFamilyName=11
-- (SHAMAN) plus a Shaman SpellFamilyMask covering Lightning Bolt, Chain
-- Lightning, Lesser Healing Wave, Healing Wave and Hex (Mask0=451,
-- Mask1=32768). The proc engine therefore never delivers an event to the
-- AuraScript when a Paragon casts a non-Shaman cast-time spell, even if the
-- IsAffected wildcard relaxes SpellFamilyName equality (the SpellFamilyMask
-- AND-with-target-FamilyFlags check still fails because Mage / Warlock /
-- Druid spell-class bits do not overlap with Shaman bits).
--
-- We relax this row so:
-- * SchoolMask=0 wildcard -- proc engine no longer gates by school
-- * SpellTypeMask=1 (DAMAGE) kept -- only damage spells trigger CheckProc
-- * SpellPhaseMask=8 (FINISH) kept -- post-cast phase, on cast finish
-- * SpellFamilyName=0 wildcard -- IsAffected short-circuits to true
-- * SpellFamilyMask{0,1,2}=0 wildcard -- no flag-bit gating at this layer
--
-- Real filtering moves into spell_sha_maelstrom_weapon::CheckProc:
--
-- * For stock Shaman owners (and for the non-wildcard runtime path), the
-- CheckProc re-enforces the EXACT original gate -- SpellFamilyName=11
-- plus the original Mask0=451 / Mask1=32768 bits -- so behavior is
-- byte-identical to before this change for any caster that is not a
-- Paragon.
--
-- * For Paragon owners with `Paragon.WildcardFamilyMatching = 1`, the
-- stock allowlist still passes, AND we additionally accept the curated
-- Mage cast-time nukes Fireball / Frostbolt / Arcane Blast (any rank,
-- matched via GetFirstRankSpell).
--
-- The matching IsAffectedBySpellMod hook in SpellInfo.cpp ensures the cast
-- time + power cost spellmods on aura 53817 also bridge across families for
-- the same Mage spell allowlist, so Paragons get the full Maelstrom Weapon
-- experience (instant cast at 5 stacks + reduced mana cost) on Fireball,
-- Frostbolt and Arcane Blast.
--
-- Net effect: Paragon characters with Maelstrom Weapon learned now spend
-- stacks on Mage cast-time nukes in addition to the stock Shaman list;
-- stock Enhancement Shamans are unchanged.
DELETE FROM `spell_proc` WHERE `SpellId` = 53817;
INSERT INTO `spell_proc`
(`SpellId`, `SchoolMask`, `SpellFamilyName`,
`SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`,
`ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`,
`AttributesMask`, `DisableEffectsMask`, `ProcsPerMinute`,
`Chance`, `Cooldown`, `Charges`)
VALUES
(53817, 0, 0, 0, 0, 0, 0, 1, 8, 0, 0, 0, 0, 0, 0, 0);
@@ -0,0 +1,66 @@
-- mod-paragon: Frostbite + Fingers of Frost cross-family wildcard.
--
-- Both talents are normally gated by SpellFamilyName=3 (MAGE) plus a Mage
-- SpellFamilyMask covering specific Mage Frost spells (Frostbolt / Frost Nova
-- / Cone of Cold / Blizzard / Frostfire Bolt / Deep Freeze for FoF; Frost
-- slow-applying spells for Frostbite). That blocks the proc engine from
-- delivering an event to the AuraScript when a Paragon casts a non-Mage
-- Frost-school chill effect (DK Howling Blast / Icy Touch / Chains of Ice,
-- Hunter Frost Trap, Shaman Frost Shock, etc.), because IsAffected's
-- familyFlags AND-with-target-FamilyFlags check fails before CheckProc runs
-- even after the Paragon family-name wildcard.
--
-- We relax these rows so:
-- * SchoolMask=16 (FROST) gate by Frost school at the proc engine
-- * SpellTypeMask=1 (DAMAGE) only damage events trigger CheckProc
-- * SpellPhaseMask=2 (HIT) post-hit phase
-- * AttributesMask=2 (TRIGGERED) triggered chill payloads still proc
-- * SpellFamilyName=0 wildcard -- IsAffected short-circuits to true
-- * SpellFamilyMask{0,1,2}=0 wildcard -- no flag-bit gating at this layer
--
-- Real filtering moves into the Mage AuraScripts:
--
-- spell_mage_fingers_of_frost_talent attached to 44543 / 44545
-- stock Mage : SpellFamilyName=MAGE AND original Mask0 0x100120 / Mask1 0x1000
-- Paragon : accept (FROST + DAMAGE gate already enforced)
--
-- spell_mage_frostbite attached to 11071 / 12496 / 12497
-- stock Mage : SpellFamilyName=MAGE AND original Mage Frost-slow Mask
-- (Frostbolt / Frost Nova / Cone of Cold / Blizzard / FFB)
-- Paragon : accept iff proc spell applies SPELL_AURA_MOD_DECREASE_SPEED
-- OR the Paragon already has a slow on the proc target
-- (covers the Improved-Blizzard-style "chill via separate
-- triggered aura" cross-class case)
--
-- Net effect: Paragon characters with these talents now have FoF / Frostbite
-- proc off cross-class Frost-school chill effects; stock Mages are unchanged.
DELETE FROM `spell_proc` WHERE `SpellId` IN (44543, 44545, 11071, 12496, 12497);
INSERT INTO `spell_proc`
(`SpellId`, `SchoolMask`, `SpellFamilyName`,
`SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`,
`ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`,
`AttributesMask`, `DisableEffectsMask`, `ProcsPerMinute`,
`Chance`, `Cooldown`, `Charges`)
VALUES
-- Fingers of Frost talent ranks (Chance 7% / 15% preserved from stock row).
(44543, 16, 0, 0, 0, 0, 0, 1, 2, 0, 2, 0, 0, 7, 0, 0),
(44545, 16, 0, 0, 0, 0, 0, 1, 2, 0, 2, 0, 0, 15, 0, 0),
-- Frostbite talent ranks (5% / 10% / 15% per rank, leave Chance=0 to use
-- the DBC ProcChance which already encodes the per-rank percentage).
(11071, 16, 0, 0, 0, 0, 0, 1, 2, 0, 2, 0, 0, 0, 0, 0),
(12496, 16, 0, 0, 0, 0, 0, 1, 2, 0, 2, 0, 0, 0, 0, 0),
(12497, 16, 0, 0, 0, 0, 0, 1, 2, 0, 2, 0, 0, 0, 0, 0);
-- Bind the new AuraScripts (defined in src/server/scripts/Spells/spell_mage.cpp).
DELETE FROM `spell_script_names`
WHERE `spell_id` IN (44543, 44545, 11071, 12496, 12497)
AND `ScriptName` IN ('spell_mage_fingers_of_frost_talent', 'spell_mage_frostbite');
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES
(44543, 'spell_mage_fingers_of_frost_talent'),
(44545, 'spell_mage_fingers_of_frost_talent'),
(11071, 'spell_mage_frostbite'),
(12496, 'spell_mage_frostbite'),
(12497, 'spell_mage_frostbite');
@@ -0,0 +1,51 @@
-- mod-paragon: Maelstrom Weapon (53817) spell_proc fixup.
--
-- The previous migration (2026_05_11_02.sql) had two bugs in the rewritten
-- spell_proc row that prevented stack consumption from firing at all -- not
-- just for our new Mage targets, but also for the stock Shaman cast-time
-- spells (Lightning Bolt, Chain Lightning, Lesser Healing Wave, etc.).
--
-- Bugs:
--
-- * SpellPhaseMask was set to 8. The valid values for SpellPhaseMask are
-- PROC_SPELL_PHASE_CAST = 1
-- PROC_SPELL_PHASE_HIT = 2
-- PROC_SPELL_PHASE_FINISH = 4
-- (see SpellMgr.h). Anything else, including 8, never matches a real
-- proc event, so the proc engine silently dropped every event before it
-- reached the AuraScript. The original stock row uses 1 (CAST), which
-- is what fires when the cast packet's setup phase completes -- exactly
-- when we want the spellmod-affected cast to consume the buff.
--
-- * AttributesMask was set to 0. The original stock row uses 8
-- PROC_ATTR_REQ_SPELLMOD = 0x8
-- which says "only proc on spells that were affected by one of this
-- aura's spellmods". This is the bridge between IsAffectedBySpellMod
-- (which records the aura into Spell::m_appliedMods when calculating
-- cast time / cost) and the proc system (which then knows that the cast
-- used the buff and should consume a charge). Without this attribute,
-- the proc would either fire too aggressively or not at all depending
-- on subsequent gating, but in practice the engine relies on it to
-- correlate spellmod use with stack consumption.
--
-- Fixed row keeps the same family/mask wildcards from the previous
-- migration (so the Paragon Mage allowlist in spell_sha_maelstrom_weapon's
-- CheckProc still gets the chance to filter), restores SpellPhaseMask=1
-- (CAST) and AttributesMask=8 (REQ_SPELLMOD) to match stock semantics, and
-- resets SpellTypeMask to 0 (any spell type -- the REQ_SPELLMOD attribute
-- already gates by "was the buff actually used", so an extra DAMAGE filter
-- is redundant and would block e.g. Lesser Healing Wave on stock Shamans).
--
-- This restores stock Shaman behavior byte-identically and lets Paragon
-- Mage casts also consume stacks via the IsAffectedBySpellMod allowlist.
DELETE FROM `spell_proc` WHERE `SpellId` = 53817;
INSERT INTO `spell_proc`
(`SpellId`, `SchoolMask`, `SpellFamilyName`,
`SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`,
`ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`,
`AttributesMask`, `DisableEffectsMask`, `ProcsPerMinute`,
`Chance`, `Cooldown`, `Charges`)
VALUES
(53817, 0, 0, 0, 0, 0, 0, 0, 1, 0, 8, 0, 0, 0, 0, 0);