Paragon: narrow weapon bypass; cascade talent ranks; Savage Defense

* Weapon-narrow: replace blanket EquippedItemSubClassMask bypass with an
  explicit allowlist (IsParagonWeaponSubclassWildcardSpell, currently
  Maelstrom Weapon 51528 only). Applied at all three gates: Player::
  HasItemFitToSpellRequirements, Player::CheckAttackFitToAuraRequirement,
  and Aura::IsProcTriggeredOnEvent. Maelstrom Weapon still procs from any
  weapon for Paragon, but Hack and Slash / Sword / Mace Specialization
  remain correctly weapon-gated.
* Talent ability-rank cascade: TeachLevelGatedAbilityChainNoPanel +
  CascadeRanksForTalentLearnSpellEffects walk the SPELL_EFFECT_LEARN_SPELL
  chain a talent rank grants and learn each rank up to the player's level.
  Wired into HandleCommit (on talent purchase) and OnPlayerLevelChanged
  (on level-up). Fixes Mangle (and any future LEARN_SPELL talent) being
  stuck at rank 1 because Player::learnSkillRewardedSpells is intentionally
  disabled for Paragon's class skill lines.
* Riding-skill gate for flight forms (IsParagonSpellAllowedByRidingSkill):
  Flight Form (33943) requires Expert Riding (34090); Swift Flight Form
  (40120) requires Artisan Riding (34091). Applied in PanelLearnSpellChain
  and TeachLevelGatedAbilityChainNoPanel so the cascade can't push past a
  rank the player isn't trained for. Also backticks `rank` in the level-up
  query (MySQL reserved word).
* Savage Defense (62600) on the panel: the SpellData bake's blanket
  SPELL_ATTR0_PASSIVE filter dropped Savage Defense even though Druid
  trainer 33 sells it at level 40. Bake (in fractured-tooling) now carves
  out a small PASSIVE_TRAINER_ALLOWLIST and the regenerated
  paragon_spell_ae_cost.sql + 2026_05_11_05.sql migration surface it on
  the Druid Feral spell tab.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Docker Build
