mirror of
https://github.com/ElCeejo/creatura.git
synced 2025-05-21 15:23:17 -04:00
632 lines
No EOL
23 KiB
Lua
632 lines
No EOL
23 KiB
Lua
-----------------
|
|
-- Pathfinding --
|
|
-----------------
|
|
|
|
local a_star_alloted_time = tonumber(minetest.settings:get("creatura_a_star_alloted_time")) or 500
|
|
local theta_star_alloted_time = tonumber(minetest.settings:get("creatura_theta_star_alloted_time")) or 700
|
|
|
|
local floor = math.floor
|
|
local abs = math.abs
|
|
|
|
local function is_node_walkable(name)
|
|
local def = minetest.registered_nodes[name]
|
|
return def and def.walkable
|
|
end
|
|
|
|
local function is_node_liquid(name)
|
|
local def = minetest.registered_nodes[name]
|
|
return def and def.drawtype == "liquid"
|
|
end
|
|
|
|
local function moveable(pos, width, height)
|
|
local pos1 = {
|
|
x = pos.x - (width + 0.2),
|
|
y = pos.y,
|
|
z = pos.z - (width + 0.2),
|
|
}
|
|
local pos2 = {
|
|
x = pos.x + (width + 0.2),
|
|
y = pos.y,
|
|
z = pos.z + (width + 0.2),
|
|
}
|
|
for z = pos1.z, pos2.z do
|
|
for x = pos1.x, pos2.x do
|
|
local pos3 = {x = x, y = pos.y + height, z = z}
|
|
local pos4 = {x = x, y = pos.y, z = z}
|
|
local ray = minetest.raycast(pos3, pos4, false, false)
|
|
for pointed_thing in ray do
|
|
if pointed_thing.type == "node" then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
creatura.get_node_height = function(name, force_node_box)
|
|
local def = minetest.registered_nodes[name]
|
|
if not def then return 0.5 end
|
|
if def.walkable then
|
|
if def.drawtype == "nodebox" then
|
|
if def.collision_box and not force_node_box
|
|
and (def.collision_box.type == "fixed" or def.collision_box.type == "connected") then
|
|
if type(def.collision_box.fixed[1]) == "number" then
|
|
return 0.5 + def.collision_box.fixed[5]
|
|
elseif type(def.collision_box.fixed[1]) == "table" then
|
|
return 0.5 + def.collision_box.fixed[1][5]
|
|
else
|
|
return 1
|
|
end
|
|
elseif def.node_box
|
|
and (def.node_box.type == "fixed" or def.node_box.type == "connected") then
|
|
if type(def.node_box.fixed[1]) == "number" then
|
|
return 0.5 + def.node_box.fixed[5]
|
|
elseif type(def.node_box.fixed[1]) == "table" then
|
|
return 0.5 + def.node_box.fixed[1][5]
|
|
else
|
|
return 1
|
|
end
|
|
else
|
|
return 1
|
|
end
|
|
else
|
|
return 1
|
|
end
|
|
else
|
|
return 1
|
|
end
|
|
end
|
|
|
|
creatura.get_ground_level = function(pos, max_up, max_down, current_node_height)
|
|
for y = math.ceil(max_up) + 1, -(math.ceil(max_down)) - 1, -1 do
|
|
local pos2 = vector.new(pos.x, pos.y + y, pos.z)
|
|
local node = minetest.get_node(pos2)
|
|
local node_under = minetest.get_node(pos2 + vector.new(0, -1, 0))
|
|
|
|
if not is_node_walkable(node.name) and is_node_walkable(node_under.name) then
|
|
local node_height = creatura.get_node_height(node_under.name)
|
|
local y_diff = y - 1 + node_height - current_node_height
|
|
if y_diff <= max_up and y_diff >= (-max_down) then
|
|
return pos2
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function get_distance(start_pos, end_pos)
|
|
local distX = abs(start_pos.x - end_pos.x)
|
|
local distZ = 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 = abs(start_pos.x - end_pos.x)
|
|
local distY = abs(start_pos.y - end_pos.y)
|
|
local distZ = 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 is_on_ground(pos)
|
|
local ground = {
|
|
x = pos.x,
|
|
y = pos.y - 1,
|
|
z = pos.z
|
|
}
|
|
if is_node_walkable(minetest.get_node(ground).name) then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function vec_raise(v, n)
|
|
return {x = v.x, y = v.y + n, z = v.z}
|
|
end
|
|
|
|
-- Find a path from start to goal
|
|
|
|
local function get_neighbors(self, pos, goal, swim, fly, climb, tbl, open, closed)
|
|
local width = self.width
|
|
local height = self.height
|
|
local result = {}
|
|
local max_up = self.stepheight or 1
|
|
local max_down = self.max_fall or 1
|
|
|
|
local node_name = minetest.get_node(pos).name
|
|
-- Get the height of the node collision box (and of its node box, if different)
|
|
local node_height = 0
|
|
local node_height_node_box = 0
|
|
if is_node_walkable(node_name) then
|
|
node_height = creatura.get_node_height(node_name)
|
|
node_height_node_box = creatura.get_node_height(node_name, true)
|
|
else
|
|
node_height = creatura.get_node_height(minetest.get_node(pos + vector.new(0, -1, 0)).name) - 1
|
|
node_height_node_box = creatura.get_node_height(minetest.get_node(pos + vector.new(0, -1, 0)).name, true) - 1
|
|
end
|
|
-- Calculate the height difference between the collision and node boxes
|
|
-- (This is because the mob will be standing on the collision box, but the
|
|
-- raycast checks will collide with the node box, so we must avoid it)
|
|
local node_height_diff = node_height_node_box - node_height
|
|
|
|
for i = 1, #tbl do
|
|
local neighbor = vector.add(pos, tbl[i])
|
|
if not open[minetest.hash_node_position(neighbor)]
|
|
and not closed[minetest.hash_node_position(neighbor)] then
|
|
|
|
local neighbor_x
|
|
local neighbor_z
|
|
|
|
if tbl[i].y == 0
|
|
and not fly
|
|
and not swim then
|
|
neighbor = creatura.get_ground_level(neighbor, max_up, max_down, node_height)
|
|
if neighbor and tbl[i].x ~= 0 and tbl[i].z ~= 0 then
|
|
-- This is a diagonal, check both corners are clear and same Y
|
|
neighbor_x = creatura.get_ground_level(vector.new(neighbor.x, neighbor.y, pos.z), max_up, max_down, node_height)
|
|
neighbor_z = creatura.get_ground_level(vector.new(pos.x, neighbor.y, neighbor.z), max_up, max_down, node_height)
|
|
if not neighbor_x or not neighbor_z
|
|
or neighbor_x.y ~= neighbor.y
|
|
or neighbor_z.y ~= neighbor.y then
|
|
neighbor = nil
|
|
end
|
|
end
|
|
end
|
|
if neighbor then
|
|
local can_move = true
|
|
if swim then
|
|
local neighbor_node = minetest.get_node(neighbor)
|
|
can_move = is_node_liquid(neighbor_node.name)
|
|
end
|
|
|
|
-- Adjust entity Y in clearance check by this much
|
|
local y_adjustment = -0.49
|
|
-- Adjust entity height in clearance check by this much
|
|
local h_adjustment = -0.02
|
|
-- Get the height of the node collision box, and the difference to the node box
|
|
local neighbor_height = creatura.get_node_height(minetest.get_node(neighbor + vector.new(0, -1, 0)).name) - 1
|
|
local neighbor_height_node_box = creatura.get_node_height(minetest.get_node(neighbor + vector.new(0, -1, 0)).name, true) - 1
|
|
local neighbor_height_diff = neighbor_height_node_box - neighbor_height
|
|
-- Check there is enough vertical clearance to move to this node
|
|
local height_clearance = math.max(pos.y + node_height - neighbor.y - neighbor_height, 0)
|
|
if not moveable(vec_raise(neighbor, y_adjustment + neighbor_height + neighbor_height_diff), width, height + h_adjustment + height_clearance - neighbor_height_diff) then
|
|
can_move = false
|
|
end
|
|
if tbl[i].x ~= 0 and tbl[i].z ~= 0 then
|
|
-- If target node is diagonal, check the orthogonal nodes too
|
|
if not moveable(vec_raise(neighbor_x, y_adjustment + neighbor_height + neighbor_height_diff), width, height + h_adjustment + height_clearance + neighbor_height_diff)
|
|
or not moveable(vec_raise(neighbor_z, y_adjustment + neighbor_height + neighbor_height_diff), width, height + h_adjustment + height_clearance + neighbor_height_diff) then
|
|
can_move = false
|
|
end
|
|
end
|
|
-- If we're going upwards, check there's enough clearance above our head
|
|
height_clearance = math.max(neighbor.y + neighbor_height - pos.y - node_height, 0)
|
|
if height_clearance > 0 and not moveable(vec_raise(pos, y_adjustment + node_height + node_height_diff), width, height + h_adjustment + height_clearance - node_height_diff) then
|
|
can_move = false
|
|
end
|
|
|
|
if (can_move
|
|
or (climb
|
|
and neighbor.x == pos.x
|
|
and neighbor.z == pos.z))
|
|
and (not swim
|
|
or is_node_liquid(minetest.get_node(neighbor).name)) then
|
|
table.insert(result, neighbor)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
function creatura.find_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim)
|
|
climb = climb or false
|
|
fly = fly or false
|
|
swim = swim or false
|
|
|
|
start = self._path_data.start or start
|
|
|
|
self._path_data.start = start
|
|
|
|
local path_neighbors = {
|
|
{x = 1, y = 0, z = 0},
|
|
{x = 1, y = 0, z = 1},
|
|
{x = 0, y = 0, z = 1},
|
|
{x = -1, y = 0, z = 1},
|
|
{x = -1, y = 0, z = 0},
|
|
{x = -1, y = 0, z = -1},
|
|
{x = 0, y = 0, z = -1},
|
|
{x = 1, y = 0, z = -1}
|
|
}
|
|
|
|
if climb then
|
|
table.insert(path_neighbors, {x = 0, y = 1, z = 0})
|
|
end
|
|
|
|
if fly
|
|
or swim then
|
|
path_neighbors = {
|
|
-- Central
|
|
{x = 1, y = 0, z = 0},
|
|
{x = 0, y = 0, z = 1},
|
|
{x = -1, y = 0, z = 0},
|
|
{x = 0, y = 0, z = -1},
|
|
-- Directly Up or Down
|
|
{x = 0, y = 1, z = 0},
|
|
{x = 0, y = -1, z = 0}
|
|
}
|
|
end
|
|
|
|
local function find_path(self, start, goal)
|
|
local us_time = minetest.get_us_time()
|
|
|
|
start = {
|
|
x = floor(start.x + 0.5),
|
|
y = floor(start.y + 0.5),
|
|
z = floor(start.z + 0.5)
|
|
}
|
|
|
|
goal = {
|
|
x = floor(goal.x + 0.5),
|
|
y = floor(goal.y + 0.5),
|
|
z = floor(goal.z + 0.5)
|
|
}
|
|
|
|
if goal.x == start.x
|
|
and goal.z == start.z then -- No path can be found
|
|
return nil
|
|
end
|
|
|
|
local openSet = self._path_data.open or {}
|
|
|
|
local closedSet = self._path_data.closed or {}
|
|
|
|
local start_index = minetest.hash_node_position(start)
|
|
|
|
openSet[start_index] = {
|
|
pos = start,
|
|
parent = nil,
|
|
gScore = 0,
|
|
fScore = get_distance(start, goal)
|
|
}
|
|
|
|
local count = self._path_data.count or 1
|
|
|
|
while count > 0 do
|
|
if minetest.get_us_time() - us_time > a_star_alloted_time then
|
|
self._path_data = {
|
|
start = start,
|
|
open = openSet,
|
|
closed = closedSet,
|
|
count = count
|
|
}
|
|
return
|
|
end
|
|
-- Initialize ID and data
|
|
local current_id
|
|
local current
|
|
|
|
-- Get an initial id in open set
|
|
for i, v in pairs(openSet) do
|
|
current_id = i
|
|
current = v
|
|
break
|
|
end
|
|
|
|
-- Find lowest f cost
|
|
for i, v in pairs(openSet) do
|
|
if v.fScore < current.fScore then
|
|
current_id = i
|
|
current = v
|
|
end
|
|
end
|
|
|
|
-- Add lowest fScore to closedSet and remove from openSet
|
|
openSet[current_id] = nil
|
|
closedSet[current_id] = current
|
|
|
|
self._path_data.open = openSet
|
|
self._path_data.closedSet = closedSet
|
|
|
|
-- Reconstruct path if end is reached
|
|
if ((is_on_ground(goal)
|
|
or fly)
|
|
and current_id == minetest.hash_node_position(goal))
|
|
or (not fly
|
|
and not is_on_ground(goal)
|
|
and goal.x == current.pos.x
|
|
and goal.z == current.pos.z) then
|
|
local path = {}
|
|
local fail_safe = 0
|
|
for k, v in pairs(closedSet) do
|
|
fail_safe = fail_safe + 1
|
|
end
|
|
repeat
|
|
if not closedSet[current_id] then return end
|
|
table.insert(path, closedSet[current_id].pos)
|
|
current_id = closedSet[current_id].parent
|
|
until current_id == start_index or #path >= fail_safe
|
|
if not closedSet[current_id] then self._path_data = {} return nil end
|
|
table.insert(path, closedSet[current_id].pos)
|
|
local reverse_path = {}
|
|
repeat table.insert(reverse_path, table.remove(path)) until #path == 0
|
|
self._path_data = {}
|
|
return reverse_path
|
|
end
|
|
|
|
count = count - 1
|
|
|
|
local adjacent = get_neighbors(self, current.pos, goal, swim, fly, climb, path_neighbors, openSet, closedSet)
|
|
|
|
-- Go through neighboring nodes
|
|
for i = 1, #adjacent do
|
|
local neighbor = {
|
|
pos = adjacent[i],
|
|
parent = current_id,
|
|
gScore = 0,
|
|
fScore = 0
|
|
}
|
|
local neighbor_id = minetest.hash_node_position(neighbor.pos)
|
|
local neighbour_gScore = current.gScore + get_distance_to_neighbor(current.pos, neighbor.pos)
|
|
if (not openSet[neighbor_id]
|
|
or neighbour_gScore < openSet[neighbor_id].gScore)
|
|
and not closedSet[neighbor_id] then
|
|
if not openSet[neighbor_id] then
|
|
count = count + 1
|
|
end
|
|
local hCost = get_distance_to_neighbor(neighbor.pos, goal)
|
|
neighbor.gScore = neighbour_gScore
|
|
neighbor.fScore = neighbour_gScore + hCost
|
|
openSet[neighbor_id] = neighbor
|
|
end
|
|
end
|
|
if count > (max_open or 100) then
|
|
self._path_data = {}
|
|
return
|
|
end
|
|
end
|
|
self._path_data = {}
|
|
return nil
|
|
end
|
|
return find_path(self, start, goal)
|
|
end
|
|
|
|
|
|
------------
|
|
-- Theta* --
|
|
------------
|
|
|
|
function get_line_of_sight(a, b)
|
|
local steps = floor(vector.distance(a, b))
|
|
local line = {}
|
|
|
|
for i = 0, steps do
|
|
local pos
|
|
|
|
if steps > 0 then
|
|
pos = {
|
|
x = a.x + (b.x - a.x) * (i / steps),
|
|
y = a.y + (b.y - a.y) * (i / steps),
|
|
z = a.z + (b.z - a.z) * (i / steps)
|
|
}
|
|
else
|
|
pos = a
|
|
end
|
|
table.insert(line, pos)
|
|
end
|
|
|
|
if #line < 1 then
|
|
return false
|
|
else
|
|
for i = 1, #line do
|
|
local node = minetest.get_node(line[i])
|
|
if minetest.registered_nodes[node.name].walkable then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function creatura.find_theta_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim)
|
|
climb = climb or false
|
|
fly = fly or false
|
|
swim = swim or false
|
|
|
|
start = self._path_data.start or start
|
|
|
|
self._path_data.start = start
|
|
|
|
local path_neighbors = {
|
|
{x = 1, y = 0, z = 0},
|
|
{x = 0, y = 0, z = 1},
|
|
{x = -1, y = 0, z = 0},
|
|
{x = 0, y = 0, z = -1},
|
|
}
|
|
|
|
if climb then
|
|
table.insert(path_neighbors, {x = 0, y = 1, z = 0})
|
|
end
|
|
|
|
if fly
|
|
or swim then
|
|
path_neighbors = {
|
|
-- Central
|
|
{x = 1, y = 0, z = 0},
|
|
{x = 0, y = 0, z = 1},
|
|
{x = -1, y = 0, z = 0},
|
|
{x = 0, y = 0, z = -1},
|
|
-- Directly Up or Down
|
|
{x = 0, y = 1, z = 0},
|
|
{x = 0, y = -1, z = 0}
|
|
}
|
|
end
|
|
|
|
local function find_path(self, start, goal)
|
|
local us_time = minetest.get_us_time()
|
|
|
|
start = {
|
|
x = floor(start.x + 0.5),
|
|
y = floor(start.y + 0.5),
|
|
z = floor(start.z + 0.5)
|
|
}
|
|
|
|
goal = {
|
|
x = floor(goal.x + 0.5),
|
|
y = floor(goal.y + 0.5),
|
|
z = floor(goal.z + 0.5)
|
|
}
|
|
|
|
if goal.x == start.x
|
|
and goal.z == start.z then -- No path can be found
|
|
return nil
|
|
end
|
|
|
|
local openSet = self._path_data.open or {}
|
|
|
|
local closedSet = self._path_data.closed or {}
|
|
|
|
local start_index = minetest.hash_node_position(start)
|
|
|
|
openSet[start_index] = {
|
|
pos = start,
|
|
parent = nil,
|
|
gScore = 0,
|
|
fScore = get_distance(start, goal)
|
|
}
|
|
|
|
local count = self._path_data.count or 1
|
|
|
|
while count > 0 do
|
|
if minetest.get_us_time() - us_time > theta_star_alloted_time then
|
|
self._path_data = {
|
|
start = start,
|
|
open = openSet,
|
|
closed = closedSet,
|
|
count = count
|
|
}
|
|
return
|
|
end
|
|
|
|
-- Initialize ID and data
|
|
local current_id
|
|
local current
|
|
|
|
-- Get an initial id in open set
|
|
for i, v in pairs(openSet) do
|
|
current_id = i
|
|
current = v
|
|
break
|
|
end
|
|
|
|
-- Find lowest f cost
|
|
for i, v in pairs(openSet) do
|
|
if v.fScore < current.fScore then
|
|
current_id = i
|
|
current = v
|
|
end
|
|
end
|
|
|
|
-- Add lowest fScore to closedSet and remove from openSet
|
|
openSet[current_id] = nil
|
|
closedSet[current_id] = current
|
|
|
|
-- Reconstruct path if end is reached
|
|
if (is_on_ground(goal)
|
|
and current_id == minetest.hash_node_position(goal))
|
|
or (not is_on_ground(goal)
|
|
and goal.x == current.pos.x
|
|
and goal.z == current.pos.z) then
|
|
local path = {}
|
|
local fail_safe = 0
|
|
for k, v in pairs(closedSet) do
|
|
fail_safe = fail_safe + 1
|
|
end
|
|
repeat
|
|
if not closedSet[current_id] then return end
|
|
table.insert(path, closedSet[current_id].pos)
|
|
current_id = closedSet[current_id].parent
|
|
until current_id == start_index or #path >= fail_safe
|
|
if not closedSet[current_id] then self._path_data = {} return nil end
|
|
table.insert(path, closedSet[current_id].pos)
|
|
local reverse_path = {}
|
|
repeat table.insert(reverse_path, table.remove(path)) until #path == 0
|
|
self._path_data = {}
|
|
return reverse_path
|
|
end
|
|
|
|
count = count - 1
|
|
|
|
local adjacent = get_neighbors(self, current.pos, goal, swim, fly, climb, path_neighbors, openSet, closedSet)
|
|
|
|
-- Go through neighboring nodes
|
|
for i = 1, #adjacent do
|
|
local neighbor = {
|
|
pos = adjacent[i],
|
|
parent = current_id,
|
|
gScore = 0,
|
|
fScore = 0
|
|
}
|
|
if not openSet[minetest.hash_node_position(neighbor.pos)]
|
|
and not closedSet[minetest.hash_node_position(neighbor.pos)] then
|
|
local current_parent = closedSet[current.parent] or closedSet[start_index]
|
|
if not current_parent then
|
|
current_parent = openSet[current.parent] or openSet[start_index]
|
|
end
|
|
if current_parent
|
|
and get_line_of_sight(current_parent.pos, neighbor.pos) then
|
|
local temp_gScore = current_parent.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos)
|
|
local new_gScore = 999
|
|
if openSet[minetest.hash_node_position(neighbor.pos)] then
|
|
new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore
|
|
end
|
|
if temp_gScore < new_gScore then
|
|
local hCost = get_distance_to_neighbor(neighbor.pos, goal)
|
|
neighbor.gScore = temp_gScore
|
|
neighbor.fScore = temp_gScore + hCost
|
|
neighbor.parent = minetest.hash_node_position(current_parent.pos)
|
|
if openSet[minetest.hash_node_position(neighbor.pos)] then
|
|
openSet[minetest.hash_node_position(neighbor.pos)] = nil
|
|
end
|
|
openSet[minetest.hash_node_position(neighbor.pos)] = neighbor
|
|
count = count + 1
|
|
end
|
|
else
|
|
local temp_gScore = current.gScore + get_distance_to_neighbor(current_parent.pos, neighbor.pos)
|
|
local new_gScore = 999
|
|
if openSet[minetest.hash_node_position(neighbor.pos)] then
|
|
new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore
|
|
end
|
|
if temp_gScore < new_gScore then
|
|
local hCost = get_distance_to_neighbor(neighbor.pos, goal)
|
|
neighbor.gScore = temp_gScore
|
|
neighbor.fScore = temp_gScore + hCost
|
|
if openSet[minetest.hash_node_position(neighbor.pos)] then
|
|
openSet[minetest.hash_node_position(neighbor.pos)] = nil
|
|
end
|
|
openSet[minetest.hash_node_position(neighbor.pos)] = neighbor
|
|
count = count + 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if count > (max_open or 100) then
|
|
self._path_data = {}
|
|
return
|
|
end
|
|
end
|
|
self._path_data = {}
|
|
return nil
|
|
end
|
|
return find_path(self, start, goal)
|
|
end |