Adds pathfinding library Jumper by Ronald Yonaba. This includes an implementation of the A* pathfinding algorithm which makes NPC now always get to their goal node.

Pathfinding: Adds functions that allows to map the Minetest 3D map to a 2D array to use by the pathfinding algorithm.
Actions: Use new code for find_path function. Improves door opening while walking on paths, and also now close them. Cottages fence gates and doors are also now supported in addition to the default doors and gates.
Changes to the Readme and the License.
This commit is contained in:
zorman2000 2017-01-06 07:57:42 -05:00
parent 80b7eb6ec9
commit 554fde4643
13 changed files with 412615 additions and 313 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "Jumper"]
path = Jumper
url = https://github.com/Yonaba/Jumper.git

1
Jumper Submodule

@ -0,0 +1 @@
Subproject commit df4eb22f2b5a95c003bad9544ed560b2e51654f5

112
README.md
View File

@ -1,78 +1,84 @@
# advanced_npc advanced_npc
============
Introduction
------------
Advanced NPC framework for Minetest, based on mobs_redo. Advanced NPC framework 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 for example), know where to go around their 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 completely alive and evolving by themselves, without necessary player intervention. 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 for example), know where to go around their 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 completely alive and evolving by themselves, without necessary player intervention.
----------
Current roadmap: License
-------
Version 1.0 __advanced_npc__ is Copyright (C) 2016-2017 Hector Franqui (zorman2000), licensed under the GPLv3 license. See 'LICENSE.txt' for details.
-----------
Phase 1: Gifts and relationships: In progress The Jumper library is Copyright (c) 2012-2013 Roland Yonaba, licensed under MIT license. See 'Jumper/LICENSE.txt' for details.
- NPCs should be able to receive items
- NPCs will have favorite and disliked items
- Giving an NPC their favorite or disliked item will affect positively/negatively their Current progress and roadmap
----------------------------
__Version 1.0__
__Phase 1__: Gifts and relationships: In progress
- [x] NPCs should be able to receive items
- [x] NPCs will have favorite and disliked items
- [x] Giving an NPC their favorite or disliked item will affect positively/negatively their
relationship with that player. relationship with that player.
- Eventually, an NPC can fall in love with that player and marry him/her - [x] Eventually, an NPC can fall in love with that player and marry him/her
- Relationships among NPCs should be possible too - [ ] Relationships among NPCs should be possible too
Phase 2: Dialogues: Completed __Phase 2__: Dialogues: In progress
- NPCs should be able to perform complex dialogues: - [ ] NPCs should be able to perform complex dialogues:
- Use yes/no or multiple option dialogue boxes to interact with player - [x] Use yes/no or multiple option dialogue boxes to interact with player
- Answers and responses by player - [x] Answers and responses by player
TODO: Specific dialogues on certain environment flag (so that events can change what an NPC says - [ ] Specific dialogues on certain flags (so that events can change what an NPC says)
Phase 3: Trading __Phase 3__: Trading: In progress
- NPCs should be able to trade, either buy or sell items to/from player and other NPCs - [ ] NPCs should be able to trade, either buy or sell items to/from player and other NPCs
- Goal is to implement trading with player first - There are two types of traders: casual and dedicated.
- [x] Casual traders are normal NPC which occasionaly make buy or sell offers to the player
- [ ] Dedicated traders are traders that, when talked to, always make buy and sell offers. They have a greater variety too.
- [ ] NPCs will also be able to offer "services", for example, repairing tools, by receiving an item and a payment, and then returning a specific item.
Phase 4: Owning nodes, being able to go to places __Phase 4__: Actions: In progress
- NPCs should be able to own chests, furnaces and doors and use them - [ ] NPCs should be able to use chests, furnaces, doors, beds and sit on "sittable" nodes (in progress)
- NPCs should be able to go to specific places in their own homes or villages or in the world in general: - [x] NPCs should be able to walk to specific places. Should also be able to open doors, fence gates and any other type of openable node while going to a place.
- For this, a places framework should be defined - [x] NPCs should have the ability to identify nodes that belong to him/her, and recall them/
- NPCs at least should know where their bed is, and use it
Phase 5: Activities and jobs __Phase 5__: Schedules and fundamental jobs
- NPCs should be able to dig and place nodes - [ ] NPCs should be able to perform different activities on depending on the time of the day. For instance, a NPC could be farming during the morning, selling its product on the afternoon, and comfortable sitting at home during the night.
- NPCs should be able to perform different activities on different times of the day - [ ] Add the fundamental jobs, which are:
- [ ] Mining
- [ ] Wood cutting
- [ ] Farming
- [ ] Cooking
Phase 6: Advanced spawners for villages __Phase 6__: Advanced spawners for villages
- [ ] Support for mg_villages mod by Sokomine
- [ ] Identify, on medieval villages, houses that NPC can live on.
- [ ] Identify the amount of NPC that the house can support
- [ ] Spawn NPCs and assign them a bed. Detect sharable objects (chest, furnace, benches)
- [ ] Assign them random schedules based on the type of building they spawn.
__Version 2.0__
Version 2.0
-----------
Phase 7: Make NPCs scriptable Phase 7: Make NPCs scriptable
Phase 8: Improve NPCs so that they can be farmers, lumberjacks and miners Phase 8: Improve NPCs pathfinding, allow them to go upstairs.
Phase 9: Improve NPCs so that they can tame and own farm animals Phase 9: Improve NPCs so that they can tame and own farm animals
Phase 10: Improve NPCs so that they can run on carts, boats and (maybe) horses Phase 10: Improve NPCs so that they can run on carts, boats and (maybe) horses
Version 3.0 __Version 3.0__
-----------
Phase 11: Integrate with commerce mod Phase 11: Integrate with commerce mod
Phase 12: Improve relationships for obtaining more benefits from a married NPC Phase 12: Improve relationships for obtaining more benefits from a married NPC
Phase 13: Improve AI to include support for house families Phase 13: Improve AI to include support for house families
Phase 14: Improve AI to create village communities Phase 14: Improve AI to create village communities
License for Code
----------------
Copyright (C) 2016 Zorman2000
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@ -14,15 +14,25 @@
npc.actions = {} npc.actions = {}
-- Describes actions with doors or openable nodes -- Describes actions with doors or openable nodes
npc.actions.door_action = { npc.actions.const = {
OPEN = 1, doors = {
CLOSE = 2 action = {
} OPEN = 1,
CLOSE = 2
-- Describe the state of doors or openable nodes },
npc.actions.door_state = { state = {
OPEN = 1, OPEN = 1,
CLOSED = 2 CLOSED = 2
}
},
beds = {
LAY = 1,
GET_UP = 2
},
sittable = {
SIT = 1,
GET_UP = 2
}
} }
function npc.actions.rotate(args) function npc.actions.rotate(args)
@ -32,12 +42,20 @@ function npc.actions.rotate(args)
self.rotate = 0 self.rotate = 0
if dir == npc.direction.north then if dir == npc.direction.north then
yaw = 0 yaw = 0
elseif dir == npc.direction.north_east then
yaw = (7 * math.pi) / 4
elseif dir == npc.direction.east then elseif dir == npc.direction.east then
yaw = (3 * math.pi) / 2 yaw = (3 * math.pi) / 2
elseif dir == npc.direction.south_east then
yaw = (5 * math.pi) / 4
elseif dir == npc.direction.south then elseif dir == npc.direction.south then
yaw = math.pi yaw = math.pi
elseif dir == npc.direction.south_west then
yaw = (3 * math.pi) / 4
elseif dir == npc.direction.west then elseif dir == npc.direction.west then
yaw = math.pi / 2 yaw = math.pi / 2
elseif dir == npc.direction.north_west then
yaw = math.pi / 4
end end
self.object:setyaw(yaw) self.object:setyaw(yaw)
end end
@ -175,12 +193,35 @@ function npc.actions.take_item_from_external_inventory(args)
return false return false
end end
function npc.actions.get_openable_node_state(node) function npc.actions.check_external_inventory_contains_item(args)
local state = npc.actions.door_state.CLOSED local self = args.self
local a_i1, a_i2 = string.find(node.name, "_a") local player = args.player
if a_i1 == nil then local pos = args.pos
state = npc.actions.door_state.OPEN local inv_list = args.inv_list
local item_name = args.item_name
local count = args.count
local inv
if player ~= nil then
inv = minetest.get_inventory({type="player", name=player})
else
inv = minetest.get_inventory({type="node", pos})
end end
-- Create ItemStack for checking the external inventory
local item = ItemStack(item_name.." "..count)
-- Check if inventory contains item
return inv:contains_item(inv_list, item)
end
function npc.actions.get_openable_node_state(node)
minetest.log("Node name: "..dump(node.name))
local state = npc.actions.const.doors.state.CLOSED
local a_i1, a_i2 = string.find(node.name, "_a")
local open_i1, open_i2 = string.find(node.name, "_close")
if a_i1 == nil and open_i1 == nil then
state = npc.actions.const.doors.state.OPEN
end
minetest.log("Door state: "..dump(state))
return state return state
end end
@ -231,7 +272,6 @@ function npc.actions.use_furnace(self, pos, item)
item_name = npc.get_item_name(fuel_item.item_string), item_name = npc.get_item_name(fuel_item.item_string),
count = npc.get_item_count(fuel_item.item_string) count = npc.get_item_count(fuel_item.item_string)
} }
minetest.log("Adding fuel action")
npc.add_action(self, npc.actions.put_item_on_external_inventory, args) npc.add_action(self, npc.actions.put_item_on_external_inventory, args)
-- Put the item that we want to cook on the furnace -- Put the item that we want to cook on the furnace
args = { args = {
@ -243,9 +283,14 @@ function npc.actions.use_furnace(self, pos, item)
count = npc.get_item_count(src_item.item_string), count = npc.get_item_count(src_item.item_string),
is_furnace = true is_furnace = true
} }
minetest.log("Adding src action")
npc.add_action(self, npc.actions.put_item_on_external_inventory, args) npc.add_action(self, npc.actions.put_item_on_external_inventory, args)
-- TODO: Need to add a way to calculate how many seconds will pass
-- until the furnace is done, or at least the items that we expect
-- to get (assume all items to be cooked are the ones ewe expect back)
-- Then, add that many stand actions, then an action to take the items.
return true return true
end end
end end
@ -253,11 +298,6 @@ function npc.actions.use_furnace(self, pos, item)
return false return false
end end
npc.actions.bed_action = {
LAY = 1,
GET_UP = 2
}
-- This function makes the NPC lay or stand up from a bed. The -- This function makes the NPC lay or stand up from a bed. The
-- pos is the location of the bed, action can be lay or get up -- pos is the location of the bed, action can be lay or get up
function npc.actions.use_bed(self, pos, action) function npc.actions.use_bed(self, pos, action)
@ -265,7 +305,7 @@ function npc.actions.use_bed(self, pos, action)
minetest.log(dump(param2)) minetest.log(dump(param2))
local dir = minetest.facedir_to_dir(param2.param2) local dir = minetest.facedir_to_dir(param2.param2)
if action == npc.actions.bed_action.LAY then if action == npc.actions.const.beds.LAY then
-- Calculate position (from beds mod) -- Calculate position (from beds mod)
local bed_pos = {x = pos.x + dir.x / 2, y = pos.y + 1, z = pos.z + dir.z / 2} local bed_pos = {x = pos.x + dir.x / 2, y = pos.y + 1, z = pos.z + dir.z / 2}
-- Sit down on bed -- Sit down on bed
@ -286,77 +326,44 @@ function npc.actions.use_bed(self, pos, action)
end end
end end
-- This function makes the NPC lay or stand up from a bed. The
-- pos is the location of the bed, action can be lay or get up
function npc.actions.use_sittable(self, pos, action)
local node = minetest.get_node(pos)
-- This function can be used to make the NPC walk from one if action == npc.actions.const.sittable.SIT then
-- position to another. -- Calculate position depending on bench
function npc.actions.walk_to_pos(self, end_pos) -- For cottages bench (code taken from Sokomine's cottages mod):
local p2 = {x=pos.x, y=pos.y, z=pos.z};
local start_pos = self.object:getpos() if not( node ) or node.param2 == 0 then
p2.z = p2.z+0.3;
-- Find path elseif node.param2 == 1 then
local path = npc.actions.find_path({x=start_pos.x, y=start_pos.y-1, z=start_pos.z}, end_pos) p2.x = p2.x+0.3;
elseif node.param2 == 2 then
if path ~= nil then p2.z = p2.z-0.3;
minetest.log("Found path to node: "..dump(end_pos)) elseif node.param2 == 3 then
p2.x = p2.x-0.3;
-- Add a first step
local dir = npc.actions.get_direction(start_pos, path[1].pos)
npc.add_action(self, npc.actions.walk_step, {self = self, dir = dir})
-- Add subsequent steps
for i = 1, #path do
--minetest.log("Path: (i) "..dump(path[i])..": Path i+1 "..dump(path[i+1]))
-- Do not add an extra step
if i == #path then
-- Add direction to last node
local dir = npc.actions.get_direction(path[i].pos, end_pos)
-- Add stand animation at end
npc.add_action(self, npc.actions.stand, {self = self})
-- Rotate to face the end node
npc.actions.rotate({self = self, dir = dir})
break
end
-- Get direction to move from path[i] to path[i+1]
local dir = npc.actions.get_direction(path[i].pos, path[i+1].pos)
-- Check if next node is a door, if it is, open it, then walk
if path[i+1].type == "O" then
-- Check if door is already open
local node = minetest.get_node(path[i+1].pos)
if npc.actions.get_openable_node_state(node) == npc.actions.door_state.CLOSED then
-- Stop to open door, this avoids misplaced movements later on
npc.add_action(self, npc.actions.stand, {self = self})
-- Open door
npc.add_action(self, npc.actions.use_door, {self=self, pos=path[i+1].pos, action=npc.actions.door_action.OPEN})
end
end
-- Add walk action to action queue
npc.add_action(self, npc.actions.walk_step, {self = self, dir = dir})
end end
end -- For stairs (based on the above code):
end local p2 = {x=pos.x, y=pos.y, z=pos.z};
if not( node ) or node.param2 == 0 then
p2.z = p2.z-0.2;
--------------------------------------------------------------------------------------- elseif node.param2 == 1 then
-- Path-finding code p2.x = p2.x-0.2;
--------------------------------------------------------------------------------------- elseif node.param2 == 2 then
-- This is the limit to search for a path based on the goal node. p2.z = p2.z+0.2;
-- If the path finder code goes beyond this limit in nodes away elseif node.param2 == 3 then
-- on the x or z plane, it will stop looking for a path p2.x = p2.x+0.2;
npc.actions.PATH_DIFF_LIMIT = 125 end
-- Sit down on bench/chair/stairs
-- Returns the opposite of a vector (scalar multiplication by -1) npc.add_action(self, npc.actions.sit, {self=self, pos=p2})
local function vector_opposite(v) -- Rotate to the correct position
return vector.multiply(v, -1) npc.add_action(self, npc.actions.rotate, {self=self, dir=node.param2 + 2 % 4})
end else
-- Walk up from bed
-- Returns a unit direction vector based on the largest coordinate npc.add_action(self, npc.actions.walk_step, {self=self, dir=param2.param2 + 2 % 4})
local function get_unit_dir_vector_based_on_diff(v) -- Stand
if math.abs(v.x) > math.abs(v.z) then npc.add_action(self, npc.actions.stand, {self=self})
return {x=(v.x/math.abs(v.x)) * -1, y=0, z=0}
elseif math.abs(v.z) > math.abs(v.x) then
return {x=0, y=0, z=(v.z/math.abs(v.z)) * -1}
elseif math.abs(v.x) == math.abs(v.z) then
return {x=(v.x/math.abs(v.x)) * -1, y=0, z=0}
end end
end end
@ -379,184 +386,344 @@ function npc.actions.get_direction(v1, v2)
end end
end end
-- This function is used to determine if a node is walkable -- This function can be used to make the NPC walk from one
-- or openable, in which case is good to use when finding a path -- position to another.
local function is_good_node(node) function npc.actions.walk_to_pos(self, end_pos)
-- Is openable is to support doors, fence gates and other
-- doors from other mods. Currently, default doors and gates local start_pos = self.object:getpos()
-- will be supported. Cottages doors should also be supported.
--minetest.log("Node name: "..dump(node.name)) -- Find path
local is_openable = false local path = pathfinder.find_path(start_pos, end_pos, 20)
local start_i,end_i = string.find(node.name, "doors:")
is_openable = start_i ~= nil if path ~= nil then
--minetest.log("Is node openable: "..dump(is_openable)) minetest.log("Found path to node: "..dump(end_pos))
--minetest.log("Is node walkable: "..dump(not minetest.registered_nodes[node.name].walkable))
if not minetest.registered_nodes[node.name].walkable then -- Add a first step
return "W" --local dir = npc.actions.get_direction(start_pos, path[1].pos)
elseif is_openable then --minetest.log("Start_pos: "..dump(start_pos)..", First path step: "..dump(path[1].pos))
return "O" --minetest.log("Direction: "..dump(dir))
--npc.add_action(self, npc.actions.walk_step, {self = self, dir = dir})
-- Add subsequent steps
local door_opened = false
for i = 1, #path do
--minetest.log("Path: (i) "..dump(path[i])..": Path i+1 "..dump(path[i+1]))
-- Do not add an extra step
if (i+1) == #path then
-- Add direction to last node
local dir = npc.actions.get_direction(path[i].pos, end_pos)
-- Add stand animation at end
npc.add_action(self, npc.actions.stand, {self = self})
-- Rotate to face the end node
npc.actions.rotate({self = self, dir = dir})
break
end
-- Get direction to move from path[i] to path[i+1]
local dir = npc.actions.get_direction(path[i].pos, path[i+1].pos)
-- Check if next node is a door, if it is, open it, then walk
if path[i+1].type == pathfinder.node_types.openable then
-- Check if door is already open
local node = minetest.get_node(path[i+1].pos)
if npc.actions.get_openable_node_state(node) == npc.actions.const.doors.state.CLOSED then
minetest.log("Opening action to open door")
-- Stop to open door, this avoids misplaced movements later on
npc.add_action(self, npc.actions.stand, {self = self})
-- Open door
npc.add_action(self, npc.actions.use_door, {self=self, pos=path[i+1].pos, action=npc.actions.const.doors.action.OPEN})
door_opened = true
end
end
-- Add walk action to action queue
npc.add_action(self, npc.actions.walk_step, {self = self, dir = dir})
if door_opened then
-- Stop to close door, this avoids misplaced movements later on
npc.add_action(self, npc.actions.stand, {self = self})
-- Close door
npc.add_action(self, npc.actions.use_door, {self=self, pos=path[i+1].pos, action=npc.actions.const.doors.action.CLOSE})
door_opened = false
end
end
else else
return "N" minetest.log("Unable to find path.")
end end
end end
-- Finds paths ignoring vertical obstacles
-- This function is recursive and attempts to move all the time on
-- the direction that will definetely lead to the end position.
local function find_path_recursive(start_pos, end_pos, path_nodes, last_dir, last_good_dir)
--minetest.log("Start pos: "..dump(start_pos))
-- Find difference. The purpose of this is to weigh movement, attempting -- ATTENTION:
-- the largest difference first, or both if equal. -- Old, deprecated, non-functional code below:
local diff = vector.subtract(start_pos, end_pos) ---------------------------------------------------------------------------------------
-- Path-finding code
---------------------------------------------------------------------------------------
-- This is the limit to search for a path based on the goal node.
-- If the path finder code goes beyond this limit in nodes away
-- on the x or z plane, it will stop looking for a path
-- npc.actions.PATH_DIFF_LIMIT = 125
--minetest.log("Difference: "..dump(diff)) -- -- Returns the opposite of a vector (scalar multiplication by -1)
-- local function vector_opposite(v)
-- return vector.multiply(v, -1)
-- end
-- End if difference is larger than max difference possible (limit) -- -- Returns a unit direction vector based on the largest coordinate
if math.abs(diff.x) > npc.actions.PATH_DIFF_LIMIT -- local function get_unit_dir_vector_based_on_diff(v)
or math.abs(diff.z) > npc.actions.PATH_DIFF_LIMIT then -- if math.abs(v.x) > math.abs(v.z) then
-- Cannot find feasable path -- return {x=(v.x/math.abs(v.x)) * -1, y=0, z=0}
return nil -- elseif math.abs(v.z) > math.abs(v.x) then
end -- return {x=0, y=0, z=(v.z/math.abs(v.z)) * -1}
-- Determine direction to move -- elseif math.abs(v.x) == math.abs(v.z) then
local dir_vector = get_unit_dir_vector_based_on_diff(diff) -- return {x=(v.x/math.abs(v.x)) * -1, y=0, z=(v.z/math.abs(v.z)) * -1}
-- end
-- end
--minetest.log("Direction vector: "..dump(dir_vector)) -- -- 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 is_good_node(node)
-- -- Is openable is to support doors, fence gates and other
-- -- doors from other mods. Currently, default doors and gates
-- -- will be supported. Cottages doors should also be supported.
-- --minetest.log("Node name: "..dump(node.name))
-- local is_openable = false
-- local start_i,end_i = string.find(node.name, "doors:")
-- is_openable = start_i ~= nil
-- --minetest.log("Is node openable: "..dump(is_openable))
-- --minetest.log("Is node walkable: "..dump(not minetest.registered_nodes[node.name].walkable))
-- if not minetest.registered_nodes[node.name].walkable then
-- return "W"
-- elseif is_openable then
-- return "O"
-- else
-- return "N"
-- end
-- end
if last_good_dir ~= nil then -- -- Finds paths ignoring vertical obstacles
dir_vector = last_good_dir -- -- This function is recursive and attempts to move all the time on
end -- -- the direction that will definetely lead to the end position.
-- local function find_path_recursive(start_pos, end_pos, path_nodes, last_dir, last_good_dir, last_good_try)
-- minetest.log("Start pos: "..dump(start_pos))
-- Get next position based on direction -- -- Find difference. The purpose of this is to weigh movement, attempting
local next_pos = vector.add(start_pos, dir_vector) -- -- the largest difference first, or both if equal.
-- local diff = vector.subtract(start_pos, end_pos)
--minetest.log("Next pos: "..dump(next_pos)) -- minetest.log("Difference: "..dump(diff))
-- Check if next_pos is actually within one block from the -- -- End if difference is larger than max difference possible (limit)
-- expected position. If so, finish -- if math.abs(diff.x) > npc.actions.PATH_DIFF_LIMIT
local diff_to_end = vector.subtract(next_pos, end_pos) -- or math.abs(diff.z) > npc.actions.PATH_DIFF_LIMIT then
if math.abs(diff_to_end.x) < 1 and math.abs(diff_to_end.y) < 1 and math.abs(diff_to_end.z) < 1 then -- minetest.log("Can't find feasable path.")
--minetest.log("Diff to end: "..dump(diff_to_end)) -- -- Cannot find feasable path
table.insert(path_nodes, {pos=next_pos, type="E"}) -- return nil
minetest.log("Found path to end.") -- end
return path_nodes -- -- Determine direction to move
end -- local dir_vector = get_unit_dir_vector_based_on_diff(diff)
-- Check if movement is possible on the calculated direction
local next_node = minetest.get_node(next_pos)
-- If direction vector is opposite to the last dir, then do not attempt to walk into it
--minetest.log("Next node is walkable: "..dump(not minetest.registered_nodes[next_node.name].walkable))
local attempted_to_go_opposite = false
if last_dir ~= nil and vector.equals(dir_vector, vector_opposite(last_dir)) then
attempted_to_go_opposite = true
--minetest.log("Last dir: "..dump(last_dir))
--minetest.log("Calculated dir vector is the opposite of last dir: "..dump(vector.equals(dir_vector, vector_opposite(last_dir))))
end
local node_type = is_good_node(next_node) -- minetest.log("Direction vector: "..dump(dir_vector))
if node_type ~= "N" and (not attempted_to_go_opposite) then
table.insert(path_nodes, {pos=next_pos, type=node_type}) -- if last_dir ~= nil then
return find_path_recursive(next_pos, end_pos, path_nodes, nil, nil) -- if last_good_try == 4
else -- or (dir_vector.x ~= 0 and dir_vector.z ~=0)
--minetest.log("------------ Second attempt ------------") -- -- Attention: Hacks below! The magic number 3 could be just extremely wrong.
-- -- This is a terrible hack based on experimentations :(
-- or (dir_vector.x ~= 0 and last_dir.x == 0 and math.abs(diff.x) > math.abs(diff.z) and math.abs(diff.z) < 3)
-- or (dir_vector.z ~= 0 and last_dir.z == 0 and math.abs(diff.z) > math.abs(diff.x) and math.abs(diff.x) < 3) then
-- if last_dir.x ~= 0 and diff.x ~= 0
-- or last_dir.z ~= 0 and diff.z ~= 0 then
-- minetest.log("Using last dir as direction vector: "..dump(last_dir))
-- dir_vector = last_dir
-- end
-- end
-- end
-- if last_good_dir ~= nil then
-- minetest.log("Using last good dir as direction vector: "..dump(last_good_dir))
-- dir_vector = last_good_dir
-- end
-- -- Get next position based on direction
-- local next_pos = vector.add(start_pos, dir_vector)
-- minetest.log("Next pos: "..dump(next_pos))
-- -- Check if next_pos is actually within one block from the
-- -- expected position. If so, finish
-- local diff_to_end = vector.subtract(next_pos, end_pos)
-- if math.abs(diff_to_end.x) < 1 and math.abs(diff_to_end.y) < 1 and math.abs(diff_to_end.z) < 1 then
-- minetest.log("Diff to end: "..dump(diff_to_end))
-- table.insert(path_nodes, {pos=next_pos, type="E"})
-- minetest.log("Found path to end.")
-- return path_nodes
-- end
-- -- Check if movement is possible on the calculated direction
-- local next_node = minetest.get_node(next_pos)
-- -- If direction vector is opposite to the last dir, then do not attempt to walk into it
-- minetest.log("Next node is walkable: "..dump(not minetest.registered_nodes[next_node.name].walkable))
-- local attempted_to_go_opposite = false
-- if last_dir ~= nil and vector.equals(dir_vector, vector_opposite(last_dir)) then
-- attempted_to_go_opposite = true
-- minetest.log("Last dir: "..dump(last_dir))
-- minetest.log("Calculated dir vector is the opposite of last dir: "..dump(vector.equals(dir_vector, vector_opposite(last_dir))))
-- end
-- local node_type = is_good_node(next_node)
-- if node_type ~= "N" and (not attempted_to_go_opposite) then
-- table.insert(path_nodes, {pos=next_pos, type=node_type})
-- return find_path_recursive(next_pos, end_pos, path_nodes, dir_vector, nil, 1)
-- else
-- minetest.log("------------ Second attempt ------------")
-- If not walkable, attempt turn into the other coordinate -- -- If not walkable, attempt turn into the other coordinate
-- Determine this coordinate based on what was the last calculated direction -- -- Determine this coordinate based on what was the last calculated direction
-- that didn't needed correction (last good dir). If this doesn't exists (e.g. -- -- that didn't needed correction (last good dir). If this doesn't exists (e.g.
-- there has been no correction for a while) then select the direction by -- -- there has been no correction for a while) then select the direction by
-- trying to shorten the distance between NPC and the end node. -- -- trying to shorten the distance between NPC and the end node.
--minetest.log("Last known good dir: "..dump(last_good_dir))
local step = 0
if last_good_dir == nil then
-- Store the current direction vector as the last non-corrected
-- calculated direction
last_good_dir = dir_vector
-- Determine which direction to move
if dir_vector.x == 0 then
--minetest.log("Choosing x direction")
step = diff.x/math.abs(diff.x) * -1
if diff.x == 0 then
if last_dir ~= nil then
step = last_dir.x
else
-- Set a default step to avoid locks
step = 1
end
end
dir_vector = {x = step, y = 0, z = 0}
elseif dir_vector.z == 0 then
step = diff.z/math.abs(diff.z) * -1
if diff.z == 0 then
if last_dir ~= nil then
step = last_dir.z
else
-- Set a default step to avoid locks
step = 1
end
end
dir_vector = {x = 0, y = 0, z = step}
end
--minetest.log("Re-calculated dir vector: "..dump(dir_vector))
next_pos = vector.add(start_pos, dir_vector)
else
dir_vector = last_good_dir
if dir_vector.x == 0 then
--minetest.log("Moving into x direction")
step = last_dir.x
elseif dir_vector.z == 0 then
--minetest.log("Moving into z direction")
step = last_dir.z
end
dir_vector = last_dir
next_pos = vector.add(start_pos, dir_vector)
end
-- Check if new node is walkable -- minetest.log("Last known good dir: "..dump(last_good_dir))
next_node = minetest.get_node(next_pos) -- local step = 0
-- if last_good_dir == nil then
-- -- Store the current direction vector as the last non-corrected
-- -- calculated direction
-- last_good_dir = dir_vector
--minetest.log("Next node is walkable: "..dump(not minetest.registered_nodes[next_node.name].walkable)) -- -- Determine which direction to move
-- if dir_vector.x == 0 then
-- minetest.log("Choosing x direction")
-- step = diff.x/math.abs(diff.x) * -1
-- if diff.x == 0 then
-- if last_dir ~= nil and last_dir.x ~= 0 then--and last_good_try == 2 then
-- step = last_dir.x
-- else
-- -- Set a default step to avoid locks
-- step = 1
-- end
-- end
-- dir_vector = {x = step, y = 0, z = 0}
-- elseif dir_vector.z == 0 then
-- step = diff.z/math.abs(diff.z) * -1
-- if diff.z == 0 then
-- if last_dir ~= nil and last_dir.z ~= 0 then -- and last_good_try == 2 then
-- step = last_dir.z
-- else
-- -- Set a default step to avoid locks
-- step = 1
-- end
-- end
-- dir_vector = {x = 0, y = 0, z = step}
-- end
-- minetest.log("Re-calculated dir vector: "..dump(dir_vector))
-- next_pos = vector.add(start_pos, dir_vector)
-- else
-- dir_vector = last_good_dir
-- if dir_vector.x == 0 then
-- minetest.log("Moving into x direction")
-- step = last_dir.x
-- elseif dir_vector.z == 0 then
-- minetest.log("Moving into z direction")
-- step = last_dir.z
-- end
-- dir_vector = last_dir
-- next_pos = vector.add(start_pos, dir_vector)
-- end
local node_type = is_good_node(next_node) -- -- Check if new node is walkable
if node_type ~= "N" then -- next_node = minetest.get_node(next_pos)
table.insert(path_nodes, {pos=next_pos, type=node_type})
return find_path_recursive(next_pos, end_pos, path_nodes, dir_vector, last_good_dir)
else
last_good_dir = dir_vector
--minetest.log("------------ Third attempt ------------")
-- If not walkable, then try the next node by finding the original -- minetest.log("Next node is walkable: "..dump(not minetest.registered_nodes[next_node.name].walkable))
-- direction vector, then choosing the opposite of that.
if dir_vector.x ~= 0 then
--minetest.log("Move into opposite z dir")
dir_vector = get_unit_dir_vector_based_on_diff(start_pos, diff)
vector.multiply(dir_vector, -1)
elseif dir_vector.z ~= 0 then
--minetest.log("Move into opposite x dir")
dir_vector = get_unit_dir_vector_based_on_diff(start_pos, diff)
vector.multiply(dir_vector, -1)
end
--minetest.log("New direction: "..dump(dir_vector))
next_pos = vector.add(start_pos, dir_vector) -- local node_type = is_good_node(next_node)
--minetest.log("New next_pos: "..dump(next_pos)) -- if node_type ~= "N" then
next_node = minetest.get_node(next_pos) -- table.insert(path_nodes, {pos=next_pos, type=node_type})
--minetest.log("Next node is walkable: "..dump(not minetest.registered_nodes[next_node.name].walkable)) -- return find_path_recursive(next_pos, end_pos, path_nodes, dir_vector, last_good_dir, 2)
local node_type = is_good_node(next_node) -- else
if node_type ~= "N" then
table.insert(path_nodes, {pos=next_pos, type=node_type})
return find_path_recursive(next_pos, end_pos, path_nodes, dir_vector, last_good_dir)
else
-- Try to return back, opposite of last dir. For now return nil as this code
-- is not good enough to work correctly.
return nil
end
end
end
end -- minetest.log("Last known good dir: "..dump(last_good_dir))
-- -- Only pick the second attempt's dir if it was actually good (meaning,
-- -- it could step on that dir)
-- if last_good_try == 2 then
-- last_good_dir = dir_vector
-- end
-- minetest.log("------------ Third attempt ------------")
-- Calls the recursive function to calculate the path -- -- If not walkable, then try the next node by finding the original
function npc.actions.find_path(start_pos, end_pos) -- -- direction vector, then choosing the opposite of that.
return find_path_recursive(start_pos, end_pos, {}, nil, nil)
end -- minetest.log("Last dir: "..dump(last_dir))
-- minetest.log("Last good try: "..dump(last_good_try))
-- minetest.log("Last attempted direction: "..dump(dir_vector))
-- if vector.equals(last_good_dir, last_dir) then
-- -- Go opposite the direction of second attempt
-- minetest.log("Moving opposite of last attempted")
-- dir_vector = vector.multiply(dir_vector, -1)
-- else
-- minetest.log("Moving opposite of last good dir")
-- dir_vector = vector.multiply(last_good_dir, -1)
-- last_good_dir = last_dir
-- end
-- -- if last_good_try > 1 or vector.equals(last_good_dir, last_dir) then
-- -- if dir_vector.x ~= 0 then
-- -- minetest.log("Move into opposite z dir")
-- -- dir_vector = get_unit_dir_vector_based_on_diff(diff)
-- -- dir_vector = vector.multiply(dir_vector, -1)
-- -- elseif dir_vector.z ~= 0 then
-- -- minetest.log("Move into opposite x dir")
-- -- dir_vector = get_unit_dir_vector_based_on_diff(diff)
-- -- dir_vector = vector.multiply(dir_vector, -1)
-- -- end
-- -- else
-- -- minetest.log("Stuck in corner, try to move out of corner")
-- -- dir_vector = vector.multiply(last_good_dir, -1)
-- -- last_good_dir = last_dir
-- -- end
-- minetest.log("New direction: "..dump(dir_vector))
-- minetest.log("New last good dir: "..dump(last_good_dir))
-- next_pos = vector.add(start_pos, dir_vector)
-- minetest.log("New next_pos: "..dump(next_pos))
-- next_node = minetest.get_node(next_pos)
-- minetest.log("Next node is walkable: "..dump(not minetest.registered_nodes[next_node.name].walkable))
-- local node_type = is_good_node(next_node)
-- if node_type ~= "N" then
-- table.insert(path_nodes, {pos=next_pos, type=node_type})
-- return find_path_recursive(next_pos, end_pos, path_nodes, dir_vector, last_good_dir, 3)
-- else
-- -- Move into the opposite of last good dir
-- minetest.log("------------ Fourth attempt ------------")
-- minetest.log("Last known good dir: "..dump(old_last_good_dir))
-- local old_dir_vector = dir_vector
-- -- If not walkable, then try moving into the opposite of last good dir
-- dir_vector = vector.multiply(last_good_dir, -1)
-- minetest.log("New direction: "..dump(dir_vector))
-- next_pos = vector.add(start_pos, dir_vector)
-- minetest.log("New next_pos: "..dump(next_pos))
-- next_node = minetest.get_node(next_pos)
-- minetest.log("Next node is walkable: "..dump(not minetest.registered_nodes[next_node.name].walkable))
-- local node_type = is_good_node(next_node)
-- if node_type ~= "N" then
-- table.insert(path_nodes, {pos=next_pos, type=node_type})
-- return find_path_recursive(next_pos, end_pos, path_nodes, dir_vector, old_dir_vector, 4)
-- else
-- minetest.log("Attempted to rotate 4 times, can't do anything else")
-- return nil
-- end
-- end
-- end
-- end
-- end
-- -- Calls the recursive function to calculate the path
-- function npc.actions.find_path(start_pos, end_pos)
-- return find_path_recursive(start_pos, end_pos, {}, nil, nil, 0)
-- end

247
actions/pathfinder.lua Normal file
View File

@ -0,0 +1,247 @@
-- Pathfinding code by Zorman2000
---------------------------------------------------------------------------------------
-- 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 external Jumper LUA library,
-- 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 = {
start = 0,
goal = 1,
walkable = 2,
openable = 3,
non_walkable = 4
}
pathfinder.nodes = {
openable_prefix = {
"doors:",
"cottages:gate",
"cottages:half_door"
}
}
-- 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 pathfinder.find_path(start_pos, end_pos, range)
-- Map the Minetest area to a 2D array
local map = pathfinder.create_map(start_pos, end_pos, range, {})
-- 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
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.
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 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
function pathfinder.create_map(start_pos, end_pos, extra_range, walkables)
-- Unused, will not use voxel areas for now
--local c_air = minetest.get_content_id("air")
minetest.log("Start pos: "..dump(start_pos))
minetest.log("End pos: "..dump(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)
--minetest.log("Start x sign: "..dump(start_x_sign)..", end x sign: "..dump(end_x_sign))
--minetest.log("End z sign: "..dump(start_z_sign)..", end z sign: "..dump(end_z_sign))
-- 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("Pos 1: "..dump(pos1))
--minetest.log("Pos 2: "..dump(pos2))
-- Get Voxel Area - Not used for the moment
-- local vm = minetest.get_voxel_manip()
-- local emin, emax = vm:read_from_map(pos1, pos2)
-- local area = VoxelArea:new({MinEdge=emin, MaxEdge=emax})
-- local data = vm:get_data()
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)
if node.name == "default:air" then
-- If air do no more checks
table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.walkable})
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
end
-- Insert the converted row into the grid
table.insert(grid, current_row)
end
return grid
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
end
-- 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
end
end
minetest.log("Found start and end positions: "..dump(result))
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)
end
end
table.insert(result, result_row)
end
return result
end
-- This function returns an array of tables with to 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
end

