From 4277ac0b262b229bf798339c77b1254aedfabf1b Mon Sep 17 00:00:00 2001 From: blinkysc <37940565+blinkysc@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:37:37 -0600 Subject: [PATCH] fix(Core/Spells): Fix Rapid Recuperation, Rapid Killing, and auto-generate PROC_ATTR_REQ_SPELLMOD (#24830) Co-authored-by: blinkysc --- .../rev_1771855011429365338.sql | 9 ++ .../game/Spells/Auras/SpellAuraEffects.cpp | 6 - src/server/game/Spells/SpellMgr.cpp | 14 ++ src/server/scripts/Spells/spell_hunter.cpp | 49 ++---- .../game/Spells/SpellProcAttributeTest.cpp | 146 ++++++++++++++++++ 5 files changed, 183 insertions(+), 41 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1771855011429365338.sql diff --git a/data/sql/updates/pending_db_world/rev_1771855011429365338.sql b/data/sql/updates/pending_db_world/rev_1771855011429365338.sql new file mode 100644 index 000000000..4d22aeed6 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1771855011429365338.sql @@ -0,0 +1,9 @@ +-- Fix spell_script_names for spell_hun_rapid_recuperation +-- Script was moved from talent (53228/53232) to periodic mana aura (56654/58882) +DELETE FROM `spell_script_names` WHERE `ScriptName` = 'spell_hun_rapid_recuperation'; +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES +(56654, 'spell_hun_rapid_recuperation'), +(58882, 'spell_hun_rapid_recuperation'); + +-- Remove explicit Inner Focus spell_proc entry (now auto-generated with PROC_ATTR_REQ_SPELLMOD) +DELETE FROM `spell_proc` WHERE `SpellId` = 14751; diff --git a/src/server/game/Spells/Auras/SpellAuraEffects.cpp b/src/server/game/Spells/Auras/SpellAuraEffects.cpp index 77250413f..1b793832b 100644 --- a/src/server/game/Spells/Auras/SpellAuraEffects.cpp +++ b/src/server/game/Spells/Auras/SpellAuraEffects.cpp @@ -6563,12 +6563,6 @@ void AuraEffect::HandlePeriodicTriggerSpellAuraTick(Unit* target, Unit* caster) caster->CastSpell(target, triggerSpellId, false); return; } - // Hunter - Rapid Recuperation - case 56654: - case 58882: - int32 amount = int32(target->GetMaxPower(POWER_MANA) * GetAmount() / 100.0f); - target->CastCustomSpell(target, triggerSpellId, &amount, nullptr, nullptr, true, nullptr, this); - return; } } diff --git a/src/server/game/Spells/SpellMgr.cpp b/src/server/game/Spells/SpellMgr.cpp index 5751e7fec..a179c1963 100644 --- a/src/server/game/Spells/SpellMgr.cpp +++ b/src/server/game/Spells/SpellMgr.cpp @@ -2073,6 +2073,20 @@ void SpellMgr::LoadSpellProcs() if (addTriggerFlag) procEntry.AttributesMask |= PROC_ATTR_TRIGGERED_CAN_PROC; + // Modifier auras with charges should require spellmod validation + if (spellInfo->ProcCharges) + { + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (spellInfo->Effects[i].IsAura(SPELL_AURA_ADD_FLAT_MODIFIER) || + spellInfo->Effects[i].IsAura(SPELL_AURA_ADD_PCT_MODIFIER)) + { + procEntry.AttributesMask |= PROC_ATTR_REQ_SPELLMOD; + break; + } + } + } + // Calculate DisableEffectsMask for effects that shouldn't trigger procs uint32 nonProcMask = 0; for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) diff --git a/src/server/scripts/Spells/spell_hunter.cpp b/src/server/scripts/Spells/spell_hunter.cpp index da98fe256..8a10dede6 100644 --- a/src/server/scripts/Spells/spell_hunter.cpp +++ b/src/server/scripts/Spells/spell_hunter.cpp @@ -73,8 +73,6 @@ enum HunterSpells // Proc system spells SPELL_HUNTER_THRILL_OF_THE_HUNT_MANA = 34720, SPELL_HUNTER_REPLENISHMENT = 57669, - SPELL_HUNTER_RAPID_RECUPERATION_R1 = 56654, - SPELL_HUNTER_RAPID_RECUPERATION_R2 = 58882, SPELL_HUNTER_GLYPH_OF_MEND_PET_HAPPINESS = 57894, SPELL_HUNTER_KILL_COMMAND_HUNTER = 34027, SPELL_HUNTER_RAPID_RECUPERATION_MANA_R1 = 56654, @@ -1403,48 +1401,29 @@ class spell_hun_hunting_party : public AuraScript } }; -// -53228 - Rapid Recuperation +// 56654, 58882 - Rapid Recuperation class spell_hun_rapid_recuperation : public AuraScript { PrepareAuraScript(spell_hun_rapid_recuperation); - bool Validate(SpellInfo const* /*spellInfo*/) override + bool Validate(SpellInfo const* spellInfo) override { - return ValidateSpellInfo({ SPELL_HUNTER_RAPID_RECUPERATION_R1, SPELL_HUNTER_RAPID_RECUPERATION_R2 }); + return ValidateSpellInfo({ spellInfo->Effects[EFFECT_0].TriggerSpell }); } - bool CheckProc(ProcEventInfo& eventInfo) - { - SpellInfo const* procSpell = eventInfo.GetSpellInfo(); - if (!procSpell) - return false; - - // This effect only from Rapid Killing (mana regen) - return (procSpell->SpellFamilyFlags[1] & 0x01000000) != 0; - } - - void HandleProc(ProcEventInfo& /*eventInfo*/) + void HandlePeriodic(AuraEffect const* aurEff) { PreventDefaultAction(); - uint32 triggeredSpell = 0; - switch (GetSpellInfo()->Id) - { - case 53228: // Rank 1 - triggeredSpell = SPELL_HUNTER_RAPID_RECUPERATION_R1; - break; - case 53232: // Rank 2 - triggeredSpell = SPELL_HUNTER_RAPID_RECUPERATION_R2; - break; - default: - return; - } - GetTarget()->CastSpell(GetTarget(), triggeredSpell, true); + + Unit* target = GetTarget(); + uint32 triggerSpellId = GetSpellInfo()->Effects[aurEff->GetEffIndex()].TriggerSpell; + int32 amount = CalculatePct(static_cast(target->GetMaxPower(POWER_MANA)), aurEff->GetAmount()); + target->CastCustomSpell(target, triggerSpellId, &amount, nullptr, nullptr, true, nullptr, aurEff); } void Register() override { - DoCheckProc += AuraCheckProcFn(spell_hun_rapid_recuperation::CheckProc); - OnProc += AuraProcFn(spell_hun_rapid_recuperation::HandleProc); + OnEffectPeriodic += AuraEffectPeriodicFn(spell_hun_rapid_recuperation::HandlePeriodic, EFFECT_0, SPELL_AURA_PERIODIC_TRIGGER_SPELL); } }; @@ -1522,7 +1501,7 @@ class spell_hun_kill_command_pet : public AuraScript } }; -// -53228 - Rapid Recuperation (trigger) +// -53228 - Rapid Recuperation (talent aura) class spell_hun_rapid_recuperation_trigger : public AuraScript { PrepareAuraScript(spell_hun_rapid_recuperation_trigger); @@ -1559,8 +1538,8 @@ class spell_hun_rapid_recuperation_trigger : public AuraScript return; uint8 rank = GetSpellInfo()->GetRank(); - if (rank > 0 && rank <= 2) - GetTarget()->CastSpell(GetTarget(), triggerSpells[rank - 1], true, nullptr, aurEff); + uint32 spellId = triggerSpells[rank - 1]; + eventInfo.GetActor()->CastSpell(eventInfo.GetActor(), spellId, true, nullptr, aurEff); } void Register() override @@ -1676,9 +1655,9 @@ void AddSC_hunter_spell_scripts() RegisterSpellScript(spell_hun_explosive_shot); RegisterSpellScript(spell_hun_thrill_of_the_hunt); RegisterSpellScript(spell_hun_hunting_party); - RegisterSpellScript(spell_hun_rapid_recuperation); RegisterSpellScript(spell_hun_glyph_of_mend_pet); // Proc system scripts + RegisterSpellScript(spell_hun_rapid_recuperation); RegisterSpellScript(spell_hun_kill_command_pet); RegisterSpellScript(spell_hun_piercing_shots); RegisterSpellScript(spell_hun_rapid_recuperation_trigger); diff --git a/src/test/server/game/Spells/SpellProcAttributeTest.cpp b/src/test/server/game/Spells/SpellProcAttributeTest.cpp index 536c105d8..1b6609881 100644 --- a/src/test/server/game/Spells/SpellProcAttributeTest.cpp +++ b/src/test/server/game/Spells/SpellProcAttributeTest.cpp @@ -31,6 +31,7 @@ #include "ProcChanceTestHelper.h" #include "ProcEventInfoHelper.h" +#include "SpellInfoTestHelper.h" #include "AuraStub.h" #include "UnitStub.h" #include "gtest/gtest.h" @@ -135,6 +136,151 @@ TEST_F(SpellProcAttributeTest, ReqSpellmod_AttributeNotSet) EXPECT_FALSE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD); } +// ============================================================================= +// PROC_ATTR_REQ_SPELLMOD Auto-Generation Tests +// Validates that LoadSpellProcs auto-generates REQ_SPELLMOD for modifier +// auras with charges, preventing charge consumption by unrelated spells. +// ============================================================================= + +namespace +{ + // Replicates the auto-generation logic from SpellMgr::LoadSpellProcs + void ApplyAutoGeneratedSpellmodFlag(SpellInfo const* spellInfo, SpellProcEntry& procEntry) + { + if (spellInfo->ProcCharges) + { + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (spellInfo->Effects[i].IsAura(SPELL_AURA_ADD_FLAT_MODIFIER) || + spellInfo->Effects[i].IsAura(SPELL_AURA_ADD_PCT_MODIFIER)) + { + procEntry.AttributesMask |= PROC_ATTR_REQ_SPELLMOD; + break; + } + } + } + } +} + +TEST_F(SpellProcAttributeTest, ReqSpellmod_AutoGen_PctModifierWithCharges) +{ + // Rapid Killing (35099): ADD_PCT_MODIFIER + 1 charge + auto spellInfo = SpellInfoBuilder() + .WithId(35099) + .WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_ADD_PCT_MODIFIER) + .WithProcFlags(PROC_FLAG_DONE_RANGED_AUTO_ATTACK | PROC_FLAG_DONE_SPELL_RANGED_DMG_CLASS) + .WithProcCharges(1) + .BuildUnique(); + + SpellProcEntry procEntry = SpellProcEntryBuilder() + .WithProcFlags(PROC_FLAG_DONE_RANGED_AUTO_ATTACK | PROC_FLAG_DONE_SPELL_RANGED_DMG_CLASS) + .WithCharges(1) + .WithChance(100.0f) + .Build(); + + ApplyAutoGeneratedSpellmodFlag(spellInfo.get(), procEntry); + + EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD); +} + +TEST_F(SpellProcAttributeTest, ReqSpellmod_AutoGen_FlatModifierWithCharges) +{ + // Clearcasting-like: ADD_FLAT_MODIFIER + charges + auto spellInfo = SpellInfoBuilder() + .WithId(12345) + .WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_ADD_FLAT_MODIFIER) + .WithProcCharges(2) + .BuildUnique(); + + SpellProcEntry procEntry = SpellProcEntryBuilder() + .WithCharges(2) + .WithChance(100.0f) + .Build(); + + ApplyAutoGeneratedSpellmodFlag(spellInfo.get(), procEntry); + + EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD); +} + +TEST_F(SpellProcAttributeTest, ReqSpellmod_AutoGen_NoCharges_NotSet) +{ + // Modifier aura without charges should NOT get the flag + auto spellInfo = SpellInfoBuilder() + .WithId(12345) + .WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_ADD_PCT_MODIFIER) + .WithProcCharges(0) + .BuildUnique(); + + SpellProcEntry procEntry = SpellProcEntryBuilder() + .WithCharges(0) + .WithChance(100.0f) + .Build(); + + ApplyAutoGeneratedSpellmodFlag(spellInfo.get(), procEntry); + + EXPECT_FALSE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD); +} + +TEST_F(SpellProcAttributeTest, ReqSpellmod_AutoGen_NonModifierWithCharges_NotSet) +{ + // Non-modifier aura with charges should NOT get the flag + auto spellInfo = SpellInfoBuilder() + .WithId(12345) + .WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_PROC_TRIGGER_SPELL) + .WithProcCharges(1) + .BuildUnique(); + + SpellProcEntry procEntry = SpellProcEntryBuilder() + .WithCharges(1) + .WithChance(100.0f) + .Build(); + + ApplyAutoGeneratedSpellmodFlag(spellInfo.get(), procEntry); + + EXPECT_FALSE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD); +} + +TEST_F(SpellProcAttributeTest, ReqSpellmod_AutoGen_ModifierOnSecondEffect) +{ + // Modifier on effect index 1, not 0 + auto spellInfo = SpellInfoBuilder() + .WithId(12345) + .WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_PROC_TRIGGER_SPELL) + .WithEffect(1, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_ADD_FLAT_MODIFIER) + .WithProcCharges(1) + .BuildUnique(); + + SpellProcEntry procEntry = SpellProcEntryBuilder() + .WithCharges(1) + .WithChance(100.0f) + .Build(); + + ApplyAutoGeneratedSpellmodFlag(spellInfo.get(), procEntry); + + EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD); +} + +TEST_F(SpellProcAttributeTest, ReqSpellmod_AutoGen_PreservesExistingAttributes) +{ + // Should OR with existing attributes, not replace + auto spellInfo = SpellInfoBuilder() + .WithId(12345) + .WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_ADD_PCT_MODIFIER) + .WithProcCharges(1) + .BuildUnique(); + + SpellProcEntry procEntry = SpellProcEntryBuilder() + .WithCharges(1) + .WithChance(100.0f) + .WithAttributesMask(PROC_ATTR_TRIGGERED_CAN_PROC) + .Build(); + + ApplyAutoGeneratedSpellmodFlag(spellInfo.get(), procEntry); + + EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD); + EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_TRIGGERED_CAN_PROC); +} + // ============================================================================= // PROC_ATTR_USE_STACKS_FOR_CHARGES (0x10) Tests // =============================================================================