diff --git a/central_message.lua b/central_message.lua index ff3402e..6a5f1bb 100644 --- a/central_message.lua +++ b/central_message.lua @@ -1,4 +1,4 @@ -if (cmsg) then +if rawget(_G, "cmsg") then function quests.show_message(t, playername, text) if (quests.hud[playername].central_message_enabled) then local player = minetest.get_player_by_name(playername) diff --git a/core.lua b/core.lua index 5c37cf3..f5d7f1a 100644 --- a/core.lua +++ b/core.lua @@ -1,3 +1,6 @@ +--- Quests core. +-- @module core + -- Boilerplate to support localized strings if intllib mod is installed. local S if minetest.get_modpath("intllib") then @@ -6,91 +9,181 @@ else -- If you don't use insertions (@1, @2, etc) you can use this: S = function(s) return s end end +local empty_callback = function(...) end --- registers a quest for later use +--- Registers a quest for later use. +-- There are two types of quests: simple and tasked. -- --- questname is the name of the quest to identify it later --- it should follow the naming conventions: "modname:questname" --- quest is a table in the following format --- { --- title, -- is shown to the player and should contain usefull information about the quest. --- description, -- a small description of the mod. --- max, -- is the desired maximum. If max is 1, no maximum is displayed. defaults to 1 --- autoaccept, -- is true or false, wether the result of the quest should be dealt by this mode or the registering mod. --- callback -- when autoaccept is true, at the end of the quest, it gets removed and callback is called. --- -- function(playername, questname, metadata) --- } --- --- returns true, when the quest was successfully registered --- returns falls, when there was already such a quest +-- * Simple quests are made of a single objective +-- * Taked quests are made of tasks, allowing simultaneous progress +-- within the quest as well as branching quest objectives +-- +-- Both quest types are defined by a table, and they share common information: +-- { +-- title, -- Self-explanatory. Should describe the objective for simple quests. +-- description, -- Description/lore of the quest +-- icon, -- Texture name of the quest's icon. If missing, a default icon is used. +-- startcallback, -- Called upon quest start. function(playername, questname, metadata) +-- autoaccept, -- If true, quest automatically becomes completed if its progress reaches the max. +-- endcallback, -- If autoaccept is true, gets called at the end of the quest. +-- -- function(playername, questname, metadata) +-- abortcallback, -- Called when a player cancels the quest. function(playername, questname, metadata) +-- periodicity -- Delay in seconds before the quest becomes available again. If nil or 0, doesn't restart. +-- } +-- +-- In addition, simple quests have a number-type `max` element indicating the max progress of the quest. +-- As for tasked quests, they have a table-type `tasks` element which value is like this: +-- tasks = { +-- start = { +-- title, +-- description, +-- icon, +-- max -- Max task progress +-- }, +-- another_task = { +-- [...], +-- +-- requires = {"start"}, +-- -- Table of task names which one must be completed for this task to unlock. +-- -- To to task completion groups (i.e. where ALL must be compileted), pass said names in a (sub)table. +-- +-- availablecallback, +-- -- Called when the task becomes available. +-- -- function(playername, questname, metadata, taskname, enablingtaskname) +-- -- enablingtaskname is a string or a table of strings, depending on the condition that unlocked the task +-- } +-- something = { +-- [...], +-- requires = {"start"}, +-- +-- disables_on = {"another_task"}, +-- -- Same as `requires`, but *disables* the task (it then does not count towards quest completion) +-- +-- disablecallback, +-- -- Called when the task becomes disabled. +-- -- function(playername, questname, metadata, taskname, disablingtaskname) +-- -- disablingtaskname is a string or a table of strings, depending on the condition that locked the task +-- } +-- } +-- In this previous example the 2 last tasks enables once the `start` one is completed, and the +-- last one disables upon `another_task` completion, effectively making it optional if one +-- completes `another_task` before it. +-- @param questname Name of the quest. Should follow the naming conventions: `modname:questname` +-- @param quest Quest definition `table` +-- @return `true` when the quest was successfully registered +-- @return `false` when there was already such a quest, or if mandatory info was omitted/corrupt function quests.register_quest(questname, quest) if (quests.registered_quests[questname] ~= nil) then return false -- The quest was not registered since there already a quest with that name end - quests.registered_quests[questname] = - { title = quest.title or S("missing title"), - description = quest.description or S("missing description"), - max = quest.max or 1, - autoaccept = quest.autoaccept or false, - callback = quest.callback, } + quests.registered_quests[questname] = { + title = quest.title or S("missing title"), + description = quest.description or S("missing description"), + icon = quest.icon or "quests_default_quest_icon.png", + startcallback = quest.startcallback or empty_callback, + autoaccept = quest.autoaccept or false, + callback = quest.callback or empty_callback, + endcallback = quest.endcallback or empty_callback, + abortcallback = quest.abortcallback or empty_callback, + periodicity = quest.periodicity or 0 + } + local new_quest = quests.registered_quests[questname] + if quest.max ~= nil then -- Simple quest + new_quest.max = quest.max or 1 + new_quest.simple = true + else + if quest.tasks == nil or type(quests.task) ~= "table" then + quests.registered_quests[questname] = nil + return false + end + new_quest.tasks = {} + local tcount = 0 + for tname, task in pairs(quest.tasks) do + new_quest.tasks[tname] = { + title = quest.title or S("missing title"), + description = quest.description or S("missing description"), + icon = quest.icon or "quests_default_quest_icon.png", + max = quest.max or 1, + requires = quest.requires, + availablecallback = quest.availablecallback or empty_callback, + disables_on = quest.disables_on, + disablecallback = quest.disablecallback or empty_callback + } + tcount = tcount + 1 + end + if tcount == 0 then -- No tasks! + quests.registered_quests[questname] = nil + return false + end + end return true end --- starts a quest for a specified player --- --- playername - the name of the player --- questname - the name of the quest, which was registered with quests.register_quest --- metadata - optional additional data --- --- returns false on failure --- returns true if the quest was started +--- Starts a quest for a specified player. +-- @param playername Name of the player +-- @param questname Name of the quest, which was registered with @{quests.register_quest} +-- @param metadata Optional additional data +-- @return `false` on failure +-- @return `true` if the quest was started function quests.start_quest(playername, questname, metadata) - if (quests.registered_quests[questname] == nil) then + local quest = quests.registered_quests[questname] + if quest == nil then return false end - if (quests.active_quests[playername] == nil) then + if quests.active_quests[playername] == nil then quests.active_quests[playername] = {} end - if (quests.active_quests[playername][questname] ~= nil) then - return false -- the player has already this quest + if quests.active_quests[playername][questname] ~= nil then + return false -- the player already has this quest + end + if quest.simple then + quests.active_quests[playername][questname] = {value = 0, metadata = metadata} + else + quests.active_quests[playername][questname] = {metadata = metadata} end - quests.active_quests[playername][questname] = {value = 0, metadata = metadata} quests.update_hud(playername) - quests.show_message("new", playername, S("New quest:") .. " " .. quests.registered_quests[questname].title) + quests.show_message("new", playername, S("New quest:") .. " " .. quest.title) return true end --- when something happens that has effect on a quest, a mod should call this method --- playername is the name of the player --- questname is the quest which gets updated --- the quest gets updated by value --- this method calls a previously specified callback if autoaccept is true --- --- returns true if the quest is finished --- returns false if there is no such quest or the quest continues +local function check_active_quest(playername, questname) + return not( + playername == nil or + questname == nil or + quests.registered_quests[questname] == nil or -- Quest doesn't exist + quests.active_quests[playername] == nil or -- Player has no data + quests.active_quests[playername][questname] == nil -- Quest isn't active + ) +end + +--- Updates a *simple* quest's status. +-- Calls the quest's `endcallback` if autoaccept is `true` and the quest reaches its max value. +-- Has no effect on tasked quests. +-- @param playername Name of the player +-- @param questname Quest which gets updated +-- @param value Value to add to the quest's progress (can be negative) +-- @return `true` if the quest is finished +-- @return `false` if there is no such quest, is a tasked one, or the quest continues +-- @see quests.update_quest_task function quests.update_quest(playername, questname, value) - if (quests.active_quests[playername] == nil) then - quests.active_quests[playername] = {} + if not check_active_quest(playername, questname) then + return false -- There is no such quest or it isn't active end - if (quests.active_quests[playername][questname] == nil) then - return false -- there is no such quest + if value == nil then + return false -- No value given end - if (quests.active_quests[playername][questname].finished) then - return false -- the quest is already finished + local plr_quest = quests.active_quests[playername][questname] + if plr_quest.finished then + return false -- The quest is already finished end - if (value == nil) then - return false -- no value given - end - quests.active_quests[playername][questname]["value"] = quests.active_quests[playername][questname]["value"] + value - if (quests.active_quests[playername][questname]["value"] >= quests.registered_quests[questname]["max"]) then - quests.active_quests[playername][questname]["value"] = quests.registered_quests[questname]["max"] - if (quests.registered_quests[questname]["autoaccept"]) then - if (quests.registered_quests[questname]["callback"] ~= nil) then - quests.registered_quests[questname]["callback"](playername, questname, - quests.active_quests[playername][questname].metadata) - end + local quest = quests.registered_quests[questname] + plr_quest.value = plr_quest.value + value + if plr_quest.value >= quest.max then + plr_quest.value = quest.max + if quest.autoaccept then + quest.endcallback(playername, questname, plr_quest.metadata) quests.accept_quest(playername,questname) quests.update_hud(playername) end @@ -100,16 +193,58 @@ function quests.update_quest(playername, questname, value) return false -- the quest continues end --- When the mod handels the end of quests himself, e.g. you have to talk to somebody to finish the quest, +--- Updates a *tasked* quest task's status. +-- Calls the quest's `endcallback` if autoaccept is `true` and all the quest's enabled +-- tasks reaches their max value. +-- Has no effect on simple quests. +-- @param playername Name of the player +-- @param questname Quest which gets updated +-- @param taskname Task to update +-- @param value Value to add to the task's progress (can be negative) +-- @return `true` if the quest is finished +-- @return `false` if there is no such quest, is a simple one, or the quest continues +-- @see quests.update_quest +function quests.update_quest_task(playername, questname, taskname, value) +--[[ + if not check_active_quest(playername, questname) then + return false -- There is no such quest or it isn't active + end + local quest = quests.registered_quests[questname] + if taskname == nil or quest.tasks[taskname] == nil or value == nil then + return false + end + local plr_quest = quests.active_quests[playername][questname] + if plr_quest.finished then + return false -- The quest is already finished + end + plr_quest.value = plr_quest.value + value + if plr_quest.value >= quest.max then + plr_quest.value = quest.max + if quest.autoaccept then + quest.endcallback(playername, questname, plr_quest.metadata) + quests.accept_quest(playername,questname) + quests.update_hud(playername) + end + return true -- the quest is finished + end + quests.update_hud(playername) + return false -- the quest continues +]] +end + +--- Confirms quest completion and ends it. +-- When the mod handles the end of quests himself, e.g. you have to talk to somebody to finish the quest, -- you have to call this method to end a quest --- returns true, when the quest is completed --- returns false, when the quest is still ongoing +-- @param playername Player's name +-- @param questname Quest name +-- @return `true` when the quest is completed +-- @return `false` when the quest is still ongoing function quests.accept_quest(playername, questname) - if (quests.active_quests[playername][questname] and not quests.active_quests[playername][questname].finished) then - if (quests.successfull_quests[playername] == nil) then + if check_active_quest(playername, questname) and not quests.active_quests[playername][questname].finished then + if quests.successfull_quests[playername] == nil then quests.successfull_quests[playername] = {} end - if (quests.successfull_quests[playername][questname] ~= nil) then + if quests.successfull_quests[playername][questname] ~= nil then quests.successfull_quests[playername][questname].count = quests.successfull_quests[playername][questname].count + 1 else quests.successfull_quests[playername][questname] = {count = 1} @@ -131,22 +266,21 @@ function quests.accept_quest(playername, questname) return false -- the quest hasn't finished end --- call this method, when you want to end a quest even when it was not finished --- example: the player failed --- --- returns false if the quest was not aborted --- returns true when the quest was aborted +--- Aborts a quest. +-- Call this method when you want to end a quest even when it was not finished. +-- Example: the player failed. +-- @param playername Player's name +-- @param questname Quest name +-- @return `false` if the quest was not aborted +-- @return `true` when the quest was aborted function quests.abort_quest(playername, questname) - if (questname == nil) then + if not check_active_quest(playername, questname) then return false - end - if (quests.failed_quests[playername] == nil) then + end + if quests.failed_quests[playername] == nil then quests.failed_quests[playername] = {} end - if (quests.active_quests[playername][questname] == nil) then - return false - end - if (quests.failed_quests[playername][questname] ~= nil) then + if quests.failed_quests[playername][questname] ~= nil then quests.failed_quests[playername][questname].count = quests.failed_quests[playername][questname].count + 1 else quests.failed_quests[playername][questname] = { count = 1 } @@ -154,30 +288,39 @@ function quests.abort_quest(playername, questname) quests.active_quests[playername][questname].finished = true for _,quest in ipairs(quests.hud[playername].list) do - if (quest.name == questname) then + if quest.name == questname then local player = minetest.get_player_by_name(playername) player:hud_change(quest.id, "number", quests.colors.failed) end end - quests.show_message("failed", playername, S("Quest failed:") .. " " .. quests.registered_quests[questname].title) + + local quest = quests.registered_quests[questname] + quest.abortcallback(playername, questname, quests.active_quests[playername][questname].metadata) + quests.show_message("failed", playername, S("Quest failed:") .. " " .. quest.title) minetest.after(3, function(playername, questname) quests.active_quests[playername][questname] = nil quests.update_hud(playername) end, playername, questname) end --- get metadata of the quest if the quest exists, else return nil +--- Get quest metadata. +-- @return Metadata of the quest, `nil` if there is none +-- @return `nil, false` if the quest doesn't exist or isn't active +-- @see quests.set_metadata function quests.get_metadata(playername, questname) - if (quests.active_quests[playername] == nil or quests.active_quests[playername][questname] == nil) then - return nil + if not check_active_quest(playername, questname) then + return nil, false end return quests.active_quests[playername][questname].metadata end --- set metadata of the quest +--- Set quest metadata. +-- @return `false` if the quest doesn't exist or isn't active +-- @return `nil` otherwise +-- @see quests.get_metadata function quests.set_metadata(playername, questname, metadata) - if (quests.active_quests[playername] == nil or quests.active_quests[playername][questname] == nil) then - return + if not check_active_quest(playername, questname) then + return false end quests.active_quests[playername][questname].metadata = metadata end diff --git a/hud.lua b/hud.lua index 527186b..c1f37be 100644 --- a/hud.lua +++ b/hud.lua @@ -1,3 +1,6 @@ +--- Quests HUD. +-- @module hud + -- Boilerplate to support localized strings if intllib mod is installed. local S if minetest.get_modpath("intllib") then @@ -13,8 +16,10 @@ local hud_config = { position = {x = 1, y = 0.2}, offset = { x = -200, y = 0}, number = quests.colors.new } --- call this function to enable the HUD for the player that shows his quests --- the HUD can only show up to show_max quests +--- Show quests HUD to player. +-- The HUD can only show up to show_max quests +-- @param playername Player whose quests HUD must be shown +-- @param autohide Whether to automatically hide the HUD once it's empty function quests.show_hud(playername, autohide) if (quests.hud[playername] == nil) then quests.hud[playername] = { autohide = autohide} @@ -41,7 +46,8 @@ function quests.show_hud(playername, autohide) minetest.after(0, quests.update_hud, playername) end --- call this method to hide the hud +--- Hide quests HUD to player. +-- @param playername Player whose quests HUD must be hidden function quests.hide_hud(playername) local player = minetest.get_player_by_name(playername) if (player == nil or quests.hud[playername] == nil or quests.hud[playername].list == nil) then