diff --git a/Conditionals.lua b/Conditionals.lua index 8fa009a..a9a18c4 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -755,9 +755,13 @@ function CleveRoids.ValidateAura(unit, args, isbuff) if current_spellID and searchName then local auraName = SpellInfo(current_spellID) - if auraName and string.lower(auraName) == searchName then - found = true - break + if auraName then + -- Strip rank for comparison (handles "Moonfire (Rank 7)" vs "moonfire") + local baseName = string.gsub(auraName, "%s*%(%s*Rank%s+%d+%s*%)", "") + if string.lower(baseName) == searchName or string.lower(auraName) == searchName then + found = true + break + end end end @@ -777,9 +781,13 @@ function CleveRoids.ValidateAura(unit, args, isbuff) if current_spellID then local auraName = SpellInfo(current_spellID) - if auraName and string.lower(auraName) == searchName then - found = true - break + if auraName then + -- Strip rank for comparison + local baseName = string.gsub(auraName, "%s*%(%s*Rank%s+%d+%s*%)", "") + if string.lower(baseName) == searchName or string.lower(auraName) == searchName then + found = true + break + end end end @@ -850,93 +858,97 @@ function CleveRoids.ValidateUnitDebuff(unit, args) return false end - -- For non-player units, use libdebuff with smart caster filtering - -- Personal debuffs (Rake, Rip, etc.) filter by "player" caster - -- Shared debuffs (Sunder, Thunder Clap, etc.) match any caster + -- For non-player units, check tracking table directly + -- SIMPLE: Did the player cast this spell? Is the timer still valid? if unit ~= "player" and CleveRoids.libdebuff then - -- Determine if we should filter by player caster - local filterCaster = nil + local _, guid = UnitExists(unit) + if not guid then return false end - -- Try to get spell ID to check if it's personal/shared - -- Search through libdebuff tables first (most reliable for debuffs) - local spellID = nil - if CleveRoids.libdebuff and (CleveRoids.libdebuff.personalDebuffs or CleveRoids.libdebuff.sharedDebuffs) then - -- Search personal debuffs by name - if CleveRoids.libdebuff.personalDebuffs then - for sid, _ in pairs(CleveRoids.libdebuff.personalDebuffs) do - local name = SpellInfo(sid) - if name then - -- Strip rank from name - name = string.gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") - if name == args.name then - spellID = sid - break - end + -- Normalize GUID to string for consistent table key lookups + guid = CleveRoids.NormalizeGUID(guid) + if not guid then return false end + + -- Find ALL spell IDs that match this name (all ranks) + local matchingSpellIDs = {} + if CleveRoids.libdebuff.personalDebuffs then + for sid, _ in pairs(CleveRoids.libdebuff.personalDebuffs) do + local name = SpellInfo(sid) + if name then + name = string.gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") + if name == args.name then + table.insert(matchingSpellIDs, sid) end end end - -- Search shared debuffs by name if not found - if not spellID and CleveRoids.libdebuff.sharedDebuffs then - for sid, _ in pairs(CleveRoids.libdebuff.sharedDebuffs) do - local name = SpellInfo(sid) - if name then - -- Strip rank from name - name = string.gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") - if name == args.name then - spellID = sid - break - end + end + if CleveRoids.libdebuff.sharedDebuffs then + for sid, _ in pairs(CleveRoids.libdebuff.sharedDebuffs) do + local name = SpellInfo(sid) + if name then + name = string.gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") + if name == args.name then + table.insert(matchingSpellIDs, sid) end end end end - -- Check if this is a personal debuff (should filter by caster) - if spellID and CleveRoids.libdebuff.IsPersonalDebuff then - if CleveRoids.libdebuff:IsPersonalDebuff(spellID) then - filterCaster = "player" - end - else - -- Unknown debuffs default to personal (safer for multi-player) - filterCaster = "player" - end + -- Check tracking table for ANY rank of this spell: Did player cast this? Is timer valid? + if table.getn(matchingSpellIDs) > 0 then + for _, spellID in ipairs(matchingSpellIDs) do + local rec = CleveRoids.libdebuff.objects[guid] and CleveRoids.libdebuff.objects[guid][spellID] + if rec and rec.caster == "player" and rec.duration and rec.start then + local timeRemaining = rec.duration + rec.start - GetTime() + if timeRemaining > 0 then + found = true + remaining = timeRemaining + stacks = rec.stacks or 0 - -- Search debuff slots - i = 1 - while true do - local name, rank, tex, stk, dtype, duration, timeleft, caster = - CleveRoids.libdebuff:UnitDebuff(unit, i, filterCaster) + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ff00[Tracking]|r %s (ID:%d): %.1fs left", args.name, spellID, timeRemaining) + ) + end - if not name then break end + -- Get texture (optional, just for display) + for i = 1, 16 do + local _, _, _, sid = UnitDebuff(unit, i) + if sid == spellID then + texture = UnitDebuff(unit, i) + break + end + end + if not texture then + for i = 1, 32 do + local _, _, sid = UnitBuff(unit, i) + if sid == spellID then + texture = UnitBuff(unit, i) + break + end + end + end - if name == args.name then - found = true - texture = tex - stacks = stk - remaining = timeleft - break - end - i = i + 1 - end - - -- If not found in debuffs, check buff slots (overflow debuffs) - if not found then - i = 1 - while true do - local name, rank, tex, stk, dtype, duration, timeleft, caster = - CleveRoids.libdebuff:UnitBuff(unit, i, filterCaster) - - if not name then break end - - if name == args.name then - found = true - texture = tex - stacks = stk - remaining = timeleft - break + -- Found active debuff, stop searching + break + elseif CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffff6600[Tracking]|r %s (ID:%d) expired %.1fs ago", + args.name, spellID, GetTime() - (rec.start + rec.duration)) + ) + end end - i = i + 1 end + + if not found and CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffff0000[Tracking]|r %s not in tracking table (checked %d ranks)", + args.name, table.getn(matchingSpellIDs)) + ) + end + elseif CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffff0000[Tracking]|r Unknown spell: %s", args.name) + ) end -- For player unit, use standard search (player only sees own debuffs on self) elseif unit == "player" then @@ -946,8 +958,15 @@ function CleveRoids.ValidateUnitDebuff(unit, args) texture, stacks, spellID, remaining = CleveRoids.GetPlayerAura(i, false) if not texture then break end - if (CleveRoids.hasSuperwow and args.name == SpellInfo(spellID)) - or (not CleveRoids.hasSuperwow and texture == CleveRoids.auraTextures[args.name]) then + if CleveRoids.hasSuperwow then + local fullName = SpellInfo(spellID) + -- Strip rank for comparison (handles "Moonfire (Rank 7)" vs "Moonfire") + local baseName = string.gsub(fullName or "", "%s*%(%s*Rank%s+%d+%s*%)", "") + if baseName == args.name or fullName == args.name then + found = true + break + end + elseif texture == CleveRoids.auraTextures[args.name] then found = true break end @@ -961,8 +980,15 @@ function CleveRoids.ValidateUnitDebuff(unit, args) texture, stacks, spellID, remaining = CleveRoids.GetPlayerAura(i, true) if not texture then break end - if (CleveRoids.hasSuperwow and args.name == SpellInfo(spellID)) - or (not CleveRoids.hasSuperwow and texture == CleveRoids.auraTextures[args.name]) then + if CleveRoids.hasSuperwow then + local fullName = SpellInfo(spellID) + -- Strip rank for comparison + local baseName = string.gsub(fullName or "", "%s*%(%s*Rank%s+%d+%s*%)", "") + if baseName == args.name or fullName == args.name then + found = true + break + end + elseif texture == CleveRoids.auraTextures[args.name] then found = true break end diff --git a/Utility.lua b/Utility.lua index 350e4a4..0d57833 100644 --- a/Utility.lua +++ b/Utility.lua @@ -17,6 +17,13 @@ local tonumber = tonumber local tostring = tostring local GetTime = GetTime +-- GUID normalization: ensure all GUIDs are strings for consistent table key lookups +-- In Lua, table["123"] is different from table[123], so we must normalize +function CleveRoids.NormalizeGUID(guid) + if not guid then return nil end + return tostring(guid) +end + if type(hooksecurefunc) ~= "function" then function hooksecurefunc(arg1, arg2, arg3) local tgt, fname, post @@ -808,6 +815,10 @@ end function lib:AddEffect(guid, unitName, spellID, duration, stacks, caster) if not guid or not spellID then return end + -- Normalize GUID to string for consistent table key lookups + guid = CleveRoids.NormalizeGUID(guid) + if not guid then return end + duration = duration or lib:GetDuration(spellID, caster) if duration <= 0 then return end @@ -828,8 +839,8 @@ function lib:AddEffect(guid, unitName, spellID, duration, stacks, caster) local spellName = SpellInfo(spellID) or "Unknown" local casterStr = caster or "nil" DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ffff[DEBUG AddEffect]|r %s (ID:%d) stored duration:%ds on %s, caster:%s", - spellName, spellID, duration, unitName or "Unknown", casterStr) + string.format("|cff00ffff[DEBUG AddEffect]|r %s (ID:%d) stored duration:%ds on %s, caster:%s, GUID:%s", + spellName, spellID, duration, unitName or "Unknown", casterStr, tostring(guid)) ) end end @@ -838,6 +849,9 @@ function lib:UnitDebuff(unit, id, filterCaster) local _, guid = UnitExists(unit) if not guid then return nil end + -- Normalize GUID to string for consistent table key lookups + guid = CleveRoids.NormalizeGUID(guid) + local texture, stacks, dtype, spellID = UnitDebuff(unit, id) if not texture or not spellID then @@ -1116,6 +1130,9 @@ ev:SetScript("OnEvent", function() local eventType = arg3 local spellID = arg4 + -- Normalize targetGUID to string for consistent table key lookups + targetGUID = CleveRoids.NormalizeGUID(targetGUID) + -- Capture combo points when cast STARTS (before they're consumed) if (eventType == "START" or eventType == "CHANNEL") and spellID then local _, playerGUID = UnitExists("player") @@ -1161,6 +1178,7 @@ ev:SetScript("OnEvent", function() local targetName = lib.guidToName[targetGUID] if not targetName then local _, currentTargetGUID = UnitExists("target") + currentTargetGUID = CleveRoids.NormalizeGUID(currentTargetGUID) if currentTargetGUID == targetGUID then targetName = UnitName("target") lib.guidToName[targetGUID] = targetName @@ -1518,11 +1536,81 @@ ev:SetScript("OnEvent", function() end end) +-- Track recently cast debuffs to validate they actually landed (not dodged/missed/resisted) +-- Format: [targetGUID][spellID] = { timestamp = GetTime(), validated = false } +lib.pendingDebuffs = lib.pendingDebuffs or {} + local evLearn = CreateFrame("Frame", "CleveRoidsLibDebuffLearnFrame", UIParent) evLearn:RegisterEvent("RAW_COMBATLOG") +evLearn:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") -- For miss/dodge/parry detection evLearn:SetScript("OnEvent", function() - if event == "RAW_COMBATLOG" then + -- Handle spell misses, dodges, parries, and resists + if event == "CHAT_MSG_SPELL_SELF_DAMAGE" then + local message = arg1 + if not message then return end + + -- Check for miss/dodge/parry/resist messages + local isMiss = find(message, "miss") or find(message, "MISS") + local isDodge = find(message, "dodge") or find(message, "dodged") + local isParry = find(message, "parry") or find(message, "parried") or find(message, "pariert") + local isResist = find(message, "resist") and not find(message, "resisted%)") -- Exclude partial resists + + if isMiss or isDodge or isParry or isResist then + -- Extract spell name and target from various message formats + local spellName, targetName + + -- "Your [Spell] was dodged by [Target]" + if not spellName then + _, _, spellName, targetName = find(message, "Your%s+(.-)%s+was%s+[a-z]+%s+by%s+(.-)%.") + end + -- "Your [Spell] missed [Target]" + if not spellName then + _, _, spellName, targetName = find(message, "Your%s+(.-)%s+missed%s+(.-)%.") + end + -- "[Target] dodges your [Spell]" + if not spellName then + _, _, targetName, spellName = find(message, "^(.-)%s+[a-z]+s%s+your%s+(.-)%.") + end + + if spellName then + -- Find the spell ID by name + local spellID = nil + if lib.personalDebuffs then + for sid, _ in pairs(lib.personalDebuffs) do + local name = SpellInfo(sid) + if name then + name = string.gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") + if lower(name) == lower(spellName) then + spellID = sid + break + end + end + end + end + + -- Remove from tracking if it exists (the cast failed) + if spellID then + -- Find the target GUID (check current target first) + local _, currentGUID = UnitExists("target") + if currentGUID and lib.objects[currentGUID] and lib.objects[currentGUID][spellID] then + -- Only remove if it was very recently added (within 0.5 seconds) + local rec = lib.objects[currentGUID][spellID] + if rec.start and (GetTime() - rec.start) < 0.5 then + lib.objects[currentGUID][spellID] = nil + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffff6600[Miss/Dodge/Parry]|r Removed %s (ID:%d) - spell didn't land", + spellName, spellID) + ) + end + end + end + end + end + end + + elseif event == "RAW_COMBATLOG" then local raw = arg2 -- PERFORMANCE: Quick length check before string search if not raw or string.len(raw) < 12 then return end -- "X fades from Y" minimum length @@ -2392,10 +2480,14 @@ local function OnSpellDamageEvent() if not spellId or not spellSchool then return end - -- PERFORMANCE: Skip if already learned (unless it might be a bleed upgrade) + -- PERFORMANCE: Skip if already learned and finalized local currentSchool = CleveRoids.spellSchoolMapping[spellId] - if currentSchool and currentSchool ~= "physical" then - return -- Already learned and not physical (can't be upgraded to bleed) + if currentSchool then + -- If already learned as anything other than physical, we're done (can't be upgraded) + if currentSchool ~= "physical" then + return + end + -- If learned as physical, continue processing - might upgrade to bleed end -- Convert school enum (0-6) to name @@ -2426,7 +2518,12 @@ local function OnSpellDamageEvent() end end - -- Only update if not already known or if this is more specific (e.g., bleed vs physical) + -- PERFORMANCE: Skip if the detected school matches what we already know + if currentSchool == schoolName then + return -- No change needed + end + + -- Only update if not already known or if upgrading from physical to bleed if not currentSchool or (currentSchool == "physical" and schoolName == "bleed") then CleveRoids.spellSchoolMapping[spellId] = schoolName