From ce017f58327f5a66d866db8e6ec58b0e5a5f26a8 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Sun, 30 Nov 2025 19:25:19 -0500 Subject: [PATCH] fix lower rank spells showing as refreshing higher ranks in pfui. further improve debuff duration handling. --- Compatibility/pfUI.lua | 61 ++++++++- Conditionals.lua | 13 +- Utility.lua | 274 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 317 insertions(+), 31 deletions(-) diff --git a/Compatibility/pfUI.lua b/Compatibility/pfUI.lua index 93dcb98..895dc7e 100644 --- a/Compatibility/pfUI.lua +++ b/Compatibility/pfUI.lua @@ -147,11 +147,68 @@ function Extension.HookPfUILibdebuff() Extension.DLOG("Hooked pfUI.api.libdebuff.GetDuration for Carnage and combo duration support") end - -- Hook AddEffect if it exists to inject combo-aware durations + -- Hook AddEffect if it exists to inject combo-aware durations and enforce rank checking if pflib.AddEffect and not Extension.pfLibAddEffectHooked then local originalAddEffect = pflib.AddEffect pflib.AddEffect = function(self, unit, unitlevel, effect, duration, caster) + -- RANK CHECKING: Preserve higher rank's remaining time if lower rank was cast + -- NOTE: 'unit' is a unit NAME (e.g., "Expert Training Dummy"), not a unit ID + if caster == "player" and CleveRoids.libdebuff and duration and duration > 0 then + -- Try to find the GUID for this unit name + local unitGUID = nil + + -- Check if this is the current target + if UnitName("target") == unit then + local _, guid = UnitExists("target") + unitGUID = CleveRoids.NormalizeGUID(guid) + end + + -- If we couldn't match to current target, check guidToName mapping + if not unitGUID and CleveRoids.libdebuff.guidToName then + for guid, name in pairs(CleveRoids.libdebuff.guidToName) do + if name == unit then + unitGUID = guid + break + end + end + end + + -- Check if a higher rank of this spell is already active + if unitGUID and CleveRoids.libdebuff.objects and CleveRoids.libdebuff.objects[unitGUID] then + -- Find all spell IDs that match this effect name + for spellID, rec in pairs(CleveRoids.libdebuff.objects[unitGUID]) do + if rec and rec.start and rec.duration then + -- Get spell name for this ID + local spellName = SpellInfo(spellID) + if spellName then + local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + if baseName == effect then + -- Same spell - check if still active + local remaining = rec.duration + rec.start - GetTime() + if remaining > 0 then + -- If incoming duration > remaining time, we're trying to add more time + -- This means either a refresh or lower rank cast - preserve existing timer + if duration > remaining then + if Extension.Debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00aaff[pfUI Rank Preserve]|r %s: Preserving timer (%.1fs remaining vs %.1fs incoming)", + effect, remaining, duration) + ) + end + + -- Preserve the existing timer + duration = remaining + end + break + end + end + end + end + end + end + end + -- Check for Carnage duration overrides FIRST (highest priority) local carnageDuration = GetCarnageOverride(effect) if carnageDuration then @@ -174,7 +231,7 @@ function Extension.HookPfUILibdebuff() end Extension.pfLibAddEffectHooked = true - Extension.DLOG("Hooked pfUI.api.libdebuff.AddEffect for combo duration support") + Extension.DLOG("Hooked pfUI.api.libdebuff.AddEffect for combo duration and rank checking support") end -- Hook UnitDebuff to return Carnage override duration to display code diff --git a/Conditionals.lua b/Conditionals.lua index ce9136a..a5d0557 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -964,7 +964,18 @@ function CleveRoids.ValidateUnitDebuff(unit, args) -- FALLBACK: If not found in tracking table (e.g., shared debuff from another player), -- scan actual debuffs on target using UnitDebuff() - if not found and CleveRoids.hasSuperwow then + -- IMPORTANT: Skip this fallback for personal debuffs - they should only match if player cast them + local isPersonalDebuff = false + if table.getn(matchingSpellIDs) > 0 then + for _, spellID in ipairs(matchingSpellIDs) do + if CleveRoids.libdebuff:IsPersonalDebuff(spellID) then + isPersonalDebuff = true + break + end + end + end + + if not found and not isPersonalDebuff and CleveRoids.hasSuperwow then -- Scan debuff slots for i = 1, 16 do local tex, debuffStacks, _, debuffSpellID = UnitDebuff(unit, i) diff --git a/Utility.lua b/Utility.lua index 434fa2e..33bff23 100644 --- a/Utility.lua +++ b/Utility.lua @@ -771,6 +771,163 @@ function lib:IsPersonalDebuff(spellID) return true end +-- Helper function to extract numeric rank from spell ID +-- Returns rank number (1, 2, 3, etc.) or 0 if no rank found +function lib:GetSpellRank(spellID) + if not spellID then return 0 end + local name, rankStr = SpellInfo(spellID) + if not rankStr or rankStr == "" then return 0 end + + -- Handle different rank formats + -- Could be: "Rank 9", "9", or number 9 + local rankType = type(rankStr) + + if rankType == "number" then + return rankStr + elseif rankType == "string" then + -- Try to extract number from "Rank X" format or just "X" + local rank = string.match(rankStr, "(%d+)") + return tonumber(rank) or 0 + end + + return 0 +end + +-- Helper function to get spell base name (without rank) +function lib:GetSpellBaseName(spellID) + if not spellID then return nil end + local name = SpellInfo(spellID) + if not name then return nil end + + -- Remove rank suffix + return string.gsub(name, "%s*%(Rank %d+%)", "") +end + +-- Track rank refreshes for pfUI integration +lib.rankRefreshOverrides = lib.rankRefreshOverrides or {} + +-- Helper function to check if we should apply a debuff based on rank comparison +-- Returns: true (apply), false (skip), or {refresh=spellID, duration=X} (refresh higher rank) +-- Also removes lower/equal rank versions when applying a higher rank +function lib:ShouldApplyDebuffRank(targetGUID, newSpellID) + if not targetGUID or not newSpellID then return true end + + -- Get base name and rank of new spell + local newBaseName = lib:GetSpellBaseName(newSpellID) + local newRank = lib:GetSpellRank(newSpellID) + + if not newBaseName then return true end + + -- Track highest existing rank and spell IDs to remove + local highestExistingRank = 0 + local highestExistingSpellID = nil + local highestExistingDuration = nil + local spellIDsToRemove = {} + + -- Check all active debuffs on target for same base name + if lib.objects[targetGUID] then + for existingSpellID, rec in pairs(lib.objects[targetGUID]) do + if rec and rec.start and rec.duration and existingSpellID ~= newSpellID then + -- Check if debuff is still active + local remaining = rec.duration + rec.start - GetTime() + if remaining > 0 then + -- Check if same spell (same base name) + local existingBaseName = lib:GetSpellBaseName(existingSpellID) + if existingBaseName == newBaseName then + -- Same spell - track for potential removal + local existingRank = lib:GetSpellRank(existingSpellID) + + if existingRank > highestExistingRank then + highestExistingRank = existingRank + highestExistingSpellID = existingSpellID + highestExistingDuration = rec.duration + end + + -- Mark all same-name spells for potential removal + table.insert(spellIDsToRemove, existingSpellID) + end + end + end + end + end + + -- If a higher rank exists, preserve its remaining time instead of blocking + if newRank > 0 and highestExistingRank > 0 and newRank < highestExistingRank then + -- Calculate remaining time on the higher rank + local rec = lib.objects[targetGUID][highestExistingSpellID] + local timeRemaining = rec.duration + rec.start - GetTime() + + -- Store refresh override for pfUI integration + lib.rankRefreshOverrides[newBaseName] = { + timestamp = GetTime(), + targetGUID = targetGUID, + lowerRankCast = newRank, + higherRankActive = highestExistingRank, + refreshSpellID = highestExistingSpellID, + preservedTimeRemaining = timeRemaining + } + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00aaff[Rank Preserve]|r Cast %s Rank %d, preserving Rank %d timer (%.1fs remaining)", + newBaseName, newRank, highestExistingRank, timeRemaining) + ) + end + + -- Return special value indicating we should preserve the higher rank's timer + return { + preserve = highestExistingSpellID, + timeRemaining = timeRemaining, + targetGUID = targetGUID + } + end + + -- Remove all other ranks (lower or equal) before applying new rank + if table.getn(spellIDsToRemove) > 0 then + -- Get target name for pfUI cleanup + local targetName = lib.guidToName[targetGUID] + + for _, removeID in ipairs(spellIDsToRemove) do + if lib.objects[targetGUID][removeID] then + local removeRank = lib:GetSpellRank(removeID) + lib.objects[targetGUID][removeID] = nil + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ff00[Rank Check]|r Removed %s (Rank %d) before applying Rank %d", + newBaseName, removeRank, newRank) + ) + end + end + end + + -- Also clean up pfUI's tracking to prevent it from showing old ranks + if pfUI and pfUI.api and pfUI.api.libdebuff and targetName then + local pflib = pfUI.api.libdebuff + + if pflib.objects and pflib.objects[targetName] then + for level, effects in pairs(pflib.objects[targetName]) do + if type(effects) == "table" and effects[newBaseName] then + -- Remove the old rank entry from pfUI + effects[newBaseName] = nil + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ff00[Rank Check]|r Removed %s from pfUI tracking before applying new rank", + newBaseName) + ) + end + break + end + end + end + end + end + + -- Apply the new rank + return true +end + lib.learnCastTimers = lib.learnCastTimers or {} CleveRoids_LearnedDurations = CleveRoids_LearnedDurations or {} @@ -1155,12 +1312,16 @@ local function ApplyCarnageRefresh(targetGUID, targetName, biteSpellID) if not lib.objects[targetGUID] then return end -- Try to refresh Rip - if CleveRoids.lastRipCast and CleveRoids.lastRipCast.duration and + if CleveRoids.lastRipCast and CleveRoids.lastRipCast.duration and CleveRoids.lastRipCast.spellID and CleveRoids.lastRipCast.targetGUID == targetGUID then - -- Find which Rip rank is currently active - for ripSpellID, _ in pairs(CleveRoids.RipSpellIDs) do - if lib.objects[targetGUID][ripSpellID] then - -- Found an active Rip, refresh it with the saved duration + -- Only refresh if the SAME rank that was cast is still active + local ripSpellID = CleveRoids.lastRipCast.spellID + local rec = lib.objects[targetGUID] and lib.objects[targetGUID][ripSpellID] + if rec and rec.duration and rec.start then + -- Check if Rip is still active (not expired) + local remaining = rec.duration + rec.start - GetTime() + if remaining > 0 then + -- Found the exact same rank active, refresh it with the saved duration local ripDuration = CleveRoids.lastRipCast.duration local ripComboPoints = CleveRoids.lastRipCast.comboPoints or 5 @@ -1240,18 +1401,21 @@ local function ApplyCarnageRefresh(targetGUID, targetName, biteSpellID) ripDuration, targetName or "Unknown") ) end - break end end end -- Try to refresh Rake - if CleveRoids.lastRakeCast and CleveRoids.lastRakeCast.duration and + if CleveRoids.lastRakeCast and CleveRoids.lastRakeCast.duration and CleveRoids.lastRakeCast.spellID and CleveRoids.lastRakeCast.targetGUID == targetGUID then - -- Find which Rake rank is currently active - for rakeSpellID, _ in pairs(CleveRoids.RakeSpellIDs) do - if lib.objects[targetGUID][rakeSpellID] then - -- Found an active Rake, refresh it with the saved duration + -- Only refresh if the SAME rank that was cast is still active + local rakeSpellID = CleveRoids.lastRakeCast.spellID + local rec = lib.objects[targetGUID] and lib.objects[targetGUID][rakeSpellID] + if rec and rec.duration and rec.start then + -- Check if Rake is still active (not expired) + local remaining = rec.duration + rec.start - GetTime() + if remaining > 0 then + -- Found the exact same rank active, refresh it with the saved duration local rakeDuration = CleveRoids.lastRakeCast.duration local rakeComboPoints = CleveRoids.lastRakeCast.comboPoints or 5 @@ -1318,7 +1482,6 @@ local function ApplyCarnageRefresh(targetGUID, targetName, biteSpellID) rakeDuration, targetName or "Unknown") ) end - break end end end @@ -1378,6 +1541,16 @@ delayedTrackingFrame:SetScript("OnUpdate", function() table.remove(lib.pendingPersonalDebuffs, toRemove[i]) end end + + -- Clean up old rank refresh overrides (older than 2 seconds) + if lib.rankRefreshOverrides then + local currentTime = GetTime() + for spellName, override in pairs(lib.rankRefreshOverrides) do + if currentTime - override.timestamp > 2.0 then + lib.rankRefreshOverrides[spellName] = nil + end + end + end end) local ev = CreateFrame("Frame", "CleveRoidsLibDebuffFrame", UIParent) @@ -1568,23 +1741,48 @@ ev:SetScript("OnEvent", function() local isPersonal = lib:IsPersonalDebuff(spellID) if isPersonal then - -- Schedule personal debuff for delayed tracking (after 0.5s) - table.insert(lib.pendingPersonalDebuffs, { - timestamp = GetTime(), - targetGUID = targetGUID, - targetName = targetName, - spellID = spellID, - duration = duration, - comboPoints = comboPoints - }) + -- Check if we should apply based on rank comparison + local rankCheck = lib:ShouldApplyDebuffRank(targetGUID, spellID) - if CleveRoids.debug then - local spellName = SpellInfo(spellID) or "Unknown" - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffaaff00[Pending Track]|r Scheduled %s (ID:%d) for tracking on %s (will apply if hit)", - spellName, spellID, targetName or "Unknown") - ) + if rankCheck == true then + -- Normal application - schedule personal debuff for delayed tracking + table.insert(lib.pendingPersonalDebuffs, { + timestamp = GetTime(), + targetGUID = targetGUID, + targetName = targetName, + spellID = spellID, + duration = duration, + comboPoints = comboPoints + }) + + if CleveRoids.debug then + local spellName = SpellInfo(spellID) or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffaaff00[Pending Track]|r Scheduled %s (ID:%d) for tracking on %s (will apply if hit)", + spellName, spellID, targetName or "Unknown") + ) + end + elseif type(rankCheck) == "table" and rankCheck.preserve then + -- Preserve higher rank's timer - schedule with higher rank's spell ID and remaining time + table.insert(lib.pendingPersonalDebuffs, { + timestamp = GetTime(), + targetGUID = targetGUID, + targetName = targetName, + spellID = rankCheck.preserve, -- Use higher rank's spell ID + duration = rankCheck.timeRemaining, -- Use remaining time, not full duration + comboPoints = comboPoints, + isRankPreserve = true -- Flag this as a rank preserve + }) + + if CleveRoids.debug then + local spellName = SpellInfo(rankCheck.preserve) or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffaaff00[Pending Track]|r Scheduled rank preserve for %s (ID:%d, %.1fs remaining) on %s", + spellName, rankCheck.preserve, rankCheck.timeRemaining, targetName or "Unknown") + ) + end end + -- If rankCheck == false, skip entirely (shouldn't happen for personal debuffs with new logic) else -- Shared debuff - add immediately with predicted stack count -- For stacking debuffs (Sunder, Faerie Fire), predict new stack count @@ -1637,7 +1835,25 @@ ev:SetScript("OnEvent", function() newStacks = 1 end - lib:AddEffect(targetGUID, targetName, spellID, duration, newStacks, "player") + -- Check if we should apply based on rank comparison + local rankCheck = lib:ShouldApplyDebuffRank(targetGUID, spellID) + + if rankCheck == true then + -- Normal application + lib:AddEffect(targetGUID, targetName, spellID, duration, newStacks, "player") + elseif type(rankCheck) == "table" and rankCheck.preserve then + -- Preserve higher rank's timer + lib:AddEffect(targetGUID, targetName, rankCheck.preserve, rankCheck.timeRemaining, newStacks, "player") + + if CleveRoids.debug then + local spellName = SpellInfo(rankCheck.preserve) or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00aaff[Shared Debuff Rank Preserve]|r Preserved %s timer (%.1fs remaining)", + spellName, rankCheck.timeRemaining) + ) + end + end + -- If rankCheck == false, skip entirely (shouldn't happen with new logic) end -- Track this cast for miss/dodge/parry removal @@ -1650,6 +1866,7 @@ ev:SetScript("OnEvent", function() -- DRUID CARNAGE TALENT: Save Rip cast duration for later refresh by Ferocious Bite if CleveRoids.RipSpellIDs and CleveRoids.RipSpellIDs[spellID] then if CleveRoids.lastRipCast then + CleveRoids.lastRipCast.spellID = spellID CleveRoids.lastRipCast.duration = duration CleveRoids.lastRipCast.targetGUID = targetGUID CleveRoids.lastRipCast.comboPoints = comboPoints or 0 @@ -1666,6 +1883,7 @@ ev:SetScript("OnEvent", function() -- DRUID CARNAGE TALENT: Save Rake cast duration for later refresh by Ferocious Bite if CleveRoids.RakeSpellIDs and CleveRoids.RakeSpellIDs[spellID] then if CleveRoids.lastRakeCast then + CleveRoids.lastRakeCast.spellID = spellID CleveRoids.lastRakeCast.duration = duration CleveRoids.lastRakeCast.targetGUID = targetGUID CleveRoids.lastRakeCast.comboPoints = comboPoints or 0