361595e6c2
Co-authored-by: blinkysc <blinkysc@users.noreply.github.com> Co-authored-by: ariel- <ariel-@users.noreply.github.com>
783 lines
27 KiB
C++
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
|