• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!
  • 2026 staff recruitment is open! Check it out and consider applying!

Lua Checking for the closest creature? [TFS 1.2]

Siegh

Thronar Developer
Joined
Mar 12, 2011
Messages
1,276
Solutions
1
Reaction score
635
Location
Brazil
Hi!

I'm working on developing a kind of "chain lightning" kind of effect for a spell. How can I have a function do something like this:

Code:
Execute combat on initial target
Check for closest creature in up to 1sqm distance (randomize if tied creatures)
After a tiny delay (about 200ms for visibility), execute combat on the next selected creature
Repeat up to a certain amount of times
Ignores players unless in a pvp area

I'm ok with the repeating part, ignoring players etc, what I'm having trouble with is the checking for creatures in a certain area and randomize one of them if tied.

Thanks in advance :)
 
Solution
LUA:
local neighbours = {{0, -1},{1, -1},{1, 0},{1, 1},{0, 1},{-1, 1},{-1, 0},{-1, -1}}
local pos = player:getPosition()

local creatures = {}
for _, neighbour in ipairs(neighbours) do
    local neighbourPos = Position(pos.x + neighbour[1], pos.y + neighbour[2], pos.z)
    if neighbourPos then
        local neighbourCreature = neighbourPos:getTopCreature()
        if neighbourCreature and neighbourCreature:isMonster() then
            table.insert(creatures, neighbourCreature)
        end
    end
end

