diff --git a/README.md b/README.md
index 1eb2bf9..2045b20 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,4 @@
-# Cursive - updated for octowow
--Requires nampower https://github.com/Emyrk/nampower
-
--Added global cooldown bar can change size/color in settings
-
--Added ghost icons for DOTs can toggle on/off in settings
-
-
-
-
-
-
+# Cursive
> [!IMPORTANT]
>
diff --git a/settings.lua b/settings.lua
index 46f8b49..e7e6184 100644
--- a/settings.lua
+++ b/settings.lua
@@ -18,6 +18,14 @@ Cursive:RegisterDefaults("profile", {
showgcdbar = true,
gcdbarheight = 3,
gcdbarwidth = 200,
+ gcdbarcolor = { r = 1, g = 0.85, b = 0.1 },
+ showghostcurse = true,
+ showghostcorruption = true,
+ showghostimmolate = true,
+ showghostsiphonlife = true,
+ ghosticonalpha = 0.4,
+ showallmissingghosts = false,
+ flashonexpiring = true,
showtargetindicator = true,
showraidicons = true,
showhealthbar = true,
@@ -82,6 +90,19 @@ local function splitIgnoreString(str, delimiter)
end
local barOptions = {
+ ["flashonexpiring"] = {
+ type = "toggle",
+ name = "Flash Icon On Expiring",
+ desc = "Flash a curse/DoT icon when it's about to fall off (same threshold as the expiring sound)",
+ order = 96,
+ get = function()
+ return Cursive.db.profile.flashonexpiring
+ end,
+ set = function(v)
+ Cursive.db.profile.flashonexpiring = v
+ Cursive.UpdateFramesFromConfig()
+ end,
+ },
["invertbars"] = {
type = "toggle",
name = "Invert Bar Display",
@@ -370,6 +391,90 @@ local barOptions = {
},
}
+local ghostOptions = {
+ ["showghostcurse"] = {
+ type = "toggle",
+ name = "Show Ghost Curse Icon",
+ desc = "Show a faded icon suggesting a curse to cast when the target has none",
+ order = 1,
+ get = function()
+ return Cursive.db.profile.showghostcurse
+ end,
+ set = function(v)
+ Cursive.db.profile.showghostcurse = v
+ Cursive.UpdateFramesFromConfig()
+ end,
+ },
+ ["showghostcorruption"] = {
+ type = "toggle",
+ name = "Show Ghost Corruption",
+ desc = "Show a faded icon suggesting Corruption when missing from the target",
+ order = 2,
+ get = function()
+ return Cursive.db.profile.showghostcorruption
+ end,
+ set = function(v)
+ Cursive.db.profile.showghostcorruption = v
+ Cursive.UpdateFramesFromConfig()
+ end,
+ },
+ ["showghostimmolate"] = {
+ type = "toggle",
+ name = "Show Ghost Immolate",
+ desc = "Show a faded icon suggesting Immolate when missing from the target",
+ order = 3,
+ get = function()
+ return Cursive.db.profile.showghostimmolate
+ end,
+ set = function(v)
+ Cursive.db.profile.showghostimmolate = v
+ Cursive.UpdateFramesFromConfig()
+ end,
+ },
+ ["showghostsiphonlife"] = {
+ type = "toggle",
+ name = "Show Ghost Siphon Life",
+ desc = "Show a faded icon suggesting Siphon Life when missing from the target",
+ order = 4,
+ get = function()
+ return Cursive.db.profile.showghostsiphonlife
+ end,
+ set = function(v)
+ Cursive.db.profile.showghostsiphonlife = v
+ Cursive.UpdateFramesFromConfig()
+ end,
+ },
+ ["ghostalpha"] = {
+ type = "range",
+ name = "Ghost Icon Transparency",
+ desc = "How faded the ghost icons appear (higher = more visible)",
+ order = 5,
+ min = 10,
+ max = 100,
+ step = 5,
+ get = function()
+ return math.floor(Cursive.db.profile.ghosticonalpha * 100 + 0.5)
+ end,
+ set = function(v)
+ Cursive.db.profile.ghosticonalpha = v / 100
+ Cursive.UpdateFramesFromConfig()
+ end,
+ },
+ ["showallmissingghosts"] = {
+ type = "toggle",
+ name = "Show All Missing (not just next)",
+ desc = "Show a ghost icon for every missing curse/DoT at once, instead of just the next one in priority order",
+ order = 6,
+ get = function()
+ return Cursive.db.profile.showallmissingghosts
+ end,
+ set = function(v)
+ Cursive.db.profile.showallmissingghosts = v
+ Cursive.UpdateFramesFromConfig()
+ end,
+ },
+}
+
local mobFilters = {
["incombat"] = {
type = "toggle",
@@ -582,6 +687,7 @@ Cursive.cmdtable = {
Cursive.db.profile.gcdbarheight = v
Cursive.UpdateFramesFromConfig()
end
+ Cursive.ui.PreviewGcdBar()
end,
},
["gcdbarwidth"] = {
@@ -600,6 +706,23 @@ Cursive.cmdtable = {
Cursive.db.profile.gcdbarwidth = v
Cursive.UpdateFramesFromConfig()
end
+ Cursive.ui.PreviewGcdBar()
+ end,
+ },
+ ["gcdbarcolor"] = {
+ type = "color",
+ name = "GCD Bar Color",
+ desc = "Color of the GCD swing-timer bar (shows a preview fill while you adjust it)",
+ order = 43,
+ get = function()
+ return Cursive.db.profile.gcdbarcolor.r, Cursive.db.profile.gcdbarcolor.g, Cursive.db.profile.gcdbarcolor.b
+ end,
+ set = function(r, g, b)
+ Cursive.db.profile.gcdbarcolor.r = r
+ Cursive.db.profile.gcdbarcolor.g = g
+ Cursive.db.profile.gcdbarcolor.b = b
+ Cursive.UpdateFramesFromConfig()
+ Cursive.ui.PreviewGcdBar()
end,
},
["clickthrough"] = {
@@ -652,6 +775,13 @@ Cursive.cmdtable = {
order = 13,
args = barOptions
},
+ ["ghosticons"] = {
+ type = "group",
+ name = "Ghost Icon Settings",
+ desc = "Ghost Icon Settings",
+ order = 14,
+ args = ghostOptions
+ },
["filters"] = {
type = "group",
name = L["Mob filters"],
diff --git a/ui.lua b/ui.lua
index 58fc402..6ca83c7 100644
--- a/ui.lua
+++ b/ui.lua
@@ -8,6 +8,9 @@ local utils = Cursive.utils
local filter = Cursive.filter
local ui = CreateFrame("Frame", "CursiveUI", UIParent)
+-- Registry of currently-expiring curse icon frames that should flash.
+-- Populated/cleared during row updates, animated by a shared ticker below.
+ui.flashingCurseIcons = {}
ui.border = {
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
@@ -107,6 +110,7 @@ local function UpdateRootBarFrame()
ui.rootBarFrame.gcdBar:SetHeight(config.gcdbarheight)
ui.rootBarFrame.gcdBar:SetWidth(config.gcdbarwidth)
ui.rootBarFrame.gcdBar:SetStatusBarTexture(config.bartexture)
+ ui.rootBarFrame.gcdBar:SetStatusBarColor(config.gcdbarcolor.r, config.gcdbarcolor.g, config.gcdbarcolor.b, 0.9)
ui.rootBarFrame.gcdBar.enabled = config.showgcdbar
if not config.showgcdbar then
ui.rootBarFrame.gcdBar:Hide()
@@ -158,7 +162,7 @@ local function CreateRoot()
gcdBar:SetWidth(Cursive.db.profile.gcdbarwidth)
gcdBar:SetPoint("TOPLEFT", frame.caption, "BOTTOMLEFT", -8, -2)
gcdBar:SetStatusBarTexture(Cursive.db.profile.bartexture)
- gcdBar:SetStatusBarColor(1, .85, .1, .9)
+ gcdBar:SetStatusBarColor(Cursive.db.profile.gcdbarcolor.r, Cursive.db.profile.gcdbarcolor.g, Cursive.db.profile.gcdbarcolor.b, 0.9)
gcdBar:SetMinMaxValues(0, 1)
gcdBar:SetValue(0)
gcdBar.enabled = Cursive.db.profile.showgcdbar
@@ -496,6 +500,21 @@ local function CreateBarThirdSection(unitFrame, guid)
curse:Hide()
unitFrame["curse" .. i] = curse
end
+
+ -- Ghost icons: faded icons showing missing curse/DoT(s) to cast on
+ -- this target. One frame per possible slot (same count as real curse
+ -- icons), so "show all missing" mode can display several at once.
+ -- Repositioned dynamically on each refresh to slot into wherever the
+ -- next real curse icon would actually go.
+ unitFrame.ghostIcons = {}
+ for i = 1, config.maxcurses do
+ local ghost = thirdSection:CreateTexture(nil, "OVERLAY")
+ ghost:SetWidth(config.curseiconsize)
+ ghost:SetHeight(config.curseiconsize)
+ ghost:SetAlpha(config.ghosticonalpha)
+ ghost:Hide()
+ unitFrame.ghostIcons[i] = ghost
+ end
end
local function CreateBar(row, col, guid)
@@ -551,6 +570,70 @@ local function hasAnySpellId(guid, spellIds)
return nil
end
+-- Priority list for the ghost icon: checks curse presence first (via
+-- Cursive's own tracked curse data), then DoTs in order, returning the
+-- icon path for whichever is missing first. Curse icon textures (CoE/CoS/
+-- CoR/CoW) are confirmed-correct from prior testing; the DoT textures and
+-- the Curse of Agony fallback below are reasonable but UNVERIFIED guesses
+-- -- worth checking in-game and reporting back if any look wrong.
+local GHOST_DOT_ICONS = {
+ { name = "Corruption", texture = "Spell_Shadow_AbominationExplosion", settingKey = "showghostcorruption" },
+ { name = "Immolate", texture = "Spell_Fire_Immolation", settingKey = "showghostimmolate" },
+ { name = "Siphon Life", texture = "Spell_Shadow_Requiem", settingKey = "showghostsiphonlife" },
+}
+local GHOST_CURSE_FALLBACK_ICON = "Interface\\Icons\\Spell_Shadow_CurseOfSargeras" -- Curse of Agony
+
+local function GetGhostTextures(guid)
+ local config = Cursive.db.profile
+ local textures = {}
+
+ -- Curse check: specifically checks Curse of Agony via Cursive's own
+ -- tested HasCurse API, rather than a custom table check. This matters
+ -- for Malediction warlocks -- casting Elements/Shadow/Recklessness also
+ -- applies Agony, so "is any curse present" isn't the right question;
+ -- what matters is whether Agony itself has fallen off and needs
+ -- refreshing, even if another curse is technically still ticking.
+ -- malediction=0 here since we're already asking about Agony directly,
+ -- not one of the curses that would normally redirect to it.
+ if config.showghostcurse then
+ local hasCurse = Cursive.curses:HasCurse(L["curse of agony"], guid, 0, 0)
+ if not hasCurse then
+ table.insert(textures, GHOST_CURSE_FALLBACK_ICON)
+ if not config.showallmissingghosts then
+ return textures
+ end
+ end
+ end
+
+ -- DoT check: direct debuff scan via SuperWoW's GUID-based UnitDebuff,
+ -- since these aren't part of Cursive's own curse tracking. Each DoT
+ -- checked individually against its own toggle, since not everyone
+ -- runs all three (e.g. pure Affliction warlocks often skip Immolate).
+ for _, dot in ipairs(GHOST_DOT_ICONS) do
+ if config[dot.settingKey] then
+ local found = false
+ local i = 1
+ while true do
+ local tex = UnitDebuff(guid, i)
+ if not tex then break end
+ if string.find(tex, dot.texture, 1, true) then
+ found = true
+ break
+ end
+ i = i + 1
+ end
+ if not found then
+ table.insert(textures, "Interface\\Icons\\" .. dot.texture)
+ if not config.showallmissingghosts then
+ return textures
+ end
+ end
+ end
+ end
+
+ return textures -- empty if everything's up (or all checks disabled)
+end
+
local function GetSortedCurses(guidCurses)
-- Collect keys
local curseNames = {}
@@ -632,6 +715,8 @@ local function DisplayGuid(guid)
local curse = unitFrame["curse" .. i]
curse:Hide()
curse.timer:Hide()
+ ui.flashingCurseIcons[curse] = nil
+ curse:SetAlpha(1)
end
local guidCurses = Cursive.curses.guids[guid]
@@ -657,18 +742,63 @@ local function DisplayGuid(guid)
curse.timer:Show()
curse:Show()
- if remaining < 1 then
+ if remaining < 1 then
if Cursive.curses:ShouldPlayExpiringSound(curseName, guid) then
PlaySoundFile("Interface\\AddOns\\Cursive\\sounds\\expiring.mp3")
end
- elseif Cursive.curses:HasRequestedExpiringSound(curseName, guid) then
- Cursive.curses:EnableExpiringSound(curseName, guid)
+ else
+ if Cursive.curses:HasRequestedExpiringSound(curseName, guid) then
+ Cursive.curses:EnableExpiringSound(curseName, guid)
+ end
end
+
+ -- Flash check: independent, wider threshold (<=1 rather than <1)
+ -- since remaining is an integer-quantized value that may jump
+ -- straight from 2 to gone without ever reading a value strictly
+ -- less than 1 for any meaningful duration.
+ if Cursive.db.profile.flashonexpiring and remaining <= 1 then
+ ui.flashingCurseIcons[curse] = true
+ else
+ ui.flashingCurseIcons[curse] = nil
+ curse:SetAlpha(1)
+ end
+
curseNumber = curseNumber + 1
end
end
end
+ local cfg = Cursive.db.profile
+
+ -- Hide all ghost slots first, matching the same pattern used for real
+ -- curse icons above.
+ for i = 1, cfg.maxcurses do
+ if unitFrame.ghostIcons[i] then
+ unitFrame.ghostIcons[i]:Hide()
+ end
+ end
+
+ if cfg.showghostcurse or cfg.showghostcorruption or cfg.showghostimmolate or cfg.showghostsiphonlife then
+ local ghostTextures = GetGhostTextures(guid)
+ local slot = curseNumber
+ for _, tex in ipairs(ghostTextures) do
+ if slot > cfg.maxcurses then
+ break
+ end
+ local ghostIcon = unitFrame.ghostIcons[slot]
+ ghostIcon:ClearAllPoints()
+ if cfg.invertbars then
+ local rightOffset = slot * ui.padding + ((slot - 1) * cfg.curseiconsize)
+ ghostIcon:SetPoint("RIGHT", unitFrame.thirdSection, "RIGHT", -rightOffset, 0)
+ else
+ ghostIcon:SetPoint("LEFT", unitFrame.thirdSection, "LEFT", slot * ui.padding + ((slot - 1) * cfg.curseiconsize), 0)
+ end
+ ghostIcon:SetTexture(tex)
+ ghostIcon:Show()
+ slot = slot + 1
+ end
+ end
+
unitFrame:Show()
ui.numDisplayed = ui.numDisplayed + 1
@@ -903,12 +1033,33 @@ ui.StartGcdBar = function(duration)
end
local gcdTicker = CreateFrame("Frame", "CursiveGcdTicker", UIParent)
+ui.gcdPreviewUntil = nil
+
+-- Call this from any GCD bar setting's set() callback to show a temporary
+-- static preview fill, since the real bar normally only appears during an
+-- actual GCD -- this lets color/size changes be seen immediately.
+function ui.PreviewGcdBar()
+ ui.gcdPreviewUntil = GetTime() + 4
+end
+
gcdTicker:SetScript("OnUpdate", function()
if not ui.rootBarFrame or not ui.rootBarFrame.gcdBar then
return
end
local bar = ui.rootBarFrame.gcdBar
+
+ -- Preview mode: triggered directly by changing a GCD bar setting,
+ -- rather than trying to detect whether the options menu is open.
+ if ui.gcdPreviewUntil and GetTime() < ui.gcdPreviewUntil then
+ bar:SetMinMaxValues(0, 1)
+ bar:SetValue(0.6)
+ if not bar:IsShown() then
+ bar:Show()
+ end
+ return
+ end
+
if not bar.enabled or not ui.gcdBarStart then
if bar:IsShown() then
bar:Hide()
@@ -931,4 +1082,25 @@ gcdTicker:SetScript("OnUpdate", function()
end
end)
+-- Flashes any curse icon currently registered as expiring, via a smooth
+-- alpha oscillation. Single shared ticker for all rows rather than one per
+-- icon, since the registry is normally small (just whatever's expiring).
+local flashTicker = CreateFrame("Frame", "CursiveFlashTicker", UIParent)
+local flashTime = 0
+flashTicker:SetScript("OnUpdate", function()
+ local any = false
+ for icon in pairs(ui.flashingCurseIcons) do
+ any = true
+ break
+ end
+ if not any then
+ return
+ end
+ flashTime = flashTime + arg1 * 3
+ local alpha = 0.35 + (0.65 * (0.5 + 0.5 * math.sin(flashTime)))
+ for icon in pairs(ui.flashingCurseIcons) do
+ icon:SetAlpha(alpha)
+ end
+end)
+
Cursive.ui = ui