Files
Nanami-UI/Units/Raid.lua
2026-04-09 09:46:47 +08:00

1326 lines
50 KiB
Lua

SFrames.Raid = {}
local _A = SFrames.ActiveTheme
local RAID_FRAME_WIDTH = 60
local RAID_FRAME_HEIGHT = 40
local GROUP_PADDING = 8
local UNIT_PADDING = 2
local RAID_UNIT_LOOKUP = {}
for i = 1, 40 do RAID_UNIT_LOOKUP["raid" .. i] = true end
-- Pre-allocated tables reused every UpdateAuras call to avoid per-call garbage
local _foundIndicators = { [1] = false, [2] = false, [3] = false, [4] = false }
local _debuffColor = { r = 0, g = 0, b = 0 }
-- Module-level helper: match aura name against a list (no closure allocation)
local function MatchesList(auraName, list)
for _, name in ipairs(list) do
if string.find(auraName, name) then
return true
end
end
return false
end
-- Module-level helper: get buff name via SuperWoW aura ID or tooltip scan
local function RaidGetBuffName(unit, index)
if SFrames.superwow_active and SpellInfo then
local texture, auraID = UnitBuff(unit, index)
if auraID and SpellInfo then
local spellName = SpellInfo(auraID)
if spellName and spellName ~= "" then
return spellName, texture
end
end
end
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
SFrames.Tooltip:SetUnitBuff(unit, index)
local buffName = SFramesScanTooltipTextLeft1:GetText()
SFrames.Tooltip:Hide()
return buffName, UnitBuff(unit, index)
end
local function RaidGetDebuffName(unit, index)
if SFrames.superwow_active and SpellInfo then
local texture, count, dtype, auraID = UnitDebuff(unit, index)
if auraID and SpellInfo then
local spellName = SpellInfo(auraID)
if spellName and spellName ~= "" then
return spellName, texture, count, dtype
end
end
end
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
SFrames.Tooltip:SetUnitDebuff(unit, index)
local debuffName = SFramesScanTooltipTextLeft1:GetText()
SFrames.Tooltip:Hide()
local texture, count, dtype = UnitDebuff(unit, index)
return debuffName, texture, count, dtype
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
function SFrames.Raid:GetMetrics()
local db = SFramesDB or {}
local width = tonumber(db.raidFrameWidth) or RAID_FRAME_WIDTH
width = math.max(30, math.min(150, width))
local height = tonumber(db.raidFrameHeight) or RAID_FRAME_HEIGHT
height = math.max(20, math.min(80, height))
local healthHeight = tonumber(db.raidHealthHeight) or math.floor((height - 3) * 0.8)
healthHeight = math.max(10, math.min(height - 6, healthHeight))
local powerHeight = tonumber(db.raidPowerHeight) or (height - healthHeight - 3)
powerHeight = math.max(0, math.min(height - 3, powerHeight))
if not db.raidShowPower then
powerHeight = 0
healthHeight = height - 2
end
local gradientStyle = SFrames:IsGradientStyle()
local availablePowerWidth = width - 2
if availablePowerWidth < 20 then
availablePowerWidth = 20
end
local rawPowerWidth = tonumber(db.raidPowerWidth)
local legacyFullWidth = tonumber(db.raidFrameWidth) or width
local defaultPowerWidth = gradientStyle and width or availablePowerWidth
local maxPowerWidth = gradientStyle and width or availablePowerWidth
local powerWidth
if gradientStyle then
-- 渐变风格:能量条始终与血条等宽(全宽)
powerWidth = width
elseif not rawPowerWidth
or math.abs(rawPowerWidth - legacyFullWidth) < 0.5
or math.abs(rawPowerWidth - availablePowerWidth) < 0.5 then
powerWidth = defaultPowerWidth
else
powerWidth = rawPowerWidth
end
powerWidth = Clamp(math.floor(powerWidth + 0.5), 20, maxPowerWidth)
local powerOffsetX = Clamp(math.floor((tonumber(db.raidPowerOffsetX) or 0) + 0.5), -120, 120)
local powerOffsetY = Clamp(math.floor((tonumber(db.raidPowerOffsetY) or 0) + 0.5), -80, 80)
local hgap = tonumber(db.raidHorizontalGap) or UNIT_PADDING
local vgap = tonumber(db.raidVerticalGap) or UNIT_PADDING
local groupGap = tonumber(db.raidGroupGap) or GROUP_PADDING
local nameFont = tonumber(db.raidNameFontSize) or 10
local valueFont = tonumber(db.raidValueFontSize) or 9
local healthFont = tonumber(db.raidHealthFontSize) or valueFont
local powerFont = tonumber(db.raidPowerFontSize) or valueFont
return {
width = width,
height = height,
healthHeight = healthHeight,
powerHeight = powerHeight,
powerWidth = powerWidth,
powerOffsetX = powerOffsetX,
powerOffsetY = powerOffsetY,
powerOnTop = db.raidPowerOnTop == true,
horizontalGap = hgap,
verticalGap = vgap,
groupGap = groupGap,
nameFont = nameFont,
valueFont = valueFont,
healthFont = healthFont,
powerFont = powerFont,
healthTexture = SFrames:ResolveBarTexture("raidHealthTexture", "barTexture"),
powerTexture = SFrames:ResolveBarTexture("raidPowerTexture", "barTexture"),
showPower = db.raidShowPower ~= false,
}
end
function SFrames.Raid:ApplyFrameStyle(frame, metrics)
if not frame then return end
frame:SetWidth(metrics.width)
frame:SetHeight(metrics.height)
if frame.health then
frame.health:ClearAllPoints()
frame.health:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1)
frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1)
frame.health:SetHeight(metrics.healthHeight)
end
if frame.healthBGFrame then
frame.healthBGFrame:ClearAllPoints()
frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", -1, 1)
frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 1, -1)
end
local bgA = (SFramesDB and type(SFramesDB.raidBgAlpha) == "number") and SFramesDB.raidBgAlpha or 0.9
local A = SFrames.ActiveTheme
if A and A.panelBg and bgA < 0.89 then
if frame.healthBGFrame and frame.healthBGFrame.SetBackdropColor then
frame.healthBGFrame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], bgA)
end
if frame.powerBGFrame and frame.powerBGFrame.SetBackdropColor then
frame.powerBGFrame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], bgA)
end
end
if frame.power then
if metrics.showPower then
frame.power:Show()
if frame.powerBGFrame then frame.powerBGFrame:Show() end
frame.power:ClearAllPoints()
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -1 + metrics.powerOffsetY)
frame.power:SetWidth(metrics.powerWidth)
frame.power:SetHeight(metrics.powerHeight)
else
frame.power:Hide()
if frame.powerBGFrame then frame.powerBGFrame:Hide() end
end
end
SFrames:ApplyStatusBarTexture(frame.health, "raidHealthTexture", "barTexture")
SFrames:ApplyStatusBarTexture(frame.power, "raidPowerTexture", "barTexture")
if frame.health and frame.power then
local healthLevel = frame:GetFrameLevel() + 2
local powerLevel = metrics.powerOnTop and (healthLevel + 1) or (healthLevel - 1)
frame.health:SetFrameLevel(healthLevel)
frame.power:SetFrameLevel(powerLevel)
end
SFrames:ApplyConfiguredUnitBackdrop(frame, "raid")
if frame.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.healthBGFrame, "raid") end
if frame.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.powerBGFrame, "raid") end
SetTextureIfPresent(frame.health and frame.health.bg, metrics.healthTexture)
SetTextureIfPresent(frame.health and frame.health.healPredMine, metrics.healthTexture)
SetTextureIfPresent(frame.health and frame.health.healPredOther, metrics.healthTexture)
SetTextureIfPresent(frame.health and frame.health.healPredOver, metrics.healthTexture)
SetTextureIfPresent(frame.power and frame.power.bg, metrics.powerTexture)
-- Gradient style preset
if SFrames:IsGradientStyle() then
-- Strip backdrops
SFrames:ClearBackdrop(frame)
SFrames:ClearBackdrop(frame.healthBGFrame)
SFrames:ClearBackdrop(frame.powerBGFrame)
-- Health bar full width
if frame.health then
frame.health:ClearAllPoints()
frame.health:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
frame.health:SetHeight(metrics.healthHeight)
end
-- Power bar full width
if frame.power and metrics.showPower then
frame.power:ClearAllPoints()
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -1 + metrics.powerOffsetY)
frame.power:SetWidth(metrics.powerWidth)
frame.power:SetHeight(metrics.powerHeight)
end
-- Apply gradient overlays
SFrames:ApplyGradientStyle(frame.health)
SFrames:ApplyGradientStyle(frame.power)
-- Flush BG frames
if frame.healthBGFrame then
frame.healthBGFrame:ClearAllPoints()
frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", 0, 0)
frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
end
if frame.powerBGFrame then
frame.powerBGFrame:ClearAllPoints()
frame.powerBGFrame:SetPoint("TOPLEFT", frame.power, "TOPLEFT", 0, 0)
frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 0, 0)
end
-- Hide bar backgrounds (transparent)
if frame.healthBGFrame then frame.healthBGFrame:Hide() end
if frame.powerBGFrame then frame.powerBGFrame:Hide() end
if frame.health and frame.health.bg then frame.health.bg:Hide() end
if frame.power and frame.power.bg then frame.power.bg:Hide() end
else
SFrames:RemoveGradientStyle(frame.health)
SFrames:RemoveGradientStyle(frame.power)
-- Restore bar backgrounds
if frame.healthBGFrame then frame.healthBGFrame:Show() end
if frame.powerBGFrame then frame.powerBGFrame:Show() end
if frame.health and frame.health.bg then frame.health.bg:Show() end
if frame.power and frame.power.bg then frame.power.bg:Show() end
end
if frame.nameText then
SFrames:ApplyFontString(frame.nameText, metrics.nameFont, "raidNameFontKey", "fontKey")
end
if frame.healthText then
SFrames:ApplyFontString(frame.healthText, metrics.healthFont, "raidHealthFontKey", "fontKey")
end
if frame.powerText then
SFrames:ApplyFontString(frame.powerText, metrics.powerFont, "raidPowerFontKey", "fontKey")
end
end
function SFrames.Raid:ApplyLayout()
if not (self.parent and self._framesBuilt) then return end
local metrics = self:GetMetrics()
self.metrics = metrics
local mode = (SFramesDB and SFramesDB.raidLayout) or "horizontal"
local showLabel = SFramesDB and SFramesDB.raidShowGroupLabel ~= false
local LABEL_H = 16 -- px above each column (horizontal mode)
local LABEL_W = 16 -- px left of each row (vertical mode)
for g = 1, 8 do
local groupParent = self.groups[g]
groupParent:ClearAllPoints()
if mode == "horizontal" then
local extraTop = showLabel and LABEL_H or 0
if g == 1 then
groupParent:SetPoint("TOPLEFT", self.parent, "TOPLEFT", 0, 0)
else
groupParent:SetPoint("TOPLEFT", self.groups[g-1], "TOPRIGHT", metrics.groupGap, 0)
end
groupParent:SetWidth(metrics.width)
groupParent:SetHeight(extraTop + (metrics.height * 5) + (metrics.verticalGap * 4))
else
local extraLeft = showLabel and LABEL_W or 0
if g == 1 then
groupParent:SetPoint("TOPLEFT", self.parent, "TOPLEFT", 0, 0)
else
groupParent:SetPoint("TOPLEFT", self.groups[g-1], "BOTTOMLEFT", 0, -metrics.groupGap)
end
groupParent:SetWidth(extraLeft + (metrics.width * 5) + (metrics.horizontalGap * 4))
groupParent:SetHeight(metrics.height)
end
-- Position label; visibility is controlled per-group in UpdateAll
local lbl = self.groupLabels and self.groupLabels[g]
if lbl then
lbl:ClearAllPoints()
if showLabel then
if mode == "horizontal" then
-- Place label at the very top of the column, centered horizontally
-- Frames start at -LABEL_H, so the strip [0, -LABEL_H] is exclusively for the label
lbl:SetPoint("TOP", groupParent, "TOP", 0, -2)
else
-- Vertically centered on the left strip, centered horizontally within it
lbl:SetPoint("CENTER", groupParent, "LEFT", LABEL_W / 2, 0)
end
else
lbl:Hide()
end
end
for i = 1, 5 do
local index = (g - 1) * 5 + i
local f = self.frames[index].frame
f:ClearAllPoints()
if mode == "horizontal" then
local extraTop = showLabel and LABEL_H or 0
if i == 1 then
-- Frames start below the label strip
f:SetPoint("TOPLEFT", groupParent, "TOPLEFT", 0, -extraTop)
else
f:SetPoint("TOPLEFT", self.frames[index-1].frame, "BOTTOMLEFT", 0, -metrics.verticalGap)
end
else
local extraLeft = showLabel and LABEL_W or 0
if i == 1 then
f:SetPoint("TOPLEFT", groupParent, "TOPLEFT", extraLeft, 0)
else
f:SetPoint("TOPLEFT", self.frames[index-1].frame, "TOPRIGHT", metrics.horizontalGap, 0)
end
end
end
end
local extraTop = showLabel and LABEL_H or 0
local extraLeft = showLabel and LABEL_W or 0
if mode == "horizontal" then
self.parent:SetWidth((metrics.width * 8) + (metrics.groupGap * 7))
self.parent:SetHeight(extraTop + (metrics.height * 5) + (metrics.verticalGap * 4))
else
self.parent:SetWidth(extraLeft + (metrics.width * 5) + (metrics.horizontalGap * 4))
self.parent:SetHeight((metrics.height * 8) + (metrics.groupGap * 7))
end
end
function SFrames.Raid:SavePosition()
if not (self.parent and SFramesDB) then return end
if not SFramesDB.Positions then SFramesDB.Positions = {} end
local point, _, relativePoint, xOfs, yOfs = self.parent:GetPoint()
if point and relativePoint then
SFramesDB.Positions["RaidFrame"] = {
point = point,
relativePoint = relativePoint,
xOfs = xOfs or 0,
yOfs = yOfs or 0,
}
end
end
function SFrames.Raid:ApplyPosition()
if not self.parent then return end
self.parent:ClearAllPoints()
local pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["RaidFrame"]
if pos and pos.point and pos.relativePoint and type(pos.xOfs) == "number" and type(pos.yOfs) == "number" then
self.parent:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs)
else
self.parent:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 15, -200)
end
end
local function TryDropCursorOnUnit(unit)
if not unit or not UnitExists(unit) then return false end
if not CursorHasItem or not CursorHasItem() then return false end
if not DropItemOnUnit then return false end
local ok = pcall(DropItemOnUnit, unit)
return not CursorHasItem()
end
function SFrames.Raid:Initialize()
self.frames = {}
self.groups = {}
self._framesBuilt = false
if not SFramesDB then SFramesDB = {} end
local parent = CreateFrame("Frame", "SFramesRaidParent", UIParent)
local frameScale = (SFramesDB and type(SFramesDB.raidFrameScale) == "number") and SFramesDB.raidFrameScale or 1
parent:SetScale(frameScale)
self.parent = parent
self:ApplyPosition()
parent:SetMovable(true)
self.groupLabels = {}
for g = 1, 8 do
local groupParent = CreateFrame("Frame", "SFramesRaidGroup"..g, parent)
self.groups[g] = groupParent
local lbl = groupParent:CreateFontString(nil, "OVERLAY")
lbl:SetFont(SFrames:GetFont(), 9, "OUTLINE")
lbl:SetTextColor(0.85, 0.78, 0.92)
lbl:SetShadowColor(0, 0, 0, 1)
lbl:SetShadowOffset(1, -1)
lbl:SetText("小队 " .. g)
lbl:Hide()
self.groupLabels[g] = lbl
end
SFrames:RegisterEvent("RAID_ROSTER_UPDATE", function() self:UpdateAll() end)
SFrames:RegisterEvent("PARTY_LEADER_CHANGED", function() self:UpdateAll() end)
SFrames:RegisterEvent("PLAYER_TARGET_CHANGED", function() self:UpdateTargetHighlight() end)
SFrames:RegisterEvent("UNIT_HEALTH", function() if RAID_UNIT_LOOKUP[arg1] then self:UpdateHealth(arg1) end end)
SFrames:RegisterEvent("UNIT_MAXHEALTH", function() if RAID_UNIT_LOOKUP[arg1] then self:UpdateHealth(arg1) end end)
SFrames:RegisterEvent("UNIT_MANA", function() if RAID_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
SFrames:RegisterEvent("UNIT_MAXMANA", function() if RAID_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
SFrames:RegisterEvent("UNIT_AURA", function() if RAID_UNIT_LOOKUP[arg1] then self:UpdateAuras(arg1) end end)
SFrames:RegisterEvent("RAID_TARGET_UPDATE", function() self:UpdateRaidIcons() end)
self:UpdateAll()
if SFrames.Movers and SFrames.Movers.RegisterMover and self.parent then
SFrames.Movers:RegisterMover("RaidFrame", self.parent, "团队",
"TOPLEFT", "UIParent", "TOPLEFT", 15, -200)
end
end
function SFrames.Raid:EnsureFrames()
if self._framesBuilt then return end
self._framesBuilt = true
for i = 1, 40 do
local unit = "raid" .. i
local groupIndex = math.ceil(i / 5)
local groupParent = self.groups[groupIndex]
local f = CreateFrame("Button", "SFramesRaidFrame"..i, groupParent)
f.id = i
f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function()
if IsAltKeyDown() or SFrames.isUnlocked then
SFrames.Raid.parent:StartMoving()
end
end)
f:SetScript("OnDragStop", function()
SFrames.Raid.parent:StopMovingOrSizing()
SFrames.Raid:SavePosition()
end)
f:SetScript("OnClick", function()
if arg1 == "LeftButton" then
if IsShiftKeyDown() and SFrames.Focus and SFrames.Focus.SetFromUnit then
pcall(SFrames.Focus.SetFromUnit, SFrames.Focus, this.unit)
return
end
if TryDropCursorOnUnit(this.unit) then return end
if SpellIsTargeting and SpellIsTargeting() then
SpellTargetUnit(this.unit)
return
end
TargetUnit(this.unit)
elseif arg1 == "RightButton" then
if UnitExists(this.unit) then
GameTooltip:Hide()
if not SFrames.Raid.dropDown then
SFrames.Raid.dropDown = CreateFrame("Frame", "SFramesRaidDropDown", UIParent, "UIDropDownMenuTemplate")
SFrames.Raid.dropDown.displayMode = "MENU"
SFrames.Raid.dropDown.initialize = function()
local dd = SFrames.Raid.dropDown
local unit = dd.unit
local name = dd.name
if not unit or not name then return end
local info = {}
info.text = name
info.isTitle = 1
info.notCheckable = 1
UIDropDownMenu_AddButton(info)
if not UnitIsUnit(unit, "player") then
info = {}
info.text = WHISPER or "密语"
info.notCheckable = 1
info.func = function() ChatFrame_SendTell(name) end
UIDropDownMenu_AddButton(info)
info = {}
info.text = INSPECT or "检查"
info.notCheckable = 1
info.func = function() InspectUnit(unit) end
UIDropDownMenu_AddButton(info)
info = {}
info.text = TRADE or "交易"
info.notCheckable = 1
info.func = function() InitiateTrade(unit) end
UIDropDownMenu_AddButton(info)
info = {}
info.text = FOLLOW or "跟随"
info.notCheckable = 1
info.func = function() FollowUnit(unit) end
UIDropDownMenu_AddButton(info)
end
if (IsRaidLeader and IsRaidLeader()) or (IsRaidOfficer and IsRaidOfficer()) then
if not UnitIsUnit(unit, "player") then
info = {}
info.text = "提升为助手"
info.notCheckable = 1
info.func = function() PromoteToAssistant(name) end
UIDropDownMenu_AddButton(info)
info = {}
info.text = "移出团队"
info.notCheckable = 1
info.func = function() UninviteByName(name) end
UIDropDownMenu_AddButton(info)
end
end
info = {}
info.text = CANCEL or "取消"
info.notCheckable = 1
UIDropDownMenu_AddButton(info)
end
end
SFrames.Raid.dropDown.unit = this.unit
SFrames.Raid.dropDown.name = UnitName(this.unit)
ToggleDropDownMenu(1, nil, SFrames.Raid.dropDown, "cursor")
end
end
end)
f:SetScript("OnReceiveDrag", function()
if TryDropCursorOnUnit(this.unit) then return end
if SpellIsTargeting and SpellIsTargeting() then
SpellTargetUnit(this.unit)
end
end)
f:SetScript("OnEnter", function()
if SetMouseoverUnit then SetMouseoverUnit(this.unit) end
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit(this.unit)
end)
f:SetScript("OnLeave", function()
if SetMouseoverUnit then SetMouseoverUnit() end
GameTooltip:Hide()
end)
f.unit = unit
-- Health Bar
f.health = SFrames:CreateStatusBar(f, "SFramesRaidFrame"..i.."Health")
local hbg = CreateFrame("Frame", nil, f)
hbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(hbg)
f.healthBGFrame = hbg
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, "SFramesRaidFrame"..i.."Power")
f.power:SetMinMaxValues(0, 100)
local powerbg = CreateFrame("Frame", nil, f)
powerbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(powerbg)
f.powerBGFrame = powerbg
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)
-- Target Highlight
f.targetHighlight = f:CreateTexture(nil, "OVERLAY")
f.targetHighlight:SetPoint("TOPLEFT", f, "TOPLEFT", -2, 2)
f.targetHighlight:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 2, -2)
f.targetHighlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
f.targetHighlight:SetBlendMode("ADD")
f.targetHighlight:SetVertexColor(1, 1, 1, 0.6)
f.targetHighlight:Hide()
-- Texts
f.nameText = SFrames:CreateFontString(f.health, 10, "CENTER")
f.nameText:SetPoint("TOP", f.health, "TOP", 0, -2)
f.nameText:SetShadowColor(0, 0, 0, 1)
f.nameText:SetShadowOffset(1, -1)
f.healthText = SFrames:CreateFontString(f.health, 9, "CENTER")
f.healthText:SetPoint("BOTTOM", f.health, "BOTTOM", 0, 2)
f.healthText:SetShadowColor(0, 0, 0, 1)
f.healthText:SetShadowOffset(1, -1)
f.healthText:SetTextColor(0.8, 0.8, 0.8)
local offOvr = CreateFrame("Frame", nil, f)
offOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 4)
offOvr:SetAllPoints(f)
local offIco = SFrames:CreateIcon(offOvr, "offline", 16)
offIco:SetPoint("CENTER", offOvr, "CENTER", 0, 0)
offIco:SetVertexColor(0.7, 0.7, 0.7, 0.9)
offIco:Hide()
f.offlineIcon = offIco
local roleOvr = CreateFrame("Frame", nil, f)
roleOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 6)
roleOvr:SetAllPoints(f)
f.leaderIcon = roleOvr:CreateTexture(nil, "OVERLAY")
f.leaderIcon:SetWidth(12)
f.leaderIcon:SetHeight(12)
f.leaderIcon:SetPoint("TOPLEFT", f, "TOPLEFT", -3, 3)
f.leaderIcon:SetTexture("Interface\\GroupFrame\\UI-Group-LeaderIcon")
f.leaderIcon:Hide()
f.assistantIcon = roleOvr:CreateFontString(nil, "OVERLAY")
f.assistantIcon:SetFont(SFrames:GetFont(), 12, "OUTLINE")
f.assistantIcon:SetPoint("TOPLEFT", f, "TOPLEFT", -1, 1)
f.assistantIcon:SetTextColor(0.5, 0.8, 1.0)
f.assistantIcon:SetShadowColor(0, 0, 0, 1)
f.assistantIcon:SetShadowOffset(1, -1)
f.assistantIcon:SetText("+")
f.assistantIcon:Hide()
-- Raid Target Icon
local raidIconSize = 16
local raidIconOvr = CreateFrame("Frame", nil, f)
raidIconOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 7)
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
f.indicators = {}
for pos = 1, 4 do
local ind = CreateFrame("Button", nil, f)
ind:SetWidth(10)
ind:SetHeight(10)
ind:SetFrameLevel(f:GetFrameLevel() + 5)
SFrames:CreateUnitBackdrop(ind)
ind.icon = ind:CreateTexture(nil, "ARTWORK")
ind.icon:SetAllPoints()
ind.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
if pos == 1 then
ind:SetPoint("TOPLEFT", f.health, "TOPLEFT", 1, -1)
elseif pos == 2 then
ind:SetPoint("TOPRIGHT", f.health, "TOPRIGHT", -1, -1)
elseif pos == 3 then
ind:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", 1, 1)
elseif pos == 4 then
ind:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", -1, 1)
end
ind:SetScript("OnEnter", function()
if this.index then
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
if this.isDebuff then
GameTooltip:SetUnitDebuff(f.unit, this.index)
else
GameTooltip:SetUnitBuff(f.unit, this.index)
end
end
end)
ind:SetScript("OnLeave", function() GameTooltip:Hide() end)
ind:Hide()
f.indicators[pos] = ind
end
self.frames[i] = { frame = f, unit = unit, index = i }
self:ApplyFrameStyle(f, self:GetMetrics())
f.rangeTimer = 0
f.auraScanTimer = 0
f.healPredTimer = 0
f:SetScript("OnShow", function()
if not SFrames.Raid.testing then
SFrames.Raid:UpdateFrame(this.unit)
end
end)
f:Hide()
end
if not self._globalUpdateFrame then
self._globalUpdateFrame = CreateFrame("Frame", nil, UIParent)
self._globalUpdateFrame._onUpdateFunc = function()
if SFrames.Raid.testing then return end
local dt = arg1
local frames = SFrames.Raid.frames
if not frames then return end
local visibleCount = SFrames.Raid._visibleCount or 0
if visibleCount == 0 then return end
for i = 1, 40 do
local entry = frames[i]
if entry then
local f = entry.frame
if f:IsVisible() and f.unit and f.unit ~= "" then
f.rangeTimer = f.rangeTimer + dt
if f.rangeTimer >= 0.5 then
if UnitExists(f.unit) and not CheckInteractDistance(f.unit, 4) then
f:SetAlpha(0.6)
else
f:SetAlpha(1.0)
end
f.rangeTimer = 0
end
f.auraScanTimer = f.auraScanTimer + dt
if f.auraScanTimer >= 0.7 then
SFrames.Raid:UpdateAuras(f.unit)
f.auraScanTimer = 0
end
f.healPredTimer = f.healPredTimer + dt
if f.healPredTimer >= 0.2 then
SFrames.Raid:UpdateHealPrediction(f.unit)
f.healPredTimer = 0
end
end
end
end
end
self._globalUpdateFrame:Hide()
end
self:ApplyLayout()
end
function SFrames.Raid:UpdateAll()
if self.testing then return end
if SFramesDB and SFramesDB.enableRaidFrames == false then
self.parent:Hide()
return
else
self.parent:Show()
end
if GetNumRaidMembers() > 0 then
self:EnsureFrames()
for i = 1, 40 do
self.frames[i].frame:Hide()
self.frames[i].frame.unit = ""
self.frames[i].unit = ""
end
local groupSlots = {}
for g = 1, 8 do
groupSlots[g] = 0
end
local visibleCount = 0
for i = 1, 40 do
local name, rank, subgroup = GetRaidRosterInfo(i)
if name and subgroup and subgroup >= 1 and subgroup <= 8 then
groupSlots[subgroup] = groupSlots[subgroup] + 1
local slot = groupSlots[subgroup]
if slot <= 5 then
local frameIndex = (subgroup - 1) * 5 + slot
local f = self.frames[frameIndex].frame
f.unit = "raid" .. i
f.raidRank = rank
self.frames[frameIndex].unit = "raid" .. i
f:Show()
self:UpdateFrame("raid" .. i)
visibleCount = visibleCount + 1
end
end
end
self._visibleCount = visibleCount
if self._globalUpdateFrame then
self._globalUpdateFrame:SetScript("OnUpdate", self._globalUpdateFrame._onUpdateFunc)
self._globalUpdateFrame:Show()
end
-- Show/hide group labels based on whether each group has members
local showLabel = SFramesDB and SFramesDB.raidShowGroupLabel ~= false
for g = 1, 8 do
local lbl = self.groupLabels and self.groupLabels[g]
if lbl then
if showLabel and groupSlots[g] > 0 then
lbl:Show()
else
lbl:Hide()
end
end
end
else
self._visibleCount = 0
if self._globalUpdateFrame then
self._globalUpdateFrame:SetScript("OnUpdate", nil)
self._globalUpdateFrame:Hide()
end
if not self.testing and self._framesBuilt then
for i = 1, 40 do
self.frames[i].frame:Hide()
self.frames[i].frame.unit = ""
self.frames[i].unit = ""
end
end
for g = 1, 8 do
local lbl = self.groupLabels and self.groupLabels[g]
if lbl then lbl:Hide() end
end
end
end
function SFrames.Raid:UpdateFrame(unit)
if self.testing or not self._framesBuilt then return end
for i=1, 40 do
if self.frames[i].unit == unit then
local f = self.frames[i].frame
local name = UnitName(unit) or ""
local _, class = UnitClass(unit)
local display = string.sub(name, 1, 15)
f.nameText:SetText(display)
local rank = f.raidRank or 0
if f.leaderIcon then
if rank == 2 then f.leaderIcon:Show() else f.leaderIcon:Hide() end
end
if f.assistantIcon then
if rank == 1 then f.assistantIcon:Show() else f.assistantIcon:Hide() end
end
if not UnitIsConnected(unit) then
f.health:SetStatusBarColor(0.5, 0.5, 0.5)
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
f.nameText:SetTextColor(0.5, 0.5, 0.5)
if f.offlineIcon then f.offlineIcon:Show() end
else
if f.offlineIcon then f.offlineIcon:Hide() end
if class and SFrames.Config.colors.class[class] then
local color = SFrames.Config.colors.class[class]
f.health:SetStatusBarColor(color.r, color.g, color.b)
f.nameText:SetTextColor(1, 1, 1)
else
f.health:SetStatusBarColor(0, 1, 0)
f.nameText:SetTextColor(1, 1, 1)
end
end
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(f.health)
end
self:UpdateHealth(unit)
self:UpdatePower(unit)
self:UpdateAuras(unit)
self:UpdateRaidIcon(unit)
self:UpdateTargetHighlight()
break
end
end
end
function SFrames.Raid:UpdateTargetHighlight()
if self.testing or not self._framesBuilt then return end
for i=1, 40 do
local f = self.frames[i].frame
if UnitExists("target") and UnitIsUnit("target", self.frames[i].unit) then
f.targetHighlight:Show()
else
f.targetHighlight:Hide()
end
end
end
function SFrames.Raid:UpdateHealth(unit)
if not self._framesBuilt then return end
for i=1, 40 do
if self.frames[i].unit == unit then
local f = self.frames[i].frame
if not UnitIsConnected(unit) then
f.health:SetMinMaxValues(0, 100)
f.health:SetValue(0)
f.healthText:SetText("离线")
if f.offlineIcon then f.offlineIcon:Show() end
return
end
if f.offlineIcon then f.offlineIcon:Hide() end
local hp = UnitHealth(unit)
local maxHp = UnitHealthMax(unit)
if UnitIsDead(unit) then
f.health:SetMinMaxValues(0, 100)
f.health:SetValue(0)
f.healthText:SetText("死亡")
f.nameText:SetTextColor(0.5, 0.5, 0.5)
return
end
if UnitIsGhost(unit) then
f.health:SetMinMaxValues(0, 100)
f.health:SetValue(0)
f.healthText:SetText("灵魂")
f.nameText:SetTextColor(0.5, 0.5, 0.5)
return
end
f.health:SetMinMaxValues(0, maxHp)
f.health:SetValue(hp)
local percent = 0
if maxHp > 0 then
percent = math.floor((hp / maxHp) * 100)
end
local db = SFramesDB or {}
local txt = ""
if db.raidHealthFormat == "percent" then
txt = percent .. "%"
elseif db.raidHealthFormat == "deficit" then
if maxHp - hp > 0 then
txt = "-" .. SFrames:FormatCompactNumber(maxHp - hp)
end
else
txt = SFrames:FormatCompactNumber(hp)
end
f.healthText:SetText(txt)
self:UpdateHealPrediction(unit)
break
end
end
end
function SFrames.Raid:UpdateHealPrediction(unit)
if not self._framesBuilt then return end
local frameData = nil
for i=1, 40 do
if self.frames[i].unit == unit then
frameData = self.frames[i]
break
end
end
if not frameData then return end
local f = frameData.frame
if not (f.health and f.health.healPredMine and f.health.healPredOther and f.health.healPredOver) then return end
local predMine = f.health.healPredMine
local predOther = f.health.healPredOther
local predOver = f.health.healPredOver
if not UnitExists(unit) or not UnitIsConnected(unit) then
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
local hp = UnitHealth(unit) or 0
local maxHp = UnitHealthMax(unit) or 0
if CheckSuperWow then
local ok, hasSW = pcall(CheckSuperWow)
if ok and hasSW then
local ok2, realHp = pcall(UnitHealth, unit)
if ok2 then
hp = realHp or hp
end
local ok3, realMaxHp = pcall(UnitHealthMax, unit)
if ok3 then
maxHp = realMaxHp or maxHp
end
end
end
if maxHp <= 0 then
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
local _, mineIncoming, othersIncoming = GetIncomingHeals(unit)
if CheckSuperWow then
local ok, hasSW = pcall(CheckSuperWow)
if ok and hasSW then
local ok2, _, realMine, realOther = pcall(GetIncomingHeals, unit)
if ok2 then
mineIncoming = realMine or mineIncoming
othersIncoming = realOther or othersIncoming
end
end
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 mineIncoming <= 0 and othersIncoming <= 0 then
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
local barWidth = f:GetWidth() - 2
if barWidth <= 0 then
predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
local currentPosition = (hp / maxHp) * barWidth
if currentPosition < 0 then currentPosition = 0 end
if currentPosition > barWidth then currentPosition = barWidth end
local availableWidth = barWidth - currentPosition
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", f.health, "TOPLEFT", currentPosition, 0)
predMine:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentPosition, 0)
predMine:SetWidth(mineWidth)
predMine:SetHeight(f.health:GetHeight())
predMine:Show()
else
predMine:Hide()
end
if otherWidth > 0 then
predOther:ClearAllPoints()
predOther:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentPosition + mineWidth, 0)
predOther:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentPosition + mineWidth, 0)
predOther:SetWidth(otherWidth)
predOther:SetHeight(f.health:GetHeight())
predOther:Show()
else
predOther:Hide()
end
local totalIncoming = mineIncoming + othersIncoming
local overHeal = totalIncoming - missing
if overHeal > 0 then
local overWidth = (overHeal / maxHp) * barWidth
if overWidth > 0 then
predOver:ClearAllPoints()
predOver:SetPoint("TOPLEFT", f.health, "TOPRIGHT", 0, 0)
predOver:SetPoint("BOTTOMLEFT", f.health, "BOTTOMRIGHT", 0, 0)
predOver:SetWidth(overWidth)
predOver:SetHeight(f.health:GetHeight())
predOver:Show()
else
predOver:Hide()
end
else
predOver:Hide()
end
end
function SFrames.Raid:UpdatePower(unit)
if not self._framesBuilt then return end
local db = SFramesDB or {}
if db.raidShowPower == false then return end
for i=1, 40 do
if self.frames[i].unit == unit then
local f = self.frames[i].frame
local power = UnitMana(unit)
local maxPower = UnitManaMax(unit)
local _, class = UnitClass(unit)
f.power:SetMinMaxValues(0, maxPower)
f.power:SetValue(power)
local pType = UnitPowerType(unit)
local color = SFrames.Config.colors.power[pType] or SFrames.Config.colors.power[0]
f.power:SetStatusBarColor(color.r, color.g, color.b)
if SFrames:IsGradientStyle() then
SFrames:ApplyBarGradient(f.power)
end
SFrames:UpdateRainbowBar(f.power, power, maxPower, unit)
break
end
end
end
function SFrames.Raid:UpdateRaidIcons()
if not self._framesBuilt then return end
for i = 1, 40 do
local unit = self.frames[i].unit
if unit and unit ~= "" and self.frames[i].frame:IsShown() then
self:UpdateRaidIcon(unit)
end
end
end
function SFrames.Raid:UpdateRaidIcon(unit)
if not self._framesBuilt then return end
for i = 1, 40 do
if self.frames[i].unit == unit then
local f = self.frames[i].frame
if not f.raidIcon then return end
if not GetRaidTargetIndex then
f.raidIcon:Hide()
return
end
if not UnitExists(unit) then
f.raidIcon:Hide()
return
end
local index = GetRaidTargetIndex(unit)
if index and index > 0 and index <= 8 then
local col = math.mod(index - 1, 4)
local row = math.floor((index - 1) / 4)
f.raidIcon:SetTexCoord(col * 0.25, (col + 1) * 0.25, row * 0.25, (row + 1) * 0.25)
f.raidIcon:Show()
else
f.raidIcon:Hide()
end
break
end
end
end
local PlayerClassBuffs = nil
function SFrames.Raid:GetClassBuffs()
if PlayerClassBuffs then return PlayerClassBuffs end
local _, playerClass = UnitClass("player")
PlayerClassBuffs = {}
if playerClass == "PRIEST" then
PlayerClassBuffs = {
[1] = { "真言术:韧", "坚韧祷言" },
[2] = { "神圣之灵", "精神祷言" },
[3] = { "恢复" },
[4] = { "虚弱灵魂", isDebuff = true },
[5] = { "防恐结界" }
}
elseif playerClass == "DRUID" then
PlayerClassBuffs = {
[1] = { "野性印记", "野性赐福" },
[2] = { "荆棘术" },
[3] = { "回春术" },
[4] = { "愈合" },
}
elseif playerClass == "PALADIN" then
PlayerClassBuffs = {
[1] = { "王者祝福", "强效王者祝福" },
[2] = { "拯救祝福", "强效拯救祝福" },
[3] = { "智慧祝福", "强效智慧祝福" },
[4] = { "力量祝福", "强效力量祝福" },
[5] = { "庇护祝福", "强效庇护祝福" },
[6] = { "光明祝福", "强效光明祝福" }
}
elseif playerClass == "MAGE" then
PlayerClassBuffs = {
[1] = { "奥术智慧", "奥术光辉" },
[2] = { "魔法抑制" },
[3] = { "魔法增效" }
}
end
return PlayerClassBuffs
end
function SFrames.Raid:UpdateAuras(unit)
if not self._framesBuilt then return end
local frameData = nil
for i=1, 40 do
if self.frames[i].unit == unit then
frameData = self.frames[i]
break
end
end
if not frameData then return end
local f = frameData.frame
local buffsNeeded = self:GetClassBuffs()
-- Reuse pre-allocated table
_foundIndicators[1] = false
_foundIndicators[2] = false
_foundIndicators[3] = false
_foundIndicators[4] = false
-- Hide all first
for i = 1, 4 do
f.indicators[i]:Hide()
f.indicators[i].index = nil
end
if not buffsNeeded or not UnitIsConnected(unit) or UnitIsDeadOrGhost(unit) then
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
return
end
-- Check Buffs (using module-level helpers, no closures)
for i = 1, 32 do
local texture, applications = UnitBuff(unit, i)
if not texture then break end
local buffName = RaidGetBuffName(unit, i)
if buffName then
for pos, listData in pairs(buffsNeeded) do
if pos <= 4 and not listData.isDebuff and not _foundIndicators[pos] then
if MatchesList(buffName, listData) then
f.indicators[pos].icon:SetTexture(texture)
f.indicators[pos].index = i
f.indicators[pos].isDebuff = false
f.indicators[pos]:Show()
_foundIndicators[pos] = true
end
end
end
end
end
local hasDebuff = false
-- Reuse pre-allocated table
_debuffColor.r = _A.slotBg[1]
_debuffColor.g = _A.slotBg[2]
_debuffColor.b = _A.slotBg[3]
-- Check Debuffs
for i = 1, 16 do
local texture, applications, dispelType = UnitDebuff(unit, i)
if not texture then break end
if dispelType then
hasDebuff = true
if dispelType == "Magic" then _debuffColor.r = 0.2; _debuffColor.g = 0.6; _debuffColor.b = 1
elseif dispelType == "Curse" then _debuffColor.r = 0.6; _debuffColor.g = 0; _debuffColor.b = 1
elseif dispelType == "Disease" then _debuffColor.r = 0.6; _debuffColor.g = 0.4; _debuffColor.b = 0
elseif dispelType == "Poison" then _debuffColor.r = 0; _debuffColor.g = 0.6; _debuffColor.b = 0
end
end
local debuffName = RaidGetDebuffName(unit, i)
if debuffName then
for pos, listData in pairs(buffsNeeded) do
if pos <= 4 and listData.isDebuff and not _foundIndicators[pos] then
if MatchesList(debuffName, listData) then
f.indicators[pos].icon:SetTexture(texture)
f.indicators[pos].index = i
f.indicators[pos].isDebuff = true
f.indicators[pos]:Show()
_foundIndicators[pos] = true
end
end
end
end
end
if hasDebuff then
f.health.bg:SetVertexColor(_debuffColor.r, _debuffColor.g, _debuffColor.b, 1)
else
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
end
end