JG1q_YumkizKrK#nj_!E+vvsbyYD_N
ztAF4KLK`oDGN6KG10}~)1f&%K=~ycA6d5_ZGL(991)LE$JJ5`vS%8@eFzvbLo)d@(
z07C(w@ya0s7y%Ic4EzE=#sE%$2_WNNNGVxWRlT~d`~5ygo1UJwZQHhmnVA`zot+Ka
zw{K57cI>Ej?%WyPfB${kwQHAs@WBUR_wL=cXV0GM!w)|Ud-v{5AAR&u`1s?G?UPSF
z>3#a?rx=daXP%j#plKGodT9Q`JJ
zsGI9-{bt!7*AKlrHdk9;tY`SMWGl}7J7ky5J&T{fFXHDbh!opU-ZBz7~`BZqk$5*g=@ONVIL}E~D2bWu3
zH8z<8Q;?0Y@9SSoUK(u(R3DWLsssuXXO%+jbZCP4Ubh
zfBccRQ4k9@GSgBr5sV2bvTj+S_^D!1isefmC43aJj}kr#nc*zE$XJsR7vZ6Vivm6}
zd=xUV$w>hx8RQl+xrM?>BDd@$8|oxqne+6LFUx?J3@?S2n?lP?fe=q}Q-_NT7a15D
zZZh0t*-Z&})9=6kp8ojb5Bu}aKmYUBUw=*Ue?I0(M9<*I3vQUQgj>lOxIOWCGN}r<
zJ)s2FL;~)>ib%j6fZM=3bR?A2fp-LN)`X}~Yr_r&c{56_ZkDY*BcJEqPt`uk8X!s(LE{L
zk?_u$r=*}EaC_#7kWfxM3hxLB<*bttPXv*pt%MUvI62?613NZ}M)VU%G}fXY$GS~F
zf!IzXV%v!4*$Zqdo0
zI-M|4mW7F1J3#&Ks;8tAKQua6_pdMSAIMm9Mq2Lh}-GaJo
zDO-86i@7x`^S-~k5L83iYr6g+v!1bmT>|YL|G55U1c{8R0tL1AH=QNPJW6hnh@h^R
zuC(dedm9mmTFFrva)OXv1fVrtDVxbwezb$9D@Avspl%OW%7VKbu82S;j_Rg!pE#;T
zASy*P26ng;QC$|?3uz_2{Ocg>j(VgcJN{4Wk+Rn-0irg58Wv43%Q_a>pZ4^hX!>RF%B;oyFDW5V(d=-fUytsGhXtw
zM%xA03hJ`LE`wU*kyg3aj_Go&tzlHJ8Ako>vEP_ZW)Z58#bXa8Z2`@8PkRp`13j#9
zEK_4S)b>iGoJD!(IHVFOGUM=|?nsP_*EV`1<5WGLE`5@c@8FTPz(z8__BIyR?4z3gjM*Cp1zBRvU)c$eP6*OS3LNp)THjN=HTE3V6)ags*n0Mv?W$YUwW
z3{(V&1=z-Jpw?zhd*Ig|Xgk!CwCLLXR~6D4X?Hb^9VK5!YAhrBKweaX?z;zV7CDry
z%Ld)!x?O^3pj~#1+7)Y8dK0C^!%%R5^_+ZQB-CE(jY*6pUwc$L(h28!Vci9DM?^0Q
z%J$^96Pev?uoWYCR%)DeQryh~0SvE>JbQ@?L|qYdsB~y4YHCzOuA-
zO-rP-o^Z$431oOwC$_{S#85;}uqof_8i&E1JM@(2PKkXQpF2hBJw-L!Gy+>&@(bu1
z^VmrRAEKnCXkwshG)8k$2p3~XMCBr)vX_Z~PTi-Gh!6XuBXZ2Am}WD`Zln+r5_yUg
zZ7*r2ea%rURtO?|&!)#{q*D*!#NE?I$=}<)4$i{VEqs7LDC!WaWN8U=6D5F%JDOd&)5aM)D
z#t|rq`n4#iq?gjJwILFS4ABtIXf`apR6&SYIa75k(yEy2xK1juZ7tc1bW#eHESssa
zY$g?b!eCrU$fO)E(qv52mMPU&*={TBU4PjCA-l*Guiqx>U{3|;(lGv
z*8Tds!+7R!)f0`~6x*>RGUyfGR`@njII#8&)6dF7i8!S^>>-kaeK{IN`on$y1h-&mt%9qq440S#Lv5ULz;3kdx0Mxi)m}lelA`W&C2a!0yNYNb$2cn^m30O(Cw__zESCq(3XDcbU&fWf-^MQ=K
hb=_s=E*N(m{iY3@S8Z9dan1U5E4Hp)vv$?W@Lxa9B0&HE
literal 0
HcmV?d00001
diff --git a/img/EliteDragon.tga b/img/EliteDragon.tga
new file mode 100644
index 0000000000000000000000000000000000000000..aa81cde28cc945b0595cae24cb98124f589dc89d
GIT binary patch
literal 27236
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
literal 0
HcmV?d00001
diff --git a/settings.lua b/settings.lua
index 9495833..a26e066 100644
--- a/settings.lua
+++ b/settings.lua
@@ -38,6 +38,7 @@ Cursive:RegisterDefaults("profile", {
showtargetindicator = true,
showraidicons = true,
showhealthbar = true,
+ classicmenu = false, menuopacity = 100, showelitedragon = false, modernhealth = false, healthborder = false, auraborder = false, useclassicapi = true,
showunitname = true,
malediction = true,
@@ -99,6 +100,47 @@ local function splitIgnoreString(str, delimiter)
end
local barOptions = {
+ ["menuopacity"] = {
+ type = "range", name = "Menu Opacity", desc = "Opacity of the settings window background; text remains readable.",
+ order = 36.1, min = 20, max = 100, step = 5,
+ get = function() return Cursive.db.profile.menuopacity end,
+ set = function(v) Cursive.db.profile.menuopacity = v; Cursive.ApplyMenuStyle() end,
+ },
+ ["showelitedragon"] = {
+ type = "toggle", name = "Elite Dragon", desc = "Small gold dragon on the upper-right corner of elite/boss bars; silver for rare elites.",
+ order = 59.5, get = function() return Cursive.db.profile.showelitedragon end,
+ set = function(v) Cursive.db.profile.showelitedragon = v; Cursive.UpdateFramesFromConfig() end,
+ },
+ ["testmode"] = {
+ type = "toggle", name = "Test Mode", desc = "Show sample unit bars and timers to test appearance. Resets on reload.",
+ order = 37, get = function() return Cursive.testMode or false end,
+ set = function(v) Cursive.SetTestMode(v) end,
+ },
+ ["classicmenu"] = {
+ type = "toggle", name = "Classic WoW Menu", desc = "Blizzard dialog artwork for the settings window.",
+ order = 36, get = function() return Cursive.db.profile.classicmenu end,
+ set = function(v) Cursive.db.profile.classicmenu = v; Cursive.ApplyMenuStyle() end,
+ },
+ ["modernhealth"] = {
+ type = "toggle", name = "Modern Blizzard Health Texture", desc = "Blizzard Modern texture from OEVG's Puppeteer.",
+ order = 56, get = function() return Cursive.db.profile.modernhealth end,
+ set = function(v) Cursive.db.profile.modernhealth = v; Cursive.UpdateFramesFromConfig() end,
+ },
+ ["healthborder"] = {
+ type = "toggle", name = "Health Bar Tooltip Border", desc = "Match ShaguTweaks Darkened UI / Golden UI.",
+ order = 57, get = function() return Cursive.db.profile.healthborder end,
+ set = function(v) Cursive.db.profile.healthborder = v; Cursive.UpdateFramesFromConfig() end,
+ },
+ ["auraborder"] = {
+ type = "toggle", name = "Curse / Debuff / DoT Tooltip Border", desc = "Border active, missing and Shadow Vulnerability icons; match ShaguTweaks.",
+ order = 58, get = function() return Cursive.db.profile.auraborder end,
+ set = function(v) Cursive.db.profile.auraborder = v; Cursive.UpdateFramesFromConfig() end,
+ },
+ ["useclassicapi"] = {
+ type = "toggle", name = "Use ClassicAPI Aura Tracking", desc = "Reconcile confirmed player auras on aura events. Nampower remains required.",
+ order = 59, get = function() return Cursive.db.profile.useclassicapi end,
+ set = function(v) Cursive.db.profile.useclassicapi = v end,
+ },
["flashonexpiring"] = {
type = "toggle",
name = "Flash Icon On Expiring",
diff --git a/tests/appearance.lua b/tests/appearance.lua
new file mode 100644
index 0000000..5e0c95b
--- /dev/null
+++ b/tests/appearance.lua
@@ -0,0 +1,68 @@
+local file = assert(io.open("ui.lua","r"))
+local source = file:read("*a"); file:close()
+Cursive = {db={profile={classicmenu=true}}}
+local function frame()
+ return {
+ SetBackdrop=function(self,v) self.backdrop=v end,
+ SetBackdropColor=function(self,r,g,b,a) self.backgroundAlpha=a end,
+ SetBackdropBorderColor=function(self,r,g,b,a) self.r=r; self.g=g end,
+ SetHeight=function(self,v) self.height=v end,
+ SetTexture=function(self,r,g,b,a) self.textureAlpha=a end,
+ SetVertexColor=function(self,r,g,b,a) self.tintR=r; self.tintG=g end,
+ Show=function(self) self.shown=true end,
+ Hide=function(self) self.shown=false end,
+ }
+end
+ShaguTweaks={GoldMode=true}
+GameTooltip={GetBackdropBorderColor=function() return .8,.62,.32,1 end}
+CursiveSettingsWindow=frame()
+CursiveSettingsWindow.titleBar=frame()
+CursiveSettingsWindow.classicHeader=frame()
+CursiveSettingsWindow.menuFill=frame()
+local start=assert(string.find(source,"local function ThemeBorder",1,true))
+local finish=assert(string.find(source,"local utils = Cursive.utils",start,true))
+assert(loadstring(string.sub(source,start,finish-1)))()
+Cursive.db.profile.menuopacity=60
+Cursive.ApplyMenuStyle()
+assert(CursiveSettingsWindow.backgroundAlpha==.6)
+assert(CursiveSettingsWindow.menuFill.textureAlpha==.6)
+assert(CursiveSettingsWindow.classicHeader.tintR==.8)
+assert(CursiveSettingsWindow.r==.8)
+assert(CursiveSettingsWindow.titleBar.backdrop==nil)
+assert(CursiveSettingsWindow.classicHeader.shown)
+Cursive.db.profile.menuopacity=100
+Cursive.ApplyMenuStyle()
+assert(CursiveSettingsWindow.menuFill.textureAlpha==1)
+Cursive.db.profile.classicmenu=false
+Cursive.ApplyMenuStyle()
+assert(CursiveSettingsWindow.r==.25)
+assert(not CursiveSettingsWindow.classicHeader.shown)
+assert(CursiveSettingsWindow.titleBar.backdrop)
+local retired=frame(); function retired:IsShown() return true end
+ui={unitFrames={{retired}},flashingCurseIcons={[{}]=3}}
+function GetUIFont() return "font" end
+function UpdateRootBarFrame() end
+function Cursive:ScheduleEvent(name,fn,delay) self.delay=delay; self.scheduled=fn end
+start=assert(string.find(source,"Cursive.UpdateFramesFromConfig = function()",1,true))
+finish=assert(string.find(source,"ui.BarEnter = function()",start,true))
+assert(loadstring(string.sub(source,start,finish-1)))()
+Cursive.UpdateFramesFromConfig()
+assert(Cursive.delay==.1)
+Cursive.scheduled()
+assert(next(ui.unitFrames)==nil)
+assert(next(ui.flashingCurseIcons)==nil)
+print("Menu style and resize refresh tests passed")
+local start = assert(string.find(source,"local function UpdateEliteDragon",1,true))
+local finish = assert(string.find(source,"ui.BarUpdate = function()",start,true))
+local updateDragon = assert(loadstring(string.sub(source,start,finish-1).." return UpdateEliteDragon"))()
+local dragon = frame()
+function dragon:SetTexture(path) self.texture=path end
+function dragon:SetDesaturated(value) self.silver=value end
+local bar={eliteDragon=dragon}
+updateDragon(bar,"elite"); assert(dragon.shown and string.find(dragon.texture,"Elite"))
+updateDragon(bar,"rareelite"); assert(dragon.shown and dragon.silver)
+updateDragon(bar,"worldboss"); assert(dragon.shown)
+updateDragon(bar,"normal"); assert(not dragon.shown)
+updateDragon(bar,"rare"); assert(not dragon.shown)
+updateDragon({},"elite")
+print("Elite dragon and opacity tests passed")
\ No newline at end of file
diff --git a/tests/classicapi.lua b/tests/classicapi.lua
new file mode 100644
index 0000000..4e04531
--- /dev/null
+++ b/tests/classicapi.lua
@@ -0,0 +1,41 @@
+local callbacks = {}
+local auras = {}
+local removed = 0
+local pending = {}
+Cursive = { nampower = true, db = {profile = {enabled=true,useclassicapi=true}},
+ core = {addGuid=function() end},
+ curses = {trackedCurseIds={[172]={name="corruption",rank=1}},guids={},pendingCast=pending} }
+function Cursive.curses:ApplyCurse(id,guid,start,duration)
+ self.pendingCast = {}
+ self.guids[guid] = self.guids[guid] or {}
+ self.guids[guid].corruption = {currentPlayer=true,start=start,duration=duration}
+end
+function Cursive.curses:RemoveCurse(guid,name) self.guids[guid][name]=nil; removed=removed+1 end
+function CreateFrame() return {RegisterEvent=function() end,SetScript=function(self,key,fn) callbacks[key]=fn end} end
+function UnitExists(unit) return true,"mob" end
+function UnitIsUnit(a,b) return a==b end
+function GetTime() return 100 end
+C_UnitAuras = {
+ GetAuraSlots=function(unit,filter,a,b,out) for i=1,table.getn(auras) do out[i]=i end; return nil,table.getn(auras) end,
+ UnitAuraBySlot=function(unit,slot) return unpack(auras[slot]) end,
+ GetAuraDataByAuraInstanceID=function() end,
+}
+dofile("classicapi.lua")
+auras[1]={"Corruption","icon",1,"Magic",18,118,"player",false,false,172}
+Cursive.ReconcileClassicAuras("target")
+assert(Cursive.curses.guids.mob.corruption.start==100)
+auras[1][6]=125
+Cursive.ReconcileClassicAuras("target")
+assert(Cursive.curses.guids.mob.corruption.start==107)
+auras={}
+Cursive.ReconcileClassicAuras("target")
+assert(removed==1)
+Cursive.curses.guids.mob.corruption={currentPlayer=true}
+Cursive.ReconcileClassicAuras("target")
+assert(Cursive.curses.guids.mob.corruption)
+auras[1]={"Corruption","icon",1,"Magic",18,118,"party1",false,false,172}
+Cursive.ReconcileClassicAuras("target")
+assert(not Cursive.curses.guids.mob.corruption.classicConfirmed)
+C_UnitAuras=nil
+Cursive.ReconcileClassicAuras("target")
+print("ClassicAPI reconciliation tests passed")
\ No newline at end of file
diff --git a/tests/testmode.lua b/tests/testmode.lua
new file mode 100644
index 0000000..c9cd411
--- /dev/null
+++ b/tests/testmode.lua
@@ -0,0 +1,48 @@
+local file=assert(io.open("ui.lua","r")); local source=file:read("*a"); file:close()
+local function widget()
+ return {
+ SetScript=function() end, ClearAllPoints=function() end,
+ SetPoint=function(self,...) self.point=arg end,
+ SetText=function(self,value) self.text=value end,
+ SetMinMaxValues=function() end,SetValue=function() end,SetStatusBarColor=function() end,
+ SetTexture=function() end, SetAlpha=function() end,
+ Show=function(self) self.shown=true end,Hide=function(self) self.shown=false end,
+ }
+end
+Cursive={db={profile={maxcol=2,maxrow=3,maxcurses=4,notifyshadowvuln=true,
+ curseiconsize=20,healthborder=true}},ResetUnitFrames=function() ui.unitFrames={} end}
+ui={unitFrames={},rootBarFrame={},padding=2}
+local created=0
+function CreateBar(row,col,guid)
+ assert(guid==nil) -- never give samples a real unit identity
+ created=created+1
+ local bar=widget()
+ bar.nameText=widget();bar.hpText=widget();bar.healthBar=widget()
+ bar.healthBar.border=widget();bar.icon=widget();bar.target_left=widget()
+ bar.shadowVulnIcon=widget();bar.shadowVulnIcon.timer=widget()
+ bar.ghostIcons={}
+ for i=1,4 do bar["curse"..i]=widget();bar["curse"..i].timer=widget();bar.ghostIcons[i]=widget() end
+ ui.unitFrames[col][row]=bar
+ return bar
+end
+function GetBarCords(row,col) return col*100,-row*20 end
+function GetTime() return 100 end
+function ThemeBorder() end
+function UpdateEliteDragon() end
+function UpdateAuraBorder() end
+function SetRaidTargetIconTexture() end
+local start=assert(string.find(source,"function Cursive.SetTestMode",1,true))
+local finish=assert(string.find(source,"local shouldDisplayGuids",start,true))
+local render=assert(loadstring(string.sub(source,start,finish-1).." return RenderTestBars"))()
+Cursive.SetTestMode(true); render()
+assert(created==6)
+assert(ui.unitFrames[1][1].testMode)
+assert(ui.unitFrames[1][1].curse1.shown)
+assert(not ui.unitFrames[1][1].curse4.shown)
+render();assert(created==6) -- reuse samples on each refresh
+Cursive.db.profile.invertbars=true
+Cursive.db.profile.expandupwards=true
+render();assert(ui.unitFrames[1][1].point[1]=="BOTTOMLEFT")
+Cursive.SetTestMode(false)
+assert(not Cursive.testMode and next(ui.unitFrames)==nil)
+print("Test mode layout and isolation tests passed")
\ No newline at end of file
diff --git a/ui.lua b/ui.lua
index 81f21c7..86f162c 100644
--- a/ui.lua
+++ b/ui.lua
@@ -4,6 +4,79 @@ 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
@@ -102,10 +175,16 @@ local function GetBarThirdSectionWidth()
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()
+ GetBarThirdSectionWidth() + GetAuraGap()
end
local function UpdateRootBarFrame()
@@ -221,6 +300,7 @@ 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
@@ -233,8 +313,8 @@ Cursive.UpdateFramesFromConfig = function()
UpdateRootBarFrame()
end
- -- after 3 seconds reset the unit frames so all changes are applied
- Cursive:ScheduleEvent("resetUnitFrames", Cursive.ResetUnitFrames, 3)
+ -- Debounce slider movement, then rebuild rows with the new geometry.
+ Cursive:ScheduleEvent("resetUnitFrames", Cursive.ResetUnitFrames, 0.1)
end
Cursive.ResetUnitFrames = function()
@@ -246,12 +326,15 @@ Cursive.ResetUnitFrames = function()
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.healthBar then
+ 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
@@ -268,6 +351,23 @@ ui.BarLeave = function()
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()
@@ -280,6 +380,7 @@ ui.BarUpdate = function()
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))
@@ -290,7 +391,7 @@ ui.BarUpdate = function()
this.healthBar:SetStatusBarColor(r, g, b, a)
-- update health bar border
- if this.healthBar.border then
+ 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
@@ -354,6 +455,7 @@ ui.BarUpdate = function()
end
ui.BarClick = function()
+ if this.parent.testMode then return end
if arg1 == "LeftButton" then
TargetUnit(this.parent.guid)
elseif arg1 == "RightButton" then
@@ -375,7 +477,7 @@ local function CreateBarFirstSection(unitFrame, guid)
-- Normal positioning (leftmost)
firstSection:SetPoint("LEFT", unitFrame, "LEFT", 0, 0)
end
-
+
firstSection:SetWidth(GetBarFirstSectionWidth())
firstSection:SetHeight(config.height)
firstSection:EnableMouse(false)
@@ -424,7 +526,7 @@ local function CreateBarSecondSection(unitFrame, guid)
-- 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
@@ -438,7 +540,7 @@ local function CreateBarSecondSection(unitFrame, guid)
-- create health bar
if config.showhealthbar then
local healthBar = CreateFrame("StatusBar", "CursiveHealthBar", secondSection)
- healthBar:SetStatusBarTexture(config.bartexture)
+ 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)
@@ -465,18 +567,18 @@ local function CreateBarSecondSection(unitFrame, guid)
end
-- create health bar backdrops
- if pfUI and pfUI.uf then
+ 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.bar)
- border:SetBackdrop(ui.border)
+ 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.bar, "TOPLEFT", -2, 2)
- border:SetPoint("BOTTOMRIGHT", healthBar.bar, "BOTTOMRIGHT", 2, -2)
+ border:SetPoint("TOPLEFT", healthBar, "TOPLEFT", -2, 2)
+ border:SetPoint("BOTTOMRIGHT", healthBar, "BOTTOMRIGHT", 2, -2)
healthBar.border = border
end
else
@@ -503,9 +605,9 @@ local function CreateBarThirdSection(unitFrame, guid)
thirdSection:SetPoint("LEFT", unitFrame, "LEFT", 0, 0)
else
-- Normal positioning relative to second section
- thirdSection:SetPoint("LEFT", unitFrame.secondSection, "RIGHT", 0, 0)
+ thirdSection:SetPoint("LEFT", unitFrame.secondSection, "RIGHT", GetAuraGap(), 0)
end
-
+
thirdSection:SetWidth(GetBarThirdSectionWidth())
thirdSection:SetHeight(config.height)
thirdSection:EnableMouse(false)
@@ -526,6 +628,7 @@ local function CreateBarThirdSection(unitFrame, guid)
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")
@@ -549,6 +652,7 @@ local function CreateBarThirdSection(unitFrame, guid)
ghost:SetHeight(config.curseiconsize)
ghost:SetAlpha(config.ghosticonalpha)
ghost:Hide()
+ AddAuraBorder(ghost, thirdSection)
unitFrame.ghostIcons[i] = ghost
end
@@ -561,6 +665,7 @@ local function CreateBarThirdSection(unitFrame, guid)
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")
@@ -575,6 +680,27 @@ 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)
@@ -596,6 +722,19 @@ local function CreateBar(row, col, 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
@@ -650,6 +789,13 @@ local GHOST_CURSE_FALLBACK_ICON = "Interface\\Icons\\Spell_Shadow_CurseOfSargera
-- 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)
@@ -945,6 +1091,12 @@ local function DisplayGuid(guid)
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
@@ -985,6 +1137,81 @@ local function CheckForCleanup(guid, time)
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 = {};
@@ -992,7 +1219,7 @@ ui:SetAllPoints()
ui:SetScript("OnUpdate", function()
local config = Cursive.db.profile
- if not config.enabled then
+ if not config.enabled and not Cursive.testMode then
return
end
@@ -1011,6 +1238,11 @@ ui:SetScript("OnUpdate", function()
return
end
+ if Cursive.testMode then
+ RenderTestBars()
+ return
+ end
+
-- reset display data
ui.row = 1
ui.col = 1
@@ -1207,7 +1439,7 @@ gcdTicker:SetScript("OnUpdate", function()
-- 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
+ 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
@@ -1744,7 +1976,10 @@ local function MakeRangeWidget(parent, entry, yOff)
getglobal(sliderName .. "High"):SetText(tostring(entry.max or 100))
getglobal(sliderName .. "Text"):SetText("")
slider:SetScript("OnValueChanged", function()
- CallGetSet(entry, true, this:GetValue())
+ 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)
@@ -1956,8 +2191,14 @@ function ui.CreateSettingsWindow()
edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 }
})
- f:SetBackdropColor(0.05, 0.05, 0.08, 0.95)
+ 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")
@@ -1973,10 +2214,20 @@ function ui.CreateSettingsWindow()
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..")")
@@ -2006,7 +2257,7 @@ function ui.CreateSettingsWindow()
previewLabel:SetFont(STANDARD_TEXT_FONT, 9, "")
previewLabel:SetTextColor(0.6, 0.6, 0.65, 1)
previewLabel:SetPoint("TopLeft", previewSection, "TopLeft", 4, -2)
- previewLabel:SetText("Font Preview")
+ previewLabel:SetText("Text Preview (bar sizes apply to unit rows)")
local previewName = previewSection:CreateFontString(nil, "OVERLAY")
SafeSetFont(previewName, GetUIFont(), 12, "THINOUTLINE")
@@ -2037,12 +2288,13 @@ function ui.CreateSettingsWindow()
previewSacrifice:SetText("Sacrifice: 18s | 1200 absorb")
local function ApplyPreviewFont(fontPath)
- previewName:SetFont(fontPath, 12, "THINOUTLINE")
- previewHp:SetFont(fontPath, 12, "THINOUTLINE")
- previewCurseTimer:SetFont(fontPath, 12, "OUTLINE")
+ 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)
@@ -2056,6 +2308,7 @@ function ui.CreateSettingsWindow()
local finalY = RenderOptionsInto(content, Cursive.cmdtable.args, -4)
content:SetHeight(math.abs(finalY) + 30)
+ Cursive.ApplyMenuStyle()
settingsWindow = f
return f
end