Module:Items: Difference between revisions

From Melvor Idle
(Added getCreationTable)
(Add blocked/occupied slot)
 
(261 intermediate revisions by 9 users not shown)
Line 1: Line 1:
--This module contains all sorts of functions for getting data on items
--Several functions related to use tables can be found at Module:Items/UseTables
--Functions related to source tables can be found at Module:Items/SourceTables
--Other functions moved to Module:Items/ComparisonTables
local p = {}
local p = {}


local ItemData = mw.loadData('Module:Items/data')
local GameData = require('Module:GameData')
 
local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local Shared = require('Module:Shared')
local Modifiers = require('Module:Modifiers')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')
local Num = require('Module:Number')
-- Used by p._getItemLevelReqs()
local LevelReqCache = {}
p.EasterEggs = {'Amulet of Calculated Promotion', 'Clue Chasers Insignia', '8', 'Lemon', 'Easter Egg',
'Abnormal Log', 'Red Herring', 'Cool Glasses'}
p.EventItems = {'Christmas Cracker', 'Christmas Coal', 'Christmas Sweater',
'Christmas Wreath', 'Candy Cane', 'Santa Hat',
'Friendship Bracelet', 'Event Clue 1', 'Event Clue 2',
'Event Clue 3', 'Event Clue 4', 'Candle', 'Cake Base',
'Magical Flavouring', 'Magical Icing', 'Birthday Cake',
'Purple Party Hat', 'Birthday Token', 'Christmas Present (Yellow)',
'Christmas Present (Blue)', 'Christmas Present (Green)', 'Christmas Present (White)',
'Christmas Present (Purple)', 'Christmas Present (Standard)', 'Event Token - Holiday 2021',
'Holiday Scarf', 'Gingerbread House', 'Gingerbread Man', 'Edible Candy Cane',
'Locked Chest', 'Locked Chest Key', 'Event Token (Holiday 2021)'}
-- List of item IDs that should typically not be included within outputs, usually
-- because they are not fully implemented despite existing within the game data
p.HiddenItems = {}
local function populateHiddenItems()
local hiddenItems = GameData.getEntities('items',
function(item)
return item.name == nil or Shared.contains({'melvorTotH:Meteorite_Dust'}, item.id)
end
)
for _, item in ipairs(hiddenItems) do
table.insert(p.HiddenItems, item.id)
end
end
populateHiddenItems()


function p.getItemByID(ID)
function p.getItemByID(ID)
  local result = Shared.clone(ItemData[ID + 1])
return GameData.getEntityByID('items', ID)
  if result ~= nil then
    result.id = ID
  end
  return result
end
end


function p.getItem(name)
function p.getItem(name)
  local result = nil
name = Shared.fixPagename(name)
  for i, item in pairs(ItemData) do
return GameData.getEntityByName('items', name)
    if(item.name == name) then
end
      result = Shared.clone(item)
 
      --Make sure every item has an id, and account for Lua being 1-index
function p.getItems(checkFunc)
      result.id = i -1
return GameData.getEntities('items',
    end
function(obj)
  end
return not Shared.contains(p.HiddenItems, obj.id) and checkFunc(obj)
  return result
end
)
end
 
function p._canItemUseSlot(item, equipSlot)
--Function to easily check if an item can fit in a given equipment slot
--Ex: p._canItemUseSlot({Bronze Platebody}, 'Platebody') returns true
if type(item) == 'string' then
item = p.getItem(item)
end
return item.validSlots ~= nil and Shared.contains(item.validSlots, equipSlot)
end
 
function p._getItemEquipSlot(item)
--Function to return the (non-Passive) equipment slot that an item occupies
if type(item) == 'string' then
item = p.getItem(item)
end
if item == nil or item.validSlots == nil then
return 'Invalid'
end
for i, slot in pairs(item.validSlots) do
if slot ~= 'Passive' then
return slot
end
end
end
 
-- Given an item, returns a table containing the item's level requirements (if any)
-- The table structure is as follows:
-- { ['normal'] = { SkillID = SkillLevel, ... }, ['abyssal'] = { SkillID = SkillLevel, ... } }
-- If optional parameter 'condense' is specified as true, any normal requirements are excluded
-- from the output if there should exist an abyssal level requirement for the same skill
function p._getItemLevelReqs(item, condense)
local condenseResult = (type(condense) == 'boolean' and condense) or false
local reqTypeMap = {
['SkillLevel'] = 'normal',
['AbyssalLevel'] = 'abyssal'
}
local levelReqs = LevelReqCache[item.id]
if levelReqs == nil then
-- Item not in cache - obtain level requirements data
if item.equipRequirements == nil then
return nil
end
levelReqs = { ['normal'] = {}, ['abyssal'] = {} }
for _, requirement in ipairs(item.equipRequirements) do
local reqTypeKey = reqTypeMap[requirement.type]
if reqTypeKey ~= nil then
-- Requirement is a level requirement
local reqSkillID, reqLevel = requirement.skillID, requirement.level
local reqCurrentLevel = levelReqs[reqTypeKey][reqSkillID]
if reqCurrentLevel == nil or reqLevel > reqCurrentLevel then
levelReqs[reqTypeKey][reqSkillID] = reqLevel
end
end
end
-- Cache level requirements data for any subsequent lookups
LevelReqCache[item.id] = levelReqs
end
 
if not condenseResult then
-- Return requirements as is
return levelReqs
else
-- Exclude any normal level requirements should an abyssal requirement exist for
-- the same skill
local levelReqsCondensed = { ['normal'] = {}, ['abyssal'] = levelReqs.abyssal }
for skillID, reqLevel in pairs(levelReqs.normal) do
if levelReqs.abyssal[skillID] == nil then
levelReqsCondensed.normal[skillID] = reqLevel
end
end
return levelReqsCondensed
end
end
end


function p._getItemStat(item, StatName, ZeroIfNil)
function p._getItemStat(item, StatName, ZeroIfNil)
  local result = item[StatName]
local result = item[StatName]
  --Special Overrides:
--Special Overrides:
  if StatName == 'stabAttackBonus' then
-- Equipment stats first
    if item.attacBonus == nil then  
if item.equipmentStats ~= nil and item.equipmentStats[StatName] ~= nil then
      result = nil
result = item.equipmentStats[StatName]
    else
elseif StatName == 'attackSpeed' and item.validSlots ~= nil and Shared.contains(item.validSlots, 'Weapon') then
      result = item.attackBonus[1]
-- Item can be equipped as a weapon but has no attack speed, so use default of 4000ms
    end
result = 4000
  elseif StatName == 'slashAttackBonus' then
elseif StatName == 'isTwoHanded' then
    if item.attackBonus == nil then  
if item.validSlots ~= nil and item.occupiesSlots ~= nil then
      result = nil
result = Shared.contains(item.validSlots, 'Weapon') and Shared.contains(item.occupiesSlots, 'Shield')
    else
else
      result = item.attackBonus[2]
result = false
    end
end
  elseif StatName == 'blockAttackBonus' then
elseif (
    if item.attackBonus == nil then  
string.find(StatName, '^(.+)LevelRequired$') ~= nil
      result = nil
or string.find(StatName, '^(.+)AbyssalLevel$') ~= nil
    else
) and item.equipRequirements ~= nil then
      result = item.attackBonus[3]
local levelType = 'normal'
    end
local skillName = string.match(StatName, '^(.+)LevelRequired$')
  elseif StatName == 'attackType' then
if skillName == nil then
    result = p._getWeaponAttackType(item)
levelType = 'abyssal'
  end
skillName = string.match(StatName, '^(.+)AbyssalLevel$')
  if result == nil and ZeroIfNil then result = 0 end
end
  return result
if skillName ~= nil then
local skillID = Constants.getSkillID(Shared.titleCase(skillName))
if skillID ~= nil then
local levelReqs = p._getItemLevelReqs(item)
result = levelReqs ~= nil and levelReqs[levelType][skillID]
end
end
elseif StatName == 'attackType' then
result = p._getWeaponAttackType(item)
elseif StatName == 'description' then
result = item.customDescription
if result == nil or result == '' then result = 'No Description' end
elseif StatName == 'completionReq' then
if item.ignoreCompletion == nil or not item.ignoreCompletion then
result = 'Yes'
else
result = 'No'
end
elseif StatName == 'slayerBonusXP' then
return p._getItemModifier(item, 'increasedSkillXP', 'Slayer', false)
elseif StatName == 'hasCombatStats' then
return tostring(p.hasCombatStats(item) or p._hasLevelRequirements(item))
elseif StatName == 'category' then
-- Some categories have a namespace for some reason, remove it
local _, localID = GameData.getLocalID(result)
return localID
end
if result == nil and ZeroIfNil then result = 0 end
return result
end
 
function p.getItemValueByID(itemID)
local item = p.getItemByID(itemID)
if item == nil then
return 0
end
return p.getItemValue(item['name'])
end
 
function p.getItemValue(item)
if type(item) == 'string' then
-- Specific check if the item is GP (value of 1)
if Shared.compareString('GP', item, true)
or Shared.compareString('Gold Pieces', item, true) then
return 1
end
 
item = p.getItem(item)
end
if item then
return item.sellsFor
end
return nil
end
 
function p.getValueText(item, minQuantity, maxQuantity)
local minQ, maxQ = 1, 1
if type(minQuantity) == 'number' then
minQ = minQuantity
end
if type(maxQuantity) == 'number' then
maxQ = maxQuantity
else
maxQ = minQ
end
local amt = item.sellsFor or 0
local currID = item.sellsForCurrency or 'melvorD:GP'
return Icons._Currency(currID, amt * minQ, amt * maxQ)
end
 
-- Function already exists, but without frame.
-- Giving it a slightly different name since function overloading doesn't exist
function p.getItemSellsFor(frame)
local args = frame:getParent().args
 
return p._getItemSellsFor(args[1], args[2], args.round)
end
 
function p._getItemSellsFor(itemName, multiplier, rounding)
local itemValue = p.getItemValue(itemName)
multiplier = tonumber(multiplier) or 1
rounding = tonumber(rounding) or 0
if itemValue == nil then
error('No item named "' .. itemName .. '" exists in the data module')
end
 
return Num.round2(itemValue * multiplier, rounding)
end
end


function p.getItemStat(frame)
function p.getItemStat(frame)
  local args = frame.args ~= nil and frame.args or frame
local args = frame.args ~= nil and frame.args or frame
  local ItemName = args[1]
local ItemName = args[1]
  local StatName = args[2]
local StatName = args[2]
  local ZeroIfNil = args.ForceZero ~= nil and args.ForceZero ~= '' and args.ForceZero ~= 'false'
local ZeroIfNil = args.ForceZero ~= nil and args.ForceZero ~= '' and args.ForceZero ~= 'false'
  local item = p.getItem(ItemName)
local formatNum = args.formatNum ~= nil and args.formatNum ~= '' and args.formatNum ~= 'false'
  if item == nil then
local item = p.getItem(ItemName)
    return "ERROR: No item named "..ItemName.." exists in the data module"
if item == nil then
  end
return Shared.printError('No item named "' .. ItemName .. '" exists in the data module')
  return p._getItemStat(item, StatName, ZeroIfNil)
