Difference between revisions of "Module:Infobox Item"

From Idlescape Wiki
Jump to navigation Jump to search
(Scroll fix)
m (Move Enchanting Base XP from column to row and to before costs)
 
(28 intermediate revisions by 3 users not shown)
Line 1: Line 1:
 
local p = {}
 
local p = {}
  
local data_module_names = {
+
local module_names = {
item = 'Module:Items/data',
+
    item = 'Module:Items/data',
enchantment = 'Module:Enchantment/data',
+
    enchantment = 'Module:Enchantment/data',
location = 'Module:Location/data',
+
    location = 'Module:Location/data',
craftaug = 'Module:CraftingAugmenting/data',
+
    craftaug = 'Module:CraftingAugmenting/data',
farming = 'Module:Farming/data'
+
    farming = 'Module:Farming/data',
 +
    augloot = 'Module:Augmenting loot/data',
 +
    cooking = 'Module:CookingList/data',
 +
    ability = 'Module:Abilities/data',
 +
    gameshop = 'Module:GameShopItems/data',
 +
    itemsets = 'Module:Item sets',
 
}
 
}
  
local loaded_data_modules = {}
+
local loaded_modules = {}
  
 
local headerCount = 1
 
local headerCount = 1
 
local labelCount = 1
 
local labelCount = 1
 
local dataCount = 1
 
local dataCount = 1
 +
 +
local cups = {
 +
    16000,16001,16002,16003,16004,16005,16006,16007,
 +
    16008,16009,16010,16011,16012,16013,16014,16015
 +
}
 +
 +
---Checks if a table contains a specific value.
 +
---@param t table
 +
---@param value any
 +
---@return boolean
 +
local function hasValue(t, value)
 +
    for _, v in pairs(t) do
 +
        if value == v then
 +
            return true
 +
        end
 +
    end
 +
    return false
 +
end
 +
 +
---Creates a deep copy of a tables and functions.
 +
---Does not work with functions with upvalues and maybe userdata.
 +
---@generic T
 +
---@param t T The value to copy.
 +
---@return T # A new value that is a deep copy of the original.
 +
local function copyTable(t)
 +
    if type(t) ~= "table" then
 +
        return t
 +
    elseif type(t) == "function" then
 +
        return loadstring(string.dump(t))
 +
    end
 +
    local copy = {}
 +
    for k, v in pairs(t) do
 +
        copy[k] = copyTable(v)
 +
    end
 +
    return copy
 +
end
 +
 +
---Formats a number up to a given number of decimal places, removing trailing zeros.
 +
---@param num number|string The number to format.
 +
---@param digits number The number of decimal places to include (must be a non-negative integer).
 +
---@return string|nil # The formatted number as a string or nil if the input is not a valid number.
 +
local function toFixed(num, digits)
 +
    local n = tonumber(num)
 +
    if not n then
 +
        return nil
 +
    end
 +
    digits = math.max(0, math.floor(digits))
 +
    local formatted = string.format("%." .. digits .. "f", n):gsub("%.?0+$", "")
 +
    return formatted
 +
end
  
 
local function h()
 
local function h()
local s = "header" .. headerCount
+
    local s = "header" .. headerCount
headerCount = headerCount + 1
+
    headerCount = headerCount + 1
labelCount = headerCount
+
    labelCount = headerCount
dataCount = headerCount
+
    dataCount = headerCount
return s
+
    return s
 
end
 
end
  
 
local function sbreak()
 
local function sbreak()
local s = "sbreak" .. headerCount
+
    local s = "sbreak" .. headerCount
headerCount = headerCount + 1
+
    headerCount = headerCount + 1
labelCount = headerCount
+
    labelCount = headerCount
dataCount = headerCount
+
    dataCount = headerCount
return s
+
    return s
 
end
 
end
  
 
local function l()
 
local function l()
local s = "label" .. labelCount
+
    local s = "label" .. labelCount
dataCount = labelCount
+
    dataCount = labelCount
labelCount = labelCount + 1
+
    labelCount = labelCount + 1
headerCount = labelCount
+
    headerCount = labelCount
return s
+
    return s
 
end
 
end
  
local function d()  
+
local function d()
local s = "data" .. dataCount
+
    local s = "data" .. dataCount
dataCount = dataCount + 1
+
    dataCount = dataCount + 1
headerCount = dataCount
+
    headerCount = dataCount
labelCount = dataCount
+
    labelCount = dataCount
return s
+
    return s
 
end
 
end
  
 
local function sl()
 
local function sl()
local s = "s" .. l{}
+
    local s = "s" .. l()
return s
+
    return s
 
end
 
end
  
 
local function sd()
 
local function sd()
local s = "s" .. d{}
+
    local s = "s" .. d()
return s
+
    return s
 +
end
 +
 
 +
---@param data_type string
 +
---@return table
 +
function p.loadData(data_type)
 +
    local module_name = module_names[data_type]
 +
    if loaded_modules[module_name] == nil then
 +
        local status, module = pcall(mw.loadData, module_name)
 +
        if not status then
 +
            status, module = pcall(require, module_name)
 +
        end
 +
        if status then
 +
            loaded_modules[module_name] = module
 +
        else
 +
            error("Failed to load module: " .. data_type .. " / " .. tostring(module_name) .. "\n" .. module)
 +
        end
 +
    end
 +
 
 +
    return loaded_modules[module_name]
 
end
 
end
  
function p.loadData(data_type)  
+
local function getItem(id)
local module_name = data_module_names[data_type]
+
    return p.loadData("item")[tostring(id)]
if loaded_data_modules[module_name] == nil then
 
loaded_data_modules[module_name] = mw.loadData(module_name)
 
end
 
 
return loaded_data_modules[module_name]
 
 
end
 
end
  
local function findItem(name)
+
local function getEnchant(id)
local lname = name:lower()
+
    return p.loadData("enchantment")[tostring(id)]
 
--Remove leading and trailing spaces.
 
lname = lname:gsub('^%s*(.-)%s*$', '%1')
 
for key, item in pairs(p.loadData("item")) do
 
if lname == item.name:lower() then
 
return item
 
end
 
end
 
return 0
 
 
end
 
end
  
local function getItem(id)
+
local function getEnchantName(id)
return p.loadData("item")[tostring(id)]
+
    local enchantment = getEnchant(id)
 +
    return enchantment and enchantment.name
 
end
 
end
  
local function getEnchantmentName(id)
+
local function getModuleItemSet()
return p.loadData("enchantment")[tostring(id)].name
+
    return p.loadData("itemsets")
 
end
 
end
  
 
local function getItemName(id)
 
local function getItemName(id)
return p.loadData("item")[tostring(id)].name
+
    return p.loadData("item")[tostring(id)].name
 
end
 
end
  
 
local function getCraftAug(id)
 
local function getCraftAug(id)
return p.loadData("craftaug")[tostring(id)]
+
    return p.loadData("craftaug")[tostring(id)]
 +
end
 +
 
 +
---@param id string|number
 +
---@return CookingItem|nil
 +
local function getCookingIngredient(id)
 +
    return p.loadData("cooking")[tostring(id)]
 +
end
 +
 
 +
---@param id string|number|nil # Returns all if nil
 +
---@return AugmentingLoot|table<string, AugmentingLoot>|nil
 +
local function getAugLoot(id)
 +
    local augloot = p.loadData("augloot")
 +
    return id == nil and augloot or augloot[tostring(id)]
 +
end
 +
 
 +
---@param id string|number
 +
---@return Ability|nil
 +
local function getAbility(id)
 +
    return p.loadData("ability")[tostring(id)]
 +
end
 +
 
 +
---@param id string|number
 +
---@return {id: number, min: number, max: number, chance: number}[]|nil
 +
local function getYield(id)
 +
    return p.loadData("farming")[tostring(id)]
 +
end
 +
 
 +
---@param itemId string|number # Item ID, is not the same as the `id` in the shop.
 +
---@return nil
 +
local function getGeneralShopItem(itemId)
 +
    local id = tonumber(itemId)
 +
    if not id then return nil end
 +
 
 +
    local items = p.loadData("gameshop")
 +
    for _, item in pairs(items) do
 +
        if item.itemID == id then
 +
            return item
 +
        end
 +
    end
 +
end
 +
 
 +
---@return Item|nil
 +
local function findItem(name)
 +
    local lname = name:lower()
 +
 
 +
    --Remove leading and trailing spaces.
 +
    lname = lname:gsub('^%s*(.-)%s*$', '%1')
 +
    for key, item in pairs(p.loadData("item")) do
 +
        if lname == item.name:lower() then
 +
            return item
 +
        end
 +
    end
 +
end
 +
 
 +
local function dottedTooltip(name, tooltip)
 +
    return string.format(
 +
        '<span class="rt-commentedText tooltip tooltip-dotted" title="%s">%s</span>',
 +
        tooltip,
 +
        name
 +
    )
 +
end
 +
 
 +
---Formats the given chance as a string presentation of percentage or 10k/100k fraction.
 +
---@param chance number The chance to format. 1 equals 100%.
 +
---@return string # The formatted chance as a string.
 +
local function formatChance(chance)
 +
    local sign = chance < 0 and "-" or ""
 +
    local abs = math.abs(chance)
 +
 
 +
    if abs < 0.001 then
 +
        return sign .. toFixed(abs * 10000, 1) .. "/10k"
 +
    elseif abs < 0.0001 then
 +
        return sign .. toFixed(abs * 100000, 1) .. "/100k"
 +
    end
 +
 
 +
    return toFixed(chance * 100, 3) .. "%"
 
end
 
end
  
 
local function fullUrl(url)
 
local function fullUrl(url)
local newUrl = url
+
    local newUrl = url
if url:sub(1,5) == "https" then
+
    if url:sub(1,5) == "https" then
return newUrl
+
        return newUrl
end
+
    end
+
 
if url:sub(1,1) ~= "/" then
+
    if url:sub(1,1) ~= "/" then
newUrl = "/" .. newUrl
+
        newUrl = "/" .. newUrl
end
+
    end
+
 
newUrl = "https://www.play.idlescape.com" .. newUrl
+
    newUrl = "https://www.play.idlescape.com" .. newUrl
return newUrl
+
    return newUrl
 
end
 
end
  
local function icon(name, url, word)
+
local function icon(name, url, word, size)
local s = fullUrl(url)
+
    size = size or 20
s = "[[" .. name .. "|<img src=\"" .. s
+
    url = fullUrl(url)
s = s .. "\" alt=\""  .. name .. "\" width=\"20\">"
+
    return string.format(
if word then
+
        '[[%s|<img src="%s" alt="%s" width="%s">%s]]',
s = s .. name
+
        name,
end
+
        url,
s = s .. "]]"
+
        name,
return s
+
        size,
 +
        word and name or ""
 +
    )
 
end
 
end
  
 +
---@param id string|number # Item ID.
 +
---@param word boolean # If true, show the name of the item after the icon.
 +
---@return string # The icon wikitext.
 
local function itemImage(id, word)
 
local function itemImage(id, word)
local item = p.loadData("item")[tostring(id)]
+
    local item = p.loadData("item")[tostring(id)]
local url = ""
+
    if not item then return "" end
if item.itemIcon then
+
    local url = ""
url = item.itemIcon
+
    if item.itemIcon then
else
+
        url = item.itemIcon
url = item.itemImage
+
    else
end
+
        url = item.itemImage
