Module:Item recipe: Difference between revisions

From Space Station 14 Wiki
(updated body and materials formatting due to layout changes caused by caching materials)
(replaced "layout" option with "materials only layout" option)
Line 229: Line 229:
local method = args[3]
local method = args[3]


-- Recipe layout.
local layout = args["layout"] or args["lay"] or "horizontal"
 
-- Whether to only generate a materials block.
-- Whether to only generate a materials block.
local materials_only = yesNo(args["materials only"] or args["mat only"] or false)
local materials_only = yesNo(args["materials only"] or args["mat only"] or false)
-- Layout of materials in materials only mode.
local materials_only_layout = args["materials only layout"] or args["mat only layout"] or "vertical"


-- ============
-- ============
Line 252: Line 253:
:node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item recipe/styles.css' }))
:node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item recipe/styles.css' }))


if layout == "vertical" or layout == "ver" then
if materials_only_layout == "vertical" or materials_only_layout == "ver" then
recipe_el:addClass("item-recipe-vertical")
recipe_el:addClass("item-recipe-materials-layout-vertical")
else
else
recipe_el:addClass("item-recipe-horizontal")
recipe_el:addClass("item-recipe-materials-layout-horizontal")
end
end



Revision as of 04:39, 3 September 2024

Module documentation
View or edit this documentation (about module documentation)

Implements {{item recipe}}.

JSON files

JSON files that are updated automatically, syncing with the upstream:

Warning
Do not make changes to the above JSON files - any changes made will be erased on next update.

JSON files that are filled manually:

  • Module:Item recipe/order of materials.json - a 1 to 1 mapping of recipe materials to order at which they appear in recipes. Less number = higher order. Materials that do not have an order defined here, will appear after those that do.
  • Module:Item recipe/product overrides.json - a 1 to 1 mapping of recipe products to item IDs. Not all products are the same as item IDs they "represent", so sometimes a connection needs to be established explicitly.

-- Contains utilities for working with in-game item recipes.

-- todo: material sorting. based on alphabetical sorting? maybe at .json generation step, convert materials to an array?

local p = {} --p stands for package
local getArgs = require('Module:Arguments').getArgs
local itemModule = require('Module:Item')
local yesNo = require('Module:Yesno')

-- An array of recipe groups.
-- Keys are production methods, values are tables containing the recipes.
local recipe_groups = {
	autolathe = mw.loadJsonData("Module:Item recipe/recipes by lathe/autolathe.json"),
	protolathe = mw.loadJsonData("Module:Item recipe/recipes by lathe/protolathe.json")
}

local product_overrides = mw.loadJsonData("Module:Item recipe/product overrides.json")

local current_frame = mw:getCurrentFrame()

local methods_items_els_cache = {}

local materials_items_els_cache = {}

-- ====================


local function numeric_table_length(t)
	local count = 0
	for _ in ipairs(t) do count = count + 1 end
	return count
end

local function table_length(t)
	local count = 0
	for _ in pairs(t) do count = count + 1 end
	return count
end

local function table_has_value(tab, val)
	for _, value in ipairs(tab) do
		if value == val then
			return true
		end
	end

	return false
end

local function assert_value_not_nil(value, error_message)
	if value == nil then
		if error_message == nil then
			error("value is nil")
		else
			error(error_message)
		end
	end
end

-- Given a value, checks if it's "nil".
-- * If it's not - returns the `value`.
-- * IF it is - returns the `value_if_nil`.
local function nil_or(value, value_if_nil)
	if value == nil then
		return value_if_nil
	else
		return value
	end
end

local function find_first_numeric_table_item_matching_condition(table, condition)
	for i, item in ipairs(table) do
		if condition(item, i, table) then
			return item
		end
	end
end

local function find_first_table_item_matching_condition(table, condition)
	for key, value in pairs(table) do
		if condition(key, value, table) then
			return value
		end
	end
end

local function find_first_table_item_key_matching_condition(table, condition)
	for key, value in pairs(table) do
		if condition(key, value, table) then
			return key
		end
	end
end

-- ====================

-- Searchs for a recipe product based on item ID that a recipe should produce.
-- Not all recipes produce products that you might think they do -
-- some produce their own "printed" or "empty" or other variants.
--
-- Note that these mapping must be defined manually in the `product_overrides`.
--
-- For instance, a recipe for the small power cell produces `PowerCellSmallPrinted` item,
-- whereas the actual power cell item has ID `PowerCellSmall`.
--
-- So, to find the recipe for the small power cell, we first would need to define a mapping from
-- the recipe product `PowerCellSmallPrinted` to the actual item `PowerCellSmall`.
-- After that, this this function can be used to get the actual items based on the recipe product.
local function lookup_recipe_product_by_item_id(product_item_id)
	local match = find_first_table_item_key_matching_condition(
		product_overrides,
		function(key, value) return value == product_item_id end
	)

	if match then
		return match
	else
		return product_item_id
	end
end

-- todo desc
local function lookup_item_id_by_recipe_product(product_item_id)
	local match = product_overrides[product_item_id]

	if match then
		return match
	else
		return product_item_id
	end
end