end
local result = p._getItemStat(item, StatName, ZeroIfNil)
if formatNum then result = Num.formatnum(result) end
return result
end
 
--Gets the value of a given modifier for a given item
--asString is false by default, when true it writes the full bonus text
function p._getItemModifier(item, modifier, skillID, asString)
if asString == nil then asString = false end
if skillID == '' then
skillID = nil
elseif string.find(skillID, ':') == nil then
-- Try to find a skill ID if it looks like a skill name has been passed
skillID = Constants.getSkillID(skillID)
end
 
local result = 0
 
if item.modifiers ~= nil and item.modifiers[modifier] ~= nil then
if type(item.modifiers[modifier]) == 'table' then
for i, subVal in Shared.skpairs(item.modifiers[modifier]) do
if subVal[1] == skillID then
result = subVal[2]
break
end
end
else
result = item.modifiers[modifier]
end
end
 
if asString then
if skillID ~= nil then
return Constants._getModifierText(modifier, {skillID, result})
else
return Constants._getModifierText(modifier, result)
end
else
return result
end
end
 
function p.hasCombatStats(item)
-- Checks if the combat stat is a valid, non-zero combat stat
-- Ensure that, only in the case where the item is a Familar AND
-- the checked stat is summoningMaxhit, the result is ignored.
local function isNonZeroStat(statName, statVal)
if statName == 'summoningMaxhit' and (p._canItemUseSlot(item, 'Summon1') or p._canItemUseSlot(item, 'Summon2')) then
return false
end
return statVal ~= 0
end
 
if item.equipmentStats ~= nil then
-- Ensure at least one stat has a non-zero value
for statName, statVal in pairs(item.equipmentStats) do
if isNonZeroStat(statName, statVal) then
return true
end
end
end
 
return false
end
 
function p._hasLevelRequirements(item)
--Function true if an item has at least one level requirement to equip
local levelReqs = p._getItemLevelReqs(item)
if levelReqs ~= nil then
for levelType, reqs in pairs(levelReqs) do
if not Shared.tableIsEmpty(reqs) then
return true
end
end
end
return false
end
 
function p.getItemModifier(frame)
local itemName = frame.args ~= nil and frame.args[1] or frame[1]
local modName = frame.args ~= nil and frame.args[2] or frame[2]
local skillName = frame.args ~= nil and frame.args[3] or frame[3]
local asString = frame.args ~= nil and frame.args[4] or frame[4]
if asString ~= nil then
asString = (string.upper(asString) ~= 'FALSE')
end
 
local item = p.getItem(itemName)
if item == nil then
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end
 
return p._getItemModifier(item, modName, skillName, asString)
end
end


function p._getWeaponAttackType(item)
function p._getWeaponAttackType(item)
  if item.type == 'Weapon' then
if (item.validSlots ~= nil and Shared.contains(item.validSlots, 'Weapon')) or
    return Icons.Icon({'Melee'})
(item.occupiesSlots ~= nil and Shared.contains(item.occupiesSlots, 'Weapon')) then
  elseif item.type == 'Ranged Weapon' then
if Shared.contains({'melee', 'ranged', 'magic'}, item.attackType) then
    return Icons.Icon({'Ranged', type='skill'})
local iconType = item.attackType ~= 'melee' and 'skill' or nil
  elseif item.type == 'Magic Staff' or item.type == 'Magic Wand' then
return Icons.Icon({Shared.titleCase(item.attackType), type=iconType, nolink='true'})
    return Icons.Icon({'Magic', type='skill'})
end
  else
end
    return "Invalid"
return 'Invalid'
  end
end
 
function p.getWeaponAttackType(frame)
local itemName = frame.args ~= nil and frame.args[1] or frame
local item = p.getItem(itemName)
if item == nil then
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end
return p._getWeaponAttackType(item)
end
 
local statChangeDefs = {
{
stat = 'stabAttackBonus',
suffix = ' ' .. Icons.Icon({'Melee', notext=true}) .. ' Stab Bonus'
},
{
stat = 'slashAttackBonus',
suffix =  ' ' .. Icons.Icon({'Melee', notext=true}) .. ' Slash Bonus'
},
{
stat = 'blockAttackBonus',
suffix = ' ' .. Icons.Icon({'Melee', notext=true}) .. ' Block Bonus'
},
{
stat = 'meleeStrengthBonus',
suffix = ' ' .. Icons.Icon({'Strength', type='skill', notext=true}) .. ' Strength Bonus'
},
{
stat = 'rangedStrengthBonus',
suffix =  ' ' .. Icons.Icon({'Ranged', type='skill', notext=true}) .. ' Strength Bonus'
},
{
stat = 'magicStrengthBonus',
suffix = '% ' .. Icons.Icon({'Magic', type='skill', notext=true}) .. ' Damage Bonus'
},
{
stat = 'meleeDefenceBonus',
suffix = ' ' .. Icons.Icon({'Defence', type='skill', notext=true}) .. ' Defence Bonus' },
{
stat = 'rangedDefenceBonus',
suffix = ' ' .. Icons.Icon({'Ranged', type='skill', notext=true}) .. ' Defence Bonus'
},
{
stat = 'magicDefenceBonus',
suffix = ' ' .. Icons.Icon({'Magic', type='skill', notext=true}) .. ' Defence Bonus'
},
{
stat = 'damageReduction',
suffix = '% Damage Reduction'
},
{
stat = 'levelRequired',
suffix = ' Level Required'
}
}
 
-- Produces a list of stat & modifier changes between two items of equipmednt
function p.getStatChangeString(item1, item2)
local changeArray = {}
 
local equipStats = {
type(item1.equipmentStats) == 'table' and item1.equipmentStats or {},
type(item2.equipmentStats) == 'table' and item2.equipmentStats or {}
}
for i, statDef in ipairs(statChangeDefs) do
local val1, val2 = 0, 0
if statDef.stat == 'levelRequired' then
-- Iterate over equipment stats for both items, determining level requirements
local levelReqs = {}
for itemNum, item in ipairs({item1, item2}) do
levelReqs[itemNum] = {}
if item.equipRequirements ~= nil then
for j, req in ipairs(item.equipRequirements) do
if req.type == 'SkillLevel' then
levelReqs[itemNum][req.skillID] = req.level
end
end
end
end
-- Iterate over all skills, checking if there are requirements for these in either skill
for j, skillData in ipairs(GameData.rawData.skillData) do
local skillID = skillData.skillID
val1, val2 = levelReqs[1][skillID] or 0, levelReqs[2][skillID] or 0
if val1 ~= val2 then
table.insert(changeArray, Num.numStrWithSign(val1 - val2) .. ' ' .. Icons.Icon({skillData.data.name, type='skill', notext=true}) .. (statDef.suffix or ''))
end
end
else
-- Equipment stats
val1, val2 = equipStats[1][statDef.stat] or 0, equipStats[2][statDef.stat] or 0
if val1 ~= val2 then
table.insert(changeArray, Num.numStrWithSign(val1 - val2) .. (statDef.suffix or ''))
end
end
end
 
-- Include differences in modifiers
-- TODO Implement getModifiersDifference
--local modDiff = Constants.getModifiersText(Constants.getModifiersDifference(item2.modifiers, item1.modifiers))
local modDiff = nil
if modDiff ~= nil and modDiff ~= '' then
table.insert(changeArray, modDiff)
end
 
return table.concat(changeArray, '<br/>')
end
 
function p._getOtherItemBoxText(item)
local resultPart = {}
--For equipment, show the slot they go in
local isPassive = false
if item.validSlots ~= nil then
local slotLinkMap = {
["Helmet"] = 'Helmets',
["Platebody"] = 'Platebodies',
["Platelegs"] = 'Platelegs',
["Boots"] = 'Boots',
["Weapon"] = 'Weapons',
["Shield"] = 'Shields',
["Amulet"] = 'Amulets',
["Ring"] = 'Rings',
["Gloves"] = 'Gloves',
["Quiver"] = 'Ammunition',
["Cape"] = 'Capes',
["Consumable"] = 'Consumables',
["Passive"] = 'Combat Passive Slot',
["Summon1"] = 'Summoning',
["Summon2"] = 'Summoning',
["Gem"] = "Gems_(Equipment)"
}
function getSlotText(slotList)
local slotText = {}
for i, slot in ipairs(slotList) do
local slotLink = slotLinkMap[slot]
if slotLink == nil then
table.insert(slotText, slot)
else
table.insert(slotText, '[[' .. slotLink .. '|' .. slot .. ']]')
end
if slot == 'Passive' then
isPassive = true
end
end
return slotText
end
slotText = getSlotText(item.validSlots)
table.insert(resultPart, "\r\n|-\r\n|'''Equipment Slot:''' "..table.concat(slotText, ', '))
if item.occupiesSlots ~= nil then
slotText = getSlotText(item.occupiesSlots)
table.insert(resultPart, "\r\n|-\r\n|'''Blocked Slot:''' "..table.concat(slotText, ', '))
end
end
--For weapons with a special attack, show the details
if item.specialAttacks ~= nil and not Shared.tableIsEmpty(item.specialAttacks) then
table.insert(resultPart, "\r\n|-\r\n|'''Special Attack:'''")
for i, spAttID in ipairs(item.specialAttacks) do
local spAtt = GameData.getEntityByID('attacks', spAttID)
if spAtt ~= nil then
local spAttChance = spAtt.defaultChance
if type(item.overrideSpecialChances) == 'table' and item.overrideSpecialChances[i] ~= nil then
spAttChance = item.overrideSpecialChances[i]
end
local spAttDesc = string.gsub(spAtt.description, '<Attack> ', '')
table.insert(resultPart, '\r\n* ' .. spAttChance .. '% chance for ' .. spAtt.name .. ':')
table.insert(resultPart, '\r\n** ' .. spAttDesc)
end
end
end
-- For Summoning combat familiars, show the max hit
if item.equipmentStats ~= nil and item.equipmentStats.summoningMaxhit ~= nil then
table.insert(resultPart, "\r\n|-\r\n|'''Max Hit:''' " .. Num.formatnum(item.equipmentStats.summoningMaxhit * 10))
end
--For potions, show the number of charges
if item.charges ~= nil then
table.insert(resultPart, "\r\n|-\r\n|'''Charges:''' "..item.charges)
end
--For food, show how much it heals for
if item.healsFor ~= nil then
table.insert(resultPart, "\r\n|-\r\n|'''Heals for:''' "..Icons.Icon({"Hitpoints", type="skill", notext="true"})..' '..(item.healsFor * 10))
end
--For Prayer Points, show how many you get
if item.prayerPoints ~= nil then
table.insert(resultPart, "\r\n|-\r\n|'''"..Icons.Icon({'Prayer', type='skill'}).." Points:''' "..item.prayerPoints)
end
if item.soulPoints ~= nil then
table.insert(resultPart, "\r\n|-\r\n|'''"..Icons.Icon({'Prayer', 'Soul', type='item', img='Lesser Soul'}).." Points:''' "..item.soulPoints)
end
--For items that provide runes, show which runes are provided
if item.providedRunes ~= nil then
table.insert(resultPart, "\r\n|-\r\n|'''Runes Provided:''' ")
local runeLines = {}
local sortVal = ''
for j, runePair in pairs(item.providedRunes) do
local runeID = runePair.id
local qty = runePair.quantity
local rune = p.getItemByID(runeID)
sortVal = sortVal..rune.name..qty
table.insert(runeLines, Icons.Icon({rune.name, type='item', qty=qty}))
end
table.insert(resultPart, table.concat(runeLines, ', '))
end
--For items with modifiers, show what those are
if item.modifiers ~= nil and not Shared.tableIsEmpty(item.modifiers) then
table.insert(resultPart, "\r\n|-\r\n|'''Modifiers:'''\r\n")
if isPassive then
table.insert(resultPart, '<span style="color:green">Passive:</span><br/>')
end
table.insert(resultPart, Modifiers.getModifiersText(item.modifiers, true, false, 10))
end
return table.concat(resultPart)
end
 
