-- SageGearLevelTooltip.lua --[[ /console reloadui ]] ---------------------------------------------------------------------------------------------------- local Class = sage and sage.class and sage.class.Class or error("Need access to sage.class.Class") local Item = sage.item and sage.item.Item or error("Need access to sage.item.Item") local Reforgings = sage.reforge and sage.reforge.Reforgings or error("Need access to sage.item.Item") local function newClass(arg) return Class:NewClass(arg) end ---------------------------------------------------------------------------------------------------- local CachingEvaluator do local class = newClass{name="CachingEvaluator"} function class:_Initialize(instance, delegate) -- self is class self.super:_Initialize(instance) instance.delegate = delegate instance.cache = {} instance.cacheHitCount = 0 instance.cacheMissCount = 0 end function class:Name() return self.delegate:Name() end function class:Evaluate(itemLink) -- TODO: normalize link first? local cached = self.cache[itemLink] if cached then self.cacheHitCount = self.cacheHitCount + 1 return unpack(cached) end local left, leftLabel, right, rightLabel = self.delegate:Evaluate(itemLink) if left and leftLabel then self.cache[itemLink] = { left, leftLabel, right, rightLabel } self.cacheMissCount = self.cacheMissCount + 1 return left, leftLabel, right, rightLabel end end CachingEvaluator = class end ---------------------------------------------------------------------------------------------------- --[[ A base class for Evaluators which *might* need to be cached. Just adds a :Cache method which wraps self with a CachingEvaluator ]] local CachableEvaluator do local class = newClass{name="CachableEvaluator"} function class:Cache() return CachingEvaluator:New(self) end CachableEvaluator = class end ---------------------------------------------------------------------------------------------------- local ItemLevelIdCommenter do local class = newClass{name="ItemLevelIdCommenter"} function class:Comment(itemLink, tooltip) if not itemLink then return end local _,_,_,level = GetItemInfo(itemLink) local itemId = itemLink:match("item:(%d+):") itemId = itemId and tonumber(itemId) or ">>" .. itemLink:gsub("|", "!") tooltip:AddDoubleLine( "iLevel " .. (level or '??'), "id " .. itemId, class.r, class.g, class.b, 1, 1, 1) end ItemLevelIdCommenter = class end ---------------------------------------------------------------------------------------------------- local ItemIdEvaluator do local class = newClass{name="ItemIdEvaluator"} local label = {label = "id", r=0.5, g=1, b=0.5} function class:Evaluate(itemLink) if not itemLink then return end local itemId = itemLink:match("item:(%d+):") itemId = itemId and tonumber(itemId) or ">>" .. itemLink:gsub("|", "!") -- local itemId = Item:New(itemLink):ItemString() return itemId, label end ItemIdEvaluator = class end ---------------------------------------------------------------------------------------------------- local ITEM_MOD_NAME_MAP = { EMPTY_SOCKET_RED="RedSocket", EMPTY_SOCKET_BLUE="BlueSocket", EMPTY_SOCKET_YELLOW="YellowSocket", EMPTY_SOCKET_PRISMATIC="PrismaticSocket", ITEM_MOD_FIRE_RESISTANCE_SHORT="FireResist", ITEM_MOD_FROST_RESISTANCE_SHORT="FrostResist", ITEM_MOD_CRIT_RATING_SHORT="Crit", ITEM_MOD_CRIT_RANGED_RATING_SHORT="CritRanged", ITEM_MOD_DODGE_RATING_SHORT="Dodge", ITEM_MOD_EXPERTISE_RATING_SHORT="Expertise", ITEM_MOD_HASTE_RATING_SHORT="Haste", ITEM_MOD_HIT_RATING_SHORT="Hit", ITEM_MOD_MASTERY_RATING_SHORT="Mastery", ITEM_MOD_PARRY_RATING_SHORT="Parry", ITEM_MOD_RESILIENCE_RATING_SHORT="Resilience", ITEM_MOD_ATTACK_POWER_SHORT="AP", ITEM_MOD_DAMAGE_PER_SECOND_SHORT="DPS", ITEM_MOD_AGILITY_SHORT="Agility", ITEM_MOD_INTELLECT_SHORT="Intellect", ITEM_MOD_SPELL_POWER_SHORT="SP", ITEM_MOD_SPIRIT_SHORT="Spirit", ITEM_MOD_STAMINA_SHORT="Stamina", ITEM_MOD_STRENGTH_SHORT="Strength", RESISTANCE0_NAME="Armor", RESISTANCE2_NAME="FireResist", RESISTANCE4_NAME="FrostResist*", -- duplicate? } ---------------------------------------------------------------------------------------------------- local SimplePawnEvaluator do local class = newClass{name="SimplePawnEvaluator", super=CachableEvaluator} function class:_Initialize(instance, label, coefficients) --self is class self.super:_Initialize(instance) instance.coefficients = coefficients instance.label = label end function class:Evaluate(itemLink) if not itemLink then return end local stats = self:_GetItemStats(itemLink) local total = 0 local missedStat, missedStatLabel = nil, nil print("stat eval") for k,v in pairs(stats) do local stat = ITEM_MOD_NAME_MAP[k] if not stat then missedStat, missedStatLabel = v, {label=k, r=class.r, g=class.g, b=class.b} end total = total + v * (self.coefficients[stat] or 0) end if total > 0 then return math.floor(0.5 + total), self.label, missedStat, missedStatLabel elseif missedStat then return missedStat, missedStatLabel end end function class:_GetItemStats(itemLink) -- broken out to be overridable return GetItemStats(itemLink) end SimplePawnEvaluator = class end ---------------------------------------------------------------------------------------------------- local AsReforgedPawnEvaluator do local class = newClass{name="AsReforgedPawnEvaluator", super=SimplePawnEvaluator} local reforgings = Reforgings:New() function class:_GetItemStats(itemLink) local reforging = reforgings:Find(itemLink) return Item:New(itemLink):GetItemStats() end AsReforgedPawnEvaluator = class end ---------------------------------------------------------------------------------------------------- local BestReforgedEvaluator do local class = newClass{name="BestReforgedEvaluator", super=CachableEvaluator} local reforgings = Reforgings:New() local reforgingLabel = {label = "Reforge", r=0.6, g=0.6, b=0.6} function class:_Initialize(instance, label, coefficients) --self is class self.super:_Initialize(instance) instance.coefficients = coefficients instance.label = label -- handle sockets local bestStatValue = 0 for i,stat in ipairs({"Stamina", "Strength", "Agility", "Intellect", "Spirit"}) do bestStatValue = math.max(bestStatValue, instance.coefficients[stat] or 0) end local socketValue = bestStatValue * 40 -- cata blue gems instance.coefficients.RedSocket = math.max(socketValue, instance.coefficients.RedSocket or 0) instance.coefficients.BlueSocket = math.max(socketValue, instance.coefficients.BlueSocket or 0) instance.coefficients.YellowSocket = math.max(socketValue, instance.coefficients.YellowSocket or 0) end function class:_GetItemStats(itemLink) local reforging = reforgings:Find(itemLink) return Item:New(itemLink):GetItemStats() end function class:Name() return self.label and self.label.label or "" end function class:Evaluate(itemLink) if not itemLink then return end itemLink = Item:New(itemLink):WithoutMods():ItemString() local stats = GetItemStats(itemLink) -- baseline w/o reforging local rightValue, rightLabel = nil, nil -- if not high enough for reforging, show base stats local reforging = self:_ItemHighEnoughForForging(itemLink) and self:_FindOptimalReforging(stats) if reforging then reforging:AdjustStats(stats) rightValue, rightLabel = reforging:Describe(), reforgingLabel end local evaluation = self:_EvalStats(stats) if evaluation and (evaluation > 0) then return evaluation, self.label, rightValue, rightLabel end end function class:_ItemHighEnoughForForging(itemLink) _,_,_,itemLevel = GetItemInfo(itemLink) return itemLevel >= 200 end function class:_FindOptimalReforging(stats) local maxDelta, maxReforging = 0, nil for index,reforging in pairs(reforgings:All()) do local from, to = reforging:GetShortNames() local c0 = self.coefficients[ITEM_MOD_NAME_MAP[from]] or 0 local c1 = self.coefficients[ITEM_MOD_NAME_MAP[to]] or 0 if stats[from] and (not stats[to]) and (c1 > c0) then local delta = stats[from] * (c1 - c0) --don't bother to scale by 40% here if delta > maxDelta then maxDelta, maxReforging = delta, reforging end end end return maxReforging end function class:_EvalStats(stats) local total = 0 for k,v in pairs(stats) do local stat = ITEM_MOD_NAME_MAP[k] total = total + v * (self.coefficients[stat] or 0) end return math.floor(0.5 + total) end function class:_GetItemStats(itemLink) -- broken out to be overridable return GetItemStats(itemLink) end BestReforgedEvaluator = class end ---------------------------------------------------------------------------------------------------- local EvaluatorToLineAdderAdaptor do local class = newClass{name="EvaluatorToLineAdderAdaptor"} function class:_Initialize(instance, evaluator) instance.evaluator = evaluator end -- Adds lines for the given item -- @param itemLink a link for the item -- @param tooltip a callback supporting :AddLine and :AddDoubleLine with same args as a GameTooltip -- NOTE: tooltip is not guaranteed to be an actual tooltip, or support other methods function class:Comment(itemLink, tooltip) local left, leftLabel, right, rightLabel = self.evaluator:Evaluate(itemLink) local leftText = left and leftLabel and (leftLabel.label .. ": " .. left) local rightText = right and rightLabel and (rightLabel.label .. ": " .. right) if leftText and rightText then tooltip:AddDoubleLine(leftText, rightText, leftLabel.r, leftLabel.g, leftLabel.b, rightLabel.r, rightLabel.g, rightLabel.b) elseif leftText then tooltip:AddLine(leftLabel.label .. ": " .. left, leftLabel.r, leftLabel.g, leftLabel.b) end end EvaluatorToLineAdderAdaptor = class end --[[ /run function qz(...) local t,i,v={...}; if (#t)==0 then return end i=math.random(#t); v=table.remove(t, i); return v, qz(unpack(t)) end ]] ---------------------------------------------------------------------------------------------------- local SageGearLevelTooltipUpdater do local class = newClass{name="TooltipUpdater"} -- class function function class:_Initialize(instance, methodsToHook) -- no call to super needed instance.evals = {} instance.commenters = {} return instance:_HookFrameMethods(methodsToHook) end -- Updates the tooltip with all the comments from the item commenters -- @param tooltip A "tooltip" support :AddLine and :AddDoubleLine *only* function class:UpdateTooltip(tooltip) local _,itemLink = tooltip:GetItem() local tooltipAdapter = { mustShow = false, AddLine = function(adapter, ...) adapter.mustShow = true; tooltip:AddLine(...) end, AddDoubleLine = function(adapter, ...) adapter.mustShow = true; tooltip:AddDoubleLine(...) end } for _,commenter in ipairs(self.commenters) do commenter:Comment(itemLink, tooltipAdapter) end if tooltipAdapter.mustShow then tooltip:Show() end end function class:_HookFrameMethods() self:Print("SageGearLevelTooltip loaded.") return self end function class:HookTooltipScript(tooltipName) local tooltip = _G[tooltipName] or error("Tooltip not found: " .. tooltipName) local update = function(tooltip, ...) self:UpdateTooltip(tooltip) end tooltip:HookScript("OnTooltipSetItem", update) return self end function class:AddSelfEvaluator(className, e) local playerClassLocal, playerClass = UnitClass('player') if className == playerClass then self:Debug("Installing evaluator for " .. playerClassLocal .. ": " .. (e:Name())) return self:AddEvaluator(e) end return self end function class:AddEvaluator(e) return self:AddItemCommenter(EvaluatorToLineAdderAdaptor:New(e)) end -- Adds an item commenter to this tooltip annotator -- @param commenter an ItemCommenter to give comment to this tooltip function class:AddItemCommenter(commenter) local commenters = self.commenters commenters[#commenters + 1] = commenter return self end SageGearLevelTooltipUpdater = class end ---------------------------------------------------------------------------------------------------- local tooltipUpdater = SageGearLevelTooltipUpdater:New(methodsToHook) :HookTooltipScript("GameTooltip") :HookTooltipScript("ItemRefTooltip") :HookTooltipScript("ShoppingTooltip1") :HookTooltipScript("ShoppingTooltip2") :AddItemCommenter(ItemLevelIdCommenter:New()) local DEFAULT_COLOR = {r=0.5, g=0.5, b=0.5} sage.gear.AllStats(function(spec, class, stats, color) color = color or DEFAULT_COLOR print(spec, class, stats, color) local specInfo = {label = spec, r=color.r, g=color.g, b=color.b} tooltipUpdater:AddSelfEvaluator( class, BestReforgedEvaluator:New(specInfo, stats):Cache()) end) --[[ tooltipUpdater --DK :AddSelfEvaluator("DEATHKNIGHT", BestReforgedEvaluator:New({label = "Blood DK", r=1.0, g=0.2, b=0.4}, {MeleeDPS=500, Mastery=100, Stamina=100, Agility=69, Dodge=50, Parry=43, Expertise=38, Strength=31, Armor=18, Haste=16, Hit=16, AdditionalArmor=11}) :Cache()) :AddSelfEvaluator("DEATHKNIGHT", BestReforgedEvaluator:New({label = "Frost DK", r=0.2, g=0.4, b=1.0}, {Strength=100, MeleeDPS=100, Hit=43, Expertise=41, Haste=37, Mastery=35, AP=32, Crit=26}) :Cache()) :AddSelfEvaluator("DEATHKNIGHT", BestReforgedEvaluator:New({label = "Unholy DK", r=0.6, g=0.6, b=0.6}, {Strength=100, MeleeDPS=100, Hit=43, Expertise=41, Haste=37, Mastery=35, AP=32, Crit=26}) :Cache()) --Druid :AddSelfEvaluator("DRUID", BestReforgedEvaluator:New({label = "Moonkin", r=0.3, g=0.3, b=1.0}, {Hit=100, Master=100, SP=66, Haste=54, Crit=43, Intellect=22, Spirit=22}) :Cache()) :AddSelfEvaluator("DRUID", BestReforgedEvaluator:New({label = "Kitty", r=1.0, g=1.0, b=0.5}, {DPS=151, Agility=100, Strength=78, AP=37, Mastery=35, Haste=32, Expertise=29, Hit=28, Crit=28}) :Cache()) :AddSelfEvaluator("DRUID", BestReforgedEvaluator:New({label = "Bear", r=1.0, g=0.4, b=0.4}, {Agility=100, Dodge=88, Armor=71, Mastery=48, Expertise=30, Crit=28, Hi=15, Stamina=13, AP=12, Haste=4}) -- {Stamina=100, Armor=75, Agility=48, Dodge=41, AdditionalArmor=21, Mastery=16, Crit=13, -- Strength=10, Expertise=10, Health=7, FeralAP=5, AP=5, Hit=5, Haste=1}) :Cache()) :AddSelfEvaluator("DRUID", BestReforgedEvaluator:New({label = "Tree", r=0.3, g=1.0, b=0.3}, {SP=100, Mastery=100, Haste=57, Intellect=51, Spirit=32, Crit=11}) :Cache()) -- Hunter :AddSelfEvaluator("HUNTER", BestReforgedEvaluator:New({label = "BM Hunter", r=0.7, g=0.3, b=1.0}, {RangedDPS=213, Hit=100, Mastery=100, Agility=58, Crit=40, Intellect=37, Haste=21}) :Cache()) :AddSelfEvaluator("HUNTER", BestReforgedEvaluator:New({label = "MM Hunter", r=1.0, g=0.7, b=0.3}, {RangedDPS=379, Hit=100, Mastery=100, Agility=74, Crit=57, Intellect=39, Haste=24}) :Cache()) :AddSelfEvaluator("HUNTER", BestReforgedEvaluator:New({label = "SV Hunter", r=0.3, g=1.0, b=0.7}, {RangedDPS=100, Agility=100, Hit=80, Crit=60, Haste=40, Mastery=20}) :Cache()) :AddSelfEvaluator("HUNTER", BestReforgedEvaluator:New({label = "SV Hunter (hit-capped)", r=0.2, g=0.8, b=0.5}, {RangedDPS=100, Agility=100, Crit=60, Haste=40, Mastery=20}) :Cache()) -- Mage :AddSelfEvaluator("MAGE", BestReforgedEvaluator:New({label = "Arcane Mage", r=1.0, g=1.0, b=1.0}, {Hit=100, Mastery=100, Haste=54, SP=49, Crit=37, Intellect=34, Spirit=14}) :Cache()) :AddSelfEvaluator("MAGE", BestReforgedEvaluator:New({label = "Fire Mage", r=1.0, g=0.8, b=0.4}, {Hit=100, Mastery=100, Haste=53, SP=46, Crit=43, Intellect=13}) :Cache()) :AddSelfEvaluator("MAGE", BestReforgedEvaluator:New({label = "Frost Mage", r=0.7, g=0.7, b=1.0}, {Hit=100, Mastery=100, Haste=42, SP=39, Crit=19, Intellect=6}) :Cache()) -- Paladin :AddSelfEvaluator("PALADIN", BestReforgedEvaluator:New({label = "Holy Paladin", r=0.3, g=1.0, b=0.3}, {Intellect=100, Mastery=100, SP=58, Crit=46, Haste=35}) :Cache()) :AddSelfEvaluator("PALADIN", BestReforgedEvaluator:New({label = "Prot Paladin", r=1.0, g=0.4, b=0.4}, {Parry=100, Dodge=100, Mastery=81, Stamina=49, Strength=28, Expertise=8, Hit=4}) -- {Mastery=100, Stamina=100, Agility=60, Expertise=59, Dodge=55, Parry=30, Strength=16, Armor=8}) :Cache()) :AddSelfEvaluator("PALADIN", BestReforgedEvaluator:New({label = "Ret Paladin", r=1.0, g=1.0, b=0.5}, {MeleeDPS=470, Mastery=100, Hit=100, Strength=80, Expertise=66, Crit=40, Agility=32, Haste=30, SP=9}) :Cache()) --Priest :AddSelfEvaluator("PRIEST", BestReforgedEvaluator:New({label = "Disc Priest", r=0.5, g=0.5, b=1.0}, {Intellect=100, Spirit=80, SP=19, Crit=11, Haste=10, Mastery=6}) :Cache()) :AddSelfEvaluator("PRIEST", BestReforgedEvaluator:New({label = "Holy Priest", r=0.3, g=1.0, b=0.3}, {Intellect=100, Spirit=95, Haste=85, Mastery=80, Crit=30}) :Cache()) :AddSelfEvaluator("PRIEST", BestReforgedEvaluator:New({label = "Shadow Priest", r=0.5, g=0.5, b=0.5}, {Intellect=100, SP=79, Haste=50, Crit=40, Mastery=38, Spirit=37, Hit=37}) :Cache()) --Rogue :AddSelfEvaluator("ROGUE", BestReforgedEvaluator:New({label = "Assassin", r=0.8, g=1.0, b=0.5}, {MeleeDPS=100, Agility=100, Hit=67, Mastery=50, Haste=46, Expertise=42, Crit=35}) :Cache()) :AddSelfEvaluator("ROGUE", BestReforgedEvaluator:New({label = "Combat Rogue", r=1.0, g=0.8, b=0.5}, {MeleeDPS=100, Agility=100, Hit=70, Expertise=59, Haste=56, Crit=35, Mastery=33}) :Cache()) :AddSelfEvaluator("ROGUE", BestReforgedEvaluator:New({label = "Subtlety", r=1.0, g=1.0, b=0.3}, {MeleeDPS=100, Agility=100, Hit=40, Haste=37, Expertise=33, Crit=31, Mastery=20}) :Cache()) --Shaman :AddSelfEvaluator("SHAMAN", BestReforgedEvaluator:New({label = "Ele Shaman", r=0.5, g=0.5, b=1.0}, {Mastery=100, Hit=100, SP=60, Haste=56, Crit=40, Intellect=11}) :Cache()) :AddSelfEvaluator("SHAMAN", BestReforgedEvaluator:New({label = "Enh Shaman", r=1.0, g=1.0, b=0.5}, {MeleeDPS=124, Agility=100, Hit=60, Expertise=48, Mastery=44, Strength=42, AP=40, Intellect=36, SP=36, Crit=28, Haste=16}) :Cache()) :AddSelfEvaluator("SHAMAN", BestReforgedEvaluator:New({label = "Resto Shaman", r=0.3, g=1.0, b=0.3}, {Intellect=100, SP=83, Spriti=75, Haste=67, Crit=58, Mastery=42, Stamina=8}) :Cache()) --Warlock :AddSelfEvaluator("WARLOCK", BestReforgedEvaluator:New({label = "Aff Warlock", r=1.0, g=0.2, b=1.0}, {Hit=100, Master=100, SP=72, Haste=61, Crit=38, Spirit=34, Intellect=15}) :Cache()) :AddSelfEvaluator("WARLOCK", BestReforgedEvaluator:New({label = "Dem Warlock", r=0.8, g=0.2, b=1.0}, {Hit=100, Master=100, Haste=50, SP=45, Crit=31, Spirit=29, Intellect=13}) :Cache()) :AddSelfEvaluator("WARLOCK", BestReforgedEvaluator:New({label = "Destro Warlock", r=1.0, g=0.6, b=0.0}, {Mastery=100, Hit=100, SP=47, Haste=46, Spirit=26, Crit=16, Intellect=13}) :Cache()) -- Warrior :AddSelfEvaluator("WARRIOR", BestReforgedEvaluator:New({label = "Arms Warrior", r=0.8, g=0.8, b=1.0}, {Mastery=100, Strength=100, Hit=90, Expertise=85, Crit=80, Agility=65, Haste=50, Armor=1}) :Cache()) :AddSelfEvaluator("WARRIOR", BestReforgedEvaluator:New({label = "Fury Warrior", r=0.8, g=0.8, b=0.2}, {Mastery=100, Expertise=100, Strength=82, Crit=66, Agility=53, Hit=48, Haste=36, Armor=5}) :Cache()) :AddSelfEvaluator("WARRIOR", BestReforgedEvaluator:New({label = "Prot Warrior", r=1.0, g=0.4, b=0.4}, {Stamina=100, Mastery=100, Dodge=90, Parry=67, Agility=67, Strength=48, Expertise=19, Hit=10, Crit=7, Armor=6, Haste=1}) :Cache()) ]]