/*
* 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 SpellImmunityTest.cpp
* @brief Tests for spell immunity mechanics
*/
#include "gtest/gtest.h"
#include
#include
#include
#include "Unit.h" // needed for SpellSchoolMask and mask helper
namespace
{
enum EffectType : uint8_t
{
EFFECT_NONE,
EFFECT_SCHOOL_DAMAGE,
EFFECT_HEALTH_LEECH,
EFFECT_POWER_DRAIN,
EFFECT_POWER_BURN,
EFFECT_NORMALIZED_WEAPON_DMG,
EFFECT_WEAPON_PERCENT_DAMAGE,
EFFECT_APPLY_AURA,
EFFECT_DUMMY
};
enum AuraType : uint8_t
{
AURA_NONE,
AURA_MOD_DECREASE_SPEED,
AURA_PERIODIC_DAMAGE,
AURA_TRANSFORM,
AURA_MOD_STUN
};
struct EffectDesc
{
EffectType effect = EFFECT_NONE;
AuraType aura = AURA_NONE;
};
struct SpellDesc
{
std::array effects{};
};
bool IsDamageEffect(EffectType effect)
{
switch (effect)
{
case EFFECT_SCHOOL_DAMAGE:
case EFFECT_HEALTH_LEECH:
case EFFECT_POWER_DRAIN:
case EFFECT_POWER_BURN:
case EFFECT_NORMALIZED_WEAPON_DMG:
case EFFECT_WEAPON_PERCENT_DAMAGE:
return true;
default:
return false;
}
}
bool HasOnlyDamageEffects(SpellDesc const& spell)
{
bool hasAny = std::ranges::any_of(spell.effects, [](EffectDesc const& effect)
{
return effect.effect != EFFECT_NONE;
});
return hasAny && std::ranges::all_of(spell.effects, [](EffectDesc const& effect)
{
return effect.effect == EFFECT_NONE || IsDamageEffect(effect.effect);
});
}
// Helper to classify spells which apply a stun aura
bool IsStunSpell(SpellDesc const& spell)
{
return std::ranges::any_of(spell.effects, [](EffectDesc const& effect)
{
return effect.effect == EFFECT_APPLY_AURA && effect.aura == AURA_MOD_STUN;
});
}
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;
}
bool IsBlockedBySchoolImmunity(bool casterFriendly, bool immunityAppliesToFriendly)
{
return !casterFriendly || immunityAppliesToFriendly;
}
// 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
// a spell like Lethargy.
SpellMissInfo ComputeSpellHitResult(bool isImmunedToSpell, bool isImmunedToDamage,
SpellDesc const& spell, bool immuneToStun = false)
{
// Mirrors current core ordering:
// 1) full spell immunity check
// 2) full immunity from mechanic immunity (all effects blocked)
// 3) damage immunity only for damage-only spells
if (isImmunedToSpell)
return SPELL_MISS_IMMUNE;
if (IsFullyImmunedByStunImmunity(spell, immuneToStun))
return SPELL_MISS_IMMUNE;
if (HasOnlyDamageEffects(spell) && isImmunedToDamage)
return SPELL_MISS_IMMUNE;
return SPELL_MISS_NONE;
}
struct EffectApplyResult
{
bool damageApplied = false;
bool slowApplied = false;
};
EffectApplyResult ApplyEffectsWithMovementImmunity(SpellDesc const& spell, bool immuneToMovementImpairing)
{
EffectApplyResult result;
for (EffectDesc const& e : spell.effects)
{
if (e.effect == EFFECT_NONE)
continue;
if (IsDamageEffect(e.effect))
result.damageApplied = true;
if (e.effect == EFFECT_APPLY_AURA && e.aura == AURA_MOD_DECREASE_SPEED)
{
if (!immuneToMovementImpairing)
result.slowApplied = true;
}
}
return result;
}
SpellDesc MakeDamageOnlySpell()
{
SpellDesc spell;
spell.effects[0] = { EFFECT_SCHOOL_DAMAGE, AURA_NONE };
return spell;
}
SpellDesc MakeFrostboltLikeSpell()
{
SpellDesc spell;
spell.effects[0] = { EFFECT_SCHOOL_DAMAGE, AURA_NONE };
spell.effects[1] = { EFFECT_APPLY_AURA, AURA_MOD_DECREASE_SPEED };
return spell;
}
SpellDesc MakeCycloneLikeSpell()
{
SpellDesc spell;
spell.effects[0] = { EFFECT_APPLY_AURA, AURA_TRANSFORM };
return spell;
}
SpellDesc MakeSlowOnlySpell()
{
// represents effects such as Frost Trap or Desecration, which only slow
SpellDesc spell;
spell.effects[0] = { EFFECT_APPLY_AURA, AURA_MOD_DECREASE_SPEED };
return spell;
}
}
TEST(SpellImmunityTest, HasOnlyDamageEffects_TrueForPureDamage)
{
SpellDesc spell = MakeDamageOnlySpell();
EXPECT_TRUE(HasOnlyDamageEffects(spell));
}
TEST(SpellImmunityTest, HasOnlyDamageEffects_FalseForDamagePlusAura)
{
SpellDesc spell = MakeFrostboltLikeSpell();
EXPECT_FALSE(HasOnlyDamageEffects(spell));
}
TEST(SpellImmunityTest, SpellImmunity_BlocksAllSpells)
{
SpellDesc damageOnly = MakeDamageOnlySpell();
SpellDesc cycloneLike = MakeCycloneLikeSpell();
EXPECT_EQ(ComputeSpellHitResult(true, false, damageOnly), SPELL_MISS_IMMUNE);
EXPECT_EQ(ComputeSpellHitResult(true, false, cycloneLike), SPELL_MISS_IMMUNE);
}
TEST(SpellImmunityTest, DamageImmunity_BlocksDamageOnlySpell)
{
SpellDesc damageOnly = MakeDamageOnlySpell();
EXPECT_EQ(ComputeSpellHitResult(false, true, damageOnly), SPELL_MISS_IMMUNE);
}
// Specific case for spell ID 16621 "Self Invulnerability".
// This aura grants immunity to melee/physical damage only. A rogue
// using Sinister Strike (a physical melee attack) should be completely
// blocked by the effect. The test uses the same simplified damage-only
// spell description as above but documents the physical context.
TEST(SpellImmunityTest, SelfInvulnerability_BlocksMeleeDamage)
{
SpellDesc meleeAttack = MakeDamageOnlySpell();
// simulate physical damage coming from a rogue melee ability
EXPECT_EQ(ComputeSpellHitResult(false, true, meleeAttack), SPELL_MISS_IMMUNE);
}
TEST(SpellImmunityTest, DamageImmunity_DoesNotMissMixedSpell)
{
// This is the key fix: damage immunity must not force SPELL_MISS_IMMUNE
// for mixed spells that include non-damage effects.
SpellDesc frostboltLike = MakeFrostboltLikeSpell();
EXPECT_EQ(ComputeSpellHitResult(false, true, frostboltLike), SPELL_MISS_NONE);
}
TEST(SpellImmunityTest, HandOfFreedomStyle_MovementImmunity_AllowsDamageBlocksSlow)
{
SpellDesc frostboltLike = MakeFrostboltLikeSpell();
EffectApplyResult result = ApplyEffectsWithMovementImmunity(frostboltLike, true);
EXPECT_TRUE(result.damageApplied);
EXPECT_FALSE(result.slowApplied);
}
TEST(SpellImmunityTest, NoMovementImmunity_FrostboltStyle_AppliesDamageAndSlow)
{
SpellDesc frostboltLike = MakeFrostboltLikeSpell();
EffectApplyResult result = ApplyEffectsWithMovementImmunity(frostboltLike, false);
EXPECT_TRUE(result.damageApplied);
EXPECT_TRUE(result.slowApplied);
}
TEST(SpellImmunityTest, CycloneLikeSpell_DivineShieldStyle_Immune)
{
SpellDesc cycloneLike = MakeCycloneLikeSpell();
EXPECT_EQ(ComputeSpellHitResult(true, false, cycloneLike), SPELL_MISS_IMMUNE);
}
// Regression test for issue #10671:
// Divine Shield (full spell immunity) should block purely slowing spells
// such as Hunter Frost Trap or DK Desecration. Previously the effect was
// applied because the core only checked damage-only spells when deciding
// immunity based on damage or spell state.
TEST(SpellImmunityTest, SpellImmunity_BlocksSlowOnlySpell)
{
SpellDesc slowOnly = MakeSlowOnlySpell();
EXPECT_EQ(ComputeSpellHitResult(true, false, slowOnly), SPELL_MISS_IMMUNE);
}
// Ensure that damage-only immunity (e.g. from Hand of Protection) does not
// accidentally prevent slow-only spells. This covers the regression when
// Divine Shield was incorrectly modelled as damage immunity only.
TEST(SpellImmunityTest, DamageImmunity_DoesNotBlockSlowOnlySpell)
{
SpellDesc slowOnly = MakeSlowOnlySpell();
EXPECT_EQ(ComputeSpellHitResult(false, true, slowOnly), SPELL_MISS_NONE);
}
// New coverage for school-mask logic. These exercises the helper used by
// Unit::IsImmunedToDamage to ensure broad masks are handled correctly.
TEST(SpellImmunityTest, ImmunityMask_PartialOverlapDoesNotCount)
{
SpellSchoolMask immune = SPELL_SCHOOL_MASK_FROST;
SpellSchoolMask checkAll = SPELL_SCHOOL_MASK_ALL;
// a frost-only immunity should *not* make you immune to all damage
EXPECT_FALSE(Unit::IsImmuneMaskFully(immune, checkAll));
}
TEST(SpellImmunityTest, ImmunityMask_FullCoverageAccepted)
{
SpellSchoolMask immune = SPELL_SCHOOL_MASK_MAGIC; // holy+spell
SpellSchoolMask checkMagic = SPELL_SCHOOL_MASK_MAGIC;
SpellSchoolMask checkSpell = SPELL_SCHOOL_MASK_SPELL;
EXPECT_TRUE(Unit::IsImmuneMaskFully(immune, checkMagic));
EXPECT_TRUE(Unit::IsImmuneMaskFully(immune, checkSpell));
}
TEST(SpellImmunityTest, ImmunityMask_SupersetMatches)
{
SpellSchoolMask immune = SPELL_SCHOOL_MASK_ALL;
SpellSchoolMask checkMagic = SPELL_SCHOOL_MASK_MAGIC;
EXPECT_TRUE(Unit::IsImmuneMaskFully(immune, checkMagic));
}
// Bladestorm grants a mechanic immunity mask which includes stuns (e.g.
// Lethargy 69133). The following test mirrors that behaviour by
// modelling a simple spell that applies a stun aura and exercising the
// new `immuneToStun` flag in ComputeSpellHitResult.
TEST(SpellImmunityTest, Bladestorm_ImmuneToStun)
{
SpellDesc stunSpell;
stunSpell.effects[0] = {EFFECT_APPLY_AURA, AURA_MOD_STUN};
// without any special immunity the stun should land
EXPECT_EQ(ComputeSpellHitResult(false, false, stunSpell), SPELL_MISS_NONE);
// 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);
}
TEST(SpellImmunityTest, SchoolImmunity_TemplateStyle_AllowsFriendlySpell)
{
EXPECT_FALSE(IsBlockedBySchoolImmunity(true, false));
}
TEST(SpellImmunityTest, SchoolImmunity_ExplicitFriendlyBlockStillApplies)
{
EXPECT_TRUE(IsBlockedBySchoolImmunity(true, true));
}
TEST(SpellImmunityTest, SchoolImmunity_BlocksHostileSpell)
{
EXPECT_TRUE(IsBlockedBySchoolImmunity(false, false));
}