diff --git a/core.lua b/core.lua index 0612e80..51f60cb 100644 --- a/core.lua +++ b/core.lua @@ -136,6 +136,12 @@ end -- 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` @@ -151,7 +157,7 @@ function quests.register_quest(questname, quest) 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 true, + autoaccept = not(quest.autoaccept == false), completecallback = quest.completecallback or empty_callback, abortcallback = quest.abortcallback or empty_callback, repeating = quest.repeating or 0 @@ -168,18 +174,20 @@ function quests.register_quest(questname, quest) new_quest.tasks = {} local tcount = 0 for tname, task in pairs(quest.tasks) do - 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 + 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 @@ -218,8 +226,8 @@ function quests.start_quest(playername, questname, metadata) finished = false } end + compute_tasks(playername, questname) end - compute_tasks(playername, questname) quests.update_hud(playername) quests.show_message("new", playername, S("New quest:") .. " " .. quest.title) @@ -280,6 +288,23 @@ function quests.update_quest(playername, questname, value) return false -- the quest continues end +--- 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. @@ -336,6 +361,33 @@ function quests.update_quest_task(playername, questname, taskname, value) 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 @@ -517,6 +569,34 @@ function quests.abort_quest(playername, questname) return true end +--- 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 diff --git a/formspecs.lua b/formspecs.lua index fd99cda..7c8860a 100644 --- a/formspecs.lua +++ b/formspecs.lua @@ -7,13 +7,13 @@ 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 @@ -25,8 +25,8 @@ function quests.create_formspec(playername, tab, integrated) end quests.formspec_lists[playername].tab = tab - local no_quests = true - for questname,questspecs in pairs(questlist) do + 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 @@ -54,23 +54,33 @@ function quests.create_formspec(playername, tab, integrated) end table.insert(queststringlist, queststring) table.insert(quests.formspec_lists[playername].list, questname) - no_quests = false + quest_count = quest_count + 1 end 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 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;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 + 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") .. ";" .. "false" .. "]" + "checkbox[.25,6.2;quests_show_quest_in_hud;" .. S("Show in HUD") .. ";" .. hud_display .. "]" end formspec = formspec .. "button[3.75,7.1;3,.7;quests_config;" .. S("Configure") .. "]".. "button[.25,8;3,.7;quests_info;" .. S("Info") .. "]".. @@ -248,37 +258,53 @@ 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 + 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 diff --git a/hud.lua b/hud.lua index 427bfc2..39e39f0 100644 --- a/hud.lua +++ b/hud.lua @@ -17,32 +17,30 @@ local hud_config = { position = {x = 1, y = 0.2}, number = quests.colors.new } --- Show quests HUD to player. --- The HUD can only show up to show_max quests +-- 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 @@ -50,7 +48,7 @@ end -- @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 @@ -66,145 +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 quest.simple and quests.registered_quests[questname].max ~= 1 then - quest_string = quest_string .. "\n ("..quests.round(quest.value, 2).."/"..quests.registered_quests[questname].max..")" - else - quest_string = quest_string .. "\n (...)" - 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 quest = quests.registered_quests[questname] - if quest then -- Quest might have been deleted - if quest.simple 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 quest.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 / quest.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 }) - else - -- TODO - end - counter = counter + 1 - if (counter >= show_max + 1) then - break - end - 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 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/init.lua b/init.lua index c683b19..d45184b 100644 --- a/init.lua +++ b/init.lua @@ -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")