mirror of
https://github.com/mod-playerbots/azerothcore-wotlk.git
synced 2026-03-08 10:10:28 +00:00
fix(Core/Spells): Add proc chain guard and TAKEN auto-trigger logic (#24966)
Co-authored-by: blinkysc <blinkysc@users.noreply.github.com> Co-authored-by: ariel- <ariel-@users.noreply.github.com>
This commit is contained in:
@@ -2029,6 +2029,24 @@ void SpellMgr::LoadSpellProcs()
|
||||
|
||||
if (!addTriggerFlag && isAlwaysTriggeredAura[auraName])
|
||||
addTriggerFlag = true;
|
||||
|
||||
// Many proc auras with taken procFlag mask don't have
|
||||
// attribute "can proc with triggered" — they should
|
||||
// proc nevertheless (e.g. mage armor spells with
|
||||
// judgement)
|
||||
if (!addTriggerFlag
|
||||
&& (spellInfo->ProcFlags & TAKEN_HIT_PROC_FLAG_MASK))
|
||||
{
|
||||
switch (auraName)
|
||||
{
|
||||
case SPELL_AURA_PROC_TRIGGER_SPELL:
|
||||
case SPELL_AURA_PROC_TRIGGER_DAMAGE:
|
||||
addTriggerFlag = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
|
||||
@@ -523,6 +523,54 @@ public:
|
||||
// Per-aura check: aura itself suppresses cascading
|
||||
if (config.auraHasDisableProcAttr)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TAKEN Auto-Trigger Logic - simulates SpellMgr.cpp:2033-2049
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief Configuration for simulating the TAKEN auto-trigger logic
|
||||
* from SpellMgr::LoadSpellProcs() auto-generation
|
||||
*/
|
||||
struct TakenAutoTriggerConfig
|
||||
{
|
||||
uint32 procFlags = 0; // SpellInfo::ProcFlags
|
||||
uint32 auraName = 0; // Effect's ApplyAuraName
|
||||
bool isAlwaysTriggeredAura = false; // Already set by isAlwaysTriggeredAura[]
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Simulate the TAKEN auto-trigger logic from SpellMgr::LoadSpellProcs()
|
||||
*
|
||||
* During auto-generation of proc entries, TAKEN-proc auras with
|
||||
* SPELL_AURA_PROC_TRIGGER_SPELL or SPELL_AURA_PROC_TRIGGER_DAMAGE
|
||||
* should get PROC_ATTR_TRIGGERED_CAN_PROC set automatically.
|
||||
*
|
||||
* @param config Configuration describing the aura
|
||||
* @return true if addTriggerFlag should be set
|
||||
*/
|
||||
static bool ShouldAutoAddTriggeredCanProc(TakenAutoTriggerConfig const& config)
|
||||
{
|
||||
// If already marked as always-triggered, keep it
|
||||
if (config.isAlwaysTriggeredAura)
|
||||
return true;
|
||||
|
||||
// TAKEN auto-trigger: TAKEN proc flags + PROC_TRIGGER_SPELL/DAMAGE
|
||||
if (config.procFlags & TAKEN_HIT_PROC_FLAG_MASK)
|
||||
{
|
||||
switch (config.auraName)
|
||||
{
|
||||
case SPELL_AURA_PROC_TRIGGER_SPELL:
|
||||
case SPELL_AURA_PROC_TRIGGER_DAMAGE:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -641,4 +689,94 @@ private:
|
||||
std::unique_ptr<AuraStub> _aura;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Simulates the proc chain guard logic from Unit::TriggerAurasProcOnEvent
|
||||
*
|
||||
* Tracks the m_procDeep counter to verify that:
|
||||
* - TRIGGERED_DISALLOW_PROC_EVENTS on the triggering spell disables procs
|
||||
* for all auras in the container
|
||||
* - SPELL_ATTR3_INSTANT_TARGET_PROCS on individual auras disables procs
|
||||
* only during that specific aura's TriggerProcOnEvent call
|
||||
* - The counter is properly balanced (returns to 0 after function exits)
|
||||
*/
|
||||
class ProcChainGuardSimulator
|
||||
{
|
||||
public:
|
||||
struct AuraConfig
|
||||
{
|
||||
uint32 spellId = 0;
|
||||
bool hasInstantTargetProcs = false; // SPELL_ATTR3_INSTANT_TARGET_PROCS
|
||||
bool isRemoved = false; // AuraApplication::GetRemoveMode()
|
||||
};
|
||||
|
||||
struct ProcRecord
|
||||
{
|
||||
uint32 spellId;
|
||||
bool canProcDuringTrigger; // CanProc() state when TriggerProcOnEvent fires
|
||||
int32 procDeepDuringTrigger; // m_procDeep value during trigger
|
||||
};
|
||||
|
||||
ProcChainGuardSimulator() : _procDeep(0) {}
|
||||
|
||||
/**
|
||||
* @brief Simulate Unit::TriggerAurasProcOnEvent
|
||||
*
|
||||
* @param triggeringSpellHasDisallowProcEvents Whether the triggering spell
|
||||
* has TRIGGERED_DISALLOW_PROC_EVENTS cast flag
|
||||
* @param auras List of auras in the proc container
|
||||
*/
|
||||
void SimulateTriggerAurasProc(
|
||||
bool triggeringSpellHasDisallowProcEvents,
|
||||
std::vector<AuraConfig> const& auras)
|
||||
{
|
||||
_records.clear();
|
||||
|
||||
bool const disableProcs = triggeringSpellHasDisallowProcEvents;
|
||||
if (disableProcs)
|
||||
SetCantProc(true);
|
||||
|
||||
for (auto const& aura : auras)
|
||||
{
|
||||
if (aura.isRemoved)
|
||||
continue;
|
||||
|
||||
if (aura.hasInstantTargetProcs)
|
||||
SetCantProc(true);
|
||||
|
||||
// Record CanProc() state during TriggerProcOnEvent
|
||||
_records.push_back({
|
||||
aura.spellId,
|
||||
CanProc(),
|
||||
_procDeep
|
||||
});
|
||||
|
||||
if (aura.hasInstantTargetProcs)
|
||||
SetCantProc(false);
|
||||
}
|
||||
|
||||
if (disableProcs)
|
||||
SetCantProc(false);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<ProcRecord> const& GetRecords() const
|
||||
{
|
||||
return _records;
|
||||
}
|
||||
|
||||
[[nodiscard]] int32 GetProcDeep() const { return _procDeep; }
|
||||
[[nodiscard]] bool CanProc() const { return _procDeep == 0; }
|
||||
|
||||
private:
|
||||
void SetCantProc(bool apply)
|
||||
{
|
||||
if (apply)
|
||||
++_procDeep;
|
||||
else
|
||||
--_procDeep;
|
||||
}
|
||||
|
||||
int32 _procDeep;
|
||||
std::vector<ProcRecord> _records;
|
||||
};
|
||||
|
||||
#endif // AZEROTHCORE_PROC_CHANCE_TEST_HELPER_H
|
||||
|
||||
600
src/test/server/game/Spells/SpellProcChainGuardTest.cpp
Normal file
600
src/test/server/game/Spells/SpellProcChainGuardTest.cpp
Normal file
@@ -0,0 +1,600 @@
|
||||
/*
|
||||
* 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 SpellProcChainGuardTest.cpp
|
||||
* @brief Unit tests for proc chain guard and TAKEN auto-trigger logic
|
||||
*
|
||||
* Tests two fixes ported from TrinityCore:
|
||||
*
|
||||
* 1. Proc chain guard (Unit::TriggerAurasProcOnEvent):
|
||||
* - TRIGGERED_DISALLOW_PROC_EVENTS on triggering spell blocks all
|
||||
* cascading procs during the proc event
|
||||
* - SPELL_ATTR3_INSTANT_TARGET_PROCS on individual auras blocks
|
||||
* cascading procs during that specific aura's proc trigger
|
||||
* - Prevents infinite proc loops between reactive damage auras
|
||||
* (e.g. Molten Armor <-> Eye for an Eye)
|
||||
*
|
||||
* 2. TAKEN auto-trigger (SpellMgr::LoadSpellProcs auto-generation):
|
||||
* - TAKEN-proc auras with SPELL_AURA_PROC_TRIGGER_SPELL or
|
||||
* SPELL_AURA_PROC_TRIGGER_DAMAGE automatically get
|
||||
* PROC_ATTR_TRIGGERED_CAN_PROC so they can proc from
|
||||
* triggered spells (e.g. Mage Armor proccing from Judgement)
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
// =============================================================================
|
||||
// TAKEN Auto-Trigger Logic Tests
|
||||
// Simulates SpellMgr.cpp:2033-2049 auto-generation
|
||||
// =============================================================================
|
||||
|
||||
class TakenAutoTriggerTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
};
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, TakenProcTriggerSpell_SetsTriggeredCanProc)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "TAKEN proc + PROC_TRIGGER_SPELL should auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, TakenProcTriggerDamage_SetsTriggeredCanProc)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_SPELL_MAGIC_DMG_CLASS_NEG;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_DAMAGE;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "TAKEN proc + PROC_TRIGGER_DAMAGE should auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, TakenProcOtherAura_DoesNotSetTriggeredCanProc)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_DUMMY;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "TAKEN proc + DUMMY aura should NOT auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, DoneProcTriggerSpell_DoesNotSetTriggeredCanProc)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_DONE_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "DONE-only proc flags should NOT trigger TAKEN auto-add logic";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, NoProcFlags_DoesNotSetTriggeredCanProc)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = 0;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "Zero proc flags should NOT auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, AlwaysTriggeredAura_StaysTrue)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = 0; // No TAKEN flags
|
||||
config.auraName = SPELL_AURA_DUMMY;
|
||||
config.isAlwaysTriggeredAura = true;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "isAlwaysTriggeredAura should keep addTriggerFlag = true";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, TakenDamage_WithProcTriggerSpell_SetsFlag)
|
||||
{
|
||||
// PROC_FLAG_TAKEN_DAMAGE is in TAKEN_HIT_PROC_FLAG_MASK
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_DAMAGE;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "PROC_FLAG_TAKEN_DAMAGE + PROC_TRIGGER_SPELL should set flag";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, TakenPeriodic_WithProcTriggerDamage_SetsFlag)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_PERIODIC;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_DAMAGE;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "PROC_FLAG_TAKEN_PERIODIC + PROC_TRIGGER_DAMAGE should set flag";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, MixedDoneAndTaken_WithProcTriggerSpell_SetsFlag)
|
||||
{
|
||||
// Both DONE and TAKEN flags present - TAKEN mask still matches
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_DONE_MELEE_AUTO_ATTACK
|
||||
| PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "Mixed DONE+TAKEN flags should still trigger TAKEN auto-add";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, TakenProcModAura_DoesNotSetTriggeredCanProc)
|
||||
{
|
||||
// SPELL_AURA_ADD_FLAT_MODIFIER is not PROC_TRIGGER_SPELL/DAMAGE
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_ADD_FLAT_MODIFIER;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "TAKEN proc + modifier aura should NOT auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real spell scenarios for TAKEN auto-trigger
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, Scenario_MoltenArmor_TakenAutoTrigger)
|
||||
{
|
||||
// Molten Armor (30482): TAKEN_MELEE_AUTO_ATTACK + PROC_TRIGGER_DAMAGE
|
||||
// Note: Molten Armor has an explicit spell_proc entry so auto-gen
|
||||
// is skipped, but the logic should still apply if it didn't
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK
|
||||
| PROC_FLAG_TAKEN_SPELL_MELEE_DMG_CLASS;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_DAMAGE;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "Molten Armor-like aura should auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, Scenario_Reckoning_TakenAutoTrigger)
|
||||
{
|
||||
// Reckoning (20177): TAKEN_MELEE_AUTO_ATTACK + PROC_TRIGGER_SPELL
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "Reckoning-like aura should auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, Scenario_Redoubt_TakenAutoTrigger)
|
||||
{
|
||||
// Redoubt (20127): TAKEN_MELEE_AUTO_ATTACK + PROC_TRIGGER_SPELL
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "Redoubt-like aura should auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Integration: TAKEN auto-trigger affects triggered spell filtering
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, AutoGenTriggeredCanProc_AllowsTriggeredSpells)
|
||||
{
|
||||
// Verify that when TAKEN auto-trigger sets addTriggerFlag,
|
||||
// the resulting proc entry with PROC_ATTR_TRIGGERED_CAN_PROC
|
||||
// allows triggered spells through the filter
|
||||
|
||||
// Step 1: Auto-trigger logic says yes
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig autoConfig;
|
||||
autoConfig.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
autoConfig.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
ASSERT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(autoConfig));
|
||||
|
||||
// Step 2: Build proc entry with the auto-added attribute
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK)
|
||||
.WithAttributesMask(PROC_ATTR_TRIGGERED_CAN_PROC)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Step 3: Verify triggered spells pass through the filter
|
||||
ProcChanceTestHelper::TriggeredSpellConfig trigConfig;
|
||||
trigConfig.isTriggered = true; // e.g. Judgement damage is triggered
|
||||
trigConfig.auraHasCanProcFromProcs = false;
|
||||
trigConfig.spellHasNotAProc = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
trigConfig, procEntry, PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK))
|
||||
<< "Auto-generated TRIGGERED_CAN_PROC should allow triggered spells";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, WithoutAutoTrigger_TriggeredSpellsBlocked)
|
||||
{
|
||||
// Without the TAKEN auto-trigger, DONE-only proc auras would block
|
||||
// triggered spells (no TRIGGERED_CAN_PROC set)
|
||||
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig autoConfig;
|
||||
autoConfig.procFlags = PROC_FLAG_DONE_MELEE_AUTO_ATTACK; // DONE only
|
||||
autoConfig.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
ASSERT_FALSE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(autoConfig));
|
||||
|
||||
// Build proc entry WITHOUT TRIGGERED_CAN_PROC
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithAttributesMask(0) // No TRIGGERED_CAN_PROC
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Triggered spells should be blocked
|
||||
ProcChanceTestHelper::TriggeredSpellConfig trigConfig;
|
||||
trigConfig.isTriggered = true;
|
||||
trigConfig.auraHasCanProcFromProcs = false;
|
||||
trigConfig.spellHasNotAProc = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
trigConfig, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "Without TRIGGERED_CAN_PROC, triggered spells should be blocked";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Proc Chain Guard Tests - simulates Unit::TriggerAurasProcOnEvent
|
||||
// =============================================================================
|
||||
|
||||
class ProcChainGuardTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
|
||||
ProcChainGuardSimulator _sim;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// TRIGGERED_DISALLOW_PROC_EVENTS behavior
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(ProcChainGuardTest, DisallowProcEvents_BlocksAllAuras)
|
||||
{
|
||||
// When triggering spell has TRIGGERED_DISALLOW_PROC_EVENTS,
|
||||
// all auras should fire with CanProc() == false
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = false},
|
||||
{.spellId = 200, .hasInstantTargetProcs = false},
|
||||
{.spellId = 300, .hasInstantTargetProcs = false},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/true, auras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 3u);
|
||||
|
||||
for (auto const& rec : records)
|
||||
{
|
||||
EXPECT_FALSE(rec.canProcDuringTrigger)
|
||||
<< "Aura " << rec.spellId
|
||||
<< " should have CanProc()=false with DISALLOW_PROC_EVENTS";
|
||||
EXPECT_EQ(rec.procDeepDuringTrigger, 1)
|
||||
<< "m_procDeep should be 1 for all auras";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, DisallowProcEvents_CounterBalanced)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/true, auras);
|
||||
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "m_procDeep should return to 0 after function exits";
|
||||
EXPECT_TRUE(_sim.CanProc())
|
||||
<< "CanProc() should be true after function exits";
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, NoDisallowProcEvents_AllAurasCanProc)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = false},
|
||||
{.spellId = 200, .hasInstantTargetProcs = false},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 2u);
|
||||
|
||||
for (auto const& rec : records)
|
||||
{
|
||||
EXPECT_TRUE(rec.canProcDuringTrigger)
|
||||
<< "Aura " << rec.spellId
|
||||
<< " should have CanProc()=true without DISALLOW_PROC_EVENTS";
|
||||
EXPECT_EQ(rec.procDeepDuringTrigger, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// SPELL_ATTR3_INSTANT_TARGET_PROCS per-aura behavior
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(ProcChainGuardTest, InstantTargetProcs_BlocksDuringSpecificAura)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = false},
|
||||
{.spellId = 200, .hasInstantTargetProcs = true}, // This one blocks
|
||||
{.spellId = 300, .hasInstantTargetProcs = false},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 3u);
|
||||
|
||||
// Aura 100: normal, can proc
|
||||
EXPECT_TRUE(records[0].canProcDuringTrigger)
|
||||
<< "First aura (no INSTANT_TARGET_PROCS) should allow procs";
|
||||
EXPECT_EQ(records[0].procDeepDuringTrigger, 0);
|
||||
|
||||
// Aura 200: has INSTANT_TARGET_PROCS, blocked
|
||||
EXPECT_FALSE(records[1].canProcDuringTrigger)
|
||||
<< "Aura with INSTANT_TARGET_PROCS should block procs during its trigger";
|
||||
EXPECT_EQ(records[1].procDeepDuringTrigger, 1);
|
||||
|
||||
// Aura 300: normal again, can proc (counter was decremented)
|
||||
EXPECT_TRUE(records[2].canProcDuringTrigger)
|
||||
<< "Next aura after INSTANT_TARGET_PROCS should allow procs again";
|
||||
EXPECT_EQ(records[2].procDeepDuringTrigger, 0);
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, InstantTargetProcs_CounterBalanced)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = true},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "m_procDeep should return to 0 after INSTANT_TARGET_PROCS aura";
|
||||
EXPECT_TRUE(_sim.CanProc());
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, MultipleInstantTargetProcs_EachIndependent)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = true},
|
||||
{.spellId = 200, .hasInstantTargetProcs = true},
|
||||
{.spellId = 300, .hasInstantTargetProcs = true},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 3u);
|
||||
|
||||
// Each aura should independently block during its own trigger
|
||||
for (auto const& rec : records)
|
||||
{
|
||||
EXPECT_FALSE(rec.canProcDuringTrigger)
|
||||
<< "Aura " << rec.spellId
|
||||
<< " with INSTANT_TARGET_PROCS should block during trigger";
|
||||
EXPECT_EQ(rec.procDeepDuringTrigger, 1)
|
||||
<< "Each aura should increment to exactly 1 (not accumulate)";
|
||||
}
|
||||
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "Counter should be balanced after all auras processed";
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Combined: DISALLOW_PROC_EVENTS + INSTANT_TARGET_PROCS
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(ProcChainGuardTest, Combined_DisallowAndInstantTarget_StackCorrectly)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = false},
|
||||
{.spellId = 200, .hasInstantTargetProcs = true},
|
||||
{.spellId = 300, .hasInstantTargetProcs = false},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/true, auras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 3u);
|
||||
|
||||
// Aura 100: disableProcs active, procDeep=1
|
||||
EXPECT_FALSE(records[0].canProcDuringTrigger);
|
||||
EXPECT_EQ(records[0].procDeepDuringTrigger, 1);
|
||||
|
||||
// Aura 200: disableProcs + INSTANT_TARGET_PROCS, procDeep=2
|
||||
EXPECT_FALSE(records[1].canProcDuringTrigger);
|
||||
EXPECT_EQ(records[1].procDeepDuringTrigger, 2)
|
||||
<< "DISALLOW_PROC_EVENTS + INSTANT_TARGET_PROCS should stack to 2";
|
||||
|
||||
// Aura 300: back to just disableProcs, procDeep=1
|
||||
EXPECT_FALSE(records[2].canProcDuringTrigger);
|
||||
EXPECT_EQ(records[2].procDeepDuringTrigger, 1);
|
||||
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "Counter should be balanced after combined scenario";
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Removed aura handling
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(ProcChainGuardTest, RemovedAura_SkippedInLoop)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = false, .isRemoved = false},
|
||||
{.spellId = 200, .hasInstantTargetProcs = true, .isRemoved = true},
|
||||
{.spellId = 300, .hasInstantTargetProcs = false, .isRemoved = false},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 2u)
|
||||
<< "Removed aura should be skipped";
|
||||
|
||||
EXPECT_EQ(records[0].spellId, 100u);
|
||||
EXPECT_EQ(records[1].spellId, 300u);
|
||||
|
||||
// The INSTANT_TARGET_PROCS aura was removed, so it shouldn't affect
|
||||
// the counter for the next aura
|
||||
EXPECT_TRUE(records[1].canProcDuringTrigger);
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, RemovedAuraWithInstantProcs_DoesNotAffectCounter)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = true, .isRemoved = true},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
EXPECT_EQ(_sim.GetRecords().size(), 0u);
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "Removed aura should not touch the proc deep counter";
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Empty container
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(ProcChainGuardTest, EmptyContainer_NoEffect)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras;
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
EXPECT_EQ(_sim.GetRecords().size(), 0u);
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0);
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, EmptyContainer_WithDisallowProc_StillBalanced)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras;
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/true, auras);
|
||||
|
||||
EXPECT_EQ(_sim.GetRecords().size(), 0u);
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "Even with disableProcs and empty container, counter should balance";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real spell scenarios for proc chain guard
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ProcChainGuardTest, Scenario_MoltenArmorVsEyeForAnEye)
|
||||
{
|
||||
// Molten Armor (43046) has SPELL_ATTR3_INSTANT_TARGET_PROCS
|
||||
// When Molten Armor deals fire damage to an attacker, that damage
|
||||
// should not trigger the attacker's reactive procs (Eye for an Eye)
|
||||
// back on the mage, preventing infinite ping-pong
|
||||
|
||||
// Mage's perspective: paladin hits mage, mage's auras proc
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> mageAuras = {
|
||||
{.spellId = 43046, .hasInstantTargetProcs = true}, // Molten Armor
|
||||
};
|
||||
|
||||
// The paladin's melee hit is NOT TRIGGERED_DISALLOW_PROC_EVENTS
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, mageAuras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 1u);
|
||||
|
||||
// During Molten Armor's proc trigger, CanProc() is false
|
||||
// This means the fire damage it deals cannot trigger further procs
|
||||
EXPECT_FALSE(records[0].canProcDuringTrigger)
|
||||
<< "Molten Armor proc should block cascading procs (prevents EfaE loop)";
|
||||
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "Counter balanced after Molten Armor scenario";
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, Scenario_SealOfRighteousness_TriggeredDamage)
|
||||
{
|
||||
// SoR bonus damage is triggered with TRIGGERED_DISALLOW_PROC_EVENTS
|
||||
// It should not trigger the target's reactive damage auras back
|
||||
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> targetAuras = {
|
||||
{.spellId = 43046, .hasInstantTargetProcs = true}, // Molten Armor
|
||||
{.spellId = 12345, .hasInstantTargetProcs = false}, // Some other aura
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/true, targetAuras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 2u);
|
||||
|
||||
// All auras should see CanProc() = false
|
||||
for (auto const& rec : records)
|
||||
{
|
||||
EXPECT_FALSE(rec.canProcDuringTrigger)
|
||||
<< "All target auras should be blocked when SoR damage"
|
||||
<< " has DISALLOW_PROC_EVENTS";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, Scenario_NormalMeleeHit_AurasCanProc)
|
||||
{
|
||||
// A normal melee swing should allow all auras to proc normally
|
||||
// (no DISALLOW_PROC_EVENTS, no INSTANT_TARGET_PROCS)
|
||||
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> targetAuras = {
|
||||
{.spellId = 20177, .hasInstantTargetProcs = false}, // Reckoning
|
||||
{.spellId = 20127, .hasInstantTargetProcs = false}, // Redoubt
|
||||
{.spellId = 16958, .hasInstantTargetProcs = false}, // Blood Craze
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, targetAuras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 3u);
|
||||
|
||||
for (auto const& rec : records)
|
||||
{
|
||||
EXPECT_TRUE(rec.canProcDuringTrigger)
|
||||
<< "Aura " << rec.spellId
|
||||
<< " should proc normally from a regular melee hit";
|
||||
EXPECT_EQ(rec.procDeepDuringTrigger, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user