diff --git a/ComboPointTracker.lua b/ComboPointTracker.lua new file mode 100644 index 0000000..739933b --- /dev/null +++ b/ComboPointTracker.lua @@ -0,0 +1,582 @@ +--[[ + 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 {} +CleveRoids.spell_tracking = CleveRoids.spell_tracking or {} +CleveRoids.lastComboPoints = CleveRoids.lastComboPoints or 0 -- Track last known CP count + +-- Initialize SavedVariable for learned combo durations +-- Structure: CleveRoids_ComboDurations[spellID][comboPoints] = duration +CleveRoids_ComboDurations = CleveRoids_ComboDurations or {} + +-- 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 +} + +-- 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 +function CleveRoids.GetComboPoints() + return GetComboPoints("player", "target") or 0 +end + +-- Update stored combo points (call this frequently) +function CleveRoids.UpdateComboPoints() + local current = CleveRoids.GetComboPoints() + if current > 0 then + CleveRoids.lastComboPoints = current + 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 = string.gsub(spell, "%(Rank %d+%)", "") + local checkName = string.gsub(spellName, "%(Rank %d+%)", "") + 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 = string.gsub(spell, "%(Rank %d+%)", "") + local checkName = string.gsub(spellName, "%(Rank %d+%)", "") + 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 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 + + -- Check for learned duration first + local learned = CleveRoids.GetLearnedComboDuration(spellID, comboPoints) + if learned then + return learned + end + + -- Fall back to formula + return data.base + (comboPoints - 1) * data.increment +end + +-- Function to track combo points when casting (by spell name) +function CleveRoids.TrackComboPointCast(spellName) + if not CleveRoids.IsComboScalingSpell(spellName) then + return + end + + local comboPoints = CleveRoids.GetComboPoints() + 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 + -- First, try lastComboPoints + if CleveRoids.lastComboPoints > 0 then + comboPoints = CleveRoids.lastComboPoints + CleveRoids.lastComboPoints = 0 -- Reset after using + else + -- Second, check if name-based tracking has recent data for this spell + local spellName = SpellInfo(spellID) + if spellName then + -- Remove rank info for comparison + local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + 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 + + return duration +end + +-- API function to get last tracked combo points for a spell +function CleveRoids.GetLastComboPointsForSpell(spellName) + if CleveRoids.ComboPointTracking[spellName] then + return CleveRoids.ComboPointTracking[spellName].combo_points + elseif CleveRoids.spell_tracking[spellName] then + return CleveRoids.spell_tracking[spellName].last_combo_points + end + return nil +end + +-- API function to get last calculated duration for a spell +function CleveRoids.GetLastDurationForSpell(spellName) + if CleveRoids.ComboPointTracking[spellName] then + return CleveRoids.ComboPointTracking[spellName].duration + elseif CleveRoids.spell_tracking[spellName] then + return CleveRoids.spell_tracking[spellName].last_duration + end + return nil +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 +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 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.RegisterEvent("UNIT_AURA", "OnUnitAura") + Extension.RegisterEvent("PLAYER_COMBO_POINTS", "OnComboPointsChanged") + + -- Set up OnUpdate to track combo points + Extension.internal.frame:SetScript("OnUpdate", function() + CleveRoids.UpdateComboPoints() + end) + + -- Hook the spell cast functions if they exist + if CleveRoids.CastSpell then + Extension.Hook("CleveRoids.CastSpell", "CastSpell_Hook") + end + + if CastSpellByName then + Extension.Hook("CastSpellByName", "CastSpellByName_Hook") + end + + -- Hook global CastSpell and UseAction for confirmation + if _G.CastSpell then + local originalCastSpell = _G.CastSpell + _G.CastSpell = function(id, bookType) + -- Call original first + originalCastSpell(id, bookType) + + -- Confirm tracking for combo spells + local spellName = GetSpellName(id, bookType) + if spellName and CleveRoids.IsComboScalingSpell(spellName) and + CleveRoids.ComboPointTracking[spellName] then + CleveRoids.ComboPointTracking[spellName].confirmed = true + end + end + end + + if _G.UseAction then + local originalUseAction = _G.UseAction + _G.UseAction = function(slot, target, button) + -- Call original first + originalUseAction(slot, target, button) + + -- Try to determine spell name from action slot + if not GetActionText(slot) and IsCurrentAction(slot) then + local spellName = GetActionText(slot) + if not spellName then + -- Try to get from tooltip + GameTooltip:SetAction(slot) + spellName = GameTooltipTextLeft1:GetText() + end + + if spellName and CleveRoids.IsComboScalingSpell(spellName) and + CleveRoids.ComboPointTracking[spellName] then + CleveRoids.ComboPointTracking[spellName].confirmed = true + end + end + end + end +end + +-- Update combo points on relevant events +function Extension.OnTargetChanged() + CleveRoids.UpdateComboPoints() +end + +function Extension.OnUnitAura() + if arg1 == "target" or arg1 == "player" then + CleveRoids.UpdateComboPoints() + end +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 + 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 + 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 + 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 + 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 diff --git a/Compatibility/pfUI.lua b/Compatibility/pfUI.lua index 992cf25..110fea1 100644 --- a/Compatibility/pfUI.lua +++ b/Compatibility/pfUI.lua @@ -91,6 +91,171 @@ function Extension.HandleSendChatMessageHook() end end +-- Hook pfUI's libdebuff to use our combo-aware durations +function Extension.HookPfUILibdebuff() + if not pfUI or not pfUI.api or not pfUI.api.libdebuff then + return false + end + + local pflib = pfUI.api.libdebuff + + -- Hook GetDuration if it exists + if pflib.GetDuration and not Extension.pfLibDebuffHooked then + local originalGetDuration = pflib.GetDuration + + pflib.GetDuration = function(self, spellID, casterGUID, comboPoints) + -- Check if this is a combo scaling spell first + if CleveRoids.IsComboScalingSpellID and CleveRoids.IsComboScalingSpellID(spellID) then + -- If combo points provided, check learned combo durations + if comboPoints and CleveRoids_ComboDurations and CleveRoids_ComboDurations[spellID] then + local comboDur = CleveRoids_ComboDurations[spellID][comboPoints] + if comboDur and comboDur > 0 then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ff00[pfUI Hook]|r Returning %ds for spell %d at %d CP", + comboDur, spellID, comboPoints) + ) + end + return comboDur + end + end + + -- Fallback: check for highest learned combo duration + if CleveRoids_ComboDurations and CleveRoids_ComboDurations[spellID] then + for cp = 5, 1, -1 do + if CleveRoids_ComboDurations[spellID][cp] then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ff00[pfUI Hook]|r Returning %ds for spell %d (fallback %d CP)", + CleveRoids_ComboDurations[spellID][cp], spellID, cp) + ) + end + return CleveRoids_ComboDurations[spellID][cp] + end + end + end + end + + -- Not a combo spell or no learned duration, use original + return originalGetDuration(self, spellID, casterGUID, comboPoints) + end + + Extension.pfLibDebuffHooked = true + Extension.DLOG("Hooked pfUI.api.libdebuff.GetDuration for combo duration support") + end + + -- Hook AddEffect if it exists to inject combo-aware durations + if pflib.AddEffect and not Extension.pfLibAddEffectHooked then + local originalAddEffect = pflib.AddEffect + + pflib.AddEffect = function(self, unit, unitlevel, effect, duration, caster) + -- effect is a spell name, not ID + -- Check if this is a combo scaling spell by name + if CleveRoids.IsComboScalingSpell and CleveRoids.IsComboScalingSpell(effect) then + -- Use name-based tracking which is populated BEFORE pfUI's AddEffect + if CleveRoids.ComboPointTracking and CleveRoids.ComboPointTracking[effect] then + local tracking = CleveRoids.ComboPointTracking[effect] + -- Only use tracking if it's confirmed (actual cast, not just evaluation) + if tracking.duration and tracking.confirmed and (GetTime() - tracking.cast_time) < 0.5 then + duration = tracking.duration + -- Don't clear confirmed flag - let time-based expiry handle it + -- pfUI may call AddEffect multiple times for the same spell + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ff00[pfUI AddEffect Hook]|r Overriding %s duration to %ds (from confirmed tracking)", + effect, duration) + ) + end + elseif CleveRoids.debug and tracking.duration and not tracking.confirmed and (GetTime() - tracking.cast_time) < 0.5 then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffaaaa00[pfUI AddEffect Hook]|r Ignoring %s tracking (not confirmed - evaluation only)", + effect) + ) + end + end + end + + return originalAddEffect(self, unit, unitlevel, effect, duration, caster) + end + + Extension.pfLibAddEffectHooked = true + Extension.DLOG("Hooked pfUI.api.libdebuff.AddEffect for combo duration support") + end + + return Extension.pfLibDebuffHooked or Extension.pfLibAddEffectHooked +end + +-- Synchronize combo durations to pfUI's libdebuff objects +function Extension.SyncComboDurationToPfUI(guid, spellID, duration) + if not pfUI or not pfUI.api or not pfUI.api.libdebuff then + return + end + + -- Get unit name from GUID + local unitName = nil + local unitLevel = 0 + + -- Check if this is the current target + local _, targetGUID = UnitExists("target") + if targetGUID == guid then + unitName = UnitName("target") + unitLevel = UnitLevel("target") or 0 + end + + -- If we couldn't find the unit, use GUID to name mapping from libdebuff + if not unitName and CleveRoids.libdebuff and CleveRoids.libdebuff.guidToName then + unitName = CleveRoids.libdebuff.guidToName[guid] + -- Default to level 0 if we don't have the unit targeted + unitLevel = 0 + end + + if not unitName then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[pfUI Sync]|r Could not find unit name for GUID") + end + return + end + + -- Get spell name from spell ID + local spellName = SpellInfo(spellID) + if not spellName then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[pfUI Sync]|r Could not find spell name for ID " .. spellID) + end + return + end + + -- Remove rank from spell name to match pfUI's format + local effectName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + + -- Update pfUI's stored debuff duration + local pflib = pfUI.api.libdebuff + if pflib.objects and pflib.objects[unitName] then + -- Try both the specific level and level 0 (fallback) + for _, level in ipairs({unitLevel, 0}) do + if pflib.objects[unitName][level] and pflib.objects[unitName][level][effectName] then + local old_duration = pflib.objects[unitName][level][effectName].duration + pflib.objects[unitName][level][effectName].duration = duration + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ffaa[pfUI Sync]|r Updated %s on %s (L%d): %ds -> %ds", + effectName, unitName, level, old_duration or 0, duration) + ) + end + return + end + end + end + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffaaaa00[pfUI Sync]|r Effect not found in pfUI storage: %s on %s", + effectName, unitName) + ) + end +end + -- Main compatibility check and setup function Extension.SetupCompatibility() Extension.pfUILoaded = (pfUI ~= nil) @@ -99,6 +264,9 @@ function Extension.SetupCompatibility() if Extension.pfUILoaded then Extension.DLOG("pfUI detected") + -- Hook libdebuff for combo duration support + Extension.HookPfUILibdebuff() + if Extension.macrotweakLoaded then Extension.DLOG("pfUI macrotweak module detected") @@ -115,6 +283,9 @@ function Extension.OnLoad() Extension.DLOG("Extension pfUI Loaded.") Extension.HookMethod(CleveRoids, "GetFocusName", "FocusNameHook", true) + -- Export extension for external access + CleveRoids.Compatibility_pfUI = Extension + -- Initial compatibility check Extension.SetupCompatibility() end diff --git a/Core.lua b/Core.lua index 110d35a..df04981 100644 --- a/Core.lua +++ b/Core.lua @@ -2250,7 +2250,7 @@ function CleveRoids.OnUpdate(self) end for guid,cast in pairs(CleveRoids.spell_tracking) do - if time > cast.expires then + if cast.expires and time > cast.expires then CleveRoids.spell_tracking[guid] = nil end end @@ -3113,12 +3113,30 @@ SlashCmdList["CLEVEROID"] = function(msg) if b then val = b else val = "" end if c then val2 = c else val2 = "" end - -- No command: show current value + -- No command: show current value and available commands if cmd == "" then CleveRoids.Print("Current Settings:") DEFAULT_CHAT_FRAME:AddMessage("realtime (force fast updates, CPU intensive) = " .. CleveRoidMacros.realtime .. " (Default: 0)") DEFAULT_CHAT_FRAME:AddMessage("refresh (updates per second) = " .. CleveRoidMacros.refresh .. " (Default: 5)") DEFAULT_CHAT_FRAME:AddMessage("debug (show learning messages) = " .. (CleveRoids.debug and "1" or "0") .. " (Default: 0)") + DEFAULT_CHAT_FRAME:AddMessage(" ") + CleveRoids.Print("Available Commands:") + DEFAULT_CHAT_FRAME:AddMessage("/cleveroid realtime 0 or 1 - Force realtime updates") + DEFAULT_CHAT_FRAME:AddMessage("/cleveroid refresh X - Set refresh rate (1-10 updates/sec)") + if CleveRoids.hasSuperwow then + DEFAULT_CHAT_FRAME:AddMessage("/cleveroid learn - Manually set spell duration") + DEFAULT_CHAT_FRAME:AddMessage("/cleveroid forget - Forget learned duration(s)") + DEFAULT_CHAT_FRAME:AddMessage("/cleveroid debug [0|1] - Toggle learning debug messages") + end + DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Immunity Tracking:|r") + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listimmune [school] - List immunity data') + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addimmune "" [buff] - Add immunity') + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeimmune "" - Remove immunity') + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearimmune [school] - Clear immunity data') + DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Combo Point Tracking:|r") + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combotrack - Show combo point tracking info') + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid comboclear - Clear combo tracking data') + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combolearn - Show learned combo durations (per CP)') return end @@ -3268,6 +3286,39 @@ SlashCmdList["CLEVEROID"] = function(msg) return end + -- combotrack (show combo point tracking info) + if cmd == "combotrack" or cmd == "combo" then + CleveRoids.ShowComboTracking() + return + end + + -- comboclear (clear combo tracking data) + if cmd == "comboclear" then + CleveRoids.ComboPointTracking = {} + CleveRoids.ComboPointTracking.byID = {} + CleveRoids.Print("Combo point tracking data cleared") + return + end + + -- combolearn (show learned combo durations) + if cmd == "combolearn" or cmd == "combodurations" then + CleveRoids.Print("=== Learned Combo Durations ===") + if not CleveRoids_ComboDurations or not next(CleveRoids_ComboDurations) then + CleveRoids.Print("No learned combo durations yet. Cast finishers and let them expire!") + else + for spellID, cpData in pairs(CleveRoids_ComboDurations) do + local spellName = SpellInfo(spellID) or ("Spell " .. spellID) + CleveRoids.Print(spellName .. " (ID:" .. spellID .. "):") + for cp = 1, 5 do + if cpData[cp] then + CleveRoids.Print(" " .. cp .. " CP = " .. cpData[cp] .. "s") + end + end + end + end + return + end + -- Unknown command fallback CleveRoids.Print("Usage:") DEFAULT_CHAT_FRAME:AddMessage("/cleveroid - Show current settings") @@ -3283,6 +3334,10 @@ SlashCmdList["CLEVEROID"] = function(msg) DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addimmune "" [buff] - Add immunity') DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeimmune "" - Remove immunity') DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearimmune [school] - Clear immunity data') + DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Combo Point Tracking:|r") + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combotrack - Show combo point tracking info') + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid comboclear - Clear combo tracking data') + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combolearn - Show learned combo durations (per CP)') end SLASH_CLEAREQUIPQUEUE1 = "/clearequipqueue" diff --git a/README.md b/README.md index 7540f2d..748a7ca 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,13 @@ Check slash command and all conditional lists for new usages! * `/cleveroid refresh X` - Set refresh rate (1 to 10 updates per second. Default: 5) * `/cleveroid learn ` - Manually set spell duration in seconds * `/cleveroid forget ` - Forget learned spell duration(s) -* `/cleveroid listimmune [school]` - List all or specific school immunities -* `/cleveroid addimmune "" [buff]` - Add manual immunity +* `/cleveroid debug [0|1]` - Toggle learning debug messages +* `/cleveroid listimmune [school]` - List all or specific school immunities +* `/cleveroid addimmune "" [buff]` - Add manual immunity * `/cleveroid removeimmune "" ` - Remove immunity -* `/cleveroid clearimmune [school]` - Clear data +* `/cleveroid clearimmune [school]` - Clear immunity data +* `/cleveroid combotrack` - Show combo point tracking info (Rip, Rupture, Kidney Shot) +* `/cleveroid comboclear` - Clear combo tracking data --- @@ -171,6 +174,25 @@ The system includes pre-configured durations for 329+ debuffs across all classes - **Paladin:** Judgements, Hammer of Justice, etc. - **Shaman:** Flame Shock, Frost Shock, etc. +### Combo Point Scaling +The addon automatically tracks combo point finishers that scale duration with combo points: +- **Rogue Rupture** (all ranks): 8s base + 2s per combo point (8s @ 1 CP, 16s @ 5 CP) +- **Rogue Kidney Shot**: Rank 1: 1s + 1s per CP, Rank 2: 2s + 1s per CP +- **Druid Rip** (all ranks): 12s base + 4s per combo point (12s @ 1 CP, 28s @ 5 CP) + +The system automatically detects combo points at cast time and calculates the correct duration: +```lua +-- Will show accurate duration based on combo points used +/cast [nodebuff:Rip] Rip +/cast [debuff:Rip<4] Rip + +-- Rupture duration tracking with combo point awareness +/cast [nodebuff:Rupture] Rupture +/cast [debuff:Rupture<2] Rupture +``` + +Use `/cleveroid combotrack` to see recent combo finisher casts and their calculated durations. + ### Technical Details - Uses **UNIT_CASTEVENT** for precise cast detection (only tracks successful hits) - Uses **RAW_COMBATLOG** to detect when debuffs fade diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index feb25dc..cbe22e3 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -4,7 +4,7 @@ ## Notes: /cleveroid for settings ## Version: 1.3 ## OptionalDeps: ClassicFocus, FocusFrame, pfUI, SuperMacro, Bongos_ActionBar -## SavedVariables: CleveRoidMacros, CleveRoids_LearnedDurations, CleveRoids_AuraTextures, CleveRoids_ImmunityData +## SavedVariables: CleveRoidMacros, CleveRoids_LearnedDurations, CleveRoids_AuraTextures, CleveRoids_ImmunityData, CleveRoids_ComboDurations Localization.lua Init.lua Utility.lua @@ -12,6 +12,7 @@ Core.lua Conditionals.lua Console.lua ExtensionsManager.lua +ComboPointTracker.lua Compatibility\SuperMacro.lua Compatibility\pfUI.lua diff --git a/Utility.lua b/Utility.lua index 913e3fb..6ab1a0f 100644 --- a/Utility.lua +++ b/Utility.lua @@ -373,7 +373,7 @@ lib.durations = lib.durations or { [2070] = 35, -- Sap (Rank 2) [11297] = 45, -- Sap (Rank 3) - [1776] = 4, -- Gouge (Rank 1) - base duration + [1776] = 4, -- Gouge (Rank 1) - Scales with talent [1777] = 4, -- Gouge (Rank 2) [8629] = 4, -- Gouge (Rank 3) [11285] = 4, -- Gouge (Rank 4) @@ -718,15 +718,47 @@ lib.learnCastTimers = lib.learnCastTimers or {} CleveRoids_LearnedDurations = CleveRoids_LearnedDurations or {} -function lib:GetDuration(spellID, casterGUID) +function lib:GetDuration(spellID, casterGUID, comboPoints) + -- Check combo-specific learned durations first if this is a combo spell + if comboPoints and CleveRoids_ComboDurations and CleveRoids_ComboDurations[spellID] then + local comboDuration = CleveRoids_ComboDurations[spellID][comboPoints] + if comboDuration and comboDuration > 0 then + if CleveRoids.debug then + local spellName = SpellInfo(spellID) or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffccccff[DEBUG GetDuration]|r %s (ID:%d) CP:%d -> %ds (from learned combo)", + spellName, spellID, comboPoints, comboDuration) + ) + end + return comboDuration + end + end + + -- Check caster-specific learned durations if casterGUID and CleveRoids_LearnedDurations[spellID] then local learned = CleveRoids_LearnedDurations[spellID][casterGUID] if learned and learned > 0 then + if CleveRoids.debug then + local spellName = SpellInfo(spellID) or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffccccff[DEBUG GetDuration]|r %s (ID:%d) -> %ds (from learned caster)", + spellName, spellID, learned) + ) + end return learned end end - return self.durations[spellID] or 0 + -- Fall back to static database + local staticDur = self.durations[spellID] or 0 + if CleveRoids.debug and staticDur > 0 then + local spellName = SpellInfo(spellID) or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffccccff[DEBUG GetDuration]|r %s (ID:%d) -> %ds (from static DB)", + spellName, spellID, staticDur) + ) + end + return staticDur end function lib:AddEffect(guid, unitName, spellID, duration, stacks, caster) @@ -746,6 +778,15 @@ function lib:AddEffect(guid, unitName, spellID, duration, stacks, caster) rec.caster = caster lib.objects[guid][spellID] = rec + + -- DEBUG: Show what we stored + if CleveRoids.debug then + local spellName = SpellInfo(spellID) or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ffff[DEBUG AddEffect]|r %s (ID:%d) stored duration:%ds on %s", + spellName, spellID, duration, unitName or "Unknown") + ) + end end function lib:UnitDebuff(unit, id) @@ -756,7 +797,7 @@ function lib:UnitDebuff(unit, id) if not texture or not spellID then texture, stacks, spellID = UnitBuff(unit, id) - -- Only accept buffs that are known debuffs (either static or learned durations) + -- Only accept buffs that are known debuffs (either static or learned durations, including combo durations) if texture and spellID and lib:GetDuration(spellID) <= 0 then return nil end @@ -797,6 +838,25 @@ local function SeedUnit(unit) local duration = lib:GetDuration(spellID) if duration > 0 then if not (lib.objects[guid] and lib.objects[guid][spellID]) then + -- For combo spells, try to use the highest learned duration as a fallback + if CleveRoids.IsComboScalingSpellID and CleveRoids.IsComboScalingSpellID(spellID) and + CleveRoids_ComboDurations and CleveRoids_ComboDurations[spellID] then + -- Use the longest learned duration (assume 5 CP) + for cp = 5, 1, -1 do + if CleveRoids_ComboDurations[spellID][cp] then + duration = CleveRoids_ComboDurations[spellID][cp] + if CleveRoids.debug then + local spellName = SpellInfo(spellID) or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffaaff00[DEBUG SeedUnit Debuff]|r %s (ID:%d) using learned %dCP duration:%ds", + spellName, spellID, cp, duration) + ) + end + break + end + end + end + duration = duration or lib:GetDuration(spellID) lib:AddEffect(guid, unitName, spellID, duration, stacks) end end @@ -811,6 +871,25 @@ local function SeedUnit(unit) local duration = lib:GetDuration(spellID) if duration > 0 then if not (lib.objects[guid] and lib.objects[guid][spellID]) then + -- For combo spells, try to use the highest learned duration as a fallback + if CleveRoids.IsComboScalingSpellID and CleveRoids.IsComboScalingSpellID(spellID) and + CleveRoids_ComboDurations and CleveRoids_ComboDurations[spellID] then + -- Use the longest learned duration (assume 5 CP) + for cp = 5, 1, -1 do + if CleveRoids_ComboDurations[spellID][cp] then + duration = CleveRoids_ComboDurations[spellID][cp] + if CleveRoids.debug then + local spellName = SpellInfo(spellID) or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffaaff00[DEBUG SeedUnit Buff]|r %s (ID:%d) using learned %dCP duration:%ds", + spellName, spellID, cp, duration) + ) + end + break + end + end + end + duration = duration or lib:GetDuration(spellID) lib:AddEffect(guid, unitName, spellID, duration, stacks) end end @@ -843,9 +922,33 @@ ev:SetScript("OnEvent", function() local _, playerGUID = UnitExists("player") if casterGUID == playerGUID and targetGUID then - local duration = lib:GetDuration(spellID, casterGUID) + -- Check if this is a combo point scaling spell first + local duration = nil + local comboPoints = nil + if CleveRoids.TrackComboPointCastByID then + duration = CleveRoids.TrackComboPointCastByID(spellID, targetGUID) + -- Get combo points used from tracking + if CleveRoids.ComboPointTracking and CleveRoids.ComboPointTracking.byID and + CleveRoids.ComboPointTracking.byID[spellID] then + comboPoints = CleveRoids.ComboPointTracking.byID[spellID].combo_points + end + end - if duration > 0 then + -- If not a combo scaling spell, use normal duration lookup + if not duration then + duration = lib:GetDuration(spellID, casterGUID) + end + + -- DEBUG: Show what duration we calculated + if CleveRoids.debug and duration then + local spellName = SpellInfo(spellID) or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffff00ff[DEBUG CAST]|r %s (ID:%d) CP:%s duration:%ds", + spellName, spellID, tostring(comboPoints or "nil"), duration) + ) + end + + if duration and duration > 0 then local targetName = lib.guidToName[targetGUID] if not targetName then local _, currentTargetGUID = UnitExists("target") @@ -858,6 +961,22 @@ ev:SetScript("OnEvent", function() end lib:AddEffect(targetGUID, targetName, spellID, duration, 0, "player") + -- Sync combo duration to pfUI if it's loaded + if comboPoints and CleveRoids.Compatibility_pfUI and + CleveRoids.Compatibility_pfUI.SyncComboDurationToPfUI then + CleveRoids.Compatibility_pfUI.SyncComboDurationToPfUI(targetGUID, spellID, duration) + end + + -- ALWAYS set up learning for combo spells (even if we have calculated duration) + if comboPoints then + lib.learnCastTimers[targetGUID] = lib.learnCastTimers[targetGUID] or {} + lib.learnCastTimers[targetGUID][spellID] = { + start = GetTime(), + caster = casterGUID, + comboPoints = comboPoints -- Store CP count for learning + } + end + else lib.learnCastTimers[targetGUID] = lib.learnCastTimers[targetGUID] or {} lib.learnCastTimers[targetGUID][spellID] = { @@ -912,14 +1031,31 @@ evLearn:SetScript("OnEvent", function() local casterGUID = lib.learnCastTimers[targetGUID][spellID].caster local actualDuration = timestamp - castTime - CleveRoids_LearnedDurations[spellID] = CleveRoids_LearnedDurations[spellID] or {} - CleveRoids_LearnedDurations[spellID][casterGUID] = floor(actualDuration + 0.5) + -- Check if this is a combo point spell - if so, learn it with combo point context + local comboPoints = lib.learnCastTimers[targetGUID][spellID].comboPoints + if comboPoints and CleveRoids.IsComboScalingSpellID and CleveRoids.IsComboScalingSpellID(spellID) then + -- Learn combo spell duration + CleveRoids_ComboDurations = CleveRoids_ComboDurations or {} + CleveRoids_ComboDurations[spellID] = CleveRoids_ComboDurations[spellID] or {} + CleveRoids_ComboDurations[spellID][comboPoints] = floor(actualDuration + 0.5) - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - "|cff4b7dccCleveRoids:|r Learned " .. spellName .. - " (ID:" .. spellID .. ") = " .. floor(actualDuration + 0.5) .. "s" - ) + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + "|cff4b7dccCleveRoids:|r Learned combo spell " .. spellName .. + " (ID:" .. spellID .. ") at " .. comboPoints .. " CP = " .. floor(actualDuration + 0.5) .. "s" + ) + end + else + -- Learn normal spell duration + CleveRoids_LearnedDurations[spellID] = CleveRoids_LearnedDurations[spellID] or {} + CleveRoids_LearnedDurations[spellID][casterGUID] = floor(actualDuration + 0.5) + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + "|cff4b7dccCleveRoids:|r Learned " .. spellName .. + " (ID:" .. spellID .. ") = " .. floor(actualDuration + 0.5) .. "s" + ) + end end lib.learnCastTimers[targetGUID][spellID] = nil