Difference between revisions of "Module:Infobox Item"

From Idlescape Wiki
Jump to navigation Jump to search
(Fix cooking categories if no tags)
(Fix enchanting if in CraftingAugmenting data but not scrapping or augmenting keys)
Line 876: Line 876:
 
     end
 
     end
  
 +
    --TODO: add transform source
 
     args[h()] = "Enchanting"
 
     args[h()] = "Enchanting"
  
     local craftAug = getCraftAug(item.id)
+
     if getScrappingByID(item.id) then
    if craftAug then
+
        local craftAug = getCraftAug(item.id)
 
         local costs = {}
 
         local costs = {}
 
         for id, cost in pairs(craftAug.scrapping or {}) do
 
         for id, cost in pairs(craftAug.scrapping or {}) do

Revision as of 08:05, 24 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

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

    if getScrappingByID(item.id) then
        local craftAug = getCraftAug(item.id)
        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 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