Module:Items/SourceTables: Difference between revisions

From Melvor Idle
(added a p.getItemMonsterSources for use in a table for Mazunki)
m (Replaced center class with style; Center 5th col in Item Loot source table)
 
(70 intermediate revisions by 8 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 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')
local Shop = require('Module:Shop')
local Shop = require('Module:Shop')
local Monsters = require('Module:Monsters')
local Monsters = require('Module:Monsters')
local GatheringSkills = require('Module:Skills/Gathering')
local Skills = require('Module:Skills')
local Num = require('Module:Number')


-- Implements overrides for sources which cannot be obtained from game data
local SourceOverrides = {
-- Currently only overrides for dungeon sources are implemented here
['melvorAoD:EarthGolem'] = 'Earth Golem (AoD)'
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
}
}
}


function p._getCreationTable(item)
local function doesRecipeHaveItemID(recipe, itemID)
local skill = ''
if recipe.productId == itemID then
local specialReq = nil
return true
local time = 0
elseif Shared.contains(recipe.primaryProducts, itemID) or Shared.contains(recipe.secondaryProducts, itemID) then
local maxTime = nil
return true
local lvl = 0
elseif type(recipe.products) == 'table' then
local xp = 0
return GameData.getEntityByProperty(recipe.products, 'itemID', itemID) ~= nil
local qty = nil
end
local req = nil
return false
end
 
function p._getCreationTableData(item, tableData)
if tableData == nil then tableData = {} end


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 item.smithingLevel ~= nil then
 
skill = 'Smithing'
local skillIDs = {
lvl = item.smithingLevel
['Gathering'] = {
xp = item.smithingXP
['Farming'] = { recipeKey = 'recipes' },
req = item.smithReq
['Woodcutting'] = { recipeKey = 'trees' },
qty = item.smithingQty
['Fishing'] = { recipeKey = 'fish' },
time = 2
['Firemaking'] = { recipeKey = 'logs' },
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
['Mining'] = { recipeKey = 'rockData' },
['Cartography'] = { recipeKey = 'paperRecipes' },
['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, reqs, isAbyssal, xp, costs, qty, source, time, maxTime, weight, totalWeight = nil, nil, false, nil, nil, nil, nil, nil, nil, nil, nil, nil
for i, recipe in ipairs(skillData[dataProp.recipeKey]) do
local hasProduct = doesRecipeHaveItemID(recipe, itemID)
if hasProduct then
lvl, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
xp = recipe.baseAbyssalExperience or recipe.baseExperience
qty = recipe.baseQuantity or 1
reqs = Icons._SkillReq(skill, lvl, false, (isAbyssal and 'melvorItA:Abyssal' or nil))
source = Icons.Icon({ skill, type='skill', class=(isAbyssal and 'abyss-icon' or nil) })
-- 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
-- Custom Chance, Qty, and Costs data
if localSkillID == 'Farming' then
costs = { recipe.seedCost }
local catData = GameData.getEntityByID(skillData.categories, recipe.categoryID)
qty = 5 * catData.harvestMultiplier
elseif localSkillID == 'Firemaking' then
local itemChanceData = GameData.getEntityByProperty(SkillData.Firemaking.primaryProducts, 'itemID', itemID)
if itemChanceData ~= nil then
weight = itemChanceData.chance
elseif itemID == 'melvorD:Generous_Fire_Spirit' then
weight = 0.1
end
 
if Shared.contains({ 'melvorD:Generous_Fire_Spirit', 'melvorD:Coal_Ore', 'melvorTotH:Charcoal' }, itemID) then
costs = 'Any ' .. Icons.Icon({ 'Firemaking', 'Melvor Logs', img='Melvor Logo', section='Melvor Logs' })
else
local costItem = Items.getItemByID(recipe.logID)
costs = Icons.Icon({ costItem.name, type='item', qty=1 })
end
 
if itemID == 'melvorF:Ash' then
qty = time
elseif itemID == 'melvorItA:Withered_Ash' or itemID == 'melvorItA:Eternal_Ash' then
qty = math.max(math.floor(recipe.abyssalLevel / 10), 1)
end
elseif localSkillID == 'Cartography' then
time = 5
 
local costItem = Items.getItemByID(recipe.costs.items[1].id)
costs = Icons.Icon({ costItem.name, type='item', qty=1 })
elseif localSkillID == 'Harvesting' then
local itemChanceData = nil
totalWeight = 0
 
for i, product in ipairs(recipe.products) do
totalWeight = totalWeight + (product.weight or 0)
 
if product.itemID == itemID then itemChanceData = product end
end
 
if itemChanceData ~= nil then
weight = itemChanceData.weight
reqs = reqs .. '<br>' .. itemChanceData.minIntensityPercent .. '% ' .. Icons.Icon({ recipe.name, type='vein', notext=true }) .. ' Intensity'
end
end
-- Special requirements
if recipe.totalMasteryRequired ~= nil then
reqs = reqs .. '<br>' .. Icons.Icon({ 'Mastery', notext=true }) .. ' ' .. Num.formatnum(recipe.totalMasteryRequired) .. ' total [[' .. skill .. ']] [[Mastery]]'
end
table.insert(tableData, {
['skill'] = skill,
['lvl'] = lvl,
['reqs'] = reqs,
['isAbyssal'] = isAbyssal,
['xp'] = xp,
['costs'] = costs,
['qty'] = qty,
['source'] = source,
['time'] = time,
['maxTime'] = maxTime,
['weight'] = weight,
['totalWeight'] = totalWeight
})
-- Most recipes have a single item source or the item source data
-- is nearly all the same. The following items have some uniqueness
if not Shared.contains({ 'melvorF:Ash', 'melvorItA:Withered_Ash', 'melvorAoD:Paper' }, itemID) then break end
end
end
end
end
if item.craftingLevel ~= nil then
 
skill = 'Crafting'
-- Artisan skills
lvl = item.craftingLevel
-- Allow follow a similar data structure
xp = item.craftingXP
for localSkillID, dataProp in pairs(skillIDs.Artisan) do
req = item.craftReq
local skillData = SkillData[localSkillID]
qty = item.craftQty
local skill = skillData.name
time = 3
local lvl, reqs, isAbyssal, xp, costs, qty, source, time, maxTime = nil, nil, false, nil, nil, nil, nil, nil, nil, nil
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, nil, nil, item.craftGPCost))
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
reqs = Icons._SkillReq(skill, lvl, false, (isAbyssal and 'melvorItA:Abyssal' or nil))
source = Icons.Icon({ skill, type='skill', class=(isAbyssal and 'abyss-icon' or nil) })
-- 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
reqs = reqs .. '<br>' .. 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', 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', qty=nonShardQty }))
end
end
costs = table.concat(shardCostArray, '<br>')
if not Shared.tableIsEmpty(otherCostArray) then
local costLen = Shared.tableCount(otherCostArray)
costs = costs .. '<br>' .. (costLen == 1 and '' or 'and one of the following:<br>') .. table.concat(otherCostArray, "<br>'''OR''' ")
end
reqs = reqs .. '<br>At least 1 ' .. Icons.Icon({ 'Summoning', item.name, img=item.name, type='mark', section='Summoning Marks' }) .. ' mark discovered'
table.insert(tableData, {
['skill'] = skill,
['lvl'] = lvl,
['reqs'] = reqs,
['isAbyssal'] = isAbyssal,
['xp'] = xp,
['costs'] = costs,
['qty'] = qty,
['source'] = source,
['time'] = time
})
-- 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
table.insert(tableData, {
['skill'] = skill,
['lvl'] = lvl,
['reqs'] = reqs,
['isAbyssal'] = isAbyssal,
['xp'] = xp,
['costs'] = Common.getCostString({ ["items"] = altCost.itemCosts, ["currencies"] = recipe.currencyCosts }),
['qty'] = qty * altCost.quantityMultiplier,
['source'] = source,
['time'] = time,
['maxTime'] = maxTime
})
end
-- Finally, normal recipes with a single set of item costs
elseif type(recipe.itemCosts) == 'table' and not Shared.tableIsEmpty(recipe.itemCosts) then
if localSkillID == 'Cooking' then
-- Cooking includes the required utility (fire, furnace, pot) as a special requirement
local cookingCatIcon = {
["melvorD:Fire"] = 'Normal Cooking Fire',
["melvorD:Furnace"] = 'Basic Furnace',
["melvorD:Pot"] = 'Basic Pot'
}
local categoryIconName, categoryName = cookingCatIcon[recipe.categoryID], nil
local recipeCategory = GameData.getEntityByID(SkillData.Cooking.categories, recipe.categoryID)
if recipeCategory ~= nil then
categoryName = recipeCategory.modifierName or recipeCategory.name
end
if categoryIconName ~= nil and categoryName ~= nil then
reqs = reqs .. '<br>' .. Icons.Icon({ 'Cooking', categoryName, section = 'Cooking Upgrades', img = categoryIconName, type = 'upgrade' })
end
end
 
table.insert(tableData, {
['skill'] = skill,
['lvl'] = lvl,
['reqs'] = reqs,
['isAbyssal'] = isAbyssal,
['xp'] = xp,
['costs'] = Common.getCostString({ ["items"] = recipe.itemCosts, ["currencies"] = recipe.currencyCosts }),
['qty'] = qty,
['source'] = source,
['time'] = time,
['maxTime'] = maxTime
})
end
end
end
end
end
if item.runecraftingLevel ~= nil then
 
skill = 'Runecrafting'
-- Alt. Magic, excludes spells which can produce a variety of items, such as Gems and Bars
lvl = item.runecraftingLevel
-- Bars are handled by getItemSuperheatTable()
xp = item.runecraftingXP
-- Gems are handled by _getItemLootSourceTable()
req = item.runecraftReq
for i, altSpell in ipairs(Magic.getSpellsBySpellBook('altMagic')) do
qty = item.runecraftQty
if altSpell.produces == itemID then
time = 2
table.insert(tableData, {
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
['skill'] = 'Alt Magic',
['lvl'] = altSpell.level,
['reqs'] = Icons.Icon({'Alt Magic', type='skill', notext=true}) .. ' Level ' .. altSpell.level,
['isAbyssal'] = false,
['xp'] = altSpell.baseExperience,
['costs'] = Magic._getAltSpellCostText(altSpell),
['qty'] = altSpell.productionRatio,
['source'] = Icons.Icon({ altSpell.name, type=Magic._getSpellIconType(altSpell) }),
['time'] = 2,
['runeCost'] = Magic._getSpellRunes(altSpell)
})
end
end
end
if item.fletchingLevel ~= nil then
 
skill = 'Fletching'
-- Add in Astrology creation items manually since each constellation has (mostly)
lvl = item.fletchingLevel
-- the same creation information so looping through them is not necessary
xp = item.fletchingXP
local stardustChanceData = GameData.getEntityByProperty(SkillData.Astrology.baseRandomItemChances, 'itemID', itemID)
req = item.fletchReq
if stardustChanceData ~= nil then
qty = item.fletchQty
local namespace, localID = Shared.getLocalID(stardustChanceData.itemID)
time = 2
local isAbyssal = namespace == 'melvorItA'
if item.name == 'Arrow Shafts' then
table.insert(tableData, {
--Arrow Shafts get special (weird) treatment
['skill'] = 'Astrology',
req = '1 of any [[Log]]'
['lvl'] = 1,
qty = '15 - 135'
['reqs'] = Icons._SkillReq('Astrology', 1, false, (isAbyssal and 'melvorItA:Abyssal' or nil)),
['isAbyssal'] = isAbyssal,
['qty'] = 1,
['xp'] = (isAbyssal and 1238 or 5), -- Use the XP value for the first constellation
['source'] = Icons.Icon({ 'Astrology', type='skill', class=(isAbyssal and 'abyss-icon' or nil) }),
['time'] = 3,
['weight'] = stardustChanceData.chance
})
end
 
