Module:Items/SourceTables: Difference between revisions

From Melvor Idle
(Alt Magic -> Alt. Magic for consistency)
(Allow Harvesting to show up in ItemBox Item Sources)
 
(46 intermediate revisions by 7 users not shown)
Line 1: Line 1:
local p = {}
local p = {}
local MonsterData = mw.loadData('Module:Monsters/data')
local ItemData = mw.loadData('Module:Items/data')
local SkillData = mw.loadData('Module:Skills/data')
local MagicData = mw.loadData('Module:Magic/data')


local Constants = require('Module:Constants')
local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local Shared = require('Module:Shared')
local Common = require('Module:Common')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Modifiers = require('Module:Modifiers')
local Magic = require('Module:Magic')
local Magic = require('Module:Magic')
local Areas = require('Module:CombatAreas')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
local Items = require('Module:Items')
Line 15: Line 13:
local Monsters = require('Module:Monsters')
local Monsters = require('Module:Monsters')
local Skills = require('Module:Skills')
local Skills = require('Module:Skills')
local Num = require('Module:Number')


local SkillEnum = mw.loadData('Module:Constants/data').skill
local SourceOverrides = {
-- Implements overrides for sources which cannot be obtained from game data
['melvorAoD:EarthGolem'] = 'Earth Golem (AoD)'
-- Currently only overrides for dungeon sources are implemented here
local sourceOverrides = {
['Dungeon'] = {
[950] = 'Volcanic Cave', -- A Tale of the Past, a future's prophecy
[951] = 'Fire God Dungeon', -- The First Hero and an Unknown Evil
[1116] = 'Into the Mist' -- Beginning of the End
}
}
}
local function doesRecipeHaveItemID(recipe, itemID)
if recipe.productId == itemID then
return true, nil
elseif type(recipe.products) == 'table' then
for i, product in ipairs(recipe.products) do
if product.itemID == itemID then
local specialReq = nil
if recipe.products[i].minIntensityPercent ~= nil then
specialReq = recipe.products[i].minIntensityPercent .. '% Intensity'
end
return true, specialReq
end
end
end
return false, nil
end


function p._getCreationTable(item)
function p._getCreationTable(item)
local skill = ''
local skill = ''
local specialReq = nil
local specialReq = nil
local category = nil
local time = 0
local time = 0
local maxTime = nil
local maxTime = nil
local lvl = 0
local lvl = 0
local isAbyssal = false
local xp = 0
local xp = 0
local qty = nil
local qty = nil
Line 38: Line 49:


local tables = {}
local tables = {}
local itemID = item.id
--First figure out what skill is used to make this...
--First figure out what skill is used to make this...
if type(item.masteryID) == 'table' then
 
local skillID, masteryID = item.masteryID[1], item.masteryID[2]
local skillIDs = {
skill = Constants.getSkillName(skillID)
['Gathering'] = {
if skillID == SkillEnum.Fishing then
['Woodcutting'] = { recipeKey = 'trees' },
-- Fishing
['Fishing'] = { recipeKey = 'fish' },
local fish = SkillData.Fishing.Fish[masteryID + 1]
['Mining'] = { recipeKey = 'rockData' },
if fish ~= nil and fish.itemID == item.id then
['Farming'] = { recipeKey = 'recipes' },
lvl = fish.level
['Harvesting'] = { recipeKey = 'veinData' }
xp = fish.baseXP
},
qty = 1
['Artisan'] = {
time = fish.baseMinInterval / 1000
['Cooking'] = { },
maxTime = fish.baseMaxInterval / 1000
['Smithing'] = { },
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime))
['Fletching'] = { },
['Crafting'] = { },
['Runecrafting'] = { },
['Herblore'] = { },
['Summoning'] = { }
}
}
 
-- Gathering skills
-- All follow a similar data structure
for localSkillID, dataProp in pairs(skillIDs.Gathering) do
local skillData = SkillData[localSkillID]
local skill = skillData.name
local lvl, isAbyssal, xp, qty, req, category, time, maxTime = 0, false, 0, 0, nil, nil, 0, nil
for i, recipe in ipairs(skillData[dataProp.recipeKey]) do
local hasProduct, specialReq = doesRecipeHaveItemID(recipe, itemID)
if hasProduct then
lvl, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
xp = recipe.baseAbyssalExperience or recipe.baseExperience
qty = recipe.baseQuantity or 1
if localSkillID == 'Farming' then
req = { recipe.seedCost }
local catData = GameData.getEntityByID(skillData.categories, recipe.categoryID)
category = catData.name
qty = 5 * catData.harvestMultiplier
end
-- Action time
if recipe.baseMinInterval ~= nil then
time = recipe.baseMinInterval / 1000
if recipe.baseMaxInterval ~= nil then
maxTime = recipe.baseMaxInterval / 1000
end
elseif recipe.baseInterval ~= nil then
time = recipe.baseInterval /1000
elseif skillData.baseInterval ~= nil then
time = skillData.baseInterval / 1000
end
-- Special requirements
if recipe.totalMasteryRequired ~= nil then
specialReq = Icons.Icon({'Mastery', notext=true}) .. Num.formatnum(recipe.totalMasteryRequired) .. ' total [[' .. skill .. ']] [[Mastery]]'
end
table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qty, category, time, maxTime, specialReq))
-- Assumes item has a single source per skill
break
end
end
elseif skillID == SkillEnum.Mining then
end
-- Mining
end
local rock = SkillData.Mining.Rocks[masteryID + 1]
 
if rock ~= nil then
-- Artisan skills
lvl = rock.levelRequired
-- Allow follow a similar data structure
xp = rock.baseExperience
for localSkillID, dataProp in pairs(skillIDs.Artisan) do
qty = rock.baseQuantity
local skillData = SkillData[localSkillID]
time = 3
local skill = skillData.name
if item.name == 'Dragonite Ore' then
local lvl, isAbyssal, xp, qty, category, req, time, maxTime = 0, false, 0, 0, nil, nil, 0, nil
specialReq = Icons.Icon({"Mastery", notext='true'})..' 271 total [[Mining]] [[Mastery]]'
for i, recipe in ipairs(skillData.recipes) do
if recipe.productID == itemID or
(localSkillID == 'Cooking' and recipe.perfectCookID == itemID) or
(localSkillID == 'Herblore' and Shared.contains(recipe.potionIDs, itemID)) then
lvl, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
xp = recipe.baseAbyssalExperience or recipe.baseExperience
qty = recipe.baseQuantity or 1
-- Recipe Category
if recipe.categoryID ~= nil then
local catData = GameData.getEntityByID(SkillData[localSkillID].categories, recipe.categoryID)
category = catData.modifierName or catData.name or nil
end
-- Action time
if recipe.baseMinInterval ~= nil then
time = recipe.baseMinInterval / 1000
if recipe.baseMaxInterval ~= nil then
maxTime = recipe.baseMaxInterval / 1000
end
elseif recipe.baseInterval ~= nil then
time = recipe.baseInterval /1000
elseif skillData.baseInterval ~= nil then
time = skillData.baseInterval / 1000
end
end
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, nil, specialReq))
-- Special requirements
end
-- Potions have a mastery level requirement depending on the tier
elseif Shared.contains({SkillEnum.Smithing, SkillEnum.Fletching, SkillEnum.Crafting,
if item.charges ~= nil and item.tier ~= nil then
SkillEnum.Runecrafting, SkillEnum.Herblore}, skillID) then
local levelUnlock = GameData.getEntityByProperty(skillData.masteryLevelUnlocks, 'descriptionID', item.tier + 1)
-- Smithing, Fletching, Crafting, Runecrafting, Herblore
if levelUnlock ~= nil then
-- All have somewhat consistent recipe data structures
specialReq = Icons._MasteryReq(item.name, levelUnlock.level, false)
local recipeKey = (skillID == SkillEnum.Herblore and 'Potions') or 'Recipes'
local recipe = SkillData[skill][recipeKey][masteryID + 1]
if recipe ~= nil then
local masteryReq = nil
local itemMatch = (recipe.itemID == item.id)
if skillID == SkillEnum.Herblore then
-- For Herblore, we need to check a table of potion IDs & determine the mastery requirement
for i, potionID in ipairs(recipe.potionIDs) do
if potionID == item.id then
itemMatch = true
masteryReq = SkillData.Herblore.TierMasteryLevels[i]
break
end
end
end
end
end
if itemMatch then
-- Materials & output quantity
local baseTime = {
-- Special case for Summoning recipes
[SkillEnum.Smithing] = 2,
if localSkillID == 'Summoning' then
[SkillEnum.Fletching] = 2,
local shardCostArray, otherCostArray = {}, {}
[SkillEnum.Crafting] = 3,
local recipeCost = 0
[SkillEnum.Runecrafting] = 2,
if isAbyssal == true then
[SkillEnum.Herblore] = 2
recipeCost = skillData.recipeAPCost
}
else
local baseQty = recipe.baseQuantity or 1
recipeCost = skillData.recipeGPCost
lvl = recipe.level
xp = recipe.baseXP
time = baseTime[skillID]
if masteryReq ~= nil and masteryReq > 1 then
specialReq = Icons._MasteryReq(item.name, masteryReq, false)
end
end
-- Some items (such as Arrow shafts) have multiple recipes
-- Shards
if type(recipe.alternativeCosts) == 'table' then
for j, itemCost in ipairs(recipe.itemCosts) do
local reqPart, qtyPart = {}, {}
local shard = Items.getItemByID(itemCost.id)
for i, altCost in ipairs(recipe.alternativeCosts) do
if shard ~= nil then
local reqSubPart = {}
table.insert(shardCostArray, Icons.Icon({shard.name, type='item', notext=true, qty=itemCost.quantity}))
for j, itemCost in ipairs(altCost.itemCosts) do
local reqItem = Items.getItemByID(itemCost.id)
if reqItem == nil then
table.insert(reqSubPart, itemCost.qty .. 'x ?????')
else
table.insert(reqSubPart, Icons.Icon({reqItem.name, type='item', qty=itemCost.qty}))
end
end
if recipe.gpCost ~= nil and recipe.gpCost > 0 then
table.insert(reqSubPart, Icons.GP(recipe.gpCost))
end
if recipe.scCost ~= nil and recipe.scCost > 0 then
table.insert(reqSubPart, Icons.GP(recipe.scCost))
end
table.insert(reqPart, table.concat(reqSubPart, ', '))
table.insert(qtyPart, Shared.formatnum(baseQty * altCost.quantityMultiplier))
end
end
local sep = "<br/>'''OR''' "
req = table.concat(reqPart, sep)
qty = table.concat(qtyPart, sep)
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime, specialReq))
elseif type(recipe.itemCosts) == 'table' and Shared.tableCount(recipe.itemCosts) > 0 then
req = recipe.itemCosts
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, baseQty, time, maxTime, specialReq, recipe.gpCost, recipe.scCost))
end
end
end
-- Other costs
end
table.insert(otherCostArray, Common.getCostString({ ["items"] = {}, ["currencies"] = recipe.currencyCosts}))
elseif skillID == SkillEnum.Summoning then
for j, nonShardID in ipairs(recipe.nonShardItemCosts) do
-- Summoning
local nonShard = Items.getItemByID(nonShardID)
local recipe = SkillData.Summoning.Marks[masteryID + 1]
if nonShard ~= nil then
if recipe ~= nil and recipe.itemID == item.id then
local itemValue = math.max(nonShard.sellsFor, 20)
lvl = recipe.level
local nonShardQty = math.max(1, math.ceil(recipeCost / itemValue))
xp = recipe.baseXP
table.insert(otherCostArray, Icons.Icon({nonShard.name, type='item', notext=true, qty=nonShardQty}))
qty = recipe.baseQuantity
end
time = 5
-- Create item requirements text
local ShardCostArray, OtherCostArray = {}, {}
-- Shards
for j, cost in ipairs(recipe.itemCosts) do
local shard = Items.getItemByID(cost.id)
if shard ~= nil then
table.insert(ShardCostArray, Icons.Icon({shard.name, type='item', notext=true, qty=cost.qty}))
end
end
end
req = table.concat(shardCostArray, ', ')
-- Other costs
if not Shared.tableIsEmpty(otherCostArray) then
local recipeGPCost = SkillData.Summoning.RecipeGPCost
local costLen = Shared.tableCount(otherCostArray)
if recipe.gpCost > 0 then
req = req .. '<br/>' .. (costLen == 1 and '' or 'and one of the following:<br/>') .. table.concat(otherCostArray, "<br/>'''OR''' ")
table.insert(OtherCostArray, Icons.GP(recipe.gpCost))
end
if recipe.scCost > 0 then
table.insert(OtherCostArray, Icons.SC(recipe.scCost))
end
for j, nonShardID in ipairs(recipe.nonShardItemCosts) do
local nonShard = Items.getItemByID(nonShardID)
if nonShard ~= nil then
local itemValue = math.max(nonShard.sellsFor, 20)
local nonShardQty = math.max(1, math.floor(recipeGPCost / itemValue))
table.insert(OtherCostArray, Icons.Icon({nonShard.name, type='item', notext=true, qty=nonShardQty}))
end
end
end
specialReq = 'At least 1 ' .. Icons.Icon({'Summoning%23Summoning Marks', item.name, img=item.name, type='mark'}) .. ' mark discovered'
req = table.concat(ShardCostArray, ', ')
table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qty, category, time, nil, specialReq))
if #OtherCostArray > 0 then
-- Some items (such as Arrow shafts) have multiple recipes
req = req .. '<br/>and one of the following:<br/>' .. table.concat(OtherCostArray, "<br/>'''OR''' ")
elseif type(recipe.alternativeCosts) == 'table' then
end
local reqPart, qtyPart = {}, {}
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
for j, altCost in ipairs(recipe.alternativeCosts) do
end
local reqSubPart = {}
end
for k, itemCost in ipairs(altCost.itemCosts) do
end
local reqItem = Items.getItemByID(itemCost.id)
if reqItem == nil then
table.insert(reqSubPart, itemCost.quantity .. 'x ?????')
else
table.insert(reqSubPart, Icons.Icon({reqItem.name, type='item', qty=itemCost.quantity}))
end
end


-- Woodcutting
table.insert(reqSubPart, Common.getCostString({ ["items"] = {}, ["currencies"] = recipe.currencyCosts}))
if item.type == 'Logs' then
table.insert(reqPart, table.concat(reqSubPart, ', '))
-- Determine which tree (if any) the log is from
table.insert(qtyPart, Num.formatnum(qty * altCost.quantityMultiplier))
for i, tree in ipairs(SkillData.Woodcutting.Trees) do
end
if tree.logID == item.id then
local sep = "<br/>'''OR''' "
skill = 'Woodcutting'
req = table.concat(reqPart, sep)
lvl = tree.levelRequired
local qtyText = table.concat(qtyPart, sep)
time = tree.baseInterval / 1000
table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qtyText, category, time, maxTime, specialReq))
xp = tree.baseExperience
-- Finally, normal recipes with a single set of item costs
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
elseif type(recipe.itemCosts) == 'table' and not Shared.tableIsEmpty(recipe.itemCosts) then
break
table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, recipe.itemCosts, qty, category, time, maxTime, specialReq, recipe.currencyCosts))
end
end
end
-- Cooking
if item.canEat then
for i, recipe in ipairs(SkillData.Cooking.Recipes) do
if recipe.itemID == item.id or recipe.perfectCookID == item.id then
skill = 'Cooking'
lvl = recipe.level
xp = recipe.baseXP
req = recipe.itemCosts
qty = recipe.baseQuantity
time = recipe.baseInterval / 1000
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
break
end
end
end
-- Farming
if item.type == 'Herb' or item.type == 'Logs' or (item.type == 'Food' and item.category ~= 'Cooking') then
-- Herb means farming
-- Logs/Food might mean farming or might not. Depends on the item
for i, seed in ipairs(ItemData.Items) do
if seed.grownItemID ~= nil and seed.grownItemID == item.id then
skill = 'Farming'
lvl = seed.farmingLevel
xp = seed.farmingXP
time = seed.timeToGrow
if item.type == 'Logs' then
qty = 35
else
qty = 15
end
end
req = {{id = seed.id, qty = (seed.seedsRequired ~= nil and seed.seedsRequired or 1)}}
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
break
end
end
end
end
end
end


-- Alt. Magic, excludes Gems and Bars
-- Alt. Magic, excludes spells which can produce a variety of items, such as Gems and Bars
-- Bars are handled by getItemSuperheatTable()
-- Bars are handled by getItemSuperheatTable()
-- Gems are handled by _getItemLootSourceTable()
-- Gems are handled by _getItemLootSourceTable()
for i, altSpell in ipairs(MagicData.AltMagic) do
for i, altSpell in ipairs(Magic.getSpellsBySpellBook('altMagic')) do
if type(altSpell.produces) == 'number' and altSpell.produces == item.id then
if altSpell.produces == item.id then
table.insert(tables, p._buildAltMagicTable(altSpell))
table.insert(tables, p._buildAltMagicTable(altSpell))
end
end
end
end


if Shared.tableCount(tables) == 0 then
if Shared.tableIsEmpty(tables) then
return ""
return ''
else
else
return table.concat(tables, '\r\n')
return table.concat(tables, '\r\n')
Line 243: Line 230:
function p.getAltMagicTable(frame)
function p.getAltMagicTable(frame)
local spellName = frame.args ~= nil and frame.args[1] or frame
local spellName = frame.args ~= nil and frame.args[1] or frame
local spell = Magic.getSpell(spellName, 'AltMagic')
local spell = Magic.getSpell(spellName, 'altMagic')
if spell == nil then
if spell == nil then
return 'ERROR: Could not find Alt. Magic spell "' .. spellName .. '"[[Category:Pages with script errors]]'
return Shared.printError('Could not find Alt. Magic spell "' .. spellName .. '"')
else
else
return p._buildAltMagicTable(spell)
return p._buildAltMagicTable(spell)
Line 253: Line 240:
function p._buildAltMagicTable(spell)
function p._buildAltMagicTable(spell)
local resultPart = {}
local resultPart = {}
local imgType = Magic._getSpellIconType(spell)
table.insert(resultPart, '{|class="wikitable"\r\n|-')
table.insert(resultPart, '{|class="wikitable"\r\n|-')
table.insert(resultPart, '\r\n!colspan="2"|'..Icons.Icon({spell.name, type='spell'}))
table.insert(resultPart, '\r\n!colspan="2"|'..Icons.Icon({spell.name, type=imgType}))
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Requirements')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Requirements')
table.insert(resultPart, '\r\n|'..Icons._SkillReq('Magic', spell.level))
table.insert(resultPart, '\r\n|'..Icons._SkillReq('Magic', spell.level))


