refactor(Core/Spells): QAston proc system (#24233)

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
Co-authored-by: QAston <qaston@gmail.com>
Co-authored-by: joschiwald <joschiwald@online.de>
Co-authored-by: ariel- <ariel-@users.noreply.github.com>
Co-authored-by: Kitzunu <24550914+Kitzunu@users.noreply.github.com>
Co-authored-by: blinkysc <your-github-email@example.com>
Co-authored-by: Tereneckla <Tereneckla@users.noreply.github.com>
Co-authored-by: Andrew <47818697+Nyeriah@users.noreply.github.com>
This commit is contained in:
blinkysc
2026-02-18 05:31:53 -06:00
committed by GitHub
parent 65a869ea27
commit 4599f26ae9
76 changed files with 22915 additions and 5181 deletions

View File

@@ -15,7 +15,7 @@ CollectSourceFiles(
)
include_directories(
"mocks"
"${CMAKE_CURRENT_SOURCE_DIR}/mocks"
)
add_executable(

View File

@@ -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

367
src/test/mocks/AuraStub.h Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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/>.
*/
#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)
* @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)
{
float chance = procEntry.Chance;
// PPM calculation overrides base chance if PPM > 0 and we have DamageInfo
if (hasDamageInfo && 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
if (!config.auraHasCanProcFromProcs &&
!(procEntry.AttributesMask & PROC_ATTR_TRIGGERED_CAN_PROC) &&
!(eventTypeMask & AUTO_ATTACK_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
}
// =============================================================================
// 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;
}
}
// =============================================================================
// 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;
};
#endif // AZEROTHCORE_PROC_CHANCE_TEST_HELPER_H

View File

@@ -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

View File

@@ -0,0 +1,215 @@
/*
* 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& 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;
}
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& 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;
}
// 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

244
src/test/mocks/UnitStub.h Normal file
View File

@@ -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

View File

@@ -0,0 +1,445 @@
/*
* 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 "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_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";
}
}
}

View File

@@ -0,0 +1,317 @@
/*
* 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_WithoutDamageInfo)
{
auto procEntry = SpellProcEntryBuilder()
.WithChance(50.0f)
.WithProcsPerMinute(6.0f)
.Build();
// Without DamageInfo, base chance is used
float result = ProcChanceTestHelper::SimulateCalcProcChance(
procEntry, 80, 2500, 0.0f, 0.0f, false);
EXPECT_NEAR(result, 50.0f, 0.01f);
}
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);
}

View File

@@ -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);
}

View File

@@ -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));
}

View File

@@ -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));
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}

View File

@@ -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";
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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";
}
}

View File

@@ -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));
}

View File

@@ -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)";
}

View File

@@ -0,0 +1,903 @@
/*
* 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));
}
// =============================================================================
// 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

View File

@@ -0,0 +1,391 @@
/*
* 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";
}
// =============================================================================
// 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";
}

View File

@@ -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));
}