-- Can we find this in an Archaeology digsite?
for i, drop in ipairs(p._getItemArchSources(item)) do
if drop.name ~= nil then
table.insert(tableData, {
['skill'] = 'Archaeology',
['lvl'] = drop.level,
['reqs'] = Icons._SkillReq('Archaeology', drop.level) .. ' ('..drop.size..')',
['isAbyssal'] = false,
['minqty'] = drop.minQty,
['qty'] = drop.maxQty,
['source'] = Icons.Icon({ drop.name, type='poi' }),
['time'] = 4,
['weight'] = drop.dropWt,
['totalWeight'] = drop.totalWt
--['expIcon'] = Icons.getExpansionIcon(drop.id)}),
})
end
end
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
end
end
if item.cookingLevel ~= nil and item.recipeRequirements ~= nil then
 
for i, reqSet in pairs(item.recipeRequirements) do
-- Mining: Gems, and also Alt. Magic spells producing random gems
skill = 'Cooking'
if Shared.contains({'Gem', 'Superior Gem', 'Abyssal Gem'}, item.type) then
lvl = item.cookingLevel
local gemKeys = { 'randomGems', 'randomSuperiorGems', 'randomAbyssalGems' }
xp = item.cookingXP
for i, gemKey in ipairs(gemKeys) do
req = reqSet
local thisGem, totalGemWeight = nil, 0
qty = item.cookingQty
for j, gem in ipairs(GameData.rawData[gemKey]) do
time = item.cookingInterval / 1000
totalGemWeight = totalGemWeight + gem.weight
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
if gem.itemID == item.id then
thisGem = gem
end
end
if thisGem ~= nil then
--local expIcon = ''
local sourceTxt, lvl, isAbyssal = nil, nil, false
 
if item.type == 'Abyssal Gem' then
sourceTxt = '[[Mining#Abyssal Gems|Abyssal Gem]]'
lvl = 1
isAbyssal = true
elseif 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
lvl = 100
else
sourceTxt = '[[Mining#Gems|Gem]]'
-- Gems can only be found with any Mining level
lvl = 1
end
table.insert(tableData, {
['skill'] = 'Mining',
['lvl'] = lvl,
['reqs'] = Icons._SkillReq('Mining', lvl, false, (isAbyssal and 'melvorItA:Abyssal' or nil)),
['isAbyssal'] = isAbyssal,
['minqty'] = thisGem.minQuantity,
['qty'] = thisGem.maxQuantity,
['source'] = sourceTxt,
['time'] = 3,
['weight'] = thisGem.weight,
['totalWeight'] = totalGemWeight,
--expIcon = expIcon
})
 
-- Check for Alt. Magic spells also
local producesKey = (gemKey == 'randomGems' and 'RandomGem') or (gemKey == 'randomSuperiorGems' and 'RandomSuperiorGem') or nil
if producesKey ~= nil then
for j, spell in ipairs(Magic.getSpellsBySpellBook('altMagic')) do
if spell.produces ~= nil and spell.produces == producesKey then
table.insert(tableData, {
['skill'] = 'Alt Magic',
['lvl'] = spell.level,
['reqs'] = Icons.Icon({'Alt Magic', type='skill', notext=true}) .. ' Level ' .. spell.level,
['minqty'] = thisGem.minQuantity,
['qty'] = thisGem.maxQuantity,
['source'] = Icons.Icon({ spell.name, type=Magic._getSpellIconType(spell) }),
['time'] = 2,
['weight'] = thisGem.weight,
['totalWeight'] = totalGemWeight,
--expIcon = Icons.getExpansionIcon(spell.id)
})
end
end
end
end
end
end
end
end
if item.herbloreReq ~= nil then
 
skill = 'Herblore'
return tableData
req = item.herbloreReq
end
--Currently using 'masteryID' as shorthand to find details, could be a better method
 
local potionID = item.masteryID[2]
function p.buildCreationTable(item, tableData)
local potionData = SkillData.Herblore.ItemData[potionID + 1]
if Shared.tableIsEmpty(tableData) then return '' end
lvl = potionData.level
 
xp = potionData.herbloreXP
table.sort(tableData, function(a, b) return (a.qty or 1) < (b.qty or 1) end)
time = 2
 
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
local showSource = false
local showRequirements = false
local showInputs = false
local showRunes = false
local showOutputs = false
local showXP = false
local showTime = false
local showChance = false
local colspan = -1 -- colspan only needs to be set when there are 3+ columns in the table
 
for i, data in ipairs(tableData) do
if not showSource and tableData[1].source ~= tableData[i].source then
showSource = true
colspan = colspan + 1
end
if not showRequirements and tableData[1].reqs ~= tableData[i].reqs then
showRequirements = true
colspan = colspan + 1
end
if not showInputs and tableData[1].costs ~= tableData[i].costs then
showInputs = true
colspan = colspan + 1
end
if not showRunes and  tableData[1].runeCost ~= tableData[i].runeCost then
showRunes = true
colspan = colspan + 1
end
if not showOutputs and (tableData[1].qty ~= tableData[i].qty or tableData[1].contents ~= tableData[i].contents) then
showOutputs = true
colspan = colspan + 1
end
if not showXP and tableData[1].xp ~= tableData[i].xp then
showXP = true
colspan = colspan + 1
end
if not showTime and tableData[1].time ~= tableData[i].time then
showTime = true
colspan = colspan + 1
end
if not showChance and tableData[1].weight ~= tableData[i].weight then
showChance = true
colspan = colspan + 2
end
end
end
if item.masteryID ~= nil and item.masteryID[1] == 4 then
skill = 'Mining'
lvl = SkillData.Mining.Rocks[item.masteryID[2] + 1].levelRequired
time = 3
xp = item.miningXP
--Rune Essence has double quantity, but that's a hard-coded thing in the game so it's hard-coded here
if item.name == 'Rune Essence' then qty = 2 else qty = 1 end


if item.name == 'Dragonite Ore' then
colspan = math.max(colspan, 1)
specialReq = Icons.Icon({"Mastery", notext='true'})..' 271 total [[Mining]] [[Mastery]]'
 
local function addCostsRow(row, data, span)
local costsRow = row:tag('td'):attr('colspan', span)
if type(data.costs) == 'table' then
for i, mat in ipairs(data.costs) do
if i > 1 then costsRow:tag('br') end
local matItem = Items.getItemByID(mat.id)
if matItem == nil then
costsRow:wikitext(mat.quantity .. 'x ?????')
else
costsRow:wikitext(Icons.Icon({ matItem.name, type='item', qty=mat.quantity }))
end
end
else
local costStr = data.costs:gsub(', ', '<br>')
costsRow:wikitext(costStr)
end
end
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, nil, specialReq))
end
end
if item.type == "Logs" then
 
--Well this feels like cheating, but for as long as logs are the first items by ID it works
local resultTable = mw.html.create('table')
local treeData = SkillData.Woodcutting.Trees[item.id + 1]
resultTable:addClass('wikitable stickyHeader')
skill = 'Woodcutting'
local tableHeader = resultTable:tag('tr'):addClass('headerRow-0')
lvl = treeData.level
 
time = treeData.interval / 1000
local makeSortable = Shared.contains({ showSource, showRequirements, showInputs, showRunes, showOutputs, showXP, showTime, showChance }, true)
xp = treeData.xp
if makeSortable then
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
resultTable:addClass('sortable')
end
end
if item.fishingLevel ~= nil then
 
skill = 'Fishing'
if showSource then tableHeader:tag('th'):wikitext('Source') end
lvl = item.fishingLevel
if showRequirements then tableHeader:tag('th'):wikitext('Requires') end
xp = item.fishingXP
if showInputs then tableHeader:tag('th'):wikitext('Costs') end
time = item.minFishingInterval/1000
if showRunes then tableHeader:tag('th'):wikitext('Runes') end
maxTime = item.maxFishingInterval/1000
if showOutputs then tableHeader:tag('th'):wikitext('Outputs') end
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime))
if showXP then tableHeader:tag('th'):wikitext('Exp') end
end
if showTime then tableHeader:tag('th'):wikitext('Time') end
--had to add cooking to the list of valid categories here to account for cherries/apples
if showChance then tableHeader:tag('th'):wikitext('Chance'):attr('colspan', 2) end
if item.category == 'Cooking' or item.type == "Harvest" or item.type == "Herb" or item.type == "Logs" or Shared.contains(item.name, '(Perfect)') then
 
