554fde4643
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.
469 lines
17 KiB
Plaintext
469 lines
17 KiB
Plaintext
-- 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 = {}
|
|
|
|
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=1}
|
|
elseif dir == npc.direction.east then
|
|
vel = {x=1, y=0, z=0}
|
|
elseif dir == npc.direction.south then
|
|
vel = {x=0, y=0, z=-1}
|
|
elseif dir == npc.direction.west then
|
|
vel = {x=-1, 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
|
|
-- Stop NPC
|
|
self.object:setvelocity({x=0, y=0, z=0})
|
|
-- 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
|
|
-- Stop NPC
|
|
self.object:setvelocity({x=0, y=0, z=0})
|
|
-- 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
|
|
|
|
|
|
---------------------------------------------------------------------------------------
|
|
-- 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, opening/closing doors, etc. 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 fuel
|
|
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
|
|
|
|
|
|
function npc.actions.walk_to_pos(self, end_pos)
|
|
|
|
local start_pos = self.object:getpos()
|
|
|
|
minetest.log("Starting pos: "..dump(start_pos))
|
|
|
|
-- Use Minetest built-in pathfinding algorithm, A*
|
|
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))
|
|
for i = 1, #path do
|
|
minetest.log("Path: (i) "..dump(path[i])..": Path i+1 "..dump(path[i+1]))
|
|
local dir = npc.actions.get_direction(path[i].pos, path[i+1].pos)
|
|
-- Add walk action to action queue
|
|
npc.add_action(self, npc.actions.walk_step, {self = self, dir = dir})
|
|
if i+1 == #path then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Add stand animation at end
|
|
npc.add_action(self, npc.actions.stand, {self = self})
|
|
|
|
end
|
|
|
|
local function vector_add(p1, p2)
|
|
return {x=p1.x+p2.x, y=p1.y+p2.y, z=p1.z+p2.z}
|
|
end
|
|
|
|
local function vector_diff(p1, p2)
|
|
return {x=p1.x-p2.x, y=p1.y-p2.y, z=p1.z-p2.z}
|
|
end
|
|
|
|
local function vector_opposite(v)
|
|
return vector.multiply(v, -1)
|
|
end
|
|
|
|
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=0}
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
DIFF_LIMIT = 125
|
|
|
|
-- 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
|
|
-- the largest difference first, or both if equal.
|
|
|
|
local diff = vector_diff(start_pos, end_pos)
|
|
minetest.log("Difference: "..dump(diff))
|
|
-- End if difference is larger than max difference possible (limit)
|
|
if math.abs(diff.x) > DIFF_LIMIT or math.abs(diff.z) > DIFF_LIMIT then
|
|
-- 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_good_dir ~= nil then
|
|
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_diff(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
|
|
if minetest.registered_nodes[next_node.name].walkable == false
|
|
and (not attempted_to_go_opposite) then
|
|
table.insert(path_nodes, {pos=next_pos, type="W"})
|
|
return find_path_recursive(next_pos, end_pos, path_nodes, nil, nil)
|
|
else
|
|
minetest.log("------------ Second attempt ------------")
|
|
-- If not walkable, attempt turn into the other coordinate
|
|
-- Store last good direction to retry at all times
|
|
minetest.log("Last known good dir: "..dump(last_good_dir))
|
|
local step = 0
|
|
if last_good_dir == nil then
|
|
last_good_dir = dir_vector
|
|
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
|
|
minetest.log("Choosing z direction")
|
|
step = diff.z/math.abs(diff.z) * -1
|
|
minetest.log("Step: "..dump(step)..". Diff: "..dump(diff))
|
|
minetest.log("Last dir: ".. dump(last_dir))
|
|
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
|
|
|
|
|
|
|
|
-- if dir_vector.x == 0 then
|
|
-- minetest.log("Moving into x direction")
|
|
-- local step = diff.x/math.abs(diff.x) * -1
|
|
-- if diff.x == 0 then
|
|
-- -- If the difference for x with end position is zero, then try
|
|
-- -- to move in the last known direction
|
|
-- if last_dir ~= nil then
|
|
-- step = last_dir.x
|
|
-- end
|
|
-- end
|
|
-- next_pos = {x = start_pos.x + step, y = start_pos.y, z = start_pos.z}
|
|
-- dir_vector = {x = step, y = 0, z = 0}
|
|
-- elseif dir_vector.z == 0 then
|
|
-- minetest.log("Moving into z direction")
|
|
-- local step = diff.z/math.abs(diff.z) * -1
|
|
-- if diff.z == 0 then
|
|
-- -- If the difference for z with end position is zero, then try
|
|
-- -- to move in the last known direction
|
|
-- if last_dir ~= nil then
|
|
-- step = last_dir.z
|
|
-- end
|
|
-- end
|
|
-- next_pos = {x = start_pos.x, y = start_pos.y, z = start_pos.z + step}
|
|
-- dir_vector = {x = 0, y = 0, z = step}
|
|
-- end
|
|
minetest.log("Next calculated position: "..dump(next_pos))
|
|
|
|
-- 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))
|
|
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
|
|
if minetest.registered_nodes[next_node.name].walkable == false then
|
|
table.insert(path_nodes, {pos=next_pos, type="W"})
|
|
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
|
|
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)
|
|
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))
|
|
-- 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
|
|
if minetest.registered_nodes[next_node.name].walkable == false then
|
|
--and (not attempted_to_go_opposite) then
|
|
table.insert(path_nodes, {pos=next_pos, type="W"})
|
|
return find_path_recursive(next_pos, end_pos, path_nodes, dir_vector, last_good_dir)
|
|
else
|
|
--return back, opposite of last dir. For now return nil as this code is not
|
|
-- good
|
|
return nil
|
|
-- minetest.log("Have to go back")
|
|
-- local return_dir = vector_opposite(last_dir)
|
|
-- -- If it is returning back already, continue on that direction
|
|
-- if attempted_to_go_opposite then
|
|
-- return_dir = last_dir
|
|
-- end
|
|
-- minetest.log("Opposite dir: "..dump(return_dir))
|
|
-- next_pos = vector_add(start_pos, return_dir)
|
|
-- minetest.log("Calculated pos: "..dump(next_pos))
|
|
-- return find_path(next_pos, end_pos, return_dir)
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
function npc.actions.find_path(start_pos, end_pos)
|
|
return find_path_recursive(start_pos, end_pos, {}, nil, nil)
|
|
end |