if next(creatures) ~= nil then
    local randomCreature = creatures[math.random(#creatures)]
    --do whatever here
end

You could just use Game.getSpectators but for something as simple as this, I would recommend to not use it.
So...
LUA:
local neighbours = {{0, -1},{1, -1},{1, 0},{1, 1},{0, 1},{-1, 1},{-1, 0},{-1, -1}}
local pos = player:getPosition()

local creatures = {}
for _, neighbour in ipairs(neighbours) do
    local neighbourPos = Position(pos.x + neighbour[1], pos.y + neighbour[2], pos.z)
    if neighbourPos then
        local neighbourCreature = neighbourPos:getTopCreature()
        if neighbourCreature and neighbourCreature:isMonster() then
            table.insert(creatures, neighbourCreature)
        end
    end
end

if next(creatures) ~= nil then
    local randomCreature = creatures[math.random(#creatures)]
    --do whatever here
end

You could just use Game.getSpectators but for something as simple as this, I would recommend to not use it.
So iterating through fast neighbours will suffice. If you ever wanted to extend to 2 sqm or more, I would switch to Game.getSpectators.
(I just wrote this in 2 mins so things may be wrong / incorrect func names etc)
 
Last edited:
Solution
Use getSpectators. Its faster than previous reply
For larger areas yes, which is what I said. For immediate neighbours, not really, it's comparative.
The whole point was to show him the logic behind it, which is relative to how he structured his question.

Also, if he wanted to extend it using a Dijkstra-like/Nearest neighbour approach, then this is a good start for understanding, before improving on it.
 
getSpectators is designed for broad area scans, you're only looking 1 sqm out — it'll sweep the entire viewport just to check 8 tiles. not worth it

just loop dx/dy from -1 to 1, grab Tile(pos):getCreatures() on each, filter out visited and non-pvp players. for the distance calc use Chebyshev (math.max(abs(dx), abs(dy))), toss the tied candidates in a table and pick with math.random — done

way cheaper and you have full control over what you're iterating
 
Well, I used the first post and it worked out fine, I understant the differences but for what I need it's perfect.

It just needed a small adjustement on here:

LUA:
local tile = Tile(neighbourPos)
if neighbourPos then
    local neighbourCreature = tile:getTopCreature()


Thanks everyone for taking your time!
 

Well, it works but the randomCreature table is a global variable, meaning its not emptied after being used as far as I've tested. That causes future casts of the spell to consider past targets that are no longer closeby. What am I doing wrong here?
LUA:
local function chainEffect(target)
    if target then
        --local neighbours = {{0, -1},{1, -1},{1, 0},{1, 1},{0, 1},{-1, 1},{-1, 0},{-1, -1}} --1 range
        local neighbours = {{0, -1},{1, -1},{1, 0},{1, 1},{0, 1},{-1, 1},{-1, 0},{-1, -1},{-2,-2},{-2,-1},{-2,0},{-2,1},{-2,2},{-1,2},{0,2},{1,2},{2,2},{2,1},{2,0},{2,-1},{2,-2},{1,-2},{0,2},{-1,-2}} --2 range
        local pos = creature:getTarget():getPosition()
        local creatures = {}
        for _, neighbour in ipairs(neighbours) do
            local neighbourPos = Position(pos.x + neighbour[1], pos.y + neighbour[2], pos.z)
            local tile = Tile(neighbourPos)
            if neighbourPos then
                local neighbourCreature = tile:getTopCreature()
                if neighbourCreature and neighbourCreature:isMonster() then
                    table.insert(creatures, neighbourCreature)
                end
            end
        end
        if next(creatures) ~= nil then
            randomCreature = creatures[math.random(#creatures)]
            doSendDistanceShoot(target:getPosition(), randomCreature:getPosition(), 33)
            doTargetCombatHealth(0, randomCreature, COMBAT_ENERGYDAMAGE, -min, -max, 49)
        end
    end
end
chainEffect(target)
addEvent(chainEffect, 200, randomCreature)
addEvent(chainEffect, 400, randomCreature)
addEvent(chainEffect, 600, randomCreature)
addEvent(chainEffect, 800, randomCreature)
 
You've done a few things wrong here. Firstly, randomCreature should be a local variable. Secondly, you should call chainEffect again within itself (it's called recursion). Thirdly (and most importantly), never pass userdata into addEvent, you cannot ever be sure that it still exists when the event is executed. It is best to pass it's ID and recreate it first.

I spent a bit more time rewriting it here for you using getSpectators (considering you are using an area wider than immediate neighbours), and with guard clauses:
LUA:
local chainAttempts = 5    -- amount of times the chain occurs
local chainInterval = 200  -- gap in ms between attacks

local function chainEffect(targetId, chainIndex)
    local target = Creature(targetId)
    if not target then
        return
    end
 
    local targetPos = target:getPosition()
    local creatures = Game.getSpectators(targetPos, false, false, 2, 2, 2, 2)
    if next(creatures) == nil then
        return
    end
 
    for i = #creatures, 1, -1 do
        local creature = creatures[i]
        if not creature:isMonster() then
            table.remove(creatures, i)
        end
    end
 
    if next(creatures) == nil then
        return
    end
 
    local randomCreature = creatures[math.random(#creatures)]
    doSendDistanceShoot(target:getPosition(), randomCreature:getPosition(), 33)
    doTargetCombatHealth(0, randomCreature, COMBAT_ENERGYDAMAGE, -min, -max, 49)
    
    if chainIndex < chainAttempts then
        addEvent(chainEffect, chainInterval, randomCreature:getId(), chainIndex + 1)
    end
end

chainEffect(target:getId(), 0)
(once again, written by hand so maybe errors)

While looping through to remove players, you can also check to see if the monster is the same as the previous target (to prevent consecutive targets)
 
Last edited:
While looping through to remove players, you can also check to see if the monster is the same as the previous target (to prevent consecutive targets)

That's why I chose to use a non local variable since, on the next loop, I don't know how to check whether the last target is the same as the previous one. How can I do it in a more appropriate way?
 
That's why I chose to use a non local variable since, on the next loop, I don't know how to check whether the last target is the same as the previous one. How can I do it in a more appropriate way?
While looping to remove players from the creatures table, we can just check to make sure that the monster's ID isn't the same as targetId, if so we can remove it too. (on line 18)
LUA:
local chainAttempts = 5    -- amount of times the chain occurs
local chainInterval = 200  -- gap in ms between attacks

local function chainEffect(targetId, chainIndex)
    local target = Creature(targetId)
    if not target then
        return
    end
 
    local targetPos = target:getPosition()
    local creatures = Game.getSpectators(targetPos, false, false, 2, 2, 2, 2)
    if next(creatures) == nil then
        return
    end
 
    for i = #creatures, 1, -1 do
        local creature = creatures[i]
        if not creature:isMonster() or creature:getId() == targetId then
            table.remove(creatures, i)
        end
    end
 
    if next(creatures) == nil then
        return
    end
 
    local randomCreature = creatures[math.random(#creatures)]
    doSendDistanceShoot(target:getPosition(), randomCreature:getPosition(), 33)
    doTargetCombatHealth(0, randomCreature, COMBAT_ENERGYDAMAGE, -min, -max, 49)
  
    if chainIndex < chainAttempts then
        addEvent(chainEffect, chainInterval, randomCreature:getId(), chainIndex + 1)
    end
end

chainEffect(target:getId(), 0)
 
While looping to remove players from the creatures table, we can just check to make sure that the monster's ID isn't the same as targetId, if so we can remove it too. (on line 18)
LUA:
local chainAttempts = 5    -- amount of times the chain occurs
local chainInterval = 200  -- gap in ms between attacks

local function chainEffect(targetId, chainIndex)
    local target = Creature(targetId)
    if not target then
        return
    end
 
    local targetPos = target:getPosition()
    local creatures = Game.getSpectators(targetPos, false, false, 2, 2, 2, 2)
    if next(creatures) == nil then
        return
    end
 
    for i = #creatures, 1, -1 do
        local creature = creatures[i]
        if not creature:isMonster() or creature:getId() == targetId then
            table.remove(creatures, i)
        end
    end
 
    if next(creatures) == nil then
        return
    end
 
    local randomCreature = creatures[math.random(#creatures)]
    doSendDistanceShoot(target:getPosition(), randomCreature:getPosition(), 33)
    doTargetCombatHealth(0, randomCreature, COMBAT_ENERGYDAMAGE, -min, -max, 49)
 
    if chainIndex < chainAttempts then
        addEvent(chainEffect, chainInterval, randomCreature:getId(), chainIndex + 1)
    end
end

chainEffect(target:getId(), 0)
Worked perfectly, thanks :)
 
Back
Top