From 52f9051d5462de0e833e16111f3d614d34ccb2e8 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Thu, 19 Mar 2026 22:19:42 -0400 Subject: [PATCH 01/12] feat: innervate cooldown, spell reordering, and dead-state toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Innervate (180s) to Druid cooldowns in Data.lua - Right-click any tracker row to toggle dead state: desaturates icon and greys out the card to indicate the player has died; right-click again to restore (battle rez). Timer continues running. /cdt reset clears all dead states. - Spell reordering via Settings panel: each ability row now has ▲/▼ arrow buttons to move it up or down in the tracker. Order is persisted in the new CooldownTrackerDB.spellOrder SavedVariable. A Reset Order button restores the default sequence. --- AGENTS.md | 1 + Core.lua | 3 ++ Data.lua | 9 ++++ Settings.lua | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++- UI.lua | 93 ++++++++++++++++++++++++++++++++--- 5 files changed, 232 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a894f66..e7bec79 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,7 @@ Also update `## Version:` in `CooldownTracker.toc` to match the tag before taggi | `playSoundOnReady` | boolean | `true` | Play alert sound on cooldown expiry | | `frameLocked` | boolean | `false` | Lock window position (disable drag) | | `point`, `relPoint`, `x`, `y` | mixed | nil | Saved window position | + | `spellOrder` | table | `{}` | Ordered array of spell `id` strings defining tracker display order; empty = default | 3. **Slash Commands:** Register slash commands via the `SlashCmdList` table. Handle arguments cleanly. 4. **No Third-Party Libraries:** The addon intentionally does not use Ace3 or other framework libraries to remain lightweight. Rely on the standard WoW API. diff --git a/Core.lua b/Core.lua index 09be810..ef83adc 100755 --- a/Core.lua +++ b/Core.lua @@ -26,6 +26,7 @@ eventFrame:SetScript("OnEvent", function(_, event, name) CooldownTrackerDB = CooldownTrackerDB or {} CooldownTrackerDB.classCounts = CooldownTrackerDB.classCounts or {} CooldownTrackerDB.disabledSpells = CooldownTrackerDB.disabledSpells or {} + CooldownTrackerDB.spellOrder = CooldownTrackerDB.spellOrder or {} if CooldownTrackerDB.playSoundOnReady == nil then CooldownTrackerDB.playSoundOnReady = true end @@ -53,6 +54,8 @@ SlashCmdList["COOLDOWNTRACKER"] = function(msg) for _, cd in ipairs(CT.expandedCooldowns) do CT.activeTimers[cd.id] = nil end + CT.deadStates = {} + CT:UpdateAllRows() print("|cffaaddff[CooldownTracker]|r All timers reset.") elseif cmd == "settings" then CT:OpenSettings() diff --git a/Data.lua b/Data.lua index c84b194..017585a 100755 --- a/Data.lua +++ b/Data.lua @@ -35,6 +35,15 @@ CT.COOLDOWNS = { icon = "Interface\\Icons\\spell_nature_tranquility", r = 1.0, g = 0.49, b = 0.04, }, + { + id = "druid_innervate", + class = "Druid", + name = "Innervate", + duration = 180, + defaultDuration = 180, + icon = "Interface\\Icons\\spell_nature_lightning", + r = 1.0, g = 0.49, b = 0.04, + }, -- ------------------------------------------------------------------------- -- Paladin (class colour: pink) diff --git a/Settings.lua b/Settings.lua index d330b83..c5ab90a 100755 --- a/Settings.lua +++ b/Settings.lua @@ -29,6 +29,52 @@ local function ApplyCustomDurations() end end +-- Ensures spellOrder is fully populated from the default CT.COOLDOWNS order, +-- then moves the spell with the given id by `delta` positions (-1 = up, +1 = down). +-- Refreshes the tracker and updates the provided order-indicator labels table. +local function MoveSpell(id, delta, labels) + local order = CooldownTrackerDB.spellOrder + if not order then return end + + -- Populate from defaults if empty. + if #order == 0 then + for _, cd in ipairs(CT.COOLDOWNS) do + table.insert(order, cd.id) + end + end + + -- Ensure every known spell is represented (handles newly added spells). + local inOrder = {} + for _, sid in ipairs(order) do inOrder[sid] = true end + for _, cd in ipairs(CT.COOLDOWNS) do + if not inOrder[cd.id] then + table.insert(order, cd.id) + end + end + + local pos = nil + for i, sid in ipairs(order) do + if sid == id then pos = i; break end + end + if not pos then return end + + local newPos = pos + delta + if newPos < 1 or newPos > #order then return end + order[pos], order[newPos] = order[newPos], order[pos] + CooldownTrackerDB.spellOrder = order + + if CT.RebuildUI then CT:RebuildUI() end + + -- Refresh position indicators in the settings panel. + if labels then + local posMap = {} + for i, sid in ipairs(order) do posMap[sid] = i end + for spellId, lbl in pairs(labels) do + lbl:SetText(tostring(posMap[spellId] or "")) + end + end +end + -- --------------------------------------------------------------------------- -- Panel construction -- --------------------------------------------------------------------------- @@ -239,10 +285,32 @@ local function CreateSettingsPanel() divider:SetWidth(CONTENT_WIDTH) divider:SetColorTexture(0.3, 0.3, 0.4, 0.6) - -- editBoxes and RefreshAllEditBoxes must be declared here so the OnShow - -- closure below can reference them (Lua requires locals before use). + -- editBoxes, spellCheckboxes, and orderLabels must be declared here so the + -- OnShow closure below can reference them (Lua requires locals before use). local editBoxes = {} local spellCheckboxes = {} + local orderLabels = {} + + local function GetEffectiveOrderPos() + local order = CooldownTrackerDB.spellOrder or {} + local posMap = {} + if #order == 0 then + for i, cd in ipairs(CT.COOLDOWNS) do posMap[cd.id] = i end + else + for i, sid in ipairs(order) do posMap[sid] = i end + end + return posMap + end + + local function RefreshOrderIndicators() + C_Timer.After(0, function() + local posMap = GetEffectiveOrderPos() + for spellId, lbl in pairs(orderLabels) do + lbl:SetText(tostring(posMap[spellId] or "")) + end + end) + end + local function RefreshAllEditBoxes() C_Timer.After(0, function() for _, cd in ipairs(CT.COOLDOWNS) do @@ -269,6 +337,7 @@ local function CreateSettingsPanel() cb:SetChecked(not disabled[spellId]) end RefreshAllEditBoxes() + RefreshOrderIndicators() end) end) @@ -412,6 +481,50 @@ local function CreateSettingsPanel() GameTooltip:Show() end) editBox:SetScript("OnLeave", function() GameTooltip:Hide() end) + + -- ----- Reorder up/down buttons ---------------------------------------- + local orderLabel = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + orderLabel:SetSize(20, 14) + orderLabel:SetPoint("TOP", row, "TOPRIGHT", -24, -3) + orderLabel:SetJustifyH("CENTER") + orderLabel:SetTextColor(0.6, 0.6, 0.6) + orderLabels[cd.id] = orderLabel + + local upBtn = CreateFrame("Button", nil, row) + upBtn:SetSize(22, 15) + upBtn:SetPoint("TOP", row, "TOPRIGHT", -24, -16) + local upTex = upBtn:CreateTexture(nil, "ARTWORK") + upTex:SetAllPoints(upBtn) + upTex:SetTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up") + upTex:SetTexCoord(0.15, 0.85, 0.15, 0.85) + upBtn:SetScript("OnClick", function() + MoveSpell(cd.id, -1, orderLabels) + end) + upBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(upBtn, "ANCHOR_RIGHT") + GameTooltip:SetText("Move Up") + GameTooltip:AddLine("Move this spell earlier in the tracker.", 0.8, 0.8, 0.8) + GameTooltip:Show() + end) + upBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) + + local downBtn = CreateFrame("Button", nil, row) + downBtn:SetSize(22, 15) + downBtn:SetPoint("BOTTOM", row, "BOTTOMRIGHT", -24, 16) + local downTex = downBtn:CreateTexture(nil, "ARTWORK") + downTex:SetAllPoints(downBtn) + downTex:SetTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up") + downTex:SetTexCoord(0.15, 0.85, 0.15, 0.85) + downBtn:SetScript("OnClick", function() + MoveSpell(cd.id, 1, orderLabels) + end) + downBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(downBtn, "ANCHOR_RIGHT") + GameTooltip:SetText("Move Down") + GameTooltip:AddLine("Move this spell later in the tracker.", 0.8, 0.8, 0.8) + GameTooltip:Show() + end) + downBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) end -- ----- Reset All button ------------------------------------------------- @@ -430,6 +543,25 @@ local function CreateSettingsPanel() print("|cffaaddff[CooldownTracker]|r All durations reset to defaults.") end) + -- ----- Reset Order button ----------------------------------------------- + local resetOrderBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + resetOrderBtn:SetSize(100, 26) + resetOrderBtn:SetPoint("LEFT", resetAllBtn, "RIGHT", 8, 0) + resetOrderBtn:SetText("Reset Order") + resetOrderBtn:SetScript("OnClick", function() + CooldownTrackerDB.spellOrder = {} + if CT.RebuildUI then CT:RebuildUI() end + RefreshOrderIndicators() + print("|cffaaddff[CooldownTracker]|r Spell order reset to defaults.") + end) + resetOrderBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(resetOrderBtn, "ANCHOR_TOP") + GameTooltip:SetText("Reset Spell Order") + GameTooltip:AddLine("Restores the default display order for all spells.", 0.8, 0.8, 0.8) + GameTooltip:Show() + end) + resetOrderBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) + return panel end diff --git a/UI.lua b/UI.lua index 6982fcd..cab6129 100755 --- a/UI.lua +++ b/UI.lua @@ -10,6 +10,11 @@ local AddonName, CT = ... +-- Runtime dead-state table: keyed by cd.id, true = player is dead. +-- Not persisted (deaths are per-session only). Initialised here so +-- UpdateRow can reference it before CT:BuildUI() is called. +CT.deadStates = {} + -- --------------------------------------------------------------------------- -- Layout constants -- --------------------------------------------------------------------------- @@ -122,6 +127,24 @@ local function ApplyCardLayout(row, cW, cH) row.isWide = false end +-- --------------------------------------------------------------------------- +-- Dead-state visual helpers +-- --------------------------------------------------------------------------- +local function ApplyDeadVisuals(row) + row.iconTex:SetDesaturated(true) + row.iconTex:SetAlpha(0.35) + row.strip:SetColorTexture(0.3, 0.3, 0.3, 0.9) + row.bg:SetColorTexture(0.15, 0.15, 0.15, 0.45) +end + +local function ClearDeadVisuals(row) + local cd = row.cd + row.iconTex:SetDesaturated(false) + row.iconTex:SetAlpha(1) + row.strip:SetColorTexture(cd.r, cd.g, cd.b, 0.9) + row.bg:SetColorTexture(0, 0, 0, 0.20) +end + -- --------------------------------------------------------------------------- -- Row update (called every OnUpdate tick) -- --------------------------------------------------------------------------- @@ -138,7 +161,9 @@ local function UpdateRow(row, now) row.barFill:SetWidth(row.bar:GetWidth()) row.barFill:SetVertexColor(0.2, 0.9, 0.2) end - row.iconTex:SetAlpha(1) + if not CT.deadStates[cd.id] then + row.iconTex:SetAlpha(1) + end if CooldownTrackerDB.playSoundOnReady ~= false then PlaySound(SOUNDKIT.ALARM_CLOCK_WARNING_3) end @@ -160,6 +185,11 @@ local function UpdateRow(row, now) row.barFill:SetVertexColor(0.2, 0.9, 0.2) end end + + -- Re-apply dead visuals each tick so they survive timer transitions. + if CT.deadStates[cd.id] then + ApplyDeadVisuals(row) + end end -- --------------------------------------------------------------------------- @@ -168,7 +198,7 @@ end local function CreateRow(parent, cd) local row = CreateFrame("Button", nil, parent) row.cd = cd - row:RegisterForClicks("LeftButtonUp") + row:RegisterForClicks("LeftButtonUp", "RightButtonUp") local bg = row:CreateTexture(nil, "BACKGROUND") bg:SetAllPoints(row) @@ -213,14 +243,24 @@ local function CreateRow(parent, cd) btn:Hide() row.button = btn - row:SetScript("OnClick", function() + row:SetScript("OnClick", function(self, button) local mycd = row.cd - if CT.activeTimers[mycd.id] then - CT.activeTimers[mycd.id] = nil + if button == "RightButton" then + if CT.deadStates[mycd.id] then + CT.deadStates[mycd.id] = nil + ClearDeadVisuals(row) + else + CT.deadStates[mycd.id] = true + ApplyDeadVisuals(row) + end else - CT.activeTimers[mycd.id] = GetTime() + mycd.duration + if CT.activeTimers[mycd.id] then + CT.activeTimers[mycd.id] = nil + else + CT.activeTimers[mycd.id] = GetTime() + mycd.duration + end + UpdateRow(row, GetTime()) end - UpdateRow(row, GetTime()) end) row:SetScript("OnEnter", function() @@ -244,6 +284,11 @@ local function CreateRow(parent, cd) else GameTooltip:AddLine("Click to start the cooldown timer.", 1, 0.8, 0) end + if CT.deadStates[mycd.id] then + GameTooltip:AddLine("|cffaaaaaa[Dead]|r Right-click to mark |cff00ff00alive|r (battle rez).", 0.8, 0.8, 0.8) + else + GameTooltip:AddLine("Right-click to mark |cffff4040dead|r.", 0.8, 0.8, 0.8) + end GameTooltip:Show() end) row:SetScript("OnLeave", function() @@ -260,12 +305,40 @@ end -- Populates CT.expandedCooldowns from CT.COOLDOWNS + class counts. -- count=1: original entry unchanged (no "#N" suffix) -- count>1: N copies with unique IDs and "#N" appended to name +-- Applies CooldownTrackerDB.spellOrder to sort the base spell list before +-- expansion. Spells absent from the saved order appear at the end in their +-- default order. -- --------------------------------------------------------------------------- function CT:BuildExpandedCooldowns() local counts = CooldownTrackerDB.classCounts or {} local disabled = CooldownTrackerDB.disabledSpells or {} - CT.expandedCooldowns = {} + local order = CooldownTrackerDB.spellOrder or {} + + -- Build a position lookup from the saved order array. + local orderPos = {} + for pos, id in ipairs(order) do + orderPos[id] = pos + end + + -- Sort a copy of CT.COOLDOWNS according to saved order. + local sorted = {} for _, cd in ipairs(CT.COOLDOWNS) do + table.insert(sorted, cd) + end + table.sort(sorted, function(a, b) + local pa = orderPos[a.id] or (1000 + #sorted) + local pb = orderPos[b.id] or (1000 + #sorted) + if pa ~= pb then return pa < pb end + -- Preserve relative default order for ties (spells not in saved order). + for _, cd in ipairs(CT.COOLDOWNS) do + if cd.id == a.id then return true end + if cd.id == b.id then return false end + end + return false + end) + + CT.expandedCooldowns = {} + for _, cd in ipairs(sorted) do if not disabled[cd.id] then local count = math.max(1, math.min(5, counts[cd.class] or 1)) if count == 1 then @@ -305,6 +378,10 @@ function CT:RebuildUI() local row = CT.pool[i] row:Show() row.cd = cd + -- Reset any lingering dead visuals before applying the new cd. + row.iconTex:SetDesaturated(false) + row.iconTex:SetAlpha(1) + row.bg:SetColorTexture(0, 0, 0, 0.20) row.strip:SetColorTexture(cd.r, cd.g, cd.b, 0.9) row.iconTex:SetTexture(cd.icon) row.nameLabel:SetText(cd.name) From a6ffed8bd5e9d6c222a7c33c924ce725ab3a20ca Mon Sep 17 00:00:00 2001 From: RubyJ Date: Thu, 19 Mar 2026 22:21:38 -0400 Subject: [PATCH 02/12] fix: sort comparator strict ordering and stale spellOrder pruning - Guard sort comparator against comparing an element with itself (a.id == b.id returns false immediately), fixing potential 'invalid order function' runtime error in Lua 5.1 when spellOrder is empty. - Prune stale spell IDs from spellOrder during BuildExpandedCooldowns so ghost entries from renamed/removed spells can never silently consume swap positions. --- UI.lua | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/UI.lua b/UI.lua index cab6129..dd02c73 100755 --- a/UI.lua +++ b/UI.lua @@ -314,10 +314,18 @@ function CT:BuildExpandedCooldowns() local disabled = CooldownTrackerDB.disabledSpells or {} local order = CooldownTrackerDB.spellOrder or {} - -- Build a position lookup from the saved order array. + -- Build a set of valid spell IDs so we can prune stale saved-order entries. + local knownIds = {} + for _, cd in ipairs(CT.COOLDOWNS) do knownIds[cd.id] = true end + + -- Build a position lookup from the saved order array, ignoring stale IDs. local orderPos = {} - for pos, id in ipairs(order) do - orderPos[id] = pos + local cleanPos = 1 + for _, id in ipairs(order) do + if knownIds[id] then + orderPos[id] = cleanPos + cleanPos = cleanPos + 1 + end end -- Sort a copy of CT.COOLDOWNS according to saved order. @@ -329,6 +337,7 @@ function CT:BuildExpandedCooldowns() local pa = orderPos[a.id] or (1000 + #sorted) local pb = orderPos[b.id] or (1000 + #sorted) if pa ~= pb then return pa < pb end + if a.id == b.id then return false end -- Preserve relative default order for ties (spells not in saved order). for _, cd in ipairs(CT.COOLDOWNS) do if cd.id == a.id then return true end From 126da2589709f40605f7e05437df87b79a6acbc1 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Fri, 20 Mar 2026 02:39:19 -0400 Subject: [PATCH 03/12] fix: replace cramped scroll-bar arrow textures with UIPanelButton arrows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorder buttons were 22x15px scroll-bar textures with overlapping anchors inside a 42px row. Replaced with UIPanelButtonTemplate buttons (24x18px each, 2px gap) anchored correctly so they stack cleanly without overlap. Switched arrow glyphs to unicode ▲/▼ which are far more legible at this size. --- Settings.lua | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/Settings.lua b/Settings.lua index c5ab90a..c2ba303 100755 --- a/Settings.lua +++ b/Settings.lua @@ -484,19 +484,17 @@ local function CreateSettingsPanel() -- ----- Reorder up/down buttons ---------------------------------------- local orderLabel = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") - orderLabel:SetSize(20, 14) - orderLabel:SetPoint("TOP", row, "TOPRIGHT", -24, -3) + orderLabel:SetSize(24, 12) + orderLabel:SetPoint("TOPRIGHT", row, "TOPRIGHT", -4, -2) orderLabel:SetJustifyH("CENTER") - orderLabel:SetTextColor(0.6, 0.6, 0.6) + orderLabel:SetTextColor(0.5, 0.5, 0.5) orderLabels[cd.id] = orderLabel - local upBtn = CreateFrame("Button", nil, row) - upBtn:SetSize(22, 15) - upBtn:SetPoint("TOP", row, "TOPRIGHT", -24, -16) - local upTex = upBtn:CreateTexture(nil, "ARTWORK") - upTex:SetAllPoints(upBtn) - upTex:SetTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up") - upTex:SetTexCoord(0.15, 0.85, 0.15, 0.85) + local upBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") + upBtn:SetSize(24, 18) + upBtn:SetPoint("TOPRIGHT", row, "TOPRIGHT", -4, -13) + upBtn:SetText("\226\150\178") -- ▲ + upBtn:GetFontString():SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE") upBtn:SetScript("OnClick", function() MoveSpell(cd.id, -1, orderLabels) end) @@ -508,13 +506,11 @@ local function CreateSettingsPanel() end) upBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) - local downBtn = CreateFrame("Button", nil, row) - downBtn:SetSize(22, 15) - downBtn:SetPoint("BOTTOM", row, "BOTTOMRIGHT", -24, 16) - local downTex = downBtn:CreateTexture(nil, "ARTWORK") - downTex:SetAllPoints(downBtn) - downTex:SetTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up") - downTex:SetTexCoord(0.15, 0.85, 0.15, 0.85) + local downBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") + downBtn:SetSize(24, 18) + downBtn:SetPoint("TOP", upBtn, "BOTTOM", 0, -2) + downBtn:SetText("\226\150\188") -- ▼ + downBtn:GetFontString():SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE") downBtn:SetScript("OnClick", function() MoveSpell(cd.id, 1, orderLabels) end) From d5ec044b088774e6054939a34a8df69d0dc73fd2 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Fri, 20 Mar 2026 02:43:13 -0400 Subject: [PATCH 04/12] fix: use ASCII glyphs and center reorder buttons vertically in row FRIZQT__.TTF lacks the Unicode geometric arrow codepoints, causing them to render as boxes. Replaced with ASCII ^ and v which the font supports. Anchored both buttons from the row RIGHT with +/-10 Y offsets so the 18+2+18px pair is centered in the 42px row instead of sitting at the top. --- Settings.lua | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Settings.lua b/Settings.lua index c2ba303..a6d0f8f 100755 --- a/Settings.lua +++ b/Settings.lua @@ -483,18 +483,11 @@ local function CreateSettingsPanel() editBox:SetScript("OnLeave", function() GameTooltip:Hide() end) -- ----- Reorder up/down buttons ---------------------------------------- - local orderLabel = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") - orderLabel:SetSize(24, 12) - orderLabel:SetPoint("TOPRIGHT", row, "TOPRIGHT", -4, -2) - orderLabel:SetJustifyH("CENTER") - orderLabel:SetTextColor(0.5, 0.5, 0.5) - orderLabels[cd.id] = orderLabel - local upBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") upBtn:SetSize(24, 18) - upBtn:SetPoint("TOPRIGHT", row, "TOPRIGHT", -4, -13) - upBtn:SetText("\226\150\178") -- ▲ - upBtn:GetFontString():SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE") + upBtn:SetPoint("RIGHT", row, "RIGHT", -14, 10) + upBtn:SetText("^") + upBtn:GetFontString():SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") upBtn:SetScript("OnClick", function() MoveSpell(cd.id, -1, orderLabels) end) @@ -508,9 +501,9 @@ local function CreateSettingsPanel() local downBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") downBtn:SetSize(24, 18) - downBtn:SetPoint("TOP", upBtn, "BOTTOM", 0, -2) - downBtn:SetText("\226\150\188") -- ▼ - downBtn:GetFontString():SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE") + downBtn:SetPoint("RIGHT", row, "RIGHT", -14, -10) + downBtn:SetText("v") + downBtn:GetFontString():SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") downBtn:SetScript("OnClick", function() MoveSpell(cd.id, 1, orderLabels) end) @@ -521,6 +514,13 @@ local function CreateSettingsPanel() GameTooltip:Show() end) downBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) + + local orderLabel = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + orderLabel:SetSize(18, 14) + orderLabel:SetPoint("RIGHT", upBtn, "LEFT", -2, 0) + orderLabel:SetJustifyH("CENTER") + orderLabel:SetTextColor(0.5, 0.5, 0.5) + orderLabels[cd.id] = orderLabel end -- ----- Reset All button ------------------------------------------------- From 28c8cce19804c9265bdf9aca28a7adccf0698194 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Fri, 20 Mar 2026 02:47:44 -0400 Subject: [PATCH 05/12] fix: use native WoW arrow textures for reorder buttons Replaced mismatched font glyphs (^ vs v render at different heights in FRIZQT) with inline |T...|t texture escapes pointing at the same Arrow-Up and Arrow-Down assets Blizzard uses in their own UI. Both arrows render at 12x12px so they are guaranteed identical in size. --- Settings.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Settings.lua b/Settings.lua index a6d0f8f..2d0340d 100755 --- a/Settings.lua +++ b/Settings.lua @@ -486,8 +486,7 @@ local function CreateSettingsPanel() local upBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") upBtn:SetSize(24, 18) upBtn:SetPoint("RIGHT", row, "RIGHT", -14, 10) - upBtn:SetText("^") - upBtn:GetFontString():SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") + upBtn:SetText("|TInterface\\Buttons\\Arrow-Up-Up:12:12|t") upBtn:SetScript("OnClick", function() MoveSpell(cd.id, -1, orderLabels) end) @@ -502,8 +501,7 @@ local function CreateSettingsPanel() local downBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") downBtn:SetSize(24, 18) downBtn:SetPoint("RIGHT", row, "RIGHT", -14, -10) - downBtn:SetText("v") - downBtn:GetFontString():SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") + downBtn:SetText("|TInterface\\Buttons\\Arrow-Down-Up:12:12|t") downBtn:SetScript("OnClick", function() MoveSpell(cd.id, 1, orderLabels) end) From fd7f7a9d57df3a26701f2eeec18da3b6aa1b49e7 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Fri, 20 Mar 2026 02:50:51 -0400 Subject: [PATCH 06/12] fix: center arrow textures in reorder buttons using child overlay Inline |T|t text can't be offset, so asymmetric padding in the native arrow textures made the up arrow hug the bottom and down arrow hug the top. Replaced with explicit child OVERLAY textures anchored at CENTER with +/-1 Y nudges to counteract the texture's internal whitespace. --- Settings.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Settings.lua b/Settings.lua index 2d0340d..67d7bc6 100755 --- a/Settings.lua +++ b/Settings.lua @@ -486,7 +486,11 @@ local function CreateSettingsPanel() local upBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") upBtn:SetSize(24, 18) upBtn:SetPoint("RIGHT", row, "RIGHT", -14, 10) - upBtn:SetText("|TInterface\\Buttons\\Arrow-Up-Up:12:12|t") + upBtn:SetText("") + local upArrow = upBtn:CreateTexture(nil, "OVERLAY") + upArrow:SetSize(12, 12) + upArrow:SetPoint("CENTER", 0, 1) + upArrow:SetTexture("Interface\\Buttons\\Arrow-Up-Up") upBtn:SetScript("OnClick", function() MoveSpell(cd.id, -1, orderLabels) end) @@ -501,7 +505,11 @@ local function CreateSettingsPanel() local downBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") downBtn:SetSize(24, 18) downBtn:SetPoint("RIGHT", row, "RIGHT", -14, -10) - downBtn:SetText("|TInterface\\Buttons\\Arrow-Down-Up:12:12|t") + downBtn:SetText("") + local downArrow = downBtn:CreateTexture(nil, "OVERLAY") + downArrow:SetSize(12, 12) + downArrow:SetPoint("CENTER", 0, -1) + downArrow:SetTexture("Interface\\Buttons\\Arrow-Down-Up") downBtn:SetScript("OnClick", function() MoveSpell(cd.id, 1, orderLabels) end) From 30350dba7b3027e8c478ec4dffcde7b0dbcb8c5d Mon Sep 17 00:00:00 2001 From: RubyJ Date: Fri, 20 Mar 2026 02:53:36 -0400 Subject: [PATCH 07/12] fix: increase down arrow Y nudge to -3 for visual centering --- Settings.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Settings.lua b/Settings.lua index 67d7bc6..7333e50 100755 --- a/Settings.lua +++ b/Settings.lua @@ -508,7 +508,7 @@ local function CreateSettingsPanel() downBtn:SetText("") local downArrow = downBtn:CreateTexture(nil, "OVERLAY") downArrow:SetSize(12, 12) - downArrow:SetPoint("CENTER", 0, -1) + downArrow:SetPoint("CENTER", 0, -3) downArrow:SetTexture("Interface\\Buttons\\Arrow-Down-Up") downBtn:SetScript("OnClick", function() MoveSpell(cd.id, 1, orderLabels) From 2cacb1d61ed17fe91205bc87cc21af40404414c3 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Fri, 20 Mar 2026 02:56:50 -0400 Subject: [PATCH 08/12] fix: grey out timer and name text when player is marked dead ApplyDeadVisuals now sets timerLabel, nameLabel, and classLabel to grey (0.5, 0.5, 0.5) alongside the icon/strip/bg. ClearDeadVisuals restores nameLabel to white and classLabel to its class colour. timerLabel is restored implicitly by the UpdateRow tick which rewrites it each frame. --- UI.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/UI.lua b/UI.lua index dd02c73..0c6e478 100755 --- a/UI.lua +++ b/UI.lua @@ -135,6 +135,9 @@ local function ApplyDeadVisuals(row) row.iconTex:SetAlpha(0.35) row.strip:SetColorTexture(0.3, 0.3, 0.3, 0.9) row.bg:SetColorTexture(0.15, 0.15, 0.15, 0.45) + row.timerLabel:SetTextColor(0.5, 0.5, 0.5) + row.nameLabel:SetTextColor(0.5, 0.5, 0.5) + row.classLabel:SetTextColor(0.5, 0.5, 0.5) end local function ClearDeadVisuals(row) @@ -143,6 +146,8 @@ local function ClearDeadVisuals(row) row.iconTex:SetAlpha(1) row.strip:SetColorTexture(cd.r, cd.g, cd.b, 0.9) row.bg:SetColorTexture(0, 0, 0, 0.20) + row.nameLabel:SetTextColor(1, 1, 1) + row.classLabel:SetTextColor(cd.r, cd.g, cd.b) end -- --------------------------------------------------------------------------- From ee37989cf22885e501b41fd60807da95ca682637 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Fri, 20 Mar 2026 03:01:11 -0400 Subject: [PATCH 09/12] fix: preserve active timers across RebuildUI calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing CT.activeTimers = {} from RebuildUI() was wiping all running timers every time a settings change (column count, class count, spell visibility, reorder) triggered a rebuild. Timers are now only cleared explicitly by the /cdt reset slash command, which is the only caller that actually intends to reset them. Orphaned entries from ID changes (e.g. count 1→2) are harmless -- UpdateRow ignores keys with no matching row. --- UI.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/UI.lua b/UI.lua index 0c6e478..652ca0d 100755 --- a/UI.lua +++ b/UI.lua @@ -377,7 +377,6 @@ end -- CreateFrame calls at runtime, which avoids WoW taint errors. -- --------------------------------------------------------------------------- function CT:RebuildUI() - CT.activeTimers = {} CT:BuildExpandedCooldowns() -- Grow the pool if needed (only at first expansion — safe during ADDON_LOADED-like context) From beb70258da977ce10dab2eb4417dc8ad0e45ad92 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Fri, 20 Mar 2026 03:07:26 -0400 Subject: [PATCH 10/12] fix: use grey color codes in timer text for dead-state rows SetTextColor() is overridden by embedded |cff...|r escape sequences in the text string. UpdateRow now reads CT.deadStates each tick and injects |cff808080 instead of the normal green/orange codes, so the grey sticks regardless of timer state. Works whether dead is marked before or after the timer starts since UpdateRow fires every frame. --- UI.lua | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/UI.lua b/UI.lua index 652ca0d..4fdf9f0 100755 --- a/UI.lua +++ b/UI.lua @@ -135,7 +135,6 @@ local function ApplyDeadVisuals(row) row.iconTex:SetAlpha(0.35) row.strip:SetColorTexture(0.3, 0.3, 0.3, 0.9) row.bg:SetColorTexture(0.15, 0.15, 0.15, 0.45) - row.timerLabel:SetTextColor(0.5, 0.5, 0.5) row.nameLabel:SetTextColor(0.5, 0.5, 0.5) row.classLabel:SetTextColor(0.5, 0.5, 0.5) end @@ -155,18 +154,19 @@ end -- --------------------------------------------------------------------------- local function UpdateRow(row, now) local cd = row.cd + local dead = CT.deadStates[cd.id] local endTime = CT.activeTimers[cd.id] if endTime then local remaining = endTime - now if remaining <= 0 then CT.activeTimers[cd.id] = nil - row.timerLabel:SetText("|cff00ff00Ready|r") + row.timerLabel:SetText(dead and "|cff808080Ready|r" or "|cff00ff00Ready|r") if row.isWide then row.barFill:SetWidth(row.bar:GetWidth()) row.barFill:SetVertexColor(0.2, 0.9, 0.2) end - if not CT.deadStates[cd.id] then + if not dead then row.iconTex:SetAlpha(1) end if CooldownTrackerDB.playSoundOnReady ~= false then @@ -181,10 +181,11 @@ local function UpdateRow(row, now) elseif frac < 0.5 then row.barFill:SetVertexColor(0.9, 0.7, 0.1) else row.barFill:SetVertexColor(cd.r, cd.g, cd.b) end end - row.timerLabel:SetText("|cffff8040" .. FormatTime(remaining) .. "|r") + local timeColor = dead and "|cff808080" or "|cffff8040" + row.timerLabel:SetText(timeColor .. FormatTime(remaining) .. "|r") end else - row.timerLabel:SetText("|cff00ff00Ready|r") + row.timerLabel:SetText(dead and "|cff808080Ready|r" or "|cff00ff00Ready|r") if row.isWide then row.barFill:SetWidth(row.bar:GetWidth()) row.barFill:SetVertexColor(0.2, 0.9, 0.2) From 9b733816a22c8cd5411ba9852d64b2472204a9da Mon Sep 17 00:00:00 2001 From: RubyJ Date: Fri, 20 Mar 2026 03:12:16 -0400 Subject: [PATCH 11/12] docs: update README for innervate, reorder, and dead-state features --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 540abb6..e23a60a 100755 --- a/README.md +++ b/README.md @@ -7,14 +7,17 @@ Because Midnight restricts addons from reading real-time combat data, this addon ## Features - Click anywhere on a spell row to start or reset its timer +- Right-click a spell row to mark a player as dead (row goes grey); right-click again to restore (battle rez) - Countdown display (`M:SS`) with colour-coded progress bar (green → yellow → red) - Audible alert when a cooldown becomes ready (toggleable) - Grid or vertical layout with configurable column count (1–9) +- Reorderable spells — use ▲/▼ arrows in the settings panel to set any display order - Per-spell visibility toggles — hide abilities you're not tracking - Lockable window — prevent accidental dragging mid-raid - Draggable window with position saved between sessions - In-game settings panel (Escape → Options → AddOns → Healer Cooldown Tracker) - Tooltips showing cooldown details on hover +- Tracks Druid cooldowns: Convoke the Spirits, Tranquility, Innervate ## Installation @@ -42,6 +45,7 @@ Clone the repo and symlink (or copy) the folder directly into your AddOns direct - Click any spell row to start the cooldown timer - Click the same row again to reset a running timer +- Right-click any spell row to mark the player as **dead** (row goes grey, timer keeps running); right-click again to restore (battle rez) - Drag the title bar to reposition; position saves on drag-stop - Click the **lock icon** (top-right of title bar) to lock/unlock the window position @@ -53,6 +57,7 @@ Open via **Escape → Options → AddOns → Healer Cooldown Tracker**: - **Play sound when cooldown is ready** — toggle the audible alert - **Class Roster** — set how many of each class are in the raid; abilities duplicate per player (up to 5) - **Show checkboxes** — hide individual spells from the tracker +- **Spell order** — use ▲/▼ arrows next to each spell to reorder them; Reset Order restores the default sequence - **Cooldown durations** — override any ability's cooldown in seconds; revert with the Default button ## Adding More Cooldowns From ed22bd0b8ef77bb41211445c7bf0277aad6dbb11 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Fri, 20 Mar 2026 03:15:42 -0400 Subject: [PATCH 12/12] fix: prune stale spellOrder IDs in MoveSpell and drop RebuildUI timer flash - MoveSpell now rebuilds the order array by removing any IDs not in CT.COOLDOWNS before searching for the swap position, matching the knownIds pruning already present in BuildExpandedCooldowns. Previously a ghost entry could silently consume a swap slot. - RebuildUI no longer unconditionally overwrites timerLabel with 'Ready'. UpdateRow handles the correct text (respecting active timers and dead state) on the next OnUpdate tick, eliminating the one-frame green flash when reordering while a timer is running. --- Settings.lua | 19 ++++++++++++++----- UI.lua | 4 +++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Settings.lua b/Settings.lua index 7333e50..e3f128a 100755 --- a/Settings.lua +++ b/Settings.lua @@ -43,14 +43,23 @@ local function MoveSpell(id, delta, labels) end end - -- Ensure every known spell is represented (handles newly added spells). - local inOrder = {} - for _, sid in ipairs(order) do inOrder[sid] = true end + -- Build known-ID set, then rebuild order: remove stale IDs and append any missing ones. + local knownIds = {} + for _, cd in ipairs(CT.COOLDOWNS) do knownIds[cd.id] = true end + + local pruned = {} + for _, sid in ipairs(order) do + if knownIds[sid] then table.insert(pruned, sid) end + end for _, cd in ipairs(CT.COOLDOWNS) do - if not inOrder[cd.id] then - table.insert(order, cd.id) + local found = false + for _, sid in ipairs(pruned) do + if sid == cd.id then found = true; break end end + if not found then table.insert(pruned, cd.id) end end + order = pruned + CooldownTrackerDB.spellOrder = order local pos = nil for i, sid in ipairs(order) do diff --git a/UI.lua b/UI.lua index 4fdf9f0..5ab2103 100755 --- a/UI.lua +++ b/UI.lua @@ -402,7 +402,9 @@ function CT:RebuildUI() row.nameLabel:SetTextColor(1, 1, 1) row.classLabel:SetText(cd.class) row.classLabel:SetTextColor(cd.r, cd.g, cd.b) - row.timerLabel:SetText("|cff00ff00Ready|r") + -- timerLabel text is intentionally not set here: UpdateRow will + -- write the correct text (with active-timer or dead-state colors) + -- on the very next OnUpdate tick, avoiding a one-frame "Ready" flash. row.barFill:SetVertexColor(cd.r, cd.g, cd.b) end