function p.getOtherItemBoxText(frame)
local itemName = frame.args ~= nil and frame.args[1] or frame
local item = p.getItem(itemName)
if item == nil then
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end
 
return p._getOtherItemBoxText(item)
end
 
function p._getCurrencyItemBoxText(item)
local span = mw.html.create('span')
:wikitext("'''Sells For:''' ")
:wikitext(Icons._Currency((item.sellsForCurrency or 'melvorD:GP'), item.sellsFor))
return span
end
 
function p.getCurrencyItemBoxText(frame)
local itemName = frame.args ~= nil and frame.args[1] or frame
local item = p.getItem(itemName)
if item == nil then
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end
 
return p._getCurrencyItemBoxText(item)
end
 
function p._getItemCategories(item)
local resultPart = {}
local isEquipment = item.validSlots ~= nil or item.occupiesSlots ~= nil or item.equipmentStats ~= nil
local category = p._getItemStat(item, 'category', false)
if category ~= nil and category ~= 'Skills' then
table.insert(resultPart, '[[Category:'..category..']]')
end
if item.type ~= nil then
table.insert(resultPart, '[[Category:'..item.type..']]')
end
if isEquipment and item.tier ~= nil then
table.insert(resultPart, '[[Category:'..Shared.titleCase(item.tier)..' '..item.type..']]')
end
if item.specialAttacks ~= nil and not Shared.tableIsEmpty(item.specialAttacks) then
table.insert(resultPart, '[[Category:Items With Special Attacks]]')
end
if item.validSlots ~= nil then
local slotRemap = {
['Passive'] = 'Passive Items',
['Summon1'] = 'Summoning Familiars',
['Summon2'] = ''
}
for i, slotName in ipairs(item.validSlots) do
local slotRemapName = slotName
if slotRemap[slotName] ~= nil then slotRemapName = slotRemap[slotName] end
if slotRemapName ~= '' then table.insert(resultPart, '[[Category:' .. slotRemapName .. ']]') end
end
end
if item.modifiers ~= nil then
local modsDL = {
'increasedChanceToDoubleLootCombat',
'decreasedChanceToDoubleLootCombat',
'increasedChanceToDoubleLootThieving',
'decreasedChanceToDoubleLootThieving',
'increasedChanceToDoubleItemsGlobal',
'decreasedChanceToDoubleItemsGlobal'
}
for modName, val in pairs(item.modifiers) do
if Shared.contains(modsDL, modName) then
table.insert(resultPart, '[[Category:Double Loot Chance Items]]')
break
end
end
end
return table.concat(resultPart)
end
 
function p.getItemCategories(frame)
local itemName = frame.args ~= nil and frame.args[1] or frame
local item = p.getItem(itemName)
if item == nil then
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end
 
return p._getItemCategories(item)
end
 
function p.getItemGrid(frame)
--melvorF, melvorD, melvorTotH, melvorAoD
local dlcFunc = function(item, dlc)
local itemDLC = Shared.getLocalID(item.id)
if dlc == nil then
return true
end
if dlc == 'base' then
return itemDLC == 'melvorD' or itemDLC == 'melvorF'
end
if itemDLC == dlc then
return true
end
return false
end
-- Convert list of hidden items into a key/value structure, such that
-- lookups are more efficient than repeated calls to Shared.contains()
local hiddenItemIDs = {}
for i, itemID in ipairs(p.HiddenItems) do
hiddenItemIDs[itemID] = 1
end
 
local args = frame:getParent().args
local dlc = args[1] or args.DLC or args.dlc or nil
local columns = tonumber(args[2] or args.Columns or args.columns) or 17
local html = mw.html.create('table')
:addClass('wikitable lighttable individual table-img-grid')
 
local curRow = html:tag('tr')
local i = 0
for _, v in pairs(GameData.rawData.items) do
if hiddenItemIDs[v.id] == nil and dlcFunc(v, dlc) == true then
if i >= columns then
curRow = html:tag('tr')
i = 0
end
local cell = curRow:tag('td')
:wikitext(Icons.Icon({v.name, type='item', notext=true, size='32'}))
-- Mod operator is slow. We use this instead
i = i + 1
end
end
return tostring(html)
end
 
function p.getEquipRequirementRow(req)
local result = ""
if (req.type == "SkillLevel" or req.type == "AbyssalLevel") then
local pre = (req.type == "AbyssalLevel" and ' Abyssal') or ''
local skillName = Constants.getSkillName(req.skillID)
local skillIcon = Icons.Icon({skillName, type='skill', notext=true})
result = '\r\n!style="text-align:right;"| '..skillIcon..pre..' Level Required'
result = result..'\r\n|style="text-align:right;"| '..req.level
elseif (req.type == "DungeonCompletion" or req.type == "AbyssDepthCompletion") then
local reqDefns = {
["DungeonCompletion"] = {
["dataKey"] = 'dungeons',
["IDKey"] = 'dungeonID',
["imgType"] = 'dungeon'
},
["AbyssDepthCompletion"] = {
["dataKey"] = 'abyssDepths',
["IDKey"] = 'depthID',
["imgType"] = 'combatArea'
}
}
local reqDefn = reqDefns[req.type]
if reqDefn ~= nil then
local area = GameData.getEntityByID(reqDefn.dataKey, req[reqDefn.IDKey])
if area == nil then
result = '\r\n!style="text-align:right;" colspan=2|' .. Shared.printError('Invalid area for requirement type "' .. req.type .. '"')
else
local areaIcon = Icons.Icon({area.name, type=reqDefn.imgType, notext=true})
result = '\r\n!style="text-align:right;"| '..areaIcon..' Completions'
result = result..'\r\n|style="text-align:right;"| '..Num.formatnum(req.count)
end
end
elseif req.type == "Completion" then
local ns = GameData.getEntityByName('namespaces', req.namespace)
if ns == nil then
return '\r\n!style="text-align:right;" colspan=2|' .. Shared.printError('Invalid namespace for completion requirement "' .. req.namespace .. '"')
else
result = '\r\n!style="text-align:right;"| ' .. ns.displayName .. ' Completion'
result = result .. '\r\n|style="text-align:right;"| ' .. req.percent .. '%'
end
else
return '\r\n!style="text-align:right;" colspan=2|' .. Shared.printError('Invalid equip requirement type "' .. req.type .. '"')
end
return result
end
 
function p._getItemResistance(item)
local namespace = Shared.getLocalID(item.id)
if namespace == 'melvorItA' then
local eternalResistance = p._getItemStat(item, 'resistanceEternal', false)
if eternalResistance ~= nil or item.damageType ~= nil and item.damageType == 'melvorItA:Eternal' then
return eternalResistance or 0, 'Eternal Resistance'
else
return p._getItemStat(item, 'resistanceAbyssal', true), 'Abyssal Resistance'
end
else
return p._getItemStat(item, 'damageReduction', true), 'Damage Reduction'
end
end
 
function p.getWeaponStatsBox(frame)
local itemName = frame.args ~= nil and frame.args[1] or frame
local item = p.getItem(itemName)
if item == nil then
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end
 
local resistance, resistanceText = p._getItemResistance(item)
local ns, damageType = nil, nil
if item.damageType ~= nil then
ns, damageType = Shared.getLocalID(p._getItemStat(item, 'damageType'))
else
damageType = 'Normal'
end
 
local ico = {
["Attack"] = Icons.Icon({'Attack', type='skill', notext=true}),
["Combat"] = Icons.Icon({'Combat', notext=true}),
["Defence"] = Icons.Icon({'Defence', type='skill', notext=true}),
["Magic"] = Icons.Icon({'Magic', type='skill', notext=true}),
["Ranged"] = Icons.Icon({'Ranged', type='skill', notext=true}),
["Strength"] = Icons.Icon({'Strength', type='skill', notext=true}),
["Slayer"] = Icons.Icon({'Slayer', type='skill', notext=true}),
["Resistance"] = Icons.Icon({resistanceText, notext=true}),
["DamageType"] = Icons.Icon({(damageType .. ' Damage'), type="damage", notext=true})
}
local reqCount = item.equipRequirements ~= nil and Shared.tableCount(item.equipRequirements) or 0
local emptyRow = '\r\n!colspan="2"|'
local resultPart = {}
table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="4" style="border-bottom:solid medium black;"| Weapon Stats')
table.insert(resultPart, '\r\n|-\r\n!colspan="2" style="border-bottom:solid thin black;"| Offensive Stats')
table.insert(resultPart, '\r\n!colspan="2" style="border-bottom:solid thin black;"| Defensive Stats')
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| Attack Speed')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.round(p._getItemStat(item, 'attackSpeed', true) / 1000, 3, 1) .. 's')
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Defence'] .. ' Defence Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'meleeDefenceBonus', true))
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| Damage Type')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. ico['DamageType'] .. ' ' .. damageType)
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Resistance'] .. ' ' .. resistanceText)
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. resistance .. '%')
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| Attack Type')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'attackType'))
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Defence Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'rangedDefenceBonus', true))
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Strength'] .. ' Strength Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'meleeStrengthBonus', true)))
table.insert(resultPart, '\r\n!style="text-align:right;border-bottom:solid thin black;"| ' .. ico['Magic'] .. ' Defence Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;border-bottom:solid thin black;"| ' .. p._getItemStat(item, 'magicDefenceBonus', true))
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Stab Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'stabAttackBonus', true)))
table.insert(resultPart, '\r\n!colspan="2" style="border-bottom:solid thin black;"| Equip Requirements')
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Slash Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'slashAttackBonus', true)))
if reqCount > 0 then
table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[1]))
else
table.insert(resultPart, '\r\n|colspan=2 style="text-align:right"|None')
end
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Block Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'blockAttackBonus', true)))
if reqCount > 1 then
table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[2]))
else
table.insert(resultPart, emptyRow)
end
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Attack Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'rangedAttackBonus', true)))
if reqCount > 2 then
table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[3]))
else
table.insert(resultPart, emptyRow)
end
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Strength Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'rangedStrengthBonus', true)))
if reqCount > 3 then
table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[4]))
else
table.insert(resultPart, emptyRow)
end
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' Attack Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'magicAttackBonus', true)))
if reqCount > 4 then
table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[5]))
else
table.insert(resultPart, emptyRow)
end
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' % Damage Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'magicDamageBonus', true)) .. '%')
if reqCount > 5 then
table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[6]))
else
table.insert(resultPart, emptyRow)
end
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| Two Handed?')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. (p._getItemStat(item, 'isTwoHanded') and 'Yes' or 'No'))
if reqCount > 6 then
table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[6]))
else
table.insert(resultPart, emptyRow)
end
 
