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

1047 lines
37 KiB
Lua

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()
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
b.cdText:SetText(SFrames:FormatTime(timeLeft))
else
b.cdText:SetText("")
end
end
end
for i = 1, 16 do
local b = self.frame.debuffs[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
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
for i = 1, 16 do
local texture = UnitDebuff("target", i)
local b = self.frame.debuffs[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:SetUnitDebuff("target", i)
local timeLeft = SFrames:GetAuraTimeLeft("target", i, false)
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()
else
b.expirationTime = 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