diff --git a/actions/places.lua b/actions/places.lua index af1b076..8c83294 100644 --- a/actions/places.lua +++ b/actions/places.lua @@ -102,7 +102,7 @@ function npc.places.add_owned_accessible_place(self, nodes, place_type) -- Set owner to this NPC nodes[i].owner = self.npc_id -- Assign node to NPC - npc.places.add_owned(self, place_type, place_type, + 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)) @@ -126,7 +126,7 @@ function npc.places.add_shared_accessible_place(self, nodes, place_type, overrid -- Check if node is accessible if #empty_nodes > 0 then -- Assign node to NPC - npc.places.add_shared(self, place_type..dump(i), + npc.places.add_shared(self, place_type..dump(i), place_type, nodes[i].node_pos, empty_nodes[1].pos) end end @@ -139,7 +139,7 @@ function npc.places.add_shared_accessible_place(self, nodes, place_type, overrid -- Check if node is accessible if #empty_nodes > 0 then -- Nodes is only one node - npc.places.add_shared(self, place_type, place_type, + npc.places.add_shared(self, place_type, place_type, nodes.node_pos, empty_nodes[1].pos) end end @@ -155,6 +155,12 @@ function npc.places.get_by_type(self, place_type) return result end +--------------------------------------------------------------------------------------- +-- Utility functions +--------------------------------------------------------------------------------------- +-- The following are utility functions that are used to operate on nodes for +-- specific conditions + -- 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) @@ -169,7 +175,7 @@ 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 + -- 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}) @@ -188,11 +194,64 @@ function npc.places.find_node_orthogonally(pos, nodes, y_adjustment) return result end +-- Wrapper around minetest.find_nodes_in_area() +-- TODO: Verify if this wrapper is actually needed 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 end +-- Function used to filter all nodes in the first floor of a building +-- If floor height isn't given, it will assume 2 +-- Notice that nodes is an array of entries {node_pos={}, type={}} +function npc.places.filter_first_floor_nodes(nodes, ground_pos, floor_height) + local height = floor_height or 2 + local result = {} + for _,node in pairs(nodes) do + if node.node_pos.y <= ground_pos.y + height then + table.insert(result, node) + end + end + return result +end + +-- Creates an array of {pos=, owner=''} for managing +-- which NPC owns what +function npc.places.get_nodes_by_type(start_pos, end_pos, type) + local result = {} + local nodes = npc.places.find_node_in_area(start_pos, end_pos, type) + --minetest.log("Found "..dump(#nodes).." nodes of type: "..dump(type)) + for _,node_pos in pairs(nodes) do + local entry = {} + entry["node_pos"] = node_pos + entry["owner"] = '' + table.insert(result, entry) + end + return result +end + +-- Scans an area for the supported nodes: beds, benches, +-- furnaces, storage (e.g. chests) and openable (e.g. doors). +-- Returns a table with these classifications +function npc.places.scan_area_for_usable_nodes(pos1, pos2) + local result = { + bed_type = {}, + sittable_type = {}, + furnace_type = {}, + storage_type = {}, + openable_type = {} + } + local start_pos, end_pos = vector.sort(pos1, pos2) + + result.bed_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.BED_TYPE) + result.sittable_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.SITTABLE_TYPE) + result.furnace_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.FURNACE_TYPE) + result.storage_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.STORAGE_TYPE) + result.openable_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.OPENABLE_TYPE) + + return result +end + -- Specialized function to find doors that are an entrance to a building. -- The definition of an entrance is: -- The openable node with the shortest path to the plotmarker node @@ -205,7 +264,7 @@ function npc.places.find_entrance_from_openable_nodes(all_openable_nodes, marker -- 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, + -- 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 @@ -231,18 +290,8 @@ function npc.places.find_entrance_from_openable_nodes(all_openable_nodes, marker 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 - - -- 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, {}) @@ -258,7 +307,7 @@ function npc.places.find_entrance_from_openable_nodes(all_openable_nodes, marker if #path < min then -- Set min to path length and the result to the currently found node min = #path - result = openable_nodes[i] + 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 @@ -419,4 +468,4 @@ function npc.places.find_node_in_front_of_door(door_pos) -- Looking west return {x=door_pos.x + 1, y=door_pos.y, z=door_pos.z} end -end \ No newline at end of file +end diff --git a/random_data/dialogues_data.lua b/data/dialogues_data.lua similarity index 100% rename from random_data/dialogues_data.lua rename to data/dialogues_data.lua diff --git a/random_data/gift_items_data.lua b/data/gift_items_data.lua similarity index 100% rename from random_data/gift_items_data.lua rename to data/gift_items_data.lua diff --git a/random_data/names_data.lua b/data/names_data.lua similarity index 100% rename from random_data/names_data.lua rename to data/names_data.lua diff --git a/random_data/occupations_data.lua b/data/occupations_data.lua similarity index 100% rename from random_data/occupations_data.lua rename to data/occupations_data.lua diff --git a/init.lua b/init.lua index 01c8fe2..23efd33 100755 --- a/init.lua +++ b/init.lua @@ -37,9 +37,9 @@ 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") +dofile(path .. "/data/dialogues_data.lua") +dofile(path .. "/data/gift_items_data.lua") +dofile(path .. "/data/names_data.lua") +dofile(path .. "/data/occupations_data.lua") print (S("[Mod] Advanced NPC loaded")) diff --git a/npc.lua b/npc.lua index ba5a47a..210b1f9 100755 --- a/npc.lua +++ b/npc.lua @@ -1,5 +1,5 @@ -- Advanced NPC by Zorman2000 --- Based on original NPC by Tenplus1 +-- Based on original NPC by Tenplus1 local S = mobs.intllib @@ -133,10 +133,10 @@ local function get_random_texture(sex, age) for i = 1, #textures do local current_texture = textures[i][1] - if (sex == npc.MALE - and string.find(current_texture, sex) + if (sex == npc.MALE + and string.find(current_texture, sex) and not string.find(current_texture, npc.FEMALE)) - or (sex == npc.FEMALE + or (sex == npc.FEMALE and string.find(current_texture, sex)) then table.insert(filtered_textures, current_texture) end @@ -156,18 +156,18 @@ function npc.get_random_texture_from_array(age, sex, textures) for i = 1, #textures do local current_texture = textures[i] -- Filter by age - if (sex == npc.MALE - and string.find(current_texture, sex) + if (sex == npc.MALE + and string.find(current_texture, sex) and not string.find(current_texture, npc.FEMALE) - and ((age == npc.age.adult + and ((age == npc.age.adult and not string.find(current_texture, npc.age.child)) or (age == npc.age.child and string.find(current_texture, npc.age.child)) ) ) - or (sex == npc.FEMALE + or (sex == npc.FEMALE and string.find(current_texture, sex) - and ((age == npc.age.adult + and ((age == npc.age.adult and not string.find(current_texture, npc.age.child)) or (age == npc.age.child and string.find(current_texture, npc.age.child)) @@ -185,7 +185,7 @@ function npc.get_random_texture_from_array(age, sex, textures) return filtered_textures[math.random(1, #filtered_textures)] end --- Choose whether NPC can have relationships. Only 30% of NPCs +-- Choose whether NPC can have relationships. Only 30% of NPCs -- cannot have relationships local function can_have_relationships(is_child) -- Children can't have relationships @@ -201,11 +201,11 @@ end local function choose_spawn_items(self) local number_of_items_to_add = math.random(1, 2) local number_of_items = #npc.FAVORITE_ITEMS[self.sex].phase1 - + for i = 1, number_of_items_to_add do npc.add_item_to_inventory( self, - npc.FAVORITE_ITEMS[self.sex].phase1[math.random(1, number_of_items)].item, + npc.FAVORITE_ITEMS[self.sex].phase1[math.random(1, number_of_items)].item, math.random(1,5) ) end @@ -244,7 +244,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- 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 @@ -256,7 +256,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- - 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 + -- Use NPC stats if provided if npc_stats then -- Default chances local male_s, male_e = 0, 50 @@ -273,7 +273,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) end -- Determine age probabilities if npc_stats["adult_total"] >= 2 then - if npc_stats["adult_total"] % 2 == 0 + 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 @@ -307,7 +307,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) ent.collisionbox = {-0.10,-0.50,-0.10, 0.10,0.40,0.10} ent.is_child = true -- For mobs_redo - ent.child = true + ent.child = true end -- Store the selected age ent.age = selected_age @@ -340,7 +340,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- Set ID ent.npc_id = tostring(math.random(1000, 9999))..":"..ent.npc_name - + -- Initialize all gift data ent.gift_data = { -- Choose favorite items. Choose phase1 per default @@ -348,7 +348,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- Choose disliked items. Choose phase1 per default disliked_items = npc.relationships.select_random_disliked_items(ent.sex), } - + -- Flag that determines if NPC can have a relationship ent.can_have_relationship = can_have_relationships(ent.is_child) @@ -365,7 +365,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- Initialize dialogues ent.dialogues = npc.dialogue.select_random_dialogues_for_npc(ent, "phase1") - + -- Declare NPC inventory ent.inventory = initialize_inventory() @@ -435,14 +435,14 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) is_walking = false, -- Path that the NPC is following path = {}, - -- Target position the NPC is supposed to walk to in this step. NOTE: + -- Target position the NPC is supposed to walk to in this step. NOTE: -- This is NOT the end of the path, but the next position in the path -- relative to the last position target_pos = {} } } - -- This flag is checked on every step. If it is true, the rest of + -- This flag is checked on every step. If it is true, the rest of -- Mobs Redo API is not executed ent.freeze = nil @@ -453,7 +453,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- Schedule data ent.schedules = { -- Flag to enable or disable the schedules functionality - enabled = true, + enabled = true, -- Lock for when executing a schedule lock = false, -- Queue of schedules executed @@ -463,7 +463,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) --- 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 + -- 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 = {}, @@ -474,7 +474,8 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- 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)) + --minetest.log("Entity age: "..dump(ent.age)..", afult? "..dump(ent.age==npc.age.adult)) + npc.log("INFO", "Overriding NPC values with occupation: "..dump(occupation_name)) if occupation_name and occupation_name ~= "" and ent.age == npc.age.adult then npc.occupations.initialize_occupation_values(ent, occupation_name) end @@ -508,7 +509,7 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- Set initialized flag on ent.initialized = true - npc.log("WARNING", "Spawned entity: "..dump(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)) @@ -561,7 +562,7 @@ function npc.add_item_to_inventory(self, item_name, count) local existing_count = npc.get_item_count(existing_item.item_string) if (existing_count + count) < npc.INVENTORY_ITEM_MAX_STACK then -- Set item here - self.inventory[existing_item.slot] = + self.inventory[existing_item.slot] = npc.get_item_name(existing_item.item_string).." "..tostring(existing_count + count) return true else @@ -569,7 +570,7 @@ function npc.add_item_to_inventory(self, item_name, count) for i = 1, #self.inventory do if self.inventory[i] == "" then -- Found slot, set item - self.inventory[i] = + self.inventory[i] = item_name.." "..tostring((existing_count + count) - npc.INVENTORY_ITEM_MAX_STACK) return true end @@ -789,7 +790,7 @@ function npc.lock_actions(self) -- Reset timer so that it has some time after interaction is done self.actions.action_timer = 0 -- Check if there are is an action executing - if self.actions.current_action_state == npc.action_state.executing + if self.actions.current_action_state == npc.action_state.executing and self.freeze == false then -- Store the current action state self.actions.state_before_lock.action_state = self.actions.current_action_state @@ -809,7 +810,7 @@ function npc.unlock_actions(self) self.actions.action_timer_lock = false -- Restore the value of self.freeze self.freeze = self.actions.state_before_lock.freeze - + if table.getn(self.actions.queue) == 0 then -- Allow mobs_redo API to execute since action queue is empty self.freeze = true @@ -822,7 +823,7 @@ end -- Schedule functionality --------------------------------------------------------------------------------------- -- Schedules allow the NPC to do different things depending on the time of the day. --- The time of the day is in 24 hours and is consistent with the Minetest Game +-- The time of the day is in 24 hours and is consistent with the Minetest Game -- /time command. Hours will be written as numbers: 1 for 1:00, 13 for 13:00 or 1:00 PM -- The API is as following: a schedule can be created for a specific date or for a -- day of the week. A date is a string in the format MM:DD @@ -839,7 +840,7 @@ npc.schedule_properties = { can_receive_gifts = "can_receive_gifts" } -local function get_time_in_hours() +local function get_time_in_hours() return minetest.get_timeofday() * 24 end @@ -848,7 +849,7 @@ end -- - Generic: Returns nil if there are already -- seven schedules, one for each day of the -- week or if the schedule attempting to add --- already exists. The date parameter is the +-- already exists. The date parameter is the -- day of the week it represents as follows: -- - 1: Monday -- - 2: Tuesday @@ -879,7 +880,7 @@ function npc.create_schedule(self, schedule_type, date) elseif schedule_type == npc.schedule_types.date then -- Check schedule doesn't exists already if self.schedules.date_based[date] == nil then - -- Add schedule + -- Add schedule self.schedules.date_based[date] = {} else -- Schedule already present @@ -895,7 +896,7 @@ end -- Schedule entries API -- Allows to add, get, update and delete entries from each --- schedule. Attempts to be as safe-fail as possible to avoid crashes. +-- schedule. Attempts to be as safe-fail as possible to avoid crashes. -- Actions is an array of actions and tasks that the NPC -- will perform at the scheduled time on the scheduled date @@ -996,11 +997,11 @@ 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 +-- 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 +-- 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. @@ -1049,7 +1050,7 @@ function npc.schedule_check(self) -- 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, + -- 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 = { @@ -1076,7 +1077,7 @@ function npc.schedule_check(self) 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 + -- 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 @@ -1105,7 +1106,7 @@ function npc.schedule_check(self) 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 + if self.schedules.current_check_params.execution_count < self.schedules.current_check_params.execution_times then npc.add_schedule_check() end @@ -1118,7 +1119,7 @@ function npc.schedule_check(self) 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 + if self.schedules.current_check_params.execution_count < self.schedules.current_check_params.execution_times then npc.add_schedule_check() end @@ -1156,7 +1157,26 @@ mobs:register_mob("advanced_npc:npc", { {"npc_male4.png"}, {"npc_male5.png"}, {"npc_male6.png"}, + {"npc_male7.png"}, + {"npc_male8.png"}, + {"npc_male9.png"}, + {"npc_male10.png"}, + {"npc_male11.png"}, + {"npc_male12.png"}, + {"npc_male13.png"}, + {"npc_male14.png"}, + {"npc_male15.png"}, {"npc_female1.png"}, -- female by nuttmeg20 + {"npc_female2.png"}, + {"npc_female3.png"}, + {"npc_female4.png"}, + {"npc_female5.png"}, + {"npc_female6.png"}, + {"npc_female7.png"}, + {"npc_female8.png"}, + {"npc_female9.png"}, + {"npc_female10.png"}, + {"npc_female11.png"}, }, child_texture = { {"npc_child_male1.png"}, @@ -1206,13 +1226,13 @@ mobs:register_mob("advanced_npc:npc", { local item = clicker:get_wielded_item() local name = clicker:get_player_name() - npc.log("DEBUG", "Right-clicked NPC: "..dump(self)) + npc.log("INFO", "Right-clicked NPC: "..dump(self)) -- 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())) - if self.can_have_relationship - and self.can_receive_gifts + 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()] @@ -1232,16 +1252,15 @@ mobs:register_mob("advanced_npc:npc", { end, name ) - else - npc.start_dialogue(self, clicker, true) - end - - end, + 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 + -- 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 @@ -1250,7 +1269,7 @@ mobs:register_mob("advanced_npc:npc", { -- NPC is initialized, check other variables -- Check child texture issues if self.is_child then - -- Check texture + -- Check texture npc.texture_check.timer = npc.texture_check.timer + dtime if npc.texture_check.timer > npc.texture_check.interval then -- Reset timer @@ -1266,19 +1285,20 @@ mobs:register_mob("advanced_npc:npc", { -- Set interval to large interval so this code isn't called frequently npc.texture_check.interval = 60 end - end + end + end - -- Timer function for casual traders to reset their trade offers + -- 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 + 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] @@ -1291,15 +1311,15 @@ mobs:register_mob("advanced_npc:npc", { relationship.talk_timer_value = relationship.talk_timer_value + dtime else -- Relationship decrease timer - if relationship.relationship_decrease_timer_value + if relationship.relationship_decrease_timer_value < relationship.relationship_decrease_interval then - relationship.relationship_decrease_timer_value = + 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) >= + if (relationship.points - 0.5) >= npc.relationships.RELATIONSHIP_PHASE["phase5"].limit then relationship.points = relationship.points - 0.5 end @@ -1335,104 +1355,103 @@ mobs:register_mob("advanced_npc:npc", { 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 - 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 + -- 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) < dtime) 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 + 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 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 + npc.log("INFO", "Executing schedule entry: "..dump(schedule[time][i])) + -- 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 - 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 - else - -- Check if lock can be released - if time % 1 > 0.1 then - -- Release lock - self.schedules.lock = false - end - 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 + else + -- Check if lock can be released + if (time % 1) > dtime then + -- Release lock + self.schedules.lock = false + end + end + end + return self.freeze end }) diff --git a/occupations/occupations.lua b/occupations/occupations.lua index 08dea73..2a27b7f 100644 --- a/occupations/occupations.lua +++ b/occupations/occupations.lua @@ -6,7 +6,7 @@ -- Occupations are essentially specific schedules, that can have slight -- random variations to provide diversity and make specific occupations -- less predictable. Occupations are associated with textures, dialogues, --- specific initial items, type of building (and surroundings) where NPC +-- specific initial items, type of building (and surroundings) where NPC -- lives, etc. -- Example of an occupation: farmer -- The farmer will have to live in a farm, or just beside a field. @@ -38,10 +38,10 @@ -- tags = {}, -- -- Array of tags to search for. This will have tags -- -- if the type is either "mix" or "tags" --- +-- -- }, -- textures = {}, --- -- Textures are an array of textures, as usually given on +-- -- Textures are an array of textures, as usually given on -- -- an entity definition. If given, the NPC will be guaranteed -- -- to have one of the given textures. Also, ensure they have sex -- -- as well in the filename so they can be chosen appropriately. @@ -76,10 +76,10 @@ -- [23] = {[1] = action=npc.action.cmd.freeze, args={freeze=false}} -- -- } -- -- The numbers, [1], [13] and [23] are the times when the entries --- -- corresponding to each are supposed to happen. The tables with +-- -- corresponding to each are supposed to happen. The tables with -- -- [1], [1],[2] and [1] actions respectively are the entries that -- -- will happen at time 1, 13 and 23. --- } +-- } -- Public API npc.occupations = {} @@ -91,14 +91,11 @@ local occupations = {} -- The key is the name of the occupation. npc.occupations.registered_occupations = {} --- This is the basic occupation definition, this is for all NPCs that +-- This is the basic occupation definition, this is for all NPCs that -- don't have a specific occupation. It serves as an example. npc.occupations.basic_def = { -- Use random textures - textures = { - {"npc_male1.png"}, - {"npc_child_male1.png"} - }, + textures = {}, -- Use random dialogues dialogues = {}, -- Initialize inventory with random items @@ -109,13 +106,13 @@ npc.occupations.basic_def = { [7] = { -- Get out of bed [1] = {task = npc.actions.cmd.USE_BED, args = { - pos = npc.places.PLACE_TYPE.BED.PRIMARY, + pos = npc.places.PLACE_TYPE.BED.PRIMARY, action = npc.actions.const.beds.GET_UP - } - }, + } + }, -- Walk to home inside [2] = {task = npc.actions.cmd.WALK_TO_POS, args = { - end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, + end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, walkable = {} }, chance = 75 @@ -127,7 +124,7 @@ npc.occupations.basic_def = { [8] = { -- Walk to outside of home [1] = {task = npc.actions.cmd.WALK_TO_POS, args = { - end_pos = npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, + end_pos = npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, walkable = {} }, chance = 75 @@ -142,7 +139,7 @@ npc.occupations.basic_def = { end_pos = {place_type=npc.places.PLACE_TYPE.SITTABLE.PRIMARY, use_access_node=true}, walkable = {"cottages:bench"} }, - chance = 75 + chance = 75 }, -- Sit on the node [2] = {task = npc.actions.cmd.USE_SITTABLE, args = { @@ -155,13 +152,13 @@ npc.occupations.basic_def = { [3] = {action = npc.actions.cmd.SET_INTERVAL, args = { freeze = true, interval = 35 - }, + }, depends = {2} }, [4] = {action = npc.actions.cmd.SET_INTERVAL, args = { freeze = true, interval = npc.actions.default_interval - }, + }, depends = {3} }, -- Get up from sit @@ -210,9 +207,9 @@ npc.occupations.basic_def = { }, -- Get inside home [3] = {task = npc.actions.cmd.WALK_TO_POS, args = { - end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, + end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, walkable = {} - } + } }, -- Allow mobs_redo wandering [4] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}} @@ -220,16 +217,16 @@ npc.occupations.basic_def = { -- Schedule entry for 10 in the evening [22] = { [1] = {task = npc.actions.cmd.WALK_TO_POS, args = { - end_pos = {place_type=npc.places.PLACE_TYPE.BED.PRIMARY, use_access_node=true}, + 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, + 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}} } @@ -239,7 +236,8 @@ npc.occupations.basic_def = { -- This function registers an occupation function npc.occupations.register_occupation(name, def) - npc.occupations.registered_occupations[name] = def + npc.occupations.registered_occupations[name] = def + npc.log("INFO", "Successfully registered occupation with name: "..dump(name)) end -- This function scans all registered occupations and filter them by @@ -256,13 +254,13 @@ function npc.occupations.get_for_building(building_type, surrounding_building_ty -- Check if building type is contained in the def's building types if npc.utils.array_contains(def.building_types, building_type) then -- Check for empty or nil surrounding building types - if def.surrounding_building_types == nil + if def.surrounding_building_types == nil or def.surrounding_building_types == {} then -- Add this occupation table.insert(result, name) else -- Check if surround type is included in the def - if npc.utils.array_is_subset_of_array(def.surrounding_building_types, + if npc.utils.array_is_subset_of_array(def.surrounding_building_types, surrounding_building_types) then -- Add this occupation table.insert(result, name) @@ -291,7 +289,7 @@ function npc.occupations.initialize_occupation_values(self, occupation_name) -- Initialize textures, else it will leave the current textures minetest.log("Texture entries: "..dump(table.getn(def.textures))) if def.textures and table.getn(def.textures) > 0 then - self.selected_texture = + self.selected_texture = npc.get_random_texture_from_array(self.sex, self.age, def.textures) -- Set texture if it found for sex and age minetest.log("No textures found for sex "..dump(self.sex).." and age "..dump(self.age)) @@ -351,7 +349,7 @@ function npc.occupations.initialize_occupation_values(self, occupation_name) -- Add keys to set of dialogue keys for _, key in npc.utils.get_map_keys(dialogues) do table.insert(dialogue_keys, key) - end + end elseif def.dialogues.type == "tags" then -- We need to find the dialogues from tags. def.dialogues.tags contains -- an array of tags that we will use to search. @@ -361,7 +359,7 @@ function npc.occupations.initialize_occupation_values(self, occupation_name) end -- Add dialogues to NPC -- Check if there is a max of dialogues to be added - local max_dialogue_count = npc.dialogue.MAX_DIALOGUES + local max_dialogue_count = npc.dialogue.MAX_DIALOGUES if def.dialogues.max_count and def.max_dialogue_count > 0 then max_dialogue_count = def.dialogues.max_count end @@ -385,4 +383,4 @@ function npc.occupations.initialize_occupation_values(self, occupation_name) npc.log("INFO", "Successfully initialized NPC with occupation values") -end \ No newline at end of file +end diff --git a/spawner.lua b/spawner.lua index a72d4ca..c5ac0b9 100644 --- a/spawner.lua +++ b/spawner.lua @@ -1,15 +1,19 @@ -- Advanced NPC spawner by Zorman2000 -- The advanced spawner will contain functionality to spawn NPC correctly on --- mg_villages building. The spawn node will be the mg_villages:plotmarker. --- This node will be replaced with one that will perform the following functions: --- +-- custom places, as well as in mg_villages building. +-- This works by using a special node to spawn NPCs on either a custom building or +-- on mg_villages building. + +-- mg_villages functionality: +-- The spawn node for mg_villages will be the mg_villages:plotmarker. +-- Based on this node, the following things will be performed -- - Scan the current building, check if it is of type: -- - House -- - Farm -- - Hut -- - NOTE: All other types are unsupported as-of now -- - If it's from any of the above types, the spawner will proceed to scan the --- the building and find out: +-- the building and find out: -- - Number and positions of beds -- - Number and positions of benches -- - Number and positions of chests @@ -67,173 +71,252 @@ 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}} - }, - -- Noon actions: go inside the house - -- This will be executed around 12 PM MTG time - noon_actions = { - -- Walk to a sittable node - [1] = {task = npc.actions.cmd.WALK_TO_POS, args = { - end_pos = {place_type=npc.places.PLACE_TYPE.SITTABLE.PRIMARY, use_access_node=true}, - walkable = {"cottages:bench"} - } - }, - -- Sit on the node - [2] = {task = npc.actions.cmd.USE_SITTABLE, args = { - pos = npc.places.PLACE_TYPE.SITTABLE.PRIMARY, - action = npc.actions.const.sittable.SIT - } - }, - -- Stay put into place - [3] = {action = npc.actions.cmd.FREEZE, args = {freeze = true}} - }, - -- Afternoon actions: go inside the house - -- This will be executed around 1 PM MTG time - afternoon_actions = { - -- Get up of the sit - [1] = {task = npc.actions.cmd.USE_SITTABLE, args = { - pos = npc.places.PLACE_TYPE.SITTABLE.PRIMARY, - action = npc.actions.const.sittable.GET_UP - } - }, - -- Give NPC money to buy from player - [2] = {property = npc.schedule_properties.put_multiple_items, args = { - itemlist = { - {name="default:iron_lump", random=true, min=2, max=4} - } - } - }, - -- Change trader status to "trader" - [3] = {property = npc.schedule_properties.trader_status, args = { - status = npc.trade.TRADER - } - }, - [4] = {property = npc.schedule_properties.can_receive_gifts, args = { - value = true - } - }, - -- Allow mobs_redo wandering - [5] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}} - }, - -- Afternoon actions: go inside the house - -- This will be executed around 6 PM MTG time - late_afternoon_actions = { - -- Change trader status to "none" - [1] = {property = npc.schedule_properties.trader_status, args = { - status = npc.trade.NONE - } - }, - -- Enable gift receiving again - [2] = {property = npc.schedule_properties.can_receive_gifts, args = { - can_receive_gifts = true - } - }, - -- Get inside home - [3] = {task = npc.actions.cmd.WALK_TO_POS, args = { - end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, - walkable = {}} - }, - -- Allow mobs_redo wandering - [4] = {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 +-- Array of nodes that serve as plotmarker of a plot, and therefore +-- as auto-spawners +spawner.plotmarker_nodes = {} +-- Array of items that are used to spawn NPCs +spawner.spawn_eggs = {} --------------------------------------------------------------------------------------- -- Scanning functions --------------------------------------------------------------------------------------- -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 + 2 then - table.insert(result, node) +-- This function scans a 3D area that encloses a building and tries to identify: +-- - Entrance door +-- - Beds +-- - Storage nodes (chests, etc.) +-- - Furnace nodes +-- - Sittable nodes +-- It will return a table with all information gathered +-- Playername should be provided if manual spawning +function npc.spawner.scan_area_for_spawn(start_pos, end_pos, player_name) + local result = { + building_type = "", + building_plot_info = {}, + building_entrance = {}, + building_usable_nodes = {}, + building_npcs = {}, + building_npc_stats = {} + } + + -- Set building_type + result.building_type = "custom" + -- Get min pos and max pos + local minp, maxp = vector.sort(start_pos, end_pos) + -- Set plot info + result.building_plot_info = { + -- TODO: Check this and see if it is accurate! + xsize = maxp.x - minp.x, + ysize = maxp.y - minp.y, + zsize = maxp.z - minp.z, + start_pos = start_pos, + end_pos = end_pos + } + + -- Scan building nodes + -- Scan building for nodes + local usable_nodes = npc.places.scan_area_for_usable_nodes(start_pos, end_pos) + -- Get all doors + local doors = usable_nodes.openable_type + + -- Find entrance node - this is very tricky when no outside position + -- is given. So to this end, three things will happen: + -- - First, we will check for plotmarker nodes. A plotmarker node should + -- be set at the left of the front door of the building. If this node is + -- found, it will assume it is at that location and use it. + -- - Second, we are going to search for an entrance marker. The entrance marker + -- will be directly in the posiition of the entrance node, so no search + -- is needed. + -- - Third, will assume that the start_pos is always at the left side of + -- the front of the building, where the entrance is + local outside_pos = start_pos + -- Check if there is a plotmarker or spawner node + local candidate_nodes = minetest.find_nodes_in_area_under_air(start_pos, end_pos, + {"mg_villages:plotmarker", "advanced_npc:auto_spawner"}) + if table.getn(candidate_nodes) > 0 then + -- Found plotmarker, use it as outside_pos. Ideally should be only one + outside_pos = candidate_nodes[1] + elseif npc.spawner_marker and player_name then + -- Get entrance from spawner marker1 + if npc.spawner_marker.entrance_markers[player_name] then + outside_pos = npc.spawner_marker.entrance_markers[player_name] + end end - end - return result + -- Try to find entrance + local entrance = npc.places.find_entrance_from_openable_nodes(doors, outside_pos) + if entrance then + npc.log("INFO", "Found building entrance at: "..minetest.pos_to_string(entrance.node_pos)) + -- Set building entrance + result.building_entrance = entrance + else + npc.log("ERROR", "Unable to find building entrance!") + end + + -- Set node_data + result.building_usable_nodes = usable_nodes + + -- Initialize NPC stats + -- Initialize NPC stats + local npc_stats = { + male = { + total = 0, + adult = 0, + child = 0 + }, + female = { + total = 0, + adult = 0, + child = 0 + }, + adult_total = 0, + child_total = 0 + } + result.building_npc_stats = npc_stats + + return result end --- Creates an array of {pos=, owner=''} for managing --- which NPC owns what -function spawner.get_nodes_by_type(start_pos, end_pos, type) - local result = {} - local nodes = npc.places.find_node_in_area(start_pos, end_pos, type) - --minetest.log("Found "..dump(#nodes).." nodes of type: "..dump(type)) - for _,node_pos in pairs(nodes) do - local entry = {} - entry["node_pos"] = node_pos - entry["owner"] = '' - table.insert(result, entry) - end - return result +--------------------------------------------------------------------------------------- +-- Spawning functions +--------------------------------------------------------------------------------------- + +-- This function is called when the node timer for spawning NPC +-- is expired. Can be called manually by supplying either: +-- - Position of mg_villages plotmarker, or, +-- - position of custom building spawner +-- Prerequisite for calling this function is: +-- - In case of mg_villages, spawner.adapt_mg_villages_plotmarker(), or, +-- - in case of custom buildings, npc.spawner.scan_area_for_spawn() +function npc.spawner.spawn_npc_on_plotmarker(pos) + -- Get timer + local timer = minetest.get_node_timer(pos) + -- Get metadata + local meta = minetest.get_meta(pos) + -- Get current NPC info + local area_info = {} + area_info["npcs"] = minetest.deserialize(meta:get_string("npcs")) + -- Get NPC stats + area_info["npc_stats"] = minetest.deserialize(meta:get_string("npc_stats")) + -- Get node data + area_info["entrance"] = minetest.deserialize(meta:get_string("entrance")) + area_info["node_data"] = minetest.deserialize(meta:get_string("node_data")) + -- Check amount of NPCs that should be spawned + area_info["npc_count"] = meta:get_int("npc_count") + area_info["spawned_npc_count"] = meta:get_int("spawned_npc_count") + + -- TODO: Get occupation name + local metadata = npc.spawner.spawn_npc(pos, area_info) + + -- Set all metadata back into the node + -- Increase NPC spawned count + area_info.spawned_npc_count = area_info.spawned_npc_count + 1 + -- Store count into node + meta:set_int("spawned_npc_count", area_info.spawned_npc_count) + -- Store spawned NPC info + meta:set_string("npcs", minetest.serialize(area_info.npcs)) + -- Store NPC stats + meta:set_string("npc_stats", minetest.serialize(area_info.npc_stats)) + + -- Check if there are more NPCs to spawn + if area_info.spawned_npc_count >= area_info.npc_count then + -- Stop timer + npc.log("INFO", "No more NPCs to spawn at this location") + timer:stop() + else + -- Start another timer to spawn more NPC + local new_delay = math.random(npc.spawner.spawn_delay) + npc.log("INFO", "Spawning one more NPC in "..dump(npc.spawner.spawn_delay).."s") + timer:start(new_delay) + end end --- Scans an area for the supported nodes: beds, benches, --- furnaces, storage (e.g. chests) and openable (e.g. doors). --- Returns a table with these classifications -function spawner.scan_area(pos1, pos2) +-- This function spawns a NPC into the given pos. +-- If area_info is given, updated area_info is returned at end +function npc.spawner.spawn_npc(pos, area_info, occupation_name) + -- Get current NPC info + local npc_table = area_info.npcs + -- Get NPC stats + local npc_stats = area_info.npc_stats + -- Get building entrance + local entrance = area_info.entrance + -- Get node data + local node_data = area_info.node_data + -- Check amount of NPCs that should be spawned + local npc_count = area_info.npc_count + local spawned_npc_count = area_info.spawned_npc_count + -- Check if we actually have these variables - if we don't, it is because + -- this is a manually spawned NPC + local can_spawn = false + if npc_count and spawned_npc_count then + npc.log("INFO", "Currently spawned "..dump(spawned_npc_count).." of "..dump(npc_count).." NPCs") + if spawned_npc_count < npc_count then + can_spawn = true + end + else + -- Manually spawned + can_spawn = true + end - local result = { - bed_type = {}, - sittable_type = {}, - furnace_type = {}, - storage_type = {}, - openable_type = {} - } - local start_pos, end_pos = vector.sort(pos1, pos2) - - result.bed_type = spawner.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.BED_TYPE) - result.sittable_type = spawner.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.SITTABLE_TYPE) - -- Filter out - result.furnace_type = spawner.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.FURNACE_TYPE) - result.storage_type = spawner.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.STORAGE_TYPE) - result.openable_type = spawner.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.OPENABLE_TYPE) - - --minetest.log("Found nodes inside area: "..dump(result)) - return result + if can_spawn then + npc.log("INFO", "Spawning NPC at "..minetest.pos_to_string(pos)) + -- Spawn a NPC + 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 - use given or default + local occupation = occupation_name or "default_basic" + -- Initialize NPC + -- Call with stats if there are NPCs + if npc_table and #npc_table > 0 then + npc.initialize(ent, pos, false, npc_stats, occupation) + else + npc.initialize(ent, pos, nil, nil, occupation) + end + -- If entrance and node_data are present, assign nodes + if entrance and node_data then + npc.spawner.assign_places(ent:get_luaentity(), entrance, node_data, pos) + end + -- Store spawned NPC data and stats into node + local age = npc.age.adult + if ent:get_luaentity().child then + age = npc.age.child + end + -- TODO: Add more information here at some time... + local entry = { + status = npc.spawner.spawn_data.status.alive, + name = ent:get_luaentity().name, + id = ent:get_luaentity().npc_id, + sex = ent:get_luaentity().sex, + age = age, + born_day = minetest.get_day_count() + } + minetest.log("Area info: "..dump(area_info)) + table.insert(area_info.npcs, entry) + -- Update and store stats + -- Increase total of NPCs for specific sex + npc_stats[ent:get_luaentity().sex].total = + npc_stats[ent:get_luaentity().sex].total + 1 + -- Increase total number of NPCs by age + npc_stats[age.."_total"] = npc_stats[age.."_total"] + 1 + -- Increase number of NPCs by age and sex + npc_stats[ent:get_luaentity().sex][age] = + npc_stats[ent:get_luaentity().sex][age] + 1 + area_info.npc_stats = npc_stats + -- Return + npc.log("INFO", "Spawning successful!") + return true + else + npc.log("ERROR", "Spawning failed!") + ent:remove() + return false + end + end end -- This function will assign places to every NPC that belongs to a specific --- house/building. It will use the resources of the house and give them +-- house/building. It will use the resources of the building and give them -- until there's no more. Call this function after NPCs are initialized -- The basic assumption: --- - Use only items that are up to y+3 (first floor of building) for now -- - Tell the NPC where the furnaces are -- - Assign a unique bed to the NPC -- - If there are as many chests as beds, assign one to a NPC @@ -241,400 +324,314 @@ end -- - If there are as many benches as beds, assign one to a NPC -- - Else, just let the NPC know one of the benches, but not own them -- - 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 entrance = minetest.deserialize(meta:get_string("entrance")) - local node_data = minetest.deserialize(meta:get_string("node_data")) +-- Self is the NPC lua entity object, pos is the position of the NPC spawner. +-- Prerequisite for using this function is to have called either +-- - In case of mg_villages, spawner.adapt_mg_villages_plotmarker(), or, +-- - in case of custom buildings, npc.spawner.scan_area_for_spawn() +-- Both function set the required metadata for this function +-- For mg_villages, this will be the position of the plotmarker node. +function npc.spawner.assign_places(self, entrance, node_data, pos) + minetest.log("Received node_data: "..dump(node_data)) + --local meta = minetest.get_meta(pos) + --local entrance = minetest.deserialize(meta:get_string("entrance")) + --local node_data = minetest.deserialize(meta:get_string("node_data")) - -- Assign plotmarker - npc.places.add_shared(self, npc.places.PLACE_TYPE.OTHER.HOME_PLOTMARKER, - npc.places.PLACE_TYPE.OTHER.HOME_PLOTMARKER, pos) + -- Assign plotmarker + if pos then + npc.places.add_shared(self, npc.places.PLACE_TYPE.OTHER.HOME_PLOTMARKER, + npc.places.PLACE_TYPE.OTHER.HOME_PLOTMARKER, pos) + end - -- Assign entrance door and related locations - if entrance ~= nil and entrance.node_pos ~= nil then - npc.places.add_shared(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) - -- Assign these places to NPC - npc.places.add_shared(self, npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, entrance_inside) - npc.places.add_shared(self, npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, entrance_outside) - end + -- Assign entrance door and related locations + if entrance ~= nil and entrance.node_pos ~= nil then + npc.places.add_shared(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) + -- Assign these places to NPC + npc.places.add_shared(self, npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, entrance_inside) + npc.places.add_shared(self, npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, entrance_outside) + end - -- Assign beds - if #node_data.bed_type > 0 then - -- Assign a specific sittable node to a NPC. - 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)) - end + -- Assign beds + if #node_data.bed_type > 0 then + -- Assign a specific sittable node to a NPC. + 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)) + end - -- Assign sits - if #node_data.sittable_type > 0 then - -- 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_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)) - end - -- Add all sits to places as shared since NPC should be able to sit - -- at any accessible sit - npc.places.add_shared_accessible_place(self, node_data.sittable_type, - npc.places.PLACE_TYPE.SITTABLE.SHARED) - end + -- Assign sits + if #node_data.sittable_type > 0 then + -- 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_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)) + end + -- Add all sits to places as shared since NPC should be able to sit + -- at any accessible sit + npc.places.add_shared_accessible_place(self, node_data.sittable_type, + npc.places.PLACE_TYPE.SITTABLE.SHARED) + end - -- Assign furnaces - if #node_data.furnace_type > 0 then - -- 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_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)) - end - -- Add all furnaces to places as shared since NPC should be able to use - -- any accessible furnace - npc.places.add_shared_accessible_place(self, node_data.furnace_type, - npc.places.PLACE_TYPE.FURNACE.SHARED) - end + -- Assign furnaces + if #node_data.furnace_type > 0 then + -- 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_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)) + end + -- Add all furnaces to places as shared since NPC should be able to use + -- any accessible furnace + npc.places.add_shared_accessible_place(self, node_data.furnace_type, + npc.places.PLACE_TYPE.FURNACE.SHARED) + end - -- Assign storage nodes - if #node_data.storage_type > 0 then - -- 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_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)) - end - -- Add all storage-types to places as shared since NPC should be able - -- to use other storage nodes as well. - npc.places.add_shared_accessible_place(self, node_data.storage_type, - npc.places.PLACE_TYPE.STORAGE.SHARED) - end + -- Assign storage nodes + if #node_data.storage_type > 0 then + -- 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_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)) + end + -- Add all storage-types to places as shared since NPC should be able + -- to use other storage nodes as well. + npc.places.add_shared_accessible_place(self, node_data.storage_type, + npc.places.PLACE_TYPE.STORAGE.SHARED) + end - npc.log("DEBUG", "Places for NPC "..self.npc_name..": "..dump(self.places_map)) + npc.log("DEBUG", "Places for NPC "..self.npc_name..": "..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) - -- TODO: In the future, this needs to actually take into account - -- type of building and different schedules, e.g. farmers, traders, etc. - 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 noon actions - npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 12, nil, basic_schedule.noon_actions) - - -- Add schedule entry for afternoon actions - npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 13, nil, basic_schedule.afternoon_actions) - - -- Add schedule entry for late afternoon actions - npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 18, nil, basic_schedule.late_afternoon_actions) - - -- Add schedule entry for evening actions - npc.add_schedule_entry(self, npc.schedule_types.generic, 0, 22, nil, basic_schedule.evening_actions) -end - --- This function is called when the node timer for spawning NPC --- is expired -function npc.spawner.spawn_npc(pos) - -- Get timer - local timer = minetest.get_node_timer(pos) - -- Get metadata - local meta = minetest.get_meta(pos) - -- Get current NPC info - local npc_table = minetest.deserialize(meta:get_string("npcs")) - -- Get NPC stats - local npc_stats = minetest.deserialize(meta:get_string("npc_stats")) - -- Check amount of NPCs that should be spawned - local npc_count = meta:get_int("npc_count") - local spawned_npc_count = meta:get_int("spawned_npc_count") - npc.log("INFO", "Currently spawned "..dump(spawned_npc_count).." of "..dump(npc_count).." NPCs") - if spawned_npc_count < npc_count then - npc.log("INFO", "Spawning NPC at "..minetest.pos_to_string(pos)) - -- Spawn a NPC - 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 and #npc_table > 0 then - npc.initialize(ent, pos, false, npc_stats, occupation_name) - else - 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) - -- Increase NPC spawned count - spawned_npc_count = spawned_npc_count + 1 - -- Store count into node - meta:set_int("spawned_npc_count", spawned_npc_count) - -- Store spawned NPC data and stats into node - local age = npc.age.adult - if ent:get_luaentity().child then - age = npc.age.child - end - -- TODO: Add more information here at some time... - local entry = { - status = npc.spawner.spawn_data.status.alive, - name = ent:get_luaentity().name, - id = ent:get_luaentity().npc_id, - sex = ent:get_luaentity().sex, - age = age, - born_day = minetest.get_day_count() - } - table.insert(npc_table, entry) - meta:set_string("npcs", minetest.serialize(npc_table)) - -- Update and store stats - -- Increase total of NPCs for specific sex - npc_stats[ent:get_luaentity().sex].total = - npc_stats[ent:get_luaentity().sex].total + 1 - -- Increase total number of NPCs by age - npc_stats[age.."_total"] = npc_stats[age.."_total"] + 1 - -- Increase number of NPCs by age and sex - npc_stats[ent:get_luaentity().sex][age] = - npc_stats[ent:get_luaentity().sex][age] + 1 - meta:set_string("npc_stats", minetest.serialize(npc_stats)) - -- Temp - --meta:set_string("infotext", meta:get_string("infotext")..", "..spawned_npc_count) - npc.log("INFO", "Spawning successful!") - -- Check if there are more NPCs to spawn - if spawned_npc_count >= npc_count then - -- Stop timer - npc.log("INFO", "No more NPCs to spawn at this location") - timer:stop() - else - -- Start another timer to spawn more NPC - local new_delay = math.random(npc.spawner.spawn_delay) - npc.log("INFO", "Spawning one more NPC in "..dump(npc.spawner.spawn_delay).."s") - timer:start(new_delay) - end - return true - else - npc.log("ERROR", "Spawning failed!") - ent:remove() - return false + -- If entrance is available let NPC + if entrance then + 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 - end - + + return node_data end -- This function takes care of calculating how many NPCs will be spawn -function spawner.calculate_npc_spawning(pos) - -- Check node metadata - local meta = minetest.get_meta(pos) - if meta:get_string("replaced") ~= "true" then - return - end - -- Get nodes for this building - local node_data = minetest.deserialize(meta:get_string("node_data")) - if node_data == nil then - npc.log("ERROR", "Mis-configured mg_villages:plotmarker at position: "..minetest.pos_to_string(pos)) - return - end - -- Check number of beds - local beds_count = #node_data.bed_type--#spawner.filter_first_floor_nodes(node_data.bed_type, pos) - - npc.log("DEBUG", "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 - -- a single NPC. - if beds_count == 0 or (beds_count > 0 and beds_count / 2 < 1) then - -- Spawn a single NPC - npc_count = 1 - else - -- Spawn (beds_count/2) NPCs - npc_count = ((beds_count / 2) - ((beds_count / 2) % 1)) - end - npc.log("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) - local delay = math.random(npc.spawner.spawn_delay) - timer:start(delay) +-- Prerequisite for calling this function is: +-- - In case of mg_villages, spawner.adapt_mg_villages_plotmarker(), or, +-- - in case of custom buildings, npc.spawner.scan_area_for_spawn() +function npc.spawner.calculate_npc_spawning_on_plotmarker(pos) + -- Check node metadata + local meta = minetest.get_meta(pos) + if meta:get_string("replaced") ~= "true" then + return + end + -- Get nodes for this building + local node_data = minetest.deserialize(meta:get_string("node_data")) + if node_data == nil then + npc.log("ERROR", "Mis-configured spawner at position: "..minetest.pos_to_string(pos)) + return + end + -- Check number of beds + local beds_count = #node_data.bed_type--#spawner.filter_first_floor_nodes(node_data.bed_type, pos) + + npc.log("DEBUG", "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 + -- a single NPC. + if beds_count == 0 or (beds_count > 0 and beds_count / 2 < 1) then + -- Spawn a single NPC + npc_count = 1 + else + -- Spawn (beds_count/2) NPCs + npc_count = ((beds_count / 2) - ((beds_count / 2) % 1)) + end + npc.log("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) + local delay = math.random(npc.spawner.spawn_delay) + timer:start(delay) end +--------------------------------------------------------------------------------------- +-- Spawner nodes and items +--------------------------------------------------------------------------------------- +-- The following are included: +-- - Auto-spawner: Basically a custom mg_villages:plotmarker that can be used +-- for custom buildings +-- - Manual spawner: This custom spawn item (egg) will show a formspec when used. +-- The formspec will allow the player the name of the NPC, the occupation and +-- the plot, entrance and workplace of the NPC. All of these are optional and +-- default values will be chosen whenever no input is provided. + + --------------------------------------------------------------------------------------- -- Support code for mg_villages mods --------------------------------------------------------------------------------------- - --- This function creates a table of the scannable nodes inside --- a mg_villages building. It needs the plotmarker position for a start --- 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("--------------------------------------------") - -- Get area of the building - local x_size = building_data.bsizex - local y_size = building_data.ysize - local z_size = building_data.bsizez - local brotate = building_data.brotate - local start_pos = {x=pos.x, y=pos.y, z=pos.z} - 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 - -- 3 - facing South -Z - if brotate == 0 then - x_sign, z_sign = 1, -1 - elseif brotate == 1 then - x_sign, z_sign = -1, -1 - local temp = z_size - z_size = x_size - x_size = temp - elseif brotate == 2 then - x_sign, z_sign = -1, 1 - elseif brotate == 3 then - x_sign, z_sign = 1, 1 - end - - ------------------------ - -- For debug: - ------------------------ - -- Red is x marker - --minetest.set_node({x=pos.x + (x_sign * x_size),y=pos.y,z=pos.z}, {name = "wool:red"}) - --minetest.get_meta({x=pos.x + (x_sign * x_size),y=pos.y,z=pos.z}):set_string("infotext", minetest.get_meta(pos):get_string("infotext")..", Axis: x, Sign: "..dump(x_sign)) - -- Blue is z marker - --minetest.set_node({x=pos.x,y=pos.y,z=pos.z + (z_sign * z_size)}, {name = "wool:blue"}) - --minetest.get_meta({x=pos.x,y=pos.y,z=pos.z + (z_sign * z_size)}):set_string("infotext", minetest.get_meta(pos):get_string("infotext")..", Axis: z, Sign: "..dump(z_sign)) - - npc.log("DEBUG", "Start pos: "..minetest.pos_to_string(start_pos)) - npc.log("DEBUG", "Plot: "..dump(minetest.get_meta(start_pos):get_string("infotext"))) - - npc.log("DEBUG", "Brotate: "..dump(brotate)) - npc.log("DEBUG", "X_sign: "..dump(x_sign)) - npc.log("DEBUG", "X_adj: "..dump(x_sign*x_size)) - npc.log("DEBUG", "Z_sign: "..dump(z_sign)) - npc.log("DEBUG", "Z_adj: "..dump(z_sign*z_size)) - - local end_pos = {x=pos.x + (x_sign * x_size), y=pos.y + y_size, z=pos.z + (z_sign * z_size)} - - -- For debug: - --minetest.set_node(start_pos, {name="default:mese_block"}) - --minetest.set_node(end_pos, {name="default:mese_block"}) - --minetest.get_meta(end_pos):set_string("infotext", minetest.get_meta(start_pos):get_string("infotext")) - - npc.log("DEBUG", "Calculated end pos: "..minetest.pos_to_string(end_pos)) - - return spawner.scan_area(start_pos, end_pos) -end - --- This function replaces an existent mg_villages:plotmarker with --- and advanced_npc:auto_spawner. The existing metadata will be kept, --- to allow compatibility. A new formspec will appear on right-click, --- however it will as well allow to buy or manage the plot. --- Also, the building is scanned for NPC-usable nodes and the amount --- of NPCs to spawn and the interval is calculated. -function spawner.replace_mg_villages_plotmarker(pos) - -- Get the meta at the current position - local meta = minetest.get_meta(pos) - local village_id = meta:get_string("village_id") - local plot_nr = meta:get_int("plot_nr") - local infotext = meta:get_string("infotext") - -- Check for nil values above - if (not village_id or (village and village == "")) - or (not plot_nr or (plot_nr and plot_nr == 0)) then - return - end - -- Following line from mg_villages mod, protection.lua - local btype = mg_villages.all_villages[village_id].to_add_data.bpos[plot_nr].btype - local building_data = mg_villages.BUILDINGS[btype] - local building_type = building_data.typ - -- Check if the building is of the support types - for _,value in pairs(npc.spawner.mg_villages_supported_building_types) do - - if building_type == value then - - npc.log("INFO", "Replacing mg_villages:plotmarker at "..minetest.pos_to_string(pos)) - -- Replace the plotmarker for auto-spawner - --minetest.set_node(pos, {name="advanced_npc:plotmarker_auto_spawner"}) - -- Store old plotmarker metadata again - meta:set_string("village_id", village_id) - meta:set_int("plot_nr", plot_nr) - meta:set_string("infotext", infotext) - -- Store building type in metadata - meta:set_string("building_type", building_type) - -- Store plot information - local plot_info = mg_villages.all_villages[village_id].to_add_data.bpos[plot_nr] - plot_info["ysize"] = building_data.ysize - -- minetest.log("Plot info at replacement time: "..dump(plot_info)) - 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 - npc.log("INFO", "Found building entrance at: "..minetest.pos_to_string(entrance.node_pos)) - else - npc.log("ERROR", "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 - local npcs = {} - meta:set_string("npcs", minetest.serialize(npcs)) - -- Initialize NPC stats - local npc_stats = { - male = { - total = 0, - adult = 0, - child = 0 - }, - female = { - total = 0, - adult = 0, - child = 0 - }, - adult_total = 0, - child_total = 0 - } - meta:set_string("npc_stats", minetest.serialize(npc_stats)) - -- Set replaced - meta:set_string("replaced", "true") - -- Calculate how many NPCs will spawn - spawner.calculate_npc_spawning(pos) - -- Stop searching for building type - break - end - end -end - --- Only register the node, the ABM and the LBM if mg_villages mod --- is present if minetest.get_modpath("mg_villages") ~= nil then + -- This function creates a table of the scannable nodes inside + -- a mg_villages building. It needs the plotmarker position for a start + -- 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("--------------------------------------------") + -- Get area of the building + local x_size = building_data.bsizex + local y_size = building_data.ysize + local z_size = building_data.bsizez + local brotate = building_data.brotate + local start_pos = {x=pos.x, y=pos.y, z=pos.z} + 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 + -- 3 - facing South -Z + if brotate == 0 then + x_sign, z_sign = 1, -1 + elseif brotate == 1 then + x_sign, z_sign = -1, -1 + local temp = z_size + z_size = x_size + x_size = temp + elseif brotate == 2 then + x_sign, z_sign = -1, 1 + elseif brotate == 3 then + x_sign, z_sign = 1, 1 + end + + ------------------------ + -- For debug: + ------------------------ + -- Red is x marker + --minetest.set_node({x=pos.x + (x_sign * x_size),y=pos.y,z=pos.z}, {name = "wool:red"}) + --minetest.get_meta({x=pos.x + (x_sign * x_size),y=pos.y,z=pos.z}):set_string("infotext", minetest.get_meta(pos):get_string("infotext")..", Axis: x, Sign: "..dump(x_sign)) + -- Blue is z marker + --minetest.set_node({x=pos.x,y=pos.y,z=pos.z + (z_sign * z_size)}, {name = "wool:blue"}) + --minetest.get_meta({x=pos.x,y=pos.y,z=pos.z + (z_sign * z_size)}):set_string("infotext", minetest.get_meta(pos):get_string("infotext")..", Axis: z, Sign: "..dump(z_sign)) + + npc.log("DEBUG", "Start pos: "..minetest.pos_to_string(start_pos)) + npc.log("DEBUG", "Plot: "..dump(minetest.get_meta(start_pos):get_string("infotext"))) + + npc.log("DEBUG", "Brotate: "..dump(brotate)) + npc.log("DEBUG", "X_sign: "..dump(x_sign)) + npc.log("DEBUG", "X_adj: "..dump(x_sign*x_size)) + npc.log("DEBUG", "Z_sign: "..dump(z_sign)) + npc.log("DEBUG", "Z_adj: "..dump(z_sign*z_size)) + + local end_pos = {x=pos.x + (x_sign * x_size), y=pos.y + y_size, z=pos.z + (z_sign * z_size)} + + -- For debug: + --minetest.set_node(start_pos, {name="default:mese_block"}) + --minetest.set_node(end_pos, {name="default:mese_block"}) + --minetest.get_meta(end_pos):set_string("infotext", minetest.get_meta(start_pos):get_string("infotext")) + + npc.log("DEBUG", "Calculated end pos: "..minetest.pos_to_string(end_pos)) + + return npc.places.scan_area_for_usable_nodes(start_pos, end_pos) + end + + -- This function "adapts" an existent mg_villages:plotmarker for NPC spawning. + -- The existing metadata will be kept, to allow compatibility. A new formspec + -- will appear on right-click, however it will as well allow to buy or manage + -- the plot. Also, the building is scanned for NPC-usable nodes and the amount + -- of NPCs to spawn and the interval is calculated. + function spawner.adapt_mg_villages_plotmarker(pos) + -- Get the meta at the current position + local meta = minetest.get_meta(pos) + local village_id = meta:get_string("village_id") + local plot_nr = meta:get_int("plot_nr") + local infotext = meta:get_string("infotext") + -- Check for nil values above + if (not village_id or (village_id and village_id == "")) + or (not plot_nr or (plot_nr and plot_nr == 0)) then + return + end + -- TODO: This should be replaced with new mg_villages API call + -- Following line from mg_villages mod, protection.lua + local btype = mg_villages.all_villages[village_id].to_add_data.bpos[plot_nr].btype + local building_data = mg_villages.BUILDINGS[btype] + local building_type = building_data.typ + -- Check if the building is of the support types + for _,value in pairs(npc.spawner.mg_villages_supported_building_types) do + + if building_type == value then + + npc.log("INFO", "Replacing mg_villages:plotmarker at "..minetest.pos_to_string(pos)) + -- Store plotmarker metadata again + meta:set_string("village_id", village_id) + meta:set_int("plot_nr", plot_nr) + meta:set_string("infotext", infotext) + + -- Store building type in metadata + meta:set_string("building_type", building_type) + -- Store plot information + local plot_info = mg_villages.all_villages[village_id].to_add_data.bpos[plot_nr] + plot_info["ysize"] = building_data.ysize + -- minetest.log("Plot info at replacement time: "..dump(plot_info)) + 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 + npc.log("INFO", "Found building entrance at: "..minetest.pos_to_string(entrance.node_pos)) + else + npc.log("ERROR", "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 + local npcs = {} + meta:set_string("npcs", minetest.serialize(npcs)) + -- Initialize NPC stats + local npc_stats = { + male = { + total = 0, + adult = 0, + child = 0 + }, + female = { + total = 0, + adult = 0, + child = 0 + }, + adult_total = 0, + child_total = 0 + } + meta:set_string("npc_stats", minetest.serialize(npc_stats)) + -- Set replaced + meta:set_string("replaced", "true") + -- Calculate how many NPCs will spawn + npc.spawner.calculate_npc_spawning_on_plotmarker(pos) + -- Stop searching for building type + break + end + end + end + -- Node registration -- This node is currently a slightly modified mg_villages:plotmarker -- TODO: Change formspec to a more detailed one. @@ -678,7 +675,7 @@ if minetest.get_modpath("mg_villages") ~= nil then -- end, on_timer = function(pos, elapsed) - npc.spawner.spawn_npc(pos) + npc.spawner.spawn_npc_on_plotmarker(pos) end, -- protect against digging @@ -718,13 +715,21 @@ if minetest.get_modpath("mg_villages") ~= nil then catch_up = true, action = function(pos, node, active_object_count, active_object_count_wider) -- Check if replacement is needed + local meta = minetest.get_meta(pos) + if meta then + -- minetest.log("------ Plotmarker metadata -------") + -- local plot_nr = meta:get_int("plot_nr") + -- local village_id = meta:get_string("village_id") + -- minetest.log("Plot nr: "..dump(plot_nr)..", village ID: "..dump(village_id)) + -- minetest.log(dump(mg_villages.get_plot_and_building_data( village_id, plot_nr ))) + end if minetest.get_meta(pos):get_string("replaced") == "true" then return end -- Check if replacement is activated if npc.spawner.replace_activated then -- Replace mg_villages:plotmarker - spawner.replace_mg_villages_plotmarker(pos) + spawner.adapt_mg_villages_plotmarker(pos) end end }) @@ -739,8 +744,9 @@ minetest.register_chatcommand("restore_plotmarkers", { privs = {server=true}, func = function(name, param) -- Check if radius is null - if param == nil then + if param == nil and type(param) ~= "number" then minetest.chat_send_player(name, "Need to enter a radius as an integer number. Ex. /restore_plotmarkers 10 for a radius of 10") + return end -- Get player position local pos = {} @@ -754,7 +760,7 @@ minetest.register_chatcommand("restore_plotmarkers", { local radius = tonumber(param) 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, + local nodes = minetest.find_nodes_in_area_under_air(start_pos, end_pos, {"mg_villages:plotmarker"}) -- Check if we have nodes to replace minetest.chat_send_player(name, "Found "..dump(#nodes).." nodes to replace...") @@ -780,4 +786,4 @@ minetest.register_chatcommand("restore_plotmarkers", { end minetest.chat_send_player(name, "Finished replacement of "..dump(#nodes).." auto-spawners successfully") end -}) \ No newline at end of file +}) diff --git a/spawner_marker.lua b/spawner_marker.lua new file mode 100644 index 0000000..533007d --- /dev/null +++ b/spawner_marker.lua @@ -0,0 +1,3 @@ +-- Spawner markers +-- Specialized functionality to allow players do NPC spawning +-- on their own custom buildings. diff --git a/textures/npc_female10.png b/textures/npc_female10.png new file mode 100644 index 0000000..6631697 Binary files /dev/null and b/textures/npc_female10.png differ diff --git a/textures/npc_female11.png b/textures/npc_female11.png new file mode 100644 index 0000000..1be74aa Binary files /dev/null and b/textures/npc_female11.png differ diff --git a/textures/npc_female2.png b/textures/npc_female2.png new file mode 100644 index 0000000..5f8060b Binary files /dev/null and b/textures/npc_female2.png differ diff --git a/textures/npc_female3.png b/textures/npc_female3.png new file mode 100644 index 0000000..321a1f4 Binary files /dev/null and b/textures/npc_female3.png differ diff --git a/textures/npc_female4.png b/textures/npc_female4.png new file mode 100644 index 0000000..7dd3e7f Binary files /dev/null and b/textures/npc_female4.png differ diff --git a/textures/npc_female5.png b/textures/npc_female5.png new file mode 100644 index 0000000..992d3fc Binary files /dev/null and b/textures/npc_female5.png differ diff --git a/textures/npc_female6.png b/textures/npc_female6.png new file mode 100644 index 0000000..26719db Binary files /dev/null and b/textures/npc_female6.png differ diff --git a/textures/npc_female7.png b/textures/npc_female7.png new file mode 100644 index 0000000..51eb352 Binary files /dev/null and b/textures/npc_female7.png differ diff --git a/textures/npc_female8.png b/textures/npc_female8.png new file mode 100644 index 0000000..90ef44f Binary files /dev/null and b/textures/npc_female8.png differ diff --git a/textures/npc_female9.png b/textures/npc_female9.png new file mode 100644 index 0000000..24bc4f4 Binary files /dev/null and b/textures/npc_female9.png differ diff --git a/textures/npc_male10.png b/textures/npc_male10.png new file mode 100644 index 0000000..c3e2548 Binary files /dev/null and b/textures/npc_male10.png differ diff --git a/textures/npc_male11.png b/textures/npc_male11.png new file mode 100644 index 0000000..10e7db0 Binary files /dev/null and b/textures/npc_male11.png differ diff --git a/textures/npc_male12.png b/textures/npc_male12.png new file mode 100644 index 0000000..19b4fc4 Binary files /dev/null and b/textures/npc_male12.png differ diff --git a/textures/npc_male13.png b/textures/npc_male13.png new file mode 100644 index 0000000..95f4ff8 Binary files /dev/null and b/textures/npc_male13.png differ diff --git a/textures/npc_male14.png b/textures/npc_male14.png new file mode 100644 index 0000000..73c2b39 Binary files /dev/null and b/textures/npc_male14.png differ diff --git a/textures/npc_male15.png b/textures/npc_male15.png new file mode 100644 index 0000000..d55800e Binary files /dev/null and b/textures/npc_male15.png differ diff --git a/textures/npc_male8.png b/textures/npc_male8.png new file mode 100644 index 0000000..a1c0847 Binary files /dev/null and b/textures/npc_male8.png differ diff --git a/textures/npc_male9.png b/textures/npc_male9.png new file mode 100644 index 0000000..7c1a218 Binary files /dev/null and b/textures/npc_male9.png differ