--Add extra rows at the end for items that have more than 3 different requirements
if reqCount > 7 then
for i = 8, reqCount, 1 do
table.insert(resultPart,"\r\n|-")
table.insert(resultPart, emptyRow)
table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[i]))
end
end
 
table.insert(resultPart, '\r\n|}')
return table.concat(resultPart)
end
 
function p.getArmourStatsBox(frame)
local itemName = frame.args ~= nil and frame.args[1] or frame
local item = p.getItem(itemName)
if item == nil then
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end
 
local resistance, resistanceText = p._getItemResistance(item)
 
local ico = {
["Attack"] = Icons.Icon({'Attack', type='skill', notext=true}),
["Combat"] = Icons.Icon({'Combat', notext=true}),
["Defence"] = Icons.Icon({'Defence', type='skill', notext=true}),
["Magic"] = Icons.Icon({'Magic', type='skill', notext=true}),
["Ranged"] = Icons.Icon({'Ranged', type='skill', notext=true}),
["Strength"] = Icons.Icon({'Strength', type='skill', notext=true}),
["Slayer"] = Icons.Icon({'Slayer', type='skill', notext=true}),
["Resistance"] = Icons.Icon({resistanceText,  notext=true}),
}
local reqCount = item.equipRequirements ~= nil and Shared.tableCount(item.equipRequirements) or 0
local emptyRow = '\r\n!colspan="2"|'
local resultPart = {}
table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="4" style="border-bottom:solid medium black;"| Armour Stats')
table.insert(resultPart, '\r\n|-\r\n!colspan="2" style="border-bottom:solid thin black;"| Offensive Stats')
table.insert(resultPart, '\r\n!colspan="2" style="border-bottom:solid thin black;"| Defensive Stats')
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Strength'] .. ' Strength Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'meleeStrengthBonus', true)))
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Defence'] .. ' Defence Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'meleeDefenceBonus', true)))
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Stab Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'stabAttackBonus', true)))
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Resistance'] .. ' ' .. resistanceText)
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. resistance .. '%')
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Slash Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'slashAttackBonus', true)))
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Defence Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'rangedDefenceBonus', true)))
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Block Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'blockAttackBonus', true)))
table.insert(resultPart, '\r\n!style="text-align:right;border-bottom:solid thin black;"| ' .. ico['Magic'] .. ' Defence Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;border-bottom:solid thin black;"| ' .. p._getItemStat(item, 'magicDefenceBonus', true))
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Attack Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'rangedAttackBonus', true)))
table.insert(resultPart, '\r\n!colspan="2" style="border-bottom:solid thin black;"| Equip Requirements')
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Strength Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'rangedStrengthBonus', true)))
if reqCount > 0 then
table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[1]))
else
table.insert(resultPart, '\r\n|colspan=2 style="text-align:right"|None')
end
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' Attack Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'magicAttackBonus', true)))
if reqCount > 1 then
table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[2]))
else
table.insert(resultPart, emptyRow)
end
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' % Damage Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'magicDamageBonus', true) .. '%')
if reqCount > 2 then
table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[3]))
else
table.insert(resultPart, emptyRow)
end
--Add extra rows at the end for items that have more than 3 different requirements
if reqCount > 3 then
for i = 4, reqCount, 1 do
table.insert(resultPart, "\r\n|-")
table.insert(resultPart, emptyRow)
table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[i]))
end
end
 
table.insert(resultPart, '\r\n|}')
return table.concat(resultPart)
end
 
function p.getItemDataExport(frame)
local resultTable = mw.html.create('table')
resultTable:addClass('wikitable')
resultTable:tag('tr'):addClass('headerRow-0')
:tag('th'):wikitext('ItemID'):done()
:tag('th'):wikitext('ItemName'):done()
:tag('th'):wikitext('GPValue'):done()
 
for i, item in ipairs(GameData.rawData.items) do
resultTable:tag('tr')
:tag('td'):wikitext(item.id):done()
:tag('td'):wikitext(item.name):done()
:tag('td'):wikitext(item.sellsFor):done()
end
return tostring(resultTable)
end
end


--Returns the expansion icon for the item if it has one
function p.getExpansionIcon(frame)
local itemName = frame.args ~= nil and frame.args[1] or frame
local item = p.getItem(itemName)
if item == nil then
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end


function p.getWeaponAttackType(frame)
return Icons.getExpansionIcon(item.id)
  local itemName = frame.args ~= nil and frame.args[1] or frame
  local item = p.getItem(itemName)
  if item == nil then
    return "ERROR: No item named "..ItemName.." exists in the data module"
  end
  return p._getWeaponAttackType(item)
end
end


function p.getPotionTable(frame)
function p.buildSmithableArmourNav(frame)
  local potionName = frame.args ~= nil and frame.args[1] or frame
local resultPart = {}
  local tiers = {'I', 'II', 'III', 'IV'}
table.insert(resultPart, '{| class="wikitable mw-collapsible navigation-not-searchable" style="margin:auto; clear:both; width: 100%"')
table.insert(resultPart, '\r\n!colspan = 2 style="background-color:#275C87;color:#FFFFFF;min-width:730px;"|')
table.insert(resultPart, Icons.Icon({'Smithing', type='skill', notext=true}))
table.insert(resultPart, ' Smithable Armour Sets')


  local result = '{| class="wikitable"'
local metalTypes = {'Bronze', 'Iron', 'Steel', 'Mithril', {'Adamant', 'Adamantite'}, {'Rune', 'Runite'}, {'Dragon', 'Dragonite'},
  result = result..'\r\n!Potion!!Tier!!Charges!!Effect'
{'Corundum', 'Corundumite', TotH = true}, {'Augite', 'Augite', TotH = true}, {'Divine', 'Divinite', TotH = true}}
local pieces = {"Helmet", "Platebody", "Platelegs", "Boots", "Shield"}
for i, metal in ipairs(metalTypes) do
local metalName, barName
local isTotH = false
if type(metal) == 'table' then
metalName = metal[1]
barName = metal[2]..' Bar'
isTotH = metal.TotH ~= nil and metal.TotH
else
metalName = metal
barName = metal..' Bar'
end
table.insert(resultPart, '\r\n|-\r\n!')
if isTotH then
table.insert(resultPart, Icons.TotH())
end
table.insert(resultPart, Icons.Icon({barName, type="item", notext=true}))
table.insert(resultPart, " "..metalName)
table.insert(resultPart, "\r\n|")


  for i, tier in pairs(tiers) do
for j, piece in ipairs(pieces) do
    local tierName = potionName..' '..tier
if j > 1 then
    local potion = p.getItem(tierName)
table.insert(resultPart, ' • ')
    if potion == nil then
end
      mw.log("Failed to get tier "..tier)
table.insert(resultPart, '<span style="display:inline-block">')
    else
table.insert(resultPart, Icons.Icon({metalName..' '..piece, piece, type='item'}))
      result = result..'\r\n|-'
if isTotH then
      result = result..'\r\n|'..Icons.Icon({tierName, type='item', notext='true', size='60'})
table.insert(resultPart, ' '..Icons.Icon({'(I) '..metalName..' '..piece, '(I)', type='item'}))
      result = result..'||'..'[['..tierName..'|'..tier..']]'
table.insert(resultPart, ' '..Icons.Icon({'(P) '..metalName..' '..piece, '(P)', type='item'}))
      result = result..'||'..potion.potionCharges..'||'..potion.description
else
    end
table.insert(resultPart, ' '..Icons.Icon({'(S) '..metalName..' '..piece, '(S)', type='item'}))
  end
table.insert(resultPart, ' '..Icons.Icon({'(G) '..metalName..' '..piece, '(G)', type='item'}))
end
table.insert(resultPart, '</span>')
end
end


  result = result..'\r\n|}'
table.insert(resultPart, '\r\n|}')
  return result
return table.concat(resultPart)
end
end


function p.getCreationTable(frame)
function p.buildCraftableArmourNav(frame)
  local itemName = frame.args ~= nil and frame.args[1] or frame
local resultPart = {}
  local item = p.getItem(itemName)
table.insert(resultPart, '{| class="wikitable mw-collapsible"')
  if item == nil then
table.insert(resultPart, '\r\n!colspan = 2 style="background-color:#275C87;color:#FFFFFF;min-width:730px;"|')
    return "ERROR: No item named "..itemName.." exists in the data module"
table.insert(resultPart, Icons.Icon({'Crafting', type='skill', notext=true}))
  end
table.insert(resultPart, ' Craftable Armour Sets')


  local skill = ''
local leatherTypes = {'Leather', 'Hard Leather'}
  local time = 0
local leatherPieces = {"Cowl", "Body", "Chaps", "Gloves", "Vambraces", "Boots"}
  local lvl = 0
table.insert(resultPart, '\r\n|-\r\n!')
  local xp = 0
table.insert(resultPart, Icons.Icon({'Leather', type='item', notext=true}))
  local qty = 0
table.insert(resultPart, ' Leather')
  local req = {}
for i, material in pairs(leatherTypes) do
  --First figure out what skill is used to make this...
if i > 1 then table.insert(resultPart, '\r\n|-\r\n!Hard Leather') end
  if item.smithingLevel ~= nil then
table.insert(resultPart, '\r\n|')
    skill = 'Smithing'
for j, piece in ipairs(leatherPieces) do
    lvl = item.smithingLevel
if j > 1 then
    xp = item.smithingXP
table.insert(resultPart, ' ')
    req = item.smithReq
end
    qty = item.smithingQty
table.insert(resultPart, Icons.Icon({material..' '..piece, piece, type='item'}))
    time = 2
end
  elseif item.craftingLevel ~= nil then
end
    skill = 'Crafting'
    lvl = item.craftingLevel
    xp = item.craftingXP
    req = item.craftReq
    qty = item.craftQty
    time = 3
  elseif item.runecraftingLevel ~= nil then
    skill = 'Runecrafting'
    lvl = item.runecraftingLevel
    xp = item.runecraftingXP
    req = item.runecraftReq
    qty = item.runecraftQty
    time = 2
  elseif item.fletchingLevel ~= nil then
    skill = 'Fletching'
    lvl = item.fletchingLevel
    xp = item.fletchingXP
    req = item.fletchReq
    qty = item.fletchQty
    time = 2
  else
    return "Failed to find creation requirements for this (Possibly the module isn't properly updated for this skill)"
  end
  if qty == nil then qty = 1 end


  local result = '{|class="wikitable"'
local materialTypes = {{'Green D-hide', 'Green Dragonhide'}, {'Blue D-hide', 'Blue Dragonhide'}, {'Red D-hide', 'Red Dragonhide'}, {'Black D-hide', 'Black Dragonhide'},
  result = result..'\r\n!colspan="2"|Item Creation\r\n|-'
{'Elderwood', 'Elderwood Logs', TotH = true}, {'Revenant', 'Revenant Logs', TotH = true}, {'Carrion', 'Carrion Logs', TotH = true}}
  result = result..'\r\n!style="text-align: right;"|Requirements'
