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

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