From adeeecd43e6c8c47f4691aa3860c672a34f1a1b5 Mon Sep 17 00:00:00 2001 From: Hector Franqui Date: Wed, 14 Jun 2017 18:42:36 -0400 Subject: [PATCH] Pathfinding (WIP): Use MarkBu's pathfinder mod as the new pathfinder. Actions: Modify walk_to_pos to use new pathfinding code. Allow support for giving positions as strings or tables referring to specific places on Places Map. Spawner: Assign beds to NPCs. Increase amount of NPCs now being spawned as they are able to reach top floor beds. Places: Slight overhaul and changes. Add more info to places. NPC: Hide nametag. --- actions/actions.lua | 172 +++++++++++++++++++++++++++++------- actions/pathfinder.lua | 29 +++--- actions/places.lua | 45 ++++++---- depends.txt | 1 + dialogue.lua | 2 +- npc.lua | 54 ++++-------- spawner.lua | 196 +++++++++++++++++++++++++++++++++-------- trade/trade.lua | 4 +- 8 files changed, 369 insertions(+), 134 deletions(-) diff --git a/actions/actions.lua b/actions/actions.lua index 39d9e30..8bede43 100644 --- a/actions/actions.lua +++ b/actions/actions.lua @@ -194,12 +194,20 @@ function npc.actions.walk_step(self, args) if dir == npc.direction.north then vel = {x=0, y=0, z=speed} + elseif dir == npc.direction.north_east then + vel = {x=speed, y=0, z=speed} elseif dir == npc.direction.east then vel = {x=speed, y=0, z=0} + elseif dir == npc.direction.south_east then + vel = {x=speed, y=0, z=-speed} elseif dir == npc.direction.south then vel = {x=0, y=0, z=-speed} + elseif dir == npc.direction.south_west then + vel = {x=-speed, y=0, z=-speed} elseif dir == npc.direction.west then vel = {x=-speed, y=0, z=0} + elseif dir == npc.direction.north_west then + vel = {x=-speed, y=0, z=speed} end -- Rotate NPC npc.actions.rotate(self, {dir=dir}) @@ -403,13 +411,67 @@ end -- walking from one place to another, operating a furnace, storing or taking -- items from a chest, are provided here. +local function get_pos_argument(self, pos) + minetest.log("Type of pos: "..dump(type(pos))) + -- Check which type of position argument we received + if type(pos) == "table" then + minetest.log("Received table pos: "..dump(pos)) + -- Check if table is position + if pos.x ~= nil and pos.y ~= nil and pos.z ~= nil then + -- Position received, return position + return pos + elseif pos.place_type ~= nil then + -- Received table in the following format: + -- {place_type = "", index = 1, use_access_node = false} + local index = pos.index or 1 + local use_access_node = pos.use_access_node or false + local places = npc.places.get_by_type(self, pos.place_type) + -- Check index is valid on the places map + if #places >= index then + -- Check if access node is desired + if use_access_node then + -- Return actual node pos + return places[index].access_node + else + -- Return node pos that allows access to node + return places[index].pos + end + end + end + elseif type(pos) == "string" then + -- Received name of place, so we are going to look for the actual pos + local places_pos = npc.places.get_by_type(self, pos) + -- Return nil if no position found + if places_pos == nil or #places_pos == 0 then + return nil + end + -- Check if received more than one position + if #places_pos > 1 then + -- Check all places, return owned if existent, else return the first one + for i = 1, #places_pos do + if places_pos[i].status == "owned" then + return places_pos[i].pos + end + end + end + -- Return the first position only if it couldn't find an owned + -- place, or if it there is only oneg + return places_pos[1].pos + end +end + -- 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, args) - local pos = args.pos + local pos = get_pos_argument(self, args.pos) + if pos == nil then + minetest.log("[advanced_npc] WARNING Got nil position in 'use_furnace' using args.pos: "..dump(args.pos)) + return + end + local item = args.item local freeze = args.freeze -- Define which items are usable as fuels. The NPC @@ -530,7 +592,11 @@ 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_bed(self, args) - local pos = args.pos + local pos = get_pos_argument(self, args.pos) + if pos == nil then + minetest.log("[advanced_npc] WARNING Got nil position in 'use_bed' using args.pos: "..dump(args.pos)) + return + end local action = args.action local node = minetest.get_node(pos) minetest.log(dump(node)) @@ -586,7 +652,11 @@ 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, args) - local pos = args.pos + local pos = get_pos_argument(self, args.pos) + if pos == nil then + minetest.log("[advanced_npc] WARNING Got nil position in 'use_sittable' using args.pos: "..dump(args.pos)) + return + end local action = args.action local node = minetest.get_node(pos) @@ -621,13 +691,24 @@ end -- 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 and dir.z ~= 0 then + if dir.x > 0 and dir.z > 0 then + return npc.direction.north_east + elseif dir.x > 0 and dir.z < 0 then + return npc.direction.south_east + elseif dir.x < 0 and dir.z > 0 then + return npc.direction.north_west + elseif dir.x < 0 and dir.z < 0 then + return npc.direction.south_west + end + elseif dir.x ~= 0 and dir.z == 0 then if dir.x > 0 then return npc.direction.east else return npc.direction.west end - elseif dir.z ~= 0 then + elseif dir.z ~= 0 and dir.x == 0 then if dir.z > 0 then return npc.direction.north else @@ -636,6 +717,7 @@ function npc.actions.get_direction(v1, v2) end end + -- This function can be used to make the NPC walk from one -- position to another. If the optional parameter walkable_nodes -- is included, which is a table of node names, these nodes are @@ -643,14 +725,20 @@ end -- path. function npc.actions.walk_to_pos(self, args) -- Get arguments for this task - local end_pos = args.end_pos + local end_pos = get_pos_argument(self, args.end_pos) + if end_pos == nil then + minetest.log("[advanced_npc] WARNING Got nil position in 'walk_to_pos' using args.pos: "..dump(args.end_pos)) + return + end + local enforce_move = args.enforce_move or true local walkable_nodes = args.walkable -- Round start_pos to make sure it can find start and end local start_pos = vector.round(self.object:getpos()) -- Use y of end_pos (this can only be done assuming flat terrain) - start_pos.y = self.object:getpos().y - minetest.log("Walk to pos: Using start position: "..dump(start_pos)) + --start_pos.y = self.object:getpos().y + minetest.log("[advanced_npc] INFO walk_to_pos: Start pos: "..minetest.pos_to_string(start_pos)) + minetest.log("[advanced_npc] INFO walk_to_pos: End pos: "..minetest.pos_to_string(end_pos)) -- Set walkable nodes to empty if the parameter hasn't been used if walkable_nodes == nil then @@ -658,10 +746,24 @@ 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, 20, walkable_nodes) + local path = pathfinder.find_path(start_pos, end_pos, self) - if path ~= nil then - minetest.log("[advanced_npc] Found path to node: "..minetest.pos_to_string(end_pos)) + 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 + + --minetest.log("Found path: "..dump(path)) + + --minetest.log("Path detail: "..dump(path_detail)) + --minetest.log("New path: "..dump(path)) + minetest.log("[advanced_npc] INFO walk_to_pos Found path to node: "..minetest.pos_to_string(end_pos)) -- Store path self.actions.walking.path = path @@ -689,11 +791,11 @@ function npc.actions.walk_to_pos(self, args) -- 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 + if path[i+1].type == npc.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, dir) == npc.actions.const.doors.state.CLOSED then - minetest.log("Opening action to open door") + --minetest.log("Opening action to open door") -- Stop to open door, this avoids misplaced movements later on npc.add_action(self, npc.actions.cmd.STAND, {dir=dir}) -- Open door @@ -701,29 +803,33 @@ function npc.actions.walk_to_pos(self, args) door_opened = true end + end -- Add walk action to action queue npc.add_action(self, npc.actions.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+1].pos}) if door_opened then - -- Stop to close door, this avoids misplaced movements later on - local x_adj, z_adj = 0, 0 - if dir == 0 then - z_adj = 0.1 - elseif dir == 1 then - x_adj = 0.1 - elseif dir == 2 then - z_adj = -0.1 - elseif dir == 3 then - x_adj = -0.1 - end - local pos_on_close = {x=path[i+1].pos.x + x_adj, y=path[i+1].pos.y + 1, z=path[i+1].pos.z + z_adj} - npc.add_action(self, npc.actions.cmd.STAND, {dir=(dir + 2)% 4, pos=pos_on_close}) - -- Close door - npc.add_action(self, npc.actions.cmd.USE_OPENABLE, {pos=path[i+1].pos, action=npc.actions.const.doors.action.CLOSE}) + -- Stop to close door, this avoids misplaced movements later on + -- local x_adj, z_adj = 0, 0 + -- if dir == 0 then + -- z_adj = 0.1 + -- elseif dir == 1 then + -- x_adj = 0.1 + -- elseif dir == 2 then + -- z_adj = -0.1 + -- elseif dir == 3 then + -- x_adj = -0.1 + -- end + -- local pos_on_close = {x=path[i+1].pos.x + x_adj, y=path[i+1].pos.y + 1, z=path[i+1].pos.z + z_adj} + -- Add extra walk step to ensure that one is standing at other side of openable node + npc.add_action(self, npc.actions.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+2].pos}) + -- Stop to close the door + npc.add_action(self, npc.actions.cmd.STAND, {dir=(dir + 2) % 4 })--, pos=pos_on_close}) + -- Close door + npc.add_action(self, npc.actions.cmd.USE_OPENABLE, {pos=path[i+1].pos, action=npc.actions.const.doors.action.CLOSE}) - door_opened = false + door_opened = false end end @@ -733,6 +839,12 @@ function npc.actions.walk_to_pos(self, args) npc.add_action(self, npc.actions.cmd.SET_INTERVAL, {interval=1, freeze=true}) else - minetest.log("Unable to find path.") + -- Unable to find path + minetest.log("[advanced_npc] INFO walk_to_pos Unable to find path. Teleporting to: "..minetest.pos_to_string(end_pos)) + -- Check if movement is enforced + if enforce_move then + -- Move to end pos + self.object:moveto(end_pos) + end end end \ No newline at end of file diff --git a/actions/pathfinder.lua b/actions/pathfinder.lua index d0d604f..a31e6f4 100644 --- a/actions/pathfinder.lua +++ b/actions/pathfinder.lua @@ -10,9 +10,11 @@ -- (https://github.com/Yonaba/Jumper). -- Mapping algorithm: transforms a Minetest map surface to a 2d grid. -pathfinder = {} +npc.pathfinder = {} -pathfinder.node_types = { +local pathfinder = {} + +npc.pathfinder.node_types = { start = 0, goal = 1, walkable = 2, @@ -20,7 +22,7 @@ pathfinder.node_types = { non_walkable = 4 } -pathfinder.nodes = { +npc.pathfinder.nodes = { openable_prefix = { "doors:", "cottages:gate", @@ -32,7 +34,7 @@ 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 pathfinder.find_path(start_pos, end_pos, range, walkable_nodes) +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 @@ -69,13 +71,16 @@ 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) + +function npc.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(pathfinder.nodes.openable_prefix) do + 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 @@ -83,16 +88,20 @@ local function 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 pathfinder.node_types.walkable + return npc.pathfinder.node_types.walkable elseif is_openable then - return pathfinder.node_types.openable + return npc.pathfinder.node_types.openable + --return pathfinder.node_types.openable else for i = 1, #exceptions do if node.name == exceptions[i] then - return pathfinder.node_types.walkable + return npc.pathfinder.node_types.walkable + --return pathfinder.node_types.walkable end end - return pathfinder.node_types.non_walkable + return npc.pathfinder.node_types.non_walkable + --return pathfinder.node_types.non_walkable end end diff --git a/actions/places.lua b/actions/places.lua index 8f0a5c6..e6eb7ef 100644 --- a/actions/places.lua +++ b/actions/places.lua @@ -53,40 +53,42 @@ npc.places.nodes = { npc.places.PLACE_TYPE = { BED = { - PRIMARY = "primary" + PRIMARY = "bed_primary" }, SITTABLE = { - PRIMARY = "primary" + PRIMARY = "sit_primary" }, OPENABLE = { HOME_ENTRANCE_DOOR = "home_entrance_door" }, OTHER = { + HOME_PLOTMARKER = "home_plotmarker", HOME_INSIDE = "home_inside", HOME_OUTSIDE = "home_outside" } } -function npc.places.add_public(self, place_name, place_type, pos) +function npc.places.add_public(self, place_name, place_type, pos, access_node) --minetest.log("Place name: "..dump(place_name)..", type: "..dump(place_type)) - self.places_map[place_name] = {type=place_type, pos=pos, status="shared"} + self.places_map[place_name] = {type=place_type, pos=pos, access_node=access_node or pos, status="shared"} end -- Adds a specific node to the NPC places, and modifies the -- node metadata to identify the NPC as the owner. This allows -- other NPCs to avoid to take this as their own. -function npc.places.add_owned(self, place_name, place_type, pos) +function npc.places.add_owned(self, place_name, place_type, pos, access_node) -- Get node metadata - local meta = minetest.get_meta(pos) + --local meta = minetest.get_meta(pos) -- Check if it is owned by an NPC? - if meta:get_string("npc_owner") == "" then + --if meta:get_string("npc_owner") == "" then -- Set owned by NPC - meta:set_string("npc_owner", self.npc_id) + --meta:set_string("npc_owner", self.npc_id) -- Add place to list - npc.places.add_public(self, place_name, place_type, pos) + self.places_map[place_name] = {type=place_type, pos=pos, access_node=access_node or pos, status="owned"} + --npc.places.add_public(self, place_name, place_type, pos) return true - end - return false + --end + --return false end function npc.places.get_by_type(self, place_type) @@ -178,19 +180,26 @@ function npc.places.find_entrance_from_openable_nodes(all_openable_nodes, marker -- Check if there's any difference in vertical position -- minetest.log("Openable node pos: "..minetest.pos_to_string(open_pos)) -- minetest.log("Plotmarker node pos: "..minetest.pos_to_string(marker_pos)) - if start_pos.y ~= end_pos.y then + -- NOTE: Commented out while testing MarkBu's pathfinder + --if start_pos.y ~= end_pos.y then -- Adjust to make pathfinder find nodes one node above - end_pos.y = start_pos.y - end + -- end_pos.y = start_pos.y + --end -- This adjustment allows the map to be created correctly - start_pos.y = start_pos.y + 1 - end_pos.y = end_pos.y + 1 + --start_pos.y = start_pos.y + 1 + --end_pos.y = end_pos.y + 1 -- Find path from the openable node to the plotmarker - local path = pathfinder.find_path(start_pos, end_pos, 20, {}) + --local path = pathfinder.find_path(start_pos, end_pos, 20, {}) + local entity = {} + 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) + --minetest.log("Found path: "..dump(path)) if path ~= nil then - minetest.log("Path distance: "..dump(#path)) + --minetest.log("Path distance: "..dump(#path)) -- Check if path length is less than the minimum found so far if #path < min then -- Set min to path length and the result to the currently found node diff --git a/depends.txt b/depends.txt index a3172c8..8e2a20c 100755 --- a/depends.txt +++ b/depends.txt @@ -1,3 +1,4 @@ default mobs intllib? +pathfinder diff --git a/dialogue.lua b/dialogue.lua index 2bb0f64..5d703a3 100644 --- a/dialogue.lua +++ b/dialogue.lua @@ -296,7 +296,7 @@ function npc.dialogue.process_dialogue(self, dialogue, player_name) -- Send dialogue line if dialogue.text then - minetest.chat_send_player(player_name, self.nametag..": "..dialogue.text) + minetest.chat_send_player(player_name, self.name..": "..dialogue.text) end -- Check if dialogue has responses. If it doesn't, unlock the actions diff --git a/npc.lua b/npc.lua index 2efad1a..f4c59fc 100755 --- a/npc.lua +++ b/npc.lua @@ -24,7 +24,11 @@ npc.direction = { north = 0, east = 1, south = 2, - west = 3 + west = 3, + north_east = 4, + north_west = 5, + south_east = 6, + south_west = 7 } npc.action_state = { @@ -41,7 +45,7 @@ function npc.get_entity_name(entity) if entity:is_player() then return entity:get_player_name() else - return entity:get_luaentity().nametag + return entity:get_luaentity().name end end @@ -149,11 +153,14 @@ function npc.initialize(entity, pos, is_lua_entity) ent.sex = npc.MALE end + -- Nametag is initialized to blank + ent.nametag = "" + -- Set name - ent.nametag = get_random_name(ent.sex) + ent.name = get_random_name(ent.sex) -- Set ID - ent.npc_id = tostring(math.random(1000, 9999))..":"..ent.nametag + ent.npc_id = tostring(math.random(1000, 9999))..":"..ent.name -- Initialize all gift data ent.gift_data = { @@ -325,33 +332,6 @@ function npc.initialize(entity, pos, is_lua_entity) local offer2 = npc.trade.create_custom_sell_trade_offer("Do you want me to fix your mese sword?", "Fix mese sword", "Fix mese sword", "default:sword_mese", {"default:sword_mese", "default:copper_lump 10"}) table.insert(ent.trader_data.custom_trades, offer2) - -- Add a simple schedule for testing - npc.create_schedule(ent, npc.schedule_types.generic, 0) - -- Add schedule entries - local morning_actions = { - [1] = {task = npc.actions.cmd.WALK_TO_POS, args = {end_pos=nodes[1], walkable={}} } , - [2] = {task = npc.actions.cmd.USE_SITTABLE, args = {pos=nodes[1], action=npc.actions.const.sittable.SIT} }, - [3] = {action = npc.actions.cmd.FREEZE, args = {freeze = true}} - } - --npc.add_schedule_entry(ent, npc.schedule_types.generic, 0, 7, nil, morning_actions) - --local afternoon_actions = { [1] = {action = npc.actions.stand, args = {}} } - local afternoon_actions = {[1] = {task = npc.actions.cmd.USE_SITTABLE, args = {pos=nodes[1], action=npc.actions.const.sittable.GET_UP} } } - --npc.add_schedule_entry(ent, npc.schedule_types.generic, 0, 9, nil, afternoon_actions) - -- local night_actions = {action: npc.action, args: {}} - -- npc.add_schedule_entry(self, npc.schedule_type.generic, 0, 19, check, actions) - - -- npc.add_action(ent, npc.action.stand, {self = ent}) - -- npc.add_action(ent, npc.action.stand, {self = ent}) - -- npc.add_action(ent, npc.action.walk_step, {self = ent, dir = npc.direction.east}) - -- npc.add_action(ent, npc.action.walk_step, {self = ent, dir = npc.direction.east}) - -- npc.add_action(ent, npc.action.walk_step, {self = ent, dir = npc.direction.east}) - -- npc.add_action(ent, npc.action.walk_step, {self = ent, dir = npc.direction.east}) - -- npc.add_action(ent, npc.action.walk_step, {self = ent, dir = npc.direction.east}) - -- npc.add_action(ent, npc.action.walk_step, {self = ent, dir = npc.direction.east}) - -- npc.add_action(ent, npc.action.sit, {self = ent}) - -- npc.add_action(ent, npc.action.rotate, {self = ent, dir = npc.direction.south}) - -- npc.add_action(ent, npc.action.lay, {self = ent}) - -- Temporary initialization of places -- local bed_nodes = npc.places.find_new_nearby(ent, npc.places.nodes.BEDS, 8) -- minetest.log("Number of bed nodes: "..dump(#bed_nodes)) @@ -360,7 +340,7 @@ function npc.initialize(entity, pos, is_lua_entity) -- end --minetest.log(dump(ent)) - minetest.log("Successfully spawned NPC with name "..dump(ent.nametag)) + minetest.log("[advanced_npc] INFO Successfully initialized NPC with name "..dump(ent.name)) -- Refreshes entity ent.object:set_properties(ent) end @@ -530,7 +510,7 @@ end function npc.execute_action(self) -- Check if an action was interrupted if self.actions.current_action_state == npc.action_state.interrupted then - minetest.log("[advanced_npc] DEBUG Re-inserting interrupted action for NPC: '"..dump(self.nametag).."': "..dump(self.actions.state_before_lock.interrupted_action)) + minetest.log("[advanced_npc] DEBUG Re-inserting interrupted action for NPC: '"..dump(self.name).."': "..dump(self.actions.state_before_lock.interrupted_action)) -- Insert into queue the interrupted action table.insert(self.actions.queue, 1, self.actions.state_before_lock.interrupted_action) -- Clear the action @@ -552,7 +532,7 @@ function npc.execute_action(self) -- If the entry is a task, then push all this new operations in -- stack fashion if action_obj.is_task == true then - minetest.log("[advanced_npc] DEBUG Executing task for NPC '"..dump(self.nametag).."': "..dump(action_obj)) + minetest.log("[advanced_npc] DEBUG Executing task for NPC '"..dump(self.name).."': "..dump(action_obj)) -- Backup current queue local backup_queue = self.actions.queue -- Remove this "task" action from queue @@ -568,7 +548,7 @@ function npc.execute_action(self) table.insert(self.actions.queue, backup_queue[i]) end else - minetest.log("[advanced_npc] DEBUG Executing action for NPC '"..dump(self.nametag).."': "..dump(action_obj)) + minetest.log("[advanced_npc] DEBUG Executing action for NPC '"..dump(self.name).."': "..dump(action_obj)) -- Store the action that is being executed self.actions.state_before_lock.interrupted_action = action_obj -- Store current position @@ -855,7 +835,7 @@ mobs:register_mob("advanced_npc:npc", { -- Show dialogue to confirm that player is giving item as gift npc.dialogue.show_yes_no_dialogue( self, - "Do you want to give "..item_name.." to "..self.nametag.."?", + "Do you want to give "..item_name.." to "..self.name.."?", npc.dialogue.POSITIVE_GIFT_ANSWER_PREFIX..item_name, function() npc.relationships.receive_gift(self, clicker) @@ -937,7 +917,7 @@ mobs:register_mob("advanced_npc:npc", { -- Check if NPC is walking if self.actions.walking.is_walking == true then local pos = self.actions.walking.target_pos - self.object:moveto({x=pos.x, y=pos.y + 1, z=pos.z}) + self.object:moveto({x=pos.x, y=pos.y + 0.5, z=pos.z}) end -- Execute action self.freeze = npc.execute_action(self) diff --git a/spawner.lua b/spawner.lua index a283efc..f38da63 100644 --- a/spawner.lua +++ b/spawner.lua @@ -63,6 +63,58 @@ npc.spawner.spawn_data = { } } +local function get_basic_schedule() + return { + -- Create schedule entries + -- Morning actions: get out of bed, walk to outside of house + -- This will be executed around 8 AM MTG time + morning_actions = { + -- Get out of bed + [1] = {task = npc.actions.cmd.USE_BED, args = { + pos = npc.places.PLACE_TYPE.BED.PRIMARY, + action = npc.actions.const.beds.GET_UP + } + }, + -- Walk outside + [2] = {task = npc.actions.cmd.WALK_TO_POS, args = { + end_pos = npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, + walkable = {} + } + }, + -- Allow mobs_redo wandering + [3] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}} + }, + -- Afternoon actions: go inside the house + -- This will be executed around 6 PM MTG time + afternoon_actions = { + -- Get inside home + [1] = {task = npc.actions.cmd.WALK_TO_POS, args = { + end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, + walkable = {}} + }, + -- Allow mobs_redo wandering + [2] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}} + }, + -- Evening actions: walk to bed and use it. + -- This will be executed around 10 PM MTG time + evening_actions = { + [1] = {task = npc.actions.cmd.WALK_TO_POS, args = { + end_pos = {place_type=npc.places.PLACE_TYPE.BED.PRIMARY, use_access_node=true}, + walkable = {} + } + }, + -- Use bed + [2] = {task = npc.actions.cmd.USE_BED, args = { + pos = npc.places.PLACE_TYPE.BED.PRIMARY, + action = npc.actions.const.beds.LAY + } + }, + -- Stay put on bed + [3] = {action = npc.actions.cmd.FREEZE, args = {freeze = true}} + } + } +end + --------------------------------------------------------------------------------------- -- Scanning functions --------------------------------------------------------------------------------------- @@ -70,7 +122,7 @@ npc.spawner.spawn_data = { function spawner.filter_first_floor_nodes(nodes, ground_pos) local result = {} for _,node in pairs(nodes) do - if node.node_pos.y <= ground_pos.y + 3 then + if node.node_pos.y <= ground_pos.y + 2 then table.insert(result, node) end end @@ -130,40 +182,80 @@ end -- - Let the NPC know all doors to the house. Identify the front one as the entrance function spawner.assign_places(self, pos) local meta = minetest.get_meta(pos) - local doors = minetest.deserialize(meta:get_string("node_data")).openable_type - minetest.log("Found "..dump(#doors).." openable nodes") + local entrance = minetest.deserialize(meta:get_string("entrance")) + local node_data = minetest.deserialize(meta:get_string("node_data")) - local entrance = npc.places.find_entrance_from_openable_nodes(doors, pos) - if entrance then - minetest.log("Found building entrance at: "..minetest.pos_to_string(entrance.node_pos)) - else - minetest.log("Unable to find building entrance!") - end + -- Assign plotmarker + npc.places.add_public(self, npc.places.PLACE_TYPE.OTHER.HOME_PLOTMARKER, + npc.places.PLACE_TYPE.OTHER.HOME_PLOTMARKER, pos) -- Assign entrance door and related locations if entrance ~= nil and entrance.node_pos ~= nil then - -- For debug purposes: - --local meta = minetest.get_meta(entrance.node_pos) - --meta:set_string("infotext", "Entrance for '"..dump(self.nametag).."': "..dump(entrance.node_pos)) - --minetest.log("Self: "..dump(self)) - --minetest.log("Places map: "..dump(self.places_map)) npc.places.add_public(self, npc.places.PLACE_TYPE.OPENABLE.HOME_ENTRANCE_DOOR, npc.places.PLACE_TYPE.OPENABLE.HOME_ENTRANCE_DOOR, entrance.node_pos) -- Find the position inside and outside the door local entrance_inside = npc.places.find_node_behind_door(entrance.node_pos) local entrance_outside = npc.places.find_node_in_front_of_door(entrance.node_pos) - --minetest.set_node(entrance_inside, {name="default:apple"}) -- Assign these places to NPC npc.places.add_public(self, npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, entrance_inside) npc.places.add_public(self, npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, entrance_outside) - -- Make NPC go into their house - --minetest.log("Place: "..dump(npc.places.get_by_type(self, npc.places.PLACE_TYPE.OTHER.HOME_INSIDE))) - npc.add_task(self, npc.actions.cmd.WALK_TO_POS, {end_pos=npc.places.get_by_type(self, npc.places.PLACE_TYPE.OTHER.HOME_INSIDE)[1].pos, walkable={}}) - npc.add_action(self, npc.actions.cmd.FREEZE, {freeze = false}) end - local plot_info = minetest.deserialize(meta:get_string("plot_info")) - minetest.log("Plot info:"..dump(plot_info)) + -- Assign beds + if #node_data.bed_type > 0 then + -- Find unowned bed + for i = 1, #node_data.bed_type do + -- Check if bed has owner + minetest.log("Node: "..dump(node_data.bed_type[i])) + if node_data.bed_type[i].owner == "" then + -- If bed has no owner, check if it is accessible + local empty_nodes = npc.places.find_node_orthogonally( + node_data.bed_type[i].node_pos, {"air"}, 0) + -- Check if bed is accessible + if #empty_nodes > 0 then + -- Set owner to this NPC + node_data.bed_type[i].owner = self.npc_id + -- Assign node to NPC + npc.places.add_owned(self, npc.places.PLACE_TYPE.BED.PRIMARY, + npc.places.PLACE_TYPE.BED.PRIMARY, node_data.bed_type[i].node_pos, empty_nodes[1].pos) + -- Store changes to node_data + meta:set_string("node_data", minetest.serialize(node_data)) + minetest.log("Added bed at "..minetest.pos_to_string(node_data.bed_type[i].node_pos) + .." to NPC "..dump(self.name)) + break + end + end + end + end + --local plot_info = minetest.deserialize(meta:get_string("plot_info")) + --minetest.log("Plot info:"..dump(plot_info)) + minetest.log("places: "..dump(self.places_map)) + + -- Make NPC go into their house + npc.add_task(self, + npc.actions.cmd.WALK_TO_POS, + {end_pos=npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, + walkable={}}) + npc.add_action(self, npc.actions.cmd.FREEZE, {freeze = false}) +end + + +function spawner.assign_schedules(self, pos) + local basic_schedule = get_basic_schedule() + -- Add a simple schedule for testing + npc.create_schedule(self, npc.schedule_types.generic, 0) + -- Add schedule entry for morning actions + npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 8, nil, basic_schedule.morning_actions) + + -- Add schedule entry for afternoon actions + npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 18, nil, basic_schedule.afternoon_actions) + + -- Add schedule entry for evening actions + npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 22, nil, basic_schedule.evening_actions) + + minetest.log("Schedules: "..dump(self.schedules)) + --local afternoon_actions = { [1] = {action = npc.actions.stand, args = {}} } + --local afternoon_actions = {[1] = {task = npc.actions.cmd.USE_SITTABLE, args = {pos=nodes[1], action=npc.actions.const.sittable.GET_UP} } } end @@ -187,6 +279,8 @@ function npc.spawner.spawn_npc(pos) npc.initialize(ent, pos) -- Assign nodes spawner.assign_places(ent:get_luaentity(), pos) + -- Assign schedules + spawner.assign_schedules(ent:get_luaentity(), pos) -- Increase NPC spawned count spawned_npc_count = spawned_npc_count + 1 -- Store count into node @@ -196,8 +290,10 @@ function npc.spawner.spawn_npc(pos) -- TODO: Add more information here at some time... local entry = { status = npc.spawner.spawn_data.status.alive, - name = ent:get_luaentity().nametag, + name = ent:get_luaentity().name, id = ent:get_luaentity().npc_id, + sex = ent:get_luaentity().sex, + age = ent:get_luaentity(). born_day = minetest.get_day_count() } table.insert(npc_table, entry) @@ -205,16 +301,17 @@ function npc.spawner.spawn_npc(pos) meta:set_string("npcs", minetest.serialize(npc_table)) -- Temp meta:set_string("infotext", meta:get_string("infotext")..", "..spawned_npc_count) - minetest.log("[advanced_npc] Spawning successful!") + minetest.log("[advanced_npc] INFO Spawning successful!") -- Check if there are more NPCs to spawn if spawned_npc_count >= npc_count then -- Stop timer - minetest.log("[advanced_npc] No more NPCs to spawn at this location") + minetest.log("[advanced_npc] INFO No more NPCs to spawn at this location") timer:stop() else -- Start another timer to spawn more NPC - minetest.log("[advanced_npc] Spawning one more NPC in "..dump(npc.spawner.spawn_delay).."s") - timer:start(npc.spawner.spawn_delay) + local new_delay = math.random(npc.spawner.spawn_delay) + minetest.log("[advanced_npc] INFO Spawning one more NPC in "..dump(npc.spawner.spawn_delay).."s") + timer:start(new_delay) end return true else @@ -242,7 +339,8 @@ function spawner.calculate_npc_spawning(pos) return end -- Check number of beds - local beds_count = #node_data.bed_type + local beds_count = #node_data.bed_type--#spawner.filter_first_floor_nodes(node_data.bed_type, pos) + minetest.log("[advanced_npc] INFO: Found "..dump(beds_count).." beds in the building at "..minetest.pos_to_string(pos)) local npc_count = 0 -- If number of beds is zero or beds/2 is less than one, spawn @@ -251,17 +349,18 @@ function spawner.calculate_npc_spawning(pos) -- Spawn a single NPC npc_count = 1 else - -- Spawn beds_count/2 NPCs + -- Spawn (beds_count/2) NPCs npc_count = ((beds_count / 2) - ((beds_count / 2) % 1)) end - minetest.log("Will spawn "..dump(npc_count).." NPCs at "..minetest.pos_to_string(pos)) + minetest.log("[advanced_npc] INFO: Will spawn "..dump(npc_count).." NPCs at "..minetest.pos_to_string(pos)) -- Store amount of NPCs to spawn meta:set_int("npc_count", npc_count) -- Store amount of NPCs spawned meta:set_int("spawned_npc_count", 0) -- Start timer local timer = minetest.get_node_timer(pos) - timer:start(npc.spawner.spawn_delay) + local delay = math.random(npc.spawner.spawn_delay) + timer:start(delay) end --------------------------------------------------------------------------------------- @@ -273,9 +372,9 @@ end -- point and the building_data to get the x, y and z-coordinate size -- of the building schematic function spawner.scan_mg_villages_building(pos, building_data) - minetest.log("--------------------------------------------") - minetest.log("Building data: "..dump(building_data)) - minetest.log("--------------------------------------------") + --minetest.log("--------------------------------------------") + --minetest.log("Building data: "..dump(building_data)) + --minetest.log("--------------------------------------------") -- Get area of the building local x_size = building_data.bsizex local y_size = building_data.ysize @@ -285,6 +384,7 @@ function spawner.scan_mg_villages_building(pos, building_data) local x_sign, z_sign = 1, 1 -- Check plot direction + -- NOTE: Below values may be wrong, very wrong! -- 0 - facing West, -X -- 1 - facing North, +Z -- 2 - facing East, +X @@ -370,6 +470,17 @@ function spawner.replace_mg_villages_plotmarker(pos) meta:set_string("plot_info", minetest.serialize(plot_info)) -- Scan building for nodes local nodedata = spawner.scan_mg_villages_building(pos, plot_info) + -- Find building entrance + local doors = nodedata.openable_type + --minetest.log("Found "..dump(#doors).." openable nodes") + local entrance = npc.places.find_entrance_from_openable_nodes(doors, pos) + if entrance then + minetest.log("Found building entrance at: "..minetest.pos_to_string(entrance.node_pos)) + else + minetest.log("Unable to find building entrance!") + end + -- Store building entrance + meta:set_string("entrance", minetest.serialize(entrance)) -- Store nodedata into the spawner's metadata meta:set_string("node_data", minetest.serialize(nodedata)) -- Initialize NPCs @@ -399,13 +510,26 @@ if minetest.get_modpath("mg_villages") ~= nil then type = "fixed", fixed = { {-0.5+2/16, -0.5, -0.5+2/16, 0.5-2/16, -0.5+2/16, 0.5-2/16}, + --{-0.5+0/16, -0.5, -0.5+0/16, 0.5-0/16, -0.5+0/16, 0.5-0/16}, } }, + walkable = false, groups = {cracky=3,stone=2}, on_rightclick = function(pos, node, clicker, itemstack, pointed_thing) - -- Get all openable-type nodes for this building -- NOTE: This is temporary code for testing... + local nodedata = minetest.deserialize(minetest.get_meta(pos):get_string("node_data")) + --minetest.log("Node data: "..dump(nodedata)) + --minetest.log("Entrance: "..dump(minetest.deserialize(minetest.get_meta(pos):get_string("entrance")))) + --minetest.log("First-floor beds: "..dump(spawner.filter_first_floor_nodes(nodedata.bed_type, pos))) + --local entrance = npc.places.find_entrance_from_openable_nodes(nodedata.openable_type, pos) + --minetest.log("Found entrance: "..dump(entrance)) + + for i = 1, #nodedata.bed_type do + nodedata.bed_type[i].owner = "" + end + minetest.get_meta(pos):set_string("node_data", minetest.serialize(nodedata)) + minetest.log("Cleared bed owners") return mg_villages.plotmarker_formspec( pos, nil, {}, clicker ) end, @@ -450,8 +574,8 @@ if minetest.get_modpath("mg_villages") ~= nil then minetest.register_abm({ label = "Replace mg_villages:plotmarker with Advanced NPC auto spawners", nodenames = {"mg_villages:plotmarker"}, - interval = npc.spawner.replacement_interval, - chance = 5, + interval = 10,--npc.spawner.replacement_interval, + chance = 1, --5, catch_up = true, action = function(pos, node, active_object_count, active_object_count_wider) -- Check if replacement is activated diff --git a/trade/trade.lua b/trade/trade.lua index 186e1bf..9234d1c 100644 --- a/trade/trade.lua +++ b/trade/trade.lua @@ -112,7 +112,7 @@ function npc.trade.show_trade_offer_formspec(self, player, offer_type) default.gui_bg.. default.gui_bg_img.. default.gui_slots.. - "label[2,0.1;"..self.nametag..prompt_string.."]".. + "label[2,0.1;"..self.name..prompt_string.."]".. "item_image_button[2,1.3;1.2,1.2;"..trade_offer.item..";item;]".. "label[3.75,1.75;"..for_string.."]".. "item_image_button[4.8,1.3;1.2,1.2;"..trade_offer.price[1]..";price;]".. @@ -221,7 +221,7 @@ function npc.trade.show_custom_trade_offer(self, player, offer) default.gui_bg.. default.gui_bg_img.. default.gui_slots.. - "label[2,0.1;"..self.nametag..": "..offer.dialogue_prompt.."]".. + "label[2,0.1;"..self.name..": "..offer.dialogue_prompt.."]".. price_grid.. "label["..(margin_x + 3.75)..",1.75;"..for_string.."]".. "item_image_button["..(margin_x + 4.8)..",1.3;1.2,1.2;"..offer.item..";item;]"..