forked from Dusk/ShaguTweaks-ClassicAPI
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
372 lines
12 KiB
Lua
372 lines
12 KiB
Lua
local _G = ShaguTweaks.GetGlobalEnv()
|
|
local T = ShaguTweaks.T
|
|
local API = ShaguTweaks.API
|
|
|
|
-- These legacy wrappers are kept only as a reader for SuperWoW's GUID-keyed
|
|
-- libcast cache. Real unit-token cast discovery is ClassicAPI-only below.
|
|
local SuperWoWCastingInfo = ShaguTweaks.UnitCastingInfo
|
|
local SuperWoWChannelInfo = ShaguTweaks.UnitChannelInfo
|
|
|
|
local module = ShaguTweaks:register({
|
|
title = T["Enemy Castbars"],
|
|
description = T["Shows an enemy castbar on target unit frame. Hold <Ctrl>, <Shift> and <Alt> to move it; right-click to reset."],
|
|
expansions = { ["vanilla"] = true },
|
|
category = T["Unit Frames"],
|
|
enabled = true,
|
|
})
|
|
|
|
local castbar = CreateFrame("StatusBar", "ShaguTargetCastbar", TargetFrame)
|
|
castbar:SetPoint("BOTTOM", TargetFrame, "BOTTOM", -12, -4)
|
|
castbar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
|
|
castbar:SetStatusBarColor(1, .8, 0, 1)
|
|
castbar:SetWidth(140)
|
|
castbar:SetHeight(10)
|
|
castbar:Hide()
|
|
|
|
castbar.texture = CreateFrame("Frame", nil, castbar)
|
|
castbar.texture:SetPoint("RIGHT", castbar, "LEFT", -2, 0)
|
|
castbar.texture:SetHeight(20)
|
|
castbar.texture:SetWidth(20)
|
|
|
|
castbar.texture.icon = castbar.texture:CreateTexture(nil, "BACKGROUND")
|
|
castbar.texture.icon:SetPoint("CENTER", 0, 0)
|
|
castbar.texture.icon:SetWidth(16)
|
|
castbar.texture.icon:SetHeight(16)
|
|
castbar.texture:SetBackdrop({
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = true, tileSize = 8, edgeSize = 12,
|
|
insets = { left = 3, right = 3, top = 3, bottom = 3 }
|
|
})
|
|
|
|
castbar.bg = castbar:CreateTexture(nil, "BACKGROUND")
|
|
castbar.bg:SetTexture("Interface\\TARGETINGFRAME\\UI-StatusBar")
|
|
castbar.bg:SetVertexColor(.1, .1, 0, .8)
|
|
castbar.bg:SetAllPoints(true)
|
|
|
|
castbar.spark = castbar:CreateTexture(nil, "OVERLAY")
|
|
castbar.spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark")
|
|
castbar.spark:SetWidth(20)
|
|
castbar.spark:SetHeight(20)
|
|
castbar.spark:SetBlendMode("ADD")
|
|
|
|
castbar.backdrop = CreateFrame("Frame", nil, castbar)
|
|
castbar.backdrop:SetFrameStrata("BACKGROUND")
|
|
castbar.backdrop:SetPoint("TOPLEFT", castbar, "TOPLEFT", -3, 3)
|
|
castbar.backdrop:SetPoint("BOTTOMRIGHT", castbar, "BOTTOMRIGHT", 3, -3)
|
|
castbar.backdrop:SetBackdrop({
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = true, tileSize = 8, edgeSize = 12,
|
|
insets = { left = 3, right = 3, top = 3, bottom = 3 }
|
|
})
|
|
|
|
castbar.text = castbar:CreateFontString(nil, "HIGH", "GameFontWhite")
|
|
castbar.text:SetPoint("CENTER", castbar, "CENTER", 0, 0)
|
|
local font, size = castbar.text:GetFont()
|
|
castbar.text:SetFont(font, size - 2, "THINOUTLINE")
|
|
|
|
local function QuerySuperWoW(guid, channelSpellID, hasChannel)
|
|
if not guid then return end
|
|
|
|
local cached = ShaguTweaks.libcast.db[guid]
|
|
if not cached then return end
|
|
|
|
if cached.channel then
|
|
if not hasChannel or cached.spellID ~= channelSpellID then return end
|
|
|
|
local channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill = SuperWoWChannelInfo(guid)
|
|
if channel then
|
|
return channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill, true
|
|
end
|
|
return
|
|
end
|
|
|
|
local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = SuperWoWCastingInfo(guid)
|
|
if cast then
|
|
return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill, false
|
|
end
|
|
end
|
|
|
|
local function QueryCast(unit)
|
|
if not unit then return end
|
|
|
|
-- ClassicAPI is authoritative for real unit tokens and provides exact
|
|
-- engine/server timing. A nil result for an existing unit means that exact
|
|
-- GUID is not casting; never fall back to the legacy name-keyed database.
|
|
local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = API.GetCastInfo(unit)
|
|
if cast then
|
|
return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill, false
|
|
end
|
|
|
|
local channel, notInterruptible, channelSpellID, hasChannel
|
|
channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill,
|
|
notInterruptible, channelSpellID, hasChannel = API.GetChannelInfo(unit)
|
|
if channel then
|
|
return channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill, true
|
|
end
|
|
|
|
-- SuperWoW remains an optional GUID-keyed complement for remote casts.
|
|
-- Channels additionally require ClassicAPI's authoritative live state and
|
|
-- matching spellID, preventing a stale cached channel from becoming a ghost bar.
|
|
if ShaguTweaks.superwow_active and not UnitIsUnit(unit, "player") then
|
|
local guid = API.UnitGUID and API.UnitGUID(unit)
|
|
if guid then
|
|
local a, b, c, d, e, f, g, h = QuerySuperWoW(guid, channelSpellID, hasChannel)
|
|
if a then return a, b, c, d, e, f, g, h end
|
|
end
|
|
end
|
|
end
|
|
|
|
local POSITION_KEY = "TargetCastBarPosition"
|
|
|
|
-- Offset of the bar from the bottom of the target frame: the saved spot, or
|
|
-- the default that moves below the target of target and the buff rows.
|
|
local function GetOffsets()
|
|
local pos = ShaguTweaks_config and ShaguTweaks_config[POSITION_KEY]
|
|
if pos and pos[1] and pos[2] then return pos[1], pos[2] end
|
|
|
|
local targetOfTarget = TargetofTargetFrame and TargetofTargetFrame:IsShown()
|
|
local debuff11 = TargetFrameDebuff11 and TargetFrameDebuff11:IsShown()
|
|
local debuff7 = TargetFrameDebuff7 and TargetFrameDebuff7:IsShown()
|
|
local buff1 = TargetFrameBuff1 and TargetFrameBuff1:IsShown()
|
|
|
|
local y = -4
|
|
if targetOfTarget then
|
|
y = -24
|
|
if debuff11 then
|
|
y = -45
|
|
if buff1 then
|
|
y = -65
|
|
end
|
|
end
|
|
elseif debuff7 then
|
|
y = -24
|
|
end
|
|
|
|
return -12, y
|
|
end
|
|
|
|
local function UpdatePosition()
|
|
local x, y = GetOffsets()
|
|
|
|
-- Buff/debuff rows usually remain unchanged for the whole cast. Keep checking
|
|
-- their cheap visibility state while animating, but only touch the frame
|
|
-- anchors when the required position actually changes.
|
|
if castbar.positionY == y and castbar.positionX == x then return end
|
|
castbar.positionX = x
|
|
castbar.positionY = y
|
|
castbar:ClearAllPoints()
|
|
castbar:SetPoint("BOTTOM", TargetFrame, "BOTTOM", x, y)
|
|
end
|
|
|
|
local function HideCast()
|
|
castbar.startTime = nil
|
|
castbar.endTime = nil
|
|
castbar.isChannel = nil
|
|
castbar:Hide()
|
|
end
|
|
|
|
local function UpdateProgress()
|
|
local startTime = castbar.startTime
|
|
local endTime = castbar.endTime
|
|
if not startTime or not endTime or endTime <= startTime then
|
|
HideCast()
|
|
return
|
|
end
|
|
|
|
local now = GetTime()
|
|
local max = endTime - startTime
|
|
if now >= endTime then
|
|
HideCast()
|
|
return
|
|
end
|
|
|
|
local cur
|
|
if castbar.isChannel then
|
|
cur = endTime - now
|
|
else
|
|
cur = now - startTime
|
|
end
|
|
|
|
cur = cur > max and max or cur
|
|
cur = cur < 0 and 0 or cur
|
|
|
|
castbar:SetValue(cur)
|
|
|
|
local percent = max > 0 and cur / max or 0
|
|
local x = castbar:GetWidth() * percent
|
|
castbar.spark:ClearAllPoints()
|
|
castbar.spark:SetPoint("CENTER", castbar, "LEFT", x, 0)
|
|
|
|
-- The target frame can move its buff/debuff rows while a cast is active.
|
|
UpdatePosition()
|
|
end
|
|
|
|
local function RefreshCast()
|
|
if not UnitExists("target") then
|
|
HideCast()
|
|
return
|
|
end
|
|
|
|
local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill, isChannel = QueryCast("target")
|
|
if not cast or not startTime or not endTime or endTime <= startTime then
|
|
HideCast()
|
|
return
|
|
end
|
|
|
|
castbar.startTime = startTime / 1000
|
|
castbar.endTime = endTime / 1000
|
|
castbar.isChannel = isChannel
|
|
|
|
local max = castbar.endTime - castbar.startTime
|
|
castbar:SetMinMaxValues(0, max)
|
|
castbar.text:SetText(cast)
|
|
|
|
if texture then
|
|
castbar.texture.icon:SetTexture(texture)
|
|
castbar.texture.icon:Show()
|
|
else
|
|
castbar.texture.icon:Hide()
|
|
end
|
|
|
|
UpdatePosition()
|
|
castbar:Show()
|
|
UpdateProgress()
|
|
end
|
|
|
|
-- Hold Ctrl+Shift+Alt to show a marker where the bar goes and drag it; the
|
|
-- offset from the target frame is saved, so the bar follows the frame when it
|
|
-- is moved. Right-click the marker to go back to the automatic position.
|
|
-- The marker lives on UIParent because the bar itself hides with the target frame.
|
|
local mover
|
|
|
|
local function PlaceMover()
|
|
local x, y = GetOffsets()
|
|
mover:SetScale(TargetFrame:GetEffectiveScale() / UIParent:GetEffectiveScale())
|
|
mover:ClearAllPoints()
|
|
mover:SetPoint("BOTTOM", TargetFrame, "BOTTOM", x, y)
|
|
end
|
|
|
|
local function SaveMoverPosition()
|
|
local mx, my = mover:GetCenter()
|
|
local tx, ty = TargetFrame:GetCenter()
|
|
local bottom, targetBottom = mover:GetBottom(), TargetFrame:GetBottom()
|
|
if not (mx and tx and bottom and targetBottom) then return end
|
|
|
|
local ms, ts = mover:GetEffectiveScale(), TargetFrame:GetEffectiveScale()
|
|
ShaguTweaks_config = ShaguTweaks_config or {}
|
|
ShaguTweaks_config[POSITION_KEY] = { (mx * ms - tx * ts) / ms, (bottom * ms - targetBottom * ts) / ms }
|
|
end
|
|
|
|
local function CreateMover()
|
|
mover = CreateFrame("Frame", "ShaguTweaksTargetCastBarMover", UIParent)
|
|
mover:SetFrameStrata("DIALOG")
|
|
mover:SetWidth(castbar:GetWidth())
|
|
mover:SetHeight(castbar:GetHeight())
|
|
mover:SetMovable(true)
|
|
mover:SetClampedToScreen(true)
|
|
mover:EnableMouse(true)
|
|
mover:Hide()
|
|
|
|
mover.bg = mover:CreateTexture(nil, "BACKGROUND")
|
|
mover.bg:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
mover.bg:SetAllPoints(mover)
|
|
mover.bg:SetVertexColor(1, .7, 0, .6)
|
|
|
|
mover.text = mover:CreateFontString(nil, "OVERLAY")
|
|
mover.text:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE")
|
|
mover.text:SetPoint("CENTER", mover, "CENTER", 0, 0)
|
|
mover.text:SetText(T["Enemy Castbar"])
|
|
|
|
mover:SetScript("OnMouseDown", function()
|
|
if arg1 == "LeftButton" then this:StartMoving() end
|
|
end)
|
|
mover:SetScript("OnMouseUp", function()
|
|
this:StopMovingOrSizing()
|
|
if arg1 == "RightButton" then
|
|
if ShaguTweaks_config then ShaguTweaks_config[POSITION_KEY] = nil end
|
|
else
|
|
SaveMoverPosition()
|
|
end
|
|
castbar.positionX = nil
|
|
UpdatePosition()
|
|
PlaceMover()
|
|
end)
|
|
end
|
|
|
|
local function SetPreview(on)
|
|
if (mover and mover:IsShown() and true or false) == on then return end
|
|
if on then
|
|
if not mover then CreateMover() end
|
|
PlaceMover()
|
|
mover:Show()
|
|
elseif mover then
|
|
mover:StopMovingOrSizing()
|
|
mover:Hide()
|
|
end
|
|
end
|
|
|
|
local function ModifiersDown()
|
|
local shift = API.IsShiftKeyDown or IsShiftKeyDown
|
|
local ctrl = API.IsControlKeyDown or IsControlKeyDown
|
|
local alt = API.IsAltKeyDown or IsAltKeyDown
|
|
return shift() and ctrl() and alt() and true or false
|
|
end
|
|
|
|
module.enable = function(self)
|
|
local castEvents = {
|
|
"UNIT_SPELLCAST_START",
|
|
"UNIT_SPELLCAST_STOP",
|
|
"UNIT_SPELLCAST_INTERRUPTED",
|
|
"UNIT_SPELLCAST_FAILED",
|
|
"UNIT_SPELLCAST_DELAYED",
|
|
"UNIT_SPELLCAST_CHANNEL_START",
|
|
"UNIT_SPELLCAST_CHANNEL_STOP",
|
|
"UNIT_SPELLCAST_CHANNEL_UPDATE",
|
|
}
|
|
|
|
-- ClassicAPI is a required dependency of this fork. Do not restore the old
|
|
-- permanent 20 Hz discovery poller when its cast/event surface is missing.
|
|
if not API or not API.casts or not API.eventutils
|
|
or not _G.C_EventUtils
|
|
or type(_G.C_EventUtils.IsEventValid) ~= "function" then
|
|
return
|
|
end
|
|
|
|
for index = 1, table.getn(castEvents) do
|
|
if not _G.C_EventUtils.IsEventValid(castEvents[index]) then
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Animation-only OnUpdate; hidden frames do not receive OnUpdate ticks.
|
|
castbar:SetScript("OnUpdate", UpdateProgress)
|
|
|
|
local listener = CreateFrame("Frame")
|
|
listener:RegisterEvent("PLAYER_TARGET_CHANGED")
|
|
listener:RegisterEvent("MODIFIER_STATE_CHANGED")
|
|
|
|
for index = 1, table.getn(castEvents) do
|
|
API.RegisterUnitEvent(listener, castEvents[index], "target")
|
|
end
|
|
|
|
listener:SetScript("OnEvent", function()
|
|
if event == "MODIFIER_STATE_CHANGED" then
|
|
SetPreview(ModifiersDown())
|
|
return
|
|
end
|
|
|
|
if event == "PLAYER_TARGET_CHANGED" then
|
|
RefreshCast()
|
|
return
|
|
end
|
|
|
|
-- ClassicAPI fans remote cast events out to every unit token currently
|
|
-- resolving to the caster, including "target" when appropriate.
|
|
if arg1 == "target" then
|
|
RefreshCast()
|
|
end
|
|
end)
|
|
|
|
RefreshCast()
|
|
end
|