Difference between revisions of "Module:Infobox Item"

From Idlescape Wiki
Jump to navigation Jump to search
(Update how set enchants are generated. Update p.loadData() to also try require() if mw.loadData() fails.)
(Add item abilities. Add enchant xp. Add rarity. Add forced enchants. Add crafting info. Fix research cups. Fix/refactor enchanting logic. Fix/update cooking items. Fix/updat farming items. Disable marketplace value. And other small edits.)
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',
Line 8: Line 8:
 
     farming = 'Module:Farming/data',
 
     farming = 'Module:Farming/data',
 
     augloot = 'Module:Augmenting loot/data',
 
     augloot = 'Module:Augmenting loot/data',
 +
    cooking = 'Module:CookingList/data',
 +
    ability = 'Module:Abilities/data',
 +
    gameshop = 'Module:GameShopItems/data',
 
     itemsets = 'Module:Item sets',
 
     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 The number to format.
 +
---@param digits number The number of decimal places to include (must be a non-negative integer).
 +
---@return string # The formatted number as a string.
 +
local function toFixed(num, digits)
 +
    digits = math.max(0, math.floor(digits))
 +
    local formatted = string.format("%." .. digits .. "f", num):gsub("%.?0+$", "")
 +
    return formatted
 +
end
  
 
local function h()
 
local function h()
Line 62: Line 111:
 
---@return table
 
---@return table
 
function p.loadData(data_type)
 
function p.loadData(data_type)
     local module_name = data_module_names[data_type]
+
     local module_name = module_names[data_type]
     if loaded_data_modules[module_name] == nil then
+
     if loaded_modules[module_name] == nil then
 
         local status, module = pcall(mw.loadData, module_name)
 
         local status, module = pcall(mw.loadData, module_name)
 
         if not status then
 
         if not status then
Line 69: Line 118:
 
         end
 
         end
 
         if status then
 
         if status then
             loaded_data_modules[module_name] = module
+
             loaded_modules[module_name] = module
 
         else
 
         else
             error("Failed to load module: " .. module_name .. "\n" .. module)
+
             error("Failed to load module: " .. data_type .. " / " .. tostring(module_name) .. "\n" .. module)
 
         end
 
         end
 
     end
 
     end
  
     return loaded_data_modules[module_name]
+
     return loaded_modules[module_name]
end
 
 
 
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
 
    return 0
 
 
end
 
end
  
Line 95: Line 131:
 
end
 
end
  
local function getEnchantment(id)
+
local function getEnchant(id)
 
     return p.loadData("enchantment")[tostring(id)]
 
     return p.loadData("enchantment")[tostring(id)]
 
end
 
end
  
local function getEnchantmentName(id)
+
local function getEnchantName(id)
     local enchantment = getEnchantment(id)
+
     local enchantment = getEnchant(id)
 
     return enchantment and enchantment.name
 
     return enchantment and enchantment.name
 
end
 
end
Line 116: Line 152:
 
end
 
end
  
 +
---@param id string|number
 +
---@return CookingItem|nil
 +
local function getCookingIngredient(id)
 +
    return p.loadData("cooking")[tostring(id)]
 +
end
  
 
---@param id string|number
 
---@param id string|number
Line 121: Line 162:
 
local function getAugLoot(id)
 
local function getAugLoot(id)
 
     return p.loadData("augloot")[tostring(id)]
 
     return p.loadData("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
 
end
  
Line 137: Line 225:
 
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)]
 +
    if not item then return "" end
 
     local url = ""
 
     local url = ""
 
     if item.itemIcon then
 
     if item.itemIcon then
Line 159: Line 253:
 
end
 
end
  
local function addSeparator(num)
+
---Creates an Wikitext link with an image inside a div.
     return tostring(tonumber(num)):reverse():gsub("(%d%d%d)","%1,"):gsub(",(%-?)$","%1"):reverse()
