From 60b847a02a9a06e300e3b8a4a2683ca0c6bb16f6 Mon Sep 17 00:00:00 2001 From: zorman2000 Date: Wed, 18 Jan 2017 19:34:02 -0500 Subject: [PATCH] Actions: (WIP) Add locks, unlocks and re-execution of actions if there are interruptions. Need to improve the lock/unlock mechanism. Dialogues, trading: Add lock and unlock upon starting/finishing an interaction. Updated README with progress. Pathfinding: Fix slight bug that avoid a map being generated if the difference of start and end positions' z coordinate is zero. --- README.md | 4 +- actions/pathfinder.lua | 7 ++- dialogue.lua | 52 ++++++++++----- npc.lua | 139 +++++++++++++++++++++++++++++++++++------ trade/trade.lua | 4 ++ 5 files changed, 169 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 0e4f8fc..b1f73ef 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,8 @@ __Phase 3__: Trading: In progress - [ ] Dedicated traders are traders that, when talked to, always make buy and sell offers. They have a greater variety too. - [ ] NPCs will also be able to offer "services", for example, repairing tools, by receiving an item and a payment, and then returning a specific item. -__Phase 4__: Actions: In progress -- [ ] NPCs should be able to use chests, furnaces, doors, beds and sit on "sittable" nodes (in progress) +__Phase 4__: Actions: Main functionality complete +- [x] NPCs should be able to use chests, furnaces, doors, beds and sit on "sittable" nodes (in progress) - [x] NPCs should be able to walk to specific places. Should also be able to open doors, fence gates and any other type of openable node while going to a place. - [x] NPCs should have the ability to identify nodes that belong to him/her, and recall them/ diff --git a/actions/pathfinder.lua b/actions/pathfinder.lua index 932843d..d12c409 100644 --- a/actions/pathfinder.lua +++ b/actions/pathfinder.lua @@ -55,6 +55,10 @@ pathfinder.nodes = { -- of the Jumper library to find a path from start_pos to end_pos. The range is -- an extra amount of nodes to search in both the x and z coordinates. function pathfinder.find_path(start_pos, end_pos, range, walkable_nodes) + -- Check that start and end position are not the same + if start_pos.x == end_pos.x and start_pos.z == end_pos.z then + return nil + end -- Set walkable nodes to empty if parameter wasn't used if walkable_nodes == nil then walkable_nodes = {} @@ -145,7 +149,8 @@ function pathfinder.create_map(start_pos, end_pos, extra_range, walkables) local grid = {} -- Loop through the area and classify nodes - for z = 1, math.abs(pos1.z - pos2.z) do + -- The +2 addition tries to ensure the loop runs at least one. + for z = 1, math.abs(pos1.z - pos2.z) + 2 do local current_row = {} for x = 1, math.abs(pos1.x - pos2.x) do -- Calculate current position diff --git a/dialogue.lua b/dialogue.lua index 1930800..d199908 100644 --- a/dialogue.lua +++ b/dialogue.lua @@ -55,12 +55,15 @@ function npc.dialogue.show_options_dialogue(self, end -- This function is used for showing a yes/no dialogue formspec -function npc.dialogue.show_yes_no_dialogue(prompt, - positive_answer_label, - positive_callback, - negative_answer_label, - negative_callback, - player_name) +function npc.dialogue.show_yes_no_dialogue(self, + prompt, + positive_answer_label, + positive_callback, + negative_answer_label, + negative_callback, + player_name) + + npc.lock_actions(self) local formspec = "size[7,3]".. "label[0.5,0.1;"..prompt.."]".. @@ -69,6 +72,7 @@ function npc.dialogue.show_yes_no_dialogue(prompt, -- Create entry into responses table npc.dialogue.dialogue_results.yes_no_dialogue[player_name] = { + npc = self, yes_callback = positive_callback, no_callback = negative_callback } @@ -176,9 +180,20 @@ 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.lock_actions(self) + -- Send dialogue line if dialogue.text then - minetest.chat_send_player(player_name, dialogue.text) + minetest.chat_send_player(player_name, self.nametag..": "..dialogue.text) + -- Check if dialogue has responses. If it doesn't, unlock the actions + -- queue and reset actions timer.' + minetest.log("Responses: "..dump(dialogue.responses)) + minetest.log("Condition: "..dump(not dialogue.responses)) + if not dialogue.responses then + npc.unlock_actions(self) + end end -- Check if there are responses, then show multi-option dialogue if there are @@ -196,7 +211,8 @@ function npc.dialogue.process_dialogue(self, dialogue, player_name) end ----------------------------------------------------------------------------- --- Functions for rotating NPC to look at player (taken from the API itself) +-- Functions for rotating NPC to look at player +-- (taken from the mobs_redo API) ----------------------------------------------------------------------------- local atan = function(x) if x ~= x then @@ -206,8 +222,7 @@ local atan = function(x) end end - -local function rotate_npc_to_player(self) +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 @@ -244,6 +259,10 @@ minetest.register_on_player_receive_fields(function (player, formname, fields) if fields then local player_response = npc.dialogue.dialogue_results.yes_no_dialogue[player_name] + + -- Unlock queue, reset action timer and unfreeze NPC. + npc.unlock_actions(player_response.npc) + if fields.yes_option then player_response.yes_callback() elseif fields.no_option then @@ -280,7 +299,7 @@ minetest.register_on_player_receive_fields(function (player, formname, fields) npc.relationships.MARRIED_NPC_DIALOGUE .responses[player_response.options[i].response_id] .action(player_response.npc, player) - + elseif player_response.is_casual_trade_dialogue == true then -- Check if trade is casual buy or sell if player_response.casual_trade_type == npc.trade.OFFER_BUY then @@ -288,14 +307,13 @@ minetest.register_on_player_receive_fields(function (player, formname, fields) npc.trade.CASUAL_TRADE_BUY_DIALOGUE .responses[player_response.options[i].response_id] .action(player_response.npc, player) - elseif player_response.casual_trade_type == npc.trade.OFFER_SELL == true then -- Get functions from casual sell dialogue npc.trade.CASUAL_TRADE_SELL_DIALOGUE .responses[player_response.options[i].response_id] - .action(player_response.npc, player) + .action(player_response.npc, player) end - + return else -- Get dialogues for sex and phase local dialogues = npc.data.DIALOGUES[player_response.npc.sex][phase] @@ -304,7 +322,11 @@ minetest.register_on_player_receive_fields(function (player, formname, fields) dialogues[player_response.options[i].dialogue_id] .responses[player_response.options[i].response_id] .action(player_response.npc, player) - end + + -- Unlock queue, reset action timer and unfreeze NPC. + npc.unlock_actions(player_response.npc) + + end end return end diff --git a/npc.lua b/npc.lua index c4911cf..2522ed4 100755 --- a/npc.lua +++ b/npc.lua @@ -27,6 +27,12 @@ npc.direction = { west = 3 } +npc.action_state = { + none = 0, + executing = 1, + interrupted = 2 +} + --------------------------------------------------------------------------------------- -- General functions --------------------------------------------------------------------------------------- @@ -203,8 +209,20 @@ end -- This function removes the first action in the action queue -- and then executes it function npc.execute_action(self) + -- Check if an action was interrupted + if self.actions.current_action_state == npc.action_state.interrupted then + minetest.log("Inserting interrupted action: ") + -- Insert into queue the interrupted action + table.insert(self.actions.queue, 1, self.actions.state_before_lock.interrupted_action) + -- Clear the action + self.actions.state_before_lock.interrupted_action = {} + -- Clear the position + self.actions.state_before_lock.pos = {} + end local result = nil if table.getn(self.actions.queue) == 0 then + -- Set state to none + self.actions.current_action_state = npc.action_state.none -- Keep state the same if there are no more actions in actions queue return self.freeze end @@ -213,10 +231,10 @@ function npc.execute_action(self) -- stack fashion if action_obj.is_task == true then minetest.log("Executing task") - -- Remove from queue - table.remove(self.actions.queue, 1) -- Backup current queue local backup_queue = self.actions.queue + -- Remove this "task" action from queue + table.remove(self.actions.queue, 1) -- Clear queue self.actions.queue = {} -- Now, execute the task with its arguments @@ -226,17 +244,77 @@ function npc.execute_action(self) for i = 1, #backup_queue do table.insert(self.actions.queue, backup_queue[i]) end - minetest.log("New actions queue: "..dump(self)) else minetest.log("Executing action") + -- Store the action that is being executed + self.actions.state_before_lock.interrupted_action = action_obj + -- Store current position + self.actions.state_before_lock.pos = self.object:getpos() -- Execute action as normal result = action_obj.action(action_obj.args) - -- Remove executed action from queue + -- Remove task table.remove(self.actions.queue, 1) + -- Set state + self.actions.current_action_state = npc.action_state.executing end return result end +function npc.lock_actions(self) + + -- Avoid re-locking if already locked + if self.actions.action_timer_lock == true then + return + end + + local pos = self.object:getpos() + + if self.freeze == false then + -- Round current pos to avoid the NPC being stopped on positions + -- where later on can't walk to the correct positions + -- Choose which position is to be taken as start position + if self.actions.state_before_lock.pos ~= {} then + pos = vector.round(self.actions.state_before_lock.pos) + else + pos = vector.round(self.object:getpos()) + end + pos.y = self.object:getpos().y + end + -- Stop NPC + npc.actions.stand({self=self, pos=pos}) + -- Avoid all timer execution + self.actions.action_timer_lock = true + -- Reset timer so that it has some time after interaction is done + self.actions.action_timer = 0 + -- Check if there are is an action executing + if self.actions.current_action_state == npc.action_state.executing + and self.freeze == false then + -- Store the current action state + self.actions.state_before_lock.action_state = self.actions.current_action_state + -- Set current action state to interrupted + self.actions.current_action_state = npc.action_state.interrupted + end + -- Store the current freeze variable + self.actions.state_before_lock.freeze = self.freeze + -- Freeze mobs_redo API + self.freeze = false + + minetest.log("Locking") +end + +function npc.unlock_actions(self) + -- Allow timers to execute + self.actions.action_timer_lock = false + -- Restore the value of self.freeze + self.freeze = self.actions.state_before_lock.freeze + + if table.getn(self.actions.queue) == 0 then + -- Allow mobs_redo API to execute since action queue is empty + self.freeze = true + end + + minetest.log("Unlocked") +end --------------------------------------------------------------------------------------- -- Spawning functions @@ -307,7 +385,7 @@ local function npc_spawn(self, pos) local ent = self:get_luaentity() -- Set name - ent.nametag = "" + ent.nametag = "Kio" -- Set ID ent.npc_id = tostring(math.random(1000, 9999))..":"..ent.nametag @@ -366,7 +444,7 @@ local function npc_spawn(self, pos) select_casual_trade_offers(ent) end - -- Action queue + -- Actions data ent.actions = { -- The queue is a queue of actions to be performed on each interval queue = {}, @@ -374,7 +452,20 @@ local function npc_spawn(self, pos) action_timer = 0, -- Determines the interval for each action in the action queue -- Default is 1. This can be changed via actions - action_interval = 1 + action_interval = 1, + -- Avoid the execution of the action timer + action_timer_lock = false, + -- Defines the state of the current action + current_action_state = npc.action_state.none, + -- Store information about action on state before lock + state_before_lock = { + -- State of the mobs_redo API + freeze = false, + -- State of execution + action_state = npc.action_state.none, + -- Action executed while on lock + interrupted_action = {} + } } -- This flag is checked on every step. If it is true, the rest of @@ -396,7 +487,7 @@ local function npc_spawn(self, pos) --npc.add_action(ent, npc.actions.stand, {self = ent}) if nodes[1] ~= nil then npc.add_task(ent, npc.actions.walk_to_pos, {self=ent, end_pos=nodes[1], walkable={}}) - npc.actions.use_furnace(ent, nodes[1], "default:cobble 10", false) + npc.actions.use_furnace(ent, nodes[1], "default:cobble 5", false) --npc.add_action(ent, npc.actions.sit, {self = ent}) -- npc.add_action(ent, npc.actions.lay, {self = ent}) -- npc.add_action(ent, npc.actions.lay, {self = ent}) @@ -497,10 +588,14 @@ mobs:register_mob("advanced_npc:npc", { }, on_rightclick = function(self, clicker) + -- 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() - minetest.log(dump(self)) + --minetest.log(dump(self)) -- Receive gift or start chat. If player has no item in hand -- then it is going to start chat directly @@ -511,6 +606,7 @@ mobs:register_mob("advanced_npc:npc", { -- Show dialogue to confirm that player is giving item as gift npc.dialogue.show_yes_no_dialogue( + self, "Do you want to give "..item_name.." to "..self.nametag.."?", npc.dialogue.POSITIVE_GIFT_ANSWER_PREFIX..item_name, function() @@ -576,19 +672,24 @@ mobs:register_mob("advanced_npc:npc", { end -- Action queue timer - self.actions.action_timer = self.actions.action_timer + dtime - if self.actions.action_timer >= self.actions.action_interval then - -- Reset action timer - self.actions.action_timer = 0 - -- Execute action - self.freeze = npc.execute_action(self) + -- Check if actions and timers aren't locked + if self.actions.action_timer_lock == false then + -- Increment action timer + self.actions.action_timer = self.actions.action_timer + dtime + if self.actions.action_timer >= self.actions.action_interval then + minetest.log("Current action state = "..dump(self.actions.current_action_state)) + -- Reset action timer + self.actions.action_timer = 0 + -- Execute action + self.freeze = npc.execute_action(self) - if self.freeze == nil and table.getn(self.actions.queue) > 0 then - self.freeze = false + if self.freeze == nil and table.getn(self.actions.queue) > 0 then + self.freeze = false + end end - - end + end + return self.freeze end }) diff --git a/trade/trade.lua b/trade/trade.lua index 53b9b00..6216522 100644 --- a/trade/trade.lua +++ b/trade/trade.lua @@ -258,11 +258,15 @@ minetest.register_on_player_receive_fields(function (player, formname, fields) if fields then local player_response = npc.trade.results.single_trade_offer[player_name] + -- Unlock the action timer + npc.unlock_actions(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