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 - -image -image - - - +# 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