+
---@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 a = getAbility(id)
 +
            if a then
 +
                local attributes = {
 +
                    src = fullUrl(a.abilityImage),
 +
                    alt = a.abilityName .. ". " .. a.description,
 +
                    width = 30,
 +
                    class = "tooltip",
 +
                    title = a.abilityName .. ". " .. a.description,
 +
                }
 +
                local css = {
 +
                    ["box-shadow"] = "0 0 2px 1px " .. (colors[a.damageType] or "white"),
 +
                    background = colors[a.damageType] or "white",
 +
                    margin = "5px"
 +
                }
 +
                table.insert(images, createWikitextImage(a.abilityName, attributes, css))
 +
            end
 +
        end
 +
    end
 +
 
 +
    return table.concat(images, "")
 
end
 
end
  
Line 328: Line 477:
 
end
 
end
  
---@param args table
+
---@param args table # infobox args table
---@param transforms ItemTransform[]
+
---@param augTransform boolean # true for augmenting, false for researching
 +
---@param transforms ItemTransform[] # list of transforms
 
local function addTransforms(args, augTransform, transforms)
 
local function addTransforms(args, augTransform, transforms)
     local text = ""
+
     local list = {}
 
     for _, transform in ipairs(transforms) do
 
     for _, transform in ipairs(transforms) do
 
         if (transform.augmentingTransform or false) == augTransform then
 
         if (transform.augmentingTransform or false) == augTransform then
 +
            local image = itemImage(transform.newItemID, true)
 
             if transform.augmentingTransform then
 
             if transform.augmentingTransform then
                 text = text .. "At level " .. (transform.augmentationLevel or 1) .. " to "
+
                 local text = "At level " .. (transform.augmentationLevel or 1) .. " to " .. image
 +
                table.insert(list, text)
 
             else
 
             else
                 text = text .. transform.chance * 100 .. "% "
+
                 table.insert(list, transform.chance * 100 .. "% " .. image)
 
             end
 
             end
            text = text .. itemImage(transform.newItemID, true) .. "<br>"
 
 
         end
 
         end
 
     end
 
     end
    text = text:gsub("<br>$", "")
 
 
     args[sbreak()] = "yes"
 
     args[sbreak()] = "yes"
 
     args[sl()] = "Transforms"
 
     args[sl()] = "Transforms"
     args[sd()] = text
+
     args[sd()] = table.concat(list, "<br>")
 
end
 
end
  
Line 352: Line 502:
 
---@return boolean
 
---@return boolean
 
local function addResearch(args, item)
 
local function addResearch(args, item)
--local function addResearch(args, id, addHeader)
+
    local isCup = hasValue(cups, item.id)
 +
    local canResearch = not getGeneralShopItem(item.id) or isCup
 
     local craftAug = getCraftAug(item.id)
 
     local craftAug = getCraftAug(item.id)
     local augLoot = getAugLoot(tostring(item.id))
+
 
    if item and craftAug then
+
     if craftAug and canResearch then
         if not item.blockAugmenting then
+
        local augLoot = getAugLoot(tostring(item.id))
             args[h()] = "Research"
+
        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
        local tier = getItemTier(item)
+
 
        local dustId = getDustId(tier)
 
        local success = augLoot and augLoot.scrappingSuccess or nil
 
        addScrapping(args, "Success", (not item.noDustFromResearching and dustId), success)
 
        local scrapId = item.researchesIntoDust and dustId or getScrapId(item.rarity)
 
        local fail = augLoot and augLoot.scrappingFail or nil
 
        addScrapping(args, "Fail", (not item.noScrapFromResearching and scrapId), fail)
 
 
         local transforms = augLoot and augLoot.transforms or {}
 
         local transforms = augLoot and augLoot.transforms or {}
 
         addTransforms(args, false, transforms)
 
         addTransforms(args, false, transforms)
 +
 
         return true
 
         return true
 
     end
 
     end
 +
 
     return false
 
     return false
 
end
 
end
Line 446: Line 605:
 
end
 
end
  
 +
---@param item Item
 
local function createInfobox(item)
 
