Files
SoulStoneTracker/SoulstoneTracker.lua.bak
T
2026-10-30 16:50:39 +00:00

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