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