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

1177 lines
40 KiB
Lua

SFrames.Party = {}
local _A = SFrames.ActiveTheme
local PARTY_FRAME_WIDTH = 150
local PARTY_FRAME_HEIGHT = 35
local PARTY_VERTICAL_GAP = 30
local PARTY_HORIZONTAL_GAP = 8
local PARTY_UNIT_LOOKUP = { party1 = true, party2 = true, party3 = true, party4 = true }
local PARTYPET_UNIT_LOOKUP = { partypet1 = true, partypet2 = true, partypet3 = true, partypet4 = true }
local function GetIncomingHeals(unit)
return SFrames:GetIncomingHeals(unit)
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)
if not ok then
return false
end
return not CursorHasItem()
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.Party:GetMetrics()
local db = SFramesDB or {}
local width = tonumber(db.partyFrameWidth) or PARTY_FRAME_WIDTH
width = Clamp(math.floor(width + 0.5), 120, 320)
local height = tonumber(db.partyFrameHeight) or PARTY_FRAME_HEIGHT
height = Clamp(math.floor(height + 0.5), 28, 80)
local portraitWidth = tonumber(db.partyPortraitWidth) or math.min(33, height - 2)
portraitWidth = Clamp(math.floor(portraitWidth + 0.5), 24, 64)
if portraitWidth > width - 70 then
portraitWidth = width - 70
end
local healthHeight = tonumber(db.partyHealthHeight) or math.floor((height - 3) * 0.7)
healthHeight = Clamp(math.floor(healthHeight + 0.5), 10, height - 8)
local powerHeight = tonumber(db.partyPowerHeight) or (height - healthHeight - 3)
powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, height - 6)
if healthHeight + powerHeight + 3 > height then
powerHeight = height - healthHeight - 3
if powerHeight < 6 then
powerHeight = 6
healthHeight = height - powerHeight - 3
if healthHeight < 10 then
healthHeight = 10
powerHeight = height - healthHeight - 3
end
end
end
local hgap = tonumber(db.partyHorizontalGap) or PARTY_HORIZONTAL_GAP
hgap = Clamp(math.floor(hgap + 0.5), 0, 40)
local vgap = tonumber(db.partyVerticalGap) or PARTY_VERTICAL_GAP
vgap = Clamp(math.floor(vgap + 0.5), 0, 80)
local nameFont = tonumber(db.partyNameFontSize) or 10
nameFont = Clamp(math.floor(nameFont + 0.5), 8, 18)
local valueFont = tonumber(db.partyValueFontSize) or 10
valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18)
return {
width = width,
height = height,
portraitWidth = portraitWidth,
healthHeight = healthHeight,
powerHeight = powerHeight,
horizontalGap = hgap,
verticalGap = vgap,
nameFont = nameFont,
valueFont = valueFont,
}
end
function SFrames.Party:ApplyFrameStyle(frame, metrics)
if not frame then return end
frame:SetWidth(metrics.width)
frame:SetHeight(metrics.height)
if frame.pbg then
frame.pbg:SetWidth(metrics.portraitWidth + 2)
frame.pbg:SetHeight(metrics.height)
frame.pbg:ClearAllPoints()
frame.pbg:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
end
if frame.portrait then
frame.portrait:SetWidth(metrics.portraitWidth)
frame.portrait:SetHeight(metrics.height - 2)
if frame.pbg then
frame.portrait:ClearAllPoints()
frame.portrait:SetPoint("CENTER", frame.pbg, "CENTER", 0, 0)
end
end
if frame.health then
frame.health:ClearAllPoints()
frame.health:SetPoint("TOPLEFT", frame.pbg, "TOPRIGHT", 2, -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
if frame.power then
frame.power:ClearAllPoints()
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", 0, -1)
frame.power:SetPoint("TOPRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
frame.power:SetHeight(metrics.powerHeight)
end
if frame.powerBGFrame then
frame.powerBGFrame:ClearAllPoints()
frame.powerBGFrame:SetPoint("TOPLEFT", frame.power, "TOPLEFT", -1, 1)
frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 1, -1)
end
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
local fontPath = SFrames:GetFont()
if frame.nameText then
frame.nameText:SetFont(fontPath, metrics.nameFont, outline)
end
if frame.healthText then
frame.healthText:SetFont(fontPath, metrics.valueFont, outline)
end
end
function SFrames.Party:ApplyConfig()
if not self.parent then return end
local frameScale = tonumber(SFramesDB and SFramesDB.partyFrameScale) or 1
frameScale = Clamp(frameScale, 0.7, 1.8)
self.parent:SetScale(frameScale)
local metrics = self:GetMetrics()
self.metrics = metrics
if self.frames then
for i = 1, 4 do
local item = self.frames[i]
if item and item.frame then
self:ApplyFrameStyle(item.frame, metrics)
end
end
end
self:ApplyLayout()
if self.testing then
for i = 1, 4 do
if self.frames[i] and self.frames[i].frame then
self.frames[i].frame:Show()
end
end
else
self:UpdateAll()
end
end
function SFrames.Party:GetLayoutMode()
if SFramesDB and SFramesDB.partyLayout == "horizontal" then
return "horizontal"
end
return "vertical"
end
function SFrames.Party: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["PartyFrame"] = {
point = point,
relativePoint = relativePoint,
xOfs = xOfs or 0,
yOfs = yOfs or 0,
}
end
end
function SFrames.Party:ApplyPosition()
if not self.parent then return end
self.parent:ClearAllPoints()
local pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PartyFrame"]
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, -150)
end
end
function SFrames.Party:ApplyLayout()
if not (self.parent and self.frames) then return end
local metrics = self.metrics or self:GetMetrics()
self.metrics = metrics
local mode = self:GetLayoutMode()
for i = 1, 4 do
local f = self.frames[i] and self.frames[i].frame
if f then
f:ClearAllPoints()
if i == 1 then
f:SetPoint("TOPLEFT", self.parent, "TOPLEFT", 0, 0)
else
local prev = self.frames[i - 1].frame
if mode == "horizontal" then
f:SetPoint("TOPLEFT", prev, "TOPRIGHT", metrics.horizontalGap, 0)
else
f:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -metrics.verticalGap)
end
end
end
end
if mode == "horizontal" then
self.parent:SetWidth((metrics.width * 4) + (metrics.horizontalGap * 3))
self.parent:SetHeight(metrics.height)
else
self.parent:SetWidth(metrics.width)
self.parent:SetHeight(metrics.height + ((metrics.height + metrics.verticalGap) * 3))
end
end
function SFrames.Party:SetLayout(mode)
if not SFramesDB then SFramesDB = {} end
if mode ~= "horizontal" then
mode = "vertical"
end
SFramesDB.partyLayout = mode
self:ApplyLayout()
if self.testing then
for i = 1, 4 do
self.frames[i].frame:Show()
end
else
self:UpdateAll()
end
end
function SFrames.Party:Initialize()
self.frames = {}
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.partyLayout then
SFramesDB.partyLayout = "vertical"
end
local parent = CreateFrame("Frame", "SFramesPartyParent", UIParent)
parent:SetWidth(PARTY_FRAME_WIDTH)
parent:SetHeight(PARTY_FRAME_HEIGHT)
local frameScale = (SFramesDB and type(SFramesDB.partyFrameScale) == "number") and SFramesDB.partyFrameScale or 1
parent:SetScale(frameScale)
self.parent = parent
self:ApplyPosition()
parent:SetMovable(true)
for i = 1, 4 do
local unit = "party" .. i
local f = CreateFrame("Button", "SFramesPartyFrame"..i, parent)
f:SetWidth(PARTY_FRAME_WIDTH)
f:SetHeight(PARTY_FRAME_HEIGHT)
f.id = i
f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function()
if IsAltKeyDown() or SFrames.isUnlocked then
SFrames.Party.parent:StartMoving()
end
end)
f:SetScript("OnDragStop", function()
SFrames.Party.parent:StopMovingOrSizing()
SFrames.Party:SavePosition()
end)
f:SetScript("OnClick", function()
if arg1 == "LeftButton" then
if TryDropCursorOnUnit(this.unit) then
return
end
if SpellIsTargeting and SpellIsTargeting() then
SpellTargetUnit(this.unit)
return
end
TargetUnit(this.unit)
SFrames.Party:UpdateFrame(this.unit)
elseif arg1 == "RightButton" then
ToggleDropDownMenu(1, nil, getglobal("PartyMemberFrame"..this.id.."DropDown"), "cursor")
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()
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit(this.unit)
end)
f:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
f.unit = unit
-- Portrait
local pbg = CreateFrame("Frame", nil, f)
pbg:SetWidth(35)
pbg:SetHeight(35)
pbg:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
pbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(pbg)
f.pbg = pbg
f.portrait = CreateFrame("PlayerModel", nil, f)
f.portrait:SetWidth(33)
f.portrait:SetHeight(33)
f.portrait:SetPoint("CENTER", pbg, "CENTER", 0, 0)
local offlineOverlay = CreateFrame("Frame", nil, f)
offlineOverlay:SetFrameLevel((f.portrait:GetFrameLevel() or 0) + 3)
offlineOverlay:SetAllPoints(pbg)
local offlineIcon = SFrames:CreateIcon(offlineOverlay, "offline", 20)
offlineIcon:SetPoint("CENTER", offlineOverlay, "CENTER", 0, 0)
offlineIcon:SetVertexColor(0.7, 0.7, 0.7, 0.9)
offlineIcon:Hide()
f.offlineIcon = offlineIcon
-- Health Bar
f.health = SFrames:CreateStatusBar(f, "SFramesPartyFrame"..i.."Health")
f.health:SetPoint("TOPLEFT", pbg, "TOPRIGHT", 2, -1)
f.health:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 10)
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
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, "SFramesPartyFrame"..i.."Power")
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
f.power:SetMinMaxValues(0, 100)
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
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)
-- Texts
f.nameText = SFrames:CreateFontString(f.health, 10, "LEFT")
f.nameText:SetPoint("LEFT", f.health, "LEFT", 4, 0)
f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT")
f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -4, 0)
f.nameText:SetShadowColor(0, 0, 0, 1)
f.nameText:SetShadowOffset(1, -1)
f.healthText:SetShadowColor(0, 0, 0, 1)
f.healthText:SetShadowOffset(1, -1)
-- Leader / Master Looter overlay (high frame level so icons aren't hidden by portrait)
local roleOvr = CreateFrame("Frame", nil, f)
roleOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 4)
roleOvr:SetAllPoints(f)
-- Leader Icon
f.leaderIcon = roleOvr:CreateTexture(nil, "OVERLAY")
f.leaderIcon:SetWidth(14)
f.leaderIcon:SetHeight(14)
f.leaderIcon:SetPoint("TOPLEFT", pbg, "TOPLEFT", -4, 4)
f.leaderIcon:SetTexture("Interface\\GroupFrame\\UI-Group-LeaderIcon")
f.leaderIcon:Hide()
-- Master Looter Icon
f.masterIcon = roleOvr:CreateTexture(nil, "OVERLAY")
f.masterIcon:SetWidth(12)
f.masterIcon:SetHeight(12)
f.masterIcon:SetPoint("TOPRIGHT", pbg, "TOPRIGHT", 4, 4)
f.masterIcon:SetTexture("Interface\\GroupFrame\\UI-Group-MasterLooter")
f.masterIcon:Hide()
-- Raid Target Icon
local raidIconSize = 18
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
-- Pet Frame
local pf = CreateFrame("Button", "SFramesPartyPetFrame"..i, f)
pf:SetHeight(8)
pf:SetPoint("BOTTOMLEFT", f.health, "TOPLEFT", 0, 2)
pf:SetPoint("BOTTOMRIGHT", f.health, "TOPRIGHT", 0, 2)
pf.unit = "partypet"..i
SFrames:CreateUnitBackdrop(pf)
pf.health = SFrames:CreateStatusBar(pf, "SFramesPartyPetHealth"..i)
pf.health:SetAllPoints()
pf.health.bg = pf.health:CreateTexture(nil, "BACKGROUND")
pf.health.bg:SetAllPoints()
pf.health.bg:SetTexture(SFrames:GetTexture())
pf.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
pf.nameText = SFrames:CreateFontString(pf.health, 8, "LEFT")
pf.nameText:SetPoint("LEFT", pf.health, "LEFT", 2, 0)
pf.nameText:SetShadowColor(0, 0, 0, 1)
pf.nameText:SetShadowOffset(1, -1)
pf:RegisterForClicks("LeftButtonUp", "RightButtonUp")
pf:SetScript("OnClick", function() TargetUnit(this.unit) end)
pf:SetScript("OnEnter", function()
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit(this.unit)
end)
pf:SetScript("OnLeave", function() GameTooltip:Hide() end)
pf:Hide()
f.petFrame = pf
self.frames[i] = { frame = f, unit = unit, index = i }
f.unit = unit
self:CreateAuras(i)
f:SetScript("OnShow", function()
if not SFrames.Party.testing then
SFrames.Party:UpdateFrame(this.unit)
end
end)
f:Hide()
end
if not self._globalUpdateFrame then
self._globalUpdateFrame = CreateFrame("Frame", nil, UIParent)
self._globalUpdateFrame:SetScript("OnUpdate", function()
if SFrames.Party.testing then return end
local dt = arg1
local frames = SFrames.Party.frames
if not frames then return end
for i = 1, 4 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.5)
else
f:SetAlpha(1.0)
end
f.rangeTimer = 0
end
f.auraScanTimer = f.auraScanTimer + dt
if f.auraScanTimer >= 0.5 then
SFrames.Party:UpdateAuras(f.unit)
f.auraScanTimer = 0
end
f.healPredTimer = f.healPredTimer + dt
if f.healPredTimer >= 0.2 then
SFrames.Party:UpdateHealPrediction(f.unit)
f.healPredTimer = 0
end
f.tickAuraTimer = f.tickAuraTimer + dt
if f.tickAuraTimer >= 0.5 then
SFrames.Party:TickAuras(f.unit)
f.tickAuraTimer = 0
end
end
end
end
end)
end
self:ApplyConfig()
SFrames:RegisterEvent("PARTY_MEMBERS_CHANGED", function() self:UpdateAll() end)
SFrames:RegisterEvent("RAID_ROSTER_UPDATE", function() self:UpdateAll() end)
SFrames:RegisterEvent("UNIT_HEALTH", function()
if PARTY_UNIT_LOOKUP[arg1] then self:UpdateHealth(arg1)
elseif PARTYPET_UNIT_LOOKUP[arg1] then self:UpdatePet(arg1) end
end)
SFrames:RegisterEvent("UNIT_MAXHEALTH", function()
if PARTY_UNIT_LOOKUP[arg1] then self:UpdateHealth(arg1)
elseif PARTYPET_UNIT_LOOKUP[arg1] then self:UpdatePet(arg1) end
end)
SFrames:RegisterEvent("UNIT_MANA", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
SFrames:RegisterEvent("UNIT_MAXMANA", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
SFrames:RegisterEvent("UNIT_RAGE", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
SFrames:RegisterEvent("UNIT_MAXRAGE", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
SFrames:RegisterEvent("UNIT_ENERGY", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
SFrames:RegisterEvent("UNIT_MAXENERGY", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
SFrames:RegisterEvent("UNIT_FOCUS", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
SFrames:RegisterEvent("UNIT_MAXFOCUS", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
SFrames:RegisterEvent("UNIT_DISPLAYPOWER", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePowerType(arg1) end end)
SFrames:RegisterEvent("UNIT_PORTRAIT_UPDATE", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePortrait(arg1) end end)
SFrames:RegisterEvent("UNIT_AURA", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdateAuras(arg1) end end)
SFrames:RegisterEvent("UNIT_LEVEL", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdateFrame(arg1) end end)
SFrames:RegisterEvent("PARTY_LEADER_CHANGED", function() self:UpdateAll() end)
SFrames:RegisterEvent("PARTY_LOOT_METHOD_CHANGED", function() self:UpdateAll() end)
SFrames:RegisterEvent("UNIT_PET", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePet("partypet" .. string.sub(arg1, 6)) end end)
SFrames:RegisterEvent("RAID_TARGET_UPDATE", function() self:UpdateRaidIcons() end)
-- Bulletproof Party load syncer (watches for count changes for 10 seconds after reload)
local syncWatcher = CreateFrame("Frame")
local syncTimer = 0
local lastCount = -1
syncWatcher:SetScript("OnUpdate", function()
syncTimer = syncTimer + arg1
local count = GetNumPartyMembers()
if count ~= lastCount then
lastCount = count
SFrames.Party:UpdateAll()
end
if syncTimer > 10.0 then
this:SetScript("OnUpdate", nil)
end
end)
self:UpdateAll()
end
function SFrames.Party:CreateAuras(index)
local f = self.frames[index].frame
f.buffs = {}
f.debuffs = {}
local size = 20
local spacing = 2
-- Party Buffs
for i = 1, 4 do
local b = CreateFrame("Button", "SFramesParty"..index.."Buff"..i, f)
b:SetWidth(size)
b:SetHeight(size)
SFrames:CreateUnitBackdrop(b)
b.icon = b:CreateTexture(nil, "ARTWORK")
b.icon:SetAllPoints()
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)
b:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
GameTooltip:SetUnitBuff(f.unit, this:GetID())
end)
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
-- Anchored BELOW the frame on the left side
if i == 1 then
b:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 0, -2)
else
b:SetPoint("LEFT", f.buffs[i-1], "RIGHT", spacing, 0)
end
b:Hide()
f.buffs[i] = b
end
-- Debuffs (Starting right after Buffs to remain linear)
for i = 1, 4 do
local b = CreateFrame("Button", "SFramesParty"..index.."Debuff"..i, f)
b:SetWidth(size)
b:SetHeight(size)
SFrames:CreateUnitBackdrop(b)
b.icon = b:CreateTexture(nil, "ARTWORK")
b.icon:SetAllPoints()
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)
b:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
GameTooltip:SetUnitDebuff(f.unit, this:GetID())
end)
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
if i == 1 then
b:SetPoint("LEFT", f.buffs[4], "RIGHT", spacing * 4, 0)
else
b:SetPoint("LEFT", f.debuffs[i-1], "RIGHT", spacing, 0)
end
b:Hide()
f.debuffs[i] = b
end
f.auraScanTimer = 0
f.rangeTimer = 0
f.healPredTimer = 0
f.tickAuraTimer = 0
end
function SFrames.Party:UpdatePet(unit)
local _, _, indexStr = string.find(unit, "(%d+)")
local index = tonumber(indexStr)
if not index or not self.frames[index] then return end
local pf = self.frames[index].frame.petFrame
if not pf then return end
if UnitExists(unit) and UnitIsConnected("party"..index) then
pf:Show()
local name = UnitName(unit)
if name == UNKNOWNOBJECT or name == "未知目标" or name == "Unknown" then
name = "宠物"
end
pf.nameText:SetText(name)
local hp = UnitHealth(unit)
local maxHp = UnitHealthMax(unit)
pf.health:SetMinMaxValues(0, maxHp)
pf.health:SetValue(hp)
pf.health:SetStatusBarColor(0.2, 0.8, 0.2)
else
pf:Hide()
end
end
function SFrames.Party:TickAuras(unit)
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
local timeNow = GetTime()
for i = 1, 4 do
local b = f.buffs[i]
if b:IsShown() and b.expirationTime then
local timeLeft = b.expirationTime - timeNow
if timeLeft > 0 and timeLeft < 3600 then
b.cdText:SetText(SFrames:FormatTime(timeLeft))
else
b.cdText:SetText("")
end
end
local db = f.debuffs[i]
if db:IsShown() and db.expirationTime then
local timeLeft = db.expirationTime - timeNow
if timeLeft > 0 and timeLeft < 3600 then
db.cdText:SetText(SFrames:FormatTime(timeLeft))
else
db.cdText:SetText("")
end
end
end
end
function SFrames.Party:GetFrameByUnit(unit)
for i = 1, 4 do
if self.frames[i].unit == unit then
return self.frames[i]
end
end
return nil
end
function SFrames.Party:UpdateAll()
if self.testing then return end
local inRaid = GetNumRaidMembers() > 0
local raidFramesEnabled = SFramesDB and SFramesDB.enableRaidFrames ~= false
if inRaid and raidFramesEnabled then
for i = 1, 4 do
if self.frames[i] and self.frames[i].frame then
self.frames[i].frame:Hide()
end
end
return
end
if self.testing then return end
local numParty = GetNumPartyMembers()
for i = 1, 4 do
local data = self.frames[i]
local f = data.frame
if i <= numParty then
f:Show()
self:UpdateFrame(data.unit)
else
f:Hide()
end
end
end
function SFrames.Party:UpdateFrame(unit)
if self.testing then return end
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
f.portrait:SetUnit(unit)
f.portrait:SetCamera(0)
f.portrait:Hide()
f.portrait:Show()
local name = UnitName(unit) or ""
local level = UnitLevel(unit)
if level == -1 then level = "??" end
local _, class = UnitClass(unit)
if not UnitIsConnected(unit) then
f.health:SetStatusBarColor(0.5, 0.5, 0.5)
f.nameText:SetText(name)
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:SetText(level .. " " .. name)
f.nameText:SetTextColor(color.r, color.g, color.b)
else
f.health:SetStatusBarColor(0, 1, 0)
f.nameText:SetText(level .. " " .. name)
f.nameText:SetTextColor(1, 1, 1)
end
end
-- Update Leader/Master Looter
if GetPartyLeaderIndex() == data.index then
f.leaderIcon:Show()
else
f.leaderIcon:Hide()
end
local method, partyIndex, raidIndex = GetLootMethod()
if method == "master" and partyIndex == data.index then
f.masterIcon:Show()
else
f.masterIcon:Hide()
end
self:UpdateHealth(unit)
self:UpdatePowerType(unit)
self:UpdatePower(unit)
self:UpdateAuras(unit)
self:UpdateRaidIcon(unit)
local petUnit = string.gsub(unit, "party", "partypet")
self:UpdatePet(petUnit)
end
function SFrames.Party:UpdatePortrait(unit)
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
f.portrait:SetUnit(unit)
f.portrait:SetCamera(0)
f.portrait:Hide()
f.portrait:Show()
end
function SFrames.Party:UpdateHealth(unit)
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
if not UnitIsConnected(unit) then
f.health:SetMinMaxValues(0, 100)
f.health:SetValue(0)
f.healthText:SetText("Offline")
if f.health.healPredMine then f.health.healPredMine:Hide() end
if f.health.healPredOther then f.health.healPredOther:Hide() end
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)
f.health:SetMinMaxValues(0, maxHp)
f.health:SetValue(hp)
if maxHp > 0 then
local percent = math.floor((hp / maxHp) * 100)
f.healthText:SetText(percent .. "%")
else
f.healthText:SetText("")
end
self:UpdateHealPrediction(unit)
end
function SFrames.Party:UpdateHealPrediction(unit)
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
if not (f.health and f.health.healPredMine and f.health.healPredOther) then return end
local predMine = f.health.healPredMine
local predOther = f.health.healPredOther
local function HidePredictions()
predMine:Hide()
predOther:Hide()
end
if not UnitExists(unit) or not UnitIsConnected(unit) then
HidePredictions()
return
end
local hp = UnitHealth(unit) or 0
local maxHp = UnitHealthMax(unit) or 0
if maxHp <= 0 or hp >= maxHp then
HidePredictions()
return
end
local _, mineIncoming, othersIncoming = GetIncomingHeals(unit)
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 = f.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", f.health, "TOPLEFT", currentWidth, 0)
predMine:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentWidth, 0)
predMine:SetWidth(mineWidth)
predMine:Show()
else
predMine:Hide()
end
if otherWidth > 0 then
predOther:ClearAllPoints()
predOther:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentWidth + mineWidth, 0)
predOther:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentWidth + mineWidth, 0)
predOther:SetWidth(otherWidth)
predOther:Show()
else
predOther:Hide()
end
end
function SFrames.Party:UpdatePowerType(unit)
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
local powerType = UnitPowerType(unit)
local color = SFrames.Config.colors.power[powerType]
if color then
f.power:SetStatusBarColor(color.r, color.g, color.b)
else
f.power:SetStatusBarColor(0, 0, 1)
end
end
function SFrames.Party:UpdatePower(unit)
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
if not UnitIsConnected(unit) then
f.power:SetMinMaxValues(0, 100)
f.power:SetValue(0)
return
end
local power = UnitMana(unit)
local maxPower = UnitManaMax(unit)
f.power:SetMinMaxValues(0, maxPower)
f.power:SetValue(power)
end
function SFrames.Party:UpdateRaidIcons()
for i = 1, 4 do
if self.frames[i] and self.frames[i].frame:IsShown() then
self:UpdateRaidIcon(self.frames[i].unit)
end
end
end
function SFrames.Party:UpdateRaidIcon(unit)
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.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
end
function SFrames.Party:UpdateAuras(unit)
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
local showDebuffs = not (SFramesDB and SFramesDB.partyShowDebuffs == false)
local showBuffs = not (SFramesDB and SFramesDB.partyShowBuffs == false)
local hasDebuff = false
local debuffColor = {r=_A.slotBg[1], g=_A.slotBg[2], b=_A.slotBg[3]}
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
-- Debuffs
if showDebuffs then
for i = 1, 4 do
local texture, applications, debuffType = UnitDebuff(unit, i)
local b = f.debuffs[i]
b:SetID(i)
if texture then
if debuffType then
hasDebuff = true
if debuffType == "Magic" then debuffColor = {r=0.2, g=0.6, b=1}
elseif debuffType == "Curse" then debuffColor = {r=0.6, g=0, b=1}
elseif debuffType == "Disease" then debuffColor = {r=0.6, g=0.4, b=0}
elseif debuffType == "Poison" then debuffColor = {r=0, g=0.6, b=0}
end
end
b.icon:SetTexture(texture)
SFrames.Tooltip:ClearLines()
SFrames.Tooltip:SetUnitDebuff(unit, i)
local timeLeft = SFrames:GetAuraTimeLeft(unit, i, false)
if timeLeft and timeLeft > 0 then
local newExp = GetTime() + timeLeft
if not b.expirationTime or math.abs(b.expirationTime - newExp) > 2 then
b.expirationTime = newExp
end
local currentLeft = b.expirationTime - GetTime()
if currentLeft > 0 and currentLeft < 3600 then
b.cdText:SetText(SFrames:FormatTime(currentLeft))
else
b.cdText:SetText("")
end
else
b.expirationTime = nil
b.cdText:SetText("")
end
b:Show()
else
b.expirationTime = nil
b.cdText:SetText("")
b:Hide()
end
end
else
for i = 1, 4 do
local b = f.debuffs[i]
b.expirationTime = nil
b.cdText:SetText("")
b:Hide()
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
-- Buffs
if showBuffs then
for i = 1, 4 do
local texture = UnitBuff(unit, i)
local b = f.buffs[i]
b:SetID(i)
if texture then
b.icon:SetTexture(texture)
SFrames.Tooltip:ClearLines()
SFrames.Tooltip:SetUnitBuff(unit, i)
local timeLeft = SFrames:GetAuraTimeLeft(unit, i, true)
if timeLeft and timeLeft > 0 then
local newExp = GetTime() + timeLeft
if not b.expirationTime or math.abs(b.expirationTime - newExp) > 2 then
b.expirationTime = newExp
end
local currentLeft = b.expirationTime - GetTime()
if currentLeft > 0 and currentLeft < 3600 then
b.cdText:SetText(SFrames:FormatTime(currentLeft))
else
b.cdText:SetText("")
end
else
b.expirationTime = nil
b.cdText:SetText("")
end
b:Show()
else
b.expirationTime = nil
b.cdText:SetText("")
b:Hide()
end
end
else
for i = 1, 4 do
local b = f.buffs[i]
b.expirationTime = nil
b.cdText:SetText("")
b:Hide()
end
end
end
function SFrames.Party:TestMode()
self.testing = not self.testing
if self.testing then
for i = 1, 4 do
local data = self.frames[i]
local f = data.frame
f:Show()
f.health:SetMinMaxValues(0, 100)
f.health:SetValue(math.random(30, 90))
f.health:SetStatusBarColor(SFrames.Config.colors.class["DRUID"].r, SFrames.Config.colors.class["DRUID"].g, SFrames.Config.colors.class["DRUID"].b)
f.nameText:SetText("60 队友" .. i)
f.nameText:SetTextColor(SFrames.Config.colors.class["DRUID"].r, SFrames.Config.colors.class["DRUID"].g, SFrames.Config.colors.class["DRUID"].b)
f.healthText:SetText(math.floor(f.health:GetValue()) .. "%")
if f.health.healPredMine then f.health.healPredMine:Hide() end
if f.health.healPredOther then f.health.healPredOther:Hide() end
f.power:SetMinMaxValues(0, 100)
f.power:SetValue(math.random(20, 100))
f.power:SetStatusBarColor(SFrames.Config.colors.power[0].r, SFrames.Config.colors.power[0].g, SFrames.Config.colors.power[0].b)
f.leaderIcon:Hide()
f.masterIcon:Hide()
if i == 1 then
f.leaderIcon:Show()
f.masterIcon:Show()
end
-- Show one dummy debuff to test positioning
f.debuffs[1].icon:SetTexture("Interface\\Icons\\Spell_Shadow_ShadowWordPain")
f.debuffs[1]:Show()
-- Test pet
if f.petFrame then
f.petFrame:Show()
f.petFrame.nameText:SetText("测试宠物")
f.petFrame.health:SetMinMaxValues(0, 100)
f.petFrame.health:SetValue(100)
f.petFrame.health:SetStatusBarColor(0.2, 0.8, 0.2)
end
end
else
self:UpdateAll()
for i = 1, 4 do
self.frames[i].frame.debuffs[1]:Hide()
end
end
end