Module:Recipe: Difference between revisions

From RuneRealm Wiki
Jump to navigation Jump to search
Content added Content deleted
No edit summary
No edit summary
 
(3 intermediate revisions by the same user not shown)
Line 90: Line 90:
function p.main(frame)
function p.main(frame)
local args = frame:getParent().args
local args = frame:getParent().args
local category = args.category or ''


local function cost_to_number(cost_v, name, currencyName)
local function cost_to_number(cost_v, name, currencyName)
Line 212: Line 213:
local xp = v.experience == '?' and edit or v.experience
local xp = v.experience == '?' and edit or v.experience


local frame = mw.getCurrentFrame()
local citeText = "{{CiteText|text=Experience boost|quote=The experience shown is for players on the normal gamemode, without any other [[experience boosts]] active.}}"
local citeText = frame:expandTemplate{

title = 'CiteText',
args = {
text = 'Experience boost',
quote = 'The experience shown is for players on the normal gamemode, without any other [[experience boosts]] active.'
}
}
requirement = mw.html.create('tr')
requirement = mw.html.create('tr')
requirement
requirement
Line 571: Line 579:
-- Because string values are expected by smw, we convert the boolean values back to strings here
-- Because string values are expected by smw, we convert the boolean values back to strings here
members = members and 'Yes' or members == false and 'No' or ''
members = members and 'Yes' or members == false and 'No' or ''
local jsonObject = {skills = skills, members = members, materials = {}, output = output[1], ticks = ticks, facilities = facilities, tools = tools}
local jsonObject = {skills = skills, members = members, materials = {}, output = output[1], ticks = ticks, facilities = facilities, tools = tools, category = args.category }
local materialNames = {}
local materialNames = {}
for _, v in ipairs(materials) do
for _, v in ipairs(materials) do
Line 582: Line 590:
['Uses facility'] = mw.text.split(facilities or '', '%s*,%s*'),
['Uses facility'] = mw.text.split(facilities or '', '%s*,%s*'),
['Is members only'] = members,
['Is members only'] = members,
['RecipeCategory'] = args.category,
['Production JSON'] = mw.text.jsonEncode(jsonObject),
['Production JSON'] = mw.text.jsonEncode(jsonObject),
['Is boostable'] = {},
['Is boostable'] = {},

Latest revision as of 18:45, 13 November 2024

Module documentation
This documentation is transcluded from Template:No documentation/doc. [edit] [history] [purge]
This module does not have any documentation. Please consider adding documentation at Module:Recipe/doc. [edit]
Module:Recipe's function main is invoked by Template:Recipe.

--<nowiki>

local p = {}

-- convert some used globals to locals to improve performance
local math = math
local string = string
local table = table
local mw = mw
local expr = mw.ext.ParserFunctions.expr

local coins = require('Module:Coins')._amount
local yesno = require('Module:Yesno')
local params = require('Module:Paramtest')
local commas = require('Module:Addcommas')
local geprice = require('Module:Exchange')._price
local skillpic = require('Module:SCP')._main
local editbutton = require('Module:Edit button')
local onmain = require('Module:Mainonly').on_main
local currencies = require('Module:Currencies')._amount

local edit = editbutton('? (edit)')

-- Tools that need special handling
local toolsList = {
	['Axe'] = '[[File:Bronze axe.png|link=Axe]]',
	['Watering can'] = '[[File:Watering can(8).png|link=Watering can]]',
}

