local curseCommands = "|cffffcc00Cursive:|cffffaaaa Commands:" local priorityChoices = "Priority choices:" local curseOptions = "Options (separate with ,):" local commandOptions = { warnings = "Display text warnings when a curse fails to cast.", resistsound = "Play a sound when a curse is resisted.", 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.", minhp = "Minimum HP for a target to be considered. Example usage minhp=10000. ", refreshtime = "Time threshold at which to allow refreshing a curse. Default is 0 seconds." } local commands = { ["curse"] = "/cursive curse ||>: Casts spell if not already on target/guid", ["multicurse"] = "/cursive multicurse ||>: Picks target based on priority and casts spell if not already on target", } local PRIORITY_HIGHEST_HP = "HIGHEST_HP" local PRIORITY_RAID_MARK = "RAID_MARK" local PRIORITY_RAID_MARK_SQUARE = "RAID_MARK_SQUARE" local PRIORITY_INVERSE_RAID_MARK = "INVERSE_RAID_MARK" local PRIORITY_HIGHEST_HP_RAID_MARK = "HIGHEST_HP_RAID_MARK" local PRIORITY_HIGHEST_HP_RAID_MARK_SQUARE = "HIGHEST_HP_RAID_MARK_SQUARE" local PRIORITY_HIGHEST_HP_INVERSE_RAID_MARK = "HIGHEST_HP_INVERSE_RAID_MARK" local priorities = { [PRIORITY_HIGHEST_HP] = "Target with the highest HP.", [PRIORITY_RAID_MARK] = "Target with the highest raid mark.", [PRIORITY_RAID_MARK_SQUARE] = "Target with the highest raid mark with Cross set to -1 and Skull set to -2 (Square highest prio at 6).", [PRIORITY_INVERSE_RAID_MARK] = "Target with the lowest raid mark.", [PRIORITY_HIGHEST_HP_RAID_MARK] = "Target with the highest HP and raid mark.", [PRIORITY_HIGHEST_HP_RAID_MARK_SQUARE] = "Same as HIGHEST_HP_RAID_MARK but with RAID_MARK_SQUARE mark prio.", [PRIORITY_HIGHEST_HP_INVERSE_RAID_MARK] = "Same as HIGHEST_HP_RAID_MARK but with INVERSE_RAID_MARK mark prio." } local curseNoTarget = "|cffffcc00Cursive:|cffffaaaa Couldn't find a target to curse." local function parseOptions(optionsStr) local options = { } if optionsStr then for option, _ in pairs(commandOptions) do -- special case for minhp as it takes a param if option == "minhp" then local _, _, minHp = string.find(optionsStr, "minhp=(%d+)") if minHp then options["minhp"] = tonumber(minHp) end elseif option == "refreshtime" then local _, _, refreshTime = string.find(optionsStr, "refreshtime=(%d+)") if refreshTime then options["refreshtime"] = tonumber(refreshTime) end elseif string.find(optionsStr, option) then options[option] = true end end end return options end local function handleSlashCommands(msg, editbox) if not msg or msg == "" then DEFAULT_CHAT_FRAME:AddMessage(curseCommands) for command, description in pairs(commands) do DEFAULT_CHAT_FRAME:AddMessage(description) end DEFAULT_CHAT_FRAME:AddMessage(priorityChoices) for priority, description in pairs(priorities) do DEFAULT_CHAT_FRAME:AddMessage(priority .. ": " .. description) end DEFAULT_CHAT_FRAME:AddMessage(curseOptions) for option, description in pairs(commandOptions) do DEFAULT_CHAT_FRAME:AddMessage(option .. ": " .. description) end return end -- get first word in string local _, _, command, args = string.find(msg, "(%w+) (.*)") if command == "curse" then local spellName, targetedGuid, optionsStr = Cursive.utils.strsplit("|", args) local options = parseOptions(optionsStr) Cursive:Curse(spellName, targetedGuid, options) elseif command == "multicurse" then local spellName, priority, optionsStr = Cursive.utils.strsplit("|", args) local options = parseOptions(optionsStr) Cursive:Multicurse(spellName, priority, options) end end local crowdControlledSpellIds = { [700] = { name = "Sleep", rank = 1, duration = 20 }, [1090] = { name = "Sleep", rank = 2, duration = 30 }, [2937] = { name = "Sleep", rank = 3, duration = 40 }, [339] = { name = "Entangling Roots", rank = 1, duration = 12 }, [1062] = { name = "Entangling Roots", rank = 2, duration = 15 }, [5195] = { name = "Entangling Roots", rank = 3, duration = 18 }, [5196] = { name = "Entangling Roots", rank = 4, duration = 21 }, [9852] = { name = "Entangling Roots", rank = 5, duration = 24 }, [9853] = { name = "Entangling Roots", rank = 6, duration = 27 }, [2637] = { name = "Hibernate", rank = 1, duration = 20 }, [18657] = { name = "Hibernate", rank = 2, duration = 30 }, [18658] = { name = "Hibernate", rank = 3, duration = 40 }, [1425] = { name = "Shackle Undead", rank = 1, duration = 30 }, [9486] = { name = "Shackle Undead", rank = 2, duration = 40 }, [10956] = { name = "Shackle Undead", rank = 3, duration = 50 }, -- polymorph [118] = { name = "Polymorph", rank = "Rank 1", duration = 20 }, [12824] = { name = "Polymorph", rank = "Rank 2", duration = 30 }, [12825] = { name = "Polymorph", rank = "Rank 3", duration = 40 }, [12826] = { name = "Polymorph", rank = "Rank 4", duration = 50 }, [28270] = { name = "Polymorph: Cow", rank = "Rank 1", duration = 50 }, [28271] = { name = "Polymorph: Turtle", rank = "Rank 1", duration = 50 }, [28272] = { name = "Polymorph: Pig", rank = "Rank 1", duration = 50 }, [2878] = { name = "Turn Undead", rank = 1, duration = 10 }, [5627] = { name = "Turn Undead", rank = 2, duration = 15 }, [10326] = { name = "Turn Undead", rank = 3, duration = 20 }, [2094] = { name = "Blind", rank = 1, duration = 10 }, [21060] = { name = "Blind", rank = 1, duration = 10 }, [6770] = { name = "Sap", rank = 1, duration = 25 }, [2070] = { name = "Sap", rank = 2, duration = 35 }, [11297] = { name = "Sap", rank = 3, duration = 45 }, [1776] = { name = "Gouge", rank = 1, duration = 4 }, [1777] = { name = "Gouge", rank = 2, duration = 4 }, [8629] = { name = "Gouge", rank = 3, duration = 4 }, [11285] = { name = "Gouge", rank = 4, duration = 4 }, [11286] = { name = "Gouge", rank = 5, duration = 4 }, [3355] = { name = "Freezing Trap", rank = 1, duration = 10 }, [14308] = { name = "Freezing Trap", rank = 2, duration = 15 }, [14309] = { name = "Freezing Trap", rank = 3, duration = 20 }, [710] = { name = "Banish", rank = 1, duration = 30 }, [18647] = { name = "Banish", rank = 2, duration = 30 }, } 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 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 local function GetSquarePrioRaidTargetIndex(guid) local index = GetRaidTargetIndex(guid) if index == 7 then return 0 -- cross becomes 0 elseif index == 8 then return -1 -- skull becomes -1 elseif index == 0 then return -2 -- nomark becomes -2 end return index or -2 end local function pickTarget(selectedPriority, spellNameNoRank, checkRange, options) -- Curse the target that best matches the selected priority local highestPrimaryValue = -10 local highestSecondaryValue = -10 local targetedGuid = nil local minHp = options["minhp"] local ignoreInFight = options["allowooc"] local refreshTime = options["refreshtime"] local _, currentTargetGuid = UnitExists("target") local seenRaidMark = nil -- if we have seen a raid mark for guid, time in pairs(Cursive.core.guids) do -- apply filters local shouldDisplay = Cursive:ShouldDisplayGuid(guid) -- check if target displayed if shouldDisplay then -- check if in combat already or player is actively targeting the mob if ignoreInFight or Cursive.filter.infight(guid) or guid == currentTargetGuid then -- prioritize targets within 28 yards first to improve chances of being in range if checkRange == false or CheckInteractDistance(guid, 4) then -- check if the target has the curse if not Cursive.curses:HasCurse(spellNameNoRank, guid, refreshTime) and not isMobCrowdControlled(guid) then local mobHp = UnitHealth(guid) if not minHp or mobHp >= minHp then local primaryValue = -1 local secondaryValue = -1 if selectedPriority == PRIORITY_HIGHEST_HP then primaryValue = UnitHealth(guid) or 0 elseif selectedPriority == PRIORITY_RAID_MARK then primaryValue = GetRaidTargetIndex(guid) or 0 elseif selectedPriority == PRIORITY_RAID_MARK_SQUARE then primaryValue = GetSquarePrioRaidTargetIndex(guid) elseif selectedPriority == PRIORITY_INVERSE_RAID_MARK then primaryValue = -1 * (GetRaidTargetIndex(guid) or 9) elseif selectedPriority == PRIORITY_HIGHEST_HP_RAID_MARK then secondaryValue = GetRaidTargetIndex(guid) or 0 if secondaryValue > 0 and not seenRaidMark then highestPrimaryValue = -10 -- reset highestPriorityValue if this is the first raid mark we've seen seenRaidMark = true end primaryValue = UnitHealth(guid) or 0 elseif selectedPriority == PRIORITY_HIGHEST_HP_RAID_MARK_SQUARE then secondaryValue = GetSquarePrioRaidTargetIndex(guid) if secondaryValue > -2 and not seenRaidMark then highestPrimaryValue = -10 -- reset highestPriorityValue if this is the first raid mark we've seen seenRaidMark = true end primaryValue = UnitHealth(guid) or 0 elseif selectedPriority == PRIORITY_HIGHEST_HP_INVERSE_RAID_MARK then secondaryValue = -1 * (GetRaidTargetIndex(guid) or 9) if secondaryValue > -9 and not seenRaidMark then highestPrimaryValue = -10 -- reset highestPriorityValue if this is the first raid mark we've seen seenRaidMark = true end primaryValue = UnitHealth(guid) or 0 end if primaryValue > highestPrimaryValue then highestPrimaryValue = primaryValue highestSecondaryValue = secondaryValue targetedGuid = guid elseif primaryValue == highestPrimaryValue and secondaryValue > highestSecondaryValue then highestSecondaryValue = secondaryValue targetedGuid = guid end end end end end end end -- run again if no target found ignoring range if not targetedGuid and checkRange == true then targetedGuid = pickTarget(selectedPriority, spellNameNoRank, false, options) end return targetedGuid end local function castSpellWithOptions(spellName, spellNameNoRank, targetedGuid, options) if options["resistsound"] then Cursive.curses:EnableResistSound(targetedGuid) end if options["expiringsound"] then Cursive.curses:RequestExpiringSound(spellNameNoRank, targetedGuid) end CastSpellByName(spellName, targetedGuid) end function Cursive:Curse(spellName, targetedGuid, options) if not spellName or not targetedGuid then DEFAULT_CHAT_FRAME:AddMessage(commands["curse"]) return end if targetedGuid and string.sub(targetedGuid, 1, 2) ~= "0x" then _, targetedGuid = UnitExists(targetedGuid) if not targetedGuid then if options["warnings"] then DEFAULT_CHAT_FRAME:AddMessage(curseNoTarget) end return end end -- remove (Rank x) from spellName if it exists local spellNameNoRank = string.gsub(spellName, "%(.+%)", "") if targetedGuid and not Cursive.curses:HasCurse(spellNameNoRank, targetedGuid, options["refreshtime"]) and not isMobCrowdControlled(targetedGuid) then castSpellWithOptions(spellName, spellNameNoRank, targetedGuid, options) elseif options["warnings"] then DEFAULT_CHAT_FRAME:AddMessage(curseNoTarget) end end function Cursive:Multicurse(spellName, priority, options) if not spellName then DEFAULT_CHAT_FRAME:AddMessage(commands["multicurse"]) return end if priority and not priorities[priority] then DEFAULT_CHAT_FRAME:AddMessage(priorityChoices) for choice, description in pairs(priorities) do DEFAULT_CHAT_FRAME:AddMessage(choice .. ": " .. description) end return end local selectedPriority = priority or PRIORITY_HIGHEST_HP -- remove (Rank x) from spellName if it exists local spellNameNoRank = string.gsub(spellName, "%(.+%)", "") local targetedGuid = pickTarget(selectedPriority, spellNameNoRank, true, options) if targetedGuid then castSpellWithOptions(spellName, spellNameNoRank, targetedGuid, options) elseif options["warnings"] then DEFAULT_CHAT_FRAME:AddMessage(curseNoTarget) end end SLASH_CURSIVE1 = "/cursive" --creating the slash command SlashCmdList["CURSIVE"] = handleSlashCommands --associating the function with the slash command