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:
@@ -490,12 +490,22 @@ class spell_mage_cold_snap : public SpellScript
|
||||
{
|
||||
Player* caster = GetCaster()->ToPlayer();
|
||||
// immediately finishes the cooldown on Frost spells
|
||||
|
||||
//
|
||||
// Fractured / Paragon: ParagonFamilyMatches() drops the
|
||||
// SpellFamilyName == SPELLFAMILY_MAGE gate when the caster is a
|
||||
// CLASS_PARAGON player AND Paragon.WildcardFamilyMatching is on,
|
||||
// so any Frost-school spell in the Paragon's spellbook with a real
|
||||
// recovery time (Howling Blast, Frost Shock, Frost Trap, etc.)
|
||||
// also gets its cooldown wiped. Stock Mage callers fall through to
|
||||
// strict family-name equality and observe identical behavior.
|
||||
PlayerSpellMap const& spellMap = caster->GetSpellMap();
|
||||
for (PlayerSpellMap::const_iterator itr = spellMap.begin(); itr != spellMap.end(); ++itr)
|
||||
{
|
||||
SpellInfo const* spellInfo = sSpellMgr->AssertSpellInfo(itr->first);
|
||||
if (spellInfo->SpellFamilyName == SPELLFAMILY_MAGE && (spellInfo->GetSchoolMask() & SPELL_SCHOOL_MASK_FROST) && spellInfo->Id != SPELL_MAGE_COLD_SNAP && spellInfo->GetRecoveryTime() > 0)
|
||||
if (ParagonFamilyMatches(caster, SPELLFAMILY_MAGE, spellInfo->SpellFamilyName)
|
||||
&& (spellInfo->GetSchoolMask() & SPELL_SCHOOL_MASK_FROST)
|
||||
&& spellInfo->Id != SPELL_MAGE_COLD_SNAP
|
||||
&& spellInfo->GetRecoveryTime() > 0)
|
||||
{
|
||||
SpellCooldowns::iterator citr = caster->GetSpellCooldownMap().find(spellInfo->Id);
|
||||
if (citr != caster->GetSpellCooldownMap().end() && citr->second.needSendToClient)
|
||||
@@ -946,6 +956,107 @@ class spell_mage_summon_water_elemental : public SpellScript
|
||||
}
|
||||
};
|
||||
|
||||
// 44543, 44545 - Fingers of Frost (talent ranks - the proc-trigger aura, NOT the
|
||||
// 74396 buff aura that is APPLIED when this talent fires).
|
||||
//
|
||||
// Stock spell_proc gates this talent by SpellFamilyName=MAGE plus a
|
||||
// SpellFamilyMask covering the Mage Frost spells that count as "chill-effect
|
||||
// dealers" (Frostbolt / Frost Nova / Cone of Cold / Blizzard / Frostfire Bolt /
|
||||
// Deep Freeze etc.). For Paragon characters with `Paragon.WildcardFamilyMatching`
|
||||
// enabled, we relax the spell_proc row to wildcard family/mask + SchoolMask=
|
||||
// FROST + SpellTypeMask=DAMAGE so that any Frost-school damage spell (DK Howling
|
||||
// Blast / Icy Touch, Hunter Frost Trap / Wing Clip-as-frost, Shaman Frost Shock,
|
||||
// Druid Hibernate damage payload, etc.) reaches this CheckProc; this script
|
||||
// then re-enforces the stock Mage allowlist for non-Paragon owners and lets
|
||||
// Paragons through unconditionally (the FROST + DAMAGE gate already happens at
|
||||
// the spell_proc layer, so any spell reaching us here is safe to accept).
|
||||
class spell_mage_fingers_of_frost_talent : public AuraScript
|
||||
{
|
||||
PrepareAuraScript(spell_mage_fingers_of_frost_talent);
|
||||
|
||||
bool CheckProc(ProcEventInfo& eventInfo)
|
||||
{
|
||||
SpellInfo const* procSpell = eventInfo.GetSpellInfo();
|
||||
if (!procSpell)
|
||||
return false;
|
||||
|
||||
// Stock Mage allowlist: re-derive from this talent's own effect-0
|
||||
// SpellClassMask so behavior matches the original auto-generated
|
||||
// proc filter exactly (no risk of mask drift across DBC versions).
|
||||
if (procSpell->SpellFamilyName == SPELLFAMILY_MAGE
|
||||
&& (GetSpellInfo()->Effects[EFFECT_0].SpellClassMask & procSpell->SpellFamilyFlags))
|
||||
return true;
|
||||
|
||||
return IsParagonWildcardCaller(GetUnitOwner());
|
||||
}
|
||||
|
||||
void Register() override
|
||||
{
|
||||
DoCheckProc += AuraCheckProcFn(spell_mage_fingers_of_frost_talent::CheckProc);
|
||||
}
|
||||
};
|
||||
|
||||
// 11071, 12496, 12497 - Frostbite (talent ranks - the proc-trigger aura that
|
||||
// chains into 12494 Frostbite freeze).
|
||||
//
|
||||
// Stock spell_proc (auto-generated from DBC) gates this talent by Mage family +
|
||||
// the talent's effect SpellClassMask (Mage Frost slow-applying spells). For
|
||||
// Paragon characters we relax the row to SchoolMask=FROST wildcard so that
|
||||
// chill-applying Frost spells from any class can reach this CheckProc; the
|
||||
// Paragon path additionally requires the proc spell to actually apply a slow
|
||||
// (SPELL_AURA_MOD_DECREASE_SPEED) so that pure damage Frost spells without a
|
||||
// chill component (e.g. raw Ice Lance on a non-frozen target) do NOT freeze.
|
||||
// Stock Mage owners get the original behavior re-enforced here.
|
||||
class spell_mage_frostbite : public AuraScript
|
||||
{
|
||||
PrepareAuraScript(spell_mage_frostbite);
|
||||
|
||||
bool CheckProc(ProcEventInfo& eventInfo)
|
||||
{
|
||||
SpellInfo const* procSpell = eventInfo.GetSpellInfo();
|
||||
if (!procSpell)
|
||||
return false;
|
||||
|
||||
// Stock Mage path: re-derive from this talent's own effect-0
|
||||
// SpellClassMask so behavior matches the original auto-generated
|
||||
// proc filter exactly (no risk of mask drift across DBC versions).
|
||||
if (procSpell->SpellFamilyName == SPELLFAMILY_MAGE
|
||||
&& (GetSpellInfo()->Effects[EFFECT_0].SpellClassMask & procSpell->SpellFamilyFlags))
|
||||
return true;
|
||||
|
||||
if (!IsParagonWildcardCaller(GetUnitOwner()))
|
||||
return false;
|
||||
|
||||
// Paragon path: any Frost-school spell that applies a chill effect
|
||||
// (decrease-speed aura). The spell_proc row already gates by
|
||||
// SchoolMask=FROST so we only need to verify chill semantics here.
|
||||
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
|
||||
{
|
||||
if (procSpell->Effects[i].ApplyAuraName == SPELL_AURA_MOD_DECREASE_SPEED)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also accept the Improved-Blizzard-style cross-class case where the
|
||||
// chill is applied by a separate triggered aura: if the proc spell's
|
||||
// damage hit landed and the target already has a chill from us, treat
|
||||
// it as eligible. Cheap and matches player expectations for Paragon.
|
||||
if (Unit* procTarget = eventInfo.GetProcTarget())
|
||||
{
|
||||
Unit::AuraEffectList const& slows = procTarget->GetAuraEffectsByType(SPELL_AURA_MOD_DECREASE_SPEED);
|
||||
for (AuraEffect const* slowEff : slows)
|
||||
if (slowEff->GetCasterGUID() == GetUnitOwner()->GetGUID())
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Register() override
|
||||
{
|
||||
DoCheckProc += AuraCheckProcFn(spell_mage_frostbite::CheckProc);
|
||||
}
|
||||
};
|
||||
|
||||
// 74396 - Fingers of Frost
|
||||
class spell_mage_fingers_of_frost : public AuraScript
|
||||
{
|
||||
@@ -1631,5 +1742,7 @@ void AddSC_mage_spell_scripts()
|
||||
RegisterSpellScript(spell_mage_polymorph_cast_visual);
|
||||
RegisterSpellScript(spell_mage_summon_water_elemental);
|
||||
RegisterSpellScript(spell_mage_fingers_of_frost);
|
||||
RegisterSpellScript(spell_mage_fingers_of_frost_talent);
|
||||
RegisterSpellScript(spell_mage_frostbite);
|
||||
RegisterSpellScript(spell_mage_magic_absorption);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user