Release OEVG Cursive 1.6 with ClassicAPI tracking and appearance options

This commit is contained in:
2026-11-03 19:55:47 -04:00
parent 6fd8901c0f
commit 8240256726
13 changed files with 611 additions and 46 deletions
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+1
View File
@@ -0,0 +1 @@
*.luac
+30
View File
@@ -0,0 +1,30 @@
# Changelog
## 1.6 — 2026-11-03
First OEVG release, based on Wayoff333/Cursive 1.5.
### Tracking
- Add optional ClassicAPI harmful-slot reconciliation for confirmed player effects, refreshes and removals, retaining Nampower casting/GUID tracking.
- Use ClassicAPI harmful aura indices for target ghost icons and Shadow Vulnerability when available.
- Preserve pending casts while discovering confirmed auras; do not erase optimistic casts before their first confirmation.
### Appearance
- Add a Classic WoW settings style with a centered title plaque, ShaguTweaks dark/gold tinting, and an opacity slider with an opaque 100% background.
- Keep the original menu's neutral border when Classic style is off.
- Bundle a Modern Blizzard health-bar texture.
- Add independent tooltip-border options for health bars and aura icons, matching the active ShaguTweaks palette.
- Add a trimmed elite dragon above the bar's upper-right corner for elites, rare elites and world bosses; desaturate rare elites and leave normal mobs/ordinary rares unmarked.
- Add space before aura icons when the dragon option is enabled in the normal layout.
- Add session-only Test Mode using the real row geometry, sample health, raid markers, timers and GCD.
- Reduce the row-rebuild debounce from three seconds to 0.1 seconds, snap sliders to configured steps, and reflect selected text sizes in the preview.
- Fix health-border anchors that referenced a nonexistent healthBar.bar, and clear retired icons from the flash registry.
### Maintenance and validation
- Add Lua 5.0 mocked tests for ClassicAPI reconciliation, menu styling/opacity, elite classification and test-mode layout.
- Add LF checkout attributes and ignore compiled Lua files.
- OEVG tested the appearance changes in game. Broader ClassicAPI combat validation and in-game CPU/memory measurements remain outstanding.
## 1.5 — upstream
Fonts and settings preview. Earlier releases and command behavior are documented in the [upstream repository](https://github.com/Wayoff333/Cursive).
+4 -3
View File
@@ -1,10 +1,10 @@
## Interface: 11200
## Title: Cursive 1.5
## Title-zhCN: Cursive 1.5
## Title: Cursive 1.6
## Title-zhCN: Cursive 1.6
## Author: Pepopo
## Notes: Uses GUIDs to show units in combat and track curses
## Notes-zhCN: 使用的GUID功能来显示战斗中的单位并监视诅咒等
## Version: 1.5
## Version: 1.6
## SavedVariablesPerCharacter: CursiveDB
## X-Embeds: Ace2, FuBarPlugin-2.0, CompostLib, DewDropLib, TabletLib
@@ -38,6 +38,7 @@ spells\shaman.lua
spells\warlock.lua
spells\warrior.lua
curses.lua
classicapi.lua
filter.lua
ui.lua
commands.lua
+41 -20
View File
@@ -1,37 +1,59 @@
# Cursive updated for octowow
# Cursive — OEVG fork
-new settings menu
[![Nampower](https://img.shields.io/badge/Nampower-Required-purple.svg)](https://github.com/brues-code/nampower)
[![ClassicAPI](https://img.shields.io/badge/ClassicAPI-Optional-yellow.svg)](https://github.com/brues-code/ClassicAPI)
-ghost dot icons
Cursive tracks curses, damage-over-time spells and enemies in combat, with commands for choosing and cursing targets. This is OEVG's fork of [Wayoff333/Cursive](https://github.com/Wayoff333/Cursive), building on the work of Pepopo and earlier contributors.
-Shadow vulnerability icon
**Version 1.6**, for **OctoWoW / TurtleWoW 1.18.1** (WoW 1.12 API, Interface 11200, Lua 5.0). See the [changelog](CHANGELOG.md) for release details.
-Global Cooldown bar
## Requirements
-1.4 added sacrifice bar
- **Nampower 2.40.0 or newer is required**, as in the upstream addon. It supplies casting events, GUID tracking and improved range checks.
- **ClassicAPI is optional.** Aura features are detected separately and fall back to upstream tracking when unavailable. No separate SuperWoW dependency is introduced by this fork.
- **ShaguTweaks is optional.** OEVG's Darkened UI and Golden UI palettes are used for enabled tooltip borders and Classic menu artwork. Darkened UI takes priority when both modes are enabled.
-1.5 added fonts
## What's new in 1.6
<img width="1631" height="1064" alt="image" src="https://github.com/user-attachments/assets/86281c6d-a178-4709-8e02-628c7f6219ba" />
- **ClassicAPI aura tracking:** confirm player-owned tracked debuffs from harmful aura slots, correct refresh and expiration times, and remove previously confirmed effects when absent. Target ghost icons and Shadow Vulnerability scans also use ClassicAPI's harmful aura index space.
- **Classic WoW menu:** Blizzard dialog and header artwork, a matching ShaguTweaks tint, and adjustable background opacity. At 100%, the background is opaque; text stays readable when opacity is lowered.
- **Blizzard Modern health texture:** bundled with Cursive; Puppeteer is not required.
- **Independent tooltip borders** for health bars and curse/DoT icons, matching the active ShaguTweaks tooltip palette.
- **Elite dragon:** a trimmed corner decoration above the health/name bar for elites, rare elites and world bosses only. Rare elites use silver artwork; ordinary rares and normal mobs remain unmarked. Aura icons leave room for the decoration.
- **Test Mode:** sample rows, health values, raid markers, curse timers and GCD for adjusting the real layout without a target. Includes elite, rare-elite and normal examples; resets on reload.
- **Quicker appearance updates:** row resizing applies after a short debounce, sliders respect their configured steps, and text previews follow selected font sizes.
ClassicAPI events update tracking as they arrive; rows retain the upstream display refresh cadence. They cannot remove server/network delay or reveal units outside the client's visibility.
<img width="1764" height="1080" alt="image" src="https://github.com/user-attachments/assets/30712479-d898-4e7d-9c13-94f700da2cbc" />
## Install and configure
1. Exit WoW completely so it can discover the bundled textures.
2. Back up your existing Cursive folder, then place this repository's addon files in **Interface/AddOns/Cursive**. The manifest must be at **Interface/AddOns/Cursive/Cursive.toc**.
3. Start WoW and open the settings through the minimap icon. Use **Size & Appearance → Test Mode** to adjust the layout, then turn it off for real units.
4. Enable **Classic WoW Menu**, **Modern Blizzard Health Texture**, tooltip borders or **Elite Dragon** as desired. **Menu Opacity** sits directly below Classic WoW Menu.
5. Position the Cursive display using **Show Frame Background**, then turn that off and enable **Allow clickthrough** if desired.
> [!IMPORTANT]
>
> **This addon requires you to have Nampower >= 2.40.0 installed.**
Keep your saved settings when updating. Changing menu opacity affects the settings background, not the unit bars.
![image](https://github.com/pepopo978/Cursive/assets/149287158/801511af-29c7-4baf-b1ac-5e8c52f0f846)
ShaguTweaks Debuff Timer, default target aura displays and DoesItDie may display some of the same effects. Choose which displays you want to keep; these addons are not required by Cursive.
Cursive combines ShaguScan unit scanning with curse tracking similar to DotTimer and the ability to automatically curse
targets similar to Decursive.
## Validation
### Recommended setup
OEVG tested the appearance changes in game during development. Changed Lua files parse with Lua 5.0, and mocked tests cover aura reconciliation, menu styling/opacity, elite classifications and sample layout isolation.
Move the Cursive label to the desired position on the screen. You can turn on 'Show Frame Background' in the settings to
help with this. Once you have it where you want it, turn off 'Show Frame Background' and turn on 'Allow clickthrough'
so that it doesn't block your clicks when it's not displaying mobs.
ClassicAPI tracking still needs broader in-game checks: applications, refreshes, dispels, expiration, other casters, target changes, and operation with ClassicAPI disabled or absent. CPU and memory improvements have not been measured in game.
From the repository root in Linux/WSL, using the workspace Lua 5.0 binary:
../tools/lua5.0/lua5.0 tests/classicapi.lua
../tools/lua5.0/lua5.0 tests/appearance.lua
../tools/lua5.0/lua5.0 tests/testmode.lua
## Artwork and credits
The bundled Blizzard Modern bar is copied from OEVG's Puppeteer. EliteDragon.tga is an unchanged copy of ShaguTweaks' UI-TargetingFrame-Elite.tga, sourced from the local ShaguTweaks-ClassicAPI fork; the addon uses cropped texture coordinates and desaturation. These are game-derived assets; the repository's code license does not establish separate ownership of Blizzard artwork. See [LICENSE](LICENSE) for the upstream code license.
The command reference below is retained from upstream.
## Commands
@@ -228,4 +250,3 @@ There is an option "always show current target" that will display your current t
You can ignore mobs based on their unit name using the ignored mob list filter. It is comma separated and you need to press enter to get it to save. For example can do:
`whelp,scarab` to ignore all mobs with those strings in their name.
+59
View File
@@ -0,0 +1,59 @@
-- Optional ClassicAPI bridge. Nampower remains the cast/range/GUID backend.
if not Cursive.nampower then return end
local slots, seen = {}, {}
local function Reconcile(unit)
if not Cursive.db.profile.enabled or not Cursive.db.profile.useclassicapi then return end
if not C_UnitAuras or not C_UnitAuras.GetAuraSlots or not C_UnitAuras.UnitAuraBySlot then return end
if not unit or not UnitExists(unit) then return end
local _, guid = UnitExists(unit)
if not guid and UnitGUID then guid = UnitGUID(unit) end
if not guid then return end
local curses = Cursive.curses
for key in pairs(seen) do seen[key] = nil end
local _, count = C_UnitAuras.GetAuraSlots(unit, "HARMFUL", nil, nil, slots)
if not count then return end -- no authoritative snapshot
for i = 1, count do
local name, icon, stacks, dispel, duration, expiration, source, removable, personal, id =
C_UnitAuras.UnitAuraBySlot(unit, slots[i])
local tracked = id and curses.trackedCurseIds[id]
if tracked and source and (source == "player" or UnitIsUnit(source, "player")) then
seen[tracked.name] = true
if duration and duration > 0 and expiration and expiration > GetTime() then
local data = curses.guids[guid] and curses.guids[guid][tracked.name]
if not data then
local pending = curses.pendingCast
curses:ApplyCurse(id, guid, expiration - duration, duration)
curses.pendingCast = pending
data = curses.guids[guid][tracked.name]
end
data.start = expiration - duration
data.duration = duration
data.spellID = id
data.rank = tracked.rank
data.classicConfirmed = true
Cursive.core.addGuid(guid)
end
end
end
local entries = curses.guids[guid]
if entries then
for name, data in pairs(entries) do
-- Do not erase optimistic casts before the first confirming server aura.
if data.classicConfirmed and data.currentPlayer and not seen[name] then
curses:RemoveCurse(guid, name)
end
end
end
end
Cursive.ReconcileClassicAuras = Reconcile
local bridge = CreateFrame("Frame")
bridge:RegisterEvent("PLAYER_TARGET_CHANGED")
bridge:RegisterEvent("UNIT_AURA")
bridge:RegisterEvent("PLAYER_ENTERING_WORLD")
if C_UnitAuras and C_UnitAuras.GetAuraDataByAuraInstanceID then
bridge:RegisterEvent("UNIT_AURA_DELTA")
end
bridge:SetScript("OnEvent", function()
if event == "PLAYER_TARGET_CHANGED" or event == "PLAYER_ENTERING_WORLD" then Reconcile("target")
else Reconcile(arg1) end
end)
Binary file not shown.
Binary file not shown.
+42
View File
@@ -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",
+68
View File
@@ -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")
+41
View File
@@ -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")
+48
View File
@@ -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")
+276 -23
View File
@@ -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