Quote:
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 = false, -- 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 = true, -- 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", "Glooth Blob"}, 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", "Glooth Blob"}},
{name = "exevo vis hur", monsters = {"devourer", "blood Beast", "rot elemental", "Quara Predator Scout", "Quara Hydromancer Scout", "quara pincher scout", "quara constrictor scout", "Glooth Blob"}, count = 3},
{name = "thunderstorm rune", count = 2, monsters = {"devourer", "blood Beast", "rot elemental", "Quara Predator Scout", "Quara Hydromancer Scout", "quara pincher scout", "quara constrictor scout", "Glooth Blob"}},
{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)