return icon(item.name, url, word)
+
    end
 +
    return icon(item.name, url, word)
 
end
 
end
  
local function locationImage(id, word)
+
---Creates an Wikitext link with an image inside a div.
local loc = p.loadData("location")[tostring(id)]
+
---@param title string # The title of the link.
local url = ""
+
---@param imageAttributes table # The html attributes for the image.
if loc.locationImage then
+
---@param divCSS table # The CSS styles for the div.
url = loc.locationImage
+
---@return string # The Wikitext link with the image.
else
+
local function createWikitextImage(title, imageAttributes, divCSS)
return "[[" .. loc.name .. "]]"
+
    divCSS = divCSS or {}
end
+
    divCSS.display = divCSS.display or "inline-block" --Makes the div same size as the image
return icon(loc.name, url, word)
+
    local e = mw.html.create("div")
 +
        :css(divCSS)
 +
        :tag("img")
 +
        :attr(imageAttributes)
 +
        :done()
 +
    e = tostring(e):gsub("<img(.-) */>", "<img%1>")
 +
    return string.format('[[%s|%s]]', title, e)
 
end
 
end
  
local function img(id)
+
---Creates a Wikitext containing links and images for the abilities.
local url = ""
+
---@param abilities number[]|{id: number}[]|nil Array of ability IDs.
if item.itemIcon then
+
---@param unique boolean|nil Skips duplicate abilities if truthy, otherwise creates an element for each ability in order.
url = item.itemIcon
+
---@return string # The Wikitext containing links and images for the abilities.
else
+
local function createAbilityRotation(abilities, unique)
url = item.itemImage
+
    if not abilities then return "" end
end
+
    local colors = {
return fullUrl(url)
+
        Melee = "red",
 +
        Range = "green",
 +
        Magic = "blue",
 +
    }
 +
 
 +
    local seenIds = {}
 +
    local images = {}
 +
    for _, id in ipairs(abilities) do
 +
        if type(id) == "table" then
 +
            id = id.id
 +
        end
 +
        if not seenIds[id] then
 +
            seenIds[id] = unique and true or false
 +
            local ability = getAbility(id)
 +
            if ability then
 +
                -- Check if the ability page exists and add tooltip if not
 +
                local exists = mw.title.new(ability.abilityName)
 +
                exists = exists and exists.exists
 +
                local attributes = {
 +
                    src = fullUrl(ability.abilityImage),
 +
                    alt = ability.abilityName .. ". " .. ability.description,
 +
                    width = 30,
 +
                    class = not exists and "tooltip" or nil,
 +
                    title = not exists and ability.abilityName .. ". " .. ability.description or nil,
 +
                }
 +
                local css = {
 +
                    ["box-shadow"] = "0 0 2px 1px " .. (colors[ability.damageType] or "white"),
 +
                    background = colors[ability.damageType] or "white",
 +
                    margin = "5px"
 +
                }
 +
                table.insert(images, createWikitextImage(ability.abilityName, attributes, css))
 +
            end
 +
        end
 +
    end
 +
 
 +
    return table.concat(images, "")
 
end
 
end
  
local function addSeparator(num)
+
local function locationImage(id, word)
return tostring(tonumber(num)):reverse():gsub("(%d%d%d)","%1,"):gsub(",(%-?)$","%1"):reverse()
+
    local loc = p.loadData("location")[tostring(id)]
 +
    local url = ""
 +
    if loc.locationImage then
 +
        url = loc.locationImage
 +
    else
 +
        return "[[" .. loc.name .. "]]"
 +
    end
 +
    return icon(loc.name, url, word)
 
end
 
end
  
 
local function gatheringSource(id)
 
local function gatheringSource(id)
local s = ""
+
    local s = ""
for key, loc in pairs(p.loadData('location')) do
+
    for key, loc in pairs(p.loadData('location')) do
if loc.loot then
+
        if loc.loot then
for key2, loot in pairs(loc.loot) do
+
            for key2, loot in pairs(loc.loot) do
if id == loot.id then
+
                if id == loot.id then
s = s .. locationImage(loc.locID, true)
+
                    s = s .. locationImage(loc.locID, true)
s = s .. "<br>"
+
                    s = s .. "<br>"
end
+
                end
end
+
            end
end
+
        end
end
+
    end
return s
+
    return s
 
end
 
end
  
-- local function farmingSource(id)
+
local function farmingSource(id)
-- local s = ""
+
    local s = ""
+
    for key, item in pairs(p.loadData('item')) do
-- for key, item in pairs(p.loadData('item')) do
+
        if item.farmingStats then
-- if item.farmingStats then
+
            for key2, yield in pairs(item.farmingStats.yield) do
-- for key2, yield in pairs(item.farmingStats.yield) do
+
                if id == yield.id then
-- if id == yield.id then
+
                    s = s .. itemImage(item.id, true)
-- s = s .. itemImage(item.id, true)
+
                    s = s .. "<br>"
-- s = s .. "<br>"
+
                end
-- end
+
            end
-- end
+
        end
-- end
+
    end
-- end
+
    return s
-- return s
+
end
-- end
 
  
 
local function smithingSource(id)
 
local function smithingSource(id)
local s = ""
+
    local s = ""
local item = getItem(id)
+
    local item = getItem(id)
if item.skill == "smithing" and item.name ~= 'Ichor' then
+
    if item.skill == "smithing" and item.name ~= 'Ichor' then
s = '[[Smithing]]<br>'
+
        s = '[[Smithing]]<br>'
end
+
    end
return s
+
    return s
 
end
 
end
  
 
local function cookingSource(id)
 
local function cookingSource(id)
local s = ""
+
    local s = ""
local item = getItem(id)
+
    local item = getItem(id)
if (item.class == "cooking-ingredient" and not item.ingredientTags) or item.class == 'cookedFish' or item.name=='Ashes' then
+
    if (item.class == "cooking-ingredient" and not item.ingredientTags) or item.class == 'cookedFish' or item.name=='Ashes' then
s = '[[Cooking]]<br>'
+
        s = '[[Cooking]]<br>'
end
+
    end
return s
+
    return s
 
end
 
end
  
 
local function runecraftingSource(id)
 
local function runecraftingSource(id)
local s = ""
+
    local s = ""
local item = getItem(id)
+
    local item = getItem(id)
if item.class == "cloth" or (item.class == 'rune' and item.requiredResources) then
+
    if item.class == "cloth" or (item.class == 'rune' and item.requiredResources) then
s = '[[Runecrafting]]<br>'
+
        s = '[[Runecrafting]]<br>'
end
+
    end
return s
+
    return s
 
end
 
end
  
 
local function scrollcraftingSource(id)
 
local function scrollcraftingSource(id)
local s = ""
+
    local s = ""
local item = getItem(id)
+
    local item = getItem(id)
if item.class == "enchanted-scroll" and item.level and item.level < 100 then
+
    if item.class == "enchanted-scroll" and item.level and item.level < 100 then
s = '[[Scrollcrafting]]<br>'
+
        s = '[[Scrollcrafting]]<br>'
end
+
    end
return s
+
    return s
 
end
 
end
 
  
 
local function craftingSource(id)
 
local function craftingSource(id)
local s = ""
+
    local s = ""
local item = getItem(id)
+
    local item = getItem(id)
if item.craftable then
+
    if item.craftable then
s = '[[Crafting]]<br>'
+
        s = '[[Crafting]]<br>'
end
+
    end
return s
+
    return s
 
end
 
end
  
 
local function findSource(id)
 
local function findSource(id)
local s = ""
+
    local s = ""
s = s .. gatheringSource(id)
+
    s = s .. gatheringSource(id)
-- s = s .. farmingSource(id)
+
    -- s = s .. farmingSource(id)
s = s .. scrollcraftingSource(id)
+
    s = s .. scrollcraftingSource(id)
s = s .. runecraftingSource(id)
+
    s = s .. runecraftingSource(id)
s = s .. smithingSource(id)
+
    s = s .. smithingSource(id)
s = s .. craftingSource(id)
+
    s = s .. craftingSource(id)
s = s .. cookingSource(id)
+
    s = s .. cookingSource(id)
if s:len() > 4 then  
+
    s = s:gsub("<br>$", "")
s = s:sub(1,s:len()-4)
+
    return s
end
+
end
return s
+
 
 +
---@param item Item
 +
---@return integer
 +
local function getItemTier(item)
 +
    local maxRequiredLevel
 +
    if item.requiredLevel then
 +
        for _, level in pairs(item.requiredLevel) do
 +
            if maxRequiredLevel == nil or maxRequiredLevel < level then
 +
                maxRequiredLevel = level
 +
            end
 +
        end
 +
        maxRequiredLevel = math.floor(math.floor(maxRequiredLevel / 10))
 +
    end
 +
    return item.overrideItemTier or maxRequiredLevel or item.enchantmentTier or 1
 +
end
 +
 
 +
local function getDustId(tier)
 +
    local AFFIX_DUST_PER_ITEM_TIER = {
 +
        550,
 +
        550,
 +
        550,
 +
        551,
 +
        552,
 +
        553,
 +
        554,
 +
        554,
 +
        554,
 +
    }
 +
    tier = (tonumber(tier) or 0) + 1 -- +1 for lua indexing
 +
    tier = math.floor(tier)
 +
    tier = math.min(math.max(tier, 1), 9)
 +
    return AFFIX_DUST_PER_ITEM_TIER[tier]
 +
end
 +
 
 +
---@param rarity string|nil
 +
---@return integer
 +
local function getScrapId(rarity)
 +
    rarity = rarity or "common"
 +
    local scrapIds = {
 +
        common = 555,
 +
        uncommon = 556,
 +
        rare = 557,
 +
        epic = 558,
 +
        legendary = 559
 +
    }
 +
    return scrapIds[rarity]
 +
end
 +
 
 +
---@param args table
 +
---@param title string
 +
---@param id string|number|false|nil # Primary loot id, skipped if falsy
 +
---@param scrapping table|nil # data.chance, data.itemID (AugmentingLoot scrappingSuccess or scrappingFail)
 +
local function addScrapping(args, title, id, scrapping)
 +
    local text = id and (itemImage(id, true) .. "<br>") or ""
 +
    if scrapping then
 +
        if scrapping.chance and scrapping.chance ~= 1 then
 +
            text = text .. formatChance(scrapping.chance) .. " "
 +
        end
 +
        if scrapping.minimum and scrapping.maximum then
 +
            text = text .. scrapping.minimum .. "–" .. scrapping.maximum .. " "
 +
        end
 +
        text = text .. itemImage(scrapping.itemID, true)
 +
    end
 +
    args[sl()] = title
 +
    args[sd()] = text
 +
end
 +
 
 +
---@param args table # infobox args table
 +
---@param augTransform boolean # true for augmenting, false for researching
 +
---@param transformsFrom table<number, ItemTransform>|nil # table of source ID as key, and list of transforms as value
 +
---@param transformsTo ItemTransform[]|nil # list of transforms
 +
