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 function SetTextureIfPresent(region, texturePath) if region and region.SetTexture and texturePath then region:SetTexture(texturePath) end end local function ApplyFontIfPresent(fs, size, fontKey, fallbackFontKey) if not fs then return end SFrames:ApplyFontString(fs, size, fontKey, fallbackFontKey) end local DIST_BASE_WIDTH = 80 local DIST_BASE_HEIGHT = 24 local DIST_BASE_FONTSIZE = 14 -- Check if UnitXP SP3 distance API is available local hasUnitXP = nil -- nil = not checked yet, true/false = cached result local function IsUnitXPAvailable() if hasUnitXP ~= nil then return hasUnitXP end hasUnitXP = (type(UnitXP) == "function") and (pcall(UnitXP, "nop", "nop")) return hasUnitXP end function SFrames.Target:GetDistance(unit) if not UnitExists(unit) then return nil end if UnitIsUnit(unit, "player") then return "0 码" end -- Prefer UnitXP precise distance when available if IsUnitXPAvailable() then local ok, dist = pcall(UnitXP, "distanceBetween", "player", unit) if ok and type(dist) == "number" and dist >= 0 then return string.format("%.1f 码", dist) end end -- Fallback: CheckInteractDistance rough ranges if CheckInteractDistance(unit, 2) then return "< 8 码" elseif CheckInteractDistance(unit, 3) then return "8-10 码" elseif CheckInteractDistance(unit, 4) then return "10-28 码" 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 showPortrait = db.targetShowPortrait ~= false local gradientStyle = SFrames:IsGradientStyle() local classicDefaultPowerWidth = width - (showPortrait and portraitWidth or 0) - 2 if classicDefaultPowerWidth < 60 then classicDefaultPowerWidth = 60 end local rawPowerWidth = tonumber(db.targetPowerWidth) local legacyFullWidth = tonumber(db.targetFrameWidth) or width local defaultPowerWidth = gradientStyle and width or classicDefaultPowerWidth local maxPowerWidth = gradientStyle and width or (width - 2) local powerWidth if gradientStyle then -- 渐变风格:能量条始终与血条等宽(全宽) powerWidth = width elseif not rawPowerWidth or math.abs(rawPowerWidth - legacyFullWidth) < 0.5 or math.abs(rawPowerWidth - classicDefaultPowerWidth) < 0.5 then powerWidth = defaultPowerWidth else powerWidth = rawPowerWidth end powerWidth = Clamp(math.floor(powerWidth + 0.5), 60, maxPowerWidth) local powerOffsetX = Clamp(math.floor((tonumber(db.targetPowerOffsetX) or 0) + 0.5), -120, 120) local powerOffsetY = Clamp(math.floor((tonumber(db.targetPowerOffsetY) or 0) + 0.5), -80, 80) 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 healthFont = tonumber(db.targetHealthFontSize) or valueFont healthFont = Clamp(math.floor(healthFont + 0.5), 8, 18) local powerFont = tonumber(db.targetPowerFontSize) or valueFont powerFont = Clamp(math.floor(powerFont + 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, powerWidth = powerWidth, powerOffsetX = powerOffsetX, powerOffsetY = powerOffsetY, powerOnTop = db.targetPowerOnTop == true, nameFont = nameFont, valueFont = valueFont, healthFont = healthFont, powerFont = powerFont, healthTexture = SFrames:ResolveBarTexture("targetHealthTexture", "barTexture"), powerTexture = SFrames:ResolveBarTexture("targetPowerTexture", "barTexture"), 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) local bgA = tonumber(db.targetBgAlpha) or 0.9 local _A = SFrames.ActiveTheme if _A and _A.panelBg and bgA < 0.89 then if f.SetBackdropColor then f:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end if f.healthBGFrame and f.healthBGFrame.SetBackdropColor then f.healthBGFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end if f.powerBGFrame and f.powerBGFrame.SetBackdropColor then f.powerBGFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end if f.portraitBG and f.portraitBG.SetBackdropColor then f.portraitBG:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end end 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", cfg.powerOffsetX, -1 + cfg.powerOffsetY) f.power:SetWidth(cfg.powerWidth) 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 SFrames:ApplyStatusBarTexture(f.health, "targetHealthTexture", "barTexture") SFrames:ApplyStatusBarTexture(f.power, "targetPowerTexture", "barTexture") SetTextureIfPresent(f.health and f.health.bg, cfg.healthTexture) SetTextureIfPresent(f.health and f.health.healPredMine, cfg.healthTexture) SetTextureIfPresent(f.health and f.health.healPredOther, cfg.healthTexture) SetTextureIfPresent(f.health and f.health.healPredOver, cfg.healthTexture) SetTextureIfPresent(f.power and f.power.bg, cfg.powerTexture) -- Gradient style preset if SFrames:IsGradientStyle() then -- Hide portrait & its backdrop if f.portrait then f.portrait:Hide() end if f.portraitBG then f.portraitBG:Hide() end -- Strip backdrops SFrames:ClearBackdrop(f) SFrames:ClearBackdrop(f.healthBGFrame) SFrames:ClearBackdrop(f.powerBGFrame) -- Health bar full width if f.health then f.health:ClearAllPoints() f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0) f.health:SetHeight(cfg.healthHeight) end -- Power bar full width, below health if f.power then f.power:ClearAllPoints() f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", cfg.powerOffsetX, -2 + cfg.powerOffsetY) f.power:SetWidth(cfg.powerWidth) f.power:SetHeight(cfg.powerHeight) end -- Apply gradient overlays SFrames:ApplyGradientStyle(f.health) SFrames:ApplyGradientStyle(f.power) -- Flush BG frames (no border padding) if f.healthBGFrame then f.healthBGFrame:ClearAllPoints() f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", 0, 0) f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 0, 0) end if f.powerBGFrame then f.powerBGFrame:ClearAllPoints() f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", 0, 0) f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 0, 0) end -- Hide bar backgrounds (transparent) if f.healthBGFrame then f.healthBGFrame:Hide() end if f.powerBGFrame then f.powerBGFrame:Hide() end if f.health and f.health.bg then f.health.bg:Hide() end if f.power and f.power.bg then f.power.bg:Hide() end else -- Classic style: remove gradient overlays if they exist SFrames:RemoveGradientStyle(f.health) SFrames:RemoveGradientStyle(f.power) -- Restore bar backgrounds if f.healthBGFrame then f.healthBGFrame:Show() end if f.powerBGFrame then f.powerBGFrame:Show() end if f.health and f.health.bg then f.health.bg:Show() end if f.power and f.power.bg then f.power.bg:Show() end end ApplyFontIfPresent(f.nameText, cfg.nameFont, "targetNameFontKey") ApplyFontIfPresent(f.healthText, cfg.healthFont, "targetHealthFontKey") ApplyFontIfPresent(f.powerText, cfg.powerFont, "targetPowerFontKey") 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 f.distText then local dfs = (db.targetDistanceFontSize and tonumber(db.targetDistanceFontSize)) or 10 dfs = Clamp(dfs, 8, 24) SFrames:ApplyFontString(f.distText, dfs, "targetDistanceFontKey", "fontKey") end if f.health and f.power then local healthLevel = f:GetFrameLevel() + 2 local powerLevel = cfg.powerOnTop and (healthLevel + 1) or (healthLevel - 1) f.health:SetFrameLevel(healthLevel) f.power:SetFrameLevel(powerLevel) end SFrames:ApplyConfiguredUnitBackdrop(f, "target") if f.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.healthBGFrame, "target") end if f.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.powerBGFrame, "target") end if f.portraitBG then SFrames:ApplyConfiguredUnitBackdrop(f.portraitBG, "target", true) 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 customSize = SFramesDB and tonumber(SFramesDB.targetDistanceFontSize) local fontSize if customSize and customSize >= 8 and customSize <= 24 then fontSize = math.floor(customSize + 0.5) else fontSize = math.max(8, math.floor(DIST_BASE_FONTSIZE * scale + 0.5)) end SFrames:ApplyFontString(f.text, fontSize, "targetDistanceFontKey", "fontKey") 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() local ticker = CreateFrame("Frame", nil, UIParent) ticker:SetWidth(1) ticker:SetHeight(1) ticker.timer = 0 ticker:Show() ticker:SetScript("OnUpdate", function() local distFrame = SFrames.Target.distanceFrame if not distFrame then return end local disabled = SFramesDB and SFramesDB.targetDistanceEnabled == false local onFrame = not SFramesDB or SFramesDB.targetDistanceOnFrame ~= false local tgtFrame = SFrames.Target and SFrames.Target.frame local embeddedText = tgtFrame and tgtFrame.distText if disabled then if distFrame:IsShown() then distFrame:Hide() end if embeddedText and embeddedText:IsShown() then embeddedText:Hide() end return end if not UnitExists("target") then if distFrame:IsShown() then distFrame:Hide() end if embeddedText and embeddedText:IsShown() then embeddedText: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") local distStr = dist or "---" if onFrame and embeddedText then embeddedText:SetText(distStr) if not embeddedText:IsShown() then embeddedText:Show() end if distFrame:IsShown() then distFrame:Hide() end else distFrame.text:SetText(distStr) if not distFrame:IsShown() then distFrame:Show() end if embeddedText and embeddedText:IsShown() then embeddedText:Hide() end 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) local frameScale = (SFramesDB and type(SFramesDB.targetFrameScale) == "number") and SFramesDB.targetFrameScale or 1 f:SetScale(frameScale) if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["TargetFrame"] then local pos = SFramesDB.Positions["TargetFrame"] local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale() if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then f:SetPoint(pos.point, UIParent, pos.relativePoint, (pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale) else f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) end else f:SetPoint("CENTER", UIParent, "CENTER", 200, -100) end 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() local fSc = f:GetEffectiveScale() / UIParent:GetEffectiveScale() if fSc > 0.01 and math.abs(fSc - 1) > 0.001 then xOfs = (xOfs or 0) * fSc yOfs = (yOfs or 0) * fSc end SFramesDB.Positions["TargetFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs } end) f:RegisterForClicks("LeftButtonUp", "RightButtonUp") f:SetScript("OnClick", function() if arg1 == "LeftButton" then -- Shift+左键 = 设为焦点 if IsShiftKeyDown() then if SFrames.Focus and SFrames.Focus.SetFromTarget then pcall(SFrames.Focus.SetFromTarget, SFrames.Focus) end return end if TryDropCursorOnUnit(this.unit) then return end if SpellIsTargeting and SpellIsTargeting() then SpellTargetUnit(this.unit) end elseif arg1 == "RightButton" then if SpellIsTargeting and SpellIsTargeting() then SpellStopTargeting() return end if not UnitExists("target") then return end if not SFrames.Target.dropDown then local ok1, err1 = pcall(function() SFrames.Target.dropDown = CreateFrame("Frame", "SFramesTargetDropDown", UIParent, "UIDropDownMenuTemplate") end) if not ok1 then return end SFrames.Target.dropDown.displayMode = "MENU" SFrames.Target.dropDown.initialize = function() local dd = SFrames.Target.dropDown local name = dd.targetName if not name then return end local info = {} info.text = name info.isTitle = 1 info.notCheckable = 1 UIDropDownMenu_AddButton(info) if UnitIsPlayer("target") and UnitIsFriend("player", "target") and not UnitIsUnit("target", "player") then -- 悄悄话 info = {} info.text = "悄悄话" info.notCheckable = 1 info.func = function() ChatFrame_SendTell(name) end UIDropDownMenu_AddButton(info) -- 组队相关(动态) local inParty = UnitInParty("target") local isLeader = IsPartyLeader() if inParty and isLeader then info = {} info.text = "提升为队长" info.notCheckable = 1 info.func = function() PromoteToLeader(name) end UIDropDownMenu_AddButton(info) info = {} info.text = "取消邀请" info.notCheckable = 1 info.func = function() UninviteByName(name) end UIDropDownMenu_AddButton(info) end -- 观察 info = {} info.text = "观察" info.notCheckable = 1 info.func = function() InspectUnit("target") end UIDropDownMenu_AddButton(info) -- 交易 info = {} info.text = "交易" info.notCheckable = 1 info.func = function() InitiateTrade("target") end UIDropDownMenu_AddButton(info) -- 邀请组队(不在队伍中时显示) if not inParty then info = {} info.text = "邀请组队" info.notCheckable = 1 info.func = function() InviteByName(name) end UIDropDownMenu_AddButton(info) end -- 跟随 info = {} info.text = "跟随" info.notCheckable = 1 info.func = function() FollowUnit("target") end UIDropDownMenu_AddButton(info) -- 决斗(不在队伍中时显示,节省按钮数) if not inParty then info = {} info.text = "决斗" info.notCheckable = 1 info.func = function() StartDuel("target") end UIDropDownMenu_AddButton(info) end end if SFrames.Focus and SFrames.Focus.GetFocusName then info = {} local ok2, curFocus = pcall(SFrames.Focus.GetFocusName, SFrames.Focus) local isSameTarget = ok2 and curFocus and curFocus == name if isSameTarget then info.text = "取消焦点" info.func = function() pcall(SFrames.Focus.Clear, SFrames.Focus) end else info.text = "设为焦点" info.func = function() pcall(SFrames.Focus.SetFromTarget, SFrames.Focus) end end info.notCheckable = 1 UIDropDownMenu_AddButton(info) end -- 取消按钮不添加,点击菜单外部即可关闭(节省按钮位) end end SFrames.Target.dropDown.targetName = UnitName("target") pcall(ToggleDropDownMenu, 1, nil, SFrames.Target.dropDown, "cursor") end end) f:SetScript("OnReceiveDrag", function() if TryDropCursorOnUnit(this.unit) then return end if SpellIsTargeting and SpellIsTargeting() then SpellTargetUnit(this.unit) end end) 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, "ARTWORK") f.health.healPredMine:SetTexture(SFrames:GetTexture()) f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78) f.health.healPredMine:SetDrawLayer("ARTWORK", 2) f.health.healPredMine:Hide() f.health.healPredOther = f.health:CreateTexture(nil, "ARTWORK") f.health.healPredOther:SetTexture(SFrames:GetTexture()) f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5) f.health.healPredOther:SetDrawLayer("ARTWORK", 2) f.health.healPredOther:Hide() f.health.healPredOver = f.health:CreateTexture(nil, "OVERLAY") f.health.healPredOver:SetTexture(SFrames:GetTexture()) f.health.healPredOver:SetVertexColor(1.0, 0.3, 0.3, 0.6) f.health.healPredOver:SetDrawLayer("OVERLAY", 7) f.health.healPredOver: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("") -- Embedded distance text (high-level overlay so it's never covered) local distOverlay = CreateFrame("Frame", nil, f) distOverlay:SetFrameLevel((f:GetFrameLevel() or 0) + 20) distOverlay:SetAllPoints(f.health) local distFS = (SFramesDB and tonumber(SFramesDB.targetDistanceFontSize)) or 10 f.distText = SFrames:CreateFontString(distOverlay, distFS, "CENTER") f.distText:SetPoint("CENTER", f.health, "TOP", 0, 0) f.distText:SetTextColor(1, 0.82, 0.25) f.distText:SetShadowColor(0, 0, 0, 1) f.distText:SetShadowOffset(1, -1) f.distText:SetText("") f.distText:Hide() -- 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() if SetMouseoverUnit then SetMouseoverUnit(this.unit) end GameTooltip_SetDefaultAnchor(GameTooltip, this) GameTooltip:SetUnit(this.unit) GameTooltip:Show() end) f:SetScript("OnLeave", function() if SetMouseoverUnit then SetMouseoverUnit() end 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, nil, { alwaysShowInLayout = true }) 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() local enabled = not (SFramesDB and SFramesDB.targetDistanceEnabled == false) local onFrame = not SFramesDB or SFramesDB.targetDistanceOnFrame ~= false if SFrames.Target.distanceFrame then local dist = self:GetDistance("target") if onFrame and self.frame.distText then self.frame.distText:SetText(dist or "---") if enabled then self.frame.distText:Show() else self.frame.distText:Hide() end SFrames.Target.distanceFrame:Hide() else SFrames.Target.distanceFrame.text:SetText(dist or "---") if enabled then SFrames.Target.distanceFrame:Show() else SFrames.Target.distanceFrame:Hide() end if self.frame.distText then self.frame.distText:Hide() end end end else self.frame:Hide() if SFrames.Target.distanceFrame then SFrames.Target.distanceFrame:Hide() end if self.frame and self.frame.distText then self.frame.distText: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) -- Gradient style always uses class colors if SFrames:IsGradientStyle() then useClassColor = true end 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 -- Re-apply gradient after color change if SFrames:IsGradientStyle() then SFrames:ApplyBarGradient(self.frame.health) 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(SFrames:FormatCompactPair(displayHp, displayMax)) else self.frame.healthText:SetText(SFrames:FormatCompactNumber(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 and self.frame.health.healPredOver) then return end local predMine = self.frame.health.healPredMine local predOther = self.frame.health.healPredOther local predOver = self.frame.health.healPredOver if not UnitExists("target") then predMine:Hide(); predOther:Hide(); predOver:Hide() return end local hp = UnitHealth("target") or 0 local maxHp = UnitHealthMax("target") or 0 if CheckSuperWow then local ok, hasSW = pcall(CheckSuperWow) if ok and hasSW then local ok2, realHp = pcall(UnitHealth, "target") if ok2 then hp = realHp or hp end local ok3, realMaxHp = pcall(UnitHealthMax, "target") if ok3 then maxHp = realMaxHp or maxHp end end end if maxHp <= 0 then predMine:Hide(); predOther:Hide(); predOver:Hide() return end local _, mineIncoming, othersIncoming = GetIncomingHeals("target") if CheckSuperWow then local ok, hasSW = pcall(CheckSuperWow) if ok and hasSW then local ok2, _, realMine, realOther = pcall(GetIncomingHeals, "target") if ok2 then mineIncoming = realMine or mineIncoming othersIncoming = realOther or othersIncoming end end end local missing = maxHp - hp if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then predMine:Hide(); predOther:Hide(); predOver:Hide() 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 and (mineIncoming <= 0 and othersIncoming <= 0) then predMine:Hide(); predOther:Hide(); predOver:Hide() return end local showPortrait = SFramesDB and SFramesDB.targetShowPortrait ~= false local barWidth = self.frame:GetWidth() - (showPortrait and (self.frame.portrait:GetWidth() + 2) or 2) if barWidth <= 0 then predMine:Hide(); predOther:Hide(); predOver:Hide() return end local currentWidth = (hp / maxHp) * barWidth if currentWidth < 0 then currentWidth = 0 end if currentWidth > barWidth then currentWidth = barWidth end local availableWidth = barWidth - currentWidth if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then predMine:Hide(); predOther:Hide(); predOver:Hide() return end local mineWidth = 0 local otherWidth = 0 if missing > 0 then mineWidth = (mineShown / missing) * availableWidth otherWidth = (otherShown / missing) * availableWidth 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 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:SetHeight(self.frame.health:GetHeight()) 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:SetHeight(self.frame.health:GetHeight()) predOther:Show() else predOther:Hide() end local totalIncoming = mineIncoming + othersIncoming local overHeal = totalIncoming - missing if overHeal > 0 then local overWidth = math.floor((overHeal / maxHp) * barWidth + 0.5) if overWidth > 0 then predOver:ClearAllPoints() predOver:SetPoint("TOPLEFT", self.frame.health, "TOPRIGHT", 0, 0) predOver:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMRIGHT", 0, 0) predOver:SetWidth(overWidth) predOver:SetHeight(self.frame.health:GetHeight()) predOver:Show() else predOver:Hide() end else predOver: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 if SFrames:IsGradientStyle() then SFrames:ApplyBarGradient(self.frame.power) 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(SFrames:FormatCompactPair(power, maxPower)) else self.frame.powerText:SetText("") end SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, "target") 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, 32 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, 32 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 tracker = SFrames.AuraTracker local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime -- Buffs for i = 1, 32 do local b = self.frame.buffs[i] if b:IsShown() then local timeLeft = tracker and tracker:GetAuraTimeLeft("target", "buff", i) 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 -- Debuffs for i = 1, 32 do local b = self.frame.debuffs[i] if b:IsShown() then local timeLeft = tracker and tracker:GetAuraTimeLeft("target", "debuff", i) 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 tracker = SFrames.AuraTracker if tracker and tracker.HandleAuraSnapshot then tracker:HandleAuraSnapshot("target") end local hasSuperWoW = SFrames.superwow_active and SpellInfo local numBuffs = 0 -- Buffs for i = 1, 32 do local texture, swAuraID = 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) -- Store aura ID when SuperWoW is available b.auraID = hasSuperWoW and swAuraID or nil local state = tracker and tracker:GetAuraState("target", "buff", i) local timeLeft = state and tracker:GetAuraTimeLeft("target", "buff", i) 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 npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime for i = 1, 32 do local texture, dbCount, dbType, swDebuffAuraID = UnitDebuff("target", i) local b = self.frame.debuffs[i] b:SetID(i) if texture then b.icon:SetTexture(texture) -- Store aura ID when SuperWoW is available b.auraID = hasSuperWoW and swDebuffAuraID or nil local state = tracker and tracker:GetAuraState("target", "debuff", i) local timeLeft = state and tracker:GetAuraTimeLeft("target", "debuff", i) if timeLeft and timeLeft > 0 then b.expirationTime = GetTime() + timeLeft b.effectName = state and state.name or nil 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