Module:Item recipe: Difference between revisions
From Space Station 14 Wiki
(stopped template styles from being added into every item instance - instead only once per page) |
(removed custom reagents handlers in favor of default logic; fixed a bug where lookup_recipe_ids_by_product_id() wasn't always returning an array; itty-bitty refactor) |
||
(18 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
-- Contains utilities for working with in-game item recipes. | -- Contains utilities for working with in-game item recipes. | ||
local p = {} --p stands for package | local p = {} --p stands for package | ||
Line 8: | Line 6: | ||
local yesNo = require('Module:Yesno') | local yesNo = require('Module:Yesno') | ||
-- | -- A table mapping recipe IDs to recipes. | ||
-- | local recipes_by_recipe_ids = mw.loadJsonData("Module:Item recipe/recipes by recipe IDs.json") | ||
local | |||
-- A table mapping product IDs to recipe IDs. | |||
-- A product ID can have either a single recipe ID mapped to it, or multiple if there's are multiple recipes. | |||
local recipe_ids_by_product_ids = mw.loadJsonData("Module:Item recipe/recipe IDs by product IDs.json") | |||
-- A table mapping production methods to recipe IDs, with a intermediate mapping by availability. | |||
local recipe_ids_by_method_and_availability = mw.loadJsonData( | |||
"Module:Item recipe/recipe IDs by method and availability.json") | |||
-- A table mapping material IDs (item IDs) to their display order. | |||
-- Order is just a number. Materials with lesser order number will appear first. | |||
local materials_order_by_material_ids = mw.loadJsonData("Module:Item recipe/order of materials.json") | |||
-- A table remapping product IDs. | |||
-- | |||
-- Not all recipes produce products that you might think they do - | |||
-- some produce their own "printed" or "empty" or other variants. | |||
-- | |||
-- For instance, a recipe for the small power cell produces `PowerCellSmallPrinted` item, | |||
-- whereas the actual power cell item has ID `PowerCellSmall`. | |||
-- | |||
-- So, for the module functions to be able 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 is done, a lookup for small power cell (or its id) will return the corresponding recipe. | |||
local product_overrides = mw.loadJsonData("Module:Item recipe/product overrides.json") | local product_overrides = mw.loadJsonData("Module:Item recipe/product overrides.json") | ||
Line 20: | Line 39: | ||
local methods_items_els_cache = {} | local methods_items_els_cache = {} | ||
local was_template_styles_tag_el_added = false | local was_template_styles_tag_el_added = false | ||
Line 29: | Line 46: | ||
local function numeric_table_length(t) | local function numeric_table_length(t) | ||
local count = 0 | |||
for _ in ipairs(t) do count = count + 1 end | |||
return count | |||
end | end | ||
local function table_length(t) | local function table_length(t) | ||
local count = 0 | |||
for _ in pairs(t) do count = count + 1 end | |||
return count | |||
end | end | ||
local function | local function numeric_table_has_value(tab, val) | ||
for _, value in ipairs(tab) do | |||
if value == val then | |||
return true | |||
end | |||
end | |||
return false | |||
end | |||
local function filter_table(tab, predicate) | |||
local tab_filtered = {} | |||
for key, value in pairs(tab) do | |||
if predicate(key, value) then | |||
tab_filtered[key] = value | |||
end | |||
end | |||
return tab_filtered | |||
end | end | ||
local function assert_value_not_nil(value, error_message) | 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 | end | ||
Line 64: | Line 92: | ||
-- * IF it is - returns the `value_if_nil`. | -- * IF it is - returns the `value_if_nil`. | ||
local function nil_or(value, value_if_nil) | local function nil_or(value, value_if_nil) | ||
if value == nil then | |||
return value_if_nil | |||
else | |||
return value | |||
end | |||
end | end | ||
local function find_first_numeric_table_item_matching_condition(table, condition) | 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 | end | ||
local function find_first_table_item_matching_condition(table, condition) | 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 | end | ||
local function find_first_table_item_key_matching_condition(table, condition) | 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 | |||
local function map_numeric_table(tbl, f) | |||
local t = {} | |||
for i, v in ipairs(tbl) do | |||
t[i] = f(v, i) | |||
end | |||
return t | |||
end | |||
local function map_table(tbl, f) | |||
local t = {} | |||
for k, v in pairs(tbl) do | |||
t[k] = f(k, v) | |||
end | |||
return t | |||
end | |||
local function passthrough_assert_true(value, valueToReturnIfTrue, errorMessageOnFalse) | |||
if value then | |||
return valueToReturnIfTrue | |||
else | |||
error(errorMessageOnFalse) | |||
end | |||
end | |||
local function ternary_strict(valueToCheck, valueIfTrue, valueIfFalse) | |||
if valueToCheck == true then | |||
return valueIfTrue | |||
else | |||
return valueIfFalse | |||
end | |||
end | |||
-- formats seconds to a string `X min. Y sec.`. | |||
-- | |||
-- - `X min.` part is omitted if there's less than a minute. | |||
-- - `Y sec.` part is omitter if there's no seconds left. | |||
local function format_seconds_to_short_string(input_seconds) | |||
local minutes = math.floor(input_seconds / 60) | |||
local seconds = input_seconds - minutes * 60 | |||
local minutes_part = ternary_strict(minutes > 0, minutes .. " min.", nil) | |||
local seconds_part = ternary_strict(seconds > 0, seconds .. " sec.", nil) | |||
if minutes_part ~= nil and seconds_part ~= nil then | |||
return minutes_part .. " " .. seconds_part | |||
elseif seconds_part ~= nil then | |||
return seconds_part | |||
elseif minutes_part ~= nil then | |||
return minutes_part | |||
else | |||
return '' | |||
end | |||
end | end | ||
-- ==================== | -- ==================== | ||
-- | -- ###################### | ||
-- | -- ### METHOD LOOKUPS ### | ||
-- | -- ###################### | ||
-- Lookups recipe IDs by production method `production_method`. | |||
-- | |||
-- Returns recipe IDs grouped by availability. | |||
-- | |||
-- Raises an error if no recipe IDs were found by the production method. | |||
-- Set `no_error` to `true` to return `nil` instead. | |||
function p.lookup_recipe_ids_and_availability_by_method(production_method, no_error) | |||
assert_value_not_nil(production_method) | |||
local method_item_id = p.lookup_method_item_id(production_method) | |||
return recipe_ids_by_method_and_availability[method_item_id] | |||
or passthrough_assert_true( | |||
no_error, | |||
nil, | |||
"recipe IDs by production method lookup failed: no recipes by production method '" .. | |||
method_item_id .. "' (input method: '" .. production_method .. "')" | |||
) | |||
end | |||
-- Lookups production methods by recipe ID `recipe_id`. | |||
-- | |||
-- Returns an array of tables, each containg: | |||
-- - `method` - production method. | |||
-- - `availability` - availability for the recipe for this production method. | |||
-- | |||
-- **NOTE:** This is an expensive function. | |||
function p.lookup_methods_with_availability_by_recipe_id(inpit_recipe_id) | |||
local result = {} | |||
for method, recipe_ids_grouped in pairs(recipe_ids_by_method_and_availability) do | |||
for availability, recipe_ids in pairs(recipe_ids_grouped) do | |||
if numeric_table_has_value(recipe_ids, inpit_recipe_id) then | |||
table.insert(result, { | |||
method = method, | |||
availability = availability | |||
}) | |||
end | |||
end | |||
end | |||
return result | |||
end | |||
-- Checks whether a production method `production_method` exists. | |||
-- Any casing is allowed. | |||
function p.method_exists(production_method) | |||
assert_value_not_nil(production_method) | |||
return p.lookup_recipe_ids_and_availability_by_method(production_method, true) ~= nil | |||
end | |||
-- Asserts that a production method `production_method` exists. | |||
-- Any casing is allowed. | |||
function p.assert_method_exists(production_method, custom_message) | |||
assert_value_not_nil(production_method) | |||
if not p.method_exists(production_method) then | |||
if custom_message then | |||
error("production method exists assertion failed for method '" .. | |||
production_method .. "': " .. custom_message) | |||
else | |||
error("production method exists assertion failed for method '" .. | |||
production_method .. "': production method is not defined") | |||
end | |||
end | |||
end | |||
-- Lookups method item ID. | |||
-- | |||
-- Raises an error if no item ID was found. | |||
-- Set `no_error` to `true` to return `nil` instead. | |||
-- | |||
-- todo this will break in the future | |||
function p.lookup_method_item_id(method, no_error) | |||
assert_value_not_nil(method) | |||
return itemModule.lookup_item_id(method, true) | |||
or passthrough_assert_true( | |||
no_error, | |||
nil, | |||
"production method item ID lookup failed: no item ID was found for method '" .. | |||
method .. "'" | |||
) | |||
end | |||
-- ################################ | |||
-- ### PRODUCT OVERRIDE LOOKUPS ### | |||
-- ################################ | |||
-- Lookups an override for product ID `product_id`. | |||
-- | |||
-- For instance, if there was an override `PowerCellSmallPrinted` that maps to to item ID `PowerCellSmall`, | |||
-- this functions would return `PowerCellSmall` if `product_id` was `PowerCellSmallPrinted`. | |||
-- | |||
-- Raises an error if no match was found. Set `no_error` to `true` to return `nil` instead. | |||
function p.lookup_product_id_override(product_id, no_error) | |||
assert_value_not_nil(product_id) | |||
return product_overrides[product_id] | |||
or passthrough_assert_true( | |||
no_error, | |||
nil, | |||
"product override lookup failed: no override was found with product ID '" .. | |||
product_id .. "'" | |||
) | |||
end | |||
-- Lookups the prodct ID that was overriden with item ID `item_id`. | |||
-- | |||
-- For instance, if there was an override `PowerCellSmallPrinted` that maps to to item ID `PowerCellSmall`, | |||
-- this functions would return `PowerCellSmallPrinted` if `item_id` was `PowerCellSmall`. | |||
-- | |||
-- **NOTE:** This is an expensive function. | |||
-- | |||
-- Raises an error if no match was found. Set `no_error` to `true` to return `nil` instead. | |||
function p.reverse_lookup_product_id_override(item_id, no_error) | |||
assert_value_not_nil(item_id) | |||
return find_first_table_item_key_matching_condition( | |||
product_overrides, | |||
function(_, value) return value == item_id end | |||
) | |||
or passthrough_assert_true( | |||
no_error, | |||
nil, | |||
"reverse product override lookup failed: no override was found that maps to item ID '" .. | |||
item_id .. "'" | |||
) | |||
end | |||
-- ####################### | |||
-- ### PRODUCT LOOKUPS ### | |||
-- ####################### | |||
-- Checks whether a an item `product` exists. | |||
-- Takes into account that `product` can have a product override. | |||
function p.product_exsits(product) | |||
assert_value_not_nil(product) | |||
local product_with_override = p.lookup_product_id_override(product, true) | |||
or product | |||
return itemModule.item_exists(product_with_override) | |||
end | |||
-- Asserts that a product `product` exists. | |||
-- `product` can be a product ID (including overriden ones), item ID or name. | |||
function p.assert_product_exists(product, custom_message) | |||
assert_value_not_nil(product) | |||
if not p.product_exsits(product) then | |||
if custom_message then | |||
error("product exist assertion failed for product '" .. product .. "': " .. custom_message) | |||
else | |||
error("product exist assertion failed for product '" .. | |||
product .. | |||
"': no product was found. Make sure that a recipe exists with given product or that a product override is defined in Module:Item recipe") | |||
end | |||
end | |||
end | |||
-- Lookups recipe IDs by product ID `product_id`. | |||
-- Accepts overriden `product_id`s. | |||
-- | -- | ||
-- | -- Returns an array of matches. | ||
-- | -- | ||
-- | -- **NOTE:** This is an expensive function. | ||
-- | function p.lookup_recipe_ids_by_product_id(product_id) | ||
assert_value_not_nil(product_id) | |||
local result = recipe_ids_by_product_ids[ | |||
p.reverse_lookup_product_id_override(product_id, true) | |||
or product_id | |||
] | |||
-- always returns an array | |||
if type(result) == 'string' then | |||
return { result } | |||
elseif result ~= nil then | |||
return result | |||
else | |||
return {} | |||
end | |||
end | |||
-- ###################### | |||
-- ### RECIPE LOOKUPS ### | |||
-- ###################### | |||
-- Lookups recipe by recipe ID `recipe_id`. | |||
-- | -- | ||
-- | -- Returns `nil` if no recipe was found. | ||
-- | -- Set `no_error` to `true` to return `nil` instead. | ||
function p.lookup_recipe_by_recipe_id(recipe_id, no_error) | |||
assert_value_not_nil(recipe_id) | |||
return recipes_by_recipe_ids[recipe_id] | |||
or passthrough_assert_true( | |||
no_error, | |||
nil, | |||
"failed to lookup recipe by recipe id '" .. recipe_id .. "': no recipe was found" | |||
) | |||
end | end | ||
-- | -- Asserts that a recipe ID `recipe_id` exists. | ||
-- function assert_recipe_id_exists(recipe_id) | |||
-- error("not impl") | |||
-- end | |||
-- Searches recipes by `query`. `query` can be an product ID (item ID - including overrden ones), | |||
-- name or a recipe ID. | |||
function p.search_recipes(query) | |||
assert_value_not_nil(query, "failed to lookup recipes by method and item ID: item ID was not provided") | |||
-- check if query is a recipe ID | |||
local recipe_by_recipe_id = recipes_by_recipe_ids[query] | |||
if recipe_by_recipe_id then | |||
-- if so - we got a direct match! | |||
return { recipe_by_recipe_id } | |||
end | |||
-- check if query is an item name/ID | |||
-- this is a last possibility. | |||
-- TODO add a custom errror here? | |||
local recipe_product_by_item_id_or_name = itemModule.lookup_item_id(query) | |||
local recipe_ids_by_item_id_or_name = p.lookup_recipe_ids_by_product_id(recipe_product_by_item_id_or_name) | |||
local recipes = {} | |||
if recipe_ids_by_item_id_or_name ~= nil then | |||
for _, recipe_id in ipairs(recipe_ids_by_item_id_or_name) do | |||
table.insert(recipes, p.lookup_recipe_by_recipe_id(recipe_id)) | |||
end | |||
end | |||
return recipes | |||
end | end | ||
-- | -- Lookups recipes by production method. | ||
-- | -- | ||
-- | -- Raises an error if no recipes were found by the production method. | ||
function p.lookup_recipes_by_production_method(method) | |||
assert_value_not_nil(method, "failed to lookup recipes by production method: no method was given") | |||
p.assert_method_exists(method, | |||
"failed to lookup recipes by production method: method '" .. method .. "' doesn't exist") | |||
local recipe_ids_grouped = p.lookup_recipe_ids_and_availability_by_method(method) | |||
local recipes = {} | |||
for availability, recipe_ids in pairs(recipe_ids_grouped) do | |||
for _, recipe_id in ipairs(recipe_ids) do | |||
local recipe = p.lookup_recipe_by_recipe_id(recipe_id) | |||
table.insert(recipes, recipe) | |||
end | |||
end | |||
return recipes | |||
end | end | ||
-- Filters given recipes by production method. | |||
-- Any casing is allowed for production method. | |||
-- function p.filter_recipes_by_production_method(recipes, production_method) | |||
-- p.assert_method_exists(production_method) | |||
-- return filter_table( | |||
-- p.lookup_recipe_ids_by_method(production_method), | |||
-- function(_, recipe_ids_grouped) | |||
-- return find_first_numeric_table_item_matching_condition( | |||
-- recipes, | |||
-- function(recipe) return recipe.id == recipe_ids_grouped end | |||
-- ) ~= nil | |||
-- end | |||
-- ) | |||
-- end | |||
-- ############################## | |||
-- ### MATERIAL ORDER LOOKUPS ### | |||
-- ############################## | |||
-- Searches a material item using the material order config, returning the order number or `nil`, | |||
-- if the material order config doesn't have the queried item. | |||
-- | |||
-- Takes in an item ID or name. | |||
local function try_lookup_order_of_material(material) | |||
assert_value_not_nil(material, "failed to lookup order of material: material was not provided") | |||
local item_id = itemModule.lookup_item_id(material, true) | |||
if item_id == nil then | |||
error("failed to lookup order of material: material '" .. material .. "' does not exist") | |||
end | |||
return materials_order_by_material_ids[item_id] | |||
end | |||
-- ======================= | |||
-- Produces a "note" element used in generation of item recipes. | -- Produces a "note" element used in generation of item recipes. | ||
-- Takes in a CSS-compatible color and text content. | -- Takes in a CSS-compatible color and text content. | ||
local function generate_note_element(color, text) | 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 | end | ||
local function generate_info_icon(frame, kind) | 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 | end | ||
Line 213: | Line 526: | ||
-- This is the main external function of this module. | -- 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) | |||
-- [REQUIRED] | |||
-- Item name, alias, item ID or a recipe ID. Required. | |||
local input_query = args[1] | |||
assert_value_not_nil(input_query, "failed to generate a recipe for query: query 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 input_amount = nil_or(args[2], "1") | |||
-- Item production method. Can be "nil", in which case it's looked up. | |||
local input_method = args[3] | |||
-- Whether to only generate a materials block. | |||
local input_materials_only = yesNo(args["materials only"] or args["mat only"] or false) | |||
-- Layout of materials in materials only mode. | |||
local input_materials_only_layout = args["materials only layout"] or args["mat only layout"] or "vertical" | |||
-- ============ | |||
-- search recipes | |||
local recipes = p.search_recipes(input_query) | |||
local recipes_count = numeric_table_length(recipes) | |||
if recipes_count == 0 then | |||
error("failed to generate a recipe for item: no recipe was found for item '" .. | |||
input_query .. | |||
"' (input method: '" .. | |||
(input_method or "nil") .. | |||
"'). Make sure a recipe exists for this item or define a product override for an existing recipe in Module:Item recipe") | |||
elseif recipes_count > 1 then | |||
error("failed to generate a recipe for item: found multiple recipes for item '" .. | |||
input_query .. | |||
"' (input method: '" .. | |||
(input_method or "nil") .. | |||
"'). Rendering multiple recipes is currently unsupported") | |||
end | |||
local recipe = recipes[1] | |||
-- search recipe methods | |||
local recipe_methods_lookup = p.lookup_methods_with_availability_by_recipe_id(recipe.id) | |||
local recipe_methods_count = table_length(recipe_methods_lookup) | |||
if recipe_methods_count == 0 then | |||
error("failed to generate a recipe for item: no methods were found for recipe ID '" .. | |||
recipe.id .. | |||
"' (input query: '" .. input_query .. "'; input method: '" .. | |||
(input_method or "nil") .. | |||
"'). This shouldn't usually happen because the present recipes are bound to some production methods. Probable cause: bug in the recipe generation code") | |||
elseif recipe_methods_count > 1 and input_method == nil then | |||
local methods = map_numeric_table( | |||
recipe_methods_lookup, | |||
function(match) | |||
return match.method | |||
end | |||
) | |||
error("failed to generate a recipe for item: found multiple production methods for recipe ID '" .. | |||
recipe.id .. | |||
"' (input query: '" .. | |||
input_query .. | |||
"') and input production method was NOT specified. Rendering multiple recipes is unsupported, so please specify a production method from available methods for this recipe: '" .. | |||
table.concat(methods, "', '") .. "'") | |||
end | |||
local recipe_method | |||
local recipe_availability | |||
if input_method == nil then | |||
-- if no input methods is specified, use the single available recipe method | |||
recipe_method = recipe_methods_lookup[1].method | |||
recipe_availability = recipe_methods_lookup[1].availability | |||
else | |||
-- otherwise, the number of available methods can vary, | |||
-- so filter it down to a single one based on the input method. | |||
local method_item_id = p.lookup_method_item_id(input_method) | |||
local recipe_methods_lookup_match = find_first_numeric_table_item_matching_condition( | |||
recipe_methods_lookup, | |||
function(lookup_result) | |||
return lookup_result.method == method_item_id | |||
end | |||
) | |||
if recipe_methods_lookup_match == nil then | |||
error("failed to generate a recipe for item: no production methods were found for recipe ID '" .. | |||
recipe.id .. | |||
"' (input query: '" .. | |||
input_query .. | |||
"') matching input method '" .. | |||
(input_method or "nil") .. | |||
"'. Make sure a recipe exists for this item with the specified production method or define a product override for an existing recipe with the specified production method in Module:Item recipe") | |||
end | |||
recipe_method = recipe_methods_lookup_match.method | |||
recipe_availability = recipe_methods_lookup_match.availability | |||
end | |||
-- extract products | |||
-- recipe product IDs mapped to amounts | |||
local recipe_products = {} | |||
if recipe.result then | |||
recipe_products[recipe.result] = 1 | |||
elseif recipe.resultReagents then | |||
for product_id, amount in pairs(recipe.resultReagents) do | |||
recipe_products[product_id] = amount | |||
end | |||
else | |||
error("failed to generate a recipe for item: no products were found for recipe ID '" .. | |||
recipe.id .. | |||
"' (input query: '" .. | |||
input_query .. | |||
"'). This might be due to recipe having another way to describe products") | |||
end | |||
-- generate recipe element | |||
local recipe_el = mw.html.create("div") | |||
:addClass("item-recipe") | |||
if input_materials_only_layout == "vertical" or input_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("div") | |||
:addClass("item-recipe-materials") | |||
assert_value_not_nil(recipe.materials, | |||
"failed to generate a recipe for item: no 'materials' are specified for recipe ID '" .. | |||
recipe.id .. "' (input query: '" .. | |||
input_query .. | |||
"')") | |||
-- copy list of materials, but without costs | |||
local materials = {} | |||
for material, cost in pairs(recipe.materials) do | |||
table.insert(materials, material) | |||
end | |||
-- sort materials list based on material order config. | |||
-- materials not in config will come after the ordered materials. | |||
table.sort(materials, function(first, second) | |||
return (try_lookup_order_of_material(first) or 999999999999) | |||
< (try_lookup_order_of_material(second) or 999999999999) | |||
end) | |||
-- generate materials elements in sorted order | |||
for _, material in ipairs(materials) do | |||
local cost = recipe.materials[material] | |||
local material_item_id = itemModule.lookup_item_id(material, true) | |||
if material_item_id == nil then | |||
error("failed to generate a recipe for item: material '" .. | |||
material .. | |||
"' was not found in item registry (for recipe ID '" .. | |||
recipe.id .. "'; input query: '" .. | |||
input_query .. | |||
"'). Make sure that the material is added to the item name overrides in Module:Item") | |||
end | |||
materials_el:node( | |||
itemModule.generate_item { | |||
material_item_id, | |||
cost * input_amount | |||
} | |||
) | |||
end | |||
body_el:node(materials_el) | |||
if input_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 products_el = mw.html.create("div") | |||
:addClass("item-recipe-products") | |||
product_and_method_container_el:node(products_el) | |||
for product_id, amount in pairs(recipe_products) do | |||
local product_el = itemModule.generate_item { | |||
[1] = product_id, | |||
[2] = amount * input_amount, | |||
cap = true | |||
} | |||
:addClass("item-recipe-product") | |||
products_el:node(product_el) | |||
end | |||
-- TODO: not all methods will be items, so this will eventually break. | |||
local method_el = methods_items_els_cache[recipe_method] | |||
if method_el == nil then | |||
method_el = mw.html.create("span") | |||
:addClass('item-recipe-method') | |||
:node(itemModule.generate_item { [1] = recipe_method, capitalize = true }) | |||
methods_items_els_cache[recipe_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') | |||
if recipe.completetime == 0 then | |||
complete_time_el:node("Instant") | |||
else | |||
complete_time_el:node(format_seconds_to_short_string(recipe.completetime * input_amount)) | |||
end | |||
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 is not available by default, | |||
-- generate "info icons" and notes (if needed) telling about it. | |||
if recipe_availability ~= 'static' 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 [[Cryptographic Sequencer|EMAG]] after it has been [[Research_and_Development#R&D_Tree|researched]]'''") | |||
) | |||
) | |||
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 | |||
end | -- 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 | |||
if not was_template_styles_tag_el_added then | |||
recipe_el:node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item recipe/styles.css' })) | |||
was_template_styles_tag_el_added = true | |||
end | |||
return recipe_el | |||
:allDone() | |||
end | end | ||
Line 426: | Line 860: | ||
-- Recipes are sorted based on their products (the items display names). | -- Recipes are sorted based on their products (the items display names). | ||
function p.generate_list_of_recipes_for_method(frame) | 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 = p.lookup_recipes_by_production_method(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_item_id, _ in ipairs(recipes) do | |||
-- table.insert(products, product_item_id) | |||
-- 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) | |||
-- local first_item_id = p.lookup_product_id_override(first, true) | |||
-- or first | |||
-- local second_item_id = p.lookup_product_id_override(second, true) | |||
-- or second | |||
-- p.assert_product_exists(first_item_id) | |||
-- p.assert_product_exists(second_item_id) | |||
-- return itemModule.lookup_item_name_by_item_id(first_item_id) | |||
-- < itemModule.lookup_item_name_by_item_id(second_item_id) | |||
-- end) | |||
-- -- generate recipe elements | |||
-- for _, product in ipairs(products) do | |||
-- container_el:node(p.generate_item_recipe { | |||
-- [1] = p.lookup_product_id_override(product), | |||
-- [2] = 1, | |||
-- [3] = method | |||
-- }) | |||
-- end | |||
-- generate recipe elements | |||
for _, recipe in ipairs(recipes) do | |||
container_el:node( | |||
p.generate_item_recipe { | |||
[1] = recipe.id, | |||
[2] = 1, | |||
[3] = method | |||
} | |||
) | |||
end | |||
return container_el | |||
:allDone() | |||
end | end | ||
return p | return p |
Latest revision as of 18:51, 16 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:
- Module:Item recipe/recipes by recipe IDs.json - contains 1 to 1 mapping of recipe IDs to recipes.
- Module:Item recipe/recipe IDs by product IDs.json - contains mapping of recipe products to recipe IDs that produce these...products. Can be 1 to 1, or 1 to many.
- Module:Item recipe/recipe IDs by method and availability.json - contains a mapping of recipe production methods (e.g. Autolathe) to → availability (condition under which a recipe is available for this production method) to → recipe IDs.
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.
local p = {} --p stands for package
local getArgs = require('Module:Arguments').getArgs
local itemModule = require('Module:Item')
local yesNo = require('Module:Yesno')
-- A table mapping recipe IDs to recipes.
local recipes_by_recipe_ids = mw.loadJsonData("Module:Item recipe/recipes by recipe IDs.json")
-- A table mapping product IDs to recipe IDs.
-- A product ID can have either a single recipe ID mapped to it, or multiple if there's are multiple recipes.
local recipe_ids_by_product_ids = mw.loadJsonData("Module:Item recipe/recipe IDs by product IDs.json")
-- A table mapping production methods to recipe IDs, with a intermediate mapping by availability.
local recipe_ids_by_method_and_availability = mw.loadJsonData(
"Module:Item recipe/recipe IDs by method and availability.json")
-- A table mapping material IDs (item IDs) to their display order.
-- Order is just a number. Materials with lesser order number will appear first.
local materials_order_by_material_ids = mw.loadJsonData("Module:Item recipe/order of materials.json")
-- A table remapping product IDs.
--
-- Not all recipes produce products that you might think they do -
-- some produce their own "printed" or "empty" or other variants.
--
-- For instance, a recipe for the small power cell produces `PowerCellSmallPrinted` item,
-- whereas the actual power cell item has ID `PowerCellSmall`.
--
-- So, for the module functions to be able 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 is done, a lookup for small power cell (or its id) will return the corresponding recipe.
local product_overrides = mw.loadJsonData("Module:Item recipe/product overrides.json")
local current_frame = mw:getCurrentFrame()
local methods_items_els_cache = {}
local was_template_styles_tag_el_added = false
-- ====================
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 numeric_table_has_value(tab, val)
for _, value in ipairs(tab) do
if value == val then
return true
end
end
return false
end
local function filter_table(tab, predicate)
local tab_filtered = {}
for key, value in pairs(tab) do
if predicate(key, value) then
tab_filtered[key] = value
end
end
return tab_filtered
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
local function map_numeric_table(tbl, f)
local t = {}
for i, v in ipairs(tbl) do
t[i] = f(v, i)
end
return t
end
local function map_table(tbl, f)
local t = {}
for k, v in pairs(tbl) do
t[k] = f(k, v)
end
return t
end
local function passthrough_assert_true(value, valueToReturnIfTrue, errorMessageOnFalse)
if value then
return valueToReturnIfTrue
else
error(errorMessageOnFalse)
end
end
local function ternary_strict(valueToCheck, valueIfTrue, valueIfFalse)
if valueToCheck == true then
return valueIfTrue
else
return valueIfFalse
end
end
-- formats seconds to a string `X min. Y sec.`.
--
-- - `X min.` part is omitted if there's less than a minute.
-- - `Y sec.` part is omitter if there's no seconds left.
local function format_seconds_to_short_string(input_seconds)
local minutes = math.floor(input_seconds / 60)
local seconds = input_seconds - minutes * 60
local minutes_part = ternary_strict(minutes > 0, minutes .. " min.", nil)
local seconds_part = ternary_strict(seconds > 0, seconds .. " sec.", nil)
if minutes_part ~= nil and seconds_part ~= nil then
return minutes_part .. " " .. seconds_part
elseif seconds_part ~= nil then
return seconds_part
elseif minutes_part ~= nil then
return minutes_part
else
return ''
end
end
-- ====================
-- ######################
-- ### METHOD LOOKUPS ###
-- ######################
-- Lookups recipe IDs by production method `production_method`.
--
-- Returns recipe IDs grouped by availability.
--
-- Raises an error if no recipe IDs were found by the production method.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_recipe_ids_and_availability_by_method(production_method, no_error)
assert_value_not_nil(production_method)
local method_item_id = p.lookup_method_item_id(production_method)
return recipe_ids_by_method_and_availability[method_item_id]
or passthrough_assert_true(
no_error,
nil,
"recipe IDs by production method lookup failed: no recipes by production method '" ..
method_item_id .. "' (input method: '" .. production_method .. "')"
)
end
-- Lookups production methods by recipe ID `recipe_id`.
--
-- Returns an array of tables, each containg:
-- - `method` - production method.
-- - `availability` - availability for the recipe for this production method.
--
-- **NOTE:** This is an expensive function.
function p.lookup_methods_with_availability_by_recipe_id(inpit_recipe_id)
local result = {}
for method, recipe_ids_grouped in pairs(recipe_ids_by_method_and_availability) do
for availability, recipe_ids in pairs(recipe_ids_grouped) do
if numeric_table_has_value(recipe_ids, inpit_recipe_id) then
table.insert(result, {
method = method,
availability = availability
})
end
end
end
return result
end
-- Checks whether a production method `production_method` exists.
-- Any casing is allowed.
function p.method_exists(production_method)
assert_value_not_nil(production_method)
return p.lookup_recipe_ids_and_availability_by_method(production_method, true) ~= nil
end
-- Asserts that a production method `production_method` exists.
-- Any casing is allowed.
function p.assert_method_exists(production_method, custom_message)
assert_value_not_nil(production_method)
if not p.method_exists(production_method) then
if custom_message then
error("production method exists assertion failed for method '" ..
production_method .. "': " .. custom_message)
else
error("production method exists assertion failed for method '" ..
production_method .. "': production method is not defined")
end
end
end
-- Lookups method item ID.
--
-- Raises an error if no item ID was found.
-- Set `no_error` to `true` to return `nil` instead.
--
-- todo this will break in the future
function p.lookup_method_item_id(method, no_error)
assert_value_not_nil(method)
return itemModule.lookup_item_id(method, true)
or passthrough_assert_true(
no_error,
nil,
"production method item ID lookup failed: no item ID was found for method '" ..
method .. "'"
)
end
-- ################################
-- ### PRODUCT OVERRIDE LOOKUPS ###
-- ################################
-- Lookups an override for product ID `product_id`.
--
-- For instance, if there was an override `PowerCellSmallPrinted` that maps to to item ID `PowerCellSmall`,
-- this functions would return `PowerCellSmall` if `product_id` was `PowerCellSmallPrinted`.
--
-- Raises an error if no match was found. Set `no_error` to `true` to return `nil` instead.
function p.lookup_product_id_override(product_id, no_error)
assert_value_not_nil(product_id)
return product_overrides[product_id]
or passthrough_assert_true(
no_error,
nil,
"product override lookup failed: no override was found with product ID '" ..
product_id .. "'"
)
end
-- Lookups the prodct ID that was overriden with item ID `item_id`.
--
-- For instance, if there was an override `PowerCellSmallPrinted` that maps to to item ID `PowerCellSmall`,
-- this functions would return `PowerCellSmallPrinted` if `item_id` was `PowerCellSmall`.
--
-- **NOTE:** This is an expensive function.
--
-- Raises an error if no match was found. Set `no_error` to `true` to return `nil` instead.
function p.reverse_lookup_product_id_override(item_id, no_error)
assert_value_not_nil(item_id)
return find_first_table_item_key_matching_condition(
product_overrides,
function(_, value) return value == item_id end
)
or passthrough_assert_true(
no_error,
nil,
"reverse product override lookup failed: no override was found that maps to item ID '" ..
item_id .. "'"
)
end
-- #######################
-- ### PRODUCT LOOKUPS ###
-- #######################
-- Checks whether a an item `product` exists.
-- Takes into account that `product` can have a product override.
function p.product_exsits(product)
assert_value_not_nil(product)
local product_with_override = p.lookup_product_id_override(product, true)
or product
return itemModule.item_exists(product_with_override)
end
-- Asserts that a product `product` exists.
-- `product` can be a product ID (including overriden ones), item ID or name.
function p.assert_product_exists(product, custom_message)
assert_value_not_nil(product)
if not p.product_exsits(product) then
if custom_message then
error("product exist assertion failed for product '" .. product .. "': " .. custom_message)
else
error("product exist assertion failed for product '" ..
product ..
"': no product was found. Make sure that a recipe exists with given product or that a product override is defined in Module:Item recipe")
end
end
end
-- Lookups recipe IDs by product ID `product_id`.
-- Accepts overriden `product_id`s.
--
-- Returns an array of matches.
--
-- **NOTE:** This is an expensive function.
function p.lookup_recipe_ids_by_product_id(product_id)
assert_value_not_nil(product_id)
local result = recipe_ids_by_product_ids[
p.reverse_lookup_product_id_override(product_id, true)
or product_id
]
-- always returns an array
if type(result) == 'string' then
return { result }
elseif result ~= nil then
return result
else
return {}
end
end
-- ######################
-- ### RECIPE LOOKUPS ###
-- ######################
-- Lookups recipe by recipe ID `recipe_id`.
--
-- Returns `nil` if no recipe was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_recipe_by_recipe_id(recipe_id, no_error)
assert_value_not_nil(recipe_id)
return recipes_by_recipe_ids[recipe_id]
or passthrough_assert_true(
no_error,
nil,
"failed to lookup recipe by recipe id '" .. recipe_id .. "': no recipe was found"
)
end
-- Asserts that a recipe ID `recipe_id` exists.
-- function assert_recipe_id_exists(recipe_id)
-- error("not impl")
-- end
-- Searches recipes by `query`. `query` can be an product ID (item ID - including overrden ones),
-- name or a recipe ID.
function p.search_recipes(query)
assert_value_not_nil(query, "failed to lookup recipes by method and item ID: item ID was not provided")
-- check if query is a recipe ID
local recipe_by_recipe_id = recipes_by_recipe_ids[query]
if recipe_by_recipe_id then
-- if so - we got a direct match!
return { recipe_by_recipe_id }
end
-- check if query is an item name/ID
-- this is a last possibility.
-- TODO add a custom errror here?
local recipe_product_by_item_id_or_name = itemModule.lookup_item_id(query)
local recipe_ids_by_item_id_or_name = p.lookup_recipe_ids_by_product_id(recipe_product_by_item_id_or_name)
local recipes = {}
if recipe_ids_by_item_id_or_name ~= nil then
for _, recipe_id in ipairs(recipe_ids_by_item_id_or_name) do
table.insert(recipes, p.lookup_recipe_by_recipe_id(recipe_id))
end
end
return recipes
end
-- Lookups recipes by production method.
--
-- Raises an error if no recipes were found by the production method.
function p.lookup_recipes_by_production_method(method)
assert_value_not_nil(method, "failed to lookup recipes by production method: no method was given")
p.assert_method_exists(method,
"failed to lookup recipes by production method: method '" .. method .. "' doesn't exist")
local recipe_ids_grouped = p.lookup_recipe_ids_and_availability_by_method(method)
local recipes = {}
for availability, recipe_ids in pairs(recipe_ids_grouped) do
for _, recipe_id in ipairs(recipe_ids) do
local recipe = p.lookup_recipe_by_recipe_id(recipe_id)
table.insert(recipes, recipe)
end
end
return recipes
end
-- Filters given recipes by production method.
-- Any casing is allowed for production method.
-- function p.filter_recipes_by_production_method(recipes, production_method)
-- p.assert_method_exists(production_method)
-- return filter_table(
-- p.lookup_recipe_ids_by_method(production_method),
-- function(_, recipe_ids_grouped)
-- return find_first_numeric_table_item_matching_condition(
-- recipes,
-- function(recipe) return recipe.id == recipe_ids_grouped end
-- ) ~= nil
-- end
-- )
-- end
-- ##############################
-- ### MATERIAL ORDER LOOKUPS ###
-- ##############################
-- Searches a material item using the material order config, returning the order number or `nil`,
-- if the material order config doesn't have the queried item.
--
-- Takes in an item ID or name.
local function try_lookup_order_of_material(material)
assert_value_not_nil(material, "failed to lookup order of material: material was not provided")
local item_id = itemModule.lookup_item_id(material, true)
if item_id == nil then
error("failed to lookup order of material: material '" .. material .. "' does not exist")
end
return materials_order_by_material_ids[item_id]
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, item ID or a recipe ID. Required.
local input_query = args[1]
assert_value_not_nil(input_query, "failed to generate a recipe for query: query 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 input_amount = nil_or(args[2], "1")
-- Item production method. Can be "nil", in which case it's looked up.
local input_method = args[3]
-- Whether to only generate a materials block.
local input_materials_only = yesNo(args["materials only"] or args["mat only"] or false)
-- Layout of materials in materials only mode.
local input_materials_only_layout = args["materials only layout"] or args["mat only layout"] or "vertical"
-- ============
-- search recipes
local recipes = p.search_recipes(input_query)
local recipes_count = numeric_table_length(recipes)
if recipes_count == 0 then
error("failed to generate a recipe for item: no recipe was found for item '" ..
input_query ..
"' (input method: '" ..
(input_method or "nil") ..
"'). Make sure a recipe exists for this item or define a product override for an existing recipe in Module:Item recipe")
elseif recipes_count > 1 then
error("failed to generate a recipe for item: found multiple recipes for item '" ..
input_query ..
"' (input method: '" ..
(input_method or "nil") ..
"'). Rendering multiple recipes is currently unsupported")
end
local recipe = recipes[1]
-- search recipe methods
local recipe_methods_lookup = p.lookup_methods_with_availability_by_recipe_id(recipe.id)
local recipe_methods_count = table_length(recipe_methods_lookup)
if recipe_methods_count == 0 then
error("failed to generate a recipe for item: no methods were found for recipe ID '" ..
recipe.id ..
"' (input query: '" .. input_query .. "'; input method: '" ..
(input_method or "nil") ..
"'). This shouldn't usually happen because the present recipes are bound to some production methods. Probable cause: bug in the recipe generation code")
elseif recipe_methods_count > 1 and input_method == nil then
local methods = map_numeric_table(
recipe_methods_lookup,
function(match)
return match.method
end
)
error("failed to generate a recipe for item: found multiple production methods for recipe ID '" ..
recipe.id ..
"' (input query: '" ..
input_query ..
"') and input production method was NOT specified. Rendering multiple recipes is unsupported, so please specify a production method from available methods for this recipe: '" ..
table.concat(methods, "', '") .. "'")
end
local recipe_method
local recipe_availability
if input_method == nil then
-- if no input methods is specified, use the single available recipe method
recipe_method = recipe_methods_lookup[1].method
recipe_availability = recipe_methods_lookup[1].availability
else
-- otherwise, the number of available methods can vary,
-- so filter it down to a single one based on the input method.
local method_item_id = p.lookup_method_item_id(input_method)
local recipe_methods_lookup_match = find_first_numeric_table_item_matching_condition(
recipe_methods_lookup,
function(lookup_result)
return lookup_result.method == method_item_id
end
)
if recipe_methods_lookup_match == nil then
error("failed to generate a recipe for item: no production methods were found for recipe ID '" ..
recipe.id ..
"' (input query: '" ..
input_query ..
"') matching input method '" ..
(input_method or "nil") ..
"'. Make sure a recipe exists for this item with the specified production method or define a product override for an existing recipe with the specified production method in Module:Item recipe")
end
recipe_method = recipe_methods_lookup_match.method
recipe_availability = recipe_methods_lookup_match.availability
end
-- extract products
-- recipe product IDs mapped to amounts
local recipe_products = {}
if recipe.result then
recipe_products[recipe.result] = 1
elseif recipe.resultReagents then
for product_id, amount in pairs(recipe.resultReagents) do
recipe_products[product_id] = amount
end
else
error("failed to generate a recipe for item: no products were found for recipe ID '" ..
recipe.id ..
"' (input query: '" ..
input_query ..
"'). This might be due to recipe having another way to describe products")
end
-- generate recipe element
local recipe_el = mw.html.create("div")
:addClass("item-recipe")
if input_materials_only_layout == "vertical" or input_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("div")
:addClass("item-recipe-materials")
assert_value_not_nil(recipe.materials,
"failed to generate a recipe for item: no 'materials' are specified for recipe ID '" ..
recipe.id .. "' (input query: '" ..
input_query ..
"')")
-- copy list of materials, but without costs
local materials = {}
for material, cost in pairs(recipe.materials) do
table.insert(materials, material)
end
-- sort materials list based on material order config.
-- materials not in config will come after the ordered materials.
table.sort(materials, function(first, second)
return (try_lookup_order_of_material(first) or 999999999999)
< (try_lookup_order_of_material(second) or 999999999999)
end)
-- generate materials elements in sorted order
for _, material in ipairs(materials) do
local cost = recipe.materials[material]
local material_item_id = itemModule.lookup_item_id(material, true)
if material_item_id == nil then
error("failed to generate a recipe for item: material '" ..
material ..
"' was not found in item registry (for recipe ID '" ..
recipe.id .. "'; input query: '" ..
input_query ..
"'). Make sure that the material is added to the item name overrides in Module:Item")
end
materials_el:node(
itemModule.generate_item {
material_item_id,
cost * input_amount
}
)
end
body_el:node(materials_el)
if input_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 products_el = mw.html.create("div")
:addClass("item-recipe-products")
product_and_method_container_el:node(products_el)
for product_id, amount in pairs(recipe_products) do
local product_el = itemModule.generate_item {
[1] = product_id,
[2] = amount * input_amount,
cap = true
}
:addClass("item-recipe-product")
products_el:node(product_el)
end
-- TODO: not all methods will be items, so this will eventually break.
local method_el = methods_items_els_cache[recipe_method]
if method_el == nil then
method_el = mw.html.create("span")
:addClass('item-recipe-method')
:node(itemModule.generate_item { [1] = recipe_method, capitalize = true })
methods_items_els_cache[recipe_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')
if recipe.completetime == 0 then
complete_time_el:node("Instant")
else
complete_time_el:node(format_seconds_to_short_string(recipe.completetime * input_amount))
end
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 is not available by default,
-- generate "info icons" and notes (if needed) telling about it.
if recipe_availability ~= 'static' 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 [[Cryptographic Sequencer|EMAG]] after it has been [[Research_and_Development#R&D_Tree|researched]]'''")
)
)
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
if not was_template_styles_tag_el_added then
recipe_el:node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item recipe/styles.css' }))
was_template_styles_tag_el_added = true
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 = p.lookup_recipes_by_production_method(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_item_id, _ in ipairs(recipes) do
-- table.insert(products, product_item_id)
-- 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)
-- local first_item_id = p.lookup_product_id_override(first, true)
-- or first
-- local second_item_id = p.lookup_product_id_override(second, true)
-- or second
-- p.assert_product_exists(first_item_id)
-- p.assert_product_exists(second_item_id)
-- return itemModule.lookup_item_name_by_item_id(first_item_id)
-- < itemModule.lookup_item_name_by_item_id(second_item_id)
-- end)
-- -- generate recipe elements
-- for _, product in ipairs(products) do
-- container_el:node(p.generate_item_recipe {
-- [1] = p.lookup_product_id_override(product),
-- [2] = 1,
-- [3] = method
-- })
-- end
-- generate recipe elements
for _, recipe in ipairs(recipes) do
container_el:node(
p.generate_item_recipe {
[1] = recipe.id,
[2] = 1,
[3] = method
}
)
end
return container_el
:allDone()
end
return p