2026-05-11 19:09:25 -04:00
parent 7c57abd69f
commit 999f7e94bd
7 changed files with 228 additions and 31 deletions
+23 -17
View File
@@ -7143,14 +7143,16 @@ bool Player::CheckAttackFitToAuraRequirement(WeaponAttackType attackType, AuraEf
if (spellInfo->EquippedItemClass == -1)
return true;
// Fractured / Paragon: cross-class wildcard relaxes weapon-class subclass
// gates on per-swing proc matches. A Paragon's talent list spans every
// class so a stock weapon-subclass mask (e.g. Maelstrom Weapon's
// axe/mace/staff/fist/dagger restriction) excludes weapons the player
// can legitimately wield. Accept any equipped weapon in attackType slot
// when listener is a Paragon AND the spell gates on ITEM_CLASS_WEAPON;
// ITEM_CLASS_ARMOR (shield) gates still enforce the original mask.
if (spellInfo->EquippedItemClass == ITEM_CLASS_WEAPON && IsParagonWildcardCaller(this))
// Fractured / Paragon: cross-class wildcard relaxes the weapon-subclass
// gate ONLY for the curated allowlist of cross-class proc talents
// (currently just Maelstrom Weapon 51528-51532). Weapon-specialization
// talents like Sword Specialization, Mace Specialization, Hack and
// Slash, Two-Handed Weapon Specialization etc. deliberately stay
// weapon-gated for Paragon -- the player picks a weapon and the
// matching specialization passive activates, same as any class.
if (spellInfo->EquippedItemClass == ITEM_CLASS_WEAPON
&& IsParagonWildcardCaller(this)
&& IsParagonWeaponSubclassWildcardSpell(spellInfo->Id))
return GetWeaponForAttack(attackType, true) != nullptr;
Item* item = GetWeaponForAttack(attackType, true);
@@ -12591,15 +12593,19 @@ bool Player::HasItemFitToSpellRequirements(SpellInfo const* spellInfo, Item cons
if (spellInfo->EquippedItemClass < 0)
return true;
// Fractured / Paragon: cross-class wildcard relaxes weapon-class subclass
// gates so passive talent auras (e.g. Maelstrom Weapon talents 51528-51532)
// attach for any equipped weapon, not just the talent's restrictive
// subclass mask. Mirrors CheckAttackFitToAuraRequirement so per-swing
// proc match agrees with talent-attach time. Still requires *some* weapon
// to be equipped (otherwise unarmed Paragons would auto-activate every
// weapon-gated talent in the game). ITEM_CLASS_ARMOR (shield) is left
// alone -- shield-gated talents still need an actual shield.
if (spellInfo->EquippedItemClass == ITEM_CLASS_WEAPON && IsParagonWildcardCaller(this))
// Fractured / Paragon: cross-class wildcard relaxes the weapon-subclass
// gate ONLY for the curated allowlist of cross-class proc talents
// (currently just Maelstrom Weapon 51528-51532) so the passive talent
// aura attaches when the player wields a non-stock weapon. Weapon-
// specialization talents (Sword/Mace Specialization, Hack and Slash,
// Two-Handed Weapon Specialization, etc.) deliberately stay weapon-
// gated -- they're meant to bind to a specific weapon type. Still
// requires *some* weapon equipped (unarmed Paragons don't auto-activate
// weapon talents). ITEM_CLASS_ARMOR (shield) is left alone -- shield-
// gated talents still need an actual shield.
if (spellInfo->EquippedItemClass == ITEM_CLASS_WEAPON
&& IsParagonWildcardCaller(this)
&& IsParagonWeaponSubclassWildcardSpell(spellInfo->Id))
{
for (uint8 i = EQUIPMENT_SLOT_MAINHAND; i < EQUIPMENT_SLOT_TABARD; ++i)
if (Item const* item = GetUseableItemByPos(INVENTORY_SLOT_BAG_0, i))
+23
View File
@@ -103,6 +103,29 @@ bool ParagonFamilyMatches(Unit const* listener, uint32 expectedFamily, uint32 ac
return expectedFamily == actualFamily;
}
bool IsParagonWeaponSubclassWildcardSpell(uint32 spellId)
{
if (!spellId)
return false;
// Resolve to the first-rank id so callers can pass any rank.
uint32 firstRankId = spellId;
if (SpellInfo const* info = sSpellMgr->GetSpellInfo(spellId))
if (SpellInfo const* first = info->GetFirstRankSpell())
firstRankId = first->Id;
switch (firstRankId)
{
// Maelstrom Weapon (talent ranks 51528 / 51529 / 51530 / 51531 / 51532).
// Cross-class proc talent that should fire off any equipped weapon
// for a Paragon caster (1H sword, polearm, staff, fist, dagger, etc.).
case 51528:
return true;
default:
return false;
}
}
float baseMoveSpeed[MAX_MOVE_TYPE] =
{
2.5f, // MOVE_WALK
+13
View File
@@ -2287,6 +2287,19 @@ private:
// beyond what they already include via Unit.h's transitive headers.
[[nodiscard]] bool ParagonFamilyMatches(Unit const* listener, uint32 expectedFamily, uint32 actualFamily);
// Fractured / Paragon: returns true iff `spellId` is on the curated allowlist
// of cross-class proc talents whose `EquippedItemSubClassMask` should be
// bypassed for Paragon casters (e.g. Maelstrom Weapon ranks 51528-51532).
// Used by `Player::HasItemFitToSpellRequirements`,
// `Player::CheckAttackFitToAuraRequirement`, and
// `Aura::IsProcTriggeredOnEvent` to let these specific talents fire from
// any equipped weapon while keeping weapon-specialization talents
// (Sword Specialization, Mace Specialization, Hack and Slash, Two-Handed
// Weapon Specialization, etc.) gated by their stock weapon-class mask.
// The allowlist is matched against `SpellInfo::GetFirstRankSpell()`'s id
// so all ranks of a talent are covered by listing the rank-1 id.
[[nodiscard]] bool IsParagonWeaponSubclassWildcardSpell(uint32 spellId);
namespace Acore
{
// Binary predicate for sorting Units based on percent value of a power
+10 -13
View File
@@ -2255,20 +2255,17 @@ uint8 Aura::GetProcEffectMask(AuraApplication* aurApp, ProcEventInfo& eventInfo,
return 0;
if (!item->IsFitToSpellRequirements(GetSpellInfo()))
{
// Fractured / Paragon: cross-class wildcard relaxes weapon-
// class subclass gates on per-event proc evaluation. This
// mirrors Player::CheckAttackFitToAuraRequirement and
// Player::HasItemFitToSpellRequirements -- without this
// third bypass, the talent attaches (HasItemFit lets it),
// the per-swing match accepts the weapon (CheckAttackFit
// lets it), but IsProcTriggeredOnEvent still kills the
// proc here for any weapon outside the talent's stock
// subclass mask (e.g. Maelstrom Weapon on a Paragon
// wielding a 1H sword or polearm). Restricted to
// ITEM_CLASS_WEAPON so shield-gated talents still need
// an actual shield.
// Fractured / Paragon: cross-class wildcard relaxes the
// weapon-subclass gate ONLY for the curated allowlist of
// cross-class proc talents (currently just Maelstrom Weapon
// 51528-51532). Weapon-specialization talents (Sword/Mace
// Specialization, Hack and Slash, Two-Handed Weapon
// Specialization, etc.) deliberately stay weapon-gated for
// Paragon. Restricted to ITEM_CLASS_WEAPON so shield-gated
// talents still need an actual shield.
if (!(GetSpellInfo()->EquippedItemClass == ITEM_CLASS_WEAPON
&& IsParagonWildcardCaller(target)))
&& IsParagonWildcardCaller(target)
&& IsParagonWeaponSubclassWildcardSpell(GetSpellInfo()->Id)))
return 0;
}
}