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() if SFrames.Movers and SFrames.Movers.RegisterMover and self.parent then SFrames.Movers:RegisterMover("PartyFrame", self.parent, "小队", "TOPLEFT", "UIParent", "TOPLEFT", 15, -150) end 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) SFrames.Tooltip:Hide() 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) SFrames.Tooltip:Hide() 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