Module:Recipe

From Against the Storm Official Wiki
Revision as of 21:40, 5 November 2023 by Aeredor (talk | contribs) (Created to replace RenderRecipe with new version of template code that is easier to maintain and can also do ingredients)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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
---
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

local INDEX_WORKSHOP_ID = 1
local INDEX_WORKSHOP_NAME = 2
local INDEX_WORKSHOP_INITIALLY_ESSENTIAL = 9

local RECIPE_INGRED_MAX = 3

local CSS_CLASS_WIKITABLE_SORTABLE = "wikitable sortable"
local WIKI_RESOURCE_SMALL_ICON_MARKUP = "|thumb|32px|alt="
local WIKI_RESOURCE_LARGE_ICON_MARKUP = "|thumb|64px|alt="
local WIKI_BUILDING_ICON_MARKUP = "|thumb|left|64px|alt="
local BR = "<br />"
local GRADE_TO_STARS = {
	["Grade0"] = "[[File:Grade Stars 0.png|12px|link=|alt=0 Stars]]",
	["Grade1"] = "★",
	["Grade2"] = "★★",
	["Grade3"] = "★★★"
}

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



---
-- Called from Template:Recipe to render a table displaying the information.
-- 
-- Retrieves data from Module:RecipeData
--
-- @param frame the template includes the calling context, mw.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 ingredient = frame.args.ingredient or frame.args[3]
	
	-- At least one parameter is required.
	if not productName and not buildingName and not ingredient then
        return "Error: The Recipe template requires that you specify at least one of the following: product, building, or ingredient."
    end
	
	-- Split according to parameters
	if ingredient then
		return buildIngredientWikitext(ingredient)
	else
		if productName and buildingName then
			return buildProductBuildingWikitext(productName, buildingName)
		else
			if productName then
				return buildProductWikitext(productName)
			else
				if buildingName then
					return buildBuildingWikitext(buildingName)
				else
					return "Error: Unknown parameter error."
				end
			end
		end
	end
end



---
-- Renders the table for the specified product. The data retrieved from
-- RecipeData looks like this:
-- 
-- table of matches {
-- 		match[recipeID] between recipe and workshops = {
--			recipe data { ... }
--			workshops that make it {
--				workshop data { ... }
--				workshop data { ... }
--				workshop data { ... }
--			}
--		}
--		match[recipeID] between recipe and workshops = {
--			...
-- 		}
-- }
--
-- @param productName the name of the product
-- @return the wikitext that renders the table
function buildProductWikitext(productName)
	
	local targetRecipeMatches = RecipeData.getAllRecipesForProduct(productName)
	
	if not targetRecipeMatches then
		error("Error retrieving recipes for product: " .. productName)
	end
	
	local wikiTable = startNewWikiTable("Recipes for " .. productName .. ".")
	
	for _, match in pairs(targetRecipeMatches) do
		local recipe = match[1]
		for _, building in ipairs(match[2]) do
			wikiTable = addRowToTable(wikiTable, recipe, building)
		end
	end
	
	wikiTable = endWikiTable(wikiTable)

	return wikiTable
end



---
-- Renders the table for the specified building. The data retrieved from 
-- RecipeData looks like this:
-- 
-- workshop,
-- table of matches {
-- 		match[recipeID] between recipe and workshops = {
--			recipe data { ... },
--			workshops that make it {
--				workshop data { ... }
--			}
--		},
--		match[recipeID] between recipe and workshops = {
--			recipe data { ... },
--			workshops that make it {
--				workshop data { ... }
--			}
-- 		}
-- }
-- 
-- @param buildingName the name of the building
-- @return the wikitext that renders the table
function buildBuildingWikitext(buildingName)
	
	local _, targetRecipeMatches = RecipeData.getAllRecipesAtBuilding(buildingName)
	
	if not targetRecipeMatches then
		error("Error retrieving recipes at building: " .. buildingName)
	end
	
	local wikiTable = startNewWikiTable("Recipes in the " .. buildingName .. ".")
	
	for _, match in pairs(targetRecipeMatches) do
		local recipe = match[1]
		for _, building in ipairs(match[2]) do
			wikiTable = addRowToTable(wikiTable, recipe, building)
		end
	end
	
	wikiTable = endWikiTable(wikiTable)

	return wikiTable
end



---
-- Renders the table for the specified product and building. The data returned
-- by RecipeData looks like this:
--	
-- table of matches {
-- 		match[recipeID] between recipe and workshops = {
--			[1] = recipe data { ... }
--			[2] = workshops that make it {
--				workshop data { ... }
--			}
-- 		}
-- }
-- 
-- @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)
	
	local targetRecipeMatches = RecipeData.getOneRecipeAtBuilding(productName, buildingName)
	
	if not targetRecipeMatches then
		error("Error retrieving the recipe for: " .. productName .. " (in the) " .. buildingName)
	end
	
	local wikiTable = startNewWikiTable("Recipe for " .. productName .. " in the " .. buildingName .. ".")
	
	for _, match in pairs(targetRecipeMatches) do
		local recipe = match[1]
		for _, building in ipairs(match[2]) do
			wikiTable = addRowToTable(wikiTable, recipe, building)
		end
	end
	
	wikiTable = endWikiTable(wikiTable)

	return wikiTable