local facilitiesIcons = {
	['Altar (Zalcano)'] = '[[File:Altar (Zalcano\'s prison) icon.png|link=Altar (Zalcano)]]',
    ['Anvil'] = '[[File:Anvil icon.png|link=Anvil]]',
    ['Apothecary'] = '[[File:Apothecary icon.png|link=Apothecary]]',
    ['Banner easel'] = '[[File:Banner easel icon.png|link=Banner easel]]',
    ['Barbarian anvil'] = '[[File:Anvil icon.png|link=Anvil]]',
    ['Bench with vice'] = '[[File:Bench with vice icon.png|link=Bench with vice]]',
    ['Bench with lathe'] = '[[File:Bench with lathe icon.png|link=Bench with lathe]]',
    ['Blast Furnace'] = '[[File:Furnace icon.png|link=Blast Furnace]]',
    ['Blast furnace'] = '[[File:Furnace icon.png|link=Blast Furnace]]',
    ['Big Compost Bin'] = '[[File:Farming patch icon.png|link=Big Compost Bin]]',
    ['Brewery'] = '[[File:Brewery icon.png|link=Brewery]]',
    ['Clay oven'] = '[[File:Cooking range icon.png|link=Clay oven]]',
    ['Compost Bin'] = '[[File:Farming patch icon.png|link=Compost Bin]]',
    ['Cooking range'] = '[[File:Cooking range icon.png|link=Cooking range]]',
    ['Cooking range (2018 Easter event)'] = '[[File:Cooking range icon.png|link=Cooking range (2018 Easter event)]]',
    ['Crafting table 1'] = '[[File:Crafting table 1 icon.png|link=Crafting table 1]]',
    ['Crafting table 2'] = '[[File:Crafting table 2 icon.png|link=Crafting table 2]]',
    ['Crafting table 3'] = '[[File:Crafting table 3 icon.png|link=Crafting table 3]]',
    ['Crafting table 4'] = '[[File:Crafting table 4 icon.png|link=Crafting table 4]]',
    ['Dairy churn'] = '[[File:Dairy churn icon.png|link=Dairy churn]]',
    ['Dairy cow'] = '[[File:Dairy cow icon.png|link=Dairy cow]]',
    ['Demon lectern'] = '[[File:Demon lectern icon.png|link=Demon lectern]]',
    ['Eagle lectern'] = '[[File:Eagle lectern icon.png|link=Eagle lectern]]',
    ['Eodan'] = '[[File:Tannery icon.png|link=Tannery]]',
    ['Fancy Clothes Store'] = '[[File:Clothes shop icon.png|link=Fancy Clothes Store]]',
    ['Farming patch'] = '[[File:Farming patch icon.png|link=Farming/Patch_locations]]',
    ['Furnace'] = '[[File:Furnace icon.png|link=Furnace]]',
    ['Furnace (Elemental Workshop)'] = '[[File:Furnace icon.png|link=Furnace (Elemental Workshop)]]',
    ['Furnace (Zalcano)'] = '[[File:Furnace icon.png|link=Furnace (Zalcano)]]',
    ['Loom'] = '[[File:Loom icon.png|link=Loom]]',
    ['Lovakite furnace'] = '[[File:Furnace icon.png|link=Lovakite furnace]]',
    ['Mahogany demon lectern'] = '[[File:Mahogany demon lectern icon.png|link=Mahogany demon lectern]]',
    ['Mahogany eagle lectern'] = '[[File:Mahogany eagle lectern icon.png|link=Mahogany eagle lectern]]',
    ['Metal Press'] = '[[File:Furnace icon.png|link=Metal Press]]',
    ['Oak lectern'] = '[[File:Oak lectern icon.png|link=Oak lectern]]',
    ['Oak workbench'] = '[[File:Oak workbench icon.png|link=Oak workbench]]',
    ['Pluming stand'] = '[[File:Pluming stand icon.png|link=Pluming stand]]',
    ['Potter\'s Wheel'] = '[[File:Pottery wheel icon.png|link=Potter\'s Wheel]]',
    ['Pottery Oven'] = '[[File:Pottery wheel icon.png|link=Pottery Oven]]',
    ['Sawmill'] = '[[File:Sawmill icon.png|link=Sawmill]]',
    ['Sand pit'] = '[[File:Sandpit icon.png|link=Sand pit]]',
    ['Sbott'] = '[[File:Tannery icon.png|link=Tannery]]',
    ['Shield easel'] = '[[File:Shield easel icon.png|link=Shield easel]]',
    ['Singing bowl'] = '[[File:Singing bowl icon.png|link=Singing bowl]]',
    ['Spinning wheel'] = '[[File:Spinning wheel icon.png|link=Spinning wheel]]',
    ['Steel framed workbench'] = '[[File:Steel framed workbench icon.png|link=Steel framed workbench]]',
    ['Tannery'] = '[[File:Tannery icon.png|link=Tannery]]',
    ['Taxidermist'] = '[[File:Taxidermist icon.png|link=Taxidermist]]',
    ['Teak demon lectern'] = '[[File:Teak demon lectern icon.png|link=Teak demon lectern]]',
    ['Teak eagle lectern'] = '[[File:Teak eagle lectern icon.png|link=Teak eagle lectern]]',
    ['Thakkrad Sigmundson'] = '[[File:Tannery icon.png|link=Thakkrad Sigmundson]]',
    ['Water'] = '[[File:Water source icon.png|link=Water]]',
    ['Whetstone'] = '[[File:Whetstone icon.png|link=Whetstone]]',
    ['Windmill'] = '[[File:Windmill icon.png|link=Windmill]]',
    ['Woodcutting stump'] = '[[File:Woodcutting stump icon.png|link=Woodcutting stump]]',
    ['Wooden workbench'] = '[[File:Wooden workbench icon.png|link=Wooden workbench]]',
    ['Workbench (Guardians of the Rift)'] = '[[File:Workbench (Guardians of the Rift).png|30px|link=Workbench (Guardians of the Rift)]]',
}

