Module:Item recipe: Difference between revisions

From Space Station 14 Wiki
(rework)
(rework fixes #1 + docs)
Line 92: Line 92:
-- If one is found, replaces it with the override product,
-- If one is found, replaces it with the override product,
-- otherwise returning the same product.
-- otherwise returning the same product.
-- Used for recipes which produce items that may not be present in the items table,
--
-- e.g. a printed variation of a power cell.
-- Used for resolving some recipes' products to known items instead of special variants produced with some recipes.
-- The exact use is for rendering recipe products - you can't render an unknown items, so you gotta override.  
--
-- For instance, a recipe for a small power cell produces `PowerCellSmallPrinted` item,
-- whereas the actual power cell item has ID `PowerCellSmall`.
--  
-- So, to find the recipe for this item, an override must be first defined that maps the product `PowerCellSmallPrinted`
-- to the actual item `PowerCellSmall`. This function will return the override item.
local function apply_product_override(product_item_id)
local function apply_product_override(product_item_id)
local match = product_overrides[product_item_id]
local match = product_overrides[product_item_id]
Line 107: Line 112:
-- searches for the original product based on the override product.
-- searches for the original product based on the override product.
-- If none found - returns the same product.
-- If none found - returns the same product.
-- The exact use is for searching for a recipe for an item - the recipe may have a different product specified.
--  
-- todo example
local function reverse_product_override(product_item_id)
local function reverse_product_override(product_item_id)
local match = find_first_table_item_key_matching_condition(
local match = find_first_table_item_key_matching_condition(
Line 140: Line 146:
table.insert(methods_to_lookup, production_method)
table.insert(methods_to_lookup, production_method)
end
end
-- apply a product override if needed
item_id = apply_product_override(item_id)


-- do the search
-- do the search

Revision as of 02:06, 2 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 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 given recipe product in product override table.
-- If one is found, replaces it with the override product,
-- otherwise returning the same product.
-- 
-- Used for resolving some recipes' products to known items instead of special variants produced with some recipes.
-- 
-- For instance, a recipe for a small power cell produces `PowerCellSmallPrinted` item, 
-- whereas the actual power cell item has ID `PowerCellSmall`.
-- 
-- So, to find the recipe for this item, an override must be first defined that maps the product `PowerCellSmallPrinted`
-- to the actual item `PowerCellSmall`. This function will return the override item.
local function apply_product_override(product_item_id)
	local match = product_overrides[product_item_id]
	if match then
		return match
	else
		return product_item_id
	end
end

-- Reverse of `apply_product_override` - 
-- searches for the original product based on the override product.
-- If none found - returns the same product.
-- 
-- todo example
local function reverse_product_override(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

-- 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 = apply_product_override(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)
	return mw.html.create('span')
		:addClass("item-recipe-note")
		:node(text)
		:css('color', color)
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]

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

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

	local current_frame = mw:getCurrentFrame()

	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/styles.css' }))
	
	if layout == "vertical" or layout == "ver" then
		recipe_el:addClass("item-recipe-vertical")
	else
		recipe_el:addClass("item-recipe-horizontal")
	end

	if materials_only then
		recipe_el:addClass("materials-only")
	end
		
	if not materials_only then
		local product_el = mw.html.create("div")
			:addClass("item-recipe-product")

		product_el:node(itemModule.generate_item{ [1] = item_id, [2] = amount, cap = true })
	
		recipe_el:node(product_el)

	
		local method_el = mw.html.create("div")
			:addClass("item-recipe-method")

			:node(mw.html.create("span"):addClass('recipe-supplementary-text'):node('is made on '))
			
			-- TODO: not all methods will be items, so this will eventually break.
			:node(itemModule.generate_item{ [1] = method })
			
			:node(mw.html.create("span"):addClass('recipe-supplementary-text'):node(' with'))
		

		recipe_el:node(method_el)


		local notes_el = mw.html.create("div")
			:addClass("item-recipe-notes")

		if recipe.availability then
		 	if string.find(recipe.availability, "dynamic", 1, true) ~= nil then
				notes_el:node(generate_note_element("gold", "'''This recipe must be researched first'''"))
			end

			if string.find(recipe.availability, "emag", 1, true) ~= nil then
				notes_el:node(generate_note_element("var(--link-color-visited)", current_frame:preprocess("'''This recipe is only available by [[Cryptographic Sequencer|<u>{{item|emag|l=EMAG}}</u>]]'''")))
			end
		end

		recipe_el:node(notes_el)
	end


	local materials_el = mw.html.create("div")
		: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
		materials_el:node(itemModule.generate_item{ [1] = material, [2] = cost * amount })
	end

	recipe_el:node(materials_el)


	return recipe_el
		:allDone()

end

-- Generates a list of recipe elements for a given production method.
-- Used to list all recipes for a particular method.
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")

	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")
		:css("display", "flex")
		:css("flex-diretion", "column")

	for _, recipe in ipairs(recipes) do
		container_el:node(p.generate_item_recipe{ [1] = recipe.result, [2] = 1, [3] = method })
	end

	return container_el
		:allDone()
end


return p