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 <cursoragent@cursor.com>
This commit is contained in:
Docker Build
2026-05-11 01:24:50 -04:00
parent b408c8a95d
commit a1c9172beb
14 changed files with 337 additions and 9 deletions
@@ -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
#
###################################################################################################
+5 -1
View File
@@ -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 <class T>
+16 -2
View File
@@ -72,11 +72,25 @@
#include "Util.h"
#include "Vehicle.h"
#include "World.h"
#include "WorldConfig.h"
#include "WorldPacket.h"
#include <algorithm>
#include <cmath>
#include <limits>
// 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)
+9
View File
@@ -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
+3 -1
View File
@@ -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)
+26
View File
@@ -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())
+24 -2
View File
@@ -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
+12
View File
@@ -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;
+6 -2
View File
@@ -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)
+6 -1
View File
@@ -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;
+6
View File
@@ -684,4 +684,10 @@ void WorldConfig::BuildConfigCache()
// Achievement
SetConfigValue<uint32>(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<bool>(CONFIG_PARAGON_WILDCARD_FAMILY, "Paragon.WildcardFamilyMatching", true);
}
+6
View File
@@ -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
};