Module:Dialogue

local p = {} local utils = require("Module:Utils")

-- Dialogue options in excess of this number will not be processed. local MAX_OPTIONS = 12

--[[ Contains all mappings from dialogue option type names (parameter values) to internal dialogue IDs.

Internal IDs include: • `more`: represented by a speech balloon icon. Used when advancing dialogue. • `accept-quest`: represented by a green check mark. Typically used when the player accepts a new quest or a quest objective. This ends the dialogue. • `decline-quest`: represented by a red diagonal cross. Typically used when the player declines a quest or a quest objective. This ends the dialogue. • `end`: represented by an arrow pointing into an open door. Ends the dialogue, and in rare cases does something beyond that (e. g. makes the NPC hostile).

This table has several uses: • Should a type alias ever need to be defined, it can be added very easily. • Similarly, should a non-English translation of this module be made, it can use localized names for option types (to be used in the template) without affecting logic based on internal IDs. • Even if no type aliases or translations are ever made, having a table allows to easily check whether an option type is valid (by indexing the table). ]] local option_type_names = { ["more"] = "more", ["accept-quest"] = "accept-quest", ["decline-quest"] = "decline-quest", ["end"] = "end" } --[[ Returns the HTML ID of a dialogue section.

One of the design ideas behind this module was that users shouldn't ever need to type section IDs manually. This also helps minimize if not nullify damage in case breaking changes to the section ID format become needed.

Parameters: • npc_name (string), dialogue_id (string), section_id (string): the parameters of the specified dialogue section, whether provided explicitly as template arguments or inferred from variables stored in    previous calls

Returns a string containing the HTML ID of the dialogue section. ]] local function get_full_section_id(npc_name, dialogue_id, section_id) return "dialogue-" .. npc_name .. "-" .. dialogue_id .. "-" .. section_id end

--[[ Returns the mw.html instance for a dialogue icon.

Parameters: • icon_type (string): the type for this dialogue option, assumed to already be valid

Returns an mw.html instance with the relevant icon. ]] local function get_icon(icon_type) return mw.html.create("span") :addClass("dialogue-icon dialogue-icon-" .. icon_type) end

-- Returns a string with the text that says the player has the only option to ask the NPC to continue their speech. local function get_continue_text local root_span = mw.html.create("span") :addClass("dialogue-continue") :tag("span") :addClass("dialogue-continue-text") :wikitext('(The player has to select')			:done		:tag("span")			:addClass("dialogue-continue-option-wrap")			:node(get_icon("more"))			:tag("span")				:addClass("dialogue-continue-option-text")				:wikitext("Continue")				:done			:done		:tag("span")			:addClass("dialogue-continue-text")			:wikitext('to advance dialogue.)') :done

return tostring(root_span) end

--[=[ Preprocesses the text of the dialogue (the NPC's speech) to replace special tokens with final wikitext.

For the description of all such tokens, see the documentation for Template:Dialogue section.

Parameters: • text (string): the unprocessed text of the character's speech

Returns a string that contains the processed text. ]=] local function process_text(text) -- Standard format for "Continue." separators text = text:gsub("$continue", get_continue_text) -- ensure that all paragraphs of the text are proper HTML tags in the -- output text = "\n" .. text .. "\n" return text end

--[=[ Preprocesses the text of a dialogue option description to replace special tokens with final wikitext.

For the description of all such tokens, see the documentation for Template:Dialogue section.

Parameters: • npc_name (string), dialogue_id (string): the parameters of the processed dialogue section • desc (string): the unprocessed dialogue option description

Returns a string that contains the processed text. ]=] local function process_option_desc(npc_name, dialogue_id, desc) -- Short syntax for advancing dialogue desc = desc:gsub("%$jump:([^ ]+)", function(section_id)		local target_id = get_full_section_id(npc_name, dialogue_id, section_id)		local link_text = "section " .. section_id:upper		local link = "" .. link_text .. ""		return "Dialogue advances to " .. link .. "."	end) -- Short syntax for a jump to another dialogue desc = desc:gsub("%$farjump:([^/]+)/([^%$]+)%$([^%$]+)%$", function(target_dialogue_id, section_id, dialogue_desc)		local target_id = get_full_section_id(npc_name, target_dialogue_id, section_id)		local link_text = "section " .. section_id:upper		local link = "" .. link_text .. ""		return "Dialogue immediately advances to " .. link .. " of the " .. dialogue_desc .. "."	end) -- Short syntax for a link to another dialogue section desc = desc:gsub("%$link:([^/]+)/([^%$]+)%$([^%$]+)%$", function(target_dialogue_id, section_id, link_text)		local target_id = get_full_section_id(npc_name, target_dialogue_id, section_id)		local link = "" .. link_text .. ""		return link	end) -- Short syntax for ending dialogue desc = desc:gsub("%$end", "Dialogue ends.") return desc end

--[[ Processes the args and page variables to get the dialogue name and a flag for whether this section is the first in the dialogue.

Parameters: • args (table): the argument table as provided to the entry point in the module

Returns: 1) a string containing the ID of the current dialogue; 2) a boolean that is true if and only if this is the first section of the current dialogue. ]] local function get_dialogue_id(args) local dialogue_id = utils.get_arg(args, "dialogue_id") local is_first_section -- for the "back to top" link if not dialogue_id then is_first_section = false dialogue_id = assert(utils.get_var("dialogue-id"), "Could not get dialogue ID") else utils.set_var("dialogue-id", dialogue_id) is_first_section = true end return dialogue_id, is_first_section end