local function createInfobox(item)
 +
    local lang = mw.language.getContentLanguage()
 
     local args = {}
 
     local args = {}
    local url = ""
 
 
     local text = ""
 
     local text = ""
 
     args.autoheaders = "y"
 
     args.autoheaders = "y"
Line 455: Line 615:
 
     args.title = item.name
 
     args.title = item.name
  
     if item.itemIcon then
+
     local url = item.itemIcon or item.itemImage
        url = item.itemIcon
+
     args.image = '<img src="' .. fullUrl(url) .. '" width="150">'
    else
 
        url = item.itemImage
 
    end
 
     args.image = "<img src=\"" .. fullUrl(url) .. "\" width=\"150\">"
 
  
 
     if item.value then
 
     if item.value then
         args[l()] = icon('Gold', "/images/gold_coin.png")
+
         args[l()] = "Vendor Value"
         args[d()] = addSeparator(item.value)
+
         args[d()] = lang:formatNum(item.value) .. " " .. icon('Gold', "/images/gold_coin.png", false, 13)
 
     end
 
     end
  
     if item.tradeable then
+
     --TODO: add this back when the market is fixed
 +
    --[[if item.tradeable then
 
         args[l()] = icon('Market', "/images/ui/marketplace_icon.png")
 
         args[l()] = icon('Market', "/images/ui/marketplace_icon.png")
 
         local market = require("Module:Market")["_price"]({item.name, 1, 1})
 
         local market = require("Module:Market")["_price"]({item.name, 1, 1})
Line 475: Line 632:
 
             args[d()] = "Yes"
 
             args[d()] = "Yes"
 
         end
 
         end
 +
    end]]
 +
 +
    if item.rarity then
 +
        args[l()] = "Rarity"
 +
        args[d()] = item.rarity:gsub("^%l", string.upper)
 
     end
 
     end
  
 
     if item.requiredLevel then
 
     if item.requiredLevel then
         text = ""
+
         local levels = {}
 
         for skill, level in pairs(item.requiredLevel) do
 
         for skill, level in pairs(item.requiredLevel) do
 
             skill = skill:gsub("^%l", string.upper)
 
             skill = skill:gsub("^%l", string.upper)
             text = text .. level .. " " .. skill .. "<br>"
+
             table.insert(levels, level .. " " .. skill)
 
         end
 
         end
        text = text:gsub("<br>$", "")
+
         args[l()] = "Level"
         args[l()] = "Level Required"
+
         args[d()] = table.concat(levels, "<br>")
         args[d()] = text
 
 
     end
 
     end
  
Line 493: Line 654:
  
 
     if item.heat then
 
     if item.heat then
         args[l()] = itemImage(2)
+
         args[l()] = itemImage(2, false)
         args[d()] = addSeparator(item.heat)
+
         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
 
     end
  
 
     local stats = item.equipmentStats
 
     local stats = item.equipmentStats
 
     if stats then
 
     if stats then
         local slot = stats.slot:gsub("^%l", string.upper)
+
         local slot = stats.slot and stats.slot:gsub("^%l", string.upper)
 
         args[l()] = "Slot"
 
         args[l()] = "Slot"
 
         args[d()] = stats.slot and slot
 
         args[d()] = stats.slot and slot
  
         if item.enchantmentTier then
+
         if stats.oneHanded == false then
             args[l()] = "Enchantment Slots"
+
             args[l()] = "Two-handed"
             args[d()] = item.enchantmentTier
+
             args[d()] = "Yes"
 
         end
 
         end
  
         if item.forcedEnchant then
+
         if stats.attackSpeed then
             args[l()] = "Enchantments"
+
             args[l()] = "Attack Speed"
             args[d()] = "[[" .. getEnchantmentName(item.forcedEnchant) .. "]]"
+
             args[d()] = stats.attackSpeed .. "s"
 
         end
 
         end
  
 
         if stats.toolBoost then
 
         if stats.toolBoost then
             text = ""
