Module:Item recipe: Difference between revisions
From Space Station 14 Wiki
(layout changes for info icons) |
(notes wording) |
||
Line 52: | Line 52: | ||
end | end | ||
-- Given a value, checks if it's "nil". | -- Given a value, checks if it's "nil". | ||
-- * If it's not - returns the `value`. | -- * If it's not - returns the `value`. | ||
-- * IF it is - returns the `value_if_nil`. | -- * IF it is - returns the `value_if_nil`. | ||
Line 90: | Line 90: | ||
-- Searchs for a recipe product based on item ID that a recipe should produce. | -- Searchs for a recipe product based on item ID that a recipe should produce. | ||
-- Not all recipes produce products that you might think they do - | -- Not all recipes produce products that you might think they do - | ||
-- some produce their own "printed" or "empty" or other variants. | -- some produce their own "printed" or "empty" or other variants. | ||
-- | -- | ||
-- Note that these mapping must be defined manually in the `product_overrides`. | -- Note that these mapping must be defined manually in the `product_overrides`. | ||
-- | -- | ||
-- For instance, a recipe for the small power cell produces `PowerCellSmallPrinted` item, | -- For instance, a recipe for the small power cell produces `PowerCellSmallPrinted` item, | ||
-- whereas the actual power cell item has ID `PowerCellSmall`. | -- whereas the actual power cell item has ID `PowerCellSmall`. | ||
-- | -- | ||
-- So, to find the recipe for the small power cell, we first would need to define a mapping from | -- So, to find the recipe for the small power cell, we first would need to define a mapping from | ||
-- the recipe product `PowerCellSmallPrinted` to the actual item `PowerCellSmall`. | -- the recipe product `PowerCellSmallPrinted` to the actual item `PowerCellSmall`. | ||
-- After that, this this function can be used to get the actual items based on the recipe product. | -- After that, this this function can be used to get the actual items based on the recipe product. | ||
Line 104: | Line 104: | ||
local match = find_first_table_item_key_matching_condition( | local match = find_first_table_item_key_matching_condition( | ||
product_overrides, | product_overrides, | ||
function (key, value) return value == product_item_id end | function(key, value) return value == product_item_id end | ||
) | ) | ||
Line 117: | Line 117: | ||
local function lookup_item_id_by_recipe_product(product_item_id) | local function lookup_item_id_by_recipe_product(product_item_id) | ||
local match = product_overrides[product_item_id] | local match = product_overrides[product_item_id] | ||
if match then | if match then | ||
return match | return match | ||
Line 126: | Line 126: | ||
-- Searches a recipe of a given item. | -- Searches a recipe of a given item. | ||
-- If `production_method` is specified, only looks through the recipes with that method, | -- If `production_method` is specified, only looks through the recipes with that method, | ||
-- otherwise searching across all methods until a match is found. | -- otherwise searching across all methods until a match is found. | ||
-- | -- | ||
-- 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) | ||
Line 160: | Line 160: | ||
end | 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") | :addClass("item-recipe-note") | ||
:node(text) | :node(text) | ||
:css('color', color) | |||
if color then | |||
el:css('color', color) | |||
end | |||
return el | |||
end | end | ||
Line 172: | Line 177: | ||
-- Generates a recipe element for a given item. | -- Generates a recipe element for a given item. | ||
-- 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) | local args = getArgs(frame) | ||
-- [REQUIRED] | -- [REQUIRED] | ||
-- Item name, alias or ID. Required. | -- Item name, alias or ID. Required. | ||
local item = args[1] | local item = args[1] | ||
Line 185: | Line 190: | ||
-- Amount of item. Default is 1. | -- Amount of item. Default is 1. | ||
-- Must be a string since Module:Item uses string amount. | -- Must be a string since Module:Item uses string amount. | ||
-- All values from templates come as strings. | -- All values from templates come as strings. | ||
local amount = nil_or(args[2], "1") | local amount = nil_or(args[2], "1") | ||
-- Item production method. Can be "nil", in which case it's looked up. | -- Item production method. Can be "nil", in which case it's looked up. | ||
local method = args[3] | local method = args[3] | ||
Line 194: | Line 199: | ||
-- Recipe layout. | -- Recipe layout. | ||
local layout = args["layout"] or args["lay"] or "horizontal" | local layout = args["layout"] or args["lay"] or "horizontal" | ||
-- Whether to only generate a materials block. | -- Whether to only generate a materials block. | ||
local materials_only = yesNo(args["materials only"] or args["mat only"] or false) | local materials_only = yesNo(args["materials only"] or args["mat only"] or false) | ||
Line 202: | Line 207: | ||
local current_frame = mw:getCurrentFrame() | local current_frame = mw:getCurrentFrame() | ||
local item_id = itemModule.lookup_item_id_by_name_and_amount{ [1] = item, [2] = amount } | local item_id = itemModule.lookup_item_id_by_name_and_amount { [1] = item, [2] = amount } | ||
local recipe_lookup_result = lookup_recipe_by_item_id(item_id, method) | local recipe_lookup_result = lookup_recipe_by_item_id(item_id, method) | ||
assert_value_not_nil(recipe_lookup_result, "failed to generate a recipe for item: no recipe found for item " .. item_id .. " (method: " .. (method or "nil") ..")") | assert_value_not_nil(recipe_lookup_result, | ||
"failed to generate a recipe for item: no recipe found for item " .. | |||
item_id .. " (method: " .. (method or "nil") .. ")") | |||
local recipe = recipe_lookup_result.recipe | local recipe = recipe_lookup_result.recipe | ||
local method = recipe_lookup_result.production_method | local method = recipe_lookup_result.production_method | ||
local recipe_el = mw.html.create("div") | local recipe_el = mw.html.create("div") | ||
:addClass("item-recipe") | :addClass("item-recipe") | ||
:node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item recipe/styles.css' })) | :node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item recipe/styles.css' })) | ||
if layout == "vertical" or layout == "ver" then | if layout == "vertical" or layout == "ver" then | ||
recipe_el:addClass("item-recipe-vertical") | recipe_el:addClass("item-recipe-vertical") | ||
Line 230: | Line 237: | ||
:addClass("item-recipe-materials") | :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 ..")") | assert_value_not_nil(recipe.materials, | ||
"failed to generate a recipe for item: no 'materials' are specified for item " .. | |||
item_id .. " recipe (method: " .. method .. ")") | |||
for material, cost in pairs(recipe.materials) do | for material, cost in pairs(recipe.materials) do | ||
materials_el:node(itemModule.generate_item{ [1] = material, [2] = cost * amount }) | materials_el:node(itemModule.generate_item { [1] = material, [2] = cost * amount }) | ||
end | end | ||
Line 243: | Line 252: | ||
recipe_el:node(body_el) | recipe_el:node(body_el) | ||
else | else | ||
local header_el = mw.html.create("div") | local header_el = mw.html.create("div") | ||
:addClass("item-recipe-header") | :addClass("item-recipe-header") | ||
Line 257: | Line 266: | ||
local product_el = itemModule.generate_item{ [1] = item_id, [2] = amount, cap = true } | local product_el = itemModule.generate_item { [1] = item_id, [2] = amount, cap = true } | ||
:addClass("item-recipe-product") | :addClass("item-recipe-product") | ||
product_and_method_container_el:node(product_el) | product_and_method_container_el:node(product_el) | ||
-- TODO: not all methods will be items, so this will eventually break. | -- TODO: not all methods will be items, so this will eventually break. | ||
local method_el = mw.html.create("span") | local method_el = mw.html.create("span") | ||
:addClass('item-recipe-method') | :addClass('item-recipe-method') | ||
:node(itemModule.generate_item{ [1] = method, capitalize = true }) | :node(itemModule.generate_item { [1] = method, capitalize = true }) | ||
product_and_method_container_el:node(method_el) | product_and_method_container_el:node(method_el) | ||
Line 275: | Line 284: | ||
header_el:node(info_icons_el) | header_el:node(info_icons_el) | ||
recipe_el:node(body_el) | recipe_el:node(body_el) | ||
Line 292: | Line 301: | ||
if recipe.availability then | if recipe.availability then | ||
local is_recipe_unlocked_by_research = string.find(recipe.availability, "dynamic", 1, true) ~= nil | |||
local is_recipe_unlocked_by_emag = string.find(recipe.availability, "emag", 1, true) ~= nil | |||
local is_recipe_unlocked_by_research_and_then_emag = is_recipe_unlocked_by_research and | |||
is_recipe_unlocked_by_emag | |||
if is_recipe_unlocked_by_research ~= nil then | |||
recipe_el:addClass('item-recipe-by-research') | recipe_el:addClass('item-recipe-by-research') | ||
info_icons_el:node("[[File:JobIconResearchDirector.png|24px]]") | info_icons_el:node("[[File:JobIconResearchDirector.png|24px]]") | ||
notes_el:node( | 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 | |||
end | end | ||
if | if is_recipe_unlocked_by_emag then | ||
recipe_el:addClass('item-recipe-by-emag') | recipe_el:addClass('item-recipe-by-emag') | ||
info_icons_el:node("[[File:Emag.png|42px]]") | info_icons_el:node("[[File:Emag.png|42px]]") | ||
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 | |||
if is_recipe_unlocked_by_research_and_then_emag then | |||
notes_el:node( | notes_el:node( | ||
generate_note_element( | generate_note_element( | ||
nil, | |||
current_frame:preprocess("'''This recipe is | current_frame:preprocess( | ||
"'''This recipe is unlocked by <span style='color: var(--danger-color);'>[[Cryptographic Sequencer|{{item|EmagUnlimited|l=EMAG}}]]</span> after it has been <span style='color: gold;'>researched<span>'''") | |||
) | ) | ||
) | ) | ||
Line 324: | Line 353: | ||
return recipe_el | return recipe_el | ||
:allDone() | :allDone() | ||
end | end | ||
Line 360: | Line 388: | ||
end | end | ||
-- sort the list of products alphabetically | -- sort the list of products alphabetically | ||
-- by looking up the item names and comparing them | -- by looking up the item names and comparing them | ||
table.sort(products, function (first, second) | table.sort(products, function(first, second) | ||
return itemModule.lookup_item_name_by_id({ [1] = lookup_item_id_by_recipe_product(first) }) | return itemModule.lookup_item_name_by_id({ [1] = lookup_item_id_by_recipe_product(first) }) | ||
< itemModule.lookup_item_name_by_id({ [1] = lookup_item_id_by_recipe_product(second) }) | < itemModule.lookup_item_name_by_id({ [1] = lookup_item_id_by_recipe_product(second) }) | ||
end) | end) | ||
Line 369: | Line 397: | ||
-- generate recipe elements | -- generate recipe elements | ||
for _, product in ipairs(products) do | for _, product in ipairs(products) do | ||
container_el:node(p.generate_item_recipe{ | container_el:node(p.generate_item_recipe { | ||
[1] = lookup_item_id_by_recipe_product(product), | [1] = lookup_item_id_by_recipe_product(product), | ||
[2] = 1, | [2] = 1, | ||
[3] = method | [3] = method | ||
}) | }) | ||
end | end | ||
Line 379: | Line 407: | ||
:allDone() | :allDone() | ||
end | end | ||
return p | return p |
Revision as of 14:49, 2 September 2024
Module documentation
|
---|
View or edit this documentation • (about module documentation) |
Implements {{item recipe}}.
JSON files
JSON files that are updated automatically, syncing with the upstream:
- 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')
-- An array of recipe groups.
-- Keys are production methods, values are tables containing the recipes.
local recipe_groups = {
autolathe = mw.loadJsonData("Module:Item recipe/recipes by lathe/autolathe.json"),
protolathe = mw.loadJsonData("Module:Item recipe/recipes by lathe/protolathe.json")
}
local product_overrides = mw.loadJsonData("Module:Item recipe/product overrides.json")
-- ====================
local function numeric_table_length(t)
local count = 0
for _ in ipairs(t) do count = count + 1 end
return count
end
local function table_length(t)
local count = 0
for _ in pairs(t) do count = count + 1 end
return count
end
local function table_has_value(tab, val)
for _, value in ipairs(tab) do
if value == val then
return true
end
end
return false
end
local function assert_value_not_nil(value, error_message)
if value == nil then
if error_message == nil then
error("value is nil")
else
error(error_message)
end
end
end
-- Given a value, checks if it's "nil".
-- * If it's not - returns the `value`.
-- * IF it is - returns the `value_if_nil`.
local function nil_or(value, value_if_nil)
if value == nil then
return value_if_nil
else
return value
end
end
local function find_first_numeric_table_item_matching_condition(table, condition)
for i, item in ipairs(table) do
if condition(item, i, table) then
return item
end
end
end
local function find_first_table_item_matching_condition(table, condition)
for key, value in pairs(table) do
if condition(key, value, table) then
return value
end
end
end
local function find_first_table_item_key_matching_condition(table, condition)
for key, value in pairs(table) do
if condition(key, value, table) then
return key
end
end
end
-- ====================
-- Searchs for a recipe product based on item ID that a recipe should produce.
-- Not all recipes produce products that you might think they do -
-- some produce their own "printed" or "empty" or other variants.
--
-- Note that these mapping must be defined manually in the `product_overrides`.
--
-- For instance, a recipe for the small power cell produces `PowerCellSmallPrinted` item,
-- whereas the actual power cell item has ID `PowerCellSmall`.
--
-- So, to find the recipe for the small power cell, we first would need to define a mapping from
-- the recipe product `PowerCellSmallPrinted` to the actual item `PowerCellSmall`.
-- After that, this this function can be used to get the actual items based on the recipe product.
local function lookup_recipe_product_by_item_id(product_item_id)
local match = find_first_table_item_key_matching_condition(
product_overrides,
function(key, value) return value == product_item_id end
)
if match then
return match
else
return product_item_id
end
end
-- todo desc
local function lookup_item_id_by_recipe_product(product_item_id)
local match = product_overrides[product_item_id]
if match then
return match
else
return product_item_id
end
end
-- Searches a recipe of a given item.
-- If `production_method` is specified, only looks through the recipes with that method,
-- otherwise searching across all methods until a match is found.
--
-- Returns table with `production_method` and `recipe`, or `nil` if no recipe was found.
local function lookup_recipe_by_item_id(item_id, production_method)
assert_value_not_nil(item_id, "failed to lookup recipes by method and item ID: item ID was not provided")
-- generate a list of production methods to search across
local methods_to_lookup = {}
if production_method == nil then
-- no method specified = search across all methods
for recipe_group_method, _ in pairs(recipe_groups) do
table.insert(methods_to_lookup, recipe_group_method)
end
else
-- method specified = only look through recipes with that production method
table.insert(methods_to_lookup, production_method)
end
-- apply a product override if needed
item_id = lookup_recipe_product_by_item_id(item_id)
-- do the search
for _, production_method in ipairs(methods_to_lookup) do
local match = recipe_groups[production_method][item_id]
if match then
return {
production_method = production_method,
recipe = match
}
end
end
end
-- Produces a "note" element used in generation of item recipes.
-- Takes in a CSS-compatible color and text content.
local function generate_note_element(color, text)
local el = mw.html.create('span')
:addClass("item-recipe-note")
:node(text)
if color then
el:css('color', color)
end
return el
end
-- ====================
-- Generates a recipe element for a given item.
-- This is the main external function of this module.
function p.generate_item_recipe(frame)
local args = getArgs(frame)
-- [REQUIRED]
-- Item name, alias or ID. Required.
local item = args[1]
assert_value_not_nil(item, "failed to generate a recipe for item: item was not provided")
-- [OPTIONAL]
-- Amount of item. Default is 1.
-- Must be a string since Module:Item uses string amount.
-- All values from templates come as strings.
local amount = nil_or(args[2], "1")
-- Item production method. Can be "nil", in which case it's looked up.
local method = args[3]
-- Recipe layout.
local layout = args["layout"] or args["lay"] or "horizontal"
-- Whether to only generate a materials block.
local materials_only = yesNo(args["materials only"] or args["mat only"] or false)
-- ============
local current_frame = mw:getCurrentFrame()
local item_id = itemModule.lookup_item_id_by_name_and_amount { [1] = item, [2] = amount }
local recipe_lookup_result = lookup_recipe_by_item_id(item_id, method)
assert_value_not_nil(recipe_lookup_result,
"failed to generate a recipe for item: no recipe found for item " ..
item_id .. " (method: " .. (method or "nil") .. ")")
local recipe = recipe_lookup_result.recipe
local method = recipe_lookup_result.production_method
local recipe_el = mw.html.create("div")
:addClass("item-recipe")
:node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item recipe/styles.css' }))
if layout == "vertical" or layout == "ver" then
recipe_el:addClass("item-recipe-vertical")
else
recipe_el:addClass("item-recipe-horizontal")
end
local body_el = mw.html.create("span")
: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 .. ")")
for material, cost in pairs(recipe.materials) do
materials_el:node(itemModule.generate_item { [1] = material, [2] = cost * amount })
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 = mw.html.create("span")
:addClass('item-recipe-method')
:node(itemModule.generate_item { [1] = method, capitalize = true })
product_and_method_container_el:node(method_el)
local info_icons_el = mw.html.create("div")
:addClass("item-recipe-info-icons")
header_el:node(info_icons_el)
recipe_el:node(body_el)
local complete_time_el = mw.html.create("span")
:addClass('item-recipe-complete-time')
:node((recipe.completetime * amount) .. " " .. "sec.")
body_el:node(complete_time_el)
local notes_el = mw.html.create("div")
:addClass("item-recipe-notes")
:css('background', 'linear-gradient(40deg, var(--bg-color-light-x3), var(--bg-color-light-x2))')
if recipe.availability then
local is_recipe_unlocked_by_research = string.find(recipe.availability, "dynamic", 1, true) ~= nil
local is_recipe_unlocked_by_emag = string.find(recipe.availability, "emag", 1, true) ~= nil
local is_recipe_unlocked_by_research_and_then_emag = is_recipe_unlocked_by_research and
is_recipe_unlocked_by_emag
if is_recipe_unlocked_by_research ~= nil then
recipe_el:addClass('item-recipe-by-research')
info_icons_el:node("[[File:JobIconResearchDirector.png|24px]]")
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
end
if is_recipe_unlocked_by_emag then
recipe_el:addClass('item-recipe-by-emag')
info_icons_el:node("[[File:Emag.png|42px]]")
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
if is_recipe_unlocked_by_research_and_then_emag then
notes_el:node(
generate_note_element(
nil,
current_frame:preprocess(
"'''This recipe is unlocked by <span style='color: var(--danger-color);'>[[Cryptographic Sequencer|{{item|EmagUnlimited|l=EMAG}}]]</span> after it has been <span style='color: gold;'>researched<span>'''")
)
)
end
end
recipe_el:node(notes_el)
end
return recipe_el
:allDone()
end
-- Generates an alphabetical list of recipes (elements) for a given production method.
-- Used to list all recipes for a particular method.
-- Recipes are sorted based on their products (the items display names).
function p.generate_list_of_recipes_for_method(frame)
local args = getArgs(frame)
local method = args[1]
assert_value_not_nil(method, "failed to generate a list of recipes for a method: method was not provided")
-- Limit on how many recipes to generate
local recipes_limit = tonumber(args.limit) or 99999999
local recipes = recipe_groups[method]
assert_value_not_nil(recipes, "failed to generate a list of recipes for a method: unknown method: " .. method)
-- =======================
local container_el = mw.html.create("div")
:addClass("item-recipes-list")
-- generate a list of products
local products = {}
local i = 1
for product, _ in pairs(recipes) do
table.insert(products, product)
if i == recipes_limit then
break
end
i = i + 1
end
-- sort the list of products alphabetically
-- by looking up the item names and comparing them
table.sort(products, function(first, second)
return itemModule.lookup_item_name_by_id({ [1] = lookup_item_id_by_recipe_product(first) })
< itemModule.lookup_item_name_by_id({ [1] = lookup_item_id_by_recipe_product(second) })
end)
-- generate recipe elements
for _, product in ipairs(products) do
container_el:node(p.generate_item_recipe {
[1] = lookup_item_id_by_recipe_product(product),
[2] = 1,
[3] = method
})
end
return container_el
:allDone()
end
return p