function p.main(frame)
	local args = frame:getParent().args
	local category = args.category or ''

	local function cost_to_number(cost_v, name, currencyName)
		if currencyName ~= nil then
			if cost_v == nil then
				return 1
			elseif tonumber(commas._strip(cost_v),10) then
				return tonumber(commas._strip(cost_v),10)
			elseif tonumber(expr(cost_v),10) then
				return expr(cost_v)
			end
		elseif cost_v == nil then
			if pcall(function () geprice(name) end) then
				return geprice(name)
			else
				return 0
			end
		elseif string.lower(cost_v) == 'no' then
			return 0
		elseif tonumber(commas._strip(cost_v),10) then
			return tonumber(commas._strip(cost_v),10)
		elseif tonumber(expr(cost_v),10) then
			return expr(cost_v)
		end
		return 0
	end

	local function mat_list(objType)
		local ret_list = {}
		for i=1,11,1 do
			local mat = args[objType..i]
			if mat and params.has_content(mat) then
				local name = mat
				local txt = params.default_to(args[objType..i..'txt'], nil)
				local qty = params.default_to(args[objType..i..'quantity'],'1')
				local img = params.default_to(args[objType..i..'pic'], name)..'.png'
				local cost_v = args[objType..i..'cost']
				local currencyName = params.default_to(args[objType..i..'currency'], nil)
				local itemnote = args[objType..i..'itemnote'] or nil
				local costnote = args[objType..i..'costnote'] or nil
				local qtynote = args[objType..i..'quantitynote'] or nil
				local subtxt = args[objType..i..'subtxt'] or nil
				table.insert(ret_list, {
					name = name,
					txt = txt,
					cost = cost_to_number(cost_v, name, currencyName),
					quantity = qty,
					image = string.format('[[File:%s|link=%s]]', img, mat),
					currency_name = currencyName,
					outputnote = itemnote,
					quantitynote = qtynote,
					subtxt = subtxt,
					costnote = costnote
				} )
			end
		end
		return ret_list
	end
	
	local function skill_list()
		local ret_list = {}
		for i=1,10,1 do
			local skill = args['skill'..i]
			if skill and params.has_content(skill) then
				local name = skill
				local lvl = params.default_to(args['skill'..i..'lvl'],'?')
				local boost = params.default_to(args['skill'..i..'boostable'],'')
				local exp = commas._strip(params.default_to(args['skill'..i..'exp'],'?'))
				table.insert(ret_list, {
					name = name,
					level = lvl,
					boostable = boost,
					experience = exp,
				} )
			end
		end
		return ret_list
	end

	local output = mat_list('output')
	local materials = mat_list('mat')
	local skills = skill_list()

	local members = ''
	if params.has_content(args.members) then
		members = yesno(args.members, true)
	end
	
	local useSmw = true
	if params.has_content(args.smw) then
		useSmw = yesno(args.smw:lower(), true)
	end

	return p._main(frame, args, args.tools, skills, members, args.notes, materials, output, args.facilities, args.ticks, args.ticksnote, useSmw)