local pieces = {"Body", "Chaps", "Vambraces", "Shield"}
  result = result..'||'..Icons.Icon({skill, type="skill", notext="true"}).."'''"..lvl.."'''"
for i, material in ipairs(materialTypes) do
  result = result..'\r\n!style="text-align: right;"|Materials||'
local isTotH = false
  for i, mat in pairs(req) do
local craftName = material[1]
    if i > 1 then result = result..'<br/>' end
local matName = material[2]
    local matItem = p.getItemByID(mat.id)
isTotH = material.TotH ~= nil and material.TotH
    if matItem == nil then
table.insert(resultPart, '\r\n|-\r\n!')
      result = result..mat.qty..'x ?????'
if isTotH then
    else
table.insert(resultPart, Icons.TotH())
      result = result..Icons.Icon({matItem.name, type='item', qty=mat.qty})
end
    end
table.insert(resultPart, Icons.Icon({matName, type="item", notext=true}))
  end
table.insert(resultPart, " "..craftName)
  result = result..'\r\n!style="text-align:right;"|Base Quantity'
table.insert(resultPart, "\r\n|")
  result = result..'||'..qty
 
  result = result..'\r\n!style="text-align:right;"|Base Experience'
for j, piece in ipairs(pieces) do
  result = result..'||'..xp
if j > 1 then
  result = result..'\r\n!style="text-align:right;"|Base Creation Time'
table.insert(resultPart, ' ')
  result = result..'||'..time..' seconds'
end
  result = result..'\r\n|}'
table.insert(resultPart, '<span style="display:inline-block">')
table.insert(resultPart, Icons.Icon({craftName..' '..piece, piece, type='item'}))
table.insert(resultPart, ' '..Icons.Icon({'(U) '..craftName..' '..piece, '(U)', type='item'}))
table.insert(resultPart, '</span>')
end
end
 
table.insert(resultPart, '\r\n|}')
return table.concat(resultPart)
end


  return result
function p.getLifestealWeapons()
local items = p.getItems(function(item)
if item.specialAttacks ~= nil and not Shared.tableIsEmpty(item.specialAttacks) then
for i, spAttID in ipairs(item.specialAttacks) do
local spAtt = GameData.getEntityByID('attacks', spAttID)
if spAtt ~= nil then
return spAtt.lifesteal > 0
end
end
end
return false
end)
for i, item in ipairs(items) do
mw.log(item.name)
end
end
end


return p
return p

Latest revision as of 03:10, 6 September 2024

Lua module containing all sorts of functions for getting data on items. Pulls data from Module:GameData/data.

Some functions were split to submodules:


--This module contains all sorts of functions for getting data on items
--Several functions related to use tables can be found at Module:Items/UseTables
--Functions related to source tables can be found at Module:Items/SourceTables
--Other functions moved to Module:Items/ComparisonTables

local p = {}

local GameData = require('Module:GameData')
local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local Modifiers = require('Module:Modifiers')
local Icons = require('Module:Icons')
local Num = require('Module:Number')

-- Used by p._getItemLevelReqs()
local LevelReqCache = {}

p.EasterEggs = {'Amulet of Calculated Promotion', 'Clue Chasers Insignia', '8', 'Lemon', 'Easter Egg',
				'Abnormal Log', 'Red Herring', 'Cool Glasses'}
p.EventItems = {'Christmas Cracker', 'Christmas Coal', 'Christmas Sweater',
				'Christmas Wreath', 'Candy Cane', 'Santa Hat',
				'Friendship Bracelet', 'Event Clue 1', 'Event Clue 2',
				'Event Clue 3', 'Event Clue 4', 'Candle', 'Cake Base',
				'Magical Flavouring', 'Magical Icing', 'Birthday Cake',
				'Purple Party Hat', 'Birthday Token', 'Christmas Present (Yellow)',
				'Christmas Present (Blue)', 'Christmas Present (Green)', 'Christmas Present (White)',
				'Christmas Present (Purple)', 'Christmas Present (Standard)', 'Event Token - Holiday 2021',
				'Holiday Scarf', 'Gingerbread House', 'Gingerbread Man', 'Edible Candy Cane',
				'Locked Chest', 'Locked Chest Key', 'Event Token (Holiday 2021)'}
-- List of item IDs that should typically not be included within outputs, usually
-- because they are not fully implemented despite existing within the game data
p.HiddenItems = {}

local function populateHiddenItems()
	local hiddenItems = GameData.getEntities('items',
		function(item)
			return item.name == nil or Shared.contains({'melvorTotH:Meteorite_Dust'}, item.id)
		end
	)
	for _, item in ipairs(hiddenItems) do
		table.insert(p.HiddenItems, item.id)
	end
end
populateHiddenItems()

function p.getItemByID(ID)
	return GameData.getEntityByID('items', ID)
end

function p.getItem(name)
	name = Shared.fixPagename(name)
	return GameData.getEntityByName('items', name)
end

function p.getItems(checkFunc)
	return GameData.getEntities('items',
		function(obj)
			return not Shared.contains(p.HiddenItems, obj.id) and checkFunc(obj)
		end
	)
end

function p._canItemUseSlot(item, equipSlot)
	--Function to easily check if an item can fit in a given equipment slot
	--Ex: p._canItemUseSlot({Bronze Platebody}, 'Platebody') returns true
	if type(item) == 'string' then
		item = p.getItem(item)
	end
	return item.validSlots ~= nil and Shared.contains(item.validSlots, equipSlot)
end

function p._getItemEquipSlot(item)
	--Function to return the (non-Passive) equipment slot that an item occupies
	if type(item) == 'string' then
		item = p.getItem(item)
	end
	if item == nil or item.validSlots == nil then
		return 'Invalid'
	end
	for i, slot in pairs(item.validSlots) do
		if slot ~= 'Passive' then
			return slot
		end
	end
end

-- Given an item, returns a table containing the item's level requirements (if any)
-- The table structure is as follows: 
-- { ['normal'] = { SkillID = SkillLevel, ... }, ['abyssal'] = { SkillID = SkillLevel, ... } }
-- If optional parameter 'condense' is specified as true, any normal requirements are excluded
-- from the output if there should exist an abyssal level requirement for the same skill
function p._getItemLevelReqs(item, condense)
	local condenseResult = (type(condense) == 'boolean' and condense) or false
	local reqTypeMap = {
		['SkillLevel'] = 'normal',
		['AbyssalLevel'] = 'abyssal'
	}
	local levelReqs = LevelReqCache[item.id]
	if levelReqs == nil then
		-- Item not in cache - obtain level requirements data
		if item.equipRequirements == nil then
			return nil
		end
		levelReqs = { ['normal'] = {}, ['abyssal'] = {} }
		for _, requirement in ipairs(item.equipRequirements) do
			local reqTypeKey = reqTypeMap[requirement.type]
			if reqTypeKey ~= nil then
				-- Requirement is a level requirement
				local reqSkillID, reqLevel = requirement.skillID, requirement.level
				local reqCurrentLevel = levelReqs[reqTypeKey][reqSkillID]
				if reqCurrentLevel == nil or reqLevel > reqCurrentLevel then
					levelReqs[reqTypeKey][reqSkillID] = reqLevel
				end
			end
		end
		-- Cache level requirements data for any subsequent lookups
		LevelReqCache[item.id] = levelReqs
	end

	if not condenseResult then
		-- Return requirements as is
		return levelReqs
	else
		-- Exclude any normal level requirements should an abyssal requirement exist for
		-- the same skill
		local levelReqsCondensed = { ['normal'] = {}, ['abyssal'] = levelReqs.abyssal }
		for skillID, reqLevel in pairs(levelReqs.normal) do
			if levelReqs.abyssal[skillID] == nil then
				levelReqsCondensed.normal[skillID] = reqLevel
			end
		end
		return levelReqsCondensed
	end
end

function p._getItemStat(item, StatName, ZeroIfNil)
	local result = item[StatName]
	--Special Overrides:
	-- Equipment stats first
	if item.equipmentStats ~= nil and item.equipmentStats[StatName] ~= nil then
		result = item.equipmentStats[StatName]
	elseif StatName == 'attackSpeed' and item.validSlots ~= nil and Shared.contains(item.validSlots, 'Weapon') then
		-- Item can be equipped as a weapon but has no attack speed, so use default of 4000ms
		result = 4000
	elseif StatName == 'isTwoHanded' then
		if item.validSlots ~= nil and item.occupiesSlots ~= nil then
			result = Shared.contains(item.validSlots, 'Weapon') and Shared.contains(item.occupiesSlots, 'Shield')
		else
			result = false
		end
	elseif (
			string.find(StatName, '^(.+)LevelRequired$') ~= nil
			or string.find(StatName, '^(.+)AbyssalLevel$') ~= nil
		) and item.equipRequirements ~= nil then
		local levelType = 'normal'
		local skillName = string.match(StatName, '^(.+)LevelRequired$')
		if skillName == nil then
			levelType = 'abyssal'
			skillName = string.match(StatName, '^(.+)AbyssalLevel$')
		end
		if skillName ~= nil then
			local skillID = Constants.getSkillID(Shared.titleCase(skillName))
			if skillID ~= nil then
				local levelReqs = p._getItemLevelReqs(item)
				result = levelReqs ~= nil and levelReqs[levelType][skillID]
			end
		end
	elseif StatName == 'attackType' then
		result = p._getWeaponAttackType(item)
	elseif StatName == 'description' then
		result = item.customDescription
		if result == nil or result == '' then result = 'No Description' end
	elseif StatName == 'completionReq' then
		if item.ignoreCompletion == nil or not item.ignoreCompletion then
			result = 'Yes'
		else
			result = 'No'
		end
	elseif StatName == 'slayerBonusXP' then
		return p._getItemModifier(item, 'increasedSkillXP', 'Slayer', false)
	elseif StatName == 'hasCombatStats' then
		return tostring(p.hasCombatStats(item) or p._hasLevelRequirements(item))
	elseif StatName == 'category' then
		-- Some categories have a namespace for some reason, remove it
		local _, localID = GameData.getLocalID(result)
		return localID
	end
	if result == nil and ZeroIfNil then result = 0 end
	return result
end

function p.getItemValueByID(itemID)
	local item = p.getItemByID(itemID)
	if item == nil then
		return 0
	end
	
	return p.getItemValue(item['name'])
end

function p.getItemValue(item)
	if type(item) == 'string' then
		-- Specific check if the item is GP (value of 1)
		if Shared.compareString('GP', item, true) 
		or Shared.compareString('Gold Pieces', item, true) then
			return 1
		end

		item = p.getItem(item)
	end
	
	if item then
		return item.sellsFor
	end
	
	return nil
end

function p.getValueText(item, minQuantity, maxQuantity)
	local minQ, maxQ = 1, 1
	if type(minQuantity) == 'number' then
		minQ = minQuantity
	end
	if type(maxQuantity) == 'number' then
		maxQ = maxQuantity
	else
		maxQ = minQ
	end
	local amt = item.sellsFor or 0
	local currID = item.sellsForCurrency or 'melvorD:GP'
	return Icons._Currency(currID, amt * minQ, amt * maxQ)