View File

@ -17,7 +17,7 @@ npc.places.nodes = {
"beds:bed_bottom", "beds:bed_bottom",
"beds:fancy_bed_bottom" "beds:fancy_bed_bottom"
}, },
CHAIRS = { SITTABLE = {
"cottages:bench" "cottages:bench"
}, },
CHESTS = { CHESTS = {

0
backup/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,628 @@
-- Actions code for Advanced NPC by Zorman2000
---------------------------------------------------------------------------------------
-- Action functionality
---------------------------------------------------------------------------------------
-- The NPCs will be able to perform five fundamental actions that will allow
-- for them to perform any other kind of interaction in the world. These
-- fundamental actions are: place a node, dig a node, put items on an inventory,
-- take items from an inventory, find a node closeby (radius 3) and
-- walk a step on specific direction. These actions will be set on an action queue.
-- The queue will have the specific steps, in order, for the NPC to be able to do
-- something (example, go to a specific place and put a chest there). The
-- fundamental actions are added to the action queue to make a complete task for the NPC.
npc.actions = {}
-- Describes actions with doors or openable nodes
npc.actions.door_action = {
OPEN = 1,
CLOSE = 2
}
-- Describe the state of doors or openable nodes
npc.actions.door_state = {
OPEN = 1,
CLOSED = 2
}
function npc.actions.rotate(args)
local self = args.self
local dir = args.dir
local yaw = 0
self.rotate = 0
if dir == npc.direction.north then
yaw = 0
elseif dir == npc.direction.east then
yaw = (3 * math.pi) / 2
elseif dir == npc.direction.south then
yaw = math.pi
elseif dir == npc.direction.west then
yaw = math.pi / 2
end
self.object:setyaw(yaw)
end
-- This function will make the NPC walk one step on a
-- specifc direction. One step means one node. It returns
-- true if it can move on that direction, and false if there is an obstacle
function npc.actions.walk_step(args)
local self = args.self
local dir = args.dir
local vel = {}
if dir == npc.direction.north then
vel = {x=0, y=0, z=0.98}
elseif dir == npc.direction.east then
vel = {x=0.98, y=0, z=0}
elseif dir == npc.direction.south then
vel = {x=0, y=0, z=-0.98}
elseif dir == npc.direction.west then
vel = {x=-0.98, y=0, z=0}
end
set_animation(self, "walk")
npc.actions.rotate({self=self, dir=dir})
self.object:setvelocity(vel)
end
-- This action makes the NPC stand and remain like that
function npc.actions.stand(args)
local self = args.self
-- Stop NPC
self.object:setvelocity({x=0, y=0, z=0})
-- Set stand animation
set_animation(self, "stand")
end
-- This action makes the NPC sit on the node where it is
function npc.actions.sit(args)
local self = args.self
local pos = args.pos
-- Stop NPC
self.object:setvelocity({x=0, y=0, z=0})
-- If position give, set to that position
if pos ~= nil then
self.object:setpos(pos)
end
-- Set sit animation
self.object:set_animation({
x = npc.ANIMATION_SIT_START,
y = npc.ANIMATION_SIT_END},
self.animation.speed_normal, 0)
end
-- This action makes the NPC lay on the node where it is
function npc.actions.lay(args)
local self = args.self
local pos = args.pos
-- Stop NPC
self.object:setvelocity({x=0, y=0, z=0})
-- If position give, set to that position
if pos ~= nil then
self.object:setpos(pos)
end
-- Set sit animation
self.object:set_animation({
x = npc.ANIMATION_LAY_START,
y = npc.ANIMATION_LAY_END},
self.animation.speed_normal, 0)
end
-- Inventory functions for players and for nodes
-- This function is a convenience function to make it easy to put
-- and get items from another inventory (be it a player inv or
-- a node inv)
function npc.actions.put_item_on_external_inventory(args)
local self = args.self
local player = args.player
local pos = args.pos
local inv_list = args.inv_list
local item_name = args.item_name
local count = args.count
local is_furnace = args.is_furnace
local inv
if player ~= nil then
inv = minetest.get_inventory({type="player", name=player})
else
inv = minetest.get_inventory({type="node", pos=pos})
end
-- Create ItemStack to put on external inventory
local item = ItemStack(item_name.." "..count)
-- Check if there is enough room to add the item on external invenotry
if inv:room_for_item(inv_list, item) then
-- Take item from NPC's inventory
if npc.take_item_from_inventory_itemstring(self, item) then
-- NPC doesn't have item and/or specified quantity
return false
end
-- Add items to external inventory
inv:add_item(inv_list, item)
-- If this is a furnace, start furnace timer
if is_furnace == true then
minetest.get_node_timer(pos):start(1.0)
end
return true
end
-- Not able to put on external inventory
return false
end
function npc.actions.take_item_from_external_inventory(args)
local self = args.self
local player = args.player
local pos = args.pos
local inv_list = args.inv_list
local item_name = args.item_name
local count = args.count
local inv
if player ~= nil then
inv = minetest.get_inventory({type="player", name=player})
else
inv = minetest.get_inventory({type="node", pos})
end
-- Create ItemSTack to take from external inventory
local item = ItemStack(item_name.." "..count)
-- Check if there is enough of the item to take
if inv:contains_item(inv_list, item) then
-- Add item to NPC's inventory
npc.add_item_to_inventory_itemstring(self, item)
-- Add items to external inventory
inv:remove_item(inv_list, item)
return true
end
-- Not able to put on external inventory
return false
end
function npc.actions.get_openable_node_state(node)
local state = npc.actions.door_state.CLOSED
local a_i1, a_i2 = string.find(node.name, "_a")
if a_i1 == nil then
state = npc.actions.door_state.OPEN
end
return state
end
-- This function is used to open or close doors from
-- that use the default doors mod
function npc.actions.use_door(args)
local self = args.self
local pos = args.pos
local action = args.action
local node = minetest.get_node(pos)
local state = npc.actions.get_openable_node_state(node)
local clicker = self.object
if action ~= state then
minetest.registered_nodes[node.name].on_rightclick(pos, node, clicker, nil, nil)
end
end
---------------------------------------------------------------------------------------
-- Tasks functionality
---------------------------------------------------------------------------------------
-- Tasks are operations that require many actions to perform. Basic tasks, like
-- walking from one place to another, operating a furnace, storing or taking
-- items from a chest, are provided here.
-- This function allows a NPC to use a furnace using only items from
-- its own inventory. Fuel is not provided. Once the furnace is finished
-- with the fuel items the NPC will take whatever was cooked and whatever
-- remained to cook. The function received the position of the furnace
-- to use, and the item to cook in furnace. Item is an itemstring
function npc.actions.use_furnace(self, pos, item)
-- Check if any item in the NPC inventory serve as fuel
-- For now, just use some specific items as fuels
local fuels = {"default:leaves", "default:tree", ""}
-- Check if NPC has a fuel item
for i = 1,2 do
local fuel_item = npc.inventory_contains(self, fuels[i])
local src_item = npc.inventory_contains(self, item)
if fuel_item ~= nil and src_item ~= nil then
-- Put this item on the fuel inventory list of the furnace
local args = {
self = self,
player = nil,
pos = pos,
inv_list = "fuel",
item_name = npc.get_item_name(fuel_item.item_string),
count = npc.get_item_count(fuel_item.item_string)
}
minetest.log("Adding fuel action")
npc.add_action(self, npc.actions.put_item_on_external_inventory, args)
-- Put the item that we want to cook on the furnace
args = {
self = self,
player = nil,
pos = pos,
inv_list = "src",
item_name = npc.get_item_name(src_item.item_string),
count = npc.get_item_count(src_item.item_string),
is_furnace = true
}
minetest.log("Adding src action")
npc.add_action(self, npc.actions.put_item_on_external_inventory, args)
return true
end
end
-- Couldn't use the furnace due to lack of items
return false
end
npc.actions.bed_action = {
LAY = 1,
GET_UP = 2
}
-- This function makes the NPC lay or stand up from a bed. The
-- pos is the location of the bed, action can be lay or get up
function npc.actions.use_bed(self, pos, action)
local param2 = minetest.get_node(pos)
minetest.log(dump(param2))
local dir = minetest.facedir_to_dir(param2.param2)
if action == npc.actions.bed_action.LAY then
-- Calculate position (from beds mod)
local bed_pos = {x = pos.x + dir.x / 2, y = pos.y + 1, z = pos.z + dir.z / 2}
-- Sit down on bed
npc.add_action(self, npc.actions.sit, {self=self})
-- Rotate to the correct position
npc.add_action(self, npc.actions.rotate, {self=self, dir=param2.param2 + 2 % 4})
-- Lay down
npc.add_action(self, npc.actions.lay, {self=self, pos=bed_pos})
else
-- Calculate position to get up
local bed_pos = {x = pos.x, y = pos.y + 1, z = pos.z}
-- Sit up
npc.add_action(self, npc.actions.sit, {self=self, pos=bed_pos})
-- Walk up from bed
npc.add_action(self, npc.actions.walk_step, {self=self, dir=param2.param2 + 2 % 4})
-- Stand
npc.add_action(self, npc.actions.stand, {self=self})
end
end
-- This function can be used to make the NPC walk from one
-- position to another.
function npc.actions.walk_to_pos(self, end_pos)
local start_pos = self.object:getpos()
-- Find path
local path = npc.actions.find_path({x=start_pos.x, y=start_pos.y-1, z=start_pos.z}, end_pos)
if path ~= nil then
minetest.log("Found path to node: "..dump(end_pos))
-- Add a first step
local dir = npc.actions.get_direction(start_pos, path[1].pos)
npc.add_action(self, npc.actions.walk_step, {self = self, dir = dir})
-- Add subsequent steps
for i = 1, #path do
--minetest.log("Path: (i) "..dump(path[i])..": Path i+1 "..dump(path[i+1]))
-- Do not add an extra step
if i == #path then
-- Add direction to last node
local dir = npc.actions.get_direction(path[i].pos, end_pos)
-- Add stand animation at end
npc.add_action(self, npc.actions.stand, {self = self})
-- Rotate to face the end node
npc.actions.rotate({self = self, dir = dir})
break
end
-- Get direction to move from path[i] to path[i+1]
local dir = npc.actions.get_direction(path[i].pos, path[i+1].pos)
-- Check if next node is a door, if it is, open it, then walk
if path[i+1].type == "O" then
-- Check if door is already open
local node = minetest.get_node(path[i+1].pos)
if npc.actions.get_openable_node_state(node) == npc.actions.door_state.CLOSED then
-- Stop to open door, this avoids misplaced movements later on
npc.add_action(self, npc.actions.stand, {self = self})
-- Open door
npc.add_action(self, npc.actions.use_door, {self=self, pos=path[i+1].pos, action=npc.actions.door_action.OPEN})
end
end
-- Add walk action to action queue
npc.add_action(self, npc.actions.walk_step, {self = self, dir = dir})
end
end
end
---------------------------------------------------------------------------------------
-- Path-finding code
---------------------------------------------------------------------------------------
-- This is the limit to search for a path based on the goal node.
-- If the path finder code goes beyond this limit in nodes away
-- on the x or z plane, it will stop looking for a path
npc.actions.PATH_DIFF_LIMIT = 125
-- Returns the opposite of a vector (scalar multiplication by -1)
local function vector_opposite(v)
return vector.multiply(v, -1)
end
-- Returns a unit direction vector based on the largest coordinate
local function get_unit_dir_vector_based_on_diff(v)
if math.abs(v.x) > math.abs(v.z) then
return {x=(v.x/math.abs(v.x)) * -1, y=0, z=0}
elseif math.abs(v.z) > math.abs(v.x) then
return {x=0, y=0, z=(v.z/math.abs(v.z)) * -1}
elseif math.abs(v.x) == math.abs(v.z) then
return {x=(v.x/math.abs(v.x)) * -1, y=0, z=(v.z/math.abs(v.z)) * -1}
end
end
-- This function returns the direction enum
-- for the moving from v1 to v2
function npc.actions.get_direction(v1, v2)
local dir = vector.subtract(v2, v1)
if dir.x ~= 0 then
if dir.x > 0 then
return npc.direction.east
else
return npc.direction.west
end
elseif dir.z ~= 0 then
if dir.z > 0 then
return npc.direction.north
else
return npc.direction.south
end
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 is_good_node(node)
-- Is openable is to support doors, fence gates and other
-- doors from other mods. Currently, default doors and gates
-- will be supported. Cottages doors should also be supported.
--minetest.log("Node name: "..dump(node.name))
local is_openable = false
local start_i,end_i = string.find(node.name, "doors:")
is_openable = start_i ~= nil
--minetest.log("Is node openable: "..dump(is_openable))
--minetest.log("Is node walkable: "..dump(not minetest.registered_nodes[node.name].walkable))
if not minetest.registered_nodes[node.name].walkable then
return "W"
elseif is_openable then
return "O"
else
return "N"
end
end
-- Finds paths ignoring vertical obstacles
-- This function is recursive and attempts to move all the time on
-- the direction that will definetely lead to the end position.
local function find_path_recursive(start_pos, end_pos, path_nodes, last_dir, last_good_dir, last_good_try)
minetest.log("Start pos: "..dump(start_pos))
-- Find difference. The purpose of this is to weigh movement, attempting
-- the largest difference first, or both if equal.
local diff = vector.subtract(start_pos, end_pos)
minetest.log("Difference: "..dump(diff))
-- End if difference is larger than max difference possible (limit)
if math.abs(diff.x) > npc.actions.PATH_DIFF_LIMIT
or math.abs(diff.z) > npc.actions.PATH_DIFF_LIMIT then
minetest.log("Can't find feasable path.")
-- Cannot find feasable path
return nil
end
-- Determine direction to move
local dir_vector = get_unit_dir_vector_based_on_diff(diff)
minetest.log("Direction vector: "..dump(dir_vector))
if last_dir ~= nil then
if last_good_try == 4
or (dir_vector.x ~= 0 and dir_vector.z ~=0)
-- Attention: Hacks below! The magic number 3 could be just extremely wrong.
-- This is a terrible hack based on experimentations :(
or (dir_vector.x ~= 0 and last_dir.x == 0 and math.abs(diff.x) > math.abs(diff.z) and math.abs(diff.z) < 3)
or (dir_vector.z ~= 0 and last_dir.z == 0 and math.abs(diff.z) > math.abs(diff.x) and math.abs(diff.x) < 3) then
if last_dir.x ~= 0 and diff.x ~= 0
or last_dir.z ~= 0 and diff.z ~= 0 then
minetest.log("Using last dir as direction vector: "..dump(last_dir))
dir_vector = last_dir
end
end
end
if last_good_dir ~= nil then
minetest.log("Using last good dir as direction vector: "..dump(last_good_dir))
dir_vector = last_good_dir
end
-- Get next position based on direction
local next_pos = vector.add(start_pos, dir_vector)
minetest.log("Next pos: "..dump(next_pos))
-- Check if next_pos is actually within one block from the
-- expected position. If so, finish
local diff_to_end = vector.subtract(next_pos, end_pos)
if math.abs(diff_to_end.x) < 1 and math.abs(diff_to_end.y) < 1 and math.abs(diff_to_end.z) < 1 then
minetest.log("Diff to end: "..dump(diff_to_end))
table.insert(path_nodes, {pos=next_pos, type="E"})
minetest.log("Found path to end.")
return path_nodes
end
-- Check if movement is possible on the calculated direction
local next_node = minetest.get_node(next_pos)
-- If direction vector is opposite to the last dir, then do not attempt to walk into it
minetest.log("Next node is walkable: "..dump(not minetest.registered_nodes[next_node.name].walkable))
local attempted_to_go_opposite = false
if last_dir ~= nil and vector.equals(dir_vector, vector_opposite(last_dir)) then
attempted_to_go_opposite = true
minetest.log("Last dir: "..dump(last_dir))
minetest.log("Calculated dir vector is the opposite of last dir: "..dump(vector.equals(dir_vector, vector_opposite(last_dir))))
end
local node_type = is_good_node(next_node)
if node_type ~= "N" and (not attempted_to_go_opposite) then
table.insert(path_nodes, {pos=next_pos, type=node_type})
return find_path_recursive(next_pos, end_pos, path_nodes, dir_vector, nil, 1)
else
minetest.log("------------ Second attempt ------------")
-- If not walkable, attempt turn into the other coordinate
-- Determine this coordinate based on what was the last calculated direction
-- that didn't needed correction (last good dir). If this doesn't exists (e.g.
-- there has been no correction for a while) then select the direction by
-- trying to shorten the distance between NPC and the end node.
minetest.log("Last known good dir: "..dump(last_good_dir))
local step = 0
if last_good_dir == nil then
-- Store the current direction vector as the last non-corrected
-- calculated direction
last_good_dir = dir_vector
-- Determine which direction to move
if dir_vector.x == 0 then
minetest.log("Choosing x direction")
step = diff.x/math.abs(diff.x) * -1
if diff.x == 0 then
if last_dir ~= nil and last_dir.x ~= 0 then--and last_good_try == 2 then
step = last_dir.x
else
-- Set a default step to avoid locks
step = 1
end
end
dir_vector = {x = step, y = 0, z = 0}
elseif dir_vector.z == 0 then
step = diff.z/math.abs(diff.z) * -1
if diff.z == 0 then
if last_dir ~= nil and last_dir.z ~= 0 then -- and last_good_try == 2 then
step = last_dir.z
else
-- Set a default step to avoid locks
step = 1
end
end
dir_vector = {x = 0, y = 0, z = step}
end
minetest.log("Re-calculated dir vector: "..dump(dir_vector))
next_pos = vector.add(start_pos, dir_vector)
else
dir_vector = last_good_dir
if dir_vector.x == 0 then
minetest.log("Moving into x direction")
step = last_dir.x
elseif dir_vector.z == 0 then
minetest.log("Moving into z direction")
step = last_dir.z
end
dir_vector = last_dir
next_pos = vector.add(start_pos, dir_vector)
end
-- Check if new node is walkable
next_node = minetest.get_node(next_pos)
minetest.log("Next node is walkable: "..dump(not minetest.registered_nodes[next_node.name].walkable))
local node_type = is_good_node(next_node)
if node_type ~= "N" then
table.insert(path_nodes, {pos=next_pos, type=node_type})
return find_path_recursive(next_pos, end_pos, path_nodes, dir_vector, last_good_dir, 2)
else
minetest.log("Last known good dir: "..dump(last_good_dir))
-- Only pick the second attempt's dir if it was actually good (meaning,
-- it could step on that dir)
if last_good_try == 2 then
last_good_dir = dir_vector
end
minetest.log("------------ Third attempt ------------")
-- If not walkable, then try the next node by finding the original
-- direction vector, then choosing the opposite of that.
minetest.log("Last dir: "..dump(last_dir))
minetest.log("Last good try: "..dump(last_good_try))
minetest.log("Last attempted direction: "..dump(dir_vector))
if vector.equals(last_good_dir, last_dir) then
-- Go opposite the direction of second attempt
minetest.log("Moving opposite of last attempted")
dir_vector = vector.multiply(dir_vector, -1)
else
minetest.log("Moving opposite of last good dir")
dir_vector = vector.multiply(last_good_dir, -1)
last_good_dir = last_dir
end
-- if last_good_try > 1 or vector.equals(last_good_dir, last_dir) then
-- if dir_vector.x ~= 0 then
-- minetest.log("Move into opposite z dir")
-- dir_vector = get_unit_dir_vector_based_on_diff(diff)
-- dir_vector = vector.multiply(dir_vector, -1)
-- elseif dir_vector.z ~= 0 then
-- minetest.log("Move into opposite x dir")
-- dir_vector = get_unit_dir_vector_based_on_diff(diff)
-- dir_vector = vector.multiply(dir_vector, -1)
-- end
-- else
-- minetest.log("Stuck in corner, try to move out of corner")
-- dir_vector = vector.multiply(last_good_dir, -1)
-- last_good_dir = last_dir
-- end
minetest.log("New direction: "..dump(dir_vector))
minetest.log("New last good dir: "..dump(last_good_dir))
next_pos = vector.add(start_pos, dir_vector)
minetest.log("New next_pos: "..dump(next_pos))
next_node = minetest.get_node(next_pos)
minetest.log("Next node is walkable: "..dump(not minetest.registered_nodes[next_node.name].walkable))
local node_type = is_good_node(next_node)
if node_type ~= "N" then
table.insert(path_nodes, {pos=next_pos, type=node_type})
return find_path_recursive(next_pos, end_pos, path_nodes, dir_vector, last_good_dir, 3)
else
-- Move into the opposite of last good dir
minetest.log("------------ Fourth attempt ------------")
minetest.log("Last known good dir: "..dump(old_last_good_dir))
local old_dir_vector = dir_vector
-- If not walkable, then try moving into the opposite of last good dir
dir_vector = vector.multiply(last_good_dir, -1)
minetest.log("New direction: "..dump(dir_vector))
next_pos = vector.add(start_pos, dir_vector)
minetest.log("New next_pos: "..dump(next_pos))
next_node = minetest.get_node(next_pos)
minetest.log("Next node is walkable: "..dump(not minetest.registered_nodes[next_node.name].walkable))
local node_type = is_good_node(next_node)
if node_type ~= "N" then
table.insert(path_nodes, {pos=next_pos, type=node_type})
return find_path_recursive(next_pos, end_pos, path_nodes, dir_vector, old_dir_vector, 4)
else
minetest.log("Attempted to rotate 4 times, can't do anything else")
return nil
end
end
end
end
end
-- Calls the recursive function to calculate the path
function npc.actions.find_path(start_pos, end_pos)
return find_path_recursive(start_pos, end_pos, {}, nil, nil, 0)
end

411240
debug.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -32,5 +32,6 @@ dofile(path .. "/trade/trade.lua")
dofile(path .. "/trade/prices.lua") dofile(path .. "/trade/prices.lua")
dofile(path .. "/actions/actions.lua") dofile(path .. "/actions/actions.lua")
dofile(path .. "/actions/places.lua") dofile(path .. "/actions/places.lua")
dofile(path .. "/actions/pathfinder.lua")
print (S("[Mod] Advanced NPC loaded")) print (S("[Mod] Advanced NPC loaded"))

View File

@ -1,3 +1,7 @@
Copyright (C) 2016-2017 Hector Franqui (zorman2000)
Full GNU GPL v3:
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 29 June 2007

25
npc.lua
View File

@ -183,7 +183,7 @@ end
-- Actions should be added in strict order for tasks to work as expected. -- Actions should be added in strict order for tasks to work as expected.
function npc.add_action(self, action, arguments) function npc.add_action(self, action, arguments)
self.freeze = true self.freeze = true
minetest.log("Current Pos: "..dump(self.object:getpos())) --minetest.log("Current Pos: "..dump(self.object:getpos()))
local action_entry = {action=action, args=arguments} local action_entry = {action=action, args=arguments}
--minetest.log(dump(action_entry)) --minetest.log(dump(action_entry))
table.insert(self.actions.queue, action_entry) table.insert(self.actions.queue, action_entry)
@ -346,17 +346,22 @@ local function npc_spawn(self, pos)
ent.places_map = {} ent.places_map = {}
-- Temporary initialization of actions for testing -- Temporary initialization of actions for testing
local nodes = npc.places.find_new_nearby(ent, {"beds:bed_bottom"}, 20) local nodes = npc.places.find_new_nearby(ent, {"cottages:tub"}, 30)
minetest.log("Found nodes: "..dump(nodes))
--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.use_door, {self = ent, pos = nodes[1], action = npc.actions.door_action.OPEN})
--npc.add_action(ent, npc.actions.stand, {self = ent}) --npc.add_action(ent, npc.actions.stand, {self = ent})
npc.add_action(ent, npc.actions.stand, {self = ent}) --npc.add_action(ent, npc.actions.stand, {self = ent})
npc.actions.walk_to_pos(ent, nodes[1]) npc.actions.walk_to_pos(ent, nodes[1])
npc.actions.use_bed(ent, nodes[1], npc.actions.bed_action.LAY) --npc.actions.use_bed(ent, nodes[1], npc.actions.const.beds.LAY)
npc.add_action(ent, npc.actions.lay, {self = ent}) --npc.add_action(ent, npc.actions.lay, {self = ent})
npc.add_action(ent, npc.actions.lay, {self = ent}) -- npc.add_action(ent, npc.actions.lay, {self = ent})
npc.add_action(ent, npc.actions.lay, {self = ent}) -- npc.add_action(ent, npc.actions.lay, {self = ent})
npc.add_action(ent, npc.actions.lay, {self = ent}) -- npc.add_action(ent, npc.actions.lay, {self = ent})
npc.actions.use_bed(ent, nodes[1], npc.actions.bed_action.GET_UP) --npc.actions.use_bed(ent, nodes[1], npc.actions.const.beds.GET_UP)
-- npc.add_action(ent, npc.action.stand, {self = ent}) -- npc.add_action(ent, npc.action.stand, {self = ent})
-- npc.add_action(ent, npc.action.stand, {self = ent}) -- npc.add_action(ent, npc.action.stand, {self = ent})
@ -546,7 +551,7 @@ mobs:register_mob("advanced_npc:npc", {
-- Spawn -- Spawn
mobs:spawn({ mobs:spawn({
name = "advanced_npc:npc", name = "advanced_npc:npc",
nodes = {"default:stone"}, nodes = {"mg_villages:plotmarker", "default:stone"},
min_light = 3, min_light = 3,
active_object_count = 1, active_object_count = 1,
interval = 5, interval = 5,