local function addTransforms(args, augTransform, transformsFrom, transformsTo)
 +
    if not (transformsFrom or transformsTo) then return end
 +
 
 +
    ---@param transform ItemTransform
 +
    ---@param id number|false|nil # uses this for item's image instead of transform.newItemID
 +
    local function createTransform(transform, id)
 +
        if (transform.augmentingTransform or false) == augTransform then
 +
            local image = itemImage(id or transform.newItemID, true)
 +
            if transform.augmentingTransform then
 +
                return image .. " at level " .. (transform.augmentationLevel or 1)
 +
            else
 +
                return formatChance(transform.chance) .. " " .. image
 +
            end
 +
        end
 +
    end
 +
 
 +
    ---@param transforms table<number, ItemTransform>
 +
    ---@param isKeyId boolean|nil # wether the table keys are item IDs or indicies
 +
    local function createTransformList(transforms, isKeyId)
 +
        local list = {}
 +
        for i, transform in pairs(transforms) do
 +
            local text = createTransform(transform, isKeyId and i)
 +
            table.insert(list, text)
 +
        end
 +
        return list
 +
    end
 +
 
 +
    args[sbreak()] = "yes"
 +
 
 +
    local list
 +
    if transformsFrom then
 +
        list = createTransformList(transformsFrom, true)
 +
        args[sl()] = "Transforms from"
 +
        args[sd()] = table.concat(list, "<br>")
 +
    end
 +
    if transformsTo then
 +
        list = createTransformList(transformsTo)
 +
        args[sl()] = "Transforms to"
 +
        args[sd()] = table.concat(list, "<br>")
 +
    end
 
end
 
end
  
 +
---@param args table
 +
---@param item Item
 +
---@param transformsTo ItemTransform[]|nil # list of transforms
 +
---@param transformsFrom table<number, ItemTransform>|nil # list of transforms
 +
---@return boolean
 +
local function addResearch(args, item, transformsTo, transformsFrom)
 +
    local isCup = hasValue(cups, item.id)
 +
    local canResearch = not getGeneralShopItem(item.id) or isCup
 +
    local craftAug = getCraftAug(item.id)
 +
 +
    if craftAug and canResearch then
 +
        local augLoot = getAugLoot(tostring(item.id))
 +
        local tier = getItemTier(item)
 +
        local dustId = not item.noDustFromResearching and getDustId(tier) or nil
 +
 +
        if not item.noScrapFromResearching then
 +
            local success = augLoot and augLoot.scrappingSuccess or nil
 +
            addScrapping(args, "Success", dustId, success)
 +
        end
 +
 +
        -- General Shop items are hardcoded to never fail (CUPS atm)
 +
        if not isCup then
 +
            local scrapId = item.researchesIntoDust and dustId or getScrapId(item.rarity)
 +
            local fail = augLoot and augLoot.scrappingFail or nil
 +
            addScrapping(args, "Fail", scrapId, fail)
 +
        end
 +
    end
 +
 +
    addTransforms(args, false, transformsTo, transformsFrom)
 +
 +
    return craftAug and canResearch or transformsFrom ~= nil
 +
end
 +
 +
---@param prefix 'offensiveDamageAffinity'|'offensiveAccuracyAffinityRating'|'defensiveDamageAffinity'
 +
---@param description string|nil
 +
---@param uppercase boolean|nil
 +
---@return table<string, string>
 +
local function createAffinityDictionary(prefix, description, uppercase)
 +
    description = description or ""
 +
    uppercase = uppercase or false
 +
 +
    local allDamageTypes = {
 +
        "Melee",
 +
        "Magic",
 +
        "Range",
 +
        "Piercing",
 +
        "Blunt",
 +
        "Slashing",
 +
        "Chaos",
 +
        "Nature",
 +
        "Fire",
 +
        "Ice",
 +
        "Lightning",
 +
        "Poison",
 +
        "Typeless",
 +
        "Heal"
 +
    }
 +
 +
    local affinities = {}
 +
    for _, damageType in ipairs(allDamageTypes) do
 +
        local key = string.format("%s.%s", prefix, damageType)
 +
        local val = string.format("%s %s", uppercase and damageType:upper() or damageType, description)
 +
        local affinity = val:gsub("%s+$", "")
 +
        affinities[key] = affinity
 +
    end
 +
    return affinities
 +
end
 +
 +
local function getLabelFromAugBonus(bonusName)
 +
    local equipmentStatsToLabelMapping = {
 +
        ["weaponBonus.strength"] = "Strength",
 +
        ["weaponBonus.intellect"] = "Intellect",
 +
        ["weaponBonus.dexterity"] = "Dexterity",
 +
        ["offensiveCritical.chance"] = "Crit Chance",
 +
        ["offensiveCritical.damageMultiplier"] = "Crit Mult",
 +
        ["armorBonus.protection"] = "Protection",
 +
        ["armorBonus.resistance"] = "Resistance",
 +
        ["armorBonus.agility"] = "Agility",
 +
        ["armorBonus.stamina"] = "Stamina",
 +
        ["defensiveCritical.chance"] = "Crit Avoidance",
 +
        ["defensiveCritical.damageMultiplier"] = "Crit Reduction",
 +
        ["toolBoost.fishing"] = "Fishing",
 +
        ["toolBoost.fishingBaitPower"] = "Bait Power",
 +
        ["toolBoost.fishingReelPower"] = "Reel Power",
 +
        ["toolBoost.fishingRarityPower"] = "Bonus Rarity",
 +
        ["toolBoost.mining"] = "Mining",
 +
        ["toolBoost.foraging"] = "Foraging",
 +
        ["toolBoost.farming"] = "Farming",
 +
        ["toolBoost.cooking"] = "Cooking",
 +
        ["toolBoost.smithing"] = "Smithing",
 +
        ["toolBoost.enchanting"] = "Enchanting",
 +
        ["toolBoost.runecrafting"] = "Runecrafting"
 +
    }
 +
    for k, v in pairs(createAffinityDictionary("offensiveDamageAffinity", "") or {}) do
 +
        equipmentStatsToLabelMapping[k] = v
 +
    end
 +
    for k, v in pairs(createAffinityDictionary("offensiveAccuracyAffinityRating", "Accuracy") or {}) do
 +
        equipmentStatsToLabelMapping[k] = v
 +
    end
 +
    for k, v in pairs(createAffinityDictionary("defensiveDamageAffinity", "") or {}) do
 +
        equipmentStatsToLabelMapping[k] = v
 +
    end
 +
 +
    return equipmentStatsToLabelMapping[bonusName] or bonusName
 +
end
 +
 +
---@param item Item
 
local function createInfobox(item)
 
local function createInfobox(item)
local args = {}
+
    local lang = mw.language.getContentLanguage()
local url = ""
+
    local args = {}
local s = ""
+
    args.autoheaders = "y"
args.autoheaders = "y"
+
    args.subbox = "no"
args.subbox = "no"
+
    args.bodystyle = " "
args.bodystyle = " "
+
    args.title = item.name
args.title = item.name
+
 
+
    local url = item.itemIcon or item.itemImage
if item.itemIcon then
+
    args.image = '<img src="' .. fullUrl(url) .. '" width="150">'
url = item.itemIcon
+
 
else
+
    if item.value then
url = item.itemImage
+
        args[l()] = "Vendor Value"
end
+
        args[d()] = lang:formatNum(item.value) .. " " .. icon('Gold', "/images/gold_coin.png", false, 13)
args.image = "<img src=\"" .. fullUrl(url) .. "\" width=\"150\">"
+
    end
+
 
if item.value then
+
    --TODO: add this back when the market is fixed
args[l()] = icon('Gold', "/images/gold_coin.png")
+
    --[[if item.tradeable then
args[d()] = addSeparator(item.value)
+
        args[l()] = icon('Market', "/images/ui/marketplace_icon.png")
end
+
        local market = require("Module:Market")["_price"]({item.name, 1, 1})
+
        if market then
if item.tradeable then
+
            args[d()] = addSeparator(market)
args[l()] = icon('Market', "/images/ui/marketplace_icon.png")
+
        else
local market = require("Module:Market")["_price"]({item.name, 1, 1})
+
            args[d()] = "Yes"
if market then
+
        end
args[d()] = addSeparator(market)
+
    end]]
else
+
 
args[d()] = "Yes"
+
    if item.rarity then
end
+
        args[l()] = "Rarity"
end
+
        args[d()] = item.rarity:gsub("^%l", string.upper)
+
    end
if item.requiredLevel then
+
 
s = ""
+
    if item.requiredLevel then
for skill, level in pairs(item.requiredLevel) do
+
        local levels = {}
s = s .. level .. " " .. skill .. "<br>"
+
        for skill, level in pairs(item.requiredLevel) do
end
+
            skill = skill:gsub("^%l", string.upper)
s = s:sub(1,s:len()-4)
+
            table.insert(levels, level .. " " .. skill)
args[l()] = "Level Required"
+
        end
args[d()] = s
+
        args[l()] = "Level"
end
+
        args[d()] = table.concat(levels, "<br>")
+
    end
args[l()] = "Source"
+
 
args[d()] = findSource(item.id)
+
    --TODO: findSource doesn't work so it's disabled for now. Does need a big rework
+
    --args[l()] = "Source"
if item.heat then
+
    --args[d()] = findSource(item.id)
args[l()] = itemImage(2)
+
 
args[d()] = addSeparator(item.heat)
+
    if item.heat then
end
+
        args[l()] = itemImage(2, false)
+
        args[d()] = lang:formatNum(item.heat)
local stats = item.equipmentStats
+
    end
if stats then
+
 
args[l()] = "Slot"
+
    if item.forcedEnchant then
args[d()] = stats.slot
+
        args[l()] = "Enchantment"
+
        args[d()] = "[[" .. getEnchantName(item.forcedEnchant) .. "]]"
if item.enchantmentTier then
+
 
args[l()] = "Enchantment Slots"
+
        args[l()] = "Enchantment Level"
args[d()] = item.enchantmentTier
+
        args[d()] = item.forcedEnchantAmount
end
+
    end
+
 
if item.forcedEnchant then
+
    if item.enchantmentTier then
args[l()] = "Enchantments"
+
        args[l()] = "Enchantment Slots"
args[d()] = "[[" .. getEnchantmentName(item.forcedEnchant) .. "]]"
+
        args[d()] = item.enchantmentTier
end
+
    end
+
 
if stats.toolBoost then
+
    local stats = item.equipmentStats
s = ""
+
    if stats then
for key, stat in pairs(stats.toolBoost) do
+
        local slot = stats.slot and stats.slot:gsub("^%l", string.upper)
s = s .. stat.boost .. " "
+
        args[l()] = "Slot"
s = s .. stat.skill .. "<br>"
+
        args[d()] = stats.slot and slot
end
+
 
s = s:sub(1,s:len()-4)
+
        if stats.oneHanded == false then
args[l()] = "Stats"
+
            args[l()] = "Two-handed"
args[d()] = s
+
            args[d()] = "Yes"
end
+
        end
+
 
if stats.oneHanded == false then
+
        if stats.attackSpeed then
args[l()] = "Two-handed"
+
            args[l()] = "Attack Speed"
args[d()] = "Yes"
+
            args[d()] = stats.attackSpeed .. "s"
end
+
        end
+
 
if stats.attackSpeed then
+
        if stats.toolBoost then
args[l()] = "Attack Speed"
+
            args[h()] = "Tool Stats"
args[d()] = stats.attackSpeed
+
 
end
+
            for _, stat in ipairs(stats.toolBoost) do
+
                if stat.boost ~= 0 then
+
                    local sign = stat.boost > 0 and "+" or ""
args[h()] = "Offensive Stats"
+
                    local boost = sign .. stat.boost
+
 
