/* * 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 . */ #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 #include /** * @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(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(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(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(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& GetAura() { return _aura; } ProcTestScenario& WithAura(uint32_t spellId, uint8_t charges = 0, uint8_t stacks = 1) { _aura = std::make_unique(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(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 _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 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 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 _records; }; #endif // AZEROTHCORE_PROC_CHANCE_TEST_HELPER_H