-------------------------------------------------------------------------------- -- 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"] -- Validate: if GetTop would be near 0 or negative, position is bad f:SetPoint(pos.point or "LEFT", UIParent, pos.relativePoint or "LEFT", pos.xOfs or 250, pos.yOfs or 0) -- After setting, check if visible on screen local top = f:GetTop() local left = f:GetLeft() if not top or not left or top < 50 or left < 0 then -- Bad position, reset f:ClearAllPoints() f:SetPoint("LEFT", UIParent, "LEFT", 250, 0) SFramesDB.Positions["FocusFrame"] = nil end 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() 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 (right side) — EnableMouse(false) so clicks pass through to main Button local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false) f.portrait = CreateFrame("PlayerModel", nil, f) f.portrait:SetWidth(pWidth) f.portrait:SetHeight(totalH - 2) f.portrait:SetPoint("RIGHT", f, "RIGHT", -1, 0) f.portrait:EnableMouse(false) 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 pbg:EnableMouse(false) if not showPortrait then f.portrait:Hide() pbg:Hide() end -- Health bar f.health = SFrames:CreateStatusBar(f, "SFramesFocusHealth") f.health:EnableMouse(false) if showPortrait then f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0) else f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0) end 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) if showPortrait then f.power:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMLEFT", -1, 0) else f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1) end 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 f.classIcon = SFrames:CreateClassIcon(f, 14) f.classIcon.overlay:SetPoint("CENTER", f.portrait, "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) local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false) if showPortrait then cb:SetPoint("BOTTOMRIGHT", self.frame.portrait, "TOPRIGHT", -(cbH + 6), 6) else cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", -(cbH + 6), 6) end 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.portrait then self.frame.portrait:Hide() end 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 showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false) if showPortrait and self.frame.portrait then -- Only reset portrait model on first load / focus change (not every update) if not self.frame._lastPortraitUID or self.frame._lastPortraitUID ~= uid then self.frame.portrait:SetUnit(uid) self.frame.portrait:SetCamera(0) self.frame.portrait:Hide() self.frame.portrait:Show() self.frame.portrait:SetPosition(-1.0, 0, 0) self.frame._lastPortraitUID = uid end end 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 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 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(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 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(power .. " / " .. maxPower) else self.frame.powerText:SetText("") end 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 and function(u) return CastingInfo(u) end) local _UCH = UnitChannelInfo or (ChannelInfo and function(u) return ChannelInfo(u) end) 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.frame._lastPortraitUID = nil -- Force portrait refresh on focus change 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 showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false) local showCastBar = not (SFramesDB and SFramesDB.focusShowCastBar == false) local showAuras = not (SFramesDB and SFramesDB.focusShowAuras == false) -- 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 if f.portrait then f.portrait:SetWidth(pWidth) f.portrait:SetHeight(totalH - 2) if showPortrait then f.portrait:Show() if f.portraitBG then f.portraitBG:Show() end else f.portrait:Hide() if f.portraitBG then f.portraitBG:Hide() end end end -- Health bar anchors if f.health then f.health:ClearAllPoints() f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) if showPortrait then f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0) else f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0) end f.health:SetHeight(hHeight) end -- Power bar anchors if f.power then f.power:ClearAllPoints() f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) if showPortrait then f.power:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMLEFT", -1, 0) else f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1) 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) if showPortrait then f.castbar:SetPoint("BOTTOMRIGHT", f.portrait, "TOPRIGHT", -(cbH + 6), 6) else f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", -(cbH + 6), 6) end 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 -- Force portrait refresh f._lastPortraitUID = nil 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_PORTRAIT_UPDATE") 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.frame._lastPortraitUID = nil 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(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(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(power .. " / " .. maxPower) else focusSelf.frame.powerText:SetText("") end end elseif event == "UNIT_AURA" then focusSelf:UpdateAuras() elseif event == "UNIT_PORTRAIT_UPDATE" then local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false) if showPortrait and focusSelf.frame.portrait and evtUID then focusSelf.frame._lastPortraitUID = nil focusSelf.frame.portrait:SetUnit(evtUID) focusSelf.frame.portrait:SetCamera(0) focusSelf.frame.portrait:SetPosition(-1.0, 0, 0) focusSelf.frame._lastPortraitUID = evtUID end 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 -- Re-scan for a valid unitID every poll cycle -- This catches cases where focus becomes target/party/raid dynamically local uid = focusSelf:GetUnitID() if uid then focusSelf:UpdateHealth() focusSelf:UpdatePowerType() focusSelf:UpdatePower() focusSelf:UpdateAuras() focusSelf:UpdateRaidIcon() -- Only refresh portrait when unitID changes (prevents 3D model flicker) local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false) if showPortrait and focusSelf.frame.portrait then if uid ~= ef.lastUID then focusSelf.frame.portrait:SetUnit(uid) focusSelf.frame.portrait:SetCamera(0) focusSelf.frame.portrait:SetPosition(-1.0, 0, 0) focusSelf.frame.portrait:Show() ef.lastUID = uid end end else ef.lastUID = nil end end) -- Register mover if SFrames.Movers and SFrames.Movers.RegisterMover then if SFramesTargetFrame then SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点", "TOPLEFT", "SFramesTargetFrame", "BOTTOMLEFT", 0, -10) else SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点", "LEFT", "UIParent", "LEFT", 250, 0) 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