Files
ShaguTweaks-ClassicAPI/mods/loss-of-control.lua
T

319 lines
9.8 KiB
Lua

-- Loss of Control Alert
--
-- Shows what is stopping you (stun, fear, silence, school lockout, ...) as a
-- large icon with a countdown in the middle of the screen, like retail.
-- Everything comes from ClassicAPI's C_LossOfControl, which collects the
-- crowd-control debuffs on the player and the school lockout after an
-- interrupt, so nothing here reads the combat log or scans buffs.
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Loss of Control Alert"],
description = T["Shows a large icon and countdown when you are stunned, feared, silenced, rooted, disarmed or locked out of a spell school. Hold <Ctrl>, <Shift> and <Alt> to move it; right-click to reset."],
expansions = { ["vanilla"] = true },
category = T["Interface"],
enabled = false,
})
local ICON = 56
local WIDTH = 300
local BAR_WIDTH = WIDTH - ICON - 30
local THROTTLE = .05
local FALLBACK_ICON = "Interface\\Icons\\Ability_Kick"
local POSITION_KEY = "LossOfControlPosition"
-- Most disabling first. Anything not listed ranks below the list.
local PRIORITY = {
"STUN", "FEAR", "CONFUSE", "CHARM", "POSSESS", "PACIFYSILENCE",
"SILENCE", "PACIFY", "DISARM", "ROOT", "SCHOOL_INTERRUPT",
}
local LABELS = {
STUN = T["Stunned"], FEAR = T["Feared"], CONFUSE = T["Confused"],
CHARM = T["Charmed"], POSSESS = T["Possessed"], SILENCE = T["Silenced"],
PACIFY = T["Pacified"], PACIFYSILENCE = T["Pacified and Silenced"],
DISARM = T["Disarmed"], ROOT = T["Rooted"], SCHOOL_INTERRUPT = T["Interrupted"],
}
local RANK = {}
for i = 1, table.getn(PRIORITY) do RANK[PRIORITY[i]] = i end
-- Border/bar color per kind of effect: hard control red, silences purple,
-- the rest orange, school lockouts blue.
local COLORS = {
STUN = { 1, .2, .2 }, FEAR = { 1, .2, .2 }, CONFUSE = { 1, .2, .2 },
CHARM = { 1, .2, .2 }, POSSESS = { 1, .2, .2 },
SILENCE = { .7, .4, 1 }, PACIFYSILENCE = { .7, .4, 1 }, PACIFY = { .7, .4, 1 },
DISARM = { 1, .6, .1 }, ROOT = { 1, .6, .1 },
SCHOOL_INTERRUPT = { .3, .6, 1 },
}
local DEFAULT_COLOR = COLORS.STUN
-- One table for state: Lua 5.0 allows only 32 upvalues per function.
local S = { unlocked = false, elapsed = 0 }
local function Supported()
local c = _G.C_LossOfControl
return type(c) == "table"
and type(c.GetActiveLossOfControlDataCount) == "function"
and type(c.GetActiveLossOfControlData) == "function"
end
local function FormatTime(seconds)
if seconds >= 10 then return string.format("%d", math.floor(seconds + .5)) end
return string.format("%.1f", seconds)
end
-- The effect to show: highest priority, the longest one among equals.
-- Returns nil when nothing is active.
local function PickEffect()
local best, bestRank, bestLeft
local count = _G.C_LossOfControl.GetActiveLossOfControlDataCount() or 0
for i = 1, count do
local data = _G.C_LossOfControl.GetActiveLossOfControlData(i)
if data then
local rank = RANK[data.locType] or (table.getn(PRIORITY) + 1)
local left = data.timeRemaining or 0
if not best or rank < bestRank or (rank == bestRank and left > bestLeft) then
best, bestRank, bestLeft = data, rank, left
end
end
end
return best
end
local function SchoolName(data)
if data.locType ~= "SCHOOL_INTERRUPT" then return nil end
local spell = _G.C_Spell
if type(spell) == "table" and type(spell.GetSchoolString) == "function" and data.lockoutSchool and data.lockoutSchool ~= 0 then
return spell.GetSchoolString(data.lockoutSchool)
end
end
local function ApplyTheme()
local f = S.frame
local color = ShaguTweaks.UIBorder
local r, g, b = color and color.r or .6, color and color.g or .6, color and color.b or .6
local a = color and color.a or 1
f:SetBackdropBorderColor(r, g, b, a)
f.border:SetBackdropBorderColor(r, g, b, a)
f.icon:SetVertexColor(1, 1, 1, 1)
end
local function Apply(data)
local f = S.frame
local color = COLORS[data.locType] or DEFAULT_COLOR
f.icon:SetTexture((data.iconTexture and data.iconTexture ~= "") and data.iconTexture or FALLBACK_ICON)
ApplyTheme()
f.bar:SetVertexColor(color[1], color[2], color[3])
f.title:SetText(LABELS[data.locType] or data.displayText or "")
-- spell name under the label; school lockouts name the locked school
local detail = SchoolName(data)
if not detail and data.locType ~= "SCHOOL_INTERRUPT" then detail = data.displayText end
f.detail:SetText(detail or "")
f.data = data
end
local function UpdateTime()
local f = S.frame
local data = f.data
if not data then return end
local left = data.timeRemaining
if left and left > 0 then
f.time:SetText(FormatTime(left))
local duration = data.duration
if duration and duration > 0 then
f.bar:SetWidth(math.max(1, BAR_WIDTH * math.min(left / duration, 1)))
f.bar:Show()
else
f.bar:Hide()
end
else
f.time:SetText("")
f.bar:Hide()
end
end
-- Pick again and redraw. Called on the events and while the alert is up,
-- because re-locking a school extends its lockout without firing an event.
local function Refresh()
local f = S.frame
if S.unlocked then return end
local data = PickEffect()
if data then
Apply(data)
UpdateTime()
if not f:IsShown() then f:Show() end
elseif f:IsShown() then
f.data = nil
f:Hide()
end
end
local function OnUpdate()
S.elapsed = S.elapsed + (arg1 or 0)
if S.elapsed < THROTTLE then return end
S.elapsed = 0
Refresh()
end
local function RestorePosition()
local pos = ShaguTweaks_config and ShaguTweaks_config[POSITION_KEY]
S.frame:ClearAllPoints()
if pos and pos[1] and pos[2] then
S.frame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", pos[1], pos[2])
else
S.frame:SetPoint("CENTER", UIParent, "CENTER", 0, 160)
end
end
local ModifiersDown = ShaguTweaks.API.AreMoveModifiersDown
-- Hold Ctrl+Shift+Alt to show a preview and drag the alert.
local function SetUnlocked(on)
if S.unlocked == on then return end
local f = S.frame
S.unlocked = on
f:EnableMouse(on)
if on then
f.data = nil
f.icon:SetTexture(FALLBACK_ICON)
ApplyTheme()
f.bar:SetVertexColor(1, .2, .2)
f.bar:SetWidth(BAR_WIDTH)
f.bar:Show()
f.title:SetText(T["Stunned"])
f.detail:SetText(T["Drag to move, right-click to reset"])
f.time:SetText("8.0")
f:Show()
else
f:StopMovingOrSizing()
f:Hide()
Refresh()
end
end
local function CreateAlert()
if S.frame then return end
local f = CreateFrame("Frame", "ShaguTweaksLossOfControl", UIParent)
f:SetWidth(WIDTH)
f:SetHeight(ICON + 24)
f:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16,
edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 } })
f:SetBackdropColor(0, 0, 0, .9)
f:SetFrameStrata("HIGH")
f:SetMovable(true)
f:SetClampedToScreen(true)
f:EnableMouse(false)
f:Hide()
S.frame = f
f.icon = f:CreateTexture(nil, "ARTWORK")
f.icon:SetWidth(ICON)
f.icon:SetHeight(ICON)
f.icon:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -10)
f.icon:SetTexCoord(.08, .92, .08, .92)
-- Classic tooltip rim, kept separate from the effect-colored timer fill.
f.border = CreateFrame("Frame", nil, f)
f.border:SetBackdrop({ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
edgeSize = 12, insets = { left = 3, right = 3, top = 3, bottom = 3 } })
f.border:SetPoint("TOPLEFT", f.icon, "TOPLEFT", -3, 3)
f.border:SetPoint("BOTTOMRIGHT", f.icon, "BOTTOMRIGHT", 3, -3)
f.title = f:CreateFontString(nil, "OVERLAY")
f.title:SetFontObject(GameFontNormalLarge)
f.title:SetPoint("TOPLEFT", f.icon, "TOPRIGHT", 10, -2)
f.title:SetJustifyH("LEFT")
f.title:SetWidth(WIDTH - ICON - 80)
f.detail = f:CreateFontString(nil, "OVERLAY")
f.detail:SetFontObject(GameFontHighlightSmall)
f.detail:SetPoint("TOPLEFT", f.title, "BOTTOMLEFT", 0, -2)
f.detail:SetJustifyH("LEFT")
f.detail:SetTextColor(.85, .85, .85)
f.detail:SetWidth(BAR_WIDTH)
f.detail:SetHeight(28)
f.time = f:CreateFontString(nil, "OVERLAY")
f.time:SetFont("Fonts\\FRIZQT__.TTF", 22, "")
f.time:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -10)
f.time:SetJustifyH("RIGHT")
f.time:SetTextColor(1, 1, 1)
-- remaining time as a bar under the text
f.bar = f:CreateTexture(nil, "ARTWORK")
f.bar:SetTexture("Interface\\TargetingFrame\\UI-StatusBar")
f.bar:SetHeight(4)
f.bar:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", ICON + 20, 10)
f:SetScript("OnMouseDown", function()
if arg1 == "LeftButton" then this:StartMoving() end
end)
f:SetScript("OnMouseUp", function()
this:StopMovingOrSizing()
ShaguTweaks_config = ShaguTweaks_config or {}
if arg1 == "RightButton" then
ShaguTweaks_config[POSITION_KEY] = nil
else
ShaguTweaks_config[POSITION_KEY] = { this:GetLeft(), this:GetBottom() }
end
RestorePosition()
end)
-- Runs only while the alert is shown (a hidden frame gets no OnUpdate).
f:SetScript("OnUpdate", OnUpdate)
S.events = CreateFrame("Frame")
S.events:SetScript("OnEvent", function()
if event == "MODIFIER_STATE_CHANGED" then
SetUnlocked(ModifiersDown())
else
Refresh()
end
end)
ApplyTheme()
RestorePosition()
end
module.enable = function(self)
if not Supported() then return end
CreateAlert()
S.events:RegisterEvent("LOSS_OF_CONTROL_ADDED")
S.events:RegisterEvent("LOSS_OF_CONTROL_UPDATE")
S.events:RegisterEvent("PLAYER_ENTERING_WORLD")
S.events:RegisterEvent("MODIFIER_STATE_CHANGED")
SLASH_SHAGULOSSOFCONTROL1 = "/lossofcontrol"
SlashCmdList["SHAGULOSSOFCONTROL"] = function(msg)
msg = string.lower(msg or "")
if msg == "reset" then
ShaguTweaks_config = ShaguTweaks_config or {}
ShaguTweaks_config[POSITION_KEY] = nil
RestorePosition()
end
end
Refresh()
end
module.disable = function(self)
if not S.frame then return end
if S.unlocked then SetUnlocked(false) end
S.events:UnregisterAllEvents()
S.frame:Hide()
end