Module:Item
Documentation for this module may be created at Module:Item/doc
-- Contains utilities for working with in-game items.
-- todo create external tests for schema tables (under /doc)
-- todo make `generate_list_of_all_items_with_icons` also display items with nontrivial image files
local p = {} --p stands for package
local getArgs = require('Module:Arguments').getArgs
local yesNo = require('Module:Yesno')
-- A table mapping item IDs to their names.
-- Keys are item IDs; each value is a string.
--
-- These names are used for labels.
-- This table will be updated automatically, DO NOT make manual changes to it - they will be lost.
local item_names_by_item_ids = mw.loadJsonData("Module:Item/item names by item ids.json")
-- A table mapping item names to their IDs.
-- Keys are item names; each value is an item ID.
-- An item can have multiple names.
--
-- These names are used for ID lookups.
-- This table will be updated automatically, DO NOT make manual changes to it - they will be lost.
local item_ids_by_item_names = mw.loadJsonData("Module:Item/item ids by item lowercase names.json")
-- Same as `item_ids_by_item_names`, but has a higher priority
-- and meant to be filled manually.
--
-- These names are used for ID lookups.
local item_ids_by_item_names_override = mw.loadJsonData("Module:Item/item ids by item lowercase names overrides.json")
-- A table mapping item IDs to their image files.
-- Keys are item IDs; each value is a file name or an object (for items with multiple textures).
--
-- Meant to be filled manually.
--
-- These are used to display item icons.
local item_images_by_item_ids = mw.loadJsonData("Module:Item/item image files by item id.json")
-- A table mapping item IDs to specific pages.
-- Keys are item IDs; each value is a page name.
--
-- Meant to be filled manually.
--
-- These are used to turn items into links.
local item_page_links_by_item_ids = mw.loadJsonData("Module:Item/item page links by item ids.json")
-- Get a reference to the current frame.
local current_frame = mw:getCurrentFrame()
-- A boolean that becomes `true` once the template styles for {{Item}} has been added to the page.
-- Used to not add them a million times for all items generations.
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
-- Makes the first letter uppercase.
-- Source: https://stackoverflow.com/a/2421746
local function capitalize(str)
return (str:gsub("^%l", string.upper))
end
local function passthrough_assert_true(value, valueToReturnIfTrue, errorMessageOnFalse)
if value then
return valueToReturnIfTrue
else
error(errorMessageOnFalse)
end
end
-- =======================
-- A table of item IDs that were validated for `validate_item_images_by_item_ids_table_entry`..
local validate_item_images_by_item_ids_table_entry__validated_item_ids = {}
-- Validator for item images table entries.
--
-- Used internally for lazilly validating schema of entries.
--
-- Once a validation is conducted for an entry, subsequent calls for the same item ID will
-- not trigger revalidation, thus the lazy part.
local function validate_item_images_by_item_ids_table_entry(entry, item_id)
-- skip validation for already validated entries
if validate_item_images_by_item_ids_table_entry__validated_item_ids[item_id] ~= nil then
return
end
assert_value_not_nil(entry, "item images json file validation failed: no entry was found with item ID: " .. item_id)
if type(entry) == 'table' then
assert_value_not_nil(entry.default,
"item images json file validation failed: expected 'default' to be defined for item: " .. item_id)
assert(type(entry.default) == "string",
"item images json file validation failed: expected 'default' to be a string, found '" ..
type(entry.default) .. "' for item: " .. item_id)
local byCondition = entry.byCondition
assert_value_not_nil(entry.byCondition,
"item images json file validation failed: expected 'byCondition' to be defined since 'amount' was given; item: " ..
item_id)
assert(type(entry.byCondition) == "table",
"item images json file validation failed: expected 'byCondition' to be a table, found '" ..
type(entry.byCondition) .. "' for item: " .. item_id)
for _, byConditionEntry in ipairs(byCondition) do
local entry_type = byConditionEntry.type
assert_value_not_nil(entry_type,
"item images json file validation failed: expected 'type' to be defined on one of 'byCondition' entries; item: " ..
item_id)
assert(type(entry_type) == "string",
"item images json file validation failed: expected 'type' to be a string on one of 'byCondition' entries, found '" ..
type(entry_type) .. "' for item: " .. item_id)
if entry_type == 'amount' then
local conditions = byConditionEntry.conditions
assert_value_not_nil(conditions,
"item images json file validation failed: expected 'conditions' to be defined on one of 'byCondition' entries; item: " ..
item_id)
assert(type(conditions) == "table",
"item images json file validation failed: expected 'conditions' to be a table on one of 'byCondition' entries, found '" ..
type(conditions) .. "' for item: " .. item_id)
for _, condition in ipairs(conditions) do
local file = condition.file
assert_value_not_nil(file,
"item images json file validation failed: expected 'file' in one of 'conditions' entries in on one of 'byCondition' entries to be defined for item: " ..
item_id)
local conditionMin = condition.min
if conditionMin ~= nil then
assert(type(conditionMin) == "number",
"item images json file validation failed: expected 'min' in one of 'conditions' entries in on one of 'byCondition' entries to be a number, found '" ..
type(condition.min) .. "' for item: " .. item_id)
end
end
else
error(
"item images json file validation failed: expected 'type' to be one of known values on one of 'byCondition' entries, but found '" ..
entry_type .. "' entries; item: " .. item_id)
end
end
end
validate_item_images_by_item_ids_table_entry__validated_item_ids[item_id] = true
end
-- =======================
-- Lookups item ID by item name override (any casing).
--
-- Raises an error if no item name override was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_item_id_by_name_override(item_name_override, no_error)
assert_value_not_nil(item_name_override,
"item ID lookup by item name override failed: item name override was not provided")
local item_name_override_lower = string.lower(item_name_override)
local item_id = item_ids_by_item_names_override[item_name_override_lower]
if item_id == nil then
if no_error then
return nil
else
error("item ID lookup by item name override failed: no item name override '" ..
item_name_override_lower ..
"' was found. Make sure that an override is defined in the item name overrides table of Module:Item")
end
end
if not p.item_exists_with_id(item_id) then
error("item ID lookup by item name override failed: item with looked up item ID '" ..
item_id .. "' does not exist (item name override: '" ..
item_name_override_lower ..
"'). Make sure that the name override for this item is defined correctly in Module:Item and the item exist")
end
return item_id
end
-- Lookups item ID by item name `item_name` (any casing).
--
-- Raises an error if no item was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_item_id_by_item_name(item_name, no_error)
assert_value_not_nil(item_name, "item ID lookup by item name failed: item name was not provided")
-- first, try to lookup item name in name overrides
return p.lookup_item_id_by_name_override(item_name, true)
-- then, look in regular item names
or item_ids_by_item_names[string.lower(item_name)]
or passthrough_assert_true(
no_error,
nil,
"item ID lookup by item name failed: no item name was found by item ID '" ..
item_name .. "'. Make sure that an item exist with this ID or a name override is defined"
)
end
-- Lookups item ID by query.
-- Query can either be an item ID (strict casing) or item name (any casing).
--
-- Raises an error if no item was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_item_id(query, no_error)
assert_value_not_nil(query, "item ID lookup failed: item ID/name (query) was not provided")
if item_names_by_item_ids[query] ~= nil then
return query
else
return p.lookup_item_id_by_item_name(query, true)
or passthrough_assert_true(
no_error,
nil,
"item ID lookup failed: no item was found by ID/name '" ..
query .. "'. Make sure that an item exist with this ID or a name override is defined"
)
end
end
-- Lookups item name by item ID `item_id` (strict casing).
--
-- Raises an error if if no item was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_item_name_by_item_id(item_id, no_error)
assert_value_not_nil(item_id, "item name lookup by item ID failed: item ID was not provided")
return item_names_by_item_ids[item_id]
or
passthrough_assert_true(
no_error,
nil,
"item name lookup by item ID failed: no item name was found by item ID '" ..
item_id .. "'. Make sure that an item exist with this ID or a name override is defined"
)
end
-- Lookups item name by query.
-- Query can either be an item ID (strict casing) or item name (any casing).
--
-- Raises an error if if no item was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_item_name(query, no_error)
assert_value_not_nil(query, "item name lookup failed: item ID/name (query) was not provided")
local query_lower = string.lower(query)
if p.lookup_item_id_by_name_override(query, true) ~= nil or item_ids_by_item_names[query_lower] ~= nil then
return query
else
return p.lookup_item_name_by_item_id(query, true)
or passthrough_assert_true(
no_error,
nil,
"item name lookup failed: no item was found by ID/name '" ..
query .. "'. Make sure that an item exist with this ID or a name override is defined"
)
end
end
-- Checks whether an item exists with name `item_name` (any casing).
function p.item_exists_with_name(item_name)
-- query non-nil assertion is done in the subsequent function call
return p.lookup_item_id_by_item_name(item_name, true) ~= nil
end
-- Checks whether an item exists with ID `item_id` (strict casing).
function p.item_exists_with_id(item_id)
-- query non-nil assertion is done in the subsequent function call
return p.lookup_item_name_by_item_id(item_id, true) ~= nil
end
-- Checks whether an item exists by query.
-- Query can either be an item ID (strict casing) or item name (any casing).
function p.item_exists(query)
-- query non-nil assertion is done in the subsequent function calls
return p.item_exists_with_id(query)
or p.item_exists_with_name(query)
end
-- Lookups item image by query.
-- Query can either be an item ID (strict casing) or item name (any casing).
--
-- An `amount` can be specified to correctly pick an image for items
-- with multiple images (depending on the amount). By default, it has no value.
--
-- Raises an error if no item was found by `query`.
--
-- Returns `nil` if no image is defined for an item.
function p.try_lookup_item_image(query, amount)
local item_id = p.lookup_item_id(query, true)
assert_value_not_nil(item_id, "item image lookup failed: no item was found by ID/name '" .. query .. "'")
local item_image = item_images_by_item_ids[item_id]
if item_image == nil then
return nil
elseif type(item_image) == 'string' then
return item_image
end
-- if item image "entry" was found and it's not a string,
-- then it must be a config for multiple images.
--
-- send the "entry" to validation.
validate_item_images_by_item_ids_table_entry(item_image, item_id)
-- if validation was successful with no errors,
-- now we can utilize the config schema without doing any checks.
-- if no amount is specified,
-- then there's no reason to resolve further.
-- use the default image file for that config.
if amount == nil then
return item_image.default
end
-- if amount is specified, then resolve further
for _, byConditionEntry in ipairs(item_image.byCondition) do
local entry_type = byConditionEntry.type
if entry_type == 'amount' then
local conditions = byConditionEntry.conditions
for _, condition in ipairs(conditions) do
-- currently, there's a single condition - "min".
-- it might be unset, meaning no other conditions,
local conditionMin = condition.min
if conditionMin == nil then
-- if this condition is not set, then there's no other conditions to check.
-- use the file from this condition
return condition.file
elseif amount >= conditionMin then
-- if condition is set - validate it.
-- if it satisfies - return the file.
return condition.file
end
end
else
error(
"item image lookup failed: unknown entry type '" .. entry_type .. "' for item '" .. item_id .. "'")
end
end
-- if not a single condition satisfied - raise an error
error("item image lookup failed: no condition satisfied for item '" .. item_id .. "'")
end
-- Lookups item page name by query.
-- Query can either be an item ID (strict casing) or item name (any casing).
--
-- Returns `nil` if no page is defined for an item.
function p.try_lookup_item_page(query)
local item_id = p.lookup_item_id(query, true)
assert_value_not_nil(item_id, "item page lookup failed: no item was found by ID/name '" .. query .. "'")
return item_page_links_by_item_ids[item_id]
end
-- ==============================
-- Generates an item element.
-- This is the main function of this module.
function p.generate_item(frame)
local args = getArgs(frame)
local argsWithWhitespace = getArgs(frame, { trim = false, removeBlanks = false })
-- [REQUIRED]
-- input item name or ID.
-- any casing is allowed for name, but ID must follow strict casing.
local input_item = args[1]
assert_value_not_nil(input_item, "failed to generate an item: item was not provided")
-- [OPTIONAL]
-- amount of item.
-- input is a string number or nil
local input_amount = tonumber(args[2])
-- item icon size. uses CSS units.
local input_icon_size = args.size or "32px"
-- text label. can be set, otherwise inferred from the item later (so "nil" for now).
local input_label = argsWithWhitespace.label or argsWithWhitespace.l
-- whether to capitalize the label. false by default.
local input_capitalize_label = yesNo(args.capitalize or args.cap or false)
-- a link to a page.
-- if set, turns item into a link.
-- if unset, and item has a link defined for it in the config - uses it.
local input_link = args.link
-- ============
local item_id = p.lookup_item_id(input_item, true)
assert_value_not_nil(item_id, "item generation failed: no item was found by ID/name '" .. input_item .. "'")
local item_image_filename = p.try_lookup_item_image(item_id, input_amount)
local item_page_link = input_link
or p.try_lookup_item_page(item_id)
local label
if input_label == nil then
-- if a custom label is not provided, lookup the item's label
label = p.lookup_item_name_by_item_id(item_id)
else
-- if a label is provided - use it
label = input_label
end
if input_capitalize_label then
label = capitalize(label)
end
if input_amount ~= nil then
label = input_amount .. " " .. label
end
if item_page_link ~= nil then
label = "[[" .. item_page_link .. "|" .. label .. "]]"
end
-- ============
local item_el = mw.html.create("span")
:addClass("item")
-- add icon element inside the label if icon is provided
if item_image_filename ~= nil then
local link_param = ''
if item_page_link ~= nil then
link_param = '|link=' .. item_page_link
end
item_el:node("[[File:" ..
item_image_filename .. "|" .. input_icon_size .. "|class=item-icon" .. link_param .. "]]")
end
item_el:node(label)
if not was_template_styles_tag_el_added then
item_el:node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item/styles.css' }))
was_template_styles_tag_el_added = true
end
return item_el
:allDone()
end
function p.generate_list_of_all_items_with_icons(frame)
local args = getArgs(frame)
local columns_count = args[1]
assert_value_not_nil(columns_count, "columns count was not provided")
local container = mw.html.create("div")
:css("column-count", columns_count)
-- an array of item ids that have images
local item_ids_with_images = {}
for item_id, _ in pairs(item_images_by_item_ids) do
table.insert(item_ids_with_images, item_id)
end
local function assert_looked_up_item_name_is_not_nil(item_id, item_name)
assert_value_not_nil(item_name,
"failed to generate a list of items with icons: no item was found by ID '" ..
item_id ..
"'. This likely indicates that the item with this ID was removed or the ID was misspelled in the item image files table")
end
-- sort alphabetically
table.sort(item_ids_with_images, function(first, second)
local first_item_name = p.lookup_item_name_by_item_id(first, true)
local second_item_name = p.lookup_item_name_by_item_id(second, true)
assert_looked_up_item_name_is_not_nil(first, first_item_name)
assert_looked_up_item_name_is_not_nil(second, second_item_name)
return first_item_name < second_item_name
end)
-- generate child elements from the template
for _, item_id in ipairs(item_ids_with_images) do
local item_el = p.generate_item { item_id }
:css("display", "block")
container:node(item_el)
end
return container
:allDone()
end
-- -- Generates a list of ALL items.
-- -- Will likely break :clueless:
function p.generate_list_of_all_items(frame)
local args = getArgs(frame)
local columns_count = args[1]
assert_value_not_nil(columns_count, "columns count was not provided")
local container = mw.html.create("div")
:css("column-count", columns_count)
local item_ids = {}
for item_id, _ in pairs(item_names_by_item_ids) do
table.insert(item_ids, item_id)
end
-- sort alphabetically
table.sort(item_ids, function(first, second)
return p.lookup_item_name_by_item_id(first) < p.lookup_item_name_by_item_id(second)
end)
-- generate child elements from the template
for _, item_id in ipairs(item_ids) do
local item_el = p.generate_item { item_id }
:css("display", "block")
:node(" <span style='color: gray;'>ID " .. item_id .. "</span>")
container:node(item_el)
end
return container
:allDone()
end
return p