diff --git a/src/server/game/Spells/Auras/SpellAuraEffects.cpp b/src/server/game/Spells/Auras/SpellAuraEffects.cpp index 5b8599136..896b0634f 100644 --- a/src/server/game/Spells/Auras/SpellAuraEffects.cpp +++ b/src/server/game/Spells/Auras/SpellAuraEffects.cpp @@ -6756,7 +6756,9 @@ void AuraEffect::HandlePeriodicDamageAurasTick(Unit* target, Unit* caster) const // Set trigger flag uint32 procAttacker = PROC_FLAG_DONE_PERIODIC; uint32 procVictim = PROC_FLAG_TAKEN_PERIODIC; - uint32 procEx = (crit ? PROC_EX_CRITICAL_HIT : PROC_EX_NORMAL_HIT) | PROC_EX_INTERNAL_DOT; + uint32 procEx = PROC_EX_INTERNAL_DOT; + if (damage) + procEx |= crit ? PROC_EX_CRITICAL_HIT : PROC_EX_NORMAL_HIT; if (absorb > 0) procEx |= PROC_EX_ABSORB; @@ -6843,7 +6845,9 @@ void AuraEffect::HandlePeriodicHealthLeechAuraTick(Unit* target, Unit* caster) c // Set trigger flag uint32 procAttacker = PROC_FLAG_DONE_PERIODIC; uint32 procVictim = PROC_FLAG_TAKEN_PERIODIC; - uint32 procEx = (crit ? PROC_EX_CRITICAL_HIT : PROC_EX_NORMAL_HIT) | PROC_EX_INTERNAL_DOT; + uint32 procEx = PROC_EX_INTERNAL_DOT; + if (dmgInfo.GetDamage()) + procEx |= crit ? PROC_EX_CRITICAL_HIT : PROC_EX_NORMAL_HIT; if (absorb > 0) procEx |= PROC_EX_ABSORB; diff --git a/src/server/game/Spells/Spell.cpp b/src/server/game/Spells/Spell.cpp index 1e6c710c2..c6e8bb3e9 100644 --- a/src/server/game/Spells/Spell.cpp +++ b/src/server/game/Spells/Spell.cpp @@ -3073,9 +3073,7 @@ SpellMissInfo Spell::DoSpellHitOnUnit(Unit* unit, uint32 effectMask, bool scaleA } if (m_caster != unit && m_caster->IsHostileTo(unit) && !m_spellInfo->IsPositive() && !m_triggeredByAuraSpell && !m_spellInfo->HasAttribute(SPELL_ATTR0_CU_DONT_BREAK_STEALTH)) - { unit->RemoveAurasByType(SPELL_AURA_MOD_STEALTH); - } if (aura_effmask) { diff --git a/src/test/server/game/Spells/PeriodicAbsorbStealthProcTest.cpp b/src/test/server/game/Spells/PeriodicAbsorbStealthProcTest.cpp new file mode 100644 index 000000000..c5b67586f --- /dev/null +++ b/src/test/server/game/Spells/PeriodicAbsorbStealthProcTest.cpp @@ -0,0 +1,156 @@ +/* + * 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 "SpellMgr.h" +#include "WorldMock.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using namespace testing; + +/** + * @brief Tests for fully absorbed periodic damage not triggering TAKEN procs + * + * When periodic damage (e.g. Consecration ticks) is fully absorbed by an + * absorb shield (e.g. Power Word: Shield), the hit mask should only contain + * PROC_HIT_ABSORB (no PROC_HIT_NORMAL/CRITICAL). Since TAKEN procs default + * to requiring PROC_HIT_NORMAL | PROC_HIT_CRITICAL, fully absorbed ticks + * should not trigger victim procs like stealth charge consumption. + * + * This aligns with TrinityCore behavior where hitMask only gets NORMAL/CRITICAL + * added when damage > 0 in HandlePeriodicDamageAurasTick. + */ +class PeriodicAbsorbStealthProcTest : 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; + } + + IWorld* _originalWorld = nullptr; + NiceMock* _worldMock = nullptr; +}; + +// Stealth-like TAKEN periodic proc with default HitMask (0) should NOT +// trigger when the only hit flag is PROC_HIT_ABSORB (fully absorbed tick) +TEST_F(PeriodicAbsorbStealthProcTest, FullyAbsorbedPeriodicDoesNotTriggerTakenProc) +{ + // Stealth has ProcFlags including PROC_FLAG_TAKEN_PERIODIC, HitMask=0 + // Default TAKEN HitMask = PROC_HIT_NORMAL | PROC_HIT_CRITICAL (no ABSORB) + auto procEntry = SpellProcEntryBuilder() + .WithProcFlags(PROC_FLAG_TAKEN_PERIODIC) + .WithHitMask(0) + .Build(); + + // Fully absorbed periodic tick: hitMask = PROC_HIT_ABSORB only + // (damage=0 so PROC_EX_NORMAL_HIT is NOT set) + auto eventInfo = ProcEventInfoBuilder() + .WithTypeMask(PROC_FLAG_TAKEN_PERIODIC) + .WithHitMask(PROC_HIT_ABSORB) + .Build(); + + EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); +} + +// Non-absorbed periodic tick (damage > 0) SHOULD trigger TAKEN procs +TEST_F(PeriodicAbsorbStealthProcTest, NonAbsorbedPeriodicTriggersTakenProc) +{ + auto procEntry = SpellProcEntryBuilder() + .WithProcFlags(PROC_FLAG_TAKEN_PERIODIC) + .WithHitMask(0) + .Build(); + + // Normal periodic tick: hitMask includes PROC_HIT_NORMAL + auto eventInfo = ProcEventInfoBuilder() + .WithTypeMask(PROC_FLAG_TAKEN_PERIODIC) + .WithHitMask(PROC_HIT_NORMAL) + .Build(); + + EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); +} + +// Critical periodic tick SHOULD trigger TAKEN procs +TEST_F(PeriodicAbsorbStealthProcTest, CriticalPeriodicTriggersTakenProc) +{ + auto procEntry = SpellProcEntryBuilder() + .WithProcFlags(PROC_FLAG_TAKEN_PERIODIC) + .WithHitMask(0) + .Build(); + + auto eventInfo = ProcEventInfoBuilder() + .WithTypeMask(PROC_FLAG_TAKEN_PERIODIC) + .WithHitMask(PROC_HIT_CRITICAL) + .Build(); + + EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); +} + +// Partially absorbed periodic tick (damage > 0, some absorbed) SHOULD trigger +// because PROC_HIT_NORMAL is set alongside PROC_HIT_ABSORB +TEST_F(PeriodicAbsorbStealthProcTest, PartiallyAbsorbedPeriodicTriggersTakenProc) +{ + auto procEntry = SpellProcEntryBuilder() + .WithProcFlags(PROC_FLAG_TAKEN_PERIODIC) + .WithHitMask(0) + .Build(); + + // Partial absorb: both NORMAL and ABSORB flags set + auto eventInfo = ProcEventInfoBuilder() + .WithTypeMask(PROC_FLAG_TAKEN_PERIODIC) + .WithHitMask(PROC_HIT_NORMAL | PROC_HIT_ABSORB) + .Build(); + + EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); +} + +// DONE procs (attacker side) SHOULD trigger on fully absorbed damage +// because DONE default HitMask includes PROC_HIT_ABSORB +TEST_F(PeriodicAbsorbStealthProcTest, FullyAbsorbedPeriodicTriggersDoneProc) +{ + auto procEntry = SpellProcEntryBuilder() + .WithProcFlags(PROC_FLAG_DONE_PERIODIC) + .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) + .WithHitMask(0) + .Build(); + + // Fully absorbed: only PROC_HIT_ABSORB + auto eventInfo = ProcEventInfoBuilder() + .WithTypeMask(PROC_FLAG_DONE_PERIODIC) + .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT) + .WithHitMask(PROC_HIT_ABSORB) + .Build(); + + // DONE default includes ABSORB, so this SHOULD trigger + EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo)); +}