Files
Nanami-UI/Units/Player.lua
2026-03-20 14:04:51 +08:00

1682 lines
59 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
if not (ShaguTweaks and ShaguTweaks.libpredict and ShaguTweaks.libpredict.UnitGetIncomingHeals) then
return 0, 0, 0
end
local libpredict = ShaguTweaks.libpredict
if libpredict.UnitGetIncomingHealsBreakdown then
local ok, total, mine, others = pcall(function()
return libpredict:UnitGetIncomingHealsBreakdown(unit, UnitName("player"))
end)
if ok then
total = math.max(0, tonumber(total) or 0)
mine = math.max(0, tonumber(mine) or 0)
others = math.max(0, tonumber(others) or 0)
return total, mine, others
end
end
local ok, amount = pcall(function()
return libpredict:UnitGetIncomingHeals(unit)
end)
if not ok then return 0, 0, 0 end
amount = tonumber(amount) or 0
if amount < 0 then amount = 0 end
return amount, 0, amount
end
local function Clamp(value, minValue, maxValue)
if value < minValue then
return minValue
end
if value > maxValue then
return maxValue
end
return value
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 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 frameScale = tonumber(db.playerFrameScale) or 1
frameScale = Clamp(frameScale, 0.7, 1.8)
return {
width = width,
height = height,
portraitWidth = portraitWidth,
healthHeight = healthHeight,
powerHeight = powerHeight,
nameFont = nameFont,
valueFont = valueFont,
scale = frameScale,
}
end
function SFrames.Player:ApplyConfig()
if not self.frame then return end
local cfg = self:GetConfig()
local f = self.frame
f:SetScale(cfg.scale)
f:SetWidth(cfg.width)
f:SetHeight(cfg.height)
if f.portrait then
f.portrait:SetWidth(cfg.portraitWidth)
f.portrait:SetHeight(cfg.height - 2)
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)
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.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", 0, -1)
f.power:SetPoint("TOPRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
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
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
local fontPath = SFrames:GetFont()
if f.nameText then
f.nameText:SetFont(fontPath, cfg.nameFont, outline)
end
if f.healthText then
f.healthText:SetFont(fontPath, cfg.valueFont, outline)
end
if f.powerText then
f.powerText:SetFont(fontPath, cfg.valueFont, outline)
end
if f.manaText then
local manaFont = cfg.valueFont - 1
if manaFont < 8 then manaFont = 8 end
f.manaText:SetFont(fontPath, manaFont, outline)
end
if f.zLetters then
for i = 1, 3 do
if f.zLetters[i] and f.zLetters[i].text then
f.zLetters[i].text:SetFont(fontPath, 8 + (i - 1) * 3, "OUTLINE")
end
end
end
self:UpdateAll()
end
function SFrames.Player:Initialize()
local f = CreateFrame("Button", "SFramesPlayerFrame", UIParent)
f:SetWidth(SFrames.Config.width)
f:SetHeight(SFrames.Config.height)
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PlayerFrame"] then
local pos = SFramesDB.Positions["PlayerFrame"]
f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs)
else
f:SetPoint("CENTER", UIParent, "CENTER", -200, -100)
end
local frameScale = (SFramesDB and type(SFramesDB.playerFrameScale) == "number") and SFramesDB.playerFrameScale or 1
f:SetScale(frameScale)
-- Make it movable
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()
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(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, "OVERLAY")
f.health.healPredMine:SetTexture(SFrames:GetTexture())
f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78)
f.health.healPredMine:Hide()
f.health.healPredOther = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredOther:SetTexture(SFrames:GetTexture())
f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5)
f.health.healPredOther: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(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)
-- 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.portrait, "TOPLEFT", -4, 4)
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()
if arg1 and mod(arg1, 2) == 0 and (not SFramesDB or SFramesDB.trainerReminder ~= false) then
self:ShowTrainerReminder(arg1)
end
end)
SFrames:RegisterEvent("TRAINER_SHOW", function()
SFrames.Player.trainerScannedThisVisit = nil
SFrames.Player.trainerShowPending = true
SFrames.Player.trainerRetryCount = 0
if not SFrames.Player.trainerRetryFrame then
SFrames.Player.trainerRetryFrame = CreateFrame("Frame")
end
SFrames.Player.trainerRetryFrame:SetScript("OnUpdate", function()
if not this.elapsed then this.elapsed = 0 end
this.elapsed = this.elapsed + arg1
if this.elapsed < 0.3 then return end
this.elapsed = 0
if SFrames.Player.trainerScannedThisVisit then
this:SetScript("OnUpdate", nil)
return
end
SFrames.Player.trainerRetryCount = (SFrames.Player.trainerRetryCount or 0) + 1
if SFrames.Player.trainerRetryCount > 10 then
SFrames.Player:ScanTrainer()
this:SetScript("OnUpdate", nil)
return
end
SFrames.Player:ScanTrainer()
end)
end)
SFrames:RegisterEvent("TRAINER_UPDATE", function()
if not SFrames.Player.trainerScannedThisVisit then
SFrames.Player:ScanTrainer()
end
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" 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()
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit(this.unit)
GameTooltip:Show()
end)
f:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
end
function SFrames.Player:HasSpellInBook(spellName)
local baseName = string.gsub(spellName, " 等级 %d+$", "")
baseName = string.gsub(baseName, " %d+级$", "")
local i = 1
while true do
local name = GetSpellName(i, BOOKTYPE_SPELL)
if not name then return false end
if name == spellName or name == baseName then return true end
i = i + 1
end
end
function SFrames.Player:GetSpellIcon(skillDisplayName)
local baseName = string.gsub(skillDisplayName, " %d+级$", "")
local altName1 = string.gsub(baseName, "", ":")
local altName2 = string.gsub(baseName, ":", "")
local i = 1
while true do
local name = GetSpellName(i, BOOKTYPE_SPELL)
if not name then break end
if name == baseName or name == altName1 or name == altName2 then
return GetSpellTexture(i, BOOKTYPE_SPELL)
end
i = i + 1
end
return nil
end
function SFrames.Player:ParseTrainerTooltipLevel(serviceIndex)
if not self.trainerScanTip then
local tt = CreateFrame("GameTooltip", "NanamiTrainerScanTip", nil, "GameTooltipTemplate")
tt:SetOwner(UIParent, "ANCHOR_NONE")
self.trainerScanTip = tt
end
local tt = self.trainerScanTip
tt:ClearLines()
if not tt.SetTrainerService then return nil end
tt:SetTrainerService(serviceIndex)
for j = 2, tt:NumLines() do
local textObj = getglobal("NanamiTrainerScanTipTextLeft" .. j)
if textObj then
local text = textObj:GetText()
if text then
local _, _, lvl = string.find(text, "需要等级%s*(%d+)")
if not lvl then
_, _, lvl = string.find(text, "Requires Level (%d+)")
end
if lvl then return tonumber(lvl) end
end
end
end
return nil
end
function SFrames.Player:FindSkillLevelInStaticData(classEn, skillName)
local staticData = SFrames.ClassSkillData and SFrames.ClassSkillData[classEn]
if not staticData then return nil end
local baseName = string.gsub(skillName, " %d+级$", "")
baseName = string.gsub(baseName, "(等级 %d+$", "")
baseName = string.gsub(baseName, "%s+$", "")
for level, skills in pairs(staticData) do
for _, s in ipairs(skills) do
local sBase = string.gsub(s, " %d+级$", "")
sBase = string.gsub(sBase, "(等级 %d+$", "")
sBase = string.gsub(sBase, "%s+$", "")
if baseName == sBase or skillName == s then
return level
end
end
end
local talentData = SFrames.TalentTrainerSkills and SFrames.TalentTrainerSkills[classEn]
if talentData then
for level, entries in pairs(talentData) do
for _, entry in ipairs(entries) do
local tBase = string.gsub(entry[1], " %d+级$", "")
if baseName == tBase or skillName == entry[1] then
return level
end
end
end
end
return nil
end
function SFrames.Player:ScanTrainer()
if self.scanningTrainer then return end
self.scanningTrainer = true
local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9"
if not GetNumTrainerServices then
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff6666训练师扫描失败: GetNumTrainerServices 不存在|r")
self.scanningTrainer = nil
return
end
if IsTradeskillTrainer and IsTradeskillTrainer() then
self.scanningTrainer = nil
return
end
local _, classEn = UnitClass("player")
if not classEn or not SFramesDB then self.scanningTrainer = nil return end
local cache = {}
local numServices = GetNumTrainerServices()
if not numServices or numServices == 0 then
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff6666训练师扫描: 未检测到技能列表 (0项),将在数据加载后重试|r")
self.scanningTrainer = nil
return
end
local hasLevelAPI = (GetTrainerServiceLevelReq ~= nil)
local noLevelCount = 0
local totalAdded = 0
local iconMissCount = 0
for i = 1, numServices do
local name, subText, serviceType = GetTrainerServiceInfo(i)
if name and subText and subText ~= "" then
local icon = GetTrainerServiceIcon and GetTrainerServiceIcon(i)
if (not icon or icon == "") and ClassTrainerFrame then
local skillButton = getglobal("ClassTrainerSkill" .. i)
if skillButton then
local iconTex = getglobal("ClassTrainerSkill" .. i .. "Icon")
if iconTex and iconTex.GetTexture then
icon = iconTex:GetTexture()
end
end
end
if not icon or icon == "" then
iconMissCount = iconMissCount + 1
end
local reqLevel
if hasLevelAPI then
reqLevel = GetTrainerServiceLevelReq(i)
end
if not reqLevel or reqLevel == 0 then
reqLevel = self:ParseTrainerTooltipLevel(i)
end
if not reqLevel or reqLevel == 0 then
local lookupName = name .. " " .. subText
reqLevel = self:FindSkillLevelInStaticData(classEn, lookupName)
if not reqLevel then
reqLevel = self:FindSkillLevelInStaticData(classEn, name)
end
end
if not reqLevel or reqLevel == 0 then
noLevelCount = noLevelCount + 1
end
if reqLevel and reqLevel > 0 then
local displayName = name .. " " .. subText
if not cache[reqLevel] then
cache[reqLevel] = {}
end
table.insert(cache[reqLevel], {
name = displayName,
icon = icon or "",
})
totalAdded = totalAdded + 1
end
end
end
if not SFramesDB.trainerCache then
SFramesDB.trainerCache = {}
end
SFramesDB.trainerCache[classEn] = cache
self.trainerScannedThisVisit = true
local levelCount = 0
for _ in pairs(cache) do levelCount = levelCount + 1 end
local iconOk = totalAdded - iconMissCount
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r 已从训练师更新技能数据(" .. levelCount .. " 个等级," .. totalAdded .. " 项技能," .. iconOk .. " 个图标)")
if noLevelCount > 0 then
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff9900有 " .. noLevelCount .. " 项技能无法确定等级要求,已跳过|r")
end
if iconMissCount > 0 then
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff9900有 " .. iconMissCount .. " 项技能未获取到图标|r")
end
self.scanningTrainer = nil
end
function SFrames.Player:ShowTrainerReminder(newLevel)
local _, classEn = UnitClass("player")
local classNames = {
WARRIOR = "战士", PALADIN = "圣骑士", HUNTER = "猎人",
ROGUE = "盗贼", PRIEST = "牧师", SHAMAN = "萨满祭司",
MAGE = "法师", WARLOCK = "术士", DRUID = "德鲁伊",
}
local className = classNames[classEn] or UnitClass("player")
local classFallbackIcons = {
WARRIOR = "Interface\\Icons\\Ability_Warrior_OffensiveStance",
PALADIN = "Interface\\Icons\\Spell_Holy_HolyBolt",
HUNTER = "Interface\\Icons\\Ability_Marksmanship",
ROGUE = "Interface\\Icons\\Ability_BackStab",
PRIEST = "Interface\\Icons\\Spell_Holy_HolyBolt",
SHAMAN = "Interface\\Icons\\Spell_Nature_Lightning",
MAGE = "Interface\\Icons\\Spell_Frost_IceStorm",
WARLOCK = "Interface\\Icons\\Spell_Shadow_DeathCoil",
DRUID = "Interface\\Icons\\Spell_Nature_Regeneration",
}
local fallbackIcon = classFallbackIcons[classEn] or "Interface\\Icons\\Trade_Engraving"
local allSkills = {}
local allIcons = {}
local talentSkills = {}
local usedCache = false
local classCache = SFramesDB and SFramesDB.trainerCache and SFramesDB.trainerCache[classEn]
if classCache then
local lowLevel = newLevel - 1
if lowLevel < 1 then lowLevel = 1 end
for lv = lowLevel, newLevel do
if classCache[lv] then
usedCache = true
for _, entry in ipairs(classCache[lv]) do
if not self:HasSpellInBook(entry.name) then
table.insert(allSkills, entry.name)
local ico = entry.icon
if not ico or ico == "" then
ico = self:GetSpellIcon(entry.name) or fallbackIcon
end
table.insert(allIcons, ico)
end
end
end
end
end
if not usedCache then
local baseSkills = SFrames.ClassSkillData and SFrames.ClassSkillData[classEn] and SFrames.ClassSkillData[classEn][newLevel]
if baseSkills then
for _, s in ipairs(baseSkills) do
table.insert(allSkills, s)
table.insert(allIcons, self:GetSpellIcon(s) or fallbackIcon)
end
end
local talentData = SFrames.TalentTrainerSkills and SFrames.TalentTrainerSkills[classEn] and SFrames.TalentTrainerSkills[classEn][newLevel]
if talentData then
for _, entry in ipairs(talentData) do
local displayName = entry[1]
local requiredSpell = entry[2]
if self:HasSpellInBook(requiredSpell) then
table.insert(allSkills, displayName)
table.insert(allIcons, self:GetSpellIcon(displayName) or fallbackIcon)
table.insert(talentSkills, displayName)
end
end
end
end
local mountQuest = SFrames.ClassMountQuests and SFrames.ClassMountQuests[classEn] and SFrames.ClassMountQuests[classEn][newLevel]
local skillCount = table.getn(allSkills)
if skillCount == 0 and not mountQuest then
return
end
local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9"
SFrames:Print(string.format("已达到 %d 级!你的%s训练师有新技能可以学习。", newLevel, className))
if skillCount > 0 then
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r 可学习的技能(共 " .. skillCount .. " 项):")
local line = " "
local lineCount = 0
for i = 1, skillCount do
if lineCount > 0 then line = line .. ", " end
line = line .. "|cffffd100" .. allSkills[i] .. "|r"
lineCount = lineCount + 1
if i == skillCount or lineCount == 4 then
DEFAULT_CHAT_FRAME:AddMessage(line)
line = " "
lineCount = 0
end
end
end
if not usedCache and table.getn(talentSkills) > 0 then
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cff00ff00(天赋)|r " .. table.concat(talentSkills, ", "))
end
if mountQuest then
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffffff00★|r " .. mountQuest)
end
local bannerMsg = string.format("Lv.%d - %s训练师有 %d 项新技能可学习", newLevel, className, skillCount)
if mountQuest then
bannerMsg = bannerMsg .. " + " .. mountQuest
end
if skillCount == 0 and not mountQuest then
bannerMsg = string.format("Lv.%d - 前往%s训练师查看可学技能", newLevel, className)
elseif skillCount == 0 and mountQuest then
bannerMsg = string.format("Lv.%d - %s", newLevel, mountQuest)
end
UIErrorsFrame:AddMessage(bannerMsg, 1.0, 0.82, 0.0, 1, 5)
PlaySound("LEVELUP")
if not self.trainerReminderFrame then
local fr = CreateFrame("Frame", "NanamiTrainerReminder", UIParent)
fr:SetWidth(440)
fr:SetHeight(106)
fr:SetPoint("TOP", UIParent, "TOP", 0, -120)
fr:SetFrameStrata("DIALOG")
local bg = fr:CreateTexture(nil, "BACKGROUND")
bg:SetAllPoints(fr)
bg:SetTexture(0, 0, 0, 0.78)
local border = fr:CreateTexture(nil, "BORDER")
border:SetPoint("TOPLEFT", fr, "TOPLEFT", -1, 1)
border:SetPoint("BOTTOMRIGHT", fr, "BOTTOMRIGHT", 1, -1)
border:SetTexture(1, 0.82, 0, 0.3)
local icon = fr:CreateTexture(nil, "ARTWORK")
icon:SetWidth(36)
icon:SetHeight(36)
icon:SetPoint("TOPLEFT", fr, "TOPLEFT", 10, -6)
icon:SetTexture("Interface\\Icons\\INV_Misc_Book_11")
fr.icon = icon
local title = fr:CreateFontString(nil, "OVERLAY", "GameFontNormal")
title:SetPoint("TOPLEFT", icon, "TOPRIGHT", 10, -2)
title:SetPoint("RIGHT", fr, "RIGHT", -10, 0)
title:SetJustifyH("LEFT")
title:SetTextColor(1, 0.82, 0)
fr.title = title
local subtitle = fr:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
subtitle:SetPoint("TOPLEFT", icon, "TOPRIGHT", 10, -18)
subtitle:SetPoint("RIGHT", fr, "RIGHT", -10, 0)
subtitle:SetJustifyH("LEFT")
subtitle:SetTextColor(0.75, 0.75, 0.75)
fr.subtitle = subtitle
fr.skillIcons = {}
fr.skillBorders = {}
local maxIcons = 13
for idx = 1, maxIcons do
local bdr = fr:CreateTexture(nil, "BORDER")
bdr:SetWidth(30)
bdr:SetHeight(30)
bdr:SetPoint("TOPLEFT", fr, "TOPLEFT", 9 + (idx - 1) * 32, -45)
bdr:SetTexture(1, 0.82, 0, 0.25)
bdr:Hide()
fr.skillBorders[idx] = bdr
local si = fr:CreateTexture(nil, "ARTWORK")
si:SetWidth(28)
si:SetHeight(28)
si:SetPoint("CENTER", bdr, "CENTER", 0, 0)
si:SetTexCoord(0.07, 0.93, 0.07, 0.93)
si:Hide()
fr.skillIcons[idx] = si
end
local detail = fr:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
detail:SetPoint("BOTTOMLEFT", fr, "BOTTOMLEFT", 10, 8)
detail:SetPoint("RIGHT", fr, "RIGHT", -10, 0)
detail:SetJustifyH("LEFT")
detail:SetTextColor(0.85, 0.85, 0.85)
fr.detail = detail
fr:SetAlpha(0)
fr:Hide()
self.trainerReminderFrame = fr
end
local fr = self.trainerReminderFrame
for idx = 1, 13 do
fr.skillIcons[idx]:Hide()
fr.skillBorders[idx]:Hide()
end
local iconCount = 0
for i = 1, skillCount do
if iconCount >= 13 then break end
iconCount = iconCount + 1
fr.skillIcons[iconCount]:SetTexture(allIcons[i])
fr.skillIcons[iconCount]:Show()
fr.skillBorders[iconCount]:Show()
end
if mountQuest and iconCount < 13 then
iconCount = iconCount + 1
fr.skillIcons[iconCount]:SetTexture("Interface\\Icons\\Spell_Nature_Swiftness")
fr.skillIcons[iconCount]:Show()
fr.skillBorders[iconCount]:Show()
end
fr.title:SetText(string.format("已达到 |cffffffff%d|r 级 — %s训练师有新技能", newLevel, className))
if skillCount > 0 or mountQuest then
local preview = ""
if skillCount > 0 then
preview = "|cffffd100" .. allSkills[1] .. "|r"
if skillCount > 1 then preview = preview .. ", |cffffd100" .. allSkills[2] .. "|r" end
if skillCount > 2 then preview = preview .. ", |cffffd100" .. allSkills[3] .. "|r" end
if skillCount > 3 then preview = preview .. "" .. skillCount .. "" end
end
if mountQuest then
if preview ~= "" then preview = preview .. " | " end
preview = preview .. "|cffffff00" .. mountQuest .. "|r"
end
fr.subtitle:SetText(preview)
fr.detail:SetText("详见聊天窗口")
else
fr.subtitle:SetText("")
fr.detail:SetText("前往职业训练师查看可学习的技能")
end
if iconCount > 0 then
fr:SetHeight(106)
else
fr:SetHeight(80)
end
fr:Show()
fr:SetAlpha(0)
fr.fadeState = "in"
fr.fadeTimer = 0
fr.holdTimer = 0
fr:SetScript("OnUpdate", function()
local dt = arg1
if fr.fadeState == "in" then
fr.fadeTimer = fr.fadeTimer + dt
local a = fr.fadeTimer / 0.5
if a >= 1 then
a = 1
fr.fadeState = "hold"
end
fr:SetAlpha(a)
elseif fr.fadeState == "hold" then
fr.holdTimer = fr.holdTimer + dt
if fr.holdTimer >= 8 then
fr.fadeState = "out"
fr.fadeTimer = 0
end
elseif fr.fadeState == "out" then
fr.fadeTimer = fr.fadeTimer + dt
local a = 1 - fr.fadeTimer / 1.0
if a <= 0 then
a = 0
fr:Hide()
fr:SetScript("OnUpdate", nil)
end
fr:SetAlpha(a)
end
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
self.frame.portrait:SetUnit("player")
self.frame.portrait:SetCamera(0)
self.frame.portrait:SetPosition(-1.0, 0, 0)
-- 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 (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)
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
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")
self.frame.health:SetMinMaxValues(0, maxHp)
self.frame.health:SetValue(hp)
if maxHp > 0 then
self.frame.healthText:SetText(hp .. " / " .. maxHp)
else
self.frame.healthText:SetText(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) then return end
local predMine = self.frame.health.healPredMine
local predOther = self.frame.health.healPredOther
local function HidePredictions()
predMine:Hide()
predOther:Hide()
end
local hp = UnitHealth("player") or 0
local maxHp = UnitHealthMax("player") or 0
if maxHp <= 0 or hp >= maxHp or UnitIsDeadOrGhost("player") then
HidePredictions()
return
end
local _, mineIncoming, othersIncoming = GetIncomingHeals("player")
local missing = maxHp - hp
if missing <= 0 then
HidePredictions()
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 then
HidePredictions()
return
end
local barWidth = self.frame.health:GetWidth() or 0
if barWidth <= 0 then
HidePredictions()
return
end
local currentWidth = math.floor((hp / maxHp) * barWidth + 0.5)
if currentWidth < 0 then currentWidth = 0 end
if currentWidth > barWidth then currentWidth = barWidth end
local availableWidth = barWidth - currentWidth
if availableWidth <= 0 then
HidePredictions()
return
end
local mineWidth = math.floor((mineShown / maxHp) * barWidth + 0.5)
local otherWidth = math.floor((otherShown / maxHp) * barWidth + 0.5)
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
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: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:Show()
else
predOther: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
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(power .. " / " .. maxPower)
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 .. "% " .. 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 16 Buff Slots
self.frame.buffs = {}
self.frame.debuffs = {}
local size = 24
local spacing = 2
local rowSpacing = 1
local buffsPerRow = 9
for i = 1, 16 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
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 > 16 then break end
local b = self.frame.buffs[slotIdx]
local texture = GetPlayerBuffTexture(buffIndex)
if texture then
b.icon:SetTexture(texture)
b.buffIndex = buffIndex
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, 16 do
self.frame.buffs[j]:Hide()
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
end
--------------------------------------------------------------------------------
-- Player Castbar
--------------------------------------------------------------------------------
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)
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)
-- 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: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 texture then
cb.icon:SetTexture(texture)
cb.icon:Show()
cb.ibg:Show()
else
cb.icon:Hide()
cb.ibg:Hide()
end
cb:SetAlpha(1)
cb.cbbg:SetAlpha(1)
if texture then
cb.icon:SetAlpha(1)
cb.ibg:SetAlpha(1)
end
cb:Show()
cb.cbbg:Show()
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 texture then
cb.icon:SetTexture(texture)
cb.icon:Show()
cb.ibg:Show()
else
cb.icon:Hide()
cb.ibg:Hide()
end
cb:SetAlpha(1)
cb.cbbg:SetAlpha(1)
if texture then
cb.icon:SetAlpha(1)
cb.ibg:SetAlpha(1)
end
cb:Show()
cb.cbbg:Show()
end
function SFrames.Player:CastbarStop()
local cb = self.frame.castbar
cb.casting = nil
cb.channeling = nil
cb.fadeOut = true
-- keep showing for a short fade out
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(delay)
local cb = self.frame.castbar
if cb.channeling then
local add = delay / 1000
cb.maxValue = cb.maxValue + add
cb.endTime = cb.endTime + add
cb:SetMinMaxValues(0, cb.maxValue)
end
end
function SFrames.Player:CastbarOnUpdate()
local cb = self.frame.castbar
if cb.casting then
local elapsed = GetTime() - cb.startTime
if elapsed >= cb.maxValue then
cb.casting = nil
cb.fadeOut = true
cb:SetValue(cb.maxValue)
return
end
cb:SetValue(elapsed)
cb.time:SetText(string.format("%.1f", math.max(cb.maxValue - elapsed, 0)))
elseif cb.channeling then
local timeRemaining = cb.endTime - GetTime()
if timeRemaining <= 0 then
cb.channeling = nil
cb.fadeOut = true
cb:SetValue(0)
return
end
cb:SetValue(timeRemaining)
cb.time:SetText(string.format("%.1f", timeRemaining))
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
cb:Hide()
cb.cbbg:Hide()
cb.icon:Hide()
cb.ibg:Hide()
end
end
end