Local snapshot for Docker build (includes mod-ale)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
#
|
||||
# This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
#
|
||||
# This file is free software; as a special exception the author gives
|
||||
# unlimited permission to copy and/or distribute it, with or without
|
||||
# modifications, as long as this notice is preserved.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY, to the extent permitted by law; without even the
|
||||
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
CollectSourceFiles(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PRIVATE_SOURCES
|
||||
)
|
||||
|
||||
include_directories(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/mocks"
|
||||
)
|
||||
|
||||
add_executable(
|
||||
unit_tests
|
||||
${PRIVATE_SOURCES}
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
unit_tests
|
||||
game
|
||||
gtest_main
|
||||
gmock_main
|
||||
game-interface
|
||||
)
|
||||
|
||||
# Add module test sources if any modules registered tests
|
||||
get_property(MODULE_TEST_SOURCES GLOBAL PROPERTY ACORE_MODULE_TEST_SOURCES)
|
||||
get_property(MODULE_TEST_INCLUDES GLOBAL PROPERTY ACORE_MODULE_TEST_INCLUDES)
|
||||
|
||||
if(MODULE_TEST_SOURCES)
|
||||
target_sources(unit_tests PRIVATE ${MODULE_TEST_SOURCES})
|
||||
message(STATUS "Added module tests to unit_tests target")
|
||||
endif()
|
||||
|
||||
if(MODULE_TEST_INCLUDES)
|
||||
list(REMOVE_DUPLICATES MODULE_TEST_INCLUDES)
|
||||
target_include_directories(unit_tests PRIVATE ${MODULE_TEST_INCLUDES})
|
||||
message(STATUS "Added module test includes to unit_tests target")
|
||||
endif()
|
||||
|
||||
# Link modules library to tests so module code is available
|
||||
if(TARGET modules)
|
||||
target_link_libraries(unit_tests modules)
|
||||
endif()
|
||||
|
||||
add_test(
|
||||
NAME
|
||||
unit
|
||||
COMMAND
|
||||
${CMAKE_BINARY_DIR}/src/test/unit_tests
|
||||
)
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "Config.h"
|
||||
#include "Define.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#if WIN32
|
||||
void inline setenv(const char* name, const char* value, int overwrite)
|
||||
{
|
||||
_putenv_s(name, value);
|
||||
}
|
||||
#endif
|
||||
|
||||
std::string CreateConfigWithMap(std::map<std::string, std::string> const& map)
|
||||
{
|
||||
auto mTempFileRel = boost::filesystem::unique_path("deleteme.ini");
|
||||
auto mTempFileAbs = boost::filesystem::temp_directory_path() / mTempFileRel;
|
||||
std::ofstream iniStream;
|
||||
iniStream.open(mTempFileAbs.c_str());
|
||||
|
||||
iniStream << "[test]\n";
|
||||
for (auto const& itr : map)
|
||||
iniStream << itr.first << " = " << itr.second << "\n";
|
||||
|
||||
iniStream.close();
|
||||
#if WIN32
|
||||
auto tmp = mTempFileAbs.native();
|
||||
return std::string(tmp.begin(), tmp.end());
|
||||
#else
|
||||
return mTempFileAbs.native();
|
||||
#endif
|
||||
}
|
||||
|
||||
class ConfigEnvTest : public testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
std::map<std::string, std::string> config;
|
||||
config["Int.Nested"] = "4242";
|
||||
config["lower"] = "simpleString";
|
||||
config["UPPER"] = "simpleString";
|
||||
config["SomeLong.NestedNameWithNumber.Like1"] = "1";
|
||||
config["GM.InGMList.Level"] = "50";
|
||||
|
||||
confFilePath = CreateConfigWithMap(config);
|
||||
|
||||
sConfigMgr->Configure(confFilePath, std::vector<std::string>());
|
||||
sConfigMgr->LoadAppConfigs();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
std::remove(confFilePath.c_str());
|
||||
}
|
||||
|
||||
std::string confFilePath;
|
||||
};
|
||||
|
||||
TEST_F(ConfigEnvTest, NestedInt)
|
||||
{
|
||||
EXPECT_EQ(sConfigMgr->GetOption<int32>("Int.Nested", 10), 4242);
|
||||
setenv("AC_INT_NESTED", "8080", 1);
|
||||
EXPECT_EQ(sConfigMgr->OverrideWithEnvVariablesIfAny().empty(), false);
|
||||
EXPECT_EQ(sConfigMgr->GetOption<int32>("Int.Nested", 10), 8080);
|
||||
}
|
||||
|
||||
TEST_F(ConfigEnvTest, SimpleLowerString)
|
||||
{
|
||||
EXPECT_EQ(sConfigMgr->GetOption<std::string>("lower", ""), "simpleString");
|
||||
setenv("AC_LOWER", "envstring", 1);
|
||||
EXPECT_EQ(sConfigMgr->OverrideWithEnvVariablesIfAny().empty(), false);
|
||||
EXPECT_EQ(sConfigMgr->GetOption<std::string>("lower", ""), "envstring");
|
||||
}
|
||||
|
||||
TEST_F(ConfigEnvTest, SimpleUpperString)
|
||||
{
|
||||
EXPECT_EQ(sConfigMgr->GetOption<std::string>("UPPER", ""), "simpleString");
|
||||
setenv("AC_UPPER", "envupperstring", 1);
|
||||
EXPECT_EQ(sConfigMgr->OverrideWithEnvVariablesIfAny().empty(), false);
|
||||
EXPECT_EQ(sConfigMgr->GetOption<std::string>("UPPER", ""), "envupperstring");
|
||||
}
|
||||
|
||||
TEST_F(ConfigEnvTest, LongNestedNameWithNumber)
|
||||
{
|
||||
EXPECT_EQ(sConfigMgr->GetOption<float>("SomeLong.NestedNameWithNumber.Like1", 0), 1);
|
||||
setenv("AC_SOME_LONG_NESTED_NAME_WITH_NUMBER_LIKE_1", "42", 1);
|
||||
EXPECT_EQ(sConfigMgr->OverrideWithEnvVariablesIfAny().empty(), false);
|
||||
EXPECT_EQ(sConfigMgr->GetOption<float>("SomeLong.NestedNameWithNumber.Like1", 0), 42);
|
||||
}
|
||||
|
||||
TEST_F(ConfigEnvTest, ValueWithSeveralUpperlLaters)
|
||||
{
|
||||
EXPECT_EQ(sConfigMgr->GetOption<int>("GM.InGMList.Level", 1), 50);
|
||||
setenv("AC_GM_IN_GMLIST_LEVEL", "42", 1);
|
||||
EXPECT_EQ(sConfigMgr->OverrideWithEnvVariablesIfAny().empty(), false);
|
||||
EXPECT_EQ(sConfigMgr->GetOption<int>("GM.InGMList.Level", 0), 42);
|
||||
}
|
||||
|
||||
TEST_F(ConfigEnvTest, StringThatNotExistInConfig)
|
||||
{
|
||||
setenv("AC_UNIQUE_STRING", "somevalue", 1);
|
||||
EXPECT_EQ(sConfigMgr->GetOption<std::string>("Unique.String", ""), "somevalue");
|
||||
}
|
||||
|
||||
TEST_F(ConfigEnvTest, IntThatNotExistInConfig)
|
||||
{
|
||||
setenv("AC_UNIQUE_INT", "100", 1);
|
||||
EXPECT_EQ(sConfigMgr->GetOption<int>("Unique.Int", 1), 100);
|
||||
}
|
||||
|
||||
TEST_F(ConfigEnvTest, NotExistingString)
|
||||
{
|
||||
EXPECT_EQ(sConfigMgr->GetOption<std::string>("NotFound.String", "none"), "none");
|
||||
}
|
||||
|
||||
TEST_F(ConfigEnvTest, NotExistingInt)
|
||||
{
|
||||
EXPECT_EQ(sConfigMgr->GetOption<int>("NotFound.Int", 1), 1);
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef AZEROTHCORE_AURA_SCRIPT_TEST_FRAMEWORK_H
|
||||
#define AZEROTHCORE_AURA_SCRIPT_TEST_FRAMEWORK_H
|
||||
|
||||
#include "AuraStub.h"
|
||||
#include "DamageHealInfoStub.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "SpellInfoTestHelper.h"
|
||||
#include "UnitStub.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* @brief Simulated proc result for testing
|
||||
*/
|
||||
struct ProcTestResult
|
||||
{
|
||||
bool shouldProc = false;
|
||||
uint8_t effectMask = 0;
|
||||
float procChance = 100.0f;
|
||||
std::vector<uint32_t> spellsCast;
|
||||
bool chargeConsumed = false;
|
||||
bool cooldownSet = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Context for a proc test scenario
|
||||
*/
|
||||
class ProcTestContext
|
||||
{
|
||||
public:
|
||||
ProcTestContext() = default;
|
||||
|
||||
// Actor (the one doing something that triggers the proc)
|
||||
UnitStub& GetActor() { return _actor; }
|
||||
UnitStub const& GetActor() const { return _actor; }
|
||||
|
||||
// Target (the one being affected)
|
||||
UnitStub& GetTarget() { return _target; }
|
||||
UnitStub const& GetTarget() const { return _target; }
|
||||
|
||||
// The aura that might proc
|
||||
AuraStub& GetAura() { return _aura; }
|
||||
AuraStub const& GetAura() const { return _aura; }
|
||||
|
||||
// Damage info for damage-based procs
|
||||
DamageInfoStub& GetDamageInfo() { return _damageInfo; }
|
||||
DamageInfoStub const& GetDamageInfo() const { return _damageInfo; }
|
||||
|
||||
// Heal info for heal-based procs
|
||||
HealInfoStub& GetHealInfo() { return _healInfo; }
|
||||
HealInfoStub const& GetHealInfo() const { return _healInfo; }
|
||||
|
||||
// Setup methods
|
||||
ProcTestContext& WithAuraId(uint32_t auraId)
|
||||
{
|
||||
_aura.SetId(auraId);
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcTestContext& WithAuraSpellFamily(uint32_t familyName)
|
||||
{
|
||||
_aura.SetSpellFamilyName(familyName);
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcTestContext& WithAuraCharges(uint8_t charges)
|
||||
{
|
||||
_aura.SetCharges(charges);
|
||||
_aura.SetUsingCharges(charges > 0);
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcTestContext& WithActorAsPlayer(bool isPlayer = true)
|
||||
{
|
||||
_actor.SetIsPlayer(isPlayer);
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcTestContext& WithDamage(uint32_t damage, uint32_t schoolMask = 1)
|
||||
{
|
||||
_damageInfo.SetDamage(damage);
|
||||
_damageInfo.SetOriginalDamage(damage);
|
||||
_damageInfo.SetSchoolMask(schoolMask);
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcTestContext& WithHeal(uint32_t heal, uint32_t effectiveHeal = 0)
|
||||
{
|
||||
_healInfo.SetHeal(heal);
|
||||
_healInfo.SetEffectiveHeal(effectiveHeal > 0 ? effectiveHeal : heal);
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcTestContext& WithCriticalHit()
|
||||
{
|
||||
_damageInfo.SetHitMask(PROC_HIT_CRITICAL);
|
||||
_healInfo.SetHitMask(PROC_HIT_CRITICAL);
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcTestContext& WithNormalHit()
|
||||
{
|
||||
_damageInfo.SetHitMask(PROC_HIT_NORMAL);
|
||||
_healInfo.SetHitMask(PROC_HIT_NORMAL);
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
UnitStub _actor;
|
||||
UnitStub _target;
|
||||
AuraStub _aura;
|
||||
DamageInfoStub _damageInfo;
|
||||
HealInfoStub _healInfo;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Base fixture for AuraScript proc testing
|
||||
*
|
||||
* This provides infrastructure for testing proc behavior at the unit level
|
||||
* without requiring full game objects.
|
||||
*/
|
||||
class AuraScriptProcTestFixture : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_context = std::make_unique<ProcTestContext>();
|
||||
_spellInfos.clear();
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
for (auto* spellInfo : _spellInfos)
|
||||
{
|
||||
delete spellInfo;
|
||||
}
|
||||
_spellInfos.clear();
|
||||
}
|
||||
|
||||
// Access the test context
|
||||
ProcTestContext& Context() { return *_context; }
|
||||
|
||||
// Create and track a test SpellInfo
|
||||
SpellInfo* CreateSpellInfo(uint32_t id, uint32_t familyName = 0,
|
||||
uint32_t familyFlags0 = 0, uint32_t familyFlags1 = 0,
|
||||
uint32_t familyFlags2 = 0)
|
||||
{
|
||||
auto* spellInfo = SpellInfoBuilder()
|
||||
.WithId(id)
|
||||
.WithSpellFamilyName(familyName)
|
||||
.WithSpellFamilyFlags(familyFlags0, familyFlags1, familyFlags2)
|
||||
.Build();
|
||||
_spellInfos.push_back(spellInfo);
|
||||
return spellInfo;
|
||||
}
|
||||
|
||||
// Create a test SpellProcEntry
|
||||
SpellProcEntry CreateProcEntry()
|
||||
{
|
||||
return SpellProcEntryBuilder().Build();
|
||||
}
|
||||
|
||||
// Create a test ProcEventInfo
|
||||
ProcEventInfo CreateEventInfo(uint32_t typeMask, uint32_t hitMask,
|
||||
uint32_t spellTypeMask = PROC_SPELL_TYPE_MASK_ALL,
|
||||
uint32_t spellPhaseMask = PROC_SPELL_PHASE_HIT)
|
||||
{
|
||||
return ProcEventInfoBuilder()
|
||||
.WithTypeMask(typeMask)
|
||||
.WithHitMask(hitMask)
|
||||
.WithSpellTypeMask(spellTypeMask)
|
||||
.WithSpellPhaseMask(spellPhaseMask)
|
||||
.Build();
|
||||
}
|
||||
|
||||
// Test if a proc entry would trigger with given event info
|
||||
bool TestCanProc(SpellProcEntry const& procEntry, uint32_t typeMask,
|
||||
uint32_t hitMask, SpellInfo const* triggerSpell = nullptr)
|
||||
{
|
||||
DamageInfo* damageInfoPtr = nullptr;
|
||||
HealInfo* healInfoPtr = nullptr;
|
||||
|
||||
// Create real DamageInfo/HealInfo if we have a trigger spell
|
||||
// Note: This requires the actual game classes, which may need adjustment
|
||||
// For now, we use the stub approach
|
||||
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithTypeMask(typeMask)
|
||||
.WithHitMask(hitMask)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_MASK_ALL)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
return sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo);
|
||||
}
|
||||
|
||||
// Check if spell family matches
|
||||
bool TestSpellFamilyMatch(uint32_t procFamilyName, flag96 const& procFamilyMask,
|
||||
SpellInfo const* triggerSpell)
|
||||
{
|
||||
if (procFamilyName && triggerSpell)
|
||||
{
|
||||
if (procFamilyName != triggerSpell->SpellFamilyName)
|
||||
return false;
|
||||
|
||||
if (procFamilyMask)
|
||||
{
|
||||
flag96 triggerMask;
|
||||
triggerMask[0] = triggerSpell->SpellFamilyFlags[0];
|
||||
triggerMask[1] = triggerSpell->SpellFamilyFlags[1];
|
||||
triggerMask[2] = triggerSpell->SpellFamilyFlags[2];
|
||||
|
||||
if (!(triggerMask & procFamilyMask))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<ProcTestContext> _context;
|
||||
std::vector<SpellInfo*> _spellInfos;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Helper class for testing specific proc scenarios
|
||||
*
|
||||
* Uses shared_ptr for resource management to allow safe copying
|
||||
* in fluent builder pattern usage.
|
||||
*/
|
||||
class ProcScenarioBuilder
|
||||
{
|
||||
public:
|
||||
ProcScenarioBuilder()
|
||||
{
|
||||
// Create a default SpellInfo for spell-type procs using shared_ptr
|
||||
_defaultSpellInfo = std::shared_ptr<SpellInfo>(
|
||||
SpellInfoBuilder()
|
||||
.WithId(99999)
|
||||
.WithSpellFamilyName(0)
|
||||
.Build()
|
||||
);
|
||||
}
|
||||
|
||||
~ProcScenarioBuilder() = default;
|
||||
|
||||
// Configure the triggering action
|
||||
ProcScenarioBuilder& OnMeleeAutoAttack()
|
||||
{
|
||||
_typeMask = PROC_FLAG_DONE_MELEE_AUTO_ATTACK;
|
||||
_needsSpellInfo = false;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& OnTakenMeleeAutoAttack()
|
||||
{
|
||||
_typeMask = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
_needsSpellInfo = false;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& OnSpellDamage()
|
||||
{
|
||||
_typeMask = PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG;
|
||||
_spellTypeMask = PROC_SPELL_TYPE_DAMAGE;
|
||||
_needsSpellInfo = true;
|
||||
_usesDamageInfo = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& OnTakenSpellDamage()
|
||||
{
|
||||
_typeMask = PROC_FLAG_TAKEN_SPELL_MAGIC_DMG_CLASS_NEG;
|
||||
_spellTypeMask = PROC_SPELL_TYPE_DAMAGE;
|
||||
_needsSpellInfo = true;
|
||||
_usesDamageInfo = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& OnHeal()
|
||||
{
|
||||
_typeMask = PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS;
|
||||
_spellTypeMask = PROC_SPELL_TYPE_HEAL;
|
||||
_needsSpellInfo = true;
|
||||
_usesHealInfo = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& OnTakenHeal()
|
||||
{
|
||||
_typeMask = PROC_FLAG_TAKEN_SPELL_MAGIC_DMG_CLASS_POS;
|
||||
_spellTypeMask = PROC_SPELL_TYPE_HEAL;
|
||||
_needsSpellInfo = true;
|
||||
_usesHealInfo = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& OnPeriodicDamage()
|
||||
{
|
||||
_typeMask = PROC_FLAG_DONE_PERIODIC;
|
||||
_spellTypeMask = PROC_SPELL_TYPE_DAMAGE;
|
||||
_needsSpellInfo = true;
|
||||
_usesDamageInfo = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& OnPeriodicHeal()
|
||||
{
|
||||
_typeMask = PROC_FLAG_DONE_PERIODIC;
|
||||
_spellTypeMask = PROC_SPELL_TYPE_HEAL;
|
||||
_needsSpellInfo = true;
|
||||
_usesHealInfo = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& OnKill()
|
||||
{
|
||||
_typeMask = PROC_FLAG_KILL;
|
||||
_needsSpellInfo = false;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& OnDeath()
|
||||
{
|
||||
_typeMask = PROC_FLAG_DEATH;
|
||||
_needsSpellInfo = false;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Configure hit result
|
||||
ProcScenarioBuilder& WithCrit()
|
||||
{
|
||||
_hitMask = PROC_HIT_CRITICAL;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& WithNormalHit()
|
||||
{
|
||||
_hitMask = PROC_HIT_NORMAL;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& WithMiss()
|
||||
{
|
||||
_hitMask = PROC_HIT_MISS;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& WithDodge()
|
||||
{
|
||||
_hitMask = PROC_HIT_DODGE;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& WithParry()
|
||||
{
|
||||
_hitMask = PROC_HIT_PARRY;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& WithBlock()
|
||||
{
|
||||
_hitMask = PROC_HIT_BLOCK;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& WithFullBlock()
|
||||
{
|
||||
_hitMask = PROC_HIT_FULL_BLOCK;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& WithAbsorb()
|
||||
{
|
||||
_hitMask = PROC_HIT_ABSORB;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Note: PROC_HIT_ABSORB covers both partial and full absorb
|
||||
// There is no separate PROC_HIT_FULL_ABSORB flag in AzerothCore
|
||||
|
||||
// Configure spell phase
|
||||
ProcScenarioBuilder& OnCast()
|
||||
{
|
||||
_spellPhaseMask = PROC_SPELL_PHASE_CAST;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& OnHit()
|
||||
{
|
||||
_spellPhaseMask = PROC_SPELL_PHASE_HIT;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcScenarioBuilder& OnFinish()
|
||||
{
|
||||
_spellPhaseMask = PROC_SPELL_PHASE_FINISH;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Build the scenario into a ProcEventInfo
|
||||
ProcEventInfo Build()
|
||||
{
|
||||
auto builder = ProcEventInfoBuilder()
|
||||
.WithTypeMask(_typeMask)
|
||||
.WithHitMask(_hitMask)
|
||||
.WithSpellTypeMask(_spellTypeMask)
|
||||
.WithSpellPhaseMask(_spellPhaseMask);
|
||||
|
||||
// Create DamageInfo or HealInfo with SpellInfo for spell-type procs
|
||||
if (_needsSpellInfo)
|
||||
{
|
||||
if (_usesDamageInfo)
|
||||
{
|
||||
// Create new DamageInfo if needed
|
||||
if (!_damageInfo)
|
||||
_damageInfo = std::make_shared<DamageInfo>(nullptr, nullptr, 100, _defaultSpellInfo.get(), SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE);
|
||||
builder.WithDamageInfo(_damageInfo.get());
|
||||
}
|
||||
else if (_usesHealInfo)
|
||||
{
|
||||
// Create new HealInfo if needed
|
||||
if (!_healInfo)
|
||||
_healInfo = std::make_shared<HealInfo>(nullptr, nullptr, 100, _defaultSpellInfo.get(), SPELL_SCHOOL_MASK_HOLY);
|
||||
builder.WithHealInfo(_healInfo.get());
|
||||
}
|
||||
}
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
// Get individual values
|
||||
[[nodiscard]] uint32_t GetTypeMask() const { return _typeMask; }
|
||||
[[nodiscard]] uint32_t GetHitMask() const { return _hitMask; }
|
||||
[[nodiscard]] uint32_t GetSpellTypeMask() const { return _spellTypeMask; }
|
||||
[[nodiscard]] uint32_t GetSpellPhaseMask() const { return _spellPhaseMask; }
|
||||
|
||||
private:
|
||||
uint32_t _typeMask = 0;
|
||||
uint32_t _hitMask = PROC_HIT_NORMAL;
|
||||
uint32_t _spellTypeMask = PROC_SPELL_TYPE_MASK_ALL;
|
||||
uint32_t _spellPhaseMask = PROC_SPELL_PHASE_HIT;
|
||||
bool _needsSpellInfo = false;
|
||||
bool _usesDamageInfo = false;
|
||||
bool _usesHealInfo = false;
|
||||
std::shared_ptr<SpellInfo> _defaultSpellInfo;
|
||||
std::shared_ptr<DamageInfo> _damageInfo;
|
||||
std::shared_ptr<HealInfo> _healInfo;
|
||||
};
|
||||
|
||||
// Convenience macros for proc testing
|
||||
#define EXPECT_PROC_TRIGGERS(procEntry, scenario) \
|
||||
do { \
|
||||
auto _eventInfo = (scenario).Build(); \
|
||||
EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, _eventInfo)); \
|
||||
} while(0)
|
||||
|
||||
#define EXPECT_PROC_DOES_NOT_TRIGGER(procEntry, scenario) \
|
||||
do { \
|
||||
auto _eventInfo = (scenario).Build(); \
|
||||
EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, _eventInfo)); \
|
||||
} while(0)
|
||||
|
||||
#endif //AZEROTHCORE_AURA_SCRIPT_TEST_FRAMEWORK_H
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef AZEROTHCORE_AURA_STUB_H
|
||||
#define AZEROTHCORE_AURA_STUB_H
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
class SpellInfo;
|
||||
class UnitStub;
|
||||
|
||||
/**
|
||||
* @brief Lightweight stub for AuraEffect proc-related functionality
|
||||
*/
|
||||
class AuraEffectStub
|
||||
{
|
||||
public:
|
||||
AuraEffectStub(uint8_t effIndex = 0, int32_t amount = 0, uint32_t auraType = 0)
|
||||
: _effIndex(effIndex), _amount(amount), _auraType(auraType) {}
|
||||
|
||||
virtual ~AuraEffectStub() = default;
|
||||
|
||||
[[nodiscard]] uint8_t GetEffIndex() const { return _effIndex; }
|
||||
[[nodiscard]] int32_t GetAmount() const { return _amount; }
|
||||
[[nodiscard]] uint32_t GetAuraType() const { return _auraType; }
|
||||
[[nodiscard]] int32_t GetBaseAmount() const { return _baseAmount; }
|
||||
[[nodiscard]] float GetCritChance() const { return _critChance; }
|
||||
|
||||
void SetEffIndex(uint8_t effIndex) { _effIndex = effIndex; }
|
||||
void SetAmount(int32_t amount) { _amount = amount; }
|
||||
void SetAuraType(uint32_t auraType) { _auraType = auraType; }
|
||||
void SetBaseAmount(int32_t baseAmount) { _baseAmount = baseAmount; }
|
||||
void SetCritChance(float critChance) { _critChance = critChance; }
|
||||
|
||||
// Periodic tracking
|
||||
[[nodiscard]] bool IsPeriodic() const { return _isPeriodic; }
|
||||
[[nodiscard]] int32_t GetTotalTicks() const { return _totalTicks; }
|
||||
[[nodiscard]] uint32_t GetTickNumber() const { return _tickNumber; }
|
||||
|
||||
void SetPeriodic(bool isPeriodic) { _isPeriodic = isPeriodic; }
|
||||
void SetTotalTicks(int32_t totalTicks) { _totalTicks = totalTicks; }
|
||||
void SetTickNumber(uint32_t tickNumber) { _tickNumber = tickNumber; }
|
||||
|
||||
private:
|
||||
uint8_t _effIndex = 0;
|
||||
int32_t _amount = 0;
|
||||
int32_t _baseAmount = 0;
|
||||
uint32_t _auraType = 0;
|
||||
float _critChance = 0.0f;
|
||||
bool _isPeriodic = false;
|
||||
int32_t _totalTicks = 0;
|
||||
uint32_t _tickNumber = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Lightweight stub for AuraApplication functionality
|
||||
*/
|
||||
class AuraApplicationStub
|
||||
{
|
||||
public:
|
||||
AuraApplicationStub() = default;
|
||||
virtual ~AuraApplicationStub() = default;
|
||||
|
||||
[[nodiscard]] uint8_t GetEffectMask() const { return _effectMask; }
|
||||
[[nodiscard]] bool HasEffect(uint8_t effIndex) const
|
||||
{
|
||||
return (_effectMask & (1 << effIndex)) != 0;
|
||||
}
|
||||
[[nodiscard]] bool IsPositive() const { return _isPositive; }
|
||||
[[nodiscard]] uint8_t GetSlot() const { return _slot; }
|
||||
|
||||
void SetEffectMask(uint8_t mask) { _effectMask = mask; }
|
||||
void SetEffect(uint8_t effIndex, bool enabled)
|
||||
{
|
||||
if (enabled)
|
||||
_effectMask |= (1 << effIndex);
|
||||
else
|
||||
_effectMask &= ~(1 << effIndex);
|
||||
}
|
||||
void SetPositive(bool isPositive) { _isPositive = isPositive; }
|
||||
void SetSlot(uint8_t slot) { _slot = slot; }
|
||||
|
||||
private:
|
||||
uint8_t _effectMask = 0x07; // All 3 effects by default
|
||||
bool _isPositive = true;
|
||||
uint8_t _slot = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Lightweight stub for Aura proc-related functionality
|
||||
*/
|
||||
class AuraStub
|
||||
{
|
||||
public:
|
||||
AuraStub(uint32_t id = 0, uint32_t spellFamilyName = 0)
|
||||
: _id(id), _spellFamilyName(spellFamilyName)
|
||||
{
|
||||
// Create 3 effect slots by default
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
_effects[i] = std::make_unique<AuraEffectStub>(static_cast<uint8_t>(i));
|
||||
}
|
||||
}
|
||||
|
||||
virtual ~AuraStub() = default;
|
||||
|
||||
// Basic identification
|
||||
[[nodiscard]] uint32_t GetId() const { return _id; }
|
||||
[[nodiscard]] uint32_t GetSpellFamilyName() const { return _spellFamilyName; }
|
||||
|
||||
void SetId(uint32_t id) { _id = id; }
|
||||
void SetSpellFamilyName(uint32_t familyName) { _spellFamilyName = familyName; }
|
||||
|
||||
// Effect access
|
||||
[[nodiscard]] AuraEffectStub* GetEffect(uint8_t effIndex) const
|
||||
{
|
||||
return (effIndex < 3) ? _effects[effIndex].get() : nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool HasEffect(uint8_t effIndex) const
|
||||
{
|
||||
return effIndex < 3 && _effects[effIndex] != nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] uint8_t GetEffectMask() const
|
||||
{
|
||||
uint8_t mask = 0;
|
||||
for (uint8_t i = 0; i < 3; ++i)
|
||||
{
|
||||
if (_effects[i])
|
||||
mask |= (1 << i);
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
// Charges management
|
||||
[[nodiscard]] uint8_t GetCharges() const { return _charges; }
|
||||
[[nodiscard]] bool IsUsingCharges() const { return _isUsingCharges; }
|
||||
|
||||
void SetCharges(uint8_t charges) { _charges = charges; }
|
||||
void SetUsingCharges(bool usingCharges) { _isUsingCharges = usingCharges; }
|
||||
|
||||
virtual bool DropCharge()
|
||||
{
|
||||
if (_charges > 0)
|
||||
{
|
||||
--_charges;
|
||||
_chargeDropped = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool WasChargeDropped() const { return _chargeDropped; }
|
||||
void ResetChargeDropped() { _chargeDropped = false; }
|
||||
|
||||
// Duration
|
||||
[[nodiscard]] int32_t GetDuration() const { return _duration; }
|
||||
[[nodiscard]] int32_t GetMaxDuration() const { return _maxDuration; }
|
||||
[[nodiscard]] bool IsPermanent() const { return _maxDuration == -1; }
|
||||
|
||||
void SetDuration(int32_t duration) { _duration = duration; }
|
||||
void SetMaxDuration(int32_t maxDuration) { _maxDuration = maxDuration; }
|
||||
|
||||
// Cooldown tracking
|
||||
using TimePoint = std::chrono::steady_clock::time_point;
|
||||
|
||||
[[nodiscard]] bool IsProcOnCooldown(TimePoint now) const
|
||||
{
|
||||
return now < _procCooldown;
|
||||
}
|
||||
|
||||
void AddProcCooldown(TimePoint cooldownEnd)
|
||||
{
|
||||
_procCooldown = cooldownEnd;
|
||||
}
|
||||
|
||||
void ResetProcCooldown()
|
||||
{
|
||||
_procCooldown = TimePoint::min();
|
||||
}
|
||||
|
||||
// Stack amount
|
||||
[[nodiscard]] uint8_t GetStackAmount() const { return _stackAmount; }
|
||||
void SetStackAmount(uint8_t amount) { _stackAmount = amount; }
|
||||
|
||||
/**
|
||||
* @brief Modify stack amount (for PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
* Mimics Aura::ModStackAmount() - removes aura if stacks reach 0
|
||||
*/
|
||||
virtual bool ModStackAmount(int32_t amount, bool /* resetPeriodicTimer */ = true)
|
||||
{
|
||||
int32_t newAmount = static_cast<int32_t>(_stackAmount) + amount;
|
||||
if (newAmount <= 0)
|
||||
{
|
||||
_stackAmount = 0;
|
||||
Remove();
|
||||
return true; // Aura removed
|
||||
}
|
||||
_stackAmount = static_cast<uint8_t>(newAmount);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Aura flags
|
||||
[[nodiscard]] bool IsPassive() const { return _isPassive; }
|
||||
[[nodiscard]] bool IsRemoved() const { return _isRemoved; }
|
||||
|
||||
void SetPassive(bool isPassive) { _isPassive = isPassive; }
|
||||
void SetRemoved(bool isRemoved) { _isRemoved = isRemoved; }
|
||||
|
||||
/**
|
||||
* @brief Mark aura as removed (for charge exhaustion)
|
||||
* Mimics Aura::Remove()
|
||||
*/
|
||||
virtual void Remove()
|
||||
{
|
||||
_isRemoved = true;
|
||||
}
|
||||
|
||||
// Application management
|
||||
AuraApplicationStub& GetOrCreateApplication()
|
||||
{
|
||||
if (!_application)
|
||||
_application = std::make_unique<AuraApplicationStub>();
|
||||
return *_application;
|
||||
}
|
||||
|
||||
[[nodiscard]] AuraApplicationStub* GetApplication() const
|
||||
{
|
||||
return _application.get();
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t _id = 0;
|
||||
uint32_t _spellFamilyName = 0;
|
||||
|
||||
std::unique_ptr<AuraEffectStub> _effects[3];
|
||||
std::unique_ptr<AuraApplicationStub> _application;
|
||||
|
||||
uint8_t _charges = 0;
|
||||
bool _isUsingCharges = false;
|
||||
bool _chargeDropped = false;
|
||||
|
||||
int32_t _duration = -1;
|
||||
int32_t _maxDuration = -1;
|
||||
|
||||
TimePoint _procCooldown = TimePoint::min();
|
||||
|
||||
uint8_t _stackAmount = 1;
|
||||
bool _isPassive = false;
|
||||
bool _isRemoved = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief GMock-enabled Aura stub for verification
|
||||
*/
|
||||
class MockAuraStub : public AuraStub
|
||||
{
|
||||
public:
|
||||
MockAuraStub(uint32_t id = 0, uint32_t spellFamilyName = 0)
|
||||
: AuraStub(id, spellFamilyName) {}
|
||||
|
||||
MOCK_METHOD(bool, DropCharge, (), (override));
|
||||
MOCK_METHOD(bool, ModStackAmount, (int32_t amount, bool resetPeriodicTimer), (override));
|
||||
MOCK_METHOD(void, Remove, (), (override));
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for creating AuraStub instances with fluent API
|
||||
*/
|
||||
class AuraStubBuilder
|
||||
{
|
||||
public:
|
||||
AuraStubBuilder() : _stub(std::make_unique<AuraStub>()) {}
|
||||
|
||||
AuraStubBuilder& WithId(uint32_t id)
|
||||
{
|
||||
_stub->SetId(id);
|
||||
return *this;
|
||||
}
|
||||
|
||||
AuraStubBuilder& WithSpellFamilyName(uint32_t familyName)
|
||||
{
|
||||
_stub->SetSpellFamilyName(familyName);
|
||||
return *this;
|
||||
}
|
||||
|
||||
AuraStubBuilder& WithCharges(uint8_t charges)
|
||||
{
|
||||
_stub->SetCharges(charges);
|
||||
_stub->SetUsingCharges(charges > 0);
|
||||
return *this;
|
||||
}
|
||||
|
||||
AuraStubBuilder& WithDuration(int32_t duration)
|
||||
{
|
||||
_stub->SetDuration(duration);
|
||||
_stub->SetMaxDuration(duration);
|
||||
return *this;
|
||||
}
|
||||
|
||||
AuraStubBuilder& WithStackAmount(uint8_t amount)
|
||||
{
|
||||
_stub->SetStackAmount(amount);
|
||||
return *this;
|
||||
}
|
||||
|
||||
AuraStubBuilder& WithPassive(bool isPassive)
|
||||
{
|
||||
_stub->SetPassive(isPassive);
|
||||
return *this;
|
||||
}
|
||||
|
||||
AuraStubBuilder& WithEffect(uint8_t effIndex, int32_t amount, uint32_t auraType = 0)
|
||||
{
|
||||
if (AuraEffectStub* eff = _stub->GetEffect(effIndex))
|
||||
{
|
||||
eff->SetAmount(amount);
|
||||
eff->SetAuraType(auraType);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
AuraStubBuilder& WithPeriodicEffect(uint8_t effIndex, int32_t amount, int32_t totalTicks)
|
||||
{
|
||||
if (AuraEffectStub* eff = _stub->GetEffect(effIndex))
|
||||
{
|
||||
eff->SetAmount(amount);
|
||||
eff->SetPeriodic(true);
|
||||
eff->SetTotalTicks(totalTicks);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::unique_ptr<AuraStub> Build()
|
||||
{
|
||||
return std::move(_stub);
|
||||
}
|
||||
|
||||
AuraStub* BuildRaw()
|
||||
{
|
||||
return _stub.release();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<AuraStub> _stub;
|
||||
};
|
||||
|
||||
#endif //AZEROTHCORE_AURA_STUB_H
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef AZEROTHCORE_DAMAGE_HEAL_INFO_STUB_H
|
||||
#define AZEROTHCORE_DAMAGE_HEAL_INFO_STUB_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class SpellInfo;
|
||||
class UnitStub;
|
||||
|
||||
/**
|
||||
* @brief Lightweight stub for DamageInfo
|
||||
*
|
||||
* Mirrors the key fields of DamageInfo for proc testing without
|
||||
* requiring actual Unit objects.
|
||||
*/
|
||||
class DamageInfoStub
|
||||
{
|
||||
public:
|
||||
DamageInfoStub() = default;
|
||||
|
||||
DamageInfoStub(uint32_t damage, uint32_t originalDamage, uint32_t schoolMask,
|
||||
uint8_t attackType, SpellInfo const* spellInfo = nullptr)
|
||||
: _damage(damage)
|
||||
, _originalDamage(originalDamage)
|
||||
, _schoolMask(schoolMask)
|
||||
, _attackType(attackType)
|
||||
, _spellInfo(spellInfo)
|
||||
{}
|
||||
|
||||
virtual ~DamageInfoStub() = default;
|
||||
|
||||
// Damage values
|
||||
[[nodiscard]] uint32_t GetDamage() const { return _damage; }
|
||||
[[nodiscard]] uint32_t GetOriginalDamage() const { return _originalDamage; }
|
||||
[[nodiscard]] uint32_t GetAbsorb() const { return _absorb; }
|
||||
[[nodiscard]] uint32_t GetResist() const { return _resist; }
|
||||
[[nodiscard]] uint32_t GetBlock() const { return _block; }
|
||||
|
||||
void SetDamage(uint32_t damage) { _damage = damage; }
|
||||
void SetOriginalDamage(uint32_t damage) { _originalDamage = damage; }
|
||||
void SetAbsorb(uint32_t absorb) { _absorb = absorb; }
|
||||
void SetResist(uint32_t resist) { _resist = resist; }
|
||||
void SetBlock(uint32_t block) { _block = block; }
|
||||
|
||||
// School and attack type
|
||||
[[nodiscard]] uint32_t GetSchoolMask() const { return _schoolMask; }
|
||||
[[nodiscard]] uint8_t GetAttackType() const { return _attackType; }
|
||||
|
||||
void SetSchoolMask(uint32_t schoolMask) { _schoolMask = schoolMask; }
|
||||
void SetAttackType(uint8_t attackType) { _attackType = attackType; }
|
||||
|
||||
// Spell info
|
||||
[[nodiscard]] SpellInfo const* GetSpellInfo() const { return _spellInfo; }
|
||||
void SetSpellInfo(SpellInfo const* spellInfo) { _spellInfo = spellInfo; }
|
||||
|
||||
// Hit result flags
|
||||
[[nodiscard]] uint32_t GetHitMask() const { return _hitMask; }
|
||||
void SetHitMask(uint32_t hitMask) { _hitMask = hitMask; }
|
||||
|
||||
private:
|
||||
uint32_t _damage = 0;
|
||||
uint32_t _originalDamage = 0;
|
||||
uint32_t _absorb = 0;
|
||||
uint32_t _resist = 0;
|
||||
uint32_t _block = 0;
|
||||
uint32_t _schoolMask = 1; // SPELL_SCHOOL_MASK_NORMAL
|
||||
uint8_t _attackType = 0; // BASE_ATTACK
|
||||
uint32_t _hitMask = 0;
|
||||
SpellInfo const* _spellInfo = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Lightweight stub for HealInfo
|
||||
*
|
||||
* Mirrors the key fields of HealInfo for proc testing.
|
||||
*/
|
||||
class HealInfoStub
|
||||
{
|
||||
public:
|
||||
HealInfoStub() = default;
|
||||
|
||||
HealInfoStub(uint32_t heal, uint32_t effectiveHeal, uint32_t absorb,
|
||||
SpellInfo const* spellInfo = nullptr)
|
||||
: _heal(heal)
|
||||
, _effectiveHeal(effectiveHeal)
|
||||
, _absorb(absorb)
|
||||
, _spellInfo(spellInfo)
|
||||
{}
|
||||
|
||||
virtual ~HealInfoStub() = default;
|
||||
|
||||
// Heal values
|
||||
[[nodiscard]] uint32_t GetHeal() const { return _heal; }
|
||||
[[nodiscard]] uint32_t GetEffectiveHeal() const { return _effectiveHeal; }
|
||||
[[nodiscard]] uint32_t GetAbsorb() const { return _absorb; }
|
||||
[[nodiscard]] uint32_t GetOverheal() const { return _heal > _effectiveHeal ? _heal - _effectiveHeal : 0; }
|
||||
|
||||
void SetHeal(uint32_t heal) { _heal = heal; }
|
||||
void SetEffectiveHeal(uint32_t effectiveHeal) { _effectiveHeal = effectiveHeal; }
|
||||
void SetAbsorb(uint32_t absorb) { _absorb = absorb; }
|
||||
|
||||
// Spell info
|
||||
[[nodiscard]] SpellInfo const* GetSpellInfo() const { return _spellInfo; }
|
||||
void SetSpellInfo(SpellInfo const* spellInfo) { _spellInfo = spellInfo; }
|
||||
|
||||
// Hit result flags
|
||||
[[nodiscard]] uint32_t GetHitMask() const { return _hitMask; }
|
||||
void SetHitMask(uint32_t hitMask) { _hitMask = hitMask; }
|
||||
|
||||
private:
|
||||
uint32_t _heal = 0;
|
||||
uint32_t _effectiveHeal = 0;
|
||||
uint32_t _absorb = 0;
|
||||
uint32_t _hitMask = 0;
|
||||
SpellInfo const* _spellInfo = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for creating DamageInfoStub instances with fluent API
|
||||
*/
|
||||
class DamageInfoStubBuilder
|
||||
{
|
||||
public:
|
||||
DamageInfoStubBuilder() = default;
|
||||
|
||||
DamageInfoStubBuilder& WithDamage(uint32_t damage)
|
||||
{
|
||||
_stub.SetDamage(damage);
|
||||
_stub.SetOriginalDamage(damage);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DamageInfoStubBuilder& WithOriginalDamage(uint32_t damage)
|
||||
{
|
||||
_stub.SetOriginalDamage(damage);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DamageInfoStubBuilder& WithSchoolMask(uint32_t schoolMask)
|
||||
{
|
||||
_stub.SetSchoolMask(schoolMask);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DamageInfoStubBuilder& WithAttackType(uint8_t attackType)
|
||||
{
|
||||
_stub.SetAttackType(attackType);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DamageInfoStubBuilder& WithSpellInfo(SpellInfo const* spellInfo)
|
||||
{
|
||||
_stub.SetSpellInfo(spellInfo);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DamageInfoStubBuilder& WithAbsorb(uint32_t absorb)
|
||||
{
|
||||
_stub.SetAbsorb(absorb);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DamageInfoStubBuilder& WithResist(uint32_t resist)
|
||||
{
|
||||
_stub.SetResist(resist);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DamageInfoStubBuilder& WithBlock(uint32_t block)
|
||||
{
|
||||
_stub.SetBlock(block);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DamageInfoStubBuilder& WithHitMask(uint32_t hitMask)
|
||||
{
|
||||
_stub.SetHitMask(hitMask);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DamageInfoStub Build() { return _stub; }
|
||||
|
||||
private:
|
||||
DamageInfoStub _stub;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for creating HealInfoStub instances with fluent API
|
||||
*/
|
||||
class HealInfoStubBuilder
|
||||
{
|
||||
public:
|
||||
HealInfoStubBuilder() = default;
|
||||
|
||||
HealInfoStubBuilder& WithHeal(uint32_t heal)
|
||||
{
|
||||
_stub.SetHeal(heal);
|
||||
_stub.SetEffectiveHeal(heal); // Assume all effective unless overridden
|
||||
return *this;
|
||||
}
|
||||
|
||||
HealInfoStubBuilder& WithEffectiveHeal(uint32_t effectiveHeal)
|
||||
{
|
||||
_stub.SetEffectiveHeal(effectiveHeal);
|
||||
return *this;
|
||||
}
|
||||
|
||||
HealInfoStubBuilder& WithOverheal(uint32_t overheal)
|
||||
{
|
||||
// Overheal = Heal - EffectiveHeal
|
||||
// So EffectiveHeal = Heal - Overheal
|
||||
if (_stub.GetHeal() >= overheal)
|
||||
{
|
||||
_stub.SetEffectiveHeal(_stub.GetHeal() - overheal);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
HealInfoStubBuilder& WithAbsorb(uint32_t absorb)
|
||||
{
|
||||
_stub.SetAbsorb(absorb);
|
||||
return *this;
|
||||
}
|
||||
|
||||
HealInfoStubBuilder& WithSpellInfo(SpellInfo const* spellInfo)
|
||||
{
|
||||
_stub.SetSpellInfo(spellInfo);
|
||||
return *this;
|
||||
}
|
||||
|
||||
HealInfoStubBuilder& WithHitMask(uint32_t hitMask)
|
||||
{
|
||||
_stub.SetHitMask(hitMask);
|
||||
return *this;
|
||||
}
|
||||
|
||||
HealInfoStub Build() { return _stub; }
|
||||
|
||||
private:
|
||||
HealInfoStub _stub;
|
||||
};
|
||||
|
||||
#endif //AZEROTHCORE_DAMAGE_HEAL_INFO_STUB_H
|
||||
@@ -0,0 +1,782 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef AZEROTHCORE_PROC_CHANCE_TEST_HELPER_H
|
||||
#define AZEROTHCORE_PROC_CHANCE_TEST_HELPER_H
|
||||
|
||||
#include "SpellMgr.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "AuraStub.h"
|
||||
#include "UnitStub.h"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
/**
|
||||
* @brief Helper class for testing proc chance calculations
|
||||
*
|
||||
* Provides standalone implementations of proc-related calculations
|
||||
* that can be tested without requiring full game objects.
|
||||
*/
|
||||
class ProcChanceTestHelper
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Calculate PPM proc chance
|
||||
* Implements the formula: (WeaponSpeed * PPM) / 600.0f
|
||||
*
|
||||
* @param weaponSpeed Weapon attack speed in milliseconds
|
||||
* @param ppm Procs per minute value
|
||||
* @param ppmModifier Additional PPM modifier (from talents/auras)
|
||||
* @return Proc chance as percentage (0-100+)
|
||||
*/
|
||||
static float CalculatePPMChance(uint32 weaponSpeed, float ppm, float ppmModifier = 0.0f)
|
||||
{
|
||||
if (ppm <= 0.0f)
|
||||
return 0.0f;
|
||||
|
||||
float modifiedPPM = ppm + ppmModifier;
|
||||
return (static_cast<float>(weaponSpeed) * modifiedPPM) / 600.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculate level 60+ reduction
|
||||
* Implements PROC_ATTR_REDUCE_PROC_60: 3.333% reduction per level above 60
|
||||
*
|
||||
* @param baseChance Base proc chance
|
||||
* @param actorLevel Actor's level
|
||||
* @return Reduced proc chance
|
||||
*/
|
||||
static float ApplyLevel60Reduction(float baseChance, uint32 actorLevel)
|
||||
{
|
||||
if (actorLevel <= 60)
|
||||
return baseChance;
|
||||
|
||||
// Reduction = (level - 60) / 30, capped at 1.0
|
||||
float reduction = static_cast<float>(actorLevel - 60) / 30.0f;
|
||||
return std::max(0.0f, (1.0f - reduction) * baseChance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Simulate CalcProcChance() from SpellAuras.cpp
|
||||
*
|
||||
* @param procEntry The proc configuration
|
||||
* @param actorLevel Actor's level (for PROC_ATTR_REDUCE_PROC_60)
|
||||
* @param weaponSpeed Weapon speed (for PPM calculation)
|
||||
* @param chanceModifier Talent/aura modifier to chance
|
||||
* @param ppmModifier Talent/aura modifier to PPM
|
||||
* @param hasDamageInfo Whether a DamageInfo is present (enables PPM)
|
||||
* @param hasHealInfo Whether a HealInfo is present (also enables PPM)
|
||||
* @return Calculated proc chance
|
||||
*/
|
||||
static float SimulateCalcProcChance(
|
||||
SpellProcEntry const& procEntry,
|
||||
uint32 actorLevel = 80,
|
||||
uint32 weaponSpeed = 2500,
|
||||
float chanceModifier = 0.0f,
|
||||
float ppmModifier = 0.0f,
|
||||
bool hasDamageInfo = true,
|
||||
bool hasHealInfo = false)
|
||||
{
|
||||
float chance = procEntry.Chance;
|
||||
|
||||
// PPM calculation overrides base chance if PPM > 0 and we have DamageInfo or HealInfo
|
||||
if ((hasDamageInfo || hasHealInfo) && procEntry.ProcsPerMinute > 0.0f)
|
||||
{
|
||||
chance = CalculatePPMChance(weaponSpeed, procEntry.ProcsPerMinute, ppmModifier);
|
||||
}
|
||||
|
||||
// Apply chance modifier (SPELLMOD_CHANCE_OF_SUCCESS)
|
||||
chance += chanceModifier;
|
||||
|
||||
// Apply level 60+ reduction if attribute is set
|
||||
if (procEntry.AttributesMask & PROC_ATTR_REDUCE_PROC_60)
|
||||
{
|
||||
chance = ApplyLevel60Reduction(chance, actorLevel);
|
||||
}
|
||||
|
||||
return chance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Simulate charge consumption from ConsumeProcCharges()
|
||||
*
|
||||
* @param aura The aura stub to modify
|
||||
* @param procEntry The proc configuration
|
||||
* @return true if aura was removed (charges/stacks exhausted)
|
||||
*/
|
||||
static bool SimulateConsumeProcCharges(AuraStub* aura, SpellProcEntry const& procEntry)
|
||||
{
|
||||
if (!aura)
|
||||
return false;
|
||||
|
||||
if (procEntry.AttributesMask & PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
{
|
||||
return aura->ModStackAmount(-1);
|
||||
}
|
||||
else if (aura->IsUsingCharges())
|
||||
{
|
||||
aura->DropCharge();
|
||||
if (aura->GetCharges() == 0)
|
||||
{
|
||||
aura->Remove();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if proc is on cooldown
|
||||
*
|
||||
* @param aura The aura stub
|
||||
* @param now Current time point
|
||||
* @return true if proc is blocked by cooldown
|
||||
*/
|
||||
static bool IsProcOnCooldown(AuraStub const* aura, std::chrono::steady_clock::time_point now)
|
||||
{
|
||||
if (!aura)
|
||||
return false;
|
||||
return aura->IsProcOnCooldown(now);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Apply proc cooldown to aura
|
||||
*
|
||||
* @param aura The aura stub
|
||||
* @param now Current time point
|
||||
* @param cooldownMs Cooldown duration in milliseconds
|
||||
*/
|
||||
static void ApplyProcCooldown(AuraStub* aura, std::chrono::steady_clock::time_point now, uint32 cooldownMs)
|
||||
{
|
||||
if (!aura || cooldownMs == 0)
|
||||
return;
|
||||
aura->AddProcCooldown(now + std::chrono::milliseconds(cooldownMs));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if spell has mana cost (for PROC_ATTR_REQ_MANA_COST)
|
||||
*
|
||||
* @param spellInfo The spell info to check
|
||||
* @return true if spell has mana cost > 0
|
||||
*/
|
||||
static bool SpellHasManaCost(SpellInfo const* spellInfo)
|
||||
{
|
||||
if (!spellInfo)
|
||||
return false;
|
||||
return spellInfo->ManaCost > 0 || spellInfo->ManaCostPercentage > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get common weapon speeds for testing
|
||||
*/
|
||||
static constexpr uint32 WEAPON_SPEED_FAST_DAGGER = 1400; // 1.4 sec
|
||||
static constexpr uint32 WEAPON_SPEED_NORMAL_SWORD = 2500; // 2.5 sec
|
||||
static constexpr uint32 WEAPON_SPEED_SLOW_2H = 3300; // 3.3 sec
|
||||
static constexpr uint32 WEAPON_SPEED_VERY_SLOW = 3800; // 3.8 sec
|
||||
static constexpr uint32 WEAPON_SPEED_STAFF = 3600; // 3.6 sec (common feral staff)
|
||||
|
||||
/**
|
||||
* @brief Shapeshift form base attack speeds (from SpellShapeshiftForm.dbc)
|
||||
*/
|
||||
static constexpr uint32 FORM_SPEED_CAT = 1000; // Cat Form: 1.0 sec
|
||||
static constexpr uint32 FORM_SPEED_BEAR = 2500; // Bear/Dire Bear: 2.5 sec
|
||||
|
||||
/**
|
||||
* @brief Simulate effective procs per minute
|
||||
*
|
||||
* Given a per-swing chance and the actual swing interval, calculate
|
||||
* how many procs occur per minute on average.
|
||||
*
|
||||
* @param chancePerSwing Proc chance per swing (0-100+)
|
||||
* @param actualSwingSpeedMs Actual time between swings in milliseconds
|
||||
* @return Average procs per minute
|
||||
*/
|
||||
static float CalculateEffectivePPM(float chancePerSwing, uint32 actualSwingSpeedMs)
|
||||
{
|
||||
if (actualSwingSpeedMs == 0)
|
||||
return 0.0f;
|
||||
float swingsPerMinute = 60000.0f / static_cast<float>(actualSwingSpeedMs);
|
||||
return swingsPerMinute * (chancePerSwing / 100.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Common PPM values from spell_proc database
|
||||
*/
|
||||
static constexpr float PPM_OMEN_OF_CLARITY = 6.0f;
|
||||
static constexpr float PPM_JUDGEMENT_OF_LIGHT = 15.0f;
|
||||
static constexpr float PPM_WINDFURY_WEAPON = 2.0f;
|
||||
|
||||
// =============================================================================
|
||||
// Triggered Spell Filtering - simulates SpellAuras.cpp:2191-2209
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief Auto-attack proc flag mask (hunter auto-shot, wands exception)
|
||||
* These triggered spells are allowed to proc without TRIGGERED_CAN_PROC
|
||||
*/
|
||||
static constexpr uint32 AUTO_ATTACK_PROC_FLAG_MASK =
|
||||
PROC_FLAG_DONE_MELEE_AUTO_ATTACK | PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK |
|
||||
PROC_FLAG_DONE_RANGED_AUTO_ATTACK | PROC_FLAG_TAKEN_RANGED_AUTO_ATTACK;
|
||||
|
||||
/**
|
||||
* @brief Configuration for simulating triggered spell filtering
|
||||
*/
|
||||
struct TriggeredSpellConfig
|
||||
{
|
||||
bool isTriggered = false; // Spell::IsTriggered()
|
||||
bool auraHasCanProcFromProcs = false; // SPELL_ATTR3_CAN_PROC_FROM_PROCS on proc aura
|
||||
bool spellHasNotAProc = false; // SPELL_ATTR3_NOT_A_PROC on triggering spell
|
||||
uint32 triggeredByAuraSpellId = 0; // GetTriggeredByAuraSpellInfo()->Id
|
||||
uint32 procAuraSpellId = 0; // The aura's spell ID (for self-loop check)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Simulate triggered spell filtering
|
||||
* Implements the self-loop prevention and triggered spell blocking from SpellAuras.cpp
|
||||
*
|
||||
* @param config Configuration for the triggered spell
|
||||
* @param procEntry The proc entry being checked
|
||||
* @param eventTypeMask The event type mask from ProcEventInfo
|
||||
* @return true if proc should be blocked (return 0), false if allowed
|
||||
*/
|
||||
static bool ShouldBlockTriggeredSpell(
|
||||
TriggeredSpellConfig const& config,
|
||||
SpellProcEntry const& procEntry,
|
||||
uint32 eventTypeMask)
|
||||
{
|
||||
// Self-loop prevention: block if triggered by the same aura
|
||||
// SpellAuras.cpp:2191-2192
|
||||
if (config.triggeredByAuraSpellId != 0 &&
|
||||
config.triggeredByAuraSpellId == config.procAuraSpellId)
|
||||
{
|
||||
return true; // Block: self-loop detected
|
||||
}
|
||||
|
||||
// Check if triggered spell filtering applies
|
||||
// SpellAuras.cpp:2195-2208
|
||||
static constexpr uint32 KILL_DEATH_PROC_FLAG_MASK =
|
||||
PROC_FLAG_KILL | PROC_FLAG_KILLED | PROC_FLAG_DEATH;
|
||||
|
||||
if (!config.auraHasCanProcFromProcs &&
|
||||
!(procEntry.AttributesMask & PROC_ATTR_TRIGGERED_CAN_PROC) &&
|
||||
!(eventTypeMask & AUTO_ATTACK_PROC_FLAG_MASK) &&
|
||||
!(eventTypeMask & KILL_DEATH_PROC_FLAG_MASK))
|
||||
{
|
||||
// Filter triggered spells unless they have NOT_A_PROC
|
||||
if (config.isTriggered && !config.spellHasNotAProc)
|
||||
{
|
||||
return true; // Block: triggered spell without exceptions
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Allow proc
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Extra Attack Chain-Proc Prevention - simulates SpellAuraEffects.cpp:1245-1261
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief Configuration for simulating extra attack chain-proc prevention
|
||||
*/
|
||||
struct ExtraAttackProcConfig
|
||||
{
|
||||
bool triggeredSpellHasExtraAttacks = false; // triggeredSpellInfo->HasEffect(SPELL_EFFECT_ADD_EXTRA_ATTACKS)
|
||||
uint32 triggerSpellId = 0; // m_spellInfo->Effects[GetEffIndex()].TriggerSpell
|
||||
uint32 lastExtraAttackSpell = 0; // eventInfo.GetActor()->GetLastExtraAttackSpell()
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Simulate extra attack chain-proc prevention from CheckEffectProc
|
||||
* Returns true if proc should be blocked
|
||||
*
|
||||
* @param config Extra attack proc configuration
|
||||
* @return true if proc should be blocked
|
||||
*/
|
||||
static bool ShouldBlockExtraAttackChainProc(ExtraAttackProcConfig const& config)
|
||||
{
|
||||
// Only applies when the triggered spell grants extra attacks
|
||||
if (!config.triggeredSpellHasExtraAttacks)
|
||||
return false;
|
||||
|
||||
// Patch 1.12.0(?) extra attack abilities can no longer chain proc themselves
|
||||
if (config.lastExtraAttackSpell == config.triggerSpellId)
|
||||
return true;
|
||||
|
||||
// Patch 2.2.0 Sword Specialization (Warrior, Rogue) extra attack can no longer proc additional extra attacks
|
||||
// 3.3.5 Sword Specialization (Warrior), Hack and Slash (Rogue)
|
||||
if (config.lastExtraAttackSpell == 16459 || config.lastExtraAttackSpell == 66923)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DisableEffectsMask - simulates SpellAuras.cpp:2244-2258
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief Apply DisableEffectsMask to get final proc effect mask
|
||||
*
|
||||
* @param initialMask Initial effect mask (usually 0x07 for all 3 effects)
|
||||
* @param disableEffectsMask Mask of effects to disable
|
||||
* @return Resulting effect mask after applying disable mask
|
||||
*/
|
||||
static uint8 ApplyDisableEffectsMask(uint8 initialMask, uint32 disableEffectsMask)
|
||||
{
|
||||
uint8 result = initialMask;
|
||||
for (uint8 i = 0; i < 3; ++i)
|
||||
{
|
||||
if (disableEffectsMask & (1u << i))
|
||||
result &= ~(1 << i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if proc should be blocked due to all effects being disabled
|
||||
*
|
||||
* @param initialMask Initial effect mask
|
||||
* @param disableEffectsMask Mask of effects to disable
|
||||
* @return true if all effects disabled (proc blocked), false otherwise
|
||||
*/
|
||||
static bool ShouldBlockDueToDisabledEffects(uint8 initialMask, uint32 disableEffectsMask)
|
||||
{
|
||||
return ApplyDisableEffectsMask(initialMask, disableEffectsMask) == 0;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PPM Modifier Simulation - simulates Unit.cpp:10378-10390
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief PPM modifier configuration for testing SPELLMOD_PROC_PER_MINUTE
|
||||
*/
|
||||
struct PPMModifierConfig
|
||||
{
|
||||
float flatModifier = 0.0f; // Additive PPM modifier
|
||||
float pctModifier = 1.0f; // Multiplicative PPM modifier (1.0 = no change)
|
||||
bool hasSpellModOwner = true; // Whether GetSpellModOwner() returns valid player
|
||||
bool hasSpellProto = true; // Whether spellProto is provided
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Calculate PPM chance with spell modifiers
|
||||
* Simulates GetPPMProcChance() with SPELLMOD_PROC_PER_MINUTE
|
||||
*/
|
||||
static float CalculatePPMChanceWithModifiers(
|
||||
uint32 weaponSpeed,
|
||||
float basePPM,
|
||||
PPMModifierConfig const& modConfig)
|
||||
{
|
||||
if (basePPM <= 0.0f)
|
||||
return 0.0f;
|
||||
|
||||
float ppm = basePPM;
|
||||
|
||||
// Apply modifiers only if we have spell proto and spell mod owner
|
||||
if (modConfig.hasSpellProto && modConfig.hasSpellModOwner)
|
||||
{
|
||||
// Apply flat modifier first (SPELLMOD_FLAT)
|
||||
ppm += modConfig.flatModifier;
|
||||
// Apply percent modifier (SPELLMOD_PCT)
|
||||
ppm *= modConfig.pctModifier;
|
||||
}
|
||||
|
||||
return (static_cast<float>(weaponSpeed) * ppm) / 600.0f;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Equipment Requirements - simulates SpellAuras.cpp:2260-2298
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief Item classes for equipment requirement checking
|
||||
*/
|
||||
static constexpr int32 ITEM_CLASS_WEAPON = 2;
|
||||
static constexpr int32 ITEM_CLASS_ARMOR = 4;
|
||||
static constexpr int32 ITEM_CLASS_ANY = -1; // No requirement
|
||||
|
||||
/**
|
||||
* @brief Attack types for weapon slot mapping
|
||||
*/
|
||||
static constexpr uint8 BASE_ATTACK = 0;
|
||||
static constexpr uint8 OFF_ATTACK = 1;
|
||||
static constexpr uint8 RANGED_ATTACK = 2;
|
||||
|
||||
/**
|
||||
* @brief Configuration for simulating equipment requirements
|
||||
*/
|
||||
struct EquipmentConfig
|
||||
{
|
||||
bool isPassive = true; // Aura::IsPassive()
|
||||
bool isPlayer = true; // Target is player
|
||||
int32 equippedItemClass = ITEM_CLASS_ANY; // SpellInfo::EquippedItemClass
|
||||
int32 equippedItemSubClassMask = 0; // SpellInfo::EquippedItemSubClassMask
|
||||
bool hasNoEquipRequirementAttr = false; // SPELL_ATTR3_NO_PROC_EQUIP_REQUIREMENT
|
||||
uint8 attackType = BASE_ATTACK; // Attack type for weapon slot mapping
|
||||
bool isInFeralForm = false; // Player::IsInFeralForm()
|
||||
bool hasEquippedItem = true; // Item is equipped in the slot
|
||||
bool itemIsBroken = false; // Item::IsBroken()
|
||||
bool itemFitsRequirements = true; // Item::IsFitToSpellRequirements()
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Simulate equipment requirement check
|
||||
* Returns true if proc should be blocked due to equipment requirements
|
||||
*
|
||||
* @param config Equipment configuration
|
||||
* @return true if proc should be blocked
|
||||
*/
|
||||
static bool ShouldBlockDueToEquipment(EquipmentConfig const& config)
|
||||
{
|
||||
// Only check for passive player auras with equipment requirements
|
||||
if (!config.isPassive || !config.isPlayer || config.equippedItemClass == ITEM_CLASS_ANY)
|
||||
return false;
|
||||
|
||||
// SPELL_ATTR3_NO_PROC_EQUIP_REQUIREMENT bypasses check
|
||||
if (config.hasNoEquipRequirementAttr)
|
||||
return false;
|
||||
|
||||
// Feral form blocks weapon procs
|
||||
if (config.equippedItemClass == ITEM_CLASS_WEAPON && config.isInFeralForm)
|
||||
return true;
|
||||
|
||||
// No item equipped in the required slot
|
||||
if (!config.hasEquippedItem)
|
||||
return true;
|
||||
|
||||
// Item is broken
|
||||
if (config.itemIsBroken)
|
||||
return true;
|
||||
|
||||
// Item doesn't fit spell requirements (wrong subclass, etc.)
|
||||
if (!config.itemFitsRequirements)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get equipment slot for attack type
|
||||
*
|
||||
* @param attackType Attack type (BASE_ATTACK, OFF_ATTACK, RANGED_ATTACK)
|
||||
* @return Equipment slot index (simulated)
|
||||
*/
|
||||
static uint8 GetWeaponSlotForAttackType(uint8 attackType)
|
||||
{
|
||||
switch (attackType)
|
||||
{
|
||||
case BASE_ATTACK:
|
||||
return 15; // EQUIPMENT_SLOT_MAINHAND
|
||||
case OFF_ATTACK:
|
||||
return 16; // EQUIPMENT_SLOT_OFFHAND
|
||||
case RANGED_ATTACK:
|
||||
return 17; // EQUIPMENT_SLOT_RANGED
|
||||
default:
|
||||
return 15;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Cascade Proc Suppression - simulates Unit.cpp TriggerAurasProcOnEvent
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief Configuration for simulating cascade proc suppression
|
||||
*
|
||||
* Models the two paths in TriggerAurasProcOnEvent that call SetCantProc():
|
||||
* 1. Outer check: triggering spell has TRIGGERED_DISALLOW_PROC_EVENTS
|
||||
* 2. Per-aura check: aura has SPELL_ATTR3_INSTANT_TARGET_PROCS (0x80000)
|
||||
*/
|
||||
struct CascadeProcConfig
|
||||
{
|
||||
bool triggeringSpellIsProcDisabled = false; // Spell::IsProcDisabled()
|
||||
bool auraHasDisableProcAttr = false; // SpellInfo::HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Returns true if cascading procs should be suppressed for this aura
|
||||
*
|
||||
* @param config Cascade proc configuration
|
||||
* @return true if SetCantProc(true) would be active during this aura's proc
|
||||
*/
|
||||
static bool ShouldSuppressCascadingProc(CascadeProcConfig const& config)
|
||||
{
|
||||
// Outer check: triggering spell disables all cascading procs
|
||||
if (config.triggeringSpellIsProcDisabled)
|
||||
return true;
|
||||
// Per-aura check: aura itself suppresses cascading
|
||||
if (config.auraHasDisableProcAttr)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TAKEN Auto-Trigger Logic - simulates SpellMgr.cpp:2033-2049
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief Configuration for simulating the TAKEN auto-trigger logic
|
||||
* from SpellMgr::LoadSpellProcs() auto-generation
|
||||
*/
|
||||
struct TakenAutoTriggerConfig
|
||||
{
|
||||
uint32 procFlags = 0; // SpellInfo::ProcFlags
|
||||
uint32 auraName = 0; // Effect's ApplyAuraName
|
||||
bool isAlwaysTriggeredAura = false; // Already set by isAlwaysTriggeredAura[]
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Simulate the TAKEN auto-trigger logic from SpellMgr::LoadSpellProcs()
|
||||
*
|
||||
* During auto-generation of proc entries, TAKEN-proc auras with
|
||||
* SPELL_AURA_PROC_TRIGGER_SPELL or SPELL_AURA_PROC_TRIGGER_DAMAGE
|
||||
* should get PROC_ATTR_TRIGGERED_CAN_PROC set automatically.
|
||||
*
|
||||
* @param config Configuration describing the aura
|
||||
* @return true if addTriggerFlag should be set
|
||||
*/
|
||||
static bool ShouldAutoAddTriggeredCanProc(TakenAutoTriggerConfig const& config)
|
||||
{
|
||||
// If already marked as always-triggered, keep it
|
||||
if (config.isAlwaysTriggeredAura)
|
||||
return true;
|
||||
|
||||
// TAKEN auto-trigger: TAKEN proc flags + PROC_TRIGGER_SPELL/DAMAGE
|
||||
if (config.procFlags & TAKEN_HIT_PROC_FLAG_MASK)
|
||||
{
|
||||
switch (config.auraName)
|
||||
{
|
||||
case SPELL_AURA_PROC_TRIGGER_SPELL:
|
||||
case SPELL_AURA_PROC_TRIGGER_DAMAGE:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Conditions System - simulates SpellAuras.cpp:2232-2236
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief Configuration for simulating conditions system
|
||||
*/
|
||||
struct ConditionsConfig
|
||||
{
|
||||
bool hasConditions = false; // ConditionMgr has conditions for this spell
|
||||
bool conditionsMet = true; // All conditions are satisfied
|
||||
uint32 sourceType = 24; // CONDITION_SOURCE_TYPE_SPELL_PROC
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Simulate conditions check
|
||||
* Returns true if proc should be blocked due to conditions
|
||||
*
|
||||
* @param config Conditions configuration
|
||||
* @return true if proc should be blocked
|
||||
*/
|
||||
static bool ShouldBlockDueToConditions(ConditionsConfig const& config)
|
||||
{
|
||||
// No conditions configured - allow proc
|
||||
if (!config.hasConditions)
|
||||
return false;
|
||||
|
||||
// Check if conditions are met
|
||||
return !config.conditionsMet;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Test context for proc simulation scenarios
|
||||
*/
|
||||
class ProcTestScenario
|
||||
{
|
||||
public:
|
||||
ProcTestScenario() : _now(std::chrono::steady_clock::now()) {}
|
||||
|
||||
// Time control
|
||||
void AdvanceTime(std::chrono::milliseconds duration)
|
||||
{
|
||||
_now += duration;
|
||||
}
|
||||
|
||||
std::chrono::steady_clock::time_point GetNow() const { return _now; }
|
||||
|
||||
// Actor configuration
|
||||
UnitStub& GetActor() { return _actor; }
|
||||
UnitStub const& GetActor() const { return _actor; }
|
||||
|
||||
ProcTestScenario& WithActorLevel(uint8_t level)
|
||||
{
|
||||
_actor.SetLevel(level);
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcTestScenario& WithWeaponSpeed(uint8_t attackType, uint32_t speed)
|
||||
{
|
||||
_actor.SetAttackTime(attackType, speed);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Aura configuration
|
||||
std::unique_ptr<AuraStub>& GetAura() { return _aura; }
|
||||
|
||||
ProcTestScenario& WithAura(uint32_t spellId, uint8_t charges = 0, uint8_t stacks = 1)
|
||||
{
|
||||
_aura = std::make_unique<AuraStub>(spellId);
|
||||
_aura->SetCharges(charges);
|
||||
_aura->SetUsingCharges(charges > 0);
|
||||
_aura->SetStackAmount(stacks);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Simulate proc and return whether it triggered
|
||||
bool SimulateProc(SpellProcEntry const& procEntry, float rollResult = 0.0f)
|
||||
{
|
||||
if (!_aura)
|
||||
return false;
|
||||
|
||||
// Check cooldown
|
||||
if (ProcChanceTestHelper::IsProcOnCooldown(_aura.get(), _now))
|
||||
return false;
|
||||
|
||||
// Calculate chance
|
||||
float chance = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry,
|
||||
_actor.GetLevel(),
|
||||
_actor.GetAttackTime(0));
|
||||
|
||||
// Roll check (rollResult of 0 means always pass)
|
||||
if (rollResult > 0.0f && rollResult > chance)
|
||||
return false;
|
||||
|
||||
// Apply cooldown if set
|
||||
if (procEntry.Cooldown.count() > 0)
|
||||
{
|
||||
ProcChanceTestHelper::ApplyProcCooldown(_aura.get(), _now,
|
||||
static_cast<uint32>(procEntry.Cooldown.count()));
|
||||
}
|
||||
|
||||
// Consume charges
|
||||
ProcChanceTestHelper::SimulateConsumeProcCharges(_aura.get(), procEntry);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
std::chrono::steady_clock::time_point _now;
|
||||
UnitStub _actor;
|
||||
std::unique_ptr<AuraStub> _aura;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Simulates the proc chain guard logic from Unit::TriggerAurasProcOnEvent
|
||||
*
|
||||
* Tracks the m_procDeep counter to verify that:
|
||||
* - TRIGGERED_DISALLOW_PROC_EVENTS on the triggering spell disables procs
|
||||
* for all auras in the container
|
||||
* - SPELL_ATTR3_INSTANT_TARGET_PROCS on individual auras disables procs
|
||||
* only during that specific aura's TriggerProcOnEvent call
|
||||
* - The counter is properly balanced (returns to 0 after function exits)
|
||||
*/
|
||||
class ProcChainGuardSimulator
|
||||
{
|
||||
public:
|
||||
struct AuraConfig
|
||||
{
|
||||
uint32 spellId = 0;
|
||||
bool hasInstantTargetProcs = false; // SPELL_ATTR3_INSTANT_TARGET_PROCS
|
||||
bool isRemoved = false; // AuraApplication::GetRemoveMode()
|
||||
};
|
||||
|
||||
struct ProcRecord
|
||||
{
|
||||
uint32 spellId;
|
||||
bool canProcDuringTrigger; // CanProc() state when TriggerProcOnEvent fires
|
||||
int32 procDeepDuringTrigger; // m_procDeep value during trigger
|
||||
};
|
||||
|
||||
ProcChainGuardSimulator() : _procDeep(0) {}
|
||||
|
||||
/**
|
||||
* @brief Simulate Unit::TriggerAurasProcOnEvent
|
||||
*
|
||||
* @param triggeringSpellHasDisallowProcEvents Whether the triggering spell
|
||||
* has TRIGGERED_DISALLOW_PROC_EVENTS cast flag
|
||||
* @param auras List of auras in the proc container
|
||||
*/
|
||||
void SimulateTriggerAurasProc(
|
||||
bool triggeringSpellHasDisallowProcEvents,
|
||||
std::vector<AuraConfig> const& auras)
|
||||
{
|
||||
_records.clear();
|
||||
|
||||
bool const disableProcs = triggeringSpellHasDisallowProcEvents;
|
||||
if (disableProcs)
|
||||
SetCantProc(true);
|
||||
|
||||
for (auto const& aura : auras)
|
||||
{
|
||||
if (aura.isRemoved)
|
||||
continue;
|
||||
|
||||
if (aura.hasInstantTargetProcs)
|
||||
SetCantProc(true);
|
||||
|
||||
// Record CanProc() state during TriggerProcOnEvent
|
||||
_records.push_back({
|
||||
aura.spellId,
|
||||
CanProc(),
|
||||
_procDeep
|
||||
});
|
||||
|
||||
if (aura.hasInstantTargetProcs)
|
||||
SetCantProc(false);
|
||||
}
|
||||
|
||||
if (disableProcs)
|
||||
SetCantProc(false);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<ProcRecord> const& GetRecords() const
|
||||
{
|
||||
return _records;
|
||||
}
|
||||
|
||||
[[nodiscard]] int32 GetProcDeep() const { return _procDeep; }
|
||||
[[nodiscard]] bool CanProc() const { return _procDeep == 0; }
|
||||
|
||||
private:
|
||||
void SetCantProc(bool apply)
|
||||
{
|
||||
if (apply)
|
||||
++_procDeep;
|
||||
else
|
||||
--_procDeep;
|
||||
}
|
||||
|
||||
int32 _procDeep;
|
||||
std::vector<ProcRecord> _records;
|
||||
};
|
||||
|
||||
#endif // AZEROTHCORE_PROC_CHANCE_TEST_HELPER_H
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef AZEROTHCORE_PROC_EVENT_INFO_HELPER_H
|
||||
#define AZEROTHCORE_PROC_EVENT_INFO_HELPER_H
|
||||
|
||||
#include "SpellMgr.h"
|
||||
#include "Unit.h"
|
||||
|
||||
/**
|
||||
* @brief Builder class for creating ProcEventInfo test instances
|
||||
*
|
||||
* This helper allows easy construction of ProcEventInfo objects for unit testing
|
||||
* the proc system without requiring full game objects.
|
||||
*/
|
||||
class ProcEventInfoBuilder
|
||||
{
|
||||
public:
|
||||
ProcEventInfoBuilder()
|
||||
: _actor(nullptr), _actionTarget(nullptr), _procTarget(nullptr),
|
||||
_typeMask(0), _spellTypeMask(0), _spellPhaseMask(0), _hitMask(0),
|
||||
_spell(nullptr), _damageInfo(nullptr), _healInfo(nullptr),
|
||||
_triggeredByAuraSpell(nullptr), _procAuraEffectIndex(-1) {}
|
||||
|
||||
ProcEventInfoBuilder& WithActor(Unit* actor)
|
||||
{
|
||||
_actor = actor;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcEventInfoBuilder& WithActionTarget(Unit* target)
|
||||
{
|
||||
_actionTarget = target;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcEventInfoBuilder& WithProcTarget(Unit* target)
|
||||
{
|
||||
_procTarget = target;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcEventInfoBuilder& WithTypeMask(uint32 typeMask)
|
||||
{
|
||||
_typeMask = typeMask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcEventInfoBuilder& WithSpellTypeMask(uint32 spellTypeMask)
|
||||
{
|
||||
_spellTypeMask = spellTypeMask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcEventInfoBuilder& WithSpellPhaseMask(uint32 spellPhaseMask)
|
||||
{
|
||||
_spellPhaseMask = spellPhaseMask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcEventInfoBuilder& WithHitMask(uint32 hitMask)
|
||||
{
|
||||
_hitMask = hitMask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcEventInfoBuilder& WithSpell(Spell const* spell)
|
||||
{
|
||||
_spell = spell;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcEventInfoBuilder& WithDamageInfo(DamageInfo* damageInfo)
|
||||
{
|
||||
_damageInfo = damageInfo;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcEventInfoBuilder& WithHealInfo(HealInfo* healInfo)
|
||||
{
|
||||
_healInfo = healInfo;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcEventInfoBuilder& WithTriggeredByAuraSpell(SpellInfo const* spellInfo)
|
||||
{
|
||||
_triggeredByAuraSpell = spellInfo;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcEventInfoBuilder& WithProcAuraEffectIndex(int8 index)
|
||||
{
|
||||
_procAuraEffectIndex = index;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProcEventInfo Build()
|
||||
{
|
||||
return ProcEventInfo(_actor, _actionTarget, _procTarget, _typeMask,
|
||||
_spellTypeMask, _spellPhaseMask, _hitMask,
|
||||
_spell, _damageInfo, _healInfo,
|
||||
_triggeredByAuraSpell, _procAuraEffectIndex);
|
||||
}
|
||||
|
||||
private:
|
||||
Unit* _actor;
|
||||
Unit* _actionTarget;
|
||||
Unit* _procTarget;
|
||||
uint32 _typeMask;
|
||||
uint32 _spellTypeMask;
|
||||
uint32 _spellPhaseMask;
|
||||
uint32 _hitMask;
|
||||
Spell const* _spell;
|
||||
DamageInfo* _damageInfo;
|
||||
HealInfo* _healInfo;
|
||||
SpellInfo const* _triggeredByAuraSpell;
|
||||
int8 _procAuraEffectIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder class for creating SpellProcEntry test instances
|
||||
*
|
||||
* This helper allows easy construction of SpellProcEntry objects for unit testing
|
||||
* the proc system.
|
||||
*/
|
||||
class SpellProcEntryBuilder
|
||||
{
|
||||
public:
|
||||
SpellProcEntryBuilder()
|
||||
{
|
||||
_entry = {};
|
||||
}
|
||||
|
||||
SpellProcEntryBuilder& WithSchoolMask(uint32 schoolMask)
|
||||
{
|
||||
_entry.SchoolMask = schoolMask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellProcEntryBuilder& WithSpellFamilyName(uint32 familyName)
|
||||
{
|
||||
_entry.SpellFamilyName = familyName;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellProcEntryBuilder& WithSpellFamilyMask(flag96 familyMask)
|
||||
{
|
||||
_entry.SpellFamilyMask = familyMask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellProcEntryBuilder& WithProcFlags(uint32 procFlags)
|
||||
{
|
||||
_entry.ProcFlags = procFlags;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellProcEntryBuilder& WithSpellTypeMask(uint32 spellTypeMask)
|
||||
{
|
||||
_entry.SpellTypeMask = spellTypeMask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellProcEntryBuilder& WithSpellPhaseMask(uint32 spellPhaseMask)
|
||||
{
|
||||
_entry.SpellPhaseMask = spellPhaseMask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellProcEntryBuilder& WithHitMask(uint32 hitMask)
|
||||
{
|
||||
_entry.HitMask = hitMask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellProcEntryBuilder& WithAttributesMask(uint32 attributesMask)
|
||||
{
|
||||
_entry.AttributesMask = attributesMask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellProcEntryBuilder& WithDisableEffectsMask(uint32 disableEffectsMask)
|
||||
{
|
||||
_entry.DisableEffectsMask = disableEffectsMask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellProcEntryBuilder& WithProcsPerMinute(float ppm)
|
||||
{
|
||||
_entry.ProcsPerMinute = ppm;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellProcEntryBuilder& WithChance(float chance)
|
||||
{
|
||||
_entry.Chance = chance;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellProcEntryBuilder& WithCooldown(Milliseconds cooldown)
|
||||
{
|
||||
_entry.Cooldown = cooldown;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellProcEntryBuilder& WithCharges(uint32 charges)
|
||||
{
|
||||
_entry.Charges = charges;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellProcEntry Build() const
|
||||
{
|
||||
return _entry;
|
||||
}
|
||||
|
||||
private:
|
||||
SpellProcEntry _entry;
|
||||
};
|
||||
|
||||
#endif //AZEROTHCORE_PROC_EVENT_INFO_HELPER_H
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef AZEROTHCORE_SPELL_INFO_TEST_HELPER_H
|
||||
#define AZEROTHCORE_SPELL_INFO_TEST_HELPER_H
|
||||
|
||||
#include "SpellInfo.h"
|
||||
#include "SharedDefines.h"
|
||||
#include <memory>
|
||||
|
||||
/**
|
||||
* @brief Helper class to create SpellEntry test instances
|
||||
*
|
||||
* This creates a SpellEntry with sensible defaults for unit testing.
|
||||
*/
|
||||
class TestSpellEntryHelper
|
||||
{
|
||||
public:
|
||||
TestSpellEntryHelper()
|
||||
{
|
||||
// Zero initialize all fields
|
||||
std::memset(&_entry, 0, sizeof(_entry));
|
||||
|
||||
// Set safe defaults
|
||||
_entry.EquippedItemClass = -1;
|
||||
_entry.SchoolMask = SPELL_SCHOOL_MASK_NORMAL;
|
||||
|
||||
// Initialize empty strings
|
||||
for (auto& name : _entry.SpellName)
|
||||
name = "";
|
||||
for (auto& rank : _entry.Rank)
|
||||
rank = "";
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithId(uint32 id)
|
||||
{
|
||||
_entry.Id = id;
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithSpellFamilyName(uint32 familyName)
|
||||
{
|
||||
_entry.SpellFamilyName = familyName;
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithSpellFamilyFlags(uint32 flag0, uint32 flag1 = 0, uint32 flag2 = 0)
|
||||
{
|
||||
_entry.SpellFamilyFlags[0] = flag0;
|
||||
_entry.SpellFamilyFlags[1] = flag1;
|
||||
_entry.SpellFamilyFlags[2] = flag2;
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithSchoolMask(uint32 schoolMask)
|
||||
{
|
||||
_entry.SchoolMask = schoolMask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithProcFlags(uint32 procFlags)
|
||||
{
|
||||
_entry.ProcFlags = procFlags;
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithProcChance(uint32 procChance)
|
||||
{
|
||||
_entry.ProcChance = procChance;
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithProcCharges(uint32 procCharges)
|
||||
{
|
||||
_entry.ProcCharges = procCharges;
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithDmgClass(uint32 dmgClass)
|
||||
{
|
||||
_entry.DmgClass = dmgClass;
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithAttributesEx3(uint32 attr)
|
||||
{
|
||||
_entry.AttributesEx3 = attr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithEffect(uint8 effIndex, uint32 effect, uint32 auraType = 0)
|
||||
{
|
||||
if (effIndex < MAX_SPELL_EFFECTS)
|
||||
{
|
||||
_entry.Effect[effIndex] = effect;
|
||||
_entry.EffectApplyAuraName[effIndex] = auraType;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithEffectTriggerSpell(uint8 effIndex, uint32 triggerSpell)
|
||||
{
|
||||
if (effIndex < MAX_SPELL_EFFECTS)
|
||||
{
|
||||
_entry.EffectTriggerSpell[effIndex] = triggerSpell;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithEffectBasePoints(uint8 effIndex, int32 basePoints)
|
||||
{
|
||||
if (effIndex < MAX_SPELL_EFFECTS)
|
||||
_entry.EffectBasePoints[effIndex] = basePoints;
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithEffectDieSides(uint8 effIndex, int32 dieSides)
|
||||
{
|
||||
if (effIndex < MAX_SPELL_EFFECTS)
|
||||
_entry.EffectDieSides[effIndex] = dieSides;
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithAttributes(uint32 attr)
|
||||
{
|
||||
_entry.Attributes = attr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellEntry const* Get() const
|
||||
{
|
||||
return &_entry;
|
||||
}
|
||||
|
||||
private:
|
||||
SpellEntry _entry;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder class for creating SpellInfo test instances
|
||||
*
|
||||
* This helper allows easy construction of SpellInfo objects for unit testing
|
||||
* without requiring DBC data.
|
||||
*/
|
||||
class SpellInfoBuilder
|
||||
{
|
||||
public:
|
||||
SpellInfoBuilder() : _entryHelper() {}
|
||||
|
||||
SpellInfoBuilder& WithId(uint32 id)
|
||||
{
|
||||
_entryHelper.WithId(id);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithSpellFamilyName(uint32 familyName)
|
||||
{
|
||||
_entryHelper.WithSpellFamilyName(familyName);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithSpellFamilyFlags(uint32 flag0, uint32 flag1 = 0, uint32 flag2 = 0)
|
||||
{
|
||||
_entryHelper.WithSpellFamilyFlags(flag0, flag1, flag2);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithSchoolMask(uint32 schoolMask)
|
||||
{
|
||||
_entryHelper.WithSchoolMask(schoolMask);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithProcFlags(uint32 procFlags)
|
||||
{
|
||||
_entryHelper.WithProcFlags(procFlags);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithProcChance(uint32 procChance)
|
||||
{
|
||||
_entryHelper.WithProcChance(procChance);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithProcCharges(uint32 procCharges)
|
||||
{
|
||||
_entryHelper.WithProcCharges(procCharges);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithDmgClass(uint32 dmgClass)
|
||||
{
|
||||
_entryHelper.WithDmgClass(dmgClass);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithAttributesEx3(uint32 attr)
|
||||
{
|
||||
_entryHelper.WithAttributesEx3(attr);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithEffect(uint8 effIndex, uint32 effect, uint32 auraType = 0)
|
||||
{
|
||||
_entryHelper.WithEffect(effIndex, effect, auraType);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithEffectTriggerSpell(uint8 effIndex, uint32 triggerSpell)
|
||||
{
|
||||
_entryHelper.WithEffectTriggerSpell(effIndex, triggerSpell);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithEffectBasePoints(uint8 effIndex, int32 basePoints)
|
||||
{
|
||||
_entryHelper.WithEffectBasePoints(effIndex, basePoints);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithEffectDieSides(uint8 effIndex, int32 dieSides)
|
||||
{
|
||||
_entryHelper.WithEffectDieSides(effIndex, dieSides);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithAttributes(uint32 attr)
|
||||
{
|
||||
_entryHelper.WithAttributes(attr);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Builds and returns a SpellInfo pointer
|
||||
// Note: Caller is responsible for lifetime management
|
||||
SpellInfo* Build()
|
||||
{
|
||||
return new SpellInfo(_entryHelper.Get());
|
||||
}
|
||||
|
||||
// Builds and returns a managed SpellInfo pointer
|
||||
std::unique_ptr<SpellInfo> BuildUnique()
|
||||
{
|
||||
return std::unique_ptr<SpellInfo>(new SpellInfo(_entryHelper.Get()));
|
||||
}
|
||||
|
||||
private:
|
||||
TestSpellEntryHelper _entryHelper;
|
||||
};
|
||||
|
||||
#endif //AZEROTHCORE_SPELL_INFO_TEST_HELPER_H
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "TestCreature.h"
|
||||
#include "ThreatManager.h"
|
||||
#include "CombatManager.h"
|
||||
|
||||
// Heap-allocated to avoid static init calling CreatureMovementData()
|
||||
// which requires sWorld to be set up (calls getIntConfig).
|
||||
CreatureTemplate* TestCreature::_fakeCreatureTemplate = nullptr;
|
||||
bool TestCreature::_fakeTemplateInitialized = false;
|
||||
|
||||
TestCreature::TestCreature() : Creature()
|
||||
{
|
||||
}
|
||||
|
||||
TestCreature::~TestCreature()
|
||||
{
|
||||
CleanupCombatState();
|
||||
}
|
||||
|
||||
void TestCreature::CleanupCombatState()
|
||||
{
|
||||
m_threatManager.ClearAllThreat();
|
||||
m_combatManager.EndAllCombat();
|
||||
// Must remove from world before destruction or ~Object will ABORT
|
||||
SetInWorld(false);
|
||||
}
|
||||
|
||||
void TestCreature::ForceInitValues(ObjectGuid::LowType guidLow, uint32 entry)
|
||||
{
|
||||
Object::_Create(guidLow, entry, HighGuid::Unit);
|
||||
|
||||
m_objectType |= TYPEMASK_UNIT;
|
||||
m_objectTypeId = TYPEID_UNIT;
|
||||
|
||||
m_originalEntry = entry;
|
||||
|
||||
// Initialize the fake creature template once with safe defaults.
|
||||
// Heap-allocated because CreatureMovementData() constructor needs sWorld.
|
||||
if (!_fakeTemplateInitialized)
|
||||
{
|
||||
_fakeCreatureTemplate = new CreatureTemplate();
|
||||
_fakeCreatureTemplate->Entry = 0;
|
||||
_fakeCreatureTemplate->faction = 14; // hostile monster default
|
||||
_fakeCreatureTemplate->unit_class = 1; // CLASS_WARRIOR
|
||||
_fakeCreatureTemplate->speed_walk = 1.0f;
|
||||
_fakeCreatureTemplate->speed_run = 1.14286f;
|
||||
_fakeCreatureTemplate->speed_swim = 1.0f;
|
||||
_fakeCreatureTemplate->speed_flight = 1.0f;
|
||||
_fakeCreatureTemplate->DamageModifier = 1.0f;
|
||||
_fakeCreatureTemplate->BaseAttackTime = 2000;
|
||||
_fakeCreatureTemplate->RangeAttackTime = 2000;
|
||||
_fakeCreatureTemplate->BaseVariance = 1.0f;
|
||||
_fakeCreatureTemplate->RangeVariance = 1.0f;
|
||||
_fakeCreatureTemplate->ModHealth = 1.0f;
|
||||
_fakeCreatureTemplate->ModMana = 1.0f;
|
||||
_fakeCreatureTemplate->ModArmor = 1.0f;
|
||||
_fakeCreatureTemplate->ModExperience = 1.0f;
|
||||
_fakeCreatureTemplate->HoverHeight = 1.0f;
|
||||
_fakeCreatureTemplate->detection_range = 20.0f;
|
||||
_fakeCreatureTemplate->flags_extra = 0;
|
||||
_fakeCreatureTemplate->unit_flags = 0;
|
||||
_fakeCreatureTemplate->unit_flags2 = 0;
|
||||
_fakeCreatureTemplate->dynamicflags = 0;
|
||||
_fakeCreatureTemplate->type = 0;
|
||||
_fakeCreatureTemplate->type_flags = 0;
|
||||
// Movement is default-constructed by new CreatureTemplate():
|
||||
// Ground=Run, Swim=true -> CanWalk() and CanSwim() return true
|
||||
_fakeTemplateInitialized = true;
|
||||
}
|
||||
|
||||
m_creatureInfo = _fakeCreatureTemplate;
|
||||
}
|
||||
|
||||
void TestCreature::SetTestMap(Map* map)
|
||||
{
|
||||
_testMap = map;
|
||||
// Also set the base class map pointer so GetMap() works
|
||||
// through the Unit* base pointer (polymorphic calls)
|
||||
WorldObject::SetMap(map);
|
||||
}
|
||||
|
||||
void TestCreature::SetAlive(bool alive)
|
||||
{
|
||||
m_deathState = alive ? DeathState::Alive : DeathState::Dead;
|
||||
}
|
||||
|
||||
void TestCreature::SetInWorld(bool inWorld)
|
||||
{
|
||||
if (inWorld && !Object::IsInWorld())
|
||||
Object::AddToWorld();
|
||||
else if (!inWorld && Object::IsInWorld())
|
||||
Object::RemoveFromWorld();
|
||||
}
|
||||
|
||||
void TestCreature::SetPhase(uint32 phase)
|
||||
{
|
||||
SetPhaseMask(phase, false);
|
||||
}
|
||||
|
||||
void TestCreature::SetFaction(uint32 faction)
|
||||
{
|
||||
// Set faction directly, bypassing Unit::SetFaction which calls
|
||||
// UpdateMoveInLineOfSightState() -> sObjectMgr->GetCreatureTemplate()
|
||||
SetUInt32Value(UNIT_FIELD_FACTIONTEMPLATE, faction);
|
||||
}
|
||||
|
||||
void TestCreature::SetupForCombatTest(Map* map, ObjectGuid::LowType guidLow, uint32 entry)
|
||||
{
|
||||
ForceInitValues(guidLow, entry);
|
||||
// SetTestMap calls WorldObject::SetMap which asserts !IsInWorld(),
|
||||
// so we must set map BEFORE SetInWorld
|
||||
SetTestMap(map);
|
||||
SetInWorld(true);
|
||||
SetAlive(true);
|
||||
SetPhase(1);
|
||||
SetHostileFaction();
|
||||
SetIsCombatDisallowed(false);
|
||||
ClearUnitState(UNIT_STATE_EVADE);
|
||||
ClearUnitState(UNIT_STATE_IN_FLIGHT);
|
||||
InitializeThreatManager();
|
||||
}
|
||||
|
||||
void TestCreature::InitializeThreatManager()
|
||||
{
|
||||
m_threatManager.Initialize();
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef TEST_CREATURE_H
|
||||
#define TEST_CREATURE_H
|
||||
|
||||
#include "Creature.h"
|
||||
#include "CreatureData.h"
|
||||
#include "ObjectGuid.h"
|
||||
|
||||
class TestMap;
|
||||
|
||||
/**
|
||||
* TestCreature - A test harness for Creature that bypasses database dependencies.
|
||||
*
|
||||
* Usage:
|
||||
* TestCreature* creature = new TestCreature();
|
||||
* creature->ForceInitValues(1, 12345); // guidLow, entry
|
||||
* creature->SetTestMap(testMap);
|
||||
* creature->SetAlive(true);
|
||||
* creature->SetupForCombatTest(); // Sets up all necessary state for combat/threat tests
|
||||
*/
|
||||
class TestCreature : public Creature
|
||||
{
|
||||
public:
|
||||
TestCreature();
|
||||
~TestCreature() override;
|
||||
|
||||
// Override methods that require database/world access
|
||||
void UpdateObjectVisibility(bool /*forced*/ = true, bool /*fromUpdate*/ = false) override { }
|
||||
void AddToWorld() override { }
|
||||
void RemoveFromWorld() override { }
|
||||
|
||||
// Force initialization without database
|
||||
void ForceInitValues(ObjectGuid::LowType guidLow, uint32 entry);
|
||||
|
||||
// Test control methods
|
||||
void SetTestMap(Map* map);
|
||||
|
||||
// Set alive state (affects m_deathState)
|
||||
void SetAlive(bool alive);
|
||||
|
||||
// Set in-world state (affects m_inWorld)
|
||||
void SetInWorld(bool inWorld);
|
||||
|
||||
// Set phase mask for phase checks
|
||||
void SetPhase(uint32 phase);
|
||||
|
||||
// Set faction for friendliness checks
|
||||
// Use hostile factions (14 = hostile monster) for combat tests
|
||||
void SetHostileFaction() { SetFaction(14); }
|
||||
void SetFriendlyFaction() { SetFaction(35); }
|
||||
void SetFaction(uint32 faction);
|
||||
|
||||
// Complete setup for combat/threat testing
|
||||
// Sets creature to be alive, in-world, hostile, and initializes managers
|
||||
void SetupForCombatTest(Map* map, ObjectGuid::LowType guidLow, uint32 entry);
|
||||
|
||||
// Initialize ThreatManager for testing
|
||||
void InitializeThreatManager();
|
||||
|
||||
// Access managers directly for testing
|
||||
ThreatManager& TestGetThreatMgr() { return m_threatManager; }
|
||||
CombatManager& TestGetCombatMgr() { return m_combatManager; }
|
||||
|
||||
// Clear all combat state for cleanup
|
||||
void CleanupCombatState();
|
||||
|
||||
private:
|
||||
Map* _testMap = nullptr;
|
||||
static CreatureTemplate* _fakeCreatureTemplate;
|
||||
static bool _fakeTemplateInitialized;
|
||||
};
|
||||
|
||||
#endif // TEST_CREATURE_H
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "TestMap.h"
|
||||
#include "DBCStores.h"
|
||||
#include "ScriptMgr.h"
|
||||
#include "ScriptDefines/AllMapScript.h"
|
||||
#include "ScriptDefines/GlobalScript.h"
|
||||
#include "ScriptDefines/MiscScript.h"
|
||||
#include "ScriptDefines/UnitScript.h"
|
||||
#include "ScriptDefines/WorldObjectScript.h"
|
||||
|
||||
TestMap::TestMap()
|
||||
: Map(0, 0, REGULAR_DIFFICULTY, nullptr)
|
||||
{
|
||||
_fakeMapEntry = {};
|
||||
_fakeMapEntry.map_type = MAP_COMMON;
|
||||
_fakeMapEntry.MapID = 0;
|
||||
const_cast<MapEntry const*&>(i_mapEntry) = &_fakeMapEntry;
|
||||
}
|
||||
|
||||
TestMap::~TestMap()
|
||||
{
|
||||
}
|
||||
|
||||
/*static*/ void TestMap::EnsureDBC()
|
||||
{
|
||||
static bool initialized = false;
|
||||
if (initialized)
|
||||
return;
|
||||
initialized = true;
|
||||
|
||||
// Insert a fake MapEntry so Map constructor doesn't crash
|
||||
if (!sMapStore.LookupEntry(0))
|
||||
{
|
||||
auto* entry = new MapEntry{};
|
||||
entry->MapID = 0;
|
||||
entry->map_type = MAP_COMMON;
|
||||
entry->entrance_map = -1;
|
||||
sMapStore.SetEntry(0, entry);
|
||||
}
|
||||
|
||||
// Initialize all script registries so CALL_ENABLED_HOOKS doesn't
|
||||
// crash on uninitialized vectors during Object/Unit/Map operations
|
||||
ScriptRegistry<AllMapScript>::InitEnabledHooksIfNeeded(ALLMAPHOOK_END);
|
||||
ScriptRegistry<GlobalScript>::InitEnabledHooksIfNeeded(GLOBALHOOK_END);
|
||||
ScriptRegistry<MiscScript>::InitEnabledHooksIfNeeded(MISCHOOK_END);
|
||||
ScriptRegistry<UnitScript>::InitEnabledHooksIfNeeded(UNITHOOK_END);
|
||||
ScriptRegistry<WorldObjectScript>::InitEnabledHooksIfNeeded(WORLDOBJECTHOOK_END);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef TEST_MAP_H
|
||||
#define TEST_MAP_H
|
||||
|
||||
#include "Map.h"
|
||||
#include "DBCStructure.h"
|
||||
|
||||
/**
|
||||
* TestMap - A minimal test harness for Map.
|
||||
*
|
||||
* Since Map has a complex constructor requiring MapEntry from DBC,
|
||||
* this creates a fake MapEntry to control IsRaid()/IsDungeon() behavior.
|
||||
*
|
||||
* Usage:
|
||||
* TestMap* map = new TestMap();
|
||||
* map->SetIsRaid(true); // Make this a raid map
|
||||
*/
|
||||
class TestMap : public Map
|
||||
{
|
||||
public:
|
||||
TestMap();
|
||||
~TestMap() override;
|
||||
|
||||
// Must be called before constructing TestMap to insert fake DBC entry
|
||||
// and initialize script registries
|
||||
static void EnsureDBC();
|
||||
|
||||
// Control map type for testing
|
||||
void SetMapType(uint32 type) { _fakeMapEntry.map_type = type; }
|
||||
void SetIsRaid(bool val) { _fakeMapEntry.map_type = val ? MAP_RAID : MAP_COMMON; }
|
||||
void SetIsDungeon(bool val) { _fakeMapEntry.map_type = val ? MAP_INSTANCE : MAP_COMMON; }
|
||||
|
||||
private:
|
||||
MapEntry _fakeMapEntry;
|
||||
};
|
||||
|
||||
#endif // TEST_MAP_H
|
||||
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef AZEROTHCORE_UNIT_STUB_H
|
||||
#define AZEROTHCORE_UNIT_STUB_H
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
class SpellInfo;
|
||||
class Aura;
|
||||
class AuraEffect;
|
||||
class Item;
|
||||
|
||||
/**
|
||||
* @brief Lightweight stub for Unit proc-related functionality
|
||||
*
|
||||
* This stub provides controlled behavior for testing proc scripts
|
||||
* without requiring the full Unit hierarchy.
|
||||
*/
|
||||
class UnitStub
|
||||
{
|
||||
public:
|
||||
UnitStub() = default;
|
||||
virtual ~UnitStub() = default;
|
||||
|
||||
// Identity
|
||||
virtual bool IsPlayer() const { return _isPlayer; }
|
||||
virtual bool IsAlive() const { return _isAlive; }
|
||||
virtual bool IsFriendlyTo(UnitStub const* unit) const { return _isFriendly; }
|
||||
|
||||
void SetIsPlayer(bool isPlayer) { _isPlayer = isPlayer; }
|
||||
void SetIsAlive(bool isAlive) { _isAlive = isAlive; }
|
||||
void SetIsFriendly(bool isFriendly) { _isFriendly = isFriendly; }
|
||||
|
||||
// Aura management
|
||||
virtual bool HasAura(uint32_t spellId) const
|
||||
{
|
||||
return _auras.find(spellId) != _auras.end();
|
||||
}
|
||||
|
||||
virtual void AddAuraStub(uint32_t spellId)
|
||||
{
|
||||
_auras[spellId] = true;
|
||||
}
|
||||
|
||||
virtual void RemoveAuraStub(uint32_t spellId)
|
||||
{
|
||||
_auras.erase(spellId);
|
||||
}
|
||||
|
||||
// Spell casting tracking
|
||||
struct CastRecord
|
||||
{
|
||||
uint32_t spellId;
|
||||
bool triggered;
|
||||
int32_t bp0;
|
||||
int32_t bp1;
|
||||
int32_t bp2;
|
||||
};
|
||||
|
||||
virtual void RecordCast(uint32_t spellId, bool triggered = true,
|
||||
int32_t bp0 = 0, int32_t bp1 = 0, int32_t bp2 = 0)
|
||||
{
|
||||
_castHistory.push_back({spellId, triggered, bp0, bp1, bp2});
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<CastRecord> const& GetCastHistory() const { return _castHistory; }
|
||||
|
||||
[[nodiscard]] bool WasCast(uint32_t spellId) const
|
||||
{
|
||||
for (auto const& record : _castHistory)
|
||||
{
|
||||
if (record.spellId == spellId)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] size_t CountCasts(uint32_t spellId) const
|
||||
{
|
||||
size_t count = 0;
|
||||
for (auto const& record : _castHistory)
|
||||
{
|
||||
if (record.spellId == spellId)
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void ClearCastHistory() { _castHistory.clear(); }
|
||||
|
||||
// Health/Power
|
||||
virtual uint32_t GetMaxHealth() const { return _maxHealth; }
|
||||
virtual uint32_t GetHealth() const { return _health; }
|
||||
virtual uint32_t CountPctFromMaxHealth(int32_t pct) const
|
||||
{
|
||||
return (_maxHealth * static_cast<uint32_t>(pct)) / 100;
|
||||
}
|
||||
|
||||
void SetMaxHealth(uint32_t maxHealth) { _maxHealth = maxHealth; }
|
||||
void SetHealth(uint32_t health) { _health = health; }
|
||||
|
||||
// Weapon speed for PPM calculations
|
||||
virtual uint32_t GetAttackTime(uint8_t attType) const
|
||||
{
|
||||
return _attackTimes.count(attType) ? _attackTimes.at(attType) : 2000;
|
||||
}
|
||||
|
||||
void SetAttackTime(uint8_t attType, uint32_t time)
|
||||
{
|
||||
_attackTimes[attType] = time;
|
||||
}
|
||||
|
||||
// PPM modifier tracking for proc tests
|
||||
// Simulates Player::ApplySpellMod(spellId, SPELLMOD_PROC_PER_MINUTE, ppm)
|
||||
void SetPPMModifier(uint32_t spellId, float modifier)
|
||||
{
|
||||
_ppmModifiers[spellId] = modifier;
|
||||
}
|
||||
|
||||
void ClearPPMModifiers()
|
||||
{
|
||||
_ppmModifiers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculate PPM proc chance with modifiers
|
||||
* Mimics Unit::GetPPMProcChance() formula: (WeaponSpeed * PPM) / 600.0f
|
||||
*/
|
||||
virtual float GetPPMProcChance(uint32_t weaponSpeed, float ppm, uint32_t spellId = 0) const
|
||||
{
|
||||
if (ppm <= 0.0f)
|
||||
return 0.0f;
|
||||
|
||||
// Apply PPM modifier if set for this spell
|
||||
float modifiedPPM = ppm;
|
||||
if (spellId > 0 && _ppmModifiers.count(spellId))
|
||||
modifiedPPM += _ppmModifiers.at(spellId);
|
||||
|
||||
return (static_cast<float>(weaponSpeed) * modifiedPPM) / 600.0f;
|
||||
}
|
||||
|
||||
// Chance modifier tracking for proc tests
|
||||
// Simulates Player::ApplySpellMod(spellId, SPELLMOD_CHANCE_OF_SUCCESS, chance)
|
||||
void SetChanceModifier(uint32_t spellId, float modifier)
|
||||
{
|
||||
_chanceModifiers[spellId] = modifier;
|
||||
}
|
||||
|
||||
void ClearChanceModifiers()
|
||||
{
|
||||
_chanceModifiers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Apply chance modifier for a spell
|
||||
*/
|
||||
float ApplyChanceModifier(uint32_t spellId, float baseChance) const
|
||||
{
|
||||
if (spellId > 0 && _chanceModifiers.count(spellId))
|
||||
return baseChance + _chanceModifiers.at(spellId);
|
||||
return baseChance;
|
||||
}
|
||||
|
||||
// Cooldowns
|
||||
virtual bool HasSpellCooldown(uint32_t spellId) const
|
||||
{
|
||||
return _cooldowns.find(spellId) != _cooldowns.end();
|
||||
}
|
||||
|
||||
virtual void AddSpellCooldown(uint32_t spellId)
|
||||
{
|
||||
_cooldowns[spellId] = true;
|
||||
}
|
||||
|
||||
virtual void RemoveSpellCooldown(uint32_t spellId)
|
||||
{
|
||||
_cooldowns.erase(spellId);
|
||||
}
|
||||
|
||||
// Class/Level
|
||||
virtual uint8_t GetClass() const { return _class; }
|
||||
virtual uint8_t GetLevel() const { return _level; }
|
||||
|
||||
void SetClass(uint8_t unitClass) { _class = unitClass; }
|
||||
void SetLevel(uint8_t level) { _level = level; }
|
||||
|
||||
private:
|
||||
bool _isPlayer = false;
|
||||
bool _isAlive = true;
|
||||
bool _isFriendly = false;
|
||||
|
||||
std::map<uint32_t, bool> _auras;
|
||||
std::vector<CastRecord> _castHistory;
|
||||
std::map<uint32_t, bool> _cooldowns;
|
||||
std::map<uint8_t, uint32_t> _attackTimes;
|
||||
std::map<uint32_t, float> _ppmModifiers; // PPM modifiers by spell ID
|
||||
std::map<uint32_t, float> _chanceModifiers; // Chance modifiers by spell ID
|
||||
|
||||
uint32_t _maxHealth = 10000;
|
||||
uint32_t _health = 10000;
|
||||
uint8_t _class = 1; // Warrior by default
|
||||
uint8_t _level = 80;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief GMock-enabled Unit stub for verification
|
||||
*/
|
||||
class MockUnitStub : public UnitStub
|
||||
{
|
||||
public:
|
||||
MOCK_METHOD(bool, IsPlayer, (), (const, override));
|
||||
MOCK_METHOD(bool, IsAlive, (), (const, override));
|
||||
MOCK_METHOD(bool, IsFriendlyTo, (UnitStub const* unit), (const, override));
|
||||
MOCK_METHOD(bool, HasAura, (uint32_t spellId), (const, override));
|
||||
MOCK_METHOD(uint32_t, GetMaxHealth, (), (const, override));
|
||||
MOCK_METHOD(uint32_t, GetHealth, (), (const, override));
|
||||
MOCK_METHOD(uint32_t, CountPctFromMaxHealth, (int32_t pct), (const, override));
|
||||
MOCK_METHOD(uint32_t, GetAttackTime, (uint8_t attType), (const, override));
|
||||
MOCK_METHOD(bool, HasSpellCooldown, (uint32_t spellId), (const, override));
|
||||
MOCK_METHOD(uint8_t, GetClass, (), (const, override));
|
||||
MOCK_METHOD(uint8_t, GetLevel, (), (const, override));
|
||||
MOCK_METHOD(float, GetPPMProcChance, (uint32_t weaponSpeed, float ppm, uint32_t spellId), (const, override));
|
||||
};
|
||||
|
||||
#endif //AZEROTHCORE_UNIT_STUB_H
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef AZEROTHCORE_WORLDMOCK_H
|
||||
#define AZEROTHCORE_WORLDMOCK_H
|
||||
|
||||
#include "ArenaSpectator.h"
|
||||
#include "Duration.h"
|
||||
#include "IWorld.h"
|
||||
#include "gmock/gmock.h"
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-parameter"
|
||||
|
||||
inline void AddScripts() {}
|
||||
|
||||
class WorldMock: public IWorld
|
||||
{
|
||||
public:
|
||||
~WorldMock() override { }
|
||||
MOCK_METHOD(bool, IsClosed, (), (const));
|
||||
MOCK_METHOD(void, SetClosed, (bool val), ());
|
||||
MOCK_METHOD(AccountTypes, GetPlayerSecurityLimit, (), (const));
|
||||
MOCK_METHOD(void, SetPlayerSecurityLimit, (AccountTypes sec), ());
|
||||
MOCK_METHOD(void, LoadDBAllowedSecurityLevel, ());
|
||||
MOCK_METHOD(bool, getAllowMovement, (), (const));
|
||||
MOCK_METHOD(void, SetAllowMovement, (bool allow), ());
|
||||
MOCK_METHOD(LocaleConstant, GetDefaultDbcLocale, (), (const));
|
||||
MOCK_METHOD(std::string const&, GetDataPath, (), (const));
|
||||
MOCK_METHOD(Seconds, GetNextDailyQuestsResetTime, (), (const));
|
||||
MOCK_METHOD(Seconds, GetNextWeeklyQuestsResetTime, (), (const));
|
||||
MOCK_METHOD(Seconds, GetNextRandomBGResetTime, (), (const));
|
||||
MOCK_METHOD(uint16, GetConfigMaxSkillValue, (), (const));
|
||||
MOCK_METHOD(void, SetInitialWorldSettings, ());
|
||||
MOCK_METHOD(void, LoadConfigSettings, (bool reload), ());
|
||||
MOCK_METHOD(bool, IsShuttingDown, (), (const));
|
||||
MOCK_METHOD(uint32, GetShutDownTimeLeft, (), (const));
|
||||
MOCK_METHOD(void, ShutdownServ, (uint32 time, uint32 options, uint8 exitcode, const std::string& reason), ());
|
||||
MOCK_METHOD(void, ShutdownCancel, ());
|
||||
MOCK_METHOD(void, ShutdownMsg, (bool show, Player* player, const std::string& reason), ());
|
||||
MOCK_METHOD(void, Update, (uint32 diff), ());
|
||||
MOCK_METHOD(void, setRate, (ServerConfigs index, float value), ());
|
||||
MOCK_METHOD(float, getRate, (ServerConfigs index), (const));
|
||||
MOCK_METHOD(void, setBoolConfig, (ServerConfigs index, bool value), ());
|
||||
MOCK_METHOD(bool, getBoolConfig, (ServerConfigs index), (const));
|
||||
MOCK_METHOD(void, setFloatConfig, (ServerConfigs index, float value), ());
|
||||
MOCK_METHOD(float, getFloatConfig, (ServerConfigs index), (const));
|
||||
MOCK_METHOD(void, setIntConfig, (ServerConfigs index, uint32 value), ());
|
||||
MOCK_METHOD(uint32, getIntConfig, (ServerConfigs index), (const));
|
||||
MOCK_METHOD(void, setStringConfig, (ServerConfigs index, std::string const& value), ());
|
||||
MOCK_METHOD(std::string_view, getStringConfig, (ServerConfigs index), (const));
|
||||
MOCK_METHOD(void, setWorldState, (uint32 index, uint64 value), ());
|
||||
MOCK_METHOD(uint64, getWorldState, (uint32 index), (const));
|
||||
MOCK_METHOD(void, LoadWorldStates, ());
|
||||
MOCK_METHOD(bool, IsPvPRealm, (), (const));
|
||||
MOCK_METHOD(bool, IsFFAPvPRealm, (), (const));
|
||||
MOCK_METHOD(uint32, GetNextWhoListUpdateDelaySecs, ());
|
||||
MOCK_METHOD(void, ProcessCliCommands, ());
|
||||
MOCK_METHOD(void, QueueCliCommand, (CliCommandHolder* commandHolder), ());
|
||||
MOCK_METHOD(void, ForceGameEventUpdate, ());
|
||||
MOCK_METHOD(void, UpdateRealmCharCount, (uint32 accid), ());
|
||||
MOCK_METHOD(LocaleConstant, GetAvailableDbcLocale, (LocaleConstant locale), (const));
|
||||
MOCK_METHOD(void, LoadDBVersion, ());
|
||||
MOCK_METHOD(char const *, GetDBVersion, (), (const));
|
||||
MOCK_METHOD(void, UpdateAreaDependentAuras, ());
|
||||
MOCK_METHOD(uint32, GetCleaningFlags, (), (const));
|
||||
MOCK_METHOD(void, SetCleaningFlags, (uint32 flags), ());
|
||||
MOCK_METHOD(void, ResetEventSeasonalQuests, (uint16 event_id), ());
|
||||
MOCK_METHOD(time_t, GetNextTimeWithDayAndHour, (int8 dayOfWeek, int8 hour), ());
|
||||
MOCK_METHOD(time_t, GetNextTimeWithMonthAndHour, (int8 month, int8 hour), ());
|
||||
MOCK_METHOD(std::string const&, GetRealmName, (), (const));
|
||||
MOCK_METHOD(void, SetRealmName, (std::string name), ());
|
||||
MOCK_METHOD(void, RemoveOldCorpses, ());
|
||||
MOCK_METHOD(void, ReloadRBAC, ());
|
||||
};
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#endif //AZEROTHCORE_WORLDMOCK_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "ScriptMgr.h"
|
||||
#include "ScriptDefines/ArenaScript.h"
|
||||
#include "ScriptDefines/AllBattlegroundScript.h"
|
||||
#include "ArenaTeam.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include "WorldMock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
/**
|
||||
* Tests that ArenaScript and AllBattlegroundScript hooks return
|
||||
* safe defaults when no scripts are registered, ensuring core
|
||||
* game logic (MemberWon/MemberLost, SaveToDB, etc.) is not
|
||||
* accidentally skipped.
|
||||
*/
|
||||
class ArenaHookDefaultsTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
static void EnsureScriptRegistriesInitialized()
|
||||
{
|
||||
static bool initialized = false;
|
||||
if (!initialized)
|
||||
{
|
||||
ScriptRegistry<ArenaScript>::InitEnabledHooksIfNeeded(ARENAHOOK_END);
|
||||
ScriptRegistry<BGScript>::InitEnabledHooksIfNeeded(ALLBATTLEGROUNDHOOK_END);
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
previousWorld_ = std::move(sWorld);
|
||||
auto* mock = new ::testing::NiceMock<WorldMock>();
|
||||
ON_CALL(*mock, getIntConfig(::testing::_))
|
||||
.WillByDefault(::testing::Return(0));
|
||||
ON_CALL(*mock, getIntConfig(CONFIG_LEGACY_ARENA_START_RATING))
|
||||
.WillByDefault(::testing::Return(1500));
|
||||
ON_CALL(*mock, getIntConfig(CONFIG_ARENA_START_RATING))
|
||||
.WillByDefault(::testing::Return(0));
|
||||
sWorld.reset(mock);
|
||||
|
||||
EnsureScriptRegistriesInitialized();
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
sWorld = std::move(previousWorld_);
|
||||
}
|
||||
|
||||
std::unique_ptr<IWorld> previousWorld_;
|
||||
};
|
||||
|
||||
// CanSaveToDB must return true by default so ArenaTeam::SaveToDB
|
||||
// proceeds to write team and member stats to the database.
|
||||
TEST_F(ArenaHookDefaultsTest, CanSaveToDBDefaultsTrue)
|
||||
{
|
||||
ArenaTeam team;
|
||||
EXPECT_TRUE(sScriptMgr->CanSaveToDB(&team));
|
||||
}
|
||||
|
||||
// OnBeforeArenaTeamMemberUpdate must return true by default so that
|
||||
// MemberWon/MemberLost execute. A false return would skip personal
|
||||
// rating and game count updates for all arena participants.
|
||||
TEST_F(ArenaHookDefaultsTest, OnBeforeArenaTeamMemberUpdateDefaultsTrue)
|
||||
{
|
||||
ArenaTeam team;
|
||||
EXPECT_TRUE(sScriptMgr->OnBeforeArenaTeamMemberUpdate(&team, nullptr, true, 1500, 0));
|
||||
EXPECT_TRUE(sScriptMgr->OnBeforeArenaTeamMemberUpdate(&team, nullptr, false, 1500, 0));
|
||||
}
|
||||
|
||||
// CanSaveArenaStatsForMember must return true by default so that
|
||||
// character_arena_stats (MMR, MaxMMR) are written to the database.
|
||||
TEST_F(ArenaHookDefaultsTest, CanSaveArenaStatsForMemberDefaultsTrue)
|
||||
{
|
||||
ArenaTeam team;
|
||||
EXPECT_TRUE(sScriptMgr->CanSaveArenaStatsForMember(&team, ObjectGuid::Empty));
|
||||
}
|
||||
|
||||
// OnBeforeArenaCheckWinConditions must return true by default so
|
||||
// the normal win condition check proceeds.
|
||||
TEST_F(ArenaHookDefaultsTest, OnBeforeArenaCheckWinConditionsDefaultsTrue)
|
||||
{
|
||||
EXPECT_TRUE(sScriptMgr->OnBeforeArenaCheckWinConditions(nullptr));
|
||||
}
|
||||
|
||||
// CanAddGroupToMatchingPool must return true by default so groups
|
||||
// are not filtered out of the BG matchmaking pool.
|
||||
TEST_F(ArenaHookDefaultsTest, CanAddGroupToMatchingPoolDefaultsTrue)
|
||||
{
|
||||
EXPECT_TRUE(sScriptMgr->CanAddGroupToMatchingPool(nullptr, nullptr, 0, nullptr, BattlegroundBracketId(0)));
|
||||
}
|
||||
|
||||
// Verify the calling convention used in Arena::EndBattleground:
|
||||
// if (sScriptMgr->OnBeforeArenaTeamMemberUpdate(...))
|
||||
// team->MemberWon/MemberLost(...)
|
||||
//
|
||||
// The hook returns true when no scripts override it.
|
||||
// The caller must NOT negate the result, or MemberWon/MemberLost
|
||||
// will never execute and arena stats will silently stop saving.
|
||||
TEST_F(ArenaHookDefaultsTest, MemberUpdateCallingConventionAllowsByDefault)
|
||||
{
|
||||
ArenaTeam team;
|
||||
bool hookResult = sScriptMgr->OnBeforeArenaTeamMemberUpdate(
|
||||
&team, nullptr, true, 1500, 0);
|
||||
|
||||
// This simulates the condition in Arena::EndBattleground.
|
||||
// MemberWon must be called when no scripts are registered.
|
||||
bool memberWonWouldExecute = hookResult; // NOT !hookResult
|
||||
EXPECT_TRUE(memberWonWouldExecute)
|
||||
<< "MemberWon/MemberLost must execute when no scripts override "
|
||||
"OnBeforeArenaTeamMemberUpdate. Check Arena.cpp is using "
|
||||
"if(sScriptMgr->OnBeforeArenaTeamMemberUpdate(...)) without negation.";
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "Define.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "ArenaSeasonRewardsDistributor.h"
|
||||
#include "WorldMock.h"
|
||||
#include <memory>
|
||||
|
||||
class MockArenaSeasonTeamRewarder : public ArenaSeasonTeamRewarder
|
||||
{
|
||||
public:
|
||||
MOCK_METHOD(void, RewardTeamWithRewardGroup, (ArenaTeam* arenaTeam, ArenaSeasonRewardGroup const& rewardGroup), (override));
|
||||
};
|
||||
|
||||
class ArenaSeasonRewardDistributorTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_previousWorld = std::move(sWorld);
|
||||
_worldMock = new ::testing::NiceMock<WorldMock>();
|
||||
ON_CALL(*_worldMock, getIntConfig(::testing::_)).WillByDefault(::testing::Return(0));
|
||||
ON_CALL(*_worldMock, getIntConfig(CONFIG_LEGACY_ARENA_START_RATING)).WillByDefault(::testing::Return(1500));
|
||||
ON_CALL(*_worldMock, getIntConfig(CONFIG_ARENA_START_RATING)).WillByDefault(::testing::Return(0));
|
||||
sWorld.reset(_worldMock);
|
||||
|
||||
_mockRewarder = std::make_unique<MockArenaSeasonTeamRewarder>();
|
||||
_distributor = std::make_unique<ArenaSeasonRewardDistributor>(_mockRewarder.get());
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
sWorld = std::move(_previousWorld);
|
||||
}
|
||||
|
||||
std::unique_ptr<MockArenaSeasonTeamRewarder> _mockRewarder;
|
||||
std::unique_ptr<ArenaSeasonRewardDistributor> _distributor;
|
||||
std::unique_ptr<IWorld> _previousWorld;
|
||||
::testing::NiceMock<WorldMock>* _worldMock = nullptr;
|
||||
};
|
||||
|
||||
ArenaTeam ArenaTeamWithRating(int rating, int gamesPlayed)
|
||||
{
|
||||
ArenaTeamStats stats;
|
||||
stats.Rating = rating;
|
||||
stats.SeasonGames = gamesPlayed;
|
||||
ArenaTeam team;
|
||||
team.SetArenaTeamStats(stats);
|
||||
return team;
|
||||
}
|
||||
|
||||
// This test verifies that a single team receives the correct reward group when multiple percent reward groups are defined.
|
||||
TEST_F(ArenaSeasonRewardDistributorTest, SingleTeamMultiplePctRewardDistribution)
|
||||
{
|
||||
ArenaTeamMgr::ArenaTeamContainer arenaTeams;
|
||||
std::vector<ArenaSeasonRewardGroup> rewardGroups;
|
||||
|
||||
ArenaTeam team = ArenaTeamWithRating(1500, 50);
|
||||
arenaTeams[1] = &team;
|
||||
|
||||
ArenaSeasonRewardGroup rewardGroup;
|
||||
rewardGroup.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
|
||||
rewardGroup.minCriteria = 0;
|
||||
rewardGroup.maxCriteria = 0.5;
|
||||
rewardGroups.push_back(rewardGroup);
|
||||
ArenaSeasonRewardGroup rewardGroup2;
|
||||
rewardGroup2.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
|
||||
rewardGroup2.minCriteria = 0.5;
|
||||
rewardGroup2.maxCriteria = 100;
|
||||
rewardGroups.push_back(rewardGroup2);
|
||||
|
||||
EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&team, rewardGroup2)).Times(1);
|
||||
|
||||
_distributor->DistributeRewards(arenaTeams, rewardGroups);
|
||||
}
|
||||
|
||||
// This test verifies that a single team receives the correct reward group when multiple abs percent reward groups are defined.
|
||||
TEST_F(ArenaSeasonRewardDistributorTest, SingleTeamMultipleAbsRewardDistribution)
|
||||
{
|
||||
ArenaTeamMgr::ArenaTeamContainer arenaTeams;
|
||||
std::vector<ArenaSeasonRewardGroup> rewardGroups;
|
||||
|
||||
ArenaTeam team = ArenaTeamWithRating(1500, 50);
|
||||
arenaTeams[1] = &team;
|
||||
|
||||
ArenaSeasonRewardGroup rewardGroup;
|
||||
rewardGroup.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_ABSOLUTE_VALUE;
|
||||
rewardGroup.minCriteria = 1;
|
||||
rewardGroup.maxCriteria = 1;
|
||||
rewardGroups.push_back(rewardGroup);
|
||||
ArenaSeasonRewardGroup rewardGroup2;
|
||||
rewardGroup2.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_ABSOLUTE_VALUE;
|
||||
rewardGroup2.minCriteria = 2;
|
||||
rewardGroup2.maxCriteria = 10;
|
||||
rewardGroups.push_back(rewardGroup2);
|
||||
|
||||
EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&team, rewardGroup)).Times(1);
|
||||
|
||||
_distributor->DistributeRewards(arenaTeams, rewardGroups);
|
||||
}
|
||||
|
||||
// Input: 1000 teams with incremental ratings and two reward groups with 0% - 0.5% and 0.5% - 3% percentage criteria.
|
||||
// Purpose: Ensures that the top 0.5% of teams receive the first reward and the next 3% receive the second reward.
|
||||
// Each team should be rewarded only once.
|
||||
TEST_F(ArenaSeasonRewardDistributorTest, ManyTeamsTwoRewardsDistribution)
|
||||
{
|
||||
ArenaTeamMgr::ArenaTeamContainer arenaTeams;
|
||||
std::vector<ArenaSeasonRewardGroup> rewardGroups;
|
||||
|
||||
const int numTeams = 1000;
|
||||
ArenaTeam teams[numTeams + 1]; // used just to prevent teams deletion
|
||||
for (int i = 1; i <= numTeams; i++)
|
||||
{
|
||||
teams[i] = ArenaTeamWithRating(i, 50);
|
||||
arenaTeams[i] = &teams[i];
|
||||
}
|
||||
|
||||
ArenaSeasonRewardGroup rewardGroup1;
|
||||
rewardGroup1.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
|
||||
rewardGroup1.minCriteria = 0.0; // 0%
|
||||
rewardGroup1.maxCriteria = 0.5; // 0.5% of total teams
|
||||
rewardGroups.push_back(rewardGroup1);
|
||||
|
||||
ArenaSeasonRewardGroup rewardGroup2;
|
||||
rewardGroup2.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
|
||||
rewardGroup2.minCriteria = 0.5; // 0.5% (the top 0.5% of the teams)
|
||||
rewardGroup2.maxCriteria = 3.0; // 3% of total teams
|
||||
rewardGroups.push_back(rewardGroup2);
|
||||
|
||||
ArenaSeasonRewardGroup rewardGroup3;
|
||||
rewardGroup3.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
|
||||
rewardGroup3.minCriteria = 3;
|
||||
rewardGroup3.maxCriteria = 10;
|
||||
rewardGroups.push_back(rewardGroup3);
|
||||
|
||||
ArenaSeasonRewardGroup rewardGroup4;
|
||||
rewardGroup4.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
|
||||
rewardGroup4.minCriteria = 10;
|
||||
rewardGroup4.maxCriteria = 35;
|
||||
rewardGroups.push_back(rewardGroup4);
|
||||
|
||||
// Top 1
|
||||
ArenaSeasonRewardGroup rewardGroup5;
|
||||
rewardGroup5.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_ABSOLUTE_VALUE;
|
||||
rewardGroup5.minCriteria = 1;
|
||||
rewardGroup5.maxCriteria = 1;
|
||||
rewardGroups.push_back(rewardGroup5);
|
||||
|
||||
// Calculate expected reward distributions
|
||||
int expectedTeamsInGroup1 = static_cast<int>(0.005 * numTeams); // 0.5% of 1000 = 5
|
||||
int expectedTeamsInGroup2 = static_cast<int>(0.03 * numTeams); // 3% of 1000 = 30
|
||||
int expectedTeamsInGroup3 = static_cast<int>(0.10 * numTeams); // 10% of 1000 = 100
|
||||
int expectedTeamsInGroup4 = static_cast<int>(0.35 * numTeams); // 35% of 1000 = 350
|
||||
|
||||
int teamsIndexCounter = numTeams;
|
||||
|
||||
// Expectation for rewardGroup1 (top 0.5% of teams)
|
||||
for (; teamsIndexCounter > numTeams - expectedTeamsInGroup1; --teamsIndexCounter)
|
||||
EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&teams[teamsIndexCounter], rewardGroup1)).Times(1);
|
||||
|
||||
// Expectation for rewardGroup2 (next 3% - 0.5% teams)
|
||||
for (; teamsIndexCounter > numTeams - expectedTeamsInGroup2; --teamsIndexCounter)
|
||||
EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&teams[teamsIndexCounter], rewardGroup2)).Times(1);
|
||||
|
||||
for (; teamsIndexCounter > numTeams - expectedTeamsInGroup3; --teamsIndexCounter)
|
||||
EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&teams[teamsIndexCounter], rewardGroup3)).Times(1);
|
||||
|
||||
for (; teamsIndexCounter > numTeams - expectedTeamsInGroup4; --teamsIndexCounter)
|
||||
EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&teams[teamsIndexCounter], rewardGroup4)).Times(1);
|
||||
|
||||
// Top 1
|
||||
EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&teams[numTeams], rewardGroup5)).Times(1);
|
||||
|
||||
_distributor->DistributeRewards(arenaTeams, rewardGroups);
|
||||
}
|
||||
|
||||
// Input: Three teams where one has fewer than the minimum required games and two have enough games.
|
||||
// Purpose: Ensures that only teams meeting the minimum required games threshold are eligible for rewards.
|
||||
TEST_F(ArenaSeasonRewardDistributorTest, MinimumRequiredGamesFilter)
|
||||
{
|
||||
ArenaTeamMgr::ArenaTeamContainer arenaTeams;
|
||||
std::vector<ArenaSeasonRewardGroup> rewardGroups;
|
||||
|
||||
// Creating three teams: one below and two above the minRequiredGames threshold (30 games)
|
||||
ArenaTeam team1 = ArenaTeamWithRating(1500, 50); // Eligible, as it has 50 games
|
||||
ArenaTeam team2 = ArenaTeamWithRating(1100, 20); // Not eligible, as it has only 20 games
|
||||
ArenaTeam team3 = ArenaTeamWithRating(1300, 40); // Eligible, as it has 40 games
|
||||
|
||||
// Adding teams to the container
|
||||
arenaTeams[1] = &team1;
|
||||
arenaTeams[2] = &team2;
|
||||
arenaTeams[3] = &team3;
|
||||
|
||||
// Creating a single reward group covering all teams
|
||||
ArenaSeasonRewardGroup rewardGroup;
|
||||
rewardGroup.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
|
||||
rewardGroup.minCriteria = 0.0;
|
||||
rewardGroup.maxCriteria = 100;
|
||||
rewardGroups.push_back(rewardGroup);
|
||||
|
||||
// We expect the rewarder to be called for team1 and team3, but not for team2.
|
||||
EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&team1, rewardGroup)).Times(1);
|
||||
EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&team3, rewardGroup)).Times(1);
|
||||
EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&team2, rewardGroup)).Times(0);
|
||||
|
||||
_distributor->DistributeRewards(arenaTeams, rewardGroups);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "Define.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "ArenaTeamFilter.h"
|
||||
#include "ArenaTeamMgr.h"
|
||||
#include "ArenaTeam.h"
|
||||
#include <memory>
|
||||
#include "WorldMock.h"
|
||||
|
||||
// Used to expose Type property.
|
||||
class ArenaTeamTest : public ArenaTeam
|
||||
{
|
||||
public:
|
||||
void SetType(uint8 type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
};
|
||||
|
||||
ArenaTeam* ArenaTeamWithType(uint8 type)
|
||||
{
|
||||
ArenaTeamTest* team = new ArenaTeamTest();
|
||||
team->SetType(type);
|
||||
return team;
|
||||
}
|
||||
|
||||
// Fixture for ArenaTeamFilter tests
|
||||
class ArenaTeamFilterTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_previousWorld = std::move(sWorld);
|
||||
_worldMock = new ::testing::NiceMock<WorldMock>();
|
||||
ON_CALL(*_worldMock, getIntConfig(::testing::_)).WillByDefault(::testing::Return(0));
|
||||
ON_CALL(*_worldMock, getIntConfig(CONFIG_LEGACY_ARENA_START_RATING)).WillByDefault(::testing::Return(1500));
|
||||
ON_CALL(*_worldMock, getIntConfig(CONFIG_ARENA_START_RATING)).WillByDefault(::testing::Return(0));
|
||||
sWorld.reset(_worldMock);
|
||||
|
||||
team1 = ArenaTeamWithType(2); // 2v2
|
||||
team2 = ArenaTeamWithType(3); // 3v3
|
||||
team3 = ArenaTeamWithType(5); // 5v5
|
||||
|
||||
arenaTeams[1] = team1;
|
||||
arenaTeams[2] = team2;
|
||||
arenaTeams[3] = team3;
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
delete team1;
|
||||
delete team2;
|
||||
delete team3;
|
||||
|
||||
sWorld = std::move(_previousWorld);
|
||||
}
|
||||
|
||||
ArenaTeamMgr::ArenaTeamContainer arenaTeams;
|
||||
ArenaTeam* team1;
|
||||
ArenaTeam* team2;
|
||||
ArenaTeam* team3;
|
||||
std::unique_ptr<IWorld> _previousWorld;
|
||||
::testing::NiceMock<WorldMock>* _worldMock = nullptr;
|
||||
};
|
||||
|
||||
// Test for ArenaTeamFilterAllTeams: it should return all teams without filtering
|
||||
TEST_F(ArenaTeamFilterTest, AllTeamsFilter)
|
||||
{
|
||||
ArenaTeamFilterAllTeams filter;
|
||||
ArenaTeamMgr::ArenaTeamContainer result = filter.Filter(arenaTeams);
|
||||
|
||||
EXPECT_EQ(result.size(), arenaTeams.size());
|
||||
EXPECT_EQ(result[1], team1);
|
||||
EXPECT_EQ(result[2], team2);
|
||||
EXPECT_EQ(result[3], team3);
|
||||
}
|
||||
|
||||
// Test for ArenaTeamFilterByTypes: should filter only teams matching the provided types
|
||||
TEST_F(ArenaTeamFilterTest, FilterBySpecificTypes)
|
||||
{
|
||||
std::vector<uint8> validTypes = {2, 3}; // Filtering for 2v2 and 3v3
|
||||
ArenaTeamFilterByTypes filter(validTypes);
|
||||
|
||||
ArenaTeamMgr::ArenaTeamContainer result = filter.Filter(arenaTeams);
|
||||
|
||||
EXPECT_EQ(result.size(), 2); // Only 2v2 and 3v3 should pass
|
||||
EXPECT_EQ(result[1], team1); // team1 is 2v2
|
||||
EXPECT_EQ(result[2], team2); // team2 is 3v3
|
||||
EXPECT_EQ(result.find(3), result.end()); // team3 (5v5) should be filtered out
|
||||
}
|
||||
|
||||
// Test for ArenaTeamFilterFactoryByUserInput: should create the correct filter based on input
|
||||
TEST_F(ArenaTeamFilterTest, FabricCreatesFilterByInput)
|
||||
{
|
||||
ArenaTeamFilterFactoryByUserInput fabric;
|
||||
|
||||
// Test for "all" input
|
||||
auto allTeamsFilter = fabric.CreateFilterByUserInput("all");
|
||||
ArenaTeamMgr::ArenaTeamContainer allTeamsResult = allTeamsFilter->Filter(arenaTeams);
|
||||
EXPECT_EQ(allTeamsResult.size(), arenaTeams.size()); // All teams should pass
|
||||
|
||||
// Test for "2,3" input
|
||||
auto specificTypesFilter = fabric.CreateFilterByUserInput("2,3");
|
||||
ArenaTeamMgr::ArenaTeamContainer filteredResult = specificTypesFilter->Filter(arenaTeams);
|
||||
EXPECT_EQ(filteredResult.size(), 2); // Only 2v2 and 3v3 teams should pass
|
||||
EXPECT_EQ(filteredResult[1], team1); // 2v2
|
||||
EXPECT_EQ(filteredResult[2], team2); // 3v3
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "CombatManager.h"
|
||||
#include "ThreatManager.h"
|
||||
#include "CreatureAI.h"
|
||||
#include "DBCStores.h"
|
||||
#include "TestCreature.h"
|
||||
#include "TestMap.h"
|
||||
#include "WorldMock.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// ============================================================================
|
||||
// Regression test for snake trap evade recursion crash.
|
||||
//
|
||||
// The old npc_pet_hunter_snake_trap::EnterEvadeMode called CombatStop(true)
|
||||
// which triggers ClearInCombat -> EndAllCombat -> CombatReference::EndCombat
|
||||
// -> JustExitedCombat -> EnterEvadeMode, causing deep recursion and a
|
||||
// freeze-detector crash during Battleground::EndBattleground.
|
||||
//
|
||||
// The fix removes the custom EnterEvadeMode override entirely, using the base
|
||||
// CreatureAI::EnterEvadeMode which properly guards against recursion via
|
||||
// UNIT_STATE_EVADE before calling CombatStop.
|
||||
//
|
||||
// This test verifies that ending combat on a creature with multiple PvE refs
|
||||
// does not cause unbounded recursion or leave stale combat state.
|
||||
// ============================================================================
|
||||
class SnakeTrapEvadeTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_previousWorld = std::move(sWorld);
|
||||
_worldMock = new NiceMock<WorldMock>();
|
||||
|
||||
ON_CALL(*_worldMock, getIntConfig(_)).WillByDefault(Return(0));
|
||||
ON_CALL(*_worldMock, getFloatConfig(_)).WillByDefault(Return(1.0f));
|
||||
ON_CALL(*_worldMock, getBoolConfig(_)).WillByDefault(Return(false));
|
||||
static std::string emptyString;
|
||||
ON_CALL(*_worldMock, GetDataPath()).WillByDefault(ReturnRef(emptyString));
|
||||
|
||||
sWorld.reset(_worldMock);
|
||||
|
||||
// Create two mutually hostile factions
|
||||
auto* factionA = new FactionTemplateEntry{};
|
||||
factionA->ID = 90001;
|
||||
factionA->faction = 90001;
|
||||
factionA->factionFlags = 0;
|
||||
factionA->ourMask = 1;
|
||||
factionA->friendlyMask = 0;
|
||||
factionA->hostileMask = 2;
|
||||
for (auto& e : factionA->enemyFaction) e = 0;
|
||||
for (auto& f : factionA->friendFaction) f = 0;
|
||||
sFactionTemplateStore.SetEntry(90001, factionA);
|
||||
|
||||
auto* factionB = new FactionTemplateEntry{};
|
||||
factionB->ID = 90002;
|
||||
factionB->faction = 90002;
|
||||
factionB->factionFlags = 0;
|
||||
factionB->ourMask = 2;
|
||||
factionB->friendlyMask = 0;
|
||||
factionB->hostileMask = 1;
|
||||
for (auto& e : factionB->enemyFaction) e = 0;
|
||||
for (auto& f : factionB->friendFaction) f = 0;
|
||||
sFactionTemplateStore.SetEntry(90002, factionB);
|
||||
|
||||
TestMap::EnsureDBC();
|
||||
_map = new TestMap();
|
||||
|
||||
// Simulate a "snake trap snake" — a creature with multiple combat refs
|
||||
_snake = new TestCreature();
|
||||
_snake->SetupForCombatTest(_map, 1, 19833); // NPC_VENOMOUS_SNAKE entry
|
||||
_snake->SetFaction(90001);
|
||||
|
||||
// Simulate two enemy targets (e.g., players in a BG)
|
||||
_targetA = new TestCreature();
|
||||
_targetA->SetupForCombatTest(_map, 2, 50001);
|
||||
_targetA->SetFaction(90002);
|
||||
|
||||
_targetB = new TestCreature();
|
||||
_targetB->SetupForCombatTest(_map, 3, 50002);
|
||||
_targetB->SetFaction(90002);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
_snake->CleanupCombatState();
|
||||
_targetA->CleanupCombatState();
|
||||
_targetB->CleanupCombatState();
|
||||
delete _snake;
|
||||
delete _targetA;
|
||||
delete _targetB;
|
||||
delete _map;
|
||||
sWorld = std::move(_previousWorld);
|
||||
}
|
||||
|
||||
std::unique_ptr<IWorld> _previousWorld;
|
||||
NiceMock<WorldMock>* _worldMock = nullptr;
|
||||
TestMap* _map = nullptr;
|
||||
TestCreature* _snake = nullptr;
|
||||
TestCreature* _targetA = nullptr;
|
||||
TestCreature* _targetB = nullptr;
|
||||
};
|
||||
|
||||
// Verify that ending all combat on a creature with multiple refs completes
|
||||
// without hanging or leaving stale state (regression: recursive EnterEvadeMode)
|
||||
// cppcheck-suppress syntaxError
|
||||
TEST_F(SnakeTrapEvadeTest, EndAllCombat_WithMultipleRefs_DoesNotRecurse)
|
||||
{
|
||||
// Put the snake in combat with both targets
|
||||
_snake->TestGetCombatMgr().SetInCombatWith(_targetA);
|
||||
_snake->TestGetCombatMgr().SetInCombatWith(_targetB);
|
||||
|
||||
ASSERT_TRUE(_snake->TestGetCombatMgr().HasCombat());
|
||||
ASSERT_TRUE(_snake->TestGetCombatMgr().IsInCombatWith(_targetA));
|
||||
ASSERT_TRUE(_snake->TestGetCombatMgr().IsInCombatWith(_targetB));
|
||||
|
||||
// This is the call path that caused the crash:
|
||||
// EndAllCombat -> EndCombat -> JustExitedCombat -> EnterEvadeMode -> CombatStop -> EndAllCombat
|
||||
// With the old custom EnterEvadeMode, this would recurse unboundedly.
|
||||
_snake->TestGetCombatMgr().EndAllPvECombat();
|
||||
|
||||
// All combat state should be cleanly resolved
|
||||
EXPECT_FALSE(_snake->TestGetCombatMgr().HasCombat());
|
||||
EXPECT_FALSE(_targetA->TestGetCombatMgr().HasCombat());
|
||||
EXPECT_FALSE(_targetB->TestGetCombatMgr().HasCombat());
|
||||
}
|
||||
|
||||
// Verify that CombatStop on a target also clears the snake's refs cleanly
|
||||
TEST_F(SnakeTrapEvadeTest, TargetCombatStop_ClearsSnakeRefs)
|
||||
{
|
||||
_snake->TestGetCombatMgr().SetInCombatWith(_targetA);
|
||||
_snake->TestGetCombatMgr().SetInCombatWith(_targetB);
|
||||
|
||||
ASSERT_TRUE(_snake->TestGetCombatMgr().HasCombat());
|
||||
|
||||
// Simulate what Battleground::EndBattleground does: CombatStop on the target
|
||||
// This triggers ClearInCombat -> EndAllCombat on _targetA, which calls
|
||||
// EndCombat on the ref(targetA, snake), which triggers JustExitedCombat
|
||||
// on the snake if it's the snake's last ref.
|
||||
_targetA->TestGetCombatMgr().EndAllPvECombat();
|
||||
|
||||
// targetA should be out of combat
|
||||
EXPECT_FALSE(_targetA->TestGetCombatMgr().HasCombat());
|
||||
// Snake should still be in combat with targetB
|
||||
EXPECT_TRUE(_snake->TestGetCombatMgr().HasCombat());
|
||||
EXPECT_TRUE(_snake->TestGetCombatMgr().IsInCombatWith(_targetB));
|
||||
|
||||
// Now end targetB's combat too
|
||||
_targetB->TestGetCombatMgr().EndAllPvECombat();
|
||||
|
||||
// Everything clean
|
||||
EXPECT_FALSE(_snake->TestGetCombatMgr().HasCombat());
|
||||
EXPECT_FALSE(_targetB->TestGetCombatMgr().HasCombat());
|
||||
}
|
||||
|
||||
// Verify that adding threat during evade is rejected (guards against
|
||||
// the old Reset() -> AddThreat -> re-enter combat pattern)
|
||||
TEST_F(SnakeTrapEvadeTest, AddThreat_DuringEvade_IsRejected)
|
||||
{
|
||||
_snake->TestGetCombatMgr().SetInCombatWith(_targetA);
|
||||
_snake->TestGetThreatMgr().AddThreat(_targetA, 100.0f);
|
||||
|
||||
ASSERT_TRUE(_snake->TestGetThreatMgr().IsThreatenedBy(_targetA));
|
||||
|
||||
// Enter evade state
|
||||
_snake->AddUnitState(UNIT_STATE_EVADE);
|
||||
|
||||
// AddThreat should be rejected while in evade
|
||||
_snake->TestGetThreatMgr().ClearAllThreat();
|
||||
_snake->AddThreat(_targetB, 100000.0f);
|
||||
|
||||
// Should NOT have threat on targetB because UNIT_STATE_EVADE blocks AddThreat
|
||||
EXPECT_FALSE(_snake->TestGetThreatMgr().IsThreatenedBy(_targetB));
|
||||
|
||||
_snake->ClearUnitState(UNIT_STATE_EVADE);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "Player.h"
|
||||
#include "ScriptMgr.h"
|
||||
#include "WorldSession.h"
|
||||
#include "WorldMock.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include "ScriptDefines/MiscScript.h"
|
||||
#include "ScriptDefines/PlayerScript.h"
|
||||
#include "ScriptDefines/WorldObjectScript.h"
|
||||
#include "ScriptDefines/UnitScript.h"
|
||||
#include "ScriptDefines/CommandScript.h"
|
||||
#include "SharedDefines.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include <string>
|
||||
|
||||
using namespace testing;
|
||||
|
||||
namespace
|
||||
{
|
||||
class TestVisibilityScript : public PlayerScript
|
||||
{
|
||||
public:
|
||||
TestVisibilityScript() : PlayerScript("TestVisibilityScript", { PLAYERHOOK_ON_SET_SERVER_SIDE_VISIBILITY }) { }
|
||||
|
||||
void OnPlayerSetServerSideVisibility(Player* player, ServerSideVisibilityType& type, AccountTypes& sec) override
|
||||
{
|
||||
++CallCount;
|
||||
LastPlayer = player;
|
||||
LastType = type;
|
||||
LastSecurity = sec;
|
||||
}
|
||||
|
||||
static void EnsureRegistered()
|
||||
{
|
||||
if (!Instance)
|
||||
Instance = new TestVisibilityScript();
|
||||
}
|
||||
|
||||
static void Reset()
|
||||
{
|
||||
CallCount = 0;
|
||||
LastPlayer = nullptr;
|
||||
LastType = SERVERSIDE_VISIBILITY_GM;
|
||||
LastSecurity = SEC_PLAYER;
|
||||
}
|
||||
|
||||
inline static TestVisibilityScript* Instance = nullptr;
|
||||
inline static uint32 CallCount = 0;
|
||||
inline static Player* LastPlayer = nullptr;
|
||||
inline static ServerSideVisibilityType LastType = SERVERSIDE_VISIBILITY_GM;
|
||||
inline static AccountTypes LastSecurity = SEC_PLAYER;
|
||||
};
|
||||
|
||||
class TestPlayer : public Player
|
||||
{
|
||||
public:
|
||||
using Player::Player;
|
||||
|
||||
void UpdateObjectVisibility(bool /*forced*/ = true, bool /*fromUpdate*/ = false) override { }
|
||||
|
||||
void ForceInitValues(ObjectGuid::LowType guidLow = 1)
|
||||
{
|
||||
Object::_Create(guidLow, uint32(0), HighGuid::Player);
|
||||
}
|
||||
};
|
||||
|
||||
class GmVisibleCommandTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
EnsureScriptRegistriesInitialized();
|
||||
|
||||
TestVisibilityScript::EnsureRegistered();
|
||||
|
||||
originalWorld = sWorld.release();
|
||||
worldMock = new NiceMock<WorldMock>();
|
||||
sWorld.reset(worldMock);
|
||||
|
||||
static std::string emptyString;
|
||||
ON_CALL(*worldMock, GetDataPath()).WillByDefault(ReturnRef(emptyString));
|
||||
ON_CALL(*worldMock, GetRealmName()).WillByDefault(ReturnRef(emptyString));
|
||||
ON_CALL(*worldMock, GetDefaultDbcLocale()).WillByDefault(Return(LOCALE_enUS));
|
||||
ON_CALL(*worldMock, getRate(_)).WillByDefault(Return(1.0f));
|
||||
ON_CALL(*worldMock, getBoolConfig(_)).WillByDefault(Return(false));
|
||||
ON_CALL(*worldMock, getIntConfig(_)).WillByDefault(Return(0));
|
||||
ON_CALL(*worldMock, getFloatConfig(_)).WillByDefault(Return(0.0f));
|
||||
ON_CALL(*worldMock, GetPlayerSecurityLimit()).WillByDefault(Return(SEC_PLAYER));
|
||||
|
||||
session = new WorldSession(1, "gm", 0, nullptr, SEC_GAMEMASTER, EXPANSION_WRATH_OF_THE_LICH_KING,
|
||||
0, LOCALE_enUS, 0, false, false, 0);
|
||||
session->InitRBACDataForTest();
|
||||
|
||||
player = new TestPlayer(session);
|
||||
player->ForceInitValues();
|
||||
session->SetPlayer(player);
|
||||
player->SetSession(session);
|
||||
|
||||
TestVisibilityScript::Reset();
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
// Intentional leaks of session/player to avoid database access in destructors.
|
||||
IWorld* currentWorld = sWorld.release();
|
||||
delete currentWorld;
|
||||
worldMock = nullptr;
|
||||
|
||||
sWorld.reset(originalWorld);
|
||||
originalWorld = nullptr;
|
||||
session = nullptr;
|
||||
player = nullptr;
|
||||
}
|
||||
|
||||
void SimulateGmVisibleOff()
|
||||
{
|
||||
player->SetServerSideVisibility(SERVERSIDE_VISIBILITY_GM, session->GetSecurity());
|
||||
}
|
||||
|
||||
void SimulateGmVisibleOn()
|
||||
{
|
||||
player->SetServerSideVisibility(SERVERSIDE_VISIBILITY_GM, SEC_PLAYER);
|
||||
}
|
||||
|
||||
static void EnsureScriptRegistriesInitialized()
|
||||
{
|
||||
static bool initialized = false;
|
||||
if (!initialized)
|
||||
{
|
||||
ScriptRegistry<MiscScript>::InitEnabledHooksIfNeeded(MISCHOOK_END);
|
||||
ScriptRegistry<WorldObjectScript>::InitEnabledHooksIfNeeded(WORLDOBJECTHOOK_END);
|
||||
ScriptRegistry<UnitScript>::InitEnabledHooksIfNeeded(UNITHOOK_END);
|
||||
ScriptRegistry<PlayerScript>::InitEnabledHooksIfNeeded(PLAYERHOOK_END);
|
||||
ScriptRegistry<CommandSC>::InitEnabledHooksIfNeeded(ALLCOMMANDHOOK_END);
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
IWorld* originalWorld = nullptr;
|
||||
NiceMock<WorldMock>* worldMock = nullptr;
|
||||
WorldSession* session = nullptr;
|
||||
TestPlayer* player = nullptr;
|
||||
};
|
||||
|
||||
// cppcheck-suppress syntaxError
|
||||
TEST_F(GmVisibleCommandTest, SetsPlayerInvisibleAndInvokesHook)
|
||||
{
|
||||
SimulateGmVisibleOff();
|
||||
|
||||
EXPECT_EQ(TestVisibilityScript::CallCount, 1u);
|
||||
EXPECT_EQ(TestVisibilityScript::LastPlayer, player);
|
||||
EXPECT_EQ(TestVisibilityScript::LastType, SERVERSIDE_VISIBILITY_GM);
|
||||
EXPECT_EQ(TestVisibilityScript::LastSecurity, session->GetSecurity());
|
||||
EXPECT_EQ(player->m_serverSideVisibility.GetValue(SERVERSIDE_VISIBILITY_GM), uint32(session->GetSecurity()));
|
||||
}
|
||||
|
||||
TEST_F(GmVisibleCommandTest, SetsPlayerVisibleAndInvokesHook)
|
||||
{
|
||||
SimulateGmVisibleOff();
|
||||
TestVisibilityScript::Reset();
|
||||
|
||||
SimulateGmVisibleOn();
|
||||
|
||||
EXPECT_EQ(TestVisibilityScript::CallCount, 1u);
|
||||
EXPECT_EQ(TestVisibilityScript::LastPlayer, player);
|
||||
EXPECT_EQ(TestVisibilityScript::LastType, SERVERSIDE_VISIBILITY_GM);
|
||||
EXPECT_EQ(TestVisibilityScript::LastSecurity, SEC_PLAYER);
|
||||
EXPECT_EQ(player->m_serverSideVisibility.GetValue(SERVERSIDE_VISIBILITY_GM), uint32(SEC_PLAYER));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Tests for Unit::resetAttackTimer arithmetic.
|
||||
*
|
||||
* Unit::resetAttackTimer resets the swing timer after an attack lands.
|
||||
* The timer value determines when the next attack can occur.
|
||||
*
|
||||
* Old (buggy) formula:
|
||||
* int32 time = int32(GetAttackTime(type) * m_modAttackSpeedPct[type]);
|
||||
* m_attackTimer[type] = std::min(m_attackTimer[type] + time, time);
|
||||
*
|
||||
* New (fixed) formula:
|
||||
* m_attackTimer[type] = int32(GetAttackTime(type) * m_modAttackSpeedPct[type]);
|
||||
*
|
||||
* The old formula carried forward negative timer debt, allowing burst
|
||||
* attacks after lag spikes or parry-haste timer reductions.
|
||||
*/
|
||||
|
||||
namespace
|
||||
{
|
||||
// Simulates the old (buggy) resetAttackTimer formula
|
||||
int32_t OldResetFormula(int32_t currentTimer, int32_t fullTime)
|
||||
{
|
||||
return std::min(currentTimer + fullTime, fullTime);
|
||||
}
|
||||
|
||||
// Simulates the new (fixed) resetAttackTimer formula
|
||||
int32_t NewResetFormula(int32_t /*currentTimer*/, int32_t fullTime)
|
||||
{
|
||||
return fullTime;
|
||||
}
|
||||
|
||||
// Calculate effective attack time: GetAttackTime(type) * m_modAttackSpeedPct[type]
|
||||
int32_t CalcFullTime(uint32_t baseAttackTime, float modSpeedPct)
|
||||
{
|
||||
return static_cast<int32_t>(baseAttackTime * modSpeedPct);
|
||||
}
|
||||
}
|
||||
|
||||
// Normal case: timer at 0 (attack just became ready)
|
||||
TEST(ResetAttackTimerTest, NormalReset_TimerAtZero)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(2000, 1.0f); // 2.0s base, no haste
|
||||
int32_t currentTimer = 0;
|
||||
|
||||
// Both formulas produce the same result when timer is exactly 0
|
||||
EXPECT_EQ(OldResetFormula(currentTimer, fullTime), 2000);
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 2000);
|
||||
}
|
||||
|
||||
// Normal case: timer slightly negative (typical single-tick overshoot)
|
||||
TEST(ResetAttackTimerTest, NormalReset_SmallNegativeTimer)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(2000, 1.0f);
|
||||
int32_t currentTimer = -50; // 50ms overshoot
|
||||
|
||||
// Old formula: min(-50 + 2000, 2000) = min(1950, 2000) = 1950
|
||||
// Carries 50ms debt — next attack 50ms sooner
|
||||
EXPECT_EQ(OldResetFormula(currentTimer, fullTime), 1950);
|
||||
|
||||
// New formula: always 2000 — clean reset
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 2000);
|
||||
}
|
||||
|
||||
// Bug scenario: large negative timer from lag spike
|
||||
TEST(ResetAttackTimerTest, LagSpike_LargeNegativeTimer)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(2000, 1.0f);
|
||||
int32_t currentTimer = -3000; // 3 second lag spike
|
||||
|
||||
// Old formula: min(-3000 + 2000, 2000) = min(-1000, 2000) = -1000
|
||||
// Timer is STILL negative — attack fires immediately again!
|
||||
EXPECT_LT(OldResetFormula(currentTimer, fullTime), 0);
|
||||
|
||||
// New formula: always 2000 — no burst
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 2000);
|
||||
}
|
||||
|
||||
// Gluth scenario: parry-haste with enrage
|
||||
TEST(ResetAttackTimerTest, GluthParryHaste_BurstAttack)
|
||||
{
|
||||
// Gluth: 1600ms base, 25% enrage haste
|
||||
// GetAttackTime returns base (1600), modSpeedPct = 0.8 (25% haste)
|
||||
int32_t fullTime = CalcFullTime(1600, 0.8f); // = 1280ms
|
||||
EXPECT_EQ(fullTime, 1280);
|
||||
|
||||
// After parry-haste reduces timer to near-zero and a lag spike
|
||||
// causes 2+ seconds of unprocessed time
|
||||
int32_t currentTimer = -2000;
|
||||
|
||||
// Old formula: min(-2000 + 1280, 1280) = min(-720, 1280) = -720
|
||||
// Timer deeply negative — immediate burst attack
|
||||
EXPECT_LT(OldResetFormula(currentTimer, fullTime), 0);
|
||||
|
||||
// New formula: 1280ms — proper cooldown before next swing
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 1280);
|
||||
}
|
||||
|
||||
// Parry-haste floor scenario: timer at minimum (20% of base)
|
||||
TEST(ResetAttackTimerTest, ParryHasteFloor_MultipleParries)
|
||||
{
|
||||
// Creature with 1600ms base, no haste
|
||||
int32_t fullTime = CalcFullTime(1600, 1.0f);
|
||||
|
||||
// Parry-haste can reduce timer to 20% of base = 320ms
|
||||
// If server tick is 200ms and timer was at 320ms, after tick: 120ms
|
||||
// Attack fires, resetAttackTimer called with timer = 120 - 200 = -80
|
||||
// (timer went from 120 to -80 during the next tick)
|
||||
int32_t currentTimer = -80;
|
||||
|
||||
// Old formula: min(-80 + 1600, 1600) = min(1520, 1600) = 1520
|
||||
// Small debt is carried — minor issue
|
||||
EXPECT_EQ(OldResetFormula(currentTimer, fullTime), 1520);
|
||||
|
||||
// Now consider two rapid parries reducing timer to 320ms,
|
||||
// followed by a 500ms server hiccup
|
||||
currentTimer = 320 - 500; // = -180
|
||||
// Old formula: min(-180 + 1600, 1600) = min(1420, 1600) = 1420
|
||||
EXPECT_EQ(OldResetFormula(currentTimer, fullTime), 1420);
|
||||
|
||||
// New formula: always full time
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 1600);
|
||||
}
|
||||
|
||||
// Multiple consecutive burst attacks (worst case)
|
||||
TEST(ResetAttackTimerTest, ConsecutiveBursts_OldFormulaChainAttacks)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(1600, 0.8f); // Gluth enraged: 1280ms
|
||||
int32_t timer = -2500; // Large lag spike
|
||||
|
||||
int attackCount = 0;
|
||||
// Simulate old formula: how many attacks fire before timer > 0?
|
||||
while (timer <= 0)
|
||||
{
|
||||
attackCount++;
|
||||
timer = OldResetFormula(timer, fullTime);
|
||||
}
|
||||
|
||||
// Old formula allows multiple attacks in burst
|
||||
EXPECT_GT(attackCount, 1);
|
||||
|
||||
// New formula: always exactly 1 attack then timer is positive
|
||||
timer = -2500;
|
||||
int newAttackCount = 0;
|
||||
// Only one attack fires (the one that triggered the reset)
|
||||
timer = NewResetFormula(timer, fullTime);
|
||||
if (timer > 0)
|
||||
newAttackCount = 1;
|
||||
|
||||
EXPECT_EQ(newAttackCount, 1);
|
||||
EXPECT_GT(timer, 0);
|
||||
}
|
||||
|
||||
// Haste modifier correctness
|
||||
TEST(ResetAttackTimerTest, HasteModifier_CorrectCalculation)
|
||||
{
|
||||
// No haste: 2000ms base
|
||||
EXPECT_EQ(CalcFullTime(2000, 1.0f), 2000);
|
||||
|
||||
// 25% haste (modSpeedPct = 0.8)
|
||||
EXPECT_EQ(CalcFullTime(2000, 0.8f), 1600);
|
||||
|
||||
// 50% slow (modSpeedPct = 1.5)
|
||||
EXPECT_EQ(CalcFullTime(2000, 1.5f), 3000);
|
||||
|
||||
// Enrage (25% haste on 1600ms base)
|
||||
EXPECT_EQ(CalcFullTime(1600, 0.8f), 1280);
|
||||
}
|
||||
|
||||
// Edge case: timer exactly equals negative of full time
|
||||
TEST(ResetAttackTimerTest, EdgeCase_TimerEqualsNegativeFullTime)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(2000, 1.0f);
|
||||
int32_t currentTimer = -2000;
|
||||
|
||||
// Old formula: min(-2000 + 2000, 2000) = min(0, 2000) = 0
|
||||
// Timer exactly 0 — attack is immediately ready again
|
||||
EXPECT_EQ(OldResetFormula(currentTimer, fullTime), 0);
|
||||
|
||||
// New formula: 2000 — proper delay
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 2000);
|
||||
}
|
||||
|
||||
// Edge case: positive timer (reset called before attack was ready)
|
||||
TEST(ResetAttackTimerTest, EdgeCase_PositiveTimer)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(2000, 1.0f);
|
||||
int32_t currentTimer = 500; // 500ms remaining
|
||||
|
||||
// Old formula: min(500 + 2000, 2000) = min(2500, 2000) = 2000
|
||||
// Capped at full time — correct behavior
|
||||
EXPECT_EQ(OldResetFormula(currentTimer, fullTime), 2000);
|
||||
|
||||
// New formula: same result
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 2000);
|
||||
}
|
||||
|
||||
// Invariant: new formula always returns exactly fullTime
|
||||
TEST(ResetAttackTimerTest, Invariant_AlwaysReturnsFullTime)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(1600, 1.0f);
|
||||
|
||||
// Test a wide range of current timer values
|
||||
for (int32_t timer = -10000; timer <= 10000; timer += 100)
|
||||
EXPECT_EQ(NewResetFormula(timer, fullTime), fullTime);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "ObjectMgr.h"
|
||||
#include "SmartScriptMgr.h"
|
||||
#include "TemporarySummon.h"
|
||||
#include "WorldMock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
class GameObjectSummonGroupTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_previousWorld = std::move(sWorld);
|
||||
auto* worldMock =
|
||||
new ::testing::NiceMock<WorldMock>();
|
||||
ON_CALL(*worldMock, getIntConfig(::testing::_))
|
||||
.WillByDefault(::testing::Return(0));
|
||||
sWorld.reset(worldMock);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
sWorld = std::move(_previousWorld);
|
||||
}
|
||||
|
||||
std::unique_ptr<IWorld> _previousWorld;
|
||||
};
|
||||
|
||||
TEST_F(GameObjectSummonGroupTest, DataStructStoresFields)
|
||||
{
|
||||
GameObjectSummonData data;
|
||||
data.entry = 2332;
|
||||
data.pos.Relocate(-14652.38f, 146.51f, 3.50f, 0.35f);
|
||||
data.rot = G3D::Quat(0.0f, 0.0f, 0.17f, 0.98f);
|
||||
data.respawnTime = 120;
|
||||
|
||||
EXPECT_EQ(data.entry, 2332u);
|
||||
EXPECT_FLOAT_EQ(data.pos.GetPositionX(), -14652.38f);
|
||||
EXPECT_FLOAT_EQ(data.pos.GetPositionY(), 146.51f);
|
||||
EXPECT_FLOAT_EQ(data.pos.GetPositionZ(), 3.50f);
|
||||
EXPECT_FLOAT_EQ(data.pos.GetOrientation(), 0.35f);
|
||||
EXPECT_FLOAT_EQ(data.rot.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(data.rot.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(data.rot.z, 0.17f);
|
||||
EXPECT_FLOAT_EQ(data.rot.w, 0.98f);
|
||||
EXPECT_EQ(data.respawnTime, 120u);
|
||||
}
|
||||
|
||||
TEST_F(GameObjectSummonGroupTest, QuaternionIdentity)
|
||||
{
|
||||
GameObjectSummonData data;
|
||||
data.rot = G3D::Quat(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(data.rot.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(data.rot.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(data.rot.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(data.rot.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(GameObjectSummonGroupTest, AccessorReturnsNullForMissing)
|
||||
{
|
||||
auto const* result = sObjectMgr->GetGameObjectSummonGroup(
|
||||
99999, SUMMONER_TYPE_CREATURE, 0);
|
||||
EXPECT_EQ(result, nullptr);
|
||||
}
|
||||
|
||||
TEST_F(GameObjectSummonGroupTest, AccessorReturnsNullForAllTypes)
|
||||
{
|
||||
auto const* r1 = sObjectMgr->GetGameObjectSummonGroup(
|
||||
99999, SUMMONER_TYPE_CREATURE, 0);
|
||||
auto const* r2 = sObjectMgr->GetGameObjectSummonGroup(
|
||||
99999, SUMMONER_TYPE_GAMEOBJECT, 0);
|
||||
auto const* r3 = sObjectMgr->GetGameObjectSummonGroup(
|
||||
99999, SUMMONER_TYPE_MAP, 0);
|
||||
|
||||
EXPECT_EQ(r1, nullptr);
|
||||
EXPECT_EQ(r2, nullptr);
|
||||
EXPECT_EQ(r3, nullptr);
|
||||
}
|
||||
|
||||
TEST_F(GameObjectSummonGroupTest, DifferentGroupsAreIndependent)
|
||||
{
|
||||
auto const* g0 = sObjectMgr->GetGameObjectSummonGroup(
|
||||
2289, SUMMONER_TYPE_GAMEOBJECT, 0);
|
||||
auto const* g1 = sObjectMgr->GetGameObjectSummonGroup(
|
||||
2289, SUMMONER_TYPE_GAMEOBJECT, 1);
|
||||
|
||||
// Both should be null since DB isn't loaded in tests,
|
||||
// but they should be independent lookups
|
||||
EXPECT_EQ(g0, nullptr);
|
||||
EXPECT_EQ(g1, nullptr);
|
||||
}
|
||||
|
||||
TEST_F(GameObjectSummonGroupTest, SmartActionEnumValue)
|
||||
{
|
||||
EXPECT_EQ(SMART_ACTION_SUMMON_GAMEOBJECT_GROUP, 241);
|
||||
EXPECT_EQ(SMART_ACTION_AC_END, 242);
|
||||
}
|
||||
|
||||
TEST_F(GameObjectSummonGroupTest, SmartActionUnionSize)
|
||||
{
|
||||
SmartAction action{};
|
||||
action.gameobjectGroup.group = 5;
|
||||
EXPECT_EQ(action.gameobjectGroup.group, 5u);
|
||||
}
|
||||
|
||||
TEST_F(GameObjectSummonGroupTest, TempSummonGroupKeyOrdering)
|
||||
{
|
||||
TempSummonGroupKey k1(100, SUMMONER_TYPE_CREATURE, 0);
|
||||
TempSummonGroupKey k2(100, SUMMONER_TYPE_GAMEOBJECT, 0);
|
||||
TempSummonGroupKey k3(100, SUMMONER_TYPE_CREATURE, 1);
|
||||
TempSummonGroupKey k4(200, SUMMONER_TYPE_CREATURE, 0);
|
||||
|
||||
// std::tuple ordering: summoner ID first, then type, then group
|
||||
EXPECT_LT(k1, k2); // same id, creature < gameobject
|
||||
EXPECT_LT(k1, k3); // same id+type, group 0 < 1
|
||||
EXPECT_LT(k1, k4); // id 100 < 200
|
||||
}
|
||||
|
||||
TEST_F(GameObjectSummonGroupTest, SummonerTypeValues)
|
||||
{
|
||||
EXPECT_EQ(SUMMONER_TYPE_CREATURE, 0);
|
||||
EXPECT_EQ(SUMMONER_TYPE_GAMEOBJECT, 1);
|
||||
EXPECT_EQ(SUMMONER_TYPE_MAP, 2);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "DBCStores.h"
|
||||
#include "Formulas.h"
|
||||
#include "WorldMock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace Acore::Honor;
|
||||
using namespace Acore::XP;
|
||||
|
||||
TEST(FormulasTest, hk_honor_at_level)
|
||||
{
|
||||
EXPECT_EQ(hk_honor_at_level(80), 124);
|
||||
EXPECT_EQ(hk_honor_at_level(80, 2), 248);
|
||||
EXPECT_EQ(hk_honor_at_level(80, 0.5), 62);
|
||||
EXPECT_EQ(hk_honor_at_level(1), 2);
|
||||
EXPECT_EQ(hk_honor_at_level(1, 10), 16);
|
||||
EXPECT_EQ(hk_honor_at_level(2), 4);
|
||||
EXPECT_EQ(hk_honor_at_level(3), 5);
|
||||
}
|
||||
|
||||
TEST(FormulasTest, GetGrayLevel)
|
||||
{
|
||||
EXPECT_EQ(GetGrayLevel(0), 0);
|
||||
EXPECT_EQ(GetGrayLevel(5), 0);
|
||||
EXPECT_EQ(GetGrayLevel(6), 1);
|
||||
EXPECT_EQ(GetGrayLevel(39), 31);
|
||||
EXPECT_EQ(GetGrayLevel(40), 31);
|
||||
EXPECT_EQ(GetGrayLevel(59), 47);
|
||||
EXPECT_EQ(GetGrayLevel(60), 51);
|
||||
EXPECT_EQ(GetGrayLevel(80), 71);
|
||||
}
|
||||
|
||||
TEST(FormulasTest, GetColorCode)
|
||||
{
|
||||
EXPECT_EQ(GetColorCode(60, 80), XP_RED);
|
||||
EXPECT_EQ(GetColorCode(60, 65), XP_RED);
|
||||
EXPECT_EQ(GetColorCode(60, 64), XP_ORANGE);
|
||||
EXPECT_EQ(GetColorCode(60, 63), XP_ORANGE);
|
||||
EXPECT_EQ(GetColorCode(60, 62), XP_YELLOW);
|
||||
EXPECT_EQ(GetColorCode(60, 58), XP_YELLOW);
|
||||
EXPECT_EQ(GetColorCode(60, 57), XP_GREEN);
|
||||
EXPECT_EQ(GetColorCode(60, 52), XP_GREEN);
|
||||
EXPECT_EQ(GetColorCode(60, 51), XP_GRAY);
|
||||
EXPECT_EQ(GetColorCode(60, 1), XP_GRAY);
|
||||
}
|
||||
|
||||
TEST(FormulasTest, GetZeroDifference)
|
||||
{
|
||||
EXPECT_EQ(GetZeroDifference(1), 5);
|
||||
EXPECT_EQ(GetZeroDifference(7), 5);
|
||||
EXPECT_EQ(GetZeroDifference(8), 6);
|
||||
EXPECT_EQ(GetZeroDifference(9), 6);
|
||||
EXPECT_EQ(GetZeroDifference(10), 7);
|
||||
EXPECT_EQ(GetZeroDifference(11), 7);
|
||||
EXPECT_EQ(GetZeroDifference(12), 8);
|
||||
EXPECT_EQ(GetZeroDifference(15), 8);
|
||||
EXPECT_EQ(GetZeroDifference(16), 9);
|
||||
EXPECT_EQ(GetZeroDifference(19), 9);
|
||||
EXPECT_EQ(GetZeroDifference(20), 11);
|
||||
EXPECT_EQ(GetZeroDifference(29), 11);
|
||||
EXPECT_EQ(GetZeroDifference(30), 12);
|
||||
EXPECT_EQ(GetZeroDifference(39), 12);
|
||||
EXPECT_EQ(GetZeroDifference(40), 13);
|
||||
EXPECT_EQ(GetZeroDifference(44), 13);
|
||||
EXPECT_EQ(GetZeroDifference(45), 14);
|
||||
EXPECT_EQ(GetZeroDifference(49), 14);
|
||||
EXPECT_EQ(GetZeroDifference(50), 15);
|
||||
EXPECT_EQ(GetZeroDifference(54), 15);
|
||||
EXPECT_EQ(GetZeroDifference(55), 16);
|
||||
EXPECT_EQ(GetZeroDifference(59), 16);
|
||||
EXPECT_EQ(GetZeroDifference(60), 17);
|
||||
EXPECT_EQ(GetZeroDifference(80), 17);
|
||||
}
|
||||
|
||||
TEST(FormulasTest, BaseGain)
|
||||
{
|
||||
EXPECT_EQ(BaseGain(60, 40, CONTENT_1_60), 0);
|
||||
EXPECT_EQ(BaseGain(60, 60, CONTENT_1_60), 345);
|
||||
EXPECT_EQ(BaseGain(50, 60, CONTENT_1_60), 354);
|
||||
EXPECT_EQ(BaseGain(65, 66, CONTENT_61_70), 588);
|
||||
EXPECT_EQ(BaseGain(79, 78, CONTENT_71_80), 917);
|
||||
|
||||
// check outError() has been called after passing an invalid ContentLevels content
|
||||
EXPECT_EQ(BaseGain(79, 1, ContentLevels(999)), 0);
|
||||
}
|
||||
|
||||
TEST(FormulasTest, Gain)
|
||||
{
|
||||
auto worldMock = new WorldMock();
|
||||
sWorld.reset((worldMock));
|
||||
/// @todo: create mocks of Player and Creature
|
||||
// Gain(nullptr, nullptr);
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Regression test for pool quest reload bug.
|
||||
*
|
||||
* When `.reload creature_queststarter` is executed, LoadQuestRelationsHelper()
|
||||
* clears the creature quest relation map (_creatureQuestRelations) which
|
||||
* contains the active pooled daily quest. It repopulates the pool mapping
|
||||
* (mQuestCreatureRelation) but never calls Spawn1Object() to re-insert the
|
||||
* active quest into the creature relation map. This causes ALL pool-based
|
||||
* daily quests (Dalaran cooking, fishing, jewelcrafting, etc.) to vanish
|
||||
* from their NPCs.
|
||||
*/
|
||||
|
||||
#include "ObjectMgr.h"
|
||||
#include "PoolMgr.h"
|
||||
#include "QuestDef.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Test IDs chosen to avoid collisions with real data
|
||||
static constexpr uint32 TEST_QUEST_ID = 99998;
|
||||
static constexpr uint32 TEST_CREATURE_ID = 99999;
|
||||
|
||||
class PoolQuestReloadTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
creatureQuestMap = sObjectMgr->GetCreatureQuestRelationMap();
|
||||
|
||||
// Establish the pool-side mapping: quest -> creature
|
||||
// This is what LoadQuestRelationsHelper populates for pooled quests
|
||||
sPoolMgr->mQuestCreatureRelation.insert(
|
||||
PooledQuestRelation::value_type(TEST_QUEST_ID, TEST_CREATURE_ID));
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
// Clean up: remove test entries from both maps
|
||||
auto range = sPoolMgr->mQuestCreatureRelation.equal_range(TEST_QUEST_ID);
|
||||
sPoolMgr->mQuestCreatureRelation.erase(range.first, range.second);
|
||||
|
||||
// Remove any test entries left in creature quest relations
|
||||
auto crRange = creatureQuestMap->equal_range(TEST_CREATURE_ID);
|
||||
for (auto it = crRange.first; it != crRange.second; )
|
||||
{
|
||||
if (it->second == TEST_QUEST_ID)
|
||||
it = creatureQuestMap->erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: count how many times quest appears on a creature in the quest relation map
|
||||
uint32 CountQuestOnCreature(uint32 creatureId, uint32 questId)
|
||||
{
|
||||
uint32 count = 0;
|
||||
auto range = creatureQuestMap->equal_range(creatureId);
|
||||
for (auto it = range.first; it != range.second; ++it)
|
||||
if (it->second == questId)
|
||||
++count;
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Simulate what Spawn1Object does: copy the active pool quest
|
||||
/// from mQuestCreatureRelation into the creature quest relation map
|
||||
void SimulateSpawn1Object(uint32 questId)
|
||||
{
|
||||
PoolGroup<Quest> poolGroup;
|
||||
PoolObject obj(questId, 0.0f);
|
||||
poolGroup.Spawn1Object(&obj);
|
||||
}
|
||||
|
||||
/// Simulate what LoadQuestRelationsHelper does on reload:
|
||||
/// 1. Clear the creature quest relation map
|
||||
/// 2. Clear and repopulate mQuestCreatureRelation
|
||||
/// Non-pooled quests would be re-added to the creature map, but
|
||||
/// pooled quests only go into mQuestCreatureRelation.
|
||||
void SimulateReload()
|
||||
{
|
||||
// Step 1: map.clear() — line 8589 of ObjectMgr.cpp
|
||||
creatureQuestMap->clear();
|
||||
|
||||
// Step 2: poolRelationMap->clear() — line 8604 of ObjectMgr.cpp
|
||||
sPoolMgr->mQuestCreatureRelation.clear();
|
||||
|
||||
// Step 3: Repopulate poolRelationMap from DB
|
||||
// (In the real code this re-reads creature_queststarter LEFT JOIN pool_quest)
|
||||
sPoolMgr->mQuestCreatureRelation.insert(
|
||||
PooledQuestRelation::value_type(TEST_QUEST_ID, TEST_CREATURE_ID));
|
||||
|
||||
// NOTE: The real reload handler does NOT call Spawn1Object here.
|
||||
// That is the bug.
|
||||
}
|
||||
|
||||
QuestRelations* creatureQuestMap = nullptr;
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Baseline: Spawn1Object correctly adds pooled quest to NPC
|
||||
// ------------------------------------------------------------------
|
||||
// cppcheck-suppress syntaxError
|
||||
TEST_F(PoolQuestReloadTest, Spawn1ObjectAddsQuestToCreatureRelationMap)
|
||||
{
|
||||
// Initially the quest should NOT be on the creature
|
||||
EXPECT_EQ(CountQuestOnCreature(TEST_CREATURE_ID, TEST_QUEST_ID), 0u)
|
||||
<< "Quest should not be on creature before Spawn1Object";
|
||||
|
||||
// Spawn1Object reads from mQuestCreatureRelation and inserts into
|
||||
// the creature quest relation map
|
||||
SimulateSpawn1Object(TEST_QUEST_ID);
|
||||
|
||||
EXPECT_EQ(CountQuestOnCreature(TEST_CREATURE_ID, TEST_QUEST_ID), 1u)
|
||||
<< "Quest should appear on creature after Spawn1Object";
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// BUG: Reload clears pooled quest without re-spawning it
|
||||
// ------------------------------------------------------------------
|
||||
TEST_F(PoolQuestReloadTest, ReloadCreatureQuestStarterRemovesPooledQuest)
|
||||
{
|
||||
// 1. Spawn the pool quest onto the NPC (normal startup behavior)
|
||||
SimulateSpawn1Object(TEST_QUEST_ID);
|
||||
ASSERT_EQ(CountQuestOnCreature(TEST_CREATURE_ID, TEST_QUEST_ID), 1u)
|
||||
<< "Precondition: quest must be on creature before reload";
|
||||
|
||||
// 2. Simulate `.reload creature_queststarter`
|
||||
SimulateReload();
|
||||
|
||||
// 3. THE BUG: quest is gone from the NPC even though it's still
|
||||
// the active daily in the pool
|
||||
EXPECT_EQ(CountQuestOnCreature(TEST_CREATURE_ID, TEST_QUEST_ID), 0u)
|
||||
<< "BUG: After reload, pooled quest vanishes from creature "
|
||||
"because Spawn1Object is never called";
|
||||
|
||||
// 4. Verify mQuestCreatureRelation still has the mapping
|
||||
// (the pool system KNOWS about the quest, it's just not on the NPC)
|
||||
auto range = sPoolMgr->mQuestCreatureRelation.equal_range(TEST_QUEST_ID);
|
||||
EXPECT_NE(range.first, range.second)
|
||||
<< "Pool mapping should still exist after reload";
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Calling Spawn1Object after reload would fix the problem
|
||||
// ------------------------------------------------------------------
|
||||
TEST_F(PoolQuestReloadTest, Spawn1ObjectAfterReloadRestoresQuest)
|
||||
{
|
||||
// Setup: spawn, reload (quest disappears)
|
||||
SimulateSpawn1Object(TEST_QUEST_ID);
|
||||
SimulateReload();
|
||||
ASSERT_EQ(CountQuestOnCreature(TEST_CREATURE_ID, TEST_QUEST_ID), 0u)
|
||||
<< "Precondition: quest must be missing after reload";
|
||||
|
||||
// Fix: call Spawn1Object again for the active pool quest
|
||||
SimulateSpawn1Object(TEST_QUEST_ID);
|
||||
|
||||
EXPECT_EQ(CountQuestOnCreature(TEST_CREATURE_ID, TEST_QUEST_ID), 1u)
|
||||
<< "Spawn1Object after reload should restore the quest on the NPC";
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Non-pooled quests survive reload (contrast with pooled quests)
|
||||
// ------------------------------------------------------------------
|
||||
TEST_F(PoolQuestReloadTest, NonPooledQuestSurvivesReload)
|
||||
{
|
||||
static constexpr uint32 REGULAR_QUEST_ID = 99990;
|
||||
|
||||
// A non-pooled quest is added directly to the creature quest map
|
||||
// (this is what LoadQuestRelationsHelper does for quests without pool_entry)
|
||||
creatureQuestMap->insert(QuestRelations::value_type(TEST_CREATURE_ID, REGULAR_QUEST_ID));
|
||||
|
||||
// Simulate reload: clear and repopulate
|
||||
creatureQuestMap->clear();
|
||||
// Non-pooled quests get re-inserted directly (simulating the DB reload)
|
||||
creatureQuestMap->insert(QuestRelations::value_type(TEST_CREATURE_ID, REGULAR_QUEST_ID));
|
||||
|
||||
EXPECT_EQ(CountQuestOnCreature(TEST_CREATURE_ID, REGULAR_QUEST_ID), 1u)
|
||||
<< "Non-pooled quests survive reload because they are re-inserted directly";
|
||||
|
||||
// Cleanup
|
||||
auto range = creatureQuestMap->equal_range(TEST_CREATURE_ID);
|
||||
for (auto it = range.first; it != range.second; )
|
||||
{
|
||||
if (it->second == REGULAR_QUEST_ID)
|
||||
it = creatureQuestMap->erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// PoolQuestReloadFixTest: exercises the actual ReSpawnPoolQuests() fix
|
||||
// by setting up PoolMgr private state (friend class access).
|
||||
// Must be at global scope to match the friend declaration in PoolMgr.
|
||||
// ------------------------------------------------------------------
|
||||
class PoolQuestReloadFixTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
creatureQuestMap = sObjectMgr->GetCreatureQuestRelationMap();
|
||||
|
||||
// Set up pool infrastructure (private members via friend access)
|
||||
sPoolMgr->mPoolTemplate[TEST_POOL_ID].MaxLimit = 1;
|
||||
|
||||
// Create the pool group entry (Spawn1Object doesn't use pool group
|
||||
// internals, it only reads mQuestCreatureRelation)
|
||||
sPoolMgr->mPoolQuestGroups[TEST_POOL_ID].SetPoolId(TEST_POOL_ID);
|
||||
|
||||
sPoolMgr->mQuestSearchMap[TEST_QUEST_ID] = TEST_POOL_ID;
|
||||
|
||||
// Mark the quest as active/spawned
|
||||
sPoolMgr->mSpawnedData.ActivateObject<Quest>(TEST_QUEST_ID, TEST_POOL_ID);
|
||||
|
||||
// Set up pool-side mapping: quest -> creature
|
||||
sPoolMgr->mQuestCreatureRelation.insert(
|
||||
PooledQuestRelation::value_type(TEST_QUEST_ID, TEST_CREATURE_ID));
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
// Clean up all test state from the singletons
|
||||
sPoolMgr->mPoolTemplate.erase(TEST_POOL_ID);
|
||||
sPoolMgr->mPoolQuestGroups.erase(TEST_POOL_ID);
|
||||
sPoolMgr->mQuestSearchMap.erase(TEST_QUEST_ID);
|
||||
sPoolMgr->mSpawnedData.RemoveObject<Quest>(TEST_QUEST_ID, TEST_POOL_ID);
|
||||
|
||||
auto range = sPoolMgr->mQuestCreatureRelation.equal_range(TEST_QUEST_ID);
|
||||
sPoolMgr->mQuestCreatureRelation.erase(range.first, range.second);
|
||||
|
||||
auto crRange = creatureQuestMap->equal_range(TEST_CREATURE_ID);
|
||||
for (auto it = crRange.first; it != crRange.second; )
|
||||
{
|
||||
if (it->second == TEST_QUEST_ID)
|
||||
it = creatureQuestMap->erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr uint32 TEST_QUEST_ID = 99998;
|
||||
static constexpr uint32 TEST_CREATURE_ID = 99999;
|
||||
static constexpr uint32 TEST_POOL_ID = 99997;
|
||||
|
||||
QuestRelations* creatureQuestMap = nullptr;
|
||||
};
|
||||
|
||||
TEST_F(PoolQuestReloadFixTest, ReSpawnPoolQuestsRestoresQuestAfterReload)
|
||||
{
|
||||
// 1. Spawn the quest onto the NPC (simulates normal startup)
|
||||
PoolGroup<Quest> poolGroup;
|
||||
PoolObject obj(TEST_QUEST_ID, 0.0f);
|
||||
poolGroup.Spawn1Object(&obj);
|
||||
|
||||
auto count = [&]() {
|
||||
uint32 n = 0;
|
||||
auto range = creatureQuestMap->equal_range(TEST_CREATURE_ID);
|
||||
for (auto it = range.first; it != range.second; ++it)
|
||||
if (it->second == TEST_QUEST_ID)
|
||||
++n;
|
||||
return n;
|
||||
};
|
||||
|
||||
ASSERT_EQ(count(), 1u) << "Quest should be on creature before reload";
|
||||
|
||||
// 2. Simulate reload: clear creature quest map and repopulate pool mapping
|
||||
creatureQuestMap->clear();
|
||||
sPoolMgr->mQuestCreatureRelation.clear();
|
||||
sPoolMgr->mQuestCreatureRelation.insert(
|
||||
PooledQuestRelation::value_type(TEST_QUEST_ID, TEST_CREATURE_ID));
|
||||
|
||||
ASSERT_EQ(count(), 0u) << "Quest should be gone after reload clears the map";
|
||||
|
||||
// 3. THE FIX: ReSpawnPoolQuests re-inserts active pool quests
|
||||
sPoolMgr->ReSpawnPoolQuests();
|
||||
|
||||
EXPECT_EQ(count(), 1u)
|
||||
<< "ReSpawnPoolQuests should restore active pool quests after reload";
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "SpellInfoTestHelper.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "SharedDefines.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
/**
|
||||
* @brief Tests for the binary spell detection condition used in
|
||||
* SpellMgr::LoadSpellInfoCustomAttributes.
|
||||
*
|
||||
* The condition determines whether a non-damage aura effect should mark
|
||||
* a spell as binary (fully resistable via spell resistance).
|
||||
*
|
||||
* Correct logic: CalcValue() || ((INTERRUPT_CAST || DONT_BREAK_STEALTH) && !NO_IMMUNITIES)
|
||||
*
|
||||
* A spell should be marked binary if:
|
||||
* - Its effect has a non-zero CalcValue (e.g. Fear, Polymorph, Frost Nova), OR
|
||||
* - It is an interrupt/stealth spell without the NO_IMMUNITIES attribute
|
||||
*/
|
||||
|
||||
namespace
|
||||
{
|
||||
// Replicates the binary detection condition from SpellMgr.cpp
|
||||
// Returns true if the effect should mark the spell as binary
|
||||
bool ShouldMarkBinary(SpellInfo const* spellInfo, uint8 effIndex)
|
||||
{
|
||||
return spellInfo->Effects[effIndex].CalcValue() ||
|
||||
((spellInfo->Effects[effIndex].Effect == SPELL_EFFECT_INTERRUPT_CAST ||
|
||||
spellInfo->HasAttribute(SPELL_ATTR0_CU_DONT_BREAK_STEALTH)) &&
|
||||
!spellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES));
|
||||
}
|
||||
}
|
||||
|
||||
class BinarySpellDetectionTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
};
|
||||
|
||||
// CC aura with non-zero CalcValue should be binary (e.g. Fear, Polymorph)
|
||||
TEST_F(BinarySpellDetectionTest, AuraWithCalcValue_IsBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_FEAR)
|
||||
.WithEffectBasePoints(0, 0)
|
||||
.WithEffectDieSides(0, 1) // CalcValue = 0 + 1 = 1
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_SHADOW)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_TRUE(ShouldMarkBinary(spell.get(), 0));
|
||||
}
|
||||
|
||||
// Aura with zero CalcValue and no special attributes should NOT be binary
|
||||
TEST_F(BinarySpellDetectionTest, AuraWithZeroCalcValue_NotBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_FEAR)
|
||||
.WithEffectBasePoints(0, 0)
|
||||
.WithEffectDieSides(0, 0) // CalcValue = 0
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_SHADOW)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_FALSE(ShouldMarkBinary(spell.get(), 0));
|
||||
}
|
||||
|
||||
// INTERRUPT_CAST effect without NO_IMMUNITIES should be binary
|
||||
TEST_F(BinarySpellDetectionTest, InterruptCast_IsBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_INTERRUPT_CAST)
|
||||
.WithEffectBasePoints(0, 0)
|
||||
.WithEffectDieSides(0, 0) // CalcValue = 0
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_SHADOW)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_TRUE(ShouldMarkBinary(spell.get(), 0));
|
||||
}
|
||||
|
||||
// INTERRUPT_CAST with NO_IMMUNITIES should NOT be binary
|
||||
TEST_F(BinarySpellDetectionTest, InterruptCastWithNoImmunities_NotBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_INTERRUPT_CAST)
|
||||
.WithEffectBasePoints(0, 0)
|
||||
.WithEffectDieSides(0, 0)
|
||||
.WithAttributes(SPELL_ATTR0_NO_IMMUNITIES)
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_SHADOW)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_FALSE(ShouldMarkBinary(spell.get(), 0));
|
||||
}
|
||||
|
||||
// Fear-like spell: APPLY_AURA MOD_FEAR with BasePoints=-1, DieSides=1
|
||||
// CalcValue = -1 + 1 = 0, but second effect has value
|
||||
TEST_F(BinarySpellDetectionTest, FearLikeSpell_SecondEffectHasValue_IsBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_FEAR)
|
||||
.WithEffectBasePoints(0, -1)
|
||||
.WithEffectDieSides(0, 1) // CalcValue = -1 + 1 = 0
|
||||
.WithEffect(1, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_INCREASE_SPEED)
|
||||
.WithEffectBasePoints(1, 24)
|
||||
.WithEffectDieSides(1, 1) // CalcValue = 24 + 1 = 25
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_SHADOW)
|
||||
.BuildUnique();
|
||||
|
||||
// Effect 0 has CalcValue 0, should not mark binary
|
||||
EXPECT_FALSE(ShouldMarkBinary(spell.get(), 0));
|
||||
// Effect 1 has CalcValue 25, should mark binary
|
||||
EXPECT_TRUE(ShouldMarkBinary(spell.get(), 1));
|
||||
}
|
||||
|
||||
// Polymorph-like: APPLY_AURA MOD_CONFUSE with positive BasePoints
|
||||
TEST_F(BinarySpellDetectionTest, PolymorphLikeSpell_IsBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_CONFUSE)
|
||||
.WithEffectBasePoints(0, 0)
|
||||
.WithEffectDieSides(0, 1) // CalcValue = 1
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_ARCANE)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_TRUE(ShouldMarkBinary(spell.get(), 0));
|
||||
}
|
||||
|
||||
// Frost Nova-like: APPLY_AURA MOD_ROOT with positive BasePoints
|
||||
TEST_F(BinarySpellDetectionTest, FrostNovaLikeSpell_IsBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_ROOT)
|
||||
.WithEffectBasePoints(0, 0)
|
||||
.WithEffectDieSides(0, 1) // CalcValue = 1
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_FROST)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_TRUE(ShouldMarkBinary(spell.get(), 0));
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
/*
|
||||
* 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 BreakableCCProcTest.cpp
|
||||
* @brief Tests for the CC break-on-damage proc mechanism
|
||||
*
|
||||
* CC auras (Fear, Polymorph, Stun, Root, Transform) have a damage threshold
|
||||
* set in CalculateAmount. When damage is taken, HandleBreakableCCAuraProc
|
||||
* subtracts the damage from the threshold and removes the aura when it
|
||||
* reaches zero.
|
||||
*
|
||||
* The threshold is calculated as:
|
||||
* BaseHealth(casterLevel, CLASS_WARRIOR) / 4.75
|
||||
*
|
||||
* This gives level 80 a threshold of ~2648 HP (12588 / 4.75).
|
||||
*/
|
||||
|
||||
#include "AuraStub.h"
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "WorldMock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
/**
|
||||
* @brief Simulates HandleBreakableCCAuraProc logic
|
||||
*
|
||||
* Mirrors AuraEffect::HandleBreakableCCAuraProc from SpellAuraEffects.cpp:
|
||||
* damageLeft = GetAmount() - damage
|
||||
* if (damageLeft <= 0) remove aura
|
||||
* else SetAmount(damageLeft)
|
||||
*
|
||||
* @param effect The CC aura effect stub (amount = damage threshold)
|
||||
* @param damage Damage dealt to the CC'd target
|
||||
* @return true if the aura should be removed (threshold exceeded)
|
||||
*/
|
||||
static bool SimulateBreakableCCProc(AuraEffectStub* effect, int32_t damage)
|
||||
{
|
||||
int32_t damageLeft = effect->GetAmount() - damage;
|
||||
if (damageLeft <= 0)
|
||||
return true; // aura removed
|
||||
effect->SetAmount(damageLeft);
|
||||
return false; // aura survives, threshold reduced
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Simulates CalculateAmount for CC auras
|
||||
*
|
||||
* Mirrors AuraEffect::CalculateAmount from SpellAuraEffects.cpp for
|
||||
* MOD_FEAR/MOD_CONFUSE/MOD_STUN/MOD_ROOT/TRANSFORM:
|
||||
* amount = BaseHealth(casterLevel, CLASS_WARRIOR) / 4.75
|
||||
*
|
||||
* Uses known Warrior base health values from CreatureBaseStats DBC.
|
||||
*/
|
||||
static int32_t SimulateCCThreshold(uint8_t casterLevel)
|
||||
{
|
||||
// Warrior base health at key levels (EXPANSION_WRATH_OF_THE_LICH_KING)
|
||||
// From creature_classlevelstats for CLASS_WARRIOR
|
||||
struct LevelHealth { uint8_t level; int32_t health; };
|
||||
static constexpr LevelHealth table[] = {
|
||||
{1, 60}, {10, 424}, {20, 1128}, {30, 2078}, {40, 3228},
|
||||
{50, 4978}, {60, 7361}, {70, 9940}, {80, 12588},
|
||||
};
|
||||
|
||||
int32_t baseHealth = 12588; // default to level 80
|
||||
for (auto const& entry : table)
|
||||
{
|
||||
if (entry.level == casterLevel)
|
||||
{
|
||||
baseHealth = entry.health;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<int32_t>(baseHealth / 4.75f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test Fixture
|
||||
// =============================================================================
|
||||
|
||||
class BreakableCCProcTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_originalWorld = sWorld.release();
|
||||
_worldMock = new NiceMock<WorldMock>();
|
||||
sWorld.reset(_worldMock);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
IWorld* currentWorld = sWorld.release();
|
||||
delete currentWorld;
|
||||
sWorld.reset(_originalWorld);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create a CC aura effect stub with the given threshold
|
||||
*/
|
||||
AuraEffectStub CreateCCEffect(int32_t threshold, uint32_t auraType = 7 /* MOD_FEAR */)
|
||||
{
|
||||
AuraEffectStub effect(0, threshold, auraType);
|
||||
return effect;
|
||||
}
|
||||
|
||||
private:
|
||||
IWorld* _originalWorld = nullptr;
|
||||
NiceMock<WorldMock>* _worldMock = nullptr;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// HandleBreakableCCAuraProc Logic Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(BreakableCCProcTest, SmallDamage_ReducesThreshold_AuraSurvives)
|
||||
{
|
||||
auto effect = CreateCCEffect(1000);
|
||||
|
||||
bool removed = SimulateBreakableCCProc(&effect, 100);
|
||||
|
||||
EXPECT_FALSE(removed);
|
||||
EXPECT_EQ(effect.GetAmount(), 900);
|
||||
}
|
||||
|
||||
TEST_F(BreakableCCProcTest, ExactThresholdDamage_RemovesAura)
|
||||
{
|
||||
auto effect = CreateCCEffect(1000);
|
||||
|
||||
bool removed = SimulateBreakableCCProc(&effect, 1000);
|
||||
|
||||
EXPECT_TRUE(removed);
|
||||
}
|
||||
|
||||
TEST_F(BreakableCCProcTest, ExceedThresholdDamage_RemovesAura)
|
||||
{
|
||||
auto effect = CreateCCEffect(1000);
|
||||
|
||||
bool removed = SimulateBreakableCCProc(&effect, 5000);
|
||||
|
||||
EXPECT_TRUE(removed);
|
||||
}
|
||||
|
||||
TEST_F(BreakableCCProcTest, MultipleDamageHits_AccumulateUntilBreak)
|
||||
{
|
||||
auto effect = CreateCCEffect(1000);
|
||||
|
||||
// First hit: 400 damage, 600 remaining
|
||||
EXPECT_FALSE(SimulateBreakableCCProc(&effect, 400));
|
||||
EXPECT_EQ(effect.GetAmount(), 600);
|
||||
|
||||
// Second hit: 300 damage, 300 remaining
|
||||
EXPECT_FALSE(SimulateBreakableCCProc(&effect, 300));
|
||||
EXPECT_EQ(effect.GetAmount(), 300);
|
||||
|
||||
// Third hit: 300 damage, exactly 0 remaining -> remove
|
||||
EXPECT_TRUE(SimulateBreakableCCProc(&effect, 300));
|
||||
}
|
||||
|
||||
TEST_F(BreakableCCProcTest, MultipleDamageHits_OvershootBreak)
|
||||
{
|
||||
auto effect = CreateCCEffect(500);
|
||||
|
||||
// First hit: 200 damage
|
||||
EXPECT_FALSE(SimulateBreakableCCProc(&effect, 200));
|
||||
EXPECT_EQ(effect.GetAmount(), 300);
|
||||
|
||||
// Second hit: 400 damage, exceeds remaining 300
|
||||
EXPECT_TRUE(SimulateBreakableCCProc(&effect, 400));
|
||||
}
|
||||
|
||||
TEST_F(BreakableCCProcTest, OneDamage_ReducesThreshold)
|
||||
{
|
||||
auto effect = CreateCCEffect(1000);
|
||||
|
||||
EXPECT_FALSE(SimulateBreakableCCProc(&effect, 1));
|
||||
EXPECT_EQ(effect.GetAmount(), 999);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Threshold Calculation Tests (CalculateAmount for CC auras)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(BreakableCCProcTest, Level80Threshold_IsReasonable)
|
||||
{
|
||||
int32_t threshold = SimulateCCThreshold(80);
|
||||
|
||||
// Level 80 warrior base health = 12588
|
||||
// Threshold = 12588 / 4.75 ≈ 2650
|
||||
EXPECT_GT(threshold, 2600);
|
||||
EXPECT_LT(threshold, 2700);
|
||||
}
|
||||
|
||||
TEST_F(BreakableCCProcTest, LowerLevelCaster_LowerThreshold)
|
||||
{
|
||||
int32_t threshold60 = SimulateCCThreshold(60);
|
||||
int32_t threshold80 = SimulateCCThreshold(80);
|
||||
|
||||
EXPECT_LT(threshold60, threshold80);
|
||||
}
|
||||
|
||||
TEST_F(BreakableCCProcTest, Level80Fear_BreaksOnModerateDamage)
|
||||
{
|
||||
// Simulate a level 80 warlock's Fear
|
||||
int32_t threshold = SimulateCCThreshold(80); // ~2650
|
||||
auto effect = CreateCCEffect(threshold);
|
||||
|
||||
// A 3000 damage hit should break it
|
||||
EXPECT_TRUE(SimulateBreakableCCProc(&effect, 3000));
|
||||
}
|
||||
|
||||
TEST_F(BreakableCCProcTest, Level80Fear_SurvivesSmallDots)
|
||||
{
|
||||
// Simulate a level 80 warlock's Fear
|
||||
int32_t threshold = SimulateCCThreshold(80); // ~2650
|
||||
auto effect = CreateCCEffect(threshold);
|
||||
|
||||
// Small DoT ticks of 200 each - Fear should survive multiple ticks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
bool removed = SimulateBreakableCCProc(&effect, 200);
|
||||
if (i < 12) // Should survive at least 12 ticks (200*13 = 2600 < 2650)
|
||||
{
|
||||
// We expect it to survive for ~13 ticks
|
||||
if (!removed)
|
||||
continue;
|
||||
}
|
||||
if (removed)
|
||||
{
|
||||
// Should break around tick 13-14
|
||||
EXPECT_GE(i, 12);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If we get here, verify remaining threshold
|
||||
EXPECT_GT(effect.GetAmount(), 0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Proc Pipeline Integration Tests (using CanSpellTriggerProcOnEvent)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(BreakableCCProcTest, FearProcEntry_MatchesTakenMeleeDamage)
|
||||
{
|
||||
// Fear's auto-generated proc entry from DBC ProcFlags
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(
|
||||
PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK |
|
||||
PROC_FLAG_TAKEN_SPELL_MELEE_DMG_CLASS |
|
||||
PROC_FLAG_TAKEN_RANGED_AUTO_ATTACK |
|
||||
PROC_FLAG_TAKEN_SPELL_RANGED_DMG_CLASS |
|
||||
PROC_FLAG_TAKEN_SPELL_NONE_DMG_CLASS_NEG |
|
||||
PROC_FLAG_TAKEN_SPELL_MAGIC_DMG_CLASS_NEG |
|
||||
PROC_FLAG_TAKEN_PERIODIC)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Melee auto attack should trigger
|
||||
auto meleeEvent = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, meleeEvent));
|
||||
}
|
||||
|
||||
TEST_F(BreakableCCProcTest, FearProcEntry_MatchesTakenSpellDamage)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(
|
||||
PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK |
|
||||
PROC_FLAG_TAKEN_SPELL_MELEE_DMG_CLASS |
|
||||
PROC_FLAG_TAKEN_RANGED_AUTO_ATTACK |
|
||||
PROC_FLAG_TAKEN_SPELL_RANGED_DMG_CLASS |
|
||||
PROC_FLAG_TAKEN_SPELL_NONE_DMG_CLASS_NEG |
|
||||
PROC_FLAG_TAKEN_SPELL_MAGIC_DMG_CLASS_NEG |
|
||||
PROC_FLAG_TAKEN_PERIODIC)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Magic damage spell should trigger
|
||||
auto spellEvent = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_TAKEN_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, spellEvent));
|
||||
}
|
||||
|
||||
TEST_F(BreakableCCProcTest, FearProcEntry_DoesNotMatchHealEvent)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(
|
||||
PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK |
|
||||
PROC_FLAG_TAKEN_SPELL_MELEE_DMG_CLASS |
|
||||
PROC_FLAG_TAKEN_RANGED_AUTO_ATTACK |
|
||||
PROC_FLAG_TAKEN_SPELL_RANGED_DMG_CLASS |
|
||||
PROC_FLAG_TAKEN_SPELL_NONE_DMG_CLASS_NEG |
|
||||
PROC_FLAG_TAKEN_SPELL_MAGIC_DMG_CLASS_NEG |
|
||||
PROC_FLAG_TAKEN_PERIODIC)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Heal should NOT trigger Fear's proc
|
||||
auto healEvent = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_TAKEN_SPELL_MAGIC_DMG_CLASS_POS)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_HEAL)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, healEvent));
|
||||
}
|
||||
|
||||
TEST_F(BreakableCCProcTest, FearProcChance_Is100Percent)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Fear has 100% proc chance from DBC - every damage event triggers
|
||||
float chance = ProcChanceTestHelper::SimulateCalcProcChance(procEntry);
|
||||
EXPECT_FLOAT_EQ(chance, 100.0f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Glyph of Fear Threshold Modifier Test
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(BreakableCCProcTest, GlyphOfFear_IncreasesThreshold)
|
||||
{
|
||||
// Glyph of Fear adds +100% to the damage threshold (MiscValue 7801)
|
||||
int32_t baseThreshold = SimulateCCThreshold(80); // ~2650
|
||||
int32_t glyphedThreshold = baseThreshold + (baseThreshold * 100 / 100); // +100%
|
||||
|
||||
auto effect = CreateCCEffect(glyphedThreshold);
|
||||
|
||||
// Should survive hits that would normally break it
|
||||
EXPECT_FALSE(SimulateBreakableCCProc(&effect, 3000));
|
||||
EXPECT_GT(effect.GetAmount(), 0);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* 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 CascadeProcSuppressionTest.cpp
|
||||
* @brief Unit tests for cascade proc suppression via SPELL_ATTR3_INSTANT_TARGET_PROCS
|
||||
*
|
||||
* Tests the logic from Unit.cpp TriggerAurasProcOnEvent:
|
||||
* - Outer check: Spell::IsProcDisabled() (TRIGGERED_DISALLOW_PROC_EVENTS) suppresses all cascade procs
|
||||
* - Per-aura check: SPELL_ATTR3_INSTANT_TARGET_PROCS (0x80000) suppresses cascade for that aura
|
||||
* - Normal spells/auras without these flags allow cascading
|
||||
* - Both flags set simultaneously still suppresses correctly
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "SpellInfoTestHelper.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
class CascadeProcSuppressionTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
ProcChanceTestHelper::CascadeProcConfig MakeConfig(
|
||||
bool isProcDisabled, bool hasDisableProcAttr)
|
||||
{
|
||||
ProcChanceTestHelper::CascadeProcConfig config;
|
||||
config.triggeringSpellIsProcDisabled = isProcDisabled;
|
||||
config.auraHasDisableProcAttr = hasDisableProcAttr;
|
||||
return config;
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Normal behavior (no suppression)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(CascadeProcSuppressionTest, NormalSpellNormalAura_NotSuppressed)
|
||||
{
|
||||
auto config = MakeConfig(false, false);
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
|
||||
<< "Normal spell + normal aura should not suppress cascading procs";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// IsProcDisabled (outer check - TRIGGERED_DISALLOW_PROC_EVENTS)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(CascadeProcSuppressionTest, ProcDisabledSpell_NormalAura_Suppressed)
|
||||
{
|
||||
auto config = MakeConfig(true, false);
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
|
||||
<< "Triggered spell with DISALLOW_PROC_EVENTS should suppress all cascading procs";
|
||||
}
|
||||
|
||||
TEST_F(CascadeProcSuppressionTest, ProcDisabledSpell_WithAttr_Suppressed)
|
||||
{
|
||||
auto config = MakeConfig(true, true);
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
|
||||
<< "Both flags set should still suppress (double-suppress doesn't break)";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SPELL_ATTR3_INSTANT_TARGET_PROCS (per-aura check)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(CascadeProcSuppressionTest, NormalSpell_AuraWithAttr_Suppressed)
|
||||
{
|
||||
auto config = MakeConfig(false, true);
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
|
||||
<< "Aura with SPELL_ATTR3_INSTANT_TARGET_PROCS should suppress cascading procs";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SpellInfo attribute verification via SpellInfoBuilder
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(CascadeProcSuppressionTest, SpellInfo_WithAttr_HasAttributeReturnsTrue)
|
||||
{
|
||||
auto spellInfo = SpellInfoBuilder()
|
||||
.WithId(99001)
|
||||
.WithAttributesEx3(SPELL_ATTR3_INSTANT_TARGET_PROCS)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_TRUE(spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
|
||||
<< "SpellInfo built with 0x80000 should report HasAttribute true";
|
||||
}
|
||||
|
||||
TEST_F(CascadeProcSuppressionTest, SpellInfo_WithoutAttr_HasAttributeReturnsFalse)
|
||||
{
|
||||
auto spellInfo = SpellInfoBuilder()
|
||||
.WithId(99002)
|
||||
.WithAttributesEx3(0)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_FALSE(spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
|
||||
<< "SpellInfo built with 0 should report HasAttribute false";
|
||||
}
|
||||
|
||||
TEST_F(CascadeProcSuppressionTest, SpellInfo_WithMixedBits_HasAttributeReturnsTrue)
|
||||
{
|
||||
// 0x80001 = SPELL_ATTR3_INSTANT_TARGET_PROCS | SPELL_ATTR3_PVP_ENABLING (bit 0)
|
||||
auto spellInfo = SpellInfoBuilder()
|
||||
.WithId(99003)
|
||||
.WithAttributesEx3(0x00080001)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_TRUE(spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
|
||||
<< "Other bits in AttributesEx3 should not interfere with attribute detection";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real spell scenarios (data-driven)
|
||||
// These spells have SPELL_ATTR3_INSTANT_TARGET_PROCS (0x80000) in DBC
|
||||
// =============================================================================
|
||||
|
||||
struct RealSpellTestCase
|
||||
{
|
||||
const char* name;
|
||||
uint32 spellId;
|
||||
bool hasAttr; // Whether the spell has SPELL_ATTR3_INSTANT_TARGET_PROCS
|
||||
};
|
||||
|
||||
class CascadeProcRealSpellTest : public ::testing::TestWithParam<RealSpellTestCase> {};
|
||||
|
||||
TEST_P(CascadeProcRealSpellTest, VerifySuppressionForRealSpell)
|
||||
{
|
||||
auto const& tc = GetParam();
|
||||
|
||||
// Build a SpellInfo mimicking the real spell's AttributesEx3
|
||||
auto spellInfo = SpellInfoBuilder()
|
||||
.WithId(tc.spellId)
|
||||
.WithAttributesEx3(tc.hasAttr ? SPELL_ATTR3_INSTANT_TARGET_PROCS : 0)
|
||||
.BuildUnique();
|
||||
|
||||
// Verify attribute detection matches expectation
|
||||
EXPECT_EQ(spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS), tc.hasAttr)
|
||||
<< tc.name << " (spell " << tc.spellId << ") attribute detection mismatch";
|
||||
|
||||
// Verify cascade suppression matches attribute presence
|
||||
ProcChanceTestHelper::CascadeProcConfig config;
|
||||
config.triggeringSpellIsProcDisabled = false;
|
||||
config.auraHasDisableProcAttr = tc.hasAttr;
|
||||
|
||||
EXPECT_EQ(ProcChanceTestHelper::ShouldSuppressCascadingProc(config), tc.hasAttr)
|
||||
<< tc.name << " (spell " << tc.spellId << ") cascade suppression mismatch";
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
CascadeProcSuppression,
|
||||
CascadeProcRealSpellTest,
|
||||
::testing::Values(
|
||||
// Spells WITH SPELL_ATTR3_INSTANT_TARGET_PROCS
|
||||
RealSpellTestCase{"Seal Fate", 14195, true},
|
||||
RealSpellTestCase{"Sword Specialization", 12281, true},
|
||||
RealSpellTestCase{"Reckoning", 20178, true},
|
||||
RealSpellTestCase{"Flurry", 16257, true},
|
||||
// Counter-example: spell WITHOUT the attribute
|
||||
RealSpellTestCase{"Eviscerate", 26865, false}
|
||||
),
|
||||
[](testing::TestParamInfo<RealSpellTestCase> const& info) {
|
||||
// Generate readable test name from spell name (replace spaces)
|
||||
std::string name = info.param.name;
|
||||
std::replace(name.begin(), name.end(), ' ', '_');
|
||||
return name;
|
||||
}
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// Nesting behavior - both flags simultaneously
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(CascadeProcSuppressionTest, BothFlagsSet_StillSuppressed)
|
||||
{
|
||||
auto config = MakeConfig(true, true);
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
|
||||
<< "Both IsProcDisabled and INSTANT_TARGET_PROCS set should still suppress";
|
||||
}
|
||||
|
||||
TEST_F(CascadeProcSuppressionTest, OnlyOuterFlag_Suppressed)
|
||||
{
|
||||
auto config = MakeConfig(true, false);
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
|
||||
<< "Only IsProcDisabled should be sufficient to suppress";
|
||||
}
|
||||
|
||||
TEST_F(CascadeProcSuppressionTest, OnlyPerAuraFlag_Suppressed)
|
||||
{
|
||||
auto config = MakeConfig(false, true);
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
|
||||
<< "Only INSTANT_TARGET_PROCS should be sufficient to suppress";
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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 ExtraAttackChainProcTest.cpp
|
||||
* @brief Unit tests for extra attack chain-proc prevention
|
||||
*
|
||||
* Tests the logic from SpellAuraEffects.cpp:1245-1261 (CheckEffectProc):
|
||||
* - Self-chain prevention (same extra attack spell can't proc itself)
|
||||
* - Cross-chain prevention (Sword Specialization / Hack and Slash block all extra attack procs)
|
||||
* - Non-blacklisted extra attack spells allow cross-proccing
|
||||
* - Non-extra-attack procs are unaffected by the guard
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
// Use existing enum from Unit.h: SPELL_SWORD_SPECIALIZATIONIALIZATION (16459), SPELL_HACK_AND_SLASH (66923)
|
||||
constexpr uint32 SPELL_RECKONING = 32746; // Reckoning (Paladin)
|
||||
constexpr uint32 SPELL_HAND_OF_JUSTICE = 15601; // Hand of Justice extra attack
|
||||
|
||||
class ExtraAttackChainProcTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
ProcChanceTestHelper::ExtraAttackProcConfig MakeConfig(
|
||||
bool hasExtraAttacks, uint32 triggerSpellId, uint32 lastExtraAttack)
|
||||
{
|
||||
ProcChanceTestHelper::ExtraAttackProcConfig config;
|
||||
config.triggeredSpellHasExtraAttacks = hasExtraAttacks;
|
||||
config.triggerSpellId = triggerSpellId;
|
||||
config.lastExtraAttackSpell = lastExtraAttack;
|
||||
return config;
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Normal proc (no extra attack in progress)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ExtraAttackChainProcTest, NormalProc_AllowedWhenNoExtraAttackInProgress)
|
||||
{
|
||||
// lastExtraAttackSpell == 0 means no extra attack is executing
|
||||
auto config = MakeConfig(true, SPELL_SWORD_SPECIALIZATION, 0);
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config))
|
||||
<< "Extra attack proc should be allowed when no extra attack is in progress";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Self-chain prevention
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ExtraAttackChainProcTest, SelfChain_BlockedWhenSameSpell)
|
||||
{
|
||||
// Sword Spec trying to proc during its own extra attack
|
||||
auto config = MakeConfig(true, SPELL_SWORD_SPECIALIZATION, SPELL_SWORD_SPECIALIZATION);
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config))
|
||||
<< "Extra attack spell should not chain-proc itself";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Cross-chain prevention (blacklisted spells)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ExtraAttackChainProcTest, CrossChain_BlockedBySwordSpecialization)
|
||||
{
|
||||
// Reckoning trying to proc during Sword Spec extra attack
|
||||
auto config = MakeConfig(true, SPELL_RECKONING, SPELL_SWORD_SPECIALIZATION);
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config))
|
||||
<< "Sword Specialization extra attack should block all other extra attack procs";
|
||||
}
|
||||
|
||||
TEST_F(ExtraAttackChainProcTest, CrossChain_BlockedByHackAndSlash)
|
||||
{
|
||||
// Reckoning trying to proc during Hack and Slash extra attack
|
||||
auto config = MakeConfig(true, SPELL_RECKONING, SPELL_HACK_AND_SLASH);
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config))
|
||||
<< "Hack and Slash extra attack should block all other extra attack procs";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Non-blacklisted extra attacks allow cross-proccing
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ExtraAttackChainProcTest, DifferentExtraAttack_AllowedWhenNotBlacklisted)
|
||||
{
|
||||
// Sword Spec trying to proc during Hand of Justice extra attack
|
||||
// Hand of Justice (15601) is not blacklisted, so cross-proc is allowed
|
||||
auto config = MakeConfig(true, SPELL_SWORD_SPECIALIZATION, SPELL_HAND_OF_JUSTICE);
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config))
|
||||
<< "Non-blacklisted extra attack spells should allow cross-proccing";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Non-extra-attack procs unaffected
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ExtraAttackChainProcTest, NonExtraAttackProc_UnaffectedByExtraAttackState)
|
||||
{
|
||||
// A proc that does NOT grant extra attacks should never be blocked,
|
||||
// even during Sword Spec extra attack
|
||||
auto config = MakeConfig(false, 12345, SPELL_SWORD_SPECIALIZATION);
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config))
|
||||
<< "Non-extra-attack procs should be unaffected by extra attack state";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real spell scenarios
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ExtraAttackChainProcTest, Reckoning_SelfChainBlocked)
|
||||
{
|
||||
// Reckoning (32746) trying to proc during its own extra attack
|
||||
auto config = MakeConfig(true, SPELL_RECKONING, SPELL_RECKONING);
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config))
|
||||
<< "Reckoning should not chain-proc itself";
|
||||
}
|
||||
|
||||
TEST_F(ExtraAttackChainProcTest, Reckoning_AllowedDuringHandOfJustice)
|
||||
{
|
||||
// Reckoning trying to proc during Hand of Justice extra attack
|
||||
// Hand of Justice is not blacklisted, so Reckoning is allowed
|
||||
auto config = MakeConfig(true, SPELL_RECKONING, SPELL_HAND_OF_JUSTICE);
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockExtraAttackChainProc(config))
|
||||
<< "Reckoning should be allowed during Hand of Justice extra attack";
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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<WorldMock>();
|
||||
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>* _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));
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
* 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 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));
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* 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 SpellProcArcanePotencyTest.cpp
|
||||
* @brief Unit tests for Arcane Potency proc behavior
|
||||
*
|
||||
* Arcane Potency (57529/57531) buffs should only be consumed by the spell
|
||||
* that was actually affected by the crit bonus, not by the same cast that
|
||||
* triggered them. This is achieved via PROC_ATTR_REQ_SPELLMOD (0x08) which
|
||||
* requires the proccing aura to be in the triggering spell's m_appliedMods.
|
||||
*
|
||||
* The crit aura registration in SpellDoneCritChance ensures that only spells
|
||||
* whose crit chance was actually modified by Arcane Potency will have it in
|
||||
* their m_appliedMods set.
|
||||
*
|
||||
* References:
|
||||
* - TrinityCore commit 81f16b201b
|
||||
* - ChromieCraft issue #9092
|
||||
*/
|
||||
|
||||
#include "AuraScriptTestFramework.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
namespace
|
||||
{
|
||||
// Arcane Potency buff spell IDs
|
||||
constexpr uint32 ARCANE_POTENCY_R1 = 57529;
|
||||
constexpr uint32 ARCANE_POTENCY_R2 = 57531;
|
||||
|
||||
// SpellFamilyMask from spell_proc for Arcane Potency
|
||||
constexpr uint32 AP_FAMILY_MASK0 = 0x61401035;
|
||||
constexpr uint32 AP_FAMILY_MASK1 = 0x00001000;
|
||||
constexpr uint32 AP_FAMILY_MASK2 = 0;
|
||||
|
||||
// Mage spell flags that SHOULD match Arcane Potency's mask
|
||||
// Fireball: bit 0
|
||||
constexpr uint32 FIREBALL_FLAG0 = 0x00000001;
|
||||
// Frostfire Bolt: bit 2 + Mask1 bit 12
|
||||
constexpr uint32 FROSTFIRE_BOLT_FLAG0 = 0x00000004;
|
||||
constexpr uint32 FROSTFIRE_BOLT_FLAG1 = 0x00001000;
|
||||
// Fire Blast: bit 4
|
||||
constexpr uint32 FIRE_BLAST_FLAG0 = 0x00000010;
|
||||
// Frostbolt: bit 5
|
||||
constexpr uint32 FROSTBOLT_FLAG0 = 0x00000020;
|
||||
// Arcane Explosion: bit 12
|
||||
constexpr uint32 ARCANE_EXPLOSION_FLAG0 = 0x00001000;
|
||||
// Scorch: bit 22
|
||||
constexpr uint32 SCORCH_FLAG0 = 0x00400000;
|
||||
// Arcane Blast: bit 29
|
||||
constexpr uint32 ARCANE_BLAST_FLAG0 = 0x20000000;
|
||||
|
||||
// Mage spell flags that should NOT match
|
||||
// Arcane Missiles: bit 11
|
||||
constexpr uint32 ARCANE_MISSILES_FLAG0 = 0x00000800;
|
||||
// Ice Lance: bit 17
|
||||
constexpr uint32 ICE_LANCE_FLAG0 = 0x00020000;
|
||||
// Arcane Barrage: Mask1 bit 15
|
||||
constexpr uint32 ARCANE_BARRAGE_FLAG1 = 0x00008000;
|
||||
|
||||
/**
|
||||
* Build the Arcane Potency proc entry matching our spell_proc SQL.
|
||||
*/
|
||||
SpellProcEntry BuildArcanePotencyProcEntry()
|
||||
{
|
||||
return SpellProcEntryBuilder()
|
||||
.WithSpellFamilyName(SPELLFAMILY_MAGE)
|
||||
.WithSpellFamilyMask(flag96(AP_FAMILY_MASK0, AP_FAMILY_MASK1, AP_FAMILY_MASK2))
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
|
||||
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Proc Entry Configuration Tests
|
||||
// =============================================================================
|
||||
|
||||
class ArcanePotencyProcTest : public AuraScriptProcTestFixture
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
AuraScriptProcTestFixture::SetUp();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, ProcEntry_HasReqSpellmodAttribute)
|
||||
{
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD)
|
||||
<< "Arcane Potency must have PROC_ATTR_REQ_SPELLMOD to prevent "
|
||||
"same-cast consumption";
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, ProcEntry_CastPhaseOnly)
|
||||
{
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_EQ(procEntry.SpellPhaseMask, PROC_SPELL_PHASE_CAST)
|
||||
<< "Arcane Potency should only proc on CAST phase";
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, ProcEntry_MageFamily)
|
||||
{
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_EQ(procEntry.SpellFamilyName, SPELLFAMILY_MAGE);
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, ProcEntry_BothRanksIdentical)
|
||||
{
|
||||
// Both ranks should use the same proc configuration
|
||||
auto r1 = BuildArcanePotencyProcEntry();
|
||||
auto r2 = BuildArcanePotencyProcEntry();
|
||||
|
||||
EXPECT_EQ(r1.SpellFamilyName, r2.SpellFamilyName);
|
||||
EXPECT_TRUE(r1.SpellFamilyMask == r2.SpellFamilyMask);
|
||||
EXPECT_EQ(r1.SpellPhaseMask, r2.SpellPhaseMask);
|
||||
EXPECT_EQ(r1.AttributesMask, r2.AttributesMask);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SpellFamilyMask Matching Tests - Spells That SHOULD Match
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_Fireball_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(133, SPELLFAMILY_MAGE, FIREBALL_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_Frostbolt_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(116, SPELLFAMILY_MAGE, FROSTBOLT_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_ArcaneBlast_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(30451, SPELLFAMILY_MAGE, ARCANE_BLAST_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_ArcaneExplosion_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(1449, SPELLFAMILY_MAGE, ARCANE_EXPLOSION_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_FrostfireBolt_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(44614, SPELLFAMILY_MAGE,
|
||||
FROSTFIRE_BOLT_FLAG0, FROSTFIRE_BOLT_FLAG1);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_FireBlast_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(2136, SPELLFAMILY_MAGE, FIRE_BLAST_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_Scorch_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(2948, SPELLFAMILY_MAGE, SCORCH_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SpellFamilyMask Matching Tests - Spells That Should NOT Match
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_ArcaneMissiles_DoesNotMatch)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(5143, SPELLFAMILY_MAGE, ARCANE_MISSILES_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_IceLance_DoesNotMatch)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(30455, SPELLFAMILY_MAGE, ICE_LANCE_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_ArcaneBarrage_DoesNotMatch)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(44425, SPELLFAMILY_MAGE, 0, ARCANE_BARRAGE_FLAG1);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_NonMageSpell_DoesNotMatch)
|
||||
{
|
||||
// Warrior spell with matching flags should NOT match due to family check
|
||||
auto* spell = CreateSpellInfo(6343, /*SPELLFAMILY_WARRIOR*/4, FIREBALL_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Proc Phase Tests - CAST phase filtering
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, Phase_TriggersOnCast)
|
||||
{
|
||||
// Test phase filtering in isolation (no family filter)
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
|
||||
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnSpellDamage()
|
||||
.OnCast();
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, Phase_DoesNotTriggerOnHit)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
|
||||
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnSpellDamage()
|
||||
.OnHit()
|
||||
.WithNormalHit();
|
||||
EXPECT_PROC_DOES_NOT_TRIGGER(procEntry, scenario);
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, Phase_DoesNotTriggerOnFinish)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
|
||||
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnSpellDamage()
|
||||
.OnFinish();
|
||||
EXPECT_PROC_DOES_NOT_TRIGGER(procEntry, scenario);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROC_ATTR_REQ_SPELLMOD Charge Gating Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, ReqSpellmod_AuraWithCharges_BlocksWithoutAppliedMod)
|
||||
{
|
||||
// When PROC_ATTR_REQ_SPELLMOD is set and aura uses charges,
|
||||
// GetProcEffectMask should return 0 if aura is not in m_appliedMods.
|
||||
// We test the prerequisite: that the proc entry is configured correctly
|
||||
// so the charge-gating code path in GetProcEffectMask is reachable.
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(ARCANE_POTENCY_R1)
|
||||
.WithCharges(1)
|
||||
.Build();
|
||||
|
||||
// The aura should be using charges (required for REQ_SPELLMOD gating)
|
||||
EXPECT_TRUE(aura->IsUsingCharges());
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD);
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, ReqSpellmod_AuraWithoutCharges_SkipsCheck)
|
||||
{
|
||||
// If aura has no charges, PROC_ATTR_REQ_SPELLMOD check is skipped
|
||||
// (no charges to consume = no need for spellmod gating)
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(ARCANE_POTENCY_R1)
|
||||
.WithCharges(0)
|
||||
.Build();
|
||||
|
||||
EXPECT_FALSE(aura->IsUsingCharges());
|
||||
// Even though PROC_ATTR_REQ_SPELLMOD is set, the condition
|
||||
// (IsUsingCharges() || USE_STACKS_FOR_CHARGES) would be false
|
||||
// so the spellmod check would be skipped
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SpellFamilyMask Bitmask Validation
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, MaskBits_CorrectBitsSet)
|
||||
{
|
||||
// Verify the exact bits in our SpellFamilyMask0
|
||||
// Bit 0: Fireball
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00000001);
|
||||
// Bit 2: Frostfire Bolt
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00000004);
|
||||
// Bit 4: Fire Blast
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00000010);
|
||||
// Bit 5: Frostbolt
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00000020);
|
||||
// Bit 12: Arcane Explosion
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00001000);
|
||||
// Bit 22: Scorch
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00400000);
|
||||
// Bit 29: Arcane Blast
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x20000000);
|
||||
|
||||
// Mask1: Bit 12 (Frostfire Bolt secondary)
|
||||
EXPECT_TRUE(AP_FAMILY_MASK1 & 0x00001000);
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, MaskBits_CorrectBitsNotSet)
|
||||
{
|
||||
// Bit 11: Arcane Missiles - should NOT be set
|
||||
EXPECT_FALSE(AP_FAMILY_MASK0 & 0x00000800);
|
||||
// Bit 17: Ice Lance - should NOT be set
|
||||
EXPECT_FALSE(AP_FAMILY_MASK0 & 0x00020000);
|
||||
// Mask1 bit 15: Arcane Barrage - should NOT be set
|
||||
EXPECT_FALSE(AP_FAMILY_MASK1 & 0x00008000);
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
/*
|
||||
* 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 SpellProcAttributeTest.cpp
|
||||
* @brief Unit tests for PROC_ATTR_* flags
|
||||
*
|
||||
* Tests all proc attribute flags:
|
||||
* - PROC_ATTR_REQ_EXP_OR_HONOR (0x01)
|
||||
* - PROC_ATTR_TRIGGERED_CAN_PROC (0x02)
|
||||
* - PROC_ATTR_REQ_MANA_COST (0x04)
|
||||
* - PROC_ATTR_REQ_SPELLMOD (0x08)
|
||||
* - PROC_ATTR_USE_STACKS_FOR_CHARGES (0x10)
|
||||
* - PROC_ATTR_REDUCE_PROC_60 (0x80)
|
||||
* - PROC_ATTR_CANT_PROC_FROM_ITEM_CAST (0x100)
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "SpellInfoTestHelper.h"
|
||||
#include "AuraStub.h"
|
||||
#include "UnitStub.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
class SpellProcAttributeTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// PROC_ATTR_REQ_EXP_OR_HONOR (0x01) Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReqExpOrHonor_AttributeSet)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REQ_EXP_OR_HONOR)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_EXP_OR_HONOR);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReqExpOrHonor_AttributeNotSet)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(0)
|
||||
.Build();
|
||||
|
||||
EXPECT_FALSE(procEntry.AttributesMask & PROC_ATTR_REQ_EXP_OR_HONOR);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROC_ATTR_TRIGGERED_CAN_PROC (0x02) Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcAttributeTest, TriggeredCanProc_AttributeSet)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_TRIGGERED_CAN_PROC)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_TRIGGERED_CAN_PROC);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, TriggeredCanProc_AttributeNotSet)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(0)
|
||||
.Build();
|
||||
|
||||
EXPECT_FALSE(procEntry.AttributesMask & PROC_ATTR_TRIGGERED_CAN_PROC);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROC_ATTR_REQ_MANA_COST (0x04) Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReqManaCost_AttributeSet)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REQ_MANA_COST)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_MANA_COST);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReqManaCost_NullSpell_ShouldNotProc)
|
||||
{
|
||||
// Null spell should never satisfy mana cost requirement
|
||||
EXPECT_FALSE(ProcChanceTestHelper::SpellHasManaCost(nullptr));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROC_ATTR_REQ_SPELLMOD (0x08) Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReqSpellmod_AttributeSet)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReqSpellmod_AttributeNotSet)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(0)
|
||||
.Build();
|
||||
|
||||
EXPECT_FALSE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROC_ATTR_REQ_SPELLMOD Auto-Generation Tests
|
||||
// Validates that LoadSpellProcs auto-generates REQ_SPELLMOD for modifier
|
||||
// auras with charges, preventing charge consumption by unrelated spells.
|
||||
// =============================================================================
|
||||
|
||||
namespace
|
||||
{
|
||||
// Replicates the auto-generation logic from SpellMgr::LoadSpellProcs
|
||||
void ApplyAutoGeneratedSpellmodFlag(SpellInfo const* spellInfo, SpellProcEntry& procEntry)
|
||||
{
|
||||
if (spellInfo->ProcCharges)
|
||||
{
|
||||
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
|
||||
{
|
||||
if (spellInfo->Effects[i].IsAura(SPELL_AURA_ADD_FLAT_MODIFIER) ||
|
||||
spellInfo->Effects[i].IsAura(SPELL_AURA_ADD_PCT_MODIFIER))
|
||||
{
|
||||
procEntry.AttributesMask |= PROC_ATTR_REQ_SPELLMOD;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReqSpellmod_AutoGen_PctModifierWithCharges)
|
||||
{
|
||||
// Rapid Killing (35099): ADD_PCT_MODIFIER + 1 charge
|
||||
auto spellInfo = SpellInfoBuilder()
|
||||
.WithId(35099)
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_ADD_PCT_MODIFIER)
|
||||
.WithProcFlags(PROC_FLAG_DONE_RANGED_AUTO_ATTACK | PROC_FLAG_DONE_SPELL_RANGED_DMG_CLASS)
|
||||
.WithProcCharges(1)
|
||||
.BuildUnique();
|
||||
|
||||
SpellProcEntry procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_RANGED_AUTO_ATTACK | PROC_FLAG_DONE_SPELL_RANGED_DMG_CLASS)
|
||||
.WithCharges(1)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
ApplyAutoGeneratedSpellmodFlag(spellInfo.get(), procEntry);
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReqSpellmod_AutoGen_FlatModifierWithCharges)
|
||||
{
|
||||
// Clearcasting-like: ADD_FLAT_MODIFIER + charges
|
||||
auto spellInfo = SpellInfoBuilder()
|
||||
.WithId(12345)
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_ADD_FLAT_MODIFIER)
|
||||
.WithProcCharges(2)
|
||||
.BuildUnique();
|
||||
|
||||
SpellProcEntry procEntry = SpellProcEntryBuilder()
|
||||
.WithCharges(2)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
ApplyAutoGeneratedSpellmodFlag(spellInfo.get(), procEntry);
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReqSpellmod_AutoGen_NoCharges_NotSet)
|
||||
{
|
||||
// Modifier aura without charges should NOT get the flag
|
||||
auto spellInfo = SpellInfoBuilder()
|
||||
.WithId(12345)
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_ADD_PCT_MODIFIER)
|
||||
.WithProcCharges(0)
|
||||
.BuildUnique();
|
||||
|
||||
SpellProcEntry procEntry = SpellProcEntryBuilder()
|
||||
.WithCharges(0)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
ApplyAutoGeneratedSpellmodFlag(spellInfo.get(), procEntry);
|
||||
|
||||
EXPECT_FALSE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReqSpellmod_AutoGen_NonModifierWithCharges_NotSet)
|
||||
{
|
||||
// Non-modifier aura with charges should NOT get the flag
|
||||
auto spellInfo = SpellInfoBuilder()
|
||||
.WithId(12345)
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_PROC_TRIGGER_SPELL)
|
||||
.WithProcCharges(1)
|
||||
.BuildUnique();
|
||||
|
||||
SpellProcEntry procEntry = SpellProcEntryBuilder()
|
||||
.WithCharges(1)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
ApplyAutoGeneratedSpellmodFlag(spellInfo.get(), procEntry);
|
||||
|
||||
EXPECT_FALSE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReqSpellmod_AutoGen_ModifierOnSecondEffect)
|
||||
{
|
||||
// Modifier on effect index 1, not 0
|
||||
auto spellInfo = SpellInfoBuilder()
|
||||
.WithId(12345)
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_PROC_TRIGGER_SPELL)
|
||||
.WithEffect(1, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_ADD_FLAT_MODIFIER)
|
||||
.WithProcCharges(1)
|
||||
.BuildUnique();
|
||||
|
||||
SpellProcEntry procEntry = SpellProcEntryBuilder()
|
||||
.WithCharges(1)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
ApplyAutoGeneratedSpellmodFlag(spellInfo.get(), procEntry);
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReqSpellmod_AutoGen_PreservesExistingAttributes)
|
||||
{
|
||||
// Should OR with existing attributes, not replace
|
||||
auto spellInfo = SpellInfoBuilder()
|
||||
.WithId(12345)
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_ADD_PCT_MODIFIER)
|
||||
.WithProcCharges(1)
|
||||
.BuildUnique();
|
||||
|
||||
SpellProcEntry procEntry = SpellProcEntryBuilder()
|
||||
.WithCharges(1)
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_TRIGGERED_CAN_PROC)
|
||||
.Build();
|
||||
|
||||
ApplyAutoGeneratedSpellmodFlag(spellInfo.get(), procEntry);
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD);
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_TRIGGERED_CAN_PROC);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROC_ATTR_USE_STACKS_FOR_CHARGES (0x10) Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcAttributeTest, UseStacksForCharges_AttributeSet)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_USE_STACKS_FOR_CHARGES);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, UseStacksForCharges_DecrementStacks)
|
||||
{
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(12345)
|
||||
.WithStackAmount(5)
|
||||
.Build();
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
.Build();
|
||||
|
||||
ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
|
||||
EXPECT_EQ(aura->GetStackAmount(), 4);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, UseStacksForCharges_NotSet_DecrementCharges)
|
||||
{
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(12345)
|
||||
.WithCharges(5)
|
||||
.WithStackAmount(5)
|
||||
.Build();
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(0) // No USE_STACKS_FOR_CHARGES
|
||||
.Build();
|
||||
|
||||
ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
|
||||
// Charges should decrement, stacks unchanged
|
||||
EXPECT_EQ(aura->GetCharges(), 4);
|
||||
EXPECT_EQ(aura->GetStackAmount(), 5);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROC_ATTR_REDUCE_PROC_60 (0x80) Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReduceProc60_AttributeSet)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REDUCE_PROC_60);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReduceProc60_Level60_NoReduction)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
float chance = ProcChanceTestHelper::SimulateCalcProcChance(procEntry, 60);
|
||||
EXPECT_NEAR(chance, 30.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReduceProc60_Level70_Reduced)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
// Level 70 = 10 levels above 60
|
||||
// Reduction = 10/30 = 33.33%
|
||||
// 30% * (1 - 0.333) = 20%
|
||||
float chance = ProcChanceTestHelper::SimulateCalcProcChance(procEntry, 70);
|
||||
EXPECT_NEAR(chance, 20.0f, 0.5f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReduceProc60_Level80_Reduced)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
// Level 80 = 20 levels above 60
|
||||
// Reduction = 20/30 = 66.67%
|
||||
// 30% * (1 - 0.667) = 10%
|
||||
float chance = ProcChanceTestHelper::SimulateCalcProcChance(procEntry, 80);
|
||||
EXPECT_NEAR(chance, 10.0f, 0.5f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReduceProc60_BelowLevel60_NoReduction)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
float chance = ProcChanceTestHelper::SimulateCalcProcChance(procEntry, 50);
|
||||
EXPECT_NEAR(chance, 30.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, ReduceProc60_NotSet_NoReduction)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(0) // No REDUCE_PROC_60
|
||||
.Build();
|
||||
|
||||
// Even at level 80, no reduction without attribute
|
||||
float chance = ProcChanceTestHelper::SimulateCalcProcChance(procEntry, 80);
|
||||
EXPECT_NEAR(chance, 30.0f, 0.01f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROC_ATTR_CANT_PROC_FROM_ITEM_CAST (0x100) Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcAttributeTest, CantProcFromItemCast_AttributeSet)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_CANT_PROC_FROM_ITEM_CAST)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_CANT_PROC_FROM_ITEM_CAST);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, CantProcFromItemCast_AttributeNotSet)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(0)
|
||||
.Build();
|
||||
|
||||
EXPECT_FALSE(procEntry.AttributesMask & PROC_ATTR_CANT_PROC_FROM_ITEM_CAST);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Combined Attribute Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcAttributeTest, CombinedAttributes_MultipleFlags)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(
|
||||
PROC_ATTR_TRIGGERED_CAN_PROC |
|
||||
PROC_ATTR_REQ_MANA_COST |
|
||||
PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_TRIGGERED_CAN_PROC);
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_MANA_COST);
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REDUCE_PROC_60);
|
||||
EXPECT_FALSE(procEntry.AttributesMask & PROC_ATTR_REQ_EXP_OR_HONOR);
|
||||
EXPECT_FALSE(procEntry.AttributesMask & PROC_ATTR_USE_STACKS_FOR_CHARGES);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, CombinedAttributes_AllFlags)
|
||||
{
|
||||
uint32 allFlags =
|
||||
PROC_ATTR_REQ_EXP_OR_HONOR |
|
||||
PROC_ATTR_TRIGGERED_CAN_PROC |
|
||||
PROC_ATTR_REQ_MANA_COST |
|
||||
PROC_ATTR_REQ_SPELLMOD |
|
||||
PROC_ATTR_USE_STACKS_FOR_CHARGES |
|
||||
PROC_ATTR_REDUCE_PROC_60 |
|
||||
PROC_ATTR_CANT_PROC_FROM_ITEM_CAST;
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(allFlags)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_EXP_OR_HONOR);
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_TRIGGERED_CAN_PROC);
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_MANA_COST);
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD);
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_USE_STACKS_FOR_CHARGES);
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REDUCE_PROC_60);
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_CANT_PROC_FROM_ITEM_CAST);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real Spell Attribute Scenarios
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcAttributeTest, Scenario_SealOfCommand_TriggeredCanProc)
|
||||
{
|
||||
// Seal of Command (Paladin) can proc from triggered spells
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_TRIGGERED_CAN_PROC)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_TRIGGERED_CAN_PROC);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, Scenario_ClearCasting_ReqManaCost)
|
||||
{
|
||||
// Clearcasting (Mage/Priest) requires spell to have mana cost
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REQ_MANA_COST)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_MANA_COST);
|
||||
|
||||
// Null spell check - free/costless spells won't trigger
|
||||
EXPECT_FALSE(ProcChanceTestHelper::SpellHasManaCost(nullptr));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, Scenario_MaelstromWeapon_UseStacks)
|
||||
{
|
||||
// Maelstrom Weapon (Shaman) uses stacks
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(53817)
|
||||
.WithStackAmount(5)
|
||||
.Build();
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
.Build();
|
||||
|
||||
// Each proc consumes one stack
|
||||
ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
EXPECT_EQ(aura->GetStackAmount(), 4);
|
||||
|
||||
ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
EXPECT_EQ(aura->GetStackAmount(), 3);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, Scenario_OldLevelScaling_ReduceProc60)
|
||||
{
|
||||
// Some old vanilla/TBC procs have reduced chance at higher levels
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(50.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
// Level 60: Full chance
|
||||
float chanceAt60 = ProcChanceTestHelper::SimulateCalcProcChance(procEntry, 60);
|
||||
EXPECT_NEAR(chanceAt60, 50.0f, 0.01f);
|
||||
|
||||
// Level 75: 50% reduction (15/30)
|
||||
float chanceAt75 = ProcChanceTestHelper::SimulateCalcProcChance(procEntry, 75);
|
||||
EXPECT_NEAR(chanceAt75, 25.0f, 0.5f);
|
||||
|
||||
// Level 90: 100% reduction (30/30), capped at 0
|
||||
float chanceAt90 = ProcChanceTestHelper::SimulateCalcProcChance(procEntry, 90);
|
||||
EXPECT_NEAR(chanceAt90, 0.0f, 0.1f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Attribute Value Validation
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcAttributeTest, AttributeValues_Correct)
|
||||
{
|
||||
// Verify attribute flag values match expected hex values
|
||||
EXPECT_EQ(PROC_ATTR_REQ_EXP_OR_HONOR, 0x0000001u);
|
||||
EXPECT_EQ(PROC_ATTR_TRIGGERED_CAN_PROC, 0x0000002u);
|
||||
EXPECT_EQ(PROC_ATTR_REQ_MANA_COST, 0x0000004u);
|
||||
EXPECT_EQ(PROC_ATTR_REQ_SPELLMOD, 0x0000008u);
|
||||
EXPECT_EQ(PROC_ATTR_USE_STACKS_FOR_CHARGES, 0x0000010u);
|
||||
EXPECT_EQ(PROC_ATTR_REDUCE_PROC_60, 0x0000080u);
|
||||
EXPECT_EQ(PROC_ATTR_CANT_PROC_FROM_ITEM_CAST, 0x0000100u);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcAttributeTest, AttributeFlags_NonOverlapping)
|
||||
{
|
||||
// Verify no two flags share the same bit
|
||||
uint32 flags[] = {
|
||||
PROC_ATTR_REQ_EXP_OR_HONOR,
|
||||
PROC_ATTR_TRIGGERED_CAN_PROC,
|
||||
PROC_ATTR_REQ_MANA_COST,
|
||||
PROC_ATTR_REQ_SPELLMOD,
|
||||
PROC_ATTR_USE_STACKS_FOR_CHARGES,
|
||||
PROC_ATTR_REDUCE_PROC_60,
|
||||
PROC_ATTR_CANT_PROC_FROM_ITEM_CAST
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(flags)/sizeof(flags[0]); ++i)
|
||||
{
|
||||
for (size_t j = i + 1; j < sizeof(flags)/sizeof(flags[0]); ++j)
|
||||
{
|
||||
EXPECT_EQ(flags[i] & flags[j], 0u)
|
||||
<< "Flags at index " << i << " and " << j << " overlap";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
/*
|
||||
* 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 SpellProcChainGuardTest.cpp
|
||||
* @brief Unit tests for proc chain guard and TAKEN auto-trigger logic
|
||||
*
|
||||
* Tests two fixes ported from TrinityCore:
|
||||
*
|
||||
* 1. Proc chain guard (Unit::TriggerAurasProcOnEvent):
|
||||
* - TRIGGERED_DISALLOW_PROC_EVENTS on triggering spell blocks all
|
||||
* cascading procs during the proc event
|
||||
* - SPELL_ATTR3_INSTANT_TARGET_PROCS on individual auras blocks
|
||||
* cascading procs during that specific aura's proc trigger
|
||||
* - Prevents infinite proc loops between reactive damage auras
|
||||
* (e.g. Molten Armor <-> Eye for an Eye)
|
||||
*
|
||||
* 2. TAKEN auto-trigger (SpellMgr::LoadSpellProcs auto-generation):
|
||||
* - TAKEN-proc auras with SPELL_AURA_PROC_TRIGGER_SPELL or
|
||||
* SPELL_AURA_PROC_TRIGGER_DAMAGE automatically get
|
||||
* PROC_ATTR_TRIGGERED_CAN_PROC so they can proc from
|
||||
* triggered spells (e.g. Mage Armor proccing from Judgement)
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
// =============================================================================
|
||||
// TAKEN Auto-Trigger Logic Tests
|
||||
// Simulates SpellMgr.cpp:2033-2049 auto-generation
|
||||
// =============================================================================
|
||||
|
||||
class TakenAutoTriggerTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
};
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, TakenProcTriggerSpell_SetsTriggeredCanProc)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "TAKEN proc + PROC_TRIGGER_SPELL should auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, TakenProcTriggerDamage_SetsTriggeredCanProc)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_SPELL_MAGIC_DMG_CLASS_NEG;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_DAMAGE;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "TAKEN proc + PROC_TRIGGER_DAMAGE should auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, TakenProcOtherAura_DoesNotSetTriggeredCanProc)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_DUMMY;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "TAKEN proc + DUMMY aura should NOT auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, DoneProcTriggerSpell_DoesNotSetTriggeredCanProc)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_DONE_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "DONE-only proc flags should NOT trigger TAKEN auto-add logic";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, NoProcFlags_DoesNotSetTriggeredCanProc)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = 0;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "Zero proc flags should NOT auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, AlwaysTriggeredAura_StaysTrue)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = 0; // No TAKEN flags
|
||||
config.auraName = SPELL_AURA_DUMMY;
|
||||
config.isAlwaysTriggeredAura = true;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "isAlwaysTriggeredAura should keep addTriggerFlag = true";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, TakenDamage_WithProcTriggerSpell_SetsFlag)
|
||||
{
|
||||
// PROC_FLAG_TAKEN_DAMAGE is in TAKEN_HIT_PROC_FLAG_MASK
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_DAMAGE;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "PROC_FLAG_TAKEN_DAMAGE + PROC_TRIGGER_SPELL should set flag";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, TakenPeriodic_WithProcTriggerDamage_SetsFlag)
|
||||
{
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_PERIODIC;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_DAMAGE;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "PROC_FLAG_TAKEN_PERIODIC + PROC_TRIGGER_DAMAGE should set flag";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, MixedDoneAndTaken_WithProcTriggerSpell_SetsFlag)
|
||||
{
|
||||
// Both DONE and TAKEN flags present - TAKEN mask still matches
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_DONE_MELEE_AUTO_ATTACK
|
||||
| PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "Mixed DONE+TAKEN flags should still trigger TAKEN auto-add";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, TakenProcModAura_DoesNotSetTriggeredCanProc)
|
||||
{
|
||||
// SPELL_AURA_ADD_FLAT_MODIFIER is not PROC_TRIGGER_SPELL/DAMAGE
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_ADD_FLAT_MODIFIER;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "TAKEN proc + modifier aura should NOT auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real spell scenarios for TAKEN auto-trigger
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, Scenario_MoltenArmor_TakenAutoTrigger)
|
||||
{
|
||||
// Molten Armor (30482): TAKEN_MELEE_AUTO_ATTACK + PROC_TRIGGER_DAMAGE
|
||||
// Note: Molten Armor has an explicit spell_proc entry so auto-gen
|
||||
// is skipped, but the logic should still apply if it didn't
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK
|
||||
| PROC_FLAG_TAKEN_SPELL_MELEE_DMG_CLASS;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_DAMAGE;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "Molten Armor-like aura should auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, Scenario_Reckoning_TakenAutoTrigger)
|
||||
{
|
||||
// Reckoning (20177): TAKEN_MELEE_AUTO_ATTACK + PROC_TRIGGER_SPELL
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "Reckoning-like aura should auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, Scenario_Redoubt_TakenAutoTrigger)
|
||||
{
|
||||
// Redoubt (20127): TAKEN_MELEE_AUTO_ATTACK + PROC_TRIGGER_SPELL
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig config;
|
||||
config.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
config.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
config.isAlwaysTriggeredAura = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(config))
|
||||
<< "Redoubt-like aura should auto-add TRIGGERED_CAN_PROC";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Integration: TAKEN auto-trigger affects triggered spell filtering
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, AutoGenTriggeredCanProc_AllowsTriggeredSpells)
|
||||
{
|
||||
// Verify that when TAKEN auto-trigger sets addTriggerFlag,
|
||||
// the resulting proc entry with PROC_ATTR_TRIGGERED_CAN_PROC
|
||||
// allows triggered spells through the filter
|
||||
|
||||
// Step 1: Auto-trigger logic says yes
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig autoConfig;
|
||||
autoConfig.procFlags = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
autoConfig.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
ASSERT_TRUE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(autoConfig));
|
||||
|
||||
// Step 2: Build proc entry with the auto-added attribute
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK)
|
||||
.WithAttributesMask(PROC_ATTR_TRIGGERED_CAN_PROC)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Step 3: Verify triggered spells pass through the filter
|
||||
ProcChanceTestHelper::TriggeredSpellConfig trigConfig;
|
||||
trigConfig.isTriggered = true; // e.g. Judgement damage is triggered
|
||||
trigConfig.auraHasCanProcFromProcs = false;
|
||||
trigConfig.spellHasNotAProc = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
trigConfig, procEntry, PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK))
|
||||
<< "Auto-generated TRIGGERED_CAN_PROC should allow triggered spells";
|
||||
}
|
||||
|
||||
TEST_F(TakenAutoTriggerTest, WithoutAutoTrigger_TriggeredSpellsBlocked)
|
||||
{
|
||||
// Without the TAKEN auto-trigger, DONE-only proc auras would block
|
||||
// triggered spells (no TRIGGERED_CAN_PROC set)
|
||||
|
||||
ProcChanceTestHelper::TakenAutoTriggerConfig autoConfig;
|
||||
autoConfig.procFlags = PROC_FLAG_DONE_MELEE_AUTO_ATTACK; // DONE only
|
||||
autoConfig.auraName = SPELL_AURA_PROC_TRIGGER_SPELL;
|
||||
ASSERT_FALSE(ProcChanceTestHelper::ShouldAutoAddTriggeredCanProc(autoConfig));
|
||||
|
||||
// Build proc entry WITHOUT TRIGGERED_CAN_PROC
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithAttributesMask(0) // No TRIGGERED_CAN_PROC
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Triggered spells should be blocked
|
||||
ProcChanceTestHelper::TriggeredSpellConfig trigConfig;
|
||||
trigConfig.isTriggered = true;
|
||||
trigConfig.auraHasCanProcFromProcs = false;
|
||||
trigConfig.spellHasNotAProc = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
trigConfig, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "Without TRIGGERED_CAN_PROC, triggered spells should be blocked";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Proc Chain Guard Tests - simulates Unit::TriggerAurasProcOnEvent
|
||||
// =============================================================================
|
||||
|
||||
class ProcChainGuardTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
|
||||
ProcChainGuardSimulator _sim;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// TRIGGERED_DISALLOW_PROC_EVENTS behavior
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(ProcChainGuardTest, DisallowProcEvents_BlocksAllAuras)
|
||||
{
|
||||
// When triggering spell has TRIGGERED_DISALLOW_PROC_EVENTS,
|
||||
// all auras should fire with CanProc() == false
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = false},
|
||||
{.spellId = 200, .hasInstantTargetProcs = false},
|
||||
{.spellId = 300, .hasInstantTargetProcs = false},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/true, auras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 3u);
|
||||
|
||||
for (auto const& rec : records)
|
||||
{
|
||||
EXPECT_FALSE(rec.canProcDuringTrigger)
|
||||
<< "Aura " << rec.spellId
|
||||
<< " should have CanProc()=false with DISALLOW_PROC_EVENTS";
|
||||
EXPECT_EQ(rec.procDeepDuringTrigger, 1)
|
||||
<< "m_procDeep should be 1 for all auras";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, DisallowProcEvents_CounterBalanced)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/true, auras);
|
||||
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "m_procDeep should return to 0 after function exits";
|
||||
EXPECT_TRUE(_sim.CanProc())
|
||||
<< "CanProc() should be true after function exits";
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, NoDisallowProcEvents_AllAurasCanProc)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = false},
|
||||
{.spellId = 200, .hasInstantTargetProcs = false},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 2u);
|
||||
|
||||
for (auto const& rec : records)
|
||||
{
|
||||
EXPECT_TRUE(rec.canProcDuringTrigger)
|
||||
<< "Aura " << rec.spellId
|
||||
<< " should have CanProc()=true without DISALLOW_PROC_EVENTS";
|
||||
EXPECT_EQ(rec.procDeepDuringTrigger, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// SPELL_ATTR3_INSTANT_TARGET_PROCS per-aura behavior
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(ProcChainGuardTest, InstantTargetProcs_BlocksDuringSpecificAura)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = false},
|
||||
{.spellId = 200, .hasInstantTargetProcs = true}, // This one blocks
|
||||
{.spellId = 300, .hasInstantTargetProcs = false},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 3u);
|
||||
|
||||
// Aura 100: normal, can proc
|
||||
EXPECT_TRUE(records[0].canProcDuringTrigger)
|
||||
<< "First aura (no INSTANT_TARGET_PROCS) should allow procs";
|
||||
EXPECT_EQ(records[0].procDeepDuringTrigger, 0);
|
||||
|
||||
// Aura 200: has INSTANT_TARGET_PROCS, blocked
|
||||
EXPECT_FALSE(records[1].canProcDuringTrigger)
|
||||
<< "Aura with INSTANT_TARGET_PROCS should block procs during its trigger";
|
||||
EXPECT_EQ(records[1].procDeepDuringTrigger, 1);
|
||||
|
||||
// Aura 300: normal again, can proc (counter was decremented)
|
||||
EXPECT_TRUE(records[2].canProcDuringTrigger)
|
||||
<< "Next aura after INSTANT_TARGET_PROCS should allow procs again";
|
||||
EXPECT_EQ(records[2].procDeepDuringTrigger, 0);
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, InstantTargetProcs_CounterBalanced)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = true},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "m_procDeep should return to 0 after INSTANT_TARGET_PROCS aura";
|
||||
EXPECT_TRUE(_sim.CanProc());
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, MultipleInstantTargetProcs_EachIndependent)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = true},
|
||||
{.spellId = 200, .hasInstantTargetProcs = true},
|
||||
{.spellId = 300, .hasInstantTargetProcs = true},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 3u);
|
||||
|
||||
// Each aura should independently block during its own trigger
|
||||
for (auto const& rec : records)
|
||||
{
|
||||
EXPECT_FALSE(rec.canProcDuringTrigger)
|
||||
<< "Aura " << rec.spellId
|
||||
<< " with INSTANT_TARGET_PROCS should block during trigger";
|
||||
EXPECT_EQ(rec.procDeepDuringTrigger, 1)
|
||||
<< "Each aura should increment to exactly 1 (not accumulate)";
|
||||
}
|
||||
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "Counter should be balanced after all auras processed";
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Combined: DISALLOW_PROC_EVENTS + INSTANT_TARGET_PROCS
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(ProcChainGuardTest, Combined_DisallowAndInstantTarget_StackCorrectly)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = false},
|
||||
{.spellId = 200, .hasInstantTargetProcs = true},
|
||||
{.spellId = 300, .hasInstantTargetProcs = false},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/true, auras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 3u);
|
||||
|
||||
// Aura 100: disableProcs active, procDeep=1
|
||||
EXPECT_FALSE(records[0].canProcDuringTrigger);
|
||||
EXPECT_EQ(records[0].procDeepDuringTrigger, 1);
|
||||
|
||||
// Aura 200: disableProcs + INSTANT_TARGET_PROCS, procDeep=2
|
||||
EXPECT_FALSE(records[1].canProcDuringTrigger);
|
||||
EXPECT_EQ(records[1].procDeepDuringTrigger, 2)
|
||||
<< "DISALLOW_PROC_EVENTS + INSTANT_TARGET_PROCS should stack to 2";
|
||||
|
||||
// Aura 300: back to just disableProcs, procDeep=1
|
||||
EXPECT_FALSE(records[2].canProcDuringTrigger);
|
||||
EXPECT_EQ(records[2].procDeepDuringTrigger, 1);
|
||||
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "Counter should be balanced after combined scenario";
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Removed aura handling
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(ProcChainGuardTest, RemovedAura_SkippedInLoop)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = false, .isRemoved = false},
|
||||
{.spellId = 200, .hasInstantTargetProcs = true, .isRemoved = true},
|
||||
{.spellId = 300, .hasInstantTargetProcs = false, .isRemoved = false},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 2u)
|
||||
<< "Removed aura should be skipped";
|
||||
|
||||
EXPECT_EQ(records[0].spellId, 100u);
|
||||
EXPECT_EQ(records[1].spellId, 300u);
|
||||
|
||||
// The INSTANT_TARGET_PROCS aura was removed, so it shouldn't affect
|
||||
// the counter for the next aura
|
||||
EXPECT_TRUE(records[1].canProcDuringTrigger);
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, RemovedAuraWithInstantProcs_DoesNotAffectCounter)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras = {
|
||||
{.spellId = 100, .hasInstantTargetProcs = true, .isRemoved = true},
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
EXPECT_EQ(_sim.GetRecords().size(), 0u);
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "Removed aura should not touch the proc deep counter";
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Empty container
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST_F(ProcChainGuardTest, EmptyContainer_NoEffect)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras;
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, auras);
|
||||
|
||||
EXPECT_EQ(_sim.GetRecords().size(), 0u);
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0);
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, EmptyContainer_WithDisallowProc_StillBalanced)
|
||||
{
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> auras;
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/true, auras);
|
||||
|
||||
EXPECT_EQ(_sim.GetRecords().size(), 0u);
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "Even with disableProcs and empty container, counter should balance";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real spell scenarios for proc chain guard
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ProcChainGuardTest, Scenario_MoltenArmorVsEyeForAnEye)
|
||||
{
|
||||
// Molten Armor (43046) has SPELL_ATTR3_INSTANT_TARGET_PROCS
|
||||
// When Molten Armor deals fire damage to an attacker, that damage
|
||||
// should not trigger the attacker's reactive procs (Eye for an Eye)
|
||||
// back on the mage, preventing infinite ping-pong
|
||||
|
||||
// Mage's perspective: paladin hits mage, mage's auras proc
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> mageAuras = {
|
||||
{.spellId = 43046, .hasInstantTargetProcs = true}, // Molten Armor
|
||||
};
|
||||
|
||||
// The paladin's melee hit is NOT TRIGGERED_DISALLOW_PROC_EVENTS
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, mageAuras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 1u);
|
||||
|
||||
// During Molten Armor's proc trigger, CanProc() is false
|
||||
// This means the fire damage it deals cannot trigger further procs
|
||||
EXPECT_FALSE(records[0].canProcDuringTrigger)
|
||||
<< "Molten Armor proc should block cascading procs (prevents EfaE loop)";
|
||||
|
||||
EXPECT_EQ(_sim.GetProcDeep(), 0)
|
||||
<< "Counter balanced after Molten Armor scenario";
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, Scenario_SealOfRighteousness_TriggeredDamage)
|
||||
{
|
||||
// SoR bonus damage is triggered with TRIGGERED_DISALLOW_PROC_EVENTS
|
||||
// It should not trigger the target's reactive damage auras back
|
||||
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> targetAuras = {
|
||||
{.spellId = 43046, .hasInstantTargetProcs = true}, // Molten Armor
|
||||
{.spellId = 12345, .hasInstantTargetProcs = false}, // Some other aura
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/true, targetAuras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 2u);
|
||||
|
||||
// All auras should see CanProc() = false
|
||||
for (auto const& rec : records)
|
||||
{
|
||||
EXPECT_FALSE(rec.canProcDuringTrigger)
|
||||
<< "All target auras should be blocked when SoR damage"
|
||||
<< " has DISALLOW_PROC_EVENTS";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ProcChainGuardTest, Scenario_NormalMeleeHit_AurasCanProc)
|
||||
{
|
||||
// A normal melee swing should allow all auras to proc normally
|
||||
// (no DISALLOW_PROC_EVENTS, no INSTANT_TARGET_PROCS)
|
||||
|
||||
std::vector<ProcChainGuardSimulator::AuraConfig> targetAuras = {
|
||||
{.spellId = 20177, .hasInstantTargetProcs = false}, // Reckoning
|
||||
{.spellId = 20127, .hasInstantTargetProcs = false}, // Redoubt
|
||||
{.spellId = 16958, .hasInstantTargetProcs = false}, // Blood Craze
|
||||
};
|
||||
|
||||
_sim.SimulateTriggerAurasProc(/*disallowProcEvents=*/false, targetAuras);
|
||||
|
||||
auto const& records = _sim.GetRecords();
|
||||
ASSERT_EQ(records.size(), 3u);
|
||||
|
||||
for (auto const& rec : records)
|
||||
{
|
||||
EXPECT_TRUE(rec.canProcDuringTrigger)
|
||||
<< "Aura " << rec.spellId
|
||||
<< " should proc normally from a regular melee hit";
|
||||
EXPECT_EQ(rec.procDeepDuringTrigger, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
* 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 SpellProcChanceTest.cpp
|
||||
* @brief Unit tests for proc chance calculations
|
||||
*
|
||||
* Tests CalcProcChance() behavior including:
|
||||
* - Base chance from SpellProcEntry
|
||||
* - PPM override when DamageInfo is present
|
||||
* - Chance modifiers (SPELLMOD_CHANCE_OF_SUCCESS)
|
||||
* - Level 60+ reduction (PROC_ATTR_REDUCE_PROC_60)
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
// =============================================================================
|
||||
// Base Chance Tests
|
||||
// =============================================================================
|
||||
|
||||
class SpellProcChanceTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
};
|
||||
|
||||
TEST_F(SpellProcChanceTest, BaseChance_UsedWhenNoPPM)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(25.0f)
|
||||
.WithProcsPerMinute(0.0f)
|
||||
.Build();
|
||||
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(procEntry);
|
||||
EXPECT_NEAR(result, 25.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, BaseChance_100Percent)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(procEntry);
|
||||
EXPECT_NEAR(result, 100.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, BaseChance_Zero)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(0.0f)
|
||||
.Build();
|
||||
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(procEntry);
|
||||
EXPECT_NEAR(result, 0.0f, 0.01f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PPM Override Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcChanceTest, PPM_OverridesBaseChance_WithDamageInfo)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(50.0f) // This should be ignored
|
||||
.WithProcsPerMinute(6.0f)
|
||||
.Build();
|
||||
|
||||
// With DamageInfo, PPM takes precedence
|
||||
// 2500ms * 6 PPM / 600 = 25%
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 80, 2500, 0.0f, 0.0f, true);
|
||||
EXPECT_NEAR(result, 25.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, PPM_NotApplied_WithoutDamageOrHealInfo)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(50.0f)
|
||||
.WithProcsPerMinute(6.0f)
|
||||
.Build();
|
||||
|
||||
// Without DamageInfo or HealInfo, base chance is used
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 80, 2500, 0.0f, 0.0f, false, false);
|
||||
EXPECT_NEAR(result, 50.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, PPM_Applied_WithHealInfo)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(0.0f)
|
||||
.WithProcsPerMinute(3.5f)
|
||||
.Build();
|
||||
|
||||
// With HealInfo (no DamageInfo), PPM should still calculate
|
||||
// 3000ms cast time * 3.5 PPM / 600 = 17.5%
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 80, 3000, 0.0f, 0.0f, false, true);
|
||||
EXPECT_NEAR(result, 17.5f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, PPM_HealInfo_ZeroBaseChance_WouldBeZeroWithoutFix)
|
||||
{
|
||||
// Reproduces the Omen of Clarity healing bug:
|
||||
// PPM=3.5, Chance=0, and only HealInfo present (no DamageInfo)
|
||||
// Without the fix, chance would be 0% because PPM branch was skipped
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(0.0f)
|
||||
.WithProcsPerMinute(3.5f)
|
||||
.Build();
|
||||
|
||||
// Instant cast uses 1500ms minimum
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 80, 1500, 0.0f, 0.0f, false, true);
|
||||
EXPECT_NEAR(result, 8.75f, 0.01f);
|
||||
EXPECT_GT(result, 0.0f) << "PPM with HealInfo must produce non-zero chance";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, PPM_WithWeaponSpeedVariation)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcsPerMinute(6.0f)
|
||||
.Build();
|
||||
|
||||
// Fast weapon: 1400ms
|
||||
float fastResult = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 80, 1400, 0.0f, 0.0f, true);
|
||||
EXPECT_NEAR(fastResult, 14.0f, 0.01f);
|
||||
|
||||
// Slow weapon: 3300ms
|
||||
float slowResult = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 80, 3300, 0.0f, 0.0f, true);
|
||||
EXPECT_NEAR(slowResult, 33.0f, 0.01f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Chance Modifier Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcChanceTest, ChanceModifier_PositiveModifier)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(20.0f)
|
||||
.Build();
|
||||
|
||||
// +10% modifier
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 80, 2500, 10.0f, 0.0f, false);
|
||||
EXPECT_NEAR(result, 30.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, ChanceModifier_NegativeModifier)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.Build();
|
||||
|
||||
// -10% modifier
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 80, 2500, -10.0f, 0.0f, false);
|
||||
EXPECT_NEAR(result, 20.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, ChanceModifier_AppliedAfterPPM)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcsPerMinute(6.0f)
|
||||
.Build();
|
||||
|
||||
// PPM gives 25%, +5% modifier = 30%
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 80, 2500, 5.0f, 0.0f, true);
|
||||
EXPECT_NEAR(result, 30.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, PPMModifier_IncreasesEffectivePPM)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcsPerMinute(6.0f)
|
||||
.Build();
|
||||
|
||||
// 6 PPM + 2 PPM modifier = 8 effective PPM
|
||||
// 2500 * 8 / 600 = 33.33%
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 80, 2500, 0.0f, 2.0f, true);
|
||||
EXPECT_NEAR(result, 33.33f, 0.01f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Level 60+ Reduction Tests (PROC_ATTR_REDUCE_PROC_60)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcChanceTest, Level60Reduction_NoReductionAtLevel60)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 60, 2500, 0.0f, 0.0f, false);
|
||||
EXPECT_NEAR(result, 30.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, Level60Reduction_NoReductionBelowLevel60)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 50, 2500, 0.0f, 0.0f, false);
|
||||
EXPECT_NEAR(result, 30.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, Level60Reduction_ReductionAtLevel70)
|
||||
{
|
||||
// Level 70 = 10 levels above 60
|
||||
// Reduction = 10/30 = 33.33%
|
||||
// 30% * (1 - 0.333) = 20%
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 70, 2500, 0.0f, 0.0f, false);
|
||||
EXPECT_NEAR(result, 20.0f, 0.5f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, Level60Reduction_ReductionAtLevel80)
|
||||
{
|
||||
// Level 80 = 20 levels above 60
|
||||
// Reduction = 20/30 = 66.67%
|
||||
// 30% * (1 - 0.667) = 10%
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 80, 2500, 0.0f, 0.0f, false);
|
||||
EXPECT_NEAR(result, 10.0f, 0.5f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, Level60Reduction_MinimumAtLevel90)
|
||||
{
|
||||
// Level 90 = 30 levels above 60
|
||||
// Reduction = 30/30 = 100%
|
||||
// 30% * (1 - 1.0) = 0%
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 90, 2500, 0.0f, 0.0f, false);
|
||||
EXPECT_NEAR(result, 0.0f, 0.1f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, Level60Reduction_NotAppliedWithoutAttribute)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(0) // No PROC_ATTR_REDUCE_PROC_60
|
||||
.Build();
|
||||
|
||||
// At level 80, without the attribute, no reduction should occur
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 80, 2500, 0.0f, 0.0f, false);
|
||||
EXPECT_NEAR(result, 30.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChanceTest, Level60Reduction_AppliedAfterPPM)
|
||||
{
|
||||
// PPM calculation gives 25%, then level reduction applied
|
||||
// Level 80 = 20 levels above 60, reduction = 66.67%
|
||||
// 25% * (1 - 0.667) = 8.33%
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcsPerMinute(6.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 80, 2500, 0.0f, 0.0f, true);
|
||||
EXPECT_NEAR(result, 8.33f, 0.5f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper Function Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcChanceTest, ApplyLevel60Reduction_DirectTest)
|
||||
{
|
||||
// Level 60: no reduction
|
||||
EXPECT_NEAR(ProcChanceTestHelper::ApplyLevel60Reduction(30.0f, 60), 30.0f, 0.01f);
|
||||
|
||||
// Level 70: 33.33% reduction
|
||||
EXPECT_NEAR(ProcChanceTestHelper::ApplyLevel60Reduction(30.0f, 70), 20.0f, 0.5f);
|
||||
|
||||
// Level 80: 66.67% reduction
|
||||
EXPECT_NEAR(ProcChanceTestHelper::ApplyLevel60Reduction(30.0f, 80), 10.0f, 0.5f);
|
||||
|
||||
// Level 90: 100% reduction
|
||||
EXPECT_NEAR(ProcChanceTestHelper::ApplyLevel60Reduction(30.0f, 90), 0.0f, 0.1f);
|
||||
|
||||
// Level 100: capped at 0% (no negative chance)
|
||||
EXPECT_GE(ProcChanceTestHelper::ApplyLevel60Reduction(30.0f, 100), 0.0f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Combined Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcChanceTest, Combined_PPM_ChanceModifier_LevelReduction)
|
||||
{
|
||||
// PPM: 6 at 2500ms = 25%
|
||||
// Chance modifier: +5% = 30%
|
||||
// Level 70 reduction: 30% * (1 - 0.333) = 20%
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcsPerMinute(6.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
float result = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
procEntry, 70, 2500, 5.0f, 0.0f, true);
|
||||
EXPECT_NEAR(result, 20.0f, 1.0f);
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
/*
|
||||
* 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 SpellProcChargeTest.cpp
|
||||
* @brief Unit tests for proc charge and stack consumption
|
||||
*
|
||||
* Tests ConsumeProcCharges() behavior including:
|
||||
* - Charge decrement on proc
|
||||
* - Aura removal when charges exhausted
|
||||
* - PROC_ATTR_USE_STACKS_FOR_CHARGES stack decrement
|
||||
* - Multiple charge consumption scenarios
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "AuraStub.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
class SpellProcChargeTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Basic Charge Consumption Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcChargeTest, ChargeDecrement_SingleCharge)
|
||||
{
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(12345)
|
||||
.WithCharges(1)
|
||||
.Build();
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Consume the single charge
|
||||
bool removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
|
||||
EXPECT_EQ(aura->GetCharges(), 0);
|
||||
EXPECT_TRUE(removed);
|
||||
EXPECT_TRUE(aura->IsRemoved());
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChargeTest, ChargeDecrement_MultipleCharges)
|
||||
{
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(12345)
|
||||
.WithCharges(5)
|
||||
.Build();
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// First consumption
|
||||
bool removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
EXPECT_EQ(aura->GetCharges(), 4);
|
||||
EXPECT_FALSE(removed);
|
||||
EXPECT_FALSE(aura->IsRemoved());
|
||||
|
||||
// Second consumption
|
||||
removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
EXPECT_EQ(aura->GetCharges(), 3);
|
||||
EXPECT_FALSE(removed);
|
||||
|
||||
// Third consumption
|
||||
removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
EXPECT_EQ(aura->GetCharges(), 2);
|
||||
EXPECT_FALSE(removed);
|
||||
|
||||
// Fourth consumption
|
||||
removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
EXPECT_EQ(aura->GetCharges(), 1);
|
||||
EXPECT_FALSE(removed);
|
||||
|
||||
// Final consumption - should remove aura
|
||||
removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
EXPECT_EQ(aura->GetCharges(), 0);
|
||||
EXPECT_TRUE(removed);
|
||||
EXPECT_TRUE(aura->IsRemoved());
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChargeTest, NoCharges_NoConsumption)
|
||||
{
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(12345)
|
||||
.WithCharges(0)
|
||||
.Build();
|
||||
|
||||
aura->SetUsingCharges(false);
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
bool removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
|
||||
EXPECT_EQ(aura->GetCharges(), 0);
|
||||
EXPECT_FALSE(removed);
|
||||
EXPECT_FALSE(aura->IsRemoved());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROC_ATTR_USE_STACKS_FOR_CHARGES Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcChargeTest, UseStacksForCharges_SingleStack)
|
||||
{
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(12345)
|
||||
.WithStackAmount(1)
|
||||
.Build();
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
.Build();
|
||||
|
||||
bool removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
|
||||
EXPECT_EQ(aura->GetStackAmount(), 0);
|
||||
EXPECT_TRUE(removed);
|
||||
EXPECT_TRUE(aura->IsRemoved());
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChargeTest, UseStacksForCharges_MultipleStacks)
|
||||
{
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(12345)
|
||||
.WithStackAmount(5)
|
||||
.Build();
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
.Build();
|
||||
|
||||
// First consumption - 5 -> 4
|
||||
bool removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
EXPECT_EQ(aura->GetStackAmount(), 4);
|
||||
EXPECT_FALSE(removed);
|
||||
EXPECT_FALSE(aura->IsRemoved());
|
||||
|
||||
// Second consumption - 4 -> 3
|
||||
removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
EXPECT_EQ(aura->GetStackAmount(), 3);
|
||||
EXPECT_FALSE(removed);
|
||||
|
||||
// Consume remaining stacks
|
||||
ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry); // 3 -> 2
|
||||
ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry); // 2 -> 1
|
||||
|
||||
// Final consumption - should remove aura
|
||||
removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
EXPECT_EQ(aura->GetStackAmount(), 0);
|
||||
EXPECT_TRUE(removed);
|
||||
EXPECT_TRUE(aura->IsRemoved());
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChargeTest, UseStacksForCharges_IgnoresCharges)
|
||||
{
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(12345)
|
||||
.WithCharges(10) // Has charges
|
||||
.WithStackAmount(2)
|
||||
.Build();
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
.Build();
|
||||
|
||||
// Should decrement stacks, not charges
|
||||
bool removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
|
||||
EXPECT_EQ(aura->GetStackAmount(), 1);
|
||||
EXPECT_EQ(aura->GetCharges(), 10); // Charges unchanged
|
||||
EXPECT_FALSE(removed);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real Spell Scenario Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcChargeTest, Scenario_HotStreak_3Charges)
|
||||
{
|
||||
// Hot Streak (Fire Mage) - 3 charges, consumed on each instant Pyroblast
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(48108) // Hot Streak
|
||||
.WithCharges(3)
|
||||
.Build();
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// First Pyroblast
|
||||
EXPECT_FALSE(ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry));
|
||||
EXPECT_EQ(aura->GetCharges(), 2);
|
||||
|
||||
// Second Pyroblast
|
||||
EXPECT_FALSE(ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry));
|
||||
EXPECT_EQ(aura->GetCharges(), 1);
|
||||
|
||||
// Third Pyroblast - aura removed
|
||||
EXPECT_TRUE(ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry));
|
||||
EXPECT_EQ(aura->GetCharges(), 0);
|
||||
EXPECT_TRUE(aura->IsRemoved());
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChargeTest, Scenario_BladeBarrier_5Stacks)
|
||||
{
|
||||
// Blade Barrier (Death Knight) - 5 stacks, consumed over time
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(55226) // Blade Barrier
|
||||
.WithStackAmount(5)
|
||||
.Build();
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
.Build();
|
||||
|
||||
// Simulate stacks being consumed
|
||||
for (int i = 5; i > 1; --i)
|
||||
{
|
||||
EXPECT_EQ(aura->GetStackAmount(), i);
|
||||
EXPECT_FALSE(ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry));
|
||||
}
|
||||
|
||||
// Last stack removal
|
||||
EXPECT_EQ(aura->GetStackAmount(), 1);
|
||||
EXPECT_TRUE(ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry));
|
||||
EXPECT_TRUE(aura->IsRemoved());
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChargeTest, Scenario_Maelstrom_5Stacks)
|
||||
{
|
||||
// Maelstrom Weapon (Enhancement Shaman) - 5 stacks
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(53817) // Maelstrom Weapon
|
||||
.WithStackAmount(5)
|
||||
.Build();
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
.Build();
|
||||
|
||||
// At 5 stacks, cast instant Lightning Bolt consumes all stacks
|
||||
// Simulate consuming all 5 stacks at once
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
}
|
||||
|
||||
EXPECT_EQ(aura->GetStackAmount(), 0);
|
||||
EXPECT_TRUE(aura->IsRemoved());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Edge Case Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcChargeTest, NullAura_SafeHandling)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Should not crash
|
||||
bool removed = ProcChanceTestHelper::SimulateConsumeProcCharges(nullptr, procEntry);
|
||||
EXPECT_FALSE(removed);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChargeTest, ZeroStacks_WithUseStacksAttribute)
|
||||
{
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(12345)
|
||||
.WithStackAmount(0)
|
||||
.Build();
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
.Build();
|
||||
|
||||
// Should handle gracefully and remove aura
|
||||
bool removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
EXPECT_TRUE(removed);
|
||||
EXPECT_TRUE(aura->IsRemoved());
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChargeTest, HighChargeCount)
|
||||
{
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(12345)
|
||||
.WithCharges(255) // Max uint8
|
||||
.Build();
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Consume one charge from max
|
||||
bool removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), procEntry);
|
||||
EXPECT_EQ(aura->GetCharges(), 254);
|
||||
EXPECT_FALSE(removed);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ProcTestScenario Integration Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcChargeTest, ProcTestScenario_ChargeConsumption)
|
||||
{
|
||||
ProcTestScenario scenario;
|
||||
scenario.WithAura(12345, 3); // 3 charges
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// First proc - consumes charge
|
||||
EXPECT_TRUE(scenario.SimulateProc(procEntry));
|
||||
EXPECT_EQ(scenario.GetAura()->GetCharges(), 2);
|
||||
|
||||
// Second proc - consumes charge
|
||||
EXPECT_TRUE(scenario.SimulateProc(procEntry));
|
||||
EXPECT_EQ(scenario.GetAura()->GetCharges(), 1);
|
||||
|
||||
// Third proc - consumes last charge and removes aura
|
||||
EXPECT_TRUE(scenario.SimulateProc(procEntry));
|
||||
EXPECT_EQ(scenario.GetAura()->GetCharges(), 0);
|
||||
EXPECT_TRUE(scenario.GetAura()->IsRemoved());
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChargeTest, ProcTestScenario_StackConsumption)
|
||||
{
|
||||
ProcTestScenario scenario;
|
||||
scenario.WithAura(12345, 0, 3); // 3 stacks
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
.Build();
|
||||
|
||||
// First proc - consumes stack
|
||||
EXPECT_TRUE(scenario.SimulateProc(procEntry));
|
||||
EXPECT_EQ(scenario.GetAura()->GetStackAmount(), 2);
|
||||
|
||||
// Second proc - consumes stack
|
||||
EXPECT_TRUE(scenario.SimulateProc(procEntry));
|
||||
EXPECT_EQ(scenario.GetAura()->GetStackAmount(), 1);
|
||||
|
||||
// Third proc - consumes last stack and removes aura
|
||||
EXPECT_TRUE(scenario.SimulateProc(procEntry));
|
||||
EXPECT_EQ(scenario.GetAura()->GetStackAmount(), 0);
|
||||
EXPECT_TRUE(scenario.GetAura()->IsRemoved());
|
||||
}
|
||||
|
||||
TEST_F(SpellProcChargeTest, ProcTestScenario_ChargesWithCooldown)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
ProcTestScenario scenario;
|
||||
scenario.WithAura(12345, 3); // 3 charges
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithCooldown(1000ms) // 1 second cooldown
|
||||
.Build();
|
||||
|
||||
// First proc at t=0 - should work
|
||||
EXPECT_TRUE(scenario.SimulateProc(procEntry));
|
||||
EXPECT_EQ(scenario.GetAura()->GetCharges(), 2);
|
||||
|
||||
// Immediate second proc - blocked by cooldown
|
||||
EXPECT_FALSE(scenario.SimulateProc(procEntry));
|
||||
EXPECT_EQ(scenario.GetAura()->GetCharges(), 2); // No charge consumed
|
||||
|
||||
// Wait for cooldown
|
||||
scenario.AdvanceTime(1100ms);
|
||||
|
||||
// Third proc - should work
|
||||
EXPECT_TRUE(scenario.SimulateProc(procEntry));
|
||||
EXPECT_EQ(scenario.GetAura()->GetCharges(), 1);
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
/*
|
||||
* 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 SpellProcConditionsTest.cpp
|
||||
* @brief Unit tests for conditions system integration in proc system
|
||||
*
|
||||
* Tests the logic from SpellAuras.cpp:2232-2236:
|
||||
* - CONDITION_SOURCE_TYPE_SPELL_PROC (24) lookup
|
||||
* - Condition met allows proc
|
||||
* - Condition not met blocks proc
|
||||
* - Empty conditions allow proc
|
||||
* - Multiple conditions (AND logic within ElseGroup)
|
||||
* - ElseGroup OR logic
|
||||
*
|
||||
* ============================================================================
|
||||
* TEST DESIGN: Configuration-Based Testing
|
||||
* ============================================================================
|
||||
*
|
||||
* These tests use ConditionsConfig structs to simulate the result of
|
||||
* condition evaluation without requiring actual ConditionMgr queries.
|
||||
* Each test configures:
|
||||
* - sourceType: The condition source type (24 = CONDITION_SOURCE_TYPE_SPELL_PROC)
|
||||
* - hasConditions: Whether any conditions are registered for this spell
|
||||
* - conditionsMet: The result of ConditionMgr::IsObjectMeetToConditions()
|
||||
*
|
||||
* The actual condition types (CONDITION_AURA, CONDITION_HP_PCT, etc.) are
|
||||
* not evaluated here - we test the proc system's response to condition
|
||||
* evaluation results. Individual condition types are tested in the
|
||||
* conditions system unit tests.
|
||||
*
|
||||
* No GTEST_SKIP() is used in this file - all tests run with their configured
|
||||
* scenarios, testing both positive and negative cases explicitly.
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
class SpellProcConditionsTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Basic Condition Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcConditionsTest, NoConditions_AllowsProc)
|
||||
{
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = false; // No conditions registered
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(config))
|
||||
<< "No conditions should allow proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, ConditionsMet_AllowsProc)
|
||||
{
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = true;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(config))
|
||||
<< "Conditions met should allow proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, ConditionsNotMet_BlocksProc)
|
||||
{
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToConditions(config))
|
||||
<< "Conditions not met should block proc";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Source Type Tests - CONDITION_SOURCE_TYPE_SPELL_PROC = 24
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcConditionsTest, SourceType_SpellProc)
|
||||
{
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.sourceType = 24; // CONDITION_SOURCE_TYPE_SPELL_PROC
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = true;
|
||||
|
||||
EXPECT_EQ(config.sourceType, 24u)
|
||||
<< "Source type should be CONDITION_SOURCE_TYPE_SPELL_PROC (24)";
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(config));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Multiple Conditions Scenarios (AND Logic)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcConditionsTest, MultipleConditions_AllMet_AllowsProc)
|
||||
{
|
||||
// Simulating multiple conditions in same ElseGroup (AND)
|
||||
// In reality, ConditionMgr evaluates all - we just test the result
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = true; // All conditions passed
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(config))
|
||||
<< "All conditions met (AND) should allow proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, MultipleConditions_OneFails_BlocksProc)
|
||||
{
|
||||
// One condition in the group fails
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = false; // At least one condition failed
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToConditions(config))
|
||||
<< "One failed condition (AND) should block proc";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ElseGroup OR Logic Scenarios
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcConditionsTest, ElseGroup_OneGroupPasses_AllowsProc)
|
||||
{
|
||||
// ElseGroup logic: any group passing means conditions met
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = true; // At least one group passed
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(config))
|
||||
<< "At least one ElseGroup passing should allow proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, ElseGroup_AllGroupsFail_BlocksProc)
|
||||
{
|
||||
// All ElseGroups fail
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = false; // No groups passed
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToConditions(config))
|
||||
<< "All ElseGroups failing should block proc";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real Spell Scenarios
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcConditionsTest, Scenario_ProcOnlyInCombat)
|
||||
{
|
||||
// Condition: Player must be in combat
|
||||
ProcChanceTestHelper::ConditionsConfig inCombat;
|
||||
inCombat.hasConditions = true;
|
||||
inCombat.conditionsMet = true; // In combat
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(inCombat))
|
||||
<< "Proc should work when in combat";
|
||||
|
||||
ProcChanceTestHelper::ConditionsConfig outOfCombat;
|
||||
outOfCombat.hasConditions = true;
|
||||
outOfCombat.conditionsMet = false; // Out of combat
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToConditions(outOfCombat))
|
||||
<< "Proc should be blocked when out of combat";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, Scenario_ProcOnlyVsUndead)
|
||||
{
|
||||
// Condition: Target must be undead creature type
|
||||
ProcChanceTestHelper::ConditionsConfig vsUndead;
|
||||
vsUndead.hasConditions = true;
|
||||
vsUndead.conditionsMet = true; // Target is undead
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(vsUndead))
|
||||
<< "Proc should work against undead";
|
||||
|
||||
ProcChanceTestHelper::ConditionsConfig vsHumanoid;
|
||||
vsHumanoid.hasConditions = true;
|
||||
vsHumanoid.conditionsMet = false; // Target is humanoid
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToConditions(vsHumanoid))
|
||||
<< "Proc should be blocked against non-undead";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, Scenario_ProcRequiresAura)
|
||||
{
|
||||
// Condition: Actor must have specific aura
|
||||
ProcChanceTestHelper::ConditionsConfig hasAura;
|
||||
hasAura.hasConditions = true;
|
||||
hasAura.conditionsMet = true;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(hasAura))
|
||||
<< "Proc should work when required aura is present";
|
||||
|
||||
ProcChanceTestHelper::ConditionsConfig noAura;
|
||||
noAura.hasConditions = true;
|
||||
noAura.conditionsMet = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToConditions(noAura))
|
||||
<< "Proc should be blocked when required aura is missing";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, Scenario_ProcRequiresHealthBelow)
|
||||
{
|
||||
// Condition: Actor health must be below threshold
|
||||
ProcChanceTestHelper::ConditionsConfig lowHealth;
|
||||
lowHealth.hasConditions = true;
|
||||
lowHealth.conditionsMet = true; // Health below 35%
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(lowHealth))
|
||||
<< "Proc should work when health is below threshold";
|
||||
|
||||
ProcChanceTestHelper::ConditionsConfig highHealth;
|
||||
highHealth.hasConditions = true;
|
||||
highHealth.conditionsMet = false; // Health above 35%
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToConditions(highHealth))
|
||||
<< "Proc should be blocked when health is above threshold";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, Scenario_ProcInAreaOnly)
|
||||
{
|
||||
// Condition: Must be in specific zone/area
|
||||
ProcChanceTestHelper::ConditionsConfig inArea;
|
||||
inArea.hasConditions = true;
|
||||
inArea.conditionsMet = true;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(inArea))
|
||||
<< "Proc should work when in required area";
|
||||
|
||||
ProcChanceTestHelper::ConditionsConfig notInArea;
|
||||
notInArea.hasConditions = true;
|
||||
notInArea.conditionsMet = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToConditions(notInArea))
|
||||
<< "Proc should be blocked when not in required area";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Condition Type Scenarios (Common CONDITION_* types used with procs)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcConditionsTest, ConditionType_Aura)
|
||||
{
|
||||
// CONDITION_AURA = 1
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = true;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(config));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, ConditionType_Item)
|
||||
{
|
||||
// CONDITION_ITEM = 2
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = true;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(config));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, ConditionType_ItemEquipped)
|
||||
{
|
||||
// CONDITION_ITEM_EQUIPPED = 3
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = false; // Required item not equipped
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToConditions(config))
|
||||
<< "Proc blocked when required item not equipped";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, ConditionType_QuestRewarded)
|
||||
{
|
||||
// CONDITION_QUESTREWARDED = 8
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = true; // Required quest completed
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(config))
|
||||
<< "Proc allowed when quest completed";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, ConditionType_CreatureType)
|
||||
{
|
||||
// CONDITION_CREATURE_TYPE = 18
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = false; // Wrong creature type
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToConditions(config))
|
||||
<< "Proc blocked when creature type doesn't match";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, ConditionType_HPVal)
|
||||
{
|
||||
// CONDITION_HP_VAL = 23
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = true; // HP threshold met
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(config));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, ConditionType_HPPct)
|
||||
{
|
||||
// CONDITION_HP_PCT = 25
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = false; // HP percent threshold not met
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToConditions(config));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, ConditionType_InCombat)
|
||||
{
|
||||
// CONDITION_IN_COMBAT = 36
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = true; // In combat
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(config));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Edge Cases
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcConditionsTest, EdgeCase_EmptyConditionList)
|
||||
{
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = false;
|
||||
config.conditionsMet = false; // Doesn't matter when no conditions
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(config))
|
||||
<< "Empty condition list should allow proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, EdgeCase_ConditionsButAlwaysTrue)
|
||||
{
|
||||
// Conditions exist but are always satisfied (e.g., always-true condition)
|
||||
ProcChanceTestHelper::ConditionsConfig config;
|
||||
config.hasConditions = true;
|
||||
config.conditionsMet = true;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(config));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcConditionsTest, EdgeCase_MultipleSourceTypes)
|
||||
{
|
||||
// Different source types shouldn't interfere
|
||||
// Each spell proc has its own conditions by spell ID
|
||||
ProcChanceTestHelper::ConditionsConfig spell1;
|
||||
spell1.sourceType = 24;
|
||||
spell1.hasConditions = true;
|
||||
spell1.conditionsMet = true;
|
||||
|
||||
ProcChanceTestHelper::ConditionsConfig spell2;
|
||||
spell2.sourceType = 24;
|
||||
spell2.hasConditions = true;
|
||||
spell2.conditionsMet = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToConditions(spell1));
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToConditions(spell2));
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* 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 SpellProcCooldownTest.cpp
|
||||
* @brief Unit tests for proc internal cooldown system
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "AuraStub.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
class SpellProcCooldownTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_now = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
std::chrono::steady_clock::time_point _now;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Basic Cooldown Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcCooldownTest, NotOnCooldown_Initially)
|
||||
{
|
||||
auto aura = AuraStubBuilder().WithId(12345).Build();
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcCooldownTest, OnCooldown_AfterProc)
|
||||
{
|
||||
auto aura = AuraStubBuilder().WithId(12345).Build();
|
||||
|
||||
// Apply 1 second cooldown
|
||||
ProcChanceTestHelper::ApplyProcCooldown(aura.get(), _now, 1000);
|
||||
|
||||
// Should be on cooldown immediately after
|
||||
EXPECT_TRUE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now));
|
||||
EXPECT_TRUE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now + 500ms));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcCooldownTest, NotOnCooldown_AfterExpiry)
|
||||
{
|
||||
auto aura = AuraStubBuilder().WithId(12345).Build();
|
||||
|
||||
// Apply 1 second cooldown
|
||||
ProcChanceTestHelper::ApplyProcCooldown(aura.get(), _now, 1000);
|
||||
|
||||
// Should not be on cooldown after 1 second
|
||||
EXPECT_FALSE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now + 1001ms));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcCooldownTest, ExactCooldownBoundary)
|
||||
{
|
||||
auto aura = AuraStubBuilder().WithId(12345).Build();
|
||||
|
||||
ProcChanceTestHelper::ApplyProcCooldown(aura.get(), _now, 1000);
|
||||
|
||||
// At exactly cooldown time, should still be on cooldown (< not <=)
|
||||
EXPECT_TRUE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now + 999ms));
|
||||
// One millisecond after should be off cooldown
|
||||
EXPECT_FALSE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now + 1000ms));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Zero Cooldown Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcCooldownTest, ZeroCooldown_NeverBlocks)
|
||||
{
|
||||
auto aura = AuraStubBuilder().WithId(12345).Build();
|
||||
|
||||
// Zero cooldown should not apply any cooldown
|
||||
ProcChanceTestHelper::ApplyProcCooldown(aura.get(), _now, 0);
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Cooldown Reset Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcCooldownTest, CooldownCanBeReset)
|
||||
{
|
||||
auto aura = AuraStubBuilder().WithId(12345).Build();
|
||||
|
||||
ProcChanceTestHelper::ApplyProcCooldown(aura.get(), _now, 5000);
|
||||
EXPECT_TRUE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now));
|
||||
|
||||
aura->ResetProcCooldown();
|
||||
EXPECT_FALSE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcCooldownTest, CooldownCanBeExtended)
|
||||
{
|
||||
auto aura = AuraStubBuilder().WithId(12345).Build();
|
||||
|
||||
// Apply 1 second cooldown
|
||||
ProcChanceTestHelper::ApplyProcCooldown(aura.get(), _now, 1000);
|
||||
|
||||
// Extend to 5 seconds
|
||||
ProcChanceTestHelper::ApplyProcCooldown(aura.get(), _now, 5000);
|
||||
|
||||
// Should still be on cooldown after 2 seconds
|
||||
EXPECT_TRUE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now + 2000ms));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Scenario Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcCooldownTest, Scenario_LeaderOfThePack_6SecCooldown)
|
||||
{
|
||||
// Leader of the Pack has a 6 second internal cooldown
|
||||
auto aura = AuraStubBuilder().WithId(24932).Build();
|
||||
|
||||
// First proc
|
||||
ProcChanceTestHelper::ApplyProcCooldown(aura.get(), _now, 6000);
|
||||
|
||||
// Blocked at 3 seconds
|
||||
EXPECT_TRUE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now + 3000ms));
|
||||
|
||||
// Blocked at 5.9 seconds
|
||||
EXPECT_TRUE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now + 5999ms));
|
||||
|
||||
// Allowed at 6 seconds
|
||||
EXPECT_FALSE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now + 6000ms));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcCooldownTest, Scenario_WanderingPlague_1SecCooldown)
|
||||
{
|
||||
// Wandering Plague has a 1 second internal cooldown
|
||||
auto aura = AuraStubBuilder().WithId(49217).Build();
|
||||
|
||||
ProcChanceTestHelper::ApplyProcCooldown(aura.get(), _now, 1000);
|
||||
|
||||
// Blocked at 0.5 seconds
|
||||
EXPECT_TRUE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now + 500ms));
|
||||
|
||||
// Allowed at 1 second
|
||||
EXPECT_FALSE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), _now + 1000ms));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcCooldownTest, Scenario_MultipleProcsWithCooldown)
|
||||
{
|
||||
auto aura = AuraStubBuilder().WithId(12345).Build();
|
||||
auto time = _now;
|
||||
|
||||
// First proc at t=0
|
||||
EXPECT_FALSE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), time));
|
||||
ProcChanceTestHelper::ApplyProcCooldown(aura.get(), time, 1000);
|
||||
|
||||
// Second attempt at t=0.5 (blocked)
|
||||
time += 500ms;
|
||||
EXPECT_TRUE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), time));
|
||||
|
||||
// Third attempt at t=1.0 (allowed)
|
||||
time += 500ms;
|
||||
EXPECT_FALSE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), time));
|
||||
ProcChanceTestHelper::ApplyProcCooldown(aura.get(), time, 1000);
|
||||
|
||||
// Fourth attempt at t=1.5 (blocked)
|
||||
time += 500ms;
|
||||
EXPECT_TRUE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), time));
|
||||
|
||||
// Fifth attempt at t=2.0 (allowed)
|
||||
time += 500ms;
|
||||
EXPECT_FALSE(ProcChanceTestHelper::IsProcOnCooldown(aura.get(), time));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ProcTestScenario Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcCooldownTest, ProcTestScenario_CooldownBlocking)
|
||||
{
|
||||
ProcTestScenario scenario;
|
||||
scenario.WithAura(12345);
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithCooldown(1000ms)
|
||||
.Build();
|
||||
|
||||
// First proc should succeed
|
||||
EXPECT_TRUE(scenario.SimulateProc(procEntry));
|
||||
|
||||
// Second proc immediately after should fail (on cooldown)
|
||||
EXPECT_FALSE(scenario.SimulateProc(procEntry));
|
||||
|
||||
// Advance time past cooldown
|
||||
scenario.AdvanceTime(1100ms);
|
||||
|
||||
// Third proc should succeed
|
||||
EXPECT_TRUE(scenario.SimulateProc(procEntry));
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
/*
|
||||
* 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 SpellProcDBCValidationTest.cpp
|
||||
* @brief Unit tests for validating spell_proc entries against Spell.dbc
|
||||
*
|
||||
* Tests validate that spell_proc entries provide value beyond DBC defaults:
|
||||
* - Entries that override DBC ProcFlags/ProcChance/ProcCharges
|
||||
* - Entries that add new functionality (PPM, cooldowns, filtering)
|
||||
* - Identification of potentially redundant entries
|
||||
*
|
||||
* ============================================================================
|
||||
* DBC DATA POPULATION STATUS
|
||||
* ============================================================================
|
||||
*
|
||||
* The DBC_ProcFlags, DBC_ProcChance, and DBC_ProcCharges fields in
|
||||
* SpellProcTestEntry are currently populated with zeros (0, 0, 0) for all
|
||||
* entries. To fully validate spell_proc entries against DBC:
|
||||
*
|
||||
* 1. Use the generate_spell_proc_dbc_data.py script with MCP connection
|
||||
* 2. Or manually query: get_spell_dbc_proc_info(spell_id) for each spell
|
||||
*
|
||||
* Tests that require DBC data will check HasDBCData() and skip appropriately.
|
||||
* Once DBC data is populated, the statistics tests will show:
|
||||
* - How many entries override DBC defaults
|
||||
* - How many entries add new functionality not in DBC
|
||||
* - How many entries might be redundant (just duplicate DBC values)
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
#include "SpellProcTestData.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
using namespace testing;
|
||||
|
||||
// =============================================================================
|
||||
// DBC Validation Test Fixture
|
||||
// =============================================================================
|
||||
|
||||
class SpellProcDBCValidationTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_allEntries = GetAllSpellProcTestEntries();
|
||||
}
|
||||
|
||||
std::vector<SpellProcTestEntry> _allEntries;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Parameterized Tests for All Entries
|
||||
// =============================================================================
|
||||
|
||||
class SpellProcDBCValidationParamTest : public ::testing::TestWithParam<SpellProcTestEntry>
|
||||
{
|
||||
};
|
||||
|
||||
TEST_P(SpellProcDBCValidationParamTest, EntryHasValidSpellId)
|
||||
{
|
||||
auto const& entry = GetParam();
|
||||
int32_t spellId = std::abs(entry.SpellId);
|
||||
|
||||
// Spell ID must be positive after abs
|
||||
EXPECT_GT(spellId, 0) << "SpellId must be non-zero";
|
||||
|
||||
// Spell IDs in WotLK should be < 80000
|
||||
EXPECT_LT(spellId, 80000u)
|
||||
<< "SpellId " << spellId << " seems out of range for WotLK";
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
AllSpellProcEntries,
|
||||
SpellProcDBCValidationParamTest,
|
||||
::testing::ValuesIn(GetAllSpellProcTestEntries()),
|
||||
[](const testing::TestParamInfo<SpellProcTestEntry>& info) {
|
||||
// Create unique test name from SpellId (handle negative IDs)
|
||||
int32_t id = info.param.SpellId;
|
||||
if (id < 0)
|
||||
return "SpellId_N" + std::to_string(-id);
|
||||
return "SpellId_" + std::to_string(id);
|
||||
}
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// Statistics Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDBCValidationTest, CountEntriesWithDBCData)
|
||||
{
|
||||
size_t withDBC = 0;
|
||||
size_t withoutDBC = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (entry.HasDBCData())
|
||||
withDBC++;
|
||||
else
|
||||
withoutDBC++;
|
||||
}
|
||||
|
||||
std::cout << "[ INFO ] Entries with DBC data: " << withDBC << "\n";
|
||||
std::cout << "[ INFO ] Entries without DBC data: " << withoutDBC << std::endl;
|
||||
|
||||
// All entries should eventually have DBC data
|
||||
// For now, just verify the count
|
||||
EXPECT_EQ(_allEntries.size(), 869u);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDBCValidationTest, CountEntriesAddingValue)
|
||||
{
|
||||
size_t addsValue = 0;
|
||||
size_t potentiallyRedundant = 0;
|
||||
size_t noDBCYet = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
// SKIP REASON: Entries without DBC data populated cannot be compared
|
||||
// against DBC defaults. The HasDBCData() check returns false when
|
||||
// DBC_ProcFlags, DBC_ProcChance, and DBC_ProcCharges are all zero.
|
||||
// Once DBC data is populated via MCP tools, this count should be 0.
|
||||
if (!entry.HasDBCData())
|
||||
{
|
||||
noDBCYet++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.AddsValueBeyondDBC())
|
||||
addsValue++;
|
||||
else
|
||||
potentiallyRedundant++;
|
||||
}
|
||||
|
||||
std::cout << "[ INFO ] Entries adding value: " << addsValue << "\n";
|
||||
std::cout << "[ INFO ] Potentially redundant: " << potentiallyRedundant << "\n";
|
||||
std::cout << "[ INFO ] DBC data not yet populated: " << noDBCYet << std::endl;
|
||||
|
||||
// Most entries should add value (have PPM, cooldowns, filtering, etc.)
|
||||
if (addsValue + potentiallyRedundant > 0)
|
||||
{
|
||||
float valueRate = static_cast<float>(addsValue) / (addsValue + potentiallyRedundant) * 100;
|
||||
std::cout << "[ INFO ] Value-add rate: " << valueRate << "%" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDBCValidationTest, CategorizeEntriesByFeature)
|
||||
{
|
||||
size_t hasPPM = 0;
|
||||
size_t hasCooldown = 0;
|
||||
size_t hasSpellTypeMask = 0;
|
||||
size_t hasSpellPhaseMask = 0;
|
||||
size_t hasHitMask = 0;
|
||||
size_t hasAttributesMask = 0;
|
||||
size_t hasSpellFamilyMask = 0;
|
||||
size_t hasSchoolMask = 0;
|
||||
size_t hasCharges = 0;
|
||||
size_t hasDisableEffectsMask = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (entry.ProcsPerMinute > 0) hasPPM++;
|
||||
if (entry.Cooldown > 0) hasCooldown++;
|
||||
if (entry.SpellTypeMask != 0) hasSpellTypeMask++;
|
||||
if (entry.SpellPhaseMask != 0) hasSpellPhaseMask++;
|
||||
if (entry.HitMask != 0) hasHitMask++;
|
||||
if (entry.AttributesMask != 0) hasAttributesMask++;
|
||||
if (entry.SpellFamilyMask0 != 0 || entry.SpellFamilyMask1 != 0 || entry.SpellFamilyMask2 != 0)
|
||||
hasSpellFamilyMask++;
|
||||
if (entry.SchoolMask != 0) hasSchoolMask++;
|
||||
if (entry.Charges > 0) hasCharges++;
|
||||
if (entry.DisableEffectsMask != 0) hasDisableEffectsMask++;
|
||||
}
|
||||
|
||||
std::cout << "[ INFO ] Feature usage (adds value beyond DBC):\n"
|
||||
<< " PPM: " << hasPPM << "\n"
|
||||
<< " Cooldown: " << hasCooldown << "\n"
|
||||
<< " SpellTypeMask: " << hasSpellTypeMask << "\n"
|
||||
<< " SpellPhaseMask: " << hasSpellPhaseMask << "\n"
|
||||
<< " HitMask: " << hasHitMask << "\n"
|
||||
<< " AttributesMask: " << hasAttributesMask << "\n"
|
||||
<< " SpellFamilyMask: " << hasSpellFamilyMask << "\n"
|
||||
<< " SchoolMask: " << hasSchoolMask << "\n"
|
||||
<< " Charges: " << hasCharges << "\n"
|
||||
<< " DisableEffectsMask: " << hasDisableEffectsMask << std::endl;
|
||||
|
||||
// Most entries should use at least one extended feature
|
||||
size_t usingExtendedFeatures = 0;
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (entry.ProcsPerMinute > 0 || entry.Cooldown > 0 ||
|
||||
entry.SpellTypeMask != 0 || entry.SpellPhaseMask != 0 ||
|
||||
entry.HitMask != 0 || entry.AttributesMask != 0 ||
|
||||
entry.SpellFamilyMask0 != 0 || entry.SpellFamilyMask1 != 0 ||
|
||||
entry.SpellFamilyMask2 != 0 || entry.SchoolMask != 0 ||
|
||||
entry.DisableEffectsMask != 0)
|
||||
{
|
||||
usingExtendedFeatures++;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "[ INFO ] Entries using extended features: " << usingExtendedFeatures
|
||||
<< " / " << _allEntries.size() << std::endl;
|
||||
|
||||
// At least 80% should use extended features
|
||||
EXPECT_GT(usingExtendedFeatures, _allEntries.size() * 80 / 100)
|
||||
<< "Most entries should use extended features";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDBCValidationTest, IdentifyDBCOverrides)
|
||||
{
|
||||
size_t overridesProcFlags = 0;
|
||||
size_t overridesChance = 0;
|
||||
size_t overridesCharges = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
// SKIP REASON: Cannot compare against DBC defaults when DBC data
|
||||
// is not populated. All 869 entries currently have DBC fields = 0.
|
||||
// Once populated, this loop will count actual DBC overrides.
|
||||
if (!entry.HasDBCData())
|
||||
continue;
|
||||
|
||||
if (entry.ProcFlags != 0 && entry.ProcFlags != entry.DBC_ProcFlags)
|
||||
overridesProcFlags++;
|
||||
|
||||
if (entry.Chance != 0 && static_cast<uint32_t>(entry.Chance) != entry.DBC_ProcChance)
|
||||
overridesChance++;
|
||||
|
||||
if (entry.Charges != 0 && entry.Charges != entry.DBC_ProcCharges)
|
||||
overridesCharges++;
|
||||
}
|
||||
|
||||
std::cout << "[ INFO ] DBC Overrides:\n"
|
||||
<< " ProcFlags: " << overridesProcFlags << "\n"
|
||||
<< " Chance: " << overridesChance << "\n"
|
||||
<< " Charges: " << overridesCharges << std::endl;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Negative Spell ID Tests (Effect-specific procs)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDBCValidationTest, CountNegativeSpellIds)
|
||||
{
|
||||
size_t negativeIds = 0;
|
||||
size_t positiveIds = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (entry.SpellId < 0)
|
||||
negativeIds++;
|
||||
else
|
||||
positiveIds++;
|
||||
}
|
||||
|
||||
std::cout << "[ INFO ] Negative SpellIds (effect-specific): " << negativeIds << "\n";
|
||||
std::cout << "[ INFO ] Positive SpellIds: " << positiveIds << std::endl;
|
||||
|
||||
// Both types should exist
|
||||
EXPECT_GT(negativeIds, 0u) << "Should have some effect-specific (negative ID) entries";
|
||||
EXPECT_GT(positiveIds, 0u) << "Should have some spell-wide (positive ID) entries";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SpellFamily Coverage Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDBCValidationTest, CoverageBySpellFamily)
|
||||
{
|
||||
std::map<uint32_t, size_t> familyCounts;
|
||||
std::map<uint32_t, std::string> familyNames = {
|
||||
{0, "Generic"}, {3, "Mage"}, {4, "Warrior"}, {5, "Warlock"},
|
||||
{6, "Priest"}, {7, "Druid"}, {8, "Rogue"}, {9, "Hunter"},
|
||||
{10, "Paladin"}, {11, "Shaman"}, {15, "DeathKnight"}
|
||||
};
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
familyCounts[entry.SpellFamilyName]++;
|
||||
}
|
||||
|
||||
std::cout << "[ INFO ] Entries by SpellFamily:\n";
|
||||
for (auto const& [family, count] : familyCounts)
|
||||
{
|
||||
std::string name = familyNames.count(family) ? familyNames[family] : "Unknown";
|
||||
std::cout << " " << name << " (" << family << "): " << count << "\n";
|
||||
}
|
||||
|
||||
// Should have entries from multiple spell families
|
||||
EXPECT_GT(familyCounts.size(), 5u) << "Should cover multiple spell families";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Data Integrity Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDBCValidationTest, NoDuplicateSpellIds)
|
||||
{
|
||||
std::set<int32_t> seenIds;
|
||||
std::vector<int32_t> duplicates;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (seenIds.count(entry.SpellId))
|
||||
duplicates.push_back(entry.SpellId);
|
||||
else
|
||||
seenIds.insert(entry.SpellId);
|
||||
}
|
||||
|
||||
if (!duplicates.empty())
|
||||
{
|
||||
std::cout << "[ WARN ] Duplicate SpellIds found: ";
|
||||
for (auto id : duplicates)
|
||||
std::cout << id << " ";
|
||||
std::cout << std::endl;
|
||||
}
|
||||
|
||||
EXPECT_TRUE(duplicates.empty()) << "Should have no duplicate SpellIds";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDBCValidationTest, AllEntriesHaveValidStructure)
|
||||
{
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
// SpellId must be non-zero
|
||||
EXPECT_NE(entry.SpellId, 0)
|
||||
<< "SpellId cannot be zero";
|
||||
|
||||
// If Chance is set, it should be reasonable (0-100, or 101 for 100% from DBC)
|
||||
if (entry.Chance > 0)
|
||||
{
|
||||
EXPECT_LE(entry.Chance, 101.0f)
|
||||
<< "SpellId " << entry.SpellId << " has unusual Chance: " << entry.Chance;
|
||||
}
|
||||
|
||||
// PPM should be reasonable (typically 0-60)
|
||||
if (entry.ProcsPerMinute > 0)
|
||||
{
|
||||
EXPECT_LE(entry.ProcsPerMinute, 60.0f)
|
||||
<< "SpellId " << entry.SpellId << " has unusual PPM: " << entry.ProcsPerMinute;
|
||||
}
|
||||
|
||||
// SpellPhaseMask should use valid values
|
||||
if (entry.SpellPhaseMask != 0)
|
||||
{
|
||||
// Valid phase masks: PROC_SPELL_PHASE_CAST(1), PROC_SPELL_PHASE_HIT(2), PROC_SPELL_PHASE_FINISH(4)
|
||||
EXPECT_LE(entry.SpellPhaseMask, 7u)
|
||||
<< "SpellId " << entry.SpellId << " has unusual SpellPhaseMask: " << entry.SpellPhaseMask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,756 @@
|
||||
/*
|
||||
* 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 SpellProcDataDrivenTest.cpp
|
||||
* @brief Comprehensive data-driven tests for ALL 869 spell_proc entries
|
||||
*
|
||||
* This file auto-tests every spell_proc entry from the database.
|
||||
* Data is generated by: src/test/scripts/generate_spell_proc_data.py
|
||||
*/
|
||||
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "SpellInfoTestHelper.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "SpellProcTestData.h"
|
||||
#include "WorldMock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
using namespace testing;
|
||||
|
||||
// =============================================================================
|
||||
// Proc Flag Mappings
|
||||
// =============================================================================
|
||||
|
||||
struct ProcFlagScenario
|
||||
{
|
||||
uint32 procFlag;
|
||||
const char* name;
|
||||
uint32 defaultHitMask;
|
||||
uint32 defaultSpellTypeMask;
|
||||
uint32 defaultSpellPhaseMask;
|
||||
bool requiresSpellPhase;
|
||||
};
|
||||
|
||||
static const std::vector<ProcFlagScenario> PROC_FLAG_SCENARIOS = {
|
||||
{ PROC_FLAG_DONE_MELEE_AUTO_ATTACK, "DoneMeleeAuto", PROC_HIT_NORMAL, 0, 0, false },
|
||||
{ PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK, "TakenMeleeAuto", PROC_HIT_NORMAL, 0, 0, false },
|
||||
{ PROC_FLAG_DONE_MAINHAND_ATTACK, "DoneMainhand", PROC_HIT_NORMAL, 0, 0, false },
|
||||
{ PROC_FLAG_DONE_OFFHAND_ATTACK, "DoneOffhand", PROC_HIT_NORMAL, 0, 0, false },
|
||||
{ PROC_FLAG_DONE_RANGED_AUTO_ATTACK, "DoneRangedAuto", PROC_HIT_NORMAL, 0, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_TAKEN_RANGED_AUTO_ATTACK, "TakenRangedAuto", PROC_HIT_NORMAL, 0, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_DONE_SPELL_MELEE_DMG_CLASS, "DoneSpellMelee", PROC_HIT_NORMAL, PROC_SPELL_TYPE_DAMAGE, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_TAKEN_SPELL_MELEE_DMG_CLASS, "TakenSpellMelee", PROC_HIT_NORMAL, PROC_SPELL_TYPE_DAMAGE, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_DONE_SPELL_RANGED_DMG_CLASS, "DoneSpellRanged", PROC_HIT_NORMAL, PROC_SPELL_TYPE_DAMAGE, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_TAKEN_SPELL_RANGED_DMG_CLASS, "TakenSpellRanged", PROC_HIT_NORMAL, PROC_SPELL_TYPE_DAMAGE, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_POS, "DoneSpellNonePos", PROC_HIT_NORMAL, PROC_SPELL_TYPE_HEAL, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_TAKEN_SPELL_NONE_DMG_CLASS_POS, "TakenSpellNonePos", PROC_HIT_NORMAL, PROC_SPELL_TYPE_HEAL, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_NEG, "DoneSpellNoneNeg", PROC_HIT_NORMAL, PROC_SPELL_TYPE_DAMAGE, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_TAKEN_SPELL_NONE_DMG_CLASS_NEG, "TakenSpellNoneNeg", PROC_HIT_NORMAL, PROC_SPELL_TYPE_DAMAGE, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS, "DoneSpellMagicPos", PROC_HIT_NORMAL, PROC_SPELL_TYPE_HEAL, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_TAKEN_SPELL_MAGIC_DMG_CLASS_POS,"TakenSpellMagicPos", PROC_HIT_NORMAL, PROC_SPELL_TYPE_HEAL, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG, "DoneSpellMagicNeg", PROC_HIT_NORMAL, PROC_SPELL_TYPE_DAMAGE, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_TAKEN_SPELL_MAGIC_DMG_CLASS_NEG,"TakenSpellMagicNeg", PROC_HIT_NORMAL, PROC_SPELL_TYPE_DAMAGE, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_DONE_PERIODIC, "DonePeriodic", PROC_HIT_NORMAL, PROC_SPELL_TYPE_DAMAGE, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_TAKEN_PERIODIC, "TakenPeriodic", PROC_HIT_NORMAL, PROC_SPELL_TYPE_DAMAGE, PROC_SPELL_PHASE_HIT, true },
|
||||
{ PROC_FLAG_TAKEN_DAMAGE, "TakenDamage", PROC_HIT_NORMAL, 0, 0, false },
|
||||
{ PROC_FLAG_KILL, "Kill", 0, 0, 0, false },
|
||||
{ PROC_FLAG_KILLED, "Killed", 0, 0, 0, false },
|
||||
{ PROC_FLAG_DEATH, "Death", 0, 0, 0, false },
|
||||
{ PROC_FLAG_DONE_TRAP_ACTIVATION, "TrapActivation", PROC_HIT_NORMAL, 0, PROC_SPELL_PHASE_HIT, true },
|
||||
};
|
||||
|
||||
static const std::vector<std::pair<uint32, const char*>> HIT_MASK_SCENARIOS = {
|
||||
{ PROC_HIT_NORMAL, "Normal" },
|
||||
{ PROC_HIT_CRITICAL, "Critical" },
|
||||
{ PROC_HIT_MISS, "Miss" },
|
||||
{ PROC_HIT_DODGE, "Dodge" },
|
||||
{ PROC_HIT_PARRY, "Parry" },
|
||||
{ PROC_HIT_BLOCK, "Block" },
|
||||
{ PROC_HIT_EVADE, "Evade" },
|
||||
{ PROC_HIT_IMMUNE, "Immune" },
|
||||
{ PROC_HIT_DEFLECT, "Deflect" },
|
||||
{ PROC_HIT_ABSORB, "Absorb" },
|
||||
{ PROC_HIT_REFLECT, "Reflect" },
|
||||
{ PROC_HIT_INTERRUPT, "Interrupt" },
|
||||
{ PROC_HIT_FULL_BLOCK, "FullBlock" },
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Test Fixture for Comprehensive Database Testing
|
||||
// =============================================================================
|
||||
|
||||
class SpellProcDatabaseTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_originalWorld = sWorld.release();
|
||||
_worldMock = new NiceMock<WorldMock>();
|
||||
sWorld.reset(_worldMock);
|
||||
|
||||
static std::string emptyString;
|
||||
ON_CALL(*_worldMock, GetDataPath()).WillByDefault(ReturnRef(emptyString));
|
||||
|
||||
// Load all entries from generated data
|
||||
_allEntries = GetAllSpellProcTestEntries();
|
||||
|
||||
// Create a default SpellInfo for spell-type procs
|
||||
_defaultSpellInfo = SpellInfoBuilder()
|
||||
.WithId(99999)
|
||||
.WithSpellFamilyName(0)
|
||||
.Build();
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
IWorld* currentWorld = sWorld.release();
|
||||
delete currentWorld;
|
||||
_worldMock = nullptr;
|
||||
sWorld.reset(_originalWorld);
|
||||
|
||||
delete _defaultSpellInfo;
|
||||
_defaultSpellInfo = nullptr;
|
||||
delete _damageInfo;
|
||||
_damageInfo = nullptr;
|
||||
delete _healInfo;
|
||||
_healInfo = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Find the first matching proc flag scenario for given flags
|
||||
*/
|
||||
ProcFlagScenario const* FindMatchingScenario(uint32 procFlags)
|
||||
{
|
||||
for (auto const& scenario : PROC_FLAG_SCENARIOS)
|
||||
{
|
||||
if (procFlags & scenario.procFlag)
|
||||
return &scenario;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get effective hit mask for an entry
|
||||
*/
|
||||
uint32 GetEffectiveHitMask(SpellProcTestEntry const& entry, ProcFlagScenario const* scenario)
|
||||
{
|
||||
if (entry.HitMask != 0)
|
||||
{
|
||||
// Return first set bit
|
||||
for (auto const& [mask, name] : HIT_MASK_SCENARIOS)
|
||||
{
|
||||
if (entry.HitMask & mask)
|
||||
return mask;
|
||||
}
|
||||
}
|
||||
return scenario ? scenario->defaultHitMask : PROC_HIT_NORMAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get effective spell type mask
|
||||
*/
|
||||
uint32 GetEffectiveSpellTypeMask(SpellProcTestEntry const& entry, ProcFlagScenario const* scenario)
|
||||
{
|
||||
if (entry.SpellTypeMask != 0)
|
||||
{
|
||||
if (entry.SpellTypeMask & PROC_SPELL_TYPE_DAMAGE)
|
||||
return PROC_SPELL_TYPE_DAMAGE;
|
||||
if (entry.SpellTypeMask & PROC_SPELL_TYPE_HEAL)
|
||||
return PROC_SPELL_TYPE_HEAL;
|
||||
if (entry.SpellTypeMask & PROC_SPELL_TYPE_NO_DMG_HEAL)
|
||||
return PROC_SPELL_TYPE_NO_DMG_HEAL;
|
||||
}
|
||||
return scenario ? scenario->defaultSpellTypeMask : PROC_SPELL_TYPE_MASK_ALL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get effective spell phase mask
|
||||
*/
|
||||
uint32 GetEffectiveSpellPhaseMask(SpellProcTestEntry const& entry, ProcFlagScenario const* scenario)
|
||||
{
|
||||
if (entry.SpellPhaseMask != 0)
|
||||
return entry.SpellPhaseMask;
|
||||
if (scenario && scenario->requiresSpellPhase)
|
||||
return scenario->defaultSpellPhaseMask ? scenario->defaultSpellPhaseMask : PROC_SPELL_PHASE_HIT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if entry requires SpellFamily matching (which we can't test without SpellInfo)
|
||||
* Any entry with SpellFamilyName > 0 will cause CanSpellTriggerProcOnEvent to access
|
||||
* eventInfo.GetSpellInfo() which returns null in our test, causing a crash.
|
||||
*/
|
||||
bool RequiresSpellFamilyMatch(SpellProcTestEntry const& entry)
|
||||
{
|
||||
// Skip any entry with SpellFamilyName set - the code will try to access SpellInfo
|
||||
return entry.SpellFamilyName != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if the proc flags indicate a spell-type event that needs SpellInfo
|
||||
*/
|
||||
bool IsSpellTypeProc(uint32 procFlags)
|
||||
{
|
||||
return (procFlags & (PERIODIC_PROC_FLAG_MASK | SPELL_PROC_FLAG_MASK | PROC_FLAG_DONE_TRAP_ACTIVATION)) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create a ProcEventInfo with proper DamageInfo/HealInfo for spell-type procs
|
||||
*/
|
||||
ProcEventInfo CreateEventInfo(uint32 typeMask, uint32 hitMask, uint32 spellTypeMask, uint32 spellPhaseMask)
|
||||
{
|
||||
auto builder = ProcEventInfoBuilder()
|
||||
.WithTypeMask(typeMask)
|
||||
.WithHitMask(hitMask)
|
||||
.WithSpellTypeMask(spellTypeMask)
|
||||
.WithSpellPhaseMask(spellPhaseMask);
|
||||
|
||||
// For spell-type procs, provide DamageInfo or HealInfo with SpellInfo
|
||||
if (IsSpellTypeProc(typeMask))
|
||||
{
|
||||
if (spellTypeMask & PROC_SPELL_TYPE_HEAL)
|
||||
{
|
||||
if (!_healInfo)
|
||||
_healInfo = new HealInfo(nullptr, nullptr, 100, _defaultSpellInfo, SPELL_SCHOOL_MASK_HOLY);
|
||||
builder.WithHealInfo(_healInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_damageInfo)
|
||||
_damageInfo = new DamageInfo(nullptr, nullptr, 100, _defaultSpellInfo, SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE);
|
||||
builder.WithDamageInfo(_damageInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
IWorld* _originalWorld = nullptr;
|
||||
NiceMock<WorldMock>* _worldMock = nullptr;
|
||||
SpellInfo* _defaultSpellInfo = nullptr;
|
||||
DamageInfo* _damageInfo = nullptr;
|
||||
HealInfo* _healInfo = nullptr;
|
||||
std::vector<SpellProcTestEntry> _allEntries;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Comprehensive Tests for All 869 Entries
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, AllEntriesLoaded)
|
||||
{
|
||||
EXPECT_EQ(_allEntries.size(), 869u) << "Should have all 869 spell_proc entries loaded";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, AllEntriesWithProcFlags_PositiveTest)
|
||||
{
|
||||
int totalTested = 0;
|
||||
int passed = 0;
|
||||
int skippedFamily = 0;
|
||||
int skippedNoFlags = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
// Skip entries with no ProcFlags (they rely on other conditions)
|
||||
if (entry.ProcFlags == 0)
|
||||
{
|
||||
skippedNoFlags++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip entries that require SpellFamily matching
|
||||
if (RequiresSpellFamilyMatch(entry))
|
||||
{
|
||||
skippedFamily++;
|
||||
continue;
|
||||
}
|
||||
|
||||
totalTested++;
|
||||
|
||||
ProcFlagScenario const* scenario = FindMatchingScenario(entry.ProcFlags);
|
||||
if (!scenario)
|
||||
continue;
|
||||
|
||||
SpellProcEntry procEntry = entry.ToSpellProcEntry();
|
||||
|
||||
auto eventInfo = CreateEventInfo(
|
||||
scenario->procFlag,
|
||||
GetEffectiveHitMask(entry, scenario),
|
||||
GetEffectiveSpellTypeMask(entry, scenario),
|
||||
GetEffectiveSpellPhaseMask(entry, scenario));
|
||||
|
||||
if (sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
{
|
||||
passed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Report statistics
|
||||
float passRate = totalTested > 0 ? (float)passed / totalTested * 100 : 0;
|
||||
SCOPED_TRACE("Total entries: " + std::to_string(_allEntries.size()));
|
||||
SCOPED_TRACE("Tested: " + std::to_string(totalTested));
|
||||
SCOPED_TRACE("Passed: " + std::to_string(passed) + " (" + std::to_string((int)passRate) + "%)");
|
||||
SCOPED_TRACE("Skipped (SpellFamily): " + std::to_string(skippedFamily));
|
||||
SCOPED_TRACE("Skipped (NoFlags): " + std::to_string(skippedNoFlags));
|
||||
|
||||
// Expect high pass rate for entries we can test
|
||||
EXPECT_GT(passed, totalTested / 2) << "At least half of tested entries should pass";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, AllEntriesWithProcFlags_NegativeTest)
|
||||
{
|
||||
int totalTested = 0;
|
||||
int correctlyRejected = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (entry.ProcFlags == 0)
|
||||
continue;
|
||||
if (RequiresSpellFamilyMatch(entry))
|
||||
continue;
|
||||
|
||||
// Find a flag that's NOT in this entry's ProcFlags
|
||||
uint32 wrongFlag = 0;
|
||||
for (auto const& scenario : PROC_FLAG_SCENARIOS)
|
||||
{
|
||||
if (!(entry.ProcFlags & scenario.procFlag) && scenario.procFlag != PROC_FLAG_KILL)
|
||||
{
|
||||
wrongFlag = scenario.procFlag;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (wrongFlag == 0)
|
||||
continue;
|
||||
|
||||
totalTested++;
|
||||
|
||||
SpellProcEntry procEntry = entry.ToSpellProcEntry();
|
||||
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithTypeMask(wrongFlag)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.Build();
|
||||
|
||||
if (!sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
{
|
||||
correctlyRejected++;
|
||||
}
|
||||
}
|
||||
|
||||
float rejectRate = totalTested > 0 ? (float)correctlyRejected / totalTested * 100 : 0;
|
||||
SCOPED_TRACE("Tested: " + std::to_string(totalTested));
|
||||
SCOPED_TRACE("Rejected: " + std::to_string(correctlyRejected) + " (" + std::to_string((int)rejectRate) + "%)");
|
||||
|
||||
EXPECT_GT(rejectRate, 90.0f) << "Most entries should reject non-matching proc flags";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests by Proc Flag Type
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, MeleeProcs_AllTriggerOnMelee)
|
||||
{
|
||||
int tested = 0, passed = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (!(entry.ProcFlags & PROC_FLAG_DONE_MELEE_AUTO_ATTACK))
|
||||
continue;
|
||||
if (RequiresSpellFamilyMatch(entry))
|
||||
continue;
|
||||
|
||||
tested++;
|
||||
SpellProcEntry procEntry = entry.ToSpellProcEntry();
|
||||
|
||||
uint32 hitMask = entry.HitMask != 0 ? (entry.HitMask & -entry.HitMask) : PROC_HIT_NORMAL;
|
||||
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(hitMask)
|
||||
.Build();
|
||||
|
||||
if (sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
passed++;
|
||||
}
|
||||
|
||||
SCOPED_TRACE("Melee procs: " + std::to_string(tested) + " tested, " + std::to_string(passed) + " passed");
|
||||
if (tested > 0)
|
||||
EXPECT_EQ(passed, tested);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, SpellDamageProcs_AllTriggerOnSpellDamage)
|
||||
{
|
||||
int tested = 0, passed = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (!(entry.ProcFlags & PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
continue;
|
||||
if (RequiresSpellFamilyMatch(entry))
|
||||
continue;
|
||||
|
||||
tested++;
|
||||
SpellProcEntry procEntry = entry.ToSpellProcEntry();
|
||||
|
||||
uint32 hitMask = entry.HitMask != 0 ? (entry.HitMask & -entry.HitMask) : PROC_HIT_NORMAL;
|
||||
uint32 spellTypeMask = entry.SpellTypeMask != 0 ? entry.SpellTypeMask : PROC_SPELL_TYPE_DAMAGE;
|
||||
uint32 spellPhaseMask = entry.SpellPhaseMask != 0 ? entry.SpellPhaseMask : PROC_SPELL_PHASE_HIT;
|
||||
|
||||
auto eventInfo = CreateEventInfo(
|
||||
PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG,
|
||||
hitMask,
|
||||
spellTypeMask,
|
||||
spellPhaseMask);
|
||||
|
||||
if (sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
passed++;
|
||||
}
|
||||
|
||||
SCOPED_TRACE("Spell damage procs: " + std::to_string(tested) + " tested, " + std::to_string(passed) + " passed");
|
||||
if (tested > 0)
|
||||
EXPECT_GT(passed, 0);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, HealProcs_AllTriggerOnHeal)
|
||||
{
|
||||
int tested = 0, passed = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (!(entry.ProcFlags & PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS))
|
||||
continue;
|
||||
if (RequiresSpellFamilyMatch(entry))
|
||||
continue;
|
||||
|
||||
tested++;
|
||||
SpellProcEntry procEntry = entry.ToSpellProcEntry();
|
||||
|
||||
uint32 hitMask = entry.HitMask != 0 ? (entry.HitMask & -entry.HitMask) : PROC_HIT_NORMAL;
|
||||
uint32 spellTypeMask = entry.SpellTypeMask != 0 ? entry.SpellTypeMask : PROC_SPELL_TYPE_HEAL;
|
||||
uint32 spellPhaseMask = entry.SpellPhaseMask != 0 ? entry.SpellPhaseMask : PROC_SPELL_PHASE_HIT;
|
||||
|
||||
auto eventInfo = CreateEventInfo(
|
||||
PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS,
|
||||
hitMask,
|
||||
spellTypeMask,
|
||||
spellPhaseMask);
|
||||
|
||||
if (sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
passed++;
|
||||
}
|
||||
|
||||
SCOPED_TRACE("Heal procs: " + std::to_string(tested) + " tested, " + std::to_string(passed) + " passed");
|
||||
if (tested > 0)
|
||||
EXPECT_GT(passed, 0);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, PeriodicProcs_AllTriggerOnPeriodic)
|
||||
{
|
||||
int tested = 0, passed = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (!(entry.ProcFlags & PROC_FLAG_DONE_PERIODIC))
|
||||
continue;
|
||||
if (RequiresSpellFamilyMatch(entry))
|
||||
continue;
|
||||
|
||||
tested++;
|
||||
SpellProcEntry procEntry = entry.ToSpellProcEntry();
|
||||
|
||||
uint32 hitMask = entry.HitMask != 0 ? (entry.HitMask & -entry.HitMask) : PROC_HIT_NORMAL;
|
||||
uint32 spellTypeMask = entry.SpellTypeMask != 0 ? entry.SpellTypeMask : PROC_SPELL_TYPE_DAMAGE;
|
||||
uint32 spellPhaseMask = entry.SpellPhaseMask != 0 ? entry.SpellPhaseMask : PROC_SPELL_PHASE_HIT;
|
||||
|
||||
auto eventInfo = CreateEventInfo(
|
||||
PROC_FLAG_DONE_PERIODIC,
|
||||
hitMask,
|
||||
spellTypeMask,
|
||||
spellPhaseMask);
|
||||
|
||||
if (sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
passed++;
|
||||
}
|
||||
|
||||
SCOPED_TRACE("Periodic procs: " + std::to_string(tested) + " tested, " + std::to_string(passed) + " passed");
|
||||
if (tested > 0)
|
||||
EXPECT_GT(passed, 0);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, KillProcs_AllTriggerOnKill)
|
||||
{
|
||||
int tested = 0, passed = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (!(entry.ProcFlags & PROC_FLAG_KILL))
|
||||
continue;
|
||||
|
||||
tested++;
|
||||
SpellProcEntry procEntry = entry.ToSpellProcEntry();
|
||||
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_KILL)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.Build();
|
||||
|
||||
if (sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
passed++;
|
||||
}
|
||||
|
||||
SCOPED_TRACE("Kill procs: " + std::to_string(tested) + " tested, " + std::to_string(passed) + " passed");
|
||||
// Kill events always proc
|
||||
EXPECT_EQ(passed, tested);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests by Hit Mask Type
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, CritOnlyProcs_OnlyTriggerOnCrit)
|
||||
{
|
||||
int tested = 0, critPassed = 0, normalRejected = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
// Only entries with EXACTLY crit requirement
|
||||
if (entry.HitMask != PROC_HIT_CRITICAL)
|
||||
continue;
|
||||
if (entry.ProcFlags == 0)
|
||||
continue;
|
||||
if (RequiresSpellFamilyMatch(entry))
|
||||
continue;
|
||||
|
||||
ProcFlagScenario const* scenario = FindMatchingScenario(entry.ProcFlags);
|
||||
if (!scenario)
|
||||
continue;
|
||||
|
||||
tested++;
|
||||
SpellProcEntry procEntry = entry.ToSpellProcEntry();
|
||||
|
||||
// Test crit - should pass
|
||||
auto critEvent = CreateEventInfo(
|
||||
scenario->procFlag,
|
||||
PROC_HIT_CRITICAL,
|
||||
GetEffectiveSpellTypeMask(entry, scenario),
|
||||
GetEffectiveSpellPhaseMask(entry, scenario));
|
||||
|
||||
if (sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, critEvent))
|
||||
critPassed++;
|
||||
|
||||
// Test normal - should fail
|
||||
auto normalEvent = CreateEventInfo(
|
||||
scenario->procFlag,
|
||||
PROC_HIT_NORMAL,
|
||||
GetEffectiveSpellTypeMask(entry, scenario),
|
||||
GetEffectiveSpellPhaseMask(entry, scenario));
|
||||
|
||||
if (!sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, normalEvent))
|
||||
normalRejected++;
|
||||
}
|
||||
|
||||
SCOPED_TRACE("Crit-only procs: " + std::to_string(tested) + " tested");
|
||||
SCOPED_TRACE("Crit passed: " + std::to_string(critPassed));
|
||||
SCOPED_TRACE("Normal rejected: " + std::to_string(normalRejected));
|
||||
|
||||
if (tested > 0)
|
||||
{
|
||||
// Most crit-only entries should work, but some may have additional requirements
|
||||
EXPECT_GT(critPassed, 0) << "At least some crit-only procs should trigger on crits";
|
||||
EXPECT_GT(normalRejected, 0) << "At least some crit-only procs should NOT trigger on normal hits";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, DodgeProcs_OnlyTriggerOnDodge)
|
||||
{
|
||||
int tested = 0, passed = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (!(entry.HitMask & PROC_HIT_DODGE))
|
||||
continue;
|
||||
if (entry.ProcFlags == 0)
|
||||
continue;
|
||||
|
||||
ProcFlagScenario const* scenario = FindMatchingScenario(entry.ProcFlags);
|
||||
if (!scenario)
|
||||
continue;
|
||||
|
||||
tested++;
|
||||
SpellProcEntry procEntry = entry.ToSpellProcEntry();
|
||||
|
||||
auto eventInfo = CreateEventInfo(
|
||||
scenario->procFlag,
|
||||
PROC_HIT_DODGE,
|
||||
GetEffectiveSpellTypeMask(entry, scenario),
|
||||
GetEffectiveSpellPhaseMask(entry, scenario));
|
||||
|
||||
if (sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
passed++;
|
||||
}
|
||||
|
||||
SCOPED_TRACE("Dodge procs: " + std::to_string(tested) + " tested, " + std::to_string(passed) + " passed");
|
||||
if (tested > 0)
|
||||
EXPECT_EQ(passed, tested);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, ParryProcs_OnlyTriggerOnParry)
|
||||
{
|
||||
int tested = 0, passed = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (!(entry.HitMask & PROC_HIT_PARRY))
|
||||
continue;
|
||||
if (entry.ProcFlags == 0)
|
||||
continue;
|
||||
|
||||
ProcFlagScenario const* scenario = FindMatchingScenario(entry.ProcFlags);
|
||||
if (!scenario)
|
||||
continue;
|
||||
|
||||
tested++;
|
||||
SpellProcEntry procEntry = entry.ToSpellProcEntry();
|
||||
|
||||
auto eventInfo = CreateEventInfo(
|
||||
scenario->procFlag,
|
||||
PROC_HIT_PARRY,
|
||||
GetEffectiveSpellTypeMask(entry, scenario),
|
||||
GetEffectiveSpellPhaseMask(entry, scenario));
|
||||
|
||||
if (sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
passed++;
|
||||
}
|
||||
|
||||
SCOPED_TRACE("Parry procs: " + std::to_string(tested) + " tested, " + std::to_string(passed) + " passed");
|
||||
if (tested > 0)
|
||||
EXPECT_EQ(passed, tested);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, BlockProcs_OnlyTriggerOnBlock)
|
||||
{
|
||||
int tested = 0, passed = 0;
|
||||
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (!(entry.HitMask & PROC_HIT_BLOCK))
|
||||
continue;
|
||||
if (entry.ProcFlags == 0)
|
||||
continue;
|
||||
|
||||
ProcFlagScenario const* scenario = FindMatchingScenario(entry.ProcFlags);
|
||||
if (!scenario)
|
||||
continue;
|
||||
|
||||
tested++;
|
||||
SpellProcEntry procEntry = entry.ToSpellProcEntry();
|
||||
|
||||
auto eventInfo = CreateEventInfo(
|
||||
scenario->procFlag,
|
||||
PROC_HIT_BLOCK,
|
||||
GetEffectiveSpellTypeMask(entry, scenario),
|
||||
GetEffectiveSpellPhaseMask(entry, scenario));
|
||||
|
||||
if (sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
passed++;
|
||||
}
|
||||
|
||||
SCOPED_TRACE("Block procs: " + std::to_string(tested) + " tested, " + std::to_string(passed) + " passed");
|
||||
if (tested > 0)
|
||||
EXPECT_EQ(passed, tested);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests by Spell Family (Class-Specific)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, GroupBySpellFamily_Statistics)
|
||||
{
|
||||
std::map<uint32, std::string> familyNames = {
|
||||
{0, "Generic"}, {3, "Mage"}, {4, "Warrior"}, {5, "Warlock"},
|
||||
{6, "Priest"}, {7, "Druid"}, {8, "Rogue"}, {9, "Hunter"},
|
||||
{10, "Paladin"}, {11, "Shaman"}, {15, "DeathKnight"}
|
||||
};
|
||||
|
||||
std::map<uint32, int> familyCounts;
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
familyCounts[entry.SpellFamilyName]++;
|
||||
}
|
||||
|
||||
for (auto const& [family, count] : familyCounts)
|
||||
{
|
||||
std::string name = familyNames.count(family) ? familyNames[family] : "Unknown(" + std::to_string(family) + ")";
|
||||
SCOPED_TRACE("SpellFamily " + name + ": " + std::to_string(count) + " entries");
|
||||
}
|
||||
|
||||
EXPECT_GT(familyCounts.size(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, GroupByProcFlags_Statistics)
|
||||
{
|
||||
std::map<uint32, int> flagCounts;
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
flagCounts[entry.ProcFlags]++;
|
||||
}
|
||||
|
||||
SCOPED_TRACE("Unique ProcFlags patterns: " + std::to_string(flagCounts.size()));
|
||||
EXPECT_GT(flagCounts.size(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, GroupByHitMask_Statistics)
|
||||
{
|
||||
std::map<uint32, int> hitCounts;
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
hitCounts[entry.HitMask]++;
|
||||
}
|
||||
|
||||
SCOPED_TRACE("Unique HitMask patterns: " + std::to_string(hitCounts.size()));
|
||||
EXPECT_GT(hitCounts.size(), 0u);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Data Integrity Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDatabaseTest, ToSpellProcEntry_ConversionCorrect)
|
||||
{
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
SpellProcEntry converted = entry.ToSpellProcEntry();
|
||||
|
||||
EXPECT_EQ(converted.SchoolMask, entry.SchoolMask);
|
||||
EXPECT_EQ(converted.SpellFamilyName, entry.SpellFamilyName);
|
||||
EXPECT_EQ(converted.SpellFamilyMask[0], entry.SpellFamilyMask0);
|
||||
EXPECT_EQ(converted.SpellFamilyMask[1], entry.SpellFamilyMask1);
|
||||
EXPECT_EQ(converted.SpellFamilyMask[2], entry.SpellFamilyMask2);
|
||||
EXPECT_EQ(converted.ProcFlags, entry.ProcFlags);
|
||||
EXPECT_EQ(converted.SpellTypeMask, entry.SpellTypeMask);
|
||||
EXPECT_EQ(converted.SpellPhaseMask, entry.SpellPhaseMask);
|
||||
EXPECT_EQ(converted.HitMask, entry.HitMask);
|
||||
EXPECT_EQ(converted.AttributesMask, entry.AttributesMask);
|
||||
EXPECT_EQ(converted.Cooldown.count(), static_cast<int64>(entry.Cooldown));
|
||||
EXPECT_FLOAT_EQ(converted.Chance, entry.Chance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* 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 SpellProcDisableEffectsTest.cpp
|
||||
* @brief Unit tests for DisableEffectsMask filtering in proc system
|
||||
*
|
||||
* Tests the logic from SpellAuras.cpp:2244-2258:
|
||||
* - Bitmask filtering for effect indices 0, 1, 2
|
||||
* - Combined filtering with multiple disabled effects
|
||||
* - Proc blocking when all effects are disabled
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
class SpellProcDisableEffectsTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
|
||||
// Default initial mask with all 3 effects enabled
|
||||
static constexpr uint8 ALL_EFFECTS_MASK = 0x07; // 0b111
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Single Effect Disable Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, DisableEffect0_BlocksOnlyEffect0)
|
||||
{
|
||||
uint32 disableMask = 0x01; // Disable effect 0
|
||||
|
||||
uint8 result = ProcChanceTestHelper::ApplyDisableEffectsMask(ALL_EFFECTS_MASK, disableMask);
|
||||
|
||||
EXPECT_EQ(result, 0x06) // 0b110 - effects 1 and 2 remain
|
||||
<< "DisableEffectsMask=0x01 should only disable effect 0";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, DisableEffect1_BlocksOnlyEffect1)
|
||||
{
|
||||
uint32 disableMask = 0x02; // Disable effect 1
|
||||
|
||||
uint8 result = ProcChanceTestHelper::ApplyDisableEffectsMask(ALL_EFFECTS_MASK, disableMask);
|
||||
|
||||
EXPECT_EQ(result, 0x05) // 0b101 - effects 0 and 2 remain
|
||||
<< "DisableEffectsMask=0x02 should only disable effect 1";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, DisableEffect2_BlocksOnlyEffect2)
|
||||
{
|
||||
uint32 disableMask = 0x04; // Disable effect 2
|
||||
|
||||
uint8 result = ProcChanceTestHelper::ApplyDisableEffectsMask(ALL_EFFECTS_MASK, disableMask);
|
||||
|
||||
EXPECT_EQ(result, 0x03) // 0b011 - effects 0 and 1 remain
|
||||
<< "DisableEffectsMask=0x04 should only disable effect 2";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Multiple Effects Disable Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, DisableEffects0And1_LeavesEffect2)
|
||||
{
|
||||
uint32 disableMask = 0x03; // Disable effects 0 and 1
|
||||
|
||||
uint8 result = ProcChanceTestHelper::ApplyDisableEffectsMask(ALL_EFFECTS_MASK, disableMask);
|
||||
|
||||
EXPECT_EQ(result, 0x04) // 0b100 - only effect 2 remains
|
||||
<< "DisableEffectsMask=0x03 should leave only effect 2";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, DisableEffects0And2_LeavesEffect1)
|
||||
{
|
||||
uint32 disableMask = 0x05; // Disable effects 0 and 2
|
||||
|
||||
uint8 result = ProcChanceTestHelper::ApplyDisableEffectsMask(ALL_EFFECTS_MASK, disableMask);
|
||||
|
||||
EXPECT_EQ(result, 0x02) // 0b010 - only effect 1 remains
|
||||
<< "DisableEffectsMask=0x05 should leave only effect 1";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, DisableEffects1And2_LeavesEffect0)
|
||||
{
|
||||
uint32 disableMask = 0x06; // Disable effects 1 and 2
|
||||
|
||||
uint8 result = ProcChanceTestHelper::ApplyDisableEffectsMask(ALL_EFFECTS_MASK, disableMask);
|
||||
|
||||
EXPECT_EQ(result, 0x01) // 0b001 - only effect 0 remains
|
||||
<< "DisableEffectsMask=0x06 should leave only effect 0";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// All Effects Disabled - Proc Blocked
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, DisableAllEffects_BlocksProc)
|
||||
{
|
||||
uint32 disableMask = 0x07; // Disable all 3 effects
|
||||
|
||||
uint8 result = ProcChanceTestHelper::ApplyDisableEffectsMask(ALL_EFFECTS_MASK, disableMask);
|
||||
|
||||
EXPECT_EQ(result, 0x00)
|
||||
<< "DisableEffectsMask=0x07 should disable all effects";
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(ALL_EFFECTS_MASK, disableMask))
|
||||
<< "Proc should be blocked when all effects are disabled";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, NotAllDisabled_ProcAllowed)
|
||||
{
|
||||
// Only effect 0 disabled
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(ALL_EFFECTS_MASK, 0x01))
|
||||
<< "Proc should be allowed when some effects remain";
|
||||
|
||||
// Only effects 0 and 1 disabled
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(ALL_EFFECTS_MASK, 0x03))
|
||||
<< "Proc should be allowed when effect 2 remains";
|
||||
|
||||
// No effects disabled
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(ALL_EFFECTS_MASK, 0x00))
|
||||
<< "Proc should be allowed when no effects are disabled";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Partial Initial Mask Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, PartialInitialMask_Effect0Only)
|
||||
{
|
||||
uint8 initialMask = 0x01; // Only effect 0 enabled
|
||||
|
||||
// Disabling effect 0 should result in 0
|
||||
EXPECT_EQ(ProcChanceTestHelper::ApplyDisableEffectsMask(initialMask, 0x01), 0x00);
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(initialMask, 0x01));
|
||||
|
||||
// Disabling effect 1 should have no impact
|
||||
EXPECT_EQ(ProcChanceTestHelper::ApplyDisableEffectsMask(initialMask, 0x02), 0x01);
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(initialMask, 0x02));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, PartialInitialMask_Effects0And1)
|
||||
{
|
||||
uint8 initialMask = 0x03; // Effects 0 and 1 enabled
|
||||
|
||||
// Disabling both should result in 0
|
||||
EXPECT_EQ(ProcChanceTestHelper::ApplyDisableEffectsMask(initialMask, 0x03), 0x00);
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(initialMask, 0x03));
|
||||
|
||||
// Disabling only effect 0 should leave effect 1
|
||||
EXPECT_EQ(ProcChanceTestHelper::ApplyDisableEffectsMask(initialMask, 0x01), 0x02);
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(initialMask, 0x01));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Zero Disable Mask Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, ZeroDisableMask_NoEffectDisabled)
|
||||
{
|
||||
uint32 disableMask = 0x00; // Nothing disabled
|
||||
|
||||
uint8 result = ProcChanceTestHelper::ApplyDisableEffectsMask(ALL_EFFECTS_MASK, disableMask);
|
||||
|
||||
EXPECT_EQ(result, ALL_EFFECTS_MASK)
|
||||
<< "Zero DisableEffectsMask should leave all effects enabled";
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(ALL_EFFECTS_MASK, disableMask))
|
||||
<< "Proc should be allowed when nothing is disabled";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Higher Bits Ignored Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, HigherBits_IgnoredForEffects)
|
||||
{
|
||||
// Bits beyond 0x07 should be ignored (only 3 effects exist)
|
||||
uint32 disableMask = 0xFF; // All bits set
|
||||
|
||||
uint8 result = ProcChanceTestHelper::ApplyDisableEffectsMask(ALL_EFFECTS_MASK, disableMask);
|
||||
|
||||
EXPECT_EQ(result, 0x00)
|
||||
<< "Only lower 3 bits should affect the result";
|
||||
|
||||
// Only lower bits matter
|
||||
uint32 highBitsOnly = 0xF8; // High bits only
|
||||
result = ProcChanceTestHelper::ApplyDisableEffectsMask(ALL_EFFECTS_MASK, highBitsOnly);
|
||||
|
||||
EXPECT_EQ(result, ALL_EFFECTS_MASK)
|
||||
<< "High bits (0xF8) should not affect lower 3 effects";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Integration with SpellProcEntry Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, SpellProcEntry_WithDisableEffectsMask)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithDisableEffectsMask(0x05) // Disable effects 0 and 2
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Verify the mask was set correctly
|
||||
EXPECT_EQ(procEntry.DisableEffectsMask, 0x05u);
|
||||
|
||||
// Apply to initial mask
|
||||
uint8 result = ProcChanceTestHelper::ApplyDisableEffectsMask(ALL_EFFECTS_MASK, procEntry.DisableEffectsMask);
|
||||
|
||||
EXPECT_EQ(result, 0x02) // Only effect 1 remains
|
||||
<< "SpellProcEntry DisableEffectsMask should filter correctly";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, SpellProcEntry_AllDisabled)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithDisableEffectsMask(0x07) // Disable all effects
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(ALL_EFFECTS_MASK, procEntry.DisableEffectsMask))
|
||||
<< "Proc should be blocked when all effects disabled in SpellProcEntry";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real Spell Scenarios
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, Scenario_SingleEffectAura)
|
||||
{
|
||||
// Many procs only have a single effect that matters
|
||||
uint8 singleEffectMask = 0x01; // Only effect 0
|
||||
|
||||
// Disabling effect 0 blocks the proc
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(singleEffectMask, 0x01));
|
||||
|
||||
// Disabling other effects has no impact
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(singleEffectMask, 0x02));
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(singleEffectMask, 0x04));
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(singleEffectMask, 0x06));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcDisableEffectsTest, Scenario_DualEffectAura)
|
||||
{
|
||||
// Aura with effects 0 and 1 (healing + damage proc for example)
|
||||
uint8 dualEffectMask = 0x03;
|
||||
|
||||
// Disabling one effect leaves the other
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(dualEffectMask, 0x01));
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(dualEffectMask, 0x02));
|
||||
|
||||
// Disabling both blocks the proc
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToDisabledEffects(dualEffectMask, 0x03));
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
/*
|
||||
* 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 SpellProcEquipmentTest.cpp
|
||||
* @brief Unit tests for equipment requirement validation in proc system
|
||||
*
|
||||
* Tests the logic from SpellAuras.cpp:2260-2298:
|
||||
* - Weapon class requirement validation
|
||||
* - Armor class requirement validation
|
||||
* - Attack type to slot mapping
|
||||
* - Feral form blocking weapon procs
|
||||
* - Broken item blocking procs
|
||||
* - SPELL_ATTR3_NO_PROC_EQUIP_REQUIREMENT bypass
|
||||
* - Item subclass mask validation
|
||||
*
|
||||
* ============================================================================
|
||||
* TEST DESIGN: Configuration-Based Testing
|
||||
* ============================================================================
|
||||
*
|
||||
* These tests use EquipmentConfig structs to simulate different equipment
|
||||
* scenarios without requiring actual game objects. Each test configures:
|
||||
* - isPassive: Whether the aura is passive (equipment check only applies to passive)
|
||||
* - isPlayer: Whether the target is a player (NPCs skip equipment checks)
|
||||
* - equippedItemClass: ITEM_CLASS_WEAPON, ITEM_CLASS_ARMOR, or ITEM_CLASS_ANY
|
||||
* - hasEquippedItem: Whether the required item slot has an item
|
||||
* - itemIsBroken: Whether the equipped item is broken (0 durability)
|
||||
* - itemFitsRequirements: Whether the item matches subclass mask requirements
|
||||
* - isInFeralForm: Whether a druid is in cat/bear form (blocks weapon procs)
|
||||
* - hasNoEquipRequirementAttr: SPELL_ATTR3_NO_PROC_EQUIP_REQUIREMENT bypass
|
||||
*
|
||||
* No GTEST_SKIP() is used in this file - all tests run with their configured
|
||||
* scenarios, testing both positive and negative cases explicitly.
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
class SpellProcEquipmentTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
|
||||
// Create default config for weapon proc
|
||||
ProcChanceTestHelper::EquipmentConfig CreateWeaponProcConfig()
|
||||
{
|
||||
ProcChanceTestHelper::EquipmentConfig config;
|
||||
config.isPassive = true;
|
||||
config.isPlayer = true;
|
||||
config.equippedItemClass = ProcChanceTestHelper::ITEM_CLASS_WEAPON;
|
||||
config.hasEquippedItem = true;
|
||||
config.itemIsBroken = false;
|
||||
config.itemFitsRequirements = true;
|
||||
return config;
|
||||
}
|
||||
|
||||
// Create default config for armor proc
|
||||
ProcChanceTestHelper::EquipmentConfig CreateArmorProcConfig()
|
||||
{
|
||||
ProcChanceTestHelper::EquipmentConfig config;
|
||||
config.isPassive = true;
|
||||
config.isPlayer = true;
|
||||
config.equippedItemClass = ProcChanceTestHelper::ITEM_CLASS_ARMOR;
|
||||
config.hasEquippedItem = true;
|
||||
config.itemIsBroken = false;
|
||||
config.itemFitsRequirements = true;
|
||||
return config;
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// No Equipment Requirement Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, NoEquipRequirement_AllowsProc)
|
||||
{
|
||||
ProcChanceTestHelper::EquipmentConfig config;
|
||||
config.isPassive = true;
|
||||
config.isPlayer = true;
|
||||
config.equippedItemClass = ProcChanceTestHelper::ITEM_CLASS_ANY; // No requirement
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "No equipment requirement should allow proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, NonPassiveAura_SkipsCheck)
|
||||
{
|
||||
ProcChanceTestHelper::EquipmentConfig config;
|
||||
config.isPassive = false; // Not a passive aura
|
||||
config.isPlayer = true;
|
||||
config.equippedItemClass = ProcChanceTestHelper::ITEM_CLASS_WEAPON;
|
||||
config.hasEquippedItem = false; // Would normally block
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Non-passive aura should skip equipment check";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, NonPlayerTarget_SkipsCheck)
|
||||
{
|
||||
ProcChanceTestHelper::EquipmentConfig config;
|
||||
config.isPassive = true;
|
||||
config.isPlayer = false; // NPC/creature
|
||||
config.equippedItemClass = ProcChanceTestHelper::ITEM_CLASS_WEAPON;
|
||||
config.hasEquippedItem = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Non-player target should skip equipment check";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Weapon Class Requirement Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, WeaponRequired_WithWeapon_AllowsProc)
|
||||
{
|
||||
auto config = CreateWeaponProcConfig();
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Weapon requirement met should allow proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, WeaponRequired_NoWeapon_BlocksProc)
|
||||
{
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.hasEquippedItem = false; // No weapon equipped
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Missing weapon should block proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, WeaponRequired_BrokenWeapon_BlocksProc)
|
||||
{
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.itemIsBroken = true; // Weapon is broken
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Broken weapon should block proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, WeaponRequired_WrongSubclass_BlocksProc)
|
||||
{
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.itemFitsRequirements = false; // Wrong weapon type
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Wrong weapon subclass should block proc";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Armor Class Requirement Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, ArmorRequired_WithArmor_AllowsProc)
|
||||
{
|
||||
auto config = CreateArmorProcConfig();
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Armor requirement met should allow proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, ArmorRequired_NoArmor_BlocksProc)
|
||||
{
|
||||
auto config = CreateArmorProcConfig();
|
||||
config.hasEquippedItem = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Missing armor should block proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, ArmorRequired_BrokenArmor_BlocksProc)
|
||||
{
|
||||
auto config = CreateArmorProcConfig();
|
||||
config.itemIsBroken = true;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Broken armor should block proc";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Feral Form Tests - SpellAuras.cpp:2266-2267
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, FeralForm_WeaponProc_BlocksProc)
|
||||
{
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.isInFeralForm = true; // Druid in cat/bear form
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Feral form should block weapon procs";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, FeralForm_ArmorProc_AllowsProc)
|
||||
{
|
||||
auto config = CreateArmorProcConfig();
|
||||
config.isInFeralForm = true; // Druid in cat/bear form
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Feral form should NOT block armor procs";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, NotInFeralForm_WeaponProc_AllowsProc)
|
||||
{
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.isInFeralForm = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Non-feral form should allow weapon procs";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SPELL_ATTR3_NO_PROC_EQUIP_REQUIREMENT Bypass Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, NoEquipRequirementAttr_BypassesMissingItem)
|
||||
{
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.hasEquippedItem = false; // Would normally block
|
||||
config.hasNoEquipRequirementAttr = true;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "NO_PROC_EQUIP_REQUIREMENT should bypass missing item check";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, NoEquipRequirementAttr_BypassesBrokenItem)
|
||||
{
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.itemIsBroken = true; // Would normally block
|
||||
config.hasNoEquipRequirementAttr = true;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "NO_PROC_EQUIP_REQUIREMENT should bypass broken item check";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, NoEquipRequirementAttr_BypassesFeralForm)
|
||||
{
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.isInFeralForm = true; // Would normally block
|
||||
config.hasNoEquipRequirementAttr = true;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "NO_PROC_EQUIP_REQUIREMENT should bypass feral form check";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Attack Type to Slot Mapping Tests - SpellAuras.cpp:2268-2284
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, SlotMapping_BaseAttack_MainHand)
|
||||
{
|
||||
uint8 slot = ProcChanceTestHelper::GetWeaponSlotForAttackType(ProcChanceTestHelper::BASE_ATTACK);
|
||||
EXPECT_EQ(slot, 15) // EQUIPMENT_SLOT_MAINHAND
|
||||
<< "BASE_ATTACK should map to main hand slot";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, SlotMapping_OffAttack_OffHand)
|
||||
{
|
||||
uint8 slot = ProcChanceTestHelper::GetWeaponSlotForAttackType(ProcChanceTestHelper::OFF_ATTACK);
|
||||
EXPECT_EQ(slot, 16) // EQUIPMENT_SLOT_OFFHAND
|
||||
<< "OFF_ATTACK should map to off hand slot";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, SlotMapping_RangedAttack_Ranged)
|
||||
{
|
||||
uint8 slot = ProcChanceTestHelper::GetWeaponSlotForAttackType(ProcChanceTestHelper::RANGED_ATTACK);
|
||||
EXPECT_EQ(slot, 17) // EQUIPMENT_SLOT_RANGED
|
||||
<< "RANGED_ATTACK should map to ranged slot";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, SlotMapping_InvalidAttack_DefaultsToMainHand)
|
||||
{
|
||||
uint8 slot = ProcChanceTestHelper::GetWeaponSlotForAttackType(255); // Invalid
|
||||
EXPECT_EQ(slot, 15) // EQUIPMENT_SLOT_MAINHAND
|
||||
<< "Invalid attack type should default to main hand";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real Spell Scenarios
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, Scenario_WeaponEnchant_Fiery)
|
||||
{
|
||||
// Fiery Weapon enchant - requires melee weapon
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.attackType = ProcChanceTestHelper::BASE_ATTACK;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Fiery Weapon with main hand should proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, Scenario_WeaponEnchant_FieryOffhand)
|
||||
{
|
||||
// Fiery Weapon on off-hand
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.attackType = ProcChanceTestHelper::OFF_ATTACK;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Fiery Weapon with off hand should proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, Scenario_Hunter_RangedProc)
|
||||
{
|
||||
// Hunter ranged weapon proc
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.attackType = ProcChanceTestHelper::RANGED_ATTACK;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Ranged proc with ranged weapon should work";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, Scenario_FeralDruid_WeaponEnchant)
|
||||
{
|
||||
// Druid with weapon enchant enters cat form
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.isInFeralForm = true;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Feral druid weapon enchant should be blocked";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, Scenario_BrokenWeapon_CombatUse)
|
||||
{
|
||||
// Player's weapon breaks during combat
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.itemIsBroken = true;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Broken weapon procs should be blocked";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, Scenario_WrongWeaponType)
|
||||
{
|
||||
// Enchant requires sword but player has mace
|
||||
auto config = CreateWeaponProcConfig();
|
||||
config.itemFitsRequirements = false;
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Wrong weapon type should block proc";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Edge Cases
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, EdgeCase_AllConditionsMet)
|
||||
{
|
||||
auto config = CreateWeaponProcConfig();
|
||||
// All requirements met
|
||||
config.isPassive = true;
|
||||
config.isPlayer = true;
|
||||
config.hasEquippedItem = true;
|
||||
config.itemIsBroken = false;
|
||||
config.itemFitsRequirements = true;
|
||||
config.isInFeralForm = false;
|
||||
config.hasNoEquipRequirementAttr = false;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "All conditions met should allow proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, EdgeCase_AllBlockingConditions)
|
||||
{
|
||||
auto config = CreateWeaponProcConfig();
|
||||
// Multiple blocking conditions
|
||||
config.hasEquippedItem = false;
|
||||
config.itemIsBroken = true;
|
||||
config.itemFitsRequirements = false;
|
||||
config.isInFeralForm = true;
|
||||
|
||||
// Should be blocked (first check that fails)
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Multiple blocking conditions should still block";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcEquipmentTest, EdgeCase_BypassOverridesAll)
|
||||
{
|
||||
auto config = CreateWeaponProcConfig();
|
||||
// Multiple blocking conditions BUT bypass is set
|
||||
config.hasEquippedItem = false;
|
||||
config.itemIsBroken = true;
|
||||
config.itemFitsRequirements = false;
|
||||
config.isInFeralForm = true;
|
||||
config.hasNoEquipRequirementAttr = true; // Bypass
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockDueToEquipment(config))
|
||||
<< "Bypass attribute should override all blocking conditions";
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
/*
|
||||
* 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 SpellProcFullCoverageTest.cpp
|
||||
* @brief Data-driven tests for ALL 869 spell_proc entries
|
||||
*
|
||||
* Tests proc calculations for every spell_proc entry:
|
||||
* - Cooldown blocking behavior
|
||||
* - Chance calculation with level reduction
|
||||
* - Attribute flag validation
|
||||
*
|
||||
* This complements SpellProcDataDrivenTest.cpp which tests CanSpellTriggerProcOnEvent().
|
||||
*
|
||||
* ============================================================================
|
||||
* DESIGN NOTE: Why Tests Skip Certain Entries
|
||||
* ============================================================================
|
||||
*
|
||||
* This test file uses parameterized tests that run against ALL 869 spell_proc
|
||||
* entries. Each test validates a specific feature (cooldowns, level reduction,
|
||||
* attribute flags, etc.). Tests use GTEST_SKIP() for entries that don't have
|
||||
* the feature being tested.
|
||||
*
|
||||
* For example (current counts from test output):
|
||||
* - CooldownBlocking_WhenCooldownSet: Tests 246 entries with Cooldown > 0 (skips 623)
|
||||
* - Level60Reduction_WhenAttributeSet: Tests entries with PROC_ATTR_REDUCE_PROC_60 (0 currently)
|
||||
* - UseStacksForCharges_Behavior: Tests entries with PROC_ATTR_USE_STACKS_FOR_CHARGES (0 currently)
|
||||
* - TriggeredCanProc_FlagSet: Tests 73 entries with PROC_ATTR_TRIGGERED_CAN_PROC (skips 796)
|
||||
* - ReqManaCost_FlagSet: Tests 5 entries with PROC_ATTR_REQ_MANA_COST (skips 864)
|
||||
*
|
||||
* This is INTENTIONAL. Running parameterized tests against all entries ensures:
|
||||
* 1. Every entry is validated for applicable features
|
||||
* 2. Statistics show exact coverage (X entries with feature Y)
|
||||
* 3. New entries added to spell_proc are automatically tested
|
||||
* 4. Regression detection if an entry unexpectedly gains/loses a feature
|
||||
*
|
||||
* The statistics tests at the bottom output the exact counts:
|
||||
* "[ INFO ] Entries with cooldown: 85 / 869"
|
||||
* "[ INFO ] Entries with REDUCE_PROC_60: 15 / 869"
|
||||
* etc.
|
||||
*
|
||||
* SKIPPED tests are expected and correct. Each skip message includes:
|
||||
* - The SpellId being skipped
|
||||
* - The reason (e.g., "has no cooldown", "doesn't have REDUCE_PROC_60")
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "SpellProcTestData.h"
|
||||
#include "AuraStub.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// =============================================================================
|
||||
// Parameterized Test Fixture for ALL Entries
|
||||
// =============================================================================
|
||||
|
||||
class SpellProcFullCoverageTest : public ::testing::TestWithParam<SpellProcTestEntry>
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_entry = GetParam();
|
||||
_procEntry = _entry.ToSpellProcEntry();
|
||||
}
|
||||
|
||||
SpellProcTestEntry _entry;
|
||||
SpellProcEntry _procEntry;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Cooldown Tests - ALL entries with Cooldown > 0
|
||||
// 246 of 869 entries have cooldowns (Internal Cooldowns / ICDs)
|
||||
// =============================================================================
|
||||
|
||||
TEST_P(SpellProcFullCoverageTest, CooldownBlocking_WhenCooldownSet)
|
||||
{
|
||||
// SKIP REASON: This test validates cooldown blocking behavior.
|
||||
// Only entries with Cooldown > 0 can be tested for ICD (Internal Cooldown).
|
||||
// Entries without cooldowns proc on every valid trigger, so there's nothing
|
||||
// to test here. The skip count shows how many entries lack cooldowns.
|
||||
if (_entry.Cooldown == 0)
|
||||
GTEST_SKIP() << "SpellId " << _entry.SpellId << " has no cooldown";
|
||||
|
||||
ProcTestScenario scenario;
|
||||
scenario.WithAura(std::abs(_entry.SpellId));
|
||||
|
||||
// Set 100% chance to isolate cooldown testing
|
||||
SpellProcEntry testEntry = _procEntry;
|
||||
testEntry.Chance = 100.0f;
|
||||
testEntry.Cooldown = Milliseconds(_entry.Cooldown);
|
||||
|
||||
// First proc should succeed
|
||||
EXPECT_TRUE(scenario.SimulateProc(testEntry))
|
||||
<< "SpellId " << _entry.SpellId << " first proc should succeed";
|
||||
|
||||
// Second proc immediately after should fail (on cooldown)
|
||||
EXPECT_FALSE(scenario.SimulateProc(testEntry))
|
||||
<< "SpellId " << _entry.SpellId << " should be blocked during "
|
||||
<< _entry.Cooldown << "ms cooldown";
|
||||
|
||||
// Wait for cooldown to expire
|
||||
scenario.AdvanceTime(std::chrono::milliseconds(_entry.Cooldown + 1));
|
||||
|
||||
// Third proc after cooldown should succeed
|
||||
EXPECT_TRUE(scenario.SimulateProc(testEntry))
|
||||
<< "SpellId " << _entry.SpellId << " should proc after cooldown expires";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Level 60+ Reduction Tests - ALL entries with PROC_ATTR_REDUCE_PROC_60
|
||||
// Currently 0 of 869 entries use this attribute (data may need population).
|
||||
// This attribute reduces proc chance by 3.333% per level above 60.
|
||||
// =============================================================================
|
||||
|
||||
TEST_P(SpellProcFullCoverageTest, Level60Reduction_WhenAttributeSet)
|
||||
{
|
||||
// SKIP REASON: This test validates the level 60+ proc chance reduction formula.
|
||||
// Only entries with PROC_ATTR_REDUCE_PROC_60 attribute have their proc chance
|
||||
// reduced at higher levels. Spells like old weapon procs (Fiery, Crusader)
|
||||
// use this to prevent them from being overpowered at level 80.
|
||||
// Entries without this attribute maintain constant proc chance at all levels.
|
||||
if (!(_entry.AttributesMask & PROC_ATTR_REDUCE_PROC_60))
|
||||
GTEST_SKIP() << "SpellId " << _entry.SpellId << " doesn't have REDUCE_PROC_60";
|
||||
|
||||
// Use a meaningful base chance for testing
|
||||
float baseChance = _entry.Chance > 0 ? _entry.Chance : 30.0f;
|
||||
|
||||
// Level 60: No reduction
|
||||
float chanceAt60 = ProcChanceTestHelper::ApplyLevel60Reduction(baseChance, 60);
|
||||
EXPECT_NEAR(chanceAt60, baseChance, 0.01f)
|
||||
<< "SpellId " << _entry.SpellId << " should have no reduction at level 60";
|
||||
|
||||
// Level 70: 33.33% reduction
|
||||
float chanceAt70 = ProcChanceTestHelper::ApplyLevel60Reduction(baseChance, 70);
|
||||
float expectedAt70 = baseChance * (1.0f - 10.0f/30.0f);
|
||||
EXPECT_NEAR(chanceAt70, expectedAt70, 0.5f)
|
||||
<< "SpellId " << _entry.SpellId << " should have 33% reduction at level 70";
|
||||
|
||||
// Level 80: 66.67% reduction
|
||||
float chanceAt80 = ProcChanceTestHelper::ApplyLevel60Reduction(baseChance, 80);
|
||||
float expectedAt80 = baseChance * (1.0f - 20.0f/30.0f);
|
||||
EXPECT_NEAR(chanceAt80, expectedAt80, 0.5f)
|
||||
<< "SpellId " << _entry.SpellId << " should have 66% reduction at level 80";
|
||||
|
||||
// Verify reduction is correct
|
||||
EXPECT_LT(chanceAt80, chanceAt70)
|
||||
<< "SpellId " << _entry.SpellId << " chance at 80 should be less than at 70";
|
||||
EXPECT_LT(chanceAt70, chanceAt60)
|
||||
<< "SpellId " << _entry.SpellId << " chance at 70 should be less than at 60";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Attribute Validation Tests - ALL entries
|
||||
// =============================================================================
|
||||
|
||||
TEST_P(SpellProcFullCoverageTest, AttributeMask_ValidFlags)
|
||||
{
|
||||
// Valid attribute flags
|
||||
constexpr uint32 VALID_ATTRIBUTE_MASK =
|
||||
PROC_ATTR_REQ_EXP_OR_HONOR |
|
||||
PROC_ATTR_TRIGGERED_CAN_PROC |
|
||||
PROC_ATTR_REQ_MANA_COST |
|
||||
PROC_ATTR_REQ_SPELLMOD |
|
||||
PROC_ATTR_USE_STACKS_FOR_CHARGES |
|
||||
PROC_ATTR_REDUCE_PROC_60 |
|
||||
PROC_ATTR_CANT_PROC_FROM_ITEM_CAST;
|
||||
|
||||
// Check for invalid bits (skip 0x20 and 0x40 which are unused/reserved)
|
||||
uint32 invalidBits = _entry.AttributesMask & ~VALID_ATTRIBUTE_MASK & ~0x60;
|
||||
EXPECT_EQ(invalidBits, 0u)
|
||||
<< "SpellId " << _entry.SpellId << " has invalid attribute bits: 0x"
|
||||
<< std::hex << invalidBits;
|
||||
}
|
||||
|
||||
TEST_P(SpellProcFullCoverageTest, UseStacksForCharges_Behavior)
|
||||
{
|
||||
// SKIP REASON: This test validates stack consumption instead of charge consumption.
|
||||
// Currently 0 entries use PROC_ATTR_USE_STACKS_FOR_CHARGES (attribute data may
|
||||
// need population). When set, this causes procs to decrement the aura's stack
|
||||
// count rather than its charge count.
|
||||
// Example: Druid's Eclipse - each proc reduces stacks until buff expires.
|
||||
// Most proc auras use charges (consumed individually) not stacks.
|
||||
if (!(_entry.AttributesMask & PROC_ATTR_USE_STACKS_FOR_CHARGES))
|
||||
GTEST_SKIP() << "SpellId " << _entry.SpellId << " doesn't use stacks for charges";
|
||||
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(std::abs(_entry.SpellId))
|
||||
.WithStackAmount(5)
|
||||
.Build();
|
||||
|
||||
SpellProcEntry testEntry = _procEntry;
|
||||
testEntry.Chance = 100.0f;
|
||||
|
||||
// Consume should decrement stacks
|
||||
bool removed = ProcChanceTestHelper::SimulateConsumeProcCharges(aura.get(), testEntry);
|
||||
|
||||
EXPECT_EQ(aura->GetStackAmount(), 4)
|
||||
<< "SpellId " << _entry.SpellId << " should decrement stacks";
|
||||
EXPECT_FALSE(removed);
|
||||
}
|
||||
|
||||
TEST_P(SpellProcFullCoverageTest, TriggeredCanProc_FlagSet)
|
||||
{
|
||||
// SKIP REASON: This test validates the PROC_ATTR_TRIGGERED_CAN_PROC attribute.
|
||||
// Most proc auras (796 entries) do NOT allow triggered spells to trigger them,
|
||||
// preventing infinite proc chains. Only 73 entries explicitly allow triggered
|
||||
// spells to proc (e.g., some talent effects that should chain-react).
|
||||
// Entries without this flag block triggered spell procs for safety.
|
||||
if (!(_entry.AttributesMask & PROC_ATTR_TRIGGERED_CAN_PROC))
|
||||
GTEST_SKIP() << "SpellId " << _entry.SpellId << " doesn't have TRIGGERED_CAN_PROC";
|
||||
|
||||
// Just verify the flag is properly set in the entry
|
||||
EXPECT_TRUE(_procEntry.AttributesMask & PROC_ATTR_TRIGGERED_CAN_PROC)
|
||||
<< "SpellId " << _entry.SpellId << " TRIGGERED_CAN_PROC should be set";
|
||||
}
|
||||
|
||||
TEST_P(SpellProcFullCoverageTest, ReqManaCost_FlagSet)
|
||||
{
|
||||
// SKIP REASON: This test validates the PROC_ATTR_REQ_MANA_COST attribute.
|
||||
// Only 5 entries require the triggering spell to have a mana cost.
|
||||
// This prevents free spells (instant casts with no cost) from triggering procs.
|
||||
// Example: Illumination should only proc from actual heals, not free procs.
|
||||
// 864 entries don't care about mana cost, so this test is skipped for them.
|
||||
if (!(_entry.AttributesMask & PROC_ATTR_REQ_MANA_COST))
|
||||
GTEST_SKIP() << "SpellId " << _entry.SpellId << " doesn't have REQ_MANA_COST";
|
||||
|
||||
// Just verify the flag is properly set in the entry
|
||||
EXPECT_TRUE(_procEntry.AttributesMask & PROC_ATTR_REQ_MANA_COST)
|
||||
<< "SpellId " << _entry.SpellId << " REQ_MANA_COST should be set";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Chance Calculation Tests - ALL entries with Chance > 0
|
||||
// =============================================================================
|
||||
|
||||
TEST_P(SpellProcFullCoverageTest, ChanceValue_InValidRange)
|
||||
{
|
||||
// Chance should be in valid range (0-100 normally, but some can exceed)
|
||||
// Just verify it's not negative
|
||||
EXPECT_GE(_entry.Chance, 0.0f)
|
||||
<< "SpellId " << _entry.SpellId << " has negative chance";
|
||||
|
||||
// And not absurdly high (>500% would be suspicious)
|
||||
EXPECT_LE(_entry.Chance, 500.0f)
|
||||
<< "SpellId " << _entry.SpellId << " has suspiciously high chance";
|
||||
}
|
||||
|
||||
TEST_P(SpellProcFullCoverageTest, ChanceCalculation_WithEntry)
|
||||
{
|
||||
// SKIP REASON: This test validates proc chance calculation with level reduction.
|
||||
// Entries with Chance = 0 rely on DBC defaults or use PPM (procs per minute) instead.
|
||||
// We can only test explicit chance calculation for entries that define a Chance value.
|
||||
// PPM-based procs are tested separately in SpellProcPPMTest.cpp.
|
||||
if (_entry.Chance <= 0.0f)
|
||||
GTEST_SKIP() << "SpellId " << _entry.SpellId << " has no base chance";
|
||||
|
||||
// Calculate chance at level 80 (typical max level)
|
||||
float calculatedChance = ProcChanceTestHelper::SimulateCalcProcChance(
|
||||
_procEntry, 80, 2500, 0.0f, 0.0f, false);
|
||||
|
||||
if (_entry.AttributesMask & PROC_ATTR_REDUCE_PROC_60)
|
||||
{
|
||||
// With level 60+ reduction at level 80
|
||||
float expectedReduced = _entry.Chance * (1.0f - 20.0f/30.0f);
|
||||
EXPECT_NEAR(calculatedChance, expectedReduced, 0.5f)
|
||||
<< "SpellId " << _entry.SpellId << " reduced chance mismatch";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Without reduction
|
||||
EXPECT_NEAR(calculatedChance, _entry.Chance, 0.01f)
|
||||
<< "SpellId " << _entry.SpellId << " base chance mismatch";
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ProcFlags Validation Tests - ALL entries
|
||||
// =============================================================================
|
||||
|
||||
TEST_P(SpellProcFullCoverageTest, ProcFlags_NotEmpty)
|
||||
{
|
||||
// Most entries should have proc flags OR spell family filters
|
||||
// Skip validation if both are zero (some entries use only SchoolMask)
|
||||
if (_entry.ProcFlags == 0 && _entry.SpellFamilyName == 0 && _entry.SchoolMask == 0)
|
||||
{
|
||||
// This is a potential configuration issue, but not necessarily an error
|
||||
// Some entries are passive effects that don't proc from events
|
||||
}
|
||||
|
||||
// Just verify ProcFlags is valid (no invalid bits)
|
||||
// All valid proc flags are defined in SpellMgr.h
|
||||
// This is a basic sanity check
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Cooldown Value Validation Tests - ALL entries with cooldown
|
||||
// =============================================================================
|
||||
|
||||
TEST_P(SpellProcFullCoverageTest, CooldownValue_Reasonable)
|
||||
{
|
||||
// SKIP REASON: This test validates cooldown values are within reasonable bounds.
|
||||
// Entries without cooldowns (Cooldown = 0) can proc on every trigger with no
|
||||
// internal cooldown. 623 entries have no ICD and this is intentional - they
|
||||
// rely on proc chance alone to limit frequency.
|
||||
// Only 246 entries with explicit cooldowns need range validation.
|
||||
if (_entry.Cooldown == 0)
|
||||
GTEST_SKIP() << "SpellId " << _entry.SpellId << " has no cooldown";
|
||||
|
||||
// Cooldowns should be reasonable (not too short, not too long)
|
||||
// Shortest reasonable cooldown is ~1ms
|
||||
// Longest reasonable cooldown is ~15 minutes (900000ms) - some trinkets have 10+ min ICDs
|
||||
EXPECT_GE(_entry.Cooldown, 1u)
|
||||
<< "SpellId " << _entry.SpellId << " has suspiciously short cooldown";
|
||||
EXPECT_LE(_entry.Cooldown, 900000u)
|
||||
<< "SpellId " << _entry.SpellId << " has suspiciously long cooldown ("
|
||||
<< _entry.Cooldown << "ms = " << _entry.Cooldown/60000 << " minutes)";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SpellId Validation Tests - ALL entries
|
||||
// =============================================================================
|
||||
|
||||
TEST_P(SpellProcFullCoverageTest, SpellId_NonZero)
|
||||
{
|
||||
// SpellId should never be zero
|
||||
EXPECT_NE(_entry.SpellId, 0)
|
||||
<< "Entry has zero SpellId which is invalid";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test Instantiation - ALL 869 entries
|
||||
// =============================================================================
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
AllSpellProcEntries,
|
||||
SpellProcFullCoverageTest,
|
||||
::testing::ValuesIn(GetAllSpellProcTestEntries()),
|
||||
[](const ::testing::TestParamInfo<SpellProcTestEntry>& info) {
|
||||
// Generate unique test name from spell ID
|
||||
int32_t id = info.param.SpellId;
|
||||
if (id < 0)
|
||||
return "NegId_" + std::to_string(-id);
|
||||
return "SpellId_" + std::to_string(id);
|
||||
}
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// Statistics Tests - Run once to summarize coverage
|
||||
// =============================================================================
|
||||
|
||||
class SpellProcCoverageStatsTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_allEntries = GetAllSpellProcTestEntries();
|
||||
}
|
||||
|
||||
std::vector<SpellProcTestEntry> _allEntries;
|
||||
};
|
||||
|
||||
TEST_F(SpellProcCoverageStatsTest, CountEntriesWithCooldown)
|
||||
{
|
||||
size_t withCooldown = 0;
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (entry.Cooldown > 0)
|
||||
++withCooldown;
|
||||
}
|
||||
std::cout << "[ INFO ] Entries with cooldown: " << withCooldown
|
||||
<< " / " << _allEntries.size() << std::endl;
|
||||
EXPECT_GT(withCooldown, 0u);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcCoverageStatsTest, CountEntriesWithChance)
|
||||
{
|
||||
size_t withChance = 0;
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (entry.Chance > 0.0f)
|
||||
++withChance;
|
||||
}
|
||||
std::cout << "[ INFO ] Entries with chance > 0: " << withChance
|
||||
<< " / " << _allEntries.size() << std::endl;
|
||||
}
|
||||
|
||||
TEST_F(SpellProcCoverageStatsTest, CountEntriesWithLevel60Reduction)
|
||||
{
|
||||
size_t withReduction = 0;
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (entry.AttributesMask & PROC_ATTR_REDUCE_PROC_60)
|
||||
++withReduction;
|
||||
}
|
||||
std::cout << "[ INFO ] Entries with REDUCE_PROC_60: " << withReduction
|
||||
<< " / " << _allEntries.size() << std::endl;
|
||||
}
|
||||
|
||||
TEST_F(SpellProcCoverageStatsTest, CountEntriesWithUseStacks)
|
||||
{
|
||||
size_t withUseStacks = 0;
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (entry.AttributesMask & PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
++withUseStacks;
|
||||
}
|
||||
std::cout << "[ INFO ] Entries with USE_STACKS_FOR_CHARGES: " << withUseStacks
|
||||
<< " / " << _allEntries.size() << std::endl;
|
||||
}
|
||||
|
||||
TEST_F(SpellProcCoverageStatsTest, CountEntriesWithTriggeredCanProc)
|
||||
{
|
||||
size_t withTriggered = 0;
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (entry.AttributesMask & PROC_ATTR_TRIGGERED_CAN_PROC)
|
||||
++withTriggered;
|
||||
}
|
||||
std::cout << "[ INFO ] Entries with TRIGGERED_CAN_PROC: " << withTriggered
|
||||
<< " / " << _allEntries.size() << std::endl;
|
||||
}
|
||||
|
||||
TEST_F(SpellProcCoverageStatsTest, CountEntriesWithReqManaCost)
|
||||
{
|
||||
size_t withReqManaCost = 0;
|
||||
for (auto const& entry : _allEntries)
|
||||
{
|
||||
if (entry.AttributesMask & PROC_ATTR_REQ_MANA_COST)
|
||||
++withReqManaCost;
|
||||
}
|
||||
std::cout << "[ INFO ] Entries with REQ_MANA_COST: " << withReqManaCost
|
||||
<< " / " << _allEntries.size() << std::endl;
|
||||
}
|
||||
|
||||
TEST_F(SpellProcCoverageStatsTest, TotalEntryCount)
|
||||
{
|
||||
std::cout << "[ INFO ] Total spell_proc entries tested: " << _allEntries.size() << std::endl;
|
||||
EXPECT_EQ(_allEntries.size(), 869u)
|
||||
<< "Expected 869 entries but got " << _allEntries.size();
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "AuraScriptTestFramework.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "gmock/gmock.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
/**
|
||||
* @brief Integration tests for the proc system
|
||||
*
|
||||
* These tests verify that the proc system correctly integrates:
|
||||
* - SpellProcEntry configuration
|
||||
* - CanSpellTriggerProcOnEvent logic
|
||||
* - Proc flag combinations
|
||||
* - Spell family matching
|
||||
* - Hit mask filtering
|
||||
*/
|
||||
class SpellProcIntegrationTest : public AuraScriptProcTestFixture
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
AuraScriptProcTestFixture::SetUp();
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Melee Attack Proc Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, MeleeAutoAttackProc_NormalHit)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(PROC_HIT_NORMAL | PROC_HIT_CRITICAL)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnMeleeAutoAttack()
|
||||
.WithNormalHit();
|
||||
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, MeleeAutoAttackProc_CritOnly)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(PROC_HIT_CRITICAL)
|
||||
.Build();
|
||||
|
||||
// Normal hit should NOT trigger crit-only proc
|
||||
auto normalScenario = ProcScenarioBuilder()
|
||||
.OnMeleeAutoAttack()
|
||||
.WithNormalHit();
|
||||
EXPECT_PROC_DOES_NOT_TRIGGER(procEntry, normalScenario);
|
||||
|
||||
// Critical hit should trigger
|
||||
auto critScenario = ProcScenarioBuilder()
|
||||
.OnMeleeAutoAttack()
|
||||
.WithCrit();
|
||||
EXPECT_PROC_TRIGGERS(procEntry, critScenario);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, MeleeAutoAttackProc_Miss)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(PROC_HIT_MISS)
|
||||
.Build();
|
||||
|
||||
auto missScenario = ProcScenarioBuilder()
|
||||
.OnMeleeAutoAttack()
|
||||
.WithMiss();
|
||||
|
||||
EXPECT_PROC_TRIGGERS(procEntry, missScenario);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Spell Damage Proc Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, SpellDamageProc_OnHit)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnSpellDamage()
|
||||
.OnHit()
|
||||
.WithNormalHit();
|
||||
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, SpellDamageProc_OnCast)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
|
||||
.Build();
|
||||
|
||||
// Should trigger on cast phase
|
||||
auto castScenario = ProcScenarioBuilder()
|
||||
.OnSpellDamage()
|
||||
.OnCast();
|
||||
EXPECT_PROC_TRIGGERS(procEntry, castScenario);
|
||||
|
||||
// Should NOT trigger on hit phase when configured for cast only
|
||||
auto hitScenario = ProcScenarioBuilder()
|
||||
.OnSpellDamage()
|
||||
.OnHit();
|
||||
EXPECT_PROC_DOES_NOT_TRIGGER(procEntry, hitScenario);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Heal Proc Tests
|
||||
// =============================================================================
|
||||
|
||||
// Heal proc tests - require SpellPhaseMask to be set
|
||||
TEST_F(SpellProcIntegrationTest, HealProc_OnHeal)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_HEAL)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnHeal()
|
||||
.OnHit();
|
||||
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, HealProc_CritHeal)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_HEAL)
|
||||
.WithHitMask(PROC_HIT_CRITICAL)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
// Normal heal should NOT trigger crit-only proc
|
||||
auto normalScenario = ProcScenarioBuilder()
|
||||
.OnHeal()
|
||||
.WithNormalHit();
|
||||
EXPECT_PROC_DOES_NOT_TRIGGER(procEntry, normalScenario);
|
||||
|
||||
// Crit heal should trigger
|
||||
auto critScenario = ProcScenarioBuilder()
|
||||
.OnHeal()
|
||||
.WithCrit();
|
||||
EXPECT_PROC_TRIGGERS(procEntry, critScenario);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Periodic Effect Proc Tests
|
||||
// =============================================================================
|
||||
|
||||
// Periodic proc tests - spell procs that require SpellPhaseMask to be set
|
||||
TEST_F(SpellProcIntegrationTest, PeriodicDamageProc)
|
||||
{
|
||||
// Note: PROC_FLAG_DONE_PERIODIC is in REQ_SPELL_PHASE_PROC_FLAG_MASK,
|
||||
// so SpellPhaseMask must be set (can't be 0)
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_PERIODIC)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnPeriodicDamage()
|
||||
.WithNormalHit();
|
||||
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, PeriodicHealProc)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_PERIODIC)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_HEAL)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnPeriodicHeal()
|
||||
.WithNormalHit();
|
||||
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Kill/Death Proc Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, KillProc)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_KILL)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnKill();
|
||||
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, DeathProc)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DEATH)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnDeath();
|
||||
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Defensive Proc Tests (Dodge/Parry/Block)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, DodgeProc)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(PROC_HIT_DODGE)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnTakenMeleeAutoAttack()
|
||||
.WithDodge();
|
||||
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, ParryProc)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(PROC_HIT_PARRY)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnTakenMeleeAutoAttack()
|
||||
.WithParry();
|
||||
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, BlockProc)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(PROC_HIT_BLOCK)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnTakenMeleeAutoAttack()
|
||||
.WithBlock();
|
||||
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, FullBlockProc)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(PROC_HIT_FULL_BLOCK)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnTakenMeleeAutoAttack()
|
||||
.WithFullBlock();
|
||||
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Absorb Proc Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, AbsorbProc)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_TAKEN_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithHitMask(PROC_HIT_ABSORB)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnTakenSpellDamage()
|
||||
.WithAbsorb();
|
||||
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
// Note: PROC_HIT_ABSORB covers both partial and full absorb
|
||||
// There is no separate PROC_HIT_FULL_ABSORB flag in AzerothCore
|
||||
|
||||
// =============================================================================
|
||||
// Spell Family Filtering Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, SpellFamilyMatch_SameFamily)
|
||||
{
|
||||
// Create a Mage spell (family 3)
|
||||
auto* triggerSpell = CreateSpellInfo(133, 3, 0x00000001); // Fireball
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellFamilyName(3) // SPELLFAMILY_MAGE
|
||||
.WithSpellFamilyMask(flag96(0x00000001, 0, 0))
|
||||
.Build();
|
||||
|
||||
// Test family match logic
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, triggerSpell));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, SpellFamilyMatch_DifferentFamily)
|
||||
{
|
||||
// Create a Warrior spell (family 4)
|
||||
auto* triggerSpell = CreateSpellInfo(6343, 4, 0x00000001); // Thunder Clap
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithSpellFamilyName(3) // SPELLFAMILY_MAGE - should NOT match
|
||||
.WithSpellFamilyMask(flag96(0x00000001, 0, 0))
|
||||
.Build();
|
||||
|
||||
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, triggerSpell));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, SpellFamilyMatch_NoFamilyFilter)
|
||||
{
|
||||
// Create any spell
|
||||
auto* triggerSpell = CreateSpellInfo(133, 3, 0x00000001);
|
||||
|
||||
// Proc with no family filter should match any spell
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithSpellFamilyName(0) // No family filter
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, triggerSpell));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, SpellFamilyMatch_FlagMismatch)
|
||||
{
|
||||
// Create a Mage spell with specific flags
|
||||
auto* triggerSpell = CreateSpellInfo(133, 3, 0x00000001); // Fireball flag
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithSpellFamilyName(3) // SPELLFAMILY_MAGE
|
||||
.WithSpellFamilyMask(flag96(0x00000002, 0, 0)) // Different flag
|
||||
.Build();
|
||||
|
||||
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, triggerSpell));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Combined Flag Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, MultipleProcFlags_MeleeOrSpell)
|
||||
{
|
||||
// Proc on melee OR spell damage
|
||||
// Note: Spell procs require SpellPhaseMask to be set, otherwise the check
|
||||
// (eventInfo.SpellPhaseMask & procEntry.SpellPhaseMask) fails when procEntry = 0
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK | PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
// Melee test
|
||||
auto meleeEventInfo = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.Build();
|
||||
EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, meleeEventInfo));
|
||||
|
||||
// Spell test - needs matching SpellPhaseMask AND SpellInfo
|
||||
auto* spellInfo = CreateSpellInfo(100);
|
||||
DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE);
|
||||
auto spellEventInfo = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.WithDamageInfo(&damageInfo)
|
||||
.Build();
|
||||
EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, spellEventInfo));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, MultipleHitMasks_CritOrNormal)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(PROC_HIT_NORMAL | PROC_HIT_CRITICAL)
|
||||
.Build();
|
||||
|
||||
auto normalScenario = ProcScenarioBuilder()
|
||||
.OnMeleeAutoAttack()
|
||||
.WithNormalHit();
|
||||
EXPECT_PROC_TRIGGERS(procEntry, normalScenario);
|
||||
|
||||
auto critScenario = ProcScenarioBuilder()
|
||||
.OnMeleeAutoAttack()
|
||||
.WithCrit();
|
||||
EXPECT_PROC_TRIGGERS(procEntry, critScenario);
|
||||
|
||||
auto missScenario = ProcScenarioBuilder()
|
||||
.OnMeleeAutoAttack()
|
||||
.WithMiss();
|
||||
EXPECT_PROC_DOES_NOT_TRIGGER(procEntry, missScenario);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// School Mask Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, SchoolMaskFilter_FireOnly_FireDamage)
|
||||
{
|
||||
// Proc entry requires fire school damage
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_FIRE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
// Create fire spell and fire damage info
|
||||
auto* fireSpell = CreateSpellInfo(133, 3, 0); // Fireball
|
||||
DamageInfo fireDamageInfo(nullptr, nullptr, 100, fireSpell, SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE);
|
||||
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.WithDamageInfo(&fireDamageInfo)
|
||||
.Build();
|
||||
|
||||
// Fire damage should trigger fire-only proc
|
||||
EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, SchoolMaskFilter_FireOnly_FrostDamage)
|
||||
{
|
||||
// Proc entry requires fire school damage
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_FIRE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
// Create frost spell and frost damage info
|
||||
auto* frostSpell = CreateSpellInfo(116, 3, 0); // Frostbolt
|
||||
DamageInfo frostDamageInfo(nullptr, nullptr, 100, frostSpell, SPELL_SCHOOL_MASK_FROST, SPELL_DIRECT_DAMAGE);
|
||||
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.WithDamageInfo(&frostDamageInfo)
|
||||
.Build();
|
||||
|
||||
// Frost damage should NOT trigger fire-only proc
|
||||
EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, SchoolMaskFilter_NoSchoolMask_AnySchoolTriggers)
|
||||
{
|
||||
// Proc entry with no school mask filter (accepts all schools)
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSchoolMask(0) // No filter
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
// Test with shadow damage
|
||||
auto* shadowSpell = CreateSpellInfo(686, 5, 0); // Shadow Bolt
|
||||
DamageInfo shadowDamageInfo(nullptr, nullptr, 100, shadowSpell, SPELL_SCHOOL_MASK_SHADOW, SPELL_DIRECT_DAMAGE);
|
||||
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.WithDamageInfo(&shadowDamageInfo)
|
||||
.Build();
|
||||
|
||||
// Any school should trigger when no school mask filter is set
|
||||
EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Edge Case Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, EmptyProcFlags_NeverTriggers)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_NONE) // No flags set
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnMeleeAutoAttack()
|
||||
.WithNormalHit();
|
||||
|
||||
// Without PROC_FLAG_NONE special handling, this might still match
|
||||
// The actual behavior depends on implementation
|
||||
auto eventInfo = scenario.Build();
|
||||
|
||||
// Event has flags but proc entry has none - should not trigger
|
||||
if (procEntry.ProcFlags == 0 && scenario.GetTypeMask() != 0)
|
||||
{
|
||||
EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SpellProcIntegrationTest, AllHitMasks_TriggersOnAny)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(PROC_HIT_MASK_ALL)
|
||||
.Build();
|
||||
|
||||
// Should trigger on any hit type
|
||||
std::vector<uint32_t> hitTypes = {
|
||||
PROC_HIT_NORMAL, PROC_HIT_CRITICAL, PROC_HIT_MISS,
|
||||
PROC_HIT_DODGE, PROC_HIT_PARRY, PROC_HIT_BLOCK
|
||||
};
|
||||
|
||||
for (uint32_t hitType : hitTypes)
|
||||
{
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(hitType)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
<< "Failed for hit type: " << hitType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
/*
|
||||
* 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 SpellProcPPMModifierTest.cpp
|
||||
* @brief Unit tests for SPELLMOD_PROC_PER_MINUTE modifier application
|
||||
*
|
||||
* Tests the logic from Unit.cpp:10378-10390:
|
||||
* - Base PPM calculation without modifiers
|
||||
* - Flat PPM modifier application
|
||||
* - Percent PPM modifier application
|
||||
* - GetSpellModOwner() null handling
|
||||
* - SpellProto null handling
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
class SpellProcPPMModifierTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
|
||||
// Standard weapon speeds for testing
|
||||
static constexpr uint32 DAGGER_SPEED = 1400; // 1.4 sec
|
||||
static constexpr uint32 SWORD_SPEED = 2500; // 2.5 sec
|
||||
static constexpr uint32 TWO_HANDED_SPEED = 3300; // 3.3 sec
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Base PPM Calculation (No Modifiers)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, BasePPM_NoModifiers)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
// Default config: no modifiers, has spell mod owner and spell proto
|
||||
|
||||
float basePPM = 6.0f;
|
||||
|
||||
// With 2500ms weapon: (2500 * 6) / 600 = 25%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, basePPM, config);
|
||||
|
||||
EXPECT_NEAR(chance, 25.0f, 0.01f)
|
||||
<< "Base PPM 6.0 with 2.5s weapon should give 25% chance";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, BasePPM_DifferentWeaponSpeeds)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
float basePPM = 6.0f;
|
||||
|
||||
// Fast dagger: (1400 * 6) / 600 = 14%
|
||||
float daggerChance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
DAGGER_SPEED, basePPM, config);
|
||||
EXPECT_NEAR(daggerChance, 14.0f, 0.01f);
|
||||
|
||||
// Slow 2H: (3300 * 6) / 600 = 33%
|
||||
float twoHandChance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
TWO_HANDED_SPEED, basePPM, config);
|
||||
EXPECT_NEAR(twoHandChance, 33.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, BasePPM_ZeroPPM)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, 0.0f, config);
|
||||
|
||||
EXPECT_EQ(chance, 0.0f)
|
||||
<< "Zero PPM should return 0% chance";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, BasePPM_NegativePPM)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, -5.0f, config);
|
||||
|
||||
EXPECT_EQ(chance, 0.0f)
|
||||
<< "Negative PPM should return 0% chance";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Flat Modifier Tests - SPELLMOD_FLAT for SPELLMOD_PROC_PER_MINUTE
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, FlatModifier_IncreasesPPM)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
config.flatModifier = 2.0f; // +2 PPM
|
||||
|
||||
float basePPM = 6.0f;
|
||||
// Modified PPM: 6 + 2 = 8
|
||||
// Chance: (2500 * 8) / 600 = 33.33%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, basePPM, config);
|
||||
|
||||
EXPECT_NEAR(chance, 33.33f, 0.1f)
|
||||
<< "Flat +2 PPM modifier should increase chance from 25% to 33.33%";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, FlatModifier_DecreasesPPM)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
config.flatModifier = -3.0f; // -3 PPM
|
||||
|
||||
float basePPM = 6.0f;
|
||||
// Modified PPM: 6 - 3 = 3
|
||||
// Chance: (2500 * 3) / 600 = 12.5%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, basePPM, config);
|
||||
|
||||
EXPECT_NEAR(chance, 12.5f, 0.1f)
|
||||
<< "Flat -3 PPM modifier should decrease chance from 25% to 12.5%";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, FlatModifier_ReducesToZero)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
config.flatModifier = -10.0f; // Would reduce to -4 PPM
|
||||
|
||||
float basePPM = 6.0f;
|
||||
// Modified PPM: 6 - 10 = -4 (negative)
|
||||
// Formula still applies: (2500 * -4) / 600 = negative
|
||||
// But the check at start for PPM <= 0 happens before modifiers
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, basePPM, config);
|
||||
|
||||
// Note: In the real code, negative results are possible after modifiers
|
||||
// The helper doesn't clamp the final result
|
||||
EXPECT_LT(chance, 0.0f)
|
||||
<< "Extreme negative modifier can produce negative chance";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Percent Modifier Tests - SPELLMOD_PCT for SPELLMOD_PROC_PER_MINUTE
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, PercentModifier_50PercentIncrease)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
config.pctModifier = 1.5f; // 150% = 50% increase
|
||||
|
||||
float basePPM = 6.0f;
|
||||
// Modified PPM: 6 * 1.5 = 9
|
||||
// Chance: (2500 * 9) / 600 = 37.5%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, basePPM, config);
|
||||
|
||||
EXPECT_NEAR(chance, 37.5f, 0.1f)
|
||||
<< "50% PPM increase should raise chance from 25% to 37.5%";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, PercentModifier_50PercentDecrease)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
config.pctModifier = 0.5f; // 50% = 50% decrease
|
||||
|
||||
float basePPM = 6.0f;
|
||||
// Modified PPM: 6 * 0.5 = 3
|
||||
// Chance: (2500 * 3) / 600 = 12.5%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, basePPM, config);
|
||||
|
||||
EXPECT_NEAR(chance, 12.5f, 0.1f)
|
||||
<< "50% PPM decrease should lower chance from 25% to 12.5%";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, PercentModifier_DoublesPPM)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
config.pctModifier = 2.0f; // 200%
|
||||
|
||||
float basePPM = 6.0f;
|
||||
// Modified PPM: 6 * 2 = 12
|
||||
// Chance: (2500 * 12) / 600 = 50%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, basePPM, config);
|
||||
|
||||
EXPECT_NEAR(chance, 50.0f, 0.1f)
|
||||
<< "100% PPM increase should double chance to 50%";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Combined Modifiers Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, CombinedModifiers_FlatThenPercent)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
config.flatModifier = 2.0f; // +2 PPM first
|
||||
config.pctModifier = 1.5f; // Then 50% increase
|
||||
|
||||
float basePPM = 6.0f;
|
||||
// Flat first: 6 + 2 = 8
|
||||
// Percent: 8 * 1.5 = 12
|
||||
// Chance: (2500 * 12) / 600 = 50%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, basePPM, config);
|
||||
|
||||
EXPECT_NEAR(chance, 50.0f, 0.1f)
|
||||
<< "Flat +2 then 50% increase should result in 50% chance";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, CombinedModifiers_BothIncrease)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
config.flatModifier = 4.0f; // +4 PPM
|
||||
config.pctModifier = 1.25f; // 25% increase
|
||||
|
||||
float basePPM = 6.0f;
|
||||
// Flat first: 6 + 4 = 10
|
||||
// Percent: 10 * 1.25 = 12.5
|
||||
// Chance: (2500 * 12.5) / 600 = 52.08%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, basePPM, config);
|
||||
|
||||
EXPECT_NEAR(chance, 52.08f, 0.1f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// No SpellModOwner Tests - GetSpellModOwner() returns null
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, NoSpellModOwner_ModifiersIgnored)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
config.hasSpellModOwner = false; // GetSpellModOwner() returns null
|
||||
config.flatModifier = 10.0f; // Would significantly change result
|
||||
config.pctModifier = 2.0f;
|
||||
|
||||
float basePPM = 6.0f;
|
||||
// Without spell mod owner, modifiers are NOT applied
|
||||
// Chance: (2500 * 6) / 600 = 25%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, basePPM, config);
|
||||
|
||||
EXPECT_NEAR(chance, 25.0f, 0.1f)
|
||||
<< "Without spell mod owner, modifiers should be ignored";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// No SpellProto Tests - spellProto is null
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, NoSpellProto_ModifiersIgnored)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
config.hasSpellProto = false; // spellProto is null
|
||||
config.flatModifier = 10.0f;
|
||||
config.pctModifier = 2.0f;
|
||||
|
||||
float basePPM = 6.0f;
|
||||
// Without spell proto, modifiers are NOT applied
|
||||
// Chance: (2500 * 6) / 600 = 25%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, basePPM, config);
|
||||
|
||||
EXPECT_NEAR(chance, 25.0f, 0.1f)
|
||||
<< "Without spell proto, modifiers should be ignored";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real Spell Scenarios
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, Scenario_OmenOfClarity_BasePPM)
|
||||
{
|
||||
// Omen of Clarity: 6 PPM
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, 6.0f, config);
|
||||
|
||||
EXPECT_NEAR(chance, 25.0f, 0.1f)
|
||||
<< "Omen of Clarity base chance with 2.5s weapon";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, Scenario_OmenOfClarity_WithTalent)
|
||||
{
|
||||
// Hypothetical talent that increases Omen of Clarity PPM by 2
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
config.flatModifier = 2.0f;
|
||||
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, 6.0f, config);
|
||||
|
||||
EXPECT_NEAR(chance, 33.33f, 0.1f)
|
||||
<< "Omen of Clarity with +2 PPM talent";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, Scenario_WindfuryWeapon_FastWeapon)
|
||||
{
|
||||
// Windfury Weapon: 2 PPM with fast weapon
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
|
||||
// Fast 1.5s weapon: (1500 * 2) / 600 = 5%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
1500, 2.0f, config);
|
||||
|
||||
EXPECT_NEAR(chance, 5.0f, 0.1f)
|
||||
<< "Windfury with 1.5s weapon";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, Scenario_WindfuryWeapon_SlowWeapon)
|
||||
{
|
||||
// Windfury Weapon: 2 PPM with slow weapon
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
|
||||
// Slow 3.6s weapon: (3600 * 2) / 600 = 12%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
3600, 2.0f, config);
|
||||
|
||||
EXPECT_NEAR(chance, 12.0f, 0.1f)
|
||||
<< "Windfury with 3.6s weapon";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, Scenario_JudgementOfLight_HighPPM)
|
||||
{
|
||||
// Judgement of Light: 15 PPM (very high)
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, 15.0f, config);
|
||||
|
||||
// (2500 * 15) / 600 = 62.5%
|
||||
EXPECT_NEAR(chance, 62.5f, 0.1f)
|
||||
<< "Judgement of Light with 2.5s weapon";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Edge Cases
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, EdgeCase_VeryFastWeapon)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
|
||||
// 1.0s weapon (very fast): (1000 * 6) / 600 = 10%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
1000, 6.0f, config);
|
||||
|
||||
EXPECT_NEAR(chance, 10.0f, 0.1f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, EdgeCase_VerySlowWeapon)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
|
||||
// 4.0s weapon (very slow): (4000 * 6) / 600 = 40%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
4000, 6.0f, config);
|
||||
|
||||
EXPECT_NEAR(chance, 40.0f, 0.1f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, EdgeCase_VeryHighPPM)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
|
||||
// 60 PPM: (2500 * 60) / 600 = 250% (over 100%, can happen)
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
SWORD_SPEED, 60.0f, config);
|
||||
|
||||
EXPECT_NEAR(chance, 250.0f, 0.1f)
|
||||
<< "Very high PPM can exceed 100% chance";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMModifierTest, EdgeCase_ZeroWeaponSpeed)
|
||||
{
|
||||
ProcChanceTestHelper::PPMModifierConfig config;
|
||||
|
||||
// Zero weapon speed should result in 0%
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChanceWithModifiers(
|
||||
0, 6.0f, config);
|
||||
|
||||
EXPECT_EQ(chance, 0.0f);
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
/*
|
||||
* 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 SpellProcPPMTest.cpp
|
||||
* @brief Unit tests for PPM (Procs Per Minute) calculation
|
||||
*
|
||||
* Tests the formula: chance = (WeaponSpeed * PPM) / 600.0f
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "UnitStub.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
// =============================================================================
|
||||
// PPM Formula Tests
|
||||
// =============================================================================
|
||||
|
||||
class SpellProcPPMTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_unit = std::make_unique<UnitStub>();
|
||||
}
|
||||
|
||||
std::unique_ptr<UnitStub> _unit;
|
||||
};
|
||||
|
||||
TEST_F(SpellProcPPMTest, PPMFormula_BasicCalculation)
|
||||
{
|
||||
// Formula: (WeaponSpeed * PPM) / 600.0f
|
||||
// 2500ms * 6 PPM / 600 = 25%
|
||||
float result = ProcChanceTestHelper::CalculatePPMChance(2500, 6.0f);
|
||||
EXPECT_NEAR(result, 25.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, PPMFormula_FastWeapon_HigherChancePerSwing)
|
||||
{
|
||||
// Fast dagger (1.4 sec = 1400ms), 6 PPM
|
||||
// 1400 * 6 / 600 = 14%
|
||||
float result = ProcChanceTestHelper::CalculatePPMChance(1400, 6.0f);
|
||||
EXPECT_NEAR(result, 14.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, PPMFormula_SlowWeapon_LowerChancePerSwing)
|
||||
{
|
||||
// Slow 2H (3.3 sec = 3300ms), 6 PPM
|
||||
// 3300 * 6 / 600 = 33%
|
||||
float result = ProcChanceTestHelper::CalculatePPMChance(3300, 6.0f);
|
||||
EXPECT_NEAR(result, 33.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, PPMFormula_VerySlowWeapon)
|
||||
{
|
||||
// Very slow weapon (3.8 sec = 3800ms), 6 PPM
|
||||
// 3800 * 6 / 600 = 38%
|
||||
float result = ProcChanceTestHelper::CalculatePPMChance(3800, 6.0f);
|
||||
EXPECT_NEAR(result, 38.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, PPMFormula_ZeroPPM_ReturnsZero)
|
||||
{
|
||||
float result = ProcChanceTestHelper::CalculatePPMChance(2500, 0.0f);
|
||||
EXPECT_FLOAT_EQ(result, 0.0f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, PPMFormula_NegativePPM_ReturnsZero)
|
||||
{
|
||||
float result = ProcChanceTestHelper::CalculatePPMChance(2500, -1.0f);
|
||||
EXPECT_FLOAT_EQ(result, 0.0f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, PPMFormula_WithPositiveModifier)
|
||||
{
|
||||
// 2500ms, 6 PPM + 2 PPM modifier = 8 effective PPM
|
||||
// 2500 * 8 / 600 = 33.33%
|
||||
float result = ProcChanceTestHelper::CalculatePPMChance(2500, 6.0f, 2.0f);
|
||||
EXPECT_NEAR(result, 33.33f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, PPMFormula_WithNegativeModifier)
|
||||
{
|
||||
// 2500ms, 6 PPM - 2 PPM modifier = 4 effective PPM
|
||||
// 2500 * 4 / 600 = 16.67%
|
||||
float result = ProcChanceTestHelper::CalculatePPMChance(2500, 6.0f, -2.0f);
|
||||
EXPECT_NEAR(result, 16.67f, 0.01f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UnitStub PPM Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPPMTest, UnitStub_GetPPMProcChance_DefaultWeaponSpeed)
|
||||
{
|
||||
// Default weapon speed is 2000ms
|
||||
float result = _unit->GetPPMProcChance(2000, 6.0f);
|
||||
EXPECT_NEAR(result, 20.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, UnitStub_GetPPMProcChance_CustomWeaponSpeed)
|
||||
{
|
||||
_unit->SetAttackTime(0, 2500); // BASE_ATTACK
|
||||
float result = _unit->GetPPMProcChance(_unit->GetAttackTime(0), 6.0f);
|
||||
EXPECT_NEAR(result, 25.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, UnitStub_GetPPMProcChance_WithPPMModifier)
|
||||
{
|
||||
_unit->SetPPMModifier(12345, 2.0f); // Spell ID 12345 has +2 PPM modifier
|
||||
float result = _unit->GetPPMProcChance(2500, 6.0f, 12345);
|
||||
// 2500 * (6 + 2) / 600 = 33.33%
|
||||
EXPECT_NEAR(result, 33.33f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, UnitStub_GetPPMProcChance_ModifierNotAppliedWithoutSpellId)
|
||||
{
|
||||
_unit->SetPPMModifier(12345, 2.0f);
|
||||
// Without spell ID, modifier is not applied
|
||||
float result = _unit->GetPPMProcChance(2500, 6.0f, 0);
|
||||
EXPECT_NEAR(result, 25.0f, 0.01f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real-World PPM Spell Examples
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPPMTest, OmenOfClarity_PPM6_VariousWeaponSpeeds)
|
||||
{
|
||||
// Omen of Clarity: 6 PPM
|
||||
constexpr float OOC_PPM = 6.0f;
|
||||
|
||||
// Fast dagger
|
||||
float daggerChance = ProcChanceTestHelper::CalculatePPMChance(1400, OOC_PPM);
|
||||
EXPECT_NEAR(daggerChance, 14.0f, 0.01f);
|
||||
|
||||
// Normal 1H sword
|
||||
float swordChance = ProcChanceTestHelper::CalculatePPMChance(2500, OOC_PPM);
|
||||
EXPECT_NEAR(swordChance, 25.0f, 0.01f);
|
||||
|
||||
// Staff
|
||||
float staffChance = ProcChanceTestHelper::CalculatePPMChance(3000, OOC_PPM);
|
||||
EXPECT_NEAR(staffChance, 30.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, JudgementOfLight_PPM15_VariousWeaponSpeeds)
|
||||
{
|
||||
// Judgement of Light: 15 PPM
|
||||
constexpr float JOL_PPM = 15.0f;
|
||||
|
||||
// Fast dagger
|
||||
float daggerChance = ProcChanceTestHelper::CalculatePPMChance(1400, JOL_PPM);
|
||||
EXPECT_NEAR(daggerChance, 35.0f, 0.01f);
|
||||
|
||||
// Normal 1H sword
|
||||
float swordChance = ProcChanceTestHelper::CalculatePPMChance(2500, JOL_PPM);
|
||||
EXPECT_NEAR(swordChance, 62.5f, 0.01f);
|
||||
|
||||
// Slow 2H weapon
|
||||
float twoHanderChance = ProcChanceTestHelper::CalculatePPMChance(3300, JOL_PPM);
|
||||
EXPECT_NEAR(twoHanderChance, 82.5f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, WindfuryWeapon_PPM2_VariousWeaponSpeeds)
|
||||
{
|
||||
// Windfury Weapon: 2 PPM (low PPM for testing)
|
||||
constexpr float WF_PPM = 2.0f;
|
||||
|
||||
// Fast dagger
|
||||
float daggerChance = ProcChanceTestHelper::CalculatePPMChance(1400, WF_PPM);
|
||||
EXPECT_NEAR(daggerChance, 4.67f, 0.01f);
|
||||
|
||||
// Slow 2H weapon
|
||||
float twoHanderChance = ProcChanceTestHelper::CalculatePPMChance(3300, WF_PPM);
|
||||
EXPECT_NEAR(twoHanderChance, 11.0f, 0.01f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Edge Cases
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPPMTest, EdgeCase_VeryFastWeapon)
|
||||
{
|
||||
// Very fast (theoretical) weapon - 1.0 sec = 1000ms
|
||||
float result = ProcChanceTestHelper::CalculatePPMChance(1000, 6.0f);
|
||||
EXPECT_NEAR(result, 10.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, EdgeCase_ExtremelySlow)
|
||||
{
|
||||
// Extremely slow weapon - 5.0 sec = 5000ms
|
||||
float result = ProcChanceTestHelper::CalculatePPMChance(5000, 6.0f);
|
||||
EXPECT_NEAR(result, 50.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, EdgeCase_HighPPM)
|
||||
{
|
||||
// High PPM value (30)
|
||||
float result = ProcChanceTestHelper::CalculatePPMChance(2500, 30.0f);
|
||||
// 2500 * 30 / 600 = 125% (can exceed 100%)
|
||||
EXPECT_NEAR(result, 125.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, EdgeCase_FractionalPPM)
|
||||
{
|
||||
// Fractional PPM value (2.5)
|
||||
float result = ProcChanceTestHelper::CalculatePPMChance(2400, 2.5f);
|
||||
// 2400 * 2.5 / 600 = 10%
|
||||
EXPECT_NEAR(result, 10.0f, 0.01f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Shapeshifter Enchant PPM Bug Tests
|
||||
//
|
||||
// Player::CastItemCombatSpell has two PPM paths:
|
||||
// 1) Item spells (line ~7308): uses GetAttackTime(attType) - CORRECT
|
||||
// 2) Enchantment procs (line ~7375): uses proto->Delay - BUG
|
||||
//
|
||||
// For non-shapeshifted players these return the same value, but for
|
||||
// Feral Druids proto->Delay reflects the weapon (e.g. 3.6s staff)
|
||||
// while GetAttackTime returns the form speed (1.0s Cat, 2.5s Bear).
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPPMTest, ShapeshiftBug_NonShifted_NoDiscrepancy)
|
||||
{
|
||||
// A warrior with a 3.6s weapon: proto->Delay == GetAttackTime()
|
||||
constexpr uint32 WEAPON_DELAY = ProcChanceTestHelper::WEAPON_SPEED_STAFF;
|
||||
constexpr float MONGOOSE_PPM = 1.0f;
|
||||
|
||||
_unit->SetAttackTime(0, WEAPON_DELAY);
|
||||
|
||||
float chanceFromProtoDelay = ProcChanceTestHelper::CalculatePPMChance(WEAPON_DELAY, MONGOOSE_PPM);
|
||||
float chanceFromGetAttackTime = ProcChanceTestHelper::CalculatePPMChance(
|
||||
_unit->GetAttackTime(0), MONGOOSE_PPM);
|
||||
|
||||
EXPECT_FLOAT_EQ(chanceFromProtoDelay, chanceFromGetAttackTime)
|
||||
<< "Non-shapeshifted: proto->Delay and GetAttackTime() should be identical";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, ShapeshiftBug_CatForm_ProtoDelayInflatesChance)
|
||||
{
|
||||
// Druid in Cat Form with a 3.6s staff equipped
|
||||
// proto->Delay = 3600ms (the staff), GetAttackTime = 1000ms (Cat Form)
|
||||
constexpr uint32 STAFF_DELAY = ProcChanceTestHelper::WEAPON_SPEED_STAFF;
|
||||
constexpr uint32 CAT_SPEED = ProcChanceTestHelper::FORM_SPEED_CAT;
|
||||
constexpr float MONGOOSE_PPM = 1.0f;
|
||||
|
||||
_unit->SetAttackTime(0, CAT_SPEED);
|
||||
|
||||
float buggyChance = ProcChanceTestHelper::CalculatePPMChance(STAFF_DELAY, MONGOOSE_PPM);
|
||||
float correctChance = ProcChanceTestHelper::CalculatePPMChance(CAT_SPEED, MONGOOSE_PPM);
|
||||
|
||||
// proto->Delay gives 3600 * 1 / 600 = 6.0% per swing
|
||||
EXPECT_NEAR(buggyChance, 6.0f, 0.01f);
|
||||
// GetAttackTime gives 1000 * 1 / 600 = 1.67% per swing
|
||||
EXPECT_NEAR(correctChance, 1.67f, 0.01f);
|
||||
|
||||
// The bug inflates chance per swing by weapon_speed / form_speed
|
||||
EXPECT_NEAR(buggyChance / correctChance,
|
||||
static_cast<float>(STAFF_DELAY) / static_cast<float>(CAT_SPEED), 0.01f)
|
||||
<< "Bug inflates per-swing chance by ratio of weapon speed to form speed";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, ShapeshiftBug_CatForm_EffectivePPMIs3Point6x)
|
||||
{
|
||||
// Cat Form attacks every 1.0s (60 swings/min)
|
||||
// With the buggy 6.0% chance per swing: 60 * 0.06 = 3.6 procs/min
|
||||
// With the correct 1.67% chance: 60 * 0.0167 = 1.0 procs/min
|
||||
constexpr uint32 STAFF_DELAY = ProcChanceTestHelper::WEAPON_SPEED_STAFF;
|
||||
constexpr uint32 CAT_SPEED = ProcChanceTestHelper::FORM_SPEED_CAT;
|
||||
constexpr float MONGOOSE_PPM = 1.0f;
|
||||
|
||||
float buggyChance = ProcChanceTestHelper::CalculatePPMChance(STAFF_DELAY, MONGOOSE_PPM);
|
||||
float correctChance = ProcChanceTestHelper::CalculatePPMChance(CAT_SPEED, MONGOOSE_PPM);
|
||||
|
||||
float buggyEffectivePPM = ProcChanceTestHelper::CalculateEffectivePPM(buggyChance, CAT_SPEED);
|
||||
float correctEffectivePPM = ProcChanceTestHelper::CalculateEffectivePPM(correctChance, CAT_SPEED);
|
||||
|
||||
// Buggy: effective PPM is 3.6 instead of 1.0
|
||||
EXPECT_NEAR(buggyEffectivePPM, 3.6f, 0.01f)
|
||||
<< "Bug: Cat Form Mongoose procs 3.6 times/min instead of 1.0";
|
||||
// Correct: effective PPM matches the intended value
|
||||
EXPECT_NEAR(correctEffectivePPM, MONGOOSE_PPM, 0.01f)
|
||||
<< "Fix: Cat Form Mongoose should proc exactly 1.0 times/min";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, ShapeshiftBug_BearForm_ProtoDelayInflatesChance)
|
||||
{
|
||||
// Bear Form with 3.6s staff: proto->Delay = 3600, GetAttackTime = 2500
|
||||
constexpr uint32 STAFF_DELAY = ProcChanceTestHelper::WEAPON_SPEED_STAFF;
|
||||
constexpr uint32 BEAR_SPEED = ProcChanceTestHelper::FORM_SPEED_BEAR;
|
||||
constexpr float MONGOOSE_PPM = 1.0f;
|
||||
|
||||
_unit->SetAttackTime(0, BEAR_SPEED);
|
||||
|
||||
float buggyChance = ProcChanceTestHelper::CalculatePPMChance(STAFF_DELAY, MONGOOSE_PPM);
|
||||
float correctChance = ProcChanceTestHelper::CalculatePPMChance(BEAR_SPEED, MONGOOSE_PPM);
|
||||
|
||||
float buggyEffectivePPM = ProcChanceTestHelper::CalculateEffectivePPM(buggyChance, BEAR_SPEED);
|
||||
float correctEffectivePPM = ProcChanceTestHelper::CalculateEffectivePPM(correctChance, BEAR_SPEED);
|
||||
|
||||
// Buggy: 1.44 PPM instead of 1.0
|
||||
EXPECT_NEAR(buggyEffectivePPM, 1.44f, 0.01f)
|
||||
<< "Bug: Bear Form Mongoose procs 1.44 times/min instead of 1.0";
|
||||
EXPECT_NEAR(correctEffectivePPM, MONGOOSE_PPM, 0.01f)
|
||||
<< "Fix: Bear Form Mongoose should proc exactly 1.0 times/min";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, ShapeshiftBug_CatForm_FieryWeapon6PPM)
|
||||
{
|
||||
// Fiery Weapon (6 PPM) in Cat Form with 3.6s staff
|
||||
constexpr uint32 STAFF_DELAY = ProcChanceTestHelper::WEAPON_SPEED_STAFF;
|
||||
constexpr uint32 CAT_SPEED = ProcChanceTestHelper::FORM_SPEED_CAT;
|
||||
constexpr float FIERY_PPM = 6.0f;
|
||||
|
||||
float buggyChance = ProcChanceTestHelper::CalculatePPMChance(STAFF_DELAY, FIERY_PPM);
|
||||
float correctChance = ProcChanceTestHelper::CalculatePPMChance(CAT_SPEED, FIERY_PPM);
|
||||
|
||||
float buggyEffectivePPM = ProcChanceTestHelper::CalculateEffectivePPM(buggyChance, CAT_SPEED);
|
||||
float correctEffectivePPM = ProcChanceTestHelper::CalculateEffectivePPM(correctChance, CAT_SPEED);
|
||||
|
||||
// Buggy: 36% chance per swing → 21.6 procs/min instead of 6.0
|
||||
EXPECT_NEAR(buggyChance, 36.0f, 0.01f);
|
||||
EXPECT_NEAR(correctChance, 10.0f, 0.01f);
|
||||
EXPECT_NEAR(buggyEffectivePPM, 21.6f, 0.01f)
|
||||
<< "Bug: Cat Form Fiery Weapon procs 21.6 times/min instead of 6.0";
|
||||
EXPECT_NEAR(correctEffectivePPM, FIERY_PPM, 0.01f)
|
||||
<< "Fix: Cat Form Fiery Weapon should proc exactly 6.0 times/min";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPPMTest, ShapeshiftBug_ItemSpellPath_AlreadyCorrect)
|
||||
{
|
||||
// The item spell PPM path (line ~7308) already uses GetAttackTime.
|
||||
// Verify that using GetAttackTime gives correct PPM for all forms.
|
||||
constexpr float PPM = 1.0f;
|
||||
|
||||
struct FormScenario
|
||||
{
|
||||
const char* name;
|
||||
uint32 formSpeed;
|
||||
};
|
||||
|
||||
FormScenario scenarios[] = {
|
||||
{"Normal (3.6s weapon)", ProcChanceTestHelper::WEAPON_SPEED_STAFF},
|
||||
{"Cat Form", ProcChanceTestHelper::FORM_SPEED_CAT},
|
||||
{"Bear Form", ProcChanceTestHelper::FORM_SPEED_BEAR},
|
||||
};
|
||||
|
||||
for (auto const& scenario : scenarios)
|
||||
{
|
||||
_unit->SetAttackTime(0, scenario.formSpeed);
|
||||
|
||||
float chance = ProcChanceTestHelper::CalculatePPMChance(
|
||||
_unit->GetAttackTime(0), PPM);
|
||||
float effectivePPM = ProcChanceTestHelper::CalculateEffectivePPM(
|
||||
chance, scenario.formSpeed);
|
||||
|
||||
EXPECT_NEAR(effectivePPM, PPM, 0.01f)
|
||||
<< scenario.name << ": GetAttackTime-based PPM should always match intended PPM";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
/*
|
||||
* 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 SpellProcPipelineTest.cpp
|
||||
* @brief End-to-end integration tests for the full proc pipeline
|
||||
*
|
||||
* Tests the complete proc execution flow:
|
||||
* 1. Cooldown check (IsProcOnCooldown)
|
||||
* 2. Chance calculation (CalcProcChance)
|
||||
* 3. Roll check (rand_chance)
|
||||
* 4. Cooldown application
|
||||
* 5. Charge consumption (ConsumeProcCharges)
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "AuraStub.h"
|
||||
#include "UnitStub.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
class SpellProcPipelineTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_scenario = std::make_unique<ProcTestScenario>();
|
||||
}
|
||||
|
||||
std::unique_ptr<ProcTestScenario> _scenario;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Full Pipeline Flow Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPipelineTest, FullFlow_BasicProc_100Percent)
|
||||
{
|
||||
_scenario->WithAura(12345);
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// 100% chance should always proc
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, FullFlow_BasicProc_0Percent)
|
||||
{
|
||||
_scenario->WithAura(12345);
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(0.0f)
|
||||
.Build();
|
||||
|
||||
// 0% chance should never proc (when roll is > 0)
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry, 50.0f));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, FullFlow_WithCooldown)
|
||||
{
|
||||
_scenario->WithAura(12345);
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithCooldown(1000ms)
|
||||
.Build();
|
||||
|
||||
// First proc succeeds
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
|
||||
// Second proc blocked by cooldown
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry));
|
||||
|
||||
// Wait for cooldown
|
||||
_scenario->AdvanceTime(1100ms);
|
||||
|
||||
// Third proc succeeds
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, FullFlow_WithCharges)
|
||||
{
|
||||
_scenario->WithAura(12345, 3); // 3 charges
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// First proc - 3 -> 2 charges
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_EQ(_scenario->GetAura()->GetCharges(), 2);
|
||||
|
||||
// Second proc - 2 -> 1 charges
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_EQ(_scenario->GetAura()->GetCharges(), 1);
|
||||
|
||||
// Third proc - 1 -> 0 charges, aura removed
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_EQ(_scenario->GetAura()->GetCharges(), 0);
|
||||
EXPECT_TRUE(_scenario->GetAura()->IsRemoved());
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, FullFlow_WithStacks)
|
||||
{
|
||||
_scenario->WithAura(12345, 0, 5); // 5 stacks, no charges
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithAttributesMask(PROC_ATTR_USE_STACKS_FOR_CHARGES)
|
||||
.Build();
|
||||
|
||||
// Each proc consumes one stack
|
||||
for (int i = 5; i > 0; --i)
|
||||
{
|
||||
EXPECT_EQ(_scenario->GetAura()->GetStackAmount(), i);
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
}
|
||||
|
||||
EXPECT_EQ(_scenario->GetAura()->GetStackAmount(), 0);
|
||||
EXPECT_TRUE(_scenario->GetAura()->IsRemoved());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Combined Feature Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPipelineTest, Combined_ChargesAndCooldown)
|
||||
{
|
||||
_scenario->WithAura(12345, 5); // 5 charges
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithCooldown(500ms)
|
||||
.Build();
|
||||
|
||||
// First proc at t=0
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_EQ(_scenario->GetAura()->GetCharges(), 4);
|
||||
|
||||
// Blocked at t=0 (cooldown)
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_EQ(_scenario->GetAura()->GetCharges(), 4);
|
||||
|
||||
// Wait and proc again at t=600ms
|
||||
_scenario->AdvanceTime(600ms);
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_EQ(_scenario->GetAura()->GetCharges(), 3);
|
||||
|
||||
// Blocked at t=600ms
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_EQ(_scenario->GetAura()->GetCharges(), 3);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, Combined_PPM_AndCooldown)
|
||||
{
|
||||
_scenario->WithAura(12345);
|
||||
_scenario->WithWeaponSpeed(0, 2500); // BASE_ATTACK = 2500ms
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcsPerMinute(6.0f) // 25% with 2500ms weapon
|
||||
.WithCooldown(1000ms)
|
||||
.Build();
|
||||
|
||||
// First proc (roll 0 = always pass)
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry, 0.0f));
|
||||
|
||||
// Blocked by cooldown even if roll would pass
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry, 0.0f));
|
||||
|
||||
// Wait for cooldown
|
||||
_scenario->AdvanceTime(1100ms);
|
||||
|
||||
// Can proc again
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry, 0.0f));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, Combined_Level60Reduction_WithCooldown)
|
||||
{
|
||||
_scenario->WithAura(12345);
|
||||
_scenario->WithActorLevel(80);
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.WithCooldown(1000ms)
|
||||
.Build();
|
||||
|
||||
// Level 80: 30% * (1 - 20/30) = 10% effective chance
|
||||
// Roll of 5 should pass
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry, 5.0f));
|
||||
|
||||
// Blocked by cooldown
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry, 5.0f));
|
||||
|
||||
// Wait and try again
|
||||
_scenario->AdvanceTime(1100ms);
|
||||
|
||||
// Roll of 15 should fail (10% chance)
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry, 15.0f));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real Spell Scenarios
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPipelineTest, Scenario_OmenOfClarity)
|
||||
{
|
||||
// Omen of Clarity: 6 PPM, no cooldown, no charges
|
||||
_scenario->WithAura(16864); // Omen of Clarity
|
||||
_scenario->WithWeaponSpeed(0, 2500); // Staff
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcsPerMinute(6.0f) // 25% with 2500ms
|
||||
.Build();
|
||||
|
||||
// Simulate multiple hits
|
||||
int procCount = 0;
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
// Roll values simulating ~25% success rate
|
||||
float roll = (i % 4 == 0) ? 10.0f : 50.0f;
|
||||
if (_scenario->SimulateProc(procEntry, roll))
|
||||
procCount++;
|
||||
}
|
||||
|
||||
// With deterministic rolls, should have 3 procs (indexes 0, 4, 8)
|
||||
// But our test is roll > chance check, so roll 10 fails against 25% chance
|
||||
// Actually roll 0 always passes, non-zero rolls check roll > chance
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, Scenario_LeaderOfThePack)
|
||||
{
|
||||
// Leader of the Pack: 6 second ICD
|
||||
_scenario->WithAura(24932);
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithCooldown(6000ms)
|
||||
.Build();
|
||||
|
||||
// First crit - procs
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
|
||||
// Second crit at 1 second - blocked
|
||||
_scenario->AdvanceTime(1000ms);
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry));
|
||||
|
||||
// Third crit at 5 seconds - blocked
|
||||
_scenario->AdvanceTime(4000ms);
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry));
|
||||
|
||||
// Fourth crit at 6.1 seconds - allowed
|
||||
_scenario->AdvanceTime(1100ms);
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, Scenario_ArtOfWar)
|
||||
{
|
||||
// Art of War: 2 charges (typically)
|
||||
_scenario->WithAura(53486, 2); // Art of War
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// First Exorcism - consumes charge
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_EQ(_scenario->GetAura()->GetCharges(), 1);
|
||||
|
||||
// Second Exorcism - consumes last charge
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_EQ(_scenario->GetAura()->GetCharges(), 0);
|
||||
EXPECT_TRUE(_scenario->GetAura()->IsRemoved());
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, Scenario_LightningShield)
|
||||
{
|
||||
// Lightning Shield: 3 charges (orbs)
|
||||
_scenario->WithAura(324, 3); // Lightning Shield Rank 1
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// First hit - uses orb
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_EQ(_scenario->GetAura()->GetCharges(), 2);
|
||||
|
||||
// Second hit - uses orb
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_EQ(_scenario->GetAura()->GetCharges(), 1);
|
||||
|
||||
// Third hit - last orb, aura removed
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_TRUE(_scenario->GetAura()->IsRemoved());
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, Scenario_WanderingPlague)
|
||||
{
|
||||
// Wandering Plague: 1 second ICD
|
||||
_scenario->WithAura(49217);
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithCooldown(1000ms)
|
||||
.Build();
|
||||
|
||||
// First tick procs
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
|
||||
// Rapid ticks blocked
|
||||
_scenario->AdvanceTime(200ms);
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry));
|
||||
|
||||
_scenario->AdvanceTime(200ms);
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry));
|
||||
|
||||
_scenario->AdvanceTime(200ms);
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry));
|
||||
|
||||
// After 1 second total, can proc again
|
||||
_scenario->AdvanceTime(600ms);
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Edge Cases
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPipelineTest, EdgeCase_NoAura_NoProcPossible)
|
||||
{
|
||||
// Don't set up aura
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, EdgeCase_ZeroCooldown_AllowsRapidProcs)
|
||||
{
|
||||
_scenario->WithAura(12345);
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithCooldown(0ms)
|
||||
.Build();
|
||||
|
||||
// Multiple rapid procs should all succeed
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, EdgeCase_VeryLongCooldown)
|
||||
{
|
||||
_scenario->WithAura(12345);
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.WithCooldown(300000ms) // 5 minute cooldown
|
||||
.Build();
|
||||
|
||||
// First proc
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
|
||||
// Blocked even after 4 minutes
|
||||
_scenario->AdvanceTime(240000ms);
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry));
|
||||
|
||||
// Allowed after 5 minutes
|
||||
_scenario->AdvanceTime(60001ms);
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, EdgeCase_ManyCharges)
|
||||
{
|
||||
_scenario->WithAura(12345, 100); // 100 charges
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Consume all charges
|
||||
for (int i = 100; i > 0; --i)
|
||||
{
|
||||
EXPECT_EQ(_scenario->GetAura()->GetCharges(), i);
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry));
|
||||
}
|
||||
|
||||
EXPECT_TRUE(_scenario->GetAura()->IsRemoved());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Actor Configuration Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcPipelineTest, ActorLevel_AffectsProcChance)
|
||||
{
|
||||
_scenario->WithAura(12345);
|
||||
_scenario->WithActorLevel(60);
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithChance(30.0f)
|
||||
.WithAttributesMask(PROC_ATTR_REDUCE_PROC_60)
|
||||
.Build();
|
||||
|
||||
// At level 60, full 30% chance
|
||||
// Roll of 25 should pass
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry, 25.0f));
|
||||
|
||||
// Reset
|
||||
_scenario->GetAura()->ResetProcCooldown();
|
||||
|
||||
// Change to level 80
|
||||
_scenario->WithActorLevel(80);
|
||||
|
||||
// At level 80, only 10% chance
|
||||
// Roll of 25 should fail
|
||||
EXPECT_FALSE(_scenario->SimulateProc(procEntry, 25.0f));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcPipelineTest, WeaponSpeed_AffectsPPMChance)
|
||||
{
|
||||
_scenario->WithAura(12345);
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcsPerMinute(6.0f)
|
||||
.Build();
|
||||
|
||||
// Fast dagger (1400ms): 14% chance
|
||||
_scenario->WithWeaponSpeed(0, 1400);
|
||||
// Roll of 10 should pass (< 14%)
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry, 10.0f));
|
||||
|
||||
// Reset cooldown
|
||||
_scenario->GetAura()->ResetProcCooldown();
|
||||
|
||||
// Slow 2H (3300ms): 33% chance
|
||||
_scenario->WithWeaponSpeed(0, 3300);
|
||||
// Roll of 30 should pass (< 33%)
|
||||
EXPECT_TRUE(_scenario->SimulateProc(procEntry, 30.0f));
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "AuraScriptTestFramework.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
/**
|
||||
* @brief Tests for SpellTypeMask calculation based on proc phase
|
||||
*
|
||||
* These tests verify that the proc system correctly calculates SpellTypeMask
|
||||
* for different proc phases. This is critical because:
|
||||
* - CAST phase: No damage/heal has occurred yet
|
||||
* - HIT phase: Damage/heal info is available
|
||||
* - FINISH phase: damageInfo may be null even for damage spells
|
||||
*
|
||||
* Regression test for: FINISH phase was incorrectly using NO_DMG_HEAL when
|
||||
* damageInfo was null, breaking procs like Killing Machine (51124) that
|
||||
* require SpellTypeMask=DAMAGE and SpellPhaseMask=FINISH.
|
||||
*/
|
||||
class SpellProcSpellTypeMaskTest : public AuraScriptProcTestFixture
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
AuraScriptProcTestFixture::SetUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculate spellTypeMask the same way ProcSkillsAndAuras does
|
||||
*
|
||||
* This mirrors the logic in Unit::ProcSkillsAndAuras to allow unit testing
|
||||
* of the spellTypeMask calculation without needing full Unit objects.
|
||||
*/
|
||||
static uint32 CalculateSpellTypeMask(uint32 procPhase, DamageInfo* damageInfo, HealInfo* healInfo, bool hasSpellInfo)
|
||||
{
|
||||
uint32 spellTypeMask = 0;
|
||||
if (procPhase == PROC_SPELL_PHASE_CAST || procPhase == PROC_SPELL_PHASE_FINISH)
|
||||
{
|
||||
// At CAST phase, no damage/heal has occurred yet - use MASK_ALL
|
||||
// At FINISH phase, damageInfo may be null but spell did do damage - use MASK_ALL
|
||||
spellTypeMask = PROC_SPELL_TYPE_MASK_ALL;
|
||||
}
|
||||
else if (healInfo && healInfo->GetHeal())
|
||||
spellTypeMask = PROC_SPELL_TYPE_HEAL;
|
||||
else if (damageInfo && damageInfo->GetDamage())
|
||||
spellTypeMask = PROC_SPELL_TYPE_DAMAGE;
|
||||
else if (hasSpellInfo)
|
||||
spellTypeMask = PROC_SPELL_TYPE_NO_DMG_HEAL;
|
||||
|
||||
return spellTypeMask;
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// SpellTypeMask Calculation Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcSpellTypeMaskTest, CastPhase_UsesMaskAll)
|
||||
{
|
||||
// CAST phase should use MASK_ALL regardless of damage/heal info
|
||||
uint32 result = CalculateSpellTypeMask(PROC_SPELL_PHASE_CAST, nullptr, nullptr, true);
|
||||
EXPECT_EQ(result, PROC_SPELL_TYPE_MASK_ALL);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcSpellTypeMaskTest, FinishPhase_UsesMaskAll_EvenWithNullDamageInfo)
|
||||
{
|
||||
// FINISH phase should use MASK_ALL even when damageInfo is null
|
||||
// This is the key regression test - previously returned NO_DMG_HEAL
|
||||
uint32 result = CalculateSpellTypeMask(PROC_SPELL_PHASE_FINISH, nullptr, nullptr, true);
|
||||
EXPECT_EQ(result, PROC_SPELL_TYPE_MASK_ALL);
|
||||
|
||||
// Verify it includes DAMAGE type (required for procs like Killing Machine)
|
||||
EXPECT_TRUE(result & PROC_SPELL_TYPE_DAMAGE);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcSpellTypeMaskTest, HitPhase_WithDamage_UsesDamageType)
|
||||
{
|
||||
auto* spellInfo = CreateSpellInfo(12345, 15, 0);
|
||||
DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_FROST, SPELL_DIRECT_DAMAGE);
|
||||
|
||||
uint32 result = CalculateSpellTypeMask(PROC_SPELL_PHASE_HIT, &damageInfo, nullptr, true);
|
||||
EXPECT_EQ(result, PROC_SPELL_TYPE_DAMAGE);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcSpellTypeMaskTest, HitPhase_WithHeal_UsesHealType)
|
||||
{
|
||||
auto* spellInfo = CreateSpellInfo(12345, 15, 0);
|
||||
HealInfo healInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_HOLY);
|
||||
|
||||
uint32 result = CalculateSpellTypeMask(PROC_SPELL_PHASE_HIT, nullptr, &healInfo, true);
|
||||
EXPECT_EQ(result, PROC_SPELL_TYPE_HEAL);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcSpellTypeMaskTest, HitPhase_NoDamageNoHeal_UsesNoDmgHeal)
|
||||
{
|
||||
// HIT phase with no damage/heal info should use NO_DMG_HEAL
|
||||
uint32 result = CalculateSpellTypeMask(PROC_SPELL_PHASE_HIT, nullptr, nullptr, true);
|
||||
EXPECT_EQ(result, PROC_SPELL_TYPE_NO_DMG_HEAL);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Killing Machine Regression Test
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief Regression test for Killing Machine (51124) proc consumption
|
||||
*
|
||||
* Killing Machine has:
|
||||
* - SpellTypeMask = 1 (PROC_SPELL_TYPE_DAMAGE)
|
||||
* - SpellPhaseMask = 4 (PROC_SPELL_PHASE_FINISH)
|
||||
*
|
||||
* When Icy Touch is cast, the FINISH phase event must have a spellTypeMask
|
||||
* that includes DAMAGE for the proc to fire and consume the buff.
|
||||
*
|
||||
* The bug was: FINISH phase calculated spellTypeMask as NO_DMG_HEAL (4)
|
||||
* because damageInfo was null, causing the proc check to fail.
|
||||
*/
|
||||
TEST_F(SpellProcSpellTypeMaskTest, KillingMachine_FinishPhase_MatchesDamageTypeMask)
|
||||
{
|
||||
// Killing Machine spell_proc entry
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithSpellFamilyName(15) // SPELLFAMILY_DEATHKNIGHT
|
||||
.WithSpellFamilyMask(flag96(2, 6, 0)) // Icy Touch, Frost Strike, Howling Blast
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MELEE_DMG_CLASS | PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_FINISH)
|
||||
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
|
||||
.WithCharges(1)
|
||||
.Build();
|
||||
|
||||
// Calculate what spellTypeMask FINISH phase would produce
|
||||
// (simulating Spell.cpp calling ProcSkillsAndAuras with nullptr damageInfo)
|
||||
uint32 finishPhaseSpellTypeMask = CalculateSpellTypeMask(PROC_SPELL_PHASE_FINISH, nullptr, nullptr, true);
|
||||
|
||||
// Verify the calculated mask includes DAMAGE type
|
||||
EXPECT_TRUE(finishPhaseSpellTypeMask & PROC_SPELL_TYPE_DAMAGE)
|
||||
<< "FINISH phase spellTypeMask must include PROC_SPELL_TYPE_DAMAGE for Killing Machine to work";
|
||||
|
||||
// Verify that the proc entry's SpellTypeMask requirement is satisfied
|
||||
EXPECT_TRUE(finishPhaseSpellTypeMask & procEntry.SpellTypeMask)
|
||||
<< "FINISH phase spellTypeMask (" << finishPhaseSpellTypeMask
|
||||
<< ") must match Killing Machine's SpellTypeMask requirement (" << procEntry.SpellTypeMask << ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify FINISH phase works with actual CanSpellTriggerProcOnEvent
|
||||
*
|
||||
* This test verifies the full integration: when we pass the correctly
|
||||
* calculated spellTypeMask to CanSpellTriggerProcOnEvent, Killing Machine
|
||||
* style procs should work.
|
||||
*/
|
||||
TEST_F(SpellProcSpellTypeMaskTest, KillingMachine_FullIntegration_ProcTriggers)
|
||||
{
|
||||
// Killing Machine spell_proc entry
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithSpellFamilyName(15) // SPELLFAMILY_DEATHKNIGHT
|
||||
.WithSpellFamilyMask(flag96(2, 0, 0)) // Icy Touch
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_FINISH)
|
||||
.Build();
|
||||
|
||||
// Create Icy Touch spell info (SpellFamilyFlags = [2, 0, 0])
|
||||
auto* icyTouchSpell = CreateSpellInfo(49909, 15, 2); // DK family, mask0=2
|
||||
DamageInfo damageInfo(nullptr, nullptr, 100, icyTouchSpell, SPELL_SCHOOL_MASK_FROST, SPELL_DIRECT_DAMAGE);
|
||||
|
||||
// Create event with FINISH phase and MASK_ALL (as the fix provides)
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_MASK_ALL) // Fixed behavior
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_FINISH)
|
||||
.WithDamageInfo(&damageInfo)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
<< "Killing Machine style proc should trigger on FINISH phase with MASK_ALL spellTypeMask";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify the bug scenario - FINISH phase with NO_DMG_HEAL fails
|
||||
*
|
||||
* This test documents the bug behavior: if FINISH phase incorrectly uses
|
||||
* NO_DMG_HEAL spellTypeMask, Killing Machine style procs fail.
|
||||
*/
|
||||
TEST_F(SpellProcSpellTypeMaskTest, KillingMachine_BugScenario_NoDmgHealFails)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithSpellFamilyName(15)
|
||||
.WithSpellFamilyMask(flag96(2, 0, 0))
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE) // Requires DAMAGE
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_FINISH)
|
||||
.Build();
|
||||
|
||||
auto* icyTouchSpell = CreateSpellInfo(49909, 15, 2);
|
||||
DamageInfo damageInfo(nullptr, nullptr, 100, icyTouchSpell, SPELL_SCHOOL_MASK_FROST, SPELL_DIRECT_DAMAGE);
|
||||
|
||||
// Simulate the bug: FINISH phase with NO_DMG_HEAL (the old broken behavior)
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.WithSpellTypeMask(PROC_SPELL_TYPE_NO_DMG_HEAL) // Bug: wrong mask
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_FINISH)
|
||||
.WithDamageInfo(&damageInfo)
|
||||
.Build();
|
||||
|
||||
// This should fail - documenting the bug behavior
|
||||
EXPECT_FALSE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
<< "With NO_DMG_HEAL spellTypeMask, DAMAGE-requiring procs should NOT trigger (this was the bug)";
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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 SpellProcTargetResolutionTest.cpp
|
||||
* @brief Tests for smart proc trigger target resolution
|
||||
*
|
||||
* Verifies the targeting expression used in HandleProcTriggerSpellAuraProc
|
||||
* and HandleProcTriggerSpellWithValueAuraProc:
|
||||
*
|
||||
* triggerTarget = (triggerCaster == actor) ? actionTarget : actor
|
||||
*
|
||||
* This expression correctly resolves targets for all proc scenarios:
|
||||
* - Actor-side HIT phase: triggerCaster==actor, returns enemy (actionTarget)
|
||||
* - Actor-side FINISH phase: triggerCaster==actor, returns nullptr (actionTarget
|
||||
* is nullptr because FINISH phase passes no victim)
|
||||
* - Victim-side HIT phase: triggerCaster!=actor, returns attacker (actor)
|
||||
*/
|
||||
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "Unit.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
// Use fake Unit* pointers for testing. The smart targeting expression only
|
||||
// performs pointer comparison (==), never dereferences, so these are safe.
|
||||
namespace
|
||||
{
|
||||
Unit* const FAKE_ROGUE = reinterpret_cast<Unit*>(uintptr_t(0x1000));
|
||||
Unit* const FAKE_ENEMY = reinterpret_cast<Unit*>(uintptr_t(0x2000));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Applies the smart targeting expression from SpellAuraEffects.cpp
|
||||
*
|
||||
* This mirrors the logic in HandleProcTriggerSpellAuraProc:
|
||||
* Unit* triggerCaster = aurApp->GetTarget(); // the aura owner
|
||||
* Unit* triggerTarget = triggerCaster == eventInfo.GetActor()
|
||||
* ? eventInfo.GetActionTarget()
|
||||
* : eventInfo.GetActor();
|
||||
*/
|
||||
static Unit* ResolveProcTriggerTarget(Unit* triggerCaster, ProcEventInfo& eventInfo)
|
||||
{
|
||||
return triggerCaster == eventInfo.GetActor()
|
||||
? eventInfo.GetActionTarget()
|
||||
: eventInfo.GetActor();
|
||||
}
|
||||
|
||||
class SpellProcTargetResolutionTest : public ::testing::Test {};
|
||||
|
||||
// =============================================================================
|
||||
// Actor-side proc scenarios (aura owner == event actor)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcTargetResolutionTest, ActorSide_HitPhase_TargetsEnemy)
|
||||
{
|
||||
// Scenario: Rogue has Ruthlessness aura, casts Eviscerate on enemy.
|
||||
// HIT phase: actor=Rogue, actionTarget=Enemy
|
||||
// triggerCaster is the aura owner (Rogue), which == actor
|
||||
// Result: returns actionTarget (Enemy)
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithActor(FAKE_ROGUE)
|
||||
.WithActionTarget(FAKE_ENEMY)
|
||||
.WithTypeMask(PROC_FLAG_DONE_SPELL_MELEE_DMG_CLASS)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.Build();
|
||||
|
||||
Unit* triggerCaster = FAKE_ROGUE; // aura owner
|
||||
Unit* result = ResolveProcTriggerTarget(triggerCaster, eventInfo);
|
||||
|
||||
EXPECT_EQ(result, FAKE_ENEMY);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTargetResolutionTest, ActorSide_FinishPhase_ReturnsNullptr)
|
||||
{
|
||||
// Scenario: Rogue has Ruthlessness aura, finishes Eviscerate.
|
||||
// FINISH phase: actor=Rogue, actionTarget=nullptr (no victim passed)
|
||||
// triggerCaster is the aura owner (Rogue), which == actor
|
||||
// Result: returns nullptr (CastSpell handles this via implicit targeting)
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithActor(FAKE_ROGUE)
|
||||
.WithActionTarget(nullptr)
|
||||
.WithTypeMask(PROC_FLAG_DONE_SPELL_MELEE_DMG_CLASS)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_FINISH)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.Build();
|
||||
|
||||
Unit* triggerCaster = FAKE_ROGUE; // aura owner
|
||||
Unit* result = ResolveProcTriggerTarget(triggerCaster, eventInfo);
|
||||
|
||||
EXPECT_EQ(result, nullptr);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTargetResolutionTest, ActorSide_CastPhase_TargetsEnemy)
|
||||
{
|
||||
// Scenario: Actor-side CAST phase proc (e.g., Nature's Grace).
|
||||
// actor=Rogue, actionTarget=Enemy
|
||||
// triggerCaster == actor, returns actionTarget
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithActor(FAKE_ROGUE)
|
||||
.WithActionTarget(FAKE_ENEMY)
|
||||
.WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
|
||||
.Build();
|
||||
|
||||
Unit* triggerCaster = FAKE_ROGUE;
|
||||
Unit* result = ResolveProcTriggerTarget(triggerCaster, eventInfo);
|
||||
|
||||
EXPECT_EQ(result, FAKE_ENEMY);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Victim-side proc scenarios (aura owner != event actor)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcTargetResolutionTest, VictimSide_HitPhase_TargetsAttacker)
|
||||
{
|
||||
// Scenario: Enemy has a "when hit" proc aura. Rogue hits Enemy.
|
||||
// HIT phase: actor=Rogue (attacker), actionTarget=Enemy (victim)
|
||||
// triggerCaster is the aura owner (Enemy), which != actor (Rogue)
|
||||
// Result: returns actor (Rogue) — the attacker
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithActor(FAKE_ROGUE)
|
||||
.WithActionTarget(FAKE_ENEMY)
|
||||
.WithTypeMask(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.Build();
|
||||
|
||||
Unit* triggerCaster = FAKE_ENEMY; // aura owner (victim)
|
||||
Unit* result = ResolveProcTriggerTarget(triggerCaster, eventInfo);
|
||||
|
||||
EXPECT_EQ(result, FAKE_ROGUE);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Edge cases
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcTargetResolutionTest, ActorSide_NullActionTarget_ReturnsNullptr)
|
||||
{
|
||||
// Generic test: when actor-side proc has nullptr actionTarget,
|
||||
// result is nullptr regardless of phase
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithActor(FAKE_ROGUE)
|
||||
.WithActionTarget(nullptr)
|
||||
.WithTypeMask(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.Build();
|
||||
|
||||
Unit* triggerCaster = FAKE_ROGUE;
|
||||
Unit* result = ResolveProcTriggerTarget(triggerCaster, eventInfo);
|
||||
|
||||
EXPECT_EQ(result, nullptr);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTargetResolutionTest, VictimSide_NullActionTarget_StillReturnsActor)
|
||||
{
|
||||
// When victim-side proc has nullptr actionTarget, the expression
|
||||
// still returns actor (the attacker) since triggerCaster != actor
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithActor(FAKE_ROGUE)
|
||||
.WithActionTarget(nullptr)
|
||||
.WithTypeMask(PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.Build();
|
||||
|
||||
Unit* triggerCaster = FAKE_ENEMY; // aura owner (victim), != actor
|
||||
Unit* result = ResolveProcTriggerTarget(triggerCaster, eventInfo);
|
||||
|
||||
EXPECT_EQ(result, FAKE_ROGUE);
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTargetResolutionTest, SelfProc_ActorIsActionTarget)
|
||||
{
|
||||
// Edge case: actor == actionTarget (self-damage/self-heal)
|
||||
// triggerCaster == actor, returns actionTarget (which is also actor)
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithActor(FAKE_ROGUE)
|
||||
.WithActionTarget(FAKE_ROGUE)
|
||||
.WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.Build();
|
||||
|
||||
Unit* triggerCaster = FAKE_ROGUE;
|
||||
Unit* result = ResolveProcTriggerTarget(triggerCaster, eventInfo);
|
||||
|
||||
EXPECT_EQ(result, FAKE_ROGUE);
|
||||
}
|
||||
@@ -0,0 +1,949 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#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<WorldMock>();
|
||||
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>* _worldMock = nullptr;
|
||||
std::vector<SpellInfo*> _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));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,499 @@
|
||||
/*
|
||||
* 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 SpellProcTriggeredFilterTest.cpp
|
||||
* @brief Unit tests for triggered spell filtering in proc system
|
||||
*
|
||||
* Tests the logic from SpellAuras.cpp:2191-2209:
|
||||
* - Self-loop prevention (spell triggered by same aura)
|
||||
* - Triggered spell blocking (default behavior)
|
||||
* - SPELL_ATTR3_CAN_PROC_FROM_PROCS exception
|
||||
* - PROC_ATTR_TRIGGERED_CAN_PROC exception
|
||||
* - SPELL_ATTR3_NOT_A_PROC exception
|
||||
* - AUTO_ATTACK_PROC_FLAG_MASK exception
|
||||
*/
|
||||
|
||||
#include "ProcChanceTestHelper.h"
|
||||
#include "ProcEventInfoHelper.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
class SpellProcTriggeredFilterTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
|
||||
// Helper to create default proc entry
|
||||
SpellProcEntry CreateBasicProcEntry()
|
||||
{
|
||||
return SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Self-Loop Prevention Tests - SpellAuras.cpp:2191-2192
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, SelfLoop_BlocksWhenTriggeredBySameAura)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.triggeredByAuraSpellId = 12345; // Same as proc aura
|
||||
config.procAuraSpellId = 12345;
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// Self-loop should be blocked
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "Self-loop should block proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, SelfLoop_AllowsWhenTriggeredByDifferentAura)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.triggeredByAuraSpellId = 12345; // Different from proc aura
|
||||
config.procAuraSpellId = 67890;
|
||||
config.auraHasCanProcFromProcs = true; // Allow triggered spells
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// Different aura should be allowed
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "Different aura trigger should allow proc";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, SelfLoop_AllowsWhenNotTriggered)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = false; // Not a triggered spell
|
||||
config.triggeredByAuraSpellId = 0;
|
||||
config.procAuraSpellId = 12345;
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// Non-triggered spell should be allowed
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "Non-triggered spell should allow proc";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Triggered Spell Blocking - Default Behavior
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, TriggeredSpell_BlockedByDefault)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
config.spellHasNotAProc = false;
|
||||
|
||||
// No TRIGGERED_CAN_PROC attribute
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// Should be blocked - no exceptions apply
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "Triggered spell should be blocked by default";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, NonTriggeredSpell_AllowedByDefault)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = false; // Not triggered
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
config.spellHasNotAProc = false;
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// Should be allowed
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "Non-triggered spell should be allowed";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SPELL_ATTR3_CAN_PROC_FROM_PROCS Exception
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, CanProcFromProcs_AllowsTriggeredSpells)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = true; // Exception: aura has SPELL_ATTR3_CAN_PROC_FROM_PROCS
|
||||
config.spellHasNotAProc = false;
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// Should be allowed due to aura attribute
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "SPELL_ATTR3_CAN_PROC_FROM_PROCS should allow triggered spells";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROC_ATTR_TRIGGERED_CAN_PROC Exception
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, TriggeredCanProcAttribute_AllowsTriggeredSpells)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
config.spellHasNotAProc = false;
|
||||
|
||||
// Set TRIGGERED_CAN_PROC attribute
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithAttributesMask(PROC_ATTR_TRIGGERED_CAN_PROC)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Should be allowed due to proc entry attribute
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "PROC_ATTR_TRIGGERED_CAN_PROC should allow triggered spells";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SPELL_ATTR3_NOT_A_PROC Exception
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, NotAProc_AllowsTriggeredSpell)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
config.spellHasNotAProc = true; // Exception: spell has SPELL_ATTR3_NOT_A_PROC
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// Should be allowed due to spell attribute
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "SPELL_ATTR3_NOT_A_PROC should allow triggered spell";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AUTO_ATTACK_PROC_FLAG_MASK Exception
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, AutoAttackMelee_AllowsTriggeredSpells)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
config.spellHasNotAProc = false;
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// Event mask includes auto-attack - exception applies
|
||||
uint32 autoAttackEvent = PROC_FLAG_DONE_MELEE_AUTO_ATTACK;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, autoAttackEvent))
|
||||
<< "AUTO_ATTACK_PROC_FLAG_MASK (melee) should allow triggered spells";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, AutoAttackRanged_AllowsTriggeredSpells)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
config.spellHasNotAProc = false;
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// Hunter auto-shot or wand (ranged auto-attack)
|
||||
uint32 rangedAutoEvent = PROC_FLAG_DONE_RANGED_AUTO_ATTACK;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, rangedAutoEvent))
|
||||
<< "AUTO_ATTACK_PROC_FLAG_MASK (ranged) should allow triggered spells";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, TakenAutoAttack_AllowsTriggeredSpells)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
config.spellHasNotAProc = false;
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// Taken melee auto-attack
|
||||
uint32 takenMeleeEvent = PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, takenMeleeEvent))
|
||||
<< "TAKEN_MELEE_AUTO_ATTACK should allow triggered spells";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROC_FLAG_KILL / PROC_FLAG_KILLED / PROC_FLAG_DEATH Exception
|
||||
// Kill/death events bypass the triggered-spell filter because
|
||||
// the kill itself is the proc trigger, not the spell that dealt
|
||||
// the killing blow.
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, KillEvent_AllowsTriggeredSpells)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
config.spellHasNotAProc = false;
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// PROC_FLAG_KILL should bypass triggered-spell filter
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_KILL))
|
||||
<< "PROC_FLAG_KILL should allow triggered spells";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, KilledEvent_AllowsTriggeredSpells)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
config.spellHasNotAProc = false;
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// PROC_FLAG_KILLED should bypass triggered-spell filter
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_KILLED))
|
||||
<< "PROC_FLAG_KILLED should allow triggered spells";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, DeathEvent_AllowsTriggeredSpells)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
config.spellHasNotAProc = false;
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// PROC_FLAG_DEATH should bypass triggered-spell filter
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DEATH))
|
||||
<< "PROC_FLAG_DEATH should allow triggered spells";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, KillWithOtherFlags_StillAllowsTriggeredSpells)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
config.spellHasNotAProc = false;
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// PROC_FLAG_KILL combined with other flags should still bypass
|
||||
uint32 combinedEvent = PROC_FLAG_KILL | PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG;
|
||||
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, combinedEvent))
|
||||
<< "PROC_FLAG_KILL combined with other flags should still bypass filter";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, Scenario_RapidKilling_AutoShotKill)
|
||||
{
|
||||
// Rapid Killing (34949) has PROC_FLAG_KILL
|
||||
// Auto Shot (75) repeated casts are IsTriggered=true
|
||||
// Kill event should proc Rapid Killing regardless
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true; // Auto Shot is triggered
|
||||
config.auraHasCanProcFromProcs = false; // Rapid Killing doesn't have this
|
||||
config.spellHasNotAProc = false; // Auto Shot doesn't have NOT_A_PROC
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_KILL)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// PROC_FLAG_KILL exemption should allow this
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_KILL))
|
||||
<< "Rapid Killing should proc from Auto Shot kill";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, KillEvent_SelfLoopStillBlocked)
|
||||
{
|
||||
// Even with kill event exemption, self-loop should still be blocked
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.triggeredByAuraSpellId = 34949; // Same as proc aura
|
||||
config.procAuraSpellId = 34949;
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_KILL)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_KILL))
|
||||
<< "Self-loop should still block even on kill events";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Combined Scenarios
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, Combined_SelfLoopTakesPrecedence)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.triggeredByAuraSpellId = 12345;
|
||||
config.procAuraSpellId = 12345; // Self-loop
|
||||
config.auraHasCanProcFromProcs = true; // Would normally allow
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithAttributesMask(PROC_ATTR_TRIGGERED_CAN_PROC)
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Self-loop should still block even with exceptions
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "Self-loop should block even when TRIGGERED_CAN_PROC is set";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, Combined_MultipleExceptions)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = true; // Exception 1
|
||||
config.spellHasNotAProc = true; // Exception 2
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithAttributesMask(PROC_ATTR_TRIGGERED_CAN_PROC) // Exception 3
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// Should be allowed (multiple exceptions all pass)
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "Multiple exceptions should still allow proc";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real Spell Scenarios
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, Scenario_HotStreak_TriggeredPyroblast)
|
||||
{
|
||||
// Hot Streak (48108) allows triggered Pyroblast to not proc it again
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true; // Pyroblast was triggered by Hot Streak
|
||||
config.triggeredByAuraSpellId = 48108; // Hot Streak
|
||||
config.procAuraSpellId = 48108; // Hot Streak is checking if it should proc
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// Self-loop: Hot Streak can't proc from spell it triggered
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "Hot Streak triggered Pyroblast should not proc Hot Streak";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, Scenario_SwordSpec_ChainProcs)
|
||||
{
|
||||
// Sword Specialization with TRIGGERED_CAN_PROC
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
config.triggeredByAuraSpellId = 12345; // Some other proc
|
||||
config.procAuraSpellId = 16459; // Sword Specialization
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithAttributesMask(PROC_ATTR_TRIGGERED_CAN_PROC)
|
||||
.WithChance(5.0f)
|
||||
.Build();
|
||||
|
||||
// TRIGGERED_CAN_PROC allows chain procs (but not self-loops)
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_MELEE_AUTO_ATTACK))
|
||||
<< "Sword Spec with TRIGGERED_CAN_PROC should allow chain procs";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, Scenario_WindfuryWeapon_AutoAttack)
|
||||
{
|
||||
// Windfury Weapon procs from auto-attacks, which are allowed for triggered spells
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true; // Windfury extra attacks are triggered
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
|
||||
.WithProcsPerMinute(2.0f)
|
||||
.Build();
|
||||
|
||||
// Auto-attack exception allows triggered Windfury attacks
|
||||
EXPECT_FALSE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_MELEE_AUTO_ATTACK))
|
||||
<< "Windfury triggered attacks should be allowed (auto-attack exception)";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Edge Cases
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, EdgeCase_ZeroEventMask)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
|
||||
auto procEntry = CreateBasicProcEntry();
|
||||
|
||||
// Zero event mask means no auto-attack exception
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, 0))
|
||||
<< "Zero event mask should not grant auto-attack exception";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTriggeredFilterTest, EdgeCase_AllExceptionsDisabled)
|
||||
{
|
||||
ProcChanceTestHelper::TriggeredSpellConfig config;
|
||||
config.isTriggered = true;
|
||||
config.auraHasCanProcFromProcs = false;
|
||||
config.spellHasNotAProc = false;
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithAttributesMask(0) // No TRIGGERED_CAN_PROC
|
||||
.WithChance(100.0f)
|
||||
.Build();
|
||||
|
||||
// No exceptions - should block
|
||||
EXPECT_TRUE(ProcChanceTestHelper::ShouldBlockTriggeredSpell(
|
||||
config, procEntry, PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG))
|
||||
<< "No exceptions should block triggered spell";
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* 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 SpellScriptMissileBarrageTest.cpp
|
||||
* @brief Unit tests for Missile Barrage (44404-44408) proc behavior
|
||||
*
|
||||
* Missile Barrage talent should proc:
|
||||
* - 100% chance when casting Arcane Blast (SpellFamilyFlags[0] & 0x20000000)
|
||||
* - 50% reduced chance when casting other spells (Arcane Barrage, Frostfire Bolt, etc.)
|
||||
*
|
||||
* DBC Base proc chances by rank:
|
||||
* - Rank 1 (44404): 4%
|
||||
* - Rank 2 (44405): 8%
|
||||
* - Rank 3 (44406): 12%
|
||||
* - Rank 4 (44407): 16%
|
||||
* - Rank 5 (44408): 20%
|
||||
*
|
||||
* Effective proc rates:
|
||||
* - Arcane Blast: Full DBC chance (4-20%)
|
||||
* - Other spells: 50% of DBC chance (2-10%)
|
||||
*/
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "SharedDefines.h"
|
||||
|
||||
// =============================================================================
|
||||
// Missile Barrage Script Logic Simulation
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief Simulates the CheckProc logic from spell_mage_missile_barrage
|
||||
*
|
||||
* This mirrors the actual script at:
|
||||
* src/server/scripts/Spells/spell_mage.cpp:1325-1338
|
||||
*
|
||||
* @param spellFamilyFlags0 The SpellFamilyFlags[0] of the triggering spell
|
||||
* @param rollResult The result of roll_chance_i(50) - pass 0-49 to succeed, 50-99 to fail
|
||||
* @return true if the proc check passes
|
||||
*/
|
||||
bool SimulateMissileBarrageCheckProc(uint32 spellFamilyFlags0, int rollResult)
|
||||
{
|
||||
// Arcane Blast - full proc chance (100%)
|
||||
// Arcane Blast spell family flags: 0x20000000
|
||||
if (spellFamilyFlags0 & 0x20000000)
|
||||
return true;
|
||||
|
||||
// Other spells - 50% proc chance
|
||||
// Simulates: return roll_chance_i(50);
|
||||
return rollResult < 50;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the SpellFamilyFlags[0] for common Mage spells
|
||||
*/
|
||||
namespace MageSpellFlags
|
||||
{
|
||||
constexpr uint32 ARCANE_BLAST = 0x20000000;
|
||||
constexpr uint32 ARCANE_MISSILES = 0x00000020;
|
||||
constexpr uint32 FIREBALL = 0x00000001;
|
||||
constexpr uint32 FROSTFIRE_BOLT = 0x00000000; // Uses SpellFamilyFlags[1]
|
||||
constexpr uint32 ARCANE_BARRAGE = 0x00000000; // Uses SpellFamilyFlags[1]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test Fixture
|
||||
// =============================================================================
|
||||
|
||||
class MissileBarrageTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
void TearDown() override {}
|
||||
|
||||
/**
|
||||
* @brief Run multiple proc checks and return the success rate
|
||||
* @param spellFamilyFlags0 The spell flags to test
|
||||
* @param iterations Number of iterations
|
||||
* @return Success rate as percentage (0-100)
|
||||
*/
|
||||
float RunStatisticalTest(uint32 spellFamilyFlags0, int iterations = 10000)
|
||||
{
|
||||
int successes = 0;
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
// Simulate random roll 0-99
|
||||
int roll = i % 100;
|
||||
if (SimulateMissileBarrageCheckProc(spellFamilyFlags0, roll))
|
||||
successes++;
|
||||
}
|
||||
return (float)successes / iterations * 100.0f;
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Deterministic Tests - Arcane Blast
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(MissileBarrageTest, ArcaneBlast_AlwaysProcs_RegardlessOfRoll)
|
||||
{
|
||||
// Arcane Blast should always pass CheckProc, regardless of the roll result
|
||||
for (int roll = 0; roll < 100; roll++)
|
||||
{
|
||||
EXPECT_TRUE(SimulateMissileBarrageCheckProc(MageSpellFlags::ARCANE_BLAST, roll))
|
||||
<< "Arcane Blast should always proc, but failed with roll=" << roll;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MissileBarrageTest, ArcaneBlast_Returns100PercentRate)
|
||||
{
|
||||
float rate = RunStatisticalTest(MageSpellFlags::ARCANE_BLAST);
|
||||
EXPECT_NEAR(rate, 100.0f, 0.01f) << "Arcane Blast should have 100% CheckProc pass rate";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Deterministic Tests - Other Spells (50% Reduction)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(MissileBarrageTest, Fireball_ProcsOnLowRoll)
|
||||
{
|
||||
// Rolls 0-49 should succeed
|
||||
for (int roll = 0; roll < 50; roll++)
|
||||
{
|
||||
EXPECT_TRUE(SimulateMissileBarrageCheckProc(MageSpellFlags::FIREBALL, roll))
|
||||
<< "Fireball should proc with roll=" << roll << " (< 50)";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MissileBarrageTest, Fireball_FailsOnHighRoll)
|
||||
{
|
||||
// Rolls 50-99 should fail
|
||||
for (int roll = 50; roll < 100; roll++)
|
||||
{
|
||||
EXPECT_FALSE(SimulateMissileBarrageCheckProc(MageSpellFlags::FIREBALL, roll))
|
||||
<< "Fireball should NOT proc with roll=" << roll << " (>= 50)";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MissileBarrageTest, Fireball_Returns50PercentRate)
|
||||
{
|
||||
float rate = RunStatisticalTest(MageSpellFlags::FIREBALL);
|
||||
EXPECT_NEAR(rate, 50.0f, 0.01f) << "Fireball should have 50% CheckProc pass rate";
|
||||
}
|
||||
|
||||
TEST_F(MissileBarrageTest, ArcaneMissiles_Returns50PercentRate)
|
||||
{
|
||||
float rate = RunStatisticalTest(MageSpellFlags::ARCANE_MISSILES);
|
||||
EXPECT_NEAR(rate, 50.0f, 0.01f) << "Arcane Missiles should have 50% CheckProc pass rate";
|
||||
}
|
||||
|
||||
TEST_F(MissileBarrageTest, OtherSpells_Returns50PercentRate)
|
||||
{
|
||||
// Any spell that doesn't have the Arcane Blast flag should get 50% rate
|
||||
float rate = RunStatisticalTest(0x00000000);
|
||||
EXPECT_NEAR(rate, 50.0f, 0.01f) << "Other spells should have 50% CheckProc pass rate";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Effective Proc Rate Tests
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief Calculate the effective proc rate combining DBC chance and CheckProc
|
||||
* @param dbcChance Base proc chance from DBC (e.g., 20 for rank 5)
|
||||
* @param checkProcRate CheckProc pass rate (100 for Arcane Blast, 50 for others)
|
||||
* @return Effective proc rate as percentage
|
||||
*/
|
||||
float CalculateEffectiveProcRate(float dbcChance, float checkProcRate)
|
||||
{
|
||||
return dbcChance * (checkProcRate / 100.0f);
|
||||
}
|
||||
|
||||
TEST_F(MissileBarrageTest, EffectiveRate_ArcaneBlast_Rank5)
|
||||
{
|
||||
// Rank 5: 20% base chance * 100% CheckProc = 20% effective
|
||||
float effective = CalculateEffectiveProcRate(20.0f, 100.0f);
|
||||
EXPECT_NEAR(effective, 20.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(MissileBarrageTest, EffectiveRate_Fireball_Rank5)
|
||||
{
|
||||
// Rank 5: 20% base chance * 50% CheckProc = 10% effective
|
||||
float effective = CalculateEffectiveProcRate(20.0f, 50.0f);
|
||||
EXPECT_NEAR(effective, 10.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(MissileBarrageTest, EffectiveRate_ArcaneBlast_Rank1)
|
||||
{
|
||||
// Rank 1: 4% base chance * 100% CheckProc = 4% effective
|
||||
float effective = CalculateEffectiveProcRate(4.0f, 100.0f);
|
||||
EXPECT_NEAR(effective, 4.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(MissileBarrageTest, EffectiveRate_Fireball_Rank1)
|
||||
{
|
||||
// Rank 1: 4% base chance * 50% CheckProc = 2% effective
|
||||
float effective = CalculateEffectiveProcRate(4.0f, 50.0f);
|
||||
EXPECT_NEAR(effective, 2.0f, 0.01f);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DBC Data Validation
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(MissileBarrageTest, DBCProcChances_MatchExpectedValues)
|
||||
{
|
||||
// Expected DBC proc chances for each rank
|
||||
// Note: These should match the actual DBC values
|
||||
struct RankData
|
||||
{
|
||||
uint32 spellId;
|
||||
int expectedChance;
|
||||
};
|
||||
|
||||
std::vector<RankData> ranks = {
|
||||
{ 44404, 4 }, // Rank 1: 4% (actually 8% in some versions)
|
||||
{ 44405, 8 }, // Rank 2
|
||||
{ 44406, 12 }, // Rank 3
|
||||
{ 44407, 16 }, // Rank 4
|
||||
{ 44408, 20 }, // Rank 5
|
||||
};
|
||||
|
||||
// This documents the expected values - actual verification would require SpellMgr
|
||||
for (auto const& rank : ranks)
|
||||
{
|
||||
SCOPED_TRACE("Spell ID: " + std::to_string(rank.spellId));
|
||||
// The actual DBC lookup would be:
|
||||
// SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(rank.spellId);
|
||||
// EXPECT_EQ(spellInfo->ProcChance, rank.expectedChance);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Boundary Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(MissileBarrageTest, BoundaryRoll_49_Succeeds)
|
||||
{
|
||||
// Roll of 49 should succeed (< 50)
|
||||
EXPECT_TRUE(SimulateMissileBarrageCheckProc(MageSpellFlags::FIREBALL, 49));
|
||||
}
|
||||
|
||||
TEST_F(MissileBarrageTest, BoundaryRoll_50_Fails)
|
||||
{
|
||||
// Roll of 50 should fail (>= 50)
|
||||
EXPECT_FALSE(SimulateMissileBarrageCheckProc(MageSpellFlags::FIREBALL, 50));
|
||||
}
|
||||
|
||||
TEST_F(MissileBarrageTest, ArcaneBlastFlag_ExactMatch)
|
||||
{
|
||||
// Test that exactly the Arcane Blast flag triggers 100% rate
|
||||
EXPECT_TRUE(SimulateMissileBarrageCheckProc(0x20000000, 99));
|
||||
|
||||
// Combined flags should also work if Arcane Blast is present
|
||||
EXPECT_TRUE(SimulateMissileBarrageCheckProc(0x20000001, 99));
|
||||
EXPECT_TRUE(SimulateMissileBarrageCheckProc(0x20000020, 99));
|
||||
EXPECT_TRUE(SimulateMissileBarrageCheckProc(0xFFFFFFFF, 99));
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* 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 WildGrowthTickScalingTest.cpp
|
||||
* @brief Tests for Wild Growth tick scaling formula
|
||||
*
|
||||
* Wild Growth heals with a front-loaded pattern: first tick heals most,
|
||||
* each subsequent tick heals less. The formula (from spell_dru_wild_growth_aura):
|
||||
*
|
||||
* bonus = 6.0 - baseReduction * (tickNumber - 1)
|
||||
* amount = baseTick + baseTick * bonus / 100
|
||||
*
|
||||
* Where baseTick MUST include caster spell power bonuses (set via
|
||||
* DoEffectCalcAmount, which runs after SpellHealingBonusDone in
|
||||
* CalculateAmount). See TrinityCore issue #21281.
|
||||
*
|
||||
* baseReduction defaults to 2.0, reduced by T10 Restoration 2P Bonus.
|
||||
* Wild Growth has 7 ticks (7s duration, 1s amplitude).
|
||||
*/
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <numeric>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int32_t TOTAL_TICKS = 7;
|
||||
constexpr float DEFAULT_REDUCTION = 2.0f;
|
||||
|
||||
/// Mirrors CalculatePct from Define.h: base * pct / 100
|
||||
template <typename T>
|
||||
T CalcPct(T base, float pct)
|
||||
{
|
||||
return static_cast<T>(base * pct / 100.0f);
|
||||
}
|
||||
|
||||
/// Mirrors spell_dru_wild_growth_aura::HandleTickUpdate
|
||||
int32_t CalcWildGrowthTickAmount(int32_t baseTick, uint32_t tickNumber, float baseReduction)
|
||||
{
|
||||
float reduction = baseReduction * static_cast<float>(tickNumber - 1);
|
||||
float bonus = 6.0f - reduction;
|
||||
return static_cast<int32_t>(baseTick + CalcPct(baseTick, bonus));
|
||||
}
|
||||
|
||||
/// Calculate all tick amounts for a Wild Growth cast
|
||||
std::vector<int32_t> CalcAllTicks(int32_t baseTick, float baseReduction = DEFAULT_REDUCTION)
|
||||
{
|
||||
std::vector<int32_t> ticks;
|
||||
ticks.reserve(TOTAL_TICKS);
|
||||
for (uint32_t i = 1; i <= TOTAL_TICKS; ++i)
|
||||
ticks.push_back(CalcWildGrowthTickAmount(baseTick, i, baseReduction));
|
||||
return ticks;
|
||||
}
|
||||
}
|
||||
|
||||
class WildGrowthTickScalingTest : public ::testing::Test {};
|
||||
|
||||
// =============================================================================
|
||||
// Formula correctness with spell power included in baseTick
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(WildGrowthTickScalingTest, FirstTickIsHighest)
|
||||
{
|
||||
// baseTick=600 simulates a spell-power-inclusive value
|
||||
auto ticks = CalcAllTicks(600);
|
||||
|
||||
for (int i = 1; i < TOTAL_TICKS; ++i)
|
||||
EXPECT_GT(ticks[0], ticks[i]) << "Tick 1 should be highest, but tick " << (i + 1) << " is higher";
|
||||
}
|
||||
|
||||
TEST_F(WildGrowthTickScalingTest, LastTickIsLowest)
|
||||
{
|
||||
auto ticks = CalcAllTicks(600);
|
||||
|
||||
for (int i = 0; i < TOTAL_TICKS - 1; ++i)
|
||||
EXPECT_LT(ticks[TOTAL_TICKS - 1], ticks[i]) << "Tick 7 should be lowest, but tick " << (i + 1) << " is lower";
|
||||
}
|
||||
|
||||
TEST_F(WildGrowthTickScalingTest, TicksAreMonotonicallyDecreasing)
|
||||
{
|
||||
auto ticks = CalcAllTicks(600);
|
||||
|
||||
for (int i = 1; i < TOTAL_TICKS; ++i)
|
||||
EXPECT_LE(ticks[i], ticks[i - 1]) << "Tick " << (i + 1) << " should be <= tick " << i;
|
||||
}
|
||||
|
||||
TEST_F(WildGrowthTickScalingTest, TotalHealingPreserved)
|
||||
{
|
||||
// Sum of all ticks should equal 7 * baseTick (scaling is symmetric)
|
||||
int32_t const baseTick = 600;
|
||||
auto ticks = CalcAllTicks(baseTick);
|
||||
|
||||
int32_t totalHealing = std::accumulate(ticks.begin(), ticks.end(), 0);
|
||||
int32_t expectedTotal = baseTick * TOTAL_TICKS;
|
||||
|
||||
// Allow +-1 per tick for integer truncation
|
||||
EXPECT_NEAR(totalHealing, expectedTotal, TOTAL_TICKS);
|
||||
}
|
||||
|
||||
TEST_F(WildGrowthTickScalingTest, MiddleTickEqualsBase)
|
||||
{
|
||||
// Tick 4 (middle) has 0% bonus, so it equals baseTick exactly
|
||||
int32_t const baseTick = 600;
|
||||
int32_t tick4 = CalcWildGrowthTickAmount(baseTick, 4, DEFAULT_REDUCTION);
|
||||
|
||||
EXPECT_EQ(tick4, baseTick);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Specific tick values with spell power
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(WildGrowthTickScalingTest, TickValues_WithSpellPower)
|
||||
{
|
||||
// baseTick = 600 (raw base ~206 + spell power ~394)
|
||||
int32_t const baseTick = 600;
|
||||
auto ticks = CalcAllTicks(baseTick);
|
||||
|
||||
// Tick 1: 600 + 6% = 636
|
||||
EXPECT_EQ(ticks[0], 636);
|
||||
// Tick 2: 600 + 4% = 624
|
||||
EXPECT_EQ(ticks[1], 624);
|
||||
// Tick 3: 600 + 2% = 612
|
||||
EXPECT_EQ(ticks[2], 612);
|
||||
// Tick 4: 600 + 0% = 600
|
||||
EXPECT_EQ(ticks[3], 600);
|
||||
// Tick 5: 600 - 2% = 588
|
||||
EXPECT_EQ(ticks[4], 588);
|
||||
// Tick 6: 600 - 4% = 576
|
||||
EXPECT_EQ(ticks[5], 576);
|
||||
// Tick 7: 600 - 6% = 564
|
||||
EXPECT_EQ(ticks[6], 564);
|
||||
}
|
||||
|
||||
TEST_F(WildGrowthTickScalingTest, TickValues_RawBaseOnly_WouldBeBroken)
|
||||
{
|
||||
// If baseTick were only the raw base (206) without spell power,
|
||||
// ticks would be far too low. This documents the bug from
|
||||
// TrinityCore #21281 / AC's CallScriptEffectCalcAmountHandlers
|
||||
// ordering issue.
|
||||
int32_t const rawBase = 206;
|
||||
auto ticks = CalcAllTicks(rawBase);
|
||||
|
||||
// Tick 1 without spell power: 206 + 6% = 218
|
||||
EXPECT_EQ(ticks[0], 218);
|
||||
// This is ~1/3 of the correct value (636), matching the player report
|
||||
EXPECT_LT(ticks[0], 636 / 2) << "Raw base ticks are less than half the SP-inclusive value";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// T10 Restoration 2P Bonus (reduces tick drop-off rate)
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(WildGrowthTickScalingTest, T10_2P_ReducesReduction)
|
||||
{
|
||||
// T10 2P reduces the drop rate. If bonus amount is 30%,
|
||||
// baseReduction = 2.0 - 2.0 * 30/100 = 1.4
|
||||
float baseReduction = DEFAULT_REDUCTION;
|
||||
float t10BonusAmount = 30.0f;
|
||||
baseReduction -= baseReduction * t10BonusAmount / 100.0f;
|
||||
EXPECT_FLOAT_EQ(baseReduction, 1.4f);
|
||||
|
||||
auto ticks = CalcAllTicks(600, baseReduction);
|
||||
|
||||
// With reduced drop-off, ticks are more uniform
|
||||
// First tick: 600 + 6% = 636 (unchanged, bonus starts at 6%)
|
||||
EXPECT_EQ(ticks[0], 636);
|
||||
// Last tick: 600 + (6 - 1.4*6)% = 600 + (-2.4)% = 600 - 14.4 -> 586
|
||||
EXPECT_EQ(ticks[TOTAL_TICKS - 1], 586);
|
||||
|
||||
// Range is smaller with T10 2P
|
||||
int32_t rangeWithT10 = ticks[0] - ticks[TOTAL_TICKS - 1];
|
||||
auto normalTicks = CalcAllTicks(600);
|
||||
int32_t rangeNormal = normalTicks[0] - normalTicks[TOTAL_TICKS - 1];
|
||||
|
||||
EXPECT_LT(rangeWithT10, rangeNormal) << "T10 2P should reduce tick-to-tick variance";
|
||||
}
|
||||
|
||||
TEST_F(WildGrowthTickScalingTest, T10_2P_TotalHealingPreserved)
|
||||
{
|
||||
float baseReduction = DEFAULT_REDUCTION;
|
||||
baseReduction -= baseReduction * 30.0f / 100.0f;
|
||||
|
||||
int32_t const baseTick = 600;
|
||||
auto ticks = CalcAllTicks(baseTick, baseReduction);
|
||||
|
||||
int32_t totalHealing = std::accumulate(ticks.begin(), ticks.end(), 0);
|
||||
int32_t expectedTotal = baseTick * TOTAL_TICKS;
|
||||
|
||||
// With T10 2P the scaling is asymmetric (bonus starts at +6% but
|
||||
// only drops by 1.4% per tick instead of 2%), so total healing is
|
||||
// slightly higher than 7 * baseTick. This is expected and matches TC.
|
||||
EXPECT_GT(totalHealing, expectedTotal);
|
||||
// Should not exceed ~2% above baseline
|
||||
EXPECT_LT(totalHealing, expectedTotal * 102 / 100);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "GameTime.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include <thread>
|
||||
|
||||
TEST(GameTimeTest, Elapsed)
|
||||
{
|
||||
GameTime::UpdateGameTimers();
|
||||
auto start = GameTime::Now();
|
||||
std::this_thread::sleep_for(50ms);
|
||||
GameTime::UpdateGameTimers();
|
||||
auto elapsed = GameTime::Elapsed(start);
|
||||
EXPECT_GE(elapsed, 50ms);
|
||||
}
|
||||
|
||||
TEST(GameTimeTest, HasElapsedTrue)
|
||||
{
|
||||
GameTime::UpdateGameTimers();
|
||||
auto start = GameTime::Now();
|
||||
std::this_thread::sleep_for(50ms);
|
||||
GameTime::UpdateGameTimers();
|
||||
EXPECT_TRUE(GameTime::HasElapsed(start, 25ms));
|
||||
}
|
||||
|
||||
TEST(GameTimeTest, HasElapsedFalse)
|
||||
{
|
||||
GameTime::UpdateGameTimers();
|
||||
auto start = GameTime::Now();
|
||||
EXPECT_FALSE(GameTime::HasElapsed(start, 10s));
|
||||
}
|
||||
|
||||
TEST(GameTimeTest, HasElapsedWithSeconds)
|
||||
{
|
||||
GameTime::UpdateGameTimers();
|
||||
auto start = GameTime::Now();
|
||||
EXPECT_FALSE(GameTime::HasElapsed(start, 1s));
|
||||
}
|
||||
|
||||
TEST(GameTimeTest, HasElapsedWithMicroseconds)
|
||||
{
|
||||
GameTime::UpdateGameTimers();
|
||||
auto start = GameTime::Now();
|
||||
std::this_thread::sleep_for(100us);
|
||||
GameTime::UpdateGameTimers();
|
||||
EXPECT_TRUE(GameTime::HasElapsed(start, Microseconds(1)));
|
||||
}
|
||||
Reference in New Issue
Block a user