From a5078840457edfae50e3e94ca98d97b5a14fdd82 Mon Sep 17 00:00:00 2001 From: LoneWolfHT Date: Fri, 20 Nov 2020 09:13:33 -0800 Subject: [PATCH] NPC-given quests! --- .luacheckrc | 4 +- api.md | 34 ++++++ mods/vk_npcs/init.lua | 173 ++++++++++++++++++++++++++--- mods/vk_npcs/mod.conf | 2 +- mods/vk_players/vk_player/init.lua | 12 ++ mods/vk_quests/init.lua | 103 ++++++++++++++++- mods/vk_quests/mod.conf | 1 + mods/vk_quests/quests.lua | 14 +++ mods/vk_quests/sfinv_page.lua | 41 +++++++ 9 files changed, 367 insertions(+), 17 deletions(-) create mode 100644 api.md create mode 100644 mods/vk_quests/quests.lua create mode 100644 mods/vk_quests/sfinv_page.lua diff --git a/.luacheckrc b/.luacheckrc index 5b1e924..ae8e883 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,13 +1,13 @@ globals = { "vkore", "player_stats", "hb", "players", "gold", "party", "nodes", "mapgen", "spawners", "pathfinder", - "mobkit", "mobkit_custom", "swords" + "mobkit", "mobkit_custom", "swords", "vk_quests", "vk_quest", } read_globals = { "minetest", "sfinv", string = {fields = {"split"}}, - table = {fields = {"copy", "getn"}}, + table = {fields = {"copy", "getn", "indexof",}}, math = {fields = {"round"}}, -- Builtin diff --git a/api.md b/api.md new file mode 100644 index 0000000..2307817 --- /dev/null +++ b/api.md @@ -0,0 +1,34 @@ +# Voxel Knights API + +May be incomplete. Any help with documentation is appreciated + +## vk_quests + +`vk_quests.register_quest(type, name, def)` + +* type - Type of quest. + * kill - Quest to kill a certain amount of enemies +* name - Used to identify the quest +* def - quest def. See [Quest Types] for more info + +### [Quest Types] + +#### kill + +* type - "kill" +* name - Name of enemy to kill (e.g spider:spider) +* def: + * description - Description of quest (e.g Kill 10 spiders) + * comments (string/list of strings) - Comments for the NPC to make about the quest + * amount - Amount of enemies that you need to kill + * rewards - Table of rewards to give on completion. + * Works with all [Player Meta] stored as an int + * Example: {xp = 3, gold = 10} to give 3 xp and 10 gold + +## [Player Meta] + +List of all player meta used for things like gold/xp + +* `gold` (int) - Stores the player's gold +* `xp` (int) - Stores the player's xp +* `availiable_statpoints` (int) - Stores the player's avaliable statpoints diff --git a/mods/vk_npcs/init.lua b/mods/vk_npcs/init.lua index b831d26..73c5783 100644 --- a/mods/vk_npcs/init.lua +++ b/mods/vk_npcs/init.lua @@ -12,13 +12,30 @@ local default_hit_replies = { } local function prettify(npcname) - local output = npcname:gsub("_", " ") + local output = npcname:gsub("_", " ") - return output:gsub("^(.)", string.upper) + return output:gsub("^(.)", string.upper) end +--[[ + context is used to save formspec info when a player is interacting with npcs. + It is cleared on exit/server restart + Default values: + { + tab = 1, -- Current tab the player is on + npcdef = def, -- NPC definition. Used to grab convos and NPC names + quest = 1, -- Selected quest in quest list + quests = {}, -- List of quests availiable from npc + } +]] +local context = {} +minetest.register_on_leaveplayer(function(player) context[player:get_player_name()] = nil end) + local function register_npc(name, def) + def.npcname = name + minetest.register_node(modname..":"..name, { + npcname = name, description = "NPC "..prettify(name), drawtype = "mesh", mesh = "player.obj", @@ -55,24 +72,142 @@ local function register_npc(name, def) on_rightclick = function(pos, node, clicker, itemstack, pointed_thing) if not clicker or not clicker:is_player() then return end - local formspec = ([[ - size[8,6] - real_coordinates[true] - label[0.2,0.3;%s] - ]]):format( - prettify(name) - ) + local pname = clicker:get_player_name() - if not def.convos then - minetest.chat_send_player(clicker:get_player_name(), ("<%s> "):format(prettify(name)).."I have nothing to say") - return + if not context[pname] or context[pname].npcdef.npcname ~= name then + context[pname] = { + tab = 1, + npcdef = def, + quest = 1 + } end - minetest.show_formspec(clicker:get_player_name(), "npcform", formspec) + vk_quests.show_npc_form(pname, context[pname]) end }) end +function vk_quests.show_npc_form(pname, pcontext) + local temp + local formspec = ([[ + size[8,6] + real_coordinates[true] + label[0.2,0.3;%s] + ]]):format( + prettify(pcontext.npcdef.npcname) + ) + + if not pcontext.npcdef.convos then + minetest.chat_send_player(pname, ("<%s> "):format(prettify(pcontext.npcdef.npcname)).."I have nothing to say") + return + end + + local convos = "" + local convo_content + temp = 0 -- tab number + for cname, content in pairs(pcontext.npcdef.convos) do + temp = temp + 1 + + -- Save the content of the currently selected convo for later use + if temp == pcontext.tab then + convo_content = content + end + + convos = convos .. cname .. "," + end + + convos = convos:sub(1, -2) -- Remove trailing comma + + formspec = formspec .. + "tabheader[0,2;1;convos;"..convos..";".. pcontext.tab ..";false;true]" + + local quests = {} + local quest_convo = false + + for _, quest in pairs(convo_content) do + if vk_quest[quest] then + quest_convo = true + table.insert(quests, vk_quest[quest]) + end + end + + if quest_convo then + local comments = quests[pcontext.quest].comments + + -- Remove quests in progress + for k, quest in ipairs(quests) do + if vk_quests.get_unfinished_quest(pname, quest.qid) then + table.remove(quests, k) + end + end + + if #quests > 0 then + formspec = formspec .. + "hypertext[0,2.2;8,4;comment;\""..comments[math.random(1, #comments)].."\"]" .. + "textlist[0,3.5;8,2.5;quests;" + + pcontext.quests = {} + for _, quest in ipairs(quests) do + table.insert(pcontext.quests, quest.qid) + formspec = ("%s%s - %s,"):format( + formspec, + quest.description, + minetest.formspec_escape(quest.rewards_description) + ) + end + + formspec = formspec:sub(1, -2) -- Remove trailing comma + formspec = formspec .. ";"..pcontext.quest..";false]" + else + formspec = formspec .. "label[0,2.4;\"I don't have any quests for you\"]" + end + else + formspec = formspec .. "label[0,2.4;\"I don't have anything to say\"]" + end + + minetest.show_formspec(pname, "npcform", formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= "npcform" or not fields then return end + + local pname = player:get_player_name() + + if not context[pname] then + minetest.log("error", "Player submitted fields without context") + minetest.close_formspec(pname, "npcform") + + return true + end + + local update_form = false + + -- Update selected tab if changed + if fields.convos then + context[pname].tab = tonumber(fields.convos) + + update_form = true + end + + if fields.quests then + local event = minetest.explode_textlist_event(fields.quests) + + if (event.type == "CHG" or event.type == "DCL") and event.index ~= context[pname].quest then + context[pname].quest = event.index + update_form = true + elseif event.type == "DCL" then + vk_quests.start_quest(pname, context[pname].quests[event.index]) + update_form = true + end + end + + if update_form then + vk_quests.show_npc_form(pname, context[pname]) + end + + return true +end) + register_npc("blacksmith", { texture = "vk_npcs_blacksmith.png", hit_replies = default_hit_replies, @@ -86,6 +221,18 @@ register_npc("stable_man", { register_npc("guard", { texture = "vk_npcs_guard.png", hit_replies = default_hit_replies, + convos = { + Quests = { + "kill_spider:spider", + }, + Rumors = { + ["I hear the tavern keeper doesn't actually sell any drinks, she just stands there, staring"] = { + ["Do they pay you money if you win?"] = "You'll have to wait in line, ".. + "they've been having a staring contest with their customers for 3 days now", + ["uhhhhh, bye"] = "Farewell", + } + } + } }) register_npc("tavern_keeper", { diff --git a/mods/vk_npcs/mod.conf b/mods/vk_npcs/mod.conf index 5aeaba8..d35203e 100644 --- a/mods/vk_npcs/mod.conf +++ b/mods/vk_npcs/mod.conf @@ -1,2 +1,2 @@ name = vk_npcs -depends = vk_mapgen +depends = vk_mapgen, vk_quests diff --git a/mods/vk_players/vk_player/init.lua b/mods/vk_players/vk_player/init.lua index ee45386..2ce0f8b 100644 --- a/mods/vk_players/vk_player/init.lua +++ b/mods/vk_players/vk_player/init.lua @@ -23,3 +23,15 @@ local dirs = { -- Lua files to include for _, filename in ipairs(dirs) do dofile(minetest.get_modpath(minetest.get_current_modname()).."/"..filename) end + +function players.set_int(player, key, newval) + local meta = player:get_meta() + + if key == "gold" then + players.set_gold(player, newval) + elseif key == "xp" then + players.add_xp(player, newval - player:get_meta():get_int("xp")) + else + meta:set_int(key, newval) + end +end diff --git a/mods/vk_quests/init.lua b/mods/vk_quests/init.lua index 0e97861..9eb2dae 100644 --- a/mods/vk_quests/init.lua +++ b/mods/vk_quests/init.lua @@ -1,7 +1,108 @@ vk_quests = {} +vk_quest = {} local modname = minetest.get_current_modname() -function vk_quests.on_enemy_death(enemy, slayer) +local nextid = 1 +function vk_quests.register_quest(type, name, def) + def.qid = nextid + nextid = nextid + 1 + if type == "kill" then + if not def.on_complete then + def.on_complete = function(player) + local meta = player:get_meta() + + for key, addition in pairs(def.rewards) do + players.set_int(player, key, meta:get_int(key) + addition) + end + + minetest.chat_send_player(player:get_player_name(), "You completed quest \""..def.description.."\"!") + vk_quests.finish_quest(player, def.qid) + end + end + end + + vk_quest[type.."_"..name] = def end + +function vk_quests.get_quest(id) + for name, quest in pairs(vk_quest) do + if quest.qid == id then + return quest, name + end + end +end + +function vk_quests.start_quest(player, id) + local unfinished_quests = vk_quests.get_unfinished_quests(player) + + unfinished_quests[id] = {} + + vk_quests.set_unfinished_quests(player, unfinished_quests) +end + +function vk_quests.finish_quest(player, id) + local unfinished_quests = vk_quests.get_unfinished_quests(player) + + unfinished_quests[id] = nil + + vk_quests.set_unfinished_quests(player, unfinished_quests) +end + +function vk_quests.get_unfinished_quest(player, id) + player = vkore.playerObj(player) + + local meta = player:get_meta() + local unfinished_quests = minetest.deserialize(meta:get_string("unfinished_quests")) or {} + + return unfinished_quests and unfinished_quests[id] or nil +end + +function vk_quests.get_unfinished_quests(player) + player = vkore.playerObj(player) + + return minetest.deserialize(player:get_meta():get_string("unfinished_quests")) or {} +end + +function vk_quests.set_unfinished_quests(player, unfinished_quests) + player = vkore.playerObj(player) + + player:get_meta():set_string("unfinished_quests", minetest.serialize(unfinished_quests)) +end + +function vk_quests.on_enemy_death(enemy, slayer) + local unfinished_quests = vk_quests.get_unfinished_quests(slayer) + + if unfinished_quests == {} then + return + end + + for quest, progress in pairs(unfinished_quests) do + local questdef = vk_quest["kill_"..enemy] + + if quest == questdef.qid then + if not progress.kills then + unfinished_quests[quest].kills = 0 + end + + unfinished_quests[quest].kills = unfinished_quests[quest].kills + 1 + + if unfinished_quests[quest].kills >= questdef.amount then + unfinished_quests[quest] = nil + questdef.on_complete(slayer) + end + end + end + + vk_quests.set_unfinished_quests(slayer, unfinished_quests) +end + +minetest.register_on_joinplayer(function(player) + if vk_quests.get_unfinished_quests(player) == {} then + vk_quests.set_unfinished_quests(player, {}) + end +end) + +dofile(minetest.get_modpath(modname).."/quests.lua") +dofile(minetest.get_modpath(modname).."/sfinv_page.lua") diff --git a/mods/vk_quests/mod.conf b/mods/vk_quests/mod.conf index 130f9bd..468d3f7 100644 --- a/mods/vk_quests/mod.conf +++ b/mods/vk_quests/mod.conf @@ -1 +1,2 @@ name = vk_quests +depends = sfinv, vkore diff --git a/mods/vk_quests/quests.lua b/mods/vk_quests/quests.lua new file mode 100644 index 0000000..6a2e005 --- /dev/null +++ b/mods/vk_quests/quests.lua @@ -0,0 +1,14 @@ +vk_quests.register_quest("kill", "spider:spider", { + description = "Kill 10 spiders", + comments = { + "I've been seeing too many spiders recently", + "I'm planning on starting a fly collection but there's too much competition", + "A spider ate my cousin Joe, but I recently took a sharpened stick to the knee so I can't avenge him myself" + }, + amount = 10, + rewards_description = "Rewards: 5 gold and 15 xp", + rewards = { + xp = 15, + gold = 5, + }, +}) diff --git a/mods/vk_quests/sfinv_page.lua b/mods/vk_quests/sfinv_page.lua new file mode 100644 index 0000000..622f0f3 --- /dev/null +++ b/mods/vk_quests/sfinv_page.lua @@ -0,0 +1,41 @@ +sfinv.register_page("vk_quests:quests", { + title = "Quests", + get = function(self, player, context) + if not context then context = {} end + -- Active quest count + local aquest_count = #vk_quests.get_unfinished_quests(player) + local formspec = ([[ + real_coordinates[true] + label[3.5,0.5;Current quests in progress: %d] + button[4.2,0.8;2,0.6;refresh;Refresh] + ]]):format( + aquest_count + ) + + if aquest_count > 0 then + formspec = formspec .. "textlist[0,1.6;10.5,4.1;quests;" + + for qid, questprog in pairs(vk_quests.get_unfinished_quests(player)) do + local quest = vk_quests.get_quest(qid) + + formspec = ("%s\\[%d/%d\\] %s - %s,"):format( + formspec, + questprog.kills or 0, + quest.amount, + minetest.formspec_escape(quest.description), + minetest.formspec_escape(quest.rewards_description) + ) + end + + formspec = formspec:sub(1, -2) -- Remove trailing comma + formspec = formspec .. ";0;true]" + end + + return sfinv.make_formspec(player, context, formspec, true) + end, + on_player_receive_fields = function(self, player, context, fields) + if fields.refresh then + sfinv.set_page(player, "vk_quests:quests") + end + end +})