SFrames.Target = {} local _A = SFrames.ActiveTheme 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.Target:GetDistance(unit) if not UnitExists(unit) then return nil end if UnitIsUnit(unit, "player") then return "0 码" end -- Using multiple "scale rulers" (rungs) for better precision in 1.12 if CheckInteractDistance(unit, 2) then return "< 8 码" -- Trade elseif CheckInteractDistance(unit, 3) then return "8-10 码" -- Duel elseif CheckInteractDistance(unit, 4) then return "10-28 码" -- Follow elseif UnitIsVisible(unit) then return "28-100 码" else return "> 100 码" end end function SFrames.Target:GetConfig() local db = SFramesDB or {} local width = tonumber(db.targetFrameWidth) or SFrames.Config.width or 220 width = Clamp(math.floor(width + 0.5), 170, 420) local portraitWidth = tonumber(db.targetPortraitWidth) or SFrames.Config.portraitWidth or 50 portraitWidth = Clamp(math.floor(portraitWidth + 0.5), 32, 95) if portraitWidth > width - 90 then portraitWidth = width - 90 end local healthHeight = tonumber(db.targetHealthHeight) or 38 healthHeight = Clamp(math.floor(healthHeight + 0.5), 14, 80) local powerHeight = tonumber(db.targetPowerHeight) or 9 powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, 40) local height = healthHeight + powerHeight + 4 height = Clamp(height, 30, 140) local nameFont = tonumber(db.targetNameFontSize) or 10 nameFont = Clamp(math.floor(nameFont + 0.5), 8, 18) local valueFont = tonumber(db.targetValueFontSize) or 10 valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18) local frameScale = tonumber(db.targetFrameScale) or 1 frameScale = Clamp(frameScale, 0.7, 1.8) return { width = width, height = height, portraitWidth = portraitWidth, healthHeight = healthHeight, powerHeight = powerHeight, nameFont = nameFont, valueFont = valueFont, scale = frameScale, } end function SFrames.Target:ApplyConfig() if not self.frame then return end local cfg = self:GetConfig() local f = self.frame f:SetScale(cfg.scale) f:SetWidth(cfg.width) f:SetHeight(cfg.height) if f.portrait then f.portrait:SetWidth(cfg.portraitWidth) f.portrait:SetHeight(cfg.height - 2) end if f.portraitBG then f.portraitBG:ClearAllPoints() f.portraitBG:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -1, 0) f.portraitBG:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) end if f.health then f.health:ClearAllPoints() f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0) f.health:SetHeight(cfg.healthHeight) end if f.healthBGFrame then f.healthBGFrame:ClearAllPoints() f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1) f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1) end if f.power then f.power:ClearAllPoints() f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) f.power:SetPoint("TOPRIGHT", f.health, "BOTTOMRIGHT", 0, 0) f.power:SetHeight(cfg.powerHeight) end if f.powerBGFrame then f.powerBGFrame:ClearAllPoints() f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1) f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1) end local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" local fontPath = SFrames:GetFont() if f.nameText then f.nameText:SetFont(fontPath, cfg.nameFont, outline) end if f.healthText then f.healthText:SetFont(fontPath, cfg.valueFont, outline) end if f.powerText then f.powerText:SetFont(fontPath, cfg.valueFont, outline) end if self.distanceFrame then local dScale = tonumber(SFramesDB and SFramesDB.targetDistanceScale) or 1 self.distanceFrame:SetScale(Clamp(dScale, 0.7, 1.8)) end if UnitExists("target") then self:UpdateAll() end end function SFrames.Target:InitializeDistanceFrame() local f = CreateFrame("Button", "SFramesTargetDistanceFrame", UIParent) f:SetWidth(80) f:SetHeight(24) f:SetFrameStrata("HIGH") local frameScale = (SFramesDB and type(SFramesDB.targetDistanceScale) == "number") and SFramesDB.targetDistanceScale or 1 f:SetScale(frameScale) if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["TargetDistanceFrame"] then local pos = SFramesDB.Positions["TargetDistanceFrame"] f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs) else -- Default position: Center of screen for visibility if first time f:SetPoint("CENTER", UIParent, "CENTER", 0, 100) end f:SetMovable(true) f:EnableMouse(true) f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", function() if IsAltKeyDown() or SFrames.isUnlocked then this:StartMoving() end end) f:SetScript("OnDragStop", function() this:StopMovingOrSizing() if not SFramesDB then SFramesDB = {} end if not SFramesDB.Positions then SFramesDB.Positions = {} end local point, relativeTo, relativePoint, xOfs, yOfs = this:GetPoint() SFramesDB.Positions["TargetDistanceFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs } end) SFrames:CreateUnitBackdrop(f) f:SetBackdrop(nil) -- Remove border and background for natural look f.text = SFrames:CreateFontString(f, 14, "CENTER") f.text:SetPoint("CENTER", f, "CENTER", 0, 0) f.text:SetTextColor(1, 0.8, 0.2) f.text:SetShadowColor(0, 0, 0, 1) f.text:SetShadowOffset(1, -1) SFrames.Target.distanceFrame = f f:Hide() -- Distance Updater on the frame itself f.timer = 0 f:SetScript("OnUpdate", function() if SFramesDB and SFramesDB.targetDistanceEnabled == false then if this:IsShown() then this:Hide() end return end if not UnitExists("target") then if this:IsShown() then this:Hide() end return end this.timer = this.timer + (arg1 or 0) if this.timer >= 0.4 then this.timer = 0 local dist = SFrames.Target:GetDistance("target") this.text:SetText(dist or "---") if not this:IsShown() then this:Show() end end end) end local AURA_SIZE = 24 local AURA_SPACING = 2 local AURA_ROW_SPACING = 1 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 function SFrames.Target:Initialize() local f = CreateFrame("Button", "SFramesTargetFrame", UIParent) f:SetWidth(SFrames.Config.width) f:SetHeight(SFrames.Config.height) if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["TargetFrame"] then local pos = SFramesDB.Positions["TargetFrame"] f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs) else f:SetPoint("CENTER", UIParent, "CENTER", 200, -100) -- Mirrored from player end local frameScale = (SFramesDB and type(SFramesDB.targetFrameScale) == "number") and SFramesDB.targetFrameScale or 1 f:SetScale(frameScale) f:SetMovable(true) f:EnableMouse(true) f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", function() if IsAltKeyDown() or SFrames.isUnlocked then f:StartMoving() end end) f:SetScript("OnDragStop", function() f:StopMovingOrSizing() if not SFramesDB then SFramesDB = {} end if not SFramesDB.Positions then SFramesDB.Positions = {} end local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint() SFramesDB.Positions["TargetFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs } end) f:RegisterForClicks("LeftButtonUp", "RightButtonUp") f:SetScript("OnClick", function() if arg1 == "LeftButton" then if TryDropCursorOnUnit(this.unit) then return end if SpellIsTargeting and SpellIsTargeting() then SpellTargetUnit(this.unit) end elseif arg1 == "RightButton" then if SpellIsTargeting() then SpellStopTargeting() return end HideDropDownMenu(1) TargetFrameDropDown.unit = "target" TargetFrameDropDown.name = UnitName("target") TargetFrameDropDown.initialize = TargetFrameDropDown_Initialize ToggleDropDownMenu(1, nil, TargetFrameDropDown, "SFramesTargetFrame", 120, 10) end end) f:SetScript("OnReceiveDrag", function() if TryDropCursorOnUnit(this.unit) then return end if SpellIsTargeting and SpellIsTargeting() then SpellTargetUnit(this.unit) end end) SFrames:CreateUnitBackdrop(f) -- 3D Portrait (Right side for target) local pWidth = SFrames.Config.portraitWidth f.portrait = CreateFrame("PlayerModel", nil, f) f.portrait:SetWidth(pWidth) f.portrait:SetHeight(SFrames.Config.height - 2) f.portrait:SetPoint("RIGHT", f, "RIGHT", -1, 0) local pbg = CreateFrame("Frame", nil, f) pbg:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -1, 0) pbg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) pbg:SetFrameLevel(f:GetFrameLevel()) SFrames:CreateUnitBackdrop(pbg) f.portraitBG = pbg -- Health Bar (Left side) f.health = SFrames:CreateStatusBar(f, "SFramesTargetHealth") f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0) f.health:SetHeight((SFrames.Config.height - 2) * 0.82 - 1) local hbg = CreateFrame("Frame", nil, f) hbg:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1) hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1) hbg:SetFrameLevel(f:GetFrameLevel() - 1) SFrames:CreateUnitBackdrop(hbg) f.healthBGFrame = hbg -- Add a dark backdrop behind the health texture f.health.bg = f.health:CreateTexture(nil, "BACKGROUND") f.health.bg:SetAllPoints() f.health.bg:SetTexture(SFrames:GetTexture()) f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) -- Heal prediction overlay (incoming heals) f.health.healPredMine = f.health:CreateTexture(nil, "OVERLAY") f.health.healPredMine:SetTexture(SFrames:GetTexture()) f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78) f.health.healPredMine:Hide() f.health.healPredOther = f.health:CreateTexture(nil, "OVERLAY") f.health.healPredOther:SetTexture(SFrames:GetTexture()) f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5) f.health.healPredOther:Hide() -- Power Bar f.power = SFrames:CreateStatusBar(f, "SFramesTargetPower") f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) f.power:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMLEFT", -1, 0) local powerbg = CreateFrame("Frame", nil, f) powerbg:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1) powerbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1) powerbg:SetFrameLevel(f:GetFrameLevel() - 1) SFrames:CreateUnitBackdrop(powerbg) f.powerBGFrame = powerbg -- Add a dark backdrop behind the power texture f.power.bg = f.power:CreateTexture(nil, "BACKGROUND") f.power.bg:SetAllPoints() f.power.bg:SetTexture(SFrames:GetTexture()) f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) -- Class Icon Badge (overlaid on portrait, top-right corner with 1/3 outside) f.classIcon = SFrames:CreateClassIcon(f, 16) f.classIcon.overlay:SetPoint("CENTER", f.portrait, "TOPRIGHT", 0, 0) -- 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.powerText = SFrames:CreateFontString(f.power, 10, "RIGHT") f.powerText:SetPoint("RIGHT", f.power, "RIGHT", -4, 0) -- Outline/shadow setup for text to make it pop f.nameText:SetShadowColor(0, 0, 0, 1) f.nameText:SetShadowOffset(1, -1) f.healthText:SetShadowColor(0, 0, 0, 1) f.healthText:SetShadowOffset(1, -1) f.powerText:SetShadowColor(0, 0, 0, 1) f.powerText:SetShadowOffset(1, -1) -- Combo Points f.comboText = SFrames:CreateFontString(f, 20, "CENTER") f.comboText:SetPoint("CENTER", f.portrait, "CENTER", 0, 0) f.comboText:SetTextColor(1, 0.8, 0) f.comboText:SetText("") -- Raid Target Icon (top center of health bar, half outside frame) local raidIconSize = 22 local raidIconOvr = CreateFrame("Frame", nil, f) raidIconOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 5) raidIconOvr:SetWidth(raidIconSize) raidIconOvr:SetHeight(raidIconSize) raidIconOvr:SetPoint("CENTER", f.health, "TOP", 0, 0) f.raidIcon = raidIconOvr:CreateTexture(nil, "OVERLAY") f.raidIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons") f.raidIcon:SetAllPoints(raidIconOvr) f.raidIcon:Hide() f.raidIconOverlay = raidIconOvr self.frame = f self:ApplyConfig() f:Hide() SFrames:RegisterEvent("PLAYER_TARGET_CHANGED", function() self:OnTargetChanged() end) SFrames:RegisterEvent("UNIT_HEALTH", function() if arg1 == "target" then self:UpdateHealth() end end) SFrames:RegisterEvent("UNIT_MAXHEALTH", function() if arg1 == "target" then self:UpdateHealth() end end) SFrames:RegisterEvent("UNIT_MANA", function() if arg1 == "target" then self:UpdatePower() end end) SFrames:RegisterEvent("UNIT_MAXMANA", function() if arg1 == "target" then self:UpdatePower() end end) SFrames:RegisterEvent("UNIT_ENERGY", function() if arg1 == "target" then self:UpdatePower() end end) SFrames:RegisterEvent("UNIT_MAXENERGY", function() if arg1 == "target" then self:UpdatePower() end end) SFrames:RegisterEvent("UNIT_RAGE", function() if arg1 == "target" then self:UpdatePower() end end) SFrames:RegisterEvent("UNIT_MAXRAGE", function() if arg1 == "target" then self:UpdatePower() end end) SFrames:RegisterEvent("PLAYER_COMBO_POINTS", function() self:UpdateComboPoints() end) SFrames:RegisterEvent("UNIT_DISPLAYPOWER", function() if arg1 == "target" then self:UpdatePowerType() end end) SFrames:RegisterEvent("UNIT_PORTRAIT_UPDATE", function() if arg1 == "target" then self.frame.portrait:SetUnit("target") self.frame.portrait:SetCamera(0) self.frame.portrait:SetPosition(-1.0, 0, 0) end end) SFrames:RegisterEvent("UNIT_DYNAMIC_FLAGS", function() if arg1 == "target" then self:UpdateAll() end end) SFrames:RegisterEvent("UNIT_FACTION", function() if arg1 == "target" then self:UpdateAll() end end) SFrames:RegisterEvent("RAID_TARGET_UPDATE", function() self:UpdateRaidIcon() end) self:CreateAuras() self:CreateCastbar() self:InitializeDistanceFrame() f.unit = "target" f:SetScript("OnEnter", function() GameTooltip_SetDefaultAnchor(GameTooltip, this) GameTooltip:SetUnit(this.unit) GameTooltip:Show() end) f:SetScript("OnLeave", function() GameTooltip:Hide() end) -- If target already exists on load (e.g. after /reload), show and update it immediately self:OnTargetChanged() -- Distance Updater removed from target frame end function SFrames.Target:OnTargetChanged() if UnitExists("target") then self.frame:Show() self:UpdateAll() -- Force distance update immediately if SFrames.Target.distanceFrame then local dist = self:GetDistance("target") SFrames.Target.distanceFrame.text:SetText(dist or "---") if not (SFramesDB and SFramesDB.targetDistanceEnabled == false) then SFrames.Target.distanceFrame:Show() else SFrames.Target.distanceFrame:Hide() end end else self.frame:Hide() if SFrames.Target.distanceFrame then SFrames.Target.distanceFrame:Hide() end end end function SFrames.Target:UpdateAll() self:UpdateHealth() self:UpdatePowerType() self:UpdatePower() self:UpdateComboPoints() self:UpdateRaidIcon() self:UpdateAuras() self.frame.portrait:SetUnit("target") self.frame.portrait:SetCamera(0) self.frame.portrait:Hide() self.frame.portrait:Show() self.frame.portrait:SetPosition(-1.0, 0, 0) local name = UnitName("target") or "" local level = UnitLevel("target") local levelText = level -- Difficulty Color logic local function RGBToHex(r, g, b) return string.format("|cff%02x%02x%02x", r*255, g*255, b*255) end local function GetLevelDiffColor(targetLevel) local playerLevel = UnitLevel("player") if targetLevel == -1 then return 1, 0, 0 end -- Skull local diff = targetLevel - playerLevel if diff >= 5 then return 1, 0.1, 0.1 -- Red elseif diff >= 3 then return 1, 0.5, 0.25 -- Orange elseif diff >= -2 then return 1, 1, 0 -- Yellow elseif -diff <= GetQuestGreenRange() then return 0.25, 0.75, 0.25 -- Green else return 0.5, 0.5, 0.5 -- Grey end end local levelColor = RGBToHex(1, 1, 1) -- default white if level == -1 then levelText = "??" levelColor = RGBToHex(1, 0, 0) -- skull is always red else local r, g, b = GetLevelDiffColor(level) levelColor = RGBToHex(r, g, b) end local classif = UnitClassification("target") if classif == "elite" or classif == "rareelite" then levelText = levelText .. "+" elseif classif == "rare" then levelText = levelText .. "R" elseif classif == "worldboss" then levelText = "??" levelColor = RGBToHex(1, 0, 0) end local formattedLevel = levelColor .. levelText .. "|r" -- Toggle level display from config DB if SFramesDB and SFramesDB.showLevel == false then formattedLevel = "" else formattedLevel = formattedLevel .. " " end local showClassText = not (SFramesDB and SFramesDB.targetShowClass == false) if showClassText and UnitIsPlayer("target") then local localizedClass = UnitClass("target") if localizedClass and localizedClass ~= "" then name = name .. " " .. localizedClass end end if UnitIsPlayer("target") and not (SFramesDB and SFramesDB.targetShowClassIcon == false) then local _, tClass = UnitClass("target") SFrames:SetClassIcon(self.frame.classIcon, tClass) else if self.frame.classIcon then self.frame.classIcon:Hide() if self.frame.classIcon.overlay then self.frame.classIcon.overlay:Hide() end end end local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false) if UnitIsPlayer("target") and useClassColor then local _, class = UnitClass("target") if class and SFrames.Config.colors.class[class] then local color = SFrames.Config.colors.class[class] -- Set Health Color self.frame.health:SetStatusBarColor(color.r, color.g, color.b) -- Apply Class Color to Name self.frame.nameText:SetText(formattedLevel .. name) self.frame.nameText:SetTextColor(color.r, color.g, color.b) else self.frame.health:SetStatusBarColor(0, 1, 0) self.frame.nameText:SetText(formattedLevel .. name) self.frame.nameText:SetTextColor(1, 1, 1) end else local r, g, b = 0.85, 0.77, 0.36 -- Neutral (Softer Yellow) local isTapped = UnitIsTapped("target") and not UnitIsTappedByPlayer("target") if isTapped then r, g, b = 0.53, 0.53, 0.53 -- Tapped by others (Grey) name = name .. " (无拾取)" -- For colorblind elseif UnitIsEnemy("player", "target") then r, g, b = 0.78, 0.25, 0.25 -- Enemy (Softer Red) elseif UnitIsFriend("player", "target") then r, g, b = 0.33, 0.59, 0.33 -- Friend (Softer Green) end self.frame.health:SetStatusBarColor(r, g, b) -- Color Name same as reaction self.frame.nameText:SetText(formattedLevel .. name) self.frame.nameText:SetTextColor(r, g, b) end end function SFrames.Target:UpdateHealth() local hp = UnitHealth("target") local maxHp = UnitHealthMax("target") self.frame.health:SetMinMaxValues(0, maxHp) self.frame.health:SetValue(hp) local displayHp, displayMax = hp, maxHp if (not SFramesDB or SFramesDB.mobRealHealth ~= false) and maxHp == 100 and not UnitIsPlayer("target") and not UnitPlayerControlled("target") then local name = UnitName("target") local level = UnitLevel("target") if name and level and LibMobHealth_Cache then local key = name .. ":" .. level local realMax = LibMobHealth_Cache[key] if realMax and realMax > 0 then displayMax = realMax displayHp = math.floor(realMax * hp / 100) end end end if displayMax > 0 then self.frame.healthText:SetText(displayHp .. " / " .. displayMax) else self.frame.healthText:SetText(displayHp) end self:UpdateHealPrediction() end function SFrames.Target:UpdateHealPrediction() if not (self.frame and self.frame.health and self.frame.health.healPredMine and self.frame.health.healPredOther) then return end local predMine = self.frame.health.healPredMine local predOther = self.frame.health.healPredOther local function HidePredictions() predMine:Hide() predOther:Hide() end if not UnitExists("target") then HidePredictions() return end local hp = UnitHealth("target") or 0 local maxHp = UnitHealthMax("target") or 0 if maxHp <= 0 or hp >= maxHp then HidePredictions() return end local _, mineIncoming, othersIncoming = GetIncomingHeals("target") local missing = maxHp - hp if missing <= 0 then HidePredictions() return end local mineShown = math.min(math.max(0, mineIncoming), missing) local remaining = missing - mineShown local otherShown = math.min(math.max(0, othersIncoming), remaining) if mineShown <= 0 and otherShown <= 0 then HidePredictions() return end local barWidth = self.frame.health:GetWidth() or 0 if barWidth <= 0 then HidePredictions() return end local currentWidth = math.floor((hp / maxHp) * barWidth + 0.5) if currentWidth < 0 then currentWidth = 0 end if currentWidth > barWidth then currentWidth = barWidth end local availableWidth = barWidth - currentWidth if availableWidth <= 0 then HidePredictions() return end local mineWidth = math.floor((mineShown / maxHp) * barWidth + 0.5) local otherWidth = math.floor((otherShown / maxHp) * barWidth + 0.5) if mineWidth < 0 then mineWidth = 0 end if otherWidth < 0 then otherWidth = 0 end if mineWidth > availableWidth then mineWidth = availableWidth end if otherWidth > (availableWidth - mineWidth) then otherWidth = availableWidth - mineWidth end if mineWidth > 0 then predMine:ClearAllPoints() predMine:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth, 0) predMine:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth, 0) predMine:SetWidth(mineWidth) predMine:Show() else predMine:Hide() end if otherWidth > 0 then predOther:ClearAllPoints() predOther:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth + mineWidth, 0) predOther:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth + mineWidth, 0) predOther:SetWidth(otherWidth) predOther:Show() else predOther:Hide() end end function SFrames.Target:UpdatePowerType() local powerType = UnitPowerType("target") local color = SFrames.Config.colors.power[powerType] if color then self.frame.power:SetStatusBarColor(color.r, color.g, color.b) else self.frame.power:SetStatusBarColor(0, 0, 1) end end function SFrames.Target:UpdatePower() local power = UnitMana("target") local maxPower = UnitManaMax("target") self.frame.power:SetMinMaxValues(0, maxPower) self.frame.power:SetValue(power) if maxPower > 0 then self.frame.powerText:SetText(power .. " / " .. maxPower) else self.frame.powerText:SetText("") end end function SFrames.Target:UpdateComboPoints() local points = GetComboPoints() if points > 0 then self.frame.comboText:SetText(points) else self.frame.comboText:SetText("") end end function SFrames.Target:UpdateRaidIcon() if not (self.frame and self.frame.raidIcon) then return end if not GetRaidTargetIndex then self.frame.raidIcon:Hide() return end if not UnitExists("target") then self.frame.raidIcon:Hide() return end local index = GetRaidTargetIndex("target") if index and index > 0 and index <= 8 then local col = math.mod(index - 1, 4) local row = math.floor((index - 1) / 4) self.frame.raidIcon:SetTexCoord(col * 0.25, (col + 1) * 0.25, row * 0.25, (row + 1) * 0.25) self.frame.raidIcon:Show() else self.frame.raidIcon:Hide() end end -------------------------------------------------------------------------------- -- Target Auras (Buffs / Debuffs) -------------------------------------------------------------------------------- function SFrames.Target:CreateAuras() self.frame.buffs = {} self.frame.debuffs = {} -- Target Buffs (Top left to right) for i = 1, 16 do local b = CreateFrame("Button", "SFramesTargetBuff"..i, self.frame) b:SetWidth(AURA_SIZE) b:SetHeight(AURA_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) -- Tooltip support b:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") GameTooltip:SetUnitBuff("target", this:GetID()) end) b:SetScript("OnLeave", function() GameTooltip:Hide() end) -- Default row anchor (Starting bottom left for buffs) if i == 1 then b:SetPoint("TOPLEFT", self.frame, "BOTTOMLEFT", 0, -1) elseif math.mod(i - 1, 8) == 0 then b:SetPoint("TOP", self.frame.buffs[i-8], "BOTTOM", 0, -AURA_ROW_SPACING) else b:SetPoint("LEFT", self.frame.buffs[i-1], "RIGHT", AURA_SPACING, 0) end b:Hide() self.frame.buffs[i] = b end -- Target Debuffs (Bottom left to right) for i = 1, 16 do local b = CreateFrame("Button", "SFramesTargetDebuff"..i, self.frame) b:SetWidth(AURA_SIZE) b:SetHeight(AURA_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) -- Tooltip support b:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") GameTooltip:SetUnitDebuff("target", this:GetID()) end) b:SetScript("OnLeave", function() GameTooltip:Hide() end) b:SetScript("OnLeave", function() GameTooltip:Hide() end) -- Debuff anchors are recalulated dynamically in UpdateAuras if i == 1 then b:SetPoint("TOPLEFT", self.frame, "BOTTOMLEFT", 0, -1) elseif math.mod(i - 1, 8) == 0 then b:SetPoint("TOP", self.frame.debuffs[i-8], "BOTTOM", 0, -AURA_ROW_SPACING) else b:SetPoint("LEFT", self.frame.debuffs[i-1], "RIGHT", AURA_SPACING, 0) end b:Hide() self.frame.debuffs[i] = b end SFrames:RegisterEvent("UNIT_AURA", function() if arg1 == "target" then self:UpdateAuras() end end) self.auraUpdater = CreateFrame("Frame", nil, self.frame) self.auraUpdater.timer = 0 self.auraUpdater:SetScript("OnUpdate", function() this.timer = this.timer + arg1 if this.timer >= 0.25 then SFrames.Target:TickAuras() SFrames.Target:UpdateHealPrediction() this.timer = 0 end end) end function SFrames.Target:TickAuras() if not UnitExists("target") then return end local timeNow = GetTime() local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime local hasNP = NanamiPlates_SpellDB and NanamiPlates_SpellDB.FindEffectData local targetName, targetLevel, targetGUID if hasNP then targetName = UnitName("target") targetLevel = UnitLevel("target") or 0 targetGUID = UnitGUID and UnitGUID("target") end -- Buffs for i = 1, 16 do local b = self.frame.buffs[i] if b:IsShown() and b.expirationTime then local timeLeft = b.expirationTime - timeNow if timeLeft > 0 and timeLeft < 3600 then if npFormat then local text, r, g, bc, a = npFormat(timeLeft) b.cdText:SetText(text) if r then b.cdText:SetTextColor(r, g, bc, a or 1) end else b.cdText:SetText(SFrames:FormatTime(timeLeft)) end else b.cdText:SetText("") end end end -- Debuffs: re-query SpellDB for live-accurate timers for i = 1, 16 do local b = self.frame.debuffs[i] if b:IsShown() then local timeLeft = nil if hasNP and b.effectName then local data = targetGUID and NanamiPlates_SpellDB:FindEffectData(targetGUID, targetLevel, b.effectName) if not data and targetName then data = NanamiPlates_SpellDB:FindEffectData(targetName, targetLevel, b.effectName) end if data and data.start and data.duration then local remaining = data.duration + data.start - timeNow if remaining > 0 then timeLeft = remaining b.expirationTime = timeNow + remaining end end end if not timeLeft and b.expirationTime then timeLeft = b.expirationTime - timeNow end if timeLeft and timeLeft > 0 and timeLeft < 3600 then if npFormat then local text, r, g, bc, a = npFormat(timeLeft) b.cdText:SetText(text) if r then b.cdText:SetTextColor(r, g, bc, a or 1) end else b.cdText:SetText(SFrames:FormatTime(timeLeft)) end else b.cdText:SetText("") end end end end function SFrames.Target:UpdateAuras() if not UnitExists("target") then return end local numBuffs = 0 -- Buffs for i = 1, 16 do local texture = UnitBuff("target", i) local b = self.frame.buffs[i] b:SetID(i) -- Ensure ID is set for tooltips if texture then b.icon:SetTexture(texture) -- Scrape tooltip for duration SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE") SFrames.Tooltip:ClearLines() SFrames.Tooltip:SetUnitBuff("target", i) local timeLeft = SFrames:GetAuraTimeLeft("target", i, true) if timeLeft and timeLeft > 0 then b.expirationTime = GetTime() + timeLeft b.cdText:SetText(SFrames:FormatTime(timeLeft)) else b.expirationTime = nil b.cdText:SetText("") end b:Show() numBuffs = numBuffs + 1 else b.expirationTime = nil b.cdText:SetText("") b:Hide() end end -- Dynamically re-anchor the first Debuff based on visible Buffs local firstDebuff = self.frame.debuffs[1] if firstDebuff then firstDebuff:ClearAllPoints() if numBuffs > 0 then -- Find the start of the LAST row of buffs local lastRowStart = math.floor((numBuffs - 1) / 8) * 8 + 1 firstDebuff:SetPoint("TOP", self.frame.buffs[lastRowStart], "BOTTOM", 0, -AURA_ROW_SPACING) else firstDebuff:SetPoint("TOPLEFT", self.frame, "BOTTOMLEFT", 0, -1) end end -- Debuffs local hasNP = NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime for i = 1, 16 do local texture = UnitDebuff("target", i) local b = self.frame.debuffs[i] b:SetID(i) if texture then b.icon:SetTexture(texture) local timeLeft = 0 local effectName = nil if hasNP then local effect, rank, _, stacks, dtype, duration, npTimeLeft, isOwn = NanamiPlates_SpellDB:UnitDebuff("target", i) effectName = effect if npTimeLeft and npTimeLeft > 0 then timeLeft = npTimeLeft elseif effect and effect ~= "" and duration and duration > 0 and NanamiPlates_Auras and NanamiPlates_Auras.timers then local unitKey = (UnitGUID and UnitGUID("target")) or UnitName("target") or "" local cached = NanamiPlates_Auras.timers[unitKey .. "_" .. effect] if not cached and UnitName("target") then cached = NanamiPlates_Auras.timers[UnitName("target") .. "_" .. effect] end if cached and cached.startTime and cached.duration then local remaining = cached.duration - (GetTime() - cached.startTime) if remaining > 0 then timeLeft = remaining end end end end if timeLeft <= 0 then SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE") SFrames.Tooltip:ClearLines() SFrames.Tooltip:SetUnitDebuff("target", i) timeLeft = SFrames:GetAuraTimeLeft("target", i, false) end if timeLeft and timeLeft > 0 then b.expirationTime = GetTime() + timeLeft b.effectName = effectName if npFormat then local text, r, g, bc, a = npFormat(timeLeft) b.cdText:SetText(text) if r then b.cdText:SetTextColor(r, g, bc, a or 1) end else b.cdText:SetText(SFrames:FormatTime(timeLeft)) end else b.expirationTime = nil b.effectName = nil b.cdText:SetText("") end b:Show() else b.expirationTime = nil b.effectName = nil b.cdText:SetText("") b:Hide() end end end -------------------------------------------------------------------------------- -- Target Castbar -------------------------------------------------------------------------------- function SFrames.Target:CreateCastbar() local cb = SFrames:CreateStatusBar(self.frame, "SFramesTargetCastbar") cb:SetHeight(SFrames.Config.castbarHeight) cb:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", 0, 6) cb:SetPoint("BOTTOMRIGHT", self.frame.portrait, "TOPRIGHT", -(SFrames.Config.castbarHeight + 6), 6) local cbbg = CreateFrame("Frame", nil, self.frame) cbbg:SetPoint("TOPLEFT", cb, "TOPLEFT", -1, 1) cbbg:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", 1, -1) cbbg:SetFrameLevel(cb:GetFrameLevel() - 1) SFrames:CreateUnitBackdrop(cbbg) cb.bg = cb:CreateTexture(nil, "BACKGROUND") cb.bg:SetAllPoints() cb.bg:SetTexture(SFrames:GetTexture()) cb.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) cb:SetStatusBarColor(1, 0.7, 0) cb.text = SFrames:CreateFontString(cb, 10, "LEFT") cb.text:SetPoint("LEFT", cb, "LEFT", 4, 0) cb.icon = cb:CreateTexture(nil, "ARTWORK") cb.icon:SetWidth(SFrames.Config.castbarHeight + 2) cb.icon:SetHeight(SFrames.Config.castbarHeight + 2) cb.icon:SetPoint("LEFT", cb, "RIGHT", 4, 0) cb.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) local ibg = CreateFrame("Frame", nil, self.frame) ibg:SetPoint("TOPLEFT", cb.icon, "TOPLEFT", -1, 1) ibg:SetPoint("BOTTOMRIGHT", cb.icon, "BOTTOMRIGHT", 1, -1) ibg:SetFrameLevel(cb:GetFrameLevel() - 1) SFrames:CreateUnitBackdrop(ibg) cb:Hide() cbbg:Hide() cb.icon:Hide() ibg:Hide() self.frame.castbar = cb self.frame.castbar.cbbg = cbbg self.frame.castbar.ibg = ibg self.frame.castbarUpdater = CreateFrame("Frame", nil, self.frame) self.frame.castbarUpdater:SetScript("OnUpdate", function() SFrames.Target:CastbarOnUpdate() end) end function SFrames.Target:CastbarOnUpdate() local cb = self.frame.castbar if not UnitExists("target") then cb:Hide() cb.cbbg:Hide() cb.icon:Hide() cb.ibg:Hide() return end -- Try to read cast from Vanilla extensions (SuperWoW or TurtleWoW modern API, or ShaguTweaks) local cast, nameSubtext, text, texture, startTime, endTime local _UnitCastingInfo = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo) if _UnitCastingInfo then cast, nameSubtext, text, texture, startTime, endTime = _UnitCastingInfo("target") end local channel local _UnitChannelInfo = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo) if not cast and _UnitChannelInfo then channel, nameSubtext, text, texture, startTime, endTime = _UnitChannelInfo("target") cast = channel end if cast and startTime and endTime then local duration = (endTime - startTime) / 1000 local cur = GetTime() - (startTime / 1000) if channel then cur = duration + (startTime / 1000) - GetTime() end if cur > duration then cur = duration end if cur < 0 then cur = 0 end cb:SetMinMaxValues(0, duration) cb:SetValue(cur) cb.text:SetText(cast) if texture then cb.icon:SetTexture(texture) end cb:SetAlpha(1) cb.cbbg:SetAlpha(1) cb.icon:SetAlpha(1) cb.ibg:SetAlpha(1) cb:Show() cb.cbbg:Show() cb.icon:Show() cb.ibg:Show() else cb:Hide() cb.cbbg:Hide() cb.icon:Hide() cb.ibg:Hide() end end -- Diagnostic Slash Command (Position Recovery) SLASH_NANAMIDIST1 = "/nanamidist" SlashCmdList["NANAMIDIST"] = function() if SFrames.Target.distanceFrame then SFrames.Target.distanceFrame:ClearAllPoints() SFrames.Target.distanceFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 100) SFrames.Target.distanceFrame:Show() DEFAULT_CHAT_FRAME:AddMessage("|cffffd100Nanami-UI:|r 距离显示已重置到屏幕中央。") end end