stat = stats.offensiveCritical
+
                    args[l()] = getLabelFromAugBonus("toolBoost." .. stat.skill)
if stat then
+
                    args[d()] = boost
args[l()] = "Crit Chance"
+
                end
args[d()] = stat.chance
+
            end
args[l()] = "Crit Multiplier"
+
        end
args[d()] = stat.damageMultiplier
+
 
end
+
        args[h()] = "Offensive Stats"
+
 
stat = stats.weaponBonus
+
        local stat = stats.offensiveCritical
if stat then
+
        if stat then
args[sl()] = "Str"
+
            args[l()] = "Crit Chance"
args[sd()] = stat.strength
+
            args[d()] = toFixed(stat.chance * 100, 3) .. "%"
args[sl()] = "Int"
+
            args[l()] = "Crit Multiplier"
args[sd()] = stat.intellect
+
            args[d()] = stat.damageMultiplier .. "x"
args[sl()] = "Dex"
+
        end
args[sd()] = stat.dexterity
+
 
end
+
        stat = stats.weaponBonus
+
        if stat then
args[h()] = "Offensive Affinity"
+
            args[sl()] = "Str"
stat = stats.offensiveDamageAffinity
+
            args[sd()] = stat.strength
if stat then
+
            args[sl()] = "Int"
args[sl()] = "Melee"
+
            args[sd()] = stat.intellect
args[sd()] = stat.Melee and stat.Melee * 100 .. "%"
+
            args[sl()] = "Dex"
args[sl()] = "Magic"
+
            args[sd()] = stat.dexterity
args[sd()] = stat.Magic and stat.Magic * 100 .. "%"
+
        end
args[sl()] = "Range"
+
 
args[sd()] = stat.Range and stat.Range * 100 .. "%"
+
        args[h()] = "Offensive Affinity"
args[sbreak()] = "yes"
+
        stat = stats.offensiveDamageAffinity
+
        if stat then
args[sl()] = "Piercing"
+
            args[sl()] = "Melee"
args[sd()] = stat.Piercing and stat.Piercing * 100 .. "%"
+
            args[sd()] = stat.Melee and toFixed(stat.Melee * 100 - 100, 3) .. "%"
args[sl()] = "Blunt"
+
            args[sl()] = "Magic"
args[sd()] = stat.Blunt and stat.Blunt * 100 .. "%"
+
            args[sd()] = stat.Magic and toFixed(stat.Magic * 100 - 100, 3) .. "%"
args[sl()] = "Slashing"
+
            args[sl()] = "Range"
args[sd()] = stat.Slashing and stat.Slashing * 100 .. "%"
+
            args[sd()] = stat.Range and toFixed(stat.Range * 100 - 100, 3) .. "%"
args[sl()] = "Fire"
+
            args[sbreak()] = "yes"
args[sd()] = stat.Fire and stat.Fire * 100 .. "%"
+
 
args[sl()] = "Ice"
+
            args[sl()] = "Piercing"
args[sd()] = stat.Ice and stat.Ice * 100 .. "%"
+
            args[sd()] = stat.Piercing and toFixed(stat.Piercing * 100 - 100, 3) .. "%"
args[sl()] = "Nature"
+
            args[sl()] = "Blunt"
args[sd()] = stat.Nature and stat.Nature * 100 .. "%"
+
            args[sd()] = stat.Blunt and toFixed(stat.Blunt * 100 - 100, 3) .. "%"
args[sl()] = "Chaos"
+
            args[sl()] = "Slashing"
args[sd()] = stat.Chaos and stat.Chaos * 100 .. "%"
+
            args[sd()] = stat.Slashing and toFixed(stat.Slashing * 100 - 100, 3) .. "%"
args[sbreak()] = "yes"
+
            args[sl()] = "Fire"
end
+
            args[sd()] = stat.Fire and toFixed(stat.Fire * 100 - 100, 3) .. "%"
+
            args[sl()] = "Ice"
args[h()] = "Accuracy"
+
            args[sd()] = stat.Ice and toFixed(stat.Ice * 100 - 100, 3) .. "%"
stat = stats.offensiveAccuracyAffinityRating
+
            args[sl()] = "Nature"
if stat then
+
            args[sd()] = stat.Nature and toFixed(stat.Nature * 100 - 100, 3) .. "%"
args[sl()] = "Melee"
+
            args[sl()] = "Chaos"
args[sd()] = stat.Melee
+
            args[sd()] = stat.Chaos and toFixed(stat.Chaos * 100 - 100, 3) .. "%"
args[sl()] = "Magic"
+
            args[sbreak()] = "yes"
args[sd()] = stat.Magic
+
        end
args[sl()] = "Range"
+
 
args[sd()] = stat.Range
+
        args[h()] = "Accuracy"
args[sbreak()] = "yes"
+
        stat = stats.offensiveAccuracyAffinityRating
+
        if stat then
args[sl()] = "Piercing"
+
            args[sl()] = "Melee"
args[sd()] = stat.Piercing
+
            args[sd()] = stat.Melee
args[sl()] = "Blunt"
+
            args[sl()] = "Magic"
args[sd()] = stat.Blunt
+
            args[sd()] = stat.Magic
args[sl()] = "Slashing"
+
            args[sl()] = "Range"
args[sd()] = stat.Slashing
+
            args[sd()] = stat.Range
args[sl()] = "Fire"
+
            args[sbreak()] = "yes"
args[sd()] = stat.Fire
+
 
args[sl()] = "Ice"
+
            args[sl()] = "Piercing"
args[sd()] = stat.Ice
+
            args[sd()] = stat.Piercing
args[sl()] = "Nature"
+
            args[sl()] = "Blunt"
args[sd()] = stat.Nature
+
            args[sd()] = stat.Blunt
args[sl()] = "Chaos"
+
            args[sl()] = "Slashing"
args[sd()] = stat.Chaos
+
            args[sd()] = stat.Slashing
args[sbreak()] = "yes"
+
            args[sl()] = "Fire"
end
+
            args[sd()] = stat.Fire
+
            args[sl()] = "Ice"
args[h()] = "Defensive Stats"
+
            args[sd()] = stat.Ice
+
            args[sl()] = "Nature"
stat = stats.defensiveCritical
+
            args[sd()] = stat.Nature
if stat then
+
            args[sl()] = "Chaos"
args[l()] = "Crit Avoidance"
+
            args[sd()] = stat.Chaos
args[d()] = stat.chance
+
            args[sbreak()] = "yes"
args[l()] = "Crit Reduction"
+
        end
args[d()] = stat.damageMultiplier
+
 
end
+
        args[h()] = "Defensive Stats"
+
 
stat = stats.armorBonus
+
        stat = stats.defensiveCritical
if stat then
+
        if stat then
args[sl()] = "Protection"
+
            args[l()] = "Crit Avoidance"
args[sd()] = stat.protection
+
            args[d()] = toFixed(stat.chance * 100, 3) .. "%"
args[sl()] = "Resistance"
+
            args[l()] = "Crit Reduction"
args[sd()] = stat.resistance
+
            args[d()] = stat.damageMultiplier
args[sl()] = "Agility"
+
        end
args[sd()] = stat.agility
+
 
args[sl()] = "Stamina"
+
        stat = stats.armorBonus
args[sd()] = stat.stamina
+
        if stat then
end
+
            args[sl()] = "Protection"
+
            args[sd()] = stat.protection
args[h()] = "Defensive Affinity"
+
            args[sl()] = "Resistance"
stat = stats.defensiveDamageAffinity
+
            args[sd()] = stat.resistance
if stat then
+
            args[sl()] = "Agility"
args[sl()] = "Melee"
+
            args[sd()] = stat.agility
args[sd()] = stat.Melee and stat.Melee * 100 .. "%"
+
            args[sl()] = "Stamina"
args[sl()] = "Magic"
+
            args[sd()] = stat.stamina
args[sd()] = stat.Magic and stat.Magic * 100 .. "%"
+
        end
args[sl()] = "Range"
+
 
args[sd()] = stat.Range and stat.Range * 100 .. "%"
+
        args[h()] = "Defensive Affinity"
args[sbreak()] = "yes"
+
        stat = stats.defensiveDamageAffinity
+
        if stat then
args[sl()] = "Piercing"
+
            args[sl()] = "Melee"
args[sd()] = stat.Piercing and stat.Piercing * 100 .. "%"
+
            args[sd()] = stat.Melee and toFixed(stat.Melee * 100 - 100, 3) .. "%"
args[sl()] = "Blunt"
+
            args[sl()] = "Magic"
args[sd()] = stat.Blunt and stat.Blunt * 100 .. "%"
+
            args[sd()] = stat.Magic and toFixed(stat.Magic * 100 - 100, 3) .. "%"
args[sl()] = "Slashing"
+
            args[sl()] = "Range"
args[sd()] = stat.Slashing and stat.Slashing * 100 .. "%"
+
            args[sd()] = stat.Range and toFixed(stat.Range * 100 - 100, 3) .. "%"
args[sl()] = "Fire"
+
            args[sbreak()] = "yes"
args[sd()] = stat.Fire and stat.Fire * 100 .. "%"
+
 
args[sl()] = "Ice"
+
            args[sl()] = "Piercing"
args[sd()] = stat.Ice and stat.Ice * 100 .. "%"
+
            args[sd()] = stat.Piercing and toFixed(stat.Piercing * 100 - 100, 3) .. "%"
args[sl()] = "Nature"
+
            args[sl()] = "Blunt"
args[sd()] = stat.Nature and stat.Nature * 100 .. "%"
+
            args[sd()] = stat.Blunt and toFixed(stat.Blunt * 100 - 100, 3) .. "%"
args[sl()] = "Chaos"
+
            args[sl()] = "Slashing"
args[sd()] = stat.Chaos and stat.Chaos * 100 .. "%"
+
            args[sd()] = stat.Slashing and toFixed(stat.Slashing * 100 - 100, 3) .. "%"
args[sbreak()] = "yes"
+
            args[sl()] = "Fire"
end
+
            args[sd()] = stat.Fire and toFixed(stat.Fire * 100 - 100, 3) .. "%"
