-- DPS Log Tracker — popup checklist for verifying all 35 COMBAT_LOG_EVENT subevents -- -- All events share: arg1=subevent, arg2=sourceGUID, arg3=destGUID -- Remaining args are subevent-specific (see layouts below). -- ============================================================================ -- Reference tables -- ============================================================================ -- Spell school bitmask (matches WotLK): -- 0x01 = Physical, 0x02 = Holy, 0x04 = Fire, 0x08 = Nature, -- 0x10 = Frost, 0x20 = Shadow, 0x40 = Arcane -- Common combos: 0x03 = Holystrike, 0x05 = Flamestrike, 0x1C = Elemental, -- 0x7E = Magic (all non-physical), 0x7F = Chaos (all 7) -- Power types: 0=Mana, 1=Rage, 2=Focus, 3=Energy, 4=Combo Points local powerNames = { [0]="Mana", "Rage", "Focus", "Energy", "Combo Points" } -- Miss types (string): "MISS", "DODGE", "PARRY", "BLOCK", "EVADE", "IMMUNE", -- "DEFLECT", "RESIST", "ABSORB", "REFLECT" -- Aura types (string): "BUFF", "DEBUFF" -- Environmental types (string): "EXHAUSTED", "DROWNING", "FALLING", "LAVA", -- "SLIME", "FIRE" -- SWING_DAMAGE arg10 flags bitmask: -- bit 0 (value 1) = glancing hit -- bit 1 (value 2) = crushing blow -- bit 2 (value 4) = offhand swing -- Extract: glancing = (math.mod(flags, 2) == 1) -- crushing = (math.mod(math.floor(flags / 2), 2) == 1) -- offhand = (math.mod(math.floor(flags / 4), 2) == 1) -- Combined: offhand glancing = 5, offhand crushing = 6, etc. -- ============================================================================ -- Helpers -- ============================================================================ local function schoolName(school) if school == 1 then return "Physical" elseif school == 2 then return "Holy" elseif school == 4 then return "Fire" elseif school == 8 then return "Nature" elseif school == 16 then return "Frost" elseif school == 32 then return "Shadow" elseif school == 64 then return "Arcane" else return "School:" .. (school or "?") end end local function s(v) if v == nil then return "nil" end return tostring(v) end -- ============================================================================ -- Subevent definitions — ordered list of all 34 -- ============================================================================ -- Each entry: { displayName, subevent, [variantField], [variantValue] } -- variantField: which arg to check for the variant (nil = any occurrence) -- variantValue: specific value to match (string or number) local SUBEVENTS = { -- Damage { "SPELL_DAMAGE", "SPELL_DAMAGE" }, { "SPELL_DAMAGE (crit)", "SPELL_DAMAGE", 10, 1 }, { "RANGE_DAMAGE", "RANGE_DAMAGE" }, { "RANGE_DAMAGE (crit)", "RANGE_DAMAGE", 10, 1 }, { "SPELL_PERIODIC_DAMAGE", "SPELL_PERIODIC_DAMAGE" }, { "DAMAGE_SHIELD", "DAMAGE_SHIELD" }, { "DAMAGE_SPLIT", "DAMAGE_SPLIT" }, { "SWING_DAMAGE", "SWING_DAMAGE" }, { "SWING_DAMAGE (crit)", "SWING_DAMAGE", 9, 1 }, { "SWING_DAMAGE (glancing)", "SWING_DAMAGE", 10, 1 }, -- flags bit 0 { "SWING_DAMAGE (crushing)", "SWING_DAMAGE", 10, 2 }, -- flags bit 1 { "SWING_DAMAGE (offhand)", "SWING_DAMAGE", 10, 4 }, -- flags bit 2 { "ENVIRONMENTAL (DROWNING)", "ENVIRONMENTAL_DAMAGE", 4, "DROWNING" }, { "ENVIRONMENTAL (FALLING)", "ENVIRONMENTAL_DAMAGE", 4, "FALLING" }, { "ENVIRONMENTAL (LAVA)", "ENVIRONMENTAL_DAMAGE", 4, "LAVA" }, { "ENVIRONMENTAL (SLIME)", "ENVIRONMENTAL_DAMAGE", 4, "SLIME" }, { "ENVIRONMENTAL (FIRE)", "ENVIRONMENTAL_DAMAGE", 4, "FIRE" }, { "ENVIRONMENTAL (EXHAUSTED)", "ENVIRONMENTAL_DAMAGE", 4, "EXHAUSTED" }, -- Misses — swing { "SWING_MISSED (MISS)", "SWING_MISSED", 4, "MISS" }, { "SWING_MISSED (DODGE)", "SWING_MISSED", 4, "DODGE" }, { "SWING_MISSED (PARRY)", "SWING_MISSED", 4, "PARRY" }, { "SWING_MISSED (BLOCK)", "SWING_MISSED", 4, "BLOCK" }, { "SWING_MISSED (EVADE)", "SWING_MISSED", 4, "EVADE" }, { "SWING_MISSED (IMMUNE)", "SWING_MISSED", 4, "IMMUNE" }, { "SWING_MISSED (RESIST)", "SWING_MISSED", 4, "RESIST" }, { "SWING_MISSED (ABSORB)", "SWING_MISSED", 4, "ABSORB" }, -- Misses — spell/range { "SPELL_MISSED (MISS)", "SPELL_MISSED", 6, "MISS" }, { "SPELL_MISSED (DODGE)", "SPELL_MISSED", 6, "DODGE" }, { "SPELL_MISSED (PARRY)", "SPELL_MISSED", 6, "PARRY" }, { "SPELL_MISSED (IMMUNE)", "SPELL_MISSED", 6, "IMMUNE" }, { "SPELL_MISSED (RESIST)", "SPELL_MISSED", 6, "RESIST" }, { "SPELL_MISSED (ABSORB)", "SPELL_MISSED", 6, "ABSORB" }, { "SPELL_MISSED (DEFLECT)", "SPELL_MISSED", 6, "DEFLECT" }, -- dead in vanilla, kept for completeness { "SPELL_MISSED (REFLECT)", "SPELL_MISSED", 6, "REFLECT" }, { "RANGE_MISSED", "RANGE_MISSED" }, { "SPELL_PERIODIC_MISSED", "SPELL_PERIODIC_MISSED" }, { "DAMAGE_SHIELD_MISSED", "DAMAGE_SHIELD_MISSED" }, -- Heals { "SPELL_HEAL", "SPELL_HEAL" }, { "SPELL_HEAL (crit)", "SPELL_HEAL", 8, 1 }, { "SPELL_PERIODIC_HEAL", "SPELL_PERIODIC_HEAL" }, -- Power { "SPELL_ENERGIZE (Mana)", "SPELL_ENERGIZE" }, -- amount=0 from hook, can't distinguish { "SPELL_PERIODIC_ENERGIZE", "SPELL_PERIODIC_ENERGIZE" }, { "SPELL_PERIODIC_DRAIN", "SPELL_PERIODIC_DRAIN" }, { "SPELL_PERIODIC_LEECH", "SPELL_PERIODIC_LEECH" }, -- Auras { "SPELL_AURA_APPLIED (BUFF)", "SPELL_AURA_APPLIED", 6, "BUFF" }, { "SPELL_AURA_APPLIED (DEBUFF)", "SPELL_AURA_APPLIED", 6, "DEBUFF" }, { "SPELL_AURA_REMOVED (BUFF)", "SPELL_AURA_REMOVED", 6, "BUFF" }, { "SPELL_AURA_REMOVED (DEBUFF)", "SPELL_AURA_REMOVED", 6, "DEBUFF" }, { "SPELL_AURA_APPLIED_DOSE", "SPELL_AURA_APPLIED_DOSE" }, { "SPELL_AURA_REMOVED_DOSE", "SPELL_AURA_REMOVED_DOSE" }, { "SPELL_AURA_REFRESH", "SPELL_AURA_REFRESH" }, { "SPELL_AURA_BROKEN", "SPELL_AURA_BROKEN" }, { "SPELL_AURA_BROKEN_SPELL", "SPELL_AURA_BROKEN_SPELL" }, -- Casts { "SPELL_CAST_START", "SPELL_CAST_START" }, { "SPELL_CAST_SUCCESS", "SPELL_CAST_SUCCESS" }, { "SPELL_CAST_FAILED", "SPELL_CAST_FAILED" }, -- Misc spell { "SPELL_INTERRUPT", "SPELL_INTERRUPT" }, { "SPELL_DISPEL", "SPELL_DISPEL" }, { "SPELL_DISPEL_FAILED", "SPELL_DISPEL_FAILED" }, { "SPELL_EXTRA_ATTACKS", "SPELL_EXTRA_ATTACKS" }, { "SPELL_SUMMON", "SPELL_SUMMON" }, { "SPELL_RESURRECT", "SPELL_RESURRECT" }, { "SPELL_INSTAKILL", "SPELL_INSTAKILL" }, -- Deaths { "UNIT_DIED", "UNIT_DIED" }, { "UNIT_DESTROYED", "UNIT_DESTROYED" }, { "PARTY_KILL", "PARTY_KILL" }, } local NUM_SUBEVENTS = table.getn(SUBEVENTS) -- Build reverse lookup: for each subevent name, list all indices that match it -- (one subevent may have multiple variant entries) local subeventIndices = {} -- [subeventName] = { idx1, idx2, ... } for i = 1, NUM_SUBEVENTS do local name = SUBEVENTS[i][2] if not subeventIndices[name] then subeventIndices[name] = {} end table.insert(subeventIndices[name], i) end -- Tracking state local seen = {} -- [index] = true local seenArgs = {} -- [index] = formatted arg string local seenCount = 0 -- ============================================================================ -- Arg formatters per subevent (return a short summary string) -- ============================================================================ local function fmtSpellDmg(spellId, amount, school, resisted, absorbed, blocked, critical) local c = (critical == 1) and " CRIT" or "" return format("spell=%s amt=%s %s res=%s abs=%s blk=%s%s", s(spellId), s(amount), schoolName(school or 0), s(resisted), s(absorbed), s(blocked), c) end local function fmtSwingDmg(amount, school, resisted, absorbed, blocked, critical, flags) local c = (critical == 1) and " CRIT" or "" local f = "" if flags and flags > 0 then if math.mod(flags, 2) == 1 then f = f .. " GLANC" end if math.mod(math.floor(flags / 2), 2) == 1 then f = f .. " CRUSH" end if math.mod(math.floor(flags / 4), 2) == 1 then f = f .. " OH" end end return format("amt=%s %s res=%s abs=%s blk=%s%s%s", s(amount), schoolName(school or 0), s(resisted), s(absorbed), s(blocked), c, f) end local argFormatters = {} -- Pattern A — numeric args argFormatters["SPELL_DAMAGE"] = function(a4,a5,a6,a7,a8,a9,a10) return fmtSpellDmg(a4,a5,a6,a7,a8,a9,a10) end argFormatters["RANGE_DAMAGE"] = function(a4,a5,a6,a7,a8,a9,a10) return fmtSpellDmg(a4,a5,a6,a7,a8,a9,a10) end argFormatters["SPELL_PERIODIC_DAMAGE"] = function(a4,a5,a6,a7,a8,a9,a10) return fmtSpellDmg(a4,a5,a6,a7,a8,a9,a10) end argFormatters["DAMAGE_SHIELD"] = function(a4,a5,a6,a7,a8,a9,a10) return fmtSpellDmg(a4,a5,a6,a7,a8,a9,a10) end argFormatters["SWING_DAMAGE"] = function(a4,a5,a6,a7,a8,a9,a10) return fmtSwingDmg(a4,a5,a6,a7,a8,a9,a10) end argFormatters["SPELL_HEAL"] = function(a4,a5,a6,a7,a8) local c = (a8 == 1) and " CRIT" or "" return format("spell=%s amt=%s oh=%s abs=%s%s", s(a4), s(a5), s(a6), s(a7), c) end argFormatters["SPELL_PERIODIC_HEAL"] = function(a4,a5) return format("spell=%s amt=%s", s(a4), s(a5)) end argFormatters["SPELL_ENERGIZE"] = function(a4,a5,a6) return format("spell=%s amt=%s type=%s", s(a4), s(a5), powerNames[a6] or s(a6)) end argFormatters["SPELL_PERIODIC_ENERGIZE"] = function(a4,a5,a6,a7) return format("spell=%s amt=%s over=%s type=%s", s(a4), s(a5), s(a6), powerNames[a7] or s(a7)) end argFormatters["SPELL_PERIODIC_LEECH"] = function(a4,a5,a6,a7,a8,a9) return format("spell=%s amt=%s %s gain=%s abs=%s res=%s", s(a4), s(a5), schoolName(a6 or 0), s(a7), s(a8), s(a9)) end argFormatters["SPELL_PERIODIC_DRAIN"] = function(a4,a5,a6,a7) return format("spell=%s amt=%s type=%s extra=%s", s(a4), s(a5), powerNames[a6] or s(a6), s(a7)) end argFormatters["SPELL_EXTRA_ATTACKS"] = function(a4,a5,a6) return format("spell=%s school=%s amt=%s", s(a4), s(a5), s(a6)) end -- Pattern B — swing missed argFormatters["SWING_MISSED"] = function(a4) return s(a4) end -- Pattern C — spell + string argFormatters["SPELL_MISSED"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end argFormatters["SPELL_PERIODIC_MISSED"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end argFormatters["DAMAGE_SHIELD_MISSED"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end argFormatters["RANGE_MISSED"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end argFormatters["SPELL_AURA_APPLIED"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end argFormatters["SPELL_AURA_REMOVED"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end argFormatters["SPELL_AURA_REFRESH"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end argFormatters["SPELL_AURA_BROKEN"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end argFormatters["SPELL_CAST_FAILED"] = function(a4,a5,a6) return format("spell=%s %s reason=%s", s(a4), schoolName(a5 or 0), s(a6)) end -- Pattern D — spell only argFormatters["SPELL_CAST_START"] = function(a4,a5) return format("spell=%s %s", s(a4), schoolName(a5 or 0)) end argFormatters["SPELL_CAST_SUCCESS"] = function(a4,a5) return format("spell=%s %s", s(a4), schoolName(a5 or 0)) end argFormatters["SPELL_SUMMON"] = function(a4,a5) return format("spell=%s %s", s(a4), schoolName(a5 or 0)) end argFormatters["SPELL_RESURRECT"] = function(a4,a5) return format("spell=%s %s", s(a4), schoolName(a5 or 0)) end argFormatters["SPELL_INSTAKILL"] = function(a4,a5) return format("spell=%s %s", s(a4), schoolName(a5 or 0)) end -- Pattern E — base (no extra args) argFormatters["UNIT_DIED"] = function() return "" end argFormatters["PARTY_KILL"] = function() return "" end -- Pattern F — spell + string + amount argFormatters["SPELL_AURA_APPLIED_DOSE"] = function(a4,a5,a6,a7) return format("spell=%s %s %s x%s", s(a4), schoolName(a5 or 0), s(a6), s(a7)) end argFormatters["SPELL_AURA_REMOVED_DOSE"] = function(a4,a5,a6,a7) return format("spell=%s %s %s x%s", s(a4), schoolName(a5 or 0), s(a6), s(a7)) end -- Pattern G — dispel/interrupt argFormatters["SPELL_DISPEL"] = function(a4,a5,a6,a7,a8) return format("spell=%s extra=%s %s %s", s(a4), s(a6), schoolName(a7 or 0), s(a8)) end argFormatters["SPELL_INTERRUPT"] = function(a4,a5,a6,a7) return format("spell=%s extra=%s %s", s(a4), s(a6), schoolName(a7 or 0)) end argFormatters["SPELL_AURA_BROKEN_SPELL"] = function(a4,a5,a6,a7,a8) return format("spell=%s extra=%s %s %s", s(a4), s(a6), schoolName(a7 or 0), s(a8)) end -- Pattern H — environmental argFormatters["ENVIRONMENTAL_DAMAGE"] = function(a4,a5,a6,a7) return format("env=%s amt=%s %s abs=%s", s(a4), s(a5), schoolName(a6 or 0), s(a7)) end -- New subevents argFormatters["DAMAGE_SPLIT"] = function(a4,a5,a6,a7,a8,a9,a10) return fmtSpellDmg(a4,a5,a6,a7,a8,a9,a10) end argFormatters["SPELL_DISPEL_FAILED"] = function(a4,a5) return format("spell=%s %s", s(a4), schoolName(a5 or 0)) end argFormatters["UNIT_DESTROYED"] = function() return "" end -- ============================================================================ -- Chat output handlers (preserved from original) -- ============================================================================ local chatHandlers = {} chatHandlers["SPELL_DAMAGE"] = function(src, dst, spellId, amount, school, resisted, absorbed, blocked, critical) local crit = critical == 1 and " (CRIT)" or "" DEFAULT_CHAT_FRAME:AddMessage(format("|cffff0000SPELL_DAMAGE|r %s -> %s: spell %d for %d %s%s", src, dst, spellId, amount, schoolName(school), crit)) end chatHandlers["RANGE_DAMAGE"] = function(src, dst, spellId, amount, school, resisted, absorbed, blocked, critical) local crit = critical == 1 and " (CRIT)" or "" DEFAULT_CHAT_FRAME:AddMessage(format("|cffff0000RANGE_DAMAGE|r %s -> %s: spell %d for %d %s%s", src, dst, spellId, amount, schoolName(school), crit)) end chatHandlers["SWING_DAMAGE"] = function(src, dst, amount, school, resisted, absorbed, blocked, critical, flags) local extra = "" if critical == 1 then extra = extra .. " CRIT" end if flags then if math.mod(flags, 2) == 1 then extra = extra .. " GLANCING" end if math.mod(math.floor(flags / 2), 2) == 1 then extra = extra .. " CRUSHING" end if math.mod(math.floor(flags / 4), 2) == 1 then extra = extra .. " (OH)" end end DEFAULT_CHAT_FRAME:AddMessage(format("|cffff0000SWING_DAMAGE|r %s -> %s: %d%s", src, dst, amount, extra)) end chatHandlers["SWING_MISSED"] = function(src, dst, missType) DEFAULT_CHAT_FRAME:AddMessage(format("|cffaaaaaaSWING_MISSED|r %s -> %s: %s", src, dst, missType)) end chatHandlers["SPELL_MISSED"] = function(src, dst, spellId, spellSchool, missType) DEFAULT_CHAT_FRAME:AddMessage(format("|cffaaaaaaSPELL_MISSED|r %s -> %s: spell %d %s", src, dst, spellId, missType)) end chatHandlers["RANGE_MISSED"] = function(src, dst, spellId, spellSchool, missType) DEFAULT_CHAT_FRAME:AddMessage(format("|cfffaaaaaaRANGE_MISSED|r %s -> %s: spell %d %s", src, dst, spellId, missType)) end chatHandlers["ENVIRONMENTAL_DAMAGE"] = function(src, dst, envType, amount, school, absorbed) DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800ENVIRONMENTAL_DAMAGE|r %s: %s for %d", dst, envType, amount)) end chatHandlers["DAMAGE_SHIELD"] = function(src, dst, spellId, amount, school) DEFAULT_CHAT_FRAME:AddMessage(format("|cffff4400DAMAGE_SHIELD|r %s -> %s: %d %s", src, dst, amount, schoolName(school))) end chatHandlers["SPELL_HEAL"] = function(src, dst, spellId, amount, overheal, absorbed, critical) local crit = critical == 1 and " (CRIT)" or "" DEFAULT_CHAT_FRAME:AddMessage(format("|cff00ff00SPELL_HEAL|r %s -> %s: spell %d for %d%s", src, dst, spellId, amount, crit)) end chatHandlers["SPELL_PERIODIC_HEAL"] = function(src, dst, spellId, amount) DEFAULT_CHAT_FRAME:AddMessage(format("|cff00cc00SPELL_PERIODIC_HEAL|r %s -> %s: spell %d for %d", src, dst, spellId, amount)) end chatHandlers["SPELL_PERIODIC_DAMAGE"] = function(src, dst, spellId, amount, school) DEFAULT_CHAT_FRAME:AddMessage(format("|cffcc0000SPELL_PERIODIC_DAMAGE|r %s -> %s: spell %d for %d %s", src, dst, spellId, amount, schoolName(school))) end chatHandlers["SPELL_ENERGIZE"] = function(src, dst, spellId, amount, powerType) local pname = powerNames[powerType] or "Unknown" DEFAULT_CHAT_FRAME:AddMessage(format("|cff4488ffSPELL_ENERGIZE|r %s -> %s: spell %d +%d %s", src, dst, spellId, amount, pname)) end chatHandlers["SPELL_PERIODIC_ENERGIZE"] = function(src, dst, spellId, amount, overEnergize, powerType) local pname = powerNames[powerType] or "Unknown" DEFAULT_CHAT_FRAME:AddMessage(format("|cff4488ffSPELL_PERIODIC_ENERGIZE|r %s -> %s: spell %d +%d %s", src, dst, spellId, amount, pname)) end chatHandlers["SPELL_PERIODIC_LEECH"] = function(src, dst, spellId, amount, school, gained) DEFAULT_CHAT_FRAME:AddMessage(format("|cff8844ffSPELL_PERIODIC_LEECH|r %s -> %s: spell %d drained %d, gained %d", src, dst, spellId, amount, gained or 0)) end chatHandlers["SPELL_PERIODIC_DRAIN"] = function(src, dst, spellId, amount, powerType, extraAmount) local pname = powerNames[powerType] or "Unknown" DEFAULT_CHAT_FRAME:AddMessage(format("|cff8844ffSPELL_PERIODIC_DRAIN|r %s -> %s: spell %d drained %d %s, gained %d", src, dst, spellId, amount, pname, extraAmount or 0)) end chatHandlers["SPELL_AURA_APPLIED"] = function(src, dst, spellId, spellSchool, auraType) DEFAULT_CHAT_FRAME:AddMessage(format("|cff8888ffSPELL_AURA_APPLIED|r %s: spell %d (%s, %s)", dst, spellId, auraType, schoolName(spellSchool))) end chatHandlers["SPELL_AURA_REMOVED"] = function(src, dst, spellId, spellSchool, auraType) DEFAULT_CHAT_FRAME:AddMessage(format("|cff6666ccSPELL_AURA_REMOVED|r %s: spell %d (%s)", dst, spellId, auraType)) end chatHandlers["SPELL_AURA_APPLIED_DOSE"] = function(src, dst, spellId, spellSchool, auraType, stacks) DEFAULT_CHAT_FRAME:AddMessage(format("|cff8888ffSPELL_AURA_APPLIED_DOSE|r %s: spell %d x%d (%s)", dst, spellId, stacks, auraType)) end chatHandlers["SPELL_AURA_REMOVED_DOSE"] = function(src, dst, spellId, spellSchool, auraType, stacks) DEFAULT_CHAT_FRAME:AddMessage(format("|cff6666ccSPELL_AURA_REMOVED_DOSE|r %s: spell %d x%d (%s)", dst, spellId, stacks, auraType)) end chatHandlers["SPELL_AURA_REFRESH"] = function(src, dst, spellId, spellSchool, auraType) DEFAULT_CHAT_FRAME:AddMessage(format("|cff7777ddSPELL_AURA_REFRESH|r %s: spell %d (%s)", dst, spellId, auraType)) end chatHandlers["SPELL_AURA_BROKEN"] = function(src, dst, spellId, spellSchool, auraType) DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800SPELL_AURA_BROKEN|r %s -> %s: spell %d broken by melee (%s)", src, dst, spellId, auraType)) end chatHandlers["SPELL_AURA_BROKEN_SPELL"] = function(src, dst, spellId, spellSchool, extraSpellId, extraSchool, auraType) DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800SPELL_AURA_BROKEN_SPELL|r %s -> %s: spell %d broken by spell %d (%s)", src, dst, spellId, extraSpellId, auraType)) end chatHandlers["SPELL_CAST_START"] = function(src, dst, spellId, spellSchool) DEFAULT_CHAT_FRAME:AddMessage(format("|cffffff00SPELL_CAST_START|r %s: spell %d (%s)", src, spellId, schoolName(spellSchool))) end chatHandlers["SPELL_CAST_SUCCESS"] = function(src, dst, spellId) DEFAULT_CHAT_FRAME:AddMessage(format("|cffffff00SPELL_CAST_SUCCESS|r %s: spell %d", src, spellId)) end chatHandlers["SPELL_CAST_FAILED"] = function(src, dst, spellId, spellSchool, failedType) DEFAULT_CHAT_FRAME:AddMessage(format("|cffff4444SPELL_CAST_FAILED|r %s: spell %d (%s)", src, spellId, failedType)) end chatHandlers["SPELL_INTERRUPT"] = function(src, dst, spellId, spellSchool, extraSpellId, extraSchool) DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800SPELL_INTERRUPT|r %s interrupted %s: spell %d (%s)", src, dst, extraSpellId, schoolName(extraSchool))) end chatHandlers["SPELL_DISPEL"] = function(src, dst, spellId, spellSchool, extraSpellId, extraSchool, auraType) DEFAULT_CHAT_FRAME:AddMessage(format("|cff88ffffSPELL_DISPEL|r %s dispelled %s: spell %d (%s, %s)", src, dst, extraSpellId, auraType, schoolName(extraSchool))) end chatHandlers["SPELL_EXTRA_ATTACKS"] = function(src, dst, spellId) DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800SPELL_EXTRA_ATTACKS|r %s: spell %d", src, spellId)) end chatHandlers["SPELL_SUMMON"] = function(src, dst, spellId) DEFAULT_CHAT_FRAME:AddMessage(format("|cff88ff88SPELL_SUMMON|r %s summoned %s: spell %d", src, dst, spellId)) end chatHandlers["SPELL_RESURRECT"] = function(src, dst, spellId) DEFAULT_CHAT_FRAME:AddMessage(format("|cff88ff88SPELL_RESURRECT|r %s resurrected %s: spell %d", src, dst, spellId)) end chatHandlers["SPELL_INSTAKILL"] = function(src, dst, spellId) DEFAULT_CHAT_FRAME:AddMessage(format("|cffff0000SPELL_INSTAKILL|r %s killed %s: spell %d", src, dst, spellId)) end chatHandlers["UNIT_DIED"] = function(src, dst) DEFAULT_CHAT_FRAME:AddMessage(format("|cff888888UNIT_DIED|r %s", dst)) end chatHandlers["PARTY_KILL"] = function(src, dst) DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800PARTY_KILL|r %s killed %s", src, dst)) end chatHandlers["SPELL_PERIODIC_MISSED"] = function(src, dst, spellId, spellSchool, missType) DEFAULT_CHAT_FRAME:AddMessage(format("|cffaaaaaaSPELL_PERIODIC_MISSED|r %s -> %s: spell %d %s", src, dst, spellId, missType)) end chatHandlers["DAMAGE_SHIELD_MISSED"] = function(src, dst, spellId, spellSchool, missType) DEFAULT_CHAT_FRAME:AddMessage(format("|cffaaaaaaDAMAGE_SHIELD_MISSED|r %s -> %s: spell %d %s", src, dst, spellId, missType)) end chatHandlers["DAMAGE_SPLIT"] = function(src, dst, spellId, amount, school) DEFAULT_CHAT_FRAME:AddMessage(format("|cffff6600DAMAGE_SPLIT|r %s -> %s: spell %d for %d %s", src, dst, spellId, amount, schoolName(school))) end chatHandlers["SPELL_DISPEL_FAILED"] = function(src, dst, spellId, spellSchool) DEFAULT_CHAT_FRAME:AddMessage(format("|cff888888SPELL_DISPEL_FAILED|r %s -> %s: spell %d resisted", src, dst, spellId)) end chatHandlers["UNIT_DESTROYED"] = function(src, dst) DEFAULT_CHAT_FRAME:AddMessage(format("|cff666666UNIT_DESTROYED|r %s", dst)) end -- ============================================================================ -- UI — Draggable popup tracker -- ============================================================================ local ROW_HEIGHT = 14 local FRAME_WIDTH = 420 local VISIBLE_ROWS = 25 local FRAME_HEIGHT = ROW_HEIGHT * VISIBLE_ROWS + 52 -- title + counter + padding local main = CreateFrame("Frame", "DPSLogTracker", UIParent) main:SetWidth(FRAME_WIDTH) main:SetHeight(FRAME_HEIGHT) main:SetPoint("CENTER", UIParent, "CENTER", 300, 0) main:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 }, }) main:SetBackdropColor(0, 0, 0, 0.85) main:EnableMouse(true) main:SetMovable(true) main:RegisterForDrag("LeftButton") main:SetScript("OnDragStart", function() main:StartMoving() end) main:SetScript("OnDragStop", function() main:StopMovingOrSizing() end) main:SetFrameStrata("DIALOG") main:SetClampedToScreen(true) -- Title local title = main:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") title:SetPoint("TOPLEFT", main, "TOPLEFT", 12, -10) title:SetText("DPS Log Tracker") title:SetTextColor(1, 0.82, 0) -- Close button local closeBtn = CreateFrame("Button", nil, main, "UIPanelCloseButton") closeBtn:SetPoint("TOPRIGHT", main, "TOPRIGHT", -2, -2) closeBtn:SetScript("OnClick", function() main:Hide() end) -- Counter local counter = main:CreateFontString(nil, "OVERLAY", "GameFontNormal") counter:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -2) counter:SetTextColor(0.7, 0.7, 0.7) local function updateCounter() counter:SetText(format("Seen: %d/%d", seenCount, NUM_SUBEVENTS)) end updateCounter() -- Scroll frame local scrollFrame = CreateFrame("ScrollFrame", "DPSLogTrackerScroll", main, "FauxScrollFrameTemplate") scrollFrame:SetPoint("TOPLEFT", main, "TOPLEFT", 8, -48) scrollFrame:SetPoint("BOTTOMRIGHT", main, "BOTTOMRIGHT", -28, 8) -- Row frames local rows = {} for i = 1, VISIBLE_ROWS do local row = CreateFrame("Frame", nil, main) row:SetWidth(FRAME_WIDTH - 40) row:SetHeight(ROW_HEIGHT) row:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", 2, -((i - 1) * ROW_HEIGHT)) local check = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") check:SetPoint("LEFT", row, "LEFT", 0, 0) check:SetWidth(20) check:SetJustifyH("LEFT") local nameStr = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") nameStr:SetPoint("LEFT", check, "RIGHT", 0, 0) nameStr:SetWidth(FRAME_WIDTH - 60) nameStr:SetJustifyH("LEFT") row.check = check row.nameStr = nameStr rows[i] = row end local function updateRows() local offset = FauxScrollFrame_GetOffset(scrollFrame) for i = 1, VISIBLE_ROWS do local idx = offset + i local row = rows[i] if idx <= NUM_SUBEVENTS then row:Show() local displayName = SUBEVENTS[idx][1] if seen[idx] then row.check:SetText("|cff00ff00x|r") local argStr = seenArgs[idx] if argStr and argStr ~= "" then row.nameStr:SetText("|cffffffff" .. displayName .. "|r |cffaaaaaa" .. argStr .. "|r") else row.nameStr:SetText("|cffffffff" .. displayName .. "|r") end else row.check:SetText("|cff666666-|r") row.nameStr:SetText("|cff666666" .. displayName .. "|r") end else row:Hide() end end FauxScrollFrame_Update(scrollFrame, NUM_SUBEVENTS, VISIBLE_ROWS, ROW_HEIGHT) end scrollFrame:SetScript("OnVerticalScroll", function() FauxScrollFrame_OnVerticalScroll(ROW_HEIGHT, updateRows) end) updateRows() -- Reset button local resetBtn = CreateFrame("Button", nil, main, "UIPanelButtonTemplate") resetBtn:SetWidth(60) resetBtn:SetHeight(18) resetBtn:SetPoint("TOPRIGHT", closeBtn, "TOPLEFT", -4, -6) resetBtn:SetText("Reset") resetBtn:SetScript("OnClick", function() for i = 1, NUM_SUBEVENTS do seen[i] = nil seenArgs[i] = nil end seenCount = 0 updateCounter() updateRows() DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00DPSLog Tracker|r reset.") end) -- ============================================================================ -- Event dispatcher -- ============================================================================ local function print(msg) DEFAULT_CHAT_FRAME:AddMessage(msg) end local eventFrame = CreateFrame("Frame") eventFrame:RegisterEvent("COMBAT_LOG_EVENT") eventFrame:SetScript("OnEvent", function() -- if event == "PLAYER_LOGIN" then -- eventFrame:RegisterEvent("COMBAT_LOG_EVENT") -- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00DPSLog|r registered COMBAT_LOG_EVENT") -- return -- end local subevent = arg1 if not subevent then return end -- Collect args into a table for variant field lookup local args = { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 } -- Check all variant entries for this subevent local indices = subeventIndices[subevent] if not indices then return end local any_new = false for _, idx in ipairs(indices) do if not seen[idx] then local entry = SUBEVENTS[idx] local varField = entry[3] local varValue = entry[4] local matches = true if varField then -- Check specific arg matches variant value -- For swing flags (bitfield), check if the bit is set if subevent == "SWING_DAMAGE" and varField == 10 then -- flags is arg10, check bitfield local flags = args[varField] or 0 if varValue == 1 then matches = (math.mod(flags, 2) == 1) -- glancing elseif varValue == 2 then matches = (math.mod(math.floor(flags / 2), 2) == 1) -- crushing elseif varValue == 4 then matches = (math.mod(math.floor(flags / 4), 2) == 1) -- offhand else matches = false end else matches = (args[varField] == varValue) end end if matches then seen[idx] = true seenCount = seenCount + 1 local formatter = argFormatters[subevent] if formatter then seenArgs[idx] = formatter(arg4, arg5, arg6, arg7, arg8, arg9, arg10) else seenArgs[idx] = "" end any_new = true -- Chat output (first occurrence of this variant) local chatHandler = chatHandlers[subevent] if chatHandler then chatHandler(arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) end end end end if any_new then updateCounter() updateRows() end end) -- ============================================================================ -- Slash command -- ============================================================================ SLASH_DPSLOG1 = "/dpslog" SlashCmdList["DPSLOG"] = function(msg) if main:IsVisible() then main:Hide() else main:Show() end end DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00DPSLog Tracker|r loaded — /dpslog to toggle window")