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
    local count = 0
for _ in ipairs(t) do count = count + 1 end
    for _ in ipairs(t) do count = count + 1 end
return count
    return count
end
end


local function table_length(t)
local function table_length(t)
local count = 0
    local count = 0
for _ in pairs(t) do count = count + 1 end
    for _ in pairs(t) do count = count + 1 end
return count
    return count
end
end


local function table_has_value(tab, val)
local function table_has_value(tab, val)
for _, value in ipairs(tab) do
    for _, value in ipairs(tab) do
if value == val then
        if value == val then
return true
            return true
end
        end
end
    end


return false
    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 value == nil then
if error_message == nil then
        if error_message == nil then
error("value is nil")
            error("value is nil")
else
        else
error(error_message)
            error(error_message)
end
        end
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
    if value == nil then
return value_if_nil
        return value_if_nil
else
    else
return value
        return value
end
    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
    for i, item in ipairs(table) do
if condition(item, i, table) then
        if condition(item, i, table) then
return item
            return item
end
        end
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
    for key, value in pairs(table) do
if condition(key, value, table) then
        if condition(key, value, table) then
return value
            return value
end
        end
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
    for key, value in pairs(table) do
if condition(key, value, table) then
        if condition(key, value, table) then
return key
            return key
end
        end
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(
    local match = find_first_table_item_key_matching_condition(
product_overrides,
        product_overrides,
function(_, value) return value == item_id end
        function(_, value) return value == item_id end
)
    )


if match then
    if match then
return match
        return match
elseif fallback_recipe_product then
    elseif fallback_recipe_product then
return fallback_recipe_product
        return fallback_recipe_product
else
    else
return nil
        return nil
end
    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]
    local match = product_overrides[product_item_id]


if match then
    if match then
return match
        return match
elseif fallback_item_id then
    elseif fallback_item_id then
return fallback_item_id
        return fallback_item_id
else
    else
return nil
        return nil
end
    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
    if not skip_product_override_lookup then
product = try_lookup_item_id_by_override_recipe_product(product, product)
        product = try_lookup_item_id_by_override_recipe_product(product, product)
end
    end


if not itemModule.item_exists_by_id_or_name(product) then
    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")
        error("no recipe was found for product item ID '" ..
end
        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")
    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
    -- generate a list of production methods to search across
local methods_to_lookup = {}
    local methods_to_lookup = {}
if production_method == nil then
    if production_method == nil then
-- no method specified = search across all methods
        -- no method specified = search across all methods
for recipe_group_method, _ in pairs(recipes_by_method) do
        for recipe_group_method, _ in pairs(recipes_by_method) do
table.insert(methods_to_lookup, recipe_group_method)
            table.insert(methods_to_lookup, recipe_group_method)
end
        end
else
    else
-- method specified = only look through recipes with that production method
        -- method specified = only look through recipes with that production method
-- lookup the proper name for the method, which is gonne be just another item ID
        -- lookup the proper name for the method, which is gonne be just another item ID
table.insert(methods_to_lookup, itemModule.try_lookup_item_id(production_method))
        table.insert(methods_to_lookup, itemModule.lookup_item_id(production_method))
end
    end


-- lookup a product id based on existing overrides
    -- lookup a product id based on existing overrides
-- if there's none, use the same item ID
    -- if there's none, use the same item ID
item_id = try_lookup_override_recipe_product_by_item_id(item_id, item_id)
    item_id = try_lookup_override_recipe_product_by_item_id(item_id, item_id)


-- do the search
    -- do the search
for _, production_method in ipairs(methods_to_lookup) do
    for _, production_method in ipairs(methods_to_lookup) do
local match = recipes_by_method[production_method][item_id]
        local match = recipes_by_method[production_method][item_id]
if match then
        if match then
return {
            return {
production_method = production_method,
                production_method = production_method,
recipe = match
                recipe = match
}
            }
end
        end
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")
    assert_value_not_nil(item_id, "failed to lookup order of material: item ID was not provided")


return order_of_materials[item_id]
    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')
    local el = mw.html.create('span')
:addClass("item-recipe-note")
        :addClass("item-recipe-note")
:node(text)
        :node(text)


if color then
    if color then
el:css('color', color)
        el:css('color', color)
end
    end


return el
    return el
end
end


local function generate_info_icon(frame, kind)
local function generate_info_icon(frame, kind)
if kind == 'research' then
    if kind == 'research' then
return frame:expandTemplate {
        return frame:expandTemplate {
title = 'Tooltip',
            title = 'Tooltip',
args = {
            args = {
"[[File:JobIconResearchDirector.png|24px|link=Research_and_Development#R&D_Tree]]",
                "[[File:JobIconResearchDirector.png|24px|link=Research_and_Development#R&D_Tree]]",
"This recipe is unlocked by '''research'''"
                "This recipe is unlocked by '''research'''"
}
            }
}
        }
elseif kind == 'emag' then
    elseif kind == 'emag' then
return frame:expandTemplate {
        return frame:expandTemplate {
title = 'Tooltip',
            title = 'Tooltip',
args = {
            args = {
"[[File:Emag.png|42px|link=Cryptographic Sequencer]]",
                "[[File:Emag.png|42px|link=Cryptographic Sequencer]]",
"This recipe is unlocked by '''EMAG'''"
                "This recipe is unlocked by '''EMAG'''"
}
            }
}
        }
elseif kind == 'progression-symbol' then
    elseif kind == 'progression-symbol' then
return mw.html.create("span")
        return mw.html.create("span")
:addClass('info-icon-progression-symbol')
            :addClass('info-icon-progression-symbol')
:node("↓")
            :node("↓")
else
    else
error("failed to generate an info icon: unknown kind " .. kind)
        error("failed to generate an info icon: unknown kind " .. kind)
end
    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)
    local args = getArgs(frame)
 
    -- [REQUIRED]