end
+
            args[sl()] = "Ice"
 +
            args[sd()] = stat.Ice and toFixed(stat.Ice * 100 - 100, 3) .. "%"
 +
            args[sl()] = "Nature"
 +
            args[sd()] = stat.Nature and toFixed(stat.Nature * 100 - 100, 3) .. "%"
 +
            args[sl()] = "Chaos"
 +
            args[sd()] = stat.Chaos and toFixed(stat.Chaos * 100 - 100, 3) .. "%"
 +
            args[sbreak()] = "yes"
 +
        end
 +
 
 +
        args[h()] = "Abilities"
 +
        args[sd()] = createAbilityRotation(stats.grantedAbility, true)
 +
 
 +
        args[h()] = "Set Bonus"
 +
        for id, desc in pairs(getModuleItemSet()._formatItemSets(stats.itemSet)) do
 +
            local enchant = getEnchant(id)
 +
            local counts = {}
 +
            for _, req in ipairs(enchant.setRequirements) do
 +
                if (req.strength > 0) then
 +
                    table.insert(counts, req.count)
 +
                end
 +
            end
 +
            args[sl()] = "[[" .. enchant.name .. "]] [" .. table.concat(counts, ", ") .. "]"
 +
            args[sd()] = desc
 +
            args[sbreak()] = "yes"
 +
        end
 +
    end
 +
 
 +
    local ammoStat = item.ammunitionMults
 +
    if ammoStat then
 +
        args[h()] = "Ammo Stats"
 +
        args[sl()] = 'Type'
 +
        args[sd()] = ammoStat.style
 +
        args[sl()] = 'Damage'
 +
        args[sd()] = ammoStat.damageMult .. 'x'
 +
        args[sl()] = 'Accuracy'
 +
        args[sd()] = ammoStat.accuracyMult .. 'x'
 +
    end
 +
 
 +
    ---@return table|nil
 +
    local function getAugmentingByID(id)
 +
        local craftAug = getCraftAug(id)
 +
        if not craftAug then return nil end
 +
        return craftAug and craftAug.augmenting;
 +
    end
 +
 
 +
    ---@return table|nil
 +
    local function getScrappingByID(id)
 +
        local craftAug = getCraftAug(id)
 +
        if not craftAug then return nil end
 +
        return craftAug.scrapping or getAugmentingByID(id)
 +
    end
 +
 
 +
    local function canAugment(id)
 +
        return not item.blockAugmenting and getAugmentingByID(id)
 +
    end
 +
 
 +
    local function canScrap(id)
 +
        return not item.blockResearching and getScrappingByID(id)
 +
    end
 +
 
 +
    --TODO: add transform source
 +
    args[h()] = "Enchanting"
 +
 
 +
    if getScrappingByID(item.id) then
 +
        if canAugment(item.id) or canScrap(item.id) then
 +
            args[l()] = dottedTooltip("Base XP", "Experience is increased by 10% for every augmentation level.")
 +
            args[d()] = lang:formatNum(20 * math.pow(getItemTier(item), 2))
 +
        end
 +
 
 +
        local craftAug = getCraftAug(item.id)
 +
        local costs = {}
 +
        for id, cost in pairs(craftAug.scrapping or {}) do
 +
            table.insert(costs, {id = id, text = lang:formatNum(cost) .. " " .. itemImage(id, true)})
 +
        end
 +
        table.sort(costs, function(a, b)
 +
            return a.id < b.id
 +
        end)
 +
        for i = 1, #costs do
 +
            costs[i] = costs[i].text
 +
        end
 +
 
 +
        args[sl()] = "Cost"
 +
        args[sd()] = table.concat(costs, "<br>")
 +
 
 +
        ---@type table<string, AugmentingLoot>|nil
 +
        local augLoot = getAugLoot()
 +
        ---@type AugmentingLoot|nil
 +
        local augLootItem = augLoot and augLoot[tostring(item.id)]
 +
        local transformsTo = augLootItem and augLootItem.transforms
 +
        ---@type table<number, ItemTransform>|nil # the key is the id of the item that transforms
 +
        local transformsFrom
 +
        for id, keys in pairs(augLoot or {}) do
 +
            for _, transform in ipairs(keys.transforms or {}) do
 +
                if transform.newItemID == item.id then
 +
                    transformsFrom = transformsFrom or {}
 +
                    transformsFrom[tonumber(id)] = transform
 +
                end
 +
            end
 +
        end
 +
 
 +
        args[h()] = "Augmentation"
 +
        if canAugment(item.id) or transformsFrom then
 +
            if item.equipmentStats then
 +
                local bonuses = {}
 +
                for _, bonus in pairs(item.equipmentStats.augmentationBonus or {}) do
 +
                    table.insert(bonuses, "+" .. bonus.value .. " " .. getLabelFromAugBonus(bonus.stat))
 +
                end
 +
                args[sl()] = "Bonus Stats"
 +
                args[sd()] = table.concat(bonuses, "<br>")
 +
            end
 +
            addTransforms(args, true, transformsFrom, transformsTo)
 +
        end
 +
 
 +
        args[h()] = "Research"
 +
        if canScrap(item.id) or transformsFrom then
 +
            addResearch(args, item, transformsFrom, transformsTo)
 +
        end
 +
    end
 +
 
 +
    local crafting = item.craftingStats
 +
    if crafting then
 +
        args[h()] = "Crafting"
 +
 
 +
        args[l()] = "Category"
 +
        args[d()] = crafting.category
 +
 
 +
        args[l()] = "Craftable"
 +
        args[d()] = not crafting.craftable and "No" or ""
 +
 
 +
        args[l()] = "Level"
 +
        args[d()] = crafting.level
 +
 
 +
        args[l()] = "Experience"
 +
        args[d()] = crafting.experience and lang:formatNum(crafting.experience) or ""
 +
 
 +
        args[l()] = "Amount"
 +
        args[d()] = crafting.multiplier
 +
 
 +
        args[sl()] = "Description"
 +
        args[sd()] = crafting.description
 +
            and '<p style="margin:auto;font-style:italic">' .. crafting.description .. '</p>' or ""
 +
    end
 +
 
 +
    local ingredient = getCookingIngredient(item.id)
 +
    if ingredient then
 +
        args[h()] = "Cooking"
  
local ammoStat = item.ammunitionMults
+
        args[l()] = "Level"
 +
        args[d()] = ingredient.level
  
if ammoStat then
+
        args[l()] = "Difficulty"
args[h()] = "Ammo Stats"
+
        args[d()] = ingredient.difficulty
args[sl()] = 'Type'
+
 
args[sd()] = ammoStat.style
+
        args[l()] = "Size"
args[sl()] = 'Damage'
+
        args[d()] = ingredient.size
args[sd()] = ammoStat.damageMult .. 'x'
+
 
args[sl()] = 'Accuracy'
+
        args[l()] = "Alchemy Size"
args[sd()] = ammoStat.accuracyMult .. 'x'
+
        args[d()] = ingredient.alchemySize
end
+
 
+
        args[l()] = "Category"
args[h()] = "Augment"
+
        args[d()] = table.concat(ingredient.ingredientTags or {}, "<br>")
local craftAug = getCraftAug(item.id)
+
 
+
        args[l()] = "Buff"
if craftAug and craftAug.augmenting then
+
        args[d()] = getEnchantName(ingredient.cookingEnchantment) or getEnchantName(ingredient.alchemyEnchantment) or ""
if item.equipmentStats then
+
    end
s = ""
+
 
for key, bonus in pairs(item.equipmentStats.augmentationBonus) do
+
    local farming = item.farmingStats
s = s .. "+" .. bonus.value .. " "
+
    if farming then
s = s .. bonus.stat:sub(bonus.stat:find('%.')+1,bonus.stat:len()) .. "<br>"
+
        local drops = {}
end
+
        local yield = copyTable(getYield(item.id) or {})
s = s:sub(1,s:len()-4)
+
        table.sort(yield, function(a, b)
args[sl()] = "Aug Bonus"
+
            return a.chance > b.chance
args[sd()] = s
+
        end)
end
+
        for _, drop in ipairs(yield) do
+
            table.insert(drops, string.format(
s = ""
+
                "%s–%s %s %s%%",
for key, cost in pairs(craftAug.augmenting) do
+
                drop.min,
s = s .. cost .. " "
+
                drop.max,
s = s .. itemImage(key, true) .. "<br>"
+
                itemImage(drop.id, true),
end
+
                toFixed(drop.chance * 100, 2)
s = s:sub(1,s:len()-4)
+
            ))
args[sl()] = "Aug Cost"
+
        end
args[sd()] = s
+
 
end
+
        args[h()] = "Farming"
+
 
args[h()] = "Cooking"
+
        args[l()] = "Level"
+
        args[d()] = farming.requiredLevel
if item.size then
+
 
args[l()] = "Size"
+
        args[l()] = "Experience"
args[d()] = item.size
+
        args[d()] = lang:formatNum(farming.experience)
end
+
 
+
        args[l()] = "Plot Size"
if item.difficulty then
+
        args[d()] = string.format(
args[l()] = "Difficulty"
+
            "%dx%d%s",
args[d()] = item.difficulty
+
            farming.width,
end
+
            farming.height,
+
            farming.maxWidth and string.format(" – %dx%d", farming.maxWidth, farming.maxHeight) or ""
if item.ingredientTags then
+
        )
s = ""
+
 
for key, tag in pairs(item.ingredientTags) do
+
        args[l()] = "Harvest Time"
s = s .. tag .. "<br>"
+
        args[d()] = farming.time .. " minutes"
end
+
 
s = s:sub(1,s:len()-4)
+
        args[l()] = "Yield"
args[l()] = "Category"
+
        args[d()] = table.concat(drops, "<br>")
args[d()] = s
+
    end
end
+
 
+
    if item.extraTooltipInfo then
if item.cookingEnchantment then
+
        args[h()] = "Tooltip"
args[l()] = "Buff"
+
        args[d()] = '<p style="margin:auto;font-style:italic;font-size:1.2em">' .. item.extraTooltipInfo .. '</p>'
args[d()] = getEnchantmentName(item.cookingEnchantment)
+
    end
end
+
 
+
    for key, data in pairs(args) do
args[h()] = "Seeds"
+
        if string.find(key, "data") then
+
            args[key] = tostring(data)
local farming = item.farmingStats
+
        end
if farming then
+
    end
local farmingData = p.loadData('farming')
+
 
local seedData = farmingData[item.id]
+
    return require('Module:Infobox').infobox(args)
if seedData then
 
s = ""
 
for key, yield in pairs(seedData) do
 
s = s .. yield.min .. "-" .. yield.max .. " "
 
s = s .. itemImage(yield.id, true)
 
if yield.chance ~= 1 then
 
s = s .. " " .. tonumber(string.format('%.2f', yield.chance * 100)) .. "%"
 
end
 
s = s .. "<br>"
 
end
 
s = s:sub(1,s:len()-4)
 
args[l()] = "Level Required"
 
args[d()] = farming.requiredLevel
 
args[l()] = "Experience"
 
args[d()] = addSeparator(farming.experience)
 
args[l()] = "Plot Size"
 
args[d()] = farming.height .. "x" .. farming.width
 
args[l()] = "Harvest Time"
 
args[d()] = farming.time .. " minutes"
 
args[l()] = "Yield"
 
args[d()] = s
 
end
 
end
 
 
local args2 = {}
 
 
args2.subbox = "yes"
 
args2.bodystyle = "padding: 0.5em; margin:auto; font-style:italic; font-size:110%; text-align: center"
 
args2.data1 = item.extraTooltipInfo
 
args[h()] = "Tooltip"
 
args[d()] = require('Module:Infobox').infobox(args2)
 
 
for key, data in pairs(args) do
 
if string.find(key, "data") then
 
args[key] = tostring(data)
 
end
 
end
 
 
return require('Module:Infobox').infobox(args)
 
 
end
 
end
  
 
function p.item(frame)
 
function p.item(frame)
local args = frame:getParent().args
+
    local args = frame:getParent().args
return p._item(args)
+
    return p._item(args)
 
end
 
end
  
 
function p._item(args)
 
function p._item(args)
local name = ""
+
    local name = args.name or args.title or args[1] or mw.title.getCurrentTitle().text
local item = 0
+
    local item = findItem(name)
local infobox = ""
+
 
+
    if not item then
if args[1] then
+
        return "<div style=\"color:red\"> No item named '" .. name .. "'</div>. The Module:Items/data maybe outdated."
name = args[1]
+
    end
else
+
 
name = mw.title.getCurrentTitle().text
+
    return createInfobox(item)