+
             args[h()] = "Tool Stats"
             for key, stat in pairs(stats.toolBoost) do
+
 
 +
             for _, stat in ipairs(stats.toolBoost) do
 
                 if stat.boost ~= 0 then
 
                 if stat.boost ~= 0 then
                     text = text .. stat.boost .. " "
+
                     local sign = stat.boost > 0 and "+" or ""
                     text = text .. getLabelFromAugBonus("toolBoost." .. stat.skill) .. "<br>"
+
                     local boost = sign .. stat.boost
 +
 
 +
                    args[l()] = getLabelFromAugBonus("toolBoost." .. stat.skill)
 +
                    args[d()] = boost
 
                 end
 
                 end
 
             end
 
             end
            text = text:gsub("<br>$", "")
 
            args[l()] = "Stats"
 
            args[d()] = text
 
        end
 
 
        if stats.oneHanded == false then
 
            args[l()] = "Two-handed"
 
            args[d()] = "Yes"
 
 
         end
 
         end
 
        if stats.attackSpeed then
 
            args[l()] = "Attack Speed"
 
            args[d()] = stats.attackSpeed
 
        end
 
 
  
 
         args[h()] = "Offensive Stats"
 
         args[h()] = "Offensive Stats"
  
         stat = stats.offensiveCritical
+
         local stat = stats.offensiveCritical
 
         if stat then
 
         if stat then
 
             args[l()] = "Crit Chance"
 
             args[l()] = "Crit Chance"
 
             args[d()] = stat.chance * 100 .. "%"
 
             args[d()] = stat.chance * 100 .. "%"
 
             args[l()] = "Crit Multiplier"
 
             args[l()] = "Crit Multiplier"
             args[d()] = stat.damageMultiplier
+
             args[d()] = stat.damageMultiplier .. "x"
 
         end
 
         end
  
Line 662: Line 826:
 
             args[sbreak()] = "yes"
 
             args[sbreak()] = "yes"
 
         end
 
         end
 +
 +
        args[h()] = "Abilities"
 +
        args[sd()] = createAbilityRotation(stats.grantedAbility, true)
  
 
         args[h()] = "Set Bonus"
 
         args[h()] = "Set Bonus"
         for id, desc in pairs(p.loadData("itemsets")._formatItemSets(stats.itemSet)) do
+
         for id, desc in pairs(getModuleItemSet()._formatItemSets(stats.itemSet)) do
             local enchant = getEnchantment(id)
+
             local enchant = getEnchant(id)
 
             local counts = {}
 
             local counts = {}
 
             for _, req in ipairs(enchant.setRequirements) do
 
             for _, req in ipairs(enchant.setRequirements) do
Line 679: Line 846:
  
 
     local ammoStat = item.ammunitionMults
 
     local ammoStat = item.ammunitionMults
 
 
     if ammoStat then
 
     if ammoStat then
 
         args[h()] = "Ammo Stats"
 
         args[h()] = "Ammo Stats"
Line 690: Line 856:
 
     end
 
     end
  
     if item.blockAugmenting then
+
     ---@return table|nil
        args[h()] = "Research"
+
    local function getAugmentingByID(id)
     else
+
        local craftAug = getCraftAug(id)
         args[h()] = "Augmentation"
+
return craftAug and craftAug.augmenting;
 +
end
 +
 
 +
    ---@return table|nil
 +
    local function getScrappingByID(id)
 +
        local craftAug = getCraftAug(id)
 +
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
 
     end
 +
 +
    args[h()] = "Enchanting"
 +
 
     local craftAug = getCraftAug(item.id)
 
     local craftAug = getCraftAug(item.id)
 +
    if craftAug then
 +
        local costs = {}
 +
        for id, cost in pairs(craftAug.scrapping or {}) do
 +
            table.insert(costs, lang:formatNum(cost) .. " " .. itemImage(id, true))
 +
        end
  
    if craftAug and craftAug.scrapping then
+
        args[sl()] = "Cost"
         if item.equipmentStats then