end

-- Function already exists, but without frame.
-- Giving it a slightly different name since function overloading doesn't exist
function p.getItemSellsFor(frame)
	local args = frame:getParent().args

	return p._getItemSellsFor(args[1], args[2], args.round)
end

function p._getItemSellsFor(itemName, multiplier, rounding)
	local itemValue = p.getItemValue(itemName)
	multiplier = tonumber(multiplier) or 1
	rounding = tonumber(rounding) or 0
	
	if itemValue == nil then
		error('No item named "' .. itemName .. '" exists in the data module')
	end

	return Num.round2(itemValue * multiplier, rounding)
end

function p.getItemStat(frame)
	local args = frame.args ~= nil and frame.args or frame
	local ItemName = args[1]
	local StatName = args[2]
	local ZeroIfNil = args.ForceZero ~= nil and args.ForceZero ~= '' and args.ForceZero ~= 'false'
	local formatNum = args.formatNum ~= nil and args.formatNum ~= '' and args.formatNum ~= 'false'
	local item = p.getItem(ItemName)
	if item == nil then
		return Shared.printError('No item named "' .. ItemName .. '" exists in the data module')
	end
	local result = p._getItemStat(item, StatName, ZeroIfNil)
	if formatNum then result = Num.formatnum(result) end
	return result
end

--Gets the value of a given modifier for a given item
--asString is false by default, when true it writes the full bonus text
function p._getItemModifier(item, modifier, skillID, asString)
	if asString == nil then asString = false end
	if skillID == '' then
		skillID = nil
	elseif string.find(skillID, ':') == nil then
		-- Try to find a skill ID if it looks like a skill name has been passed
		skillID = Constants.getSkillID(skillID)
	end

	local result = 0

	if item.modifiers ~= nil and item.modifiers[modifier] ~= nil then
		if type(item.modifiers[modifier]) == 'table' then
			for i, subVal in Shared.skpairs(item.modifiers[modifier]) do
				if subVal[1] == skillID then
					result = subVal[2]
					break
				end
			end
		else
			result = item.modifiers[modifier]
		end
	end

	if asString then
		if skillID ~= nil then
			return Constants._getModifierText(modifier, {skillID, result})
		else
			return Constants._getModifierText(modifier, result)
		end
	else
		return result
	end
end

function p.hasCombatStats(item)
	-- Checks if the combat stat is a valid, non-zero combat stat
	-- Ensure that, only in the case where the item is a Familar AND
	-- the checked stat is summoningMaxhit, the result is ignored.
	local function isNonZeroStat(statName, statVal)
		if statName == 'summoningMaxhit' and (p._canItemUseSlot(item, 'Summon1') or p._canItemUseSlot(item, 'Summon2')) then
			return false
		end
		return statVal ~= 0
	end

	if item.equipmentStats ~= nil then
		-- Ensure at least one stat has a non-zero value
		for statName, statVal in pairs(item.equipmentStats) do
			if isNonZeroStat(statName, statVal) then
				return true
			end
		end
	end

	return false
end

function p._hasLevelRequirements(item)
	--Function true if an item has at least one level requirement to equip
	local levelReqs = p._getItemLevelReqs(item)
	if levelReqs ~= nil then
		for levelType, reqs in pairs(levelReqs) do
			if not Shared.tableIsEmpty(reqs) then
				return true
			end
		end
	end
	return false
end