end
 
 
item = findItem(name)
 
 
if item == 0 then
 
return "<div style=\"color:red\"> No item named '" .. name .. "'</div>. The Module:Items/data maybe outdated."
 
end
 
 
infobox = createInfobox(item)
 
return infobox
 
 
end
 
end
  
 
return p
 
return p

Latest revision as of 13:47, 19 August 2025


local p = {}

local module_names = {
    item = 'Module:Items/data',
    enchantment = 'Module:Enchantment/data',
    location = 'Module:Location/data',
    craftaug = 'Module:CraftingAugmenting/data',
    farming = 'Module:Farming/data',
    augloot = 'Module:Augmenting loot/data',
    cooking = 'Module:CookingList/data',
    ability = 'Module:Abilities/data',
    gameshop = 'Module:GameShopItems/data',
    itemsets = 'Module:Item sets',
}

local loaded_modules = {}

local headerCount = 1
local labelCount = 1
local dataCount = 1

local cups = {
    16000,16001,16002,16003,16004,16005,16006,16007,
    16008,16009,16010,16011,16012,16013,16014,16015
}

---Checks if a table contains a specific value.
---@param t table
---@param value any
---@return boolean
local function hasValue(t, value)
    for _, v in pairs(t) do
        if value == v then
            return true
        end
    end
    return false
end

---Creates a deep copy of a tables and functions.
---Does not work with functions with upvalues and maybe userdata.
---@generic T
---@param t T The value to copy.
---@return T # A new value that is a deep copy of the original.
local function copyTable(t)
    if type(t) ~= "table" then
        return t
    elseif type(t) == "function" then
        return loadstring(string.dump(t))
    end
    local copy = {}
    for k, v in pairs(t) do
        copy[k] = copyTable(v)
    end
    return copy
end

---Formats a number up to a given number of decimal places, removing trailing zeros.
---@param num number|string The number to format.
---@param digits number The number of decimal places to include (must be a non-negative integer).
---@return string|nil # The formatted number as a string or nil if the input is not a valid number.
local function toFixed(num, digits)
    local n = tonumber(num)
    if not n then
        return nil
    end
    digits = math.max(0, math.floor(digits))
    local formatted = string.format("%." .. digits .. "f", n):gsub("%.?0+$", "")
    return formatted
end

local function h()
    local s = "header" .. headerCount
    headerCount = headerCount + 1
    labelCount = headerCount
    dataCount = headerCount
    return s
end

local function sbreak()
    local s = "sbreak" .. headerCount
    headerCount = headerCount + 1
    labelCount = headerCount
    dataCount = headerCount
    return s
end

local function l()
    local s = "label" .. labelCount
    dataCount = labelCount
    labelCount = labelCount + 1
    headerCount = labelCount
    return s
end

local function d()
    local s = "data" .. dataCount
    dataCount = dataCount + 1
    headerCount = dataCount
    labelCount = dataCount
    return s
end

local function sl()
    local s = "s" .. l()
    return s
end

local function sd()
    local s = "s" .. d()
    return s
end

---@param data_type string
---@return table
function p.loadData(data_type)
    local module_name = module_names[data_type]
    if loaded_modules[module_name] == nil then
        local status, module = pcall(mw.loadData, module_name)
        if not status then
            status, module = pcall(require, module_name)
        end
        if status then
            loaded_modules[module_name] = module
        else
            error("Failed to load module: " .. data_type .. " / " .. tostring(module_name) .. "\n" .. module)
        end
    end

    return loaded_modules[module_name]
end

local function getItem(id)
    return p.loadData("item")[tostring(id)]
end

local function getEnchant(id)
    return p.loadData("enchantment")[tostring(id)]
end

local function getEnchantName(id)
    local enchantment = getEnchant(id)
    return enchantment and enchantment.name
end

local function getModuleItemSet()
    return p.loadData("itemsets")
end

local function getItemName(id)
    return p.loadData("item")[tostring(id)].name
end

local function getCraftAug(id)
    return p.loadData("craftaug")[tostring(id)]
end

---@param id string|number
---@return CookingItem|nil
local function getCookingIngredient(id)
    return p.loadData("cooking")[tostring(id)]
end

---@param id string|number|nil # Returns all if nil
---@return AugmentingLoot|table<string, AugmentingLoot>|nil
local function getAugLoot(id)
    local augloot = p.loadData("augloot")
    return id == nil and augloot or augloot[tostring(id)]
end

---@param id string|number
---@return Ability|nil
local function getAbility(id)
    return p.loadData("ability")[tostring(id)]
end

---@param id string|number
---@return {id: number, min: number, max: number, chance: number}[]|nil
local function getYield(id)
    return p.loadData("farming")[tostring(id)]
end

---@param itemId string|number # Item ID, is not the same as the `id` in the shop.
---@return nil
local function getGeneralShopItem(itemId)
    local id = tonumber(itemId)
    if not id then return nil end

    local items = p.loadData("gameshop")
    for _, item in pairs(items) do
        if item.itemID == id then
            return item
        end
    end
end

---@return Item|nil
local function findItem(name)
    local lname = name:lower()

    --Remove leading and trailing spaces.
    lname = lname:gsub('^%s*(.-)%s*$', '%1')
    for key, item in pairs(p.loadData("item")) do
        if lname == item.name:lower() then
            return item
        end
    end
end

local function dottedTooltip(name, tooltip)
    return string.format(
        '<span class="rt-commentedText tooltip tooltip-dotted" title="%s">%s</span>',
        tooltip,
        name
    )
end

---Formats the given chance as a string presentation of percentage or 10k/100k fraction.
---@param chance number The chance to format. 1 equals 100%.
---@return string # The formatted chance as a string.
local function formatChance(chance)
    local sign = chance < 0 and "-" or ""
    local abs = math.abs(chance)

    if abs < 0.001 then
        return sign .. toFixed(abs * 10000, 1) .. "/10k"
    elseif abs < 0.0001 then
        return sign .. toFixed(abs * 100000, 1) .. "/100k"
    end

    return toFixed(chance * 100, 3) .. "%"
end

local function fullUrl(url)
    local newUrl = url
    if url:sub(1,5) == "https" then
        return newUrl
    end

    if url:sub(1,1) ~= "/" then
        newUrl = "/" .. newUrl
    end

    newUrl = "https://www.play.idlescape.com" .. newUrl
    return newUrl
end

local function icon(name, url, word, size)
    size = size or 20
    url = fullUrl(url)
    return string.format(
        '[[%s|<img src="%s" alt="%s" width="%s">%s]]',
        name,
        url,
        name,
        size,
        word and name or ""
    )
end

---@param id string|number # Item ID.
---@param word boolean # If true, show the name of the item after the icon.
---@return string # The icon wikitext.
local function itemImage(id, word)
    local item = p.loadData("item")[tostring(id)]
    if not item then return "" end
    local url = ""
    if item.itemIcon then
        url = item.itemIcon
    else
        url = item.itemImage
    end
    return icon(item.name, url, word)
end

---Creates an Wikitext link with an image inside a div.
---@param title string # The title of the link.
---@param imageAttributes table # The html attributes for the image.
---@param divCSS table # The CSS styles for the div.
---@return string # The Wikitext link with the image.
local function createWikitextImage(title, imageAttributes, divCSS)
    divCSS = divCSS or {}
    divCSS.display = divCSS.display or "inline-block" --Makes the div same size as the image
    local e = mw.html.create("div")
        :css(divCSS)
        :tag("img")
        :attr(imageAttributes)
        :done()
    e = tostring(e):gsub("<img(.-) */>", "<img%1>")
    return string.format('[[%s|%s]]', title, e)
end

---Creates a Wikitext containing links and images for the abilities.
---@param abilities number[]|{id: number}[]|nil Array of ability IDs.
---@param unique boolean|nil Skips duplicate abilities if truthy, otherwise creates an element for each ability in order.
---@return string # The Wikitext containing links and images for the abilities.
local function createAbilityRotation(abilities, unique)
    if not abilities then return "" end
    local colors = {
        Melee = "red",
        Range = "green",
        Magic = "blue",
    }

    local seenIds = {}
    local images = {}
    for _, id in ipairs(abilities) do
        if type(id) == "table" then
            id = id.id
        end
        if not seenIds[id] then
            seenIds[id] = unique and true or false
            local ability = getAbility(id)
            if ability then
                -- Check if the ability page exists and add tooltip if not
                local exists = mw.title.new(ability.abilityName)
                exists = exists and exists.exists
                local attributes = {
                    src = fullUrl(ability.abilityImage),
                    alt = ability.abilityName .. ". " .. ability.description,
                    width = 30,
                    class = not exists and "tooltip" or nil,
                    title = not exists and ability.abilityName .. ". " .. ability.description or nil,
                }
                local css = {
                    ["box-shadow"] = "0 0 2px 1px " .. (colors[ability.damageType] or "white"),
                    background = colors[ability.damageType] or "white",
                    margin = "5px"
                }
                table.insert(images, createWikitextImage(ability.abilityName, attributes, css))
            end
        end
    end

    return table.concat(images, "")
end

local function locationImage(id, word)
    local loc = p.loadData("location")[tostring(id)]
    local url = ""
    if loc.locationImage then
        url = loc.locationImage
    else
        return "[[" .. loc.name .. "]]"
    end
    return icon(loc.name, url, word)
end

local function gatheringSource(id)
    local s = ""
    for key, loc in pairs(p.loadData('location')) do
        if loc.loot then
            for key2, loot in pairs(loc.loot) do
                if id == loot.id then
                    s = s .. locationImage(loc.locID, true)
                    s = s .. "<br>"
                end
            end
        end
    end
    return s
end

local function farmingSource(id)
    local s = ""
    for key, item in pairs(p.loadData('item')) do
        if item.farmingStats then
            for key2, yield in pairs(item.farmingStats.yield) do
                if id == yield.id then
                    s = s .. itemImage(item.id, true)
                    s = s .. "<br>"
                end
            end
        end
    end
    return s
end

local function smithingSource(id)
    local s = ""
    local item = getItem(id)
    if item.skill == "smithing" and item.name ~= 'Ichor' then
        s = '[[Smithing]]<br>'
    end
    return s
end

local function cookingSource(id)
    local s = ""
    local item = getItem(id)
    if (item.class == "cooking-ingredient" and not item.ingredientTags) or item.class == 'cookedFish' or item.name=='Ashes' then
        s = '[[Cooking]]<br>'
    end
    return s
end

local function runecraftingSource(id)
    local s = ""
    local item = getItem(id)
    if item.class == "cloth" or (item.class == 'rune' and item.requiredResources) then
        s = '[[Runecrafting]]<br>'
    end
    return s
end

local function scrollcraftingSource(id)
    local s = ""
    local item = getItem(id)
    if item.class == "enchanted-scroll" and item.level and item.level < 100 then
        s = '[[Scrollcrafting]]<br>'
    end
    return s
end

local function craftingSource(id)
    local s = ""
    local item = getItem(id)
    if item.craftable then
        s = '[[Crafting]]<br>'
    end
    return s
end

local function findSource(id)
    local s = ""
    s = s .. gatheringSource(id)
    -- s = s .. farmingSource(id)
    s = s .. scrollcraftingSource(id)
    s = s .. runecraftingSource(id)
    s = s .. smithingSource(id)
    s = s .. craftingSource(id)
    s = s .. cookingSource(id)
    s = s:gsub("<br>$", "")
    return s