end

--
-- Generates an array of table rows based on skills required for the recipe.
--
-- @param skills {array} List of skill requirements generated by skill_list function.
-- @return {array} List of html tr elements for each skill requirement. Empty table if skills is an empty table or nil.
-- @return {bool} True if any skill requirement, other than level 1, is missing a boostable value in skills. False otherwise.
--
local function generate_skills_rows(skills)
	local requirements = {}
	local unknown_boostable_flag = false

	for i, v in ipairs(skills) do

		local levelText = v.level == '?' and edit or v.level
		-- Determine which boostable flag to add
		local boostable = yesno(v.boostable:lower() or nil) -- If v.boostable can't be lowered, boostable is nil. If it can, and isn't an expected value, yesno returns nil, so boostable is nil either way.
		if boostable == nil and (tonumber(v.level) or 0) > 1 then
			levelText = levelText .. ' <sup title="Unknown whether this requirement is boostable" style="cursor:help; text-decoration: underline dotted;">?</sup>'
			unknown_boostable_flag = true
		elseif boostable == false then
			levelText = levelText .. ' <sup title="This requirement is not boostable" style="cursor:help; text-decoration: underline dotted;">(nb)</sup>'
		elseif boostable == true then
			levelText = levelText .. ' <sup title="This requirement is boostable" style="cursor:help; text-decoration: underline dotted;">(b)</sup>'
		end -- if boostable is anything else, the skill level is probably 1 or unknown, so no boostable note is added
		
		local xp = v.experience == '?' and edit or v.experience

		local frame = mw.getCurrentFrame()
	    local citeText = frame:expandTemplate{
	        title = 'CiteText',
	        args = {
	            text = 'Experience boost',
	            quote = 'The experience shown is for players on the normal gamemode, without any other [[experience boosts]] active.'
	        }
	    }
	    
		requirement = mw.html.create('tr')
		requirement
			:tag('td'):attr('colspan', 2):wikitext(skillpic(v.name, nil, true)):done()
			:tag('td'):wikitext(levelText):done()
			:tag('td'):wikitext((tonumber(xp) ~= nil and commas._add(xp) or xp) .. " " .. citeText):done()

		table.insert(requirements, requirement)
	end

	return requirements, unknown_boostable_flag
end

--
-- Generates a td element for the ticks required.
--
-- @param frame {table} Frame passed in to main.
-- @param ticks {string|number} One of {nil, '', 'NA', 'Varies', [0-9]+} (case agnostic). Other strings will result in an error.
-- @param ticks_note {string} Custom note to be inserted into tick cell if ticks are varied or have a number value.
-- @return {html td element} A td element holding the number of ticks required, along with a note if applicable.
-- @return {bool} True if ticks_note was added to the td element. False otherwise.
--
local function generate_ticks_cell(frame, ticks, ticks_note)
	local has_ref_tag = false
	local ticks_cell = mw.html.create('td')
	local note = ''

	-- Prepare a note if ticks_note is given
	if ticks_note ~= nil then
		note = frame:extensionTag{ name='ref', content = ticks_note, args = { group='r' } }
	end

	-- Handle cases where no ticks are added, NA is used, Varies is used, or a number is given (default).
	-- Breaks if passed a string for ticks since tonumber produces a nil value.
	if (ticks or '') == '' then
		ticks_cell:wikitext(edit):done()
	elseif string.lower(ticks) == 'na' then
		ticks_cell:addClass('table-na'):css({ ['text-align'] = center }):wikitext('N/A'):done()
	elseif string.lower(ticks) == 'varies' then
		has_ref_tag = true
		ticks_cell:wikitext('Varies' .. note):done()
	else
		local secs = tonumber(ticks, 10) * 0.6
		if(note ~= '') then
			has_ref_tag = true
		end
		ticks_cell:attr('title', ticks .. ' ticks (' .. secs .. 's) per action'):wikitext(ticks .. ' (' .. secs .. 's)' .. note):done()
	end

	return ticks_cell, has_ref_tag
