Module:Infobox Item

From Idlescape Wiki
Jump to navigation Jump to search

local p = {}

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

local loaded_modules = {}

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

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

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

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

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

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

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

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

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

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

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

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

    return loaded_modules[module_name]
end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return table.concat(images, "")
end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    args[sbreak()] = "yes"

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

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

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

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

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

    addTransforms(args, false, transformsTo, transformsFrom)

    return craftAug and canResearch or transformsFrom ~= nil
end

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

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

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

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

    return equipmentStatsToLabelMapping[bonusName] or bonusName
end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        args[h()] = "Offensive Stats"

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

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

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

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

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

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

        args[h()] = "Defensive Stats"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        args[h()] = "Farming"

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

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

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

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

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

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

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

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

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

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

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

    return createInfobox(item)
end

return p