forked from Sokomine/yl_speak_up
added general support for generic dialogs
This commit is contained in:
parent
f8138908bc
commit
5c95c6ae6d
47
README.md
47
README.md
@ -294,3 +294,50 @@ contain all the blocks which do not allow the NPCs this kind of
|
||||
interaction.
|
||||
You may i.e. set the put and take tables for blocks that do extensive
|
||||
checks on the player object which the NPC simply can't provide.
|
||||
|
||||
Generic behaviour
|
||||
=================
|
||||
Sometimes you may have a group of NPC that ought to show a common behaviour
|
||||
- like for example guards, smiths, bakers, or inhabitants of a town, or other
|
||||
NPC that have something in common. Not each NPC may warrant its own, individual
|
||||
dialogs.
|
||||
|
||||
The Tutoial (TODO!) is another example: Not each NPC needs to present the player
|
||||
with a tutorial, but those that are owned and where the owner tries to
|
||||
program them ought to offer some help.
|
||||
|
||||
That's where generic dialogs come in. You can create a new type of generic
|
||||
dialog with any NPC. That NPC can from then on only be used for this one
|
||||
purpose and ought not to be found in the "normal" world! Multiple such generic
|
||||
dialogs and NPC for their creation can exist.
|
||||
|
||||
Generic dialogs have to start with a dialog with just one option. This option
|
||||
has to be set to "automaticly" (see Autoanswer). The preconditions of this
|
||||
option are very important: They determine if this particular generic dialog
|
||||
fits to this particular NPC or not. If it fits, all dialogs that are part of
|
||||
this NPC that provides the generic dialog will be added to the "normal"
|
||||
dialogs the importing actual NPC offers. If it doesn't fit, these generic
|
||||
dialogs will be ignored here.
|
||||
|
||||
The creator of a generic dialog can't know all situations where NPC may want
|
||||
to use his dialogs and where those NPC will be standing and by whom they
|
||||
are owned. Therefore only a limited amount of types of preconditions are
|
||||
allowed for the preconditions of this first automatic option: state,
|
||||
player_inv and custom.
|
||||
|
||||
The other dialogs that follow after this first automatic dialog may contain
|
||||
a few more types of preconditions: player_offered_item, function and other
|
||||
are allowed here as well, while block, trade, npc_inv and block_inv make no
|
||||
sense and are not available.
|
||||
|
||||
All types of actions are allowed.
|
||||
|
||||
Regarding effects/results, the types block, put_into_block_inv,
|
||||
take_from_block_inv and craft are not supported.
|
||||
|
||||
The "automaticly" selected only option from the start dialog leads
|
||||
via the usual "dialog" effect to the actual start dialog for the
|
||||
imported dialogs from that NPC. The options found there will be
|
||||
added into the target NPC and the dialog text will be appended to
|
||||
its dialog text.
|
||||
|
||||
|
@ -1,8 +1,388 @@
|
||||
-- this table holds all generic dialogs with indices
|
||||
-- generic_dialog[n_id][d_id_n_id]
|
||||
yl_speak_up.generic_dialogs = {}
|
||||
-- this table holds the preconditions for all generic dialogs
|
||||
-- so that we know which ones are relevant for a particular NPC
|
||||
yl_speak_up.generic_dialog_conditions = {}
|
||||
-- the start dialog of a generic dialog needs to have just one option,
|
||||
-- and that option has to be an autoanswer; all those options of the
|
||||
-- dialogs that follow this start dialog are stored in dialog
|
||||
-- "d_generic_start_dialog"
|
||||
|
||||
yl_speak_up.add_generic_dialogs = function(dialog, n_id, player)
|
||||
if(not(player)) then
|
||||
-- this is similar to the function
|
||||
-- yl_speak_up.calculate_displayable_options
|
||||
-- in exec_eval_preconditions.lua
|
||||
-- - except that some things like sorting or extensive debug output can be skipped
|
||||
yl_speak_up.calculate_available_generic_dialogs = function(current_n_id, player)
|
||||
-- if this is a generic npc: don't inject anything
|
||||
if(yl_speak_up.generic_dialogs[current_n_id]) then
|
||||
return {}
|
||||
end
|
||||
local pname = player:get_player_name()
|
||||
-- the IDs of all those NPCs whose dialogs can be added
|
||||
local n_id_list = {}
|
||||
-- Let's go through all the options and see if we need to display them to the user
|
||||
-- check all options: option key (n_id), option value/data (list of preconditions)
|
||||
for n_id, prereq in pairs(yl_speak_up.generic_dialog_conditions) do
|
||||
-- if true: this generic dialog fits and will be added
|
||||
local include_this = true
|
||||
-- check all preconditions: precondition key, precondition data/data
|
||||
for p_id, p in pairs(prereq) do
|
||||
-- as soon as we locate one precondition that is false, this option (and thus the
|
||||
-- generic dialog it stands for) cannot be included
|
||||
if(not(include_this)
|
||||
-- only certain types of preconditions are allowed because the other ones would
|
||||
-- be too expensive or make no sense here
|
||||
or not(yl_speak_up.eval_precondition(player, current_n_id, p, nil))) then
|
||||
include_this = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if(include_this) then
|
||||
table.insert(n_id_list, n_id)
|
||||
end
|
||||
end
|
||||
return n_id_list
|
||||
end
|
||||
|
||||
|
||||
-- helper function for yl_speak_up.check_and_add_as_generic_dialog;
|
||||
-- appends append-str to data[field_name] if the name of the
|
||||
-- target dialog stored there needs to be rewritten
|
||||
yl_speak_up.rewrite_dialog_id = function(data, field_name, start_dialog, append_str)
|
||||
if(not(data) or not(data[field_name])) then
|
||||
return nil
|
||||
end
|
||||
-- we need this to point to a *common* start dialog
|
||||
if(data[field_name] == start_dialog
|
||||
or data[field_name]..append_str == start_dialog) then
|
||||
return "d_generic_start_dialog"
|
||||
elseif(data[field_name] ~= "d_got_item") then
|
||||
return data[field_name]..append_str
|
||||
end
|
||||
return data[field_name]
|
||||
end
|
||||
|
||||
|
||||
-- returns "OK" if the dialog can be used as a generic dialog, that is:
|
||||
-- * has at least one dialog
|
||||
yl_speak_up.check_and_add_as_generic_dialog = function(dialog, n_id)
|
||||
-- get the start dialog
|
||||
local d_id = yl_speak_up.get_start_dialog_id(dialog)
|
||||
if(not(d_id)
|
||||
or not(dialog.n_dialogs[d_id])
|
||||
or not(dialog.n_dialogs[d_id].d_options)) then
|
||||
return "No start dialog found."
|
||||
end
|
||||
-- the start dialog shall have exactly one option/answer
|
||||
local one_option = nil
|
||||
local anz_options = 0
|
||||
for o_id, o in pairs(dialog.n_dialogs[d_id].d_options) do
|
||||
anz_options = anz_options + 1
|
||||
one_option = o_id
|
||||
end
|
||||
if(anz_options ~= 1) then
|
||||
return "The start dialog has more than one option/answer."
|
||||
end
|
||||
local option = dialog.n_dialogs[d_id].d_options[one_option]
|
||||
-- and this one option/answer shall be of the type autoanswer
|
||||
if(not(option) or not(option.o_autoanswer) or not(option.o_autoanswer ~= "1")) then
|
||||
return "The option of the start dialog is set to \"by clicking manually\" instead "..
|
||||
"of \"automatcily\"."
|
||||
end
|
||||
-- only some few types are allowed for preconditions
|
||||
-- (these checks have to be cheap and quick, and any npc inventory is not available
|
||||
-- at the time of these checks; let alone any block inventories or the like)
|
||||
local prereq = option.o_prerequisites
|
||||
if(prereq) then
|
||||
for p_id, p in pairs(prereq) do
|
||||
if(p.p_type ~= "state" and p.p_type ~= "player_inv" and p.p_type ~= "custom") then
|
||||
return "Precondition "..tostring(p_id)..
|
||||
" of option "..tostring(one_option)..
|
||||
" of dialog "..tostring(d_id)..
|
||||
" has unsupported type: "..tostring(p.p_type)..
|
||||
". Supported types: state, player_inv and custom."
|
||||
end
|
||||
end
|
||||
end
|
||||
-- the original start dialog of the generic dialog, containing only the
|
||||
-- one automatic option
|
||||
local orig_start_dialog = d_id
|
||||
|
||||
-- not everything is suitable for generic dialogs
|
||||
-- for all dialogs (doesn't hurt here to check the start dialog again):
|
||||
for d_id, d in pairs(dialog.n_dialogs) do
|
||||
-- if there are any options
|
||||
if(d.d_options) then
|
||||
-- for all options:
|
||||
for o_id, o in pairs(d.d_options) do
|
||||
-- if there are any preconditions:
|
||||
if(o.o_prerequisites) then
|
||||
-- for all preconditions:
|
||||
for p_id, p in pairs(o.o_prerequisites) do
|
||||
-- makes no sense: block, trade, npc_inv, npc_inv, block_inv
|
||||
if( p.p_type ~= "state"
|
||||
and p.p_type ~= "player_inv"
|
||||
and p.p_type ~= "player_offered_item"
|
||||
and p.p_type ~= "function"
|
||||
and p.p_type ~= "custom"
|
||||
-- depends on the preconditions of another option
|
||||
and p.p_type ~= "other") then
|
||||
return "Precondition "..tostring(p_id)..
|
||||
" of option "..tostring(o_id)..
|
||||
" of dialog "..tostring(d_id)..
|
||||
" has (for generic dialogs) unsupported "..
|
||||
"type: "..tostring(p.p_type).."."
|
||||
end
|
||||
end
|
||||
end
|
||||
-- actions ought to be fully supported but MUST NOT access the NPCs inventory;
|
||||
-- if there are any effects/results:
|
||||
if(o.o_results) then
|
||||
-- for all actions (there ought to be only one)
|
||||
for r_id, r in pairs(o.o_results) do
|
||||
-- makes no sense:
|
||||
-- block, put_into_block_inv, take_from_block_inv,
|
||||
-- craft
|
||||
if( r.r_type ~= "state"
|
||||
-- only accepting or refusing makes sense here
|
||||
and r.r_type ~= "deal_with_offered_item"
|
||||
and r.r_type ~= "on_failure"
|
||||
and r.r_type ~= "chat_all"
|
||||
and r.r_type ~= "give_item"
|
||||
and r.r_type ~= "take_item"
|
||||
and r.r_type ~= "move"
|
||||
and r.r_type ~= "function"
|
||||
and r.r_type ~= "custom"
|
||||
and r.r_type ~= "dialog"
|
||||
) then
|
||||
return "Effect "..tostring(r_id)..
|
||||
" of option "..tostring(o_id)..
|
||||
" of dialog "..tostring(d_id)..
|
||||
" has (for generic dialogs) unsupported "..
|
||||
"type: "..tostring(r.r_type).."."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- this looks good; we may actually add these dialogs;
|
||||
-- if data for this generic dialog (from this NPC) was stored before: reset it
|
||||
yl_speak_up.generic_dialogs[n_id] = {}
|
||||
|
||||
-- store the prerequirements for this generic dialog
|
||||
yl_speak_up.generic_dialog_conditions[n_id] = option.o_prerequisites
|
||||
|
||||
-- some modifications are necessary because dialog IDs are only uniq regarding
|
||||
-- *one* NPC; for example, the dialog d_1 will be present in almost all NPC
|
||||
-- we need to append a uniq postfix to all IDs
|
||||
-- - except for d_got_item; that needs diffrent treatment
|
||||
-- - and except for the start_dialog
|
||||
local append_str = "_"..tostring(n_id)
|
||||
|
||||
-- this is the actual dialog where all those options of intrest are that
|
||||
-- later on need to be injected into the receiving NPC
|
||||
local start_dialog = nil
|
||||
local effects = option.o_results
|
||||
if(effects) then
|
||||
for r_id, r in pairs(option.o_results) do
|
||||
if(r.r_type == "dialog") then
|
||||
-- the start_dialog will soon be renamed for uniqueness; so store that name
|
||||
start_dialog = r.r_value..append_str
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- if no first dialog has been found or if the option loops back to
|
||||
-- the first one: give up
|
||||
if(not(start_dialog)
|
||||
or start_dialog == orig_start_dialog
|
||||
or start_dialog == "d_got_item"..append_str) then
|
||||
yl_speak_up.generic_dialog_conditions[n_id] = {}
|
||||
return "Option of first dialog loops back to first dialog. Giving up."
|
||||
end
|
||||
|
||||
-- we need to make sure that o_sort and d_sort make some sense;
|
||||
-- anz_generic is multipiled with a factor later on, so we want at least 1 here
|
||||
-- so that the values of the inserted dialogs are sufficiently high compared to
|
||||
-- the "normal" ones of the npc
|
||||
local anz_generic = 1
|
||||
for n_id, d in pairs(yl_speak_up.generic_dialogs) do
|
||||
anz_generic = anz_generic + 1
|
||||
end
|
||||
|
||||
-- for setting d_sort to a useful, non-conflicting value
|
||||
local anz_dialogs = 0
|
||||
-- we rename the dialogs where necessary;
|
||||
-- we also mark each dialog, precondition, action and effect with a
|
||||
-- ?_is_generic = append_str entry so that it can later be easily recognized
|
||||
-- and any interactions with the inventory of the NPC be avoided
|
||||
for d_id, d in pairs(dialog.n_dialogs) do
|
||||
-- if there are any options
|
||||
if(d.d_options) then
|
||||
-- for all options:
|
||||
for o_id, o in pairs(d.d_options) do
|
||||
-- if there are any preconditions:
|
||||
if(o.o_prerequisites) then
|
||||
-- for all preconditions:
|
||||
for p_id, p in pairs(o.o_prerequisites) do
|
||||
-- this comes from a generic dialog
|
||||
p.p_is_generic = append_str
|
||||
if(p.p_type == "other") then
|
||||
-- ID of the other dialog that is checked
|
||||
p.p_value = yl_speak_up.rewrite_dialog_id(p,
|
||||
"p_value", start_dialog, append_str)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- if there are any actions:
|
||||
if(o.actions) then
|
||||
-- for all actions (usually just one):
|
||||
for a_id, a in pairs(o.actions) do
|
||||
-- this comes from a generic dialog
|
||||
a.a_is_generic = append_str
|
||||
-- ID of the target dialog when the action failed
|
||||
a.a_on_failure = yl_speak_up.rewrite_dialog_id(a,
|
||||
"a_on_failure", start_dialog, append_str)
|
||||
end
|
||||
end
|
||||
-- if there are any effects/results:
|
||||
if(o.o_results) then
|
||||
-- for all actions (there ought to be only one)
|
||||
for r_id, r in pairs(o.o_results) do
|
||||
-- this comes from a generic dialog
|
||||
r.r_is_generic = append_str
|
||||
if(r.r_type == "on_failure") then
|
||||
r.r_on_failure = yl_speak_up.rewrite_dialog_id(r,
|
||||
"r_on_failure", start_dialog, append_str)
|
||||
elseif(r.r_type=="dialog") then
|
||||
-- ID of the normal target dialog
|
||||
r.r_value = yl_speak_up.rewrite_dialog_id(r,
|
||||
"r_value", start_dialog, append_str)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- d_sort needs to be set accordingly
|
||||
local d_sort = tonumber(d.d_sort or "0")
|
||||
if(not(d_sort) or d_sort < 0) then
|
||||
d_sort = anz_dialogs
|
||||
end
|
||||
-- make sure there is enough room for internal sorting
|
||||
d.d_sort = d_sort + (anz_generic * 100000)
|
||||
anz_dialogs = anz_dialogs + 1
|
||||
|
||||
-- remember where this generic dialog comes from and that it is a generic one
|
||||
d.is_generic = append_str
|
||||
-- store this new dialog with its new ID
|
||||
-- Note: This may also create d_got_item_n_<id>
|
||||
d.d_id = yl_speak_up.rewrite_dialog_id(d, "d_id", start_dialog, append_str)
|
||||
-- the dialog ID will only be equal to start_dialog after adding append_str,
|
||||
-- so...check again here
|
||||
if(d.d_id == start_dialog) then
|
||||
d.d_id = "d_generic_start_dialog"
|
||||
end
|
||||
yl_speak_up.generic_dialogs[n_id][d.d_id] = d
|
||||
end
|
||||
|
||||
start_dialog = "d_generic_start_dialog"
|
||||
-- make the necessary adjustments for the options of the start_dialog
|
||||
local options = yl_speak_up.generic_dialogs[n_id][start_dialog].d_options
|
||||
if(not(options)) then
|
||||
return "There are no options/answers that might be injected/made generic."
|
||||
end
|
||||
local new_options = {}
|
||||
local anz_options = 0
|
||||
for o_id, o in pairs(options) do
|
||||
-- o_sort needs to be set accordingly
|
||||
local o_sort = tonumber(o.o_sort or "0")
|
||||
if(not(o_sort) or o_sort < 0) then
|
||||
o_sort = anz_options
|
||||
end
|
||||
-- make sure there is enough room for internal sorting
|
||||
o.o_sort = o_sort + (anz_generic * 100000)
|
||||
anz_options = anz_options + 1
|
||||
|
||||
-- adjust o_id
|
||||
o.o_id = o_id..append_str
|
||||
-- the options of the first dialog need to be renamed to r.r_id_n_id to avoid duplicates
|
||||
new_options[o_id..append_str] = o
|
||||
-- TODO: there may be preconditions refering this option which also might need changing
|
||||
end
|
||||
yl_speak_up.generic_dialogs[n_id][start_dialog].d_options = new_options
|
||||
|
||||
-- all fine - no error occoured, no error message to display;
|
||||
-- the NPC's dialog has been added
|
||||
return "OK"
|
||||
end
|
||||
|
||||
|
||||
yl_speak_up.add_generic_dialogs = function(dialog, current_n_id, player)
|
||||
if(not(player) or not(current_n_id)) then
|
||||
return dialog
|
||||
end
|
||||
-- TODO: extend the dialog with generic dialog texts
|
||||
|
||||
-- which is the start dialog of the current NPC? where do we want to insert the options?
|
||||
local start_dialog_current = yl_speak_up.get_start_dialog_id(dialog)
|
||||
if(not(start_dialog_current)) then
|
||||
start_dialog_current = "d_1"
|
||||
end
|
||||
-- unconfigured NPC are in special need of generic dialogs
|
||||
if(not(dialog)) then
|
||||
dialog = {}
|
||||
end
|
||||
if(not(dialog.n_dialogs)) then
|
||||
dialog.n_dialogs = {}
|
||||
end
|
||||
if(not(dialog.n_dialogs[start_dialog_current])) then
|
||||
dialog.n_dialogs[start_dialog_current] = {}
|
||||
end
|
||||
if(not(dialog.n_dialogs[start_dialog_current].d_options)) then
|
||||
dialog.n_dialogs[start_dialog_current].d_options = {}
|
||||
end
|
||||
|
||||
-- TODO: supply npc.self directly as parameter?
|
||||
|
||||
-- which generic dialogs shall be included?
|
||||
local n_id_list = yl_speak_up.calculate_available_generic_dialogs(current_n_id, player)
|
||||
-- TODO: just debug info
|
||||
-- minetest.chat_send_player("singleplayer", "Ok, generic option will be included: "..minetest.serialize(n_id_list))
|
||||
-- actually integrate those generic parts
|
||||
for i, n_id in ipairs(n_id_list) do
|
||||
-- add the dialogs as such first
|
||||
for d_id, d in pairs(yl_speak_up.generic_dialogs[n_id]) do
|
||||
-- no need to deep copy here - this is not added in edit mode
|
||||
dialog.n_dialogs[d_id] = d
|
||||
-- minetest.chat_send_player("singleplayer","copying d_id: "..tostring(d_id))
|
||||
end
|
||||
-- add the options so that these new dialogs can be accessed
|
||||
local d = yl_speak_up.generic_dialogs[n_id]["d_generic_start_dialog"]
|
||||
if(d and d.d_options) then
|
||||
for o_id, o in pairs(d.d_options) do
|
||||
-- actually insert the new option
|
||||
dialog.n_dialogs[start_dialog_current].d_options[o.o_id] = o
|
||||
-- minetest.chat_send_player("singleplayer","injecting option "..tostring(o_id).." into "..tostring(start_dialog_current)..": "..minetest.serialize(o))
|
||||
end
|
||||
end
|
||||
end
|
||||
-- TODO: deal with d_got_item
|
||||
return dialog
|
||||
end
|
||||
|
||||
yl_speak_up.load_generic_dialogs = function()
|
||||
-- TODO: keep list of added npc dialogs
|
||||
local n_id = "n_31"
|
||||
local dialog = yl_speak_up.load_dialog(n_id, false)
|
||||
-- TODO: do this check when the generic dialog is added
|
||||
local res = yl_speak_up.check_and_add_as_generic_dialog(dialog, n_id)
|
||||
if(res == "OK") then
|
||||
minetest.log("action", "[MOD] yl_speak_up: "..
|
||||
"Generic dialog from NPC "..tostring(n_id).." loaded successfully.")
|
||||
else
|
||||
minetest.log("action", "[MOD] yl_speak_up: "..
|
||||
"Generic dialog from NPC "..tostring(n_id).." failed to load: "..tostring(res)..".")
|
||||
end
|
||||
end
|
||||
|
@ -164,7 +164,8 @@ yl_speak_up.input_talk = function(player, formname, fields)
|
||||
end
|
||||
|
||||
for k, v in pairs(fields) do
|
||||
local s = string.split(k, "_")
|
||||
-- only split into 2 parts at max
|
||||
local s = string.split(k, "_", false, 2)
|
||||
|
||||
if
|
||||
s[1] == "button" and s[2] ~= nil and s[2] ~= "" and s[2] ~= "exit" and s[2] ~= "back" and s[3] ~= nil and
|
||||
@ -271,7 +272,7 @@ end
|
||||
|
||||
-- helper function for
|
||||
-- yl_speak_up.get_fs_talkdialog and
|
||||
-- yl_speak_up.check_generic_dialog_get_errors
|
||||
-- yl_speak_up.check_and_add_as_generic_dialog
|
||||
-- find the dialog with d_sort == 0 or lowest number
|
||||
yl_speak_up.get_start_dialog_id = function(dialog)
|
||||
if(not(dialog) or not(dialog.n_dialogs)) then
|
||||
|
4
init.lua
4
init.lua
@ -107,4 +107,8 @@ minetest.mkdir(yl_speak_up.worldpath..yl_speak_up.inventory_path)
|
||||
|
||||
yl_speak_up.mob_table = yl_speak_up.init_mob_table() or {}
|
||||
|
||||
-- initialize and load all registered generic dialogs
|
||||
yl_speak_up.load_generic_dialogs()
|
||||
|
||||
minetest.log("action","[MOD] yl_speak_up loaded")
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user