Files
SuperCleveRoidMacros/ComboPointTracker.lua
T
Brues c8bc4c134d Filter UNIT_AURA by unit token and drop the unit streams in realtime mode
Two handlers opened by testing arg1 against a fixed set of unit tokens, so
every other unit's copy of UNIT_AURA reached Lua only to be compared away
-- in a raid that is the bulk of them. ClassicAPI's RegisterUnitEvent
pushes that filter into the client, so the handler only runs for the units
named. Filtering is per (frame, event), so the other events sharing the
libdebuff frame are unaffected.

  Utility.lua      UNIT_AURA -> "target"            (libdebuff seeding)
  ComboPointTracker UNIT_AURA -> "target", "player"

Extensions register through CleveRoids.RegisterEvent, so ExtensionsManager
gains a RegisterUnitEvent alongside it.

Separately, the main frame's unit state streams (UNIT_AURA / UNIT_HEALTH /
UNIT_POWER, or the _GUID variants under Nampower v2.39+) are now registered
only in event-driven mode. Their handlers are wholly wrapped in
`realtime == 0`, so with realtime on every one of them crossed into Lua and
returned immediately while the OnUpdate did the refreshing. Registration is
applied at load, again at VARIABLES_LOADED once the saved setting is
readable, and whenever /cleveroid realtime flips it.

Those three stay plain RegisterEvent calls: their handlers ignore the unit
and refresh every macro, and a conditional may name any unit
([@party3,hp:<50], @focus, @mouseover), so narrowing the token set would
leave those icons stale. The _GUID variants could not be filtered anyway --
their first argument is a GUID, not a unit token.
2026-09-11 00:42:42 -05:00

997 lines
40 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 {}
-- Storage for last Rip cast (for Carnage talent mechanic)
-- Carnage talent: When Ferocious Bite procs Carnage, it refreshes Rip and Rake to their original duration
-- Detection: When combo points don't drop to 0 after FB (they stay at 1 = Carnage proc)
-- Talent Position: Tab 2 (Feral Combat), Talent 17
-- Rank 1: 10% per CP, Rank 2: 20% per CP
CleveRoids.lastRipCast = CleveRoids.lastRipCast or {
duration = nil,
targetGUID = nil,
timestamp = 0
}
CleveRoids.lastRakeCast = CleveRoids.lastRakeCast or {
duration = nil,
targetGUID = nil,
timestamp = 0
}
-- 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 (for Carnage talent mechanic)
-- Carnage talent: When FB procs Carnage, refreshes Rip and Rake back to their original duration
-- Proc detection: combo points stay at 1 after FB instead of dropping to 0
-- Talent Position: Tab 2 (Feral Combat), Talent 17
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
}
-- Rip / Rake families (for Carnage talent). Seeded by Rank 1; matches every rank.
CleveRoids.RipSpellIDs = RankSet(1079)
CleveRoids.RakeSpellIDs = RankSet(1822)
-- 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
}
-- =============================================================================
-- WARLOCK: Dark Harvest Duration Acceleration (TWoW Custom)
-- Channeled spell that accelerates DoT tick rate by 30% while channeling
-- Complex tracking: debuff expires 30% faster while Dark Harvest is active
-- =============================================================================
-- TWoW custom (see MoltenBlast note).
CleveRoids.DarkHarvestSpellIDs = RankSet(52550)
-- =============================================================================
-- 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()
-- CARNAGE PROC DETECTION (Cursive-style)
-- When Ferocious Bite is used, combo points should drop to 0
-- If Carnage procs, combo points will be 1 instead (the Carnage-granted combo point)
-- Check: After Ferocious Bite (within 0.5s), if combo points > 0, Carnage procced
if CleveRoids.lastFerociousBiteTime and CleveRoids.lastFerociousBiteTargetGUID then
local timeSinceBite = GetTime() - CleveRoids.lastFerociousBiteTime
if timeSinceBite < 0.5 then
local currentCP = CleveRoids.GetComboPoints()
if currentCP > 0 then
-- Carnage procced! Combo points didn't drop to 0 (or rose back to 1)
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff00ff[Carnage]|r PROC DETECTED! CP=%d after Ferocious Bite (%.2fs ago)",
currentCP, timeSinceBite)
)
end
-- Apply the Carnage refresh to Rip and Rake
local targetGUID = CleveRoids.lastFerociousBiteTargetGUID
local targetName = CleveRoids.lastFerociousBiteTargetName or "Unknown"
local biteSpellID = CleveRoids.lastFerociousBiteSpellID
-- Call the Carnage refresh function in Utility.lua
if CleveRoids.libdebuff and CleveRoids.libdebuff.ApplyCarnageRefresh then
CleveRoids.libdebuff.ApplyCarnageRefresh(targetGUID, targetName, biteSpellID)
end
-- Clear the tracking to prevent multiple refreshes
CleveRoids.lastFerociousBiteTime = nil
CleveRoids.lastFerociousBiteTargetGUID = nil
CleveRoids.lastFerociousBiteTargetName = nil
CleveRoids.lastFerociousBiteSpellID = nil
end
else
-- Time window expired, clear tracking
if CleveRoids.lastFerociousBiteTime then
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff00ff[Carnage]|r No proc - time window expired (%.2fs)", timeSinceBite)
)
end
CleveRoids.lastFerociousBiteTime = nil
CleveRoids.lastFerociousBiteTargetGUID = nil
CleveRoids.lastFerociousBiteTargetName = nil
CleveRoids.lastFerociousBiteSpellID = nil
end
end
end
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