mirror of
https://github.com/brues-code/SuperCleveRoidMacros.git
synced 2026-09-16 03:38:00 +00:00
Add set conditional, rmarkorder, Reliquary DBC integration, and fix immunity tracking false positives
This commit is contained in:
+172
-25
@@ -1323,10 +1323,20 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
|
||||
entry.timestamp = now
|
||||
end
|
||||
|
||||
-- PERFORMANCE: Throttle cleanup to every 5 seconds instead of every aura event
|
||||
-- Previously this O(n) scan ran on EVERY aura application (10-50+/sec in raids)
|
||||
if (now - CleveRoids.AuraCapStatus.lastCleanupTime) < 5 then return end
|
||||
CleveRoids.AuraCapStatus.lastCleanupTime = now
|
||||
-- Cleanup moved to CleveRoids.CleanupAuraTracking(), called by UnitXP timer when available
|
||||
-- Fallback: inline throttled cleanup when UnitXP is not present
|
||||
if not CleveRoids.hasUnitXP then
|
||||
if (now - CleveRoids.AuraCapStatus.lastCleanupTime) >= 5 then
|
||||
CleveRoids.AuraCapStatus.lastCleanupTime = now
|
||||
CleveRoids.CleanupAuraTracking(now)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Periodic cleanup of AuraCapStatus and AllCasterAuraTracking tables
|
||||
--- Called by UnitXP timer (5s) or inline from OnAuraCastOther when UnitXP absent
|
||||
function CleveRoids.CleanupAuraTracking(now)
|
||||
if not now then now = GetTime() end
|
||||
|
||||
-- Cleanup old cap status entries (older than 60 seconds)
|
||||
local guid, data = next(CleveRoids.AuraCapStatus.targetCapStatus)
|
||||
@@ -1340,7 +1350,7 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
|
||||
end
|
||||
|
||||
-- Cleanup old aura tracking entries (3-level: targetGuid → spellName → casterGuid)
|
||||
-- PERFORMANCE: Skip cleanup if nothing was written since last cleanup
|
||||
-- Skip cleanup if nothing was written since last cleanup
|
||||
if not CleveRoids._allCasterAuraDirty then return end
|
||||
CleveRoids._allCasterAuraDirty = false
|
||||
local tGuid, spellNames = next(CleveRoids.AllCasterAuraTracking)
|
||||
@@ -1380,6 +1390,17 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
|
||||
end
|
||||
end
|
||||
|
||||
-- UnitXP threaded timer for aura tracking cleanup (5s interval)
|
||||
-- Replaces inline cleanup that ran on every aura event
|
||||
if CleveRoids.hasUnitXP then
|
||||
function CleveRoids_AuraTrackingCleanupTimer()
|
||||
if CleveRoids.isShuttingDown then return end
|
||||
CleveRoids.CleanupAuraTracking()
|
||||
end
|
||||
|
||||
CleveRoids._auraCleanupTimerId = UnitXP("timer", "arm", 5000, 5000, "CleveRoids_AuraTrackingCleanupTimer")
|
||||
end
|
||||
|
||||
-- Create frame to listen for Nampower auto-attack and aura events
|
||||
local autoAttackFrame = CreateFrame("Frame", "CleveRoidsAutoAttackFrame")
|
||||
|
||||
@@ -2659,8 +2680,9 @@ local function ScanSpellCastTime(spellName)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get spell cast time from tooltip (includes all modifiers, even overflow buffs)
|
||||
-- Get spell cast time (includes all modifiers when possible)
|
||||
-- Returns cast time in seconds, or nil if spell not found
|
||||
-- Priority: 1) Tooltip (accurate with haste/talents), 2) Nampower DBC (base cast time, works for all spells)
|
||||
function CleveRoids.GetSpellCastTime(spellName)
|
||||
if not spellName then return nil end
|
||||
|
||||
@@ -2677,9 +2699,20 @@ function CleveRoids.GetSpellCastTime(spellName)
|
||||
end
|
||||
end
|
||||
|
||||
-- Scan tooltip for cast time (guaranteed accurate)
|
||||
-- Try tooltip first (most accurate — reflects haste, talents, overflow buffs)
|
||||
local castTime = ScanSpellCastTime(spellName)
|
||||
|
||||
-- Fallback: Nampower DBC cast time (base value, no modifiers — better than nil)
|
||||
if castTime == nil and CleveRoids.NampowerAPI and CleveRoids.NampowerAPI.GetSpellCastTime then
|
||||
local spellId = nil
|
||||
if GetSpellIdForName then
|
||||
spellId = GetSpellIdForName(spellName)
|
||||
end
|
||||
if spellId then
|
||||
castTime = CleveRoids.NampowerAPI.GetSpellCastTime(spellId)
|
||||
end
|
||||
end
|
||||
|
||||
-- Cache the result (even nil to avoid repeated lookups)
|
||||
spellCastTimeCache[spellName] = castTime
|
||||
spellCastTimeCacheTime[spellName] = now
|
||||
@@ -5937,6 +5970,65 @@ CleveRoids.Keywords = {
|
||||
end)
|
||||
end,
|
||||
|
||||
-- [set:SetName>=N] — true if equipped piece count of named set meets comparison
|
||||
-- [set:SetName] — true if any pieces of that set are equipped (count > 0)
|
||||
-- [set:123>=3] — numeric IDs also supported (Nampower only, no Reliquary needed)
|
||||
-- Name lookup requires Reliquary (for DBC set name); ID lookup requires Nampower
|
||||
set = function(conditionals)
|
||||
return Multi(conditionals.set, function(args)
|
||||
if type(args) == "table" and args.operator and args.amount then
|
||||
-- Comparison mode: [set:Judgement_Battlegear>=3]
|
||||
local count = CleveRoids.GetEquippedSetPieceCount(args.name)
|
||||
if CleveRoids.operators[args.operator] then
|
||||
return CleveRoids.comparators[args.operator](count, args.amount)
|
||||
end
|
||||
return false
|
||||
elseif type(args) == "table" and args.comparisons then
|
||||
-- Multi-comparison: [set:Judgement_Battlegear>=3&<8]
|
||||
local count = CleveRoids.GetEquippedSetPieceCount(args.name)
|
||||
for _, comp in ipairs(args.comparisons) do
|
||||
if not CleveRoids.operators[comp.operator] then return false end
|
||||
if not CleveRoids.comparators[comp.operator](count, comp.amount) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
elseif type(args) == "string" then
|
||||
-- Existence mode: [set:Judgement_Battlegear] — any pieces equipped
|
||||
local count = CleveRoids.GetEquippedSetPieceCount(args)
|
||||
return count > 0
|
||||
end
|
||||
return false
|
||||
end, conditionals, "set")
|
||||
end,
|
||||
|
||||
-- [noset:SetName] — true if NO pieces of that set are equipped
|
||||
-- [noset:SetName>=3] — true if count does NOT meet comparison (negated)
|
||||
noset = function(conditionals)
|
||||
return NegatedMulti(conditionals.noset, function(args)
|
||||
if type(args) == "table" and args.operator and args.amount then
|
||||
local count = CleveRoids.GetEquippedSetPieceCount(args.name)
|
||||
if CleveRoids.operators[args.operator] then
|
||||
return not CleveRoids.comparators[args.operator](count, args.amount)
|
||||
end
|
||||
return true
|
||||
elseif type(args) == "table" and args.comparisons then
|
||||
local count = CleveRoids.GetEquippedSetPieceCount(args.name)
|
||||
for _, comp in ipairs(args.comparisons) do
|
||||
if not CleveRoids.operators[comp.operator] then return true end
|
||||
if not CleveRoids.comparators[comp.operator](count, comp.amount) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
elseif type(args) == "string" then
|
||||
local count = CleveRoids.GetEquippedSetPieceCount(args)
|
||||
return count == 0
|
||||
end
|
||||
return true
|
||||
end, conditionals, "noset")
|
||||
end,
|
||||
|
||||
-- [inbag:Item] — true if item exists in bags or equipped
|
||||
-- [inbag:Item<12] — true if bag count of Item is less than 12
|
||||
-- Supports multi-value: [inbag:Item1/Item2 inbag:Item3] = (Item1 OR Item2) AND Item3
|
||||
@@ -6589,15 +6681,16 @@ CleveRoids.Keywords = {
|
||||
-- Get GCD remaining in seconds
|
||||
local gcdRemaining = 0
|
||||
|
||||
-- Try Nampower API first (most accurate)
|
||||
-- Try Nampower API first (handles GetCastInfo + GetSpellIdCooldown)
|
||||
if CleveRoids.NampowerAPI and CleveRoids.NampowerAPI.GetGCDRemainingMs then
|
||||
local gcdMs = CleveRoids.NampowerAPI.GetGCDRemainingMs()
|
||||
if gcdMs and gcdMs > 0 then
|
||||
gcdRemaining = gcdMs / 1000
|
||||
end
|
||||
else
|
||||
-- Fallback: check the first spell in spellbook for GCD
|
||||
-- GCD shows as cooldown <= 1.5s on any GCD-triggering spell
|
||||
end
|
||||
|
||||
-- Fallback: scan spellbook for GCD cooldown (pre-v2.18 or if API returned 0)
|
||||
if gcdRemaining == 0 then
|
||||
for i = 1, 200 do
|
||||
local spellName = GetSpellName(i, BOOKTYPE_SPELL)
|
||||
if not spellName then break end
|
||||
@@ -6631,14 +6724,16 @@ CleveRoids.Keywords = {
|
||||
-- Get GCD remaining in seconds
|
||||
local gcdRemaining = 0
|
||||
|
||||
-- Try Nampower API first (most accurate)
|
||||
-- Try Nampower API first (handles GetCastInfo + GetSpellIdCooldown)
|
||||
if CleveRoids.NampowerAPI and CleveRoids.NampowerAPI.GetGCDRemainingMs then
|
||||
local gcdMs = CleveRoids.NampowerAPI.GetGCDRemainingMs()
|
||||
if gcdMs and gcdMs > 0 then
|
||||
gcdRemaining = gcdMs / 1000
|
||||
end
|
||||
else
|
||||
-- Fallback: check the first spell in spellbook for GCD
|
||||
end
|
||||
|
||||
-- Fallback: scan spellbook for GCD cooldown (pre-v2.18 or if API returned 0)
|
||||
if gcdRemaining == 0 then
|
||||
for i = 1, 200 do
|
||||
local spellName = GetSpellName(i, BOOKTYPE_SPELL)
|
||||
if not spellName then break end
|
||||
@@ -8751,6 +8846,8 @@ CleveRoids.MULTISCAN_PRIORITIES = {
|
||||
lowestrawhp = "lowestrawhp",
|
||||
-- Raid mark order (skull → cross → square → moon → triangle → diamond → circle → star)
|
||||
markorder = "markorder",
|
||||
-- Reverse mark order (star → circle → diamond → triangle → moon → square → cross → skull)
|
||||
rmarkorder = "rmarkorder",
|
||||
-- Individual raid marks by name (direct unit reference via SuperWoW "mark#" tokens)
|
||||
skull = 8, cross = 7, square = 6, moon = 5,
|
||||
triangle = 4, diamond = 3, circle = 2, star = 1,
|
||||
@@ -8767,6 +8864,7 @@ CleveRoids.STATIC_CONDITIONALS = {
|
||||
stealth = true, nostealth = true,
|
||||
form = true, noform = true, stance = true, nostance = true,
|
||||
equipped = true, noequipped = true,
|
||||
set = true, noset = true,
|
||||
inbag = true, noinbag = true,
|
||||
mod = true, nomod = true,
|
||||
keydown = true, nokeydown = true,
|
||||
@@ -8903,8 +9001,18 @@ function CleveRoids.ResolveMultiscanTarget(conditionals, specifiedUnit)
|
||||
if not priority then return nil end
|
||||
priority = string.lower(CleveRoids.Trim(priority))
|
||||
|
||||
-- Parse custom mark order: "markorder:876" → base="markorder", customOrder="876"
|
||||
-- Also supports "rmarkorder:876" for reverse base with custom order
|
||||
local customMarkOrder = nil
|
||||
local basePriority = priority
|
||||
local _, _, base, order = string.find(priority, "^(r?markorder):(%d+)$")
|
||||
if base then
|
||||
basePriority = base
|
||||
customMarkOrder = order
|
||||
end
|
||||
|
||||
-- Validate priority type
|
||||
local priorityType = CleveRoids.MULTISCAN_PRIORITIES[priority]
|
||||
local priorityType = CleveRoids.MULTISCAN_PRIORITIES[basePriority]
|
||||
if not priorityType then
|
||||
CleveRoids.Print("|cffff0000[multiscan]|r Unknown priority: " .. priority)
|
||||
return nil
|
||||
@@ -8977,23 +9085,53 @@ function CleveRoids.ResolveMultiscanTarget(conditionals, specifiedUnit)
|
||||
-- Handle raid mark priorities (direct unit reference)
|
||||
if type(priorityType) == "number" then
|
||||
local markUnit = "mark" .. priorityType
|
||||
if UnitExists(markUnit) and UnitCanAttack("player", markUnit) then
|
||||
-- Validate against target-dependent conditionals
|
||||
if CleveRoids.ValidateMultiscanCandidate(conditionals, markUnit) then
|
||||
if UnitExists(markUnit) then
|
||||
local isValid
|
||||
if friendly then
|
||||
isValid = UnitIsFriend("player", markUnit)
|
||||
else
|
||||
isValid = UnitCanAttack("player", markUnit)
|
||||
end
|
||||
if isValid and CleveRoids.ValidateMultiscanCandidate(conditionals, markUnit) then
|
||||
return CleveRoids.GetGUID(markUnit)
|
||||
end
|
||||
end
|
||||
return nil -- Raid mark not found or doesn't pass conditionals
|
||||
end
|
||||
|
||||
-- Handle markorder priority (skull → cross → square → moon → triangle → diamond → circle → star)
|
||||
if priorityType == "markorder" then
|
||||
-- Iterate marks from skull (8) down to star (1) in standard kill order
|
||||
for markIndex = 8, 1, -1 do
|
||||
-- Handle markorder / rmarkorder priority
|
||||
-- markorder: skull(8) → star(1) default, or custom order via "markorder:876"
|
||||
-- rmarkorder: star(1) → skull(8) default, or custom order via "rmarkorder:138"
|
||||
if priorityType == "markorder" or priorityType == "rmarkorder" then
|
||||
-- Build mark iteration order
|
||||
local markIndices
|
||||
if customMarkOrder then
|
||||
-- Custom order: each digit is a raid mark index (1=star, 8=skull)
|
||||
markIndices = {}
|
||||
for i = 1, string.len(customMarkOrder) do
|
||||
local digit = tonumber(string.sub(customMarkOrder, i, i))
|
||||
if digit and digit >= 1 and digit <= 8 then
|
||||
table.insert(markIndices, digit)
|
||||
end
|
||||
end
|
||||
elseif priorityType == "rmarkorder" then
|
||||
-- Reverse: star(1) → skull(8)
|
||||
markIndices = { 1, 2, 3, 4, 5, 6, 7, 8 }
|
||||
else
|
||||
-- Default: skull(8) → star(1)
|
||||
markIndices = { 8, 7, 6, 5, 4, 3, 2, 1 }
|
||||
end
|
||||
|
||||
for _, markIndex in ipairs(markIndices) do
|
||||
local markUnit = "mark" .. markIndex
|
||||
if UnitExists(markUnit) and UnitCanAttack("player", markUnit) then
|
||||
-- Validate against target-dependent conditionals
|
||||
if CleveRoids.ValidateMultiscanCandidate(conditionals, markUnit) then
|
||||
if UnitExists(markUnit) then
|
||||
local isValid
|
||||
if friendly then
|
||||
isValid = UnitIsFriend("player", markUnit)
|
||||
else
|
||||
isValid = UnitCanAttack("player", markUnit)
|
||||
end
|
||||
if isValid and CleveRoids.ValidateMultiscanCandidate(conditionals, markUnit) then
|
||||
return CleveRoids.GetGUID(markUnit)
|
||||
end
|
||||
end
|
||||
@@ -9103,6 +9241,15 @@ function CleveRoids.ResolveMultiscanTarget(conditionals, specifiedUnit)
|
||||
evaluateCandidate(specifiedUnit)
|
||||
end
|
||||
|
||||
-- Include all raid-marked enemies as candidates (mark1-mark8)
|
||||
-- These may be outside nextEnemyConsideringDistance range/cone
|
||||
for markIdx = 1, 8 do
|
||||
local markUnit = "mark" .. markIdx
|
||||
if UnitExists(markUnit) then
|
||||
evaluateCandidate(markUnit)
|
||||
end
|
||||
end
|
||||
|
||||
-- Scan via UnitXP enemy iteration
|
||||
local firstGuid = nil
|
||||
local maxIterations = 50
|
||||
|
||||
@@ -5037,7 +5037,8 @@ end
|
||||
-- Create a hidden tooltip frame to read buff names
|
||||
if not AuraScanTooltip and not CleveRoids.hasSuperwow then
|
||||
CreateFrame("GameTooltip", "AuraScanTooltip")
|
||||
AuraScanTooltip:SetOwner(WorldFrame, "ANCHORNONE")
|
||||
AuraScanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||||
AuraScanTooltip:EnableMouse(false)
|
||||
AuraScanTooltip:AddFontStrings(
|
||||
AuraScanTooltip:CreateFontString("$parentTextLeft1", nil, "GameTooltipText"),
|
||||
AuraScanTooltip:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
|
||||
@@ -5060,10 +5061,12 @@ if not CleveRoidsTooltipScan then
|
||||
end
|
||||
end
|
||||
CleveRoidsTooltipScan:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||||
CleveRoidsTooltipScan:EnableMouse(false)
|
||||
end
|
||||
|
||||
-- This single dummy frame handles events AND serves as our tooltip scanner.
|
||||
CleveRoids.Frame = CreateFrame("GameTooltip")
|
||||
CleveRoids.Frame:EnableMouse(false) -- Prevent tooltip from capturing mouse input
|
||||
|
||||
-- Create the extra font strings needed for other functions like GetSpellCost.
|
||||
CleveRoids.Frame.costFontString = CleveRoids.Frame:CreateFontString()
|
||||
@@ -5168,6 +5171,15 @@ function CleveRoids.Frame:PLAYER_LOGOUT()
|
||||
this:UnregisterAllEvents()
|
||||
this:SetScript("OnUpdate", nil)
|
||||
this:SetScript("OnEvent", nil)
|
||||
-- Disarm UnitXP timers (they run in a separate thread and survive /reload)
|
||||
if CleveRoids.hasUnitXP then
|
||||
if CleveRoids._cleanupTimerId then
|
||||
pcall(UnitXP, "timer", "disarm", CleveRoids._cleanupTimerId)
|
||||
end
|
||||
if CleveRoids._auraCleanupTimerId then
|
||||
pcall(UnitXP, "timer", "disarm", CleveRoids._auraCleanupTimerId)
|
||||
end
|
||||
end
|
||||
-- Clear spell caches to prevent stale data
|
||||
if CleveRoids.spellIdCache then
|
||||
for k in pairs(CleveRoids.spellIdCache) do
|
||||
|
||||
@@ -22,6 +22,7 @@ CleveRoids.mouseOverUnit = nil
|
||||
-- Environment flags
|
||||
CleveRoids.hasSuperwow = SetAutoloot and true or false
|
||||
CleveRoids.hasTurtle = (type(_G.TURTLE_WOW_VERSION) ~= "nil")
|
||||
CleveRoids.hasReliquary = (RQ_GetVersion ~= nil)
|
||||
CleveRoids.supported = CleveRoids.hasTurtle
|
||||
|
||||
CleveRoids.ParsedMsg = {}
|
||||
@@ -279,6 +280,14 @@ local function PrintFeatures()
|
||||
end
|
||||
end
|
||||
if CleveRoids.hasUnitXP then table.insert(features, "UnitXP") end
|
||||
if CleveRoids.hasReliquary then
|
||||
local ok, major, minor, patch = pcall(RQ_GetVersion)
|
||||
if ok and major then
|
||||
table.insert(features, string.format("Reliquary v%d.%d.%d", major, minor, patch))
|
||||
else
|
||||
table.insert(features, "Reliquary")
|
||||
end
|
||||
end
|
||||
if CleveRoids.hasTurtle then table.insert(features, "Turtle") end
|
||||
|
||||
if table.getn(features) > 0 then
|
||||
@@ -291,7 +300,8 @@ end
|
||||
-- v3: Fixed Master Strike false physical immunity recording (split CC spell handling)
|
||||
-- v4: Fixed false immunity recording when target dies with spells in-flight (dead = IMMUNE)
|
||||
-- v5: Fixed false physical immunity from unknown spell schools (now uses DBC lookup; unknown defaults to nil not "physical")
|
||||
CleveRoids.IMMUNITY_DATA_VERSION = 5
|
||||
-- v6: Reset stale immunity data that may contain false positives
|
||||
CleveRoids.IMMUNITY_DATA_VERSION = 6
|
||||
|
||||
-- Call on next frame to ensure everything is loaded
|
||||
local initFrame = CreateFrame("Frame")
|
||||
|
||||
+136
-1
@@ -1909,12 +1909,35 @@ function API.IsCasting()
|
||||
return info ~= nil
|
||||
end
|
||||
|
||||
-- Cached spell ID for GCD queries via GetSpellIdCooldown
|
||||
local _gcdProbeSpellId = nil
|
||||
|
||||
-- Get GCD remaining in milliseconds
|
||||
-- Uses GetSpellIdCooldown (works for instant casts) with GetCastInfo as fast-path
|
||||
function API.GetGCDRemainingMs()
|
||||
-- Fast path: GetCastInfo reports GCD during active casts/channels
|
||||
local info = API.GetCastInfo()
|
||||
if info and info.gcdRemainingMs then
|
||||
if info and info.gcdRemainingMs and info.gcdRemainingMs > 0 then
|
||||
return info.gcdRemainingMs
|
||||
end
|
||||
|
||||
-- Primary path: GetSpellIdCooldown reads GCD from spell history (works after instant casts)
|
||||
if API.features.hasGetSpellIdCooldown and _G.GetSpellIdCooldown then
|
||||
-- Get a probe spell ID: first spell in the spellbook
|
||||
if not _gcdProbeSpellId then
|
||||
local name = GetSpellName(1, BOOKTYPE_SPELL)
|
||||
if name and _G.GetSpellIdForName then
|
||||
_gcdProbeSpellId = _G.GetSpellIdForName(name)
|
||||
end
|
||||
end
|
||||
if _gcdProbeSpellId then
|
||||
local cd = _G.GetSpellIdCooldown(_gcdProbeSpellId)
|
||||
if cd and cd.gcdCategoryRemainingMs and cd.gcdCategoryRemainingMs > 0 then
|
||||
return cd.gcdCategoryRemainingMs
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
@@ -3527,5 +3550,117 @@ function API.GetUnitMaxPower(unitToken, powerType)
|
||||
return UnitManaMax(unitToken)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- RELIQUARY DBC FUNCTIONS (optional DLL)
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Safe Reliquary call wrapper — returns nil on missing DLL or lookup failure
|
||||
local function RQ_SafeCall(func, ...)
|
||||
if not func then return nil end
|
||||
local ok, result = pcall(func, unpack(arg))
|
||||
if ok then return result end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get item set data from DBC by set ID
|
||||
-- Returns: { name, itemId_1..17, setSpellId_1..8, setThreshold_1..8 } or nil
|
||||
function API.GetItemSet(setId)
|
||||
if not setId or not _G.RQ_GetItemSet then return nil end
|
||||
return RQ_SafeCall(_G.RQ_GetItemSet, setId)
|
||||
end
|
||||
|
||||
-- Get the set ID for an item (via Nampower's GetItemStatsField)
|
||||
-- Returns: setId (number) or nil if item has no set
|
||||
function API.GetItemSetId(itemId)
|
||||
if not itemId then return nil end
|
||||
local setId = API.GetItemField(itemId, "itemSet")
|
||||
if setId and setId ~= 0 then return setId end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get all item IDs belonging to a set (from Reliquary DBC)
|
||||
-- Returns: { itemId1, itemId2, ... } or nil
|
||||
function API.GetItemSetItems(setId)
|
||||
local setData = API.GetItemSet(setId)
|
||||
if not setData then return nil end
|
||||
|
||||
local items = {}
|
||||
for i = 1, 17 do
|
||||
local id = setData["itemId_" .. i]
|
||||
if id then
|
||||
id = tonumber(id)
|
||||
if id and id ~= 0 then
|
||||
table.insert(items, id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if table.getn(items) > 0 then return items end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get set bonus spell/threshold pairs from DBC
|
||||
-- Returns: { { spellId = N, threshold = N }, ... } or nil
|
||||
function API.GetItemSetBonuses(setId)
|
||||
local setData = API.GetItemSet(setId)
|
||||
if not setData then return nil end
|
||||
|
||||
local bonuses = {}
|
||||
for i = 1, 8 do
|
||||
local spellId = setData["setSpellId_" .. i]
|
||||
local threshold = setData["setThreshold_" .. i]
|
||||
if spellId and threshold then
|
||||
spellId = tonumber(spellId)
|
||||
threshold = tonumber(threshold)
|
||||
if spellId and spellId ~= 0 and threshold and threshold > 0 then
|
||||
table.insert(bonuses, { spellId = spellId, threshold = threshold })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if table.getn(bonuses) > 0 then return bonuses end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get spell effect radius in yards from DBC
|
||||
-- spellId: the spell to look up
|
||||
-- effectIndex: 1, 2, or 3 (which effect slot, default 1)
|
||||
-- Returns: radius (number) or nil
|
||||
function API.GetSpellEffectRadius(spellId, effectIndex)
|
||||
effectIndex = effectIndex or 1
|
||||
if not spellId then return nil end
|
||||
|
||||
-- Get the radius index from the spell's effect via Nampower
|
||||
local radiusField = "effectRadiusIndex"
|
||||
local rec = API.GetSpellRecord(spellId)
|
||||
if not rec then return nil end
|
||||
|
||||
-- effectRadiusIndex is an array field — access by effect index
|
||||
local radiusIndex = nil
|
||||
if rec.effectRadiusIndex then
|
||||
if type(rec.effectRadiusIndex) == "table" then
|
||||
radiusIndex = rec.effectRadiusIndex[effectIndex]
|
||||
else
|
||||
-- Single value (effect 1 only)
|
||||
if effectIndex == 1 then
|
||||
radiusIndex = rec.effectRadiusIndex
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not radiusIndex or radiusIndex == 0 then return nil end
|
||||
|
||||
-- Try Reliquary for the SpellRadius DBC lookup
|
||||
if _G.RQ_GetSpellRadius then
|
||||
local radiusData = RQ_SafeCall(_G.RQ_GetSpellRadius, radiusIndex)
|
||||
if radiusData and radiusData.radius then
|
||||
local radius = tonumber(radiusData.radius)
|
||||
if radius and radius > 0 then return radius end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Expose API globally for other addons
|
||||
_G.CleveRoidsNampowerAPI = API
|
||||
|
||||
+495
-215
@@ -175,6 +175,7 @@ end
|
||||
|
||||
-- Scan spell tooltip for channel/cast duration
|
||||
-- Returns duration in seconds, or nil if not found
|
||||
-- Tries Nampower GetSpellDuration (DBC, v2.38+) first, falls back to tooltip scanning
|
||||
function CleveRoids.GetSpellDurationFromTooltip(spellName)
|
||||
if not spellName then return nil end
|
||||
|
||||
@@ -186,6 +187,26 @@ function CleveRoids.GetSpellDurationFromTooltip(spellName)
|
||||
end
|
||||
end
|
||||
|
||||
-- Try Nampower DBC lookup first (v2.38+ GetSpellDuration) — faster and works for all spells
|
||||
if CleveRoids.NampowerAPI and CleveRoids.NampowerAPI.features
|
||||
and CleveRoids.NampowerAPI.features.hasGetSpellDuration then
|
||||
-- Resolve name to ID
|
||||
local spellID = nil
|
||||
if GetSpellIdForName then
|
||||
spellID = GetSpellIdForName(spellName)
|
||||
end
|
||||
if spellID then
|
||||
local durationMs = CleveRoids.NampowerAPI.GetSpellDuration(spellID)
|
||||
if durationMs and durationMs > 0 then
|
||||
local duration = durationMs / 1000
|
||||
cachedSpellDurations[spellName] = duration
|
||||
spellDurationCacheTime[spellName] = now
|
||||
return duration
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Fallback: tooltip scanning (works for player spellbook spells)
|
||||
local slot, bookType = GetSpellSlotByName(spellName)
|
||||
if not slot then return nil end
|
||||
|
||||
@@ -211,9 +232,22 @@ function CleveRoids.GetSpellDurationFromTooltip(spellName)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get channel duration for a spell by ID (looks up name first)
|
||||
-- Get channel duration for a spell by ID
|
||||
-- Tries Nampower GetSpellDuration (DBC) first, falls back to tooltip via name
|
||||
function CleveRoids.GetChannelDurationFromTooltipByID(spellID)
|
||||
if not spellID or not GetSpellRecField then return nil end
|
||||
if not spellID then return nil end
|
||||
|
||||
-- Try Nampower DBC lookup first (v2.38+)
|
||||
if CleveRoids.NampowerAPI and CleveRoids.NampowerAPI.features
|
||||
and CleveRoids.NampowerAPI.features.hasGetSpellDuration then
|
||||
local durationMs = CleveRoids.NampowerAPI.GetSpellDuration(spellID)
|
||||
if durationMs and durationMs > 0 then
|
||||
return durationMs / 1000
|
||||
end
|
||||
end
|
||||
|
||||
-- Fallback: resolve name and scan tooltip
|
||||
if not GetSpellRecField then return nil end
|
||||
local spellName = GetSpellRecField(spellID, "name")
|
||||
if not spellName then return nil end
|
||||
return CleveRoids.GetSpellDurationFromTooltip(spellName)
|
||||
@@ -3033,6 +3067,93 @@ local _pendingPersonalBuffer = {}
|
||||
local _pendingCCBuffer = {}
|
||||
local _pendingSharedBuffer = {}
|
||||
|
||||
-- Spells with INVULNERABILITY mechanic (mechanic 25) from BuffLib SpellData DBC
|
||||
-- These grant temporary immunity and should not trigger permanent immunity recording
|
||||
-- Source: BuffLib/SpellData.lua - extracted from DBC files
|
||||
local INVULNERABILITY_SPELL_IDS = {
|
||||
-- Paladin
|
||||
[498] = true, -- Divine Protection (Rank 1)
|
||||
[642] = true, -- Divine Shield (Rank 1)
|
||||
[1020] = true, -- Divine Shield (Rank 2)
|
||||
[1022] = true, -- Blessing of Protection (Rank 1)
|
||||
[5573] = true, -- Divine Protection (Rank 2)
|
||||
[5599] = true, -- Blessing of Protection (Rank 2)
|
||||
[10278] = true, -- Blessing of Protection (Rank 3)
|
||||
[25771] = true, -- Forbearance (debuff after immunity)
|
||||
-- Old/Unused Paladin
|
||||
[1052] = true, -- zzOLDBlessing of Righteousness
|
||||
[5601] = true, -- zzOLDBlessing of Righteousness
|
||||
[5602] = true, -- zzOLDBlessing of Righteousness
|
||||
[10280] = true, -- zzOLDBlessing of Righteousness
|
||||
[10281] = true, -- zzOLDBlessing of Righteousness
|
||||
-- Rogue
|
||||
[6770] = true, -- Sap (invuln during effect)
|
||||
-- NPC/Misc
|
||||
[7992] = true, -- Slowing Poison
|
||||
[11638] = true, -- Radiation Poisoning
|
||||
[14897] = true, -- Slowing Poison
|
||||
[16603] = true, -- Demonfork
|
||||
[16791] = true, -- Furious Anger
|
||||
[17407] = true, -- Wound
|
||||
[18208] = true, -- Poison
|
||||
[23230] = true, -- Blood Fury
|
||||
[24005] = true, -- Food
|
||||
[24707] = true, -- Food
|
||||
[24865] = true, -- Sanctified Orb
|
||||
[26263] = true, -- Dim Sum
|
||||
[28522] = true, -- Icebolt
|
||||
[29055] = true, -- Refreshing Red Apple
|
||||
[29325] = true, -- Acid Volley
|
||||
[29330] = true, -- Sapphiron's Wing Buffet Despawn
|
||||
-- Spell Reflection (no DBC mechanic, but grants immunity to reflected schools)
|
||||
[9941] = true, -- Spell Reflection
|
||||
[9943] = true, -- Spell Reflection
|
||||
[10074] = true, -- Spell Reflection
|
||||
[11818] = true, -- Spell Reflection
|
||||
[21118] = true, -- Spell Reflection
|
||||
-- School-specific Reflectors (Engineering items)
|
||||
[23097] = true, -- Fire Reflector
|
||||
[23131] = true, -- Frost Reflector
|
||||
[23132] = true, -- Shadow Reflector
|
||||
[23178] = true, -- Nature Reflector
|
||||
[23216] = true, -- Arcane Reflector
|
||||
-- Multi-school Reflect (NPC abilities)
|
||||
[13022] = true, -- Fire and Arcane Reflect
|
||||
[19595] = true, -- Shadow and Frost Reflect
|
||||
-- Generic Reflection buffs
|
||||
[3651] = true, -- Shield of Reflection
|
||||
[9906] = true, -- Reflection
|
||||
[10831] = true, -- Reflection Field
|
||||
[17106] = true, -- Reflection
|
||||
[17107] = true, -- Reflection
|
||||
[17108] = true, -- Reflection
|
||||
[20223] = true, -- Magic Reflection
|
||||
[20619] = true, -- Magic Reflection
|
||||
[22067] = true, -- Reflection
|
||||
[23920] = true, -- Shield Reflection
|
||||
[23921] = true, -- Shield Reflection
|
||||
[27564] = true, -- Reflection
|
||||
}
|
||||
|
||||
-- Check if a unit has any immunity-granting buff active
|
||||
-- Uses only spell IDs from DBC mechanic 25 (INVULNERABILITY) for reliability
|
||||
-- Returns the buff name if found, nil otherwise
|
||||
local function HasImmunityGrantingBuff(unit)
|
||||
if not UnitExists(unit) then return nil end
|
||||
|
||||
for i = 1, 32 do
|
||||
local texture, stacks, spellID = UnitBuff(unit, i)
|
||||
if not texture then break end
|
||||
|
||||
if spellID and INVULNERABILITY_SPELL_IDS[spellID] then
|
||||
local buffName = GetSpellRecField(spellID, "name") or ("SpellID:" .. spellID)
|
||||
return buffName
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- PERFORMANCE: Throttling - run at 20Hz max instead of every frame (60+Hz)
|
||||
-- Minimum delay is 0.2s, so 20Hz (50ms) gives us 4 checks per minimum delay
|
||||
local _lastDelayedTrackingUpdate = 0
|
||||
@@ -3279,22 +3400,16 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
|
||||
-- Note: We set bleedVerified = false but DON'T record immunity
|
||||
-- This is intentional - one-shots are inconclusive
|
||||
else
|
||||
-- Target lived long enough but no "afflicted by" = immunity
|
||||
-- Target died without "afflicted by" — inconclusive, don't record immunity.
|
||||
-- "afflicted by" combat log messages can be delayed/throttled, so absence
|
||||
-- is not proof of immunity. SPELL_MISS_SELF handles true immunity detection.
|
||||
bleedVerified = false
|
||||
if pending.targetName and pending.targetName ~= "" then
|
||||
if debug then
|
||||
local spellNameForImmunity = _GetSpellRecField(pending.spellID, "name") or "Bleed"
|
||||
|
||||
if not CleveRoids_ImmunityData["bleed"] then
|
||||
CleveRoids_ImmunityData["bleed"] = {}
|
||||
end
|
||||
CleveRoids_ImmunityData["bleed"][pending.targetName] = true
|
||||
|
||||
if debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
_string_format("|cffff6600[Bleed Immunity]|r %s is immune to bleed (%s) - target died without 'afflicted by' message (%.2fs)",
|
||||
pending.targetName, spellNameForImmunity, timeSinceCast)
|
||||
)
|
||||
end
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
_string_format("|cffaaaaaa[Bleed Inconclusive]|r %s on %s - target died without 'afflicted by' (%.2fs), not recording immunity",
|
||||
spellNameForImmunity, pending.targetName or "Unknown", timeSinceCast)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3332,7 +3447,25 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
|
||||
-- If bleed is missing and target has few debuffs, it's likely immunity (not debuff cap)
|
||||
if not bleedVerified then
|
||||
local DEBUFF_CAP_THRESHOLD = 47 -- Max is 48 (16 visible + 32 overflow)
|
||||
if totalDebuffs < DEBUFF_CAP_THRESHOLD then
|
||||
-- Guard: don't record immunity for player targets (PvP resists, trinkets, etc.)
|
||||
local isPlayer = UnitIsPlayer(verifyUnit)
|
||||
-- Guard: don't record if target has temporary immunity buff
|
||||
local immunityBuff = not isPlayer and HasImmunityGrantingBuff(verifyUnit)
|
||||
if isPlayer then
|
||||
if debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
_string_format("|cffaaaaaa[Bleed Skip]|r Player target %s - not recording immunity",
|
||||
pending.targetName or "Unknown")
|
||||
)
|
||||
end
|
||||
elseif immunityBuff then
|
||||
if debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
_string_format("|cffaaaaaa[Bleed Skip]|r %s has %s - not recording immunity",
|
||||
pending.targetName or "Unknown", immunityBuff)
|
||||
)
|
||||
end
|
||||
elseif totalDebuffs < DEBUFF_CAP_THRESHOLD then
|
||||
-- Few debuffs = likely bleed immunity, not debuff cap
|
||||
if pending.targetName and pending.targetName ~= "" then
|
||||
-- Record as BLEED immunity directly (bypass split damage override in RecordImmunity)
|
||||
@@ -3554,6 +3687,23 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
|
||||
|
||||
-- If CC didn't land, check if it's immunity or debuff cap
|
||||
if not ccVerified and ccVerifyUnit and not _IsUnitDead(ccVerifyUnit) then
|
||||
-- Guard: don't record immunity for player targets (PvP trinkets, resists, etc.)
|
||||
if UnitIsPlayer(ccVerifyUnit) then
|
||||
if debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
_string_format("|cffaaaaaa[CC Skip]|r Player target %s - not recording %s immunity",
|
||||
pending.targetName or "Unknown", pending.ccType or "CC")
|
||||
)
|
||||
end
|
||||
-- Guard: don't record if target has temporary immunity buff
|
||||
elseif HasImmunityGrantingBuff(ccVerifyUnit) then
|
||||
if debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
_string_format("|cffaaaaaa[CC Skip]|r %s has immunity buff - not recording %s immunity",
|
||||
pending.targetName or "Unknown", pending.ccType or "CC")
|
||||
)
|
||||
end
|
||||
else
|
||||
-- totalDebuffs already counted above, reuse it
|
||||
if totalDebuffs == 0 then
|
||||
-- Count wasn't done (dead target check skipped counting), do it now
|
||||
@@ -3647,6 +3797,7 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
|
||||
)
|
||||
end
|
||||
end
|
||||
end -- end of player/immunity-buff guard else block
|
||||
else
|
||||
-- CC landed successfully - remove any existing immunity record for this NPC/CC type
|
||||
local resolvedTargetName = pending.targetName
|
||||
@@ -3795,11 +3946,30 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
|
||||
-- Debuff didn't land - check if it's immunity or debuff cap
|
||||
local DEBUFF_CAP_THRESHOLD = 47 -- Max is 48 (16 visible + 32 overflow)
|
||||
|
||||
-- Guard: don't record immunity for player targets
|
||||
local sharedQueryUnit = pending.targetGUID and ResolveGUIDUnit(pending.targetGUID) or nil
|
||||
local isPlayer = sharedQueryUnit and UnitIsPlayer(sharedQueryUnit)
|
||||
local sharedImmunityBuff = sharedQueryUnit and not isPlayer and HasImmunityGrantingBuff(sharedQueryUnit)
|
||||
|
||||
-- SPLIT CC SPELLS: Skip immunity recording for spells with physical damage + resistable CC
|
||||
-- (e.g., Master Strike) - the physical damage lands but CC can be resisted independently
|
||||
-- spellGoHit: SPELL_GO confirmed hit, so debuff was resisted independently (not true immunity)
|
||||
local isSplitCCSpell = SPLIT_CC_SPELLS[pending.spellID] or SPLIT_CC_SPELLS[pending.castSpellID] or pending.spellGoHit
|
||||
if isSplitCCSpell then
|
||||
if isPlayer then
|
||||
if debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
_string_format("|cffaaaaaa[Shared Skip]|r Player target %s - not recording immunity",
|
||||
pending.targetName or "Unknown")
|
||||
)
|
||||
end
|
||||
elseif sharedImmunityBuff then
|
||||
if debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
_string_format("|cffaaaaaa[Shared Skip]|r %s has immunity buff - not recording immunity",
|
||||
pending.targetName or "Unknown")
|
||||
)
|
||||
end
|
||||
elseif isSplitCCSpell then
|
||||
if debug then
|
||||
local spellNameDebug = _GetSpellRecField(pending.castSpellID or pending.spellID, "name") or "Unknown"
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
@@ -3809,19 +3979,31 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
|
||||
end
|
||||
-- Skip immunity recording - damage landed, only CC was resisted
|
||||
elseif totalDebuffs < DEBUFF_CAP_THRESHOLD then
|
||||
-- Few debuffs = likely immunity
|
||||
if pending.targetName and pending.targetName ~= "" and pending.school then
|
||||
-- Record immunity for this spell's school
|
||||
if not CleveRoids_ImmunityData[pending.school] then
|
||||
CleveRoids_ImmunityData[pending.school] = {}
|
||||
-- Few debuffs = likely immunity — use DBC school if available (more reliable than name-based)
|
||||
local recordSchool = pending.school
|
||||
if pending.spellID and _GetSpellRecField then
|
||||
local dbcSchool = _GetSpellRecField(pending.spellID, "school")
|
||||
-- Inline school mapping (SCHOOL_NAMES local is defined later in file, not visible here)
|
||||
local dbcSchoolName = dbcSchool == 0 and "physical" or dbcSchool == 1 and "holy"
|
||||
or dbcSchool == 2 and "fire" or dbcSchool == 3 and "nature"
|
||||
or dbcSchool == 4 and "frost" or dbcSchool == 5 and "shadow"
|
||||
or dbcSchool == 6 and "arcane" or nil
|
||||
if dbcSchoolName then
|
||||
recordSchool = dbcSchoolName
|
||||
end
|
||||
CleveRoids_ImmunityData[pending.school][pending.targetName] = true
|
||||
end
|
||||
if pending.targetName and pending.targetName ~= "" and recordSchool then
|
||||
-- Record immunity for this spell's school
|
||||
if not CleveRoids_ImmunityData[recordSchool] then
|
||||
CleveRoids_ImmunityData[recordSchool] = {}
|
||||
end
|
||||
CleveRoids_ImmunityData[recordSchool][pending.targetName] = true
|
||||
|
||||
if debug then
|
||||
local spellNameDebug = _GetSpellRecField(pending.spellID, "name") or "Unknown"
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
_string_format("|cffff6600[Shared Immunity]|r %s is immune to %s (%s) - only %d debuffs on target",
|
||||
pending.targetName, pending.school, spellNameDebug, totalDebuffs)
|
||||
pending.targetName, recordSchool, spellNameDebug, totalDebuffs)
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -3857,10 +4039,29 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
|
||||
end
|
||||
end
|
||||
|
||||
-- Periodically clean up stale tracking data (immune/reflect/evade correlation tables)
|
||||
lib:CleanupStaleTrackingData()
|
||||
-- CleanupStaleTrackingData moved to UnitXP timer (30s interval) when available,
|
||||
-- falls back to inline call here when UnitXP is not present
|
||||
if not CleveRoids.hasUnitXP then
|
||||
lib:CleanupStaleTrackingData()
|
||||
end
|
||||
end)
|
||||
|
||||
-- UnitXP threaded timer for periodic cleanup (runs in separate thread, fires only when due)
|
||||
-- Replaces piggyback on 20Hz OnUpdate — avoids function call overhead every 50ms
|
||||
if CleveRoids.hasUnitXP then
|
||||
-- Global callback (UnitXP timers require global function name as string)
|
||||
function CleveRoids_LibDebuffCleanupTimer()
|
||||
if CleveRoids.isShuttingDown then return end
|
||||
local libRef = CleveRoids.libdebuff
|
||||
if libRef and libRef.CleanupStaleTrackingData then
|
||||
libRef:CleanupStaleTrackingData()
|
||||
end
|
||||
end
|
||||
|
||||
-- Arm timer: 30s initial delay, 30s repeat interval
|
||||
CleveRoids._cleanupTimerId = UnitXP("timer", "arm", 30000, 30000, "CleveRoids_LibDebuffCleanupTimer")
|
||||
end
|
||||
|
||||
local ev = CreateFrame("Frame", "CleveRoidsLibDebuffFrame", UIParent)
|
||||
ev:RegisterEvent("PLAYER_TARGET_CHANGED")
|
||||
ev:RegisterEvent("UNIT_AURA")
|
||||
@@ -6673,9 +6874,38 @@ end
|
||||
|
||||
-- Database of set bonus modifiers for debuff durations
|
||||
-- Structure: [spellID] = { items = {itemID1, itemID2, ...}, threshold = X, modifier = function(baseDuration) }
|
||||
-- When Reliquary is available, items can be omitted and setId used instead (auto-resolves from DBC)
|
||||
CleveRoids.setbonusModifiers = CleveRoids.setbonusModifiers or {}
|
||||
|
||||
-- Cache: setId -> { itemId1, itemId2, ... } (resolved from Reliquary or hardcoded)
|
||||
local setItemCache = {}
|
||||
|
||||
-- Resolve set items: use Reliquary DBC if available, otherwise fall back to hardcoded list
|
||||
local function ResolveSetItems(modifier)
|
||||
-- If items are already provided (hardcoded), use them directly
|
||||
if modifier.items and table.getn(modifier.items) > 0 then
|
||||
return modifier.items
|
||||
end
|
||||
|
||||
-- Try Reliquary DBC lookup by setId
|
||||
if modifier.setId and CleveRoids.NampowerAPI then
|
||||
-- Check cache first
|
||||
if setItemCache[modifier.setId] then
|
||||
return setItemCache[modifier.setId]
|
||||
end
|
||||
|
||||
local items = CleveRoids.NampowerAPI.GetItemSetItems(modifier.setId)
|
||||
if items then
|
||||
setItemCache[modifier.setId] = items
|
||||
return items
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Function to count how many items from a set are currently equipped
|
||||
-- Accepts either a direct items table or a modifier entry with setId for Reliquary lookup
|
||||
function CleveRoids.CountEquippedSetItems(items)
|
||||
if not items or type(items) ~= "table" then return 0 end
|
||||
|
||||
@@ -6701,6 +6931,42 @@ function CleveRoids.CountEquippedSetItems(items)
|
||||
return count
|
||||
end
|
||||
|
||||
-- Count equipped set items by set ID (uses Nampower to check each slot's itemSet field)
|
||||
-- Falls back to CountEquippedSetItems with Reliquary item list if GetItemStatsField unavailable
|
||||
function CleveRoids.CountEquippedSetItemsBySetId(setId)
|
||||
if not setId then return 0 end
|
||||
|
||||
-- Fast path: use Nampower GetItemStatsField to check itemSet directly per slot
|
||||
if CleveRoids.NampowerAPI and CleveRoids.NampowerAPI.GetItemField then
|
||||
local count = 0
|
||||
for slot = 1, 19 do
|
||||
local itemLink = GetInventoryItemLink("player", slot)
|
||||
if itemLink then
|
||||
local _, _, itemID = string.find(itemLink, "item:(%d+)")
|
||||
if itemID then
|
||||
local itemSetId = CleveRoids.NampowerAPI.GetItemSetId(tonumber(itemID))
|
||||
if itemSetId == setId then
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
-- Fallback: resolve set item list and count matches
|
||||
local items = setItemCache[setId]
|
||||
if not items and CleveRoids.NampowerAPI then
|
||||
items = CleveRoids.NampowerAPI.GetItemSetItems(setId)
|
||||
if items then setItemCache[setId] = items end
|
||||
end
|
||||
if items then
|
||||
return CleveRoids.CountEquippedSetItems(items)
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Apply set bonus modifiers to a debuff duration
|
||||
-- Parameters:
|
||||
-- spellID: The spell ID
|
||||
@@ -6716,8 +6982,16 @@ function CleveRoids.ApplySetBonusModifier(spellID, baseDuration)
|
||||
return baseDuration
|
||||
end
|
||||
|
||||
-- Check if player has enough set pieces equipped
|
||||
local equippedCount = CleveRoids.CountEquippedSetItems(modifier.items)
|
||||
-- Count equipped pieces: use setId for fast Nampower lookup, or resolve item list
|
||||
local equippedCount
|
||||
if modifier.setId then
|
||||
equippedCount = CleveRoids.CountEquippedSetItemsBySetId(modifier.setId)
|
||||
else
|
||||
local items = ResolveSetItems(modifier)
|
||||
if not items then return baseDuration end
|
||||
equippedCount = CleveRoids.CountEquippedSetItems(items)
|
||||
end
|
||||
|
||||
if equippedCount < modifier.threshold then
|
||||
return baseDuration
|
||||
end
|
||||
@@ -6725,9 +6999,13 @@ function CleveRoids.ApplySetBonusModifier(spellID, baseDuration)
|
||||
local modifiedDuration = modifier.modifier(baseDuration)
|
||||
|
||||
if modifiedDuration ~= baseDuration and CleveRoids.debug then
|
||||
local spellName = "Unknown"
|
||||
if GetSpellRecField then
|
||||
spellName = GetSpellRecField(spellID, "name") or spellName
|
||||
end
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cff00ffff[Set Bonus Modifier]|r %s (ID:%d): %.1fs -> %.1fs (%d/%d pieces)",
|
||||
GetSpellRecField(spellID, "name") or "Unknown", spellID, baseDuration, modifiedDuration,
|
||||
spellName, spellID, baseDuration, modifiedDuration,
|
||||
equippedCount, modifier.threshold)
|
||||
)
|
||||
end
|
||||
@@ -6737,13 +7015,18 @@ end
|
||||
|
||||
-- Helper function to register a set bonus modifier
|
||||
-- Usage: CleveRoids.RegisterSetBonusModifier(spellID, itemsTable, threshold, modifierFunction)
|
||||
function CleveRoids.RegisterSetBonusModifier(spellID, items, threshold, modifierFunc)
|
||||
if not spellID or not items or not threshold or not modifierFunc then
|
||||
-- With Reliquary: CleveRoids.RegisterSetBonusModifier(spellID, nil, threshold, modifierFunction, setId)
|
||||
function CleveRoids.RegisterSetBonusModifier(spellID, items, threshold, modifierFunc, setId)
|
||||
if not spellID or not threshold or not modifierFunc then
|
||||
return false
|
||||
end
|
||||
if not items and not setId then
|
||||
return false
|
||||
end
|
||||
|
||||
CleveRoids.setbonusModifiers[spellID] = {
|
||||
items = items,
|
||||
setId = setId,
|
||||
threshold = threshold,
|
||||
modifier = modifierFunc
|
||||
}
|
||||
@@ -6751,42 +7034,113 @@ function CleveRoids.RegisterSetBonusModifier(spellID, items, threshold, modifier
|
||||
return true
|
||||
end
|
||||
|
||||
-- Get info about an equipped item's set (combines Nampower + Reliquary)
|
||||
-- Returns: setName, setId, equippedCount, totalPieces, items or nil
|
||||
function CleveRoids.GetEquippedItemSetInfo(itemId)
|
||||
if not itemId or not CleveRoids.NampowerAPI then return nil end
|
||||
|
||||
local setId = CleveRoids.NampowerAPI.GetItemSetId(itemId)
|
||||
if not setId then return nil end
|
||||
|
||||
local setData = CleveRoids.NampowerAPI.GetItemSet(setId)
|
||||
local setName = setData and setData.name_enUS or nil
|
||||
local items = CleveRoids.NampowerAPI.GetItemSetItems(setId)
|
||||
local totalPieces = items and table.getn(items) or 0
|
||||
local equippedCount = CleveRoids.CountEquippedSetItemsBySetId(setId)
|
||||
|
||||
return setName, setId, equippedCount, totalPieces, items
|
||||
end
|
||||
|
||||
-- Count equipped pieces of a named or ID-referenced item set
|
||||
-- nameOrId: set name (string, e.g. "Judgement Battlegear") or set ID (number)
|
||||
-- Returns: equipped count (number), setId (number or nil)
|
||||
-- Name lookup requires Reliquary; ID lookup requires Nampower
|
||||
function CleveRoids.GetEquippedSetPieceCount(nameOrId)
|
||||
if not nameOrId then return 0, nil end
|
||||
|
||||
-- Numeric: treat as set ID directly
|
||||
local numericId = tonumber(nameOrId)
|
||||
if numericId then
|
||||
return CleveRoids.CountEquippedSetItemsBySetId(numericId), numericId
|
||||
end
|
||||
|
||||
-- String: scan equipped items and match by set name from DBC
|
||||
if not CleveRoids.NampowerAPI or not CleveRoids.NampowerAPI.GetItemSetId then
|
||||
return 0, nil
|
||||
end
|
||||
|
||||
-- Scan equipped items to resolve set name → set ID, then count via fast path
|
||||
local lowerName = string.lower(nameOrId)
|
||||
|
||||
for slot = 1, 19 do
|
||||
local itemLink = GetInventoryItemLink("player", slot)
|
||||
if itemLink then
|
||||
local _, _, itemID = string.find(itemLink, "item:(%d+)")
|
||||
if itemID then
|
||||
local setId = CleveRoids.NampowerAPI.GetItemSetId(tonumber(itemID))
|
||||
if setId then
|
||||
local setData = CleveRoids.NampowerAPI.GetItemSet(setId)
|
||||
if setData then
|
||||
local dbcName = setData.name_enUS
|
||||
if dbcName and string.lower(dbcName) == lowerName then
|
||||
return CleveRoids.CountEquippedSetItemsBySetId(setId), setId
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return 0, nil
|
||||
end
|
||||
|
||||
-- Get spell effect radius in yards (Nampower + Reliquary)
|
||||
-- spellId: the spell to look up
|
||||
-- effectIndex: 1, 2, or 3 (which effect slot, default 1)
|
||||
-- Returns: radius in yards or nil
|
||||
function CleveRoids.GetSpellEffectRadius(spellId, effectIndex)
|
||||
if not CleveRoids.NampowerAPI then return nil end
|
||||
return CleveRoids.NampowerAPI.GetSpellEffectRadius(spellId, effectIndex)
|
||||
end
|
||||
|
||||
-- DRUID set bonus modifiers
|
||||
-- Dreamwalker Regalia (4/9): Increases Moonfire duration by 3 seconds and Insect Swarm by 2 seconds
|
||||
-- Item IDs: 47372, 47373, 47374, 47375, 47376, 47377, 47378, 47379, 47380
|
||||
-- setId 536 = Dreamwalker Regalia; hardcoded items as fallback when Reliquary unavailable
|
||||
local DREAMWALKER_SET_ID = 536
|
||||
local dreamwalkerItems = { 47372, 47373, 47374, 47375, 47376, 47377, 47378, 47379, 47380 }
|
||||
local dreamwalkerMoonfireModifier = function(base) return base + 3 end
|
||||
local dreamwalkerInsectSwarmModifier = function(base) return base + 2 end
|
||||
|
||||
-- Moonfire (all ranks)
|
||||
CleveRoids.setbonusModifiers[8921] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 1
|
||||
CleveRoids.setbonusModifiers[8924] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 2
|
||||
CleveRoids.setbonusModifiers[8925] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 3
|
||||
CleveRoids.setbonusModifiers[8926] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 4
|
||||
CleveRoids.setbonusModifiers[8927] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 5
|
||||
CleveRoids.setbonusModifiers[8928] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 6
|
||||
CleveRoids.setbonusModifiers[8929] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 7
|
||||
CleveRoids.setbonusModifiers[9833] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 8
|
||||
CleveRoids.setbonusModifiers[9834] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 9
|
||||
CleveRoids.setbonusModifiers[9835] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 10
|
||||
CleveRoids.setbonusModifiers[8921] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 1
|
||||
CleveRoids.setbonusModifiers[8924] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 2
|
||||
CleveRoids.setbonusModifiers[8925] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 3
|
||||
CleveRoids.setbonusModifiers[8926] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 4
|
||||
CleveRoids.setbonusModifiers[8927] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 5
|
||||
CleveRoids.setbonusModifiers[8928] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 6
|
||||
CleveRoids.setbonusModifiers[8929] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 7
|
||||
CleveRoids.setbonusModifiers[9833] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 8
|
||||
CleveRoids.setbonusModifiers[9834] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 9
|
||||
CleveRoids.setbonusModifiers[9835] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 10
|
||||
|
||||
-- Insect Swarm (all ranks)
|
||||
CleveRoids.setbonusModifiers[5570] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 1
|
||||
CleveRoids.setbonusModifiers[24974] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 2
|
||||
CleveRoids.setbonusModifiers[24975] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 3
|
||||
CleveRoids.setbonusModifiers[24976] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 4
|
||||
CleveRoids.setbonusModifiers[24977] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 5
|
||||
CleveRoids.setbonusModifiers[5570] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 1
|
||||
CleveRoids.setbonusModifiers[24974] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 2
|
||||
CleveRoids.setbonusModifiers[24975] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 3
|
||||
CleveRoids.setbonusModifiers[24976] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 4
|
||||
CleveRoids.setbonusModifiers[24977] = { setId = DREAMWALKER_SET_ID, items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 5
|
||||
|
||||
-- Haruspex's Garb (3/5): Increases Faerie Fire duration by 5 seconds
|
||||
-- Item IDs: 19613, 19955, 19840, 19839, 19838
|
||||
-- setId 474 = Haruspex's Garb; hardcoded items as fallback when Reliquary unavailable
|
||||
local HARUSPEX_SET_ID = 474
|
||||
local haruspexItems = { 19613, 19955, 19840, 19839, 19838 }
|
||||
local haruspexFaerieFireModifier = function(base) return base + 5 end
|
||||
|
||||
-- Faerie Fire (non-feral, all ranks)
|
||||
CleveRoids.setbonusModifiers[770] = { items = haruspexItems, threshold = 3, modifier = haruspexFaerieFireModifier } -- Faerie Fire Rank 1
|
||||
CleveRoids.setbonusModifiers[778] = { items = haruspexItems, threshold = 3, modifier = haruspexFaerieFireModifier } -- Faerie Fire Rank 2
|
||||
CleveRoids.setbonusModifiers[9749] = { items = haruspexItems, threshold = 3, modifier = haruspexFaerieFireModifier } -- Faerie Fire Rank 3
|
||||
CleveRoids.setbonusModifiers[9907] = { items = haruspexItems, threshold = 3, modifier = haruspexFaerieFireModifier } -- Faerie Fire Rank 4
|
||||
CleveRoids.setbonusModifiers[770] = { setId = HARUSPEX_SET_ID, items = haruspexItems, threshold = 3, modifier = haruspexFaerieFireModifier } -- Faerie Fire Rank 1
|
||||
CleveRoids.setbonusModifiers[778] = { setId = HARUSPEX_SET_ID, items = haruspexItems, threshold = 3, modifier = haruspexFaerieFireModifier } -- Faerie Fire Rank 2
|
||||
CleveRoids.setbonusModifiers[9749] = { setId = HARUSPEX_SET_ID, items = haruspexItems, threshold = 3, modifier = haruspexFaerieFireModifier } -- Faerie Fire Rank 3
|
||||
CleveRoids.setbonusModifiers[9907] = { setId = HARUSPEX_SET_ID, items = haruspexItems, threshold = 3, modifier = haruspexFaerieFireModifier } -- Faerie Fire Rank 4
|
||||
|
||||
-- IMMUNITY TRACKING SYSTEM
|
||||
-- Initialize SavedVariables for immunity tracking
|
||||
@@ -7169,7 +7523,8 @@ local function GetSpellSchool(spellName, spellID)
|
||||
elseif string.find(text, "fire") or string.find(text, "flame") then
|
||||
school = "fire"
|
||||
break
|
||||
elseif string.find(text, "frost") or string.find(text, "ice") then
|
||||
elseif string.find(text, "frost") or
|
||||
string.find(text, "^ice[^%a]") or string.find(text, "[^%a]ice[^%a]") or string.find(text, "[^%a]ice$") then
|
||||
school = "frost"
|
||||
break
|
||||
elseif string.find(text, "nature") or string.find(text, "poison") then
|
||||
@@ -7217,8 +7572,10 @@ local function GetSpellSchool(spellName, spellID)
|
||||
string.find(lower, "ignite") or string.find(lower, "combustion") then
|
||||
school = "fire"
|
||||
|
||||
-- Frost
|
||||
elseif string.find(lower, "frost") or string.find(lower, "ice") or
|
||||
-- Frost (use word-boundary matching for "ice" to avoid false positives
|
||||
-- from spell names like "Slice and Dice" containing "ice" as substring)
|
||||
elseif string.find(lower, "frost") or
|
||||
string.find(lower, "^ice[^%a]") or string.find(lower, "[^%a]ice[^%a]") or string.find(lower, "[^%a]ice$") or
|
||||
string.find(lower, "blizzard") or string.find(lower, "freeze") or
|
||||
string.find(lower, "chill") then
|
||||
school = "frost"
|
||||
@@ -7283,93 +7640,6 @@ local function GetUnitBuffs(unit)
|
||||
return buffs
|
||||
end
|
||||
|
||||
-- Spells with INVULNERABILITY mechanic (mechanic 25) from BuffLib SpellData DBC
|
||||
-- These grant temporary immunity and should not trigger permanent immunity recording
|
||||
-- Source: BuffLib/SpellData.lua - extracted from DBC files
|
||||
local INVULNERABILITY_SPELL_IDS = {
|
||||
-- Paladin
|
||||
[498] = true, -- Divine Protection (Rank 1)
|
||||
[642] = true, -- Divine Shield (Rank 1)
|
||||
[1020] = true, -- Divine Shield (Rank 2)
|
||||
[1022] = true, -- Blessing of Protection (Rank 1)
|
||||
[5573] = true, -- Divine Protection (Rank 2)
|
||||
[5599] = true, -- Blessing of Protection (Rank 2)
|
||||
[10278] = true, -- Blessing of Protection (Rank 3)
|
||||
[25771] = true, -- Forbearance (debuff after immunity)
|
||||
-- Old/Unused Paladin
|
||||
[1052] = true, -- zzOLDBlessing of Righteousness
|
||||
[5601] = true, -- zzOLDBlessing of Righteousness
|
||||
[5602] = true, -- zzOLDBlessing of Righteousness
|
||||
[10280] = true, -- zzOLDBlessing of Righteousness
|
||||
[10281] = true, -- zzOLDBlessing of Righteousness
|
||||
-- Rogue
|
||||
[6770] = true, -- Sap (invuln during effect)
|
||||
-- NPC/Misc
|
||||
[7992] = true, -- Slowing Poison
|
||||
[11638] = true, -- Radiation Poisoning
|
||||
[14897] = true, -- Slowing Poison
|
||||
[16603] = true, -- Demonfork
|
||||
[16791] = true, -- Furious Anger
|
||||
[17407] = true, -- Wound
|
||||
[18208] = true, -- Poison
|
||||
[23230] = true, -- Blood Fury
|
||||
[24005] = true, -- Food
|
||||
[24707] = true, -- Food
|
||||
[24865] = true, -- Sanctified Orb
|
||||
[26263] = true, -- Dim Sum
|
||||
[28522] = true, -- Icebolt
|
||||
[29055] = true, -- Refreshing Red Apple
|
||||
[29325] = true, -- Acid Volley
|
||||
[29330] = true, -- Sapphiron's Wing Buffet Despawn
|
||||
-- Spell Reflection (no DBC mechanic, but grants immunity to reflected schools)
|
||||
[9941] = true, -- Spell Reflection
|
||||
[9943] = true, -- Spell Reflection
|
||||
[10074] = true, -- Spell Reflection
|
||||
[11818] = true, -- Spell Reflection
|
||||
[21118] = true, -- Spell Reflection
|
||||
-- School-specific Reflectors (Engineering items)
|
||||
[23097] = true, -- Fire Reflector
|
||||
[23131] = true, -- Frost Reflector
|
||||
[23132] = true, -- Shadow Reflector
|
||||
[23178] = true, -- Nature Reflector
|
||||
[23216] = true, -- Arcane Reflector
|
||||
-- Multi-school Reflect (NPC abilities)
|
||||
[13022] = true, -- Fire and Arcane Reflect
|
||||
[19595] = true, -- Shadow and Frost Reflect
|
||||
-- Generic Reflection buffs
|
||||
[3651] = true, -- Shield of Reflection
|
||||
[9906] = true, -- Reflection
|
||||
[10831] = true, -- Reflection Field
|
||||
[17106] = true, -- Reflection
|
||||
[17107] = true, -- Reflection
|
||||
[17108] = true, -- Reflection
|
||||
[20223] = true, -- Magic Reflection
|
||||
[20619] = true, -- Magic Reflection
|
||||
[22067] = true, -- Reflection
|
||||
[23920] = true, -- Shield Reflection
|
||||
[23921] = true, -- Shield Reflection
|
||||
[27564] = true, -- Reflection
|
||||
}
|
||||
|
||||
-- Check if a unit has any immunity-granting buff active
|
||||
-- Uses only spell IDs from DBC mechanic 25 (INVULNERABILITY) for reliability
|
||||
-- Returns the buff name if found, nil otherwise
|
||||
local function HasImmunityGrantingBuff(unit)
|
||||
if not UnitExists(unit) then return nil end
|
||||
|
||||
for i = 1, 32 do
|
||||
local texture, stacks, spellID = UnitBuff(unit, i)
|
||||
if not texture then break end
|
||||
|
||||
if spellID and INVULNERABILITY_SPELL_IDS[spellID] then
|
||||
local buffName = GetSpellRecField(spellID, "name") or ("SpellID:" .. spellID)
|
||||
return buffName
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- CC IMMUNITY TRACKING
|
||||
|
||||
-- Get CC type from a spell ID using the CCSpellMechanics table from Conditionals.lua
|
||||
@@ -7377,12 +7647,33 @@ end
|
||||
local function GetSpellCCType(spellID)
|
||||
if not spellID or spellID <= 0 then return nil end
|
||||
|
||||
-- CCSpellMechanics is defined in Conditionals.lua and loaded before this
|
||||
local mechanic = CleveRoids.CCSpellMechanics and CleveRoids.CCSpellMechanics[spellID]
|
||||
if not mechanic then return nil end
|
||||
-- Priority 1: DBC mechanic lookup (authoritative, covers all spells)
|
||||
if GetSpellRecField then
|
||||
-- Check spell-level mechanic first
|
||||
local mechanic = GetSpellRecField(spellID, "mechanic")
|
||||
if mechanic and mechanic > 0 and MECHANIC_TO_CC_TYPE[mechanic] then
|
||||
return MECHANIC_TO_CC_TYPE[mechanic]
|
||||
end
|
||||
|
||||
-- Map mechanic ID to CC type name
|
||||
return MECHANIC_TO_CC_TYPE[mechanic]
|
||||
-- Check per-effect mechanics (array of 3 effects)
|
||||
local effectMechanics = GetSpellRecField(spellID, "effectMechanic")
|
||||
if effectMechanics then
|
||||
for i = 1, 3 do
|
||||
local em = effectMechanics[i]
|
||||
if em and em > 0 and MECHANIC_TO_CC_TYPE[em] then
|
||||
return MECHANIC_TO_CC_TYPE[em]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Priority 2: Hardcoded table fallback (CCSpellMechanics in Conditionals.lua)
|
||||
local mechanic = CleveRoids.CCSpellMechanics and CleveRoids.CCSpellMechanics[spellID]
|
||||
if mechanic and MECHANIC_TO_CC_TYPE[mechanic] then
|
||||
return MECHANIC_TO_CC_TYPE[mechanic]
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Expose publicly for use by other modules
|
||||
@@ -7536,12 +7827,13 @@ local function RecordImmunity(npcName, spellName, conditionalBuff, spellID)
|
||||
return
|
||||
end
|
||||
|
||||
-- Try to get school using spell ID if available (most accurate)
|
||||
local school = GetSpellSchool(spellName, spellID)
|
||||
-- Determine school: DBC lookup by spellID is authoritative for immunity recording.
|
||||
-- Unlike GetSpellSchool() (designed for debuff tracking, skips physical to detect bleed),
|
||||
-- immunity recording needs the actual DBC school — physical immunity IS a real thing.
|
||||
local school = nil
|
||||
|
||||
-- SPLIT DAMAGE SPELLS: Combat log immunity messages refer to the INITIAL hit failing,
|
||||
-- not the DoT/debuff. For spells like Pounce (stun + bleed), if a mob is stun-immune,
|
||||
-- the bleed might still apply. Use the initial school for immunity recording.
|
||||
-- SPLIT DAMAGE SPELLS: Use initial hit school, not the debuff school.
|
||||
-- e.g., Pounce stun-immune doesn't mean bleed-immune.
|
||||
if spellName then
|
||||
local baseName = string.gsub(spellName, "%s*%(.-%)%s*$", "")
|
||||
if SPLIT_DAMAGE_SPELLS[baseName] then
|
||||
@@ -7552,11 +7844,28 @@ local function RecordImmunity(npcName, spellName, conditionalBuff, spellID)
|
||||
end
|
||||
end
|
||||
|
||||
-- If we can't determine the school, use "unknown" and store the spell name
|
||||
-- Direct DBC lookup: spellID → GetSpellRecField → school enum → name
|
||||
if not school and spellID and GetSpellRecField then
|
||||
local dbcSchool = GetSpellRecField(spellID, "school")
|
||||
if dbcSchool and SCHOOL_NAMES[dbcSchool] then
|
||||
school = SCHOOL_NAMES[dbcSchool]
|
||||
end
|
||||
end
|
||||
|
||||
-- Fallback: try learned mapping from damage events
|
||||
if not school and spellID and CleveRoids.spellSchoolMapping[spellID] then
|
||||
school = CleveRoids.spellSchoolMapping[spellID]
|
||||
end
|
||||
|
||||
-- Last resort: name-based lookup (no spellID available)
|
||||
if not school and spellName then
|
||||
school = GetSpellSchool(spellName, nil)
|
||||
end
|
||||
|
||||
if not school then
|
||||
school = "unknown"
|
||||
if CleveRoids.debug then
|
||||
CleveRoids.Print("|cffff9900[Unknown School]|r Could not determine school for: " .. spellName)
|
||||
CleveRoids.Print("|cffff9900[Unknown School]|r Could not determine school for: " .. (spellName or tostring(spellID)))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7924,24 +8233,30 @@ local function ParseImmunityCombatLog()
|
||||
-- Only "immune" messages should create immunity records
|
||||
|
||||
-- Extract damage school if explicitly mentioned in message
|
||||
-- IMPORTANT: Only search the text AFTER "is immune to" for the school keyword,
|
||||
-- NOT the entire message. NPC names like "Frostmane Troll" or "Shadowforge Dwarf"
|
||||
-- contain school keywords that would cause false immunity records.
|
||||
if string.find(message, "is immune to") then
|
||||
local lowerMsg = string.lower(message)
|
||||
if string.find(lowerMsg, "fire") then
|
||||
school = "fire"
|
||||
elseif string.find(lowerMsg, "frost") then
|
||||
school = "frost"
|
||||
elseif string.find(lowerMsg, "nature") then
|
||||
school = "nature"
|
||||
elseif string.find(lowerMsg, "shadow") then
|
||||
school = "shadow"
|
||||
elseif string.find(lowerMsg, "arcane") then
|
||||
school = "arcane"
|
||||
elseif string.find(lowerMsg, "holy") then
|
||||
school = "holy"
|
||||
elseif string.find(lowerMsg, "physical") then
|
||||
school = "physical"
|
||||
elseif string.find(lowerMsg, "bleed") then
|
||||
school = "bleed"
|
||||
local _, immuneEnd = string.find(message, "is immune to%s*")
|
||||
if immuneEnd then
|
||||
local schoolText = string.lower(string.sub(message, immuneEnd + 1))
|
||||
if string.find(schoolText, "^fire") then
|
||||
school = "fire"
|
||||
elseif string.find(schoolText, "^frost") then
|
||||
school = "frost"
|
||||
elseif string.find(schoolText, "^nature") then
|
||||
school = "nature"
|
||||
elseif string.find(schoolText, "^shadow") then
|
||||
school = "shadow"
|
||||
elseif string.find(schoolText, "^arcane") then
|
||||
school = "arcane"
|
||||
elseif string.find(schoolText, "^holy") then
|
||||
school = "holy"
|
||||
elseif string.find(schoolText, "^physical") then
|
||||
school = "physical"
|
||||
elseif string.find(schoolText, "^bleed") then
|
||||
school = "bleed"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8003,53 +8318,18 @@ local function ParseImmunityCombatLog()
|
||||
|
||||
-- If we have a spell and target, record immunity
|
||||
if spellName and targetName then
|
||||
-- Debug: Show what we extracted
|
||||
-- SPELL_MISS_SELF (Nampower v2.31+, always available since addon requires v3.0+)
|
||||
-- provides exact spellId for accurate school detection and already handles immunity
|
||||
-- recording via ProcessSpellMissSelf. Skip duplicate text-based recording to avoid
|
||||
-- false positives from unreliable spell name → school resolution (e.g., "ice" substring
|
||||
-- matching in GetSpellSchool, NPC name contamination, etc.)
|
||||
-- The reason was already stored above for SPELL_GO correlation (recentCombatLogReasons).
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cffff9900[Immunity Detected]|r Spell: %s | Target: %s | School: %s",
|
||||
spellName, targetName, school or "auto-detect")
|
||||
string.format("|cff00aaff[CombatLog Deferred]|r %s on %s - SPELL_MISS_SELF handles immunity recording",
|
||||
spellName, targetName)
|
||||
)
|
||||
end
|
||||
|
||||
local spellID = CleveRoids.GetSpellIdForName and CleveRoids.GetSpellIdForName(spellName)
|
||||
|
||||
-- SPLIT CC SPELLS: Skip immunity recording for spells with physical damage + resistable CC
|
||||
local baseName = string.gsub(spellName, "%s*%(.-%)%s*$", "")
|
||||
if (spellID and SPLIT_CC_SPELLS[spellID]) or SPLIT_CC_SPELL_NAMES[baseName] then
|
||||
if CleveRoids.debug then
|
||||
CleveRoids.Print("|cff00aaff[Split CC Skip]|r " .. spellName .. " CC immune on " .. targetName .. " - skipping immunity recording (physical damage landed)")
|
||||
end
|
||||
CancelPendingVerification(targetName, spellName)
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if target is queryable for buff check
|
||||
local canQuery = UnitExists("target") and UnitName("target") == targetName
|
||||
if canQuery then
|
||||
local immunityBuff = HasImmunityGrantingBuff("target")
|
||||
if immunityBuff then
|
||||
if CleveRoids.debug then
|
||||
CleveRoids.Print("|cff00aaff[CombatLog Temp Skip]|r " .. targetName .. " has " .. immunityBuff .. " active - skipping permanent immunity recording for " .. spellName)
|
||||
end
|
||||
CancelPendingVerification(targetName, spellName)
|
||||
return
|
||||
end
|
||||
else
|
||||
-- Cannot query target — inconclusive, don't record
|
||||
if CleveRoids.debug then
|
||||
CleveRoids.Print("|cff00aaff[CombatLog Inconclusive]|r Cannot query " .. targetName .. " for buffs - not recording immunity for " .. spellName)
|
||||
end
|
||||
CancelPendingVerification(targetName, spellName)
|
||||
return
|
||||
end
|
||||
|
||||
-- Record as permanent immunity
|
||||
local ccType = spellID and GetSpellCCType(spellID)
|
||||
if ccType then
|
||||
RecordCCImmunity(targetName, ccType, nil, spellName)
|
||||
else
|
||||
RecordImmunity(targetName, spellName, nil, spellID)
|
||||
end
|
||||
CancelPendingVerification(targetName, spellName)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user