diff --git a/README.md b/README.md index 004860f..59f2e6c 100644 --- a/README.md +++ b/README.md @@ -4,25 +4,40 @@ advanced_npc Introduction ------------ -Advanced NPC is a mod for Minetest, based on mobs_redo. -The goal of this mod is to be able to have live villages in Minetest. These NPCs are highly inspired by the typical NPCs of Harvest Moon games. The general idea is that on almost all buildings of a village there are NPCs that are kind of intelligent: they have daily tasks they perform, can speak to players, can trade with the player, can use their own items (chests and furnaces for example), know where to go around their house and village, can be lumbers, miners or any other Minetest-suitable profession and can ultimately engage into relationships with the player. And while basically only players are mentioned here, the ultimate goal is that they can do all of this also among themselves, so that villages are alive and evolving by themselves, without player intervention. +Advanced NPC is a mod for Minetest using mobs_redo API. +The goal of this mod is to be able to have live villages in Minetest. These NPCs are highly inspired by the typical NPCs of _Harvest Moon_ games. The general idea is that on almost all buildings of a village there are NPCs that are kind of intelligent: they have daily tasks they perform, can speak to players, can trade with the player, can use their own items (chests and furnaces for example), know where to go around their house and village, can be lumbers, miners or any other Minetest-suitable profession and can ultimately engage into relationships with the player. And while basically only players are mentioned here, the ultimate goal is that they can do all of this also among themselves, so that villages are alive and evolving by themselves, without player intervention. Installation ------------ -__NOTE__: Advanced NPC is still under development. While the mod is largely stable, it lacks one of the most important pieces: spawning. Currently, NPCs will spawn on stone (default:stone) and the mg_villages' plotmarkers (mg_villages:plotmarker). The spawning is not controlled, so you will have several of them walking around. This is not how it is planned and is just for testing purposes. In the future, only a handful of NPCs should spawn at village house's plotmarker and they will know their way around the house and have specific jobs. +__NOTE__: Advanced NPC is still under development. While the mod is largely stable, it lacks one of the most important pieces: spawning. Currently, NPCs can be spawned using eggs (found in creative inventory as 'NPC') and by themselves on villages of the [mg_villages mod](https://forum.minetest.net/viewtopic.php?t=13589). NPCs will spawn automatically on mg_villages villages and over time will populate the entire village. If something goes wrong, you can reset the village by: + - Clearing all objects (in chat, type /clearobjects quick) + - Restore original plotmarkers (in chat, type /restore_plotmarkers radius) + - The radius can be any number, but it is recommended you use a not so large number. 200 is suitable. So stand in the middle of the village and then run that command. +This will actually restore the village and will slowly make NPCs spawn again. Currently there's no way to disable NPCs spawning on village, except by going to `spawner.lua` and commenting out all of `minetest.register_abm()` code. -Download the mod [here](https://github.com/hkzorman/advanced_npc/archive/master.zip) (link always pointing to latest version) +__Download__ the mod [here](https://github.com/hkzorman/advanced_npc/archive/master.zip) (link always pointing to latest version) For this mod to work correctly, you also need to install the [mobs_redo](https://github.com/tenplus1/mobs_redo) mod. After installation, make sure you enable it in your world. + License ------- __advanced_npc__ is Copyright (C) 2016-2017 Hector Franqui (zorman2000), licensed under the GPLv3 license. See `license.txt` for details. -The `jumper.lua` file contains code based on the [Jumper library](https://github.com/Yonaba/Jumper), which is Copyright (c) 2012-2013 Roland Yonaba, licensed under MIT license. See `actions/jumper.lua` for details. +The `pathfinder.lua` file contains code slighlty modified from the [pathfinder mod](https://github.com/Yonaba/Jumper) by MarkBu, which is licensed as WTFPL. See `actions/pathfinder.lua` for details. + +Current NPC textures are from mobs_redo mod. +The following textures are by Zorman2000: +- marriage_ring.png - CC BY-SA + + +Documentation and API +--------------------- + +This mod requires a good user manual, and also is planned to have an extensive API, properly documented. Unfortunately, these still aren't ready. A very very very WIP manual can be found in the [wiki](https://github.com/hkzorman/advanced_npc/wiki/Concept%3A-Dialogues) Roadmap diff --git a/actions/actions.lua b/actions/actions.lua index c88eda2..27a1d3f 100644 --- a/actions/actions.lua +++ b/actions/actions.lua @@ -599,7 +599,7 @@ function npc.actions.use_bed(self, args) end local action = args.action local node = minetest.get_node(pos) - minetest.log(dump(node)) + --minetest.log(dump(node)) local dir = minetest.facedir_to_dir(node.param2) if action == npc.actions.const.beds.LAY then @@ -746,20 +746,9 @@ function npc.actions.walk_to_pos(self, args) end -- Find path - --local path = pathfinder.find_path(start_pos, end_pos, 20, walkable_nodes) - local path = pathfinder.find_path(start_pos, end_pos, self) + local path = npc.pathfinder.find_path(start_pos, end_pos, self, true) if path ~= nil and #path > 1 then - -- Get details from path nodes - -- This might get moved to proper place, pathfinder.lua code - local path_detail = {} - for i = 1, #path do - local node = minetest.get_node(path[i]) - table.insert(path_detail, {pos=path[i], type=npc.pathfinder.is_good_node(node, {})}) - end - path = path_detail - - npc.log("DEBUG", "Detailed path: "..dump(path)) npc.log("INFO", "walk_to_pos Found path to node: "..minetest.pos_to_string(end_pos)) -- Store path self.actions.walking.path = path diff --git a/actions/jumper.lua b/actions/jumper.lua deleted file mode 100644 index 62f65a7..0000000 --- a/actions/jumper.lua +++ /dev/null @@ -1,1349 +0,0 @@ ---------------------------------------------------------------------------------------- --- This code is entirely based on Jumper library 1.8.1 by Roland Yonaba. --- The modifications are only to make it work under Minetest's secure --- environment. Therefore, the code in this file is under the MIT license --- as the original Jumper library (please see copyright notice below). --- The original library code can be found here: --- https://github.com/Yonaba/Jumper/releases/tag/jumper-1.8.1-1 - --- Modifications are by Hector Franqui (Zorman2000) - ---------------------------------------------------------------------------------------- --- Copyright (c) 2012-2013 Roland Yonaba - --- Permission is hereby granted, free of charge, to any person obtaining a --- copy of this software and associated documentation files (the --- "Software"), to deal in the Software without restriction, including --- without limitation the rights to use, copy, modify, merge, publish, --- distribute, sublicense, and/or sell copies of the Software, and to --- permit persons to whom the Software is furnished to do so, subject to --- the following conditions: - --- The above copyright notice and this permission notice shall be included --- in all copies or substantial portions of the Software. - --- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS --- OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF --- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. --- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY --- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, --- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE --- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- ---------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------- - --- Local variables declarations -local abs = math.abs -local sqrt = math.sqrt -local max = math.max -local floor = math.floor -local t_insert, t_remove = table.insert, table.remove -local huge = math.huge - - ---------------------------------------------------------------------------------------- --- Heuristics based on implementation by Ronald Yonaba --- Original code here: https://github.com/Yonaba/Jumper/jumper/core/heuristics.lua ---------------------------------------------------------------------------------------- -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 -} - - ---------------------------------------------------------------------------------------- --- Node class implementation by Ronald Yonaba --- Original code here: https://github.com/Yonaba/Jumper/jumper/core/node.lua ---------------------------------------------------------------------------------------- -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: https://github.com/Yonaba/Jumper/jumper/core/path.lua ---------------------------------------------------------------------------------------- ---- 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 - - ---------------------------------------------------------------------------------------- --- Utils class based on implementation by Ronald Yonaba --- Original code here: https://github.com/Yonaba/Jumper/jumper/core/utils.lua ---------------------------------------------------------------------------------------- -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 -} - - ---------------------------------------------------------------------------------------- --- Bheap class implementation by Ronald Yonaba --- Original code here: https://github.com/Yonaba/Jumper/jumper/core/bheap.lua ---------------------------------------------------------------------------------------- --- 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 class implementation by Ronald Yonaba --- Original code here: https://github.com/Yonaba/Jumper/jumper/grid.lua ---------------------------------------------------------------------------------------- -local pairs = pairs -local assert = assert -local next = next -local setmetatable = setmetatable -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 based on implementation by Ronald Yonaba --- Original code here: https://github.com/Yonaba/Jumper/jumper/search/astar.lua ---------------------------------------------------------------------------------------- --- Internalization -local ipairs = ipairs - --- 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 - - ---------------------------------------------------------------------------------------- --- Pathfinder class based on implementation by Ronald Yonaba --- Original code here: https://github.com/Yonaba/Jumper/jumper/pathfinder.lua ---------------------------------------------------------------------------------------- -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 diff --git a/actions/pathfinder.lua b/actions/pathfinder.lua index a31e6f4..eb10792 100644 --- a/actions/pathfinder.lua +++ b/actions/pathfinder.lua @@ -1,14 +1,13 @@ --- Pathfinding code by Zorman2000 +-- Pathfinding code by MarkBu, original can be found here: +-- https://github.com/MarkuBu/pathfinder +-- +-- Modifications by Zorman2000 +-- This version is slightly modified to use another "walkable" function, +-- plus add a "decorating" path function which allows to know the type +-- of nodes in the path. --------------------------------------------------------------------------------------- -- Pathfinding functionality --------------------------------------------------------------------------------------- --- This class contains functions that allows to map the 3D map of Minetest into --- a 2D array (basically by ignoring the y coordinate for the moment being) in order --- to use the A* pathfinding algorithm to find the shortest path from one node to --- another. The A* algorithm implementation is in the 'jumper.lua' file which a --- reduced and slightly modified version of the Jumper library, by Roland Yonaba --- (https://github.com/Yonaba/Jumper). --- Mapping algorithm: transforms a Minetest map surface to a 2d grid. npc.pathfinder = {} @@ -30,57 +29,15 @@ npc.pathfinder.nodes = { } } - --- This function uses the mapping functions and the A* algorithm implementation --- of the Jumper library to find a path from start_pos to end_pos. The range is --- an extra amount of nodes to search in both the x and z coordinates. -function npc.pathfinder.find_path(start_pos, end_pos, range, walkable_nodes) - -- Check that start and end position are not the same - if start_pos.x == end_pos.x and start_pos.z == end_pos.z then - return nil - end - -- Set walkable nodes to empty if parameter wasn't used - if walkable_nodes == nil then - walkable_nodes = {} - end - -- Map the Minetest area to a 2D array - local map = pathfinder.create_map(start_pos, end_pos, range, walkable_nodes) - -- Find start and end positions - local pos = pathfinder.find_start_and_end_pos(map) - -- Normalize the map - local normalized_map = pathfinder.normalize_map(map) - -- Create pathfinder object - local grid_object = Grid(normalized_map) - -- Define what is a walkable node - local walkable = 0 - - -- Pathfinder object using A* algorithm - local finder = Pathfinder(grid_object, "ASTAR", walkable) - -- Set orthogonal mode meaning it will not move in diagonal directions - finder:setMode("ORTHOGONAL") - - -- Calculates the path, and its length - local path = finder:getPath(pos.start_pos.x, pos.start_pos.z, pos.end_pos.x, pos.end_pos.z) - - --minetest.log("Found path: "..dump(path)) - -- Pretty-printing the results - if path then - return pathfinder.get_path(map, path:nodes()) - end -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 npc.pathfinder.is_good_node(node, exceptions) +function pathfinder.is_good_node(node, exceptions) --local function 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. - --minetest.log("Is good node: "..dump(node)) local is_openable = false for _,node_prefix in pairs(npc.pathfinder.nodes.openable_prefix) do - --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 @@ -88,159 +45,346 @@ function npc.pathfinder.is_good_node(node, exceptions) end end if node ~= nil and node.name ~= nil and not minetest.registered_nodes[node.name].walkable then - --return pathfinder.node_types.walkable return npc.pathfinder.node_types.walkable elseif is_openable then return npc.pathfinder.node_types.openable - --return pathfinder.node_types.openable else for i = 1, #exceptions do if node.name == exceptions[i] then return npc.pathfinder.node_types.walkable - --return pathfinder.node_types.walkable end end return npc.pathfinder.node_types.non_walkable - --return pathfinder.node_types.non_walkable end end -function pathfinder.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)} - - 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)} - -- 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(current_row, {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(current_row, {pos=current_pos, type=pathfinder.node_types.goal}) - else - -- Check if node is walkable - local node = minetest.get_node(current_pos) - -- Check node has air above it - local node_above = minetest.get_node({x=current_pos.x, y=current_pos.y+1, z=current_pos.z}) - if node.name == "air" then - -- Check if node above is air - if node.name == "air" then - -- If air do no more checks - table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.walkable}) - end - else - -- Check if it is of a walkable or openable type - table.insert(current_row, {pos=current_pos, type=is_good_node(node, walkables)}) - end - end +function pathfinder.get_decorated_path(path) + -- Get details from path nodes + local path_detail = {} + for i = 1, #path do + local node = minetest.get_node(path[i]) + table.insert(path_detail, {pos=path[i], type=pathfinder.is_good_node(node, {})}) end - -- Insert the converted row into the grid - table.insert(grid, current_row) + + npc.log("DEBUG", "Detailed path: "..dump(path_detail)) + return path_detail +end + +function npc.pathfinder.find_path(start_pos, end_pos, entity, decorate_path) + local path = pathfinder.find_path(start_pos, end_pos, entity) + if path then + if decorate_path then + path = pathfinder.get_decorated_path(path) + end + else + npc.log("ERROR", "Couldn't find path from "..minetest.pos_to_string(start_pos) + .." to "..minetest.pos_to_string(end_pos)) end - - return grid + return path end --- Utility function to print the created map to the console. --- Used for debug. -local function print_map(map) - for z,row in pairs(map) do - local row_string = "[" - for x,node in pairs(row) do - if node.type == 2 then - row_string = row_string.."- " - else - row_string = row_string..node.type.." " - end - -- Use the following if the coordinates are also needed - --row_string = row_string..node.type..": {"..node.pos.x..", "..node.pos.y..", "..node.pos.z.."}, " - end - row_string = row_string.."]" - print(row_string) - end +-- From this point onwards is MarkBu's original pathfinder code, +-- except for the "walkable" function, which is modified by Zorman2000 +-- to include doors and other "walkable" nodes. +-- The version here is exactly this: +-- https://github.com/MarkuBu/pathfinder/commit/ca0b433bf5efde5da545b11b2691fa7f7e53dc30 + +--[[ +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} +]]-- + +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) --- This function find the starting and ending points in the --- map representation, and returns the coordinates in the map --- for the pathfinding algorithm to use -function pathfinder.find_start_and_end_pos(map) - -- This is for debug - --print_map(map) - local result = {} - for z,row in pairs(map) do - for x,node in pairs(row) do - if node.type == pathfinder.node_types.start then - --minetest.log("Start node: "..dump(node)) - result["start_pos"] = {x=x, z=z} - elseif node.type == pathfinder.node_types.goal then - --minetest.log("End node: "..dump(node)) - result["end_pos"] = {x=x, z=z} - end + if distX > distZ then + return (14 * distZ + 10 * (distX - distZ)) * (distY + 1) + else + return (14 * distX + 10 * (distZ - distX)) * (distY + 1) + end +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 +local function walkable(node, exceptions) + local exceptions = exceptions or {} + -- Is openable is to support doors, fence gates and other + -- doors from other mods. Currently, default doors, gates + -- and cottages doors are supported. + --minetest.log("Is good node: "..dump(node)) + local is_openable = false + for _,node_prefix in pairs(npc.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 - --minetest.log("Found start and end positions: ("..result.start_pos.)..", "..minetest.pos_to_string(result.end_pos)) - return result -end - --- This function transforms the grid into binary values --- (0 walkable, 1 non-walkable) for the pathfinding algorithm. -function pathfinder.normalize_map(map) - local result = {} - for _,row in pairs(map) do - local result_row = {} - for _,node in pairs(row) do - if node.type ~= pathfinder.node_types.non_walkable then - table.insert(result_row, 0) - else - table.insert(result_row, 1) + -- Detect mg_villages ceilings usage of thin wood nodeboxes + -- TODO: Improve + local is_mg_villages_ceiling = false + if node.name == "cottages:wood_flat" then + is_mg_villages_ceiling = true + end + if node ~= nil and node.name ~= nil and not minetest.registered_nodes[node.name].walkable then + return false + elseif is_openable then + return false + elseif is_mg_villages_ceiling then + return false + else + for i = 1, #exceptions do + if node.name == exceptions[i] then + return false end end - table.insert(result, result_row) + return true end - return result end --- 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) - local result = {} - for node, count in path_nodes do - table.insert(result, map[node:getY()][node:getX()]) - -- For debug - --minetest.log("Node: "..dump(map[node:getY()][node:getX()])) - --print(('Step: %d - x: %d - y: %d'):format(count, node:getX(), node:getY())) - end - return result +local function check_clearance(cpos, x, z, height) + for i = 1, height do + local n_name = minetest.get_node({x = cpos.x + x, y = cpos.y + i, z = cpos.z + z}).name + local c_name = minetest.get_node({x = cpos.x, y = cpos.y + i, z = cpos.z}).name + --~ print(i, n_name, c_name) + if walkable(n_name) or walkable(c_name) then + return false + end + end + return true +end + +local function get_neighbor_ground_level(pos, jump_height, fall_height) + local node = minetest.get_node(pos) + local height = 0 + if walkable(node) 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) + 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) + return {x = pos.x, y = pos.y + 1, z = pos.z} + end +end + +function pathfinder.find_path(pos, endpos, entity) + 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 > 100 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 = minetest.get_node(neighbor_pos) + local neighbor_ground_level = get_neighbor_ground_level(neighbor_pos, entity_jump_height, entity_fear_height) + 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 = minetest.get_node( + {x = current_pos.x, y = current_pos.y + entity_height, z = current_pos.z}) + if neighbor_ground_level.y - current_pos.y > 0 and not walkable(node_above_head) then + local height = -1 + repeat + height = height + 1 + local node = minetest.get_node( + {x = neighbor_ground_level.x, + y = neighbor_ground_level.y + height, + z = neighbor_ground_level.z}) + until walkable(node) 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) then + neighbors[neighbors_index] = { + hash = nil, + pos = nil, + clear = nil, + walkable = nil, + } + else + local height = -1 + repeat + height = height + 1 + local node = minetest.get_node( + {x = neighbor_ground_level.x, + y = current_pos.y + height, + z = neighbor_ground_level.z}) + until walkable(node) 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), + } + 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 > 100 then + -- print("fail") + return + end + until count < 1 + -- print("count < 1") + return {pos} end diff --git a/actions/places.lua b/actions/places.lua index bc198ed..0954533 100644 --- a/actions/places.lua +++ b/actions/places.lua @@ -196,7 +196,7 @@ function npc.places.find_entrance_from_openable_nodes(all_openable_nodes, marker entity.collisionbox = {-0.20,-1.0,-0.20, 0.20,0.8,0.20} --minetest.log("Start pos: "..minetest.pos_to_string(start_pos)) --minetest.log("End pos: "..minetest.pos_to_string(end_pos)) - local path = pathfinder.find_path(start_pos, end_pos, entity) + local path = npc.pathfinder.find_path(start_pos, end_pos, entity, false) --minetest.log("Found path: "..dump(path)) if path ~= nil then --minetest.log("Path distance: "..dump(#path)) diff --git a/init.lua b/init.lua index 0c007f5..6718988 100755 --- a/init.lua +++ b/init.lua @@ -32,7 +32,6 @@ dofile(path .. "/trade/prices.lua") dofile(path .. "/actions/actions.lua") dofile(path .. "/actions/places.lua") dofile(path .. "/actions/pathfinder.lua") -dofile(path .. "/actions/jumper.lua") dofile(path .. "/actions/node_registry.lua") dofile(path .. "/random_data.lua") diff --git a/textures/diamond_ring.png b/textures/diamond_ring.png deleted file mode 100644 index 11c56a8..0000000 Binary files a/textures/diamond_ring.png and /dev/null differ diff --git a/textures/mobs_blood.png b/textures/mobs_blood.png deleted file mode 100644 index 77cfbda..0000000 Binary files a/textures/mobs_blood.png and /dev/null differ diff --git a/textures/mobs_trader.png b/textures/mobs_trader.png deleted file mode 100755 index 4c469c9..0000000 Binary files a/textures/mobs_trader.png and /dev/null differ diff --git a/textures/mobs_trader2.png b/textures/mobs_trader2.png deleted file mode 100755 index cbd9b93..0000000 Binary files a/textures/mobs_trader2.png and /dev/null differ diff --git a/textures/mobs_trader3.png b/textures/mobs_trader3.png deleted file mode 100755 index e6b239a..0000000 Binary files a/textures/mobs_trader3.png and /dev/null differ diff --git a/textures/smoke.png b/textures/smoke.png deleted file mode 100644 index 488b50f..0000000 Binary files a/textures/smoke.png and /dev/null differ