From aac930864a6e2ba346348bd478640fdd28aa7ddd Mon Sep 17 00:00:00 2001 From: sogladev Date: Wed, 25 Mar 2026 12:42:25 +0100 Subject: [PATCH] fix(Core/Unit): partial immunity for mixed spells (#25219) Co-authored-by: Shauren --- src/server/game/Entities/Unit/Unit.cpp | 21 +++++++++- .../server/game/Spells/SpellImmunityTest.cpp | 40 ++++++++++++++++++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 2070ea04d..c1657cfb0 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -910,15 +910,34 @@ bool Unit::IsImmunedToSpell(SpellInfo const* spellInfo, uint32 effectMask, Unit if (!spellInfo) return false; + bool immuneToAllEffects = true; + bool hasCheckedEffect = false; + for (uint8 i = EFFECT_0; i < MAX_SPELL_EFFECTS; ++i) { if (!(effectMask & (1u << i))) continue; + if (!spellInfo->Effects[i].IsEffect()) + continue; + + hasCheckedEffect = true; + if (IsImmunedToSpellEffect(spellInfo, i, caster)) - return true; + { + if (spellInfo->HasAttribute(SPELL_ATTR4_NO_PARTIAL_IMMUNITY)) + return true; + + continue; + } + + immuneToAllEffects = false; + break; } + if (hasCheckedEffect && immuneToAllEffects) + return true; + if (!spellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) { if (IsImmunedToSchool(spellInfo)) diff --git a/src/test/server/game/Spells/SpellImmunityTest.cpp b/src/test/server/game/Spells/SpellImmunityTest.cpp index 342e2818f..a88e8c048 100644 --- a/src/test/server/game/Spells/SpellImmunityTest.cpp +++ b/src/test/server/game/Spells/SpellImmunityTest.cpp @@ -105,6 +105,31 @@ namespace return false; } + bool IsEffectBlockedByStunImmunity(EffectDesc const& effect, bool immuneToStun) + { + if (!immuneToStun) + return false; + + return effect.effect == EFFECT_APPLY_AURA && effect.aura == AURA_MOD_STUN; + } + + bool IsFullyImmunedByStunImmunity(SpellDesc const& spell, bool immuneToStun) + { + bool hasAnyEffect = false; + + for (EffectDesc const& effect : spell.effects) + { + if (effect.effect == EFFECT_NONE) + continue; + + hasAnyEffect = true; + if (!IsEffectBlockedByStunImmunity(effect, immuneToStun)) + return false; + } + + return hasAnyEffect; + } + // The last parameter defaults to false to avoid updating existing tests // that don't care about mechanic immunities. Bladestorm grants a // specific mechanic immunity (including stun) that should block @@ -114,12 +139,12 @@ namespace { // Mirrors current core ordering: // 1) full spell immunity check - // 2) mechanic immunity check (e.g. bladestorm vs stun) + // 2) full immunity from mechanic immunity (all effects blocked) // 3) damage immunity only for damage-only spells if (isImmunedToSpell) return SPELL_MISS_IMMUNE; - if (immuneToStun && IsStunSpell(spell)) + if (IsFullyImmunedByStunImmunity(spell, immuneToStun)) return SPELL_MISS_IMMUNE; if (HasOnlyDamageEffects(spell) && isImmunedToDamage) @@ -328,3 +353,14 @@ TEST(SpellImmunityTest, Bladestorm_ImmuneToStun) // with a bladestorm‑style stun immunity it is blocked EXPECT_EQ(ComputeSpellHitResult(false, false, stunSpell, true), SPELL_MISS_IMMUNE); } + +TEST(SpellImmunityTest, StunImmunity_DoesNotFullyBlockMixedSpell) +{ + SpellDesc mixedSpell; + mixedSpell.effects[0] = { EFFECT_SCHOOL_DAMAGE, AURA_NONE }; + mixedSpell.effects[1] = { EFFECT_APPLY_AURA, AURA_MOD_STUN }; + + // This models partial immunity: stun effect is blocked, damage still hits. + EXPECT_TRUE(IsStunSpell(mixedSpell)); + EXPECT_EQ(ComputeSpellHitResult(false, false, mixedSpell, true), SPELL_MISS_NONE); +}