-------------------------------------------------------------------------------- -- Nanami-UI: Focus Frame -- Complete focus unit frame with health/power/portrait/auras/castbar/distance -- Supports: native focus > SuperWoW GUID > static (name-only) mode -------------------------------------------------------------------------------- SFrames.Focus = {} local _A = SFrames.ActiveTheme local AURA_SIZE = 20 local AURA_SPACING = 2 local AURA_ROW_SPACING = 1 -------------------------------------------------------------------------------- -- Focus combat-log cast tracker (name-based, no GUID/unitID needed) -- Populated by CHAT_MSG_SPELL_* events, consumed by CastbarOnUpdate -------------------------------------------------------------------------------- local focusCastTracker = {} -- [casterName] = { spell, startTime, duration, icon, channel } -- Pattern matcher for localized global strings (same logic as Target.lua CLMatch) local function cmatch(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 end local function Trim(text) if type(text) ~= "string" then return "" end text = string.gsub(text, "^%s+", "") text = string.gsub(text, "%s+$", "") return text end -- Drop cursor item onto a unit (same helper as Target/Party/Raid) 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 -- TurtleWoW throws "Unknown unit" for "focus" if not supported. -- Detect once at load time whether native focus is available. local NATIVE_FOCUS_OK = false do local ok, val = pcall(function() return UnitExists("focus") end) if ok then NATIVE_FOCUS_OK = true end end local function FocusUnitExists() if not NATIVE_FOCUS_OK then return false end local ok, val = pcall(UnitExists, "focus") return ok and val end -- Simple spell icon lookup (mirrors Target.lua's local GetSpellIcon) local focusSpellIconCache = {} local function FocusGetSpellIcon(spellName) if not spellName then return nil end if focusSpellIconCache[spellName] then return focusSpellIconCache[spellName] end local i = 1 while true do local name, _, tex = GetSpellName(i, BOOKTYPE_SPELL) if not name then break end if tex then focusSpellIconCache[name] = tex end i = i + 1 end return focusSpellIconCache[spellName] end -------------------------------------------------------------------------------- -- DB helpers -------------------------------------------------------------------------------- function SFrames.Focus:EnsureDB() if not SFramesDB then SFramesDB = {} end if not SFramesDB.Focus then SFramesDB.Focus = {} end return SFramesDB.Focus end function SFrames.Focus:GetFocusName() if FocusUnitExists() and UnitName then local name = UnitName("focus") if name and name ~= "" then return name end end local db = self:EnsureDB() if db.name and db.name ~= "" then return db.name end return nil end -- Determine the best unitID to query focus data function SFrames.Focus:GetUnitID() -- 1) Native focus if FocusUnitExists() then return "focus" end local db = self:EnsureDB() local focusName = db.name -- 2) SuperWoW GUID if SUPERWOW_VERSION and db.guid and db.guid ~= "" and UnitExists then local ok, exists = pcall(UnitExists, db.guid) if ok and exists then return db.guid end end if not focusName or focusName == "" then return nil end -- 3) If focus name matches current target, use "target" if UnitExists("target") then local tName = UnitName("target") if tName and tName == focusName then -- Also try to grab GUID if we didn't have one if SUPERWOW_VERSION and (not db.guid or db.guid == "") and UnitGUID then local ok, g = pcall(UnitGUID, "target") if ok and g then db.guid = g end end return "target" end end -- 4) Check party/raid members by name local n = GetNumPartyMembers and GetNumPartyMembers() or 0 for i = 1, n do local pUnit = "party" .. i if UnitExists(pUnit) and UnitName(pUnit) == focusName then return pUnit end end local r = GetNumRaidMembers and GetNumRaidMembers() or 0 for i = 1, r do local rUnit = "raid" .. i if UnitExists(rUnit) and UnitName(rUnit) == focusName then return rUnit end end -- 5) Scan indirect unitIDs: targettarget, party targets, etc. local scanUnits = { "targettarget" } for i = 1, (n > 0 and n or 0) do table.insert(scanUnits, "party" .. i .. "target") end for _, u in ipairs(scanUnits) do local ok, exists = pcall(UnitExists, u) if ok and exists then local ok2, uName = pcall(UnitName, u) if ok2 and uName == focusName then return u end end end return nil end -------------------------------------------------------------------------------- -- Set / Clear / Target / Cast (data logic) -------------------------------------------------------------------------------- -- STUB: SetFromTarget function SFrames.Focus:SetFromTarget() return self:SetFromUnit("target") end -- Set focus from any unitID (target, party1, raid3, etc.) function SFrames.Focus:SetFromUnit(unit) if not unit or not UnitExists or not UnitExists(unit) then return false, "NO_UNIT" end local name = UnitName and UnitName(unit) if not name or name == "" then return false, "INVALID_UNIT" end local db = self:EnsureDB() db.name = name db.level = UnitLevel and UnitLevel(unit) or nil local classToken if UnitClass then _, classToken = UnitClass(unit) end db.class = classToken -- Get GUID with pcall protection db.guid = nil if UnitGUID then local ok, g = pcall(UnitGUID, unit) if ok and g then db.guid = g if SFrames.guidToName then SFrames.guidToName[g] = name end end end -- If UnitGUID failed, try reverse lookup from guidToName if not db.guid and SFrames.guidToName then for guid, gname in pairs(SFrames.guidToName) do if gname == name then db.guid = guid break end end end -- Try to set native focus local usedNative = false if FocusUnit then -- FocusUnit() sets current target as focus; if unit is not target, try FocusUnit(unit) pcall(FocusUnit, unit) if not FocusUnitExists() then pcall(FocusUnit) end if FocusUnitExists() then usedNative = true end end self:OnFocusChanged() return true, name, usedNative end function SFrames.Focus:Clear() if ClearFocus then pcall(ClearFocus) end local db = self:EnsureDB() db.name = nil db.level = nil db.class = nil db.guid = nil self:OnFocusChanged() return true end function SFrames.Focus:Target() if FocusUnitExists() then TargetUnit("focus") return true, "NATIVE" end local focusName = self:GetFocusName() if not focusName then return false, "NO_FOCUS" end -- SuperWoW GUID local db = self:EnsureDB() if SUPERWOW_VERSION and db.guid and db.guid ~= "" then local ok = pcall(TargetUnit, db.guid) if ok and UnitExists("target") then return true, "GUID" end end if TargetByName then TargetByName(focusName, true) if UnitExists("target") and UnitName("target") == focusName then return true, "NAME" end end return false, "NOT_FOUND" end function SFrames.Focus:Cast(spellName) local spell = Trim(spellName) if spell == "" then return false, "NO_SPELL" end -- Best: native focus + SuperWoW CastSpellByName(spell, unit) if FocusUnitExists() and SUPERWOW_VERSION then local ok = pcall(CastSpellByName, spell, "focus") if ok then if SpellIsTargeting and SpellIsTargeting() then SpellTargetUnit("focus") end if SpellIsTargeting and SpellIsTargeting() then SpellStopTargeting(); return false, "BAD_TARGET" end return true, "SUPERWOW_NATIVE" end end -- Native focus without SuperWoW if FocusUnitExists() then CastSpellByName(spell) if SpellIsTargeting and SpellIsTargeting() then SpellTargetUnit("focus") end if SpellIsTargeting and SpellIsTargeting() then SpellStopTargeting(); return false, "BAD_TARGET" end return true, "NATIVE" end -- Fallback: GUID local db = self:EnsureDB() if SUPERWOW_VERSION and db.guid and db.guid ~= "" then local ok = pcall(CastSpellByName, spell, db.guid) if ok then if SpellIsTargeting and SpellIsTargeting() then SpellTargetUnit(db.guid) end if SpellIsTargeting and SpellIsTargeting() then SpellStopTargeting(); return false, "BAD_TARGET" end return true, "SUPERWOW_GUID" end end -- Last resort: target-switch local focusName = self:GetFocusName() if not focusName then return false, "NO_FOCUS" end local hadTarget = UnitExists and UnitExists("target") local prevName = hadTarget and UnitName and UnitName("target") or nil local onFocus = false if hadTarget and prevName == focusName then onFocus = true elseif TargetByName then TargetByName(focusName, true) if UnitExists("target") and UnitName("target") == focusName then onFocus = true end end if not onFocus then return false, "FOCUS_NOT_FOUND" end CastSpellByName(spell) if SpellIsTargeting and SpellIsTargeting() then SpellTargetUnit("target") end if SpellIsTargeting and SpellIsTargeting() then SpellStopTargeting() if hadTarget and prevName and prevName ~= focusName and TargetLastTarget then TargetLastTarget() end return false, "BAD_TARGET" end if hadTarget and prevName and prevName ~= focusName and TargetLastTarget then TargetLastTarget() end return true, "NAME" end -------------------------------------------------------------------------------- -- Frame Creation -------------------------------------------------------------------------------- -- STUB: CreateFocusFrame function SFrames.Focus:CreateFocusFrame() if SFramesDB and SFramesDB.focusEnabled == false then return end local width = tonumber(SFramesDB and SFramesDB.focusFrameWidth) or 200 local pWidth = tonumber(SFramesDB and SFramesDB.focusPortraitWidth) or 45 local hHeight = tonumber(SFramesDB and SFramesDB.focusHealthHeight) or 32 local pHeight = tonumber(SFramesDB and SFramesDB.focusPowerHeight) or 10 local totalH = hHeight + pHeight + 3 local scale = tonumber(SFramesDB and SFramesDB.focusFrameScale) or 0.9 local bgAlpha = tonumber(SFramesDB and SFramesDB.focusBgAlpha) or 0.9 local nameFontSize = tonumber(SFramesDB and SFramesDB.focusNameFontSize) or 11 local valueFontSize = tonumber(SFramesDB and SFramesDB.focusValueFontSize) or 10 local f = CreateFrame("Button", "SFramesFocusFrame", UIParent) f:SetWidth(width) f:SetHeight(totalH) f:SetScale(scale) if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["FocusFrame"] then local pos = SFramesDB.Positions["FocusFrame"] local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale() if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then f:SetPoint(pos.point or "LEFT", UIParent, pos.relativePoint or "LEFT", (pos.xOfs or 250) / fScale, (pos.yOfs or 0) / fScale) else f:SetPoint(pos.point or "LEFT", UIParent, pos.relativePoint or "LEFT", pos.xOfs or 250, pos.yOfs or 0) end local top = f:GetTop() local left = f:GetLeft() if not top or not left or top < 50 or left < 0 then f:ClearAllPoints() f:SetPoint("LEFT", UIParent, "LEFT", 250, 0) SFramesDB.Positions["FocusFrame"] = nil end elseif SFramesTargetFrame then f:SetPoint("TOPLEFT", SFramesTargetFrame, "BOTTOMLEFT", 0, -75) else f:SetPoint("LEFT", UIParent, "LEFT", 250, 0) end f:SetMovable(true) f:EnableMouse(true) f:RegisterForClicks("LeftButtonUp", "RightButtonUp") 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, _, relativePoint, xOfs, yOfs = this:GetPoint() local fScale = this:GetEffectiveScale() / UIParent:GetEffectiveScale() if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then xOfs = (xOfs or 0) * fScale yOfs = (yOfs or 0) * fScale end SFramesDB.Positions["FocusFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs } end) f:SetScript("OnClick", function() local uid = SFrames.Focus:GetUnitID() if arg1 == "LeftButton" then -- User clicked the focus frame, don't restore target on leave this._focusSwappedTarget = false -- 物品拖拽到焦点 if uid and TryDropCursorOnUnit(uid) then return end if uid then if SpellIsTargeting and SpellIsTargeting() then SpellTargetUnit(uid) else TargetUnit(uid) end else SFrames.Focus:Target() end elseif arg1 == "RightButton" then if SpellIsTargeting and SpellIsTargeting() then SpellStopTargeting() return end -- 有可查询的 unitID 时弹出右键菜单,否则清除焦点 if uid and UnitExists(uid) then if not SFrames.Focus.dropDown then SFrames.Focus.dropDown = CreateFrame("Frame", "SFramesFocusDropDown", UIParent, "UIDropDownMenuTemplate") SFrames.Focus.dropDown.displayMode = "MENU" SFrames.Focus.dropDown.initialize = function() local dd = SFrames.Focus.dropDown local unit = dd.focusUID local name = dd.focusName if not name then return end local info = {} info.text = "|cff88ccff[焦点]|r " .. name info.isTitle = 1 info.notCheckable = 1 UIDropDownMenu_AddButton(info) if unit and UnitIsPlayer(unit) and UnitIsFriend("player", unit) and not UnitIsUnit(unit, "player") then -- 悄悄话 info = {} info.text = "悄悄话" info.notCheckable = 1 info.func = function() ChatFrame_SendTell(name) end UIDropDownMenu_AddButton(info) -- 观察 info = {} info.text = "观察" info.notCheckable = 1 info.func = function() local u = SFrames.Focus:GetUnitID() if u then InspectUnit(u) end end UIDropDownMenu_AddButton(info) -- 交易 info = {} info.text = "交易" info.notCheckable = 1 info.func = function() local u = SFrames.Focus:GetUnitID() if u then InitiateTrade(u) end end UIDropDownMenu_AddButton(info) -- 邀请组队 local inParty = UnitInParty(unit) 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() local u = SFrames.Focus:GetUnitID() if u then FollowUnit(u) end end UIDropDownMenu_AddButton(info) -- 决斗 if not inParty then info = {} info.text = "决斗" info.notCheckable = 1 info.func = function() local u = SFrames.Focus:GetUnitID() if u then StartDuel(u) end end UIDropDownMenu_AddButton(info) end end -- 取消焦点(始终显示) info = {} info.text = "取消焦点" info.notCheckable = 1 info.func = function() SFrames.Focus:Clear() end UIDropDownMenu_AddButton(info) end end SFrames.Focus.dropDown.focusUID = uid SFrames.Focus.dropDown.focusName = UnitName(uid) or SFrames.Focus:GetFocusName() ToggleDropDownMenu(1, nil, SFrames.Focus.dropDown, "cursor") else SFrames.Focus:Clear() end end end) -- 物品拖拽释放到焦点框体 f:SetScript("OnReceiveDrag", function() local uid = SFrames.Focus:GetUnitID() if uid and TryDropCursorOnUnit(uid) then return end if uid and SpellIsTargeting and SpellIsTargeting() then SpellTargetUnit(uid) end end) -- Track whether we did a temporary target-switch for mouseover casting f._focusSwappedTarget = false f._focusPrevTargetName = nil f:SetScript("OnEnter", function() local uid = SFrames.Focus:GetUnitID() if uid then this.unit = uid if SetMouseoverUnit then SetMouseoverUnit(uid) end GameTooltip_SetDefaultAnchor(GameTooltip, this) GameTooltip:SetUnit(uid) GameTooltip:AddLine("|cff999999按住 Alt + 左键拖动|r", 0.6, 0.6, 0.6) GameTooltip:Show() else -- No valid unitID — try temporary target switch for mouseover casting local focusName = SFrames.Focus:GetFocusName() if focusName and TargetByName then this._focusPrevTargetName = UnitExists("target") and UnitName("target") or nil this._focusSwappedTarget = false -- Only switch if current target is not already the focus if not (UnitExists("target") and UnitName("target") == focusName) then TargetByName(focusName, true) if UnitExists("target") and UnitName("target") == focusName then this._focusSwappedTarget = true end end -- Now "target" should be our focus if UnitExists("target") and UnitName("target") == focusName then this.unit = "target" if SetMouseoverUnit then SetMouseoverUnit("target") end GameTooltip_SetDefaultAnchor(GameTooltip, this) GameTooltip:SetUnit("target") GameTooltip:AddLine("|cff999999按住 Alt + 左键拖动|r", 0.6, 0.6, 0.6) GameTooltip:Show() else this.unit = nil GameTooltip_SetDefaultAnchor(GameTooltip, this) GameTooltip:SetText("焦点: " .. focusName) GameTooltip:AddLine("|cff999999按住 Alt + 左键拖动|r", 0.6, 0.6, 0.6) GameTooltip:Show() end else GameTooltip_SetDefaultAnchor(GameTooltip, this) GameTooltip:SetText("|cff999999按住 Alt + 左键拖动|r") GameTooltip:Show() end end end) f:SetScript("OnLeave", function() if SetMouseoverUnit then SetMouseoverUnit() end GameTooltip:Hide() -- Restore previous target if we swapped if this._focusSwappedTarget then this._focusSwappedTarget = false if this._focusPrevTargetName and this._focusPrevTargetName ~= "" then TargetByName(this._focusPrevTargetName, true) else ClearTarget() end end this._focusPrevTargetName = nil this.unit = nil end) SFrames:CreateUnitBackdrop(f) -- Portrait placeholder (hidden, focus frame does not use 3D portraits) f.portrait = CreateFrame("Frame", nil, f) f.portrait:SetWidth(pWidth) f.portrait:SetHeight(totalH - 2) f.portrait:SetPoint("RIGHT", f, "RIGHT", -1, 0) f.portrait:EnableMouse(false) f.portrait:Hide() 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()) f.portraitBG = pbg pbg:EnableMouse(false) pbg:Hide() -- Health bar (full width, no portrait) f.health = SFrames:CreateStatusBar(f, "SFramesFocusHealth") f.health:EnableMouse(false) f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0) f.health:SetHeight(hHeight) 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 hbg:EnableMouse(false) 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) -- Power bar f.power = SFrames:CreateStatusBar(f, "SFramesFocusPower") f.power:EnableMouse(false) f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1) 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 powerbg:EnableMouse(false) 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 (anchored to frame top-right corner) f.classIcon = SFrames:CreateClassIcon(f, 14) f.classIcon.overlay:SetPoint("CENTER", f, "TOPRIGHT", 0, 0) f.classIcon.overlay:EnableMouse(false) -- Texts f.nameText = SFrames:CreateFontString(f.health, nameFontSize, "LEFT") f.nameText:SetPoint("LEFT", f.health, "LEFT", 4, 0) f.nameText:SetShadowColor(0, 0, 0, 1) f.nameText:SetShadowOffset(1, -1) f.healthText = SFrames:CreateFontString(f.health, valueFontSize, "RIGHT") f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -4, 0) f.healthText:SetShadowColor(0, 0, 0, 1) f.healthText:SetShadowOffset(1, -1) f.powerText = SFrames:CreateFontString(f.power, valueFontSize - 1, "RIGHT") f.powerText:SetPoint("RIGHT", f.power, "RIGHT", -4, 0) f.powerText:SetShadowColor(0, 0, 0, 1) f.powerText:SetShadowOffset(1, -1) -- Raid icon local raidIconSize = 18 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) raidIconOvr:EnableMouse(false) f.raidIcon = raidIconOvr:CreateTexture(nil, "OVERLAY") f.raidIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons") f.raidIcon:SetAllPoints(raidIconOvr) f.raidIcon:Hide() f.raidIconOverlay = raidIconOvr -- "焦点" label (small text at top-left corner) f.focusLabel = SFrames:CreateFontString(f, 9, "LEFT") f.focusLabel:SetPoint("BOTTOMLEFT", f, "TOPLEFT", 2, 2) f.focusLabel:SetText("|cff88ccff焦点|r") f.focusLabel:SetShadowColor(0, 0, 0, 1) f.focusLabel:SetShadowOffset(1, -1) self.frame = f f:Hide() self:CreateAuras() self:CreateCastbar() end -- STUB: CreateAuras function SFrames.Focus:CreateAuras() if not self.frame then return end if SFramesDB and SFramesDB.focusShowAuras == false then return end self.frame.buffs = {} self.frame.debuffs = {} for i = 1, 8 do local b = CreateFrame("Button", "SFramesFocusBuff"..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, 8, "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) b:SetScript("OnEnter", function() local uid = SFrames.Focus:GetUnitID() if uid then GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") GameTooltip:SetUnitBuff(uid, this:GetID()) end end) b:SetScript("OnLeave", function() GameTooltip:Hide() end) if i == 1 then b:SetPoint("TOPLEFT", self.frame, "BOTTOMLEFT", 0, -1) else b:SetPoint("LEFT", self.frame.buffs[i-1], "RIGHT", AURA_SPACING, 0) end b:Hide() self.frame.buffs[i] = b end for i = 1, 8 do local b = CreateFrame("Button", "SFramesFocusDebuff"..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, 8, "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) b.border = b:CreateTexture(nil, "OVERLAY") b.border:SetPoint("TOPLEFT", -1, 1) b.border:SetPoint("BOTTOMRIGHT", 1, -1) b.border:SetTexture("Interface\\Buttons\\UI-Debuff-Overlays") b.border:SetTexCoord(0.296875, 0.5703125, 0, 0.515625) b:SetScript("OnEnter", function() local uid = SFrames.Focus:GetUnitID() if uid then GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") GameTooltip:SetUnitDebuff(uid, this:GetID()) end end) b:SetScript("OnLeave", function() GameTooltip:Hide() end) if i == 1 then b:SetPoint("TOPLEFT", self.frame.buffs[1], "BOTTOMLEFT", 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 end -- STUB: CreateCastbar function SFrames.Focus:CreateCastbar() if not self.frame then return end if SFramesDB and SFramesDB.focusShowCastBar == false then return end local cbH = 12 local cb = SFrames:CreateStatusBar(self.frame, "SFramesFocusCastbar") cb:SetHeight(cbH) cb:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", 0, 6) cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", -(cbH + 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, 9, "LEFT") cb.text:SetPoint("LEFT", cb, "LEFT", 4, 0) cb.icon = cb:CreateTexture(nil, "ARTWORK") cb.icon:SetWidth(cbH + 2) cb.icon:SetHeight(cbH + 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", "SFramesFocusCastbarUpdater", UIParent) self.frame.castbarUpdater:SetScript("OnUpdate", function() SFrames.Focus:CastbarOnUpdate() end) end -------------------------------------------------------------------------------- -- Data Update -------------------------------------------------------------------------------- -- STUB: UpdateAll function SFrames.Focus:UpdateAll() local uid = self:GetUnitID() if not uid then -- Static mode: show name only local db = self:EnsureDB() if db.name then self.frame.nameText:SetText(db.name or "") if db.class and SFrames.Config.colors.class[db.class] then local c = SFrames.Config.colors.class[db.class] self.frame.nameText:SetTextColor(c.r, c.g, c.b) self.frame.health:SetStatusBarColor(c.r, c.g, c.b) else self.frame.nameText:SetTextColor(0.7, 0.7, 0.7) self.frame.health:SetStatusBarColor(0.4, 0.4, 0.4) end self.frame.health:SetMinMaxValues(0, 1) self.frame.health:SetValue(1) self.frame.healthText:SetText("") self.frame.power:SetMinMaxValues(0, 1) self.frame.power:SetValue(0) self.frame.powerText:SetText("") if self.frame.classIcon then self.frame.classIcon:Hide(); if self.frame.classIcon.overlay then self.frame.classIcon.overlay:Hide() end end self.frame.raidIcon:Hide() self:HideAuras() end return end self:UpdateHealth() self:UpdatePowerType() self:UpdatePower() self:UpdateRaidIcon() self:UpdateAuras() local name = UnitName(uid) or "" local level = UnitLevel(uid) local levelText = level 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 local diff = targetLevel - playerLevel if diff >= 5 then return 1, 0.1, 0.1 elseif diff >= 3 then return 1, 0.5, 0.25 elseif diff >= -2 then return 1, 1, 0 elseif -diff <= (GetQuestGreenRange and GetQuestGreenRange() or 5) then return 0.25, 0.75, 0.25 else return 0.5, 0.5, 0.5 end end local levelColor = RGBToHex(1, 1, 1) if level == -1 then levelText = "??" levelColor = RGBToHex(1, 0, 0) elseif level then local r, g, b = GetLevelDiffColor(level) levelColor = RGBToHex(r, g, b) end local formattedLevel = "" if level and not (SFramesDB and SFramesDB.showLevel == false) then formattedLevel = levelColor .. tostring(levelText) .. "|r " end -- Class icon if UnitIsPlayer(uid) then local _, tClass = UnitClass(uid) if tClass and SFrames.SetClassIcon then SFrames:SetClassIcon(self.frame.classIcon, tClass) end else if self.frame.classIcon then self.frame.classIcon:Hide(); if self.frame.classIcon.overlay then self.frame.classIcon.overlay:Hide() end end end -- Color by class or reaction local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false) if SFrames:IsGradientStyle() then useClassColor = true end if UnitIsPlayer(uid) and useClassColor then local _, class = UnitClass(uid) if class and SFrames.Config.colors.class[class] then local color = SFrames.Config.colors.class[class] self.frame.health:SetStatusBarColor(color.r, color.g, color.b) 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 if UnitIsEnemy("player", uid) then r, g, b = 0.78, 0.25, 0.25 elseif UnitIsFriend("player", uid) then r, g, b = 0.33, 0.59, 0.33 end self.frame.health:SetStatusBarColor(r, g, b) self.frame.nameText:SetText(formattedLevel .. name) self.frame.nameText:SetTextColor(r, g, b) end if SFrames:IsGradientStyle() then SFrames:ApplyBarGradient(self.frame.health) end end function SFrames.Focus:HideAuras() if not self.frame then return end if self.frame.buffs then for i = 1, 8 do if self.frame.buffs[i] then self.frame.buffs[i]:Hide() end end end if self.frame.debuffs then for i = 1, 8 do if self.frame.debuffs[i] then self.frame.debuffs[i]:Hide() end end end end -- STUB: UpdateHealth function SFrames.Focus:UpdateHealth() local uid = self:GetUnitID() if not uid or not self.frame then return end local hp = UnitHealth(uid) local maxHp = UnitHealthMax(uid) self.frame.health:SetMinMaxValues(0, maxHp) self.frame.health:SetValue(hp) if maxHp > 0 then local pct = math.floor(hp / maxHp * 100) self.frame.healthText:SetText(SFrames:FormatCompactPair(hp, maxHp) .. " (" .. pct .. "%)") else self.frame.healthText:SetText("") end end -- STUB: UpdatePowerType function SFrames.Focus:UpdatePowerType() local uid = self:GetUnitID() if not uid or not self.frame then return end local powerType = UnitPowerType(uid) 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 -- STUB: UpdatePower function SFrames.Focus:UpdatePower() local uid = self:GetUnitID() if not uid or not self.frame then return end local power = UnitMana(uid) local maxPower = UnitManaMax(uid) 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, uid) end -- STUB: UpdateRaidIcon function SFrames.Focus:UpdateRaidIcon() local uid = self:GetUnitID() if not uid or not self.frame then self.frame.raidIcon:Hide(); return end local index = GetRaidTargetIndex(uid) if index 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 -- STUB: UpdateAuras function SFrames.Focus:UpdateAuras() local uid = self:GetUnitID() if not uid or not self.frame or not self.frame.buffs then self:HideAuras(); return end for i = 1, 8 do local texture = UnitBuff(uid, i) local b = self.frame.buffs[i] if b then b:SetID(i) if texture then b.icon:SetTexture(texture) b:Show() else b:Hide() end b.cdText:SetText("") end end for i = 1, 8 do local texture, count, dtype = UnitDebuff(uid, i) local b = self.frame.debuffs[i] if b then b:SetID(i) if texture then b.icon:SetTexture(texture) if b.border then if dtype == "Magic" then b.border:SetVertexColor(0.2, 0.6, 1) elseif dtype == "Curse" then b.border:SetVertexColor(0.6, 0, 1) elseif dtype == "Disease" then b.border:SetVertexColor(0.6, 0.4, 0) elseif dtype == "Poison" then b.border:SetVertexColor(0, 0.6, 0) else b.border:SetVertexColor(0.8, 0, 0) end end if count and count > 1 then b.cdText:SetText(count) else b.cdText:SetText("") end b:Show() else b:Hide() end end end end -- STUB: CastbarOnUpdate function SFrames.Focus:CastbarOnUpdate() if not self.frame or not self.frame.castbar then return end local cb = self.frame.castbar local uid = self:GetUnitID() local focusName = self:GetFocusName() if not focusName then if cb:IsShown() then cb:Hide(); cb.cbbg:Hide(); cb.icon:Hide(); cb.ibg:Hide() end return end local spell, texture, startTime, endTime, channel -- 1) Native UnitCastingInfo / UnitChannelInfo (if available) if uid then local _UCI = UnitCastingInfo or CastingInfo local _UCH = UnitChannelInfo or ChannelInfo if _UCI then local ok, cSpell, _, _, cIcon, cStart, cEnd = pcall(_UCI, uid) if ok and cSpell and cStart then spell, texture = cSpell, cIcon startTime, endTime, channel = cStart / 1000, cEnd / 1000, false end end if not spell and _UCH then local ok, cSpell, _, _, cIcon, cStart, cEnd = pcall(_UCH, uid) if ok and cSpell and cStart then spell, texture = cSpell, cIcon startTime, endTime, channel = cStart / 1000, cEnd / 1000, true end end end -- 2) SFrames.castdb via stored GUID (SuperWoW UNIT_CASTEVENT) if not spell and SFrames.castdb then local db = self:EnsureDB() local guid = db.guid if guid and SFrames.castdb[guid] then local data = SFrames.castdb[guid] if data.cast and data.start and data.casttime then spell = data.cast texture = data.icon startTime = data.start endTime = data.start + data.casttime / 1000 channel = data.channel end end end -- 3) SFrames.castByName: name-based UNIT_CASTEVENT data (works out of combat!) if not spell and SFrames.castByName and focusName and SFrames.castByName[focusName] then local data = SFrames.castByName[focusName] if data.cast and data.start and data.casttime then local dur = data.casttime / 1000 local elapsed = GetTime() - data.start if elapsed <= dur + 0.5 then spell = data.cast texture = data.icon startTime = data.start endTime = data.start + dur channel = data.channel else SFrames.castByName[focusName] = nil end end end -- 4) focusCastTracker: combat-log based tracker (in-combat only) if not spell and focusCastTracker[focusName] then local entry = focusCastTracker[focusName] local now = GetTime() local elapsed = now - entry.startTime if elapsed <= entry.duration + 0.5 then spell = entry.spell texture = entry.icon startTime = entry.startTime endTime = entry.startTime + entry.duration channel = entry.channel or false else focusCastTracker[focusName] = nil end end -- 5) self.clCast fallback (UNIT_CASTEVENT direct capture via GUID) if not spell and self.clCast then local now = GetTime() local elapsed = now - self.clCast.startTime if elapsed <= self.clCast.duration + 0.5 then spell = self.clCast.spell texture = self.clCast.icon startTime = self.clCast.startTime endTime = self.clCast.startTime + self.clCast.duration channel = self.clCast.channel else self.clCast = nil end end if not spell or not startTime or not endTime then if cb:IsShown() then cb:Hide(); cb.cbbg:Hide(); cb.icon:Hide(); cb.ibg:Hide() end return end local now = GetTime() local duration = endTime - startTime if duration <= 0 then cb:Hide(); cb.cbbg:Hide(); cb.icon:Hide(); cb.ibg:Hide() return end local elapsed = now - startTime if elapsed > duration + 0.5 then cb:Hide(); cb.cbbg:Hide(); cb.icon:Hide(); cb.ibg:Hide() return end cb:SetMinMaxValues(0, duration) if channel then cb:SetValue(duration - elapsed) cb:SetStatusBarColor(0.3, 0.7, 1) else cb:SetValue(elapsed) cb:SetStatusBarColor(1, 0.7, 0) end cb.text:SetText(spell or "") if texture then cb.icon:SetTexture(texture) cb.icon:Show() cb.ibg:Show() else cb.icon:Hide() cb.ibg:Hide() end cb:Show() cb.cbbg:Show() end -- STUB: OnFocusChanged function SFrames.Focus:OnFocusChanged() if not self.frame then if DEFAULT_CHAT_FRAME then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Focus] frame is nil!|r") end return end local name = self:GetFocusName() if name then self.frame:Show() self:UpdateAll() else self.frame:Hide() if self.frame.castbar then self.frame.castbar:Hide() self.frame.castbar.cbbg:Hide() self.frame.castbar.icon:Hide() self.frame.castbar.ibg:Hide() end end end -------------------------------------------------------------------------------- -- Live-apply settings (no reload needed) -------------------------------------------------------------------------------- function SFrames.Focus:ApplySettings() local f = self.frame if not f then return end local width = tonumber(SFramesDB and SFramesDB.focusFrameWidth) or 200 local pWidth = tonumber(SFramesDB and SFramesDB.focusPortraitWidth) or 45 local hHeight = tonumber(SFramesDB and SFramesDB.focusHealthHeight) or 32 local pHeight = tonumber(SFramesDB and SFramesDB.focusPowerHeight) or 10 local totalH = hHeight + pHeight + 3 local scale = tonumber(SFramesDB and SFramesDB.focusFrameScale) or 0.9 local bgAlpha = tonumber(SFramesDB and SFramesDB.focusBgAlpha) or 0.9 local nameFontSize = tonumber(SFramesDB and SFramesDB.focusNameFontSize) or 11 local valueFontSize = tonumber(SFramesDB and SFramesDB.focusValueFontSize) or 10 local showCastBar = not (SFramesDB and SFramesDB.focusShowCastBar == false) local showAuras = not (SFramesDB and SFramesDB.focusShowAuras == false) local powerOnTop = SFramesDB and SFramesDB.focusPowerOnTop == true local gradientStyle = SFrames:IsGradientStyle() local defaultPowerWidth = width - 2 if defaultPowerWidth < 60 then defaultPowerWidth = 60 end local rawPowerWidth = tonumber(SFramesDB and SFramesDB.focusPowerWidth) local legacyFullWidth = tonumber(SFramesDB and SFramesDB.focusFrameWidth) or width 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 then powerWidth = defaultPowerWidth else powerWidth = rawPowerWidth end powerWidth = math.floor(powerWidth + 0.5) if powerWidth < 60 then powerWidth = 60 end if powerWidth > maxPowerWidth then powerWidth = maxPowerWidth end -- Main frame size & scale f:SetWidth(width) f:SetHeight(totalH) f:SetScale(scale) -- Background alpha if f.SetBackdropColor then local r, g, b = 0, 0, 0 if f.GetBackdropColor then r, g, b = f:GetBackdropColor() end f:SetBackdropColor(r, g, b, bgAlpha) end -- Portrait always hidden (focus frame uses class icon only) if f.portrait then f.portrait:Hide() end if f.portraitBG then f.portraitBG:Hide() end -- Health bar anchors (always full width, no portrait) if f.health then f.health:ClearAllPoints() f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0) f.health:SetHeight(hHeight) end -- Power bar anchors if f.power then f.power:ClearAllPoints() f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", tonumber(SFramesDB and SFramesDB.focusPowerOffsetX) or 0, -1 + (tonumber(SFramesDB and SFramesDB.focusPowerOffsetY) or 0)) f.power:SetWidth(powerWidth) f.power:SetHeight(pHeight) end if f.health and f.power then local healthLevel = f:GetFrameLevel() + 2 local powerLevel = powerOnTop and (healthLevel + 1) or (healthLevel - 1) f.health:SetFrameLevel(healthLevel) f.power:SetFrameLevel(powerLevel) end if SFrames:IsGradientStyle() then SFrames:ClearBackdrop(f) SFrames:ClearBackdrop(f.healthBGFrame) SFrames:ClearBackdrop(f.powerBGFrame) 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(hHeight) end if f.power then f.power:ClearAllPoints() f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", tonumber(SFramesDB and SFramesDB.focusPowerOffsetX) or 0, -2 + (tonumber(SFramesDB and SFramesDB.focusPowerOffsetY) or 0)) f.power:SetWidth(powerWidth) f.power:SetHeight(pHeight) end SFrames:ApplyGradientStyle(f.health) SFrames:ApplyGradientStyle(f.power) 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) f.healthBGFrame:Hide() 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) 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 SFrames:ApplyConfiguredUnitBackdrop(f, "focus") if f.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.healthBGFrame, "focus") end if f.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.powerBGFrame, "focus") end SFrames:RemoveGradientStyle(f.health) SFrames:RemoveGradientStyle(f.power) 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 -- Castbar anchors if f.castbar then f.castbar:ClearAllPoints() local cbH = 12 f.castbar:SetHeight(cbH) f.castbar:SetPoint("BOTTOMLEFT", f, "TOPLEFT", 0, 6) f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", -(cbH + 6), 6) if not showCastBar then f.castbar:Hide() if f.castbar.cbbg then f.castbar.cbbg:Hide() end if f.castbar.icon then f.castbar.icon:Hide() end if f.castbar.ibg then f.castbar.ibg:Hide() end end end -- Font sizes if f.nameText then local font, _, flags = f.nameText:GetFont() if font then f.nameText:SetFont(font, nameFontSize, flags) end end if f.healthText then local font, _, flags = f.healthText:GetFont() if font then f.healthText:SetFont(font, valueFontSize, flags) end end if f.powerText then local font, _, flags = f.powerText:GetFont() if font then f.powerText:SetFont(font, valueFontSize - 1, flags) end end -- Auras visibility if not showAuras then self:HideAuras() elseif self:GetFocusName() then self:UpdateAuras() end if self:GetFocusName() then self:UpdateAll() end end -------------------------------------------------------------------------------- -- Initialize -------------------------------------------------------------------------------- function SFrames.Focus:Initialize() self:EnsureDB() local ok, err = pcall(function() SFrames.Focus:CreateFocusFrame() end) if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: FocusFrame create failed: " .. tostring(err) .. "|r") end if not self.frame then return end local focusSelf = self -- Event frame for focus-specific events local ef = CreateFrame("Frame", "SFramesFocusEvents", UIParent) -- Native focus events (TurtleWoW may not support these) pcall(ef.RegisterEvent, ef, "PLAYER_FOCUS_CHANGED") pcall(ef.RegisterEvent, ef, "UNIT_CASTEVENT") ef:RegisterEvent("PLAYER_TARGET_CHANGED") ef:RegisterEvent("UNIT_HEALTH") ef:RegisterEvent("UNIT_MANA") ef:RegisterEvent("UNIT_ENERGY") ef:RegisterEvent("UNIT_RAGE") ef:RegisterEvent("UNIT_MAXHEALTH") ef:RegisterEvent("UNIT_MAXMANA") ef:RegisterEvent("UNIT_MAXENERGY") ef:RegisterEvent("UNIT_MAXRAGE") ef:RegisterEvent("UNIT_DISPLAYPOWER") ef:RegisterEvent("UNIT_AURA") ef:RegisterEvent("UNIT_TARGET") ef:RegisterEvent("RAID_TARGET_UPDATE") -- Combat log events for castbar detection (non-SuperWoW fallback) 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", "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE", } for _, evt in ipairs(CL_EVENTS) do pcall(ef.RegisterEvent, ef, evt) end ef:SetScript("OnEvent", function() if event == "PLAYER_FOCUS_CHANGED" then focusSelf:OnFocusChanged() return end if event == "RAID_TARGET_UPDATE" then if focusSelf:GetFocusName() then focusSelf:UpdateRaidIcon() end return end -- When target changes, if new target is our focus, do a full refresh if event == "PLAYER_TARGET_CHANGED" then local focusName = focusSelf:GetFocusName() if focusName and UnitExists("target") and UnitName("target") == focusName then focusSelf:UpdateAll() -- Try to grab GUID while we have target if UnitGUID then local db = focusSelf:EnsureDB() local ok, g = pcall(UnitGUID, "target") if ok and g then db.guid = g if SFrames.guidToName then SFrames.guidToName[g] = focusName end end end end return end -- When any unit changes target, check if the new target is our focus if event == "UNIT_TARGET" then local focusName = focusSelf:GetFocusName() if focusName and arg1 then local tgtUnit = arg1 .. "target" local ok, exists = pcall(UnitExists, tgtUnit) if ok and exists then local ok2, tName = pcall(UnitName, tgtUnit) if ok2 and tName == focusName then focusSelf:UpdateAll() end end end return end -- UNIT_CASTEVENT (SuperWoW): only use if we have a stored GUID if event == "UNIT_CASTEVENT" then local db = focusSelf:EnsureDB() if db.guid and db.guid ~= "" and arg1 == db.guid then if arg3 == "START" or arg3 == "CAST" or arg3 == "CHANNEL" then -- castdb is already updated by Tweaks.lua, nothing extra needed -- But also write clCast as backup local spellName, icon if SpellInfo and arg4 then local ok, s, _, ic = pcall(SpellInfo, arg4) if ok then spellName = s; icon = ic end end spellName = spellName or "Casting" icon = icon or FocusGetSpellIcon(spellName) or "Interface\\Icons\\INV_Misc_QuestionMark" focusSelf.clCast = { spell = spellName, startTime = GetTime(), duration = (arg5 or 2000) / 1000, icon = icon, channel = (arg3 == "CHANNEL"), } elseif arg3 == "FAIL" then focusSelf.clCast = nil end end return end -- Combat log events: fill focusCastTracker by name if arg1 and string.find(event, "CHAT_MSG_SPELL") then local msg = arg1 -- Cast start detection (localized) local caster, spell local castStart = SPELLCASTOTHERSTART or "%s begins to cast %s." caster, spell = cmatch(msg, castStart) if not caster then local perfStart = SPELLPERFORMOTHERSTART or "%s begins to perform %s." caster, spell = cmatch(msg, perfStart) end if caster and spell then local icon = FocusGetSpellIcon(spell) or "Interface\\Icons\\INV_Misc_QuestionMark" focusCastTracker[caster] = { spell = spell, startTime = GetTime(), duration = 2.0, icon = icon, channel = false, } return end -- Cast interrupt / fail detection (localized) local interrupted = false -- English fallback patterns for u in string.gfind(msg, "(.+)'s .+ is interrupted%.") do if focusCastTracker[u] then focusCastTracker[u] = nil; interrupted = true end end if not interrupted then for u in string.gfind(msg, "(.+)'s .+ fails%.") do if focusCastTracker[u] then focusCastTracker[u] = nil; interrupted = true end end end -- Localized patterns if not interrupted and SPELLINTERRUPTOTHEROTHER then local a = cmatch(msg, SPELLINTERRUPTOTHEROTHER) if a and focusCastTracker[a] then focusCastTracker[a] = nil end end if not interrupted and SPELLFAILCASTOTHER then local a = cmatch(msg, SPELLFAILCASTOTHER) if a and focusCastTracker[a] then focusCastTracker[a] = nil end end if not interrupted and SPELLFAILPERFORMOTHER then local a = cmatch(msg, SPELLFAILPERFORMOTHER) if a and focusCastTracker[a] then focusCastTracker[a] = nil end end return end -- Unit events: check if it's our focus by name matching local focusName2 = focusSelf:GetFocusName() if not focusName2 then return end local isOurFocus = false if arg1 == "focus" and FocusUnitExists() then isOurFocus = true elseif arg1 and focusName2 then local ok, eName = pcall(UnitName, arg1) if ok and eName and eName == focusName2 then isOurFocus = true end end if isOurFocus then local evtUID = arg1 if event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH" then if evtUID and focusSelf.frame then local hp = UnitHealth(evtUID) local maxHp = UnitHealthMax(evtUID) focusSelf.frame.health:SetMinMaxValues(0, maxHp) focusSelf.frame.health:SetValue(hp) if maxHp > 0 then local pct = math.floor(hp / maxHp * 100) focusSelf.frame.healthText:SetText(SFrames:FormatCompactPair(hp, maxHp) .. " (" .. pct .. "%)") else focusSelf.frame.healthText:SetText("") end end elseif event == "UNIT_MANA" or event == "UNIT_MAXMANA" or event == "UNIT_ENERGY" or event == "UNIT_MAXENERGY" or event == "UNIT_RAGE" or event == "UNIT_MAXRAGE" then if evtUID and focusSelf.frame then local power = UnitMana(evtUID) local maxPower = UnitManaMax(evtUID) focusSelf.frame.power:SetMinMaxValues(0, maxPower) focusSelf.frame.power:SetValue(power) if maxPower > 0 then focusSelf.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower)) else focusSelf.frame.powerText:SetText("") end end elseif event == "UNIT_DISPLAYPOWER" then if evtUID and focusSelf.frame then local powerType = UnitPowerType(evtUID) local color = SFrames.Config.colors.power[powerType] if color then focusSelf.frame.power:SetStatusBarColor(color.r, color.g, color.b) else focusSelf.frame.power:SetStatusBarColor(0, 0, 1) end local power = UnitMana(evtUID) local maxPower = UnitManaMax(evtUID) focusSelf.frame.power:SetMinMaxValues(0, maxPower) focusSelf.frame.power:SetValue(power) if maxPower > 0 then focusSelf.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower)) else focusSelf.frame.powerText:SetText("") end end elseif event == "UNIT_AURA" then focusSelf:UpdateAuras() end end end) -- Polling for non-native focus (SuperWoW GUID or static) -- Also handles periodic refresh for native focus ef.pollTimer = 0 ef.lastUID = nil -- Track last unitID to avoid portrait flicker ef:SetScript("OnUpdate", function() ef.pollTimer = ef.pollTimer + (arg1 or 0) if ef.pollTimer < 0.25 then return end ef.pollTimer = 0 local name = focusSelf:GetFocusName() if not name then if focusSelf.frame:IsShown() then focusSelf.frame:Hide() end ef.lastUID = nil return end if not focusSelf.frame:IsShown() then focusSelf.frame:Show() end local uid = focusSelf:GetUnitID() if uid then focusSelf:UpdateHealth() focusSelf:UpdatePowerType() focusSelf:UpdatePower() focusSelf:UpdateAuras() focusSelf:UpdateRaidIcon() end end) -- Register mover (Y aligned with pet frame, X aligned with target frame) if SFrames.Movers and SFrames.Movers.RegisterMover then if SFramesTargetFrame then SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点", "TOPLEFT", "SFramesTargetFrame", "BOTTOMLEFT", 0, -75, nil, { alwaysShowInLayout = true }) else SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点", "LEFT", "UIParent", "LEFT", 250, 0, nil, { alwaysShowInLayout = true }) end end -- If focus already set (e.g. after /reload), show it self:OnFocusChanged() -- Hook WorldFrame for Shift+LeftClick to set focus from 3D world local origWorldFrameOnMouseDown = WorldFrame:GetScript("OnMouseDown") WorldFrame:SetScript("OnMouseDown", function() if arg1 == "LeftButton" and IsShiftKeyDown() then if UnitExists("mouseover") then pcall(SFrames.Focus.SetFromUnit, SFrames.Focus, "mouseover") return end end if origWorldFrameOnMouseDown then origWorldFrameOnMouseDown() end end) end