Module:Item recipe: Difference between revisions

From Space Station 14 Wiki
(wip)
(rework)
Line 9: Line 9:


-- An array of recipe groups.
-- An array of recipe groups.
-- Recipes are grouped by `method`.
-- Keys are production methods, values are tables containing the recipes.
-- Note that the order matters for perfomance - the recipe lookup happens from top to bottom.
local recipe_groups = {
local recipe_groups = {
{ method = "autolathe", recipes = mw.loadJsonData("Module:Item recipe/recipes by lathe/autolathe.json") },
autolathe = mw.loadJsonData("Module:Item recipe/recipes by lathe/autolathe.json"),
{ method = "protolathe", recipes = mw.loadJsonData("Module:Item recipe/recipes by lathe/protolathe.json") },
protolathe = mw.loadJsonData("Module:Item recipe/recipes by lathe/protolathe.json")
}
}


-- A table mapping overriding some recipes' products.
local product_overrides = mw.loadJsonData("Module:Item recipe/product overrides.json")
-- Keys are the recipe products, values are the new products for these recipes.
-- It's advised to use item IDs for values, as they offer much more perfomance.
--
-- Used for recipes which use separate items for produced items,
-- which are the same or almost the same as regular variants.
-- This lets the system know the proper items to display.
local recipes_products_overrides = mw.loadJsonData("Module:Item recipe/product overrides.json")
 
-- A table containing item recipes, identified by recipe IDs.
-- local recipes_by_recipe_id =
 
-- A table containing item recipe categories, identified by recipe category IDs.
-- local recipy_categories_by_recipe_category_id = mw.loadJsonData("Module:Item recipe/recipy categories by recipe category id.json")


-- ====================
-- ====================
Line 86: Line 72:


local function find_first_table_item_matching_condition(table, condition)
local function find_first_table_item_matching_condition(table, condition)
for key, item in pairs(table) do
for key, value in pairs(table) do
if condition(item, key, table) then
if condition(key, value, table) then
return item
return value
end
end
end
end
Line 94: Line 80:


local function find_first_table_item_key_matching_condition(table, condition)
local function find_first_table_item_key_matching_condition(table, condition)
for key, item in pairs(table) do
for key, value in pairs(table) do
if condition(item, key, table) then
if condition(key, value, table) then
return key
return key
end
end
Line 103: Line 89:
-- ====================
-- ====================


local function get_recipes_by_method(method)
-- Searchs for given recipe product in product override table.
local recipe_group = find_first_numeric_table_item_matching_condition(
-- If one is found, replaces it with the override product,
recipe_groups,
-- otherwise returning the same product.
function (item)
-- Used for recipes which produce items that may not be present in the items table,
return item.method == method
-- e.g. a printed variation of a power cell.
end
-- The exact use is for rendering recipe products - you can't render an unknown items, so you gotta override.
)
local function apply_product_override(product_item_id)
 
local match = product_overrides[product_item_id]
if recipe_group == nil then
if match then
error("failed to get recipes by method: no such recipe group with method " .. method)
return match
else
return product_item_id
end
end
return recipe_group.recipes
end
end


-- Given an item ID as a some recipe product, lookups it in the prduct overrides.
-- Reverse of `apply_product_override` -
-- On a match, returns a new item ID to use as a product.
-- searches for the original product based on the override product.
local function resolve_recipe_product_override_if_needed(product_item_id)
-- If none found - returns the same product.
local match_override_product = find_first_table_item_matching_condition(
-- The exact use is for searching for a recipe for an item - the recipe may have a different product specified.  
recipes_products_overrides,  
local function reverse_product_override(product_item_id)
function (override_product, recipe_product) return recipe_product == product_item_id end
local match = find_first_table_item_key_matching_condition(
product_overrides,
function (key, value) return value == product_item_id end
)
)


if match_override_product == nil then
if match then
return match
else
return product_item_id
return product_item_id
else
return match_override_product
end
end
end
end


-- Searches a recipe for a given item, optionally with a specific production method.
-- Searches a recipe of a given item.
-- @returns A table with `method` and `recipe`, or `nil` if no recipe was found.
-- If `production_method` is specified, only looks through the recipes with that method,
local function lookup_recipe_by_item_id_and_method(item_id, 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")
assert_value_not_nil(item_id, "failed to lookup recipes by method and item ID: item ID was not provided")


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


-- do the search
-- do the search
for _, method in ipairs(methods_to_lookup) do
for _, production_method in ipairs(methods_to_lookup) do
for _, recipe in ipairs(get_recipes_by_method(method)) do
local match = recipe_groups[production_method][item_id]
if resolve_recipe_product_override_if_needed(recipe.result) == item_id then
if match then
return {
return {
method = method,
production_method = production_method,
recipe = recipe
recipe = match
}
}
end
end
end
end
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 function generate_note_element(color, text)
return mw.html.create('span')
return mw.html.create('span')
Line 172: Line 164:
-- ====================
-- ====================


-- Generates a recipe element for a given item.
-- This is the main external function of this module.
function p.generate_item_recipe(frame)
function p.generate_item_recipe(frame)
local args = getArgs(frame)
local args = getArgs(frame)
Line 203: Line 197:
local item_id = itemModule.lookup_item_id_by_name_and_amount{ [1] = item, [2] = amount }
local item_id = itemModule.lookup_item_id_by_name_and_amount{ [1] = item, [2] = amount }
local recipe_lookup_result = lookup_recipe_by_item_id_and_method(item_id, method)
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") ..")")
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 recipe = recipe_lookup_result.recipe
local method = recipe_lookup_result.method
local method = recipe_lookup_result.production_method


Line 250: Line 244:
:addClass("item-recipe-notes")
:addClass("item-recipe-notes")


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


if string.find(recipe.latheRecipeType, "emag", 1, true) ~= nil then
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>]]'''")))
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
Line 281: Line 275:
end
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)
function p.generate_list_of_recipes_for_method(frame)
local args = getArgs(frame)
local args = getArgs(frame)
Line 288: Line 283:
assert_value_not_nil(method, "failed to generate a list of recipes for a method: method was not provided")
assert_value_not_nil(method, "failed to generate a list of recipes for a method: method was not provided")


local recipes = get_recipes_by_method(method)
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")
local container_el = mw.html.create("div")

Revision as of 01:59, 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 recipes which produce items that may not be present in the items table,
-- e.g. a printed variation of a power cell.
-- The exact use is for rendering recipe products - you can't render an unknown items, so you gotta override. 
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.
-- The exact use is for searching for a recipe for an item - the recipe may have a different product specified. 
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

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