fix(Core/Spells): Fix Arcane Potency consumed by same cast that triggered it (#24997)

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
Co-authored-by: ariel- <ariel-@users.noreply.github.com>
This commit is contained in:
blinkysc
2026-03-09 13:32:50 -05:00
committed by GitHub
parent 900be15597
commit f66685c374
5 changed files with 367 additions and 23 deletions

View File

@@ -0,0 +1,4 @@
DELETE FROM `spell_proc` WHERE `SpellId` IN (57529, 57531);
INSERT INTO `spell_proc` (`SpellId`, `SchoolMask`, `SpellFamilyName`, `SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`, `ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`, `AttributesMask`, `DisableEffectsMask`, `ProcsPerMinute`, `Chance`, `Cooldown`, `Charges`) VALUES
(57529, 0, 3, 0x61401035, 0x00001000, 0, 0, 0, 1, 0, 0x8, 0, 0, 0, 0, 0),
(57531, 0, 3, 0x61401035, 0x00001000, 0, 0, 0, 1, 0, 0x8, 0, 0, 0, 0, 0);

View File

@@ -8977,7 +8977,22 @@ float Unit::SpellDoneCritChance(Unit const* /*victim*/, SpellInfo const* spellPr
crit_chance = 0.0f;
// For other schools
else if (IsPlayer())
{
crit_chance = GetFloatValue(static_cast<uint16>(PLAYER_SPELL_CRIT_PERCENTAGE1) + GetFirstSchoolInMask(schoolMask));
// register aura mod, this is needed for Arcane Potency
if (Spell* spell = ToPlayer()->m_spellModTakingSpell)
{
Unit::AuraEffectList const& critAuras = GetAuraEffectsByType(SPELL_AURA_MOD_SPELL_CRIT_CHANCE);
for (AuraEffect const* aurEff : critAuras)
spell->m_appliedMods.insert(aurEff->GetBase());
Unit::AuraEffectList const& critSchoolAuras = GetAuraEffectsByType(SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL);
for (AuraEffect const* aurEff : critSchoolAuras)
if (aurEff->GetMiscValue() & schoolMask)
spell->m_appliedMods.insert(aurEff->GetBase());
}
}
else
{
crit_chance = (float)m_baseSpellCritChance;

View File

@@ -1348,28 +1348,6 @@ void Aura::HandleAuraSpecificMods(AuraApplication const* aurApp, Unit* caster, b
}
switch (GetId())
{
case 12536: // Clearcasting
case 12043: // Presence of Mind
// Arcane Potency
if (AuraEffect const* aurEff = caster->GetAuraEffect(SPELL_AURA_DUMMY, SPELLFAMILY_MAGE, 2120, 0))
{
uint32 spellId = 0;
switch (aurEff->GetId())
{
case 31571:
spellId = 57529;
break;
case 31572:
spellId = 57531;
break;
default:
LOG_ERROR("spells.aura", "Aura::HandleAuraSpecificMods: Unknown rank of Arcane Potency ({}) found", aurEff->GetId());
}
if (spellId)
caster->CastSpell(caster, spellId, true);
}
break;
case 44544: // Fingers of Frost
{
// See if we already have the indicator aura. If not, create one.

View File

@@ -1963,7 +1963,8 @@ void SpellMgr::LoadSpellProcs()
if (!spellInfo->Effects[i].IsAura())
continue;
if (spellInfo->Effects[i].ApplyAuraName == SPELL_AURA_ADD_PCT_MODIFIER || spellInfo->Effects[i].ApplyAuraName == SPELL_AURA_ADD_FLAT_MODIFIER)
if (spellInfo->Effects[i].ApplyAuraName == SPELL_AURA_ADD_PCT_MODIFIER || spellInfo->Effects[i].ApplyAuraName == SPELL_AURA_ADD_FLAT_MODIFIER
|| spellInfo->Effects[i].ApplyAuraName == SPELL_AURA_MOD_SPELL_CRIT_CHANCE || spellInfo->Effects[i].ApplyAuraName == SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL)
{
found = true;
break;

View File

@@ -0,0 +1,346 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* @file SpellProcArcanePotencyTest.cpp
* @brief Unit tests for Arcane Potency proc behavior
*
* Arcane Potency (57529/57531) buffs should only be consumed by the spell
* that was actually affected by the crit bonus, not by the same cast that
* triggered them. This is achieved via PROC_ATTR_REQ_SPELLMOD (0x08) which
* requires the proccing aura to be in the triggering spell's m_appliedMods.
*
* The crit aura registration in SpellDoneCritChance ensures that only spells
* whose crit chance was actually modified by Arcane Potency will have it in
* their m_appliedMods set.
*
* References:
* - TrinityCore commit 81f16b201b
* - ChromieCraft issue #9092
*/
#include "AuraScriptTestFramework.h"
#include "SpellMgr.h"
#include "gtest/gtest.h"
using namespace testing;
namespace
{
// Arcane Potency buff spell IDs
constexpr uint32 ARCANE_POTENCY_R1 = 57529;
constexpr uint32 ARCANE_POTENCY_R2 = 57531;
// SpellFamilyMask from spell_proc for Arcane Potency
constexpr uint32 AP_FAMILY_MASK0 = 0x61401035;
constexpr uint32 AP_FAMILY_MASK1 = 0x00001000;
constexpr uint32 AP_FAMILY_MASK2 = 0;
// Mage spell flags that SHOULD match Arcane Potency's mask
// Fireball: bit 0
constexpr uint32 FIREBALL_FLAG0 = 0x00000001;
// Frostfire Bolt: bit 2 + Mask1 bit 12
constexpr uint32 FROSTFIRE_BOLT_FLAG0 = 0x00000004;
constexpr uint32 FROSTFIRE_BOLT_FLAG1 = 0x00001000;
// Fire Blast: bit 4
constexpr uint32 FIRE_BLAST_FLAG0 = 0x00000010;
// Frostbolt: bit 5
constexpr uint32 FROSTBOLT_FLAG0 = 0x00000020;
// Arcane Explosion: bit 12
constexpr uint32 ARCANE_EXPLOSION_FLAG0 = 0x00001000;
// Scorch: bit 22
constexpr uint32 SCORCH_FLAG0 = 0x00400000;
// Arcane Blast: bit 29
constexpr uint32 ARCANE_BLAST_FLAG0 = 0x20000000;
// Mage spell flags that should NOT match
// Arcane Missiles: bit 11
constexpr uint32 ARCANE_MISSILES_FLAG0 = 0x00000800;
// Ice Lance: bit 17
constexpr uint32 ICE_LANCE_FLAG0 = 0x00020000;
// Arcane Barrage: Mask1 bit 15
constexpr uint32 ARCANE_BARRAGE_FLAG1 = 0x00008000;
/**
* Build the Arcane Potency proc entry matching our spell_proc SQL.
*/
SpellProcEntry BuildArcanePotencyProcEntry()
{
return SpellProcEntryBuilder()
.WithSpellFamilyName(SPELLFAMILY_MAGE)
.WithSpellFamilyMask(flag96(AP_FAMILY_MASK0, AP_FAMILY_MASK1, AP_FAMILY_MASK2))
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
.Build();
}
}
// =============================================================================
// Proc Entry Configuration Tests
// =============================================================================
class ArcanePotencyProcTest : public AuraScriptProcTestFixture
{
protected:
void SetUp() override
{
AuraScriptProcTestFixture::SetUp();
}
};
TEST_F(ArcanePotencyProcTest, ProcEntry_HasReqSpellmodAttribute)
{
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD)
<< "Arcane Potency must have PROC_ATTR_REQ_SPELLMOD to prevent "
"same-cast consumption";
}
TEST_F(ArcanePotencyProcTest, ProcEntry_CastPhaseOnly)
{
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_EQ(procEntry.SpellPhaseMask, PROC_SPELL_PHASE_CAST)
<< "Arcane Potency should only proc on CAST phase";
}
TEST_F(ArcanePotencyProcTest, ProcEntry_MageFamily)
{
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_EQ(procEntry.SpellFamilyName, SPELLFAMILY_MAGE);
}
TEST_F(ArcanePotencyProcTest, ProcEntry_BothRanksIdentical)
{
// Both ranks should use the same proc configuration
auto r1 = BuildArcanePotencyProcEntry();
auto r2 = BuildArcanePotencyProcEntry();
EXPECT_EQ(r1.SpellFamilyName, r2.SpellFamilyName);
EXPECT_TRUE(r1.SpellFamilyMask == r2.SpellFamilyMask);
EXPECT_EQ(r1.SpellPhaseMask, r2.SpellPhaseMask);
EXPECT_EQ(r1.AttributesMask, r2.AttributesMask);
}
// =============================================================================
// SpellFamilyMask Matching Tests - Spells That SHOULD Match
// =============================================================================
TEST_F(ArcanePotencyProcTest, FamilyMask_Fireball_Matches)
{
auto* spell = CreateSpellInfo(133, SPELLFAMILY_MAGE, FIREBALL_FLAG0);
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
}
TEST_F(ArcanePotencyProcTest, FamilyMask_Frostbolt_Matches)
{
auto* spell = CreateSpellInfo(116, SPELLFAMILY_MAGE, FROSTBOLT_FLAG0);
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
}
TEST_F(ArcanePotencyProcTest, FamilyMask_ArcaneBlast_Matches)
{
auto* spell = CreateSpellInfo(30451, SPELLFAMILY_MAGE, ARCANE_BLAST_FLAG0);
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
}
TEST_F(ArcanePotencyProcTest, FamilyMask_ArcaneExplosion_Matches)
{
auto* spell = CreateSpellInfo(1449, SPELLFAMILY_MAGE, ARCANE_EXPLOSION_FLAG0);
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
}
TEST_F(ArcanePotencyProcTest, FamilyMask_FrostfireBolt_Matches)
{
auto* spell = CreateSpellInfo(44614, SPELLFAMILY_MAGE,
FROSTFIRE_BOLT_FLAG0, FROSTFIRE_BOLT_FLAG1);
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
}
TEST_F(ArcanePotencyProcTest, FamilyMask_FireBlast_Matches)
{
auto* spell = CreateSpellInfo(2136, SPELLFAMILY_MAGE, FIRE_BLAST_FLAG0);
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
}
TEST_F(ArcanePotencyProcTest, FamilyMask_Scorch_Matches)
{
auto* spell = CreateSpellInfo(2948, SPELLFAMILY_MAGE, SCORCH_FLAG0);
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
}
// =============================================================================
// SpellFamilyMask Matching Tests - Spells That Should NOT Match
// =============================================================================
TEST_F(ArcanePotencyProcTest, FamilyMask_ArcaneMissiles_DoesNotMatch)
{
auto* spell = CreateSpellInfo(5143, SPELLFAMILY_MAGE, ARCANE_MISSILES_FLAG0);
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
}
TEST_F(ArcanePotencyProcTest, FamilyMask_IceLance_DoesNotMatch)
{
auto* spell = CreateSpellInfo(30455, SPELLFAMILY_MAGE, ICE_LANCE_FLAG0);
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
}
TEST_F(ArcanePotencyProcTest, FamilyMask_ArcaneBarrage_DoesNotMatch)
{
auto* spell = CreateSpellInfo(44425, SPELLFAMILY_MAGE, 0, ARCANE_BARRAGE_FLAG1);
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
}
TEST_F(ArcanePotencyProcTest, FamilyMask_NonMageSpell_DoesNotMatch)
{
// Warrior spell with matching flags should NOT match due to family check
auto* spell = CreateSpellInfo(6343, /*SPELLFAMILY_WARRIOR*/4, FIREBALL_FLAG0);
auto procEntry = BuildArcanePotencyProcEntry();
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
}
// =============================================================================
// Proc Phase Tests - CAST phase filtering
// =============================================================================
TEST_F(ArcanePotencyProcTest, Phase_TriggersOnCast)
{
// Test phase filtering in isolation (no family filter)
auto procEntry = SpellProcEntryBuilder()
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
.Build();
auto scenario = ProcScenarioBuilder()
.OnSpellDamage()
.OnCast();
EXPECT_PROC_TRIGGERS(procEntry, scenario);
}
TEST_F(ArcanePotencyProcTest, Phase_DoesNotTriggerOnHit)
{
auto procEntry = SpellProcEntryBuilder()
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
.Build();
auto scenario = ProcScenarioBuilder()
.OnSpellDamage()
.OnHit()
.WithNormalHit();
EXPECT_PROC_DOES_NOT_TRIGGER(procEntry, scenario);
}
TEST_F(ArcanePotencyProcTest, Phase_DoesNotTriggerOnFinish)
{
auto procEntry = SpellProcEntryBuilder()
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
.Build();
auto scenario = ProcScenarioBuilder()
.OnSpellDamage()
.OnFinish();
EXPECT_PROC_DOES_NOT_TRIGGER(procEntry, scenario);
}
// =============================================================================
// PROC_ATTR_REQ_SPELLMOD Charge Gating Tests
// =============================================================================
TEST_F(ArcanePotencyProcTest, ReqSpellmod_AuraWithCharges_BlocksWithoutAppliedMod)
{
// When PROC_ATTR_REQ_SPELLMOD is set and aura uses charges,
// GetProcEffectMask should return 0 if aura is not in m_appliedMods.
// We test the prerequisite: that the proc entry is configured correctly
// so the charge-gating code path in GetProcEffectMask is reachable.
auto procEntry = BuildArcanePotencyProcEntry();
auto aura = AuraStubBuilder()
.WithId(ARCANE_POTENCY_R1)
.WithCharges(1)
.Build();
// The aura should be using charges (required for REQ_SPELLMOD gating)
EXPECT_TRUE(aura->IsUsingCharges());
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD);
}
TEST_F(ArcanePotencyProcTest, ReqSpellmod_AuraWithoutCharges_SkipsCheck)
{
// If aura has no charges, PROC_ATTR_REQ_SPELLMOD check is skipped
// (no charges to consume = no need for spellmod gating)
auto procEntry = BuildArcanePotencyProcEntry();
auto aura = AuraStubBuilder()
.WithId(ARCANE_POTENCY_R1)
.WithCharges(0)
.Build();
EXPECT_FALSE(aura->IsUsingCharges());
// Even though PROC_ATTR_REQ_SPELLMOD is set, the condition
// (IsUsingCharges() || USE_STACKS_FOR_CHARGES) would be false
// so the spellmod check would be skipped
}
// =============================================================================
// SpellFamilyMask Bitmask Validation
// =============================================================================
TEST_F(ArcanePotencyProcTest, MaskBits_CorrectBitsSet)
{
// Verify the exact bits in our SpellFamilyMask0
// Bit 0: Fireball
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00000001);
// Bit 2: Frostfire Bolt
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00000004);
// Bit 4: Fire Blast
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00000010);
// Bit 5: Frostbolt
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00000020);
// Bit 12: Arcane Explosion
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00001000);
// Bit 22: Scorch
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00400000);
// Bit 29: Arcane Blast
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x20000000);
// Mask1: Bit 12 (Frostfire Bolt secondary)
EXPECT_TRUE(AP_FAMILY_MASK1 & 0x00001000);
}
TEST_F(ArcanePotencyProcTest, MaskBits_CorrectBitsNotSet)
{
// Bit 11: Arcane Missiles - should NOT be set
EXPECT_FALSE(AP_FAMILY_MASK0 & 0x00000800);
// Bit 17: Ice Lance - should NOT be set
EXPECT_FALSE(AP_FAMILY_MASK0 & 0x00020000);
// Mask1 bit 15: Arcane Barrage - should NOT be set
EXPECT_FALSE(AP_FAMILY_MASK1 & 0x00008000);
}