1650 lines
62 KiB
Lua
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
|