Files
Brues 22633aa16c Replace GetSpellRecField name/rank reads with C_Spell equivalents
Swap 166 call sites from GetSpellRecField(id, "name") to
C_Spell.GetSpellName(id) and GetSpellRecField(id, "rank") to
C_Spell.GetSpellSubtext(id) across Core, Conditionals, Utility,
ComboPointTracker, CursiveCustomSpells, pfUI, OverflowBuffFrame, and
Generic. Guard forms and the _GetSpellRecField alias calls collapse to the
direct C_Spell call.

GetSpellRecField stays for fields with no C_Spell equivalent (school,
spellIconID, mechanic, effectMechanic, effectApplyAuraName, stackAmount,
rangeIndex) and inside the NampowerAPI wrapper layer.
2026-07-26 14:07:31 -05:00

612 lines
25 KiB
Lua

--[[
CursiveCustomSpells Extension
Automatically injects custom spell definitions into Cursive's tracking system.
Any spell in this list will be tracked by Cursive when you cast it normally.
Format:
[spellID] = { name = "lowercase spell name", rank = X, duration = Y },
Optional fields:
variableDuration = true -- Duration can be modified by talents/haste
calculateDuration = func -- Function returning dynamic duration
numTicks = N -- Number of DoT ticks (for tick tracking)
darkHarvest = true -- Affected by Dark Harvest talent
]]
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids
--============================================================================
-- CUSTOM SPELL DEFINITIONS
-- Add your custom debuffs here. They will be auto-tracked when cast.
--============================================================================
CleveRoids.CustomCursiveSpells = CleveRoids.CustomCursiveSpells or {
--========================================================================
-- WARRIOR DEBUFFS (from database.turtlecraft.gg)
--========================================================================
-- Deep Wounds (talent proc, bleed over 12 sec - ticks every 3s)
[12721] = { name = "deep wounds", rank = 1, duration = 6 },
[12162] = { name = "deep wounds", rank = 1, duration = 6 },
[12850] = { name = "deep wounds", rank = 2, duration = 6 },
[12868] = { name = "deep wounds", rank = 3, duration = 6 },
-- Hamstring (15 sec snare, 50% movement speed)
[1715] = { name = "hamstring", rank = 1, duration = 15 },
[7372] = { name = "hamstring", rank = 2, duration = 15 },
[7373] = { name = "hamstring", rank = 3, duration = 15 },
--========================================================================
-- TAUNTS (personal debuffs)
--========================================================================
-- Taunt (Warrior, 3 sec)
[355] = { name = "taunt", rank = 1, duration = 3 },
[29060] = { name = "taunt", rank = 1, duration = 3 }, -- variant
-- Growl (Druid Bear, 3 sec)
[6795] = { name = "growl", rank = 1, duration = 3 },
-- Mocking Blow (Warrior, 6 sec)
[694] = { name = "mocking blow", rank = 1, duration = 6 },
[7400] = { name = "mocking blow", rank = 2, duration = 6 },
[7402] = { name = "mocking blow", rank = 3, duration = 6 },
[20559] = { name = "mocking blow", rank = 4, duration = 6 },
[20560] = { name = "mocking blow", rank = 5, duration = 6 },
-- Hand of Reckoning (Paladin, 3 sec)
[62124] = { name = "hand of reckoning", rank = 1, duration = 3 },
--========================================================================
-- PALADIN JUDGEMENT DEBUFFS
-- Generic 20271 is handled specially below to detect seal type
--========================================================================
-- Judgement of the Crusader (10 sec, +holy damage taken)
[21183] = { name = "judgement of the crusader", rank = 1, duration = 10 },
[20188] = { name = "judgement of the crusader", rank = 2, duration = 10 },
[20300] = { name = "judgement of the crusader", rank = 3, duration = 10 },
[20301] = { name = "judgement of the crusader", rank = 4, duration = 10 },
[20302] = { name = "judgement of the crusader", rank = 5, duration = 10 },
[20303] = { name = "judgement of the crusader", rank = 6, duration = 10 },
[20304] = { name = "judgement of the crusader", rank = 6, duration = 10 }, -- variant
[25942] = { name = "judgement of the crusader", rank = 1, duration = 10 }, -- variant
-- Judgement of Light (10 sec, heals attackers)
[20185] = { name = "judgement of light", rank = 1, duration = 10 },
[20344] = { name = "judgement of light", rank = 2, duration = 10 },
[20345] = { name = "judgement of light", rank = 3, duration = 10 },
[20346] = { name = "judgement of light", rank = 4, duration = 10 },
-- Judgement of Wisdom (10 sec, restores mana to attackers)
[20186] = { name = "judgement of wisdom", rank = 1, duration = 10 },
[20354] = { name = "judgement of wisdom", rank = 2, duration = 10 },
[20355] = { name = "judgement of wisdom", rank = 3, duration = 10 },
[51750] = { name = "judgement of wisdom", rank = 5, duration = 10 }, -- Rank 5 cast (confirmed)
[51751] = { name = "judgement of wisdom", rank = 4, duration = 10 }, -- Rank 4 cast
[51752] = { name = "judgement of wisdom", rank = 5, duration = 10 }, -- debuff effect ID
[25757] = { name = "judgement of wisdom", rank = 1, duration = 10 }, -- variant
-- Judgement of Justice (10 sec, prevents fleeing)
[20184] = { name = "judgement of justice", rank = 1, duration = 10 },
[25945] = { name = "judgement of justice", rank = 2, duration = 10 },
--========================================================================
-- ROGUE POISONS
--========================================================================
-- Deadly Poison (12 sec, stacking nature DoT)
[2818] = { name = "deadly poison", rank = 1, duration = 12 },
[2819] = { name = "deadly poison", rank = 2, duration = 12 },
[11353] = { name = "deadly poison", rank = 3, duration = 12 },
[11354] = { name = "deadly poison", rank = 4, duration = 12 },
[25349] = { name = "deadly poison", rank = 5, duration = 12 },
[3583] = { name = "deadly poison", rank = 1, duration = 12 }, -- proc effect
[10022] = { name = "deadly poison", rank = 2, duration = 12 }, -- proc effect
[13582] = { name = "deadly poison", rank = 3, duration = 12 }, -- proc effect
[21787] = { name = "deadly poison", rank = 4, duration = 12 }, -- proc effect
[21788] = { name = "deadly poison", rank = 5, duration = 12 }, -- proc effect
-- Crippling Poison (12 sec, -50% movement speed)
[3409] = { name = "crippling poison", rank = 1, duration = 12 },
[11201] = { name = "crippling poison", rank = 2, duration = 12 },
[25809] = { name = "crippling poison", rank = 2, duration = 12 }, -- variant
-- Wound Poison (15 sec, -healing received)
[13218] = { name = "wound poison", rank = 1, duration = 15 },
[13222] = { name = "wound poison", rank = 2, duration = 15 },
[13223] = { name = "wound poison", rank = 3, duration = 15 },
[13224] = { name = "wound poison", rank = 4, duration = 15 },
[25648] = { name = "wound poison", rank = 5, duration = 15 },
-- Mind-numbing Poison (10 sec, -casting speed)
[5760] = { name = "mind-numbing poison", rank = 1, duration = 10 },
[8692] = { name = "mind-numbing poison", rank = 2, duration = 10 },
[11398] = { name = "mind-numbing poison", rank = 3, duration = 10 },
[25810] = { name = "mind-numbing poison", rank = 2, duration = 10 }, -- variant
--========================================================================
-- THUNDERFURY PROC DEBUFFS
--========================================================================
-- Thunderfury - Nature Resist Reduction (12 sec, chains to 5 targets)
[21992] = { name = "thunderfury", rank = 1, duration = 12 },
-- Thunderfury - Attack Speed Slow (12 sec, -20% attack speed on primary target)
[27648] = { name = "thunderfury slow", rank = 1, duration = 12 },
--========================================================================
-- CUSTOM/SERVER-SPECIFIC (add your own below)
--========================================================================
-- [12345] = { name = "custom debuff", rank = 1, duration = 18 },
}
--============================================================================
-- PALADIN JUDGEMENT SEAL DETECTION
-- Maps seal buff textures to judgement debuff names and their reference spellIDs
--============================================================================
local sealToJudgement = {
-- Seal texture -> { name, referenceSpellID (must be in trackedCurseIds), judgementTexture }
["Interface\\Icons\\Spell_Holy_RighteousnessAura"] = {
name = "judgement of wisdom",
spellID = 20186, -- JoW rank 1 (in our custom list)
texture = "Interface\\Icons\\Spell_Holy_RighteousnessAura",
},
["Interface\\Icons\\Spell_Holy_HealingAura"] = {
name = "judgement of light",
spellID = 20185, -- JoL rank 1 (in our custom list)
texture = "Interface\\Icons\\Spell_Holy_HealingAura",
},
["Interface\\Icons\\Spell_Holy_HolySmite"] = {
name = "judgement of the crusader",
spellID = 21183, -- JotC rank 1 (in our custom list)
texture = "Interface\\Icons\\Spell_Holy_HolySmite",
},
["Interface\\Icons\\Spell_Holy_SealOfWrath"] = {
name = "judgement of justice",
spellID = 20184, -- JoJ rank 1 (in our custom list)
texture = "Interface\\Icons\\Spell_Holy_SealOfWrath",
},
["Interface\\Icons\\Ability_ThunderClap"] = {
name = "judgement of righteousness",
spellID = 20187, -- fallback, may need correct ID
texture = "Interface\\Icons\\Ability_ThunderClap",
},
["Interface\\Icons\\Spell_Holy_SealOfMight"] = {
name = "judgement of command",
spellID = 20467, -- fallback, may need correct ID
texture = "Interface\\Icons\\Spell_Holy_SealOfMight",
},
}
-- Detect active seal from player buffs and return judgement info
local function GetActiveSealJudgementInfo()
for i = 1, 32 do
local texture = GetPlayerBuffTexture(i)
if not texture then break end
if sealToJudgement[texture] then
return sealToJudgement[texture]
end
end
-- Fallback to generic judgement
return { name = "judgement", spellID = 20271, texture = "Interface\\Icons\\Spell_Holy_RighteousFury" }
end
--============================================================================
-- AUTOMATIC INJECTION INTO CURSIVE
-- Hooks Cursive's LoadCurses to inject our spells after every reload
--============================================================================
local injectedCount = 0
local function InjectCustomSpells()
if not Cursive or not Cursive.curses or not Cursive.curses.trackedCurseIds then
return 0
end
local count = 0
for spellID, data in pairs(CleveRoids.CustomCursiveSpells) do
-- Get texture from GetSpellRecField + GetSpellIconTexture
local name = C_Spell.GetSpellName(spellID)
local rank = C_Spell.GetSpellSubtext(spellID)
local texture = CleveRoids.libdebuff and CleveRoids.libdebuff:GetCachedIcon(spellID)
if texture then
-- Always update/add (in case Cursive reloaded and cleared them)
Cursive.curses.trackedCurseIds[spellID] = {
name = data.name,
rank = data.rank or 1,
duration = data.duration,
texture = texture,
variableDuration = data.variableDuration,
calculateDuration = data.calculateDuration,
numTicks = data.numTicks,
darkHarvest = data.darkHarvest,
}
Cursive.curses.trackedCurseNamesToTextures[data.name] = texture
count = count + 1
end
end
injectedCount = count
return count
end
-- Hook Cursive's LoadCurses function to inject after every reload
local function HookCursiveLoadCurses()
if not Cursive or not Cursive.curses or not Cursive.curses.LoadCurses then
return false
end
-- Check if already hooked
if Cursive.curses._customSpellsHooked then
return true
end
-- Store original function
local originalLoadCurses = Cursive.curses.LoadCurses
-- Replace with hooked version
Cursive.curses.LoadCurses = function(self)
-- Call original first
originalLoadCurses(self)
-- Then inject our custom spells (silent)
InjectCustomSpells()
end
Cursive.curses._customSpellsHooked = true
return true
end
--============================================================================
-- EXTENSION REGISTRATION
--============================================================================
local ext = CleveRoids.RegisterExtension("CursiveCustomSpells")
local hooked = false
local retryFrame = nil
-- Retry injection periodically until successful
local function StartRetryTimer()
if retryFrame then return end
retryFrame = CreateFrame("Frame")
local elapsed = 0
local attempts = 0
retryFrame:SetScript("OnUpdate", function()
elapsed = elapsed + arg1
if elapsed >= 0.5 then -- Check every 0.5 seconds
elapsed = 0
attempts = attempts + 1
if HookCursiveLoadCurses() then
hooked = true
InjectCustomSpells()
retryFrame:SetScript("OnUpdate", nil)
retryFrame = nil
elseif attempts >= 20 then -- Give up after 10 seconds
retryFrame:SetScript("OnUpdate", nil)
retryFrame = nil
end
end
end)
end
ext.OnLoad = function()
-- Try to hook immediately (Cursive might already be loaded)
if HookCursiveLoadCurses() then
hooked = true
InjectCustomSpells()
else
-- Start retry timer and also register events as backup
StartRetryTimer()
ext.RegisterEvent("PLAYER_ENTERING_WORLD", "OnPlayerEnteringWorld")
end
end
ext.OnPlayerEnteringWorld = function()
if not hooked then
if HookCursiveLoadCurses() then
hooked = true
InjectCustomSpells()
end
end
-- Don't unregister - keep checking on zone changes
end
--============================================================================
-- PALADIN JUDGEMENT HOOK
-- Intercepts generic Judgement (20271) and applies correct debuff name
--============================================================================
local JUDGEMENT_SPELL_ID = 20271
local judgementHooked = false
local function HookJudgementDetection()
if judgementHooked then return end
if not Cursive or not Cursive.curses then return end
-- DO NOT register 20271 in trackedCurseIds - we handle it ourselves
-- This prevents Cursive's native handler from creating a duplicate
-- Register our own UNIT_CASTEVENT handler
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("UNIT_CASTEVENT")
eventFrame:SetScript("OnEvent", function()
local casterGuid, targetGuid, event, spellID = arg1, arg2, arg3, arg4
-- Only handle our Judgement casts
if event ~= "CAST" or spellID ~= JUDGEMENT_SPELL_ID then return end
-- Check if it's the player casting
local playerGuid = CleveRoids.GetGUID("player")
if casterGuid ~= playerGuid then return end
-- Detect which seal is active and get full judgement info
local info = GetActiveSealJudgementInfo()
-- Make sure the reference spellID is in trackedCurseIds
if not Cursive.curses.trackedCurseIds[info.spellID] then
Cursive.curses.trackedCurseIds[info.spellID] = {
name = info.name,
rank = 1,
duration = 10,
texture = info.texture,
}
end
-- Apply the curse manually with the correct name
if not Cursive.curses.guids[targetGuid] then
Cursive.curses.guids[targetGuid] = {}
end
Cursive.curses.guids[targetGuid][info.name] = {
rank = 1,
duration = 10,
start = GetTime(),
spellID = info.spellID, -- Use the seal-specific spellID
targetGuid = targetGuid,
currentPlayer = true,
}
-- Add texture mapping for display
Cursive.curses.trackedCurseNamesToTextures[info.name] = info.texture
end)
judgementHooked = true
end
-- Hook judgement detection when Cursive is ready
local judgementRetryFrame = CreateFrame("Frame")
judgementRetryFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
judgementRetryFrame:SetScript("OnEvent", function()
-- Delay slightly to ensure Cursive is fully loaded
local elapsed = 0
judgementRetryFrame:SetScript("OnUpdate", function()
elapsed = elapsed + arg1
if elapsed >= 1 then
HookJudgementDetection()
judgementRetryFrame:SetScript("OnUpdate", nil)
end
end)
end)
--============================================================================
-- JUDGEMENT REFRESH ON MELEE HIT
-- Judgement of Light/Wisdom refresh their duration when the Paladin hits
--============================================================================
local judgementNames = {
["judgement of wisdom"] = true,
["judgement of light"] = true,
["judgement of the crusader"] = true,
["judgement of justice"] = true,
["judgement of righteousness"] = true,
["judgement of command"] = true,
}
-- Refresh all active judgements on a target
local function RefreshJudgementsOnTarget(targetGuid)
if not Cursive or not Cursive.curses or not Cursive.curses.guids then return end
if not targetGuid or not Cursive.curses.guids[targetGuid] then return end
local now = GetTime()
for curseName, curseData in pairs(Cursive.curses.guids[targetGuid]) do
if judgementNames[curseName] and curseData.currentPlayer then
-- Refresh the timer
curseData.start = now
end
end
end
-- Hook melee combat events for auto attacks
local meleeRefreshFrame = CreateFrame("Frame")
meleeRefreshFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_HITS")
meleeRefreshFrame:SetScript("OnEvent", function()
-- Auto attack hit patterns (vanilla WoW format)
-- "You hit TargetName for X damage."
-- "You crit TargetName for X damage."
local msg = arg1
if not msg then return end
-- Parse target name from combat message
local targetName = nil
-- Pattern: "You hit TargetName for X"
local _, _, name = string.find(msg, "^You hit (.+) for")
if name then targetName = name end
-- Pattern: "You crit TargetName for X"
if not targetName then
_, _, name = string.find(msg, "^You crit (.+) for")
if name then targetName = name end
end
if not targetName then return end
-- Check if current target matches the hit target
local currentTargetName = UnitName("target")
if currentTargetName and currentTargetName == targetName then
local targetGuid = CleveRoids.GetGUID("target")
if targetGuid then
RefreshJudgementsOnTarget(targetGuid)
end
end
end)
-- Hook UNIT_CASTEVENT for melee ability hits (Crusader Strike, etc.)
local meleeAbilityFrame = CreateFrame("Frame")
meleeAbilityFrame:RegisterEvent("UNIT_CASTEVENT")
meleeAbilityFrame:SetScript("OnEvent", function()
local casterGuid, targetGuid, event, spellID = arg1, arg2, arg3, arg4
-- Only handle CAST events (successful casts)
if event ~= "CAST" then return end
-- Check if it's the player casting
local playerGuid = CleveRoids.GetGUID("player")
if casterGuid ~= playerGuid then return end
-- Skip Judgement itself (20271) - it applies, doesn't refresh
if spellID == JUDGEMENT_SPELL_ID then return end
-- Only refresh for melee abilities (instant casts with target)
-- We can't easily detect "melee only" so refresh on any successful cast on target
if targetGuid and targetGuid ~= "" then
RefreshJudgementsOnTarget(targetGuid)
end
end)
--============================================================================
-- CONSOLE COMMANDS: /cleveroid cursive
--============================================================================
local originalConsoleHandler = CleveRoids.HandleConsoleCommand
CleveRoids.HandleConsoleCommand = function(msg)
local _, _, cmd, args = string.find(msg or "", "^(%S+)%s*(.*)$")
cmd = cmd and string.lower(cmd) or ""
if cmd == "cursive" then
local _, _, subcmd, subargs = string.find(args or "", "^(%S+)%s*(.*)$")
subcmd = subcmd and string.lower(subcmd) or "list"
if subcmd == "list" then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[CursiveCustomSpells]|r Custom spell list:")
local count = 0
for spellID, data in pairs(CleveRoids.CustomCursiveSpells) do
local status = ""
if Cursive and Cursive.curses and Cursive.curses.trackedCurseIds[spellID] then
status = " |cff00ff00[active]|r"
else
status = " |cffff0000[not loaded]|r"
end
DEFAULT_CHAT_FRAME:AddMessage(string.format(" [%d] %s (rank %d, %ds)%s",
spellID, data.name, data.rank or 1, data.duration, status))
count = count + 1
end
if count == 0 then
DEFAULT_CHAT_FRAME:AddMessage(" (none - edit CursiveCustomSpells.lua to add)")
end
DEFAULT_CHAT_FRAME:AddMessage(string.format(" Hook status: %s",
hooked and "|cff00ff00active|r" or "|cffff0000not hooked|r"))
elseif subcmd == "add" then
local _, _, spellID, duration, customName = string.find(subargs, "^(%d+)%s+(%d+)%s*(.*)$")
spellID = tonumber(spellID)
duration = tonumber(duration)
if not spellID or not duration then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Usage:|r /cleveroid cursive add <spellID> <duration> [name]")
return
end
local name = C_Spell.GetSpellName(spellID)
local rank = C_Spell.GetSpellSubtext(spellID)
local texture = CleveRoids.libdebuff and CleveRoids.libdebuff:GetCachedIcon(spellID)
if not name then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Error:|r Spell ID " .. spellID .. " not found.")
return
end
local spellName = customName ~= "" and string.lower(customName) or string.lower(name)
CleveRoids.CustomCursiveSpells[spellID] = {
name = spellName,
rank = 1,
duration = duration,
}
-- Inject immediately
if Cursive and Cursive.curses then
Cursive.curses.trackedCurseIds[spellID] = {
name = spellName,
rank = 1,
duration = duration,
texture = texture,
}
Cursive.curses.trackedCurseNamesToTextures[spellName] = texture
end
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CursiveCustomSpells]|r Added: [%d] %s (%ds) - will auto-track on cast",
spellID, spellName, duration))
DEFAULT_CHAT_FRAME:AddMessage(" Note: Edit CursiveCustomSpells.lua to make permanent.")
elseif subcmd == "remove" then
local spellID = tonumber(subargs)
if not spellID then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Usage:|r /cleveroid cursive remove <spellID>")
return
end
if CleveRoids.CustomCursiveSpells[spellID] then
local name = CleveRoids.CustomCursiveSpells[spellID].name
CleveRoids.CustomCursiveSpells[spellID] = nil
if Cursive and Cursive.curses and Cursive.curses.trackedCurseIds[spellID] then
Cursive.curses.trackedCurseIds[spellID] = nil
end
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CursiveCustomSpells]|r Removed: [%d] %s",
spellID, name))
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Error:|r Spell ID " .. spellID .. " not in custom list.")
end
elseif subcmd == "inject" then
local count = InjectCustomSpells()
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CursiveCustomSpells]|r Injected %d spell(s).", count))
elseif subcmd == "debugseal" then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[CursiveCustomSpells]|r Scanning player buffs for seals:")
for i = 1, 32 do
local texture = GetPlayerBuffTexture(i)
if not texture then break end
local match = sealToJudgement[texture] and "|cff00ff00 MATCH|r" or ""
DEFAULT_CHAT_FRAME:AddMessage(string.format(" [%d] %s%s", i, texture, match))
end
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[CursiveCustomSpells]|r Known seal textures:")
for tex, data in pairs(sealToJudgement) do
DEFAULT_CHAT_FRAME:AddMessage(string.format(" %s -> %s", tex, data.name))
end
else
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[CursiveCustomSpells]|r Commands:")
DEFAULT_CHAT_FRAME:AddMessage(" /cleveroid cursive list - Show custom spells and status")
DEFAULT_CHAT_FRAME:AddMessage(" /cleveroid cursive add <spellID> <duration> [name]")
DEFAULT_CHAT_FRAME:AddMessage(" /cleveroid cursive remove <spellID>")
DEFAULT_CHAT_FRAME:AddMessage(" /cleveroid cursive inject - Force re-injection")
end
return
end
if originalConsoleHandler then
originalConsoleHandler(msg)
end
end
return ext