From a1c9172beb76730f1a91f5146e167060970993d9 Mon Sep 17 00:00:00 2001 From: Docker Build Date: Mon, 11 May 2026 01:24:50 -0400 Subject: [PATCH] Paragon cross-class family wildcard + Predatory Strikes proc - CONFIG_PARAGON_WILDCARD_FAMILY + Paragon.WildcardFamilyMatching (reloadable) - SpellInfo::IsAffected / IsAffectedBySpellMod(listenerOwner) for Paragon proc/mod wildcard - SpellMgr::CanSpellTriggerProcOnEvent(procOwner) + Aura::IsProcTriggeredOnEvent wiring - Player::IsAffectedBySpellmod passes listener for SpellMod wildcard - ParagonFamilyMatches helper + Nourish / Shred-Maul bleed gate usage in Unit.cpp - Spell::prepare: Paragon consumes 69369 for Nature spells <10s base cast (non-channeled) - spell_paragon_predatory_strikes + SQL 2026_05_11_00.sql (spell_proc + spell_script_names) Co-authored-by: Cursor --- .../sql/db-world/updates/2026_05_11_00.sql | 60 ++++++++ modules/mod-paragon/src/Paragon_SC.cpp | 128 ++++++++++++++++++ .../apps/worldserver/worldserver.conf.dist | 30 ++++ src/server/game/Entities/Player/Player.cpp | 6 +- src/server/game/Entities/Unit/Unit.cpp | 18 ++- src/server/game/Entities/Unit/Unit.h | 9 ++ src/server/game/Spells/Auras/SpellAuras.cpp | 4 +- src/server/game/Spells/Spell.cpp | 26 ++++ src/server/game/Spells/SpellInfo.cpp | 26 +++- src/server/game/Spells/SpellInfo.h | 12 ++ src/server/game/Spells/SpellMgr.cpp | 8 +- src/server/game/Spells/SpellMgr.h | 7 +- src/server/game/World/WorldConfig.cpp | 6 + src/server/game/World/WorldConfig.h | 6 + 14 files changed, 337 insertions(+), 9 deletions(-) create mode 100644 modules/mod-paragon/data/sql/db-world/updates/2026_05_11_00.sql diff --git a/modules/mod-paragon/data/sql/db-world/updates/2026_05_11_00.sql b/modules/mod-paragon/data/sql/db-world/updates/2026_05_11_00.sql new file mode 100644 index 0000000..8a40491 --- /dev/null +++ b/modules/mod-paragon/data/sql/db-world/updates/2026_05_11_00.sql @@ -0,0 +1,60 @@ +-- mod-paragon: Predatory Strikes (16972 / 16974 / 16975) Cataclysm-style +-- finisher proc for CLASS_PARAGON characters. +-- +-- The 3.3.5 Predatory Strikes is a passive AP / ranged-attack-power talent +-- with no proc payload. The Cataclysm redesign added "Predator's Swiftness" +-- (69369), which makes the next Nature spell <10s base cast time instant. +-- That buff already exists in the WotLK Spell.dbc (because Blizzard reused +-- the spell id), but no server-side trigger ever calls CastSpell(69369) on +-- a 3.3.5 server. We need both halves: the proc handler AND a spell_proc +-- row so the proc evaluator actually invokes our AuraScript. +-- +-- AuraScript binding: spell_paragon_predatory_strikes is registered in +-- modules/mod-paragon/src/Paragon_SC.cpp. It checks +-- (a) caster is CLASS_PARAGON, +-- (b) source spell consumes combo points (NeedsComboPoints), +-- (c) source spell deals damage (DmgClass MELEE/RANGED + at least one +-- damage effect or periodic-damage aura -- filters Slice and Dice, +-- Savage Roar, Maim, Kidney Shot, Expose Armor, Recuperate), +-- then rolls a per-rank chance of (CP * 3 / 5 / 7)% to cast 69369 on +-- the caster. +-- +-- spell_proc row params: +-- ProcFlags = 0x40000 = PROC_FLAG_DONE_SPELL_MELEE_DMG_CLASS +-- SpellTypeMask = 0x3 (DAMAGE | HEAL bitmask in proc engine; we +-- filter precisely in CheckProc anyway, the +-- mask just gates "spell-type events" through) +-- SpellPhaseMask = 0x2 = PROC_SPELL_PHASE_CAST -- fires DURING cast +-- so player->GetComboPoints() inside HandleProc +-- still returns the pre-_handle_finish_phase +-- value. +-- Chance = 100 (the per-CP chance is rolled inside the script) +-- +-- Note: this row's SpellFamilyName / SpellFamilyMask are 0 so the proc +-- engine's IsAffected check is a wildcard at the entry-level. The +-- AuraScript's CheckProc owns all real filtering. Combined with Phase A +-- (Paragon SpellFamilyName wildcard) this is harmless on stock classes +-- because non-Paragon characters cannot learn Predatory Strikes via the +-- Character Advancement panel. + +DELETE FROM `spell_script_names` + WHERE `spell_id` IN (16972, 16974, 16975) + AND `ScriptName` = 'spell_paragon_predatory_strikes'; + +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES + (16972, 'spell_paragon_predatory_strikes'), + (16974, 'spell_paragon_predatory_strikes'), + (16975, 'spell_paragon_predatory_strikes'); + +DELETE FROM `spell_proc` WHERE `SpellId` IN (16972, 16974, 16975); + +INSERT INTO `spell_proc` + (`SpellId`, `SchoolMask`, `SpellFamilyName`, + `SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`, + `ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`, + `AttributesMask`, `DisableEffectsMask`, `ProcsPerMinute`, + `Chance`, `Cooldown`, `Charges`) +VALUES + (16972, 0, 0, 0, 0, 0, 0x40000, 0x3, 0x2, 0, 0, 0, 0, 100.0, 0, 0), + (16974, 0, 0, 0, 0, 0, 0x40000, 0x3, 0x2, 0, 0, 0, 0, 100.0, 0, 0), + (16975, 0, 0, 0, 0, 0, 0x40000, 0x3, 0x2, 0, 0, 0, 0, 100.0, 0, 0); diff --git a/modules/mod-paragon/src/Paragon_SC.cpp b/modules/mod-paragon/src/Paragon_SC.cpp index 6e00f5e..f3a8bf8 100644 --- a/modules/mod-paragon/src/Paragon_SC.cpp +++ b/modules/mod-paragon/src/Paragon_SC.cpp @@ -514,8 +514,136 @@ class spell_paragon_arcane_torrent : public SpellScript } }; +// Predatory Strikes (16972 / 16974 / 16975) for Paragon: re-implements the +// Cataclysm-era proc behavior of the talent so a Paragon's damaging +// finishers (Eviscerate / Envenom / Ferocious Bite / Rip / Rupture) can +// roll Predator's Swiftness (69369) -- the same buff that real druids +// get from the Cata redesign of this talent. Combined with the +// Spell::prepare interception in core (Spell.cpp), 69369 makes the +// Paragon's NEXT Nature-school spell with a base cast time below 10s +// instant cast: Chain Lightning, Lightning Bolt, Healing Touch, Wrath, +// Nourish, etc. -- not just the Druid-family Nature subset that the +// stock SPELLMOD_CASTING_TIME mask on 69369 covers. +// +// Filter logic: +// - Source spell must consume combo points (NeedsComboPoints() — gates +// out non-finisher combo-point builders). +// - "Damaging finisher": SPELL_ATTR1_FINISHING_MOVE_DAMAGE (Eviscerate, +// Envenom, Ferocious Bite, ...) OR a SPELL_ATTR1_FINISHING_MOVE_DURATION +// finisher that applies periodic damage (Rip, Rupture). Duration +// finishers that only heal (Recuperate) or only buff / CC / armor shred +// (Slice and Dice, Savage Roar, Kidney Shot, Maim, Expose Armor) are +// rejected. +// +// Chance per combo point matches the Cataclysm tuning that the user's +// client tooltip text reflects: rank 1 = 3% per CP, rank 2 = 5% per CP, +// rank 3 = 7% per CP. At 5 CP that is 15% / 25% / 35%, capped at 100%. +// +// Combo-point read happens during PROC_SPELL_PHASE_CAST, which fires in +// Spell::cast → Spell::ProcReflectProcs / Unit::ProcDamageAndSpellFor +// BEFORE Spell::_handle_finish_phase clears the player's combo points +// (see Spell.cpp:_handle_finish_phase clearing combo points). So +// player->GetComboPoints() inside HandleProc returns the pre-clear value. +class spell_paragon_predatory_strikes : public AuraScript +{ + PrepareAuraScript(spell_paragon_predatory_strikes); + + static constexpr uint32 SPELL_PARAGON_PREDATORS_SWIFTNESS = 69369; + + bool Validate(SpellInfo const* /*spellInfo*/) override + { + return ValidateSpellInfo({ SPELL_PARAGON_PREDATORS_SWIFTNESS }); + } + + bool CheckProc(ProcEventInfo& eventInfo) + { + SpellInfo const* spellInfo = eventInfo.GetSpellInfo(); + if (!spellInfo || !spellInfo->NeedsComboPoints()) + return false; + + if (spellInfo->HasAttribute(SPELL_ATTR1_FINISHING_MOVE_DAMAGE)) + return true; + + if (spellInfo->HasAttribute(SPELL_ATTR1_FINISHING_MOVE_DURATION)) + { + bool periodicHeal = false; + bool periodicDamage = false; + for (SpellEffectInfo const& eff : spellInfo->Effects) + { + if (eff.Effect != SPELL_EFFECT_APPLY_AURA && eff.Effect != SPELL_EFFECT_APPLY_AREA_AURA_PARTY + && eff.Effect != SPELL_EFFECT_PERSISTENT_AREA_AURA) + continue; + + switch (eff.ApplyAuraName) + { + case SPELL_AURA_PERIODIC_HEAL: + case SPELL_AURA_PERIODIC_HEALTH_FUNNEL: + case SPELL_AURA_OBS_MOD_HEALTH: + periodicHeal = true; + break; + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_PERIODIC_DAMAGE_PERCENT: + case SPELL_AURA_PERIODIC_LEECH: + periodicDamage = true; + break; + default: + break; + } + } + + if (periodicHeal) + return false; + + return periodicDamage; + } + + return false; + } + + void HandleProc(ProcEventInfo& eventInfo) + { + PreventDefaultAction(); + + Unit* actor = eventInfo.GetActor(); + Player* player = actor ? actor->ToPlayer() : nullptr; + if (!player || player->getClass() != CLASS_PARAGON) + return; + + uint8 const cp = player->GetComboPoints(); + if (cp == 0) + return; + + SpellInfo const* talent = GetSpellInfo(); + if (!talent) + return; + + uint32 pctPerCP = 0; + switch (talent->Id) + { + case 16972: pctPerCP = 3; break; + case 16974: pctPerCP = 5; break; + case 16975: pctPerCP = 7; break; + default: + return; + } + + uint32 const chance = std::min(100u, pctPerCP * uint32(cp)); + if (!roll_chance_i(int32(chance))) + return; + + player->CastSpell(player, SPELL_PARAGON_PREDATORS_SWIFTNESS, true); + } + + void Register() override + { + DoCheckProc += AuraCheckProcFn(spell_paragon_predatory_strikes::CheckProc); + OnProc += AuraProcFn(spell_paragon_predatory_strikes::HandleProc); + } +}; + void AddSC_paragon() { new Paragon_PlayerScript(); RegisterSpellScript(spell_paragon_arcane_torrent); + RegisterSpellScript(spell_paragon_predatory_strikes); } diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index 6244841..45f6378 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -4767,6 +4767,36 @@ Respawn.DynamicEscortNPC = 0 Respawn.ForceCompatibilityMode = 0 +# +# Paragon.WildcardFamilyMatching +# Description: Fractured / Paragon class (CLASS_PARAGON, id 12) only. +# When enabled, the SpellFamilyName equality check is +# wildcarded for Paragon characters in proc evaluation +# (SpellMgr::CanSpellTriggerProcOnEvent), talent +# SpellMod application (Player::ApplySpellMod / +# SpellInfo::IsAffectedBySpellMod), and the +# ParagonFamilyMatches() helper used by ad-hoc +# `switch (SpellFamilyName)` listener gates in +# Unit/SpellEffects/SpellAuraEffects code. +# This makes cross-class talent procs and modifiers +# (e.g. Predator's Swiftness 69369 making Shaman +# Chain Lightning instant cast off a Rogue Eviscerate +# finisher) apply to Paragon characters even when the +# listener was authored for one specific class family. +# SpellFamilyFlags / class-mask flag-bit checks still +# run, so listener gates that explicitly opt into a +# subset of spells via flag bits are still respected. +# Stock classes (Warrior / Paladin / etc.) are NEVER +# wildcarded; this only affects players whose class +# id is CLASS_PARAGON. Set to 0 to disable the +# wildcard at runtime (no rebuild required) if a +# regression appears. +# Default: 1 - (Enabled, Paragon characters get cross-class procs/mods) +# 0 - (Disabled, Paragon characters are gated by stock family equality) +# + +Paragon.WildcardFamilyMatching = 1 + # ################################################################################################### diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index e9319e4..e9c3ff6 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -9773,7 +9773,11 @@ bool Player::IsAffectedBySpellmod(SpellInfo const* spellInfo, SpellModifier* mod if (mod->op == SPELLMOD_DURATION && spellInfo->GetDuration() == -1) return false; - return spellInfo->IsAffectedBySpellMod(mod); + // Fractured / Paragon: pass the player owning the modifier aura so the + // SpellFamilyName equality check can be wildcarded for CLASS_PARAGON. + // Stock classes hit the same code path with `this` as a non-Paragon + // unit, which makes IsAffected behave identically to the 2-arg form. + return spellInfo->IsAffectedBySpellMod(mod, this); } template diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 4e84d0a..6b5acb5 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -72,11 +72,25 @@ #include "Util.h" #include "Vehicle.h" #include "World.h" +#include "WorldConfig.h" #include "WorldPacket.h" #include #include #include +// Fractured / Paragon: cross-class wildcard helper used by ad-hoc +// `switch (SpellFamilyName)` listener gates in Unit / SpellEffects / +// SpellAuraEffects. Returns true when the listener is a CLASS_PARAGON +// player and the wildcard config flag is enabled, otherwise falls back +// to strict family-name equality. +bool ParagonFamilyMatches(Unit const* listener, uint32 expectedFamily, uint32 actualFamily) +{ + if (listener && listener->getClass() == CLASS_PARAGON + && sWorld->getBoolConfig(CONFIG_PARAGON_WILDCARD_FAMILY)) + return true; + return expectedFamily == actualFamily; +} + float baseMoveSpeed[MAX_MOVE_TYPE] = { 2.5f, // MOVE_WALK @@ -9702,7 +9716,7 @@ uint32 Unit::SpellHealingBonusTaken(Unit* caster, SpellInfo const* spellProto, u // Nourish cast - 20% bonus if target has Rejuvenation, Regrowth, Lifebloom, or Wild Growth from caster // Glyph of Nourish is handled by spell_dru_nourish script - if (spellProto->SpellFamilyName == SPELLFAMILY_DRUID && spellProto->SpellFamilyFlags[1] & 0x2000000 && caster) + if (ParagonFamilyMatches(caster, SPELLFAMILY_DRUID, spellProto->SpellFamilyName) && spellProto->SpellFamilyFlags[1] & 0x2000000 && caster) { AuraEffectList const& auras = GetAuraEffectsByType(SPELL_AURA_PERIODIC_HEAL); for (AuraEffectList::const_iterator i = auras.begin(); i != auras.end(); ++i) @@ -10421,7 +10435,7 @@ uint32 Unit::MeleeDamageBonusTaken(Unit* attacker, uint32 pdamage, WeaponAttackT uint64 mechanicMask = spellProto->GetAllEffectsMechanicMask(); // Shred, Maul - "Effects which increase Bleed damage also increase Shred damage" - if (spellProto->SpellFamilyName == SPELLFAMILY_DRUID && spellProto->SpellFamilyFlags[0] & 0x00008800) + if (ParagonFamilyMatches(attacker, SPELLFAMILY_DRUID, spellProto->SpellFamilyName) && spellProto->SpellFamilyFlags[0] & 0x00008800) mechanicMask |= (1ULL << MECHANIC_BLEED); if (mechanicMask) diff --git a/src/server/game/Entities/Unit/Unit.h b/src/server/game/Entities/Unit/Unit.h index 1f3984b..c98b154 100644 --- a/src/server/game/Entities/Unit/Unit.h +++ b/src/server/game/Entities/Unit/Unit.h @@ -2268,6 +2268,15 @@ private: ValuesUpdateCache _valuesUpdateCache; }; +// Fractured / Paragon: helper for ad-hoc `switch (SpellFamilyName)` listener +// gates scattered across Unit / SpellEffects / SpellAuraEffects. When the +// listener (i.e. the unit holding the gating talent / aura) is a Paragon +// AND `Paragon.WildcardFamilyMatching` is enabled, accept any source family +// so cross-class procs / bonuses can fire. Stock classes use stock equality. +// Defined inline here so call sites do not need an extra include for World.h +// beyond what they already include via Unit.h's transitive headers. +[[nodiscard]] bool ParagonFamilyMatches(Unit const* listener, uint32 expectedFamily, uint32 actualFamily); + namespace Acore { // Binary predicate for sorting Units based on percent value of a power diff --git a/src/server/game/Spells/Auras/SpellAuras.cpp b/src/server/game/Spells/Auras/SpellAuras.cpp index 3ca928b..5398ca5 100644 --- a/src/server/game/Spells/Auras/SpellAuras.cpp +++ b/src/server/game/Spells/Auras/SpellAuras.cpp @@ -2175,7 +2175,9 @@ uint8 Aura::GetProcEffectMask(AuraApplication* aurApp, ProcEventInfo& eventInfo, return 0; // do checks against db data - if (!sSpellMgr->CanSpellTriggerProcOnEvent(*procEntry, eventInfo)) + // Fractured / Paragon: the unit that owns this aura is the listener; + // pass it through so cross-family procs can match for Paragon players. + if (!sSpellMgr->CanSpellTriggerProcOnEvent(*procEntry, eventInfo, aurApp->GetTarget())) return 0; // check if spell was affected by this aura's spellmod (used by Arcane Potency and similar effects) diff --git a/src/server/game/Spells/Spell.cpp b/src/server/game/Spells/Spell.cpp index 75b2198..7d0c25a 100644 --- a/src/server/game/Spells/Spell.cpp +++ b/src/server/game/Spells/Spell.cpp @@ -3540,6 +3540,32 @@ SpellCastResult Spell::prepare(SpellCastTargets const* targets, AuraEffect const if (m_caster->ToPlayer()->GetCommandStatus(CHEAT_CASTTIME)) m_casttime = 0; + // Fractured / Paragon: cross-class Predator's Swiftness (69369). + // Stock 3.3.5 only ADD_PCT_MODIFIER's the cast time of Druid-family + // Nature spells via class mask, so a Paragon with the buff cannot + // instant-cast Shaman Chain Lightning / Lightning Bolt or any other + // non-Druid Nature spell. The tooltip ("next Nature spell with a + // base cast time below 10 sec becomes instant") expects all-Nature + // behavior; honor that here for CLASS_PARAGON. We deliberately do + // not touch the stock SpellMod path -- real Druids continue to hit + // the existing class-mask code path unchanged. + if (Player* paragonCaster = m_caster->ToPlayer()) + { + constexpr uint32 SPELL_PARAGON_PREDATORY_SWIFTNESS = 69369; + if (m_casttime > 0 + && paragonCaster->getClass() == CLASS_PARAGON + && (m_spellInfo->SchoolMask & SPELL_SCHOOL_MASK_NATURE) + && m_spellInfo->CastTimeEntry + && !m_spellInfo->IsChanneled() + && !HasTriggeredCastFlag(TRIGGERED_FULL_MASK) + && m_spellInfo->CalcCastTime() < 10 * IN_MILLISECONDS + && paragonCaster->HasAura(SPELL_PARAGON_PREDATORY_SWIFTNESS)) + { + m_casttime = 0; + paragonCaster->RemoveAurasDueToSpell(SPELL_PARAGON_PREDATORY_SWIFTNESS); + } + } + // don't allow channeled spells / spells with cast time to be casted while moving // (even if they are interrupted on moving, spells with almost immediate effect get to have their effect processed before movement interrupter kicks in) if ((m_spellInfo->IsChanneled() || m_casttime) && m_caster->IsPlayer() && m_caster->isMoving() && m_spellInfo->InterruptFlags & SPELL_INTERRUPT_FLAG_MOVEMENT && !IsTriggered()) diff --git a/src/server/game/Spells/SpellInfo.cpp b/src/server/game/Spells/SpellInfo.cpp index f2e5d80..bd2ce6f 100644 --- a/src/server/game/Spells/SpellInfo.cpp +++ b/src/server/game/Spells/SpellInfo.cpp @@ -27,6 +27,8 @@ #include "SpellAuraDefines.h" #include "SpellAuraEffects.h" #include "SpellMgr.h" +#include "World.h" +#include "WorldConfig.h" uint32 GetTargetFlagMask(SpellTargetObjectTypes objType) { @@ -1323,11 +1325,26 @@ bool SpellInfo::HasInitialAggro() const } bool SpellInfo::IsAffected(uint32 familyName, flag96 const& familyFlags) const +{ + return IsAffected(familyName, familyFlags, nullptr); +} + +bool SpellInfo::IsAffected(uint32 familyName, flag96 const& familyFlags, + Unit const* listenerOwner) const { if (!familyName) return true; - if (familyName != SpellFamilyName) + // Fractured / Paragon: when the unit that owns the listening proc / + // spellmod aura is a Paragon, accept any source family. The class + // mask flag-bit check below still runs, so listeners that explicitly + // opt into a subset of spells via SpellFamilyFlags / class mask are + // still respected; only the family-name equality gate is wildcarded. + bool const wildcardFamily = listenerOwner + && listenerOwner->getClass() == CLASS_PARAGON + && sWorld->getBoolConfig(CONFIG_PARAGON_WILDCARD_FAMILY); + + if (!wildcardFamily && familyName != SpellFamilyName) return false; if (familyFlags && !(familyFlags & SpellFamilyFlags)) @@ -1342,6 +1359,11 @@ bool SpellInfo::IsAffectedBySpellMods() const } bool SpellInfo::IsAffectedBySpellMod(SpellModifier const* mod) const +{ + return IsAffectedBySpellMod(mod, nullptr); +} + +bool SpellInfo::IsAffectedBySpellMod(SpellModifier const* mod, Unit const* listenerOwner) const { // xinef: dont check duration mod if (mod->op != SPELLMOD_DURATION) @@ -1356,7 +1378,7 @@ bool SpellInfo::IsAffectedBySpellMod(SpellModifier const* mod) const if (!sScriptMgr->OnIsAffectedBySpellModCheck(affectSpell, this, mod)) return true; - return IsAffected(affectSpell->SpellFamilyName, mod->mask); + return IsAffected(affectSpell->SpellFamilyName, mod->mask, listenerOwner); } bool SpellInfo::CanPierceImmuneAura(SpellInfo const* auraSpellInfo) const diff --git a/src/server/game/Spells/SpellInfo.h b/src/server/game/Spells/SpellInfo.h index b87b5e3..5cae034 100644 --- a/src/server/game/Spells/SpellInfo.h +++ b/src/server/game/Spells/SpellInfo.h @@ -494,9 +494,21 @@ public: bool HasInitialAggro() const; [[nodiscard]] bool IsAffected(uint32 familyName, flag96 const& familyFlags) const; + // Fractured / Paragon overload. When `listenerOwner` is a CLASS_PARAGON + // unit and Paragon.WildcardFamilyMatching is enabled, the + // SpellFamilyName equality check is skipped (flag-bit check still runs) + // so cross-class procs / spellmods can react to the spell. Passing + // nullptr (or any non-Paragon unit) reproduces the stock 2-arg + // behavior; the 2-arg form forwards to this overload with nullptr. + [[nodiscard]] bool IsAffected(uint32 familyName, flag96 const& familyFlags, + Unit const* listenerOwner) const; bool IsAffectedBySpellMods() const; bool IsAffectedBySpellMod(SpellModifier const* mod) const; + // Fractured / Paragon overload: pass the player who owns the modifier + // aura so wildcard-family matching can apply when that player is a + // Paragon. Stock callers may forward to this with nullptr. + bool IsAffectedBySpellMod(SpellModifier const* mod, Unit const* listenerOwner) const; bool CanPierceImmuneAura(SpellInfo const* auraSpellInfo) const; bool CanDispelAura(SpellInfo const* auraSpellInfo) const; diff --git a/src/server/game/Spells/SpellMgr.cpp b/src/server/game/Spells/SpellMgr.cpp index 011c8ec..7c4d077 100644 --- a/src/server/game/Spells/SpellMgr.cpp +++ b/src/server/game/Spells/SpellMgr.cpp @@ -842,7 +842,8 @@ SpellProcEntry const* SpellMgr::GetSpellProcEntry(uint32 spellId) const return nullptr; } -bool SpellMgr::CanSpellTriggerProcOnEvent(SpellProcEntry const& procEntry, ProcEventInfo& eventInfo) const +bool SpellMgr::CanSpellTriggerProcOnEvent(SpellProcEntry const& procEntry, ProcEventInfo& eventInfo, + Unit const* procOwner /*= nullptr*/) const { // proc type doesn't match if (!(eventInfo.GetTypeMask() & procEntry.ProcFlags)) @@ -873,7 +874,10 @@ bool SpellMgr::CanSpellTriggerProcOnEvent(SpellProcEntry const& procEntry, ProcE // check spell family name/flags (if set) for spells if (eventInfo.GetTypeMask() & SPELL_PROC_FLAG_MASK) if (SpellInfo const* eventSpellInfo = eventInfo.GetSpellInfo()) - if (!eventSpellInfo->IsAffected(procEntry.SpellFamilyName, procEntry.SpellFamilyMask)) + // Fractured / Paragon: thread the proc-aura owner so a Paragon + // listener accepts cross-family source spells. See + // SpellInfo::IsAffected(family, flags, listenerOwner). + if (!eventSpellInfo->IsAffected(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, procOwner)) return false; // check spell type mask (if set) diff --git a/src/server/game/Spells/SpellMgr.h b/src/server/game/Spells/SpellMgr.h index 32f34b0..15c67d7 100644 --- a/src/server/game/Spells/SpellMgr.h +++ b/src/server/game/Spells/SpellMgr.h @@ -699,7 +699,12 @@ public: // Spell proc table [[nodiscard]] SpellProcEntry const* GetSpellProcEntry(uint32 spellId) const; - bool CanSpellTriggerProcOnEvent(SpellProcEntry const& procEntry, ProcEventInfo& eventInfo) const; + // Fractured / Paragon: `procOwner` is the unit that holds the listening + // proc aura. Passing it lets SpellInfo::IsAffected wildcard the family + // check when the listener is on a CLASS_PARAGON player. Non-Paragon + // owners (or nullptr) reproduce stock behavior exactly. + bool CanSpellTriggerProcOnEvent(SpellProcEntry const& procEntry, ProcEventInfo& eventInfo, + Unit const* procOwner = nullptr) const; // Spell bonus data table [[nodiscard]] SpellBonusEntry const* GetSpellBonusData(uint32 spellId) const; diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp index 0ffd94a..8e710b7 100644 --- a/src/server/game/World/WorldConfig.cpp +++ b/src/server/game/World/WorldConfig.cpp @@ -684,4 +684,10 @@ void WorldConfig::BuildConfigCache() // Achievement SetConfigValue(CONFIG_ACHIEVEMENT_REALM_FIRST_KILL_WINDOW, "Achievement.RealmFirstKillWindow", 60); + + // Fractured / Paragon: cross-class wildcard for SpellFamilyName gating. + // Default ON because the Paragon class is designed around it; flip to 0 + // (no rebuild required) if a regression appears and stock family + // gating needs to be restored without backing out the code. + SetConfigValue(CONFIG_PARAGON_WILDCARD_FAMILY, "Paragon.WildcardFamilyMatching", true); } diff --git a/src/server/game/World/WorldConfig.h b/src/server/game/World/WorldConfig.h index 8e62a1e..7831ddb 100644 --- a/src/server/game/World/WorldConfig.h +++ b/src/server/game/World/WorldConfig.h @@ -495,6 +495,12 @@ enum ServerConfigs CONFIG_NEW_CHAR_STRING, CONFIG_VALIDATE_SKILL_LEARNED_BY_SPELLS, CONFIG_ACHIEVEMENT_REALM_FIRST_KILL_WINDOW, + // Fractured / Paragon: when true, CLASS_PARAGON characters bypass the + // SpellFamilyName equality check in proc / spellmod / aura listener + // gates so cross-class talent procs and modifiers can interact with + // spells learned from other classes (e.g. Predator's Swiftness 69369 + // making Shaman Chain Lightning instant). Stock classes are unaffected. + CONFIG_PARAGON_WILDCARD_FAMILY, MAX_NUM_SERVER_CONFIGS };