function p.getItemModifier(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame[1]
	local modName = frame.args ~= nil and frame.args[2] or frame[2]
	local skillName = frame.args ~= nil and frame.args[3] or frame[3]
	local asString = frame.args ~= nil and frame.args[4] or frame[4]
	if asString ~= nil then
		asString = (string.upper(asString) ~= 'FALSE')
	end

	local item = p.getItem(itemName)
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end

	return p._getItemModifier(item, modName, skillName, asString)
end

function p._getWeaponAttackType(item)
	if (item.validSlots ~= nil and Shared.contains(item.validSlots, 'Weapon')) or
		(item.occupiesSlots ~= nil and Shared.contains(item.occupiesSlots, 'Weapon')) then
		if Shared.contains({'melee', 'ranged', 'magic'}, item.attackType) then
			local iconType = item.attackType ~= 'melee' and 'skill' or nil
			return Icons.Icon({Shared.titleCase(item.attackType), type=iconType, nolink='true'})
		end
	end
	return 'Invalid'
end

function p.getWeaponAttackType(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = p.getItem(itemName)
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end
	return p._getWeaponAttackType(item)
end

local statChangeDefs = {
	{
		stat = 'stabAttackBonus',
		suffix = ' ' .. Icons.Icon({'Melee', notext=true}) .. ' Stab Bonus'
	},
	{
		stat = 'slashAttackBonus',
		suffix =  ' ' .. Icons.Icon({'Melee', notext=true}) .. ' Slash Bonus'
	},
	{
		stat = 'blockAttackBonus',
		suffix = ' ' .. Icons.Icon({'Melee', notext=true}) .. ' Block Bonus'
	},
	{
		stat = 'meleeStrengthBonus',
		suffix = ' ' .. Icons.Icon({'Strength', type='skill', notext=true}) .. ' Strength Bonus'
	},
	{
		stat = 'rangedStrengthBonus',
		suffix =  ' ' .. Icons.Icon({'Ranged', type='skill', notext=true}) .. ' Strength Bonus'
	},
	{
		stat = 'magicStrengthBonus',
		suffix = '% ' .. Icons.Icon({'Magic', type='skill', notext=true}) .. ' Damage Bonus'
	},
	{
		stat = 'meleeDefenceBonus',
		suffix = ' ' .. Icons.Icon({'Defence', type='skill', notext=true}) .. ' Defence Bonus' },
	{
		stat = 'rangedDefenceBonus',
		suffix = ' ' .. Icons.Icon({'Ranged', type='skill', notext=true}) .. ' Defence Bonus'
	},
	{
		stat = 'magicDefenceBonus',
		suffix = ' ' .. Icons.Icon({'Magic', type='skill', notext=true}) .. ' Defence Bonus'
	},
	{
		stat = 'damageReduction',
		suffix = '% Damage Reduction'
	},
	{
		stat = 'levelRequired',
		suffix = ' Level Required'
	}
}

-- Produces a list of stat & modifier changes between two items of equipmednt
function p.getStatChangeString(item1, item2)
	local changeArray = {}

	local equipStats = {
		type(item1.equipmentStats) == 'table' and item1.equipmentStats or {},
		type(item2.equipmentStats) == 'table' and item2.equipmentStats or {}
	}
	for i, statDef in ipairs(statChangeDefs) do
		local val1, val2 = 0, 0
		if statDef.stat == 'levelRequired' then
			-- Iterate over equipment stats for both items, determining level requirements
			local levelReqs = {}
			for itemNum, item in ipairs({item1, item2}) do
				levelReqs[itemNum] = {}
				if item.equipRequirements ~= nil then
					for j, req in ipairs(item.equipRequirements) do
						if req.type == 'SkillLevel' then
							levelReqs[itemNum][req.skillID] = req.level
						end
					end
				end
			end
			-- Iterate over all skills, checking if there are requirements for these in either skill
			for j, skillData in ipairs(GameData.rawData.skillData) do
				local skillID = skillData.skillID
				val1, val2 = levelReqs[1][skillID] or 0, levelReqs[2][skillID] or 0
				if val1 ~= val2 then
					table.insert(changeArray, Num.numStrWithSign(val1 - val2) .. ' ' .. Icons.Icon({skillData.data.name, type='skill', notext=true}) .. (statDef.suffix or ''))
				end
			end
		else
			-- Equipment stats
			val1, val2 = equipStats[1][statDef.stat] or 0, equipStats[2][statDef.stat] or 0
			if val1 ~= val2 then
				table.insert(changeArray, Num.numStrWithSign(val1 - val2) .. (statDef.suffix or ''))
			end
		end
	end

	-- Include differences in modifiers
	-- TODO Implement getModifiersDifference
	--local modDiff = Constants.getModifiersText(Constants.getModifiersDifference(item2.modifiers, item1.modifiers))
	local modDiff = nil
	if modDiff ~= nil and modDiff ~= '' then
		table.insert(changeArray, modDiff)
	end

	return table.concat(changeArray, '<br/>')
end

function p._getOtherItemBoxText(item)
	local resultPart = {}
	--For equipment, show the slot they go in
	local isPassive = false
	if item.validSlots ~= nil then
		local slotLinkMap = {
			["Helmet"] = 'Helmets',
			["Platebody"] = 'Platebodies',
			["Platelegs"] = 'Platelegs',
			["Boots"] = 'Boots',
			["Weapon"] = 'Weapons',
			["Shield"] = 'Shields',
			["Amulet"] = 'Amulets',
			["Ring"] = 'Rings',
			["Gloves"] = 'Gloves',
			["Quiver"] = 'Ammunition',
			["Cape"] = 'Capes',
			["Consumable"] = 'Consumables',
			["Passive"] = 'Combat Passive Slot',
			["Summon1"] = 'Summoning',
			["Summon2"] = 'Summoning',
			["Gem"] = "Gems_(Equipment)"
		}
		function getSlotText(slotList)
			local slotText = {}
			for i, slot in ipairs(slotList) do
				local slotLink = slotLinkMap[slot]
				if slotLink == nil then
					table.insert(slotText, slot)
				else
					table.insert(slotText, '[[' .. slotLink .. '|' .. slot .. ']]')
				end
				
				if slot == 'Passive' then
					isPassive = true
				end
			end
			return slotText
		end
		slotText = getSlotText(item.validSlots)
		table.insert(resultPart, "\r\n|-\r\n|'''Equipment Slot:''' "..table.concat(slotText, ', '))
		if item.occupiesSlots ~= nil then
			slotText = getSlotText(item.occupiesSlots)
			table.insert(resultPart, "\r\n|-\r\n|'''Blocked Slot:''' "..table.concat(slotText, ', '))
		end
	end
	--For weapons with a special attack, show the details
	if item.specialAttacks ~= nil and not Shared.tableIsEmpty(item.specialAttacks) then
		table.insert(resultPart, "\r\n|-\r\n|'''Special Attack:'''")
		for i, spAttID in ipairs(item.specialAttacks) do
			local spAtt = GameData.getEntityByID('attacks', spAttID)
			if spAtt ~= nil then
				local spAttChance = spAtt.defaultChance
				if type(item.overrideSpecialChances) == 'table' and item.overrideSpecialChances[i] ~= nil then
					spAttChance = item.overrideSpecialChances[i]
				end
				local spAttDesc = string.gsub(spAtt.description, '<Attack> ', '')
				table.insert(resultPart, '\r\n* ' .. spAttChance .. '% chance for ' .. spAtt.name .. ':')
				table.insert(resultPart, '\r\n** ' .. spAttDesc)
			end
		end
	end
	-- For Summoning combat familiars, show the max hit
	if item.equipmentStats ~= nil and item.equipmentStats.summoningMaxhit ~= nil then
		table.insert(resultPart, "\r\n|-\r\n|'''Max Hit:''' " .. Num.formatnum(item.equipmentStats.summoningMaxhit * 10))
	end
	--For potions, show the number of charges
	if item.charges ~= nil then
		table.insert(resultPart, "\r\n|-\r\n|'''Charges:''' "..item.charges)
	end
	--For food, show how much it heals for
	if item.healsFor ~= nil then
		table.insert(resultPart, "\r\n|-\r\n|'''Heals for:''' "..Icons.Icon({"Hitpoints", type="skill", notext="true"})..' '..(item.healsFor * 10))
	end
	--For Prayer Points, show how many you get
	if item.prayerPoints ~= nil then
		table.insert(resultPart, "\r\n|-\r\n|'''"..Icons.Icon({'Prayer', type='skill'}).." Points:''' "..item.prayerPoints)
	end
	if item.soulPoints ~= nil then
		table.insert(resultPart, "\r\n|-\r\n|'''"..Icons.Icon({'Prayer', 'Soul', type='item', img='Lesser Soul'}).." Points:''' "..item.soulPoints)
	end
	--For items that provide runes, show which runes are provided
	if item.providedRunes ~= nil then
		table.insert(resultPart, "\r\n|-\r\n|'''Runes Provided:''' ")
		local runeLines = {}
		local sortVal = ''
		for j, runePair in pairs(item.providedRunes) do
			local runeID = runePair.id
			local qty = runePair.quantity
			local rune = p.getItemByID(runeID)
			sortVal = sortVal..rune.name..qty
			table.insert(runeLines, Icons.Icon({rune.name, type='item', qty=qty}))
		end
		table.insert(resultPart, table.concat(runeLines, ', '))
	end
	--For items with modifiers, show what those are
	if item.modifiers ~= nil and not Shared.tableIsEmpty(item.modifiers) then
		table.insert(resultPart, "\r\n|-\r\n|'''Modifiers:'''\r\n")
		if isPassive then
			table.insert(resultPart, '<span style="color:green">Passive:</span><br/>')
		end
		table.insert(resultPart, Modifiers.getModifiersText(item.modifiers, true, false, 10))
	end
	return table.concat(resultPart)
end

function p.getOtherItemBoxText(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = p.getItem(itemName)
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end

	return p._getOtherItemBoxText(item)
end

function p._getCurrencyItemBoxText(item)
	local span = mw.html.create('span')
		:wikitext("'''Sells For:''' ")
		:wikitext(Icons._Currency((item.sellsForCurrency or 'melvorD:GP'), item.sellsFor))
	return span
end

function p.getCurrencyItemBoxText(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = p.getItem(itemName)
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end

	return p._getCurrencyItemBoxText(item)
end

function p._getItemCategories(item)
	local resultPart = {}
	local isEquipment = item.validSlots ~= nil or item.occupiesSlots ~= nil or item.equipmentStats ~= nil
	local category = p._getItemStat(item, 'category', false)
	if category ~= nil and category ~= 'Skills' then
		table.insert(resultPart, '[[Category:'..category..']]')
	end
	if item.type ~= nil then
		table.insert(resultPart, '[[Category:'..item.type..']]')
	end
	if isEquipment and item.tier ~= nil then
		table.insert(resultPart, '[[Category:'..Shared.titleCase(item.tier)..' '..item.type..']]')
	end
	if item.specialAttacks ~= nil and not Shared.tableIsEmpty(item.specialAttacks) then
		table.insert(resultPart, '[[Category:Items With Special Attacks]]')
	end
	if item.validSlots ~= nil then
		local slotRemap = {
			['Passive'] = 'Passive Items',
			['Summon1'] = 'Summoning Familiars',
			['Summon2'] = ''
		}
		for i, slotName in ipairs(item.validSlots) do
			local slotRemapName = slotName
			if slotRemap[slotName] ~= nil then slotRemapName = slotRemap[slotName] end
			if slotRemapName ~= '' then table.insert(resultPart, '[[Category:' .. slotRemapName .. ']]') end
		end
	end
	if item.modifiers ~= nil then
		local modsDL = {
			'increasedChanceToDoubleLootCombat',
			'decreasedChanceToDoubleLootCombat',
			'increasedChanceToDoubleLootThieving',
			'decreasedChanceToDoubleLootThieving',
			'increasedChanceToDoubleItemsGlobal',
			'decreasedChanceToDoubleItemsGlobal'
		}
		for modName, val in pairs(item.modifiers) do
			if Shared.contains(modsDL, modName) then
				table.insert(resultPart, '[[Category:Double Loot Chance Items]]')
				break
			end
		end
	end
	return table.concat(resultPart)
end

function p.getItemCategories(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = p.getItem(itemName)
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end

	return p._getItemCategories(item)
end

function p.getItemGrid(frame)
	--melvorF, melvorD, melvorTotH, melvorAoD
	local dlcFunc = function(item, dlc) 
		local itemDLC = Shared.getLocalID(item.id)
		if dlc == nil then
			return true
		end
		
		if dlc == 'base' then
			return itemDLC == 'melvorD' or itemDLC == 'melvorF'
		end
		
		if itemDLC == dlc then
			return true
		end
		return false
	end
	
	-- Convert list of hidden items into a key/value structure, such that
	-- lookups are more efficient than repeated calls to Shared.contains()
	local hiddenItemIDs = {}
	for i, itemID in ipairs(p.HiddenItems) do
		hiddenItemIDs[itemID] = 1
	end

	local args = frame:getParent().args
	local dlc = args[1] or args.DLC or args.dlc or nil
	local columns = tonumber(args[2] or args.Columns or args.columns) or 17
	
	local html = mw.html.create('table')
		:addClass('wikitable lighttable individual table-img-grid')

	local curRow = html:tag('tr')
	local i = 0
	for _, v in pairs(GameData.rawData.items) do
		if hiddenItemIDs[v.id] == nil and dlcFunc(v, dlc) == true then
			if i >= columns then
				curRow = html:tag('tr')
				i = 0
			end
			
			local cell = curRow:tag('td')
				:wikitext(Icons.Icon({v.name, type='item', notext=true, size='32'}))
				
			-- Mod operator is slow. We use this instead	
			i = i + 1
		end
	end
	
	return tostring(html)
end

function p.getEquipRequirementRow(req)
	local result = ""
	if (req.type == "SkillLevel" or req.type == "AbyssalLevel") then
		local pre = (req.type == "AbyssalLevel" and ' Abyssal') or ''
		local skillName = Constants.getSkillName(req.skillID)
		local skillIcon = Icons.Icon({skillName, type='skill', notext=true})
		result = '\r\n!style="text-align:right;"| '..skillIcon..pre..' Level Required'
		result = result..'\r\n|style="text-align:right;"| '..req.level
	elseif (req.type == "DungeonCompletion" or req.type == "AbyssDepthCompletion") then
		local reqDefns = {
			["DungeonCompletion"] = {
				["dataKey"] = 'dungeons',
				["IDKey"] = 'dungeonID',
				["imgType"] = 'dungeon'
			},
			["AbyssDepthCompletion"] = {
				["dataKey"] = 'abyssDepths',
				["IDKey"] = 'depthID',
				["imgType"] = 'combatArea'
			}
		}
		local reqDefn = reqDefns[req.type]
		if reqDefn ~= nil then
			local area = GameData.getEntityByID(reqDefn.dataKey, req[reqDefn.IDKey])
			if area == nil then
				result = '\r\n!style="text-align:right;" colspan=2|' .. Shared.printError('Invalid area for requirement type "' .. req.type .. '"')
			else
				local areaIcon = Icons.Icon({area.name, type=reqDefn.imgType, notext=true})
				result = '\r\n!style="text-align:right;"| '..areaIcon..' Completions'
				result = result..'\r\n|style="text-align:right;"| '..Num.formatnum(req.count)
			end
		end
	elseif req.type == "Completion" then
		local ns = GameData.getEntityByName('namespaces', req.namespace)
		if ns == nil then
			return '\r\n!style="text-align:right;" colspan=2|' .. Shared.printError('Invalid namespace for completion requirement "' .. req.namespace .. '"')
		else
			result = '\r\n!style="text-align:right;"| ' .. ns.displayName .. ' Completion'
			result = result .. '\r\n|style="text-align:right;"| ' .. req.percent .. '%'
		end
	else
		return '\r\n!style="text-align:right;" colspan=2|' .. Shared.printError('Invalid equip requirement type "' .. req.type .. '"')
	end
		return result
end

function p._getItemResistance(item)
	local namespace = Shared.getLocalID(item.id)
	if namespace == 'melvorItA' then
		local eternalResistance = p._getItemStat(item, 'resistanceEternal', false)
		if eternalResistance ~= nil or item.damageType ~= nil and item.damageType == 'melvorItA:Eternal' then
			return eternalResistance or 0, 'Eternal Resistance'
		else	
			return p._getItemStat(item, 'resistanceAbyssal', true), 'Abyssal Resistance'
		end
	else
		return p._getItemStat(item, 'damageReduction', true), 'Damage Reduction'
	end
end

function p.getWeaponStatsBox(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = p.getItem(itemName)
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end

	local resistance, resistanceText = p._getItemResistance(item)
	local ns, damageType = nil, nil
	if item.damageType ~= nil then
		ns, damageType = Shared.getLocalID(p._getItemStat(item, 'damageType'))
	else
		damageType = 'Normal'
	end

	local ico = {
		["Attack"] = Icons.Icon({'Attack', type='skill', notext=true}),
		["Combat"] = Icons.Icon({'Combat', notext=true}),
		["Defence"] = Icons.Icon({'Defence', type='skill', notext=true}),
		["Magic"] = Icons.Icon({'Magic', type='skill', notext=true}),
		["Ranged"] = Icons.Icon({'Ranged', type='skill', notext=true}),
		["Strength"] = Icons.Icon({'Strength', type='skill', notext=true}),
		["Slayer"] = Icons.Icon({'Slayer', type='skill', notext=true}),
		["Resistance"] = Icons.Icon({resistanceText, notext=true}),
		["DamageType"] = Icons.Icon({(damageType .. ' Damage'), type="damage", notext=true})
	}
	
	local reqCount = item.equipRequirements ~= nil and Shared.tableCount(item.equipRequirements) or 0
	local emptyRow = '\r\n!colspan="2"|'
	
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="4" style="border-bottom:solid medium black;"| Weapon Stats')
	table.insert(resultPart, '\r\n|-\r\n!colspan="2" style="border-bottom:solid thin black;"| Offensive Stats')
	table.insert(resultPart, '\r\n!colspan="2" style="border-bottom:solid thin black;"| Defensive Stats')

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| Attack Speed')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.round(p._getItemStat(item, 'attackSpeed', true) / 1000, 3, 1) .. 's')
	table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Defence'] .. ' Defence Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'meleeDefenceBonus', true))

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| Damage Type')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. ico['DamageType'] .. ' ' .. damageType)
	table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Resistance'] .. ' ' .. resistanceText)
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. resistance .. '%')

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| Attack Type')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'attackType'))
	table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Defence Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'rangedDefenceBonus', true))

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Strength'] .. ' Strength Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'meleeStrengthBonus', true)))
	table.insert(resultPart, '\r\n!style="text-align:right;border-bottom:solid thin black;"| ' .. ico['Magic'] .. ' Defence Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;border-bottom:solid thin black;"| ' .. p._getItemStat(item, 'magicDefenceBonus', true))

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Stab Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'stabAttackBonus', true)))
	table.insert(resultPart, '\r\n!colspan="2" style="border-bottom:solid thin black;"| Equip Requirements')

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Slash Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'slashAttackBonus', true)))
	if reqCount > 0 then
		table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[1]))
	else
		table.insert(resultPart, '\r\n|colspan=2 style="text-align:right"|None')
	end

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Block Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'blockAttackBonus', true)))
	if reqCount > 1 then
		table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[2]))
	else
		table.insert(resultPart, emptyRow)
	end

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Attack Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'rangedAttackBonus', true)))
	if reqCount > 2 then
		table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[3]))
	else
		table.insert(resultPart, emptyRow)
	end

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Strength Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'rangedStrengthBonus', true)))
	if reqCount > 3 then
		table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[4]))
	else
		table.insert(resultPart, emptyRow)
	end

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' Attack Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'magicAttackBonus', true)))
	if reqCount > 4 then
		table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[5]))
	else
		table.insert(resultPart, emptyRow)
	end

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' % Damage Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'magicDamageBonus', true)) .. '%')
	if reqCount > 5 then
		table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[6]))
	else
		table.insert(resultPart, emptyRow)
	end
	
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| Two Handed?')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. (p._getItemStat(item, 'isTwoHanded') and 'Yes' or 'No'))
	if reqCount > 6 then
		table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[6]))
	else
		table.insert(resultPart, emptyRow)
	end

	--Add extra rows at the end for items that have more than 3 different requirements
	if reqCount > 7 then
		for i = 8, reqCount, 1 do
			table.insert(resultPart,"\r\n|-")
			table.insert(resultPart, emptyRow)
			table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[i]))
		end
	end

	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