-- [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")


-- Item name, alias or ID. Required.
    -- [OPTIONAL]
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")


-- Amount of item. Default is 1.
    -- Item production method. Can be "nil", in which case it's looked up.
-- Must be a string since Module:Item uses string amount.
    local method = args[3]
-- 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)


-- Whether to only generate a materials block.
    -- Layout of materials in materials only mode.
local materials_only = yesNo(args["materials only"] or args["mat only"] or false)
    local materials_only_layout = args["materials only layout"] or args["mat only layout"] or "vertical"


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


-- first, try to lookup the item ID in case the given item as a overriden product (for some reason).
    -- then, check that the item exists.
-- fallback to the same item in case there's no overrides.
    -- at this point, it can either a name or an ID.
item = try_lookup_item_id_by_override_recipe_product(item, item)
    -- 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)


-- then, check that the item exists.
    -- finally, get the item ID.
-- at this point, it can either a name or an ID.
    -- this is done after the assertion for purposes of a pretty error message in case a lookup fails.
-- skip product override lookup with an extra `true` param.
    -- here we are guaranteed to have an item.
-- in case items doesn't exist, this will give a pretty error message.
    local item_id = itemModule.lookup_item_id(item)
assert_product_exists(item, true)


-- finally, get the item ID.
    -- for a recipe using the resulting item ID.
-- this is done after the assertion for purposes of a pretty error message in case a lookup fails.
    local recipe_lookup_result = lookup_recipe_by_item_id(item_id, method)
-- here we are guaranteed to have an item.
    assert_value_not_nil(recipe_lookup_result,
local item_id = itemModule.try_lookup_item_id(item)
        "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")


-- for a recipe using the resulting item ID.
    local recipe = recipe_lookup_result.recipe
local recipe_lookup_result = lookup_recipe_by_item_id(item_id, method)
    local method = recipe_lookup_result.production_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")


local recipe_el = mw.html.create("div")
    if materials_only_layout == "vertical" or materials_only_layout == "ver" then
:addClass("item-recipe")
        recipe_el:addClass("item-recipe-materials-layout-vertical")
    else
        recipe_el:addClass("item-recipe-materials-layout-horizontal")
    end


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


