fix(Core/Spells): Fix Rapid Recuperation, Rapid Killing, and auto-generate PROC_ATTR_REQ_SPELLMOD (#24830)

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
This commit is contained in:
blinkysc 2026-02-23 12:37:37 -06:00 committed by GitHub
parent 57119764a7
commit 4277ac0b26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 183 additions and 41 deletions

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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)

View file

@ -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<int32>(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);

View file

@ -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
// =============================================================================