diff --git a/src/server/game/Spells/Auras/SpellAuraEffects.cpp b/src/server/game/Spells/Auras/SpellAuraEffects.cpp index ef713a32d..775aab3d5 100644 --- a/src/server/game/Spells/Auras/SpellAuraEffects.cpp +++ b/src/server/game/Spells/Auras/SpellAuraEffects.cpp @@ -1239,6 +1239,29 @@ bool AuraEffect::CheckEffectProc(AuraApplication* aurApp, ProcEventInfo& eventIn if (GetCasterGUID() != eventInfo.GetActor()->GetGUID()) return false; break; + case SPELL_AURA_PROC_TRIGGER_SPELL: + case SPELL_AURA_PROC_TRIGGER_SPELL_WITH_VALUE: + { + // Don't proc extra attacks while already processing extra attack spell + uint32 triggerSpellId = m_spellInfo->Effects[GetEffIndex()].TriggerSpell; + if (SpellInfo const* triggeredSpellInfo = sSpellMgr->GetSpellInfo(triggerSpellId)) + { + if (triggeredSpellInfo->HasEffect(SPELL_EFFECT_ADD_EXTRA_ATTACKS)) + { + uint32 lastExtraAttackSpell = eventInfo.GetActor()->GetLastExtraAttackSpell(); + + // Patch 1.12.0(?) extra attack abilities can no longer chain proc themselves + if (lastExtraAttackSpell == triggerSpellId) + return false; + + // Patch 2.2.0 Sword Specialization (Warrior, Rogue) extra attack can no longer proc additional extra attacks + // 3.3.5 Sword Specialization (Warrior), Hack and Slash (Rogue) + if (lastExtraAttackSpell == SPELL_SWORD_SPECIALIZATION || lastExtraAttackSpell == SPELL_HACK_AND_SLASH) + return false; + } + } + break; + } default: break; } diff --git a/src/test/mocks/ProcChanceTestHelper.h b/src/test/mocks/ProcChanceTestHelper.h index 32a3dac98..41e0dfa38 100644 --- a/src/test/mocks/ProcChanceTestHelper.h +++ b/src/test/mocks/ProcChanceTestHelper.h @@ -282,6 +282,45 @@ public: return false; // Allow proc } + // ============================================================================= + // Extra Attack Chain-Proc Prevention - simulates SpellAuraEffects.cpp:1245-1261 + // ============================================================================= + + /** + * @brief Configuration for simulating extra attack chain-proc prevention + */ + struct ExtraAttackProcConfig + { + bool triggeredSpellHasExtraAttacks = false; // triggeredSpellInfo->HasEffect(SPELL_EFFECT_ADD_EXTRA_ATTACKS) + uint32 triggerSpellId = 0; // m_spellInfo->Effects[GetEffIndex()].TriggerSpell + uint32 lastExtraAttackSpell = 0; // eventInfo.GetActor()->GetLastExtraAttackSpell() + }; + + /** + * @brief Simulate extra attack chain-proc prevention from CheckEffectProc + * Returns true if proc should be blocked + * + * @param config Extra attack proc configuration + * @return true if proc should be blocked + */ + static bool ShouldBlockExtraAttackChainProc(ExtraAttackProcConfig const& config) + { + // Only applies when the triggered spell grants extra attacks + if (!config.triggeredSpellHasExtraAttacks) + return false; + + // Patch 1.12.0(?) extra attack abilities can no longer chain proc themselves + if (config.lastExtraAttackSpell == config.triggerSpellId) + return true; + + // Patch 2.2.0 Sword Specialization (Warrior, Rogue) extra attack can no longer proc additional extra attacks + // 3.3.5 Sword Specialization (Warrior), Hack and Slash (Rogue) + if (config.lastExtraAttackSpell == 16459 || config.lastExtraAttackSpell == 66923) + return true; + + return false; + } + // ============================================================================= // DisableEffectsMask - simulates SpellAuras.cpp:2244-2258 // ============================================================================= diff --git a/src/test/server/game/Spells/ExtraAttackChainProcTest.cpp b/src/test/server/game/Spells/ExtraAttackChainProcTest.cpp new file mode 100644 index 000000000..50e33768f --- /dev/null +++ b/src/test/server/game/Spells/ExtraAttackChainProcTest.cpp @@ -0,0 +1,149 @@ +/* + * 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 . + */ + +/** + * @file ExtraAttackChainProcTest.cpp + * @brief Unit tests for extra attack chain-proc prevention + * + * Tests the logic from SpellAuraEffects.cpp:1245-1261 (CheckEffectProc): + * - Self-chain prevention (same extra attack spell can't proc itself) + * - Cross-chain prevention (Sword Specialization / Hack and Slash block all extra attack procs) + * - Non-blacklisted extra attack spells allow cross-proccing + * - Non-extra-attack procs are unaffected by the guard + */ + +#include "ProcChanceTestHelper.h" +#include "gtest/gtest.h" + +using namespace testing; + +// Use existing enum from Unit.h: SPELL_SWORD_SPECIALIZATIONIALIZATION (16459), SPELL_HACK_AND_SLASH (66923) +constexpr uint32 SPELL_RECKONING = 32746; // Reckoning (Paladin) +constexpr uint32 SPELL_HAND_OF_JUSTICE = 15601; // Hand of Justice extra attack + +class ExtraAttackChainProcTest : public ::testing::Test +{ +protected: + ProcChanceTestHelper::ExtraAttackProcConfig MakeConfig( + bool hasExtraAttacks, uint32 triggerSpellId, uint32 lastExtraAttack) + { + ProcChanceTestHelper::ExtraAttackProcConfig config; + config.triggeredSpellHasExtraAttacks = hasExtraAttacks; + config.triggerSpellId = triggerSpellId; + config.lastExtraAttackSpell = lastExtraAttack; + return config; + } +}; + +// ============================================================================= +// Normal proc (no extra attack in progress) +// ============================================================================= + +TEST_F(ExtraAttackChainProcTest, NormalProc_AllowedWhenNoExtraAttackInProgress) +{ + // lastExtraAttackSpell == 0 means no extra attack is executing + auto config = MakeConfig(true, SPELL_SWORD_SPECIALIZATION, 0); + + EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config)) + << "Extra attack proc should be allowed when no extra attack is in progress"; +} + +// ============================================================================= +// Self-chain prevention +// ============================================================================= + +TEST_F(ExtraAttackChainProcTest, SelfChain_BlockedWhenSameSpell) +{ + // Sword Spec trying to proc during its own extra attack + auto config = MakeConfig(true, SPELL_SWORD_SPECIALIZATION, SPELL_SWORD_SPECIALIZATION); + + EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config)) + << "Extra attack spell should not chain-proc itself"; +} + +// ============================================================================= +// Cross-chain prevention (blacklisted spells) +// ============================================================================= + +TEST_F(ExtraAttackChainProcTest, CrossChain_BlockedBySwordSpecialization) +{ + // Reckoning trying to proc during Sword Spec extra attack + auto config = MakeConfig(true, SPELL_RECKONING, SPELL_SWORD_SPECIALIZATION); + + EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config)) + << "Sword Specialization extra attack should block all other extra attack procs"; +} + +TEST_F(ExtraAttackChainProcTest, CrossChain_BlockedByHackAndSlash) +{ + // Reckoning trying to proc during Hack and Slash extra attack + auto config = MakeConfig(true, SPELL_RECKONING, SPELL_HACK_AND_SLASH); + + EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config)) + << "Hack and Slash extra attack should block all other extra attack procs"; +} + +// ============================================================================= +// Non-blacklisted extra attacks allow cross-proccing +// ============================================================================= + +TEST_F(ExtraAttackChainProcTest, DifferentExtraAttack_AllowedWhenNotBlacklisted) +{ + // Sword Spec trying to proc during Hand of Justice extra attack + // Hand of Justice (15601) is not blacklisted, so cross-proc is allowed + auto config = MakeConfig(true, SPELL_SWORD_SPECIALIZATION, SPELL_HAND_OF_JUSTICE); + + EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config)) + << "Non-blacklisted extra attack spells should allow cross-proccing"; +} + +// ============================================================================= +// Non-extra-attack procs unaffected +// ============================================================================= + +TEST_F(ExtraAttackChainProcTest, NonExtraAttackProc_UnaffectedByExtraAttackState) +{ + // A proc that does NOT grant extra attacks should never be blocked, + // even during Sword Spec extra attack + auto config = MakeConfig(false, 12345, SPELL_SWORD_SPECIALIZATION); + + EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config)) + << "Non-extra-attack procs should be unaffected by extra attack state"; +} + +// ============================================================================= +// Real spell scenarios +// ============================================================================= + +TEST_F(ExtraAttackChainProcTest, Reckoning_SelfChainBlocked) +{ + // Reckoning (32746) trying to proc during its own extra attack + auto config = MakeConfig(true, SPELL_RECKONING, SPELL_RECKONING); + + EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config)) + << "Reckoning should not chain-proc itself"; +} + +TEST_F(ExtraAttackChainProcTest, Reckoning_AllowedDuringHandOfJustice) +{ + // Reckoning trying to proc during Hand of Justice extra attack + // Hand of Justice is not blacklisted, so Reckoning is allowed + auto config = MakeConfig(true, SPELL_RECKONING, SPELL_HAND_OF_JUSTICE); + + EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config)) + << "Reckoning should be allowed during Hand of Justice extra attack"; +}