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