end

--
-- Generates a tr element for the item described by item_data.
--
-- @param frame {table} Frame passed in to main.
-- @param item_data {table} A table representing a single item required or output by the recipe. Single member of a table produced by mat_list.
-- @return {html tr element} A tr element holding all information contained in item_data.
-- @return {int} Total cost of the given amount of this item.
-- @return {bool} True if either an item note, quantity note, or cost note was added to the tr element. False otherwise.
--
local function make_row(frame, item_data)
	local classOverride, mat_ttl
	local textAlign = 'right'
	local has_ref_tag = false

	if item_data.currency_name ~= nil then
		mat_ttl = currencies(item_data.quantity * item_data.cost, item_data.currency_name)
	elseif item_data.cost == 0 then
		mat_ttl = 'N/A'
		classOverride = 'table-na'
		textAlign = 'center'
	else
		mat_ttl = coins(item_data.quantity * item_data.cost)
	end
	local name = item_data.txt and string.format('[[%s|%s]]', item_data.name, item_data.txt) or string.format('[[%s]]', item_data.name)
	local itemnote = item_data.outputnote and frame:extensionTag{ name = 'ref', content = item_data.outputnote, args = { group = 'r' } } or ''
	if (itemnote ~= '') then has_ref_tag = true end
	local quantitynote = item_data.quantitynote and frame:extensionTag{ name = 'ref', content = item_data.quantitynote, args = { group = 'r' } } or ''
	if (quantitynote ~= '') then has_ref_tag = true end
	local costnote
	if (item_data.costnote and string.lower(item_data.costnote) == 'calculated') then
		local class = string.gsub(name, '%W', '')
		costnote = frame:extensionTag{ name = 'ref', content = 'Calculated value given in the cost field (generally based on GE prices of ingredients).', args = { group = 'r' } } or ''
	else
		costnote = item_data.costnote and frame:extensionTag{ name = 'ref', content = item_data.costnote, args = { group = 'r' } } or ''
	end
	if (costnote ~= '') then has_ref_tag = true end
	costnote = costnote_v or costnote
	return mw.html.create('tr')
		:tag('td'):wikitext(item_data.image):done()
		:tag('td'):wikitext(name .. itemnote):done()
		:tag('td'):wikitext(commas._add(item_data.quantity) .. quantitynote):done()
		:tag('td'):addClass(classOverride):css({ ['text-align'] = textAlign }):wikitext(mat_ttl .. costnote):done(),
			item_data.quantity * item_data.cost,
			has_ref_tag
end

--
-- Generates a list of tr elements describing all the items given.
--
-- @param frame {table} Frame passed in to main.
-- @param items {array} A list containing a all items required or output by the recipe. Produced by mat_list.
-- @return {array} A list of tr elements, holding one row for each item in items.
-- @return {table} A table of total prices for each currency used by members of items.
-- @return {bool} True if either an item note, quantity note, or cost note was added to any tr element. False otherwise.
--
local function generate_rows(frame, items)

	local currency_costs = {
		['Coins'] = 0 
	}

	local has_ref_tag = false
	local rows = {}
	for i, v in ipairs(items) do
		local row, row_cost, has_row_note = make_row(frame, v)
		
		if row_cost ~= 0 then
			if v.currency_name ~= nil then
				currency_costs[v.currency_name] = (currency_costs[v.currency_name] and currency_costs[v.currency_name] or 0) + v.quantity * v.cost
			else
				currency_costs['Coins'] = currency_costs['Coins'] + v.quantity * v.cost
			end
		end

		has_ref_tag = has_ref_tag or has_row_note
		table.insert(rows, row)
	end


	return rows, currency_costs, has_ref_tag
end

