Module:Recipe: Difference between revisions

From Against the Storm Official Wiki
(Made the product row sort by the product name instead of the stack size. This is important for tables other than specifying product, since in that case the stack sizes are all the same anyway.)
m (Shrinking icon sizes)
 
(2 intermediate revisions by the same user not shown)
Line 52: Line 52:
local CSS_CLASS_WIKITABLE = "wikitable sortable mw-collapsible"
local CSS_CLASS_WIKITABLE = "wikitable sortable mw-collapsible"
-- Shortcut markup to go after the filename to add the extension and display parameters
-- Shortcut markup to go after the filename to add the extension and display parameters
local WIKI_MED_ICON_MARKUP = ".png|32px|alt="
local WIKI_MED_ICON_MARKUP = ".png|24px|alt="
local WIKI_LARGE_ICON_MARKUP = ".png|64px|alt="
local WIKI_LARGE_ICON_MARKUP = ".png|42px|alt="
-- Shortcut markup that's easier for Lua string concatenations
-- Shortcut markup that's easier for Lua string concatenations
local BR = "<br />"
local BR = "<br />"
Line 458: Line 458:
local goodName, goodIconFile = RecipeData.getGoodNameAndIcon(ingredientGoodID)
local goodName, goodIconFile = RecipeData.getGoodNameAndIcon(ingredientGoodID)
local iconMarkup = "[[File:" .. goodIconFile .. WIKI_MED_ICON_MARKUP .. goodName .. "|link=" .. goodName .. "]]"
local iconMarkup = "[[File:" .. goodIconFile .. WIKI_MED_ICON_MARKUP .. goodName .. "|link=" .. goodName .. "#Product]]"
local linkMarkup = "[[" .. goodName .. "]]"
local linkMarkup = "[[" .. goodName .. "#Product|" .. goodName .. "]]"
return iconMarkup .. NBSP .. linkMarkup
return iconMarkup .. NBSP .. linkMarkup
Line 476: Line 476:
local goodName, goodIconFile = RecipeData.getGoodNameAndIcon(productGoodID)
local goodName, goodIconFile = RecipeData.getGoodNameAndIcon(productGoodID)
local iconMarkup = "[[File:" .. goodIconFile .. WIKI_LARGE_ICON_MARKUP .. goodName .. "|link=" .. goodName .. "]]"
local iconMarkup = "[[File:" .. goodIconFile .. WIKI_LARGE_ICON_MARKUP .. goodName .. "|link=" .. goodName .. "#Ingredient]]"
local linkMarkup = "[[" .. goodName .. "]]"
local linkMarkup = "[[" .. goodName .. "#Ingredient|" .. goodName .. "]]"
return iconMarkup .. NBSP .. linkMarkup, goodName
return iconMarkup .. NBSP .. linkMarkup, goodName

Latest revision as of 02:58, 12 November 2023

Documentation for this module may be created at Module:Recipe/doc

-------------------------------------------------------------------------------
-- This module renders the {{Recipe}} template with a nice wikitable.
-- https://hoodedhorse.com/wiki/Against_the_Storm/Template:Recipe.
--
-- The template #invokes Recipe.render(frame), with provided parameters.
-- The template requires at least one of its three parameters, or it will
-- return an error.
--
-- The first parameter `product` specifies that you want a table rendered to 
-- show all recipes that result in the specified product. Make sure it is 
-- spelled correctly, as it appears in the game.
--
-- The second parameter `building` species that you want a table rendered to 
-- show all recipes that are produced in the specified building. Again, make
-- sure it is spelled and puncutated (apostrophes) correctly.
--
-- The third, parameter `ingredient` species that you want a table rendered to
-- show all the recipes that take that ingredient to produce, whether 
-- specifically (like Wood for Planks) or as an option among many. This 
-- parameter is intended to be used by itself, without a product or building
-- named.
-- 
-- The data for the recipes is retrieved from Module:RecipeData, which hides the 
-- variations in the original data from this module that extracts only what's
-- needed to display one or more wikitables on a wiki page. Using the data, this
-- module creates an HTML table.
-- 
-- The table will have exactly one row if both the product and building are 
-- specified. If either is missing, then expect a table with multiple rows.
-- If an ingredient is specified, then the table will likely be very long.
-- @module Recipe
local Recipe = {}



