From 29e7359ada2f3386ee794d2af1d3c9947909f5b1 Mon Sep 17 00:00:00 2001 From: pepopo978 Date: Thu, 26 Feb 2026 11:45:56 -0800 Subject: [PATCH 1/4] refactor and switch to using nampower events --- core.lua | 10 +- curses.lua | 246 +++++++++++++++++++-------------------------- filter.lua | 2 +- global.lua | 32 ++++-- settings.lua | 2 +- spells/hunter.lua | 40 ++++---- spells/warlock.lua | 6 +- ui.lua | 4 +- utils.lua | 2 +- 9 files changed, 158 insertions(+), 186 deletions(-) diff --git a/core.lua b/core.lua index 07d9918..1045cf2 100644 --- a/core.lua +++ b/core.lua @@ -1,4 +1,4 @@ -if not Cursive.superwow then +if not Cursive.nampower then return end @@ -24,10 +24,6 @@ Cursive.core.add = function(unit) end Cursive.core.addGuid = function(guid) - -- check if first two characters are 0x - if string.sub(guid, 1, 2) ~= "0x" then - return - end if UnitExists(guid) and not UnitIsDead(guid) then Cursive.core.guids[guid] = GetTime() end @@ -41,8 +37,8 @@ Cursive.core.enable = function() -- unitstr Cursive.core:RegisterEvent("PLAYER_TARGET_CHANGED") -- arg1 - Cursive.core:RegisterEvent("UNIT_COMBAT") -- this can get called with player/target/raid1 etc - Cursive.core:RegisterEvent("UNIT_MODEL_CHANGED") + Cursive.core:RegisterEvent("UNIT_COMBAT_GUID") -- this can get called with player/target/raid1 etc + Cursive.core:RegisterEvent("UNIT_MODEL_CHANGED_GUID") end Cursive.core.disable = function() diff --git a/curses.lua b/curses.lua index 2e8008f..932ed1e 100644 --- a/curses.lua +++ b/curses.lua @@ -1,4 +1,4 @@ -if not Cursive.superwow then +if not Cursive.nampower then return end local L = AceLibrary("AceLocale-2.2"):new("Cursive") @@ -20,6 +20,7 @@ local curses = { [52551] = true, [52552] = true, }, + travelTimeSpellIds = {}, darkHarvestData = {}, guids = {}, isChanneling = false, @@ -76,14 +77,6 @@ local curses = { -- combat events for curses local fades_test = L["(.+) fades from (.+)"] -local resist_test = L["Your (.+) was resisted by (.+)"] -local missed_test = L["Your (.+) missed (.+)"] -local parry_test = L["Your (.+) is parried by (.+)"] -local immune_test = L["Your (.+) fail.+\. (.+) is immune"] -local block_test = L["Your (.+) was blocked by (.+)"] -local dodge_test = L["Your (.+) was dodged by (.+)"] - -local molten_blast_test = L["Your Molten Blast(.+)for .+ Fire damage"] local lastGuid = nil @@ -131,9 +124,39 @@ function curses:LoadCurses() curses.trackedCurseIds = getWarriorSpells() end + -- build travelTimeSpellIds from spell data + curses.travelTimeSpellIds = {} + for id, data in pairs(curses.trackedCurseIds) do + if data.travelTime then + curses.travelTimeSpellIds[id] = true + end + end + + -- register travel time spell tracking + if next(curses.travelTimeSpellIds) and not Cursive:IsEventRegistered("SPELL_DAMAGE_EVENT_SELF") then + Cursive:RegisterEvent("SPELL_DAMAGE_EVENT_SELF", function(targetGuid, casterGuid, spellID, amount, mitigationStr, hitInfo, spellSchool, effectAuraStr) + if curses.travelTimeSpellIds[spellID] and curses.trackedCurseIds[spellID] then + lastGuid = targetGuid + local duration = curses:GetCurseDuration(spellID) + curses:ApplyCurse(spellID, targetGuid, GetTime(), duration) + end + end) + end + -- load shared debuffs curses.sharedDebuffs = getSharedDebuffs() + -- register faerie fire tracking from other players + if Cursive.db.profile.shareddebuffs.faeriefire then + if not Cursive:IsEventRegistered("SPELL_GO_OTHER") then + Cursive:RegisterEvent("SPELL_GO_OTHER", function(itemId, spellID, casterGuid, targetGuid, castFlags, numTargetsHit, numTargetsMissed) + if curses.sharedDebuffs.faeriefire[spellID] then + curses.sharedDebuffGuids.faeriefire[targetGuid] = GetTime() + end + end) + end + end + -- go through spell slots and local i = 1 while true do @@ -152,7 +175,7 @@ function curses:LoadCurses() for id, data in pairs(curses.trackedCurseIds) do -- get the texture - local name, rank, texture = SpellInfo(id) + local texture = GetSpellIconTexture(GetSpellRecField(id, "spellIconID")) -- update trackedCurseNamesToTextures curses.trackedCurseNamesToTextures[data.name] = texture -- update trackedCurseIds @@ -274,155 +297,92 @@ end); Cursive:RegisterEvent("SPELLCAST_CHANNEL_STOP", StopChanneling); Cursive:RegisterEvent("SPELLCAST_INTERRUPTED", StopChanneling); -Cursive:RegisterEvent("UNIT_CASTEVENT", function(casterGuid, targetGuid, event, spellID, castDuration) - -- immolate will fire both start and cast - if event == "CAST" then - local _, guid = UnitExists("player") - if casterGuid ~= guid then - -- check for faeriefire - if Cursive.db.profile.shareddebuffs.faeriefire and curses.sharedDebuffs.faeriefire[spellID] then - curses.sharedDebuffGuids.faeriefire[targetGuid] = GetTime() - end - - return - end - - -- store pending cast +-- player spell completions +Cursive:RegisterEvent("SPELL_GO_SELF", function(itemId, spellID, casterGuid, targetGuid, castFlags, numTargetsHit, numTargetsMissed) + if numTargetsMissed > 0 then + curses.pendingCast = {} + elseif numTargetsHit > 0 or curses.travelTimeSpellIds[spellID] then curses.pendingCast = { spellID = spellID, targetGuid = targetGuid, - castDuration = castDuration } + end - if curses.isDruid then - -- track ferocious bite cast time and target - if spellID == 22557 or - spellID == 22568 or - spellID == 22827 or - spellID == 22828 or - spellID == 22829 or - spellID == 31018 then - curses.lastFerociousBiteTime = GetTime() - curses.lastFerociousBiteTargetGuid = targetGuid - end - end + if curses.isDruid then + -- track ferocious bite cast time and target + if spellID == 22557 or + spellID == 22568 or + spellID == 22827 or + spellID == 22828 or + spellID == 22829 or + spellID == 31018 then + curses.lastFerociousBiteTime = GetTime() + curses.lastFerociousBiteTargetGuid = targetGuid + end + end - if curses.isShaman then - if spellID >= 36916 and spellID <= 36921 then - curses.lastMoltenBlastTargetGuid = targetGuid - end - end + if curses.isShaman then + if spellID >= 36916 and spellID <= 36921 then + curses.lastMoltenBlastTargetGuid = targetGuid + if numTargetsHit > 0 then + local flame_shock = L["flame shock"] + if curses:HasCurse(flame_shock, targetGuid, 0) then + curses.guids[targetGuid][flame_shock]["start"] = GetTime() + end + end + end + end - -- delay to check for resists/failures - local delay = 0.2 - - local _, _, nping = GetNetStats() - -- ignore extreme pings - if nping and nping > 0 and nping < 500 then - delay = 0.05 + (nping / 1000.0) -- convert to seconds - end - - if curses.trackedCurseIds[spellID] then + if numTargetsHit > 0 then + if curses.trackedCurseIds[spellID] and not curses.travelTimeSpellIds[spellID] then lastGuid = targetGuid - local duration = curses:GetCurseDuration(spellID) - delay - Cursive:ScheduleEvent("addCurse" .. targetGuid .. curses.trackedCurseIds[spellID].name, curses.ApplyCurse, delay, self, spellID, targetGuid, GetTime(), duration) + local duration = curses:GetCurseDuration(spellID) + curses:ApplyCurse(spellID, targetGuid, GetTime(), duration) elseif curses.conflagrateSpellIds[spellID] then - Cursive:ScheduleEvent("updateCurse" .. targetGuid .. L["conflagrate"], curses.UpdateCurse, delay, self, spellID, targetGuid, GetTime()) + curses:UpdateCurse(spellID, targetGuid, GetTime()) end - elseif event == "START" then - if curses.trackedCurseIds[spellID] then - local _, guid = UnitExists("player") - if casterGuid ~= guid then - return - end + end +end) - -- store pending cast - curses.pendingCast = { - spellID = spellID, - targetGuid = targetGuid, - castDuration = castDuration - } - end - elseif event == "FAIL" then - if curses.trackedCurseIds[spellID] then - local _, guid = UnitExists("player") - if casterGuid ~= guid then - return - end - -- clear pending cast - curses.pendingCast = {} - end - elseif event == "CHANNEL" then - -- dark harvest - if curses.darkHarvestSpellIds[spellID] then - local _, guid = UnitExists("player") - if casterGuid ~= guid then - return - end +-- player spell start (cast time spells) +Cursive:RegisterEvent("SPELL_START_SELF", function(itemId, spellID, casterGuid, targetGuid, castFlags, castTime) + if curses.trackedCurseIds[spellID] then + curses.pendingCast = { + spellID = spellID, + targetGuid = targetGuid, + castDuration = castTime / 1000 + } + end +end) - curses.darkHarvestData = { - spellID = spellID, - targetGuid = targetGuid, - castDuration = curses:ScanTooltipForDuration(spellID), - start = GetTime() - } - end +-- player spell failure +Cursive:RegisterEvent("SPELL_FAILED_SELF", function(spellID, spellResult, failedByServer) + if curses.trackedCurseIds[spellID] then + curses.pendingCast = {} + end +end) + +-- player channel start (dark harvest) +Cursive:RegisterEvent("SPELL_CHANNEL_START", function(spellID, targetGuid, durationMs) + if curses.darkHarvestSpellIds[spellID] then + curses.darkHarvestData = { + spellID = spellID, + targetGuid = targetGuid, + castDuration = curses:ScanTooltipForDuration(spellID), + start = GetTime() + } end end) -Cursive:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE", - function(message) - local spell_failed_tests = { - resist_test, - immune_test - } - -- only some classes have melee spells that need to check for dodge, parry, miss, block - if playerClassName == "DRUID" or playerClassName == "HUNTER" or playerClassName == "ROGUE" then - spell_failed_tests = { - resist_test, - immune_test, - missed_test, - parry_test, - block_test, - dodge_test - } - end +Cursive:RegisterEvent("SPELL_MISS_SELF", function(casterGuid, targetGuid, spellID, missInfo) + curses.pendingCast = {} - local spellName, failedTarget - for _, test in pairs(spell_failed_tests) do - local _, _, foundSpell, foundTarget = string.find(message, test) - if foundSpell and foundTarget then - spellName = foundSpell - failedTarget = foundTarget - break - end - end - - if spellName and failedTarget then - spellName = curses:GetLowercaseSpellName(spellName) - - curses.pendingCast = {} - - if curses.trackedCurseNamesToTextures[spellName] and lastGuid then - Cursive:CancelScheduledEvent("addCurse" .. lastGuid .. spellName) - -- check if sound should be played - if curses:ShouldPlayResistSound(lastGuid) then - PlaySoundFile("Interface\\AddOns\\Cursive\\Sounds\\resist.mp3") - end - elseif spellName == L["conflagrate"] and lastGuid then - Cursive:CancelScheduledEvent("updateCurse" .. lastGuid .. spellName) - end - return - end - - if curses.isShaman and string.find(message, molten_blast_test) then - local flame_shock = L["flame shock"] - if curses:HasCurse(flame_shock, curses.lastMoltenBlastTargetGuid, 0) then - curses.guids[curses.lastMoltenBlastTargetGuid][flame_shock]["start"] = GetTime() -- reset start time to current time - end - end - end -) -- resists + if curses.trackedCurseIds[spellID] and lastGuid then + if curses:ShouldPlayResistSound(lastGuid) then + PlaySoundFile("Interface\\AddOns\\Cursive\\Sounds\\resist.mp3") + end + end +end) Cursive:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_OTHER", function(message) -- check if spell that faded is relevant diff --git a/filter.lua b/filter.lua index 24a1063..f5ded95 100644 --- a/filter.lua +++ b/filter.lua @@ -1,4 +1,4 @@ -if not Cursive.superwow then +if not Cursive.nampower then return end diff --git a/global.lua b/global.lua index e32d14a..2c414d8 100644 --- a/global.lua +++ b/global.lua @@ -1,22 +1,36 @@ local L = AceLibrary("AceLocale-2.2"):new("Cursive") Cursive = AceLibrary("AceAddon-2.0"):new("AceEvent-2.0", "AceDebug-2.0", "AceModuleCore-2.0", "AceConsole-2.0", "AceDB-2.0", "AceHook-2.1") -Cursive.superwow = true +local function HasMinimumNampowerVersion(major, minor, patch) + if GetNampowerVersion then + local installedMajor, installedMinor, installedPatch = GetNampowerVersion() -if not GetPlayerBuffID or not CombatLogAdd or not SpellInfo then - local notify = CreateFrame("Frame", "CursiveNoSuperwow", UIParent) - notify:SetScript("OnUpdate", function() - DEFAULT_CHAT_FRAME:AddMessage(L["|cffffcc00Cursive:|cffffaaaa Couldn't detect SuperWoW."]) - this:Hide() - end) + if installedMajor > major then + return true + elseif installedMajor == major and installedMinor > minor then + return true + elseif installedMajor == major and installedMinor == minor and installedPatch >= patch then + return true + end + end - Cursive.superwow = false + return false +end + +Cursive.nampower = HasMinimumNampowerVersion(2, 37, 0) + +if not Cursive.nampower then + DEFAULT_CHAT_FRAME:AddMessage("|cffff6060Cursive:|r could not detect nampower >= v2.37.0") + return end function Cursive:OnEnable() - if not Cursive.superwow then + if not Cursive.nampower then return end + SetCVar("NP_EnableSpellStartEvents", 1) + SetCVar("NP_EnableSpellGoEvents", 1) + DEFAULT_CHAT_FRAME:AddMessage(L["|cffffcc00Cursive:|cffffaaaa Loaded. /cursive for commands and minimap icon for options."]) Cursive.curses:LoadCurses() diff --git a/settings.lua b/settings.lua index dca9fd3..7027608 100644 --- a/settings.lua +++ b/settings.lua @@ -1,4 +1,4 @@ -if not Cursive.superwow then +if not Cursive.nampower then return end diff --git a/spells/hunter.lua b/spells/hunter.lua index 31a5764..8619954 100644 --- a/spells/hunter.lua +++ b/spells/hunter.lua @@ -1,34 +1,34 @@ local L = AceLibrary("AceLocale-2.2"):new("Cursive") function getHunterSpells() return { - [3043] = { name = L["scorpid sting"], rank = 1, duration = 20 }, - [14275] = { name = L["scorpid sting"], rank = 2, duration = 20 }, - [14276] = { name = L["scorpid sting"], rank = 3, duration = 20 }, - [14277] = { name = L["scorpid sting"], rank = 4, duration = 20 }, + [3043] = { name = L["scorpid sting"], rank = 1, duration = 20, travelTime = true }, + [14275] = { name = L["scorpid sting"], rank = 2, duration = 20, travelTime = true }, + [14276] = { name = L["scorpid sting"], rank = 3, duration = 20, travelTime = true }, + [14277] = { name = L["scorpid sting"], rank = 4, duration = 20, travelTime = true }, - [1978] = { name = L["serpent sting"], rank = 1, duration = 15 }, - [13549] = { name = L["serpent sting"], rank = 2, duration = 15 }, - [13550] = { name = L["serpent sting"], rank = 3, duration = 15 }, - [13551] = { name = L["serpent sting"], rank = 4, duration = 15 }, - [13552] = { name = L["serpent sting"], rank = 5, duration = 15 }, - [13553] = { name = L["serpent sting"], rank = 6, duration = 15 }, - [13554] = { name = L["serpent sting"], rank = 7, duration = 15 }, - [13555] = { name = L["serpent sting"], rank = 8, duration = 15 }, - [25295] = { name = L["serpent sting"], rank = 9, duration = 15 }, + [1978] = { name = L["serpent sting"], rank = 1, duration = 15, travelTime = true }, + [13549] = { name = L["serpent sting"], rank = 2, duration = 15, travelTime = true }, + [13550] = { name = L["serpent sting"], rank = 3, duration = 15, travelTime = true }, + [13551] = { name = L["serpent sting"], rank = 4, duration = 15, travelTime = true }, + [13552] = { name = L["serpent sting"], rank = 5, duration = 15, travelTime = true }, + [13553] = { name = L["serpent sting"], rank = 6, duration = 15, travelTime = true }, + [13554] = { name = L["serpent sting"], rank = 7, duration = 15, travelTime = true }, + [13555] = { name = L["serpent sting"], rank = 8, duration = 15, travelTime = true }, + [25295] = { name = L["serpent sting"], rank = 9, duration = 15, travelTime = true }, - [3034] = { name = L["viper sting"], rank = 1, duration = 8 }, - [14279] = { name = L["viper sting"], rank = 2, duration = 8 }, - [14280] = { name = L["viper sting"], rank = 3, duration = 8 }, + [3034] = { name = L["viper sting"], rank = 1, duration = 8, travelTime = true }, + [14279] = { name = L["viper sting"], rank = 2, duration = 8, travelTime = true }, + [14280] = { name = L["viper sting"], rank = 3, duration = 8, travelTime = true }, [2974] = { name = L["wing clip"], rank = 1, duration = 10 }, [14267] = { name = L["wing clip"], rank = 2, duration = 10 }, [14268] = { name = L["wing clip"], rank = 3, duration = 10 }, - [5116] = { name = L["concussive shot"], rank = 1, duration = 4 }, + [5116] = { name = L["concussive shot"], rank = 1, duration = 4, travelTime = true }, - [19386] = { name = L["wyvern sting"], rank = 1, duration = 12 }, - [24132] = { name = L["wyvern sting"], rank = 2, duration = 12 }, - [24133] = { name = L["wyvern sting"], rank = 3, duration = 12 }, + [19386] = { name = L["wyvern sting"], rank = 1, duration = 12, travelTime = true }, + [24132] = { name = L["wyvern sting"], rank = 2, duration = 12, travelTime = true }, + [24133] = { name = L["wyvern sting"], rank = 3, duration = 12, travelTime = true }, [19306] = { name = L["counterattack"], rank = 1, duration = 5 }, [20909] = { name = L["counterattack"], rank = 2, duration = 5 }, diff --git a/spells/warlock.lua b/spells/warlock.lua index eeb98e2..2b71619 100644 --- a/spells/warlock.lua +++ b/spells/warlock.lua @@ -60,9 +60,9 @@ function getWarlockSpells() [11668] = { name = L["immolate"], rank = 7, duration = 15 }, [25309] = { name = L["immolate"], rank = 8, duration = 15 }, - [6789] = { name = L["death coil"], rank = 1, duration = 3 }, - [17925] = { name = L["death coil"], rank = 2, duration = 3 }, - [17926] = { name = L["death coil"], rank = 3, duration = 3 }, + [6789] = { name = L["death coil"], rank = 1, duration = 3, travelTime = true }, + [17925] = { name = L["death coil"], rank = 2, duration = 3, travelTime = true }, + [17926] = { name = L["death coil"], rank = 3, duration = 3, travelTime = true }, [710] = { name = L["banish"], rank = 1, duration = 20 }, [18647] = { name = L["banish"], rank = 2, duration = 30 }, diff --git a/ui.lua b/ui.lua index 612e294..12ffc35 100644 --- a/ui.lua +++ b/ui.lua @@ -1,4 +1,4 @@ -if not Cursive.superwow then +if not Cursive.nampower then return end @@ -184,6 +184,7 @@ ui.BarEnter = function() end this.parent.hover = true + SetMouseoverUnit(this.parent.guid) GameTooltip_SetDefaultAnchor(GameTooltip, this) GameTooltip:SetUnit(this.parent.guid) GameTooltip:Show() @@ -191,6 +192,7 @@ end ui.BarLeave = function() this.parent.hover = false + SetMouseoverUnit() GameTooltip:Hide() end diff --git a/utils.lua b/utils.lua index 4ac74aa..9b38faf 100644 --- a/utils.lua +++ b/utils.lua @@ -1,4 +1,4 @@ -if not Cursive.superwow then +if not Cursive.nampower then return end From afd74e167e8f879e496ad47b0bf18d7f2c47a7c1 Mon Sep 17 00:00:00 2001 From: pepopo978 Date: Thu, 26 Feb 2026 12:56:02 -0800 Subject: [PATCH 2/4] add new malediction option, refactor more stuff --- Cursive.toc | 8 +++---- Localization.lua | 2 ++ commands.lua | 56 ++++++++++++------------------------------------ curses.lua | 28 +++++++++++++----------- settings.lua | 2 ++ ui.lua | 18 ++-------------- 6 files changed, 39 insertions(+), 75 deletions(-) diff --git a/Cursive.toc b/Cursive.toc index 71d6bbc..41e5042 100644 --- a/Cursive.toc +++ b/Cursive.toc @@ -1,9 +1,9 @@ ## Interface: 11200 -## Title: [|CFF33AAFFSUPERWOW|R] Cursive 2.6.6 -## Title-zhCN: [|CFF33AAFFSUPERWOW|R] Cursive 2.6.6 +## Title: Cursive 3.0.0 +## Title-zhCN: Cursive 3.0.0 ## Author: Pepopo -## Notes: Uses SuperWoW GUIDs to show units in combat and track curses -## Notes-zhCN: 使用SuperWoW的GUID功能来显示战斗中的单位并监视诅咒等 +## Notes: Uses GUIDs to show units in combat and track curses +## Notes-zhCN: 使用的GUID功能来显示战斗中的单位并监视诅咒等 ## Version: 1.4.0 ## SavedVariablesPerCharacter: CursiveDB ## X-Embeds: Ace2, FuBarPlugin-2.0, CompostLib, DewDropLib, TabletLib diff --git a/Localization.lua b/Localization.lua index 5b87b7f..038e266 100644 --- a/Localization.lua +++ b/Localization.lua @@ -13,6 +13,7 @@ L:RegisterTranslations("enUS", function() ["Filter targets by name. Can be a partial match. If no match is found, the command will do nothing."] = true, ["Ignore targets with the specified spell id already on them. Useful for ignoring targets that already have a shared debuff."] = true, ["Ignore targets with the specified spell texture already on them. Useful for ignoring targets that already have a shared debuff."] = true, + ["For Warlocks with the Malediction talent: when checking if Curse of Recklessness, Curse of the Elements, or Curse of Shadow is already on a target, check for Curse of Agony instead. Default is 1 (enabled). Set malediction=0 to disable and check for the original curse."] = true, ["Time threshold at which to allow refreshing a curse. Default is 0 seconds."] = true, ["Always prioritize current target when choosing target for multicurse. Does not affect 'curse' command."] = true, ["Ignore the current target when choosing target for multicurse. Does not affect 'curse' command."] = true, @@ -221,6 +222,7 @@ L:RegisterTranslations("zhCN", function() ["Filter targets by name. Can be a partial match. If no match is found, the command will do nothing."] = "通过名称过滤目标。可以是部分匹配。如果没有找到匹配项,命令将不执行.", ["Ignore targets with the specified spell id already on them. Useful for ignoring targets that already have a shared debuff."] = "忽略已经在目标身上的指定法术ID。对于忽略已经有共享debuff的目标很有用.", ["Ignore targets with the specified spell texture already on them. Useful for ignoring targets that already have a shared debuff."] = "忽略已经在目标身上的指定法术纹理。对于忽略已经有共享debuff的目标很有用.", + ["For Warlocks with the Malediction talent: when checking if Curse of Recklessness, Curse of the Elements, or Curse of Shadow is already on a target, check for Curse of Agony instead. Default is 1 (enabled). Set malediction=0 to disable and check for the original curse."] = "对于拥有诅咒天赋的术士:检查鲁莽诅咒、元素诅咒或暗影诅咒是否已在目标身上时,改为检查痛苦诅咒。默认为1(启用)。设置malediction=0可禁用并检查原始诅咒。", ["Time threshold at which to allow refreshing a curse. Default is 0 seconds."] = "允许刷新诅咒的时间阈值。默认是0秒.例如:refreshtime=2", ["Always prioritize current target when choosing target for multicurse. Does not affect 'curse' command."] = "选择多重诅咒目标时总是优先当前目标。不影响‘诅咒’命令。", ["Ignore the current target when choosing target for multicurse. Does not affect 'curse' command."] = "选择多重诅咒目标时忽略当前目标。不影响‘诅咒’命令。", diff --git a/commands.lua b/commands.lua index edd2151..7458623 100644 --- a/commands.lua +++ b/commands.lua @@ -16,6 +16,7 @@ local commandOptions = { name = L["Filter targets by name. Can be a partial match. If no match is found, the command will do nothing."], ignorespellid = L["Ignore targets with the specified spell id already on them. Useful for ignoring targets that already have a shared debuff."], ignorespelltexture = L["Ignore targets with the specified spell texture already on them. Useful for ignoring targets that already have a shared debuff."], + malediction = L["For Warlocks with the Malediction talent: when checking if Curse of Recklessness, Curse of the Elements, or Curse of Shadow is already on a target, check for Curse of Agony instead. Default is 1 (enabled). Set malediction=0 to disable and check for the original curse."], } local commands = { @@ -77,6 +78,11 @@ local function parseOptions(optionsStr) if texture then options["ignorespelltexture"] = texture end + elseif option == "malediction" then + local _, _, maledictionVal = string.find(optionsStr, "malediction=(%d)") + if maledictionVal then + options["malediction"] = tonumber(maledictionVal) + end elseif string.find(optionsStr, option) then options[option] = true end @@ -241,32 +247,12 @@ local crowdControlledSpellIds = { } local function isMobCrowdControlled(guid) - -- check if mob is CC'ed - - -- check debuffs - for i = 1, 16 do - local _, _, _, spellId = UnitDebuff(guid, i) - if spellId then - if crowdControlledSpellIds[spellId] then - return true - end - else - break + local auras = GetUnitField(guid, "aura") + for i, spellId in pairs(auras) do + if crowdControlledSpellIds[spellId] then + return true end end - - -- check buffs - for i = 1, 32 do - local _, _, spellId = UnitBuff(guid, i) - if spellId then - if crowdControlledSpellIds[spellId] then - return true - end - else - break - end - end - return false end @@ -283,26 +269,12 @@ local function GetSquarePrioRaidTargetIndex(guid) end local function hasSpellId(guid, ignoreSpellId) - for i = 1, 16 do - local texture, stacks, spellSchool, spellId = UnitDebuff(guid, i); - if not spellId then - break - end + local auras = GetUnitField(guid, "aura") + for i, spellId in pairs(auras) do if spellId == ignoreSpellId then return true end end - - for i = 1, 32 do - local texture, stacks, spellId = UnitBuff(guid, i); - if not spellId then - break - end - if spellId == ignoreSpellId then - return true - end - end - return false end @@ -401,7 +373,7 @@ local function pickTarget(selectedPriority, lowercaseSpellNameNoRank, checkRange end if passedRangeCheck then -- check if the target has the curse - if not Cursive.curses:HasCurse(lowercaseSpellNameNoRank, guid, refreshTime) and not isMobCrowdControlled(guid) then + if not Cursive.curses:HasCurse(lowercaseSpellNameNoRank, guid, refreshTime, options["malediction"]) and not isMobCrowdControlled(guid) then local mobHp = UnitHealth(guid) if not minHp or mobHp >= minHp then local primaryValue = -1 @@ -512,7 +484,7 @@ function Cursive:Curse(spellName, targetedGuid, options) -- remove (Rank x) from spellName if it exists local lowercaseSpellNameNoRank = Cursive.utils.GetLowercaseSpellNameNoRank(spellName) - if targetedGuid and not Cursive.curses:HasCurse(lowercaseSpellNameNoRank, targetedGuid, options["refreshtime"]) and not isMobCrowdControlled(targetedGuid) then + if targetedGuid and not Cursive.curses:HasCurse(lowercaseSpellNameNoRank, targetedGuid, options["refreshtime"], options["malediction"]) and not isMobCrowdControlled(targetedGuid) then castSpellWithOptions(string.lower(spellName), lowercaseSpellNameNoRank, targetedGuid, options) return true elseif options["warnings"] then diff --git a/curses.lua b/curses.lua index 932ed1e..ed63a77 100644 --- a/curses.lua +++ b/curses.lua @@ -534,29 +534,31 @@ function curses:GetCurseData(spellName, guid) return nil end -function curses:HasCurse(lowercaseSpellNameNoRank, targetGuid, minRemaining) +function curses:HasCurse(lowercaseSpellNameNoRank, targetGuid, minRemaining, malediction) if not minRemaining then minRemaining = 0 -- default to 0 end + if malediction == nil then + malediction = 1 -- default to enabled + end + -- handle faerie fire special case if curses.isDruid and string.find(lowercaseSpellNameNoRank, L["faerie fire"]) then -- remove (feral) or (bear) from spell name lowercaseSpellNameNoRank = L["faerie fire"] end - -- handle malediction for warlocks - if curses.isWarlock and - lowercaseSpellNameNoRank == L["curse of recklessness"] or - lowercaseSpellNameNoRank == L["curse of the elements"] or - lowercaseSpellNameNoRank == L["curse of shadow"] then - -- check if they have malediction - local _, _, _, _, malediction = GetTalentInfo(1, 17) - - if malediction > 0 then - lowercaseSpellNameNoRank = L["curse of agony"] -- check for curse of agony instead - end - end + -- handle malediction for warlocks + if curses.isWarlock and malediction ~= 0 and ( + lowercaseSpellNameNoRank == L["curse of recklessness"] or + lowercaseSpellNameNoRank == L["curse of the elements"] or + lowercaseSpellNameNoRank == L["curse of shadow"]) then + local _, _, _, _, hasMalediction = GetTalentInfo(1, 17) + if hasMalediction > 0 then + lowercaseSpellNameNoRank = L["curse of agony"] + end + end if curses.guids[targetGuid] and curses.guids[targetGuid][lowercaseSpellNameNoRank] then local remaining = Cursive.curses:TimeRemaining(curses.guids[targetGuid][lowercaseSpellNameNoRank]) diff --git a/settings.lua b/settings.lua index 7027608..5147a01 100644 --- a/settings.lua +++ b/settings.lua @@ -20,6 +20,8 @@ Cursive:RegisterDefaults("profile", { showhealthbar = true, showunitname = true, + malediction = true, + shareddebuffs = { faeriefire = false, }, diff --git a/ui.lua b/ui.lua index 12ffc35..016509f 100644 --- a/ui.lua +++ b/ui.lua @@ -509,26 +509,12 @@ local function GetBarCords(row, col) end local function hasAnySpellId(guid, spellIds) - for i = 1, 16 do - local texture, stacks, spellSchool, spellId = UnitDebuff(guid, i); - if not spellId then - break - end + local auras = GetUnitField(guid, "aura") + for i, spellId in pairs(auras) do if spellIds[spellId] then return spellId end end - - for i = 1, 32 do - local texture, stacks, spellId = UnitBuff(guid, i); - if not spellId then - break - end - if spellIds[spellId] then - return spellId - end - end - return nil end From 5c45ffbbc1957badf8e69a0d6f455047f27c4419 Mon Sep 17 00:00:00 2001 From: pepopo978 Date: Fri, 27 Feb 2026 08:05:43 -0800 Subject: [PATCH 3/4] update readme --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6a1773c..d71c63d 100644 --- a/README.md +++ b/README.md @@ -100,8 +100,11 @@ All commands can take the following options separated by commas: - `name=` : "Filter targets by name. Can be a partial match. If no match is found, the command will do nothing.", - `ignorespellid=` : "Ignore targets with the specified spell id already on them. Useful for ignoring targets that already have a shared debuff.", -- `ignorespelltexture=` : "Ignore targets with the specified spell texture already on them. Useful for ignoring +- `ignorespelltexture=` : "Ignore targets with the specified spell texture already on them. Useful for ignoring targets that already have a shared debuff.", +- `malediction=<0|1>` : "For Warlocks with the Malediction talent: when checking if Curse of Recklessness, Curse of + the Elements, or Curse of Shadow is already on a target, check for Curse of Agony instead. Default is 1 (enabled). + Set `malediction=0` to disable and check for the original curse.", EXAMPLE: `/cursive multicurse Corruption|HIGHEST_HP|warnings,resistsound,expiringsound,minhp=10000,refreshtime=2` @@ -187,7 +190,7 @@ All commands will ignore targets with the following CCs on them: Multicurse will only ever target enemies that are already in combat (except if you target a mob directly first) to prevent pulling things you didn't intend like marked patrols. -For Warlocks with the Malediction talent, when checking if Curse of Recklessness, Curse of the Elements, or Curse of Shadow is already on a target, the addon will instead check for Curse of Agony to allow you to refresh agony using them. Don't use the refreshtime option with this as the server won't refresh Curse of Agony if it is still on the mob. +For Warlocks with the Malediction talent, when checking if Curse of Recklessness, Curse of the Elements, or Curse of Shadow is already on a target, the addon will instead check for Curse of Agony to allow you to refresh agony using them. This is enabled by default and can be disabled per-command with `malediction=0`. Don't use the `refreshtime` option with this as the server won't refresh Curse of Agony if it is still on the mob. Mobs with raid marks will be displayed first. From ac3ce91a27b016ae468975774adfc946c365410d Mon Sep 17 00:00:00 2001 From: pepopo978 Date: Wed, 4 Mar 2026 12:14:19 -0800 Subject: [PATCH 4/4] remove travel time from hunter spells --- curses.lua | 1 + spells/hunter.lua | 42 +++++++++++++++++++++--------------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/curses.lua b/curses.lua index ed63a77..e5c7849 100644 --- a/curses.lua +++ b/curses.lua @@ -125,6 +125,7 @@ function curses:LoadCurses() end -- build travelTimeSpellIds from spell data + -- travelTime spells must have a damage component otherwise it won't work curses.travelTimeSpellIds = {} for id, data in pairs(curses.trackedCurseIds) do if data.travelTime then diff --git a/spells/hunter.lua b/spells/hunter.lua index 8619954..e33ee4d 100644 --- a/spells/hunter.lua +++ b/spells/hunter.lua @@ -1,34 +1,34 @@ local L = AceLibrary("AceLocale-2.2"):new("Cursive") function getHunterSpells() return { - [3043] = { name = L["scorpid sting"], rank = 1, duration = 20, travelTime = true }, - [14275] = { name = L["scorpid sting"], rank = 2, duration = 20, travelTime = true }, - [14276] = { name = L["scorpid sting"], rank = 3, duration = 20, travelTime = true }, - [14277] = { name = L["scorpid sting"], rank = 4, duration = 20, travelTime = true }, + [3043] = { name = L["scorpid sting"], rank = 1, duration = 21 }, -- 1 sec added to estimate travel time. Can't use travelTime due to the spell having no direct damage component + [14275] = { name = L["scorpid sting"], rank = 2, duration = 21 }, + [14276] = { name = L["scorpid sting"], rank = 3, duration = 21 }, + [14277] = { name = L["scorpid sting"], rank = 4, duration = 21 }, - [1978] = { name = L["serpent sting"], rank = 1, duration = 15, travelTime = true }, - [13549] = { name = L["serpent sting"], rank = 2, duration = 15, travelTime = true }, - [13550] = { name = L["serpent sting"], rank = 3, duration = 15, travelTime = true }, - [13551] = { name = L["serpent sting"], rank = 4, duration = 15, travelTime = true }, - [13552] = { name = L["serpent sting"], rank = 5, duration = 15, travelTime = true }, - [13553] = { name = L["serpent sting"], rank = 6, duration = 15, travelTime = true }, - [13554] = { name = L["serpent sting"], rank = 7, duration = 15, travelTime = true }, - [13555] = { name = L["serpent sting"], rank = 8, duration = 15, travelTime = true }, - [25295] = { name = L["serpent sting"], rank = 9, duration = 15, travelTime = true }, + [1978] = { name = L["serpent sting"], rank = 1, duration = 16 }, -- 1 sec added to estimate travel time. Can't use travelTime due to the spell having no direct damage component + [13549] = { name = L["serpent sting"], rank = 2, duration = 16 }, + [13550] = { name = L["serpent sting"], rank = 3, duration = 16 }, + [13551] = { name = L["serpent sting"], rank = 4, duration = 16 }, + [13552] = { name = L["serpent sting"], rank = 5, duration = 16 }, + [13553] = { name = L["serpent sting"], rank = 6, duration = 16 }, + [13554] = { name = L["serpent sting"], rank = 7, duration = 16 }, + [13555] = { name = L["serpent sting"], rank = 8, duration = 16 }, + [25295] = { name = L["serpent sting"], rank = 9, duration = 16 }, - [3034] = { name = L["viper sting"], rank = 1, duration = 8, travelTime = true }, - [14279] = { name = L["viper sting"], rank = 2, duration = 8, travelTime = true }, - [14280] = { name = L["viper sting"], rank = 3, duration = 8, travelTime = true }, + [3034] = { name = L["viper sting"], rank = 1, duration = 9 }, -- 1 sec added to estimate travel time. Can't use travelTime due to the spell having no direct damage component + [14279] = { name = L["viper sting"], rank = 2, duration = 9 }, + [14280] = { name = L["viper sting"], rank = 3, duration = 9 }, + + [19386] = { name = L["wyvern sting"], rank = 1, duration = 13 }, -- 1 sec added to estimate travel time. Can't use travelTime due to the spell having no direct damage component + [24132] = { name = L["wyvern sting"], rank = 2, duration = 13 }, + [24133] = { name = L["wyvern sting"], rank = 3, duration = 13 }, [2974] = { name = L["wing clip"], rank = 1, duration = 10 }, [14267] = { name = L["wing clip"], rank = 2, duration = 10 }, [14268] = { name = L["wing clip"], rank = 3, duration = 10 }, - [5116] = { name = L["concussive shot"], rank = 1, duration = 4, travelTime = true }, - - [19386] = { name = L["wyvern sting"], rank = 1, duration = 12, travelTime = true }, - [24132] = { name = L["wyvern sting"], rank = 2, duration = 12, travelTime = true }, - [24133] = { name = L["wyvern sting"], rank = 3, duration = 12, travelTime = true }, + [5116] = { name = L["concussive shot"], rank = 1, duration = 5 }, -- 1 sec added to estimate travel time. Can't use travelTime due to the spell having no direct damage component [19306] = { name = L["counterattack"], rank = 1, duration = 5 }, [20909] = { name = L["counterattack"], rank = 2, duration = 5 },