NPC-given quests!
This commit is contained in:
parent
430ef52287
commit
a507884045
@ -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
34
api.md
Normal 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
|
@ -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", {
|
||||
|
@ -1,2 +1,2 @@
|
||||
name = vk_npcs
|
||||
depends = vk_mapgen
|
||||
depends = vk_mapgen, vk_quests
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -1 +1,2 @@
|
||||
name = vk_quests
|
||||
depends = sfinv, vkore
|
||||
|
14
mods/vk_quests/quests.lua
Normal file
14
mods/vk_quests/quests.lua
Normal 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,
|
||||
},
|
||||
})
|
41
mods/vk_quests/sfinv_page.lua
Normal file
41
mods/vk_quests/sfinv_page.lua
Normal 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
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user