--
-- Generates a tr element containing all of the total costs in the currencies for items in item_costs.
--
-- @param items {array} A list containing a all items required or output by the recipe. Produced by mat_list.
-- @param item_costs {table} A table of total prices for each currency used by members of items. Produced by generate_rows.
-- @return {html tr element} A tr element holding all costs found in item_costs.
--
function generate_total_cost_row(items, item_costs)
	local total_cost_row = mw.html.create('tr')

	if #items == 0 then
		total_cost_row:tag('td'):attr('colspan','5')
			:css({ ['font-style'] = 'italic', ['text-align'] = 'center' }):wikitext('Materials unlisted '..editbutton()):done()
	else
		local total_cost_breakdown = ''
		for i, v in next, item_costs, nil do
			total_cost_breakdown = (string.len(total_cost_breakdown) == 0 and total_cost_breakdown or total_cost_breakdown .. '<br />') .. (i == 'Coins' and coins(v) or currencies(v, i))
		end
		total_cost_row:tag('th'):attr('colspan', 3):css({['text-align'] = 'right'}):wikitext('Total cost'):done()
			:tag('td'):css({['text-align'] = 'right'}):wikitext(total_cost_breakdown)
	end

	return total_cost_row
end

--
-- Generates a tr element containing the difference between output and material costs (in coins).
--
-- @param frame {table} Frame passed in to main.
-- @param ticks {string|number} The number of ticks required to create one output. String or nil values will result in an assumption of 5.
-- @param materials_coins_cost {number} The total cost in coins of the materials.
-- @param outputs_coins_cost {number} The total cost in coins of the outputs.
-- @return {html tr element} A tr element holding the profit from one conversion of materials into outputs.
-- @return {bool} True if a note was added to the profit indicating questionable profits. False otherwise.
--
-- FIXME: It's not clear how the note field is supposed to be used. It's not documented and along with has_ref_tag, doesn't seem to actually ever get set to anything other than '' and false respectively.
function generate_profit_row(frame, ticks, materials_coins_cost, outputs_coins_cost)
	local profit = outputs_coins_cost - materials_coins_cost
	local note = ''
	local has_ref_tag = false

	-- Find ticks per action. Assume 5 if nothing else is given.
	-- If it takes 0 ticks, set to 1/8 since 8 actions can be performed per tick.
	local ticks_per_action = tonumber(ticks) or 5
	if ticks_per_action == 0 then
		ticks_per_action = 1/8
	end

	-- Create and populate the table row element
	local profit_row = mw.html.create('tr')
	profit_row
		:tag('th'):attr('colspan', 3):css({['text-align'] = 'right'}):wikitext('Profit'):done()
		:tag('td'):css({['text-align'] = 'right'}):wikitext(coins(profit) .. note):done()

	return profit_row, has_ref_tag
end

--
-- Main
--
function p._main(frame, args, tools, skills, members, notes, materials, output, facilities, ticks, ticks_note, useSmw)	
	local function toolImages(t)
		local images = {}
				
		if params.is_empty(t) then
			return 'None'
		end
		
		local spl = mw.text.split(t, ",")
		for _, image_i in ipairs(spl) do
			image_i = mw.text.trim(image_i)
			if toolsList[image_i] then
				table.insert(images, toolsList[image_i])
			else
				table.insert(images, string.format("[[File:%s.png|link=%s]]", image_i, image_i))
			end
		end
		return table.concat(images)
	end
	
	local function facilityLinks(f)
		local links = {}
		
		if params.is_empty(f) then
			return 'None'
		end
		
		local spl = mw.text.split(f, ",")
		for _, link_i in ipairs(spl) do
			if facilitiesIcons[link_i] ~= nil then
				table.insert(links, string.format("%s [[%s]]", facilitiesIcons[link_i], link_i))
			else
				table.insert(links, string.format("[[%s]]", link_i))
			end
		end
		return table.concat(links, "<br />")
	end