end



---
-- Renders the table for the specified ingredient. The data retrieved from
-- RecipeData looks like this:
-- 
-- ingredient good data { ... },
-- table of matches {
-- 		match[recipeID] between recipe and workshops = {
--			recipe data { ... }
--			workshops that make it {
--				workshop data { ... }
--				workshop data { ... }
--				workshop data { ... }
--			}
--		}
--		match[recipeID] between recipe and workshops = {
--			...
-- 		}
-- }
--
-- @param ingredientName the name of the ingredient
-- @return the wikitext that renders the table
function buildIngredientWikitext(ingredientName)
	
	local _, targetRecipeMatches = RecipeData.getAllRecipesWithIngredient(ingredientName)
	
	if not targetRecipeMatches then
		error("Error retrieving recipes that require ingredient: " .. ingredientName)
	end
	
	local wikiTable = startNewWikiTable("Recipes that require " .. ingredientName .. ".")

	for _, match in pairs(targetRecipeMatches) do
		local recipe = match[1]
		for _, building in ipairs(match[2]) do
			wikiTable = addRowToTable(wikiTable, recipe, building)
		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)
	
	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]
	local buildingIsEssential = workshop[INDEX_WORKSHOP_INITIALLY_ESSENTIAL]
	local buildingIconFile = workshop[INDEX_WORKSHOP_ID] .. "_icon.png"
	
	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:wikitext("\n")
	
	wikiTableRow:tag('td')
		:wikitext(writeBuildingCellContent(buildingIconFile, buildingName, recipeGrade, recipeProdTime))
	:done()
	wikiTableRow:wikitext("\n")
	
	-- Skip empty cells for 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()
		wikiTableRow:wikitext("\n")
	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
		
		local innerTable = wikiTableRow:tag('td'):tag('table')
		
		
		-- Loop with step size of 2 to get the stack size and good together.
		for _, option in ipairs(optionsGroup) do
			local ingredientStackSize = option[1]
			local ingredientGoodID = option[2]
			
			innerTable:tag('tr')
				:tag('td'):wikitext("\'\'" .. ingredientStackSize .. "\'\'"):done()
				:tag('td'):wikitext(writeIngredientCellContent(ingredientGoodID)):done()
			:done()
			innerTable:wikitext("\n")
		end
	end
	
	wikiTableRow:tag('td')
		:wikitext("\'\'" .. recipeProductStackSize .. "\'\'" .. writeProductCellContent(recipeProductID))
	: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 = getGoodNameAndIcon(ingredientGoodID)
	
	local iconMarkup = "[[File:" .. goodIconFile .. WIKI_RESOURCE_SMALL_ICON_MARKUP .. goodName .. "|" .. goodName .. "]]"
	local linkMarkup = "[[" .. goodName .. "]]"
	
	return iconMarkup .. 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
function writeProductCellContent(productGoodID)
	
	local goodName, goodIconFile = getGoodNameAndIcon(productGoodID)
	
	local iconMarkup = "[[File:" .. goodIconFile .. WIKI_RESOURCE_LARGE_ICON_MARKUP .. goodName .. "|" .. goodName .. "]]"
	local linkMarkup = "[[" .. goodName .. "]]"
	
	return iconMarkup .. linkMarkup
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:'
-- @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_BUILDING_ICON_MARKUP .. buildingName .. "|" .. buildingName .. "]]"
	local linkMarkup = "[[" .. buildingName .. "]]"
	
	local grade = GRADE_TO_STARS[recipeGrade]
	if not grade then
		error("Incorrectly formatted efficiency grade: " .. recipeGrade)
	end
	
	return iconMarkup .. BR .. linkMarkup .. BR .. grade .. BR .. formatTime(recipeProdTime)
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_SORTABLE)
	wikiTable:wikitext("\n")
	
	if caption then
		wikiTable:tag('caption'):wikitext(caption):done()
		wikiTable:wikitext("\n")
	end
	
	wikiTable:tag('tr')
		:tag('th'):wikitext("Building"):done()
		:tag('th'):wikitext("Ingredient #1"):done()
		:tag('th'):wikitext("Ingredient #2"):done()
		:tag('th'):wikitext("Ingredient #3"):done()
		:tag('th'):wikitext("Product"):done()
		
	wikiTable:wikitext("\n")
	
	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