2363 lines
79 KiB
Lua
2363 lines
79 KiB
Lua
if not Cursive.nampower then
|
|
return
|
|
end
|
|
|
|
local L = AceLibrary("AceLocale-2.2"):new("Cursive")
|
|
|
|
local function ThemeBorder(frame)
|
|
if ShaguTweaks and (ShaguTweaks.DarkMode or ShaguTweaks.GoldMode) and GameTooltip then
|
|
frame:SetBackdropBorderColor(GameTooltip:GetBackdropBorderColor())
|
|
else
|
|
frame:SetBackdropBorderColor(1, 1, 1, 1)
|
|
end
|
|
end
|
|
local tooltipBorder = { edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 8 }
|
|
local function AddAuraBorder(icon, parent)
|
|
if not Cursive.db.profile.auraborder then return end
|
|
local border = CreateFrame("Frame", nil, parent)
|
|
border:SetPoint("TOPLEFT", icon, "TOPLEFT", -2, 2)
|
|
border:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 2, -2)
|
|
border:SetBackdrop(tooltipBorder)
|
|
border:EnableMouse(false)
|
|
border:Hide()
|
|
icon.tooltipBorder = border
|
|
end
|
|
local function UpdateAuraBorder(icon)
|
|
if icon.tooltipBorder then
|
|
if icon:IsShown() then ThemeBorder(icon.tooltipBorder); icon.tooltipBorder:Show()
|
|
else icon.tooltipBorder:Hide() end
|
|
end
|
|
end
|
|
function Cursive.ApplyMenuStyle()
|
|
local f = CursiveSettingsWindow
|
|
if not f then return end
|
|
if Cursive.db.profile.classicmenu then
|
|
f:SetBackdrop({bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
|
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true,
|
|
tileSize = 32, edgeSize = 32, insets = {left=8, right=8, top=8, bottom=8}})
|
|
f:SetBackdropColor(1, 1, 1, (Cursive.db.profile.menuopacity or 100) / 100)
|
|
ThemeBorder(f)
|
|
else
|
|
f:SetBackdrop({bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1})
|
|
f:SetBackdropColor(0.05, 0.05, 0.08, (Cursive.db.profile.menuopacity or 100) / 100)
|
|
f:SetBackdropBorderColor(0.25, 0.25, 0.35, 1)
|
|
end
|
|
if f.menuFill then
|
|
f.menuFill:SetTexture(0.025, 0.025, 0.035, (Cursive.db.profile.menuopacity or 100) / 100)
|
|
end
|
|
if f.classicHeader then
|
|
if ShaguTweaks and (ShaguTweaks.DarkMode or ShaguTweaks.GoldMode) and GameTooltip then
|
|
local r, g, b = GameTooltip:GetBackdropBorderColor()
|
|
f.classicHeader:SetVertexColor(r, g, b, 1)
|
|
else
|
|
f.classicHeader:SetVertexColor(1, 1, 1, 1)
|
|
end
|
|
end
|
|
if f.titleText then
|
|
f.titleText:ClearAllPoints()
|
|
if Cursive.db.profile.classicmenu then
|
|
f.titleText:SetPoint("TOP", f, "TOP", 0, -1)
|
|
else
|
|
f.titleText:SetPoint("LEFT", f.titleBar, "LEFT", 8, 0)
|
|
end
|
|
end
|
|
if f.titleBar then
|
|
|
|
if Cursive.db.profile.classicmenu then
|
|
f.titleBar:SetBackdrop(nil)
|
|
f.classicHeader:Show()
|
|
f.titleBar:SetHeight(32)
|
|
else
|
|
f.titleBar:SetBackdrop({bgFile = "Interface\\Buttons\\WHITE8X8"})
|
|
f.classicHeader:Hide()
|
|
f.titleBar:SetHeight(24)
|
|
f.titleBar:SetBackdropColor(0.22, 0.08, 0.38, 1)
|
|
end
|
|
end
|
|
end
|
|
|
|
local utils = Cursive.utils
|
|
local filter = Cursive.filter
|
|
|
|
local ui = CreateFrame("Frame", "CursiveUI", UIParent)
|
|
|
|
-- User-selectable font for the actual display elements (unit row text,
|
|
-- curse/shadow-vuln timers, Sacrifice bar). Falls back to the default if
|
|
-- unset (e.g. existing profiles upgrading without this setting yet).
|
|
local function GetUIFont()
|
|
return (Cursive.db.profile.uifont) or STANDARD_TEXT_FONT
|
|
end
|
|
|
|
-- SetFont returns a success boolean -- if a font fails to load (missing
|
|
-- file, unsupported format, etc), fall back to the standard font rather
|
|
-- than leaving the FontString with no font set at all, which throws a
|
|
-- hard error on the next SetText call (confirmed cause of a full addon
|
|
-- crash earlier this session).
|
|
ui.fontDebugMode = false
|
|
|
|
local function SafeSetFont(fontString, fontPath, size, flags)
|
|
local ok = fontString:SetFont(fontPath, size, flags)
|
|
if not ok then
|
|
if ui.fontDebugMode then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffff6060[FontDebug]|r FAILED to load: "..tostring(fontPath))
|
|
end
|
|
fontString:SetFont(STANDARD_TEXT_FONT, size, flags)
|
|
elseif ui.fontDebugMode then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff60ff60[FontDebug]|r Loaded OK: "..tostring(fontPath))
|
|
end
|
|
end
|
|
ui.SafeSetFont = SafeSetFont
|
|
|
|
SLASH_CURSIVEFONTDEBUG1 = "/cursivefontdebug"
|
|
SlashCmdList["CURSIVEFONTDEBUG"] = function()
|
|
ui.fontDebugMode = not ui.fontDebugMode
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00Cursive|r font debug mode: "..(ui.fontDebugMode and "|cff00ff00ON|r" or "|cffff6060OFF|r"))
|
|
end
|
|
-- Empirically-adjusted duration for the Shadow Vulnerability local
|
|
-- countdown timer -- see comment at its usage for why this isn't 12s.
|
|
local SHADOW_VULN_DURATION = 9
|
|
-- 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",
|
|
tile = true, tileSize = 16, edgeSize = 8,
|
|
insets = { left = 2, right = 2, top = 2, bottom = 2 }
|
|
}
|
|
|
|
ui.background = {
|
|
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
|
tile = true, tileSize = 16, edgeSize = 8,
|
|
insets = { left = 0, right = 0, top = 0, bottom = 0 }
|
|
}
|
|
|
|
ui.rootBarFrame = nil
|
|
ui.targetIndicatorSize = 8
|
|
ui.padding = 2
|
|
|
|
ui.row = 1
|
|
ui.col = 1
|
|
ui.maxBarsDisplayed = false
|
|
ui.numDisplayed = 0
|
|
|
|
local function GetBarFirstSectionWidth()
|
|
local config = Cursive.db.profile
|
|
|
|
local size = 1
|
|
if config.showraidicons then
|
|
size = size + config.raidiconsize
|
|
end
|
|
if config.showtargetindicator then
|
|
size = size + ui.targetIndicatorSize
|
|
end
|
|
if size > 0 then
|
|
size = size + ui.padding
|
|
end
|
|
|
|
return size
|
|
end
|
|
|
|
local function GetBarSecondSectionWidth()
|
|
local config = Cursive.db.profile
|
|
|
|
if config.showhealthbar == false and config.showunitname == false then
|
|
return 1
|
|
end
|
|
|
|
return config.healthwidth + ui.padding
|
|
end
|
|
|
|
local function GetBarThirdSectionWidth()
|
|
local config = Cursive.db.profile
|
|
|
|
return config.maxcurses * (config.curseiconsize + ui.padding)
|
|
end
|
|
|
|
local function GetAuraGap()
|
|
-- Inverted rows put auras on the left, away from the top-right dragon.
|
|
local config = Cursive.db.profile
|
|
return (config.showelitedragon and not config.invertbars) and 12 or 0
|
|
end
|
|
|
|
local function GetBarWidth()
|
|
return GetBarFirstSectionWidth() +
|
|
GetBarSecondSectionWidth() +
|
|
GetBarThirdSectionWidth() + GetAuraGap()
|
|
end
|
|
|
|
local function UpdateRootBarFrame()
|
|
local config = Cursive.db.profile
|
|
|
|
if config.showbackdrop then
|
|
ui.rootBarFrame:SetBackdrop(ui.background)
|
|
else
|
|
ui.rootBarFrame:SetBackdrop(nil)
|
|
end
|
|
|
|
ui.rootBarFrame:EnableMouse(not config.clickthrough)
|
|
|
|
ui.rootBarFrame.pos = config.anchor .. config.x .. config.y .. config.scale
|
|
ui.rootBarFrame:ClearAllPoints()
|
|
ui.rootBarFrame:SetPoint(config.anchor, config.x, config.y)
|
|
|
|
ui.rootBarFrame:SetScale(config.scale)
|
|
|
|
SafeSetFont(ui.rootBarFrame.caption, GetUIFont(), Cursive.db.profile.textsize, "THINOUTLINE")
|
|
ui.rootBarFrame.caption:SetText(Cursive.db.profile.caption)
|
|
if Cursive.db.profile.showtitle then
|
|
ui.rootBarFrame.caption:Show()
|
|
else
|
|
ui.rootBarFrame.caption:Hide()
|
|
end
|
|
|
|
ui.rootBarFrame:SetWidth(config.maxcol * GetBarWidth())
|
|
-- Calculate height: title area + GCD bar (if shown) + all rows + extra spacing
|
|
local title_size = 12 + config.spacing
|
|
local gcdbar_size = config.showgcdbar and (config.gcdbarheight + config.spacing) or 0
|
|
local total_height = title_size + gcdbar_size + (config.maxrow * (config.height + config.spacing)) + config.spacing
|
|
ui.rootBarFrame:SetHeight(total_height)
|
|
|
|
-- GCD swing-timer bar: thin strip spanning the frame width, sitting
|
|
-- just below the title caption and above the first row of bars.
|
|
if ui.rootBarFrame.gcdBar then
|
|
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()
|
|
end
|
|
end
|
|
end
|
|
|
|
local function CreateRoot()
|
|
local frame = CreateFrame("Frame", Cursive.db.profile.caption, UIParent)
|
|
ui.rootBarFrame = frame
|
|
|
|
frame.id = Cursive.db.profile.caption
|
|
|
|
frame:RegisterForDrag("LeftButton")
|
|
frame:SetMovable(true)
|
|
|
|
frame:SetScript("OnDragStart", function()
|
|
this.lock = true
|
|
this:StartMoving()
|
|
end)
|
|
|
|
frame:SetScript("OnDragStop", function()
|
|
-- convert to best anchor depending on position
|
|
local new_anchor = utils.GetBestAnchor(this)
|
|
local anchor, x, y = utils.ConvertFrameAnchor(this, new_anchor)
|
|
this:ClearAllPoints()
|
|
this:SetPoint(anchor, UIParent, anchor, x, y)
|
|
|
|
-- save new position
|
|
anchor, _, _, x, y = this:GetPoint()
|
|
Cursive.db.profile.anchor, Cursive.db.profile.x, Cursive.db.profile.y = anchor, x, y
|
|
|
|
-- stop drag
|
|
this:StopMovingOrSizing()
|
|
this.lock = false
|
|
|
|
this:ClearAllPoints()
|
|
this:SetPoint(anchor, x, y)
|
|
end)
|
|
|
|
-- create title text
|
|
frame.caption = frame:CreateFontString(nil, "HIGH", "GameFontWhite")
|
|
frame.caption:SetPoint("TOPLEFT", frame, "TOPLEFT", 10, -2)
|
|
frame.caption:SetTextColor(1, 1, 1, 1)
|
|
|
|
-- GCD swing-timer bar
|
|
local gcdBar = CreateFrame("StatusBar", "CursiveGcdBar", frame)
|
|
gcdBar:SetHeight(Cursive.db.profile.gcdbarheight)
|
|
gcdBar:SetWidth(Cursive.db.profile.gcdbarwidth)
|
|
gcdBar:SetPoint("TOPLEFT", frame.caption, "BOTTOMLEFT", -8, -2)
|
|
gcdBar:SetStatusBarTexture(Cursive.db.profile.bartexture)
|
|
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
|
|
|
|
local gcdBarBg = gcdBar:CreateTexture(nil, "BACKGROUND")
|
|
gcdBarBg:SetAllPoints(gcdBar)
|
|
gcdBarBg:SetTexture(0, 0, 0)
|
|
gcdBarBg:SetAlpha(0.4)
|
|
|
|
gcdBar:Hide()
|
|
frame.gcdBar = gcdBar
|
|
|
|
UpdateRootBarFrame()
|
|
|
|
frame:Show()
|
|
|
|
return frame
|
|
end
|
|
|
|
ui.unitFrames = {} -- holds all unitFrames for all columns/rows
|
|
|
|
Cursive.UpdateFramesFromConfig = function()
|
|
if ui.UpdateFontPreview then ui.UpdateFontPreview(GetUIFont()) end
|
|
for col, rows in pairs(ui.unitFrames) do
|
|
for row, unitFrame in pairs(rows) do
|
|
if unitFrame and unitFrame:IsShown() then
|
|
unitFrame:Hide()
|
|
end
|
|
end
|
|
end
|
|
|
|
if ui.rootBarFrame then
|
|
UpdateRootBarFrame()
|
|
end
|
|
|
|
-- Debounce slider movement, then rebuild rows with the new geometry.
|
|
Cursive:ScheduleEvent("resetUnitFrames", Cursive.ResetUnitFrames, 0.1)
|
|
end
|
|
|
|
Cursive.ResetUnitFrames = function()
|
|
-- hide all existing unit frames
|
|
for col, rows in pairs(ui.unitFrames) do
|
|
for row, unitFrame in pairs(rows) do
|
|
if unitFrame and unitFrame:IsShown() then
|
|
unitFrame:Hide()
|
|
end
|
|
end
|
|
end
|
|
-- Retired frames must not remain referenced by the flash driver.
|
|
for icon in pairs(ui.flashingCurseIcons) do ui.flashingCurseIcons[icon] = nil end
|
|
-- clear cached frames so they are recreated
|
|
ui.unitFrames = {}
|
|
end
|
|
|
|
ui.BarEnter = function()
|
|
if this.parent.testMode then return end
|
|
if this.parent.healthBar and not Cursive.db.profile.healthborder then
|
|
this.parent.healthBar.border:SetBackdropBorderColor(1, 1, 1, 1)
|
|
end
|
|
this.parent.hover = true
|
|
|
|
SetMouseoverUnit(this.parent.guid)
|
|
GameTooltip_SetDefaultAnchor(GameTooltip, this)
|
|
GameTooltip:SetUnit(this.parent.guid)
|
|
GameTooltip:Show()
|
|
end
|
|
|
|
ui.BarLeave = function()
|
|
this.parent.hover = false
|
|
SetMouseoverUnit()
|
|
GameTooltip:Hide()
|
|
end
|
|
|
|
local function UpdateEliteDragon(bar, classification)
|
|
if not bar.eliteDragon then return end
|
|
if classification == "elite" or classification == "worldboss" or classification == "rareelite" then
|
|
bar.eliteDragon:SetTexture("Interface\\AddOns\\Cursive\\img\\EliteDragon")
|
|
bar.eliteDragon:SetDesaturated(classification == "rareelite")
|
|
bar.eliteDragon:Show()
|
|
if bar.eliteDragonTail then
|
|
bar.eliteDragonTail:SetTexture("Interface\\AddOns\\Cursive\\img\\EliteDragon")
|
|
bar.eliteDragonTail:SetDesaturated(classification == "rareelite")
|
|
bar.eliteDragonTail:Show()
|
|
end
|
|
else
|
|
bar.eliteDragon:Hide()
|
|
if bar.eliteDragonTail then bar.eliteDragonTail:Hide() end
|
|
end
|
|
end
|
|
|
|
ui.BarUpdate = function()
|
|
if not this.guid or this.guid == 0 then
|
|
this:Hide()
|
|
return
|
|
end
|
|
|
|
if (this.tick or 1) > GetTime() then
|
|
return
|
|
else
|
|
this.tick = GetTime() + 0.05
|
|
end
|
|
|
|
UpdateEliteDragon(this, UnitClassification(this.guid))
|
|
-- update statusbar values if it exists
|
|
if this.healthBar then
|
|
this.healthBar:SetMinMaxValues(0, UnitHealthMax(this.guid))
|
|
this.healthBar:SetValue(UnitHealth(this.guid))
|
|
|
|
-- update health bar color
|
|
local hex, r, g, b, a = utils.GetUnitColor(this.guid)
|
|
this.healthBar:SetStatusBarColor(r, g, b, a)
|
|
|
|
-- update health bar border
|
|
if this.healthBar.border and not Cursive.db.profile.healthborder then
|
|
if this.hover then
|
|
this.healthBar.border:SetBackdropBorderColor(1, 1, 1, 1)
|
|
elseif UnitAffectingCombat(this.guid) then
|
|
this.healthBar.border:SetBackdropBorderColor(.8, .2, .2, 1)
|
|
else
|
|
this.healthBar.border:SetBackdropBorderColor(.2, .2, .2, 1)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- update caption text
|
|
local name = UnitName(this.guid)
|
|
if name and this.nameText then
|
|
this.nameText:SetText(name)
|
|
end
|
|
|
|
if this.hpText then
|
|
local hp = UnitHealth(this.guid)
|
|
if GetLocale() == "zhCN" then
|
|
if hp then
|
|
if hp >= 10000 then
|
|
hp = math.floor(hp / 1000) / 10 .. "万"
|
|
-- elseif hp >= 1000 then
|
|
-- hp = math.floor(hp / 100) / 10 .. "k"
|
|
end
|
|
end
|
|
else
|
|
-- convert hp to k if > 1000
|
|
if hp then
|
|
if hp >= 1000000 then
|
|
hp = math.floor(hp / 100000) / 10 .. "m"
|
|
elseif hp >= 1000 then
|
|
hp = math.floor(hp / 100) / 10 .. "k"
|
|
end
|
|
end
|
|
end
|
|
|
|
if hp then
|
|
this.hpText:SetText(hp)
|
|
end
|
|
end
|
|
|
|
-- show raid icon if existing
|
|
if this.icon then
|
|
if GetRaidTargetIndex(this.guid) and Cursive.filter.alive(this.guid) then
|
|
SetRaidTargetIconTexture(this.icon, GetRaidTargetIndex(this.guid))
|
|
this.icon:Show()
|
|
else
|
|
this.icon:Hide()
|
|
end
|
|
end
|
|
|
|
-- update target indicator
|
|
if this.target_left then
|
|
if UnitIsUnit("target", this.guid) then
|
|
this.target_left:Show()
|
|
else
|
|
this.target_left:Hide()
|
|
end
|
|
end
|
|
end
|
|
|
|
ui.BarClick = function()
|
|
if this.parent.testMode then return end
|
|
if arg1 == "LeftButton" then
|
|
TargetUnit(this.parent.guid)
|
|
elseif arg1 == "RightButton" then
|
|
TargetUnit(this.parent.guid)
|
|
if (not PlayerFrame.inCombat) then
|
|
AttackTarget()
|
|
end
|
|
end
|
|
end
|
|
|
|
local function CreateBarFirstSection(unitFrame, guid)
|
|
local config = Cursive.db.profile
|
|
local firstSection = CreateFrame("Frame", "Cursive1stSection", unitFrame)
|
|
|
|
if config.invertbars then
|
|
-- When inverted, position relative to second section (rightmost)
|
|
firstSection:SetPoint("LEFT", unitFrame.secondSection, "RIGHT", 0, 0)
|
|
else
|
|
-- Normal positioning (leftmost)
|
|
firstSection:SetPoint("LEFT", unitFrame, "LEFT", 0, 0)
|
|
end
|
|
|
|
firstSection:SetWidth(GetBarFirstSectionWidth())
|
|
firstSection:SetHeight(config.height)
|
|
firstSection:EnableMouse(false)
|
|
unitFrame.firstSection = firstSection
|
|
|
|
-- create target indicator
|
|
if config.showtargetindicator then
|
|
local targetLeft = firstSection:CreateTexture(nil, "OVERLAY")
|
|
targetLeft:SetWidth(ui.targetIndicatorSize)
|
|
targetLeft:SetHeight(8)
|
|
if config.invertbars then
|
|
targetLeft:SetPoint("RIGHT", firstSection, "RIGHT", 0, 0)
|
|
targetLeft:SetTexture("Interface\\AddOns\\Cursive\\img\\target-right")
|
|
else
|
|
targetLeft:SetPoint("LEFT", unitFrame, "LEFT", 0, 0)
|
|
targetLeft:SetTexture("Interface\\AddOns\\Cursive\\img\\target-left")
|
|
end
|
|
targetLeft:Hide()
|
|
unitFrame.target_left = targetLeft
|
|
end
|
|
|
|
-- create raid icon textures
|
|
if config.showraidicons then
|
|
local icon = firstSection:CreateTexture(nil, "OVERLAY")
|
|
icon:SetWidth(config.raidiconsize)
|
|
icon:SetHeight(config.raidiconsize)
|
|
if config.invertbars then
|
|
icon:SetPoint("LEFT", firstSection, "LEFT", 0, 0)
|
|
else
|
|
icon:SetPoint("RIGHT", firstSection, "RIGHT", 0, 0)
|
|
end
|
|
icon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
|
|
icon:Hide()
|
|
unitFrame.icon = icon
|
|
end
|
|
end
|
|
|
|
local function CreateBarSecondSection(unitFrame, guid)
|
|
local config = Cursive.db.profile
|
|
local secondSection = CreateFrame("Button", "Cursive2ndSection", unitFrame)
|
|
|
|
if config.invertbars then
|
|
-- When inverted, position relative to third section (which is created first)
|
|
secondSection:SetPoint("LEFT", unitFrame.thirdSection, "RIGHT", 0, 0)
|
|
else
|
|
-- Normal positioning relative to first section
|
|
secondSection:SetPoint("LEFT", unitFrame.firstSection, "RIGHT", 0, 0)
|
|
end
|
|
|
|
secondSection:SetWidth(GetBarSecondSectionWidth())
|
|
secondSection:SetHeight(config.height)
|
|
unitFrame.secondSection = secondSection
|
|
secondSection.parent = unitFrame
|
|
|
|
secondSection:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
secondSection:SetScript("OnClick", ui.BarClick)
|
|
secondSection:SetScript("OnEnter", ui.BarEnter)
|
|
secondSection:SetScript("OnLeave", ui.BarLeave)
|
|
|
|
-- create health bar
|
|
if config.showhealthbar then
|
|
local healthBar = CreateFrame("StatusBar", "CursiveHealthBar", secondSection)
|
|
healthBar:SetStatusBarTexture(config.modernhealth and "Interface\\AddOns\\Cursive\\img\\BlizzardRetailBar" or config.bartexture)
|
|
healthBar:SetStatusBarColor(1, .8, .2, 1)
|
|
healthBar:SetMinMaxValues(0, 100)
|
|
healthBar:SetValue(20)
|
|
healthBar:SetPoint("LEFT", secondSection, "LEFT", ui.padding, 0)
|
|
healthBar:SetWidth(config.healthwidth)
|
|
healthBar:SetHeight(config.height)
|
|
unitFrame.healthBar = healthBar
|
|
|
|
local hp = healthBar:CreateFontString(nil, "HIGH", "GameFontWhite")
|
|
hp:SetPoint("TOPRIGHT", healthBar, "TOPRIGHT", -2, -2)
|
|
hp:SetWidth(30)
|
|
hp:SetHeight(config.height - 4)
|
|
SafeSetFont(hp, GetUIFont(), config.textsize, "THINOUTLINE")
|
|
hp:SetJustifyH("RIGHT")
|
|
unitFrame.hpText = hp
|
|
|
|
if config.showunitname then
|
|
local name = healthBar:CreateFontString(nil, "HIGH", "GameFontWhite")
|
|
name:SetPoint("TOPLEFT", healthBar, "TOPLEFT", 2, -2)
|
|
name:SetPoint("BOTTOMRIGHT", hp, "BOTTOMLEFT", 2, 0)
|
|
SafeSetFont(name, GetUIFont(), config.textsize, "THINOUTLINE")
|
|
name:SetJustifyH("LEFT")
|
|
unitFrame.nameText = name
|
|
end
|
|
|
|
-- create health bar backdrops
|
|
if pfUI and pfUI.uf and not config.healthborder then
|
|
pfUI.api.CreateBackdrop(healthBar)
|
|
healthBar.border = healthBar.backdrop
|
|
else
|
|
healthBar:SetBackdrop(ui.background)
|
|
healthBar:SetBackdropColor(0, 0, 0, 1)
|
|
|
|
local border = CreateFrame("Frame", "CursiveBorder", healthBar)
|
|
border:SetBackdrop(config.healthborder and tooltipBorder or ui.border)
|
|
border:SetBackdropColor(.2, .2, .2, 1)
|
|
border:SetPoint("TOPLEFT", healthBar, "TOPLEFT", -2, 2)
|
|
border:SetPoint("BOTTOMRIGHT", healthBar, "BOTTOMRIGHT", 2, -2)
|
|
healthBar.border = border
|
|
end
|
|
else
|
|
if config.showunitname then
|
|
local name = secondSection:CreateFontString(nil, "HIGH", "GameFontWhite")
|
|
name:SetPoint("TOPLEFT", secondSection, "TOPLEFT", 2, -2)
|
|
name:SetPoint("BOTTOMRIGHT", secondSection, "BOTTOMRIGHT", 2, 0)
|
|
SafeSetFont(name, GetUIFont(), config.textsize, "THINOUTLINE")
|
|
name:SetWidth(config.healthwidth)
|
|
name:SetHeight(config.height - 4)
|
|
name:SetJustifyH("LEFT")
|
|
unitFrame.nameText = name
|
|
end
|
|
end
|
|
end
|
|
|
|
local function CreateBarThirdSection(unitFrame, guid)
|
|
local config = Cursive.db.profile
|
|
|
|
local thirdSection = CreateFrame("Frame", "Cursive3rdSection", unitFrame)
|
|
|
|
if config.invertbars then
|
|
-- When inverted, this is positioned first (leftmost)
|
|
thirdSection:SetPoint("LEFT", unitFrame, "LEFT", 0, 0)
|
|
else
|
|
-- Normal positioning relative to second section
|
|
thirdSection:SetPoint("LEFT", unitFrame.secondSection, "RIGHT", GetAuraGap(), 0)
|
|
end
|
|
|
|
thirdSection:SetWidth(GetBarThirdSectionWidth())
|
|
thirdSection:SetHeight(config.height)
|
|
thirdSection:EnableMouse(false)
|
|
unitFrame.thirdSection = thirdSection
|
|
|
|
-- display up to maxcurses curses
|
|
for i = 1, config.maxcurses do
|
|
local curse = thirdSection:CreateTexture(nil, "OVERLAY")
|
|
curse:SetWidth(config.curseiconsize)
|
|
curse:SetHeight(config.curseiconsize)
|
|
|
|
if config.invertbars then
|
|
-- When inverted, position from right to left
|
|
local rightOffset = i * ui.padding + ((i - 1) * config.curseiconsize)
|
|
curse:SetPoint("RIGHT", thirdSection, "RIGHT", -rightOffset, 0)
|
|
else
|
|
-- Normal positioning from left to right
|
|
curse:SetPoint("LEFT", thirdSection, "LEFT", i * ui.padding + ((i - 1) * config.curseiconsize), 0)
|
|
end
|
|
|
|
AddAuraBorder(curse, thirdSection)
|
|
curse.timer = thirdSection:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
|
curse.timer:SetFontObject(GameFontHighlight)
|
|
SafeSetFont(curse.timer, GetUIFont(), config.cursetimersize, "OUTLINE")
|
|
curse.timer:SetTextColor(1, 1, 1)
|
|
curse.timer:SetAllPoints(curse)
|
|
|
|
curse.timer:Hide()
|
|
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()
|
|
AddAuraBorder(ghost, thirdSection)
|
|
unitFrame.ghostIcons[i] = ghost
|
|
end
|
|
|
|
-- Shadow Vulnerability indicator: always claims slot 1 when enabled.
|
|
-- Dim (ghost-like) when inactive, brightens and flashes when actually
|
|
-- active -- position gets set dynamically on each refresh.
|
|
local shadowVulnIcon = thirdSection:CreateTexture(nil, "OVERLAY")
|
|
shadowVulnIcon:SetWidth(config.curseiconsize)
|
|
shadowVulnIcon:SetHeight(config.curseiconsize)
|
|
shadowVulnIcon:SetTexture("Interface\\Icons\\Spell_Shadow_ShadowBolt")
|
|
shadowVulnIcon:SetAlpha(0.3)
|
|
|
|
AddAuraBorder(shadowVulnIcon, thirdSection)
|
|
shadowVulnIcon.timer = thirdSection:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
|
shadowVulnIcon.timer:SetFontObject(GameFontHighlight)
|
|
SafeSetFont(shadowVulnIcon.timer, GetUIFont(), config.cursetimersize, "OUTLINE")
|
|
shadowVulnIcon.timer:SetTextColor(1, 1, 1)
|
|
shadowVulnIcon.timer:SetAllPoints(shadowVulnIcon)
|
|
shadowVulnIcon.timer:Hide()
|
|
|
|
shadowVulnIcon:Hide()
|
|
unitFrame.shadowVulnIcon = shadowVulnIcon
|
|
end
|
|
|
|
local function CreateBar(row, col, guid)
|
|
local unitFrame = CreateFrame("Frame", "CursiveUnitFrame", ui.rootBarFrame)
|
|
unitFrame.guid = guid
|
|
if Cursive.db.profile.showelitedragon then
|
|
local dragonLayer = CreateFrame("Frame", nil, unitFrame)
|
|
dragonLayer:EnableMouse(false)
|
|
local dragon = dragonLayer:CreateTexture(nil, "OVERLAY")
|
|
unitFrame.eliteDragonLayer = dragonLayer
|
|
dragon:SetWidth(30)
|
|
dragon:SetHeight(28 * 34 / 108)
|
|
-- Anchored after the health/name section has been constructed.
|
|
-- Two adjoining crops keep the crest and right wing, leaving out
|
|
-- the portrait ring and the bottom-left curl.
|
|
dragon:SetTexCoord(140 / 256, 1, 0, 34 / 128)
|
|
local tail = dragonLayer:CreateTexture(nil, "OVERLAY")
|
|
tail:SetWidth(30 * 36 / 116)
|
|
tail:SetHeight(28 * 74 / 108)
|
|
tail:SetTexCoord(220 / 256, 1, 34 / 128, 108 / 128)
|
|
tail:SetPoint("TOPRIGHT", dragon, "BOTTOMRIGHT", 0, 0)
|
|
tail:Hide()
|
|
unitFrame.eliteDragonTail = tail
|
|
dragon:Hide()
|
|
unitFrame.eliteDragon = dragon
|
|
end
|
|
|
|
unitFrame:SetScript("OnUpdate", ui.BarUpdate)
|
|
|
|
local config = Cursive.db.profile
|
|
local width = GetBarWidth()
|
|
unitFrame:SetWidth(width)
|
|
unitFrame:SetHeight(config.height)
|
|
|
|
local config = Cursive.db.profile
|
|
if config.invertbars then
|
|
-- Create sections in reverse order: 3 -> 2 -> 1
|
|
CreateBarThirdSection(unitFrame, guid)
|
|
CreateBarSecondSection(unitFrame, guid)
|
|
CreateBarFirstSection(unitFrame, guid)
|
|
else
|
|
-- Normal order: 1 -> 2 -> 3
|
|
CreateBarFirstSection(unitFrame, guid)
|
|
CreateBarSecondSection(unitFrame, guid)
|
|
CreateBarThirdSection(unitFrame, guid)
|
|
end
|
|
|
|
if unitFrame.eliteDragon then
|
|
-- Raise above the health StatusBar and its border child frame.
|
|
local anchor = unitFrame.healthBar or unitFrame.secondSection
|
|
local topLevel = anchor:GetFrameLevel()
|
|
if unitFrame.healthBar and unitFrame.healthBar.border then
|
|
topLevel = math.max(topLevel, unitFrame.healthBar.border:GetFrameLevel())
|
|
end
|
|
unitFrame.eliteDragonLayer:SetFrameLevel(topLevel + 2)
|
|
unitFrame.eliteDragonLayer:SetAllPoints(anchor)
|
|
-- Curl the ornament around the upper-right corner of the health/name bar.
|
|
unitFrame.eliteDragon:SetPoint("TOPRIGHT", unitFrame.healthBar or unitFrame.secondSection,
|
|
"TOPRIGHT", 8, 5)
|
|
end
|
|
ui.unitFrames[col][row] = unitFrame
|
|
return unitFrame
|
|
end
|
|
|
|
local function GetBarCords(row, col)
|
|
local config = Cursive.db.profile
|
|
local x = (col - 1) * GetBarWidth()
|
|
local y
|
|
if config.expandupwards then
|
|
-- For upward expansion: start from bottom with spacing, then go up
|
|
y = config.spacing + ((row - 1) * (config.height + config.spacing))
|
|
else
|
|
-- For downward expansion: use original logic (don't subtract 1 to account for header)
|
|
local gcdOffset = config.showgcdbar and (config.gcdbarheight + config.spacing) or 0
|
|
y = -(row * (config.height + config.spacing)) - gcdOffset
|
|
end
|
|
return x, y
|
|
end
|
|
|
|
local function hasAnySpellId(guid, spellIds)
|
|
local auras = GetUnitField(guid, "aura")
|
|
for i, spellId in pairs(auras) do
|
|
if spellIds[spellId] then
|
|
return spellId
|
|
end
|
|
end
|
|
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
|
|
|
|
-- UnitDebuff(guid, i) via the GUID extension has been confirmed (via live
|
|
-- /cgd diagnostic) to sometimes return nothing at all for a guid, even
|
|
-- when that guid IS the player's own current target. Unit tokens are
|
|
-- always reliable, so substitute one whenever the guid matches something
|
|
-- checkable -- covers being directly targeted, or being your target's
|
|
-- target (common on bosses when tanking/watching a tank). Other tracked
|
|
-- guids (raid members' own targets etc) still have no equivalent safe
|
|
-- substitute without a broader token scan. Note: "focus" was tried here
|
|
-- too but removed -- confirmed via live error that vanilla 1.12 has no
|
|
-- focus frame support at all (that's a later-expansion addition), and
|
|
-- UnitExists("focus") hard-errors here rather than just returning nil.
|
|
local function SafeUnitDebuff(guid, i)
|
|
if Cursive.db.profile.useclassicapi and C_UnitAuras and C_UnitAuras.UnitAura then
|
|
local _, targetGuid = UnitExists("target")
|
|
if guid == targetGuid then
|
|
local name, texture = C_UnitAuras.UnitAura("target", i, "HARMFUL")
|
|
return texture
|
|
end
|
|
end
|
|
local _, targetGuid = UnitExists("target")
|
|
if targetGuid and guid == targetGuid then
|
|
return UnitDebuff("target", i)
|
|
end
|
|
local _, totGuid = UnitExists("targettarget")
|
|
if totGuid and guid == totGuid then
|
|
return UnitDebuff("targettarget", i)
|
|
end
|
|
return UnitDebuff(guid, i)
|
|
end
|
|
|
|
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 = SafeUnitDebuff(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 = {}
|
|
for key in pairs(guidCurses) do
|
|
table.insert(curseNames, key)
|
|
end
|
|
|
|
if Cursive.db.profile.curseordering == L["Order applied"] then
|
|
table.sort(curseNames, function(a, b)
|
|
return guidCurses[a].start < guidCurses[b].start
|
|
end)
|
|
elseif Cursive.db.profile.curseordering == L["Expiring soonest -> latest"] then
|
|
table.sort(curseNames, function(a, b)
|
|
return Cursive.curses:TimeRemaining(guidCurses[a]) < Cursive.curses:TimeRemaining(guidCurses[b])
|
|
end)
|
|
elseif Cursive.db.profile.curseordering == L["Expiring latest -> soonest"] then
|
|
table.sort(curseNames, function(a, b)
|
|
return Cursive.curses:TimeRemaining(guidCurses[a]) > Cursive.curses:TimeRemaining(guidCurses[b])
|
|
end)
|
|
end
|
|
|
|
local i = 0
|
|
return function()
|
|
i = i + 1
|
|
local key = curseNames[i]
|
|
if key then
|
|
return key, guidCurses[key]
|
|
end
|
|
end
|
|
end
|
|
|
|
local function DisplayGuid(guid)
|
|
if not ui.unitFrames[ui.col] then
|
|
ui.unitFrames[ui.col] = {}
|
|
end
|
|
|
|
local unitFrame
|
|
if ui.unitFrames[ui.col][ui.row] then
|
|
unitFrame = ui.unitFrames[ui.col][ui.row]
|
|
unitFrame.guid = guid
|
|
else
|
|
unitFrame = CreateBar(ui.row, ui.col, guid)
|
|
ui.unitFrames[ui.col][ui.row] = unitFrame
|
|
end
|
|
|
|
local x, y = GetBarCords(ui.row, ui.col)
|
|
|
|
-- update position if required
|
|
local config = Cursive.db.profile
|
|
if not unitFrame.pos or unitFrame.pos ~= x .. y then
|
|
unitFrame:ClearAllPoints()
|
|
if config.expandupwards then
|
|
unitFrame:SetPoint("BOTTOMLEFT", ui.rootBarFrame, "BOTTOMLEFT", x, y)
|
|
else
|
|
unitFrame:SetPoint("TOPLEFT", ui.rootBarFrame, "TOPLEFT", x, y)
|
|
end
|
|
unitFrame.pos = x .. y
|
|
end
|
|
|
|
-- check for shared debuffs
|
|
for sharedDebuffKey, guids in pairs(Cursive.curses.sharedDebuffGuids) do
|
|
if guids[guid] then
|
|
local sharedDebuffSpellIds = Cursive.curses.sharedDebuffs[sharedDebuffKey]
|
|
local spellId = hasAnySpellId(guid, sharedDebuffSpellIds)
|
|
if spellId ~= nil then
|
|
-- add curse to curses
|
|
Cursive.curses:ApplySharedCurse(sharedDebuffKey, spellId, guid, GetTime())
|
|
-- remove guid
|
|
Cursive.curses.sharedDebuffGuids[sharedDebuffKey][guid] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- update curses
|
|
local cfg = Cursive.db.profile
|
|
-- Shadow Vulnerability always claims slot 1 when enabled, so real
|
|
-- curses/ghosts start filling from slot 2 instead.
|
|
local curseNumber = cfg.notifyshadowvuln and 2 or 1
|
|
|
|
-- Shadow Vulnerability indicator: always visible (dim) once enabled,
|
|
-- brightens and flashes when actually active, rather than showing/
|
|
-- hiding entirely -- reuses the same flash-ticker infrastructure as
|
|
-- the (currently unused) curse-expiring flash.
|
|
if cfg.notifyshadowvuln then
|
|
local hasShadowVuln = false
|
|
local i = 1
|
|
while true do
|
|
local tex = SafeUnitDebuff(guid, i)
|
|
if not tex then break end
|
|
if string.find(tex, "Spell_Shadow_ShadowBolt", 1, true) then
|
|
hasShadowVuln = true
|
|
break
|
|
end
|
|
i = i + 1
|
|
end
|
|
-- Preview mode: force the flashing state regardless of real debuff
|
|
-- presence while previewing a flash-speed change in settings.
|
|
if ui.shadowVulnPreviewUntil and GetTime() < ui.shadowVulnPreviewUntil then
|
|
hasShadowVuln = true
|
|
end
|
|
unitFrame.shadowVulnIcon:ClearAllPoints()
|
|
if cfg.invertbars then
|
|
unitFrame.shadowVulnIcon:SetPoint("RIGHT", unitFrame.thirdSection, "RIGHT", -(1 * ui.padding), 0)
|
|
else
|
|
unitFrame.shadowVulnIcon:SetPoint("LEFT", unitFrame.thirdSection, "LEFT", 1 * ui.padding, 0)
|
|
end
|
|
unitFrame.shadowVulnIcon:Show()
|
|
if hasShadowVuln then
|
|
if not unitFrame.shadowVulnActive then
|
|
-- Newly appeared -- start our own countdown, since this
|
|
-- client has no API for a third party's remaining aura
|
|
-- duration. Wowhead's classic spell page listed 12 seconds,
|
|
-- but real observed behavior showed the icon disappearing
|
|
-- ~3s before that countdown hit zero -- adjusted to 9s to
|
|
-- match what's actually been seen in practice (Wowhead's
|
|
-- value may reflect a different talent rank, or this
|
|
-- server's own tuning differs from stock classic data).
|
|
unitFrame.shadowVulnAppliedAt = GetTime()
|
|
end
|
|
unitFrame.shadowVulnActive = true
|
|
local remaining = SHADOW_VULN_DURATION - (GetTime() - unitFrame.shadowVulnAppliedAt)
|
|
if remaining > 0 then
|
|
unitFrame.shadowVulnIcon.timer:SetText(math.ceil(remaining))
|
|
unitFrame.shadowVulnIcon.timer:Show()
|
|
else
|
|
unitFrame.shadowVulnIcon.timer:Hide()
|
|
end
|
|
ui.flashingCurseIcons[unitFrame.shadowVulnIcon] = cfg.shadowvulnflashspeed
|
|
else
|
|
unitFrame.shadowVulnActive = false
|
|
ui.flashingCurseIcons[unitFrame.shadowVulnIcon] = nil
|
|
unitFrame.shadowVulnIcon:SetAlpha(0.3)
|
|
unitFrame.shadowVulnIcon.timer:Hide()
|
|
end
|
|
else
|
|
unitFrame.shadowVulnIcon:Hide()
|
|
unitFrame.shadowVulnIcon.timer:Hide()
|
|
unitFrame.shadowVulnActive = false
|
|
ui.flashingCurseIcons[unitFrame.shadowVulnIcon] = nil
|
|
end
|
|
|
|
-- make sure old curses are hidden
|
|
for i = 1, Cursive.db.profile.maxcurses do
|
|
local curse = unitFrame["curse" .. i]
|
|
curse:Hide()
|
|
curse.timer:Hide()
|
|
ui.flashingCurseIcons[curse] = nil
|
|
curse:SetAlpha(1)
|
|
end
|
|
|
|
local guidCurses = Cursive.curses.guids[guid]
|
|
if guidCurses then
|
|
for curseName, curseData in GetSortedCurses(guidCurses) do
|
|
if curseNumber > Cursive.db.profile.maxcurses then
|
|
break
|
|
end
|
|
|
|
local remaining = Cursive.curses:TimeRemaining(curseData)
|
|
local curse = unitFrame["curse" .. curseNumber]
|
|
if remaining >= 0 then
|
|
curse:SetTexture(Cursive.curses.trackedCurseIds[curseData.spellID].texture)
|
|
|
|
if curseData["currentPlayer"] == false then
|
|
curse:SetDesaturated(true); -- desaturate if not applied by current player
|
|
curse:SetAlpha(Cursive.db.profile.othercursealpha or 1)
|
|
else
|
|
curse:SetDesaturated(false); -- saturate if applied by current player
|
|
curse:SetAlpha(1)
|
|
end
|
|
|
|
-- curse:SetTexCoord(.078, .92, .079, .937) rounded icons
|
|
curse.timer:SetText(remaining)
|
|
curse.timer:Show()
|
|
curse:Show()
|
|
|
|
if remaining < 1 then
|
|
if Cursive.curses:ShouldPlayExpiringSound(curseName, guid) then
|
|
PlaySoundFile("Interface\\AddOns\\Cursive\\sounds\\expiring.mp3")
|
|
end
|
|
else
|
|
if Cursive.curses:HasRequestedExpiringSound(curseName, guid) then
|
|
Cursive.curses:EnableExpiringSound(curseName, guid)
|
|
end
|
|
end
|
|
|
|
-- Flash check: independent, wider threshold than the sound's <1.
|
|
-- Confirmed via live diagnostic earlier that "remaining" is
|
|
-- integer-quantized and stays at 2 for nearly a full second --
|
|
-- using <=2 catches that reliably-observed value directly, rather
|
|
-- than gambling on 1/0 ever actually appearing before the curse
|
|
-- disappears from the list entirely.
|
|
if Cursive.db.profile.flashonexpiring and remaining <= 2 then
|
|
ui.flashingCurseIcons[curse] = 3
|
|
else
|
|
ui.flashingCurseIcons[curse] = nil
|
|
curse:SetAlpha(1)
|
|
end
|
|
|
|
curseNumber = curseNumber + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- 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:SetDesaturated(cfg.ghosticongreyscale and true or false)
|
|
ghostIcon:Show()
|
|
slot = slot + 1
|
|
end
|
|
end
|
|
|
|
for i = 1, Cursive.db.profile.maxcurses do
|
|
UpdateAuraBorder(unitFrame["curse" .. i])
|
|
UpdateAuraBorder(unitFrame.ghostIcons[i])
|
|
end
|
|
UpdateAuraBorder(unitFrame.shadowVulnIcon)
|
|
if unitFrame.healthBar and Cursive.db.profile.healthborder then ThemeBorder(unitFrame.healthBar.border) end
|
|
unitFrame:Show()
|
|
ui.numDisplayed = ui.numDisplayed + 1
|
|
|
|
local config = Cursive.db.profile
|
|
|
|
-- update row/col
|
|
ui.row = ui.row + 1
|
|
if ui.row > config.maxrow then
|
|
ui.row = 1
|
|
ui.col = ui.col + 1
|
|
if ui.col > config.maxcol then
|
|
ui.maxBarsDisplayed = true
|
|
end
|
|
end
|
|
end
|
|
|
|
local function CheckForCleanup(guid, time)
|
|
local active = UnitExists(guid) and Cursive.filter.alive(guid)
|
|
if active then
|
|
local old = GetTime() - time >= 900 -- >= 15 minutes old
|
|
if old and not UnitIsVisible(guid) then
|
|
active = false
|
|
end
|
|
end
|
|
|
|
if not active then
|
|
-- remove from core
|
|
Cursive.core.remove(guid)
|
|
-- remove from curses
|
|
Cursive.curses:RemoveGuid(guid)
|
|
|
|
-- remove from sharedDebuffGuids
|
|
for sharedDebuffKey, guids in pairs(Cursive.curses.sharedDebuffGuids) do
|
|
if guids[guid] then
|
|
Cursive.curses.sharedDebuffGuids[sharedDebuffKey][guid] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Session-only samples reuse the real row construction and layout.
|
|
function Cursive.SetTestMode(enabled)
|
|
Cursive.testMode = enabled and true or false
|
|
Cursive.ResetUnitFrames()
|
|
end
|
|
|
|
local testIcons = {
|
|
"Interface\\Icons\\Spell_Shadow_CurseOfSargeras",
|
|
"Interface\\Icons\\Spell_Shadow_AbominationExplosion",
|
|
"Interface\\Icons\\Spell_Fire_Immolation",
|
|
"Interface\\Icons\\Spell_Shadow_Requiem",
|
|
}
|
|
local function RenderTestBars()
|
|
local config = Cursive.db.profile
|
|
local now = GetTime()
|
|
for col = 1, config.maxcol do
|
|
ui.unitFrames[col] = ui.unitFrames[col] or {}
|
|
for row = 1, config.maxrow do
|
|
local bar = ui.unitFrames[col][row]
|
|
if not bar then
|
|
bar = CreateBar(row, col, nil)
|
|
bar.testMode = true
|
|
bar:SetScript("OnUpdate", nil)
|
|
end
|
|
UpdateEliteDragon(bar, row == 1 and "elite" or (row == 2 and "rareelite" or "normal"))
|
|
local x, y = GetBarCords(row, col)
|
|
bar:ClearAllPoints()
|
|
bar:SetPoint(config.expandupwards and "BOTTOMLEFT" or "TOPLEFT", ui.rootBarFrame,
|
|
config.expandupwards and "BOTTOMLEFT" or "TOPLEFT", x, y)
|
|
if bar.nameText then bar.nameText:SetText(row == 1 and "Test: Defias Raider" or "Test: Training Dummy") end
|
|
if bar.hpText then bar.hpText:SetText("2453") end
|
|
if bar.healthBar then
|
|
bar.healthBar:SetMinMaxValues(0, 4000)
|
|
bar.healthBar:SetValue(2453)
|
|
bar.healthBar:SetStatusBarColor(0.8, 0.2 + math.mod(row, 3) * 0.2, 0.2, 1)
|
|
if config.healthborder then ThemeBorder(bar.healthBar.border) end
|
|
end
|
|
if bar.icon then SetRaidTargetIconTexture(bar.icon, math.mod(row - 1, 8) + 1); bar.icon:Show() end
|
|
if bar.target_left then
|
|
if row == 1 then bar.target_left:Show() else bar.target_left:Hide() end
|
|
end
|
|
local offset = config.notifyshadowvuln and 1 or 0
|
|
if config.notifyshadowvuln then
|
|
bar.shadowVulnIcon:ClearAllPoints()
|
|
bar.shadowVulnIcon:SetPoint(config.invertbars and "RIGHT" or "LEFT", bar.thirdSection,
|
|
config.invertbars and "RIGHT" or "LEFT", config.invertbars and -ui.padding or ui.padding, 0)
|
|
bar.shadowVulnIcon:SetAlpha(1)
|
|
bar.shadowVulnIcon.timer:SetText("5")
|
|
bar.shadowVulnIcon.timer:Show()
|
|
bar.shadowVulnIcon:Show()
|
|
else bar.shadowVulnIcon:Hide() end
|
|
for i = 1, config.maxcurses do
|
|
local icon = bar["curse" .. i]
|
|
local slot = i + offset
|
|
if slot <= config.maxcurses then
|
|
local position = slot * ui.padding + (slot - 1) * config.curseiconsize
|
|
icon:ClearAllPoints()
|
|
icon:SetPoint(config.invertbars and "RIGHT" or "LEFT", bar.thirdSection,
|
|
config.invertbars and "RIGHT" or "LEFT", config.invertbars and -position or position, 0)
|
|
icon:SetTexture(testIcons[math.mod(i - 1, table.getn(testIcons)) + 1])
|
|
icon:SetAlpha(1)
|
|
local remaining = 18 - math.mod(now + i * 3, 18)
|
|
icon.timer:SetText(config.curseshowdecimals and string.format("%.1f", remaining) or math.ceil(remaining))
|
|
icon.timer:Show()
|
|
icon:Show()
|
|
else icon:Hide(); icon.timer:Hide() end
|
|
bar.ghostIcons[i]:Hide()
|
|
UpdateAuraBorder(icon)
|
|
end
|
|
UpdateAuraBorder(bar.shadowVulnIcon)
|
|
bar:Show()
|
|
end
|
|
end
|
|
end
|
|
|
|
local shouldDisplayGuids = {};
|
|
local displayedGuids = {};
|
|
|
|
ui:SetAllPoints()
|
|
ui:SetScript("OnUpdate", function()
|
|
local config = Cursive.db.profile
|
|
|
|
if not config.enabled and not Cursive.testMode then
|
|
return
|
|
end
|
|
|
|
if (this.tick or 1) > GetTime() then
|
|
return
|
|
else
|
|
this.tick = GetTime() + 0.1
|
|
end
|
|
|
|
if not ui.rootBarFrame then
|
|
ui.rootBarFrame = CreateRoot()
|
|
end
|
|
|
|
-- skip if locked (due to moving)
|
|
if ui.rootBarFrame.lock then
|
|
return
|
|
end
|
|
|
|
if Cursive.testMode then
|
|
RenderTestBars()
|
|
return
|
|
end
|
|
|
|
-- reset display data
|
|
ui.row = 1
|
|
ui.col = 1
|
|
ui.maxBarsDisplayed = false
|
|
ui.numDisplayed = 0
|
|
|
|
-- clear shouldDisplayGuids
|
|
for guid, _ in pairs(shouldDisplayGuids) do
|
|
shouldDisplayGuids[guid] = nil
|
|
end
|
|
|
|
-- clear displayedGuids
|
|
for guid, _ in pairs(displayedGuids) do
|
|
displayedGuids[guid] = nil
|
|
end
|
|
|
|
-- run through all guids and fill with bars
|
|
local title_size = 12 + config.spacing
|
|
|
|
local topMaxHp = 0
|
|
local secondMaxHp = 0
|
|
local thirdMaxHp = 0
|
|
|
|
local topMaxGuid = 0
|
|
local secondMaxGuid = 0
|
|
local thirdMaxGuid = 0
|
|
|
|
local numDisplayable = 0
|
|
|
|
local averageMaxHp = 0
|
|
|
|
local _, currentTargetGuid = UnitExists("target")
|
|
|
|
-- first consider raid marks
|
|
for i = 8, 1, -1 do
|
|
local _, guid = UnitExists("mark" .. i)
|
|
if guid then
|
|
if Cursive:ShouldDisplayGuid(guid) then
|
|
numDisplayable = numDisplayable + 1
|
|
|
|
-- display guid
|
|
displayedGuids[guid] = true
|
|
DisplayGuid(guid)
|
|
if ui.maxBarsDisplayed then
|
|
break
|
|
end
|
|
end
|
|
-- don't try to display this guid again
|
|
shouldDisplayGuids[guid] = false
|
|
end
|
|
end
|
|
|
|
for guid, time in pairs(Cursive.core.guids) do
|
|
-- calculate shouldDisplay
|
|
local shouldDisplay = false
|
|
if shouldDisplayGuids[guid] == nil then
|
|
shouldDisplay = Cursive:ShouldDisplayGuid(guid)
|
|
shouldDisplayGuids[guid] = shouldDisplay
|
|
|
|
if shouldDisplay then
|
|
numDisplayable = numDisplayable + 1
|
|
end
|
|
else
|
|
shouldDisplay = shouldDisplayGuids[guid]
|
|
end
|
|
|
|
-- calculate top 3 max hps
|
|
if shouldDisplay then
|
|
local maxHp = UnitHealthMax(guid)
|
|
if maxHp > topMaxHp then
|
|
thirdMaxHp = secondMaxHp
|
|
thirdMaxGuid = secondMaxGuid
|
|
secondMaxHp = topMaxHp
|
|
secondMaxGuid = topMaxGuid
|
|
topMaxHp = maxHp
|
|
topMaxGuid = guid
|
|
elseif maxHp > secondMaxHp then
|
|
thirdMaxHp = secondMaxHp
|
|
thirdMaxGuid = secondMaxGuid
|
|
secondMaxHp = maxHp
|
|
secondMaxGuid = guid
|
|
elseif maxHp > thirdMaxHp then
|
|
thirdMaxHp = maxHp
|
|
thirdMaxGuid = guid
|
|
end
|
|
else
|
|
CheckForCleanup(guid, time)
|
|
end
|
|
end
|
|
|
|
-- top max hp
|
|
if not ui.maxBarsDisplayed and numDisplayable > ui.numDisplayed and not displayedGuids[topMaxGuid] then
|
|
displayedGuids[topMaxGuid] = true
|
|
DisplayGuid(topMaxGuid)
|
|
end
|
|
|
|
-- second max hp
|
|
if not ui.maxBarsDisplayed and numDisplayable > ui.numDisplayed and not displayedGuids[secondMaxGuid] then
|
|
displayedGuids[secondMaxGuid] = true
|
|
DisplayGuid(secondMaxGuid)
|
|
end
|
|
|
|
-- third max hp
|
|
if not ui.maxBarsDisplayed and numDisplayable > ui.numDisplayed and not displayedGuids[thirdMaxGuid] then
|
|
displayedGuids[thirdMaxGuid] = true
|
|
|
|
DisplayGuid(thirdMaxGuid)
|
|
end
|
|
|
|
-- fill in remaining slots
|
|
for guid, time in pairs(Cursive.core.guids) do
|
|
if ui.maxBarsDisplayed or numDisplayable <= ui.numDisplayed then
|
|
break
|
|
end
|
|
|
|
if not displayedGuids[guid] and shouldDisplayGuids[guid] == true then
|
|
displayedGuids[guid] = true
|
|
DisplayGuid(guid)
|
|
end
|
|
end
|
|
|
|
-- if current target not yet displayed, show it at maxrow/maxcol
|
|
if currentTargetGuid and
|
|
shouldDisplayGuids[currentTargetGuid] and
|
|
not displayedGuids[currentTargetGuid] and
|
|
Cursive.db.profile.alwaysshowcurrenttarget then
|
|
-- replace the last displayed guid with the current target
|
|
displayedGuids[currentTargetGuid] = true
|
|
ui.col = config.maxcol
|
|
ui.row = config.maxrow
|
|
DisplayGuid(currentTargetGuid)
|
|
end
|
|
|
|
-- hide any remaining unit frames
|
|
for col, rows in pairs(ui.unitFrames) do
|
|
for row, unitFrame in pairs(rows) do
|
|
if unitFrame:IsShown() then
|
|
if not displayedGuids[unitFrame.guid] then
|
|
unitFrame:Hide()
|
|
else
|
|
displayedGuids[unitFrame.guid] = nil -- avoid displaying duplicate rows
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
end)
|
|
|
|
-- GCD swing-timer bar. Cursive.ui.StartGcdBar(duration) is called from
|
|
-- curses.lua's SPELL_GO_SELF handler, using nampower's precise
|
|
-- GetSpellIdCooldown() data at the moment a curse actually goes off.
|
|
-- This ticker just smoothly drains the bar from that snapshot every
|
|
-- frame (unlike the throttled 0.1s main UI loop above).
|
|
ui.gcdBarStart = nil
|
|
ui.gcdBarDuration = nil
|
|
|
|
ui.StartGcdBar = function(duration)
|
|
if not ui.rootBarFrame or not ui.rootBarFrame.gcdBar then
|
|
return
|
|
end
|
|
if not ui.rootBarFrame.gcdBar.enabled then
|
|
return
|
|
end
|
|
ui.gcdBarStart = GetTime()
|
|
ui.gcdBarDuration = 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
|
|
|
|
ui.shadowVulnPreviewUntil = nil
|
|
|
|
-- Call this from the flash-speed setting's set() callback to force any
|
|
-- currently-displayed Shadow Vulnerability icon into its flashing state
|
|
-- temporarily, regardless of real debuff presence -- only affects rows
|
|
-- that are actually displayed right now (requires a target/row visible).
|
|
function ui.PreviewShadowVulnFlash()
|
|
ui.shadowVulnPreviewUntil = 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 (Cursive.testMode and bar.enabled) or (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()
|
|
end
|
|
return
|
|
end
|
|
|
|
local remaining = (ui.gcdBarStart + ui.gcdBarDuration) - GetTime()
|
|
if remaining > 0 then
|
|
bar:SetMinMaxValues(0, ui.gcdBarDuration)
|
|
bar:SetValue(remaining)
|
|
if not bar:IsShown() then
|
|
bar:Show()
|
|
end
|
|
else
|
|
ui.gcdBarStart = nil
|
|
if bar:IsShown() then
|
|
bar:Hide()
|
|
end
|
|
end
|
|
end)
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Sacrifice shield tracking (Voidwalker's damage-absorb shield on the
|
|
-- warlock). Detection and initial-absorb-value reading use a tooltip scan
|
|
-- of the player's own buff (robust across all ranks, since it reads the
|
|
-- actual computed value including any Demonic Brutality talent bonus,
|
|
-- rather than guessing based on rank). Duration is a fixed 30s regardless
|
|
-- of rank (confirmed).
|
|
--
|
|
-- IMPORTANT CAVEAT: remaining-absorb tracking (damage taken while the
|
|
-- shield is up) is UNVERIFIED. Vanilla's combat log is chat-message-based
|
|
-- and its exact wording for absorbed hits hasn't been confirmed on this
|
|
-- server. Diagnostic mode (/cursivesacdebug) prints every combat message
|
|
-- received while the shield is active, so the real format can be seen and
|
|
-- the parsing refined -- until then, the absorb estimate may be inaccurate
|
|
-- or simply not update at all if the message format doesn't match what's
|
|
-- guessed below.
|
|
-------------------------------------------------------------------------------
|
|
|
|
local SACRIFICE_DURATION = 30
|
|
ui.sacrificeDebugMode = false
|
|
ui.cachedSacrificeAbsorb = nil
|
|
|
|
local sacrificeState = {
|
|
active = false,
|
|
initialAbsorb = 0,
|
|
appliedAt = 0,
|
|
damageTaken = 0,
|
|
}
|
|
|
|
-- The APPLIED buff's own tooltip is confirmed abbreviated ("Absorbs all
|
|
-- damage", no number) -- the real absorb value only appears in the full
|
|
-- spell description, shown on the Voidwalker's pet action bar. Since the
|
|
-- Voidwalker is consumed by the cast, this has to be scanned proactively
|
|
-- while it's still alive and cached, rather than read after the fact.
|
|
local function ScanPetBarForSacrifice()
|
|
local slot = 1
|
|
while slot <= 10 do
|
|
local button = getglobal("pfActionBarPetButton"..slot) or getglobal("PetActionButton"..slot)
|
|
if button and button:IsVisible() then
|
|
Cursive.core.tooltipScan:SetOwner(UIParent, "ANCHOR_NONE")
|
|
Cursive.core.tooltipScan:SetPetAction(slot)
|
|
local nameLine = getglobal("CursiveTooltipScanTextLeft1")
|
|
local nameText = nameLine and nameLine:GetText()
|
|
if nameText and string.find(nameText, "Sacrifice", 1, true) then
|
|
local lineNum = 2
|
|
while lineNum <= 6 do
|
|
local descLine = getglobal("CursiveTooltipScanTextLeft"..lineNum)
|
|
local desc = descLine and descLine:GetText()
|
|
if desc then
|
|
local _, _, num = string.find(desc, "absorb (%d+)")
|
|
if num then
|
|
if ui.sacrificeDebugMode and ui.cachedSacrificeAbsorb ~= tonumber(num) then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff60ffff[SacDebug]|r Cached Sacrifice absorb value from pet bar: "..num)
|
|
end
|
|
ui.cachedSacrificeAbsorb = tonumber(num)
|
|
Cursive.core.tooltipScan:Hide()
|
|
return
|
|
end
|
|
end
|
|
lineNum = lineNum + 1
|
|
end
|
|
end
|
|
Cursive.core.tooltipScan:Hide()
|
|
end
|
|
slot = slot + 1
|
|
end
|
|
end
|
|
|
|
-- One-shot diagnostic: dumps the full state of PetActionButton1-10 --
|
|
-- whether each exists, is visible, and what its tooltip shows (regardless
|
|
-- of whether it matches "Sacrifice"). Run this with your Voidwalker
|
|
-- summoned if the pet-bar scan above isn't finding anything -- likely
|
|
-- explanation is a UI replacement addon (e.g. pfUI) using different pet
|
|
-- bar frame names than the vanilla default.
|
|
-- Guessing frame naming conventions has failed twice -- this reports
|
|
-- exactly which frame is under the mouse cursor, no guessing involved.
|
|
-- Run the command, THEN hover over the Sacrifice icon on the pet bar
|
|
-- within 3 seconds (gives time to move the mouse after typing/hitting
|
|
-- enter, since GetMouseFocus needs the cursor there at the exact moment
|
|
-- it's checked).
|
|
local whatFrameChecker = CreateFrame("Frame")
|
|
SLASH_CURSIVEWHATFRAME1 = "/cursivewhatframe"
|
|
SlashCmdList["CURSIVEWHATFRAME"] = function()
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00Cursive:|r hover over the Sacrifice pet icon now -- checking in 3 seconds...")
|
|
local elapsed = 0
|
|
whatFrameChecker:SetScript("OnUpdate", function()
|
|
elapsed = elapsed + arg1
|
|
if elapsed < 3 then return end
|
|
whatFrameChecker:SetScript("OnUpdate", nil)
|
|
|
|
local frame = GetMouseFocus()
|
|
if not frame then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00Cursive:|r no frame under mouse")
|
|
return
|
|
end
|
|
local name = frame.GetName and frame:GetName()
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00Cursive:|r frame under mouse: "..tostring(name))
|
|
local parent = frame.GetParent and frame:GetParent()
|
|
if parent then
|
|
local parentName = parent.GetName and parent:GetName()
|
|
DEFAULT_CHAT_FRAME:AddMessage(" parent: "..tostring(parentName))
|
|
end
|
|
end)
|
|
end
|
|
|
|
SLASH_CURSIVEPETDEBUG1 = "/cursivepetdebug"
|
|
SlashCmdList["CURSIVEPETDEBUG"] = function()
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00Cursive pet bar debug|r")
|
|
local prefixes = { "pfActionBarPetButton", "PetActionButton", "ActionBarPetButton", "ActionBarPet" }
|
|
local anyFound = false
|
|
local pIdx = 1
|
|
while pIdx <= 4 do
|
|
local prefix = prefixes[pIdx]
|
|
local slot = 1
|
|
while slot <= 10 do
|
|
local buttonName = prefix..slot
|
|
local button = getglobal(buttonName)
|
|
if button then
|
|
anyFound = true
|
|
local visible = button:IsVisible()
|
|
DEFAULT_CHAT_FRAME:AddMessage(" "..buttonName..": exists, visible="..tostring(visible))
|
|
if visible then
|
|
-- Try SetPetAction directly on this slot number (works
|
|
-- regardless of the button's frame naming convention,
|
|
-- since SetPetAction addresses pet action slots by
|
|
-- index, not frame name).
|
|
Cursive.core.tooltipScan:SetOwner(UIParent, "ANCHOR_NONE")
|
|
local ok = pcall(function() Cursive.core.tooltipScan:SetPetAction(slot) end)
|
|
local nameLine = getglobal("CursiveTooltipScanTextLeft1")
|
|
local nameText = nameLine and nameLine:GetText()
|
|
DEFAULT_CHAT_FRAME:AddMessage(" SetPetAction("..slot..") tooltip name: "..tostring(nameText).." (call ok: "..tostring(ok)..")")
|
|
Cursive.core.tooltipScan:Hide()
|
|
end
|
|
end
|
|
slot = slot + 1
|
|
end
|
|
pIdx = pIdx + 1
|
|
end
|
|
if not anyFound then
|
|
DEFAULT_CHAT_FRAME:AddMessage(" None of PetActionButton/ActionBarPetButton/ActionBarPet 1-10 exist -- naming convention is something else entirely.")
|
|
end
|
|
end
|
|
|
|
local function ScanForSacrificeBuff()
|
|
local found = false
|
|
local i = 1
|
|
while true do
|
|
local tex = UnitBuff("player", i)
|
|
if not tex then break end
|
|
Cursive.core.tooltipScan:SetOwner(UIParent, "ANCHOR_NONE")
|
|
Cursive.core.tooltipScan:SetUnitBuff("player", i)
|
|
local nameLine = getglobal("CursiveTooltipScanTextLeft1")
|
|
local name = nameLine and nameLine:GetText()
|
|
if name and string.find(name, "Sacrifice", 1, true) then
|
|
found = true
|
|
if not sacrificeState.active then
|
|
sacrificeState.active = true
|
|
sacrificeState.appliedAt = GetTime()
|
|
sacrificeState.damageTaken = 0
|
|
sacrificeState.initialAbsorb = ui.cachedSacrificeAbsorb or 0
|
|
|
|
if ui.sacrificeDebugMode then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff60ffff[SacDebug]|r Shield applied, using cached absorb value: "..tostring(ui.cachedSacrificeAbsorb))
|
|
end
|
|
end
|
|
break
|
|
end
|
|
i = i + 1
|
|
end
|
|
Cursive.core.tooltipScan:Hide()
|
|
if not found and sacrificeState.active then
|
|
if ui.sacrificeDebugMode then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff60ffff[SacDebug]|r Shield no longer detected (expired or broken)")
|
|
end
|
|
sacrificeState.active = false
|
|
end
|
|
return found
|
|
end
|
|
|
|
-- Diagnostic: logs every combat message received while the shield is
|
|
-- active, to determine the real absorb-message wording on this server.
|
|
-- Also attempts a best-effort parse (any number immediately preceding or
|
|
-- following "absorb") to estimate damage taken, but this is unverified.
|
|
local sacDebugFrame = CreateFrame("Frame")
|
|
-- Confirmed (via community-documented vanilla 1.12.1 event listings) as
|
|
-- the correct events for damage TAKEN by the player -- the original guess
|
|
-- above was wrong: CHAT_MSG_SPELL_SELF_DAMAGE and CHAT_MSG_COMBAT_SELF_HITS
|
|
-- report damage the player DEALS to others, not damage taken, which is why
|
|
-- zero debug messages ever fired despite the player clearly taking damage.
|
|
sacDebugFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS") -- melee from a creature to the player
|
|
sacDebugFrame:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE") -- spell from a creature to the player
|
|
sacDebugFrame:RegisterEvent("CHAT_MSG_COMBAT_HOSTILEPLAYER_HITS") -- melee from a hostile player (PvP)
|
|
sacDebugFrame:RegisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE") -- spell from a hostile player (PvP)
|
|
sacDebugFrame:SetScript("OnEvent", function()
|
|
if not sacrificeState.active then return end
|
|
local msg = arg1
|
|
if not msg then return end
|
|
|
|
if ui.sacrificeDebugMode then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff60ffff[SacDebug]|r ["..event.."] "..msg)
|
|
end
|
|
|
|
if string.find(string.lower(msg), "absorb", 1, true) then
|
|
local _, _, num = string.find(msg, "%((%d+) absorbed%)")
|
|
if num then
|
|
sacrificeState.damageTaken = sacrificeState.damageTaken + tonumber(num)
|
|
if ui.sacrificeDebugMode then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff60ffff[SacDebug]|r Parsed absorbed amount: "..num..", running total: "..sacrificeState.damageTaken)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
SLASH_CURSIVESACDEBUG1 = "/cursivesacdebug"
|
|
SlashCmdList["CURSIVESACDEBUG"] = function()
|
|
ui.sacrificeDebugMode = not ui.sacrificeDebugMode
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00Cursive|r sacrifice debug mode: "..(ui.sacrificeDebugMode and "|cff00ff00ON|r" or "|cffff6060OFF|r"))
|
|
end
|
|
|
|
-- Standalone, draggable Sacrifice shield bar. Depletes visually with time
|
|
-- remaining (same mechanic as the GCD bar); the damage-remaining estimate
|
|
-- is shown as text overlaid on the bar rather than driving the bar's fill,
|
|
-- since only the time value is fully reliable.
|
|
-- Repositions the label/absorb text based on orientation. Horizontal keeps
|
|
-- them side-by-side inside the bar; vertical stacks them below the bar
|
|
-- instead, since a narrow vertical bar can't fit readable text inside it.
|
|
local function UpdateSacrificeBarTextLayout(bar, isVertical)
|
|
bar.label:ClearAllPoints()
|
|
bar.absorbText:ClearAllPoints()
|
|
if isVertical then
|
|
bar.label:SetPoint("Top", bar, "Bottom", 0, -4)
|
|
bar.label:SetJustifyH("Center")
|
|
bar.absorbText:SetPoint("Top", bar.label, "Bottom", 0, -2)
|
|
bar.absorbText:SetJustifyH("Center")
|
|
else
|
|
bar.label:SetPoint("Left", bar, "Left", 4, 0)
|
|
bar.label:SetJustifyH("Left")
|
|
bar.absorbText:SetPoint("Right", bar, "Right", -4, 0)
|
|
bar.absorbText:SetJustifyH("Right")
|
|
end
|
|
end
|
|
|
|
local function CreateSacrificeBar()
|
|
local p = Cursive.db.profile
|
|
|
|
local bar = CreateFrame("StatusBar", "CursiveSacrificeBar", UIParent)
|
|
bar:SetWidth(p.sacrificebarwidth or 200)
|
|
bar:SetHeight(p.sacrificebarheight or 14)
|
|
|
|
if p.sacrificebaranchor then
|
|
bar:SetPoint(p.sacrificebaranchor, UIParent, p.sacrificebaranchor, p.sacrificebarx or 0, p.sacrificebary or 0)
|
|
else
|
|
bar:SetPoint("Center", UIParent, "Center", 0, -100)
|
|
end
|
|
|
|
bar:SetStatusBarTexture(Cursive.db.profile.bartexture)
|
|
bar:SetOrientation(p.sacrificebarorientation or "HORIZONTAL")
|
|
local c = p.sacrificebarcolor or { r = 0.6, g = 0.2, b = 0.85 }
|
|
bar:SetStatusBarColor(c.r, c.g, c.b, 0.9)
|
|
bar:SetMinMaxValues(0, 1)
|
|
bar:SetValue(0)
|
|
|
|
-- Border color reflects % of absorb remaining (green -> red), as an
|
|
-- at-a-glance cue independent of the text number.
|
|
bar:SetBackdrop({
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
edgeSize = 12,
|
|
})
|
|
bar:SetBackdropBorderColor(0, 1, 0, 1)
|
|
|
|
local bg = bar:CreateTexture(nil, "BACKGROUND")
|
|
bg:SetAllPoints(bar)
|
|
bg:SetTexture(0, 0, 0)
|
|
bg:SetAlpha(0.4)
|
|
|
|
local label = bar:CreateFontString(nil, "OVERLAY")
|
|
SafeSetFont(label, GetUIFont(), 10, "OUTLINE")
|
|
label:SetTextColor(1, 1, 1, 1)
|
|
label:SetText("Sacrifice")
|
|
|
|
local absorbText = bar:CreateFontString(nil, "OVERLAY")
|
|
SafeSetFont(absorbText, GetUIFont(), 10, "OUTLINE")
|
|
absorbText:SetTextColor(1, 1, 1, 1)
|
|
|
|
bar.label = label
|
|
bar.absorbText = absorbText
|
|
UpdateSacrificeBarTextLayout(bar, (p.sacrificebarorientation == "VERTICAL"))
|
|
|
|
bar:EnableMouse(true)
|
|
bar:SetMovable(true)
|
|
bar:RegisterForDrag("LeftButton")
|
|
bar:SetScript("OnDragStart", function()
|
|
this:StartMoving()
|
|
end)
|
|
bar:SetScript("OnDragStop", function()
|
|
this:StopMovingOrSizing()
|
|
local newAnchor = utils.GetBestAnchor(this)
|
|
local anchor, x, y = utils.ConvertFrameAnchor(this, newAnchor)
|
|
this:ClearAllPoints()
|
|
this:SetPoint(anchor, UIParent, anchor, x, y)
|
|
|
|
Cursive.db.profile.sacrificebaranchor = anchor
|
|
Cursive.db.profile.sacrificebarx = x
|
|
Cursive.db.profile.sacrificebary = y
|
|
end)
|
|
|
|
bar:Hide()
|
|
ui.sacrificeBar = bar
|
|
return bar
|
|
end
|
|
|
|
ui.UpdateSacrificeBarTextLayout = UpdateSacrificeBarTextLayout
|
|
|
|
local sacrificeTicker = CreateFrame("Frame", "CursiveSacrificeTicker", UIParent)
|
|
local sacrificeTickAccum = 0
|
|
ui.sacrificeBarPreviewUntil = nil
|
|
|
|
-- Call this from any Sacrifice bar setting's set() callback to show a
|
|
-- temporary static preview fill, since the real bar normally only
|
|
-- appears while the shield is actually active -- lets color/size changes
|
|
-- be seen immediately.
|
|
function ui.PreviewSacrificeBar()
|
|
ui.sacrificeBarPreviewUntil = GetTime() + 4
|
|
end
|
|
|
|
sacrificeTicker:SetScript("OnUpdate", function()
|
|
sacrificeTickAccum = sacrificeTickAccum + arg1
|
|
if sacrificeTickAccum < 0.5 then return end
|
|
sacrificeTickAccum = 0
|
|
|
|
ScanPetBarForSacrifice()
|
|
ScanForSacrificeBuff()
|
|
|
|
if not ui.sacrificeBar then
|
|
CreateSacrificeBar()
|
|
end
|
|
local bar = ui.sacrificeBar
|
|
|
|
-- Preview mode: triggered directly by changing a Sacrifice bar
|
|
-- setting, rather than trying to detect whether the options menu is
|
|
-- open.
|
|
if ui.sacrificeBarPreviewUntil and GetTime() < ui.sacrificeBarPreviewUntil then
|
|
bar:SetMinMaxValues(0, 30)
|
|
bar:SetValue(18)
|
|
bar.absorbText:SetText("18s | 1200 absorb")
|
|
if not bar:IsShown() then bar:Show() end
|
|
return
|
|
end
|
|
|
|
if not sacrificeState.active then
|
|
if bar:IsShown() then bar:Hide() end
|
|
return
|
|
end
|
|
|
|
local remaining = SACRIFICE_DURATION - (GetTime() - sacrificeState.appliedAt)
|
|
if remaining <= 0 then
|
|
sacrificeState.active = false
|
|
bar:Hide()
|
|
return
|
|
end
|
|
|
|
if not bar:IsShown() then bar:Show() end
|
|
bar:SetMinMaxValues(0, SACRIFICE_DURATION)
|
|
bar:SetValue(remaining)
|
|
|
|
local remainingAbsorb = sacrificeState.initialAbsorb - sacrificeState.damageTaken
|
|
if remainingAbsorb < 0 then remainingAbsorb = 0 end
|
|
bar.absorbText:SetText(math.floor(remaining + 1).."s | "..remainingAbsorb.." absorb")
|
|
|
|
if sacrificeState.initialAbsorb > 0 then
|
|
local pct = remainingAbsorb / sacrificeState.initialAbsorb
|
|
if pct > 1 then pct = 1 end
|
|
bar.borderR, bar.borderG, bar.borderB = 1 - pct, pct, 0
|
|
else
|
|
bar.borderR, bar.borderG, bar.borderB = 0.5, 0.5, 0.5
|
|
end
|
|
end)
|
|
|
|
-- Glowing border: pulses the border alpha smoothly via sin() oscillation.
|
|
-- Runs every frame (unthrottled), separate from the main 0.5s update
|
|
-- above, since a visible pulse needs much more frequent updates than the
|
|
-- bar's actual data does.
|
|
local sacrificeGlowTicker = CreateFrame("Frame", "CursiveSacrificeGlowTicker", UIParent)
|
|
sacrificeGlowTicker:SetScript("OnUpdate", function()
|
|
local bar = ui.sacrificeBar
|
|
if not bar or not bar:IsShown() then return end
|
|
local r = bar.borderR or 0.5
|
|
local g = bar.borderG or 0.5
|
|
local b = bar.borderB or 0.5
|
|
local alpha = 0.5 + 0.5 * math.sin(GetTime() * 4)
|
|
bar:SetBackdropBorderColor(r, g, b, alpha)
|
|
end)
|
|
|
|
-- Flashes any icon currently registered, via a smooth alpha oscillation.
|
|
-- Single shared ticker for all rows rather than one per icon. Registry
|
|
-- values are a speed multiplier (falls back to 3 if just `true`), so
|
|
-- different icons can flash at different rates using the same ticker.
|
|
local flashTicker = CreateFrame("Frame", "CursiveFlashTicker", UIParent)
|
|
flashTicker:SetScript("OnUpdate", function()
|
|
local any = false
|
|
for icon in pairs(ui.flashingCurseIcons) do
|
|
any = true
|
|
break
|
|
end
|
|
if not any then
|
|
return
|
|
end
|
|
local now = GetTime()
|
|
for icon, speed in pairs(ui.flashingCurseIcons) do
|
|
local s = (type(speed) == "number") and speed or 3
|
|
local alpha = 0.35 + (0.65 * (0.5 + 0.5 * math.sin(now * s)))
|
|
icon:SetAlpha(alpha)
|
|
end
|
|
end)
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Standalone settings window (alternative to the Dewdrop dropdown menu).
|
|
-- Generic renderer reads the EXISTING Ace2-style options table (Cursive.
|
|
-- cmdtable) directly rather than hand-transcribing every setting, so it
|
|
-- automatically stays in sync with whatever settings.lua defines.
|
|
-------------------------------------------------------------------------------
|
|
|
|
local settingsWindow = nil
|
|
|
|
-- Ace2 options entries can specify get/set as either a direct function, or
|
|
-- a STRING naming a method to call on a "handler" object (falling back to
|
|
-- Cursive itself, since Cursive.cmdtable.handler = Cursive). This resolves
|
|
-- either form correctly. Fixed-arity (not vararg) since this client runs
|
|
-- Lua 5.0, where "..." can't be used directly as a forwarded expression
|
|
-- the way Lua 5.1 allows -- 3 args covers every case used here (color
|
|
-- needs r,g,b; toggle/range need at most 1; get calls need 0).
|
|
local function CallGetSet(entry, isSet, a1, a2, a3)
|
|
local fn = isSet and entry.set or entry.get
|
|
if type(fn) == "function" then
|
|
return fn(a1, a2, a3)
|
|
elseif type(fn) == "string" then
|
|
local handler = entry.handler or Cursive
|
|
return handler[fn](handler, a1, a2, a3)
|
|
end
|
|
end
|
|
|
|
local function MakeHeaderWidget(parent, text, yOff)
|
|
local hdr = parent:CreateFontString(nil, "OVERLAY")
|
|
hdr:SetFont(STANDARD_TEXT_FONT, 10, "OUTLINE")
|
|
hdr:SetTextColor(0.7, 0.5, 1, 1)
|
|
hdr:SetPoint("TopLeft", parent, "TopLeft", 4, yOff)
|
|
hdr:SetText(text)
|
|
|
|
local divider = parent:CreateTexture(nil, "ARTWORK")
|
|
divider:SetHeight(1)
|
|
divider:SetPoint("TopLeft", parent, "TopLeft", 4, yOff - 12)
|
|
divider:SetPoint("TopRight", parent, "TopRight", -4, yOff - 12)
|
|
divider:SetTexture(0.3, 0.15, 0.5, 0.8)
|
|
|
|
return 18
|
|
end
|
|
|
|
local function MakeToggleWidget(parent, entry, yOff)
|
|
local cb = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
|
|
cb:SetWidth(20) cb:SetHeight(20)
|
|
cb:SetPoint("TopLeft", parent, "TopLeft", 8, yOff)
|
|
cb:SetChecked(CallGetSet(entry, false) and true or false)
|
|
cb:SetScript("OnClick", function()
|
|
CallGetSet(entry, true, this:GetChecked() and true or false)
|
|
this:SetChecked(CallGetSet(entry, false) and true or false)
|
|
end)
|
|
|
|
local label = parent:CreateFontString(nil, "OVERLAY")
|
|
label:SetFont(STANDARD_TEXT_FONT, 10, "")
|
|
label:SetTextColor(0.9, 0.9, 0.9, 1)
|
|
label:SetPoint("Left", cb, "Right", 2, 0)
|
|
label:SetWidth(230)
|
|
label:SetJustifyH("Left")
|
|
label:SetText(entry.name or "")
|
|
|
|
return 24
|
|
end
|
|
|
|
local sliderCounter = 0
|
|
local function MakeRangeWidget(parent, entry, yOff)
|
|
local label = parent:CreateFontString(nil, "OVERLAY")
|
|
label:SetFont(STANDARD_TEXT_FONT, 10, "")
|
|
label:SetTextColor(0.9, 0.9, 0.9, 1)
|
|
label:SetPoint("TopLeft", parent, "TopLeft", 8, yOff)
|
|
label:SetText(entry.name or "")
|
|
|
|
local valLbl = parent:CreateFontString(nil, "OVERLAY")
|
|
valLbl:SetFont(STANDARD_TEXT_FONT, 10, "")
|
|
valLbl:SetTextColor(0.6, 0.85, 1, 1)
|
|
valLbl:SetPoint("TopRight", parent, "TopRight", -10, yOff)
|
|
valLbl:SetJustifyH("Right")
|
|
valLbl:SetText(tostring(CallGetSet(entry, false)))
|
|
|
|
sliderCounter = sliderCounter + 1
|
|
local sliderName = "CursiveSettingsWindowSlider" .. sliderCounter
|
|
local slider = CreateFrame("Slider", sliderName, parent, "OptionsSliderTemplate")
|
|
slider:SetWidth(250)
|
|
slider:SetHeight(14)
|
|
slider:SetPoint("TopLeft", parent, "TopLeft", 10, yOff - 16)
|
|
slider:SetMinMaxValues(entry.min or 0, entry.max or 100)
|
|
slider:SetValueStep(entry.step or 1)
|
|
slider:SetValue(CallGetSet(entry, false) or 0)
|
|
getglobal(sliderName .. "Low"):SetText(tostring(entry.min or 0))
|
|
getglobal(sliderName .. "High"):SetText(tostring(entry.max or 100))
|
|
getglobal(sliderName .. "Text"):SetText("")
|
|
slider:SetScript("OnValueChanged", function()
|
|
local minimum, maximum, step = entry.min or 0, entry.max or 100, entry.step or 1
|
|
local value = minimum + math.floor((this:GetValue() - minimum) / step + 0.5) * step
|
|
value = math.max(minimum, math.min(maximum, value))
|
|
CallGetSet(entry, true, value)
|
|
valLbl:SetText(tostring(CallGetSet(entry, false)))
|
|
end)
|
|
|
|
return 40
|
|
end
|
|
|
|
local function MakeColorWidget(parent, entry, yOff)
|
|
local label = parent:CreateFontString(nil, "OVERLAY")
|
|
label:SetFont(STANDARD_TEXT_FONT, 10, "")
|
|
label:SetTextColor(0.9, 0.9, 0.9, 1)
|
|
label:SetPoint("TopLeft", parent, "TopLeft", 8, yOff)
|
|
label:SetText(entry.name or "")
|
|
|
|
local swatch = CreateFrame("Button", nil, parent)
|
|
swatch:SetWidth(16) swatch:SetHeight(16)
|
|
swatch:SetPoint("TopRight", parent, "TopRight", -10, yOff)
|
|
local tex = swatch:CreateTexture(nil, "OVERLAY")
|
|
tex:SetAllPoints(swatch)
|
|
local r, g, b = CallGetSet(entry, false)
|
|
tex:SetTexture(r, g, b)
|
|
swatch.tex = tex
|
|
|
|
swatch:SetScript("OnClick", function()
|
|
local r2, g2, b2 = CallGetSet(entry, false)
|
|
ColorPickerFrame.func = function()
|
|
local nr, ng, nb = ColorPickerFrame:GetColorRGB()
|
|
CallGetSet(entry, true, nr, ng, nb)
|
|
swatch.tex:SetTexture(nr, ng, nb)
|
|
end
|
|
ColorPickerFrame.cancelFunc = function(prev)
|
|
CallGetSet(entry, true, prev.r, prev.g, prev.b)
|
|
swatch.tex:SetTexture(prev.r, prev.g, prev.b)
|
|
end
|
|
ColorPickerFrame.hasOpacity = false
|
|
ColorPickerFrame.previousValues = { r = r2, g = g2, b = b2 }
|
|
ColorPickerFrame:SetColorRGB(r2, g2, b2)
|
|
ShowUIPanel(ColorPickerFrame)
|
|
end)
|
|
|
|
return 24
|
|
end
|
|
|
|
-- Cycle-button widget for choosing among a fixed list of options. Entry
|
|
-- format: { type="select", name="...", options={ {label="...", value="..."}, ... },
|
|
-- get=function() return currentValue end, set=function(value) ... end }
|
|
local function MakeSelectWidget(parent, entry, yOff)
|
|
local label = parent:CreateFontString(nil, "OVERLAY")
|
|
label:SetFont(STANDARD_TEXT_FONT, 10, "")
|
|
label:SetTextColor(0.9, 0.9, 0.9, 1)
|
|
label:SetPoint("TopLeft", parent, "TopLeft", 8, yOff)
|
|
label:SetText(entry.name or "")
|
|
|
|
local btn = CreateFrame("Button", nil, parent)
|
|
btn:SetWidth(150) btn:SetHeight(20)
|
|
btn:SetPoint("TopRight", parent, "TopRight", -8, yOff)
|
|
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
local btnBg = btn:CreateTexture(nil, "BACKGROUND")
|
|
btnBg:SetAllPoints(btn)
|
|
btnBg:SetTexture(0.15, 0.15, 0.22)
|
|
local btnText = btn:CreateFontString(nil, "OVERLAY")
|
|
btnText:SetFont(STANDARD_TEXT_FONT, 9, "")
|
|
btnText:SetAllPoints(btn)
|
|
btnText:SetJustifyH("Center")
|
|
btnText:SetTextColor(0.8, 0.9, 1, 1)
|
|
|
|
local function FindLabel(val)
|
|
local i = 1
|
|
while i <= table.getn(entry.options) do
|
|
if entry.options[i].value == val then
|
|
return entry.options[i].label
|
|
end
|
|
i = i + 1
|
|
end
|
|
return tostring(val)
|
|
end
|
|
btnText:SetText(FindLabel(CallGetSet(entry, false)))
|
|
|
|
local dropdown = nil
|
|
|
|
local function CreateDropdown()
|
|
local dd = CreateFrame("Frame", nil, UIParent)
|
|
dd:SetWidth(150)
|
|
dd:SetHeight(table.getn(entry.options) * 16 + 4)
|
|
dd:SetPoint("TopLeft", btn, "BottomLeft", 0, -2)
|
|
dd:SetFrameStrata("TOOLTIP")
|
|
dd:SetBackdrop({
|
|
bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
edgeSize = 8,
|
|
})
|
|
dd:SetBackdropColor(0.05, 0.05, 0.08, 0.98)
|
|
dd:SetBackdropBorderColor(0.3, 0.3, 0.4, 1)
|
|
dd:Hide()
|
|
|
|
local i = 1
|
|
while i <= table.getn(entry.options) do
|
|
local opt = entry.options[i]
|
|
local row = CreateFrame("Button", nil, dd)
|
|
row:SetWidth(146) row:SetHeight(16)
|
|
row:SetPoint("TopLeft", dd, "TopLeft", 2, -2 - (i - 1) * 16)
|
|
row:RegisterForClicks("LeftButtonUp")
|
|
|
|
local rowHighlight = row:CreateTexture(nil, "HIGHLIGHT")
|
|
rowHighlight:SetAllPoints(row)
|
|
rowHighlight:SetTexture(0.35, 0.35, 0.55)
|
|
rowHighlight:SetBlendMode("ADD")
|
|
|
|
local rowText = row:CreateFontString(nil, "OVERLAY")
|
|
rowText:SetFont(STANDARD_TEXT_FONT, 9, "")
|
|
rowText:SetAllPoints(row)
|
|
rowText:SetJustifyH("Left")
|
|
rowText:SetTextColor(0.85, 0.85, 0.9, 1)
|
|
rowText:SetText(opt.label)
|
|
|
|
row:SetScript("OnClick", function()
|
|
CallGetSet(entry, true, opt.value)
|
|
btnText:SetText(opt.label)
|
|
dd:Hide()
|
|
end)
|
|
|
|
i = i + 1
|
|
end
|
|
|
|
return dd
|
|
end
|
|
|
|
btn:SetScript("OnClick", function()
|
|
if arg1 == "RightButton" then
|
|
-- Right-click: cycle to the next option
|
|
local val = CallGetSet(entry, false)
|
|
local idx = 1
|
|
local i = 1
|
|
while i <= table.getn(entry.options) do
|
|
if entry.options[i].value == val then
|
|
idx = i
|
|
break
|
|
end
|
|
i = i + 1
|
|
end
|
|
local nextIdx = idx + 1
|
|
if nextIdx > table.getn(entry.options) then nextIdx = 1 end
|
|
local nextOpt = entry.options[nextIdx]
|
|
CallGetSet(entry, true, nextOpt.value)
|
|
btnText:SetText(nextOpt.label)
|
|
else
|
|
-- Left-click: toggle a dropdown listing every option directly
|
|
if not dropdown then
|
|
dropdown = CreateDropdown()
|
|
end
|
|
if dropdown:IsShown() then
|
|
dropdown:Hide()
|
|
else
|
|
dropdown:Show()
|
|
end
|
|
end
|
|
end)
|
|
|
|
return 24
|
|
end
|
|
|
|
local function RenderOptionsInto(parent, argsTable, yStart)
|
|
local sorted = {}
|
|
for k, v in pairs(argsTable) do
|
|
table.insert(sorted, { key = k, entry = v })
|
|
end
|
|
table.sort(sorted, function(a, b)
|
|
return (a.entry.order or 999) < (b.entry.order or 999)
|
|
end)
|
|
|
|
local y = yStart
|
|
for _, item in ipairs(sorted) do
|
|
local entry = item.entry
|
|
if (not entry.name or entry.name == "") and entry.type ~= "group" then
|
|
entry.name = item.key
|
|
end
|
|
if entry.type == "toggle" then
|
|
y = y - MakeToggleWidget(parent, entry, y)
|
|
elseif entry.type == "range" then
|
|
y = y - MakeRangeWidget(parent, entry, y)
|
|
elseif entry.type == "color" then
|
|
y = y - MakeColorWidget(parent, entry, y)
|
|
elseif entry.type == "select" then
|
|
y = y - MakeSelectWidget(parent, entry, y)
|
|
elseif entry.type == "header" then
|
|
y = y - MakeHeaderWidget(parent, entry.name or "", y)
|
|
elseif entry.type == "group" then
|
|
y = y - 8
|
|
y = y - MakeHeaderWidget(parent, "== " .. (entry.name or item.key) .. " ==", y)
|
|
if entry.args then
|
|
y = RenderOptionsInto(parent, entry.args, y)
|
|
end
|
|
end
|
|
end
|
|
return y
|
|
end
|
|
|
|
function ui.CreateSettingsWindow()
|
|
if settingsWindow then
|
|
return settingsWindow
|
|
end
|
|
|
|
local f = CreateFrame("Frame", "CursiveSettingsWindow", UIParent)
|
|
f:SetWidth(320)
|
|
f:SetHeight(480)
|
|
f:SetPoint("Center", UIParent, "Center", 0, 0)
|
|
f:SetBackdrop({
|
|
bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeSize = 1,
|
|
insets = { left = 1, right = 1, top = 1, bottom = 1 }
|
|
})
|
|
f:SetBackdropColor(0.05, 0.05, 0.08, (Cursive.db.profile.menuopacity or 100) / 100)
|
|
f:SetBackdropBorderColor(0.25, 0.25, 0.35, 1)
|
|
Cursive.ApplyMenuStyle()
|
|
local menuFill = f:CreateTexture(nil, "BACKGROUND")
|
|
menuFill:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -8)
|
|
menuFill:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -8, 8)
|
|
f.menuFill = menuFill
|
|
Cursive.ApplyMenuStyle()
|
|
f:SetMovable(true)
|
|
f:EnableMouse(true)
|
|
f:RegisterForDrag("LeftButton")
|
|
f:SetScript("OnDragStart", function() this:StartMoving() end)
|
|
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
|
|
f:SetFrameStrata("DIALOG")
|
|
f:Hide()
|
|
|
|
local titleBar = CreateFrame("Frame", nil, f)
|
|
titleBar:SetHeight(24)
|
|
titleBar:SetPoint("TopLeft", f, "TopLeft", 0, 0)
|
|
titleBar:SetPoint("TopRight", f, "TopRight", 0, 0)
|
|
titleBar:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
|
|
titleBar:SetBackdropColor(0.22, 0.08, 0.38, 1)
|
|
|
|
f.titleBar = titleBar
|
|
local classicHeader = titleBar:CreateTexture(nil, "BACKGROUND")
|
|
classicHeader:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Header")
|
|
classicHeader:SetWidth(360)
|
|
classicHeader:SetHeight(64)
|
|
classicHeader:SetPoint("TOP", f, "TOP", 0, 14)
|
|
f.classicHeader = classicHeader
|
|
Cursive.ApplyMenuStyle()
|
|
|
|
local title = titleBar:CreateFontString(nil, "OVERLAY")
|
|
title:SetFont(STANDARD_TEXT_FONT, 12, "OUTLINE")
|
|
title:SetTextColor(1, 0.84, 0, 1)
|
|
title:SetPoint("Left", titleBar, "Left", 8, 0)
|
|
f.titleText = title
|
|
local versionStr = GetAddOnMetadata("Cursive", "Version") or "?"
|
|
title:SetText("Cursive — Settings (v"..versionStr..")")
|
|
|
|
local closeBtn = CreateFrame("Button", nil, titleBar)
|
|
closeBtn:SetWidth(16) closeBtn:SetHeight(16)
|
|
closeBtn:SetPoint("Right", titleBar, "Right", -6, 0)
|
|
local closeTxt = closeBtn:CreateFontString(nil, "OVERLAY")
|
|
closeTxt:SetFont(STANDARD_TEXT_FONT, 13, "OUTLINE")
|
|
closeTxt:SetAllPoints(closeBtn)
|
|
closeTxt:SetText("|cffaaaaaaX|r")
|
|
closeBtn:SetScript("OnEnter", function() closeTxt:SetText("|cffffffffX|r") end)
|
|
closeBtn:SetScript("OnLeave", function() closeTxt:SetText("|cffaaaaaaX|r") end)
|
|
closeBtn:SetScript("OnClick", function() f:Hide() end)
|
|
|
|
-- Live font preview: sample versions of the main bar text elements
|
|
-- (unit row name/hp, curse timer, Sacrifice bar text), so a font
|
|
-- change can be seen immediately without needing an active
|
|
-- target/shield to test against.
|
|
local previewSection = CreateFrame("Frame", nil, f)
|
|
previewSection:SetHeight(78)
|
|
previewSection:SetPoint("TopLeft", f, "TopLeft", 16, -30)
|
|
previewSection:SetPoint("TopRight", f, "TopRight", -16, -30)
|
|
previewSection:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
|
|
previewSection:SetBackdropColor(0.02, 0.02, 0.04, 0.6)
|
|
|
|
local previewLabel = previewSection:CreateFontString(nil, "OVERLAY")
|
|
previewLabel:SetFont(STANDARD_TEXT_FONT, 9, "")
|
|
previewLabel:SetTextColor(0.6, 0.6, 0.65, 1)
|
|
previewLabel:SetPoint("TopLeft", previewSection, "TopLeft", 4, -2)
|
|
previewLabel:SetText("Text Preview (bar sizes apply to unit rows)")
|
|
|
|
local previewName = previewSection:CreateFontString(nil, "OVERLAY")
|
|
SafeSetFont(previewName, GetUIFont(), 12, "THINOUTLINE")
|
|
previewName:SetTextColor(1, 1, 1, 1)
|
|
previewName:SetPoint("TopLeft", previewSection, "TopLeft", 4, -16)
|
|
previewName:SetJustifyH("Left")
|
|
previewName:SetText("Defias Raider")
|
|
|
|
local previewHp = previewSection:CreateFontString(nil, "OVERLAY")
|
|
SafeSetFont(previewHp, GetUIFont(), 12, "THINOUTLINE")
|
|
previewHp:SetTextColor(0.6, 1, 0.6, 1)
|
|
previewHp:SetPoint("TopRight", previewSection, "TopRight", -4, -16)
|
|
previewHp:SetJustifyH("Right")
|
|
previewHp:SetText("2453")
|
|
|
|
local previewCurseTimer = previewSection:CreateFontString(nil, "OVERLAY")
|
|
SafeSetFont(previewCurseTimer, GetUIFont(), 12, "OUTLINE")
|
|
previewCurseTimer:SetTextColor(1, 0.9, 0.3, 1)
|
|
previewCurseTimer:SetPoint("TopLeft", previewSection, "TopLeft", 4, -36)
|
|
previewCurseTimer:SetJustifyH("Left")
|
|
previewCurseTimer:SetText("Curse timer: 12")
|
|
|
|
local previewSacrifice = previewSection:CreateFontString(nil, "OVERLAY")
|
|
SafeSetFont(previewSacrifice, GetUIFont(), 10, "OUTLINE")
|
|
previewSacrifice:SetTextColor(0.8, 0.6, 1, 1)
|
|
previewSacrifice:SetPoint("TopLeft", previewSection, "TopLeft", 4, -54)
|
|
previewSacrifice:SetJustifyH("Left")
|
|
previewSacrifice:SetText("Sacrifice: 18s | 1200 absorb")
|
|
|
|
local function ApplyPreviewFont(fontPath)
|
|
previewName:SetFont(fontPath, Cursive.db.profile.textsize, "THINOUTLINE")
|
|
previewHp:SetFont(fontPath, Cursive.db.profile.textsize, "THINOUTLINE")
|
|
previewCurseTimer:SetFont(fontPath, Cursive.db.profile.cursetimersize, "OUTLINE")
|
|
previewSacrifice:SetFont(fontPath, 10, "OUTLINE")
|
|
end
|
|
ui.UpdateFontPreview = ApplyPreviewFont
|
|
ApplyPreviewFont(GetUIFont())
|
|
|
|
local scrollFrame = CreateFrame("ScrollFrame", "CursiveSettingsScrollFrame", f, "UIPanelScrollFrameTemplate")
|
|
scrollFrame:SetPoint("TopLeft", f, "TopLeft", 16, -116)
|
|
scrollFrame:SetPoint("BottomRight", f, "BottomRight", -32, 16)
|
|
|
|
local content = CreateFrame("Frame", nil, scrollFrame)
|
|
content:SetWidth(272)
|
|
content:SetHeight(1)
|
|
scrollFrame:SetScrollChild(content)
|
|
|
|
local finalY = RenderOptionsInto(content, Cursive.cmdtable.args, -4)
|
|
content:SetHeight(math.abs(finalY) + 30)
|
|
|
|
Cursive.ApplyMenuStyle()
|
|
settingsWindow = f
|
|
return f
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Diagnostic: /cgd prints exactly what UnitDebuff sees for the current
|
|
-- target, and whether each GHOST_DOT_ICONS entry matches -- run this while
|
|
-- a ghost icon is stuck showing despite the DoT clearly being active, to
|
|
-- get real data instead of guessing further.
|
|
-------------------------------------------------------------------------------
|
|
|
|
SLASH_CURSIVEGHOSTDEBUG1 = "/cgd"
|
|
SlashCmdList["CURSIVEGHOSTDEBUG"] = function()
|
|
local _, guid = UnitExists("target")
|
|
if not guid then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffff6060Cursive ghost debug:|r no target selected")
|
|
return
|
|
end
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00Cursive ghost debug|r -- guid: "..guid)
|
|
DEFAULT_CHAT_FRAME:AddMessage("(this is your current target, so the fix resolves it via the \"target\" token)")
|
|
DEFAULT_CHAT_FRAME:AddMessage("Raw UnitDebuff scan:")
|
|
local i = 1
|
|
local foundAny = false
|
|
while true do
|
|
local tex = SafeUnitDebuff(guid, i)
|
|
if not tex then break end
|
|
foundAny = true
|
|
DEFAULT_CHAT_FRAME:AddMessage(" slot "..i..": "..tex)
|
|
i = i + 1
|
|
end
|
|
if not foundAny then
|
|
DEFAULT_CHAT_FRAME:AddMessage(" (UnitDebuff returned nothing at all for this guid)")
|
|
end
|
|
DEFAULT_CHAT_FRAME:AddMessage("Checking against GHOST_DOT_ICONS:")
|
|
for _, dot in ipairs(GHOST_DOT_ICONS) do
|
|
local found = false
|
|
local j = 1
|
|
while true do
|
|
local tex = SafeUnitDebuff(guid, j)
|
|
if not tex then break end
|
|
if string.find(tex, dot.texture, 1, true) then
|
|
found = true
|
|
break
|
|
end
|
|
j = j + 1
|
|
end
|
|
DEFAULT_CHAT_FRAME:AddMessage(" "..dot.name..": "..(found and "|cff00ff00FOUND|r" or "|cffff6060NOT FOUND|r").." (looking for '"..dot.texture.."')")
|
|
end
|
|
end
|
|
|
|
Cursive.ui = ui
|