Module:Item recipe: Difference between revisions
From Space Station 14 Wiki
(improved override lookups logic, related error messages) |
(updated due to deps on Module:Item and its rework) |
||
Line 19: | Line 19: | ||
-- whereas the actual power cell item has ID `PowerCellSmall`. | -- 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, | -- 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` | -- we first would need to define a mapping from the recipe product `PowerCellSmallPrinted` | ||
-- to the actual item `PowerCellSmall`. | -- to the actual item `PowerCellSmall`. | ||
-- | -- | ||
-- After that is done, lookups by small power cell or its id will return corresponding recipe. | -- After that is done, lookups by small power cell or its id will return 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 38: | Line 38: | ||
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 table_has_value(tab, val) | local function table_has_value(tab, val) | ||
for _, value in ipairs(tab) do | |||
if value == val then | |||
return true | |||
end | |||
end | |||
return false | |||
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 73: | Line 73: | ||
-- * 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 | end | ||
-- ==================== | -- ==================== | ||
-- Searchs for recipe product in the product overrides table that maps to a an item with ID `item_id`. | -- Searchs for recipe product in the product overrides table that maps to a an item with ID `item_id`. | ||
-- | -- | ||
-- For instance, for an override that maps recipe product `PowerCellSmallPrinted` to item ID `PowerCellSmall`, | -- For instance, for an override that maps recipe product `PowerCellSmallPrinted` to item ID `PowerCellSmall`, | ||
-- this functions would return recipe product `PowerCellSmallPrinted` if given the item ID `PowerCellSmall`. | -- this functions would return recipe product `PowerCellSmallPrinted` if given the item ID `PowerCellSmall`. | ||
-- | -- | ||
-- Returns `nil` if no match was found or `fallback_recipe_product`, if it was given. | -- Returns `nil` if no match was found or `fallback_recipe_product`, if it was given. | ||
local function try_lookup_override_recipe_product_by_item_id(item_id, fallback_recipe_product) | local function try_lookup_override_recipe_product_by_item_id(item_id, fallback_recipe_product) | ||
local match = find_first_table_item_key_matching_condition( | |||
product_overrides, | |||
function(_, value) return value == item_id end | |||
) | |||
if match then | |||
return match | |||
elseif fallback_recipe_product then | |||
return fallback_recipe_product | |||
else | |||
return nil | |||
end | |||
end | end | ||
-- Searchs for an item ID in in the product overrides table that is mapped to from a product ID `product_item_id`. | -- Searchs for an item ID in in the product overrides table that is mapped to from a product ID `product_item_id`. | ||
-- | -- | ||
-- For instance, for an override that maps recipe product `PowerCellSmallPrinted` to item ID `PowerCellSmall`, | -- For instance, for an override that maps recipe product `PowerCellSmallPrinted` to item ID `PowerCellSmall`, | ||
-- this functions would return item ID `PowerCellSmall` based on recipe product `PowerCellSmallPrinted`. | -- this functions would return item ID `PowerCellSmall` based on recipe product `PowerCellSmallPrinted`. | ||
-- | -- | ||
-- Returns `nil` if no match was found or `fallback_item_id`, if it was given. | -- Returns `nil` if no match was found or `fallback_item_id`, if it was given. | ||
local function try_lookup_item_id_by_override_recipe_product(product_item_id, fallback_item_id) | local function try_lookup_item_id_by_override_recipe_product(product_item_id, fallback_item_id) | ||
local match = product_overrides[product_item_id] | |||
if match then | |||
return match | |||
elseif fallback_item_id then | |||
return fallback_item_id | |||
else | |||
return nil | |||
end | |||
end | end | ||
Line 148: | Line 148: | ||
-- Includes items defined in the products override table. | -- Includes items defined in the products override table. | ||
local function assert_product_exists(product, skip_product_override_lookup) | local function assert_product_exists(product, skip_product_override_lookup) | ||
if not skip_product_override_lookup then | |||
product = try_lookup_item_id_by_override_recipe_product(product, product) | |||
end | |||
if not itemModule.item_exists(product) then | |||
error("no recipe was found for product item ID '" .. | |||
product .. | |||
"' (product lookup: '" .. | |||
item_id .. | |||
"'). Make sure that the product item exists with that name/ID or an override is defined in the item recipe module product overrides in Module:Item recipe") | |||
end | |||
end | end | ||
Line 163: | Line 167: | ||
-- Returns table with `production_method` and `recipe`, or `nil` if no recipe was 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) | 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(recipes_by_method) do | |||
table.insert(methods_to_lookup, recipe_group_method) | |||
end | |||
else | |||
-- method specified = only look through recipes with that production method | |||
-- lookup the proper name for the method, which is gonne be just another item ID | |||
table.insert(methods_to_lookup, itemModule.lookup_item_id(production_method)) | |||
end | |||
-- lookup a product id based on existing overrides | |||
-- if there's none, use the same item ID | |||
item_id = try_lookup_override_recipe_product_by_item_id(item_id, item_id) | |||
-- do the search | |||
for _, production_method in ipairs(methods_to_lookup) do | |||
local match = recipes_by_method[production_method][item_id] | |||
if match then | |||
return { | |||
production_method = production_method, | |||
recipe = match | |||
} | |||
end | |||
end | |||
end | end | ||
Line 197: | Line 201: | ||
-- if the config doesn't have the queried item. | -- if the config doesn't have the queried item. | ||
local function lookup_order_of_material(item_id) | local function lookup_order_of_material(item_id) | ||
assert_value_not_nil(item_id, "failed to lookup order of material: item ID was not provided") | |||
return order_of_materials[item_id] | |||
end | end | ||
Line 205: | Line 209: | ||
-- 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 247: | Line 251: | ||
-- 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 or ID. Required. | |||
local item = args[1] | |||
assert_value_not_nil(item, "failed to generate a recipe for item: item was not provided") | |||
-- [OPTIONAL] | |||
-- Amount of item. Default is 1. | |||
-- Must be a string since Module:Item uses string amount. | |||
-- All values from templates come as strings. | |||
local amount = nil_or(args[2], "1") | |||
-- Item production method. Can be "nil", in which case it's looked up. | |||
local method = args[3] | |||
-- Whether to only generate a materials block. | |||
local materials_only = yesNo(args["materials only"] or args["mat only"] or false) | |||
-- Layout of materials in materials only mode. | |||
local materials_only_layout = args["materials only layout"] or args["mat only layout"] or "vertical" | |||
-- ============ | |||
-- first, try to lookup the item ID in case the given item as a overriden product (for some reason). | |||
-- fallback to the same item in case there's no overrides. | |||
item = try_lookup_item_id_by_override_recipe_product(item, item) | |||
-- then, check that the item exists. | |||
-- at this point, it can either a name or an ID. | |||
-- skip product override lookup with an extra `true` param. | |||
-- in case items doesn't exist, this will give a pretty error message. | |||
assert_product_exists(item, true) | |||
-- finally, get the item ID. | |||
-- this is done after the assertion for purposes of a pretty error message in case a lookup fails. | |||
-- here we are guaranteed to have an item. | |||
local item_id = itemModule.lookup_item_id(item) | |||
-- for a recipe using the resulting item ID. | |||
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 ID '" .. | |||
item_id .. | |||
"' (given item name: '" .. | |||
item .. | |||
"'; method: '" .. | |||
(method or "nil") .. | |||
"'). Make sure a recipe exists for this item or define a product override for an existing recipe in Module:Item recipe") | |||
local recipe = recipe_lookup_result.recipe | |||
local method = recipe_lookup_result.production_method | |||
local recipe_el = mw.html.create("div") | |||
:addClass("item-recipe") | |||
if materials_only_layout == "vertical" or materials_only_layout == "ver" then | |||
recipe_el:addClass("item-recipe-materials-layout-vertical") | |||
else | |||
recipe_el:addClass("item-recipe-materials-layout-horizontal") | |||
end | |||
local body_el = mw.html.create("div") | |||
:addClass('item-recipe-body') | |||
:css('background', 'linear-gradient(135deg, var(--bg-color-light-x2), var(--bg-color-light))') | |||
local materials_el = mw.html.create("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 .. ")") | |||
-- get a list of materials (item ids) | |||
local materials_item_ids = {} | |||
for item_id in pairs(recipe.materials) do | |||
table.insert(materials_item_ids, item_id) | |||
end | |||
-- sort materials list based on material order config | |||
-- if item doesn't have an order, use a big number as its order, placing them after | |||
-- materials that have an order defined | |||
table.sort(materials_item_ids, function(first, second) | |||
return (lookup_order_of_material(first) or 999999999999) | |||
< (lookup_order_of_material(second) or 999999999999) | |||
end) | |||
for _, material in ipairs(materials_item_ids) do | |||
local cost = recipe.materials[material] | |||
if not itemModule.item_exists(material) then | |||
error("failed to generate a recipe for item ID '" .. | |||
item_id .. | |||
"' produced on '" .. | |||
method .. | |||
"': material '" .. | |||
material .. | |||
"' was not found in item registry. Make sure that the material is added to the item name overrides in Module:Item") | |||
end | |||
materials_el:node(itemModule.generate_item { material, cost }) | |||
end | |||
body_el:node(materials_el) | |||
if materials_only then | |||
recipe_el:addClass("materials-only") | |||
recipe_el:node(body_el) | |||
else | |||
local header_el = mw.html.create("div") | |||
:addClass("item-recipe-header") | |||
:css('background', 'linear-gradient(120deg, var(--bg-color-light-x3), var(--bg-color-light-x2))') | |||
recipe_el:node(header_el) | |||
local product_and_method_container_el = mw.html.create("div") | |||
:addClass("item-recipe-product-and-method-container") | |||
header_el:node(product_and_method_container_el) | |||
local product_el = itemModule.generate_item { [1] = item_id, [2] = amount, cap = true } | |||
:addClass("item-recipe-product") | |||
product_and_method_container_el:node(product_el) | |||
-- TODO: not all methods will be items, so this will eventually break. | |||
local method_el = methods_items_els_cache[method] | |||
if method_el == nil then | |||
method_el = mw.html.create("span") | |||
:addClass('item-recipe-method') | |||
:node(itemModule.generate_item { [1] = method, capitalize = true }) | |||
methods_items_els_cache[method] = method_el | |||
end | |||
product_and_method_container_el:node(method_el) | |||
local info_icons_el = mw.html.create("div") | |||
:addClass("item-recipe-info-icons") | |||
header_el:node(info_icons_el) | |||
recipe_el:node(body_el) | |||
local complete_time_el = mw.html.create("span") | |||
:addClass('item-recipe-complete-time') | |||
if recipe.completetime == 0 then | |||
complete_time_el:node("Instant") | |||
else | |||
complete_time_el:node((recipe.completetime * amount) .. " " .. "sec.") | |||
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))') | |||
assert_value_not_nil(recipe.availability, | |||
"failed to generate a recipe for item '" .. item_id .. "': recipe doesn't have its 'availability defined") | |||
-- 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 | end | ||
Line 486: | Line 502: | ||
-- 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 = recipes_by_method[itemModule.lookup_item_id(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 pairs(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 = try_lookup_item_id_by_override_recipe_product(first, first) | |||
local second_item_id = try_lookup_item_id_by_override_recipe_product(second, second) | |||
assert_product_exists(first_item_id, true) | |||
assert_product_exists(second_item_id, true) | |||
return itemModule.lookup_item_name(first_item_id) | |||
< itemModule.lookup_item_name(second_item_id) | |||
end) | |||
-- generate recipe elements | |||
for _, product in ipairs(products) do | |||
container_el:node(p.generate_item_recipe { | |||
[1] = try_lookup_item_id_by_override_recipe_product(product, product), | |||
[2] = 1, | |||
[3] = method | |||
}) | |||
end | |||
return container_el | |||
:allDone() | |||
end | end | ||
return p | return p |
Revision as of 16:49, 8 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.
-- 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')
-- A table mapping production methods to recipes that a given method can produce.
local recipes_by_method = mw.loadJsonData("Module:Item recipe/recipes/lathes.json")
-- Not all recipes produce products that you might think they do -
-- some produce their own "printed" or "empty" or other variants.
--
-- Note that these mapping must be defined manually here.
--
-- 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, lookups by small power cell or its id will return corresponding recipe.
local product_overrides = mw.loadJsonData("Module:Item recipe/product overrides.json")
local order_of_materials = mw.loadJsonData("Module:Item recipe/order of materials.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 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 recipe product in the product overrides table that maps to a an item with ID `item_id`.
--
-- For instance, for an override that maps recipe product `PowerCellSmallPrinted` to item ID `PowerCellSmall`,
-- this functions would return recipe product `PowerCellSmallPrinted` if given the item ID `PowerCellSmall`.
--
-- Returns `nil` if no match was found or `fallback_recipe_product`, if it was given.
local function try_lookup_override_recipe_product_by_item_id(item_id, fallback_recipe_product)
local match = find_first_table_item_key_matching_condition(
product_overrides,
function(_, value) return value == item_id end
)
if match then
return match
elseif fallback_recipe_product then
return fallback_recipe_product
else
return nil
end
end
-- Searchs for an item ID in in the product overrides table that is mapped to from a product ID `product_item_id`.
--
-- For instance, for an override that maps recipe product `PowerCellSmallPrinted` to item ID `PowerCellSmall`,
-- this functions would return item ID `PowerCellSmall` based on recipe product `PowerCellSmallPrinted`.
--
-- Returns `nil` if no match was found or `fallback_item_id`, if it was given.
local function try_lookup_item_id_by_override_recipe_product(product_item_id, fallback_item_id)
local match = product_overrides[product_item_id]
if match then
return match
elseif fallback_item_id then
return fallback_item_id
else
return nil
end
end
-- Asserts that a given product item exists.
-- Includes items defined in the products override table.
local function assert_product_exists(product, skip_product_override_lookup)
if not skip_product_override_lookup then
product = try_lookup_item_id_by_override_recipe_product(product, product)
end
if not itemModule.item_exists(product) then
error("no recipe was found for product item ID '" ..
product ..
"' (product lookup: '" ..
item_id ..
"'). Make sure that the product item exists with that name/ID or an override is defined in the item recipe module product overrides in Module:Item recipe")
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(recipes_by_method) do
table.insert(methods_to_lookup, recipe_group_method)
end
else
-- method specified = only look through recipes with that production method
-- lookup the proper name for the method, which is gonne be just another item ID
table.insert(methods_to_lookup, itemModule.lookup_item_id(production_method))
end
-- lookup a product id based on existing overrides
-- if there's none, use the same item ID
item_id = try_lookup_override_recipe_product_by_item_id(item_id, item_id)
-- do the search
for _, production_method in ipairs(methods_to_lookup) do
local match = recipes_by_method[production_method][item_id]
if match then
return {
production_method = production_method,
recipe = match
}
end
end
end
-- Searches a material item in the material order config, returning the order number or `nil`,
-- if the config doesn't have the queried item.
local function lookup_order_of_material(item_id)
assert_value_not_nil(item_id, "failed to lookup order of material: item ID was not provided")
return order_of_materials[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 or ID. Required.
local item = args[1]
assert_value_not_nil(item, "failed to generate a recipe for item: item was not provided")
-- [OPTIONAL]
-- Amount of item. Default is 1.
-- Must be a string since Module:Item uses string amount.
-- All values from templates come as strings.
local amount = nil_or(args[2], "1")
-- Item production method. Can be "nil", in which case it's looked up.
local method = args[3]
-- Whether to only generate a materials block.
local materials_only = yesNo(args["materials only"] or args["mat only"] or false)
-- Layout of materials in materials only mode.
local materials_only_layout = args["materials only layout"] or args["mat only layout"] or "vertical"
-- ============
-- first, try to lookup the item ID in case the given item as a overriden product (for some reason).
-- fallback to the same item in case there's no overrides.
item = try_lookup_item_id_by_override_recipe_product(item, item)
-- then, check that the item exists.
-- at this point, it can either a name or an ID.
-- skip product override lookup with an extra `true` param.
-- in case items doesn't exist, this will give a pretty error message.
assert_product_exists(item, true)
-- finally, get the item ID.
-- this is done after the assertion for purposes of a pretty error message in case a lookup fails.
-- here we are guaranteed to have an item.
local item_id = itemModule.lookup_item_id(item)
-- for a recipe using the resulting item ID.
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 ID '" ..
item_id ..
"' (given item name: '" ..
item ..
"'; method: '" ..
(method or "nil") ..
"'). Make sure a recipe exists for this item or define a product override for an existing recipe in Module:Item recipe")
local recipe = recipe_lookup_result.recipe
local method = recipe_lookup_result.production_method
local recipe_el = mw.html.create("div")
:addClass("item-recipe")
if materials_only_layout == "vertical" or materials_only_layout == "ver" then
recipe_el:addClass("item-recipe-materials-layout-vertical")
else
recipe_el:addClass("item-recipe-materials-layout-horizontal")
end
local body_el = mw.html.create("div")
:addClass('item-recipe-body')
:css('background', 'linear-gradient(135deg, var(--bg-color-light-x2), var(--bg-color-light))')
local materials_el = mw.html.create("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 .. ")")
-- get a list of materials (item ids)
local materials_item_ids = {}
for item_id in pairs(recipe.materials) do
table.insert(materials_item_ids, item_id)
end
-- sort materials list based on material order config
-- if item doesn't have an order, use a big number as its order, placing them after
-- materials that have an order defined
table.sort(materials_item_ids, function(first, second)
return (lookup_order_of_material(first) or 999999999999)
< (lookup_order_of_material(second) or 999999999999)
end)
for _, material in ipairs(materials_item_ids) do
local cost = recipe.materials[material]
if not itemModule.item_exists(material) then
error("failed to generate a recipe for item ID '" ..
item_id ..
"' produced on '" ..
method ..
"': material '" ..
material ..
"' was not found in item registry. Make sure that the material is added to the item name overrides in Module:Item")
end
materials_el:node(itemModule.generate_item { material, cost })
end
body_el:node(materials_el)
if materials_only then
recipe_el:addClass("materials-only")
recipe_el:node(body_el)
else
local header_el = mw.html.create("div")
:addClass("item-recipe-header")
:css('background', 'linear-gradient(120deg, var(--bg-color-light-x3), var(--bg-color-light-x2))')
recipe_el:node(header_el)
local product_and_method_container_el = mw.html.create("div")
:addClass("item-recipe-product-and-method-container")
header_el:node(product_and_method_container_el)
local product_el = itemModule.generate_item { [1] = item_id, [2] = amount, cap = true }
:addClass("item-recipe-product")
product_and_method_container_el:node(product_el)
-- TODO: not all methods will be items, so this will eventually break.
local method_el = methods_items_els_cache[method]
if method_el == nil then
method_el = mw.html.create("span")
:addClass('item-recipe-method')
:node(itemModule.generate_item { [1] = method, capitalize = true })
methods_items_els_cache[method] = method_el
end
product_and_method_container_el:node(method_el)
local info_icons_el = mw.html.create("div")
:addClass("item-recipe-info-icons")
header_el:node(info_icons_el)
recipe_el:node(body_el)
local complete_time_el = mw.html.create("span")
:addClass('item-recipe-complete-time')
if recipe.completetime == 0 then
complete_time_el:node("Instant")
else
complete_time_el:node((recipe.completetime * amount) .. " " .. "sec.")
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))')
assert_value_not_nil(recipe.availability,
"failed to generate a recipe for item '" .. item_id .. "': recipe doesn't have its 'availability defined")
-- 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 = recipes_by_method[itemModule.lookup_item_id(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 pairs(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 = try_lookup_item_id_by_override_recipe_product(first, first)
local second_item_id = try_lookup_item_id_by_override_recipe_product(second, second)
assert_product_exists(first_item_id, true)
assert_product_exists(second_item_id, true)
return itemModule.lookup_item_name(first_item_id)
< itemModule.lookup_item_name(second_item_id)
end)
-- generate recipe elements
for _, product in ipairs(products) do
container_el:node(p.generate_item_recipe {
[1] = try_lookup_item_id_by_override_recipe_product(product, product),
[2] = 1,
[3] = method
})
end
return container_el
:allDone()
end
return p