-- The produces property of Alt. Magic spells is as follows:
local costText = Magic._getAltSpellCostText(spell)
-- -3 = A random gem, using the same weights as Mining
if costText ~= nil then
-- -2 = A bar of the type being created (Superheat)
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials')
-- -1 = GP (Alchemy)
table.insert(resultPart, '\r\n| ' .. costText)
--  0 = Undefined
-- >0 = Item ID of the item being produced
-- The amount produced is determined by the productionRatio property
 
-- The consumes property of Alt. Magic spells is as follows:
-- 0 = Any item
-- 1 = Junk item
-- 2 = Superheat/ores with Coal
-- 3 = Superheat/ores without Coal
-- 4 = Nothing
-- 5 = Coal ore
-- Superheat (2, 3) is handled by _getItemSuperheatTable()
if spell.consumes ~= nil then
local consumeText = {
'1 of any item',
'1 of any [[Fishing#Junk|Junk]] item',
'1 x required ores for the chosen bar',
'1 x required ores (except ' .. Icons.Icon({'Coal Ore', type='item'}) .. ') for the chosen bar',
nil,
Icons.Icon({'Coal Ore', type='item', qty=1})
}
local consumeStr = consumeText[spell.consumes + 1]
if consumeStr ~= nil then
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials')
table.insert(resultPart, '\r\n| ' .. consumeStr)
end
end
end


Line 301: Line 263:
end
end


function p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime, specialReq, gpCost, scCost)
function p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qty, category, time, maxTime, specialReq, currencyCost)
if qty == nil then qty = 1 end
if qty == nil then qty = 1 end
local resultPart = {}
local resultPart = {}
table.insert(resultPart, '{|class="wikitable"')
table.insert(resultPart, '{|class="wikitable"')
if req ~= nil then
table.insert(resultPart, '\r\n!colspan="2"|Item ' .. (req == nil and 'Creation' or 'Production'))
table.insert(resultPart, '\r\n!colspan="2"|Item Creation')
else
table.insert(resultPart, '\r\n!colspan="2"|Item Production')
end
table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Requirements')
table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Requirements')
table.insert(resultPart, '\r\n|'..Icons._SkillReq(skill, lvl))
table.insert(resultPart, '\r\n|'..Icons._SkillReq(skill, lvl, false, (isAbyssal and "melvorItA:Abyssal" or nil)))
if specialReq ~= nil then table.insert(resultPart, '<br/>'..specialReq) end
if specialReq ~= nil then table.insert(resultPart, '<br/>'..specialReq) end


Line 317: Line 275:
table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Materials\r\n|')
table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Materials\r\n|')
if type(req) == 'table' then
if type(req) == 'table' then
for i, mat in pairs(req) do
for i, mat in ipairs(req) do
if i > 1 then table.insert(resultPart, '<br/>') end
if i > 1 then table.insert(resultPart, '<br/>') end
local matItem = Items.getItemByID(mat.id)
local matItem = Items.getItemByID(mat.id)
if matItem == nil then
if matItem == nil then
table.insert(resultPart, mat.qty..'x ?????')
table.insert(resultPart, mat.quantity..'x ?????')
else
else
table.insert(resultPart, Icons.Icon({matItem.name, type='item', qty=mat.qty}))
table.insert(resultPart, Icons.Icon({matItem.name, type='item', qty=mat.quantity}))
end
end
end
end
if gpCost ~= nil and gpCost > 0 then
table.insert(resultPart, '<br/>')
if currencyCost ~= nil then
table.insert(resultPart, Icons.GP(gpCost))
table.insert(resultPart, Common.getCostString({ ["items"] = {}, ["currencies"] = currencyCost }))
end
if scCost ~= nil and scCost > 0 then
table.insert(resultPart, '<br/>')
table.insert(resultPart, Icons.SC(scCost))
end
end
else
else
table.insert(resultPart, req)
table.insert(resultPart, req)
end
end
end
if category ~= nil then
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Recipe Category')
table.insert(resultPart, '\r\n|'..category)
end
end
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Quantity')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Quantity')
table.insert(resultPart, '\r\n|'..qty)
table.insert(resultPart, '\r\n|'..qty)
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Experience')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Experience')
table.insert(resultPart, '\r\n|'..Shared.formatnum(xp)..' XP')
table.insert(resultPart, '\r\n|'..Num.formatnum(xp)..' XP')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Creation Time')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Creation Time')
table.insert(resultPart, '\r\n|'..Shared.formatnum(Shared.round(time, 2, 0))..'s')
table.insert(resultPart, '\r\n|'..Shared.timeString(time, true))
if maxTime ~= nil and maxTime > time then table.insert(resultPart, ' - '..Shared.formatnum(Shared.round(maxTime, 2, 0))..'s') end
if maxTime ~= nil and maxTime > time then table.insert(resultPart, ' - '..Shared.timeString(maxTime, true)) end
table.insert(resultPart, '\r\n|}')
table.insert(resultPart, '\r\n|}')


Line 354: Line 312:
local item = Items.getItem(itemName)
local item = Items.getItem(itemName)
if item == nil then
if item == nil then
return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end
end


Line 360: Line 318:
end
end


function p._getItemSources(item, asList, addCategories)
function p._getItemSources(item, asList, addCategories, separator)
local lineArray = {}
local lineArray = {}
local categoryArray = {}
local categoryArray = {}
local sep = separator or ','


--Alright, time to go through all the ways you can get an item...
--Alright, time to go through all the ways you can get an item...
--First up: Can we kill somebody and take theirs?
--First up: Can we kill somebody and take theirs?
local killStrPart = {}
local killStrPart = {}
for i, monster in ipairs(MonsterData.Monsters) do
for i, monster in ipairs(GameData.rawData.monsters) do
local isDrop = false
local isDrop = false
if monster.bones == item.id and Monsters._getMonsterBones(monster) ~= nil then
if monster.bones ~= nil and monster.bones.itemID == item.id and Monsters._getMonsterBones(monster) ~= nil then
-- Item is a bone, and is either a shard from God dungeons or dropped by a non-boss monster with a loot table
-- Item is a bone, and is either a shard from God dungeons or dropped by a non-boss monster with a loot table
isDrop = true
elseif monster.barrierPercent ~= nil and 'melvorAoD:Barrier_Dust' == item.id and not Monsters._isDungeonOnlyMonster(monster) then
-- Item is Barrier Dust and is not a dungeon exclusive monster
isDrop = true
isDrop = true
elseif monster.lootTable ~= nil then
elseif monster.lootTable ~= nil then
Line 378: Line 340:
--  - A boss monster, whose drops are accounted for in data from Areas instead
--  - A boss monster, whose drops are accounted for in data from Areas instead
for j, loot in ipairs(monster.lootTable) do
for j, loot in ipairs(monster.lootTable) do
if loot[1] == item.id and not Monsters._isDungeonOnlyMonster(monster) then
if loot.itemID == item.id and not Monsters._isDungeonOnlyMonster(monster) then
isDrop = true
isDrop = true
break
break
Line 386: Line 348:
if isDrop then
if isDrop then
-- Item drops when the monster is killed
-- Item drops when the monster is killed
table.insert(killStrPart, Icons.Icon({monster.name, type='monster', notext=true}))
local iconName = monster.name
if SourceOverrides[monster.id] ~= nil then
iconName = SourceOverrides[monster.id]
end
table.insert(killStrPart, Icons.Icon({iconName, type='monster', notext=true}))
end
end
end
end
-- Is the item dropped from any dungeon?
-- Is the item dropped from any dungeon?
local dungeonStrPart = {}
local dungeonStrPart = {}
local dungeonList = Areas.getAreas(function(area) return area.type == 'dungeon' and type(area.rewards) == 'table' and Shared.contains(area.rewards, item.id) end)
local dungeonEntities = {
if dungeonList ~= nil then
['Dungeon'] = GameData.rawData.dungeons,
for i, dungeon in ipairs(dungeonList) do
['The Abyss'] = GameData.rawData.abyssDepths
table.insert(dungeonStrPart, Icons.Icon({dungeon.name, type='dungeon', notext=true}))
}
for entity, dungeons in pairs(dungeonEntities) do
local iconType = entity == 'Dungeon' and 'dungeon' or 'combatArea'
for i, dungeon in ipairs(dungeons) do
if (dungeon.oneTimeRewardID ~= nil and item.id == dungeon.oneTimeRewardID) or
(type(dungeon.rewardItemIDs) == 'table' and Shared.contains(dungeon.rewardItemIDs, item.id)) then
table.insert(dungeonStrPart, Icons.Icon({dungeon.name, type=iconType, notext=true}))
elseif dungeon.eventID ~= nil then
-- Is the item dropped from a combat event (e.g. Impending Darkness event)?
local event = GameData.getEntityByID('combatEvents', dungeon.eventID)
if type(event) == 'table' and type(event.itemRewardIDs) == 'table' then
for eventCycle, itemRewardID in ipairs(event.itemRewardIDs) do
if item.id == itemRewardID then
local dungPrefix = (eventCycle == Shared.tableCount(event.itemRewardIDs) and '' or eventCycle .. (eventCycle == 1 and ' cycle' or ' cycles') .. ' of ')
table.insert(dungeonStrPart, dungPrefix .. Icons.Icon({dungeon.name, type=iconType, notext=true}))
break
end
end
end
end
end
end
end
end
-- Is the item dropped from a cycle of the Impending Darkness event?
for i, eventItemID in ipairs(Areas.eventData.rewards) do
for i, stronghold in ipairs(GameData.rawData.strongholds) do
if item.id == eventItemID then
for tier, tierData in pairs(stronghold.tiers) do
local dungPrefix = (i == Shared.tableCount(Areas.eventData.rewards) and '' or i .. ' ' .. (i == 1 and 'cycle' or 'cycles') .. ' of ')
if type(tierData.rewards) == 'table' and type(tierData.rewards.items) == 'table' then
table.insert(dungeonStrPart, dungPrefix .. Icons.Icon({'Impending Darkness Event', type='dungeon', notext=true}))
for i, reward in ipairs(tierData.rewards.items) do
break
if reward.id == item.id then
table.insert(dungeonStrPart, Icons.Icon({stronghold.name, type='combatArea', notext=true}))
end
end
end
end
end
end
-- Special exceptions for lore books
if sourceOverrides['Dungeon'][item.id] ~= nil then
table.insert(dungeonStrPart, Icons.Icon({sourceOverrides['Dungeon'][item.id], type='dungeon', notext=true}))
end
end


if Shared.tableCount(dungeonStrPart) > 0 then
if not Shared.tableIsEmpty(dungeonStrPart) then
table.insert(lineArray, 'Completing: ' .. table.concat(dungeonStrPart, ','))
table.insert(lineArray, 'Completing: ' .. table.concat(dungeonStrPart, sep))
end
end
if Shared.tableCount(killStrPart) > 0 then
if not Shared.tableIsEmpty(killStrPart) then
table.insert(lineArray, 'Killing: ' .. table.concat(killStrPart, ','))
table.insert(lineArray, 'Killing: ' .. table.concat(killStrPart, sep))
end
end


--Next: Can we find it in a box?
-- Can we find it in an openable item?
--While we're here, check for upgrades, and growing
local lootPart = {}
local lootPart, upgradePart, growPart = {}, {}, {}
for i, item2 in ipairs(GameData.rawData.items) do
for i, item2 in pairs(ItemData.Items) do
if item2.dropTable ~= nil then
if item2.dropTable ~= nil then
for j, loot in ipairs(item2.dropTable) do
for j, loot in ipairs(item2.dropTable) do
if loot[1] == item.id then
if loot.itemID == item.id then
table.insert(lootPart, Icons.Icon({item2.name, type='item', notext=true}))
table.insert(lootPart, Icons.Icon({item2.name, type='item', notext=true}))
break
break
end
end
end
end
end
if item2.trimmedItemID ~= nil and item2.trimmedItemID == item.id then
table.insert(upgradePart, Icons.Icon({item2.name, type='item', notext=true}))
end
if item2.grownItemID == item.id then
-- Farming
table.insert(growPart, Icons.Icon({item2.name, type='item', notext=true}))
end
end
end
end
if #lootPart > 0 then
 
table.insert(lineArray, 'Opening: ' .. table.concat(lootPart, ','))
if not Shared.tableIsEmpty(lootPart) then
table.insert(lineArray, 'Opening: ' .. table.concat(lootPart, sep))
end
end
if #upgradePart > 0 then
 
table.insert(categoryArray, '[[Category:Upgraded Items]]')
-- Is the item a result of upgrading/downgrading another item?
table.insert(lineArray, 'Upgrading: ' .. table.concat(upgradePart, ','))
local upgradePart = { up = {}, down = {} }
for i, upgrade in ipairs(GameData.rawData.itemUpgrades) do
if item.id == upgrade.upgradedItemID then
local key = (upgrade.isDowngrade and 'down' or 'up')
for j, rootItemID in ipairs(upgrade.rootItemIDs) do
local rootItem = Items.getItemByID(rootItemID)
if rootItem ~= nil then
table.insert(upgradePart[key], Icons.Icon({rootItem.name, type='item', notext=true}))
end
end
end
end
end
if #growPart > 0 then
 
table.insert(categoryArray, '[[Category:Harvestable Items]]')
local upgradeCat = false
table.insert(lineArray, 'Growing: ' .. table.concat(growPart, ','))
for catName, parts in pairs(upgradePart) do
if not Shared.tableIsEmpty(parts) then
if not upgradeCat then
table.insert(categoryArray, '[[Category:Upgraded Items]]')
upgradeCat = true
end
local typeText = (catName == 'up' and 'Upgrading') or 'Downgrading'
table.insert(lineArray, typeText .. ': ' .. table.concat(parts, sep))
end
end
end


Line 452: Line 448:
local thiefItems = Skills.getThievingSourcesForItem(item.id)
local thiefItems = Skills.getThievingSourcesForItem(item.id)
if type(thiefItems) == 'table' then
if type(thiefItems) == 'table' then
local includedNPCs = {}
local thiefPart = {}
local thiefPart = {}
for i, thiefRow in ipairs(thiefItems) do
for i, thiefRow in ipairs(thiefItems) do
Line 457: Line 454:
--if 'all' is the npc, this is a rare item so just say 'Thieving level 1'
--if 'all' is the npc, this is a rare item so just say 'Thieving level 1'
table.insert(lineArray, Icons._SkillReq('Thieving', 1))
table.insert(lineArray, Icons._SkillReq('Thieving', 1))
else
elseif not Shared.contains(includedNPCs, thiefRow.npc) then
table.insert(thiefPart, Icons.Icon({thiefRow.npc, type='thieving', notext=true}))
table.insert(thiefPart, Icons.Icon({thiefRow.npc, type='thieving', notext=true}))
table.insert(includedNPCs, thiefRow.npc)
end
end
end
end
if #thiefPart > 0 then
if not Shared.tableIsEmpty(thiefPart) then
table.insert(lineArray, 'Pickpocketing: ' .. table.concat(thiefPart, ','))
table.insert(lineArray, 'Pickpocketing: ' .. table.concat(thiefPart, sep))
end
end
end
-- Can we get this item by casting an Alt. Magic spell?
local castPart = {}
for i, spell in ipairs(Magic.getSpellsProducingItem(item.id)) do
table.insert(castPart, Icons.Icon({spell.name, type=Magic._getSpellIconType(spell), notext=true}))
end
if not Shared.tableIsEmpty(castPart) then
table.insert(lineArray, 'Casting: ' .. table.concat(castPart, sep))
end
end


--Check if we can make it ourselves
--Check if we can make it ourselves
--AstrologyCheck (Just a brute force for now because only two items)
local skillIDs = {
if Shared.contains({'Stardust', 'Golden Stardust'}, item.name) then
['Gathering'] = {
table.insert(lineArray, Icons.Icon({'Astrology', type='skill'}))
['Woodcutting'] = { recipeKey = 'trees' },
['Fishing'] = { recipeKey = 'fish' },
['Mining'] = { recipeKey = 'rockData' },
['Farming'] = { recipeKey = 'recipes' },
['Harvesting'] = { recipeKey = 'veinData' }
},
['Artisan'] = {
['Cooking'] = { },
['Smithing'] = { },
['Fletching'] = { },
['Crafting'] = { },
['Runecrafting'] = { },
['Herblore'] = { },
['Summoning'] = { }
}
}
 
-- Gathering skills
for localSkillID, dataProp in pairs(skillIDs.Gathering) do
local skillData = SkillData[localSkillID]
local skill = skillData.name
for i, recipe in ipairs(skillData[dataProp.recipeKey]) do
local hasProduct = doesRecipeHaveItemID(recipe, item.id)
if hasProduct then
if localSkillID == 'Farming' and recipe.seedCost ~= nil then
local seedItem = Items.getItemByID(recipe.seedCost.id)
if seedItem ~= nil then
table.insert(lineArray, 'Growing: ' .. Icons.Icon({seedItem.name, type='item', notext='true'}))
end
else
local level, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
table.insert(lineArray, Icons._SkillReq(skill, level, false, (isAbyssal and "melvorItA:Abyssal" or nil)))
end
break
end
end
end
end


-- Sources discoverable through mastery IDs
-- Artisan skills
-- Does _not_ handle:
for localSkillID, dataProp in pairs(skillIDs.Artisan) do
-- Fishing: Junk, special items
local skillData = SkillData[localSkillID]
-- Cooking: perfect items
local skill = skillData.name
if type(item.masteryID) == 'table' then
for i, recipe in ipairs(skillData.recipes) do
local skillID, masteryID = item.masteryID[1], item.masteryID[2]
if recipe.productID == item.id or
local skill = Constants.getSkillName(skillID)
(localSkillID == 'Cooking' and recipe.perfectCookID == item.id) or
local keyData = {
(localSkillID == 'Herblore' and Shared.contains(recipe.potionIDs, item.id)) then
[SkillEnum.Fishing] = { ["recipe"] = 'Fish' },
local level, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
[SkillEnum.Mining] = { ["recipe"] = 'Rocks', ["level"] = 'levelRequired', ["item"] = 'oreID' },
table.insert(lineArray, Icons._SkillReq(skill, level, false, (isAbyssal and "melvorItA:Abyssal" or nil)))
[SkillEnum.Smithing] = {},
break
[SkillEnum.Fletching] = {},
[SkillEnum.Crafting] = {},
[SkillEnum.Runecrafting] = {},
[SkillEnum.Herblore] = { ["recipe"] = 'Potions', ["item"] = 'potionIDs', ["isItemList"] = true },
[SkillEnum.Summoning] = { ["recipe"] = 'Marks' }
}
local keys = keyData[skillID]
if type(keys) == 'table' then
if keys.recipe == nil then
keys.recipe = 'Recipes'
end
end
if keys.level == nil then
end
keys.level = 'level'
end
 