local materials_el = mw.html.create("div")
    assert_value_not_nil(recipe.materials,
:addClass("item-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,
    -- get a list of materials (item ids)
"failed to generate a recipe for item: no 'materials' are specified for item " ..
    local materials_item_ids = {}
item_id .. " recipe (method: " .. method .. ")")
    for item_id in pairs(recipe.materials) do
        table.insert(materials_item_ids, item_id)
    end


-- get a list of materials (item ids)
    -- sort materials list based on material order config
local materials_item_ids = {}
    -- if item doesn't have an order, use a big number as its order, placing them after
for item_id in pairs(recipe.materials) do
    -- materials that have an order defined
table.insert(materials_item_ids, item_id)
    table.sort(materials_item_ids, function(first, second)
end
        return (lookup_order_of_material(first) or 999999999999)
            < (lookup_order_of_material(second) or 999999999999)
    end)


-- sort materials list based on material order config
    for _, material in ipairs(materials_item_ids) do
-- if item doesn't have an order, use a big number as its order, placing them after
        local cost = recipe.materials[material]
-- 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
        if not itemModule.item_exists(material) then
local cost = recipe.materials[material]
            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


if not itemModule.item_exists_by_id_or_name(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


materials_el:node(itemModule.generate_item { material, cost })
    body_el:node(materials_el)
end


body_el:node(materials_el)


    if materials_only then
        recipe_el:addClass("materials-only")


if materials_only then
        recipe_el:node(body_el)
recipe_el:addClass("materials-only")
    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(body_el)
        recipe_el:node(header_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")


local product_and_method_container_el = mw.html.create("div")
        header_el:node(product_and_method_container_el)
: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")


local product_el = itemModule.generate_item { [1] = item_id, [2] = amount, cap = true }
        product_and_method_container_el:node(product_el)
: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 })


-- TODO: not all methods will be items, so this will eventually break.
            methods_items_els_cache[method] = method_el
local method_el = methods_items_els_cache[method]
        end
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
        product_and_method_container_el:node(method_el)
end


product_and_method_container_el:node(method_el)


        local info_icons_el = mw.html.create("div")
            :addClass("item-recipe-info-icons")


local info_icons_el = mw.html.create("div")
        header_el:node(info_icons_el)
:addClass("item-recipe-info-icons")


header_el:node(info_icons_el)


        recipe_el:node(body_el)


recipe_el:node(body_el)


        local complete_time_el = mw.html.create("span")
            :addClass('item-recipe-complete-time')


local complete_time_el = mw.html.create("span")
        if recipe.completetime == 0 then
:addClass('item-recipe-complete-time')
            complete_time_el:node("Instant")
        else
            complete_time_el:node((recipe.completetime * amount) .. " " .. "sec.")
        end


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)
        body_el:node(complete_time_el)




local notes_el = mw.html.create("div")
        local notes_el = mw.html.create("div")
:addClass("item-recipe-notes")
            :addClass("item-recipe-notes")
:css('background', 'linear-gradient(40deg, var(--bg-color-light-x3), var(--bg-color-light-x2))')
            :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")
        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,
        -- if recipe is not available by default,
-- generate "info icons" and notes (if needed) telling about it.
        -- generate "info icons" and notes (if needed) telling about it.
if recipe.availability ~= 'static' then
        if recipe.availability ~= 'static' then
local is_recipe_unlocked_by_research = string.find(recipe.availability, "dynamic", 1, true) ~= nil
            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_emag = string.find(recipe.availability, "emag", 1, true) ~= nil
local is_recipe_unlocked_by_research_and_then_emag = is_recipe_unlocked_by_research and
            local is_recipe_unlocked_by_research_and_then_emag = is_recipe_unlocked_by_research and
is_recipe_unlocked_by_emag
                is_recipe_unlocked_by_emag


if is_recipe_unlocked_by_research_and_then_emag then
            if is_recipe_unlocked_by_research_and_then_emag then
recipe_el:addClass('item-recipe-by-research')
                recipe_el:addClass('item-recipe-by-research')
recipe_el:addClass('item-recipe-by-emag')
                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, 'research'))
info_icons_el:node(generate_info_icon(current_frame, 'progression-symbol'))
                info_icons_el:node(generate_info_icon(current_frame, 'progression-symbol'))
info_icons_el:node(generate_info_icon(current_frame, 'emag'))
                info_icons_el:node(generate_info_icon(current_frame, 'emag'))


