diff --git a/OLDmods/README.md b/OLDmods/README.md index 34c7fe3..a784c12 100644 --- a/OLDmods/README.md +++ b/OLDmods/README.md @@ -3,3 +3,4 @@ their name for reference : villagers: not needed and suspected to slow down chests; darkage: seems to add game-time map generation; + advanced_npc: seems to slow down gameplay; diff --git a/mods/advanced_npc/.gitignore b/mods/advanced_npc/.gitignore deleted file mode 100644 index 2b977e6..0000000 --- a/mods/advanced_npc/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.idea/ -backup/ -debug.txt -advanced_npc.iml \ No newline at end of file diff --git a/mods/advanced_npc/README.md b/mods/advanced_npc/README.md deleted file mode 100644 index afafe32..0000000 --- a/mods/advanced_npc/README.md +++ /dev/null @@ -1,46 +0,0 @@ -advanced_npc -============ - -Introduction ------------- - -Advanced NPC is a mod for Minetest using mobs_redo API. -The goal of this mod is to be able to have live villages in Minetest. These NPCs are highly inspired by the typical NPCs of _Harvest Moon_ games. The general idea is that on almost all buildings of a village there are NPCs that are kind of intelligent: they have daily tasks they perform, can speak to players, can trade with the player, can use their own items (chests and furnaces for example), know where to go around their house and village, can be lumbers, miners or any other Minetest-suitable profession and can ultimately engage into relationships with the player. And while basically only players are mentioned here, the ultimate goal is that they can do all of this also among themselves, so that villages are alive and evolving by themselves, without player intervention. - - -Installation ------------- - -__NOTE__: Advanced NPC is still under development. While the mod is largely stable, it lacks one of the most important pieces: spawning. Currently, NPCs can be spawned using eggs (found in creative inventory as 'NPC') and by themselves on villages of the [mg_villages mod](https://forum.minetest.net/viewtopic.php?t=13589). NPCs will spawn automatically on mg_villages villages and over time will populate the entire village. If something goes wrong, you can reset the village by: - - Clearing all objects (in chat, type /clearobjects quick) - - Restore original plotmarkers (in chat, type /restore_plotmarkers radius) - - The radius can be any number, but it is recommended you use a not so large number. 200 is suitable. So stand in the middle of the village and then run that command. -This will actually restore the village and will slowly make NPCs spawn again. Currently there's no way to disable NPCs spawning on village, except by going to `spawner.lua` and commenting out all of `minetest.register_abm()` code. - -__Download__ the mod [here](https://github.com/hkzorman/advanced_npc/archive/master.zip) (link always pointing to latest version) - -For this mod to work correctly, you also need to install the [mobs_redo](https://github.com/tenplus1/mobs_redo) mod. After installation, make sure you enable it in your world. - - -License -------- - -__advanced_npc__ is Copyright (C) 2016-2017 Hector Franqui (zorman2000), licensed under the GPLv3 license. See `license.txt` for details. - -The `pathfinder.lua` file contains code slighlty modified from the [pathfinder mod](https://github.com/MarkuBu/pathfinder) by MarkBu, which is licensed as WTFPL. See `actions/pathfinder.lua` for details. - -Current NPC textures are from mobs_redo mod. -The following textures are by Zorman2000: -- marriage_ring.png - CC BY-SA - - -Documentation and API ---------------------- - -This mod requires a good user manual, and also is planned to have an extensive API, properly documented. Unfortunately, these still aren't ready. A very very very WIP manual can be found in the [wiki](https://github.com/hkzorman/advanced_npc/wiki/Concept%3A-Dialogues) - - -Roadmap -------- - -See it on the [wiki](https://github.com/hkzorman/advanced_npc/wiki). diff --git a/mods/advanced_npc/depends.txt b/mods/advanced_npc/depends.txt deleted file mode 100755 index 40071e0..0000000 --- a/mods/advanced_npc/depends.txt +++ /dev/null @@ -1,4 +0,0 @@ -default -mobs -mg_villages? -intllib? \ No newline at end of file diff --git a/mods/advanced_npc/description.txt b/mods/advanced_npc/description.txt deleted file mode 100755 index cb9659d..0000000 --- a/mods/advanced_npc/description.txt +++ /dev/null @@ -1 +0,0 @@ -Adds NPCs which are smart, have homes, can talk, trade and even establish friendships and more with you! \ No newline at end of file diff --git a/mods/advanced_npc/dialogue.lua b/mods/advanced_npc/dialogue.lua deleted file mode 100644 index f6968be..0000000 --- a/mods/advanced_npc/dialogue.lua +++ /dev/null @@ -1,695 +0,0 @@ -------------------------------------------------------------------------------------- --- NPC dialogue code by Zorman2000 -------------------------------------------------------------------------------------- - -npc.dialogue = {} - -npc.dialogue.POSITIVE_GIFT_ANSWER_PREFIX = "Yes, give " -npc.dialogue.NEGATIVE_ANSWER_LABEL = "Nevermind" - -npc.dialogue.MIN_DIALOGUES = 2 -npc.dialogue.MAX_DIALOGUES = 4 - -npc.dialogue.dialogue_type = { - married = 1, - casual_trade = 2, - dedicated_trade = 3, - custom_trade = 4 -} - --- This table contains the answers of dialogue boxes -npc.dialogue.dialogue_results = { - options_dialogue = {}, - yes_no_dialogue = {} -} - -npc.dialogue.tags = { - UNISEX = "unisex", - MALE = "male", - FEMALE = "female", - -- Relationship based tags - these are one-to-one with the - -- phase names. - DEFAULT_MARRIED_DIALOGUE = "default_married_dialogue", - PHASE_1 = "phase1", - PHASE_2 = "phase2", - PHASE_3 = "phase3", - PHASE_4 = "phase4", - PHASE_5 = "phase5", - GIFT_ITEM_HINT = "gift_item_hint", - GIFT_ITEM_RESPONSE = "gift_item_response", - GIFT_ITEM_LIKED = "gift_item_liked", - GIFT_ITEM_UNLIKED = "gift_item_unliked", - -- Trade-related tags - DEFAULT_CASUAL_TRADE = "default_casual_trade_dialogue", - DEFAULT_DEDICATED_TRADE = "default_dedicated_trade_dialogue", - DEFAULT_BUY_OFFER = "buy_offer", - DEFAULT_SELL_OFFER = "sell_offer", - -- Occupation-based tags - these are one-to-one with the - -- default occupation names - BASIC = "basic", -- Dialogues related to the basic occupation should - -- use this. As basic occupation is generic, any occupation - -- should be able to use these dialogues. - DEFAULT_FARMER = "default_farmer", - DEFAULT_COOKER = "default_cooker" -} - --- This table will contain all the registered dialogues for NPCs -npc.dialogue.registered_dialogues = {} - -npc.dialogue.cache_keys = { - CASUAL_BUY_DIALOGUE = {key="CASUAL_BUY_DIALOGUE", tags={npc.dialogue.tags.DEFAULT_CASUAL_TRADE, npc.dialogue.tags.DEFAULT_BUY_OFFER}}, - CASUAL_SELL_DIALOGUE = {key="CASUAL_SELL_DIALOGUE", tags={npc.dialogue.tags.DEFAULT_CASUAL_TRADE, npc.dialogue.tags.DEFAULT_SELL_OFFER}}, - DEDICATED_TRADER_DIALOGUE = {key="DEDICATED_TRADER_DIALOGUE", tags={npc.dialogue.tags.DEFAULT_DEDICATED_TRADE}}, - MARRIED_DIALOGUE = {key="MARRIED_DIALOGUE", tags={npc.dialogue.tags.DEFAULT_MARRIED_DIALOGUE}}, -} - -npc.dialogue.cache = {} - --------------------------------------------------------------------------------------- --- Dialogue registration functions --- All dialogues will be registered by providing a definition. --- A unique key will be assigned to them. The dialogue definition is the following: --- { --- text: "", --- ^ The "spoken" dialogue line --- flag: --- ^ If the flag with the specified name has the specified value --- then this dialogue is valid --- { --- name: "" --- ^ Name of the flag --- value: --- ^ Expected value of the flag. A flag can be a function. In such a case, it is --- expected the function will return this value. --- }, --- tags = { --- -- Tags are an array of string that allow to classify dialogues --- -- A dialogue can have as many tags as desired and can take any form. --- -- However, for consistency, some predefined tags can be found at --- -- npc.dialogue.tags. --- -- Example: --- "phase1", --- "any" --- } --- responses = { --- -- Array of responses the player can choose. A response can be of --- -- two types: as [1] or as [2] (see example below) --- [1] = { --- text = "Yes", --- -- Text displayed to the player --- action_type = "dialogue", --- -- Type of action that happens when the player chooses this response. --- -- can be "dialogue" or "function". This example shows "dialogue" --- action = { --- text = "It's so beautiful, and big, and large, and infinite, and..." --- }, --- }, --- -- A table containing a dialogue. This means you can include not only --- -- text but also flag and responses as well. Dialogues are recursive. --- [2] = { --- text = "No", --- action_type = "function", --- action = function(self, player) --- -- A function will have access to self, which is the NPC --- -- and the player, which is the player ObjectRef. You can --- -- pretty much do anything here. The example here is very simple, --- -- just sending a chat message. But you can add items to players --- -- or to NPCs and so on. --- minetest.chat_send_player(player:get_player_name(), "Oh, ok...") --- end, --- }, --- } --- } --------------------------------------------------------------------------------------- --- This function sets a unique response ID (made of :) to --- each response that features a function. This is to be able to locate the --- function easily later -local function set_response_ids_recursively(dialogue, depth, dialogue_id) - -- Base case: dialogue object with no responses and no responses below it - if dialogue.responses == nil - and (dialogue.action_type == "dialogue" and dialogue.action.responses == nil) then - return - elseif dialogue.responses ~= nil then - -- Assign a response ID to each response - local response_id_prefix = tostring(depth)..":" - for key,value in ipairs(dialogue.responses) do - if value.action_type == "function" then - value.response_id = response_id_prefix..key - value.dialogue_id = dialogue_id - else - -- We have a dialogue action type. Need to check if dialogue has further responses - if value.action.responses ~= nil then - set_response_ids_recursively(value.action, depth + 1, dialogue_id) - end - end - end - end -end - --- The register dialogue function will just receive the definition as --- explained above. The unique key will be the index it gets into the --- array when inserted. -function npc.dialogue.register_dialogue(def) - -- If def has not tags then apply the default ones - if not def.tags then - def.tags = {npc.dialogue.tags.UNISEX, npc.dialogue.tags.PHASE_1} - end - - local dialogue_id = table.getn(npc.dialogue.registered_dialogues) + 1 - -- Set the response IDs - required for dialogue objects that - -- form trees of dialogues - set_response_ids_recursively(def, 0, dialogue_id) - - def.key = dialogue_id - - -- Insert dialogue into table - table.insert(npc.dialogue.registered_dialogues, def) - return dialogue_id -end - --- This function returns a table of dialogues that meet the given --- tags array. The keys in the table are the keys in --- npc.dialogue.registered_dialogues, therefore you can use them to ---retrieve specific dialogues. However, it should be stored by the NPC. -function npc.dialogue.search_dialogue_by_tags(tags, find_all) - --minetest.log("Tags being searched: "..dump(tags)) - local result = {} - for key, def in pairs(npc.dialogue.registered_dialogues) do - -- Check if def.tags have any of the provided tags - local tags_found = 0 - --minetest.log("Tags on dialogue def: "..dump(def.tags)) - for i = 1, #tags do - if npc.utils.array_contains(def.tags, tags[i]) then - tags_found = tags_found + 1 - end - end - --minetest.log("Tags found: "..dump(tags_found)) - -- Check if we found all tags - if find_all then - if tags_found == #tags then - -- Add result - result[key] = def - end - elseif not find_all then - if tags_found == #tags or tags_found == #def.tags then - -- Add result - result[key] = def - end - end - end - return result -end - -function npc.dialogue.get_cached_dialogue_key(_cache_key, tags) - local cache_key = _cache_key - if type(_cache_key) == "table" then - cache_key = _cache_key.key - tags = _cache_key.tags - end - - local key = npc.dialogue.cache[cache_key] - -- Check if key isn't cached - if not key then - -- Search for the dialogue - local dialogues = npc.dialogue.search_dialogue_by_tags(tags, true) - key = npc.utils.get_map_keys(dialogues)[1] - -- Populate cache - npc.dialogue.cache[cache_key] = key - -- Return key - return key - else - -- Return the cached key - return key - end -end - --------------------------------------------------------------------------------------- --- Dialogue box definitions --- The dialogue boxes are used for the player to interact with the --- NPC in dialogues. --------------------------------------------------------------------------------------- --- Creates and shows a multi-option dialogue based on the number of responses --- that the dialogue object contains -function npc.dialogue.show_options_dialogue(self, -dialogue_key, -dialogue, -dismiss_option_label, -player_name) - local responses = dialogue.responses - local options_length = table.getn(responses) + 1 - local formspec_height = (options_length * 0.7) + 0.4 - local formspec = "size[7,"..tostring(formspec_height).."]" - - for i = 1, #responses do - local y = 0.8; - if i > 1 then - y = (0.75 * i) - end - formspec = formspec.."button_exit[0.5," - ..(y - 0.5)..";6,0.5;opt"..tostring(i)..";"..responses[i].text.."]" - end - formspec = formspec.."button_exit[0.5," - ..(formspec_height - 0.7)..";6,0.5;exit;"..dismiss_option_label.."]" - - -- Create entry on options_dialogue table - npc.dialogue.dialogue_results.options_dialogue[player_name] = { - npc = self, - dialogue = dialogue, - dialogue_key = dialogue_key, - is_married_dialogue = - (dialogue.dialogue_type == npc.dialogue.dialogue_type.married), - is_custom_trade_dialogue = - (dialogue.dialogue_type == npc.dialogue.dialogue_type.custom_trade), - casual_trade_type = dialogue.casual_trade_type, - options = responses - } - - minetest.show_formspec(player_name, "advanced_npc:options", formspec) -end - --- This function is used for showing a yes/no dialogue formspec -function npc.dialogue.show_yes_no_dialogue(self, -prompt, -positive_answer_label, -positive_callback, -negative_answer_label, -negative_callback, -player_name) - - npc.exec.set_input_wait_state(self) - - local formspec = "size[7,3]".. - "label[0.5,0.1;"..prompt.."]".. - "button_exit[0.5,1.15;6,0.5;yes_option;"..positive_answer_label.."]".. - "button_exit[0.5,1.95;6,0.5;no_option;"..negative_answer_label.."]" - - -- Create entry into responses table - npc.dialogue.dialogue_results.yes_no_dialogue[player_name] = { - npc = self, - yes_callback = positive_callback, - no_callback = negative_callback - } - - minetest.show_formspec(player_name, "advanced_npc:yes_no", formspec) -end - --------------------------------------------------------------------------------------- --- Dialogue methods --------------------------------------------------------------------------------------- --- Select random dialogue objects for an NPC based on gender --- and the relationship phase with player -function npc.dialogue.select_random_dialogues_for_npc(self, phase) - local result = { - normal = {}, - hints = {} - } - - local phase_tag = "phase1" - if phase then - phase_tag = phase - end - - local search_tags = { - "unisex", - self.gender, - phase_tag, - self.occupation - } - - local dialogues = npc.dialogue.search_dialogue_by_tags(search_tags) - if dialogues and next(dialogues) ~= nil then - local keys = npc.utils.get_map_keys(dialogues) - - -- Determine how many dialogue lines the NPC will have - local number_of_dialogues = math.random(npc.dialogue.MIN_DIALOGUES, npc.dialogue.MAX_DIALOGUES) - - for i = 1, number_of_dialogues do - local key_id = math.random(1, #keys) - result.normal[i] = keys[key_id] - npc.log("DEBUG", "Adding dialogue: "..dump(dialogues[keys[key_id]])) - end - - -- Add item hints. - for i = 1, 2 do - local hints = npc.relationships.get_dialogues_for_gift_item( - self.gift_data.favorite_items["fav"..tostring(i)], - npc.dialogue.tags.GIFT_ITEM_HINT, - npc.dialogue.tags.GIFT_ITEM_LIKED, - self.gender, - phase_tag) - for key, value in pairs(hints) do - result.hints[i] = key - end - end - - for i = 3, 4 do - local hints = npc.relationships.get_dialogues_for_gift_item( - self.gift_data.disliked_items["dis"..tostring(i-2)], - npc.dialogue.tags.GIFT_ITEM_HINT, - npc.dialogue.tags.GIFT_ITEM_UNLIKED, - self.gender) - for key, value in pairs(hints) do - result.hints[i] = key - end - end - end - - npc.log("DEBUG", "Dialogue results:"..dump(result)) - return result -end - --- This function creates a multi-option dialogue from the custom trades that the --- NPC have. -function npc.dialogue.create_custom_trade_options(self, player) - -- Create the action for each option - local actions = {} - for i = 1, #self.trader_data.custom_trades do - table.insert(actions, - function() - npc.trade.show_custom_trade_offer(self, player, self.trader_data.custom_trades[i]) - end) - end - -- Default text to be shown for dialogue prompt - local text = npc.trade.CUSTOM_TRADES_PROMPT_TEXT - -- Get the options from each custom trade entry - local options = {} - if #self.trader_data.custom_trades == 1 then - table.insert(options, self.trader_data.custom_trades[1].button_prompt) - text = self.trader_data.custom_trades[1].option_prompt - else - for i = 1, #self.trader_data.custom_trades do - table.insert(options, self.trader_data.custom_trades[i].button_prompt) - end - end - -- Create dialogue object - local dialogue = npc.dialogue.create_option_dialogue(text, options, actions) - dialogue.dialogue_type = npc.dialogue.dialogue_type.custom_trade - - return dialogue -end - --- This function will choose randomly a dialogue from the NPC data --- and process it. -function npc.dialogue.start_dialogue(self, player, show_married_dialogue) - -- Choose a dialogue randomly - local dialogue = {} - - -- Construct dialogue for marriage - if npc.relationships.get_relationship_phase(self, player:get_player_name()) == "phase6" - and show_married_dialogue == true then - dialogue = npc.relationships.MARRIED_NPC_DIALOGUE - npc.dialogue.process_dialogue(self, dialogue, player:get_player_name()) - return - end - - -- Show options dialogue for dedicated trader - if self.trader_data.trader_status == npc.trade.TRADER then - dialogue = npc.dialogue.get_cached_dialogue_key(npc.dialogue.cache_keys.DEDICATED_TRADER_DIALOGUE) - npc.dialogue.process_dialogue(self, dialogue, player:get_player_name()) - return - end - - local chance = math.random(1, 100) - --minetest.log("Chance: "..dump(chance)) - if chance < 30 then - -- Show trading options for casual traders - -- If NPC has custom trading options, these will be - -- shown as well with equal chance as the casual - -- buy/sell options - if self.trader_data.trader_status == npc.trade.NONE then - -- Show custom trade options if available - if table.getn(self.trader_data.custom_trades) > 0 then - -- Show custom trade options - dialogue = npc.dialogue.create_custom_trade_options(self, player) - else - -- If not available, choose normal dialogue - dialogue = self.dialogues.normal[math.random(1, #self.dialogues.normal)] - end - elseif self.trader_data.trader_status == npc.trade.CASUAL then - local max_trade_chance = 2 - if table.getn(self.trader_data.custom_trades) > 0 then - max_trade_chance = 3 - end - -- Show buy/sell with 50% chance each - local trade_chance = math.random(1, max_trade_chance) - if trade_chance == 1 then - -- Show casual buy dialogue - dialogue = npc.dialogue.get_cached_dialogue_key(npc.dialogue.cache_keys.CASUAL_BUY_DIALOGUE) - elseif trade_chance == 2 then - -- Show casual sell dialogue - dialogue = npc.dialogue.get_cached_dialogue_key(npc.dialogue.cache_keys.CASUAL_SELL_DIALOGUE) - elseif trade_chance == 3 then - -- Show custom trade options - dialogue = npc.dialogue.create_custom_trade_options(self, player) - end - end - elseif chance >= 30 and chance < 90 then - -- Choose a random dialogue from the common ones - dialogue = self.dialogues.normal[math.random(1, #self.dialogues.normal)] - elseif chance >= 90 then - -- Check if gift items hints are enabled - minetest.log("Self gift data enable: "..dump(self.gift_data.enable_gift_items_hints)) - if self.gift_data.enable_gift_items_hints then - -- Choose a random dialogue line from the favorite/disliked item hints - dialogue = self.dialogues.hints[math.random(1, 4)] - else - -- Choose a random dialogue from the common ones - dialogue = self.dialogues.normal[math.random(1, #self.dialogues.normal)] - end - end - - local dialogue_result = npc.dialogue.process_dialogue(self, dialogue, player:get_player_name()) - if dialogue_result == false then - -- Try to find another dialogue line - npc.dialogue.start_dialogue(self, player, show_married_dialogue) - end -end - --- This function processes a dialogue object and performs --- actions depending on what is defined in the object -function npc.dialogue.process_dialogue(self, dialogue, player_name) - -- Freeze NPC actions - npc.exec.set_input_wait_state(self) - --npc.lock_actions(self) - - local dialogue_key = -1 - - if type(dialogue) ~= "table" then - dialogue_key = dialogue - dialogue = npc.dialogue.registered_dialogues[dialogue] - --minetest.log("Found dialogue: "..dump(dialogue)) - end - - -- Check if this dialogue has a flag definition - if dialogue.flag then - -- Check if the NPC has this flag - local flag_value = npc.get_flag(self, dialogue.flag.name) - if flag_value ~= nil then - -- Check if value of the flag is equal to the expected value - if flag_value ~= dialogue.flag.value then - -- Do not process this dialogue - return false - end - else - - if (type(dialogue.flag.value) == "boolean" and dialogue.flag.value ~= false) - or (type(dialogue.flag.value) == "number" and dialogue.flag.value > 0) then - -- Do not process this dialogue - return false - end - end - end - - -- Send dialogue line - if dialogue.text then - npc.chat(self.npc_name, player_name, dialogue.text) - end - - -- Check if dialogue has responses. If it doesn't, unlock the actions - -- queue and reset actions timer.' - if not dialogue.responses then - npc.exec.set_ready_state(self) - end - - -- Check if there are responses, then show multi-option dialogue if there are - if dialogue.responses then - npc.dialogue.show_options_dialogue( - self, - dialogue_key, - dialogue, - npc.dialogue.NEGATIVE_ANSWER_LABEL, - player_name - ) - end - - -- Dialogue object processed successfully - return true -end - -function npc.dialogue.create_option_dialogue(prompt, options, actions) - local result = {} - result.text = prompt - result.responses = {} - for i = 1, #options do - table.insert(result.responses, {text = options[i], action_type="function", action=actions[i]}) - end - return result -end - ------------------------------------------------------------------------------ --- Functions for rotating NPC to look at player --- (taken from the mobs_redo API) ------------------------------------------------------------------------------ -local atan = function(x) - if x ~= x then - return 0 - else - return math.atan(x) - end -end - -function npc.dialogue.rotate_npc_to_player(self) - local s = self.object:getpos() - local objs = minetest.get_objects_inside_radius(s, 4) - local lp = nil - local yaw = 0 - - for n = 1, #objs do - if objs[n]:is_player() then - lp = objs[n]:getpos() - break - end - end - if lp then - local vec = { - x = lp.x - s.x, - y = lp.y - s.y, - z = lp.z - s.z - } - - yaw = (atan(vec.z / vec.x) + math.pi / 2) - self.rotate - - if lp.x > s.x then - yaw = yaw + math.pi - end - end - self.object:setyaw(yaw) -end - ---------------------------------------------------------------------------------------- --- Answer processing functions ---------------------------------------------------------------------------------------- --- This function locates a response object that has function on the dialogue tree. -local function get_response_object_by_id_recursive(dialogue, current_depth, response_id) - if dialogue.responses == nil - and (dialogue.action_type == "dialogue" and dialoge.action.responses == nil) then - return nil - elseif dialogue.responses ~= nil then - -- Get current depth and response ID - local d_i1, d_i2 = string.find(response_id, ":") - --minetest.log("N1: "..dump(string.sub(response_id, 0, d_i1))..", N2: "..dump(string.sub(response_id, 1, d_i1-1))) - local depth = tonumber(string.sub(response_id, 0, d_i1-1)) - local id = tonumber(string.sub(response_id, d_i2 + 1)) - --minetest.log("Depth: "..dump(depth)..", id: "..dump(id)) - -- Check each response - for key,value in ipairs(dialogue.responses) do - --minetest.log("Key: "..dump(key)..", value: "..dump(value)..", comp1: "..dump(current_depth == depth)) - if value.action_type == "function" then - -- Check if we are on correct response and correct depth - if current_depth == depth then - if key == id then - return value - end - end - else - --minetest.log("Entering again...") - -- We have a dialogue action type. Need to check if dialogue has further responses - if value.action.responses ~= nil then - local response = get_response_object_by_id_recursive(value.action, current_depth + 1, response_id) - if response ~= nil then - return response - end - end - end - end - end -end - --- Handler for dialogue formspec -minetest.register_on_player_receive_fields(function (player, formname, fields) - -- Additional checks for other forms should be handled here - -- Handle yes/no dialogue - if formname == "advanced_npc:yes_no" then - local player_name = player:get_player_name() - - if fields then - local player_response = npc.dialogue.dialogue_results.yes_no_dialogue[player_name] - - -- Unlock queue, reset action timer and unfreeze NPC. - npc.exec.set_ready_state(player_response.npc) - - if fields.yes_option then - player_response.yes_callback() - elseif fields.no_option then - player_response.no_callback() - end - end - end - - -- Manage options dialogue - if formname == "advanced_npc:options" then - local player_name = player:get_player_name() - - if fields then - -- Get player response - local player_response = npc.dialogue.dialogue_results.options_dialogue[player_name] - - -- Check if the player hit the negative option or esc button - if fields["exit"] or fields["quit"] == "true" then - -- Unlock queue, reset action timer and unfreeze NPC. - npc.exec.set_ready_state(player_response.npc) - end - - for i = 1, #player_response.options do - local button_label = "opt"..tostring(i) - if fields[button_label] then - if player_response.options[i].action_type == "dialogue" then - -- Process dialogue object - npc.dialogue.process_dialogue(player_response.npc, - player_response.options[i].action, - player_name) - elseif player_response.options[i].action_type == "function" then - -- Execute function - get it directly from definition - -- Find NPC relationship phase with player - local phase = - npc.relationships.get_relationship_phase(player_response.npc, player_name) - -- Check if NPC is married and the married NPC dialogue should be shown - if phase == "phase6" and player_response.is_married_dialogue == true then - -- Get the function definitions from the married dialogue - npc.relationships.MARRIED_NPC_DIALOGUE - .responses[player_response.options[i].response_id] - .action(player_response.npc, player) - elseif player_response.is_custom_trade_dialogue == true then - -- Functions for a custom trade should be available from the same dialogue - -- object as they are created on demand - minetest.log("Player response: "..dump(player_response.options[i])) - player_response.options[i].action(player_response.npc, player) - else - -- Get dialogue from registered dialogues - local dialogue = npc.dialogue.registered_dialogues[player_response.options[i].dialogue_id] - local response = get_response_object_by_id_recursive(dialogue, 0, player_response.options[i].response_id) - - -- Execute function - response.action(player_response.npc, player) - - -- Unlock queue, reset action timer and unfreeze NPC. - npc.exec.set_ready_state(player_response.npc) - end - end - return - end - end - end - end - -end) diff --git a/mods/advanced_npc/doc/api.md b/mods/advanced_npc/doc/api.md deleted file mode 100644 index a87b302..0000000 --- a/mods/advanced_npc/doc/api.md +++ /dev/null @@ -1,598 +0,0 @@ -Advanced_NPC API Reference Alpha-2 (DEV) -========================================= -* More information at - -IMPORTANT: This WIP & unfinished file contains the definitions of current advanced_npc functions -(Some documentation is lacking, so please bear in mind that this WIP file is just to enhance it) - - -Summary -------- -* Introduction -* Initialize NPC -* NPC Steps -* Programs -* Schedules -* Occupations -* Locations -* Dialogues -* Definition tables - - -Introduction ------------- -You can consult this document for help on API of behaviors for the NPCs. -The goal is to be able to have NPCs that have the same functionality as normal players. -The NPCs make Sokomine's mg_villages in Minetest alive although they can -be manually spawned outside the village and work as good as new. -Here is some information about the API methods and systems. -* npc.lua also uses methods and functions from the dependency: mobs_redo - -Initialize NPC --------------- -The API works with some variables into Lua Entity that represent a NPC, -then you should initialize the Lua Entity before that it really assume -a controled behavior. - -### Methods -* `npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name)` : Initialize a NPC - -The simplest way to start a mob (of mobs_redo API) is by using the `on_spawn` function -Note: currently this call is unduly repeated (mobs_redo problem), so you should check if npc has already been initialized. - - on_spawn = function(self) - if self.initialized == nil then - npc.initialize(self, self.object:getpos(), true) - self.tamed = false - end - end - -Or after add in the world - - local obj = minetest.add_entity({x=0, y=10, z=0}, "mobs:sheep", {naked = true}) - local luaentity = get_luaentity(obj) - npc.initialize(luaentity, luaentity.object:getpos(), true) - luaentity.tamed = false - - -NPC Steps ---------- -The API works with NPC steps, then `on_step` callback need run the -`npc.on_step(luaentity)`. This function process the NPC actions -and return the freeze state, which is used for stop mobs_redo behavior. - -Example: - - on_step = function(self, dtime) - npc.step(self, dtime) - end - -Mobs of Mobs_Redo API uses `do_custom` function instead of `on_step` callback -and it needs return the freeze state to stop mobs_redo behavior. -Here is a recommended code. - - do_custom = function(self, dtime) - - -- Here is my "do_custom" code - - -- Process the NPC action and return freeze state - return npc.step(self, dtime) - end - - -Execution API -------------- -The API follows a simple OS-based model where tasks performed by NPCs are encapsulated -in the concepts of `instructions` and `programs`. `Instructions` are "small", "atomic" -actions performed by a NPC (like rotating, standing, etc.) and `programs` are a -"collection" of instructions with logic on what to execute and what not (for example, -walking to a specific position). The NPC executes different programs in order to be -able to perform tasks (e.g. going to sleep on a bed). - -The execution environment of Advanced NPC is based on processes, which are instances -of a program. Processes have an internal instruction queue and execution context for -storing variables; they can be interrupted and their state upon interruption is -stored for later restoration. Processes can also be enqueued into a process queue -which is managed by a process scheduler (which runs roughly each second). The process -scheduler has the responsibility of determining what is the next process to be executed. - -### State processes -A very important concept introduced by the execution environment are `state processes`. -A state process is used to determine the actions of a NPC on a given state. The usual -examples for states are: - - idle - - wandering - - following an object - - attacking - -All of the above `states` are actions that have similar properties: - - Triggered by a particular action, e.g. NPC is punched (attack state) or NPC is sleeping (idle state) - - Executed constantly until a particular goal is reached or more important action takes place - -Therefore, a `state process` is a special type of process that is executed constantly while -the process queue is empty. - -### Operation principle -Note: The information in this subtopic should not be considered for external development, -only for knowledge about the principle of internal operation. - -A process is an instance of a program, with the following attributes: - * A state, which can be any of: - * inactive: process that was just enqueued - * executing: process' Lua function is being executed and not finished yet - * running: process finished execution, and may or may not have instructions on its queue - * paused: interrupted process - * ready: process was interrupted, and then restored, it is ready to run again - * waiting_user_input: happens when on_rightclick interaction occurs - * An instruction_queue, where instructions are enqueued and executed over time. - In terms of OS, think of this as some kind of program counter - * An execution_context, which is the data space of the process. - The execution context is a map of key-value pairs, - supporting read-only values (can't be updated again). - * An interrupted_process, in case that this process interrupted a previous one, - so that it can restored exactly as it was - * An instruction state, where the current instruction being executed is stored as well as its state - (so it can be re-executed in case process is interrupted) - -The process definition is in private `_exec.create_process_entry()` function. This is like this so a process -is always complete and ensured to have all its attributes. The proper way to create a process entry and enqueue -it into the NPC's process queue is by using the `npc.exec.enqueue_program()`. - -The process definition (as Lua table) is the following: - - { - id = process_id, - program_name = program_name, - arguments = arguments, - state = npc.exec.proc.state.INACTIVE, - execution_context = { - data = {}, - instr_interval = 1, - instr_timer = 0 - }, - instruction_queue = {}, - current_instruction = { - entry = {}, - state = npc.exec.proc.instr.state.INACTIVE, - pos = {} - }, - interrupt_options = npc.exec.create_interrupt_options(interrupt_options), - interrupted_process = {}, - is_state_process = is_state_program - } - -The state process have an additional attribute and is_state_process is set to true: - - state_process_id = os.time() - -### Writing and registering programs -Programs are just a Lua function. -Many examples of programs can be found on the code, but the following -are some general tips to keep in mind while writing programs -* If you are doing anything that needs to be done in the future - (example, walking and then checking a node), run the initial instruction and enqueue - the rest. -* The correct way to run a program from a program is to use `advanced_npc:interrupt` -* If you need to evaluate any value in the future (example, after movement), store - it in a process variable (see the `npc.exec.var*` functions) -* You can use instruction recursion to do loops. -* If you are writing any state program, do not make it loop. It will loop for free - (scheduler will execute again and again, so your variables are not lost) -* And finally, if your process is simple, don't enqueue any instruction unless you - want to have a certain pause between instruction execution for visual reasons - (e.g NPC sitting to laying, everythig executed quickly will not look nice) - -### Permanent storage functionality -Permanent storage functionality - create, read, update and delete variables -in the NPC's permnanent storage. -IMPORTANT: These variables are *NOT* deleted. Be careful what you store on it or -the NPC object can grow in size very quickly. -For temporary storage, use `npc.exec.var.*` functions. - -#### Methods -* `npc.data.put(luaentity, key_name, value, readonly)`: This function adds a value to the permanent data storage in the Lua entity - * Readonly defaults to false. - * Returns false if failed due to key_name conflict, or returns true if successful. -* `npc.data.get(luaentity, key_name)`: Returns the value of a given key. If not found returns nil -* `npc.data.set(luaentity, key_name, new_value)`: This function updates a value in the permanent data storage - * Returns false if the value is read-only or if key isn't found. - * Returns true if able to update value. -* `npc.data.remove(luaentity, key_name)`: This function removes a value in the permanent data storage in the Lua entity - * If the key doesn't exist, returns nil, otherwise, returns the value removed. - -### Variable functionality -Variable functionality - create, read, update and delete variables in the -current process. -IMPORTANT: These variables are deleted when the process is finished execution. -For permanent storage, use `npc.data.*` functions. - -#### Methods -* `npc.exec.var.put(luaentity, key_name, value, readonly)`: Put a value to the execution context of the current process - * Readonly defaults to false - * Returns false if failed due to key_name conflict, or returns true if successful -* `npc.exec.var.get(luaentity, key_name)`: Returns the value of a given key - * If not found returns nil -* `npc.exec.var.set(luaentity, key_name, new_value)`: Update a value in the execution context - * Returns false if the value is read-only or if key isn't found - * Returns true if able to update value -* `npc.exec.var.remove(luaentity, key_name)`: Remove a variable from the execution context - * If the key doesn't exist, returns nil, otherwise, returns the value removed - -### Methods -* `npc.programs.register(program_name, func)`: Register a program -* `npc.programs.is_registered(program_name)`: Check if a program exists -* `npc.programs.execute(luaentity, program_name, {program arguments})`: Execute a program for a NPC -* `npc.programs.instr.register(name, func)`: Register a instruction -* `npc.programs.instr.execute(self, name, args)`: Execute a instruction for a NPC -* `npc.exec.enqueue_program(luaentity, program_name, {program arguments}, interrupt_options, is_state_program)`: Add program to schedule queue -* `npc.exec.proc.enqueue(luaentity, instruction_name, {instruction arguments})`: Add instruction to process queue -* `npc.exec.var.put(luaentity, key_name, value, readonly)`: Put a value to the execution context of the current process -* `npc.exec.var.get(luaentity, key_name)`: Returns the value of a given key_name -* `npc.exec.var.set(luaentity, key_name, new_value)`: Update a value in the execution context -* `npc.exec.var.remove(luaentity, key_name)`: Remove a variable from the execution context -* `npc.data.put(luaentity, key_name, value, readonly)`: This function adds a value to the permanent data storage in the Lua entity -* `npc.data.get(luaentity, key_name)`: Returns the value of a given key -* `npc.data.set(luaentity, key_name, new_value)`: This function updates a value in the permanent data storage in the Lua entity -* `npc.data.remove(luaentity, key_name)`: This function removes a value from the permanent data storage in the Lua entity - -Example 1 - - npc.programs.execute(self, "advanced_npc:walk_to_pos", { - end_pos = {x=0,y=0,z=0}, - walkable = {} - }) - -See more about different programs and his arguments in [programs.md](programs.md) documentation. - -Example 2 - - -- Syntacic sugar to make a process wait for a specific interval - npc.programs.instr.register("advanced_npc:wait", function(self, args) - local wait_time = args.time - npc.programs.instr.execute(self, "advanced_npc:set_instruction_interval", {interval = wait_time - 1}) - npc.exec.proc.enqueue(self, "advanced_npc:set_instruction_interval", {interval = 1}) - end) - -See more about different instructions and his arguments in [instructions.md](instructions.md) documentation. - -### Monitoring API -To complete the OS/microprocessor analogy, the Execution API has a sub-API for registering -timers and callbacks of certain events. This API is called "monitor" API because its main -purpose is to be able to keep track of actions that the NPC performs and act according to this -data. The key concept behind the Monitoring API is to be able to introduce some concepts of -artificial intelligence into the Advanced NPC programs. - -#### Timers -Timers can be registered (globally on the `npc.*` namespace) and then added to a NPC for -execution. To register a timer, use: -`npc.monitor.timer.register(name, interval, callback)` -where: - - `name` is a unique name for the timer. Recommended naming convention to use: `::` - - `interval`: the default interval, this can be overriden - - `callback`: a Lua function that is called with `self` (the NPC Lua entity) and a Lua table `args` for arguments - -To run a timer, a new instance is created for the particular NPC that will use the timer -and then it is executed internally. The following function is used to start a timer: -`npc.monitor.timer.start(self, name, interval, args)` -where: - - `name` is the unique name of the timer - - `interval` optional, interval for the timer (if nil, uses the default interval) - - `args` a Lua table of arguments for the timer callback - -To stop a timer, simply use: -`npc.monitor.timer.stop(self, name)` - -##### A word of caution with timers: -While timers can be very useful, they can also be very disruptive, specially if they are -changing state process. Therefore, every timer `callback` function *should* have a condition -check at the very beginning before anything else runs. This way, if the condition for the timer -is no longer valid, it stops and doesn't interferes with other processes running. - -#### Callbacks -Callbacks are functions executed whenever another action is executed. All callbacks -execute *after* the actual action. Currently, there are three types of callbacks supported: - - Program callback: executed whenever a program is executed - - Instruction callback: executed whenever a instruction is executed - - Interaction callback: executed whenever a interaction occurred, which are: - - on punch, - - on right-click - - on schedule - -Callbacks are categorized in terms of `type` (mentioned above) and `subtype`. For programs and -instruction callbacks, the `subtype` is the program or instruction name. -For interaction callbacks, the subtypes are predetermined (as shown above). - -To register a callback, use: -`npc.monitor.callback.register(name, type, subtype, callback)` -where: - - `name` is a unique name for the callback - - `type` is one of the three callback types (defined in `npc.monitor.callback.type`), - - `subtype` is an arbitrary string that denotes the program or instruction name for `program` and `instruction` callbacks respectively, or one of the three subtypes (defined in `npc.monitor.callback.subtype`) as mentioned above for `interaction` callbacks - - `callback` is the function to be executed. The only argument of this function is `self` (the NPC Lua entity) - -To execute a callback, use: -`npc.monitor.callback.enqueue(self, type, subtype, name)` - -Or to enqueue all callbacks for a specific `type` and `subtype`, do: -`npc.monitor.callback.enqueue_all(self, type, subtype)` - - -Schedules ---------- -The interesting part of Advanced NPC is its ability to simulate realistic -behavior in NPCs. Realistic behavior is defined simply as being able to -perform tasks/programs at a certain time of the day, like usually people do. -This allow the NPC to go to bed, sleep, get up from it, sit in benches, etc. -All of this is simulated through a structured code using programs for action -and tasks. - -The implementation resembles a rough OS process scheduling algorithm where -only one process is allowed at a time. The processes or tasks are held in -a queue, where they are executed one at a time in queue fashion. -Interruptions are allowed, and the interrupted action is re-started once -the interruption is finished. - -### Schedule time -Only integer value 0 until 23 -* 0: 0/24000 - 999 -* 1: 1000 - 1999 -* 2: 2000 - 2999 -* ... -* 22: 22000 - 22999 -* 23: 23000 - 23999 - -### Schedule Type -* "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 day of the week it represents as follows: - Note: Currently only one schedule is supported, for day 0 - 1: Monday - 2: Tuesday - 3: Wednesday - 4: Thursday - 5: Friday - 6: Saturday - 7: Sunday -* "date_based" : The date parameter should be a string of the format "MM:DD". - If it already exists, function retuns nil - -### Methods -* `npc.schedule.create(luaentity, schedule_type, day)` : Create a schedule for a NPC -* `npc.schedule.delete(luaentity, schedule_type, date)` : Delete a schedule for a NPC -* `npc.schedule.entry.put(luaentity, schedule_type, date, time, check, commands)` : Add a schedule entry for a time -* `npc.schedule.entry.get(luaentity, schedule_type, date, time)` : Get a schedule entry -* `npc.schedule.entry.set(luaentity, schedule_type, date, time, check, commands)` : Update a schedule entry - -### Examples - - -- Schedule entry for 7 in the morning - npc.schedule.entry.put(self, "generic", 0, 7, nil, { - -- Get out of bed - [1] = { - program_name = "schedules:default:wake_up", - arguments = {}, - interrupt_options = {} - }, - -- Walk to home inside - [2] = { - program_name = "advanced_npc:walk_to_pos", - arguments = { - end_pos = npc.locations.PLACE_TYPE.OTHER.HOME_INSIDE, - walkable = {} - }, - interrupt_options = {}, - }, - }) - - -Occupations ------------ -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. - -### Methods -* `npc.occupations.register_occupation(occupation_name, {occupation definition})` : Register an occupation -* `npc.occupations.initialize_occupation_values(luaentity, occupation_name)` : Initialize an occupation for a NPC - - -Locations ----------- -Locations define which NPCs can access which places and are separated into different types. - -### Locations types -Current location types -* `bed_primary` : the bed of a NPC -* `sit_primary` -* `sit_shared` -* `furnace_primary` -* `furnace_shared` -* `storage_primary` -* `storage_shared` -* `home_entrance_door` -* `schedule_target_pos` : used in the schedule actions -* `calculated_target_pos` -* `workplace_primary` -* `workplace_tool` -* `home_plotmarker` -* `home_inside` -* `home_outside` - -### Methods -* `npc.locations.add_owned(luaentity, place_name, place_type, pos, access_pos)` : Add owned place. - `luaentity` npc owner. - `place_name` a specific place name. - `place_type` place typing. - `pos` is a position of a node to be owned. - `access_pos` is the coordinate where npc must be to initiate the access. - Location is added for the NPC. -* `npc.locations.add_shared(luaentity, place_name, place_type, pos, access_node)` : Add shared place - - -Dialogues ---------- -Dialogs can be registered to be spoken by NPCs. - -### Tags -The flags or marks of the dialogue text. Tags can be used for .... - -* "unisex" : Both male and female NPCs can say the defined text. -* "phase1" : NPCs in phase 1 of a relationship can say the defined text. - -### Methods -* `set_response_ids_recursively()` : A local function that assigns unique - key IDs to dialogue responses. -* `npc.dialogue.register_dialogue({dialogue definition})` : Defines and - registers dialogues. -* `npc.dialogue.search_dialogue_by_tags({search_tags})` : A method returning - a Lua table of dialogues if called. - - -Definition tables ------------------ - -### Program definition (Programs) - - { - program_name = "modname:program1", -- Programs name - - arguments = {program arguments}, -- Lua table of arguments for the program - - is_state_program = true, --[[ - ^ [OPTIONAL] - ^ If this is true, then this program will be - repeated while there is no next program]] - - interrupt_options = {} --[[ - ^ [OPTIONAL] - ^ Is a Lua table that defines what kind of interaction can interrupt the process - when it is running. The "interruption" is not a literal process pause. - It means that the defined interactions can happe while the process is running. - In that fashion, for example, if the NPC is sleeping, talking (right click interaction) - to the NPC can be disabled. - ^ The three supported interaction types are defined below. - They are all optional and accept values of true or false - * allow_punch: if enabled, the entity's on_punch() function is executed. - * allow_rightclick: if enabled, when the rightclick of the entitiy is called, - the process is put on waiting_user_input state and entity's on_rightclick() executed - * allow_schedule: enables or disables schedule entries. If disabled, schedule will not run.]] - - depends = {}, --[[ - ^ [OPTIONAL] - ^ is an array of numbers, where each number represents an index in the array - of schedule entries for that time. - ^ Is a schedule entry concept. For a certain time, an array of programs - is enqueued when the scheduled time arrives. The programs are enqueued - in the order they are given in the array. If a program have a chance argument, - it means that it could or couldn't happen. Therefore, some programs may or - may not run, hence the depends. - - chance = , --[[ - ^ [OPTIONAL] - ^ chance x in 100 of this program be executed - } - -### Occupation definition (`register_occupation`) - - { - 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. ]] - - walkable_nodes = {}, -- Walkable nodes - - 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 Lua table of schedules where the index is a schedule time: - { - [] = { - [1] = {program definition}, - [2] = {program definition}, - ... - }, - [] = { - [1] = {program definition}, - [2] = {program definition}, - ... - }, - ... - } - } - -### Dialogue definition (`register_dialogue`) - - { - text = "Hello.", --[[ - ^ The dialogue text itself. - ^ It must be included in the method.]] - - tags = {"tag1", "tag2"} --[[ - ^ The flags or marks of the dialogue text. - ^ The object can be excluded. ]] - } - -Examples: - -Syntax example 1: - - npc.dialogue.register_dialogue({ - text = "Hello.", -- "Hello." will be said by the NPC upon rightclick and displayed in the messages section. - tags = {"unisex", "phase1"} -- The flags that define the conditions of who and what can say the text. - }) - -Syntax example 2: - - npc.dialogue.register_dialogue({ - text = "Hello again." - -- The tags object is excluded, meaning that any NPC can say "Hello again." upon rightclick under no condition. - }) diff --git a/mods/advanced_npc/doc/instructions.md b/mods/advanced_npc/doc/instructions.md deleted file mode 100644 index ebc6f4c..0000000 --- a/mods/advanced_npc/doc/instructions.md +++ /dev/null @@ -1,19 +0,0 @@ -Instructions for Programs -Advanced_NPC Alpha-2 (DEV) -========================== - -IMPORTANT: In this documentation is only the explanation of the particular -operation of each predefined instructions. Read reference documentation -for details about API operation at [api.md](api.md). - -### Default Instructions -These instructions are already registered in the API. -This section describes these instructions and their respective arguments. - -#### `WAIT` (advanced_npc:wait) -This instruction causes the object to wait stopped for a time. -In other words, syntacic sugar to make a process wait for a specific interval. - - { - time = , -- Time number in seconds - } diff --git a/mods/advanced_npc/doc/new_api.md b/mods/advanced_npc/doc/new_api.md deleted file mode 100644 index a42fbe7..0000000 --- a/mods/advanced_npc/doc/new_api.md +++ /dev/null @@ -1,90 +0,0 @@ -Advanced NPC 1.0 proposal -------------------------- - -While Advanced NPC provides functionality and a level of intelligence that no other mob mod can, it is still limited in some features and to its ultimate purpose of creating functional towns and/or simulated communities. The following are the areas that has been identified as lacking: - - - Idle/wandering - - When NPCs aren't executing actions, their movement is very dumb. They wander aimlessly, constantly and usually bump into obstacles and keep walking nevertheless. They get stuck at places they shouldn't. - - Relationships - - Relationships are very hardcoded, and there's no flexibility on them. - - Unable to add more functionality - - All actions are hardcoded. While the essentials are in, making a NPC operate another node that is not a furnace/chest/door is almost impossible. If a mod adds a node and wants NPC to be able to operate it, it is certainly very hard. - - More randomness in schedules - - While schedules are all about making NPCs do actions at certain times, it is not flexible enough to make it look more realistic. One morning a NPC can get up and make breakfast or not, put some music on a music player or not, go outside their home and wander around, etc. - - Unable to react to certain triggers - - When NPCs are punched, `mobs_redo` takes over and controls the NPC. Also, NPCs are unable to scan an area for certain things and perform actions continually based on it. - -The above are all playability issues and deficiencies. Some technical issues has to be addressed as well regarding the API. Given all these, the following is a proposal to move the mod towards the correct direction. - - -##Proposed changes: - - Unify the actions/tasks/schedule property change/schedule query API into a `commands` API - - Add new commands to bring the NPC interaction level closer to that of a player - - Rename `flags` to `properties` - - Allow registering scripts, or collections of commands for external mods to provide extra functionality - - -Unified Commands API --------------------- - -The goal of this API is to provide consistency and extensibility to the actions a NPC can perform. First of all, rename actions/tasks/property change/query to `commands`. Each command will have the following properties that determines how it is to be executed and what it does: - - - Type: specifies the type of command. The following are valid types: - - `instruction`: Used for fundamental, atomic operations. This type maps directly to what are called now `actions`, which are for example, walk one step, dig, place, etc. - - `control`: Used for specific commands that are flow control statements. Example: If-else, for loops. The conditional statements is a Lua boolean expression. - - `script`: Used for collections of commands, executed on a sequential structure. This type maps directly to `tasks`. - - Execution: specifies how the command is to be executed. The following are valid valuesf for this parameter: - - `immediate`: Will execute this command immediately, without any enqueing. Very little commands should be able to do this. The `control` commands should be executed immediately as they need to enqueue certain commands depending on their conditions. - - `default`: Command will be enqueued and executed on the global command timer call. - - Interruptable: specifies whether the global command timer and/or the scheduler can interrupt the command. Boolean value, can be set to false or true. - - _Important_: Non-interruptable commands should be able to finish by themselves. The API will execute the default command once a non-interruptable command is done and if it doesn't executes another command. - - Parameters: a Lua table with all the parameters that the command requires. Depending on the type, some parameters are required. Below is a list of required parameters per type: - - `instruction`: Requires just the parameters required by the instruction to execute. - - `control`: Requires different parameters depending on the type of control. - - Required: - - `condition`: The condition to be evaluated. This is a Lua boolean expression. - - `match_commands`: A Lua array with the commands to be executed if condition evaluates to `true`. - - Dependent on type: - - `operation`: Only required in `for-loop` command. Operation to execute on the loop variable (e.g. increase/decrease) - - `repetition`: Optional for `for-loop` command. Can't be used together with `max` and `min`. - - `max`: Optional for `for-loop` command. Can't be used together with `repetition`. Requires `min`. Randomizes a loop execution and sets the upper bound of how many times the loop will execute. - - `min`: Optional for `for-loop` command. Can't be used together with `repetition`. Requires `max`. Randomizes a loop execution and sets the lower bound of how many times the loop will execute. - - `else_commands`: Only required in `if-else` command. A Lua array with the commands to be executed if condition evaluates to `false`. - - `script`: A Lua array of commands to execute, in order - -The following `instruction` commands will be added to the default set: - - `do_punch`: Executes the `on_punch` function of a node, object or player - - `do_rightclick`: Executes the `on_rightclick` function of a node, object or player - - `set_property`: Sets the value of a variable in the `self.properties` object. If the variable doesn't exists, it is created. This command is executed immediately and is not enqueued. - - Parameters: - - `key`: The property key-name. This is a variable in the `self.properties` object - - `value`: The property value. - - `get_property`: Returns the value of a given property. This command is executed immediately and is not enqueued. - - Parameters: - - `key`: The property key-name. - - `set_internal_property`: Sets the value of a limited set of internal properties related to the NPC trading and personality variables. - - `get_internal_property`: Gets the value of a limited set of internal properties related to the NPC trading and personality variables. - - `add_item_to_npc`: Adds an item to the NPC inventory, without any specific source. - - `remove_item_from_npc`: Removes a specific item from the NPC inventory. - - `query`: Executes a query for nodes or objects. Returns a Lua table with none, single or many positions. - -The following `control` commands will be added to the default set: - - `if-else`: An if-else control statement that will execute immediately. It will evaluate the given `condition` parameter and execute commands depending on the evaluation of the `condition`. - - Parameters: - - `condition`: A Lua boolean expression to be evaluated. - - `true-commands`: A Lua array of commands to be executed if `condition` evaluates to `true`. - - `else-commands`: A Lua array of commands to be executed if `condition` evaluates to `false`. - - `loop`: A flexible loop command. Supports for-loop and while-loops. The amount of loops done will be available in `npc.commands.current_loop_count`. Executes immediately, it is not enqueued. - - Parameters: - - -##Extensibility -Once the above commands has been added, it is possible to safely build scripts which don't touch directly many of the internal NPC mechanisms. An API will be provided for external mods to register scripts that let NPCs perform actions related to those mods, e.g. operating a node provided by the mod. The API for this will be: - -`npc.commands.register_script(name, script)` - -All registered scripts have the following properties: - - They are interruptable by the command queue/scheduler - - They are not immediately executed - -The `script` parameter is a Lua array of commands that will be executed when the script is executed. \ No newline at end of file diff --git a/mods/advanced_npc/doc/programs.md b/mods/advanced_npc/doc/programs.md deleted file mode 100644 index 036fb1f..0000000 --- a/mods/advanced_npc/doc/programs.md +++ /dev/null @@ -1,148 +0,0 @@ -Programs for Actions and Tasks -Advanced_NPC Alpha-2 (DEV) -========================== - -IMPORTANT: In this documentation is only the explanation of the particular -operation of each predefined action and task programs. Read reference documentation -for details about API operation at [api.md](api.md). - -### Default Programs -These programs are already registered in the API. -This section describes these programs and their respective arguments. - -#### `IDLE` (advanced_npc:idle) -This program meant to be run when NPC are doing nothing and standing idle. -Idle program doesn't loops, it is meant to be executed as a state program -(which is scheduled continously as long as the process queue is empty) -It has two main features (as-of the moment, more planned): - - { - acknowledge_nearby_objs = true, --[[ - ^ Acknowledge nearby objects by looking at them, - with configurable object search interval and radius]] - wander_chance = 0, --[[ - ^ Trigger wandering with configurable chance (1-100 chance of wander/0 for never) - and radius (how many nodes to wander from starting point)]] - } - - -#### `USE BED` (advanced_npc:use_bed) -Sequence of actions that allows the NPC to use a bed. - - { - pos = {x=0,y=0,z=0}, --[[ - ^ Position of bed to be used. - ^ Can be a coordinate x,y,z. - ^ Can be a place name of the NPC place map. - Example: "bed_primary" ]] - - action = action, --[[ - ^ Whether to get up or lay on bed - ^ Defined in npc.commands.const.beds.action - ^ Available options: - * npc.commands.const.beds.LAY : lay - * npc.commands.const.beds.GET_UP : get up - } - -#### `WALK TO POS` (advanced_npc:walk_to_pos) -NPC will walk to the given position. This task uses the pathfinder to calculate the nodes -in the path that the NPC will walk through, then enqueues walk_step actions, combined with -correct directional rotations and opening/closing of doors on the path. - - { - end_pos = {x=0,y=0,z=0}, --[[ - ^ Destination position to reach. - ^ Can be a coordinate x,y,z. - ^ Can be a place name of the NPC place map. - The position must be walkable for the npc to stop in, - or in the access position of the place. - Example: "home_inside" ]] - - walkable = {}, --[[ - ^ An array of node names to consider as walkable nodes - for finding the path to the destination. ]] - - use_access_node = true, --[[ - ^ Boolean, if true, when using places, it will find path - to the "accessible" node (empty or walkable node around - the target node) instead of to the target node. - ^ Default is true. ]] - - enforce_move = true, --[[ - ^ Boolean, if true and no path is found from the NPC's - position to the end_pos, the NPC will be teleported - to the destination (or, if use_access_node == true it will - teleport to the access position) - ^ Default is true. ]] - } - -#### `INTERNAL PROPERTY CHANGE` (advanced_npc:internal_property_change) -Changes the value of an internal property of a NPC Lua entity. - - { - property = , --[[ - ^ Property type - ^ Property types: - "flag" for flags save in `flags` Lua table for in Lua entity]] - - args = { - - action = , --[[ - ^ Type change action - ^ Change types: - "set" for set the value - "reset" for reset the value 0 for number, false for boolean and "" for strings]] - - flag_name = , -- Flag name - flag_value = , -- New flag value - } - } - -#### `NODE QUERY` (advanced_npc:node_query) -Check and run a program with nodes found near. - - { - range = 2, -- Range of checked area in blocks. - - count = 20, -- How many checks will be performed. - - random_execution_times = true, --[[ - ^ Randomizes the number of checks that will be performed. - ^ min_count and max_count is required ]] - - min_count = 20, -- minimum of checks - max_count = 25, -- maximum of checks - - nodes = {"itemstring1", "itemstring2"}, --[[ - ^ Nodes to be found for the actions. - ^ When a node is found, it is add in the npc place map - with the place name "schedule_target_pos" - - prefer_last_acted_upon_node = true, -- If prefer to act on nodes already acted upon - - walkable_nodes = {"itemstring1", "itemstring2"}, -- Walkable nodes - - on_found_executables = { --[[ - ^ Table where index is a itemstring of the node to be found, - and value is an array of programs to be performed - when found the node. ]] - - ["itemstring1"] = { - [1] = , - [2] = , - [3] = - }, - ["itemstring2"] = { - [1] = , - [2] = - } - }, - - on_not_found_executables = { --[[ - ^ An array of programs to be performed when not found any node. - - [1] = , - [2] = - }, - - } diff --git a/mods/advanced_npc/executable/actions.lua b/mods/advanced_npc/executable/actions.lua deleted file mode 100644 index cbb18aa..0000000 --- a/mods/advanced_npc/executable/actions.lua +++ /dev/null @@ -1,1644 +0,0 @@ --- Commands API code for Advanced NPC by Zorman2000 ---------------------------------------------------------------------------------------- --- Commands API functionality ---------------------------------------------------------------------------------------- --- --- Description: ------------------------------------------------------------------------------------------- --- The Commands API is a Bash Script-like execution environment for Minetest entities. --- There are the following fundamental constructs: --- - commands: execute a specific action (e.g. dig a node, search for nodes, etc.) --- - variables: stores information temporarily (e.g. results of node search) --- - control statements: if/else and loops --- --- All of these is done using the Lua programming language, and a few custom --- expressions where Lua can't help. --- --- The fundamental concept is to use the three constructs together in the form of --- a script to allow NPCs (and any Minetest entity) to perform complex actions, --- like walking from one place to the other, operating furnaces, etc. In this sense, --- this "commands" API can be considered a domain-specific language (DSL), that is --- defined using Lua language structures. --- --- Basic definitions: ------------------------------------------------------------------------------------------- --- A `variable` is any value that can be accessed using a specific key, or name. --- In the context of the commands API, there is an `execution` context where --- variables can be stored into, read and deleted from. The execution context --- is nothing but a map of key-value pairs, with the key being the variable name. --- Some rules regarding variables: --- - A variable can be read-write or read-only. Read-only variables cannot be --- updated, but can be deleted. --- - A variable cannot be overwritten by another variable of the same name. --- - The scope of variables is global *within* a script. The execution context --- is cleared after a script finishes executing. For more info about scripts, --- see below. --- - The Lua entity variables, referring to any `self.*` value isn't available --- as a variable. This is to keep the NPC integrity from a security perspective. --- However, as some values are very useful and often needed (such as --- `self.object:getpos()`), some variables are exposed. These are referred to --- as *internal* variables. They are read-only. --- --- A 'command' is a Lua function, with only two parameters: `self`, the Lua entity, --- and `args`, a Lua table containing all arguments required for the command. The --- control statements (if/else, loop) and variable set/read are defined as commands --- as well. The arguments are not strictly controlled, with the following exceptions: --- - A `variable expression string` is special string that allows a variable --- to be passed as an argument to a command. The reason why a function can't --- be used for this is because the execution context, where variables are --- stored, lives in the entity itself (the `self` object), to which there's no --- access when a script is defined as a Lua array. --- The special string has a specific format: ":" where the --- accepted values for `` are: --- - `var`, referring to a variable from the execution context, and, --- - `ivar`, referring to an internal variable (an exposed self.* variable) --- - A `function expression table` is a Lua table that contains a executable --- Lua function and the arguments to be executed. The function is executed --- at the proper moment when passed as an argument to a command, instead of --- executing immediately while defining a script. --- The function expression table has the following format: --- { --- func: , --- args: --- } --- - A `boolean expression table` is a Lua table that is reconstructed into a --- Lua boolean expression. The reason for this to exist is similar to the --- above explanation, and is that, at the moment a script is defined as a --- Lua array, any function or variable passed as a boolean expression will --- evaluate, making the value effectively a constant. That would render --- loops and if/else statements useless. --- The boolean expression table has the following format: --- { --- left_side: , --- operator: , --- right_side: , --- } --- `operator` and `right_side` are optional: a single function in `left_side` is --- enough as long as it evaluates to a `boolean` value. The `operator` argument --- accepts the following values: --- - `equals` --- - `not_equals` --- - `greater_than` --- - `greater_than_equals` --- - `less_than` --- - `less_than_equals` --- `right_side` is required if `operator` is defined. --- --- A `script` is an ordered sequence of commands to be executed, and is defined --- using a Lua array, where each element is a Lua function corresponding to a --- command. Scripts are intended to be implemented by users of the API, and as --- such it is possible to register a script for other mods to use. For example, --- a script can be used by a mod that creates a music player node so that NPCs --- can also be able to use it. --- Scripts can also be executed at certain times during a Minetest day thanks --- to the schedules functionality. --- --- Execution: ------------------------------------------------------------------------------------------- --- The execution of commands is performed on a timer basis, per NPC, with a default --- interval value of one second. This interval can be changed by a command itself, --- however it is the recommended interval is one second to avoid lag caused by many NPCs --- executing commands. --- Commands has to be enqueued in order to execute. Enqueuing commands directly isn't --- recommended, and instead it should be done through a script. Nonetheless, the API --- for enqueuing commands and scripts is the following: --- - npc.enqueue_command(command_name, args) --- - npc.enqueue_script(script_name, args) --- --- The control statement commands (if/else, loops) and variable set/read commands --- will execute the next command in queue immediately after finishing instead of --- waiting for the timer interval. --- --- There is an `execution context` which contains all the variables that are defined --- using the variable set commands. Also, it contains values specific to the loops, --- like the number of times it has executed. The execution context lives in the NPC --- `self` object, and therefore, has to be carefully used, or otherwise it can create --- huge memory usage. In order to avoid this, variables can be deleted from the execution --- context using a specific command (`npc.commands.del_var(key)`). Also, as basic --- memory management routine, the `execution context` is cleared after the end of --- executing a script. --- To keep global variables, use the npc.command.get/set_flag() API which is not --- deleted after execution. ------------------------------------------------------------------------------------------- - -npc.commands = {} ---local registered_commands = {} - -npc.commands.default_interval = 1 - -npc.commands.dir_data = { - -- North - [0] = { - yaw = 0, - vel = {x=0, y=0, z=1} - }, - -- East - [1] = { - yaw = (3 * math.pi) / 2, - vel = {x=1, y=0, z=0} - }, - -- South - [2] = { - yaw = math.pi, - vel = {x=0, y=0, z=-1} - }, - -- West - [3] = { - yaw = math.pi / 2, - vel = {x=-1, y=0, z=0} - }, - -- North east - [4] = { - yaw = (7 * math.pi) / 4, - vel = {x=1, y=0, z=1} - }, - -- North west - [5] = { - yaw = math.pi / 4, - vel = {x=-1, y=0, z=1} - }, - -- South east - [6] = { - yaw = (5 * math.pi) / 4, - vel = {x=1, y=0, z=-1} - }, - -- South west - [7] = { - yaw = (3 * math.pi) / 4, - vel = {x=-1, y=0, z=-1} - } -} - --- Describes commands with doors or openable nodes -npc.commands.const = { - doors = { - command = { - OPEN = 1, - CLOSE = 2 - }, - state = { - OPEN = 1, - CLOSED = 2 - } - }, - beds = { - LAY = 1, - GET_UP = 2 - }, - sittable = { - SIT = 1, - GET_UP = 2 - } -} - -npc.commands.cmd = { - SET_INTERVAL = 0, - FREEZE = 1, - ROTATE = 2, - WALK_STEP = 3, - STAND = 4, - SIT = 5, - LAY = 6, - PUT_ITEM = 7, - TAKE_ITEM = 8, - CHECK_ITEM = 9, - USE_OPENABLE = 10, - USE_FURNACE = 11, - USE_BED = 12, - USE_SITTABLE = 13, - WALK_TO_POS = 14, - DIG = 15, - PLACE = 16 -} - ---npc.commands.one_nps_speed = 0.98 ---npc.commands.one_half_nps_speed = 1.40 ---npc.commands.two_nps_speed = 1.90' -npc.commands.one_nps_speed = 1 -npc.commands.one_half_nps_speed = 1.5 -npc.commands.two_nps_speed = 2 - -npc.commands.take_from_inventory = "take_from_inventory" -npc.commands.take_from_inventory_forced = "take_from_inventory_forced" -npc.commands.force_place = "force_place" - --------------- --- Executor -- --------------- --- Function references aren't reliable in Minetest entities. Objects get serialized --- and deserialized, as well as loaded and unloaded frequently which causes many --- function references to be lost and then crashes occurs due to nil variables. --- Using constants to refer to each method of this API and a function that --- understands those constants and executes the proper function is the way to avoid --- this frequent crashes. - -npc.commands.registered_scripts = {} - -function npc.commands.register_script(name, func) - npc.commands.registered_scripts[name] = { - executable = func, - functions = {} - } - npc.log("INFO", "Successfully registered script with name "..dump(name)) - --npc.log("INFO", dump(npc.registered_scripts)) -end - -function npc.commands.register_script_function(name, func_name, func) - if npc.commands.registered_scripts[name] then - npc.commands.registered_scripts[name].functions[func_name] = func - else - npc.log("ERROR", "Unable to register script function '"..dump(func_name).."' for script '"..dump(name).."'") - end - npc.log("INFO", "Script: "..dump(npc.commands.registered_scripts)) -end - -function npc.commands.execute(self, command, args) - if command == npc.commands.cmd.SET_INTERVAL then - -- - return npc.commands.set_interval(self, args) - elseif command == npc.commands.cmd.FREEZE then - -- - return npc.commands.freeze(self, args) - elseif command == npc.commands.cmd.ROTATE then - -- - return npc.commands.rotate(self, args) - elseif command == npc.commands.cmd.WALK_STEP then - -- - return npc.commands.walk_step(self, args) - elseif command == npc.commands.cmd.STAND then - -- - return npc.commands.stand(self, args) - elseif command == npc.commands.cmd.SIT then - -- - return npc.commands.sit(self, args) - elseif command == npc.commands.cmd.LAY then - -- - return npc.commands.lay(self, args) - elseif command == npc.commands.cmd.PUT_ITEM then - -- - return npc.commands.put_item_on_external_inventory(self, args) - elseif command == npc.commands.cmd.TAKE_ITEM then - -- - return npc.commands.take_item_from_external_inventory(self, args) - elseif command == npc.commands.cmd.CHECK_ITEM then - -- - return npc.commands.check_external_inventory_contains_item(self, args) - elseif command == npc.commands.cmd.USE_OPENABLE then - -- - return npc.commands.use_openable(self, args) - elseif command == npc.commands.cmd.USE_FURNACE then - -- - return npc.commands.use_furnace(self, args) - elseif command == npc.commands.cmd.USE_BED then - -- - return npc.commands.use_bed(self, args) - elseif command == npc.commands.cmd.USE_SITTABLE then - -- Call use sittable task - return npc.commands.use_sittable(self, args) - elseif command == npc.commands.cmd.WALK_TO_POS then - -- Call walk to position task - --minetest.log("Self: "..dump(self)..", Command: "..dump(command)..", args: "..dump(args)) - return npc.commands.execute_script(self, "advanced_npc:walk_to_pos", args) - elseif command == npc.commands.cmd.DIG then - -- Call dig node command - return npc.commands.dig(self, args) - elseif command == npc.commands.cmd.PLACE then - -- Call place node command - return npc.commands.place(self, args) - end -end - -function npc.commands.execute_script(self, name, args, execution_options) - local script = npc.commands.registered_scripts[name] - npc.log("Scriptf: "..dump(script)) - if script then - npc.log("INFO", "Executing script with name "..dump(name).." and args: "..dump(args)) - if execution_options then - npc.set_execution_params(self, execution_options) - end - return script.executable(self, args) - end - npc.log("WARNING", "No script found for name "..dump(name)) -end - -function npc.commands.execute_script_function(self, name, func_name, args) - local script = npc.commands.registered_scripts[name] - npc.log("Scriptf: "..dump(script)) - if script and script.functions[func_name] then - npc.log("INFO", "Executing script '"..dump(name).."' function '"..dump(func_name).."' with args: "..dump(args)) - return script.functions[func_name](self, args) - else - npc.log("WARNING", "Failed to execute script function '"..dump(func_name).."' in script '" - ..dump(name).."'. Either script or function doesn't exsits.") - end -end - --- TODO: Thanks to executor function, all the functions for Commands and Tasks --- should be made into private API - - ---------------------------------------------------------------------------------------- --- Commands ---------------------------------------------------------------------------------------- ------------------------ --- Control commands -- ------------------------ --- The following command alters the timer interval for executing commands, therefore --- making waits and pauses possible, or increase timing when some commands want to --- be performed faster, like walking. -function npc.commands.set_interval(self, args) - local self_actions = args.self_actions - local new_interval = args.interval - local freeze_mobs_api = args.freeze - - self.actions.action_interval = new_interval - return not freeze_mobs_api -end - --- The following command is for allowing the rest of mobs redo API to be executed --- after this command ends. This is useful for times when no command is needed --- and the NPC is allowed to roam freely. -function npc.commands.freeze(self, args) - local freeze_mobs_api = args.freeze - local disable_rightclick = args.disable_rightclick - if disable_rightclick ~= nil then - npc.log("INFO", "Enabling interactions for NPC "..self.npc_name..": "..dump(not(disable_rightclick))) - self.enable_rightclick_interaction = not(disable_rightclick) - end - - return not(freeze_mobs_api) -end - --------------------------- --- Interaction commands -- --------------------------- --- This command digs the node at the given position --- If 'add_to_inventory' is true, it will put the digged node in the NPC --- inventory. --- Returns true if dig is successful, otherwise false -function npc.commands.dig(self, args) - local pos = args.pos - local add_to_inventory = args.add_to_inventory - local bypass_protection = args.bypass_protection - local play_sound = args.play_sound or true - local node = minetest.get_node_or_nil(pos) - if node then - -- Set mine animation - self.object:set_animation({ - x = npc.ANIMATION_MINE_START, - y = npc.ANIMATION_MINE_END}, - self.animation.speed_normal, 0) - - -- Play dig sound - if play_sound == true then - minetest.sound_play( - minetest.registered_nodes[node.name].sounds.dug, - { - max_hear_distance = 10, - object = self.object - } - ) - end - - -- Check if protection not enforced - if not bypass_protection then - -- Try to dig node - if minetest.dig_node(pos) then - -- Add to inventory the node drops - if add_to_inventory then - -- Get node drop - local drop = minetest.registered_nodes[node.name].drop - local drop_itemname = node.name - if drop and drop.items then - local random_item = drop.items[math.random(1, #drop.items)] - if random_item then - drop_itemname = random_item.items[1] - end - end - -- Add to NPC inventory - npc.add_item_to_inventory(self, drop_itemname, 1) - end - --return true - return - end - else - -- Add to inventory - if add_to_inventory then - -- Get node drop - local drop = minetest.registered_nodes[node.name].drop - local drop_itemname = node.name - if drop and drop.items then - local random_item = drop.items[math.random(1, #drop.items)] - if random_item then - drop_itemname = random_item.items[1] - end - end - -- Add to NPC inventory - npc.add_item_to_inventory(self, drop_itemname, 1) - end - -- Dig node - minetest.set_node(pos, {name="air"}) - end - end - --return false -end - - --- This command places a given node at the given position --- There are three ways to source the node: --- 1. take_from_inventory: takes node from inventory. If not in inventory, --- node isn't placed. --- 2. take_from_inventory_forced: takes node from inventory. If not in --- inventory, node will be placed anyways. --- 3. force_place: places node regardless of inventory - will not touch --- the NPCs inventory -function npc.commands.place(self, args) - local pos = args.pos - local node = args.node - local source = args.source - local bypass_protection = args.bypass_protection - local play_sound = args.play_sound or true - local node_at_pos = minetest.get_node_or_nil(pos) - -- Check if position is empty or has a node that can be built to - if node_at_pos and - (node_at_pos.name == "air" or minetest.registered_nodes[node_at_pos.name].buildable_to == true) then - -- Check protection - if (not bypass_protection and not minetest.is_protected(pos, self.npc_name)) - or bypass_protection == true then - -- Take from inventory if necessary - local place_item = false - if source == npc.commands.take_from_inventory then - if npc.take_item_from_inventory(self, node, 1) then - place_item = true - end - elseif source == npc.commands.take_from_inventory_forced then - npc.take_item_from_inventory(self, node, 1) - place_item = true - elseif source == npc.commands.force_place then - place_item = true - end - -- Place node - if place_item == true then - -- Set mine animation - self.object:set_animation({ - x = npc.ANIMATION_MINE_START, - y = npc.ANIMATION_MINE_END}, - self.animation.speed_normal, 0) - -- Place node - minetest.set_node(pos, {name=node}) - -- Play place sound - if play_sound == true then - minetest.sound_play( - minetest.registered_nodes[node].sounds.place, - { - max_hear_distance = 10, - object = self.object - } - ) - end - end - end - end -end - --- This function allows to query for nodes and entities within a radius. --- Parameters: --- - query_type: string, specifies whether to query nodes or entities. --- Default value is "node". Accepted values are: --- - "node" --- - "entity" --- - position: table or string, specifies the starting position --- for query. This should be the center of a square box. If --- given a String, the string should be the place type. --- Two types of tables are accepted: --- - A simple position table, {x=1, y=1, z=1} --- - An improved position table in this format: --- { --- place_category = "", --- place_type = "", --- index = 1, (specific index in the places map) --- use_access_node = false|true, --- } --- - radius: integer, specifies the radius of the square box to search --- around the starting position. --- - result_type: string, specifies how to return results. Accepted --- values are: --- - "first": Get the first result found (default if left blank), --- - "nearest": Get the result nearest to the NPC, --- - "all": Return array of all results -function npc.commands.query(self, args) - -end - - - --- This function allows to move into directions that are walkable. It --- avoids fences and allows to move on plants. --- This will make for nice wanderings, making the NPC move smartly instead --- of just getting stuck at places -local function random_dir_helper(start_pos, speed, dir_start, dir_end) - -- Limit the number of tries - otherwise it could become an infinite loop - for i = 1, 8 do - local dir = math.random(dir_start, dir_end) - local vel = vector.multiply(npc.commands.dir_data[dir].vel, speed) - local pos = vector.add(start_pos, vel) - local node = minetest.get_node(pos) - if node then - if node.name == "air" - -- Any walkable node except fences - or (minetest.registered_nodes[node.name].walkable == true - and minetest.registered_nodes[node.name].groups.fence ~= 1) - -- Farming plants - or minetest.registered_nodes[node.name].groups.plant == 1 then - return dir - end - end - end - -- Return -1 signaling that no good direction could be found - return -1 -end - --- This command is to rotate to mob to a specifc direction. Currently, the code --- contains also for diagonals, but remaining in the orthogonal domain is preferrable. -function npc.commands.rotate(self, args) - local dir = args.dir - local yaw = args.yaw or 0 - local start_pos = args.start_pos - local end_pos = args.end_pos - -- Calculate dir if positions are given - if start_pos and end_pos and not dir then - dir = npc.commands.get_direction(start_pos, end_pos) - end - -- Only yaw was given - if yaw and not dir and not start_pos and not end_pos then - self.object:setyaw(yaw) - return - end - - self.rotate = 0 - if dir == npc.direction.north then - yaw = 0 - elseif dir == npc.direction.north_east then - yaw = (7 * math.pi) / 4 - elseif dir == npc.direction.east then - yaw = (3 * math.pi) / 2 - elseif dir == npc.direction.south_east then - yaw = (5 * math.pi) / 4 - elseif dir == npc.direction.south then - yaw = math.pi - elseif dir == npc.direction.south_west then - yaw = (3 * math.pi) / 4 - elseif dir == npc.direction.west then - yaw = math.pi / 2 - elseif dir == npc.direction.north_west then - yaw = math.pi / 4 - end - self.object:setyaw(yaw) -end - --- This function will make the NPC walk one step on a --- specifc direction. One step means one node. It returns --- true if it can move on that direction, and false if there is an obstacle -function npc.commands.walk_step(self, args) - local dir = args.dir - local yaw = args.yaw or 0 - local step_into_air_only = args.step_into_air_only - local speed = args.speed - local target_pos = args.target_pos - local start_pos = args.start_pos - local vel = {} - - -- Set default node per seconds - if speed == nil then - speed = npc.commands.one_nps_speed - end - - -- Only yaw was given, purely rotate and walk in that dir - if yaw and not dir then - vel = vector.multiply(vector.normalize(minetest.yaw_to_dir(yaw)), speed) - else - -- Check if dir should be random - if dir == "random_all" or dir == "random" then - dir = random_dir_helper(start_pos, speed, 0, 7) - end - if dir == "random_orthogonal" then - dir = random_dir_helper(start_pos, speed, 0, 3) - end - - if dir == npc.direction.north then - vel = {x=0, y=0, z=speed} - elseif dir == npc.direction.north_east then - vel = {x=speed, y=0, z=speed} - elseif dir == npc.direction.east then - vel = {x=speed, y=0, z=0} - elseif dir == npc.direction.south_east then - vel = {x=speed, y=0, z=-speed} - elseif dir == npc.direction.south then - vel = {x=0, y=0, z=-speed} - elseif dir == npc.direction.south_west then - vel = {x=-speed, y=0, z=-speed} - elseif dir == npc.direction.west then - vel = {x=-speed, y=0, z=0} - elseif dir == npc.direction.north_west then - vel = {x=-speed, y=0, z=speed } - else - -- No direction provided or NPC is trapped, don't move NPC - vel = {x=0, y=0, z=0} - end - end - - -- If there is a target position to reach, set it and set walking to true - if target_pos ~= nil then - self.actions.walking.target_pos = target_pos - -- Set is_walking = true - self.actions.walking.is_walking = true - end - - -- Rotate NPC - npc.commands.rotate(self, {dir=dir, yaw=yaw}) - -- Set velocity so that NPC walks - self.object:setvelocity(vel) - -- Set walk animation - self.object:set_animation({ - x = npc.ANIMATION_WALK_START, - y = npc.ANIMATION_WALK_END}, - self.animation.speed_normal, 0) -end - --- This command makes the NPC stand and remain like that -function npc.commands.stand(self, args) - local pos = args.pos - local dir = args.dir - -- Set is_walking = false - self.actions.walking.is_walking = false - -- Stop NPC - self.object:setvelocity({x=0, y=0, z=0}) - -- If position given, set to that position - if pos ~= nil then - self.object:moveto(pos) - end - -- If dir given, set to that dir - if dir ~= nil then - npc.commands.rotate(self, {dir=dir}) - end - -- Set stand animation - self.object:set_animation({ - x = npc.ANIMATION_STAND_START, - y = npc.ANIMATION_STAND_END}, - self.animation.speed_normal, 0) -end - --- This command makes the NPC sit on the node where it is -function npc.commands.sit(self, args) - local pos = args.pos - local dir = args.dir - -- Stop NPC - self.object:setvelocity({x=0, y=0, z=0}) - -- If position given, set to that position - if pos ~= nil then - self.object:moveto(pos) - end - -- If dir given, set to that dir - if dir ~= nil then - npc.commands.rotate(self, {dir=dir}) - end - -- Set sit animation - self.object:set_animation({ - x = npc.ANIMATION_SIT_START, - y = npc.ANIMATION_SIT_END}, - self.animation.speed_normal, 0) -end - --- This command makes the NPC lay on the node where it is -function npc.commands.lay(self, args) - local pos = args.pos - -- Stop NPC - self.object:setvelocity({x=0, y=0, z=0}) - -- If position give, set to that position - if pos ~= nil then - self.object:moveto(pos) - end - -- Set sit animation - self.object:set_animation({ - x = npc.ANIMATION_LAY_START, - y = npc.ANIMATION_LAY_END}, - self.animation.speed_normal, 0) -end - --- Inventory functions for players and for nodes --- This function is a convenience function to make it easy to put --- and get items from another inventory (be it a player inv or --- a node inv) -function npc.commands.put_item_on_external_inventory(self, args) - local player = args.player - local pos = args.pos - local inv_list = args.inv_list - local item_name = args.item_name - local count = args.count - local is_furnace = args.is_furnace - local inv - if player ~= nil then - inv = minetest.get_inventory({type="player", name=player}) - else - inv = minetest.get_inventory({type="node", pos=pos}) - end - - -- Create ItemStack to put on external inventory - local item = ItemStack(item_name.." "..count) - -- Check if there is enough room to add the item on external invenotry - if inv:room_for_item(inv_list, item) then - -- Take item from NPC's inventory - if npc.take_item_from_inventory_itemstring(self, item) then - -- NPC doesn't have item and/or specified quantity - return false - end - -- Add items to external inventory - inv:add_item(inv_list, item) - - -- If this is a furnace, start furnace timer - if is_furnace == true then - minetest.get_node_timer(pos):start(1.0) - end - - return true - end - -- Not able to put on external inventory - return false -end - -function npc.commands.take_item_from_external_inventory(self, args) - local player = args.player - local pos = args.pos - local inv_list = args.inv_list - local item_name = args.item_name - local count = args.count - local inv - if player ~= nil then - inv = minetest.get_inventory({type="player", name=player}) - else - inv = minetest.get_inventory({type="node", pos=pos}) - end - -- Create ItemStack to take from external inventory - local item = ItemStack(item_name.." "..count) - -- Check if there is enough of the item to take - if inv:contains_item(inv_list, item) then - -- Add item to NPC's inventory - npc.add_item_to_inventory_itemstring(self, item) - -- Add items to external inventory - inv:remove_item(inv_list, item) - return true - end - -- Not able to put on external inventory - return false -end - -function npc.commands.check_external_inventory_contains_item(self, args) - local player = args.player - local pos = args.pos - local inv_list = args.inv_list - local item_name = args.item_name - local count = args.count - local inv - if player ~= nil then - inv = minetest.get_inventory({type="player", name=player}) - else - inv = minetest.get_inventory({type="node", pos=pos}) - end - - -- Create ItemStack for checking the external inventory - local item = ItemStack(item_name.." "..count) - -- Check if inventory contains item - return inv:contains_item(inv_list, item) -end - --- TODO: Refactor this function so that it uses a table to check --- for doors instead of having separate logic for each door type -function npc.commands.get_openable_node_state(node, pos, npc_dir) - --minetest.log("Node name: "..dump(node.name)) - local state = npc.commands.const.doors.state.CLOSED - -- Check for MTG doors and gates - local mtg_door_closed = false - if minetest.get_item_group(node.name, "door") > 0 then - local back_pos = vector.add(pos, minetest.facedir_to_dir(node.param2)) - local back_node = minetest.get_node(back_pos) - if back_node.name == "air" or minetest.registered_nodes[back_node.name].walkable == false then - mtg_door_closed = true - end - end - -- Check for cottages gates - local open_i1, open_i2 = string.find(node.name, "_close") - -- Check for cottages half door - local half_door_is_closed = false - if node.name == "cottages:half_door" then - half_door_is_closed = (node.param2 + 2) % 4 == npc_dir - end - if mtg_door_closed == false and open_i1 == nil and half_door_is_closed == false then - state = npc.commands.const.doors.state.OPEN - end - --minetest.log("Door state: "..dump(state)) - return state -end - --- This function is used to open or close openable nodes. --- Currently supported openable nodes are: any doors using the --- default doors API, and the cottages mod gates and doors. -function npc.commands.use_openable(self, args) - local pos = args.pos - local command = args.command - local dir = args.dir - local node = minetest.get_node(pos) - local state = npc.commands.get_openable_node_state(node, pos, dir) - - local clicker = self.object - if command ~= state then - minetest.registered_nodes[node.name].on_rightclick(pos, node, clicker, nil, nil) - end -end - - ---------------------------------------------------------------------------------------- --- Tasks functionality ---------------------------------------------------------------------------------------- --- Tasks are operations that require many commands to perform. Basic tasks, like --- walking from one place to another, operating a furnace, storing or taking --- items from a chest, are provided here. - -local function get_pos_argument(self, pos, use_access_node) - --minetest.log("Type of pos: "..dump(type(pos))) - -- Check which type of position argument we received - if type(pos) == "table" then - --minetest.log("Received table pos: "..dump(pos)) - -- Check if table is position - if pos.x ~= nil and pos.y ~= nil and pos.z ~= nil then - -- Position received, return position - return pos - elseif pos.place_type ~= nil then - -- Received table in the following format: - -- { - -- place_category = "", - -- place_type = "", - -- index = 1, - -- use_access_node = false|true, - -- try_alternative_if_used = true|false - -- } - local index = pos.index or 1 - local use_access_node = pos.use_access_node or false - local try_alternative_if_used = pos.try_alternative_if_used or false - local places = npc.locations.get_by_type(self, pos.place_type) - --minetest.log("Place type: "..dump(pos.place_type)) - --minetest.log("Places: "..dump(places)) - -- Check index is valid on the places map - if #places >= index then - local place = places[index] - -- Check if place is used, and if it is, find alternative if required - if try_alternative_if_used == true then - place = npc.locations.find_unused_place(self, pos.place_category, pos.place_type, place) - - --minetest.log("Mark as used? "..dump(pos.mark_target_as_used)) - if pos.mark_target_as_used == true then - --minetest.log("Marking as used: "..minetest.pos_to_string(place.pos)) - npc.locations.mark_place_used(place.pos, npc.locations.USE_STATE.USED) - end - - npc.locations.add_shared_accessible_place( - self, {owner="", node_pos=place.pos}, npc.locations.data.CALCULATED.TARGET, true, {}) - end - -- Check if access node is desired - if use_access_node == true then - -- Return actual node pos - return place.access_node, place.pos - else - -- Return node pos that allows access to node - return place.pos - end - end - end - elseif type(pos) == "string" then - -- Received name of place, so we are going to look for the actual pos - local places_pos = npc.locations.get_by_type(self, pos, false) - -- Return nil if no position found - if places_pos == nil or #places_pos == 0 then - return nil - end - -- Check if received more than one position - if #places_pos > 1 then - -- Check all places, return owned if existent, else return the first one - for i = 1, #places_pos do - if places_pos[i].status == "owned" then - if use_access_node == true then - return places_pos[i].access_node, places_pos[i].pos - else - return places_pos[i].pos - end - end - end - end - -- Return the first position only if it couldn't find an owned - -- place, or if it there is only one - if use_access_node == true then - return places_pos[1].access_node, places_pos[1].pos - else - return places_pos[1].pos - end - end -end - --- This function allows a NPC to use a furnace using only items from --- its own inventory. Fuel is not provided. Once the furnace is finished --- with the fuel items the NPC will take whatever was cooked and whatever --- remained to cook. The function received the position of the furnace --- to use, and the item to cook in furnace. Item is an itemstring -function npc.commands.use_furnace(self, args) - local pos = get_pos_argument(self, args.pos) - if pos == nil then - npc.log("WARNING", "Got nil position in 'use_furnace' using args.pos: "..dump(args.pos)) - return - end - - local enable_usage_marking = args.enable_usage_marking or true - local item = args.item - local freeze = args.freeze - -- Define which items are usable as fuels. The NPC - -- will mainly use this as fuels to avoid getting useful - -- items (such as coal lumps) for burning - local fuels = {"default:leaves", - "default:pine_needles", - "default:tree", - "default:acacia_tree", - "default:aspen_tree", - "default:jungletree", - "default:pine_tree", - "default:coalblock", - "farming:straw"} - - -- Check if NPC has item to cook - local src_item = npc.inventory_contains(self, npc.get_item_name(item)) - if src_item == nil then - -- Unable to cook item that is not in inventory - return false - end - - -- Check if NPC has a fuel item - for i = 1,9 do - local fuel_item = npc.inventory_contains(self, fuels[i]) - - if fuel_item ~= nil then - -- Get fuel item's burn time - local fuel_time = - minetest.get_craft_result({method="fuel", width=1, items={ItemStack(fuel_item.item_string)}}).time - local total_fuel_time = fuel_time * npc.get_item_count(fuel_item.item_string) - npc.log("DEBUG", "Fuel time: "..dump(fuel_time)) - - -- Get item to cook's cooking time - local cook_result = - minetest.get_craft_result({method="cooking", width=1, items={ItemStack(src_item.item_string)}}) - local total_cook_time = cook_result.time * npc.get_item_count(item) - npc.log("DEBUG", "Cook: "..dump(cook_result)) - - npc.log("DEBUG", "Total cook time: "..total_cook_time - ..", total fuel burn time: "..dump(total_fuel_time)) - - -- Check if there is enough fuel to cook all items - if total_cook_time > total_fuel_time then - -- Don't have enough fuel to cook item. Return the difference - -- so it may help on trying to acquire the fuel later. - -- NOTE: Yes, returning here means that NPC could probably have other - -- items usable as fuels and ignore them. This should be ok for now, - -- considering that fuel items are ordered in a way where cheaper, less - -- useless items come first, saving possible valuable items. - return cook_result.time - fuel_time - end - - -- Set furnace as used if flag is enabled - if enable_usage_marking then - -- Set place as used - npc.locations.mark_place_used(pos, npc.locations.USE_STATE.USED) - end - - -- Calculate how much fuel is needed - local fuel_amount = total_cook_time / fuel_time - if fuel_amount < 1 then - fuel_amount = 1 - end - - npc.log("DEBUG", "Amount of fuel needed: "..fuel_amount) - - -- Put this item on the fuel inventory list of the furnace - local args = { - player = nil, - pos = pos, - inv_list = "fuel", - item_name = npc.get_item_name(fuel_item.item_string), - count = fuel_amount - } - npc.enqueue_command(self, npc.commands.cmd.PUT_ITEM, args) - -- Put the item that we want to cook on the furnace - args = { - player = nil, - pos = pos, - inv_list = "src", - item_name = npc.get_item_name(src_item.item_string), - count = npc.get_item_count(item), - is_furnace = true - } - npc.enqueue_command(self, npc.commands.cmd.PUT_ITEM, args) - - -- Now, set NPC to wait until furnace is done. - npc.log("DEBUG", "Setting wait command for "..dump(total_cook_time)) - npc.enqueue_command(self, npc.commands.cmd.SET_INTERVAL, {interval=total_cook_time, freeze=freeze}) - - -- Reset timer - npc.enqueue_command(self, npc.commands.cmd.SET_INTERVAL, {interval=1, freeze=true}) - - -- If freeze is false, then we will have to find the way back to the furnace - -- once cooking is done. - if freeze == false then - npc.log("DEBUG", "Adding walk to position to wandering: "..dump(pos)) - npc.enqueue_script(self, npc.commands.cmd.WALK_TO_POS, {end_pos=pos, walkable={}}) - end - - -- Take cooked items back - args = { - - player = nil, - pos = pos, - inv_list = "dst", - item_name = cook_result.item:get_name(), - count = npc.get_item_count(item), - is_furnace = false - } - npc.log("DEBUG", "Taking item back: "..minetest.pos_to_string(pos)) - npc.enqueue_command(self, npc.commands.cmd.TAKE_ITEM, args) - - npc.log("DEBUG", "Inventory: "..dump(self.inventory)) - - -- Set furnace as unused if flag is enabled - if enable_usage_marking then - -- Set place as used - npc.locations.mark_place_used(pos, npc.locations.USE_STATE.NOT_USED) - end - - return true - end - end - -- Couldn't use the furnace due to lack of items - return false -end - --- This function makes the NPC lay or stand up from a bed. The --- pos is the location of the bed, command can be lay or get up -function npc.commands.use_bed(self, args) - local pos = get_pos_argument(self, args.pos) - if pos == nil then - npc.log("WARNING", "Got nil position in 'use_bed' using args.pos: "..dump(args.pos)) - return - end - local command = args.command - local enable_usage_marking = args.enable_usage_marking or true - local node = minetest.get_node(pos) - --minetest.log(dump(node)) - local dir = minetest.facedir_to_dir(node.param2) - - if command == npc.commands.const.beds.LAY then - -- Get position - -- Error here due to ignore. Need to come up with better solution - if node.name == "ignore" then - return - end - local bed_pos = npc.commands.nodes.beds[node.name].get_lay_pos(pos, dir) - -- Sit down on bed, rotate to correct direction - npc.enqueue_command(self, npc.commands.cmd.SIT, {pos=bed_pos, dir=(node.param2 + 2) % 4}) - -- Lay down - npc.enqueue_command(self, npc.commands.cmd.LAY, {}) - if enable_usage_marking then - -- Set place as used - npc.locations.mark_place_used(pos, npc.locations.USE_STATE.USED) - end - self.npc_state.movement.is_laying = true - else - -- Calculate position to get up - -- Error here due to ignore. Need to come up with better solution - if node.name == "ignore" then - return - end - local bed_pos_y = npc.commands.nodes.beds[node.name].get_lay_pos(pos, dir).y - local bed_pos = {x = pos.x, y = bed_pos_y, z = pos.z} - -- Sit up - npc.enqueue_command(self, npc.commands.cmd.SIT, {pos=bed_pos}) - -- Initialize direction: Default is front of bottom of bed - local dir = (node.param2 + 2) % 4 - -- Find empty node around node - -- Take into account that mats are close to the floor, so y adjustmen is zero - local y_adjustment = -1 - if npc.commands.nodes.beds[node.name].type == "mat" then - y_adjustment = 0 - end - - local pos_out_of_bed = pos - local empty_nodes = npc.locations.find_node_orthogonally(bed_pos, {"air", "cottages:bench"}, y_adjustment) - if empty_nodes ~= nil and #empty_nodes > 0 then - -- Get direction to the empty node - dir = npc.commands.get_direction(bed_pos, empty_nodes[1].pos) - - -- Calculate position to get out of bed - pos_out_of_bed = - {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} - -- Account for benches if they are present to avoid standing over them - if empty_nodes[1].name == "cottages:bench" then - pos_out_of_bed = {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} - if empty_nodes[1].param2 == 0 then - pos_out_of_bed.z = pos_out_of_bed.z - 0.3 - elseif empty_nodes[1].param2 == 1 then - pos_out_of_bed.x = pos_out_of_bed.x - 0.3 - elseif empty_nodes[1].param2 == 2 then - pos_out_of_bed.z = pos_out_of_bed.z + 0.3 - elseif empty_nodes[1].param2 == 3 then - pos_out_of_bed.x = pos_out_of_bed.x + 0.3 - end - end - - end - -- Stand out of bed - npc.enqueue_command(self, npc.commands.cmd.STAND, {pos=pos_out_of_bed, dir=dir}) - if enable_usage_marking then - -- Set place as unused - npc.locations.mark_place_used(pos, npc.locations.USE_STATE.NOT_USED) - end - self.npc_state.movement.is_laying = false - end -end - --- This function makes the NPC lay or stand up from a sittable node. The --- pos is the location of the sittable node, command can be lay or get up -function npc.commands.use_sittable(self, args) - local pos = get_pos_argument(self, args.pos) - if pos == nil then - npc.log("WARNING", "Got nil position in 'use_sittable' using args.pos: "..dump(args.pos)) - return - end - local command = args.command - local enable_usage_marking = args.enable_usage_marking or true - local node = minetest.get_node(pos) - - if command == npc.commands.const.sittable.SIT then - -- Calculate position depending on bench - -- Error here due to ignore. Need to come up with better solution - if node.name == "ignore" then - return - end - local sit_pos = npc.commands.nodes.sittable[node.name].get_sit_pos(pos, node.param2) - -- Sit down on bench/chair/stairs - npc.enqueue_command(self, npc.commands.cmd.SIT, {pos=sit_pos, dir=(node.param2 + 2) % 4}) - if enable_usage_marking then - -- Set place as used - npc.locations.mark_place_used(pos, npc.locations.USE_STATE.USED) - end - sstate.is_sitting = true - else - if self.npc_state.movement.is_sitting == false then - npc.log("DEBUG_ACTION", "NPC "..self.npc_name.." attempted to get up from sit when it is not sitting.") - return - end - -- Find empty areas around chair - local dir = node.param2 + 2 % 4 - -- Default it to the current position in case it can't find empty - -- position around sittable node. Weird - local pos_out_of_sittable = pos - local empty_nodes = npc.locations.find_node_orthogonally(pos, {"air"}, 0) - if empty_nodes ~= nil and #empty_nodes > 0 then - --minetest.log("Empty nodes: "..dump(empty_nodes)) - --minetest.log("Npc.commands.get_direction: "..dump(npc.commands.get_direction)) - --minetest.log("Pos: "..dump(pos)) - -- Get direction to the empty node - dir = npc.commands.get_direction(pos, empty_nodes[1].pos) - -- Calculate position to get out of sittable node - pos_out_of_sittable = - {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} - end - -- Stand - npc.enqueue_command(self, npc.commands.cmd.STAND, {pos=pos_out_of_sittable, dir=dir}) - minetest.log("Setting sittable at "..minetest.pos_to_string(pos).." as not used") - if enable_usage_marking then - -- Set place as unused - npc.locations.mark_place_used(pos, npc.locations.USE_STATE.NOT_USED) - end - self.npc_state.movement.is_sitting = false - end -end - --- This function returns the direction enum --- for the moving from v1 to v2 -function npc.commands.get_direction(v1, v2) - local vector_dir = vector.direction(v1, v2) - local dir = vector.round(vector_dir) - - if dir.x ~= 0 and dir.z ~= 0 then - if dir.x > 0 and dir.z > 0 then - return npc.direction.north_east - elseif dir.x > 0 and dir.z < 0 then - return npc.direction.south_east - elseif dir.x < 0 and dir.z > 0 then - return npc.direction.north_west - elseif dir.x < 0 and dir.z < 0 then - return npc.direction.south_west - end - elseif dir.x ~= 0 and dir.z == 0 then - if dir.x > 0 then - return npc.direction.east - else - return npc.direction.west - end - elseif dir.z ~= 0 and dir.x == 0 then - if dir.z > 0 then - return npc.direction.north - else - return npc.direction.south - end - end -end - - --- This function can be used to make the NPC walk from one --- position to another. If the optional parameter walkable_nodes --- is included, which is a table of node names, these nodes are --- going to be considered walkable for the algorithm to find a --- path. -npc.commands.register_script("advanced_npc:walk_to_pos", function(self, args) - minetest.log("Received arguments: "..dump(args)) - -- Get arguments for this task - local use_access_node = true - if args.use_access_node ~= nil then - use_access_node = args.use_access_node - end - local end_pos, node_pos = get_pos_argument(self, args.end_pos, use_access_node) - if end_pos == nil then - npc.log("WARNING", "Got nil position in 'walk_to_pos' using args.pos: "..dump(args.end_pos)) - return - end - local enforce_move = args.enforce_move or true - local walkable_nodes = args.walkable - - -- Round start_pos to make sure it can find start and end - local start_pos = vector.round(self.object:getpos()) - npc.log("DEBUG", "walk_to_pos: Start pos: "..minetest.pos_to_string(start_pos)) - npc.log("DEBUG", "walk_to_pos: End pos: "..minetest.pos_to_string(end_pos)) - - -- Check if start_pos and end_pos are the same - if vector.equals(start_pos, end_pos) == true then - -- Check if it was using access node, if it was, add command to - -- rotate NPC into that direction - if use_access_node == true then - local dir = npc.commands.get_direction(end_pos, node_pos) - npc.enqueue_command(self, npc.commands.cmd.STAND, {dir = dir}) - end - npc.log("WARNING", "walk_to_pos Found start_pos == end_pos") - return - end - - - -- Set walkable nodes to empty if the parameter hasn't been used - if walkable_nodes == nil then - walkable_nodes = {} - end - - -- Find path - local path = npc.pathfinder.find_path(start_pos, end_pos, self, true) - - if path ~= nil and #path > 1 then - npc.log("INFO", "walk_to_pos Found path to node: "..minetest.pos_to_string(end_pos)) - -- Store path - self.actions.walking.path = path - - -- Local variables - local door_opened = false - local speed = npc.commands.two_nps_speed - - -- Set the command timer interval to half second. This is to account for - -- the increased speed when walking. - npc.enqueue_command(self, npc.commands.cmd.SET_INTERVAL, {interval=0.5, freeze=true}) - - -- Set the initial last and target positions - self.actions.walking.target_pos = path[1].pos - - -- Add steps to path - for i = 1, #path do - -- Do not add an extra step if reached the goal node - if (i+1) == #path then - -- Add direction to last node - local dir = npc.commands.get_direction(path[i].pos, end_pos) - -- Add the last step - npc.enqueue_command(self, npc.commands.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+1].pos}) - -- Add stand animation at end - if use_access_node == true then - dir = npc.commands.get_direction(end_pos, node_pos) - end - minetest.log("Dir: "..dump(dir)) - -- Change dir if using access_node - npc.enqueue_command(self, npc.commands.cmd.STAND, {dir = dir}) - break - end - -- Get direction to move from path[i] to path[i+1] - local dir = npc.commands.get_direction(path[i].pos, path[i+1].pos) - -- Check if next node is a door, if it is, open it, then walk - if path[i+1].type == npc.pathfinder.node_types.openable then - -- Check if door is already open - local node = minetest.get_node(path[i+1].pos) - if npc.commands.get_openable_node_state(node, path[i+1].pos, dir) == npc.commands.const.doors.state.CLOSED then - --minetest.log("Opening command to open door") - -- Stop to open door, this avoids misplaced movements later on - npc.enqueue_command(self, npc.commands.cmd.STAND, {dir=dir}) - -- Open door - npc.enqueue_command(self, npc.commands.cmd.USE_OPENABLE, {pos=path[i+1].pos, dir=dir, command=npc.commands.const.doors.command.OPEN}) - - door_opened = true - end - - end - - -- Add walk command to command queue - npc.enqueue_command(self, npc.commands.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+1].pos}) - - if door_opened then - -- Stop to close door, this avoids misplaced movements later on - -- local x_adj, z_adj = 0, 0 - -- if dir == 0 then - -- z_adj = 0.1 - -- elseif dir == 1 then - -- x_adj = 0.1 - -- elseif dir == 2 then - -- z_adj = -0.1 - -- elseif dir == 3 then - -- x_adj = -0.1 - -- end - -- local pos_on_close = {x=path[i+1].pos.x + x_adj, y=path[i+1].pos.y + 1, z=path[i+1].pos.z + z_adj} - -- Add extra walk step to ensure that one is standing at other side of openable node - -- npc.enqueue_command(self, npc.commands.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+2].pos}) - -- Stop to close the door - npc.enqueue_command(self, npc.commands.cmd.STAND, {dir=(dir + 2) % 4 })--, pos=pos_on_close}) - -- Close door - npc.enqueue_command(self, npc.commands.cmd.USE_OPENABLE, {pos=path[i+1].pos, command=npc.commands.const.doors.command.CLOSE}) - - door_opened = false - end - - end - - -- Return the command interval to default interval of 1 second - -- By default, always freeze. - npc.enqueue_command(self, npc.commands.cmd.SET_INTERVAL, {interval=1, freeze=true}) - - else - -- Unable to find path - npc.log("WARNING", "walk_to_pos Unable to find path. Teleporting to: "..minetest.pos_to_string(end_pos)) - -- Check if movement is enforced - if enforce_move then - -- Move to end pos - self.object:moveto({x=end_pos.x, y=end_pos.y+1, z=end_pos.z}) - end - end -end) - -npc.commands.register_script("advanced_npc:optimized_walk_to_pos", function(self, args) - local start_pos = self.object:getpos() - local end_pos = args.end_pos - local walkable_nodes = args.walkable_nodes or {} - -- Optimized walking -- since distances can be really short, - -- a simple walk_step() action can do most of the times. For - -- this, however, we need to calculate direction - -- First of all, check distance - local distance = vector.distance(start_pos, end_pos) - if distance < 3 then - -- Will do walk_step based instead - if distance > 1 then - args = { - dir = npc.commands.get_direction(start_pos, end_pos), - speed = npc.commands.one_nps_speed - } - -- Enqueue walk step - npc.enqueue_command(self, npc.commands.cmd.WALK_STEP, args) - end - -- Add standing action to look at end_pos - npc.enqueue_command(self, npc.commands.cmd.STAND, - {dir = npc.commands.get_direction(self.object:getpos(), end_pos)} - ) - else - -- Set proper use_access_node param --- local use_access_node = true --- if args.use_access_node ~= nil then --- use_access_node = args.use_access_node --- end --- local new_args = { --- end_pos = end_pos, --- walkable = walkable_nodes, --- use_access_node = use_access_node --- } --- -- Enqueue --- npc.enqueue_script(self, "advanced_npc:walk_to_pos", new_args) - local walk_args = { - dir = npc.commands.get_direction(start_pos, end_pos), - speed = npc.commands.one_nps_speed - } - -- Enqueue walk step - npc.enqueue_command(self, npc.commands.cmd.WALK_STEP, walk_args) - end -end) - --- Helper function to determine if a NPC is moving -function npc.commands.is_moving(self) - return math.abs(vector.length(self.object:getvelocity())) > 0 -end - --- Idle state script. NPC stays still on this state. --- It is possible for it to acknowledge other NPCs or players if --- configured as arguments. --- Arguments: --- - `acknowledge_nearby_objs`: boolean. If true, will look for objects and --- rotate towards them when close by. --- - `obj_search_interval`: integer, interval in seconds to search for objects. --- Default is 5 --- - `obj_search_radius`: integer, radius in nodes to search for objects. --- Default is 5 -npc.commands.register_script("advanced_npc:idle", function (self, args) - local search_nearby_objs = args.acknowledge_nearby_objs - local obj_search_interval = args.obj_search_interval or 5 - local obj_search_radius = args.obj_search_radius or 5 - - -- Check if NPC is moving, if it is, stop. - if npc.commands.is_moving(self) then - npc.commands.execute(self, npc.commands.cmd.STAND, {}) - end - - if search_nearby_objs == true then - -- Search nearby objects - local objs = minetest.get_objects_inside_radius(self.object:getpos(), obj_search_radius) - - if #objs > 1 then - for _,obj in pairs(objs) do - -- Check if it is player or NPC or mob (mobs_redo) in general - if obj:is_player() or - (obj:get_luaentity() and obj:get_luaentity().npc_id and obj:get_luaentity().npc_id ~= self.npc_id) or - (obj:get_luaentity() and obj:get_luaentity().type == "animal") then - -- Shorten interval to rotate accurately towards object - npc.commands.execute(self, npc.commands.cmd.SET_INTERVAL, {interval=0.5}) - -- Rotate NPC towards object - local yaw = minetest.dir_to_yaw(vector.direction(self.object:getpos(), obj:getpos())) - npc.commands.execute(self, npc.commands.cmd.ROTATE, {yaw=yaw}) - end - end - else - -- Set interval - npc.commands.execute(self, npc.commands.cmd.SET_INTERVAL, {interval=obj_search_interval}) - minetest.log("No obj found") - end - end -end) - --- Follow state script. This is a looping script that will try to follow an --- entity or player until either of the following conditions are met: --- - A certain flag is set to false --- - The object is reached and a callback executed. --- This script is not interruptable by the scheduler function. --- Arguments: --- - `radius`: integer, initial search radius. Default is 3 --- - `max_radius`: integer, maximum search radius. If target isn't found within initial radius, --- radius will increase up to this value. Default is 20 --- - `speed`: number, walking speed for the NPC while following. Default is 3 --- - `target`: string, can be "player" or "entity". --- - `player_name`: string, name of player to follow --- - `entity_type`: string, type of entity to follow. NOT IMPLEMENTED. --- - `on_reach`: function, if given, on reaching the target, this function will --- be called and executed. On execution, the script will finish. DO NOT use with --- `follow_flag`. --- - `follow_flag`: string, flag name. If given, the script will keep running until the --- value of this flag is false. DO NOT use with `on_reach`. -npc.commands.register_script("advanced_npc:follow", function(self, args) - -- Set default arguments if not present - args.radius = args.radius or 3 - args.max_radius = args.max_radius or 20 - args.speed = args.speed or 3 - args.results_key = "advanced_npc:follow:player_follow" - - -- Run this 1/speed times in a second - npc.commands.execute(self, npc.commands.cmd.SET_INTERVAL, {interval=1/args.speed, freeze=true}) - -- Make NPC climb one-block heights. Makes following easier - self.stepheight = 1.1 - self.object:set_properties(self) - - -- Execution - -- Follow, results to be stored on execution context with key "results_key" - npc.enqueue_function(self, "advanced_npc:follow", "follow_player", args, args.results_key) - -- Check if follow is complete - npc.enqueue_function(self, "advanced_npc:follow", "check_if_complete", args) -end) - --- Follow script functions --- Function used to reset NPC values once following is complete -npc.commands.register_script_function("advanced_npc:follow", "reset", function(self) - self.stepheight = 0.6 - self.object:set_properties(self) - npc.enqueue_command(self, npc.commands.cmd.SET_INTERVAL, {interval=1, freeze=false}) -end) - --- Follow the player -npc.commands.register_script_function("advanced_npc:follow", "follow_player", function(self, args) - - if args.target == "player" then - local player_name = args.player_name - local objs = minetest.get_objects_inside_radius(self.object:getpos(), args.radius) - -- Check if objects were found - minetest.log("Objects found: "..dump(objs)) - if #objs > 0 then - for _,obj in pairs(objs) do - if obj then - -- Check if this is the player we are looking for - if obj:is_player() and obj:get_player_name() == player_name then - local target_pos = vector.round(obj:getpos()) - -- Calculate distance - if less than 3, avoid walking any further - if vector.distance(self.object:getpos(), target_pos) < 3 then - npc.log("SCRIPT", "[follow] Destination reached") - -- Destination reached - -- Add standing action if NPC is still moving - if math.abs(vector.length(self.object:getvelocity())) > 0 then - npc.execute_command(self, npc.commands.cmd.STAND, - {dir = minetest.dir_to_yaw(vector.direction(self.object:getpos(), target_pos))} - ) - end - - -- Rotate NPC towards player - npc.execute_command(self, npc.commands.cmd.ROTATE, {yaw = minetest.dir_to_yaw(vector.direction(self.object:getpos(), target_pos))}) - - -- Execute `on_reach` function if present - if args.on_reach then - npc.log("SCRIPT", "[follow] Executing on_reach callback...") - args.on_reach(self, obj) - return {reached_target = true, target_pos = target_pos, end_execution = true} - end - - return {reached_target = true, target_pos = target_pos} - else - npc.log("SCRIPT", "[follow] Walking towards player...") - local walk_args = { - yaw = minetest.dir_to_yaw(vector.direction(self.object:getpos(), target_pos)), - speed = args.speed - } - -- Enqueue walk step - npc.enqueue_command(self, npc.commands.cmd.WALK_STEP, walk_args) - return {reached_target = false, target_pos = target_pos} - end - end - end - end - -- Player not found, stop - npc.enqueue_command(self, npc.commands.cmd.STAND, {}) - return {reached_target = false, target_pos = nil} - end - end - return {reached_target = false, target_pos = nil} -end) - - -npc.commands.register_script_function("advanced_npc:follow", "check_if_complete", function(self, args) - -- Check if follow is still needed - if npc.get_flag(self, args.follow_flag) == false then - -- Stop, follow no more - npc.enqueue_command(self, npc.commands.cmd.STAND, {}) - -- Clear flag - npc.update_flag(self, args.follow_flag, nil) - -- Reset actions interval and NPC stepheight - npc.commands.execute_script_function(self, "advanced_npc:follow", "reset", {}) - return - end - - -- Get results from following - local follow_result = npc.execution.context.get(self, args.results_key) - -- Check results - if follow_result == nil then - npc.log("WARNING", "Unable to find result in execution context for 'follow_player' function using key: ".. - dump(args.results_key)) - return - end - -- Clean execution context - npc.execution.context.remove(self, args.results_key) - - -- Check if target reached and on_reach function executed - if follow_result.reached_target == true and follow_result.end_execution == true then - return - end - -- on_reach is not set, keep executing until follow flag is off. - if follow_result.target_pos ~= nil then - -- Keep walking or waiting for player to keep moving - npc.enqueue_function(self, "advanced_npc:follow", "follow_player", args, args.results_key) - -- Check if follow is complete - npc.enqueue_function(self, "advanced_npc:follow", "check_if_complete", args) - --npc.enqueue_function(self, detect_more_movement, {player_pos = follow_result.target_pos}) - else - -- Cannot find - npc.log("SCRIPT", "[follow] Walking towards player") - -- Modify args to increase radius - args.radius = args.radius + 1 - npc.enqueue_function(self, "advanced_npc:follow", "follow_player", args, args.results_key) - -- Check if follow is complete - npc.enqueue_function(self, "advanced_npc:follow", "check_if_complete", args) - end -end) - diff --git a/mods/advanced_npc/executable/helper.lua b/mods/advanced_npc/executable/helper.lua deleted file mode 100644 index 438c980..0000000 --- a/mods/advanced_npc/executable/helper.lua +++ /dev/null @@ -1,340 +0,0 @@ --- --- User: hfranqui --- Date: 3/8/18 --- Time: 2:41 PM --- - -npc.programs.const = { - dir_data = { - -- North - [0] = { - yaw = 0, - vel = {x=0, y=0, z=1} - }, - -- East - [1] = { - yaw = (3 * math.pi) / 2, - vel = {x=1, y=0, z=0} - }, - -- South - [2] = { - yaw = math.pi, - vel = {x=0, y=0, z=-1} - }, - -- West - [3] = { - yaw = math.pi / 2, - vel = {x=-1, y=0, z=0} - }, - -- North east - [4] = { - yaw = (7 * math.pi) / 4, - vel = {x=1, y=0, z=1} - }, - -- North west - [5] = { - yaw = math.pi / 4, - vel = {x=-1, y=0, z=1} - }, - -- South east - [6] = { - yaw = (5 * math.pi) / 4, - vel = {x=1, y=0, z=-1} - }, - -- South west - [7] = { - yaw = (3 * math.pi) / 4, - vel = {x=-1, y=0, z=-1} - } - }, - node_ops = { - doors = { - command = { - OPEN = 1, - CLOSE = 2 - }, - state = { - OPEN = 1, - CLOSED = 2 - } - }, - beds = { - LAY = 1, - GET_UP = 2 - }, - sittable = { - SIT = 1, - GET_UP = 2 - } - }, - speeds = { - one_nps_speed = 1, - one_half_nps_speed = 1.5, - two_nps_speed = 2 - }, - place_src = { - take_from_inventory = "take_from_inventory", - take_from_inventory_forced = "take_from_inventory_forced", - force_place = "force_place" - }, - craft_src = { - take_from_inventory = "take_from_inventory", - take_from_inventory_forced = "take_from_inventory_forced", - force_craft = "force_craft" - } -} - -npc.programs.helper = {} - --- Helper functions --- This function returns the direction enum --- for the moving from v1 to v2 -function npc.programs.helper.get_direction(v1, v2) - local vector_dir = vector.direction(v1, v2) - local dir = vector.round(vector_dir) - - if dir.x ~= 0 and dir.z ~= 0 then - if dir.x > 0 and dir.z > 0 then - return npc.direction.north_east - elseif dir.x > 0 and dir.z < 0 then - return npc.direction.south_east - elseif dir.x < 0 and dir.z > 0 then - return npc.direction.north_west - elseif dir.x < 0 and dir.z < 0 then - return npc.direction.south_west - end - elseif dir.x ~= 0 and dir.z == 0 then - if dir.x > 0 then - return npc.direction.east - else - return npc.direction.west - end - elseif dir.z ~= 0 and dir.x == 0 then - if dir.z > 0 then - return npc.direction.north - else - return npc.direction.south - end - end -end - --- This function allows to move into directions that are walkable. It --- avoids fences and allows to move on plants. --- This will make for nice wanderings, making the NPC move smartly instead --- of just *oftenly* getting stuck at places... note that this will *NOT* --- completely avoid the NPC being stuck -function npc.programs.helper.random_dir(start_pos, speed, dir_start, dir_end) - -- - local bad_dirs = {} - --minetest.log("Args: "..dump(start_pos)..", "..dump(speed)..","..dump(dir_start)..", "..dump(dir_end)) - -- Limit the number of tries - otherwise it could become an infinite loop - for i = 1, 8 do - local dir = math.random(dir_start, dir_end) - if (bad_dirs[dir] == false) then - -- Found dir that was known as bad, try dir + 1 until not - -- found or greater than dir_end - local good_found = false - for j = dir_start, dir_end do - dir = dir + 1 - if bad_dirs[dir] == nil then - break - end - end - if good_found == false then - return -1 - end - end - - -- Find out if there are walkable nodes in the path ahead - local vel = vector.multiply(npc.programs.const.dir_data[dir].vel, speed) - local pos = vector.add(start_pos, vel) - local node_below = minetest.get_node(vector.round(pos)) - local node_above = minetest.get_node(vector.round({x=pos.x,y=pos.y+1,z=pos.z})) - - if node_below and node_above then - if npc.locations.is_walkable(node_below.name) and npc.locations.is_walkable(node_above.name) then - return dir - else - bad_dirs[dir] = false - end - end - end - -- Return -1 signaling that no good direction could be found - return -1 -end - --- TODO: Refactor this function so that it uses a table to check --- for doors instead of having separate logic for each door type -function npc.programs.helper.get_openable_node_state(node, pos, npc_dir) - --minetest.log("Node name: "..dump(node.name)) - local state = npc.programs.const.node_ops.doors.state.CLOSED - -- Check for MTG doors and gates - local mtg_door_closed = false - if minetest.get_item_group(node.name, "door") > 0 then - local back_pos = vector.add(pos, minetest.facedir_to_dir(node.param2)) - local back_node = minetest.get_node(back_pos) - if back_node.name == "air" or minetest.registered_nodes[back_node.name].walkable == false then - mtg_door_closed = true - end - end - -- Check for cottages gates - local open_i1, open_i2 = string.find(node.name, "_close") - -- Check for cottages half door - local half_door_is_closed = false - if node.name == "cottages:half_door" then - half_door_is_closed = (node.param2 + 2) % 4 == npc_dir - end - if mtg_door_closed == false and open_i1 == nil and half_door_is_closed == false then - state = npc.programs.const.node_ops.doors.state.OPEN - end - --minetest.log("Door state: "..dump(state)) - return state -end - --- Can receive a position argument in different formats --- TODO: Document formats -function npc.programs.helper.get_pos_argument(self, pos, use_access_node) --- minetest.log("Type of pos: "..dump(type(pos))) --- minetest.log("Pos: "..dump(pos)) - -- Check which type of position argument we received - if type(pos) == "table" then - --minetest.log("Received table pos: "..dump(pos)) - -- Check if table is position - if pos.x ~= nil and pos.y ~= nil and pos.z ~= nil then - -- Position received, return position - return pos - elseif pos.place_type ~= nil then - -- Received table in the following format: - -- { - -- place_category = "", - -- place_type = "", - -- index = 1, - -- use_access_node = false|true, - -- try_alternative_if_used = true|false - -- } - local index = pos.index or 1 - local use_access_node = pos.use_access_node or false - local try_alternative_if_used = pos.try_alternative_if_used or false - local places = npc.locations.get_by_type(self, pos.place_type) - minetest.log("Place type: "..dump(pos.place_type)) - minetest.log("Places: "..dump(places)) - -- Check index is valid on the places map - if #places >= index then - local place = places[index] - -- Check if place is used, and if it is, find alternative if required - if try_alternative_if_used == true then - minetest.log("Self places map: "..dump(self.places_map)) - minetest.log("Place category: "..dump(pos.place_category)) - minetest.log("Place type: "..dump(pos.place_type)) - minetest.log("Original Place: "..dump(place)) - place = npc.locations.find_unused_place(self, pos.place_category, pos.place_type, place) - minetest.log("New place: "..dump(place)) - - if next(place) ~= nil then - --minetest.log("Mark as used? "..dump(pos.mark_target_as_used)) - if pos.mark_target_as_used == true then - --minetest.log("Marking as used: "..minetest.pos_to_string(place.pos)) - npc.locations.mark_place_used(place.pos, npc.locations.USE_STATE.USED) - end - - npc.locations.add_shared_accessible_place( - self, {owner="", node_pos=place.pos}, npc.locations.data.calculated.target, true, {}) - else - return nil - end - end - -- Check if access node is desired - if use_access_node == true then - -- Return actual node pos - return place.access_node, place.pos - else - -- Return node pos that allows access to node - return place.pos - end - end - end - elseif type(pos) == "string" then - --npc.log("INFO", "Places map: "..dump(self.places_map)) - -- Received name of place, so we are going to look for the actual pos - local places_pos = npc.locations.get_by_type(self, pos, false) - --npc.log("INFO", "FOUND: "..dump(places_pos)) - -- Return nil if no position found - if places_pos == nil or #places_pos == 0 then - return nil - end - -- Check if received more than one position - if #places_pos > 1 then - -- Check all places, return owned if existent, else return the first one - for i = 1, #places_pos do - if places_pos[i].status == "owned" then - if use_access_node == true then - return places_pos[i].access_node, places_pos[i].pos - else - return places_pos[i].pos - end - end - end - end - -- Return the first position only if it couldn't find an owned - -- place, or if it there is only one - if use_access_node == true then - return places_pos[1].access_node, places_pos[1].pos - else - return places_pos[1].pos - end - end -end - --- Helper function to determine if a NPC is moving -function npc.programs.helper.is_moving(self) - return math.abs(vector.length(self.object:getvelocity())) > 0 -end - --- Leave this for now as it might be useful ---npc.commands.register_script("advanced_npc:optimized_walk_to_pos", function(self, args) --- local start_pos = self.object:getpos() --- local end_pos = args.end_pos --- local walkable_nodes = args.walkable_nodes or {} --- -- Optimized walking -- since distances can be really short, --- -- a simple walk_step() action can do most of the times. For --- -- this, however, we need to calculate direction --- -- First of all, check distance --- local distance = vector.distance(start_pos, end_pos) --- if distance < 3 then --- -- Will do walk_step based instead --- if distance > 1 then --- args = { --- dir = npc.commands.get_direction(start_pos, end_pos), --- speed = npc.programs.const.speeds.one_nps_speed --- } --- -- Enqueue walk step --- npc.enqueue_command(self, npc.commands.cmd.WALK_STEP, args) --- end --- -- Add standing action to look at end_pos --- npc.enqueue_command(self, npc.commands.cmd.STAND, --- {dir = npc.commands.get_direction(self.object:getpos(), end_pos)} --- ) --- else --- -- Set proper use_access_node param --- -- local use_access_node = true --- -- if args.use_access_node ~= nil then --- -- use_access_node = args.use_access_node --- -- end --- -- local new_args = { --- -- end_pos = end_pos, --- -- walkable = walkable_nodes, --- -- use_access_node = use_access_node --- -- } --- -- -- Enqueue --- -- npc.enqueue_script(self, "advanced_npc:walk_to_pos", new_args) --- local walk_args = { --- dir = npc.commands.get_direction(start_pos, end_pos), --- speed = npc.commands.one_nps_speed --- } --- -- Enqueue walk step --- npc.enqueue_command(self, npc.commands.cmd.WALK_STEP, walk_args) --- end ---end) - - - diff --git a/mods/advanced_npc/executable/instructions/api.lua b/mods/advanced_npc/executable/instructions/api.lua deleted file mode 100644 index 6b07973..0000000 --- a/mods/advanced_npc/executable/instructions/api.lua +++ /dev/null @@ -1,39 +0,0 @@ --- --- Created by IntelliJ IDEA. --- Date: 3/8/18 --- Time: 2:06 PM --- --- Global namespace -npc.programs.instr = { - helper = {} -} --- Private namespace -local _programs = { - instr = { - registered_instructions = {} - } -} - --- Registration function -function npc.programs.instr.register(name, func) - if _programs.instr.registered_instructions[name] ~= nil then - npc.log("ERROR", "Attempted to register instrcution with name: "..dump(name)..".\nInstruction already exists.") - return - end - _programs.instr.registered_instructions[name] = {func = func} -end - --- Execution function -function npc.programs.instr.execute(self, name, args) - if _programs.instr.registered_instructions[name] == nil then - npc.log("ERROR", "Attempted to execute instruction with name "..dump(name)..".\nInstruction doesn't exists.") - return - end - -- Enqueue callbacks if any - if npc.monitor.callback.exists(npc.monitor.callback.type.instruction, name) then - -- Enqueue all callbacks for this instruction - npc.monitor.callback.enqueue_all(self, npc.monitor.callback.type.instruction, name) - end - --npc.log("INFO", "Executing instruction '"..dump(name).."' with args:\n"..dump(args)) - return _programs.instr.registered_instructions[name].func(self, args) -end diff --git a/mods/advanced_npc/executable/instructions/builtin_instructions.lua b/mods/advanced_npc/executable/instructions/builtin_instructions.lua deleted file mode 100644 index b53b359..0000000 --- a/mods/advanced_npc/executable/instructions/builtin_instructions.lua +++ /dev/null @@ -1,730 +0,0 @@ --- --- Created by IntelliJ IDEA. --- Date: 3/8/18 --- Time: 2:16 PM --- - ---------------------------------------------------------------------------------------- --- Default advanced_npc instructions ---------------------------------------------------------------------------------------- --- Provides a rich set of default instructions to perform most common actions --- a NPC needs to, like walking, rotating, standing, sitting, inventory --- interaction, etc. - -npc.programs.instr.default = { - SET_INTERVAL = "advanced_npc:set_instruction_interval", - WAIT = "advanced_npc:wait", - SET_PROCESS_INTERVAL = "advanced_npc:set_process_interval", - FREEZE = "advanced_npc:freeze", - INTERRUPT = "advanced_npc:interrupt", - DIG = "advanced_npc:dig", - PLACE = "advanced_npc:place", - ROTATE = "advanced_npc:rotate", - WALK_STEP = "advanced_npc:walk_step", - STAND = "advanced_npc:stand", - SIT = "advanced_npc:sit", - LAY = "advanced_npc:lay", - PUT_ITEM = "advanced_npc:external_inventory_put", - TAKE_ITEM = "advanced_npc:external_inventory_take", - CHECK_ITEM = "advanced_npc:external_inventory_check", - USE_OPENABLE = "advanced_npc:use_openable_node" -} - --- Control instructions -- --- The following instruction alters the instruction timer interval, therefore --- making waits and pauses possible, or increase timing when some commands want to --- be performed faster, like walking. -npc.programs.instr.register("advanced_npc:set_instruction_interval", function(self, args) - local new_interval = args.interval - local freeze_mobs_api = args.freeze - - self.execution.process_queue[1].execution_context.instr_interval = new_interval - return not freeze_mobs_api -end) - -npc.programs.instr.register("advanced_npc:set_process_interval", function(self, args) - local new_interval = args.interval - -- Update interval - self.execution.scheduler_interval = new_interval -end) - --- Syntacic sugar to make a process wait for a specific interval -npc.programs.instr.register("advanced_npc:wait", function(self, args) - local wait_time = args.time - -- npc.programs.instr.execute(self, "advanced_npc:set_process_interval", {interval = wait_time - 1}) - -- npc.exec.proc.enqueue(self, "advanced_npc:set_process_interval", {interval = 1}) - npc.programs.instr.execute(self, "advanced_npc:set_instruction_interval", {interval = wait_time - 1}) - npc.exec.proc.enqueue(self, "advanced_npc:set_instruction_interval", {interval = 1}) -end) - --- The following command is for allowing the rest of mobs redo API to be executed --- after this command ends. This is useful for times when no command is needed --- and the NPC is allowed to roam freely. -npc.programs.instr.register("advanced_npc:freeze", function(self, args) - local freeze_mobs_api = args.freeze - local disable_rightclick = args.disable_rightclick - if disable_rightclick ~= nil then - npc.log("INFO", "Enabling right-click interrupts for NPC "..self.npc_name..": "..dump(not(disable_rightclick))) - self.enable_rightclick_interaction = not(disable_rightclick) - end - - return not(freeze_mobs_api) -end) - --- This instruction allow interrupts to be enqueable, in case some programs --- needs to be run in the future. -npc.programs.instr.register("advanced_npc:interrupt", function(self, args) - local new_program = args.new_program - local new_args = args.new_args - local interrupt_options = args.interrupt_options - - npc.exec.interrupt(self, new_program, new_args, interrupt_options) -end) - -npc.programs.instr.register("advanced_npc:set_interrupt_options", function(self, args) - local allow_punch = args.allow_punch - local allow_rightclick = args.allow_rightclick - local allow_schedule = args.allow_schedule - -- Set defaults - if allow_punch == nil then allow_punch = true end - if allow_rightclick == nil then allow_rightclick = true end - if allow_schedule == nil then allow_schedule = true end - - -- Set interrupt options - self.execution.process_queue[1].interrupt_options = { - allow_punch = allow_punch, - allow_rightclick = allow_rightclick, - allow_schedule = allow_schedule - } - npc.log("INFO", "New process: "..dump(self.execution.process_queue[1])) -end) - --- This instructions sets the object animation -npc.programs.instr.register("advanced_npc:set_animation", function(self, args) - self.object:set_animation( - { - x = args.start_frame, - y = args.end_frame - }, - args.frame_speed, - args.frame_blend or 0, - args.frame_loop or true) -end) - - --- Interaction instructions -- --- This command digs the node at the given position --- If 'add_to_inventory' is true, it will put the digged node in the NPC --- inventory. --- Returns true if dig is successful, otherwise false -npc.programs.instr.register("advanced_npc:dig", function(self, args) - local pos = args.pos - local add_to_inventory = args.add_to_inventory - local bypass_protection = args.bypass_protection - local play_sound = args.play_sound or true - local node = minetest.get_node_or_nil(pos) - if node then - -- Set mine animation - self.object:set_animation({ - x = npc.ANIMATION_MINE_START, - y = npc.ANIMATION_MINE_END}, - self.animation.speed_normal, 0) - - -- Play dig sound - if play_sound == true then - if minetest.registered_nodes[node.name].sounds then - minetest.sound_play( - minetest.registered_nodes[node.name].sounds.dug, - { - max_hear_distance = 10, - object = self.object - } - ) - end - end - - -- Check if protection not enforced - if not bypass_protection then - -- Try to dig node - if minetest.dig_node(pos) then - -- Add to inventory the node drops - if add_to_inventory then - -- Get node drop - local drop = minetest.registered_nodes[node.name].drop - local drop_itemname = node.name - if drop and drop.items then - local random_item = drop.items[math.random(1, #drop.items)] - if random_item then - drop_itemname = random_item.items[1] - end - end - -- Add to NPC inventory - npc.add_item_to_inventory(self, drop_itemname, 1) - end - --return true - return - end - else - -- Add to inventory - if add_to_inventory then - -- Get node drop - local drop = minetest.registered_nodes[node.name].drop - local drop_itemname = node.name - if drop and drop.items then - local random_item = drop.items[math.random(1, #drop.items)] - if random_item then - drop_itemname = random_item.items[1] - end - end - -- Add to NPC inventory - npc.add_item_to_inventory(self, drop_itemname, 1) - end - -- Dig node - minetest.log("Setting air at pos: "..minetest.pos_to_string(pos)) - minetest.set_node(pos, {name="air"}) - end - end - --return false -end) - --- This command places a given node at the given position --- There are three ways to source the node: --- 1. take_from_inventory: takes node from inventory. If not in inventory, --- node isn't placed. --- 2. take_from_inventory_forced: takes node from inventory. If not in --- inventory, node will be placed anyways. --- 3. force_place: places node regardless of inventory - will not touch --- the NPCs inventory -npc.programs.instr.register("advanced_npc:place", function(self, args) - local pos = args.pos - local node = args.node - local source = args.source - local bypass_protection = args.bypass_protection - local play_sound = args.play_sound or true - local node_at_pos = minetest.get_node_or_nil(pos) - -- Check if position is empty or has a node that can be built to - if node_at_pos and - (node_at_pos.name == "air" or minetest.registered_nodes[node_at_pos.name].buildable_to == true) then - -- Check protection - if (not bypass_protection and not minetest.is_protected(pos, self.npc_name)) - or bypass_protection == true then - -- Take from inventory if necessary - local place_item = false - if source == npc.programs.const.place_src.take_from_inventory then - if npc.take_item_from_inventory(self, node, 1) then - place_item = true - end - elseif source == npc.programs.const.place_src.take_from_inventory_forced then - npc.take_item_from_inventory(self, node, 1) - place_item = true - elseif source == npc.programs.const.place_src.force_place then - place_item = true - end - -- Place node - if place_item == true then - -- Set mine animation - self.object:set_animation({ - x = npc.ANIMATION_MINE_START, - y = npc.ANIMATION_MINE_END}, - self.animation.speed_normal, 0) - -- Place node - minetest.set_node(pos, {name=node}) - -- Play place sound - if play_sound == true then - if minetest.registered_nodes[node].sounds then - minetest.sound_play( - minetest.registered_nodes[node].sounds.place, - { - max_hear_distance = 10, - object = self.object - } - ) - end - end - end - end - end -end) - --- The following instruction simulates what a player does when it punches something. --- In this case, we have two possibilities: --- - Punch an object (entity or player), --- - Punch a node --- If directed against a node, and the node has no special on_punch() callback, --- the `advanced_npc:dig` instruction will be executed --- Arguments: --- - `pointed_thing`, for consistency, this is as explained in the `lua_api.txt`, --- but without the `{type="nothing"} support. It supports the other two definitions. --- - `wield_item`, which is an itemstring, that represents the item the NPC --- is wielding at the time of punching. -npc.programs.instr.register("advanced_npc:punch", function(self, args) - local pointed_thing = self.pointed_thing - local target_type = pointed_thing.type - local target_pos = pointed_thing.above - local target_obj = pointed_thing.ref - local wielded_item = args.wielded_item - local time_from_last_punch = minetest.get_gametime() - self.npc_state.punch.last_punch_time - - -- Set time from last punch - self.npc_state.punch.last_punch_time = minetest.get_gametime() - - -- If given, enable wielded item - if wielded_item then - self:set_wielded_item(wielded_item) - end - - if target_type == "object" and target_obj then - -- Call obj's punch() - target_obj:punch(self, time_from_last_punch, self.object:getyaw()) - elseif target_type == "node" and target_pos then - local node = minetest.get_node(target_pos) - local node_def = minetest.registered_nodes[node.name] - if node_def and node_def.on_punch then - -- Call the node's on_punch - node_def.on_punch(target_pos, node, self, {type="node", above=target_pos, below=target_pos}) - else - -- Execute the dig instrcution - npc.programs.instr.execute(self, npc.programs.instr.default.DIG, { - pos = target_pos, - bypass_protection = false, - add_to_inventory = true - }) - end - end -end) - --- The following instruction simulates what a player does when it rightclicks something. --- In this case, we have two possibilities: --- - Right-click an object (entity or player), --- - Right-click a node --- Arguments: --- - `pointed_thing`, for consistency, this is as explained in the `lua_api.txt`, --- but without the `{type="nothing"} support. It supports the other two definitions. --- - `wield_item`, which is an itemstring, that represents the item the NPC --- is wielding at the time of punching. -npc.programs.instr.register("advanced_npc:rightclick", function(self, args) - local pointed_thing = self.pointed_thing - local target_type = pointed_thing.type - local target_pos = pointed_thing.above - local target_obj = pointed_thing.ref - local wielded_item = args.wielded_item - - -- If given, enable wielded item - if wielded_item then - self:set_wielded_item(wielded_item) - end - - if target_type == "object" and target_obj then - -- Call obj's right-click() - target_obj:right_click(self) - elseif target_type == "node" and target_pos then - local node = minetest.get_node(target_pos) - local node_def = minetest.registered_nodes[node.name] - if node_def and node_def.on_rightclick then - -- Call the node's on_punch - node_def.on_rightclick(target_pos, node, self, self.object:get_wielded_item(), pointed_thing) - end - end -end) - --- This instruction allows the NPC to craft a certain item if it has --- the required items on its inventory. If the "force_craft" option is --- used, the NPC will get the item regardless if it has the required items --- or not -npc.programs.instr.register("advanced_npc:craft", function(self, args) - local item = args.item - local source = args.source - - -- Check if source is force-craft, if it is, just add the item to inventory - if source == npc.programs.const.craft_src.force_craft then - -- Add item to inventory - npc.add_item_to_inventory_itemstring(self, item) - return - end - - local recipes = minetest.get_all_craftt_recipes(item) - -- Iterate through recipes, only care about those that are "normal", - -- we don't care about cooking or fuel recipes. - -- Check if required items are present - if recipes then - for i = 1, #recipes do - if recipe.method == "normal" then - local missing_items = {} - if recipe.items then - -- Check how many items we have and which we don't - for i = 1, #recipe.items do - if npc.inventory_contains(self, recipe.items[i]) == nil then - missing_items[#missing_items + 1] = recipe.items[i] - end - end - -- Now, check the source for items - local craftable = false - if source == npc.programs.const.craft_src.take_from_inventory then - -- Check if we have all - if next(missing_items) == nil then - craftable = true - end - elseif source == npc.programs.const.craft_src.take_from_inventory_forced then - -- Check if we have missing items - if next(missing_items) ~= nil then - -- Add all missing items - for j = 1, #missing_items do - npc.add_item_to_inventory(self, missing_items[j], 1) - end - craftable = true - end - end - -- Check if item is craftable - if craftable == true then - -- We have all items, craft - -- First, remove all items from NPC inventory - for j = 1, #recipe.items do - npc.take_item_from_inventory(self, recipe.items[j], 1) - end - -- Then add "crafted" element - npc.add_item_to_inventory_itemstring(self, item) - return true - end - return false - end - end - end - else - npc.log("WARNING", "[instr][craft] Found no recipes for item: "..dump(args.item)) - end -end) - --- This command is to rotate a mob to a specifc direction. Currently, the code --- contains also for diagonals, but remaining in the orthogonal domain is preferrable. -npc.programs.instr.register("advanced_npc:rotate", function(self, args) - local dir = args.dir - local yaw = args.yaw or 0 - local start_pos = args.start_pos - local end_pos = args.end_pos - -- Calculate dir if positions are given - if start_pos and end_pos and not dir then - dir = npc.programs.helper.get_direction(start_pos, end_pos) - end - -- Only yaw was given - if yaw and not dir and not start_pos and not end_pos then - if (yaw ~= yaw) then yaw = 0 end - --if type(yaw) == "table" then yaw = 0 end - self.object:setyaw(yaw) - return - end - - self.rotate = 0 - if dir == npc.direction.north then - yaw = 0 - elseif dir == npc.direction.north_east then - yaw = (7 * math.pi) / 4 - elseif dir == npc.direction.east then - yaw = (3 * math.pi) / 2 - elseif dir == npc.direction.south_east then - yaw = (5 * math.pi) / 4 - elseif dir == npc.direction.south then - yaw = math.pi - elseif dir == npc.direction.south_west then - yaw = (3 * math.pi) / 4 - elseif dir == npc.direction.west then - yaw = math.pi / 2 - elseif dir == npc.direction.north_west then - yaw = math.pi / 4 - end - if (yaw ~= yaw) then yaw = 0 end - self.object:setyaw(yaw) -end) - --- This function will make the NPC walk one step on a --- specifc direction. One step means one node. It returns --- true if it can move on that direction, and false if there is an obstacle -npc.programs.instr.register("advanced_npc:walk_step", function(self, args) - local dir = args.dir - local yaw = args.yaw or 0 - local step_into_air_only = args.step_into_air_only - local speed = args.speed - local target_pos = args.target_pos - local start_pos = args.start_pos - local vel = {} - - -- Set default node per seconds - if speed == nil then - speed = npc.programs.const.speeds.one_nps_speed - end - - -- Only yaw was given, purely rotate and walk in that dir - if yaw and not dir then - vel = vector.multiply(vector.normalize(minetest.yaw_to_dir(yaw)), speed) - else - -- Check if dir should be random - if dir == "random_all" or dir == "random" then - dir = npc.programs.helper.random_dir(start_pos, speed, 0, 7) - end - if dir == "random_orthogonal" then - dir = npc.programs.helper.random_dir(start_pos, speed, 0, 3) - --minetest.log("Returned: "..dump(dir)) - end - - if dir == npc.direction.north then - vel = {x=0, y=0, z=speed} - elseif dir == npc.direction.north_east then - vel = {x=speed, y=0, z=speed} - elseif dir == npc.direction.east then - vel = {x=speed, y=0, z=0} - elseif dir == npc.direction.south_east then - vel = {x=speed, y=0, z=-speed} - elseif dir == npc.direction.south then - vel = {x=0, y=0, z=-speed} - elseif dir == npc.direction.south_west then - vel = {x=-speed, y=0, z=-speed} - elseif dir == npc.direction.west then - vel = {x=-speed, y=0, z=0} - elseif dir == npc.direction.north_west then - vel = {x=-speed, y=0, z=speed } - else - -- No direction provided or NPC is trapped, center NPC position - -- and return - -- local npc_pos = self.object:getpos() - -- local proper_pos = {x=math.floor(npc_pos.x), y=npc_pos.y, z=math.floor(npc_pos.z)} - -- self.object:moveto(proper_pos) - return - end - end - - -- If there is a target position to reach, set it and set walking to true - if target_pos ~= nil then - self.npc_state.movement.walking.target_pos = target_pos - -- Set is_walking = true - npc.set_movement_state(self, {is_walking = true}) - end - - -- Rotate NPC - npc.programs.instr.execute(self, npc.programs.instr.default.ROTATE, {dir=dir, yaw=yaw}) - -- Set velocity so that NPC walks - self.object:setvelocity(vel) - -- Set walk animation - self.object:set_animation({ - x = npc.ANIMATION_WALK_START, - y = npc.ANIMATION_WALK_END}, - self.animation.speed_normal, 0) -end) - --- This command makes the NPC stand and remain like that -npc.programs.instr.register("advanced_npc:stand", function(self, args) - local pos = args.pos - local dir = args.dir - local yaw = args.yaw - -- Set is_walking = false - npc.set_movement_state(self, {is_idle = true}) - -- Stop NPC - self.object:setvelocity({x=0, y=0, z=0}) - -- If position given, set to that position - if pos ~= nil then - self.object:moveto(pos) - end - -- If dir given, set to that dir - if dir ~= nil or yaw ~= nil then - npc.programs.instr.execute(self, npc.programs.instr.default.ROTATE, {dir=dir, yaw=yaw}) - end - -- Set stand animation - self.object:set_animation({ - x = npc.ANIMATION_STAND_START, - y = npc.ANIMATION_STAND_END}, - self.animation.speed_normal, 0) -end) - --- This command makes the NPC sit on the node where it is -npc.programs.instr.register("advanced_npc:sit", function(self, args) - local pos = args.pos - local dir = args.dir - -- Set movement state - npc.set_movement_state(self, {is_idle = true, is_sitting = true}) - -- Stop NPC - self.object:setvelocity({x=0, y=0, z=0}) - -- If position given, set to that position - if pos ~= nil then - self.object:moveto(pos) - end - -- If dir given, set to that dir - if dir ~= nil then - npc.programs.instr.execute(self, npc.programs.instr.default.ROTATE, {dir=dir}) - end - -- Set sit animation - self.object:set_animation({ - x = npc.ANIMATION_SIT_START, - y = npc.ANIMATION_SIT_END}, - self.animation.speed_normal, 0) -end) - --- This command makes the NPC lay on the node where it is -npc.programs.instr.register("advanced_npc:lay", function(self, args) - local pos = args.pos - -- Set movement state - npc.set_movement_state(self, {is_idle = true, is_laying = true}) - -- Stop NPC - self.object:setvelocity({x=0, y=0, z=0}) - -- If position give, set to that position - if pos ~= nil then - self.object:moveto(pos) - end - -- Set sit animation - self.object:set_animation({ - x = npc.ANIMATION_LAY_START, - y = npc.ANIMATION_LAY_END}, - self.animation.speed_normal, 0) -end) - --- Inventory interaction instructions -- --- This function is a convenience function to make it easy to put --- and get items from another inventory (be it a player inv or --- a node inv) -npc.programs.instr.register("advanced_npc:external_inventory_put", function(self, args) - local player = args.player - local pos = args.pos - local inv_list = args.inv_list - local item_name = args.item_name - local count = args.count - local is_furnace = args.is_furnace - local inv - if player ~= nil then - inv = minetest.get_inventory({type="player", name=player}) - else - inv = minetest.get_inventory({type="node", pos=pos}) - end - - -- Create ItemStack to put on external inventory - local item = ItemStack(item_name.." "..count) - -- Check if there is enough room to add the item on external invenotry - if inv:room_for_item(inv_list, item) then - -- Take item from NPC's inventory - if npc.take_item_from_inventory_itemstring(self, item) then - -- NPC doesn't have item and/or specified quantity - return false - end - -- Add items to external inventory - inv:add_item(inv_list, item) - - -- If this is a furnace, start furnace timer - if is_furnace == true then - minetest.get_node_timer(pos):start(1.0) - end - - return true - end - -- Not able to put on external inventory - return false -end) - -npc.programs.instr.register("advanced_npc:external_inventory_take", function(self, args) - local player = args.player - local pos = args.pos - local inv_list = args.inv_list - local item_name = args.item_name - local count = args.count - local inv - if player ~= nil then - inv = minetest.get_inventory({type="player", name=player}) - else - inv = minetest.get_inventory({type="node", pos=pos}) - end - -- Create ItemStack to take from external inventory - local item = ItemStack(item_name.." "..count) - -- Check if there is enough of the item to take - if inv:contains_item(inv_list, item) then - -- Add item to NPC's inventory - npc.add_item_to_inventory_itemstring(self, item) - -- Add items to external inventory - inv:remove_item(inv_list, item) - return true - end - -- Not able to put on external inventory - return false -end) - -npc.programs.instr.register("advanced_npc:external_inventory_contains", function(self, args) - local player = args.player - local pos = args.pos - local inv_list = args.inv_list - local item_name = args.item_name - local count = args.count - local inv - if player ~= nil then - inv = minetest.get_inventory({type="player", name=player}) - else - inv = minetest.get_inventory({type="node", pos=pos}) - end - - -- Create ItemStack for checking the external inventory - local item = ItemStack(item_name.." "..count) - -- Check if inventory contains item - return inv:contains_item(inv_list, item) -end) - --- This function is used to open or close openable nodes. --- Currently supported openable nodes are: any doors using the --- default doors API, and the cottages mod gates and doors. -npc.programs.instr.register("advanced_npc:use_openable_node", function(self, args) - local pos = args.pos - local command = args.command - local dir = args.dir - local node = minetest.get_node(pos) - local state = npc.programs.helper.get_openable_node_state(node, pos, dir) - - -- Emulate the NPC being a player - local clicker = self - clicker.is_player = function() return true end - clicker.get_player_name = function(self) return self.npc_id end - if command ~= state then - minetest.registered_nodes[node.name].on_rightclick(pos, node, clicker, nil, nil) - end -end) - --- Internal NPC properties --- These instructions are mostly syntactic sugar for doing certain operations. -npc.programs.instr.register("advanced_npc:trade:change_trader_status", function(self, args) - -- Get status from args - local status = args.status - -- Set status to NPC - npc.set_trading_status(self, status) -end) - -npc.programs.instr.register("advanced_npc:trade:set_trade_list", function(self, args) - -- Insert items - for i = 1, #args.items do - -- Insert entry into trade list - self.trader_data.trade_list[args.items[i].name] = { - max_item_buy_count = args.items[i].buy, - max_item_sell_count = args.items[i].sell, - amount_to_keep = args.items[i].keep - } - end -end) - --- Accepts itemstring -npc.programs.instr.register("advanced_npc:inventory_put", function(self, args) - local itemstring = args.itemstring - -- Add item - npc.add_item_to_inventory_itemstring(self, itemstring) -end) - -npc.programs.instr.register("advanced_npc:inventory_put_multiple", function(self, args) - local itemlist = args.itemlist - for i = 1, #itemlist do - local itemlist_entry = itemlist[i] - local current_itemstring = itemlist[i].name - if itemlist_entry.random == true then - current_itemstring = current_itemstring - .." "..dump(math.random(itemlist_entry.min, itemlist_entry.max)) - else - current_itemstring = current_itemstring.." "..tostring(itemlist_entry.count) - end - -- Add item to inventory - npc.add_item_to_inventory_itemstring(self, current_itemstring) - end -end) - -npc.programs.instr.register("advanced_npc:inventory_take", function(self, args) - local itemstring = args.itemstring - -- Add item - npc.take_item_from_inventory_itemstring(self, itemstring) -end) diff --git a/mods/advanced_npc/executable/locations.lua b/mods/advanced_npc/executable/locations.lua deleted file mode 100644 index 9605a14..0000000 --- a/mods/advanced_npc/executable/locations.lua +++ /dev/null @@ -1,819 +0,0 @@ --- Places code for Advanced NPC by Zorman2000 ---------------------------------------------------------------------------------------- --- Places functionality ---------------------------------------------------------------------------------------- --- In addition, the NPCs need to know where some places are, and know --- where there are nodes they can use. For example, they need to know where the --- chest they use is located, both to walk to it and to use it. They also need --- to know where the farm they work is located, or where the bed they sleep is. --- Other mods have to be supported for this to work correctly, as there are --- many sitting nodes, many beds, many tables, chests, etc. For now, by default, --- support for default MTG games and cottages mod is going to be provided. - --- Public API -npc.locations = {} - --- Function to register nodes --- Categories are utilized to categorize nodes by type, example beds, --- furnaces, etc. Sub-categories give more particular categorization to --- beds in terms of how the NPC uses them -function npc.locations.register_node(node_name, category, sub_category) - -- Check category, if it doesn't exists, create it - if npc.locations.data.categories[category] == nil then - -- Create category name - npc.locations.data.categories[category] = category - npc.locations.data[category] = {} - end - -- Check sub-category and create it if necessary - if npc.locations.data[category][sub_category] == nil then - -- Create sub category - npc.locations.data[category][sub_category] = sub_category - end - -- Add node - npc.locations.nodes[category][#npc.locations.nodes[category] + 1] = node_name -end - --- Default set of registered nodes -npc.locations.nodes = { - bed = { - "beds:bed_bottom", - "beds:fancy_bed_bottom", - "cottages:bed_foot", - "cottages:straw_mat", - "cottages:sleeping_mat" - }, - sittable = { - "cottages:bench", - -- currently commented out since some npcs - -- were sitting at stairs that are actually staircases - -- TODO: register other stair types - --"stairs:stair_wood" - }, - storage = { - "default:chest", - "default:chest_locked", - "cottages:shelf" - }, - furnace = { - "default:furnace", - "default:furnace_active" - }, - openable = { - -- TODO: register fences - "doors:door_glass_a", - "doors:door_glass_b", - "doors:door_obsidian_a", - "doors:door_obsidian_b", - "doors:door_steel_a", - "doors:door_steel_b", - "doors:door_wood_a", - "doors:door_wood_b", - "cottages:gate_open", - "cottages:gate_closed", - "cottages:half_door" - }, - plotmarker = { - "mg_villages:plotmarker", - "advanced_npc:plotmarker" - }, - workplace = { - -- TODO: do we have an advanced_npc workplace? - "mg_villages:mob_workplace_marker", - "advanced_npc:workplace_marker" - } -} - --- Spawner function assign_places can also take a generic categories, and specially --- process the hardcoded ones. -npc.locations.data = { - categories = { - bed = "bed", - sittable = "sittable", - furnace = "furnace", - storage = "storage", - openable = "openable", - schedule = "schedule", - calculated = "calculated", - workplace = "workplace", - other = "other" - }, - bed = { - primary = "bed_primary" - }, - sittable = { - primary = "sit_primary", - shared = "sit_shared" - }, - furnace = { - primary = "furnace_primary", - shared = "furnace_shared" - }, - storage = { - primary = "storage_primary", - shared = "storage_shared" - }, - openable = { - home_entrance_door = "home_entrance_door", - room_entrance_door = "room_entrance_door" - }, - schedule = { - target = "schedule_target_pos" - }, - calculated = { - target = "calculated_target_pos" - }, - workplace = { - primary = "workplace_primary", - tool = "workplace_tool" - }, - other = { - home_plotmarker = "home_plotmarker", - home_inside = "home_inside", - home_outside = "home_outside", - room_inside = "room_inside", - room_outside = "room_outside" - }, - is_primary = function(place_type) - local p1,p2 = string.find(place_type, "primary") - return p1 ~= nil - end, - -- Only works for place types where there is a "primary" and a "shared" - get_alternative = function(place_category, place_type) - local result = {} - local place_types = npc.locations.data[place_category] - -- Determine search type - local search_shared = false - if npc.locations.data.is_primary(place_type) then - search_shared = true - end - for key,place_type in pairs(place_types) do - if search_shared == true then - if npc.locations.data.is_primary(place_type) == false then - return place_type - end - else - if npc.locations.data.is_primary(place_type) == true then - return place_type - end - end - end - end -} - -npc.locations.USE_STATE = { - USED = "true", - NOT_USED = "false" -} - -function npc.locations.add_shared(self, place_name, place_type, pos, access_node) - -- Set metadata of node - local meta = minetest.get_meta(pos) - if not meta:get_string("advanced_npc:used") then - meta:set_string("advanced_npc:used", npc.locations.USE_STATE.NOT_USED) - end - meta:set_string("advanced_npc:owner", "") - -- This *should* avoid lags - meta:mark_as_private({"advanced_npc:used", "advanced_npc:owner"}) - self.places_map[place_name] = {type=place_type, pos=pos, access_node=access_node or pos, status="shared"} -end - -function npc.locations.add_owned(self, place_name, place_type, pos, access_node) - -- Set metadata of node - local meta = minetest.get_meta(pos) - if not meta:get_string("advanced_npc:used") then - meta:set_string("advanced_npc:used", npc.locations.USE_STATE.NOT_USED) - end - meta:set_string("advanced_npc:owner", self.npc_id) - -- This *should* avoid lags - meta:mark_as_private({"advanced_npc:used", "advanced_npc:owner"}) - self.places_map[place_name] = {type=place_type, pos=pos, access_node=access_node or pos, status="owned"} -end - -function npc.locations.add_owned_accessible_place(self, nodes, place_type, walkables) - for i = 1, #nodes do - -- Check if node has owner - local owner = minetest.get_meta(nodes[i].node_pos):get_string("advanced_npc:owner") - --minetest.log("Condition: "..dump(owner == "")) - if owner == "" then - -- If node has no owner, check if it is accessible - local empty_nodes = npc.locations.find_orthogonal_accessible_node( - nodes[i].node_pos, nil, 0, true, walkables) - -- Check if node is accessible - if #empty_nodes > 0 then - -- Set owner to this NPC - nodes[i].owner = self.npc_id - -- Assign node to NPC - npc.locations.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)) - -- Return node - return nodes[i] - end - end - end -end - --- Override flag allows to overwrite a place in the places_map. --- The only valid use right now is for schedules - don't use this --- anywhere else unless you have a position that changes over time. -function npc.locations.add_shared_accessible_place(self, nodes, place_type, override, walkables) - - if not override or (override and override == false) then - for i = 1, #nodes do - -- Check if not adding same owned place - if nodes[i].owner ~= self.npc_id then - -- Check if it is accessible - local empty_nodes = npc.locations.find_orthogonal_accessible_node( - nodes[i].node_pos, nil, 0, true, walkables) - -- Check if node is accessible - if #empty_nodes > 0 then - -- Assign node to NPC - npc.locations.add_shared(self, place_type..dump(i), - place_type, nodes[i].node_pos, empty_nodes[1].pos) - else - npc.log("WARNING", "Found non-accessible place at pos: "..minetest.pos_to_string(nodes[i].node_pos)) - end - end - end - elseif override == true then - -- Note: Nodes is only *one* node in case override = true - -- Check if it is accessible - local empty_nodes = npc.locations.find_orthogonal_accessible_node( - nodes.node_pos, nil, 0, true, walkables) - -- Check if node is accessible - if #empty_nodes > 0 then - -- Nodes is only one node - npc.locations.add_shared(self, place_type, place_type, nodes.node_pos, empty_nodes[1].pos) - end - end -end - -function npc.locations.mark_place_used(pos, value) - local meta = minetest.get_meta(pos) - local used = meta:get_string("advanced_npc:used") - if value == used then - npc.log("WARNING", "Attempted to set 'used' property of node at " - ..minetest.pos_to_string(pos).." to the same value: '"..dump(value).."'") - return false - else - meta:set_string("advanced_npc:used", value) - npc.log("DEBUG", "'Used' value at pos "..minetest.pos_to_string(pos)..": "..dump(meta:get_string("advanced_npc:used"))) - return true - end -end - --- This function is to find an alternative place if the original is --- not usable. If the original place is a "primary" place, it will --- try to find a "shared" place. If it is a "shared" place, it will try --- to find a "primary" place. If none is found, it retuns the given type. -function npc.locations.find_unused_place(self, place_category, place_type, original_place) - local result = {} - -- Check if node is being used - local meta = minetest.get_meta(original_place.pos) - local used = meta:get_string("advanced_npc:used") - if used == npc.locations.USE_STATE.USED then - -- Node is being used, try to find alternative - local alternative_place_type = npc.locations.data.get_alternative(place_category, place_type) - --minetest.log("Alternative place type: "..dump(alternative_place_type)) - local alternative_places = npc.locations.get_by_type(self, alternative_place_type) - --minetest.log("Alternatives: "..dump(alternative_places)) - for i = 1, #alternative_places do - meta = minetest.get_meta(alternative_places[i].pos) - local used = meta:get_string("advanced_npc:used") - if used == npc.locations.USE_STATE.NOT_USED then - return alternative_places[i] - end - end - else - result = original_place - end - return result -end - -function npc.locations.get_by_type(self, place_type, exact_match) - local result = {} - for _, place_entry in pairs(self.places_map) do - --minetest.log("Looking for: "..dump(place_type)..", in: "..dump(place_entry.type)) - local s, _ = string.find(place_entry.type, place_type) - if s ~= nil then - --minetest.log("Found: "..dump(place_entry)) - result[#result + 1] = place_entry - end - end - --minetest.log("Returning: "..dump(result)) - return result -end - ---------------------------------------------------------------------------------------- --- Utility functions ---------------------------------------------------------------------------------------- --- The following are utility functions that are used to operate on nodes for --- specific conditions - --- This func -function npc.locations.is_walkable(node_name) - return node_name == "air" - -- Any walkable node except fences - or (minetest.registered_nodes[node_name].walkable == false - and minetest.registered_nodes[node_name].groups.fence ~= 1) - -- Farming plants - or minetest.registered_nodes[node_name].groups.plant == 1 -end - --- This function searches on a squared are of the given radius --- for nodes of the given type. The type should be npc.locations.nodes -function npc.locations.find_node_nearby(pos, type, radius, vertical_range_limit) - local y_offset = radius - if vertical_range_limit then - y_offset = vertical_range_limit - end - -- Determine area points - local start_pos = {x=pos.x - radius, y=pos.y - y_offset, z=pos.z - radius} - local end_pos = {x=pos.x + radius, y=pos.y + y_offset, z=pos.z + radius} - -- Get nodes - local nodes = minetest.find_nodes_in_area(start_pos, end_pos, type) - - return nodes -end - --- TODO: This function can be improved to support a radius greater than 1. -function npc.locations.find_node_orthogonally(pos, nodes, y_adjustment) - -- Call the more generic function with appropriate params - return npc.locations.find_orthogonal_accessible_node(pos, nodes, y_adjustment, nil, nil) -end - --- TODO: This function can be improved to support a radius greater than 1. -function npc.locations.find_orthogonal_accessible_node(pos, nodes, y_adjustment, include_walkables, extra_walkables) - -- Calculate orthogonal points - local points = {} - points[#points + 1] = {x=pos.x+1,y=pos.y+y_adjustment,z=pos.z} - points[#points + 1] = {x=pos.x-1,y=pos.y+y_adjustment,z=pos.z} - points[#points + 1] = {x=pos.x,y=pos.y+y_adjustment,z=pos.z+1} - points[#points + 1] = {x=pos.x,y=pos.y+y_adjustment,z=pos.z-1} - local result = {} - for _,point in pairs(points) do - local node = minetest.get_node(point) - -- Search for specific node names - if nodes then - for _,node_name in pairs(nodes) do - if node.name == node_name then - table.insert(result, {name=node.name, pos=point, param2=node.param2}) - end - end - else - -- Search for air, walkable nodes, or any node availble in the extra_walkables array - if node.name == "air" - or (include_walkables == true - and minetest.registered_nodes[node.name].walkable == false - and minetest.registered_nodes[node.name].groups.fence ~= 1) - or (extra_walkables and npc.utils.array_contains(extra_walkables, node.name)) then - result[#result + 1] = {name=node.name, pos=point, param2=node.param2} - end - end - end - return result -end - - --- Wrapper around minetest.find_nodes_in_area() --- TODO: Verify if this wrapper is actually needed -function npc.locations.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.locations.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 - result[#result + 1] = node - end - end - return result -end - --- Creates an array of {pos=, owner=''} for managing --- which NPC owns what -function npc.locations.get_nodes_by_type(start_pos, end_pos, type) - local result = {} - local nodes = npc.locations.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"] = '' - result[#result + 1] = entry - end - return result -end - --- Function to get mg_villages building data -if minetest.get_modpath("mg_villages") ~= nil then - function npc.locations.get_mg_villages_building_data(pos) - local result = { - village_id = "", - plot_nr = -1, - building_data = {}, - building_type = "", - } - local meta = minetest.get_meta(pos) - result.plot_nr = meta:get_int("plot_nr") - result.village_id = meta:get_string("village_id") - - -- Get building data - if mg_villages.get_plot_and_building_data then - local all_data = mg_villages.get_plot_and_building_data(result.village_id, result.plot_nr) - if all_data then - result.building_data = all_data.building_data - result.building_type = result.building_data.typ - result["building_pos_data"] = all_data.bpos - end - else - -- Following line from mg_villages mod, protection.lua - local btype = mg_villages.all_villages[result.village_id].to_add_data.bpos[result.plot_nr].btype - result.building_data = mg_villages.BUILDINGS[btype] - result.building_type = result.building_data.typ - end - return result - end - - -- Pre-requisite: only run this function on mg_villages:plotmarker that has been adapted - -- by using spawner.adapt_mg_villages_plotmarker - function npc.locations.get_all_workplaces_from_plotmarker(pos) - local result = {} - local meta = minetest.get_meta(pos) - local pos_data = minetest.deserialize(meta:get_string("building_pos_data")) - if pos_data then - local workplaces = pos_data.workplaces - if workplaces then - -- Insert all workplaces in this plotmarker - for i = 1, #workplaces do - table.insert(result, - { - workplace=workplaces[i], - building_type=meta:get_string("building_type"), - surrounding_workplace = false, - node_pos= { - x=workplaces[i].x, - y=workplaces[i].y, - z=workplaces[i].z - } - }) - end - end - end - -- Check the other plotmarkers as well - local nearby_plotmarkers = minetest.deserialize(meta:get_string("nearby_plotmarkers")) - npc.log("DEBUG", "Nearby plotmarkers: "..dump(nearby_plotmarkers)) - if nearby_plotmarkers then - for i = 1, #nearby_plotmarkers do - if nearby_plotmarkers[i].workplaces then - -- Insert all workplaces in this plotmarker - for j = 1, #nearby_plotmarkers[i].workplaces do - --minetest.log("Nearby plotmarker workplace #"..dump(j)..": "..dump(nearby_plotmarkers[i].workplaces[j])) - table.insert(result, { - workplace=nearby_plotmarkers[i].workplaces[j], - building_type = nearby_plotmarkers[i].building_type, - surrounding_workplace = true, - node_pos = { - x=nearby_plotmarkers[i].workplaces[j].x, - y=nearby_plotmarkers[i].workplaces[j].y, - z=nearby_plotmarkers[i].workplaces[j].z - } - }) - end - end - end - end - return result - end -end - --- This function will search for nodes of type plotmarker and, --- in case of being an mg_villages plotmarker, it will fetch building --- information and include in result. -function npc.locations.find_plotmarkers(pos, radius, exclude_current_pos) - local result = {} - local start_pos = {x=pos.x - radius, y=pos.y - 1, z=pos.z - radius} - local end_pos = {x=pos.x + radius, y=pos.y + 1, z=pos.z + radius} - local nodes = minetest.find_nodes_in_area(start_pos, end_pos, - npc.locations.nodes.plotmarker) - npc.log("INFO", "Found "..dump(#nodes).." plotmarkers") - -- Scan nodes - for i = 1, #nodes do - -- Check if current plotmarker is to be excluded from the list - local exclude = false - if exclude_current_pos then - if pos.x == nodes[i].x and pos.y == nodes[i].y and pos.z == nodes[i].z then - exclude = true - end - end - -- Analyze and include node if not excluded - if not exclude then - local node = minetest.get_node(nodes[i]) - local def = {} - def["pos"] = nodes[i] - def["name"] = node.name - if node.name == "mg_villages:plotmarker" and npc.locations.get_mg_villages_building_data then - local data = npc.locations.get_mg_villages_building_data(nodes[i]) - def["plot_nr"] = data.plot_nr - def["village_id"] = data.village_id - --def["building_data"] = data.building_data - def["building_type"] = data.building_type - npc.log("INFO", "["..dump(data.building_type).."]") - if data.building_pos_data then - def["building_pos_data"] = data.building_pos_data - if next(data.building_pos_data.workplaces) ~= nil then - def["workplaces"] = data.building_pos_data.workplaces - end - end - if data.workplaces and next(data.workplaces) ~= nil then - def["workplaces"] = data.workplaces - end - end - -- Add building - --minetest.log("Adding building: "..dump(def)) - table.insert(result, def) - end - 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.locations.scan_area_for_usable_nodes(pos1, pos2) - minetest.log("Bed Nodes: "..dump(npc.locations.nodes.bed)) - local result = { - bed_type = {}, - sittable_type = {}, - furnace_type = {}, - storage_type = {}, - openable_type = {}, - workplace_type = {} - } - local start_pos, end_pos = vector.sort(pos1, pos2) - - result.bed_type = npc.locations.get_nodes_by_type(start_pos, end_pos, npc.locations.nodes.bed) - result.sittable_type = npc.locations.get_nodes_by_type(start_pos, end_pos, npc.locations.nodes.sittable) - result.furnace_type = npc.locations.get_nodes_by_type(start_pos, end_pos, npc.locations.nodes.furnace) - result.storage_type = npc.locations.get_nodes_by_type(start_pos, end_pos, npc.locations.nodes.storage) - result.openable_type = npc.locations.get_nodes_by_type(start_pos, end_pos, npc.locations.nodes.openable) - - -- Find workplace nodes: if mg_villages:plotmarker is given as start pos, take it from there. - -- If not, search for them. - local node = minetest.get_node(pos1) - if node.name == "mg_villages:plotmarker" then - if npc.locations.get_all_workplaces_from_plotmarker then - result.workplace_type = npc.locations.get_all_workplaces_from_plotmarker(pos1) - end - else - -- Just search for workplace nodes - -- The search radius is increased by 2 - result.workplace_type = npc.locations.get_nodes_by_type( - {x=start_pos.x-20, y=start_pos.y, z=start_pos.z-20}, - {x=end_pos.x+20, y=end_pos.y, z=end_pos.z+20}, - npc.locations.nodes.workplace) - -- Find out building type and add it to the result - for i = 1, #result.workplace_type do - local meta = minetest.get_meta(result.workplace_type[i].node_pos) - local building_type = meta:get_string("building_type") or "none" - local surrounding_workplace = meta:get_string("surrounding_workplace") or "false" - result.workplace_type[i]["surrounding_workplace"] = minetest.is_yes(surrounding_workplace) - result.workplace_type[i]["building_type"] = building_type - end - end - - return result -end - --- Helper function to clear metadata in an array of nodes --- Metadata that will be cleared is: --- advanced_npc:used --- advanced_npc:owner -local function clear_metadata(nodes) - local c = 0 - for i = 1, #nodes do - local meta = minetest.get_meta(nodes[i].node_pos) - meta:set_string("advanced_npc:owner", "") - meta:set_string("advanced_npc:used", npc.locations.USE_STATE.NOT_USED) - c = c + 1 - end - return c -end - -function npc.locations.clear_metadata_usable_nodes_in_area(node_data) - local count = 0 - if node_data then - count = count + clear_metadata(node_data.bed_type) - count = count + clear_metadata(node_data.sittable_type) - count = count + clear_metadata(node_data.furnace_type) - count = count + clear_metadata(node_data.storage_type) - count = count + clear_metadata(node_data.openable_type) - -- Clear workplace nodes - if node_data.workplace_type then - for i = 1, #node_data.workplace_type do - local meta = minetest.get_meta(node_data.workplace_type[i].node_pos) - meta:set_string("work_data", nil) - count = count + 1 - end - end - end - return count -end - -local function get_decorated_path(start_pos, end_pos) - local entity = {} - entity.collisionbox = {-0.20,-1.0,-0.20, 0.20,0.8,0.20} - local path = npc.pathfinder.find_path(start_pos, end_pos, entity, true) - return path -end - -function npc.locations.find_building_entrance(bed_nodes, marker_pos) - -- Exit if no bed nodes given - if bed_nodes == nil or (bed_nodes and #bed_nodes == 0) then - return - end - - -- Iterate through bed nodes to try and find an entrance - for i = 1, #bed_nodes do - - -- Initialize positions - local start_pos = bed_nodes[i].node_pos - local end_pos = {x=marker_pos.x, y=marker_pos.y, z=marker_pos.z} - npc.log("INFO", "Trying to find path from "..minetest.pos_to_string(start_pos).." to "..minetest.pos_to_string(end_pos)) - - -- Find path from the bed node to the plotmarker - local decorated_path = get_decorated_path(start_pos, end_pos) - - -- Find building entrance, traverse path backwards and return first node that is openable - if decorated_path then - for j = #decorated_path, 1, -1 do - if decorated_path[j].type == npc.pathfinder.node_types.openable then - local result = { - door = vector.round(decorated_path[j].pos), - inside = vector.round(decorated_path[j-1].pos), - outside = vector.round(decorated_path[j+1].pos) - } - return result - end - end - end - - npc.log("INFO", "Attempt "..dump(i).." of "..dump(#bed_nodes).." of finding entrance from bed failed.") - end -end - -function npc.locations.find_bedroom_entrance(bed_node, marker_pos) - local start_pos = bed_node.node_pos - local end_pos = {x=marker_pos.x, y=marker_pos.y, z=marker_pos.z } - -- Find path from the bed node to the plotmarker - local decorated_path = get_decorated_path(start_pos, end_pos) - --minetest.log("Decorated path: "..dump(decorated_path)) - -- Find building entrance, traverse path forward and return first node that is openable - for i = 1, #decorated_path do - if decorated_path[i].type == npc.pathfinder.node_types.openable then - return { - door = vector.floor(decorated_path[i].pos), - inside = vector.floor(decorated_path[i-1].pos), - outside = vector.floor(decorated_path[i+1].pos) - } - end - end -end - - --------------------------------------------------------------------- --- WARNING! Code below here DOESN'T WORKS correctly... don't use! -- --------------------------------------------------------------------- - --- Specialized function to find all sittable nodes supported by the --- mod, namely default stairs and cottages' benches. Since not all --- stairs nodes placed aren't meant to simulate benches, this function --- is necessary in order to find stairs that are meant to be benches. -function npc.locations.find_sittable_nodes_nearby(pos, radius) - local result = {} - -- Try to find sittable nodes - local nodes = npc.locations.find_node_nearby(pos, npc.locations.nodes.sittable, radius) - -- Highly unorthodox check for emptinnes - if nodes[1] ~= nil then - for i = 1, #nodes do - -- Get node name, try to avoid using the staircase check if not a stair node - local node = minetest.get_node(nodes[i]) - local i1, _ = string.find(node.name, "stairs:") - if i1 ~= nil then - if npc.locations.is_in_staircase(nodes[i]) < 1 then - table.insert(result, nodes[i]) - end - else - -- Add node as it is sittable - table.insert(result, nodes[i]) - end - end - end - -- Return sittable nodes - return result -end - --- Specialized function to find sittable stairs: stairs that don't --- have any other stair above them. Only stairs using the default --- stairs mod are supported for now. --- Receives a position of a stair node. - -npc.locations.staircase = { - none = 0, - bottom = 1, - middle = 2, - top = 3 -} - -function npc.locations.is_in_staircase(pos) - local node = minetest.get_node(pos) - -- Verify node is actually from default stairs mod - local p1, _ = string.find(node.name, "stairs:") - if p1 ~= nil then - -- Calculate the logical position to the lower and upper stairs node location - local up_x_adj, up_z_adj = 0, 0 - local lo_x_adj, lo_z_adj = 0, 0 - if node.param2 == 1 then - up_z_adj = -1 - lo_z_adj = 1 - elseif node.param2 == 2 then - up_z_adj = 1 - lo_z_adj = -1 - elseif node.param2 == 3 then - up_x_adj = -1 - lo_x_adj = 1 - elseif node.param2 == 4 then - up_x_adj = 1 - lo_x_adj = -1 - else - -- This is not a staircase - return false - end - - -- Calculate upper and lower position - local upper_pos = {x=pos.x + up_x_adj, y=pos.y + 1, z=pos.z + up_z_adj} - local lower_pos = {x=pos.x + lo_x_adj, y=pos.y - 1, z=pos.z + lo_z_adj} - -- Get next node - local upper_node = minetest.get_node(upper_pos) - local lower_node = minetest.get_node(lower_pos) - --minetest.log("Next node: "..dump(upper_pos)) - -- Check if next node is also a stairs node - local up_p1, _ = string.find(upper_node.name, "stairs:") - local lo_p1, _ = string.find(lower_node.name, "stairs:") - - if up_p1 ~= nil then - -- By default, think this is bottom of staircase. - local result = npc.locations.staircase.bottom - -- Try downwards now - if lo_p1 ~= nil then - result = npc.locations.staircase.middle - end - return result - else - -- Check if there is a staircase downwards - if lo_p1 ~= nil then - return npc.locations.staircase.top - else - return npc.locations.staircase.none - end - end - end - -- This is not a stairs node - return nil -end - --- WARNING: DEPRECATED --- Specialized function to find the node position right behind --- a door. Used to make NPCs enter buildings. -function npc.locations.find_node_in_front_and_behind_door(door_pos) - local door = minetest.get_node(door_pos) - local scan_pos = { - {x=door_pos.x + 1, y=door_pos.y, z=door_pos.z}, - {x=door_pos.x - 1, y=door_pos.y, z=door_pos.z}, - {x=door_pos.x, y=door_pos.y, z=door_pos.z + 1}, - {x=door_pos.x, y=door_pos.y, z=door_pos.z - 1} - } - - local facedir_vector = minetest.facedir_to_dir(door.param2) - local back_pos = vector.add(door_pos, facedir_vector) - local back_node = minetest.get_node(back_pos) - local front_pos - if back_node.name == "air" or minetest.registered_nodes[back_node.name].walkable == false then - -- Door is closed, so back_pos is the actual behind position. - -- Calculate front node - front_pos = vector.add(door_pos, vector.multiply(facedir_vector, -1)) - else - -- Door is open, need to find the front and back pos - facedir_vector = minetest.facedir_to_dir((door.param2 + 2) % 4) - back_pos = vector.add(door_pos, facedir_vector) - front_pos = vector.add(door_pos, vector.multiply(facedir_vector, -1)) - end - return back_pos, front_pos -end \ No newline at end of file diff --git a/mods/advanced_npc/executable/node_registry.lua b/mods/advanced_npc/executable/node_registry.lua deleted file mode 100644 index 58e4c7b..0000000 --- a/mods/advanced_npc/executable/node_registry.lua +++ /dev/null @@ -1,99 +0,0 @@ --- Node functionality registry for NPC actions by Zorman2000 ---------------------------------------------------------------------------------------- --- In this script, some functionality and information required for nodes --- to be used correctly by an NPC is described. --- To avoid as many definitions as possible, the names of the nodes --- can actually be prefixes. - --- This table will contain the registered nodes -npc.programs.instr.nodes = { - doors = {}, - beds = {}, - sittable = {} -} - - ---------------------------------------------------------------------------------------- --- Beds functionality supported by default ---------------------------------------------------------------------------------------- --- Functionality for default beds. --- Since other mods may be used in the same way as the default beds, --- this one is a global registration -npc.programs.instr.nodes.default_bed_registration = { - get_lay_pos = function(pos, dir) - return {x = pos.x + dir.x / 2, y = pos.y + 1, z = pos.z + dir.z / 2} - end, - type = "bed" -} - --- The code used in get_lay_pos is from cottages mod and slightly modified. -local cottages_bed_registration = { - get_lay_pos = function(pos, dir) - return {x = pos.x + dir.x / 2, y = pos.y + 1.4, z = pos.z + dir.z / 2} - end, - type = "bed" -} - -local cottages_mat_registration = { - get_lay_pos = function(pos, dir) - return {x = pos.x + dir.x / 2, y = pos.y + 1, z = pos.z + dir.z / 2} - end, - type = "mat" -} - ---------------------------------------------------------------------------------------- --- Sitting functionality supported by default ---------------------------------------------------------------------------------------- --- Functionality for allowing the NPC to sit on default stairs and cottages' bench -local sittable_stair_registration = { - get_sit_pos = function(pos, param2) - local result = {x=pos.x, y=pos.y+1, z=pos.z}; - if param2 == 0 then - result.z = result.z-0.2; - elseif param2 == 1 then - result.x = result.x-0.2; - elseif param2 == 2 then - result.z = result.z+0.2; - elseif param2 == 3 then - result.x = result.x+0.2; - end - return result - end -} - -local cottages_bench_registration = { - get_sit_pos = function(pos, param2) - local result = {x=pos.x, y=pos.y+1, z=pos.z}; - if param2 == 0 then - result.z = result.z+0.3; - elseif param2 == 1 then - result.x = result.x+0.3; - elseif param2 == 2 then - result.z = result.z-0.3; - elseif param2 == 3 then - result.x = result.x-0.3; - end - return result - end -} - - ---------------------------------------------------------------------------------------- --- Registry of bed nodes ---------------------------------------------------------------------------------------- --- Default beds. -npc.programs.instr.nodes.beds["beds:bed_bottom"] = npc.programs.instr.nodes.default_bed_registration -npc.programs.instr.nodes.beds["beds:fancy_bed_bottom"] = npc.programs.instr.nodes.default_bed_registration - --- Cottages beds -npc.programs.instr.nodes.beds["cottages:bed_foot"] = cottages_bed_registration -npc.programs.instr.nodes.beds["cottages:sleeping_mat"] = cottages_mat_registration -npc.programs.instr.nodes.beds["cottages:straw_mat"] = cottages_mat_registration - ---------------------------------------------------------------------------------------- --- Registry of sittable nodes ---------------------------------------------------------------------------------------- --- Normal wooden stairs -npc.programs.instr.nodes.sittable["stairs:stair_wood"] = sittable_stair_registration --- Cottages bench -npc.programs.instr.nodes.sittable["cottages:bench"] = cottages_bench_registration \ No newline at end of file diff --git a/mods/advanced_npc/executable/pathfinder.lua b/mods/advanced_npc/executable/pathfinder.lua deleted file mode 100644 index e055aed..0000000 --- a/mods/advanced_npc/executable/pathfinder.lua +++ /dev/null @@ -1,395 +0,0 @@ --- Pathfinding code by MarkBu, original can be found here: --- https://github.com/MarkuBu/pathfinder --- --- Modifications by Zorman2000 --- This version is slightly modified to use another "walkable" function, --- plus add a "decorating" path function which allows to know the type --- of nodes in the path. ---------------------------------------------------------------------------------------- --- Pathfinding functionality ---------------------------------------------------------------------------------------- - -npc.pathfinder = {} - -local pathfinder = {} - -npc.pathfinder.node_types = { - start = 0, - goal = 1, - walkable = 2, - openable = 3, - non_walkable = 4 -} - -npc.pathfinder.nodes = { - openable_prefix = { - "doors:", - "cottages:gate", - "cottages:half_door" - } -} - --- This function is used to determine if a node is walkable --- or openable, in which case is good to use when finding a path -function pathfinder.is_good_node(node, exceptions) - --local function is_good_node(node, exceptions) - -- Is openable is to support doors, fence gates and other - -- doors from other mods. Currently, default doors, gates - -- and cottages doors are supported. - local is_openable = false - for _,node_prefix in pairs(npc.pathfinder.nodes.openable_prefix) do - local start_i,end_i = string.find(node.name, node_prefix) - if start_i ~= nil then - is_openable = true - break - end - end - if node ~= nil and node.name ~= nil and not minetest.registered_nodes[node.name].walkable then - return npc.pathfinder.node_types.walkable - elseif is_openable then - return npc.pathfinder.node_types.openable - else - for i = 1, #exceptions do - if node.name == exceptions[i] then - return npc.pathfinder.node_types.walkable - end - end - return npc.pathfinder.node_types.non_walkable - end -end - -function pathfinder.get_decorated_path(path) - -- Get details from path nodes - local path_detail = {} - for i = 1, #path do - local node = minetest.get_node(path[i]) - table.insert(path_detail, {pos={x=path[i].x, y=path[i].y-0.5, z=path[i].z}, - type=pathfinder.is_good_node(node, {})}) - end - - npc.log("DEBUG", "Detailed path: "..dump(path_detail)) - return path_detail -end - -function npc.pathfinder.find_path(start_pos, end_pos, entity, decorate_path) - local path = pathfinder.find_path(start_pos, end_pos, entity) - if path then - if decorate_path then - path = pathfinder.get_decorated_path(path) - end - else - npc.log("ERROR", "Couldn't find path from "..minetest.pos_to_string(start_pos) - .." to "..minetest.pos_to_string(end_pos)) - end - return path -end - --- From this point onwards is MarkBu's original pathfinder code, --- except for the "walkable" function, which is modified by Zorman2000 --- to include doors and other "walkable" nodes. --- The version here is exactly this: --- https://github.com/MarkuBu/pathfinder/commit/ca0b433bf5efde5da545b11b2691fa7f7e53dc30 - ---[[ -minetest.get_content_id(name) -minetest.registered_nodes -minetest.get_name_from_content_id(id) -local ivm = a:index(pos.x, pos.y, pos.z) -local ivm = a:indexp(pos) -minetest.hash_node_position({x=,y=,z=}) -minetest.get_position_from_hash(hash) - -start_index, target_index, current_index -^ Hash of position - -current_value -^ {int:hCost, int:gCost, int:fCost, hash:parent, vect:pos} -]]-- - -local openSet = {} -local closedSet = {} - -local function get_distance(start_pos, end_pos) - local distX = math.abs(start_pos.x - end_pos.x) - local distZ = math.abs(start_pos.z - end_pos.z) - - if distX > distZ then - return 14 * distZ + 10 * (distX - distZ) - else - return 14 * distX + 10 * (distZ - distX) - end -end - -local function get_distance_to_neighbor(start_pos, end_pos) - local distX = math.abs(start_pos.x - end_pos.x) - local distY = math.abs(start_pos.y - end_pos.y) - local distZ = math.abs(start_pos.z - end_pos.z) - - if distX > distZ then - return (14 * distZ + 10 * (distX - distZ)) * (distY + 1) - else - return (14 * distX + 10 * (distZ - distX)) * (distY + 1) - end -end - --- This function is used to determine if a node is walkable --- or openable, in which case is good to use when finding a path -local function walkable(node, exceptions) - local exceptions = exceptions or {} - -- Is openable is to support doors, fence gates and other - -- doors from other mods. Currently, default doors, gates - -- and cottages doors are supported. - --minetest.log("Is good node: "..dump(node)) - local is_openable = false - for _,node_prefix in pairs(npc.pathfinder.nodes.openable_prefix) do - local start_i,end_i = string.find(node.name, node_prefix) - if start_i ~= nil then - is_openable = true - break - end - end - -- Detect mg_villages ceilings usage of thin wood nodeboxes - -- TODO: Improve - local is_mg_villages_ceiling = false - if node.name == "cottages:wood_flat" then - is_mg_villages_ceiling = true - end - if node ~= nil - and node.name ~= nil - and node.name ~= "ignore" - and minetest.registered_nodes[node.name] - and not minetest.registered_nodes[node.name].walkable then - return false - elseif is_openable then - return false - elseif is_mg_villages_ceiling then - return false - else - for i = 1, #exceptions do - if node.name == exceptions[i] then - return false - end - end - return true - end -end - -local function check_clearance(cpos, x, z, height) - for i = 1, height do - local n_name = minetest.get_node({x = cpos.x + x, y = cpos.y + i, z = cpos.z + z}).name - local c_name = minetest.get_node({x = cpos.x, y = cpos.y + i, z = cpos.z}).name - --~ print(i, n_name, c_name) - if walkable(n_name) or walkable(c_name) then - return false - end - end - return true -end - -local function get_neighbor_ground_level(pos, jump_height, fall_height) - local node = minetest.get_node(pos) - local height = 0 - if walkable(node) then - repeat - height = height + 1 - if height > jump_height then - return nil - end - pos.y = pos.y + 1 - node = minetest.get_node(pos) - until not walkable(node) - return pos - else - repeat - height = height + 1 - if height > fall_height then - return nil - end - pos.y = pos.y - 1 - node = minetest.get_node(pos) - until walkable(node) - return {x = pos.x, y = pos.y + 1, z = pos.z} - end -end - -function pathfinder.find_path(pos, endpos, entity) - local start_index = minetest.hash_node_position(pos) - local target_index = minetest.hash_node_position(endpos) - local count = 1 - - openSet = {} - closedSet = {} - - local h_start = get_distance(pos, endpos) - openSet[start_index] = {hCost = h_start, gCost = 0, fCost = h_start, parent = nil, pos = pos} - - -- Entity values - local entity_height = math.ceil(entity.collisionbox[5] - entity.collisionbox[2]) - local entity_fear_height = entity.fear_height or 2 - local entity_jump_height = entity.jump_height or 1 - - repeat - local current_index - local current_values - - -- Get one index as reference from openSet - for i, v in pairs(openSet) do - current_index = i - current_values = v - break - end - - -- Search for lowest fCost - for i, v in pairs(openSet) do - if v.fCost < openSet[current_index].fCost or v.fCost == current_values.fCost and v.hCost < current_values.hCost then - current_index = i - current_values = v - end - end - - openSet[current_index] = nil - closedSet[current_index] = current_values - count = count - 1 - - if current_index == target_index then - -- print("Success") - local path = {} - local reverse_path = {} - repeat - if not closedSet[current_index] then - return - end - table.insert(path, closedSet[current_index].pos) - current_index = closedSet[current_index].parent - if #path > 100 then - -- print("path to long") - return - end - until start_index == current_index - repeat - table.insert(reverse_path, table.remove(path)) - until #path == 0 - -- print("path lenght: "..#reverse_path) - return reverse_path - end - - local current_pos = current_values.pos - - local neighbors = {} - local neighbors_index = 1 - for z = -1, 1 do - for x = -1, 1 do - local neighbor_pos = {x = current_pos.x + x, y = current_pos.y, z = current_pos.z + z} - local neighbor = minetest.get_node(neighbor_pos) - local neighbor_ground_level = get_neighbor_ground_level(neighbor_pos, entity_jump_height, entity_fear_height) - local neighbor_clearance = false - if neighbor_ground_level then - -- print(neighbor_ground_level.y - current_pos.y) - --minetest.set_node(neighbor_ground_level, {name = "default:dry_shrub"}) - local node_above_head = minetest.get_node( - {x = current_pos.x, y = current_pos.y + entity_height, z = current_pos.z}) - if neighbor_ground_level.y - current_pos.y > 0 and not walkable(node_above_head) then - local height = -1 - repeat - height = height + 1 - local node = minetest.get_node( - {x = neighbor_ground_level.x, - y = neighbor_ground_level.y + height, - z = neighbor_ground_level.z}) - until walkable(node) or height > entity_height - if height >= entity_height then - neighbor_clearance = true - end - elseif neighbor_ground_level.y - current_pos.y > 0 and walkable(node_above_head) then - neighbors[neighbors_index] = { - hash = nil, - pos = nil, - clear = nil, - walkable = nil, - } - else - local height = -1 - repeat - height = height + 1 - local node = minetest.get_node( - {x = neighbor_ground_level.x, - y = current_pos.y + height, - z = neighbor_ground_level.z}) - until walkable(node) or height > entity_height - if height >= entity_height then - neighbor_clearance = true - end - end - - neighbors[neighbors_index] = { - hash = minetest.hash_node_position(neighbor_ground_level), - pos = neighbor_ground_level, - clear = neighbor_clearance, - walkable = walkable(neighbor), - } - else - neighbors[neighbors_index] = { - hash = nil, - pos = nil, - clear = nil, - walkable = nil, - } - end - neighbors_index = neighbors_index + 1 - end - end - - for id, neighbor in pairs(neighbors) do - -- don't cut corners - local cut_corner = false - if id == 1 then - if not neighbors[id + 1].clear or not neighbors[id + 3].clear - or neighbors[id + 1].walkable or neighbors[id + 3].walkable then - cut_corner = true - end - elseif id == 3 then - if not neighbors[id - 1].clear or not neighbors[id + 3].clear - or neighbors[id - 1].walkable or neighbors[id + 3].walkable then - cut_corner = true - end - elseif id == 7 then - if not neighbors[id + 1].clear or not neighbors[id - 3].clear - or neighbors[id + 1].walkable or neighbors[id - 3].walkable then - cut_corner = true - end - elseif id == 9 then - if not neighbors[id - 1].clear or not neighbors[id - 3].clear - or neighbors[id - 1].walkable or neighbors[id - 3].walkable then - cut_corner = true - end - end - - if neighbor.hash ~= current_index and not closedSet[neighbor.hash] and neighbor.clear and not cut_corner then - local move_cost_to_neighbor = current_values.gCost + get_distance_to_neighbor(current_values.pos, neighbor.pos) - local gCost = 0 - if openSet[neighbor.hash] then - gCost = openSet[neighbor.hash].gCost - end - if move_cost_to_neighbor < gCost or not openSet[neighbor.hash] then - if not openSet[neighbor.hash] then - count = count + 1 - end - local hCost = get_distance(neighbor.pos, endpos) - openSet[neighbor.hash] = { - gCost = move_cost_to_neighbor, - hCost = hCost, - fCost = move_cost_to_neighbor + hCost, - parent = current_index, - pos = neighbor.pos - } - end - end - end - if count > 100 then - -- print("fail") - return - end - until count < 1 - -- print("count < 1") - return {pos} -end diff --git a/mods/advanced_npc/executable/programs/api.lua b/mods/advanced_npc/executable/programs/api.lua deleted file mode 100644 index 1fb0fe4..0000000 --- a/mods/advanced_npc/executable/programs/api.lua +++ /dev/null @@ -1,41 +0,0 @@ --- --- User: hfranqui --- Date: 3/8/18 --- Time: 2:06 PM --- - --- Global namespace -npc.programs = {} --- Private namespace -local _programs = { - registered_programs = {} -} - --- Registration function -function npc.programs.register(name, func) - if _programs.registered_programs[name] ~= nil then - npc.log("ERROR", "Attempted to register program with name: "..dump(name)..".\nProgram already exists.") - return - end - _programs.registered_programs[name] = {func = func } - npc.log("INFO", "Successfully registered program '"..dump(name).."'") -end - -function npc.programs.is_registered(name) - return _programs.registered_programs[name] ~= nil -end - --- Execution function -function npc.programs.execute(self, name, args) - if _programs.registered_programs[name] == nil then - npc.log("ERROR", "Attempted to execute program with name "..dump(name)..".\nProgram doesn't exists.") - return - end - -- Enqueue callbacks if any - if npc.monitor.callback.exists(npc.monitor.callback.type.program, name) then - -- Enqueue all callbacks for this instruction - npc.monitor.callback.enqueue_all(self, npc.monitor.callback.type.program, name) - end - --npc.log("INFO", "Executing program '"..dump(name).."' with args:\n"..dump(args)) - return _programs.registered_programs[name].func(self, args) -end \ No newline at end of file diff --git a/mods/advanced_npc/executable/programs/builtin/follow.lua b/mods/advanced_npc/executable/programs/builtin/follow.lua deleted file mode 100644 index ac52b1c..0000000 --- a/mods/advanced_npc/executable/programs/builtin/follow.lua +++ /dev/null @@ -1,154 +0,0 @@ --- --- Created by IntelliJ IDEA. --- Date: 3/8/18 --- Time: 2:42 PM --- - --- Follow program. This is a looping program that will try to follow an --- entity or player until either of the following conditions are met: --- - A certain flag is set to false --- - The object is reached and a callback executed. --- Arguments: --- - `radius`: integer, initial search radius. Default is 3 --- - `max_radius`: integer, maximum search radius. If target isn't found within initial radius, --- radius will increase up to this value. Default is 20 --- - `speed`: number, walking speed for the NPC while following. Default is 3 --- - `target`: string, can be "player" or "entity". --- - `player_name`: string, name of player to follow --- - `entity_type`: string, type of entity to follow. NOT IMPLEMENTED. --- - `on_reach`: function, if given, on reaching the target, this function will --- be called and executed. On execution, the script will finish. DO NOT use with --- `follow_flag`. --- - `follow_flag`: string, flag name. If given, the script will keep running until the --- value of this flag is false. DO NOT use with `on_reach`. -npc.programs.register("advanced_npc:follow", function(self, args) - -- Set default arguments if not present - args.radius = args.radius or 3 - args.max_radius = args.max_radius or 20 - args.speed = args.speed or 3 - args.results_key = "advanced_npc:follow:player_follow" - - -- Run this 1/speed times in a second - npc.programs.instr.execute(self, npc.programs.instr.default.SET_INTERVAL, {interval=1/args.speed, freeze=true}) - -- Make NPC climb one-block heights. Makes following easier - self.stepheight = 1.1 - self.object:set_properties(self) - - -- Execution - -- Follow, results to be stored on execution context with key "results_key" - npc.exec.proc.enqueue(self, "advanced_npc:follow:follow_player", args, args.results_key) - -- Check if follow is complete - npc.exec.proc.enqueue(self, "advanced_npc:follow:check_if_complete", args) -end) - --- Follow script functions --- Function used to reset NPC values once following is complete -npc.programs.instr.register("advanced_npc:follow:reset", function(self) - self.stepheight = 0.6 - self.object:set_properties(self) - npc.programs.instr.execute(self, npc.programs.instr.default.SET_INTERVAL, {interval=1, freeze=false}) -end) - --- Follow the player -npc.programs.instr.register("advanced_npc:follow:follow_player", function(self, args) - if args.target == "player" then - local player_name = args.player_name - local objs = minetest.get_objects_inside_radius(self.object:getpos(), args.radius) - -- Check if objects were found - minetest.log("Objects found: "..dump(objs)) - if #objs > 0 then - for _,obj in pairs(objs) do - if obj then - -- Check if this is the player we are looking for - if obj:is_player() and obj:get_player_name() == player_name then - local target_pos = vector.round(obj:getpos()) - -- Calculate distance - if less than 3, avoid walking any further - if vector.distance(self.object:getpos(), target_pos) < 3 then - npc.log("PROCESS", "[follow] Destination reached") - -- Destination reached - -- Add standing action if NPC is still moving - if math.abs(vector.length(self.object:getvelocity())) > 0 then - npc.programs.instr.execute(self, npc.programs.instr.default.STAND, - {dir = minetest.dir_to_yaw(vector.direction(self.object:getpos(), target_pos))} - ) - end - - -- Rotate NPC towards player - npc.programs.instr.execute(self, npc.programs.instr.default.ROTATE, - {yaw = minetest.dir_to_yaw(vector.direction(self.object:getpos(), target_pos))}) - - -- Execute `on_reach` function if present - if args.on_reach then - npc.log("PROCESS", "[follow] Executing on_reach callback...") - args.on_reach(self, obj) - return {reached_target = true, target_pos = target_pos, end_execution = true} - end - - return {reached_target = true, target_pos = target_pos} - else - npc.log("PROCESS", "[follow] Walking towards player...") - local walk_args = { - yaw = minetest.dir_to_yaw(vector.direction(self.object:getpos(), target_pos)), - speed = args.speed - } - -- Enqueue walk step - npc.programs.instr.execute(self, npc.programs.instr.default.WALK_STEP, walk_args) - return {reached_target = false, target_pos = target_pos} - end - end - end - end - -- Player not found, stop - npc.programs.instr.execute(self, npc.programs.instr.default.STAND, {}) - return {reached_target = false, target_pos = nil} - end - end - return {reached_target = false, target_pos = nil} -end) - - -npc.programs.instr.register("advanced_npc:follow:check_if_complete", function(self, args) - -- Check if follow is still needed - if npc.get_flag(self, args.follow_flag) == false then - -- Stop, follow no more - npc.programs.instr.execute(self, npc.programs.instr.default.STAND, {}) - -- Clear flag - npc.update_flag(self, args.follow_flag, nil) - -- Reset actions interval and NPC stepheight - npc.programs.instr.execute(self, "advanced_npc:follow:reset", {}) - return - end - - -- Get results from following - local follow_result = npc.exec.var.get(self, args.results_key) - -- Check results - if follow_result == nil then - npc.log("WARNING", "Unable to find result in execution context for 'follow_player' function using key: ".. - dump(args.results_key)) - return - end - -- Clean execution context - npc.exec.var.remove(self, args.results_key) - - -- Check if target reached and on_reach function executed - if follow_result.reached_target == true and follow_result.end_execution == true then - return - end - -- on_reach is not set, keep executing until follow flag is off. - if follow_result.target_pos ~= nil then - -- Keep walking or waiting for player to keep moving - npc.exec.proc.enqueue(self, "advanced_npc:follow:follow_player", args, args.results_key) - -- Check if follow is complete - npc.exec.proc.enqueue(self, "advanced_npc:follow:check_if_complete", args) - --npc.enqueue_function(self, detect_more_movement, {player_pos = follow_result.target_pos}) - else - -- Cannot find - npc.log("PROCESS", "[follow] Walking towards player") - -- Modify args to increase radius - args.radius = args.radius + 1 - npc.exec.proc.enqueue(self, "advanced_npc:follow:follow_player", args, args.results_key) - -- Check if follow is complete - npc.exec.proc.enqueue(self, "advanced_npc:follow:check_if_complete", args) - end -end) - diff --git a/mods/advanced_npc/executable/programs/builtin/idle.lua b/mods/advanced_npc/executable/programs/builtin/idle.lua deleted file mode 100644 index d7891d5..0000000 --- a/mods/advanced_npc/executable/programs/builtin/idle.lua +++ /dev/null @@ -1,166 +0,0 @@ --- --- Created by IntelliJ IDEA. --- Date: 3/8/18 --- Time: 6:34 PM --- - --- Register callback - count number of rightclick interactions -npc.monitor.callback.register("interactions_since_ack", "interaction", "on_rightclick", function(self) - local interaction_count_since_ack = - npc.data.get_or_put_if_nil(self, "interaction_count_since_ack", 0) - npc.data.set(self, "interaction_count_since_ack", interaction_count_since_ack + 1) -end) - --- Timer for stopping acknowledge of nearby objects if no interactions -npc.monitor.timer.register("advanced_npc:idle:acknowledge_burnout", 5, function(self, args) - -- Check if timer should run - if self.execution.state_process.program_name ~= "advanced_npc:idle" - and self.execution.state_process.program_name ~= "advanced_npc:wander" - or self.execution.state_process.arguments.acknowledge_nearby_objs == false then - -- Stop current timer - npc.monitor.timer.stop(self, "advanced_npc:idle:acknowledge_burnout") - return - end - - -- Get number of interactions - local interaction_count_since_ack = npc.data.get(self, "interaction_count_since_ack") - -- Check if there has been any interaction - if interaction_count_since_ack == 0 or interaction_count_since_ack == nil then - -- Stop current timer - npc.monitor.timer.stop(self, "advanced_npc:idle:acknowledge_burnout") - -- Activate burnout reversal timer - npc.monitor.timer.start(self, "advanced_npc:idle:burnout_reversal", args.reversal_timeout or 5, args) - -- Change to wander state - npc.exec.set_state_program(self, "advanced_npc:wander", { - idle_chance = 0, - acknowledge_nearby_objs = false - }, {}) - else - -- Reset interaction count - npc.data.set(self, "interaction_count_since_ack", 0) - end -end) - --- Timer to start acknowledging again -npc.monitor.timer.register("advanced_npc:idle:burnout_reversal", 5, function(self, args) - -- Check if timer should run - if self.execution.state_process.program_name ~= "advanced_npc:idle" - and self.execution.state_process.program_name ~= "advanced_npc:wander" - or self.execution.state_process.arguments.acknowledge_nearby_objs == false then - -- Stop current timer - npc.monitor.timer.stop(self, "advanced_npc:idle:burnout_reversal") - return - end - -- Stop burnot timer and burnout reversal - npc.monitor.timer.stop(self, "advanced_npc:idle:burnout_reversal") - -- Signal instruction to restart acknowldge burnout - npc.exec.var.set(self, "start_acknowledge_burnout", true) - -- Change to wander state with acknowledge true - npc.exec.set_state_program(self, "advanced_npc:idle", { - acknowledge_nearby_objs = true - }, {}) -end) - - --- TODO: Implement whitelist -npc.programs.instr.register("advanced_npc:idle:acknowledge_objects", function(self, args) - local obj_search_radius = args.obj_search_radius or 4 - local acknowledge_burnout = args.acknowledge_burnout or 0 - local obj_whitelist = args.whitelist - - local objs = minetest.get_objects_inside_radius(self.object:getpos(), obj_search_radius) - - if #objs > 1 then - for _,obj in pairs(objs) do - if obj:is_player() or - (obj:get_luaentity() and obj:get_luaentity().npc_id and obj:get_luaentity().npc_id ~= self.npc_id) or - (obj:get_luaentity() and obj:get_luaentity().type == "animal") then - -- Rotate NPC towards object - local yaw = minetest.dir_to_yaw(vector.direction(self.object:getpos(), obj:getpos())) - npc.programs.instr.execute(self, npc.programs.instr.default.ROTATE, {yaw=yaw}) - -- Check if we have to activate timer to stop acknowledging - if acknowledge_burnout > 0 then - local start_timer = npc.exec.var.get_or_put_if_nil(self, "start_acknowledge_burnout", true) - if start_timer == true then - npc.exec.var.set(self, "start_acknowledge_burnout", false) - -- Activate burnout timer - npc.monitor.timer.start(self, - "advanced_npc:idle:acknowledge_burnout", - acknowledge_burnout, - {reversal_timeout = acknowledge_burnout}) - end - - end - -- Object found - return true - end - end - end - -- Object not found - return false -end) - - --- Idle state script. NPC stays still on this state. --- It is possible for it to acknowledge other NPCs or players if --- configured as arguments. --- Arguments: --- - `acknowledge_nearby_objs`: boolean. If true, will look for objects and --- rotate towards them when close by. --- - `obj_search_interval`: integer, interval in seconds to search for objects. --- Default is 5 --- - `obj_search_radius`: integer, radius in nodes to search for objects. --- Default is 5 -npc.programs.register("advanced_npc:idle", function(self, args) - local search_nearby_objs = args.acknowledge_nearby_objs - local obj_search_interval = args.obj_search_interval or 5 - local obj_search_radius = args.obj_search_radius or 4 - local wander_chance = args.wander_chance or 30 - local max_wandering_radius = args.max_wandering_radius or 10 - local max_acknowledge_time = args.max_acknowledge_time - - -- Check if NPC is moving, if it is, stop. - if npc.programs.helper.is_moving(self) then - npc.programs.instr.execute(self, npc.programs.instr.default.STAND, {}) - end - - local objs_found = false - if search_nearby_objs == true then - -- Search nearby objects and acknowledge them - objs_found = npc.programs.instr.execute(self, "advanced_npc:idle:acknowledge_objects", { - obj_search_radius = obj_search_radius, - acknowledge_burnout = max_acknowledge_time - }) - - -- if objs_found == true then - -- -- Shorten interval to rotate accurately towards object - -- --npc.programs.instr.execute(self, npc.programs.instr.default.SET_PROCESS_INTERVAL, {interval=0.5}) - -- else - - -- end - else - -- Stop all acknowledging timers - npc.monitor.timer.stop(self, "advanced_npc:idle:acknowledge_burnout") - npc.monitor.timer.stop(self, "advanced_npc:idle:burnout_reversal") - end - - -- Calculate wandering chance - if objs_found == false then - local calculated_wander_chance = math.random(0, 100) - if calculated_wander_chance < wander_chance then - npc.log("INFO", "Switching to wander state") - -- Change to wander state process with mostly default args - npc.exec.set_state_program(self, "advanced_npc:wander", { - max_radius = max_wandering_radius, - idle_chance = 0, - acknowledge_nearby_objs = search_nearby_objs - }, {}) - return - else - -- Set interval - npc.programs.instr.execute(self, "advanced_npc:wait", {time=5}) - --npc.programs.instr.execute(self, npc.programs.instr.default.SET_PROCESS_INTERVAL, {interval=obj_search_interval}) - minetest.log("No obj found") - end - end -end) diff --git a/mods/advanced_npc/executable/programs/builtin/internal_property_change.lua b/mods/advanced_npc/executable/programs/builtin/internal_property_change.lua deleted file mode 100644 index 59a7f69..0000000 --- a/mods/advanced_npc/executable/programs/builtin/internal_property_change.lua +++ /dev/null @@ -1,66 +0,0 @@ --- --- User: zorman2000 --- Date: 3/27/18 --- Time: 6:54 PM --- - -npc.programs.internal_properties = { - put_item = "put_item", - put_multiple_items = "put_multiple_items", - take_item = "take_item", - change_trader_status = "trader_status", - can_receive_gifts = "can_receive_gifts", - flag = "flag", - enable_gift_items_hints = "enable_gift_items_hints", - set_trade_list = "set_trade_list" -} - -npc.programs.register("advanced_npc:internal_property_change", function(self, args) - - local properties = {} - -- Check if this is a just a single property - if args.property and args.args then - properties[#properties + 1] = {property = args.property, args = args.args } - else - -- Args is an array of property objects as above - properties = args - end - - -- Process each property - for i = 1, #properties do - local property = properties[i].property - local args = properties[i].args - if property == npc.programs.internal_properties.change_trader_status then - npc.programs.instr.execute(self, "advanced_npc:trade:change_trader_status", args) - elseif property == npc.programs.internal_properties.set_trade_list then - npc.programs.instr.execute(self, "advanced_npc:trade:set_trade_list", args) - elseif property == npc.programs.internal_properties.put_item then - npc.programs.instr.execute(self, "advanced_npc:inventory_put", args) - elseif property == npc.programs.internal_properties.put_multiple_items then - npc.programs.instr.execute(self, "advanced_npc:inventory_put_multiple", args) - elseif property == npc.programs.internal_properties.take_item then - npc.programs.instr.execute(self, "advanced_npc:inventory_take", args) - elseif property == npc.programs.internal_properties.can_receive_gifts then - local value = args.can_receive_gifts - -- Set status - self.can_receive_gifts = value - elseif property == npc.programs.internal_properties.flag then - local action = args.action - if action == "set" then - -- Adds or overwrites an existing flag and sets it to the given value - self.flags[args.flag_name] = args.flag_value - elseif action == "reset" then - -- Sets value of flag to false or to 0 - local flag_type = type(self.flags[args.flag_name]) - if flag_type == "number" then - self.flags[args.flag_name] = 0 - elseif flag_type == "boolean" then - self.flags[args.flag_name] = false - end - end - elseif property == npc.schedule_properties.enable_gift_item_hints then - self.gift_data.enable_gift_items_hints = args.value - end - end -end) - diff --git a/mods/advanced_npc/executable/programs/builtin/node_query.lua b/mods/advanced_npc/executable/programs/builtin/node_query.lua deleted file mode 100644 index 2a5e000..0000000 --- a/mods/advanced_npc/executable/programs/builtin/node_query.lua +++ /dev/null @@ -1,244 +0,0 @@ --- --- User: hfranqui --- Date: 4/20/18 --- Time: 9:16 AM --- Description: Node query program to replace scheduler check functionality --- - -npc.programs.register("advanced_npc:node_query", function(self, args) - - local times_to_execute = args.count or 0 - local randomize_execution_count = args.randomize_execution_count - local max_count = args.max_count - local min_count = args.min_count - local state_program_on_finished = args.state_program_on_finished - local range = args.range - local vertical_range_limit = args.vertical_range_limit or args.range - local walkable_nodes = args.walkable_nodes - local nodes = args.nodes - local prefer_last_acted_upon_node = args.prefer_last_acted_upon_node - local on_found_executables = args.on_found_executables - local on_not_found_executables = args.on_not_found_executables - - -- Set random execution count to false if argument not provided - if randomize_execution_count == nil then - randomize_execution_count = false - elseif randomize_execution_count == true then - -- Calculate count if random - times_to_execute = math.random(min_count, max_count) - end - - -- Get NPC position - local start_pos = self.object:getpos() - -- Search nodes - local found_nodes = npc.locations.find_node_nearby(start_pos, nodes, range, vertical_range_limit) - -- Check if any node was found - npc.log("DEBUG_SCHEDULE", "Found nodes using radius: "..dump(found_nodes)) - if found_nodes and #found_nodes > 0 then - local node_pos - local node - local last_node_acted_upon = npc.exec.var.get(self, "last_node_acted_upon") - -- Check if there is preference to act on nodes already acted upon - if prefer_last_acted_upon_node == true and last_node_acted_upon then - -- Find a node other than the acted upon - try 3 times - for i = 1, #found_nodes do - node_pos = found_nodes[i] - -- Get node info - node = minetest.get_node(node_pos) - if node.name == last_node_acted_upon then - break - end - end - else - -- Create variable - npc.exec.var.put(self, "last_node_acted_upon", "") - -- Pick a random node to act upon - node_pos = found_nodes[math.random(1, #found_nodes)] - -- Get node info - node = minetest.get_node(node_pos) - end - -- Save this node as the last acted upon - npc.exec.var.set(self, "last_node_acted_upon", node.name) - -- Set node as a place - -- Note: Code below isn't *adding* a node, but overwriting the - -- place with "schedule_target_pos" place type - npc.log("DEBUG_SCHEDULE", "Found "..dump(node.name).." at pos: "..minetest.pos_to_string(node_pos)) - npc.locations.add_shared_accessible_place( - self, {owner="", node_pos=node_pos}, npc.locations.data.calculated.target, true, walkable_nodes) - -- Get actions related to node and enqueue them - for i = 1, #on_found_executables[node.name] do --- local args = {} --- local action --- -- Calculate arguments for the following supported actions: --- -- - Dig --- -- - Place --- -- - Walk step --- -- - Walk to position --- -- - Use furnace --- if actions[node.name][i].action == npc.commands.cmd.DIG then --- -- Defaults: items will be added to inventory if not specified --- -- otherwise, and protection will be respected, if not specified --- -- otherwise --- args = { --- pos = node_pos, --- add_to_inventory = actions[node.name][i].args.add_to_inventory or true, --- bypass_protection = actions[node.name][i].args.bypass_protection or false --- } --- npc.add_action(self, actions[node.name][i].action, args) --- elseif actions[node.name][i].action == npc.commands.cmd.PLACE then --- -- Position: providing node_pos is because the currently planned --- -- behavior for placing nodes is replacing digged nodes. A NPC farmer, --- -- for instance, might dig a plant node and plant another one on the --- -- same position. --- -- Defaults: items will be taken from inventory if existing, --- -- if not will be force-placed (item comes from thin air) --- -- Protection will be respected --- args = { --- pos = actions[node.name][i].args.pos or node_pos, --- source = actions[node.name][i].args.source or npc.commands.take_from_inventory_forced, --- node = actions[node.name][i].args.node, --- bypass_protection = actions[node.name][i].args.bypass_protection or false --- } --- --minetest.log("Enqueue dig action with args: "..dump(args)) --- npc.add_action(self, actions[node.name][i].action, args) --- elseif actions[node.name][i].action == npc.commands.cmd.ROTATE then --- -- Set arguments --- args = { --- dir = actions[node.name][i].dir, --- start_pos = actions[node.name][i].start_pos --- or {x=start_pos.x, y=node_pos.y, z=start_pos.z}, --- end_pos = actions[node.name][i].end_pos or node_pos --- } --- -- Enqueue action --- npc.add_action(self, actions[node.name][i].action, args) --- elseif actions[node.name][i].action == npc.commands.cmd.WALK_STEP then --- -- Defaults: direction is calculated from start node to node_pos. --- -- Speed is default wandering speed. Target pos is node_pos --- -- Calculate dir if dir is random --- local dir = npc.commands.get_direction(start_pos, node_pos) --- minetest.log("actions: "..dump(actions[node.name][i])) --- if actions[node.name][i].args.dir == "random" then --- dir = math.random(0,7) --- elseif type(actions[node.name][i].args.dir) == "number" then --- dir = actions[node.name][i].args.dir --- end --- args = { --- dir = dir, --- speed = actions[node.name][i].args.speed or npc.commands.one_nps_speed, --- target_pos = actions[node.name][i].args.target_pos or node_pos --- } --- npc.add_action(self, actions[node.name][i].action, args) --- elseif actions[node.name][i].task == npc.commands.cmd.WALK_TO_POS then --- -- Optimize walking -- since distances can be really short, --- -- a simple walk_step() action can do most of the times. For --- -- this, however, we need to calculate direction --- -- First of all, check distance --- local distance = vector.distance(start_pos, node_pos) --- if distance < 3 then --- -- Will do walk_step based instead --- if distance > 1 then --- args = { --- dir = npc.commands.get_direction(start_pos, node_pos), --- speed = npc.commands.one_nps_speed --- } --- -- Enqueue walk step --- npc.add_action(self, npc.commands.cmd.WALK_STEP, args) --- end --- -- Add standing action to look at node --- npc.add_action(self, npc.commands.cmd.STAND, --- {dir = npc.commands.get_direction(self.object:getpos(), node_pos)} --- ) --- else --- -- Set end pos to be node_pos --- args = { --- end_pos = actions[node.name][i].args.end_pos or node_pos, --- walkable = actions[node.name][i].args.walkable or walkable_nodes or {} --- } --- -- Enqueue --- npc.enqueue_script(self, actions[node.name][i].task, args) --- end --- elseif actions[node.name][i].task == npc.commands.cmd.USE_FURNACE then --- -- Defaults: pos is node_pos. Freeze is true --- args = { --- pos = actions[node.name][i].args.pos or node_pos, --- item = actions[node.name][i].args.item, --- freeze = actions[node.name][i].args.freeze or true --- } --- npc.enqueue_script(self, actions[node.name][i].task, args) --- else --- -- Action or task that is not supported for value calculation --- npc.enqueue_schedule_action(self, actions[node.name][i]) --- end - local executable = on_found_executables[node.name][i] - --minetest.log("Executable["..dump(i).."]: "..dump(executable)) - if executable then - if executable.is_state_program then - -- Set state program - npc.exec.set_state_program(self, - executable.program_name, - executable.arguments, - executable.interrupt_option) - end - -- Enqueue entry - npc.exec.proc.enqueue(self, "advanced_npc:interrupt", { - new_program = executable.program_name, - new_args = executable.arguments, - interrupt_options = executable.interrupt_options} - ) - end - end - - --npc.log("DEBUG_SCHEDULE", "Actions queue: "..dump(self.actions.queue)) - else - -- No nodes found, enqueue none_actions - for i = 1, #on_not_found_executables do - -- Add start_pos to none_actions - --on_not_found_executables[i].args["start_pos"] = start_pos - -- Enqueue actions - --npc.add_action(self, none_actions[i].action, none_actions[i].args) - npc.exec.enqueue_program(self, - on_not_found_executables[i].program_name, - on_not_found_executables[i].arguments, - on_not_found_executables[i].interrupt_options) - end - - -- No nodes found - --npc.log("DEBUG_SCHEDULE", "Actions queue: "..dump(self.actions.queue)) - end - - if times_to_execute or (randomize_execution_count and max_count and min_count) then - -- Increase execution count - local execution_count = npc.exec.var.get(self, "execution_count") - if execution_count == nil then - execution_count = 0 - npc.exec.var.put(self, "execution_count", execution_count) - end - execution_count = execution_count + 1 - npc.exec.var.set(self, "execution_count", execution_count) - - -- Check if max number of executions was reached - if execution_count > times_to_execute then - if state_program_on_finished then - npc.exec.set_state_program(self, - state_program_on_finished.program_name, - state_program_on_finished.arguments, - state_program_on_finished.interrupt_option) - end - end - end -end) - - ----- Range: integer, radius in which nodes will be searched. Recommended radius is ----- between 1-3 ----- Nodes: array of node names ----- Actions: map of node names to entries {action=, args={}}. ----- Arguments can be empty - the check function will try to determine most ----- arguments anyways (like pos and dir). ----- Special node "any" will execute those actions on any node except the ----- already specified ones. ----- None-action: array of entries {action=, args={}}. ----- Will be executed when no node is found. ---function npc.schedule_check(self) --- npc.log("DEBUG_SCHEDULE", "Prev Actions queue: "..dump(self.actions.queue)) ---end diff --git a/mods/advanced_npc/executable/programs/builtin/use_bed.lua b/mods/advanced_npc/executable/programs/builtin/use_bed.lua deleted file mode 100644 index 496fcac..0000000 --- a/mods/advanced_npc/executable/programs/builtin/use_bed.lua +++ /dev/null @@ -1,87 +0,0 @@ --- --- User: hfranqui --- Date: 3/12/18 --- Time: 9:00 AM --- - --- This function makes the NPC lay or stand up from a bed. The --- pos is the location of the bed, command can be lay or get up -npc.programs.register("advanced_npc:use_bed", function(self, args) - local pos = npc.programs.helper.get_pos_argument(self, args.pos) - if pos == nil then - npc.log("WARNING", "Got nil position in 'use_bed' using args.pos: "..dump(args.pos)) - return - end - local action = args.action - local enable_usage_marking = args.enable_usage_marking or true - local node = minetest.get_node(pos) - --minetest.log(dump(node)) - local dir = minetest.facedir_to_dir(node.param2) - - if action == npc.programs.const.node_ops.beds.LAY then - -- Get position - -- Error here due to ignore. Need to come up with better solution - if node.name == "ignore" then - return - end - local bed_pos = npc.programs.instr.nodes.beds[node.name].get_lay_pos(pos, dir) - -- Sit down on bed, rotate to correct direction - npc.programs.instr.execute(self, npc.programs.instr.default.SIT, {pos=bed_pos, dir=(node.param2 + 2) % 4}) - -- Lay down - npc.exec.proc.enqueue(self, npc.programs.instr.default.LAY, {}) - if enable_usage_marking then - -- Set place as used - npc.locations.mark_place_used(pos, npc.locations.USE_STATE.USED) - end - else - -- Calculate position to get up - -- Error here due to ignore. Need to come up with better solution - if node.name == "ignore" then - return - end - local bed_pos_y = npc.programs.instr.nodes.beds[node.name].get_lay_pos(pos, dir).y - local bed_pos = {x = pos.x, y = bed_pos_y, z = pos.z} - -- Sit up - npc.programs.instr.execute(self, npc.programs.instr.default.SIT, {pos=bed_pos}) - -- Initialize direction: Default is front of bottom of bed - local dir = (node.param2 + 2) % 4 - -- Find empty node around node - -- Take into account that mats are close to the floor, so y adjustmen is zero - local y_adjustment = -1 - if npc.programs.instr.nodes.beds[node.name].type == "mat" then - y_adjustment = 0 - end - - local pos_out_of_bed = pos - local empty_nodes = npc.locations.find_node_orthogonally(bed_pos, {"air", "cottages:bench"}, y_adjustment) - if empty_nodes ~= nil and #empty_nodes > 0 then - -- Get direction to the empty node - dir = npc.programs.helper.get_direction(bed_pos, empty_nodes[1].pos) - - -- Calculate position to get out of bed - pos_out_of_bed = - {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} - -- Account for benches if they are present to avoid standing over them - if empty_nodes[1].name == "cottages:bench" then - pos_out_of_bed = {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} - if empty_nodes[1].param2 == 0 then - pos_out_of_bed.z = pos_out_of_bed.z - 0.3 - elseif empty_nodes[1].param2 == 1 then - pos_out_of_bed.x = pos_out_of_bed.x - 0.3 - elseif empty_nodes[1].param2 == 2 then - pos_out_of_bed.z = pos_out_of_bed.z + 0.3 - elseif empty_nodes[1].param2 == 3 then - pos_out_of_bed.x = pos_out_of_bed.x + 0.3 - end - end - - end - -- Stand out of bed - npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {pos=pos_out_of_bed, dir=dir}) - if enable_usage_marking then - -- Set place as unused - npc.locations.mark_place_used(pos, npc.locations.USE_STATE.NOT_USED) - end - end -end) - diff --git a/mods/advanced_npc/executable/programs/builtin/use_furnace.lua b/mods/advanced_npc/executable/programs/builtin/use_furnace.lua deleted file mode 100644 index 067dee1..0000000 --- a/mods/advanced_npc/executable/programs/builtin/use_furnace.lua +++ /dev/null @@ -1,153 +0,0 @@ --- --- User: hfranqui --- Date: 3/12/18 --- Time: 9:00 AM --- - --- This function allows a NPC to use a furnace using only items from --- its own inventory. Fuel is not provided. Once the furnace is finished --- with the fuel items the NPC will take whatever was cooked and whatever --- remained to cook. The function received the position of the furnace --- to use, and the item to cook in furnace. Item is an itemstring -npc.programs.register("advanced_npc:use_furnace", function(self, args) - local pos = npc.programs.helper.get_pos_argument(self, args.pos) - if pos == nil then - npc.log("WARNING", "Got nil position in 'use_furnace' using args.pos: "..dump(args.pos)) - return - end - - local enable_usage_marking = args.enable_usage_marking or true - local item = args.item - local freeze = args.freeze - -- Define which items are usable as fuels. The NPC - -- will mainly use this as fuels to avoid getting useful - -- items (such as coal lumps) for burning - local fuels = {"default:leaves", - "default:pine_needles", - "default:tree", - "default:acacia_tree", - "default:aspen_tree", - "default:jungletree", - "default:pine_tree", - "default:coalblock", - "farming:straw"} - - -- Check if NPC has item to cook - local src_item = npc.inventory_contains(self, npc.get_item_name(item)) - if src_item == nil then - -- Unable to cook item that is not in inventory - return false - end - - -- Check if NPC has a fuel item - for i = 1,9 do - local fuel_item = npc.inventory_contains(self, fuels[i]) - - if fuel_item ~= nil then - -- Get fuel item's burn time - local fuel_time = - minetest.get_craft_result({method="fuel", width=1, items={ItemStack(fuel_item.item_string)}}).time - local total_fuel_time = fuel_time * npc.get_item_count(fuel_item.item_string) - npc.log("DEBUG", "Fuel time: "..dump(fuel_time)) - - -- Get item to cook's cooking time - local cook_result = - minetest.get_craft_result({method="cooking", width=1, items={ItemStack(src_item.item_string)}}) - local total_cook_time = cook_result.time * npc.get_item_count(item) - npc.log("DEBUG", "Cook: "..dump(cook_result)) - - npc.log("DEBUG", "Total cook time: "..total_cook_time - ..", total fuel burn time: "..dump(total_fuel_time)) - - -- Check if there is enough fuel to cook all items - if total_cook_time > total_fuel_time then - -- Don't have enough fuel to cook item. Return the difference - -- so it may help on trying to acquire the fuel later. - -- NOTE: Yes, returning here means that NPC could probably have other - -- items usable as fuels and ignore them. This should be ok for now, - -- considering that fuel items are ordered in a way where cheaper, less - -- useless items come first, saving possible valuable items. - return cook_result.time - fuel_time - end - - -- Set furnace as used if flag is enabled - if enable_usage_marking then - -- Set place as used - npc.locations.mark_place_used(pos, npc.locations.USE_STATE.USED) - end - - -- Calculate how much fuel is needed - local fuel_amount = total_cook_time / fuel_time - if fuel_amount < 1 then - fuel_amount = 1 - end - - npc.log("DEBUG", "Amount of fuel needed: "..fuel_amount) - - -- Put this item on the fuel inventory list of the furnace - local args = { - player = nil, - pos = pos, - inv_list = "fuel", - item_name = npc.get_item_name(fuel_item.item_string), - count = fuel_amount - } - npc.programs.instr.execute(self, npc.programs.instr.default.PUT_ITEM, args) - -- Put the item that we want to cook on the furnace - args = { - player = nil, - pos = pos, - inv_list = "src", - item_name = npc.get_item_name(src_item.item_string), - count = npc.get_item_count(item), - is_furnace = true - } - npc.exec.proc.enqueue(self, npc.programs.instr.default.PUT_ITEM, args) - - -- Now, set NPC to wait until furnace is done. - npc.log("DEBUG", "Setting wait command for "..dump(total_cook_time)) - npc.exec.proc.enqueue(self, npc.programs.instr.default.SET_INTERVAL, {interval=total_cook_time, freeze=freeze}) - - -- Reset timer - npc.exec.proc.enqueue(self, npc.programs.instr.default.SET_INTERVAL, {interval=1, freeze=true}) - - -- If freeze is false, then we will have to find the way back to the furnace - -- once cooking is done. - if freeze == false then - npc.log("DEBUG", "Adding walk to position to wandering: "..dump(pos)) - npc.exec.proc.enqueue(self, npc.programs.instr.default.INTERRUPT, { - new_program = "advanced_npc:walk_to_pos", - new_args = {end_pos=pos, walkable={}}, - {} - }) - --npc.enqueue_script(self, npc.programs.instr.default.WALK_TO_POS, {end_pos=pos, walkable={}}) - end - - -- Take cooked items back - args = { - player = nil, - pos = pos, - inv_list = "dst", - item_name = cook_result.item:get_name(), - count = npc.get_item_count(item), - is_furnace = false - } - npc.log("DEBUG", "Taking item back: "..minetest.pos_to_string(pos)) - npc.exec.proc.enqueue(self, npc.programs.instr.default.TAKE_ITEM, args) - - npc.log("DEBUG", "Inventory: "..dump(self.inventory)) - - -- Set furnace as unused if flag is enabled - if enable_usage_marking then - -- Set place as used - npc.locations.mark_place_used(pos, npc.locations.USE_STATE.NOT_USED) - end - - return true - end - end - -- Couldn't use the furnace due to lack of items - return false -end) - - diff --git a/mods/advanced_npc/executable/programs/builtin/use_sittable.lua b/mods/advanced_npc/executable/programs/builtin/use_sittable.lua deleted file mode 100644 index 2ec7260..0000000 --- a/mods/advanced_npc/executable/programs/builtin/use_sittable.lua +++ /dev/null @@ -1,65 +0,0 @@ --- --- User: hfranqui --- Date: 3/12/18 --- Time: 9:00 AM --- - --- This function makes the NPC lay or stand up from a sittable node. The --- pos is the location of the sittable node, command can be lay or get up -npc.programs.register("advanced_npc:use_sittable", function(self, args) - local pos = npc.programs.helper.get_pos_argument(self, args.pos) - if pos == nil then - npc.log("WARNING", "Got nil position in 'use_sittable' using args.pos: "..dump(args.pos)) - return - end - local action = args.action - local enable_usage_marking = args.enable_usage_marking or true - local node = minetest.get_node(pos) - - if action == npc.programs.const.node_ops.sittable.SIT then - minetest.log("Sitting...") - -- Calculate position depending on bench - -- Error here due to ignore. Need to come up with better solution - if node.name == "ignore" then - return - end - if npc.programs.instr.nodes.sittable[node.name] == nil then - npc.log("WARNING", "Couldn't find node def for sittable node for node: "..dump(node.name)) - return - end - local sit_pos = npc.programs.instr.nodes.sittable[node.name].get_sit_pos(pos, node.param2) - -- Sit down on bench/chair/stairs - npc.programs.instr.execute(self, npc.programs.instr.default.SIT, {pos=sit_pos, dir=(node.param2 + 2) % 4}) - if enable_usage_marking then - -- Set place as used - npc.locations.mark_place_used(pos, npc.locations.USE_STATE.USED) - end - else - if self.npc_state.movement.is_sitting == false then - npc.log("DEBUG_ACTION", "NPC "..self.npc_name.." attempted to get up from sit when it is not sitting.") - return - end - -- Find empty areas around chair - local dir = node.param2 + 2 % 4 - -- Default it to the current position in case it can't find empty - -- position around sittable node. Weird - local pos_out_of_sittable = pos - local empty_nodes = npc.locations.find_node_orthogonally(pos, {"air"}, 0) - if empty_nodes ~= nil and #empty_nodes > 0 then - -- Get direction to the empty node - dir = npc.programs.helper.get_direction(pos, empty_nodes[1].pos) - -- Calculate position to get out of sittable node - pos_out_of_sittable = - {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} - end - -- Stand - npc.programs.instr.execute(self, npc.programs.instr.default.STAND, {pos=pos_out_of_sittable, dir=dir}) - minetest.log("Setting sittable at "..minetest.pos_to_string(pos).." as not used") - if enable_usage_marking then - -- Set place as unused - npc.locations.mark_place_used(pos, npc.locations.USE_STATE.NOT_USED) - end - end - -end) - diff --git a/mods/advanced_npc/executable/programs/builtin/walk_to_pos.lua b/mods/advanced_npc/executable/programs/builtin/walk_to_pos.lua deleted file mode 100644 index c20201f..0000000 --- a/mods/advanced_npc/executable/programs/builtin/walk_to_pos.lua +++ /dev/null @@ -1,185 +0,0 @@ --- --- User: hfranqui --- Date: 3/12/18 --- Time: 9:00 AM --- - --- This program can be used to make the NPC walk from one --- position to another. If the optional parameter walkable_nodes --- is included, which is a table of node names, these nodes are --- going to be considered walkable for the algorithm to find a --- path. -npc.programs.register("advanced_npc:walk_to_pos", function(self, args) - -- Get arguments for this task - local use_access_node = true - if args.use_access_node ~= nil then - use_access_node = args.use_access_node - end - local end_pos, node_pos = npc.programs.helper.get_pos_argument(self, args.end_pos, use_access_node) - if end_pos == nil then - npc.log("WARNING", "Got nil position in 'walk_to_pos' using args.pos: "..dump(args.end_pos)) - return - end - local enforce_move = args.enforce_move or true - local optimize_one_node_distance = args.optimize_one_node_distance or true - local walkable_nodes = args.walkable - self.stepheight = 1.1 - self.object:set_properties(self) - - -- Round start_pos to make sure it can find start and end - local start_pos = vector.round(self.object:getpos()) - - -- Check if start_pos and end_pos are the same - local distance = vector.distance(start_pos, end_pos) - if distance < 0.75 then - -- Check if it was using access node, if it was, rotate NPC into that direction - if use_access_node == true and node_pos then - local yaw = minetest.dir_to_yaw(vector.direction(end_pos, node_pos)) - npc.programs.instr.execute(self, npc.programs.instr.default.ROTATE, {yaw = yaw}) - end - npc.log("WARNING", "walk_to_pos Found start_pos == end_pos") - return - elseif distance >= 0.75 and distance < 2 then - local yaw = minetest.dir_to_yaw(vector.direction(start_pos, end_pos)) - local target_pos = {x=end_pos.x, y=self.object:getpos().y, z=end_pos.z} - -- Check if it is using access node - if use_access_node == true and node_pos then - -- Walk to end_pos, rotate to node_pos - local final_yaw = minetest.dir_to_yaw(vector.direction(end_pos, node_pos)) - npc.programs.instr.execute(self, npc.programs.instr.default.WALK_STEP, - {yaw = yaw, target_pos=target_pos}) - npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {yaw=final_yaw}) - else - -- Walk to end_pos - npc.programs.instr.execute(self, npc.programs.instr.default.WALK_STEP, - {yaw = yaw, target_pos=target_pos}) - npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {}) - end - return - else - -- Set walkable nodes to empty if the parameter hasn't been used - if walkable_nodes == nil then - walkable_nodes = {} - end - - -- Find path - local path = npc.pathfinder.find_path(start_pos, end_pos, self, walkable_nodes) - - if path ~= nil and #path >= 1 then - - npc.log("INFO", "walk_to_pos Found path ("..dump(#path).." nodes) from " - ..minetest.pos_to_string(start_pos).." to: "..minetest.pos_to_string(end_pos)) - -- Add start pos to path - table.insert(path, 1, {pos=start_pos, type=2}) - -- Store path - self.npc_state.movement.walking.path = path - - -- Local variables - local door_opened = false - local steps_since_door_opened = 0 - local speed = npc.programs.const.speeds.two_nps_speed - - -- Set the command timer interval to half second. This is to account for - -- the increased speed when walking. - npc.programs.instr.execute(self, npc.programs.instr.default.SET_INTERVAL, {interval=0.5, freeze=true}) - - -- Set the initial last and target positions - --self.npc_state.movement.walking.target_pos = path[2].pos - - -- Add steps to path - for i = 1, #path do - - -- Do not add an extra step if reached the goal node - if (i+1) == #path then - -- Add direction to last node - local dir = vector.direction(path[i].pos, end_pos) - local yaw = minetest.dir_to_yaw(dir) - -- Add the last step - npc.exec.proc.enqueue(self, npc.programs.instr.default.WALK_STEP, - {yaw = minetest.dir_to_yaw(dir), speed = speed, target_pos = path[i+1].pos}) - -- Add stand animation at end - -- This is not the proper fix (and node_pos), but for now - -- it will avoid crashes - if use_access_node == true and node_pos then - --dir = npc.programs.helper.get_direction(end_pos, node_pos) - --minetest.log("end pos: "..dump(end_pos)) - --minetest.log("Node pos: "..dump(node_pos)) - yaw = minetest.dir_to_yaw(vector.direction(end_pos, node_pos)) - end - - -- If door is opened, close it - if door_opened then - npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {yaw=minetest.dir_to_yaw(vector.direction(path[i+1].pos, path[i].pos))}) - -- Close door - npc.exec.proc.enqueue(self, npc.programs.instr.default.USE_OPENABLE, { - pos=path[i].pos, command=npc.programs.const.node_ops.doors.command.CLOSE}) - door_opened = false - end - - -- Change dir if using access_node - npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {yaw = yaw}) - break - end - -- Get direction to move from path[i] to path[i+1] - local dir = vector.direction(path[i].pos, path[i+1].pos) - -- If a diagonal, increase speed by sqrt(2) - if dir.x ~= 0 and dir.z ~=0 then - speed = speed * math.sqrt(2) - end - -- Check if next node is a door, if it is, open it, then walk - if path[i+1].type == npc.pathfinder.node_types.openable then - -- Check if door is already open - local node = minetest.get_node(path[i+1].pos) - if npc.programs.helper.get_openable_node_state(node, path[i+1].pos, dir) - == npc.programs.const.node_ops.doors.state.CLOSED then - --minetest.log("Opening command to open door") - -- Stop to open door, this avoids misplaced movements later on - npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {yaw=minetest.dir_to_yaw(dir)}) - -- Open door - npc.exec.proc.enqueue(self, npc.programs.instr.default.USE_OPENABLE, - {pos=path[i+1].pos, dir=dir, command=npc.programs.const.node_ops.doors.command.OPEN}) - - door_opened = true - end - end - - -- Add walk command to command queue - npc.exec.proc.enqueue(self, npc.programs.instr.default.WALK_STEP, - {yaw = minetest.dir_to_yaw(dir), speed = speed, target_pos = path[i+1].pos}) - -- Restore speed to default - speed = npc.programs.const.speeds.two_nps_speed - -- Count the number of steps taken after opening a door - if door_opened then - steps_since_door_opened = steps_since_door_opened + 1 - end - - if door_opened then - if steps_since_door_opened == 2 then - -- Stop to close door, this avoids misplaced movements later on - npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {yaw=minetest.dir_to_yaw(vector.direction(path[i+1].pos, path[i].pos))}) - -- Close door - npc.exec.proc.enqueue(self, npc.programs.instr.default.USE_OPENABLE, { - pos=path[i].pos, command=npc.programs.const.node_ops.doors.command.CLOSE}) - -- Reset values - steps_since_door_opened = 0 - door_opened = false - end - end - end - - -- Return the command interval to default interval of 1 second - -- By default, always freeze. - npc.exec.proc.enqueue(self, npc.programs.instr.default.SET_INTERVAL, {interval=1}) - - else - -- Unable to find path - npc.log("WARNING", "walk_to_pos Unable to find path. Teleporting to: "..minetest.pos_to_string(end_pos)) - -- Check if movement is enforced - if enforce_move then - -- Move to end pos - self.object:moveto({x=end_pos.x, y=end_pos.y+1, z=end_pos.z}) - end - end - end -end) - diff --git a/mods/advanced_npc/executable/programs/builtin/wander.lua b/mods/advanced_npc/executable/programs/builtin/wander.lua deleted file mode 100644 index 866be02..0000000 --- a/mods/advanced_npc/executable/programs/builtin/wander.lua +++ /dev/null @@ -1,77 +0,0 @@ --- --- User: hfranqui --- Date: 4/6/18 --- Time: 9:18 AM --- Description: Wander program for Advanced NPC --- - --- Chance is a number from 0 to 100, indicates the chance --- a NPC will have of walking one step on a random direction -npc.programs.register("advanced_npc:wander", function(self, args) - local acknowledge_nearby_objs = args.acknowledge_nearby_objs - local max_acknowledge_time = args.max_acknowledge_time - local obj_search_radius = args.obj_search_radius or 3 - local chance = args.chance or 60 - local max_radius = args.max_radius or 10 - local speed = args.speed or npc.programs.const.speeds.one_nps_speed - local idle_chance = args.idle_chance or 10 - - -- First check if there's any object to acknowledge - local objs_found = false - if acknowledge_nearby_objs == true then - objs_found = npc.programs.instr.execute(self, "advanced_npc:idle:acknowledge_objects", { - obj_search_radius = obj_search_radius, - acknowledge_burnout = max_acknowledge_time - }) - end - -- Check if there was any object found - if objs_found == false then - -- No object found, proceed to wander - -- Calculate chance of walking - local calculated_chance = math.random(0, 100) - if calculated_chance < chance then - -- Store initial position - local init_pos = npc.exec.var.get(self, "init_pos") - if init_pos == nil then - init_pos = vector.round(self.object:getpos()) - npc.exec.var.put(self, "init_pos", init_pos) - end - -- Check if NPC has reached its maximum wandering radius - if vector.distance(init_pos, self.object:getpos()) >= max_radius then - - --minetest.log("Walking back") - - -- Walk back to the initial position - npc.exec.proc.enqueue(self, npc.programs.instr.default.WALK_STEP, { - yaw = minetest.dir_to_yaw(vector.direction(self.object:getpos(), init_pos)), - speed = speed - }) - npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {}) - else - minetest.log("Walking randomly") - -- Walk in a random direction - local npc_pos = self.object:getpos() - npc.exec.proc.enqueue(self, npc.programs.instr.default.WALK_STEP, { - dir = "random_orthogonal", - start_pos = npc_pos, - speed = speed - }) - npc.exec.proc.enqueue(self, npc.programs.instr.default.STAND, {}) - end - end - else - -- Object found, switch to idle - npc.exec.set_state_program(self, "advanced_npc:idle", {acknowledge_nearby_objs = true}, {}) - return - end - - -- Calculate idle chance - local calculated_idle_chance = math.random(0, 100) - if calculated_idle_chance < idle_chance then - npc.log("INFO", "Switching BACK to idle state") - -- Change to idle state process - npc.exec.set_state_program(self, "advanced_npc:idle", {acknowledge_nearby_objs = acknowledge_nearby_objs}, {}) - end - -end) - diff --git a/mods/advanced_npc/info/info.lua b/mods/advanced_npc/info/info.lua deleted file mode 100644 index dd5ec68..0000000 --- a/mods/advanced_npc/info/info.lua +++ /dev/null @@ -1,100 +0,0 @@ --- --- User: hfranqui --- Date: 5/3/18 --- Time: 9:30 PM --- Description: --- - -npc.info = { - names = {}, - textures = {}, - gift_items = {} -} - -npc.info.search_criteria = { - any_match = "any_match", - all_match = "all_match", - exact_match = "exact_match" -} - -function npc.info.register_name(name, tags) - if npc.info.names[name] ~= nil then - npc.log("WARNING", "Attempt to register an existing name: "..dump(name)) - return - end - npc.info.names[name] = tags -end - -local function search_using_tags(map, tags_to_search, search_criteria) - local result = {} - -- Do a very inefficient search - need to see how to organize this better - -- Traverse all tags for each name, one by one - -- minetest.log("Search: "..dump(tags_to_search)) - --minetest.log("Map: "..dump(map)) - for name, tags_for_name in pairs(map) do - -- minetest.log("Name: "..dump(name)..", "..dump(tags_for_name)) - local tags_found = 0 - -- For every tags array for a name, compare with tags_to_search - -- and count how many tags match - for i = 1, #tags_to_search do - for j = 1, #tags_for_name do - -- minetest.log("Tag[i]: "..tags_to_search[i]) - -- minetest.log("Tag[j]: "..tags_for_name[j]) - - if tags_to_search[i] == tags_for_name[j] then - tags_found = tags_found + 1 - end - end - end - -- minetest.log("Found: "..dump(tags_found)) - -- Check if exact match true is true. If it is, tags_for_name and - -- tags_to_search need to have same number of tags and all match - if tags_found > 0 then - if search_criteria == npc.info.search_criteria.exact_match then - if tags_found == #tags_to_search and tags_found == #tags_for_name then - result[#result + 1] = name - end - elseif search_criteria == npc.info.search_criteria.all_match then - if tags_found == #tags_to_search then - result[#result + 1] = name - end - -- minetest.log("Result: "..dump(result)) - elseif search_criteria == npc.info.search_criteria.any_match then - result[#result + 1] = name - end - end - end - -- minetest.log("Result: "..dump(result)) - return result -end - -function npc.info.get_names(tags_to_search, search_criteria) - return search_using_tags(npc.info.names, tags_to_search, search_criteria) -end - -function npc.info.register_texture(filename, tags) - if npc.info.textures[filename] ~= nil then - -- Compare tags, ignore same, add new - local existing_tags = npc.info.textures[filename] - for i = 1, #tags do - local unmatched_count = 0 - for j = 1, #existing_tags do - if tags[i] ~= existing_tags[j] then - unmatched_count = unmatched_count + 1 - end - end - if unmatched_count == #existing_tags then - -- Tag was not found, add it - npc.info.textures[filename][#existing_tags + 1] = tags[i] - end - end - npc.log("WARNING", "Attempt to register an existing texture with filename: "..dump(filename)) - return - end - npc.info.textures[filename] = tags -end - -function npc.info.get_textures(tags_to_search, search_criteria) - --minetest.log("Textures: "..dump(npc.info.textures)) - return search_using_tags(npc.info.textures, tags_to_search, search_criteria) -end diff --git a/mods/advanced_npc/init.lua b/mods/advanced_npc/init.lua deleted file mode 100755 index 18cbd6a..0000000 --- a/mods/advanced_npc/init.lua +++ /dev/null @@ -1,55 +0,0 @@ --- Advanced NPC mod by Zorman2000 -local path = minetest.get_modpath("advanced_npc") - --- Intllib -local S -if minetest.get_modpath("intllib") then - S = intllib.Getter() -else - S = function(s, a, ...) - if a == nil then - return s - end - a = {a, ...} - return s:gsub("(@?)@(%(?)(%d+)(%)?)", - function(e, o, n, c) - if e == ""then - return a[tonumber(n)] .. (o == "" and c or "") - else - return "@" .. o .. n .. c - end - end) - end -end -mobs.intllib = S - -dofile(path .. "/npc.lua") -dofile(path .. "/utils.lua") -dofile(path .. "/spawner.lua") -dofile(path .. "/relationships.lua") -dofile(path .. "/dialogue.lua") -dofile(path .. "/trade/trade.lua") -dofile(path .. "/trade/prices.lua") ---dofile(path .. "/actions/actions.lua") --- New program/instructions API -dofile(path .. "/executable/programs/api.lua") -dofile(path .. "/executable/helper.lua") -dofile(path .. "/executable/instructions/api.lua") -dofile(path .. "/executable/instructions/builtin_instructions.lua") --- Builtin programs -dofile(path .. "/executable/programs/builtin/follow.lua") -dofile(path .. "/executable/programs/builtin/idle.lua") -dofile(path .. "/executable/programs/builtin/wander.lua") -dofile(path .. "/executable/programs/builtin/walk_to_pos.lua") -dofile(path .. "/executable/programs/builtin/use_bed.lua") -dofile(path .. "/executable/programs/builtin/use_sittable.lua") -dofile(path .. "/executable/programs/builtin/internal_property_change.lua") -dofile(path .. "/executable/programs/builtin/node_query.lua") -dofile(path .. "/executable/locations.lua") -dofile(path .. "/executable/pathfinder.lua") -dofile(path .. "/executable/node_registry.lua") -dofile(path .. "/occupations/occupations.lua") --- Load random data definitions -dofile(path .. "/info/info.lua") - -print (S("[Mod] Advanced NPC loaded")) diff --git a/mods/advanced_npc/license.txt b/mods/advanced_npc/license.txt deleted file mode 100755 index 1d2bb1e..0000000 --- a/mods/advanced_npc/license.txt +++ /dev/null @@ -1,678 +0,0 @@ -Copyright (C) 2016-2017 Hector Franqui (zorman2000) - -Full GNU GPL v3: - - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - {project} Copyright (C) {year} {fullname} - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/mods/advanced_npc/locale/de.txt b/mods/advanced_npc/locale/de.txt deleted file mode 100755 index cffa840..0000000 --- a/mods/advanced_npc/locale/de.txt +++ /dev/null @@ -1,23 +0,0 @@ -# German Translation for mobs_npc mod -# Deutsche Übersetzung der mobs_npc Mod -# last update: 2016/June/10 -# Author: Xanthin - -#init.lua -[MOD] Mobs Redo 'NPCs' loaded = [MOD] Mobs Redo 'NPCs' geladen - -#npc.lua -NPC dropped you an item for gold! = NSC ließ dir für Gold einen Gegenstand fallen! -NPC stands still. = NSC bleibt stehen. -NPC will follow you. = NSC wird dir folgen. -Npc = Nsc - -#trader.lua -Trader @1 = Händler @1 -[NPC] Hello, @2, have a look at my wares. = [NSC] Hallo, @2, wirf einen Blick auf meine Waren. -Trader @1's stock: = Händler @1s Warenlager -Selection = Auswahl -Price = Preis -Payment = Bezahlung -Bought items = Ware -Trader = Händler \ No newline at end of file diff --git a/mods/advanced_npc/locale/template.txt b/mods/advanced_npc/locale/template.txt deleted file mode 100755 index 906ddd2..0000000 --- a/mods/advanced_npc/locale/template.txt +++ /dev/null @@ -1,21 +0,0 @@ -# Template for translations of mobs_npc mod -# last update: 2016/June/10 - -#init.lua -[MOD] Mobs Redo 'NPCs' loaded = - -#npc.lua -NPC dropped you an item for gold! = -NPC stands still. = -NPC will follow you. = -Npc = - -#trader.lua -Trader @1 = -[NPC] Hello, @2, have a look at my wares. = -Trader @1's stock: = -Selection = -Price = -Payment = -Bought items = -Trader = \ No newline at end of file diff --git a/mods/advanced_npc/mod.conf b/mods/advanced_npc/mod.conf deleted file mode 100755 index e1164a5..0000000 --- a/mods/advanced_npc/mod.conf +++ /dev/null @@ -1 +0,0 @@ -name = advanced_npc diff --git a/mods/advanced_npc/npc.lua b/mods/advanced_npc/npc.lua deleted file mode 100755 index 88d7442..0000000 --- a/mods/advanced_npc/npc.lua +++ /dev/null @@ -1,2353 +0,0 @@ --- Advanced NPC by Zorman2000 --- Based on original NPC by Tenplus1 - -local S = mobs.intllib - -npc = {} - --- Constants -npc.FEMALE = "female" -npc.MALE = "male" - -npc.age = { - adult = "adult", - child = "child" -} - -npc.INVENTORY_ITEM_MAX_STACK = 99 - -npc.ANIMATION_STAND_START = 0 -npc.ANIMATION_STAND_END = 79 -npc.ANIMATION_SIT_START = 81 -npc.ANIMATION_SIT_END = 160 -npc.ANIMATION_LAY_START = 162 -npc.ANIMATION_LAY_END = 166 -npc.ANIMATION_WALK_START = 168 -npc.ANIMATION_WALK_END = 187 -npc.ANIMATION_MINE_START = 189 -npc.ANIMATION_MINE_END =198 - -npc.direction = { - north = 0, - east = 1, - south = 2, - west = 3, - north_east = 4, - north_west = 5, - south_east = 6, - south_west = 7 -} - -npc.action_state = { - none = 0, - executing = 1, - interrupted = 2 -} - -npc.log_level = { - INFO = true, - WARNING = true, - ERROR = true, - DEBUG = false, - DEBUG_ACTION = false, - DEBUG_SCHEDULE = false, - EXECUTION = false -} - -npc.texture_check = { - timer = 0, - interval = 2 -} - ---------------------------------------------------------------------------------------- --- General functions ---------------------------------------------------------------------------------------- --- Logging -function npc.log(level, message) - if npc.log_level[level] then - minetest.log("[advanced_npc] "..level..": "..message) - end -end - --- NPC chat -function npc.chat(npc_name, player_name, message) - minetest.chat_send_player(player_name, npc_name..": "..message) -end - --- Simple wrapper over minetest.add_particle() --- Copied from mobs_redo/api.lua -function npc.effect(pos, amount, texture, min_size, max_size, radius, gravity, glow) - - radius = radius or 2 - min_size = min_size or 0.5 - max_size = max_size or 1 - gravity = gravity or -10 - glow = glow or 0 - - minetest.add_particlespawner({ - amount = amount, - time = 0.25, - minpos = pos, - maxpos = pos, - minvel = {x = -radius, y = -radius, z = -radius}, - maxvel = {x = radius, y = radius, z = radius}, - minacc = {x = 0, y = gravity, z = 0}, - maxacc = {x = 0, y = gravity, z = 0}, - minexptime = 0.1, - maxexptime = 1, - minsize = min_size, - maxsize = max_size, - texture = texture, - glow = glow, - }) -end - --- Gets name of player or NPC -function npc.get_entity_name(entity) - if entity:is_player() then - return entity:get_player_name() - else - return entity:get_luaentity().name - end -end - --- Returns the item "wielded" by player or NPC --- TODO: Implement NPC -function npc.get_entity_wielded_item(entity) - if entity:is_player() then - return entity:get_wielded_item() - end -end - ---------------------------------------------------------------------------------------- --- Spawning functions ---------------------------------------------------------------------------------------- --- These functions are used at spawn time to determine several --- random attributes for the NPC in case they are not already --- defined. On a later phase, pre-defining many of the NPC values --- will be allowed. - -local function get_random_name(gender, tags) - local search_tags = {gender} - if tags then - search_tags = { gender, unpack(tags) } - end - - local names = npc.info.get_names(search_tags, "all_match") - if next(names) ~= nil then - local i = math.random(#names) - return names[i] - else - -- Return a default name if no name was found - return "Anonymous" - end -end - -local function initialize_inventory() - return { - [1] = "", [2] = "", [3] = "", [4] = "", - [5] = "", [6] = "", [7] = "", [8] = "", - [9] = "", [10] = "", [11] = "", [12] = "", - [13] = "", [14] = "", [15] = "", [16] = "", - } -end - --- This function checks for "female" text on the texture name -local function is_female_texture(textures) - for i = 1, #textures do - if string.find(textures[i], "female") ~= nil then - return true - end - end - return false -end - -function npc.assign_gender_from_texture(self) - if is_female_texture(self.base_texture) then - return npc.FEMALE - else - return npc.MALE - end -end - -local function get_random_texture(gender, age) - - local textures = npc.info.get_textures({gender, age}, "all_match") - if next(textures) ~= nil then - local i = math.random(#textures) - return {textures[i]} - else - return {"default_"..gender..".png"} - end - --- local textures = {} --- local filtered_textures = {} --- -- Find textures by gender and age --- if age == npc.age.adult then --- --minetest.log("Registered: "..dump(minetest.registered_entities["advanced_npc:npc"])) --- textures = minetest.registered_entities["advanced_npc:npc"].texture_list --- elseif age == npc.age.child then --- textures = minetest.registered_entities["advanced_npc:npc"].child_texture --- end --- --- for i = 1, #textures do --- local current_texture = textures[i][1] --- if (gender == npc.MALE --- and string.find(current_texture, gender) --- and not string.find(current_texture, npc.FEMALE)) --- or (gender == npc.FEMALE --- and string.find(current_texture, gender)) then --- table.insert(filtered_textures, current_texture) --- end --- end --- --- -- Check if filtered textures is empty --- if filtered_textures == {} then --- return textures[1][1] --- end --- --- return filtered_textures[math.random(1,#filtered_textures)] -end - ---function npc.get_random_texture_from_array(age, gender, textures) --- local filtered_textures = {} --- --- for i = 1, #textures do --- local current_texture = textures[i] --- -- Filter by age --- if (gender == npc.MALE --- and string.find(current_texture, gender) --- and not string.find(current_texture, npc.FEMALE) --- 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 (gender == npc.FEMALE --- and string.find(current_texture, gender) --- 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)) --- ) --- ) then --- table.insert(filtered_textures, current_texture) --- end --- end --- --- -- Check if there are no textures --- if #filtered_textures == 0 then --- -- Return whole array for re-evaluation --- npc.log("DEBUG", "No textures found, returning original array") --- return textures --- end --- --- return filtered_textures[math.random(1, #filtered_textures)] ---end - --- 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 - if is_child then - return false - end - local chance = math.random(1,10) - return chance > 3 -end - --- Choose a maximum of two items that the NPC will have at spawn time --- These items are chosen from the favorite items list. -local function choose_spawn_items(self) - local number_of_items_to_add = math.random(1, 2) --- local number_of_items = #npc.FAVORITE_ITEMS[self.gender].phase1 --- --- for i = 1, number_of_items_to_add do --- npc.add_item_to_inventory( --- self, --- npc.FAVORITE_ITEMS[self.gender].phase1[math.random(1, number_of_items)].item, --- math.random(1,5) --- ) --- end - -- Add currency to the items spawned with. Will add 5-10 tier 3 - -- currency items - local currency_item_count = math.random(5, 10) - npc.add_item_to_inventory(self, npc.trade.prices.get_currency_itemstring("tier3"), currency_item_count) - - -- For test - --npc.add_item_to_inventory(self, "default:tree", 10) - --npc.add_item_to_inventory(self, "default:cobble", 10) - --npc.add_item_to_inventory(self, "default:diamond", 2) - --npc.add_item_to_inventory(self, "default:mese_crystal", 2) - --npc.add_item_to_inventory(self, "flowers:rose", 2) - --npc.add_item_to_inventory(self, "advanced_npc:marriage_ring", 2) - --npc.add_item_to_inventory(self, "flowers:geranium", 2) - --npc.add_item_to_inventory(self, "mobs:meat", 2) - --npc.add_item_to_inventory(self, "mobs:leather", 2) - --npc.add_item_to_inventory(self, "default:sword_stone", 2) - --npc.add_item_to_inventory(self, "default:shovel_stone", 2) - --npc.add_item_to_inventory(self, "default:axe_stone", 2) - - --minetest.log("Initial inventory: "..dump(self.inventory)) -end - --- Spawn function. Initializes all variables that the --- NPC will have and choose random, starting values -function npc.initialize(entity, pos, is_lua_entity, npc_stats, npc_info) - npc.log("INFO", "Initializing NPC at "..minetest.pos_to_string(pos)) - - -- Get variables - local ent = entity - if not is_lua_entity then - ent = entity:get_luaentity() - end - local occupation_name - if npc_info then - occupation_name = npc_info.occupation_name - end - - -- Avoid NPC to be removed by mobs_redo API - ent.remove_ok = false - - -- Flag that enables/disables right-click interaction - good for moments where NPC - -- can't be disturbed - ent.enable_rightclick_interaction = true - - -- Determine gender and age - -- If there's no previous NPC data, gender and age will be randomly chosen. - -- - Sex: Female or male will have each 50% of spawning - -- - Age: 90% chance of spawning adults, 10% chance of spawning children. - -- If there is previous data then: - -- - Sex: The unbalanced gender will get a 75% chance of spawning - -- - Example: If there's one male, then female will have 75% spawn chance. - -- - If there's male and female, then each have 50% spawn chance. - -- - Age: For each two adults, the chance of spawning a child next will be 50% - -- If there's a child for two adults, the chance of spawning a child goes to - -- 40% and keeps decreasing unless two adults have no child. - -- Use NPC stats if provided - if npc_stats then - -- Default chances - local male_s, male_e = 0, 50 - local female_s, female_e = 51, 100 - local adult_s, adult_e = 0, 85 - local child_s, child_e = 86, 100 - -- Determine gender probabilities - if npc_stats[npc.FEMALE].total > npc_stats[npc.MALE].total then - male_e = 75 - female_s, female_e = 76, 100 - elseif npc_stats[npc.FEMALE].total < npc_stats[npc.MALE].total then - male_e = 25 - female_s, female_e = 26, 100 - end - -- Determine age probabilities - if npc_stats["adult_total"] >= 2 then - if npc_stats["adult_total"] % 2 == 0 - and (npc_stats["adult_total"] / 2 > npc_stats["child_total"]) then - child_s,child_e = 26, 100 - adult_e = 25 - else - child_s, child_e = 61, 100 - adult_e = 60 - end - end - -- Get gender and age based on the probabilities - local gender_chance = math.random(1, 100) - local age_chance = math.random(1, 100) - local selected_gender = "" - local selected_age = "" - -- Select gender - if male_s <= gender_chance and gender_chance <= male_e then - selected_gender = npc.MALE - elseif female_s <= gender_chance and gender_chance <= female_e then - selected_gender = npc.FEMALE - end - -- Set gender for NPC - ent.gender = selected_gender - -- Select age - if adult_s <= age_chance and age_chance <= adult_e then - selected_age = npc.age.adult - elseif child_s <= age_chance and age_chance <= child_e then - selected_age = npc.age.child - ent.visual_size = { - x = 0.65, - y = 0.65 - } - ent.collisionbox = {-0.10,-0.50,-0.10, 0.10,0.40,0.10} - ent.is_child = true - -- For mobs_redo - ent.child = true - end - -- Store the selected age - ent.age = selected_age - - -- Set texture accordingly - local selected_texture = get_random_texture(selected_gender, selected_age) - --minetest.log("Selected texture: "..dump(selected_texture)) - -- Store selected texture due to the need to restore it later - ent.selected_texture = selected_texture - -- Set texture and base texture - ent.textures = {selected_texture} - ent.base_texture = {selected_texture} - elseif npc_info then - -- Attempt to assign gender from npc_info - if npc_info.gender then - ent.gender = npc_info.gender - else - local gender_chance = math.random(1,2) - ent.gender = npc.FEMALE - if gender_chance == 1 then - ent.gender = npc.MALE - end - end - -- Attempt to assign age from npc_info - if npc_info.age then - ent.age = npc_info.age - else - ent.age = npc.age.adult - end - else - -- Randomly choose gender, and spawn as adult - local gender_chance = math.random(1,2) - ent.gender = npc.FEMALE - if gender_chance == 1 then - ent.gender = npc.MALE - end - ent.age = npc.age.adult - end - - -- Initialize all gift data - ent.gift_data = { - -- Choose favorite items. Choose phase1 per default - favorite_items = npc.relationships.select_random_favorite_items(ent.gender, "phase1"), - -- Choose disliked items. Choose phase1 per default - disliked_items = npc.relationships.select_random_disliked_items(ent.gender), - -- Enable/disable gift item hints dialogue lines - enable_gift_items_hints = true - } - - -- Flag that determines if NPC can have a relationship - ent.can_have_relationship = can_have_relationships(ent.is_child) - - --ent.infotext = "Interested in relationships: "..dump(ent.can_have_relationship) - - -- Flag to determine if NPC can receive gifts - ent.can_receive_gifts = ent.can_have_relationship - - -- Initialize relationships object - ent.relationships = {} - - -- Determines if NPC is married or not - ent.is_married_to = nil - - -- Initialize dialogues - ent.dialogues = npc.dialogue.select_random_dialogues_for_npc(ent, "phase1") - - -- Declare NPC inventory - ent.inventory = initialize_inventory() - - -- Choose items to spawn with - choose_spawn_items(ent) - - -- Flags: generic booleans or functions that help drive functionality - ent.flags = {} - - -- Declare trade data - ent.trader_data = { - -- Type of trader - trader_status = npc.trade.get_random_trade_status(), - -- Current buy offers - buy_offers = {}, - -- Current sell offers - sell_offers = {}, - -- Items to buy change timer - change_offers_timer = 0, - -- Items to buy change timer interval - change_offers_timer_interval = 60, - -- Trading list: a list of item names the trader is expected to trade in. - -- It is mostly related to its occupation. - -- If empty, the NPC will revert to casual trading - -- If not, it will try to sell those that it have, and buy the ones it not. - trade_list = {}, - -- Custom trade allows to specify more than one payment - -- and a custom prompt (instead of the usual buy or sell prompts) - custom_trades = {} - } - - -- To model and control behavior of a NPC, advanced_npc follows an OS model - -- where it allows developers to create processes. These processes executes - -- programs, or a group of instructions that together make the NPC do something, - -- e.g. follow a player, use a furnace, etc. The model is: - -- - Each process has: - -- - An `execution context`, which is memory to store variables - -- - An `instruction queue`, which is a queue with the program instructions - -- to execute - -- - A `state`, whether the process is running or is paused - -- - Processes can specify whether they allow interruptions or not. They also - -- can opt to handle the interruption with a callback. The possible - -- interruptions are: - -- - Punch interruption - -- - Rightclick interruption - -- - Schedule interruption - -- - Only one process can run at a time. If another process is executed, - -- the currently running process is paused, and restored when the other ends. - -- - Processes can be enqueued, so once the executing process finishes, the - -- next one in the queue can be started. - -- - One process, called the `state` process, will run by default when no - -- processes are executing. - ent.execution = { - process_id = 0, - -- Queue of processes - process_queue = {}, - -- State process - state_process = {}, - -- Whether state process was changed or not - state_process_changed = false, - -- Whether to enable process execution or not - enable = true, - -- Interval to run process queue scheduler - scheduler_interval = 1, - -- Timer for next scheduler interval - scheduler_timer = 0, - -- Monitor environment executes timers and registered callbacks - monitor = { - timer = {}, - callback = { - to_execute = {} - }, - enabled = true - } - } - - -- NPC permanent storage for data - ent.data = {} - - -- State date - ent.npc_state = { - -- This table defines the types of interaction the NPC is performing - interaction = { - dialogues = { - is_in_dialogue = false, - in_dialogue_with = "", - in_dialogue_with_name = "" - }, - yaw_before_interaction = 0 - }, - punch = { - last_punch_time = 0, - }, - movement = { - is_idle = false, - is_sitting = false, - is_laying = false, - walking = { - is_walking = false, - path = {}, - target_pos = {}, - } - }, - following = { - is_following = false, - following_obj = "", - following_obj_name = "" - } - } - - -- This flag is checked on every step. If it is true, the rest of - -- Mobs Redo API is not executed - ent.freeze = nil - - -- This map will hold all the places for the NPC - -- Map entries should be like: "bed" = {x=1, y=1, z=1} - ent.places_map = {} - - -- Schedule data - ent.schedules = { - -- Flag to enable or disable the schedules functionality - enabled = true, - -- Lock for when executing a schedule - lock = -1, - -- Queue of programs in schedule to be enqueued - -- Used to calculate dependencies - dependency_queue = {}, - -- An array of schedules, meant to be one per day at some point - -- when calendars are implemented. Allows for only 7 schedules, - -- one for each day of the week - generic = {}, - -- An array of schedules, meant to be for specific dates in the - -- year. Can contain as many as possible. The keys will be strings - -- in the format MM:DD - date_based = {}, - -- The following holds the check parameters provided by the - -- current schedule - current_check_params = {} - } - - -- If occupation name given, override properties with - -- occupation values and initialize schedules - if occupation_name and occupation_name ~= "" and ent.age == npc.age.adult then - -- Set occupation name - ent.occupation_name = occupation_name - -- Override relevant values - npc.occupations.initialize_occupation_values(ent, occupation_name) - end - - -- Nametag is initialized to blank - ent.nametag = "" - - -- Set name - if npc_info and npc_info.name then - if npc_info.name.value then - ent.npc_name = npc_info.name.value - elseif npc_info.name.tags then - ent.npc_name = get_random_name(ent.gender, npc_info.name.tags) - else - ent.npc_name = get_random_name(ent.gender) - end - else - ent.npc_name = get_random_name(ent.gender) - end - - -- Set ID - ent.npc_id = tostring(math.random(1000, 9999))..":"..ent.npc_name - - -- Generate trade offers - npc.trade.generate_trade_offers_by_status(ent) - - -- Set initialized flag on - ent.initialized = true - --npc.log("WARNING", "Spawned entity: "..dump(ent)) - npc.log("INFO", "Successfully initialized NPC with name "..dump(ent.npc_name) - ..", gender: "..ent.gender..", is child: "..dump(ent.is_child) - ..", texture: "..dump(ent.textures)) - -- Refreshes entity - ent.object:set_properties(ent) -end - ---------------------------------------------------------------------------------------- --- Trading functions ---------------------------------------------------------------------------------------- -function npc.generate_trade_list_from_inventory(self) - local list = {} - for i = 1, #self.inventory do - list[npc.get_item_name(self.inventory[i])] = {} - end - self.trader_data.trade_list = list -end - -function npc.set_trading_status(self, status) - -- Stop, if any, the casual offer regeneration timer - npc.monitor.timer.stop(self, "advanced_npc:trade:casual_offer_regeneration") - --minetest.log("Trader_data: "..dump(self.trader_data)) - -- Set status - self.trader_data.trader_status = status - -- Check if status is casual - if status == npc.trade.CASUAL then - -- Register timer for changing casual trade offers - local timer_reg_success = npc.monitor.timer.register(self, "advanced_npc:trade:casual_offer_regeneration", 60, - function(self) - -- Re-select casual trade offers - npc.trade.generate_trade_offers_by_status(self) - end) - if timer_reg_success == false then - -- Activate timer instead - npc.monitor.timer.start(self, "advanced_npc:trade:casual_offer_regeneration") - end - end - - -- Re-generate trade offers - npc.trade.generate_trade_offers_by_status(self) -end - ---------------------------------------------------------------------------------------- --- Inventory functions ---------------------------------------------------------------------------------------- --- NPCs inventories are restrained to 16 slots. --- Each slot can hold one item up to 99 count. - --- Utility function to get item name from a string -function npc.get_item_name(item_string) - return ItemStack(item_string):get_name() -end - --- Utility function to get item count from a string -function npc.get_item_count(item_string) - return ItemStack(item_string):get_count() -end - --- Add an item to inventory. Returns true if add successful --- These function can be used to give items to other NPCs --- given that the "self" variable can be any NPC -function npc.add_item_to_inventory(self, item_name, count) - -- Check if NPC already has item - local existing_item = npc.inventory_contains(self, item_name) - if existing_item ~= nil and existing_item.item_string ~= nil then - -- NPC already has item. Get count and see - 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] = - npc.get_item_name(existing_item.item_string).." "..tostring(existing_count + count) - return true - else - --Find next free slot - for i = 1, #self.inventory do - if self.inventory[i] == "" then - -- Found slot, set item - self.inventory[i] = - item_name.." "..tostring((existing_count + count) - npc.INVENTORY_ITEM_MAX_STACK) - return true - end - end - -- No free slot found - return false - end - else - -- Find a free slot - for i = 1, #self.inventory do - if self.inventory[i] == "" then - -- Found slot, set item - self.inventory[i] = item_name.." "..tostring(count) - return true - end - end - -- No empty slot found - return false - end -end - --- Same add method but with itemstring for convenience -function npc.add_item_to_inventory_itemstring(self, item_string) - local item_name = npc.get_item_name(item_string) - local item_count = npc.get_item_count(item_string) - npc.add_item_to_inventory(self, item_name, item_count) -end - --- Checks if an item is contained in the inventory. Returns --- the item string or nil if not found -function npc.inventory_contains(self, item_name) - for key,value in pairs(self.inventory) do - if value ~= "" and string.find(value, item_name) then - return {slot=key, item_string=value} - end - end - -- Item not found - return nil -end - --- Removes the item from an NPC inventory and returns the item --- with its count (as a string, e.g. "default:apple 2"). Returns --- nil if unable to get the item. -function npc.take_item_from_inventory(self, item_name, count) - local existing_item = npc.inventory_contains(self, item_name) - if existing_item ~= nil then - -- Found item - local existing_count = npc.get_item_count(existing_item.item_string) - local new_count = existing_count - if existing_count - count < 0 then - -- Remove item first - self.inventory[existing_item.slot] = "" - -- TODO: Support for retrieving from next stack. Too complicated - -- and honestly might be unecessary. - return item_name.." "..tostring(new_count) - else - new_count = existing_count - count - if new_count == 0 then - self.inventory[existing_item.slot] = "" - else - self.inventory[existing_item.slot] = item_name.." "..new_count - end - return item_name.." "..tostring(count) - end - else - -- Not able to take item because not found - return nil - end -end - --- Same take method but with itemstring for convenience -function npc.take_item_from_inventory_itemstring(self, item_string) - local item_name = npc.get_item_name(item_string) - local item_count = npc.get_item_count(item_string) - npc.take_item_from_inventory(self, item_name, item_count) -end - ---------------------------------------------------------------------------------------- --- Flag functionality ---------------------------------------------------------------------------------------- --- TODO: Consider removing them as they are pretty simple and straight forward. --- Generic variables or function that help drive some functionality for the NPC. -function npc.add_flag(self, flag_name, value) - self.flags[flag_name] = value -end - -function npc.update_flag(self, flag_name, value) - self.flags[flag_name] = value -end - -function npc.get_flag(self, flag_name) - return self.flags[flag_name] -end - ---------------------------------------------------------------------------------------- --- Dialogue functionality ---------------------------------------------------------------------------------------- -function npc.start_dialogue(self, clicker, show_married_dialogue) - - -- Call dialogue function as normal - npc.dialogue.start_dialogue(self, clicker, show_married_dialogue) - - -- Check and update relationship if needed - npc.relationships.dialogue_relationship_update(self, clicker) - -end - ---------------------------------------------------------------------------------------- --- State functionality ---------------------------------------------------------------------------------------- --- All the self.npc_state variables are used to track the state of the NPC, and --- if necessary, restore it back in case of changes. The following functions allow --- to set different aspects of the state. -function npc.set_movement_state(self, args) - self.npc_state.movement.is_idle = args.is_idle or false - self.npc_state.movement.is_sitting = args.is_sitting or false - self.npc_state.movement.is_laying = args.is_laying or false - self.npc_state.movement.walking.is_walking = args.is_walking or false -end - - ---------------------------------------------------------------------------------------- --- Execution API ---------------------------------------------------------------------------------------- --- Methods for: --- - Enqueue a program --- - Set a program as the `state` process --- - Execute next process in queue --- - Pause/restore current process --- - Process scheduling --- - Get the current process data --- - Create, read, write and update variables in current process --- - Enqueue and execute instructions for the current process - - --- Global namespace -npc.exec = { - var = {}, - proc = { - instr = {} - } -} --- Private namespace -local _exec = { - proc = {} -} - --- Process states -npc.exec.proc.state = { - INACTIVE = "inactive", - RUNNING = "running", - EXECUTING = "executing", - PAUSED = "paused", - WAITING_USER_INPUT = "waiting_user_input", - READY = "ready" -} - -npc.exec.proc.instr.state = { - INACTIVE = "inactive", - EXECUTING = "executing", - INTERRUPTED = "interrupted" -} - - --- This function sets the interrupt options as given from the `interrupt_options` --- table. This table can have the following values: --- - allow_punch, boolean --- - allow_rightclick, boolean --- - allow_schedule, boolean -function npc.exec.create_interrupt_options(interrupt_options) - local interrupt_options = interrupt_options or {} - if next(interrupt_options) ~= nil then - local allow_punch = interrupt_options.allow_punch - local allow_rightclick = interrupt_options.allow_rightclick - local allow_schedule = interrupt_options.allow_schedule - - -- Set defaults - if allow_punch == nil then allow_punch = true end - if allow_rightclick == nil then allow_rightclick = true end - if allow_schedule == nil then allow_schedule = true end - - return { - allow_punch = allow_punch, - allow_rightclick = allow_rightclick, - allow_schedule = allow_schedule - } - else - return { - allow_punch = true, - allow_rightclick = true, - allow_schedule = true - } - end -end - -function _exec.get_new_process_id(self) - self.execution.process_id = self.execution.process_id + 1 - if self.execution.process_id > 10000 then - self.execution.process_id = 0 - end - return self.execution.process_id -end - -function _exec.create_process_entry(program_name, arguments, interrupt_options, is_state_program, process_id) - return { - id = process_id, - program_name = program_name, - arguments = arguments, - state = npc.exec.proc.state.INACTIVE, - execution_context = { - data = {}, - instr_interval = 1, - instr_timer = 0 - }, - instruction_queue = {}, - current_instruction = { - entry = {}, - state = npc.exec.proc.instr.state.INACTIVE, - pos = {} - }, - interrupt_options = npc.exec.create_interrupt_options(interrupt_options), - interrupted_process = {}, - is_state_process = is_state_program - } -end - --- This function creates a process for the given program, and --- places it into the process queue. -function npc.exec.enqueue_program(self, program_name, arguments, interrupt_options, is_state_program) - if is_state_program == nil then - is_state_program = false - end - if is_state_program == true then - npc.exec.set_state_program(self, program_name, arguments, interrupt_options) - -- Enqueue state process - self.execution.process_queue[#self.execution.process_queue + 1] = self.execution.state_process - else - -- Enqueue process - self.execution.process_queue[#self.execution.process_queue + 1] = - _exec.create_process_entry(program_name, arguments, interrupt_options, is_state_program, _exec.get_new_process_id(self)) - end -end - --- This function creates a state process. The state process will execute --- everytime there's no other process executing -function npc.exec.set_state_program(self, program_name, arguments, interrupt_options) - -- Disable monitor - give a chance to this state process to do what it has to do - self.execution.monitor.enabled = false - -- This flag signals the state process was changed and scheduler needs to consume - self.execution.state_process_changed = true - self.execution.state_process = { - program_name = program_name, - arguments = arguments, - state = npc.exec.proc.state.INACTIVE, - execution_context = { - data = {}, - instr_interval = 1, - instr_timer = 0 - }, - instruction_queue = {}, - current_instruction = { - entry = {}, - state = npc.exec.proc.instr.state.INACTIVE, - pos = {} - }, - interrupt_options = npc.exec.create_interrupt_options(interrupt_options), - is_state_process = true, - state_process_id = os.time() - } -end - --- Convenience function that returns first process in the queue -function npc.exec.get_current_process(self) - local result = self.execution.process_queue[1] - if result then - if next(result) == 0 then - return nil - end - end - return result -end - - --- This function always execute the process at the start of the process --- queue. When a process is stopped (because its instruction queue is empty --- or because the process itself stops), the entry is removed from the --- process queue, and thus the next process to execute will be the first one --- in the queue. - -function npc.exec.execute_process(self) - local current_process = self.execution.process_queue[1] - -- Execute current process - if current_process then - -- Restore scheduler interval - self.execution.scheduler_interval = 1 - if not current_process.is_state_process then - npc.log("EXECUTION", "NPC "..dump(self.npc_name).." is executing: "..dump(current_process.program_name)) - end - current_process.state = npc.exec.proc.state.EXECUTING - npc.programs.execute(self, current_process.program_name, current_process.arguments) - current_process.state = npc.exec.proc.state.RUNNING - -- Re-enable monitor - if current_process.is_state_process then - self.execution.monitor.enabled = true - end - end -end - - ---------------------------------------------------------------------------------------- --- Interruption algorithm ---------------------------------------------------------------------------------------- --- Interruption of an executing process can come from three sources: --- - NPC is left-clicked (or punch) --- - NPC is right-clicked (or rightclick) --- - Job scheduler has identified it is time to start a process --- When an interrupt happens, and another process needs to be executed, the --- workflow should be the following: --- 1. Enqueue the new process to be scheduled. --- a. If for some reason the process queue *has more than one* process, --- then the process will have to be enqueued with high priority, --- meaning next to the current process. --- 2. Pause the current executing process using `npc.exec.pause_process(self)` --- The new process will be executed by `npc.exec.pause_process()`. --- 3. The process finishes execution successfully, in which the scheduler --- will notice that and restore the interrupted process properly --- --- It is very important that a process is enqueued before pausing the current --- process. The pause will not work itself if that condition is not met - --- This function enqueues an array of processes right after the current process --- Each element in `program_entries` is a Lua table with three parameters: --- - program_name --- - arguments --- - interrupt_options -function _exec.priority_enqueue(self, program_entries) --- minetest.log("BEGIN PRIORITY ENQUEUE") --- minetest.log("Initial queue: "..dump(self.execution.process_queue)) - -- Check if the queue has more than one (current) process - if #self.execution.process_queue > 1 then - npc.log("EXECUTION", "More than One: "..dump(#self.execution.process_queue)) - -- Get current process entry - --local current_process = self.execution.process_queue[1] - -- Backup the current process queue --- local backup_queue = self.execution.process_queue --- minetest.log("Backup queue size: "..dump(#backup_queue)) --- -- Remove current process from backup_queue --- table.remove(backup_queue, 1) --- minetest.log("Backup queue size after dequeue: "..dump(#backup_queue)) --- -- Recreate queue, re-enqueue first process --- minetest.log("ENqueue") --- self.execution.process_queue[#self.execution.process_queue + 1] = current_process - npc.log("EXECUTION", "Queue size after enqueue: "..dump(#self.execution.process_queue)) - -- Enqueue the next processes with high priority (next to the current) - npc.log("EXECUTION", "Enqueue all new") - for i = 1, #program_entries do - if program_entries[i].is_state_program == true then - npc.exec.set_state_program(self, - program_entries[i].program_name, - program_entries[i].arguments, - program_entries[i].interrupt_options) - -- Enqueue state process - table.insert(self.execution.process_queue, i + 1, self.execution.state_process) - else - -- Enqueue normal process - table.insert( - self.execution.process_queue, i + 1, _exec.create_process_entry( - program_entries[i].program_name, - program_entries[i].arguments, - program_entries[i].interrupt_options, - program_entries[i].is_state_program, - _exec.get_new_process_id(self))) - end - end - --minetest.log("Backup queue after all new: "..dump(#backup_queue)) - else - npc.log("EXECUTION", "Only one process in queue") - -- There is only one process, therefore just enqueue every process - for i = 1, #program_entries do - if program_entries[i].is_state_program == true then - npc.exec.set_state_program(self, - program_entries[i].program_name, - program_entries[i].arguments, - program_entries[i].interrupt_options) - -- Enqueue state process - self.execution.process_queue[#self.execution.process_queue + 1] = self.execution.state_process - else - -- Enqueue normal process - self.execution.process_queue[#self.execution.process_queue + 1] = _exec.create_process_entry( - program_entries[i].program_name, - program_entries[i].arguments, - program_entries[i].interrupt_options, - program_entries[i].is_state_program, - _exec.get_new_process_id(self)) - end - end - end -end - --- This function handles a new process called by an interrupt. --- Will execute steps 1 and 2 of the above algorithm. The scheduler --- will take care of handling step 3. -function npc.exec.interrupt(self, new_program, new_arguments, interrupt_options) - -- Enqueue process with priority - _exec.priority_enqueue(self, - {[1] = {program_name=new_program, arguments=new_arguments, interrupt_options=interrupt_options}}) - --minetest.log("Pause") - minetest.log("Interrupted process: "..dump(self.execution.process_queue[1])) - -- Check process - if the instruction queue is empty, do not store - -- Pause current process - _exec.pause_process(self) - - local interrupted_process = self.execution.process_queue[1] - -- Dequeue process - table.remove(self.execution.process_queue, 1) - - -- Find if interrupted process has more instructions to execute - local has_more_instructions = next(interrupted_process.instruction_queue) ~= nil - --minetest.log("Process has more instructions: "..dump()) - if has_more_instructions then - -- Store interrupted process - local current_process = self.execution.process_queue[1] - current_process.interrupted_process = interrupted_process - end - -- Restore process scheduler interval - self.execution.scheduler_interval = 1 - --minetest.log("Execute") - -- Execute current process - npc.exec.execute_process(self) -end - --- This function pauses a process and sets its state as waiting for user input. --- The process scheduler and instruction executer will skip any process in this state. --- Once the process is ready to run again, the `npc.exec.set_ready_state()` function --- should be called, and execution will continue. -function npc.exec.set_input_wait_state(self) - npc.log("EXECUTION", "Setting input wait...") - if self.execution.process_queue[1] then - -- Call pause to do the instruction interruption - _exec.pause_process(self) - -- Change process state - self.execution.process_queue[1].state = npc.exec.proc.state.WAITING_USER_INPUT - end -end - -function npc.exec.set_ready_state(self) - if self.execution.process_queue[1] then - -- Change process state - self.execution.process_queue[1].state = npc.exec.proc.state.READY - end -end - --- If there is another process in the queue, this function pauses a --- currently executing process, then executes the -function _exec.pause_process(self, set_instruction_as_interrupted) - if #self.execution.process_queue == 1 then - npc.log("WARNING", "Unable to pause current process without anoher process in queue.\nCurrent queue: " - ..dump(self.execution.process_queue)) - return - end - - local current_process = self.execution.process_queue[1] - if current_process then - -- Check if there are instructions in the instruction queue - if next(current_process.instruction_queue) ~= nil then - -- If the instruction is interrupt, then dequeue that instruction :) - if current_process.instruction_queue[1].name == "advanced_npc:interrupt" then - -- Dequeue instruction - table.remove(current_process.instruction_queue, 1) - -- Check if there are more instructions - if next(current_process.instruction_queue) ~= nil then - -- Set entry - current_process.current_instruction.entry = current_process.instruction_queue[1] - -- Set state - current_process.current_instruction.state = npc.exec.proc.instr.state.INTERRUPTED - else - -- Set entry to blank as there is no other instruction - current_process.current_instruction.entry = {} - -- Set state - current_process.current_instruction.state = npc.exec.proc.instr.state.INACTIVE - end - else - -- Check current instruction - if current_process.current_instruction.entry - and current_process.current_instruction.state == npc.exec.proc.instr.state.EXECUTING then - -- This condition shouldn't become true - --and set_instruction_as_interrupted == true then - -- Change instruction state - current_process.current_instruction.state = npc.exec.proc.instr.state.INTERRUPTED - -- The following flow has been commented out as it doesn't gets executed. - --elseif set_instruction_as_interrupted == nil or set_instruction_as_interrupted == false then - -- current_process.current_instruction.state = npc.exec.proc.instr.state.INACTIVE - end - end - end - --minetest.log("Process after pausing: "..dump(current_process)) - -- Change process state - current_process.state = npc.exec.proc.state.PAUSED - end -end - --- This function restores the process that was running before the --- current one (the interrupted process). --- As it can only be runned with the interrupted process being enqueued --- before calling this function, this function is private and only --- used by the scheduler (which will enqueue the interrupted process before --- calling this) -function _exec.restore_process(self) - local current_process = self.execution.process_queue[1] - if current_process then - minetest.log("Restoring process: "..dump(current_process.program_name)) - -- Change process state - current_process.state = npc.exec.proc.state.RUNNING - -- Check if any instruction was interrupted - if current_process.current_instruction.entry - and current_process.current_instruction.state == npc.exec.proc.instr.state.INTERRUPTED then - -- TODO: Do we really want to restore position? - -- Restore position - --self.object:setpos(current_process.current_instruction.pos) - -- Execute instruction - minetest.log("Re-executing instruction: "..dump(current_process.current_instruction.entry.name)) - _exec.proc.execute(self, current_process.current_instruction.entry) - end - end -end - ---------------------------------------------------------------------------------------- --- Scheduler algorithm ---------------------------------------------------------------------------------------- --- This function will manage how processes are executed. This function needs --- to be called on a one second interval. The function will check: --- - If the process queue is emtpy and there is a state process, enqueue the --- the state process and execute --- - If the current process' instruction queue is empty: --- - If the process is a `state` process, and no other process is in queue, --- re-execute `state` process. --- - If the process is a `state` process and there is a process in queue, --- - Remove current process from queue --- - Store the current process entry into the `interrupted_process` field of --- the next process in queue. --- - Execute next process in queue --- - If the process is *not* a `state` process and there is a process entry in --- the `interrupted_process` field: --- - Remove current process from queue --- - Enqueue the entry in the `interrupted_process` field --- - Execute next process in the queue --- - If the instruction queue is not empty, continue -function npc.exec.process_scheduler(self) - npc.log("EXECUTION", "Current process queue size: "..dump(#self.execution.process_queue)) - -- minetest.log("Queue for "..dump(self.npc_name)) - -- for i = 1, #self.execution.process_queue do - -- minetest.log("["..dump(self.execution.process_queue[i].program_name).."]") - -- end - -- Check current process - local current_process = self.execution.process_queue[1] - if current_process then - -- Check current process state - if current_process.state == npc.exec.proc.state.EXECUTING then - -- Do not interrupt process while the process is enqueuing instructions - return - elseif current_process.state == npc.exec.proc.state.INACTIVE then - -- Execute process - npc.exec.execute_process(self) - elseif current_process.state == npc.exec.proc.state.READY then - -- Change state to running - current_process.state = npc.exec.proc.state.RUNNING - elseif current_process.state == npc.exec.proc.state.PAUSED then - -- Restore process - _exec.restore_process(self) - end - -- Check if instruction queue is empty - if current_process.instruction_queue and #current_process.instruction_queue == 0 - and current_process.state == npc.exec.proc.state.RUNNING then - -- Check if this is a state process - if current_process.is_state_process == true then - -- Check if the process queue only has this process - if #self.execution.process_queue == 1 then - -- Check if state process was changed - if self.execution.state_process_changed == true then - npc.log("EXECUTION", "Switching from state process " - ..dump(self.execution.process_queue[1].program_name) - .." to " - ..dump(self.execution.state_process.program_name)) - -- Dequeue this process, enqueue new one - self.execution.process_queue[1] = self.execution.state_process - -- Change flag back - self.execution.state_process_changed = false - end - -- Since this is a state process, re-execute - npc.log("EXECUTION", "Hi, executing state process "..dump(self.execution.process_queue[1].program_name)) - npc.exec.execute_process(self) - else - -- Changed state process check - an old state process could be enqueued, - -- but the state process was changed. If this is is true, ignore old - -- entry in the process queue. - local next_enqueued_process = self.execution.process_queue[2] - if self.execution.state_process_changed == true - and next_enqueued_process.id ~= current_process.id - and next_enqueued_process.is_state_process == true then - -- Assume every enqueued state process is old and discard - table.remove(self.execution.process_queue, 2) - -- Change flag back - self.execution.state_process_changed = false - else - -- Next process is not a state process, interrupt current state process - npc.log("EXECUTION", "Current process queue size: "..dump(#self.execution.process_queue)) - -- Pause current process - current_process.state = npc.exec.proc.state.PAUSED - -- Dequeue process - table.remove(self.execution.process_queue, 1) - -- Get next process in queue - local next_process = self.execution.process_queue[1] - -- Store the interrupted process in the next process - next_process.interrupted_process = current_process - end - -- Execute next process - npc.exec.execute_process(self) - end - else - npc.log("EXECUTION", "Current process name: "..dump(current_process.program_name)) - npc.log("EXECUTION", "Process queue size: "..dump(#self.execution.process_queue)) - npc.log("EXECUTION", "Current instrcution queue size: "..dump(#current_process.instruction_queue)) - npc.log("EXECUTION", "Current process state: "..dump(current_process.state)) - -- This is not a state process, check the interrupted process field - if next(current_process.interrupted_process) ~= nil then - npc.log("EXECUTION", "There is an interrupted process: "..dump(current_process.interrupted_process.program_name)) - npc.log("EXECUTION", "------------------------------") - npc.log("EXECUTION", "Is state process? "..dump(current_process.interrupted_process.is_state_process)) - npc.log("EXECUTION", "State process ID: "..dump(current_process.interrupted_process.state_process_id)) - npc.log("EXECUTION", "Valid state process ID: "..dump(self.execution.state_process.state_process_id)) - - if current_process.interrupted_process.is_state_process == true - and current_process.interrupted_process.state_process_id - and (current_process.interrupted_process.state_process_id < self.execution.state_process.state_process_id) then - -- Do nothing, just dequeue process - npc.log("EXECUTION", "Found an old state process that was interrupted.\n" - ..dump(current_process.interrupted_process.program_name).." WILL NOT be re-enqueued") - npc.log("EXECUTION", "Process "..dump(self.execution.process_queue[1].program_name) - .." is finished execution and will be dequeued") - -- Dequeue process - table.remove(self.execution.process_queue, 1) - -- Check if there are more processes - if #self.execution.process_queue > 0 then - -- Execute new process - npc.exec.execute_process(self) - end - return - end - - -- Dequeue process - table.remove(self.execution.process_queue, 1) - -- Re-enqueue the interrupted process - self.execution.process_queue[#self.execution.process_queue + 1] = current_process.interrupted_process - if #self.execution.process_queue > 1 then - -- Execute next process in queue - npc.exec.execute_process(self) - else - -- Execute next process in queue which is interrupted - _exec.restore_process(self) - end - else - npc.log("EXECUTION", "Process "..dump(self.execution.process_queue[1].program_name).." is finished execution") - -- Dequeue process - table.remove(self.execution.process_queue, 1) - -- Check if there are more processes - if #self.execution.process_queue > 0 then - -- Execute new process - npc.exec.execute_process(self) - end - end - end - end - else - -- Process queue is empty, enqueue state process if it is defined - if next(self.execution.state_process) ~= nil then - npc.log("EXECUTION", "NPC "..dump(self.npc_name).." is executing: "..dump(self.execution.state_process.program_name)) - self.execution.process_queue[#self.execution.process_queue + 1] = self.execution.state_process - -- Execute state process - npc.exec.execute_process(self) - end - end -end - ---------------------------------------------------------------------------------------- --- Process instructions functionality - enqueue and execute instructions --- for the currently executing process ---------------------------------------------------------------------------------------- --- This function enqueues a given instruction with its arguments --- in the current process' instruction queue. If var_name is given, --- results of this function are stored in the execution context with that --- var_key -function npc.exec.proc.enqueue(self, name, args, var_name) - local current_process = self.execution.process_queue[1] - if current_process then - current_process.instruction_queue[#current_process.instruction_queue + 1] = - {name=name, args=args, var_name=var_name} - end -end - --- Private function to execute a given instruction entry -function _exec.proc.execute(self, entry) - if entry ~= nil and next(entry) ~= nil then - local current_process = self.execution.process_queue[1] - if current_process then - -- Set current instruction params - current_process.current_instruction.entry = entry - current_process.current_instruction.pos = self.object:getpos() - current_process.current_instruction.state = npc.exec.proc.instr.state.EXECUTING - -- Execute current instruction - npc.log("EXECUTION", "Executing instruction: "..dump(entry.name)) - local result = npc.programs.instr.execute(self, entry.name, entry.args) - if entry.name == "advanced_npc:interrupt" then - -- Do not do anything else, the interrupt instruction was already - -- dequeued. - return - end - -- Check if var_name was given - if entry.var_name then - if npc.exec.var.get(self, entry.var_name) then - -- Update the value - npc.exec.var.set(self, entry.var_name, result) - else - -- Create new var with value - npc.exec.var.put(self, entry.var_name, result, false) - end - end - -- Dequeue from instruction queue - table.remove(current_process.instruction_queue, 1) - end - end --- minetest.log("END PRIVATE PROC EXEC") -end - --- This function executes the next instruction entry in the current --- process' instruction queue -function npc.exec.proc.execute(self) - --minetest.log("PROCESS EXECUTE BEGIN") - local current_process = self.execution.process_queue[1] - if current_process then - -- Get next instruction entry in queue - local entry = current_process.instruction_queue[1] - -- Execute instruction - _exec.proc.execute(self, entry) - end - --minetest.log("PROCESS EXECUTE END") -end - ---------------------------------------------------------------------------------------- --- Execution routine ---------------------------------------------------------------------------------------- --- This function is to be executed on each step() of the Lua entity --- Algorithm: --- 1. Increase the timer with dtime --- 2. If the timer has reached the interval, then: --- a. Reset the timer and execute `npc.exec.process_scheduler(self)` --- 3. Increase the current process' instruction timer with dtime --- 4. If the instruction timer has reached the interval, then: --- a. Reset the instruction timer and execute `noc.exec.proc.execute(self)` -function npc.exec.execution_routine(self, dtime) - local execution = self.execution - -- Increase process scheduler timer - execution.scheduler_timer = execution.scheduler_timer + dtime - -- Check if timer reached interval - if execution.scheduler_timer >= execution.scheduler_interval then - -- Reset timer - execution.scheduler_timer = 0 - npc.log("EXECUTION", "Executing scheduler for NPC "..dump(self.npc_name)) - -- Execute process scheduler - npc.exec.process_scheduler(self) - end - -- Get current process - local current_process = execution.process_queue[1] - if current_process ~= nil and current_process.execution_context ~= nil then - --minetest.log("STATE: "..dump(self.execution.process_queue[1].state)) - --minetest.log("PROCESS: "..dump(self.execution.process_queue[1])) - if current_process.state == npc.exec.proc.state.RUNNING then - -- Increase timer - current_process.execution_context.instr_timer = - current_process.execution_context.instr_timer + dtime - -- Check if timer reached interval - if current_process.execution_context.instr_timer - >= current_process.execution_context.instr_interval then - -- Reset timer - --minetest.log("HI, RESET") - current_process.execution_context.instr_timer = 0 - -- Check if NPC is walking - if self.npc_state.movement.walking.is_walking == true then - -- Move NPC to expected position to ensure not getting lost - local pos = self.npc_state.movement.walking.target_pos - if vector.distance(self.object:getpos(), pos) > 0.2 then - npc.log("INFO", "Corrected position for walking NPC "..dump(self.npc_name).." to "..minetest.pos_to_string(pos)) - self.object:moveto({x=pos.x, y=pos.y, z=pos.z}) - end - end - -- Execute next instruction in process' queue - npc.exec.proc.execute(self) - end - end - end -end - ---------------------------------------------------------------------------------------- --- Variable functionality - create, read, update and delete variables in the --- current process. --- IMPORTANT: These variables are deleted when the process is finished execution. --- For permanent storage, use npc.data.* functions. ---------------------------------------------------------------------------------------- --- This function adds a value to the execution context of the --- current process. --- Readonly defaults to false. Returns false if failed due to --- key-name conflict, or returns true if successful -function npc.exec.var.put(self, name, value, readonly) - -- Retrieve current process execution context - local current_process = self.execution.process_queue[1] - if current_process then - local context = current_process.execution_context - -- Check if variable exists - if context[name] ~= nil then - npc.log("ERROR", "Attempt to create new variable with name "..name.." failed".. - "due to variable already existing: "..dump(context[name])) - return false - end - context[name] = {value = value, readonly = readonly} - return true - end -end - --- Returns the value of a given key. If not found returns nil. -function npc.exec.var.get(self, name) - -- Retrieve current process execution context - local current_process = self.execution.process_queue[1] - if current_process then - local context = current_process.execution_context - local result = context[name] - if result == nil then - return nil - else - return result.value - end - end -end - -function npc.exec.var.get_or_put_if_nil(self, name, initial_value) - local var = npc.exec.var.get(self, name) - if var == nil then - npc.exec.var.put(self, name, initial_value) - return initial_value - else - return var - end -end - --- This function updates a value in the execution context. --- Returns false if the value is read-only or if key isn't found. --- Returns true if able to update value -function npc.exec.var.set(self, name, new_value) - -- Retrieve current process execution context - local current_process = self.execution.process_queue[1] - if current_process then - local context = current_process.execution_context - local var = context[name] - if var == nil then - return false - else - if var.readonly == true then - npc.log("ERROR", "Attempt to set value of readonly variable: "..name) - return false - end - var.value = new_value - end - return true - end -end - --- This function removes a variable from the execution context. --- If the key doesn't exist, returns nil, otherwise, returns --- the value removed. -function npc.exec.var.remove(self, name) - -- Retrieve current process execution context - local current_process = self.execution.process_queue[1] - if current_process then - local context = current_process.execution_context - local result = context[name] - if result == nil then - return nil - else - -- Clear variable - npc.exec.get_current_process(self).execution_context[name] = nil - return result - end - end -end - ---------------------------------------------------------------------------------------- --- Permanent storage functionality - create, read, update and delete variables --- in the NPC's permnanent storage. --- IMPORTANT: These variables are *NOT* deleted. Be careful what you store on it or --- the NPC object can grow in size very quickly. --- For temporary storage, use npc.exec.var.* functions. ---------------------------------------------------------------------------------------- --- Namespace -npc.data = {} - --- This function adds a value to the execution context of the --- current process. --- Readonly defaults to false. Returns false if failed due to --- key-name conflict, or returns true if successful -function npc.data.put(self, name, value, readonly) - -- Check if variable exists - if self.data[name] ~= nil then - npc.log("ERROR", "Attempt to create new variable with name "..name.." failed".. - "due to variable already existing: "..dump(self.data[name])) - return false - end - self.data[name] = {value = value, readonly = readonly} - return true -end - --- Returns the value of a given key. If not found returns nil. -function npc.data.get(self, name) - local result = self.data[name] - if result == nil then - return nil - else - return result.value - end -end - --- Convenience function for initializing a variable if nil -function npc.data.get_or_put_if_nil(self, name, initial_value) - local var = npc.data.get(self, name) - if var == nil then - npc.data.put(self, name, initial_value, false) - return initial_value - else - return var - end -end - --- This function updates a value in the execution context. --- Returns false if the value is read-only or if key isn't found. --- Returns true if able to update value -function npc.data.set(self, name, new_value) - local var = self.data[name] - if var == nil then - return false - else - if var.readonly == true then - npc.log("ERROR", "Attempt to set value of readonly variable: "..name) - return false - end - var.value = new_value - end - return true -end - --- This function removes a variable from the execution context. --- If the key doesn't exist, returns nil, otherwise, returns --- the value removed. -function npc.data.remove(self, name) - local result = self.data[name] - if result == nil then - return nil - else - -- Clear variable - self.data[name] = nil - return result - end -end - ---------------------------------------------------------------------------------------- --- Monitor API: API that executes timers and registered callbacks. --- - Timers can be registered by programs or by code in general, and can --- have a callback which is executed when the timer reaches the interval. --- - Callbacks are for programs, instructions and for interrupts (punch, right-click, --- and scheduled entries). The callback is executed after a program, --- instruction or interrupt is executed. --- IMPORTANT: Please, keep *all your callbacks* as light as possible. While useful, --- too many timers or callbacks can deteriorate performance, as all could --- run on NPC steps. ---------------------------------------------------------------------------------------- --- Namespace -npc.monitor = { - timer = { - registered = {} - }, - callback = { - registered = {}, - -- Constant values - type = { - program = "program", - instruction = "instruction", - interaction = "interaction", - }, - subtype = { - on_punch = "on_punch", - on_rightclick = "on_rightclick", - on_schedule = "on_schedule", - } - } -} - --- Register a timer. The timer can have the following arguments: --- - name: unique identifier for timer --- - interval: when timer reaches this value, callback will be executed --- - callback: function to be executed when timer reaches interval --- - initial_value: default is 0. Give this to start with a specific value -function npc.monitor.timer.register(name, interval, callback) - if npc.monitor.timer.registered[name] ~= nil then - npc.log("DEBUG", "Attempt to register an existing timer: "..dump(name)) - return false - else - local timer = { - interval = interval, - callback = callback - } - npc.monitor.timer.registered[name] = timer - end - return true -end - -function npc.monitor.timer.start(self, name, interval, args) - if self.execution.monitor.timer[name] then - npc.log("DEBUG", "Attempted to start already started timer: "..dump(name)) - return - end - local timer = npc.monitor.timer.registered[name] - if timer then - -- Activate timer by moving it into the active timer array - self.execution.monitor.timer[name] = { - value = 0, - interval = interval or timer.interval, - args = args - } - else - npc.log("DEBUG", "Attempted to start non-existent timer: "..dump(name)) - end -end - -function npc.monitor.timer.stop(self, name) - if self.execution.monitor.timer[name] == nil then - npc.log("DEBUG", "Attempted to stop already stopped timer: "..dump(name)) - return - end - local timer = self.execution.monitor.timer[name] - if timer then - -- Set timer for removal on next monitor execution routine - self.execution.monitor.timer[name].remove = true - else - npc.log("DEBUG", "Attempted to stop non-existent timer: "..dump(name)) - end -end - --- Name is the name of function for which callback is being registered. --- Use program or instruction name for corresponding programs or instructions, --- and "on_punch", "on_rightclick", "on_activate", "on_schedule" for interrupts -function npc.monitor.callback.register(name, type, subtype, callback) - -- Initialize type and subtype if they don't exist - if npc.monitor.callback.registered[type] == nil then - npc.monitor.callback.registered[type] = {} - end - if npc.monitor.callback.registered[type][subtype] == nil then - npc.monitor.callback.registered[type][subtype] = {} - end - -- Check if callback already exists - if npc.monitor.callback.registered[type][subtype][name] ~= nil then - npc.log("DEBUG", "Attempt to register an existing callback: "..dump(name)) - return - else - -- Register callback - npc.monitor.callback.registered[type][subtype][name] = callback - end -end - -function npc.monitor.callback.exists(type, subtype) - if npc.monitor.callback.registered[type] ~= nil then - if npc.monitor.callback.registered[type][subtype] ~= nil then - return next(npc.monitor.callback.registered[type][subtype]) ~= nil - end - end - return false -end - -function npc.monitor.callback.enqueue(self, type, subtype, name) - self.execution.monitor.callback.to_execute[#self.execution.monitor.callback.to_execute + 1] = { - name = name, - type = type, - subtype = subtype - } -end - -function npc.monitor.callback.enqueue_all(self, type, subtype) - for name,_ in pairs(npc.monitor.callback.registered[type][subtype]) do - self.execution.monitor.callback.to_execute[#self.execution.monitor.callback.to_execute + 1] = { - name = name, - type = type, - subtype = subtype - } - end -end - -function npc.monitor.execution_routine(self, dtime) - if self.execution.monitor.enabled == false then - return - end - -- Execute timers - traverse the array of active timers and increase - -- their respective values - for name,timer in pairs(self.execution.monitor.timer) do - if timer.remove == true then - self.execution.monitor.timer[name] = nil - else - -- Increase value - timer.value = timer.value + dtime - -- Check if interval is met - if timer.value >= timer.interval then - -- Reset value - timer.value = 0 - -- Execute callback - npc.monitor.timer.registered[name].callback(self, timer.args) - end - end - end - -- Execute callbacks - traverse array of callbacks to execute - for i = #self.execution.monitor.callback.to_execute, 1, -1 do - local callback = self.execution.monitor.callback.to_execute[i] - -- Execute callback - npc.monitor.callback.registered[callback.type][callback.subtype][callback.name](self) - -- Remove callback from the execute array - self.execution.monitor.callback.to_execute[i] = nil - end -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 --- /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 -npc.schedule = { - const = { - types = { - generic = "generic", - date_based = "date_based" - } - }, - entry = {} -} - -npc.schedule_properties = { - put_item = "put_item", - put_multiple_items = "put_multiple_items", - take_item = "take_item", - trader_status = "trader_status", - can_receive_gifts = "can_receive_gifts", - flag = "flag", - enable_gift_items_hints = "enable_gift_items_hints", - set_trade_list = "set_trade_list" -} - -local function get_time_in_hours() - return minetest.get_timeofday() * 24 -end - --- Create a schedule on a NPC. --- Schedule types: --- - 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 --- day of the week it represents as follows: --- - 1: Monday --- - 2: Tuesday --- - 3: Wednesday --- - 4: Thursday --- - 5: Friday --- - 6: Saturday --- - 7: Sunday --- - Date-based: The date parameter should be a --- string of the format "MM:DD". If it already --- exists, function retuns nil -function npc.schedule.create(self, schedule_type, date) - if schedule_type == npc.schedule.const.types.generic then - -- Check that there are no more than 7 schedules - if #self.schedules.generic == 7 then - -- Unable to add schedule - return nil - elseif #self.schedules.generic < 7 then - -- Check schedule doesn't exists already - if self.schedules.generic[date] == nil then - -- Add schedule - self.schedules.generic[date] = {} - else - -- Schedule already present - return nil - end - end - elseif schedule_type == npc.schedule.const.types.date then - -- Check schedule doesn't exists already - if self.schedules.date_based[date] == nil then - -- Add schedule - self.schedules.date_based[date] = {} - else - -- Schedule already present - return nil - end - end -end - -function npc.schedule.delete(self, schedule_type, date) - -- Delete schedule by setting entry to nil - self.schedules[schedule_type][date] = nil -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. - --- Actions is an array of actions and tasks that the NPC --- will perform at the scheduled time on the scheduled date -function npc.schedule.entry.put(self, schedule_type, date, time, check, actions) - -- Check that schedule for date exists - if self.schedules[schedule_type][date] ~= nil then - -- Add schedule entry - if check == nil then - self.schedules[schedule_type][date][time] = actions - else - self.schedules[schedule_type][date][time].check = check - end - else - -- No schedule found, need to be created for date - return nil - end -end - -function npc.schedule.entry.get(self, schedule_type, date, time) - -- Check if schedule for date exists - if self.schedules[schedule_type][date] ~= nil then - -- Return schedule - return self.schedules[schedule_type][date][time] - else - -- Schedule for date not found - return nil - end -end - -function npc.schedule.entry.set(self, schedule_type, date, time, check, actions) - -- Check schedule for date exists - if self.schedules[schedule_type][date] ~= nil then - -- Check that a schedule entry for that time exists - if self.schedules[schedule_type][date][time] ~= nil then - -- Set the new actions - if check == nil then - self.schedules[schedule_type][date][time] = actions - else - self.schedules[schedule_type][date][time].check = check - end - else - -- Schedule not found for specified time - return nil - end - else - -- Schedule not found for date - return nil - end -end - -function npc.schedule.entry.remove(self, schedule_type, date, time) - -- Check schedule for date exists - if self.schedules[schedule_type][date] ~= nil then - -- Remove schedule entry by setting to nil - self.schedules[schedule_type][date][time] = nil - else - -- Schedule not found for date - return nil - end -end - --- Execution routine for schedules --- For now, only one program per hour should be created by schedule -function npc.schedule.execution_routine(self, dtime) - - 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) then - -- Get integer part of time - time = (time) - (time % 1) - if not(time > self.schedules.lock or (time == 0 and self.schedules.lock == 23)) then - return - end - npc.log("INFO", "Time: "..dump(time)) - -- Activate lock to avoid more than one entry to this code - self.schedules.lock = time - -- Check if there is a schedule entry for this time - -- Note: Currently only one schedule is supported, for day 0 - local schedule = self.schedules.generic[0] - if schedule ~= nil then - -- Check if schedule for this time exists - if schedule[time] ~= nil then - -- Check if schedules are enabled, and interruptions by scheduler allowed by - -- current state/executing script - local current_process = self.execution.process_queue[1] - --minetest.log("CURRENT PROCESS name: "..dump(current_process.program_name)) - if current_process and current_process.interrupt_options.allow_scheduler == false then - -- Don't check schedules any further - return - end - -- Hold the programs to be enqueued - local programs_to_enqueue = {} - local entries_to_enqueue = {} - -- Check if a program should be enqueued or not - for i = 1, #schedule[time] do - -- 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 - if schedule[time][i].depends then - -- TODO: Fix dependency check issue - -- minetest.log("Programs to enqueue size: "..dump(programs_to_enqueue)) - -- minetest.log("i: "..dump(i)) - -- minetest.log("Dependency: "..dump(schedule[time][i].depends[1])) - -- minetest.log("programs to enqueue[1]: "..dump(programs_to_enqueue[1])) - -- for key,var in pairs(programs_to_enqueue) do - -- minetest.log("Key: "..dump(key)) - -- end - -- minetest.log("entries to enqueue[i]: "..dump(entries_to_enqueue[schedule[time][i].depends[1]])) - if entries_to_enqueue[schedule[time][i].depends[1]] ~= nil then - dependencies_met = true - else - dependencies_met = false - end - end - - -- minetest.log("Dependencies met for entry with name: "..dump(schedule[time][i].program_name)..": "..dump(dependencies_met)) - - -- Check for dependencies being met - if dependencies_met == nil or dependencies_met == true then - programs_to_enqueue[#programs_to_enqueue + 1] = schedule[time][i] - entries_to_enqueue[i] = i - else - npc.log("DEBUG", "Skipping schedule entry for time "..dump(time)..": "..dump(schedule[time][i])) - end - end - end - -- Enqueue all programs in programs_to_enqueue - if #programs_to_enqueue > 0 then - npc.log("INFO", "Enqueueing the following programs into process queue for time: "..dump(time).."\n" - ..dump(programs_to_enqueue)) - _exec.priority_enqueue(self, programs_to_enqueue) - end - -- Clear programs_to_enqueue - programs_to_enqueue = nil - entries_to_enqueue = nil - end - end - -- else - -- -- Check if lock can be released - -- if (time % 1) > dtime + 0.1 then - -- -- Release lock - -- self.schedules.lock = false - -- end - end - end -end - ---------------------------------------------------------------------------------------- --- NPC Lua object functions ---------------------------------------------------------------------------------------- --- The following functions make up the definitions of on_rightclick(), do_custom() --- and other functions that are assigned to the Lua entity definition --- This function is executed each time the NPC is loaded -function npc.after_activate(self) - --minetest.log("Self: "..dump(self)) - -- Reset animation - if self.npc_state then - if self.npc_state.movement then - if self.npc_state.movement.is_sitting == true then - npc.programs.instr.execute(self, npc.programs.instr.default.SIT, {pos=self.object:getpos()}) - elseif self.npc_state.movement.is_laying == true then - npc.programs.instr.execute(self, npc.programs.instr.default.LAY, {pos=self.object:getpos()}) - end - -- Reset yaw if available - if self.yaw_before_interaction then - self.object:setyaw(self.yaw_before_interaction) - end - end - end -end - --- This function is executed on right-click -function npc.rightclick_interaction(self, clicker) - -- Disable right click interaction per execution options - local current_process = self.execution.process_queue[1] - if current_process then - if current_process.interrupt_options.allow_rightclick == false then - npc.log("WARNING", "Attempted to right-click a NPC with disabled rightlick interaction") - return - end - end - - -- Enqueue callback if any - if npc.monitor.callback.exists(npc.monitor.callback.type.interaction, npc.monitor.callback.subtype.on_rightclick) then - -- Enqueue all right-click callbacks for execution - npc.monitor.callback.enqueue_all(self, - npc.monitor.callback.type.interaction, - npc.monitor.callback.subtype.on_rightclick) - end - - -- Store original yaw - self.yaw_before_interaction = self.object:getyaw() - - -- Rotate NPC toward its clicker - npc.dialogue.rotate_npc_to_player(self) - - -- Get information from clicker - local item = clicker:get_wielded_item() - local name = clicker:get_player_name() - - 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 - and item:to_table() ~= nil then - -- Get item name - local item = minetest.registered_items[item:get_name()] - local item_name = item.description - - -- Show dialogue to confirm that player is giving item as gift - npc.dialogue.show_yes_no_dialogue( - self, - "Do you want to give "..item_name.." to "..self.npc_name.."?", - npc.dialogue.POSITIVE_GIFT_ANSWER_PREFIX..item_name, - function() - npc.relationships.receive_gift(self, clicker) - end, - npc.dialogue.NEGATIVE_ANSWER_LABEL, - function() - npc.start_dialogue(self, clicker, true) - end, - name - ) - else - npc.start_dialogue(self, clicker, true) - end -end - -function npc.step(self, dtime) - if self.initialized == nil or self.initialized == false then - -- Initialize NPC if spawned using the spawn egg built in from - -- mobs_redo. This functionality will be removed in the future in - -- favor of a better manual spawning method with customization - npc.log("WARNING", "Initializing NPC from entity step. This message should only be appearing if an NPC is being spawned from inventory with egg!") - npc.initialize(self, self.object:getpos(), true) - self.tamed = false - self.owner = nil - else - -- NPC is initialized, check other variables - -- Check child texture issues - if self.is_child then - -- Check texture - npc.texture_check.timer = npc.texture_check.timer + dtime - if npc.texture_check.timer > npc.texture_check.interval then - -- Reset timer - npc.texture_check.timer = 0 - -- Set hornytimer to zero every 60 seconds so that children - -- don't grow automatically - self.hornytimer = 0 - -- Set correct textures - self.texture = {self.selected_texture} - self.base_texture = {self.selected_texture} - self.object:set_properties(self) - npc.log("WARNING", "Corrected textures on NPC child "..dump(self.npc_name)) - -- Set interval to large interval so this code isn't called frequently - npc.texture_check.interval = 60 - end - end - end - - -- Timer function for gifts - for i = 1, #self.relationships do - local relationship = self.relationships[i] - -- Gift timer check - if relationship.gift_timer_value < relationship.gift_interval then - relationship.gift_timer_value = relationship.gift_timer_value + dtime - elseif relationship.talk_timer_value < relationship.gift_interval then - -- Relationship talk timer - only allows players to increase relationship - -- by talking on the same intervals as gifts - relationship.talk_timer_value = relationship.talk_timer_value + dtime - else - -- Relationship decrease timer - if relationship.relationship_decrease_timer_value - < relationship.relationship_decrease_interval then - relationship.relationship_decrease_timer_value = - relationship.relationship_decrease_timer_value + dtime - else - -- Check if married to decrease half - if relationship.phase == "phase6" then - -- Avoid going below the marriage phase limit - if (relationship.points - 0.5) >= - npc.relationships.RELATIONSHIP_PHASE["phase5"].limit then - relationship.points = relationship.points - 0.5 - end - else - relationship.points = relationship.points - 1 - end - relationship.relationship_decrease_timer_value = 0 - end - end - end - - -- Execute monitor - npc.monitor.execution_routine(self, dtime) - - -- Execute process scheduler - npc.exec.execution_routine(self, dtime) - - -- Schedule timer - npc.schedule.execution_routine(self, dtime) - - return false--self.freeze -end - - ---------------------------------------------------------------------------------------- --- NPC Definition ---------------------------------------------------------------------------------------- ---mobs:register_mob("advanced_npc:npc", { --- type = "npc", --- passive = false, --- damage = 3, --- attack_type = "dogfight", --- attacks_monsters = true, --- -- Added group attack --- group_attack = true, --- -- Pathfinder = 2 to make NPCs more smart when attacking --- pathfinding = 2, --- hp_min = 10, --- hp_max = 20, --- armor = 100, --- collisionbox = {-0.20,0,-0.20, 0.20,1.8,0.20}, --- --collisionbox = {-0.20,-1.0,-0.20, 0.20,0.8,0.20}, --- --collisionbox = {-0.35,-1.0,-0.35, 0.35,0.8,0.35}, --- visual = "mesh", --- mesh = "character.b3d", --- drawtype = "front", --- textures = { --- {"npc_male1.png"}, --- {"npc_male2.png"}, --- {"npc_male3.png"}, --- {"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_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"}, --- {"npc_child_female1.png"}, --- }, --- makes_footstep_sound = true, --- sounds = {}, --- -- Added walk chance --- walk_chance = 20, --- -- Added stepheight --- stepheight = 0.6, --- walk_velocity = 1, --- run_velocity = 3, --- jump = false, --- drops = { --- {name = "default:wood", chance = 1, min = 1, max = 3}, --- {name = "default:apple", chance = 2, min = 1, max = 2}, --- {name = "default:axe_stone", chance = 5, min = 1, max = 1}, --- }, --- water_damage = 0, --- lava_damage = 2, --- light_damage = 0, --- --follow = {"farming:bread", "mobs:meat", "default:diamond"}, --- view_range = 15, --- owner = "", --- order = "follow", --- --order = "stand", --- fear_height = 3, --- animation = { --- speed_normal = 30, --- speed_run = 30, --- stand_start = 0, --- stand_end = 79, --- walk_start = 168, --- walk_end = 187, --- run_start = 168, --- run_end = 187, --- punch_start = 200, --- punch_end = 219, --- }, --- after_activate = function(self, staticdata, def, dtime) --- npc.after_activate(self) --- end, --- on_rightclick = function(self, clicker) --- -- Check if right-click interaction is enabled --- if self.enable_rightclick_interaction == true then --- npc.rightclick_interaction(self, clicker) --- end --- end, --- do_custom = function(self, dtime) --- return npc.step(self, dtime) --- end ---}) - -------------------------------------------------------------------------- --- Item definitions -------------------------------------------------------------------------- - ---mobs:register_egg("advanced_npc:npc", S("NPC"), "default_brick.png", 1) - --- compatibility ---mobs:alias_mob("mobs:npc", "advanced_npc:npc") - --- Marriage ring -minetest.register_craftitem("advanced_npc:marriage_ring", { - description = S("Marriage Ring"), - inventory_image = "marriage_ring.png", -}) - --- Marriage ring craft recipe -minetest.register_craft({ - output = "advanced_npc:marriage_ring", - recipe = { {"", "", ""}, - {"", "default:diamond", ""}, - {"", "default:gold_ingot", ""} }, -}) diff --git a/mods/advanced_npc/occupations/occupations.lua b/mods/advanced_npc/occupations/occupations.lua deleted file mode 100644 index 092f451..0000000 --- a/mods/advanced_npc/occupations/occupations.lua +++ /dev/null @@ -1,501 +0,0 @@ --- 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 gender --- -- 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.commands.cmd.USE_BED, args = { --- pos = npc.locations.data.bed.primary, --- action = npc.commands.const.beds.GET_UP --- } --- }, --- -- Walk to home inside --- [2] = {task = npc.commands.cmd.WALK_TO_POS, args = { --- end_pos = npc.locations.data.OTHER.HOME_INSIDE, --- walkable = {} --- }, --- chance = 75 --- }, --- -- Allow mobs_redo wandering --- [3] = {action = npc.commands.cmd.FREEZE, args = {freeze = false}} --- }, --- -- Schedule entry for 7 in the morning --- [8] = { --- -- Walk to outside of home --- [1] = {task = npc.commands.cmd.WALK_TO_POS, args = { --- end_pos = npc.locations.data.OTHER.HOME_OUTSIDE, --- walkable = {} --- }, --- chance = 75 --- }, --- -- Allow mobs_redo wandering --- [2] = {action = npc.commands.cmd.FREEZE, args = {freeze = false}} --- }, --- -- Schedule entry for 12 midday --- [12] = { --- -- Walk to a sittable node --- [1] = {task = npc.commands.cmd.WALK_TO_POS, args = { --- end_pos = {place_type=npc.locations.data.SITTABLE.PRIMARY, use_access_node=true}, --- walkable = {"cottages:bench"} --- }, --- chance = 75 --- }, --- -- Sit on the node --- [2] = {task = npc.commands.cmd.USE_SITTABLE, args = { --- pos = npc.locations.data.SITTABLE.PRIMARY, --- action = npc.commands.const.sittable.SIT --- }, --- depends = {1} --- }, --- -- Stay put into place --- [3] = {action = npc.commands.cmd.SET_INTERVAL, args = { --- freeze = true, --- interval = 35 --- }, --- depends = {2} --- }, --- [4] = {action = npc.commands.cmd.SET_INTERVAL, args = { --- freeze = true, --- interval = npc.commands.default_interval --- }, --- depends = {3} --- }, --- -- Get up from sit --- [5] = {action = npc.commands.cmd.USE_SITTABLE, args = { --- pos = npc.locations.data.SITTABLE.PRIMARY, --- action = npc.commands.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.commands.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.commands.cmd.WALK_TO_POS, args = { --- end_pos = npc.locations.data.OTHER.HOME_INSIDE, --- walkable = {} --- } --- }, --- -- Allow mobs_redo wandering --- [4] = {action = npc.commands.cmd.FREEZE, args = {freeze = false}} --- }, --- -- Schedule entry for 10 in the evening --- [22] = { --- [1] = {task = npc.commands.cmd.WALK_TO_POS, args = { --- end_pos = {place_type=npc.locations.data.bed.primary, use_access_node=true}, --- walkable = {} --- } --- }, --- -- Use bed --- [2] = {task = npc.commands.cmd.USE_BED, args = { --- pos = npc.locations.data.bed.primary, --- action = npc.commands.const.beds.LAY --- } --- }, --- -- Stay put on bed --- [3] = {action = npc.commands.cmd.FREEZE, args = {freeze = true}} --- } - } -} - - --- This function registers an occupation -function npc.occupations.register_occupation(name, def) - -- Register all textures per definition - if def.textures and next(def.textures) ~= nil then - -- These are in the format: {name="", tags={"tag1","tag2", ...}} - for i = 1, #def.textures do - npc.info.register_texture(def.textures[i].name, def.textures[i].tags) - end - end - - -- 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 - - -- Validate state program - if def.state_program then - if npc.programs.is_registered(def.state_program.name) == false then - npc.log("ERROR", "Unable to find program with name: "..dump(def.state_program.name)) - return - end - end - - -- 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 - -- 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 - -- Pick them from tags - if def.textures and table.getn(def.textures) > 0 then - -- Select a texture - local available_textures = npc.info.get_textures({self.gender, self.age, occupation_name}, "all_match") - - -- Set texture if it found for gender and age - -- If an array was returned, select a random texture from it - if next(available_textures) ~= nil then - self.selected_texture = available_textures[math.random(1, #available_textures)] - end - else - -- Try to choose a random texture - if exists - if next(npc.info.textures) ~= nil then - local available_textures = npc.info.get_textures({self.gender, self.age}, "all_match") - self.selected_texture = available_textures[math.random(1, #available_textures)] - else - -- Return a default texture - self.selected_texture = "default_"..self.gender..".png" - end - end - minetest.log("Result: "..dump(self.selected_texture)) - - -- Set texture and base texture - self.textures = {self.selected_texture} - self.base_texture = {self.selected_texture} - -- Refresh entity - self.object:set_properties(self) - - -- 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.dialogue.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.dialogue.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 state program - if def.state_program then - npc.exec.set_state_program(self, - def.state_program.name, - def.state_program.args, - def.state_program.interrupt_options) - npc.log("INFO", "Successfully set state program "..dump(def.state_program.name)) - 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.schedule.create(self, npc.schedule.const.types.generic, 0) - -- Traverse schedules - for time, entries in pairs(def.schedules_entries) do - -- Add schedule entry for each time - npc.schedule.entry.put(self, npc.schedule.const.types.generic, 0, time, nil, entries) - end - end - - npc.log("INFO", "Successfully initialized NPC with occupation values") - -end diff --git a/mods/advanced_npc/relationships.lua b/mods/advanced_npc/relationships.lua deleted file mode 100644 index 884a335..0000000 --- a/mods/advanced_npc/relationships.lua +++ /dev/null @@ -1,680 +0,0 @@ --- Relationships code for Advanced NPC by Zorman2000 ---------------------------------------------------------------------------------------- --- Gift and relationship system ---------------------------------------------------------------------------------------- --- Each NPCs has 2 favorite and 2 disliked items. These items are chosen at spawn --- time and will be re-chosen when the age changes (from child to adult, for example). --- The items are chosen from the npc.FAVORITE_ITEMS table, and depends on gender and age. --- A player, via right-click, or another NPC, can gift an item to a NPC. In the case --- of the player, the player will give one of the currently wielded item. Gifts can be --- given only once per some time period, the NPC will reject the given item if still --- the period isn't over. --- If the NPC is neutral on the item (meanining it's neither favorite or disliked), it --- is possible it will not accept it, and the relationship the giver has with the NPC --- will be unchanged. --- In the other hand, if the item given its a favorite, the relationship points the NPC --- has with giver will increase by a given amount, depending on favoriteness. Favorite 1 --- will increase the relationship by 2 * npc.ITEM_GIFT_EFFECT, and favorite 2 only by --- npc.ITEM_GIFT_EFFECT. Similarly, if the item given is a disliked item, the NPC will --- not take it, and its relationship points with the giver will decrease by 2 or 1 times --- npc.ITEM_GIFT_EFFECT. - -local S = mobs.intllib - -npc.relationships = {} - --- Constants -npc.relationships.ITEM_GIFT_EFFECT = 2.5 - --- Expected values for these are 720 each respectively -npc.relationships.GIFT_TIMER_INTERVAL = 360 -npc.relationships.RELATIONSHIP_DECREASE_TIMER_INTERVAL = 720 - -npc.relationships.RELATIONSHIP_PHASE = {} --- Define phases -npc.relationships.RELATIONSHIP_PHASE["phase1"] = {limit = 10} -npc.relationships.RELATIONSHIP_PHASE["phase2"] = {limit = 25} -npc.relationships.RELATIONSHIP_PHASE["phase3"] = {limit = 45} -npc.relationships.RELATIONSHIP_PHASE["phase4"] = {limit = 70} -npc.relationships.RELATIONSHIP_PHASE["phase5"] = {limit = 100} - -npc.relationships.GIFT_ITEM_LIKED = "liked" -npc.relationships.GIFT_ITEM_DISLIKED = "disliked" -npc.relationships.GIFT_ITEM_HINT = "hint" -npc.relationships.GIFT_ITEM_RESPONSE = "response" - --- Favorite and disliked items tables -npc.relationships.gift_items = { - liked = { - female = { - ["phase1"] = {}, - ["phase2"] = {}, - ["phase3"] = {}, - ["phase4"] = {}, - ["phase5"] = {}, - ["phase6"] = {} - }, - male = { - ["phase1"] = {}, - ["phase2"] = {}, - ["phase3"] = {}, - ["phase4"] = {}, - ["phase5"] = {}, - ["phase6"] = {} - } - }, - disliked = { - female = {}, - male = {} - } -} - -npc.relationships.DEFAULT_RESPONSE_NO_GIFT_RECEIVE = -"Thank you, but I don't need anything for now." - --- Married NPC dialogue definition -npc.relationships.MARRIED_NPC_DIALOGUE = { - text = "Hi darling!", - is_married_dialogue = true, - responses = { - [1] = { - text = "Let's talk!", - action_type = "function", - response_id = 1, - action = function(self, player) - npc.start_dialogue(self, player, false) - end - }, - [2] = { - text = "Honey, can you wait for me here?", - action_type = "function", - response_id = 2, - action = function(self, player) - self.order = "stand" - npc.chat(self.npc_name, player:get_player_name(), - S("Ok dear, I will wait here for you.")) - end - }, - [3] = { - text = "Please, come with me!", - action_type = "function", - response_id = 3, - action = function(self, player) - self.order = "follow" - npc.chat(self.npc_name, player:get_player_name(), S("Ok, let's go!")) - end - } - } -} - --- Function to get relationship phase -function npc.relationships.get_relationship_phase_by_points(points) - if points > npc.relationships.RELATIONSHIP_PHASE["phase5"].limit then - return "phase6" - elseif points > npc.relationships.RELATIONSHIP_PHASE["phase4"].limit then - return "phase5" - elseif points > npc.relationships.RELATIONSHIP_PHASE["phase3"].limit then - return "phase4" - elseif points > npc.relationships.RELATIONSHIP_PHASE["phase2"].limit then - return "phase3" - elseif points > npc.relationships.RELATIONSHIP_PHASE["phase1"].limit then - return "phase2" - else - return "phase1" - end -end - --- Registration functions ------------------------------------------------------------------------------ --- Items can be registered to be part of the gift system using the --- below function. The def is the following: --- { --- dialogues = { --- liked = { --- -- ^ This is an array of the following table: --- [1] = {dialogue_type="", gender="", text=""} --- -- ^ dialogue_type: defines is this is a hint or a response. --- -- valid values are: "hint", "response" --- -- gender: valid values are: "male", female" --- -- text: the dialogue text --- }, --- disliked = { --- -- ^ This is an array with the same type of tables as above --- } --- } --- } - -function npc.relationships.register_favorite_item(item_name, phase, gender, def) - local dialogues = {} - -- Register dialogues based on the hints and responses - -- Liked - for i = 1, #def.hints do - table.insert(dialogues, { - text = def.hints[i], - tags = {phase, item_name, gender, - npc.dialogue.tags.GIFT_ITEM_HINT, npc.dialogue.tags.GIFT_ITEM_LIKED} - }) - end - for i = 1, #def.responses do - table.insert(dialogues, { - text = def.responses[i], - tags = {phase, item_name, gender, - npc.dialogue.tags.GIFT_ITEM_RESPONSE, npc.dialogue.tags.GIFT_ITEM_LIKED} - }) - end - -- Register all dialogues - for i = 1, #dialogues do - npc.dialogue.register_dialogue(dialogues[i]) - end - - -- Insert item into table - table.insert(npc.relationships.gift_items.liked[gender][phase], item_name) -end - -function npc.relationships.register_disliked_item(item_name, gender, def) - local dialogues = {} - -- Register dialogues based on the hints and responses - -- Liked - for i = 1, #def.hints do - table.insert(dialogues, { - text = def.hints[i], - tags = {item_name, gender, - npc.dialogue.tags.GIFT_ITEM_HINT, npc.dialogue.tags.GIFT_ITEM_UNLIKED} - }) - end - for i = 1, #def.responses do - table.insert(dialogues, { - text = def.responses[i], - tags = {item_name, gender, - npc.dialogue.tags.GIFT_ITEM_RESPONSE, npc.dialogue.tags.GIFT_ITEM_UNLIKED} - }) - end - -- Register all dialogues - for i = 1, #dialogues do - npc.dialogue.register_dialogue(dialogues[i]) - end - - -- Insert item into table - table.insert(npc.relationships.gift_items.disliked[gender], item_name) -end - - -function npc.relationships.get_dialogues_for_gift_item(item_name, dialogue_type, item_type, gender, phase) - local tags = { - [1] = item_name, - [2] = dialogue_type, - [3] = item_type, - [4] = gender - } - if phase ~= nil then - tags[5] = phase - end - npc.log("DEBUG","Searching with tags: "..dump(tags)) - - return npc.dialogue.search_dialogue_by_tags(tags, true) -end - --- Returns the response message for a given item -function npc.relationships.get_response_for_favorite_item(item_name, gender, phase) - local items = npc.FAVORITE_ITEMS.female - if gender == npc.MALE then - items = npc.FAVORITE_ITEMS.male - end - - for i = 1, #items[phase] do - if items[phase][i].item == item_name then - return items[phase][i].response - end - end - return nil -end - --- Returns the response message for a disliked item -function npc.relationships.get_response_for_disliked_item(item_name, gender) - local items = npc.DISLIKED_ITEMS.female - if gender == npc.MALE then - items = npc.DISLIKED_ITEMS.male - end - - for i = 1, #items do - minetest.log(dump(items[i])) - if items[i].item == item_name then - --minetest.log("Returning: "..dump(items[i].response)) - return items[i].response - end - end - return nil -end - --- Gets the item hint for a favorite item -function npc.relationships.get_hint_for_favorite_item(item_name, gender, phase) - for i = 1, #npc.FAVORITE_ITEMS[gender][phase] do - if npc.FAVORITE_ITEMS[gender][phase][i].item == item_name then - return npc.FAVORITE_ITEMS[gender][phase][i].hint - end - end - return nil -end - --- Gets the item hint for a disliked item -function npc.relationships.get_hint_for_disliked_item(item_name, gender) - for i = 1, #npc.DISLIKED_ITEMS[gender] do - if npc.DISLIKED_ITEMS[gender][i].item == item_name then - return npc.DISLIKED_ITEMS[gender][i].hint - end - end - return nil -end - - --- Relationship functions ------------------------------------------------------------------------------ - --- This function selects two random items from the npc.favorite_items table --- It checks for gender and phase for choosing the items -function npc.relationships.select_random_favorite_items(gender, phase) - local result = {} - local items = {} - - -- -- Filter gender - -- if gender == npc.FEMALE then - -- items = npc.FAVORITE_ITEMS.female - -- else - -- items = npc.FAVORITE_ITEMS.male - -- end - - -- Select the phase - -- items = items[phase] - items = npc.relationships.gift_items.liked[gender][phase] - if items and next(items) ~= nil then - result.fav1 = items[math.random(1, #items)] - result.fav2 = items[math.random(1, #items)] - end - return result -end - --- This function selects two random items from the npc.disliked_items table --- It checks for gender for choosing the items. They stay the same for all --- phases -function npc.relationships.select_random_disliked_items(gender) - local result = {} - local items = {} - - -- -- Filter gender - -- if gender == npc.FEMALE then - -- items = npc.DISLIKED_ITEMS.female - -- else - -- items = npc.DISLIKED_ITEMS.male - -- end - items = npc.relationships.gift_items.disliked[gender] - if items and next(items) ~= nil then - result.dis1 = items[math.random(1, #items)] - result.dis2 = items[math.random(1, #items)] - end - return result -end - --- Creates a relationship with a given player or NPC -local function create_relationship(self, clicker_name) - local count = #self.relationships - self.relationships[count + 1] = { - -- Player or NPC name with whom the relationship is with - name = clicker_name, - -- Relationship points - points = 0, - -- Relationship phase, used for items and for phrases - phase = "phase1", - -- How frequent can the NPC receive a gift - gift_interval = npc.relationships.GIFT_TIMER_INTERVAL, - -- Current timer count since last gift - gift_timer_value = 0, - -- The amount of time without providing gift or talking that will decrease relationship points - relationship_decrease_interval = npc.relationships.RELATIONSHIP_DECREASE_TIMER_INTERVAL, - -- Current timer count for relationship decrease - relationship_decrease_timer_value = 0, - -- Current timer count since last time player talked to NPC - talk_timer_value = 0 - } -end - --- Returns a relationship points -local function get_relationship_points(self, clicker_name) - for i = 1, #self.relationships do - if self.relationships[i].name == clicker_name then - return self.relationships[i].points - end - end - return nil -end - --- Updates relationship with given points -local function update_relationship(self, clicker_name, modifier) - for i = 1, #self.relationships do - if self.relationships[i].name == clicker_name then - self.relationships[i].points = self.relationships[i].points + modifier - local current_phase = self.relationships[i].phase - self.relationships[i].phase = - npc.relationships.get_relationship_phase_by_points(self.relationships[i].points) - if current_phase ~= self.relationships[i].phase then - -- Re-select favorite items per new phase - self.gift_data.favorite_items = - npc.relationships.select_random_favorite_items(self.gender, self.relationships[i].phase) - -- Re-select dialogues per new - self.dialogues = - npc.dialogue.select_random_dialogues_for_npc(self, - self.relationships[i].phase) - return true - end - return false - end - end - -- Relationship not found, huge error - return nil -end - --- Checks if a relationship with given player or NPC exists -local function check_relationship_exists(self, clicker_name) - for i = 1, #self.relationships do - if self.relationships[i].name == clicker_name then - return true - end - end - return false -end - --- Returns the relationship phase given the name of the player -function npc.relationships.get_relationship_phase(self, clicker_name) - for i = 1, #self.relationships do - if clicker_name == self.relationships[i].name then - return self.relationships[i].phase - end - end - return nil -end - --- Checks if NPC can receive gifts -local function check_npc_can_receive_gift(self, clicker_name) - for i = 1, #self.relationships do - if self.relationships[i].name == clicker_name then - -- Checks avoid married NPC to receive from others - if self.is_married_to == nil - or (self.is_married ~= nil and self.is_married_to == clicker_name) then - return self.relationships[i].gift_timer_value >= self.relationships[i].gift_interval - else - return false - end - end - end - -- Not found - return nil -end - --- Checks if relationship can be updated by talking -local function check_relationship_by_talk_timer_ready(self, clicker_name) - for i = 1, #self.relationships do - if self.relationships[i].name == clicker_name then - return self.relationships[i].talk_timer_value >= self.relationships[i].gift_interval - end - end - -- Not found - return nil -end - --- Resets the gift timer -local function reset_gift_timer(self, clicker_name) - for i = 1, #self.relationships do - if self.relationships[i].name == clicker_name then - self.relationships[i].gift_timer_value = 0 - self.relationships[i].relationship_decrease_timer_value = 0 - return - end - end -end - --- Resets the talk timer -local function reset_talk_timer(self, clicker_name) - for i = 1, #self.relationships do - if self.relationships[i].name == clicker_name then - self.relationships[i].talk_timer_value = 0 - return - end - end -end - --- Resets the relationshop decrease timer -local function reset_relationship_decrease_timer(self, clicker_name) - for i = 1, #self.relationships do - if self.relationships[i].name == clicker_name then - self.relationships[i].relationship_decrease_timer_value = 0 - return - end - end -end - --- Gifts functions ---------------------------------------------------------------------------------------- - --- Displays message and hearts depending on relationship level -local function show_receive_gift_reaction(self, item_name, modifier, clicker_name, phase_change) - local points = get_relationship_points(self, clicker_name) - - local pos = self.object:getpos() - -- Positive modifier (favorite items) reactions - if modifier >= 0 then - local phase = npc.relationships.get_relationship_phase_by_points(points) - if phase == "phase3" then - npc.effect({x = pos.x, y = pos.y + 1, z = pos.z}, 2, "heart.png") - elseif phase == "phase4" then - npc.effect({x = pos.x, y = pos.y + 1, z = pos.z}, 4, "heart.png") - elseif phase == "phase5" then - npc.effect({x = pos.x, y = pos.y + 1, z = pos.z}, 6, "heart.png") - elseif phase == "phase6" then - npc.effect({x = pos.x, y = pos.y + 1, z = pos.z}, 8, "heart.png") - end - if phase_change then - local number_code = phase:byte(phase:len()) - 1 - phase = "phase"..string.char(number_code) - end - -- Send message - -- TODO: There might be an error with getting the message... - --minetest.log("Item_name: "..dump(item_name)..", gender: "..dump(self.gender)..", phase: "..dump(phase)) - local dialogues_found = npc.relationships.get_dialogues_for_gift_item( - item_name, - npc.dialogue.tags.GIFT_ITEM_RESPONSE, - npc.dialogue.tags.GIFT_ITEM_LIKED, - self.gender, - phase) - for _, item_dialogue in pairs(dialogues_found) do - npc.chat(self.npc_name, clicker_name, item_dialogue.text) - end - -- Disliked items reactions - elseif modifier < 0 then - npc.effect({x = pos.x, y = pos.y + 1, z = pos.z}, 8, "default_item_smoke.png") - --minetest.log("Item name: "..item_name..", gender: "..self.gender) - -- There should be only one dialogue, however, it returns a key-value - -- result where we will have to do one loop - local dialogues_found = npc.relationships.get_dialogues_for_gift_item( - item_name, - npc.dialogue.tags.GIFT_ITEM_RESPONSE, - npc.dialogue.tags.GIFT_ITEM_UNLIKED, - self.gender) --- minetest.log("Message: "..dump(message_to_send)) - for _, item_dialogue in pairs(dialogues_found) do - npc.chat(self.npc_name, clicker_name, item_dialogue.text) - end - end - -end - --- Receive gift function; applies relationship points as explained above --- Also, creates a relationship object if not present -function npc.relationships.receive_gift(self, clicker) - -- Return if clicker is not offering an item - local item = npc.get_entity_wielded_item(clicker) - if item:get_name() == "" then return false end - - -- Get clicker name - local clicker_name = npc.get_entity_name(clicker) - - -- Create relationship if it doesn't exists - if check_relationship_exists(self, clicker_name) == false then - create_relationship(self, clicker_name) - end - - -- If NPC received a gift from this person, then reject any more gifts for now - if check_npc_can_receive_gift(self, clicker_name) == false then - npc.chat(self.npc_name, clicker_name, "Thanks, but I don't need anything for now") - return false - end - - -- If NPC is ready for marriage, do no accept anything else but the ring, - -- and that with only a certain chance. The self.owner is to whom is married - -- this NPC... he he. - if get_relationship_points(self, clicker_name) >= - npc.relationships.RELATIONSHIP_PHASE["phase5"].limit - and self.owner ~= clicker_name - and item:get_name() ~= "advanced_npc:marriage_ring" then - npc.chat(self.npc_name, clicker_name, - "Thank you my love, but I think that you have given me") - npc.chat(self.npc_name, clicker_name, - "enough gifts for now. Maybe we should go a step further") - -- Reset gift timer - reset_gift_timer(self, clicker_name) - return true - elseif get_relationship_points(self, clicker_name) >= - npc.relationships.RELATIONSHIP_PHASE["phase5"].limit - and item:get_name() == "advanced_npc:marriage_ring" then - -- If the player/entity is offering a marriage ring, then NPC will accept with a 50% - -- chance to marry the clicker - local receive_chance = math.random(1, 10) - -- Receive ring and get married - if receive_chance < 6 then - npc.chat(self.npc_name, clicker_name, - "Oh, oh you make me so happy! Yes! I will marry you!") - -- Get ring - item:take_item() - clicker:set_wielded_item(item) - -- TODO: Implement marriage event - -- Show marriage reaction - local pos = self.object:getpos() - effect({x = pos.x, y = pos.y + 1, z = pos.z}, 20, "heart.png", 4) - -- Give 100 points, so NPC is really happy on marriage - update_relationship(self, clicker_name, 100) - -- This sets the married state, for now. Hehe - self.owner = clicker_name - -- Reject ring for now - else - npc.chat(self.npc_name, clicker_name, - "Dear, I feel the same as you. But maybe not yet...") - end - -- Reset gift timer - reset_gift_timer(self, clicker_name) - return true - end - -- Marriage gifts: except for disliked items, all product a 0.5 * npc.ITEM_GIFT_EFFECT - -- Disliked items cause only a -0.5 point effect - if get_relationship_points(self, clicker_name) >= - npc.relationships.RELATIONSHIP_PHASE["phase5"].limit then - local modifier = 0.5 * npc.ITEM_GIFT_EFFECT - -- Check for disliked items - if item:get_name() == self.gift_data.disliked_items.dis1 - or item:get_name() == self.gift_data.disliked_items.dis2 then - modifier = -0.5 - show_receive_gift_reaction(self, item:get_name(), modifier, clicker_name, false) - elseif item:get_name() == self.gift_data.favorite_items.fav1 - or item:get_name() == self.gift_data.favorite_items.fav2 then - -- Favorite item reaction - show_receive_gift_reaction(self, item:get_name(), modifier, clicker_name, false) - else - -- Neutral item reaction - npc.chat(self.npc_name, clicker_name, "Thank you honey!") - end - -- Take item - item:take_item() - clicker:set_wielded_item(item) - -- Update relationship - update_relationship(self, clicker_name, modifier) - -- Reset gift timer - reset_gift_timer(self, clicker_name) - return true - end - - -- Modifies relationship depending on given item - local modifier = 0 - local take = true - local show_reaction = false - - if item:get_name() == self.gift_data.favorite_items.fav1 then - modifier = 2 * npc.relationships.ITEM_GIFT_EFFECT - show_reaction = true - elseif item:get_name() == self.gift_data.favorite_items.fav2 then - modifier = npc.relationships.ITEM_GIFT_EFFECT - show_reaction = true - elseif item:get_name() == self.gift_data.disliked_items.dis1 then - modifier = (-2) * npc.relationships.ITEM_GIFT_EFFECT - show_reaction = true - elseif item:get_name() == self.gift_data.disliked_items.dis2 then - modifier = (-1) * npc.relationships.ITEM_GIFT_EFFECT - show_reaction = true - else - -- If item is not a favorite or a dislike, then receive chance - -- if 70% - local receive_chance = math.random(1,10) - if receive_chance < 7 then - npc.chat(self.npc_name, clicker_name, "Thanks. I will find some use for this.") - else - npc.chat(self.npc_name, clicker_name, "Thank you, but no, I have no use for this.") - take = false - end - show_reaction = false - end - - -- Update relationship status - local is_phase_changed = update_relationship(self, clicker_name, modifier) - - -- Show NPC reaction to gift - if show_reaction == true then - show_receive_gift_reaction(self, item:get_name(), modifier, clicker_name, is_phase_changed) - end - - -- Take item if NPC accepted it - if take == true then - item:take_item() - clicker:set_wielded_item(item) - end - - npc.log("DEBUG", "NPC: "..dump(self)) - -- Reset gift timer - reset_gift_timer(self, clicker_name) - return true -end - --- Relationships are slowly increased by talking, increases by +0.2. --- Talking to married NPC increases relationship by +1 --- TODO: This needs a timer as the gift timer. NPC will talk anyways --- but relationship will not increase. -function npc.relationships.dialogue_relationship_update(self, clicker) - -- Get clicker name - local clicker_name = npc.get_entity_name(clicker) - - -- Check if relationship can be updated via talk - if check_relationship_by_talk_timer_ready(self, clicker_name) == false then - return - end - - -- Create relationship if it doesn't exists - if check_relationship_exists(self, clicker_name) == false then - create_relationship(self, clicker_name) - end - - local modifier = 0.2 - if self.is_married_to ~= nil and clicker_name == self.is_married_to then - modifier = 1 - end - -- Update relationship - update_relationship(self, clicker_name, modifier) - - -- Resert timers - reset_talk_timer(self, clicker_name) - reset_relationship_decrease_timer(self, clicker_name) -end diff --git a/mods/advanced_npc/spawner.lua b/mods/advanced_npc/spawner.lua deleted file mode 100644 index 9c06f3f..0000000 --- a/mods/advanced_npc/spawner.lua +++ /dev/null @@ -1,1231 +0,0 @@ --- Advanced NPC spawner by Zorman2000 --- The advanced spawner will contain functionality to spawn NPC correctly on --- 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: --- - Number and positions of beds --- - Number and positions of benches --- - Number and positions of chests --- - Position of furnaces --- - Position of doors --- - NOTE: Scanning will be implemented for first floors only in the first --- version. It's expected to also include upper floors later. --- - After that, it will store these information in the node's metadata. --- - The spawner will analyze the information and will spawn (# of beds/2) or 1 --- NPC in that house. The NPCs will be spawned in intervals, for which the node --- will create node timers for each NPC. --- - When a NPC is spawned: --- - The NPC will be given a schedule --- - If in a farm, the NPC will have a "farmer schedule" with a 40% chance --- - If in a house or hut, the NPC will have either a miner, woodcutter, cooker --- or simple schedule with an equal chance each --- - The NPC will be assigned a unique bed --- - The NPC will know the location of one chest, one bench and one furnace --- - A life timer for the NPC will be created (albeit a long one). Once the NPC's --- timer is invoked, the NPC will be de-spawned (dies). The spawner should keep --- track of these. --- - If a NPC has died, the spawner will choose with 50% chance to spawn a new NPC. --- --- This is the basic functionality expected for the spawner in version 1. Other things --- such as scanning upper floors, spawning families of NPCs and creating relationships --- among them, etc. will be for other versions. - --- Public API -npc.spawner = {} --- Private API -local spawner = {} - --- This is the official list of support building types --- from the mg_villages mod -npc.spawner.mg_villages_supported_building_types = { - "house", - "farm_full", - "farm_tiny", - "hut", - "lumberjack" -} - -npc.spawner.replace_activated = true -npc.spawner.replacement_interval = 60 -npc.spawner.spawn_delay = 10 - -npc.spawner.spawn_data = { - status = { - dead = 0, - alive = 1 - }, - age = { - adult = "adult", - child = "child" - } -} - --- 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 ---------------------------------------------------------------------------------------- - --- 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, spawn_pos) - local result = { - building_type = "", - plot_info = {}, - entrance = {}, - node_data = {}, - npcs = {}, - 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.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.locations.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 - elseif spawn_pos ~= nil then - -- A spawn egg was used, assume it was spawned outside the building - outside_pos = spawn_pos - end - -- Try to find entrance - local entrance = npc.locations.find_building_entrance(usable_nodes.bed_type, outside_pos) - if entrance then - npc.log("INFO", "Found building entrance at: "..minetest.pos_to_string(entrance.door)) - -- Set building entrance - result.entrance = entrance - else - npc.log("ERROR", "Unable to find building entrance!") - end - - -- Set node_data - result.node_data = 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.npc_stats = npc_stats - - return result -end - ---------------------------------------------------------------------------------------- --- Spawning functions ---------------------------------------------------------------------------------------- --- This function attempts to determine an occupation for an NPC given: --- - The local building type (building NPC is spawning) --- - The surrounding workplaces' building types --- - The NPCs in the local building --- Lo and behold! In this function lies a code monster, smelly, incomprehensible.. -function npc.spawner.determine_npc_occupation(building_type, workplace_nodes, npcs) - local surrounding_buildings_map = {} - local current_building_map = {} - local current_building_npc_occupations = {} - local result = {} - - -- Get all occupation names in the current building - for i = 1, #npcs do - if not npc.utils.array_contains(current_building_npc_occupations, npcs[i].occupation) then - table.insert(current_building_npc_occupations, npcs[i].occupations) - end - end - -- Classify workplaces into local and surrounding - for i = 1, #workplace_nodes do - local workplace = workplace_nodes[i] - if workplace.surrounding_workplace == true then - table.insert(surrounding_buildings_map, - {type=workplace.building_type, origin_building_type=building_type}) - else - current_building_map[workplace.building_type] = workplace - end - end - - -- Get occupation names for the buildings - local occupation_names = npc.occupations.get_for_building( - building_type, - surrounding_buildings_map - ) - npc.log("INFO", "Found occupations: "..dump(occupation_names).."\nfor local building type: " - ..dump(building_type).."\nAnd surrounding building types: "..dump(surrounding_buildings_map)) - - -- Insert default occupation into result - result[#result + 1] = {name=npc.occupations.basic_name, node={node_pos={}}} - - --------------------------------------------------------------------------------------- - -- Determine occupation - --------------------------------------------------------------------------------------- - -- First of all, iterate through all names, discard the default basic occupation. - -- Next, check if no-one in this builiding has this occupation name. - -- Next, check if the workplace node has no data assigned to it. - -- Finally, if not, return an table with the occupation name, and the selected - -- workplace node. - -- Note: Much more can be done here. This is a simplistic implementation, - -- given this is already complicated enough. For example, existing NPCs' occupation - -- can play a much more important role, not only taken in consideration for discarding. - -- Beware: Incomprehensible code lies ahead - for i = 1, #occupation_names do - -- Check if this occupation name is the default occupation, and if it is, continue - if occupation_names[i] ~= npc.occupations.basic_name then - -- Check if someone already works on this - if npc.utils.array_contains(current_building_npc_occupations, occupation_names[i]) == false then - -- Check if someone else already has this occupation at the same workplace - -- Get building types from occupation - local local_building_types = - npc.occupations.registered_occupations[occupation_names[i]].building_types or {} - local surrounding_building_types = - npc.occupations.registered_occupations[occupation_names[i]].surrounding_building_types or {} - - if #workplace_nodes > 0 then - for j = 1, #workplace_nodes do - -- Attempt to match the occupation definition's local and surrounding types - -- to the workplace node's building type. - local local_building_match = false - local surrounding_building_match = false - - -- Check if there is building_type match between the def's local - -- building_types and the current workplace node's building_type - if #local_building_types > 0 then - -- New matching algorithm - for i = 1, #occupation_names do - for j = 1, #local_building_types do - if building_type == local_building_types[j] then - npc.log("INFO", "Found suitable occupation: "..dump(result)) - -- Forget about workplace location - let NPC find it - local_building_match = true - result[#result + 1] = {name=occupation_names[i], node={}} - break - end - end - end --- local_building_match = --- npc.utils.array_contains(local_building_types, workplace_nodes[j].building_type) - end - - -- Check if there is building_type match between the def's surrounding - -- building_types and the current workplace node's building_type - if #surrounding_building_types > 0 then - npc.log("DEBUG", "Scanning "..dump(#workplace_nodes).." plotmarkers for surrounding workplaces") - for k = 1, #surrounding_building_types do - if surrounding_building_types[k].type == workplace_nodes[j].building_type then - surrounding_building_match = true - break - end - end - end - -- Check if there was a match - if local_building_match == true or surrounding_building_match == true then - -- Match found, attempt to map this workplace node to the - -- current occupation. How? Well, if the workplace isn't being - -- used by another NPC, then, use it - local meta = minetest.get_meta(workplace_nodes[j].node_pos) - local worker_data = minetest.deserialize(meta:get_string("work_data") or "") - npc.log("DEBUG", "Found worker data: "..dump(worker_data)) - -- If no worker data is found, then create it - if not worker_data then - npc.log("INFO", "Found suitable occupation and workplace: "..dump(result)) - table.insert(result, {name=occupation_names[i], node=workplace_nodes[j]}) - end - end - end - else - -- Try to match building type with the occupation local building types - minetest.log("Building type: "..dump(building_type)) - minetest.log("Occupation local building types: "..dump(local_building_types)) - for i = 1, #occupation_names do - for j = 1, #local_building_types do - if building_type == local_building_types[j] then - npc.log("INFO", "Found suitable occupation: "..dump(result)) - -- Forget about workplace location - let NPC find it - result[#result + 1] = {name=occupation_names[i], node={}} - break - end - end - end - minetest.log("Local building match after: "..dump(result)) - end - end - end - end - - -- Determine result. Choose profession, how to do it? - -- First, check previous NPCs' occupation. - -- - If there is a NPC working at something, check the NPC count. - -- - If count is less than three (only two NPCs), default_basic occupation. - -- - If count is greater than two, assign any eligible occupation with 50% chance - -- - If not NPC is working, choose an occupation that is not default_basic - minetest.log("Current building occupations: "..dump(current_building_npc_occupations)) - minetest.log("Result #: "..dump(#result)) - minetest.log("Result: "..dump(result)) - if next(current_building_npc_occupations) ~= nil then - for i = 1, #current_building_npc_occupations do - if current_building_npc_occupations[i] ~= npc.occupations.basic_name then - if #current_building_npc_occupations < 3 then - -- Choose basic default occupation - return result[1] - elseif #current_building_npc_occupations > 2 then - -- Choose any occupation - return result[math.random(1, #result)] - end - end - end - else - -- Check how many occupation names we have - if #result == 1 then - -- Choose basic default occupation - return result[1] - elseif #result == 2 then - -- Return other than the basic default - return result[2] - else - -- Choose an occupation with equal chance each - return result[math.random(2, #result)] - end - end - -- By default, if nothing else works, return basic default occupation - return result[1] -end - --- 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(entity_name, 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") - - -- Determine occupation - area_info["building_type"] = meta:get_string("building_type") - local nearby_plotmarkers = minetest.deserialize(meta:get_string("nearby_plotmarkers")) - --minetest.log("BEFORE Workplace nodes: "..dump(area_info.node_data.workplace_type)) - local occupation_data = npc.spawner.determine_npc_occupation( - area_info.building_type, - area_info.node_data.workplace_type, - area_info.npcs) - - --minetest.log("AFTER Workplace nodes: "..dump(area_info.node_data.workplace_type)) - -- Assign workplace node - if occupation_data then - for i = 1, #area_info.node_data.workplace_type do - if area_info.node_data.workplace_type[i].node_pos == occupation_data.node.node_pos then - -- Found node, mark it as being used by NPC - area_info.node_data.workplace_type[i]["occupation"] = occupation_data.name - break - end - end - end - - npc.log("INFO", "Attempting spawning of "..dump(entity_name).." at "..minetest.pos_to_string(pos)) - - -- Spawn NPC - local metadata = npc.spawner.spawn_npc(entity_name, pos, area_info, {occupation_name=occupation_data.name, occupation_workplace_pos=occupation_data.node.node_pos}) - if type(metadata) == "boolean" then - return - end - -- Set all metadata back into the node - -- Increase NPC spawned count - area_info.spawned_npc_count = metadata.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(metadata.npcs)) - -- Store NPC stats - meta:set_string("npc_stats", minetest.serialize(metadata.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 - --- 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(entity_name, pos, area_info, npc_info) - -- Get occupation data - local occupation_name = npc_info.occupation_name - local occupation_workplace_pos = npc_info.occupation_workplace_pos - -- 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 - - 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}, entity_name) - 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, npc_info) - else - npc.initialize(ent, pos, nil, nil, npc_info) - end - -- If node_data is present, assign nodes - npc.log("DEBUG", "Node data: "..dump(node_data)) - if 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 - local entry = { - status = npc.spawner.spawn_data.status.alive, - name = ent:get_luaentity().name, - id = ent:get_luaentity().npc_id, - gender = ent:get_luaentity().gender, - age = age, - occupation = occupation, - workplace = occupation_workplace_pos, - born_day = minetest.get_day_count() - } - npc.log("DEBUG", "Area info: "..dump(area_info)) - table.insert(area_info.npcs, entry) - -- Update and store stats - -- Increase total of NPCs for specific gender - npc_stats[ent:get_luaentity().gender].total = - npc_stats[ent:get_luaentity().gender].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 gender - npc_stats[ent:get_luaentity().gender][age] = - npc_stats[ent:get_luaentity().gender][age] + 1 - area_info.npc_stats = npc_stats - -- Return - npc.log("INFO", "Spawning successful!") - return area_info - 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 building and give them --- until there's no more. Call this function after NPCs are initialized --- The basic assumption: --- - 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 --- - Else, just let the NPC know one of the chests, but not to be owned --- - 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 --- 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) - -- Assign plotmarker if position given - if pos then - npc.locations.add_shared(self, npc.locations.data.other.home_plotmarker, - npc.locations.data.other.home_plotmarker, pos) - end - - -- Assign building entrance door - if entrance ~= nil and entrance.door ~= nil and entrance.inside ~= nil and entrance.outside ~= nil then - npc.locations.add_shared(self, npc.locations.data.openable.home_entrance_door, npc.locations.data.openable.home_entrance_door, entrance.door) - -- Assign these places to npc - npc.locations.add_shared(self, npc.locations.data.other.home_inside, npc.locations.data.other.home_inside, entrance.inside) - npc.locations.add_shared(self, npc.locations.data.other.home_outside, npc.locations.data.other.home_outside, entrance.outside) - end - - -- Assign beds - local assigned_bed - if #node_data.bed_type > 0 then - -- Assign a specific bed node to a NPC. - assigned_bed = npc.locations.add_owned_accessible_place(self, node_data.bed_type, - npc.locations.data.bed.primary) - end - - -- Assign rooms - if assigned_bed then - local bedroom_entrance = npc.locations.find_bedroom_entrance(assigned_bed, pos) - --minetest.log("Entrance: "..dump(bedroom_entrance)) - if bedroom_entrance ~= nil - and bedroom_entrance.door ~= nil - and bedroom_entrance.inside ~= nil - and bedroom_entrance.outside ~= nil then - npc.locations.add_shared(self, npc.locations.data.openable.room_entrance_door, npc.locations.data.openable.room_entrance_door, bedroom_entrance.door) - -- Assign these places to npc - npc.locations.add_shared(self, npc.locations.data.other.room_inside, npc.locations.data.other.room_inside, bedroom_entrance.inside) - npc.locations.add_shared(self, npc.locations.data.other.room_outside, npc.locations.data.other.room_outside, bedroom_entrance.outside) - end - 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.locations.add_owned_accessible_place(self, node_data.sittable_type, - npc.locations.data.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.locations.add_shared_accessible_place(self, node_data.sittable_type, - npc.locations.data.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.locations.add_owned_accessible_place(self, node_data.furnace_type, - npc.locations.data.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.locations.add_shared_accessible_place(self, node_data.furnace_type, - npc.locations.data.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.locations.add_owned_accessible_place(self, node_data.storage_type, - npc.locations.data.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 storaage nodes as well. - npc.locations.add_shared_accessible_place(self, node_data.storage_type, - npc.locations.data.storage.shared) - end - - -- Assign workplace nodes - -- Beware: More incomprehensibe code lies ahead! - npc.log("INFO", "Assigning workplace node to NPC "..self.npc_name.." with occupation "..dump(self.occupation_name)) - if #node_data.workplace_type > 0 then - npc.log("DEBUG", "Node Data workplace nodes: "..dump(node_data.workplace_type)) - -- First, find the workplace_node that was marked - for i = 1, #node_data.workplace_type do --- minetest.log("In assign places: workplace nodes: "..dump(node_data.workplace_type)) --- minetest.log("Condition? "..dump(node_data.workplace_type[i].occupation --- and node_data.workplace_type[i].occupation == self.occupation_name)) - if node_data.workplace_type[i].occupation - and node_data.workplace_type[i].occupation == self.occupation_name then - npc.log("INFO", "Found a workplace node that is match to NPC occupation: "..dump(node_data.workplace_type[i])) - -- Walkable nodes from occupation - local walkables = npc.occupations.registered_occupations[self.occupation_name].walkable_nodes - -- Found the node. Assign only this node to the NPC. - npc.locations.add_shared_accessible_place(self, {node_data.workplace_type[i]}, - npc.locations.data.workplace.primary, false, walkables) - -- Edit metadata of this workplace node to not allow it for other NPCs - local meta = minetest.get_meta(node_data.workplace_type[i].node_pos) - local work_data = { - npc_name = self.npc_name, - occupation = self.occupation_name, - multiple_npcs = - npc.occupations.registered_occupations[self.occupation_name].allow_multiple_npcs_at_workplace - } - meta:set_string("work_data", minetest.serialize(work_data)) - end - end - end - - - npc.log("DEBUG", "Places for NPC "..self.npc_name..": "..dump(self.places_map)) - - -- Make NPC go into their house - -- If entrance is available let NPC - if entrance then --- npc.enqueue_script(self, --- npc.commands.cmd.WALK_TO_POS, --- {end_pos=npc.locations.data.OTHER.HOME_INSIDE, --- walkable={}}) --- npc.enqueue_command(self, npc.commands.cmd.FREEZE, {freeze = false}) - end - - return node_data -end - --- This function takes care of calculating how many NPCs will be spawn --- 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 to set 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. - --- This map holds the spawning position chosen by a player at a given time. -local spawner = { - spawn_pos = {}, - entity_name = "" -} - --- Spawn egg (WIP) --- Use for manually spawning NPCs. Up to now, supports local occupations only. -function npc.spawner.register_spawn_egg(entity_name) - minetest.register_craftitem(entity_name.."_spawn_egg", { - description = "NPC Spawner", - inventory_image = "mobs_chicken_egg.png^(default_brick.png^[mask:mobs_chicken_egg_overlay.png)", - on_place = function(itemstack, user, pointed_thing) - -- Store spawn pos - spawner.spawn_pos[user:get_player_name()] = pointed_thing.above - -- This looks horrible - please change - spawner.name = string.split(itemstack:get_name(), "_spawn_egg")[1] - - local occupation_names = npc.utils.get_map_keys(npc.occupations.registered_occupations) - - local building_dropdown_string = "dropdown[0.5,0.75;6;building_type;" - for i = 1, #npc.spawner.mg_villages_supported_building_types do - building_dropdown_string = building_dropdown_string - ..npc.spawner.mg_villages_supported_building_types[i].."," - end - building_dropdown_string = building_dropdown_string..";1]" - - -- Generate occupation dropdown - local occupation_dropdown_string = "dropdown[0.5,1.95;6;occupation_name;" - for i = 1, #occupation_names do - occupation_dropdown_string = occupation_dropdown_string..occupation_names[i].."," - end - occupation_dropdown_string = occupation_dropdown_string..";1]" - - local formspec = "size[7,7]".. - "label[0.1,0.25;Building type]".. - building_dropdown_string.. - "label[0.1,1.45;Occupation]".. - occupation_dropdown_string.. - "field[0.5,3;3,2;radius;Search radius;20]".. - "field[3.5,3;3,2;height;Search height;2]".. - "button_exit[2.25,6.25;2.5,0.75;exit;Spawn]" - - minetest.show_formspec(user:get_player_name(), "advanced_npc:spawn_egg_main", formspec) - end - }) -end - --- This map holds the name of the player and the position of the workplace that --- he/she placed -spawner.workplace_pos = {} - --- Manual workplace marker (WIP) --- Put this where a workplace for a NPC is. For example: in a cotton field, --- inside a church, etc. -minetest.register_node("advanced_npc:workplace_marker", { - description = "NPC Workplace Marker", - tiles = {"default_stone.png"}, - paramtype = "light", - paramtype2 = "facedir", - drawtype = "nodebox", - node_box = { - type = "fixed", - fixed = { - {-0.5+2/16, -0.5, -0.5+2/16, 0.5-2/16, -0.5+3/16, 0.5-2/16}, - }, - }, - groups = {cracky=1}, - after_place_node = function(pos, placer, itemstack, pointed_thing) - local meta = minetest.get_meta(pos) - meta:set_string("infotext", "Unconfigured workplace marker") - end, - on_rightclick = function(pos, node, clicker, itemstack, pointed_thing) - -- Read current value - local meta = minetest.get_meta(pos) - local building_type = meta:get_string("building_type") or "" - local surrounding_workplace = meta:get_string("surrounding_workplace") - -- Consider changing the field for a dropdown - local formspec = "size[7,4]".. - "label[0.1,0.25;Building type]".. - "field[0.5,1;6.5,2;text;(farm_tiny, farm_full, house, church, etc.);"..building_type.."]".. - "checkbox[0.5,2.25;is_surrounding;Is surrounding building;" - ..surrounding_workplace.."]".. - "button_exit[0.95,3.25;2.5,0.75;exit_btn;Proceed]".. - "button_exit[3.5,3.25;2.5,0.75;reset_btn;Reset]" - - spawner.workplace_pos[clicker:get_player_name()] = pos - - minetest.show_formspec(clicker:get_player_name(), "advanced_npc:workplace_marker_formspec", formspec) - end, -}) - --- Handle formspecs -minetest.register_on_player_receive_fields(function(player, formname, fields) - - if formname then - -- Handle spawn egg formspec - if formname == "advanced_npc:spawn_egg_main" then - if fields then - -- Handle exit (spawn) button - if fields.exit then - local pos = spawner.spawn_pos[player:get_player_name()] - local name = spawner.name - local radius = 20 - local y_adj = 2 - -- Set radius if present - if fields.radius then - radius = tonumber(fields.radius) - end - -- Set y adjustment if present - if fields.height then - y_adj = tonumber(fields.height) - end - -- Calculate positions - local start_pos = {x=pos.x-radius, y=pos.y-y_adj, z=pos.z-radius } - local end_pos = {x=pos.x+radius, y=pos.y+y_adj, z=pos.z+radius } - - -- Scan for usable nodes - local area_info = npc.spawner.scan_area_for_spawn(start_pos, end_pos, player:get_player_name(), pos) - minetest.log("Area info: "..dump(area_info)) - -- Assign occupation - local occupation_data = npc.spawner.determine_npc_occupation( - fields.building_type or area_info.building_type, - area_info.node_data.workplace_type, - area_info.npcs) - - -- Assign workplace node - if occupation_data then - for i = 1, #area_info.node_data.workplace_type do - if area_info.node_data.workplace_type[i].node_pos == occupation_data.node.node_pos then - -- Found node, mark it as being used by NPC - area_info.node_data.workplace_type[i]["occupation"] = occupation_data.name - end - end - end - - -- Spawn NPC - local metadata = npc.spawner.spawn_npc(name, pos, area_info, {occupation_name=fields.occupation_name}) - end - end - end - -- Handle workplace marker formspec - if formname == "advanced_npc:workplace_marker_formspec" then - if fields then - local pos = spawner.workplace_pos[player:get_player_name()] - local meta = minetest.get_meta(pos) - -- Checkbox setting - if fields.is_surrounding then - --minetest.log("Saving.. "..fields.is_surrounding) - meta:set_string("surrounding_workplace", ""..fields.is_surrounding.."") - end - -- Handle reset button - if fields.reset_btn then - meta:set_string("building_type", "") - meta:set_string("surrounding_workplace", false) - meta:set_string("infotext", "Unconfigured workplace marker") - meta:set_string("work_data", nil) - return - end - -- Handle set button - if (pos and fields.text and fields.exit_btn) - or (fields.key_enter_field and fields.key_enter_field == "building_type") then - local meta = minetest.get_meta(pos) - meta:set_string("building_type", fields.text) - meta:set_string("infotext", fields.text.." (workplace)") - end - end - end - end - -end) - - ---------------------------------------------------------------------------------------- --- Support code for mg_villages mods ---------------------------------------------------------------------------------------- -if minetest.get_modpath("mg_villages") ~= nil then - local mg_villages_entity_name = "" - function npc.spawner.set_mg_villages_entity_name(name) - mg_villages_entity_name = name - end - - -- 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) - local result = {} - -- 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 - -- Attempt rotating the search area if no data is found - for i = 1, 4 do - 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 - - 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)) - - result = npc.locations.scan_area_for_usable_nodes(start_pos, end_pos) - if result and result.bed_type and #result.bed_type > 0 then - -- Store start and end pos in plotmarker metadata for any future use - local meta = minetest.get_meta(pos) - meta:set_string("advanced_npc:area_pos1", minetest.serialize(start_pos)) - meta:set_string("advanced_npc:area_pos2", minetest.serialize(end_pos)) - meta:mark_as_private({"advanced_npc:area_pos1", "advanced_npc:area_pos2"}) - return result - else - npc.log("WARNING", "Failed attempt "..dump(i).." in finding usable nodes. "..dump(4-i).." attempts left.") - -- Rotate search area and try again - brotate = (brotate + 1) % 4 - end - 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)) - 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 - - local all_data = npc.locations.get_mg_villages_building_data(pos) - local building_data = all_data.building_data - local building_type = all_data.building_type - local building_pos_data = all_data.building_pos_dataS - - --minetest.log("bldng data: "..dump(building_data)) - --minetest.log("bldng type: "..dump(building_type)) - --minetest.log("Pos data: "..dump(building_pos_data)) - --minetest.log("Found building data: "..dump(building_data)) - - -- 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 - npc.log("DEBUG", "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) - - if not nodedata then - npc.log("ERROR", "Unable to find usable nodes in building.") - return - end - - -- Find building entrance - local doors = nodedata.openable_type - --minetest.log("Found "..dump(#doors).." openable nodes") - npc.log("DEBUG", "Nodedata: "..dump(nodedata)) - local entrance = npc.locations.find_building_entrance(nodedata.bed_type, pos) - --minetest.log("Found good entrance: "..dump(entrance1)) - --local entrance = npc.locations.find_entrance_from_openable_nodes(doors, pos) - if entrance then - npc.log("INFO", "Found building entrance at: "..minetest.pos_to_string(entrance.door)) - 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)) - -- Find nearby plotmarkers, excluding current plotmarker - local nearby_plotmarkers = npc.locations.find_plotmarkers(pos, 40, true) - --npc.log("INFO", "SPWNER: Found nearby plotmarkers: "..dump(nearby_plotmarkers)) - meta:set_string("nearby_plotmarkers", minetest.serialize(nearby_plotmarkers)) - -- Check if building position data is also available (recent mg_villages) - if building_pos_data then - meta:set_string("building_pos_data", minetest.serialize(building_pos_data)) - end - -- 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 - minetest.override_item("mg_villages:plotmarker", { - walkable = false, - groups = {cracky=3,stone=2}, - - -- TODO: Change formspec to a more detailed one. - on_rightclick = function(pos, node, clicker, itemstack, pointed_thing) - local meta = minetest.get_meta(pos) - minetest.log("NPCs: "..dump(minetest.deserialize(meta:get_string("npcs")))) - minetest.log("Node data: "..dump(minetest.deserialize(meta:get_string("node_data")))) - return mg_villages.plotmarker_formspec( pos, nil, {}, clicker ) - end, - - on_timer = function(pos, elapsed) - -- Adds timer support - if mg_villages_entity_name ~= nil and mg_villages_entity_name ~= "" then - npc.spawner.spawn_npc_on_plotmarker(mg_villages_entity_name, pos) - end - end, - }) - - -- ABM Registration - -- Consider changing this to be on nodetimer - minetest.register_abm({ - label = "Replace mg_villages:plotmarker with Advanced NPC auto spawners", - nodenames = {"mg_villages:plotmarker"}, - interval = 10,--npc.spawner.replacement_interval, - chance = 1,--5, - catch_up = true, - action = function(pos, node, active_object_count, active_object_count_wider) - -- Check if replacement is needed - local meta = minetest.get_meta(pos) - 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.adapt_mg_villages_plotmarker(pos) - end - end - }) - -end - --- Chat commands to manage spawners -minetest.register_chatcommand("restore_plotmarkers", { - description = "Replaces all advanced_npc:plotmarker_auto_spawner with mg_villages:plotmarker in the specified radius.", - privs = {server=true}, - func = function(name, param) - -- Check if radius is null - 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 = {} - for _,player in pairs(minetest.get_connected_players()) do - if player:get_player_name() == name then - pos = player:get_pos() - break - end - end - -- Search for nodes - 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, - {"mg_villages:plotmarker"}) - -- Check if we have nodes to replace - minetest.chat_send_player(name, "Found "..dump(#nodes).." nodes to replace...") - if #nodes == 0 then - return - end - -- Replace all nodes - for i = 1, #nodes do - local meta = minetest.get_meta(nodes[i]) - local village_id = meta:get_string("village_id") - local plot_nr = meta:get_int("plot_nr") - local infotext = meta:get_string("infotext") - local npcs = minetest.deserialize(meta:get_string("npcs")) - -- Restore workplaces to original status - if npcs then - for i = 1, #npcs do - if npcs[i].workplace then - -- Remove work data - local workplace_meta = minetest.get_meta(npcs[i].workplace) - workplace_meta:set_string("work_data", nil) - end - end - end - -- Set metadata - meta = minetest.get_meta(nodes[i]) - meta:set_string("village_id", village_id) - meta:set_int("plot_nr", plot_nr) - meta:set_string("infotext", infotext) - -- Clear NPC stats, NPC data and node data - -- Clear node_data metadata - local node_data = minetest.deserialize(meta:get_string("node_data")) - npc.locations.clear_metadata_usable_nodes_in_area(node_data) - - meta:set_string("node_data", nil) - meta:set_string("npcs", nil) - meta:set_string("npc_stats", nil) - meta:set_string("replaced", "false") - end - minetest.chat_send_player(name, "Finished replacement of "..dump(#nodes).." auto-spawners successfully") - end -}) - -minetest.register_chatcommand("restore_area", { - description = "", - privs = {server = true}, - func = function(name, param) - local args = npc.utils.split(param, " ") - minetest.log("Params: "..dump(args)) - if #args < 2 then - minetest.chat_send_player("Please specify horizontal and vertical radius.") - return - end - local radius = args[1] - local y_adj = args[2] - -- Get player position - local pos = {} - for _,player in pairs(minetest.get_connected_players()) do - if player:get_player_name() == name then - pos = player:get_pos() - break - end - end - -- Search for nodes - -- Calculate positions - local start_pos = {x=pos.x-radius, y=pos.y-y_adj, z=pos.z-radius } - local end_pos = {x=pos.x+radius, y=pos.y+y_adj, z=pos.z+radius } - - -- Scan for usable nodes - local node_data = npc.locations.scan_area_for_usable_nodes(start_pos, end_pos) - local removed_count = npc.locations.clear_metadata_usable_nodes_in_area(node_data) - - minetest.chat_send_player(name, "Restored "..dump(removed_count).." nodes in area from " - ..minetest.pos_to_string(start_pos).." to "..minetest.pos_to_string(end_pos)) - end -}) diff --git a/mods/advanced_npc/spawner_marker.lua b/mods/advanced_npc/spawner_marker.lua deleted file mode 100644 index 533007d..0000000 --- a/mods/advanced_npc/spawner_marker.lua +++ /dev/null @@ -1,3 +0,0 @@ --- Spawner markers --- Specialized functionality to allow players do NPC spawning --- on their own custom buildings. diff --git a/mods/advanced_npc/textures/default_female.png b/mods/advanced_npc/textures/default_female.png deleted file mode 100755 index a9d1a2c..0000000 Binary files a/mods/advanced_npc/textures/default_female.png and /dev/null differ diff --git a/mods/advanced_npc/textures/default_male.png b/mods/advanced_npc/textures/default_male.png deleted file mode 100755 index 9356398..0000000 Binary files a/mods/advanced_npc/textures/default_male.png and /dev/null differ diff --git a/mods/advanced_npc/textures/marriage_ring.png b/mods/advanced_npc/textures/marriage_ring.png deleted file mode 100644 index 101fa49..0000000 Binary files a/mods/advanced_npc/textures/marriage_ring.png and /dev/null differ diff --git a/mods/advanced_npc/trade/prices.lua b/mods/advanced_npc/trade/prices.lua deleted file mode 100644 index a8c7e13..0000000 --- a/mods/advanced_npc/trade/prices.lua +++ /dev/null @@ -1,159 +0,0 @@ --- Price table for items bought/sold by NPC traders by Zorman2000 --- This table should be globally accessible so that other mods can set --- prices as they see fit. - -npc.trade.prices = { - currency = { - tier1 = "tier1", - tier2 = "tier2", - tier3 = "tier3" - } -} --- Table that contains the prices -local price_table = {} --- Currency table --- Define default currency (based on lumps from default) -local currency_table = { - tier1 = {string = "default:gold_lump", name = "Gold lump"}, - tier2 = {string = "default:copper_lump", name = "Copper lump"}, - tier3 = {string = "default:iron_lump", name = "Iron lump"} -} - --- Functions -function npc.trade.prices.update(item_name, tier, count) - for key,value in pairs(price_table) do - if key == item_name then - value = {tier=currency_table[tier].string, count=count} - return - end - end - return nil -end - -function npc.trade.prices.get(item_name) - local price_entry = price_table[item_name] - if price_entry then - return price_entry - end - return nil -end - -function npc.trade.prices.add(item_name, tier, count) - if npc.trade.prices.get(item_name) == nil then - price_table[item_name] = {tier=currency_table[tier].string, count=count} - else - npc.trade.prices.update(item_name, tier, count) - end -end - -function npc.trade.prices.remove(item_name) - price_table[item_name] = nil -end - --- Gets all the item for a specified budget -function npc.trade.prices.get_items_for_currency_count(tier, count, price_factor) - local result = {} - --minetest.log("Currency quantity: "..dump(count)) - for item_name, price in pairs(price_table) do - -- Check price currency is of the same tier - if price.tier == tier and price.count <= count then - result[item_name] = {price = price} - - --minetest.log("Item name: "..dump(item_name)..", Price: "..dump(price)) - - local min_buying_item_count = 1 - -- Calculate price NPC is going to buy for - local buying_price_count = price.count * price_factor - -- Check if the buying price is not an integer - if buying_price_count % 1 ~= 0 then - -- If not, increase the buying item count until we get an integer - local adjust = 1 / price_factor - if price.count < 1 then - adjust = 1 / (price.count * price_factor) - end - min_buying_item_count = min_buying_item_count * adjust - end - --minetest.log("Minimum item buy quantity: "..dump(min_buying_item_count)) - --minetest.log("Minimum item price quantity: "..dump(buying_price_count)) - -- Calculate maximum buyable quantity - local max_buying_item_count = min_buying_item_count - while ((max_buying_item_count + min_buying_item_count) * buying_price_count <= count) do - max_buying_item_count = max_buying_item_count + min_buying_item_count - end - --minetest.log("Maximum item buy quantity: "..dump(max_buying_item_count)) - - result[item_name].min_buyable_item_count = min_buying_item_count - result[item_name].min_buyable_item_price = buying_price_count - result[item_name].max_buyable_item_count = max_buying_item_count - end - end - --minetest.log("Final result: "..dump(result)) - return result -end - --- Accepts table in format : --- {string = "itemstring", name = "Currency Item Name"} -function npc.trade.prices.set_currency(tier1, tier2, tier3) - currency_table = { - tier1 = tier1, - tier2 = tier2, - tier3 = tier3 - } -end - -function npc.trade.prices.get_currency_name(tier) - return currency_table[tier].name -end - -function npc.trade.prices.get_currency_itemstring(tier) - return currency_table[tier].string -end - - --- This method will compare the given item string to the --- currencies set in the currencies table. Returns true if --- itemstring is a currency. -function npc.trade.prices.is_item_currency(itemstring) - if npc.get_item_name(itemstring) == currency_table.tier3.string - or npc.get_item_name(itemstring) == currency_table.tier2.string - or npc.get_item_name(itemstring) == currency_table.tier1.string then - return true - end - return false -end - --- Default definitions for in-game items --- Tier 3 items: cheap items -price_table["default:cobble"] = {tier = currency_table["tier3"].string, count = 0.1} -price_table["flowers:geranium"] = {tier = currency_table["tier3"].string, count = 0.5} -price_table["default:apple"] = {tier = currency_table["tier3"].string, count = 1} -price_table["vessels:drinking_glass"] = {tier = currency_table["tier3"].string, count = 1} -price_table["default:tree"] = {tier = currency_table["tier3"].string, count = 2} -price_table["flowers:rose"] = {tier = currency_table["tier3"].string, count = 2} -price_table["flowers:dandelion_yellow"]= {tier = currency_table["tier3"].string, count = 2} -price_table["flowers:dandelion_white"] = {tier = currency_table["tier3"].string, count = 2} -price_table["default:stone"] = {tier = currency_table["tier3"].string, count = 2} -price_table["farming:seed_cotton"] = {tier = currency_table["tier3"].string, count = 3} -price_table["farming:seed_wheat"] = {tier = currency_table["tier3"].string, count = 3} -price_table["default:clay_lump"] = {tier = currency_table["tier3"].string, count = 3} -price_table["default:wood"] = {tier = currency_table["tier3"].string, count = 3} -price_table["mobs:meat_raw"] = {tier = currency_table["tier3"].string, count = 4} -price_table["flowers:chrysanthemum_green"] = {tier = currency_table["tier3"].string, count = 4} -price_table["default:sapling"] = {tier = currency_table["tier3"].string, count = 5} -price_table["mobs:meat"] = {tier = currency_table["tier3"].string, count = 5} -price_table["mobs:leather"] = {tier = currency_table["tier3"].string, count = 6} -price_table["default:sword_stone"] = {tier = currency_table["tier3"].string, count = 6} -price_table["default:shovel_stone"] = {tier = currency_table["tier3"].string, count = 6} -price_table["default:axe_stone"] = {tier = currency_table["tier3"].string, count = 6} -price_table["farming:hoe_stone"] = {tier = currency_table["tier3"].string, count = 6} -price_table["default:pick_stone"] = {tier = currency_table["tier3"].string, count = 7} -price_table["bucket:bucket_empty"] = {tier = currency_table["tier3"].string, count = 10} -price_table["farming:cotton"] = {tier = currency_table["tier3"].string, count = 15} -price_table["farming:bread"] = {tier = currency_table["tier3"].string, count = 20} - --- Tier 2 items: medium priced items - --- Tier 1 items: expensive items -price_table["default:mese_crystal"] = {tier = currency_table["tier1"].string, count = 45} -price_table["default:diamond"] = {tier = currency_table["tier1"].string, count = 90} -price_table["advanced_npc:marriage_ring"] = {tier = currency_table["tier1"].string, count = 100} \ No newline at end of file diff --git a/mods/advanced_npc/trade/trade.lua b/mods/advanced_npc/trade/trade.lua deleted file mode 100644 index de535fc..0000000 --- a/mods/advanced_npc/trade/trade.lua +++ /dev/null @@ -1,771 +0,0 @@ --- Trading code for Advanced NPC by Zorman2000 - -npc.trade = {} - -npc.trade.CASUAL = "casual" -npc.trade.TRADER = "trader" -npc.trade.NONE = "none" - -npc.trade.OFFER_BUY = "buy" -npc.trade.OFFER_SELL = "sell" - --- This variable establishes how much items a dedicated --- trader will buy until retiring the offer -npc.trade.DEDICATED_MAX_BUY_AMOUNT = 5 - --- This table holds all responses for trades -npc.trade.results = { - single_trade_offer = {}, - trade_offers = {}, - custom_trade_offer = {} -} - --- This is the text to be shown each time the NPC has more --- than one custom trade options to choose from -npc.trade.CUSTOM_TRADES_PROMPT_TEXT = "Hi there, how can I help you today?" - --- Casual trader NPC dialogues definition --- Casual buyer -npc.dialogue.register_dialogue({ - text = "I'm looking to buy some items, are you interested?", - --casual_trade_type = npc.trade.OFFER_BUY, - tags = {"default_casual_trade_dialogue", "buy_offer"}, - --dialogue_type = npc.dialogue.dialogue_type.casual_trade, - responses = { - [1] = { - text = "Sell", - action_type = "function", - response_id = 1, - action = function(self, player) - npc.trade.show_trade_offer_formspec(self, player, npc.trade.OFFER_BUY) - end - } - } -}) - --- npc.trade.CASUAL_TRADE_BUY_DIALOGUE = { --- text = "I'm looking to buy some items, are you interested?", --- casual_trade_type = npc.trade.OFFER_BUY, --- dialogue_type = npc.dialogue.dialogue_type.casual_trade, --- responses = { --- [1] = { --- text = "Sell", --- action_type = "function", --- response_id = 1, --- action = function(self, player) --- npc.trade.show_trade_offer_formspec(self, player, npc.trade.OFFER_BUY) --- end --- } --- } --- } - --- Casual seller -npc.dialogue.register_dialogue({ - text = "I have some items to sell, are you interested?", - --dialogue_type = npc.dialogue.dialogue_type.casual_trade, - tags = {"default_casual_trade_dialogue", "sell_offer"}, - --casual_trade_type = npc.trade.OFFER_SELL, - responses = { - [1] = { - text = "Buy", - action_type = "function", - response_id = 1, - action = function(self, player) - npc.trade.show_trade_offer_formspec(self, player, npc.trade.OFFER_SELL) - end - } - } -}) - --- npc.trade.CASUAL_TRADE_SELL_DIALOGUE = { --- text = "I have some items to sell, are you interested?", --- dialogue_type = npc.dialogue.dialogue_type.casual_trade, --- casual_trade_type = npc.trade.OFFER_SELL, --- responses = { --- [1] = { --- text = "Buy", --- action_type = "function", --- response_id = 1, --- action = function(self, player) --- npc.trade.show_trade_offer_formspec(self, player, npc.trade.OFFER_SELL) --- end --- } --- } --- } - --- Dedicated trade dialogue prompt -npc.dialogue.register_dialogue({ - text = "Hello there, would you like to trade?", - tags = {npc.dialogue.tags.DEFAULT_DEDICATED_TRADE}, - dialogue_type = npc.dialogue.dialogue_type.dedicated_trade, - responses = { - [1] = { - text = "Buy", - action_type = "function", - response_id = 1, - action = function(self, player) - npc.trade.show_dedicated_trade_formspec(self, player, npc.trade.OFFER_SELL) - end - }, - [2] = { - text = "Sell", - action_type = "function", - response_id = 2, - action = function(self, player) - npc.trade.show_dedicated_trade_formspec(self, player, npc.trade.OFFER_BUY) - end - }, - [3] = { - text = "Other", - action_type = "function", - response_id = 3, - action = function(self, player) - local dialogue = npc.dialogue.create_custom_trade_options(self, player) - npc.dialogue.process_dialogue(self, dialogue, player:get_player_name()) - end - } - } -}) --- npc.trade.DEDICATED_TRADER_PROMPT = { --- text = "Hello there, would you like to trade?", --- dialogue_type = npc.dialogue.dialogue_type.dedicated_trade, --- responses = { --- [1] = { --- text = "Buy", --- action_type = "function", --- response_id = 1, --- action = function(self, player) --- npc.trade.show_dedicated_trade_formspec(self, player, npc.trade.OFFER_SELL) --- end --- }, --- [2] = { --- text = "Sell", --- action_type = "function", --- response_id = 2, --- action = function(self, player) --- npc.trade.show_dedicated_trade_formspec(self, player, npc.trade.OFFER_BUY) --- end --- }, --- [3] = { --- text = "Other", --- action_type = "function", --- response_id = 3, --- action = function(self, player) --- local dialogue = npc.dialogue.create_custom_trade_options(self, player) --- npc.dialogue.process_dialogue(self, dialogue, player:get_player_name()) --- end --- } --- } --- } - -function npc.trade.show_trade_offer_formspec(self, player, offer_type) - - -- Strings for formspec, to include international support later - local prompt_string = " offers to buy from you" - local for_string = "for" - local buy_sell_string = "Sell" - - -- Get offer. As this is casual trading, NPCs will only have - -- one trade offer - local trade_offer = self.trader_data.buy_offers[1] - if offer_type == npc.trade.OFFER_SELL then - trade_offer = self.trader_data.sell_offers[1] - prompt_string = " wants to sell to you" - buy_sell_string = "Buy" - end - - local formspec = "size[8,4]".. - default.gui_bg.. - default.gui_bg_img.. - default.gui_slots.. - "label[2,0.1;"..self.npc_name..prompt_string.."]".. - "item_image_button[2,1.3;1.2,1.2;"..trade_offer.item..";item;]".. - "label[3.75,1.75;"..for_string.."]".. - "item_image_button[4.8,1.3;1.2,1.2;"..trade_offer.price[1]..";price;]".. - "button_exit[1,3.3;2.9,0.5;yes_option;"..buy_sell_string.."]".. - "button_exit[4.1,3.3;2.9,0.5;no_option;"..npc.dialogue.NEGATIVE_ANSWER_LABEL.."]" - - -- Create entry into results table - npc.trade.results.single_trade_offer[player:get_player_name()] = { - trade_offer = trade_offer, - npc = self - } - -- Show formspec to player - minetest.show_formspec(player:get_player_name(), "advanced_npc:trade_offer", formspec) -end - - -function npc.trade.show_dedicated_trade_formspec(self, player, offers_type) - - -- Choose the correct offers - local offers = self.trader_data.buy_offers - local menu_offer_type = "sell" - if offers_type == npc.trade.OFFER_SELL then - offers = self.trader_data.sell_offers - menu_offer_type = "buy" - end - - -- Create a grid with the items for trade offer - local max_columns = 4 - local current_x = 0.2 - local current_y = 0.5 - local current_col = 1 - local current_row = 1 - local formspec = "size[8.9,8.2]".. - default.gui_bg.. - default.gui_bg_img.. - default.gui_slots.. - "label[0.2,0.05;Click on the price button to "..menu_offer_type.." item]" - for i = 1, #offers do - local price_item_name = minetest.registered_items[npc.get_item_name(offers[i].price[1])].description - local count_label = "" - if npc.get_item_count(offers[i].item) > 1 then - count_label = "label["..(current_x + 1.35)..","..(current_y + 1)..";"..npc.get_item_count(offers[i].item).."]" - end - formspec = formspec.. - "box["..current_x..","..current_y..";2,2.3;#212121]".. - "item_image_button["..(current_x + 0.45)..","..(current_y + 0.15)..";1.3,1.3;"..npc.get_item_name(offers[i].item)..";item"..i..";]".. - count_label.. - "item_image_button["..(current_x + 1.15)..","..(current_y + 1.4)..";1,1;"..offers[i].price[1]..";price"..i..";]".. - "label["..(current_x + 0.15)..","..(current_y + 1.7)..";Price]" - current_x = current_x + 2.1 - current_col = current_col + 1 - if current_col > 4 then - current_col = 1 - current_x = 0.2 - current_y = current_y + 2.4 - end - end - - formspec = formspec .. "button_exit[2.5,7.9;3.9,0.5;exit;Nevermind]" - - -- Create entry into results table - npc.trade.results.trade_offers[player:get_player_name()] = { - offers_type = offers_type, - offers = offers, - npc = self - } - - minetest.show_formspec(player:get_player_name(), "advanced_npc:dedicated_trading_offers", formspec) - -end - --- For the moment, the trade offer for custom trade is always of sell type -function npc.trade.show_custom_trade_offer(self, player, offer) - local for_string = "for" - -- Create payments grid. Try to center it. When there are 4 - -- payment options, a grid is to be displayed. - local price_count = #offer.price - local start_x = 2 - local margin_x = 0 - local start_y = 1.45 - if price_count == 2 then - start_x = 1.5 - margin_x = 0.3 - elseif price_count == 3 then - start_x = 1.15 - margin_x = 0.85 - elseif price_count == 4 then - start_x = 1.5 - start_y = 0.8 - margin_x = 0.3 - end - - -- Create payment grid - local price_grid = "" - for i = 1, #offer.price do - price_grid = price_grid.."item_image_button["..start_x..","..start_y..";1,1;"..offer.price[i]..";price"..i..";]" - if #offer.price == 4 and i == 2 then - start_x = 1.5 - start_y = start_y + 1 - else - start_x = start_x + 1 - end - end - - local formspec = "size[8,4]".. - default.gui_bg.. - default.gui_bg_img.. - default.gui_slots.. - "label[2,0.1;"..self.npc_name..": "..offer.dialogue_prompt.."]".. - price_grid.. - "label["..(margin_x + 3.75)..",1.75;"..for_string.."]".. - "item_image_button["..(margin_x + 4.8)..",1.3;1.2,1.2;"..offer.item..";item;]".. - "button_exit[1,3.3;2.9,0.5;yes_option;"..offer.button_prompt.."]".. - "button_exit[4.1,3.3;2.9,0.5;no_option;"..npc.dialogue.NEGATIVE_ANSWER_LABEL.."]" - - -- Create entry into results table - npc.trade.results.custom_trade_offer[player:get_player_name()] = { - trade_offer = offer, - npc = self - } - -- Show formspec to player - minetest.show_formspec(player:get_player_name(), "advanced_npc:custom_trade_offer", formspec) -end - -function npc.trade.get_random_trade_status() - local chance = math.random(1,10) - - if chance < 3 then - -- Non-trader - return npc.trade.NONE - elseif 3 <= chance and chance <= 7 then - -- Casual trader - return npc.trade.CASUAL - elseif chance > 7 then - -- Trader by profession - return npc.trade.TRADER - end -end - --- This function generates and stores on the NPC data trade --- offers depending on the trader status. -function npc.trade.generate_trade_offers_by_status(self) - -- Get trader status - local status = self.trader_data.trader_status - -- Check what is the trader status - if status == npc.trade.NONE then - -- For none, clear all offers - self.trader_data.buy_offers = {} - self.trader_data.sell_offers = {} - elseif status == npc.trade.CASUAL then - -- For casual, generate one buy and one sell offer - self.trader_data.buy_offers = { - [1] = npc.trade.get_casual_trade_offer(self, npc.trade.OFFER_BUY) - } - self.trader_data.sell_offers = { - [1] = npc.trade.get_casual_trade_offer(self, npc.trade.OFFER_SELL) - } - elseif status == npc.trade.TRADER then - -- Clear current offers - self.trader_data.buy_offers = {} - self.trader_data.sell_offers = {} - -- Get trade offers for a dedicated trader - local offers = npc.trade.get_dedicated_trade_offers(self) - -- Store buy offers - for i = 1, #offers.buy do - table.insert(self.trader_data.buy_offers, offers.buy[i]) - end - -- Store sell offers - for i = 1, #offers.sell do - table.insert(self.trader_data.sell_offers, offers.sell[i]) - end - end -end - --- Convenience method that retrieves all the currency --- items that a NPC has on his/her inventory -function npc.trade.get_currencies_in_inventory(self) - local result = {} - local tier3 = npc.inventory_contains(self, npc.trade.prices.get_currency_itemstring("tier3")) - local tier2 = npc.inventory_contains(self, npc.trade.prices.get_currency_itemstring("tier2")) - local tier1 = npc.inventory_contains(self, npc.trade.prices.get_currency_itemstring("tier1")) - if tier3 ~= nil then - table.insert(result, {name = npc.get_item_name(tier3.item_string), - count = npc.get_item_count(tier3.item_string)} ) - end - if tier2 ~= nil then - table.insert(result, {name = npc.get_item_name(tier2.item_string), - count = npc.get_item_count(tier2.item_string)} ) - end - if tier1 ~= nil then - table.insert(result, {name = npc.get_item_name(tier1.item_string), - count = npc.get_item_count(tier1.item_string)} ) - end - - --minetest.log("Found currency in inventory: "..dump(result)) - return result -end - --- This function will return an offer object, based --- on the items the NPC has. -function npc.trade.get_casual_trade_offer(self, offer_type) - local result = {} - -- Check offer type - if offer_type == npc.trade.OFFER_BUY then - -- Create buy offer based on what the NPC can actually buy - local currencies = npc.trade.get_currencies_in_inventory(self) - -- Choose a random currency - local chosen_tier = currencies[math.random(#currencies)] - -- Get items for this currency - local buyable_items = - npc.trade.prices.get_items_for_currency_count(chosen_tier.name, chosen_tier.count, 0.5) - -- Select a random item from the buyable items - local item_set = {} - for item,price_info in pairs(buyable_items) do - table.insert(item_set, item) - end - local item = item_set[math.random(#item_set)] - -- Choose buying quantity. Since this is a buy offer, NPC will buy items - -- at half the price. Therefore, NPC will always ask for even quantities - -- so that the price count is always an integer number - local amount_to_buy = math.random(buyable_items[item].min_buyable_item_count, buyable_items[item].max_buyable_item_count) - -- Create trade offer - --minetest.log("Buyable item: "..dump(buyable_items[item])) - result = npc.trade.create_offer(npc.trade.OFFER_BUY, item, buyable_items[item].price, buyable_items[item].min_buyable_item_price, amount_to_buy) - else - -- Make sell offer, NPC will sell items to player at regular price - -- NPC will also offer items from their inventory - local sellable_items = {} - for i = 1, #self.inventory do - if self.inventory[i] ~= "" then - if npc.trade.prices.is_item_currency(self.inventory[i]) == false then - table.insert(sellable_items, self.inventory[i]) - end - end - end - -- Check if there are no sellable items to avoid crash - if #sellable_items > 0 then - -- Choose a random item from the sellable items - local item = sellable_items[math.random(#sellable_items)] - -- Choose how many of this item will be sold to player - local count = math.random(npc.get_item_count(item)) - -- Create trade offer - result = npc.trade.create_offer(npc.trade.OFFER_SELL, npc.get_item_name(item), nil, nil, count) - end - end - - return result -end - --- The following function create buy and sell offers for dedicated traders, --- based on the trader list and the source of items. Initially, it will only --- be NPC inventories. In the future, it should support both NPC and chest --- inventories, -function npc.trade.get_dedicated_trade_offers(self) - local offers = { - sell = {}, - buy = {} - } - - local trade_list = self.trader_data.trade_list - - npc.log("INFO", "NPC Inventory: "..dump(self.inventory)) - - for item_name, trade_info in pairs(trade_list) do - -- Abort if more than 12 buy or sell offers are made - if table.getn(offers.sell) >= 12 or table.getn(offers.buy) >= 12 then - break - end - -- For each item on the trader list, check if it is in the NPC inventory. - -- If it is, create a sell offer, else create a buy offer if possible. - -- Also, avoid creating sell offers immediately if the item was just bought - local item = npc.inventory_contains(self, item_name) - minetest.log("Searched item: "..dump(item_name)) - minetest.log("Found: "..dump(item)) - if item ~= nil and trade_info.last_offer_type ~= npc.trade.OFFER_BUY then - -- Check if item can be sold - if trade_info.item_sold_count == nil - or (trade_info.item_sold_count ~= nil - and (trade_info.max_item_sell_count - and trade_info.item_sold_count < trade_info.max_item_sell_count)) then - -- This check makes sure that the NPC will keep max_item_sell_count at any time. - if trade_info.amount_to_keep == nil or (trade_info.amount_to_keep ~= nil - and trade_info.amount_to_keep < ItemStack(item.item_string):get_count()) then - -- Create sell offer for this item. Currently, traders will offer to sell only - -- one of their items to allow the fine control for players to buy what they want. - -- This requires, however, that the trade offers are re-generated everytime a - -- sell is made. - table.insert(offers.sell, npc.trade.create_offer( - npc.trade.OFFER_SELL, - item_name, - nil, - nil, - 1) - ) - -- Set last offer type - trade_info.last_offer_type = npc.trade.OFFER_SELL - end - else - -- Clear the trade info for this item - trade_info.item_sold_count = 0 - end - - else - -- Avoid flipping an item to the buy side if the stock was just depleted - if trade_info.last_offer_type ~= npc.trade.OFFER_SELL then - -- Create buy offer for this item - -- Only do if the NPC can actually afford the items. - local currencies = npc.trade.get_currencies_in_inventory(self) - -- Check if currency isn't empty - if #currencies > 0 then - -- Choose a random currency - local chosen_tier = currencies[math.random(#currencies)] - -- Get items for this currency - local buyable_items = - npc.trade.prices.get_items_for_currency_count(chosen_tier.name, chosen_tier.count, 0.5) - -- Check if the item from trader list is present in the buyable items list - for buyable_item, price_info in pairs(buyable_items) do - if buyable_item == item_name then - -- If item found, create a buy offer for this item - -- Again, offers are created for one item only. Buy offers should be removed - -- after the NPC has bought a certain quantity, say, 5 items. - if trade_info.item_bought_count == nil - or (trade_info.item_bought_count ~= nil - and (trade_info.max_item_buy_count and trade_info.item_bought_count <= trade_info.max_item_buy_count - or trade_info.item_bought_count <= npc.trade.DEDICATED_MAX_BUY_AMOUNT)) then - -- Create trade offer for this item - table.insert(offers.buy, npc.trade.create_offer( - npc.trade.OFFER_BUY, - item_name, - price_info.price, - price_info.min_buyable_item_price, - price_info.min_buyable_item_count) - ) - -- Set last offer type - trade_info.last_offer_type = npc.trade.OFFER_BUY - else - -- Clear the trade info for this item - trade_info.item_bought_count = 0 - end - end - end - end - end - end - end - return offers -end - --- Creates a trade offer based on the offer type, given item and count. If --- the offer is a "buy" offer, it is required to provide the price item and --- the minimum price item count. -function npc.trade.create_offer(offer_type, item, price, min_price_item_count, count) - local result = {} - -- Check offer type - if offer_type == npc.trade.OFFER_BUY then - -- Get price for the given item - -- Create price itemstring - local price_string = price.tier.." " - ..tostring( min_price_item_count * count ) - - -- Build the return object - -- Price is always an array, in this case of size 1 - result = { - offer_type = offer_type, - item = item.." "..count, - price = {[1] = price_string} - } - else - -- Make sell offer, NPC will sell items to player at regular price - -- Get and calculate price for this object - local price_object = npc.trade.prices.get(item) - if price_object == nil then - npc.log("WARNING", "Found nil price for item: "..dump(item)) - return nil - end - -- Check price object, if price < 1 then offer to sell for 1 - if price_object.count < 1 then - price_object.count = 1 - end - local price_string = price_object.tier.." "..tostring(price_object.count * count) - -- Build return object - -- Price is always an array, in this case of size 1 - result = { - offer_type = offer_type, - item = npc.get_item_name(item).." "..count, - price = {[1] = price_string} - } - end - - return result - -end - --- A custom sell trade offer is a special type of trading the NPC can --- have where a different prompt and multiple payment objects are --- required from the player. A good example is offering to repair a sword, --- where the player has to give an amount of currency and the sword to --- repair in exchange to get a fully repaired sword. --- For the moment being, only sell is supported. -function npc.trade.create_custom_sell_trade_offer(option_prompt, dialogue_prompt, button_prompt, item, payments) - return { - offer_type = npc.OFFER_SELL, - option_prompt = option_prompt, - dialogue_prompt = dialogue_prompt, - button_prompt = button_prompt, - item = item, - price = payments - } -end - - --- TODO: This method needs to be refactored to be able to manage --- both NPC inventories and chest inventories. --- Returns true if trade was possible, else returns false. -function npc.trade.perform_trade(self, player_name, offer) - - local item_stack = ItemStack(offer.item) - -- Create item stacks for each price item - local price_stacks = {} - for i = 1, #offer.price do - table.insert(price_stacks, ItemStack(offer.price[i])) - end - local inv = minetest.get_inventory({type = "player", name = player_name}) - - -- Check if offer is a buy or sell - if offer.offer_type == npc.trade.OFFER_BUY then - -- If NPC is buying from player, then player loses item, gets price - -- Check player has the item being buyed - if inv:contains_item("main", item_stack) then - -- Check if there is enough room to add the price item to player - for i = 1, #price_stacks do - if inv:room_for_item("main", price_stacks[i]) then - -- Remove item from player - inv:remove_item("main", item_stack) - -- Remove price item(s) from NPC - for j = 1, #price_stacks do - npc.take_item_from_inventory_itemstring(self, price_stacks[j]) - end - -- Add item to NPC's inventory - npc.add_item_to_inventory_itemstring(self, offer.item) - -- Add price items to player - for j = 1, #price_stacks do - inv:add_item("main", price_stacks[j]) - end - -- Send message to player - npc.chat(self.npc_name, player_name, "Thank you!") - return true - else - npc.chat(self.npc_name, player_name, - "Looks like you can't get what I'm giving you for payment!") - return false - end - end - else - npc.chat(self.npc_name, player_name, - "Looks like you don't have what I want to buy...") - return false - end - else - -- If NPC is selling to the player, then player gives price and gets - -- item, NPC loses item and gets price. - for i = 1, #price_stacks do - -- Check NPC has the required item to pay - if inv:contains_item("main", price_stacks[i]) then - -- Check if there is enough room to add the item to player - if inv:room_for_item("main", item_stack) then - -- Remove price item from player - for j = 1, #price_stacks do - inv:remove_item("main", price_stacks[j]) - end - -- Remove sell item from NPC - npc.take_item_from_inventory_itemstring(self, offer.item) - -- Add price to NPC's inventory - for i = 1, #offer.price do - npc.add_item_to_inventory_itemstring(self, offer.price[i]) - end - -- Add item items to player - inv:add_item("main", item_stack) - -- Send message to player - npc.chat(self.npc_name, player_name, "Thank you!") - return true - else - npc.chat(self.npc_name, player_name, - "Looks like you can't carry anything else...") - return false - end - else - npc.chat(self.npc_name, player_name, - "Looks like you don't have what I'm asking for!") - return false - end - end - end -end - --- Handler for chat formspec -minetest.register_on_player_receive_fields(function (player, formname, fields) - -- Additional checks for other forms should be handled here - -- Handle casual trade dialogue - if formname == "advanced_npc:trade_offer" then - local player_name = player:get_player_name() - - if fields then - local player_response = npc.trade.results.single_trade_offer[player_name] - -- Unlock the action timer - npc.exec.set_ready_state(player_response.npc) - - if fields.yes_option then - npc.trade.perform_trade(player_response.npc, player_name, player_response.trade_offer) - elseif fields.no_option then - minetest.chat_send_player(player_name, "Talk to me if you change your mind!") - end - - end - elseif formname == "advanced_npc:dedicated_trading_offers" then - local player_name = player:get_player_name() - - if fields then - local player_response = npc.trade.results.trade_offers[player_name] - -- Unlock the action timer - npc.exec.set_ready_state(player_response.npc) - - local trade_offers = npc.trade.results.trade_offers[player_name].offers - -- Check which price was clicked - for i = 1, #trade_offers do - local price_button = "price"..tostring(i) - if fields[price_button] then - local trade_result = npc.trade.perform_trade(player_response.npc, player_name, trade_offers[i]) - if trade_result == true then - -- Lock actions - npc.exec.set_input_wait_state(player_response.npc) - -- Account for buyed items - if player_response.offers_type == npc.trade.OFFER_BUY then - -- Increase the item bought count - local offer_item_name = npc.get_item_name(trade_offers[i].item) - --minetest.log("Bought item name: "..dump(offer_item_name)) - --minetest.log(dump(player_response.npc.trader_data.trade_list[offer_item_name])) - -- Check if this item has been bought before - if player_response.npc.trader_data.trade_list[offer_item_name].item_bought_count == nil then - -- Set first count to 1 - player_response.npc.trader_data.trade_list[offer_item_name].item_bought_count = 1 - else - -- Increase count - player_response.npc.trader_data.trade_list[offer_item_name].item_bought_count - = player_response.npc.trader_data.trade_list[offer_item_name].item_bought_count + 1 - end - else - -- Also count how many items are sold - local offer_item_name = npc.get_item_name(trade_offers[i].item) - -- Check if this item has been sold before - if player_response.npc.trader_data.trade_list[offer_item_name].item_sold_count == nil then - -- Set first count to 1 - player_response.npc.trader_data.trade_list[offer_item_name].item_sold_count = 1 - else - -- Increase count - player_response.npc.trader_data.trade_list[offer_item_name].item_sold_count - = player_response.npc.trader_data.trade_list[offer_item_name].item_sold_count + 1 - end - end - -- Re-generate trade offers - npc.trade.generate_trade_offers_by_status(player_response.npc) - -- Show refreshed formspec again to player - npc.trade.show_dedicated_trade_formspec(player_response.npc, player, player_response.offers_type) - return true - else - minetest.close_formspec(player_name, "advanced_npc:dedicated_trading_offers") - return false - end - --minetest.log("Player selected: "..dump(trade_offers[i])) - end - end - end - elseif formname == "advanced_npc:custom_trade_offer" then - -- Handle custom trade formspec - local player_name = player:get_player_name() - - if fields then - local player_response = npc.trade.results.custom_trade_offer[player_name] - -- Unlock the action timer - npc.exec.set_ready_state(player_response.npc) - - if fields.yes_option then - npc.trade.perform_trade(player_response.npc, player_name, player_response.trade_offer) - elseif fields.no_option then - minetest.chat_send_player(player_name, "Talk to me if you change your mind!") - end - - end - end -end) \ No newline at end of file diff --git a/mods/advanced_npc/utils.lua b/mods/advanced_npc/utils.lua deleted file mode 100644 index 1a7db52..0000000 --- a/mods/advanced_npc/utils.lua +++ /dev/null @@ -1,97 +0,0 @@ --- Basic utilities to work with table operations in Lua, and specific querying --- By Zorman2000 - -npc.utils = {} - -function npc.utils.split(inputstr, sep) - if sep == nil then - sep = "%s" - end - local t={} - local i=1 - for str in string.gmatch(inputstr, "([^"..sep.."]+)") do - t[i] = str - i = i + 1 - end - return t -end - -function npc.utils.array_contains(array, item) - --minetest.log("Array: "..dump(array)) - --minetest.log("Item being searched: "..dump(item)) - for i = 1, #array do - --minetest.log("Equals? "..dump(array[i] == item)) - if array[i] == item then - return true - end - end - return false -end - -function npc.utils.array_is_subset_of_array(set, subset) - local match_count = 0 - for j = 1, #subset do - for k = 1, #set do - if subset[j] == set[k] then - match_count = match_count + 1 - end - end - end - -- Check match count - return match_count == #subset -end - -function npc.utils.get_map_keys(map) - local result = {} - for key, _ in pairs(map) do - table.insert(result, key) - end - return result -end - -function npc.utils.get_map_values(map) - local result = {} - for _, value in pairs(map) do - table.insert(result, value) - end - return result -end - --- This function searches for a node given the conditions specified in the --- query object, starting from the given start_pos and up to a certain, specified --- range. --- Query object: --- search_type: determines the direction to search nodes. --- Valid values are: orthogonal, cross, cube --- - orthogonal search means only nodes which are parallel to the search node's faces --- will be considered. This limits the search to only 6 nodes. --- - cross search will look at the same nodes as orthogonal, plus will also --- check nodes diagonal to the node four horizontal nodes. This search looks at 14 nodes --- - cube search means to look every node surrounding the node, including all diagonals. --- This search looks at 26 nodes. --- search_nodes: array of nodes to search for --- surrounding_nodes: object specifying which neighbor nodes are to be expected and --- at which locations. Valid keys are: --- - North (+Z dir) --- - East (+x dir) --- - South (-Z dir) --- - West (-X dir) --- - Top (+Y dir) --- - Bottom (-Y dir) --- Example: ["bottom"] = {nodes={"default:dirt"}, criteria="all"} --- Each object will contain nodes, and criteria for acceptance. --- Criteria values are: --- - any: true as long as one of the nodes on this side is one of the specified --- in "nodes" --- - all: true when the set of neighboring nodes on this side contain one or many of --- the specified "nodes" --- - all-exact: true when the set of neighboring nodes on this side contain all nodes --- specified in "nodes" --- - shape: true when the set of neighboring nodes on this side contains nodes in --- the exact given shape. If so, nodes will not be an array, but a 2d array --- of three rows and three columns, with the specific shape. Notice that --- the nodes on the side can vary depending on the search type (orthogonal, --- cross, cube) -function npc.utils.search_node(query, start_pos, range) - -end \ No newline at end of file