Add Loss of Control Alert, Totem Timers, Movable Cast Bar and movable Enemy Castbars (3.7.2), Docs/EXTRAS_MERGE.md for the Extras merge

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
This commit is contained in:
2026-11-02 22:23:04 +00:00
co-authored by Claude Sonnet 5.5
parent 338135be34
commit 8e47666644
9 changed files with 944 additions and 10 deletions
+13
View File
@@ -4,6 +4,19 @@ Changes in this fork (OEVG/ShaguTweaks-ClassicAPI), newest first. Each entry is
This fork is based on [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-ClassicAPI) at commit `4391564` (2026-09-08). Earlier changes are in that repository's history.
## 3.7.2 — 2026-11-02 — Loss of Control Alert, Totem Timers and Movable Cast Bar
### Added
- **Loss of Control Alert** (Interface, off): a large icon, the effect's name and a countdown with a time bar in the middle of the screen while you are stunned, feared, confused, charmed, possessed, silenced, pacified, disarmed, rooted or locked out of a spell school (a kick or Counterspell shows the school). The most disabling effect is shown when several are active. Hold Ctrl+Shift+Alt to show a preview and drag it, right-click to reset (`/lossofcontrol reset` also works). Uses ClassicAPI's `C_LossOfControl`, redraws on its events and ticks only while the alert is visible.
- **Totem Timers** (Interface, off, Shamans only): an icon per active totem in the order Fire, Earth, Water, Air, with the time left (yellow under 10 seconds, red under 5) and a bar. Left-click targets the totem (`TargetTotem`), hovering shows its tooltip. Hold Ctrl+Shift+Alt to show four preview icons and drag them, right-click to reset (`/totemtimers reset` also works). Uses ClassicAPI's `GetTotemInfo`, `GetTotemTimeLeft` and `PLAYER_TOTEM_UPDATE`, and ticks only while a totem is out.
- **Enemy Castbars** (Unit Frames) is movable too: hold Ctrl+Shift+Alt to show a marker where the target's cast bar goes, drag it, right-click to reset. The spot is saved as an offset from the target frame, so the bar follows the frame when you move it, and the automatic position below the buffs no longer applies once you have moved it. The marker also shows without a target.
- **Movable Cast Bar** (Action Bar, on): hold Ctrl+Shift+Alt to show a marker over the player's cast bar and drag it anywhere, right-click it to put the bar back. The position is saved per character, and it keeps the spot even when Reduced Actionbar or another addon re-anchors the bar. The Swing Timer, Wand Timer and the Improved Castbar icon are anchored to the cast bar and follow it. Nothing changes until you move it.
### Changed
- **README:** the "Built-in Extras modules" link now opens [Docs/EXTRAS_MERGE.md](Docs/EXTRAS_MERGE.md), which lists where each Extras module went, which ones were left out and why.
## 3.7.1 — 2026-11-02 — Key-Down Casting removed
### Removed
+53
View File
@@ -0,0 +1,53 @@
# ShaguTweaks Extras ClassicAPI: what moved where
Since version 3.7 the modules of Dusk's [ShaguTweaks Extras ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI) are part of this addon, so you no longer need to install it. This page lists where every module went, what was left out and why.
Copied from Extras commit `8e14bb4` (2026-09-18). Credits and licenses are in `THIRD_PARTY_NOTICES.md`.
## What to do when updating
1. Install this addon (3.7 or newer).
2. Disable or delete the `ShaguTweaks-extras` folder in `Interface\AddOns`. While it stays enabled, its older copies replace the built-in ones, and a message in chat reminds you at login.
3. Your on/off choices carry over, because the modules kept their names.
## Where the modules went
Open the settings with **Esc → Advanced Options**.
| Module | Now in category |
|---|---|
| Dragonflight Gryphons | Action Bar |
| Floating Actionbar | Action Bar |
| Hide Macro Text | Action Bar |
| Mouseover Right, Mouseover Right 2 | Action Bar |
| Reagent Counter | Action Bar |
| Center Vertical Actionbar | Action Bar |
| Show Bags, Show Micro Menu | Action Bar |
| Center Text Input Box | Chat & Social |
| Block NPC Spam | Chat & Social |
| Enable Text Shadow | Chat & Social |
| Chat Timestamps | Chat & Social |
| World Chat Hider | Chat & Social |
| Loot Quality | Loot |
| Macro Icons, Macro Tweaks | **Macro** (new category) |
| Buy Em All | **Merchant** (new category) |
| MiniMap Framerate & Latency | Minimap & World Map |
| Reveal World Map | Minimap & World Map |
| Cursor Tooltip, Hide Combat Tooltip, Metric Range | Tooltip & Items |
| Unit Frame Abbreviated Names | Unit Frames |
The textures these modules use (the gryphons and the XP bar) moved to this addon's `img` folder.
## Left out, and why
| Module | Reason |
|---|---|
| **Raid Frames**, with all its options (Show Aggro Indicators, Show Combat Feedback, Use Compact Layout, Show Dispel Indicators, Show Group Headers, Show Healing Predictions, Hide Party Frames, Use As Party Frames) | Not wanted in this fork. If you used them, the default raid frames come back. |
| **Bag Item Click** and **Bag Search Bar** | [Bagshui](https://octowow.st/git/OEVG/Bagshui) already has a search box and sends items to trade, the auction house and mail on right-click. |
| **Auction Alt-Buy** | [aux](https://octowow.st/git/Dusk/aux-addon-ClassicAPI) already buys out or cancels with Alt+click on a row. |
## Removed later
| Module | Version | Reason |
|---|---|---|
| **Key-Down Casting** and **Alt Self-Cast** | 3.7.1 | ClassicAPI now has the game's own `ActionButtonUseKeyDown` setting. Type `/console ActionButtonUseKeyDown 1` once instead. Details in the [changelog](../CHANGELOG.md). |
+16 -4
View File
@@ -9,15 +9,15 @@ This is an English-only customization of [Dusk/ShaguTweaks-ClassicAPI](https://o
> This fork requires ClassicAPI and targets **Turtle WoW-like server/client environments**.
**Version 3.7.1.** What changed in each update: see the [changelog](CHANGELOG.md).
**Version 3.7.2.** What changed in each update: see the [changelog](CHANGELOG.md).
> **Upgrading to 3.7:** the modules of the separate *ShaguTweaks Extras ClassicAPI* addon are now built into ShaguTweaks. **Disable or delete `ShaguTweaks-extras`** after updating, otherwise its older copies replace the built-in ones. Your on/off settings carry over. Raid Frames, Bag Item Click, Bag Search Bar and Auction Alt-Buy were not merged. See [Built-in Extras modules](#built-in-extras-modules).
> **Upgrading to 3.7:** the modules of the separate *ShaguTweaks Extras ClassicAPI* addon are now built into ShaguTweaks. **Disable or delete `ShaguTweaks-extras`** after updating, otherwise its older copies replace the built-in ones. Your on/off settings carry over. Raid Frames, Bag Item Click, Bag Search Bar and Auction Alt-Buy were not merged. See [Docs/EXTRAS_MERGE.md](Docs/EXTRAS_MERGE.md) for where each module went and why some were left out.
## 🔌 Requirements
- **[ClassicAPI](https://github.com/brues-code/ClassicAPI) — required.** Install both the DLL and its bundled `!!!ClassicAPI` addon, and restart the client after a DLL update. v1.18.10 or newer is recommended (it includes a crash fix for addons that restyle nameplates). The integration was last reviewed against v1.18.13; see [Docs/CLASSICAPI_UPDATES.md](Docs/CLASSICAPI_UPDATES.md).
- **[SuperWoW](https://github.com/balakethelock/SuperWoW) — optional / recommended.** Only used as an additional fallback for some cast/GUID information.
- **[ShaguTweaks Extras ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI) — no longer needed.** Its modules are built in since 3.7 (see **Built-in Extras modules** below). Disable or remove it; while it is enabled, its copies replace the built-in ones.
- **[ShaguTweaks Extras ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI) — no longer needed.** Its modules are built in since 3.7 (see [Docs/EXTRAS_MERGE.md](Docs/EXTRAS_MERGE.md)). Disable or remove it; while it is enabled, its copies replace the built-in ones.
ClassicAPI is the main API layer.
@@ -43,6 +43,13 @@ Settings: **Esc → Advanced Options**.
- **Movable Unit Frames:** Player, Target, Party, Minimap, Buffs, Debuffs and Weapon Buffs share one mover and alignment grid. Hold **Ctrl+Shift+Alt** to drag; right-click while unlocked to reset.
- **Hide Pet Unit Frame:** hides the pet frame below the player frame for pet classes. The stock pet code keeps running, so nothing breaks when the pet is summoned, dismissed or dies.
**Loss of Control Alert and Totem Timers** (both off by default; they need ClassicAPI)
- **Loss of Control Alert** (Interface): a large icon, the name of the effect and a countdown above the middle of the screen when you are stunned, feared, confused, charmed, silenced, pacified, disarmed, rooted or locked out of a spell school after an interrupt. If several apply, the most disabling one is shown. Hold Ctrl+Shift+Alt to show a preview and drag it, right-click to reset (like the Swing Timer). Uses ClassicAPI's `C_LossOfControl`.
- **Totem Timers** (Interface, Shaman only): one icon per totem you have out (Fire, Earth, Water, Air) with the time left, turning yellow at 10 seconds and red at 5. Left-click an icon to target the totem; hover for its tooltip. Hold Ctrl+Shift+Alt to show four preview icons and drag them, right-click to reset. Uses ClassicAPI's `GetTotemInfo` and `PLAYER_TOTEM_UPDATE`.
**Movable Cast Bar** (Action Bar, on): hold Ctrl+Shift+Alt, drag the marker that appears over the cast bar, right-click to reset. The position is saved per character and survives other addons re-anchoring the bar; the Swing Timer, Wand Timer and Improved Castbar icon follow it. **Enemy Castbars** (the target's cast bar) works the same way: its marker appears even without a target, and the saved spot is an offset from the target frame, so the bar follows the frame.
**Swing Timer and Wand Timer**
- **Swing Timer:** main-hand and off-hand swing timers styled like the classic cast bar, with the weapon's icon on the left. They sit just above the cast bar; hold **Ctrl+Shift+Alt** to drag them and right-click to reset.
@@ -61,7 +68,7 @@ Settings: **Esc → Advanced Options**.
- **WorldMap Window:** the map keeps the size you pick with Ctrl+Mousewheel (saved per character), and the title bar drags the window as well. With [OctoBus](https://github.com/Phridgey/OctoBus) installed, a **Transports** button left of the Continent dropdown shows or hides its transport timers.
- **Loot History:** `/loothistory` lists recent group-loot rolls with the winner of each item; click an item for everyone's need, greed or pass and their rolls. Ported from [brues-code/pfUI](https://github.com/brues-code/pfUI). Requires ClassicAPI.
**Built-in Extras modules**
**Built-in Extras modules** — full list of where each one went and what was left out: [Docs/EXTRAS_MERGE.md](Docs/EXTRAS_MERGE.md)
- The modules of Dusk's [ShaguTweaks Extras ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI) are part of this addon, each under the matching category: Action Bar, Chat & Social, Loot, Minimap & World Map, Tooltip & Items and Unit Frames, plus two new categories, **Macro** and **Merchant**.
- They keep their names, so your saved on/off choices carry over.
@@ -102,6 +109,8 @@ Modules that use ClassicAPI directly:
- Skip Gossip Text
- Swing Timer
- Wand Timer
- Loss of Control Alert
- Totem Timers
- Enemy Castbars
- Tooltip Details
- WorldMap Window
@@ -126,6 +135,7 @@ Names match the in-game options list. Modules marked *(off)* are disabled until
- Cooldown Numbers
- Hide Gryphons *(off)*
- Improved Castbar *(off)*
- Movable Cast Bar (hold Ctrl+Shift+Alt to drag it, right-click to reset)
- Swing Timer *(off)*
- Wand Timer *(off)*
- No Toggle Auto-Attack
@@ -223,6 +233,8 @@ Names match the in-game options list. Modules marked *(off)* are disabled until
- Darkened UI *(off)*
- Hide Errors *(off)*
- Smaller Error Frame *(off)*
- Loss of Control Alert *(off)*
- Totem Timers *(off, Shaman)*
- XP Bar Text (current/needed XP, percentage and rested bonus on hover)
### General
+4 -1
View File
@@ -2,7 +2,7 @@
## Title: |cffffcc00Shagu|cffffffffTweaks |cffcccccc[ClassicAPI]
## Author: Shagu
## Notes: A collection of tweaks and enhancements to the Default UI
## Version: 3.7.1
## Version: 3.7.2
## Dependencies: !!!ClassicAPI
## SavedVariables: ShaguTweaks_player_cache, ShaguTweaks_social_cache
## SavedVariablesPerCharacter: ShaguTweaks_config, ShaguTweaks_cache
@@ -80,8 +80,11 @@ mods\no-toggle.lua
mods\aoe-loot.lua
mods\loot-cursor.lua
mods\loot-history.lua
mods\loss-of-control.lua
mods\totem-timers.lua
mods\improved-roll-frames.lua
mods\improved-castbar.lua
mods\castbar-move.lua
mods\swing-timer.lua
mods\smaller-errors-frame.lua
mods\skip-gossip.lua
+21 -1
View File
@@ -64,7 +64,7 @@ ShaguTweaks_translation["enUS"] = {
["Show class colors in Who, Guild, Friends and Chat."] = nil,
["Show debuff durations on the target unit frame."] = nil,
["Show item rarity as the border color on bags, bank, character and inspect frames."] = nil,
["Shows an enemy castbar on target unit frame."] = nil,
["Shows an enemy castbar on target unit frame. Hold <Ctrl>, <Shift> and <Alt> to move it; right-click to reset."] = nil,
["Shows current and needed XP, percentage and rested bonus while hovering over the experience bar."] = nil,
["Shows currently equipped items on tooltips while the shift key is pressed."] = nil,
["Shows the vendor sell values on all item tooltips."] = nil,
@@ -101,6 +101,26 @@ ShaguTweaks_translation["enUS"] = {
["Clear"] = nil,
["No loot rolls yet."] = nil,
["Loot History requires ClassicAPI."] = nil,
["Enemy Castbar"] = nil,
["Movable Cast Bar"] = nil,
["Hold <Ctrl>, <Shift> and <Alt> to move the cast bar; right-click to reset."] = nil,
["Cast Bar"] = nil,
["Loss of Control Alert"] = nil,
["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."] = nil,
["Stunned"] = nil,
["Feared"] = nil,
["Confused"] = nil,
["Charmed"] = nil,
["Possessed"] = nil,
["Silenced"] = nil,
["Pacified"] = nil,
["Pacified and Silenced"] = nil,
["Disarmed"] = nil,
["Rooted"] = nil,
["Interrupted"] = nil,
["Drag to move, right-click to reset"] = nil,
["Totem Timers"] = nil,
["Shamans: shows the totems you have out as icons with the time left. Left-click targets a totem. Hold <Ctrl>, <Shift> and <Alt> to move them; right-click to reset."] = nil,
["Dragonflight Gryphons"] = nil,
["Replaces actionbar gryphons with the dragonflight version."] = nil,
["Action Bar"] = nil,
+136
View File
@@ -0,0 +1,136 @@
-- Movable Cast Bar
--
-- Hold Ctrl+Shift+Alt to show a marker over the player's cast bar and drag
-- it anywhere; right-click the marker to put it back. The position is saved
-- per character. The Swing Timer, Wand Timer and the Improved Castbar icon
-- are anchored to the cast bar, so they follow it.
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Movable Cast Bar"],
description = T["Hold <Ctrl>, <Shift> and <Alt> to move the cast bar; right-click to reset."],
expansions = { ["vanilla"] = true },
category = T["Action Bar"],
enabled = true,
})
local POSITION_KEY = "CastBarPosition"
-- One table for state: Lua 5.0 allows only 32 upvalues per function.
local S = { preview = false }
local function ModifiersDown()
local API = ShaguTweaks.API or {}
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
local function SavedPosition()
local pos = ShaguTweaks_config and ShaguTweaks_config[POSITION_KEY]
if pos and pos[1] and pos[2] then return pos end
end
-- Put the bar at the saved spot. Goes through the original SetPoint because
-- the wrapper below ignores everybody else while a saved spot exists.
local function Apply()
local pos = SavedPosition()
if not pos then return end
CastingBarFrame:ClearAllPoints()
S.orig(CastingBarFrame, "BOTTOMLEFT", UIParent, "BOTTOMLEFT", pos[1], pos[2])
end
-- Back to the stock spot; Reduced Actionbar re-anchors it on the next layout pass.
local function Reset()
if ShaguTweaks_config then ShaguTweaks_config[POSITION_KEY] = nil end
CastingBarFrame:ClearAllPoints()
S.orig(CastingBarFrame, "BOTTOM", UIParent, "BOTTOM", 0, 60)
if _G.UIParent_ManageFramePositions then _G.UIParent_ManageFramePositions() end
end
local function SetPreview(on)
if S.preview == on then return end
S.preview = on
if on then
S.mover:Show()
else
CastingBarFrame:StopMovingOrSizing()
S.mover:Hide()
end
end
local function CreateMover()
local m = CreateFrame("Frame", "ShaguTweaksCastBarMover", UIParent)
m:SetFrameStrata("DIALOG")
m:SetAllPoints(CastingBarFrame)
m:EnableMouse(true)
m:Hide()
m.bg = m:CreateTexture(nil, "BACKGROUND")
m.bg:SetTexture("Interface\\Buttons\\WHITE8X8")
m.bg:SetAllPoints(m)
m.bg:SetVertexColor(1, .7, 0, .6)
m.text = m:CreateFontString(nil, "OVERLAY")
m.text:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
m.text:SetPoint("CENTER", m, "CENTER", 0, 0)
m.text:SetText(T["Cast Bar"])
m:SetScript("OnMouseDown", function()
if arg1 == "LeftButton" then CastingBarFrame:StartMoving() end
end)
m:SetScript("OnMouseUp", function()
CastingBarFrame:StopMovingOrSizing()
ShaguTweaks_config = ShaguTweaks_config or {}
if arg1 == "RightButton" then
Reset()
else
ShaguTweaks_config[POSITION_KEY] = { CastingBarFrame:GetLeft(), CastingBarFrame:GetBottom() }
Apply()
end
end)
S.mover = m
end
module.enable = function(self)
if not CastingBarFrame then return end
if not S.mover then
S.orig = CastingBarFrame.SetPoint
CreateMover()
CastingBarFrame:SetMovable(true)
CastingBarFrame:SetClampedToScreen(true)
S.events = CreateFrame("Frame")
S.events:SetScript("OnEvent", function()
if event == "MODIFIER_STATE_CHANGED" then
SetPreview(ModifiersDown())
else
Apply()
end
end)
end
-- Reduced Actionbar and others re-anchor the bar on every layout pass.
-- While a saved spot exists their SetPoint calls are ignored.
CastingBarFrame.SetPoint = function(frame, a, b, c, d, e)
if SavedPosition() then return end
return S.orig(frame, a, b, c, d, e)
end
S.events:RegisterEvent("PLAYER_ENTERING_WORLD")
S.events:RegisterEvent("MODIFIER_STATE_CHANGED")
Apply()
end
module.disable = function(self)
if not S.mover then return end
SetPreview(false)
S.events:UnregisterAllEvents()
CastingBarFrame.SetPoint = nil
end
+304
View File
@@ -0,0 +1,304 @@
-- 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 = 220
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 },
}
-- 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 Apply(data)
local f = S.frame
local color = COLORS[data.locType] or { 1, .2, .2 }
f.icon:SetTexture((data.iconTexture and data.iconTexture ~= "") and data.iconTexture or FALLBACK_ICON)
f.border:SetVertexColor(color[1], color[2], color[3])
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, (WIDTH - ICON - 8) * 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 function ModifiersDown()
local API = ShaguTweaks.API or {}
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
-- 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)
f.border:SetVertexColor(1, .2, .2)
f.bar:SetVertexColor(1, .2, .2)
f.bar:SetWidth(WIDTH - ICON - 8)
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 + 14)
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", 0, 0)
f.icon:SetTexCoord(.08, .92, .08, .92)
-- colored frame around the icon: a slightly larger texture behind it
f.border = f:CreateTexture(nil, "BACKGROUND")
f.border:SetTexture("Interface\\Buttons\\WHITE8X8")
f.border:SetPoint("TOPLEFT", f.icon, "TOPLEFT", -2, 2)
f.border:SetPoint("BOTTOMRIGHT", f.icon, "BOTTOMRIGHT", 2, -2)
f.title = f:CreateFontString(nil, "OVERLAY")
f.title:SetFont("Fonts\\FRIZQT__.TTF", 20, "OUTLINE")
f.title:SetPoint("TOPLEFT", f.icon, "TOPRIGHT", 10, -2)
f.title:SetJustifyH("LEFT")
f.title:SetTextColor(1, 1, 1)
f.detail = f:CreateFontString(nil, "OVERLAY")
f.detail:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
f.detail:SetPoint("TOPLEFT", f.title, "BOTTOMLEFT", 0, -2)
f.detail:SetJustifyH("LEFT")
f.detail:SetTextColor(.85, .85, .85)
f.time = f:CreateFontString(nil, "OVERLAY")
f.time:SetFont("Fonts\\FRIZQT__.TTF", 26, "OUTLINE")
f.time:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -2)
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\\Buttons\\WHITE8X8")
f.bar:SetHeight(4)
f.bar:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", ICON + 8, 0)
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)
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
+103 -4
View File
@@ -9,7 +9,7 @@ local SuperWoWChannelInfo = ShaguTweaks.UnitChannelInfo
local module = ShaguTweaks:register({
title = T["Enemy Castbars"],
description = T["Shows an enemy castbar on target unit frame."],
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,
@@ -116,7 +116,14 @@ local function QueryCast(unit)
end
end
local function UpdatePosition()
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()
@@ -135,13 +142,20 @@ local function UpdatePosition()
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 then return end
if castbar.positionY == y and castbar.positionX == x then return end
castbar.positionX = x
castbar.positionY = y
castbar:ClearAllPoints()
castbar:SetPoint("BOTTOM", TargetFrame, "BOTTOM", -12, y)
castbar:SetPoint("BOTTOM", TargetFrame, "BOTTOM", x, y)
end
local function HideCast()
@@ -219,6 +233,85 @@ local function RefreshCast()
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",
@@ -250,12 +343,18 @@ module.enable = function(self)
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
+294
View File
@@ -0,0 +1,294 @@
-- Totem Timers (Shaman)
--
-- One icon per active totem (Fire, Earth, Water, Air) with the time left.
-- Left-click targets the totem, hovering shows its tooltip. The data comes
-- from ClassicAPI's GetTotemInfo / GetTotemTimeLeft, and the display is
-- redrawn on PLAYER_TOTEM_UPDATE, with a light timer only while a totem is up.
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Totem Timers"],
description = T["Shamans: shows the totems you have out as icons with the time left. Left-click targets a totem. Hold <Ctrl>, <Shift> and <Alt> to move them; right-click to reset."],
expansions = { ["vanilla"] = true },
category = T["Interface"],
enabled = false,
})
local SIZE = 34
local GAP = 4
local THROTTLE = .1
local POSITION_KEY = "TotemTimersPosition"
-- slot 1 Fire, 2 Earth, 3 Water, 4 Air
local SLOT_COLORS = {
{ 1, .45, .1 }, -- fire
{ .7, .55, .3 }, -- earth
{ .3, .6, 1 }, -- water
{ .8, .85, 1 }, -- air
}
local PREVIEW_ICONS = {
"Interface\\Icons\\Spell_Fire_SearingTotem",
"Interface\\Icons\\Spell_Nature_StrengthOfEarthTotem02",
"Interface\\Icons\\Spell_Nature_ManaRegenTotem",
"Interface\\Icons\\Spell_Nature_InvisibilityTotem",
}
-- One table for state: Lua 5.0 allows only 32 upvalues per function.
local S = { unlocked = false, elapsed = 0, buttons = {} }
local function Supported()
return type(_G.GetTotemInfo) == "function" and type(_G.GetTotemTimeLeft) == "function"
end
local function FormatTime(seconds)
if seconds >= 60 then
return string.format("%d:%02d", math.floor(seconds / 60), math.floor(seconds - math.floor(seconds / 60) * 60))
end
return string.format("%d", seconds)
end
local function Layout()
local shown = 0
for slot = 1, 4 do
local b = S.buttons[slot]
if b:IsShown() then
b:ClearAllPoints()
b:SetPoint("LEFT", S.frame, "LEFT", (slot - 1) * (SIZE + GAP), 0)
shown = shown + 1
end
end
return shown
end
-- Redraw the time and bar of the visible buttons; returns how many are up.
local function UpdateTimes()
local active = 0
for slot = 1, 4 do
local b = S.buttons[slot]
if b:IsShown() and not S.unlocked then
local left = _G.GetTotemTimeLeft(slot) or 0
if left > 0 then
active = active + 1
b.time:SetText(FormatTime(left))
if left <= 5 then
b.time:SetTextColor(1, .2, .2)
elseif left <= 10 then
b.time:SetTextColor(1, .9, .2)
else
b.time:SetTextColor(1, 1, 1)
end
if b.duration and b.duration > 0 then
b.bar:SetWidth(math.max(1, SIZE * math.min(left / b.duration, 1)))
end
else
-- duration ran out, the creature is about to go
b.time:SetText("")
b.bar:SetWidth(1)
end
end
end
return active
end
-- Re-read the four slots. Active totem = start time above 0; "have a totem"
-- is also true when you only carry the totem tool.
local function Refresh()
if S.unlocked then return end
local any = false
for slot = 1, 4 do
local b = S.buttons[slot]
local have, name, start, duration, icon = _G.GetTotemInfo(slot)
if start and start > 0 then
any = true
b.duration = duration
if icon and icon ~= "" then b.icon:SetTexture(icon) end
b:Show()
else
b.duration = nil
b:Hide()
end
end
if any then
Layout()
UpdateTimes()
if not S.frame:IsShown() then S.frame:Show() end
else
S.frame:Hide()
end
end
local function OnUpdate()
S.elapsed = S.elapsed + (arg1 or 0)
if S.elapsed < THROTTLE then return end
S.elapsed = 0
UpdateTimes()
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("BOTTOM", UIParent, "BOTTOM", 0, 200)
end
end
local function ModifiersDown()
local API = ShaguTweaks.API or {}
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
-- Hold Ctrl+Shift+Alt to show four preview icons and drag them.
local function SetUnlocked(on)
if S.unlocked == on then return end
S.unlocked = on
S.frame:EnableMouse(on)
if on then
for slot = 1, 4 do
local b = S.buttons[slot]
b:EnableMouse(false) -- let the drag reach the container
b.icon:SetTexture(PREVIEW_ICONS[slot])
b.time:SetText(FormatTime(60 - slot * 10))
b.time:SetTextColor(1, 1, 1)
b.bar:SetWidth(SIZE)
b:Show()
end
Layout()
S.frame:Show()
else
S.frame:StopMovingOrSizing()
for slot = 1, 4 do S.buttons[slot]:EnableMouse(true); S.buttons[slot]:Hide() end
S.frame:Hide()
Refresh()
end
end
local function CreateButton(slot)
local b = CreateFrame("Button", "ShaguTweaksTotem" .. slot, S.frame)
b:SetWidth(SIZE)
b:SetHeight(SIZE)
b:Hide()
b.border = b:CreateTexture(nil, "BACKGROUND")
b.border:SetTexture("Interface\\Buttons\\WHITE8X8")
b.border:SetPoint("TOPLEFT", b, "TOPLEFT", -1, 1)
b.border:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 1, -1)
b.border:SetVertexColor(SLOT_COLORS[slot][1], SLOT_COLORS[slot][2], SLOT_COLORS[slot][3])
b.icon = b:CreateTexture(nil, "ARTWORK")
b.icon:SetAllPoints(b)
b.icon:SetTexCoord(.08, .92, .08, .92)
b.time = b:CreateFontString(nil, "OVERLAY")
b.time:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")
b.time:SetPoint("CENTER", b, "CENTER", 0, 0)
b.time:SetTextColor(1, 1, 1)
-- remaining time as a thin bar under the icon
b.bar = b:CreateTexture(nil, "OVERLAY")
b.bar:SetTexture("Interface\\Buttons\\WHITE8X8")
b.bar:SetHeight(3)
b.bar:SetPoint("TOPLEFT", b, "BOTTOMLEFT", 0, -2)
b.bar:SetVertexColor(SLOT_COLORS[slot][1], SLOT_COLORS[slot][2], SLOT_COLORS[slot][3])
b:RegisterForClicks("LeftButtonUp")
b:SetScript("OnClick", function()
if S.unlocked then return end
if type(_G.TargetTotem) == "function" then _G.TargetTotem(slot) end
end)
b:SetScript("OnEnter", function()
if S.unlocked then return end
if GameTooltip.SetTotem then
-- shows itself, no :Show() needed
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:SetTotem(slot)
end
end)
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
S.buttons[slot] = b
end
local function CreateTimers()
if S.frame then return end
local f = CreateFrame("Frame", "ShaguTweaksTotemTimers", UIParent)
f:SetWidth(4 * SIZE + 3 * GAP)
f:SetHeight(SIZE)
f:SetFrameStrata("MEDIUM")
f:SetMovable(true)
f:SetClampedToScreen(true)
f:EnableMouse(false)
f:Hide()
S.frame = f
for slot = 1, 4 do CreateButton(slot) end
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 a totem is out (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)
RestorePosition()
end
module.enable = function(self)
local _, class = UnitClass("player")
if class ~= "SHAMAN" or not Supported() then return end
CreateTimers()
S.events:RegisterEvent("PLAYER_TOTEM_UPDATE")
S.events:RegisterEvent("PLAYER_ENTERING_WORLD")
S.events:RegisterEvent("MODIFIER_STATE_CHANGED")
SLASH_SHAGUTOTEMTIMERS1 = "/totemtimers"
SlashCmdList["SHAGUTOTEMTIMERS"] = 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