notes_el:node(
                notes_el:node(
generate_note_element(
                    generate_note_element(
nil,
                        nil,
current_frame:preprocess(
                        current_frame:preprocess(
"'''This recipe is unlocked by [[Cryptographic Sequencer|EMAG]] after it has been [[Research_and_Development#R&D_Tree|researched]]'''")
                            "'''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
            elseif is_recipe_unlocked_by_research then
recipe_el:addClass('item-recipe-by-research')
                recipe_el:addClass('item-recipe-by-research')


info_icons_el:node(generate_info_icon(current_frame, 'research'))
                info_icons_el:node(generate_info_icon(current_frame, 'research'))


-- if not is_recipe_unlocked_by_research_and_then_emag then
                -- if not is_recipe_unlocked_by_research_and_then_emag then
-- notes_el:node(
                -- notes_el:node(
-- generate_note_element(
                -- generate_note_element(
-- "gold",
                -- "gold",
-- "'''This recipe is unlocked by research'''"
                -- "'''This recipe is unlocked by research'''"
-- )
                -- )
-- )
                -- )
-- end
                -- end
else
            else
recipe_el:addClass('item-recipe-by-emag')
                recipe_el:addClass('item-recipe-by-emag')


info_icons_el:node(generate_info_icon(current_frame, 'emag'))
                info_icons_el:node(generate_info_icon(current_frame, 'emag'))


-- if not is_recipe_unlocked_by_research_and_then_emag then
                -- if not is_recipe_unlocked_by_research_and_then_emag then
-- notes_el:node(
                -- notes_el:node(
-- generate_note_element(
                -- generate_note_element(
-- "var(--danger-color)",
                -- "var(--danger-color)",
-- current_frame:preprocess(
                -- current_frame:preprocess(
-- "'''This recipe is unlocked by [[Cryptographic Sequencer|{{item|EmagUnlimited|l=EMAG}}]]'''")
                -- "'''This recipe is unlocked by [[Cryptographic Sequencer|{{item|EmagUnlimited|l=EMAG}}]]'''")
-- )
                -- )
-- )
                -- )
-- end
                -- end
end
            end
end
        end


recipe_el:node(notes_el)
        recipe_el:node(notes_el)
end
    end




if not was_template_styles_tag_el_added then
    if not was_template_styles_tag_el_added then
recipe_el:node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item recipe/styles.css' }))
        recipe_el:node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item recipe/styles.css' }))


was_template_styles_tag_el_added = true
        was_template_styles_tag_el_added = true
end
    end


return recipe_el
    return recipe_el
:allDone()
        :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 args = getArgs(frame)


local method = args[1]
    local method = args[1]
assert_value_not_nil(method, "failed to generate a list of recipes for a method: method was not provided")
    assert_value_not_nil(method, "failed to generate a list of recipes for a method: method was not provided")


-- Limit on how many recipes to generate
    -- Limit on how many recipes to generate
local recipes_limit = tonumber(args.limit) or 99999999
    local recipes_limit = tonumber(args.limit) or 99999999


local recipes = recipes_by_method[itemModule.try_lookup_item_id(method)]
    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)
    assert_value_not_nil(recipes, "failed to generate a list of recipes for a method: unknown method: " .. method)


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


local container_el = mw.html.create("div")
    local container_el = mw.html.create("div")
:addClass("item-recipes-list")
        :addClass("item-recipes-list")


-- generate a list of products
    -- generate a list of products
local products = {}
    local products = {}
local i = 1
    local i = 1
for product_item_id, _ in pairs(recipes) do
    for product_item_id, _ in pairs(recipes) do
table.insert(products, product_item_id)
        table.insert(products, product_item_id)


if i == recipes_limit then
        if i == recipes_limit then
break
            break
end
        end


i = i + 1
        i = i + 1
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)
local first_item_id = try_lookup_item_id_by_override_recipe_product(first, first)
        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)
        local second_item_id = try_lookup_item_id_by_override_recipe_product(second, second)


assert_product_exists(first_item_id, true)
        assert_product_exists(first_item_id, true)
assert_product_exists(second_item_id, true)
        assert_product_exists(second_item_id, true)


return itemModule.lookup_item_name_by_id(first_item_id)
        return itemModule.lookup_item_name(first_item_id)
< itemModule.lookup_item_name_by_id(second_item_id)
            < itemModule.lookup_item_name(second_item_id)
end)
    end)


-- 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] = try_lookup_item_id_by_override_recipe_product(product, product),
            [1] = try_lookup_item_id_by_override_recipe_product(product, product),
[2] = 1,
            [2] = 1,
[3] = method
            [3] = method
})
        })
end
    end


return container_el
    return container_el
:allDone()
        :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:

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