-- Searches a recipe of a given item.
-- If `production_method` is specified, only looks through the recipes with that method,
-- otherwise searching across all methods until a match is found.
--
-- Returns table with `production_method` and `recipe`, or `nil` if no recipe was found.
local function lookup_recipe_by_item_id(item_id, production_method)
	assert_value_not_nil(item_id, "failed to lookup recipes by method and item ID: item ID was not provided")

	-- generate a list of production methods to search across
	local methods_to_lookup = {}
	if production_method == nil then
		-- no method specified = search across all methods
		for recipe_group_method, _ in pairs(recipe_groups) do
			table.insert(methods_to_lookup, recipe_group_method)
		end
	else
		-- method specified = only look through recipes with that production method
		table.insert(methods_to_lookup, production_method)
	end

	-- apply a product override if needed
	item_id = lookup_recipe_product_by_item_id(item_id)

	-- do the search
	for _, production_method in ipairs(methods_to_lookup) do
		local match = recipe_groups[production_method][item_id]
		if match then
			return {
				production_method = production_method,
				recipe = match
			}
		end
	end
end

-- Produces a "note" element used in generation of item recipes.
-- Takes in a CSS-compatible color and text content.
local function generate_note_element(color, text)
	local el = mw.html.create('span')
		:addClass("item-recipe-note")
		:node(text)

	if color then
		el:css('color', color)
	end

	return el
end

local function generate_info_icon(frame, kind)
	if kind == 'research' then
		return frame:expandTemplate {
			title = 'Tooltip',
			args = {
				"[[File:JobIconResearchDirector.png|24px|link=Research_and_Development#R&D_Tree]]",
				"This recipe is unlocked by '''research'''"
			}
		}
	elseif kind == 'emag' then
		return frame:expandTemplate {
			title = 'Tooltip',
			args = {
				"[[File:Emag.png|42px|link=Cryptographic Sequencer]]",
				"This recipe is unlocked by '''EMAG'''"
			}
		}
	elseif kind == 'progression-symbol' then
		return mw.html.create("span")
			:addClass('info-icon-progression-symbol')
			:node("↓")
	else
		error("failed to generate an info icon: unknown kind " .. kind)
	end
end

-- ====================