+
        args[sd()] = table.concat(costs, "<br>")
             text = ""
+
 
             for key, bonus in pairs(item.equipmentStats.augmentationBonus) do
+
         if canAugment(item.id) or canScrap(item.id) then
                text = text .. "+" .. bonus.value .. " "
+
             args[sl()] = dottedTooltip("Base XP", "Experience is increased by 10% for every augmentation level.")
                text = text .. getLabelFromAugBonus(bonus.stat) .. "<br>"
+
             args[sd()] = lang:formatNum(20 * math.pow(getItemTier(item), 2))
 +
        end
 +
 
 +
        args[h()] = "Augmentation"
 +
        if canAugment(item.id) 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
 
             end
             text = text:gsub("<br>$", "")
+
 
             args[sl()] = "Aug Bonus"
+
             local augLoot = getAugLoot(item.id)
             args[sd()] = text
+
             local transforms = augLoot and augLoot.transforms
 +
             if transforms then addTransforms(args, true, transforms) end
 
         end
 
         end
  
         text = ""
+
         args[h()] = "Research"
         local costs = {}
+
         if canScrap(item.id) then
        for key, cost in pairs(craftAug.scrapping) do
+
             addResearch(args, item)
            table.insert(costs, key)
 
        end
 
        -- Lua tables are unsorted, order by itemID
 
        table.sort(costs)
 
        for _, key in ipairs(costs) do
 
            text = text .. craftAug.scrapping[key] .. " "
 
             text = text .. itemImage(key, true) .. "<br>"
 
 
         end
 
         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 ""
  
        text = text:gsub("<br>$", "")
+
         args[l()] = "Amount"
         args[sl()] = "Cost"
+
         args[d()] = crafting.multiplier
         args[sd()] = text
 
  
         local augLoot = getAugLoot(item.id)
+
         args[sl()] = "Description"
         local transforms = augLoot and augLoot.transforms or {}
+
         args[sd()] = crafting.description
        addTransforms(args, true, transforms) --only add transforms if the item transforms by augmenting
+
            and '<p style="margin:auto;font-style:italic">' .. crafting.description .. '</p>' or ""
        addResearch(args, item)
 
 
     end
 
     end
  
     args[h()] = "Cooking"
+
     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
  
    if item.size then
 
 
         args[l()] = "Size"
 
         args[l()] = "Size"
         args[d()] = item.size
+
         args[d()] = ingredient.size
    end
 
  
    if item.difficulty then
+
         args[l()] = "Alchemy Size"
         args[l()] = "Difficulty"
+
         args[d()] = ingredient.alchemySize
         args[d()] = item.difficulty
 
    end
 
  
    if item.ingredientTags then
 
        text = ""
 
        for key, tag in pairs(item.ingredientTags) do
 
            text = text .. tag .. "<br>"
 
        end
 
        text = text:gsub("<br>$", "")
 
 
         args[l()] = "Category"
 
         args[l()] = "Category"
         args[d()] = text
+
         args[d()] = table.concat(ingredient.ingredientTags, "<br>")
    end
 
  
    if item.cookingEnchantment then
 
 
         args[l()] = "Buff"
 
         args[l()] = "Buff"
         args[d()] = getEnchantmentName(item.cookingEnchantment)
+
         args[d()] = getEnchantName(ingredient.cookingEnchantment) or getEnchantName(ingredient.alchemyEnchantment) or ""
 
     end
 
     end
 
    args[h()] = "Seeds"
 
  
 
     local farming = item.farmingStats
 
     local farming = item.farmingStats
 
     if farming then
 
     if farming then
         local farmingData = p.loadData('farming')
+
         local drops = {}
         local seedData = farmingData[item.id]
+
         local yield = copyTable(getYield(item.id) or {})
         if seedData then
+
         table.sort(yield, function(a, b)
             text = ""
+
             return a.chance > b.chance
            for key, yield in pairs(seedData) do
+
        end)
                text = text .. yield.min .. "-" .. yield.max .. " "
