Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions AGENTS.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ The addon is split into modular components, sharing a single private namespace (
local AddonName, CT = ...
-- CT is the shared table across all files
```
2. **SavedVariables:** Use `CooldownTrackerDB` for persistence. Initialize it in `ADDON_LOADED` in `Core.lua`.
2. **SavedVariables:** Use `CooldownTrackerDB` for persistence. Initialize it in `ADDON_LOADED` in `Core.lua`. Current keys:
| Key | Type | Default | Purpose |
|---|---|---|---|
| `columns` | number | `1` | Grid column count (1–9) |
| `classCounts` | table | `{}` | Per-class player counts (1–5) |
| `customDurations` | table | `{}` | Per-spell duration overrides (seconds) |
| `disabledSpells` | table | `{}` | Set of spell IDs hidden from tracker |
| `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 |
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.

Expand All @@ -34,5 +43,7 @@ World of Warcraft has a strict "taint" system for UI frames. Violating these rul

## 🎨 UI & Layout Preferences
- **Grid vs Vertical:** The UI supports both a wide-row vertical layout (columns = 1) and a compact square-card grid layout (columns 2-9). Use `CT:LayoutRows()` to reflow.
- **Clickable Rows:** Each spell row is a `Button` frame (not a plain `Frame`). The entire row is the click target — there is no separate "Used"/"Reset" button. `OnClick` is set once at creation time and reads `row.cd` dynamically to avoid taint.
- **Font:** Use `Fonts\\FRIZQT__.TTF` with an `"OUTLINE"` flag for clear, readable text in the tracker.
- **Backdrops:** Use the `BackdropTemplate` mixin for frames requiring backgrounds/borders.
- **Backdrops:** Use the `BackdropTemplate` mixin for frames requiring backgrounds/borders. Main frame backdrop alpha is `0.55` (translucent); title bar is `0.65`.
- **Title Bar Controls:** The title bar contains a close button (`UIPanelCloseButton`, 18px) and a lock toggle button (plain `Button`, 32px) anchored to its left. The lock button uses `Interface\\BUTTONS\\LockButton-Locked-Up` / `LockButton-Unlocked-Up` textures and toggles `CooldownTrackerDB.frameLocked`. Always guard `OnDragStart` with a `frameLocked` check to avoid a Lua error when the frame is not movable.
9 changes: 8 additions & 1 deletion Core.lua
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ eventFrame:SetScript("OnEvent", function(_, event, name)
if event == "ADDON_LOADED" and name == AddonName then
-- Initialise saved variables
CooldownTrackerDB = CooldownTrackerDB or {}
CooldownTrackerDB.classCounts = CooldownTrackerDB.classCounts or {}
CooldownTrackerDB.classCounts = CooldownTrackerDB.classCounts or {}
CooldownTrackerDB.disabledSpells = CooldownTrackerDB.disabledSpells or {}
if CooldownTrackerDB.playSoundOnReady == nil then
CooldownTrackerDB.playSoundOnReady = true
end
if CooldownTrackerDB.frameLocked == nil then
CooldownTrackerDB.frameLocked = false
end
-- Register the settings panel (applies saved durations to COOLDOWNS)
CT:InitSettings()
-- Expand cooldowns before building UI (so copies inherit saved durations)
Expand Down
75 changes: 64 additions & 11 deletions Data.lua
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,37 @@ CT.COOLDOWNS = {
r = 0.0, g = 0.44, b = 0.87,
},

-- -------------------------------------------------------------------------
-- Priest (class colour: white)
-- -------------------------------------------------------------------------
{
id = "priest_apotheosis",
class = "Priest",
name = "Apotheosis",
duration = 120,
defaultDuration = 120,
icon = "Interface\\Icons\\ability_priest_ascension",
r = 1.0, g = 1.0, b = 1.0,
},
{
id = "priest_divine_hymn",
class = "Priest",
name = "Divine Hymn",
duration = 180,
defaultDuration = 180,
icon = "Interface\\Icons\\spell_holy_divinehymn",
r = 1.0, g = 1.0, b = 1.0,
},
{
id = "priest_halo",
class = "Priest",
name = "Halo",
duration = 40,
defaultDuration = 40,
icon = "Interface\\Icons\\ability_priest_halo",
r = 1.0, g = 1.0, b = 1.0,
},

-- -------------------------------------------------------------------------
-- Warrior (class colour: tan/gold)
-- -------------------------------------------------------------------------
Expand All @@ -93,6 +124,32 @@ CT.COOLDOWNS = {
r = 0.78, g = 0.61, b = 0.23,
},

-- -------------------------------------------------------------------------
-- Death Knight (class colour: red)
-- -------------------------------------------------------------------------
{
id = "dk_amz",
class = "Death Knight",
name = "Anti-Magic Zone",
duration = 120,
defaultDuration = 120,
icon = "Interface\\Icons\\spell_deathknight_antimagiczone",
r = 0.77, g = 0.12, b = 0.23,
},

-- -------------------------------------------------------------------------
-- Demon Hunter (class colour: purple)
-- -------------------------------------------------------------------------
{
id = "dh_darkness",
class = "Demon Hunter",
name = "Darkness",
duration = 300,
defaultDuration = 300,
icon = "Interface\\Icons\\ability_demonhunter_darkness",
r = 0.64, g = 0.19, b = 0.79,
},

-- -------------------------------------------------------------------------
-- Evoker (class colour: teal)
-- -------------------------------------------------------------------------
Expand All @@ -105,17 +162,13 @@ CT.COOLDOWNS = {
icon = "Interface\\Icons\\ability_evoker_hoverblack",
r = 0.2, g = 0.58, b = 0.5,
},

-- -------------------------------------------------------------------------
-- Death Knight (class colour: red)
-- -------------------------------------------------------------------------
{
id = "dk_amz",
class = "Death Knight",
name = "Anti-Magic Zone",
duration = 120,
defaultDuration = 120,
icon = "Interface\\Icons\\spell_deathknight_antimagiczone",
r = 0.77, g = 0.12, b = 0.23,
id = "evoker_spatial_paradox",
class = "Evoker",
name = "Spatial Paradox",
duration = 180,
defaultDuration = 180,
icon = "Interface\\Icons\\ability_evoker_spatialparadox",
r = 0.2, g = 0.58, b = 0.5,
},
}
42 changes: 31 additions & 11 deletions README.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

A World of Warcraft **Midnight** addon for raid leaders to manually track healer cooldowns.

Because Midnight restricts addons from reading real-time combat data, this addon takes a manual approach: your healers call out on comms, you click a button, and the addon counts down the cooldown for you.
Because Midnight restricts addons from reading real-time combat data, this addon takes a manual approach: your healers call out on comms, you click a spell row, and the addon counts down the cooldown for you.

## Features

- One-click timer start/reset per ability
- Click anywhere on a spell row to start or reset its timer
- Countdown display (`M:SS`) with colour-coded progress bar (green → yellow → red)
- Audible alert when a cooldown becomes available
- Audible alert when a cooldown becomes ready (toggleable)
- Grid or vertical layout with configurable column count (1–9)
- 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

## Installation
Expand All @@ -26,23 +30,37 @@ Because Midnight restricts addons from reading real-time combat data, this addon
|---|---|
| `/cdt` | Toggle the tracker window |
| `/cdt reset` | Reset all running timers |
| `/cdt columns N` | Set grid columns (1–9) |
| `/cdt settings` | Print reminder to open settings manually |

- Click **Used** when a healer uses an ability → timer starts
- Click **Reset** to cancel a running timer early
- Click any spell row to start the cooldown timer
- Click the same row again to reset a running timer
- 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

## Settings Panel

Open via **Escape → Options → AddOns → Healer Cooldown Tracker**:

- **Columns** — number of grid columns (1 = vertical stack)
- **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
- **Cooldown durations** — override any ability's cooldown in seconds; revert with the Default button

## Adding More Cooldowns

Open `Data.lua` and add a new entry to the `CT.COOLDOWNS` table:

```lua
{
id = "priest_divine_hymn", -- unique key
class = "Priest",
name = "Divine Hymn",
duration = 180, -- seconds
icon = "Interface\\Icons\\spell_holy_divinehymn",
r = 1.0, g = 0.8, b = 0.2, -- accent colour (RGB 0-1)
id = "priest_divine_hymn", -- unique key
class = "Priest",
name = "Divine Hymn",
duration = 180, -- seconds (overrideable in settings)
defaultDuration = 180, -- used by the "Default" reset button
icon = "Interface\\Icons\\spell_holy_divinehymn",
r = 1.0, g = 1.0, b = 1.0, -- accent colour (RGB 0-1)
},
```

Expand All @@ -55,7 +73,9 @@ CooldownTracker/
├── CooldownTracker.toc — Addon manifest & metadata
├── Data.lua — Cooldown definitions (edit this to add abilities)
├── UI.lua — Frame, row widgets, timer rendering
├── Settings.lua — In-game options panel
├── Core.lua — Init, events, slash commands
├── AGENTS.md — AI agent coding guidelines
└── README.md — This file
```

Expand Down
60 changes: 54 additions & 6 deletions Settings.lua
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,22 @@ local function CreateSettingsPanel()
end)
colBox:SetScript("OnLeave", function() GameTooltip:Hide() end)

-- ----- Sound toggle -----------------------------------------------------
local soundCheck = CreateFrame("CheckButton", "CTSettingsSoundCheck", panel, "UICheckButtonTemplate")
soundCheck:SetSize(24, 24)
soundCheck:SetPoint("TOPLEFT", colLabel, "BOTTOMLEFT", 0, -14)
soundCheck:SetChecked(CooldownTrackerDB.playSoundOnReady ~= false)
soundCheck:SetScript("OnClick", function(self)
CooldownTrackerDB.playSoundOnReady = self:GetChecked()
end)

local soundLabel = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
soundLabel:SetPoint("LEFT", soundCheck, "RIGHT", 4, 0)
soundLabel:SetText("Play sound when cooldown is ready")

local layoutDivider = panel:CreateTexture(nil, "ARTWORK")
layoutDivider:SetHeight(1)
layoutDivider:SetPoint("TOPLEFT", colLabel, "BOTTOMLEFT", 0, -10)
layoutDivider:SetPoint("TOPLEFT", soundCheck, "BOTTOMLEFT", 0, -10)
layoutDivider:SetWidth(CONTENT_WIDTH)
layoutDivider:SetColorTexture(0.3, 0.3, 0.4, 0.4)

Expand Down Expand Up @@ -206,12 +219,17 @@ local function CreateSettingsPanel()

-- ----- Cooldown duration headers ----------------------------------------
local hdrAbility = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
hdrAbility:SetPoint("TOPLEFT", rosterDivider, "BOTTOMLEFT", ICON_SIZE + 12, -8)
hdrAbility:SetPoint("TOPLEFT", rosterDivider, "BOTTOMLEFT", 28 + ICON_SIZE + 12, -8)
hdrAbility:SetText("Ability")
hdrAbility:SetTextColor(0.7, 0.7, 0.7)

local hdrShow = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
hdrShow:SetPoint("LEFT", rosterDivider, "BOTTOMLEFT", 4, -8)
hdrShow:SetText("Show")
hdrShow:SetTextColor(0.7, 0.7, 0.7)

local hdrDur = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
hdrDur:SetPoint("LEFT", hdrAbility, "LEFT", 260, 0)
hdrDur:SetPoint("LEFT", hdrAbility, "LEFT", 248, 0)
hdrDur:SetText("Cooldown (seconds)")
hdrDur:SetTextColor(0.7, 0.7, 0.7)

Expand All @@ -224,6 +242,7 @@ local function CreateSettingsPanel()
-- editBoxes and RefreshAllEditBoxes must be declared here so the OnShow
-- closure below can reference them (Lua requires locals before use).
local editBoxes = {}
local spellCheckboxes = {}
local function RefreshAllEditBoxes()
C_Timer.After(0, function()
for _, cd in ipairs(CT.COOLDOWNS) do
Expand All @@ -240,10 +259,15 @@ local function CreateSettingsPanel()
panel:SetScript("OnShow", function()
C_Timer.After(0, function()
colBox:SetText(tostring(CooldownTrackerDB.columns or 1))
local counts = CooldownTrackerDB.classCounts or {}
soundCheck:SetChecked(CooldownTrackerDB.playSoundOnReady ~= false)
local counts = CooldownTrackerDB.classCounts or {}
local disabled = CooldownTrackerDB.disabledSpells or {}
for clsName, box in pairs(classCountBoxes) do
box:SetText(tostring(counts[clsName] or 1))
end
for spellId, cb in pairs(spellCheckboxes) do
cb:SetChecked(not disabled[spellId])
end
RefreshAllEditBoxes()
end)
end)
Expand Down Expand Up @@ -281,10 +305,26 @@ local function CreateSettingsPanel()
strip:SetPoint("LEFT", row, "LEFT", 0, 0)
strip:SetColorTexture(cd.r, cd.g, cd.b, 0.9)

-- Spell visibility checkbox
local spellCB = CreateFrame("CheckButton", "CTSettingsSpell_" .. cd.id, row, "UICheckButtonTemplate")
spellCB:SetSize(24, 24)
spellCB:SetPoint("LEFT", row, "LEFT", 4, 0)
spellCB:SetChecked(not (CooldownTrackerDB.disabledSpells or {})[cd.id])
spellCB:SetScript("OnClick", function(self)
CooldownTrackerDB.disabledSpells = CooldownTrackerDB.disabledSpells or {}
if self:GetChecked() then
CooldownTrackerDB.disabledSpells[cd.id] = nil
else
CooldownTrackerDB.disabledSpells[cd.id] = true
end
if CT.RebuildUI then CT:RebuildUI() end
end)
spellCheckboxes[cd.id] = spellCB

-- Icon
local icon = row:CreateTexture(nil, "ARTWORK")
icon:SetSize(ICON_SIZE, ICON_SIZE)
icon:SetPoint("LEFT", row, "LEFT", 8, 0)
icon:SetPoint("LEFT", row, "LEFT", 36, 0)
icon:SetTexture(cd.icon)
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

Expand All @@ -304,7 +344,7 @@ local function CreateSettingsPanel()
-- Duration edit box — built manually to avoid template init issues
local editBox = CreateFrame("EditBox", "CTSettingsEdit_" .. cd.id, row, "BackdropTemplate")
editBox:SetSize(EDIT_WIDTH, 24)
editBox:SetPoint("LEFT", row, "LEFT", 280, 0)
editBox:SetPoint("LEFT", row, "LEFT", 308, 0)
editBox:SetAutoFocus(false)
editBox:SetFontObject("ChatFontNormal")
editBox:SetJustifyH("CENTER")
Expand All @@ -329,6 +369,14 @@ local function CreateSettingsPanel()
CooldownTrackerDB.customDurations = CooldownTrackerDB.customDurations or {}
CooldownTrackerDB.customDurations[cd.id] = (val ~= cd.defaultDuration) and val or nil
cd.duration = val
if CT.expandedCooldowns then
local prefix = cd.id .. "_"
for _, ecd in ipairs(CT.expandedCooldowns) do
if ecd.id == cd.id or ecd.id:sub(1, #prefix) == prefix then
ecd.duration = val
end
end
end
end
end
editBox:SetScript("OnTextChanged", function(self, userInput)
Expand Down
Loading