Files
Nanami-UI/CharacterPanel.lua
2026-03-24 15:56:28 +08:00

3914 lines
156 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

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

--------------------------------------------------------------------------------
-- Nanami-UI: 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] = "崇拜",
}
local PET_TAB_INDEX = nil
local PET_FOOD_MAP = {
["Meat"] = "肉类", ["Fish"] = "鱼类", ["Cheese"] = "奶酪",
["Bread"] = "面包", ["Fungus"] = "蘑菇", ["Fruit"] = "水果",
["Raw Meat"] = "生肉", ["Raw Fish"] = "生鱼",
["Cooked Meat"] = "熟肉", ["Cooked Fish"] = "熟鱼",
}
local PET_HAPPINESS = {
[1] = { text = "不高兴", color = { 0.9, 0.2, 0.2 } },
[2] = { text = "满足", color = { 0.9, 0.75, 0.2 } },
[3] = { text = "高兴", color = { 0.2, 0.9, 0.2 } },
}
local BEAST_TRAINING_NAMES = {
["Beast Training"] = true, ["训练野兽"] = true,
}
--------------------------------------------------------------------------------
-- 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 SaveCharPanelPosition()
if not (panel and SFramesDB) then return end
if not SFramesDB.charPanel then SFramesDB.charPanel = {} end
local point, _, relPoint, x, y = panel:GetPoint()
if not point or not relPoint then return end
SFramesDB.charPanel.position = {
point = point,
relPoint = relPoint,
x = x or 0,
y = y or 0,
}
end
local function ApplyCharPanelPosition(f)
f:ClearAllPoints()
local pos = SFramesDB and SFramesDB.charPanel and SFramesDB.charPanel.position
if pos and pos.point and pos.relPoint and type(pos.x) == "number" and type(pos.y) == "number" then
f:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y)
else
f:SetPoint("LEFT", UIParent, "LEFT", 20, 0)
end
end
local function CreateMainFrame()
if panel then return panel end
local f = CreateFrame("Frame", "SFramesCharacterPanel", UIParent)
f:SetWidth(FRAME_W)
f:SetHeight(FRAME_H)
ApplyCharPanelPosition(f)
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()
SaveCharPanelPosition()
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)
local _, playerClass = UnitClass("player")
if (playerClass == "HUNTER" or playerClass == "WARLOCK") and not PET_TAB_INDEX then
table.insert(TAB_NAMES, "宠物")
PET_TAB_INDEX = table.getn(TAB_NAMES)
end
-- 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()
elseif PET_TAB_INDEX and tab == PET_TAB_INDEX then self:UpdatePet()
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()
if PET_TAB_INDEX then self:BuildPetPage() end
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()
local targetID = popup.anchorSlot and popup.anchorSlot.slotID
if targetID then
PickupContainerItem(this.itemBag, this.itemSlot)
PickupInventoryItem(targetID)
else
UseContainerItem(this.itemBag, this.itemSlot)
end
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
if BEAST_TRAINING_NAMES[sn] then
local tp = 0
if GetPetTrainingPoints then
local ok2, total, spent = pcall(GetPetTrainingPoints)
if ok2 then tp = (total or 0) - (spent or 0) end
end
local tpFs = MakeFS(sf, 8, "LEFT", { 0.55, 0.85, 0.4 })
tpFs:SetPoint("TOPLEFT", nfs, "BOTTOMLEFT", 0, -1)
tpFs:SetText("可用训练点数: " .. tostring(tp))
sf:EnableMouse(true)
sf.skillName = sn
sf:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:AddLine(this.skillName or "Beast Training", 1, 0.82, 0)
GameTooltip:AddLine("打开训练野兽窗口,教授宠物技能", 0.7, 0.7, 0.7)
GameTooltip:AddLine("消耗训练点数来教授宠物各种技能", 0.7, 0.7, 0.7)
GameTooltip:Show()
end)
sf:SetScript("OnLeave", function() GameTooltip:Hide() end)
sf:SetHeight(rowH + 12)
y = y - 12
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
--------------------------------------------------------------------------------
-- Tab 5: Pet (Hunter)
--------------------------------------------------------------------------------
function CP:BuildPetPage()
if not PET_TAB_INDEX then return end
local page = pages[PET_TAB_INDEX]
if not page or page.built then return end
page.built = true
local cw = CONTENT_W
local contentH = FRAME_H - (HEADER_H + TAB_BAR_H) - INNER_PAD - 4
local pad = 8
local pc = CreateFrame("Frame", nil, page)
pc:SetAllPoints(page)
page.petContent = pc
page.noPetText = MakeFS(page, 12, "CENTER", T.dimText)
page.noPetText:SetPoint("CENTER", page, "CENTER", 0, 0)
page.noPetText:SetText("当前没有宠物")
page.noPetText:Hide()
-- 3D Model
local modelH = 180
local modelW = cw - 8
local modelBg = CreateFrame("Frame", nil, pc)
modelBg:SetWidth(modelW)
modelBg:SetHeight(modelH)
modelBg:SetPoint("TOP", pc, "TOP", 0, -2)
SetRoundBackdrop(modelBg, T.modelBg, T.modelBorder)
page.modelBgFrame = modelBg
local modelFrame = CreateFrame("Frame", nil, pc)
modelFrame:SetWidth(modelW - 8)
modelFrame:SetHeight(modelH - 8)
modelFrame:SetPoint("CENTER", modelBg, "CENTER", 0, 0)
modelFrame:SetFrameLevel(pc:GetFrameLevel() + 5)
local model = CreateFrame("PlayerModel", NextName("PetModel"), modelFrame)
model:SetAllPoints(modelFrame)
page.model = model
page.modelFrame = modelFrame
model:EnableMouse(true)
model:EnableMouseWheel(1)
model.rotating = false
model.curFacing = 0.4
model.curScale = 0.55
model.posX = 0
model.posY = -0.5
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 ns = (this.curScale or 1) + arg1 * 0.1
if ns < 0.3 then ns = 0.3 end
if ns > 3.0 then ns = 3.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
this.curFacing = (this.startFacing or 0) + diff
this:SetFacing(this.curFacing)
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)
end
end)
-- Name overlay at bottom of model
page.petNameText = MakeFS(pc, 12, "LEFT", T.gold)
page.petNameText:SetPoint("BOTTOMLEFT", modelBg, "BOTTOMLEFT", 8, 4)
page.petFamilyText = MakeFS(pc, 9, "RIGHT", T.dimText)
page.petFamilyText:SetPoint("BOTTOMRIGHT", modelBg, "BOTTOMRIGHT", -8, 4)
-- Scrollable stats area below model
local statsTop = -(modelH + 6)
local statsH = contentH - modelH - 6
local scrollArea = CreateScrollFrame(pc, cw, statsH)
scrollArea:SetPoint("TOPLEFT", pc, "TOPLEFT", 0, statsTop)
page.scrollArea = scrollArea
local child = scrollArea.child
local sY = -4
-- Info line: happiness, loyalty, training points
page.happyLabel = MakeFS(child, 9, "LEFT", T.labelText)
page.happyLabel:SetPoint("TOPLEFT", child, "TOPLEFT", pad, sY)
page.happyLabel:SetText("心情:")
page.happyValue = MakeFS(child, 9, "LEFT", T.valueText)
page.happyValue:SetPoint("LEFT", page.happyLabel, "RIGHT", 2, 0)
page.loyalLabel = MakeFS(child, 9, "LEFT", T.labelText)
page.loyalLabel:SetPoint("LEFT", page.happyValue, "RIGHT", 10, 0)
page.loyalLabel:SetText("忠诚度:")
page.loyalValue = MakeFS(child, 9, "LEFT", T.valueText)
page.loyalValue:SetPoint("LEFT", page.loyalLabel, "RIGHT", 2, 0)
page.tpLabel = MakeFS(child, 9, "LEFT", T.labelText)
page.tpLabel:SetPoint("LEFT", page.loyalValue, "RIGHT", 10, 0)
page.tpLabel:SetText("训练点:")
page.tpValue = MakeFS(child, 9, "LEFT", { 0.55, 0.85, 0.4 })
page.tpValue:SetPoint("LEFT", page.tpLabel, "RIGHT", 2, 0)
sY = sY - 16
-- XP bar
page.xpSectionLabel = MakeFS(child, 9, "LEFT", T.sectionTitle)
page.xpSectionLabel:SetPoint("TOPLEFT", child, "TOPLEFT", pad, sY)
page.xpSectionLabel:SetText("经验值")
sY = sY - 12
local xpBf = CreateFrame("Frame", nil, child)
xpBf:SetHeight(8)
xpBf:SetPoint("TOPLEFT", child, "TOPLEFT", pad, sY)
xpBf:SetPoint("TOPRIGHT", child, "TOPRIGHT", -pad, sY)
SetPixelBackdrop(xpBf, T.barBg, { 0.15, 0.15, 0.18, 0.5 })
page.xpBarFrame = xpBf
local xpFill = xpBf:CreateTexture(nil, "ARTWORK")
xpFill:SetTexture(SFrames:GetTexture())
xpFill:SetVertexColor(0.4, 0.65, 0.85, 0.9)
xpFill:SetPoint("TOPLEFT", xpBf, "TOPLEFT", 1, -1)
xpFill:SetPoint("BOTTOMLEFT", xpBf, "BOTTOMLEFT", 1, 1)
xpFill:SetWidth(1)
page.xpBarFill = xpFill
page.xpBarText = MakeFS(xpBf, 7, "CENTER", { 1, 1, 1 })
page.xpBarText:SetPoint("CENTER", xpBf, "CENTER", 0, 0)
sY = sY - 14
-- Stats dual-column section
sY = self:CreateStatSection(child, "属性与攻防", sY)
local leftLabels = { "力量", "敏捷", "耐力", "智力", "精神" }
local rightLabels = { "攻击", "强度", "伤害", "防御", "护甲" }
page.petStatLeft = {}
page.petStatRight = {}
for idx = 1, 5 do
local row1 = {}
row1.label = MakeFS(child, 9, "LEFT", T.labelText)
row1.label:SetPoint("TOPLEFT", child, "TOPLEFT", 14, sY)
row1.label:SetText(leftLabels[idx] .. ":")
row1.value = MakeFS(child, 9, "RIGHT", T.valueText)
row1.value:SetPoint("TOPLEFT", child, "TOPLEFT", 56, sY)
row1.value:SetWidth(80)
row1.value:SetJustifyH("RIGHT")
table.insert(page.petStatLeft, row1)
local row2 = {}
row2.label = MakeFS(child, 9, "LEFT", T.labelText)
row2.label:SetPoint("TOPLEFT", child, "TOPLEFT", 160, sY)
row2.label:SetText(rightLabels[idx] .. ":")
row2.value = MakeFS(child, 9, "RIGHT", T.valueText)
row2.value:SetPoint("TOPRIGHT", child, "TOPRIGHT", -14, sY)
row2.value:SetWidth(80)
row2.value:SetJustifyH("RIGHT")
table.insert(page.petStatRight, row2)
sY = sY - 14
end
page.petAtkSpeedLabel = MakeFS(child, 9, "LEFT", T.labelText)
page.petAtkSpeedLabel:SetPoint("TOPLEFT", child, "TOPLEFT", 160, sY)
page.petAtkSpeedLabel:SetText("攻速:")
page.petAtkSpeedValue = MakeFS(child, 9, "RIGHT", T.valueText)
page.petAtkSpeedValue:SetPoint("TOPRIGHT", child, "TOPRIGHT", -14, sY)
page.petAtkSpeedValue:SetWidth(80)
page.petAtkSpeedValue:SetJustifyH("RIGHT")
sY = sY - 14
-- Resistances
sY = sY - 4
sY = self:CreateStatSection(child, "抗性", sY)
page.resStats = {}
local resSchools = { 2, 3, 4, 5, 6 }
local resPerRow = 3
local resColW = math.floor((cw - 28) / resPerRow)
for idx = 1, 5 do
local col = math.mod(idx - 1, resPerRow)
local rowOff = math.floor((idx - 1) / resPerRow)
local rx = 14 + col * resColW
local ry = sY - rowOff * 14
local row = {}
local school = resSchools[idx]
local rc = T.resistColors[school] or T.labelText
row.label = MakeFS(child, 9, "LEFT", rc)
row.label:SetPoint("TOPLEFT", child, "TOPLEFT", rx, ry)
row.label:SetText(RESIST_NAMES[school] .. ":")
row.value = MakeFS(child, 9, "LEFT", T.valueText)
row.value:SetPoint("LEFT", row.label, "RIGHT", 2, 0)
row.school = school
table.insert(page.resStats, row)
end
sY = sY - math.ceil(5 / resPerRow) * 14
-- Food (manually created so we can show/hide for warlock)
sY = sY - 4
local foodHeader = MakeFS(child, 11, "LEFT", T.sectionTitle)
foodHeader:SetPoint("TOPLEFT", child, "TOPLEFT", 8, sY)
foodHeader:SetText("喜好食物")
local foodSep = child:CreateTexture(nil, "ARTWORK")
foodSep:SetTexture("Interface\\Buttons\\WHITE8X8")
foodSep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
foodSep:SetHeight(1)
foodSep:SetPoint("TOPLEFT", child, "TOPLEFT", 8, sY - 14)
foodSep:SetPoint("TOPRIGHT", child, "TOPRIGHT", -8, sY - 14)
sY = sY - 18
page.foodHeader = foodHeader
page.foodSep = foodSep
page.foodText = MakeFS(child, 9, "LEFT", T.valueText)
page.foodText:SetPoint("TOPLEFT", child, "TOPLEFT", 14, sY)
page.foodText:SetWidth(cw - 28)
sY = sY - 16
page.fullContentH = math.abs(sY) + 8
scrollArea:SetContentHeight(page.fullContentH)
end
function CP:UpdatePet()
if not PET_TAB_INDEX then return end
local page = pages[PET_TAB_INDEX]
if not page or not page.built then return end
local hasPetUI = HasPetUI and HasPetUI()
local hasPet = UnitExists("pet") and hasPetUI
if not hasPet then
page.petContent:Hide()
page.noPetText:Show()
return
end
page.petContent:Show()
page.noPetText:Hide()
-- Detect hunter pet vs warlock demon
local _, isHunterPet = HasPetUI()
-- Set 3D model (only reload when pet identity changes to avoid animation reset)
if page.model then
local petKey = (UnitName("pet") or "") .. ":" .. (UnitLevel("pet") or 0)
if page.model.lastPetKey ~= petKey then
page.model.lastPetKey = petKey
page.model.curFacing = 0.4
page.model.curScale = 0.55
page.model.posX = 0
page.model.posY = -0.5
page.model:SetUnit("pet")
page.model:SetFacing(0.4)
page.model:SetModelScale(0.55)
page.model:SetPosition(-0.5, 0, 0)
end
end
local petName = UnitName("pet") or "未知"
local petLevel = UnitLevel("pet") or 0
page.petNameText:SetText(petName .. " |cff88bbddLv." .. petLevel .. "|r")
local family = ""
if UnitCreatureFamily then
local ok, val = pcall(UnitCreatureFamily, "pet")
if ok and val then family = val end
end
page.petFamilyText:SetText(family)
-- Hunter-only: happiness, loyalty, training points, XP, food
if isHunterPet then
page.happyLabel:Show(); page.happyValue:Show()
page.loyalLabel:Show(); page.loyalValue:Show()
page.tpLabel:Show(); page.tpValue:Show()
if page.xpSectionLabel then page.xpSectionLabel:Show() end
page.xpBarFrame:Show()
if page.foodHeader then page.foodHeader:Show() end
if page.foodSep then page.foodSep:Show() end
page.foodText:Show()
local happiness = 0
if GetPetHappiness then
local ok, val = pcall(GetPetHappiness)
if ok and val then happiness = val end
end
local hData = PET_HAPPINESS[happiness]
if hData then
page.happyValue:SetText(hData.text)
page.happyValue:SetTextColor(hData.color[1], hData.color[2], hData.color[3])
else
page.happyValue:SetText("--")
page.happyValue:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
end
local loyalty = "--"
if GetPetLoyalty then
local ok, val = pcall(GetPetLoyalty)
if ok and val then loyalty = val end
end
page.loyalValue:SetText(tostring(loyalty))
local tp = 0
if GetPetTrainingPoints then
local ok, total, spent = pcall(GetPetTrainingPoints)
if ok then tp = (total or 0) - (spent or 0) end
end
page.tpValue:SetText(tostring(tp))
local curXP, maxXP = 0, 1
if GetPetExperience then
local ok, cx, mx = pcall(GetPetExperience)
if ok then curXP = cx or 0; maxXP = mx or 1 end
end
if maxXP == 0 then maxXP = 1 end
local xpPct = curXP / maxXP
local bw = page.xpBarFrame:GetWidth() - 2
if bw < 1 then bw = 1 end
page.xpBarFill:SetWidth(math.max(bw * xpPct, 1))
page.xpBarText:SetText(curXP .. " / " .. maxXP)
else
page.happyLabel:Hide(); page.happyValue:Hide()
page.loyalLabel:Hide(); page.loyalValue:Hide()
page.tpLabel:Hide(); page.tpValue:Hide()
if page.xpSectionLabel then page.xpSectionLabel:Hide() end
page.xpBarFrame:Hide()
if page.foodHeader then page.foodHeader:Hide() end
if page.foodSep then page.foodSep:Hide() end
page.foodText:Hide()
end
-- Base stats (UnitStat returns base, effective in vanilla; use first non-nil)
local statLabels = { "力量", "敏捷", "耐力", "智力", "精神" }
for i, r in ipairs(page.petStatLeft) do
local base, eff = UnitStat("pet", i)
local val = eff or base or 0
r.label:SetText(statLabels[i] .. ":")
r.value:SetText(tostring(val))
end
-- Combat stats
local mainBase, mainMod = 0, 0
if UnitAttack then
local ok, b, m = pcall(UnitAttack, "pet")
if ok then mainBase = b or 0; mainMod = m or 0 end
end
local apBase, apPos, apNeg = 0, 0, 0
if UnitAttackPower then
local ok, b, p, n = pcall(UnitAttackPower, "pet")
if ok then apBase = b or 0; apPos = p or 0; apNeg = n or 0 end
end
local ap = apBase + apPos + apNeg
local minDmg, maxDmg = 0, 0
if UnitDamage then
local ok, d1, d2 = pcall(UnitDamage, "pet")
if ok then minDmg = d1 or 0; maxDmg = d2 or 0 end
end
local defBase, defMod = 0, 0
if UnitDefense then
local ok, b, m = pcall(UnitDefense, "pet")
if ok then defBase = b or 0; defMod = m or 0 end
end
local armorBase, armorEff = 0, 0
if UnitArmor then
local ok, b, e = pcall(UnitArmor, "pet")
if ok then armorBase = b or 0; armorEff = e or 0 end
end
local combatLabels = { "攻击:", "强度:", "伤害:", "防御:", "护甲:" }
local combatVals = {
tostring(mainBase + mainMod),
tostring(ap),
string.format("%d-%d", math.floor(minDmg), math.floor(maxDmg)),
tostring(defBase + defMod),
tostring(armorEff > 0 and armorEff or armorBase),
}
for i, r in ipairs(page.petStatRight) do
r.label:SetText(combatLabels[i])
r.value:SetText(combatVals[i] or "0")
end
local petAtkSpeed = 2.0
if UnitAttackSpeed then
local ok, ms = pcall(UnitAttackSpeed, "pet")
if ok and ms then petAtkSpeed = ms end
end
page.petAtkSpeedValue:SetText(string.format("%.1f", petAtkSpeed))
for _, r in ipairs(page.resStats) do
local base, bonus = 0, 0
if UnitResistance then
local ok, b, bn = pcall(UnitResistance, "pet", r.school)
if ok then base = b or 0; bonus = bn or 0 end
end
r.label:SetText(RESIST_NAMES[r.school] .. ":")
r.value:SetText(tostring(base + bonus))
end
if isHunterPet then
local foodStr = ""
if GetPetFoodTypes then
local ok, r1, r2, r3, r4, r5, r6 = pcall(GetPetFoodTypes)
if ok then
local foods = {}
if r1 and r1 ~= "" then table.insert(foods, PET_FOOD_MAP[r1] or r1) end
if r2 and r2 ~= "" then table.insert(foods, PET_FOOD_MAP[r2] or r2) end
if r3 and r3 ~= "" then table.insert(foods, PET_FOOD_MAP[r3] or r3) end
if r4 and r4 ~= "" then table.insert(foods, PET_FOOD_MAP[r4] or r4) end
if r5 and r5 ~= "" then table.insert(foods, PET_FOOD_MAP[r5] or r5) end
if r6 and r6 ~= "" then table.insert(foods, PET_FOOD_MAP[r6] or r6) end
foodStr = table.concat(foods, "")
end
end
page.foodText:SetText(foodStr)
page.scrollArea:SetContentHeight(page.fullContentH)
else
page.scrollArea:SetContentHeight(page.fullContentH)
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",
"UNIT_PET", "PET_UI_UPDATE", "PET_BAR_UPDATE",
"UNIT_PET_EXPERIENCE", "PET_UI_CLOSE", "UNIT_HAPPINESS",
}
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
if tab == "PetPaperDollFrame" then
CreateMainFrame()
CP:Toggle(PET_TAB_INDEX or 1)
return
end
local tabMap = {
["PaperDollFrame"] = 1,
["ReputationFrame"] = 2,
["SkillFrame"] = 3,
["HonorFrame"] = 4,
}
CP:Toggle(tabMap[tab] or 1)
end