Implement pathfinding
parent
5a2c575e14
commit
bfa290bb4f
|
@ -0,0 +1,23 @@
|
|||
Queue = {}
|
||||
function Queue.new ()
|
||||
return {first = 0, last = -1}
|
||||
end
|
||||
|
||||
function Queue.push (queue, value)
|
||||
local last = queue.last + 1
|
||||
queue.last = last
|
||||
queue[last] = value
|
||||
end
|
||||
|
||||
function Queue.pop (queue)
|
||||
local first = queue.first
|
||||
if first > queue.last then error("queue is empty") end
|
||||
local value = queue[first]
|
||||
queue[first] = nil
|
||||
queue.first = first + 1
|
||||
return value
|
||||
end
|
||||
|
||||
function Queue.size (queue)
|
||||
return queue.last - queue.first + 1
|
||||
end
|
|
@ -85,7 +85,7 @@ function director:on_interval()
|
|||
end
|
||||
|
||||
if self.cooldown_timer <= 0 then
|
||||
if defense:is_dark() and #minetest.luaentities < self.max_entities then
|
||||
if defense:is_dark() and #minetest.luaentities < self.max_entities and not defense.debug then
|
||||
self:spawn_monsters()
|
||||
end
|
||||
|
||||
|
|
|
@ -17,16 +17,6 @@ minetest.register_chatcommand("debug", {
|
|||
end,
|
||||
})
|
||||
|
||||
function minetest.wallmounted_to_dir(wallmounted)
|
||||
return ({[0]={x=0, y=1, z=0},
|
||||
{x=0, y=-1, z=0},
|
||||
{x=1, y=0, z=0},
|
||||
{x=-1, y=0, z=0},
|
||||
{x=0, y=0, z=1},
|
||||
{x=0, y=0, z=-1}})
|
||||
[wallmounted]
|
||||
end
|
||||
|
||||
local modpath = minetest.get_modpath("defense")
|
||||
local function dofile2(file)
|
||||
dofile(modpath .. "/" .. file)
|
||||
|
@ -37,12 +27,16 @@ function defense:is_dark()
|
|||
return tod < 0.21 or tod > 0.8 or defense.debug
|
||||
end
|
||||
|
||||
dofile2("util.lua")
|
||||
dofile2("Queue.lua")
|
||||
|
||||
dofile2("initial_stuff.lua")
|
||||
dofile2("pathfinder.lua")
|
||||
dofile2("director.lua")
|
||||
dofile2("music.lua")
|
||||
|
||||
dofile2("mob.lua")
|
||||
dofile2("mobs/unggoy.lua")
|
||||
dofile2("mobs/sarangay.lua")
|
||||
dofile2("mobs/paniki.lua")
|
||||
dofile2("mobs/botete.lua")
|
||||
|
||||
dofile2("director.lua")
|
||||
dofile2("music.lua")
|
||||
dofile2("initial_stuff.lua")
|
||||
dofile2("mobs/botete.lua")
|
|
@ -11,6 +11,8 @@ mobs.default_prototype = {
|
|||
stepheight = 0.6,
|
||||
-- custom properties
|
||||
id = 0,
|
||||
smart_path = true,
|
||||
mass = 1,
|
||||
movement = "ground", -- "ground"/"air"
|
||||
move_speed = 1,
|
||||
jump_height = 1,
|
||||
|
@ -121,15 +123,26 @@ end
|
|||
function mobs.default_prototype:hunt()
|
||||
local nearest = self:find_nearest_player()
|
||||
if nearest.player then
|
||||
local dir = vector.direction(nearest.position, self.object:getpos())
|
||||
if nearest.distance <= self.attack_range then
|
||||
self:do_attack(nearest.player)
|
||||
end
|
||||
if nearest.distance > self.attack_range or nearest.distance < self.attack_range/2-1 then
|
||||
local r = math.max(0, self.attack_range - 2)
|
||||
self.destination = vector.add(nearest.position, vector.multiply(dir, r))
|
||||
local pos = self.object:getpos()
|
||||
local direction = nil
|
||||
if self.smart_path and nearest.distance < defense.pathfinder.path_max_range then
|
||||
direction = defense.pathfinder:get_direction(self.name, pos)
|
||||
end
|
||||
|
||||
if direction then
|
||||
self.destination = vector.add(pos, vector.multiply(direction, 1.25))
|
||||
else
|
||||
local r = math.max(0, self.attack_range - 2)
|
||||
local dir = vector.direction(nearest.position, self.object:getpos())
|
||||
self.destination = vector.add(nearest.position, vector.multiply(dir, r))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function mobs.default_prototype:do_attack(obj)
|
||||
|
@ -149,7 +162,11 @@ end
|
|||
|
||||
function mobs.default_prototype:jump(direction)
|
||||
if self:is_standing() then
|
||||
direction = vector.normalize(direction)
|
||||
if direction then
|
||||
direction = vector.normalize(direction)
|
||||
else
|
||||
direction = {x=0,y=0,z=0}
|
||||
end
|
||||
local v = self.object:getvelocity()
|
||||
v.y = math.sqrt(2 * -mobs.gravity * (self.jump_height + 0.2))
|
||||
v.x = direction.x * self.jump_height
|
||||
|
@ -212,21 +229,19 @@ function mobs.default_prototype:find_nearest_player()
|
|||
local nearest_pos = p
|
||||
local nearest_dist = 9999
|
||||
for _,obj in ipairs(minetest.get_connected_players()) do
|
||||
if obj:is_player() then
|
||||
if not nearest_player then
|
||||
if not nearest_player then
|
||||
nearest_player = obj
|
||||
nearest_pos = obj:getpos()
|
||||
nearest_pos.y = nearest_pos.y + 1
|
||||
nearest_dist = vector.distance(nearest_pos, p)
|
||||
else
|
||||
local pos = obj:getpos()
|
||||
pos.y = pos.y + 1
|
||||
local d = vector.distance(pos, p)
|
||||
if d < nearest_dist then
|
||||
nearest_player = obj
|
||||
nearest_pos = obj:getpos()
|
||||
nearest_pos.y = nearest_pos.y + 1
|
||||
nearest_dist = vector.distance(nearest_pos, p)
|
||||
else
|
||||
local pos = obj:getpos()
|
||||
pos.y = pos.y + 1
|
||||
local d = vector.distance(pos, p)
|
||||
if d < nearest_dist then
|
||||
nearest_player = obj
|
||||
nearest_pos = pos
|
||||
nearest_dist = d
|
||||
end
|
||||
nearest_pos = pos
|
||||
nearest_dist = d
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -357,5 +372,17 @@ function mobs.register_mob(name, def)
|
|||
|
||||
prototype.move = def.move or mobs.move_method[prototype.movement]
|
||||
|
||||
if defense.pathfinder and prototype.smart_path then
|
||||
defense.pathfinder:register_class(name, {
|
||||
size = {
|
||||
x = math.ceil(prototype.collisionbox[4] - prototype.collisionbox[1]),
|
||||
y = math.ceil(prototype.collisionbox[5] - prototype.collisionbox[2]),
|
||||
z = math.ceil(prototype.collisionbox[6] - prototype.collisionbox[3])
|
||||
},
|
||||
collisionbox = prototype.collisionbox,
|
||||
cost_method = def.pathfinder_cost or defense.pathfinder.cost_method[prototype.movement]
|
||||
})
|
||||
end
|
||||
|
||||
minetest.register_entity(name, prototype)
|
||||
end
|
|
@ -135,6 +135,7 @@ defense.mobs.register_mob("defense:botete", {
|
|||
move_attack = {a=80, b=99, rate=25},
|
||||
},
|
||||
|
||||
smart_path = false,
|
||||
mass = 1,
|
||||
movement = "air",
|
||||
move_speed = 4,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
defense.mobs.register_mob("defense:paniki", {
|
||||
hp_max = 7,
|
||||
collisionbox = {-0.3,-0.3,-0.3, 0.3,0.3,0.3},
|
||||
collisionbox = {-0.4,-0.4,-0.4, 0.4,0.4,0.4},
|
||||
mesh = "defense_paniki.b3d",
|
||||
textures = {"defense_paniki.png"},
|
||||
makes_footstep_sound = false,
|
||||
|
|
|
@ -27,6 +27,7 @@ defense.mobs.register_mob("defense:sarangay", {
|
|||
start = {a=110, b=119, rate=15},
|
||||
},
|
||||
|
||||
smart_path = false,
|
||||
mass = 12,
|
||||
move_speed = 6,
|
||||
jump_height = 1,
|
||||
|
|
|
@ -50,6 +50,9 @@ defense.mobs.register_mob("defense:unggoy", {
|
|||
self:hunt()
|
||||
end
|
||||
end
|
||||
if math.random() < 0.05 then
|
||||
self:jump()
|
||||
end
|
||||
end,
|
||||
|
||||
is_standing = function(self)
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
defense.pathfinder = {}
|
||||
local pathfinder = defense.pathfinder
|
||||
pathfinder.path_max_range = 16
|
||||
pathfinder.fields = {}
|
||||
pathfinder.classes = {}
|
||||
|
||||
local visit_queues = {}
|
||||
local chunk_size = 16
|
||||
|
||||
function pathfinder:register_class(class, properties)
|
||||
self.fields[class] = self.fields[class] or {}
|
||||
self.classes[class] = properties
|
||||
visit_queues[class] = Queue.new()
|
||||
end
|
||||
|
||||
-- Returns a number
|
||||
-- function pathfinder:get_distance(class, position)
|
||||
-- local field = self:get_field(class, position)
|
||||
-- if not field then
|
||||
-- return nil
|
||||
-- end
|
||||
-- return field.distance
|
||||
-- end
|
||||
|
||||
-- Returns a vector
|
||||
function pathfinder:get_direction(class, position)
|
||||
local field = self:get_field(class, position)
|
||||
if not field then
|
||||
return nil
|
||||
end
|
||||
if field.distance == 0 then
|
||||
return {x=0, y=0, z=0}
|
||||
end
|
||||
return ({{x=-1, y=0, z=0},
|
||||
{x=1, y=0, z=0},
|
||||
{x=0, y=-1, z=0},
|
||||
{x=0, y=1, z=0},
|
||||
{x=0, y=0, z=-1},
|
||||
{x=0, y=0, z=1}})
|
||||
[field.direction]
|
||||
end
|
||||
|
||||
-- Returns a table {time, distance}
|
||||
function pathfinder:get_field(class, position)
|
||||
local collisionbox = self.classes[class].collisionbox
|
||||
local x = math.floor(position.x + collisionbox[1])
|
||||
local y = math.floor(position.y + collisionbox[2])
|
||||
local z = math.floor(position.z + collisionbox[3])
|
||||
|
||||
local chunk_key = math.floor(x/chunk_size) ..
|
||||
":" .. math.floor(y/chunk_size) ..
|
||||
":" .. math.floor(z/chunk_size)
|
||||
local chunk = self.fields[class][chunk_key]
|
||||
if not chunk then
|
||||
return nil
|
||||
end
|
||||
|
||||
local cx = x % chunk_size
|
||||
local cy = y % chunk_size
|
||||
local cz = z % chunk_size
|
||||
local index = (cy * chunk_size + cz) * chunk_size + cx
|
||||
return chunk[index]
|
||||
end
|
||||
|
||||
function pathfinder:set_field(class, position, distance, direction, time)
|
||||
local collisionbox = self.classes[class].collisionbox
|
||||
local x = math.floor(position.x + collisionbox[1])
|
||||
local y = math.floor(position.y + collisionbox[2])
|
||||
local z = math.floor(position.z + collisionbox[3])
|
||||
|
||||
local chunk_key = math.floor(x/chunk_size) ..
|
||||
":" .. math.floor(y/chunk_size) ..
|
||||
":" .. math.floor(z/chunk_size)
|
||||
local chunk = self.fields[class][chunk_key]
|
||||
if not chunk then
|
||||
chunk = {}
|
||||
self.fields[class][chunk_key] = chunk
|
||||
end
|
||||
|
||||
local cx = x % chunk_size
|
||||
local cy = y % chunk_size
|
||||
local cz = z % chunk_size
|
||||
local index = (cy * chunk_size + cz) * chunk_size + cx
|
||||
chunk[index] = {time=time, direction=direction, distance=distance}
|
||||
end
|
||||
|
||||
function pathfinder:update(dtime)
|
||||
if not defense:is_dark() then
|
||||
-- reset flow fields
|
||||
return
|
||||
end
|
||||
|
||||
local neighborhood = {
|
||||
{x=1, y=0, z=0},
|
||||
{x=-1, y=0, z=0},
|
||||
{x=0, y=1, z=0},
|
||||
{x=0, y=-1, z=0},
|
||||
{x=0, y=0, z=1},
|
||||
{x=0, y=0, z=-1},
|
||||
}
|
||||
-- Update the field
|
||||
for c,class in pairs(self.classes) do
|
||||
local vq = visit_queues[c]
|
||||
local size = Queue.size(vq)
|
||||
minetest.debug(size)
|
||||
for i=1,math.min(size,1000 * dtime) do
|
||||
local current = Queue.pop(vq)
|
||||
for di,n in ipairs(neighborhood) do
|
||||
local npos = vector.add(current.position, n)
|
||||
npos.x = math.floor(npos.x)
|
||||
npos.y = math.floor(npos.y)
|
||||
npos.z = math.floor(npos.z)
|
||||
local cost = class.cost_method(npos, current.position, class.size)
|
||||
if cost then
|
||||
local next_distance = current.distance + cost
|
||||
local neighbor_field = self:get_field(c, npos)
|
||||
if not neighbor_field
|
||||
or neighbor_field.time < current.time
|
||||
and neighbor_field.direction ~= di
|
||||
or neighbor_field.time == current.time
|
||||
and neighbor_field.distance > next_distance then
|
||||
self:set_field(c, npos, next_distance, di, current.time)
|
||||
if next_distance < self.path_max_range then
|
||||
Queue.push(vq, {
|
||||
position = npos,
|
||||
distance = next_distance,
|
||||
direction = di,
|
||||
time = current.time,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Update player positions
|
||||
local time = minetest.get_gametime()
|
||||
for _,p in ipairs(minetest.get_connected_players()) do
|
||||
local pos = p:getpos()
|
||||
pos.y = pos.y + 1
|
||||
for c,_ in pairs(self.classes) do
|
||||
local field = self:get_field(c, pos)
|
||||
if not field or field.distance > 0 then
|
||||
self:set_field(c, pos, 0, 0, time)
|
||||
Queue.push(visit_queues[c], {position=pos, distance=0, direction=0, time=time})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
pathfinder.cost_method = {}
|
||||
function pathfinder.cost_method.air(pos, parent, size)
|
||||
-- Check if solid
|
||||
for y=pos.y,pos.y+size.y-1 do
|
||||
for z=pos.z,pos.z+size.z-1 do
|
||||
for x=pos.x,pos.x+size.x-1 do
|
||||
local node = minetest.get_node_or_nil({x=x, y=y, z=z})
|
||||
if not node then return nil end
|
||||
if minetest.registered_nodes[node.name].walkable then
|
||||
return pathfinder.path_max_range + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return 1
|
||||
end
|
||||
function pathfinder.cost_method.ground(pos, parent, size)
|
||||
local on_ground = false
|
||||
for z=pos.z,pos.z+size.z-1 do
|
||||
for x=pos.x,pos.x+size.x-1 do
|
||||
-- Check if solid
|
||||
for y=pos.y,pos.y+size.y-1 do
|
||||
local node = minetest.get_node_or_nil({x=x, y=y, z=z})
|
||||
if not node then return nil end
|
||||
if minetest.registered_nodes[node.name].walkable then
|
||||
return pathfinder.path_max_range + 1
|
||||
end
|
||||
end
|
||||
|
||||
if not on_ground then
|
||||
-- Check if on top of solid
|
||||
local node = minetest.get_node_or_nil({x=x, y=pos.y-1, z=z})
|
||||
if not node then return nil end
|
||||
if minetest.registered_nodes[node.name].walkable then
|
||||
on_ground = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not on_ground then
|
||||
return pathfinder.path_max_range + 1
|
||||
end
|
||||
return 1 + math.ceil(math.abs(pos.y - parent.y))
|
||||
end
|
||||
|
||||
minetest.register_globalstep(function(dtime)
|
||||
pathfinder:update(dtime)
|
||||
end)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,19 @@
|
|||
minetest.wallmounted_to_dir = minetest.wallmounted_to_dir or function(wallmounted)
|
||||
return ({[0]={x=0, y=1, z=0},
|
||||
{x=0, y=-1, z=0},
|
||||
{x=1, y=0, z=0},
|
||||
{x=-1, y=0, z=0},
|
||||
{x=0, y=0, z=1},
|
||||
{x=0, y=0, z=-1}})
|
||||
[wallmounted]
|
||||
end
|
||||
|
||||
math.sign = math.sign or function(x)
|
||||
if x < 0 then
|
||||
return -1
|
||||
elseif x > 0 then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue