From 546330f835fa43b3a8d6242a22a86d233f3ec067 Mon Sep 17 00:00:00 2001 From: pepopo978 Date: Wed, 5 Mar 2025 18:36:52 -0800 Subject: [PATCH] add option to always prio current target --- Localization.lua | 2 + README.md | 18 +- commands.lua | 11 +- curses.lua~ | 475 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 496 insertions(+), 10 deletions(-) create mode 100644 curses.lua~ diff --git a/Localization.lua b/Localization.lua index 093df43..b50d895 100644 --- a/Localization.lua +++ b/Localization.lua @@ -14,6 +14,7 @@ L:RegisterTranslations("enUS", function() ["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, ["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, ["Only choose players and ignore npcs when choosing target for multicurse. Does not affect 'curse' command."] = true, ["/cursive curse ||>: Casts spell if not already on target/guid"] = true, @@ -189,6 +190,7 @@ L:RegisterTranslations("zhCN", function() ["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的目标很有用.", ["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."] = "选择多重诅咒目标时忽略当前目标。不影响‘诅咒’命令。", ["Only choose players and ignore npcs when choosing target for multicurse. Does not affect 'curse' command."] = "选择多重诅咒目标时只选择玩家,忽略NPC。不影响‘诅咒’命令。", ["/cursive curse ||>: Casts spell if not already on target/guid"] = "/cursive curse <法术名称>|<目标ID>|<选项列表,用半角逗号分隔>: 如果目标/ID身上没有此法术,目标ID可以是以下内容(mouseover), (player), (pet), (party)(%d), (partypet)(%d), (raid)(%d+), (raidpet)(%d+), (target), (targettarget)", diff --git a/README.md b/README.md index 661d714..f8d3edc 100644 --- a/README.md +++ b/README.md @@ -75,17 +75,23 @@ All commands can take the following options separated by commas: - `expiringsound` : "Play a sound when a curse is about to expire.", - `allowooc` : "Allow out of combat targets to be multicursed. Would only consider using this solo to avoid potentially griefing raids/dungeons by pulling unintended mobs.", -- `ignoretarget` : "Ignore the current target when choosing target for multicurse. Does not affect 'curse' command.", -- `playeronly` : "Only choose players and ignore npcs when choosing target for multicurse. Does not affect 'curse' command.", +- `priotarget` : "Always prioritize current target when choosing target for multicurse. Does not affect 'curse' + command.", +- `ignoretarget` : "Ignore the current target when choosing target for multicurse. Does not affect 'curse' command.", +- `playeronly` : "Only choose players and ignore npcs when choosing target for multicurse. Does not affect 'curse' + command.", - `minhp=` : "Minimum HP for a target to be considered.", - `refreshtime=` : "Time threshold at which to allow refreshing a curse. Default is 0 seconds.", -- `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 targets that already have a shared debuff.", +- `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 + targets that already have a shared debuff.", EXAMPLE: `/cursive multicurse Corruption|HIGHEST_HP|warnings,resistsound,expiringsound,minhp=10000,refreshtime=2` -EXAMPLE: `/cursive multicurse Curse of Recklessness|RAID_MARK|name=Touched Warrior,ignorespelltexture=Spell_Shadow_UnholyStrength,resistsound,expiringsound` +EXAMPLE: +`/cursive multicurse Curse of Recklessness|RAID_MARK|name=Touched Warrior,ignorespelltexture=Spell_Shadow_UnholyStrength,resistsound,expiringsound` ## Important info diff --git a/commands.lua b/commands.lua index 1c92793..9d637aa 100644 --- a/commands.lua +++ b/commands.lua @@ -10,6 +10,7 @@ local commandOptions = { allowooc = L["Allow out of combat targets to be multicursed. Would only consider using this solo to avoid potentially griefing raids/dungeons by pulling unintended mobs."], minhp = L["Minimum HP for a target to be considered. Example usage minhp=10000. "], refreshtime = L["Time threshold at which to allow refreshing a curse. Default is 0 seconds."], + priotarget = L["Always prioritize current target when choosing target for multicurse. Does not affect 'curse' command."], ignoretarget = L["Ignore the current target when choosing target for multicurse. Does not affect 'curse' command."], playeronly = L["Only choose players and ignore npcs when choosing target for multicurse. Does not affect 'curse' command."], name = L["Filter targets by name. Can be a partial match. If no match is found, the command will do nothing."], @@ -244,7 +245,6 @@ local function GetSquarePrioRaidTargetIndex(guid) return index or -2 end - local function hasSpellId(guid, ignoreSpellId) for i = 1, 16 do local texture, stacks, spellSchool, spellId = UnitDebuff(guid, i); @@ -372,7 +372,10 @@ local function pickTarget(selectedPriority, spellNameNoRank, checkRange, options if not minHp or mobHp >= minHp then local primaryValue = -1 local secondaryValue = -1 - if selectedPriority == PRIORITY_HIGHEST_HP then + if options["priotarget"] and guid == currentTargetGuid then + seenRaidMark = true + primaryValue = 999999999999 -- should be bigger than any mob hp + elseif selectedPriority == PRIORITY_HIGHEST_HP then primaryValue = UnitHealth(guid) or 0 elseif selectedPriority == PRIORITY_RAID_MARK then primaryValue = GetRaidTargetIndex(guid) or 0 @@ -466,10 +469,10 @@ function Cursive:Curse(spellName, targetedGuid, options) end -- remove (Rank x) from spellName if it exists - local spellNameNoRank = string.lower(string.gsub(spellName, "%(.+%)", "")) + local spellNameNoRank = string.lower(string.gsub(spellName, "%(.+%)", "")) if targetedGuid and not Cursive.curses:HasCurse(spellNameNoRank, targetedGuid, options["refreshtime"]) and not isMobCrowdControlled(targetedGuid) then - castSpellWithOptions(string.lower(spellName),spellNameNoRank, targetedGuid, options) + castSpellWithOptions(string.lower(spellName), spellNameNoRank, targetedGuid, options) elseif options["warnings"] then DEFAULT_CHAT_FRAME:AddMessage(curseNoTarget) end diff --git a/curses.lua~ b/curses.lua~ new file mode 100644 index 0000000..e6b89e5 --- /dev/null +++ b/curses.lua~ @@ -0,0 +1,475 @@ +if not Cursive.superwow then + return +end +local L = AceLibrary("AceLocale-2.2"):new("Cursive") + +local _, playerClassName = UnitClass("player") + +local curses = { + trackedCurseIds = {}, + trackedCurseNamesToTextures = {}, + trackedCurseNameRanksToSpellSlots = {}, + conflagrateSpellIds = { + [17962] = true, + [18930] = true, + [18931] = true, + [18932] = true, + }, + darkHarvestSpellIds = { + [52550] = true, + [52551] = true, + [52552] = true, + }, + darkHarvestData = {}, + guids = {}, + isChanneling = false, + pendingCast = {}, + resistSoundGuids = {}, + expiringSoundGuids = {}, + requestedExpiringSoundGuids = {} -- guid added on spellcast, moved to expiringSoundGuids once rendered by ui +} + +-- combat events for curses +local fades_test = L["(.+) fades from (.+)"] +local resist_test = L["Your (.+) was resisted by (.+)"] + +local lastGuid = nil + +function curses:LoadCurses() + -- reset dicts + curses.trackedCurseIds = {} + curses.trackedCurseNamesToTextures = {} + curses.trackedCurseNameRanksToSpellSlots = {} + + -- curses to track + if playerClassName == "WARLOCK" then + curses.trackedCurseIds = getWarlockSpells() + elseif playerClassName == "PRIEST" then + curses.trackedCurseIds = getPriestSpells() + elseif playerClassName == "MAGE" then + curses.trackedCurseIds = getMageSpells() + elseif playerClassName == "DRUID" then + curses.trackedCurseIds = getDruidSpells() + elseif playerClassName == "HUNTER" then + curses.trackedCurseIds = getHunterSpells() + elseif playerClassName == "ROGUE" then + curses.trackedCurseIds = getRogueSpells() + elseif playerClassName == "SHAMAN" then + curses.trackedCurseIds = getShamanSpells() + end + + -- go through spell slots and + local i = 1 + while true do + local spellname, spellrank = GetSpellName(i, BOOKTYPE_SPELL) + if not spellname then + break + end + + if spellrank == "" then + spellrank = "Rank 1" + end + + curses.trackedCurseNameRanksToSpellSlots[string.lower(spellname) .. spellrank] = i + i = i + 1 + end + + for id, data in pairs(curses.trackedCurseIds) do + -- get the texture + local name, rank, texture = SpellInfo(id) + -- update trackedCurseNamesToTextures + curses.trackedCurseNamesToTextures[data.name] = texture + -- update trackedCurseIds + curses.trackedCurseIds[id].texture = texture + end +end + +function curses:ScanTooltipForDuration(curseSpellID) + -- scan spellbook for duration in case they have haste talent + local nameRank = curses.trackedCurseIds[curseSpellID].name .. "Rank " .. curses.trackedCurseIds[curseSpellID].rank + local spellSlot = curses.trackedCurseNameRanksToSpellSlots[nameRank] + + if spellSlot then + Cursive.core.tooltipScan:SetOwner(Cursive.core.tooltipScan, "ANCHOR_NONE") + Cursive.core.tooltipScan:ClearLines() + Cursive.core.tooltipScan:SetSpell(spellSlot, BOOKTYPE_SPELL) + local numLines = Cursive.core.tooltipScan:NumLines() + if numLines and numLines > 0 then + -- get the last line + local text = getglobal("CursiveTooltipScan" .. "TextLeft" .. numLines):GetText() + if text then + local _, _, duration = string.find(text, L["curse_duration_format"]) + if duration then + return tonumber(duration) + end + end + end + end + + return curses.trackedCurseIds[curseSpellID].duration +end + +function curses:GetCurseDuration(curseSpellID) + if curses.trackedCurseIds[curseSpellID].variableDuration then + return curses:ScanTooltipForDuration(curseSpellID) + end + + return curses.trackedCurseIds[curseSpellID].duration +end + +function curses:ScanGuidForCurse(guid, curseSpellID) + for i = 1, 64 do + local _, _, _, spellID = UnitDebuff(guid, i) + if spellID then + if spellID == curseSpellID then + return true + end + else + break + end + end + for i = 1, 64 do + local _, _, spellID = UnitBuff(guid, i) + if spellID then + if spellID == curseSpellID then + return true + end + else + break + end + end + + return nil +end + +Cursive:RegisterEvent("LEARNED_SPELL_IN_TAB", function() + -- reload curses in case spell slots changed + curses:LoadCurses() +end) + +local function StopChanneling() + curses.isChanneling = false +end + +Cursive:RegisterEvent("SPELLCAST_CHANNEL_START", function() + curses.isChanneling = true +end); +Cursive:RegisterEvent("SPELLCAST_CHANNEL_STOP", StopChanneling); +Cursive:RegisterEvent("SPELLCAST_INTERRUPTED", StopChanneling); +Cursive:RegisterEvent("SPELLCAST_FAILED", StopChanneling); +-- +--Cursive:RegisterEvent("SPELL_DAMAGE_EVENT_SELF", +-- function(targetGuidStr, +-- casterGuidStr, +-- spellId, +-- amount, +-- spellSchool, +-- mitigationStr, +-- hitInfo, +-- effectAuraStr) +-- print(targetGuidStr .. " " .. casterGuidStr .. " " .. tostring(spellId) .. " " .. tostring(amount) .. " " .. tostring(spellSchool) .. " " .. mitigationStr .. " " .. hitInfo .. " " .. effectAuraStr) +-- end); +-- +--Cursive:RegisterEvent("RAW_COMBATLOG", function(arg1, arg2) +-- print(arg1 .. " " .. arg2) +--end); + +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 + return + end + + -- store pending cast + curses.pendingCast = { + spellID = spellID, + targetGuid = targetGuid, + castDuration = castDuration + } + + if curses.trackedCurseIds[spellID] then + lastGuid = targetGuid + Cursive:ScheduleEvent("addCurse" .. targetGuid .. curses.trackedCurseIds[spellID].name, curses.ApplyCurse, 0.2, self, spellID, targetGuid, GetTime()) + elseif curses.conflagrateSpellIds[spellID] then + Cursive:ScheduleEvent("updateCurse" .. targetGuid .. L["conflagrate"], curses.UpdateCurse, 0.2, self, spellID, targetGuid, GetTime()) + end + elseif event == "START" then + if curses.trackedCurseIds[spellID] then + local _, guid = UnitExists("player") + if casterGuid ~= guid then + return + 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 + + curses.darkHarvestData = { + spellID = spellID, + targetGuid = targetGuid, + castDuration = curses:ScanTooltipForDuration(spellID), + start = GetTime() + } + end + end +end) + +Cursive:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE", + function(message) + -- check for resist + local _, _, spellName, target = string.find(message, resist_test) + if spellName and target then + spellName = string.lower(spellName) + + -- clear pending cast + 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 + end + end +) -- resists + +Cursive:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_OTHER", function(message) + -- check if spell that faded is relevant + local _, _, spellName, target = string.find(message, fades_test) + if spellName and target then + spellName = string.lower(spellName) + if curses.trackedCurseNamesToTextures[spellName] then + -- loop through targets with active curses + for guid, data in pairs(curses.guids) do + for curseName, curseData in pairs(data) do + if curseName == spellName then + -- see if target still has that curse + if not curses:ScanGuidForCurse(guid, curseData.spellID) then + -- remove curse + curses:RemoveCurse(guid, curseName) + end + end + end + end + end + end +end +) + +function curses:GetLastTickTime(curseData) + local ticks = curses.trackedCurseIds[curseData.spellID].numTicks + if not ticks then + return GetTime() + end + + local tickTime = curseData.duration / ticks + local currentTime = GetTime() + tickTime * .2 -- dh won't apply to previous tick if within 20% of tick time + + return math.floor((currentTime - curseData.start) / tickTime) * tickTime + curseData.start +end + +function curses:TrackDarkHarvest(curseData) + if curses.darkHarvestData["targetGuid"] and curses.darkHarvestData["targetGuid"] == curseData["targetGuid"] then + local dhActive = false + -- check if still channeling + if curses.isChanneling then + local dhTimeRemaining = curses.darkHarvestData.castDuration - (GetTime() - curses.darkHarvestData.start) + -- check if dh still active based on cast duration + if dhTimeRemaining > 0 then + dhActive = true + -- dh is active + if not curseData["dhStartTime"] then + curseData["dhStartTime"] = curses:GetLastTickTime(curseData) -- dh will reduce full tick duration + end + end + end + if curseData["dhStartTime"] and dhActive == false and not curseData["dhEndTime"] then + -- if dh no longer active, store end time if not already stored + curseData["dhEndTime"] = GetTime() + end + end +end + +function curses:GetDarkHarvestReduction(curseData) + if curseData["dhStartTime"] then + local endTime = curseData["dhEndTime"] or GetTime() + local dhActiveTime = endTime - curseData["dhStartTime"] + if dhActiveTime > 0 then + return dhActiveTime * .2 -- 20% reduction + end + end + return 0 +end + +function curses:TimeRemaining(curseData) + local dhReduction = 0 + + if curses.trackedCurseIds[curseData.spellID].darkHarvest then + curses:TrackDarkHarvest(curseData) + + dhReduction = curses:GetDarkHarvestReduction(curseData) + end + + local remaining = curseData.duration - (GetTime() - curseData.start) - dhReduction + if Cursive.db.profile.curseshowdecimals and remaining < 10 then + -- round to 1 decimal point + remaining = math.floor(remaining * 10) / 10 + else + remaining = math.ceil(remaining) + end + + return remaining +end + +function curses:EnableResistSound(guid) + curses.resistSoundGuids[guid] = true +end + +function curses:EnableExpiringSound(spellNameNoRank, guid) + if curses.requestedExpiringSoundGuids[guid] and curses.requestedExpiringSoundGuids[guid][spellNameNoRank] then + curses.requestedExpiringSoundGuids[guid][spellNameNoRank] = nil + end + + if not curses.expiringSoundGuids[guid] then + curses.expiringSoundGuids[guid] = {} + end + curses.expiringSoundGuids[guid][spellNameNoRank] = true +end + +function curses:RequestExpiringSound(spellNameNoRank, guid) + if not curses.requestedExpiringSoundGuids[guid] then + curses.requestedExpiringSoundGuids[guid] = {} + end + curses.requestedExpiringSoundGuids[guid][spellNameNoRank] = true +end + +function curses:HasRequestedExpiringSound(spellNameNoRank, guid) + return curses.requestedExpiringSoundGuids[guid] and curses.requestedExpiringSoundGuids[guid][spellNameNoRank] +end + +function curses:ShouldPlayExpiringSound(spellNameNoRank, guid) + if curses.expiringSoundGuids[guid] and curses.expiringSoundGuids[guid][spellNameNoRank] then + curses.expiringSoundGuids[guid][spellNameNoRank] = nil -- remove entry to avoid playing sound multiple times + return true + end + + return false +end + +function curses:ShouldPlayResistSound(guid) + if curses.resistSoundGuids[guid] then + curses.resistSoundGuids[guid] = nil -- remove entry to avoid playing sound multiple times + return true + end + + return false +end + +function curses:HasAnyCurse(guid) + if curses.guids[guid] and next(curses.guids[guid]) then + return true + end + return nil +end + +function curses:HasCurse(spellName, targetGuid, minRemaining) + if not minRemaining then + minRemaining = 0 -- default to 0 + end + + if curses.guids[targetGuid] and curses.guids[targetGuid][spellName] then + local remaining = Cursive.curses:TimeRemaining(curses.guids[targetGuid][spellName]) + if remaining > minRemaining then + return true + end + end + + -- check pending cast + if curses.pendingCast and + curses.pendingCast.targetGuid == targetGuid and + curses.pendingCast.spellID and + curses.trackedCurseIds[curses.pendingCast.spellID] and + curses.trackedCurseIds[curses.pendingCast.spellID].name == spellName then + return true + end + + return nil +end + +function curses:ApplyCurse(spellID, targetGuid, startTime) + -- clear pending cast + curses.pendingCast = {} + + local name = curses.trackedCurseIds[spellID].name + local rank = curses.trackedCurseIds[spellID].rank + local duration = curses:GetCurseDuration(spellID) + + if not curses.guids[targetGuid] then + curses.guids[targetGuid] = {} + end + + curses.guids[targetGuid][name] = { + rank = rank, + duration = duration, + start = startTime, + spellID = spellID, + targetGuid = targetGuid + } +end + +function curses:UpdateCurse(spellID, targetGuid, startTime) + -- clear pending cast + curses.pendingCast = {} + + if curses.conflagrateSpellIds[spellID] then + -- check if target has immolate + if curses:HasCurse(L["immolate"], targetGuid) then + -- reduce duration by 3 sec + curses.guids[targetGuid][L["immolate"]].duration = curses.guids[targetGuid][L["immolate"]].duration - 3 + end + end +end + +function curses:RemoveCurse(guid, curseName) + if curses.guids[guid] and curses.guids[guid][curseName] then + curses.guids[guid][curseName] = nil + end + if curses.expiringSoundGuids[guid] and curses.expiringSoundGuids[guid][curseName] then + curses.expiringSoundGuids[guid][curseName] = nil + end +end + +function curses:RemoveGuid(guid) + curses.guids[guid] = nil + curses.resistSoundGuids[guid] = nil + curses.expiringSoundGuids[guid] = nil + curses.requestedExpiringSoundGuids[guid] = nil +end + +Cursive.curses = curses