NPC-given quests!

master
LoneWolfHT 2020-11-20 09:13:33 -08:00
parent 430ef52287
commit a507884045
9 changed files with 367 additions and 17 deletions

View File

@ -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

34
api.md Normal file
View File

@ -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

View File

@ -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", {

View File

@ -1,2 +1,2 @@
name = vk_npcs
depends = vk_mapgen
depends = vk_mapgen, vk_quests

View File

@ -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

View File

@ -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")

View File

@ -1 +1,2 @@
name = vk_quests
depends = sfinv, vkore

14
mods/vk_quests/quests.lua Normal file
View File

@ -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,
},
})

View File

@ -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
})