-- Township trading
for i, tsResource in ipairs(SkillData.Township.itemConversions.fromTownship) do
local found = false
for j, tradeDef in ipairs(tsResource.items) do
if tradeDef.itemID == item.id then
found = true
local levelReq = nil
if tradeDef.unlockRequirements ~= nil then
for k, req in ipairs(tradeDef.unlockRequirements) do
if req.type == 'SkillLevel' and req.skillID == 'melvorD:Township' then
levelReq = req.level
break
end
end
if levelReq == nil then
table.insert(lineArray, Icons.Icon({SkillData.Township.name, type='skill'}))
else
table.insert(lineArray, Icons._SkillReq(SkillData.Township.name, levelReq))
end
end
end
end
if keys.item == nil then
if found then
keys.item = 'itemID'
break
end
if keys.isItemList == nil then
keys.isItemList = false
end
end
end
if found then
break
end
end


local recipe = SkillData[skill][keys.recipe][masteryID + 1]
-- Archaeology sources
if recipe ~= nil and (
-- Digsites
(not keys.isItemList and recipe[keys.item] == item.id) or
for i, digsite in ipairs(SkillData.Archaeology.digSites) do
(keys.isItemList and Shared.contains(recipe[keys.item], item.id))) then
local found = false
local levelReq = recipe[keys.level]
for artefactType, artefactItems in pairs(digsite.artefacts) do
if levelReq ~= nil then
for j, itemDef in ipairs(artefactItems) do
table.insert(lineArray, Icons._SkillReq(skill, levelReq))
if itemDef.itemID == item.id then
table.insert(lineArray, Icons._SkillReq(SkillData.Archaeology.name, digsite.level))
found = true
break
end
end
end
end
if found then
break
end
end
if found then
break
end
end
-- Museum rewards
for i, museumReward in ipairs(SkillData.Archaeology.museumRewards) do
if type(museumReward.items) == 'table' and Shared.contains(museumReward.items, item.id) then
table.insert(lineArray, Icons.Icon('Museum'))
break
end
end
end
end


-- Woodcutting
-- Cartography
for i, tree in ipairs(SkillData.Woodcutting.Trees) do
-- Paper
if tree.logID == item.id then
for i, recipe in ipairs(SkillData.Cartography.paperRecipes) do
table.insert(lineArray, Icons._SkillReq('Woodcutting', tree.levelRequired))
if recipe.productId == item.id then
table.insert(lineArray, Icons.Icon({SkillData.Cartography.name, type='skill'}))
break
break
end
end
end
end
 
-- POI discovery rewards
-- Woodcutting: Nests
for i, worldMap in ipairs(SkillData.Cartography.worldMaps) do
if item.name == 'Bird Nest' then
local found = false
table.insert(lineArray, Icons._SkillReq('Woodcutting', 1))
for j, poi in ipairs(worldMap.pointsOfInterest) do
if type(poi.discoveryRewards) == 'table' and type(poi.discoveryRewards.items) == 'table' then
for k, itemDef in ipairs(poi.discoveryRewards.items) do
if itemDef.id == item.id then
-- Find level for POI hex
local level = 1
local poiHex = nil
local skillID = SkillData.Cartography.skillID
for m, hex in ipairs(worldMap.hexes) do
if hex.coordinates.q == poi.coords.q and hex.coordinates.r == poi.coords.r then
for n, req in ipairs(hex.requirements) do
if req.type == 'SkillLevel' and req.skillID == skillID then
level = req.level
break
end
end
break
end
end
table.insert(lineArray, Icons._SkillReq(SkillData.Cartography.name, level))
found = true
break
end
end
if found then
break
end
end
end
if found then
break
end
end
end
 
-- Travel events
-- Fishing: Junk & Specials
for i, event in ipairs(SkillData.Cartography.travelEvents) do
if Shared.contains(SkillData.Fishing.JunkItems, item.id) then
local found = false
table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Junk|Junk]]')
if type(event.rewards) == 'table' and type(event.rewards.items) == 'table' then
else
for j, itemDef in ipairs(event.rewards.items) do
for i, specialItem in ipairs(SkillData.Fishing.SpecialItems) do
if itemDef.id == item.id and itemDef.quantity > 0 then
if specialItem[1] == item.id then
table.insert(lineArray, Icons.Icon({SkillData.Cartography.name, type='skill'}))
table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Special|Special]]')
found = true
break
end
end
if found then
break
break
end
end
Line 541: Line 648:
end
end


-- Cooking
--AstrologyCheck
if item.canEat then
for i, dustDrop in ipairs(SkillData.Astrology.baseRandomItemChances) do
for i, recipe in ipairs(SkillData.Cooking.Recipes) do
if dustDrop.itemID == item.id then
if recipe.itemID == item.id or recipe.perfectCookID == item.id then
table.insert(lineArray, Icons.Icon({SkillData.Astrology.name, type='skill'}))
table.insert(lineArray, Icons._SkillReq('Cooking', recipe.level))
end
break
end
 
-- Woodcutting
-- Raven Nest
if item.id == SkillData.Woodcutting.ravenNestItemID then
local levelReq = nil
for i, tree in ipairs(SkillData.Woodcutting.trees) do
if tree.canDropRavenNest and (levelReq == nil or tree.level < levelReq) then
levelReq = tree.level
end
end
end
end
table.insert(lineArray, Icons._SkillReq(SkillData.Woodcutting.name, levelReq))
-- Bird Nest, Ash, and Mushroom
elseif Shared.contains({
SkillData.Woodcutting.nestItemID,
SkillData.Woodcutting.ashItemID,
SkillData.Woodcutting.mushroomItemID
}, item.id) then
table.insert(lineArray, Icons._SkillReq(SkillData.Woodcutting.name, 1))
end
end


-- Alt. Magic
-- Fishing
for i, altSpell in ipairs(MagicData.AltMagic) do
-- Junk
if type(altSpell.produces) == 'number' and
if Shared.contains(SkillData.Fishing.junkItemIDs, item.id) then
((altSpell.produces == -3 and item.type == 'Gem' and item.name ~= 'Jadestone')
table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Junk|Junk]]')
or (altSpell.produces == -2 and item.type == 'Bar')
elseif item.id == SkillData.Fishing.lostChestItem then
    or (altSpell.produces > 0 and altSpell.produces == item.id)) then
table.insert(lineArray, Icons._SkillReq(SkillData.Fishing.name, 100))
table.insert(lineArray, Icons.Icon({"Alt. Magic", type='skill'}))
end
break
-- Specials
for i, specialItem in ipairs(SkillData.Fishing.specialItems) do
if GameData.getEntityByProperty(specialItem.drops, 'itemID', item.id) ~= nil then
table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Special|Special]]')
end
end
end
end


--Finally there are some weird exceptions:
-- Firemaking: Coal
--Coal can be acquired via firemaking
if Shared.contains({SkillData.Firemaking.coalItemID,
if item.name == "Coal Ore" then
SkillData.Firemaking.ashItemID,
table.insert(lineArray, Icons._SkillReq("Firemaking", 1))
SkillData.Firemaking.charcoalItemID,
SkillData.Firemaking.fireSpiritItemID,
SkillData.Firemaking.diamondItemID
}, item.id) then
table.insert(lineArray, Icons._SkillReq(SkillData.Firemaking.name, 1))
end
end


--Gems can be acquired from Mining
-- Mining: Gems
if item.type == 'Gem' and item.name ~= 'Jadestone' then
if (GameData.getEntityByProperty('randomGems', 'itemID', item.id) ~= nil or
GameData.getEntityByProperty('randomSuperiorGems', 'itemID', item.id) ~= nil) then
table.insert(lineArray, Icons.Icon({"Mining", type='skill', notext=true})..' [[Mining#Gems|Gem]]')
table.insert(lineArray, Icons.Icon({"Mining", type='skill', notext=true})..' [[Mining#Gems|Gem]]')
elseif item.id == SkillData.Mining.runestoneItemID then
-- From pure essence mining
local recipe = GameData.getEntityByID(SkillData.Mining.rockData, 'melvorTotH:Pure_Essence')
if recipe ~= nil then
table.insert(lineArray, Icons._SkillReq(SkillData.Mining.name, recipe.level))
end
end
end


--Rhaelyx pieces are also special
-- General rare drops for non-combat skills
if Shared.contains({'Circlet of Rhaelyx', 'Jewel of Rhaelyx', 'Mysterious Stone'}, item.name) then
-- Includes items like Circlet/Jewel of Rhaelyx, Mysterious stones, Signet ring half (a),
local rhaSkills = {
-- relics (for Ancient Relics mode)
Circlet = {'Woodcutting', 'Fishing', 'Mining', 'Thieving', 'Farming', 'Agility', 'Astrology'},
local skillIconList, subText = {}, ''
Jewel = {'Firemaking', 'Cooking', 'Smithing', 'Fletching', 'Crafting', 'Runecrafting', 'Herblore', 'Summoning'}
for i, skillDataAll in ipairs(GameData.rawData.skillData) do
}
local skillData = skillDataAll.data
local rhaSkList, subText = nil, ''
local skillName, displaySkillName = skillData.name, false
if item.name == 'Circlet of Rhaelyx' then
-- All general rare drops within the Magic are for Alt. Magic
rhaSkList = {rhaSkills.Circlet}
if skillDataAll.skillID == 'melvorD:Magic' then
elseif item.name == 'Jewel of Rhaelyx' then
skillName, displaySkillName = 'Alt. Magic', true
rhaSkList = {rhaSkills.Jewel}
elseif item.name == 'Mysterious Stone' then
rhaSkList = {rhaSkills.Jewel, rhaSkills.Circlet}
subText = '<br/>after finding ' .. Icons.Icon({'Crown of Rhaelyx', type='item'})
end
end
 
if type(skillData.rareDrops) == 'table' then
local rhaStrPart = {}
for j, rareDrop in ipairs(skillData.rareDrops) do
for i, skillList in ipairs(rhaSkList) do
local isAltItem = (rareDrop.altItemID ~= nil and rareDrop.altItemID == item.id)
for j, skillName in ipairs(skillList) do
if isAltItem or rareDrop.itemID == item.id then
table.insert(rhaStrPart, Icons.Icon({skillName, type='skill', notext=true}))
if Shared.tableIsEmpty(skillIconList) then
-- Initialize subText
if isAltItem then
local wornItem = Items.getItemByID(rareDrop.itemID)
subText = ' while wearing ' .. Icons.Icon({wornItem.name, type='item'})
elseif rareDrop.altItemID ~= nil then
-- There exists an alt item, but we are not searching for it
local altItem = Items.getItemByID(rareDrop.altItemID)
subText = ' if not worn (Instead of ' .. Icons.Icon({altItem.name, type='item'}) .. ')'
elseif rareDrop.itemID == 'melvorD:Mysterious_Stone' then
local foundItem = Items.getItemByID('melvorD:Crown_of_Rhaelyx')
subText = '<br/>after finding ' .. Icons.Icon({foundItem.name, type='item'})
end
if type(rareDrop.gamemodes) == 'table' then
local gamemodeText = {}
for k, gamemodeID in ipairs(rareDrop.gamemodes) do
local gamemode = GameData.getEntityByID('gamemodes', gamemodeID)
if gamemode ~= nil then
table.insert(gamemodeText, gamemode.name)
end
end
if not Shared.tableIsEmpty(gamemodeText) then
subText = subText .. ' (' .. table.concat(gamemodeText, ', ') .. ' only)'
end
end
end
local skillText = Icons.Icon({skillName, type='skill', notext=true})
if displaySkillName then
skillText = skillText .. ' (' .. Icons.Icon({skillName, type='skill', noicon=true}) .. ')'
end
table.insert(skillIconList, skillText)
end
end
end
end
end
table.insert(lineArray, 'Any action in: ' .. table.concat(rhaStrPart, ', ') .. subText)
end
if not Shared.tableIsEmpty(skillIconList) then
table.insert(lineArray, 'Any action in: ' .. table.concat(skillIconList, ', ') .. subText)
skillIconList, subText = {}, ''
end
 
-- Supplementary stuff on top of general rare drops
if item.id == 'melvorD:Gold_Topaz_Ring' then
table.insert(lineArray, 'Killing any monster if not worn (Instead of '..Icons.Icon({"Signet Ring Half (b)", type="item"})..')')
elseif item.id == 'melvorD:Signet_Ring_Half_B' then
table.insert(lineArray, 'Killing any monster while wearing '..Icons.Icon({'Gold Topaz Ring', type='item'}))
elseif item.id == 'melvorTotH:Deadly_Toxins_Potion' then
--Adding a special override for Deadly Toxins potions
table.insert(lineArray, 'Brewing [[Lethal Toxins Potion]]s while wearing '..Icons.Icon({'Toxic Maker Gloves', type='item'}))
end
end


--Tokens are from the appropriate skill
--Tokens are from the appropriate skill
if item.isToken and item.skill ~= nil then
if item.modifiers ~= nil and item.modifiers.masteryToken ~= nil then
table.insert(lineArray, Icons._SkillReq(Constants.getSkillName(item.skill), 1))
for localSkillID, skillData in pairs(SkillData) do
if skillData.masteryTokenID ~= nil and skillData.masteryTokenID == item.id then
table.insert(lineArray, Icons._SkillReq(skillData.name, 1))
break
end
end
end
end


Line 609: Line 791:


--Shop items (including special items like gloves that aren't otherwise listed)
--Shop items (including special items like gloves that aren't otherwise listed)
local shopSources = Shop.getItemSourceArray(item.id)
if not Shared.tableIsEmpty(Shop.getItemSourceArray(item.id)) then
if Shared.tableCount(shopSources) > 0 then
table.insert(lineArray, Icons.Icon({'Shop'}))
table.insert(lineArray, Icons.Icon({'Shop'}))
end
end
Line 623: Line 804:
end
end


--Gold Topaz Ring drops from any action (when not wearing a Gold Topaz Ring)
-- Township Task reward
--Also handling Signet Ring things here
for _, task in ipairs(SkillData.Township.tasks) do
if item.name == 'Gold Topaz Ring' then
if task.rewards.items[1] ~= nil then -- Skip tasks with no items
table.insert(lineArray, 'Any non-combat action if not worn (Instead of '..Icons.Icon({"Signet Ring Half (a)", type="item"})..')')
if GameData.getEntityByID(task.rewards.items, item.id) then
table.insert(lineArray, 'Killing any monster if not worn (Instead of '..Icons.Icon({"Signet Ring Half (b)", type="item"})..')')
table.insert(lineArray, Icons.Icon({'Tasks', type='township'}))
elseif item.name == 'Signet Ring Half (a)' then
break
table.insert(lineArray, 'Any non-combat action while wearing '..Icons.Icon({'Gold Topaz Ring', type='item'}))
end
elseif item.name == 'Signet Ring Half (b)' then
end
table.insert(lineArray, 'Killing any monster while wearing '..Icons.Icon({'Gold Topaz Ring', type='item'}))
end
end


Line 654: Line 834:
end
end
if item == nil then
if item == nil then
return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end
end


Line 664: Line 844:
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
table.insert(resultPart, '\r\n|- class="headerRow-0"')
table.insert(resultPart, '\r\n|- class="headerRow-0"')
table.insert(resultPart, '\r\n!Source!!Source Type!!Quantity!!colspan="2"|Chance')
table.insert(resultPart, '\r\n!Source!!Level!!Quantity!!colspan="2"|Chance')


--Set up function for adding rows
--Set up function for adding rows
local buildRow = function(source, type, minqty, qty, weight, totalWeight)
local buildRow = function(source, level, levelNum, minqty, qty, weight, totalWeight, expIcon)
if minqty == nil then minqty = 1 end
if minqty == nil then minqty = 1 end
if expIcon == nil then expIcon = '' end
if level == nil then level = 'N/A' end
local rowPart = {}
local rowPart = {}
table.insert(rowPart, '\r\n|-')
table.insert(rowPart, '\r\n|-')
table.insert(rowPart, '\r\n|style="text-align: left;"|'..source)
table.insert(rowPart, '\r\n|style="text-align: left;"|'..source)
table.insert(rowPart, '\r\n|style="text-align: left;"|'..type)
-- Retrieve numeric level value for sorting, or remove anything between [[]]
 
local levelValue = ''
table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="'..qty..'"|'..Shared.formatnum(minqty))
if levelNum ~= nil then
if qty ~= minqty then table.insert(rowPart, ' - '..Shared.formatnum(qty)) end
levelValue = tostring(levelNum)
else
levelValue = level:match('%[%[.-%]%]%s*(%w+)$') or ''
end
table.insert(rowPart, '\r\n|style="text-align: left;" data-sort-value="'..levelValue..'"|'..expIcon.. level)
table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="'..qty..'"|'..Num.formatnum(minqty))
if qty ~= minqty then table.insert(rowPart, ' - '..Num.formatnum(qty)) end
local chance = weight / totalWeight * 100
local chance = weight / totalWeight * 100
-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
local fmt = (chance < 0.10 and '%.2g') or '%.2f'
local fmt = (chance < 0.10 and '%.2g') or '%.2f'
chance = string.format(fmt, chance)
local chanceStr = string.format(fmt, chance)
if weight >= totalWeight then
if weight >= totalWeight then
-- Fraction would be 1/1, so only show the percentage
-- Fraction would be 1/1, so only show the percentage
chance = 100
chanceStr = '100'
table.insert(rowPart, '\r\n|colspan="2" ')
table.insert(rowPart, '\r\n|colspan="2" ')
else
else
local fraction = Shared.fraction(weight, totalWeight)
local fraction = Num.fraction(weight, totalWeight)
if Shared.contains(fraction, '%.') then
if Shared.contains(fraction, '%.') then
--If fraction contains decimals, something screwy happened so just show only percentage
--If fraction contains decimals, something screwy happened so just show only percentage
Line 691: Line 879:
table.insert(rowPart, '\r\n|colspan="2" ')
table.insert(rowPart, '\r\n|colspan="2" ')
else
else
table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="' .. chance .. '"| ' .. Shared.fraction(weight, totalWeight) .. '\r\n|')
table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="' .. chanceStr .. '"| ' .. Num.fraction(weight, totalWeight) .. '\r\n|')
end
end
end
end
Line 698: Line 886:
table.insert(rowPart, 'style="text-align: right;" data-sort-value="0"|Varies (see Thieving page)')
table.insert(rowPart, 'style="text-align: right;" data-sort-value="0"|Varies (see Thieving page)')
else
else
table.insert(rowPart, 'style="text-align: right;" data-sort-value="'.. chance .. '"|'..chance..'%')
table.insert(rowPart, 'style="text-align: right;" data-sort-value="'.. chanceStr .. '"|'..chanceStr..'%')
end
end
return table.concat(rowPart)
return table.concat(rowPart)
Line 706: Line 894:
--Alright, time to go through a few ways to get the item
--Alright, time to go through a few ways to get the item
--First up: Can we kill somebody and take theirs?
--First up: Can we kill somebody and take theirs?
for i, monster in ipairs(MonsterData.Monsters) do
for i, drop in ipairs(p._getItemMonsterSources(item)) do
local minqty = 1
local monster = GameData.getEntityByID('monsters', drop.id)
local qty = 1
local iconName = monster.name
local wt = 0
if SourceOverrides[drop.id] ~= nil then
local totalWt = 0
iconName = SourceOverrides[drop.id]
--Only add bones if this monster has loot (ie appears outside a dungeon) and isn't a boss
--... unless we're looking for Shards of course, at which point we'll take any monster with the right bones
if monster.bones == item.id and Monsters._getMonsterBones(monster) ~= nil then
qty = monster.boneQty ~= nil and monster.boneQty or 1
minqty = qty
wt = 1
totalWt = 1
elseif monster.lootTable ~= nil then
-- If the monster has a loot table, check if the item we are looking for is in there
-- Dungeon exclusive monsters don't count as they are either:
--  - A monster before the boss, which doesn't drop anything except shards (checked above)
--  - A boss monster, whose drops are accounted for in data from Areas instead
for j, loot in ipairs(monster.lootTable) do
totalWt = totalWt + loot[2]
if loot[1] == item.id and not Monsters._isDungeonOnlyMonster(monster) then
wt = loot[2]
qty = loot[3]
end
end
end
end
local lootChance = monster.lootChance ~= nil and monster.bones ~= item.id and monster.lootChance or 100


if wt > 0 and lootChance > 0 then
if monster ~= nil then
-- Item drops when the monster is killed
local monsterLevel = Monsters._getMonsterCombatLevel(monster)
table.insert(dropRows, {source = Icons.Icon({monster.name, type='monster'}), type = '[[Monster]]', minqty = minqty, qty = qty, weight = wt * lootChance, totalWeight = totalWt * 100})
table.insert(dropRows, {
source = Icons.Icon({iconName, type='monster'}),
level = Icons.Icon({'Combat', 'Monsters', notext=true}) .. ' Level ' .. Num.formatnum(monsterLevel),
levelNum = monsterLevel,
minqty = drop.minQty,
qty = drop.maxQty,
weight = drop.dropWt,
totalWeight = drop.totalWt,
expIcon = Icons.getExpansionIcon(drop.id)})
end
end
--Patching in here because it uses the same format
--Can we find this in an Archaeology digsite?
for i, drop in ipairs(p._getItemArchSources(item)) do
if drop.name ~= nil then
table.insert(dropRows, {
source = Icons.Icon({drop.name, type='poi'}),  
level = Icons._SkillReq('Archaeology', drop.level) .. ' ('..drop.size..')',
levelNum = drop.level,
minqty = drop.minQty,  
qty = drop.maxQty,  
weight = drop.dropWt,  
totalWeight = drop.totalWt,
expIcon = Icons.getExpansionIcon(drop.id)})
end
end
end
end
-- Is the item dropped from any dungeon?
-- Is the item dropped from any dungeon?
local dungeonList = Areas.getAreas(function(area) return area.type == 'dungeon' and type(area.rewards) == 'table' and Shared.contains(area.rewards, item.id) end)
local dungeonEntities = {
if dungeonList ~= nil then
['Dungeon'] = GameData.rawData.dungeons,
for i, dungeon in ipairs(dungeonList) do
['The Abyss'] = GameData.rawData.abyssDepths
table.insert(dropRows, {source = Icons.Icon({dungeon.name, type='dungeon'}), type = '[[Dungeon]]', minqty = 1, qty = 1, weight = 1, totalWeight = 1})
}
for entity, dungeons in pairs(dungeonEntities) do
local iconType = entity == 'Dungeon' and 'dungeon' or 'combatArea'
for i, dungeon in ipairs(dungeons) do
if (dungeon.oneTimeRewardID ~= nil and item.id == dungeon.oneTimeRewardID) or
(type(dungeon.rewardItemIDs) == 'table' and Shared.contains(dungeon.rewardItemIDs, item.id)) then
table.insert(dropRows, {
source = Icons.Icon({dungeon.name, type=iconType}),
level = '[['..entity..']]',
minqty = 1,
qty = 1,
weight = 1,
totalWeight = 1,
expIcon = Icons.getExpansionIcon(dungeon.id)})
elseif dungeon.eventID ~= nil then
-- Is the item dropped from a combat event (e.g. Impending Darkness event)?
local event = GameData.getEntityByID('combatEvents', dungeon.eventID)
if type(event) == 'table' and type(event.itemRewardIDs) == 'table' then
for eventCycle, itemRewardID in ipairs(event.itemRewardIDs) do
if item.id == itemRewardID then
local sourceTxt = Icons.Icon({dungeon.name, type=iconType}) .. (eventCycle == Shared.tableCount(event.itemRewardIDs) and '' or ', Cycle ' .. eventCycle)
table.insert(dropRows, {
source = sourceTxt,  
level = '[['..entity..']]',
minqty = 1,  
qty = 1,  
weight = 1,  
totalWeight = 1})
break
end
end
end
end
end
end
end
end
-- Is the item dropped from a cycle of the Impending Darkness event?
 
for i, eventItemID in ipairs(Areas.eventData.rewards) do
for i, stronghold in ipairs(GameData.rawData.strongholds) do
if item.id == eventItemID then
for tier, tierData in pairs(stronghold.tiers) do
sourceTxt = Icons.Icon({'Impending Darkness Event', type='dungeon'}) .. (i == Shared.tableCount(Areas.eventData.rewards) and '' or ', Cycle ' .. i)
if type(tierData.rewards) == 'table' and type(tierData.rewards.items) == 'table' then
table.insert(dropRows, {source = sourceTxt, type = '[[Dungeon]]', minqty = 1, qty = 1, weight = 1, totalWeight = 1})
for i, reward in ipairs(tierData.rewards.items) do
break
if reward.id == item.id then
table.insert(dropRows, {
source = Icons.Icon({stronghold.name, type='combatArea'}),
level = '[[Strongholds|'..tier..']]',
minqty = 1,  
qty = 1,  
weight = tierData.rewards.chance,  
totalWeight = 100,
expIcon = Icons.getExpansionIcon(stronghold.id)})
end
end
end
end
end
end
--Special exception for the Fire/Infernal Cape and first two lore books as bonus dungeon drops
if sourceOverrides['Dungeon'][item.id] ~= nil then
local sourceTxt = Icons.Icon({sourceOverrides['Dungeon'][item.id], type='dungeon'})
table.insert(dropRows, {source = sourceTxt, type = '[[Dungeon]]', minqty = 1, qty = 1, weight = 1, totalWeight = 1})
end
end


--Next: Can we find it by rummaging around in another item?
-- Can we find it in an openable item?
for i, item2 in ipairs(ItemData.Items) do
for i, item2 in ipairs(GameData.rawData.items) do
if item2.dropTable ~= nil then
if item2.dropTable ~= nil then
local qty = 1
local minQty, maxQty, wt, totalWt = 1, 1, 0, 0
local wt = 0
for j, loot in ipairs(item2.dropTable) do
local totalWt = 0
totalWt = totalWt + loot.weight
for j, loot in pairs(item2.dropTable) do
if loot.itemID == item.id then
totalWt = totalWt + loot[2]
wt = loot.weight
if loot[1] == item.id then
minQty = loot.minQuantity
wt = loot[2]
maxQty = loot.maxQuantity
if item2.dropQty ~= nil then qty = item2.dropQty[j] end
end
end
end
end
Line 775: Line 1,005:
if wt > 0 then
if wt > 0 then
local sourceTxt = Icons.Icon({item2.name, type='item'})
local sourceTxt = Icons.Icon({item2.name, type='item'})
table.insert(dropRows, {source = sourceTxt, type = '[[Chest]]', minqty = 1, qty = qty, weight = wt, totalWeight = totalWt})
table.insert(dropRows, {
source = sourceTxt,  
level = '[[Chest]]',
minqty = minQty,  
qty = maxQty,  
weight = wt,  
totalWeight = totalWt,
expIcon = Icons.getExpansionIcon(item2.id)})
end
end
end
end
end
end


--Finally, let's try just stealing it
-- Can it be obtained from Thieving?
local thiefType = Icons.Icon({"Thieving", type='skill'})
local thiefItems = Skills.getThievingSourcesForItem(item.id)
local thiefItems = Skills.getThievingSourcesForItem(item.id)
for i, thiefRow in ipairs(thiefItems) do
for i, thiefRow in ipairs(thiefItems) do
local sourceTxt = ''
local sourceTxt = ''
if thiefRow.npc == 'all' then
if thiefRow.npc == 'all' then
sourceTxt = "Thieving Rare Drop"
sourceTxt = 'Thieving Rare Drop'
else
else
sourceTxt = Icons.Icon({thiefRow.npc, type='thieving'})
sourceTxt = Icons.Icon({thiefRow.npc, type='thieving'})
end
end
table.insert(dropRows, {source = sourceTxt, type = thiefType, minqty = thiefRow.minQty, qty = thiefRow.maxQty, weight = thiefRow.wt, totalWeight = thiefRow.totalWt})
local levelNum = thiefRow.abyssalLevel or thiefRow.level
local isAbyssal = thiefRow.abyssalLevel ~= nil
table.insert(dropRows, {
source = sourceTxt,
level = Icons._SkillReq("Thieving", levelNum, false, (isAbyssal and "melvorItA:Abyssal" or nil)),
levelNum = levelNum,
minqty = thiefRow.minQty,  
qty = thiefRow.maxQty,  
weight = thiefRow.wt,  
totalWeight = thiefRow.totalWt,
expIcon = Icons.getExpansionIcon(thiefRow.npcID)})
end
end


--Bonus overtime: Special Fishing table & mining gem table. Also Rags to Riches
-- Fishing: Junk & Specials
if Shared.contains(SkillData.Fishing.JunkItems, item.id) then
if Shared.contains(SkillData.Fishing.junkItemIDs, item.id) then
local fishSource = '[[Fishing#Junk|Junk]]'
local fishSource = '[[Fishing#Junk|Junk]]'
local fishType = Icons.Icon({'Fishing', type='skill'})
local fishType = Icons.Icon({'Fishing', type='skill'})
local fishTotWeight = Shared.tableCount(SkillData.Fishing.JunkItems)
local fishTotWeight = Shared.tableCount(SkillData.Fishing.junkItemIDs)
table.insert(dropRows, {source = fishSource, type = fishType, minqty = 1, qty = 1, weight = 1, totalWeight = fishTotWeight})
table.insert(dropRows, {
source = fishSource,  
level = Icons._SkillReq("Fishing", 1),
levelNum = 1,
minqty = 1,  
qty = 1,  
weight = 1,  
totalWeight = fishTotWeight})
else
else
local fishTotWeight, fishItem = 0, nil
local fishTotWeight, fishItem, realmID = {['melvorD:Melvor'] = 0, ['melvorItA:Abyssal'] = 0}, nil, nil
for i, specialItem in ipairs(SkillData.Fishing.SpecialItems) do
for i, specialItem in ipairs(SkillData.Fishing.specialItems) do
if specialItem[1] == item.id then
for f, drop in ipairs(specialItem.drops) do
fishItem = specialItem
if drop.itemID == item.id then
fishItem = drop
realmID = specialItem.realmID
end
fishTotWeight[specialItem.realmID] = fishTotWeight[specialItem.realmID] + drop.weight
end
end
fishTotWeight = fishTotWeight + specialItem[2]
end
end
if fishItem ~= nil then
if fishItem ~= nil then
local fishSource = '[[Fishing#Special|Special]]'
local fishSource = '[[Fishing#Special|Special]]'
local fishType = Icons.Icon({'Fishing', type='skill'})
local fishType = Icons.Icon({SkillData.Fishing.name, type='skill'})
table.insert(dropRows, {source = fishSource, type = fishType, minqty = fishItem[3], qty = fishItem[3], weight = fishItem[2], totalWeight = fishTotWeight})
table.insert(dropRows, {
source = fishSource,  
level = Icons._SkillReq("Fishing", 1, false, realmID),
levelNum = 1,
minqty = fishItem.minQuantity,  
qty = fishItem.maxQuantity,  
weight = fishItem.weight,  
totalWeight = fishTotWeight[realmID]})
end
end
end
end
--Jadestone is special and doesn't count
 
if item.type == 'Gem' and item.name ~= 'Jadestone' then
-- Mining: Gems, and also Alt. Magic spells producing random gems
local mineType = Icons.Icon({'Mining', type='skill'})
if Shared.contains({'Gem', 'Superior Gem'}, item.type) then
local thisGemChance = Items.GemTable[item.name].chance
local gemKeys = { 'randomGems', 'randomSuperiorGems' }
local totalGemChance = 0
for i, gemKey in ipairs(gemKeys) do
for i, gem in pairs(Items.GemTable) do
local thisGem, totalGemWeight = nil, 0
totalGemChance = totalGemChance + gem.chance
for j, gem in ipairs(GameData.rawData[gemKey]) do
end
totalGemWeight = totalGemWeight + gem.weight
table.insert(dropRows, {source = '[[Mining#Gems|Gem]]', type = mineType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
if gem.itemID == item.id then
local magicType = Icons.Icon({'Magic', type = 'skill'})
thisGem = gem
for i, altSpell in ipairs(MagicData.AltMagic) do
end
if type(altSpell.produces) == 'number' and altSpell.produces == -3 then
end
table.insert(dropRows, {source = Icons.Icon({altSpell.name, type='spell'}), type = magicType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
if thisGem ~= nil then
local expIcon = ''
local sourceTxt
local lv = nil
if item.type == 'Superior Gem' then
expIcon = Icons.TotH()
sourceTxt = '[[Mining#Superior Gems|Superior Gem]]'
-- Superior gems can only be found with Mining 100 or above
lv = 100
else
sourceTxt = '[[Mining#Gems|Gem]]'
-- Gems can only be found with any Mining level
lv = 1
end
table.insert(dropRows, {
source = sourceTxt,  
level = Icons._SkillReq('Mining', lv),
levelNum = lv,
minqty = thisGem.minQuantity,  
qty = thisGem.maxQuantity,  
weight = thisGem.weight,  
totalWeight = totalGemWeight,
expIcon = expIcon})
-- Check for Alt. Magic spells also
local producesKey = (gemKey == 'randomGems' and 'RandomGem') or 'RandomSuperiorGem'
for j, spell in ipairs(Magic.getSpellsBySpellBook('altMagic')) do
if spell.produces ~= nil and spell.produces == producesKey then
table.insert(dropRows, {
source = Icons.Icon({spell.name, type=Magic._getSpellIconType(spell)}),
level = Icons.Icon({'Alternative Magic', type='skill', img='Magic', notext=true}) .. ' Level ' .. spell.level,
levelNum = spell.level,
minqty = thisGem.minQuantity,  
qty = thisGem.maxQuantity,
weight = thisGem.weight,  
totalWeight = totalGemWeight,
expIcon = Icons.getExpansionIcon(spell.id)})
end
end
end
end
end
end
Line 831: Line 1,132:


--Make sure to return nothing if there are no drop sources
--Make sure to return nothing if there are no drop sources
if Shared.tableCount(dropRows) == 0 then return '' end
if Shared.tableIsEmpty(dropRows) then return '' end
 
table.sort(dropRows, function(a, b)
table.sort(dropRows, function(a, b)
if a.weight / a.totalWeight == b.weight / b.totalWeight then
if a.weight / a.totalWeight == b.weight / b.totalWeight then
if a.minqty + a.qty == b.minqty + b.qty then
if a.minqty + a.qty == b.minqty + b.qty then
return (a.type == b.type and a.source < b.source) or a.type < b.type
return (a.level == b.level and a.source < b.source) or a.level < b.level
else
else
return a.minqty + a.qty > b.minqty + b.qty
return a.minqty + a.qty > b.minqty + b.qty
Line 844: Line 1,145:
end
end
end)
end)
for i, data in pairs(dropRows) do
for i, data in ipairs(dropRows) do
table.insert(resultPart, buildRow(data.source, data.type, data.minqty, data.qty, data.weight, data.totalWeight))
table.insert(resultPart, buildRow(data.source, data.level, data.levelNum, data.minqty, data.qty, data.weight, data.totalWeight, data.expIcon))
end
end


Line 856: Line 1,157:
local item = Items.getItem(itemName)
local item = Items.getItem(itemName)
if item == nil then
if item == nil then
return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end
end


Line 864: Line 1,165:
function p._getItemUpgradeTable(item)
function p._getItemUpgradeTable(item)
local resultPart = {}
local resultPart = {}
if item.itemsRequired ~= nil then
local upgrade = GameData.getEntityByProperty('itemUpgrades', 'upgradedItemID', item.id)
--First, get details on all the required materials
if upgrade ~= nil then
local upgradeFrom = {}
local upgradeCost = Common.getCostString({
local materials = {}
["items"] = upgrade.itemCosts,
for i, row in pairs(item.itemsRequired) do
["currencies"] = upgrade.currencyCosts
local mat = Items.getItemByID(row[1])
})
--Check to see if the source item can trigger the upgrade
 
if mat.canUpgrade or (mat.type == 'Armour' and mat.canUpgrade == nil) then
table.insert(upgradeFrom, Icons.Icon({mat.name, type='item'}))
end
table.insert(materials, Icons.Icon({mat.name, type='item', qty=row[2]}))
end
if type(item.trimmedGPCost) == 'number' and item.trimmedGPCost > 0 then
table.insert(materials, Icons.GP(item.trimmedGPCost))
end
table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="2"|[[Upgrading Items|Item Upgrade]]')
table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="2"|[[Upgrading Items|Item Upgrade]]')
--[[result = result..'\r\n|-\r\n!style="text-align:right;"|Upgrades From\r\n|'
result = result..table.concat(upgradeFrom, '<br/>')--]]
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials\r\n|')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials\r\n|')
table.insert(resultPart, table.concat(materials, '<br/>'))
table.insert(resultPart, upgradeCost)
table.insert(resultPart, '\r\n|}')
table.insert(resultPart, '\r\n|}')
end
end
Line 893: Line 1,184:
local item = Items.getItem(itemName)
local item = Items.getItem(itemName)
if item == nil then
if item == nil then
return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end
end


return p._getItemUpgradeTable(item)
return p._getItemUpgradeTable(item)
end
function p._getSuperheatSmithRecipe(item)
local smithRecipe = GameData.getEntityByProperty(SkillData.Smithing.recipes, 'productID', item.id)
if smithRecipe ~= nil and smithRecipe.categoryID == 'melvorD:Bars' then
return smithRecipe
end
end
end


Line 902: Line 1,200:
--Manually build the Superheat Item table
--Manually build the Superheat Item table
-- Validate that the item can be superheated
-- Validate that the item can be superheated
local canSuperheat, smithRecipe = true, nil
local smithRecipe = p._getSuperheatSmithRecipe(item)
if type(item.masteryID) ~= 'table' then
if smithRecipe == nil then
canSuperheat = false
return Shared.printError('The item "' .. item.name .. '" cannot be superheated')
elseif item.masteryID[1] ~= SkillEnum.Smithing then
canSuperheat = false
else
smithRecipe = SkillData.Smithing.Recipes[item.masteryID[2] + 1]
if smithRecipe == nil or smithRecipe.category ~= 0 then
canSuperheat = false
end
end
if not canSuperheat then
return 'ERROR: The item "' .. item.name .. '" cannot be superheated[[Category:Pages with script errors]]'
end
end


local oreStringPart, coalString = {}, ''
local oreStringPart, coalString = {}, ''
for i, mat in ipairs(smithRecipe.itemCosts) do
for i, mat in ipairs(smithRecipe.itemCosts) do
local thisMat = Items.getItemByID(mat.id)
local matItem = Items.getItemByID(mat.id)
if thisMat.name == 'Coal Ore' then
if mat.id == 'melvorD:Coal_Ore' then
coalString = Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty})
coalString = Icons.Icon({matItem.name, type='item', notext='true', qty=mat.quantity})
else
else
table.insert(oreStringPart, Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty}))
table.insert(oreStringPart, Icons.Icon({matItem.name, type='item', notext='true', qty=mat.quantity}))
end
end
end
end
--Set up the header
--Set up the header
local superheatTable = {}
local superheatTable = {}
Line 934: Line 1,223:
table.insert(superheatTable, '!!'..Icons.Icon({item.name, type='item', notext='true'})..' Bars')
table.insert(superheatTable, '!!'..Icons.Icon({item.name, type='item', notext='true'})..' Bars')
table.insert(superheatTable, '!!Ore!!Runes')
table.insert(superheatTable, '!!Ore!!Runes')
--Loop through all the variants
 
local spellNames = {'Superheat I', 'Superheat II', 'Superheat III', 'Superheat IV'}
--Loop through all the variants
for i, sName in pairs(spellNames) do
local spells = Magic.getSpellsProducingItem(item.id)
local spell = Magic.getSpell(sName, 'AltMagic')
for i, spell in ipairs(spells) do
table.insert(superheatTable, '\r\n|-\r\n|'..Icons.Icon({spell.name, type='spell', notext=true, size=50}))
if spell.specialCost ~= nil and Shared.contains({ 'BarIngredientsWithCoal', 'BarIngredientsWithoutCoal' }, spell.specialCost.type) then
table.insert(superheatTable, '||'..Icons.Icon({spell.name, type='spell', noicon=true})..'||style="text-align:right;"|'..smithRecipe.level)
local imgType = Magic._getSpellIconType(spell)
table.insert(superheatTable, '||style="text-align:right;"|'..spell.level..'||style="text-align:right;"|'..spell.baseExperience)
table.insert(superheatTable, '\r\n|-\r\n|'..Icons.Icon({spell.name, type=imgType, notext=true, size=50}))
table.insert(superheatTable, '||style="text-align:right;"|'..spell.productionRatio)
table.insert(superheatTable, '||'..Icons.Icon({spell.name, type=imgType, noicon=true})..'||style="text-align:right;"|'..smithRecipe.level)
table.insert(superheatTable, '|| '..table.concat(oreStringPart, ', '))
table.insert(superheatTable, '||style="text-align:right;"|'..spell.level..'||style="text-align:right;"|'..spell.baseExperience)
if spell.consumes == 2 and coalString ~= '' then
table.insert(superheatTable, '||style="text-align:right;"|'..spell.productionRatio)
-- 2 = Superheat with coal, 3 = Superheat without coal
table.insert(superheatTable, '|| '..table.concat(oreStringPart, ', '))
table.insert(superheatTable, (Shared.tableCount(oreStringPart) > 0 and ', ' or '') .. coalString)
if spell.specialCost.type == 'BarIngredientsWithCoal' and coalString ~= '' then
table.insert(superheatTable, (not Shared.tableIsEmpty(oreStringPart) and ', ' or '') .. coalString)
end
table.insert(superheatTable, '||style="text-align:center"| ' .. Magic._getSpellRunes(spell))
end
end
table.insert(superheatTable, '||style="text-align:center"| ' .. Magic._getSpellRunes(spell))
end
end
--Add the table end and add the table to the result string
 
--Add the table end and add the table to the result string
table.insert(superheatTable, '\r\n|}')
table.insert(superheatTable, '\r\n|}')
return table.concat(superheatTable)
return table.concat(superheatTable)
Line 958: Line 1,250:
local item = Items.getItem(itemName)
local item = Items.getItem(itemName)
if item == nil then
if item == nil then
return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end
end


return p._getItemSuperheatTable(item)
return p._getItemSuperheatTable(item)
end
function p._getTownshipTraderTable(item)
for i, tsResource in ipairs(SkillData.Township.itemConversions.fromTownship) do
for j, tradeDef in ipairs(tsResource.items) do
if tradeDef.itemID == item.id then
-- Item found, build table
local res = GameData.getEntityByID(SkillData.Township.resources, tsResource.resourceID)
local resName = (res ~= nil and res.name) or 'Unknown'
local resQty = math.max(item.sellsFor, 2)
local resultPart = {}
table.insert(resultPart, '{| class="wikitable"\n|-')
table.insert(resultPart, '\n!colspan="2"| ' .. Icons.Icon({'Township', 'Trader', type='skill'}))
table.insert(resultPart, '\n|-\n!style="text-align:right;"| Cost')
table.insert(resultPart, '\n| ' .. Icons.Icon({resName, qty=resQty, type='resource'}))
table.insert(resultPart, '\n|-\n!style="text-align:right;| Requirements')
table.insert(resultPart, '\n| ' .. Shop.getRequirementString(tradeDef.unlockRequirements))
table.insert(resultPart, '\n|}')
return table.concat(resultPart)
end
end
end
return ''
end
end


Line 967: Line 1,284:
local resultPart = {}
local resultPart = {}
local shopTable = Shop._getItemShopTable(item)
local shopTable = Shop._getItemShopTable(item)
if string.len(shopTable) > 0 then
if shopTable ~= '' then
table.insert(resultPart, '===Shop===\r\n'..shopTable)
table.insert(resultPart, '===Shop===\r\n'..shopTable)
end
end


local creationTable = p._getCreationTable(item)
local creationTable = p._getCreationTable(item)
if string.len(creationTable) > 0 then
if creationTable ~= '' then
if #resultPart > 0 then table.insert(resultPart, '\r\n') end
if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\r\n') end
table.insert(resultPart, '===Creation===\r\n'..creationTable)
table.insert(resultPart, '===Creation===\r\n'..creationTable)
end
end


local upgradeTable = p._getItemUpgradeTable(item)
local upgradeTable = p._getItemUpgradeTable(item)
if string.len(upgradeTable) > 0 then
if upgradeTable ~= '' then
if #resultPart > 0 then table.insert(resultPart, '\r\n') end
if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\r\n') end
if string.len(creationTable) == 0 then table.insert(resultPart, '===Creation===\r\n') end
if creationTable ~= '' then table.insert(resultPart, '===Creation===\r\n') end
table.insert(resultPart, upgradeTable)
table.insert(resultPart, upgradeTable)
end
end


if type(item.masteryID) == 'table' and item.masteryID[1] == SkillEnum.Smithing then
local townshipTable = p._getTownshipTraderTable(item)
local recipe = SkillData.Smithing.Recipes[item.masteryID[2] + 1]
if townshipTable ~= '' then
if recipe ~= nil and recipe.category == 0 then
if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\n') end
table.insert(resultPart, '\r\n==='..Icons.Icon({'Alt. Magic', type='skill'})..'===\r\n'..p._getItemSuperheatTable(item))
table.insert(resultPart, '===Township===\n' .. townshipTable)
end
end
 
if p._getSuperheatSmithRecipe(item) ~= nil then
table.insert(resultPart, '\r\n==='..Icons.Icon({'Alt. Magic', type='skill'})..'===\r\n'..p._getItemSuperheatTable(item))
end
end


local lootTable = p._getItemLootSourceTable(item)
local lootTable = p._getItemLootSourceTable(item)
if string.len(lootTable) > 0 then
if lootTable ~= '' then
if #resultPart > 0 then table.insert(resultPart, '\r\n') end
if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\r\n') end
table.insert(resultPart, '===Loot===\r\n'..lootTable)
table.insert(resultPart, '===Loot===\r\n'..lootTable)
end
end
Line 1,003: Line 1,323:
local item = Items.getItem(itemName)
local item = Items.getItem(itemName)
if item == nil then
if item == nil then
return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
end
end


Line 1,015: Line 1,335:
table.insert(resultPart, '!colspan="2"|Item\r\n! Passive\r\n')
table.insert(resultPart, '!colspan="2"|Item\r\n! Passive\r\n')


local itemArray = Items.getItems(function(item) return item.validSlots ~= nil and Shared.contains(item.validSlots, 'Passive') end)
local itemArray = Items.getItems(function(item) return item.validSlots ~= nil and (item.golbinRaidExclusive == nil or not item.golbinRaidExclusive) and Shared.contains(item.validSlots, 'Passive') end)
 
table.sort(itemArray, function(a, b) return a.id < b.id end)


for i, item in ipairs(itemArray) do
for i, item in ipairs(itemArray) do
local passiveDesc = item.customDescription or Modifiers.getModifiersText(item.modifiers, false, false, 10)
table.insert(resultPart, '|-\r\n')
table.insert(resultPart, '|-\r\n')
table.insert(resultPart, '! '..Icons.Icon({item.name, type='item', notext='true'})..'\r\n! '..Icons.Icon({item.name, type='item', noicon=true})..'\r\n')
table.insert(resultPart, '! '..Icons.Icon({item.name, type='item', notext='true'})..'\r\n! '..Icons.Icon({item.name, type='item', noicon=true})..'\r\n')
table.insert(resultPart, '| '..item.description..'\r\n')
table.insert(resultPart, '| '..passiveDesc..'\r\n')
end
end


Line 1,032: Line 1,351:
function p._getItemMonsterSources(item)
function p._getItemMonsterSources(item)
local resultArray = {}
local resultArray = {}
for i, monster in ipairs(MonsterData.Monsters) do
for i, monster in ipairs(GameData.rawData.monsters) do
local chance = 0
local chance = 0
local weight = 0
local weight = 0
local minQty = 1
local minQty = 1
local maxQty = 1
local maxQty = 1
if monster.bones == item.id and Monsters._getMonsterBones(monster) ~= nil then
if monster.bones ~= nil and monster.bones.itemID == item.id and Monsters._getMonsterBones(monster) ~= nil then
-- Item is a bone, and is either a shard from God dungeons or dropped by a non-boss monster with a loot table
-- Item is a bone, and is either a shard from God dungeons or dropped by a non-boss monster with a loot table
maxQty = (monster.bones.quantity ~= nil and monster.bones.quantity) or 1
minQty = maxQty
chance = 1
chance = 1
weight = 1
weight = 1
if monster.boneQty ~= nil then
elseif monster.barrierPercent ~= nil and 'melvorAoD:Barrier_Dust' == item.id and not Monsters._isDungeonOnlyMonster(monster) then
minQty = monster.boneQty
-- Item is Barrier Dust and is not a dungeon exclusive monster
maxQty = monster.boneQty
maxQty = math.max(math.floor(Monsters._getMonsterStat(monster, 'Barrier') / 10 / 20), 1)
end
minQty = maxQty
elseif monster.lootTable ~= nil then
chance = 1
elseif monster.lootTable ~= nil and not Monsters._isDungeonOnlyMonster(monster) then
-- If the monster has a loot table, check if the item we are looking for is in there
-- If the monster has a loot table, check if the item we are looking for is in there
-- Dungeon exclusive monsters don't count as they are either:
-- Dungeon exclusive monsters don't count as they are either:
Line 1,051: Line 1,373:
--  - A boss monster, whose drops are accounted for in data from Areas instead
--  - A boss monster, whose drops are accounted for in data from Areas instead
for j, loot in ipairs(monster.lootTable) do
for j, loot in ipairs(monster.lootTable) do
weight = weight + loot[2]
weight = weight + loot.weight
if loot[1] == item.id and not Monsters._isDungeonOnlyMonster(monster) then
if loot.itemID == item.id then
chance = loot[2]
chance = loot.weight
maxQty = loot[3]
minQty = loot.minQuantity
maxQty = loot.maxQuantity
end
end
end
end
local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
local lootChance = monster.lootChance ~= nil and (monster.bones == nil or monster.bones.itemID ~= item.id) and monster.lootChance or 100
chance = chance * lootChance
chance = chance * lootChance
weight = weight * 100
weight = weight * 100
chance, weight = Shared.fractionpair(chance, weight)
chance, weight = Num.fractionpair(chance, weight)
end
end
if chance > 0 then
if chance > 0 then
Line 1,075: Line 1,398:
end
end


--[==[
function p._getItemArchSources(item)
local check = false
local itemID = item.id
local resultArray = {}
for i, digSite in pairs(SkillData.Archaeology.digSites) do
for sizeName, size in pairs(digSite.artefacts) do
local found = nil
local sizeWeight = 0
for k, artefact in pairs(size) do
sizeWeight = sizeWeight + artefact.weight
if artefact.itemID == itemID then
found = artefact
end
end
if found ~= nil then
local min = found.minQuantity
local max = found.maxQuantity
table.insert(resultArray, {
id = digSite.id,
name = digSite.name,
level = digSite.level,
size = sizeName,
minQty = min,
maxQty = max,
dropWt = found.weight,
totalWt = sizeWeight})
end
end
end
return resultArray
end
 
function p.getItemArchSources(itemName)
local item = Items.getItem(itemName)
return p._getItemArchSources(item)
end
 
--[[
-- Uncomment this block and execute 'p.test()' within the debug console
-- Uncomment this block and execute 'p.test()' within the debug console
-- to test after making changes
-- to test after making changes
function p.test()
function p.test()
local checkItems = {
local checkItems = {
'Gold Bar',
"Circlet of Rhaelyx",
'Raw Shrimp',
"Jewel of Rhaelyx",
'Coal Ore',
"Signet Ring Half (a)",
'Rune Platebody',
"Signet Ring Half (b)",
'Arrow Shafts',
"Gold Topaz Ring",
'Yew Longbow',
"Astrology Lesser Relic",
'Water Rune',
"Mysterious Stone",
'Steam Rune',
"Gold Bar",
'Controlled Heat Potion II',
"Raw Shrimp",
'Wolf',
"Coal Ore",
'Cyclops',
"Rune Platebody",
'Leprechaun',
"Arrow Shafts",
'Redwood Logs',
"Yew Longbow",
'Carrot Cake',
"Water Rune",
'Carrot Cake (Perfect)',
"Steam Rune",
'Mantalyme Herb',
"Controlled Heat Potion II",
'Carrot',
"Wolf",
'Topaz',
"Cyclops",
'Rune Essence',
"Leprechaun",
'Sanguine Blade',
"Redwood Logs",
'Ring of Power',
"Carrot Cake",
'Infernal Claw',
"Carrot Cake (Perfect)",
'Chapeau Noir',
"Mantalyme Herb",
'Stardust',
"Carrot",
'Rope',
"Topaz",
'Ancient Ring of Mastery',
"Rune Essence",
'Mysterious Stone',
"Sanguine Blade",
'Mastery Token (Cooking)',
"Ring of Power",
'Gem Gloves',
"Infernal Claw",
"Thief's Moneysack"
"Chapeau Noir",
"Stardust",
"Rope",
"Ancient Ring of Mastery",
"Mastery Token (Cooking)",
"Gem Gloves",
"Thief's Moneysack",
"Golden Stardust",
"Golden Star",
"Slayer Deterer",
"Paper",
"Lemon",
"Aranite Brush",
"Barrier Dust",
"Shadow Raven Nest",
"Void Wisp"
}
}
local checkFuncs = {
local checkFuncs = {
p.getItemSourceTables,
--p.getItemSourceTables,
--p.getCreationTable,
--p.getCreationTable,
p.getItemSources,
--p.getItemSources,
--p.getItemLootSourceTable,
--p.getItemLootSourceTable,
}
}
Line 1,137: Line 1,516:
end
end
end
end
--]==]
--]]


return p
return p

Latest revision as of 17:54, 24 November 2024

Documentation for this module may be created at Module:Items/SourceTables/doc

local p = {}

local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local Common = require('Module:Common')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Modifiers = require('Module:Modifiers')
local Magic = require('Module:Magic')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
local Shop = require('Module:Shop')
local Monsters = require('Module:Monsters')
local Skills = require('Module:Skills')
local Num = require('Module:Number')

local SourceOverrides = {
	['melvorAoD:EarthGolem'] = 'Earth Golem (AoD)'
}

local function doesRecipeHaveItemID(recipe, itemID)
	if recipe.productId == itemID then
		return true, nil
	elseif type(recipe.products) == 'table' then
		for i, product in ipairs(recipe.products) do
			if product.itemID == itemID then
				local specialReq = nil
				if recipe.products[i].minIntensityPercent ~= nil then
					specialReq = recipe.products[i].minIntensityPercent .. '% Intensity'
				end
				return true, specialReq
			end
		end
	end
	return false, nil
end

function p._getCreationTable(item)
	local skill = ''
	local specialReq = nil
	local category = nil
	local time = 0
	local maxTime = nil
	local lvl = 0
	local isAbyssal = false
	local xp = 0
	local qty = nil
	local req = nil

	local tables = {}
	local itemID = item.id
	--First figure out what skill is used to make this...

	local skillIDs = {
		['Gathering'] = {
			['Woodcutting'] = { recipeKey = 'trees' },
			['Fishing'] = { recipeKey = 'fish' },
			['Mining'] = { recipeKey = 'rockData' },
			['Farming'] = { recipeKey = 'recipes' },
			['Harvesting'] = { recipeKey = 'veinData' }
		},
		['Artisan'] = {
			['Cooking'] = { },
			['Smithing'] = { },
			['Fletching'] = { },
			['Crafting'] = { },
			['Runecrafting'] = { },
			['Herblore'] = { },
			['Summoning'] = { }
		}
	}

	-- Gathering skills
	-- All follow a similar data structure
	for localSkillID, dataProp in pairs(skillIDs.Gathering) do
		local skillData = SkillData[localSkillID]
		local skill = skillData.name
		local lvl, isAbyssal, xp, qty, req, category, time, maxTime = 0, false, 0, 0, nil, nil, 0, nil
		for i, recipe in ipairs(skillData[dataProp.recipeKey]) do
			local hasProduct, specialReq = doesRecipeHaveItemID(recipe, itemID)
			if hasProduct then
				lvl, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
				xp = recipe.baseAbyssalExperience or recipe.baseExperience
				qty = recipe.baseQuantity or 1
				if localSkillID == 'Farming' then
					req = { recipe.seedCost }
					local catData = GameData.getEntityByID(skillData.categories, recipe.categoryID)
					category = catData.name
					qty = 5 * catData.harvestMultiplier
				end
				-- Action time
				if recipe.baseMinInterval ~= nil then
					time = recipe.baseMinInterval / 1000
					if recipe.baseMaxInterval ~= nil then
						maxTime = recipe.baseMaxInterval / 1000
					end
				elseif recipe.baseInterval ~= nil then
					time = recipe.baseInterval /1000
				elseif skillData.baseInterval ~= nil then
					time = skillData.baseInterval / 1000
				end
				-- Special requirements
				if recipe.totalMasteryRequired ~= nil then
					specialReq = Icons.Icon({'Mastery', notext=true}) .. Num.formatnum(recipe.totalMasteryRequired) .. ' total [[' .. skill .. ']] [[Mastery]]'
				end
				table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qty, category, time, maxTime, specialReq))
				-- Assumes item has a single source per skill
				break
			end
		end
	end

	-- Artisan skills
	-- Allow follow a similar data structure
	for localSkillID, dataProp in pairs(skillIDs.Artisan) do
		local skillData = SkillData[localSkillID]
		local skill = skillData.name
		local lvl, isAbyssal, xp, qty, category, req, time, maxTime = 0, false, 0, 0, nil, nil, 0, nil
		for i, recipe in ipairs(skillData.recipes) do
			if recipe.productID == itemID or
				(localSkillID == 'Cooking' and recipe.perfectCookID == itemID) or
				(localSkillID == 'Herblore' and Shared.contains(recipe.potionIDs, itemID)) then
				lvl, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
				xp = recipe.baseAbyssalExperience or recipe.baseExperience
				qty = recipe.baseQuantity or 1
				-- Recipe Category
				if recipe.categoryID ~= nil then
					local catData = GameData.getEntityByID(SkillData[localSkillID].categories, recipe.categoryID)
					category = catData.modifierName or catData.name or nil
				end
				-- Action time
				if recipe.baseMinInterval ~= nil then
					time = recipe.baseMinInterval / 1000
					if recipe.baseMaxInterval ~= nil then
						maxTime = recipe.baseMaxInterval / 1000
					end
				elseif recipe.baseInterval ~= nil then
					time = recipe.baseInterval /1000
				elseif skillData.baseInterval ~= nil then
					time = skillData.baseInterval / 1000
				end
				-- Special requirements
				-- Potions have a mastery level requirement depending on the tier
				if item.charges ~= nil and item.tier ~= nil then
					local levelUnlock = GameData.getEntityByProperty(skillData.masteryLevelUnlocks, 'descriptionID', item.tier + 1)
					if levelUnlock ~= nil then
						specialReq = Icons._MasteryReq(item.name, levelUnlock.level, false)
					end
				end
				-- Materials & output quantity
				-- Special case for Summoning recipes
				if localSkillID == 'Summoning' then
					local shardCostArray, otherCostArray = {}, {}
					local recipeCost = 0
					if isAbyssal == true then
						recipeCost = skillData.recipeAPCost
					else
						recipeCost = skillData.recipeGPCost
					end
					-- Shards
					for j, itemCost in ipairs(recipe.itemCosts) do
						local shard = Items.getItemByID(itemCost.id)
						if shard ~= nil then
							table.insert(shardCostArray, Icons.Icon({shard.name, type='item', notext=true, qty=itemCost.quantity}))
						end
					end
					-- Other costs
					table.insert(otherCostArray, Common.getCostString({ ["items"] = {}, ["currencies"] = recipe.currencyCosts}))
					for j, nonShardID in ipairs(recipe.nonShardItemCosts) do
						local nonShard = Items.getItemByID(nonShardID)
						if nonShard ~= nil then
							local itemValue = math.max(nonShard.sellsFor, 20)
							local nonShardQty = math.max(1, math.ceil(recipeCost / itemValue))
							table.insert(otherCostArray, Icons.Icon({nonShard.name, type='item', notext=true, qty=nonShardQty}))
						end
					end
					req = table.concat(shardCostArray, ', ')
					if not Shared.tableIsEmpty(otherCostArray) then
						local costLen = Shared.tableCount(otherCostArray)
						req = req .. '<br/>' .. (costLen == 1 and '' or 'and one of the following:<br/>') .. table.concat(otherCostArray, "<br/>'''OR''' ")
					end
					specialReq = 'At least 1 ' .. Icons.Icon({'Summoning%23Summoning Marks', item.name, img=item.name, type='mark'}) .. ' mark discovered'
					table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qty, category, time, nil, specialReq))
				-- Some items (such as Arrow shafts) have multiple recipes
				elseif type(recipe.alternativeCosts) == 'table' then
					local reqPart, qtyPart = {}, {}
					for j, altCost in ipairs(recipe.alternativeCosts) do
						local reqSubPart = {}
						for k, itemCost in ipairs(altCost.itemCosts) do
							local reqItem = Items.getItemByID(itemCost.id)
							if reqItem == nil then
								table.insert(reqSubPart, itemCost.quantity .. 'x ?????')
							else
								table.insert(reqSubPart, Icons.Icon({reqItem.name, type='item', qty=itemCost.quantity}))
							end
						end

						table.insert(reqSubPart, Common.getCostString({ ["items"] = {}, ["currencies"] = recipe.currencyCosts}))
						table.insert(reqPart, table.concat(reqSubPart, ', '))
							table.insert(qtyPart, Num.formatnum(qty * altCost.quantityMultiplier))
					end
					local sep = "<br/>'''OR''' "
					req = table.concat(reqPart, sep)
					local qtyText = table.concat(qtyPart, sep)
					table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qtyText, category, time, maxTime, specialReq))
				-- Finally, normal recipes with a single set of item costs
				elseif type(recipe.itemCosts) == 'table' and not Shared.tableIsEmpty(recipe.itemCosts) then
					table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, recipe.itemCosts, qty, category, time, maxTime, specialReq, recipe.currencyCosts))
				end
			end
		end
	end

	-- Alt. Magic, excludes spells which can produce a variety of items, such as Gems and Bars
	-- Bars are handled by getItemSuperheatTable()
	-- Gems are handled by _getItemLootSourceTable()
	for i, altSpell in ipairs(Magic.getSpellsBySpellBook('altMagic')) do
		if altSpell.produces == item.id then
			table.insert(tables, p._buildAltMagicTable(altSpell))
		end
	end

	if Shared.tableIsEmpty(tables) then
		return ''
	else
		return table.concat(tables, '\r\n')
	end