---
-- Constants
--
-- Currently this is the max number of ingredients a recipe can have
local RECIPE_INGRED_MAX = 3
-- Indexes to use for recipe records as they come from the CSV data
local INDEX_RECIPE_GRADE = 2
local INDEX_RECIPE_PRODTIME = 3
local INDEX_RECIPE_PRODUCT_STACK_SIZE = 4
local INDEX_RECIPE_PRODUCT_ID = 5
local INDEX_RECIPE_INGREDIENTS = 6
-- Indexes to use for workshop records as they come from the CSV data
local INDEX_WORKSHOP_ID = 1
local INDEX_WORKSHOP_NAME = 2

-- CSS styling for the recipe table... for now
local CSS_CLASS_WIKITABLE = "wikitable sortable mw-collapsible"
-- Shortcut markup to go after the filename to add the extension and display parameters
local WIKI_MED_ICON_MARKUP = ".png|24px|alt="
local WIKI_LARGE_ICON_MARKUP = ".png|42px|alt="
-- Shortcut markup that's easier for Lua string concatenations
local BR = "<br />"
local NBSP = "&nbsp;"
local BOLD = "'''"
-- Shortcuts for displaying the efficency grades as stars or the "low meter"
local GRADE_TO_STARS = {
	["Grade0"] = "[[File:Grade Stars 0.png|12px|link=|alt=0 Stars]]",
	["Grade1"] = "★",
	["Grade2"] = "★★",
	["Grade3"] = "★★★"
}



---
-- Member variables
---
local RecipeData = require('Module:RecipeData')



