Features:
- Increased pvp safety
- Syncronized mana potions
- Automatically uses the best strike
- Supports all ultimates, waves, ball aoe runes and strikes
More Details
When checking for players the script adds some extra size on top of the spell's natural aoe in order to take moving players into account. How much extra space that should be added can be configured and should depend on your ping. This extra space is also added around the position in front of you when checking if a strike is pvp safe. Furthermore, if a strike fails the pvp-check the script will try to turn away from nearby players before retrying.
Keep in mind that the bot can only see one square outside of the screen in each direction.
Because of this it will only consider squares close to the character when selecting the best tile to shoot an aoe rune. How close depends on what buffer size you have configured. Finally, some beams and ultimates are too large to check with any buffer size, the script will still try to cast those spells if you have configured it to do so though.
The script also comes bundled with a potions drinker. The potions drinker and spellcaster keeps track of cooldowns and potions will only be drunk if at least one of these circumstances hold:
- There's at most 800ms left until the next spellcast.
- There's at most one rune target on the screen.
- Your mana is critically low.
The script also has a list of blacklist ids. Here you should add things like holes, teleport exits and rope spots. If one of these ids are on the screen the script will also check for players on the floors just above and just below you. However, note that level spy doesn't work between the ground floor and the first underground floor.
In the config you'll be asked to add a list of monsters for each spell. If this parameter is left out all monsters will be considered for that spell. If you use a string instead of a table the script will treat it as a targeting category. The script checks the contents of the category when you start the script, not when it casts spells. If you change your category you need to restart the script.
In the config you can also add 'minhp' and 'maxhp' fields to filter out which monsters should be considered. You can also add an 'atLeastOne' parameter -- the spell will the only be used if at least one of the targets is in the specified list. This is for example useful for dealing with Demon Skeletons in the tombs.
Finally, the built in magic shooter and potions drinker should be disabled while running this script.
init start
-----------------------------------------------------------------------
-------------------------------- CONFIG -------------------------------
-----------------------------------------------------------------------
Config = {
pvpsafe = true, -- Do you want to avoid hitting other players with spells?
buffersize = 2, -- How far outside of the spell's aoe to check for players?
ignoreParty = true, -- Is it ok to hit party members? Enable if you want to team hunt.
multiFloor = true, -- Check for players on other floors?
blacklistIds = {469, 1959, 1960}, -- Enable multi floor checks regardless of above settings if one of these items are on the screen. Add stuff like stairs and rope holes.
manaPotion = "ultimate mana potion", -- Which mana potion to use?
useStrongStrikes = true, -- Do you want to use strong strikes?
turnForWaves = false, -- Do you want to wave in directions other than the on your are facing?
spells = {
{name = "exevo gran mas vis", count = 9},
{name = "exevo gran mas vis", monsters = {"devourer", "blood Beast", "rot elemental"}, count = 7, minhp = 20},
{name = "thunderstorm rune", count = 5, monsters = {"devourer", "blood Beast", "rot elemental", "Quara Predator Scout", "Quara Hydromancer Scout", "quara pincher scout", "quara constrictor scout"}},
{name = "exevo vis hur", monsters = 't', count = 2},
{name = "thunderstorm rune", count = 2, monsters = {"devourer", "blood Beast", "rot elemental", "Quara Predator Scout", "Quara Hydromancer Scout", "quara pincher scout", "quara constrictor scout"}},
{name = "sudden death rune", monsters = {"devourer"}},
}
}
-----------------------------------------------------------------------
---------------------------- END OF CONFIG ----------------------------
-----------------------------------------------------------------------
local NORTH, EAST, SOUTH, WEST = 'n', 'e', 's', 'w'
local lastPlayerSeenTime = 0
-- All masks must be quadratical. You cannot leave out the zeros.
-- Also, all masks must have sizes that are odd numbers.
masks = {
ball = {
{0,0,1,1,1,0,0},
{0,1,1,1,1,1,0},
{1,1,1,1,1,1,1},
{1,1,1,1,1,1,1},
{1,1,1,1,1,1,1},
{0,1,1,1,1,1,0},
{0,0,1,1,1,0,0},
},
["exevo gran mas flam"] = {
{0,0,0,0,0,1,0,0,0,0,0},
{0,0,0,1,1,1,1,1,0,0,0},
{0,0,1,1,1,1,1,1,1,0,0},
{0,1,1,1,1,1,1,1,1,1,0},
{0,1,1,1,1,1,1,1,1,1,0},
{1,1,1,1,1,1,1,1,1,1,1},
{0,1,1,1,1,1,1,1,1,1,0},
{0,1,1,1,1,1,1,1,1,1,0},
{0,0,1,1,1,1,1,1,1,0,0},
{0,0,0,1,1,1,1,1,0,0,0},
{0,0,0,0,0,1,0,0,0,0,0},
},
["exevo gran mas vis"] = {
{0,0,0,0,0,0,1,0,0,0,0,0,0},
{0,0,0,0,0,1,1,1,0,0,0,0,0},
{0,0,0,0,1,1,1,1,1,0,0,0,0},
{0,0,0,1,1,1,1,1,1,1,0,0,0},
{0,0,1,1,1,1,1,1,1,1,1,0,0},
{0,1,1,1,1,1,1,1,1,1,1,1,0},
{1,1,1,1,1,1,1,1,1,1,1,1,1},
{0,1,1,1,1,1,1,1,1,1,1,1,0},
{0,0,1,1,1,1,1,1,1,1,1,0,0},
{0,0,0,1,1,1,1,1,1,1,0,0,0},
{0,0,0,0,1,1,1,1,1,0,0,0,0},
{0,0,0,0,0,1,1,1,0,0,0,0,0},
{0,0,0,0,0,0,1,0,0,0,0,0,0},
},
["exevo gran mas frigo"] = {
{0,0,0,0,0,1,0,0,0,0,0},
{0,0,0,0,1,1,1,0,0,0,0},
{0,0,0,1,1,1,1,1,0,0,0},
{0,0,1,1,1,1,1,1,1,0,0},
{0,1,1,1,1,1,1,1,1,1,0},
{1,1,1,1,1,1,1,1,1,1,1},
{0,1,1,1,1,1,1,1,1,1,0},
{0,0,1,1,1,1,1,1,1,0,0},
{0,0,0,1,1,1,1,1,0,0,0},
{0,0,0,0,1,1,1,0,0,0,0},
{0,0,0,0,0,1,0,0,0,0,0},
},
["exevo gran mas tera"] = {
{0,0,0,0,0,0,1,0,0,0,0,0,0},
{0,0,0,0,1,1,1,1,1,0,0,0,0},
{0,0,0,1,1,1,1,1,1,1,0,0,0},
{0,0,1,1,1,1,1,1,1,1,1,0,0},
{0,1,1,1,1,1,1,1,1,1,1,1,0},
{0,1,1,1,1,1,1,1,1,1,1,1,0},
{1,1,1,1,1,1,1,1,1,1,1,1,1},
{0,1,1,1,1,1,1,1,1,1,1,1,0},
{0,1,1,1,1,1,1,1,1,1,1,1,0},
{0,0,1,1,1,1,1,1,1,1,1,0,0},
{0,0,0,1,1,1,1,1,1,1,0,0,0},
{0,0,0,0,1,1,1,1,1,0,0,0,0},
{0,0,0,0,0,0,1,0,0,0,0,0,0},
},
-- Waves should be entered as their NORTH direction.
-- The extra zeros in the other directions are needed for the
-- rotations to work properly.
-- Also frigo hur and gran frigo hur
["exevo flam hur"] = {
{0,0,0,1,1,1,0,0,0,},
{0,0,0,1,1,1,0,0,0,},
{0,0,0,0,1,0,0,0,0,},
{0,0,0,0,1,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,},
},
["exevo vis hur"] = {
{0,0,0,0,1,1,1,0,0,0,0,},
{0,0,0,0,1,1,1,0,0,0,0,},
{0,0,0,0,0,1,0,0,0,0,0,},
{0,0,0,0,0,1,0,0,0,0,0,},
{0,0,0,0,0,1,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,},
},
["exevo vis lux"] = {
{0,0,0,0,0,1,0,0,0,0,0,},
{0,0,0,0,0,1,0,0,0,0,0,},
{0,0,0,0,0,1,0,0,0,0,0,},
{0,0,0,0,0,1,0,0,0,0,0,},
{0,0,0,0,0,1,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,},
},
["exevo gran vis lux"] = {
{0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,},
},
-- Note that the strike mask will be centered over your look position rather than yourself.
strikes = {
-- Strikes dont get any buffer around them, and are treated sort of like waves in that the mask changes depending on which direction you're facing.
{0,0,1,0,0,},
{0,1,1,1,0,},
{1,1,1,1,1,},
{0,1,1,1,0,},
{0,0,0,0,0,},
}
}
masks["exevo frigo hur"] = masks["exevo flam hur"]
masks["exevo gran frigo hur"] = masks["exevo vis hur"]
-- Positions are stored in one dimensional tables by first converting them to numbers.
-- Note that for the sake of this script the z-coordinate is irrelevant.
-- Also, the function is linear. The number for a sum of two positions is the sum of the numbers for each position.
local function posToNum(x, y)
return 100000*x+y
end
-- Inverse of posToNum.
local function numToPos(n)
local y = n % 100000
local x = (n - y) / 100000
return x, y
end
-- Constants representing what to check for in a converted mask
local PLAYERS, PLAYERS_AND_MONSTERS = 0, 1
-- Converts a spell mask to a hashset containing the numerical offsets.
-- Every position within *margin* squares from a 1 in the original mask is added to the set.
local function convertMask(mask, margin, marginValue)
local marginValue = marginValue or PLAYERS
-- Node that the size of a mask must be an odd number since all spells are symmetrical.
local xmid, ymid = math.ceil(#mask / 2), math.ceil(#mask[1] / 2)
local convertedMask = {}
local function fillMask(margin, value)
for y = 1, #mask do
for x = 1, #mask[y] do
if mask[y][x] == 1 then
for oy = y - margin, y + margin do
for ox = x - margin, x + margin do
-- The masks are stored with indexes starting at 1,
-- but in-game we have negative offsets for squares above or to the left of the character.
convertedMask[posToNum(ox - xmid, oy - ymid)] = value
end
end
end
end
end
end
-- First write the pvp-safe area
if margin > 0 then
fillMask(margin, marginValue)
end
-- Then overwrite the area in which we check for monsters
fillMask(0, PLAYERS_AND_MONSTERS)
return convertedMask
end
local function getSurroundings(multifloor, blacklist)
local monsters, players, shootables = {}, {}, {}
if not multifloor then
-- First check for blacklisted ids and enable multifloor if one is found
for x = $posx - 7, $posx + 7 do
for y = $posy - 5, $posy + 5 do
local tile = gettile(x, y, $posz)
for i = 1, tile.itemcount do
-- Also treat blacklisted ids as players, since players may arrive
if blacklist[tile.item[i].id] then multifloor = true end
end
end
end
end
local maxzdiff = multifloor and 1 or 0
local xdiff = Config.pvpsafe and math.max(2, 4 - Config.buffersize) or 4
local ydiff = Config.pvpsafe and math.max(1, 3 - Config.buffersize) or 4
-- First filter out the walkable positions since we cannot shoot on top of walls
for x = $posx - xdiff, $posx + xdiff do
for y = $posy - ydiff, $posy + ydiff do
if tileshootable(x, y, $posz) and tileclickable(x, y, $posz) then
shootables[posToNum(x, y)] = true
end
end
end
-- Add our creature tiles
foreach creature c 'm' do
if c.posz == $posz then
if c.issummon then
players[posToNum(c.posx, c.posy)] = c
else
monsters[posToNum(c.posx, c.posy)] = c
end
end
end
-- Add our player tiles
foreach creature c 'p' do
local isPartyMember = c.party >= 3 and c.party <= 10
if math.abs(c.posz - $posz) <= maxzdiff and not (Config.ignoreParty and isPartyMember) and
c.name ~= $name then
players[posToNum(c.posx, c.posy)] = c
end
end
return monsters, players, shootables
end
local function creatures(monsters, targetMonsters, filter)
local validMonsters = {}
local cogx, cogy, numMonsters = 0, 0, 0
for pos, c in pairs(monsters) do
if (not targetMonsters or targetMonsters[c.name:lower()]) and
(not filter or filter(c))
then
validMonsters[pos] = c
cogx = cogx + c.posx
cogy = cogy + c.posy
numMonsters = numMonsters + 1
end
end
return validMonsters, {x = cogx/numMonsters, y = cogy/numMonsters}
end
local function getScore(pos, mask, monsters, players, finalcheck, pvpsafe)
local score = 0
local effectedMonsters = {}
local monsters = monsters or {}
for offset, TYPE in pairs(mask) do
if pvpsafe and players[pos + offset] then
return -1
elseif TYPE == PLAYERS_AND_MONSTERS and monsters[pos + offset] then
score = score + 1
table.insert(effectedMonsters, monsters[pos + offset])
end
end
if not finalcheck or finalcheck(effectedMonsters) then
return score
else
return 0
end
end
local function getBestBallPos(monsters, players, shootables, mask, monsterNames, pvpsafe, filter, finalcheck)
local monsters, cog = creatures(monsters, monsterNames, filter)
local cx, cy = cog.x, cog.y
-- Ties are broken by preference for positions close to the monster's center of gravity
local function getTieScore(num)
local x, y = numToPos(num)
return math.abs(cx-x) + math.abs(cy-y)
end
-- Then find the best one among the shootables
local bestScore, bestTieScore, bestPos
for pos, _ in pairs(shootables) do
local score = getScore(pos, mask, monsters, players, finalcheck, pvpsafe)
local tieScore = getTieScore(pos)
if not bestScore or score > bestScore or (score == bestScore and tieScore < bestTieScore) then
bestScore, bestTieScore, bestPos = score, tieScore, pos
end
end
return bestPos, bestScore
end
-- For printing purposes
local SUCCESS, SKIP, RETRY = 0, 1, 2
statusString = {
[SUCCESS] = "SUCCESS",
[SKIP] = "SKIP",
[RETRY] = "RETRY"
}
-- Possible return values for a function that tries to shoot a spell
local STRIKE, SINGLE_TARGET_RUNE, BALL, UE, WAVE = 0, 1, 2, 3, 4
local typeString = {
[STRIKE] = "STRIKE",
[SINGLE_TARGET_RUNE] = "SINGLE TARGET RUNE",
[BALL] = "BALL",
[UE] = "UE",
[WAVE] = "WAVE",
}
local function getType(spell)
if type(spell) == 'number' then
spell = Item.GetName(spell)
end
spell = spell:lower()
for _, name in ipairs({"thunderstorm", "avalanche", "great fireball", "stone shower"}) do
if spell:match(name) then
return BALL
end
end
if spell:match("exori") then
return STRIKE
end
if spell:match("exevo gran mas") then
return UE
end
if spell:match("exevo") then
return WAVE
end
return SINGLE_TARGET_RUNE
end
local function rotateMask(mask, dir)
local ymax, xmax = #mask, #mask[1]
local function rotate(x, y)
if dir == NORTH then
return x, y
elseif dir == EAST then
return (ymax - y) + 1, x
elseif dir == SOUTH then
return x, (ymax - y) + 1
elseif dir == WEST then
return y, x
end
end
local newMask = {}
for y = 1, ymax do
table.insert(newMask, {})
for x = 1, xmax do
table.insert(newMask[y], 0)
end
end
for y = 1, ymax do
for x = 1, xmax do
if mask[y][x] == 1 then
local nx, ny = rotate(x, y)
newMask[ny][nx] = 1
end
end
end
return newMask
end
local function getMask(spell, TYPE, margin)
if TYPE == WAVE then
if not masks[spell] then print("Unknown spell: " .. spell) end
local convertedMasks = {}
for _, dir in ipairs({NORTH, EAST, SOUTH, WEST}) do
convertedMasks[dir] = convertMask(rotateMask(masks[spell], dir), margin)
end
return convertedMasks
elseif TYPE == STRIKE then
local convertedMasks = {}
for _, dir in ipairs({NORTH, EAST, SOUTH, WEST}) do
convertedMasks[dir] = convertMask(rotateMask(masks.strikes, dir), 0)
end
return convertedMasks
end
local base = masks.strikes
if TYPE == BALL then
base = masks.ball
elseif TYPE == UE then
if not masks[spell] then print("Unknown spell: " .. spell) end
base = masks[spell]
end
return convertMask(base, margin)
end
local mana = {
["exevo gran mas vis"] = 600,
["exevo gran mas flam"] = 1100,
["exevo gran mas tera"] = 700,
["exevo gran mas frigo"] = 1050,
["exori vis"] = 20,
["exori flam"] = 20,
["exori tera"] = 20,
["exori frigo"] = 20,
["exori mort"] = 20,
["exori moe ico"] = 20,
["exori gran vis"] = 60,
["exori gran flam"] = 60,
["exori gran tera"] = 60,
["exori gran frigo"] = 60,
["exori max frigo"] = 100,
["exori max frigo"] = 100,
["exori max frigo"] = 100,
["exori max frigo"] = 100,
["exori min flam"] = 6,
["exori amp vis"] = 60,
["exevo flam hur"] = 25,
["exevo vis hur"] = 170,
["exevo frigo hur"] = 25,
["exevo gran frigo hur"] = 170,
["exevo vis lux"] = 40,
["exevo gran vis lux"] = 110,
}
function string:titlecase()
return self:gsub("(%a)([%w_']*)", function(first, rest) return first:upper()..rest:lower() end)
end
local function convertCategory(c)
local monsters = {}
foreach settingsentry e 'Targeting/Creatures' do
local name = getsetting(e, 'Name')
local cat = getsetting(e, 'Category')
if not name:find('category') and (cat ~= '' and c:find(cat)) then
monsters[name:lower()] = true
end
end
return monsters
end
function convertSpellConfig(arg)
local TYPE = getType(arg.name)
local spell = arg.name:lower()
local monsters
-- Build up the target list, if there is one
if arg.monsters then
if type(arg.monsters) == "string" then
monsters = convertCategory(arg.monsters)
else
monsters = {}
for _, name in ipairs(arg.monsters) do
monsters[name:lower()] = true
end
end
end
-- Range and mana settings.
-- Could be rewritten to use spellinfo, but
-- I wanted to reuse as much as possible of my old Xeno code.
local range = spell == "exori amp vis" and 5 or 3
local mana = mana[spell] or 0
-- Filters determine wheter a given creature is valid or not.
-- Generally this will inspect the creatures health percentage
local filter
if arg.minhp or arg.maxhp then
local minhp = arg.minhp or 0
local maxhp = arg.maxhp or 100
filter = function(c)
return c.hppc < maxhp and c.hppc > minhp
end
end
-- Final checks determine whether a group of creatures are valid.
-- For example, check that an ava cast hits at least one Demon Skeleton, otherwise fall through and try a GFB instead.
local finalcheck
if arg.atLeastOne then
local atLeastOneMonsters = {}
for _, name in ipairs(arg.atLeastOne) do
atLeastOneMonsters[name] = true
end
finalcheck = function(creatures)
for _, c in ipairs(creatures) do
if atLeastOneMonsters[c.name] then return true end
end
return false
end
end
return {
spell = spell,
type = TYPE,
monsters = monsters,
range = range,
mana = mana,
count = function() return arg.count end,
enabled = function() return true end,
filter = filter,
finalcheck = finalcheck,
}
end
local opposite = {
[NORTH] = SOUTH,
[SOUTH] = NORTH,
[EAST] = WEST,
[WEST] = EAST,
}
local function tryTurnAway()
local dirs = {}
foreach creature c 'ps' do
if math.max(math.abs(c.posx - $posx), math.abs(c.posy - $posy)) <= 3 then
if c.posx - $posx > 0 then
dirs[EAST] = true
elseif c.posx - $posx < 0 then
dirs[WEST] = true
end
if c.posy - $posy > 0 then
dirs[SOUTH] = true
elseif c.posy - $posy < 0 then
dirs[NORTH] = true
end
end
end
for _, dir in ipairs({NORTH, EAST, SOUTH, WEST}) do
if dirs[dir] and not dirs[opposite[dir]] then return turn(opposite[dir]) end
end
end
local function getLookPos()
if $self.dir == NORTH then
return $posx, $posy - 1
elseif $self.dir == EAST then
return $posx + 1, $posy
elseif $self.dir == SOUTH then
return $posx, $posy + 1
elseif $self.dir == WEST then
return $posx - 1, $posy
end
end
local currentMasks = {}
local function updateMasks()
local spells = {}
for _, data in ipairs(Config.spells) do
local spell = data.name:lower()
currentMasks[spell] = getMask(spell, getType(spell), Config.buffersize)
end
currentMasks["exori flam"] = getMask("exori flam", STRIKE, Config.buffersize)
end
updateMasks()
lastRuneCast = os.clock()
function getShooterFunc(config, blacklist)
if config.type == UE then
return function(_, monsters, players)
local spell = config.spell
if not config.enabled() then return SKIP end
-- UE's cant be checked precisely, so dont cast them when weve seen players around lately.
if Config.pvpsafe and (os.clock() - lastPlayerSeenTime) < 10 then
return SKIP
end
-- Return instantly if the spell is on cooldown or we dont have mana
if cooldown(spell) > 2000 or $mp < config.mana then
return SKIP
elseif cooldown(spell) > 0 then
return RETRY
end
local mask = currentMasks[spell]
local monsters = creatures(monsters, config.monsters, config.filter)
local score = getScore(posToNum($posx, $posy), mask, monsters, players, config.finalcheck, Config.pvpsafe)
-- If we found enough creatures, try casting, else skip the spell
-- Also, don't UE if there's more monsters standing just outside the area
if score >= config.count() then
cast(spell)
return cooldown(spell) == 0 and RETRY or SUCCESS
else
return SKIP
end
end
elseif config.type == BALL then
return function(mask, monsters, players, shootables)
--print(config.spell, tostring(config.enabled()))
if not config.enabled() then return SKIP end
-- Skip if we dont have that rune
if itemcount(config.spell) == 0 then return SKIP end
if cooldown("exori flam") > 0 then return RETRY end
-- Find the best position to shoot the rune
local numpos, score = getBestBallPos(monsters, players, shootables, mask, config.monsters, Config.pvpsafe, config.filter, config.finalcheck)
-- Casting on self is more efficient, so always compare it with the best pos
local monsters = creatures(monsters, config.monsters, config.filter)
local selfScore = getScore(posToNum($posx, $posy), mask, monsters, players, config.finalcheck, Config.pvpsafe)
-- If we found a good position)
if numpos and score >= config.count() then
local x, y = numToPos(numpos)
lastRuneCast = os.clock()
pausewalking(1000)
if $fasthotkeys and selfScore + 0 >= score and selfScore >= config.count() then
useoncreature(itemid(config.spell), $self)
else
useitemon(itemid(config.spell), 0, ground(x, y, $posz))
end
pausewalking(100 + $ping)
if cooldown("exori flam") ~= 0 then
return SUCCESS
else
return RETRY
end
end
return SKIP
end
elseif config.type == WAVE then
return function(mask, monsters, players)
if not config.enabled() then return SKIP end
-- Skip if the spell is on a long cooldown or we dont have mana
if cooldown(config.spell) > 2000 or $mp < config.mana then return SKIP end
local pos = posToNum($posx, $posy)
local monsters = creatures(monsters, config.monsters, config.filter)
-- Determine the best direction to wave
local bestDir, bestScore
for _, dir in ipairs({NORTH, EAST, SOUTH, WEST}) do
local score = getScore(pos, mask[dir], monsters, players, config.finalcheck, Config.pvpsafe)
-- All directions must be pvp-safe for us to wave, cause we can fail to turn.
if score == -1 then return SKIP end
if (Config.turnForWaves or dir == $self.dir) and (not bestScore or score > bestScore) then
bestDir, bestScore = dir, score
end
end
-- Skip if we couldn't find any sufficiently good directions
if not bestDir or bestScore < config.count() then
return SKIP
else
pausewalking(1000)
-- Note that for waves we prefer to skip the spell rather than retry if something goes wrong
if $self.dir ~= bestDir then
turn(bestDir)
waitping()
end
if $self.dir ~= bestDir then return end
cast(config.spell)
pausewalking(200)
return cooldown(config.spell) == 0 and SKIP or SUCCESS
end
end
elseif config.type == SINGLE_TARGET_RUNE then
return function()
-- Skip if we dont have a target or we dont have the rune
if itemcount(config.spell) == 0 then return SKIP end
-- Retry if the spell is on cooldown
if cooldown("exori flam") > 0 then return SKIP end
local function wouldDieToAStrike(c)
local distance = math.max(math.abs($posx - c.posx), math.abs($posy - c.posy))
if distance > 3 then return false end
local info = creatureinfo(c.name)
local hp = c.hppc * info.hp / 100
local _, dmg = getBestStrike(c)
return dmg >= hp
end
local rinfo = runeinfo(config.spell)
local target, bdmg, bkills
foreach creature c "mf" do
if ((not config.monsters) or config.monsters[c.name:lower()]) and
(not config.filter or config.filter(c)) and c.isshootable then
if wouldDieToAStrike(c) then
return SKIP
end
local info = creatureinfo(c.name)
local hp = info.hp * c.hppc / 100
local mod = info[rinfo.dmgtype:lower() .. "mod"] / 100
local mindmg = mod * rinfo.mindmg
local dmg = math.min(mindmg, hp)
local kills = mindmg >= hp
if (not target) or
(kills and not bkills) or
(dmg > bdmg)
then
target = c
bdmg = dmg
bkills = kills
end
end
end
-- Check that the target is valid for out spell before casting
local c = $target
if target then
-- Retry if the spell is still on a short cooldown
useoncreature(config.spell, target)
if cooldown("exori flam") ~= 0 then
lastRuneCast = os.clock()
return SUCCESS
else
return SKIP
end
else
return SKIP
end
end
end
end
function parseConfig(ShooterConfig)
local spells = {}
-- The blacklist is a list of IDs. If an ID from the blacklist is on the screen
-- the shooter will enable multi-floor pvp safety.
local blacklist = {}
for _, id in ipairs(ShooterConfig.blacklistIds) do
blacklist[id] = true
end
-- The set of ball targets is used to allow the potion drinker to drink more frequently.
local ballTargets = {}
for _, spell in ipairs(ShooterConfig.spells) do
local config = convertSpellConfig(spell)
local shooter = getShooterFunc(config, blacklist)
table.insert(spells, {name = config.spell, type = config.type, shooter = shooter})
if config.type == BALL then
if config.monsters and ballTargets then
for _, name in ipairs(config.monsters) do
ballTargets[name] = true
end
else
ballTargets = nil
end
end
end
return {spells, blacklist}, ballTargets
end
function getBestStrike(target)
local targetName = target.Name
local info = creatureinfo(targetName)
local hp = target.hppc * info.hp / 100
local spells = {"exori flam", "exori frigo", "exori vis", "exori tera"}
if $vocshort == "S" then
table.insert(spells, "exori mort")
if Config.useStrongStrikes then
table.insert(spells, "exori gran vis")
table.insert(spells, "exori gran flam")
end
elseif $vocshort == "D" then
table.insert(spells, "exori moe ico")
if Config.useStrongStrikes then
table.insert(spells, "exori gran frigo")
table.insert(spells, "exori gran tera")
end
end
local function priority(spell)
local sinfo = spellinfo(spell)
local mod = info[sinfo.dmgtype:lower() .. "mod"] / 100
if mod * sinfo.mindmg > hp then
return hp, sinfo.mp, mod
else
return mod * sinfo.mindmg, sinfo.mp, mod
end
end
local bspell, bdmg, bmp, bmod
for _, spell in ipairs(spells) do
local sdmg, smp, smod = priority(spell)
local hotkeyed = clientspellhotkey(spell) ~= 'not found' or $fasthotkeys
if cooldown(spell) == 0 and hotkeyed and (not bspell or sdmg > bdmg or (sdmg == bdmg and smp < bmp) or (sdmg == bdmg and smp == bmp and smod > bmod)) then
bspell, bdmg, bmp, bmod = spell, sdmg, smp, smod
end
end
return bspell, bdmg
end
local function tryCastBestStrike(players)
local c = $target
-- Don't case without a target!
if c.id == 0 then return SKIP end
local spell = getBestStrike(c)
-- getBestSpell only returns nil if all strikes are on cooldown
if not spell then return SKIP end
-- Skip if we dont have mana
if $mp < spellinfo(spell).mp then return SKIP end
local lx, ly = getLookPos()
local score = getScore(posToNum(lx, ly), currentMasks["exori flam"][$self.dir], nil, players, nil, Config.pvpsafe)
-- If the strike isn't pvpsafe, then try turning away from any other player and then skip the spell.
-- Don't retry since that may get us stuck in a loop.
if score == -1 then
tryTurnAway()
return SKIP
end
-- Check that the target is in range before casting
if math.max(math.abs(c.posx - $posx), math.abs(c.posy - $posy)) <= 3 and $target.id ~= 0 then
cast(spell)
return cooldown(spell) == 0 and RETRY or SUCCESS
end
return SKIP
end
local function tryCastSpell(spells, blacklist)
local monsters, players, shootables = getSurroundings(Config.multiFloor, blacklist)
for _, spell in ipairs(spells) do
status = spell.shooter(currentMasks[spell.name], monsters, players, shootables)
if status == SUCCESS or status == RETRY then
return
end
monsters, players, shootables = getSurroundings(Config.multiFloor, blacklist)
end
tryCastBestStrike(players)
end
pconfig, _RuneMonsters = parseConfig(Config)
local pconfig = pconfig
lastRuneCast = 0
lastDrunk = 0
updateMasks()
init end
-- Check for players to disable ue
foreach creature c 'pf' do
local isPartyMember = c.party >= 3 and c.party <= 10
if c.name ~= $name and not (Config.ignoreParty and isPartyMember) then
lastPlayerSeenTime = os.clock()
end
end
-- Cast a spell
if not $pzone then
tryCastSpell(unpack(pconfig))
end
-- Drink a potion
local timeToNextCast = cooldown(SPELL_GROUP_ATTACK)
local timeSinceRuneCast = os.clock() - lastRuneCast
local potion = Config.manaPotion
local precount = itemcount(potion)
if precount > 0 then
if (timeSinceRuneCast > 1 and os.clock() - lastDrunk > 1) and
($mppc < 30 or ($mppc < 80 and (maround(7, unpack(_RuneMonsters)) < 2 or timeToNextCast > 800))) then
useoncreature(potion, $self)
if precount ~= itemcount(potion) then
lastDrunk = os.clock()
end
end
end
auto(100)