Files
Nanami-UI/Units/Target.lua
2026-03-24 15:56:28 +08:00

1650 lines
61 KiB
Lua

SFrames.Target = {}
local _A = SFrames.ActiveTheme
local targetCLCast = nil
local spellIconCache = {}
local SPELL_ICONS = {
-- Mage
["Fireball"] = "Spell_Fire_FlameBolt",
["火球术"] = "Spell_Fire_FlameBolt",
["Frostbolt"] = "Spell_Frost_FrostBolt02",
["寒冰箭"] = "Spell_Frost_FrostBolt02",
["Polymorph"] = "Spell_Nature_Polymorph",
["变形术"] = "Spell_Nature_Polymorph",
["Arcane Missiles"] = "Spell_Nature_StarFall",
["奥术飞弹"] = "Spell_Nature_StarFall",
["Pyroblast"] = "Spell_Fire_Fireball02",
["炎爆术"] = "Spell_Fire_Fireball02",
["Scorch"] = "Spell_Fire_SoulBurn",
["灼烧"] = "Spell_Fire_SoulBurn",
["Flamestrike"] = "Spell_Fire_SelfDestruct",
["烈焰风暴"] = "Spell_Fire_SelfDestruct",
["Blizzard"] = "Spell_Frost_IceStorm",
["暴风雪"] = "Spell_Frost_IceStorm",
-- Warlock
["Shadow Bolt"] = "Spell_Shadow_ShadowBolt",
["暗影箭"] = "Spell_Shadow_ShadowBolt",
["Fear"] = "Spell_Shadow_Possession",
["恐惧术"] = "Spell_Shadow_Possession",
["Immolate"] = "Spell_Fire_Immolation",
["献祭"] = "Spell_Fire_Immolation",
["Soul Fire"] = "Spell_Fire_Fireball02",
["灵魂之火"] = "Spell_Fire_Fireball02",
["Drain Life"] = "Spell_Shadow_LifeDrain02",
["吸取生命"] = "Spell_Shadow_LifeDrain02",
["Drain Mana"] = "Spell_Shadow_SiphonMana",
["吸取法力"] = "Spell_Shadow_SiphonMana",
["Rain of Fire"] = "Spell_Shadow_RainOfFire",
["火焰之雨"] = "Spell_Shadow_RainOfFire",
["Hellfire"] = "Spell_Fire_Incinerate",
["地狱烈焰"] = "Spell_Fire_Incinerate",
-- Priest
["Greater Heal"] = "Spell_Holy_GreaterHeal",
["强效治疗术"] = "Spell_Holy_GreaterHeal",
["Flash Heal"] = "Spell_Holy_FlashHeal",
["快速治疗"] = "Spell_Holy_FlashHeal",
["Heal"] = "Spell_Holy_Heal",
["治疗术"] = "Spell_Holy_Heal",
["Smite"] = "Spell_Holy_HolySmite",
["惩击"] = "Spell_Holy_HolySmite",
["Mind Blast"] = "Spell_Shadow_UnholyFrenzy",
["心灵震爆"] = "Spell_Shadow_UnholyFrenzy",
["Mind Flay"] = "Spell_Shadow_SiphonMana",
["精神鞭笞"] = "Spell_Shadow_SiphonMana",
["Mind Control"] = "Spell_Shadow_ShadowWordDominate",
["精神控制"] = "Spell_Shadow_ShadowWordDominate",
["Holy Fire"] = "Spell_Holy_SearingLight",
["神圣之火"] = "Spell_Holy_SearingLight",
["Resurrection"] = "Spell_Holy_Resurrection",
["复活术"] = "Spell_Holy_Resurrection",
-- Shaman
["Lightning Bolt"] = "Spell_Nature_Lightning",
["闪电箭"] = "Spell_Nature_Lightning",
["Chain Lightning"] = "Spell_Nature_ChainLightning",
["闪电链"] = "Spell_Nature_ChainLightning",
["Healing Wave"] = "Spell_Nature_MagicImmunity",
["治疗波"] = "Spell_Nature_MagicImmunity",
["Lesser Healing Wave"] = "Spell_Nature_HealingWaveLesser",
["次级治疗波"] = "Spell_Nature_HealingWaveLesser",
["Chain Heal"] = "Spell_Nature_HealingWaveGreater",
["治疗链"] = "Spell_Nature_HealingWaveGreater",
["Ancestral Spirit"] = "Spell_Nature_Regenerate",
["先祖之魂"] = "Spell_Nature_Regenerate",
-- Druid
["Wrath"] = "Spell_Nature_AbolishMagic",
["愤怒"] = "Spell_Nature_AbolishMagic",
["Starfire"] = "Spell_Arcane_StarFire",
["星火术"] = "Spell_Arcane_StarFire",
["Regrowth"] = "Spell_Nature_ResistNature",
["愈合"] = "Spell_Nature_ResistNature",
["Healing Touch"] = "Spell_Nature_HealingTouch",
["治疗之触"] = "Spell_Nature_HealingTouch",
["Entangling Roots"] = "Spell_Nature_StrangleVines",
["纠缠根须"] = "Spell_Nature_StrangleVines",
["Hibernate"] = "Spell_Nature_Sleep",
["休眠"] = "Spell_Nature_Sleep",
["Rebirth"] = "Spell_Nature_Reincarnation",
["复生"] = "Spell_Nature_Reincarnation",
["Tranquility"] = "Spell_Nature_Tranquility",
["宁静"] = "Spell_Nature_Tranquility",
["Moonfire"] = "Spell_Nature_StarFall",
["月火术"] = "Spell_Nature_StarFall",
-- Paladin
["Holy Light"] = "Spell_Holy_HolyBolt",
["圣光术"] = "Spell_Holy_HolyBolt",
["Flash of Light"] = "Spell_Holy_FlashHeal",
["圣光闪现"] = "Spell_Holy_FlashHeal",
["Hammer of Wrath"] = "Spell_Holy_SealOfMight",
["愤怒之锤"] = "Spell_Holy_SealOfMight",
["Exorcism"] = "Spell_Holy_Excorcism_02",
["驱邪术"] = "Spell_Holy_Excorcism_02",
["Redemption"] = "Spell_Holy_Resurrection",
["救赎"] = "Spell_Holy_Resurrection",
-- Hunter
["Aimed Shot"] = "INV_Spear_07",
["瞄准射击"] = "INV_Spear_07",
["Multi-Shot"] = "Ability_UpgradeMoonGlaive",
["多重射击"] = "Ability_UpgradeMoonGlaive",
["Volley"] = "Ability_Marksmanship",
["乱射"] = "Ability_Marksmanship",
["Revive Pet"] = "Ability_Hunter_BeastSoothe",
["复活宠物"] = "Ability_Hunter_BeastSoothe",
-- Common NPC / generic
["Shoot"] = "Ability_Marksmanship",
["射击"] = "Ability_Marksmanship",
["Mend"] = "Spell_Holy_Heal",
["修补"] = "Spell_Holy_Heal",
["Rejuvenation"] = "Spell_Nature_Rejuvenation",
["回春术"] = "Spell_Nature_Rejuvenation",
}
for k, v in pairs(SPELL_ICONS) do
SPELL_ICONS[k] = "Interface\\Icons\\" .. v
end
local function BuildSpellIconCache()
if not GetSpellName or not GetSpellTexture then return end
local i = 1
while true do
local name = GetSpellName(i, "spell")
if not name then break end
local tex = GetSpellTexture(i, "spell")
if tex then spellIconCache[name] = tex end
i = i + 1
end
end
local function GetSpellIcon(spellName)
if not spellName then return nil end
local tex = spellIconCache[spellName]
or SPELL_ICONS[spellName]
or (NanamiPlates_CombatLog and NanamiPlates_CombatLog.castIcons
and NanamiPlates_CombatLog.castIcons[spellName])
if tex then return tex end
if GetSpellName and GetSpellTexture then
local i = 1
while true do
local name = GetSpellName(i, "spell")
if not name then break end
if name == spellName then
tex = GetSpellTexture(i, "spell")
if tex then
spellIconCache[spellName] = tex
return tex
end
end
i = i + 1
end
end
return nil
end
SFrames.GetSpellIcon = GetSpellIcon
SFrames.BuildSpellIconCache = BuildSpellIconCache
local function CLMatch(str, pattern)
if not str or not pattern then return nil end
local pat = string.gsub(pattern, "%%%d?%$?s", "(.+)")
pat = string.gsub(pat, "%%%d?%$?d", "(%%d+)")
for a, b, c, d in string.gfind(str, pat) do
return a, b, c, d
end
return nil
end
local function Clamp(value, minValue, maxValue)
if value < minValue then
return minValue
end
if value > maxValue then
return maxValue
end
return value
end
local DIST_BASE_WIDTH = 80
local DIST_BASE_HEIGHT = 24
local DIST_BASE_FONTSIZE = 14
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
local db = SFramesDB or {}
local showPortrait = db.targetShowPortrait ~= false
local frameAlpha = tonumber(db.targetFrameAlpha) or 1
frameAlpha = Clamp(frameAlpha, 0.1, 1.0)
f:SetScale(cfg.scale)
f:SetWidth(cfg.width)
f:SetHeight(cfg.height)
f:SetAlpha(frameAlpha)
if showPortrait then
if f.portrait then
f.portrait:SetWidth(cfg.portraitWidth)
f.portrait:SetHeight(cfg.height - 2)
f.portrait:Show()
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)
f.portraitBG:Show()
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.classIcon and f.classIcon.overlay then
f.classIcon.overlay:ClearAllPoints()
f.classIcon.overlay:SetPoint("CENTER", f.portrait, "TOPRIGHT", 0, 0)
end
if f.comboText then
f.comboText:ClearAllPoints()
f.comboText:SetPoint("CENTER", f.portrait, "CENTER", 0, 0)
end
else
if f.portrait then f.portrait:Hide() end
if f.portraitBG then f.portraitBG:Hide() end
if f.health then
f.health:ClearAllPoints()
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1)
f.health:SetHeight(cfg.healthHeight)
end
if f.classIcon and f.classIcon.overlay then
f.classIcon.overlay:ClearAllPoints()
f.classIcon.overlay:SetPoint("CENTER", f, "TOPRIGHT", -8, 0)
end
if f.comboText then
f.comboText:ClearAllPoints()
f.comboText:SetPoint("RIGHT", f.health, "RIGHT", -4, 0)
end
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 f.castbar then
f.castbar:ClearAllPoints()
if showPortrait then
f.castbar:SetPoint("BOTTOMLEFT", f, "TOPLEFT", 0, 6)
f.castbar:SetPoint("BOTTOMRIGHT", f.portrait, "TOPRIGHT", -(SFrames.Config.castbarHeight + 6), 6)
else
f.castbar:SetPoint("BOTTOMLEFT", f, "TOPLEFT", 0, 6)
f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", -(SFrames.Config.castbarHeight + 6), 6)
end
end
if self.distanceFrame then
local dScale = tonumber(SFramesDB and SFramesDB.targetDistanceScale) or 1
self:ApplyDistanceScale(dScale)
end
if UnitExists("target") then
self:UpdateAll()
end
end
function SFrames.Target:ApplyDistanceScale(scale)
local f = self.distanceFrame
if not f then return end
scale = Clamp(tonumber(scale) or 1, 0.7, 1.8)
f:SetWidth(DIST_BASE_WIDTH * scale)
f:SetHeight(DIST_BASE_HEIGHT * scale)
if f.text then
local fontPath = SFrames:GetFont()
local outline = (SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
local fontSize = math.max(8, math.floor(DIST_BASE_FONTSIZE * scale + 0.5))
f.text:SetFont(fontPath, fontSize, outline)
end
end
function SFrames.Target:InitializeDistanceFrame()
local f = CreateFrame("Button", "SFramesTargetDistanceFrame", UIParent)
f:SetFrameStrata("HIGH")
local dScale = (SFramesDB and type(SFramesDB.targetDistanceScale) == "number") and SFramesDB.targetDistanceScale or 1
dScale = Clamp(dScale, 0.7, 1.8)
f:SetWidth(DIST_BASE_WIDTH * dScale)
f:SetHeight(DIST_BASE_HEIGHT * dScale)
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
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)
local fontSize = math.max(8, math.floor(DIST_BASE_FONTSIZE * dScale + 0.5))
f.text = SFrames:CreateFontString(f, fontSize, "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()
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" and self.frame.portrait and not (SFramesDB and SFramesDB.targetShowPortrait == false) 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()
self:InitCastDetection()
BuildSpellIconCache()
SFrames:RegisterEvent("SPELLS_CHANGED", BuildSpellIconCache)
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()
-- Register movers
if SFrames.Movers and SFrames.Movers.RegisterMover then
SFrames.Movers:RegisterMover("TargetFrame", f, "目标",
"CENTER", "UIParent", "CENTER", 200, -100)
if SFrames.Target.distanceFrame then
SFrames.Movers:RegisterMover("TargetDistanceFrame", SFrames.Target.distanceFrame, "目标距离",
"CENTER", "UIParent", "CENTER", 0, 100)
end
end
end
function SFrames.Target:InitCastDetection()
local castFrame = CreateFrame("Frame", nil, UIParent)
castFrame:RegisterEvent("UNIT_CASTEVENT")
castFrame:RegisterEvent("SPELLCAST_START")
castFrame:RegisterEvent("SPELLCAST_STOP")
castFrame:RegisterEvent("SPELLCAST_FAILED")
castFrame:RegisterEvent("SPELLCAST_INTERRUPTED")
castFrame:RegisterEvent("SPELLCAST_CHANNEL_START")
castFrame:RegisterEvent("SPELLCAST_CHANNEL_STOP")
local CL_EVENTS = {
"CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE",
"CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE",
"CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE",
"CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF",
"CHAT_MSG_SPELL_CREATURE_VS_PARTY_BUFF",
"CHAT_MSG_SPELL_CREATURE_VS_SELF_BUFF",
"CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE",
"CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF",
"CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE",
"CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF",
"CHAT_MSG_SPELL_PARTY_DAMAGE",
"CHAT_MSG_SPELL_PARTY_BUFF",
"CHAT_MSG_SPELL_SELF_DAMAGE",
"CHAT_MSG_SPELL_SELF_BUFF",
}
for _, ev in ipairs(CL_EVENTS) do
castFrame:RegisterEvent(ev)
end
local function ResolveSelfIcon(spellName)
local texture
local _UCI = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo)
if _UCI then
local _, _, _, tex = _UCI("player")
texture = tex
end
if not texture then
local _UCH = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo)
if _UCH then
local _, _, _, tex = _UCH("player")
texture = tex
end
end
if not texture and SFrames.castdb and UnitGUID then
local guid = UnitGUID("player")
if guid and SFrames.castdb[guid] and SFrames.castdb[guid].icon then
texture = SFrames.castdb[guid].icon
end
end
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
texture = GetSpellIcon(spellName) or texture
end
return texture or "Interface\\Icons\\INV_Misc_QuestionMark"
end
castFrame:SetScript("OnEvent", function()
-- Player's own cast events (for self-target and friendly-target-is-self)
if event == "SPELLCAST_START" then
if UnitExists("target") and UnitIsUnit("target", "player") then
local spellName = arg1
local duration = arg2
targetCLCast = {
spell = spellName,
startTime = GetTime(),
duration = (duration or 2000) / 1000,
icon = ResolveSelfIcon(spellName),
channel = false,
}
end
return
end
if event == "SPELLCAST_CHANNEL_START" then
if UnitExists("target") and UnitIsUnit("target", "player") then
local duration = arg1
local spellName = arg2
targetCLCast = {
spell = spellName,
startTime = GetTime(),
duration = (duration or 2000) / 1000,
icon = ResolveSelfIcon(spellName),
channel = true,
}
end
return
end
if event == "SPELLCAST_STOP" or event == "SPELLCAST_FAILED"
or event == "SPELLCAST_INTERRUPTED" or event == "SPELLCAST_CHANNEL_STOP" then
if UnitExists("target") and UnitIsUnit("target", "player") then
targetCLCast = nil
end
return
end
-- UNIT_CASTEVENT (SuperWoW / TurtleWoW): works for all units
if event == "UNIT_CASTEVENT" then
if not UnitGUID or not UnitExists("target") then return end
local targetGUID = UnitGUID("target")
if not targetGUID or arg1 ~= targetGUID then return end
if arg3 == "START" or arg3 == "CAST" or arg3 == "CHANNEL" then
local spell, icon
if SpellInfo and arg4 then
spell, _, icon = SpellInfo(arg4)
end
spell = spell or "Casting"
if not icon or icon == "" or icon == "Interface\\Icons\\INV_Misc_QuestionMark" then
icon = GetSpellIcon(spell) or icon
end
icon = icon or "Interface\\Icons\\INV_Misc_QuestionMark"
targetCLCast = {
spell = spell,
startTime = GetTime(),
duration = (arg5 or 2000) / 1000,
icon = icon,
channel = (arg3 == "CHANNEL"),
}
elseif arg3 == "FAIL" then
targetCLCast = nil
end
return
end
-- Combat log parsing: "X begins to cast Y" (third-person, all other units)
if not arg1 or not UnitExists("target") then return end
local targetName = UnitName("target")
if not targetName then return end
local msg = arg1
local caster, spell
local castStart = SPELLCASTOTHERSTART or "%s begins to cast %s."
caster, spell = CLMatch(msg, castStart)
if not caster then
local perfStart = SPELLPERFORMOTHERSTART or "%s begins to perform %s."
caster, spell = CLMatch(msg, perfStart)
end
if caster and caster == targetName and spell then
local icon = GetSpellIcon(spell) or "Interface\\Icons\\INV_Misc_QuestionMark"
targetCLCast = {
spell = spell,
startTime = GetTime(),
duration = 2.0,
icon = icon,
channel = false,
}
return
end
if targetCLCast then
local isFail = false
for u in string.gfind(msg, "(.+)'s .+ is interrupted%.") do
if u == targetName then isFail = true end
end
if not isFail then
for u in string.gfind(msg, "(.+)'s .+ fails%.") do
if u == targetName then isFail = true end
end
end
if not isFail and SPELLINTERRUPTOTHEROTHER then
local a = CLMatch(msg, SPELLINTERRUPTOTHEROTHER)
if a == targetName then isFail = true end
end
if isFail then targetCLCast = nil end
end
end)
end
function SFrames.Target:OnTargetChanged()
targetCLCast = nil
if UnitExists("target") then
self.frame:Show()
self:UpdateAll()
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()
local showPortrait = not (SFramesDB and SFramesDB.targetShowPortrait == false)
if showPortrait and self.frame.portrait then
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)
end
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)
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
SFrames.Tooltip:ClearLines()
SFrames.Tooltip:SetUnitBuff("target", i)
local timeLeft = SFrames:GetAuraTimeLeft("target", i, true)
SFrames.Tooltip:Hide()
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)
SFrames.Tooltip:Hide()
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
local cast, texture, startTime, endTime, channel
-- 1) UnitCastingInfo / UnitChannelInfo (TurtleWoW / ShaguTweaks)
local _UnitCastingInfo = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo)
if _UnitCastingInfo then
local c, _, _, tex, st, et = _UnitCastingInfo("target")
if c then
cast, texture, startTime, endTime = c, tex, st, et
end
end
if not cast then
local _UnitChannelInfo = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo)
if _UnitChannelInfo then
local c, _, _, tex, st, et = _UnitChannelInfo("target")
if c then
cast, texture, startTime, endTime = c, tex, st, et
channel = true
end
end
end
-- 2) SFrames.castdb (UNIT_CASTEVENT via Tweaks.lua, has SpellInfo icon)
if SFrames.castdb and UnitGUID then
local guid = UnitGUID("target")
if guid then
local entry = SFrames.castdb[guid]
if entry and entry.cast and entry.start and entry.casttime then
local elapsed = GetTime() - entry.start
local duration = entry.casttime / 1000
if elapsed < duration + 0.5 then
if not cast then
cast = entry.cast
startTime = entry.start * 1000
endTime = (entry.start + duration) * 1000
channel = entry.channel
end
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
texture = entry.icon
end
end
end
end
end
-- 3) NanamiPlates castDB (GUID-based, UNIT_CASTEVENT with SpellInfo icon)
if NanamiPlates and NanamiPlates.castDB and UnitGUID then
local guid = UnitGUID("target")
if guid then
local entry = NanamiPlates.castDB[guid]
if entry and entry.spell and entry.startTime and entry.duration then
local elapsed = GetTime() - entry.startTime
local duration = entry.duration / 1000
if elapsed < duration + 0.5 then
if not cast then
cast = entry.spell
startTime = entry.startTime * 1000
endTime = (entry.startTime + duration) * 1000
channel = entry.channel
end
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
texture = entry.icon
end
end
end
end
end
-- 4) NanamiPlates castTracker (name-based, combat log)
if not cast and NanamiPlates and NanamiPlates.castTracker then
local targetName = UnitName("target")
if targetName and NanamiPlates.castTracker[targetName] then
local entries = NanamiPlates.castTracker[targetName]
if entries and entries[1] then
local entry = entries[1]
if entry.spell and entry.startTime then
local duration = (entry.duration or 2000) / 1000
local elapsed = GetTime() - entry.startTime
if elapsed < duration + 0.5 then
cast = entry.spell
texture = entry.icon
startTime = entry.startTime * 1000
endTime = (entry.startTime + duration) * 1000
channel = false
end
end
end
end
end
-- 5) Local UNIT_CASTEVENT / SPELLCAST_* tracker
if targetCLCast then
local elapsed = GetTime() - targetCLCast.startTime
if elapsed < targetCLCast.duration + 0.5 then
if not cast then
cast = targetCLCast.spell
startTime = targetCLCast.startTime * 1000
endTime = (targetCLCast.startTime + targetCLCast.duration) * 1000
channel = targetCLCast.channel
end
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
texture = targetCLCast.icon
end
else
targetCLCast = nil
end
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 not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
texture = GetSpellIcon(cast) or texture
end
cb:SetAlpha(1)
cb.cbbg:SetAlpha(1)
cb:Show()
cb.cbbg:Show()
if texture then
cb.icon:SetTexture(texture)
cb.icon:SetAlpha(1)
cb.ibg:SetAlpha(1)
cb.icon:Show()
cb.ibg:Show()
else
cb.icon:Hide()
cb.ibg:Hide()
end
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