end

function p.getAltMagicTable(frame)
	local spellName = frame.args ~= nil and frame.args[1] or frame
	local spell = Magic.getSpell(spellName, 'altMagic')
	if spell == nil then
		return Shared.printError('Could not find Alt. Magic spell "' .. spellName .. '"')
	else
		return p._buildAltMagicTable(spell)
	end
end

function p._buildAltMagicTable(spell)
	local resultPart = {}
	local imgType = Magic._getSpellIconType(spell)
	table.insert(resultPart, '{|class="wikitable"\r\n|-')
	table.insert(resultPart, '\r\n!colspan="2"|'..Icons.Icon({spell.name, type=imgType}))
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Requirements')
	table.insert(resultPart, '\r\n|'..Icons._SkillReq('Magic', spell.level))

	local costText = Magic._getAltSpellCostText(spell)
	if costText ~= nil then
		table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials')
		table.insert(resultPart, '\r\n| ' .. costText)
	end

	--Add runes
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Runes\r\n| ' .. Magic._getSpellRunes(spell))

	--Now just need the output quantity, xp, and casting time (which is always 2)
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Quantity\r\n|' .. spell.productionRatio)
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base XP\r\n|' .. spell.baseExperience)
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Cast Time\r\n|2s')
	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

function p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qty, category, time, maxTime, specialReq, currencyCost)
	if qty == nil then qty = 1 end
	local resultPart = {}
	table.insert(resultPart, '{|class="wikitable"')
	table.insert(resultPart, '\r\n!colspan="2"|Item ' .. (req == nil and 'Creation' or 'Production'))
	table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Requirements')
	table.insert(resultPart, '\r\n|'..Icons._SkillReq(skill, lvl, false, (isAbyssal and "melvorItA:Abyssal" or nil)))
	if specialReq ~= nil then table.insert(resultPart, '<br/>'..specialReq) end

	if req ~= nil then
		table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Materials\r\n|')
		if type(req) == 'table' then
			for i, mat in ipairs(req) do
				if i > 1 then table.insert(resultPart, '<br/>') end
				local matItem = Items.getItemByID(mat.id)
				if matItem == nil then
					table.insert(resultPart, mat.quantity..'x ?????')
				else
					table.insert(resultPart, Icons.Icon({matItem.name, type='item', qty=mat.quantity}))
				end
			end
			
			if currencyCost ~= nil then
				table.insert(resultPart, Common.getCostString({ ["items"] = {}, ["currencies"] = currencyCost }))
			end
		else
			table.insert(resultPart, req)
		end
	end
	if category ~= nil then
		table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Recipe Category')
		table.insert(resultPart, '\r\n|'..category)
	end
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Quantity')
	table.insert(resultPart, '\r\n|'..qty)
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Experience')
	table.insert(resultPart, '\r\n|'..Num.formatnum(xp)..' XP')
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Creation Time')
	table.insert(resultPart, '\r\n|'..Shared.timeString(time, true))
	if maxTime ~= nil and maxTime > time then table.insert(resultPart, ' - '..Shared.timeString(maxTime, true)) end
	table.insert(resultPart, '\r\n|}')

	return table.concat(resultPart)
