-------------------------------------------------------------------------------- -- 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 existing = getglobal("NanamiAFKScreen") if existing then existing:SetScript("OnUpdate", nil) existing:SetScript("OnKeyDown", nil) existing:SetScript("OnMouseDown", nil) existing:EnableKeyboard(false) existing:EnableMouse(false) existing:Hide() end local existingWatcher = getglobal("NanamiAFKWatcher") if existingWatcher then existingWatcher:SetScript("OnUpdate", nil) existingWatcher:Hide() 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(520) model:SetHeight(640) model:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 10, 65) 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" or currentHeader == "武器") local isTrade = (currentHeader == "Trade Skills" or currentHeader == "专业技能" or currentHeader == "Professions" or currentHeader == "专业") local isSecondary = (currentHeader == "Secondary Skills" or currentHeader == "辅助技能" or currentHeader == "Secondary") 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 = getglobal("NanamiAFKBuffTip") or CreateFrame("GameTooltip", "NanamiAFKBuffTip", UIParent, "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 or not self.frame 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() if not self.particles then return end 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:Cleanup() if self.model and self.model.ClearModel then pcall(function() self.model:ClearModel() end) end if self.frame then self.frame:SetScript("OnUpdate", nil) self.frame:SetScript("OnKeyDown", nil) self.frame:SetScript("OnMouseDown", nil) self.frame:EnableKeyboard(false) self.frame:EnableMouse(false) self.frame:Hide() self.frame:SetAlpha(0) end local watcher = getglobal("NanamiAFKWatcher") if watcher then watcher:SetScript("OnUpdate", nil) watcher:Hide() end if self._buffTip then self._buffTip:Hide() end self.isShowing = false self.fadeDirection = nil self._exiting = false self._danceWait = nil 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() UIParent:Show() self:Cleanup() self:Build() self._isAFK = false self._lastActivity = GetTime() local function MarkActive() AFK._lastActivity = GetTime() end if not self._hooked then self._hooked = true 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 end 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 local watcher = getglobal("NanamiAFKWatcher") or CreateFrame("Frame", "NanamiAFKWatcher", UIParent) watcher._checkTimer = 0 watcher._lastCursorX = 0 watcher._lastCursorY = 0 watcher:Show() watcher:SetScript("OnUpdate", function() this._checkTimer = (this._checkTimer or 0) + arg1 if this._checkTimer < 1 then return end this._checkTimer = 0 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) 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) SFrames:RegisterEvent("PLAYER_LEAVING_WORLD", function() AFK:Cleanup() end) end