+
        for _, drop in ipairs(yield) do
                 text = text .. itemImage(yield.id, true)
+
            table.insert(drops, string.format(
                 if yield.chance ~= 1 then
+
                "%s–%s %s %s%%",
                    text = text .. " " .. tonumber(string.format('%.2f', yield.chance * 100)) .. "%"
+
                drop.min,
                end
+
                drop.max,
                text = text .. "<br>"
+
                 itemImage(drop.id, true),
            end
+
                 toFixed(drop.chance * 100, 2)
            text = text:gsub("<br>$", "")
+
             ))
             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()] = text
 
 
         end
 
         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
 
     end
  
     local args2 = {}
+
     if item.extraTooltipInfo then
 
+
        args[h()] = "Tooltip"
    args2.subbox = "yes"
+
        args[d()] = '<p style="margin:auto;font-style:italic;font-size:1.2em">' .. item.extraTooltipInfo .. '</p>'
    args2.bodystyle = "padding: 0.5em; margin:auto; font-style:italic; font-size:110%; text-align: center"
+
     end
    args2.data1 = item.extraTooltipInfo
 
     args[h()] = "Tooltip"
 
    args[d()] = require('Module:Infobox').infobox(args2)
 
  
 
     for key, data in pairs(args) do
 
     for key, data in pairs(args) do
Line 811: Line 1,022:
  
 
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 args[1] then
 
        name = args[1]
 
    else
 
        name = mw.title.getCurrentTitle().text
 
     end
 
 
 
    item = findItem(name)
 
  
     if item == 0 then
+
     if not item then
 
         return "<div style=\"color:red\"> No item named '" .. name .. "'</div>. The Module:Items/data maybe outdated."
 
         return "<div style=\"color:red\"> No item named '" .. name .. "'</div>. The Module:Items/data maybe outdated."
 
     end
 
     end
  
     infobox = createInfobox(item)
+
     return createInfobox(item)
    return infobox
 
 
end
 
end
  
 
return p
 
return p

Revision as of 22:05, 23 May 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 The number to format.
---@param digits number The number of decimal places to include (must be a non-negative integer).
---@return string # The formatted number as a string.
local function toFixed(num, digits)
    digits = math.max(0, math.floor(digits))
    local formatted = string.format("%." .. digits .. "f", num):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
