forked from your-land-mirror/yl_speak_up
403 lines
14 KiB
Lua
403 lines
14 KiB
Lua
--
|
|
-- These functions here access and manipulate the "dialogs" data structure.
|
|
-- It is loaded for each player whenever the player talks to an NPC. Each
|
|
-- talking player gets *a copy* of that data structure.
|
|
--
|
|
-- As this mod is about this "dialogs" data structure and its editing, this
|
|
-- isn't the only place in this mod where the data structure is accessed
|
|
-- and/or manipulated. This here just contains some common functions.
|
|
--
|
|
--###
|
|
-- Helpers
|
|
--###
|
|
|
|
yl_speak_up.get_number_from_id = function(any_id)
|
|
if(not(any_id) or any_id == "d_got_item" or any_id == "d_end" or any_id == "d_dynamic") then
|
|
return "0"
|
|
end
|
|
return string.split(any_id, "_")[2]
|
|
end
|
|
|
|
|
|
yl_speak_up.find_next_id = function(t)
|
|
local start_id = 1
|
|
|
|
if t == nil then
|
|
return start_id
|
|
end
|
|
|
|
local keynum = 1
|
|
for k, _ in pairs(t) do
|
|
local keynum = tonumber(yl_speak_up.get_number_from_id(k))
|
|
if keynum and keynum >= start_id then
|
|
start_id = keynum + 1
|
|
end
|
|
end
|
|
return start_id
|
|
end
|
|
|
|
yl_speak_up.sanitize_sort = function(options, value)
|
|
local retval = value
|
|
|
|
if value == "" or value == nil or tonumber(value) == nil then
|
|
local temp = 0
|
|
for k, v in pairs(options) do
|
|
if v.o_sort ~= nil then
|
|
if tonumber(v.o_sort) > temp then
|
|
temp = tonumber(v.o_sort)
|
|
end
|
|
end
|
|
end
|
|
retval = tostring(temp + 1)
|
|
end
|
|
return retval
|
|
end
|
|
|
|
|
|
|
|
--###
|
|
--Formspecs
|
|
--###
|
|
|
|
|
|
-- helper function
|
|
-- the option to override next_id and provide a value is needed when a new dialog was
|
|
-- added, then edited, and then discarded; it's still needed after that, but has to
|
|
-- be reset to empty state (wasn't stored before)
|
|
yl_speak_up.add_new_dialog = function(dialog, pname, next_id, dialog_text)
|
|
if(not(next_id)) then
|
|
next_id = yl_speak_up.find_next_id(dialog.n_dialogs)
|
|
end
|
|
local future_d_id = "d_" .. next_id
|
|
-- Initialize empty dialog
|
|
dialog.n_dialogs[future_d_id] = {
|
|
d_id = future_d_id,
|
|
d_type = "text",
|
|
d_text = (dialog_text or ""),
|
|
d_sort = next_id
|
|
}
|
|
-- store that there have been changes to this npc
|
|
-- (better ask only when the new dialog is changed)
|
|
-- table.insert(yl_speak_up.npc_was_changed[ yl_speak_up.edit_mode[pname] ],
|
|
-- "Dialog "..future_d_id..": New dialog added.")
|
|
|
|
-- add an option for going back to the start of the dialog;
|
|
-- this is an option which the player can delete and change according to needs,
|
|
-- not a fixed button which may not always fit
|
|
if(not(dialog_text)) then
|
|
-- we want to go back to the start from here
|
|
local target_dialog = yl_speak_up.get_start_dialog_id(dialog)
|
|
-- this text will be used for the button
|
|
local option_text = "Let's go back to the start of our talk."
|
|
-- we just created this dialog - this will be the first option
|
|
yl_speak_up.add_new_option(dialog, pname, "1", future_d_id, option_text, target_dialog)
|
|
end
|
|
return future_d_id
|
|
end
|
|
|
|
-- add a new option/answer to dialog d_id with option_text (or default "")
|
|
-- option_text (optional) the text that shall be shown as option/answer
|
|
-- target_dialog (optional) the target dialog where the player will end up when choosing
|
|
-- this option/answer
|
|
yl_speak_up.add_new_option = function(dialog, pname, next_id, d_id, option_text, target_dialog)
|
|
if(not(dialog) or not(dialog.n_dialogs) or not(dialog.n_dialogs[d_id])) then
|
|
return nil
|
|
end
|
|
if dialog.n_dialogs[d_id].d_options == nil then
|
|
-- make sure d_options exists
|
|
dialog.n_dialogs[d_id].d_options = {}
|
|
else
|
|
-- we don't want an infinite amount of answers per dialog
|
|
local sorted_list = yl_speak_up.get_sorted_options(dialog.n_dialogs[d_id].d_options, "o_sort")
|
|
local anz_options = #sorted_list
|
|
if(anz_options >= yl_speak_up.max_number_of_options_per_dialog) then
|
|
-- nothing added
|
|
return nil
|
|
end
|
|
end
|
|
if(not(next_id)) then
|
|
next_id = yl_speak_up.find_next_id(dialog.n_dialogs[d_id].d_options)
|
|
end
|
|
local future_o_id = "o_" .. next_id
|
|
dialog.n_dialogs[d_id].d_options[future_o_id] = {
|
|
o_id = future_o_id,
|
|
o_hide_when_prerequisites_not_met = "false",
|
|
o_grey_when_prerequisites_not_met = "false",
|
|
o_sort = -1,
|
|
o_text_when_prerequisites_not_met = "",
|
|
o_text_when_prerequisites_met = (option_text or ""),
|
|
}
|
|
-- necessary in order for it to work
|
|
local s = yl_speak_up.sanitize_sort(dialog.n_dialogs[d_id].d_options, yl_speak_up.speak_to[pname].o_sort)
|
|
dialog.n_dialogs[d_id].d_options[future_o_id].o_sort = s
|
|
-- log only in edit mode
|
|
local n_id = yl_speak_up.speak_to[pname].n_id
|
|
-- would be too difficult to add an exception for edit_mode here; thus, we do it directly here:
|
|
if(yl_speak_up.npc_was_changed
|
|
and yl_speak_up.npc_was_changed[n_id]) then
|
|
table.insert(yl_speak_up.npc_was_changed[ n_id ],
|
|
"Dialog "..d_id..": Added new option/answer "..future_o_id..".")
|
|
end
|
|
|
|
-- letting d_got_item point back to itself is not a good idea because the
|
|
-- NPC will then end up in a loop; plus the d_got_item dialog is intended for
|
|
-- automatic processing, not for showing to the player
|
|
if(d_id == "d_got_item") then
|
|
-- unless the player specifies something better, we go back to the start dialog
|
|
-- (that is where d_got_item got called from anyway)
|
|
target_dialog = yl_speak_up.get_start_dialog_id(dialog)
|
|
-- ...and this option needs to be selected automaticly
|
|
dialog.n_dialogs[d_id].d_options[future_o_id].o_autoanswer = 1
|
|
elseif(d_id == "d_trade") then
|
|
-- we really don't want to go to another dialog from here
|
|
target_dialog = "d_trade"
|
|
-- ...and this option needs to be selected automaticly
|
|
dialog.n_dialogs[d_id].d_options[future_o_id].o_autoanswer = 1
|
|
end
|
|
local future_r_id = nil
|
|
-- create a fitting dialog result automaticly if possible:
|
|
-- give this new dialog a dialog result that leads back to this dialog
|
|
-- (which is more helpful than creating tons of empty dialogs)
|
|
if(target_dialog and (dialog.n_dialogs[target_dialog] or target_dialog == "d_end")) then
|
|
future_r_id = yl_speak_up.add_new_result(dialog, d_id, future_o_id)
|
|
-- actually store the new result
|
|
dialog.n_dialogs[d_id].d_options[future_o_id].o_results = {}
|
|
dialog.n_dialogs[d_id].d_options[future_o_id].o_results[future_r_id] = {
|
|
r_id = future_r_id,
|
|
r_type = "dialog",
|
|
r_value = target_dialog}
|
|
end
|
|
|
|
-- the d_got_item dialog is special; players can easily forget to add the
|
|
-- necessary preconditions and effects, so we do that manually here
|
|
if(d_id == "d_got_item") then
|
|
-- we also need a precondition so that the o_autoanswer can actually get called
|
|
dialog.n_dialogs[d_id].d_options[future_o_id].o_prerequisites = {}
|
|
-- we just added this option; this is the first and for now only precondition for it;
|
|
-- the player still has to adjust it, but at least it is a reasonable default
|
|
dialog.n_dialogs[d_id].d_options[future_o_id].o_prerequisites["p_1"] = {
|
|
p_id = "p_1",
|
|
p_type = "player_offered_item",
|
|
p_item_stack_size = tostring(next_id),
|
|
p_match_stack_size = "exactly",
|
|
-- this is just a simple example item and ought to be changed after adding
|
|
p_value = "default:stick "..tostring(next_id)}
|
|
-- we need to show the player that his action was successful
|
|
dialog.n_dialogs[d_id].d_options[future_o_id].o_results[future_r_id].alternate_text =
|
|
"Thank you for the "..tostring(next_id).." stick(s)! "..
|
|
"Never can't have enough sticks.\n$TEXT$"
|
|
-- we need an effect for accepting the item;
|
|
-- taking all that was offered and putting it into the NPC's inventory is a good default
|
|
future_r_id = yl_speak_up.add_new_result(dialog, d_id, future_o_id)
|
|
dialog.n_dialogs[d_id].d_options[future_o_id].o_results[future_r_id] = {
|
|
r_id = future_r_id,
|
|
r_type = "deal_with_offered_item",
|
|
r_value = "take_all"}
|
|
|
|
-- the trade dialog is equally special
|
|
elseif(d_id == "d_trade") then
|
|
dialog.n_dialogs[d_id].d_options[future_o_id].o_prerequisites = {}
|
|
-- this is just an example
|
|
dialog.n_dialogs[d_id].d_options[future_o_id].o_prerequisites["p_1"] = {
|
|
p_id = "p_1",
|
|
p_type = "npc_inv",
|
|
p_value = "inv_does_not_contain",
|
|
p_inv_list_name = "npc_main",
|
|
p_itemstack = "default:stick "..tostring(100-next_id)}
|
|
future_r_id = yl_speak_up.add_new_result(dialog, d_id, future_o_id)
|
|
-- example craft
|
|
dialog.n_dialogs[d_id].d_options[future_o_id].o_results[future_r_id] = {
|
|
r_id = future_r_id,
|
|
r_type = "craft",
|
|
r_value = "default:stick 4",
|
|
o_sort = "1",
|
|
r_craft_grid = {"default:wood", "", "", "", "", "", "", "", ""}}
|
|
end
|
|
return future_o_id
|
|
end
|
|
|
|
|
|
-- add a new result to option o_id of dialog d_id
|
|
yl_speak_up.add_new_result = function(dialog, d_id, o_id)
|
|
if(not(dialog) or not(dialog.n_dialogs) or not(dialog.n_dialogs[d_id])
|
|
or not(dialog.n_dialogs[d_id].d_options) or not(dialog.n_dialogs[d_id].d_options[o_id])) then
|
|
return
|
|
end
|
|
-- create a new result (first the id, then the actual result)
|
|
local future_r_id = "r_" .. yl_speak_up.find_next_id(dialog.n_dialogs[d_id].d_options[o_id].o_results)
|
|
if future_r_id == "r_1" then
|
|
dialog.n_dialogs[d_id].d_options[o_id].o_results = {}
|
|
end
|
|
dialog.n_dialogs[d_id].d_options[o_id].o_results[future_r_id] = {}
|
|
return future_r_id
|
|
end
|
|
|
|
|
|
-- this is useful for result types that can exist only once per option
|
|
-- (apart from editing with the staff);
|
|
-- examples: "dialog" and "trade";
|
|
-- returns tue r_id or nil if no result of that type has been found
|
|
yl_speak_up.get_result_id_by_type = function(dialog, d_id, o_id, result_type)
|
|
if(not(dialog) or not(dialog.n_dialogs) or not(dialog.n_dialogs[d_id])
|
|
or not(dialog.n_dialogs[d_id].d_options) or not(dialog.n_dialogs[d_id].d_options[o_id])) then
|
|
return
|
|
end
|
|
local results = dialog.n_dialogs[d_id].d_options[o_id].o_results
|
|
if(not(results)) then
|
|
return
|
|
end
|
|
for k, v in pairs(results) do
|
|
if(v.r_type == result_type) then
|
|
return k
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- helper function for sorting options/answers using options[o_id].o_sort
|
|
-- (or dialogs by d_sort)
|
|
yl_speak_up.get_sorted_options = function(options, sort_by)
|
|
local sorted_list = {}
|
|
for k,v in pairs(options) do
|
|
table.insert(sorted_list, k)
|
|
end
|
|
table.sort(sorted_list,
|
|
function(a,b)
|
|
if(not(options[a][sort_by])) then
|
|
return false
|
|
elseif(not(options[b][sort_by])) then
|
|
return true
|
|
-- sadly not all entries are numeric
|
|
elseif(tonumber(options[a][sort_by]) and tonumber(options[b][sort_by])) then
|
|
return (tonumber(options[a][sort_by]) < tonumber(options[b][sort_by]))
|
|
-- numbers have a higher priority
|
|
elseif(tonumber(options[a][sort_by])) then
|
|
return true
|
|
elseif(tonumber(options[b][sort_by])) then
|
|
return false
|
|
-- if the value is the same: sort by index
|
|
elseif(options[a][sort_by] == options[b][sort_by]) then
|
|
return (a < b)
|
|
else
|
|
return (options[a][sort_by] < options[b][sort_by])
|
|
end
|
|
end
|
|
)
|
|
return sorted_list
|
|
end
|
|
|
|
|
|
-- simple sort of keys of a table numericly;
|
|
-- this is not efficient - but that doesn't matter: the lists are small and
|
|
-- it is only executed when configuring an NPC
|
|
-- simple: if the parameter is true, the keys will just be sorted (i.e. player names) - which is
|
|
-- not enough for d_<nr>, o_<nr> etc. (which need more care when sorting)
|
|
yl_speak_up.sort_keys = function(t, simple)
|
|
local keys = {}
|
|
for k, v in pairs(t) do
|
|
-- add a prefix so that p_2 ends up before p_10
|
|
if(not(simple) and string.len(k) == 3) then
|
|
k = "a"..k
|
|
end
|
|
table.insert(keys, k)
|
|
end
|
|
table.sort(keys)
|
|
if(simple) then
|
|
return keys
|
|
end
|
|
for i,k in ipairs(keys) do
|
|
-- avoid cutting the single a from a_1 (action 1)
|
|
if(k and string.sub(k, 1, 1) == "a" and string.sub(k, 2, 2) ~= "_") then
|
|
-- remove the leading blank
|
|
keys[i] = string.sub(k, 2)
|
|
end
|
|
end
|
|
return keys
|
|
end
|
|
|
|
|
|
-- checks if dialog contains d_id and o_id
|
|
yl_speak_up.check_if_dialog_has_option = function(dialog, d_id, o_id)
|
|
return (dialog and d_id and o_id
|
|
and dialog.n_dialogs
|
|
and dialog.n_dialogs[d_id]
|
|
and dialog.n_dialogs[d_id].d_options
|
|
and dialog.n_dialogs[d_id].d_options[o_id])
|
|
end
|
|
|
|
-- checks if dialog exists
|
|
yl_speak_up.check_if_dialog_exists = function(dialog, d_id)
|
|
return (dialog and d_id
|
|
and dialog.n_dialogs
|
|
and dialog.n_dialogs[d_id])
|
|
end
|
|
|
|
|
|
|
|
yl_speak_up.is_special_dialog = function(d_id)
|
|
if(not(d_id)) then
|
|
return false
|
|
end
|
|
return (d_id == "d_trade" or d_id == "d_got_item" or d_id == "d_dynamic" or d_id == "d_end")
|
|
end
|
|
|
|
|
|
yl_speak_up.d_name_to_d_id = function(dialog, d_name)
|
|
if(not(dialog) or not(dialog.n_dialogs) or not(d_name) or d_name == "") then
|
|
return nil
|
|
end
|
|
-- it is already the ID of an existing dialog
|
|
if(dialog.n_dialogs[d_name]) then
|
|
return d_name
|
|
end
|
|
-- search all dialogs for one with a fitting d_name
|
|
for k,v in pairs(dialog.n_dialogs) do
|
|
if(v and v.d_name and v.d_name == d_name) then
|
|
return k
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- get the name of a dialog (reverse of above)
|
|
yl_speak_up.d_id_to_d_name = function(dialog, d_id)
|
|
if(not(dialog) or not(dialog.n_dialogs) or not(d_id) or d_id == ""
|
|
or not(dialog.n_dialogs[d_id])
|
|
or not(dialog.n_dialogs[d_id].d_name)
|
|
or dialog.n_dialogs[d_id].d_name == "") then
|
|
return d_id
|
|
end
|
|
return dialog.n_dialogs[d_id].d_name
|
|
end
|
|
|
|
|
|
yl_speak_up.get_sorted_dialog_name_list = function(dialog)
|
|
local liste = {}
|
|
if(dialog and dialog.n_dialogs) then
|
|
for k, v in pairs(dialog.n_dialogs) do
|
|
-- this will be used for dropdown lists - so we use formspec_escape
|
|
table.insert(liste, minetest.formspec_escape(v.d_name or k or "?"))
|
|
end
|
|
-- sort alphabethicly
|
|
table.sort(liste)
|
|
end
|
|
return liste
|
|
end
|
|
|
|
|
|
-- how many own (not special, not generic) dialogs does the NPC have?
|
|
yl_speak_up.count_dialogs = function(dialog)
|
|
local count = 0
|
|
if(not(dialog) or not(dialog.n_dialogs)) then
|
|
return 0
|
|
end
|
|
for d_id, v in pairs(dialog.n_dialogs) do
|
|
if(d_id
|
|
and not(yl_speak_up.is_special_dialog(d_id))
|
|
and not(dialog.n_dialogs[d_id].is_generic)) then
|
|
count = count + 1
|
|
end
|
|
end
|
|
return count
|
|
end
|