diff --git a/actions/actions.lua b/actions/actions.lua index 5b9cb5d..fc02822 100644 --- a/actions/actions.lua +++ b/actions/actions.lua @@ -13,6 +13,8 @@ npc.actions = {} +npc.actions.default_interval = 1 + -- Describes actions with doors or openable nodes npc.actions.const = { doors = { @@ -50,7 +52,9 @@ npc.actions.cmd = { USE_FURNACE = 11, USE_BED = 12, USE_SITTABLE = 13, - WALK_TO_POS = 14 + WALK_TO_POS = 14, + DIG = 15, + PLACE = 16 } --npc.actions.one_nps_speed = 0.98 @@ -60,6 +64,10 @@ npc.actions.one_nps_speed = 1 npc.actions.one_half_nps_speed = 1.5 npc.actions.two_nps_speed = 2 +npc.actions.take_from_inventory = "take_from_inventory" +npc.actions.take_from_inventory_forced = "take_from_inventory_forced" +npc.actions.force_place = "force_place" + -- Executor -- -------------- -- Function references aren't reliable in Minetest entities. Objects get serialized @@ -115,6 +123,12 @@ function npc.actions.execute(self, command, args) -- Call walk to position task --minetest.log("Self: "..dump(self)..", Command: "..dump(command)..", args: "..dump(args)) return npc.actions.walk_to_pos(self, args) + elseif command == npc.actions.cmd.DIG then + -- Call dig node action + return npc.actions.dig(self, args) + elseif command == npc.actions.cmd.PLACE then + -- Call place node action + return npc.actions.place(self, args) end end @@ -146,6 +160,85 @@ function npc.actions.freeze(self, args) return not(freeze_mobs_api) end +-- This action digs the node at the given position +-- If 'add_to_inventory' is true, it will put the digged node in the NPC +-- inventory. +-- Returns true if dig is successful, otherwise false +function npc.actions.dig(self, args) + local pos = args.pos + local add_to_inventory = args.add_to_inventory + local bypass_protection = args.bypass_protection + local node = minetest.get_node_or_nil(pos) + if node then + -- Check if protection not enforced + if not force_dig then + -- Try to dig node + if minetest.dig_node(pos) then + -- Add to inventory the node drops + if add_to_inventory then + -- Get node drop + local drop = minetest.registered_nodes[node.name].drop + -- Add to NPC inventory + npc.npc.add_item_to_inventory(self, drop, 1) + end + return true + end + else + -- Add to inventory + if add_to_inventory then + -- Get node drop + local drop = minetest.registered_nodes[node.name].drop + -- Add to NPC inventory + npc.npc.add_item_to_inventory(self, drop, 1) + end + -- Dig node + minetest.set_node(pos, {name="air"}) + end + end + return false +end + + +-- This action places a given node at the given position +-- There are three ways to source the node: +-- 1. take_from_inventory: takes node from inventory. If not in inventory, +-- node isn't placed. +-- 2. take_from_inventory_forced: takes node from inventory. If not in +-- inventory, node will be placed anyways. +-- 3. force_place: places node regardless of inventory - will not touch +-- the NPCs inventory +function npc.actions.place(self, args) + local pos = args.pos + local node = args.node + local source = args.source + local bypass_protection = args.bypass_protection + local node_at_pos = minetest.get_node_or_nil(pos) + -- Check if position is empty or has a node that can be built to + if node_at_pos and + (node_at_pos.name == "air" or minetest.registered_nodes(node_at_pos.name).buildable_to == true) then + -- Check protection + if (not bypass_protection and not minetest.is_protected(pos, self.npc_name)) + or bypass_protection == true then + -- Take from inventory if necessary + local place_item = false + if source == npc.actions.take_from_inventory then + if npc.take_item_from_inventory(self, node.name, 1) then + place_item = true + end + elseif source == npc.actions.take_from_inventory_forced then + npc.take_item_from_inventory(self, node.name, 1) + place_item = true + elseif source == npc.actions.force_place then + place_item = true + end + -- Place node + if place_item then + minetest.set_node(pos, node) + end + end + end +end + -- This action is to rotate to mob to a specifc direction. Currently, the code -- contains also for diagonals, but remaining in the orthogonal domain is preferrable. function npc.actions.rotate(self, args) diff --git a/actions/places.lua b/actions/places.lua index 1cc787b..af1b076 100644 --- a/actions/places.lua +++ b/actions/places.lua @@ -13,70 +13,73 @@ npc.places = {} npc.places.nodes = { - BED_TYPE = { - "beds:bed_bottom", - "beds:fancy_bed_bottom", - "cottages:bed_foot", - "cottages:straw_mat", - "cottages:sleeping_mat" - }, - SITTABLE_TYPE = { - "cottages:bench", - -- Currently commented out since some NPCs - -- were sitting at stairs that are actually staircases - -- TODO: Register other stair types - --"stairs:stair_wood" - }, - STORAGE_TYPE = { - "default:chest", - "default:chest_locked", - "cottages:shelf" - }, - FURNACE_TYPE = { - "default:furnace", - "default:furnace_active" - }, - OPENABLE_TYPE = { - -- TODO: Register fences - "doors:door_glass_a", - "doors:door_glass_b", - "doors:door_obsidian_a", - "doors:door_obsidian_b", - "doors:door_steel_a", - "doors:door_steel_b", - "doors:door_wood_a", - "doors:door_wood_b", - "cottages:gate_open", - "cottages:gate_closed", - "cottages:half_door" - } + BED_TYPE = { + "beds:bed_bottom", + "beds:fancy_bed_bottom", + "cottages:bed_foot", + "cottages:straw_mat", + "cottages:sleeping_mat" + }, + SITTABLE_TYPE = { + "cottages:bench", + -- Currently commented out since some NPCs + -- were sitting at stairs that are actually staircases + -- TODO: Register other stair types + --"stairs:stair_wood" + }, + STORAGE_TYPE = { + "default:chest", + "default:chest_locked", + "cottages:shelf" + }, + FURNACE_TYPE = { + "default:furnace", + "default:furnace_active" + }, + OPENABLE_TYPE = { + -- TODO: Register fences + "doors:door_glass_a", + "doors:door_glass_b", + "doors:door_obsidian_a", + "doors:door_obsidian_b", + "doors:door_steel_a", + "doors:door_steel_b", + "doors:door_wood_a", + "doors:door_wood_b", + "cottages:gate_open", + "cottages:gate_closed", + "cottages:half_door" + } } npc.places.PLACE_TYPE = { - BED = { - PRIMARY = "bed_primary" - }, - SITTABLE = { - PRIMARY = "sit_primary", - SHARED = "sit_shared" - }, - FURNACE = { - PRIMARY = "furnace_primary", - SHARED = "furnace_shared" - }, - STORAGE = { - PRIMARY = "storage_primary", - SHARED = "storage_shared" - }, - OPENABLE = { - HOME_ENTRANCE_DOOR = "home_entrance_door" - }, - OTHER = { - HOME_PLOTMARKER = "home_plotmarker", - HOME_INSIDE = "home_inside", - HOME_OUTSIDE = "home_outside" - } + BED = { + PRIMARY = "bed_primary" + }, + SITTABLE = { + PRIMARY = "sit_primary", + SHARED = "sit_shared" + }, + FURNACE = { + PRIMARY = "furnace_primary", + SHARED = "furnace_shared" + }, + STORAGE = { + PRIMARY = "storage_primary", + SHARED = "storage_shared" + }, + OPENABLE = { + HOME_ENTRANCE_DOOR = "home_entrance_door" + }, + SCHEDULE = { + TARGET = "schedule_target_pos" + }, + OTHER = { + HOME_PLOTMARKER = "home_plotmarker", + HOME_INSIDE = "home_inside", + HOME_OUTSIDE = "home_outside" + } } function npc.places.add_shared(self, place_name, place_type, pos, access_node) @@ -84,94 +87,110 @@ function npc.places.add_shared(self, place_name, place_type, pos, access_node) end function npc.places.add_owned(self, place_name, place_type, pos, access_node) - self.places_map[place_name] = {type=place_type, pos=pos, access_node=access_node or pos, status="owned"} + self.places_map[place_name] = {type=place_type, pos=pos, access_node=access_node or pos, status="owned"} end -function npc.places.add_unowned_accessible_place(self, nodes, place_type) - for i = 1, #nodes do - -- Check if node has owner - if nodes[i].owner == "" then - -- If node has no owner, check if it is accessible - local empty_nodes = npc.places.find_node_orthogonally( - nodes[i].node_pos, {"air"}, 0) - -- Check if node is accessible - if #empty_nodes > 0 then - -- Set owner to this NPC - nodes[i].owner = self.npc_id - -- Assign node to NPC - npc.places.add_owned(self, place_type, place_type, - nodes[i].node_pos, empty_nodes[1].pos) - npc.log("DEBUG", "Added node at "..minetest.pos_to_string(nodes[i].node_pos) - .." to NPC "..dump(self.npc_name)) - break - end - end - end +function npc.places.add_owned_accessible_place(self, nodes, place_type) + for i = 1, #nodes do + -- Check if node has owner + if nodes[i].owner == "" then + -- If node has no owner, check if it is accessible + local empty_nodes = npc.places.find_node_orthogonally( + nodes[i].node_pos, {"air"}, 0) + -- Check if node is accessible + if #empty_nodes > 0 then + -- Set owner to this NPC + nodes[i].owner = self.npc_id + -- Assign node to NPC + npc.places.add_owned(self, place_type, place_type, + nodes[i].node_pos, empty_nodes[1].pos) + npc.log("DEBUG", "Added node at "..minetest.pos_to_string(nodes[i].node_pos) + .." to NPC "..dump(self.npc_name)) + break + end + end + end end -function npc.places.add_shared_accessible_place(self, nodes, place_type) - for i = 1, #nodes do - -- Check of not adding same owned sit - if nodes[i].owner ~= self.npc_id then - -- Check if it is accessible - local empty_nodes = npc.places.find_node_orthogonally( - nodes[i].node_pos, {"air"}, 0) - -- Check if bed is accessible - if #empty_nodes > 0 then - -- Assign node to NPC - npc.places.add_shared(self, place_type..dump(i), - place_type, nodes[i].node_pos, empty_nodes[1].pos) - end - end - end +-- Override flag allows to overwrite a place in the places_map. +-- The only valid use right now is for schedules - don't use this +-- anywhere else unless you have a position that changes over time. +function npc.places.add_shared_accessible_place(self, nodes, place_type, override) + if not override then + for i = 1, #nodes do + -- Check if not adding same owned place + if nodes[i].owner ~= self.npc_id then + -- Check if it is accessible + local empty_nodes = npc.places.find_node_orthogonally( + nodes[i].node_pos, {"air"}, 0) + -- Check if node is accessible + if #empty_nodes > 0 then + -- Assign node to NPC + npc.places.add_shared(self, place_type..dump(i), + place_type, nodes[i].node_pos, empty_nodes[1].pos) + end + end + end + elseif override then + -- Note: Nodes is only *one* node in case override = true + -- Check if it is accessible + local empty_nodes = npc.places.find_node_orthogonally( + nodes.node_pos, {"air"}, 0) + -- Check if node is accessible + if #empty_nodes > 0 then + -- Nodes is only one node + npc.places.add_shared(self, place_type, place_type, + nodes.node_pos, empty_nodes[1].pos) + end + end end function npc.places.get_by_type(self, place_type) local result = {} for place_name, place_entry in pairs(self.places_map) do if place_entry.type == place_type then - table.insert(result, place_entry) - end + table.insert(result, place_entry) + end end - return result + return result end -- This function searches on a squared are of the given radius -- for nodes of the given type. The type should be npc.places.nodes function npc.places.find_node_nearby(pos, type, radius) - -- Determine area points - local start_pos = {x=pos.x - radius, y=pos.y - 1, z=pos.z - radius} - local end_pos = {x=pos.x + radius, y=pos.y + 1, z=pos.z + radius} - -- Get nodes - local nodes = minetest.find_nodes_in_area(start_pos, end_pos, type) + -- Determine area points + local start_pos = {x=pos.x - radius, y=pos.y - 1, z=pos.z - radius} + local end_pos = {x=pos.x + radius, y=pos.y + 1, z=pos.z + radius} + -- Get nodes + local nodes = minetest.find_nodes_in_area(start_pos, end_pos, type) - return nodes + return nodes end -- TODO: This function can be improved to support a radius greater than 1. function npc.places.find_node_orthogonally(pos, nodes, y_adjustment) - -- Calculate orthogonal points - local points = {} - table.insert(points, {x=pos.x+1,y=pos.y+y_adjustment,z=pos.z}) - table.insert(points, {x=pos.x-1,y=pos.y+y_adjustment,z=pos.z}) - table.insert(points, {x=pos.x,y=pos.y+y_adjustment,z=pos.z+1}) - table.insert(points, {x=pos.x,y=pos.y+y_adjustment,z=pos.z-1}) - local result = {} - for _,point in pairs(points) do - local node = minetest.get_node(point) - --minetest.log("Found node: "..dump(node)..", at pos: "..dump(point)) - for _,node_name in pairs(nodes) do - if node.name == node_name then - table.insert(result, {name=node.name, pos=point, param2=node.param2}) - end - end - end - return result + -- Calculate orthogonal points + local points = {} + table.insert(points, {x=pos.x+1,y=pos.y+y_adjustment,z=pos.z}) + table.insert(points, {x=pos.x-1,y=pos.y+y_adjustment,z=pos.z}) + table.insert(points, {x=pos.x,y=pos.y+y_adjustment,z=pos.z+1}) + table.insert(points, {x=pos.x,y=pos.y+y_adjustment,z=pos.z-1}) + local result = {} + for _,point in pairs(points) do + local node = minetest.get_node(point) + --minetest.log("Found node: "..dump(node)..", at pos: "..dump(point)) + for _,node_name in pairs(nodes) do + if node.name == node_name then + table.insert(result, {name=node.name, pos=point, param2=node.param2}) + end + end + end + return result end function npc.places.find_node_in_area(start_pos, end_pos, type) - local nodes = minetest.find_nodes_in_area(start_pos, end_pos, type) - return nodes + local nodes = minetest.find_nodes_in_area(start_pos, end_pos, type) + return nodes end -- Specialized function to find doors that are an entrance to a building. @@ -180,90 +199,90 @@ end -- Based on this definition, other entrances aren't going to be used -- by the NPC to get into the building function npc.places.find_entrance_from_openable_nodes(all_openable_nodes, marker_pos) - local result = nil - local openable_nodes = {} - local min = 100 + local result = nil + local openable_nodes = {} + local min = 100 - -- Filter out all other openable nodes except MTG doors. - -- Why? For supported village types (which are: medieval, nore - -- and logcabin) all buildings use, as the main entrance, - -- a MTG door. Some medieval building have "half_doors" (like farms) - -- which NPCs love to confuse with the right building entrance. - for i = 1, #all_openable_nodes do - local name = minetest.get_node(all_openable_nodes[i].node_pos).name - local doors_st, doors_en = string.find(name, "doors:") - if doors_st ~= nil then - table.insert(openable_nodes, all_openable_nodes[i]) - end - end + -- Filter out all other openable nodes except MTG doors. + -- Why? For supported village types (which are: medieval, nore + -- and logcabin) all buildings use, as the main entrance, + -- a MTG door. Some medieval building have "half_doors" (like farms) + -- which NPCs love to confuse with the right building entrance. + for i = 1, #all_openable_nodes do + local name = minetest.get_node(all_openable_nodes[i].node_pos).name + local doors_st, doors_en = string.find(name, "doors:") + if doors_st ~= nil then + table.insert(openable_nodes, all_openable_nodes[i]) + end + end - for i = 1, #openable_nodes do + for i = 1, #openable_nodes do - local open_pos = openable_nodes[i].node_pos + local open_pos = openable_nodes[i].node_pos - -- Get node name - check if this node is a 'door'. The way to check - -- is by explicitly checking for 'door' string - local name = minetest.get_node(open_pos).name - local start_i, end_i = string.find(name, "door") + -- Get node name - check if this node is a 'door'. The way to check + -- is by explicitly checking for 'door' string + local name = minetest.get_node(open_pos).name + local start_i, end_i = string.find(name, "door") - if start_i ~= nil then - -- Define start and end pos - local start_pos = {x=open_pos.x, y=open_pos.y, z=open_pos.z} - local end_pos = {x=marker_pos.x, y=marker_pos.y, z=marker_pos.z} + if start_i ~= nil then + -- Define start and end pos + local start_pos = {x=open_pos.x, y=open_pos.y, z=open_pos.z} + local end_pos = {x=marker_pos.x, y=marker_pos.y, z=marker_pos.z} - -- 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)) - -- 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 + -- 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)) + -- 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 - -- This adjustment allows the map to be created correctly - --start_pos.y = start_pos.y + 1 - --end_pos.y = end_pos.y + 1 + -- This adjustment allows the map to be created correctly + --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 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 = npc.pathfinder.find_path(start_pos, end_pos, entity, false) - --minetest.log("Found path: "..dump(path)) - if path ~= nil then - --minetest.log("Path distance: "..dump(#path)) - -- 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 - min = #path - result = openable_nodes[i] - else - -- Specific check to prefer mtg's doors to cottages' doors. - -- The reason? Sometimes a cottages' door could be closer to the - -- plotmarker, but not being the building entrance. MTG doors - -- are usually the entrance... so yes, hackity hack. - -- Get the name of the currently mininum-distance door - min_node_name = minetest.get_node(result.node_pos).name - -- Check if this is a door from MTG's doors. - local doors_st, doors_en = string.find(name, "doors:") - -- Check if min-distance door is a cottages door - -- while we have a MTG door - if min_node_name == "cottages:half_door" and doors_st ~= nil then - --minetest.log("Assigned new door...") - min = #path - result = openable_nodes[i] - end - end - else - npc.log("ERROR", "Path not found to marker from "..minetest.pos_to_string(start_pos)) - end - end - end - -- Return result - return result + -- Find path from the openable node to the plotmarker + --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 = npc.pathfinder.find_path(start_pos, end_pos, entity, false) + --minetest.log("Found path: "..dump(path)) + if path ~= nil then + --minetest.log("Path distance: "..dump(#path)) + -- 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 + min = #path + result = openable_nodes[i] + else + -- Specific check to prefer mtg's doors to cottages' doors. + -- The reason? Sometimes a cottages' door could be closer to the + -- plotmarker, but not being the building entrance. MTG doors + -- are usually the entrance... so yes, hackity hack. + -- Get the name of the currently mininum-distance door + min_node_name = minetest.get_node(result.node_pos).name + -- Check if this is a door from MTG's doors. + local doors_st, doors_en = string.find(name, "doors:") + -- Check if min-distance door is a cottages door + -- while we have a MTG door + if min_node_name == "cottages:half_door" and doors_st ~= nil then + --minetest.log("Assigned new door...") + min = #path + result = openable_nodes[i] + end + end + else + npc.log("ERROR", "Path not found to marker from "..minetest.pos_to_string(start_pos)) + end + end + end + -- Return result + return result end -- Specialized function to find all sittable nodes supported by the @@ -271,27 +290,27 @@ end -- stairs nodes placed aren't meant to simulate benches, this function -- is necessary in order to find stairs that are meant to be benches. function npc.places.find_sittable_nodes_nearby(pos, radius) - local result = {} - -- Try to find sittable nodes - local nodes = npc.places.find_node_nearby(pos, npc.places.nodes.SITTABLE, radius) - -- Highly unorthodox check for emptinnes - if nodes[1] ~= nil then - for i = 1, #nodes do - -- Get node name, try to avoid using the staircase check if not a stair node - local node = minetest.get_node(nodes[i]) - local i1, i2 = string.find(node.name, "stairs:") - if i1 ~= nil then - if npc.places.is_in_staircase(nodes[i]) < 1 then - table.insert(result, nodes[i]) - end - else - -- Add node as it is sittable - table.insert(result, nodes[i]) - end - end - end - -- Return sittable nodes - return result + local result = {} + -- Try to find sittable nodes + local nodes = npc.places.find_node_nearby(pos, npc.places.nodes.SITTABLE, radius) + -- Highly unorthodox check for emptinnes + if nodes[1] ~= nil then + for i = 1, #nodes do + -- Get node name, try to avoid using the staircase check if not a stair node + local node = minetest.get_node(nodes[i]) + local i1, i2 = string.find(node.name, "stairs:") + if i1 ~= nil then + if npc.places.is_in_staircase(nodes[i]) < 1 then + table.insert(result, nodes[i]) + end + else + -- Add node as it is sittable + table.insert(result, nodes[i]) + end + end + end + -- Return sittable nodes + return result end -- Specialized function to find sittable stairs: stairs that don't @@ -300,104 +319,104 @@ end -- Receives a position of a stair node. npc.places.staircase = { - none = 0, - bottom = 1, - middle = 2, - top = 3 + none = 0, + bottom = 1, + middle = 2, + top = 3 } function npc.places.is_in_staircase(pos) - local node = minetest.get_node(pos) - -- Verify node is actually from default stairs mod - local p1, p2 = string.find(node.name, "stairs:") - if p1 ~= nil then - -- Calculate the logical position to the lower and upper stairs node location - local up_x_adj, up_z_adj = 0, 0 - local lo_x_adj, lo_z_adj = 0, 0 - if node.param2 == 1 then - up_z_adj = -1 - lo_z_adj = 1 - elseif node.param2 == 2 then - up_z_adj = 1 - lo_z_adj = -1 - elseif node.param2 == 3 then - up_x_adj = -1 - lo_x_adj = 1 - elseif node.param2 == 4 then - up_x_adj = 1 - lo_x_adj = -1 - else - -- This is not a staircase - return false - end + local node = minetest.get_node(pos) + -- Verify node is actually from default stairs mod + local p1, p2 = string.find(node.name, "stairs:") + if p1 ~= nil then + -- Calculate the logical position to the lower and upper stairs node location + local up_x_adj, up_z_adj = 0, 0 + local lo_x_adj, lo_z_adj = 0, 0 + if node.param2 == 1 then + up_z_adj = -1 + lo_z_adj = 1 + elseif node.param2 == 2 then + up_z_adj = 1 + lo_z_adj = -1 + elseif node.param2 == 3 then + up_x_adj = -1 + lo_x_adj = 1 + elseif node.param2 == 4 then + up_x_adj = 1 + lo_x_adj = -1 + else + -- This is not a staircase + return false + end - -- Calculate upper and lower position - local upper_pos = {x=pos.x + up_x_adj, y=pos.y + 1, z=pos.z + up_z_adj} - local lower_pos = {x=pos.x + lo_x_adj, y=pos.y - 1, z=pos.z + lo_z_adj} - -- Get next node - local upper_node = minetest.get_node(upper_pos) - local lower_node = minetest.get_node(lower_pos) - --minetest.log("Next node: "..dump(upper_pos)) - -- Check if next node is also a stairs node - local up_p1, up_p2 = string.find(upper_node.name, "stairs:") - local lo_p1, lo_p2 = string.find(lower_node.name, "stairs:") + -- Calculate upper and lower position + local upper_pos = {x=pos.x + up_x_adj, y=pos.y + 1, z=pos.z + up_z_adj} + local lower_pos = {x=pos.x + lo_x_adj, y=pos.y - 1, z=pos.z + lo_z_adj} + -- Get next node + local upper_node = minetest.get_node(upper_pos) + local lower_node = minetest.get_node(lower_pos) + --minetest.log("Next node: "..dump(upper_pos)) + -- Check if next node is also a stairs node + local up_p1, up_p2 = string.find(upper_node.name, "stairs:") + local lo_p1, lo_p2 = string.find(lower_node.name, "stairs:") - if up_p1 ~= nil then - -- By default, think this is bottom of staircase. - local result = npc.places.staircase.bottom - -- Try downwards now - if lo_p1 ~= nil then - result = npc.places.staircase.middle - end - return result - else - -- Check if there is a staircase downwards - if lo_p1 ~= nil then - return npc.places.staircase.top - else - return npc.places.staircase.none - end - end - end - -- This is not a stairs node - return nil + if up_p1 ~= nil then + -- By default, think this is bottom of staircase. + local result = npc.places.staircase.bottom + -- Try downwards now + if lo_p1 ~= nil then + result = npc.places.staircase.middle + end + return result + else + -- Check if there is a staircase downwards + if lo_p1 ~= nil then + return npc.places.staircase.top + else + return npc.places.staircase.none + end + end + end + -- This is not a stairs node + return nil end -- Specialized function to find the node position right behind -- a door. Used to make NPCs enter buildings. function npc.places.find_node_behind_door(door_pos) - local door = minetest.get_node(door_pos) - if door.param2 == 0 then - -- Looking south - return {x=door_pos.x, y=door_pos.y, z=door_pos.z + 1} - elseif door.param2 == 1 then - -- Looking east - return {x=door_pos.x + 1, y=door_pos.y, z=door_pos.z} - elseif door.param2 == 2 then - -- Looking north - return {x=door_pos.x, y=door_pos.y, z=door_pos.z - 1} - -- Looking west - elseif door.param2 == 3 then - return {x=door_pos.x - 1, y=door_pos.y, z=door_pos.z} - end + local door = minetest.get_node(door_pos) + if door.param2 == 0 then + -- Looking south + return {x=door_pos.x, y=door_pos.y, z=door_pos.z + 1} + elseif door.param2 == 1 then + -- Looking east + return {x=door_pos.x + 1, y=door_pos.y, z=door_pos.z} + elseif door.param2 == 2 then + -- Looking north + return {x=door_pos.x, y=door_pos.y, z=door_pos.z - 1} + -- Looking west + elseif door.param2 == 3 then + return {x=door_pos.x - 1, y=door_pos.y, z=door_pos.z} + end end -- Specialized function to find the node position right in -- front of a door. Used to make NPCs exit buildings. function npc.places.find_node_in_front_of_door(door_pos) - local door = minetest.get_node(door_pos) - --minetest.log("Param2 of door: "..dump(door.param2)) - if door.param2 == 0 then - -- Looking south - return {x=door_pos.x, y=door_pos.y, z=door_pos.z - 1} - elseif door.param2 == 1 then - -- Looking east - return {x=door_pos.x - 1, y=door_pos.y, z=door_pos.z} - elseif door.param2 == 2 then - -- Looking north - return {x=door_pos.x, y=door_pos.y, z=door_pos.z + 1} - -- Looking west - elseif door.param2 == 3 then - return {x=door_pos.x + 1, y=door_pos.y, z=door_pos.z} - end + local door = minetest.get_node(door_pos) + --minetest.log("Param2 of door: "..dump(door.param2)) + if door.param2 == 0 then + -- Looking south + return {x=door_pos.x, y=door_pos.y, z=door_pos.z - 1} + elseif door.param2 == 1 then + -- Looking east + return {x=door_pos.x - 1, y=door_pos.y, z=door_pos.z} + elseif door.param2 == 2 then + -- Looking north + return {x=door_pos.x, y=door_pos.y, z=door_pos.z + 1} + elseif door.param2 == 3 then + -- Looking west + return {x=door_pos.x + 1, y=door_pos.y, z=door_pos.z} + end end \ No newline at end of file diff --git a/init.lua b/init.lua index e7e9fdd..01c8fe2 100755 --- a/init.lua +++ b/init.lua @@ -34,10 +34,12 @@ dofile(path .. "/actions/actions.lua") dofile(path .. "/actions/places.lua") dofile(path .. "/actions/pathfinder.lua") dofile(path .. "/actions/node_registry.lua") +dofile(path .. "/occupations/occupations.lua") -- Load random data definitions dofile(path .. "/random_data.lua") dofile(path .. "/random_data/dialogues_data.lua") dofile(path .. "/random_data/gift_items_data.lua") dofile(path .. "/random_data/names_data.lua") +dofile(path .. "/random_data/occupations_data.lua") print (S("[Mod] Advanced NPC loaded")) diff --git a/npc.lua b/npc.lua index f620b9e..c65aa36 100755 --- a/npc.lua +++ b/npc.lua @@ -24,6 +24,8 @@ npc.ANIMATION_LAY_START = 162 npc.ANIMATION_LAY_END = 166 npc.ANIMATION_WALK_START = 168 npc.ANIMATION_WALK_END = 187 +npc.ANIMATION_MINE_START = 189 +npc.ANIMATION_MINE_END =198 npc.direction = { north = 0, @@ -171,10 +173,15 @@ function npc.get_random_texture_from_array(age, sex, textures) and string.find(current_texture, npc.age.child)) ) ) then - table.insert(filtered_textures, current_texture) + table.insert(filtered_textures, current_texture) end end + -- Check if there are no textures + if #filtered_textures == 0 then + return nil + end + return filtered_textures[math.random(1, #filtered_textures)] end @@ -226,114 +233,116 @@ end -- Spawn function. Initializes all variables that the -- NPC will have and choose random, starting values -function npc.initialize(entity, pos, is_lua_entity, npc_stats) - npc.log("INFO", "Initializing NPC at "..minetest.pos_to_string(pos)) +function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) + npc.log("INFO", "Initializing NPC at "..minetest.pos_to_string(pos)) - -- Get variables - local ent = entity - if not is_lua_entity then - ent = entity:get_luaentity() - end + -- Get variables + local ent = entity + if not is_lua_entity then + ent = entity:get_luaentity() + end - -- Avoid NPC to be removed by mobs_redo API - ent.remove_ok = false + -- Avoid NPC to be removed by mobs_redo API + ent.remove_ok = false - -- Determine sex and age - -- If there's no previous NPC data, sex and age will be randomly chosen. - -- - Sex: Female or male will have each 50% of spawning - -- - Age: 90% chance of spawning adults, 10% chance of spawning children. - -- If there is previous data then: - -- - Sex: The unbalanced sex will get a 75% chance of spawning - -- - Example: If there's one male, then female will have 75% spawn chance. - -- - If there's male and female, then each have 50% spawn chance. - -- - Age: For each two adults, the chance of spawning a child next will be 50% - -- If there's a child for two adults, the chance of spawning a child goes to - -- 40% and keeps decreasing unless two adults have no child. - if npc_stats then - -- Default chances - local male_s, male_e = 0, 50 - local female_s, female_e = 51, 100 - local adult_s, adult_e = 0, 90 - local child_s, child_e = 91, 100 - -- Determine sex probabilities - if npc_stats[npc.FEMALE].total > npc_stats[npc.MALE].total then - male_e = 75 - female_s, female_e = 76, 100 - elseif npc_stats[npc.FEMALE].total < npc_stats[npc.MALE].total then - male_e = 25 - female_s, female_e = 26, 100 - end - -- Determine age probabilities - if npc_stats["adult_total"] >= 2 then - if npc_stats["adult_total"] % 2 == 0 - and (npc_stats["adult_total"] / 2 > npc_stats["child_total"]) then - child_s,child_e = 26, 100 - adult_e = 25 - else - child_s, child_e = 61, 100 - adult_e = 60 - end - end - -- Get sex and age based on the probabilities - local sex_chance = math.random(1, 100) - local age_chance = math.random(1, 100) - local selected_sex = "" - local selected_age = "" - -- Select sex - if male_s <= sex_chance and sex_chance <= male_e then - selected_sex = npc.MALE - elseif female_s <= sex_chance and sex_chance <= female_e then - selected_sex = npc.FEMALE - end - -- Set sex for NPC - ent.sex = selected_sex - -- Select age - if adult_s <= age_chance and age_chance <= adult_e then - selected_age = npc.age.adult - elseif child_s <= age_chance and age_chance <= child_e then - selected_age = npc.age.child - ent.visual_size = { - x = 0.60, - y = 0.60 - } - ent.collisionbox = {-0.10,-0.50,-0.10, 0.10,0.40,0.10} - ent.is_child = true - -- For mobs_redo - ent.child = true - end - -- Store the selected age - ent.age = selected_age + -- Determine sex and age + -- If there's no previous NPC data, sex and age will be randomly chosen. + -- - Sex: Female or male will have each 50% of spawning + -- - Age: 90% chance of spawning adults, 10% chance of spawning children. + -- If there is previous data then: + -- - Sex: The unbalanced sex will get a 75% chance of spawning + -- - Example: If there's one male, then female will have 75% spawn chance. + -- - If there's male and female, then each have 50% spawn chance. + -- - Age: For each two adults, the chance of spawning a child next will be 50% + -- If there's a child for two adults, the chance of spawning a child goes to + -- 40% and keeps decreasing unless two adults have no child. + -- Use NPC stats if provided + if npc_stats then + -- Default chances + local male_s, male_e = 0, 50 + local female_s, female_e = 51, 100 + local adult_s, adult_e = 0, 85 + local child_s, child_e = 86, 100 + -- Determine sex probabilities + if npc_stats[npc.FEMALE].total > npc_stats[npc.MALE].total then + male_e = 75 + female_s, female_e = 76, 100 + elseif npc_stats[npc.FEMALE].total < npc_stats[npc.MALE].total then + male_e = 25 + female_s, female_e = 26, 100 + end + -- Determine age probabilities + if npc_stats["adult_total"] >= 2 then + if npc_stats["adult_total"] % 2 == 0 + and (npc_stats["adult_total"] / 2 > npc_stats["child_total"]) then + child_s,child_e = 26, 100 + adult_e = 25 + else + child_s, child_e = 61, 100 + adult_e = 60 + end + end + -- Get sex and age based on the probabilities + local sex_chance = math.random(1, 100) + local age_chance = math.random(1, 100) + local selected_sex = "" + local selected_age = "" + -- Select sex + if male_s <= sex_chance and sex_chance <= male_e then + selected_sex = npc.MALE + elseif female_s <= sex_chance and sex_chance <= female_e then + selected_sex = npc.FEMALE + end + -- Set sex for NPC + ent.sex = selected_sex + -- Select age + if adult_s <= age_chance and age_chance <= adult_e then + selected_age = npc.age.adult + elseif child_s <= age_chance and age_chance <= child_e then + selected_age = npc.age.child + ent.visual_size = { + x = 0.65, + y = 0.65 + } + ent.collisionbox = {-0.10,-0.50,-0.10, 0.10,0.40,0.10} + ent.is_child = true + -- For mobs_redo + ent.child = true + end + -- Store the selected age + ent.age = selected_age - -- Set texture accordingly - local selected_texture = get_random_texture(selected_sex, selected_age) - --minetest.log("Selected texture: "..dump(selected_texture)) - -- Store selected texture due to the need to restore it later - ent.selected_texture = selected_texture - -- Set texture and base texture - ent.textures = {selected_texture} - ent.base_texture = {selected_texture} - else - -- Get sex based on texture. This is a 50% chance for - -- each sex as there's same amount of textures for male and female. - -- Do not spawn child as first NPC - if (is_female_texture(ent.base_texture)) then - ent.sex = npc.FEMALE + -- Set texture accordingly + local selected_texture = get_random_texture(selected_sex, selected_age) + --minetest.log("Selected texture: "..dump(selected_texture)) + -- Store selected texture due to the need to restore it later + ent.selected_texture = selected_texture + -- Set texture and base texture + ent.textures = {selected_texture} + ent.base_texture = {selected_texture} else - ent.sex = npc.MALE + -- Get sex based on texture. This is a 50% chance for + -- each sex as there's same amount of textures for male and female. + -- Do not spawn child as first NPC + if (is_female_texture(ent.base_texture)) then + ent.sex = npc.FEMALE + else + ent.sex = npc.MALE + end + ent.age = npc.age.adult end - end - -- Nametag is initialized to blank - ent.nametag = "" + -- Nametag is initialized to blank + ent.nametag = "" - -- Set name - ent.npc_name = get_random_name(ent.sex) + -- Set name + ent.npc_name = get_random_name(ent.sex) - -- Set ID - ent.npc_id = tostring(math.random(1000, 9999))..":"..ent.npc_name + -- Set ID + ent.npc_id = tostring(math.random(1000, 9999))..":"..ent.npc_name - -- Initialize all gift data - ent.gift_data = { + -- Initialize all gift data + ent.gift_data = { -- Choose favorite items. Choose phase1 per default favorite_items = npc.relationships.select_random_favorite_items(ent.sex, "phase1"), -- Choose disliked items. Choose phase1 per default @@ -406,7 +415,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats) action_timer = 0, -- Determines the interval for each action in the action queue -- Default is 1. This can be changed via actions - action_interval = 1, + action_interval = npc.actions.default_interval, -- Avoid the execution of the action timer action_timer_lock = false, -- Defines the state of the current action @@ -441,57 +450,70 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats) -- Map entries should be like: "bed" = {x=1, y=1, z=1} ent.places_map = {} - -- Schedule data - ent.schedules = { - -- Flag to enable or disable the schedules functionality - enabled = true, - -- Lock for when executing a schedule - lock = false, - -- Queue of schedules executed - -- Used to calculate dependencies - temp_executed_queue = {}, - -- An array of schedules, meant to be one per day at some point - -- when calendars are implemented. Allows for only 7 schedules, - -- one for each day of the week - generic = {}, - -- An array of schedules, meant to be for specific dates in the - -- year. Can contain as many as possible. The keys will be strings - -- in the format MM:DD - date_based = {} - } + -- Schedule data + ent.schedules = { + -- Flag to enable or disable the schedules functionality + enabled = true, + -- Lock for when executing a schedule + lock = false, + -- Queue of schedules executed + -- Used to calculate dependencies + temp_executed_queue = {}, + -- An array of schedules, meant to be one per day at some point + --- when calendars are implemented. Allows for only 7 schedules, + -- one for each day of the week + generic = {}, + -- An array of schedules, meant to be for specific dates in the + -- year. Can contain as many as possible. The keys will be strings + -- in the format MM:DD + date_based = {}, + -- The following holds the check parameters provided by the + -- current schedule + current_check_params = {} + } - -- Dedicated trade test - ent.trader_data.trade_list.both = { - ["default:tree"] = {}, - ["default:cobble"] = {}, - ["default:wood"] = {}, - ["default:diamond"] = {}, - ["default:mese_crystal"] = {}, - ["flowers:rose"] = {}, - ["advanced_npc:marriage_ring"] = {}, - ["flowers:geranium"] = {}, - ["mobs:meat"] = {}, - ["mobs:leather"] = {}, - ["default:sword_stone"] = {}, - ["default:shovel_stone"] = {}, - ["default:axe_stone"] = {} - } + -- If occupation name given, override properties with + -- occupation values and initialize schedules + minetest.log("Entity age: "..dump(ent.age)..", afult? "..dump(ent.age==npc.age.adult)) + if occupation_name and occupation_name ~= "" and ent.age == npc.age.adult then + npc.occupations.initialize_occupation_values(ent, occupation_name) + end - npc.trade.generate_trade_offers_by_status(ent) + -- TODO: Remove this - do inside occupation + -- Dedicated trade test + ent.trader_data.trade_list.both = { + ["default:tree"] = {}, + ["default:cobble"] = {}, + ["default:wood"] = {}, + ["default:diamond"] = {}, + ["default:mese_crystal"] = {}, + ["flowers:rose"] = {}, + ["advanced_npc:marriage_ring"] = {}, + ["flowers:geranium"] = {}, + ["mobs:meat"] = {}, + ["mobs:leather"] = {}, + ["default:sword_stone"] = {}, + ["default:shovel_stone"] = {}, + ["default:axe_stone"] = {} + } - -- Add a custom trade offer - local offer1 = npc.trade.create_custom_sell_trade_offer("Do you want me to fix your steel sword?", "Fix steel sword", "Fix steel sword", "default:sword_steel", {"default:sword_steel", "default:iron_lump 5"}) - table.insert(ent.trader_data.custom_trades, offer1) - --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) + -- Generate trade offers + npc.trade.generate_trade_offers_by_status(ent) + -- Add a custom trade offer + -- local offer1 = npc.trade.create_custom_sell_trade_offer("Do you want me to fix your steel sword?", "Fix steel sword", "Fix steel sword", "default:sword_steel", {"default:sword_steel", "default:iron_lump 5"}) + -- table.insert(ent.trader_data.custom_trades, offer1) + --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) + + -- Set initialized flag on ent.initialized = true - --minetest.log(dump(ent)) - npc.log("INFO", "Successfully initialized NPC with name "..dump(ent.npc_name) - ..", sex: "..ent.sex..", is child: "..dump(ent.is_child) - ..", texture: "..dump(ent.textures)) - -- Refreshes entity - ent.object:set_properties(ent) + npc.log("WARNING", "Spawned entity: "..dump(ent)) + npc.log("INFO", "Successfully initialized NPC with name "..dump(ent.npc_name) + ..", sex: "..ent.sex..", is child: "..dump(ent.is_child) + ..", texture: "..dump(ent.textures)) + -- Refreshes entity + ent.object:set_properties(ent) end --------------------------------------------------------------------------------------- @@ -675,60 +697,69 @@ end -- This function removes the first action in the action queue -- and then executes it function npc.execute_action(self) - -- Check if an action was interrupted - if self.actions.current_action_state == npc.action_state.interrupted then - npc.log("DEBUG", "Re-inserting interrupted action for NPC: '"..dump(self.npc_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 - self.actions.state_before_lock.interrupted_action = {} - -- Clear the position - self.actions.state_before_lock.pos = {} - end - local result = nil - if table.getn(self.actions.queue) == 0 then - -- Set state to none - self.actions.current_action_state = npc.action_state.none - -- Keep state the same if there are no more actions in actions queue - return self.freeze - end - local action_obj = self.actions.queue[1] - if action_obj.action == nil then - return - end - -- If the entry is a task, then push all this new operations in - -- stack fashion - if action_obj.is_task == true then - npc.log("DEBUG", "Executing task for NPC '"..dump(self.npc_name).."': "..dump(action_obj)) - -- Backup current queue - local backup_queue = self.actions.queue - -- Remove this "task" action from queue - table.remove(self.actions.queue, 1) - -- Clear queue - self.actions.queue = {} - -- Now, execute the task with its arguments - result = npc.actions.execute(self, action_obj.action, action_obj.args) - --result = action_obj.action(self, action_obj.args) - -- After all new actions has been added by task, add the previously - -- queued actions back - for i = 1, #backup_queue do - table.insert(self.actions.queue, backup_queue[i]) - end - else - npc.log("DEBUG", "Executing action for NPC '"..dump(self.npc_name).."': "..dump(action_obj)) - -- Store the action that is being executed - self.actions.state_before_lock.interrupted_action = action_obj - -- Store current position - self.actions.state_before_lock.pos = self.object:getpos() - -- Execute action as normal - result = npc.actions.execute(self, action_obj.action, action_obj.args) - --result = action_obj.action(self, action_obj.args) - -- Remove task - table.remove(self.actions.queue, 1) - -- Set state - self.actions.current_action_state = npc.action_state.executing - end - return result + -- Check if an action was interrupted + if self.actions.current_action_state == npc.action_state.interrupted then + npc.log("DEBUG", "Re-inserting interrupted action for NPC: '"..dump(self.npc_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 + self.actions.state_before_lock.interrupted_action = {} + -- Clear the position + self.actions.state_before_lock.pos = {} + end + local result = nil + if table.getn(self.actions.queue) == 0 then + -- Set state to none + self.actions.current_action_state = npc.action_state.none + -- Keep state the same if there are no more actions in actions queue + return self.freeze + end + local action_obj = self.actions.queue[1] + -- Check if action is null + if action_obj.action == nil then + return + end + -- Check if action is an schedule check + if action_obj.action == "schedule_check" then + -- Execute schedule check + npc.schedule_check(self) + -- Remove table entry + table.remove(self.actions.queue, 1) + -- Return + return false + end + -- If the entry is a task, then push all this new operations in + -- stack fashion + if action_obj.is_task == true then + npc.log("DEBUG", "Executing task for NPC '"..dump(self.npc_name).."': "..dump(action_obj)) + -- Backup current queue + local backup_queue = self.actions.queue + -- Remove this "task" action from queue + table.remove(self.actions.queue, 1) + -- Clear queue + self.actions.queue = {} + -- Now, execute the task with its arguments + result = npc.actions.execute(self, action_obj.action, action_obj.args) + --result = action_obj.action(self, action_obj.args) + -- After all new actions has been added by task, add the previously + -- queued actions back + for i = 1, #backup_queue do + table.insert(self.actions.queue, backup_queue[i]) + end + else + npc.log("DEBUG", "Executing action for NPC '"..dump(self.npc_name).."': "..dump(action_obj)) + -- Store the action that is being executed + self.actions.state_before_lock.interrupted_action = action_obj + -- Store current position + self.actions.state_before_lock.pos = self.object:getpos() + -- Execute action as normal + result = npc.actions.execute(self, action_obj.action, action_obj.args) + -- Remove task + table.remove(self.actions.queue, 1) + -- Set state + self.actions.current_action_state = npc.action_state.executing + end + return result end function npc.lock_actions(self) @@ -858,8 +889,8 @@ function npc.create_schedule(self, schedule_type, date) end function npc.delete_schedule(self, schedule_type, date) - -- Delete schedule by setting entry to nil - self.schedules[schedule_type][date] = nil + -- Delete schedule by setting entry to nil + self.schedules[schedule_type][date] = nil end -- Schedule entries API @@ -869,96 +900,231 @@ end -- Actions is an array of actions and tasks that the NPC -- will perform at the scheduled time on the scheduled date function npc.add_schedule_entry(self, schedule_type, date, time, check, actions) - -- Check that schedule for date exists - if self.schedules[schedule_type][date] ~= nil then - -- Add schedule entry - if check == nil then - self.schedules[schedule_type][date][time] = actions + -- Check that schedule for date exists + if self.schedules[schedule_type][date] ~= nil then + -- Add schedule entry + if check == nil then + self.schedules[schedule_type][date][time] = actions + else + self.schedules[schedule_type][date][time].check = check + end else - self.schedules[schedule_type][date][time].check = check + -- No schedule found, need to be created for date + return nil end - else - -- No schedule found, need to be created for date - return nil - end end function npc.get_schedule_entry(self, schedule_type, date, time) - -- Check if schedule for date exists - if self.schedules[schedule_type][date] ~= nil then - -- Return schedule - return self.schedules[schedule_type][date][time] - else - -- Schedule for date not found - return nil - end + -- Check if schedule for date exists + if self.schedules[schedule_type][date] ~= nil then + -- Return schedule + return self.schedules[schedule_type][date][time] + else + -- Schedule for date not found + return nil + end end function npc.update_schedule_entry(self, schedule_type, date, time, check, actions) - -- Check schedule for date exists - if self.schedules[schedule_type][date] ~= nil then - -- Check that a schedule entry for that time exists - if self.schedules[schedule_type][date][time] ~= nil then - -- Set the new actions - if check == nil then - self.schedules[schedule_type][date][time] = actions - else - self.schedules[schedule_type][date][time].check = check - end + -- Check schedule for date exists + if self.schedules[schedule_type][date] ~= nil then + -- Check that a schedule entry for that time exists + if self.schedules[schedule_type][date][time] ~= nil then + -- Set the new actions + if check == nil then + self.schedules[schedule_type][date][time] = actions + else + self.schedules[schedule_type][date][time].check = check + end + else + -- Schedule not found for specified time + return nil + end else - -- Schedule not found for specified time - return nil + -- Schedule not found for date + return nil end - else - -- Schedule not found for date - return nil - end end function npc.delete_schedule_entry(self, schedule_type, date, time) - -- Check schedule for date exists - if self.schedules[schedule_type][date] ~= nil then - -- Remove schedule entry by setting to nil - self.schedules[schedule_type][date][time] = nil - else - -- Schedule not found for date - return nil - end + -- Check schedule for date exists + if self.schedules[schedule_type][date] ~= nil then + -- Remove schedule entry by setting to nil + self.schedules[schedule_type][date][time] = nil + else + -- Schedule not found for date + return nil + end end function npc.schedule_change_property(self, property, args) - if property == npc.schedule_properties.trader_status then - -- Get status from args - local status = args.status - -- Set status to NPC - npc.set_trading_status(self, status) - elseif property == npc.schedule_properties.put_item then - local itemstring = args.itemstring - -- Add item - npc.add_item_to_inventory_itemstring(self, itemstring) - elseif property == npc.schedule_properties.put_multiple_items then - local itemlist = args.itemlist - for i = 1, #itemlist do - local itemlist_entry = itemlist[i] - local current_itemstring = itemlist[i].name - if itemlist_entry.random == true then + if property == npc.schedule_properties.trader_status then + -- Get status from args + local status = args.status + -- Set status to NPC + npc.set_trading_status(self, status) + elseif property == npc.schedule_properties.put_item then + local itemstring = args.itemstring + -- Add item + npc.add_item_to_inventory_itemstring(self, itemstring) + elseif property == npc.schedule_properties.put_multiple_items then + local itemlist = args.itemlist + for i = 1, #itemlist do + local itemlist_entry = itemlist[i] + local current_itemstring = itemlist[i].name + if itemlist_entry.random == true then current_itemstring = current_itemstring - .." "..dump(math.random(itemlist_entry.min, itemlist_entry.max)) + .." "..dump(math.random(itemlist_entry.min, itemlist_entry.max)) else current_itemstring = current_itemstring.." "..tostring(itemlist_entry.count) - end - -- Add item to inventory - npc.add_item_to_inventory_itemstring(self, current_itemstring) + end + -- Add item to inventory + npc.add_item_to_inventory_itemstring(self, current_itemstring) end - elseif property == npc.schedule_properties.take_item then + elseif property == npc.schedule_properties.take_item then local itemstring = args.itemstring - -- Add item - npc.take_item_from_inventory_itemstring(self, itemstring) - elseif property == npc.schedule_properties.can_receive_gifts then - local value = args.can_receive_gifts - -- Set status - self.can_receive_gifts = value - end + -- Add item + npc.take_item_from_inventory_itemstring(self, itemstring) + elseif property == npc.schedule_properties.can_receive_gifts then + local value = args.can_receive_gifts + -- Set status + self.can_receive_gifts = value + end +end + +function npc.add_schedule_check(self) + table.insert(self.actions.queue, {action="schedule_check", args={}, is_task=false}) +end + +-- Range: integer, radius in which nodes will be searched. Recommended radius is +-- between 1-3 +-- Nodes: array of node names +-- Actions: map of node names to entries {action=, args={}}. +-- Arguments can be empty - the check function will try to determine most +-- arguments anyways (like pos and dir). +-- Special node "any" will execute those actions on any node except the +-- already specified ones. +-- None-action: array of entries {action=, args={}}. +-- Will be executed when no node is found. +function npc.schedule_check(self) + local range = self.schedules.current_check_params.range + local nodes = self.schedules.current_check_params.nodes + local actions = self.schedules.current_check_params.actions + local none_actions = self.schedules.current_check_params.none_actions + -- Get NPC position + local start_pos = self.object:getpos() + -- Search nodes + local found_nodes = npc.places.find_node_nearby(start_pos, nodes, range) + -- Check if any node was found + if found_nodes then + -- Pick a random node to act upon + local node_pos = found_nodes[math.random(1, #found_nodes)] + local node = minetest.get_node(node_pos) + -- Set node as a place + -- Note: Code below isn't *adding* a node, but overwriting the + -- place with "schedule_target_pos" place type + npc.places.add_shared_accessible_place( + self, node, npc.places.PLACE_TYPE.SCHEDULE.TARGET, true) + -- Get actions related to node and enqueue them + for i = 1, #actions[node.name] do + local args = {} + local action = nil + -- Calculate arguments for the following supported actions: + -- - Dig + -- - Place + -- - Walk step + -- - Walk to position + -- - Use furnace + if actions[node.name][i].action == npc.actions.cmd.DIG then + -- Defaults: items will be added to inventory if not specified + -- otherwise, and protection will be respected, if not specified + -- otherwise + args = { + pos = node_pos, + add_to_inventory = action[node.name][i].args.add_to_inventory or true, + bypass_protection = action[node.name][i].args.bypass_protection or false + } + elseif actions[node.name][i].action == npc.actions.cmd.PLACE then + -- Position: providing node_pos is because the currently planned + -- behavior for placing nodes is replacing digged nodes. A NPC farmer, + -- for instance, might dig a plant node and plant another one on the + -- same position. + -- Defaults: items will be taken from inventory if existing, + -- if not will be force-placed (item comes from thin air) + -- Protection will be respected + args = { + pos = action[node.name][i].args.pos or node_pos, + source = action[node.name][i].args.source or npc.actions.take_from_inventory_forced, + node = action[node.name][i].args.node, + bypass_protection = action[node.name][i].args.bypass_protection or false + } + elseif actions[node.name][i].action == npc.actions.cmd.WALK_STEP then + -- Defaults: direction is calculated from start node to node_pos. + -- Speed is default wandering speed. Target pos is node_pos + -- Calculate dir if dir is random + local dir = npc.actions.get_direction(start_pos, node_pos) + if actions[node.name][i].args.dir == "random" then + dir = math.random(0,7) + elseif type(actions[node.name][i].args.dir) == "number" then + dir = actions[node.name][i].args.dir + end + args = { + dir = dir, + speed = actions[node.name][i].args.speed or npc.actions.one_nps_speed, + target_pos = actions[node.name][i].args.target_pos or node_pos + } + elseif actions[node.name][i].action == npc.actions.cmd.WALK_TO_POS then + -- Optimize walking -- since distances can be really short, + -- a simple walk_step() action can do most of the times. For + -- this, however, we need to calculate direction + -- First of all, check distance + if vector.distance(start_pos, node_pos) < 3 then + -- Will do walk_step based instead + action = npc.actions.cmd.WALK_STEP + args = { + dir = npc.actions.get_direction(start_pos, node_pos), + speed = npc.actions.one_nps_speed, + target_pos = node_pos + } + else + -- Set end pos to be node_pos + args = { + end_pos = actions[node.name][i].args.end_pos or node_pos, + walkable = actions[node.name][i].args.walkable or {} + } + end + elseif actions[node.name][i].action == npc.actions.cmd.USE_FURNACE then + -- Defaults: pos is node_pos. Freeze is true + args = { + pos = actions[node.name][i].args.pos or node_pos, + item = actions[node.name][i].args.item, + freeze = actions[node.name][i].args.freeze or true + } + end + -- Enqueue actions + npc.add_action(self, action or actions[node.name][i].action, args or actions[node.name][i].args) + end + -- Enqueue next schedule check + if self.schedules.current_check_params.execution_count + < self.schedules.current_check_params.execution_times then + npc.add_schedule_check() + end + -- Nodes found + return true + else + -- No nodes found, enqueue none_actions + for i = 1, #none_actions do + -- Enqueue actions + npc.add_action(self, none_actions[i].action, none_actions[i].args) + end + -- Enqueue next schedule check + if self.schedules.current_check_params.execution_count + < self.schedules.current_check_params.execution_times then + npc.add_schedule_check() + end + -- No nodes found + return false + end end --------------------------------------------------------------------------------------- @@ -977,8 +1143,9 @@ mobs:register_mob("advanced_npc:npc", { hp_min = 10, hp_max = 20, armor = 100, - collisionbox = {-0.20,-1.0,-0.20, 0.20,0.8,0.20}, - --collisionbox = {-0.35,-1.0,-0.35, 0.35,0.8,0.35}, + collisionbox = {-0.20,0,-0.20, 0.20,1.8,0.20}, + --collisionbox = {-0.20,-1.0,-0.20, 0.20,0.8,0.20}, + --collisionbox = {-0.35,-1.0,-0.35, 0.35,0.8,0.35}, visual = "mesh", mesh = "character.b3d", drawtype = "front", @@ -1043,209 +1210,231 @@ mobs:register_mob("advanced_npc:npc", { -- Receive gift or start chat. If player has no item in hand -- then it is going to start chat directly - minetest.log("self.can_have_relationship: "..dump(self.can_have_relationship)..", self.can_receive_gifts: "..dump(self.can_receive_gifts)..", table: "..dump(item:to_table())) + --minetest.log("self.can_have_relationship: "..dump(self.can_have_relationship)..", self.can_receive_gifts: "..dump(self.can_receive_gifts)..", table: "..dump(item:to_table())) if self.can_have_relationship - and self.can_receive_gifts - and item:to_table() ~= nil then - -- Get item name - local item = minetest.registered_items[item:get_name()] - local item_name = item.description + and self.can_receive_gifts + and item:to_table() ~= nil then + -- Get item name + local item = minetest.registered_items[item:get_name()] + local item_name = item.description - -- 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.npc_name.."?", - npc.dialogue.POSITIVE_GIFT_ANSWER_PREFIX..item_name, - function() - npc.relationships.receive_gift(self, clicker) - end, - npc.dialogue.NEGATIVE_ANSWER_LABEL, - function() - npc.start_dialogue(self, clicker, true) - end, - name - ) + -- 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.npc_name.."?", + npc.dialogue.POSITIVE_GIFT_ANSWER_PREFIX..item_name, + function() + npc.relationships.receive_gift(self, clicker) + end, + npc.dialogue.NEGATIVE_ANSWER_LABEL, + function() + npc.start_dialogue(self, clicker, true) + end, + name + ) else npc.start_dialogue(self, clicker, true) end end, do_custom = function(self, dtime) - if self.initialized == nil then - -- Initialize NPC if spawned using the spawn egg built in from - -- mobs_redo. This functionality will be removed in the future in - -- favor of a better manual spawning method with customization - npc.log("WARNING", "Initializing NPC from entity step. This message should only be appearing if an NPC is being spawned from inventory with egg!") - npc.initialize(self, self.object:getpos(), true) - self.tamed = false - self.owner = nil - else - -- NPC is initialized, check other variables - -- Check child texture issues - if self.is_child then - -- Check texture - npc.texture_check.timer = npc.texture_check.timer + dtime - if npc.texture_check.timer > npc.texture_check.interval then - -- Reset timer - npc.texture_check.timer = 0 - -- Set hornytimer to zero every 60 seconds so that children - -- don't grow automatically - self.hornytimer = 0 - -- Set correct textures - self.texture = {self.selected_texture} - self.base_texture = {self.selected_texture} - self.object:set_properties(self) - npc.log("WARNING", "Corrected textures on NPC child "..dump(self.npc_name)) - -- Set interval to large interval so this code isn't called frequently - npc.texture_check.interval = 60 - end - end - - -- Timer function for casual traders to reset their trade offers - self.trader_data.change_offers_timer = self.trader_data.change_offers_timer + dtime - -- Check if time has come to change offers - if self.trader_data.trader_status == npc.trade.CASUAL and - self.trader_data.change_offers_timer >= self.trader_data.change_offers_timer_interval then - -- Reset timer - self.trader_data.change_offers_timer = 0 - -- Re-select casual trade offers - npc.trade.generate_trade_offers_by_status(self) - end - - -- Timer function for gifts - for i = 1, #self.relationships do - local relationship = self.relationships[i] - -- Gift timer check - if relationship.gift_timer_value < relationship.gift_interval then - relationship.gift_timer_value = relationship.gift_timer_value + dtime - elseif relationship.talk_timer_value < relationship.gift_interval then - -- Relationship talk timer - only allows players to increase relationship - -- by talking on the same intervals as gifts - relationship.talk_timer_value = relationship.talk_timer_value + dtime + if self.initialized == nil then + -- Initialize NPC if spawned using the spawn egg built in from + -- mobs_redo. This functionality will be removed in the future in + -- favor of a better manual spawning method with customization + npc.log("WARNING", "Initializing NPC from entity step. This message should only be appearing if an NPC is being spawned from inventory with egg!") + npc.initialize(self, self.object:getpos(), true) + self.tamed = false + self.owner = nil else - -- Relationship decrease timer - if relationship.relationship_decrease_timer_value - < relationship.relationship_decrease_interval then - relationship.relationship_decrease_timer_value = - relationship.relationship_decrease_timer_value + dtime - else - -- Check if married to decrease half - if relationship.phase == "phase6" then - -- Avoid going below the marriage phase limit - if (relationship.points - 0.5) >= - npc.relationships.RELATIONSHIP_PHASE["phase5"].limit then - relationship.points = relationship.points - 0.5 - end + -- NPC is initialized, check other variables + -- Check child texture issues + if self.is_child then + -- Check texture + npc.texture_check.timer = npc.texture_check.timer + dtime + if npc.texture_check.timer > npc.texture_check.interval then + -- Reset timer + npc.texture_check.timer = 0 + -- Set hornytimer to zero every 60 seconds so that children + -- don't grow automatically + self.hornytimer = 0 + -- Set correct textures + self.texture = {self.selected_texture} + self.base_texture = {self.selected_texture} + self.object:set_properties(self) + npc.log("WARNING", "Corrected textures on NPC child "..dump(self.npc_name)) + -- Set interval to large interval so this code isn't called frequently + npc.texture_check.interval = 60 + end + end + + -- Timer function for casual traders to reset their trade offers + self.trader_data.change_offers_timer = self.trader_data.change_offers_timer + dtime + -- Check if time has come to change offers + if self.trader_data.trader_status == npc.trade.CASUAL and + self.trader_data.change_offers_timer >= self.trader_data.change_offers_timer_interval then + -- Reset timer + self.trader_data.change_offers_timer = 0 + -- Re-select casual trade offers + npc.trade.generate_trade_offers_by_status(self) + end + + -- Timer function for gifts + for i = 1, #self.relationships do + local relationship = self.relationships[i] + -- Gift timer check + if relationship.gift_timer_value < relationship.gift_interval then + relationship.gift_timer_value = relationship.gift_timer_value + dtime + elseif relationship.talk_timer_value < relationship.gift_interval then + -- Relationship talk timer - only allows players to increase relationship + -- by talking on the same intervals as gifts + relationship.talk_timer_value = relationship.talk_timer_value + dtime else - relationship.points = relationship.points - 1 + -- Relationship decrease timer + if relationship.relationship_decrease_timer_value + < relationship.relationship_decrease_interval then + relationship.relationship_decrease_timer_value = + relationship.relationship_decrease_timer_value + dtime + else + -- Check if married to decrease half + if relationship.phase == "phase6" then + -- Avoid going below the marriage phase limit + if (relationship.points - 0.5) >= + npc.relationships.RELATIONSHIP_PHASE["phase5"].limit then + relationship.points = relationship.points - 0.5 + end + else + relationship.points = relationship.points - 1 + end + relationship.relationship_decrease_timer_value = 0 + --minetest.log(dump(self)) + end end - relationship.relationship_decrease_timer_value = 0 - --minetest.log(dump(self)) - end end - end - -- Action queue timer - -- Check if actions and timers aren't locked - if self.actions.action_timer_lock == false then - -- Increment action timer - self.actions.action_timer = self.actions.action_timer + dtime - if self.actions.action_timer >= self.actions.action_interval then - -- Reset action timer - self.actions.action_timer = 0 - -- 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 + 0.5, z=pos.z}) - end - -- Execute action - self.freeze = npc.execute_action(self) - -- Check if there are still remaining actions in the queue - if self.freeze == nil and table.getn(self.actions.queue) > 0 then - self.freeze = false - end + -- Action queue timer + -- Check if actions and timers aren't locked + if self.actions.action_timer_lock == false then + -- Increment action timer + self.actions.action_timer = self.actions.action_timer + dtime + if self.actions.action_timer >= self.actions.action_interval then + -- Reset action timer + self.actions.action_timer = 0 + -- Check if NPC is walking + if self.actions.walking.is_walking == true then + -- Move NPC to expected position to ensure not getting lost + local pos = self.actions.walking.target_pos + self.object:moveto({x=pos.x, y=pos.y-0.5, z=pos.z}) + end + -- Execute action + self.freeze = npc.execute_action(self) + -- Check if there are still remaining actions in the queue + if self.freeze == nil and table.getn(self.actions.queue) > 0 then + self.freeze = false + end + end end - end - -- Schedule timer - -- Check if schedules are enabled - if self.schedules.enabled == true then - -- Get time of day - local time = get_time_in_hours() - -- Check if time is an hour - if time % 1 < 0.1 and self.schedules.lock == false then - -- Activate lock to avoid more than one entry to this code - self.schedules.lock = true - -- Get integer part of time - time = (time) - (time % 1) - -- Check if there is a schedule entry for this time - -- Note: Currently only one schedule is supported, for day 0 - --minetest.log("Time: "..dump(time)) - local schedule = self.schedules.generic[0] - if schedule ~= nil then - -- Check if schedule for this time exists - --minetest.log("Found default schedule") - if schedule[time] ~= nil then - -- Check if schedule has a check function - if schedule[time].check ~= nil then - -- Execute check function and then add corresponding action - -- to action queue. This is for jobs. - -- TODO: Need to implement - else - npc.log("DEBUG", "Adding actions to action queue") - -- Add to action queue all actions on schedule - for i = 1, #schedule[time] do - --minetest.log("schedule[time]: "..dump(schedule[time])) - -- Check chance - local execution_chance = math.random(1, 100) - if not schedule[time][i].chance or - (schedule[time][i].chance and execution_chance <= schedule[time][i].chance) then - -- Check if entry has dependency on other entry - local dependencies_met = nil - if schedule[time][i].depends then - dependencies_met = npc.utils.array_is_subset_of_array( - self.schedules.temp_executed_queue, - schedule[time][i].depends) - end + -- Schedule timer + -- Check if schedules are enabled + if self.schedules.enabled == true then + -- Get time of day + local time = get_time_in_hours() + -- Check if time is an hour + if time % 1 < 0.1 and self.schedules.lock == false then + -- Activate lock to avoid more than one entry to this code + self.schedules.lock = true + -- Get integer part of time + time = (time) - (time % 1) + -- Check if there is a schedule entry for this time + -- Note: Currently only one schedule is supported, for day 0 + --minetest.log("Time: "..dump(time)) + local schedule = self.schedules.generic[0] + if schedule ~= nil then + -- Check if schedule for this time exists + --minetest.log("Found default schedule") + if schedule[time] ~= nil then + npc.log("WARNING", "Found schedule for time "..dump(time)..": "..dump(schedule[time])) + npc.log("DEBUG", "Adding actions to action queue") + -- Add to action queue all actions on schedule + for i = 1, #schedule[time] do + -- Check if schedule has a check function + if not schedule[time][i].check then + -- Add parameters for check function and run for first time + npc.log("INFO", "NPC "..dump(self.npc_name).." is starting check on "..minetest.pos_to_string(self.object:getpos())) + local check_params = schedule[time][i] + -- Calculates how many times check will be executed + local execution_times = check_params.count + if check_params.random_execution_times then + execution_times = math.random(check_params.min_count, check_params.max_count) + end + -- Set current parameters + self.schedules.current_check_params = { + range = check_params.range, + nodes = check_params.nodes, + actions = check_params.actions, + none_actions = check_params.none_actions, + execution_count = 0, + execution_times = execution_times + } + -- Execute check for the first time + npc.schedule_check(self) + else + -- Run usual schedule entry + -- Check chance + local execution_chance = math.random(1, 100) + if not schedule[time][i].chance or + (schedule[time][i].chance and execution_chance <= schedule[time][i].chance) then + -- Check if entry has dependency on other entry + local dependencies_met = nil + if schedule[time][i].depends then + dependencies_met = npc.utils.array_is_subset_of_array( + self.schedules.temp_executed_queue, + schedule[time][i].depends) + end - -- Check for dependencies being met - if dependencies_met == nil or dependencies_met == true then - -- Add tasks - if schedule[time][i].task ~= nil then - -- Add task - npc.add_task(self, schedule[time][i].task, schedule[time][i].args) - elseif schedule[time][i].action ~= nil then - -- Add action - npc.add_action(self, schedule[time][i].action, schedule[time][i].args) - elseif schedule[time][i].property ~= nil then - -- Change NPC property - npc.schedule_change_property(self, schedule[time][i].property, schedule[time][i].args) - end - -- Backward compatibility check - if self.schedules.temp_executed_queue then - -- Add into execution queue to meet dependency - table.insert(self.schedules.temp_executed_queue, i) - end + -- Check for dependencies being met + if dependencies_met == nil or dependencies_met == true then + -- Add tasks + if schedule[time][i].task ~= nil then + -- Add task + npc.add_task(self, schedule[time][i].task, schedule[time][i].args) + elseif schedule[time][i].action ~= nil then + -- Add action + npc.add_action(self, schedule[time][i].action, schedule[time][i].args) + elseif schedule[time][i].property ~= nil then + -- Change NPC property + npc.schedule_change_property(self, schedule[time][i].property, schedule[time][i].args) + end + -- Backward compatibility check + if self.schedules.temp_executed_queue then + -- Add into execution queue to meet dependency + table.insert(self.schedules.temp_executed_queue, i) + end + end + else + -- TODO: Change to debug + npc.log("WARNING", "Skipping schedule entry for time "..dump(time)..": "..dump(schedule[time][i])) + end + end + end + -- Clear execution queue + self.schedules.temp_executed_queue = {} + npc.log("WARNING", "New action queue: "..dump(self.actions)) end end end - -- Clear execution queue - self.schedules.temp_executed_queue = {} - npc.log("DEBUG", "New action queue: "..dump(self.actions)) - end + else + -- Check if lock can be released + if time % 1 > 0.1 then + -- Release lock + self.schedules.lock = false + end end - end - else - -- Check if lock can be released - if time % 1 > 0.1 then - -- Release lock - self.schedules.lock = false - end end - end - - return self.freeze - end + + return self.freeze + end end }) diff --git a/spawner.lua b/spawner.lua index eb54f42..a72d4ca 100644 --- a/spawner.lua +++ b/spawner.lua @@ -264,7 +264,7 @@ function spawner.assign_places(self, pos) -- Assign beds if #node_data.bed_type > 0 then -- Assign a specific sittable node to a NPC. - npc.places.add_unowned_accessible_place(self, node_data.bed_type, + npc.places.add_owned_accessible_place(self, node_data.bed_type, npc.places.PLACE_TYPE.BED.PRIMARY) -- Store changes to node_data meta:set_string("node_data", minetest.serialize(node_data)) @@ -275,7 +275,7 @@ function spawner.assign_places(self, pos) -- Check if there are same or more amount of sits as beds if #node_data.sittable_type >= #node_data.bed_type then -- Assign a specific sittable node to a NPC. - npc.places.add_unowned_accessible_place(self, node_data.sittable_type, + npc.places.add_owned_accessible_place(self, node_data.sittable_type, npc.places.PLACE_TYPE.SITTABLE.PRIMARY) -- Store changes to node_data meta:set_string("node_data", minetest.serialize(node_data)) @@ -291,7 +291,7 @@ function spawner.assign_places(self, pos) -- Check if there are same or more amount of furnace as beds if #node_data.furnace_type >= #node_data.bed_type then -- Assign a specific furnace node to a NPC. - npc.places.add_unowned_accessible_place(self, node_data.furnace_type, + npc.places.add_owned_accessible_place(self, node_data.furnace_type, npc.places.PLACE_TYPE.FURNACE.PRIMARY) -- Store changes to node_data meta:set_string("node_data", minetest.serialize(node_data)) @@ -307,7 +307,7 @@ function spawner.assign_places(self, pos) -- Check if there are same or more amount of storage as beds if #node_data.storage_type >= #node_data.bed_type then -- Assign a specific storage node to a NPC. - npc.places.add_unowned_accessible_place(self, node_data.storage_type, + npc.places.add_owned_accessible_place(self, node_data.storage_type, npc.places.PLACE_TYPE.STORAGE.PRIMARY) -- Store changes to node_data meta:set_string("node_data", minetest.serialize(node_data)) @@ -372,17 +372,19 @@ function npc.spawner.spawn_npc(pos) local ent = minetest.add_entity({x=pos.x, y=pos.y+1, z=pos.z}, "advanced_npc:npc") if ent and ent:get_luaentity() then ent:get_luaentity().initialized = false + -- Determine NPC occupation + local occupation_name = "default_basic" -- Initialize NPC -- Call with stats if there are NPCs - if #npc_table > 0 then - npc.initialize(ent, pos, false, npc_stats) + if npc_table and #npc_table > 0 then + npc.initialize(ent, pos, false, npc_stats, occupation_name) else - npc.initialize(ent, pos) + npc.initialize(ent, pos, nil, nil, occupation_name) end -- Assign nodes spawner.assign_places(ent:get_luaentity(), pos) -- Assign schedules - spawner.assign_schedules(ent:get_luaentity(), pos) + --spawner.assign_schedules(ent:get_luaentity(), pos) -- Increase NPC spawned count spawned_npc_count = spawned_npc_count + 1 -- Store count into node @@ -750,8 +752,8 @@ minetest.register_chatcommand("restore_plotmarkers", { end -- Search for nodes local radius = tonumber(param) - local start_pos = {x=pos.x - radius, y=pos.y - radius, z=pos.z - radius} - local end_pos = {x=pos.x + radius, y=pos.y + radius, z=pos.z + radius} + local start_pos = {x=pos.x - radius, y=pos.y - 5, z=pos.z - radius} + local end_pos = {x=pos.x + radius, y=pos.y + 5, z=pos.z + radius} local nodes = minetest.find_nodes_in_area_under_air(start_pos, end_pos, {"mg_villages:plotmarker"}) -- Check if we have nodes to replace diff --git a/utils.lua b/utils.lua index c36ee48..2391b73 100644 --- a/utils.lua +++ b/utils.lua @@ -1,4 +1,4 @@ --- Basic utilities to work with array operations in Lua +-- Basic utilities to work with table operations in Lua, and specific querying -- By Zorman2000 npc.utils = {} @@ -34,4 +34,43 @@ function npc.utils.get_map_keys(map) table.insert(result, key) end return result +end + +-- This function searches for a node given the conditions specified in the +-- query object, starting from the given start_pos and up to a certain, specified +-- range. +-- Query object: +-- search_type: determines the direction to search nodes. +-- Valid values are: orthogonal, cross, cube +-- - orthogonal search means only nodes which are parallel to the search node's faces +-- will be considered. This limits the search to only 6 nodes. +-- - cross search will look at the same nodes as orthogonal, plus will also +-- check nodes diagonal to the node four horizontal nodes. This search looks at 14 nodes +-- - cube search means to look every node surrounding the node, including all diagonals. +-- This search looks at 26 nodes. +-- search_nodes: array of nodes to search for +-- surrounding_nodes: object specifying which neighbor nodes are to be expected and +-- at which locations. Valid keys are: +-- - North (+Z dir) +-- - East (+x dir) +-- - South (-Z dir) +-- - West (-X dir) +-- - Top (+Y dir) +-- - Bottom (-Y dir) +-- Example: ["bottom"] = {nodes={"default:dirt"}, criteria="all"} +-- Each object will contain nodes, and criteria for acceptance. +-- Criteria values are: +-- - any: true as long as one of the nodes on this side is one of the specified +-- in "nodes" +-- - all: true when the set of neighboring nodes on this side contain one or many of +-- the specified "nodes" +-- - all-exact: true when the set of neighboring nodes on this side contain all nodes +-- specified in "nodes" +-- - shape: true when the set of neighboring nodes on this side contains nodes in +-- the exact given shape. If so, nodes will not be an array, but a 2d array +-- of three rows and three columns, with the specific shape. Notice that +-- the nodes on the side can vary depending on the search type (orthogonal, +-- cross, cube) +function npc.utils.search_node(query, start_pos, range) + end \ No newline at end of file