--[[ Processes the args and page variables to get the name of the NPC the dialogue is with.

Parameters: • args (table): the argument table as provided to the entry point in the module

Returns a string containing the name of the NPC being spoken to. ]] local function get_npc_name(args) local npc_name = utils.get_arg(args, "npc_name") if not npc_name then npc_name = utils.get_var("dialogue-npc-name") or mw.title.getCurrentTitle.baseText else utils.set_var("dialogue-npc-name", npc_name) end return npc_name end

--[[ Returns an mw.html instance representing the section's initial state, with the character's name and their speech filled in.

Parameters: • id (string): the complete HTML ID of the dialogue section • npc_name (string): the name of the NPC being spoken to, as supplied to the template • text (string): the unprocessed text of the NPC's speech

Returns an mw.html instance with a partially filled in dialogue section. ]] -- local function initialize_section(id, npc_name, text) return mw.html.create("div") :addClass("dialogue-section") :attr("id", id) :tag("span") :addClass("dialogue-npc-name") :wikitext(npc_name) :done :tag("div") :addClass("dialogue-text") :wikitext(process_text(text)) :done end

--[[ Processes the args table to get a list of options.

Dialogue option structure format: • type (string): the type of the option (a fallback type of "more" is used) • text (string): the text of the dialogue option • desc (string): the unprocessed description of the dialogue option

Parameters: • args (table): the argument table as provided to the entry point in the module

Returns an array of dialogue option structures (see above). ]] local function get_options(args) local options = {} for option_index = 1, MAX_OPTIONS do local arg_prefix = "option" .. option_index .. "_"		local option_text = utils.get_arg(args, arg_prefix .. "text") if not option_text then break end local option_type = utils.get_arg(args, arg_prefix .. "type", "more") local option_description = utils.get_arg(args, arg_prefix .. "desc") -- Use default descriptions for declining quests and end options if no description is specified if (option_type == "end" or option_type == "decline-quest") and not option_description then -- this will be processed into the actual description later option_description = "$end" end table.insert(options, {type = option_type, text = option_text, desc = option_description}) end return options end

--[[ Returns an mw.html instance representing a dialogue option's initial state, with the description not yet provided.

Parameters: • option_type (string): a valid type for this dialogue option • option_text (string): the text for this option

Returns an mw.html instance for this dialogue option (without the description). ]] local function initialize_option_html(option_type, option_text) return mw.html.create("li") :addClass("dialogue-option dialogue-option-" .. option_type) :tag("span") :addClass("dialogue-option-wrap") :node(get_icon(option_type)) :tag("span") :addClass("dialogue-option-text") :wikitext(option_text) :done :done end

--[[ Modifies the provided option mw.html instance to add the description.

Parameters: • option_html (mw.html instance): the descriptionless option HTML structure • option_desc (string): the unprocessed description for the option • npc_name (string), dialogue_id (string): the parameters of the processed dialogue section

Returns the original mw.html instance with the description appended. ]] local function add_option_description(option_html, option_desc, npc_name, dialogue_id) option_html:tag("span") :addClass("dialogue-option-description") :wikitext(process_option_desc(npc_name, dialogue_id, option_desc)) :done end

--[[ Returns an mw.html instance representing the list of dialogue options.

Parameters: • args (table): the argument table as provided to the entry point in the module • npc_name (string), dialogue_id (string): the parameters of the processed dialogue section

Returns an mw.html instance with the complete option list. ]] local function get_option_list_html(args, npc_name, dialogue_id) local option_list = mw.html.create("ol") :addClass("dialogue-option-list")

for index, option in ipairs(get_options(args)) do		local option_element = initialize_option_html(option.type, option.text) if option.desc then add_option_description(option_element, option.desc, npc_name, dialogue_id) end option_list:node(option_element) end return option_list end

-- Returns an mw.html instance with the link to the top section of this dialogue. local function get_top_section_link_html return mw.html.create("div") :addClass("dialogue-back-to-top") :wikitext(" ") end

--[=[ Entry point for Template:Dialogue section. For information on parameters, refer to the documentation for that template. ]=] function p.main(f) local args = f	if f == mw.getCurrentFrame then args = f:getParent.args end local dialogue_id, is_first_section = get_dialogue_id(args) local section_id = assert(utils.get_arg(args, "section_id"), "Dialogue section ID not specified") local npc_name = get_npc_name(args) local id = get_full_section_id(npc_name, dialogue_id, section_id) if is_first_section then utils.set_var("dialogue-top-section-id", id) end local text = args["text"] local section = initialize_section(id, npc_name, text) section:node(get_option_list_html(args, npc_name, dialogue_id)) if not is_first_section then section:node(get_top_section_link_html) end return tostring(section) end

--[=[ Entry point for Template:Dialogue section ID. For information on parameters, refer to the documentation for that template. ]=] function p.section_id(f) local args = f	if f == mw.getCurrentFrame then args = f:getParent.args end local npc_name = assert(		utils.get_arg(args, 1, utils.get_var("dialogue-npc-name")),		"Could not retrieve NPC name"	) local dialogue_id = assert(		utils.get_arg(args, 2, utils.get_var("dialogue-id")),		"Could not retrieve dialogue ID"	) local section_id = assert(utils.get_arg(args, 3), "Could not retrieve section ID") return get_full_section_id(npc_name, dialogue_id, section_id) end

return p