NPC-given quests!
This commit is contained in:
parent
430ef52287
commit
a507884045
@ -1,13 +1,13 @@
|
|||||||
globals = {
|
globals = {
|
||||||
"vkore", "player_stats", "hb", "players", "gold",
|
"vkore", "player_stats", "hb", "players", "gold",
|
||||||
"party", "nodes", "mapgen", "spawners", "pathfinder",
|
"party", "nodes", "mapgen", "spawners", "pathfinder",
|
||||||
"mobkit", "mobkit_custom", "swords"
|
"mobkit", "mobkit_custom", "swords", "vk_quests", "vk_quest",
|
||||||
}
|
}
|
||||||
|
|
||||||
read_globals = {
|
read_globals = {
|
||||||
"minetest", "sfinv",
|
"minetest", "sfinv",
|
||||||
string = {fields = {"split"}},
|
string = {fields = {"split"}},
|
||||||
table = {fields = {"copy", "getn"}},
|
table = {fields = {"copy", "getn", "indexof",}},
|
||||||
math = {fields = {"round"}},
|
math = {fields = {"round"}},
|
||||||
|
|
||||||
-- Builtin
|
-- 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 function prettify(npcname)
|
||||||
local output = npcname:gsub("_", " ")
|
local output = npcname:gsub("_", " ")
|
||||||
|
|
||||||
return output:gsub("^(.)", string.upper)
|
return output:gsub("^(.)", string.upper)
|
||||||
end
|
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)
|
local function register_npc(name, def)
|
||||||
|
def.npcname = name
|
||||||
|
|
||||||
minetest.register_node(modname..":"..name, {
|
minetest.register_node(modname..":"..name, {
|
||||||
|
npcname = name,
|
||||||
description = "NPC "..prettify(name),
|
description = "NPC "..prettify(name),
|
||||||
drawtype = "mesh",
|
drawtype = "mesh",
|
||||||
mesh = "player.obj",
|
mesh = "player.obj",
|
||||||
@ -55,24 +72,142 @@ local function register_npc(name, def)
|
|||||||
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
|
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
|
||||||
if not clicker or not clicker:is_player() then return end
|
if not clicker or not clicker:is_player() then return end
|
||||||
|
|
||||||
local formspec = ([[
|
local pname = clicker:get_player_name()
|
||||||
size[8,6]
|
|
||||||
real_coordinates[true]
|
|
||||||
label[0.2,0.3;%s]
|
|
||||||
]]):format(
|
|
||||||
prettify(name)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not def.convos then
|
if not context[pname] or context[pname].npcdef.npcname ~= name then
|
||||||
minetest.chat_send_player(clicker:get_player_name(), ("<%s> "):format(prettify(name)).."I have nothing to say")
|
context[pname] = {
|
||||||
return
|
tab = 1,
|
||||||
|
npcdef = def,
|
||||||
|
quest = 1
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
minetest.show_formspec(clicker:get_player_name(), "npcform", formspec)
|
vk_quests.show_npc_form(pname, context[pname])
|
||||||
end
|
end
|
||||||
})
|
})
|
||||||
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", {
|
register_npc("blacksmith", {
|
||||||
texture = "vk_npcs_blacksmith.png",
|
texture = "vk_npcs_blacksmith.png",
|
||||||
hit_replies = default_hit_replies,
|
hit_replies = default_hit_replies,
|
||||||
@ -86,6 +221,18 @@ register_npc("stable_man", {
|
|||||||
register_npc("guard", {
|
register_npc("guard", {
|
||||||
texture = "vk_npcs_guard.png",
|
texture = "vk_npcs_guard.png",
|
||||||
hit_replies = default_hit_replies,
|
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", {
|
register_npc("tavern_keeper", {
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
name = vk_npcs
|
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
|
for _, filename in ipairs(dirs) do
|
||||||
dofile(minetest.get_modpath(minetest.get_current_modname()).."/"..filename)
|
dofile(minetest.get_modpath(minetest.get_current_modname()).."/"..filename)
|
||||||
end
|
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_quests = {}
|
||||||
|
vk_quest = {}
|
||||||
|
|
||||||
local modname = minetest.get_current_modname()
|
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
|
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
|
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