Files
Fractured/src/test/server/game/Spells/SpellImmunityTest.cpp
T
2026-05-06 21:18:20 -04:00

382 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 SpellImmunityTest.cpp
* @brief Tests for spell immunity mechanics
*/
#include "gtest/gtest.h"
#include <algorithm>
#include <array>
#include <cstdint>
#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<EffectDesc, 3> 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 bladestormstyle 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));
}