---------------------------------------- -- Stat categories ---------------------------------------- Outfitter.StatCategories = {} function Outfitter:GetCategoryByID(pCategoryID) for _, vCategory in ipairs(Outfitter.StatCategories) do if vCategory.CategoryID == pCategoryID then return vCategory end end end function Outfitter:GetStatByID(pStatID) for _, vCategory in ipairs(Outfitter.StatCategories) do local vNumStats = vCategory:GetNumStats() for vStatIndex = 1, vNumStats do local vStat = vCategory:GetIndexedStat(vStatIndex) if vStat.ID == pStatID then return vStat end end end end function Outfitter:GetStatIDName(pStatID) local vStat = self:GetStatByID(pStatID) return vStat and vStat.Name end function Outfitter:GetStatConfigName(pStatConfig) if not pStatConfig then return end local vName for _, vStatConfig in ipairs(pStatConfig) do local vStat = self:GetStatByID(vStatConfig.StatID) if not vStat then return -- One of the stats is missing, return nothing end if not vName then vName = vStat.Name else vName = vName..", "..vStat.Name end end return vName end function Outfitter:CalcOutfitScore(pOutfit, pStat) if pStat.GetOutfitScore then return pStat:GetOutfitScore(pOutfit) elseif pStat.GetItemScore then local vTotalScore = 0 for _, vItem in pairs(pOutfit:GetItems()) do local vItemScore = pStat:GetItemScore(vItem) if vItemScore then vTotalScore = vTotalScore + vItemScore end end return vTotalScore else return 0 end end function Outfitter:ConstrainScore(pScore, pStatInfo) if pStatInfo.MinValue then -- If the score doesn't meet the minimum then set it to zero -- so that it doesn't affect the final outfit if pScore < pStatInfo.MinValue then pScore = 0 -- Set it to the minimum so that higher scores aren't better -- This keeps the score as close to minValue as possible else pScore = pStatInfo.MinValue end elseif pStatInfo.MaxValue then -- If the score is more than the max then set it to zero -- so that it doesn't affect the final outfit if pScore > pStatInfo.MaxValue then pScore = 0 end end return pScore end function Outfitter:GetMultiStatScore(pOutfit, pParams) local vCombiScores = {} for vIndex, vStatInfo in ipairs(pParams) do vCombiScores[vIndex] = self:CalcOutfitScore(pOutfit, vStatInfo.Stat) or 0 end return vCombiScores end ---------------------------------------- Outfitter._SimpleStatCategory = {} ---------------------------------------- function Outfitter._SimpleStatCategory:GetNumStats() return #self.Stats end function Outfitter._SimpleStatCategory:GetIndexedStat(pIndex) return self.Stats[pIndex] end Outfitter._SimpleStatCategoryMetaTable = {__index = Outfitter._SimpleStatCategory} ---------------------------------------- Outfitter._SimpleStat = {} ---------------------------------------- function Outfitter._SimpleStat:GetItemScore(pItem) -- Don't need to parse anything for item level if self.ID == "ITEM_LEVEL" then if not pItem.Quality or not pItem.Level or pItem.Quality < 2 then return elseif pItem.Quality == 2 then -- Greens return pItem.Level - 26 elseif pItem.Quality == 3 then -- Blues return pItem.Level - 13 else return pItem.Level end end -- Parse the stats local vStats = Outfitter.LibStatLogic:GetSum(pItem.Link) if not vStats then return end if pItem.Name == "Big Iron Fishing Pole" then Outfitter:DebugTable(vStats) end if self.ID == "TOTAL_STATS" then -- Ignore items with no stats if not vStats.AGI and not vStats.INT and not vStats.SPI and not vStats.STA and not vStats.STR then return end -- Total the stats return (vStats.AGI or 0) + (vStats.INT or 0) + (vStats.SPI or 0) + (vStats.STA or 0) + (vStats.STR or 0) else return vStats[self.ID] end end Outfitter._SimpleStatMetaTable = {__index = Outfitter._SimpleStat} ---------------------------------------- -- Simple stats ---------------------------------------- Outfitter.SimpleStatCategories = { { CategoryID = "Stat", Name = PLAYERSTAT_BASE_STATS, Stats = { {ID = "AGI"}, {ID = "INT"}, {ID = "SPI"}, {ID = "STA"}, {ID = "STR"}, {ID = "TOTAL_STATS", Name = Outfitter.cTotalStatsName}, {ID = "ITEM_LEVEL", Name = Outfitter.cItemLevelName}, }, }, { CategoryID = "Melee", Name = PLAYERSTAT_MELEE_COMBAT, Stats = { {ID = "MELEE_DMG"}, {ID = "MELEE_HASTE_RATING"}, {ID = "AP"}, {ID = "RANGED_AP"}, {ID = "MELEE_HIT_RATING"}, {ID = "MELEE_CRIT_RATING"}, {ID = "EXPERTISE_RATING"}, {ID = "ARMOR_PENETRATION_RATING"}, }, }, { CategoryID = "Ranged", Name = PLAYERSTAT_RANGED_COMBAT, Stats = { {ID = "RANGED_DMG"}, {ID = "RANGED_HASTE_RATING"}, {ID = "RANGED_AP"}, {ID = "RANGED_HIT_RATING"}, {ID = "RANGED_CRIT_RATING"}, }, }, { CategoryID = "Spell", Name = PLAYERSTAT_SPELL_COMBAT, Stats = { {ID = "SPELL_DMG"}, {ID = "HEAL"}, {ID = "SPELL_HIT_RATING"}, {ID = "SPELL_CRIT_RATING"}, {ID = "SPELL_HASTE_RATING"}, {ID = "SPELLPEN"}, {ID = "MANA"}, {ID = "MANA_REG"}, }, }, { CategoryID = "Defense", Name = PLAYERSTAT_DEFENSES, Stats = { {ID = "ARMOR"}, {ID = "DEFENSE_RATING"}, {ID = "DODGE_RATING"}, {ID = "PARRY_RATING"}, {ID = "BLOCK_RATING"}, {ID = "RESILIENCE_RATING"}, {ID = "HEALTH"}, {ID = "HEALTH_REG"}, }, }, { CategoryID = "Resist", Name = Outfitter.cResistCategory, Stats = { {ID = "ARCANE_RES"}, {ID = "FIRE_RES"}, {ID = "FROST_RES"}, {ID = "NATURE_RES"}, {ID = "SHADOW_RES"}, }, }, { CategoryID = "Trade", Name = Outfitter.cTradeCategory, Stats = { {ID = "FISHING"}, {ID = "HERBALISM"}, {ID = "MINING"}, {ID = "SKINNING"}, {ID = "RUN_SPEED"}, {ID = "MOUNT_SPEED"}, }, }, } ---------------------------------------- -- Install the simple stats ---------------------------------------- for _, vStatCategory in ipairs(Outfitter.SimpleStatCategories) do setmetatable(vStatCategory, Outfitter._SimpleStatCategoryMetaTable) for _, vStat in ipairs(vStatCategory.Stats) do if not vStat.Name then vStat.Name = Outfitter.LibStatLogic:GetStatNameFromID(vStat.ID) end setmetatable(vStat, Outfitter._SimpleStatMetaTable) end table.insert(Outfitter.StatCategories, vStatCategory) end ---------------------------------------- Outfitter.PawnScalesCategory = ---------------------------------------- { Name = "Pawn", CategoryID = "Pawn", } function Outfitter.PawnScalesCategory:GetNumStats() local vScaleNames = PawnGetAllScales() if self.Scales then for vKey, _ in pairs(self.Scales) do self.Scales[vKey] = nil end else self.Scales = {} end for _, vScaleName in ipairs(vScaleNames) do if PawnIsScaleVisible(vScaleName) then local vScale = { Name = vScaleName, ID = "Pawn_"..vScaleName, } setmetatable(vScale, Outfitter._PawnScaleStatMetaTable) table.insert(self.Scales, vScale) end end return #self.Scales end function Outfitter.PawnScalesCategory:GetIndexedStat(pIndex) return self.Scales[pIndex] end ---------------------------------------- Outfitter._PawnScaleStat = {} ---------------------------------------- function Outfitter._PawnScaleStat:GetItemScore(pItem) if not pItem or not pItem.Link then return end local vItemData = PawnGetItemData(pItem.Link) if not vItemData then return end for _, vEntry in pairs(vItemData.Values) do local vScaleName, vValue, UnenchantedValue = vEntry[1], vEntry[2], vEntry[3] if vScaleName == self.Name then return vValue end end end Outfitter._PawnScaleStatMetaTable = {__index = Outfitter._PawnScaleStat} ---------------------------------------- -- Install Pawn scales ---------------------------------------- if IsAddOnLoaded("Pawn") then table.insert(Outfitter.StatCategories, Outfitter.PawnScalesCategory) else Outfitter.EventLib:RegisterEvent("ADDON_LOADED", function (pEventID, pAddOnName) if pAddOnName == "Pawn" then table.insert(Outfitter.StatCategories, Outfitter.PawnScalesCategory) end end) end ---------------------------------------- Outfitter.WeightsWatcherCategory = ---------------------------------------- { Name = "WeightsWatcher", CategoryID = "WW", } function Outfitter.WeightsWatcherCategory:GetNumStats() local _, vClassID = UnitClass("player") if not ww_vars.weightsList[vClassID] then return 0 end local vActiveWeights = ww_charVars.activeWeights[vClassID] if not vActiveWeights then return 0 end if self.ActiveWeights then Outfitter:EraseTable(self.ActiveWeights) else self.ActiveWeights = {} end for _, vWeightName in ipairs(vActiveWeights) do local vWeight = { Name = vWeightName, ID = "WW_"..vWeightName, ClassID = vClassID, Weight = vWeightName, } setmetatable(vWeight, Outfitter._WeightsWatcherStatMetaTable) table.insert(self.ActiveWeights, vWeight) end return #self.ActiveWeights end function Outfitter.WeightsWatcherCategory:GetIndexedStat(pIndex) return self.ActiveWeights[pIndex] end ---------------------------------------- Outfitter._WeightsWatcherStat = {} ---------------------------------------- function Outfitter._WeightsWatcherStat:GetItemScore(pItem) return ww_weightCache[self.ClassID][self.Weight][pItem.Link] end Outfitter._WeightsWatcherStatMetaTable = {__index = Outfitter._WeightsWatcherStat} ---------------------------------------- -- Install WeightsWatcher ---------------------------------------- if IsAddOnLoaded("WeightsWatcher") then table.insert(Outfitter.StatCategories, Outfitter.WeightsWatcherCategory) else Outfitter.EventLib:RegisterEvent("ADDON_LOADED", function (pEventID, pAddOnName) if pAddOnName == "WeightsWatcher" then table.insert(Outfitter.StatCategories, Outfitter.WeightsWatcherCategory) end end) end ---------------------------------------- Outfitter._TankPointsStat = {} ---------------------------------------- Outfitter._TankPointsStat.Complex = true function Outfitter._TankPointsStat:Begin(pInventoryCache) -- Calculate the TankPoints base stats self.TPSchool = _G[self.ID] assert(self.TPSchool, "TankPoints school "..tostring(self.ID).." couldn't be found") self.TPBaseData = TankPoints:GetSourceData(nil, nil, true) --Outfitter:DebugTable(self.TPBaseData, "TPOrigData") -- Subtract the current outfit to get the base stats local vTotalStats = {} for vInventorySlot, vSlotID in pairs(Outfitter.cSlotIDs) do local vItemLink = GetInventoryItemLink("player", vSlotID) local vStats = Outfitter.LibStatLogic:GetSum(vItemLink) if vStats then for vField, vValue in pairs(vStats) do if type(vValue) == "number" then vTotalStats[vField] = (vTotalStats[vField] or 0) + vValue else vTotalStats[vField] = vValue end end end end -- Test by showing the starting TankPoints self.TPData = Outfitter:RecycleTable(self.TPData) Outfitter:CopyTable(self.TPData, self.TPBaseData) TankPoints:GetTankPoints(self.TPData, self.TPSchool) Outfitter:DebugMessage("Starting TankPoints: %s", self.TPData.tankPoints[self.TPSchool]) -- Adjust the base local vTotalChanges = TankPointsTooltips:BuildChanges({}, vTotalStats) local vNegativeChanges = {} for vField, vValue in pairs(vTotalChanges) do vNegativeChanges[vField] = -vValue end TankPoints:AlterSourceData(self.TPBaseData, vNegativeChanges, true) -- Test by showing the naked TankPoints self.TPData = Outfitter:RecycleTable(self.TPData) Outfitter:CopyTable(self.TPData, self.TPBaseData) TankPoints:GetTankPoints(self.TPData, self.TPSchool) Outfitter:DebugMessage("Naked TankPoints: %s", self.TPData.tankPoints[self.TPSchool]) -- Test by re-applying the base changes self.TPData = Outfitter:RecycleTable(self.TPData) Outfitter:CopyTable(self.TPData, self.TPBaseData) TankPoints:AlterSourceData(self.TPData, vTotalChanges, true) TankPoints:GetTankPoints(self.TPData, self.TPSchool) Outfitter:DebugMessage("Re-generated TankPoints: %s", self.TPData.tankPoints[self.TPSchool]) -- --Outfitter:DebugTable(self.TPBaseData, "TPBaseData") end function Outfitter._TankPointsStat:GetOutfitScore(pOutfit, pStatParams) -- Sum up the changes self.TPChanges = Outfitter:RecycleTable(self.TPChanges) for vInventorySlot, vItem in pairs(pOutfit.Items) do if not vItem.TPChanges then local vStats = Outfitter.LibStatLogic:GetSum(vItem.Link) if vStats then vItem.TPChanges = TankPointsTooltips:BuildChanges({}, vStats) end end if vItem.TPChanges then for vStatID, vValue in pairs(vItem.TPChanges) do self.TPChanges[vStatID] = (self.TPChanges[vStatID] or 0) + vValue end end end -- Clone the base data and apply the changes self.TPData = Outfitter:RecycleTable(self.TPData) Outfitter:CopyTable(self.TPData, self.TPBaseData) TankPoints:AlterSourceData(self.TPData, self.TPChanges, true) TankPoints:GetTankPoints(self.TPData, self.TPSchool) -- pOutfit.TankPoints = self.TPData.tankPoints[self.TPSchool] return pOutfit.TankPoints end function Outfitter._TankPointsStat:CompareOutfits(pOldOutfit, pNewOutfit, pStatParams) local vScore = self:GetOutfitScore(pNewOutfit) -- Return the result if not pOldOutfit then return true end if pNewOutfit.TankPoints <= pOldOutfit.TankPoints then return false end Outfitter:DebugMessage("Old outfit %s points, new outfit %s points", pOldOutfit.TankPoints, pNewOutfit.TankPoints) return true, string.format("%s TankPoints", Outfitter:FormatThousands(pNewOutfit.TankPoints)) end function Outfitter._TankPointsStat:End(pInventoryCache) -- Clear the cached TP data for vInventorySlot, vItems in pairs(pInventoryCache.ItemsBySlot) do for _, vItem in ipairs(vItems) do vItem.TPChanges = nil end end end function Outfitter._TankPointsStat:GetFilterStats() return self.FilterStats end Outfitter._TankPointsStatMetaTable = {__index = Outfitter._TankPointsStat} ---------------------------------------- Outfitter._MultiStat = {} ---------------------------------------- Outfitter._MultiStat.Name = "Multiple stats" function Outfitter._MultiStat:GetFilterStats() return self.FilterStats end function Outfitter._MultiStat:Begin(pInventoryCache) for _, vStatInfo in ipairs(self.Config) do if vStatInfo.Stat.Begin then vStatInfo.Stat:Begin(pInventoryCache) end end end function Outfitter._MultiStat:Begin(pInventoryCache) for _, vStatInfo in ipairs(self.Config) do if vStatInfo.Stat.Begin then vStatInfo.Stat:Begin(pInventoryCache) end end end function Outfitter._MultiStat:End(pInventoryCache) for _, vStatInfo in ipairs(self.Config) do if vStatInfo.Stat.End then vStatInfo.Stat:End(pInventoryCache) end end end function Outfitter._MultiStat:GetOutfitScore(pOutfit, pStatParams) return Outfitter:GetMultiStatScore(pOutfit, self.Config) end function Outfitter._MultiStat:CompareOutfits(pOldOutfit, pNewOutfit, pStatParams) return Outfitter:MultiStatCompare(pPreviousOutfit, pNewOutfit, self.Config) end Outfitter._MultiStatMetaTable = {__index = Outfitter._MultiStat} ---------------------------------------- Outfitter.TankPointsCategory = ---------------------------------------- { Name = "TankPoints", CategoryID = "TankPoints", Stats = { {Name = "Melee TankPoints", School = "MELEE"}, {Name = "Ranged TankPoints", School = "RANGED"}, {Name = "Holy TankPoints", School = "HOLY"}, {Name = "Fire TankPoints", School = "FIRE"}, {Name = "Nature TankPoints", School = "NATURE"}, {Name = "Frost TankPoints", School = "FROST"}, {Name = "Shadow TankPoints", School = "SHADOW"}, {Name = "Arcane TankPoints", School = "ARCANE"}, }, } for _, vStat in ipairs(Outfitter.TankPointsCategory.Stats) do vStat.ID = "TP_"..vStat.School setmetatable(vStat, Outfitter._TankPointsStatMetaTable) end function Outfitter.TankPointsCategory:GetNumStats() return #self.Stats end function Outfitter.TankPointsCategory:GetIndexedStat(pIndex) return self.Stats[pIndex] end ---------------------------------------- -- Install TankPoints ---------------------------------------- if IsAddOnLoaded("TankPoints") then table.insert(Outfitter.StatCategories, Outfitter.TankPointsCategory) else Outfitter.EventLib:RegisterEvent("ADDON_LOADED", function (pEventID, pAddOnName) if pAddOnName == "TankPoints" then table.insert(Outfitter.StatCategories, Outfitter.TankPointsCategory) end end) end ---------------------------------------- -- Tank Points ---------------------------------------- function Outfitter.TankPoints_New() local vTankPointData = {} local vStatDistribution = Outfitter:GetPlayerStatDistribution() if not vStatDistribution then Outfitter:ErrorMessage("Missing stat distribution data for "..Outfitter.PlayerClass) return end vTankPointData.PlayerLevel = UnitLevel("player") vTankPointData.StaminaFactor = 1.0 -- Warlocks with demonic embrace = 1.15 -- Get the base stats vTankPointData.BaseStats = {} Outfitter.Stats_AddStatValue(vTankPointData.BaseStats, "Strength", UnitStat("player", 1)) Outfitter.Stats_AddStatValue(vTankPointData.BaseStats, "Agility", UnitStat("player", 2)) Outfitter.Stats_AddStatValue(vTankPointData.BaseStats, "Stamina", UnitStat("player", 3)) Outfitter.Stats_AddStatValue(vTankPointData.BaseStats, "Intellect", UnitStat("player", 4)) Outfitter.Stats_AddStatValue(vTankPointData.BaseStats, "Spirit", UnitStat("player", 5)) Outfitter.Stats_AddStatValue(vTankPointData.BaseStats, "Health", UnitHealthMax("player")) vTankPointData.BaseStats.Health = vTankPointData.BaseStats.Health - vTankPointData.BaseStats.Stamina * 10 vTankPointData.BaseStats.Dodge = GetDodgeChance() vTankPointData.BaseStats.Parry = GetParryChance() vTankPointData.BaseStats.Block = GetBlockChance() local vBaseDefense, vBuffDefense = UnitDefense("player") Outfitter.Stats_AddStatValue(vTankPointData.BaseStats, "Defense", vBaseDefense + vBuffDefense) -- Replace the armor with the current value since that already includes various factors local vBaseArmor, vEffectiveArmor, vArmor, vArmorPosBuff, vArmorNegBuff = UnitArmor("player") vTankPointData.BaseStats.Armor = vEffectiveArmor Outfitter:DebugMessage("------------------------------------------") Outfitter:DebugTable(vTankPointData, "vTankPointData") -- Subtract out the current outfit local vCurrentOutfitStats = Outfitter.TankPoints_GetCurrentOutfitStats(vStatDistribution) Outfitter:DebugMessage("------------------------------------------") Outfitter:DebugTable(vCurrentOutfitStats, "vCurrentOutfitStats") Outfitter.Stats_SubtractStats(vTankPointData.BaseStats, vCurrentOutfitStats) -- Calculate the buff stats (stuff from auras/spell buffs/whatever) vTankPointData.BuffStats = {} -- Reset the cumulative values Outfitter.TankPoints_Reset(vTankPointData) Outfitter:DebugMessage("------------------------------------------") Outfitter:DebugTable(vTankPointData, "vTankPointData") Outfitter:DebugMessage("------------------------------------------") return vTankPointData end function Outfitter.TankPoints_Reset(pTankPointData) pTankPointData.AdditionalStats = {} end function Outfitter.TankPoints_GetTotalStat(pTankPointData, pStat) local vTotalStat = pTankPointData.BaseStats[pStat] if not vTotalStat then vTotalStat = 0 end local vAdditionalStat = pTankPointData.AdditionalStats[pStat] if vAdditionalStat then vTotalStat = vTotalStat + vAdditionalStat end local vBuffStat = pTankPointData.BuffStats[pStat] if vBuffStat then vTotalStat = vTotalStat + vBuffStat end -- return vTotalStat end function Outfitter.TankPoints_CalcTankPoints(pTankPointData, pStanceModifier) if not pStanceModifier then pStanceModifier = 1 end Outfitter:DebugTable(pTankPointData, "pTankPointData") local vEffectiveArmor = Outfitter.TankPoints_GetTotalStat(pTankPointData, "Armor") Outfitter:TestMessage("Armor: "..vEffectiveArmor) local vArmorReduction = vEffectiveArmor / ((85 * pTankPointData.PlayerLevel) + 400) vArmorReduction = vArmorReduction / (vArmorReduction + 1) local vEffectiveHealth = Outfitter.TankPoints_GetTotalStat(pTankPointData, "Health") Outfitter:TestMessage("Health: "..vEffectiveHealth) Outfitter:TestMessage("Stamina: "..Outfitter.TankPoints_GetTotalStat(pTankPointData, "Stamina")) -- local vEffectiveDodge = Outfitter.TankPoints_GetTotalStat(pTankPointData, "Dodge") * 0.01 local vEffectiveParry = Outfitter.TankPoints_GetTotalStat(pTankPointData, "Parry") * 0.01 local vEffectiveBlock = Outfitter.TankPoints_GetTotalStat(pTankPointData, "Block") * 0.01 local vEffectiveDefense = Outfitter.TankPoints_GetTotalStat(pTankPointData, "Defense") -- Add agility and defense to dodge -- defenseInputBox:GetNumber() * 0.04 + agiInputBox:GetNumber() * 0.05 Outfitter:TestMessage("Dodge: "..vEffectiveDodge) Outfitter:TestMessage("Parry: "..vEffectiveParry) Outfitter:TestMessage("Block: "..vEffectiveBlock) Outfitter:TestMessage("Defense: "..vEffectiveDefense) local vDefenseModifier = (vEffectiveDefense - pTankPointData.PlayerLevel * 5) * 0.04 * 0.01 Outfitter:TestMessage("Crit reduction: "..vDefenseModifier) local vMobCrit = max(0, 0.05 - vDefenseModifier) local vMobMiss = 0.05 + vDefenseModifier local vMobDPS = 1 local vTotalReduction = 1 - (vMobCrit * 2 + (1 - vMobCrit - vMobMiss - vEffectiveDodge - vEffectiveParry)) * (1 - vArmorReduction) * pStanceModifier Outfitter:TestMessage("Total reduction: "..vTotalReduction) local vTankPoints = vEffectiveHealth / (vMobDPS * (1 - vTotalReduction)) return vTankPoints --[[ Stats used in TankPoints calculation: Health Dodge Parry Block Defense Armor ]]-- end function Outfitter.TankPoints_GetCurrentOutfitStats(pStatDistribution) local vTotalStats = {} for _, vSlotName in ipairs(Outfitter.cSlotNames) do local vStats = Outfitter.ItemList_GetItemStats({SlotName = vSlotName}) if vStats then Outfitter:TestMessage("--------- "..vSlotName) for vStat, vValue in pairs(vStats) do Outfitter.Stats_AddStatValue(vTotalStats, vStat, vValue) end end end return vTotalStats end function Outfitter.TankPoints_Test() local vStatDistribution = Outfitter:GetPlayerStatDistribution() local vTankPointData = Outfitter.TankPoints_New() local vStats = Outfitter.TankPoints_GetCurrentOutfitStats(vStatDistribution) Outfitter.Stats_AddStats(vTankPointData.AdditionalStats, vStats) local vTankPoints = Outfitter.TankPoints_CalcTankPoints(vTankPointData) Outfitter:TestMessage("TankPoints = "..vTankPoints) end