--------------------------------------------------------------------------------
-- START OF REQUIREMENTS TABLE
	-- This table contains skill reqs and xp, quest reqs, members req, and ticks
	local requirementsTable = mw.html.create('table')
			:addClass('wikitable align-center-2 align-right-3')
			:css({ width = '100%',
				['margin-bottom'] = '0' })
	
	requirementsTable:tag('caption'):wikitext("Requirements"):done()
	
	-- Skills
	local unknown_boostable_flag = false

	if #skills ~= 0 then

		local skill_requirements = mw.html.create('tr')
		skill_requirements
			:tag('th'):attr('colspan', 2):wikitext('Skill'):done()
			:tag('th'):wikitext('Level'):done()
			:tag('th'):wikitext('XP'):done()
		
		skill_requirement_rows, unknown_boostable_flag = generate_skills_rows(skills)

		for _, row in ipairs(skill_requirement_rows) do
			skill_requirements:node(row)
		end
		
		requirementsTable:node(skill_requirements)
	end


	-- Notes
	if notes ~= nil then
		requirementsTable:tag('tr')
			:tag('td'):attr('colspan', 4):wikitext(notes):done()
	end

	-- Members and Ticks row
	local members_and_ticks_row = mw.html.create('tr')
	-- lua var members has historically been either 'Yes', 'No', or '' at this point
	-- However it wasn't quite obvious that there could be 3 values which led to the previous comments asking if yesno should be used.
	-- Now p.main() leaves the variable as either true, false, or '' instead of converting the true and false to strings 'Yes' and 'No' respectively.
	-- This makes it so we can more cleanly and obviously handle the 3 cases here without ambiguity about the string values and whether to use yesno or not.
	local membersTemplate = members and "[[File:Member icon.png|center|link=Members]]" or members == false and "[[File:Free-to-play icon.png|center|link=Free-to-play]]" or edit

	members_and_ticks_row
		:tag('th'):wikitext('Members'):done()
		:tag('td'):wikitext(membersTemplate):done()
		:tag('th'):attr('title', 'Ticks per action'):wikitext('Ticks'):done()

	local ticks_cell, has_ticks_ref_tag = generate_ticks_cell(frame, ticks, ticks_note)
	members_and_ticks_row:node(ticks_cell)
	
	requirementsTable:node(members_and_ticks_row)

	--Tools and Facilities row
	if params.has_content(tools) or params.has_content(facilities) then
		local toolImgs = toolImages(tools)
		local facilityLnks = facilityLinks(facilities)
		requirementsTable:tag('tr')
			:tag('th'):wikitext('Tools'):done()
			:tag('td'):css({ ['text-align'] = 'center' }):wikitext(toolImgs):done()
			:tag('th'):wikitext('Facilities'):done()
			:tag('td'):css({ ['text-align'] = 'center' }):wikitext(facilityLnks):done()
	end
	
-- END OF REQUIREMENTS TABLE
----------------------------------------------------------------------------

----------------------------------------------------------------------------
-- START OF MATERIALS AND PRODUCTS TABLE
	-- Contains materials (item, qty, cost), total cost, products, and profit

	-- All rows to be in the materials and products table should be appended to materialsTable
	local materialsTable = mw.html.create('table')
			:addClass('wikitable align-center-1 align-right-3 align-right-4')
			:css({ width = '100%',
				['margin-top'] = '0' })	

	materialsTable:tag('caption'):wikitext("Materials"):done()

	-- Table header
	materialsTable:tag('tr')
		:tag('th'):attr('colspan', 2):wikitext('Item'):done()
		:tag('th'):wikitext('Quantity'):done()
		:tag('th'):wikitext('Cost'):done()

	-- Materials
	local material_rows, material_costs, has_material_ref_tag = generate_rows(frame, materials)
	for _, row in ipairs(material_rows) do
		materialsTable:node(row)
	end

	-- Total cost
	local total_cost_row = generate_total_cost_row(materials, material_costs)
	materialsTable:node(total_cost_row)
	
	-- Products
	local output_rows, output_cost, has_output_ref_tag = generate_rows(frame, output)
	for _, row in ipairs(output_rows) do
		materialsTable:node(row)
	end

	-- Profit
	local profit_row, has_profit_ref_tag
	if output_cost['Coins'] > 0 then
		profit_row, has_profit_ref_tag = generate_profit_row(frame, ticks, material_costs['Coins'], output_cost['Coins'])
		materialsTable:node(profit_row)
	end
	