-- Generates a recipe element for a given item.
-- This is the main external function of this module.
function p.generate_item_recipe(frame)
	local args = getArgs(frame)

	-- [REQUIRED]

	-- Item name, alias or ID. Required.
	local item = args[1]
	assert_value_not_nil(item, "failed to generate a recipe for item: item was not provided")

	-- [OPTIONAL]

	-- Amount of item. Default is 1.
	-- Must be a string since Module:Item uses string amount.
	-- All values from templates come as strings.
	local amount = nil_or(args[2], "1")

	-- Item production method. Can be "nil", in which case it's looked up.
	local method = args[3]

	
	-- Whether to only generate a materials block.
	local materials_only = yesNo(args["materials only"] or args["mat only"] or false)

	-- Layout of materials in materials only mode.
	local materials_only_layout = args["materials only layout"] or args["mat only layout"] or "vertical"

	-- ============

	local item_id = itemModule.lookup_item_id_by_name_and_amount { [1] = item, [2] = amount }

	local recipe_lookup_result = lookup_recipe_by_item_id(item_id, method)
	assert_value_not_nil(recipe_lookup_result,
		"failed to generate a recipe for item: no recipe found for item " ..
		item_id .. " (method: " .. (method or "nil") .. ")")

	local recipe = recipe_lookup_result.recipe
	local method = recipe_lookup_result.production_method


	local recipe_el = mw.html.create("div")
		:addClass("item-recipe")
		:node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item recipe/styles.css' }))

	if materials_only_layout == "vertical" or materials_only_layout == "ver" then
		recipe_el:addClass("item-recipe-materials-layout-vertical")
	else
		recipe_el:addClass("item-recipe-materials-layout-horizontal")
	end


	local body_el = mw.html.create("div")
		:addClass('item-recipe-body')
		:css('background', 'linear-gradient(135deg, var(--bg-color-light-x2), var(--bg-color-light))')


	local materials_el = mw.html.create("table")
		:addClass("item-recipe-materials")

	assert_value_not_nil(recipe.materials,
		"failed to generate a recipe for item: no 'materials' are specified for item " ..
		item_id .. " recipe (method: " .. method .. ")")

	for material, cost in pairs(recipe.materials) do	
		material_row_el = mw.html.create('tr')

		materials_el:node(material_row_el)


		material_row_el:node(
			mw.html.create("td")
				:node(cost * amount)
		)


		local material_item_el = materials_items_els_cache[material]
		if material_item_el == nil then
			material_item_el = itemModule.generate_item { material }
			materials_items_els_cache[material] = material_item_el
		end

		material_row_el:node(
			mw.html.create("td")
				:node(material_item_el)
		)
	end

	body_el:node(materials_el)


	if materials_only then
		recipe_el:addClass("materials-only")

		recipe_el:node(body_el)
	else
		local header_el = mw.html.create("div")
			:addClass("item-recipe-header")
			:css('background', 'linear-gradient(120deg, var(--bg-color-light-x3), var(--bg-color-light-x2))')

		recipe_el:node(header_el)


		local product_and_method_container_el = mw.html.create("div")
			:addClass("item-recipe-product-and-method-container")

		header_el:node(product_and_method_container_el)


		local product_el = itemModule.generate_item { [1] = item_id, [2] = amount, cap = true }
			:addClass("item-recipe-product")

		product_and_method_container_el:node(product_el)


		-- TODO: not all methods will be items, so this will eventually break.
		local method_el = methods_items_els_cache[method]
		if method_el == nil then
			method_el = mw.html.create("span")
				:addClass('item-recipe-method')
				:node(itemModule.generate_item { [1] = method, capitalize = true })

			methods_items_els_cache[method] = method_el
		end
		
		product_and_method_container_el:node(method_el)


		local info_icons_el = mw.html.create("div")
			:addClass("item-recipe-info-icons")

		header_el:node(info_icons_el)


		recipe_el:node(body_el)


		local complete_time_el = mw.html.create("span")
			:addClass('item-recipe-complete-time')
			:node((recipe.completetime * amount) .. " " .. "sec.")

		body_el:node(complete_time_el)


		local notes_el = mw.html.create("div")
			:addClass("item-recipe-notes")
			:css('background', 'linear-gradient(40deg, var(--bg-color-light-x3), var(--bg-color-light-x2))')

		if recipe.availability then
			local is_recipe_unlocked_by_research = string.find(recipe.availability, "dynamic", 1, true) ~= nil
			local is_recipe_unlocked_by_emag = string.find(recipe.availability, "emag", 1, true) ~= nil
			local is_recipe_unlocked_by_research_and_then_emag = is_recipe_unlocked_by_research and
				is_recipe_unlocked_by_emag

			if is_recipe_unlocked_by_research_and_then_emag then
				recipe_el:addClass('item-recipe-by-research')
				recipe_el:addClass('item-recipe-by-emag')

				info_icons_el:node(generate_info_icon(current_frame, 'research'))
				info_icons_el:node(generate_info_icon(current_frame, 'progression-symbol'))
				info_icons_el:node(generate_info_icon(current_frame, 'emag'))

				notes_el:node(
					generate_note_element(
						nil,
						current_frame:preprocess(
							"'''This recipe is unlocked by <span style='color: var(--danger-color);'>[[Cryptographic Sequencer|{{item|EmagUnlimited|l=EMAG}}]]</span> after it has been <span style='color: gold;'>researched<span>'''")
					)
				)
			elseif is_recipe_unlocked_by_research then
				recipe_el:addClass('item-recipe-by-research')

				info_icons_el:node(generate_info_icon(current_frame, 'research'))

				-- if not is_recipe_unlocked_by_research_and_then_emag then
				-- 	notes_el:node(
				-- 		generate_note_element(
				-- 			"gold",
				-- 			"'''This recipe is unlocked by research'''"
				-- 		)
				-- 	)
				-- end
			else
				recipe_el:addClass('item-recipe-by-emag')

				info_icons_el:node(generate_info_icon(current_frame, 'emag'))

				-- if not is_recipe_unlocked_by_research_and_then_emag then
				-- 	notes_el:node(
				-- 		generate_note_element(
				-- 			"var(--danger-color)",
				-- 			current_frame:preprocess(
				-- 			"'''This recipe is unlocked by [[Cryptographic Sequencer|{{item|EmagUnlimited|l=EMAG}}]]'''")
				-- 		)
				-- 	)
				-- end
			end
		end

		recipe_el:node(notes_el)
	end

	return recipe_el
		:allDone()
end

-- Generates an alphabetical list of recipes (elements) for a given production method.
-- Used to list all recipes for a particular method.
-- Recipes are sorted based on their products (the items display names).
function p.generate_list_of_recipes_for_method(frame)
	local args = getArgs(frame)

	local method = args[1]
	assert_value_not_nil(method, "failed to generate a list of recipes for a method: method was not provided")

	-- Limit on how many recipes to generate
	local recipes_limit = tonumber(args.limit) or 99999999

	local recipes = recipe_groups[method]
	assert_value_not_nil(recipes, "failed to generate a list of recipes for a method: unknown method: " .. method)

	-- =======================

	local container_el = mw.html.create("div")
		:addClass("item-recipes-list")

	-- generate a list of products
	local products = {}
	local i = 1
	for product, _ in pairs(recipes) do
		table.insert(products, product)

		if i == recipes_limit then
			break
		end

		i = i + 1
	end

	-- sort the list of products alphabetically
	-- by looking up the item names and comparing them
	table.sort(products, function(first, second)
		return itemModule.lookup_item_name_by_id({ [1] = lookup_item_id_by_recipe_product(first) })
			< itemModule.lookup_item_name_by_id({ [1] = lookup_item_id_by_recipe_product(second) })
	end)

	-- generate recipe elements
	for _, product in ipairs(products) do
		container_el:node(p.generate_item_recipe {
			[1] = lookup_item_id_by_recipe_product(product),
			[2] = 1,
			[3] = method
		})
	end

	return container_el
		:allDone()
end

return p