WoWInterface SVN Critline

[/] [trunk/] [core.lua] - Rev 10

Go to most recent revision | Compare with Previous | Blame | View Log

local addonName, Critline = ...
_G.Critline = Critline

local L = LibStub("AceLocale-3.0"):GetLocale(addonName)
local templates = Critline.templates
local playerClass = select(2, UnitClass("player"))
local debugging

-- auto attack spell
local AUTOATK = GetSpellInfo(6603)

-- local references to commonly used functions and variables for faster access
local HasPetUI = HasPetUI
local tonumber = tonumber
local CombatLog_Object_IsA = CombatLog_Object_IsA
local bor = bit.bor
local band = bit.band

local COMBATLOG_FILTER_MINE = COMBATLOG_FILTER_MINE
local COMBATLOG_FILTER_MY_PET = COMBATLOG_FILTER_MY_PET
local COMBATLOG_OBJECT_REACTION_FRIENDLY = COMBATLOG_OBJECT_REACTION_FRIENDLY
local COMBATLOG_OBJECT_REACTION_HOSTILE = COMBATLOG_OBJECT_REACTION_HOSTILE
local COMBATLOG_OBJECT_CONTROL_PLAYER = COMBATLOG_OBJECT_CONTROL_PLAYER
local COMBATLOG_OBJECT_TYPE_GUARDIAN = COMBATLOG_OBJECT_TYPE_GUARDIAN


local treeNames = {
        dmg = L["damage"],
        heal = L["healing"],
        pet = L["pet"],
}
Critline.treeNames = treeNames


Critline.icons = {
        dmg = "Interface\\Icons\\Ability_SteelMelee",
        heal = "Interface\\Icons\\Spell_Holy_FlashHeal",
        pet = "Interface\\Icons\\Ability_Hunter_Pet_Bear",
}


-- non hunter pets whose damage we may want to register
local classPets = {
        [510] = true,   -- Water Elemental
        [11859] = true, -- Doomguard
        [15438] = true, -- Greater Fire Elemental
        [27829] = true, -- Ebon Gargoyle
        [29264] = true, -- Spirit Wolf
        [37994] = true, -- Water Elemental (glyphed)
}


local swingDamage = function(amount, _, school, resisted, _, _, critical)
        return AUTOATK, amount, resisted, critical, school
end

local spellDamage = function(_, spellName, _, amount, _, school, resisted, _, _, critical)
        return spellName, amount, resisted, critical, school
end

local healing = function(_, spellName, _, amount, _, _, critical)
        return spellName, amount, 0, critical, 0
end


local combatEvents = {
        SWING_DAMAGE = swingDamage,
        RANGE_DAMAGE = spellDamage,
        SPELL_DAMAGE = spellDamage,
        SPELL_PERIODIC_DAMAGE = spellDamage,
        SPELL_HEAL = healing,
        SPELL_PERIODIC_HEAL = healing,
}


local recordSorters = {
        -- alpha: sort by name
        alpha = function(a, b)
                if a == b then return end
                if a.spellName == b.spellName then
                        -- sort DoT entries after non DoT
                        return b.isPeriodic
                else
                        return a.spellName < b.spellName
                end
        end,
        -- crit: sort by crit > normal > name
        crit = function(a, b)
                if a == b then return end
                local critA, critB = (a.crit and a.crit.amount or 0), (b.crit and b.crit.amount or 0)
                if critA == critB then
                        -- equal crit amounts, sort by normal amount instead
                        local normalA, normalB = (a.normal and a.normal.amount or 0), (b.normal and b.normal.amount or 0)
                        if normalA == normalB then
                                -- equal normal amounts, sort by name instead
                                if a.spellName == b.spellName then
                                        return b.isPeriodic
                                else
                                        return a.spellName < b.spellName
                                end
                        else
                                return normalA > normalB
                        end
                else
                        return critA > critB
                end
        end,
        -- normal: sort by normal > crit > name
        normal = function(a, b)
                if a == b then return end
                local normalA, normalB = (a.normal and a.normal.amount or 0), (b.normal and b.normal.amount or 0)
                if normalA == normalB then
                        local critA, critB = (a.crit and a.crit.amount or 0), (b.crit and b.crit.amount or 0)
                        if critA == critB then
                                if a.spellName == b.spellName then
                                        return b.isPeriodic
                                else
                                        return a.spellName < b.spellName
                                end
                        else
                                return critA > critB
                        end
                else
                        return normalA > normalB
                end
        end,
}


