Files
Nanami-UI/GearScore.lua
2026-04-09 09:46:47 +08:00

1379 lines
55 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--------------------------------------------------------------------------------
-- Nanami-UI: GearScore (GearScore.lua)
-- 基于乌龟服(Turtle WoW)属性收益理论的装备评分系统
-- 评分 1-10: 衡量该装备对当前职业各天赋的适配度
-- 考虑: 属性权重 × 装备类型兼容性 × 等级适配
--------------------------------------------------------------------------------
SFrames.GearScore = {}
local GS = SFrames.GearScore
--------------------------------------------------------------------------------
-- Item budget cost per unit of each stat (for normalization)
-- Higher cost = stat is "rarer/more expensive" per item budget point
--------------------------------------------------------------------------------
local BUDGET_COST = {
STR = 1.0, AGI = 1.0, STA = 1.0, INT = 1.0, SPI = 1.0,
CRIT = 14.0, TOHIT = 15.0, RANGEDCRIT = 14.0,
SPELLCRIT = 14.0, SPELLTOHIT = 15.0,
ATTACKPOWER = 0.5, RANGEDATTACKPOWER = 0.5,
DMG = 0.86, HEAL = 0.58,
DEFENSE = 1.5, DODGE = 12.0, PARRY = 12.0, BLOCK = 12.0,
BLOCKVALUE = 0.5, ARMOR = 0.05, BASEARMOR = 0.02,
HEALTHREG = 2.0, MANAREG = 2.0,
HEALTH = 0.07, MANA = 0.07,
WEAPONDPS = 3.0,
WEAPONSPEED = 1.5,
}
--------------------------------------------------------------------------------
-- EP Weight Tables (tab = talent tab index for primary spec detection)
--------------------------------------------------------------------------------
local WEIGHTS = {
-- ================================================================
-- Pawn-style normalization: primary stat = 1.0
-- TOHIT/CRIT per 1%; PDF: 1%hit≈18AP, 1%crit≈25AP, 1%spellhit≈14SP
-- ================================================================
WARRIOR = {
specs = {
{ name = "武器", tab = 1, color = "ffC79C6E",
w = { STR=1.0, AGI=0.7, STA=0.5, TOHIT=9, CRIT=12,
ATTACKPOWER=0.5, HEALTH=0.05, WEAPONDPS=5, BASEARMOR=0.005 } },
{ name = "狂怒", tab = 2, color = "ffC79C6E",
w = { STR=1.0, AGI=0.6, STA=0.5, TOHIT=10, CRIT=11,
ATTACKPOWER=0.5, HEALTH=0.05, WEAPONDPS=5, BASEARMOR=0.005 } },
{ name = "防护", tab = 3, color = "ff69CCF0",
w = { STA=1.0, STR=0.5, AGI=0.7, TOHIT=5, CRIT=3, ATTACKPOWER=0.2,
DEFENSE=0.8, DODGE=8, PARRY=7, BLOCK=6, BLOCKVALUE=0.35,
ARMOR=0.02, HEALTH=0.1, HEALTHREG=1.0, WEAPONDPS=3, BASEARMOR=0.03 } },
},
-- 硬核物理(战士型): 耐 > 力 > 敏 > 攻强
hc = { name = "硬核", color = "ffFF4444",
w = { STA=1.5, STR=1.0, AGI=0.8, ATTACKPOWER=0.5,
TOHIT=3, CRIT=3, DEFENSE=0.5, DODGE=5, PARRY=3, BLOCK=3, BLOCKVALUE=0.3,
ARMOR=0.05, HEALTH=0.1, HEALTHREG=1.5, WEAPONDPS=3, BASEARMOR=0.02 } },
},
PALADIN = {
specs = {
{ name = "神圣", tab = 1, color = "ff00FF96",
w = { INT=1.0, SPI=0.3, STA=0.5, HEAL=0.55, DMG=0.1,
SPELLCRIT=5, MANAREG=1.3, MANA=0.01, BASEARMOR=0.005 } },
{ name = "防护", tab = 2, color = "ff69CCF0",
w = { STA=1.0, STR=0.5, AGI=0.5, INT=0.3, TOHIT=5, CRIT=3,
ATTACKPOWER=0.2, DMG=0.4,
DEFENSE=0.7, DODGE=7, PARRY=6, BLOCK=6, BLOCKVALUE=0.15,
ARMOR=0.02, HEALTH=0.1, MANAREG=1.0, WEAPONDPS=2, BASEARMOR=0.03 } },
{ name = "惩戒", tab = 3, color = "ffF58CBA",
w = { STR=1.0, AGI=0.6, STA=0.5, INT=0.3,
TOHIT=8, CRIT=10, SPELLCRIT=5,
ATTACKPOWER=0.5, DMG=0.3, HEAL=0.05, WEAPONDPS=5, BASEARMOR=0.005 } },
},
-- 硬核圣骑士: 耐 > 力 > 智 = 敏 = 精
hc = { name = "硬核", color = "ffFF4444",
w = { STA=1.5, STR=1.0, INT=0.8, AGI=0.8, SPI=0.8,
TOHIT=3, CRIT=3, ATTACKPOWER=0.3, HEAL=0.4, DMG=0.2,
DEFENSE=0.5, DODGE=3, ARMOR=0.04, HEALTH=0.1,
HEALTHREG=1.0, MANAREG=1.0, WEAPONDPS=2, BASEARMOR=0.02 } },
},
HUNTER = {
specs = {
{ name = "野兽", tab = 1, color = "ffABD473",
w = { AGI=1.0, STR=0.05, STA=0.5, INT=0.8,
TOHIT=10, CRIT=10, RANGEDCRIT=10,
ATTACKPOWER=0.4, RANGEDATTACKPOWER=0.5, MANAREG=2.0,
WEAPONDPS=4, BASEARMOR=0.005 } },
{ name = "射击", tab = 2, color = "ffABD473",
w = { AGI=1.0, STR=0.05, STA=0.5, INT=0.9,
TOHIT=10, CRIT=10, RANGEDCRIT=10,
ATTACKPOWER=0.4, RANGEDATTACKPOWER=0.5, MANAREG=2.0,
WEAPONDPS=4, BASEARMOR=0.005 } },
{ name = "生存", tab = 3, color = "ffABD473",
w = { AGI=1.0, STR=0.4, STA=0.5, INT=0.3, SPI=0.3,
TOHIT=8, CRIT=8, RANGEDCRIT=8,
ATTACKPOWER=0.5, RANGEDATTACKPOWER=0.5, MANAREG=1.0,
WEAPONDPS=3, BASEARMOR=0.005 } },
},
-- 硬核猎人: 耐 > 敏 > 智 > 力 (精神配合灵魂链接有价值)
hc = { name = "硬核", color = "ffFF4444",
w = { STA=1.5, AGI=1.0, INT=0.5, STR=0.3, SPI=0.8,
TOHIT=3, CRIT=3, RANGEDCRIT=3,
ATTACKPOWER=0.2, RANGEDATTACKPOWER=0.3,
ARMOR=0.04, HEALTH=0.1, HEALTHREG=1.5, WEAPONDPS=2, BASEARMOR=0.01 } },
},
ROGUE = {
specs = {
{ name = "刺杀", tab = 1, color = "ffFFF569",
w = { AGI=1.0, STR=0.5, STA=0.5, TOHIT=10, CRIT=12,
ATTACKPOWER=0.45, WEAPONDPS=5, BASEARMOR=0.005 } },
{ name = "战斗", tab = 2, color = "ffFFF569",
w = { AGI=1.0, STR=0.5, STA=0.5, TOHIT=10, CRIT=11,
ATTACKPOWER=0.45, WEAPONDPS=6, BASEARMOR=0.005 } },
{ name = "敏锐", tab = 3, color = "ffFFF569",
w = { AGI=1.0, STR=0.5, STA=0.5, TOHIT=8, CRIT=10,
ATTACKPOWER=0.45, DODGE=3, WEAPONDPS=4, BASEARMOR=0.005 } },
},
-- 硬核潜行者: 耐 > 敏 > 力 > 攻强
hc = { name = "硬核", color = "ffFF4444",
w = { STA=1.5, AGI=1.2, STR=0.8, ATTACKPOWER=0.5,
TOHIT=3, CRIT=3, DODGE=3, ARMOR=0.04,
HEALTH=0.1, HEALTHREG=1.5, WEAPONDPS=3, BASEARMOR=0.01 } },
},
PRIEST = {
specs = {
{ name = "戒律", tab = 1, color = "ff00FF96",
w = { INT=1.0, SPI=0.5, STA=0.5, HEAL=0.7, DMG=0.1,
SPELLCRIT=4, MANAREG=1.2, MANA=0.01,
WEAPONDPS=2, WEAPONSPEED=0.5, BASEARMOR=0.003 } },
{ name = "神圣", tab = 2, color = "ff00FF96",
w = { INT=1.0, SPI=0.7, STA=0.5, HEAL=0.8, DMG=0.1,
SPELLCRIT=3, MANAREG=1.35, MANA=0.01,
WEAPONDPS=2, WEAPONSPEED=0.5, BASEARMOR=0.003 } },
{ name = "暗影", tab = 3, color = "ff9482C9",
w = { DMG=1.0, INT=0.2, SPI=0.2, STA=0.5,
SPELLTOHIT=14, SPELLCRIT=8, MANAREG=1.0,
WEAPONDPS=2.5, WEAPONSPEED=0.6, BASEARMOR=0.003 } },
},
-- 硬核法系(牧师): 耐 > 智 = 治疗 > 精
hc = { name = "硬核", color = "ffFF4444",
w = { STA=1.5, INT=1.0, HEAL=1.0, DMG=0.8, SPI=0.8,
SPELLCRIT=2, SPELLTOHIT=3, MANAREG=1.5,
WEAPONDPS=1.5, WEAPONSPEED=0.3,
ARMOR=0.04, HEALTH=0.08, HEALTHREG=0.8, BASEARMOR=0.01 } },
},
SHAMAN = {
specs = {
{ name = "元素", tab = 1, color = "ff0070DE",
w = { DMG=1.0, INT=0.3, SPI=0.1, STA=0.5,
SPELLTOHIT=10, SPELLCRIT=8, ATTACKPOWER=0.1,
MANAREG=1.1, BASEARMOR=0.005 } },
{ name = "增强", tab = 2, color = "ff0070DE",
w = { STR=1.0, AGI=0.9, STA=0.5, INT=0.3,
TOHIT=10, CRIT=11, ATTACKPOWER=0.5,
DMG=0.3, SPELLCRIT=3, MANAREG=1.0,
WEAPONDPS=5, BASEARMOR=0.005 } },
{ name = "恢复", tab = 3, color = "ff00FF96",
w = { INT=1.0, SPI=0.3, STA=0.5, HEAL=0.9, DMG=0.1,
SPELLCRIT=5, MANAREG=1.7, MANA=0.01, BASEARMOR=0.005 } },
},
-- 硬核萨满(混合): 耐 > 力=敏 > 智=精
hc = { name = "硬核", color = "ffFF4444",
w = { STA=1.5, STR=0.8, AGI=0.8, INT=0.5, SPI=0.5,
TOHIT=3, CRIT=3, ATTACKPOWER=0.3, HEAL=0.4, DMG=0.3,
MANAREG=1.0, ARMOR=0.04, HEALTH=0.1, HEALTHREG=1.0,
WEAPONDPS=2, BASEARMOR=0.01 } },
},
MAGE = {
specs = {
{ name = "奥术", tab = 1, color = "ff69CCF0",
w = { DMG=1.0, INT=0.46, SPI=0.6, STA=0.3,
SPELLTOHIT=10, SPELLCRIT=7, MANAREG=1.1,
MANA=0.04, WEAPONDPS=2, WEAPONSPEED=0.5, BASEARMOR=0.003 } },
{ name = "火焰", tab = 2, color = "ff69CCF0",
w = { DMG=1.0, INT=0.44, SPI=0.07, STA=0.3,
SPELLTOHIT=12, SPELLCRIT=9, MANAREG=1.0,
MANA=0.04, WEAPONDPS=2, WEAPONSPEED=0.5, BASEARMOR=0.003 } },
{ name = "冰霜", tab = 3, color = "ff69CCF0",
w = { DMG=1.0, INT=0.37, SPI=0.06, STA=0.3,
SPELLTOHIT=12, SPELLCRIT=7, MANAREG=0.8,
MANA=0.03, WEAPONDPS=2, WEAPONSPEED=0.5, BASEARMOR=0.003 } },
},
-- 硬核法系(法师): 耐 > 智 = 法伤 > 精
hc = { name = "硬核", color = "ffFF4444",
w = { STA=1.5, INT=1.0, DMG=1.0, SPI=0.8,
SPELLCRIT=2, SPELLTOHIT=3, MANAREG=1.5,
WEAPONDPS=1.5, WEAPONSPEED=0.3,
ARMOR=0.05, HEALTH=0.08, HEALTHREG=0.8, BASEARMOR=0.01 } },
},
WARLOCK = {
specs = {
{ name = "痛苦", tab = 1, color = "ff9482C9",
w = { DMG=1.0, INT=0.4, SPI=0.1, STA=0.5,
SPELLTOHIT=12, SPELLCRIT=4, MANAREG=1.0,
HEALTH=0.05, WEAPONDPS=2, WEAPONSPEED=0.5, BASEARMOR=0.003 } },
{ name = "恶魔", tab = 2, color = "ff9482C9",
w = { DMG=1.0, INT=0.4, SPI=0.5, STA=0.5,
SPELLTOHIT=12, SPELLCRIT=7, MANAREG=1.0,
WEAPONDPS=2, WEAPONSPEED=0.5, BASEARMOR=0.003 } },
{ name = "毁灭", tab = 3, color = "ff9482C9",
w = { DMG=1.0, INT=0.34, SPI=0.25, STA=0.5,
SPELLTOHIT=14, SPELLCRIT=9, MANAREG=0.65,
WEAPONDPS=2.5, WEAPONSPEED=0.5, BASEARMOR=0.003 } },
},
-- 硬核法系(术士): 耐 > 智 = 法伤 > 精 (精神低于法师因有生命分流)
hc = { name = "硬核", color = "ffFF4444",
w = { STA=1.5, INT=1.0, DMG=1.0, SPI=0.5,
SPELLCRIT=2, SPELLTOHIT=3, MANAREG=1.0,
WEAPONDPS=1.5, WEAPONSPEED=0.3,
ARMOR=0.04, HEALTH=0.08, HEALTHREG=0.8, BASEARMOR=0.01 } },
},
DRUID = {
specs = {
{ name = "平衡", tab = 1, color = "ffFF7D0A",
w = { DMG=1.0, INT=0.38, SPI=0.34, STA=0.5,
HEAL=0.1, SPELLTOHIT=12, SPELLCRIT=7,
MANAREG=0.6, MANA=0.03, BASEARMOR=0.005 } },
{ name = "野猫", tab = 2, color = "ffFF7D0A",
w = { STR=1.2, AGI=1.0, STA=0.5, TOHIT=10, CRIT=11,
ATTACKPOWER=0.5, DODGE=1, WEAPONDPS=0.5, BASEARMOR=0.005 } },
{ name = "野熊", tab = 2, color = "ff69CCF0",
w = { STA=1.0, AGI=0.5, STR=0.2, TOHIT=3, CRIT=3, ATTACKPOWER=0.35,
DEFENSE=0.5, DODGE=6, ARMOR=0.1, HEALTH=0.08, BASEARMOR=0.04 } },
{ name = "恢复", tab = 3, color = "ff00FF96",
w = { INT=1.0, SPI=0.87, STA=0.5, HEAL=1.2, DMG=0.1,
SPELLCRIT=4, MANAREG=1.7, MANA=0.01, BASEARMOR=0.005 } },
},
-- 硬核德鲁伊(混合偏生存): 耐 > 敏 > 力 > 智 = 精
hc = { name = "硬核", color = "ffFF4444",
w = { STA=1.5, AGI=1.0, STR=0.8, INT=0.5, SPI=0.5,
TOHIT=3, CRIT=3, ATTACKPOWER=0.3, HEAL=0.4, DMG=0.3,
DODGE=3, ARMOR=0.04, HEALTH=0.1, HEALTHREG=1.0,
WEAPONDPS=1, BASEARMOR=0.01 } },
},
}
--------------------------------------------------------------------------------
-- Armor type compatibility (class × armor subclass × level)
-- 1.0 = ideal, 0 = should never wear this
--------------------------------------------------------------------------------
local ARMOR_COMPAT = {
WARRIOR = { Plate=1.0, Mail=0.3, Leather=0.1, Cloth=0.05 },
PALADIN = { Plate=1.0, Mail=0.3, Leather=0.1, Cloth=0.05 },
HUNTER = { Mail=1.0, Leather=0.5, Cloth=0.1 },
SHAMAN = { Mail=1.0, Leather=0.5, Cloth=0.1 },
ROGUE = { Leather=1.0, Cloth=0.3 },
DRUID = { Leather=1.0, Cloth=0.3 },
PRIEST = { Cloth=1.0 },
MAGE = { Cloth=1.0 },
WARLOCK = { Cloth=1.0 },
}
local ARMOR_COMPAT_LOW = {
WARRIOR = { Mail=1.0, Leather=0.6, Cloth=0.1 },
PALADIN = { Mail=1.0, Leather=0.6, Cloth=0.1 },
HUNTER = { Leather=1.0, Cloth=0.3 },
SHAMAN = { Leather=1.0, Cloth=0.3 },
ROGUE = { Leather=1.0, Cloth=0.3 },
DRUID = { Leather=1.0, Cloth=0.3 },
PRIEST = { Cloth=1.0 },
MAGE = { Cloth=1.0 },
WARLOCK = { Cloth=1.0 },
}
--------------------------------------------------------------------------------
-- Equipment slot compatibility per spec (CLASS_specIdx)
-- 2H weapons for tanks = 0.05, shields for DPS = low, etc.
-- Missing entries default to 1.0
--------------------------------------------------------------------------------
local SLOT_COMPAT = {
WARRIOR_1 = { INVTYPE_SHIELD = 0.1, INVTYPE_HOLDABLE = 0.05 },
WARRIOR_2 = { INVTYPE_SHIELD = 0.1, INVTYPE_HOLDABLE = 0.05 },
WARRIOR_3 = { INVTYPE_2HWEAPON = 0.05, INVTYPE_HOLDABLE = 0.05 },
PALADIN_1 = { INVTYPE_2HWEAPON = 0.3 },
PALADIN_2 = { INVTYPE_2HWEAPON = 0.05 },
PALADIN_3 = { INVTYPE_SHIELD = 0.2, INVTYPE_HOLDABLE = 0.1 },
HUNTER_1 = { INVTYPE_SHIELD = 0 },
HUNTER_2 = { INVTYPE_SHIELD = 0 },
HUNTER_3 = { INVTYPE_SHIELD = 0 },
ROGUE_1 = { INVTYPE_2HWEAPON = 0, INVTYPE_SHIELD = 0, INVTYPE_HOLDABLE = 0 },
ROGUE_2 = { INVTYPE_2HWEAPON = 0, INVTYPE_SHIELD = 0, INVTYPE_HOLDABLE = 0 },
ROGUE_3 = { INVTYPE_2HWEAPON = 0, INVTYPE_SHIELD = 0, INVTYPE_HOLDABLE = 0 },
SHAMAN_1 = { INVTYPE_2HWEAPON = 0.3 },
SHAMAN_2 = { INVTYPE_SHIELD = 0.3 },
DRUID_1 = { INVTYPE_SHIELD = 0 },
DRUID_2 = { INVTYPE_SHIELD = 0, INVTYPE_HOLDABLE = 0 },
DRUID_3 = { INVTYPE_SHIELD = 0, INVTYPE_HOLDABLE = 0 },
DRUID_4 = { INVTYPE_SHIELD = 0 },
MAGE_1 = { INVTYPE_SHIELD = 0 },
MAGE_2 = { INVTYPE_SHIELD = 0 },
MAGE_3 = { INVTYPE_SHIELD = 0 },
WARLOCK_1 = { INVTYPE_SHIELD = 0 },
WARLOCK_2 = { INVTYPE_SHIELD = 0 },
WARLOCK_3 = { INVTYPE_SHIELD = 0 },
PRIEST_1 = { INVTYPE_SHIELD = 0 },
PRIEST_2 = { INVTYPE_SHIELD = 0 },
PRIEST_3 = { INVTYPE_SHIELD = 0 },
}
--------------------------------------------------------------------------------
-- Level-based weight adjustments
--------------------------------------------------------------------------------
local function AdjustWeightsForLevel(baseWeights, level, isHC)
if level >= 55 then return baseWeights end
local adj = {}
for k, v in pairs(baseWeights) do adj[k] = v end
if isHC then
-- HC: STA always stays high (survival is the whole point)
if level <= 20 then
adj.SPI = math.max((adj.SPI or 0) * 1.3, 0.3)
adj.TOHIT = (adj.TOHIT or 0) * 0.2
adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.2
adj.CRIT = (adj.CRIT or 0) * 0.3
adj.SPELLCRIT = (adj.SPELLCRIT or 0) * 0.3
adj.HEALTHREG = (adj.HEALTHREG or 0) + 0.8
adj.ARMOR = (adj.ARMOR or 0) + 0.02
elseif level <= 40 then
adj.TOHIT = (adj.TOHIT or 0) * 0.5
adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.5
adj.ARMOR = (adj.ARMOR or 0) + 0.01
else
adj.TOHIT = (adj.TOHIT or 0) * 0.8
adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.8
end
else
if level <= 20 then
-- PDF: 精神至上 at 1-20; SPI matters for all classes (regen)
adj.SPI = math.max((adj.SPI or 0) * 1.5, 0.3)
adj.STA = (adj.STA or 0) * 0.7
-- SP/HEAL don't exist on items at this level
adj.DMG = (adj.DMG or 0) * 0.15
adj.HEAL = (adj.HEAL or 0) * 0.15
adj.TOHIT = (adj.TOHIT or 0) * 0.2
adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.2
adj.CRIT = (adj.CRIT or 0) * 0.2
adj.SPELLCRIT = (adj.SPELLCRIT or 0) * 0.2
adj.RANGEDCRIT = (adj.RANGEDCRIT or 0) * 0.2
adj.HEALTHREG = (adj.HEALTHREG or 0) + 0.8
adj.ARMOR = (adj.ARMOR or 0) + 0.02
elseif level <= 40 then
-- PDF: 40级解锁板甲/锁甲, SP开始出现
adj.SPI = math.max((adj.SPI or 0) * 1.2, 0.15)
adj.DMG = (adj.DMG or 0) * 0.5
adj.HEAL = (adj.HEAL or 0) * 0.5
adj.TOHIT = (adj.TOHIT or 0) * 0.6
adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.6
adj.ARMOR = (adj.ARMOR or 0) + 0.01
else
adj.TOHIT = (adj.TOHIT or 0) * 0.85
adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.85
end
end
return adj
end
--------------------------------------------------------------------------------
-- Player info helpers
--------------------------------------------------------------------------------
local function GetPlayerClassToken()
local _, classEN = UnitClass("player")
if classEN then return string.upper(classEN) end
return nil
end
local function GetPlayerLevel()
return UnitLevel("player") or 60
end
local function GetPrimaryTabIndex()
if not GetNumTalentTabs then return 1 end
local maxPts, maxIdx = 0, 1
for i = 1, GetNumTalentTabs() do
local _, _, pts = GetTalentTabInfo(i)
if pts and pts > maxPts then
maxPts = pts
maxIdx = i
end
end
return maxIdx
end
local function IsPlayerHardcore()
if IsHardcore and IsHardcore("player") then return true end
if C_TurtleWoW and C_TurtleWoW.IsHardcore and C_TurtleWoW.IsHardcore("player") then
return true
end
return false
end
--------------------------------------------------------------------------------
-- Item link/info utilities
--------------------------------------------------------------------------------
local function ExtractItemString(link)
if not link then return nil end
local _, _, itemStr = string.find(link, "(item:[%-?%d:]+)")
return itemStr
end
-- Item quality detection from link color code
-- WoW link format: |cffXXXXXX|Hitem:...|h[Name]|h|r
local QUALITY_FROM_COLOR = {
["9d9d9d"] = 0, -- Poor (gray)
["ffffff"] = 1, -- Common (white)
["1eff00"] = 2, -- Uncommon (green)
["0070dd"] = 3, -- Rare (blue)
["a335ee"] = 4, -- Epic (purple)
["ff8000"] = 5, -- Legendary (orange)
["e6cc80"] = 6, -- Artifact
}
local QUALITY_MULT = {
[0] = 0, -- Poor: skip entirely
[1] = 0.50, -- Common: penalty
[2] = 0.90, -- Uncommon: slight penalty
[3] = 0.95, -- Rare: near full
[4] = 1.0, -- Epic: full
[5] = 1.0, -- Legendary: full
[6] = 1.0, -- Artifact: full
}
local function GetQualityFromLink(link)
if not link then return -1 end
local _, _, hex = string.find(link, "|cff(%x%x%x%x%x%x)")
if hex then
return QUALITY_FROM_COLOR[string.lower(hex)] or 1
end
return 1
end
local function GetQualityFromTooltip(tooltip)
if not tooltip then return 1 end
local tipName = tooltip:GetName()
if not tipName then return 1 end
local nameObj = getglobal(tipName .. "TextLeft1")
if not nameObj then return 1 end
local r, g, b = nameObj:GetTextColor()
if not r then return 1 end
-- Gray: r≈0.62, g≈0.62, b≈0.62
if r < 0.7 and g < 0.7 and b < 0.7 then return 0 end
-- White: r=1, g=1, b=1
if r > 0.95 and g > 0.95 and b > 0.95 then return 1 end
-- Green: r≈0.12, g=1, b=0
if g > 0.9 and r < 0.2 then return 2 end
-- Blue: r=0, g≈0.44, b≈0.87
if b > 0.8 and r < 0.1 then return 3 end
-- Purple: r≈0.64, g≈0.21, b≈0.93
if b > 0.85 and r > 0.5 then return 4 end
-- Orange: r=1, g≈0.5, b=0
if r > 0.9 and g > 0.4 and b < 0.1 then return 5 end
return 1
end
-- Normalize item class/subclass strings (Chinese client support)
local ARMOR_CLASS_NAMES = { Armor=true, ["护甲"]=true, ["铠甲"]=true }
local ARMOR_MISC_NAMES = { Miscellaneous=true, ["其它"]=true, ["杂项"]=true, ["其他"]=true }
local SHIELD_NAMES = { Shield=true, Shields=true, ["盾牌"]=true, [""]=true }
local SUBCLASS_NORMALIZE = {
Plate="Plate", ["板甲"]="Plate", ["铠甲"]="Plate",
Mail="Mail", ["锁甲"]="Mail", ["链甲"]="Mail",
Leather="Leather", ["皮甲"]="Leather", ["皮革"]="Leather",
Cloth="Cloth", ["布甲"]="Cloth", ["布料"]="Cloth",
}
-- Parse equipLoc and armor type directly from tooltip text
local SLOT_TEXT_TO_INVTYPE = {
["Head"]="INVTYPE_HEAD", ["头部"]="INVTYPE_HEAD",
["Neck"]="INVTYPE_NECK", ["颈部"]="INVTYPE_NECK",
["Shoulder"]="INVTYPE_SHOULDER", ["肩部"]="INVTYPE_SHOULDER",
["Shirt"]="INVTYPE_BODY", ["衬衣"]="INVTYPE_BODY",
["Chest"]="INVTYPE_CHEST", ["胸部"]="INVTYPE_CHEST",
["Robe"]="INVTYPE_ROBE", ["胸甲"]="INVTYPE_ROBE",
["Waist"]="INVTYPE_WAIST", ["腰部"]="INVTYPE_WAIST",
["Legs"]="INVTYPE_LEGS", ["腿部"]="INVTYPE_LEGS",
["Feet"]="INVTYPE_FEET", [""]="INVTYPE_FEET", [""]="INVTYPE_FEET",
["Wrist"]="INVTYPE_WRIST", ["手腕"]="INVTYPE_WRIST",
["Hands"]="INVTYPE_HAND", [""]="INVTYPE_HAND",
["Finger"]="INVTYPE_FINGER", ["手指"]="INVTYPE_FINGER",
["Trinket"]="INVTYPE_TRINKET", ["饰品"]="INVTYPE_TRINKET",
["Back"]="INVTYPE_CLOAK", ["背部"]="INVTYPE_CLOAK",
["Main Hand"]="INVTYPE_WEAPONMAINHAND", ["主手"]="INVTYPE_WEAPONMAINHAND",
["One-Hand"]="INVTYPE_WEAPON", ["单手"]="INVTYPE_WEAPON",
["Off Hand"]="INVTYPE_WEAPONOFFHAND", ["副手"]="INVTYPE_WEAPONOFFHAND",
["Two-Hand"]="INVTYPE_2HWEAPON", ["双手"]="INVTYPE_2HWEAPON",
["Held In Off-hand"]="INVTYPE_HOLDABLE", ["副手物品"]="INVTYPE_HOLDABLE",
["Ranged"]="INVTYPE_RANGED", ["远程"]="INVTYPE_RANGED",
["Thrown"]="INVTYPE_THROWN", ["投掷"]="INVTYPE_THROWN",
["Relic"]="INVTYPE_RELIC", ["圣物"]="INVTYPE_RELIC",
["Shield"]="INVTYPE_SHIELD", ["盾牌"]="INVTYPE_SHIELD",
}
local function ParseEquipLocFromTooltip(tooltip)
if not tooltip then return nil, nil, nil end
local tipName = tooltip:GetName()
if not tipName then return nil, nil, nil end
local numLines = tooltip:NumLines()
if not numLines or numLines < 2 then return nil, nil, nil end
local foundEquipLoc, foundArmorType, foundItemClass
for i = 2, math.min(numLines, 8) do
local leftObj = getglobal(tipName .. "TextLeft" .. i)
local rightObj = getglobal(tipName .. "TextRight" .. i)
if leftObj then
local leftText = leftObj:GetText()
if leftText and leftText ~= "" then
local invtype = SLOT_TEXT_TO_INVTYPE[leftText]
if invtype then
foundEquipLoc = invtype
if rightObj then
local rightText = rightObj:GetText()
if rightText and rightText ~= "" then
foundArmorType = rightText
if ARMOR_CLASS_NAMES[rightText] or SUBCLASS_NORMALIZE[rightText]
or SHIELD_NAMES[rightText] then
foundItemClass = "Armor"
end
end
end
break
end
end
end
end
return foundEquipLoc, foundArmorType, foundItemClass
end
local GS_EQUIP_LOCS = {
INVTYPE_HEAD=true, INVTYPE_NECK=true, INVTYPE_SHOULDER=true,
INVTYPE_BODY=true, INVTYPE_CHEST=true, INVTYPE_ROBE=true,
INVTYPE_WAIST=true, INVTYPE_LEGS=true, INVTYPE_FEET=true,
INVTYPE_WRIST=true, INVTYPE_HAND=true,
INVTYPE_FINGER=true, INVTYPE_TRINKET=true,
INVTYPE_CLOAK=true,
INVTYPE_WEAPON=true, INVTYPE_2HWEAPON=true, INVTYPE_WEAPONMAINHAND=true,
INVTYPE_SHIELD=true, INVTYPE_WEAPONOFFHAND=true, INVTYPE_HOLDABLE=true,
INVTYPE_RANGED=true, INVTYPE_RANGEDRIGHT=true, INVTYPE_THROWN=true,
INVTYPE_RELIC=true,
}
--------------------------------------------------------------------------------
-- Tooltip text parser (zero external dependency)
--------------------------------------------------------------------------------
local STAT_PATTERNS = {
-- Weapon DPS (float, e.g. "(每秒伤害19.7)" or "(12.3 damage per second)")
{ p = "%((%d+%.?%d*) damage per second%)", s = "WEAPONDPS" },
{ p = "每秒伤害(%d+%.?%d*)", s = "WEAPONDPS" },
{ p = "每秒(%d+%.?%d*)点伤害", s = "WEAPONDPS" },
-- Weapon attack speed ("速度 1.50" / "Speed 1.50") — stored as raw value
{ p = "^速度 (%d+%.?%d*)", s = "WEAPONSPEED" },
{ p = "^Speed (%d+%.?%d*)", s = "WEAPONSPEED" },
-- Base armor value ("63点护甲" / "123 护甲" / "500 Armor", no + prefix)
{ p = "^(%d+)点护甲", s = "BASEARMOR" },
{ p = "^(%d+) 点护甲", s = "BASEARMOR" },
{ p = "^(%d+) 护甲$", s = "BASEARMOR" },
{ p = "^(%d+) Armor$", s = "BASEARMOR" },
-- Bonus armor ("+30 Armor" / "+30 护甲" from enchants)
{ p = "%+(%d+) Armor", s = "ARMOR" },
{ p = "%+(%d+) 护甲", s = "ARMOR" },
{ p = "护甲值提高(%d+)", s = "ARMOR" },
-- Primary stats
{ p = "%+(%d+) Strength", s = "STR" },
{ p = "%+(%d+) Agility", s = "AGI" },
{ p = "%+(%d+) Stamina", s = "STA" },
{ p = "%+(%d+) Intellect", s = "INT" },
{ p = "%+(%d+) Spirit", s = "SPI" },
{ p = "%+(%d+) 力量", s = "STR" },
{ p = "%+(%d+) 敏捷", s = "AGI" },
{ p = "%+(%d+) 耐力", s = "STA" },
{ p = "%+(%d+) 智力", s = "INT" },
{ p = "%+(%d+) 精神", s = "SPI" },
-- Crit (spell > ranged > melee, more specific patterns first)
{ p = "critical strike with spells by (%d+)%%", s = "SPELLCRIT" },
{ p = "法术暴击.-(%d+)%%", s = "SPELLCRIT" },
{ p = "法术.-致命一击.-(%d+)%%", s = "SPELLCRIT" },
{ p = "法术.-爆击.-(%d+)%%", s = "SPELLCRIT" },
{ p = "critical strike with ranged weapons by (%d+)%%", s = "RANGEDCRIT" },
{ p = "远程暴击.-(%d+)%%", s = "RANGEDCRIT" },
{ p = "远程.-致命一击.-(%d+)%%", s = "RANGEDCRIT" },
{ p = "critical strike by (%d+)%%", s = "CRIT" },
{ p = "致命一击几率.-(%d+)%%", s = "CRIT" },
{ p = "致命一击.-提高(%d+)%%", s = "CRIT" },
{ p = "致命一击.-(%d+)%%", s = "CRIT" },
{ p = "暴击几率.-(%d+)%%", s = "CRIT" },
{ p = "暴击.-(%d+)%%", s = "CRIT" },
-- Hit (green equip effects: "使你击中目标的几率提高X%", "chance to hit by X%")
{ p = "hit with spells by (%d+)%%", s = "SPELLTOHIT" },
{ p = "法术击中.-(%d+)%%", s = "SPELLTOHIT" },
{ p = "法术命中.-(%d+)%%", s = "SPELLTOHIT" },
{ p = "用法术击中.-几率.-(%d+)%%", s = "SPELLTOHIT" },
{ p = "chance to hit by (%d+)%%", s = "TOHIT" },
{ p = "击中目标.-几率.-(%d+)%%", s = "TOHIT" },
{ p = "击中.-提高(%d+)%%", s = "TOHIT" },
{ p = "命中.-(%d+)%%", s = "TOHIT" },
-- Attack Power
{ p = "%+(%d+) ranged Attack Power", s = "RANGEDATTACKPOWER" },
{ p = "%+(%d+) 远程攻击强度", s = "RANGEDATTACKPOWER" },
{ p = "远程攻击强度提高(%d+)", s = "RANGEDATTACKPOWER" },
{ p = "%+(%d+) Attack Power", s = "ATTACKPOWER" },
{ p = "%+(%d+) 攻击强度", s = "ATTACKPOWER" },
{ p = "攻击强度提高(%d+)", s = "ATTACKPOWER" },
-- Spell damage + healing (combined, MUST be before individual patterns)
{ p = "damage and healing done by magical spells and effects by up to (%d+)", s = "DMG", a = "HEAL" },
{ p = "伤害和治疗效果.-最多(%d+)", s = "DMG", a = "HEAL" },
{ p = "法术伤害和治疗.-(%d+)", s = "DMG", a = "HEAL" },
-- Healing only
{ p = "healing done by spells and effects by up to (%d+)", s = "HEAL" },
{ p = "Increases healing done by up to (%d+)", s = "HEAL" },
{ p = "治疗效果.-最多(%d+)", s = "HEAL" },
{ p = "治疗法术.-最多(%d+)", s = "HEAL" },
{ p = "治疗量.-最多(%d+)", s = "HEAL" },
-- Spell damage only
{ p = "damage done by magical spells and effects by up to (%d+)", s = "DMG" },
{ p = "法术伤害.-最多(%d+)", s = "DMG" },
{ p = "魔法伤害.-最多(%d+)", s = "DMG" },
-- Mana/health regen
{ p = "Restores (%d+) mana per 5 sec", s = "MANAREG" },
{ p = "(%d+) mana per 5 sec", s = "MANAREG" },
{ p = "每5秒恢复(%d+)点法力", s = "MANAREG" },
{ p = "每5秒回复(%d+)点法力", s = "MANAREG" },
{ p = "5秒回复(%d+)点法力", s = "MANAREG" },
{ p = "Restores (%d+) health per 5 sec", s = "HEALTHREG" },
{ p = "每5秒恢复(%d+)点生命", s = "HEALTHREG" },
{ p = "每5秒回复(%d+)点生命", s = "HEALTHREG" },
-- Defense (green equip: "提高你的防御技能X点", "+X Defense")
{ p = "Increased Defense %+(%d+)", s = "DEFENSE" },
{ p = "Defense %+(%d+)", s = "DEFENSE" },
{ p = "%+(%d+) Defense", s = "DEFENSE" },
{ p = "防御技能提高(%d+)", s = "DEFENSE" },
{ p = "防御等级提高(%d+)", s = "DEFENSE" },
{ p = "防御.-提高(%d+)", s = "DEFENSE" },
-- Avoidance (green equip: "使你的躲闪几率提高X%")
{ p = "dodge.-by (%d+)%%", s = "DODGE" },
{ p = "躲闪几率.-(%d+)%%", s = "DODGE" },
{ p = "躲闪.-提高(%d+)%%", s = "DODGE" },
{ p = "躲闪.-(%d+)%%", s = "DODGE" },
{ p = "parry.-by (%d+)%%", s = "PARRY" },
{ p = "招架几率.-(%d+)%%", s = "PARRY" },
{ p = "招架.-提高(%d+)%%", s = "PARRY" },
{ p = "招架.-(%d+)%%", s = "PARRY" },
{ p = "block attacks.-by (%d+)%%", s = "BLOCK" },
{ p = "格挡几率.-(%d+)%%", s = "BLOCK" },
{ p = "格挡.-提高(%d+)%%", s = "BLOCK" },
{ p = "格挡率.-(%d+)%%", s = "BLOCK" },
{ p = "block value.-by (%d+)", s = "BLOCKVALUE" },
{ p = "格挡值.-(%d+)", s = "BLOCKVALUE" },
{ p = "盾牌格挡值.-(%d+)", s = "BLOCKVALUE" },
-- HP/Mana
{ p = "%+(%d+) Health", s = "HEALTH" },
{ p = "%+(%d+) Hit Points", s = "HEALTH" },
{ p = "%+(%d+) 生命值", s = "HEALTH" },
{ p = "%+(%d+) Mana", s = "MANA" },
{ p = "%+(%d+) 法力值", s = "MANA" },
}
--------------------------------------------------------------------------------
-- Debug helper: /gsdebug to toggle
--------------------------------------------------------------------------------
local GS_DEBUG = false
local function GSDebug(msg)
if GS_DEBUG and DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage("|cffFFFF00[GS]|r " .. tostring(msg))
end
end
SLASH_GSDEBUG1 = "/gsdebug"
SlashCmdList["GSDEBUG"] = function()
GS_DEBUG = not GS_DEBUG
DEFAULT_CHAT_FRAME:AddMessage("|cffFFFF00[GearScore]|r Debug: " .. (GS_DEBUG and "ON" or "OFF"))
end
--------------------------------------------------------------------------------
-- Stat parser: scan a VISIBLE tooltip's text lines directly
-- This is the most reliable method — no hidden tooltip, no library needed
--------------------------------------------------------------------------------
local function ScanTooltipForStats(tooltip)
if not tooltip then return nil end
local tipName = tooltip:GetName()
if not tipName then return nil end
local numLines = tooltip:NumLines()
if not numLines or numLines < 2 then return nil end
local stats = {}
for i = 2, numLines do
local lineObj = getglobal(tipName .. "TextLeft" .. i)
if lineObj then
local text = lineObj:GetText()
if text and text ~= "" then
for _, pat in ipairs(STAT_PATTERNS) do
local _, _, val = string.find(text, pat.p)
if val then
local n = tonumber(val)
if n and n > 0 then
stats[pat.s] = (stats[pat.s] or 0) + n
if pat.a then
stats[pat.a] = (stats[pat.a] or 0) + n
end
GSDebug(" L" .. i .. ": " .. pat.s .. "=" .. n .. " (" .. text .. ")")
end
break
end
end
end
end
end
local hasAny = false
for _ in pairs(stats) do hasAny = true; break end
return hasAny and stats or nil
end
local function ParseItemWithLib(link)
if not link then return nil end
if not AceLibrary or not AceLibrary.HasInstance then return nil end
if not AceLibrary:HasInstance("ItemBonusLib-1.0") then return nil end
local lib = AceLibrary("ItemBonusLib-1.0")
if not lib or not lib.ScanItem then return nil end
local ok, result = pcall(function() return lib:ScanItem(link, true) end)
if ok and result then
local hasAny = false
for _ in pairs(result) do hasAny = true; break end
if hasAny then return result end
end
return nil
end
--------------------------------------------------------------------------------
-- Compatibility multipliers
--------------------------------------------------------------------------------
local function GetArmorCompat(classToken, itemClass, itemSubClass, level)
if not itemClass or not ARMOR_CLASS_NAMES[itemClass] then return 1.0 end
if not itemSubClass or itemSubClass == "" then return 1.0 end
if ARMOR_MISC_NAMES[itemSubClass] then return 1.0 end
if SHIELD_NAMES[itemSubClass] then return 1.0 end
local normalizedSub = SUBCLASS_NORMALIZE[itemSubClass]
if not normalizedSub then return 1.0 end
local tbl = (level < 40) and ARMOR_COMPAT_LOW or ARMOR_COMPAT
local classTbl = tbl[classToken]
if not classTbl then return 1.0 end
return classTbl[normalizedSub] or 0.3
end
local function GetSlotCompat(classToken, specIdx, equipLoc)
if not equipLoc or equipLoc == "" then return 1.0 end
local key = classToken .. "_" .. specIdx
local tbl = SLOT_COMPAT[key]
if not tbl then return 1.0 end
local val = tbl[equipLoc]
if val ~= nil then return val end
return 1.0
end
--------------------------------------------------------------------------------
-- Core scoring: 1-10 normalized scale
-- Score = (actual_EP / ideal_EP) * 10 * equipment_compat
-- ideal_EP = total_budget * best_efficiency_for_spec
--------------------------------------------------------------------------------
-- Slot-specific and non-standard stats excluded from reference efficiency
local EFF_EXCLUDE = { WEAPONDPS=true, WEAPONSPEED=true, BASEARMOR=true, HEALTH=true, MANA=true }
-- Stats that only appear on higher-level items
local EFF_LATE_GAME = { DMG=true, HEAL=true, SPELLTOHIT=true, SPELLCRIT=true,
TOHIT=true, CRIT=true, RANGEDCRIT=true }
local EFF_MID_GAME = { DMG=true, HEAL=true }
local function GetRefEfficiency(weights, level)
local effs = {}
for stat, w in pairs(weights) do
if not EFF_EXCLUDE[stat] then
local cost = BUDGET_COST[stat]
if cost and cost > 0 and w > 0 then
local skip = false
if level and level < 25 and EFF_LATE_GAME[stat] then skip = true end
if level and level >= 25 and level < 40 and EFF_MID_GAME[stat] then skip = true end
if not skip then
table.insert(effs, w / cost)
end
end
end
end
table.sort(effs, function(a,b) return a > b end)
local n = table.getn(effs)
local ref = 0
if n >= 3 then
ref = effs[1] * 0.45 + effs[2] * 0.30 + effs[3] * 0.25
elseif n == 2 then
ref = effs[1] * 0.55 + effs[2] * 0.45
elseif n == 1 then
ref = effs[1]
end
if ref <= 0 then ref = 1.0 end
return ref
end
local DPS_DAMPEN_MELEE = 0.40
local DPS_DAMPEN_RANGED = 0.70
local function CalcRawEP(bonuses, weights, dpsDampen)
if not bonuses or not weights then return 0 end
dpsDampen = dpsDampen or DPS_DAMPEN_MELEE
local ep = 0
for stat, value in pairs(bonuses) do
local w = weights[stat]
if w and w > 0 then
if stat == "WEAPONDPS" then
ep = ep + value * w * dpsDampen
elseif stat == "WEAPONSPEED" then
local speedBonus = 3.0 - value
if speedBonus > 0 then
ep = ep + speedBonus * w
end
else
ep = ep + value * w
end
end
end
return ep
end
local function CalcTotalBudget(bonuses)
if not bonuses then return 0 end
local total = 0
for stat, value in pairs(bonuses) do
local cost = BUDGET_COST[stat]
if cost and cost > 0 then
if stat == "WEAPONSPEED" then
local speedBonus = 3.0 - value
if speedBonus > 0 then
total = total + speedBonus * cost
end
else
total = total + math.abs(value) * cost
end
end
end
return total
end
local GS_RANGED_LOCS = {
INVTYPE_RANGED = true, INVTYPE_RANGEDRIGHT = true, INVTYPE_THROWN = true,
}
--------------------------------------------------------------------------------
-- Horizontal comparison: reference EP for the best rare item at level/slot
--------------------------------------------------------------------------------
local SLOT_BUDGET_MOD = {
INVTYPE_HEAD = 1.0, INVTYPE_CHEST = 1.0, INVTYPE_ROBE = 1.0, INVTYPE_LEGS = 1.0,
INVTYPE_SHOULDER = 0.77, INVTYPE_HAND = 0.77, INVTYPE_WAIST = 0.77, INVTYPE_FEET = 0.77,
INVTYPE_WRIST = 0.56, INVTYPE_CLOAK = 0.56,
INVTYPE_NECK = 0.56, INVTYPE_FINGER = 0.56, INVTYPE_TRINKET = 0.56,
INVTYPE_WEAPON = 0.42, INVTYPE_WEAPONMAINHAND = 0.42, INVTYPE_WEAPONOFFHAND = 0.36,
INVTYPE_2HWEAPON = 1.0,
INVTYPE_SHIELD = 0.56, INVTYPE_HOLDABLE = 0.42,
INVTYPE_RANGED = 0.32, INVTYPE_RANGEDRIGHT = 0.32, INVTYPE_THROWN = 0.32,
INVTYPE_RELIC = 0.32, INVTYPE_TABARD = 0, INVTYPE_BODY = 0,
}
local WEAPON_EQUIP_LOCS = {
INVTYPE_WEAPON = true, INVTYPE_WEAPONMAINHAND = true, INVTYPE_WEAPONOFFHAND = true,
INVTYPE_2HWEAPON = true, INVTYPE_RANGED = true, INVTYPE_RANGEDRIGHT = true,
INVTYPE_THROWN = true,
}
local ARMOR_EQUIP_LOCS = {
INVTYPE_HEAD = true, INVTYPE_CHEST = true, INVTYPE_ROBE = true, INVTYPE_LEGS = true,
INVTYPE_SHOULDER = true, INVTYPE_HAND = true, INVTYPE_WAIST = true, INVTYPE_FEET = true,
INVTYPE_WRIST = true, INVTYPE_SHIELD = true,
}
local function GetRefWeaponDPS(equipLoc, level)
local dps
if level >= 58 then dps = 48
elseif level >= 40 then dps = 28 + (level - 40) * 0.55
elseif level >= 20 then dps = 13 + (level - 20) * 0.75
elseif level >= 10 then dps = 6 + (level - 10) * 0.7
else dps = 3 + level * 0.3
end
if equipLoc == "INVTYPE_2HWEAPON" then dps = dps * 1.3 end
return dps
end
local function GetReferenceEP(equipLoc, level, weights, refEff)
local slotMod = SLOT_BUDGET_MOD[equipLoc] or 0.56
local refIlvl
if level >= 58 then refIlvl = 63
elseif level >= 40 then refIlvl = level + 7
elseif level >= 20 then refIlvl = level + 5
else refIlvl = level + 3
end
local statBudget = refIlvl * 0.65 * slotMod
local ep = statBudget * refEff
if WEAPON_EQUIP_LOCS[equipLoc] then
local wDPS = weights.WEAPONDPS
if wDPS and wDPS > 0 then
local refDPS = GetRefWeaponDPS(equipLoc, level)
local dampen = GS_RANGED_LOCS[equipLoc] and DPS_DAMPEN_RANGED or DPS_DAMPEN_MELEE
ep = ep + refDPS * wDPS * dampen
end
end
if ARMOR_EQUIP_LOCS[equipLoc] then
local wArmor = weights.BASEARMOR
if wArmor and wArmor > 0 then
local refArmor = level * 1.5 * slotMod
ep = ep + refArmor * wArmor
end
end
return ep
end
local function CalcNormalizedScore(bonuses, weights, armorCompat, slotCompat, level, equipLoc)
local dpsDampen = DPS_DAMPEN_MELEE
if equipLoc and GS_RANGED_LOCS[equipLoc] then dpsDampen = DPS_DAMPEN_RANGED end
local rawEP = CalcRawEP(bonuses, weights, dpsDampen)
local refEff = GetRefEfficiency(weights, level)
local refEP = GetReferenceEP(equipLoc, level, weights, refEff)
if refEP <= 0 then return 0 end
local rawScore = (rawEP / refEP) * 10
local compat = (armorCompat or 1.0) * (slotCompat or 1.0)
local finalScore = rawScore * compat
if finalScore < 0 then finalScore = 0 end
if finalScore > 10 then finalScore = 10 end
return math.floor(finalScore * 10 + 0.5) / 10
end
--------------------------------------------------------------------------------
-- Score color (1-10 scale)
--------------------------------------------------------------------------------
local function ScoreColorHex(score)
if score >= 9 then return "ffFF8800" end
if score >= 7 then return "ff00FF00" end
if score >= 5 then return "ffFFFFFF" end
if score >= 3 then return "ffFFFF00" end
return "ffFF4444"
end
local function ScoreLabel(score)
if score >= 9 then return "极品" end
if score >= 7 then return "优秀" end
if score >= 5 then return "适用" end
if score >= 3 then return "一般" end
return "不适"
end
--------------------------------------------------------------------------------
-- Score tooltip (uses GameTooltipTemplate — proven reliable in this addon)
-- Created lazily on first use (nil parent like Trade.lua pattern)
--------------------------------------------------------------------------------
local GSTip
local function GS_ShowTip(parentTip, scoreLines)
if not scoreLines or not parentTip then return end
if not GSTip then
GSTip = CreateFrame("GameTooltip", "NanamiGSTooltip", nil, "GameTooltipTemplate")
GSTip:SetFrameStrata("TOOLTIP")
GSTip:SetClampedToScreen(true)
end
GSTip:SetOwner(parentTip, "ANCHOR_NONE")
GSTip:ClearAllPoints()
local bottom = parentTip:GetBottom()
if bottom and bottom > 80 then
GSTip:SetPoint("TOPLEFT", parentTip, "BOTTOMLEFT", 0, 2)
else
GSTip:SetPoint("BOTTOMLEFT", parentTip, "TOPLEFT", 0, -2)
end
for _, entry in ipairs(scoreLines) do
if entry.left and entry.right then
GSTip:AddDoubleLine(entry.left, entry.right,
entry.lr or 1, entry.lg or 1, entry.lb or 1,
entry.rr or 1, entry.rg or 1, entry.rb or 1)
else
GSTip:AddLine(entry.text or "", entry.r or 1, entry.g or 0.84, entry.b or 0)
end
end
GSTip:Show()
end
local function GS_HideFrame()
if GSTip then GSTip:Hide() end
end
--------------------------------------------------------------------------------
-- Main tooltip function
--------------------------------------------------------------------------------
local GS_SCORE_CACHE = {}
local GS_CACHE_SIZE = 0
local GS_CACHE_MAX = 200
function GS:AddScoreToTooltip(tooltip, link)
if not tooltip or not link then return end
if SFramesDB and SFramesDB.gearScore == false then return end
if tooltip._gsScoreAdded then return end
local classToken = GetPlayerClassToken()
if not classToken then return end
local classData = WEIGHTS[classToken]
if not classData then return end
local cacheKey = classToken .. "|" .. link
local cached = GS_SCORE_CACHE[cacheKey]
if cached then
tooltip._gsScoreAdded = true
if cached.scoreLines then
GS_ShowTip(tooltip, cached.scoreLines)
else
GS_HideFrame()
end
return
end
GSDebug("Processing: " .. tostring(link))
local quality = GetQualityFromLink(link)
if quality < 0 then
quality = GetQualityFromTooltip(tooltip)
end
local qualityMult = QUALITY_MULT[quality] or 0.75
if quality == 0 then
GSDebug("Skipped: poor quality (gray) item")
return
end
GSDebug("Quality=" .. quality .. " mult=" .. qualityMult)
local equipLoc, itemClass, itemSubClass
pcall(function()
local itemStr = ExtractItemString(link)
local infoArg = itemStr or link
GSDebug("GetItemInfo arg: " .. tostring(infoArg))
local results = { GetItemInfo(infoArg) }
if results[1] then
itemClass = results[6]
itemSubClass = results[7]
for idx = 8, 10 do
local v = results[idx]
if type(v) == "string" and (v == "" or string.find(v, "^INVTYPE")) then
equipLoc = v
break
end
end
end
GSDebug("GetItemInfo: equip=" .. tostring(equipLoc)
.. " class=" .. tostring(itemClass) .. " sub=" .. tostring(itemSubClass))
end)
if not equipLoc then
local ttEquip, ttArmor, ttClass = ParseEquipLocFromTooltip(tooltip)
if ttEquip then
equipLoc = ttEquip
if ttArmor and not itemSubClass then itemSubClass = ttArmor end
if ttClass and not itemClass then itemClass = ttClass end
GSDebug("Parsed from tooltip: equip=" .. tostring(equipLoc) .. " armor=" .. tostring(ttArmor))
end
end
if equipLoc == "INVTYPE_BAG" or equipLoc == "INVTYPE_AMMO" or equipLoc == "INVTYPE_TABARD" then
GSDebug("Skipped: bag/ammo/tabard")
return
end
if equipLoc and equipLoc ~= "" and not GS_EQUIP_LOCS[equipLoc] then
GSDebug("Skipped: not equippable (" .. tostring(equipLoc) .. ")")
return
end
local bonuses = ParseItemWithLib(link)
if bonuses then
GSDebug("Stats from ItemBonusLib")
else
bonuses = ScanTooltipForStats(tooltip)
if bonuses then
GSDebug("Stats from tooltip text")
end
end
if not bonuses then
GSDebug("No stats found, skipping")
return
end
if not equipLoc or equipLoc == "" then
GSDebug("No equipLoc, not equipment, skipping")
return
end
local level = GetPlayerLevel()
local primaryTab = GetPrimaryTabIndex()
local isHC = IsPlayerHardcore()
local armorCompat = GetArmorCompat(classToken, itemClass, itemSubClass, level)
GSDebug("ArmorCompat=" .. armorCompat .. " class=" .. classToken
.. " slot=" .. tostring(equipLoc) .. " lv=" .. level)
local specs = classData.specs
if not specs or table.getn(specs) == 0 then return end
local scores = {}
local anyShow = false
for i, spec in ipairs(specs) do
local w = AdjustWeightsForLevel(spec.w, level, false)
local slotCompat = GetSlotCompat(classToken, i, equipLoc)
local refEff = GetRefEfficiency(w, level)
local refEP = GetReferenceEP(equipLoc, level, w, refEff)
local s = CalcNormalizedScore(bonuses, w, armorCompat, slotCompat, level, equipLoc)
GSDebug(" " .. spec.name .. ": rawScore=" .. string.format("%.2f", s)
.. " refEP=" .. string.format("%.1f", refEP) .. " refEff=" .. string.format("%.3f", refEff))
s = math.floor(s * qualityMult * 10 + 0.5) / 10
if s < 1.0 and s > 0 then s = 1.0 end
table.insert(scores, {
name = spec.name,
color = spec.color,
score = s,
isPrimary = (spec.tab == primaryTab),
label = ScoreLabel(s),
})
if s > 0 then anyShow = true end
end
local hcScore = 0
if classData.hc then
local hw = AdjustWeightsForLevel(classData.hc.w, level, true)
hcScore = CalcNormalizedScore(bonuses, hw, armorCompat, 1.0, level, equipLoc)
if not bonuses.STA or bonuses.STA <= 0 then
hcScore = hcScore * 0.35
end
hcScore = math.floor(hcScore * qualityMult * 10 + 0.5) / 10
if hcScore < 1.0 and hcScore > 0 then hcScore = 1.0 end
if hcScore > 0 then anyShow = true end
end
if not anyShow then
GS_SCORE_CACHE[cacheKey] = { scoreLines = nil }
GS_CACHE_SIZE = GS_CACHE_SIZE + 1
GS_HideFrame()
return
end
tooltip._gsScoreAdded = true
local scoreLines = {}
table.insert(scoreLines, { text = "── 装备评分 ──", r = 1, g = 0.84, b = 0 })
for _, sd in ipairs(scores) do
local star = sd.isPrimary and "" or " "
local sStr = string.format("%.1f", sd.score)
local sColor = ScoreColorHex(sd.score)
table.insert(scoreLines, {
left = star .. "|c" .. sd.color .. sd.name .. "|r",
right = "|c" .. sColor .. sStr .. " " .. sd.label .. "|r",
})
end
if classData.hc and hcScore > 0 then
local hcSColor = ScoreColorHex(hcScore)
local hcStar = isHC and "" or " "
table.insert(scoreLines, {
left = hcStar .. "|c" .. classData.hc.color .. "硬核|r",
right = "|c" .. hcSColor .. string.format("%.1f", hcScore)
.. " " .. ScoreLabel(hcScore) .. "|r",
})
end
GS_ShowTip(tooltip, scoreLines)
if GS_CACHE_SIZE >= GS_CACHE_MAX then
GS_SCORE_CACHE = {}
GS_CACHE_SIZE = 0
end
GS_SCORE_CACHE[cacheKey] = { scoreLines = scoreLines }
GS_CACHE_SIZE = GS_CACHE_SIZE + 1
end
--------------------------------------------------------------------------------
-- Self-contained tooltip hooks
--------------------------------------------------------------------------------
local function GS_TryEnhance(tooltip, link)
if not link then return end
pcall(function() GS:AddScoreToTooltip(tooltip, link) end)
end
function GS:HookTooltips()
if self._hooked then return end
self._hooked = true
local origHide = GameTooltip:GetScript("OnHide")
GameTooltip:SetScript("OnHide", function()
this._gsScoreAdded = nil
GS_HideFrame()
if origHide then origHide() end
end)
local origBag = GameTooltip.SetBagItem
GameTooltip.SetBagItem = function(self, bag, slot)
self._gsScoreAdded = nil
local r1, r2, r3 = origBag(self, bag, slot)
GS_TryEnhance(self, GetContainerItemLink(bag, slot))
return r1, r2, r3
end
local origInv = GameTooltip.SetInventoryItem
GameTooltip.SetInventoryItem = function(self, unit, slotId)
self._gsScoreAdded = nil
local r1, r2, r3 = origInv(self, unit, slotId)
if unit and slotId then
GS_TryEnhance(self, GetInventoryItemLink(unit, slotId))
end
return r1, r2, r3
end
if GameTooltip.SetMerchantItem then
local orig = GameTooltip.SetMerchantItem
GameTooltip.SetMerchantItem = function(self, idx)
self._gsScoreAdded = nil
orig(self, idx)
if GetMerchantItemLink then GS_TryEnhance(self, GetMerchantItemLink(idx)) end
end
end
if GameTooltip.SetQuestItem then
local orig = GameTooltip.SetQuestItem
GameTooltip.SetQuestItem = function(self, qtype, idx)
self._gsScoreAdded = nil
orig(self, qtype, idx)
if GetQuestItemLink then GS_TryEnhance(self, GetQuestItemLink(qtype, idx)) end
end
end
if GameTooltip.SetQuestLogItem then
local orig = GameTooltip.SetQuestLogItem
GameTooltip.SetQuestLogItem = function(self, itype, idx)
self._gsScoreAdded = nil
orig(self, itype, idx)
if GetQuestLogItemLink then GS_TryEnhance(self, GetQuestLogItemLink(itype, idx)) end
end
end
if GameTooltip.SetLootItem then
local orig = GameTooltip.SetLootItem
GameTooltip.SetLootItem = function(self, idx)
self._gsScoreAdded = nil
orig(self, idx)
if GetLootSlotLink then GS_TryEnhance(self, GetLootSlotLink(idx)) end
end
end
if GameTooltip.SetLootRollItem then
local orig = GameTooltip.SetLootRollItem
GameTooltip.SetLootRollItem = function(self, rollId)
self._gsScoreAdded = nil
orig(self, rollId)
if GetLootRollItemLink then GS_TryEnhance(self, GetLootRollItemLink(rollId)) end
end
end
if GameTooltip.SetAuctionItem then
local orig = GameTooltip.SetAuctionItem
GameTooltip.SetAuctionItem = function(self, atype, idx)
self._gsScoreAdded = nil
orig(self, atype, idx)
if GetAuctionItemLink then GS_TryEnhance(self, GetAuctionItemLink(atype, idx)) end
end
end
if GameTooltip.SetTradeSkillItem then
local orig = GameTooltip.SetTradeSkillItem
GameTooltip.SetTradeSkillItem = function(self, skillIdx, reagentIdx)
self._gsScoreAdded = nil
orig(self, skillIdx, reagentIdx)
local link
if reagentIdx then
if GetTradeSkillReagentItemLink then link = GetTradeSkillReagentItemLink(skillIdx, reagentIdx) end
else
if GetTradeSkillItemLink then link = GetTradeSkillItemLink(skillIdx) end
end
GS_TryEnhance(self, link)
end
end
if GameTooltip.SetCraftItem then
local orig = GameTooltip.SetCraftItem
GameTooltip.SetCraftItem = function(self, skill, slot)
self._gsScoreAdded = nil
orig(self, skill, slot)
if GetCraftReagentItemLink then GS_TryEnhance(self, GetCraftReagentItemLink(skill, slot)) end
end
end
if GameTooltip.SetTradePlayerItem then
local orig = GameTooltip.SetTradePlayerItem
GameTooltip.SetTradePlayerItem = function(self, idx)
self._gsScoreAdded = nil
orig(self, idx)
if GetTradePlayerItemLink then GS_TryEnhance(self, GetTradePlayerItemLink(idx)) end
end
end
if GameTooltip.SetTradeTargetItem then
local orig = GameTooltip.SetTradeTargetItem
GameTooltip.SetTradeTargetItem = function(self, idx)
self._gsScoreAdded = nil
orig(self, idx)
if GetTradeTargetItemLink then GS_TryEnhance(self, GetTradeTargetItemLink(idx)) end
end
end
if GameTooltip.SetInboxItem then
local orig = GameTooltip.SetInboxItem
GameTooltip.SetInboxItem = function(self, mailID, attachIdx)
self._gsScoreAdded = nil
orig(self, mailID, attachIdx)
local left1 = getglobal("GameTooltipTextLeft1")
if left1 and GetItemLinkByName then
GS_TryEnhance(self, GetItemLinkByName(left1:GetText()))
end
end
end
end
--------------------------------------------------------------------------------
-- Initialize
--------------------------------------------------------------------------------
local initFrame = CreateFrame("Frame")
initFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
initFrame:SetScript("OnEvent", function()
initFrame:UnregisterEvent("PLAYER_ENTERING_WORLD")
GS:HookTooltips()
end)