397 lines
13 KiB
Plaintext
397 lines
13 KiB
Plaintext
-- SoulstoneTracker
|
|
-- Tracks soulstones cast by warlocks on raid members
|
|
-- Author: Waylock
|
|
|
|
SoulstoneTracker = {}
|
|
local m = SoulstoneTracker
|
|
|
|
local SOULSTONE_DURATION = 30 * 60
|
|
local FONT = "Fonts\\FRIZQT__.TTF"
|
|
local FRAME_W = 380
|
|
local ROW_H = 18
|
|
local TITLE_H = 24
|
|
local COL1 = 8 -- Caster x
|
|
local COL2 = 148 -- Arrow x
|
|
local COL3 = 172 -- Target x
|
|
local COL4 = 300 -- Expires x
|
|
|
|
m.stones = {}
|
|
m.frame = nil
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Utility
|
|
-------------------------------------------------------------------------------
|
|
|
|
local function timeLeft(expires)
|
|
return math.max(0, expires - time())
|
|
end
|
|
|
|
local function formatTime(secs)
|
|
if secs <= 0 then
|
|
return "|cffff3333EXPIRED|r"
|
|
elseif secs <= 300 then
|
|
return "|cffff7c0a"..string.format("%dm %02ds", math.floor(secs/60), math.mod(secs,60)).."|r"
|
|
else
|
|
return "|cff00ff98"..string.format("%dm %02ds", math.floor(secs/60), math.mod(secs,60)).."|r"
|
|
end
|
|
end
|
|
|
|
local function say(msg)
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffa050ff[SS]|r " .. msg)
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Tracking
|
|
-------------------------------------------------------------------------------
|
|
|
|
function m.addStone(target, caster)
|
|
m.stones[target] = { caster = caster, expires = time() + SOULSTONE_DURATION }
|
|
say("|cffa050ff"..caster.."|r soulstoned |cffffffff"..target.."|r")
|
|
m.refresh()
|
|
end
|
|
|
|
function m.removeStone(target)
|
|
if m.stones[target] then
|
|
m.stones[target] = nil
|
|
m.refresh()
|
|
end
|
|
end
|
|
|
|
function m.clearExpired()
|
|
local now = time()
|
|
local changed = false
|
|
for target, data in pairs(m.stones) do
|
|
if now >= data.expires then
|
|
say("|cffaaaaaa Soulstone expired on |cffffffff"..target.."|r")
|
|
m.stones[target] = nil
|
|
changed = true
|
|
end
|
|
end
|
|
if changed then m.refresh() end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- UI
|
|
-------------------------------------------------------------------------------
|
|
|
|
function m.createFrame()
|
|
-- Outer frame
|
|
local f = CreateFrame("Frame", "SoulstoneTrackerFrame", UIParent)
|
|
f:SetWidth(FRAME_W)
|
|
f:SetHeight(TITLE_H + 22 + ROW_H)
|
|
f:SetFrameStrata("MEDIUM")
|
|
f:SetBackdrop({
|
|
bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeSize = 1,
|
|
insets = { left=1, right=1, top=1, bottom=1 }
|
|
})
|
|
f:SetBackdropColor(0.05, 0.05, 0.08, 0.95)
|
|
f:SetBackdropBorderColor(0.25, 0.25, 0.35, 1)
|
|
f:EnableMouse(true)
|
|
f:SetMovable(true)
|
|
f:SetClampedToScreen(true)
|
|
f:RegisterForDrag("LeftButton")
|
|
f:SetScript("OnDragStart", function() this:StartMoving() end)
|
|
f:SetScript("OnDragStop", function()
|
|
this:StopMovingOrSizing()
|
|
local point, _, rpoint, x, y = this:GetPoint()
|
|
SoulstoneTrackerDB.position = { point=point, rpoint=rpoint, x=x, y=y }
|
|
end)
|
|
|
|
-- Title bar - full width purple gradient
|
|
local titleBar = CreateFrame("Frame", nil, f)
|
|
titleBar:SetPoint("TopLeft", f, "TopLeft", 1, -1)
|
|
titleBar:SetPoint("TopRight", f, "TopRight", -1, -1)
|
|
titleBar:SetHeight(TITLE_H)
|
|
titleBar:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
|
|
titleBar:SetBackdropColor(0.22, 0.1, 0.38, 1)
|
|
|
|
-- Gradient overlay on title bar
|
|
local grad = titleBar:CreateTexture(nil, "OVERLAY")
|
|
grad:SetAllPoints(titleBar)
|
|
grad:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
|
|
grad:SetBlendMode("ADD")
|
|
grad:SetVertexColor(0.3, 0.1, 0.5, 0.5)
|
|
|
|
-- Title text
|
|
local title = titleBar:CreateFontString(nil, "OVERLAY")
|
|
title:SetFont(FONT, 11, "OUTLINE")
|
|
title:SetPoint("Left", titleBar, "Left", 8, 0)
|
|
title:SetText("|cffffd700Soulstone Tracker v1.0|r")
|
|
|
|
-- X close button (plain text style like Threatmeter)
|
|
local closeBtn = CreateFrame("Button", nil, titleBar)
|
|
closeBtn:SetWidth(16)
|
|
closeBtn:SetHeight(16)
|
|
closeBtn:SetPoint("Right", titleBar, "Right", -4, 0)
|
|
local closeTxt = closeBtn:CreateFontString(nil, "OVERLAY")
|
|
closeTxt:SetFont(FONT, 12, "OUTLINE")
|
|
closeTxt:SetAllPoints(closeBtn)
|
|
closeTxt:SetJustifyH("Center")
|
|
closeTxt:SetText("|cffaaaaааX|r")
|
|
closeBtn:SetScript("OnEnter", function() closeTxt:SetText("|cffffffffX|r") end)
|
|
closeBtn:SetScript("OnLeave", function() closeTxt:SetText("|cffaaaaaaX|r") end)
|
|
closeBtn:SetScript("OnClick", function()
|
|
f:Hide()
|
|
SoulstoneTrackerDB.hidden = true
|
|
end)
|
|
|
|
-- Column header row
|
|
local hY = -(TITLE_H + 4)
|
|
|
|
local hCaster = f:CreateFontString(nil, "OVERLAY")
|
|
hCaster:SetFont(FONT, 10, "OUTLINE")
|
|
hCaster:SetTextColor(1, 1, 1, 1)
|
|
hCaster:SetPoint("TopLeft", f, "TopLeft", COL1, hY)
|
|
hCaster:SetText("Caster")
|
|
|
|
local hTarget = f:CreateFontString(nil, "OVERLAY")
|
|
hTarget:SetFont(FONT, 10, "OUTLINE")
|
|
hTarget:SetTextColor(1, 1, 1, 1)
|
|
hTarget:SetPoint("TopLeft", f, "TopLeft", COL3, hY)
|
|
hTarget:SetText("Target")
|
|
|
|
local hExpires = f:CreateFontString(nil, "OVERLAY")
|
|
hExpires:SetFont(FONT, 10, "OUTLINE")
|
|
hExpires:SetTextColor(1, 1, 1, 1)
|
|
hExpires:SetPoint("TopLeft", f, "TopLeft", COL4, hY)
|
|
hExpires:SetText("Expires")
|
|
|
|
-- Thin divider under headers
|
|
local div = f:CreateTexture(nil, "ARTWORK")
|
|
div:SetHeight(1)
|
|
div:SetPoint("TopLeft", f, "TopLeft", 1, -(TITLE_H + 18))
|
|
div:SetPoint("TopRight", f, "TopRight", -1, -(TITLE_H + 18))
|
|
div:SetTexture(0.3, 0.2, 0.5, 0.8)
|
|
|
|
-- Empty state
|
|
local empty = f:CreateFontString(nil, "OVERLAY")
|
|
empty:SetFont(FONT, 10, "OUTLINE")
|
|
empty:SetTextColor(0.4, 0.4, 0.4, 1)
|
|
empty:SetPoint("TopLeft", f, "TopLeft", COL1, -(TITLE_H + 22))
|
|
empty:SetText("No active soulstones")
|
|
f.empty = empty
|
|
|
|
-- Data rows
|
|
f.rows = {}
|
|
for i = 1, 10 do
|
|
local rowY = -(TITLE_H + 20 + (i-1) * ROW_H)
|
|
|
|
-- Alternating row background
|
|
local bg = f:CreateTexture(nil, "BACKGROUND")
|
|
bg:SetPoint("TopLeft", f, "TopLeft", 1, rowY)
|
|
bg:SetPoint("TopRight", f, "TopRight", -1, rowY)
|
|
bg:SetHeight(ROW_H)
|
|
if math.mod(i, 2) == 0 then
|
|
bg:SetTexture(1, 1, 1, 0.04)
|
|
else
|
|
bg:SetTexture(0, 0, 0, 0)
|
|
end
|
|
|
|
local caster = f:CreateFontString(nil, "OVERLAY")
|
|
caster:SetFont(FONT, 11, "OUTLINE")
|
|
caster:SetPoint("TopLeft", f, "TopLeft", COL1, rowY)
|
|
caster:SetWidth(COL2 - COL1 - 4)
|
|
caster:SetJustifyH("Left")
|
|
|
|
local arrow = f:CreateFontString(nil, "OVERLAY")
|
|
arrow:SetFont(FONT, 11, "OUTLINE")
|
|
arrow:SetTextColor(0.4, 0.4, 0.4, 1)
|
|
arrow:SetPoint("TopLeft", f, "TopLeft", COL2, rowY)
|
|
arrow:SetText("->")
|
|
|
|
local target = f:CreateFontString(nil, "OVERLAY")
|
|
target:SetFont(FONT, 11, "OUTLINE")
|
|
target:SetPoint("TopLeft", f, "TopLeft", COL3, rowY)
|
|
target:SetWidth(COL4 - COL3 - 4)
|
|
target:SetJustifyH("Left")
|
|
|
|
local timer = f:CreateFontString(nil, "OVERLAY")
|
|
timer:SetFont(FONT, 11, "OUTLINE")
|
|
timer:SetPoint("TopLeft", f, "TopLeft", COL4, rowY)
|
|
timer:SetJustifyH("Left")
|
|
|
|
f.rows[i] = { caster=caster, arrow=arrow, target=target, timer=timer }
|
|
end
|
|
|
|
-- Resize grip (bottom-right corner)
|
|
f:SetResizable(true)
|
|
local grip = CreateFrame("Button", nil, f)
|
|
grip:SetWidth(12)
|
|
grip:SetHeight(12)
|
|
grip:SetPoint("BottomRight", f, "BottomRight", -2, 2)
|
|
local gripTex = grip:CreateTexture(nil, "OVERLAY")
|
|
gripTex:SetAllPoints(grip)
|
|
gripTex:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
|
|
grip:SetScript("OnEnter", function()
|
|
gripTex:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Highlight")
|
|
end)
|
|
grip:SetScript("OnLeave", function()
|
|
gripTex:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
|
|
end)
|
|
grip:SetScript("OnMouseDown", function()
|
|
f:StartSizing("BOTTOMRIGHT")
|
|
end)
|
|
grip:SetScript("OnMouseUp", function()
|
|
f:StopMovingOrSizing()
|
|
local w, h = f:GetWidth(), f:GetHeight()
|
|
SoulstoneTrackerDB.size = { w = w, h = h }
|
|
end)
|
|
|
|
-- Ticker
|
|
local tick = 0
|
|
f:SetScript("OnUpdate", function()
|
|
tick = tick + arg1
|
|
if tick >= 1 then
|
|
tick = 0
|
|
m.clearExpired()
|
|
m.refresh()
|
|
end
|
|
end)
|
|
|
|
-- Restore saved position
|
|
if SoulstoneTrackerDB and SoulstoneTrackerDB.position then
|
|
local p = SoulstoneTrackerDB.position
|
|
f:ClearAllPoints()
|
|
f:SetPoint(p.point, UIParent, p.rpoint, p.x, p.y)
|
|
else
|
|
f:SetPoint("Center", UIParent, "Center", 300, 0)
|
|
end
|
|
|
|
-- Restore saved size
|
|
if SoulstoneTrackerDB and SoulstoneTrackerDB.size then
|
|
local s = SoulstoneTrackerDB.size
|
|
f:SetWidth(s.w)
|
|
f:SetHeight(s.h)
|
|
end
|
|
|
|
m.frame = f
|
|
m.refresh()
|
|
end
|
|
|
|
function m.refresh()
|
|
if not m.frame then return end
|
|
local f = m.frame
|
|
|
|
local list = {}
|
|
for target, data in pairs(m.stones) do
|
|
table.insert(list, { target=target, caster=data.caster, expires=data.expires })
|
|
end
|
|
table.sort(list, function(a,b) return a.expires < b.expires end)
|
|
|
|
local count = table.getn(list)
|
|
|
|
if count == 0 then f.empty:Show() else f.empty:Hide() end
|
|
|
|
for i = 1, 10 do
|
|
local row = f.rows[i]
|
|
local data = list[i]
|
|
if data then
|
|
row.caster:SetText("|cffa050ff"..data.caster.."|r")
|
|
row.arrow:SetText("->")
|
|
row.target:SetText("|cffffffff"..data.target.."|r")
|
|
row.timer:SetText(formatTime(timeLeft(data.expires)))
|
|
else
|
|
row.caster:SetText("")
|
|
row.arrow:SetText("")
|
|
row.target:SetText("")
|
|
row.timer:SetText("")
|
|
end
|
|
end
|
|
|
|
local rows = math.max(1, count)
|
|
f:SetHeight(TITLE_H + 20 + (rows * ROW_H) + 6)
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Events
|
|
-------------------------------------------------------------------------------
|
|
|
|
local eFrame = CreateFrame("Frame")
|
|
eFrame:RegisterEvent("PLAYER_LOGIN")
|
|
eFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_BUFF")
|
|
eFrame:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_SELF")
|
|
eFrame:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_OTHER")
|
|
eFrame:RegisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF")
|
|
eFrame:RegisterEvent("CHAT_MSG_SPELL_PARTY_BUFF")
|
|
eFrame:RegisterEvent("CHAT_MSG_COMBAT_FRIENDLY_DEATH")
|
|
|
|
eFrame:SetScript("OnEvent", function()
|
|
if event == "PLAYER_LOGIN" then
|
|
SoulstoneTrackerDB = SoulstoneTrackerDB or {}
|
|
m.createFrame()
|
|
if SoulstoneTrackerDB.hidden then m.frame:Hide() else m.frame:Show() end
|
|
say("Loaded. |cffffd700/ss|r toggle |cffffd700/ss test|r preview |cffffd700/ss clear|r reset")
|
|
|
|
elseif event == "CHAT_MSG_SPELL_SELF_BUFF" then
|
|
if arg1 and string.find(arg1, "Soulstone Resurrection") then
|
|
m.addStone(UnitName("player"), "Unknown")
|
|
end
|
|
|
|
elseif event == "CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF"
|
|
or event == "CHAT_MSG_SPELL_PARTY_BUFF" then
|
|
if arg1 then
|
|
local caster, target = string.match(arg1, "(.+)'s Soulstone Resurrection is cast on (.+)%.")
|
|
if caster and target then m.addStone(target, caster) return end
|
|
local gained = string.match(arg1, "(.+) gains Soulstone Resurrection%.")
|
|
if gained then m.addStone(gained, "Unknown") end
|
|
end
|
|
|
|
elseif event == "CHAT_MSG_SPELL_AURA_GONE_SELF" then
|
|
if arg1 and string.find(arg1, "Soulstone Resurrection") then
|
|
m.removeStone(UnitName("player"))
|
|
end
|
|
|
|
elseif event == "CHAT_MSG_SPELL_AURA_GONE_OTHER" then
|
|
if arg1 then
|
|
local target = string.match(arg1, "Soulstone Resurrection fades from (.+)%.")
|
|
if target then m.removeStone(target) end
|
|
end
|
|
|
|
elseif event == "CHAT_MSG_COMBAT_FRIENDLY_DEATH" then
|
|
if arg1 then
|
|
local dead = string.match(arg1, "(.+) dies%.")
|
|
if dead and m.stones[dead] then m.removeStone(dead) end
|
|
end
|
|
end
|
|
end)
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Slash
|
|
-------------------------------------------------------------------------------
|
|
|
|
SLASH_SOULSTONETRACKER1 = "/ss"
|
|
SLASH_SOULSTONETRACKER2 = "/soulstone"
|
|
SlashCmdList["SOULSTONETRACKER"] = function(args)
|
|
args = string.lower(args or "")
|
|
args = string.gsub(args, "^%s*(.-)%s*$", "%1")
|
|
if args == "clear" then
|
|
m.stones = {}
|
|
m.refresh()
|
|
say("Cleared.")
|
|
elseif args == "test" then
|
|
m.addStone(UnitName("player"), UnitName("player"))
|
|
say("Test stone added.")
|
|
elseif args == "list" then
|
|
local n = 0
|
|
for target, data in pairs(m.stones) do
|
|
local s = timeLeft(data.expires)
|
|
say(data.caster.." -> "..target.." ("..math.floor(s/60).."m "..math.mod(s,60).."s)")
|
|
n = n + 1
|
|
end
|
|
if n == 0 then say("No active soulstones.") end
|
|
else
|
|
if m.frame and m.frame:IsVisible() then
|
|
m.frame:Hide()
|
|
SoulstoneTrackerDB.hidden = true
|
|
say("Hidden.")
|
|
else
|
|
if not m.frame then m.createFrame() end
|
|
m.frame:Show()
|
|
SoulstoneTrackerDB.hidden = false
|
|
say("Shown.")
|
|
end
|
|
end
|
|
end
|