---
-- Called from Template:Recipe to render a table displaying the information.
-- Retrieves data from Module:RecipeData. The data retrieved looks like this:
-- 
-- (sometimes there's an extra first variable like workshop or ingredient)
-- targetRecipeMatches = {
-- 		[recipeID] match between recipe and workshops = {
--			[1] = { recipe data },
--			[2] = list of workshops that make it = {
--				{ workshop data },
--				{ workshop data },
--				{ workshop data }
--			}
--		},
--		[recipeID] match between recipe and workshops = {
--			similar
-- 		}
-- }
--
-- @param frame the template passes parameters and page data via frame
-- @return the formatted wikitext/HTML
function Recipe.render(frame)
	
	-- Extract the passed parameters.
    local productName = frame.args.product or frame.args[1]
    local buildingName = frame.args.building or frame.args[2]
    local ingredientName = frame.args.ingredient
	local display = frame.args.display
	local displayListOverride = false
	if display == "list" then
		displayListOverride = true
	end
	
	-- At least one parameter is required.
	if productName == "" and buildingName == "" and ingredientName == "" then
        return "The Recipe template requires that you specify at least one of the following: product, building, or ingredient."
    end
	
	-- Decide according to parameters
	if ingredientName and ingredientName ~= "" then
		return buildIngredientWikitext(ingredientName, displayListOverride)
	else
		if productName and productName ~= "" and buildingName and buildingName ~= "" then
			return buildProductBuildingWikitext(productName, buildingName, displayListOverride)
		else
			if productName and productName ~= "" then
				return buildProductWikitext(productName, displayListOverride)
			else
				if buildingName and buildingName ~= "" then
					return buildBuildingWikitext(buildingName, displayListOverride)
				else
					return "Error in Recipe template: Unknown parameter error."
				end
			end
		end
	end
end



---
-- Renders the table for the specified product.
--
-- @param productName the name of the product
-- @return the wikitext that renders the table
function buildProductWikitext(productName, displayListOverride)
	
	local targetRecipeMatches = RecipeData.getAllRecipesForProduct(productName)
	
	if not targetRecipeMatches then
		return "There are no recipes for product " .. productName .. "."
	end
	
	-- Start a wiki table the same way each time, and then populate each row.
	local wikiTable
	if displayListOverride then
		wikiTable = startList()
	else
		wikiTable = startNewWikiTable("Recipes for " .. productName .. ".")
	end
	
	-- Add the data retrieved to the wiki table
	for _, match in pairs(targetRecipeMatches) do
	
		local recipe = match[1]
		
		for _, workshop in ipairs(match[2]) do
			
			wikiTable = addRowToTable(wikiTable, recipe, workshop, displayListOverride)
		end
	end
	
	wikiTable = endWikiTable(wikiTable)

	return wikiTable
end



---
-- Renders the table for the specified building.
-- 
-- @param buildingName the name of the building
-- @return the wikitext that renders the table
function buildBuildingWikitext(buildingName, displayListOverride)
	
	-- Ignore the workshop also returned.
	local _, targetRecipeMatches = RecipeData.getAllRecipesAtBuilding(buildingName)
	
	if not targetRecipeMatches then
		return "There are no recipes in the building " .. buildingName .. "."
	end
	
	-- Start a wiki table the same way each time, and then populate each row.
	local wikiTable
	if displayListOverride then
		wikiTable = startList()
	else
		wikiTable = startNewWikiTable("Recipes in the " .. buildingName .. ".")
	end
	
	-- Add the data retrieved to the wiki table
	for _, match in pairs(targetRecipeMatches) do
	
		local recipe = match[1]
		
		for _, workshop in ipairs(match[2]) do
		
			wikiTable = addRowToTable(wikiTable, recipe, workshop, displayListOverride)
		end
	end
	
	wikiTable = endWikiTable(wikiTable)

	return wikiTable
end



---
-- Renders the table for the specified product and building.
-- 
-- @param productName the name of the product
-- @param buildingName the name of the building
-- @return the wikitext that renders the table
function buildProductBuildingWikitext(productName, buildingName, displayListOverride)
	
	local targetRecipeMatches = RecipeData.getOneRecipeAtBuilding(productName, buildingName)
	
	if not targetRecipeMatches then
		return "There is no recipe for product " .. productName .. " in the building " .. buildingName .. "."
	end
	
	-- Start a wiki table the same way each time, and then populate each row.
	local wikiTable
	if displayListOverride then
		wikiTable = startList()
	else
		wikiTable = startNewWikiTable("Recipe for " .. productName .. " in the " .. buildingName .. ".")
	end
	
	-- Add the data retrieved to the wiki table
	for _, match in pairs(targetRecipeMatches) do
	
		local recipe = match[1]
		
		for _, workshop in ipairs(match[2]) do
		
			wikiTable = addRowToTable(wikiTable, recipe, workshop, displayListOverride)
		end
	end
	
	wikiTable = endWikiTable(wikiTable)

	return wikiTable
end



---
-- Renders the table for the specified ingredient.
--
-- @param ingredientName the name of the ingredient
-- @return the wikitext that renders the table
function buildIngredientWikitext(ingredientName, displayListOverride)
	
	-- Ignore the ingredient also returned
	local _, targetRecipeMatches = RecipeData.getAllRecipesWithIngredient(ingredientName)
	
	if not targetRecipeMatches then
		return "There are no recipes for ingredient " .. ingredientName .. "."
	end
	
	-- Start a wiki table the same way each time, and then populate each row.
	local wikiTable
	if displayListOverride then
		wikiTable = startList()
	else
		wikiTable = startNewWikiTable("Recipes that require " .. ingredientName .. ".")
	end
	
	-- Add the data retrieved to the wiki table
	for _, match in pairs(targetRecipeMatches) do
	
		local recipe = match[1]
		
		for _, workshop in ipairs(match[2]) do
		
			wikiTable = addRowToTable(wikiTable, recipe, workshop, displayListOverride)
		end
	end
	
	wikiTable = endWikiTable(wikiTable)

	return wikiTable
end



---
-- Adds a row to the existing wiki table based on the recipe and building 
-- provided.
--
-- The structures look like this:
-- recipe {
-- 		id
--		efficiency grade
--		production time
--		product stack size
--		product good's id
--		ingredients {
--			options for ingredient #1 {
--				{ option #1 good's stack size, option #1 good's id }
--				{ option #2 good's stack size, option #2 good's id }
--				{ option #3 good's stack size, option #3 good's id }
--				etc.
--			}
--			options for ingredient #2 {
--				same as above
--			}
--			options for ingredient #3 {
--				same as above
--			}
--		}
-- }
--
-- workshop {
-- 		id
--		display name
--		description
--		category
--		size x
--		size y
--		city score
--		movable
--		initially essential
--		storage
--		construction time
--		required goods {
--			required good #1 stack size
--			required good #1 id
--			required good #2 stack size
--			required good #2 id
--			required good #3 stack size
--			required good #3 id
--		}
--		workplaces {
--			workplace #1
--			workplace #2
--			workplace #3
--			workplace #4
--		}
--		recipes {
--			recipe #1
--			recipe #2
--			recipe #3
--			recipe #4
--		}
-- }
--
-- @param wikiTable the table to continue adding to
-- @param recipe the recipe to pull data from
-- @param workshop the workshop to pull data from
-- @return wikiTable, augmented with the new row
function addRowToTable(wikiTable, recipe, workshop, displayListOverride)
	
	if not wikiTable then
		error("Wikitable in-progress is not set up correctly.")
	else
		if not recipe or not workshop then
			error("Recipe and workshop to display not set up correctly.")
		end
	end
	
	-- Get everything extracted and ready to go for easier code below
	local buildingName = workshop[INDEX_WORKSHOP_NAME]
	-- Do not add the extension here
	local buildingIconFile = workshop[INDEX_WORKSHOP_ID] .. "_icon"
	
	local recipeGrade = recipe[INDEX_RECIPE_GRADE]
	local recipeProdTime = recipe[INDEX_RECIPE_PRODTIME]
	local recipeProductStackSize = recipe[INDEX_RECIPE_PRODUCT_STACK_SIZE]
	local recipeProductID = recipe[INDEX_RECIPE_PRODUCT_ID]
	
	-- Now build the table tags. We're going to add everything to this row, so
	-- save the row itself to keep the hierarchy straight.
	local wikiTableRow = wikiTable:tag('tr')
	wikiTableRow:newline()
	
	-- Simple name and stars if display overriding
	if displayListOverride then
		wikiTableRow:tag("td")
			:wikitext("[[" .. buildingName .. "]]" .. NBSP)
			:wikitext("(" .. GRADE_TO_STARS[recipeGrade] .. ")" .. BR)
		:allDone():newline()
		return wikiTable
	end
	
	wikiTableRow:tag('td')
		:wikitext(writeBuildingCellContent(buildingIconFile, buildingName, recipeGrade, recipeProdTime))
	:done():newline()
	
	-- Skip empty cells of ingredients in this recipe
	local blanks = 0
	for i, tableToCheck in ipairs(recipe[INDEX_RECIPE_INGREDIENTS]) do
		if #tableToCheck == 0 then
			blanks = blanks + 1
		end
	end
	for i = 1,blanks do
		wikiTableRow:tag('td'):done():newline()
	end
	
	-- Then fill in the rest with the remaining ingredients, with inner tables
	-- to keep things aligned well
	for _, optionsGroup in ipairs(recipe[INDEX_RECIPE_INGREDIENTS]) do
		
		-- Skip blank groups; we already inserted blanks earlier.
		if #optionsGroup > 0 then
			
			local innerTable = wikiTableRow:tag('td'):tag('table')
			innerTable:newline()
			
			-- Loop through each option, but only up until they are blank
			for _, option in ipairs(optionsGroup) do
			
				local ingredientStackSize = option[1]
				local ingredientGoodID = option[2]
				
				innerTable:tag('tr')
					:tag('td'):wikitext(BOLD .. ingredientStackSize .. BOLD):done()
					:tag('td'):wikitext(writeIngredientCellContent(ingredientGoodID)):done()
				:done()
				innerTable:wikitext("\n")
			end
		end
	end
	
	local productCell, productName = writeProductCellContent(recipeProductID)
	
	-- Sort on product name, not the stack size
	wikiTableRow:tag('td'):attr("data-sort-value", productName)
		:wikitext(BOLD .. recipeProductStackSize .. BOLD .. NBSP .. productCell)
	:done()
	wikiTableRow:wikitext("\n")
	wikiTableRow:done()
	
	wikiTable:wikitext("\n")
	
	return wikiTable
end



---
-- Writes an icon and link to a resource page.
--
-- @param ingredientGoodID the good ID to use to create the link
-- @return a string of wiki markup
function writeIngredientCellContent(ingredientGoodID)
	
	local goodName, goodIconFile = RecipeData.getGoodNameAndIcon(ingredientGoodID)
	
	local iconMarkup = "[[File:" .. goodIconFile .. WIKI_MED_ICON_MARKUP .. goodName .. "|link=" .. goodName .. "#Product]]"
	local linkMarkup = "[[" .. goodName .. "#Product|" .. goodName .. "]]"
	
	return iconMarkup .. NBSP .. linkMarkup
end



---
-- Writes an icon and link to a resource page.
--
-- @param productGoodID the good ID to use to create the link
-- @return a string of wiki markup
-- @return the string name of the product
function writeProductCellContent(productGoodID)
	
	local goodName, goodIconFile = RecipeData.getGoodNameAndIcon(productGoodID)
	
	local iconMarkup = "[[File:" .. goodIconFile .. WIKI_LARGE_ICON_MARKUP .. goodName .. "|link=" .. goodName .. "#Ingredient]]"
	local linkMarkup = "[[" .. goodName .. "#Ingredient|" .. goodName .. "]]"
	
	return iconMarkup .. NBSP .. linkMarkup, goodName
end



---
-- Writes the contents of the building cell and returns a string to be used 
-- in a call to wikitext()
--
-- @param buildingIconFile icon filename, excluding 'File:', including extension
-- @param buildingName plain text name of the building to display
-- @param recipeGrade efficiency of the recipe
-- @param recipeProdTime production time in seconds
-- @return a string of wiki markup
function writeBuildingCellContent(buildingIconFile, buildingName, recipeGrade, recipeProdTime)

	local iconMarkup = "[[File:" .. buildingIconFile .. WIKI_LARGE_ICON_MARKUP .. buildingName .. "|link=" .. buildingName .. "]]"
	local linkMarkup = "[[" .. buildingName .. "]]"
	
	local grade = GRADE_TO_STARS[recipeGrade]
	if not grade then
		error("Incorrectly formatted efficiency grade: " .. recipeGrade)
	end
	
	local innerTable = mw.ustring.format("<table><tr><td>%s</td><td>%s<br />%s<br />%s</td></tr></table>", iconMarkup, linkMarkup, grade, formatTime(recipeProdTime))
	
	return innerTable
end



---
-- Formats seconds as mm:ss format, with leading zeros where necessary.
--
-- @param seconds number of seconds to reformat
-- @return a string representing mm:ss time format
function formatTime(seconds)

	local minutes = math.floor(seconds/60)
	local remainingSeconds = seconds % 60
	return string.format("%02d:%02d", minutes, remainingSeconds)
end



---
-- Start all the tables the same way so they look the same

-- @param the caption, if wanted
-- @return the wikitext
function startNewWikiTable(caption)
	
	local wikiTable = mw.html.create('table'):addClass(CSS_CLASS_WIKITABLE)
	wikiTable:newline()
	
	if caption then
		wikiTable:tag('caption'):wikitext(caption):done():newline()
	end
	
	wikiTable:tag('tr')
		:tag('th'):wikitext("Building"):done()
		:tag('th'):wikitext("Ingredient #1 options"):done()
		:tag('th'):wikitext("Ingredient #2 options"):done()
		:tag('th'):wikitext("Ingredient #3 options"):done()
		:tag('th'):wikitext("Product"):done()
	:done():newline()
	
	return wikiTable
end



---
-- Start the simple list display
-- 
-- @return the wikitext
function startList()
	
	local wikiTable = mw.html.create('table')
	wikiTable:newline()
	
	return wikiTable
end



---
-- Finishes the wikitext by closing out remaining tags.
--
-- @paramn wikiTable the table to close out
-- @return the wikitext to display
function endWikiTable(wikiTable)
	
	-- There might be more later.
	wikiTable:done()
	
	return wikiTable
end



return Recipe