water_life/paths.lua
2020-12-15 18:07:38 +01:00

518 lines
15 KiB
Lua

local openSet = {}
local closedSet = {}
local random = water_life.random
local abs = math.abs
local function show_path(ptable)
if not ptable or #ptable < 1 then return end
for i= 1,#ptable,1 do
water_life.temp_show(ptable[i],1,1)
end
end
local function get_distance(start_pos, end_pos)
local distX = math.abs(start_pos.x - end_pos.x)
local distZ = math.abs(start_pos.z - end_pos.z)
if distX > distZ then
return 14 * distZ + 10 * (distX - distZ)
else
return 14 * distX + 10 * (distZ - distX)
end
end
local function get_distance_to_neighbor(start_pos, end_pos)
local distX = math.abs(start_pos.x - end_pos.x)
local distY = math.abs(start_pos.y - end_pos.y)
local distZ = math.abs(start_pos.z - end_pos.z)
if distX > distZ then
return (14 * distZ + 10 * (distX - distZ)) * (distY + 1)
else
return (14 * distX + 10 * (distZ - distX)) * (distY + 1)
end
end
local function walkable(node, pos, current_pos)
if string.find(node.name,"doors:door") then
if (node.param2 == 0 or
node.param2 == 2) and
math.abs(pos.z - current_pos.z) > 0 and
pos.x == current_pos.x then
return true
elseif (node.param2 == 1 or
node.param2 == 3) and
math.abs(pos.z - current_pos.z) > 0 and
pos.x == current_pos.x then
return false
elseif (node.param2 == 0 or
node.param2 == 2) and
math.abs(pos.x - current_pos.x) > 0 and
pos.z == current_pos.z then
return false
elseif (node.param2 == 1 or
node.param2 == 3) and
math.abs(pos.x - current_pos.x) > 0 and
pos.z == current_pos.z then
return true
end
elseif string.find(node.name,"doors:hidden") then
local node_door = minetest.get_node({x = pos.x, y = pos.y - 1, z = pos.z})
if (node_door.param2 == 0 or
node_door.param2 == 2) and
math.abs(pos.z - current_pos.z) > 0 and
pos.x == current_pos.x then
return true
elseif (node_door.param2 == 1 or
node_door.param2 == 3) and
math.abs(pos.z - current_pos.z) > 0 and
pos.x == current_pos.x then
return false
elseif (node_door.param2 == 0 or
node_door.param2 == 2) and
math.abs(pos.x - current_pos.x) > 0 and
pos.z == current_pos.z then
return false
elseif (node_door.param2 == 1 or
node_door.param2 == 3) and
math.abs(pos.x - current_pos.x) > 0 and
pos.z == current_pos.z then
return true
end
end
if minetest.registered_nodes[node.name] and
minetest.registered_nodes[node.name].walkable then
return true
else
return false
end
end
local function get_neighbor_ground_level(pos, jump_height, fall_height, current_pos)
local node = minetest.get_node(pos)
local height = 0
if walkable(node, pos, current_pos) then
repeat
height = height + 1
if height > jump_height then
return nil
end
pos.y = pos.y + 1
node = minetest.get_node(pos)
until not walkable(node, pos, current_pos)
return pos
else
repeat
height = height + 1
if height > fall_height then
return nil
end
pos.y = pos.y - 1
node = minetest.get_node(pos)
until walkable(node, pos, current_pos)
return {x = pos.x, y = pos.y + 1, z = pos.z}
end
end
-- local function dot(a, b)
-- return a.x * b.x + a.y * b.y + a.z * b.z
-- end
--
-- local function len(a)
-- return math.sqrt(a.x * a.x + a.y * a.y + a.z * a.z)
-- end
--
-- local function lensq(a)
-- return a.x * a.x + a.y * a.y + a.z * a.z
-- end
--
-- local function normalize(a)
-- local l = len(a)
-- a.x = a.x / l
-- a.y = a.y / l
-- a.z = a.z / l
-- return a
-- end
--[[
minetest.find_path(pos1,pos2,searchdistance,max_jump,max_drop,algorithm)`
* returns table containing path that can be walked on
* returns a table of 3D points representing a path from `pos1` to `pos2` or
`nil` on failure.
* Reasons for failure:
* No path exists at all
* No path exists within `searchdistance` (see below)
* Start or end pos is buried in land
* `pos1`: start position
* `pos2`: end position
* `searchdistance`: maximum distance from the search positions to search in.
In detail: Path must be completely inside a cuboid. The minimum
`searchdistance` of 1 will confine search between `pos1` and `pos2`.
Larger values will increase the size of this cuboid in all directions
* `max_jump`: maximum height difference to consider walkable
* `max_drop`: maximum height difference to consider droppable
* `algorithm`: One of `"A*_noprefetch"` (default), `"A*"`, `"Dijkstra"`.
Difference between `"A*"` and `"A*_noprefetch"` is that
`"A*"` will pre-calculate the cost-data, the other will calculate it
on-the-fly]]
function water_life.find_path(pos, endpos, entity, dtime, fast)
if fast then
--minetest.chat_send_all("FAST")
return minetest.find_path(pos,endpos,water_life.abr*16,entity.jump_height,1,"A*")
end
--minetest.chat_send_all("SLOW")
pos = {
x = math.floor(pos.x + 0.5),
y = math.floor(pos.y + 0.5),
z = math.floor(pos.z + 0.5)
}
endpos = {
x = math.floor(endpos.x + 0.5),
y = math.floor(endpos.y + 0.5),
z = math.floor(endpos.z + 0.5)
}
local target_node = minetest.get_node(endpos)
if walkable(target_node, endpos, endpos) then
endpos.y = endpos.y + 1
end
local start_node = minetest.get_node(pos)
if string.find(start_node.name,"doors:door") then
if start_node.param2 == 0 then
pos.z = pos.z + 1
elseif start_node.param2 == 1 then
pos.x = pos.x + 1
elseif start_node.param2 == 2 then
pos.z = pos.z - 1
elseif start_node.param2 == 3 then
pos.x = pos.x - 1
end
end
local start_time = minetest.get_us_time()
local start_index = minetest.hash_node_position(pos)
local target_index = minetest.hash_node_position(endpos)
local count = 1
openSet = {}
closedSet = {}
-- minetest.set_node(pos, {name = "default:glass"})
-- minetest.set_node(endpos, {name = "default:glass"})
-- print(dump(pos))
-- print(endpos)
local h_start = get_distance(pos, endpos)
openSet[start_index] = {hCost = h_start, gCost = 0, fCost = h_start, parent = nil, pos = pos}
-- Entity values
local entity_height = math.ceil(entity.collisionbox[5] - entity.collisionbox[2]) or 2
local entity_fear_height = entity.fear_height or 3
local entity_jump_height = entity.jump_height or 1
local neighbors_cache = {}
repeat
local current_index
local current_values
-- Get one index as reference from openSet
for i, v in pairs(openSet) do
current_index = i
current_values = v
break
end
-- Search for lowest fCost
for i, v in pairs(openSet) do
if v.fCost < openSet[current_index].fCost or v.fCost == current_values.fCost and v.hCost < current_values.hCost then
current_index = i
current_values = v
end
end
openSet[current_index] = nil
closedSet[current_index] = current_values
count = count - 1
if current_index == target_index then
--~ minetest.chat_send_all("Found path in " .. (minetest.get_us_time() - start_time) / 1000 .. "ms")
local path = {}
repeat
if not closedSet[current_index] then
return
end
table.insert(path, closedSet[current_index].pos)
current_index = closedSet[current_index].parent
until start_index == current_index
table.insert(path, closedSet[current_index].pos)
local reverse_path = {}
repeat
table.insert(reverse_path, table.remove(path))
until #path == 0
--minetest.chat_send_all("Found path in " .. (minetest.get_us_time() - start_time) / 1000 .. "ms. " .. "Path length: " .. #reverse_path)
return reverse_path
end
local current_pos = current_values.pos
local neighbors = {}
local neighbors_index = 1
for z = -1, 1 do
for x = -1, 1 do
local neighbor_pos = {x = current_pos.x + x, y = current_pos.y, z = current_pos.z + z}
local neighbor = minetest.get_node(neighbor_pos)
local neighbor_ground_level = get_neighbor_ground_level(neighbor_pos, entity_jump_height, entity_fear_height, current_pos)
local neighbor_clearance = false
if neighbor_ground_level then
local neighbor_hash = minetest.hash_node_position(neighbor_ground_level)
local pos_above_head = {x = current_pos.x, y = current_pos.y + entity_height, z = current_pos.z}
local node_above_head = minetest.get_node(pos_above_head)
if neighbor_ground_level.y - current_pos.y > 0 and not walkable(node_above_head, pos_above_head, current_pos) then
local height = -1
repeat
height = height + 1
local pos = { x = neighbor_ground_level.x,
y = neighbor_ground_level.y + height,
z = neighbor_ground_level.z}
local node = minetest.get_node(pos)
until walkable(node, pos, current_pos) or height > entity_height
if height >= entity_height then
neighbor_clearance = true
end
elseif neighbor_ground_level.y - current_pos.y > 0 and walkable(node_above_head, pos_above_head, current_pos) then
neighbors[neighbors_index] = {
hash = nil,
pos = nil,
clear = nil,
walkable = nil,
}
else
local height = -1
repeat
height = height + 1
local pos = { x = neighbor_ground_level.x,
y = current_pos.y + height,
z = neighbor_ground_level.z}
local node = minetest.get_node(pos)
until walkable(node, pos, current_pos) or height > entity_height
if height >= entity_height then
neighbor_clearance = true
end
end
neighbors[neighbors_index] = {
hash = minetest.hash_node_position(neighbor_ground_level),
pos = neighbor_ground_level,
clear = neighbor_clearance,
walkable = walkable(neighbor, neighbor_pos, current_pos),
}
else
neighbors[neighbors_index] = {
hash = nil,
pos = nil,
clear = nil,
walkable = nil,
}
end
neighbors_index = neighbors_index + 1
end
end
for id, neighbor in pairs(neighbors) do
-- don't cut corners
local cut_corner = false
if id == 1 then
if not neighbors[id + 1].clear or not neighbors[id + 3].clear
or neighbors[id + 1].walkable or neighbors[id + 3].walkable then
cut_corner = true
end
elseif id == 3 then
if not neighbors[id - 1].clear or not neighbors[id + 3].clear
or neighbors[id - 1].walkable or neighbors[id + 3].walkable then
cut_corner = true
end
elseif id == 7 then
if not neighbors[id + 1].clear or not neighbors[id - 3].clear
or neighbors[id + 1].walkable or neighbors[id - 3].walkable then
cut_corner = true
end
elseif id == 9 then
if not neighbors[id - 1].clear or not neighbors[id - 3].clear
or neighbors[id - 1].walkable or neighbors[id - 3].walkable then
cut_corner = true
end
end
if neighbor.hash ~= current_index and not closedSet[neighbor.hash] and neighbor.clear and not cut_corner then
local move_cost_to_neighbor = current_values.gCost + get_distance_to_neighbor(current_values.pos, neighbor.pos)
local gCost = 0
if openSet[neighbor.hash] then
gCost = openSet[neighbor.hash].gCost
end
if move_cost_to_neighbor < gCost or not openSet[neighbor.hash] then
if not openSet[neighbor.hash] then
count = count + 1
end
local hCost = get_distance(neighbor.pos, endpos)
openSet[neighbor.hash] = {
gCost = move_cost_to_neighbor,
hCost = hCost,
fCost = move_cost_to_neighbor + hCost,
parent = current_index,
pos = neighbor.pos
}
end
end
end
if count > 300 then
--minetest.chat_send_all("Path fail")
return
end
if (minetest.get_us_time() - start_time) / 1000 > 100 - dtime * 50 then
--minetest.chat_send_all("Path timeout")
return
end
until count < 1
--minetest.chat_send_all("count < 1")
return {pos}
end
------------------------------------
-- Bahaviors and helper functions --
------------------------------------
--[[
function water_life.hq_findpath(self,prty,tpos, dist,speed,fast)
if not dist then dist = 1 end
if not speed then speed = 1 end
local func = function(self)
if mobkit.is_queue_empty_low(self) and self.isonground then
local pos = self.object:get_pos()
if vector.distance(pos,tpos) > dist then
if not water_life.gopath(self,tpos,speed,fast) then return true end
else
return true
end
end
end
mobkit.queue_high(self,func,prty)
end]]
function water_life.hq_findpath(self,prty,tpos,dist,speed,fast)
mobkit.clear_queue_low(self)
if not dist then dist = 1 end
if not speed then speed = 1 end
local way = water_life.find_path(self.object:get_pos(), tpos, self, self.dtime,fast)
local func = function(self)
if not way or #way < 2 then return true end --no way ? finish
--show_path(way) --debug visualation
if mobkit.is_queue_empty_low(self) and self.isonground then
local pos = self.object:get_pos()
local pos2 = way[2]
local height = abs(pos.y - pos2.y)-0.5
local node = mobkit.nodeatpos({x=pos2.x, y=pos2.y -1, z = pos2.z})
if not node or not node.walkable then return true end -- something has changed, quit!
if vector.distance(pos,tpos) > dist then
if height <= 0.01 then
local yaw = self.object:get_yaw()
local tyaw = minetest.dir_to_yaw(vector.direction(self.object:get_pos(),pos2))
if math.abs(tyaw-yaw) > 1 then
mobkit.lq_turn2pos(self,pos2)
end
mobkit.lq_dumbwalk(self,pos2,speed)
else
mobkit.lq_turn2pos(self,pos2)
mobkit.lq_dumbjump(self,height)
end
if vector.distance(pos,pos2) <= 2 then
table.remove(way,2)
end
else
return true
end
end
end
mobkit.queue_high(self,func,prty)
end
function water_life.gopath(self,tpos,speedfactor,fast)
local height, pos2 = water_life.go_further(self,tpos,fast)
if not speedfactor then speedfactor = 1 end
if not height then return false end
if height <= 0.01 then
local yaw = self.object:get_yaw()
local tyaw = minetest.dir_to_yaw(vector.direction(self.object:get_pos(),pos2))
if math.abs(tyaw-yaw) > 1 then
mobkit.lq_turn2pos(self,pos2)
end
mobkit.lq_dumbwalk(self,pos2,speedfactor)
else
mobkit.lq_turn2pos(self,pos2)
mobkit.lq_dumbjump(self,height)
end
return true
end
function water_life.go_further(self,tpos,fast)
local height = 0
local pos = mobkit.get_stand_pos(self)--self.object:get_pos()
local way = water_life.find_path(pos, tpos, self, self.dtime,fast)
if not way or #way < 2 then return nil end
height = way[2].y - pos.y -0.5
--minetest.chat_send_all(dump(height))
return height, way[2]
end
function water_life.hq_path_attack(self,prty,tgtobj,fast)
local func = function(self)
if not mobkit.is_alive(tgtobj) then return true end
if mobkit.is_queue_empty_low(self) then
local pos = mobkit.get_stand_pos(self)
-- local tpos = tgtobj:get_pos()
local tpos = mobkit.get_stand_pos(tgtobj)
local dist = vector.distance(pos,tpos)
if dist > 3 or not water_life.find_path(pos, tpos, self, self.dtime,fast) then
return true
else
mobkit.lq_turn2pos(self,tpos)
local height = tgtobj:is_player() and 0.35 or tgtobj:get_luaentity().height*0.6
if tpos.y+height>pos.y then
mobkit.lq_jumpattack(self,tpos.y+height-pos.y,tgtobj)
else
mobkit.lq_dumbwalk(self,mobkit.pos_shift(tpos,{x=random()-0.5,z=random()-0.5}))
end
end
end
end
mobkit.queue_high(self,func,prty)
end