-- Occupations/jobs functionality by Zorman2000 ----------------------------------------------- -- Occupations functionality -- NPCs need an occupation or job in order to simulate being alive. -- This functionality is built on top of the schedules functionality. -- 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 -- lives, etc. -- Example of an occupation: farmer -- The farmer will have to live in a farm, or just beside a field. -- It will have the following schedule: -- 6AM - get out of bed, walk to home inside, goes to chest, retrieves -- seeds and wander -- 7AM - goes out to the field and randomly start harvesting and planting -- crops that are already fully grown -- 12PM - gets a random but moderate (5-15) amount of seeds and harvested -- - crops. Goes into the house, stores 1/4 of the amount in a chest, -- - gets all currency items it has, and sits into a bench -- 1PM - goes outside the house and becomes trader, sells the remaining -- - seeds and crops -- 6PM - goes inside the house. Stores all currency items it has, all -- - remainin seeds and crops, and sits on a bench -- 8PM - gets out of the bench, wanders inside home -- 10PM - goes to bed -- Implementation: -- A function, npc.register_occupation(), will be provided to register an -- occupation that can be used to initialize NPCs. The format is the following: -- { -- dialogues = { -- enable_gift_item_dialogues = true, -- -- This flag enables/disables gift item dialogues. -- -- If not set, it defaults to true. -- type = "", -- -- The type can be "given", "mix" or "tags" -- data = {}, -- -- Array of dialogue definitions. This will have dialogue -- -- if the type is either "mix" or "given" -- 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 -- -- 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. -- -- If left empty, it can spawn with any texture. -- building_types = {}, -- -- An array of string where each string is the type of building -- -- where the NPC can spawn with this occupation. -- -- Example: building_type = {"farm", "house"} -- -- If left empty or nil, NPC can spawn in any building -- surrounding_building_types = {}, -- -- An array of string where each string is the type of building -- -- that is an immediate neighbor of the NPC's home which can also -- -- be suitable for this occupation. Example, if NPC is farmer and -- -- spawns on house, then it has to be because there is a field -- -- nearby. If left empty or nil, surrounding buildings doesn't -- -- matter -- workplace_nodes = {}, -- -- An array of string where each string is a node the NPC -- -- works with. These are useful for assigning workplaces and work -- -- work nodes. -- initial_inventory = {}, -- -- An array of entries like the following: -- -- {name="", count=1} -- or -- -- {name="", random=true, min=1, max=10} -- -- This will initialize the inventory for the NPC with the given -- -- items and the specified count, or, a count between min and max -- -- when the entry contains random=true -- -- If left empty, it will initialize with random items. -- initial_trader_status = "", -- -- String that specifies initial trader value. Valid values are: -- -- "casual", "trader", "none" -- schedules_entries = {}, -- -- This is a table of tables in the following format: -- -- { -- [1] = {[1] = action = npc.action.cmd.freeze, args={freeze=true}}, -- [13] = {[1] = action = npc.action.cmd.freeze, args={freeze=false}, -- [2] = action = npc.action.cmd.freeze, args={freeze=true} -- }, -- [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 -- -- [1], [1],[2] and [1] actions respectively are the entries that -- -- will happen at time 1, 13 and 23. -- } -- Public API npc.occupations = {} -- Private API local occupations = {} -- This array contains all the registered occupations. -- The key is the name of the occupation. npc.occupations.registered_occupations = {} -- Basic occupation name npc.occupations.basic_name = "default_basic" -- 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 = {}, -- Use random dialogues dialogues = {}, -- Initialize inventory with random items initial_inventory = {}, -- Initialize schedule schedules_entries = { -- Schedule entry for 7 in the morning [7] = { -- 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 to home inside [2] = {task = npc.actions.cmd.WALK_TO_POS, args = { end_pos = npc.places.PLACE_TYPE.OTHER.HOME_INSIDE, walkable = {} }, chance = 75 }, -- Allow mobs_redo wandering [3] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}} }, -- Schedule entry for 7 in the morning [8] = { -- Walk to outside of home [1] = {task = npc.actions.cmd.WALK_TO_POS, args = { end_pos = npc.places.PLACE_TYPE.OTHER.HOME_OUTSIDE, walkable = {} }, chance = 75 }, -- Allow mobs_redo wandering [2] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}} }, -- Schedule entry for 12 midday [12] = { -- 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"} }, chance = 75 }, -- 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 }, depends = {1} }, -- Stay put into place [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 [5] = {action = npc.actions.cmd.USE_SITTABLE, args = { pos = npc.places.PLACE_TYPE.SITTABLE.PRIMARY, action = npc.actions.const.sittable.GET_UP }, depends = {4} } }, -- Schedule entry for 1 in the afternoon [13] = { -- Give NPC money to buy from player [1] = {property = npc.schedule_properties.put_multiple_items, args = { itemlist = { {name="default:iron_lump", random=true, min=2, max=4} } }, chance = 75 }, -- Change trader status to "trader" [2] = {property = npc.schedule_properties.trader_status, args = { status = npc.trade.TRADER }, chance = 75 }, [3] = {property = npc.schedule_properties.can_receive_gifts, args = { can_receive_gifts = false }, depends = {1} }, -- Allow mobs_redo wandering [4] = {action = npc.actions.cmd.FREEZE, args = {freeze = false}} }, -- Schedule entry for 6 in the evening [18] = { -- 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}} }, -- 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}, 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}} } } } -- This function registers an occupation function npc.occupations.register_occupation(name, def) -- Register all dialogues per definition local dialogue_keys = {} if def.dialogues then -- Check which type of dialogues we have if def.dialogues.type == "given" then -- We have been given the dialogues, so def.dialogues.data contains -- an array of dialogues for _, dialogue in pairs(def.dialogues.data) do -- Add to the dialogue tags the "occupation name" table.insert(dialogue.tags, name) -- Register dialogue npc.log("INFO", "Registering dialogue for occupation "..dump(name)..": "..dump(dialogue)) local key = npc.dialogue.register_dialogue(dialogue) -- Add key to set of dialogue keys table.insert(dialogue_keys, key) end elseif def.dialogues.type == "mix" then -- We have been given the dialogues, so def.dialogues.data contains -- an array of dialogues and def.dialogues.tags contains an array of -- tags. Currently only registering will be performed. -- Register dialogues for _, dialogue in pairs(def.dialogues.data) do -- Add to the dialogue tags the "occupation name" table.insert(dialogue.tags, name) -- Register dialogue local key = npc.dialogue.register_dialogue(dialogue) -- Add key to set of dialogue keys table.insert(dialogue_keys, key) end end end -- Save into the definition the dialogue keys def.dialogues["keys"] = dialogue_keys -- Save the definition 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 -- building type and surrounding building type, returning an array -- of occupation names (strings) -- BEWARE! Below this lines lies ugly, incomprehensible code! function npc.occupations.get_for_building(building_type, surrounding_building_types) local result = {} for name,def in pairs(npc.occupations.registered_occupations) do -- Check for empty or nil building types, in that case, any building if def.building_types == nil or def.building_types == {} and def.surrounding_building_types == nil or def.surrounding_building_types == {} then --minetest.log("Empty") -- Empty building types, add to result table.insert(result, name) elseif def.building_types ~= nil and #def.building_types > 0 then -- Check if building type is contained in the def's building types if npc.utils.array_contains(def.building_types, building_type) then table.insert(result, name) end end -- Check for empty or nil surrounding building types if def.surrounding_building_types ~= nil and #def.surrounding_building_types > 0 then -- -- Add this occupation -- --table.insert(result, name) -- else -- Surrounding buildings is not empty, loop though them and compare -- to the given ones for i = 1, #surrounding_building_types do for j = 1, #def.surrounding_building_types do -- Check if the definition's surrounding building type is the same -- as the given one if def.surrounding_building_types[j].type == surrounding_building_types[i].type then -- Check if the origin buildings contain the expected type if npc.utils.array_contains(def.surrounding_building_types[j].origin_building_types, surrounding_building_types[i].origin_building_type) then -- Add this occupation table.insert(result, name) end end end end end end return result end -- This function will initialize entities values related to -- the occupation: textures, dialogues, inventory items and -- will set schedules accordingly. function npc.occupations.initialize_occupation_values(self, occupation_name) -- Get occupation definition local def = npc.occupations.registered_occupations[occupation_name] if not def then npc.log("WARNING", "No definition found for occupation name: "..dump(occupation_name)) return end npc.log("INFO", "Overriding NPC values using occupation '"..dump(occupation_name).."' values") -- Initialize textures, else it will leave the current textures if def.textures and table.getn(def.textures) > 0 then self.selected_texture = npc.get_random_texture_from_array(self.sex, self.age, def.textures) -- Set texture if it found for sex and age -- If an array was returned, select a random texture from it if type(self.selected_texture) == "table" then local selected_texture = self.selected_texture[math.random(1, #self.selected_texture)] self.selected_texture = selected_texture end -- Set texture and base texture self.textures = {self.selected_texture} self.base_texture = {self.selected_texture } -- Assign sex based on texture self.sex = npc.assign_sex_from_texture(self) -- Refresh entity self.object:set_properties(self) end -- Initialize inventory if def.initial_inventory and table.getn(def.initial_inventory) > 0 then for i = 1, #def.initial_inventory do local item = def.initial_inventory[i] -- Check if item count is randomized if item.random and item.min and item.max then npc.add_item_to_inventory(self, item.name, math.random(item.min, item.max)) else -- Add item with the given count npc.add_item_to_inventory(self, item.name, item.count) end end end -- Initialize dialogues if def.dialogues then -- Check for gift item dialogues enable if def.dialogues.disable_gift_item_dialogues then self.dialogues.hints = {} end local dialogue_keys = {} -- Check which type of dialogues we have if def.dialogues.type == "given" and def.dialogues.keys then -- We have been given the dialogues, so def.dialogues.data contains -- an array of dialogues. These dialogues were registered, therefore we need -- just the keys for i = 1, #def.dialogues.keys do table.insert(dialogue_keys, def.dialogues.keys[i]) end elseif def.dialogues.type == "mix" then -- We have been given the dialogues, so def.dialogues.data contains -- an array of dialogues and def.dialogues.tags contains an array of -- tags that we will use to search if def.dialogues.keys then -- Add the registered dialogues for i = 1, #def.dialogues.keys do table.insert(dialogue_keys, def.dialogues.keys[i]) end end -- Find dialogues using tags local dialogues = npc.search_dialogue_by_tags(def.dialogues.tags, true) -- Add keys to set of dialogue keys for _, key in pairs(npc.utils.get_map_keys(dialogues)) do table.insert(dialogue_keys, key) 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. local dialogues = npc.search_dialogue_by_tags(def.dialogues.tags, true) -- Add keys to set of dialogue keys dialogue_keys = npc.utils.get_map_keys(dialogues) end -- Add dialogues to NPC -- Check if there is a max of dialogues to be added local max_dialogue_count = npc.dialogue.MAX_DIALOGUES if def.dialogues.max_count and def.dialogues.max_count > 0 then max_dialogue_count = def.dialogues.max_count end -- Add dialogues to the normal dialogues for NPC if #dialogue_keys > 0 then self.dialogues.normal = {} for i = 1, math.min(max_dialogue_count, #dialogue_keys) do self.dialogues.normal[i] = dialogue_keys[i] end end end -- Initialize properties minetest.log("def.properties: "..dump(def.properties)) if def.properties then -- Initialize trader status if def.properties.initial_trader_status then self.trader_data.trader_status = def.properties.initial_trader_status end -- Enable/disable gift items hints if def.properties.enable_gift_items_hints ~= nil then self.gift_data.enable_gift_items_hints = def.properties.enable_gift_items_hints end end -- Initialize schedule entries if def.schedules_entries and table.getn(npc.utils.get_map_keys(def.schedules_entries)) > 0 then -- Create schedule in NPC npc.create_schedule(self, npc.schedule_types.generic, 0) -- Traverse schedules for time, entries in pairs(def.schedules_entries) do -- Add schedule entry for each time npc.add_schedule_entry(self, npc.schedule_types.generic, 0, time, nil, entries) end end npc.log("INFO", "Successfully initialized NPC with occupation values") end