--Harvest/Herb means farming
if makeSortable then
--Logs might mean farming or might not. Depends on the logs
-- Populate table data with any unique entries (Ex: Ash's Inputs, Outputs, Exp, Time)
for i, item2 in pairs(ItemData.Items) do
for i, data in ipairs(tableData) do
if item2.grownItemID == item.id then
local recipeRow = resultTable:tag('tr')
skill = 'Farming'
 
lvl = item2.farmingLevel
if showSource then recipeRow:tag('td'):wikitext(data.source):attr('data-sort-value', data.skill) end
xp = item2.farmingXP
 
time = item2.timeToGrow
if showRequirements then
if item.type == 'Logs' then
if data.reqs ~= nil then
qty = 35
recipeRow:tag('td'):wikitext(data.reqs):attr('data-sort-value', (data.lvl or 0))
else
else
qty = 15
recipeRow:tag('td'):wikitext('N/A'):addClass('table-na'):attr('data-sort-value', 0)
end
end
req = {{id = i - 1, qty = (item2.seedsRequired ~= nil and item2.seedsRequired or 1)}}
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
break
end
end


--If this is a perfect item, need to find the original
if showInputs then
if item2.perfectItem == item.id and item2.recipeRequirements ~= nil then
if data.costs ~= nil then
for j, reqSet in pairs(item2.recipeRequirements) do
addCostsRow(recipeRow, data, 1)
skill = 'Cooking'
else
lvl = item2.cookingLevel
recipeRow:tag('td'):wikitext('N/A'):addClass('table-na')
xp = item2.cookingXP
req = reqSet
qty = item2.cookingQty
time = item2.cookingInterval / 1000
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
end
end
end
end
end
 
end
if showRunes then
if item.summoningLevel ~= nil then
if data.runeCost ~= nil then
skill = 'Summoning'
recipeRow:tag('td'):wikitext(data.runeCost):css('text-align', 'center')
lvl = item.summoningLevel
else
--Summoning uses a formula to calculate XP for creation instead of referring to the item's XP data directly
recipeRow:tag('td'):wikitext('N/A'):addClass('table-na')
xp = (5 + 2 * math.floor(item.summoningLevel / 5))
local ShardCostArray = {}
for j, cost in Shared.skpairs(item.summoningReq[1]) do
if cost.id >= 0 then
local item = Items.getItemByID(cost.id)
if item.type == 'Shard' then
table.insert(ShardCostArray, Icons.Icon({item.name, type='item', notext=true, qty=cost.qty}))
end
end
end
end
end
 
req = table.concat(ShardCostArray, ', ')
if showOutputs then
req = req..'<br/>\r\nand one of the following<br/>\r\n'
local outputData = recipeRow:tag('td'):attr('data-sort-value', (data.qty or 1))
local OtherCostArray = {}
if data.contents ~= nil then
local recipeGPCost = SkillData.Summoning.Settings.recipeGPCost
outputData:wikitext(data.contents)
for j, altCost in Shared.skpairs(item.summoningReq) do
if data.center then outputData:css('text-align', 'center') end
local nonShardArray = {}
elseif data.qty ~= nil then
for k, cost in Shared.skpairs(altCost) do
if data.minqty ~= nil and data.minqty ~= data.qty then
if cost.id >= 0 then
outputRow:wikitext((data.minqty ~= nil and (Num.formatnum(data.minqty) .. ' - ') or ''))
local item = Items.getItemByID(cost.id)
if item.type ~= 'Shard' then
local sellPrice = math.max(item.sellsFor, 20)
table.insert(nonShardArray, Icons.Icon({item.name, type='item', notext=true, qty=math.max(1, math.floor(recipeGPCost / sellPrice))}))
end
end
outputData:wikitext(Icons.Icon({ item.name, type='item', notext=true, qty=(data.qty or 1) })):css('text-align', 'center'):attr('data-sort-value', (data.qty or 1))
else
outputData:wikitext('N/A'):addClass('table-na')
end
end
if showXP then
if data.skill ~= nil and data.xp ~= nil then
local iconClass = (data.isAbyssal and 'abyss-icon' or nil)
local xpText = (data.isAbyssal and ' AXP' or ' XP')
recipeRow:tag('td'):attr('data-sort-value', data.xp)
:wikitext(Icons.Icon({ data.skill, notext=true, type='skill', class=iconClass }))
:wikitext(' ' .. Num.formatnum(data.xp) .. xpText)
else
recipeRow:tag('td'):wikitext('N/A'):addClass('table-na'):attr('data-sort-value', 0)
end
end
if showTime then
if data.time ~= nil then
recipeRow:tag('td'):wikitext(Shared.timeString(data.time, true)):css('text-align', 'center'):attr('data-sort-value', data.time)
else
else
if cost.id == -4 then
recipeRow:tag('td'):wikitext('N/A'):addClass('table-na')
table.insert(nonShardArray, Icons.GP(recipeGPCost))
end
elseif cost.id == -5 then
end
table.insert(nonShardArray, Icons.SC(recipeGPCost))
 
end
if showChance then
if data.weight ~= nil then
-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
local chance = data.weight / (data.totalWeight or 100) * 100
local fmt = (chance < 0.10 and '%.2g') or '%.2f'
local percent = string.format(fmt, chance)
 
recipeRow:tag('td'):wikitext(Num.fraction(data.weight, (data.totalWeight or 100))):css('text-align', 'center'):attr('data-sort-value', percent)
recipeRow:tag('td'):wikitext(percent .. '%'):css('text-align', 'center')
else
recipeRow:tag('td'):wikitext('100%'):css('text-align', 'center'):attr('colspan', 2):attr('data-sort-value', 100)
end
end
end
end
table.insert(OtherCostArray, table.concat(nonShardArray, ', '))
end
end
req = req..table.concat(OtherCostArray, "<br/>'''OR''' ")
qty = item.summoningQty
time = 5
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
end
end
--A couple special exceptions for Alt Magic
 
--Not Gems or Bars though since those have their own separate thing
-- Add all non-unique data below the table data (Ex: Ash's Source, Requires, Chance)
if item.name == 'Rune Essence' then
if not showSource and tableData[1].source ~= nil then
table.insert(tables, p.buildAltMagicTable('Just Learning'))
resultTable:tag('tr')
elseif item.name == 'Bones' then
:tag('th'):wikitext('Source'):css('text-align', 'right')
table.insert(tables, p.buildAltMagicTable('Bone Offering'))
:tag('td'):attr('colspan', colspan):wikitext(tableData[1].source)
elseif item.name == 'Holy Dust' then
table.insert(tables, p.buildAltMagicTable('Blessed Offering'))
end
end


if Shared.tableCount(tables) == 0 then
if not showRequirements and tableData[1].reqs ~= nil then
return ""
local reqRow = resultTable:tag('tr')
else
:tag('th'):wikitext('Requires'):css('text-align', 'right')
return table.concat(tables, '\r\n')
:tag('td'):wikitext(tableData[1].reqs):attr('colspan', colspan)
end
end
end


function p.buildAltMagicTable(spellName)
if not showInputs and tableData[1].costs ~= nil then
local spell = Magic.getSpell(spellName, 'AltMagic')
local costRow = resultTable:tag('tr')
local resultPart = {}
:tag('th'):wikitext('Costs'):css('text-align', 'right')
table.insert(resultPart, '{|class="wikitable"\r\n|-')
 
table.insert(resultPart, '\r\n!colspan="2"|'..Icons.Icon({spell.name, type='spell'}))
addCostsRow(costRow, tableData[1], colspan)
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Requirements')
table.insert(resultPart, '\r\n|'..Icons._SkillReq('Magic', spell.level))
-- 1 means select any item. 0 would mean Superheat, but that's handled elsewhere
-- -1 means no item is needed, so hide this section
if spell.selectItem == 1 then
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials')
table.insert(resultPart, '\r\n|1 of any item')
end
end
--Add runes
 
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Runes\r\n|')
if not showRunes and type(tableData[1].runeCost) == 'string' then
for i, req in pairs(spell.runesRequired) do
resultTable:tag('tr')
local rune = Items.getItemByID(req.id)
:tag('th'):wikitext('Runes'):css('text-align', 'right')
if i > 1 then table.insert(resultPart, ', ') end
:tag('td'):wikitext(tableData[1].runeCost):css('text-align', 'center')
table.insert(resultPart, Icons.Icon({rune.name, type='item', notext=true, qty=req.qty}))
end
end
if spell.runesRequiredAlt ~= nil and Shared.tableCount(spell.runesRequired) ~= Shared.tableCount(spell.runesRequiredAlt) then
 
table.insert(resultPart, "<br/>'''OR'''<br/>")
if not showOutputs and (tableData[1].qty ~= nil or tableData[1].contents ~= nil) then
for i, req in pairs(spell.runesRequiredAlt) do
local outputRow = resultTable:tag('tr')
local rune = Items.getItemByID(req.id)
:tag('th'):wikitext('Outputs'):css('text-align', 'right')
if i > 1 then table.insert(resultPart, ', ') end
 
table.insert(resultPart, Icons.Icon({rune.name, type='item', notext=true, qty=req.qty}))
if tableData[1].contents ~= nil then
outputRow:tag('td'):wikitext(tableData[1].contents)
else
local outputData = outputRow:tag('td'):attr('colspan', colspan)
if tableData[1].minqty ~= nil and tableData[1].minqty ~= tableData[1].qty then
outputData:wikitext((tableData[1].minqty ~= nil and (Num.formatnum(tableData[1].minqty) .. ' - ') or ''))
end
outputData:wikitext(Icons.Icon({ item.name, type='item', qty=(tableData[1].qty or 1) }))
end
end
end
end


--Now just need the output quantity, xp, and casting time (which is always 2)
if not showXP and tableData[1].xp ~= nil then
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Quantity\r\n|'..spell.convertToQty)
local xpText = (tableData[1].isAbyssal and ' AXP' or ' XP')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base XP\r\n|'..spell.magicXP)
resultTable:tag('tr')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Cast Time\r\n|2s')
:tag('th'):wikitext('Base Exp'):css('text-align', 'right')
table.insert(resultPart, '\r\n|}')
:tag('td'):attr('colspan', colspan):wikitext(Num.formatnum(tableData[1].xp) .. xpText)
return table.concat(resultPart)
end
end
 
if not showTime and tableData[1].time ~= nil then
resultTable:tag('tr')
local timeHeader = resultTable:tag('th'):wikitext('Base Time'):css('text-align', 'right')
 
local timeData = timeHeader:tag('td'):attr('colspan', colspan)
:wikitext(Shared.timeString(tableData[1].time, true))


function p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime, specialReq, gpCost)
if tableData[1].maxTime ~= nil and tableData[1].maxTime > tableData[1].time then
if qty == nil then qty = 1 end
timeData:wikitext(' - ' .. Shared.timeString(tableData[1].maxTime, true))
local resultPart = {}
end
table.insert(resultPart, '{|class="wikitable"')
if req ~= nil then
table.insert(resultPart, '\r\n!colspan="2"|Item Creation')
else
table.insert(resultPart, '\r\n!colspan="2"|Item Production')
end
end
table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Requirements')
table.insert(resultPart, '\r\n|'..Icons._SkillReq(skill, lvl))
if specialReq ~= nil then table.insert(resultPart, '<br/>'..specialReq) end


if req ~= nil then
if not showChance and tableData[1].weight ~= nil then
table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Materials\r\n|')
-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
if type(req) == 'table' then
local chance = tableData[1].weight / (tableData[1].totalWeight or 100) * 100
for i, mat in pairs(req) do
local fmt = (chance < 0.10 and '%.2g') or '%.2f'
if i > 1 then table.insert(resultPart, '<br/>') end
local percent = string.format(fmt, chance)
local matItem = Items.getItemByID(mat.id)
local chanceData = resultTable:tag('tr')
if matItem == nil then
:tag('th'):wikitext('Base Chance'):css('text-align', 'right')
table.insert(resultPart, mat.qty..'x ?????')
:tag('td'):attr('colspan', colspan)
else
:wikitext(Num.fraction(tableData[1].weight, (tableData[1].totalWeight or 100)) .. ' (' .. percent .. '%)')
table.insert(resultPart, Icons.Icon({matItem.name, type='item', qty=mat.qty}))
end
end
if gpCost ~= nil and gpCost > 0 then
table.insert(resultPart, '<br/>')
table.insert(resultPart, Icons.GP(gpCost))
end
else
table.insert(resultPart, req)
end
end
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|'..Shared.formatnum(xp)..' XP')
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')
if maxTime ~= nil then table.insert(resultPart, ' - '..Shared.formatnum(Shared.round(maxTime, 2, 0))..'s') end
table.insert(resultPart, '\r\n|}')


return table.concat(resultPart)
return tostring(resultTable)
end
end


Line 309: Line 659:
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._getCreationTable(item)
return p.buildCreationTable(item, p._getCreationTableData(item))
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 333: Line 687:
--  - 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 341: Line 695:
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
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='combatArea', 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='combatArea', 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 Fire/Infernal Cape and first two 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, originals (for perfect items), and growing
local lootPart = {}
local lootStr = ''
for i, item2 in ipairs(GameData.rawData.items) do
local upgradeStr = ''
local cookStr = ''
local growStr = ''
local count1 = 0
local count2 = 0
for i, item2 in pairs(ItemData.Items) do
if item2.dropTable ~= nil then
if item2.dropTable ~= nil then
for j, loot in pairs(item2.dropTable) do
for j, loot in ipairs(item2.dropTable) do
if loot[1] == item.id then
if loot.itemID == item.id then
count1 = count1 + 1
table.insert(lootPart, Icons.Icon({item2.name, type='item', notext=true}))
if string.len(lootStr) > 0 then
break
lootStr = lootStr..','
--if count1 % 3 == 1 and count1 > 1 then lootStr = lootStr..'<br/>' end
lootStr = lootStr..Icons.Icon({item2.name, type="item", notext="true"})
else
lootStr = lootStr..'Opening: '..Icons.Icon({item2.name, type="item", notext="true"})
end
end
end
end
end
end
end
if item2.trimmedItemID == item.id then
end
count2 = count2 + 1
 
if string.len(upgradeStr) > 0 then
if not Shared.tableIsEmpty(lootPart) then
upgradeStr = upgradeStr..','
table.insert(lineArray, 'Opening: ' .. table.concat(lootPart, sep))
--if count2 % 3 == 1 and count2 > 1 then upgradeStr = upgradeStr..'<br/>' end
end
upgradeStr = upgradeStr..Icons.Icon({item2.name, type="item", notext="true"})
 
else
-- Is the item a result of upgrading/downgrading another item?
table.insert(categoryArray, '[[Category:Upgraded Items]]')
local upgradePart = { up = {}, down = {} }
upgradeStr = upgradeStr..'Upgrading: '..Icons.Icon({item2.name, type="item", notext="true"})
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 item2.grownItemID == item.id then
end
if string.len(growStr) > 0 then
 
growStr = growStr..','..Icons.Icon({item2.name, type="item", notext="true"})
local upgradeCat = false
else
for catName, parts in pairs(upgradePart) do
table.insert(categoryArray, '[[Category:Harvestable Items]]')
if not Shared.tableIsEmpty(parts) then
growStr = growStr..'Growing: '..Icons.Icon({item2.name, type="item", notext="true"})
if not upgradeCat then
table.insert(categoryArray, '[[Category:Upgraded Items]]')
upgradeCat = true
end
end
end
local typeText = (catName == 'up' and 'Upgrading') or 'Downgrading'
if item2.perfectItem == item.id and item2.cookingLevel ~= nil then
table.insert(lineArray, typeText .. ': ' .. table.concat(parts, sep))
table.insert(lineArray, Icons._SkillReq('Cooking', item2.cookingLevel))
end
end
end
end
if string.len(lootStr) > 0 then table.insert(lineArray, lootStr) end
if string.len(upgradeStr) > 0 then table.insert(lineArray, upgradeStr) end
if string.len(cookStr) > 0 then table.insert(lineArray, cookStr) end
if string.len(growStr) > 0 then table.insert(lineArray, growStr) end


--Next: Can we take it from somebody else -without- killing them?
--Next: Can we take it from somebody else -without- killing them?
local thiefItems = GatheringSkills.getThievingSourcesForItem(item.id)
local thiefItems = Skills.getThievingSourcesForItem(item.id)
local thiefStr = ''
if type(thiefItems) == 'table' then
if Shared.tableCount(thiefItems) > 0 then
local includedNPCs = {}
thiefStr = 'Pickpocketing: '
local thiefPart = {}
for i, thiefRow in pairs(thiefItems) do
for i, thiefRow in ipairs(thiefItems) do
if thiefRow.npc == 'all' then
if thiefRow.npc == 'all' then
--if 'any' 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'
thiefStr = Icons._SkillReq('Thieving', 1)
table.insert(lineArray, Icons._SkillReq('Thieving', 1))
else
elseif not Shared.contains(includedNPCs, thiefRow.npc) then
if i > 1 then thiefStr = thiefStr..', ' end
table.insert(thiefPart, Icons.Icon({thiefRow.npc, type='thieving', notext=true}))
thiefStr = thiefStr..Icons.Icon({thiefRow.npc, type='thieving', notext='true'})
table.insert(includedNPCs, thiefRow.npc)
end
end
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
end
if string.len(thiefStr) > 0 then table.insert(lineArray, thiefStr) end


--If all else fails, I guess we should check if we can make it ourselves
--Check if we can make it ourselves
--AstrologyCheck
local skillIDs = {
--(Just a brute force for now because only two items and I'm lazy)
['Gathering'] = {
if item.name == 'Stardust' or item.name == 'Golden Stardust' then
['Woodcutting'] = { recipeKey = 'trees' },
table.insert(lineArray, Icons.Icon({"Astrology", type="skill"}))
['Fishing'] = { recipeKey = 'fish' },
['Firemaking'] = { recipeKey = 'logs' },
['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


--SmithCheck:
-- Artisan skills
if item.smithingLevel ~= nil then
for localSkillID, dataProp in pairs(skillIDs.Artisan) do
table.insert(lineArray, Icons._SkillReq("Smithing", item.smithingLevel))
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
end


--CraftCheck:
-- Township trading
if item.craftingLevel ~= nil then
for i, tsResource in ipairs(SkillData.Township.itemConversions.fromTownship) do
table.insert(lineArray, Icons._SkillReq("Crafting", item.craftingLevel))
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
end


--FletchCheck:
-- Archaeology sources
if item.fletchingLevel ~= nil then
-- Digsites
table.insert(lineArray, Icons._SkillReq("Fletching", item.fletchingLevel))
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
end


--RunecraftCheck:
-- Cartography
if item.runecraftingLevel ~= nil then
-- Paper
table.insert(lineArray, Icons._SkillReq("Runecrafting", item.runecraftingLevel))
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
end


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


--MineCheck:
-- Woodcutting
if item.masteryID ~= nil and item.masteryID[1] == 4 then
-- Raven Nest
table.insert(lineArray, Icons._SkillReq("Mining", SkillData.Mining.Rocks[item.masteryID[2] + 1].levelRequired))
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
end


--FishCheck:
-- Fishing
if (item.category == "Fishing" and (item.type == "Junk" or item.type == "Special")) then
-- Junk
table.insert(lineArray, Icons.Icon({"Fishing", type='skill', notext=true})..' [[Fishing#'..item.type..'|'..item.type..']]')
if Shared.contains(SkillData.Fishing.junkItemIDs, item.id) then
elseif item.fishingLevel ~= nil then
table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Junk|Junk]]')
table.insert(lineArray, Icons._SkillReq("Fishing", item.fishingLevel))
elseif item.id == SkillData.Fishing.lostChestItem then
table.insert(lineArray, Icons._SkillReq(SkillData.Fishing.name, 100))
end
end
 
-- Specials
--HerbCheck:
for i, specialItem in ipairs(SkillData.Fishing.specialItems) do
if item.masteryID ~= nil and item.masteryID[1] == 19 then
if GameData.getEntityByProperty(specialItem.drops, 'itemID', item.id) ~= nil then
local potionData = SkillData.Herblore.ItemData[item.masteryID[2] + 1].level
table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Special|Special]]')
table.insert(lineArray, Icons._SkillReq("Herblore", potionData))
end
end
end


--WoodcuttingCheck:
-- Firemaking: Coal
if item.type == 'Logs' then
if Shared.contains({SkillData.Firemaking.coalItemID,
local treeData = SkillData.Woodcutting.Trees[item.id + 1]
SkillData.Firemaking.ashItemID,
local lvl = treeData.level
SkillData.Firemaking.charcoalItemID,
table.insert(lineArray, Icons._SkillReq("Woodcutting", lvl))
SkillData.Firemaking.fireSpiritItemID,
SkillData.Firemaking.diamondItemID
}, item.id) then
table.insert(lineArray, Icons._SkillReq(SkillData.Firemaking.name, 1))
end
end


--SummoningCheck:
-- Mining: Gems
if item.summoningLevel ~= nil then
if (GameData.getEntityByProperty('randomGems', 'itemID', item.id) ~= nil or
table.insert(lineArray, Icons._SkillReq("Summoning", item.summoningLevel))
GameData.getEntityByProperty('randomSuperiorGems', 'itemID', item.id) ~= nil or
GameData.getEntityByProperty('randomAbyssalGems', '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
end


--Finally there are some weird exceptions:
-- General rare drops for non-combat skills
--Coal can be acquired via firemaking
-- Includes items like Circlet/Jewel of Rhaelyx, Mysterious stones, Signet ring half (a),
if item.name == "Coal Ore" then
-- relics (for Ancient Relics mode)
table.insert(lineArray, Icons._SkillReq("Firemaking", 1))
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
end
 
if not Shared.tableIsEmpty(skillIconList) then
--Gems can be acquired from mining, fishing, and alt. magic
table.insert(lineArray, 'Any action in: ' .. table.concat(skillIconList, ', ') .. subText)
if item.type == 'Gem' and item.name ~= 'Jadestone' then
skillIconList, subText = {}, ''
table.insert(lineArray, Icons.Icon({"Fishing", type='skill', notext=true})..' [[Fishing#Special|Special]]')
table.insert(lineArray, Icons.Icon({"Mining", type='skill', notext=true})..' [[Mining#Gems|Gem]]')
table.insert(lineArray, Icons.Icon({"Alt. Magic", type='skill'}))
end
end


--Bars and some other stuff can also be acquired via Alt. Magic
-- Supplementary stuff on top of general rare drops
if item.type == 'Bar' or Shared.contains(Items.AltMagicProducts, item.name) then
if item.id == 'melvorD:Gold_Topaz_Ring' then
table.insert(lineArray, Icons.Icon({"Alt. Magic", type='skill'}))
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


--Rhaelyx pieces are also special
--Tokens are from the appropriate skill
if Shared.contains({'Circlet of Rhaelyx', 'Jewel of Rhaelyx', 'Mysterious Stone'}, item.name) then
if item.modifiers ~= nil and item.modifiers.masteryToken ~= nil then
local rhaSkills = {
for localSkillID, skillData in pairs(SkillData) do
Circlet = {'Woodcutting', 'Fishing', 'Mining', 'Thieving', 'Farming', 'Agility', 'Astrology'},
if skillData.masteryTokenID ~= nil and skillData.masteryTokenID == item.id then
Jewel = {'Firemaking', 'Cooking', 'Smithing', 'Fletching', 'Crafting', 'Runecrafting', 'Herblore', 'Summoning'}
table.insert(lineArray, Icons._SkillReq(skillData.name, 1))
}
break
local rhaSkList = {}
if item.name == 'Circlet of Rhaelyx' then
rhaSkList = rhaSkills.Circlet
elseif item.name == 'Jewel of Rhaelyx' then
rhaSkList = rhaSkills.Jewel
elseif item.name == 'Mysterious Stone' then
rhaSkList = rhaSkills.Jewel
for i, v in ipairs(rhaSkills.Circlet) do
table.insert(rhaSkList, v)
end
end
end
end
local rhaStrPart = {}
for i, skillName in ipairs(rhaSkList) do
table.insert(rhaStrPart, Icons.Icon({skillName, type='skill', notext=true}))
end
local rhaStr = 'Any action in: ' .. table.concat(rhaStrPart, ', ')
if item.name == 'Mysterious Stone' then rhaStr = rhaStr .. '<br/>after finding ' .. Icons.Icon({'Crown of Rhaelyx', type='item'}) end
table.insert(lineArray, rhaStr)
end
end


--Tokens are from the appropriate skill
-- Golbin Raid exclusive items
if item.isToken and item.skill ~= nil then
if item.golbinRaidExclusive then
table.insert(lineArray, Icons._SkillReq(Constants.getSkillName(item.skill), 1))
table.insert(lineArray, Icons.Icon({'Golbin Raid', type='pet', img='Golden Golbin'}))
end
end


--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 567: Line 1,152:
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 582: Line 1,166:
table.insert(resultPart, '* '..table.concat(lineArray, "\r\n* "))
table.insert(resultPart, '* '..table.concat(lineArray, "\r\n* "))
else
else
table.insert(resultPart, '<div style="max-width:180px;text-align:right">' .. table.concat(lineArray, "<br/>") .. '</div>')
table.insert(resultPart, '<div style="max-width:180px;text-align:right">' .. table.concat(lineArray, "<br>") .. '</div>')
end
end
if addCategories then table.insert(resultPart, table.concat(categoryArray, '')) end
if addCategories then table.insert(resultPart, table.concat(categoryArray, '')) end
Line 598: Line 1,182:
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 606: Line 1,190:
function p._getItemLootSourceTable(item)
function p._getItemLootSourceTable(item)
local resultPart = {}
local resultPart = {}
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
table.insert(resultPart, '{| class="wikitable sortable stickyHeader col-3-center col-4-center col-5-center"')
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!!Qty!!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..'"|'..minqty)
if levelNum ~= nil then
if qty ~= minqty then table.insert(rowPart, ' - '..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|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 635: Line 1,227:
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|data-sort-value="' .. chanceStr .. '"| ' .. Num.fraction(weight, totalWeight) .. '\r\n|')
end
end
end
end
if weight == -1 then
if weight == -1 then
--Weight of -1 means this is a weird row that has a variable percentage
--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)')
table.insert(rowPart, '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, 'data-sort-value="'.. chanceStr .. '"|'..chanceStr..'%')
end
end
return table.concat(rowPart)
return table.concat(rowPart)
Line 650: Line 1,242:
--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
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
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='combatArea'}),
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='combatArea'}) .. (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 pairs(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 719: Line 1,336:
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 = GatheringSkills.getThievingSourcesForItem(item.id)
for i, thiefRow in ipairs(thiefItems) do
for i, thiefRow in pairs(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
end
local isAbyssal = thiefRow.abyssalLevel ~= nil
 
table.insert(dropRows, {
--Bonus overtime: Special Fishing table & mining gem table. Also Rags to Riches
source = sourceTxt,
--Jadestone is special and doesn't count
level = Icons._SkillReq("Thieving", levelNum, false, (isAbyssal and "melvorItA:Abyssal" or nil)),
if item.type == 'Gem' and item.name ~= 'Jadestone' then
levelNum = levelNum,
local mineType = Icons.Icon({'Mining', type='skill'})
minqty = thiefRow.minQty,  
local thisGemChance = Items.GemTable[item.name].chance
qty = thiefRow.maxQty,  
local totalGemChance = 0
weight = thiefRow.wt,  
for i, gem in pairs(Items.GemTable) do
totalWeight = thiefRow.totalWt,
totalGemChance = totalGemChance + gem.chance
expIcon = Icons.getExpansionIcon(thiefRow.npcID)})
end
table.insert(dropRows, {source = '[[Mining#Gems|Gem]]', type = mineType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
local magicType = Icons.Icon({'Magic', type = 'skill'})
table.insert(dropRows, {source = Icons.Icon({"Rags to Riches I", type="spell"}), type = magicType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
table.insert(dropRows, {source = Icons.Icon({"Rags to Riches II", type="spell"}), type = magicType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
end
 
if item.fishingCatchWeight ~= nil then
local fishSource = '[[Fishing#Special|Special]]'
local fishType = Icons.Icon({'Fishing', type='skill'})
table.insert(dropRows, {source = fishSource, type = fishType, minqty = 1, qty = 1, weight = item.fishingCatchWeight, totalWeight = Items.specialFishWt})
end
end


if item.type == 'Junk' then
-- Fishing: Junk & Specials
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'})
table.insert(dropRows, {source = fishSource, type = fishType, minqty = 1, qty = 1, weight = 1, totalWeight = Items.junkCount})
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
end


--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
return a.minqty + a.qty > b.minqty + b.qty
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
else
return a.weight / a.totalWeight > b.weight / b.totalWeight
return a.weight / a.totalWeight > b.weight / b.totalWeight
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 786: Line 1,434:
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 792: Line 1,440:
end
end


function p._getItemUpgradeTable(item)
function p._getItemUpgradeTableData(item, tableData)
local resultPart = {}
if tableData == nil then tableData = {} end
if item.itemsRequired ~= nil then
 
--First, get details on all the required materials
local upgrade = GameData.getEntityByProperty('itemUpgrades', 'upgradedItemID', item.id)
local upgradeFrom = {}
if upgrade ~= nil then
local materials = {}
local reqs = nil
for i, row in pairs(item.itemsRequired) do
if item.charges ~= nil and item.tier ~= nil then
local mat = Items.getItemByID(row[1])
local levelUnlock = GameData.getEntityByProperty(SkillData.Herblore.masteryLevelUnlocks, 'descriptionID', item.tier + 1)
--Check to see if the source item can trigger the upgrade
if levelUnlock ~= nil then
if mat.canUpgrade or (mat.type == 'Armour' and mat.canUpgrade == nil) then
reqs = Icons._MasteryReq(item.name, levelUnlock.level, false)
table.insert(upgradeFrom, Icons.Icon({mat.name, type='item'}))
end
end
table.insert(materials, Icons.Icon({mat.name, type='item', qty=row[2]}))
end
end
if item.trimmedGPCost ~= nil then
 
table.insert(materials, Icons.GP(item.trimmedGPCost))
table.insert(tableData, {
end
['reqs'] = reqs,
table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="2"|[[Upgrading Items|Item Upgrade]]')
['costs'] = Common.getCostString({ ["items"] = upgrade.itemCosts, ["currencies"] = upgrade.currencyCosts }),
--[[result = result..'\r\n|-\r\n!style="text-align:right;"|Upgrades From\r\n|'
['qty'] = (upgrade.quantity or 1),
result = result..table.concat(upgradeFrom, '<br/>')--]]
['source'] = '[[Upgrading Items|Item ' .. (upgrade.isDowngrade and 'Downgrade' or 'Upgrade') ..']]'
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials\r\n|')
})
table.insert(resultPart, table.concat(materials, '<br/>'))
table.insert(resultPart, '\r\n|}')
end
end
return table.concat(resultPart)
 
return tableData
end
end


Line 823: Line 1,468:
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.buildCreationTable(item, p._getItemUpgradeTableData(item))
end
end


function p._getItemSuperheatTable(item)
function p._getSuperheatSmithRecipe(item)
--Manually build the Superheat Item table
local smithRecipe = GameData.getEntityByProperty(SkillData.Smithing.recipes, 'productID', item.id)
local oreString = ''
if smithRecipe ~= nil and smithRecipe.categoryID == 'melvorD:Bars' then
local coalString = ''
return smithRecipe
for i, mat in pairs(item.smithReq) do
end
local thisMat = Items.getItemByID(mat.id)
end
if thisMat.name == 'Coal Ore' then
 
coalString = ', '..Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty})
function p._getItemSuperheatTableData(item, tableData)
if tableData == nil then tableData = {} end
 
-- 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, smithingReq = {}, '', Icons._SkillReq('Smithing', smithRecipe.level, false)
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', qty=mat.quantity })
else
else
if string.len(oreString) > 0 then oreString = oreString..', ' end
table.insert(oreStringPart, Icons.Icon({ matItem.name, type='item', qty=mat.quantity }))
oreString = oreString..Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty})
end
end
end
end
--Set up the header
 
local superheatTable = {}
--Loop through all the variants
table.insert(superheatTable, '{|class="wikitable"\r\n!colspan="2"|Spell')
local spells = Magic.getSpellsProducingItem(item.id)
table.insert(superheatTable, '!!'..Icons.Icon({'Smithing', type='skill', notext='true'})..' Level')
for i, spell in ipairs(spells) do
table.insert(superheatTable, '!!'..Icons.Icon({'Magic', type='skill', notext='true'})..' Level')
if spell.specialCost ~= nil and Shared.contains({ 'BarIngredientsWithCoal', 'BarIngredientsWithoutCoal' }, spell.specialCost.type) then
table.insert(superheatTable, '!!'..Icons.Icon({'Magic', type='skill', notext='true'})..' XP')
local imgType = Magic._getSpellIconType(spell)
table.insert(superheatTable, '!!'..Icons.Icon({item.name, type='item', notext='true'})..' Bars')
local costs = table.concat(oreStringPart, '<br>')
table.insert(superheatTable, '!!Ore!!Runes')
 
--Loop through all the variants
if spell.specialCost.type == 'BarIngredientsWithCoal' and coalString ~= '' then
local spellNames = {'Superheat I', 'Superheat II', 'Superheat III', 'Superheat IV'}
costs = costs .. '<br>' .. coalString
for i, sName in pairs(spellNames) do
end
local spell = Magic.getSpell(sName, 'AltMagic')
 
local rowPart = {}
table.insert(tableData, {
table.insert(rowPart, '\r\n|-\r\n|'..Icons.Icon({spell.name, type='spell', notext=true, size=50}))
['skill'] = 'Alt Magic',
table.insert(rowPart, '||'..Icons.Icon({spell.name, type='spell', noicon=true})..'||style="text-align:right;"|'..item.smithingLevel)
['lvl'] = spell.level,
table.insert(rowPart, '||style="text-align:right;"|'..spell.level..'||style="text-align:right;"|'..spell.magicXP..'||style="text-align:right;"|'..spell.convertToQty)
['reqs'] = smithingReq .. '<br>' .. Icons.Icon({'Alt Magic', type='skill', notext=true}) .. ' Level ' .. spell.level,
table.insert(rowPart, '||'..oreString)
['isAbyssal'] = false,
if spell.ignoreCoal ~= nil and not spell.ignoreCoal then table.insert(rowPart, coalString) end
['xp'] = spell.baseExperience,
table.insert(rowPart, '||style="text-align:center"|')
['costs'] = costs,
for i, req in pairs(spell.runesRequired) do
['qty'] = spell.productionRatio,
local rune = Items.getItemByID(req.id)
['source'] = Icons.Icon({ spell.name, type=imgType }),
if i > 1 then table.insert(rowPart, ', ') end
['time'] = 2,
table.insert(rowPart, Icons.Icon({rune.name, type='item', notext=true, qty=req.qty}))
['runeCost'] = Magic._getSpellRunes(spell)
})
end
end
table.insert(rowPart, "<br/>'''OR'''<br/>")
for i, req in pairs(spell.runesRequiredAlt) do
local rune = Items.getItemByID(req.id)
if i > 1 then table.insert(rowPart, ', ') end
table.insert(rowPart, Icons.Icon({rune.name, type='item', notext=true, qty=req.qty}))
end
table.insert(superheatTable, table.concat(rowPart))
end
end
--Add the table end and add the table to the result string
 
table.insert(superheatTable, '\r\n|}')
return tableData
return table.concat(superheatTable)
end
end


Line 883: Line 1,533:
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.buildCreationTable(item, p._getItemSuperheatTableData(item))
end
end


function p._getItemSourceTables(item)
function p._getTownshipTraderTableData(item, tableData)
local resultPart = {}
if tableData == nil then tableData = {} end
local shopTable = Shop._getItemShopTable(item)
 
if string.len(shopTable) > 0 then
for i, tsResource in ipairs(SkillData.Township.itemConversions.fromTownship) do
table.insert(resultPart, '===Shop===\r\n'..shopTable)
for j, tradeDef in ipairs(tsResource.items) do
if tradeDef.itemID == item.id then
-- Item found, insert the data
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 lvl = nil
local townshipReq = GameData.getEntities(tradeDef.unlockRequirements, function(req) return req.skillID == 'melvorD:Township' end)
if townshipReq ~= nil and townshipReq[1] ~= nil then
lvl = townshipReq[1].level + (townshipReq[1].type == 'AbyssalLevel' and 120 or 0)
end
 
table.insert(tableData, {
['lvl'] = lvl,
['reqs'] = Common.getRequirementString(tradeDef.unlockRequirements),
['isAbyssal'] = namespace == 'melvorItA',
['costs'] = Icons.Icon({ resName, qty=resQty, type='resource' }),
['qty'] = 1,
['source'] = Icons.Icon({ 'Township', 'Trader', type='skill' }),
})
break
end
end
end
end


local creationTable = p._getCreationTable(item)
return tableData
if string.len(creationTable) > 0 then
end
if #resultPart > 0 then table.insert(resultPart, '\r\n') end
 
table.insert(resultPart, '===Creation===\r\n'..creationTable)
function p._getItemShopTableData(item, tableData)
if tableData == nil then tableData = {} end
 
local purchaseArray = Shop.getItemSourceArray(item.id)
 
for i, purchaseData in ipairs(purchaseArray) do
local purchase = purchaseData.purchase
local namespace, localID = Shared.getLocalID(purchase.id)
local source = nil
-- Show icon text when it's the only source of this item
local notext = (Shared.tableCount(tableData) + Shared.tableCount(purchaseArray) > 1)
 
if purchase.contains.items ~= nil and Shared.tableCount(purchase.contains.items) > 1 then
source = Shop._getPurchaseExpansionIcon(purchase) .. Common.getPurchaseIcon({purchase})
-- Always show icon text when there's multiple items
-- notext = false
else
source = Icons.Icon({'Shop'}) .. ' Purchase'
end
 
table.insert(tableData, {
['reqs'] = Common.getRequirementString(purchase.purchaseRequirements),
['isAbyssal'] = namespace == 'melvorItA',
['costs'] = Common.getCostString(purchase.cost),
['qty'] = purchaseData.qty,
['contents'] = Shop._getPurchaseContents(purchase, true, notext),
['source'] = source,
['center'] = notext
})
end
end


local upgradeTable = p._getItemUpgradeTable(item)
return tableData
if string.len(upgradeTable) > 0 then
end
if #resultPart > 0 then table.insert(resultPart, '\r\n') end
 
if string.len(creationTable) == 0 then table.insert(resultPart, '===Creation===\r\n') end
function p._getItemSourceTables(item)
table.insert(resultPart, upgradeTable)
local resultPart = {}
local sourceData = {}
-- Is the item Created or Produced by anything?
p._getCreationTableData(item, sourceData)
-- Can the item be obtained by upgrading?
p._getItemUpgradeTableData(item, sourceData)
-- Can the item be obtained by Superheating?
if p._getSuperheatSmithRecipe(item) ~= nil then
p._getItemSuperheatTableData(item, sourceData)
end
end
-- Can the item be traded for in Township?
p._getTownshipTraderTableData(item, sourceData)
-- Can the item be purchased?
p._getItemShopTableData(item, sourceData)


if item.type == 'Bar' then
local sourceTable = p.buildCreationTable(item, sourceData)
table.insert(resultPart, '\r\n==='..Icons.Icon({'Alt Magic', type='skill'})..'===\r\n'..p._getItemSuperheatTable(item))
if sourceTable ~= '' then
table.insert(resultPart, '===Creation===\r\n' .. sourceTable)
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
return table.concat(resultPart)
return table.concat(resultPart)
end
end
Line 925: Line 1,639:
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 937: Line 1,651:
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 954: Line 1,667:
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.bonesQty ~= nil then
elseif monster.barrierPercent ~= nil and 'melvorAoD:Barrier_Dust' == item.id and not Monsters._isDungeonOnlyMonster(monster) then
minQty = monster.bonesQty
-- Item is Barrier Dust and is not a dungeon exclusive monster
maxQty = monster.bonesQty
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:
--  - A monster before the boss, which doesn't drop anything except shards (checked above)
--  - 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
--  - A boss monster, whose drops are accounted for in data from Areas instead
local monsterWeight
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
-- Item drops when the monster is killed
-- Item drops when the monster is killed
table.insert(resultArray, {monster = monster.id, chance = chance, weight = weight, minQty = minQty, maxQty = maxQty})
table.insert(resultArray, {id = monster.id, dropWt = chance, totalWt = weight, minQty = minQty, maxQty = maxQty})
end
end
end
end
Line 997: Line 1,713:
return p._getItemMonsterSources(item)
return p._getItemMonsterSources(item)
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
table.insert(resultArray, {
id = digSite.id,
name = digSite.name,
level = digSite.level,
size = sizeName,
minQty = found.minQuantity,
maxQty = found.maxQuantity,
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)",
-- "Astrology Lesser Relic",
"Mysterious Stone",
"Charge Stone of Rhaelyx",
"Gold Topaz Ring",
"Charcoal",
"Ash",
"Coal Ore",
"Golden Star",
"Potion Box III",
"Rune Essence",
"Dragonite Bar",
"Holy Dust",
-- "Rune Platebody",
"Arrow Shafts",
-- "Yew Longbow",
"Blood Rune",
"Steam Rune",
-- "Wolf",
"Fox",
"Leprechaun",
"Void Wisp",
-- "Redwood Logs",
-- "Shadow Raven Nest",
"Raw Shrimp",
"Shrimp",
"Carrot Cake",
-- "Carrot Cake (Perfect)",
-- "Mantalyme Herb",
"Carrot",
"Controlled Heat Potion II",
"Topaz",
"Oricha",
"Nightopal",
-- "Sanguine Blade",
-- "Ring of Power",
-- "Infernal Claw",
-- "Chapeau Noir",
"Stardust",
"Golden Stardust",
"Abyssal Stardust",
"Rope",
"Ancient Ring of Mastery",
"Mastery Token (Cooking)",
"Thief's Moneysack",
-- "Slayer Deterer",
"Paper",
-- "Lemon",
"Aranite Brush",
"Charged Diamond Shard",
"Barrier Dust",
"Gloom Resin",
"Gloom Amber",
"Gloom Vine",
"Gloom Vein Seed",
"Elite Chest",
"Gem Gloves",
"Magic Bones",
"Bowstring",
"Superior Max Skillcape",
"Abyssal Coin Contract II",
"Dark Summon Consumable II",
"Abyssal Slayer Gear Upgrade Kit",
"Topaz Bolts (Enchanted)",
"Summoning Shard (Black)",
"Dragon Javelin",
"Skillers Body",
"Abyssal Compost",
}
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
return p

Latest revision as of 21:20, 18 January 2025

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
	elseif Shared.contains(recipe.primaryProducts, itemID) or Shared.contains(recipe.secondaryProducts, itemID) then
		return true
	elseif type(recipe.products) == 'table' then
		return GameData.getEntityByProperty(recipe.products, 'itemID', itemID) ~= nil
	end
	return false
end

function p._getCreationTableData(item, tableData)
	if tableData == nil then tableData = {} end

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

	local skillIDs = {
		['Gathering'] = {
			['Farming'] = { recipeKey = 'recipes' },
			['Woodcutting'] = { recipeKey = 'trees' },
			['Fishing'] = { recipeKey = 'fish' },
			['Firemaking'] = { recipeKey = 'logs' },
			['Mining'] = { recipeKey = 'rockData' },
			['Cartography'] = { recipeKey = 'paperRecipes' },
			['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, reqs, isAbyssal, xp, costs, qty, source, time, maxTime, weight, totalWeight = nil, nil, false, nil, nil, nil, nil, nil, nil, nil, nil, nil
		for i, recipe in ipairs(skillData[dataProp.recipeKey]) do
			local hasProduct = doesRecipeHaveItemID(recipe, itemID)
			if hasProduct then
				lvl, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
				xp = recipe.baseAbyssalExperience or recipe.baseExperience
				qty = recipe.baseQuantity or 1
				reqs = Icons._SkillReq(skill, lvl, false, (isAbyssal and 'melvorItA:Abyssal' or nil))
				source = Icons.Icon({ skill, type='skill', class=(isAbyssal and 'abyss-icon' or nil) })
				-- 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
				-- Custom Chance, Qty, and Costs data
				if localSkillID == 'Farming' then
					costs = { recipe.seedCost }
					local catData = GameData.getEntityByID(skillData.categories, recipe.categoryID)
					qty = 5 * catData.harvestMultiplier
				elseif localSkillID == 'Firemaking' then
					local itemChanceData = GameData.getEntityByProperty(SkillData.Firemaking.primaryProducts, 'itemID', itemID)
					if itemChanceData ~= nil then
						weight = itemChanceData.chance
					elseif itemID == 'melvorD:Generous_Fire_Spirit' then
						weight = 0.1
					end

					if Shared.contains({ 'melvorD:Generous_Fire_Spirit', 'melvorD:Coal_Ore', 'melvorTotH:Charcoal' }, itemID) then
						costs = 'Any ' .. Icons.Icon({ 'Firemaking', 'Melvor Logs', img='Melvor Logo', section='Melvor Logs' })
					else
						local costItem = Items.getItemByID(recipe.logID)
						costs = Icons.Icon({ costItem.name, type='item', qty=1 })
					end

					if itemID == 'melvorF:Ash' then
						qty = time
					elseif itemID == 'melvorItA:Withered_Ash' or itemID == 'melvorItA:Eternal_Ash' then
						qty = math.max(math.floor(recipe.abyssalLevel / 10), 1)
					end
				elseif localSkillID == 'Cartography' then
					time = 5

					local costItem = Items.getItemByID(recipe.costs.items[1].id)
					costs = Icons.Icon({ costItem.name, type='item', qty=1 })
				elseif localSkillID == 'Harvesting' then
					local itemChanceData = nil
					totalWeight = 0

					for i, product in ipairs(recipe.products) do
						totalWeight = totalWeight + (product.weight or 0)

						if product.itemID == itemID then itemChanceData = product end
					end

					if itemChanceData ~= nil then
						weight = itemChanceData.weight
						reqs = reqs .. '<br>' .. itemChanceData.minIntensityPercent .. '% ' .. Icons.Icon({ recipe.name, type='vein', notext=true }) .. ' Intensity'
					end
				end
				-- Special requirements
				if recipe.totalMasteryRequired ~= nil then
					reqs = reqs .. '<br>' .. Icons.Icon({ 'Mastery', notext=true }) .. ' ' .. Num.formatnum(recipe.totalMasteryRequired) .. ' total [[' .. skill .. ']] [[Mastery]]'
				end
				table.insert(tableData, {
					['skill'] = skill,
					['lvl'] = lvl,
					['reqs'] = reqs,
					['isAbyssal'] = isAbyssal,
					['xp'] = xp,
					['costs'] = costs,
					['qty'] = qty,
					['source'] = source,
					['time'] = time,
					['maxTime'] = maxTime,
					['weight'] = weight,
					['totalWeight'] = totalWeight
				})
				-- Most recipes have a single item source or the item source data
				-- is nearly all the same. The following items have some uniqueness
				if not Shared.contains({ 'melvorF:Ash', 'melvorItA:Withered_Ash', 'melvorAoD:Paper' }, itemID) then break end
			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, reqs, isAbyssal, xp, costs, qty, source, time, maxTime = nil, nil, false, nil, nil, nil, nil, nil, nil, 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
				reqs = Icons._SkillReq(skill, lvl, false, (isAbyssal and 'melvorItA:Abyssal' or nil))
				source = Icons.Icon({ skill, type='skill', class=(isAbyssal and 'abyss-icon' or nil) })
				-- 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
						reqs = reqs .. '<br>' .. 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', 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', qty=nonShardQty }))
						end
					end
					costs = table.concat(shardCostArray, '<br>')
					if not Shared.tableIsEmpty(otherCostArray) then
						local costLen = Shared.tableCount(otherCostArray)
						costs = costs .. '<br>' .. (costLen == 1 and '' or 'and one of the following:<br>') .. table.concat(otherCostArray, "<br>'''OR''' ")
					end
					reqs = reqs .. '<br>At least 1 ' .. Icons.Icon({ 'Summoning', item.name, img=item.name, type='mark', section='Summoning Marks' }) .. ' mark discovered'
					table.insert(tableData, {
						['skill'] = skill,
						['lvl'] = lvl,
						['reqs'] = reqs,
						['isAbyssal'] = isAbyssal,
						['xp'] = xp,
						['costs'] = costs,
						['qty'] = qty,
						['source'] = source,
						['time'] = time
					})
				-- 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
						table.insert(tableData, {
							['skill'] = skill,
							['lvl'] = lvl,
							['reqs'] = reqs,
							['isAbyssal'] = isAbyssal,
							['xp'] = xp,
							['costs'] = Common.getCostString({ ["items"] = altCost.itemCosts, ["currencies"] = recipe.currencyCosts }),
							['qty'] = qty * altCost.quantityMultiplier,
							['source'] = source,
							['time'] = time,
							['maxTime'] = maxTime
						})
					end
				-- Finally, normal recipes with a single set of item costs
				elseif type(recipe.itemCosts) == 'table' and not Shared.tableIsEmpty(recipe.itemCosts) then
					if localSkillID == 'Cooking' then
						-- Cooking includes the required utility (fire, furnace, pot) as a special requirement
						local cookingCatIcon = {
							["melvorD:Fire"] = 'Normal Cooking Fire',
							["melvorD:Furnace"] = 'Basic Furnace',
							["melvorD:Pot"] = 'Basic Pot'
						}
						local categoryIconName, categoryName = cookingCatIcon[recipe.categoryID], nil
						local recipeCategory = GameData.getEntityByID(SkillData.Cooking.categories, recipe.categoryID)
						if recipeCategory ~= nil then
							categoryName = recipeCategory.modifierName or recipeCategory.name
						end
						if categoryIconName ~= nil and categoryName ~= nil then
							reqs = reqs .. '<br>' .. Icons.Icon({ 'Cooking', categoryName, section = 'Cooking Upgrades', img = categoryIconName, type = 'upgrade' })
						end
					end

					table.insert(tableData, {
						['skill'] = skill,
						['lvl'] = lvl,
						['reqs'] = reqs,
						['isAbyssal'] = isAbyssal,
						['xp'] = xp,
						['costs'] = Common.getCostString({ ["items"] = recipe.itemCosts, ["currencies"] = recipe.currencyCosts }),
						['qty'] = qty,
						['source'] = source,
						['time'] = time,
						['maxTime'] = maxTime
					})
				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 == itemID then
			table.insert(tableData, {
				['skill'] = 'Alt Magic',
				['lvl'] = altSpell.level,
				['reqs'] = Icons.Icon({'Alt Magic', type='skill', notext=true}) .. ' Level ' .. altSpell.level,
				['isAbyssal'] = false,
				['xp'] = altSpell.baseExperience,
				['costs'] = Magic._getAltSpellCostText(altSpell),
				['qty'] = altSpell.productionRatio,
				['source'] = Icons.Icon({ altSpell.name, type=Magic._getSpellIconType(altSpell) }),
				['time'] = 2,
				['runeCost'] = Magic._getSpellRunes(altSpell)
			})
		end
	end

	-- Add in Astrology creation items manually since each constellation has (mostly)
	-- the same creation information so looping through them is not necessary
	local stardustChanceData = GameData.getEntityByProperty(SkillData.Astrology.baseRandomItemChances, 'itemID', itemID)
	if stardustChanceData ~= nil then
		local namespace, localID = Shared.getLocalID(stardustChanceData.itemID)
		local isAbyssal = namespace == 'melvorItA'
		table.insert(tableData, {
			['skill'] = 'Astrology',
			['lvl'] = 1,
			['reqs'] = Icons._SkillReq('Astrology', 1, false, (isAbyssal and 'melvorItA:Abyssal' or nil)),
			['isAbyssal'] = isAbyssal,
			['qty'] = 1,
			['xp'] = (isAbyssal and 1238 or 5), -- Use the XP value for the first constellation
			['source'] = Icons.Icon({ 'Astrology', type='skill', class=(isAbyssal and 'abyss-icon' or nil) }),
			['time'] = 3,
			['weight'] = stardustChanceData.chance
		})
	end

	-- Can we find this in an Archaeology digsite?
	for i, drop in ipairs(p._getItemArchSources(item)) do
		if drop.name ~= nil then
			table.insert(tableData, {
				['skill'] = 'Archaeology',
				['lvl'] = drop.level,
				['reqs'] = Icons._SkillReq('Archaeology', drop.level) .. ' ('..drop.size..')',
				['isAbyssal'] = false,
				['minqty'] = drop.minQty,
				['qty'] = drop.maxQty,
				['source'] = Icons.Icon({ drop.name, type='poi' }),
				['time'] = 4,
				['weight'] = drop.dropWt,
				['totalWeight'] = drop.totalWt
				--['expIcon'] = Icons.getExpansionIcon(drop.id)}),
			})
		end
	end

	-- Mining: Gems, and also Alt. Magic spells producing random gems
	if Shared.contains({'Gem', 'Superior Gem', 'Abyssal Gem'}, item.type) then
		local gemKeys = { 'randomGems', 'randomSuperiorGems', 'randomAbyssalGems' }
		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, lvl, isAbyssal = nil, nil, false

				if item.type == 'Abyssal Gem' then
					sourceTxt = '[[Mining#Abyssal Gems|Abyssal Gem]]'
					lvl = 1
					isAbyssal = true
				elseif 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
					lvl = 100
				else
					sourceTxt = '[[Mining#Gems|Gem]]'
					-- Gems can only be found with any Mining level
					lvl = 1
				end
				table.insert(tableData, {
					['skill'] = 'Mining',
					['lvl'] = lvl,
					['reqs'] = Icons._SkillReq('Mining', lvl, false, (isAbyssal and 'melvorItA:Abyssal' or nil)),
					['isAbyssal'] = isAbyssal,
					['minqty'] = thisGem.minQuantity,
					['qty'] = thisGem.maxQuantity,
					['source'] = sourceTxt,
					['time'] = 3,
					['weight'] = thisGem.weight,
					['totalWeight'] = totalGemWeight,
					--expIcon = expIcon
				})

				-- Check for Alt. Magic spells also
				local producesKey = (gemKey == 'randomGems' and 'RandomGem') or (gemKey == 'randomSuperiorGems' and 'RandomSuperiorGem') or nil
				if producesKey ~= nil then
					for j, spell in ipairs(Magic.getSpellsBySpellBook('altMagic')) do
						if spell.produces ~= nil and spell.produces == producesKey then
							table.insert(tableData, {
								['skill'] = 'Alt Magic',
								['lvl'] = spell.level,
								['reqs'] = Icons.Icon({'Alt Magic', type='skill', notext=true}) .. ' Level ' .. spell.level,
								['minqty'] = thisGem.minQuantity,
								['qty'] = thisGem.maxQuantity,
								['source'] = Icons.Icon({ spell.name, type=Magic._getSpellIconType(spell) }),
								['time'] = 2,
								['weight'] = thisGem.weight,
								['totalWeight'] = totalGemWeight,
								--expIcon = Icons.getExpansionIcon(spell.id)
							})
						end
					end
				end
			end
		end
	end

	return tableData
end

function p.buildCreationTable(item, tableData)
	if Shared.tableIsEmpty(tableData) then return '' end

	table.sort(tableData, function(a, b) return (a.qty or 1) < (b.qty or 1) end)

	local showSource = false
	local showRequirements = false
	local showInputs = false
	local showRunes = false
	local showOutputs = false
	local showXP = false
	local showTime = false
	local showChance = false
	local colspan = -1 -- colspan only needs to be set when there are 3+ columns in the table

	for i, data in ipairs(tableData) do
		if not showSource and tableData[1].source ~= tableData[i].source then
			showSource = true
			colspan = colspan + 1
		end
		if not showRequirements and tableData[1].reqs ~= tableData[i].reqs then
			showRequirements = true
			colspan = colspan + 1
		end
		if not showInputs and tableData[1].costs ~= tableData[i].costs then
			showInputs = true
			colspan = colspan + 1
		end
		if not showRunes and  tableData[1].runeCost ~= tableData[i].runeCost then
			showRunes = true
			colspan = colspan + 1
		end
		if not showOutputs and (tableData[1].qty ~= tableData[i].qty or tableData[1].contents ~= tableData[i].contents) then
			showOutputs = true
			colspan = colspan + 1
		end
		if not showXP and tableData[1].xp ~= tableData[i].xp then
			showXP = true
			colspan = colspan + 1
		end
		if not showTime and tableData[1].time ~= tableData[i].time then
			showTime = true
			colspan = colspan + 1
		end
		if not showChance and tableData[1].weight ~= tableData[i].weight then
			showChance = true
			colspan = colspan + 2
		end
	end

	colspan = math.max(colspan, 1)

	local function addCostsRow(row, data, span)
		local costsRow = row:tag('td'):attr('colspan', span)
		if type(data.costs) == 'table' then
			for i, mat in ipairs(data.costs) do
				if i > 1 then costsRow:tag('br') end
				local matItem = Items.getItemByID(mat.id)
				if matItem == nil then
					costsRow:wikitext(mat.quantity .. 'x ?????')
				else
					costsRow:wikitext(Icons.Icon({ matItem.name, type='item', qty=mat.quantity }))
				end
			end
		else
			local costStr = data.costs:gsub(', ', '<br>')
			costsRow:wikitext(costStr)
		end
	end

	local resultTable = mw.html.create('table')
	resultTable:addClass('wikitable stickyHeader')
	local tableHeader = resultTable:tag('tr'):addClass('headerRow-0')

	local makeSortable = Shared.contains({ showSource, showRequirements, showInputs, showRunes, showOutputs, showXP, showTime, showChance }, true)
	if makeSortable then
		resultTable:addClass('sortable')
	end

	if showSource then tableHeader:tag('th'):wikitext('Source') end
	if showRequirements then tableHeader:tag('th'):wikitext('Requires') end
	if showInputs then tableHeader:tag('th'):wikitext('Costs') end
	if showRunes then tableHeader:tag('th'):wikitext('Runes') end
	if showOutputs then tableHeader:tag('th'):wikitext('Outputs') end
	if showXP then tableHeader:tag('th'):wikitext('Exp') end
	if showTime then tableHeader:tag('th'):wikitext('Time') end
	if showChance then tableHeader:tag('th'):wikitext('Chance'):attr('colspan', 2) end

	if makeSortable then
		-- Populate table data with any unique entries (Ex: Ash's Inputs, Outputs, Exp, Time)
		for i, data in ipairs(tableData) do
			local recipeRow = resultTable:tag('tr')

			if showSource then recipeRow:tag('td'):wikitext(data.source):attr('data-sort-value', data.skill) end

			if showRequirements then
				if data.reqs ~= nil then
					recipeRow:tag('td'):wikitext(data.reqs):attr('data-sort-value', (data.lvl or 0))
				else
					recipeRow:tag('td'):wikitext('N/A'):addClass('table-na'):attr('data-sort-value', 0)
				end
			end

			if showInputs then
				if data.costs ~= nil then
					addCostsRow(recipeRow, data, 1)
				else
					recipeRow:tag('td'):wikitext('N/A'):addClass('table-na')
				end
			end

			if showRunes then
				if data.runeCost ~= nil then
					recipeRow:tag('td'):wikitext(data.runeCost):css('text-align', 'center')
				else
					recipeRow:tag('td'):wikitext('N/A'):addClass('table-na')
				end
			end

			if showOutputs then
				local outputData = recipeRow:tag('td'):attr('data-sort-value', (data.qty or 1))
				if data.contents ~= nil then
					outputData:wikitext(data.contents)
					if data.center then outputData:css('text-align', 'center') end
				elseif data.qty ~= nil then
					if data.minqty ~= nil and data.minqty ~= data.qty then
						outputRow:wikitext((data.minqty ~= nil and (Num.formatnum(data.minqty) .. ' - ') or ''))
					end
					outputData:wikitext(Icons.Icon({ item.name, type='item', notext=true, qty=(data.qty or 1) })):css('text-align', 'center'):attr('data-sort-value', (data.qty or 1))
				else
					outputData:wikitext('N/A'):addClass('table-na')
				end
			end

			if showXP then
				if data.skill ~= nil and data.xp ~= nil then
					local iconClass = (data.isAbyssal and 'abyss-icon' or nil)
					local xpText = (data.isAbyssal and ' AXP' or ' XP')
					recipeRow:tag('td'):attr('data-sort-value', data.xp)
						:wikitext(Icons.Icon({ data.skill, notext=true, type='skill', class=iconClass }))
						:wikitext(' ' .. Num.formatnum(data.xp) .. xpText)
				else
					recipeRow:tag('td'):wikitext('N/A'):addClass('table-na'):attr('data-sort-value', 0)
				end
			end

			if showTime then
				if data.time ~= nil then
					recipeRow:tag('td'):wikitext(Shared.timeString(data.time, true)):css('text-align', 'center'):attr('data-sort-value', data.time)
				else
					recipeRow:tag('td'):wikitext('N/A'):addClass('table-na')
				end
			end

			if showChance then
				if data.weight ~= nil then
					-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
					local chance = data.weight / (data.totalWeight or 100) * 100
					local fmt = (chance < 0.10 and '%.2g') or '%.2f'
					local percent = string.format(fmt, chance)

					recipeRow:tag('td'):wikitext(Num.fraction(data.weight, (data.totalWeight or 100))):css('text-align', 'center'):attr('data-sort-value', percent)
					recipeRow:tag('td'):wikitext(percent .. '%'):css('text-align', 'center')
				else
					recipeRow:tag('td'):wikitext('100%'):css('text-align', 'center'):attr('colspan', 2):attr('data-sort-value', 100)
				end
			end
		end
	end

	-- Add all non-unique data below the table data (Ex: Ash's Source, Requires, Chance)
	if not showSource and tableData[1].source ~= nil then
		resultTable:tag('tr')
			:tag('th'):wikitext('Source'):css('text-align', 'right')
			:tag('td'):attr('colspan', colspan):wikitext(tableData[1].source)
	end

	if not showRequirements and tableData[1].reqs ~= nil then
		local reqRow = resultTable:tag('tr')
			:tag('th'):wikitext('Requires'):css('text-align', 'right')
			:tag('td'):wikitext(tableData[1].reqs):attr('colspan', colspan)
	end

	if not showInputs and tableData[1].costs ~= nil then
		local costRow = resultTable:tag('tr')
			:tag('th'):wikitext('Costs'):css('text-align', 'right')

		addCostsRow(costRow, tableData[1], colspan)
	end

	if not showRunes and type(tableData[1].runeCost) == 'string' then
		resultTable:tag('tr')
			:tag('th'):wikitext('Runes'):css('text-align', 'right')
			:tag('td'):wikitext(tableData[1].runeCost):css('text-align', 'center')
	end

	if not showOutputs and (tableData[1].qty ~= nil or tableData[1].contents ~= nil) then
		local outputRow = resultTable:tag('tr')
			:tag('th'):wikitext('Outputs'):css('text-align', 'right')

		if tableData[1].contents ~= nil then
			outputRow:tag('td'):wikitext(tableData[1].contents)
		else
			local outputData = outputRow:tag('td'):attr('colspan', colspan)
			if tableData[1].minqty ~= nil and tableData[1].minqty ~= tableData[1].qty then
				outputData:wikitext((tableData[1].minqty ~= nil and (Num.formatnum(tableData[1].minqty) .. ' - ') or ''))
			end
			outputData:wikitext(Icons.Icon({ item.name, type='item', qty=(tableData[1].qty or 1) }))
		end
	end

	if not showXP and tableData[1].xp ~= nil then
		local xpText = (tableData[1].isAbyssal and ' AXP' or ' XP')
		resultTable:tag('tr')
			:tag('th'):wikitext('Base Exp'):css('text-align', 'right')
			:tag('td'):attr('colspan', colspan):wikitext(Num.formatnum(tableData[1].xp) .. xpText)
	end

	if not showTime and tableData[1].time ~= nil then
		resultTable:tag('tr')
		local timeHeader = resultTable:tag('th'):wikitext('Base Time'):css('text-align', 'right')

		local timeData = timeHeader:tag('td'):attr('colspan', colspan)
			:wikitext(Shared.timeString(tableData[1].time, true))

		if tableData[1].maxTime ~= nil and tableData[1].maxTime > tableData[1].time then
			timeData:wikitext(' - ' .. Shared.timeString(tableData[1].maxTime, true))
		end
	end

	if not showChance and tableData[1].weight ~= nil then
		-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
		local chance = tableData[1].weight / (tableData[1].totalWeight or 100) * 100
		local fmt = (chance < 0.10 and '%.2g') or '%.2f'
		local percent = string.format(fmt, chance)
		local chanceData = resultTable:tag('tr')
			:tag('th'):wikitext('Base Chance'):css('text-align', 'right')
			:tag('td'):attr('colspan', colspan)
				:wikitext(Num.fraction(tableData[1].weight, (tableData[1].totalWeight or 100)) .. ' (' .. percent .. '%)')
	end

	return tostring(resultTable)
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.buildCreationTable(item, p._getCreationTableData(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
		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='combatArea', 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='combatArea', 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' },
			['Firemaking'] = { recipeKey = 'logs' },
			['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 or
		GameData.getEntityByProperty('randomAbyssalGems', '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 col-3-center col-4-center col-5-center"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"')
	table.insert(resultPart, '\r\n!Source!!Level!!Qty!!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|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|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, 'data-sort-value="0"|Varies (see Thieving page)')
		else
			table.insert(rowPart, '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

	-- 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
		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='combatArea'}), 
						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='combatArea'}) .. (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

	--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._getItemUpgradeTableData(item, tableData)
	if tableData == nil then tableData = {} end

	local upgrade = GameData.getEntityByProperty('itemUpgrades', 'upgradedItemID', item.id)
	if upgrade ~= nil then
		local reqs = nil
		if item.charges ~= nil and item.tier ~= nil then
			local levelUnlock = GameData.getEntityByProperty(SkillData.Herblore.masteryLevelUnlocks, 'descriptionID', item.tier + 1)
			if levelUnlock ~= nil then
				reqs = Icons._MasteryReq(item.name, levelUnlock.level, false)
			end
		end

		table.insert(tableData, {
			['reqs'] = reqs,
			['costs'] = Common.getCostString({ ["items"] = upgrade.itemCosts, ["currencies"] = upgrade.currencyCosts }),
			['qty'] = (upgrade.quantity or 1),
			['source'] = '[[Upgrading Items|Item ' .. (upgrade.isDowngrade and 'Downgrade' or 'Upgrade') ..']]'
		})
	end

	return tableData
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.buildCreationTable(item, p._getItemUpgradeTableData(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._getItemSuperheatTableData(item, tableData)
	if tableData == nil then tableData = {} end

	-- 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, smithingReq = {}, '', Icons._SkillReq('Smithing', smithRecipe.level, false)
	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', qty=mat.quantity })
		else
			table.insert(oreStringPart, Icons.Icon({ matItem.name, type='item', qty=mat.quantity }))
		end
	end

	--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)
			local costs = table.concat(oreStringPart, '<br>')

			if spell.specialCost.type == 'BarIngredientsWithCoal' and coalString ~= '' then
				costs = costs .. '<br>' .. coalString
			end

			table.insert(tableData, {
				['skill'] = 'Alt Magic',
				['lvl'] = spell.level,
				['reqs'] = smithingReq .. '<br>' .. Icons.Icon({'Alt Magic', type='skill', notext=true}) .. ' Level ' .. spell.level,
				['isAbyssal'] = false,
				['xp'] = spell.baseExperience,
				['costs'] = costs,
				['qty'] = spell.productionRatio,
				['source'] = Icons.Icon({ spell.name, type=imgType }),
				['time'] = 2,
				['runeCost'] = Magic._getSpellRunes(spell)
			})
		end
	end

	return tableData
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.buildCreationTable(item, p._getItemSuperheatTableData(item))
end

function p._getTownshipTraderTableData(item, tableData)
	if tableData == nil then tableData = {} end

	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, insert the data
				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 lvl = nil
				local townshipReq = GameData.getEntities(tradeDef.unlockRequirements, function(req) return req.skillID == 'melvorD:Township' end)
				if townshipReq ~= nil and townshipReq[1] ~= nil then
					lvl = townshipReq[1].level + (townshipReq[1].type == 'AbyssalLevel' and 120 or 0)
				end

				table.insert(tableData, {
					['lvl'] = lvl,
					['reqs'] = Common.getRequirementString(tradeDef.unlockRequirements),
					['isAbyssal'] = namespace == 'melvorItA',
					['costs'] = Icons.Icon({ resName, qty=resQty, type='resource' }),
					['qty'] = 1,
					['source'] = Icons.Icon({ 'Township', 'Trader', type='skill' }),
				})
				break
			end
		end
	end

	return tableData
end

function p._getItemShopTableData(item, tableData)
	if tableData == nil then tableData = {} end

	local purchaseArray = Shop.getItemSourceArray(item.id)

	for i, purchaseData in ipairs(purchaseArray) do
		local purchase = purchaseData.purchase
		local namespace, localID = Shared.getLocalID(purchase.id)
		local source = nil
		-- Show icon text when it's the only source of this item
		local notext = (Shared.tableCount(tableData) + Shared.tableCount(purchaseArray) > 1)

		if purchase.contains.items ~= nil and Shared.tableCount(purchase.contains.items) > 1 then
			source = Shop._getPurchaseExpansionIcon(purchase) .. Common.getPurchaseIcon({purchase})
			-- Always show icon text when there's multiple items
			-- notext = false
		else
			source = Icons.Icon({'Shop'}) .. ' Purchase'
		end

		table.insert(tableData, {
			['reqs'] = Common.getRequirementString(purchase.purchaseRequirements),
			['isAbyssal'] = namespace == 'melvorItA',
			['costs'] = Common.getCostString(purchase.cost),
			['qty'] = purchaseData.qty,
			['contents'] = Shop._getPurchaseContents(purchase, true, notext),
			['source'] = source,
			['center'] = notext
		})
	end

	return tableData
end

function p._getItemSourceTables(item)
	local resultPart = {}
	local sourceData = {}
	-- Is the item Created or Produced by anything?
	p._getCreationTableData(item, sourceData)
	-- Can the item be obtained by upgrading?
	p._getItemUpgradeTableData(item, sourceData)
	-- Can the item be obtained by Superheating?
	if p._getSuperheatSmithRecipe(item) ~= nil then
		p._getItemSuperheatTableData(item, sourceData)
	end
	-- Can the item be traded for in Township?
	p._getTownshipTraderTableData(item, sourceData)
	-- Can the item be purchased?
	p._getItemShopTableData(item, sourceData)

	local sourceTable = p.buildCreationTable(item, sourceData)
	if sourceTable ~= '' then
		 table.insert(resultPart, '===Creation===\r\n' .. sourceTable)
	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
				table.insert(resultArray, {
					id = digSite.id, 
					name = digSite.name, 
					level = digSite.level,
					size = sizeName, 
					minQty = found.minQuantity, 
					maxQty = found.maxQuantity, 
					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)",
		-- "Astrology Lesser Relic",
		"Mysterious Stone",
		"Charge Stone of Rhaelyx",
		"Gold Topaz Ring",
		"Charcoal",
		"Ash",
		"Coal Ore",
		"Golden Star",
		"Potion Box III",
		"Rune Essence",
		"Dragonite Bar",
		"Holy Dust",
		-- "Rune Platebody",
		"Arrow Shafts",
		-- "Yew Longbow",
		"Blood Rune",
		"Steam Rune",
		-- "Wolf",
		"Fox",
		"Leprechaun",
		"Void Wisp",
		-- "Redwood Logs",
		-- "Shadow Raven Nest",
		"Raw Shrimp",
		"Shrimp",
		"Carrot Cake",
		-- "Carrot Cake (Perfect)",
		-- "Mantalyme Herb",
		"Carrot",
		"Controlled Heat Potion II",
		"Topaz",
		"Oricha",
		"Nightopal",
		-- "Sanguine Blade",
		-- "Ring of Power",
		-- "Infernal Claw",
		-- "Chapeau Noir",
		"Stardust",
		"Golden Stardust",
		"Abyssal Stardust",
		"Rope",
		"Ancient Ring of Mastery",
		"Mastery Token (Cooking)",
		"Thief's Moneysack",
		-- "Slayer Deterer",
		"Paper",
		-- "Lemon",
		"Aranite Brush",
		"Charged Diamond Shard",
		"Barrier Dust",
		"Gloom Resin",
		"Gloom Amber",
		"Gloom Vine",
		"Gloom Vein Seed",
		"Elite Chest",
		"Gem Gloves",
		"Magic Bones",
		"Bowstring",
		"Superior Max Skillcape",
		"Abyssal Coin Contract II",
		"Dark Summon Consumable II",
		"Abyssal Slayer Gear Upgrade Kit",
		"Topaz Bolts (Enchanted)",
		"Summoning Shard (Black)",
		"Dragon Javelin",
		"Skillers Body",
		"Abyssal Compost",
	}
	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