From 297dd379a61c3464bc72abdced8d86808d8d2a24 Mon Sep 17 00:00:00 2001 From: Zorman2000 Date: Mon, 3 Apr 2017 21:48:17 -0400 Subject: [PATCH] Pathfinding: Fix for mod security - joining up Jumper library code (WIP) --- actions/pathfinder.lua | 25 +- actions/pathfinder2.lua | 274 ++++++++ actions/pathfinding.lua | 1311 +++++++++++++++++++++++++++++++++++++++ actions/places.lua | 4 +- init.lua | 26 + npc.lua | 83 ++- random_data.lua | 53 +- 7 files changed, 1745 insertions(+), 31 deletions(-) create mode 100644 actions/pathfinder2.lua create mode 100644 actions/pathfinding.lua diff --git a/actions/pathfinder.lua b/actions/pathfinder.lua index 874481b..80bac23 100644 --- a/actions/pathfinder.lua +++ b/actions/pathfinder.lua @@ -9,29 +9,6 @@ -- by Roland Yonaba (https://github.com/Yonaba/Jumper). -- Mapping algorithm: transforms a Minetest map surface to a 2d grid. -local path = minetest.get_modpath("advanced_npc") - --- Below code for require is taken and slightly modified --- from irc mod by Diego Martinez (kaeza) --- https://github.com/minetest-mods/irc --- Handle mod security if needed -local ie, req_ie = _G, minetest.request_insecure_environment -if req_ie then ie = req_ie() end -if not ie then - error("The Advances NPC mod requires access to insecure functions in ".. - "order to work. Please add the Advanced NPC mod to the ".. - "secure.trusted_mods setting or disable the mod.") -end - --- Modify package path so that it can find the Jumper library files -ie.package.path = - path .. "/Jumper/?.lua;".. - ie.package.path - --- Require the main files from Jumper -local Grid = ie.require("jumper.grid") -local Pathfinder = ie.require("jumper.pathfinder") - pathfinder = {} pathfinder.node_types = { @@ -238,7 +215,7 @@ function pathfinder.normalize_map(map) return result end --- This function returns an array of tables with to parameters: type and pos. +-- This function returns an array of tables with two parameters: type and pos. -- The position parameter is the actual coordinate on the Minetest map. The -- type is the type of the node at the coordinate defined as pathfinder.node_types. function pathfinder.get_path(map, path_nodes) diff --git a/actions/pathfinder2.lua b/actions/pathfinder2.lua new file mode 100644 index 0000000..593ab57 --- /dev/null +++ b/actions/pathfinder2.lua @@ -0,0 +1,274 @@ +-- Pathfinder by Zorman2000 +-- Pathfinding code with included A* implementation, customized +-- for Minetest. At the moment, paths can only be found in flat +-- terrain (only 2D pathfinding) + +-- Public namespace +pathfinder = {} +-- Private namespace for pathfinder functions +local finder = {} + +pathfinder.node_types = { + start = 0, + goal = 1, + walkable = 2, + openable = 3, + non_walkable = 4 +} + +pathfinder.nodes = { + openable_prefix = { + "doors:", + "cottages:gate", + "cottages:half_door" + } +} + +function pathfinder.find_path(start_pos, end_pos, extra_range, walkable_nodes) + -- Create map + local map = finder.create_map(start_pos, end_pos, extra_range, walkable_nodes) + minetest.log("Number of nodes in map: "..dump(#map)) + -- Use A* algorithm + local path = minetest.find_path(start_pos, end_pos, 30, 1, 1, "Dijkstra") + minetest.log("Path: "..dump(path)) + return path + --return finder.astar({name="air", pos=start_pos}, {name="air", pos=end_pos}, map) +end + +-- This function is used to determine if a node is walkable +-- or openable, in which case is good to use when finding a path +function finder.is_good_node(node, exceptions) + -- Is openable is to support doors, fence gates and other + -- doors from other mods. Currently, default doors, gates + -- and cottages doors are supported. + local is_openable = false + for _,node_prefix in pairs(pathfinder.nodes.openable_prefix) do + local start_i,end_i = string.find(node.name, node_prefix) + if start_i ~= nil then + is_openable = true + break + end + end + if node ~= nil and node.name ~= nil and not minetest.registered_nodes[node.name].walkable then + return pathfinder.node_types.walkable + elseif is_openable then + return pathfinder.node_types.openable + else + for i = 1, #exceptions do + if node.name == exceptions[i] then + return pathfinder.node_types.walkable + end + end + return pathfinder.node_types.non_walkable + end +end + +-- Maps a 2D slice of Minetest terrain into an array of nodes +-- Extra range is a number that will be added in the x and the z coordinates +-- to allow more room to find paths. +-- Walkables is an array of node names which are considered walkable, +-- even if they are not. +function finder.create_map(start_pos, end_pos, extra_range, walkables) + + minetest.log("Start pos: "..minetest.pos_to_string(start_pos)) + minetest.log("End pos: "..minetest.pos_to_string(end_pos)) + + -- Calculate all signs to ensure: + -- 1. Correct area calculation + -- 2. Iterate in the correct direction + local start_x_sign = (start_pos.x - end_pos.x) / math.abs(start_pos.x - end_pos.x) + local start_z_sign = (start_pos.z - end_pos.z) / math.abs(start_pos.z - end_pos.z) + local end_x_sign = (end_pos.x - start_pos.x) / math.abs(end_pos.x - start_pos.x) + local end_z_sign = (end_pos.z - start_pos.z) / math.abs(end_pos.z - start_pos.z) + + -- Correct the signs if they are nan + if math.abs(start_pos.x - end_pos.x) == 0 then + start_x_sign = -1 + end_x_sign = 1 + end + if math.abs(start_pos.z - end_pos.z) == 0 then + start_z_sign = -1 + end_z_sign = 1 + end + + -- Get starting and ending positions, adding the extra nodes to the area + local pos1 = {x=start_pos.x + (extra_range * start_x_sign), y = start_pos.y - 1, z=start_pos.z + (extra_range * start_z_sign)} + local pos2 = {x=end_pos.x + (extra_range * end_x_sign), y = end_pos.y, z=end_pos.z + (extra_range * end_z_sign)} + + minetest.log("Recalculated pos1: "..minetest.pos_to_string(pos1)) + minetest.log("Recalculated pos2: "..minetest.pos_to_string(pos2)) + + local grid = {} + + -- Loop through the area and classify nodes + for z = 1, math.abs(pos1.z - pos2.z) do + --local current_row = {} + for x = 1, math.abs(pos1.x - pos2.x) do + -- Calculate current position + local current_pos = {x=pos1.x + (x*end_x_sign), y=pos1.y, z=pos1.z + (z*end_z_sign)} + -- Get node info + local node = minetest.get_node(current_pos) + -- Check if this is the starting position + if current_pos.x == start_pos.x and current_pos.z == start_pos.z then + -- Is start position + table.insert(grid, {name=node.name, pos=current_pos, type=pathfinder.node_types.start}) + elseif current_pos.x == end_pos.x and current_pos.z == end_pos.z then + -- Is ending position or goal position + table.insert(grid, {name=node.name, pos=current_pos, type=pathfinder.node_types.goal}) + else + -- Check if node is walkable + if node.name == "air" then + -- If air do no more checks + table.insert(grid, {name=node.name, pos=current_pos, type=pathfinder.node_types.walkable}) + else + -- Check if it is of a walkable or openable type + table.insert(grid, {name=node.name, pos=current_pos, type=finder.is_good_node(node, walkables)}) + end + end + end + -- Insert the converted row into the grid + --table.insert(grid, current_row) + end + + return grid +end + +-------------------------------------------------------------------------- +-- A* algorithm implementation +-------------------------------------------------------------------------- +-- Utility functions +function finder.distance(node1, node2) + return math.sqrt(math.pow(node2.pos.x - node1.pos.x, 2) + math.pow(node2.pos.z - node1.pos.z, 2)) +end + +function finder.is_valid_neighbor(node1, node2) + -- Consider only orthogonal nodes + if (node1.pos.x == node2.pos.x and node1.pos.z ~= node2.pos.z) + or (node1.pos.z == node2.pos.z and node1.pos.x ~= node2.pos.z) then + if (finder.distance(node1, node2) < 2) then + return finder.is_good_node(node2, {}) + end + end + return false +end + +function finder.cost_estimate(node1, node2) + return finder.distance(node1, node2) +end + +function finder.get_lowest_fscore(nodes, f_scores) + local lowest = 1/0 + local best_node = nil + for _, node in pairs(nodes) do + local score = f_scores[node] + if score < lowest then + lowest = score + best_node = node + end + end + return best_node +end + +function finder.get_neighbor(node, nodes) + local neighbors = {} + for _, current_node in pairs(nodes) do + if current_node ~= node then + if finder.is_valid_neighbor(node, current_node) then + table.insert(neighbors, current_node) + end + end + end + return neighbors +end + +function finder.contains_node(node, all_nodes) + for _,current_node in pairs(all_nodes) do + if current_node == node then + return true + end + end + return false +end + +function finder.remove_node(node, all_nodes) + --minetest.log("On remove_node: "..dump(all_nodes)) + for key, current_node in pairs(all_nodes) do + if current_node == node then + --minetest.log("Table before: "..dump(all_nodes)) + table.remove(all_nodes, key) + --minetest.log("Table after: "..dump(all_nodes)) + return + end + end +end + +function finder.create_path(path, grid, node) + if grid[node] ~= nil then + table.insert(path, 1, grid[node]) + return create_path(path, grid, grid[node]) + else + return path + end +end + +function finder.astar(start_pos, end_pos, nodes) + + local closed_set = {} + local open_set = { + start_pos + } + local came_from = {} + + local g_score = {} + local f_score = {} + g_score[start_pos] = 0 + f_score[start_pos] = g_score[start_pos] + finder.cost_estimate(start_pos, end_pos) + + minetest.log("Open set: "..dump(#open_set)) + + while #open_set > 0 do + minetest.log("Nodes size: "..dump(#nodes)) + + local current = finder.get_lowest_fscore(open_set, f_score) + minetest.log("Node with best fscore: "..dump(current)) + if current == end_pos then + minetest.log("Creating path: "..dump(came_from)) + local path = create_path({}, came_from, end_pos) + table.insert(path, end_pos) + return path + end + + minetest.log("Removing node from openset..."..dump(#open_set)) + finder.remove_node(current, open_set) + minetest.log("Removed. Open set size: "..dump(#open_set)) + minetest.log("Adding to closed set..."..dump(#closed_set)) + table.insert(closed_set, current) + minetest.log("Added. New closed set size: "..dump(#closed_set)) + + local neighbors = finder.get_neighbor(current, nodes) + minetest.log("Found "..dump(#neighbors).." neighbors for current node")--dump(neighbors)) + for _, neighbor in pairs(neighbors) do + minetest.log("Currently looking at neighbor: "..minetest.pos_to_string(neighbor.pos)) + if finder.contains_node(neighbor, closed_set) == false then + minetest.log("Node is not in closed set") + + local tentative_g_score = g_score[current] + finder.distance(current, neighbor) + minetest.log("Tentative g score is: "..dump(tentative_g_score)) + minetest.log("Logic: "..dump(finder.contains_node(neighbor, open_set) == false or tentative_g_score < g_score[neighbor])) + if finder.contains_node(neighbor, open_set) == false or tentative_g_score < g_score[neighbor] then + came_from[neighbor] = current + minetest.log("Added node to came_from set: "..dump(table.getn(came_from))) + g_score[neighbor] = tentative_g_score + f_score[neighbor] = g_score[neighbor] + finder.cost_estimate(neighbor, end_pos) + if finder.contains_node(neighbor, open_set) == false then + minetest.log("Adding neighbor node to open_set: "..dump(#open_set)) + table.insert(open_set, neighbor) + minetest.log("Added. New open set size: "..dump(#open_set)) + end + end + end + end + end + -- Path not found + return nil +end diff --git a/actions/pathfinding.lua b/actions/pathfinding.lua new file mode 100644 index 0000000..3fa56e5 --- /dev/null +++ b/actions/pathfinding.lua @@ -0,0 +1,1311 @@ +-- Code based on Jumper library + +--- The Pathfinder class + +-- Modification by Zorman2000: +-- This file has been modified to be usable with Minetest. +-- Do to Minetest's mod security, the global "require()" call +-- is disabled, and needs to be replaced by one using Minetest's +-- environment. +-- - Get Minetest's insecure environment +-- - Change all "require" calls to "ie.require()" +-- +-- The rest of the code is left intact and is (c) 2012-2013 Roland Yonaba + +local abs = math.abs +local sqrt = math.sqrt + +local Heuristics = { + ['MANHATTAN'] = function(nodeA, nodeB) + local dx = abs(nodeA._x - nodeB._x) + local dy = abs(nodeA._y - nodeB._y) + return (dx + dy) + end, + ['EUCLIDIAN'] = function(nodeA, nodeB) + local dx = nodeA._x - nodeB._x + local dy = nodeA._y - nodeB._y + return sqrt(dx*dx+dy*dy) + end +} + +local Node = setmetatable({}, + {__call = function(self,...) + return Node:new(...) + end} + ) + Node.__index = Node + + --- Inits a new `node` + -- @class function + -- @tparam int x the x-coordinate of the node on the collision map + -- @tparam int y the y-coordinate of the node on the collision map + -- @treturn node a new `node` + -- @usage local node = Node(3,4) + function Node:new(x,y) + return setmetatable({_x = x, _y = y, _clearance = {}}, Node) + end + + -- Enables the use of operator '<' to compare nodes. + -- Will be used to sort a collection of nodes in a binary heap on the basis of their F-cost + function Node.__lt(A,B) return (A._f < B._f) end + + --- Returns x-coordinate of a `node` + -- @class function + -- @treturn number the x-coordinate of the `node` + -- @usage local x = node:getX() + function Node:getX() return self._x end + + --- Returns y-coordinate of a `node` + -- @class function + -- @treturn number the y-coordinate of the `node` + -- @usage local y = node:getY() + function Node:getY() return self._y end + + --- Returns x and y coordinates of a `node` + -- @class function + -- @treturn number the x-coordinate of the `node` + -- @treturn number the y-coordinate of the `node` + -- @usage local x, y = node:getPos() + function Node:getPos() return self._x, self._y end + + --- Returns the amount of true [clearance](http://aigamedev.com/open/tutorial/clearance-based-pathfinding/#TheTrueClearanceMetric) + -- for a given `node` + -- @class function + -- @tparam string|int|func walkable the value for walkable locations in the collision map array. + -- @treturn int the clearance of the `node` + -- @usage + -- -- Assuming walkable was 0 + -- local clearance = node:getClearance(0) + function Node:getClearance(walkable) + return self._clearance[walkable] + end + + --- Removes the clearance value for a given walkable. + -- @class function + -- @tparam string|int|func walkable the value for walkable locations in the collision map array. + -- @treturn node self (the calling `node` itself, can be chained) + -- @usage + -- -- Assuming walkable is defined + -- node:removeClearance(walkable) + function Node:removeClearance(walkable) + self._clearance[walkable] = nil + return self + end + + --- Clears temporary cached attributes of a `node`. + -- Deletes the attributes cached within a given node after a pathfinding call. + -- This function is internally used by the search algorithms, so you should not use it explicitely. + -- @class function + -- @treturn node self (the calling `node` itself, can be chained) + -- @usage + -- local thisNode = Node(1,2) + -- thisNode:reset() + function Node:reset() + self._g, self._h, self._f = nil, nil, nil + self._opened, self._closed, self._parent = nil, nil, nil + return self + end + +--------------------------------------------------------------- +-- Path class implementation by Ronald Yonaba +-- Original code here: +--------------------------------------------------------------- +-- Local references +local abs, max = math.abs, math.max +local t_insert, t_remove = table.insert, table.remove + +--- The `Path` class.
+-- This class is callable. +-- Therefore, Path(...) acts as a shortcut to Path:new(...). +-- @type Path +local Path = setmetatable({}, + {__call = function(self,...) + return Path:new(...) + end + }) + +Path.__index = Path + +--- Inits a new `path`. +-- @class function +-- @treturn path a `path` +-- @usage local p = Path() +function Path:new() + return setmetatable({_nodes = {}}, Path) +end + +--- Iterates on each single `node` along a `path`. At each step of iteration, +-- returns the `node` plus a count value. Aliased as @{Path:nodes} +-- @class function +-- @treturn node a `node` +-- @treturn int the count for the number of nodes +-- @see Path:nodes +-- @usage +-- for node, count in p:iter() do +-- ... +-- end +function Path:iter() + local i,pathLen = 1,#self._nodes + return function() + if self._nodes[i] then + i = i+1 + return self._nodes[i-1],i-1 + end + end +end + +--- Iterates on each single `node` along a `path`. At each step of iteration, +-- returns a `node` plus a count value. Alias for @{Path:iter} +-- @class function +-- @name Path:nodes +-- @treturn node a `node` +-- @treturn int the count for the number of nodes +-- @see Path:iter +-- @usage +-- for node, count in p:nodes() do +-- ... +-- end +Path.nodes = Path.iter + +--- Evaluates the `path` length +-- @class function +-- @treturn number the `path` length +-- @usage local len = p:getLength() +function Path:getLength() + local len = 0 + for i = 2,#self._nodes do + len = len + Heuristic.EUCLIDIAN(self._nodes[i], self._nodes[i-1]) + end + return len +end + +--- Counts the number of steps. +-- Returns the number of waypoints (nodes) in the current path. +-- @class function +-- @tparam node node a node to be added to the path +-- @tparam[opt] int index the index at which the node will be inserted. If omitted, the node will be appended after the last node in the path. +-- @treturn path self (the calling `path` itself, can be chained) +-- @usage local nSteps = p:countSteps() +function Path:addNode(node, index) + index = index or #self._nodes+1 + t_insert(self._nodes, index, node) + return self +end + + +--- `Path` filling modifier. Interpolates between non contiguous nodes along a `path` +-- to build a fully continuous `path`. This maybe useful when using search algorithms such as Jump Point Search. +-- Does the opposite of @{Path:filter} +-- @class function +-- @treturn path self (the calling `path` itself, can be chained) +-- @see Path:filter +-- @usage p:fill() +function Path:fill() + local i = 2 + local xi,yi,dx,dy + local N = #self._nodes + local incrX, incrY + while true do + xi,yi = self._nodes[i]._x,self._nodes[i]._y + dx,dy = xi-self._nodes[i-1]._x,yi-self._nodes[i-1]._y + if (abs(dx) > 1 or abs(dy) > 1) then + incrX = dx/max(abs(dx),1) + incrY = dy/max(abs(dy),1) + t_insert(self._nodes, i, self._grid:getNodeAt(self._nodes[i-1]._x + incrX, self._nodes[i-1]._y +incrY)) + N = N+1 + else i=i+1 + end + if i>N then break end + end + return self +end + +--- `Path` compression modifier. Given a `path`, eliminates useless nodes to return a lighter `path` +-- consisting of straight moves. Does the opposite of @{Path:fill} +-- @class function +-- @treturn path self (the calling `path` itself, can be chained) +-- @see Path:fill +-- @usage p:filter() +function Path:filter() + local i = 2 + local xi,yi,dx,dy, olddx, olddy + xi,yi = self._nodes[i]._x, self._nodes[i]._y + dx, dy = xi - self._nodes[i-1]._x, yi-self._nodes[i-1]._y + while true do + olddx, olddy = dx, dy + if self._nodes[i+1] then + i = i+1 + xi, yi = self._nodes[i]._x, self._nodes[i]._y + dx, dy = xi - self._nodes[i-1]._x, yi - self._nodes[i-1]._y + if olddx == dx and olddy == dy then + t_remove(self._nodes, i-1) + i = i - 1 + end + else break end + end + return self +end + +--- Clones a `path`. +-- @class function +-- @treturn path a `path` +-- @usage local p = path:clone() +function Path:clone() + local p = Path:new() + for node in self:nodes() do p:addNode(node) end + return p +end + +--- Checks if a `path` is equal to another. It also supports *filtered paths* (see @{Path:filter}). +-- @class function +-- @tparam path p2 a path +-- @treturn boolean a boolean +-- @usage print(myPath:isEqualTo(anotherPath)) +function Path:isEqualTo(p2) + local p1 = self:clone():filter() + local p2 = p2:clone():filter() + for node, count in p1:nodes() do + if not p2._nodes[count] then return false end + local n = p2._nodes[count] + if n._x~=node._x or n._y~=node._y then return false end + end + return true +end + +--- Reverses a `path`. +-- @class function +-- @treturn path self (the calling `path` itself, can be chained) +-- @usage myPath:reverse() +function Path:reverse() + local _nodes = {} + for i = #self._nodes,1,-1 do + _nodes[#_nodes+1] = self._nodes[i] + end + self._nodes = _nodes + return self +end + +--- Appends a given `path` to self. +-- @class function +-- @tparam path p a path +-- @treturn path self (the calling `path` itself, can be chained) +-- @usage myPath:append(anotherPath) +function Path:append(p) + for node in p:nodes() do self:addNode(node) end + return self +end + + + + -- Local reference +local floor = math.floor + +local Utils = { + traceBackPath = function(finder, node, startNode) + local path = Path:new() + path._grid = finder._grid + while true do + if node._parent then + t_insert(path._nodes,1,node) + node = node._parent + else + t_insert(path._nodes,1,startNode) + return path + end + end + end, + + -- Lookup for value in a table + indexOf = function(t,v) + for i = 1,#t do + if t[i] == v then return i end + end + return nil + end, + + getArrayBounds = function(map) + local min_x, max_x + local min_y, max_y + for y in pairs(map) do + min_y = not min_y and y or (ymax_y and y or max_y) + for x in pairs(map[y]) do + min_x = not min_x and x or (xmax_x and x or max_x) + end + end + return min_x,max_x,min_y,max_y + end, + + -- Converts an array to a set of nodes + arrayToNodes = function(map) + local min_x, max_x + local min_y, max_y + local nodes = {} + for y in pairs(map) do + min_y = not min_y and y or (ymax_y and y or max_y) + nodes[y] = {} + for x in pairs(map[y]) do + min_x = not min_x and x or (xmax_x and x or max_x) + nodes[y][x] = Node:new(x,y) + end + end + return nodes, + (min_x or 0), (max_x or 0), + (min_y or 0), (max_y or 0) + end +} + +--------------------------------------------------------------- +-- B-Heap implementation by Ronald Yonaba +-- Original code here: +--------------------------------------------------------------- + +-- Default comparison function +local function f_min(a,b) return a < b end + +-- Percolates up +local function percolate_up(heap, index) + if index == 1 then return end + local pIndex + if index <= 1 then return end + if index%2 == 0 then + pIndex = index/2 + else pIndex = (index-1)/2 + end + if not heap._sort(heap._heap[pIndex], heap._heap[index]) then + heap._heap[pIndex], heap._heap[index] = + heap._heap[index], heap._heap[pIndex] + percolate_up(heap, pIndex) + end +end + +-- Percolates down +local function percolate_down(heap,index) + local lfIndex,rtIndex,minIndex + lfIndex = 2*index + rtIndex = lfIndex + 1 + if rtIndex > heap._size then + if lfIndex > heap._size then return + else minIndex = lfIndex end + else + if heap._sort(heap._heap[lfIndex],heap._heap[rtIndex]) then + minIndex = lfIndex + else + minIndex = rtIndex + end + end + if not heap._sort(heap._heap[index],heap._heap[minIndex]) then + heap._heap[index],heap._heap[minIndex] = heap._heap[minIndex],heap._heap[index] + percolate_down(heap,minIndex) + end +end + +-- Produces a new heap +local function newHeap(template,comp) + return setmetatable({_heap = {}, + _sort = comp or f_min, _size = 0}, + template) +end + + +--- The `heap` class.
+-- This class is callable. +-- _Therefore,_ heap(...) _is used to instantiate new heaps_. +-- @type heap +local heap = setmetatable({}, + {__call = function(self,...) + return newHeap(self,...) + end}) +heap.__index = heap + +--- Checks if a `heap` is empty +-- @class function +-- @treturn bool __true__ of no item is queued in the heap, __false__ otherwise +-- @usage +-- if myHeap:empty() then +-- print('Heap is empty!') +-- end +function heap:empty() + return (self._size==0) +end + +--- Clears the `heap` (removes all items queued in the heap) +-- @class function +-- @treturn heap self (the calling `heap` itself, can be chained) +-- @usage myHeap:clear() +function heap:clear() + self._heap = {} + self._size = 0 + self._sort = self._sort or f_min + return self +end + +--- Adds a new item in the `heap` +-- @class function +-- @tparam value item a new value to be queued in the heap +-- @treturn heap self (the calling `heap` itself, can be chained) +-- @usage +-- myHeap:push(1) +-- -- or, with chaining +-- myHeap:push(1):push(2):push(4) +function heap:push(item) + if item then + self._size = self._size + 1 + self._heap[self._size] = item + percolate_up(self, self._size) + end + return self +end + +--- Pops from the `heap`. +-- Removes and returns the lowest cost item (with respect to the comparison function being used) from the `heap`. +-- @class function +-- @treturn value a value previously pushed into the heap +-- @usage +-- while not myHeap:empty() do +-- local lowestValue = myHeap:pop() +-- ... +-- end +function heap:pop() + local root + if self._size > 0 then + root = self._heap[1] + self._heap[1] = self._heap[self._size] + self._heap[self._size] = nil + self._size = self._size-1 + if self._size>1 then + percolate_down(self, 1) + end + end + return root +end + +--- Restores the `heap` property. +-- Reorders the `heap` with respect to the comparison function being used. +-- When given argument __item__ (a value existing in the `heap`), will sort from that very item in the `heap`. +-- Otherwise, the whole `heap` will be cheacked. +-- @class function +-- @tparam[opt] value item the modified value +-- @treturn heap self (the calling `heap` itself, can be chained) +-- @usage myHeap:heapify() +function heap:heapify(item) + if self._size == 0 then return end + if item then + local i = Utils.indexOf(self._heap,item) + if i then + percolate_down(self, i) + percolate_up(self, i) + end + return + end + for i = floor(self._size/2),1,-1 do + percolate_down(self,i) + end + return self +end + +--------------------------------------------------------------- +-- Grid implementation by Ronald Yonaba +-- Original code here: +--------------------------------------------------------------- +local pairs = pairs +local assert = assert +local next = next +local setmetatable = setmetatable +local floor = math.floor +local coroutine = coroutine + +-- Offsets for straights moves +local straightOffsets = { + {x = 1, y = 0} --[[W]], {x = -1, y = 0}, --[[E]] + {x = 0, y = 1} --[[S]], {x = 0, y = -1}, --[[N]] +} + +-- Offsets for diagonal moves +local diagonalOffsets = { + {x = -1, y = -1} --[[NW]], {x = 1, y = -1}, --[[NE]] + {x = -1, y = 1} --[[SW]], {x = 1, y = 1}, --[[SE]] +} + +--- The `Grid` class.
+-- This class is callable. +-- Therefore,_ Grid(...) _acts as a shortcut to_ Grid:new(...). +-- @type Grid +Grid = setmetatable({},{ + __call = function(self,...) + return self:new(...) + end +}) +Grid.__index = Grid + +-- Specialized grids +local PreProcessGrid = setmetatable({},Grid) +local PostProcessGrid = setmetatable({},Grid) +PreProcessGrid.__index = PreProcessGrid +PostProcessGrid.__index = PostProcessGrid +PreProcessGrid.__call = function (self,x,y) + return self:getNodeAt(x,y) +end +PostProcessGrid.__call = function (self,x,y,create) + if create then return self:getNodeAt(x,y) end + return self._nodes[y] and self._nodes[y][x] +end + +--- Inits a new `grid` +-- @class function +-- @tparam table|string map A collision map - (2D array) with consecutive indices (starting at 0 or 1) +-- or a `string` with line-break chars (\n or \r) as row delimiters. +-- @tparam[opt] bool cacheNodeAtRuntime When __true__, returns an empty `grid` instance, so that +-- later on, indexing a non-cached `node` will cause it to be created and cache within the `grid` on purpose (i.e, when needed). +-- This is a __memory-safe__ option, in case your dealing with some tight memory constraints. +-- Defaults to __false__ when omitted. +-- @treturn grid a new `grid` instance +-- @usage +-- -- A simple 3x3 grid +-- local myGrid = Grid:new({{0,0,0},{0,0,0},{0,0,0}}) +-- +-- -- A memory-safe 3x3 grid +-- myGrid = Grid('000\n000\n000', true) +function Grid:new(map, cacheNodeAtRuntime) + if type(map) == 'string' then + map = Utils.strToMap(map) + end + if cacheNodeAtRuntime then + return PostProcessGrid:new(map,walkable) + end + return PreProcessGrid:new(map,walkable) +end + +--- Checks if `node` at [x,y] is __walkable__. +-- Will check if `node` at location [x,y] both *exists* on the collision map and *is walkable* +-- @class function +-- @tparam int x the x-location of the node +-- @tparam int y the y-location of the node +-- @tparam[opt] string|int|func walkable the value for walkable locations in the collision map array (see @{Grid:new}). +-- Defaults to __false__ when omitted. +-- If this parameter is a function, it should be prototyped as __f(value)__ and return a `boolean`: +-- __true__ when value matches a __walkable__ `node`, __false__ otherwise. If this parameter is not given +-- while location [x,y] __is valid__, this actual function returns __true__. +-- @tparam[optchain] int clearance the amount of clearance needed. Defaults to 1 (normal clearance) when not given. +-- @treturn bool __true__ if `node` exists and is __walkable__, __false__ otherwise +-- @usage +-- -- Always true +-- print(myGrid:isWalkableAt(2,3)) +-- +-- -- True if node at [2,3] collision map value is 0 +-- print(myGrid:isWalkableAt(2,3,0)) +-- +-- -- True if node at [2,3] collision map value is 0 and has a clearance higher or equal to 2 +-- print(myGrid:isWalkableAt(2,3,0,2)) +-- +function Grid:isWalkableAt(x, y, walkable, clearance) + local nodeValue = self._map[y] and self._map[y][x] + if nodeValue then + if not walkable then return true end + else + return false + end + local hasEnoughClearance = not clearance and true or false + if not hasEnoughClearance then + if not self._isAnnotated[walkable] then return false end + local node = self:getNodeAt(x,y) + local nodeClearance = node:getClearance(walkable) + hasEnoughClearance = (nodeClearance >= clearance) + end + if self._eval then + return walkable(nodeValue) and hasEnoughClearance + end + return ((nodeValue == walkable) and hasEnoughClearance) +end + +--- Returns the `grid` width. +-- @class function +-- @treturn int the `grid` width +-- @usage print(myGrid:getWidth()) +function Grid:getWidth() + return self._width +end + +--- Returns the `grid` height. +-- @class function +-- @treturn int the `grid` height +-- @usage print(myGrid:getHeight()) +function Grid:getHeight() + return self._height +end + +--- Returns the collision map. +-- @class function +-- @treturn map the collision map (see @{Grid:new}) +-- @usage local map = myGrid:getMap() +function Grid:getMap() + return self._map +end + +--- Returns the set of nodes. +-- @class function +-- @treturn {{node,...},...} an array of nodes +-- @usage local nodes = myGrid:getNodes() +function Grid:getNodes() + return self._nodes +end + +--- Returns the `grid` bounds. Returned values corresponds to the upper-left +-- and lower-right coordinates (in tile units) of the actual `grid` instance. +-- @class function +-- @treturn int the upper-left corner x-coordinate +-- @treturn int the upper-left corner y-coordinate +-- @treturn int the lower-right corner x-coordinate +-- @treturn int the lower-right corner y-coordinate +-- @usage local left_x, left_y, right_x, right_y = myGrid:getBounds() +function Grid:getBounds() + return self._min_x, self._min_y,self._max_x, self._max_y +end + +--- Returns neighbours. The returned value is an array of __walkable__ nodes neighbouring a given `node`. +-- @class function +-- @tparam node node a given `node` +-- @tparam[opt] string|int|func walkable the value for walkable locations in the collision map array (see @{Grid:new}). +-- Defaults to __false__ when omitted. +-- @tparam[optchain] bool allowDiagonal when __true__, allows adjacent nodes are included (8-neighbours). +-- Defaults to __false__ when omitted. +-- @tparam[optchain] bool tunnel When __true__, allows the `pathfinder` to tunnel through walls when heading diagonally. +-- @tparam[optchain] int clearance When given, will prune for the neighbours set all nodes having a clearance value lower than the passed-in value +-- Defaults to __false__ when omitted. +-- @treturn {node,...} an array of nodes neighbouring a given node +-- @usage +-- local aNode = myGrid:getNodeAt(5,6) +-- local neighbours = myGrid:getNeighbours(aNode, 0, true) +function Grid:getNeighbours(node, walkable, allowDiagonal, tunnel, clearance) + local neighbours = {} + for i = 1,#straightOffsets do + local n = self:getNodeAt( + node._x + straightOffsets[i].x, + node._y + straightOffsets[i].y + ) + if n and self:isWalkableAt(n._x, n._y, walkable, clearance) then + neighbours[#neighbours+1] = n + end + end + + if not allowDiagonal then return neighbours end + + tunnel = not not tunnel + for i = 1,#diagonalOffsets do + local n = self:getNodeAt( + node._x + diagonalOffsets[i].x, + node._y + diagonalOffsets[i].y + ) + if n and self:isWalkableAt(n._x, n._y, walkable, clearance) then + if tunnel then + neighbours[#neighbours+1] = n + else + local skipThisNode = false + local n1 = self:getNodeAt(node._x+diagonalOffsets[i].x, node._y) + local n2 = self:getNodeAt(node._x, node._y+diagonalOffsets[i].y) + if ((n1 and n2) and not self:isWalkableAt(n1._x, n1._y, walkable, clearance) and not self:isWalkableAt(n2._x, n2._y, walkable, clearance)) then + skipThisNode = true + end + if not skipThisNode then neighbours[#neighbours+1] = n end + end + end + end + + return neighbours +end + +--- Grid iterator. Iterates on every single node +-- in the `grid`. Passing __lx, ly, ex, ey__ arguments will iterate +-- only on nodes inside the bounding-rectangle delimited by those given coordinates. +-- @class function +-- @tparam[opt] int lx the leftmost x-coordinate of the rectangle. Default to the `grid` leftmost x-coordinate (see @{Grid:getBounds}). +-- @tparam[optchain] int ly the topmost y-coordinate of the rectangle. Default to the `grid` topmost y-coordinate (see @{Grid:getBounds}). +-- @tparam[optchain] int ex the rightmost x-coordinate of the rectangle. Default to the `grid` rightmost x-coordinate (see @{Grid:getBounds}). +-- @tparam[optchain] int ey the bottom-most y-coordinate of the rectangle. Default to the `grid` bottom-most y-coordinate (see @{Grid:getBounds}). +-- @treturn node a `node` on the collision map, upon each iteration step +-- @treturn int the iteration count +-- @usage +-- for node, count in myGrid:iter() do +-- print(node:getX(), node:getY(), count) +-- end +function Grid:iter(lx,ly,ex,ey) + local min_x = lx or self._min_x + local min_y = ly or self._min_y + local max_x = ex or self._max_x + local max_y = ey or self._max_y + + local x, y + y = min_y + return function() + x = not x and min_x or x+1 + if x > max_x then + x = min_x + y = y+1 + end + if y > max_y then + y = nil + end + return self._nodes[y] and self._nodes[y][x] or self:getNodeAt(x,y) + end +end + +--- Grid iterator. Iterates on each node along the outline (border) of a squared area +-- centered on the given node. +-- @tparam node node a given `node` +-- @tparam[opt] int radius the area radius (half-length). Defaults to __1__ when not given. +-- @treturn node a `node` at each iteration step +-- @usage +-- for node in myGrid:around(node, 2) do +-- ... +-- end +function Grid:around(node, radius) + local x, y = node._x, node._y + radius = radius or 1 + local _around = Utils.around() + local _nodes = {} + repeat + local state, x, y = coroutine.resume(_around,x,y,radius) + local nodeAt = state and self:getNodeAt(x, y) + if nodeAt then _nodes[#_nodes+1] = nodeAt end + until (not state) + local _i = 0 + return function() + _i = _i+1 + return _nodes[_i] + end +end + +--- Each transformation. Calls the given function on each `node` in the `grid`, +-- passing the `node` as the first argument to function __f__. +-- @class function +-- @tparam func f a function prototyped as __f(node,...)__ +-- @tparam[opt] vararg ... args to be passed to function __f__ +-- @treturn grid self (the calling `grid` itself, can be chained) +-- @usage +-- local function printNode(node) +-- print(node:getX(), node:getY()) +-- end +-- myGrid:each(printNode) +function Grid:each(f,...) + for node in self:iter() do f(node,...) end + return self +end + +--- Each (in range) transformation. Calls a function on each `node` in the range of a rectangle of cells, +-- passing the `node` as the first argument to function __f__. +-- @class function +-- @tparam int lx the leftmost x-coordinate coordinate of the rectangle +-- @tparam int ly the topmost y-coordinate of the rectangle +-- @tparam int ex the rightmost x-coordinate of the rectangle +-- @tparam int ey the bottom-most y-coordinate of the rectangle +-- @tparam func f a function prototyped as __f(node,...)__ +-- @tparam[opt] vararg ... args to be passed to function __f__ +-- @treturn grid self (the calling `grid` itself, can be chained) +-- @usage +-- local function printNode(node) +-- print(node:getX(), node:getY()) +-- end +-- myGrid:eachRange(1,1,8,8,printNode) +function Grid:eachRange(lx,ly,ex,ey,f,...) + for node in self:iter(lx,ly,ex,ey) do f(node,...) end + return self +end + +--- Map transformation. +-- Calls function __f(node,...)__ on each `node` in a given range, passing the `node` as the first arg to function __f__ and replaces +-- it with the returned value. Therefore, the function should return a `node`. +-- @class function +-- @tparam func f a function prototyped as __f(node,...)__ +-- @tparam[opt] vararg ... args to be passed to function __f__ +-- @treturn grid self (the calling `grid` itself, can be chained) +-- @usage +-- local function nothing(node) +-- return node +-- end +-- myGrid:imap(nothing) +function Grid:imap(f,...) + for node in self:iter() do + node = f(node,...) + end + return self +end + +--- Map in range transformation. +-- Calls function __f(node,...)__ on each `node` in a rectangle range, passing the `node` as the first argument to the function and replaces +-- it with the returned value. Therefore, the function should return a `node`. +-- @class function +-- @tparam int lx the leftmost x-coordinate coordinate of the rectangle +-- @tparam int ly the topmost y-coordinate of the rectangle +-- @tparam int ex the rightmost x-coordinate of the rectangle +-- @tparam int ey the bottom-most y-coordinate of the rectangle +-- @tparam func f a function prototyped as __f(node,...)__ +-- @tparam[opt] vararg ... args to be passed to function __f__ +-- @treturn grid self (the calling `grid` itself, can be chained) +-- @usage +-- local function nothing(node) +-- return node +-- end +-- myGrid:imap(1,1,6,6,nothing) +function Grid:imapRange(lx,ly,ex,ey,f,...) + for node in self:iter(lx,ly,ex,ey) do + node = f(node,...) + end + return self +end + +-- Specialized grids +-- Inits a preprocessed grid +function PreProcessGrid:new(map) + local newGrid = {} + newGrid._map = map + newGrid._nodes, newGrid._min_x, newGrid._max_x, newGrid._min_y, newGrid._max_y = Utils.arrayToNodes(newGrid._map) + newGrid._width = (newGrid._max_x-newGrid._min_x)+1 + newGrid._height = (newGrid._max_y-newGrid._min_y)+1 + newGrid._isAnnotated = {} + return setmetatable(newGrid,PreProcessGrid) +end + +-- Inits a postprocessed grid +function PostProcessGrid:new(map) + local newGrid = {} + newGrid._map = map + newGrid._nodes = {} + newGrid._min_x, newGrid._max_x, newGrid._min_y, newGrid._max_y = Utils.getArrayBounds(newGrid._map) + newGrid._width = (newGrid._max_x-newGrid._min_x)+1 + newGrid._height = (newGrid._max_y-newGrid._min_y)+1 + newGrid._isAnnotated = {} + return setmetatable(newGrid,PostProcessGrid) +end + +--- Returns the `node` at location [x,y]. +-- @class function +-- @name Grid:getNodeAt +-- @tparam int x the x-coordinate coordinate +-- @tparam int y the y-coordinate coordinate +-- @treturn node a `node` +-- @usage local aNode = myGrid:getNodeAt(2,2) + +-- Gets the node at location on a preprocessed grid +function PreProcessGrid:getNodeAt(x,y) + return self._nodes[y] and self._nodes[y][x] or nil +end + +-- Gets the node at location on a postprocessed grid +function PostProcessGrid:getNodeAt(x,y) + if not x or not y then return end + if Utils.outOfRange(x,self._min_x,self._max_x) then return end + if Utils.outOfRange(y,self._min_y,self._max_y) then return end + if not self._nodes[y] then self._nodes[y] = {} end + if not self._nodes[y][x] then self._nodes[y][x] = Node:new(x,y) end + return self._nodes[y][x] +end + + +--------------------------------------------------------------- +-- A* algorithm implementation by Ronald Yonaba +-- Original code here: +--------------------------------------------------------------- +-- Internalization +local ipairs = ipairs +local huge = math.huge + +-- Updates G-cost +local function computeCost(node, neighbour, finder, clearance) + local mCost = Heuristics.EUCLIDIAN(neighbour, node) + if node._g + mCost < neighbour._g then + neighbour._parent = node + neighbour._g = node._g + mCost + end +end + +-- Updates vertex node-neighbour +local function updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval) + local oldG = neighbour._g + local cmpCost = overrideCostEval or computeCost + cmpCost(node, neighbour, finder, clearance) + if neighbour._g < oldG then + local nClearance = neighbour._clearance[finder._walkable] + local pushThisNode = clearance and nClearance and (nClearance >= clearance) + if (clearance and pushThisNode) or (not clearance) then + if neighbour._opened then neighbour._opened = false end + neighbour._h = heuristic(endNode, neighbour) + neighbour._f = neighbour._g + neighbour._h + openList:push(neighbour) + neighbour._opened = true + end + end +end + +-- Calculates a path. +-- Returns the path from location `` to location ``. +local function ASTAR(finder, startNode, endNode, clearance, toClear, overrideHeuristic, overrideCostEval) + + local heuristic = overrideHeuristic or finder._heuristic + local openList = heap() + startNode._g = 0 + startNode._h = heuristic(endNode, startNode) + startNode._f = startNode._g + startNode._h + openList:push(startNode) + toClear[startNode] = true + startNode._opened = true + + while not openList:empty() do + local node = openList:pop() + node._closed = true + if node == endNode then return node end + local neighbours = finder._grid:getNeighbours(node, finder._walkable, finder._allowDiagonal, finder._tunnel) + for i = 1,#neighbours do + local neighbour = neighbours[i] + if not neighbour._closed then + toClear[neighbour] = true + if not neighbour._opened then + neighbour._g = huge + neighbour._parent = nil + end + updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval) + end + end + end + + return nil +end + +local Finders = { + ['ASTAR'] = ASTAR +} + +-- Internalization +local pairs = pairs +local assert = assert +local type = type + +local setmetatable, getmetatable = setmetatable, getmetatable + +-- Will keep track of all nodes expanded during the search +-- to easily reset their properties for the next pathfinding call +local toClear = {} + +--- Search modes. Refers to the search modes. In ORTHOGONAL mode, 4-directions are only possible when moving, +-- including North, East, West, South. In DIAGONAL mode, 8-directions are possible when moving, +-- including North, East, West, South and adjacent directions. +-- +--
  • ORTHOGONAL
  • +--
  • DIAGONAL
  • +-- @mode Modes +-- @see Pathfinder:getModes +local searchModes = {['DIAGONAL'] = true, ['ORTHOGONAL'] = true} + +-- Performs a traceback from the goal node to the start node +-- Only happens when the path was found + +--- The `Pathfinder` class.
    +-- This class is callable. +-- Therefore,_ Pathfinder(...) _acts as a shortcut to_ Pathfinder:new(...). +-- @type Pathfinder +Pathfinder = setmetatable({},{ + __call = function(self,...) + return self:new(...) + end +}) +Pathfinder.__index = Pathfinder + +--- Inits a new `pathfinder` +-- @class function +-- @tparam grid grid a `grid` +-- @tparam[opt] string finderName the name of the `Finder` (search algorithm) to be used for search. +-- Defaults to `ASTAR` when not given (see @{Pathfinder:getFinders}). +-- @tparam[optchain] string|int|func walkable the value for __walkable__ nodes. +-- If this parameter is a function, it should be prototyped as __f(value)__, returning a boolean: +-- __true__ when value matches a __walkable__ `node`, __false__ otherwise. +-- @treturn pathfinder a new `pathfinder` instance +-- @usage +-- -- Example one +-- local finder = Pathfinder:new(myGrid, 'ASTAR', 0) +-- +-- -- Example two +-- local function walkable(value) +-- return value > 0 +-- end +-- local finder = Pathfinder(myGrid, 'JPS', walkable) +function Pathfinder:new(grid, finderName, walkable) + local newPathfinder = {} + setmetatable(newPathfinder, Pathfinder) + newPathfinder:setGrid(grid) + newPathfinder:setFinder(finderName) + newPathfinder:setWalkable(walkable) + newPathfinder:setMode('DIAGONAL') + newPathfinder:setHeuristic('MANHATTAN') + newPathfinder:setTunnelling(false) + return newPathfinder +end + +--- Evaluates [clearance](http://aigamedev.com/open/tutorial/clearance-based-pathfinding/#TheTrueClearanceMetric) +-- for the whole `grid`. It should be called only once, unless the collision map or the +-- __walkable__ attribute changes. The clearance values are calculated and cached within the grid nodes. +-- @class function +-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) +-- @usage myFinder:annotateGrid() +function Pathfinder:annotateGrid() + --assert(self._walkable, 'Finder must implement a walkable value') + for x=self._grid._max_x,self._grid._min_x,-1 do + for y=self._grid._max_y,self._grid._min_y,-1 do + local node = self._grid:getNodeAt(x,y) + if self._grid:isWalkableAt(x,y,self._walkable) then + local nr = self._grid:getNodeAt(node._x+1, node._y) + local nrd = self._grid:getNodeAt(node._x+1, node._y+1) + local nd = self._grid:getNodeAt(node._x, node._y+1) + if nr and nrd and nd then + local m = nrd._clearance[self._walkable] or 0 + m = (nd._clearance[self._walkable] or 0)0 +-- end +function Pathfinder:setWalkable(walkable) + --assert(Assert.matchType(walkable,'stringintfunctionnil'), + -- ('Wrong argument #1. Expected \'string\', \'number\' or \'function\', got %s.'):format(type(walkable))) + self._walkable = walkable + self._grid._eval = type(self._walkable) == 'function' + return self +end + +--- Gets the __walkable__ value or function. +-- @class function +-- @treturn string|int|func the `walkable` value or function +-- @usage local walkable = myFinder:getWalkable() +function Pathfinder:getWalkable() + return self._walkable +end + +--- Defines the `finder`. It refers to the search algorithm used by the `pathfinder`. +-- Default finder is `ASTAR`. Use @{Pathfinder:getFinders} to get the list of available finders. +-- @class function +-- @tparam string finderName the name of the `finder` to be used for further searches. +-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) +-- @usage +-- --To use Breadth-First-Search +-- myFinder:setFinder('BFS') +-- @see Pathfinder:getFinders +function Pathfinder:setFinder(finderName) + if not finderName then + if not self._finder then + finderName = 'ASTAR' + else return + end + end + --assert(Finders[finderName],'Not a valid finder name!') + self._finder = finderName + return self +end + +--- Returns the name of the `finder` being used. +-- @class function +-- @treturn string the name of the `finder` to be used for further searches. +-- @usage local finderName = myFinder:getFinder() +function Pathfinder:getFinder() + return self._finder +end + +--- Returns the list of all available finders names. +-- @class function +-- @treturn {string,...} array of built-in finders names. +-- @usage +-- local finders = myFinder:getFinders() +-- for i, finderName in ipairs(finders) do +-- print(i, finderName) +-- end +function Pathfinder:getFinders() + return Utils.getKeys(Finders) +end + +--- Sets a heuristic. This is a function internally used by the `pathfinder` to find the optimal path during a search. +-- Use @{Pathfinder:getHeuristics} to get the list of all available `heuristics`. One can also define +-- his own `heuristic` function. +-- @class function +-- @tparam func|string heuristic `heuristic` function, prototyped as __f(dx,dy)__ or as a `string`. +-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) +-- @see Pathfinder:getHeuristics +-- @see core.heuristics +-- @usage myFinder:setHeuristic('MANHATTAN') +function Pathfinder:setHeuristic(heuristic) + --assert(Heuristic[heuristic] or (type(heuristic) == 'function'),'Not a valid heuristic!') + self._heuristic = Heuristics[heuristic] or heuristic + return self +end + +--- Returns the `heuristic` used. Returns the function itself. +-- @class function +-- @treturn func the `heuristic` function being used by the `pathfinder` +-- @see core.heuristics +-- @usage local h = myFinder:getHeuristic() +function Pathfinder:getHeuristic() + return self._heuristic +end + +--- Gets the list of all available `heuristics`. +-- @class function +-- @treturn {string,...} array of heuristic names. +-- @see core.heuristics +-- @usage +-- local heur = myFinder:getHeuristic() +-- for i, heuristicName in ipairs(heur) do +-- ... +-- end +function Pathfinder:getHeuristics() + return Utils.getKeys(Heuristic) +end + +--- Defines the search `mode`. +-- The default search mode is the `DIAGONAL` mode, which implies 8-possible directions when moving (north, south, east, west and diagonals). +-- In `ORTHOGONAL` mode, only 4-directions are allowed (north, south, east and west). +-- Use @{Pathfinder:getModes} to get the list of all available search modes. +-- @class function +-- @tparam string mode the new search `mode`. +-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) +-- @see Pathfinder:getModes +-- @see Modes +-- @usage myFinder:setMode('ORTHOGONAL') +function Pathfinder:setMode(mode) + --assert(searchModes[mode],'Invalid mode') + self._allowDiagonal = (mode == 'DIAGONAL') + return self +end + +--- Returns the search mode. +-- @class function +-- @treturn string the current search mode +-- @see Modes +-- @usage local mode = myFinder:getMode() +function Pathfinder:getMode() + return (self._allowDiagonal and 'DIAGONAL' or 'ORTHOGONAL') +end + +--- Gets the list of all available search modes. +-- @class function +-- @treturn {string,...} array of search modes. +-- @see Modes +-- @usage local modes = myFinder:getModes() +-- for modeName in ipairs(modes) do +-- ... +-- end +function Pathfinder:getModes() + return Utils.getKeys(searchModes) +end + +--- Enables tunnelling. Defines the ability for the `pathfinder` to tunnel through walls when heading diagonally. +-- This feature __is not compatible__ with Jump Point Search algorithm (i.e. enabling it will not affect Jump Point Search) +-- @class function +-- @tparam bool bool a boolean +-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) +-- @usage myFinder:setTunnelling(true) +function Pathfinder:setTunnelling(bool) + --assert(Assert.isBool(bool), ('Wrong argument #1. Expected boolean, got %s'):format(type(bool))) + self._tunnel = bool + return self +end + +--- Returns tunnelling feature state. +-- @class function +-- @treturn bool tunnelling feature actual state +-- @usage local isTunnellingEnabled = myFinder:getTunnelling() +function Pathfinder:getTunnelling() + return self._tunnel +end + +--- Calculates a `path`. Returns the `path` from location __[startX, startY]__ to location __[endX, endY]__. +-- Both locations must exist on the collision map. The starting location can be unwalkable. +-- @class function +-- @tparam int startX the x-coordinate for the starting location +-- @tparam int startY the y-coordinate for the starting location +-- @tparam int endX the x-coordinate for the goal location +-- @tparam int endY the y-coordinate for the goal location +-- @tparam int clearance the amount of clearance (i.e the pathing agent size) to consider +-- @treturn path a path (array of nodes) when found, otherwise nil +-- @usage local path = myFinder:getPath(1,1,5,5) +function Pathfinder:getPath(startX, startY, endX, endY, clearance) + self:reset() + local startNode = self._grid:getNodeAt(startX, startY) + local endNode = self._grid:getNodeAt(endX, endY) + --assert(startNode, ('Invalid location [%d, %d]'):format(startX, startY)) + --assert(endNode and self._grid:isWalkableAt(endX, endY), + -- ('Invalid or unreachable location [%d, %d]'):format(endX, endY)) + local _endNode = Finders[self._finder](self, startNode, endNode, clearance, toClear) + if _endNode then + return Utils.traceBackPath(self, _endNode, startNode) + end + return nil +end + +--- Resets the `pathfinder`. This function is called internally between successive pathfinding calls, so you should not +-- use it explicitely, unless under specific circumstances. +-- @class function +-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) +-- @usage local path, len = myFinder:getPath(1,1,5,5) +function Pathfinder:reset() + for node in pairs(toClear) do node:reset() end + toClear = {} + return self +end + + +-- Returns Pathfinder class +Pathfinder._VERSION = _VERSION +Pathfinder._RELEASEDATE = _RELEASEDATE \ No newline at end of file diff --git a/actions/places.lua b/actions/places.lua index eaae95c..d7a121c 100644 --- a/actions/places.lua +++ b/actions/places.lua @@ -146,6 +146,7 @@ function npc.places.openable_node_is_entrance(pos) local node = minetest.get_node(pos) local x_adj = 0 local z_adj = 0 + minetest.log(node.param2) if node.param2 then if node.param2 == 0 then @@ -157,7 +158,8 @@ function npc.places.openable_node_is_entrance(pos) end end - --local first_check_pos = {x=, y=, z=} + local y_adj = 2 + local first_check_pos = {x=pos.x + x_adj, y=pos.y + y_adj, z=pos.z + z_adj} end diff --git a/init.lua b/init.lua index eb1ad9f..3b44a38 100755 --- a/init.lua +++ b/init.lua @@ -1,6 +1,30 @@ -- Advanced NPC mod by Zorman2000 local path = minetest.get_modpath("advanced_npc") +-- Below code for require is taken and slightly modified +-- from irc mod by Diego Martinez (kaeza) +-- https://github.com/minetest-mods/irc +-- Handle mod security if needed +-- local ie = minetest.request_insecure_environment() +-- -- local req_ie = minetest.request_insecure_environment() +-- -- if req_ie then +-- -- ie = req_ie +-- -- end +-- if not ie then +-- error("The Advanced NPC mod requires access to insecure functions in ".. +-- "order to work. Please add the Advanced NPC mod to the ".. +-- "secure.trusted_mods setting or disable the mod.") +-- end + +-- -- Modify package path so that it can find the Jumper library files +-- ie.package.path = +-- path .. "/Jumper/?.lua;".. +-- ie.package.path + +-- -- Require the main files from Jumper +-- Grid = ie.require("jumper.grid") +-- Pathfinder = ie.require("jumper.pathfinder") + -- Intllib local S if minetest.get_modpath("intllib") then @@ -33,6 +57,8 @@ dofile(path .. "/trade/prices.lua") dofile(path .. "/actions/actions.lua") dofile(path .. "/actions/places.lua") dofile(path .. "/actions/pathfinder.lua") +dofile(path .. "/actions/pathfinding.lua") +--dofile(path .. "/actions/pathfinder2.lua") dofile(path .. "/actions/node_registry.lua") dofile(path .. "/random_data.lua") diff --git a/npc.lua b/npc.lua index bf5afe4..e80a728 100755 --- a/npc.lua +++ b/npc.lua @@ -269,9 +269,11 @@ function npc.initialize(entity, pos, is_lua_entity) -- Temporary initialization of actions for testing local nodes = npc.places.find_node_nearby(ent.object:getpos(), {"cottages:bench"}, 20) - --minetest.log("Found nodes: "..dump(nodes)) - --local path = pathfinder.find_path(ent.object:getpos(), nodes[1], 20) + + minetest.log("Self destination: "..minetest.pos_to_string(nodes[1])) + + local path = pathfinder.find_path(ent.object:getpos(), nodes[1], 20, {}) --minetest.log("Path to node: "..dump(path)) --npc.add_action(ent, npc.actions.use_door, {self = ent, pos = nodes[1], action = npc.actions.door_action.OPEN}) --npc.add_action(ent, npc.actions.stand, {self = ent}) @@ -861,6 +863,8 @@ mobs:register_mob("advanced_npc:npc", { minetest.log("[advanced_npc] WARNING: Initializing NPC from entity step. This message should only be appearing if an NPC is being spawned from inventory with egg!") npc.initialize(self, self.object:getpos(), true) else + self.tamed = false + self.owner = nil -- NPC is initialized, check other variables -- Timer function for casual traders to reset their trade offers self.trader_data.change_offers_timer = self.trader_data.change_offers_timer + dtime @@ -971,6 +975,81 @@ mobs:register_mob("advanced_npc:npc", { end end end + + -- if self.follow_path then + -- self.home_timer = (self.home_timer or 0) + dtime + -- if self.home_timer < 1 then return end -- every 1 second + -- self.home_timer = 0 + + -- -- if self.time_of_day > 0.2 and self.time_of_day < 0.8 then + -- -- return -- return if not night time + -- -- end + + -- local h = self.destination + -- --local h = {x = 1, y = 8, z = 2} -- destination coords + -- local p = self.object:getpos() -- mob position + + -- -- lets try find a path, first take care of positions + -- -- since pathfinder is very sensitive + -- local pheight = self.collisionbox[5] - self.collisionbox[2] + + -- -- round position to center of node to avoid stuck in walls + -- -- also adjust height for player models! + -- p.x = math.floor(p.x + 0.5) + -- p.y = math.floor(p.y + 0.5) - pheight + -- p.z = math.floor(p.z + 0.5) + + -- local ssight, sground = minetest.line_of_sight(p, { + -- x = p.x, y = p.y - 4, z = p.z}, 1) + + -- -- determine node above ground + -- if not ssight then + -- p.y = sground.y + 1 + -- end + + -- h.x = math.floor(h.x + 0.5) + -- h.y = math.floor(h.y + 0.5) + -- h.z = math.floor(h.z + 0.5) + + + -- local x, y, z = p.x - h.x, p.y - h.y, p.z - h.z + -- local dist = math.floor(math.sqrt(x * x + y * y + z * z)) + + -- minetest.log("Self pos : "..minetest.pos_to_string(p)) + -- minetest.log("Self dest: "..minetest.pos_to_string(h)) + + -- if dist <= 1 then + -- print ("--- home!") + -- self.homepath = nil + -- self.state = "stand" + -- return + -- end + + -- if self.homepath == nil then + -- self.homepath = minetest.find_path(p, h, 50, 3, 6, "A*") + -- print ("--- finding route", self.homepath, dist) + -- end + + -- if self.homepath then + -- print ("--- following path", dist, #self.homepath) + + -- local np = self.homepath[1] ; if not np then return end + + -- if math.abs(np.x - p.x) + math.abs(np.z - p.z) < 0.6 then + -- table.remove(self.homepath, 1) ; print ("-- removed entry") + -- end + + -- np = {x = np.x, y = np.y, z = np.z} + + -- local vec = {x = np.x - p.x, z = np.z - p.z} + -- local yaw = (math.atan(vec.z / vec.x) + math.pi / 2) - self.rotate + + -- if np.x > p.x then yaw = yaw + math.pi end + + -- self.object:setyaw(yaw) + -- set_velocity(self, self.walk_velocity) + -- end + -- end return self.freeze end diff --git a/random_data.lua b/random_data.lua index c31fc36..ceb3564 100644 --- a/random_data.lua +++ b/random_data.lua @@ -327,12 +327,55 @@ npc.data.FIRST_NAMES = { female = { "Kimy", "Lili", - "Cora", - "Caroline", + "Coraline", "Gloria", "Mary", "Mayra", - "Arlene" + "Arlene", + "Tita", + "Lola", + "Olivia", + "Katherine", + "Cataline", + "Pinky", + "Kathleen", + "Marilyn", + "Sunshine", + "April", + "Rainy", + "Lulu", + "Sandra", + "Marlene", + "Lany", + "Zoe", + "Jolie", + "Vicky", + "Natalia", + "Evelyn", + "Elizabeth", + "Giselle", + "Jasmine", + "Karla", + "Leslie", + "Karen", + "Dana", + "Merry", + "Helena", + "Rose", + "Thalia", + "Luna", + "Valery", + "Carol", + "Paulette", + "Rosie", + "Leti", + "Sophie", + "Miranda", + "Arianne", + "Lizzy", + "Amy", + "Chole", + "Alisson" }, male = { "Jote", @@ -341,7 +384,9 @@ npc.data.FIRST_NAMES = { "Joseph", "Gerald", "Kiko", - "Michael" + "Michael", + "Alexis", + "Rafa" } }