-------------------------------------------------------------------------------- -- Nanami-UI: Character Panel (CharacterPanel.lua) -- Replaces the default CharacterFrame with a modern rounded UI -------------------------------------------------------------------------------- SFrames.CharacterPanel = {} local CP = SFrames.CharacterPanel local panel, tabs, pages -------------------------------------------------------------------------------- -- Theme -------------------------------------------------------------------------------- local T = SFrames.Theme:Extend({ repColors = { [1] = { 0.8, 0.13, 0.13 }, [2] = { 0.8, 0.3, 0.22 }, [3] = { 0.75, 0.55, 0.15 }, [4] = { 0.9, 0.7, 0.0 }, [5] = { 0.0, 0.6, 0.1 }, [6] = { 0.0, 0.7, 0.3 }, [7] = { 0.0, 0.6, 0.75 }, [8] = { 0.0, 0.5, 0.9 }, }, resistColors = { [2] = { 1, 0.5, 0.15 }, [3] = { 0.35, 0.9, 0.25 }, [4] = { 0.45, 0.7, 1 }, [5] = { 0.6, 0.35, 0.9 }, [6] = { 0.95, 0.9, 0.4 }, }, }) local FRAME_W = 340 local FRAME_H = 490 local HEADER_H = 28 local TAB_H = 22 local TAB_BAR_H = 26 local CONTENT_TOP = -(HEADER_H + TAB_BAR_H) local SIDE_PAD = 8 local INNER_PAD = 4 local CONTENT_W = FRAME_W - INNER_PAD * 2 local SCROLL_W = CONTENT_W - 10 local SLOT_SIZE = 32 local SLOT_GAP = 2 local QUALITY_COLORS = { [0] = { 0.62, 0.62, 0.62 }, [1] = { 1, 1, 1 }, [2] = { 0.12, 1, 0 }, [3] = { 0.0, 0.44, 0.87 }, [4] = { 0.64, 0.21, 0.93 }, [5] = { 1, 0.5, 0 }, } local SLOT_LABEL = { HeadSlot = "头部", NeckSlot = "颈部", ShoulderSlot = "肩部", BackSlot = "背部", ChestSlot = "胸部", ShirtSlot = "衬衣", WristSlot = "手腕", HandsSlot = "手套", WaistSlot = "腰带", LegsSlot = "腿部", FeetSlot = "脚部", Finger0Slot = "戒指1", Finger1Slot = "戒指2", Trinket0Slot = "饰品1", Trinket1Slot = "饰品2", MainHandSlot = "主手", SecondaryHandSlot = "副手", RangedSlot = "远程", AmmoSlot = "弹药", TabardSlot = "战袍", } local EQUIP_SLOTS_LEFT = { { id = 1, name = "HeadSlot" }, { id = 2, name = "NeckSlot" }, { id = 3, name = "ShoulderSlot" }, { id = 15, name = "BackSlot" }, { id = 5, name = "ChestSlot" }, { id = 4, name = "ShirtSlot" }, { id = 19, name = "TabardSlot" }, { id = 9, name = "WristSlot" }, } local EQUIP_SLOTS_RIGHT = { { id = 10, name = "HandsSlot" }, { id = 6, name = "WaistSlot" }, { id = 7, name = "LegsSlot" }, { id = 8, name = "FeetSlot" }, { id = 11, name = "Finger0Slot" }, { id = 12, name = "Finger1Slot" }, { id = 13, name = "Trinket0Slot" }, { id = 14, name = "Trinket1Slot" }, } local EQUIP_SLOTS_BOTTOM = { { id = 16, name = "MainHandSlot" }, { id = 17, name = "SecondaryHandSlot" }, { id = 18, name = "RangedSlot" }, { id = 0, name = "AmmoSlot" }, } local TAB_NAMES = { "装备", "声望", "技能", "荣誉" } local STAT_NAMES = { "力量", "敏捷", "耐力", "智力", "精神" } local RESIST_NAMES = { [2] = "火焰", [3] = "自然", [4] = "冰霜", [5] = "暗影", [6] = "奥术" } local REP_STANDING = { [1] = "仇恨", [2] = "敌对", [3] = "不友好", [4] = "中立", [5] = "友善", [6] = "尊敬", [7] = "崇敬", [8] = "崇拜", } -------------------------------------------------------------------------------- -- EP Stat Weights per class (Turtle WoW) -- Physical DPS: 1 AP = 1 EP; Caster DPS: 1 SP = 1 EP -- Source: Turtle WoW deep theorycraft & EP evaluation report -------------------------------------------------------------------------------- local STAT_WEIGHTS = { WARRIOR = { STR = 2.0, AGI = 1.5, STA = 1.0, INT = 0, SPI = 0, ATTACKPOWER = 1.0, CRIT = 25, TOHIT = 18, DEFENSE = 1.5, DODGE = 12, PARRY = 12, BLOCK = 8, BLOCKVALUE = 0.5, ARMOR = 0.02, HEALTHREG = 2, HEALTH = 0.1, }, ROGUE = { STR = 1.0, AGI = 2.0, STA = 0.5, INT = 0, SPI = 0, ATTACKPOWER = 1.0, CRIT = 25, TOHIT = 18, DODGE = 10, HEALTH = 0.067, }, HUNTER = { STR = 0.5, AGI = 2.0, STA = 0.5, INT = 0.3, SPI = 0.1, ATTACKPOWER = 1.0, RANGEDATTACKPOWER = 1.0, CRIT = 25, TOHIT = 18, RANGEDCRIT = 25, HEALTH = 0.067, MANA = 0.033, }, DRUID = { STR = 2.4, AGI = 1.5, STA = 1.0, INT = 0.5, SPI = 0.5, ATTACKPOWER = 1.0, ATTACKPOWERFERAL = 1.0, CRIT = 25, TOHIT = 18, DMG = 1.0, HEAL = 0.8, SPELLCRIT = 8, SPELLTOHIT = 14, DEFENSE = 1.2, DODGE = 10, ARMOR = 0.02, MANAREG = 3, HEALTHREG = 2, HEALTH = 0.1, MANA = 0.033, }, PALADIN = { STR = 2.0, AGI = 1.0, STA = 1.0, INT = 0.5, SPI = 0.3, ATTACKPOWER = 1.0, CRIT = 25, TOHIT = 18, DMG = 1.0, HEAL = 0.8, HOLYDMG = 1.0, SPELLCRIT = 8, SPELLTOHIT = 14, DEFENSE = 1.5, DODGE = 12, PARRY = 12, BLOCK = 8, BLOCKVALUE = 0.5, ARMOR = 0.02, MANAREG = 3, HEALTH = 0.1, MANA = 0.033, }, SHAMAN = { STR = 1.0, AGI = 1.0, STA = 0.8, INT = 0.5, SPI = 0.3, ATTACKPOWER = 1.0, CRIT = 20, TOHIT = 15, DMG = 1.0, HEAL = 0.8, NATUREDMG = 1.0, SPELLCRIT = 8, SPELLTOHIT = 14, MANAREG = 3, HEALTH = 0.1, MANA = 0.033, }, MAGE = { STR = 0, AGI = 0, STA = 0.3, INT = 0.125, SPI = 0.5, DMG = 1.0, ARCANEDMG = 1.0, FIREDMG = 1.0, FROSTDMG = 1.0, SPELLCRIT = 8, SPELLTOHIT = 14, MANAREG = 3, HEALTH = 0.067, MANA = 0.033, }, WARLOCK = { STR = 0, AGI = 0, STA = 0.5, INT = 0.125, SPI = 0.3, DMG = 1.0, SHADOWDMG = 1.0, FIREDMG = 1.0, SPELLCRIT = 8, SPELLTOHIT = 14, MANAREG = 3, HEALTH = 0.1, MANA = 0.033, }, PRIEST = { STR = 0, AGI = 0, STA = 0.3, INT = 0.5, SPI = 1.0, DMG = 0.8, HEAL = 1.0, SHADOWDMG = 0.8, HOLYDMG = 0.8, SPELLCRIT = 8, SPELLTOHIT = 14, MANAREG = 3, HEALTH = 0.067, MANA = 0.033, }, } local widgetId = 0 -------------------------------------------------------------------------------- -- Helpers -------------------------------------------------------------------------------- local function GetFont() if SFrames and SFrames.GetFont then return SFrames:GetFont() end return "Fonts\\ARIALN.TTF" end local function SetRoundBackdrop(frame, bgColor, borderColor) frame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 3, right = 3, top = 3, bottom = 3 } }) local bg = bgColor or T.bg local bd = borderColor or T.border frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) end local function SetPixelBackdrop(frame, bgColor, borderColor) frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) if bgColor then frame:SetBackdropColor(bgColor[1], bgColor[2], bgColor[3], bgColor[4] or 1) end if borderColor then frame:SetBackdropBorderColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1) end end local function CreateShadow(parent, size) local s = CreateFrame("Frame", nil, parent) local sz = size or 4 s:SetPoint("TOPLEFT", parent, "TOPLEFT", -sz, sz) s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", sz, -sz) s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0)) s:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 } }) s:SetBackdropColor(0, 0, 0, 0.6) s:SetBackdropBorderColor(0, 0, 0, 0.45) return s end local function MakeSep(parent, x1, y1, x2, y2) local sep = parent:CreateTexture(nil, "ARTWORK") sep:SetTexture("Interface\\Buttons\\WHITE8X8") sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) sep:SetHeight(1) sep:SetPoint("TOPLEFT", parent, "TOPLEFT", x1, y1) sep:SetPoint("TOPRIGHT", parent, "TOPRIGHT", x2, y1) return sep end local function MakeFS(parent, size, justifyH, color) local fs = parent:CreateFontString(nil, "OVERLAY") fs:SetFont(GetFont(), size or 11, "OUTLINE") fs:SetJustifyH(justifyH or "LEFT") local c = color or T.valueText fs:SetTextColor(c[1], c[2], c[3]) return fs end local function NextName(p) widgetId = widgetId + 1 return "SFramesCP" .. (p or "") .. tostring(widgetId) end local function Clamp(v, lo, hi) if v < lo then return lo end if v > hi then return hi end return v end -------------------------------------------------------------------------------- -- Combat stat helpers: multi-strategy retrieval for Turtle WoW compatibility -- 1) Standard 1.12 API (GetCritChance, etc.) -- 2) GetCombatRatingBonus (TBC-style, Turtle WoW may support) -- 3) Manual calc: agility/intellect base + ItemBonusLib gear bonus -------------------------------------------------------------------------------- local function TryAPIs(names) for _, name in ipairs(names) do local fn = _G[name] if fn then local ok, val = pcall(fn) if ok and type(val) == "number" and val > 0 then return val end end end return 0 end local function TryAPIsArg(names, arg1) for _, name in ipairs(names) do local fn = _G[name] if fn then local ok, val = pcall(fn, arg1) if ok and type(val) == "number" and val > 0 then return val end end end return 0 end local function TryCombatRating(crId) if GetCombatRatingBonus then local ok, val = pcall(GetCombatRatingBonus, crId) if ok and type(val) == "number" and val > 0 then return val end end return 0 end -- Try to get ItemBonusLib from AceLibrary (provides gear-only crit/hit/etc.) local function GetItemBonusLib() if AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("ItemBonusLib-1.0") then return AceLibrary("ItemBonusLib-1.0") end return nil end local function GetGearBonus(key) local lib = GetItemBonusLib() if lib and lib.GetBonus then return lib:GetBonus(key) or 0 end return 0 end local function CalcItemEP(link, class) local weights = STAT_WEIGHTS[class] if not weights or not link then return 0 end local lib = GetItemBonusLib() if not lib or not lib.ScanItemLink then return 0 end local ok, info = pcall(function() return lib:ScanItemLink(link) end) if not ok or not info or not info.bonuses then return 0 end local ep = 0 for stat, value in pairs(info.bonuses) do local w = weights[stat] if w and type(value) == "number" then ep = ep + value * w end end return ep end -- Turtle WoW agility → melee crit (agi per 1% crit at level 60) local AGI_PER_MELEE_CRIT = { WARRIOR = 20, PALADIN = 20, ROGUE = 29, HUNTER = 52.91, DRUID = 20, SHAMAN = 20, MAGE = 0, WARLOCK = 0, PRIEST = 0, } local AGI_PER_RANGED_CRIT = { HUNTER = 52.91, WARRIOR = 20, ROGUE = 29, } -- Turtle WoW agility → dodge (agi per 1% dodge at level 60) local AGI_PER_DODGE = { ROGUE = 14.5, WARRIOR = 20, PALADIN = 20, SHAMAN = 20, DRUID = 20, HUNTER = 0, MAGE = 0, WARLOCK = 0, PRIEST = 0, } -- Turtle WoW intellect → spell crit (int per 1% crit at level 60) local INT_PER_SPELL_CRIT = { MAGE = 59.5, WARLOCK = 60.6, PRIEST = 59.5, DRUID = 60, SHAMAN = 59.2, PALADIN = 29.5, } -- Turtle WoW base melee crit at level 60 (before agility) local BASE_MELEE_CRIT = { WARRIOR = 0, PALADIN = 0.7, ROGUE = 0, HUNTER = 0, DRUID = 0.9, SHAMAN = 1.7, MAGE = 0, WARLOCK = 0, PRIEST = 0, } local BASE_SPELL_CRIT = { MAGE = 0.2, WARLOCK = 1.7, PRIEST = 0.8, DRUID = 1.8, SHAMAN = 2.3, PALADIN = 0, } local function CalcMeleeCrit() local _, class = UnitClass("player") class = class or "" local baseCrit = BASE_MELEE_CRIT[class] or 0 local ratio = AGI_PER_MELEE_CRIT[class] or 0 local agiCrit = 0 if ratio > 0 then local _, agi = UnitStat("player", 2) agiCrit = (agi or 0) / ratio end local gearCrit = GetGearBonus("CRIT") return baseCrit + agiCrit + gearCrit end local function CalcRangedCrit() local _, class = UnitClass("player") class = class or "" local baseCrit = BASE_MELEE_CRIT[class] or 0 local ratio = AGI_PER_RANGED_CRIT[class] or 0 local agiCrit = 0 if ratio > 0 then local _, agi = UnitStat("player", 2) agiCrit = (agi or 0) / ratio end local gearCrit = GetGearBonus("RANGEDCRIT") + GetGearBonus("CRIT") return baseCrit + agiCrit + gearCrit end local function CalcSpellCrit() local _, class = UnitClass("player") class = class or "" local baseCrit = BASE_SPELL_CRIT[class] or 0 local ratio = INT_PER_SPELL_CRIT[class] or 0 local intCrit = 0 if ratio > 0 then local _, intel = UnitStat("player", 4) intCrit = (intel or 0) / ratio end local gearCrit = GetGearBonus("SPELLCRIT") return baseCrit + intCrit + gearCrit end local function SafeGetMeleeCrit() local v = TryAPIs({ "GetCritChance", "GetMeleeCritChance", "GetPlayerCritChance" }) if v > 0 then return v end v = TryCombatRating(_G.CR_CRIT_MELEE or 9) if v > 0 then return v end return CalcMeleeCrit() end local function SafeGetRangedCrit() local v = TryAPIs({ "GetRangedCritChance" }) if v > 0 then return v end v = TryCombatRating(_G.CR_CRIT_RANGED or 10) if v > 0 then return v end return CalcRangedCrit() end local function SafeGetSpellCrit() local v = TryAPIsArg({ "GetSpellCritChance" }, 2) if v > 0 then return v end v = TryCombatRating(_G.CR_CRIT_SPELL or 11) if v > 0 then return v end return CalcSpellCrit() end local function SafeGetMeleeHit() local v = TryAPIs({ "GetHitModifier", "GetMeleeHitModifier", "GetCombatMissChance" }) if v > 0 then return v end v = TryCombatRating(_G.CR_HIT_MELEE or 6) if v > 0 then return v end return GetGearBonus("TOHIT") end local function SafeGetSpellHit() local v = TryAPIs({ "GetSpellHitModifier" }) if v > 0 then return v end v = TryCombatRating(_G.CR_HIT_SPELL or 8) if v > 0 then return v end return GetGearBonus("SPELLTOHIT") end local function IsCritFromAPI() return TryAPIs({ "GetCritChance", "GetMeleeCritChance", "GetPlayerCritChance" }) > 0 or TryCombatRating(_G.CR_CRIT_MELEE or 9) > 0 end -------------------------------------------------------------------------------- -- Talent scanning: identify hit/crit bonuses from talent trees -- Uses both English and Chinese name matching for Turtle WoW compatibility -------------------------------------------------------------------------------- local TALENT_DB = { meleeHit = { { names = {"Precision", "精准"}, perRank = 1 }, { names = {"Surefooted", "稳固射击", "脚踏实地"}, perRank = 1 }, { names = {"Nature's Guidance", "自然指引", "自然引导"}, perRank = 1 }, }, spellHit = { { names = {"Elemental Precision", "元素精准"}, perRank = 2 }, { names = {"Arcane Focus", "奥术集中"}, perRank = 2 }, { names = {"Suppression", "镇压"}, perRank = 2 }, { names = {"Shadow Focus", "暗影集中"}, perRank = 2 }, { names = {"Nature's Guidance", "自然指引", "自然引导"}, perRank = 1 }, }, meleeCrit = { { names = {"Cruelty", "残忍"}, perRank = 1 }, { names = {"Malice", "恶意"}, perRank = 1 }, { names = {"Lethal Shots", "致命射击"}, perRank = 1 }, { names = {"Conviction", "信念", "坚定信念"}, perRank = 1 }, { names = {"Sharpened Claws", "利爪强化", "尖锐之爪"}, perRank = 2 }, { names = {"Thundering Strikes", "雷霆一击", "雷霆之击"}, perRank = 1 }, }, spellCrit = { { names = {"Arcane Instability", "奥术不稳定"}, perRank = 1 }, { names = {"Critical Mass", "临界质量"}, perRank = 2 }, { names = {"Holy Specialization", "神圣专精"}, perRank = 1 }, { names = {"Tidal Mastery", "潮汐掌握"}, perRank = 1 }, }, } local _talentCache, _talentCacheTime local function ScanTalentBonuses() local now = GetTime and GetTime() or 0 if _talentCache and _talentCacheTime and (now - _talentCacheTime) < 5 then return _talentCache end local r = { meleeHit = 0, spellHit = 0, meleeCrit = 0, spellCrit = 0, talentDetails = {} } if not GetNumTalentTabs then _talentCache = r; _talentCacheTime = now; return r end local function NameMatch(tname, patterns) if not tname then return false end for _, p in ipairs(patterns) do if string.find(tname, p, 1, true) then return true end end return false end for cat, entries in pairs(TALENT_DB) do if cat ~= "talentDetails" then for tab = 1, GetNumTalentTabs() do for i = 1, GetNumTalents(tab) do local name, _, _, _, rank, maxRank = GetTalentInfo(tab, i) if name and rank and rank > 0 then for _, entry in ipairs(entries) do if NameMatch(name, entry.names) then local bonus = rank * entry.perRank r[cat] = r[cat] + bonus table.insert(r.talentDetails, { cat = cat, name = name, rank = rank, maxRank = maxRank or rank, bonus = bonus }) end end end end end end end _talentCache = r; _talentCacheTime = now return r end local function GetTalentBonus(cat) return ScanTalentBonuses()[cat] or 0 end local function GetTalentDetailsFor(cat) local info = ScanTalentBonuses() local out = {} for _, d in ipairs(info.talentDetails or {}) do if d.cat == cat then table.insert(out, d) end end return out end -- Comprehensive stat builders (base + agi/int + gear + talent) local function FullMeleeCrit() local _, class = UnitClass("player") class = class or "" local baseCrit = BASE_MELEE_CRIT[class] or 0 local ratio = AGI_PER_MELEE_CRIT[class] or 0 local agiCrit = 0 if ratio > 0 then local _, agi = UnitStat("player", 2); agiCrit = (agi or 0) / ratio end local gearCrit = GetGearBonus("CRIT") local talentCrit = GetTalentBonus("meleeCrit") return baseCrit + agiCrit + gearCrit + talentCrit, baseCrit, agiCrit, gearCrit, talentCrit end local function FullRangedCrit() local _, class = UnitClass("player") class = class or "" local baseCrit = BASE_MELEE_CRIT[class] or 0 local ratio = AGI_PER_RANGED_CRIT[class] or 0 local agiCrit = 0 if ratio > 0 then local _, agi = UnitStat("player", 2); agiCrit = (agi or 0) / ratio end local gearCrit = GetGearBonus("RANGEDCRIT") + GetGearBonus("CRIT") local talentCrit = GetTalentBonus("meleeCrit") return baseCrit + agiCrit + gearCrit + talentCrit, baseCrit, agiCrit, gearCrit, talentCrit end local function FullSpellCrit() local _, class = UnitClass("player") class = class or "" local baseCrit = BASE_SPELL_CRIT[class] or 0 local ratio = INT_PER_SPELL_CRIT[class] or 0 local intCrit = 0 if ratio > 0 then local _, intel = UnitStat("player", 4); intCrit = (intel or 0) / ratio end local gearCrit = GetGearBonus("SPELLCRIT") local talentCrit = GetTalentBonus("spellCrit") return baseCrit + intCrit + gearCrit + talentCrit, baseCrit, intCrit, gearCrit, talentCrit end local function FullMeleeHit() local gearHit = GetGearBonus("TOHIT") local talentHit = GetTalentBonus("meleeHit") return gearHit + talentHit, gearHit, talentHit end local function FullSpellHit() local gearHit = GetGearBonus("SPELLTOHIT") local talentHit = GetTalentBonus("spellHit") return gearHit + talentHit, gearHit, talentHit end -- Override Safe* to use Full* when API fails local _origSafeGetMeleeCrit = SafeGetMeleeCrit SafeGetMeleeCrit = function() local v = TryAPIs({ "GetCritChance", "GetMeleeCritChance", "GetPlayerCritChance" }) if v > 0 then return v end v = TryCombatRating(_G.CR_CRIT_MELEE or 9) if v > 0 then return v end local total = FullMeleeCrit() return total end SafeGetRangedCrit = function() local v = TryAPIs({ "GetRangedCritChance" }) if v > 0 then return v end v = TryCombatRating(_G.CR_CRIT_RANGED or 10) if v > 0 then return v end local total = FullRangedCrit() return total end SafeGetSpellCrit = function() local v = TryAPIsArg({ "GetSpellCritChance" }, 2) if v > 0 then return v end v = TryCombatRating(_G.CR_CRIT_SPELL or 11) if v > 0 then return v end local total = FullSpellCrit() return total end SafeGetMeleeHit = function() local v = TryAPIs({ "GetHitModifier", "GetMeleeHitModifier", "GetCombatMissChance" }) if v > 0 then return v end v = TryCombatRating(_G.CR_HIT_MELEE or 6) if v > 0 then return v end local total = FullMeleeHit() return total end SafeGetSpellHit = function() local v = TryAPIs({ "GetSpellHitModifier" }) if v > 0 then return v end v = TryCombatRating(_G.CR_HIT_SPELL or 8) if v > 0 then return v end local total = FullSpellHit() return total end -------------------------------------------------------------------------------- -- CS: Combat Stats utility table (single upvalue for all tooltip helpers, -- avoids Lua 5.0's 32-upvalue-per-closure limit) -------------------------------------------------------------------------------- local CS = {} function CS.TipKV(key, val, kr, kg, kb, vr, vg, vb) GameTooltip:AddDoubleLine(key, val, kr or 0.7, kg or 0.7, kb or 0.75, vr or 1, vg or 1, vb or 1) end function CS.TipLine(txt, r, g, b) GameTooltip:AddLine(txt, r or 0.65, g or 0.65, b or 0.7, 1) end function CS.CalcArmorReduction(armor, level) local lvl = level or UnitLevel("player") or 60 local k = 400 + 85 * lvl local pct = armor / (armor + k) * 100 if pct < 0 then pct = 0 end if pct > 75 then pct = 75 end return pct end function CS.StatBaseBonus(idx) local base, eff = UnitStat("player", idx) base = math.floor(base or 0) eff = math.floor(eff or 0) return base, eff, eff - base end function CS.TipStatValues(base, eff, bonus) CS.TipKV("基础值:", tostring(base)) if bonus ~= 0 then CS.TipKV("装备/Buff 加成:", string.format("%+d", bonus), 0.7,0.7,0.75, bonus > 0 and 0 or 1, bonus > 0 and 1 or 0, 0) end CS.TipKV("当前合计:", tostring(eff), 0.7,0.7,0.75, 1,0.9,0.6) end CS.SafeGetMeleeCrit = SafeGetMeleeCrit CS.SafeGetRangedCrit = SafeGetRangedCrit CS.SafeGetSpellCrit = SafeGetSpellCrit CS.SafeGetMeleeHit = SafeGetMeleeHit CS.SafeGetSpellHit = SafeGetSpellHit CS.IsCritFromAPI = IsCritFromAPI CS.TryAPIs = TryAPIs CS.TryAPIsArg = TryAPIsArg CS.FullMeleeCrit = FullMeleeCrit CS.FullRangedCrit = FullRangedCrit CS.FullSpellCrit = FullSpellCrit CS.FullMeleeHit = FullMeleeHit CS.FullSpellHit = FullSpellHit CS.GetTalentDetailsFor = GetTalentDetailsFor CS.GetGearBonus = GetGearBonus CS.GetItemBonusLib = GetItemBonusLib CS.AGI_PER_MELEE_CRIT = AGI_PER_MELEE_CRIT SFrames.CharacterPanel.CS = CS local function GetItemQualityFromLink(link) if not link then return nil end local _, _, q = string.find(link, "|c(%x+)|H") if not q then return nil end local m = { ["ff9d9d9d"] = 0, ["ffffffff"] = 1, ["ff1eff00"] = 2, ["ff0070dd"] = 3, ["ffa335ee"] = 4, ["ffff8000"] = 5, } return m[q] end -------------------------------------------------------------------------------- -- Scroll helper -------------------------------------------------------------------------------- local function CreateScrollFrame(parent, width, height) local holder = CreateFrame("Frame", NextName("SH"), parent) holder:SetWidth(width) holder:SetHeight(height) local scroll = CreateFrame("ScrollFrame", NextName("SF"), holder) scroll:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, 0) scroll:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -10, 0) local child = CreateFrame("Frame", NextName("SC"), scroll) child:SetWidth(width - 14) child:SetHeight(1) scroll:SetScrollChild(child) local slider = CreateFrame("Slider", NextName("SB"), holder) slider:SetWidth(6) slider:SetPoint("TOPRIGHT", holder, "TOPRIGHT", -1, -2) slider:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -1, 2) slider:SetOrientation("VERTICAL") slider:SetMinMaxValues(0, 1) slider:SetValue(0) SetPixelBackdrop(slider, { 0.1, 0.1, 0.12, 0.4 }, { 0.15, 0.15, 0.18, 0.3 }) local thumb = slider:CreateTexture(nil, "OVERLAY") thumb:SetTexture("Interface\\Buttons\\WHITE8X8") thumb:SetVertexColor(0.4, 0.4, 0.48, 0.7) thumb:SetWidth(6) thumb:SetHeight(28) slider:SetThumbTexture(thumb) slider:SetScript("OnValueChanged", function() scroll:SetVerticalScroll(this:GetValue()) end) scroll:EnableMouseWheel(1) scroll:SetScript("OnMouseWheel", function() local cur = slider:GetValue() local step = 28 local _, mx = slider:GetMinMaxValues() if arg1 > 0 then slider:SetValue(math.max(cur - step, 0)) else slider:SetValue(math.min(cur + step, mx)) end end) holder.scroll = scroll holder.child = child holder.slider = slider holder.SetContentHeight = function(self, h) child:SetHeight(h) local visH = scroll:GetHeight() local maxS = math.max(h - visH, 0) slider:SetMinMaxValues(0, maxS) if maxS == 0 then slider:Hide() else slider:Show() end slider:SetValue(math.min(slider:GetValue(), maxS)) end return holder end -------------------------------------------------------------------------------- -- Main Frame -------------------------------------------------------------------------------- local function CreateMainFrame() if panel then return panel end local f = CreateFrame("Frame", "SFramesCharacterPanel", UIParent) f:SetWidth(FRAME_W) f:SetHeight(FRAME_H) f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) f:SetFrameStrata("HIGH") f:EnableMouse(true) f:SetMovable(true) f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", function() this:StartMoving() end) f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) f:SetClampedToScreen(true) SetRoundBackdrop(f, T.bg, T.border) CreateShadow(f, 4) f:Hide() table.insert(UISpecialFrames, "SFramesCharacterPanel") f:SetScript("OnHide", function() if SFrames.StatSummary and SFrames.StatSummary.Hide then SFrames.StatSummary:Hide() end end) -- Title bar: Class icon + Name + subtitle + title button + close f.classIcon = SFrames:CreateClassIcon(f, 16) f.classIcon.overlay:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD + 2, -6) f.titleText = MakeFS(f, 12, "LEFT", T.gold) f.titleText:SetPoint("LEFT", f.classIcon.overlay, "RIGHT", 4, 0) f.subtitleText = MakeFS(f, 9, "LEFT", T.dimText) f.subtitleText:SetPoint("LEFT", f.titleText, "RIGHT", 6, 0) -- Weight level display (hoverable, shows iLvl in tooltip) local weightBtn = CreateFrame("Button", nil, f) weightBtn:SetHeight(14) weightBtn:SetWidth(80) weightBtn:SetPoint("LEFT", f.titleText, "RIGHT", 4, 0) weightBtn:SetFrameLevel(f:GetFrameLevel() + 3) local weightText = MakeFS(weightBtn, 9, "LEFT", { 0.55, 0.95, 0.55 }) weightText:SetPoint("LEFT", weightBtn, "LEFT", 0, 0) weightText:SetText("") weightBtn.text = weightText weightBtn.cachedAvgIlvl = nil weightBtn.cachedAvgEP = nil weightBtn.cachedClass = nil weightBtn:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_BOTTOM") GameTooltip:AddLine("装备权重等级", 0.55, 0.95, 0.55) if this.cachedAvgEP then GameTooltip:AddDoubleLine("权重等级 (EP)", string.format("%.1f", this.cachedAvgEP), 0.8, 0.8, 0.8, 1, 1, 1) end if this.cachedAvgIlvl then GameTooltip:AddDoubleLine("平均装等 (iLvl)", string.format("%.1f", this.cachedAvgIlvl), 0.8, 0.8, 0.8, 1, 0.82, 0) end if this.cachedClass then local classNames = { WARRIOR = "战士", ROGUE = "盗贼", HUNTER = "猎人", DRUID = "德鲁伊", PALADIN = "圣骑士", SHAMAN = "萨满祭司", MAGE = "法师", WARLOCK = "术士", PRIEST = "牧师", } GameTooltip:AddDoubleLine("权重模板", classNames[this.cachedClass] or this.cachedClass, 0.8, 0.8, 0.8, 0.6, 0.8, 1) end GameTooltip:AddLine(" ") GameTooltip:AddLine("基于职业属性EP权重计算", 0.5, 0.5, 0.5) GameTooltip:Show() end) weightBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) f.weightBtn = weightBtn -- Stat summary toggle button local statBtn = CreateFrame("Button", nil, f) statBtn:SetWidth(14) statBtn:SetHeight(14) statBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -42, -7) statBtn:SetFrameLevel(f:GetFrameLevel() + 3) SetPixelBackdrop(statBtn, T.btnBg, T.btnBorder) local stTxt = MakeFS(statBtn, 9, "CENTER", T.dimText) stTxt:SetPoint("CENTER", statBtn, "CENTER", 0, 0) stTxt:SetText("S") statBtn:SetScript("OnEnter", function() this:SetBackdropColor(T.btnHover[1], T.btnHover[2], T.btnHover[3], T.btnHover[4]) GameTooltip:SetOwner(this, "ANCHOR_BOTTOM") GameTooltip:AddLine("属性总览/装备附魔", 1, 1, 1) GameTooltip:Show() end) statBtn:SetScript("OnLeave", function() this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) GameTooltip:Hide() end) statBtn:SetScript("OnClick", function() if SFrames.StatSummary and SFrames.StatSummary.Toggle then SFrames.StatSummary:Toggle() else DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami-UI] StatSummary module not loaded!|r") end end) f.statBtn = statBtn -- Title selection button (small, next to subtitle) local titleBtn = CreateFrame("Button", nil, f) titleBtn:SetWidth(14) titleBtn:SetHeight(14) titleBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -26, -7) titleBtn:SetFrameLevel(f:GetFrameLevel() + 3) SetPixelBackdrop(titleBtn, T.btnBg, T.btnBorder) local tbTxt = MakeFS(titleBtn, 9, "CENTER", T.dimText) tbTxt:SetPoint("CENTER", titleBtn, "CENTER", 0, 0) tbTxt:SetText("T") titleBtn:SetScript("OnEnter", function() this:SetBackdropColor(T.btnHover[1], T.btnHover[2], T.btnHover[3], T.btnHover[4]) GameTooltip:SetOwner(this, "ANCHOR_BOTTOM") GameTooltip:AddLine("选择称号", 1, 1, 1) GameTooltip:Show() end) titleBtn:SetScript("OnLeave", function() this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) GameTooltip:Hide() end) titleBtn:SetScript("OnClick", function() CP:ToggleTitlePopup() end) f.titleBtn = titleBtn -- Close button (round) local closeBtn = CreateFrame("Button", nil, f) closeBtn:SetWidth(16) closeBtn:SetHeight(16) closeBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -7, -6) closeBtn:SetFrameLevel(f:GetFrameLevel() + 3) SetRoundBackdrop(closeBtn, T.buttonDownBg, T.btnBorder) local closeIco = SFrames:CreateIcon(closeBtn, "close", 10) closeIco:SetDrawLayer("OVERLAY") closeIco:SetPoint("CENTER", closeBtn, "CENTER", 0, 0) closeIco:SetVertexColor(1, 0.7, 0.7) closeBtn:SetScript("OnClick", function() f:Hide() end) closeBtn:SetScript("OnEnter", function() this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) end) closeBtn:SetScript("OnLeave", function() this:SetBackdropColor(T.buttonDownBg[1], T.buttonDownBg[2], T.buttonDownBg[3], T.buttonDownBg[4]) this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) end) MakeSep(f, 6, -HEADER_H, -6, -HEADER_H) -- Tab bar tabs = {} pages = {} local numTabs = table.getn(TAB_NAMES) local gap = 2 local tabW = (FRAME_W - SIDE_PAD * 2 - (numTabs - 1) * gap) / numTabs for i, name in ipairs(TAB_NAMES) do local btn = CreateFrame("Button", NextName("Tab"), f) btn:SetWidth(tabW) btn:SetHeight(TAB_H) btn:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD + (i - 1) * (tabW + gap), -(HEADER_H + 3)) btn:SetFrameLevel(f:GetFrameLevel() + 2) SetRoundBackdrop(btn, T.tabBg, T.tabBorder) local txt = MakeFS(btn, 9, "CENTER", T.tabText) txt:SetPoint("CENTER", btn, "CENTER", 0, 0) txt:SetText(name) btn.label = txt btn.tabIndex = i btn:SetScript("OnClick", function() CP:SetTab(this.tabIndex) end) btn:SetScript("OnEnter", function() if this.tabIndex ~= CP.activeTab then this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], 0.6) end end) btn:SetScript("OnLeave", function() if this.tabIndex ~= CP.activeTab then this:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) end end) tabs[i] = btn local page = CreateFrame("Frame", NextName("Page"), f) page:SetPoint("TOPLEFT", f, "TOPLEFT", 4, CONTENT_TOP) page:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -4, 4) page:Hide() pages[i] = page end MakeSep(f, 6, CONTENT_TOP, -6, CONTENT_TOP) panel = f return f end function CP:SetTab(index) self.activeTab = index for i = 1, table.getn(TAB_NAMES) do if i == index then tabs[i]:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4]) tabs[i]:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4]) tabs[i].label:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) pages[i]:Show() else tabs[i]:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) tabs[i]:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], T.tabBorder[4]) tabs[i].label:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) pages[i]:Hide() end end self:UpdateCurrentTab() end function CP:UpdateTitle() if not panel then return end local name = UnitName("player") or "" local level = UnitLevel("player") or 0 local classLocal, classEn = UnitClass("player") classLocal = classLocal or "" classEn = classEn or "" local raceLocal = UnitRace("player") or "" local cc = SFrames.Config and SFrames.Config.colors and SFrames.Config.colors.class and SFrames.Config.colors.class[classEn] local selectedTitle = SFramesDB and SFramesDB.charSelectedTitle or nil local titlePrefix = "" if selectedTitle and selectedTitle ~= "" then titlePrefix = selectedTitle .. " " elseif selectedTitle == nil then if GetPVPRankInfo and UnitPVPRank then local rankName = GetPVPRankInfo(UnitPVPRank("player")) if rankName and rankName ~= "" then titlePrefix = rankName .. " " end end end local fullName = titlePrefix .. name if pages and pages[1] and pages[1].modelNameText then if cc then pages[1].modelNameText:SetTextColor(cc.r, cc.g, cc.b) else pages[1].modelNameText:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) end pages[1].modelNameText:SetText(fullName) end if pages and pages[1] and pages[1].modelGuildText then local guildName = (SFramesDB and SFramesDB.charShowGuild ~= false) and GetGuildInfo and GetGuildInfo("player") or nil if guildName and guildName ~= "" then pages[1].modelGuildText:SetText("<" .. guildName .. ">") else pages[1].modelGuildText:SetText("") end end local parts = {} table.insert(parts, "Lv." .. level) table.insert(parts, raceLocal .. classLocal) local scoreSlots = { 1,2,3,5,6,7,8,9,10,11,12,13,14,15,16,17,18 } local totalIlvl, ilvlCount = 0, 0 local totalEP, epCount = 0, 0 local showIlvl = (not SFramesDB or SFramesDB.showItemLevel ~= false) and LibItem_Level for _, sid in ipairs(scoreSlots) do local link = GetInventoryItemLink("player", sid) if link then if showIlvl then local _, _, itemId = string.find(link, "item:(%d+)") local ilvl = itemId and LibItem_Level[tonumber(itemId)] if ilvl and ilvl > 0 then totalIlvl = totalIlvl + ilvl ilvlCount = ilvlCount + 1 end end local ep = CalcItemEP(link, classEn) if ep > 0 then totalEP = totalEP + ep epCount = epCount + 1 end end end local avgIlvl = ilvlCount > 0 and (totalIlvl / ilvlCount) or nil local avgEP = epCount > 0 and (totalEP / epCount) or nil if panel.weightBtn then panel.weightBtn.cachedAvgIlvl = avgIlvl panel.weightBtn.cachedAvgEP = avgEP panel.weightBtn.cachedClass = classEn if avgEP then panel.weightBtn.text:SetText(string.format("EP:%.1f", avgEP)) panel.weightBtn:SetWidth(panel.weightBtn.text:GetStringWidth() + 4) panel.weightBtn:Show() elseif avgIlvl then panel.weightBtn.text:SetText(string.format("iLvl:%.1f", avgIlvl)) panel.weightBtn:SetWidth(panel.weightBtn.text:GetStringWidth() + 4) panel.weightBtn:Show() else panel.weightBtn.text:SetText("") panel.weightBtn:Hide() end end if cc then panel.titleText:SetTextColor(cc.r, cc.g, cc.b) else panel.titleText:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) end panel.titleText:SetText(table.concat(parts, " ")) panel.subtitleText:SetText("") if panel.classIcon then SFrames:SetClassIcon(panel.classIcon, classEn) end end function CP:UpdateCurrentTab() if not panel or not panel:IsShown() then return end self:UpdateTitle() local tab = self.activeTab or 1 if tab == 1 then self:UpdateEquipment() elseif tab == 2 then self:UpdateReputation() elseif tab == 3 then self:UpdateSkills() elseif tab == 4 then self:UpdateHonor() end end function CP:Toggle(tab) CreateMainFrame() self:BuildAllPages() if panel:IsShown() then if tab and self.activeTab ~= tab then self:SetTab(tab) else panel:Hide() end else local scale = SFramesDB and SFramesDB.charPanelScale or 1.0 panel:SetScale(scale) panel:Show() self:SetTab(tab or self.activeTab or 1) if SFrames.StatSummary and SFrames.StatSummary.Show then SFrames.StatSummary:Show() end end end function CP:Show(tab) CreateMainFrame() self:BuildAllPages() panel:Show() self:SetTab(tab or self.activeTab or 1) if SFrames.StatSummary and SFrames.StatSummary.Show then SFrames.StatSummary:Show() end end function CP:Hide() if panel then panel:Hide() end end -------------------------------------------------------------------------------- -- Title selection popup -------------------------------------------------------------------------------- local titlePopup function CP:ToggleTitlePopup() if titlePopup and titlePopup:IsShown() then titlePopup:Hide() return end self:ShowTitlePopup() end function CP:ShowTitlePopup() if not panel then return end if not titlePopup then titlePopup = CreateFrame("Frame", "SFramesCPTitlePopup", panel) titlePopup:SetWidth(160) titlePopup:SetFrameStrata("TOOLTIP") titlePopup:SetFrameLevel(200) SetRoundBackdrop(titlePopup, T.bg, T.border) titlePopup:Hide() titlePopup:EnableMouse(true) end -- Clear old children if titlePopup.rows then for _, row in ipairs(titlePopup.rows) do if row.frame then row.frame:Hide() end end end titlePopup.rows = {} -- Build title list from all available sources local titles = {} local seen = {} table.insert(titles, { name = "无称号", key = "" }) -- PVP rank title if GetPVPRankInfo and UnitPVPRank then local rank = UnitPVPRank("player") if rank and rank > 0 then local rankName = GetPVPRankInfo(rank) if rankName and rankName ~= "" and not seen[rankName] then table.insert(titles, { name = rankName, key = rankName }) seen[rankName] = true end end end -- Turtle WoW custom titles: GetNumTitles / GetTitleName / IsTitleKnown if GetNumTitles and GetTitleName then local numT = GetNumTitles() if numT and numT > 0 then for ti = 1, numT do local tName = GetTitleName(ti) if tName and tName ~= "" and not seen[tName] then local known = true if IsTitleKnown then known = IsTitleKnown(ti) end if known then table.insert(titles, { name = tName, key = tName, titleId = ti }) seen[tName] = true end end end end end -- Fallback: scan CharacterTitleText if it exists (some private servers) if table.getn(titles) <= 1 and CharacterTitleText then local curTitle = CharacterTitleText:GetText() if curTitle and curTitle ~= "" and not seen[curTitle] then table.insert(titles, { name = curTitle, key = curTitle }) seen[curTitle] = true end end local currentTitle = SFramesDB and SFramesDB.charSelectedTitle or "" local rowH = 20 local y = -4 local popH = 8 for i, t in ipairs(titles) do local row = CreateFrame("Button", nil, titlePopup) row:SetWidth(152) row:SetHeight(rowH) row:SetPoint("TOPLEFT", titlePopup, "TOPLEFT", 4, y) local isActive = (currentTitle == t.key) or (currentTitle == "" and t.key == "") local bg = isActive and T.tabActiveBg or T.tabBg local bd = isActive and T.tabActiveBorder or T.tabBorder SetPixelBackdrop(row, bg, bd) local txt = MakeFS(row, 10, "LEFT", isActive and T.tabActiveText or T.valueText) txt:SetPoint("LEFT", row, "LEFT", 6, 0) txt:SetText(t.name) if isActive then local mark = MakeFS(row, 9, "RIGHT", T.gold) mark:SetPoint("RIGHT", row, "RIGHT", -6, 0) mark:SetText("*") end row.titleKey = t.key row.titleId = t.titleId row:SetScript("OnClick", function() SFramesDB = SFramesDB or {} SFramesDB.charSelectedTitle = this.titleKey -- Try to set server-side title via Turtle WoW API if SetCurrentTitle and this.titleId then SetCurrentTitle(this.titleId) elseif SetCurrentTitle and this.titleKey == "" then SetCurrentTitle(0) end titlePopup:Hide() CP:UpdateTitle() end) row:SetScript("OnEnter", function() if not isActive then this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], 0.5) end end) row:SetScript("OnLeave", function() if not isActive then this:SetBackdropColor(bg[1], bg[2], bg[3], bg[4]) end end) table.insert(titlePopup.rows, { frame = row }) y = y - rowH - 1 popH = popH + rowH + 1 end titlePopup:SetHeight(popH) titlePopup:ClearAllPoints() titlePopup:SetPoint("TOPRIGHT", panel.titleBtn, "BOTTOMRIGHT", 0, -2) titlePopup:Show() end function CP:BuildAllPages() if self.built then return end self.built = true self:BuildEquipmentPage() self:BuildReputationPage() self:BuildSkillsPage() self:BuildHonorPage() end -------------------------------------------------------------------------------- -- Stat helpers (shared by equipment page and others) -------------------------------------------------------------------------------- function CP:CreateStatSection(parent, title, yOffset) local header = MakeFS(parent, 11, "LEFT", T.sectionTitle) header:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, yOffset) header:SetText(title) local sep = parent:CreateTexture(nil, "ARTWORK") sep:SetTexture("Interface\\Buttons\\WHITE8X8") sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) sep:SetHeight(1) sep:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, yOffset - 14) sep:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -8, yOffset - 14) return yOffset - 18 end function CP:CreateStatRow(parent, label, yOffset, color) local row = {} row.label = MakeFS(parent, 10, "LEFT", T.labelText) row.label:SetPoint("TOPLEFT", parent, "TOPLEFT", 14, yOffset) row.label:SetText(label) row.value = MakeFS(parent, 10, "RIGHT", color or T.valueText) row.value:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -14, yOffset) row.value:SetWidth(120) row.value:SetJustifyH("RIGHT") return row, yOffset - 16 end -------------------------------------------------------------------------------- -- Tab 1: Equipment + Stats (merged, scrollable) -------------------------------------------------------------------------------- function CP:BuildEquipmentPage() local page = pages[1] if page.built then return end page.built = true local contentH = FRAME_H - (HEADER_H + TAB_BAR_H) - INNER_PAD - 4 local scrollArea = CreateScrollFrame(page, CONTENT_W, contentH) scrollArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) page.scrollArea = scrollArea local child = scrollArea.child local cw = SCROLL_W -- 3D Model: created on page level (not scroll child) so it follows panel drag local slotColW = SLOT_SIZE + 6 local modelW = cw - slotColW * 2 - 26 if modelW < 80 then modelW = 80 end local modelH = 8 * (SLOT_SIZE + SLOT_GAP) - SLOT_GAP -- Background frame in scroll child (for the dark border visual) local modelBg = CreateFrame("Frame", nil, child) modelBg:SetWidth(modelW) modelBg:SetHeight(modelH) modelBg:SetPoint("TOP", child, "TOP", 0, -4) SetRoundBackdrop(modelBg, T.modelBg, T.modelBorder) page.modelBgFrame = modelBg -- PlayerModel on page level, anchored to the background frame local modelFrame = CreateFrame("Frame", nil, page) modelFrame:SetWidth(modelW - 8) modelFrame:SetHeight(modelH - 24) modelFrame:SetPoint("TOP", modelBg, "TOP", 0, -4) modelFrame:SetFrameLevel(page:GetFrameLevel() + 5) local model = CreateFrame("PlayerModel", NextName("Model"), modelFrame) model:SetAllPoints(modelFrame) page.model = model page.modelFrame = modelFrame model:EnableMouse(true) model:EnableMouseWheel(1) model.rotating = false model.panning = false model.curFacing = 0.4 model.curScale = 1.0 model.posX = 0 model.posY = 0 model.posZ = 0 model:SetScript("OnMouseDown", function() if arg1 == "LeftButton" then this.rotating = true this.startX = GetCursorPosition() this.startFacing = this.curFacing or 0 elseif arg1 == "RightButton" then this.panning = true local cx, cy = GetCursorPosition() this.panStartX = cx this.panStartY = cy this.panOriginX = this.posX or 0 this.panOriginY = this.posY or 0 end end) model:SetScript("OnMouseUp", function() if arg1 == "LeftButton" then this.rotating = false elseif arg1 == "RightButton" then this.panning = false end end) model:SetScript("OnMouseWheel", function() local step = 0.1 local ns = (this.curScale or 1) + arg1 * step if ns < 0.2 then ns = 0.2 end if ns > 4.0 then ns = 4.0 end this.curScale = ns this:SetModelScale(ns) end) model:SetScript("OnUpdate", function() if this.rotating then local cx = GetCursorPosition() local diff = (cx - (this.startX or cx)) * 0.01 local nf = (this.startFacing or 0) + diff this.curFacing = nf this:SetFacing(nf) elseif this.panning then local cx, cy = GetCursorPosition() local es = this:GetEffectiveScale() if es < 0.01 then es = 1 end local dx = (cx - (this.panStartX or cx)) / (es * 35) local dy = (cy - (this.panStartY or cy)) / (es * 35) this.posX = (this.panOriginX or 0) + dx this.posY = (this.panOriginY or 0) + dy this:SetPosition(this.posY, 0, this.posX) elseif this.autoRotateDir then local speed = 1.5 * (arg1 or 0.016) this.curFacing = (this.curFacing or 0) + speed * this.autoRotateDir this:SetFacing(this.curFacing) end end) model.ResetView = function(self) self.curFacing = 0.4 self.curScale = 1.0 self.posX = 0 self.posY = 0 self:SetFacing(0.4) self:SetModelScale(1.0) self:SetPosition(0, 0, 0) end -- Name/guild overlay floats on top of 3D model local nameOverlay = CreateFrame("Frame", nil, page) nameOverlay:SetWidth(modelW) nameOverlay:SetHeight(30) nameOverlay:SetPoint("TOP", modelBg, "TOP", 0, -4) nameOverlay:SetFrameLevel(page:GetFrameLevel() + 7) nameOverlay:EnableMouse(false) local modelNameText = nameOverlay:CreateFontString(nil, "OVERLAY") modelNameText:SetFont(GetFont(), 11, "OUTLINE") modelNameText:SetPoint("TOP", nameOverlay, "TOP", 0, -5) modelNameText:SetJustifyH("CENTER") modelNameText:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) page.modelNameText = modelNameText local modelGuildText = nameOverlay:CreateFontString(nil, "OVERLAY") modelGuildText:SetFont(GetFont(), 9, "OUTLINE") modelGuildText:SetPoint("TOP", modelNameText, "BOTTOM", 0, -1) modelGuildText:SetJustifyH("CENTER") modelGuildText:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) page.modelGuildText = modelGuildText -- Model toolbar: unified strip with 3 segments local tbH = 14 local segW = 24 local tbW = segW * 3 + 2 local toolbar = CreateFrame("Frame", nil, page) toolbar:SetWidth(tbW) toolbar:SetHeight(tbH) toolbar:SetPoint("BOTTOM", modelBg, "BOTTOM", 0, 5) toolbar:SetFrameLevel(page:GetFrameLevel() + 6) SetPixelBackdrop(toolbar, { 0.04, 0.04, 0.06, 0.75 }, { 0.22, 0.22, 0.28, 0.5 }) page.modelToolbar = toolbar local function MakeToolBtn(parent, w, text, anchor, ox) local btn = CreateFrame("Button", nil, parent) btn:SetWidth(w) btn:SetHeight(tbH - 2) btn:SetPoint("LEFT", parent, "LEFT", ox, 0) btn:SetFrameLevel(parent:GetFrameLevel() + 1) btn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 0, }) btn:SetBackdropColor(0, 0, 0, 0) local fs = MakeFS(btn, 9, "CENTER", { 0.5, 0.5, 0.55 }) fs:SetPoint("CENTER", btn, "CENTER", 0, 0) fs:SetText(text) btn.label = fs btn:SetScript("OnEnter", function() this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5) this.label:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) end) btn:SetScript("OnLeave", function() this:SetBackdropColor(0, 0, 0, 0) this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) end) return btn end local rotLeft = MakeToolBtn(toolbar, segW, "<", "LEFT", 1) rotLeft:SetScript("OnMouseDown", function() model.autoRotateDir = 1 this:SetBackdropColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4] or 0.6) end) rotLeft:SetScript("OnMouseUp", function() model.autoRotateDir = nil this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5) end) rotLeft:SetScript("OnLeave", function() this:SetBackdropColor(0, 0, 0, 0) this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) model.autoRotateDir = nil end) page.rotLeft = rotLeft local sep1 = toolbar:CreateTexture(nil, "OVERLAY") sep1:SetTexture("Interface\\Buttons\\WHITE8X8") sep1:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4] or 0.4) sep1:SetWidth(1) sep1:SetHeight(tbH - 4) sep1:SetPoint("LEFT", toolbar, "LEFT", segW + 1, 0) local resetBtn = MakeToolBtn(toolbar, segW, "O", "LEFT", segW + 1) resetBtn:SetScript("OnClick", function() model:ResetView() end) resetBtn:SetScript("OnEnter", function() this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5) this.label:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) GameTooltip:SetOwner(this, "ANCHOR_BOTTOM") GameTooltip:AddLine("重置视角", 1, 1, 1) GameTooltip:Show() end) resetBtn:SetScript("OnLeave", function() this:SetBackdropColor(0, 0, 0, 0) this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) GameTooltip:Hide() end) page.resetBtn = resetBtn local sep2 = toolbar:CreateTexture(nil, "OVERLAY") sep2:SetTexture("Interface\\Buttons\\WHITE8X8") sep2:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4] or 0.4) sep2:SetWidth(1) sep2:SetHeight(tbH - 4) sep2:SetPoint("LEFT", toolbar, "LEFT", segW * 2 + 1, 0) local rotRight = MakeToolBtn(toolbar, segW, ">", "LEFT", segW * 2 + 1) rotRight:SetScript("OnMouseDown", function() model.autoRotateDir = -1 this:SetBackdropColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4] or 0.6) end) rotRight:SetScript("OnMouseUp", function() model.autoRotateDir = nil this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5) end) rotRight:SetScript("OnLeave", function() this:SetBackdropColor(0, 0, 0, 0) this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) model.autoRotateDir = nil end) page.rotRight = rotRight -- Equipment slots local slotY = -6 page.equipSlots = {} for idx, si in ipairs(EQUIP_SLOTS_LEFT) do local slot = self:CreateEquipSlot(child, si.id, si.name) slot:SetPoint("TOPLEFT", child, "TOPLEFT", 2, slotY - (idx - 1) * (SLOT_SIZE + SLOT_GAP)) page.equipSlots[si.id] = slot end for idx, si in ipairs(EQUIP_SLOTS_RIGHT) do local slot = self:CreateEquipSlot(child, si.id, si.name) slot:SetPoint("TOPRIGHT", child, "TOPRIGHT", -2, slotY - (idx - 1) * (SLOT_SIZE + SLOT_GAP)) page.equipSlots[si.id] = slot end local bottomY = slotY - table.getn(EQUIP_SLOTS_LEFT) * (SLOT_SIZE + SLOT_GAP) - 4 local bottomCount = table.getn(EQUIP_SLOTS_BOTTOM) local totalBW = bottomCount * SLOT_SIZE + (bottomCount - 1) * (SLOT_GAP + 2) local bsx = math.max((cw - totalBW) / 2, 4) for idx, si in ipairs(EQUIP_SLOTS_BOTTOM) do local slot = self:CreateEquipSlot(child, si.id, si.name) slot:SetPoint("TOPLEFT", child, "TOPLEFT", bsx + (idx - 1) * (SLOT_SIZE + SLOT_GAP + 1), bottomY) page.equipSlots[si.id] = slot end local infoY = bottomY - SLOT_SIZE -- Stats panel: fixed at bottom of page (not in scroll child) -- This avoids scroll/overflow issues local STAT_PANEL_H = 100 local ROW_H = 14 local statPanel = CreateFrame("Frame", nil, page) statPanel:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, 0) statPanel:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) statPanel:SetHeight(STAT_PANEL_H) statPanel:SetFrameLevel(page:GetFrameLevel() + 2) page.statPanel = statPanel -- Resize scroll area to not overlap stat panel scrollArea:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, STAT_PANEL_H) MakeSep(statPanel, 4, 0, -4, 0) -- Category buttons local catNames = { "基础/抗性", "攻击", "法术/防御" } local catBtnW = math.floor((CONTENT_W - 12) / 3) page.statCatBtns = {} page.statCatFrames = {} page.activeStatCat = 1 for ci = 1, 3 do local cb = CreateFrame("Button", NextName("SC"), statPanel) cb:SetWidth(catBtnW) cb:SetHeight(16) cb:SetPoint("TOPLEFT", statPanel, "TOPLEFT", 4 + (ci - 1) * (catBtnW + 2), -4) SetPixelBackdrop(cb, T.tabBg, T.tabBorder) local cbt = MakeFS(cb, 8, "CENTER", T.tabText) cbt:SetPoint("CENTER", cb, "CENTER", 0, 0) cbt:SetText(catNames[ci]) cb.label = cbt cb.catIndex = ci cb:SetScript("OnClick", function() page.activeStatCat = this.catIndex CP:RefreshStatCatVisual(page) CP:RefreshStatValues(page) end) cb:SetScript("OnEnter", function() if this.catIndex ~= page.activeStatCat then this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], 0.5) end end) cb:SetScript("OnLeave", function() if this.catIndex ~= page.activeStatCat then this:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) end end) page.statCatBtns[ci] = cb end local catContentY = -26 local function MakeStatHitArea(parent, anchorFrom, offX, yOff, isRight) local btn = CreateFrame("Button", nil, parent) btn:SetHeight(ROW_H) btn:SetFrameLevel(parent:GetFrameLevel() + 4) if isRight then btn:SetPoint("TOPLEFT", parent, "TOP", offX, yOff) btn:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -4, yOff) else btn:SetPoint("TOPLEFT", parent, "TOPLEFT", 4, yOff) btn:SetPoint("TOPRIGHT", parent, "TOP", -4, yOff) end if btn.SetBackdrop then btn:SetBackdrop(nil) end if btn.SetNormalTexture then btn:SetNormalTexture(nil) end if btn.SetHighlightTexture then btn:SetHighlightTexture(nil) end if btn.SetPushedTexture then btn:SetPushedTexture(nil) end return btn end local function Make2ColRow(parent, labelL, labelR, yOff, colorL, colorR) local r = {} if labelL then r.labelL = MakeFS(parent, 9, "LEFT", T.labelText) r.labelL:SetPoint("TOPLEFT", parent, "TOPLEFT", 6, yOff) r.labelL:SetText(labelL) r.valueL = MakeFS(parent, 9, "RIGHT", colorL or T.valueText) r.valueL:SetPoint("TOPLEFT", parent, "TOPLEFT", 6, yOff) r.valueL:SetPoint("TOPRIGHT", parent, "TOP", -4, yOff) r.valueL:SetJustifyH("RIGHT") r.hitL = MakeStatHitArea(parent, "TOPLEFT", 0, yOff, false) end if labelR then r.labelR = MakeFS(parent, 9, "LEFT", T.labelText) r.labelR:SetPoint("TOPLEFT", parent, "TOP", 6, yOff) r.labelR:SetText(labelR) r.valueR = MakeFS(parent, 9, "RIGHT", colorR or T.valueText) r.valueR:SetPoint("TOPLEFT", parent, "TOP", 6, yOff) r.valueR:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -6, yOff) r.valueR:SetJustifyH("RIGHT") r.hitR = MakeStatHitArea(parent, "TOP", 0, yOff, true) end return r, yOff - ROW_H end -- Cat 1: Base + Resist local cat1 = CreateFrame("Frame", nil, statPanel) cat1:SetAllPoints(statPanel) page.statCatFrames[1] = cat1 page.cat1Rows = {} local resistSchools = { 2, 3, 4, 5, 6 } local resistNames = { "火焰", "自然", "冰霜", "暗影", "奥术" } local resistSchoolName = { [2]="火焰", [3]="自然", [4]="冰霜", [5]="暗影", [6]="奥术" } local statOnEnter = { -- 力量 function() local base, eff, bonus = CS.StatBaseBonus(1) GameTooltip:AddLine("力量 (Strength)", 1, 0.82, 0.0) CS.TipLine("提升近战攻强(AP)与格挡值。") GameTooltip:AddLine(" ") CS.TipLine("攻击强度转化:", 0.5,0.8,1) CS.TipLine(" 战/骑/萨/德(熊豹):1力 = 2AP") CS.TipLine(" 猫德+野性之心:1力 = 2.4AP") CS.TipLine(" 盗/猎/法/牧/术:1力 = 1AP") GameTooltip:AddLine(" ") CS.TipLine("格挡值(需装备盾牌):", 0.5,0.8,1) CS.TipLine(" 战/骑/萨:每20点额外力量 +1 BV") CS.TipLine(" BV = (力量-种族初始力量)/20") GameTooltip:AddLine(" ") CS.TipStatValues(base, eff, bonus) end, -- 敏捷 function() local base, eff, bonus = CS.StatBaseBonus(2) local crit = CS.SafeGetMeleeCrit() local dodge = GetDodgeChance and GetDodgeChance() or 0 local _, effArmor = UnitArmor("player") GameTooltip:AddLine("敏捷 (Agility)", 1, 0.82, 0.0) CS.TipLine("提升暴击、护甲、闪避。1敏 = 2护甲。") GameTooltip:AddLine(" ") CS.TipLine("攻击强度转化:", 0.5,0.8,1) CS.TipLine(" 猎人:1敏 = 2远程AP + 1近战AP") CS.TipLine(" 盗贼:1敏 = 1近战/远程AP") CS.TipLine(" 德鲁伊(熊/豹):1敏 = 1AP") CS.TipLine(" 其他职业:不提供额外AP") GameTooltip:AddLine(" ") CS.TipLine("暴击转化(60级):", 0.5,0.8,1) CS.TipLine(" 战/骑/萨/德:20敏 = 1%暴击") CS.TipLine(" 盗贼:29敏 = 1%暴击(最高效)") CS.TipLine(" 猎人:52.91敏 = 1%暴击(最严苛)") GameTooltip:AddLine(" ") CS.TipLine("闪避转化(60级):", 0.5,0.8,1) CS.TipLine(" 盗贼:14.5敏 = 1%闪避(最强)") CS.TipLine(" 战/骑/萨/德:20敏 = 1%闪避") GameTooltip:AddLine(" ") CS.TipStatValues(base, eff, bonus) GameTooltip:AddLine(" ") CS.TipKV("当前物理暴击:", string.format("%.2f%%", crit), 0.7,0.7,0.75, 1,1,0.5) if not CS.IsCritFromAPI() and crit > 0 then CS.TipLine("(基础+敏捷+装备+天赋,无Buff)", 0.8,0.5,0.3) end CS.TipKV("当前闪避率:", string.format("%.2f%%", dodge), 0.7,0.7,0.75, 1,1,0.5) CS.TipKV("当前护甲值:", tostring(math.floor(effArmor or 0)), 0.7,0.7,0.75, 1,1,0.5) end, -- 耐力 function() local base, eff, bonus = CS.StatBaseBonus(3) local hp = UnitHealthMax("player") or 0 GameTooltip:AddLine("耐力 (Stamina)", 1, 0.82, 0.0) CS.TipLine("所有职业通用的生存核心属性。") GameTooltip:AddLine(" ") CS.TipLine("生命值转化:", 0.5,0.8,1) CS.TipLine(" 前20点:每1耐 = 1生命值") CS.TipLine(" 超过20点:每1耐 = 10生命值") GameTooltip:AddLine(" ") CS.TipLine("特殊加成:", 0.5,0.8,1) CS.TipLine(" 牛头人:生命值总额 ×5%") CS.TipLine(" 术士:可通过生命分流转法力") GameTooltip:AddLine(" ") CS.TipStatValues(base, eff, bonus) CS.TipKV("最大生命值:", tostring(hp), 0.7,0.7,0.75, 0.4,1,0.4) end, -- 智力 function() local base, eff, bonus = CS.StatBaseBonus(4) local mana = UnitManaMax("player") or 0 local scrit = CS.SafeGetSpellCrit() GameTooltip:AddLine("智力 (Intellect)", 1, 0.82, 0.0) CS.TipLine("增加法力值和法术暴击率。") CS.TipLine("1智力 = 15法力值(通用)。") GameTooltip:AddLine(" ") CS.TipLine("法爆转化(60级):", 0.5,0.8,1) CS.TipLine(" 圣骑:29.5智 = 1%法爆(最高效!)") CS.TipLine(" 法师:59.5智 = 1%(基础0.2%)") CS.TipLine(" 牧师:59.5智 = 1%(基础0.8%)") CS.TipLine(" 萨满:59.2智 = 1%(基础2.3%)") CS.TipLine(" 德鲁:60.0智 = 1%(基础1.8%)") CS.TipLine(" 术士:60.6智 = 1%(基础1.7%)") CS.TipLine(" 战士/盗贼:无法系收益") GameTooltip:AddLine(" ") CS.TipStatValues(base, eff, bonus) CS.TipKV("最大法力值:", tostring(mana), 0.7,0.7,0.75, 0.4,0.7,1) CS.TipKV("法术暴击率:", string.format("%.2f%%", scrit), 0.7,0.7,0.75, 1,1,0.5) end, -- 精神 function() local base, eff, bonus = CS.StatBaseBonus(5) GameTooltip:AddLine("精神 (Spirit)", 1, 0.82, 0.0) CS.TipLine("控制脱战/五秒规则外的回复速度。") CS.TipLine("停止施法5秒后触发精神回蓝。") GameTooltip:AddLine(" ") CS.TipLine("法力回复(每2秒/跳):", 0.5,0.8,1) CS.TipLine(" 牧师/法师:13 + 精神/4") CS.TipLine(" 德/萨/骑/猎:15 + 精神/5") CS.TipLine(" 术士(惩罚):8 + 精神/4") GameTooltip:AddLine(" ") CS.TipLine("各职业收益:", 0.5,0.8,1) CS.TipLine(" 牧师(极高):冥想天赋施法中回蓝") CS.TipLine(" 精神指引:25%精神→法强/治疗") CS.TipLine(" 德鲁伊(核心):配合激活回蓝") CS.TipLine(" 法师(重要):配合奥术冥想天赋") CS.TipLine(" 萨满:MP5更稳定") CS.TipLine(" 战士:脱战回血 = 精神/50") CS.TipLine(" 巨魔(精神战):战斗中25%回复") GameTooltip:AddLine(" ") CS.TipStatValues(base, eff, bonus) end, } local cy = catContentY for i = 1, 5 do local row row, cy = Make2ColRow(cat1, STAT_NAMES[i], resistNames[i], cy, nil, T.resistColors[resistSchools[i]]) row.baseIdx = i row.resistSchool = resistSchools[i] if row.hitL then local fn = statOnEnter[i] row.hitL:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:ClearLines() fn() GameTooltip:Show() end) row.hitL:SetScript("OnLeave", function() GameTooltip:Hide() end) end if row.hitR then local school = resistSchools[i] local rname = resistNames[i] row.hitR:SetScript("OnEnter", function() local base, total = UnitResistance("player", school) base = math.floor(base or 0) total = math.floor(total or base) local bonus = total - base -- Resistance → damage reduction: Classic formula uses total resist / (total resist + 5*attackerLevel) -- At player level vs same-level attacker: average ~(R/(R+5L))*0.75 full absorption chance local lvl = UnitLevel("player") or 60 local avgPct = total / (total + 5 * lvl) * 75 GameTooltip:SetOwner(this, "ANCHOR_LEFT") GameTooltip:ClearLines() GameTooltip:AddLine(rname .. "抗性", 1, 0.82, 0.0) CS.TipLine("减少受到的" .. rname .. "系魔法伤害。") CS.TipLine("抗性值越高,被完全抵抗或部分减免的概率越大。") GameTooltip:AddLine(" ") CS.TipKV("基础抗性:", tostring(base)) if bonus ~= 0 then CS.TipKV("装备加成:", (bonus > 0 and "+" or "") .. tostring(bonus), 0.7,0.7,0.75, bonus>0 and 0 or 1, bonus>0 and 1 or 0, 0) end CS.TipKV("当前合计:", tostring(total), 0.7,0.7,0.75, 1,0.9,0.6) if total > 0 then CS.TipKV("平均减伤约:", string.format("%.1f%%", avgPct), 0.7,0.7,0.75, 1,1,0.5) end GameTooltip:Show() end) row.hitR:SetScript("OnLeave", function() GameTooltip:Hide() end) end table.insert(page.cat1Rows, row) end -- Cat 2: Melee + Ranged local cat2 = CreateFrame("Frame", nil, statPanel) cat2:SetAllPoints(statPanel) cat2:Hide() page.statCatFrames[2] = cat2 page.cat2Rows = {} local meleeL = { "攻强", "伤害", "速度", "暴击", "命中" } local meleeK = { "meleeAP", "meleeDmg", "meleeSpeed", "meleeCrit", "meleeHit" } local rangedL = { "攻强", "伤害", "速度", "暴击", "" } local rangedK = { "rangedAP", "rangedDmg", "rangedSpeed", "rangedCrit", nil } local meleeOnEnter = { -- 近战攻强 function() local base, pos, neg = UnitAttackPower("player") local total = math.floor((base or 0) + (pos or 0) + (neg or 0)) local dps = total / 14 local bmod = math.floor((pos or 0) + (neg or 0)) GameTooltip:AddLine("近战攻击强度 (AP)", 1, 0.82, 0.0) CS.TipLine("直接提升近战白字伤害。") CS.TipLine("每 14 点攻击强度 = +1 DPS。") GameTooltip:AddLine(" ") CS.TipLine("来源:力量(战/骑/萨/德 1力=2AP,", 0.5,0.8,1) CS.TipLine("盗/猎/法系 1力=1AP) + 装备/Buff", 0.5,0.8,1) GameTooltip:AddLine(" ") CS.TipKV("基础 AP:", tostring(math.floor(base or 0))) if bmod ~= 0 then CS.TipKV("装备/Buff 加成:", string.format("%+d", bmod), 0.7,0.7,0.75, bmod > 0 and 0 or 1, bmod > 0 and 1 or 0, 0) end CS.TipKV("当前合计:", tostring(total), 0.7,0.7,0.75, 1,0.9,0.6) CS.TipKV("等效 DPS 增益:", string.format("+%.1f", dps), 0.7,0.7,0.75, 1,1,0.5) end, -- 近战伤害 function() local lo, hi = UnitDamage("player") lo = math.floor(lo or 0); hi = math.floor(hi or 0) local ms = UnitAttackSpeed("player") or 2 local avgDps = ms > 0 and (lo + hi) / 2 / ms or 0 GameTooltip:AddLine("近战伤害", 1, 0.82, 0.0) CS.TipLine("主手武器基础伤害 + 攻击强度加成后的实际白字伤害区间。") CS.TipLine("AP 加成 = 武器速度 × AP / 14。") GameTooltip:AddLine(" ") CS.TipKV("伤害区间:", lo .. " - " .. hi) CS.TipKV("主手 DPS:", string.format("%.1f", avgDps), 0.7,0.7,0.75, 1,1,0.5) end, -- 近战速度 function() local ms, os = UnitAttackSpeed("player") GameTooltip:AddLine("近战攻击速度", 1, 0.82, 0.0) CS.TipLine("每次自动攻击的间隔(秒),数值越低攻击越快。") CS.TipLine("受急速效果(如狂暴/风怒图腾)影响。") GameTooltip:AddLine(" ") CS.TipKV("主手速度:", string.format("%.2f 秒", ms or 0)) if os and os > 0 then CS.TipKV("副手速度:", string.format("%.2f 秒", os)) CS.TipLine(" ") CS.TipLine("副手白字伤害为主手50%。", 0.5,0.8,1) CS.TipLine("双持白字未命中:26.4%(300技能)", 0.5,0.8,1) CS.TipLine("315技能仍有24.0%未命中。", 0.5,0.8,1) end end, -- 近战暴击 function() local fromAPI = CS.IsCritFromAPI() local _, class = UnitClass("player") local ratio = CS.AGI_PER_MELEE_CRIT[class or ""] or 0 GameTooltip:AddLine("近战暴击率", 1, 0.82, 0.0) CS.TipLine("近战暴击造成 200% 伤害。") if ratio > 0 then if ratio == math.floor(ratio) then CS.TipLine(string.format("当前职业:每 %d 敏捷 = 1%% 暴击。", ratio)) else CS.TipLine(string.format("当前职业:每 %.2f 敏捷 = 1%% 暴击。", ratio)) end end GameTooltip:AddLine(" ") if fromAPI then local crit = CS.SafeGetMeleeCrit() CS.TipKV("当前暴击率:", string.format("%.2f%%", crit), 0.7,0.7,0.75, 1,1,0.5) else local total, base, agiC, gearC, talC = CS.FullMeleeCrit() CS.TipLine("来源分项:", 0.5,0.8,1) if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end if agiC > 0 then CS.TipKV(" 敏捷暴击:", string.format("%.2f%%", agiC)) end if gearC > 0 then CS.TipKV(" 装备暴击:", string.format("+%d%%", gearC)) end if talC > 0 then CS.TipKV(" 天赋暴击:", string.format("+%d%%", talC)) for _, d in ipairs(CS.GetTalentDetailsFor("meleeCrit")) do CS.TipLine(string.format(" %s (%d/%d) +%d%%", d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6) end end GameTooltip:AddLine(" ") CS.TipKV("合计暴击率:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5) CS.TipLine("(Buff 暴击未计入)", 0.8,0.5,0.3) end end, -- 近战命中 function() local fromAPI = CS.TryAPIs({ "GetHitModifier", "GetMeleeHitModifier" }) > 0 GameTooltip:AddLine("近战命中", 1, 0.82, 0.0) CS.TipLine("减少近战未命中概率。物理DPS最优先属性。") CS.TipLine("乌龟服对BOSS基础未命中率 8%。") GameTooltip:AddLine(" ") if fromAPI then local hit = CS.SafeGetMeleeHit() CS.TipKV("当前命中加成:", string.format("%+d%%", hit), 0.7,0.7,0.75, 1,1,0.5) else local total, gearH, talH = CS.FullMeleeHit() CS.TipLine("来源分项:", 0.5,0.8,1) if gearH > 0 then CS.TipKV(" 装备命中:", string.format("+%d%%", gearH)) end if talH > 0 then CS.TipKV(" 天赋命中:", string.format("+%d%%", talH)) for _, d in ipairs(CS.GetTalentDetailsFor("meleeHit")) do CS.TipLine(string.format(" %s (%d/%d) +%d%%", d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6) end end GameTooltip:AddLine(" ") CS.TipKV("合计命中:", string.format("+%d%%", total), 0.7,0.7,0.75, 1,1,0.5) end local totalHit = CS.SafeGetMeleeHit() local lvl = UnitLevel("player") or 60 if lvl >= 60 then GameTooltip:AddLine(" ") CS.TipLine("达标判定(对+3级BOSS):", 1, 0.82, 0.0) CS.TipLine("已移除隐性1%命中压制。", 0.5,0.8,1) local cap = 8 if totalHit >= cap then CS.TipKV(" 黄字8%(300技能):", "已达标", 0.7,0.7,0.75, 0.3,1,0.3) else CS.TipKV(" 黄字8%(300技能):", string.format("差%d%%", cap - totalHit), 0.7,0.7,0.75, 1,0.3,0.3) end if totalHit >= 5 then CS.TipKV(" 种族5%(305技能):", "已达标", 0.7,0.7,0.75, 0.3,1,0.3) else CS.TipKV(" 种族5%(305技能):", string.format("差%d%%", 5 - totalHit), 0.7,0.7,0.75, 1,0.3,0.3) end if totalHit >= 2 then CS.TipKV(" 高技能2%(310):", "已达标", 0.7,0.7,0.75, 0.3,1,0.3) else CS.TipKV(" 高技能2%(310):", string.format("差%d%%", 2 - totalHit), 0.7,0.7,0.75, 1,0.3,0.3) end CS.TipLine(" 315技能时命中需求归零", 0.55,0.55,0.6) CS.TipLine(" (人类剑锤/兽人斧/矮人枪锤/侏儒短剑)", 0.55,0.55,0.6) GameTooltip:AddLine(" ") CS.TipLine("偏斜减免(白字对BOSS):", 0.5,0.8,1) CS.TipLine(" 300技能:约35%伤害损失", 0.55,0.55,0.6) CS.TipLine(" 305技能:约15%伤害损失", 0.55,0.55,0.6) CS.TipLine(" 315技能:无减免(全额)", 0.55,0.55,0.6) end end, } local rangedOnEnter = { -- 远程攻强 function() local base, pos, neg = UnitRangedAttackPower("player") local total = math.floor((base or 0) + (pos or 0) + (neg or 0)) local bmod = math.floor((pos or 0) + (neg or 0)) GameTooltip:AddLine("远程攻击强度 (RAP)", 1, 0.82, 0.0) CS.TipLine("直接提升弓/枪/弩/投掷武器的伤害。") CS.TipLine("每 14 点远程攻击强度 = +1 DPS。") GameTooltip:AddLine(" ") CS.TipLine("猎人核心属性:1 敏捷 = 2 远程AP。", 0.5,0.8,1) GameTooltip:AddLine(" ") CS.TipKV("基础 RAP:", tostring(math.floor(base or 0))) if bmod ~= 0 then CS.TipKV("装备/Buff 加成:", string.format("%+d", bmod), 0.7,0.7,0.75, bmod > 0 and 0 or 1, bmod > 0 and 1 or 0, 0) end CS.TipKV("当前合计:", tostring(total), 0.7,0.7,0.75, 1,0.9,0.6) CS.TipKV("等效 DPS 增益:", string.format("+%.1f", total / 14), 0.7,0.7,0.75, 1,1,0.5) end, -- 远程伤害 function() local _, lo, hi = UnitRangedDamage("player") lo = math.floor(lo or 0); hi = math.floor(hi or 0) local rspd = UnitRangedDamage("player") or 2 local avgDps = rspd > 0 and (lo + hi) / 2 / rspd or 0 GameTooltip:AddLine("远程伤害", 1, 0.82, 0.0) CS.TipLine("远程武器基础伤害 + 远程攻击强度加成后的实际伤害区间。") GameTooltip:AddLine(" ") CS.TipKV("伤害区间:", lo .. " - " .. hi) CS.TipKV("远程 DPS:", string.format("%.1f", avgDps), 0.7,0.7,0.75, 1,1,0.5) end, -- 远程速度 function() local spd = UnitRangedDamage("player") or 0 GameTooltip:AddLine("远程攻击速度", 1, 0.82, 0.0) CS.TipLine("远程武器每次射击的间隔(秒)。") CS.TipLine("受急速效果影响(如猎人急速射击天赋)。") GameTooltip:AddLine(" ") CS.TipKV("射击间隔:", string.format("%.2f 秒", spd)) end, -- 远程暴击 function() local fromAPI = CS.TryAPIs({ "GetRangedCritChance" }) > 0 GameTooltip:AddLine("远程暴击率", 1, 0.82, 0.0) CS.TipLine("远程暴击造成 200% 伤害。猎人:每 52.91 敏 = 1%。") GameTooltip:AddLine(" ") if fromAPI then CS.TipKV("当前暴击率:", string.format("%.2f%%", CS.SafeGetRangedCrit()), 0.7,0.7,0.75, 1,1,0.5) else local total, base, agiC, gearC, talC = CS.FullRangedCrit() CS.TipLine("来源分项:", 0.5,0.8,1) if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end if agiC > 0 then CS.TipKV(" 敏捷暴击:", string.format("%.2f%%", agiC)) end if gearC > 0 then CS.TipKV(" 装备暴击:", string.format("+%d%%", gearC)) end if talC > 0 then CS.TipKV(" 天赋暴击:", string.format("+%d%%", talC)) for _, d in ipairs(CS.GetTalentDetailsFor("meleeCrit")) do CS.TipLine(string.format(" %s (%d/%d) +%d%%", d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6) end end GameTooltip:AddLine(" ") CS.TipKV("合计暴击率:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5) CS.TipLine("(Buff 暴击未计入)", 0.8,0.5,0.3) end end, } cy = catContentY for i = 1, 5 do local row row, cy = Make2ColRow(cat2, meleeL[i], rangedL[i] ~= "" and rangedL[i] or nil, cy) row.meleeKey = meleeK[i] row.rangedKey = rangedK[i] if row.hitL and meleeOnEnter[i] then local fn = meleeOnEnter[i] row.hitL:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:ClearLines() fn() GameTooltip:Show() end) row.hitL:SetScript("OnLeave", function() GameTooltip:Hide() end) end if row.hitR and rangedOnEnter[i] then local fn = rangedOnEnter[i] row.hitR:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_LEFT") GameTooltip:ClearLines() fn() GameTooltip:Show() end) row.hitR:SetScript("OnLeave", function() GameTooltip:Hide() end) end table.insert(page.cat2Rows, row) end -- Cat 3: Spell + Defense local cat3 = CreateFrame("Frame", nil, statPanel) cat3:SetAllPoints(statPanel) cat3:Hide() page.statCatFrames[3] = cat3 page.cat3Rows = {} local spellL = { "法伤", "治疗", "暴击", "命中", "" } local spellK = { "spellDmg", "spellHeal", "spellCrit", "spellHit", nil } local defL = { "护甲", "防御", "闪避", "招架", "格挡" } local defK = { "armor", "defense", "dodge", "parry", "block" } local spellOnEnter = { -- 法术伤害 function() local maxDmg = 0 local perSchool = {} local fromAPI = false if GetSpellBonusDamage then for s = 2, 7 do local d = GetSpellBonusDamage(s) or 0 perSchool[s] = d if d > maxDmg then maxDmg = d; fromAPI = true end end end if maxDmg == 0 then local lib = CS.GetItemBonusLib and CS.GetItemBonusLib() or nil if lib and lib.GetBonus then local baseDmg = lib:GetBonus("DMG") or 0 local schoolKeys = { [2] = "FIREDMG", [3] = "NATUREDMG", [4] = "FROSTDMG", [5] = "SHADOWDMG", [6] = "ARCANEDMG", [7] = "HOLYDMG", } for s = 2, 7 do local sd = baseDmg + (lib:GetBonus(schoolKeys[s]) or 0) perSchool[s] = sd if sd > maxDmg then maxDmg = sd end end end end GameTooltip:AddLine("法术伤害加成", 1, 0.82, 0.0) CS.TipLine("提升伤害法术输出,按系别独立计算。") CS.TipLine("加成 = 法伤 × 系数(0.3~1.0)。") GameTooltip:AddLine(" ") CS.TipLine("乌龟服特色:", 0.5,0.8,1) CS.TipLine(" 部分锁/皮甲装备带法伤属性", 0.5,0.8,1) CS.TipLine(" 增强萨:震击获10%AP法伤", 0.5,0.8,1) CS.TipLine(" 惩戒骑:十字军受33%SP加成", 0.5,0.8,1) GameTooltip:AddLine(" ") CS.TipKV("最高法术伤害:", tostring(math.floor(maxDmg)), 0.7,0.7,0.75, 1,0.9,0.6) local snames = {[2]="火焰",[3]="自然",[4]="冰霜",[5]="暗影",[6]="奥术",[7]="神圣"} for s = 2, 7 do if perSchool[s] and perSchool[s] > 0 then CS.TipKV(" " .. (snames[s] or s) .. ":", tostring(math.floor(perSchool[s]))) end end if not fromAPI and maxDmg > 0 then CS.TipLine("(来自装备扫描,Buff 加成未计入)", 0.8,0.5,0.3) end end, -- 治疗 function() local heal = GetSpellBonusHealing and GetSpellBonusHealing() or 0 local fromAPI = heal > 0 if heal == 0 then heal = CS.GetGearBonus("HEAL") end heal = math.floor(heal) GameTooltip:AddLine("治疗加成", 1, 0.82, 0.0) CS.TipLine("提升所有治疗法术的恢复量。") CS.TipLine("实际加成 = 治疗加成 × 法术系数(通常 0.4~1.0)。") GameTooltip:AddLine(" ") CS.TipLine("神圣骑/恢复德/牧师核心属性。", 0.5,0.8,1) CS.TipLine("骑士Ironclad:护甲1-2%→治疗量", 0.5,0.8,1) GameTooltip:AddLine(" ") CS.TipKV("当前治疗加成:", tostring(heal), 0.7,0.7,0.75, 1,0.9,0.6) if not fromAPI and heal > 0 then CS.TipLine("(来自装备扫描,Buff 加成未计入)", 0.8,0.5,0.3) end end, -- 法术暴击 function() local fromAPI = CS.TryAPIsArg({ "GetSpellCritChance" }, 2) > 0 GameTooltip:AddLine("法术暴击率", 1, 0.82, 0.0) CS.TipLine("法术暴击造成 150% 伤害/治疗量(+50%)。") GameTooltip:AddLine(" ") if fromAPI then CS.TipKV("当前法术暴击:", string.format("%.2f%%", CS.SafeGetSpellCrit()), 0.7,0.7,0.75, 1,1,0.5) else local total, base, intC, gearC, talC = CS.FullSpellCrit() CS.TipLine("来源分项:", 0.5,0.8,1) if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end if intC > 0 then CS.TipKV(" 智力暴击:", string.format("%.2f%%", intC)) end if gearC > 0 then CS.TipKV(" 装备暴击:", string.format("+%d%%", gearC)) end if talC > 0 then CS.TipKV(" 天赋暴击:", string.format("+%d%%", talC)) for _, d in ipairs(CS.GetTalentDetailsFor("spellCrit")) do CS.TipLine(string.format(" %s (%d/%d) +%d%%", d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6) end end GameTooltip:AddLine(" ") CS.TipKV("合计法术暴击:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5) CS.TipLine("(Buff 暴击未计入)", 0.8,0.5,0.3) end end, -- 法术命中 function() local fromAPI = CS.TryAPIs({ "GetSpellHitModifier" }) > 0 GameTooltip:AddLine("法术命中", 1, 0.82, 0.0) CS.TipLine("减少法术被抵抗概率。法系最优先。") CS.TipLine("对+3级BOSS基础未命中16%。") GameTooltip:AddLine(" ") if fromAPI then CS.TipKV("当前法术命中:", string.format("%+d%%", CS.SafeGetSpellHit()), 0.7,0.7,0.75, 1,1,0.5) else local total, gearH, talH = CS.FullSpellHit() CS.TipLine("来源分项:", 0.5,0.8,1) if gearH > 0 then CS.TipKV(" 装备命中:", string.format("+%d%%", gearH)) end if talH > 0 then CS.TipKV(" 天赋命中:", string.format("+%d%%", talH)) for _, d in ipairs(CS.GetTalentDetailsFor("spellHit")) do CS.TipLine(string.format(" %s (%d/%d) +%d%%", d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6) end CS.TipLine(" (天赋命中可能仅对特定系别)", 0.8,0.5,0.3) end GameTooltip:AddLine(" ") CS.TipKV("合计法术命中:", string.format("+%d%%", total), 0.7,0.7,0.75, 1,1,0.5) end local totalHit = CS.SafeGetSpellHit() local lvl = UnitLevel("player") or 60 if lvl >= 60 then GameTooltip:AddLine(" ") local cap = 16 CS.TipLine("达标(对+3级BOSS需16%):", 1, 0.82, 0.0) if totalHit >= cap then CS.TipKV(" 状态:", "已达标", 0.7,0.7,0.75, 0.3,1,0.3) else CS.TipKV(" 状态:", string.format("差%d%%", cap - totalHit), 0.7,0.7,0.75, 1,0.3,0.3) end CS.TipLine(" 法师精准6%/奥术集中10%", 0.55,0.55,0.6) CS.TipLine(" 术士镇压10%/暗牧集中10%", 0.55,0.55,0.6) end end, } local defOnEnter = { -- 护甲 function() local baseArmor, effArmor = UnitArmor("player") effArmor = math.floor(effArmor or baseArmor or 0) baseArmor = math.floor(baseArmor or 0) local bonusArmor = effArmor - baseArmor local lvl = UnitLevel("player") or 60 local pct = CS.CalcArmorReduction(effArmor, lvl) local pctBoss = CS.CalcArmorReduction(effArmor, lvl + 3) GameTooltip:AddLine("护甲", 1, 0.82, 0.0) CS.TipLine("减少受到的物理伤害。减伤上限为 75%。") GameTooltip:AddLine(" ") CS.TipLine("公式:护甲/(护甲+400+85×等级)", 0.5,0.8,1) CS.TipLine("来源:装备 + 敏捷护甲加成。", 0.5,0.8,1) CS.TipLine("熊德/防战护甲需求最高。", 0.5,0.8,1) GameTooltip:AddLine(" ") CS.TipKV("基础护甲:", tostring(baseArmor)) if bonusArmor ~= 0 then CS.TipKV("Buff/敏捷加成:", string.format("%+d", bonusArmor), 0.7,0.7,0.75, bonusArmor > 0 and 0 or 1, bonusArmor > 0 and 1 or 0, 0) end CS.TipKV("当前护甲:", tostring(effArmor), 0.7,0.7,0.75, 1,0.9,0.6) GameTooltip:AddLine(" ") CS.TipKV("减伤(同级):", string.format("%.1f%%", pct), 0.7,0.7,0.75, 0.4,1,0.4) CS.TipKV("减伤(+3BOSS):", string.format("%.1f%%", pctBoss), 0.7,0.7,0.75, 1,0.7,0.3) end, -- 防御 function() local base, mod = UnitDefense("player") base = math.floor(base or 0) mod = math.floor(mod or 0) local total = base + mod local uncritCap = 440 GameTooltip:AddLine("防御技能", 1, 0.82, 0.0) CS.TipLine("提升闪避/招架/格挡,降低被暴击/碾压。") GameTooltip:AddLine(" ") CS.TipLine("每1点防御(超出攻击者):", 0.5,0.8,1) CS.TipLine(" +0.04%闪避/招架/格挡") CS.TipLine(" -0.04%被暴击/被碾压") GameTooltip:AddLine(" ") CS.TipLine("坦克达标(对63级BOSS):", 1, 0.82, 0.0) CS.TipLine(" 免暴击:防御≥440(额外140)", 1,0.4,0.4) CS.TipLine(" BOSS暴击率5.6%,每防-0.04%", 0.55,0.55,0.6) GameTooltip:AddLine(" ") CS.TipLine(" 免碾压(102.4%圆桌):", 1,0.4,0.4) CS.TipLine(" 闪避+招架+格挡≥102.4%", 0.55,0.55,0.6) CS.TipLine(" 碾压(150%伤)被排挤出圆桌", 0.55,0.55,0.6) CS.TipLine(" 防战靠盾挡,防骑靠神圣之盾", 0.55,0.55,0.6) GameTooltip:AddLine(" ") CS.TipLine("坦怪仇恨压力大,命中不可忽视。", 0.5,0.8,1) GameTooltip:AddLine(" ") CS.TipKV("基础防御:", tostring(base)) if mod ~= 0 then CS.TipKV("装备加成:", string.format("%+d", mod), 0.7,0.7,0.75, mod > 0 and 0 or 1, mod > 0 and 1 or 0, 0) end CS.TipKV("当前防御:", tostring(total), 0.7,0.7,0.75, 1,0.9,0.6) if total >= uncritCap then CS.TipKV("免暴击(440):", "已达标", 0.7,0.7,0.75, 0.3,1,0.3) else CS.TipKV("免暴击(440):", string.format("差%d防御", uncritCap - total), 0.7,0.7,0.75, 1,0.3,0.3) end end, -- 闪避 function() local dodge = GetDodgeChance and GetDodgeChance() or 0 if dodge == 0 then dodge = CS.GetGearBonus("DODGE") end GameTooltip:AddLine("闪避", 1, 0.82, 0.0) CS.TipLine("完全规避一次近战物理攻击。") CS.TipLine("闪避时不受伤害,不触发命中特效。") CS.TipLine("注意:对远程攻击无效。") GameTooltip:AddLine(" ") CS.TipLine("来源:敏捷+防御+天赋+装备。", 0.5,0.8,1) CS.TipLine("熊德无法招架,闪避是主要减伤。", 0.5,0.8,1) GameTooltip:AddLine(" ") CS.TipKV("当前闪避率:", string.format("%.2f%%", dodge), 0.7,0.7,0.75, 1,1,0.5) if (GetDodgeChance and GetDodgeChance() or 0) == 0 and dodge > 0 then CS.TipLine("(仅含装备加成)", 0.8,0.5,0.3) end end, -- 招架 function() local parry = GetParryChance and GetParryChance() or 0 if parry == 0 then parry = CS.GetGearBonus("PARRY") end GameTooltip:AddLine("招架", 1, 0.82, 0.0) CS.TipLine("完全免疫一次近战攻击伤害,") CS.TipLine("并使下次攻击加速40%(招架反击)。") GameTooltip:AddLine(" ") CS.TipLine("可招架:战士/骑士/盗贼/猎人。", 0.5,0.8,1) CS.TipLine("熊德无法招架(仅靠闪避+护甲)。", 1,0.5,0.5) CS.TipLine("只能招架正面攻击。", 0.5,0.8,1) GameTooltip:AddLine(" ") CS.TipKV("当前招架率:", string.format("%.2f%%", parry), 0.7,0.7,0.75, 1,1,0.5) if (GetParryChance and GetParryChance() or 0) == 0 and parry > 0 then CS.TipLine("(仅含装备加成)", 0.8,0.5,0.3) end end, -- 格挡 function() local block = GetBlockChance and GetBlockChance() or 0 local bv = 0 if GetBlockValue then bv = GetBlockValue() or 0 end GameTooltip:AddLine("格挡", 1, 0.82, 0.0) CS.TipLine("盾牌格挡吸收等同格挡值的伤害。") CS.TipLine("不能完全免伤,但大幅降低受伤。") GameTooltip:AddLine(" ") CS.TipLine("需装备盾牌。来源:防御+装备。", 0.5,0.8,1) CS.TipLine("持盾:战士/圣骑/萨满。", 0.5,0.8,1) CS.TipLine("BV = (力量-初始力量)/20", 0.5,0.8,1) GameTooltip:AddLine(" ") CS.TipLine("102.4%圆桌中格挡占重要份额。", 0.5,0.8,1) GameTooltip:AddLine(" ") CS.TipKV("当前格挡率:", string.format("%.2f%%", block), 0.7,0.7,0.75, 1,1,0.5) if bv > 0 then CS.TipKV("格挡值 (BV):", tostring(math.floor(bv)), 0.7,0.7,0.75, 0.4,1,0.4) CS.TipLine("(每次格挡吸收该数值的物理伤害)", 0.55,0.55,0.6) end end, } cy = catContentY for i = 1, 5 do local row local lbl = spellL[i] ~= "" and spellL[i] or nil row, cy = Make2ColRow(cat3, lbl, defL[i], cy) row.spellKey = spellK[i] row.defKey = defK[i] if row.hitL and spellOnEnter[i] then local fn = spellOnEnter[i] row.hitL:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:ClearLines() fn() GameTooltip:Show() end) row.hitL:SetScript("OnLeave", function() GameTooltip:Hide() end) end if row.hitR and defOnEnter[i] then local fn = defOnEnter[i] row.hitR:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_LEFT") GameTooltip:ClearLines() fn() GameTooltip:Show() end) row.hitR:SetScript("OnLeave", function() GameTooltip:Hide() end) end table.insert(page.cat3Rows, row) end -- Set scroll content height (equipment area only, stats are fixed) page.totalContentH = math.abs(infoY) + 4 scrollArea:SetContentHeight(page.totalContentH) -- Initialize stat category visual (highlight first button) CP:RefreshStatCatVisual(page) end function CP:RefreshStatCatVisual(page) for ci = 1, 3 do if ci == page.activeStatCat then page.statCatBtns[ci]:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4]) page.statCatBtns[ci]:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4]) page.statCatBtns[ci].label:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) page.statCatFrames[ci]:Show() else page.statCatBtns[ci]:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) page.statCatBtns[ci]:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], T.tabBorder[4]) page.statCatBtns[ci].label:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) page.statCatFrames[ci]:Hide() end end end -------------------------------------------------------------------------------- -- Equipment Swap Popup (Alt+Hover to show bag alternatives) -------------------------------------------------------------------------------- local SLOT_TO_EQUIP_LOCS = { [1] = { INVTYPE_HEAD = true }, [2] = { INVTYPE_NECK = true }, [3] = { INVTYPE_SHOULDER = true }, [4] = { INVTYPE_BODY = true }, [5] = { INVTYPE_CHEST = true, INVTYPE_ROBE = true }, [6] = { INVTYPE_WAIST = true }, [7] = { INVTYPE_LEGS = true }, [8] = { INVTYPE_FEET = true }, [9] = { INVTYPE_WRIST = true }, [10] = { INVTYPE_HAND = true }, [11] = { INVTYPE_FINGER = true }, [12] = { INVTYPE_FINGER = true }, [13] = { INVTYPE_TRINKET = true }, [14] = { INVTYPE_TRINKET = true }, [15] = { INVTYPE_CLOAK = true }, [16] = { INVTYPE_WEAPON = true, INVTYPE_2HWEAPON = true, INVTYPE_WEAPONMAINHAND = true }, [17] = { INVTYPE_SHIELD = true, INVTYPE_WEAPONOFFHAND = true, INVTYPE_HOLDABLE = true, INVTYPE_WEAPON = true, INVTYPE_WEAPONMAINHAND = true }, [18] = { INVTYPE_RANGED = true, INVTYPE_RANGEDRIGHT = true, INVTYPE_THROWN = true, INVTYPE_RELIC = true }, [19] = { INVTYPE_TABARD = true }, } local WEAPON_SLOT_EQUIP_LOCS = { INVTYPE_WEAPON = true, INVTYPE_2HWEAPON = true, INVTYPE_WEAPONMAINHAND = true, INVTYPE_WEAPONOFFHAND = true, INVTYPE_SHIELD = true, INVTYPE_HOLDABLE = true, } local swapPopup local SWAP_POPUP_W = 235 local SWAP_ROW_H = 26 local SWAP_MAX_VISIBLE = 8 local SWAP_ICON_SIZE = 20 local function TryGetEquipLoc(link) if not link then return nil end local name, _, quality, _, _, _, _, equipLoc, tex = GetItemInfo(link) if equipLoc and equipLoc ~= "" then return name, quality, equipLoc, tex end local _, _, itemString = string.find(link, "(item:[%d:]+)") if itemString then name, _, quality, _, _, _, _, equipLoc, tex = GetItemInfo(itemString) if equipLoc and equipLoc ~= "" then return name, quality, equipLoc, tex end end local _, _, itemId = string.find(link, "item:(%d+)") if itemId then name, _, quality, _, _, _, _, equipLoc, tex = GetItemInfo(tonumber(itemId)) if equipLoc and equipLoc ~= "" then return name, quality, equipLoc, tex end end return nil end local function ScanBagsForSlot(slotID) local locSet = SLOT_TO_EQUIP_LOCS[slotID] if not locSet then return {}, "no_mapping" end local items = {} local debugEquipLocs = {} local isWeaponSlot = (slotID == 16 or slotID == 17) for bag = 0, 4 do local numSlots = GetContainerNumSlots(bag) if numSlots and numSlots > 0 then for slot = 1, numSlots do local link = GetContainerItemLink(bag, slot) if link then local name, quality, equipLoc, tex = TryGetEquipLoc(link) if equipLoc then debugEquipLocs[equipLoc] = (debugEquipLocs[equipLoc] or 0) + 1 local matched = locSet[equipLoc] if (not matched) and isWeaponSlot and WEAPON_SLOT_EQUIP_LOCS[equipLoc] then matched = true end if matched then if not tex then tex = GetContainerItemInfo(bag, slot) end if not quality then quality = GetItemQualityFromLink(link) end local ilvl if LibItem_Level then local _, _, itemId = string.find(link, "item:(%d+)") ilvl = itemId and LibItem_Level[tonumber(itemId)] end table.insert(items, { bag = bag, slot = slot, link = link, name = name or "?", texture = tex, quality = quality or 1, ilvl = ilvl, }) end end end end end end table.sort(items, function(a, b) if a.quality ~= b.quality then return a.quality > b.quality end if (a.ilvl or 0) ~= (b.ilvl or 0) then return (a.ilvl or 0) > (b.ilvl or 0) end return a.name < b.name end) return items, debugEquipLocs end local function GetOrCreateSwapPopup() if swapPopup then return swapPopup end local f = CreateFrame("Frame", "SFramesCPSwapPopup", UIParent) f:SetWidth(SWAP_POPUP_W) f:SetHeight(60) f:SetFrameStrata("TOOLTIP") f:EnableMouse(true) SetRoundBackdrop(f, T.bg, T.border) CreateShadow(f, 3) f:Hide() local title = MakeFS(f, 10, "CENTER", T.accentLight) title:SetPoint("TOP", f, "TOP", 0, -6) f.title = title local hint = f:CreateFontString(nil, "OVERLAY") hint:SetFont(GetFont(), 8, "OUTLINE") hint:SetTextColor(0.55, 0.55, 0.55) hint:SetText("") hint:SetPoint("BOTTOM", f, "BOTTOM", 0, 4) f.hint = hint f.rows = {} f.scrollOffset = 0 f.totalItems = 0 f.items = {} f.anchorSlot = nil f:EnableMouseWheel(1) f:SetScript("OnMouseWheel", function() local maxOff = math.max(f.totalItems - SWAP_MAX_VISIBLE, 0) if arg1 > 0 then f.scrollOffset = math.max(f.scrollOffset - 1, 0) else f.scrollOffset = math.min(f.scrollOffset + 1, maxOff) end CP:RefreshSwapRows() end) f:SetScript("OnUpdate", function() if not IsAltKeyDown() then f:Hide() return end if panel and not panel:IsShown() then f:Hide() return end local overPopup = MouseIsOver and MouseIsOver(f) local overSlot = f.anchorSlot and MouseIsOver and MouseIsOver(f.anchorSlot) if not overPopup and not overSlot then if not f._gracePeriod then f._gracePeriod = GetTime() elseif GetTime() - f._gracePeriod > 0.3 then f._gracePeriod = nil f:Hide() end else f._gracePeriod = nil end end) f:SetScript("OnLeave", function() end) swapPopup = f return f end function CP:RefreshSwapRows() local popup = swapPopup if not popup then return end local items = popup.items local offset = popup.scrollOffset local total = table.getn(items) local numVis = math.min(total - offset, SWAP_MAX_VISIBLE) for i = 1, table.getn(popup.rows) do local row = popup.rows[i] if i <= numVis then local item = items[offset + i] row.icon:SetTexture(item.texture) row.nameText:SetText(item.name) local qc = QUALITY_COLORS[item.quality] or QUALITY_COLORS[1] row.nameText:SetTextColor(qc[1], qc[2], qc[3]) row.ilvlText:SetText(item.ilvl and tostring(item.ilvl) or "") row.itemBag = item.bag row.itemSlot = item.slot row.itemLink = item.link row:Show() else row:Hide() end end end function CP:ShowSwapPopup(slot) local items, debugInfo = ScanBagsForSlot(slot.slotID) local count = table.getn(items) local popup = GetOrCreateSwapPopup() popup.anchorSlot = slot popup.items = items popup.totalItems = count popup.scrollOffset = 0 local label = SLOT_LABEL[slot.slotName] or slot.slotName for i = 1, table.getn(popup.rows) do popup.rows[i]:Hide() end if count == 0 then popup.title:SetText(label .. " - 背包中无可替换装备") popup:SetHeight(30) popup.hint:SetText("") else popup.title:SetText(label .. " - 可替换装备 (" .. count .. ")") local numVis = math.min(count, SWAP_MAX_VISIBLE) local hasScroll = count > SWAP_MAX_VISIBLE local hintH = hasScroll and 14 or 0 popup:SetHeight(22 + numVis * SWAP_ROW_H + 6 + hintH) popup.hint:SetText(hasScroll and "滚轮翻页" or "") for i = 1, numVis do if not popup.rows[i] then local row = CreateFrame("Button", nil, popup) row:SetWidth(SWAP_POPUP_W - 12) row:SetHeight(SWAP_ROW_H) row:EnableMouse(true) local icon = row:CreateTexture(nil, "ARTWORK") icon:SetPoint("LEFT", row, "LEFT", 2, 0) icon:SetWidth(SWAP_ICON_SIZE) icon:SetHeight(SWAP_ICON_SIZE) icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) row.icon = icon local nameText = row:CreateFontString(nil, "OVERLAY") nameText:SetFont(GetFont(), 10, "OUTLINE") nameText:SetPoint("LEFT", icon, "RIGHT", 4, 0) nameText:SetPoint("RIGHT", row, "RIGHT", -28, 0) nameText:SetJustifyH("LEFT") row.nameText = nameText local ilvlText = row:CreateFontString(nil, "OVERLAY") ilvlText:SetFont(GetFont(), 9, "OUTLINE") ilvlText:SetPoint("RIGHT", row, "RIGHT", -4, 0) ilvlText:SetJustifyH("RIGHT") ilvlText:SetTextColor(1, 0.82, 0) row.ilvlText = ilvlText local hl = row:CreateTexture(nil, "HIGHLIGHT") hl:SetTexture("Interface\\Buttons\\WHITE8X8") hl:SetVertexColor(T.accentLight[1], T.accentLight[2], T.accentLight[3], 0.12) hl:SetAllPoints(row) row:SetScript("OnClick", function() UseContainerItem(this.itemBag, this.itemSlot) popup:Hide() CP:ScheduleEquipUpdate() end) row:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetBagItem(this.itemBag, this.itemSlot) GameTooltip:Show() end) row:SetScript("OnLeave", function() GameTooltip:Hide() end) popup.rows[i] = row end popup.rows[i]:SetPoint("TOPLEFT", popup, "TOPLEFT", 6, -(20 + (i - 1) * SWAP_ROW_H)) end for i = numVis + 1, table.getn(popup.rows) do popup.rows[i]:Hide() end self:RefreshSwapRows() end popup:ClearAllPoints() local slotLeft = slot:GetLeft() or 0 local screenW = GetScreenWidth() or 1024 if slotLeft + SLOT_SIZE + SWAP_POPUP_W + 10 > screenW then popup:SetPoint("TOPRIGHT", slot, "TOPLEFT", -4, 0) else popup:SetPoint("TOPLEFT", slot, "TOPRIGHT", 4, 0) end popup:Show() end function CP:HideSwapPopup() if swapPopup then swapPopup:Hide() end end function CP:CreateEquipSlot(parent, slotID, slotName) local frame = CreateFrame("Button", NextName("Slot"), parent) frame:SetWidth(SLOT_SIZE) frame:SetHeight(SLOT_SIZE) SetRoundBackdrop(frame, T.slotBg, T.slotBorder) local icon = frame:CreateTexture(nil, "ARTWORK") icon:SetPoint("TOPLEFT", frame, "TOPLEFT", 4, -4) icon:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -4, 4) icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) frame.icon = icon local ilvlText = frame:CreateFontString(nil, "OVERLAY") ilvlText:SetFont(GetFont(), 8, "OUTLINE") ilvlText:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -2, 2) ilvlText:SetTextColor(1, 0.82, 0) ilvlText:SetText("") frame.ilvlText = ilvlText local glow = frame:CreateTexture(nil, "OVERLAY") glow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") glow:SetBlendMode("ADD") glow:SetAlpha(0.8) glow:SetWidth(SLOT_SIZE * 2) glow:SetHeight(SLOT_SIZE * 2) glow:SetPoint("CENTER", frame, "CENTER", 0, 0) glow:Hide() frame.qualGlow = glow frame.slotID = slotID frame.slotName = slotName local _, emptyTex = GetInventorySlotInfo(slotName) frame.emptyTexture = emptyTex frame:RegisterForClicks("LeftButtonUp", "RightButtonUp") frame:RegisterForDrag("LeftButton") frame:SetScript("OnClick", function() local sid = this.slotID if CursorHasItem and CursorHasItem() then PickupInventoryItem(sid) elseif arg1 == "RightButton" then if GetInventoryItemLink("player", sid) then UseInventoryItem(sid) end elseif IsShiftKeyDown() and arg1 == "LeftButton" then local link = GetInventoryItemLink("player", sid) if link and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then ChatFrameEditBox:Insert(link) end else PickupInventoryItem(sid) end CP:ScheduleEquipUpdate() end) frame:SetScript("OnDragStart", function() PickupInventoryItem(this.slotID) CP:ScheduleEquipUpdate() end) frame:SetScript("OnReceiveDrag", function() PickupInventoryItem(this.slotID) CP:ScheduleEquipUpdate() end) frame:SetScript("OnEnter", function() this._mouseOver = true this._swapActive = nil this:SetBackdropBorderColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4]) if IsAltKeyDown() then this._swapActive = true GameTooltip:Hide() CP:ShowSwapPopup(this) else GameTooltip:SetOwner(this, "ANCHOR_RIGHT") if GetInventoryItemLink("player", this.slotID) then GameTooltip:SetInventoryItem("player", this.slotID) else local label = SLOT_LABEL[this.slotName] or this.slotName GameTooltip:AddLine(label .. " - 未装备", 0.5, 0.5, 0.5) end GameTooltip:AddLine("[右键] 使用装备效果", 0.45, 0.45, 0.45) GameTooltip:AddLine("[Alt] 查看可替换装备", 0.45, 0.45, 0.45) GameTooltip:Show() end end) frame:SetScript("OnLeave", function() this:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) GameTooltip:Hide() this._mouseOver = nil this._swapActive = nil end) frame:SetScript("OnUpdate", function() if not this._mouseOver then return end if IsAltKeyDown() then if not this._swapActive then this._swapActive = true GameTooltip:Hide() CP:ShowSwapPopup(this) end else if this._swapActive then this._swapActive = nil if swapPopup then swapPopup:Hide() end GameTooltip:SetOwner(this, "ANCHOR_RIGHT") if GetInventoryItemLink("player", this.slotID) then GameTooltip:SetInventoryItem("player", this.slotID) else local label = SLOT_LABEL[this.slotName] or this.slotName GameTooltip:AddLine(label .. " - 未装备", 0.5, 0.5, 0.5) end GameTooltip:AddLine("[Alt] 查看可替换装备", 0.45, 0.45, 0.45) GameTooltip:Show() end end end) return frame end local equipUpdateTimer = CreateFrame("Frame", nil, UIParent) equipUpdateTimer:Hide() equipUpdateTimer._elapsed = 0 equipUpdateTimer._pending = 0 equipUpdateTimer:SetScript("OnUpdate", function() this._elapsed = this._elapsed + arg1 if this._elapsed >= 0.15 then this._elapsed = 0 this._pending = this._pending - 1 CP:UpdateEquipment() if this._pending <= 0 then this._pending = 0 this:Hide() end end end) function CP:ScheduleEquipUpdate() equipUpdateTimer._elapsed = 0 equipUpdateTimer._pending = 3 equipUpdateTimer:Show() end function CP:UpdateEquipment() local page = pages[1] if not page or not page.built then return end local showModel = not SFramesDB or SFramesDB.charShowModel ~= false if page.model then if showModel then page.model:Show() if page.modelFrame then page.modelFrame:Show() end if page.modelBgFrame then page.modelBgFrame:Show() end if page.modelToolbar then page.modelToolbar:Show() end page.model:SetUnit("player") page.model:SetFacing(page.model.curFacing or 0.4) page.model:SetModelScale(page.model.curScale or 1.0) page.model:SetPosition(page.model.posY or 0, 0, page.model.posX or 0) else page.model:Hide() if page.modelFrame then page.modelFrame:Hide() end if page.modelBgFrame then page.modelBgFrame:Hide() end if page.modelToolbar then page.modelToolbar:Hide() end end end local showGlow = not SFramesDB or SFramesDB.charShowQualityGlow ~= false local allSlots = {} for _, t in ipairs({ EQUIP_SLOTS_LEFT, EQUIP_SLOTS_RIGHT, EQUIP_SLOTS_BOTTOM }) do for _, s in ipairs(t) do table.insert(allSlots, s) end end for _, si in ipairs(allSlots) do local slot = page.equipSlots[si.id] if slot then local tex = GetInventoryItemTexture("player", si.id) local link = GetInventoryItemLink("player", si.id) if tex then slot.icon:SetTexture(tex) slot.icon:SetVertexColor(1, 1, 1) if slot.ilvlText then local showIlvl = not SFramesDB or SFramesDB.showItemLevel ~= false if showIlvl and link and LibItem_Level then local _, _, itemId = string.find(link, "item:(%d+)") local ilvl = itemId and LibItem_Level[tonumber(itemId)] slot.ilvlText:SetText(ilvl and tostring(ilvl) or "") else slot.ilvlText:SetText("") end end local quality = GetItemQualityFromLink(link) if quality and quality >= 2 and QUALITY_COLORS[quality] then local qc = QUALITY_COLORS[quality] if showGlow then slot.qualGlow:SetVertexColor(qc[1], qc[2], qc[3]) slot.qualGlow:Show() else slot.qualGlow:Hide() end else slot.qualGlow:Hide() end else if slot.emptyTexture then slot.icon:SetTexture(slot.emptyTexture) slot.icon:SetVertexColor(T.emptySlot[1], T.emptySlot[2], T.emptySlot[3], T.emptySlot[4]) else slot.icon:SetTexture(nil) end slot:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) slot.currentBorderColor = nil slot.qualGlow:Hide() if slot.ilvlText then slot.ilvlText:SetText("") end end end end self:RefreshStatValues(page) end local function GetStatVal(key) if key == "meleeAP" then local b, p, n = UnitAttackPower("player"); return tostring(math.floor((b or 0) + (p or 0) + (n or 0))) elseif key == "meleeDmg" then local lo, hi = UnitDamage("player"); return math.floor(lo or 0) .. "-" .. math.floor(hi or 0) elseif key == "meleeSpeed" then local ms, os = UnitAttackSpeed("player") local t = string.format("%.1f", ms or 0) if os and os > 0 then t = t .. "/" .. string.format("%.1f", os) end; return t elseif key == "meleeCrit" then return string.format("%.1f%%", SafeGetMeleeCrit()) elseif key == "meleeHit" then return string.format("%d%%", SafeGetMeleeHit()) elseif key == "rangedAP" then local b, p, n = UnitRangedAttackPower("player"); return tostring(math.floor((b or 0) + (p or 0) + (n or 0))) elseif key == "rangedDmg" then local _, lo, hi = UnitRangedDamage("player"); return math.floor(lo or 0) .. "-" .. math.floor(hi or 0) elseif key == "rangedSpeed" then local sp = UnitRangedDamage("player"); return string.format("%.1f", sp or 0) elseif key == "rangedCrit" then return string.format("%.1f%%", SafeGetRangedCrit()) elseif key == "spellDmg" then local mx = 0 if GetSpellBonusDamage then for s = 2, 7 do local d = GetSpellBonusDamage(s); if d and d > mx then mx = d end end end if mx == 0 then local lib = GetItemBonusLib() if lib then local baseDmg = lib:GetBonus("DMG") or 0 mx = baseDmg local schools = { "FIREDMG","FROSTDMG","SHADOWDMG","ARCANEDMG","NATUREDMG","HOLYDMG" } for _, sk in ipairs(schools) do local sv = baseDmg + (lib:GetBonus(sk) or 0) if sv > mx then mx = sv end end end end return tostring(math.floor(mx)) elseif key == "spellHeal" then local v = GetSpellBonusHealing and GetSpellBonusHealing() or 0 if v == 0 then v = GetGearBonus("HEAL") end return tostring(math.floor(v)) elseif key == "spellCrit" then return string.format("%.1f%%", SafeGetSpellCrit()) elseif key == "spellHit" then return string.format("%d%%", SafeGetSpellHit()) elseif key == "armor" then local b, e = UnitArmor("player"); return tostring(math.floor(e or b or 0)) elseif key == "defense" then local b, m = UnitDefense("player"); return tostring(math.floor((b or 0) + (m or 0))) elseif key == "dodge" then local v = GetDodgeChance and GetDodgeChance() or 0 if v == 0 then v = GetGearBonus("DODGE") end return string.format("%.1f%%", v) elseif key == "parry" then local v = GetParryChance and GetParryChance() or 0 if v == 0 then v = GetGearBonus("PARRY") end return string.format("%.1f%%", v) elseif key == "block" then local v = GetBlockChance and GetBlockChance() or 0 if v == 0 then v = GetGearBonus("BLOCK") end return string.format("%.1f%%", v) end return "0" end function CP:RefreshStatValues(page) if not page.built then return end CP:RefreshStatCatVisual(page) -- Cat 1: base (left) + resist (right) if page.cat1Rows then for _, row in ipairs(page.cat1Rows) do local stat, eff, pos, neg = UnitStat("player", row.baseIdx) local e = eff or stat or 0 row.valueL:SetText(tostring(math.floor(e))) if row.valueR and row.resistSchool then local b, total = UnitResistance("player", row.resistSchool) row.valueR:SetText(tostring(math.floor(total or b or 0))) end end end -- Cat 2: melee (left) + ranged (right) if page.cat2Rows then for _, row in ipairs(page.cat2Rows) do if row.meleeKey then row.valueL:SetText(GetStatVal(row.meleeKey)) end if row.rangedKey and row.valueR then row.valueR:SetText(GetStatVal(row.rangedKey)) end end end -- Cat 3: spell (left) + defense (right) if page.cat3Rows then for _, row in ipairs(page.cat3Rows) do if row.spellKey and row.valueL then row.valueL:SetText(GetStatVal(row.spellKey)) end if row.defKey and row.valueR then row.valueR:SetText(GetStatVal(row.defKey)) end end end end -------------------------------------------------------------------------------- -- Tab 2: Reputation (was Tab 3) -------------------------------------------------------------------------------- function CP:BuildReputationPage() local page = pages[2] if page.built then return end page.built = true local contentH = FRAME_H - (HEADER_H + TAB_BAR_H) - INNER_PAD - 4 local scrollArea = CreateScrollFrame(page, CONTENT_W, contentH) scrollArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) page.scrollArea = scrollArea page.repRows = {} end function CP:UpdateReputation() local page = pages[2] if not page or not page.built then return end local child = page.scrollArea.child if page.repRows then for _, row in ipairs(page.repRows) do if row.frame then row.frame:Hide() end end end page.repRows = {} local numFactions = GetNumFactions() local y = -6 local rowH, barH, headerH = 30, 9, 24 for i = 1, numFactions do local name, desc, standingId, barMin, barMax, barValue, atWar, canWar, isHeader, isCollapsed, hasRep, isWatched, isChild if GetFactionInfo then name, desc, standingId, barMin, barMax, barValue, atWar, canWar, isHeader, isCollapsed, hasRep, isWatched, isChild = GetFactionInfo(i) end if not name then break end if isHeader then local hf = CreateFrame("Button", nil, child) hf:SetWidth(SCROLL_W - 16) hf:SetHeight(headerH) hf:SetPoint("TOPLEFT", child, "TOPLEFT", 8, y) local arrow = MakeFS(hf, 10, "LEFT", T.dimText) arrow:SetPoint("LEFT", hf, "LEFT", 0, 0) arrow:SetText(isCollapsed and "+" or "-") local ht = MakeFS(hf, 11, "LEFT", T.sectionTitle) ht:SetPoint("LEFT", arrow, "RIGHT", 3, 0) ht:SetText(name) hf.factionIndex = i hf:SetScript("OnClick", function() if isCollapsed then ExpandFactionHeader(this.factionIndex) else CollapseFactionHeader(this.factionIndex) end CP:UpdateReputation() end) table.insert(page.repRows, { frame = hf }) y = y - headerH else local rf = CreateFrame("Frame", nil, child) rf:SetWidth(SCROLL_W - 24) rf:SetHeight(rowH) rf:SetPoint("TOPLEFT", child, "TOPLEFT", isChild and 22 or 14, y) local nfs = MakeFS(rf, 9, "LEFT", T.valueText) nfs:SetPoint("TOPLEFT", rf, "TOPLEFT", 0, -2) nfs:SetText(name or "") local rc = T.repColors[standingId] or { 0.5, 0.5, 0.5 } local sfs = MakeFS(rf, 8, "RIGHT", rc) sfs:SetPoint("TOPRIGHT", rf, "TOPRIGHT", 0, -2) sfs:SetText(REP_STANDING[standingId] or "") local bf = CreateFrame("Frame", nil, rf) bf:SetHeight(barH) bf:SetPoint("BOTTOMLEFT", rf, "BOTTOMLEFT", 0, 2) bf:SetPoint("BOTTOMRIGHT", rf, "BOTTOMRIGHT", 0, 2) SetPixelBackdrop(bf, T.barBg, { 0.15, 0.15, 0.18, 0.5 }) local bar = bf:CreateTexture(nil, "ARTWORK") bar:SetTexture(SFrames:GetTexture()) bar:SetVertexColor(rc[1], rc[2], rc[3], 0.85) bar:SetPoint("TOPLEFT", bf, "TOPLEFT", 1, -1) bar:SetPoint("BOTTOMLEFT", bf, "BOTTOMLEFT", 1, 1) local range = math.max((barMax or 1) - (barMin or 0), 1) local fill = Clamp(((barValue or 0) - (barMin or 0)) / range, 0, 1) bar:SetWidth(math.max((SCROLL_W - 26) * fill, 1)) local vfs = MakeFS(bf, 7, "CENTER", { 0.85, 0.85, 0.85 }) vfs:SetPoint("CENTER", bf, "CENTER", 0, 0) vfs:SetText(math.floor((barValue or 0) - (barMin or 0)) .. "/" .. math.floor(range)) table.insert(page.repRows, { frame = rf }) y = y - rowH end end page.scrollArea:SetContentHeight(math.abs(y) + 16) end -------------------------------------------------------------------------------- -- Tab 3: Skills (was Tab 4) -------------------------------------------------------------------------------- function CP:BuildSkillsPage() local page = pages[3] if page.built then return end page.built = true local contentH = FRAME_H - (HEADER_H + TAB_BAR_H) - INNER_PAD - 4 local scrollArea = CreateScrollFrame(page, CONTENT_W, contentH) scrollArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) page.scrollArea = scrollArea page.skillRows = {} end do local TRADE_HEADERS = { ["Trade Skills"] = true, ["专业技能"] = true, ["Professions"] = true, ["商业技能"] = true, ["专业"] = true } local pendingAbandonIndex StaticPopupDialogs = StaticPopupDialogs or {} StaticPopupDialogs["NANAMI_ABANDON_SKILL"] = { text = "确定要遗弃 %s 吗?\n该操作不可撤销!", button1 = "确定", button2 = "取消", OnAccept = function() if pendingAbandonIndex and AbandonSkill then AbandonSkill(pendingAbandonIndex) pendingAbandonIndex = nil CP:UpdateSkills() end end, OnCancel = function() pendingAbandonIndex = nil end, timeout = 0, whileDead = true, hideOnEscape = true, } function CP:UpdateSkills() local page = pages[3] if not page or not page.built then return end local child = page.scrollArea.child if page.skillRows then for _, row in ipairs(page.skillRows) do if row.frame then row.frame:Hide() end end end page.skillRows = {} local numSkills = GetNumSkillLines and GetNumSkillLines() or 0 local y = -6 local rowH, barH, headerH = 28, 7, 24 local currentHeader = "" local delBtnSize = 14 for i = 1, numSkills do local sn, isH, isE, sr, nt, sm, smr, isAbandonable if GetSkillLineInfo then sn, isH, isE, sr, nt, sm, smr, isAbandonable = GetSkillLineInfo(i) end if not sn then break end if isH then currentHeader = sn or "" local hf = CreateFrame("Button", nil, child) hf:SetWidth(SCROLL_W - 16) hf:SetHeight(headerH) hf:SetPoint("TOPLEFT", child, "TOPLEFT", 8, y) local arrow = MakeFS(hf, 10, "LEFT", T.dimText) arrow:SetPoint("LEFT", hf, "LEFT", 0, 0) arrow:SetText(isE and "-" or "+") local ht = MakeFS(hf, 11, "LEFT", T.sectionTitle) ht:SetPoint("LEFT", arrow, "RIGHT", 3, 0) ht:SetText(sn) hf.skillIndex = i hf:SetScript("OnClick", function() if isE then CollapseSkillHeader(this.skillIndex) else ExpandSkillHeader(this.skillIndex) end CP:UpdateSkills() end) table.insert(page.skillRows, { frame = hf }) y = y - headerH else local canAbandon = TRADE_HEADERS[currentHeader] local rightPad = canAbandon and (delBtnSize + 6) or 0 local sf = CreateFrame("Frame", nil, child) sf:SetWidth(SCROLL_W - 24) sf:SetHeight(rowH) sf:SetPoint("TOPLEFT", child, "TOPLEFT", 18, y) local nfs = MakeFS(sf, 9, "LEFT", T.valueText) nfs:SetPoint("TOPLEFT", sf, "TOPLEFT", 0, -2) nfs:SetText(sn or "") local rt = tostring(sr or 0) if smr and smr > 0 then rt = rt .. "/" .. tostring(smr) end local rfs = MakeFS(sf, 8, "RIGHT", T.dimText) rfs:SetPoint("TOPRIGHT", sf, "TOPRIGHT", -rightPad, -2) rfs:SetText(rt) if smr and smr > 0 then local bf = CreateFrame("Frame", nil, sf) bf:SetHeight(barH) bf:SetPoint("BOTTOMLEFT", sf, "BOTTOMLEFT", 0, 2) bf:SetPoint("BOTTOMRIGHT", sf, "BOTTOMRIGHT", -rightPad, 2) SetPixelBackdrop(bf, T.barBg, { 0.15, 0.15, 0.18, 0.5 }) local bar = bf:CreateTexture(nil, "ARTWORK") bar:SetTexture(SFrames:GetTexture()) bar:SetVertexColor(0.4, 0.65, 0.85, 0.85) bar:SetPoint("TOPLEFT", bf, "TOPLEFT", 1, -1) bar:SetPoint("BOTTOMLEFT", bf, "BOTTOMLEFT", 1, 1) bar:SetWidth(math.max((SCROLL_W - 26 - rightPad) * Clamp((sr or 0) / smr, 0, 1), 1)) end if canAbandon then local db = CreateFrame("Button", nil, sf) db:SetWidth(delBtnSize) db:SetHeight(delBtnSize) db:SetPoint("RIGHT", sf, "RIGHT", 0, 0) db:SetFrameLevel(sf:GetFrameLevel() + 2) SetPixelBackdrop(db, { 0.25, 0.08, 0.08, 0.7 }, { 0.5, 0.15, 0.15, 0.6 }) local ico = SFrames:CreateIcon(db, "close", 8) ico:SetDrawLayer("OVERLAY") ico:SetPoint("CENTER", db, "CENTER", 0, 0) ico:SetVertexColor(0.9, 0.4, 0.4) db.skillIndex = i db.skillName = sn db:SetScript("OnClick", function() pendingAbandonIndex = this.skillIndex if StaticPopup_Show then StaticPopup_Show("NANAMI_ABANDON_SKILL", this.skillName) end end) db:SetScript("OnEnter", function() this:SetBackdropColor(0.45, 0.1, 0.1, 0.9) this:SetBackdropBorderColor(0.8, 0.2, 0.2, 0.9) GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:AddLine("遗弃 " .. (this.skillName or ""), 1, 0.4, 0.4) GameTooltip:AddLine("点击遗弃该技能", 0.7, 0.7, 0.7) GameTooltip:Show() end) db:SetScript("OnLeave", function() this:SetBackdropColor(0.25, 0.08, 0.08, 0.7) this:SetBackdropBorderColor(0.5, 0.15, 0.15, 0.6) GameTooltip:Hide() end) end table.insert(page.skillRows, { frame = sf }) y = y - rowH end end page.scrollArea:SetContentHeight(math.abs(y) + 16) end end -------------------------------------------------------------------------------- -- Tab 4: Honor (was Tab 5) -------------------------------------------------------------------------------- function CP:BuildHonorPage() local page = pages[4] if page.built then return end page.built = true local pad = SIDE_PAD + 2 page.rankIcon = page:CreateTexture(nil, "ARTWORK") page.rankIcon:SetWidth(42) page.rankIcon:SetHeight(42) page.rankIcon:SetPoint("TOPLEFT", page, "TOPLEFT", pad, -16) page.rankName = MakeFS(page, 13, "LEFT", T.gold) page.rankName:SetPoint("LEFT", page.rankIcon, "RIGHT", 10, 5) page.rankProgress = MakeFS(page, 10, "LEFT", T.dimText) page.rankProgress:SetPoint("LEFT", page.rankIcon, "RIGHT", 10, -8) local barY = -68 local bf = CreateFrame("Frame", nil, page) bf:SetHeight(12) bf:SetPoint("TOPLEFT", page, "TOPLEFT", pad, barY) bf:SetPoint("TOPRIGHT", page, "TOPRIGHT", -pad, barY) SetPixelBackdrop(bf, T.barBg, { 0.15, 0.15, 0.18, 0.5 }) page.rankBarFrame = bf local bfill = bf:CreateTexture(nil, "ARTWORK") bfill:SetTexture(SFrames:GetTexture()) bfill:SetVertexColor(0.85, 0.6, 0.1, 0.9) bfill:SetPoint("TOPLEFT", bf, "TOPLEFT", 1, -1) bfill:SetPoint("BOTTOMLEFT", bf, "BOTTOMLEFT", 1, 1) bfill:SetWidth(1) page.rankBarFill = bfill page.rankBarText = MakeFS(bf, 8, "CENTER", { 1, 1, 1 }) page.rankBarText:SetPoint("CENTER", bf, "CENTER", 0, 0) local sY = barY - 26 sY = self:CreateStatSection(page, "荣誉统计", sY) page.honorStats = {} local hl = { "本次击杀", "本次荣誉", "昨日击杀", "昨日荣誉", "本周击杀", "上周击杀", "上周荣誉", "上周排名", "终身击杀" } for _, lbl in ipairs(hl) do local row; row, sY = self:CreateStatRow(page, lbl, sY) table.insert(page.honorStats, { row = row }) end end function CP:UpdateHonor() local page = pages[4] if not page or not page.built then return end local rn, rnum if GetPVPRankInfo then rn, rnum = GetPVPRankInfo(UnitPVPRank("player")) end rn = rn or "无军衔"; rnum = rnum or 0 page.rankName:SetText(rn) if rnum > 0 then page.rankIcon:SetTexture(string.format("Interface\\PvPRankBadges\\PvPRank%02d", rnum)) else page.rankIcon:SetTexture(nil) end local prog = GetPVPRankProgress and GetPVPRankProgress() or 0 page.rankProgress:SetText(string.format("进度: %.1f%%", prog * 100)) local bw = page.rankBarFrame:GetWidth() - 2 page.rankBarFill:SetWidth(math.max(bw * prog, 1)) page.rankBarText:SetText(string.format("%.1f%%", prog * 100)) local thk, tho = 0, 0; if GetPVPSessionStats then thk, tho = GetPVPSessionStats() end local yhk, yho = 0, 0; if GetPVPYesterdayStats then yhk, yho = GetPVPYesterdayStats() end local whk = 0; if GetPVPThisWeekStats then whk = GetPVPThisWeekStats() end local lhk, lho, ls = 0, 0, 0; if GetPVPLastWeekStats then lhk, lho, ls = GetPVPLastWeekStats() end local ltk = 0; if GetPVPLifetimeStats then ltk = GetPVPLifetimeStats() end local vals = { tostring(thk or 0), tostring(math.floor(tho or 0)), tostring(yhk or 0), tostring(math.floor(yho or 0)), tostring(whk or 0), tostring(lhk or 0), tostring(math.floor(lho or 0)), tostring(ls or 0), tostring(ltk or 0), } for i, stat in ipairs(page.honorStats) do stat.row.value:SetText(vals[i] or "0") end end -------------------------------------------------------------------------------- -- Events -------------------------------------------------------------------------------- local eventFrame = CreateFrame("Frame", "SFramesCPEvents", UIParent) local cpEvents = { "UNIT_INVENTORY_CHANGED", "PLAYER_AURAS_CHANGED", "UPDATE_FACTION", "SKILL_LINES_CHANGED", "PLAYER_PVP_KILLS_CHANGED", "PLAYER_PVP_RANK_CHANGED", "UNIT_ATTACK_POWER", "UNIT_RANGEDDAMAGE", "UNIT_ATTACK", "UNIT_DEFENSE", "UNIT_RESISTANCES", "CHAT_MSG_SKILL", "CHAT_MSG_COMBAT_HONOR_GAIN", "CHARACTER_POINTS_CHANGED", "PLAYER_ENTERING_WORLD", } for _, ev in ipairs(cpEvents) do pcall(function() eventFrame:RegisterEvent(ev) end) end eventFrame:SetScript("OnEvent", function() if not panel or not panel:IsShown() then return end if event == "UNIT_INVENTORY_CHANGED" then CP:ScheduleEquipUpdate() end CP:UpdateCurrentTab() end) -------------------------------------------------------------------------------- -- Hook: replace ToggleCharacter -------------------------------------------------------------------------------- -- Save CharacterFrame's original Show before HideBlizzardFrames may suppress it local origCharFrameShow = CharacterFrame and CharacterFrame.Show local origToggleCharacter = ToggleCharacter ToggleCharacter = function(tab) if SFramesDB and SFramesDB.charPanelEnable == false then if CharacterFrame then if origCharFrameShow then CharacterFrame.Show = origCharFrameShow end if origToggleCharacter then origToggleCharacter(tab) else if CharacterFrame:IsShown() then HideUIPanel(CharacterFrame) else ShowUIPanel(CharacterFrame) end end end return end local tabMap = { ["PaperDollFrame"] = 1, ["ReputationFrame"] = 2, ["SkillFrame"] = 3, ["HonorFrame"] = 4, ["PetPaperDollFrame"] = 1, } CP:Toggle(tabMap[tab] or 1) end