Files
Nanami-UI/Focus.lua
2026-04-09 09:46:47 +08:00

1650 lines
62 KiB
Lua

--------------------------------------------------------------------------------
-- 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