diff --git a/mods/mff/mff_quests/init.lua b/mods/mff/mff_quests/init.lua index 9fe2b92c..362006a2 100755 --- a/mods/mff/mff_quests/init.lua +++ b/mods/mff/mff_quests/init.lua @@ -1,34 +1,85 @@ ---- HIGLY UNFINISHED!!!! ---- HIGLY UNFINISHED!!!! ---- HIGLY UNFINISHED!!!! ---- HIGLY UNFINISHED!!!! ---- HIGLY UNFINISHED!!!! ---- HIGLY UNFINISHED!!!! ---- HIGLY UNFINISHED!!!! ---- HIGLY UNFINISHED!!!! ---- HIGLY UNFINISHED!!!! ---- HIGLY UNFINISHED!!!! ---- HIGLY UNFINISHED!!!! ---- HIGLY UNFINISHED!!!! --- GOT THAT ENOUGH? --- - gravgun mff.quests = {} mff.QPREFIX = "mff_quests:" +mff.QNOPREFIX = function(s) return s:sub(mff.QPREFIX:len()+1) end mff.quests.quests = { - testdiggydiggyhole = { - title = "Dig 10 stone nodes", - max = 10, - desc = "As long as you can not dig stone, you are not a real miner.", - periodicity = 10, - objective = { - dig = {"default:stone"} + still_testing_quests = { + title = "Stone digger", + description = "TEST QUEST!\nGet a mithril ingot at the end!", + repeating = 60*60*24, + awards = {["moreores:mithril_ingot"] = 1}, + tasks = { + diggy = { + title = "Dig 99 stone", + description = "Show you can dig through stone", + max = 99, + objective = { + dig = {"default:stone"} + } + }, + diggysrevenge = { + title = "Dig the last stone", + description = "You really thought 99 was a good number? Dig the last one.", + requires = {"diggy"}, + max = 1, + objective = { + dig = {"default:stone"} + } + } + } + }, + still_testing_quests2 = { + title = "Coal digger", + description = "TEST QUEST!\nGet a mithril ingot at the end!", + repeating = 60*60*24, + awards = {["moreores:mithril_ingot"] = 1}, + tasks = { + diggy = { + title = "Dig 19 coal", + description = "Get the fire mineral", + max = 19, + objective = { + dig = {"default:stone_with_coal"} + } + }, + diggysrevenge = { + title = "Dig the last one", + description = "I do this because of a technical issue, sorry", + requires = {"diggy"}, + max = 1, + objective = { + dig = {"default:stone_with_coal"} + } + } + } + }, + still_testing_quests3 = { + title = "Shiny diamonds", + description = "TEST QUEST!\nGet a mithril ingot at the end!", + repeating = 60*60*24, + awards = {["moreores:mithril_ingot"] = 1}, + tasks = { + diggy = { + title = "Dig 4 diamond", + description = "Yarr harr fiddle dee-dee, being a pirate is alright with me! Do what you want 'cause a pirate is free, you are a pirate! Go get the precious booty... underground. Mine it :/", + max = 4, + objective = { + dig = {"default:stone_with_diamond"} + } + }, + diggysrevenge = { + title = "Ultimate calbon atom alignement", + description = "Really, we must fix this", + requires = {"diggy"}, + max = 1, + objective = { + dig = {"default:stone_with_diamond"} + } + } } } } -mff.quests.quest_status = {} - function table.contains(table, element) for _, value in pairs(table) do if value == element then @@ -39,82 +90,49 @@ function table.contains(table, element) end function mff.quests.start_quest(playername, qname, meta) - mff.quests.quest_status[playername][qname] = 0 quests.start_quest(playername, mff.QPREFIX .. qname, meta) end -function mff.quests.restart_periodic_quest(playername, qname) - mff.quests.start_quest(playername, qname) -end - -function mff.quests.start_periodicity_timer(playername, qname) - local tstamp = -mff.quests.quest_status[playername][qname] - minetest.after(tstamp-os.time(), mff.quests.restart_periodic_quest, playername, qname) -end - -function mff.quests.start_all_periodicity_timers(playername) - local qstatus = mff.quests.quest_status[playername] - for qname, _ in pairs(qstatus) do - mff.quests.start_periodicity_timer(playername, qname) - end -end - -function mff.quests.set_quest_ended(playername, questname, metadata) - local qstatus = mff.quests.quest_status[playername] - local qname = questname:sub(mff.QPREFIX:len()+1) - local qinfo = mff.quests.quests[qname] - if qinfo.periodicity ~= nil and qinfo.periodicity >= 1 then - qstatus[qname] = -(os.time() + qinfo.periodicity) - mff.quests.start_periodicity_timer(playername, qname) - else - qstatus[qname] = nil +function mff.quests.handle_quest_end(playername, questname, metadata) + for item, count in pairs(mff.quests.quests[mff.QNOPREFIX(questname)].awards) do + minetest.add_item(minetest.get_player_by_name(playername):getpos(), {name=item, count=count, wear=0, metadata=""}) end end -- Register the quests defined above for qname, quest in pairs(mff.quests.quests) do - quests.register_quest(mff.QPREFIX .. qname, { - title = quest.title, - description = quest.desc, - max = quest.max, - autoaccept = true, - callback = mff.quests.set_quest_ended - }) + quest.completecallback = mff.quests.handle_quest_end + local ret = quests.register_quest(mff.QPREFIX .. qname, quest) end --- For quests where you have to dig something, the updates happen here +-- TODO +-- implement magical iterator, going through BOTH the simple quests +-- AND tasked quests objectives, returning a tuple like this: +-- questname, questdef, taskname (nil?), taskdef (nil?), objective_container (that is, either questdef or taskdef), pointer_to_function_to_update_the_objective_progress_with_only_one_parameter_the_others_being_automagically_passed_to_the_quests_API_so_that_we_dont_have_to_write_ifs_and_elses_everywhere_to_handle_both_quest_and_tasks_cases_because_it_would_give_crap_code + minetest.register_on_dignode(function(pos, oldnode, digger) - if not digger then return end -- Already happened before - local qstatus = mff.quests.quest_status[digger:get_player_name()] + if not digger then return end + local pname = digger:get_player_name() for qname, quest in pairs(mff.quests.quests) do - if qstatus[qname] ~= nil and qstatus[qname] >= 0 then - if quest.objective.dig then - if table.contains(quest.objective.dig, oldnode.name) then - quests.update_quest(digger:get_player_name(), mff.QPREFIX .. qname, 1) + if quest.tasks then + for tname, task in pairs(quest.tasks) do + if quests.is_task_visible(pname, mff.QPREFIX .. qname, tname) + and not quests.is_task_disabled(pname, mff.QPREFIX .. qname, tname) + and task.objective.dig then + if table.contains(task.objective.dig, oldnode.name) then + quests.update_quest_task(pname, mff.QPREFIX .. qname, tname, 1) + end end end end end end) --- TODO load data ---[[ -for playername in players do - mff.quests.start_all_periodicity_timers(playername) -end -]] - minetest.register_on_joinplayer(function (player) - -- TODO do nothing - mff.quests.quest_status[player:get_player_name()] = {} - mff.quests.start_quest(player:get_player_name(), "testdiggydiggyhole") -end) - -minetest.register_on_leaveplayer(function (player) - -- TODO do nothing - mff.quests.quest_status[player:get_player_name()] = nil -end) - -minetest.register_on_shutdown(function() - -- TODO save data -end) + local playername = player:get_player_name() + for _, qname in ipairs({"still_testing_quests", "still_testing_quests2", "still_testing_quests3"}) do + if not quests.quest_restarting_in(playername, mff.QPREFIX .. qname) then + mff.quests.start_quest(playername, qname) + end + end +end) \ No newline at end of file diff --git a/mods/quests/README b/mods/quests/README old mode 100755 new mode 100644 diff --git a/mods/quests/central_message.lua b/mods/quests/central_message.lua old mode 100755 new mode 100644 diff --git a/mods/quests/core.lua b/mods/quests/core.lua old mode 100755 new mode 100644 index c03b7ca8..51f60cb6 --- a/mods/quests/core.lua +++ b/mods/quests/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,276 @@ 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 --- --- 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 -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 +local function compute_tasks(playername, questname, nocallback) + local quest = quests.registered_quests[questname] + local plr_quest = quests.active_quests[playername][questname] + for taskname, task in pairs(quest.tasks) do + local plr_task = plr_quest[taskname] + if task.requires == nil then + plr_task.visible = true + else + plr_task.visible = false + local was_visible = task.visible + local final_enabler = "" + for _, enabler_name in ipairs(task.requires) do + if type(enabler_name) == "table" then + plr_task.visible = true + for _, subena_name in ipairs(plr_quest[enabler_name]) do + local plr_subena = plr_task[subena_name] + if plr_task.visible and plr_subena and (not plr_subena.visible or not plr_subena.finished) then + plr_task.visible = false + end + end + else + if plr_quest[enabler_name] then + plr_task.visible = plr_quest[enabler_name].finished or false + else + plr_task.visible = true + end + end + if plr_task.visible then + final_enabler = enabler_name + break + end + end + if plr_task.visible and not was_visible and not nocallback then + task.availablecallback(playername, questname, taskname, final_enabler, quest.metadata) + end + end + if task.disables_on ~= nil then + local was_disabled = task.disabled + local final_disabler = "" + for _, disabler_name in ipairs(task.disables_on) do + if type(disabler) == "table" then + plr_task.disabled = true + for _, subdis_name in ipairs(disabler) do + local plr_subdis = plr_quest[subdis_name] + if not plr_task.disabled and plr_subdis.visible and plr_subdis.finished then + plr_task.disabled = true + end + end + else + plr_task.disabled = plr_quest[disabler_name].finished + end + if plr_task.disabled then + final_disabler = disabler_name + break + end + end + if plr_task.disabled and not was_disabled and not nocallback then + task.disablecallback(playername, questname, taskname, final_disabler, quest.metadata) + end + end + end +end + +--- Registers a quest for later use. +-- There are two types of quests: simple and tasked. +-- +-- * 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. +-- -- Defaults to true. +-- completecallback, -- If autoaccept is true, gets called at quest completion. +-- -- function(playername, questname, metadata) +-- abortcallback, -- Called when a player cancels the quest. function(playername, questname, metadata) +-- repeating -- Delay in seconds before the quest becomes available again. If nil, 0 or false, 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. Not called when there are no task requirements (i.e. task is available from the start). +-- -- function(playername, questname, taskname, enablingtaskname, metadata) +-- -- enablingtaskname is a string or a table of strings, depending on the condition that unlocked the task +-- +-- completecallback, +-- -- Called upon task completion. +-- -- function(playername, questname, taskname, metadata) +-- } +-- 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. Not called when the task is disabled upon quest start. +-- -- function(playername, questname, taskname, disablingtaskname, metadata) +-- -- 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. +-- Some task names are reserved and will be ignored: +-- +-- * `metadata` +-- * `finished` +-- * `value` +-- +-- Note: this function *copies* the `quest` table, keeping only what's needed. This way you can implement custom +-- quest attributes in your mod and register the quest directly without worrying about keyvalue name collision. +-- @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's 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"), + icon = quest.icon or "quests_default_quest_icon.png", + startcallback = quest.startcallback or empty_callback, + autoaccept = not(quest.autoaccept == false), + completecallback = quest.completecallback or empty_callback, + abortcallback = quest.abortcallback or empty_callback, + repeating = quest.repeating 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(quest.tasks) ~= "table" then + quests.registered_quests[questname] = nil + return false + end + new_quest.tasks = {} + local tcount = 0 + for tname, task in pairs(quest.tasks) do + if tname ~= "metadata" and tname ~= "finished" and tname ~= "value" then + new_quest.tasks[tname] = { + title = task.title or S("missing title"), + description = task.description or S("missing description"), + icon = task.icon or "quests_default_quest_icon.png", + max = task.max or 1, + requires = task.requires, + availablecallback = task.availablecallback or empty_callback, + disables_on = task.disables_on, + disablecallback = task.disablecallback or empty_callback, + completecallback = task.completecallback or empty_callback + } + tcount = tcount + 1 + end + end + if tcount == 0 then -- No tasks! + quests.registered_quests[questname] = nil + return false + end 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, } 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, finished = false} + else + quests.active_quests[playername][questname] = {metadata = metadata} + for tname, task in pairs(quest.tasks) do + quests.active_quests[playername][questname][tname] = { + value = 0, + visible = false, + disabled = false, + finished = false + } + end + compute_tasks(playername, questname) 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 +local function check_active_quest_task(playername, questname, taskname) + return not( + taskname == nil or + not check_active_quest(playername, questname) or + quests.registered_quests[questname].simple or -- Quest is simple (i.e. no tasks) + quests.registered_quests[questname].tasks == nil or -- Who knows? Avoid crash. + quests.registered_quests[questname].tasks[taskname] == nil or -- No such task + quests.active_quests[playername][questname][taskname] == nil -- Player quest data has no such task + ) +end + +--- Updates a *simple* quest's status. +-- Calls the quest's `completecallback` 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 the quest continues +-- @return `nil` if there is no such quest, it is a tasked or non-active one, or no value was given +-- @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) or not quests.registered_quests[questname].simple + or value == nil then + return nil end - if (quests.active_quests[playername][questname] == nil) then - return false -- there is no such quest + local plr_quest = quests.active_quests[playername][questname] + if plr_quest.finished then + return true -- The quest is already finished end - if (quests.active_quests[playername][questname].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.completecallback(playername, questname, plr_quest.metadata) quests.accept_quest(playername,questname) quests.update_hud(playername) end @@ -100,27 +288,238 @@ 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, +--- Get a *simple* quest's progress. +-- @param playername Name of the player +-- @param questname Quest to get the progress value from +-- @return `number` of the progress +-- @return `nil` if there is no such quest, it is a tasked or non-active one +-- @see quests.get_task_progress +function quests.get_quest_progress(playername, questname) + if not check_active_quest(playername, questname) or not quests.registered_quests[questname].simple then + return nil + end + local plr_quest = quests.active_quests[playername][questname] + if plr_quest.finished then + return nil + end + return plr_quest.value +end + +--- Updates a *tasked* quest task's status. +-- Calls the quest's `completecallback` if autoaccept is `true` and all the quest's visible +-- and non-disabled tasks reaches their max value. +-- Also calls the task's `completecallback` it it gets completed. +-- 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 task is finished +-- @return `false` if it continues +-- @return `nil` if there is no such quest/task, is a simple or non-active quest, or no value was given +-- @see quests.update_quest +function quests.update_quest_task(playername, questname, taskname, value) + if not check_active_quest_task(playername, questname, taskname) or value == nil then + return nil + end + local plr_quest = quests.active_quests[playername][questname] + local plr_task = plr_quest[taskname] + if plr_task.finished then + return true -- The task is already finished + end + + local quest = quests.registered_quests[questname] + local task = quest.tasks[taskname] + local task_finished = false + plr_task.value = plr_task.value + value + if plr_task.value >= task.max then + plr_task.value = task.max + plr_task.finished = true + task.completecallback(playername, questname, taskname, quest.metadata) + task_finished = true + end + + compute_tasks(playername, questname) + -- Check for quest completion + local all_tasks_finished = true + for taskname, task in pairs(quest.tasks) do + local plr_task = plr_quest[taskname] + if plr_task.visible and not plr_task.disabled and not plr_task.finished then + all_tasks_finished = false + end + end + if all_tasks_finished then + if quest.autoaccept then + quest.completecallback(playername, questname, plr_quest.metadata) + quests.accept_quest(playername,questname) + quests.update_hud(playername) + end + -- If the update of this task ends the quest, it consequently *is* finished. + return true + end + quests.update_hud(playername) + return task_finished +end + +--- Get a task's progress. +-- Returns the max progress value possible for the given task if it is complete. +-- @param playername Name of the player +-- @param questname Quest the task belongs to +-- @param taskname Task to get the progress value from +-- @return `number` of the progress +-- @return `false` if the task has been disabled by another +-- @return `nil` if there is no such quest/task, or is a simple or non-active quest +-- @see quests.get_quest_progress +function quests.get_task_progress(playername, questname, taskname) + if not not check_active_quest_task(playername, questname, taskname) then + return nil + end + local plr_quest = quests.active_quests[playername][questname] + if plr_quest.finished then + return nil + end + local plr_task = plr_quest[taskname] + if not plr_task then + return nil + end + if plr_task.disabled then + return false + end + return plr_task.value +end + +--- Checks if a quest's task is visible to the player. +-- @param playername Name of the player +-- @param questname Quest which contains the task +-- @param taskname Task to check visibility +-- @return `true` if the task is visible +-- @return `false` if it is not +-- @return `nil` if the quest/task doesn't exist, is simple or isn't active +function quests.is_task_visible(playername, questname, taskname) + if not check_active_quest_task(playername, questname, taskname) then + return nil + end + return quests.active_quests[playername][questname][taskname].visible +end + +--- Checks if a quest's task is disabled to the player. +-- @param playername Name of the player +-- @param questname Quest which contains the task +-- @param taskname Task to check if it is disabled +-- @return `true` if the task is disabled +-- @return `false` if it is not +-- @return `nil` if the quest/task doesn't exist, is simple or isn't active +function quests.is_task_disabled(playername, questname, taskname) + if not check_active_quest_task(playername, questname, taskname) then + return nil + end + return quests.active_quests[playername][questname][taskname].disabled +end + +--- Gets the number of active (visible & non-disabled) tasks, and how many of them are completed +-- @param playername Name of the player +-- @param questname Quest name +-- @return `number, number` pair, where the first is the number of active tasks, and the second how many of them are completed +-- @return `nil` if the quest doesn't exist, is simple or isn't active +function quests.get_active_tasks_stats(playername, questname) + if not check_active_quest(playername, questname) or quests.registered_quests[questname].simple then + return nil + end + local plr_quest = quests.active_quests[playername][questname] + local active_tasks = 0 + local completed_active = 0 + for taskname, _ in pairs(quests.registered_quests[questname].tasks) do + local plr_task = plr_quest[taskname] + if plr_task.visible and not plr_task.disabled then + active_tasks = active_tasks + 1 + if plr_task.finished then + completed_active = completed_active + 1 + end + end + end + return active_tasks, completed_active +end + +--- Gets number of seconds before a quest can be done again. +-- @param playername Player's name +-- @param questname Quest name +-- @return `number` of seconds before quests becomes available +-- @return `nil` if the quest isn't repeating +function quests.quest_restarting_in(playername, questname) + if quests.info_quests[playername] and + quests.info_quests[playername][questname] and + quests.info_quests[playername][questname].restart_tstamp then + return quests.info_quests[playername][questname].restart_tstamp - os.time() + end + return nil +end + +local function restart_periodic_quest(playername, questname) + quests.start_quest(playername, questname) + if quests.info_quests[playername] and quests.info_quests[playername][questname] then + quests.info_quests[playername][questname].restart_tstamp = nil + end +end + +local function start_repeating_timer(playername, questname) + local delay = quests.quest_restarting_in(playername, questname) + if delay ~= nil then + minetest.after(delay, restart_periodic_quest, playername, questname) + end +end + +local function start_all_repeating_timers(playername) + local qinfos = quests.info_quests[playername] + if qinfos then + for questname, qinfo in pairs(qinfos) do + if qinfo.restart_tstamp then + start_repeating_timer(playername, questname) + end + end + end +end + +-- Restart all stopped repeating quests' timers +for playername, _ in pairs(quests.info_quests) do + start_all_repeating_timers(playername) +end + +local function handle_quest_end(playername, questname) + local quest = quests.registered_quests[questname] + if quest.repeating ~= 0 then + quests.info_quests[playername] = quests.info_quests[playername] or {} + quests.info_quests[playername][questname] = quests.info_quests[playername][questname] or {} + local qinfo = quests.info_quests[playername][questname] + qinfo.restart_tstamp = os.time() + quest.repeating + start_repeating_timer(playername, questname) + end +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 an error occured (the quest is still ongoing if it was) 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} end 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.success) end end + handle_quest_end(playername, questname) quests.show_message("success", playername, S("Quest completed:") .. " " .. quests.registered_quests[questname].title) minetest.after(3, function(playername, questname) quests.active_quests[playername][questname] = nil @@ -131,22 +530,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 `true` when the quest was aborted +-- @return `false` if there was an error (quest not 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 + 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 +552,69 @@ 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) + handle_quest_end(playername, questname) + 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) + return true end --- get metadata of the quest if the quest exists, else return nil +--- Set quest HUD visibility. +-- @param playername Player's name +-- @param questname Quest name +-- @param visible `bool` indicating if the quest should be visible +-- @see quests.get_quest_hud_visibility +function quests.set_quest_hud_visibility(playername, questname, visible) + if not check_active_quest(playername, questname) then + return + end + quests.info_quests[playername] = quests.info_quests[playername] or {} + quests.info_quests[playername][questname] = quests.info_quests[playername][questname] or {} + quests.info_quests[playername][questname].hide_from_hud = not visible + quests.update_hud(playername) +end + +--- Get quest HUD visibility. +-- @param playername Player's name +-- @param questname Quest name +-- @return `bool`: quest HUD visibility +-- @see quests.set_quest_hud_visibility +function quests.get_quest_hud_visibility(playername, questname) + if not check_active_quest(playername, questname) then + return false + end + local plr_qinfos = quests.info_quests[playername] + return not(plr_qinfos and plr_qinfos[questname] and plr_qinfos[questname].hide_from_hud) +end + +--- 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/mods/quests/depends.txt b/mods/quests/depends.txt old mode 100755 new mode 100644 diff --git a/mods/quests/formspecs.lua b/mods/quests/formspecs.lua old mode 100755 new mode 100644 index 0bab4870..7c8860a9 --- a/mods/quests/formspecs.lua +++ b/mods/quests/formspecs.lua @@ -7,52 +7,82 @@ else S = function(s) return s end end - -- construct the questlog function quests.create_formspec(playername, tab, integrated) local queststringlist = {} local questlist = {} - quests.formspec_lists[playername] = quests.formspec_lists[playername] or {} - quests.formspec_lists[playername].id = 1 + quests.formspec_lists[playername] = quests.formspec_lists[playername] or { + id = 1 + } quests.formspec_lists[playername].list = {} tab = tab or quests.formspec_lists[playername].tab or "1" - if (tab == "1") then + if tab == "1" then questlist = quests.active_quests[playername] or {} - elseif (tab == "2") then + elseif tab == "2" then questlist = quests.successfull_quests[playername] or {} - elseif (tab == "3") then + elseif tab == "3" then questlist = quests.failed_quests[playername] or {} end quests.formspec_lists[playername].tab = tab - local no_quests = true - for questname,questspecs in pairs(questlist) do - if (questspecs.finished == nil) then - local queststring = quests.registered_quests[questname]["title"] - if (questspecs["count"] and questspecs["count"] > 1) then - queststring = queststring .. " - " .. questspecs["count"] - elseif(not questspecs["count"] and quests.registered_quests[questname]["max"] ~= 1) then - queststring = queststring .. " - (" .. quests.round(questspecs["value"], 2) .. "/" .. quests.registered_quests[questname]["max"] .. ")" + local quest_count = 0 + for questname,questspecs in quests.sorted_pairs(questlist) do + if not questspecs.finished then + local quest = quests.registered_quests[questname] + if quest then -- Quest might have been deleted + local queststring = quest.title + if questspecs.count then + if questspecs.count > 1 then + queststring = queststring .. " - " .. questspecs.count + end + local restart_remaining = quests.quest_restarting_in(playername, questname) + if restart_remaining ~= nil then + queststring = queststring .. " (" .. S("restarts in %ds"):format(restart_remaining) .. ")" + end + elseif not questspecs.count and quest.max ~= 1 then + if quest.simple then + queststring = queststring .. " (" .. quests.round(questspecs.value, 2) .. "/" .. quest.max .. ")" + else + local active_tasks, active_completed = quests.get_active_tasks_stats(playername, questname) + if active_tasks and active_completed then + queststring = queststring .. " (" .. S("%d/%d tasks done"):format(active_completed, active_tasks) .. ")" + else + -- Kind of an error + queststring = queststring .. " (...)" + end + end + end + table.insert(queststringlist, queststring) + table.insert(quests.formspec_lists[playername].list, questname) + quest_count = quest_count + 1 end - table.insert(queststringlist, queststring) - table.insert(quests.formspec_lists[playername].list, questname) - no_quests = false end end + if quest_count ~= 0 and quests.formspec_lists[playername].id > quest_count then + quests.formspec_lists[playername].id = quest_count + end local formspec = "" - if (not integrated) then + if not integrated then formspec = formspec .. "size[7,9]" end formspec = formspec .. "tabheader[0,0;quests_header;" .. S("Open quests") .. "," .. S("Finished quests") .. "," .. S("Failed quests") .. ";" .. tab .. "]" - if (no_quests) then + if quest_count == 0 then formspec = formspec .. "label[0.25,0.25;" .. S("There are no quests in this category.") .. "]" else - formspec = formspec .. "textlist[0.25,0.25;6.5,6.5;quests_questlist;"..table.concat(queststringlist, ",") .. ";1;false]" + formspec = formspec .. "textlist[0.25,0.25;6.5,6;quests_questlist;"..table.concat(queststringlist, ",") .. ";" .. tostring(quests.formspec_lists[playername].id) .. ";false]" end - if (quests.formspec_lists[playername].tab == "1") then - formspec = formspec .."button[0.25,7;3,.7;quests_abort;" .. S("Abort quest") .. "]" + if quests.formspec_lists[playername].tab == "1" then + local hud_display = "true" + if quests.formspec_lists[playername].id then + local questname = quests.formspec_lists[playername].list[quests.formspec_lists[playername].id] + if not quests.get_quest_hud_visibility(playername, questname) then + hud_display = "false" + end + end + formspec = formspec .."button[0.25,7.1;3,.7;quests_abort;" .. S("Abort quest") .. "]" .. + "checkbox[.25,6.2;quests_show_quest_in_hud;" .. S("Show in HUD") .. ";" .. hud_display .. "]" end - formspec = formspec .. "button[3.75,7;3,.7;quests_config;" .. S("Configure") .. "]".. + formspec = formspec .. "button[3.75,7.1;3,.7;quests_config;" .. S("Configure") .. "]".. "button[.25,8;3,.7;quests_info;" .. S("Info") .. "]".. "button_exit[3.75,8;3,.7;quests_exit;" .. S("Exit") .. "]" return formspec @@ -75,7 +105,7 @@ function quests.create_config(playername, integrated) formspec = formspec .. "true" else formspec = formspec .. "false" - end + end formspec = formspec .. "]checkbox[.25,1.25;quests_config_central_message;" .. S("Central messages") .. ";" if(quests.hud[playername] ~= nil and quests.hud[playername].central_message_enabled) then formspec = formspec .. "true" @@ -117,31 +147,77 @@ local function wordwrap(text, linelength) end -- construct the info formspec -function quests.create_info(playername, questname, integrated) +function quests.create_info(playername, questname, taskid, integrated) local formspec = "" - if (not integrated) then - formspec = formspec .. "size[9,6.5]" + if not integrated then + formspec = formspec .. "size[7.5,9]" end - formspec = formspec .. "label[0.5,0.5;" - if (questname) then - formspec = formspec .. quests.registered_quests[questname].title .. "]" .. - "box[.4,1.5;8.2,4.5;#999999]" .. - "label[.5,1.5;" .. - wordwrap(quests.registered_quests[questname].description, 60) .. "]" + if questname then + local restart_remaining = quests.quest_restarting_in(playername, questname) + local quest = quests.registered_quests[questname] + formspec = formspec .. "image[0,0;0.8,0.8;" .. quest.icon .. "]" + if restart_remaining ~= nil then + formspec = formspec .. "label[0.8,0;" .. quest.title .. "]" .. + "label[0.8,0.3;" .. S("%ds seconds remaining"):format(restart_remaining) .. "]" + else + formspec = formspec .. "label[0.8,0.1;" .. quest.title .. "]" + end - if (quests.formspec_lists[playername].tab == "1") then - formspec = formspec .. "button[.5,6;3,.7;quests_info_abort;" .. S("Abort quest") .. "]" + if quest.simple then + formspec = formspec .. "textarea[.4,1;7.2,7;_;;" .. minetest.formspec_escape(quest.description) .. "]" + else + quests.formspec_lists[playername].taskid = nil + local taskidlist = {} + local taskstringlist = {} + for taskname, task in pairs(quest.tasks) do + local plr_task = nil + if quests.active_quests[playername] and quests.active_quests[playername][questname] then + plr_task = quests.active_quests[playername][questname][taskname] + end + if not plr_task or (plr_task and plr_task.visible) then + -- not plr_task => quest is finished, display all tasks + table.insert(taskidlist, taskname) + local color = "" + local suffix = "" + if plr_task then + if plr_task.finished then + color = "#00BB00" + end + if plr_task.disabled then + color = "#AAAAAA" + end + suffix = " - " .. quests.round(plr_task.value, 2) .. "/" .. task.max + end + table.insert(taskstringlist, color .. task.title .. suffix) + end + end + local task = false + if taskid ~= nil then + task = quest.tasks[taskidlist[taskid]] + end + task = task or {title=S("No task selected"), description=""} + formspec = formspec .. "textarea[.4,1;7.2,2;_;;" .. minetest.formspec_escape(quest.description) .. "]" .. + "textlist[0.1,2.9;7,2;quest_info_tasklist;" .. table.concat(taskstringlist, ",") .. "]" .. + "label[0.8,5.2;" .. task.title .. "]" .. + "textarea[.4,6;7.2,2;__;;" .. minetest.formspec_escape(task.description) .. "]" + if task.icon then + formspec = formspec .. "image[0,5.1;0.8,0.8;" .. task.icon .. "]" + end + end + + if quests.formspec_lists[playername].tab == "1" then + formspec = formspec .. "button[3.6,8;3,.7;quests_info_abort;" .. S("Abort quest") .. "]" end else - formspec = formspec .. S("No quest specified.") .. "]" + formspec = formspec .. "label[0.8,0.1;" .. S("No quest specified.") .. "]" end - formspec = formspec .. "button[3.25,6;3,.7;quests_info_return;" .. S("Return") .. "]" + formspec = formspec .. "button[.4,8;3,.7;quests_info_return;" .. S("Return") .. "]" return formspec end -- show the player playername his/her questlog -function quests.show_formspec(playername) +function quests.show_formspec(playername) minetest.show_formspec(playername, "quests:questlog", quests.create_formspec(playername)) end @@ -157,22 +233,22 @@ minetest.register_chatcommand("quests", { -- Handle the return fields of the questlog minetest.register_on_player_receive_fields(function(player, formname, fields) - if (player == nil) then + if player == nil then return end - local playername = player:get_player_name(); - if (playername == "") then + local playername = player:get_player_name() + if playername == "" then return end -- questlog - if (fields["quests_header"]) then - if (formname == "quests:questlog") then - minetest.show_formspec(playername, "quests:questlog", quests.create_formspec(playername, fields["quests_header"])) + if fields.quests_header then + if formname == "quests:questlog" then + minetest.show_formspec(playername, "quests:questlog", quests.create_formspec(playername, fields.quests_header)) else - if (fields["quests_header"] == "1") then + if fields.quests_header == "1" then unified_inventory.set_inventory_formspec(player, "quests") - elseif (fields["quests_header"] == "2") then + elseif fields.quests_header == "2" then unified_inventory.set_inventory_formspec(player, "quests_successfull") return else @@ -182,42 +258,58 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) end return end - if (fields["quests_questlist"]) then - local event = minetest.explode_textlist_event(fields["quests_questlist"]) - if (event.type == "CHG") then + if fields.quests_questlist then + local event = minetest.explode_textlist_event(fields.quests_questlist) + if event.type == "CHG" then quests.formspec_lists[playername].id = event.index + if formname == "quests:questlog" then + minetest.show_formspec(playername, "quests:questlog", quests.create_formspec(playername)) + else + unified_inventory.set_inventory_formspec(player, "quests") + end end end - if (fields["quests_abort"]) then - if (quests.formspec_lists[playername].id == nil) then + if fields.quests_abort then + if quests.formspec_lists[playername].id == nil then return end - quests.abort_quest(playername, quests.formspec_lists[playername]["list"][quests.formspec_lists[playername].id]) - if (formname == "quests:questlog") then + quests.abort_quest(playername, quests.formspec_lists[playername].list[quests.formspec_lists[playername].id]) + if formname == "quests:questlog" then minetest.show_formspec(playername, "quests:questlog", quests.create_formspec(playername)) else unified_inventory.set_inventory_formspec(player, "quests") end end - if (fields["quests_config"]) then - if (formname == "quests:questlog") then + if fields.quests_config then + if formname == "quests:questlog" then minetest.show_formspec(playername, "quests:config", quests.create_config(playername)) else unified_inventory.set_inventory_formspec(player, "quests_config") end end - if (fields["quests_info"]) then - if (formname == "quests:questlog") then - minetest.show_formspec(playername, "quests:info", quests.create_info(playername, quests.formspec_lists[playername].list[quests.formspec_lists[playername].id])) + if fields.quests_info then + if formname == "quests:questlog" then + minetest.show_formspec(playername, "quests:info", quests.create_info(playername, quests.formspec_lists[playername].list[quests.formspec_lists[playername].id], nil, false)) else unified_inventory.set_inventory_formspec(player, "quests_info") end end + if fields.quests_show_quest_in_hud ~= nil then + local questname = quests.formspec_lists[playername].list[quests.formspec_lists[playername].id] + if questname then + quests.set_quest_hud_visibility(playername, questname, fields.quests_show_quest_in_hud == "true") + if formname == "quests:questlog" then + minetest.show_formspec(playername, "quests:questlog", quests.create_formspec(playername)) + else + unified_inventory.set_inventory_formspec(player, "quests") + end + end + end -- config if (fields["quests_config_enable"]) then quests.hud[playername].autohide = false - if (fields["quests_config_enable"] == "true") then + if (fields["quests_config_enable"] == "true") then quests.show_hud(playername) else quests.hide_hud(playername) @@ -257,27 +349,38 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) if (fields["quests_config_return"]) then if (formname == "quests:config") then minetest.show_formspec(playername, "quests:questlog", quests.create_formspec(playername)) - else + else unified_inventory.set_inventory_formspec(player, "quests") end end -- info - if (fields["quests_info_abort"]) then - if (quests.formspec_lists[playername].id == nil) then + if fields.quest_info_tasklist then + local event = minetest.explode_textlist_event(fields.quest_info_tasklist) + if event.type == "CHG" then + if formname == "quests:info" then + minetest.show_formspec(playername, "quests:info", quests.create_info(playername, quests.formspec_lists[playername].list[quests.formspec_lists[playername].id], event.index, false)) + else + quests.formspec_lists[playername].taskid = event.index + unified_inventory.set_inventory_formspec(player, "quests_info") + end + end + end + if fields.quests_info_abort then + if quests.formspec_lists[playername].id == nil then return end - quests.abort_quest(playername, quests.formspec_lists[playername]["list"][quests.formspec_lists[playername].id]) - if (formname == "quests:info") then + quests.abort_quest(playername, quests.formspec_lists[playername].list[quests.formspec_lists[playername].id]) + if formname == "quests:info" then minetest.show_formspec(playername, "quests:questlog", quests.create_formspec(playername)) - else + else unified_inventory.set_inventory_formspec(player, "quests") end end - if (fields["quests_info_return"]) then - if (formname == "quests:info") then + if fields.quests_info_return then + if formname == "quests:info" then minetest.show_formspec(playername, "quests:questlog", quests.create_formspec(playername)) - else + else unified_inventory.set_inventory_formspec(player, "quests") end end diff --git a/mods/quests/hud.lua b/mods/quests/hud.lua old mode 100755 new mode 100644 index 527186bd..39e39f02 --- a/mods/quests/hud.lua +++ b/mods/quests/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,38 +16,39 @@ 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} + if quests.hud[playername] == nil then + quests.hud[playername] = { autohide = autohide } end - if (quests.hud[playername].list ~= nil) then + if quests.hud[playername].list ~= nil then return end - local hud = { + local player = minetest.get_player_by_name(playername) + if player == nil then + return false + end + quests.hud[playername].header = player:hud_add({ hud_elem_type = "text", alignment = {x=1, y=1}, position = {x = hud_config.position.x, y = hud_config.position.y}, - offset = {x = hud_config.offset.x, y = hud_config.offset.y}, + offset = {x = hud_config.offset.x, y = hud_config.offset.y - 20}, number = hud_config.number, - text = S("Quests:") } + text = S("Quests:") + }) - - - local player = minetest.get_player_by_name(playername) - if (player == nil) then - return false - end quests.hud[playername].list = {} - table.insert(quests.hud[playername].list, { value=0, id=player:hud_add(hud) }) 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 + if player == nil or quests.hud[playername] == nil or quests.hud[playername].list == nil then return end for _,quest in pairs(quests.hud[playername].list) do @@ -60,136 +64,203 @@ function quests.hide_hud(playername) end -local function get_quest_hud_string(questname, quest) - local quest_string = quests.registered_quests[questname].title - if (quests.registered_quests[questname].max ~= 1) then - quest_string = quest_string .. "\n ("..quests.round(quest.value, 2).."/"..quests.registered_quests[questname].max..")" - end - return quest_string +local function get_quest_hud_string(title, value, max) + return title .. "\n ("..quests.round(value, 2).."/"..max..")" end +local function get_hud_list(playername) + local deftable = {} + local counter = 0 + for questname, plr_quest in quests.sorted_pairs(quests.active_quests[playername]) do + local quest = quests.registered_quests[questname] + local hide_from_hud + if quests.info_quests[playername] and quests.info_quests[playername][questname] then + hide_from_hud = quests.info_quests[playername][questname].hide_from_hud + else + hide_from_hud = false + end + if quest and not hide_from_hud then -- Quest might have been deleted + local function get_table(name, value, max) + local def = { + text = { + hud_elem_type = "text", + alignment = { x=1, y= 1 }, + position = {x = hud_config.position.x, y = hud_config.position.y}, + offset = {x = hud_config.offset.x, y = hud_config.offset.y + counter * 40}, + number = hud_config.number, + text = name + } + } + if plr_quest.finished then + if quests.failed_quests[playername] and quests.failed_quests[playername][questname] then + def.text.number = quests.colors.failed + else + def.text.number = quests.colors.success + end + else + def.text.number = hud_config.number + end + if value and max then + def.bar = { + hud_elem_type = "image", + scale = { x = math.floor(20 * value / max), y = 1 }, + alignment = { x = 1, y = 1 }, + position = { x = hud_config.position.x, y = hud_config.position.y }, + offset = { x = hud_config.offset.x + 2, y = hud_config.offset.y + counter * 40 + 24 }, + text = "quests_questbar.png" + } + def.background = { + hud_elem_type = "image", + scale = { x = 1, y = 1 }, + size = { x = 2, y = 4 }, + alignment = { x = 1, y = 1 }, + position = { x = hud_config.position.x, y = hud_config.position.y }, + offset = { x = hud_config.offset.x, y = hud_config.offset.y + counter * 40 + 22 }, + text = "quests_questbar_background.png" + } + end + return def + end + if quest.simple then + deftable[questname] = get_table(get_quest_hud_string(quest.title, plr_quest.value, quest.max), plr_quest.value, quest.max) + counter = counter + 1 + else + deftable[questname] = get_table(quest.title, plr_quest.value, quest.max) + counter = counter + 0.5 + for taskname, task in pairs(quest.tasks) do + local plr_task = quests.active_quests[playername][questname][taskname] + if plr_task.visible and not plr_task.disabled and not plr_task.finished then + deftable[questname .. "#" .. taskname] = get_table("- " .. get_quest_hud_string(task.title, plr_task.value, task.max), plr_task.value, task.max) + counter = counter + 1 + if counter >= show_max + 1 then + break + end + end + end + counter = counter + 0.1 + end + if counter >= show_max + 1 then + break + end + end + end + return deftable +end + +local DELETED = {} -- only for internal use -- updates the hud function quests.update_hud(playername) - if (quests.hud[playername] == nil or quests.active_quests[playername] == nil) then + if quests.hud[playername] == nil or quests.active_quests[playername] == nil then return end - if (quests.hud[playername].list == nil) then - if (quests.hud[playername].autohide and next(quests.active_quests[playername]) ~= nil) then + if quests.hud[playername].list == nil then + if quests.hud[playername].autohide and next(quests.active_quests[playername]) ~= nil then quests.show_hud(playername) end return end local player = minetest.get_player_by_name(playername) - if (player == nil) then + if player == nil then return end - -- Check for changes in the hud - local i = 2 -- the first element is the title - local change = false - local visible = {} - local remove = {} - for j,hud_element in ipairs(quests.hud[playername].list) do - if (hud_element.name ~= nil) then - if (quests.active_quests[playername][hud_element.name] ~= nil) then - if (hud_element.value ~= quests.active_quests[playername][hud_element.name].value) then - hud_element.value = quests.active_quests[playername][hud_element.name].value - player:hud_change(hud_element.id, "text", get_quest_hud_string(hud_element.name, quests.active_quests[playername][hud_element.name])) - if (hud_element.id_bar ~= nil) then - player:hud_change(hud_element.id_bar, "scale", - { x = math.floor(20 * hud_element.value / quests.registered_quests[hud_element.name].max), - y = 1}) - end - end - if (i ~= j) then - player:hud_change(hud_element.id, "offset", { x= hud_config.offset.x, y=hud_config.offset.y + (i-1) *40}) - if (hud_element.id_background ~= nil) then - player:hud_change(hud_element.id_background, "offset", { x= hud_config.offset.x, y=hud_config.offset.y + (i-1) *40 + 22}) - end - if (hud_element.id_bar ~= nil) then - player:hud_change(hud_element.id_bar, "offset", { x= hud_config.offset.x + 2, y=hud_config.offset.y + (i-1) *40 + 24}) - end - - end - visible[hud_element.name] = true - i = i + 1 - else - player:hud_remove(hud_element.id) - if (hud_element.id_background ~= nil) then - player:hud_remove(hud_element.id_background) - end - if (hud_element.id_bar ~= nil) then - player:hud_remove(hud_element.id_bar) - end - table.insert(remove, j) - end - end - end - --remove ended quests - if (remove[1] ~= nil) then - for _,j in ipairs(remove) do - table.remove(quests.hud[playername].list, j) - i = i - 1 - end - end - - if (i >= show_max + 1) then - return - end - -- add new quests - local counter = i - 1 - for questname,questspecs in pairs(quests.active_quests[playername]) do - if (not visible[questname]) then - local id = player:hud_add({ hud_elem_type = "text", - alignment = { x=1, y= 1 }, - position = {x = hud_config.position.x, y = hud_config.position.y}, - offset = {x = hud_config.offset.x, y = hud_config.offset.y + counter * 40}, - number = hud_config.number, - text = get_quest_hud_string(questname, questspecs) }) - local id_background - local id_bar - if (quests.registered_quests[questname].max ~= 1) then - id_background = player:hud_add({ hud_elem_type = "image", - scale = { x = 1, y = 1 }, - size = { x = 2, y = 4 }, - alignment = { x = 1, y = 1 }, - position = { x = hud_config.position.x, y = hud_config.position.y }, - offset = { x = hud_config.offset.x, y = hud_config.offset.y + counter * 40 + 22 }, - text = "quests_questbar_background.png" }) - id_bar = player:hud_add({hud_elem_type = "image", - scale = { x = math.floor(20 * questspecs.value / quests.registered_quests[questname].max), - y = 1 }, - alignment = { x = 1, y = 1 }, - position = { x = hud_config.position.x, y = hud_config.position.y }, - offset = { x = hud_config.offset.x + 2, y = hud_config.offset.y + counter * 40 + 24 }, - text = "quests_questbar.png" }) - end - - table.insert(quests.hud[playername].list, { name = questname, - id = id, - id_background = id_background, - id_bar = id_bar, - value = questspecs.value }) - counter = counter + 1 - if (counter >= show_max + 1) then - break - end - end - end - - if (quests.hud[playername].autohide) then - if (next(quests.active_quests[playername]) == nil) then - player:hud_change(quests.hud[playername].list[1].id, "text", S("No more Quests")) + if quests.hud[playername].autohide then + if next(quests.active_quests[playername]) == nil then + player:hud_change(quests.hud[playername].header, "text", S("No more Quests")) minetest.after(3, function(playername) - if (next(quests.active_quests[playername]) ~= nil) then - player:hud_change(quests.hud[playername].list[1].id, "text", S("Quests:")) + if next(quests.active_quests[playername]) ~= nil then + player:hud_change(quests.hud[playername].header, "text", S("Quests:")) + quests.update_hud(playername) else quests.hide_hud(playername) end end, playername) end end + + -- Check for changes in the hud + local function table_diff(tab1, tab2) + local result_tab + for k, v in pairs(tab2) do + if not tab1[k] or tab1[k] ~= v then + if type(tab1[k]) == "table" and type(v) == "table" then + local diff = table_diff(tab1[k], v) + if diff ~= nil then + if not result_tab then + result_tab = {} + end + result_tab[k] = diff + end + else + if not result_tab then + result_tab = {} + end + result_tab[k] = v + end + end + end + for k, _ in pairs(tab1) do + if tab2[k] == nil then + if not result_tab then + result_tab = {} + end + result_tab[k] = DELETED + end + end + return result_tab + end + -- Merge `from` into table `into` + local function table_merge(from, into) + for k, v in pairs(from) do + if type(v) == "table" and type(into[k]) == "table" then + table_merge(v, into[k]) + else + into[k] = v + end + end + end + local old_hud = quests.hud[playername].list + local new_hud = get_hud_list(playername) + local diff = table_diff(old_hud, new_hud) + -- Copy the HUD IDs from the old table to the new one, to avoid loosing them + for questname, hud_elms in pairs(old_hud) do + for elm_name, elm_def in pairs(hud_elms) do + if new_hud[questname] and new_hud[questname][elm_name] then + new_hud[questname][elm_name].id = elm_def.id + end + end + end + if diff ~= nil then + for questname, hud_elms in pairs(diff) do + if hud_elms == DELETED then + for elm_name, elm_def in pairs(old_hud[questname]) do + player:hud_remove(elm_def.id) + end + else + for elm_name, elm_def in pairs(hud_elms) do + if not old_hud[questname] or not old_hud[questname][elm_name] or not old_hud[questname][elm_name].id then + new_hud[questname][elm_name].id = player:hud_add(elm_def) + else + for elm_prop_name, elm_prop in pairs(elm_def) do + if elm_prop_name ~= "id" then + if type(elm_prop) == "table" then + -- For table-based properties, MT expects a full table to be specified, + -- so we must create a merged table. Just merge the changes with the old + -- HUD table, since it will disappear. + table_merge(elm_prop, old_hud[questname][elm_name][elm_prop_name]) + else + old_hud[questname][elm_name][elm_prop_name] = elm_prop + end + player:hud_change(new_hud[questname][elm_name].id, elm_prop_name, old_hud[questname][elm_name][elm_prop_name]) + end + end + end + end + end + end + end + quests.hud[playername].list = new_hud end diff --git a/mods/quests/init.lua b/mods/quests/init.lua old mode 100755 new mode 100644 index a37a8baf..d45184b4 --- a/mods/quests/init.lua +++ b/mods/quests/init.lua @@ -1,6 +1,5 @@ -- reading previous quests local file = io.open(minetest.get_worldpath().."/quests", "r") -quests = {} if file then minetest.log("action", "Reading quests...") quests = minetest.deserialize(file:read("*all")) @@ -11,6 +10,7 @@ quests.registered_quests = {} quests.active_quests = quests.active_quests or {} quests.successfull_quests = quests.successfull_quests or {} quests.failed_quests = quests.failed_quests or {} +quests.info_quests = quests.info_quests or {} quests.hud = quests.hud or {} for idx,_ in pairs(quests.hud) do quests.hud[idx].first = true @@ -18,7 +18,7 @@ end quests.formspec_lists = {} -function quests.round(num, n) +function quests.round(num, n) local mult = 10^(n or 0) return math.floor(num * mult + .5) / mult end @@ -26,12 +26,26 @@ end quests.colors = { new = "0xAAAA00", success = "0x00AD00", - failed = "0xAD0000" + failed = "0xAD0000", } local MP = minetest.get_modpath("quests") +function quests.sorted_pairs(t) + local a = {} + for n in pairs(t) do table.insert(a, n) end + table.sort(a) + local i = 0 -- iterator variable + local iter = function () -- iterator function + i = i + 1 + if a[i] == nil then return nil + else return a[i], t[a[i]] + end + end + return iter +end + dofile(MP .. "/central_message.lua") dofile(MP .. "/core.lua") dofile(MP .. "/hud.lua") @@ -47,21 +61,24 @@ end -- write the quests to file minetest.register_on_shutdown(function() - minetest.log("action", "Writing quests to file") for playername, quest in pairs(quests.active_quests) do for questname, questspecs in pairs(quest) do - if (questspecs.finished) then + if questspecs.finished then quests.active_quests[playername][questname] = nil -- make sure no finished quests are saved as unfinished end end end - local file = io.open(minetest.get_worldpath().."/quests", "w") - if (file) then - file:write(minetest.serialize({ --registered_quests = quests.registered_quests, + local file, err = io.open(minetest.get_worldpath().."/quests", "w") + if file then + file:write(minetest.serialize({ active_quests = quests.active_quests, successfull_quests = quests.successfull_quests, - failed_quests = quests.failed_quests, - hud = quests.hud})) + failed_quests = quests.failed_quests, + info_quests = quests.info_quests, + hud = quests.hud})) file:close() + minetest.log("action", "Wrote quests to file") + else + minetest.log("action", "Failed writing quests to file: open failed: " .. err) end end) diff --git a/mods/quests/inventory_plus.lua b/mods/quests/inventory_plus.lua old mode 100755 new mode 100644 index 11ebfd04..24b65883 --- a/mods/quests/inventory_plus.lua +++ b/mods/quests/inventory_plus.lua @@ -1,4 +1,4 @@ -minetest.register_on_joinplayer(function(player) +minetest.register_on_joinplayer(function(player) inventory_plus.register_button(player, "quests") end) diff --git a/mods/quests/locale/de.txt b/mods/quests/locale/de.txt old mode 100755 new mode 100644 diff --git a/mods/quests/locale/template.txt b/mods/quests/locale/template.txt old mode 100755 new mode 100644 diff --git a/mods/quests/sounds/quests_failed.ogg b/mods/quests/sounds/quests_failed.ogg old mode 100755 new mode 100644 diff --git a/mods/quests/sounds/quests_new.ogg b/mods/quests/sounds/quests_new.ogg old mode 100755 new mode 100644 diff --git a/mods/quests/sounds/quests_success.ogg b/mods/quests/sounds/quests_success.ogg old mode 100755 new mode 100644 diff --git a/mods/quests/textures/inventory_plus_quests.png b/mods/quests/textures/inventory_plus_quests.png old mode 100755 new mode 100644 index e973b3bc..05363b5a Binary files a/mods/quests/textures/inventory_plus_quests.png and b/mods/quests/textures/inventory_plus_quests.png differ diff --git a/mods/quests/textures/quests_default_quest_icon.png b/mods/quests/textures/quests_default_quest_icon.png new file mode 100644 index 00000000..eb25506f Binary files /dev/null and b/mods/quests/textures/quests_default_quest_icon.png differ diff --git a/mods/quests/textures/quests_questbar.png b/mods/quests/textures/quests_questbar.png old mode 100755 new mode 100644 index 23371ac6..bc63aaa5 Binary files a/mods/quests/textures/quests_questbar.png and b/mods/quests/textures/quests_questbar.png differ diff --git a/mods/quests/textures/quests_questbar_background.png b/mods/quests/textures/quests_questbar_background.png old mode 100755 new mode 100644 index e846498f..ef980c9b Binary files a/mods/quests/textures/quests_questbar_background.png and b/mods/quests/textures/quests_questbar_background.png differ diff --git a/mods/quests/unified_inventory.lua b/mods/quests/unified_inventory.lua old mode 100755 new mode 100644 index 6be57540..fc71ff20 --- a/mods/quests/unified_inventory.lua +++ b/mods/quests/unified_inventory.lua @@ -8,7 +8,7 @@ unified_inventory.register_button("quests", { }) unified_inventory.register_page("quests", { - get_formspec = function(player) + get_formspec = function(player) local playername = player:get_player_name() local formspec = quests.create_formspec(playername, "1", true) return {formspec = formspec, draw_inventory=false} @@ -16,7 +16,7 @@ unified_inventory.register_page("quests", { }) unified_inventory.register_page("quests_successfull", { - get_formspec = function(player) + get_formspec = function(player) local playername = player:get_player_name() local formspec = quests.create_formspec(playername, "2", true) return {formspec = formspec, draw_inventory=false} @@ -24,7 +24,7 @@ unified_inventory.register_page("quests_successfull", { }) unified_inventory.register_page("quests_failed", { - get_formspec = function(player) + get_formspec = function(player) local playername = player:get_player_name() local formspec = quests.create_formspec(playername, "3", true) return {formspec = formspec, draw_inventory=false} @@ -41,7 +41,8 @@ unified_inventory.register_page("quests_config", { unified_inventory.register_page("quests_info", { get_formspec = function(player) local playername = player:get_player_name() - local formspec = quests.create_info(playername, quests.formspec_lists[playername].list[quests.formspec_lists[playername].id], true) + local formspec = quests.create_info(playername, quests.formspec_lists[playername].list[quests.formspec_lists[playername].id], + quests.formspec_lists[playername].taskid, true) return {formspec = formspec, draw_inventory = false } end })