Module:Infobox Item

From Idlescape Wiki
Jump to navigation Jump to search

local p = {}

local data_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',
    itemsets = 'Module:Item sets',
}

local loaded_data_modules = {}

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

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 = data_module_names[data_type]
    if loaded_data_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_data_modules[module_name] = module
        else
            error("Failed to load module: " .. module_name .. "\n" .. module)
        end
    end

    return loaded_data_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

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

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

local function getEnchantmentName(id)
    local enchantment = getEnchantment(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 AugmentingLoot|nil
local function getAugLoot(id)
    return p.loadData("augloot")[tostring(id)]
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)
    local s = fullUrl(url)
    s = "[[" .. name .. "|<img src=\"" .. s
    s = s .. "\" alt=\""  .. name .. "\" width=\"20\">"
    if word then
        s = s .. name
    end
    s = s .. "]]"
    return s
end

local function itemImage(id, word)
    local item = p.loadData("item")[tostring(id)]
    local url = ""
    if item.itemIcon then
        url = item.itemIcon
    else
        url = item.itemImage
    end
    return icon(item.name, url, word)
end

local function addSeparator(num)
    return tostring(tonumber(num)):reverse():gsub("(%d%d%d)","%1,"):gsub(",(%-?)$","%1"):reverse()
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
---@param transforms ItemTransform[]
local function addTransforms(args, augTransform, transforms)
    local text = ""
    for _, transform in ipairs(transforms) do
        if (transform.augmentingTransform or false) == augTransform then
            if transform.augmentingTransform then
                text = text .. "At level " .. (transform.augmentationLevel or 1) .. " to "
            else
                text = text .. transform.chance * 100 .. "% "
            end
            text = text .. itemImage(transform.newItemID, true) .. "<br>"
        end
    end
    text = text:gsub("<br>$", "")
    args[sbreak()] = "yes"
    args[sl()] = "Transforms"
    args[sd()] = text
end

---@param args table
---@param item Item
---@return boolean
local function addResearch(args, item)
--local function addResearch(args, id, addHeader)
    local craftAug = getCraftAug(item.id)
    local augLoot = getAugLoot(tostring(item.id))
    if item and craftAug then
        if not item.blockAugmenting then
            args[h()] = "Research"
        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 {}
        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

local function createInfobox(item)
    local args = {}
    local url = ""
    local text = ""
    args.autoheaders = "y"
    args.subbox = "no"
    args.bodystyle = " "
    args.title = item.name

    if item.itemIcon then
        url = item.itemIcon
    else
        url = item.itemImage
    end
    args.image = "<img src=\"" .. fullUrl(url) .. "\" width=\"150\">"

    if item.value then
        args[l()] = icon('Gold', "/images/gold_coin.png")
        args[d()] = addSeparator(item.value)
    end

    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.requiredLevel then
        text = ""
        for skill, level in pairs(item.requiredLevel) do
            skill = skill:gsub("^%l", string.upper)
            text = text .. level .. " " .. skill .. "<br>"
        end
        text = text:gsub("<br>$", "")
        args[l()] = "Level Required"
        args[d()] = text
    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)
        args[d()] = addSeparator(item.heat)
    end

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

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

        if item.forcedEnchant then
            args[l()] = "Enchantments"
            args[d()] = "[[" .. getEnchantmentName(item.forcedEnchant) .. "]]"
        end

        if stats.toolBoost then
            text = ""
            for key, stat in pairs(stats.toolBoost) do
                if stat.boost ~= 0 then
                    text = text .. stat.boost .. " "
                    text = text .. getLabelFromAugBonus("toolBoost." .. stat.skill) .. "<br>"
                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

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


        args[h()] = "Offensive Stats"

        stat = stats.offensiveCritical
        if stat then
            args[l()] = "Crit Chance"
            args[d()] = stat.chance * 100 .. "%"
            args[l()] = "Crit Multiplier"
            args[d()] = stat.damageMultiplier
        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()] = "Set Bonus"
        for id, desc in pairs(p.loadData("itemsets")._formatItemSets(stats.itemSet)) do
            local enchant = getEnchantment(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

    if item.blockAugmenting then
        args[h()] = "Research"
    else
        args[h()] = "Augmentation"
    end
    local craftAug = getCraftAug(item.id)

    if craftAug and craftAug.scrapping then
        if item.equipmentStats then
            text = ""
            for key, bonus in pairs(item.equipmentStats.augmentationBonus) do
                text = text .. "+" .. bonus.value .. " "
                text = text .. getLabelFromAugBonus(bonus.stat) .. "<br>"
            end
            text = text:gsub("<br>$", "")
            args[sl()] = "Aug Bonus"
            args[sd()] = text
        end

        text = ""
        local costs = {}
        for key, cost in pairs(craftAug.scrapping) do
            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

        text = text:gsub("<br>$", "")
        args[sl()] = "Cost"
        args[sd()] = text

        local augLoot = getAugLoot(item.id)
        local transforms = augLoot and augLoot.transforms or {}
        addTransforms(args, true, transforms) --only add transforms if the item transforms by augmenting
        addResearch(args, item)
    end

    args[h()] = "Cooking"

    if item.size then
        args[l()] = "Size"
        args[d()] = item.size
    end

    if item.difficulty then
        args[l()] = "Difficulty"
        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[d()] = text
    end

    if item.cookingEnchantment then
        args[l()] = "Buff"
        args[d()] = getEnchantmentName(item.cookingEnchantment)
    end

    args[h()] = "Seeds"

    local farming = item.farmingStats
    if farming then
        local farmingData = p.loadData('farming')
        local seedData = farmingData[item.id]
        if seedData then
            text = ""
            for key, yield in pairs(seedData) do
                text = text .. yield.min .. "-" .. yield.max .. " "
                text = text .. itemImage(yield.id, true)
                if yield.chance ~= 1 then
                    text = text .. " " .. tonumber(string.format('%.2f', yield.chance * 100)) .. "%"
                end
                text = text .. "<br>"
            end
            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

    local args2 = {}

    args2.subbox = "yes"
    args2.bodystyle = "padding: 0.5em; margin:auto; font-style:italic; font-size:110%; text-align: center"
    args2.data1 = item.extraTooltipInfo
    args[h()] = "Tooltip"
    args[d()] = require('Module:Infobox').infobox(args2)

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

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

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

function p._item(args)
    local name = ""
    local item = 0
    local infobox = ""

    if args[1] then
        name = args[1]
    else
        name = mw.title.getCurrentTitle().text
    end

    item = findItem(name)

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

    infobox = createInfobox(item)
    return infobox
end

return p