end

---@param item Item
---@return integer
local function getItemTier(item)
    local maxRequiredLevel
    if item.requiredLevel then
        for _, level in pairs(item.requiredLevel) do
            if maxRequiredLevel == nil or maxRequiredLevel < level then
                maxRequiredLevel = level
            end
        end
        maxRequiredLevel = math.floor(math.floor(maxRequiredLevel / 10))
    end
    return item.overrideItemTier or maxRequiredLevel or item.enchantmentTier or 1
end

local function getDustId(tier)
    local AFFIX_DUST_PER_ITEM_TIER = {
        550,
        550,
        550,
        551,
        552,
        553,
        554,
        554,
        554,
    }
    tier = (tonumber(tier) or 0) + 1 -- +1 for lua indexing
    tier = math.floor(tier)
    tier = math.min(math.max(tier, 1), 9)
    return AFFIX_DUST_PER_ITEM_TIER[tier]
end

---@param rarity string|nil
---@return integer
local function getScrapId(rarity)
    rarity = rarity or "common"
    local scrapIds = {
        common = 555,
        uncommon = 556,
        rare = 557,
        epic = 558,
        legendary = 559
    }
    return scrapIds[rarity]
end

---@param args table
---@param title string
---@param id string|number|false|nil # Primary loot id, skipped if falsy
---@param scrapping table|nil # data.chance, data.itemID (AugmentingLoot scrappingSuccess or scrappingFail)
local function addScrapping(args, title, id, scrapping)
    local text = id and (itemImage(id, true) .. "<br>") or ""
    if scrapping then
        if scrapping.chance and scrapping.chance ~= 1 then
            text = text .. formatChance(scrapping.chance) .. " "
        end
        if scrapping.minimum and scrapping.maximum then
            text = text .. scrapping.minimum .. "–" .. scrapping.maximum .. " "
        end
        text = text .. itemImage(scrapping.itemID, true)
    end
    args[sl()] = title
    args[sd()] = text
end

---@param args table # infobox args table
---@param augTransform boolean # true for augmenting, false for researching
---@param transformsFrom table<number, ItemTransform>|nil # table of source ID as key, and list of transforms as value
---@param transformsTo ItemTransform[]|nil # list of transforms
local function addTransforms(args, augTransform, transformsFrom, transformsTo)
    if not (transformsFrom or transformsTo) then return end

    ---@param transform ItemTransform
    ---@param id number|false|nil # uses this for item's image instead of transform.newItemID
    local function createTransform(transform, id)
        if (transform.augmentingTransform or false) == augTransform then
            local image = itemImage(id or transform.newItemID, true)
            if transform.augmentingTransform then
                return image .. " at level " .. (transform.augmentationLevel or 1)
            else
                return formatChance(transform.chance) .. " " .. image
            end
        end
    end

    ---@param transforms table<number, ItemTransform>
    ---@param isKeyId boolean|nil # wether the table keys are item IDs or indicies
    local function createTransformList(transforms, isKeyId)
        local list = {}
        for i, transform in pairs(transforms) do
            local text = createTransform(transform, isKeyId and i)
            table.insert(list, text)
        end
        return list
    end

    args[sbreak()] = "yes"

    local list
    if transformsFrom then
        list = createTransformList(transformsFrom, true)
        args[sl()] = "Transforms from"
        args[sd()] = table.concat(list, "<br>")
    end
    if transformsTo then
        list = createTransformList(transformsTo)
        args[sl()] = "Transforms to"
        args[sd()] = table.concat(list, "<br>")
    end
end

---@param args table
---@param item Item
---@param transformsTo ItemTransform[]|nil # list of transforms
---@param transformsFrom table<number, ItemTransform>|nil # list of transforms
---@return boolean
local function addResearch(args, item, transformsTo, transformsFrom)
    local isCup = hasValue(cups, item.id)
    local canResearch = not getGeneralShopItem(item.id) or isCup
    local craftAug = getCraftAug(item.id)

    if craftAug and canResearch then
        local augLoot = getAugLoot(tostring(item.id))
        local tier = getItemTier(item)
        local dustId = not item.noDustFromResearching and getDustId(tier) or nil

        if not item.noScrapFromResearching then
            local success = augLoot and augLoot.scrappingSuccess or nil
            addScrapping(args, "Success", dustId, success)
        end

        -- General Shop items are hardcoded to never fail (CUPS atm)
        if not isCup then
            local scrapId = item.researchesIntoDust and dustId or getScrapId(item.rarity)
            local fail = augLoot and augLoot.scrappingFail or nil
            addScrapping(args, "Fail", scrapId, fail)
        end
    end

    addTransforms(args, false, transformsTo, transformsFrom)

    return craftAug and canResearch or transformsFrom ~= nil
end

---@param prefix 'offensiveDamageAffinity'|'offensiveAccuracyAffinityRating'|'defensiveDamageAffinity'
---@param description string|nil
---@param uppercase boolean|nil
---@return table<string, string>
local function createAffinityDictionary(prefix, description, uppercase)
    description = description or ""
    uppercase = uppercase or false

    local allDamageTypes = {
        "Melee",
        "Magic",
        "Range",
        "Piercing",
        "Blunt",
        "Slashing",
        "Chaos",
        "Nature",
        "Fire",
        "Ice",
        "Lightning",
        "Poison",
        "Typeless",
        "Heal"
    }

    local affinities = {}
    for _, damageType in ipairs(allDamageTypes) do
        local key = string.format("%s.%s", prefix, damageType)
        local val = string.format("%s %s", uppercase and damageType:upper() or damageType, description)
        local affinity = val:gsub("%s+$", "")
        affinities[key] = affinity
    end
    return affinities
end

local function getLabelFromAugBonus(bonusName)
    local equipmentStatsToLabelMapping = {
        ["weaponBonus.strength"] = "Strength",
        ["weaponBonus.intellect"] = "Intellect",
        ["weaponBonus.dexterity"] = "Dexterity",
        ["offensiveCritical.chance"] = "Crit Chance",
        ["offensiveCritical.damageMultiplier"] = "Crit Mult",
        ["armorBonus.protection"] = "Protection",
        ["armorBonus.resistance"] = "Resistance",
        ["armorBonus.agility"] = "Agility",
        ["armorBonus.stamina"] = "Stamina",
        ["defensiveCritical.chance"] = "Crit Avoidance",
        ["defensiveCritical.damageMultiplier"] = "Crit Reduction",
        ["toolBoost.fishing"] = "Fishing",
        ["toolBoost.fishingBaitPower"] = "Bait Power",
        ["toolBoost.fishingReelPower"] = "Reel Power",
        ["toolBoost.fishingRarityPower"] = "Bonus Rarity",
        ["toolBoost.mining"] = "Mining",
        ["toolBoost.foraging"] = "Foraging",
        ["toolBoost.farming"] = "Farming",
        ["toolBoost.cooking"] = "Cooking",
        ["toolBoost.smithing"] = "Smithing",
        ["toolBoost.enchanting"] = "Enchanting",
        ["toolBoost.runecrafting"] = "Runecrafting"
    }
    for k, v in pairs(createAffinityDictionary("offensiveDamageAffinity", "") or {}) do
        equipmentStatsToLabelMapping[k] = v
    end
    for k, v in pairs(createAffinityDictionary("offensiveAccuracyAffinityRating", "Accuracy") or {}) do
        equipmentStatsToLabelMapping[k] = v
    end
    for k, v in pairs(createAffinityDictionary("defensiveDamageAffinity", "") or {}) do
        equipmentStatsToLabelMapping[k] = v
    end

    return equipmentStatsToLabelMapping[bonusName] or bonusName
end

