1530 lines
54 KiB
Lua
1530 lines
54 KiB
Lua
--------------------------------------------------------------------------------
|
|
-- Nanami-UI: AFK Screen (AFKScreen.lua)
|
|
-- Full-screen idle screen with dancing character model & info panel
|
|
--------------------------------------------------------------------------------
|
|
|
|
SFrames.AFKScreen = {}
|
|
local AFK = SFrames.AFKScreen
|
|
|
|
local FADE_SPEED = 1.25
|
|
local PARTICLE_COUNT = 18
|
|
local CLOCK_UPDATE_INTERVAL = 0.5
|
|
|
|
local CLASS_NAMES_ZH = {
|
|
["WARRIOR"] = "战士", ["MAGE"] = "法师", ["ROGUE"] = "盗贼",
|
|
["DRUID"] = "德鲁伊", ["HUNTER"] = "猎人", ["SHAMAN"] = "萨满",
|
|
["PRIEST"] = "牧师", ["WARLOCK"] = "术士", ["PALADIN"] = "圣骑士",
|
|
}
|
|
|
|
local T = SFrames.ActiveTheme
|
|
|
|
local WORLD_BUFF_DEFS = {
|
|
["Rallying Cry of the Dragonslayer"] = { display = "屠龙者的咆哮 (龙头)", priority = 1, color = {1, 0.85, 0.30} },
|
|
["屠龙者的咆哮"] = { display = "屠龙者的咆哮 (龙头)", priority = 1, color = {1, 0.85, 0.30} },
|
|
["巨龙杀手的战吼"] = { display = "屠龙者的咆哮 (龙头)", priority = 1, color = {1, 0.85, 0.30} },
|
|
|
|
["Warchief's Blessing"] = { display = "酋长的祝福", priority = 2, color = {0.90, 0.55, 0.30} },
|
|
["酋长的祝福"] = { display = "酋长的祝福", priority = 2, color = {0.90, 0.55, 0.30} },
|
|
|
|
["Spirit of Zandalar"] = { display = "赞达拉之魂", priority = 2, color = {0.40, 0.90, 0.45} },
|
|
["赞达拉之魂"] = { display = "赞达拉之魂", priority = 2, color = {0.40, 0.90, 0.45} },
|
|
|
|
["Songflower Serenade"] = { display = "歌唱花小夜曲", priority = 3, color = {0.75, 0.55, 0.90} },
|
|
["歌唱花小夜曲"] = { display = "歌唱花小夜曲", priority = 3, color = {0.75, 0.55, 0.90} },
|
|
|
|
["Mol'dar's Moxie"] = { display = "莫达尔的勇气", priority = 3, color = {0.50, 0.80, 0.55} },
|
|
["莫达尔的勇气"] = { display = "莫达尔的勇气", priority = 3, color = {0.50, 0.80, 0.55} },
|
|
["Fengus' Ferocity"] = { display = "芬古斯的凶猛", priority = 3, color = {0.85, 0.55, 0.40} },
|
|
["芬古斯的凶猛"] = { display = "芬古斯的凶猛", priority = 3, color = {0.85, 0.55, 0.40} },
|
|
["Slip'kik's Savvy"] = { display = "斯里基克的精明", priority = 3, color = {0.55, 0.70, 0.90} },
|
|
["斯里基克的精明"] = { display = "斯里基克的精明", priority = 3, color = {0.55, 0.70, 0.90} },
|
|
|
|
["Darkmoon Faire"] = { display = "暗月马戏团", priority = 4, color = {0.65, 0.50, 0.80} },
|
|
}
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Helpers
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function NextName(prefix)
|
|
if not AFK._nameCounter then AFK._nameCounter = 0 end
|
|
AFK._nameCounter = AFK._nameCounter + 1
|
|
return "NanamiAFK_" .. prefix .. AFK._nameCounter
|
|
end
|
|
|
|
local function GetFont()
|
|
return SFrames:GetFont()
|
|
end
|
|
|
|
local function GetOutline()
|
|
return SFrames.Media.fontOutline or "OUTLINE"
|
|
end
|
|
|
|
local function GetClassColor(class)
|
|
local c = SFrames.Config.colors.class[class]
|
|
if c then return c.r, c.g, c.b end
|
|
return 1, 1, 1
|
|
end
|
|
|
|
local function FormatDuration(seconds)
|
|
local h = math.floor(seconds / 3600)
|
|
local m = math.floor((seconds - h * 3600) / 60)
|
|
local s = math.floor(seconds - h * 3600 - m * 60)
|
|
if h > 0 then
|
|
return string.format("%d:%02d:%02d", h, m, s)
|
|
else
|
|
return string.format("%02d:%02d", m, s)
|
|
end
|
|
end
|
|
|
|
local function GetDurabilityPercent()
|
|
local current, maximum = 0, 0
|
|
for slot = 1, 18 do
|
|
local ok, cur, mx = pcall(function()
|
|
local hasItem = GetInventoryItemLink("player", slot)
|
|
if hasItem then
|
|
local c, m = GetInventoryItemDurability(slot)
|
|
return c, m
|
|
end
|
|
return nil, nil
|
|
end)
|
|
if ok and cur and mx and mx > 0 then
|
|
current = current + cur
|
|
maximum = maximum + mx
|
|
end
|
|
end
|
|
if maximum == 0 then return nil end
|
|
return math.floor((current / maximum) * 100)
|
|
end
|
|
|
|
local function GetPvPRankName()
|
|
local ok, name = pcall(function() return GetPVPRankInfo(UnitPVPRank("player")) end)
|
|
if ok and name and name ~= "" then return name end
|
|
return nil
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- UI Construction
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:Build()
|
|
if self.frame then return end
|
|
|
|
local f = CreateFrame("Frame", "NanamiAFKScreen", WorldFrame)
|
|
f:SetFrameStrata("FULLSCREEN_DIALOG")
|
|
f:SetFrameLevel(100)
|
|
f:SetAllPoints(UIParent)
|
|
f:EnableMouse(true)
|
|
f:EnableKeyboard(true)
|
|
f:SetAlpha(0)
|
|
f:Hide()
|
|
self.frame = f
|
|
|
|
-- Full-screen dark overlay
|
|
f.overlay = f:CreateTexture(nil, "BACKGROUND")
|
|
f.overlay:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
f.overlay:SetAllPoints(f)
|
|
f.overlay:SetVertexColor(T.overlayBg[1], T.overlayBg[2], T.overlayBg[3])
|
|
f.overlay:SetAlpha(T.overlayBg[4])
|
|
|
|
-- Top gradient (subtle vignette)
|
|
f.topGrad = f:CreateTexture(nil, "BACKGROUND", nil, 1)
|
|
f.topGrad:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
f.topGrad:SetPoint("TOPLEFT", f, "TOPLEFT")
|
|
f.topGrad:SetPoint("TOPRIGHT", f, "TOPRIGHT")
|
|
f.topGrad:SetHeight(200)
|
|
f.topGrad:SetGradientAlpha("VERTICAL", 0, 0, 0, 0, 0.02, 0.005, 0.02, 0.6)
|
|
|
|
-- Bottom gradient
|
|
f.botGrad = f:CreateTexture(nil, "BACKGROUND", nil, 1)
|
|
f.botGrad:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
f.botGrad:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT")
|
|
f.botGrad:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT")
|
|
f.botGrad:SetHeight(250)
|
|
f.botGrad:SetGradientAlpha("VERTICAL", 0.03, 0.01, 0.03, 0.45, 0, 0, 0, 0)
|
|
|
|
-- Bottom accent line
|
|
f.accentLine = f:CreateTexture(nil, "ARTWORK")
|
|
f.accentLine:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
f.accentLine:SetHeight(1)
|
|
f.accentLine:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 60)
|
|
f.accentLine:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 60)
|
|
f.accentLine:SetVertexColor(T.accentLine[1], T.accentLine[2], T.accentLine[3])
|
|
f.accentLine:SetAlpha(T.accentLine[4])
|
|
|
|
-- Accent glow (soft line above the accent)
|
|
f.accentGlow = f:CreateTexture(nil, "ARTWORK", nil, -1)
|
|
f.accentGlow:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
f.accentGlow:SetHeight(30)
|
|
f.accentGlow:SetPoint("BOTTOMLEFT", f.accentLine, "TOPLEFT", 0, 0)
|
|
f.accentGlow:SetPoint("BOTTOMRIGHT", f.accentLine, "TOPRIGHT", 0, 0)
|
|
f.accentGlow:SetGradientAlpha("VERTICAL",
|
|
T.accentLine[1], T.accentLine[2], T.accentLine[3], 0.18,
|
|
T.accentLine[1], T.accentLine[2], T.accentLine[3], 0)
|
|
|
|
self:BuildModel(f)
|
|
self:BuildInfoPanel(f)
|
|
self:BuildStatsPanel(f)
|
|
self:BuildSkillsPanel(f)
|
|
self:BuildWorldBuffPanel(f)
|
|
self:BuildClock(f)
|
|
self:BuildParticles(f)
|
|
self:BuildBrand(f)
|
|
|
|
-- Exit hint below accent line
|
|
local hintFs = f:CreateFontString(nil, "OVERLAY")
|
|
hintFs:SetFont(GetFont(), 11, GetOutline())
|
|
hintFs:SetPoint("BOTTOM", f, "BOTTOM", 0, 22)
|
|
hintFs:SetJustifyH("CENTER")
|
|
hintFs:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3])
|
|
hintFs:SetText("- 点击或按任意键退出 -")
|
|
self.hintText = hintFs
|
|
|
|
-- Exit AFK on key press or mouse click
|
|
f:SetScript("OnKeyDown", function()
|
|
if AFK.isShowing and not AFK._exiting then
|
|
AFK:RequestExit()
|
|
end
|
|
end)
|
|
f:SetScript("OnMouseDown", function()
|
|
if AFK.isShowing and not AFK._exiting then
|
|
AFK:RequestExit()
|
|
end
|
|
end)
|
|
|
|
-- Master OnUpdate
|
|
f:SetScript("OnUpdate", function()
|
|
AFK:OnUpdate(arg1)
|
|
end)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- 3D Character Model
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:BuildModel(parent)
|
|
local model = CreateFrame("PlayerModel", NextName("Model"), parent)
|
|
model:SetWidth(420)
|
|
model:SetHeight(520)
|
|
model:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 30, 70)
|
|
model:SetFrameLevel(parent:GetFrameLevel() + 1)
|
|
|
|
-- Soft darkening on the right side so the info panel is readable
|
|
local modelFade = parent:CreateTexture(nil, "ARTWORK", nil, 0)
|
|
modelFade:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
modelFade:SetPoint("TOPRIGHT", parent, "TOPRIGHT")
|
|
modelFade:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT")
|
|
modelFade:SetWidth(420)
|
|
modelFade:SetGradientAlpha("HORIZONTAL",
|
|
0, 0, 0, 0,
|
|
T.overlayBg[1], T.overlayBg[2], T.overlayBg[3], 0.65)
|
|
|
|
self.model = model
|
|
self.modelFacing = 0
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Info Panel (bottom-right area above accent line)
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:BuildInfoPanel(parent)
|
|
local panel = CreateFrame("Frame", nil, parent)
|
|
panel:SetFrameLevel(parent:GetFrameLevel() + 5)
|
|
panel:SetWidth(210)
|
|
panel:SetHeight(150)
|
|
panel:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -40, 75)
|
|
self.infoPanel = panel
|
|
|
|
panel:SetBackdrop({
|
|
bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = false, tileSize = 0, edgeSize = 12,
|
|
insets = { left = 3, right = 3, top = 3, bottom = 3 }
|
|
})
|
|
panel:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4])
|
|
panel:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4])
|
|
|
|
local yOff = -10
|
|
local xPad = 12
|
|
|
|
local classIconFrame = CreateFrame("Frame", nil, panel)
|
|
classIconFrame:SetFrameLevel(panel:GetFrameLevel() + 1)
|
|
classIconFrame:SetWidth(36)
|
|
classIconFrame:SetHeight(36)
|
|
classIconFrame:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad, yOff)
|
|
local classIcon = classIconFrame:CreateTexture(nil, "OVERLAY")
|
|
classIcon:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\UI-Classes-Circles")
|
|
classIcon:SetAllPoints(classIconFrame)
|
|
classIcon:Hide()
|
|
self.classIcon = classIcon
|
|
self.classIconFrame = classIconFrame
|
|
|
|
local nameFs = panel:CreateFontString(nil, "OVERLAY")
|
|
nameFs:SetFont(GetFont(), 16, GetOutline())
|
|
nameFs:SetPoint("TOPLEFT", classIconFrame, "TOPRIGHT", 8, 0)
|
|
nameFs:SetJustifyH("LEFT")
|
|
self.nameText = nameFs
|
|
|
|
local classFs = panel:CreateFontString(nil, "OVERLAY")
|
|
classFs:SetFont(GetFont(), 10, GetOutline())
|
|
classFs:SetPoint("TOPLEFT", nameFs, "BOTTOMLEFT", 0, -2)
|
|
classFs:SetJustifyH("LEFT")
|
|
classFs:SetTextColor(T.valueColor[1], T.valueColor[2], T.valueColor[3])
|
|
self.classText = classFs
|
|
|
|
local sep1 = panel:CreateTexture(nil, "ARTWORK")
|
|
sep1:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
sep1:SetHeight(1)
|
|
sep1:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad, yOff - 44)
|
|
sep1:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -xPad, yOff - 44)
|
|
sep1:SetVertexColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3])
|
|
sep1:SetAlpha(0.35)
|
|
|
|
local rowStart = yOff - 52
|
|
local rowGap = 15
|
|
|
|
local function MakeRow(label, yPos, iconKey)
|
|
if iconKey and SFrames and SFrames.CreateIcon then
|
|
local ico = SFrames:CreateIcon(panel, iconKey, 9)
|
|
ico:SetDrawLayer("OVERLAY")
|
|
ico:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad, yPos)
|
|
ico:SetVertexColor(T.labelColor[1], T.labelColor[2], T.labelColor[3])
|
|
ico:SetAlpha(0.85)
|
|
end
|
|
|
|
local lbl = panel:CreateFontString(nil, "OVERLAY")
|
|
lbl:SetFont(GetFont(), 9, GetOutline())
|
|
lbl:SetPoint("TOPLEFT", panel, "TOPLEFT", iconKey and (xPad + 13) or xPad, yPos)
|
|
lbl:SetJustifyH("LEFT")
|
|
lbl:SetTextColor(T.labelColor[1], T.labelColor[2], T.labelColor[3])
|
|
lbl:SetText(label)
|
|
|
|
local val = panel:CreateFontString(nil, "OVERLAY")
|
|
val:SetFont(GetFont(), 9, GetOutline())
|
|
val:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -xPad, yPos)
|
|
val:SetJustifyH("RIGHT")
|
|
val:SetTextColor(T.valueColor[1], T.valueColor[2], T.valueColor[3])
|
|
return val
|
|
end
|
|
|
|
self.guildText = MakeRow("公会", rowStart, "party")
|
|
self.zoneText = MakeRow("位置", rowStart - rowGap, "worldmap")
|
|
self.durText = MakeRow("耐久度", rowStart - rowGap * 2, "tank")
|
|
self.pvpText = MakeRow("军衔", rowStart - rowGap * 3, "honor")
|
|
self.playtimeText = MakeRow("游戏时长", rowStart - rowGap * 4, "hearthstone")
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Stats Panel (top-right area — character attributes)
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:BuildStatsPanel(parent)
|
|
local panel = CreateFrame("Frame", nil, parent)
|
|
panel:SetFrameLevel(parent:GetFrameLevel() + 5)
|
|
panel:SetWidth(210)
|
|
panel:SetHeight(230)
|
|
panel:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -40, -80)
|
|
self.statsPanel = panel
|
|
|
|
panel:SetBackdrop({
|
|
bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = false, tileSize = 0, edgeSize = 12,
|
|
insets = { left = 3, right = 3, top = 3, bottom = 3 }
|
|
})
|
|
panel:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], 0.72)
|
|
panel:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.45)
|
|
|
|
local statsIco = SFrames:CreateIcon(panel, "charsheet", 12)
|
|
statsIco:SetDrawLayer("OVERLAY")
|
|
statsIco:SetPoint("TOP", panel, "TOP", -38, -8)
|
|
statsIco:SetVertexColor(T.titleColor[1], T.titleColor[2], T.titleColor[3])
|
|
|
|
local titleFs = panel:CreateFontString(nil, "OVERLAY")
|
|
titleFs:SetFont(GetFont(), 11, GetOutline())
|
|
titleFs:SetPoint("LEFT", statsIco, "RIGHT", 4, 0)
|
|
titleFs:SetJustifyH("CENTER")
|
|
titleFs:SetTextColor(T.titleColor[1], T.titleColor[2], T.titleColor[3])
|
|
titleFs:SetText("- 角色属性 -")
|
|
|
|
local sep = panel:CreateTexture(nil, "ARTWORK")
|
|
sep:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
sep:SetHeight(1)
|
|
sep:SetPoint("TOPLEFT", panel, "TOPLEFT", 12, -22)
|
|
sep:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -12, -22)
|
|
sep:SetVertexColor(T.accentLine[1], T.accentLine[2], T.accentLine[3])
|
|
sep:SetAlpha(0.3)
|
|
|
|
local xPad = 14
|
|
local yStart = -30
|
|
local rowGap = 15
|
|
self._statRows = {}
|
|
|
|
local statDefs = {
|
|
{ key = "hp", label = "生命值" },
|
|
{ key = "mp", label = "法力值" },
|
|
{ key = "armor", label = "护甲" },
|
|
{ divider = true },
|
|
{ key = "str", label = "力量" },
|
|
{ key = "agi", label = "敏捷" },
|
|
{ key = "sta", label = "耐力" },
|
|
{ key = "int", label = "智力" },
|
|
{ key = "spi", label = "精神" },
|
|
{ divider = true },
|
|
{ key = "melee", label = "攻击强度" },
|
|
{ key = "spell", label = "法术强度" },
|
|
{ key = "crit", label = "暴击率" },
|
|
{ key = "dodge", label = "躲闪" },
|
|
}
|
|
|
|
local curY = yStart
|
|
for _, def in ipairs(statDefs) do
|
|
if def.divider then
|
|
local div = panel:CreateTexture(nil, "ARTWORK")
|
|
div:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
div:SetHeight(1)
|
|
div:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad + 4, curY - 2)
|
|
div:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -xPad - 4, curY - 2)
|
|
div:SetVertexColor(0.45, 0.28, 0.40)
|
|
div:SetAlpha(0.25)
|
|
curY = curY - 8
|
|
else
|
|
local lbl = panel:CreateFontString(nil, "OVERLAY")
|
|
lbl:SetFont(GetFont(), 9, GetOutline())
|
|
lbl:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad, curY)
|
|
lbl:SetJustifyH("LEFT")
|
|
lbl:SetTextColor(0.58, 0.48, 0.55)
|
|
lbl:SetText(def.label)
|
|
|
|
local val = panel:CreateFontString(nil, "OVERLAY")
|
|
val:SetFont(GetFont(), 10, GetOutline())
|
|
val:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -xPad, curY)
|
|
val:SetJustifyH("RIGHT")
|
|
val:SetTextColor(0.92, 0.84, 0.90)
|
|
|
|
self._statRows[def.key] = val
|
|
curY = curY - rowGap
|
|
end
|
|
end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Skills Panel (right side, below stats — professions & weapon skills)
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:BuildSkillsPanel(parent)
|
|
local panel = CreateFrame("Frame", nil, parent)
|
|
panel:SetFrameLevel(parent:GetFrameLevel() + 5)
|
|
panel:SetWidth(210)
|
|
panel:SetHeight(200)
|
|
panel:SetPoint("TOPRIGHT", self.statsPanel, "BOTTOMRIGHT", 0, -6)
|
|
self.skillsPanel = panel
|
|
|
|
panel:SetBackdrop({
|
|
bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = false, tileSize = 0, edgeSize = 12,
|
|
insets = { left = 3, right = 3, top = 3, bottom = 3 }
|
|
})
|
|
panel:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], 0.72)
|
|
panel:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.45)
|
|
|
|
local skillsIco = SFrames:CreateIcon(panel, "profession", 12)
|
|
skillsIco:SetDrawLayer("OVERLAY")
|
|
skillsIco:SetPoint("TOP", panel, "TOP", -38, -8)
|
|
skillsIco:SetVertexColor(T.titleColor[1], T.titleColor[2], T.titleColor[3])
|
|
|
|
local titleFs = panel:CreateFontString(nil, "OVERLAY")
|
|
titleFs:SetFont(GetFont(), 11, GetOutline())
|
|
titleFs:SetPoint("LEFT", skillsIco, "RIGHT", 4, 0)
|
|
titleFs:SetJustifyH("CENTER")
|
|
titleFs:SetTextColor(T.titleColor[1], T.titleColor[2], T.titleColor[3])
|
|
titleFs:SetText("- 技能总览 -")
|
|
|
|
local sep = panel:CreateTexture(nil, "ARTWORK")
|
|
sep:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
sep:SetHeight(1)
|
|
sep:SetPoint("TOPLEFT", panel, "TOPLEFT", 12, -22)
|
|
sep:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -12, -22)
|
|
sep:SetVertexColor(T.accentLine[1], T.accentLine[2], T.accentLine[3])
|
|
sep:SetAlpha(0.3)
|
|
|
|
self._skillBars = {}
|
|
end
|
|
|
|
function AFK:CreateSkillBar(parent, yOffset, name, rank, maxRank, barColor)
|
|
local xPad = 14
|
|
local barWidth = 182
|
|
local barHeight = 8
|
|
|
|
local lbl = parent:CreateFontString(nil, "OVERLAY")
|
|
lbl:SetFont(GetFont(), 8, GetOutline())
|
|
lbl:SetPoint("TOPLEFT", parent, "TOPLEFT", xPad, yOffset)
|
|
lbl:SetJustifyH("LEFT")
|
|
lbl:SetTextColor(0.62, 0.52, 0.58)
|
|
lbl:SetText(name)
|
|
|
|
local valFs = parent:CreateFontString(nil, "OVERLAY")
|
|
valFs:SetFont(GetFont(), 8, GetOutline())
|
|
valFs:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -xPad, yOffset)
|
|
valFs:SetJustifyH("RIGHT")
|
|
valFs:SetTextColor(0.82, 0.74, 0.80)
|
|
if maxRank and maxRank > 0 then
|
|
valFs:SetText(rank .. " / " .. maxRank)
|
|
else
|
|
valFs:SetText(rank)
|
|
end
|
|
|
|
local bgBar = parent:CreateTexture(nil, "ARTWORK")
|
|
bgBar:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
bgBar:SetWidth(barWidth)
|
|
bgBar:SetHeight(barHeight)
|
|
bgBar:SetPoint("TOPLEFT", parent, "TOPLEFT", xPad, yOffset - 10)
|
|
bgBar:SetVertexColor(T.panelBg[1], T.panelBg[2], T.panelBg[3])
|
|
bgBar:SetAlpha(0.85)
|
|
|
|
-- Bar fill
|
|
local fillBar = parent:CreateTexture(nil, "OVERLAY")
|
|
fillBar:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
fillBar:SetHeight(barHeight)
|
|
fillBar:SetPoint("TOPLEFT", bgBar, "TOPLEFT", 0, 0)
|
|
|
|
local pct = 0
|
|
if maxRank and maxRank > 0 then
|
|
pct = rank / maxRank
|
|
elseif rank and rank > 0 then
|
|
pct = 1
|
|
end
|
|
local fillWidth = math.max(1, barWidth * pct)
|
|
fillBar:SetWidth(fillWidth)
|
|
|
|
local cr, cg, cb = barColor[1], barColor[2], barColor[3]
|
|
fillBar:SetVertexColor(cr, cg, cb, 0.75)
|
|
|
|
-- Subtle highlight on bar top
|
|
local shine = parent:CreateTexture(nil, "OVERLAY", nil, 1)
|
|
shine:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
shine:SetHeight(math.floor(barHeight / 2))
|
|
shine:SetPoint("TOPLEFT", fillBar, "TOPLEFT", 0, 0)
|
|
shine:SetPoint("TOPRIGHT", fillBar, "TOPRIGHT", 0, 0)
|
|
shine:SetVertexColor(1, 1, 1, 0.08)
|
|
|
|
return { lbl = lbl, val = valFs, bg = bgBar, fill = fillBar, shine = shine }
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- World Buff Panel (center — prominent buff reminders)
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:BuildWorldBuffPanel(parent)
|
|
local panel = CreateFrame("Frame", nil, parent)
|
|
panel:SetFrameLevel(parent:GetFrameLevel() + 7)
|
|
panel:SetWidth(320)
|
|
panel:SetHeight(60)
|
|
panel:SetPoint("CENTER", parent, "CENTER", -50, 30)
|
|
self.wbPanel = panel
|
|
panel:Hide()
|
|
|
|
panel:SetBackdrop({
|
|
bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = false, tileSize = 0, edgeSize = 12,
|
|
insets = { left = 3, right = 3, top = 3, bottom = 3 }
|
|
})
|
|
panel:SetBackdropColor(0.06, 0.04, 0.01, 0.82)
|
|
panel:SetBackdropBorderColor(T.wbBorder[1], T.wbBorder[2], T.wbBorder[3], 0.55)
|
|
|
|
local glow = CreateFrame("Frame", nil, parent)
|
|
glow:SetFrameLevel(parent:GetFrameLevel() + 6)
|
|
glow:SetWidth(328)
|
|
glow:SetHeight(68)
|
|
glow:SetPoint("CENTER", panel, "CENTER", 0, 0)
|
|
glow:SetBackdrop({
|
|
bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = false, tileSize = 0, edgeSize = 14,
|
|
insets = { left = 3, right = 3, top = 3, bottom = 3 }
|
|
})
|
|
glow:SetBackdropColor(0, 0, 0, 0)
|
|
glow:SetBackdropBorderColor(1, 0.82, 0.30, 0.20)
|
|
glow:Hide()
|
|
self._wbGlow = glow
|
|
|
|
local titleFs = panel:CreateFontString(nil, "OVERLAY")
|
|
titleFs:SetFont(GetFont(), 12, GetOutline())
|
|
titleFs:SetPoint("TOP", panel, "TOP", 0, -10)
|
|
titleFs:SetJustifyH("CENTER")
|
|
titleFs:SetTextColor(T.wbGold[1], T.wbGold[2], T.wbGold[3])
|
|
titleFs:SetText("⚔ 世界增益 ⚔")
|
|
self.wbTitle = titleFs
|
|
|
|
local sep = panel:CreateTexture(nil, "ARTWORK")
|
|
sep:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
sep:SetHeight(1)
|
|
sep:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, -26)
|
|
sep:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -14, -26)
|
|
sep:SetVertexColor(1, 0.78, 0.30, 0.35)
|
|
|
|
self._wbRows = {}
|
|
self._wbPulse = 0
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Clock & AFK Timer (top center)
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:BuildClock(parent)
|
|
local clockFrame = CreateFrame("Frame", nil, parent)
|
|
clockFrame:SetFrameLevel(parent:GetFrameLevel() + 5)
|
|
clockFrame:SetWidth(400)
|
|
clockFrame:SetHeight(150)
|
|
clockFrame:SetPoint("TOP", parent, "TOP", 0, -30)
|
|
self.clockFrame = clockFrame
|
|
|
|
-- Main clock: local time (large)
|
|
local timeFs = clockFrame:CreateFontString(nil, "OVERLAY")
|
|
timeFs:SetFont(GetFont(), 72, GetOutline())
|
|
timeFs:SetPoint("TOP", clockFrame, "TOP", 0, 0)
|
|
timeFs:SetJustifyH("CENTER")
|
|
timeFs:SetTextColor(T.clockColor[1], T.clockColor[2], T.clockColor[3])
|
|
self.clockText = timeFs
|
|
|
|
-- Sub clock: server time (below main)
|
|
local serverFs = clockFrame:CreateFontString(nil, "OVERLAY")
|
|
serverFs:SetFont(GetFont(), 18, GetOutline())
|
|
serverFs:SetPoint("TOP", timeFs, "BOTTOM", 0, -4)
|
|
serverFs:SetJustifyH("CENTER")
|
|
serverFs:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3])
|
|
self.serverClockText = serverFs
|
|
|
|
-- AFK timer
|
|
local timerFs = clockFrame:CreateFontString(nil, "OVERLAY")
|
|
timerFs:SetFont(GetFont(), 18, GetOutline())
|
|
timerFs:SetPoint("TOP", serverFs, "BOTTOM", 0, -8)
|
|
timerFs:SetJustifyH("CENTER")
|
|
timerFs:SetTextColor(T.timerColor[1], T.timerColor[2], T.timerColor[3])
|
|
self.timerText = timerFs
|
|
|
|
-- Subtle decorative dots flanking the main clock
|
|
for _, side in ipairs({ -1, 1 }) do
|
|
local dot = clockFrame:CreateTexture(nil, "ARTWORK")
|
|
dot:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
dot:SetWidth(4)
|
|
dot:SetHeight(4)
|
|
dot:SetPoint("CENTER", timeFs, side > 0 and "RIGHT" or "LEFT", side * 18, 0)
|
|
dot:SetVertexColor(T.accentLine[1], T.accentLine[2], T.accentLine[3])
|
|
dot:SetAlpha(0.5)
|
|
end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Floating Particles (sakura / firefly effect)
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:BuildParticles(parent)
|
|
self.particles = {}
|
|
for i = 1, PARTICLE_COUNT do
|
|
local p = parent:CreateTexture(nil, "ARTWORK", nil, 2)
|
|
p:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
local size = math.random(2, 6)
|
|
p:SetWidth(size)
|
|
p:SetHeight(size)
|
|
p:SetVertexColor(
|
|
T.particleColor[1] + math.random() * 0.1 - 0.05,
|
|
T.particleColor[2] + math.random() * 0.2 - 0.1,
|
|
T.particleColor[3] + math.random() * 0.1 - 0.05
|
|
)
|
|
p:SetAlpha(0)
|
|
|
|
p._baseAlpha = 0.15 + math.random() * 0.25
|
|
p._speed = 8 + math.random() * 20
|
|
p._drift = (math.random() - 0.5) * 40
|
|
p._phase = math.random() * 6.28
|
|
p._pulseSpeed = 1.5 + math.random() * 2
|
|
p._x = math.random() * 1024
|
|
p._y = math.random() * 768
|
|
|
|
p:SetPoint("CENTER", parent, "BOTTOMLEFT", p._x, p._y)
|
|
table.insert(self.particles, p)
|
|
end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Brand / Logo (bottom-left)
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:BuildBrand(parent)
|
|
local brandIco = SFrames:CreateIcon(parent, "logo", 28)
|
|
brandIco:SetDrawLayer("OVERLAY")
|
|
brandIco:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 20, 30)
|
|
brandIco:SetVertexColor(T.brandColor[1], T.brandColor[2], T.brandColor[3])
|
|
brandIco:SetAlpha(0.85)
|
|
self.brandIcon = brandIco
|
|
|
|
local brandFs = parent:CreateFontString(nil, "OVERLAY")
|
|
brandFs:SetFont(GetFont(), 16, GetOutline())
|
|
brandFs:SetPoint("LEFT", brandIco, "RIGHT", 6, 2)
|
|
brandFs:SetJustifyH("LEFT")
|
|
brandFs:SetTextColor(T.brandColor[1], T.brandColor[2], T.brandColor[3])
|
|
brandFs:SetAlpha(0.85)
|
|
brandFs:SetText("Nanami-UI v1.0.0")
|
|
self.brandText = brandFs
|
|
|
|
local catFs = parent:CreateFontString(nil, "OVERLAY")
|
|
catFs:SetFont(GetFont(), 12, GetOutline())
|
|
catFs:SetPoint("TOPLEFT", brandIco, "BOTTOMLEFT", 0, -3)
|
|
catFs:SetJustifyH("LEFT")
|
|
catFs:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3])
|
|
catFs:SetAlpha(0.7)
|
|
catFs:SetText("=^_^= Meow~")
|
|
self.catText = catFs
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Data Refresh
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:RefreshInfo()
|
|
local name = UnitName("player") or "Unknown"
|
|
local level = UnitLevel("player") or 0
|
|
local _, classEn = UnitClass("player")
|
|
classEn = classEn or "WARRIOR"
|
|
|
|
local cr, cg, cb = GetClassColor(classEn)
|
|
self.nameText:SetText(name)
|
|
self.nameText:SetTextColor(cr, cg, cb)
|
|
|
|
local className = CLASS_NAMES_ZH[classEn] or UnitClass("player") or classEn
|
|
self.classText:SetText("Lv." .. level .. " " .. className)
|
|
|
|
-- Class icon
|
|
local coords = SFrames.CLASS_ICON_TCOORDS and SFrames.CLASS_ICON_TCOORDS[classEn]
|
|
if coords then
|
|
self.classIcon:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
|
|
self.classIcon:Show()
|
|
self.classIconFrame:Show()
|
|
else
|
|
self.classIcon:Hide()
|
|
end
|
|
|
|
-- Guild
|
|
local guildName, guildRank = GetGuildInfo("player")
|
|
if guildName then
|
|
self.guildText:SetText("<" .. guildName .. "> " .. (guildRank or ""))
|
|
else
|
|
self.guildText:SetText("-")
|
|
self.guildText:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3])
|
|
end
|
|
|
|
-- Zone
|
|
local zone = GetZoneText() or ""
|
|
local subZone = GetSubZoneText() or ""
|
|
if subZone ~= "" and subZone ~= zone then
|
|
self.zoneText:SetText(zone .. " - " .. subZone)
|
|
else
|
|
self.zoneText:SetText(zone)
|
|
end
|
|
|
|
-- Durability
|
|
local dur = GetDurabilityPercent()
|
|
if dur then
|
|
local dr, dg, db = 0.4, 1, 0.4
|
|
if dur < 25 then
|
|
dr, dg, db = 1, 0.2, 0.2
|
|
elseif dur < 50 then
|
|
dr, dg, db = 1, 0.7, 0.2
|
|
end
|
|
self.durText:SetText(dur .. "%")
|
|
self.durText:SetTextColor(dr, dg, db)
|
|
else
|
|
self.durText:SetText("-")
|
|
self.durText:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3])
|
|
end
|
|
|
|
-- PvP Rank
|
|
local pvpRank = GetPvPRankName()
|
|
if pvpRank then
|
|
self.pvpText:SetText(pvpRank)
|
|
else
|
|
self.pvpText:SetText("-")
|
|
self.pvpText:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3])
|
|
end
|
|
|
|
-- Playtime (request it; the event callback fills it in)
|
|
self.playtimeText:SetText("...")
|
|
self.playtimeText:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3])
|
|
self._waitingPlaytime = true
|
|
RequestTimePlayed()
|
|
|
|
-- Character stats, skills & world buffs
|
|
self:RefreshStats()
|
|
self:RefreshSkills()
|
|
self:RefreshWorldBuffs()
|
|
end
|
|
|
|
function AFK:RefreshStats()
|
|
if not self._statRows then return end
|
|
local R = self._statRows
|
|
|
|
-- HP / MP
|
|
local hp = UnitHealthMax("player") or 0
|
|
local mp = UnitManaMax("player") or 0
|
|
R.hp:SetText(hp)
|
|
R.mp:SetText(mp)
|
|
|
|
local _, pClass = UnitClass("player")
|
|
if pClass == "WARRIOR" or pClass == "ROGUE" then
|
|
R.mp:SetTextColor(0.50, 0.42, 0.48)
|
|
end
|
|
|
|
-- Armor
|
|
local base, effectiveArmor = UnitArmor("player")
|
|
R.armor:SetText(effectiveArmor or base or 0)
|
|
|
|
-- Primary stats
|
|
local function GetStat(id)
|
|
local base, stat, posBuff, negBuff = UnitStat("player", id)
|
|
return stat or base or 0
|
|
end
|
|
R.str:SetText(GetStat(1))
|
|
R.agi:SetText(GetStat(2))
|
|
R.sta:SetText(GetStat(3))
|
|
R.int:SetText(GetStat(4))
|
|
R.spi:SetText(GetStat(5))
|
|
|
|
-- Melee attack power
|
|
local mBase, mPos, mNeg = UnitAttackPower("player")
|
|
local ap = (mBase or 0) + (mPos or 0) + (mNeg or 0)
|
|
R.melee:SetText(ap)
|
|
|
|
-- Spell power (approximate from bonus healing / damage stats if available)
|
|
local sp = 0
|
|
if GetSpellBonusDamage then
|
|
for school = 2, 7 do
|
|
local v = GetSpellBonusDamage(school)
|
|
if v and v > sp then sp = v end
|
|
end
|
|
end
|
|
R.spell:SetText(sp)
|
|
|
|
-- Crit chance
|
|
local crit = 0
|
|
if GetCritChance then crit = GetCritChance() or 0
|
|
elseif GetMeleeCritChance then crit = GetMeleeCritChance() or 0
|
|
end
|
|
R.crit:SetText(string.format("%.1f%%", crit))
|
|
|
|
-- Dodge
|
|
local dodge = GetDodgeChance and GetDodgeChance() or 0
|
|
R.dodge:SetText(string.format("%.1f%%", dodge))
|
|
end
|
|
|
|
function AFK:RefreshSkills()
|
|
if not self.skillsPanel then return end
|
|
|
|
-- Clean up old bars
|
|
if self._skillBars then
|
|
for _, bar in ipairs(self._skillBars) do
|
|
if bar.lbl then bar.lbl:Hide() end
|
|
if bar.val then bar.val:Hide() end
|
|
if bar.bg then bar.bg:Hide() end
|
|
if bar.fill then bar.fill:Hide() end
|
|
if bar.shine then bar.shine:Hide() end
|
|
end
|
|
end
|
|
self._skillBars = {}
|
|
|
|
-- Also clean up section labels
|
|
if self._skillLabels then
|
|
for _, fs in ipairs(self._skillLabels) do fs:Hide() end
|
|
end
|
|
self._skillLabels = {}
|
|
|
|
local tradeSkills = {}
|
|
local secondarySkills = {}
|
|
local weapons = {}
|
|
local numSkills = GetNumSkillLines and GetNumSkillLines() or 0
|
|
local currentHeader = ""
|
|
|
|
for i = 1, numSkills do
|
|
local name, header, isExpanded, skillRank, numTemp, skillMod, skillMax, isAbandonable = GetSkillLineInfo(i)
|
|
if header then
|
|
currentHeader = name or ""
|
|
elseif name then
|
|
local isWeapon = (currentHeader == "Weapon Skills" or currentHeader == "武器技能"
|
|
or currentHeader == "Weapons" or currentHeader == "Combat")
|
|
local isTrade = (currentHeader == "Trade Skills" or currentHeader == "专业技能"
|
|
or currentHeader == "Professions")
|
|
local isSecondary = (currentHeader == "Secondary Skills" or currentHeader == "辅助技能")
|
|
|
|
if isWeapon and skillMax and skillMax > 0 then
|
|
table.insert(weapons, { name = name, rank = skillRank or 0, max = skillMax })
|
|
elseif isTrade and skillMax and skillMax > 0 then
|
|
table.insert(tradeSkills, { name = name, rank = skillRank or 0, max = skillMax })
|
|
elseif isSecondary and skillMax and skillMax > 0 then
|
|
table.insert(secondarySkills, { name = name, rank = skillRank or 0, max = skillMax })
|
|
end
|
|
end
|
|
end
|
|
|
|
local professions = {}
|
|
local maxProf = 3
|
|
for _, p in ipairs(tradeSkills) do
|
|
if table.getn(professions) < maxProf then table.insert(professions, p) end
|
|
end
|
|
for _, p in ipairs(secondarySkills) do
|
|
if table.getn(professions) < maxProf then table.insert(professions, p) end
|
|
end
|
|
|
|
local panel = self.skillsPanel
|
|
local yOff = -28
|
|
local barSpacing = 22
|
|
|
|
local profColor = { 0.45, 0.75, 0.90 }
|
|
local weapColor = { 0.85, 0.60, 0.45 }
|
|
|
|
local function AddSectionLabel(text, y)
|
|
local fs = panel:CreateFontString(nil, "OVERLAY")
|
|
fs:SetFont(GetFont(), 9, GetOutline())
|
|
fs:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, y)
|
|
fs:SetJustifyH("LEFT")
|
|
fs:SetTextColor(T.titleColor[1], T.titleColor[2], T.titleColor[3])
|
|
fs:SetAlpha(0.85)
|
|
fs:SetText(text)
|
|
table.insert(self._skillLabels, fs)
|
|
end
|
|
|
|
if table.getn(professions) > 0 then
|
|
AddSectionLabel("专业技能", yOff)
|
|
yOff = yOff - 14
|
|
for _, p in ipairs(professions) do
|
|
local bar = self:CreateSkillBar(panel, yOff, p.name, p.rank, p.max, profColor)
|
|
table.insert(self._skillBars, bar)
|
|
yOff = yOff - barSpacing
|
|
end
|
|
yOff = yOff - 4
|
|
end
|
|
|
|
-- Sort weapons by rank descending; show top 4
|
|
table.sort(weapons, function(a, b) return a.rank > b.rank end)
|
|
local maxWeapons = 4
|
|
if table.getn(weapons) > maxWeapons then
|
|
local trimmed = {}
|
|
for i = 1, maxWeapons do table.insert(trimmed, weapons[i]) end
|
|
weapons = trimmed
|
|
end
|
|
|
|
if table.getn(weapons) > 0 then
|
|
AddSectionLabel("武器熟练", yOff)
|
|
yOff = yOff - 14
|
|
for _, w in ipairs(weapons) do
|
|
local bar = self:CreateSkillBar(panel, yOff, w.name, w.rank, w.max, weapColor)
|
|
table.insert(self._skillBars, bar)
|
|
yOff = yOff - barSpacing
|
|
end
|
|
end
|
|
|
|
local totalHeight = math.abs(yOff) + 10
|
|
if totalHeight < 50 then totalHeight = 50 end
|
|
panel:SetHeight(totalHeight)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- World Buff Scanning & Refresh
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Texture-based fallback for world buff detection (server-independent)
|
|
local WORLD_BUFF_TEXTURES = {
|
|
["Interface\\Icons\\Spell_Holy_GreaterHeal"] = { display = "屠龙者的咆哮 (龙头)", priority = 1, color = {1, 0.85, 0.30} },
|
|
["Interface\\Icons\\INV_Misc_Head_Dragon_01"] = { display = "屠龙者的咆哮 (龙头)", priority = 1, color = {1, 0.85, 0.30} },
|
|
["Interface\\Icons\\Spell_Arcane_TeleportOrgrimmar"] = { display = "酋长的祝福", priority = 2, color = {0.90, 0.55, 0.30} },
|
|
["Interface\\Icons\\Ability_Creature_Poison_05"] = { display = "赞达拉之魂", priority = 2, color = {0.40, 0.90, 0.45} },
|
|
["Interface\\Icons\\Spell_Holy_MindSoothe"] = { display = "歌唱花小夜曲", priority = 3, color = {0.75, 0.55, 0.90} },
|
|
}
|
|
|
|
function AFK:ScanWorldBuffs()
|
|
local found = {}
|
|
local matched = {}
|
|
|
|
if not self._buffTip then
|
|
self._buffTip = CreateFrame("GameTooltip", "NanamiAFKBuffTip", WorldFrame, "GameTooltipTemplate")
|
|
end
|
|
|
|
-- Build texture → remaining time mapping
|
|
local texTime = {}
|
|
if GetPlayerBuff then
|
|
for slot = 0, 31 do
|
|
local bi = GetPlayerBuff(slot, "HELPFUL")
|
|
if bi and bi >= 0 then
|
|
local tex = GetPlayerBuffTexture and GetPlayerBuffTexture(bi)
|
|
local tl = GetPlayerBuffTimeLeft and GetPlayerBuffTimeLeft(bi)
|
|
if tex then texTime[tex] = tl or 0 end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Debug: print all buffs once per AFK session to help identify names
|
|
local debugOnce = not self._wbDebugDone
|
|
if debugOnce then self._wbDebugDone = true end
|
|
|
|
for i = 1, 32 do
|
|
local tex = UnitBuff("player", i)
|
|
if not tex then break end
|
|
|
|
self._buffTip:SetOwner(WorldFrame, "ANCHOR_NONE")
|
|
self._buffTip:SetUnitBuff("player", i)
|
|
local bName = NanamiAFKBuffTipTextLeft1 and NanamiAFKBuffTipTextLeft1:GetText()
|
|
self._buffTip:Hide()
|
|
|
|
if debugOnce and bName then
|
|
local tl = texTime[tex] or 0
|
|
if tl > 300 then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff88aacc[NanamiAFK Debug] buff: \"" .. bName .. "\" tex: " .. tex .. " time: " .. math.floor(tl) .. "s|r")
|
|
end
|
|
end
|
|
|
|
local def = nil
|
|
-- 1) Exact name match
|
|
if bName then
|
|
def = WORLD_BUFF_DEFS[bName]
|
|
end
|
|
-- 2) Partial name match (substring)
|
|
if not def and bName then
|
|
for pattern, d in pairs(WORLD_BUFF_DEFS) do
|
|
if string.find(bName, pattern, 1, true) then
|
|
def = d
|
|
break
|
|
end
|
|
end
|
|
end
|
|
-- 3) Texture-based fallback
|
|
if not def and tex then
|
|
def = WORLD_BUFF_TEXTURES[tex]
|
|
end
|
|
|
|
if def and not matched[def.display] then
|
|
matched[def.display] = true
|
|
table.insert(found, {
|
|
name = def.display,
|
|
timeLeft = texTime[tex] or 0,
|
|
priority = def.priority,
|
|
color = def.color,
|
|
texture = tex,
|
|
})
|
|
end
|
|
end
|
|
|
|
table.sort(found, function(a, b) return a.priority < b.priority end)
|
|
return found
|
|
end
|
|
|
|
function AFK:RefreshWorldBuffs()
|
|
if not self.wbPanel then return end
|
|
|
|
for _, row in ipairs(self._wbRows or {}) do
|
|
if row.icon then row.icon:Hide() end
|
|
if row.nameFs then row.nameFs:Hide() end
|
|
if row.timeFs then row.timeFs:Hide() end
|
|
if row.highlight then row.highlight:Hide() end
|
|
end
|
|
self._wbRows = {}
|
|
|
|
local buffs = self:ScanWorldBuffs()
|
|
|
|
if table.getn(buffs) == 0 then
|
|
self.wbPanel:Hide()
|
|
if self._wbGlow then self._wbGlow:Hide() end
|
|
return
|
|
end
|
|
|
|
local panel = self.wbPanel
|
|
local yOff = -34
|
|
local rowHeight = 28
|
|
local xPad = 16
|
|
|
|
for _, buff in ipairs(buffs) do
|
|
local isPrimary = (buff.priority == 1)
|
|
local nameSize = isPrimary and 13 or 11
|
|
local timeSize = isPrimary and 15 or 12
|
|
|
|
local icon = panel:CreateFontString(nil, "OVERLAY")
|
|
icon:SetFont(GetFont(), isPrimary and 14 or 11, GetOutline())
|
|
icon:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad, yOff)
|
|
icon:SetTextColor(buff.color[1], buff.color[2], buff.color[3])
|
|
icon:SetText(isPrimary and "★" or "◆")
|
|
|
|
local nameFs = panel:CreateFontString(nil, "OVERLAY")
|
|
nameFs:SetFont(GetFont(), nameSize, GetOutline())
|
|
nameFs:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad + 18, yOff)
|
|
nameFs:SetJustifyH("LEFT")
|
|
nameFs:SetTextColor(buff.color[1], buff.color[2], buff.color[3])
|
|
nameFs:SetText(buff.name)
|
|
|
|
local timeFs = panel:CreateFontString(nil, "OVERLAY")
|
|
timeFs:SetFont(GetFont(), timeSize, GetOutline())
|
|
timeFs:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -xPad, yOff)
|
|
timeFs:SetJustifyH("RIGHT")
|
|
self:_SetBuffTimeColor(timeFs, buff.timeLeft)
|
|
|
|
local highlight = nil
|
|
if isPrimary then
|
|
highlight = panel:CreateTexture(nil, "ARTWORK")
|
|
highlight:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
highlight:SetHeight(rowHeight - 4)
|
|
highlight:SetPoint("TOPLEFT", panel, "TOPLEFT", 6, yOff + 3)
|
|
highlight:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -6, yOff + 3)
|
|
highlight:SetVertexColor(1, 0.82, 0.25, 0.06)
|
|
end
|
|
|
|
table.insert(self._wbRows, {
|
|
icon = icon, nameFs = nameFs, timeFs = timeFs,
|
|
highlight = highlight, buff = buff,
|
|
})
|
|
yOff = yOff - rowHeight
|
|
end
|
|
|
|
local totalH = math.abs(yOff) + 10
|
|
if totalH < 60 then totalH = 60 end
|
|
panel:SetHeight(totalH)
|
|
panel:Show()
|
|
|
|
if self._wbGlow then
|
|
self._wbGlow:SetHeight(totalH + 8)
|
|
self._wbGlow:Show()
|
|
end
|
|
end
|
|
|
|
function AFK:_SetBuffTimeColor(fs, timeLeft)
|
|
if timeLeft and timeLeft > 0 then
|
|
fs:SetText(FormatDuration(timeLeft))
|
|
if timeLeft < 600 then
|
|
fs:SetTextColor(1, 0.35, 0.30)
|
|
elseif timeLeft < 1800 then
|
|
fs:SetTextColor(1, 0.72, 0.30)
|
|
else
|
|
fs:SetTextColor(0.92, 0.88, 0.72)
|
|
end
|
|
else
|
|
fs:SetText("--:--")
|
|
fs:SetTextColor(0.50, 0.46, 0.38)
|
|
end
|
|
end
|
|
|
|
function AFK:UpdateWorldBuffTimers()
|
|
if not self._wbRows or table.getn(self._wbRows) == 0 then return end
|
|
|
|
local texTime = {}
|
|
if GetPlayerBuff then
|
|
for slot = 0, 31 do
|
|
local bi = GetPlayerBuff(slot, "HELPFUL")
|
|
if bi and bi >= 0 then
|
|
local tex = GetPlayerBuffTexture and GetPlayerBuffTexture(bi)
|
|
local tl = GetPlayerBuffTimeLeft and GetPlayerBuffTimeLeft(bi)
|
|
if tex then texTime[tex] = tl or 0 end
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, row in ipairs(self._wbRows) do
|
|
if row.buff and row.buff.texture then
|
|
local tl = texTime[row.buff.texture]
|
|
if tl and tl > 0 then
|
|
self:_SetBuffTimeColor(row.timeFs, tl)
|
|
if row.nameFs then row.nameFs:SetAlpha(1) end
|
|
if row.icon then row.icon:SetAlpha(1) end
|
|
elseif tl == nil then
|
|
row.timeFs:SetText("|cff665e50已消失|r")
|
|
row.timeFs:SetTextColor(0.42, 0.38, 0.32)
|
|
if row.nameFs then row.nameFs:SetAlpha(0.45) end
|
|
if row.icon then row.icon:SetAlpha(0.45) end
|
|
else
|
|
self:_SetBuffTimeColor(row.timeFs, 0)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- OnUpdate (master tick)
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:OnUpdate(elapsed)
|
|
if not elapsed then return end
|
|
|
|
-- Fade logic
|
|
if self.fadeDirection then
|
|
local alpha = self.frame:GetAlpha()
|
|
alpha = alpha + self.fadeDirection * FADE_SPEED * elapsed
|
|
if self.fadeDirection > 0 and alpha >= 1 then
|
|
alpha = 1
|
|
self.fadeDirection = nil
|
|
elseif self.fadeDirection < 0 and alpha <= 0 then
|
|
alpha = 0
|
|
self.fadeDirection = nil
|
|
self.frame:Hide()
|
|
self.frame:EnableKeyboard(false)
|
|
self.isShowing = false
|
|
self._exiting = false
|
|
UIParent:Show()
|
|
return
|
|
end
|
|
self.frame:SetAlpha(alpha)
|
|
end
|
|
|
|
if not self.isShowing then return end
|
|
|
|
-- Dance initialization: apply SetSequence once after model is loaded
|
|
if self._danceWait then
|
|
self._danceWait = self._danceWait - elapsed
|
|
if self._danceWait <= 0 then
|
|
self._danceWait = nil
|
|
if self.model and self.model.SetSequence then
|
|
self.model:SetSequence(69)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Clock & timer
|
|
self._clockElapsed = (self._clockElapsed or 0) + elapsed
|
|
if self._clockElapsed >= CLOCK_UPDATE_INTERVAL then
|
|
self._clockElapsed = 0
|
|
|
|
-- Main clock: local time
|
|
local localTime = date("%H:%M")
|
|
self.clockText:SetText(localTime)
|
|
|
|
-- Sub clock: server time
|
|
local sHour, sMin = GetGameTime()
|
|
self.serverClockText:SetText("服务器 " .. string.format("%02d:%02d", sHour, sMin))
|
|
|
|
-- AFK duration
|
|
if self.afkStartTime then
|
|
local duration = GetTime() - self.afkStartTime
|
|
self.timerText:SetText("已离开 " .. FormatDuration(duration))
|
|
end
|
|
|
|
-- World buff timers
|
|
self:UpdateWorldBuffTimers()
|
|
end
|
|
|
|
-- Particle animation
|
|
local now = GetTime()
|
|
local sw = self.frame:GetWidth()
|
|
local sh = self.frame:GetHeight()
|
|
if sw < 100 then sw = 1024 end
|
|
if sh < 100 then sh = 768 end
|
|
|
|
for _, p in ipairs(self.particles) do
|
|
p._y = p._y + p._speed * elapsed
|
|
p._x = p._x + p._drift * elapsed
|
|
|
|
if p._y > sh + 20 then
|
|
p._y = -20
|
|
p._x = math.random() * sw
|
|
end
|
|
if p._x < -20 then p._x = sw + 10 end
|
|
if p._x > sw + 20 then p._x = -10 end
|
|
|
|
p:ClearAllPoints()
|
|
p:SetPoint("CENTER", self.frame, "BOTTOMLEFT", p._x, p._y)
|
|
|
|
local pulse = p._baseAlpha * (0.6 + 0.4 * math.sin(now * p._pulseSpeed + p._phase))
|
|
p:SetAlpha(pulse)
|
|
end
|
|
|
|
-- Accent line gentle pulse
|
|
if self.frame.accentLine then
|
|
local glow = 0.7 + 0.3 * math.sin(now * 1.2)
|
|
self.frame.accentLine:SetAlpha(glow * T.accentLine[4])
|
|
end
|
|
|
|
-- World buff panel glow pulse
|
|
if self._wbGlow and self._wbGlow:IsShown() then
|
|
self._wbPulse = (self._wbPulse or 0) + elapsed * 1.8
|
|
local pa = 0.18 + 0.12 * math.sin(self._wbPulse)
|
|
self._wbGlow:SetBackdropBorderColor(1, 0.82, 0.30, pa)
|
|
end
|
|
|
|
-- Hint text gentle pulse
|
|
if self.hintText then
|
|
local hAlpha = 0.4 + 0.25 * math.sin(now * 1.8)
|
|
self.hintText:SetAlpha(hAlpha)
|
|
end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Show / Hide
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:Show()
|
|
if self.isShowing then return end
|
|
if self._exiting then return end
|
|
|
|
-- Check config: is AFK screen enabled?
|
|
if SFramesDB and SFramesDB.afkEnabled == false then return end
|
|
|
|
-- Only trigger in resting areas unless config allows everywhere, or manual toggle
|
|
if not self._manualTrigger then
|
|
local allowOutside = SFramesDB and SFramesDB.afkOutsideRest == true
|
|
if not allowOutside and not IsResting() then return end
|
|
end
|
|
|
|
if not self.frame then
|
|
self:Build()
|
|
end
|
|
|
|
self.isShowing = true
|
|
self._exiting = false
|
|
self._manualTrigger = false
|
|
self._wbDebugDone = false
|
|
self.afkStartTime = GetTime()
|
|
self._clockElapsed = 99
|
|
|
|
self.model:SetUnit("player")
|
|
self.model:SetCamera(1)
|
|
self.model:SetPosition(0, 0, 0)
|
|
self.model:SetFacing(0.4)
|
|
|
|
-- WoW 1.12 AnimationData.dbc: 69 = EmoteDance; apply after model loads
|
|
self._danceWait = 0.3
|
|
|
|
-- Reset particles
|
|
local sw = UIParent:GetWidth() or 1024
|
|
local sh = UIParent:GetHeight() or 768
|
|
for _, p in ipairs(self.particles) do
|
|
p._x = math.random() * sw
|
|
p._y = math.random() * sh
|
|
end
|
|
|
|
self:RefreshInfo()
|
|
|
|
-- Hide the game UI (our frame is parented to WorldFrame, unaffected)
|
|
UIParent:Hide()
|
|
|
|
-- Fade in
|
|
self.frame:SetAlpha(0)
|
|
self.frame:EnableKeyboard(true)
|
|
self.frame:Show()
|
|
self.fadeDirection = 1
|
|
end
|
|
|
|
function AFK:Hide()
|
|
if not self.isShowing then return end
|
|
if self._exiting then return end
|
|
self._exiting = true
|
|
self.fadeDirection = -1
|
|
self.frame:EnableKeyboard(false)
|
|
self._danceWait = nil
|
|
end
|
|
|
|
function AFK:ForceHide()
|
|
if self.frame then
|
|
self.frame:SetAlpha(0)
|
|
self.frame:Hide()
|
|
self.frame:EnableKeyboard(false)
|
|
end
|
|
self.isShowing = false
|
|
self.fadeDirection = nil
|
|
self._exiting = false
|
|
UIParent:Show()
|
|
end
|
|
|
|
function AFK:RequestExit()
|
|
if self._exiting then return end
|
|
self:Hide()
|
|
self._lastActivity = GetTime()
|
|
self._isAFK = false
|
|
end
|
|
|
|
function AFK:Toggle()
|
|
if self.isShowing then
|
|
self:RequestExit()
|
|
else
|
|
self._manualTrigger = true
|
|
self:Show()
|
|
end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Event Handling
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:ResetIdleTimer()
|
|
self._lastActivity = GetTime()
|
|
if self.isShowing and not self._exiting then
|
|
self:RequestExit()
|
|
end
|
|
end
|
|
|
|
function AFK:OnSystemMessage(msg)
|
|
if not msg then return end
|
|
local isNowAFK = false
|
|
if MARKED_AFK_MESSAGE then
|
|
local pattern = string.gsub(MARKED_AFK_MESSAGE, "%%s", ".+")
|
|
if string.find(msg, pattern) then isNowAFK = true end
|
|
end
|
|
if not isNowAFK and string.find(msg, "You are now AFK") then isNowAFK = true end
|
|
if not isNowAFK and string.find(msg, "暂离") then isNowAFK = true end
|
|
|
|
if isNowAFK then
|
|
self._isAFK = true
|
|
end
|
|
|
|
-- Detect leaving AFK: reset flag
|
|
local isNoLongerAFK = false
|
|
if CLEARED_AFK_MESSAGE and string.find(msg, CLEARED_AFK_MESSAGE, 1, true) then
|
|
isNoLongerAFK = true
|
|
end
|
|
if not isNoLongerAFK and string.find(msg, "no longer AFK") then isNoLongerAFK = true end
|
|
if not isNoLongerAFK and string.find(msg, "取消") and string.find(msg, "暂离") then isNoLongerAFK = true end
|
|
if isNoLongerAFK then
|
|
self._isAFK = false
|
|
end
|
|
end
|
|
|
|
function AFK:OnTimePlayed(totalTime, levelTime)
|
|
if not self._waitingPlaytime then return end
|
|
self._waitingPlaytime = false
|
|
|
|
if not self.playtimeText then return end
|
|
if not totalTime or totalTime == 0 then
|
|
self.playtimeText:SetText("-")
|
|
return
|
|
end
|
|
|
|
local days = math.floor(totalTime / 86400)
|
|
local hours = math.floor((totalTime - days * 86400) / 3600)
|
|
local mins = math.floor((totalTime - days * 86400 - hours * 3600) / 60)
|
|
|
|
local text = ""
|
|
if days > 0 then
|
|
text = days .. "天 " .. hours .. "小时"
|
|
elseif hours > 0 then
|
|
text = hours .. "小时 " .. mins .. "分钟"
|
|
else
|
|
text = mins .. "分钟"
|
|
end
|
|
self.playtimeText:SetText(text)
|
|
self.playtimeText:SetTextColor(T.valueColor[1], T.valueColor[2], T.valueColor[3])
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Initialize
|
|
--------------------------------------------------------------------------------
|
|
|
|
function AFK:Initialize()
|
|
self:Build()
|
|
self._isAFK = false
|
|
self._lastActivity = GetTime()
|
|
|
|
local function MarkActive()
|
|
AFK._lastActivity = GetTime()
|
|
end
|
|
|
|
-- Hook action bar usage
|
|
local origUseAction = UseAction
|
|
UseAction = function(a1, a2, a3)
|
|
MarkActive()
|
|
return origUseAction(a1, a2, a3)
|
|
end
|
|
|
|
if CastSpellByName then
|
|
local origCast = CastSpellByName
|
|
CastSpellByName = function(a1, a2)
|
|
MarkActive()
|
|
return origCast(a1, a2)
|
|
end
|
|
end
|
|
|
|
if JumpOrAscendStart then
|
|
local origJump = JumpOrAscendStart
|
|
JumpOrAscendStart = function()
|
|
MarkActive()
|
|
return origJump()
|
|
end
|
|
end
|
|
|
|
-- Events that indicate LOCAL player activity (not other players)
|
|
local activityEvents = {
|
|
"PLAYER_STARTED_MOVING", "PLAYER_STOPPED_MOVING",
|
|
"SPELLCAST_START", "SPELLCAST_STOP",
|
|
"PLAYER_REGEN_DISABLED", "PLAYER_TARGET_CHANGED",
|
|
"LOOT_OPENED", "MERCHANT_SHOW", "BANKFRAME_OPENED",
|
|
"MAIL_SHOW", "QUEST_DETAIL", "GOSSIP_SHOW",
|
|
"TRADE_SHOW", "AUCTION_HOUSE_SHOW",
|
|
}
|
|
for _, ev in ipairs(activityEvents) do
|
|
SFrames:RegisterEvent(ev, function() AFK:ResetIdleTimer() end)
|
|
end
|
|
|
|
-- Watcher frame: tracks cursor movement + checks idle time
|
|
local watcher = CreateFrame("Frame", "NanamiAFKWatcher", UIParent)
|
|
watcher._checkTimer = 0
|
|
watcher._lastCursorX = 0
|
|
watcher._lastCursorY = 0
|
|
watcher:SetScript("OnUpdate", function()
|
|
this._checkTimer = (this._checkTimer or 0) + arg1
|
|
if this._checkTimer < 1 then return end
|
|
this._checkTimer = 0
|
|
|
|
-- Detect mouse cursor movement (catches all mouse activity)
|
|
local cx, cy = GetCursorPosition()
|
|
if cx ~= this._lastCursorX or cy ~= this._lastCursorY then
|
|
this._lastCursorX = cx
|
|
this._lastCursorY = cy
|
|
AFK._lastActivity = GetTime()
|
|
if AFK.isShowing and not AFK._exiting then
|
|
AFK:RequestExit()
|
|
end
|
|
return
|
|
end
|
|
|
|
if AFK.isShowing or AFK._exiting then return end
|
|
if SFramesDB and SFramesDB.afkEnabled == false then return end
|
|
|
|
local delay = (SFramesDB and SFramesDB.afkDelay) or 5
|
|
if delay <= 0 then delay = 0.01 end
|
|
|
|
local idle = GetTime() - (AFK._lastActivity or GetTime())
|
|
if idle >= delay * 60 then
|
|
local allowOutside = SFramesDB and SFramesDB.afkOutsideRest == true
|
|
if allowOutside or IsResting() then
|
|
AFK:Show()
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Server AFK message as secondary instant trigger
|
|
SFrames:RegisterEvent("CHAT_MSG_SYSTEM", function()
|
|
AFK:OnSystemMessage(arg1)
|
|
end)
|
|
|
|
SFrames:RegisterEvent("TIME_PLAYED_MSG", function()
|
|
AFK:OnTimePlayed(arg1, arg2)
|
|
end)
|
|
|
|
SFrames:RegisterEvent("ZONE_CHANGED_NEW_AREA", function()
|
|
if AFK.isShowing and AFK.zoneText then
|
|
local zone = GetZoneText() or ""
|
|
local subZone = GetSubZoneText() or ""
|
|
if subZone ~= "" and subZone ~= zone then
|
|
AFK.zoneText:SetText(zone .. " - " .. subZone)
|
|
else
|
|
AFK.zoneText:SetText(zone)
|
|
end
|
|
end
|
|
end)
|
|
|
|
SFrames:RegisterEvent("ZONE_CHANGED", function()
|
|
if AFK.isShowing and AFK.zoneText then
|
|
local zone = GetZoneText() or ""
|
|
local subZone = GetSubZoneText() or ""
|
|
if subZone ~= "" and subZone ~= zone then
|
|
AFK.zoneText:SetText(zone .. " - " .. subZone)
|
|
else
|
|
AFK.zoneText:SetText(zone)
|
|
end
|
|
end
|
|
end)
|
|
end
|