/* * 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 . */ #include "ProcEventInfoHelper.h" #include "SpellInfoTestHelper.h" #include "SpellMgr.h" #include "WorldMock.h" #include "gtest/gtest.h" #include "gmock/gmock.h" using namespace testing; /** * @brief Test fixture for SpellMgr proc tests * * Tests the CanSpellTriggerProcOnEvent function and related proc logic. */ class SpellProcTest : public ::testing::Test { protected: void SetUp() override { _originalWorld = sWorld.release(); _worldMock = new NiceMock(); sWorld.reset(_worldMock); static std::string emptyString; ON_CALL(*_worldMock, GetDataPath()).WillByDefault(ReturnRef(emptyString)); } void TearDown() override { IWorld* currentWorld = sWorld.release(); delete currentWorld; _worldMock = nullptr; sWorld.reset(_originalWorld); _originalWorld = nullptr; // Clean up any SpellInfo objects we created for (auto* spellInfo : _spellInfos) delete spellInfo; _spellInfos.clear(); } // Helper to create and track SpellInfo objects for cleanup SpellInfo* CreateSpellInfo(uint32 id = 1, uint32 familyName = 0, uint32 familyFlag0 = 0, uint32 familyFlag1 = 0, uint32 familyFlag2 = 0) { auto* spellInfo = SpellInfoBuilder() .WithId(id) .WithSpellFamilyName(familyName) .WithSpellFamilyFlags(familyFlag0, familyFlag1, familyFlag2) .Build(); _spellInfos.push_back(spellInfo); return spellInfo; } IWorld* _originalWorld = nullptr; NiceMock* _worldMock = nullptr; std::vector _spellInfos; }; // ============================================================================= // ProcFlags Tests - Basic proc flag matching // ============================================================================= TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_ProcFlagsMatch) { // Setup: Create a proc entry that triggers on melee auto attacks auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK) .Build(); // Create ProcEventInfo with matching type mask auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_NORMAL) .Build(); // Should match EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_ProcFlagsNoMatch) { // Setup: Create a proc entry that triggers on melee auto attacks auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK) .Build(); // Create ProcEventInfo with different type mask (ranged instead of melee) auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_RANGED_AUTO_ATTACK) .WithHitMask(PROC_HIT_NORMAL) .Build(); // Should not match EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_MultipleProcFlagsPartialMatch) { // Setup: Create a proc entry that triggers on melee OR ranged auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK | PROC_FLAG_DONE_RANGED_AUTO_ATTACK) .Build(); // Create ProcEventInfo with only melee auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_NORMAL) .Build(); // Should match (partial match is OK - it's an OR relationship) EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } // ============================================================================= // Kill/Death Event Tests - These always trigger regardless of other conditions // ============================================================================= TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_KillEventAlwaysProcs) { // Setup: Create a proc entry for kill events auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_KILL) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_KILL) .Build(); // Kill events should always trigger EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_KilledEventAlwaysProcs) { auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_KILLED) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_KILLED) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_DeathEventAlwaysProcs) { auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DEATH) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DEATH) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } // ============================================================================= // HitMask Tests - Test hit type filtering // ============================================================================= TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_HitMaskCriticalMatch) { auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_CRITICAL) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_CRITICAL) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_HitMaskCriticalNoMatch) { // Proc entry requires critical hit auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_CRITICAL) .Build(); // Event is a normal hit auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_NORMAL) .Build(); EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_HitMaskDefaultForDone) { // When HitMask is 0, default for DONE procs is NORMAL | CRITICAL | ABSORB auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK) .WithHitMask(0) // Default .Build(); // Normal hit should work with default mask auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_NORMAL) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_HitMaskDefaultForTaken) { // When HitMask is 0, default for TAKEN procs is NORMAL | CRITICAL auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK) .WithHitMask(0) // Default .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_CRITICAL) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_HitMaskMissNoMatch) { // Miss should not trigger default hit mask auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK) .WithHitMask(0) // Default allows NORMAL | CRITICAL | ABSORB .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_MISS) .Build(); EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_HitMaskDodge) { // Explicitly require dodge auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_DODGE) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_DODGE) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_HitMaskParry) { auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_PARRY) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_PARRY) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_HitMaskBlock) { auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_BLOCK) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_BLOCK) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } // ============================================================================= // SpellTypeMask Tests - Damage vs Heal vs Other // ============================================================================= TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SpellTypeMaskDamage) { auto* spellInfo = CreateSpellInfo(1); // Create DamageInfo for the test DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .WithDamageInfo(&damageInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SpellTypeMaskHeal) { auto* spellInfo = CreateSpellInfo(1); // Create HealInfo with the spell info so GetSpellInfo() works HealInfo healInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_HOLY); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS) .WithSpellTypeMask(PROC_SPELL_TYPE_HEAL) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS) .WithSpellTypeMask(PROC_SPELL_TYPE_HEAL) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .WithHealInfo(&healInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SpellTypeMaskNoMatch) { // Proc requires heal but event is damage auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS) .WithSpellTypeMask(PROC_SPELL_TYPE_HEAL) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS) .WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE) // Mismatch .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .Build(); EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } // ============================================================================= // SpellPhaseMask Tests - Cast vs Hit vs Finish // ============================================================================= TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SpellPhaseMaskCast) { auto* spellInfo = CreateSpellInfo(1); DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_CAST) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_CAST) .WithDamageInfo(&damageInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SpellPhaseMaskHit) { auto* spellInfo = CreateSpellInfo(1); DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .WithDamageInfo(&damageInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SpellPhaseMaskNoMatch) { auto* spellInfo = CreateSpellInfo(1); DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE); // Proc requires cast phase auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_CAST) .Build(); // Event is hit phase auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .WithDamageInfo(&damageInfo) .Build(); EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_CastPhaseWithExplicitHitMaskCrit) { // Nature's Grace scenario: CAST phase + explicit HitMask for crit // Crit is pre-calculated for travel-time spells auto* spellInfo = CreateSpellInfo(1); DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_NATURE, SPELL_DIRECT_DAMAGE); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_CAST) .WithHitMask(PROC_HIT_CRITICAL) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_CAST) .WithHitMask(PROC_HIT_CRITICAL) .WithDamageInfo(&damageInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_CastPhaseWithExplicitHitMaskNoCrit) { // CAST phase + explicit HitMask requires crit, but spell didn't crit auto* spellInfo = CreateSpellInfo(1); DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_NATURE, SPELL_DIRECT_DAMAGE); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_CAST) .WithHitMask(PROC_HIT_CRITICAL) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_CAST) .WithHitMask(PROC_HIT_NORMAL) // No crit .WithDamageInfo(&damageInfo) .Build(); EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_CastPhaseWithDefaultHitMask) { // CAST phase + HitMask=0 should skip HitMask check (old behavior) auto* spellInfo = CreateSpellInfo(1); DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_NATURE, SPELL_DIRECT_DAMAGE); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_CAST) .WithHitMask(0) // Default - no explicit HitMask .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_CAST) .WithHitMask(PROC_HIT_NORMAL) // Doesn't matter - HitMask check skipped .WithDamageInfo(&damageInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } // ============================================================================= // Combined Condition Tests // ============================================================================= TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_AllConditionsMatch) { auto* spellInfo = CreateSpellInfo(1); DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_CRITICAL) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_CRITICAL) .WithDamageInfo(&damageInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_OneConditionFails) { auto* spellInfo = CreateSpellInfo(1); DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_CRITICAL) // Requires crit .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) // But we got normal hit .WithDamageInfo(&damageInfo) .Build(); EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } // ============================================================================= // Edge Cases // ============================================================================= TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_ZeroProcFlags) { // Zero proc flags should never match anything auto procEntry = SpellProcEntryBuilder() .WithProcFlags(0) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_NORMAL) .Build(); EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_PeriodicDamage) { auto* spellInfo = CreateSpellInfo(1); DamageInfo damageInfo(nullptr, nullptr, 50, spellInfo, SPELL_SCHOOL_MASK_SHADOW, SPELL_DIRECT_DAMAGE); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_PERIODIC) .WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_PERIODIC) .WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .WithDamageInfo(&damageInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_TakenDamage) { auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_TAKEN_DAMAGE) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_TAKEN_DAMAGE) .WithHitMask(PROC_HIT_NORMAL) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } // ============================================================================= // SpellFamilyName/SpellFamilyFlags Tests - Class-specific proc matching // ============================================================================= TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SpellFamilyNameMatch) { // Create a Mage spell (SpellFamilyName = SPELLFAMILY_MAGE = 3) auto* spellInfo = SpellInfoBuilder() .WithId(133) // Fireball .WithSpellFamilyName(SPELLFAMILY_MAGE) .Build(); _spellInfos.push_back(spellInfo); DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE); // Proc entry requires Mage spells auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellFamilyName(SPELLFAMILY_MAGE) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .WithDamageInfo(&damageInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SpellFamilyNameNoMatch) { // Create a Warlock spell but proc requires Mage auto* spellInfo = SpellInfoBuilder() .WithId(686) // Shadow Bolt .WithSpellFamilyName(SPELLFAMILY_WARLOCK) .Build(); _spellInfos.push_back(spellInfo); DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_SHADOW, SPELL_DIRECT_DAMAGE); // Proc entry requires Mage spells auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellFamilyName(SPELLFAMILY_MAGE) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .WithDamageInfo(&damageInfo) .Build(); EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SpellFamilyFlagsMatch) { // Create a Paladin Holy Light spell with specific family flags auto* spellInfo = SpellInfoBuilder() .WithId(635) // Holy Light .WithSpellFamilyName(SPELLFAMILY_PALADIN) .WithSpellFamilyFlags(0x80000000, 0, 0) // Example flag for Holy Light .Build(); _spellInfos.push_back(spellInfo); HealInfo healInfo(nullptr, nullptr, 500, spellInfo, SPELL_SCHOOL_MASK_HOLY); // Proc entry requires specific Paladin family flag auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS) .WithSpellFamilyName(SPELLFAMILY_PALADIN) .WithSpellFamilyMask(flag96(0x80000000, 0, 0)) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .WithHealInfo(&healInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SpellFamilyFlagsNoMatch) { // Create a Paladin spell with different family flags auto* spellInfo = SpellInfoBuilder() .WithId(19750) // Flash of Light .WithSpellFamilyName(SPELLFAMILY_PALADIN) .WithSpellFamilyFlags(0x40000000, 0, 0) // Different flag .Build(); _spellInfos.push_back(spellInfo); HealInfo healInfo(nullptr, nullptr, 300, spellInfo, SPELL_SCHOOL_MASK_HOLY); // Proc entry requires different family flag auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS) .WithSpellFamilyName(SPELLFAMILY_PALADIN) .WithSpellFamilyMask(flag96(0x80000000, 0, 0)) // Wants Holy Light flag .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .WithHealInfo(&healInfo) .Build(); EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SpellFamilyNameZeroAcceptsAll) { // When SpellFamilyName is 0, it should accept any spell family auto* spellInfo = SpellInfoBuilder() .WithId(100) .WithSpellFamilyName(SPELLFAMILY_DRUID) .Build(); _spellInfos.push_back(spellInfo); DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_NATURE, SPELL_DIRECT_DAMAGE); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellFamilyName(0) // Accept any family .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .WithDamageInfo(&damageInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SpellFamilyFlagsZeroAcceptsAll) { // When SpellFamilyMask is 0, it should accept any flags within the family auto* spellInfo = SpellInfoBuilder() .WithId(100) .WithSpellFamilyName(SPELLFAMILY_PRIEST) .WithSpellFamilyFlags(0x12345678, 0xABCDEF01, 0x87654321) // Any flags .Build(); _spellInfos.push_back(spellInfo); HealInfo healInfo(nullptr, nullptr, 200, spellInfo, SPELL_SCHOOL_MASK_HOLY); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS) .WithSpellFamilyName(SPELLFAMILY_PRIEST) .WithSpellFamilyMask(flag96(0, 0, 0)) // Accept any flags .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .WithHealInfo(&healInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_GenericFamilyIgnoresMask) { // Generic family (0) should bypass family mask checks entirely auto* spellInfo = SpellInfoBuilder() .WithId(101) .WithSpellFamilyName(SPELLFAMILY_MAGE) .WithSpellFamilyFlags(0x1, 0, 0) // some mage flag .Build(); _spellInfos.push_back(spellInfo); DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellFamilyName(0) // generic family .WithSpellFamilyMask(flag96(0x2, 0, 0)) // does NOT match the spell's flags .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .WithDamageInfo(&damageInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)) << "Generic family entries should ignore mask restrictions"; } TEST_F(SpellProcTest, SpellInfo_IsAffected_GenericBehavior) { auto* spellInfo = SpellInfoBuilder() .WithId(102) .WithSpellFamilyName(SPELLFAMILY_WARLOCK) .WithSpellFamilyFlags(0x4, 0, 0) .Build(); _spellInfos.push_back(spellInfo); // generic family should return true regardless of mask EXPECT_TRUE(spellInfo->IsAffected(0, flag96(0x4, 0, 0))); EXPECT_TRUE(spellInfo->IsAffected(0, flag96(0x8, 0, 0))); // a non-generic family still respects mask EXPECT_FALSE(spellInfo->IsAffected(SPELLFAMILY_PALADIN, flag96(0x4, 0, 0))); } // ============================================================================= // Real-world Spell Proc Examples // ============================================================================= TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_HotStreakScenario) { // Hot Streak: Proc on critical damage spell from Mage auto* fireballSpell = SpellInfoBuilder() .WithId(133) .WithSpellFamilyName(SPELLFAMILY_MAGE) .WithSpellFamilyFlags(0x00000001, 0, 0) // Fireball flag .Build(); _spellInfos.push_back(fireballSpell); DamageInfo damageInfo(nullptr, nullptr, 1000, fireballSpell, SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE); // Hot Streak proc entry - triggers on fire spell crits auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellFamilyName(SPELLFAMILY_MAGE) .WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_CRITICAL) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG) .WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_CRITICAL) .WithDamageInfo(&damageInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_IlluminationScenario) { // Illumination: Proc on critical heals from Paladin auto* holyLightSpell = SpellInfoBuilder() .WithId(635) .WithSpellFamilyName(SPELLFAMILY_PALADIN) .WithSpellFamilyFlags(0x80000000, 0, 0) .Build(); _spellInfos.push_back(holyLightSpell); HealInfo healInfo(nullptr, nullptr, 2000, holyLightSpell, SPELL_SCHOOL_MASK_HOLY); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS) .WithSpellFamilyName(SPELLFAMILY_PALADIN) .WithSpellTypeMask(PROC_SPELL_TYPE_HEAL) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_CRITICAL) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS) .WithSpellTypeMask(PROC_SPELL_TYPE_HEAL) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_CRITICAL) .WithHealInfo(&healInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SecondWindScenario) { // Second Wind: Proc when stunned/immobilized (taken hit with dodge/parry) auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK | PROC_FLAG_TAKEN_SPELL_MELEE_DMG_CLASS) .WithHitMask(PROC_HIT_DODGE | PROC_HIT_PARRY) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK) .WithHitMask(PROC_HIT_DODGE) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); } TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SwordAndBoardScenario) { // Sword and Board: Proc on Devastate/Revenge (block effects) auto* devastateSpell = SpellInfoBuilder() .WithId(20243) // Devastate .WithSpellFamilyName(SPELLFAMILY_WARRIOR) .WithSpellFamilyFlags(0x00000000, 0x00000000, 0x00000100) // Devastate flag .Build(); _spellInfos.push_back(devastateSpell); DamageInfo damageInfo(nullptr, nullptr, 500, devastateSpell, SPELL_SCHOOL_MASK_NORMAL, SPELL_DIRECT_DAMAGE); auto procEntry = SpellProcEntryBuilder() .WithProcFlags(PROC_FLAG_DONE_SPELL_MELEE_DMG_CLASS) .WithSpellFamilyName(SPELLFAMILY_WARRIOR) .WithSpellFamilyMask(flag96(0, 0, 0x100)) // Devastate flag .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .Build(); auto eventInfo = ProcEventInfoBuilder() .WithTypeMask(PROC_FLAG_DONE_SPELL_MELEE_DMG_CLASS) .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) .WithHitMask(PROC_HIT_NORMAL) .WithDamageInfo(&damageInfo) .Build(); EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); }