diff --git a/CHANGELOG.md b/CHANGELOG.md index c7427ba..58edb29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Docs/EXTRAS_MERGE.md b/Docs/EXTRAS_MERGE.md new file mode 100644 index 0000000..3b5c374 --- /dev/null +++ b/Docs/EXTRAS_MERGE.md @@ -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). | diff --git a/README.md b/README.md index 24b7573..055fef6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/ShaguTweaks.toc b/ShaguTweaks.toc index 1dec74e..0d9eddf 100644 --- a/ShaguTweaks.toc +++ b/ShaguTweaks.toc @@ -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 diff --git a/locales/translations_enUS.lua b/locales/translations_enUS.lua index 275258a..ab48f26 100644 --- a/locales/translations_enUS.lua +++ b/locales/translations_enUS.lua @@ -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 , and 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 , and 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 , and 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 , and to move them; right-click to reset."] = nil, ["Dragonflight Gryphons"] = nil, ["Replaces actionbar gryphons with the dragonflight version."] = nil, ["Action Bar"] = nil, diff --git a/mods/castbar-move.lua b/mods/castbar-move.lua new file mode 100644 index 0000000..4c523ce --- /dev/null +++ b/mods/castbar-move.lua @@ -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 , and 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 diff --git a/mods/loss-of-control.lua b/mods/loss-of-control.lua new file mode 100644 index 0000000..ac7284d --- /dev/null +++ b/mods/loss-of-control.lua @@ -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 , and 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 diff --git a/mods/target-castbar.lua b/mods/target-castbar.lua index 47588fa..5ec3956 100644 --- a/mods/target-castbar.lua +++ b/mods/target-castbar.lua @@ -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 , and 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 diff --git a/mods/totem-timers.lua b/mods/totem-timers.lua new file mode 100644 index 0000000..6c9f37f --- /dev/null +++ b/mods/totem-timers.lua @@ -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 , and 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