SFrames.Player = {} local _A = SFrames.ActiveTheme local CLASS_NAME_ZH = { WARRIOR = "\230\136\152\229\163\171", MAGE = "\230\179\149\229\184\136", ROGUE = "\230\189\156\232\161\140\232\128\133", DRUID = "\229\190\183\233\178\129\228\188\138", HUNTER = "\231\140\142\228\186\186", SHAMAN = "\232\144\168\230\187\161\231\165\173\229\143\184", PRIEST = "\231\137\167\229\184\136", WARLOCK = "\230\156\175\229\163\171", PALADIN = "\229\156\163\233\170\145\229\163\171", } local function GetChineseClassName(classToken, localizedClass) if classToken and CLASS_NAME_ZH[classToken] then return CLASS_NAME_ZH[classToken] end return localizedClass or "" end local function GetIncomingHeals(unit) return SFrames:GetIncomingHeals(unit) end local function Clamp(value, minValue, maxValue) if value < minValue then return minValue end if value > maxValue then return maxValue end return value end local function SetTextureIfPresent(region, texturePath) if region and region.SetTexture and texturePath then region:SetTexture(texturePath) end end local function ApplyFontIfPresent(fs, size, fontKey, fallbackFontKey) if not fs then return end SFrames:ApplyFontString(fs, size, fontKey, fallbackFontKey) end function SFrames.Player:GetConfig() local db = SFramesDB or {} local width = tonumber(db.playerFrameWidth) or SFrames.Config.width or 220 width = Clamp(math.floor(width + 0.5), 170, 420) local portraitWidth = tonumber(db.playerPortraitWidth) or SFrames.Config.portraitWidth or 50 portraitWidth = Clamp(math.floor(portraitWidth + 0.5), 32, 95) if portraitWidth > width - 90 then portraitWidth = width - 90 end local healthHeight = tonumber(db.playerHealthHeight) or 38 healthHeight = Clamp(math.floor(healthHeight + 0.5), 14, 80) local powerHeight = tonumber(db.playerPowerHeight) or 9 powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, 40) local showPortrait = db.playerShowPortrait ~= false local gradientStyle = SFrames:IsGradientStyle() local classicDefaultPowerWidth = width - (showPortrait and portraitWidth or 0) - 2 if classicDefaultPowerWidth < 60 then classicDefaultPowerWidth = 60 end local rawPowerWidth = tonumber(db.playerPowerWidth) local legacyFullWidth = tonumber(db.playerFrameWidth) or width local defaultPowerWidth = gradientStyle and width or classicDefaultPowerWidth local maxPowerWidth = gradientStyle and width or (width - 2) local powerWidth if gradientStyle then -- 渐变风格:能量条始终与血条等宽(全宽) powerWidth = width elseif not rawPowerWidth or math.abs(rawPowerWidth - legacyFullWidth) < 0.5 or math.abs(rawPowerWidth - classicDefaultPowerWidth) < 0.5 then powerWidth = defaultPowerWidth else powerWidth = rawPowerWidth end powerWidth = Clamp(math.floor(powerWidth + 0.5), 60, maxPowerWidth) local powerOffsetX = Clamp(math.floor((tonumber(db.playerPowerOffsetX) or 0) + 0.5), -120, 120) local powerOffsetY = Clamp(math.floor((tonumber(db.playerPowerOffsetY) or 0) + 0.5), -80, 80) local height = healthHeight + powerHeight + 4 height = Clamp(height, 30, 140) local nameFont = tonumber(db.playerNameFontSize) or 10 nameFont = Clamp(math.floor(nameFont + 0.5), 8, 18) local valueFont = tonumber(db.playerValueFontSize) or 10 valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18) local healthFont = tonumber(db.playerHealthFontSize) or valueFont healthFont = Clamp(math.floor(healthFont + 0.5), 8, 18) local powerFont = tonumber(db.playerPowerFontSize) or valueFont powerFont = Clamp(math.floor(powerFont + 0.5), 8, 18) local frameScale = tonumber(db.playerFrameScale) or 1 frameScale = Clamp(frameScale, 0.7, 1.8) return { width = width, height = height, portraitWidth = portraitWidth, healthHeight = healthHeight, powerHeight = powerHeight, powerWidth = powerWidth, powerOffsetX = powerOffsetX, powerOffsetY = powerOffsetY, powerOnTop = db.playerPowerOnTop == true, nameFont = nameFont, valueFont = valueFont, healthFont = healthFont, powerFont = powerFont, healthTexture = SFrames:ResolveBarTexture("playerHealthTexture", "barTexture"), powerTexture = SFrames:ResolveBarTexture("playerPowerTexture", "barTexture"), scale = frameScale, } end function SFrames.Player:ApplyConfig() if not self.frame then return end local cfg = self:GetConfig() local f = self.frame local db = SFramesDB or {} local showPortrait = db.playerShowPortrait ~= false local frameAlpha = tonumber(db.playerFrameAlpha) or 1 frameAlpha = Clamp(frameAlpha, 0.1, 1.0) f:SetScale(cfg.scale) f:SetWidth(cfg.width) f:SetHeight(cfg.height) f:SetAlpha(frameAlpha) local bgA = tonumber(db.playerBgAlpha) or 0.9 local _A = SFrames.ActiveTheme if _A and _A.panelBg and bgA < 0.89 then if f.SetBackdropColor then f:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end if f.healthBGFrame and f.healthBGFrame.SetBackdropColor then f.healthBGFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end if f.powerBGFrame and f.powerBGFrame.SetBackdropColor then f.powerBGFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end if f.portraitBG and f.portraitBG.SetBackdropColor then f.portraitBG:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end end if showPortrait then if f.portrait then f.portrait:SetWidth(cfg.portraitWidth) f.portrait:SetHeight(cfg.height - 2) f.portrait:Show() end if f.portraitBG then f.portraitBG:ClearAllPoints() f.portraitBG:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) f.portraitBG:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMRIGHT", 1, -1) f.portraitBG:Show() end if f.health then f.health:ClearAllPoints() f.health:SetPoint("TOPLEFT", f.portrait, "TOPRIGHT", 1, 0) f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1) f.health:SetHeight(cfg.healthHeight) end if f.classIcon and f.classIcon.overlay then f.classIcon.overlay:ClearAllPoints() f.classIcon.overlay:SetPoint("CENTER", f.portrait, "TOPRIGHT", 0, 0) end else if f.portrait then f.portrait:Hide() end if f.portraitBG then f.portraitBG:Hide() end if f.health then f.health:ClearAllPoints() f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1) f.health:SetHeight(cfg.healthHeight) end if f.classIcon and f.classIcon.overlay then f.classIcon.overlay:ClearAllPoints() f.classIcon.overlay:SetPoint("CENTER", f, "TOPLEFT", 8, 0) end end if f.healthBGFrame then f.healthBGFrame:ClearAllPoints() f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1) f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1) end if f.power then f.power:ClearAllPoints() f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", cfg.powerOffsetX, -1 + cfg.powerOffsetY) f.power:SetWidth(cfg.powerWidth) f.power:SetHeight(cfg.powerHeight) end if f.powerBGFrame then f.powerBGFrame:ClearAllPoints() f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1) f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1) end if f.restOverlay then if showPortrait then f.restOverlay:SetAlpha(1) else f.restOverlay:SetAlpha(0) end end if f.health and f.power then local healthLevel = f:GetFrameLevel() + 2 local powerLevel = cfg.powerOnTop and (healthLevel + 1) or (healthLevel - 1) f.health:SetFrameLevel(healthLevel) f.power:SetFrameLevel(powerLevel) end SFrames:ApplyConfiguredUnitBackdrop(f, "player") if f.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.healthBGFrame, "player") end if f.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.powerBGFrame, "player") end if f.portraitBG then SFrames:ApplyConfiguredUnitBackdrop(f.portraitBG, "player", true) end if f.castbar then f.castbar:ClearAllPoints() if showPortrait then f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", 0, 6) f.castbar:SetPoint("BOTTOMLEFT", f.portrait, "TOPLEFT", SFrames.Config.castbarHeight + 6, 6) else f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", 0, 6) f.castbar:SetPoint("BOTTOMLEFT", f, "TOPLEFT", SFrames.Config.castbarHeight + 6, 6) end end SFrames:ApplyStatusBarTexture(f.health, "playerHealthTexture", "barTexture") SFrames:ApplyStatusBarTexture(f.power, "playerPowerTexture", "barTexture") if f.manaBar then SFrames:ApplyStatusBarTexture(f.manaBar, "playerPowerTexture", "barTexture") end SetTextureIfPresent(f.health and f.health.bg, cfg.healthTexture) SetTextureIfPresent(f.health and f.health.healPredMine, cfg.healthTexture) SetTextureIfPresent(f.health and f.health.healPredOther, cfg.healthTexture) SetTextureIfPresent(f.health and f.health.healPredOver, cfg.healthTexture) SetTextureIfPresent(f.power and f.power.bg, cfg.powerTexture) SetTextureIfPresent(f.manaBar and f.manaBar.bg, cfg.powerTexture) -- Gradient style preset if SFrames:IsGradientStyle() then -- Hide portrait, its backdrop, and class icon (no portrait in gradient mode) if f.portrait then f.portrait:Hide() end if f.portraitBG then f.portraitBG:Hide() end if f.restOverlay then f.restOverlay:SetAlpha(0) end if f.classIcon then f.classIcon:Hide(); if f.classIcon.overlay then f.classIcon.overlay:Hide() end end -- Strip backdrops SFrames:ClearBackdrop(f) SFrames:ClearBackdrop(f.healthBGFrame) SFrames:ClearBackdrop(f.powerBGFrame) -- Health bar full width if f.health then f.health:ClearAllPoints() f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0) f.health:SetHeight(cfg.healthHeight) end -- Power bar full width, below health if f.power then f.power:ClearAllPoints() f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", cfg.powerOffsetX, -2 + cfg.powerOffsetY) f.power:SetWidth(cfg.powerWidth) f.power:SetHeight(cfg.powerHeight) end -- Apply gradient overlays SFrames:ApplyGradientStyle(f.health) SFrames:ApplyGradientStyle(f.power) if f.manaBar then SFrames:ApplyGradientStyle(f.manaBar) end -- Reposition healthBGFrame / powerBGFrame flush (no border padding) if f.healthBGFrame then f.healthBGFrame:ClearAllPoints() f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", 0, 0) f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 0, 0) end if f.powerBGFrame then f.powerBGFrame:ClearAllPoints() f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", 0, 0) f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 0, 0) end -- Hide bar backgrounds (transparent) if f.healthBGFrame then f.healthBGFrame:Hide() end if f.powerBGFrame then f.powerBGFrame:Hide() end if f.health and f.health.bg then f.health.bg:Hide() end if f.power and f.power.bg then f.power.bg:Hide() end if f.manaBar and f.manaBar.bg then f.manaBar.bg:Hide() end else -- Classic style: remove gradient overlays if they exist SFrames:RemoveGradientStyle(f.health) SFrames:RemoveGradientStyle(f.power) if f.manaBar then SFrames:RemoveGradientStyle(f.manaBar) end -- Restore bar backgrounds if f.healthBGFrame then f.healthBGFrame:Show() end if f.powerBGFrame then f.powerBGFrame:Show() end if f.health and f.health.bg then f.health.bg:Show() end if f.power and f.power.bg then f.power.bg:Show() end if f.manaBar and f.manaBar.bg then f.manaBar.bg:Show() end end ApplyFontIfPresent(f.nameText, cfg.nameFont, "playerNameFontKey") ApplyFontIfPresent(f.healthText, cfg.healthFont, "playerHealthFontKey") ApplyFontIfPresent(f.powerText, cfg.powerFont, "playerPowerFontKey") if f.manaText then local manaFont = cfg.powerFont - 1 if manaFont < 8 then manaFont = 8 end ApplyFontIfPresent(f.manaText, manaFont, "playerPowerFontKey") end if f.zLetters then for i = 1, 3 do if f.zLetters[i] and f.zLetters[i].text then SFrames:ApplyFontString(f.zLetters[i].text, 8 + (i - 1) * 3, nil, "fontKey") end end end -- Icon positions if f.leaderIcon then local ox = tonumber(db.playerLeaderIconOffsetX) or 0 local oy = tonumber(db.playerLeaderIconOffsetY) or 0 f.leaderIcon:ClearAllPoints() f.leaderIcon:SetPoint("TOPLEFT", f, "TOPLEFT", ox, oy) end if f.raidIconOverlay then local ox = tonumber(db.playerRaidIconOffsetX) or 0 local oy = tonumber(db.playerRaidIconOffsetY) or 0 f.raidIconOverlay:ClearAllPoints() f.raidIconOverlay:SetPoint("CENTER", f.health, "TOP", ox, oy) end self:UpdateAll() end local RAINBOW_TEX_PATH = "Interface\\AddOns\\Nanami-UI\\img\\progress" function SFrames.Player:Initialize() local f = CreateFrame("Button", "SFramesPlayerFrame", UIParent) f:SetWidth(SFrames.Config.width) f:SetHeight(SFrames.Config.height) local frameScale = (SFramesDB and type(SFramesDB.playerFrameScale) == "number") and SFramesDB.playerFrameScale or 1 f:SetScale(frameScale) if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PlayerFrame"] then local pos = SFramesDB.Positions["PlayerFrame"] local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale() if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then f:SetPoint(pos.point, UIParent, pos.relativePoint, (pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale) else f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) end else f:SetPoint("CENTER", UIParent, "CENTER", -200, -100) end f:SetMovable(true) f:EnableMouse(true) f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", function() if IsAltKeyDown() or SFrames.isUnlocked then f:StartMoving() end end) f:SetScript("OnDragStop", function() f:StopMovingOrSizing() if not SFramesDB then SFramesDB = {} end if not SFramesDB.Positions then SFramesDB.Positions = {} end local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint() local fSc = f:GetEffectiveScale() / UIParent:GetEffectiveScale() if fSc > 0.01 and math.abs(fSc - 1) > 0.001 then xOfs = (xOfs or 0) * fSc yOfs = (yOfs or 0) * fSc end SFramesDB.Positions["PlayerFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs } end) -- Register clicks for targeting f:RegisterForClicks("LeftButtonUp", "RightButtonUp") f:SetScript("OnClick", function() if arg1 == "LeftButton" then TargetUnit("player") else ToggleDropDownMenu(1, nil, PlayerFrameDropDown, this:GetName(), 106, 27) end end) -- Base Backdrop SFrames:CreateUnitBackdrop(f) -- 3D Portrait local pWidth = SFrames.Config.portraitWidth f.portrait = CreateFrame("PlayerModel", nil, f) f.portrait:SetWidth(pWidth) f.portrait:SetHeight(SFrames.Config.height - 2) f.portrait:SetPoint("LEFT", f, "LEFT", 1, 0) f.portrait:SetUnit("player") f.portrait:SetCamera(0) f.portrait:SetPosition(-1.0, 0, 0) -- We need a backdrop for the portrait to separate it from health bar local pbg = CreateFrame("Frame", nil, f) pbg:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) pbg:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMRIGHT", 1, -1) pbg:SetFrameLevel(f:GetFrameLevel()) SFrames:CreateUnitBackdrop(pbg) f.portraitBG = pbg -- Health Bar f.health = SFrames:CreateStatusBar(f, "SFramesPlayerHealth") f.health:SetPoint("TOPLEFT", f.portrait, "TOPRIGHT", 1, 0) f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1) f.health:SetHeight((SFrames.Config.height - 2) * 0.82 - 1) -- 82% height, minus 1px gap f.health:SetMinMaxValues(0, 100) -- Health Backdrop local hbg = CreateFrame("Frame", nil, f) hbg:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1) hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1) hbg:SetFrameLevel(math.max(0, f:GetFrameLevel() - 1)) SFrames:CreateUnitBackdrop(hbg) f.healthBGFrame = hbg -- Add a dark backdrop behind the health texture f.health.bg = f.health:CreateTexture(nil, "BACKGROUND") f.health.bg:SetAllPoints() f.health.bg:SetTexture(SFrames:GetTexture()) f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) -- Heal prediction overlay (incoming heals) f.health.healPredMine = f.health:CreateTexture(nil, "ARTWORK") f.health.healPredMine:SetTexture(SFrames:GetTexture()) f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78) f.health.healPredMine:SetDrawLayer("ARTWORK", 2) f.health.healPredMine:Hide() f.health.healPredOther = f.health:CreateTexture(nil, "ARTWORK") f.health.healPredOther:SetTexture(SFrames:GetTexture()) f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5) f.health.healPredOther:SetDrawLayer("ARTWORK", 2) f.health.healPredOther:Hide() f.health.healPredOver = f.health:CreateTexture(nil, "OVERLAY") f.health.healPredOver:SetTexture(SFrames:GetTexture()) f.health.healPredOver:SetVertexColor(1.0, 0.3, 0.3, 0.6) f.health.healPredOver:SetDrawLayer("OVERLAY", 7) f.health.healPredOver:Hide() -- Power Bar f.power = SFrames:CreateStatusBar(f, "SFramesPlayerPower") f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1) f.power:SetMinMaxValues(0, 100) -- Power Backdrop local powerbg = CreateFrame("Frame", nil, f) powerbg:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1) powerbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1) powerbg:SetFrameLevel(math.max(0, f:GetFrameLevel() - 1)) SFrames:CreateUnitBackdrop(powerbg) f.powerBGFrame = powerbg -- Add a dark backdrop behind the power texture f.power.bg = f.power:CreateTexture(nil, "BACKGROUND") f.power.bg:SetAllPoints() f.power.bg:SetTexture(SFrames:GetTexture()) f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) -- Rainbow overlay for power bar f.power.rainbowTex = f.power:CreateTexture(nil, "OVERLAY") f.power.rainbowTex:SetTexture(RAINBOW_TEX_PATH) f.power.rainbowTex:SetAllPoints() f.power.rainbowTex:Hide() -- Five-second rule ticker (mana regen delay indicator) f.power.fsrGlow = f.power:CreateTexture(nil, "OVERLAY") f.power.fsrGlow:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark") f.power.fsrGlow:SetVertexColor(0.62, 0.90, 1.0, 0.52) f.power.fsrGlow:SetBlendMode("ADD") pcall(function() f.power.fsrGlow:SetDrawLayer("OVERLAY", 5) end) f.power.fsrGlow:Hide() -- Texts f.nameText = SFrames:CreateFontString(f.health, 10, "LEFT") f.nameText:SetPoint("LEFT", f.health, "LEFT", 6, 0) f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT") f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -6, 0) f.powerText = SFrames:CreateFontString(f.power, 10, "RIGHT") f.powerText:SetPoint("RIGHT", f.power, "RIGHT", -6, 0) -- Extra mana text for shapeshift druids (show blue mana while rage/energy is active) f.manaText = SFrames:CreateFontString(f.power, 9, "LEFT") f.manaText:SetPoint("LEFT", f.power, "LEFT", 6, 0) f.manaText:SetTextColor(0.30, 0.65, 1.0) f.manaText:Hide() f.manaBar = CreateFrame("StatusBar", nil, f) f.manaBar:SetHeight(4) f.manaBar:SetPoint("TOPLEFT", f.power, "BOTTOMLEFT", 0, -1) f.manaBar:SetPoint("TOPRIGHT", f.power, "BOTTOMRIGHT", 0, -1) f.manaBar:SetStatusBarTexture(SFrames:GetTexture()) f.manaBar:SetStatusBarColor(0.30, 0.65, 1.0, 0.90) f.manaBar:SetMinMaxValues(0, 100) f.manaBar:SetValue(0) f.manaBar:SetFrameLevel(f:GetFrameLevel() + 1) f.manaBar.bg = f.manaBar:CreateTexture(nil, "BACKGROUND") f.manaBar.bg:SetAllPoints() f.manaBar.bg:SetTexture(SFrames:GetTexture()) f.manaBar.bg:SetVertexColor(0.05, 0.10, 0.20, 0.7) f.manaBar:Hide() -- Outline/shadow setup for text to make it pop f.nameText:SetShadowColor(0, 0, 0, 1) f.nameText:SetShadowOffset(1, -1) f.healthText:SetShadowColor(0, 0, 0, 1) f.healthText:SetShadowOffset(1, -1) f.powerText:SetShadowColor(0, 0, 0, 1) f.powerText:SetShadowOffset(1, -1) f.manaText:SetShadowColor(0, 0, 0, 1) f.manaText:SetShadowOffset(1, -1) -- Resting Indicator (animated zzz on portrait) local restOverlay = CreateFrame("Frame", nil, f) restOverlay:SetFrameLevel((f:GetFrameLevel() or 0) + 6) restOverlay:SetWidth(pWidth) restOverlay:SetHeight(SFrames.Config.height) restOverlay:SetPoint("CENTER", f.portrait, "CENTER", 0, 0) f.restOverlay = restOverlay local zLetters = {} for i = 1, 3 do local zf = CreateFrame("Frame", nil, restOverlay) zf:SetWidth(16) zf:SetHeight(16) local zt = zf:CreateFontString(nil, "OVERLAY") zt:SetFont(SFrames:GetFont(), 8 + (i - 1) * 3, "OUTLINE") zt:SetText("z") zt:SetTextColor(0.85, 0.85, 1.0) zt:SetShadowColor(0, 0, 0, 0.8) zt:SetShadowOffset(1, -1) zt:SetAllPoints(zf) zf.text = zt zf.phase = (i - 1) * 1.2 zf.baseX = (i - 1) * 6 - 2 zf.baseY = (i - 1) * 5 zf:SetPoint("BOTTOMLEFT", restOverlay, "CENTER", zf.baseX, zf.baseY - 4) zLetters[i] = zf end f.zLetters = zLetters local restElapsed = 0 restOverlay:SetScript("OnUpdate", function() restElapsed = restElapsed + arg1 for idx = 1, 3 do local zf = zLetters[idx] local t = math.mod(restElapsed + zf.phase, 3.6) local ratio = t / 3.6 local floatY = ratio * 14 local alpha if ratio < 0.15 then alpha = ratio / 0.15 elseif ratio < 0.7 then alpha = 1.0 else alpha = 1.0 - (ratio - 0.7) / 0.3 end if alpha < 0 then alpha = 0 end if alpha > 1 then alpha = 1 end zf.text:SetAlpha(alpha) zf:ClearAllPoints() zf:SetPoint("BOTTOMLEFT", restOverlay, "CENTER", zf.baseX, zf.baseY - 4 + floatY) end end) restOverlay:Hide() -- Class Icon Badge (overlaid on portrait, top-right corner with 1/3 outside) f.classIcon = SFrames:CreateClassIcon(f, 16) f.classIcon.overlay:SetPoint("CENTER", f.portrait, "TOPRIGHT", 0, 0) -- Party Leader Icon local leaderOvr = CreateFrame("Frame", nil, f) leaderOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 4) leaderOvr:SetAllPoints(f) f.leaderIcon = leaderOvr:CreateTexture(nil, "OVERLAY") f.leaderIcon:SetTexture("Interface\\GroupFrame\\UI-Group-LeaderIcon") f.leaderIcon:SetWidth(16) f.leaderIcon:SetHeight(16) f.leaderIcon:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) f.leaderIcon:Hide() -- Raid Target Icon (top center of health bar, half outside frame) local raidIconSize = 22 local raidIconOvr = CreateFrame("Frame", nil, f) raidIconOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 5) raidIconOvr:SetWidth(raidIconSize) raidIconOvr:SetHeight(raidIconSize) raidIconOvr:SetPoint("CENTER", f.health, "TOP", 0, 0) f.raidIcon = raidIconOvr:CreateTexture(nil, "OVERLAY") f.raidIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons") f.raidIcon:SetAllPoints(raidIconOvr) f.raidIcon:Hide() f.raidIconOverlay = raidIconOvr self.frame = f self:ApplyConfig() self.frame:Show() -- Ensure it's explicitly shown self:UpdateAll() -- Events SFrames:RegisterEvent("UNIT_HEALTH", function() if arg1 == "player" then self:UpdateHealth() end end) SFrames:RegisterEvent("UNIT_MAXHEALTH", function() if arg1 == "player" then self:UpdateHealth() end end) SFrames:RegisterEvent("UNIT_MANA", function() if arg1 == "player" then self:UpdatePower() end end) SFrames:RegisterEvent("UNIT_MAXMANA", function() if arg1 == "player" then self:UpdatePower() end end) SFrames:RegisterEvent("UNIT_ENERGY", function() if arg1 == "player" then self:UpdatePower() end end) SFrames:RegisterEvent("UNIT_MAXENERGY", function() if arg1 == "player" then self:UpdatePower() end end) SFrames:RegisterEvent("UNIT_RAGE", function() if arg1 == "player" then self:UpdatePower() end end) SFrames:RegisterEvent("UNIT_MAXRAGE", function() if arg1 == "player" then self:UpdatePower() end end) SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() self:UpdateAll() end) SFrames:RegisterEvent("PLAYER_LEVEL_UP", function() if arg1 then self.currentLevel = arg1 end self:UpdateAll() end) SFrames:RegisterEvent("PARTY_MEMBERS_CHANGED", function() self:UpdateLeaderIcon() end) SFrames:RegisterEvent("PARTY_LEADER_CHANGED", function() self:UpdateLeaderIcon() end) SFrames:RegisterEvent("RAID_TARGET_UPDATE", function() self:UpdateRaidIcon() end) SFrames:RegisterEvent("UNIT_PORTRAIT_UPDATE", function() if arg1 == "player" and self.frame.portrait and not (SFramesDB and SFramesDB.playerShowPortrait == false) then self.frame.portrait:SetUnit("player") self.frame.portrait:SetCamera(0) self.frame.portrait:SetPosition(-1.0, 0, 0) end end) SFrames:RegisterEvent("UNIT_DISPLAYPOWER", function() if arg1 == "player" then self:UpdatePowerType(); self:UpdatePower() end end) SFrames:RegisterEvent("UPDATE_SHAPESHIFT_FORM", function() self:UpdatePowerType(); self:UpdatePower() end) SFrames:RegisterEvent("PLAYER_UPDATE_RESTING", function() self:UpdateRestingStatus() end) f.unit = "player" f:SetScript("OnEnter", function() if SetMouseoverUnit then SetMouseoverUnit(this.unit) end GameTooltip_SetDefaultAnchor(GameTooltip, this) GameTooltip:SetUnit(this.unit) GameTooltip:Show() end) f:SetScript("OnLeave", function() if SetMouseoverUnit then SetMouseoverUnit() end GameTooltip:Hide() end) end function SFrames.Player:UpdateAll() if not self.frame then return end self:UpdateHealth() self:UpdatePowerType() self:UpdatePower() self:UpdateLeaderIcon() self:UpdateRaidIcon() self:UpdateRestingStatus() local name = UnitName("player") or "" -- Use the stored level from PLAYER_LEVEL_UP if it exists, since API might lag slightly local level = self.currentLevel or UnitLevel("player") local formattedLevel = string.format("|cffffff00%d|r", level) if SFramesDB and SFramesDB.showLevel == false then formattedLevel = "" else formattedLevel = formattedLevel .. " " end local showPortrait = not (SFramesDB and SFramesDB.playerShowPortrait == false) if showPortrait and self.frame.portrait then self.frame.portrait:SetUnit("player") self.frame.portrait:SetCamera(0) self.frame.portrait:SetPosition(-1.0, 0, 0) end -- Class Color for Health local localizedClass, class = UnitClass("player") local className = GetChineseClassName(class, localizedClass) local nameLine = formattedLevel .. name local showClassText = not (SFramesDB and SFramesDB.playerShowClass == false) if showClassText and className and className ~= "" then nameLine = nameLine .. " " .. className end if not SFrames:IsGradientStyle() and not (SFramesDB and SFramesDB.playerShowClassIcon == false) then SFrames:SetClassIcon(self.frame.classIcon, class) else self.frame.classIcon:Hide() if self.frame.classIcon.overlay then self.frame.classIcon.overlay:Hide() end end local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false) -- Gradient style always uses class colors if SFrames:IsGradientStyle() then useClassColor = true end if useClassColor and class and SFrames.Config.colors.class[class] then local color = SFrames.Config.colors.class[class] self.frame.health:SetStatusBarColor(color.r, color.g, color.b) -- Apply Class Color to Name self.frame.nameText:SetText(nameLine) self.frame.nameText:SetTextColor(color.r, color.g, color.b) else self.frame.health:SetStatusBarColor(0, 1, 0) self.frame.nameText:SetText(nameLine) self.frame.nameText:SetTextColor(1, 1, 1) end -- Re-apply gradient after color change (SetStatusBarColor resets SetGradientAlpha) if SFrames:IsGradientStyle() then SFrames:ApplyBarGradient(self.frame.health) end end function SFrames.Player:UpdateRestingStatus() if not self.frame or not self.frame.restOverlay then return end if IsResting() then self.frame.restOverlay:Show() else self.frame.restOverlay:Hide() end end function SFrames.Player:UpdateLeaderIcon() if IsPartyLeader() then self.frame.leaderIcon:Show() else self.frame.leaderIcon:Hide() end end function SFrames.Player:UpdateRaidIcon() if not (self.frame and self.frame.raidIcon) then return end if not GetRaidTargetIndex then self.frame.raidIcon:Hide() return end local index = GetRaidTargetIndex("player") if index and index > 0 and index <= 8 then local col = math.mod(index - 1, 4) local row = math.floor((index - 1) / 4) self.frame.raidIcon:SetTexCoord(col * 0.25, (col + 1) * 0.25, row * 0.25, (row + 1) * 0.25) self.frame.raidIcon:Show() else self.frame.raidIcon:Hide() end end function SFrames.Player:UpdateHealth() local hp = UnitHealth("player") local maxHp = UnitHealthMax("player") if CheckSuperWow then local ok, hasSW = pcall(CheckSuperWow) if ok and hasSW then local ok2, realHp = pcall(UnitHealth, "player") if ok2 then hp = realHp or hp end local ok3, realMaxHp = pcall(UnitHealthMax, "player") if ok3 then maxHp = realMaxHp or maxHp end end end self.frame.health:SetMinMaxValues(0, maxHp) self.frame.health:SetValue(hp) if maxHp > 0 then self.frame.healthText:SetText(SFrames:FormatCompactPair(hp, maxHp)) else self.frame.healthText:SetText(SFrames:FormatCompactNumber(hp)) end self:UpdateHealPrediction() end function SFrames.Player:UpdateHealPrediction() if not (self.frame and self.frame.health and self.frame.health.healPredMine and self.frame.health.healPredOther and self.frame.health.healPredOver) then return end local predMine = self.frame.health.healPredMine local predOther = self.frame.health.healPredOther local predOver = self.frame.health.healPredOver local hp = UnitHealth("player") or 0 local maxHp = UnitHealthMax("player") or 0 if CheckSuperWow then local ok, hasSW = pcall(CheckSuperWow) if ok and hasSW then local ok2, realHp = pcall(UnitHealth, "player") if ok2 then hp = realHp or hp end local ok3, realMaxHp = pcall(UnitHealthMax, "player") if ok3 then maxHp = realMaxHp or maxHp end end end if maxHp <= 0 or UnitIsDeadOrGhost("player") then predMine:Hide(); predOther:Hide(); predOver:Hide() return end local totalIncoming, mineIncoming, othersIncoming = 0, 0, 0 local ok, t, m, o = pcall(function() return GetIncomingHeals("player") end) if ok then totalIncoming, mineIncoming, othersIncoming = t or 0, m or 0, o or 0 end local missing = maxHp - hp if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then predMine:Hide(); predOther:Hide(); predOver:Hide() return end local mineShown = math.min(math.max(0, mineIncoming), missing) local remaining = missing - mineShown local otherShown = math.min(math.max(0, othersIncoming), remaining) if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then predMine:Hide(); predOther:Hide(); predOver:Hide() return end local showPortrait = SFramesDB and SFramesDB.playerShowPortrait ~= false local barWidth = self.frame:GetWidth() - (showPortrait and (self.frame.portrait:GetWidth() + 2) or 2) if barWidth <= 0 then predMine:Hide(); predOther:Hide(); predOver:Hide() return end local currentWidth = (hp / maxHp) * barWidth if currentWidth < 0 then currentWidth = 0 end if currentWidth > barWidth then currentWidth = barWidth end local availableWidth = barWidth - currentWidth if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then predMine:Hide(); predOther:Hide(); predOver:Hide() return end local mineWidth = 0 local otherWidth = 0 if missing > 0 then mineWidth = (mineShown / missing) * availableWidth otherWidth = (otherShown / missing) * availableWidth if mineWidth < 0 then mineWidth = 0 end if otherWidth < 0 then otherWidth = 0 end if mineWidth > availableWidth then mineWidth = availableWidth end if otherWidth > (availableWidth - mineWidth) then otherWidth = availableWidth - mineWidth end end if mineWidth > 0 then predMine:ClearAllPoints() predMine:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth, 0) predMine:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth, 0) predMine:SetWidth(mineWidth) predMine:SetHeight(self.frame.health:GetHeight()) predMine:Show() else predMine:Hide() end if otherWidth > 0 then predOther:ClearAllPoints() predOther:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth + mineWidth, 0) predOther:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth + mineWidth, 0) predOther:SetWidth(otherWidth) predOther:SetHeight(self.frame.health:GetHeight()) predOther:Show() else predOther:Hide() end local totalIncomingValue = mineIncoming + othersIncoming local overHeal = totalIncomingValue - missing if overHeal > 0 then local overWidth = math.floor((overHeal / maxHp) * barWidth + 0.5) if overWidth > 0 then predOver:ClearAllPoints() predOver:SetPoint("TOPLEFT", self.frame.health, "TOPRIGHT", 0, 0) predOver:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMRIGHT", 0, 0) predOver:SetWidth(overWidth) predOver:SetHeight(self.frame.health:GetHeight()) predOver:Show() else predOver:Hide() end else predOver:Hide() end end function SFrames.Player:UpdatePowerType() local powerType = UnitPowerType("player") local color = SFrames.Config.colors.power[powerType] if color then self.frame.power:SetStatusBarColor(color.r, color.g, color.b) else self.frame.power:SetStatusBarColor(0, 0, 1) end if SFrames:IsGradientStyle() then SFrames:ApplyBarGradient(self.frame.power) end end function SFrames.Player:GetDruidAltMana(currentPower, currentMaxPower) -- Method 1: DruidManaLib (tracks regen ticks, MP5, talents, shapeshift cost) if AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("DruidManaLib-1.0") then local ok, lib = pcall(function() return AceLibrary("DruidManaLib-1.0") end) if ok and lib and lib.GetMana then local mana, maxMana = lib:GetMana() if type(mana) == "number" and type(maxMana) == "number" and maxMana > 0 then return math.floor(mana + 0.5), math.floor(maxMana + 0.5) end end end -- Method 2: SuperWow returns real mana as second value of UnitMana if CheckSuperWow then local ok, hasSW = pcall(CheckSuperWow) if ok and hasSW then local ok2, _, realMana = pcall(UnitMana, "player") local ok3, _, realMax = pcall(UnitManaMax, "player") if ok2 and ok3 and type(realMana) == "number" and type(realMax) == "number" and realMax > 0 then return realMana, realMax end end end -- Method 3: TBC-style UnitPower API if UnitPower and UnitPowerMax then local okMana, pMana = pcall(function() return UnitPower("player", 0) end) local okMax, pMax = pcall(function() return UnitPowerMax("player", 0) end) if okMana and okMax and type(pMana) == "number" and type(pMax) == "number" and pMax > 0 then if not (pMax == currentMaxPower and pMana == currentPower) and pMax > 100 then return pMana, pMax end end end return nil, nil end function SFrames.Player:UpdateFiveSecondRule() if not (self.frame and self.frame.power and self.frame.power.fsrGlow) then return end local powerBar = self.frame.power local glow = powerBar.fsrGlow local function HideTicker() glow:Hide() end local powerType = UnitPowerType("player") if powerType ~= 0 then self.fiveSecondStart = nil self.fiveSecondPendingStart = nil self.fiveSecondLastMana = nil HideTicker() return end local now = GetTime() -- Continuous monitoring: whenever mana drops, (re)start 5-second rule. -- If mana drops while hard-casting, delay start until cast end. local currentMana = UnitMana("player") or 0 if self.fiveSecondLastMana and currentMana < self.fiveSecondLastMana then local delay = 0 local cb = self.frame and self.frame.castbar if cb and cb.casting and cb.startTime and cb.maxValue then local castEnd = cb.startTime + cb.maxValue if castEnd > now then delay = castEnd - now end end local startAt = now + delay + 0.08 if delay > 0 then self.fiveSecondPendingStart = startAt else self.fiveSecondStart = startAt self.fiveSecondPendingStart = nil end end self.fiveSecondLastMana = currentMana if self.fiveSecondPendingStart then if now >= self.fiveSecondPendingStart then self.fiveSecondStart = self.fiveSecondPendingStart self.fiveSecondPendingStart = nil else HideTicker() return end end local startTime = self.fiveSecondStart if not startTime then HideTicker() return end local elapsed = now - startTime if elapsed < 0 then HideTicker() return end if elapsed >= 5 then self.fiveSecondStart = nil HideTicker() return end local barWidth = powerBar:GetWidth() or 0 if barWidth <= 0 then HideTicker() return end local progress = elapsed / 5 if progress < 0 then progress = 0 end if progress > 1 then progress = 1 end local glowWidth = 40 if glowWidth > barWidth then glowWidth = barWidth end local maxGlowX = barWidth - glowWidth -- Move strictly from left edge to right edge across full 5 seconds. local glowX = math.floor(progress * maxGlowX + 0.5) local pulse = 0.82 + 0.18 * math.sin(GetTime() * 10) glow:ClearAllPoints() glow:SetPoint("TOPLEFT", powerBar, "TOPLEFT", glowX, 0) glow:SetPoint("BOTTOMLEFT", powerBar, "BOTTOMLEFT", glowX, 0) glow:SetWidth(glowWidth) glow:SetAlpha(0.62 * pulse) glow:Show() end function SFrames.Player:UpdatePower() local power = UnitMana("player") local maxPower = UnitManaMax("player") self.frame.power:SetMinMaxValues(0, maxPower) self.frame.power:SetValue(power) self.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower)) SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, "player") local _, class = UnitClass("player") local powerType = UnitPowerType("player") self:UpdateFiveSecondRule() if class ~= "DRUID" then if self.frame.manaText then self.frame.manaText:Hide() end if self.frame.manaBar then self.frame.manaBar:Hide() end return end if not self.druidManaCache then self.druidManaCache = { value = 0, max = 0, ts = GetTime() } end if powerType == 0 then self.druidManaCache.value = power or 0 self.druidManaCache.max = maxPower or 0 self.druidManaCache.ts = GetTime() if self.frame.manaText then self.frame.manaText:Hide() end if self.frame.manaBar then self.frame.manaBar:Hide() end return end if not self.frame.manaText then return end local mana, maxMana = self:GetDruidAltMana(power, maxPower) if mana and maxMana and maxMana > 0 then self.druidManaCache.value = mana self.druidManaCache.max = maxMana self.druidManaCache.ts = GetTime() else local cache = self.druidManaCache if cache and cache.max and cache.max > 0 then local now = GetTime() local dt = now - (cache.ts or now) if dt < 0 then dt = 0 end local regen = 0 if GetManaRegen then local base, casting = GetManaRegen() if type(base) == "number" and base > 0 then regen = base elseif type(casting) == "number" and casting > 0 then regen = casting end end if (not regen or regen <= 0) and UnitStat then local ok, _, spi = pcall(UnitStat, "player", 5) if not (ok and type(spi) == "number" and spi > 0) then ok, spi = pcall(function() return UnitStat("player", 5) end) end if ok and type(spi) == "number" and spi > 0 then regen = (math.ceil(spi / 5) + 15) / 2 end end if regen and regen > 0 and dt > 0 then cache.value = math.min(cache.max, (cache.value or 0) + regen * dt) end cache.ts = now mana = math.floor((cache.value or 0) + 0.5) maxMana = cache.max end end if maxMana and maxMana > 0 then local pct = math.floor(mana / maxMana * 100 + 0.5) self.frame.manaText:SetText(pct .. "% " .. SFrames:FormatCompactNumber(mana)) self.frame.manaText:Show() if self.frame.manaBar then self.frame.manaBar:SetMinMaxValues(0, maxMana) self.frame.manaBar:SetValue(mana) self.frame.manaBar:Show() end else self.frame.manaText:SetText("--") self.frame.manaText:Show() if self.frame.manaBar then self.frame.manaBar:Hide() end end end -------------------------------------------------------------------------------- -- Player Auras (Buffs / Debuffs) -------------------------------------------------------------------------------- function SFrames.Player:CreateAuras() -- Create 32 Buff Slots self.frame.buffs = {} self.frame.debuffs = {} local size = 24 local spacing = 2 local rowSpacing = 1 local buffsPerRow = 9 for i = 1, 32 do local b = CreateFrame("Button", "SFramesPlayerBuff"..i, self.frame) b:SetWidth(size) b:SetHeight(size) SFrames:CreateUnitBackdrop(b) b.icon = b:CreateTexture(nil, "ARTWORK") b.icon:SetPoint("TOPLEFT", b, "TOPLEFT", 1, -1) b.icon:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -1, 1) b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) b.cdText = SFrames:CreateFontString(b, 9, "CENTER") b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1) b.cdText:SetTextColor(1, 0.82, 0) b.cdText:SetShadowColor(0, 0, 0, 1) b.cdText:SetShadowOffset(1, -1) -- Tooltip support for precise buff checking b:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") GameTooltip:SetPlayerBuff(this.buffIndex) end) b:SetScript("OnLeave", function() GameTooltip:Hide() end) -- Default row anchor if i == 1 then b:SetPoint("TOPLEFT", self.frame, "BOTTOMLEFT", 0, -1) elseif math.mod(i - 1, buffsPerRow) == 0 then b:SetPoint("TOP", self.frame.buffs[i-buffsPerRow], "BOTTOM", 0, -rowSpacing) else b:SetPoint("LEFT", self.frame.buffs[i-1], "RIGHT", spacing, 0) end b:Hide() self.frame.buffs[i] = b end end function SFrames.Player:UpdateAuras() local slotIdx = 0 local hasGetPlayerBuffID = SFrames.superwow_active and type(GetPlayerBuffID) == "function" for i = 0, 31 do local buffIndex, untilCancelled = GetPlayerBuff(i, "HELPFUL") if buffIndex and buffIndex >= 0 then if not SFrames:IsBuffHidden(buffIndex) then slotIdx = slotIdx + 1 if slotIdx > 32 then break end local b = self.frame.buffs[slotIdx] local texture = GetPlayerBuffTexture(buffIndex) if texture then b.icon:SetTexture(texture) b.buffIndex = buffIndex -- Store aura ID when SuperWoW is available if hasGetPlayerBuffID then local ok, auraID = pcall(GetPlayerBuffID, buffIndex) b.auraID = ok and auraID or nil else b.auraID = nil end b:Show() local timeLeft = GetPlayerBuffTimeLeft(buffIndex) if timeLeft and timeLeft > 0 and timeLeft < 9999 then b.cdText:SetText(SFrames:FormatTime(timeLeft)) else b.cdText:SetText("") end else b:Hide() end end end end for j = slotIdx + 1, 32 do self.frame.buffs[j]:Hide() self.frame.buffs[j].auraID = nil end end -- Initialization Hook for Auras and Castbar local origInit = SFrames.Player.Initialize function SFrames.Player:Initialize() origInit(self) -- Setup Auras self:CreateAuras() self.auraUpdater = CreateFrame("Frame") self.auraUpdater.timer = 0 self.auraUpdater:SetScript("OnUpdate", function() this.timer = this.timer + arg1 if this.timer >= 0.2 then SFrames.Player:UpdateFiveSecondRule() SFrames.Player:UpdateAuras() SFrames.Player:UpdatePower() SFrames.Player:UpdateHealPrediction() this.timer = 0 end end) -- Setup Castbar self:CreateCastbar() -- Hide default castbar if CastingBarFrame then CastingBarFrame:UnregisterAllEvents() CastingBarFrame:Hide() end -- Register mover if SFrames.Movers and SFrames.Movers.RegisterMover and self.frame then SFrames.Movers:RegisterMover("PlayerFrame", self.frame, "玩家", "CENTER", "UIParent", "CENTER", -200, -100) end end -------------------------------------------------------------------------------- -- Player Castbar -------------------------------------------------------------------------------- local function GetLatencySeconds() if GetNetStats then local _, _, latency = GetNetStats() if latency and latency > 0 then return latency / 1000 end end return 0 end local function HSVtoRGB(h, s, v) local i = math.floor(h * 6) local f = h * 6 - i local p = v * (1 - s) local q = v * (1 - f * s) local t = v * (1 - (1 - f) * s) local m = math.mod(i, 6) if m == 0 then return v, t, p elseif m == 1 then return q, v, p elseif m == 2 then return p, v, t elseif m == 3 then return p, q, v elseif m == 4 then return t, p, v else return v, p, q end end local function UpdateRainbowProgress(cb, progress) if not cb.rainbowTex then return end local barWidth = cb:GetWidth() if barWidth <= 0 then return end local fillW = progress * barWidth cb.rainbowTex:ClearAllPoints() cb.rainbowTex:SetPoint("TOPLEFT", cb, "TOPLEFT", 0, 0) cb.rainbowTex:SetPoint("BOTTOMRIGHT", cb, "BOTTOMLEFT", fillW, 0) cb.rainbowTex:SetTexCoord(0, progress, 0, 1) cb.rainbowTex:Show() end function SFrames.Player:CreateCastbar() local cb = SFrames:CreateStatusBar(self.frame, "SFramesPlayerCastbar") cb:SetHeight(SFrames.Config.castbarHeight) cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", 0, 6) cb:SetPoint("BOTTOMLEFT", self.frame.portrait, "TOPLEFT", SFrames.Config.castbarHeight + 6, 6) local cbbg = CreateFrame("Frame", nil, self.frame) cbbg:SetPoint("TOPLEFT", cb, "TOPLEFT", -1, 1) cbbg:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", 1, -1) cbbg:SetFrameLevel(cb:GetFrameLevel() - 1) SFrames:CreateUnitBackdrop(cbbg) cb.bg = cb:CreateTexture(nil, "BACKGROUND") cb.bg:SetAllPoints() cb.bg:SetTexture(SFrames:GetTexture()) cb.bg:SetVertexColor(0.2, 0.2, 0.2, 1) cb:SetStatusBarColor(1, 0.7, 0) cb.text = SFrames:CreateFontString(cb, 10, "LEFT") cb.text:SetPoint("LEFT", cb, "LEFT", 4, 0) cb.time = SFrames:CreateFontString(cb, 10, "RIGHT") cb.time:SetPoint("RIGHT", cb, "RIGHT", -4, 0) cb.icon = cb:CreateTexture(nil, "ARTWORK") cb.icon:SetWidth(SFrames.Config.castbarHeight + 2) cb.icon:SetHeight(SFrames.Config.castbarHeight + 2) cb.icon:SetPoint("RIGHT", cb, "LEFT", -4, 0) cb.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) -- Icon Backdrop local ibg = CreateFrame("Frame", nil, self.frame) ibg:SetPoint("TOPLEFT", cb.icon, "TOPLEFT", -1, 1) ibg:SetPoint("BOTTOMRIGHT", cb.icon, "BOTTOMRIGHT", 1, -1) ibg:SetFrameLevel(cb:GetFrameLevel() - 1) SFrames:CreateUnitBackdrop(ibg) local lagTex = cb:CreateTexture(nil, "OVERLAY") lagTex:SetTexture(SFrames:GetTexture()) lagTex:SetVertexColor(1, 0.2, 0.2, 0.5) lagTex:Hide() cb.lagTex = lagTex cb.rainbowTex = cb:CreateTexture(nil, "ARTWORK") cb.rainbowTex:SetTexture(RAINBOW_TEX_PATH) cb.rainbowTex:Hide() cb:Hide() cbbg:Hide() cb.icon:Hide() ibg:Hide() self.frame.castbar = cb self.frame.castbar.cbbg = cbbg self.frame.castbar.ibg = ibg cb:SetScript("OnUpdate", function() SFrames.Player:CastbarOnUpdate() end) self:ApplyCastbarPosition() -- Hook events SFrames:RegisterEvent("SPELLCAST_START", function() self:CastbarStart(arg1, arg2) end) SFrames:RegisterEvent("SPELLCAST_STOP", function() self:CastbarStop() end) SFrames:RegisterEvent("SPELLCAST_FAILED", function() self:CastbarStop() end) SFrames:RegisterEvent("SPELLCAST_INTERRUPTED", function() self:CastbarStop() end) SFrames:RegisterEvent("SPELLCAST_DELAYED", function() self:CastbarDelayed(arg1) end) SFrames:RegisterEvent("SPELLCAST_CHANNEL_START", function() self:CastbarChannelStart(arg1, arg2) end) SFrames:RegisterEvent("SPELLCAST_CHANNEL_UPDATE", function() self:CastbarChannelUpdate(arg1) end) SFrames:RegisterEvent("SPELLCAST_CHANNEL_STOP", function() self:CastbarStop() end) end function SFrames.Player:ApplyCastbarPosition() local cb = self.frame.castbar if not cb then return end local db = SFramesDB or {} cb:ClearAllPoints() cb.cbbg:ClearAllPoints() cb.icon:ClearAllPoints() cb.ibg:ClearAllPoints() if db.castbarStandalone then cb:SetParent(UIParent) cb.cbbg:SetParent(UIParent) cb.ibg:SetParent(UIParent) local cbW = db.castbarWidth or 280 local cbH = db.castbarHeight or 20 cb:SetWidth(cbW) cb:SetHeight(cbH) cb:SetFrameStrata("HIGH") if SFrames.Movers and SFrames.Movers.ApplyPosition then SFrames.Movers:ApplyPosition("PlayerCastbar", cb, "BOTTOM", "SFramesPetHolder", "TOP", 0, 6) else local petHolder = _G["SFramesPetHolder"] if petHolder then cb:SetPoint("BOTTOM", petHolder, "TOP", 0, 6) else cb:SetPoint("BOTTOM", UIParent, "BOTTOM", 0, 120) end end cb.cbbg:SetPoint("TOPLEFT", cb, "TOPLEFT", -1, 1) cb.cbbg:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", 1, -1) cb.cbbg:SetFrameLevel(math.max(1, cb:GetFrameLevel() - 1)) cb.icon:SetWidth(cbH + 2) cb.icon:SetHeight(cbH + 2) cb.icon:SetPoint("RIGHT", cb, "LEFT", -4, 0) cb.ibg:SetPoint("TOPLEFT", cb.icon, "TOPLEFT", -1, 1) cb.ibg:SetPoint("BOTTOMRIGHT", cb.icon, "BOTTOMRIGHT", 1, -1) cb.ibg:SetFrameLevel(math.max(1, cb:GetFrameLevel() - 1)) if SFrames.Movers and SFrames.Movers.RegisterMover then SFrames.Movers:RegisterMover("PlayerCastbar", cb, "施法条", "BOTTOM", "SFramesPetHolder", "TOP", 0, 6, nil, { alwaysShowInLayout = true }) end if SFrames.Movers and SFrames.Movers.SetMoverAlwaysShow then SFrames.Movers:SetMoverAlwaysShow("PlayerCastbar", true) end else cb:SetParent(self.frame) cb.cbbg:SetParent(self.frame) cb.ibg:SetParent(self.frame) local cbH = SFrames.Config.castbarHeight cb:SetWidth(0) -- 清除独立模式的显式宽度,让双锚点自动计算 cb:SetHeight(cbH) cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", 0, 6) cb:SetPoint("BOTTOMLEFT", self.frame.portrait, "TOPLEFT", cbH + 6, 6) cb:SetFrameStrata("MEDIUM") cb.cbbg:SetPoint("TOPLEFT", cb, "TOPLEFT", -1, 1) cb.cbbg:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", 1, -1) cb.cbbg:SetFrameLevel(math.max(1, cb:GetFrameLevel() - 1)) cb.icon:SetWidth(cbH + 2) cb.icon:SetHeight(cbH + 2) cb.icon:SetPoint("RIGHT", cb, "LEFT", -4, 0) cb.ibg:SetPoint("TOPLEFT", cb.icon, "TOPLEFT", -1, 1) cb.ibg:SetPoint("BOTTOMRIGHT", cb.icon, "BOTTOMRIGHT", 1, -1) cb.ibg:SetFrameLevel(math.max(1, cb:GetFrameLevel() - 1)) if SFrames.Movers and SFrames.Movers.SetMoverAlwaysShow then SFrames.Movers:SetMoverAlwaysShow("PlayerCastbar", false) end end end function SFrames.Player:CastbarStart(spellName, duration) local cb = self.frame.castbar cb.casting = true cb.channeling = nil cb.fadeOut = nil cb.startTime = GetTime() cb.maxValue = duration / 1000 cb:SetMinMaxValues(0, cb.maxValue) cb:SetValue(0) cb.text:SetText(spellName) local texture local _UnitCastingInfo = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo) if _UnitCastingInfo then local _, _, _, tex = _UnitCastingInfo("player") texture = tex end if not texture and SFrames.castdb and UnitGUID then local guid = UnitGUID("player") if guid and SFrames.castdb[guid] and SFrames.castdb[guid].icon then texture = SFrames.castdb[guid].icon end end if (not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark") and SFrames.GetSpellIcon then texture = SFrames.GetSpellIcon(spellName) or texture end if texture then cb.icon:SetTexture(texture) cb.icon:Show() cb.ibg:Show() else cb.icon:Hide() cb.ibg:Hide() end local cbAlpha = tonumber((SFramesDB or {}).castbarAlpha) or 1 cb:SetAlpha(cbAlpha) cb.cbbg:SetAlpha(cbAlpha) if texture then cb.icon:SetAlpha(cbAlpha) cb.ibg:SetAlpha(cbAlpha) end cb:Show() cb.cbbg:Show() local lag = GetLatencySeconds() if lag > 0 and cb.maxValue > 0 then local barW = cb:GetWidth() local lagW = math.min((lag / cb.maxValue) * barW, barW * 0.5) if lagW >= 1 then cb.lagTex:ClearAllPoints() cb.lagTex:SetPoint("TOPRIGHT", cb, "TOPRIGHT", 0, 0) cb.lagTex:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", 0, 0) cb.lagTex:SetWidth(lagW) cb.lagTex:Show() else cb.lagTex:Hide() end else cb.lagTex:Hide() end end function SFrames.Player:CastbarChannelStart(duration, spellName) local cb = self.frame.castbar cb.casting = nil cb.channeling = true cb.fadeOut = nil cb.startTime = GetTime() cb.maxValue = duration / 1000 cb.endTime = cb.startTime + cb.maxValue cb:SetMinMaxValues(0, cb.maxValue) cb:SetValue(cb.maxValue) cb.text:SetText(spellName) local texture local _UnitChannelInfo = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo) if _UnitChannelInfo then local _, _, _, tex = _UnitChannelInfo("player") texture = tex end if not texture and SFrames.castdb and UnitGUID then local guid = UnitGUID("player") if guid and SFrames.castdb[guid] and SFrames.castdb[guid].icon then texture = SFrames.castdb[guid].icon end end if (not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark") and SFrames.GetSpellIcon then texture = SFrames.GetSpellIcon(spellName) or texture end if texture then cb.icon:SetTexture(texture) cb.icon:Show() cb.ibg:Show() else cb.icon:Hide() cb.ibg:Hide() end local cbAlpha = tonumber((SFramesDB or {}).castbarAlpha) or 1 cb:SetAlpha(cbAlpha) cb.cbbg:SetAlpha(cbAlpha) if texture then cb.icon:SetAlpha(cbAlpha) cb.ibg:SetAlpha(cbAlpha) end cb:Show() cb.cbbg:Show() local lag = GetLatencySeconds() if lag > 0 and cb.maxValue > 0 then local barW = cb:GetWidth() local lagW = math.min((lag / cb.maxValue) * barW, barW * 0.5) if lagW >= 1 then cb.lagTex:ClearAllPoints() cb.lagTex:SetPoint("TOPLEFT", cb, "TOPLEFT", 0, 0) cb.lagTex:SetPoint("BOTTOMLEFT", cb, "BOTTOMLEFT", 0, 0) cb.lagTex:SetWidth(lagW) cb.lagTex:Show() else cb.lagTex:Hide() end else cb.lagTex:Hide() end end function SFrames.Player:CastbarStop() local cb = self.frame.castbar cb.casting = nil cb.channeling = nil cb.fadeOut = true if cb.lagTex then cb.lagTex:Hide() end end function SFrames.Player:CastbarDelayed(delay) local cb = self.frame.castbar if cb.casting then cb.maxValue = cb.maxValue + (delay / 1000) cb:SetMinMaxValues(0, cb.maxValue) end end function SFrames.Player:CastbarChannelUpdate(remainingMs) local cb = self.frame.castbar if cb.channeling then cb.endTime = GetTime() + remainingMs / 1000 end end function SFrames.Player:CastbarOnUpdate() local cb = self.frame.castbar local db = SFramesDB or {} if cb.casting then local elapsed = GetTime() - cb.startTime if elapsed >= cb.maxValue then cb.casting = nil cb.fadeOut = true cb:SetValue(cb.maxValue) if db.castbarRainbow and cb.rainbowActive then UpdateRainbowProgress(cb, 1) end return end cb:SetValue(elapsed) cb.time:SetText(string.format("%.1f", math.max(cb.maxValue - elapsed, 0))) if db.castbarRainbow then if not cb.rainbowActive then cb:SetStatusBarColor(0, 0, 0, 0) cb.rainbowActive = true end UpdateRainbowProgress(cb, elapsed / cb.maxValue) elseif cb.rainbowActive then cb:SetStatusBarColor(1, 0.7, 0) if cb.rainbowTex then cb.rainbowTex:Hide() end cb.rainbowActive = nil end if not cb.icon:IsShown() then self:CastbarTryResolveIcon() end elseif cb.channeling then local timeRemaining = cb.endTime - GetTime() if timeRemaining <= 0 then cb.channeling = nil cb.fadeOut = true cb:SetValue(0) if db.castbarRainbow and cb.rainbowActive then UpdateRainbowProgress(cb, 0) end return end cb:SetValue(timeRemaining) cb.time:SetText(string.format("%.1f", timeRemaining)) if db.castbarRainbow then if not cb.rainbowActive then cb:SetStatusBarColor(0, 0, 0, 0) cb.rainbowActive = true end UpdateRainbowProgress(cb, timeRemaining / cb.maxValue) elseif cb.rainbowActive then cb:SetStatusBarColor(1, 0.7, 0) if cb.rainbowTex then cb.rainbowTex:Hide() end cb.rainbowActive = nil end if not cb.icon:IsShown() then self:CastbarTryResolveIcon() end elseif cb.fadeOut then local alpha = cb:GetAlpha() - 0.05 if alpha > 0 then cb:SetAlpha(alpha) cb.cbbg:SetAlpha(alpha) cb.icon:SetAlpha(alpha) cb.ibg:SetAlpha(alpha) else cb.fadeOut = nil if cb.rainbowActive then if cb.rainbowTex then cb.rainbowTex:Hide() end cb:SetStatusBarColor(1, 0.7, 0) cb.rainbowActive = nil end cb:Hide() cb.cbbg:Hide() cb.icon:Hide() cb.ibg:Hide() end end end function SFrames.Player:CastbarTryResolveIcon() local cb = self.frame.castbar local spellName = cb.text:GetText() local texture if SFrames.castdb and UnitGUID then local guid = UnitGUID("player") if guid and SFrames.castdb[guid] then local entry = SFrames.castdb[guid] if entry.icon and entry.icon ~= "Interface\\Icons\\INV_Misc_QuestionMark" then texture = entry.icon end end end if not texture and NanamiPlates and NanamiPlates.castDB and UnitGUID then local guid = UnitGUID("player") if guid and NanamiPlates.castDB[guid] then local entry = NanamiPlates.castDB[guid] if entry.icon and entry.icon ~= "Interface\\Icons\\INV_Misc_QuestionMark" then texture = entry.icon end end end if not texture and SFrames.GetSpellIcon then texture = SFrames.GetSpellIcon(spellName) end if texture then local cbAlpha = tonumber((SFramesDB or {}).castbarAlpha) or 1 cb.icon:SetTexture(texture) cb.icon:SetAlpha(cbAlpha) cb.ibg:SetAlpha(cbAlpha) cb.icon:Show() cb.ibg:Show() end end