minetest-pvp/mods/mob_world_interaction/pathfinder.lua

304 lines
9.0 KiB
Lua

-- pathfinder from burli.
-- See https://forum.minetest.net/viewtopic.php?f=9&t=17406 and
-- https://github.com/MarkuBu/pathfinder for original version.
--
-- Modifications:
-- * can plot paths through doors
-- * thin slabs on the ceiling (up to 2/16 thick) do not hinder pathfinding
-- * TODO: thin slabs on the floor ought to be ok as well
-- * climbable nodes (ladders etc.) are handled to some degree
mob_world_interaction.pathfinder = {}
mob_world_interaction.pathfinder.max_path_length = 100;
--[[
minetest.get_content_id(name)
minetest.registered_nodes
minetest.get_name_from_content_id(id)
local ivm = a:index(pos.x, pos.y, pos.z)
local ivm = a:indexp(pos)
minetest.hash_node_position({x=,y=,z=})
minetest.get_position_from_hash(hash)
start_index, target_index, current_index
^ Hash of position
current_value
^ {int:hCost, int:gCost, int:fCost, hash:parent, vect:pos}
]]--
--print("loading mob_world_interaction.pathfinder")
local openSet = {}
local closedSet = {}
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, curr_height, max_height )
if( mob_world_interaction ) then
return mob_world_interaction.walkable( node, curr_height, max_height );
end
return minetest.registered_nodes[node.name].walkable
end
mob_world_interaction.climbable = function(node)
if( not( node ) or not( node.name ) or not( minetest.registered_nodes[ node.name ])) then
return false;
end
return minetest.registered_nodes[node.name].climbable;
end
-- seems to be unused
local function check_clearance(cpos, x, z, height, data)
for i = 1, height do
local n_name = mob_world_interaction.get_node({x = cpos.x + x, y = cpos.y + i, z = cpos.z + z}, data).name
local c_name = mob_world_interaction.get_node({x = cpos.x, y = cpos.y + i, z = cpos.z}, data).name
--~ print(i, n_name, c_name)
if walkable(n_name, i, height) or walkable(c_name, i, height) then
return false
end
end
return true
end
local function get_neighbor_ground_level(pos, jump_height, fall_height, data)
local node = mob_world_interaction.get_node(pos, data)
local height = 0
local relevant_height = 0;
if walkable(node,1,2) then
repeat
height = height + 1
if( not( mob_world_interaction.climbable(node ))) then
relevant_height = relevant_height + 1;
end
if relevant_height > jump_height then
return nil
end
pos.y = pos.y + 1
node = mob_world_interaction.get_node(pos, data)
until not walkable(node,2,2)
return pos
else
repeat
height = height + 1
if( not( mob_world_interaction.climbable(node ))) then
relevant_height = relevant_height + 1;
end
if relevant_height > fall_height then
return nil
end
pos.y = pos.y - 1
node = mob_world_interaction.get_node(pos, data)
until walkable(node,1,2)
return {x = pos.x, y = pos.y + 1, z = pos.z}
end
end
function mob_world_interaction.find_path(pos, endpos, entity, data)
local start_index = minetest.hash_node_position(pos)
local target_index = minetest.hash_node_position(endpos)
local count = 1
openSet = {}
closedSet = {}
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])
local entity_fear_height = entity.fear_height or 2
local entity_jump_height = entity.jump_height or 1
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
-- print("Success")
local path = {}
local reverse_path = {}
repeat
if not closedSet[current_index] then
return
end
table.insert(path, closedSet[current_index].pos)
current_index = closedSet[current_index].parent
if #path > mob_world_interaction.pathfinder.max_path_length then
-- print("path to long")
return
end
until start_index == current_index
repeat
table.insert(reverse_path, table.remove(path))
until #path == 0
-- print("path lenght: "..#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 = mob_world_interaction.get_node(neighbor_pos, data)
local neighbor_ground_level = get_neighbor_ground_level(neighbor_pos, entity_jump_height, entity_fear_height, data)
local neighbor_clearance = false
if neighbor_ground_level then
-- print(neighbor_ground_level.y - current_pos.y)
-- minetest.set_node(neighbor_ground_level, {name = "default:dry_shrub"})
local node_above_head = mob_world_interaction.get_node(
{x = current_pos.x, y = current_pos.y + entity_height, z = current_pos.z}, data)
if neighbor_ground_level.y - current_pos.y > 0 and not walkable(node_above_head,entity_height,entity_height) then
local height = -1
repeat
height = height + 1
local node = mob_world_interaction.get_node(
{x = neighbor_ground_level.x,
y = neighbor_ground_level.y + height,
z = neighbor_ground_level.z}, data)
until walkable(node, entity_height, entity_height) 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,entity_height,entity_height) then
neighbors[neighbors_index] = {
hash = nil,
pos = nil,
clear = nil,
walkable = nil,
}
else
local height = -1
repeat
height = height + 1
local node = mob_world_interaction.get_node(
{x = neighbor_ground_level.x,
y = current_pos.y + height,
z = neighbor_ground_level.z}, data)
until walkable(node,2,2) 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,1,2),
}
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 > mob_world_interaction.pathfinder.max_path_length then
-- print("fail")
return
end
until count < 1
-- print("count < 1")
return {pos}
end