-- buy-button-based trading from trade list: one item(stack) for another item(stack) -- helper function yl_speak_up.get_trade_item_desc = function(item) local stack = ItemStack(item) local def = minetest.registered_items[stack:get_name()] if(def and def.description) then return minetest.formspec_escape(tostring(stack:get_count()).."x "..def.description) end return minetest.formspec_escape(tostring(stack:get_count()).."x "..stack:get_name()) end -- helper function; returns how often a trade can be done -- stock_buy how much of the buy stack does the NPC have in storage? -- stock_pay how much of the price stack does the NPC have in storage? -- buy_stack stack containing the item the NPC sells -- pay_stack stack containing the price for said item -- min_storage how many items of the buy stack items shall the NPC keep? -- max_storage how many items of the pay stack items can the NPC accept? yl_speak_up.get_trade_amount_available = function(stock_buy, stock_pay, buy_stack, pay_stack, min_storage, max_storage) local stock = 0 -- the NPC shall not sell more than this if(min_storage and min_storage > 0) then stock_buy = math.max(0, stock_buy - min_storage) end stock = math.floor(stock_buy / buy_stack:get_count()) -- the NPC shall not buy more than this if(max_storage and max_storage < 10000) then stock_pay = math.min(max_storage - stock_pay, 10000) stock = math.min(stock, math.floor(stock_pay / pay_stack:get_count())) end return stock end -- helper function; also used by fs_trade_list.lua yl_speak_up.get_sorted_trade_id_list = function(dialog, show_dialog_option_trades) -- make sure all fields exist yl_speak_up.setup_trade_limits(dialog) local keys = {} if(show_dialog_option_trades) then for k, v in pairs(dialog.trades) do if(k ~= "limits" and k ~= "" and v.d_id) then table.insert(keys, k) end end else for k, v in pairs(dialog.trades) do if(k ~= "limits" and k ~= "") then -- structure of the indices: sell name amount for name amount local parts = string.split(k, " ") if(parts and #parts == 6 and parts[4] == "for" and v.pay and v.pay[1] ~= "" and v.pay[1] == parts[5].." "..parts[6] and v.buy and v.buy[1] ~= "" and v.buy[1] == parts[2].." "..parts[3] and minetest.registered_items[parts[5]] and minetest.registered_items[parts[2]] and tonumber(parts[6]) > 0 and tonumber(parts[3]) > 0) then table.insert(keys, k) end end end end table.sort(keys) return keys end yl_speak_up.input_trade_via_buy_button = function(player, formname, fields) local pname = player:get_player_name() if(fields.buy_directly) then local trade_id = yl_speak_up.speak_to[pname].trade_id local res = yl_speak_up.check_trade_via_buy_button(player, trade_id, true) if(res.msg ~= "OK") then yl_speak_up.show_fs(player, "msg", { input_to = "yl_speak_up:trade_via_buy_button", formspec = "size[6,2.5]".. "label[0.2,-0.2;"..res.msg.."\nTrade aborted.]".. "button[2,1.5;1,0.9;back_from_error_msg;Back]"}) return end yl_speak_up.show_fs(player, "trade_via_buy_button", trade_id) return end -- scroll through the trades with prev/next buttons if(fields.prev_trade or fields.next_trade) then local pname = player:get_player_name() local n_id = yl_speak_up.speak_to[pname].n_id local dialog = yl_speak_up.speak_to[pname].dialog local keys = yl_speak_up.speak_to[pname].trade_id_list or {} local trade_id = yl_speak_up.speak_to[pname].trade_id local idx = math.max(1, table.indexof(keys, trade_id)) if(fields.prev_trade) then idx = idx - 1 elseif(fields.next_trade) then idx = idx + 1 end if(idx > #keys) then idx = 1 elseif(idx < 1) then idx = #keys end yl_speak_up.speak_to[pname].trade_id = keys[idx] -- this is another trade; count from 0 again yl_speak_up.speak_to[pname].trade_done = nil end if(fields.delete_trade_via_buy_button) then local trade_id = yl_speak_up.speak_to[pname].trade_id yl_speak_up.delete_trade_simple(player, trade_id) return end -- the owner wants to go back to the trade list from a dialog trade (action) view if(fields.back_to_trade_list_dialog_trade) then yl_speak_up.show_fs(player, "trade_list", true) return -- a dialog trade (action) was displayed; go back to the corresponding dialog elseif(fields.back_to_dialog) then local pname = player:get_player_name() local n_id = yl_speak_up.speak_to[pname].n_id local dialog = yl_speak_up.speak_to[pname].dialog local trade_id = yl_speak_up.speak_to[pname].trade_id local new_d_id = dialog.trades[ trade_id ].d_id yl_speak_up.speak_to[pname].d_id = new_d_id yl_speak_up.speak_to[pname].trade_list = {} yl_speak_up.show_fs(player, "talk") -- TODO parameters return -- show the trade list elseif(fields.back_to_trade_list or fields.quit or not(yl_speak_up.speak_to[pname].trade_id)) then yl_speak_up.show_fs(player, "trade_list") return end -- show the trade formspec again yl_speak_up.show_fs(player, "trade_via_buy_button", yl_speak_up.speak_to[pname].trade_id) end -- helper function -- if do_trade is false: check only if the trade would be possible and return -- error message if not; return "OK" when trade is possible -- if do_trade is true: if possible, execute the trade; return the same as above -- also returns how many times the trade could be done (stock= ..) yl_speak_up.check_trade_via_buy_button = function(player, trade_id, do_trade) local pname = player:get_player_name() local n_id = yl_speak_up.speak_to[pname].n_id local dialog = yl_speak_up.speak_to[pname].dialog -- make sure all necessary table entries exist yl_speak_up.setup_trade_limits(dialog) local this_trade = dialog.trades[trade_id] -- the players' inventory local player_inv = player:get_inventory() -- the NPCs' inventory local npc_inv = minetest.get_inventory({type="detached", name="yl_speak_up_npc_"..tostring(n_id)}) if(not(this_trade)) then return {msg = "Trade not found.", stock=0} elseif(not(player_inv)) then return {msg = "Couldn't find player's inventory.", stock=0} elseif(not(npc_inv)) then return {msg = "Couldn't find the NPC's inventory.", stock=0} end -- store which trade we're doing yl_speak_up.speak_to[pname].trade_id = trade_id -- what the player pays to the npc: local pay_stack = ItemStack(dialog.trades[ trade_id ].pay[1]) -- what the npc sells and the player buys: local buy_stack = ItemStack(dialog.trades[ trade_id ].buy[1]) local npc_name = minetest.formspec_escape(dialog.n_npc) -- can the NPC provide his part? if(not(npc_inv:contains_item("npc_main", buy_stack))) then return {msg = "Out of stock", stock = 0} -- return {msg = "Sorry. "..npc_name.." ran out of stock.\nPlease come back later.", stock=0} -- has the NPC room for the payment? elseif(not(npc_inv:room_for_item("npc_main", pay_stack))) then return {npc_name.." has no room left!", stock = 0} -- return "Sorry. "..npc_name.." ran out of inventory space.\n".. -- "There is no room to store your payment!" end local counted_npc_inv = yl_speak_up.count_npc_inv(n_id) local stock_pay = counted_npc_inv[ pay_stack:get_name() ] or 0 local stock_buy = counted_npc_inv[ buy_stack:get_name() ] or 0 -- are there any limits which we have to take into account? local min_storage = dialog.trades.limits.sell_if_more[ buy_stack:get_name() ] local max_storage = dialog.trades.limits.buy_if_less[ pay_stack:get_name() ] if((min_storage and min_storage > 0) or (max_storage and max_storage < 10000)) then -- trade limit: is enough left after the player buys the item? if( min_storage and min_storage > stock_buy - buy_stack:get_count()) then -- return "Stock too low. Only "..tostring(stock_buy).. -- " left, want to keep "..tostring(min_storage).."." return {msg = "Sorry. "..npc_name.." currently does not want to\nsell that much.".. " Current stock: "..tostring(stock_buy).. " (min: "..tostring(min_storage).."). Perhaps later?", stock = 0} -- trade limit: make sure the bought amount does not exceed the desired maximum elseif(max_storage and max_storage < stock_pay + pay_stack:get_count()) then return {msg = "Sorry. "..npc_name.." currently does not want to\n".. "buy that much.".. " Current stock: "..tostring(stock_pay).. " (max: "..tostring(max_storage).."). Perhaps later?", stock = 0} end -- the NPC shall not sell more than this if(min_storage and min_storage > 0) then stock_buy = math.max(0, stock_buy - min_storage) end end -- how often can this trade be done? local stock = yl_speak_up.get_trade_amount_available( stock_buy, stock_pay, buy_stack, pay_stack, min_storage, max_storage) -- can the player pay? if(not(player_inv:contains_item("main", pay_stack))) then -- both slots will remain empty return {msg = "You can't pay the price.", stock = stock} elseif(not(player_inv:room_for_item("main", buy_stack))) then -- the player has no room for the sold item; give a warning return {msg = "You don't have enough free inventory\nspace to store your purchase.", stock = stock} end -- was it a dry run to check if the trade is possible? if(not(do_trade)) then return {msg = "OK", stock = stock} end -- actually do the trade local payment = player_inv:remove_item("main", pay_stack) local sold = npc_inv:remove_item("npc_main", buy_stack) -- used items cannot be sold as there is no fair way to indicate how -- much they are used if(payment:get_wear() > 0 or sold:get_wear() > 0) then -- revert the trade player_inv:add_item("main", payment) npc_inv:add_item("npc_main", sold) return {msg = "Sorry. "..npc_name.." accepts only undammaged items.", stock = stock} end player_inv:add_item("main", sold) npc_inv:add_item("npc_main", payment) -- save the inventory of the npc so that the payment does not get lost yl_speak_up.save_npc_inventory( n_id ) -- store for statistics how many times the player has executed this trade -- (this is also necessary to switch to the right target dialog when -- dealing with dialog options trades) if(not(yl_speak_up.speak_to[pname].trade_done)) then yl_speak_up.speak_to[pname].trade_done = 0 end yl_speak_up.speak_to[pname].trade_done = yl_speak_up.speak_to[pname].trade_done + 1 -- log the trade yl_speak_up.log_change(pname, n_id, "bought "..tostring(buy_stack:to_string()).. " for "..tostring(pay_stack:to_string())) return {msg = "OK", stock = stock} end -- trade for a player (the owner of the NPC): one item(stack) for another -- trade by clicking on the "buy" button instead of moving inventory items around -- checks if payment and buying is possible yl_speak_up.get_fs_trade_via_buy_button = function(player, trade_id) local pname = player:get_player_name() local n_id = yl_speak_up.speak_to[pname].n_id local dialog = yl_speak_up.speak_to[pname].dialog -- make sure all necessary table entries exist yl_speak_up.setup_trade_limits(dialog) local this_trade = dialog.trades[trade_id] -- the players' inventory local player_inv = player:get_inventory() -- the NPCs' inventory local npc_inv = minetest.get_inventory({type="detached", name="yl_speak_up_npc_"..tostring(n_id)}) if(not(this_trade) or not(player_inv) or not(npc_inv)) then return yl_speak_up.trade_fail_fs end -- what the player pays to the npc: local pay = dialog.trades[ trade_id ].pay[1] -- what the npc sells and the player buys: local buy = dialog.trades[ trade_id ].buy[1] local pay_name = yl_speak_up.get_trade_item_desc(pay) local buy_name = yl_speak_up.get_trade_item_desc(buy) local npc_name = minetest.formspec_escape(dialog.n_npc) -- get the number of the trade local keys = yl_speak_up.speak_to[pname].trade_id_list or {} local idx = math.max(1, table.indexof(keys, trade_id)) -- the common formspec, shared by actual trade and configuration -- no listring here as that would make things more complicated local formspec = { -- "size[8.5,8]".. yl_speak_up.show_fs_simple_deco(10, 8.8).. "container[0.75,0]".. "label[4.35,1.4;", npc_name, " sells:]", "list[current_player;main;0.2,4.55;8,1;]", "list[current_player;main;0.2,5.78;8,3;8]", -- "label[7.0,0.2;Offer ", tostring(idx), "/", tostring(#keys), "]", -- "label[2.5,0.7;Trading with ", npc_name, "]", "label[1.5,0.7;Offer no. ", tostring(idx), "/", tostring(#keys), " from ", npc_name, "]", "label[1.5,1.4;You pay:]", -- show images of price and what is sold so that the player knows what -- it costs and what he will get even if the trade is not possible at -- that moment -- "item_image[2.1,2.0;1,1;", "item_image_button[2.1,1.9;1.2,1.2;", tostring(pay), ";pay_item_img;", "]", -- "item_image[5.1,2.0;1,1;", -- tostring(buy), "item_image_button[5.1,1.9;1.2,1.2;", tostring(buy), ";buy_item_img;", "]", "label[1.5,3.0;", pay_name, "]", "label[4.35,3.0;", buy_name, "]", "image[3.5,2.0;1,1;gui_furnace_arrow_bg.png^[transformR270]", } if(not(dialog.trades[ trade_id ].d_id)) then -- go back to the trade list table.insert(formspec, "button[0.2,0.0;8.0,1.0;back_to_trade_list;Back to trade list]") table.insert(formspec, "tooltip[back_to_trade_list;".. "Click here once you've traded enough with this ".. "NPC and want to get back to the trade list.]") elseif(true) then -- go back to the trade list table.insert(formspec, "button[0.2,0.0;3.8,1.0;back_to_trade_list_dialog_trade;Back to trade list]") table.insert(formspec, "tooltip[back_to_trade_list_dialog_trade;".. "Click here once you've traded enough with this ".. "NPC and want to get back to the trade list.]") -- go back to dialog table.insert(formspec, "button[4.2,0.0;3.8,1.0;back_to_dialog;Back to dialog ") table.insert(formspec, minetest.formspec_escape(dialog.trades[ trade_id ].d_id).. " (option ".. minetest.formspec_escape(dialog.trades[ trade_id ].o_id).. ")") table.insert(formspec, "]") table.insert(formspec, "tooltip[back_to_dialog;".. "Click here once you've traded enough with this ".. "NPC and want to get back to talking with the NPC.]") else -- go back to dialog table.insert(formspec, "button[0.2,0.0;8.0,1.0;back_to_dialog;Back to dialog ") table.insert(formspec, minetest.formspec_escape(dialog.trades[ trade_id ].d_id)) table.insert(formspec, "]") table.insert(formspec, "tooltip[back_to_dialog;".. "Click here once you've traded enough with this ".. "NPC and want to get back to talking with the NPC.]") end -- show edit button for the owner if in edit_mode if(yl_speak_up.may_edit_npc(player, n_id)) then -- for trades in trade list: allow delete (new trades can easily be added) -- allow delete for trades in trade list even if not in edit mode -- (entering edit mode for that would be too much work) table.insert(formspec, "button[0.2,2.0;1.2,0.9;delete_trade_via_buy_button;Delete]".. "tooltip[delete_trade_via_buy_button;".. "Delete this trade. You can do so only if\n".. "you can edit the NPC as such (i.e. own it).]") end -- dry-run: test if the trade can be done local res = yl_speak_up.check_trade_via_buy_button(player, trade_id, false) if(res.msg == "OK") then local buy_str = "Buy" local trade_done = yl_speak_up.speak_to[pname].trade_done if(trade_done and trade_done > 0) then buy_str = "Buy again. Bought: "..tostring(trade_done).."x" end table.insert(formspec, "button[0.2,3.5;8.0,1.0;buy_directly;") -- "button[6.5,2.0;1.7,0.9;buy_directly;Buy]".. table.insert(formspec, buy_str) table.insert(formspec, "]") table.insert(formspec, "tooltip[buy_directly;Click here in order to buy.]") else -- set a red background color in order to alert thep layer to the error table.insert(formspec, "style_type[button;bgcolor=#FF4444]".. "button[0.2,3.5;8.0,1.0;back_from_error_msg;") -- table.insert(formspec, "label[0.5,3.5;") table.insert(formspec, res.msg) table.insert(formspec, ']') -- set the background color for the next buttons back to our normal one table.insert(formspec, 'style_type[button;bgcolor=#a37e45]') -- table.insert(formspec, "label[6.5,2.0;Trade not\npossible.]") end -- how often can this trade be repeated? if(res.stock and res.stock > 0) then table.insert(formspec, "label[6.5,2.0;Trade ") table.insert(formspec, tostring(res.stock)) table.insert(formspec, " x\navailable]") end table.insert(formspec, "container_end[]".. "real_coordinates[true]".. "button[0.5,1.9;0.8,2.0;prev_trade;<]".. "button[11.7,1.9;0.8,2.0;next_trade;>]".. "tooltip[prev_trade;Show previous trade offer]".. "tooltip[next_trade;Show next trade offer]") return table.concat(formspec, '') end