Files
Brues 53a1cf1441 Delete the Lua Dark Harvest acceleration
Same shape as Carnage, opposite direction: Dark Harvest is a channel that makes
the caster's DoTs on the target tick 30% faster, and the server never tells an
observer the remaining durations changed. So this addon stamped dhStartTime on
every tracked record when the channel began, dhEndTime when it stopped, and
subtracted 30% of the elapsed window from each remaining time on read.

ClassicAPI compresses the ticks in the DLL (src/turtle/DarkHarvest.cpp), so
expirationTime already reflects it.

Removed: GetDarkHarvestReduction, ApplyDarkHarvestStart, ApplyDarkHarvestEnd,
GetTimeRemainingWithDarkHarvest, the channel-start capture on both the
UNIT_CASTEVENT and nampower paths, the channel-stop finalizer in
SPELLCAST_CHANNEL_STOP, the darkHarvestData state, and DarkHarvestSpellIDs.

The darkHarvest flag in Extensions/CursiveCustomSpells.lua stays -- that is
spell metadata this addon exports to Cursive, not tracking of our own.
2026-09-12 20:27:16 -05:00

917 lines
37 KiB
Lua

--[[
ComboPoint Tracker for Duration-Scaling Spells
Author: Extension for CleveRoids
Purpose: Track combo points used when casting spells that scale duration with combo points
]]
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
-- Initialize combo point tracking table
CleveRoids.ComboPointTracking = CleveRoids.ComboPointTracking or {}
-- Note: CleveRoids.lastComboPoints and CleveRoids.lastComboPointsTime are initialized in Init.lua
-- Initialize SavedVariable for learned combo durations
-- Structure: CleveRoids_ComboDurations[spellID][comboPoints] = duration
CleveRoids_ComboDurations = CleveRoids_ComboDurations or {}
-- Family membership without hardcoded rank lists. C_Spell.GetSpellName resolves
-- ANY spellID from the client's Spell.dbc -- every rank (so no enumeration or
-- spellbook scan) and TWoW's custom spells alike -- so "is this spellID a Rip?"
-- is just a name match against one seed rank. seedNameCache memoizes the seed's
-- localized name (locale-safe: derived from the ID, not a hardcoded string; a
-- seed absent from the DBC, e.g. a TWoW spell on a stock client, resolves to nil
-- and the set simply never matches -- which is correct, that spell can't be cast).
local seedNameCache = {}
local function SeedName(seedID)
local n = seedNameCache[seedID]
if n == nil then
n = C_Spell.GetSpellName(seedID) or false
seedNameCache[seedID] = n
end
return n or nil
end
-- Stand-in for a hardcoded {spellID=true,...} rank set: t[spellID] is true iff
-- spellID is any rank of the family seeded by seedID. Same read shape as the old
-- tables, so every membership consumer (`X and X[id]`) keeps working unchanged.
-- Only valid for membership tests -- these are not iterable (pairs() sees empty).
local function RankSet(seedID)
return setmetatable({}, { __index = function(_, spellID)
if type(spellID) ~= "number" then return nil end
local name = SeedName(seedID)
return (name and C_Spell.GetSpellName(spellID) == name) and true or nil
end })
end
-- Define spells that scale with combo points by SPELL ID and their duration formulas
-- Duration = base + (combo_points - 1) * increment
CleveRoids.ComboScalingSpellsByID = {
-- ROGUE: Rupture - 8 sec base, +2 sec per additional combo point
[1943] = { base = 8, increment = 2, name = "Rupture" }, -- Rank 1
[8639] = { base = 8, increment = 2, name = "Rupture" }, -- Rank 2
[8640] = { base = 8, increment = 2, name = "Rupture" }, -- Rank 3
[11273] = { base = 8, increment = 2, name = "Rupture" }, -- Rank 4
[11274] = { base = 8, increment = 2, name = "Rupture" }, -- Rank 5
[11275] = { base = 8, increment = 2, name = "Rupture" }, -- Rank 6
-- ROGUE: Kidney Shot - Rank 1: 1 sec base, Rank 2: 2 sec base, +1 sec per CP
[408] = { base = 1, increment = 1, name = "Kidney Shot" }, -- Rank 1
[8643] = { base = 2, increment = 1, name = "Kidney Shot" }, -- Rank 2
-- DRUID: Rip - 12 sec base, +4 sec per additional combo point (Source: Vanilla WoW Wiki)
-- Note: Some sources say 10+2, but testing shows 12+4 is accurate for 1.12
[1079] = { base = 10, increment = 2, name = "Rip" }, -- Rank 1
[9492] = { base = 10, increment = 2, name = "Rip" }, -- Rank 2
[9493] = { base = 10, increment = 2, name = "Rip" }, -- Rank 3
[9752] = { base = 10, increment = 2, name = "Rip" }, -- Rank 4
[9894] = { base = 10, increment = 2, name = "Rip" }, -- Rank 5
[9896] = { base = 10, increment = 2, name = "Rip" }, -- Rank 6
}
-- Ferocious Bite spell IDs. A combo-point finisher, so the cast-time combo
-- snapshot has to know it.
CleveRoids.FerociousBiteSpellIDs = {
[22557] = true, -- Rank 1
[22568] = true, -- Rank 2
[22827] = true, -- Rank 3
[22828] = true, -- Rank 4
[22829] = true, -- Rank 5
[31018] = true, -- Rank 6
}
-- Combined table for all bleed spells that need immunity detection
-- Used when checking if a cast bleed failed to apply (indicates bleed immunity)
-- NOTE: These are the DEBUFF spell IDs (what appears on target), not cast spell IDs
CleveRoids.BleedSpellIDs = {
-- Rip (bleed DoT from Cat Form finisher)
[1079] = true, -- Rank 1
[9492] = true, -- Rank 2
[9493] = true, -- Rank 3
[9752] = true, -- Rank 4
[9894] = true, -- Rank 5
[9896] = true, -- Rank 6
-- Rake (cast ID = debuff ID for Rake)
[1822] = true, -- Rank 1
[1823] = true, -- Rank 2
[1824] = true, -- Rank 3
[9904] = true, -- Rank 4
-- Pounce Bleed (triggered spell IDs, different from cast IDs)
[9007] = true, -- Rank 1 (triggered by Pounce 9005)
[9824] = true, -- Rank 2 (triggered by Pounce 9823)
[9826] = true, -- Rank 3 (triggered by Pounce 9827)
}
-- Mapping from Pounce cast spell IDs to their triggered Pounce Bleed spell IDs
-- Used to track the correct debuff when UNIT_CASTEVENT fires for Pounce
CleveRoids.PounceToBleedMapping = {
[9005] = 9007, -- Pounce Rank 1 → Pounce Bleed Rank 1
[9823] = 9824, -- Pounce Rank 2 → Pounce Bleed Rank 2
[9827] = 9826, -- Pounce Rank 3 → Pounce Bleed Rank 3
}
-- =============================================================================
-- SHAMAN: Molten Blast → Flame Shock Refresh (TWoW Custom)
-- When Molten Blast hits, it refreshes Flame Shock duration on the target
-- Detection: Monitor combat log for Molten Blast damage, then refresh Flame Shock
-- =============================================================================
-- TWoW custom; seed resolves on the TWoW client (nil/never-matches on stock,
-- where Molten Blast can't be cast anyway).
CleveRoids.MoltenBlastSpellIDs = RankSet(36916)
CleveRoids.FlameShockSpellIDs = {
[8050] = true, -- Rank 1
[8052] = true, -- Rank 2
[8053] = true, -- Rank 3
[10447] = true, -- Rank 4
[10448] = true, -- Rank 5
[29228] = true, -- Rank 6
}
-- =============================================================================
-- WARLOCK: Conflagrate → Immolate Duration Reduction
-- When Conflagrate is cast, it reduces Immolate duration by 3 seconds
-- =============================================================================
CleveRoids.ConflagrateSpellIDs = RankSet(17962)
CleveRoids.ImmolateSpellIDs = {
[348] = true, -- Rank 1
[707] = true, -- Rank 2
[1094] = true, -- Rank 3
[2941] = true, -- Rank 4
[11665] = true, -- Rank 5
[11667] = true, -- Rank 6
[11668] = true, -- Rank 7
[25309] = true, -- Rank 8
}
-- =============================================================================
-- DRUID: Rake Debuff Cap Boss Whitelist
-- These bosses are likely to hit the 48 debuff cap, causing Rake to get pushed off
-- For mobs NOT in this list, we verify Rake is actually on the target before tracking
-- Prevents false tracking when debuffs get pushed off at cap
-- =============================================================================
CleveRoids.MobsThatBleed = {
-- Karazhan Crypts / TWoW Custom
["0xF13000F1F3276A33"] = true, -- Keeper Gnarlmoon
["0xF13000F1FA276A32"] = true, -- Ley-Watcher Incantagos
["0xF13000EA3F276C05"] = true, -- King
["0xF13000EA31279058"] = true, -- Queen
["0xF13000EA43276C06"] = true, -- Bishop
["0xF13000EA44279044"] = true, -- Pawn
["0xF13000EA42276C07"] = true, -- Rook
["0xF13000EA4D05DA44"] = true, -- Sanv Tas'dal
["0xF13000EA57276C04"] = true, -- Kruul
["0xF130016C95276DAF"] = true, -- Mephistroth
-- Naxxramas
["0xF130003E5401591A"] = true, -- Anub'Rekhan
["0xF130003E510159B0"] = true, -- Grand Widow Faerlina
["0xF130003E500159A3"] = true, -- Maexxna
["0xF130003EBD01598C"] = true, -- Razuvious
["0xF130003EBC01599F"] = true, -- Gothik
["0xF130003EBF015AB2"] = true, -- Zeliek
["0xF130003EBE015AB3"] = true, -- Mograine
["0xF130003EC1015AB1"] = true, -- Blaumeux
["0xF130003EC0015AB0"] = true, -- Thane
["0xF130003E52015824"] = true, -- Noth
["0xF130003E4001588D"] = true, -- Heigan
["0xF130003E8B0158A2"] = true, -- Loatheb
["0xF130003E9C0158EA"] = true, -- Patchwerk
["0xF130003E3B0158EF"] = true, -- Grobbulus
["0xF130003E3C0158F0"] = true, -- Gluth
["0xF130003E380159A0"] = true, -- Thaddius
["0xF130003E75015AB4"] = true, -- Sapphiron
["0xF130003E76015AED"] = true, -- Kel'Thuzad
}
-- Legacy name-based table for backwards compatibility
CleveRoids.ComboScalingSpells = {
["Rupture"] = { base_duration = 8, increment = 2, all_ranks = true },
["Kidney Shot(Rank 1)"] = { base_duration = 1, increment = 1 },
["Kidney Shot(Rank 2)"] = { base_duration = 2, increment = 1 },
["Rip"] = { base_duration = 10, increment = 2, all_ranks = true }
}
-- Function to get current combo points
-- Note: In Vanilla WoW 1.12, GetComboPoints() takes no parameters
-- (The "player", "target" syntax is from TBC/WotLK)
function CleveRoids.GetComboPoints()
return GetComboPoints() or 0
end
-- Update stored combo points (call this frequently)
function CleveRoids.UpdateComboPoints()
local current = CleveRoids.GetComboPoints()
if current > 0 then
CleveRoids.lastComboPoints = current
CleveRoids.lastComboPointsTime = GetTime()
end
end
-- Function to check if a spell scales with combo points
function CleveRoids.IsComboScalingSpell(spellName)
if not spellName then return false end
-- Check exact match first
if CleveRoids.ComboScalingSpells[spellName] then
return true
end
-- Check for spells marked as all_ranks
for spell, data in pairs(CleveRoids.ComboScalingSpells) do
if data.all_ranks then
-- Remove rank from spell name for comparison
local baseName = CleveRoids.StripRank(spell)
local checkName = CleveRoids.StripRank(spellName)
if baseName == checkName then
return true
end
end
end
return false
end
-- Function to get spell data for combo scaling
function CleveRoids.GetComboScalingData(spellName)
if not spellName then return nil end
-- Check exact match first
if CleveRoids.ComboScalingSpells[spellName] then
return CleveRoids.ComboScalingSpells[spellName]
end
-- Check for spells marked as all_ranks
for spell, data in pairs(CleveRoids.ComboScalingSpells) do
if data.all_ranks then
local baseName = CleveRoids.StripRank(spell)
local checkName = CleveRoids.StripRank(spellName)
if baseName == checkName then
return data
end
end
end
-- Special handling for Kidney Shot ranks
if string.find(spellName, "Kidney Shot") then
local _, _, rank = string.find(spellName, "Kidney Shot%(Rank (%d+)%)")
if rank then
rank = tonumber(rank)
if rank == 1 then
return CleveRoids.ComboScalingSpells["Kidney Shot(Rank 1)"]
elseif rank == 2 then
return CleveRoids.ComboScalingSpells["Kidney Shot(Rank 2)"]
end
end
-- Default to base Kidney Shot data
return CleveRoids.ComboScalingSpells["Kidney Shot"]
end
return nil
end
-- Function to calculate duration based on combo points
function CleveRoids.CalculateComboScaledDuration(spellName, comboPoints)
local data = CleveRoids.GetComboScalingData(spellName)
if not data then return nil end
comboPoints = comboPoints or CleveRoids.GetComboPoints()
if comboPoints < 1 then comboPoints = 1 end -- Minimum 1 combo point
if comboPoints > 5 then comboPoints = 5 end -- Maximum 5 combo points
return data.base_duration + (comboPoints - 1) * data.increment
end
-- NEW: Check if spell ID is a combo scaling spell
function CleveRoids.IsComboScalingSpellID(spellID)
return CleveRoids.ComboScalingSpellsByID[spellID] ~= nil
end
-- NEW: Get combo scaling data by spell ID
function CleveRoids.GetComboScalingDataByID(spellID)
return CleveRoids.ComboScalingSpellsByID[spellID]
end
-- NEW: Get learned duration for specific combo point count
function CleveRoids.GetLearnedComboDuration(spellID, comboPoints)
if not CleveRoids_ComboDurations then
CleveRoids_ComboDurations = {}
end
if CleveRoids_ComboDurations[spellID] and CleveRoids_ComboDurations[spellID][comboPoints] then
return CleveRoids_ComboDurations[spellID][comboPoints]
end
return nil
end
-- NEW: Calculate duration by spell ID and combo points (uses learned durations first)
function CleveRoids.CalculateComboScaledDurationByID(spellID, comboPoints)
local data = CleveRoids.GetComboScalingDataByID(spellID)
if not data then return nil end
comboPoints = comboPoints or CleveRoids.GetComboPoints()
if comboPoints < 1 then comboPoints = 1 end -- Minimum 1 combo point
if comboPoints > 5 then comboPoints = 5 end -- Maximum 5 combo points
local baseDuration
-- Check for learned duration first
local learned = CleveRoids.GetLearnedComboDuration(spellID, comboPoints)
if learned then
baseDuration = learned
else
-- Fall back to formula
baseDuration = data.base + (comboPoints - 1) * data.increment
end
-- Apply all duration modifiers (Nampower, talents, equipment, set bonuses)
if CleveRoids.ApplyAllDurationModifiers then
baseDuration = CleveRoids.ApplyAllDurationModifiers(spellID, baseDuration)
else
-- Fallback for load order (ApplyAllDurationModifiers not yet defined)
if CleveRoids.ApplyTalentModifier then
baseDuration = CleveRoids.ApplyTalentModifier(spellID, baseDuration)
end
if CleveRoids.ApplyEquipmentModifier then
baseDuration = CleveRoids.ApplyEquipmentModifier(spellID, baseDuration)
end
end
return baseDuration
end
-- Function to track combo points when casting (by spell name)
function CleveRoids.TrackComboPointCast(spellName)
if not CleveRoids.IsComboScalingSpell(spellName) then
return
end
-- Get current combo points, but if they're 0 (already consumed), use last known value
local comboPoints = CleveRoids.GetComboPoints()
-- If combo points are 0, use lastComboPoints as fallback
if comboPoints == 0 and CleveRoids.lastComboPoints > 0 then
comboPoints = CleveRoids.lastComboPoints
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff9900CleveRoids:|r ComboTrack: Using lastComboPoints (%d) for %s",
comboPoints, spellName)
)
end
end
local duration = CleveRoids.CalculateComboScaledDuration(spellName, comboPoints)
local currentTime = GetTime()
-- Only update if this is a better (higher CP) value than existing, or if existing is stale
local existing = CleveRoids.ComboPointTracking[spellName]
if existing then
local age = currentTime - existing.cast_time
-- Keep existing if it's fresh (<0.5s) and EITHER has more combo points OR is confirmed
if age < 0.5 then
if existing.combo_points > comboPoints then
-- Don't overwrite better data with worse data
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff888888CleveRoids:|r ComboTrack: Ignoring %s with %d CP (have %d CP)",
spellName, comboPoints, existing.combo_points)
)
end
return
elseif existing.confirmed then
-- Don't overwrite confirmed tracking with unconfirmed evaluation
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff888888CleveRoids:|r ComboTrack: Ignoring %s - already confirmed",
spellName)
)
end
return
end
end
end
-- Store the tracking data
CleveRoids.ComboPointTracking[spellName] = {
combo_points = comboPoints,
duration = duration,
cast_time = currentTime,
target = UnitName("target") or "Unknown",
confirmed = false -- Will be set to true by SPELLCAST_START
}
-- Also store in spell_tracking for integration with existing system
if not CleveRoids.spell_tracking[spellName] then
CleveRoids.spell_tracking[spellName] = {}
end
CleveRoids.spell_tracking[spellName].last_combo_points = comboPoints
CleveRoids.spell_tracking[spellName].last_duration = duration
CleveRoids.spell_tracking[spellName].last_cast_time = currentTime
-- Debug output
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff4b7dccCleveRoids:|r ComboTrack: %s cast with %d CP, duration: %d seconds",
spellName, comboPoints, duration)
)
end
end
-- NEW: Track combo points by spell ID (for UNIT_CASTEVENT integration)
function CleveRoids.TrackComboPointCastByID(spellID, targetGUID)
if not CleveRoids.IsComboScalingSpellID(spellID) then
return nil
end
-- Get current combo points, but if they're 0 (already consumed), use last known value
local comboPoints = CleveRoids.GetComboPoints()
-- If combo points are 0, try multiple fallback sources
if comboPoints == 0 then
-- PRIMARY: Check SPELL_CAST_EVENT capture (most reliable - client-side pre-server)
local pending = CleveRoids.pendingCasts and CleveRoids.pendingCasts[spellID]
if pending and pending.comboPoints and pending.comboPoints > 0 then
comboPoints = pending.comboPoints
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff88[TrackComboByID]|r Using SPELL_CAST_EVENT CP: %d for spell ID %d",
comboPoints, spellID)
)
end
-- Fallback: lastComboPoints (don't reset immediately - let it persist)
elseif CleveRoids.lastComboPoints > 0 then
comboPoints = CleveRoids.lastComboPoints
-- Don't reset here - let it persist for multiple events
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff9900[TrackComboByID]|r Using lastComboPoints: %d for spell ID %d",
comboPoints, spellID)
)
end
else
-- Second, check if name-based tracking has recent data for this spell
local spellName = C_Spell.GetSpellName(spellID)
if spellName then
-- Remove rank info for comparison
local baseName = CleveRoids.StripRank(spellName)
if CleveRoids.ComboPointTracking[spellName] then
local tracking = CleveRoids.ComboPointTracking[spellName]
if tracking.combo_points and tracking.combo_points > 0 and
(GetTime() - tracking.cast_time) < 0.5 then -- Within last 0.5 seconds
comboPoints = tracking.combo_points
end
elseif CleveRoids.ComboPointTracking[baseName] then
local tracking = CleveRoids.ComboPointTracking[baseName]
if tracking.combo_points and tracking.combo_points > 0 and
(GetTime() - tracking.cast_time) < 0.5 then
comboPoints = tracking.combo_points
end
end
end
end
end
local duration = CleveRoids.CalculateComboScaledDurationByID(spellID, comboPoints)
if not duration then return nil end
-- Store tracking data by spell ID
if not CleveRoids.ComboPointTracking.byID then
CleveRoids.ComboPointTracking.byID = {}
end
CleveRoids.ComboPointTracking.byID[spellID] = {
combo_points = comboPoints,
duration = duration,
cast_time = GetTime(),
target_guid = targetGUID
}
-- Debug output
if CleveRoids.debug then
local data = CleveRoids.ComboScalingSpellsByID[spellID]
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff4b7dccCleveRoids:|r ComboTrack: %s (ID:%d) cast with %d CP, duration: %d seconds",
data.name, spellID, comboPoints, duration)
)
end
-- Reset lastComboPoints after successfully using it
if comboPoints > 0 and comboPoints == CleveRoids.lastComboPoints then
CleveRoids.lastComboPoints = 0
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff888888[TrackComboByID]|r Reset lastComboPoints after use")
)
end
end
return duration
end
-- Utility function to display current combo tracking info
function CleveRoids.ShowComboTracking()
CleveRoids.Print("=== Combo Point Tracking ===")
local hasData = false
for spell, data in pairs(CleveRoids.ComboPointTracking) do
if spell ~= "byID" and data.combo_points then
CleveRoids.Print(string.format("%s: %d CP, %ds duration, target: %s",
spell, data.combo_points, data.duration, data.target))
hasData = true
end
end
if not hasData then
CleveRoids.Print("No combo finishers tracked yet. Cast Rupture, Rip, or Kidney Shot!")
end
end
-- Slash command for debugging
SLASH_COMBOTRACK1 = "/combotrack"
SlashCmdList.COMBOTRACK = function(msg)
if msg == "show" then
CleveRoids.ShowComboTracking()
elseif msg == "clear" then
CleveRoids.ComboPointTracking = {}
CleveRoids.ComboPointTracking.byID = {}
CleveRoids.Print("Combo tracking data cleared")
elseif msg == "debug" then
CleveRoids.debug = not CleveRoids.debug
CleveRoids.Print("Combo tracking debug: " .. (CleveRoids.debug and "ON" or "OFF"))
else
CleveRoids.Print("ComboTrack commands:")
CleveRoids.Print(" /combotrack show - Display current tracking data")
CleveRoids.Print(" /combotrack clear - Clear tracking data")
CleveRoids.Print(" /combotrack debug - Toggle debug output")
end
end
-- Export to global namespace NOW, before Extension registration
_G["CleveRoids"] = CleveRoids
-- DEBUG: Confirm ShowComboTracking is defined (disabled to reduce login spam)
-- if CleveRoids.ShowComboTracking then
-- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00ComboPointTracker: ShowComboTracking defined and exported!|r")
-- else
-- DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ComboPointTracker: ERROR - ShowComboTracking NOT defined!|r")
-- end
-- Hook global CastSpell IMMEDIATELY (before Extension system)
if _G.CastSpell then
local originalCastSpell = _G.CastSpell
_G.CastSpell = function(id, bookType)
-- Capture combo points IMMEDIATELY, before anything else
local currentCP = CleveRoids.GetComboPoints()
-- Get spell name for checking
local spellName = GetSpellName(id, bookType)
if currentCP and currentCP > 0 then
CleveRoids.lastComboPoints = currentCP
CleveRoids.lastComboPointsTime = GetTime()
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffaaff00[CastSpell Hook]|r Captured %d CP before spell %d/%s (name:%s)",
currentCP, id or 0, bookType or "nil", spellName or "nil")
)
end
end
-- Check if it's a combo finisher and pre-populate tracking
if spellName then
local isCombo = CleveRoids.IsComboScalingSpell(spellName)
if CleveRoids.debug and currentCP and currentCP > 0 then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff888888[CastSpell Debug]|r spell='%s' isCombo=%s CP=%d",
spellName, tostring(isCombo), currentCP)
)
end
if isCombo and currentCP and currentCP > 0 then
local duration = CleveRoids.CalculateComboScaledDuration(spellName, currentCP)
if duration then
local baseName = CleveRoids.StripRank(spellName)
CleveRoids.ComboPointTracking[baseName] = {
combo_points = currentCP,
duration = duration,
cast_time = GetTime(),
target = UnitName("target") or "Unknown",
confirmed = true
}
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff00[CastSpell Pre-Track]|r %s with %d CP, duration: %ds",
baseName, currentCP, duration)
)
end
end
end
end
return originalCastSpell(id, bookType)
end
-- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00ComboPointTracker: CastSpell hook installed!|r")
end
-- Hook global UseAction IMMEDIATELY (before Extension system)
if _G.UseAction then
local originalUseAction = _G.UseAction
_G.UseAction = function(slot, target, button)
-- Always capture combo points before action, in case it's a finisher
local currentCP = CleveRoids.GetComboPoints()
-- Inspect the action button to see what spell it is
local macroName, actionType, actionID = GetActionText(slot)
local spellName = nil
if actionType == "SPELL" and actionID then
spellName = C_Spell.GetSpellName(actionID)
end
if currentCP and currentCP > 0 then
CleveRoids.lastComboPoints = currentCP
CleveRoids.lastComboPointsTime = GetTime()
-- Debug message removed to reduce spam
-- If it's a combo finisher, pre-populate tracking
if spellName and CleveRoids.IsComboScalingSpell(spellName) then
local duration = CleveRoids.CalculateComboScaledDuration(spellName, currentCP)
if duration then
local baseName = CleveRoids.StripRank(spellName)
CleveRoids.ComboPointTracking[baseName] = {
combo_points = currentCP,
duration = duration,
cast_time = GetTime(),
target = UnitName("target") or "Unknown",
confirmed = true
}
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff00[UseAction Pre-Track]|r %s with %d CP, duration: %ds",
baseName, currentCP, duration)
)
end
end
end
end
-- Debug message removed to reduce spam
return originalUseAction(slot, target, button)
end
-- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00ComboPointTracker: UseAction hook installed!|r")
end
-- Hook global CastSpellByName IMMEDIATELY (before Extension system)
if CastSpellByName then
local originalCastSpellByName = CastSpellByName
CastSpellByName = function(spellName, onSelf)
local currentCP = CleveRoids.GetComboPoints()
-- Debug message removed to reduce spam
if spellName and CleveRoids.IsComboScalingSpell(spellName) then
if currentCP and currentCP > 0 then
CleveRoids.lastComboPoints = currentCP
CleveRoids.lastComboPointsTime = GetTime()
-- Pre-populate tracking
local duration = CleveRoids.CalculateComboScaledDuration(spellName, currentCP)
if duration then
local baseName = CleveRoids.StripRank(spellName)
CleveRoids.ComboPointTracking[baseName] = {
combo_points = currentCP,
duration = duration,
cast_time = GetTime(),
target = UnitName("target") or "Unknown",
confirmed = true
}
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff8800[CastSpellByName Pre-Track]|r %s with %d CP, duration: %ds",
baseName, currentCP, duration)
)
end
end
end
end
return originalCastSpellByName(spellName, onSelf)
end
-- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00ComboPointTracker: CastSpellByName hook installed!|r")
end
-- Hook into the existing DoCast function (safe to fail)
if not CleveRoids.RegisterExtension then
-- ExtensionsManager not loaded yet, skip extension system
-- DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ComboPointTracker: RegisterExtension not found, skipping Extension system|r")
return
end
local Extension = CleveRoids.RegisterExtension("ComboPointTracker")
function Extension.OnLoad()
-- Register for cast events
Extension.RegisterEvent("SPELLCAST_START", "OnSpellcastStart")
Extension.RegisterEvent("SPELLCAST_STOP", "OnSpellcastStop")
Extension.RegisterEvent("SPELLCAST_FAILED", "OnSpellcastFailed")
Extension.RegisterEvent("SPELLCAST_INTERRUPTED", "OnSpellcastInterrupted")
Extension.RegisterEvent("PLAYER_TARGET_CHANGED", "OnTargetChanged")
Extension.RegisterUnitEvent("UNIT_AURA", "OnUnitAura", "target", "player")
Extension.RegisterEvent("PLAYER_COMBO_POINTS", "OnComboPointsChanged")
-- PERFORMANCE OPTIMIZATION: Removed OnUpdate polling for combo points
-- Combo points are now tracked via PLAYER_COMBO_POINTS event + spell cast hooks
-- This eliminates continuous polling while maintaining identical functionality
-- Events that trigger combo point updates:
-- - PLAYER_COMBO_POINTS (native event when CP change)
-- - PLAYER_TARGET_CHANGED (target switch clears CP)
-- - UNIT_AURA (for debuff applications)
-- - CastSpellByName/QueueSpellByName hooks (pre-cast capture)
-- Note: CastSpell, UseAction, and CastSpellByName hooks are set up immediately when file loads (see above)
end
-- Update combo points on relevant events
function Extension.OnTargetChanged()
CleveRoids.UpdateComboPoints()
end
-- Registered as a unit event for target and player, so arg1 is always one of
-- those two -- no token check needed.
function Extension.OnUnitAura()
CleveRoids.UpdateComboPoints()
end
function Extension.OnComboPointsChanged()
CleveRoids.UpdateComboPoints()
end
-- Event handlers
function Extension.OnSpellcastStart()
-- Track combo points at cast start for channeled combo spells
local spellName = arg1
if spellName and CleveRoids.IsComboScalingSpell(spellName) then
-- Capture current combo points BEFORE the spell cast
local currentCP = CleveRoids.GetComboPoints()
if currentCP and currentCP > 0 then
CleveRoids.lastComboPoints = currentCP
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffaaaaff[Pre-Cast]|r Captured %d combo points before casting %s",
currentCP, spellName)
)
end
end
CleveRoids.TrackComboPointCast(spellName)
-- Mark this tracking as confirmed (actual cast, not just evaluation)
if CleveRoids.ComboPointTracking[spellName] then
CleveRoids.ComboPointTracking[spellName].confirmed = true
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff00[Confirmed]|r %s tracking confirmed (SPELLCAST_START)",
spellName)
)
end
end
end
end
function Extension.OnSpellcastStop()
-- Track combo points at successful cast completion
local spellName = arg1
if spellName and CleveRoids.IsComboScalingSpell(spellName) then
-- Update tracking if spell successfully completed
local tracking = CleveRoids.ComboPointTracking[spellName]
if tracking and (GetTime() - tracking.cast_time) < 1 then
-- Cast was recent, mark as successful
tracking.successful = true
end
end
end
function Extension.OnSpellcastFailed()
-- Clear tracking for failed casts
local spellName = arg1
if spellName and CleveRoids.ComboPointTracking[spellName] then
local tracking = CleveRoids.ComboPointTracking[spellName]
if tracking and (GetTime() - tracking.cast_time) < 1 then
-- Recent cast failed, clear confirmed flag and the tracking
tracking.confirmed = false
CleveRoids.ComboPointTracking[spellName] = nil
end
end
end
function Extension.OnSpellcastInterrupted()
-- Clear tracking for interrupted casts
Extension.OnSpellcastFailed()
end
-- Hook CastSpellByName to track combo points
function Extension.CastSpellByName_Hook(spellName, onSelf)
if spellName and CleveRoids.IsComboScalingSpell(spellName) then
-- Capture current combo points BEFORE the spell cast
local currentCP = CleveRoids.GetComboPoints()
if currentCP and currentCP > 0 then
CleveRoids.lastComboPoints = currentCP
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffaaaaff[Pre-Cast]|r Captured %d combo points before casting %s",
currentCP, spellName)
)
end
end
CleveRoids.TrackComboPointCast(spellName)
-- Confirm the tracking immediately - this only fires on actual casts
if CleveRoids.ComboPointTracking[spellName] then
CleveRoids.ComboPointTracking[spellName].confirmed = true
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff00[Confirmed]|r %s tracking confirmed (CastSpellByName)",
spellName)
)
end
end
end
end
-- Hook CleveRoids.CastSpell if it exists
function Extension.CastSpell_Hook(spellName)
if spellName and CleveRoids.IsComboScalingSpell(spellName) then
-- Capture current combo points BEFORE the spell cast
local currentCP = CleveRoids.GetComboPoints()
if currentCP and currentCP > 0 then
CleveRoids.lastComboPoints = currentCP
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffaaaaff[Pre-Cast]|r Captured %d combo points before casting %s",
currentCP, spellName)
)
end
end
CleveRoids.TrackComboPointCast(spellName)
-- Confirm the tracking immediately - this only fires on actual casts
if CleveRoids.ComboPointTracking[spellName] then
CleveRoids.ComboPointTracking[spellName].confirmed = true
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff00[Confirmed]|r %s tracking confirmed (CleveRoids.CastSpell)",
spellName)
)
end
end
end
end
-- Integration with DoCast function (moved before Extension registration)
if CleveRoids.DoCast then
local originalDoCast = CleveRoids.DoCast
CleveRoids.DoCast = function(msg)
-- Extract spell name from message
local spellName = msg
-- Remove conditionals if present
local condEnd = string.find(msg, "]")
if condEnd then
spellName = string.sub(msg, condEnd + 1)
end
spellName = CleveRoids.Trim(spellName)
-- Track combo points if it's a scaling spell
if CleveRoids.IsComboScalingSpell(spellName) then
-- Capture current combo points BEFORE the spell cast
local currentCP = CleveRoids.GetComboPoints()
if currentCP and currentCP > 0 then
CleveRoids.lastComboPoints = currentCP
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffaaaaff[Pre-Cast]|r Captured %d combo points before casting %s",
currentCP, spellName)
)
end
end
CleveRoids.TrackComboPointCast(spellName)
-- Confirm the tracking - DoCast only fires on actual casts
if CleveRoids.ComboPointTracking[spellName] then
CleveRoids.ComboPointTracking[spellName].confirmed = true
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff00[Confirmed]|r %s tracking confirmed (DoCast)",
spellName)
)
end
end
end
-- Call original function
return originalDoCast(msg)
end
end