end

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

	return p._getCreationTable(item)
end

function p._getItemSources(item, asList, addCategories, separator)
	local lineArray = {}
	local categoryArray = {}
	local sep = separator or ','

	--Alright, time to go through all the ways you can get an item...
	--First up: Can we kill somebody and take theirs?
	local killStrPart = {}
	for i, monster in ipairs(GameData.rawData.monsters) do
		local isDrop = false
		if monster.bones ~= nil and monster.bones.itemID == item.id and Monsters._getMonsterBones(monster) ~= nil then
			-- Item is a bone, and is either a shard from God dungeons or dropped by a non-boss monster with a loot table
			isDrop = true
		elseif monster.barrierPercent ~= nil and 'melvorAoD:Barrier_Dust' == item.id and not Monsters._isDungeonOnlyMonster(monster) then
			-- Item is Barrier Dust and is not a dungeon exclusive monster
			isDrop = true
		elseif monster.lootTable ~= nil then
			-- If the monster has a loot table, check if the item we are looking for is in there
			-- Dungeon exclusive monsters don't count as they are either:
			--   - A monster before the boss, which doesn't drop anything except shards (checked above)
			--   - A boss monster, whose drops are accounted for in data from Areas instead
			for j, loot in ipairs(monster.lootTable) do
				if loot.itemID == item.id and not Monsters._isDungeonOnlyMonster(monster) then
					isDrop = true
					break
				end
			end
		end
		if isDrop then
			-- Item drops when the monster is killed
			local iconName = monster.name
			if SourceOverrides[monster.id] ~= nil then
				iconName = SourceOverrides[monster.id]
			end
			table.insert(killStrPart, Icons.Icon({iconName, type='monster', notext=true}))
		end
	end
	-- Is the item dropped from any dungeon?
	local dungeonStrPart = {}
	local dungeonEntities = {
		['Dungeon'] = GameData.rawData.dungeons,
		['The Abyss'] = GameData.rawData.abyssDepths
	}
	for entity, dungeons in pairs(dungeonEntities) do
		local iconType = entity == 'Dungeon' and 'dungeon' or 'combatArea'
		for i, dungeon in ipairs(dungeons) do
			if (dungeon.oneTimeRewardID ~= nil and item.id == dungeon.oneTimeRewardID) or
				(type(dungeon.rewardItemIDs) == 'table' and Shared.contains(dungeon.rewardItemIDs, item.id)) then
				table.insert(dungeonStrPart, Icons.Icon({dungeon.name, type=iconType, notext=true}))
			elseif dungeon.eventID ~= nil then
				-- Is the item dropped from a combat event (e.g. Impending Darkness event)?
				local event = GameData.getEntityByID('combatEvents', dungeon.eventID)
				if type(event) == 'table' and type(event.itemRewardIDs) == 'table' then
					for eventCycle, itemRewardID in ipairs(event.itemRewardIDs) do
						if item.id == itemRewardID then
							local dungPrefix = (eventCycle == Shared.tableCount(event.itemRewardIDs) and '' or eventCycle .. (eventCycle == 1 and ' cycle' or ' cycles') .. ' of ')
							table.insert(dungeonStrPart, dungPrefix .. Icons.Icon({dungeon.name, type=iconType, notext=true}))
							break
						end
					end
				end
			end
		end
	end
	
	for i, stronghold in ipairs(GameData.rawData.strongholds) do
		for tier, tierData in pairs(stronghold.tiers) do
			if type(tierData.rewards) == 'table' and type(tierData.rewards.items) == 'table' then
				for i, reward in ipairs(tierData.rewards.items) do
					if reward.id == item.id then
						table.insert(dungeonStrPart, Icons.Icon({stronghold.name, type='combatArea', notext=true}))
					end
				end
			end
		end
	end

	if not Shared.tableIsEmpty(dungeonStrPart) then
		table.insert(lineArray, 'Completing: ' .. table.concat(dungeonStrPart, sep))
	end
	if not Shared.tableIsEmpty(killStrPart) then
		table.insert(lineArray, 'Killing: ' .. table.concat(killStrPart, sep))
	end

	-- Can we find it in an openable item?
	local lootPart = {}
	for i, item2 in ipairs(GameData.rawData.items) do
		if item2.dropTable ~= nil then
			for j, loot in ipairs(item2.dropTable) do
				if loot.itemID == item.id then
					table.insert(lootPart, Icons.Icon({item2.name, type='item', notext=true}))
					break
				end
			end
		end
	end

	if not Shared.tableIsEmpty(lootPart) then
		table.insert(lineArray, 'Opening: ' .. table.concat(lootPart, sep))
	end

	-- Is the item a result of upgrading/downgrading another item?
	local upgradePart = { up = {}, down = {} }
	for i, upgrade in ipairs(GameData.rawData.itemUpgrades) do
		if item.id == upgrade.upgradedItemID then
			local key = (upgrade.isDowngrade and 'down' or 'up')
			for j, rootItemID in ipairs(upgrade.rootItemIDs) do
				local rootItem = Items.getItemByID(rootItemID)
				if rootItem ~= nil then
					table.insert(upgradePart[key], Icons.Icon({rootItem.name, type='item', notext=true}))
				end
			end
		end
	end

	local upgradeCat = false
	for catName, parts in pairs(upgradePart) do
		if not Shared.tableIsEmpty(parts) then
			if not upgradeCat then
				table.insert(categoryArray, '[[Category:Upgraded Items]]')
				upgradeCat = true
			end
			local typeText = (catName == 'up' and 'Upgrading') or 'Downgrading'
			table.insert(lineArray, typeText .. ': ' .. table.concat(parts, sep))
		end
	end

	--Next: Can we take it from somebody else -without- killing them?
	local thiefItems = Skills.getThievingSourcesForItem(item.id)
	if type(thiefItems) == 'table' then
		local includedNPCs = {}
		local thiefPart = {}
		for i, thiefRow in ipairs(thiefItems) do
			if thiefRow.npc == 'all' then
				--if 'all' is the npc, this is a rare item so just say 'Thieving level 1'
				table.insert(lineArray, Icons._SkillReq('Thieving', 1))
			elseif not Shared.contains(includedNPCs, thiefRow.npc) then
				table.insert(thiefPart, Icons.Icon({thiefRow.npc, type='thieving', notext=true}))
				table.insert(includedNPCs, thiefRow.npc)
			end
		end
		if not Shared.tableIsEmpty(thiefPart) then
			table.insert(lineArray, 'Pickpocketing: ' .. table.concat(thiefPart, sep))
		end
	end
	
	-- Can we get this item by casting an Alt. Magic spell?
	local castPart = {}
	for i, spell in ipairs(Magic.getSpellsProducingItem(item.id)) do
		table.insert(castPart, Icons.Icon({spell.name, type=Magic._getSpellIconType(spell), notext=true}))
	end
	if not Shared.tableIsEmpty(castPart) then
		table.insert(lineArray, 'Casting: ' .. table.concat(castPart, sep))
	end

	--Check if we can make it ourselves
	local skillIDs = {
		['Gathering'] = {
			['Woodcutting'] = { recipeKey = 'trees' },
			['Fishing'] = { recipeKey = 'fish' },
			['Mining'] = { recipeKey = 'rockData' },
			['Farming'] = { recipeKey = 'recipes' },
			['Harvesting'] = { recipeKey = 'veinData' }
		},
		['Artisan'] = {
			['Cooking'] = { },
			['Smithing'] = { },
			['Fletching'] = { },
			['Crafting'] = { },
			['Runecrafting'] = { },
			['Herblore'] = { },
			['Summoning'] = { }
		}
	}

	-- Gathering skills
	for localSkillID, dataProp in pairs(skillIDs.Gathering) do
		local skillData = SkillData[localSkillID]
		local skill = skillData.name
		for i, recipe in ipairs(skillData[dataProp.recipeKey]) do
			local hasProduct = doesRecipeHaveItemID(recipe, item.id)
			if hasProduct then
				if localSkillID == 'Farming' and recipe.seedCost ~= nil then
					local seedItem = Items.getItemByID(recipe.seedCost.id)
					if seedItem ~= nil then
						table.insert(lineArray, 'Growing: ' .. Icons.Icon({seedItem.name, type='item', notext='true'}))
					end
				else
					local level, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
					table.insert(lineArray, Icons._SkillReq(skill, level, false, (isAbyssal and "melvorItA:Abyssal" or nil)))
				end
				break
			end
		end
	end

	-- Artisan skills
	for localSkillID, dataProp in pairs(skillIDs.Artisan) do
		local skillData = SkillData[localSkillID]
		local skill = skillData.name
		for i, recipe in ipairs(skillData.recipes) do
			if recipe.productID == item.id or
				(localSkillID == 'Cooking' and recipe.perfectCookID == item.id) or
				(localSkillID == 'Herblore' and Shared.contains(recipe.potionIDs, item.id)) then
				local level, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
				table.insert(lineArray, Icons._SkillReq(skill, level, false, (isAbyssal and "melvorItA:Abyssal" or nil)))
				break
			end
		end
	end

	-- Township trading
	for i, tsResource in ipairs(SkillData.Township.itemConversions.fromTownship) do
		local found = false
		for j, tradeDef in ipairs(tsResource.items) do
			if tradeDef.itemID == item.id then
				found = true
				local levelReq = nil
				if tradeDef.unlockRequirements ~= nil then
					for k, req in ipairs(tradeDef.unlockRequirements) do
						if req.type == 'SkillLevel' and req.skillID == 'melvorD:Township' then
							levelReq = req.level
							break
						end
					end
					if levelReq == nil then
						table.insert(lineArray, Icons.Icon({SkillData.Township.name, type='skill'}))
					else
						table.insert(lineArray, Icons._SkillReq(SkillData.Township.name, levelReq))
					end
				end
			end
			if found then
				break
			end
		end
		if found then
			break
		end
	end

	-- Archaeology sources
	-- Digsites
	for i, digsite in ipairs(SkillData.Archaeology.digSites) do
		local found = false
		for artefactType, artefactItems in pairs(digsite.artefacts) do
			for j, itemDef in ipairs(artefactItems) do
				if itemDef.itemID == item.id then
					table.insert(lineArray, Icons._SkillReq(SkillData.Archaeology.name, digsite.level))
					found = true
					break
				end
			end
			if found then
				break
			end
		end
		if found then
			break
		end
	end
	-- Museum rewards
	for i, museumReward in ipairs(SkillData.Archaeology.museumRewards) do
		if type(museumReward.items) == 'table' and Shared.contains(museumReward.items, item.id) then
			table.insert(lineArray, Icons.Icon('Museum'))
			break
		end
	end

	-- Cartography
	-- Paper
	for i, recipe in ipairs(SkillData.Cartography.paperRecipes) do
		if recipe.productId == item.id then
			table.insert(lineArray, Icons.Icon({SkillData.Cartography.name, type='skill'}))
			break
		end
	end
	-- POI discovery rewards
	for i, worldMap in ipairs(SkillData.Cartography.worldMaps) do
		local found = false
		for j, poi in ipairs(worldMap.pointsOfInterest) do
			if type(poi.discoveryRewards) == 'table' and type(poi.discoveryRewards.items) == 'table' then
				for k, itemDef in ipairs(poi.discoveryRewards.items) do
					if itemDef.id == item.id then
						-- Find level for POI hex
						local level = 1
						local poiHex = nil
						local skillID = SkillData.Cartography.skillID
						for m, hex in ipairs(worldMap.hexes) do
							if hex.coordinates.q == poi.coords.q and hex.coordinates.r == poi.coords.r then
								for n, req in ipairs(hex.requirements) do
									if req.type == 'SkillLevel' and req.skillID == skillID then
										level = req.level
										break
									end
								end
								break
							end
						end
						table.insert(lineArray, Icons._SkillReq(SkillData.Cartography.name, level))
						found = true
						break
					end
				end
				if found then
					break
				end
			end
		end
		if found then
			break
		end
	end
	-- Travel events
	for i, event in ipairs(SkillData.Cartography.travelEvents) do
		local found = false
		if type(event.rewards) == 'table' and type(event.rewards.items) == 'table' then
			for j, itemDef in ipairs(event.rewards.items) do
				if itemDef.id == item.id and itemDef.quantity > 0 then
					table.insert(lineArray, Icons.Icon({SkillData.Cartography.name, type='skill'}))
					found = true
					break
				end
			end
			if found then
				break
			end
		end
	end

	--AstrologyCheck
	for i, dustDrop in ipairs(SkillData.Astrology.baseRandomItemChances) do
		if dustDrop.itemID == item.id then
			table.insert(lineArray, Icons.Icon({SkillData.Astrology.name, type='skill'}))
		end
	end

	-- Woodcutting
	-- Raven Nest
	if item.id == SkillData.Woodcutting.ravenNestItemID then
		local levelReq = nil
		for i, tree in ipairs(SkillData.Woodcutting.trees) do
			if tree.canDropRavenNest and (levelReq == nil or tree.level < levelReq) then
				levelReq = tree.level
			end
		end
		table.insert(lineArray, Icons._SkillReq(SkillData.Woodcutting.name, levelReq))
	-- Bird Nest, Ash, and Mushroom
	elseif Shared.contains({
		SkillData.Woodcutting.nestItemID,
		SkillData.Woodcutting.ashItemID,
		SkillData.Woodcutting.mushroomItemID
		}, item.id) then
		table.insert(lineArray, Icons._SkillReq(SkillData.Woodcutting.name, 1))
	end

	-- Fishing
	-- Junk
	if Shared.contains(SkillData.Fishing.junkItemIDs, item.id) then
		table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Junk|Junk]]')
	elseif item.id == SkillData.Fishing.lostChestItem then
		table.insert(lineArray, Icons._SkillReq(SkillData.Fishing.name, 100))
	end
	-- Specials
	for i, specialItem in ipairs(SkillData.Fishing.specialItems) do
		if GameData.getEntityByProperty(specialItem.drops, 'itemID', item.id) ~= nil then
			table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Special|Special]]')
		end
	end

	-- Firemaking: Coal
	if Shared.contains({SkillData.Firemaking.coalItemID,
		SkillData.Firemaking.ashItemID,
		SkillData.Firemaking.charcoalItemID,
		SkillData.Firemaking.fireSpiritItemID,
		SkillData.Firemaking.diamondItemID
		}, item.id) then
		table.insert(lineArray, Icons._SkillReq(SkillData.Firemaking.name, 1))
	end

	-- Mining: Gems
	if (GameData.getEntityByProperty('randomGems', 'itemID', item.id) ~= nil or
		GameData.getEntityByProperty('randomSuperiorGems', 'itemID', item.id) ~= nil) then
		table.insert(lineArray, Icons.Icon({"Mining", type='skill', notext=true})..' [[Mining#Gems|Gem]]')
	elseif item.id == SkillData.Mining.runestoneItemID then
		-- From pure essence mining
		local recipe = GameData.getEntityByID(SkillData.Mining.rockData, 'melvorTotH:Pure_Essence')
		if recipe ~= nil then
			table.insert(lineArray, Icons._SkillReq(SkillData.Mining.name, recipe.level))
		end
	end

	-- General rare drops for non-combat skills
	-- Includes items like Circlet/Jewel of Rhaelyx, Mysterious stones, Signet ring half (a),
	-- relics (for Ancient Relics mode)
	local skillIconList, subText = {}, ''
	for i, skillDataAll in ipairs(GameData.rawData.skillData) do
		local skillData = skillDataAll.data
		local skillName, displaySkillName = skillData.name, false
		-- All general rare drops within the Magic are for Alt. Magic
		if skillDataAll.skillID == 'melvorD:Magic' then
			skillName, displaySkillName = 'Alt. Magic', true
		end
		if type(skillData.rareDrops) == 'table' then
			for j, rareDrop in ipairs(skillData.rareDrops) do
				local isAltItem = (rareDrop.altItemID ~= nil and rareDrop.altItemID == item.id)
				if isAltItem or rareDrop.itemID == item.id then
					if Shared.tableIsEmpty(skillIconList) then
						-- Initialize subText
						if isAltItem then
							local wornItem = Items.getItemByID(rareDrop.itemID)
							subText = ' while wearing ' .. Icons.Icon({wornItem.name, type='item'})
						elseif rareDrop.altItemID ~= nil then
							-- There exists an alt item, but we are not searching for it
							local altItem = Items.getItemByID(rareDrop.altItemID)
							subText = ' if not worn (Instead of ' .. Icons.Icon({altItem.name, type='item'}) .. ')'
						elseif rareDrop.itemID == 'melvorD:Mysterious_Stone' then
							local foundItem = Items.getItemByID('melvorD:Crown_of_Rhaelyx')
							subText = '<br/>after finding ' .. Icons.Icon({foundItem.name, type='item'})
						end
						if type(rareDrop.gamemodes) == 'table' then
							local gamemodeText = {}
							for k, gamemodeID in ipairs(rareDrop.gamemodes) do
								local gamemode = GameData.getEntityByID('gamemodes', gamemodeID)
								if gamemode ~= nil then
									table.insert(gamemodeText, gamemode.name)
								end
							end
							if not Shared.tableIsEmpty(gamemodeText) then
								subText = subText .. ' (' .. table.concat(gamemodeText, ', ') .. ' only)'
							end
						end
					end
					local skillText = Icons.Icon({skillName, type='skill', notext=true})
					if displaySkillName then
						skillText = skillText .. ' (' .. Icons.Icon({skillName, type='skill', noicon=true}) .. ')'
					end
					table.insert(skillIconList, skillText)
				end
			end
		end
	end
	if not Shared.tableIsEmpty(skillIconList) then
		table.insert(lineArray, 'Any action in: ' .. table.concat(skillIconList, ', ') .. subText)
		skillIconList, subText = {}, ''
	end

	-- Supplementary stuff on top of general rare drops
	if item.id == 'melvorD:Gold_Topaz_Ring' then
		table.insert(lineArray, 'Killing any monster if not worn (Instead of '..Icons.Icon({"Signet Ring Half (b)", type="item"})..')')
	elseif item.id == 'melvorD:Signet_Ring_Half_B' then
		table.insert(lineArray, 'Killing any monster while wearing '..Icons.Icon({'Gold Topaz Ring', type='item'}))
	elseif item.id == 'melvorTotH:Deadly_Toxins_Potion' then
		--Adding a special override for Deadly Toxins potions
		table.insert(lineArray, 'Brewing [[Lethal Toxins Potion]]s while wearing '..Icons.Icon({'Toxic Maker Gloves', type='item'}))
	end

	--Tokens are from the appropriate skill
	if item.modifiers ~= nil and item.modifiers.masteryToken ~= nil then
		for localSkillID, skillData in pairs(SkillData) do
			if skillData.masteryTokenID ~= nil and skillData.masteryTokenID == item.id then
				table.insert(lineArray, Icons._SkillReq(skillData.name, 1))
				break
			end
		end
	end

	-- Golbin Raid exclusive items
	if item.golbinRaidExclusive then
		table.insert(lineArray, Icons.Icon({'Golbin Raid', type='pet', img='Golden Golbin'}))
	end

	--Shop items (including special items like gloves that aren't otherwise listed)
	if not Shared.tableIsEmpty(Shop.getItemSourceArray(item.id)) then
		table.insert(lineArray, Icons.Icon({'Shop'}))
	end

	--Easter Eggs (manual list 'cause don't have a better way to do that)
	if Shared.contains(Items.EasterEggs, item.name) then
		table.insert(lineArray, '[[Easter Eggs]]')
	end
	-- Event exclusive items (also a manual list)
	if Shared.contains(Items.EventItems, item.name) then
		table.insert(lineArray, '[[Events]]')
	end

	-- Township Task reward
	for _, task in ipairs(SkillData.Township.tasks) do
		if task.rewards.items[1] ~= nil then -- Skip tasks with no items
			if GameData.getEntityByID(task.rewards.items, item.id) then
				table.insert(lineArray, Icons.Icon({'Tasks', type='township'}))
				break
			end
		end
	end

	local resultPart = {}
	if asList then
		table.insert(resultPart, '* '..table.concat(lineArray, "\r\n* "))
	else
		table.insert(resultPart, '<div style="max-width:180px;text-align:right">' .. table.concat(lineArray, "<br/>") .. '</div>')
	end
	if addCategories then table.insert(resultPart, table.concat(categoryArray, '')) end
	return table.concat(resultPart)
end

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

	return p._getItemSources(item, asList, addCategories)
end

function p._getItemLootSourceTable(item)
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"')
	table.insert(resultPart, '\r\n!Source!!Level!!Quantity!!colspan="2"|Chance')

	--Set up function for adding rows
	local buildRow = function(source, level, levelNum, minqty, qty, weight, totalWeight, expIcon)
		if minqty == nil then minqty = 1 end
		if expIcon == nil then expIcon = '' end
		if level == nil then level = 'N/A' end
		local rowPart = {}
		table.insert(rowPart, '\r\n|-')
		table.insert(rowPart, '\r\n|style="text-align: left;"|'..source)
		-- Retrieve numeric level value for sorting, or remove anything between [[]]
		local levelValue = ''
		if levelNum ~= nil then
			levelValue = tostring(levelNum)
		else
			levelValue = level:match('%[%[.-%]%]%s*(%w+)$') or ''
		end
		table.insert(rowPart, '\r\n|style="text-align: left;" data-sort-value="'..levelValue..'"|'..expIcon.. level)
		table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="'..qty..'"|'..Num.formatnum(minqty))
		if qty ~= minqty then table.insert(rowPart, ' - '..Num.formatnum(qty)) end
		local chance = weight / totalWeight * 100
		-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
		local fmt = (chance < 0.10 and '%.2g') or '%.2f'
		local chanceStr = string.format(fmt, chance)
		if weight >= totalWeight then
			-- Fraction would be 1/1, so only show the percentage
			chanceStr = '100'
			table.insert(rowPart, '\r\n|colspan="2" ')
		else
			local fraction = Num.fraction(weight, totalWeight)
			if Shared.contains(fraction, '%.') then
				--If fraction contains decimals, something screwy happened so just show only percentage
				--(happens sometimes with the rare thieving items)
				table.insert(rowPart, '\r\n|colspan="2" ')
			else
				table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="' .. chanceStr .. '"| ' .. Num.fraction(weight, totalWeight) .. '\r\n|')
			end
		end
		if weight == -1 then
			--Weight of -1 means this is a weird row that has a variable percentage
			table.insert(rowPart, 'style="text-align: right;" data-sort-value="0"|Varies (see Thieving page)')
		else
			table.insert(rowPart, 'style="text-align: right;" data-sort-value="'.. chanceStr .. '"|'..chanceStr..'%')
		end
		return table.concat(rowPart)
	end
	local dropRows = {}

	--Alright, time to go through a few ways to get the item
	--First up: Can we kill somebody and take theirs?
	for i, drop in ipairs(p._getItemMonsterSources(item)) do
		local monster = GameData.getEntityByID('monsters', drop.id)
		local iconName = monster.name
		if SourceOverrides[drop.id] ~= nil then
			iconName = SourceOverrides[drop.id]
		end

		if monster ~= nil then
			local monsterLevel = Monsters._getMonsterCombatLevel(monster)
			table.insert(dropRows, {
				source = Icons.Icon({iconName, type='monster'}), 
				level = Icons.Icon({'Combat', 'Monsters', notext=true}) .. ' Level ' .. Num.formatnum(monsterLevel),
				levelNum = monsterLevel,
				minqty = drop.minQty, 
				qty = drop.maxQty, 
				weight = drop.dropWt, 
				totalWeight = drop.totalWt, 
				expIcon = Icons.getExpansionIcon(drop.id)})
		end
	end
	
	--Patching in here because it uses the same format
	--Can we find this in an Archaeology digsite?
	for i, drop in ipairs(p._getItemArchSources(item)) do
		if drop.name ~= nil then
			table.insert(dropRows, {
				source = Icons.Icon({drop.name, type='poi'}), 
				level = Icons._SkillReq('Archaeology', drop.level) .. ' ('..drop.size..')',
				levelNum = drop.level,
				minqty = drop.minQty, 
				qty = drop.maxQty, 
				weight = drop.dropWt, 
				totalWeight = drop.totalWt, 
				expIcon = Icons.getExpansionIcon(drop.id)})
		end
	end

	-- Is the item dropped from any dungeon?
	local dungeonEntities = {
		['Dungeon'] = GameData.rawData.dungeons,
		['The Abyss'] = GameData.rawData.abyssDepths
	}
	for entity, dungeons in pairs(dungeonEntities) do
		local iconType = entity == 'Dungeon' and 'dungeon' or 'combatArea'
		for i, dungeon in ipairs(dungeons) do
			if (dungeon.oneTimeRewardID ~= nil and item.id == dungeon.oneTimeRewardID) or
				(type(dungeon.rewardItemIDs) == 'table' and Shared.contains(dungeon.rewardItemIDs, item.id)) then
					table.insert(dropRows, {
						source = Icons.Icon({dungeon.name, type=iconType}), 
						level = '[['..entity..']]',
						minqty = 1, 
						qty = 1, 
						weight = 1, 
						totalWeight = 1, 
						expIcon = Icons.getExpansionIcon(dungeon.id)})
			elseif dungeon.eventID ~= nil then
				-- Is the item dropped from a combat event (e.g. Impending Darkness event)?
				local event = GameData.getEntityByID('combatEvents', dungeon.eventID)
				if type(event) == 'table' and type(event.itemRewardIDs) == 'table' then
					for eventCycle, itemRewardID in ipairs(event.itemRewardIDs) do
						if item.id == itemRewardID then
							local sourceTxt = Icons.Icon({dungeon.name, type=iconType}) .. (eventCycle == Shared.tableCount(event.itemRewardIDs) and '' or ', Cycle ' .. eventCycle)
							table.insert(dropRows, {
								source = sourceTxt, 
								level = '[['..entity..']]',
								minqty = 1, 
								qty = 1, 
								weight = 1, 
								totalWeight = 1})
							break
						end
					end
				end
			end
		end
	end

	for i, stronghold in ipairs(GameData.rawData.strongholds) do
		for tier, tierData in pairs(stronghold.tiers) do
			if type(tierData.rewards) == 'table' and type(tierData.rewards.items) == 'table' then
				for i, reward in ipairs(tierData.rewards.items) do
					if reward.id == item.id then
						table.insert(dropRows, {
							source = Icons.Icon({stronghold.name, type='combatArea'}), 
							level = '[[Strongholds|'..tier..']]',
							minqty = 1, 
							qty = 1, 
							weight = tierData.rewards.chance, 
							totalWeight = 100, 
							expIcon = Icons.getExpansionIcon(stronghold.id)})
					end
				end
			end
		end
	end

	-- Can we find it in an openable item?
	for i, item2 in ipairs(GameData.rawData.items) do
		if item2.dropTable ~= nil then
			local minQty, maxQty, wt, totalWt = 1, 1, 0, 0
			for j, loot in ipairs(item2.dropTable) do
				totalWt = totalWt + loot.weight
				if loot.itemID == item.id then
					wt = loot.weight
					minQty = loot.minQuantity
					maxQty = loot.maxQuantity
				end
			end

			if wt > 0 then
				local sourceTxt = Icons.Icon({item2.name, type='item'})
				table.insert(dropRows, {
					source = sourceTxt, 
					level = '[[Chest]]',
					minqty = minQty, 
					qty = maxQty, 
					weight = wt, 
					totalWeight = totalWt, 
					expIcon = Icons.getExpansionIcon(item2.id)})
			end
		end
	end

	-- Can it be obtained from Thieving?
	local thiefItems = Skills.getThievingSourcesForItem(item.id)
	for i, thiefRow in ipairs(thiefItems) do
		local sourceTxt = ''
		if thiefRow.npc == 'all' then
			sourceTxt = 'Thieving Rare Drop'
		else
			sourceTxt = Icons.Icon({thiefRow.npc, type='thieving'})
		end
		local levelNum = thiefRow.abyssalLevel or thiefRow.level
		local isAbyssal = thiefRow.abyssalLevel ~= nil
		table.insert(dropRows, {
			source = sourceTxt,
			level = Icons._SkillReq("Thieving", levelNum, false, (isAbyssal and "melvorItA:Abyssal" or nil)),
			levelNum = levelNum,
			minqty = thiefRow.minQty, 
			qty = thiefRow.maxQty, 
			weight = thiefRow.wt, 
			totalWeight = thiefRow.totalWt, 
			expIcon = Icons.getExpansionIcon(thiefRow.npcID)})
	end

	-- Fishing: Junk & Specials
	if Shared.contains(SkillData.Fishing.junkItemIDs, item.id) then
		local fishSource = '[[Fishing#Junk|Junk]]'
		local fishType = Icons.Icon({'Fishing', type='skill'})
		local fishTotWeight = Shared.tableCount(SkillData.Fishing.junkItemIDs)
		table.insert(dropRows, {
			source = fishSource, 
			level = Icons._SkillReq("Fishing", 1),
			levelNum = 1,
			minqty = 1, 
			qty = 1, 
			weight = 1, 
			totalWeight = fishTotWeight})
	else
		local fishTotWeight, fishItem, realmID = {['melvorD:Melvor'] = 0, ['melvorItA:Abyssal'] = 0}, nil, nil
		for i, specialItem in ipairs(SkillData.Fishing.specialItems) do
			for f, drop in ipairs(specialItem.drops) do
				if drop.itemID == item.id then
					fishItem = drop
					realmID = specialItem.realmID
				end
				fishTotWeight[specialItem.realmID] = fishTotWeight[specialItem.realmID] + drop.weight
			end
		end
		if fishItem ~= nil then
			local fishSource = '[[Fishing#Special|Special]]'
			local fishType = Icons.Icon({SkillData.Fishing.name, type='skill'})
			table.insert(dropRows, {
				source = fishSource, 
				level = Icons._SkillReq("Fishing", 1, false, realmID),
				levelNum = 1,
				minqty = fishItem.minQuantity, 
				qty = fishItem.maxQuantity, 
				weight = fishItem.weight, 
				totalWeight = fishTotWeight[realmID]})
		end
	end

	-- Mining: Gems, and also Alt. Magic spells producing random gems
	if Shared.contains({'Gem', 'Superior Gem'}, item.type) then
		local gemKeys = { 'randomGems', 'randomSuperiorGems' }
		for i, gemKey in ipairs(gemKeys) do
			local thisGem, totalGemWeight = nil, 0
			for j, gem in ipairs(GameData.rawData[gemKey]) do
				totalGemWeight = totalGemWeight + gem.weight
				if gem.itemID == item.id then
					thisGem = gem
				end
			end
			if thisGem ~= nil then
				local expIcon = ''
				local sourceTxt
				local lv = nil
				if item.type == 'Superior Gem' then
					expIcon = Icons.TotH()
					sourceTxt = '[[Mining#Superior Gems|Superior Gem]]'
					-- Superior gems can only be found with Mining 100 or above
					lv = 100
				else
					sourceTxt = '[[Mining#Gems|Gem]]'
					-- Gems can only be found with any Mining level
					lv = 1
				end
				table.insert(dropRows, {
					source = sourceTxt, 
					level = Icons._SkillReq('Mining', lv),
					levelNum = lv,
					minqty = thisGem.minQuantity, 
					qty = thisGem.maxQuantity, 
					weight = thisGem.weight, 
					totalWeight = totalGemWeight, 
					expIcon = expIcon})
				
				-- Check for Alt. Magic spells also
				local producesKey = (gemKey == 'randomGems' and 'RandomGem') or 'RandomSuperiorGem'
				for j, spell in ipairs(Magic.getSpellsBySpellBook('altMagic')) do
					if spell.produces ~= nil and spell.produces == producesKey then
						table.insert(dropRows, {
							source = Icons.Icon({spell.name, type=Magic._getSpellIconType(spell)}), 
							level = Icons.Icon({'Alternative Magic', type='skill', img='Magic', notext=true}) .. ' Level ' .. spell.level,
							levelNum = spell.level,
							minqty = thisGem.minQuantity, 
							qty = thisGem.maxQuantity,
							weight = thisGem.weight, 
							totalWeight = totalGemWeight,
							expIcon = Icons.getExpansionIcon(spell.id)})
					end
				end
			end
		end
	end

	--Make sure to return nothing if there are no drop sources
	if Shared.tableIsEmpty(dropRows) then return '' end
	
	table.sort(dropRows, function(a, b)
							if a.weight / a.totalWeight == b.weight / b.totalWeight then
								if a.minqty + a.qty == b.minqty + b.qty then
									return (a.level == b.level and a.source < b.source) or a.level < b.level
								else
									return a.minqty + a.qty > b.minqty + b.qty
								end
							else
								return a.weight / a.totalWeight > b.weight / b.totalWeight
							end
						end)
	for i, data in ipairs(dropRows) do
		table.insert(resultPart, buildRow(data.source, data.level, data.levelNum, data.minqty, data.qty, data.weight, data.totalWeight, data.expIcon))
	end

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

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

	return p._getItemLootSourceTable(item)
end

function p._getItemUpgradeTable(item)
	local resultPart = {}
	local upgrade = GameData.getEntityByProperty('itemUpgrades', 'upgradedItemID', item.id)
	if upgrade ~= nil then
		local upgradeCost = Common.getCostString({
			["items"] = upgrade.itemCosts,
			["currencies"] = upgrade.currencyCosts
		})

		table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="2"|[[Upgrading Items|Item Upgrade]]')
		table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials\r\n|')
		table.insert(resultPart, upgradeCost)
		table.insert(resultPart, '\r\n|}')
	end
	return table.concat(resultPart)
end

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

	return p._getItemUpgradeTable(item)
end

function p._getSuperheatSmithRecipe(item)
	local smithRecipe = GameData.getEntityByProperty(SkillData.Smithing.recipes, 'productID', item.id)
	if smithRecipe ~= nil and smithRecipe.categoryID == 'melvorD:Bars' then
		return smithRecipe
	end
end

function p._getItemSuperheatTable(item)
	--Manually build the Superheat Item table
	-- Validate that the item can be superheated
	local smithRecipe = p._getSuperheatSmithRecipe(item)
	if smithRecipe == nil then
		return Shared.printError('The item "' .. item.name .. '" cannot be superheated')
	end

	local oreStringPart, coalString = {}, ''
	for i, mat in ipairs(smithRecipe.itemCosts) do
		local matItem = Items.getItemByID(mat.id)
		if mat.id == 'melvorD:Coal_Ore' then
			coalString = Icons.Icon({matItem.name, type='item', notext='true', qty=mat.quantity})
		else
			table.insert(oreStringPart, Icons.Icon({matItem.name, type='item', notext='true', qty=mat.quantity}))
		end
	end

	--Set up the header
	local superheatTable = {}
	table.insert(superheatTable, '{|class="wikitable"\r\n!colspan="2"|Spell')
	table.insert(superheatTable, '!!'..Icons.Icon({'Smithing', type='skill', notext='true'})..' Level')
	table.insert(superheatTable, '!!'..Icons.Icon({'Magic', type='skill', notext='true'})..' Level')
	table.insert(superheatTable, '!!'..Icons.Icon({'Magic', type='skill', notext='true'})..' XP')
	table.insert(superheatTable, '!!'..Icons.Icon({item.name, type='item', notext='true'})..' Bars')
	table.insert(superheatTable, '!!Ore!!Runes')

	--Loop through all the variants
	local spells = Magic.getSpellsProducingItem(item.id)
	for i, spell in ipairs(spells) do
		if spell.specialCost ~= nil and Shared.contains({ 'BarIngredientsWithCoal', 'BarIngredientsWithoutCoal' }, spell.specialCost.type) then
			local imgType = Magic._getSpellIconType(spell)
			table.insert(superheatTable, '\r\n|-\r\n|'..Icons.Icon({spell.name, type=imgType, notext=true, size=50}))
			table.insert(superheatTable, '||'..Icons.Icon({spell.name, type=imgType, noicon=true})..'||style="text-align:right;"|'..smithRecipe.level)
			table.insert(superheatTable, '||style="text-align:right;"|'..spell.level..'||style="text-align:right;"|'..spell.baseExperience)
			table.insert(superheatTable, '||style="text-align:right;"|'..spell.productionRatio)
			table.insert(superheatTable, '|| '..table.concat(oreStringPart, ', '))
			if spell.specialCost.type == 'BarIngredientsWithCoal' and coalString ~= '' then
				table.insert(superheatTable, (not Shared.tableIsEmpty(oreStringPart) and ', ' or '') .. coalString)
			end
			table.insert(superheatTable, '||style="text-align:center"| ' .. Magic._getSpellRunes(spell))
		end
	end

	--Add the table end and add the table to the result string
	table.insert(superheatTable, '\r\n|}')
	return table.concat(superheatTable)
end

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

	return p._getItemSuperheatTable(item)
end

function p._getTownshipTraderTable(item)
	for i, tsResource in ipairs(SkillData.Township.itemConversions.fromTownship) do
		for j, tradeDef in ipairs(tsResource.items) do
			if tradeDef.itemID == item.id then
				-- Item found, build table
				local res = GameData.getEntityByID(SkillData.Township.resources, tsResource.resourceID)
				local resName = (res ~= nil and res.name) or 'Unknown'
				local resQty = math.max(item.sellsFor, 2)

				local resultPart = {}
				table.insert(resultPart, '{| class="wikitable"\n|-')
				table.insert(resultPart, '\n!colspan="2"| ' .. Icons.Icon({'Township', 'Trader', type='skill'}))
				table.insert(resultPart, '\n|-\n!style="text-align:right;"| Cost')
				table.insert(resultPart, '\n| ' .. Icons.Icon({resName, qty=resQty, type='resource'}))
				table.insert(resultPart, '\n|-\n!style="text-align:right;| Requirements')
				table.insert(resultPart, '\n| ' .. Shop.getRequirementString(tradeDef.unlockRequirements))
				table.insert(resultPart, '\n|}')

				return table.concat(resultPart)
			end
		end
	end
	return ''
end

function p._getItemSourceTables(item)
	local resultPart = {}
	local shopTable = Shop._getItemShopTable(item)
	if shopTable ~= '' then
		table.insert(resultPart, '===Shop===\r\n'..shopTable)
	end

	local creationTable = p._getCreationTable(item)
	if creationTable ~= '' then
		if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\r\n') end
		table.insert(resultPart, '===Creation===\r\n'..creationTable)
	end

	local upgradeTable = p._getItemUpgradeTable(item)
	if upgradeTable ~= '' then
		if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\r\n') end
		if creationTable ~= '' then table.insert(resultPart, '===Creation===\r\n') end
		table.insert(resultPart, upgradeTable)
	end

	local townshipTable = p._getTownshipTraderTable(item)
	if townshipTable ~= '' then
		if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\n') end
		table.insert(resultPart, '===Township===\n' .. townshipTable)
	end

	if p._getSuperheatSmithRecipe(item) ~= nil then
		table.insert(resultPart, '\r\n==='..Icons.Icon({'Alt. Magic', type='skill'})..'===\r\n'..p._getItemSuperheatTable(item))
	end

	local lootTable = p._getItemLootSourceTable(item)
	if lootTable ~= '' then
		if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\r\n') end
		table.insert(resultPart, '===Loot===\r\n'..lootTable)
	end
	return table.concat(resultPart)
end

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

	return p._getItemSourceTables(item)
end

function p.getCombatPassiveSlotItems(frame)
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable"\r\n')
	table.insert(resultPart, '|-\r\n')
	table.insert(resultPart, '!colspan="2"|Item\r\n! Passive\r\n')

	local itemArray = Items.getItems(function(item) return item.validSlots ~= nil and (item.golbinRaidExclusive == nil or not item.golbinRaidExclusive) and Shared.contains(item.validSlots, 'Passive') end)

	for i, item in ipairs(itemArray) do
		local passiveDesc = item.customDescription or Modifiers.getModifiersText(item.modifiers, false, false, 10)
		table.insert(resultPart, '|-\r\n')
		table.insert(resultPart, '! '..Icons.Icon({item.name, type='item', notext='true'})..'\r\n! '..Icons.Icon({item.name, type='item', noicon=true})..'\r\n')
		table.insert(resultPart, '| '..passiveDesc..'\r\n')
	end

	table.insert(resultPart, '|}')

	return table.concat(resultPart)
end

function p._getItemMonsterSources(item)
	local resultArray = {}
	for i, monster in ipairs(GameData.rawData.monsters) do
		local chance = 0
		local weight = 0
		local minQty = 1
		local maxQty = 1
		if monster.bones ~= nil and monster.bones.itemID == item.id and Monsters._getMonsterBones(monster) ~= nil then
			-- Item is a bone, and is either a shard from God dungeons or dropped by a non-boss monster with a loot table
			maxQty = (monster.bones.quantity ~= nil and monster.bones.quantity) or 1
			minQty = maxQty
			chance = 1
			weight = 1
		elseif monster.barrierPercent ~= nil and 'melvorAoD:Barrier_Dust' == item.id and not Monsters._isDungeonOnlyMonster(monster) then
			-- Item is Barrier Dust and is not a dungeon exclusive monster
			maxQty = math.max(math.floor(Monsters._getMonsterStat(monster, 'Barrier') / 10 / 20), 1)
			minQty = maxQty
			chance = 1
		elseif monster.lootTable ~= nil and not Monsters._isDungeonOnlyMonster(monster) then
			-- If the monster has a loot table, check if the item we are looking for is in there
			-- Dungeon exclusive monsters don't count as they are either:
			--   - A monster before the boss, which doesn't drop anything except shards (checked above)
			--   - A boss monster, whose drops are accounted for in data from Areas instead
			for j, loot in ipairs(monster.lootTable) do
				weight = weight + loot.weight
				if loot.itemID == item.id then
					chance = loot.weight
					minQty = loot.minQuantity
					maxQty = loot.maxQuantity
				end
			end
			local lootChance = monster.lootChance ~= nil and (monster.bones == nil or monster.bones.itemID ~= item.id) and monster.lootChance or 100
			chance = chance * lootChance
			weight = weight * 100
			chance, weight = Num.fractionpair(chance, weight)
		end
		if chance > 0 then
			-- Item drops when the monster is killed
			table.insert(resultArray, {id = monster.id, dropWt = chance, totalWt = weight, minQty = minQty, maxQty = maxQty})
		end
	end
	return resultArray
end

function p.getItemMonsterSources(itemName)
	local item = Items.getItem(itemName)
	return p._getItemMonsterSources(item)
end

function p._getItemArchSources(item)
	local check = false
	local itemID = item.id
	
	local resultArray = {}
	
	for i, digSite in pairs(SkillData.Archaeology.digSites) do
		for sizeName, size in pairs(digSite.artefacts) do
			local found = nil
			local sizeWeight = 0
			for k, artefact in pairs(size) do
				sizeWeight = sizeWeight + artefact.weight
				if artefact.itemID == itemID then
					found = artefact
				end
			end
			
			if found ~= nil then
				local min = found.minQuantity
				local max = found.maxQuantity
				table.insert(resultArray, {
					id = digSite.id, 
					name = digSite.name, 
					level = digSite.level,
					size = sizeName, 
					minQty = min, 
					maxQty = max, 
					dropWt = found.weight, 
					totalWt = sizeWeight})
			end
		end
	end
	
	return resultArray
end

function p.getItemArchSources(itemName)
	local item = Items.getItem(itemName)
	return p._getItemArchSources(item)
end

--[[
-- Uncomment this block and execute 'p.test()' within the debug console
-- to test after making changes
function p.test()
	local checkItems = {
		"Circlet of Rhaelyx",
		"Jewel of Rhaelyx",
		"Signet Ring Half (a)",
		"Signet Ring Half (b)",
		"Gold Topaz Ring",
		"Astrology Lesser Relic",
		"Mysterious Stone",
		"Gold Bar",
		"Raw Shrimp",
		"Coal Ore",
		"Rune Platebody",
		"Arrow Shafts",
		"Yew Longbow",
		"Water Rune",
		"Steam Rune",
		"Controlled Heat Potion II",
		"Wolf",
		"Cyclops",
		"Leprechaun",
		"Redwood Logs",
		"Carrot Cake",
		"Carrot Cake (Perfect)",
		"Mantalyme Herb",
		"Carrot",
		"Topaz",
		"Rune Essence",
		"Sanguine Blade",
		"Ring of Power",
		"Infernal Claw",
		"Chapeau Noir",
		"Stardust",
		"Rope",
		"Ancient Ring of Mastery",
		"Mastery Token (Cooking)",
		"Gem Gloves",
		"Thief's Moneysack",
		"Golden Stardust",
		"Golden Star",
		"Slayer Deterer",
		"Paper",
		"Lemon",
		"Aranite Brush",
		"Barrier Dust",
		"Shadow Raven Nest",
		"Void Wisp"
	}
	local checkFuncs = {
		--p.getItemSourceTables,
		--p.getCreationTable,
		--p.getItemSources,
		--p.getItemLootSourceTable,
	}
	local errCount = 0
	for i, item in ipairs(checkItems) do
		local param = {args={item}}
		mw.log('==' .. item .. '==')
		for j, func in ipairs(checkFuncs) do
			local callSuccess, retVal = pcall(func, param)
			if not callSuccess then
				errCount = errCount + 1
				mw.log('Error with item "' .. item .. '": ' .. retVal)
			else
				mw.log(retVal)
			end
		end
	end
	if errCount == 0 then
		mw.log('Test successful')
	else
		mw.log('Test failed with ' .. errCount .. ' failures')
	end
end
--]]

return p