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