local callbacks = LibStub("CallbackHandler-1.0"):New(Critline)
Critline.callbacks = callbacks


-- this will hold the text for the summary tooltip
local tooltips = {dmg = {}, heal = {}, pet = {}}


Critline.eventFrame = CreateFrame("Frame")
function Critline:RegisterEvent(event)
        self.eventFrame:RegisterEvent(event)
end
function Critline:UnregisterEvent(event)
        self.eventFrame:UnregisterEvent(event)
end
Critline:RegisterEvent("ADDON_LOADED")
Critline:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
Critline.eventFrame:SetScript("OnEvent", function(self, event, ...)
        return Critline[event] and Critline[event](Critline, ...)
end)


local config = templates:CreateConfigFrame(addonName, nil, true)


do
        local options = {}
        Critline.options = options
        
        local function toggleTree(self)
                local display = Critline.display
                if display then
                        display:UpdateTree(self.setting)
                end
        end
        
        local checkButtons = {
                db = {},
                percharDB = {},
                {
                        text = L["Record damage"],
                        tooltipText = L["Check to enable damage events to be recorded."],
                        setting = "dmg",
                        perchar = true,
                        func = toggleTree,
                },
                {
                        text = L["Record healing"],
                        tooltipText = L["Check to enable healing events to be recorded."],
                        setting = "heal",
                        perchar = true,
                        func = toggleTree,
                },
                {
                        text = L["Record pet damage"],
                        tooltipText = L["Check to enable pet damage events to be recorded."],
                        setting = "pet",
                        perchar = true,
                        func = toggleTree,
                },
                {
                        text = L["Record PvE"],
                        tooltipText = L["Disable to ignore records where the target is an NPC."],
                        setting = "PvE",
                        gap = 16,
                },
                {
                        text = L["Record PvP"],
                        tooltipText = L["Disable to ignore records where the target is a player."],
                        setting = "PvP",
                },
                {
                        text = L["Chat output"],
                        tooltipText = L["Prints new record notifications to the chat frame."],
                        setting = "chatOutput",
                },
                {
                        text = L["Play sound"],
                        tooltipText = L["Plays a sound on a new record."],
                        setting = "sound",
                },
                {
                        text = L["Screenshot"],
                        tooltipText = L["Saves a screenshot on a new record."],
                        setting = "screenshot",
                        newColumn = true,
                },
                {
                        text = L["Detailed tooltip"],
                        tooltipText = L["Use detailed format in the summary tooltip."],
                        setting = "detailedTooltip",
                        func = function(self) Critline:UpdateTooltips() end,
                },
                {
                        text = L["Ignore vulnerability"],
                        tooltipText = L["Enable to ignore additional damage due to vulnerability."],
                        setting = "ignoreVulnerability",
                },
        }
        
        options.checkButtons = checkButtons
        
        for i, v in ipairs(checkButtons) do
                local btn = templates:CreateCheckButton(config, v)
                if i == 1 then
                        btn:SetPoint("TOPLEFT", config.title, "BOTTOMLEFT", -2, -16)
                elseif v.newColumn then
                        btn:SetPoint("TOPLEFT", config.title, "BOTTOM", 0, -16)
                else
                        btn:SetPoint("TOP", checkButtons[i - 1], "BOTTOM", 0, -(v.gap or 8))
                end
                btn.module = Critline
                local btns = checkButtons[btn.db]
                btns[#btns + 1] = btn
                checkButtons[i] = btn
        end
        
        -- summary sort dropdown
        local menu = {
                onClick = function(self)
                        self.owner:SetSelectedValue(self.value)
                        Critline.db.profile.tooltipSort = self.value
                        Critline:UpdateTooltips()
                end,
                {
                        text = L["Alphabetically"],
                        value = "alpha",
                },
                {
                        text = L["By normal record"],
                        value = "normal",
                },
                {
                        text = L["By crit record"],
                        value = "crit",
                },
        }
        
        local sorting = templates:CreateDropDownMenu("CritlineTooltipSorting", config, menu)
        sorting:SetFrameWidth(120)
        sorting:SetPoint("TOPLEFT", checkButtons[#checkButtons], "BOTTOMLEFT", -15, -24)
        sorting.label:SetText(L["Tooltip sorting:"])
        
        options.tooltipSort = sorting
end


SlashCmdList.CRITLINE = function(msg)
        msg = msg:trim():lower()
        if msg == "debug" then
                Critline:ToggleDebug()
        elseif msg == "reset" then
                local display = Critline.display
                if display then
                        display:ClearAllPoints()
                        display:SetPoint("CENTER")
                end
        else
                Critline:OpenConfig()
        end
end

SLASH_CRITLINE1 = "/critline"
SLASH_CRITLINE2 = "/cl"


local defaults = {
        profile = {
                PvE = true,
                PvP = true,
                chatOutput = false,
                sound = false,
                screenshot = false,
                detailedTooltip = false,
                ignoreVulnerability = true,
                tooltipSort = "normal",
        },
}


-- which trees are enabled by default for a given class
local treeDefaults = {
        DEATHKNIGHT     = {dmg = true, heal = false, pet = false},
        DRUID           = {dmg = true, heal = true, pet = false},
        HUNTER          = {dmg = true, heal = false, pet = true},
        MAGE            = {dmg = true, heal = false, pet = false},
        PALADIN         = {dmg = true, heal = true, pet = false},
        PRIEST          = {dmg = true, heal = true, pet = false},
        ROGUE           = {dmg = true, heal = false, pet = false},
        SHAMAN          = {dmg = true, heal = true, pet = false},
        WARLOCK         = {dmg = true, heal = false, pet = true},
        WARRIOR         = {dmg = true, heal = false, pet = false},
}

function Critline:ADDON_LOADED(addon)
        if addon == addonName then
                local AceDB = LibStub("AceDB-3.0")
                local db = AceDB:New("CritlineDB", defaults, nil)
                self.db = db
                
                local percharDefaults = {
                        profile = treeDefaults[playerClass],
                }
                
                percharDefaults.profile.spells = {
                        dmg = {},
                        heal = {},
                        pet = {},
                }
                
                local percharDB = AceDB:New("CritlinePerCharDB", percharDefaults)
                self.percharDB = percharDB
                
                -- dual spec support
                local LibDualSpec = LibStub("LibDualSpec-1.0")
                LibDualSpec:EnhanceDatabase(self.db, addonName)
                LibDualSpec:EnhanceDatabase(self.percharDB, addonName)
                
                db.RegisterCallback(self, "OnProfileChanged", "LoadSettings")
                db.RegisterCallback(self, "OnProfileCopied", "LoadSettings")
                db.RegisterCallback(self, "OnProfileReset", "LoadSettings")
                
                percharDB.RegisterCallback(self, "OnProfileChanged", "LoadPerCharSettings")
                percharDB.RegisterCallback(self, "OnProfileCopied", "LoadPerCharSettings")
                percharDB.RegisterCallback(self, "OnProfileReset", "LoadPerCharSettings")
                
                self:UnregisterEvent("ADDON_LOADED")
                callbacks:Fire("AddonLoaded")
                
                -- import old records and mob filter
                if CritlineSettings then
                        for i, v in ipairs({"dmg","heal","pet"}) do
                                percharDB.profile.spells[v] = CritlineDB[v]
                                CritlineDB[v] = nil
                        end
                        
                        if self.filters and CritlineMobFilter then
                                self.filters.db.global.mobs = CritlineMobFilter
                        end
                end
                
                self:LoadSettings()
                self:LoadPerCharSettings()
                
                self.ADDON_LOADED = nil
        end
end


function Critline:COMBAT_LOG_EVENT_UNFILTERED(timestamp, eventType, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...)
        -- we seem to get events with standard arguments equal to nil, so they need to be ignored
        if not (timestamp and eventType) then
                self:Debug("nil errors on start")
                return
        end

        -- if we don't have a destName (who we hit or healed) and we don't have a sourceName (us or our pets) then we leave
        if not (destName or sourceName) then
                self:Debug("nil source/dest")
                return
        end
        
        -- check for filtered auras
        if self.filters then
                if self.filters:IsAuraEvent(eventType, ..., sourceFlags, destFlags, destGUID) then
                        return
                end
        end
        
        local isPet
        
        -- if sourceGUID is not us or our pet, we leave
        if not CombatLog_Object_IsA(sourceFlags, COMBATLOG_FILTER_MINE) then
                local isMyPet = CombatLog_Object_IsA(sourceFlags, COMBATLOG_FILTER_MY_PET)
                local isGuardian = band(sourceFlags, COMBATLOG_OBJECT_TYPE_GUARDIAN) ~= 0
                -- only register if it's a real pet, or a guardian tree pet that's included in the filter
                if isMyPet and ((not isGuardian and HasPetUI()) or classPets[tonumber(sourceGUID:sub(6, 12), 16)]) then
                        isPet = true
                        -- self:Debug(format("This is my pet (%s)", sourceName))
                else
                        -- self:Debug("This is not me, my trap or my pet; return.")
                        return
                end
        else
                -- self:Debug(format("This is me or my trap (%s)", sourceName))
        end
        
        if not combatEvents[eventType] then
                return
        end
        
        local isHeal
        local isPeriodic
        if eventType:find("_HEAL$") then
                isHeal = true
                if isPet then
                        return
                end
        end
        if eventType:find("_PERIODIC_") then
                isPeriodic = true
        end
        
        local spellName, amount, resisted, critical, school = combatEvents[eventType](...)
        
        -- below are some checks to see if we want to register the hit at all
        if amount <= 0 then
                self:Debug(format("Amount <= 0. (%s) Return.", self:GetFullSpellName(tree, spellName, isPeriodic)))
                return
        end

        local tree = "dmg"
        
        if isPet then
                tree = "pet"
        elseif isHeal then
                tree = "heal"
        end
        
        local passed, isFiltered, targetLevel
        if self.filters then
                passed, isFiltered, targetLevel = self.filters:SpellPassesFilters(tree, spellName, ..., isPeriodic, destGUID, destName, school)
                if not passed then
                        return
                end
        end
        
        local isPlayer = band(destFlags, COMBATLOG_OBJECT_CONTROL_PLAYER) ~= 0
        local friendlyFire = band(destFlags, COMBATLOG_OBJECT_REACTION_FRIENDLY) ~= 0
        local hostileTarget = band(destFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
        
        if not (isPlayer or self.db.profile.PvE or isHeal) then
                self:Debug(format("Target (%s) is an NPC and PvE damage is not registered.", destName))
                return
        end
        
        if isPlayer and not (self.db.profile.PvP or isHeal or friendlyFire) then
                self:Debug(format("Target (%s) is a player and PvP damage is not registered.", destName))
                return
        end
        
        -- ignore damage done to friendly targets
        if friendlyFire and not isHeal then
                self:Debug(format("Friendly fire (%s, %s).", spellName, destName))
                return
        end
        
        -- ignore healing done to hostile targets
        if hostileTarget and isHeal then
                self:Debug(format("Healing hostile target (%s, %s).", spellName, destName))
                return
        end
        
        -- we only want damage spells from the pet
        if isHeal and isPet then
                self:Debug("Pet healing. Return.")
                return
        end

        -- exit if not recording tree dmg
        if not self.percharDB.profile[tree] then
                self:Debug(format("Not recording this tree (%s). Return.", tree))
                return
        end
        
        -- ignore vulnerability damage if necessary
        if self.db.profile.ignoreVulnerability and resisted and resisted < 0 then
                amount = amount + resisted
                self:Debug(format("%d vulnerability damage ignored for a real value of %d.", abs(resisted), amount))
        end
        
        local hitType = critical and "crit" or "normal"
        
        local data = self:GetSpellInfo(tree, spellName, isPeriodic)
        
        -- create spell database entries as required
        if not data then
                self:Debug(format("Creating data for %s (%s)", self:GetFullSpellName(tree, spellName, isPeriodic), tree))
                data = {
                        spellName = spellName,
                        isPeriodic = isPeriodic,
                }
                self:AddSpell(tree, data)
                self:UpdateSpells(tree)
        end
        
        if not data[hitType] then
                data[hitType] = {amount = 0}
        end
        
        data = data[hitType]

        if amount > data.amount then
                local oldAmount = data.amount
                data.amount = amount
                data.target = destName
                data.targetLevel = targetLevel
                data.isPvPTarget = isPlayer
                
                if not isFiltered then
                        self:NewRecord(self:GetFullSpellName(tree, spellName, isPeriodic), amount, critical, oldAmount)
                end
                
                self:UpdateRecords(tree, isFiltered)
        end
end


function Critline:Message(msg)
        if msg then
                DEFAULT_CHAT_FRAME:AddMessage("|cffffff00Critline:|r "..msg)
        end
end


function Critline:Debug(msg)
        if debugging then
                DEFAULT_CHAT_FRAME:AddMessage("|cff56a3ffCritlineDebug:|r "..msg)
        end
end


function Critline:ToggleDebug()
        debugging = not debugging
        self:Message("Debugging "..(debugging and "enabled" or "disabled"))
end


function Critline:OpenConfig()
        InterfaceOptionsFrame_OpenToCategory(config)
end


function Critline:LoadSettings()
        callbacks:Fire("SettingsLoaded")
        
        local options = self.options
        
        for _, btn in ipairs(options.checkButtons.db) do
                btn:LoadSetting()
        end
        
        options.tooltipSort:SetSelectedValue(self.db.profile.tooltipSort)
end


function Critline:LoadPerCharSettings()
        callbacks:Fire("PerCharSettingsLoaded")
        
        self:UpdateTooltips()
        
        for _, btn in ipairs(self.options.checkButtons.percharDB) do
                btn:LoadSetting()
        end
end


function Critline:NewRecord(spell, amount, crit, oldAmount)
        callbacks:Fire("NewRecord", spell, amount, crit, oldAmount)
        
        if self.db.profile.chatOutput then
                self:Message(format(L["New %s%s record - %d"], crit and L["critical "] or "", spell, amount))
        end
        
        if self.db.profile.sound then 
                PlaySound("LEVELUP", 1, 1, 0, 1, 3) 
        end
        
        if self.db.profile.screenshot then 
                TakeScreenshot() 
        end
end


-- return spell table from database, given tree, spell name and isPeriodic value
function Critline:GetSpellInfo(tree, spellName, periodic)
        for i, v in ipairs(self.percharDB.profile.spells[tree]) do
                if v.spellName == spellName and v.isPeriodic == periodic then
                        return v, i
                end
        end
end


function Critline:GetFullSpellName(tree, spellName, isPeriodic)
        local suffix = ""
        if isPeriodic then
                if tree == "heal" then
                        suffix = L[" (HoT)"]
                else
                        suffix = L[" (DoT)"]
                end
        end
        return format("%s%s", spellName, suffix)
end


function Critline:GetFullTargetName(spell)
        local suffix = ""
        if spell.isPvPTarget then
                suffix = format(" (%s)", PVP)
        end
        return format("%s%s", spell.target, suffix)
end


-- retrieves the top, non filtered record amounts and spell names for a given tree
function Critline:GetHighest(tree)
        local normalRecord, critRecord = 0, 0
        local normalSpell, critSpell
        
        for _, data in ipairs(self.percharDB.profile.spells[tree]) do
                if not (self.filters and self.filters:IsFilteredSpell(tree, data.spellName, data.isPeriodic)) then
                        local normal = data.normal
                        if normal and normal.amount > normalRecord then
                                normalRecord = normal.amount
                                normalSpell = data.spellName
                        end
                        local crit = data.crit
                        if crit and crit.amount > critRecord then
                                critRecord = crit.amount
                                critSpell = data.spellName
                        end
                end
        end
        return normalRecord, critRecord, normalSpell, critSpell
end


function Critline:AddSpell(tree, spell)
        local spells = self.percharDB.profile.spells[tree]
        tinsert(spells, spell)
        sort(spells, recordSorters.alpha)
end


function Critline:DeleteSpell(tree, index)
        tremove(self.percharDB.profile.spells[tree], index)
end


-- this "fires" when spells are added to/removed from the database
function Critline:UpdateSpells(tree)
        if tree then
                self:UpdateTooltip(tree)
                callbacks:Fire("SpellsChanged", tree)
        else
                for k in pairs(tooltips) do
                        self:UpdateSpells(k)
                end
        end
end


-- this "fires" when a new record has been registered
function Critline:UpdateRecords(tree, isFiltered)
        if tree then
                self:UpdateTooltip(tree)
                callbacks:Fire("RecordsChanged", tree, isFiltered)
        else
                for k in pairs(tooltips) do
                        self:UpdateRecords(k, isFiltered)
                end
        end
end


function Critline:ShowTooltip(tree)
        GameTooltip:AddLine("Critline "..treeNames[tree], HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
        for i, v in ipairs(tooltips[tree]) do
                local left, right = v:match("(.+)\t(.+)")
                if left and right then
                        GameTooltip:AddDoubleLine(left, right)
                else
                        GameTooltip:AddLine(v)
                end
        end
        GameTooltip:Show()
end


function Critline:UpdateTooltips()
        for k in pairs(tooltips) do
                self:UpdateTooltip(k)
        end
end


local sortedSpells = {}

function Critline:UpdateTooltip(tree)
        local line = "   |cffc0c0c0%s:|r %s\t%s (%s)"
        
        wipe(sortedSpells)
        
        local n = 1
        
        for i, v in ipairs(self.percharDB.profile.spells[tree]) do
                if (v.normal or v.crit) and not (self.filters and self.filters:IsFilteredSpell(tree, v.spellName, v.isPeriodic)) then
                        sortedSpells[n] = {
                                spellName = v.spellName,
                                isPeriodic = v.isPeriodic,
                                normal = v.normal,
                                crit = v.crit,
                        }
                        n = n + 1
                end
        end
        
        sort(sortedSpells, recordSorters[self.db.profile.tooltipSort])
        
        local tooltip = tooltips[tree]
        wipe(tooltip)
        
        local normalRecord, critRecord = self:GetHighest(tree)
        n = 1
        
        for _, v in ipairs(sortedSpells) do
                -- if this is a DoT/HoT, and a direct entry exists, add the proper suffix
                if v.isPeriodic then
                        for _, v2 in ipairs(sortedSpells) do
                                if v2.spellName == v.spellName and not v2.isPeriodic then
                                        v.spellName = self:GetFullSpellName(tree, v.spellName, true)
                                        break
                                end
                        end
                end
                
                local normalAmount, critAmount = HIGHLIGHT_FONT_COLOR_CODE..(0)..FONT_COLOR_CODE_CLOSE, HIGHLIGHT_FONT_COLOR_CODE..(0)..FONT_COLOR_CODE_CLOSE
                
                -- color the top score amount green
                local normal = v.normal
                if normal then
                        normalAmount = (normal.amount == normalRecord and GREEN_FONT_COLOR_CODE or HIGHLIGHT_FONT_COLOR_CODE)..normal.amount..FONT_COLOR_CODE_CLOSE
                end
                
                local crit = v.crit
                if crit then
                        critAmount = (crit.amount == critRecord and GREEN_FONT_COLOR_CODE or HIGHLIGHT_FONT_COLOR_CODE)..crit.amount..FONT_COLOR_CODE_CLOSE
                end
                
                if self.db.profile.detailedTooltip then
                        tooltip[n] = v.spellName
                        if normal then
                                n = n + 1
                                local target = HIGHLIGHT_FONT_COLOR_CODE..self:GetFullTargetName(normal)..FONT_COLOR_CODE_CLOSE
                                local level = (normal.targetLevel > 0) and normal.targetLevel or "??"
                                tooltip[n] = format(line, L["Normal"], normalAmount, target, level)
                        end
                        if crit then
                                n = n + 1
                                local target = HIGHLIGHT_FONT_COLOR_CODE..self:GetFullTargetName(crit)..FONT_COLOR_CODE_CLOSE
                                local level = (crit.targetLevel > 0) and crit.targetLevel or "??"
                                tooltip[n] = format(line, L["Crit"], critAmount, target, level)
                        end
                else
                        tooltip[n] = format("%s\t%s%s", v.spellName, normalAmount, (crit and "/"..critAmount or ""))
                end
                
                n = n + 1
        end
        
        if #tooltip == 0 then
                tooltip[1] = L["No records"]
        end
end

Go to most recent revision | Compare with Previous | Blame