function p.getArmourStatsBox(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = p.getItem(itemName)
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end

	local resistance, resistanceText = p._getItemResistance(item)

	local ico = {
		["Attack"] = Icons.Icon({'Attack', type='skill', notext=true}),
		["Combat"] = Icons.Icon({'Combat', notext=true}),
		["Defence"] = Icons.Icon({'Defence', type='skill', notext=true}),
		["Magic"] = Icons.Icon({'Magic', type='skill', notext=true}),
		["Ranged"] = Icons.Icon({'Ranged', type='skill', notext=true}),
		["Strength"] = Icons.Icon({'Strength', type='skill', notext=true}),
		["Slayer"] = Icons.Icon({'Slayer', type='skill', notext=true}),
		["Resistance"] = Icons.Icon({resistanceText,  notext=true}),
	}
	
	local reqCount = item.equipRequirements ~= nil and Shared.tableCount(item.equipRequirements) or 0
	local emptyRow = '\r\n!colspan="2"|'
	
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="4" style="border-bottom:solid medium black;"| Armour Stats')
	table.insert(resultPart, '\r\n|-\r\n!colspan="2" style="border-bottom:solid thin black;"| Offensive Stats')
	table.insert(resultPart, '\r\n!colspan="2" style="border-bottom:solid thin black;"| Defensive Stats')

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Strength'] .. ' Strength Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'meleeStrengthBonus', true)))
	table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Defence'] .. ' Defence Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'meleeDefenceBonus', true)))

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Stab Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'stabAttackBonus', true)))
	table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Resistance'] .. ' ' .. resistanceText)
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. resistance .. '%')

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Slash Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'slashAttackBonus', true)))
	table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Defence Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'rangedDefenceBonus', true)))

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Block Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'blockAttackBonus', true)))
	table.insert(resultPart, '\r\n!style="text-align:right;border-bottom:solid thin black;"| ' .. ico['Magic'] .. ' Defence Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;border-bottom:solid thin black;"| ' .. p._getItemStat(item, 'magicDefenceBonus', true))

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Attack Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'rangedAttackBonus', true)))
	table.insert(resultPart, '\r\n!colspan="2" style="border-bottom:solid thin black;"| Equip Requirements')

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Strength Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'rangedStrengthBonus', true)))
	if reqCount > 0 then
		table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[1]))
	else
		table.insert(resultPart, '\r\n|colspan=2 style="text-align:right"|None')
	end

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' Attack Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Num.formatnum(p._getItemStat(item, 'magicAttackBonus', true)))
	if reqCount > 1 then
		table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[2]))
	else
		table.insert(resultPart, emptyRow)
	end

	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' % Damage Bonus')
	table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'magicDamageBonus', true) .. '%')
	if reqCount > 2 then
		table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[3]))
	else
		table.insert(resultPart, emptyRow)
	end
	
	--Add extra rows at the end for items that have more than 3 different requirements
	if reqCount > 3 then
		for i = 4, reqCount, 1 do
			table.insert(resultPart, "\r\n|-")
			table.insert(resultPart, emptyRow)
			table.insert(resultPart, p.getEquipRequirementRow(item.equipRequirements[i]))
		end
	end

	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

function p.getItemDataExport(frame)
	local resultTable = mw.html.create('table')
	resultTable:addClass('wikitable')
	resultTable:tag('tr'):addClass('headerRow-0')
		:tag('th'):wikitext('ItemID'):done()
		:tag('th'):wikitext('ItemName'):done()
		:tag('th'):wikitext('GPValue'):done()

	for i, item in ipairs(GameData.rawData.items) do
		resultTable:tag('tr')
			:tag('td'):wikitext(item.id):done()
			:tag('td'):wikitext(item.name):done()
			:tag('td'):wikitext(item.sellsFor):done()
	end
	return tostring(resultTable)
end

--Returns the expansion icon for the item if it has one
function p.getExpansionIcon(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = p.getItem(itemName)
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end

	return Icons.getExpansionIcon(item.id)
end

function p.buildSmithableArmourNav(frame)
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable mw-collapsible navigation-not-searchable" style="margin:auto; clear:both; width: 100%"')
	table.insert(resultPart, '\r\n!colspan = 2 style="background-color:#275C87;color:#FFFFFF;min-width:730px;"|')
	table.insert(resultPart, Icons.Icon({'Smithing', type='skill', notext=true}))
	table.insert(resultPart, ' Smithable Armour Sets')

	local metalTypes = {'Bronze', 'Iron', 'Steel', 'Mithril', {'Adamant', 'Adamantite'}, {'Rune', 'Runite'}, {'Dragon', 'Dragonite'},
						{'Corundum', 'Corundumite', TotH = true}, {'Augite', 'Augite', TotH = true}, {'Divine', 'Divinite', TotH = true}}
	local pieces = {"Helmet", "Platebody", "Platelegs", "Boots", "Shield"}
	for i, metal in ipairs(metalTypes) do
		local metalName, barName
		local isTotH = false
		if type(metal) == 'table' then
			metalName = metal[1]
			barName = metal[2]..' Bar'
			isTotH = metal.TotH ~= nil and metal.TotH
		else
			metalName = metal
			barName = metal..' Bar'
		end
		table.insert(resultPart, '\r\n|-\r\n!')
		if isTotH then
			table.insert(resultPart, Icons.TotH())
		end
		table.insert(resultPart, Icons.Icon({barName, type="item", notext=true}))
		table.insert(resultPart, " "..metalName)
		table.insert(resultPart, "\r\n|")

		for j, piece in ipairs(pieces) do
			if j > 1 then
				table.insert(resultPart, ' • ')
			end
			table.insert(resultPart, '<span style="display:inline-block">')
			table.insert(resultPart, Icons.Icon({metalName..' '..piece, piece, type='item'}))
			if isTotH then
				table.insert(resultPart, ' '..Icons.Icon({'(I) '..metalName..' '..piece, '(I)', type='item'}))
				table.insert(resultPart, ' '..Icons.Icon({'(P) '..metalName..' '..piece, '(P)', type='item'}))
			else
				table.insert(resultPart, ' '..Icons.Icon({'(S) '..metalName..' '..piece, '(S)', type='item'}))
				table.insert(resultPart, ' '..Icons.Icon({'(G) '..metalName..' '..piece, '(G)', type='item'}))
			end
			table.insert(resultPart, '</span>')
		end
	end

	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

function p.buildCraftableArmourNav(frame)
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable mw-collapsible"')
	table.insert(resultPart, '\r\n!colspan = 2 style="background-color:#275C87;color:#FFFFFF;min-width:730px;"|')
	table.insert(resultPart, Icons.Icon({'Crafting', type='skill', notext=true}))
	table.insert(resultPart, ' Craftable Armour Sets')

	local leatherTypes = {'Leather', 'Hard Leather'}
	local leatherPieces = {"Cowl", "Body", "Chaps", "Gloves", "Vambraces", "Boots"}
	table.insert(resultPart, '\r\n|-\r\n!')
	table.insert(resultPart, Icons.Icon({'Leather', type='item', notext=true}))
	table.insert(resultPart, ' Leather')
	for i, material in pairs(leatherTypes) do
		if i > 1 then table.insert(resultPart, '\r\n|-\r\n!Hard Leather') end
		table.insert(resultPart, '\r\n|')
		for j, piece in ipairs(leatherPieces) do
			if j > 1 then
				table.insert(resultPart, ' • ')
			end
			table.insert(resultPart, Icons.Icon({material..' '..piece, piece, type='item'}))
		end
	end

	local materialTypes = {{'Green D-hide', 'Green Dragonhide'}, {'Blue D-hide', 'Blue Dragonhide'}, {'Red D-hide', 'Red Dragonhide'}, {'Black D-hide', 'Black Dragonhide'},
						{'Elderwood', 'Elderwood Logs', TotH = true}, {'Revenant', 'Revenant Logs', TotH = true}, {'Carrion', 'Carrion Logs', TotH = true}}
	local pieces = {"Body", "Chaps", "Vambraces", "Shield"}
	for i, material in ipairs(materialTypes) do
		local isTotH = false
		local craftName = material[1]
		local matName = material[2]
		isTotH = material.TotH ~= nil and material.TotH
		table.insert(resultPart, '\r\n|-\r\n!')
		if isTotH then
			table.insert(resultPart, Icons.TotH())
		end
		table.insert(resultPart, Icons.Icon({matName, type="item", notext=true}))
		table.insert(resultPart, " "..craftName)
		table.insert(resultPart, "\r\n|")

		for j, piece in ipairs(pieces) do
			if j > 1 then
				table.insert(resultPart, ' • ')
			end
			table.insert(resultPart, '<span style="display:inline-block">')
			table.insert(resultPart, Icons.Icon({craftName..' '..piece, piece, type='item'}))
			table.insert(resultPart, ' '..Icons.Icon({'(U) '..craftName..' '..piece, '(U)', type='item'}))
			table.insert(resultPart, '</span>')
		end
	end

	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

function p.getLifestealWeapons()
	local items = p.getItems(function(item) 
		if item.specialAttacks ~= nil and not Shared.tableIsEmpty(item.specialAttacks) then
			for i, spAttID in ipairs(item.specialAttacks) do
				local spAtt = GameData.getEntityByID('attacks', spAttID)
				if spAtt ~= nil then
					return spAtt.lifesteal > 0
				end
			end
		end
		return false
	end)
	
	for i, item in ipairs(items) do
		mw.log(item.name)
	end
end

return p