Files
moonwell-core/src/test/mocks/ProcChanceTestHelper.h
T

783 lines
27 KiB
C++

/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef AZEROTHCORE_PROC_CHANCE_TEST_HELPER_H
#define AZEROTHCORE_PROC_CHANCE_TEST_HELPER_H
#include "SpellMgr.h"
#include "SpellInfo.h"
#include "AuraStub.h"
#include "UnitStub.h"
#include <algorithm>
#include <chrono>
/**
* @brief Helper class for testing proc chance calculations
*
* Provides standalone implementations of proc-related calculations
* that can be tested without requiring full game objects.
*/
class ProcChanceTestHelper
{
public:
/**
* @brief Calculate PPM proc chance
* Implements the formula: (WeaponSpeed * PPM) / 600.0f
*
* @param weaponSpeed Weapon attack speed in milliseconds
* @param ppm Procs per minute value
* @param ppmModifier Additional PPM modifier (from talents/auras)
* @return Proc chance as percentage (0-100+)
*/
static float CalculatePPMChance(uint32 weaponSpeed, float ppm, float ppmModifier = 0.0f)
{
if (ppm <= 0.0f)
return 0.0f;
float modifiedPPM = ppm + ppmModifier;
return (static_cast<float>(weaponSpeed) * modifiedPPM) / 600.0f;
}
/**
* @brief Calculate level 60+ reduction
* Implements PROC_ATTR_REDUCE_PROC_60: 3.333% reduction per level above 60
*
* @param baseChance Base proc chance
* @param actorLevel Actor's level
* @return Reduced proc chance
*/
static float ApplyLevel60Reduction(float baseChance, uint32 actorLevel)
{
if (actorLevel <= 60)
return baseChance;
// Reduction = (level - 60) / 30, capped at 1.0
float reduction = static_cast<float>(actorLevel - 60) / 30.0f;
return std::max(0.0f, (1.0f - reduction) * baseChance);
}
/**
* @brief Simulate CalcProcChance() from SpellAuras.cpp
*
* @param procEntry The proc configuration
* @param actorLevel Actor's level (for PROC_ATTR_REDUCE_PROC_60)
* @param weaponSpeed Weapon speed (for PPM calculation)
* @param chanceModifier Talent/aura modifier to chance
* @param ppmModifier Talent/aura modifier to PPM
* @param hasDamageInfo Whether a DamageInfo is present (enables PPM)
* @param hasHealInfo Whether a HealInfo is present (also enables PPM)
* @return Calculated proc chance
*/
static float SimulateCalcProcChance(
SpellProcEntry const& procEntry,
uint32 actorLevel = 80,
uint32 weaponSpeed = 2500,
float chanceModifier = 0.0f,
float ppmModifier = 0.0f,
bool hasDamageInfo = true,
bool hasHealInfo = false)
{
float chance = procEntry.Chance;
// PPM calculation overrides base chance if PPM > 0 and we have DamageInfo or HealInfo
if ((hasDamageInfo || hasHealInfo) && procEntry.ProcsPerMinute > 0.0f)
{
chance = CalculatePPMChance(weaponSpeed, procEntry.ProcsPerMinute, ppmModifier);
}
// Apply chance modifier (SPELLMOD_CHANCE_OF_SUCCESS)
chance += chanceModifier;
// Apply level 60+ reduction if attribute is set
if (procEntry.AttributesMask & PROC_ATTR_REDUCE_PROC_60)
{
chance = ApplyLevel60Reduction(chance, actorLevel);
}
return chance;
}
/**
* @brief Simulate charge consumption from ConsumeProcCharges()
*
* @param aura The aura stub to modify
* @param procEntry The proc configuration
* @return true if aura was removed (charges/stacks exhausted)
*/
static bool SimulateConsumeProcCharges(AuraStub* aura, SpellProcEntry const& procEntry)
{
if (!aura)
return false;
if (procEntry.AttributesMask & PROC_ATTR_USE_STACKS_FOR_CHARGES)
{
return aura->ModStackAmount(-1);
}
else if (aura->IsUsingCharges())
{
aura->DropCharge();
if (aura->GetCharges() == 0)
{
aura->Remove();
return true;
}
}
return false;
}
/**
* @brief Check if proc is on cooldown
*
* @param aura The aura stub
* @param now Current time point
* @return true if proc is blocked by cooldown
*/
static bool IsProcOnCooldown(AuraStub const* aura, std::chrono::steady_clock::time_point now)
{
if (!aura)
return false;
return aura->IsProcOnCooldown(now);
}
/**
* @brief Apply proc cooldown to aura
*
* @param aura The aura stub
* @param now Current time point
* @param cooldownMs Cooldown duration in milliseconds
*/
static void ApplyProcCooldown(AuraStub* aura, std::chrono::steady_clock::time_point now, uint32 cooldownMs)
{
if (!aura || cooldownMs == 0)
return;
aura->AddProcCooldown(now + std::chrono::milliseconds(cooldownMs));
}
/**
* @brief Check if spell has mana cost (for PROC_ATTR_REQ_MANA_COST)
*
* @param spellInfo The spell info to check
* @return true if spell has mana cost > 0
*/
static bool SpellHasManaCost(SpellInfo const* spellInfo)
{
if (!spellInfo)
return false;
return spellInfo->ManaCost > 0 || spellInfo->ManaCostPercentage > 0;
}
/**
* @brief Get common weapon speeds for testing
*/
static constexpr uint32 WEAPON_SPEED_FAST_DAGGER = 1400; // 1.4 sec
static constexpr uint32 WEAPON_SPEED_NORMAL_SWORD = 2500; // 2.5 sec
static constexpr uint32 WEAPON_SPEED_SLOW_2H = 3300; // 3.3 sec
static constexpr uint32 WEAPON_SPEED_VERY_SLOW = 3800; // 3.8 sec
static constexpr uint32 WEAPON_SPEED_STAFF = 3600; // 3.6 sec (common feral staff)
/**
* @brief Shapeshift form base attack speeds (from SpellShapeshiftForm.dbc)
*/
static constexpr uint32 FORM_SPEED_CAT = 1000; // Cat Form: 1.0 sec
static constexpr uint32 FORM_SPEED_BEAR = 2500; // Bear/Dire Bear: 2.5 sec
/**
* @brief Simulate effective procs per minute
*
* Given a per-swing chance and the actual swing interval, calculate
* how many procs occur per minute on average.
*
* @param chancePerSwing Proc chance per swing (0-100+)
* @param actualSwingSpeedMs Actual time between swings in milliseconds
* @return Average procs per minute
*/
static float CalculateEffectivePPM(float chancePerSwing, uint32 actualSwingSpeedMs)
{
if (actualSwingSpeedMs == 0)
return 0.0f;
float swingsPerMinute = 60000.0f / static_cast<float>(actualSwingSpeedMs);
return swingsPerMinute * (chancePerSwing / 100.0f);
}
/**
* @brief Common PPM values from spell_proc database
*/
static constexpr float PPM_OMEN_OF_CLARITY = 6.0f;
static constexpr float PPM_JUDGEMENT_OF_LIGHT = 15.0f;
static constexpr float PPM_WINDFURY_WEAPON = 2.0f;
// =============================================================================
// Triggered Spell Filtering - simulates SpellAuras.cpp:2191-2209
// =============================================================================
/**
* @brief Auto-attack proc flag mask (hunter auto-shot, wands exception)
* These triggered spells are allowed to proc without TRIGGERED_CAN_PROC
*/
static constexpr uint32 AUTO_ATTACK_PROC_FLAG_MASK =
PROC_FLAG_DONE_MELEE_AUTO_ATTACK | PROC_FLAG_TAKEN_MELEE_AUTO_ATTACK |
PROC_FLAG_DONE_RANGED_AUTO_ATTACK | PROC_FLAG_TAKEN_RANGED_AUTO_ATTACK;
/**
* @brief Configuration for simulating triggered spell filtering
*/
struct TriggeredSpellConfig
{
bool isTriggered = false; // Spell::IsTriggered()
bool auraHasCanProcFromProcs = false; // SPELL_ATTR3_CAN_PROC_FROM_PROCS on proc aura
bool spellHasNotAProc = false; // SPELL_ATTR3_NOT_A_PROC on triggering spell
uint32 triggeredByAuraSpellId = 0; // GetTriggeredByAuraSpellInfo()->Id
uint32 procAuraSpellId = 0; // The aura's spell ID (for self-loop check)
};
/**
* @brief Simulate triggered spell filtering
* Implements the self-loop prevention and triggered spell blocking from SpellAuras.cpp
*
* @param config Configuration for the triggered spell
* @param procEntry The proc entry being checked
* @param eventTypeMask The event type mask from ProcEventInfo
* @return true if proc should be blocked (return 0), false if allowed
*/
static bool ShouldBlockTriggeredSpell(
TriggeredSpellConfig const& config,
SpellProcEntry const& procEntry,
uint32 eventTypeMask)
{
// Self-loop prevention: block if triggered by the same aura
// SpellAuras.cpp:2191-2192
if (config.triggeredByAuraSpellId != 0 &&
config.triggeredByAuraSpellId == config.procAuraSpellId)
{
return true; // Block: self-loop detected
}
// Check if triggered spell filtering applies
// SpellAuras.cpp:2195-2208
static constexpr uint32 KILL_DEATH_PROC_FLAG_MASK =
PROC_FLAG_KILL | PROC_FLAG_KILLED | PROC_FLAG_DEATH;
if (!config.auraHasCanProcFromProcs &&
!(procEntry.AttributesMask & PROC_ATTR_TRIGGERED_CAN_PROC) &&
!(eventTypeMask & AUTO_ATTACK_PROC_FLAG_MASK) &&
!(eventTypeMask & KILL_DEATH_PROC_FLAG_MASK))
{
// Filter triggered spells unless they have NOT_A_PROC
if (config.isTriggered && !config.spellHasNotAProc)
{
return true; // Block: triggered spell without exceptions
}
}
return false; // Allow proc
}
// =============================================================================
// Extra Attack Chain-Proc Prevention - simulates SpellAuraEffects.cpp:1245-1261
// =============================================================================
/**
* @brief Configuration for simulating extra attack chain-proc prevention
*/
struct ExtraAttackProcConfig
{
bool triggeredSpellHasExtraAttacks = false; // triggeredSpellInfo->HasEffect(SPELL_EFFECT_ADD_EXTRA_ATTACKS)
uint32 triggerSpellId = 0; // m_spellInfo->Effects[GetEffIndex()].TriggerSpell
uint32 lastExtraAttackSpell = 0; // eventInfo.GetActor()->GetLastExtraAttackSpell()
};
/**
* @brief Simulate extra attack chain-proc prevention from CheckEffectProc
* Returns true if proc should be blocked
*
* @param config Extra attack proc configuration
* @return true if proc should be blocked
*/
static bool ShouldBlockExtraAttackChainProc(ExtraAttackProcConfig const& config)
{
// Only applies when the triggered spell grants extra attacks
if (!config.triggeredSpellHasExtraAttacks)
return false;
// Patch 1.12.0(?) extra attack abilities can no longer chain proc themselves
if (config.lastExtraAttackSpell == config.triggerSpellId)
return true;
// Patch 2.2.0 Sword Specialization (Warrior, Rogue) extra attack can no longer proc additional extra attacks
// 3.3.5 Sword Specialization (Warrior), Hack and Slash (Rogue)
if (config.lastExtraAttackSpell == 16459 || config.lastExtraAttackSpell == 66923)
return true;
return false;
}
// =============================================================================
// DisableEffectsMask - simulates SpellAuras.cpp:2244-2258
// =============================================================================
/**
* @brief Apply DisableEffectsMask to get final proc effect mask
*
* @param initialMask Initial effect mask (usually 0x07 for all 3 effects)
* @param disableEffectsMask Mask of effects to disable
* @return Resulting effect mask after applying disable mask
*/
static uint8 ApplyDisableEffectsMask(uint8 initialMask, uint32 disableEffectsMask)
{
uint8 result = initialMask;
for (uint8 i = 0; i < 3; ++i)
{
if (disableEffectsMask & (1u << i))
result &= ~(1 << i);
}
return result;
}
/**
* @brief Check if proc should be blocked due to all effects being disabled
*
* @param initialMask Initial effect mask
* @param disableEffectsMask Mask of effects to disable
* @return true if all effects disabled (proc blocked), false otherwise
*/
static bool ShouldBlockDueToDisabledEffects(uint8 initialMask, uint32 disableEffectsMask)
{
return ApplyDisableEffectsMask(initialMask, disableEffectsMask) == 0;
}
// =============================================================================
// PPM Modifier Simulation - simulates Unit.cpp:10378-10390
// =============================================================================
/**
* @brief PPM modifier configuration for testing SPELLMOD_PROC_PER_MINUTE
*/
struct PPMModifierConfig
{
float flatModifier = 0.0f; // Additive PPM modifier
float pctModifier = 1.0f; // Multiplicative PPM modifier (1.0 = no change)
bool hasSpellModOwner = true; // Whether GetSpellModOwner() returns valid player
bool hasSpellProto = true; // Whether spellProto is provided
};
/**
* @brief Calculate PPM chance with spell modifiers
* Simulates GetPPMProcChance() with SPELLMOD_PROC_PER_MINUTE
*/
static float CalculatePPMChanceWithModifiers(
uint32 weaponSpeed,
float basePPM,
PPMModifierConfig const& modConfig)
{
if (basePPM <= 0.0f)
return 0.0f;
float ppm = basePPM;
// Apply modifiers only if we have spell proto and spell mod owner
if (modConfig.hasSpellProto && modConfig.hasSpellModOwner)
{
// Apply flat modifier first (SPELLMOD_FLAT)
ppm += modConfig.flatModifier;
// Apply percent modifier (SPELLMOD_PCT)
ppm *= modConfig.pctModifier;
}
return (static_cast<float>(weaponSpeed) * ppm) / 600.0f;
}
// =============================================================================
// Equipment Requirements - simulates SpellAuras.cpp:2260-2298
// =============================================================================
/**
* @brief Item classes for equipment requirement checking
*/
static constexpr int32 ITEM_CLASS_WEAPON = 2;
static constexpr int32 ITEM_CLASS_ARMOR = 4;
static constexpr int32 ITEM_CLASS_ANY = -1; // No requirement
/**
* @brief Attack types for weapon slot mapping
*/
static constexpr uint8 BASE_ATTACK = 0;
static constexpr uint8 OFF_ATTACK = 1;
static constexpr uint8 RANGED_ATTACK = 2;
/**
* @brief Configuration for simulating equipment requirements
*/
struct EquipmentConfig
{
bool isPassive = true; // Aura::IsPassive()
bool isPlayer = true; // Target is player
int32 equippedItemClass = ITEM_CLASS_ANY; // SpellInfo::EquippedItemClass
int32 equippedItemSubClassMask = 0; // SpellInfo::EquippedItemSubClassMask
bool hasNoEquipRequirementAttr = false; // SPELL_ATTR3_NO_PROC_EQUIP_REQUIREMENT
uint8 attackType = BASE_ATTACK; // Attack type for weapon slot mapping
bool isInFeralForm = false; // Player::IsInFeralForm()
bool hasEquippedItem = true; // Item is equipped in the slot
bool itemIsBroken = false; // Item::IsBroken()
bool itemFitsRequirements = true; // Item::IsFitToSpellRequirements()
};
/**
* @brief Simulate equipment requirement check
* Returns true if proc should be blocked due to equipment requirements
*
* @param config Equipment configuration
* @return true if proc should be blocked
*/
static bool ShouldBlockDueToEquipment(EquipmentConfig const& config)
{
// Only check for passive player auras with equipment requirements
if (!config.isPassive || !config.isPlayer || config.equippedItemClass == ITEM_CLASS_ANY)
return false;
// SPELL_ATTR3_NO_PROC_EQUIP_REQUIREMENT bypasses check
if (config.hasNoEquipRequirementAttr)
return false;
// Feral form blocks weapon procs
if (config.equippedItemClass == ITEM_CLASS_WEAPON && config.isInFeralForm)
return true;
// No item equipped in the required slot
if (!config.hasEquippedItem)
return true;
// Item is broken
if (config.itemIsBroken)
return true;
// Item doesn't fit spell requirements (wrong subclass, etc.)
if (!config.itemFitsRequirements)
return true;
return false;
}
/**
* @brief Get equipment slot for attack type
*
* @param attackType Attack type (BASE_ATTACK, OFF_ATTACK, RANGED_ATTACK)
* @return Equipment slot index (simulated)
*/
static uint8 GetWeaponSlotForAttackType(uint8 attackType)
{
switch (attackType)
{
case BASE_ATTACK:
return 15; // EQUIPMENT_SLOT_MAINHAND
case OFF_ATTACK:
return 16; // EQUIPMENT_SLOT_OFFHAND
case RANGED_ATTACK:
return 17; // EQUIPMENT_SLOT_RANGED
default:
return 15;
}
}
// =============================================================================
// Cascade Proc Suppression - simulates Unit.cpp TriggerAurasProcOnEvent
// =============================================================================
/**
* @brief Configuration for simulating cascade proc suppression
*
* Models the two paths in TriggerAurasProcOnEvent that call SetCantProc():
* 1. Outer check: triggering spell has TRIGGERED_DISALLOW_PROC_EVENTS
* 2. Per-aura check: aura has SPELL_ATTR3_INSTANT_TARGET_PROCS (0x80000)
*/
struct CascadeProcConfig
{
bool triggeringSpellIsProcDisabled = false; // Spell::IsProcDisabled()
bool auraHasDisableProcAttr = false; // SpellInfo::HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS)
};
/**
* @brief Returns true if cascading procs should be suppressed for this aura
*
* @param config Cascade proc configuration
* @return true if SetCantProc(true) would be active during this aura's proc
*/
static bool ShouldSuppressCascadingProc(CascadeProcConfig const& config)
{
// Outer check: triggering spell disables all cascading procs
if (config.triggeringSpellIsProcDisabled)
return true;
// Per-aura check: aura itself suppresses cascading
if (config.auraHasDisableProcAttr)
return true;
return false;
}
// =============================================================================
// TAKEN Auto-Trigger Logic - simulates SpellMgr.cpp:2033-2049
// =============================================================================
/**
* @brief Configuration for simulating the TAKEN auto-trigger logic
* from SpellMgr::LoadSpellProcs() auto-generation
*/
struct TakenAutoTriggerConfig
{
uint32 procFlags = 0; // SpellInfo::ProcFlags
uint32 auraName = 0; // Effect's ApplyAuraName
bool isAlwaysTriggeredAura = false; // Already set by isAlwaysTriggeredAura[]
};
/**
* @brief Simulate the TAKEN auto-trigger logic from SpellMgr::LoadSpellProcs()
*
* During auto-generation of proc entries, TAKEN-proc auras with
* SPELL_AURA_PROC_TRIGGER_SPELL or SPELL_AURA_PROC_TRIGGER_DAMAGE
* should get PROC_ATTR_TRIGGERED_CAN_PROC set automatically.
*
* @param config Configuration describing the aura
* @return true if addTriggerFlag should be set
*/
static bool ShouldAutoAddTriggeredCanProc(TakenAutoTriggerConfig const& config)
{
// If already marked as always-triggered, keep it
if (config.isAlwaysTriggeredAura)
return true;
// TAKEN auto-trigger: TAKEN proc flags + PROC_TRIGGER_SPELL/DAMAGE
if (config.procFlags & TAKEN_HIT_PROC_FLAG_MASK)
{
switch (config.auraName)
{
case SPELL_AURA_PROC_TRIGGER_SPELL:
case SPELL_AURA_PROC_TRIGGER_DAMAGE:
return true;
default:
break;
}
}
return false;
}
// =============================================================================
// Conditions System - simulates SpellAuras.cpp:2232-2236
// =============================================================================
/**
* @brief Configuration for simulating conditions system
*/
struct ConditionsConfig
{
bool hasConditions = false; // ConditionMgr has conditions for this spell
bool conditionsMet = true; // All conditions are satisfied
uint32 sourceType = 24; // CONDITION_SOURCE_TYPE_SPELL_PROC
};
/**
* @brief Simulate conditions check
* Returns true if proc should be blocked due to conditions
*
* @param config Conditions configuration
* @return true if proc should be blocked
*/
static bool ShouldBlockDueToConditions(ConditionsConfig const& config)
{
// No conditions configured - allow proc
if (!config.hasConditions)
return false;
// Check if conditions are met
return !config.conditionsMet;
}
};
/**
* @brief Test context for proc simulation scenarios
*/
class ProcTestScenario
{
public:
ProcTestScenario() : _now(std::chrono::steady_clock::now()) {}
// Time control
void AdvanceTime(std::chrono::milliseconds duration)
{
_now += duration;
}
std::chrono::steady_clock::time_point GetNow() const { return _now; }
// Actor configuration
UnitStub& GetActor() { return _actor; }
UnitStub const& GetActor() const { return _actor; }
ProcTestScenario& WithActorLevel(uint8_t level)
{
_actor.SetLevel(level);
return *this;
}
ProcTestScenario& WithWeaponSpeed(uint8_t attackType, uint32_t speed)
{
_actor.SetAttackTime(attackType, speed);
return *this;
}
// Aura configuration
std::unique_ptr<AuraStub>& GetAura() { return _aura; }
ProcTestScenario& WithAura(uint32_t spellId, uint8_t charges = 0, uint8_t stacks = 1)
{
_aura = std::make_unique<AuraStub>(spellId);
_aura->SetCharges(charges);
_aura->SetUsingCharges(charges > 0);
_aura->SetStackAmount(stacks);
return *this;
}
// Simulate proc and return whether it triggered
bool SimulateProc(SpellProcEntry const& procEntry, float rollResult = 0.0f)
{
if (!_aura)
return false;
// Check cooldown
if (ProcChanceTestHelper::IsProcOnCooldown(_aura.get(), _now))
return false;
// Calculate chance
float chance = ProcChanceTestHelper::SimulateCalcProcChance(
procEntry,
_actor.GetLevel(),
_actor.GetAttackTime(0));
// Roll check (rollResult of 0 means always pass)
if (rollResult > 0.0f && rollResult > chance)
return false;
// Apply cooldown if set
if (procEntry.Cooldown.count() > 0)
{
ProcChanceTestHelper::ApplyProcCooldown(_aura.get(), _now,
static_cast<uint32>(procEntry.Cooldown.count()));
}
// Consume charges
ProcChanceTestHelper::SimulateConsumeProcCharges(_aura.get(), procEntry);
return true;
}
private:
std::chrono::steady_clock::time_point _now;
UnitStub _actor;
std::unique_ptr<AuraStub> _aura;
};
/**
* @brief Simulates the proc chain guard logic from Unit::TriggerAurasProcOnEvent
*
* Tracks the m_procDeep counter to verify that:
* - TRIGGERED_DISALLOW_PROC_EVENTS on the triggering spell disables procs
* for all auras in the container
* - SPELL_ATTR3_INSTANT_TARGET_PROCS on individual auras disables procs
* only during that specific aura's TriggerProcOnEvent call
* - The counter is properly balanced (returns to 0 after function exits)
*/
class ProcChainGuardSimulator
{
public:
struct AuraConfig
{
uint32 spellId = 0;
bool hasInstantTargetProcs = false; // SPELL_ATTR3_INSTANT_TARGET_PROCS
bool isRemoved = false; // AuraApplication::GetRemoveMode()
};
struct ProcRecord
{
uint32 spellId;
bool canProcDuringTrigger; // CanProc() state when TriggerProcOnEvent fires
int32 procDeepDuringTrigger; // m_procDeep value during trigger
};
ProcChainGuardSimulator() : _procDeep(0) {}
/**
* @brief Simulate Unit::TriggerAurasProcOnEvent
*
* @param triggeringSpellHasDisallowProcEvents Whether the triggering spell
* has TRIGGERED_DISALLOW_PROC_EVENTS cast flag
* @param auras List of auras in the proc container
*/
void SimulateTriggerAurasProc(
bool triggeringSpellHasDisallowProcEvents,
std::vector<AuraConfig> const& auras)
{
_records.clear();
bool const disableProcs = triggeringSpellHasDisallowProcEvents;
if (disableProcs)
SetCantProc(true);
for (auto const& aura : auras)
{
if (aura.isRemoved)
continue;
if (aura.hasInstantTargetProcs)
SetCantProc(true);
// Record CanProc() state during TriggerProcOnEvent
_records.push_back({
aura.spellId,
CanProc(),
_procDeep
});
if (aura.hasInstantTargetProcs)
SetCantProc(false);
}
if (disableProcs)
SetCantProc(false);
}
[[nodiscard]] std::vector<ProcRecord> const& GetRecords() const
{
return _records;
}
[[nodiscard]] int32 GetProcDeep() const { return _procDeep; }
[[nodiscard]] bool CanProc() const { return _procDeep == 0; }
private:
void SetCantProc(bool apply)
{
if (apply)
++_procDeep;
else
--_procDeep;
}
int32 _procDeep;
std::vector<ProcRecord> _records;
};
#endif // AZEROTHCORE_PROC_CHANCE_TEST_HELPER_H