diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 7d097d6..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -wow-api-type-definitions/ -.gitignore diff --git a/Conditionals.lua b/Conditionals.lua index f8f1554..03b2598 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -337,6 +337,17 @@ function CleveRoids.ValidateComboPoints(operator, amount) return false end +function CleveRoids.ValidateLevel(unit, operator, amount) + if not unit or not operator or not amount then return false end + local level = UnitLevel(unit) + + if level and CleveRoids.operators[operator] then + return CleveRoids.comparators[operator](level, amount) + end + + return false +end + function CleveRoids.ValidateKnown(args) if not args then return false @@ -821,15 +832,38 @@ function CleveRoids.ValidatePlayerDebuff(args) end function CleveRoids.ValidateWeaponImbue(slot, imbueName) - -- Check if weapon has enchant + -- Check if weapon has enchant via API local hasMainEnchant, mainExpiration, mainCharges, hasOffEnchant, offExpiration, offCharges = GetWeaponEnchantInfo() + + local hasEnchant, expiration, charges + if slot == "mh" then + hasEnchant = hasMainEnchant + expiration = mainExpiration + charges = mainCharges + else + hasEnchant = hasOffEnchant + expiration = offExpiration + charges = offCharges + end + + -- Only consider temporary enchants (with time or charges) + -- This filters out permanent enchants like Crusader, Lifestealing, etc. + local hasTemporaryEnchant = hasEnchant and (expiration and expiration > 0 or charges and charges > 0) - local hasEnchant = (slot == "mh" and hasMainEnchant) or (slot == "oh" and hasOffEnchant) - if not hasEnchant then return false end - - -- If no specific imbue requested, just return enchant exists - if not imbueName or imbueName == "" then return true end + -- If no specific imbue requested, return temporary enchant status + if not imbueName or imbueName == "" then + return hasTemporaryEnchant + end + + -- If no temporary enchant, don't bother scanning + if not hasTemporaryEnchant then + return false + end + -- For specific imbue names, scan tooltip to match the name + -- BUT only check lines that have time markers (temporary enchants) + -- This prevents matching weapon stats like "Equip: ... critical strike ..." + -- Create tooltip scanner if needed if not CleveRoidsTooltip then CreateFrame("GameTooltip", "CleveRoidsTooltip", nil, "GameTooltipTemplate") @@ -840,22 +874,33 @@ function CleveRoids.ValidateWeaponImbue(slot, imbueName) CleveRoidsTooltip:ClearLines() CleveRoidsTooltip:SetInventoryItem("player", slot == "mh" and 16 or 17) - -- Look for green text (temporary enchant) + -- Normalize search term once + local searchTerm = string.lower(string.gsub(imbueName, "_", " ")) + + -- Look for green text with time markers - check ALL green lines with time for i = 1, CleveRoidsTooltip:NumLines() do local text = _G["CleveRoidsTooltipTextLeft"..i] if text then local line = text:GetText() - local r, g, b = text:GetTextColor() - -- Green text indicates temporary enchant - if line and g > 0.8 and r < 0.2 and b < 0.2 then - -- Normalize and compare - line = string.lower(line) - imbueName = string.lower(string.gsub(imbueName, "_", " ")) - return string.find(line, imbueName, 1, true) ~= nil + if line then + local r, g, b = text:GetTextColor() + -- Green text indicates enchant + if g > 0.8 and r < 0.2 and b < 0.2 then + local lowerLine = string.lower(line) + -- Only check lines with time markers (temporary enchants) + -- This skips permanent weapon stats like "Equip: ... critical strike ..." + if string.find(lowerLine, "%(") and (string.find(lowerLine, " min%)") or string.find(lowerLine, " sec%)") or string.find(lowerLine, " charge")) then + -- This is a temporary enchant line, check if it matches + if string.find(lowerLine, searchTerm, 1, true) then + return true -- Found it! + end + end + end end end end + -- Checked all lines, didn't find it return false end @@ -1554,6 +1599,20 @@ CleveRoids.Keywords = { end) end, + level = function(conditionals) + return And(conditionals.level, function(args) + if type(args) ~= "table" then return false end + return CleveRoids.ValidateLevel(conditionals.target, args.operator, args.amount) + end) + end, + + mylevel = function(conditionals) + return And(conditionals.mylevel, function(args) + if type(args) ~= "table" then return false end + return CleveRoids.ValidateLevel("player", args.operator, args.amount) + end) + end, + myhp = function(conditionals) return And(conditionals.myhp, function(args) if type(args) ~= "table" then return false end @@ -2020,18 +2079,114 @@ CleveRoids.Keywords = { end, mhimbue = function(conditionals) - return CleveRoids.ValidateWeaponImbue("mh", conditionals.value) + local imbueName = nil + + -- Case 1: conditionals.mhimbue is a string (e.g., [mhimbue]Frostbrand) + if type(conditionals.mhimbue) == "string" then + imbueName = conditionals.mhimbue + -- Case 2: conditionals.mhimbue is a table (e.g., [mhimbue:Frostbrand]) + elseif type(conditionals.mhimbue) == "table" and table.getn(conditionals.mhimbue) > 0 then + imbueName = conditionals.mhimbue[1] -- Use first value + -- Case 3: Boolean true means check for any imbue + elseif conditionals.mhimbue == true then + imbueName = nil -- Check for existence only + end + + return CleveRoids.ValidateWeaponImbue("mh", imbueName) end, nomhimbue = function(conditionals) - return not CleveRoids.ValidateWeaponImbue("mh", conditionals.value) + local imbueName = nil + + -- Case 1: conditionals.nomhimbue is a string + if type(conditionals.nomhimbue) == "string" then + imbueName = conditionals.nomhimbue + -- Case 2: conditionals.nomhimbue is a table + elseif type(conditionals.nomhimbue) == "table" and table.getn(conditionals.nomhimbue) > 0 then + imbueName = conditionals.nomhimbue[1] + -- Case 3: Boolean true + elseif conditionals.nomhimbue == true then + imbueName = nil + end + + return not CleveRoids.ValidateWeaponImbue("mh", imbueName) end, ohimbue = function(conditionals) - return CleveRoids.ValidateWeaponImbue("oh", conditionals.value) + local imbueName = nil + + -- Case 1: conditionals.ohimbue is a string + if type(conditionals.ohimbue) == "string" then + imbueName = conditionals.ohimbue + -- Case 2: conditionals.ohimbue is a table + elseif type(conditionals.ohimbue) == "table" and table.getn(conditionals.ohimbue) > 0 then + imbueName = conditionals.ohimbue[1] + -- Case 3: Boolean true + elseif conditionals.ohimbue == true then + imbueName = nil + end + + return CleveRoids.ValidateWeaponImbue("oh", imbueName) end, noohimbue = function(conditionals) - return not CleveRoids.ValidateWeaponImbue("oh", conditionals.value) + local imbueName = nil + + -- Case 1: conditionals.noohimbue is a string + if type(conditionals.noohimbue) == "string" then + imbueName = conditionals.noohimbue + -- Case 2: conditionals.noohimbue is a table + elseif type(conditionals.noohimbue) == "table" and table.getn(conditionals.noohimbue) > 0 then + imbueName = conditionals.noohimbue[1] + -- Case 3: Boolean true + elseif conditionals.noohimbue == true then + imbueName = nil + end + + return not CleveRoids.ValidateWeaponImbue("oh", imbueName) + end, + + immune = function(conditionals) + -- Check if target is immune to the spell being cast or damage school + -- Usage: [immune] SpellName OR [immune:SpellName] OR [immune:fire] + local checkValue = nil + + -- Case 1: [immune:SpellName] or [immune:fire] + if type(conditionals.immune) == "table" and table.getn(conditionals.immune) > 0 then + checkValue = conditionals.immune[1] + elseif type(conditionals.immune) == "string" then + checkValue = conditionals.immune + -- Case 2: [immune] SpellName (check the action being cast) + elseif conditionals.action then + checkValue = conditionals.action + end + + if not checkValue then + return false + end + + return CleveRoids.CheckImmunity(conditionals.target or "target", checkValue) + end, + + noimmune = function(conditionals) + -- Check if target is NOT immune to the spell being cast or damage school + -- Usage: [noimmune] SpellName OR [noimmune:SpellName] OR [noimmune:fire] + local checkValue = nil + + -- Case 1: [noimmune:SpellName] or [noimmune:fire] + if type(conditionals.noimmune) == "table" and table.getn(conditionals.noimmune) > 0 then + checkValue = conditionals.noimmune[1] + elseif type(conditionals.noimmune) == "string" then + checkValue = conditionals.noimmune + -- Case 2: [noimmune] SpellName (check the action being cast) + elseif conditionals.action then + checkValue = conditionals.action + end + + if not checkValue then + return true -- If we can't determine spell/school, assume not immune + end + + return not CleveRoids.CheckImmunity(conditionals.target or "target", checkValue) end } diff --git a/Core.lua b/Core.lua index 2d61106..110d35a 100644 --- a/Core.lua +++ b/Core.lua @@ -459,6 +459,7 @@ function CleveRoids.TestForActiveAction(actions) local hasActive = false local newActiveAction = nil local newSequence = nil + local firstUnconditional = nil if actions.tooltip and table.getn(actions.list) == 0 then if CleveRoids.TestAction(actions.cmd or "", actions.args or "") then @@ -467,9 +468,20 @@ function CleveRoids.TestForActiveAction(actions) actions.active = actions.tooltip end else + -- First pass: find first action with conditionals that passes for _, action in ipairs(actions.list) do + local result = CleveRoids.TestAction(action.cmd, action.args) + + -- Check if action has conditionals + local _, conditionals = CleveRoids.GetParsedMsg(action.args) + + if not conditionals and not firstUnconditional then + -- Track first unconditional action as fallback + firstUnconditional = action + end + -- break on first action that passes tests - if CleveRoids.TestAction(action.cmd, action.args) then + if result then hasActive = true if action.sequence then newSequence = action.sequence @@ -481,6 +493,12 @@ function CleveRoids.TestForActiveAction(actions) if hasActive then break end end end + + -- If no conditional action passed, use first unconditional action + if not hasActive and firstUnconditional then + hasActive = true + newActiveAction = firstUnconditional + end end local changed = false @@ -1021,6 +1039,7 @@ function CleveRoids.ParseMacro(name) -- #showtooltip and item/spell/macro specified, only use this tooltip if st and tt ~= "" then + showTooltipHasArg = true for _, arg in ipairs(CleveRoids.splitStringIgnoringQuotes(tt)) do macro.actions.tooltip = CleveRoids.CreateActionInfo(arg) local action = CleveRoids.CreateActionInfo(CleveRoids.GetParsedMsg(arg)) @@ -1052,6 +1071,14 @@ function CleveRoids.ParseMacro(name) end end + -- If #showtooltip was present but had no argument, use the first action as the tooltip + if hasShowTooltip and not showTooltipHasArg and table.getn(macro.actions.list) > 0 then + macro.actions.tooltip = macro.actions.list[1] + end + + -- Store whether #showtooltip had an explicit argument (for icon fallback logic) + macro.actions.explicitTooltip = showTooltipHasArg + CleveRoids.Macros[name] = macro return macro end @@ -1152,6 +1179,10 @@ function CleveRoids.ParseMsg(msg) noresting = true, isplayer = true, isnpc = true, + mhimbue = true, + nomhimbue = true, + ohimbue = true, + noohimbue = true, } if booleanConditionals[condition] then @@ -2385,28 +2416,47 @@ CleveRoids.Hooks.GetActionTexture = GetActionTexture function GetActionTexture(slot) local actions = CleveRoids.GetAction(slot) + -- Check if this is one of our macros if actions and (actions.active or actions.tooltip) then + + -- This block handles the case where all conditionals fail and no explicit + -- #showtooltip was set. It defaults to the macro's chosen icon (e.g., the red '?') + if not actions.active and not actions.explicitTooltip and actions.list and table.getn(actions.list) > 0 then + local macroName = GetActionText(slot) + if macroName then + local macroID = GetMacroIndexByName(macroName) + if macroID and macroID > 0 then + local _, macroTexture = GetMacroInfo(macroID) + if macroTexture then + return macroTexture + end + end + end + return CleveRoids.unknownTexture + end + -- Prioritize active action, fall back to tooltip local a = actions.active or actions.tooltip - - -- NEW: For slot-based actions, always fetch current equipment texture + + -- Handle numeric slot actions (e.g., /use 13) local slotId = tonumber(a.action) if slotId and slotId >= 1 and slotId <= 19 then local currentTexture = GetInventoryItemTexture("player", slotId) if currentTexture then return currentTexture end - -- No item equipped in that slot, return unknown texture return CleveRoids.unknownTexture end - - local proxySlot = (actions.active and actions.active.spell) and CleveRoids.GetProxyActionSlot(actions.active.spell.name) - if proxySlot and CleveRoids.Hooks.GetActionTexture(proxySlot) ~= actions.active.spell.texture then - return CleveRoids.Hooks.GetActionTexture(proxySlot) - else - return (actions.active and actions.active.texture) or (actions.tooltip and actions.tooltip.texture) or CleveRoids.unknownTexture - end + + -- *** THIS IS THE FIX *** + -- If an action is active, return its texture directly. + -- If no action is active, return the tooltip's texture. + -- This bypasses the 'proxySlot' check that required the spell to be on the action bar. + return (actions.active and actions.active.texture) or (actions.tooltip and actions.tooltip.texture) or CleveRoids.unknownTexture + end + + -- Not one of our macros, use the original function return CleveRoids.Hooks.GetActionTexture(slot) end @@ -3158,6 +3208,66 @@ SlashCmdList["CLEVEROID"] = function(msg) return end + -- listimmune (list immunity data) + if cmd == "listimmune" or cmd == "immunelist" then + CleveRoids.ListImmunities(val ~= "" and val or nil) + return + end + + -- clearimmune (clear immunity data) + if cmd == "clearimmune" then + CleveRoids.ClearImmunities(val ~= "" and val or nil) + return + end + + -- addimmune (manually add immunity) + if cmd == "addimmune" then + -- Parse: /cleveroid addimmune [buff] + -- Example: /cleveroid addimmune "Golemagg the Incinerator" fire + -- Example: /cleveroid addimmune Vaelastrasz fire "Burning Adrenaline" + local npcName, school, buffName = nil, nil, nil + + -- Try to extract quoted NPC name + local _, _, quotedNpc, rest = string.find(msg, '^addimmune%s+"([^"]+)"%s*(.*)$') + if quotedNpc then + npcName = quotedNpc + -- Parse school and optional buff from rest + local _, _, sch, buff = string.find(rest, "^(%S+)%s*(.*)$") + school = sch + if buff and buff ~= "" then + -- Check if buff is quoted + local _, _, quotedBuff = string.find(buff, '^"([^"]+)"$') + buffName = quotedBuff or buff + end + else + -- No quoted NPC, use simple parsing + npcName = val + school = val2 + end + + CleveRoids.AddImmunity(npcName, school, buffName) + return + end + + -- removeimmune (manually remove immunity) + if cmd == "removeimmune" then + -- Parse: /cleveroid removeimmune + local npcName, school = nil, nil + + -- Try to extract quoted NPC name + local _, _, quotedNpc, sch = string.find(msg, '^removeimmune%s+"([^"]+)"%s*(%S*)$') + if quotedNpc then + npcName = quotedNpc + school = sch + else + npcName = val + school = val2 + end + + CleveRoids.RemoveImmunity(npcName, school) + return + end + -- Unknown command fallback CleveRoids.Print("Usage:") DEFAULT_CHAT_FRAME:AddMessage("/cleveroid - Show current settings") @@ -3168,6 +3278,11 @@ SlashCmdList["CLEVEROID"] = function(msg) 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') end SLASH_CLEAREQUIPQUEUE1 = "/clearequipqueue" diff --git a/README.md b/README.md index 4ec847a..7540f2d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,10 @@ 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 debug [0|1]` - Toggle debug messages for spell duration learning +* `/cleveroid listimmune [school]` - List all or specific school immunities +* `/cleveroid addimmune "" [buff]` - Add manual immunity +* `/cleveroid removeimmune "" ` - Remove immunity +* `/cleveroid clearimmune [school]` - Clear data --- @@ -363,6 +366,7 @@ The system includes pre-configured durations for 329+ debuffs across all classes | behind | [behind] | | * | If the player is behind the target.| | insight | [insight] | | * | If the player is in line of sight of the target. | | meleerange | [meleerange] | | * | If the player is melee range of the target.| +| immune | [immune:fire]
[immune:Flame_Shock] | * | * | If the npc has immunities to a damage type or spell. check slash commands section for more information. | ### Unitids | Name (N=party/raid slot number) | diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index 6bbe5c8..b044bb0 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 +## SavedVariables: CleveRoidMacros, CleveRoids_LearnedDurations, CleveRoids_AuraTextures, CleveRoids_ImmunityData Localization.lua Init.lua Utility.lua diff --git a/Utility.lua b/Utility.lua index c6f35ee..98042ea 100644 --- a/Utility.lua +++ b/Utility.lua @@ -996,3 +996,400 @@ evCleanup:SetScript("OnEvent", function() end end end) + +-- ============================================================================ +-- IMMUNITY TRACKING SYSTEM +-- ============================================================================ + +-- Initialize SavedVariables for immunity tracking +CleveRoids_ImmunityData = CleveRoids_ImmunityData or {} + +-- Spell school constants +local IMMUNITY_SCHOOLS = { + physical = 1, + holy = 2, + fire = 3, + nature = 4, + frost = 5, + shadow = 6, + arcane = 7, +} + +-- Cache for spell school lookups +local spellSchoolCache = {} + +-- Get the damage school of a spell +local function GetSpellSchool(spellName) + if not spellName then return nil end + + -- Remove rank information for cache consistency + local baseName = string.gsub(spellName, "%s*%(.-%)%s*$", "") + + -- Check cache first + if spellSchoolCache[baseName] then + return spellSchoolCache[baseName] + end + + -- Try to find spell in player's spellbook + local spell = CleveRoids.GetSpell(baseName) + if not spell then + return nil + end + + -- Create tooltip if needed + if not CleveRoidsSchoolTooltip then + CreateFrame("GameTooltip", "CleveRoidsSchoolTooltip", nil, "GameTooltipTemplate") + CleveRoidsSchoolTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") + end + + CleveRoidsSchoolTooltip:ClearLines() + CleveRoidsSchoolTooltip:SetSpell(spell.spellSlot, spell.bookType) + + -- Scan tooltip for school keywords + local school = nil + for i = 1, CleveRoidsSchoolTooltip:NumLines() do + local line = _G["CleveRoidsSchoolTooltipTextLeft" .. i] + if line then + local text = string.lower(line:GetText() or "") + + if string.find(text, "fire") or string.find(text, "flame") then + school = "fire" + break + elseif string.find(text, "frost") or string.find(text, "ice") then + school = "frost" + break + elseif string.find(text, "nature") or string.find(text, "poison") then + school = "nature" + break + elseif string.find(text, "shadow") or string.find(text, "dark") then + school = "shadow" + break + elseif string.find(text, "arcane") then + school = "arcane" + break + elseif string.find(text, "holy") or string.find(text, "divine") then + school = "holy" + break + end + end + end + + -- Fallback: Use spell name patterns for common spells + if not school then + local lower = string.lower(baseName) + if string.find(lower, "fire") or string.find(lower, "flame") or string.find(lower, "immolat") or string.find(lower, "scorch") then + school = "fire" + elseif string.find(lower, "frost") or string.find(lower, "ice") or string.find(lower, "blizzard") then + school = "frost" + elseif string.find(lower, "nature") or string.find(lower, "poison") or string.find(lower, "sting") then + school = "nature" + elseif string.find(lower, "shadow") or string.find(lower, "curse") or string.find(lower, "corruption") then + school = "shadow" + elseif string.find(lower, "arcane") then + school = "arcane" + elseif string.find(lower, "holy") or string.find(lower, "smite") or string.find(lower, "exorcism") then + school = "holy" + else + school = "physical" + end + end + + -- Cache the result + spellSchoolCache[baseName] = school + return school +end + +-- Get current buffs on a unit +local function GetUnitBuffs(unit) + local buffs = {} + if not CleveRoids.hasSuperwow then return buffs end + + for i = 1, 32 do + local texture, stacks, spellID = UnitBuff(unit, i) + if not texture then break end + + if spellID then + local buffName = SpellInfo(spellID) + if buffName then + buffs[buffName] = true + end + end + end + + return buffs +end + +-- Record an immunity (permanent or buff-based) +local function RecordImmunity(npcName, spellName, conditionalBuff) + if not npcName or not spellName or npcName == "" then + return + end + + local school = GetSpellSchool(spellName) + if not school then + return + end + + -- Initialize school table + if not CleveRoids_ImmunityData[school] then + CleveRoids_ImmunityData[school] = {} + end + + -- Record immunity + if conditionalBuff then + -- Buff-based immunity + CleveRoids_ImmunityData[school][npcName] = { + buff = conditionalBuff + } + if CleveRoids.debug then + CleveRoids.Print("|cffff6600Immunity:|r " .. npcName .. " is immune to " .. school .. " when buffed with: " .. conditionalBuff) + end + else + -- Permanent immunity + if CleveRoids_ImmunityData[school][npcName] ~= true then + CleveRoids_ImmunityData[school][npcName] = true + if CleveRoids.debug then + CleveRoids.Print("|cffff6600Immunity:|r " .. npcName .. " is permanently immune to " .. school) + end + end + end +end + +-- Combat log parser for immunity detection +local function ParseImmunityCombatLog() + local message = arg1 -- Formatted message + local rawMessage = arg2 -- Raw message + + if not rawMessage then return end + + -- Pattern: "X's SpellName fails. Y is immune." + local spellName = nil + local targetName = nil + + -- Extract spell name from raw message + local _, _, extractedSpell = string.find(rawMessage, "'s%s+(.-)%s+fails%.") + if not extractedSpell then + _, _, extractedSpell = string.find(rawMessage, "Your%s+(.-)%s+fails%.") + end + + -- Extract target name from formatted message (more reliable) + if message then + local _, _, extractedTarget = string.find(message, "fails%.%s+(.-)%s+is immune") + if extractedTarget then + targetName = extractedTarget + end + end + + -- Fallback: Try to get target from current target + if not targetName and UnitExists("target") then + local _, targetGUID = UnitExists("target") + if targetGUID and rawMessage and string.find(rawMessage, targetGUID) then + targetName = UnitName("target") + end + end + + if extractedSpell and targetName then + -- Check if target has any buffs (for conditional immunity) + local buffs = nil + if UnitExists("target") and UnitName("target") == targetName then + buffs = GetUnitBuffs("target") + end + + -- If target has exactly one buff, assume it's causing the immunity + if buffs and next(buffs) then + local buffCount = 0 + local singleBuff = nil + for buff, _ in pairs(buffs) do + buffCount = buffCount + 1 + singleBuff = buff + if buffCount > 1 then + singleBuff = nil + break + end + end + + if singleBuff then + RecordImmunity(targetName, extractedSpell, singleBuff) + return + end + end + + -- No single buff detected, record as permanent immunity + RecordImmunity(targetName, extractedSpell, nil) + end +end + +-- Check if a unit is immune to a spell or damage school +-- Supports: CheckImmunity(unitId, "Flame Shock") or CheckImmunity(unitId, "fire") +function CleveRoids.CheckImmunity(unitId, spellOrSchool) + if not unitId or not UnitExists(unitId) then + return false + end + + -- Only works on NPCs + if UnitIsPlayer(unitId) then + return false + end + + local targetName = UnitName(unitId) + if not targetName or targetName == "" then + return false + end + + if not spellOrSchool or spellOrSchool == "" then + return false + end + + -- Check if input is a spell school name directly + local inputLower = string.lower(spellOrSchool) + local school = nil + + if IMMUNITY_SCHOOLS[inputLower] then + -- Input is a damage school name (fire, frost, nature, etc.) + school = inputLower + else + -- Input is a spell name, need to determine its school + school = GetSpellSchool(spellOrSchool) + if not school then + return false + end + end + + -- Check immunity data for this school + if not CleveRoids_ImmunityData[school] then + return false + end + + local immunityData = CleveRoids_ImmunityData[school][targetName] + + -- No immunity data for this NPC + if not immunityData then + return false + end + + -- Permanent immunity + if immunityData == true then + return true + end + + -- Buff-based immunity (check if NPC has the required buff) + if type(immunityData) == "table" and immunityData.buff then + local requiredBuff = immunityData.buff + + -- Check target's buffs + if CleveRoids.hasSuperwow then + for i = 1, 32 do + local texture, stacks, spellID = UnitBuff(unitId, i) + if not texture then break end + + if spellID then + local buffName = SpellInfo(spellID) + if buffName and buffName == requiredBuff then + return true + end + end + end + end + + -- Buff not found, not currently immune + return false + end + + return false +end + +-- Management functions for immunity data +function CleveRoids.ListImmunities(school) + if school then + school = string.lower(school) + if not CleveRoids_ImmunityData[school] then + CleveRoids.Print("No immunity data for school: " .. school) + return + end + + CleveRoids.Print("|cff00ff00" .. school .. " immunities:|r") + local count = 0 + for npc, data in pairs(CleveRoids_ImmunityData[school]) do + if data == true then + CleveRoids.Print(" - " .. npc .. " (permanent)") + elseif type(data) == "table" and data.buff then + CleveRoids.Print(" - " .. npc .. " (when buffed: " .. data.buff .. ")") + end + count = count + 1 + end + CleveRoids.Print("Total: " .. count) + else + -- List all schools + CleveRoids.Print("|cff00ff00Immunity Data by School:|r") + for schoolName, npcs in pairs(CleveRoids_ImmunityData) do + local count = 0 + for _ in pairs(npcs) do + count = count + 1 + end + if count > 0 then + CleveRoids.Print(" " .. schoolName .. ": " .. count .. " NPCs") + end + end + end +end + +function CleveRoids.ClearImmunities(school) + if school then + school = string.lower(school) + CleveRoids_ImmunityData[school] = {} + CleveRoids.Print("Cleared " .. school .. " immunity data") + else + CleveRoids_ImmunityData = {} + CleveRoids.Print("Cleared all immunity data") + end +end + +function CleveRoids.AddImmunity(npcName, school, buffName) + if not npcName or not school then + CleveRoids.Print("Usage: /cleveroid addimmune [buff name]") + CleveRoids.Print("Schools: fire, frost, nature, shadow, arcane, holy, physical") + return + end + + school = string.lower(school) + if not IMMUNITY_SCHOOLS[school] then + CleveRoids.Print("Invalid school. Use: fire, frost, nature, shadow, arcane, holy, physical") + return + end + + if not CleveRoids_ImmunityData[school] then + CleveRoids_ImmunityData[school] = {} + end + + if buffName and buffName ~= "" then + CleveRoids_ImmunityData[school][npcName] = { buff = buffName } + CleveRoids.Print("Added: " .. npcName .. " is immune to " .. school .. " when buffed with: " .. buffName) + else + CleveRoids_ImmunityData[school][npcName] = true + CleveRoids.Print("Added: " .. npcName .. " is permanently immune to " .. school) + end +end + +function CleveRoids.RemoveImmunity(npcName, school) + if not npcName or not school then + CleveRoids.Print("Usage: /cleveroid removeimmune ") + return + end + + school = string.lower(school) + if CleveRoids_ImmunityData[school] and CleveRoids_ImmunityData[school][npcName] then + CleveRoids_ImmunityData[school][npcName] = nil + CleveRoids.Print("Removed: " .. npcName .. " from " .. school .. " immunities") + else + CleveRoids.Print("Not found: " .. npcName .. " in " .. school .. " immunities") + end +end + +-- Register combat log event for immunity tracking +local immunityFrame = CreateFrame("Frame", "CleveRoidsImmunityFrame") +immunityFrame:RegisterEvent("RAW_COMBATLOG") +immunityFrame:SetScript("OnEvent", function() + if event == "RAW_COMBATLOG" then + ParseImmunityCombatLog() + end +end)