---@return AugmentingLoot|nil
local function getAugLoot(id)
    return p.loadData("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

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 a = getAbility(id)
            if a then
                local attributes = {
                    src = fullUrl(a.abilityImage),
                    alt = a.abilityName .. ". " .. a.description,
                    width = 30,
                    class = "tooltip",
                    title = a.abilityName .. ". " .. a.description,
                }
                local css = {
                    ["box-shadow"] = "0 0 2px 1px " .. (colors[a.damageType] or "white"),
                    background = colors[a.damageType] or "white",
                    margin = "5px"
                }
                table.insert(images, createWikitextImage(a.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 .. scrapping.chance * 100 .. "% "
        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 transforms ItemTransform[] # list of transforms
local function addTransforms(args, augTransform, transforms)
    local list = {}
    for _, transform in ipairs(transforms) do
        if (transform.augmentingTransform or false) == augTransform then
            local image = itemImage(transform.newItemID, true)
            if transform.augmentingTransform then
                local text = "At level " .. (transform.augmentationLevel or 1) .. " to " .. image
                table.insert(list, text)
            else
                table.insert(list, transform.chance * 100 .. "% " .. image)
            end
        end
    end
    args[sbreak()] = "yes"
    args[sl()] = "Transforms"
    args[sd()] = table.concat(list, "<br>")
end

---@param args table
---@param item Item
---@return boolean
local function addResearch(args, item)
    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

        local transforms = augLoot and augLoot.transforms or {}
        addTransforms(args, false, transforms)

        return true
    end

    return false
end

---@param prefix 'offensiveDamageAffinity'|'offensiveAccuracyAffinityRating'|'defensiveDamageAffinity'
---@param description string|nil
---@param uppercase boolean|nil
---@return table<string, string>
local function generateAffinityDictionary(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(generateAffinityDictionary("offensiveDamageAffinity", "") or {}) do
        equipmentStatsToLabelMapping[k] = v
    end
    for k, v in pairs(generateAffinityDictionary("offensiveAccuracyAffinityRating", "Accuracy") or {}) do
        equipmentStatsToLabelMapping[k] = v
    end
    for k, v in pairs(generateAffinityDictionary("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 = {}
    local text = ""
    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()] = stat.chance * 100 .. "%"
            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 stat.Melee * 100 - 100 .. "%"
            args[sl()] = "Magic"
            args[sd()] = stat.Magic and stat.Magic * 100 - 100 .. "%"
            args[sl()] = "Range"
            args[sd()] = stat.Range and stat.Range * 100 - 100 .. "%"
            args[sbreak()] = "yes"

            args[sl()] = "Piercing"
            args[sd()] = stat.Piercing and stat.Piercing * 100 - 100 .. "%"
            args[sl()] = "Blunt"
            args[sd()] = stat.Blunt and stat.Blunt * 100 - 100 .. "%"
            args[sl()] = "Slashing"
            args[sd()] = stat.Slashing and stat.Slashing * 100 - 100 .. "%"
            args[sl()] = "Fire"
            args[sd()] = stat.Fire and stat.Fire * 100 - 100 .. "%"
            args[sl()] = "Ice"
            args[sd()] = stat.Ice and stat.Ice * 100 - 100 .. "%"
            args[sl()] = "Nature"
            args[sd()] = stat.Nature and stat.Nature * 100 - 100 .. "%"
            args[sl()] = "Chaos"
            args[sd()] = stat.Chaos and stat.Chaos * 100 - 100 .. "%"
            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()] = stat.chance * 100 .. "%"
            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 stat.Melee * 100 - 100 .. "%"
            args[sl()] = "Magic"
            args[sd()] = stat.Magic and stat.Magic * 100 - 100 .. "%"
            args[sl()] = "Range"
            args[sd()] = stat.Range and stat.Range * 100 - 100 .. "%"
            args[sbreak()] = "yes"

            args[sl()] = "Piercing"
            args[sd()] = stat.Piercing and stat.Piercing * 100 - 100 .. "%"
            args[sl()] = "Blunt"
            args[sd()] = stat.Blunt and stat.Blunt * 100 - 100 .. "%"
            args[sl()] = "Slashing"
            args[sd()] = stat.Slashing and stat.Slashing * 100 - 100 .. "%"
            args[sl()] = "Fire"
            args[sd()] = stat.Fire and stat.Fire * 100 - 100 .. "%"
            args[sl()] = "Ice"
            args[sd()] = stat.Ice and stat.Ice * 100 - 100 .. "%"
            args[sl()] = "Nature"
            args[sd()] = stat.Nature and stat.Nature * 100 - 100 .. "%"
            args[sl()] = "Chaos"
            args[sd()] = stat.Chaos and stat.Chaos * 100 - 100 .. "%"
            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)
		return craftAug and craftAug.augmenting;
	end

    ---@return table|nil
    local function getScrappingByID(id)
        local craftAug = getCraftAug(id)
		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

    args[h()] = "Enchanting"

    local craftAug = getCraftAug(item.id)
    if craftAug then
        local costs = {}
        for id, cost in pairs(craftAug.scrapping or {}) do
            table.insert(costs, lang:formatNum(cost) .. " " .. itemImage(id, true))
        end

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

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

        args[h()] = "Augmentation"
        if canAugment(item.id) 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

            local augLoot = getAugLoot(item.id)
            local transforms = augLoot and augLoot.transforms
            if transforms then addTransforms(args, true, transforms) end
        end

        args[h()] = "Research"
        if canScrap(item.id) then
            addResearch(args, item)
        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, "<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