Files
Nanami-UI/AFKScreen.lua
2026-03-16 13:48:46 +08:00

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