---@param item Item
local function createInfobox(item)
    local lang = mw.language.getContentLanguage()
    local args = {}
    args.autoheaders = "y"
    args.subbox = "no"
    args.bodystyle = " "
    args.title = item.name

    local url = item.itemIcon or item.itemImage
    args.image = '<img src="' .. fullUrl(url) .. '" width="150">'

    if item.value then
        args[l()] = "Vendor Value"
        args[d()] = lang:formatNum(item.value) .. " " .. icon('Gold', "/images/gold_coin.png", false, 13)
    end

    --TODO: add this back when the market is fixed
    --[[if item.tradeable then
        args[l()] = icon('Market', "/images/ui/marketplace_icon.png")
        local market = require("Module:Market")["_price"]({item.name, 1, 1})
        if market then
            args[d()] = addSeparator(market)
        else
            args[d()] = "Yes"
        end
    end]]

    if item.rarity then
        args[l()] = "Rarity"
        args[d()] = item.rarity:gsub("^%l", string.upper)
    end

    if item.requiredLevel then
        local levels = {}
        for skill, level in pairs(item.requiredLevel) do
            skill = skill:gsub("^%l", string.upper)
            table.insert(levels, level .. " " .. skill)
        end
        args[l()] = "Level"
        args[d()] = table.concat(levels, "<br>")
    end

    --TODO: findSource doesn't work so it's disabled for now. Does need a big rework
    --args[l()] = "Source"
    --args[d()] = findSource(item.id)

    if item.heat then
        args[l()] = itemImage(2, false)
        args[d()] = lang:formatNum(item.heat)
    end

    if item.forcedEnchant then
        args[l()] = "Enchantment"
        args[d()] = "[[" .. getEnchantName(item.forcedEnchant) .. "]]"

        args[l()] = "Enchantment Level"
        args[d()] = item.forcedEnchantAmount
    end

    if item.enchantmentTier then
        args[l()] = "Enchantment Slots"
        args[d()] = item.enchantmentTier
    end

    local stats = item.equipmentStats
    if stats then
        local slot = stats.slot and stats.slot:gsub("^%l", string.upper)
        args[l()] = "Slot"
        args[d()] = stats.slot and slot

        if stats.oneHanded == false then
            args[l()] = "Two-handed"
            args[d()] = "Yes"
        end

        if stats.attackSpeed then
            args[l()] = "Attack Speed"
            args[d()] = stats.attackSpeed .. "s"
        end

        if stats.toolBoost then
            args[h()] = "Tool Stats"

            for _, stat in ipairs(stats.toolBoost) do
                if stat.boost ~= 0 then
                    local sign = stat.boost > 0 and "+" or ""
                    local boost = sign .. stat.boost

                    args[l()] = getLabelFromAugBonus("toolBoost." .. stat.skill)
                    args[d()] = boost
                end
            end
        end

        args[h()] = "Offensive Stats"

        local stat = stats.offensiveCritical
        if stat then
            args[l()] = "Crit Chance"
            args[d()] = toFixed(stat.chance * 100, 3) .. "%"
            args[l()] = "Crit Multiplier"
            args[d()] = stat.damageMultiplier .. "x"
        end

        stat = stats.weaponBonus
        if stat then
            args[sl()] = "Str"
            args[sd()] = stat.strength
            args[sl()] = "Int"
            args[sd()] = stat.intellect
            args[sl()] = "Dex"
            args[sd()] = stat.dexterity
        end

        args[h()] = "Offensive Affinity"
        stat = stats.offensiveDamageAffinity
        if stat then
            args[sl()] = "Melee"
            args[sd()] = stat.Melee and toFixed(stat.Melee * 100 - 100, 3) .. "%"
            args[sl()] = "Magic"
            args[sd()] = stat.Magic and toFixed(stat.Magic * 100 - 100, 3) .. "%"
            args[sl()] = "Range"
            args[sd()] = stat.Range and toFixed(stat.Range * 100 - 100, 3) .. "%"
            args[sbreak()] = "yes"

            args[sl()] = "Piercing"
            args[sd()] = stat.Piercing and toFixed(stat.Piercing * 100 - 100, 3) .. "%"
            args[sl()] = "Blunt"
            args[sd()] = stat.Blunt and toFixed(stat.Blunt * 100 - 100, 3) .. "%"
            args[sl()] = "Slashing"
            args[sd()] = stat.Slashing and toFixed(stat.Slashing * 100 - 100, 3) .. "%"
            args[sl()] = "Fire"
            args[sd()] = stat.Fire and toFixed(stat.Fire * 100 - 100, 3) .. "%"
            args[sl()] = "Ice"
            args[sd()] = stat.Ice and toFixed(stat.Ice * 100 - 100, 3) .. "%"
            args[sl()] = "Nature"
            args[sd()] = stat.Nature and toFixed(stat.Nature * 100 - 100, 3) .. "%"
            args[sl()] = "Chaos"
            args[sd()] = stat.Chaos and toFixed(stat.Chaos * 100 - 100, 3) .. "%"
            args[sbreak()] = "yes"
        end

        args[h()] = "Accuracy"
        stat = stats.offensiveAccuracyAffinityRating
        if stat then
            args[sl()] = "Melee"
            args[sd()] = stat.Melee
            args[sl()] = "Magic"
            args[sd()] = stat.Magic
            args[sl()] = "Range"
            args[sd()] = stat.Range
            args[sbreak()] = "yes"

            args[sl()] = "Piercing"
            args[sd()] = stat.Piercing
            args[sl()] = "Blunt"
            args[sd()] = stat.Blunt
            args[sl()] = "Slashing"
            args[sd()] = stat.Slashing
            args[sl()] = "Fire"
            args[sd()] = stat.Fire
            args[sl()] = "Ice"
            args[sd()] = stat.Ice
            args[sl()] = "Nature"
            args[sd()] = stat.Nature
            args[sl()] = "Chaos"
            args[sd()] = stat.Chaos
            args[sbreak()] = "yes"
        end

        args[h()] = "Defensive Stats"

        stat = stats.defensiveCritical
        if stat then
            args[l()] = "Crit Avoidance"
            args[d()] = toFixed(stat.chance * 100, 3) .. "%"
            args[l()] = "Crit Reduction"
            args[d()] = stat.damageMultiplier
        end

        stat = stats.armorBonus
        if stat then
            args[sl()] = "Protection"
            args[sd()] = stat.protection
            args[sl()] = "Resistance"
            args[sd()] = stat.resistance
            args[sl()] = "Agility"
            args[sd()] = stat.agility
            args[sl()] = "Stamina"
            args[sd()] = stat.stamina
        end

        args[h()] = "Defensive Affinity"
        stat = stats.defensiveDamageAffinity
        if stat then
            args[sl()] = "Melee"
            args[sd()] = stat.Melee and toFixed(stat.Melee * 100 - 100, 3) .. "%"
            args[sl()] = "Magic"
            args[sd()] = stat.Magic and toFixed(stat.Magic * 100 - 100, 3) .. "%"
            args[sl()] = "Range"
            args[sd()] = stat.Range and toFixed(stat.Range * 100 - 100, 3) .. "%"
            args[sbreak()] = "yes"

            args[sl()] = "Piercing"
            args[sd()] = stat.Piercing and toFixed(stat.Piercing * 100 - 100, 3) .. "%"
            args[sl()] = "Blunt"
            args[sd()] = stat.Blunt and toFixed(stat.Blunt * 100 - 100, 3) .. "%"
            args[sl()] = "Slashing"
            args[sd()] = stat.Slashing and toFixed(stat.Slashing * 100 - 100, 3) .. "%"
            args[sl()] = "Fire"
            args[sd()] = stat.Fire and toFixed(stat.Fire * 100 - 100, 3) .. "%"
            args[sl()] = "Ice"
            args[sd()] = stat.Ice and toFixed(stat.Ice * 100 - 100, 3) .. "%"
            args[sl()] = "Nature"
            args[sd()] = stat.Nature and toFixed(stat.Nature * 100 - 100, 3) .. "%"
            args[sl()] = "Chaos"
            args[sd()] = stat.Chaos and toFixed(stat.Chaos * 100 - 100, 3) .. "%"
            args[sbreak()] = "yes"
        end

        args[h()] = "Abilities"
        args[sd()] = createAbilityRotation(stats.grantedAbility, true)

        args[h()] = "Set Bonus"
        for id, desc in pairs(getModuleItemSet()._formatItemSets(stats.itemSet)) do
            local enchant = getEnchant(id)
            local counts = {}
            for _, req in ipairs(enchant.setRequirements) do
                if (req.strength > 0) then
                    table.insert(counts, req.count)
                end
            end
            args[sl()] = "[[" .. enchant.name .. "]] [" .. table.concat(counts, ", ") .. "]"
            args[sd()] = desc
            args[sbreak()] = "yes"
        end
    end

    local ammoStat = item.ammunitionMults
    if ammoStat then
        args[h()] = "Ammo Stats"
        args[sl()] = 'Type'
        args[sd()] = ammoStat.style
        args[sl()] = 'Damage'
        args[sd()] = ammoStat.damageMult .. 'x'
        args[sl()] = 'Accuracy'
        args[sd()] = ammoStat.accuracyMult .. 'x'
    end

    ---@return table|nil
    local function getAugmentingByID(id)
        local craftAug = getCraftAug(id)
        if not craftAug then return nil end
        return craftAug and craftAug.augmenting;
    end

    ---@return table|nil
    local function getScrappingByID(id)
        local craftAug = getCraftAug(id)
        if not craftAug then return nil end
        return craftAug.scrapping or getAugmentingByID(id)
    end

    local function canAugment(id)
        return not item.blockAugmenting and getAugmentingByID(id)
    end

    local function canScrap(id)
        return not item.blockResearching and getScrappingByID(id)
    end

    --TODO: add transform source
    args[h()] = "Enchanting"

    if getScrappingByID(item.id) then
        if canAugment(item.id) or canScrap(item.id) then
            args[l()] = dottedTooltip("Base XP", "Experience is increased by 10% for every augmentation level.")
            args[d()] = lang:formatNum(20 * math.pow(getItemTier(item), 2))
        end

        local craftAug = getCraftAug(item.id)
        local costs = {}
        for id, cost in pairs(craftAug.scrapping or {}) do
            table.insert(costs, {id = id, text = lang:formatNum(cost) .. " " .. itemImage(id, true)})
        end
        table.sort(costs, function(a, b)
            return a.id < b.id
        end)
        for i = 1, #costs do
            costs[i] = costs[i].text
        end

        args[sl()] = "Cost"
        args[sd()] = table.concat(costs, "<br>")

        ---@type table<string, AugmentingLoot>|nil
        local augLoot = getAugLoot()
        ---@type AugmentingLoot|nil
        local augLootItem = augLoot and augLoot[tostring(item.id)]
        local transformsTo = augLootItem and augLootItem.transforms
        ---@type table<number, ItemTransform>|nil # the key is the id of the item that transforms
        local transformsFrom
        for id, keys in pairs(augLoot or {}) do
            for _, transform in ipairs(keys.transforms or {}) do
                if transform.newItemID == item.id then
                    transformsFrom = transformsFrom or {}
                    transformsFrom[tonumber(id)] = transform
                end
            end
        end

        args[h()] = "Augmentation"
        if canAugment(item.id) or transformsFrom then
            if item.equipmentStats then
                local bonuses = {}
                for _, bonus in pairs(item.equipmentStats.augmentationBonus or {}) do
                    table.insert(bonuses, "+" .. bonus.value .. " " .. getLabelFromAugBonus(bonus.stat))
                end
                args[sl()] = "Bonus Stats"
                args[sd()] = table.concat(bonuses, "<br>")
            end
            addTransforms(args, true, transformsFrom, transformsTo)
        end

        args[h()] = "Research"
        if canScrap(item.id) or transformsFrom then
            addResearch(args, item, transformsFrom, transformsTo)
        end
    end

    local crafting = item.craftingStats
    if crafting then
        args[h()] = "Crafting"

        args[l()] = "Category"
        args[d()] = crafting.category

        args[l()] = "Craftable"
        args[d()] = not crafting.craftable and "No" or ""

        args[l()] = "Level"
        args[d()] = crafting.level

        args[l()] = "Experience"
        args[d()] = crafting.experience and lang:formatNum(crafting.experience) or ""

        args[l()] = "Amount"
        args[d()] = crafting.multiplier

        args[sl()] = "Description"
        args[sd()] = crafting.description
            and '<p style="margin:auto;font-style:italic">' .. crafting.description .. '</p>' or ""
    end

    local ingredient = getCookingIngredient(item.id)
    if ingredient then
        args[h()] = "Cooking"

        args[l()] = "Level"
        args[d()] = ingredient.level

        args[l()] = "Difficulty"
        args[d()] = ingredient.difficulty

        args[l()] = "Size"
        args[d()] = ingredient.size

        args[l()] = "Alchemy Size"
        args[d()] = ingredient.alchemySize

        args[l()] = "Category"
        args[d()] = table.concat(ingredient.ingredientTags or {}, "<br>")

        args[l()] = "Buff"
        args[d()] = getEnchantName(ingredient.cookingEnchantment) or getEnchantName(ingredient.alchemyEnchantment) or ""
    end

    local farming = item.farmingStats
    if farming then
        local drops = {}
        local yield = copyTable(getYield(item.id) or {})
        table.sort(yield, function(a, b)
            return a.chance > b.chance
        end)
        for _, drop in ipairs(yield) do
            table.insert(drops, string.format(
                "%s–%s %s %s%%",
                drop.min,
                drop.max,
                itemImage(drop.id, true),
                toFixed(drop.chance * 100, 2)
            ))
        end

        args[h()] = "Farming"

        args[l()] = "Level"
        args[d()] = farming.requiredLevel

        args[l()] = "Experience"
        args[d()] = lang:formatNum(farming.experience)

        args[l()] = "Plot Size"
        args[d()] = string.format(
            "%dx%d%s",
            farming.width,
            farming.height,
            farming.maxWidth and string.format(" – %dx%d", farming.maxWidth, farming.maxHeight) or ""
        )

        args[l()] = "Harvest Time"
        args[d()] = farming.time .. " minutes"

        args[l()] = "Yield"
        args[d()] = table.concat(drops, "<br>")
    end

    if item.extraTooltipInfo then
        args[h()] = "Tooltip"
        args[d()] = '<p style="margin:auto;font-style:italic;font-size:1.2em">' .. item.extraTooltipInfo .. '</p>'
    end

    for key, data in pairs(args) do
        if string.find(key, "data") then
            args[key] = tostring(data)
        end
    end

    return require('Module:Infobox').infobox(args)
end

function p.item(frame)
    local args = frame:getParent().args
    return p._item(args)
end

function p._item(args)
    local name = args.name or args.title or args[1] or mw.title.getCurrentTitle().text
    local item = findItem(name)

    if not item then
        return "<div style=\"color:red\"> No item named '" .. name .. "'</div>. The Module:Items/data maybe outdated."
    end

    return createInfobox(item)
end

return p