diff --git a/fs_manage_quest_steps.lua b/fs_manage_quest_steps.lua new file mode 100644 index 0000000..0996fb3 --- /dev/null +++ b/fs_manage_quest_steps.lua @@ -0,0 +1,120 @@ + +-- Imposing an order on the quest steps is...tricky as best as what will +-- be more important to the players will be the order in which the +-- quest steps have to be solved/done - and not an alphabetical order. +-- But we need an order here for a dropdown menu to select each +-- quest step even if it hasn't been assigned any place in the chain +-- of quest steps yet. So - alphabetical order. +yl_speak_up.get_sorted_quest_step_list = function(pname, q_id) + local quest_step_list = {} + if(not(pname) or not(yl_speak_up.speak_to[pname])) then + return {} + end + local q_id = yl_speak_up.speak_to[pname].q_id + + if(q_id and yl_speak_up.quests[q_id] and yl_speak_up.quests[q_id].step_data) then + for step_id, v in pairs(yl_speak_up.quests[q_id].step_data) do + table.insert(quest_step_list, step_id) + end + end + table.sort(quest_step_list) + return quest_step_list +end + + + +-- helper functions for yl_speak_up.input_fs_manage_quest_steps(..) +-- returns the index of the new quest step +yl_speak_up.input_fs_manage_quest_steps_add_new_entry = function(pname, entry_name) + local q_id = yl_speak_up.speak_to[pname].q_id + local res = yl_speak_up.quest_step_add_quest_step(pname, q_id, entry_name) + -- might make sense to show the error message somewhere + if(res ~= "OK") then + return res + end + -- the new entry will be somewhere in it + local quest_step_list = yl_speak_up.get_sorted_quest_step_list(pname) + return table.indexof(quest_step_list, entry_name) +end + + +-- helper functions for yl_speak_up.input_fs_manage_quest_steps(..) +-- returns a text describing if deleting the quest worked +yl_speak_up.input_fs_manage_quest_steps_del_old_entry = function(pname, entry_name) + local q_id = yl_speak_up.speak_to[pname].q_id + return yl_speak_up.quest_step_del_quest_step(pname, q_id, entry_name) +end + + +-- helper functions for yl_speak_up.input_fs_manage_quest_steps(..) +-- implements all the functions that are specific to managing quest steps and not part of +-- general item management +yl_speak_up.input_fs_manage_quest_steps_check_fields = function(player, formname, fields, quest_step_name, list_of_entries) + local pname = player:get_player_name() + if(not(quest_step_name)) then + quest_step_name = "" + end +--[[ TODO: implement some back button functionality? + if(fields and fields.show_variable) then + yl_speak_up.show_fs(player, "manage_variables", {var_name = quest_name}) + return + end +--]] + -- this function didn't have anything to do + return "NOTHING FOUND" +end + + +-- makes use of yl_speak_up.input_fs_manage_general and is thus pretty short +yl_speak_up.input_fs_manage_quest_steps = function(player, formname, fields) + minetest.chat_send_player("singleplayer", "INPUT: "..minetest.serialize(fields)) + local pname = player:get_player_name() + + local quest_step_list = yl_speak_up.get_sorted_quest_step_list(pname) + local res = yl_speak_up.input_fs_manage_general(player, formname, fields, + -- what_is_the_list_about, min_length, max_length, function_add_new_entry, + "quest step", 2, 70, + yl_speak_up.input_fs_manage_quest_steps_add_new_entry, + quest_step_list, + yl_speak_up.input_fs_manage_quest_steps_del_old_entry, + yl_speak_up.input_fs_manage_quest_steps_check_fields) + return true +end + + +yl_speak_up.get_fs_manage_quest_steps = function(player, param) + local pname = player:get_player_name() + local formspec = {} + + local q_id = yl_speak_up.speak_to[pname].q_id + local quest = yl_speak_up.quests[q_id] + local step_data = quest.step_data + + local quest_step_list = yl_speak_up.get_sorted_quest_step_list(pname) + if(param and param ~= "") then + local index = table.indexof(quest_step_list, param) + yl_speak_up.speak_to[pname].tmp_index_general = index + 1 + end + table.insert(formspec, "size[18,12]".. + "label[0.2,1.2;A quest step is a single thing a player may do in a quest - ".. + "like talking to an NPC.\n".. + "Usually not all quest steps can be done/solved at all times.]") + local selected = yl_speak_up.get_fs_manage_general(player, param, + formspec, quest_step_list, + "Create quest step", + "Create a new quest step for this quest.", + "quest step", + "Enter the name of the new quest step you want to create.\n".. + "This is an internal text shown only to yourself.\n".. + "Players cannot see the names of quest steps.", + "If you click here, the selected quest step will be deleted.\n".. + "This will only be possible if it's not used anywhere.") +--[[ + if(selected and selected ~= "") then + local k = selected + -- index 1 is "Add variable:" + table.insert(formspec, "button[12,2.15;4.5,0.6;show_variable;Show and edit this variable]") + end +--]] + return table.concat(formspec, "") +end diff --git a/init.lua b/init.lua index bad0479..5fea0b9 100644 --- a/init.lua +++ b/init.lua @@ -218,6 +218,8 @@ yl_speak_up.reload = function(modpath, log_entry) dofile(modpath .. "quest_api.lua") -- GUI for adding/editing quests dofile(modpath .. "fs_manage_quests.lua") + -- GUI for adding/editing quest steps for the quests + dofile(modpath .. "fs_manage_quest_steps.lua") -- setting skin, wielded item etc. dofile(modpath .. "fs_fashion.lua") -- properties for NPC without specific dialogs that want to make use of diff --git a/quest_api.lua b/quest_api.lua index f6f2653..48ed7c7 100644 --- a/quest_api.lua +++ b/quest_api.lua @@ -25,6 +25,20 @@ yl_speak_up.save_quest_variables = function(force_save) end +yl_speak_up.handle_json_nil_values = function(data) + if(data and type(data) == "table") then + for k,v in pairs(data) do + if( type(v) == "string" and v == "$NIL_VALUE$") then + data[ k ] = {} + elseif(type(v) == "table") then + data[ k ] = yl_speak_up.handle_json_nil_values(v) + end + end + end + return data +end + + -- load the data from disc yl_speak_up.load_quest_variables = function() -- load the data from the file @@ -41,12 +55,7 @@ yl_speak_up.load_quest_variables = function() if(type(data) ~= "table") then return end - for k,v in pairs(data) do - if(v == "$NIL_VALUE$") then - data[ k ] = {} - end - end - yl_speak_up.player_vars = data + yl_speak_up.player_vars = yl_speak_up.handle_json_nil_values(data) if(not(yl_speak_up.player_vars.meta)) then yl_speak_up.player_vars["meta"] = {} end @@ -195,7 +204,8 @@ yl_speak_up.get_quest_variables = function(pname, has_write_access) end -- if the player has the right privs: allow to access all other variables as well if( minetest.check_player_privs(pname, {npc_master=true}) - or minetest.check_player_privs(pname, {npc_talk_master=true})) then + or minetest.check_player_privs(pname, {npc_talk_master=true}) + or minetest.check_player_privs(pname, {npc_talk_admin=true})) then for k, v in pairs(yl_speak_up.player_vars) do local parts = string.split(k, " ") -- variables owned by *other* players @@ -546,6 +556,7 @@ yl_speak_up.save_quest = function(q_id) end + -- read quest q_id from disc yl_speak_up.load_quest = function(q_id) -- load the data from the file @@ -563,12 +574,7 @@ yl_speak_up.load_quest = function(q_id) if(type(data) ~= "table") then return end - for k,v in pairs(data) do - if(v == "$NIL_VALUE$") then - data[ k ] = {} - end - end - yl_speak_up.quests[q_id] = data + yl_speak_up.quests[q_id] = yl_speak_up.handle_json_nil_values(data) return end @@ -627,6 +633,7 @@ yl_speak_up.add_quest = function(owner_name, variable_name, quest_name, descr_lo -- the list of quest steps is stored in the variables' metadata for quicker access -- (this way we won't have to load the quest file if we want to check a precondition -- or update the variable value to the next quest step) + -- TODO: store those in the quest file yl_speak_up.set_variable_metadata(var_name, owner_name, "quest_data", "steps", {"start","finish"}) -- create the quest data structure @@ -658,8 +665,10 @@ yl_speak_up.add_quest = function(owner_name, variable_name, quest_name, descr_lo -- -> determined from quests.npcs and quests.locations quest.npcs = {} -- list of NPC that contribute to this quest -- -> derived from quest.var_name + -- --> or derived from quest.step_data.where quest.locations = {} -- list of locations that contribute to this quest -- -> derived from quest.var_name + -- --> or derived from quest.step_data.where quest.items = {} -- data of quest items created and accepted -- -> derived from the quest steps quest.rewards = {} -- list of rewards (item stacks) for this ques @@ -746,6 +755,186 @@ yl_speak_up.get_sorted_quest_list = function(pname) end + +-- quests have a name and a variable which stores their data +-- this returns the q_id (index in the quest table) based on the variable name +yl_speak_up.get_quest_id_by_var_name = function(var_name, owner_name) + local var_name = yl_speak_up.add_pname_to_var(var_name, owner_name) + -- find out which quest we're talking about + for q_id, quest in pairs(yl_speak_up.quests) do + if(quest.var_name == var_name) then + return q_id + end + end + return nil +end + + +-- quests can also be identified by their name +-- this returns the q_id (index in the quest table) based on the quest name +yl_speak_up.get_quest_id_by_quest_name = function(quest_name) + -- find out which quest we're talking about + for q_id, quest in pairs(yl_speak_up.quests) do + if(quest.name == quest_name) then + return q_id + end + end + return nil +end + + +-- finds out if player pname is allowed to view (read_only is true) +-- or edit (read_only is false) the quest q_id +yl_speak_up.quest_allow_access = function(q_id, pname, read_only) + -- no quest with that variable as base found + if(not(q_id) or not(yl_speak_up.quests[q_id])) then + return "Quest not found (id: "..tostring(q_id)..")." + end + local quest = yl_speak_up.quests[q_id] + -- check if the player has access rights to that quest + if(quest.owner ~= pname + and not(minetest.check_player_privs(pname, {npc_master=true})) + and not(minetest.check_player_privs(pname, {npc_talk_master=true})) + and not(minetest.check_player_privs(pname, {npc_talk_admin=true}))) then + -- the player may have suitable privs (check access to the variable) + local access_what = "write_access" + if(read_only) then + access_what = "read_access" + end + local allowed = yl_speak_up.get_access_list_for_var(quest.var_name, "", access_what) + if(table.indexof(allowed, pname) == -1) then + return "Sorry. You have no write access to quest \""..tostring(quest.name).."\" ".. + "["..tostring(k).."]." + end + end + -- quests that are already open to the public cannot be changed anymore + -- as that would cause chaos; only in "created" and "testing" stage it's + -- possible to change quest steps + if(not(read_only) and (quest.state == "open" or quest.state == "official")) then + return "The quest is in state \""..tostring(quest.state).."\". Quests in such ".. + "a state cannot be changed/extended as that would confuse players. ".. + "Reset quest state first if changes are unavoidable." + end + return "OK" +end + + +-- add a quest step to a quest +yl_speak_up.quest_step_add_quest_step = function(pname, q_id, quest_step_name) + local error_msg = yl_speak_up.quest_allow_access(q_id, pname, false) + if(error_msg ~= "OK") then + return error_msg + end + if(not(quest_step_name) or quest_step_name == "" + or string.len(quest_step_name) < 2 or string.len(quest_step_name) > 70) then + return "No name for this quest step given or too long (>70) or too short (<2 characters)." + end + if(not(yl_speak_up.quests[q_id].step_data)) then + yl_speak_up.quests[q_id].step_data = {} + end + -- create an entry for the quest step if needed + if(not(yl_speak_up.quests[q_id].step_data[quest_step_name])) then + yl_speak_up.quests[q_id].step_data[quest_step_name] = { + -- where (NPCs, locations) can this quest step be set? + where = {}} + yl_speak_up.save_quest(q_id) + end + -- return OK even if the quest step existed already + return "OK" +end + + +-- delete a quest step - but only if it's not used +yl_speak_up.quest_step_del_quest_step = function(pname, q_id, quest_step_name) + local error_msg = yl_speak_up.quest_allow_access(q_id, pname, false) + if(error_msg ~= "OK") then + return error_msg + end + if(not(quest_step_name) + or not(yl_speak_up.quests[q_id].step_data) + or not(yl_speak_up.quests[q_id].step_data[quest_step_name])) then + return "OK" + end + -- the quest step exists; can we delete it? + local quest_step = yl_speak_up.quests[q_id].step_data[quest_step_name] + local anz_where = 0 + for k, _ in pairs(quest_step.where or {}) do + anz_where = anz_where + 1 + end + if(anz_where > 0) then + return "This quest step is used/set by "..tostring(anz_where).. + " NPCs and/or locations.\nRemove them from this quest step first!" + end + -- is this the previous quest step of another step? + for sn, step_data in pairs(yl_speak_up.quests[q_id].step_data) do + if(step_data and step_data.previous_step and step_data.previous_step == quest_step_name) then + return "Quest step \""..tostring(sn).."\" names this quest step that you want ".. + "to delete as its previous step. Please remove that requirement first ".. + "for quest step \""..tostring(sn).."\" before deleting this step here." + end + if(step_data and step_data.further_required_steps + and table.indexof(step_data.further_required_steps, quest_step_name) ~= -1) then + return "Quest step \""..tostring(sn).."\" gives this quest step that you want ".. + "to delete as one of the further steps required to reach it. Please ".. + "remove that requirement first ".. + "for quest step \""..tostring(sn).."\" before deleting this step here." + end + -- offered_until_quest_step_reached would be no problem/hinderance and doesn't need checking + end + yl_speak_up.quests[q_id].step_data[quest_step_name] = nil + yl_speak_up.save_quest(q_id) + return "OK" +end + + +-- add an NPC or location to a quest step (quest_step.where = list of such locations) +-- Note: This is for NPC and locations that SET this very quest step. They ought to be listed here. +-- new_location has to be a table, and new_loc_id an ID to avoid duplicates +-- for NPC, new_loc_id ought to look like this: "NPC " +yl_speak_up.quest_step_add_where = function(pname, q_id, quest_step_name, new_location, new_loc_id) local error_msg = yl_speak_up.quest_allow_access(q_id, pname, false) + if(error_msg ~= "OK") then + return error_msg + end + local quest_step = yl_speak_up.quests[q_id].step_data[quest_step_name] + if(not(quest_step)) then + return "Quest step \""..tostring(quest_step_name).."\" does not exist." + end + if(not(step_data.where)) then + step_data.where = {} + end + if(step_data.where[new_loc_id]) then + return "OK" + end + yl_speak_up.quests[q_id].step_data[quest_step_name].where[new_loc_id] = new_location + -- return OK even if the quest step existed already + return "OK" +end + + +-- delete a quest step location with the id location_id +yl_speak_up.quest_step_del_where = function(pname, q_id, quest_step_name, location_id) + local error_msg = yl_speak_up.quest_allow_access(q_id, pname, false) + if(error_msg ~= "OK") then + return error_msg + end + local quest_step = yl_speak_up.quests[q_id].step_data[quest_step_name] + if(not(quest_step)) then + return "Quest step \""..tostring(quest_step_name).."\" does not exist." + end + if(not(step_data.where)) then + step_data.where = {} + end + -- delete the quest step location + yl_speak_up.quests[q_id].step_data[quest_step_name].where[location_id] = nil + return "OK" +end + + +-- TODO: quest_step: previous_step +-- TODO: quest_step: further_required_steps +-- TODO: quest_step: offered_until_quest_step_reached + + -- called for example by yl_speak_up.eval_all_preconditions to see if the player -- can reach quest_step in quest quest_id yl_speak_up.quest_step_possible = function(player, quest_step, quest_id, n_id, d_id, o_id) @@ -766,3 +955,21 @@ yl_speak_up.quest_step_reached = function(player, quest_step, quest_id, n_id, d_ -- TODO: actually store the quest progress -- minetest.chat_send_player("singleplayer", "SETTING quest step "..tostring(quest_step).." for quest "..tostring(quest_id)) end + + + +-- load all known quests +yl_speak_up.load_all_quests = function() + for var_name, var_data in pairs(yl_speak_up.player_vars) do + local var_type = yl_speak_up.get_variable_metadata(var_name, "var_type") + if(var_type == "quest") then + local data = yl_speak_up.get_variable_metadata(var_name, "quest_data", true) + if(data and data["quest_nr"]) then + yl_speak_up.load_quest("q_"..tostring(data["quest_nr"])) + end + end + end +end + +-- do so on startup and reload +yl_speak_up.load_all_quests() diff --git a/show_fs.lua b/show_fs.lua index 2d5f531..2449662 100644 --- a/show_fs.lua +++ b/show_fs.lua @@ -107,6 +107,10 @@ yl_speak_up.input_handler = function(player, formname, fields) elseif formname == "yl_speak_up:manage_quests" then yl_speak_up.input_fs_manage_quests(player, formname, fields) return true + -- handled in fs_manage_quest_steps.lua + elseif formname == "yl_speak_up:manage_quest_steps" then + yl_speak_up.input_fs_manage_quest_steps(player, formname, fields) + return true -- handled in fs_alternate_text.lua elseif formname == "yl_speak_up:show_what_points_to_this_dialog" then yl_speak_up.input_fs_show_what_points_to_this_dialog(player, formname, fields) @@ -410,6 +414,10 @@ yl_speak_up.show_fs = function(player, fs_name, param) yl_speak_up.show_fs_ver(pname, "yl_speak_up:manage_quests", yl_speak_up.get_fs_manage_quests(player, param)) + elseif(fs_name == "manage_quest_steps") then + yl_speak_up.show_fs_ver(pname, "yl_speak_up:manage_quest_steps", + yl_speak_up.get_fs_manage_quest_steps(player, param)) + elseif(fs_name == "show_what_points_to_this_dialog") then yl_speak_up.show_fs_ver(pname, "yl_speak_up:show_what_points_to_this_dialog", yl_speak_up.show_what_points_to_this_dialog(player, param))