-------------------------------------------------------------------------------- -- 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 local origRef = SetItemRef if origRef then SetItemRef = function(link, text, button) origRef(link, text, button) if IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then return end pcall(function() local _, _, itemStr = string.find(link or "", "(item:[%-?%d:]+)") if itemStr then ItemRefTooltip._gsScoreAdded = nil GS:AddScoreToTooltip(ItemRefTooltip, itemStr) end 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)