-- END OF MATERIALS AND PRODUCTS TABLE
----------------------------------------------------------------------------

	-- Append the two tables a parent div
	local parent = mw.html.create('div')
			:addClass('recipe-table')
			:cssText('width:-moz-fit-content;width:fit-content;')
	parent:node(requirementsTable)
	parent:node(materialsTable)
	
	-- Set smw stuff
	if useSmw then
		-- See the comment around line 484 (near the members and ticks row)
		-- The members lua var now is one of true, false, or '' to avoid ambiguity around string values 'Yes' and 'No', and a not obvious possibility of the value ''
		-- Because string values are expected by smw, we convert the boolean values back to strings here
		members = members and 'Yes' or members == false and 'No' or ''
		local jsonObject = {skills = skills, members = members, materials = {}, output = output[1], ticks = ticks, facilities = facilities, tools = tools, category = args.category }
		local materialNames = {}
		for _, v in ipairs(materials) do
			table.insert(jsonObject.materials, {name = v.name, quantity = v.quantity})
			table.insert(materialNames, v.name)
		end
		local smwmap = {
			['Uses material'] = materialNames,
			['Uses tool'] = mw.text.split(tools or '', '%s*,%s*'),
			['Uses facility'] = mw.text.split(facilities or '', '%s*,%s*'),
			['Is members only'] = members,
			['RecipeCategory'] = args.category,
			['Production JSON'] = mw.text.jsonEncode(jsonObject),
			['Is boostable'] = {},
			['Uses skill'] = {},
		}
		for i, v in pairs(smwmap) do
			-- trim off any {{!}}foo
			if type(v) == 'table' then
				for j, w in ipairs(v) do
					v[j] = mw.text.split(w, '|')[1]
				end
			end
		end
		for _, s in ipairs(skills) do
			smwmap[s.name..' level'] = tonumber(s.level)
			smwmap[s.name..' experience'] = tonumber(s.experience)
			table.insert(smwmap['Uses skill'], s.name)
			if yesno(s.boostable, false) then
				table.insert(smwmap['Is boostable'], s.name)
			end
		end

		mw.smw.set(smwmap)
	end

	-- If there are any ref tags, add a Reflist section
	local outro = ''
	local has_ref_tag = has_ticks_ref_tag or has_material_ref_tag or has_output_ref_tag or has_profit_ref_tag
	if has_ref_tag then
		outro = '<div class="reflist">\n' .. frame:extensionTag{ name='references', args = { group='r' } } .. '</div>'
	end

	-- Return div with tables + categories + reflist
	return tostring(parent) .. categories(args, skills, unknown_boostable_flag) .. outro
end

function categories(args, skills, unknown_boostable_flag)
	if not onmain() then
		return ''
	end
	local cats = {}
	
	if unknown_boostable_flag then
		table.insert(cats, '[[Category:Recipes missing boostable]]')
	end
	
	local missinglvl = false
	local missingexp = false
	local nonnumexp = false
	for i, s in ipairs(skills) do
		if s.name then
			table.insert(cats, string.format('[[Category:%s]]', s.name))	
		end
		if string.find(s.level, '?') then
			missinglvl = true
		end
		if string.find(s.experience, '?') then
			missingexp = true
		elseif tonumber(s.experience) == nil then
			nonnumexp = true
		end
	end
	if missinglvl then
		table.insert(cats, '[[Category:Missing skill info values]]')
	end
	if missingexp then
		table.insert(cats, '[[Category:Needs experience info]]')
	end
	if nonnumexp then
		table.insert(cats, '[[Category:Pages with non-numeric experience quantity]]')
	end

	if (args.ticks or '') == '' then
		table.insert(cats, '[[Category:Recipes missing ticks]]')
	end

	if args.tools ~= nil then
		table.insert(cats, '[[Category:Recipes that require a tool]]')
	end
	
	if args.facilities ~= nil then
		table.insert(cats, '[[Category:Recipes that use a facility]]')
	end

	return table.concat(cats,'')
end

return p