Module:Items/SourceTables: Difference between revisions

From Melvor Idle
(Remove redundant item source overrides)
(Alt Magic -> Alt. Magic for consistency)
(17 intermediate revisions by 3 users not shown)
Line 4: Line 4:
local ItemData = mw.loadData('Module:Items/data')
local ItemData = mw.loadData('Module:Items/data')
local SkillData = mw.loadData('Module:Skills/data')
local SkillData = mw.loadData('Module:Skills/data')
local MagicData = mw.loadData('Module:Magic/data')


local Constants = require('Module:Constants')
local Constants = require('Module:Constants')
Line 13: Line 14:
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 SkillEnum = mw.loadData('Module:Constants/data').skill
-- Implements overrides for sources which cannot be obtained from game data
-- Implements overrides for sources which cannot be obtained from game data
-- Currently only overrides for dungeon sources are implemented here
-- Currently only overrides for dungeon sources are implemented here
Line 37: Line 39:
local tables = {}
local tables = {}
--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
if type(item.masteryID) == 'table' then
skill = 'Smithing'
local skillID, masteryID = item.masteryID[1], item.masteryID[2]
lvl = item.smithingLevel
skill = Constants.getSkillName(skillID)
xp = item.smithingXP
if skillID == SkillEnum.Fishing then
req = item.smithReq
-- Fishing
qty = item.smithingQty
local fish = SkillData.Fishing.Fish[masteryID + 1]
time = 2
if fish ~= nil and fish.itemID == item.id then
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
lvl = fish.level
end
xp = fish.baseXP
if item.craftingLevel ~= nil then
qty = 1
skill = 'Crafting'
time = fish.baseMinInterval / 1000
lvl = item.craftingLevel
maxTime = fish.baseMaxInterval / 1000
xp = item.craftingXP
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime))
req = item.craftReq
end
qty = item.craftQty
elseif skillID == SkillEnum.Mining then
time = 3
-- Mining
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, nil, nil, item.craftGPCost))
local rock = SkillData.Mining.Rocks[masteryID + 1]
end
if rock ~= nil then
if item.runecraftingLevel ~= nil then
lvl = rock.levelRequired
skill = 'Runecrafting'
xp = rock.baseExperience
lvl = item.runecraftingLevel
qty = rock.baseQuantity
xp = item.runecraftingXP
time = 3
req = item.runecraftReq
if item.name == 'Dragonite Ore' then
qty = item.runecraftQty
specialReq = Icons.Icon({"Mastery", notext='true'})..' 271 total [[Mining]] [[Mastery]]'
time = 2
end
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, nil, specialReq))
end
end
if item.fletchingLevel ~= nil then
elseif Shared.contains({SkillEnum.Smithing, SkillEnum.Fletching, SkillEnum.Crafting,
skill = 'Fletching'
SkillEnum.Runecrafting, SkillEnum.Herblore}, skillID) then
lvl = item.fletchingLevel
-- Smithing, Fletching, Crafting, Runecrafting, Herblore
xp = item.fletchingXP
-- All have somewhat consistent recipe data structures
req = item.fletchReq
local recipeKey = (skillID == SkillEnum.Herblore and 'Potions') or 'Recipes'
qty = item.fletchQty
local recipe = SkillData[skill][recipeKey][masteryID + 1]
time = 2
if recipe ~= nil then
if item.name == 'Arrow Shafts' then
local masteryReq = nil
--Arrow Shafts get special (weird) treatment
local itemMatch = (recipe.itemID == item.id)
req = '1 of any [[Log]]'
if skillID == SkillEnum.Herblore then
qty = '15 - 135'
-- For Herblore, we need to check a table of potion IDs & determine the mastery requirement
for i, potionID in ipairs(recipe.potionIDs) do
if potionID == item.id then
itemMatch = true
masteryReq = SkillData.Herblore.TierMasteryLevels[i]
break
end
end
end
if itemMatch then
local baseTime = {
[SkillEnum.Smithing] = 2,
[SkillEnum.Fletching] = 2,
[SkillEnum.Crafting] = 3,
[SkillEnum.Runecrafting] = 2,
[SkillEnum.Herblore] = 2
}
local baseQty = recipe.baseQuantity or 1
lvl = recipe.level
xp = recipe.baseXP
time = baseTime[skillID]
if masteryReq ~= nil and masteryReq > 1 then
specialReq = Icons._MasteryReq(item.name, masteryReq, false)
end
-- Some items (such as Arrow shafts) have multiple recipes
if type(recipe.alternativeCosts) == 'table' then
local reqPart, qtyPart = {}, {}
for i, altCost in ipairs(recipe.alternativeCosts) do
local reqSubPart = {}
for j, itemCost in ipairs(altCost.itemCosts) do
local reqItem = Items.getItemByID(itemCost.id)
if reqItem == nil then
table.insert(reqSubPart, itemCost.qty .. 'x ?????')
else
table.insert(reqSubPart, Icons.Icon({reqItem.name, type='item', qty=itemCost.qty}))
end
end
if recipe.gpCost ~= nil and recipe.gpCost > 0 then
table.insert(reqSubPart, Icons.GP(recipe.gpCost))
end
if recipe.scCost ~= nil and recipe.scCost > 0 then
table.insert(reqSubPart, Icons.GP(recipe.scCost))
end
table.insert(reqPart, table.concat(reqSubPart, ', '))
table.insert(qtyPart, Shared.formatnum(baseQty * altCost.quantityMultiplier))
end
local sep = "<br/>'''OR''' "
req = table.concat(reqPart, sep)
qty = table.concat(qtyPart, sep)
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime, specialReq))
elseif type(recipe.itemCosts) == 'table' and Shared.tableCount(recipe.itemCosts) > 0 then
req = recipe.itemCosts
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, baseQty, time, maxTime, specialReq, recipe.gpCost, recipe.scCost))
end
end
end
elseif skillID == SkillEnum.Summoning then
-- Summoning
local recipe = SkillData.Summoning.Marks[masteryID + 1]
if recipe ~= nil and recipe.itemID == item.id then
lvl = recipe.level
xp = recipe.baseXP
qty = recipe.baseQuantity
time = 5
-- Create item requirements text
local ShardCostArray, OtherCostArray = {}, {}
-- Shards
for j, cost in ipairs(recipe.itemCosts) do
local shard = Items.getItemByID(cost.id)
if shard ~= nil then
table.insert(ShardCostArray, Icons.Icon({shard.name, type='item', notext=true, qty=cost.qty}))
end
end
-- Other costs
local recipeGPCost = SkillData.Summoning.RecipeGPCost
if recipe.gpCost > 0 then
table.insert(OtherCostArray, Icons.GP(recipe.gpCost))
end
if recipe.scCost > 0 then
table.insert(OtherCostArray, Icons.SC(recipe.scCost))
end
for j, nonShardID in ipairs(recipe.nonShardItemCosts) do
local nonShard = Items.getItemByID(nonShardID)
if nonShard ~= nil then
local itemValue = math.max(nonShard.sellsFor, 20)
local nonShardQty = math.max(1, math.floor(recipeGPCost / itemValue))
table.insert(OtherCostArray, Icons.Icon({nonShard.name, type='item', notext=true, qty=nonShardQty}))
end
end
req = table.concat(ShardCostArray, ', ')
if #OtherCostArray > 0 then
req = req .. '<br/>and one of the following:<br/>' .. table.concat(OtherCostArray, "<br/>'''OR''' ")
end
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
end
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
-- Woodcutting
skill = 'Cooking'
if item.type == 'Logs' then
lvl = item.cookingLevel
-- Determine which tree (if any) the log is from
xp = item.cookingXP
for i, tree in ipairs(SkillData.Woodcutting.Trees) do
req = reqSet
if tree.logID == item.id then
qty = item.cookingQty
skill = 'Woodcutting'
time = item.cookingInterval / 1000
lvl = tree.levelRequired
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
time = tree.baseInterval / 1000
xp = tree.baseExperience
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
break
end
end
end
end
end
if item.herbloreReq ~= nil then
-- Cooking
skill = 'Herblore'
if item.canEat then
req = item.herbloreReq
for i, recipe in ipairs(SkillData.Cooking.Recipes) do
--Currently using 'masteryID' as shorthand to find details, could be a better method
if recipe.itemID == item.id or recipe.perfectCookID == item.id then
local potionID = item.masteryID[2]
skill = 'Cooking'
local potionData = SkillData.Herblore.ItemData[potionID + 1]
lvl = recipe.level
lvl = potionData.level
xp = recipe.baseXP
xp = potionData.herbloreXP
req = recipe.itemCosts
time = 2
qty = recipe.baseQuantity
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
time = recipe.baseInterval / 1000
end
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
if item.masteryID ~= nil and item.masteryID[1] == 4 then
break
skill = 'Mining'
end
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
specialReq = Icons.Icon({"Mastery", notext='true'})..' 271 total [[Mining]] [[Mastery]]'
end
end
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, nil, specialReq))
end
end
if item.type == "Logs" then
-- Farming
--Well this feels like cheating, but for as long as logs are the first items by ID it works
if item.type == 'Herb' or item.type == 'Logs' or (item.type == 'Food' and item.category ~= 'Cooking') then
local treeData = SkillData.Woodcutting.Trees[item.id + 1]
-- Herb means farming
skill = 'Woodcutting'
-- Logs/Food might mean farming or might not. Depends on the item
lvl = treeData.level
for i, seed in ipairs(ItemData.Items) do
time = treeData.interval / 1000
if seed.grownItemID ~= nil and seed.grownItemID == item.id then
xp = treeData.xp
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
end
if item.fishingLevel ~= nil then
skill = 'Fishing'
lvl = item.fishingLevel
xp = item.fishingXP
time = item.minFishingInterval/1000
maxTime = item.maxFishingInterval/1000
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime))
end
--had to add cooking to the list of valid categories here to account for cherries/apples
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
--Logs might mean farming or might not. Depends on the logs
for i, item2 in pairs(ItemData.Items) do
if item2.grownItemID == item.id then
skill = 'Farming'
skill = 'Farming'
lvl = item2.farmingLevel
lvl = seed.farmingLevel
xp = item2.farmingXP
xp = seed.farmingXP
time = item2.timeToGrow
time = seed.timeToGrow
if item.type == 'Logs' then
if item.type == 'Logs' then
qty = 35
qty = 35
Line 145: Line 218:
qty = 15
qty = 15
end
end
req = {{id = i - 1, qty = (item2.seedsRequired ~= nil and item2.seedsRequired or 1)}}
req = {{id = seed.id, qty = (seed.seedsRequired ~= nil and seed.seedsRequired or 1)}}
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
break
break
end
end
--If this is a perfect item, need to find the original
if item2.perfectItem == item.id and item2.recipeRequirements ~= nil then
for j, reqSet in pairs(item2.recipeRequirements) do
skill = 'Cooking'
lvl = item2.cookingLevel
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 item.summoningLevel ~= nil then
 
skill = 'Summoning'
-- Alt. Magic, excludes Gems and Bars
lvl = item.summoningLevel
-- Bars are handled by getItemSuperheatTable()
--Summoning uses a formula to calculate XP for creation instead of referring to the item's XP data directly
-- Gems are handled by _getItemLootSourceTable()
xp = (5 + 2 * math.floor(item.summoningLevel / 5))
for i, altSpell in ipairs(MagicData.AltMagic) do
local ShardCostArray = {}
if type(altSpell.produces) == 'number' and altSpell.produces == item.id then
for j, cost in Shared.skpairs(item.summoningReq[1]) do
table.insert(tables, p._buildAltMagicTable(altSpell))
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
req = table.concat(ShardCostArray, ', ')
req = req..'<br/>\r\nand one of the following<br/>\r\n'
local OtherCostArray = {}
local recipeGPCost = SkillData.Summoning.Settings.recipeGPCost
for j, altCost in Shared.skpairs(item.summoningReq) do
local nonShardArray = {}
for k, cost in Shared.skpairs(altCost) do
if cost.id >= 0 then
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
else
if cost.id == -4 then
  table.insert(nonShardArray, Icons.GP(recipeGPCost))
elseif cost.id == -5 then
  table.insert(nonShardArray, Icons.SC(recipeGPCost))
end
end
end
table.insert(OtherCostArray, table.concat(nonShardArray, ', '))
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
--A couple special exceptions for Alt Magic
--Not Gems or Bars though since those have their own separate thing
if item.name == 'Rune Essence' then
table.insert(tables, p.buildAltMagicTable('Just Learning'))
elseif item.name == 'Bones' then
table.insert(tables, p.buildAltMagicTable('Bone Offering'))
elseif item.name == 'Holy Dust' then
table.insert(tables, p.buildAltMagicTable('Blessed Offering'))
end
end


Line 223: Line 241:
end
end


function p.buildAltMagicTable(spellName)
function p.getAltMagicTable(frame)
local spellName = frame.args ~= nil and frame.args[1] or frame
local spell = Magic.getSpell(spellName, 'AltMagic')
local spell = Magic.getSpell(spellName, 'AltMagic')
if spell == nil then
return 'ERROR: Could not find Alt. Magic spell "' .. spellName .. '"[[Category:Pages with script errors]]'
else
return p._buildAltMagicTable(spell)
end
end
function p._buildAltMagicTable(spell)
local resultPart = {}
local resultPart = {}
table.insert(resultPart, '{|class="wikitable"\r\n|-')
table.insert(resultPart, '{|class="wikitable"\r\n|-')
Line 230: Line 257:
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Requirements')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Requirements')
table.insert(resultPart, '\r\n|'..Icons._SkillReq('Magic', spell.level))
table.insert(resultPart, '\r\n|'..Icons._SkillReq('Magic', spell.level))
-- 1 means select any item. 0 would mean Superheat, but that's handled elsewhere
 
-- -1 means no item is needed, so hide this section
-- The produces property of Alt. Magic spells is as follows:
if spell.selectItem == 1 then
-- -3 = A random gem, using the same weights as Mining
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials')
-- -2 = A bar of the type being created (Superheat)
table.insert(resultPart, '\r\n|1 of any item')
-- -1 = GP (Alchemy)
--  0 = Undefined
-- >0 = Item ID of the item being produced
-- The amount produced is determined by the productionRatio property
 
-- The consumes property of Alt. Magic spells is as follows:
-- 0 = Any item
-- 1 = Junk item
-- 2 = Superheat/ores with Coal
-- 3 = Superheat/ores without Coal
-- 4 = Nothing
-- 5 = Coal ore
-- Superheat (2, 3) is handled by _getItemSuperheatTable()
if spell.consumes ~= nil then
local consumeText = {
'1 of any item',
'1 of any [[Fishing#Junk|Junk]] item',
'1 x required ores for the chosen bar',
'1 x required ores (except ' .. Icons.Icon({'Coal Ore', type='item'}) .. ') for the chosen bar',
nil,
Icons.Icon({'Coal Ore', type='item', qty=1})
}
local consumeStr = consumeText[spell.consumes + 1]
if consumeStr ~= nil then
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials')
table.insert(resultPart, '\r\n| ' .. consumeStr)
end
end
end
--Add runes
--Add runes
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Runes\r\n|')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Runes\r\n| ' .. Magic._getSpellRunes(spell))
for i, req in pairs(spell.runesRequired) do
local rune = Items.getItemByID(req.id)
if i > 1 then table.insert(resultPart, ', ') end
table.insert(resultPart, Icons.Icon({rune.name, type='item', notext=true, qty=req.qty}))
end
if spell.runesRequiredAlt ~= nil and Shared.tableCount(spell.runesRequired) ~= Shared.tableCount(spell.runesRequiredAlt) then
table.insert(resultPart, "<br/>'''OR'''<br/>")
for i, req in pairs(spell.runesRequiredAlt) do
local rune = Items.getItemByID(req.id)
if i > 1 then table.insert(resultPart, ', ') end
table.insert(resultPart, Icons.Icon({rune.name, type='item', notext=true, qty=req.qty}))
end
end


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


function p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime, specialReq, gpCost)
function p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime, specialReq, gpCost, scCost)
if qty == nil then qty = 1 end
if qty == nil then qty = 1 end
local resultPart = {}
local resultPart = {}
Line 286: Line 327:
end
end
if gpCost ~= nil and gpCost > 0 then
if gpCost ~= nil and gpCost > 0 then
table.insert(resultPart, '<br/>')
table.insert(resultPart, '<br/>')
table.insert(resultPart, Icons.GP(gpCost))
table.insert(resultPart, Icons.GP(gpCost))
end
if scCost ~= nil and scCost > 0 then
table.insert(resultPart, '<br/>')
table.insert(resultPart, Icons.SC(scCost))
end
end
else
else
table.insert(resultPart, req)
table.insert(resultPart, req)
end
end
end
end
Line 299: Line 344:
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Creation Time')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Creation Time')
table.insert(resultPart, '\r\n|'..Shared.formatnum(Shared.round(time, 2, 0))..'s')
table.insert(resultPart, '\r\n|'..Shared.formatnum(Shared.round(time, 2, 0))..'s')
if maxTime ~= nil then table.insert(resultPart, ' - '..Shared.formatnum(Shared.round(maxTime, 2, 0))..'s') end
if maxTime ~= nil and maxTime > time then table.insert(resultPart, ' - '..Shared.formatnum(Shared.round(maxTime, 2, 0))..'s') end
table.insert(resultPart, '\r\n|}')
table.insert(resultPart, '\r\n|}')


Line 322: Line 367:
--First up: Can we kill somebody and take theirs?
--First up: Can we kill somebody and take theirs?
local killStrPart = {}
local killStrPart = {}
local dungeonStrPart = {}
for i, monster in ipairs(MonsterData.Monsters) do
for i, monster in ipairs(MonsterData.Monsters) do
local isDrop, isBones = false, false
local isDrop = false
if monster.bones == item.id and Monsters.getMonsterBones(monster) ~= nil then
if monster.bones == 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
isDrop = true
isBones = true
elseif monster.lootTable ~= nil then
elseif monster.lootTable ~= nil 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:
for j, loot in ipairs(monster.lootTable) do
--  - A monster before the boss, which doesn't drop anything except shards (checked above)
    if loot[1] == item.id then
--  - A boss monster, whose drops are accounted for in data from Areas instead
  isDrop = true
for j, loot in ipairs(monster.lootTable) do
  break
if loot[1] == item.id and not Monsters._isDungeonOnlyMonster(monster) then
  end
isDrop = true
break
end
end
end
end
end
if isDrop then
if isDrop then
if not isBones and Monsters._isDungeonOnlyMonster(monster) then
-- Item drops when the monster is killed
-- For dungeon exclusive monsters, loot is only rolled when they are the last
table.insert(killStrPart, Icons.Icon({monster.name, type='monster', notext=true}))
-- monster within that dungeon (unless it is a shard)
end
if monster.isBoss then
end
local areaList = Areas.getMonsterAreas(monster.id)
-- Is the item dropped from any dungeon?
for k, area in ipairs(areaList) do
local dungeonStrPart = {}
if area.type == 'dungeon' and area.monsters[#area.monsters] == monster.id then
local dungeonList = Areas.getAreas(function(area) return area.type == 'dungeon' and type(area.rewards) == 'table' and Shared.contains(area.rewards, item.id) end)
table.insert(dungeonStrPart, Icons.Icon({area.name, type='dungeon', notext=true}))
if dungeonList ~= nil then
end
for i, dungeon in ipairs(dungeonList) do
end
table.insert(dungeonStrPart, Icons.Icon({dungeon.name, type='dungeon', notext=true}))
end
else
-- Item is not an end of dungeon reward, and drops when the monster is killed
table.insert(killStrPart, Icons.Icon({monster.name, type='monster', notext=true}))
end
end
end
end
end
-- Is the item dropped from a cycle of the Impending Darkness event?
-- Is the item dropped from a cycle of the Impending Darkness event?
for i, eventItemID in ipairs(Areas.eventData.rewards) do
for i, eventItemID in ipairs(Areas.eventData.rewards) do
if item.id == eventItemID then
if item.id == eventItemID then
local dungPrefix = (i == Shared.tableCount(Areas.eventData.rewards) and '' or i .. ' ' .. (i == 1 and 'cycle' or 'cycles') .. ' of ')
local dungPrefix = (i == Shared.tableCount(Areas.eventData.rewards) and '' or i .. ' ' .. (i == 1 and 'cycle' or 'cycles') .. ' of ')
table.insert(dungeonStrPart, dungPrefix .. Icons.Icon({'Impending Darkness Event', type='dungeon', notext=true}))
table.insert(dungeonStrPart, dungPrefix .. Icons.Icon({'Impending Darkness Event', type='dungeon', notext=true}))
break
break
end
end
end
end
-- Special exceptions for Fire/Infernal Cape and first two lore books
-- Special exceptions for lore books
if sourceOverrides['Dungeon'][item.id] ~= nil then
if sourceOverrides['Dungeon'][item.id] ~= nil then
table.insert(dungeonStrPart, Icons.Icon({sourceOverrides['Dungeon'][item.id], type='dungeon', notext=true}))
table.insert(dungeonStrPart, Icons.Icon({sourceOverrides['Dungeon'][item.id], type='dungeon', notext=true}))
Line 377: Line 418:


--Next: Can we find it in a box?
--Next: Can we find it in a box?
--While we're here, check for upgrades, originals (for perfect items), and growing
--While we're here, check for upgrades, and growing
local lootStr = ''
local lootPart, upgradePart, growPart = {}, {}, {}
local upgradeStr = ''
local cookStr = ''
local growStr = ''
local count1 = 0
local count2 = 0
for i, item2 in pairs(ItemData.Items) do
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[1] == 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
if item2.trimmedItemID ~= nil and item2.trimmedItemID == item.id then
count2 = count2 + 1
table.insert(upgradePart, Icons.Icon({item2.name, type='item', notext=true}))
if string.len(upgradeStr) > 0 then
upgradeStr = upgradeStr..','
--if count2 % 3 == 1 and count2 > 1 then upgradeStr = upgradeStr..'<br/>' end
upgradeStr = upgradeStr..Icons.Icon({item2.name, type="item", notext="true"})
else
table.insert(categoryArray, '[[Category:Upgraded Items]]')
upgradeStr = upgradeStr..'Upgrading: '..Icons.Icon({item2.name, type="item", notext="true"})
end
end
end
if item2.grownItemID == item.id then
if item2.grownItemID == item.id then
if string.len(growStr) > 0 then
-- Farming
growStr = growStr..','..Icons.Icon({item2.name, type="item", notext="true"})
table.insert(growPart, Icons.Icon({item2.name, type='item', notext=true}))
else
table.insert(categoryArray, '[[Category:Harvestable Items]]')
growStr = growStr..'Growing: '..Icons.Icon({item2.name, type="item", notext="true"})
end
end
if item2.perfectItem == item.id and item2.cookingLevel ~= nil then
table.insert(lineArray, Icons._SkillReq('Cooking', item2.cookingLevel))
end
end
end
end
if string.len(lootStr) > 0 then table.insert(lineArray, lootStr) end
if #lootPart > 0 then
if string.len(upgradeStr) > 0 then table.insert(lineArray, upgradeStr) end
table.insert(lineArray, 'Opening: ' .. table.concat(lootPart, ','))
if string.len(cookStr) > 0 then table.insert(lineArray, cookStr) end
end
if string.len(growStr) > 0 then table.insert(lineArray, growStr) end
if #upgradePart > 0 then
table.insert(categoryArray, '[[Category:Upgraded Items]]')
table.insert(lineArray, 'Upgrading: ' .. table.concat(upgradePart, ','))
end
if #growPart > 0 then
table.insert(categoryArray, '[[Category:Harvestable Items]]')
table.insert(lineArray, 'Growing: ' .. table.concat(growPart, ','))
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 thiefPart = {}
thiefStr = 'Pickpocketing: '
for i, thiefRow in ipairs(thiefItems) do
for i, thiefRow in pairs(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
else
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'})
end
end
end
if #thiefPart > 0 then
table.insert(lineArray, 'Pickpocketing: ' .. table.concat(thiefPart, ','))
end
end
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
--AstrologyCheck (Just a brute force for now because only two items)
--(Just a brute force for now because only two items and I'm lazy)
if Shared.contains({'Stardust', 'Golden Stardust'}, item.name) then
if item.name == 'Stardust' or item.name == 'Golden Stardust' then
table.insert(lineArray, Icons.Icon({'Astrology', type='skill'}))
table.insert(lineArray, Icons.Icon({"Astrology", type="skill"}))
end
end


--SmithCheck:
-- Sources discoverable through mastery IDs
if item.smithingLevel ~= nil then
-- Does _not_ handle:
table.insert(lineArray, Icons._SkillReq("Smithing", item.smithingLevel))
-- Fishing: Junk, special items
end
-- Cooking: perfect items
if type(item.masteryID) == 'table' then
local skillID, masteryID = item.masteryID[1], item.masteryID[2]
local skill = Constants.getSkillName(skillID)
local keyData = {
[SkillEnum.Fishing] = { ["recipe"] = 'Fish' },
[SkillEnum.Mining] = { ["recipe"] = 'Rocks', ["level"] = 'levelRequired', ["item"] = 'oreID' },
[SkillEnum.Smithing] = {},
[SkillEnum.Fletching] = {},
[SkillEnum.Crafting] = {},
[SkillEnum.Runecrafting] = {},
[SkillEnum.Herblore] = { ["recipe"] = 'Potions', ["item"] = 'potionIDs', ["isItemList"] = true },
[SkillEnum.Summoning] = { ["recipe"] = 'Marks' }
}
local keys = keyData[skillID]
if type(keys) == 'table' then
if keys.recipe == nil then
keys.recipe = 'Recipes'
end
if keys.level == nil then
keys.level = 'level'
end
if keys.item == nil then
keys.item = 'itemID'
end
if keys.isItemList == nil then
keys.isItemList = false
end


--CraftCheck:
local recipe = SkillData[skill][keys.recipe][masteryID + 1]
if item.craftingLevel ~= nil then
if recipe ~= nil and (
table.insert(lineArray, Icons._SkillReq("Crafting", item.craftingLevel))
(not keys.isItemList and recipe[keys.item] == item.id) or
(keys.isItemList and Shared.contains(recipe[keys.item], item.id))) then
local levelReq = recipe[keys.level]
if levelReq ~= nil then
table.insert(lineArray, Icons._SkillReq(skill, levelReq))
end
end
end
end
end


--FletchCheck:
-- Woodcutting
if item.fletchingLevel ~= nil then
for i, tree in ipairs(SkillData.Woodcutting.Trees) do
table.insert(lineArray, Icons._SkillReq("Fletching", item.fletchingLevel))
if tree.logID == item.id then
table.insert(lineArray, Icons._SkillReq('Woodcutting', tree.levelRequired))
break
end
end
end


--RunecraftCheck:
-- Woodcutting: Nests
if item.runecraftingLevel ~= nil then
if item.name == 'Bird Nest' then
table.insert(lineArray, Icons._SkillReq("Runecrafting", item.runecraftingLevel))
table.insert(lineArray, Icons._SkillReq('Woodcutting', 1))
end
end


--CookCheck
-- Fishing: Junk & Specials
if item.cookingLevel ~= nil and item.recipeRequirements ~= nil then
if Shared.contains(SkillData.Fishing.JunkItems, item.id) then
table.insert(lineArray, Icons._SkillReq('Cooking', item.cookingLevel))
table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Junk|Junk]]')
else
for i, specialItem in ipairs(SkillData.Fishing.SpecialItems) do
if specialItem[1] == item.id then
table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Special|Special]]')
break
end
end
end
end


--MineCheck:
-- Cooking
if item.masteryID ~= nil and item.masteryID[1] == 4 then
if item.canEat then
table.insert(lineArray, Icons._SkillReq("Mining", SkillData.Mining.Rocks[item.masteryID[2] + 1].levelRequired))
for i, recipe in ipairs(SkillData.Cooking.Recipes) do
if recipe.itemID == item.id or recipe.perfectCookID == item.id then
table.insert(lineArray, Icons._SkillReq('Cooking', recipe.level))
break
end
end
end
end


--FishCheck:
-- Alt. Magic
if (item.category == "Fishing" and (item.type == "Junk" or item.type == "Special")) then
for i, altSpell in ipairs(MagicData.AltMagic) do
table.insert(lineArray, Icons.Icon({"Fishing", type='skill', notext=true})..' [[Fishing#'..item.type..'|'..item.type..']]')
if type(altSpell.produces) == 'number' and
elseif item.fishingLevel ~= nil then
((altSpell.produces == -3 and item.type == 'Gem' and item.name ~= 'Jadestone')
table.insert(lineArray, Icons._SkillReq("Fishing", item.fishingLevel))
or (altSpell.produces == -2 and item.type == 'Bar')
end
    or (altSpell.produces > 0 and altSpell.produces == item.id)) then
 
table.insert(lineArray, Icons.Icon({"Alt. Magic", type='skill'}))
--HerbCheck:
break
if item.masteryID ~= nil and item.masteryID[1] == 19 then
end
local potionData = SkillData.Herblore.ItemData[item.masteryID[2] + 1].level
table.insert(lineArray, Icons._SkillReq("Herblore", potionData))
end
 
--WoodcuttingCheck:
if item.type == 'Logs' then
local treeData = SkillData.Woodcutting.Trees[item.id + 1]
local lvl = treeData.level
table.insert(lineArray, Icons._SkillReq("Woodcutting", lvl))
end
 
--SummoningCheck:
if item.summoningLevel ~= nil then
table.insert(lineArray, Icons._SkillReq("Summoning", item.summoningLevel))
end
end


Line 512: Line 568:
end
end


--Gems can be acquired from mining, fishing, and alt. magic
--Gems can be acquired from Mining
if item.type == 'Gem' and item.name ~= 'Jadestone' then
if item.type == 'Gem' and item.name ~= 'Jadestone' then
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({"Mining", type='skill', notext=true})..' [[Mining#Gems|Gem]]')
table.insert(lineArray, Icons.Icon({"Alt. Magic", type='skill'}))
end
--Bars and some other stuff can also be acquired via Alt. Magic
if item.type == 'Bar' or Shared.contains(Items.AltMagicProducts, item.name) then
table.insert(lineArray, Icons.Icon({"Alt. Magic", type='skill'}))
end
end


Line 530: Line 579:
Jewel = {'Firemaking', 'Cooking', 'Smithing', 'Fletching', 'Crafting', 'Runecrafting', 'Herblore', 'Summoning'}
Jewel = {'Firemaking', 'Cooking', 'Smithing', 'Fletching', 'Crafting', 'Runecrafting', 'Herblore', 'Summoning'}
}
}
local rhaSkList = {}
local rhaSkList, subText = nil, ''
if item.name == 'Circlet of Rhaelyx' then
if item.name == 'Circlet of Rhaelyx' then
rhaSkList = rhaSkills.Circlet
rhaSkList = {rhaSkills.Circlet}
elseif item.name == 'Jewel of Rhaelyx' then
elseif item.name == 'Jewel of Rhaelyx' then
rhaSkList = rhaSkills.Jewel
rhaSkList = {rhaSkills.Jewel}
elseif item.name == 'Mysterious Stone' then
elseif item.name == 'Mysterious Stone' then
rhaSkList = rhaSkills.Jewel
rhaSkList = {rhaSkills.Jewel, rhaSkills.Circlet}
for i, v in ipairs(rhaSkills.Circlet) do
subText = '<br/>after finding ' .. Icons.Icon({'Crown of Rhaelyx', type='item'})
table.insert(rhaSkList, v)
end
end
end


local rhaStrPart = {}
local rhaStrPart = {}
for i, skillName in ipairs(rhaSkList) do
for i, skillList in ipairs(rhaSkList) do
table.insert(rhaStrPart, Icons.Icon({skillName, type='skill', notext=true}))
for j, skillName in ipairs(skillList) do
table.insert(rhaStrPart, Icons.Icon({skillName, type='skill', notext=true}))
end
end
end
local rhaStr = 'Any action in: ' .. table.concat(rhaStrPart, ', ')
table.insert(lineArray, 'Any action in: ' .. table.concat(rhaStrPart, ', ') .. subText)
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


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


Line 570: Line 622:
table.insert(lineArray, '[[Events]]')
table.insert(lineArray, '[[Events]]')
end
end
 
--Gold Topaz Ring drops from any action (when not wearing a Gold Topaz Ring)
--Gold Topaz Ring drops from any action (when not wearing a Gold Topaz Ring)
--Also handling Signet Ring things here
--Also handling Signet Ring things here
if item.name == 'Gold Topaz Ring' then
if item.name == 'Gold Topaz Ring' then
table.insert(lineArray, 'Any non-combat action if not worn (Instead of '..Icons.Icon({"Signet Ring Half (a)", type="item"})..')')
table.insert(lineArray, 'Any non-combat action if not worn (Instead of '..Icons.Icon({"Signet Ring Half (a)", type="item"})..')')
table.insert(lineArray, 'Killing any monster if not worn (Instead of '..Icons.Icon({"Signet Ring Half (b)", type="item"})..')')
table.insert(lineArray, 'Killing any monster if not worn (Instead of '..Icons.Icon({"Signet Ring Half (b)", type="item"})..')')
elseif item.name == 'Signet Ring Half (a)' then
elseif item.name == 'Signet Ring Half (a)' then
table.insert(lineArray, 'Any non-combat action while wearing '..Icons.Icon({'Gold Topaz Ring', type='item'}))
table.insert(lineArray, 'Any non-combat action while wearing '..Icons.Icon({'Gold Topaz Ring', type='item'}))
elseif item.name == 'Signet Ring Half (b)' then
elseif item.name == 'Signet Ring Half (b)' then
table.insert(lineArray, 'Killing any monster while wearing '..Icons.Icon({'Gold Topaz Ring', type='item'}))
table.insert(lineArray, 'Killing any monster while wearing '..Icons.Icon({'Gold Topaz Ring', type='item'}))
Line 622: Line 674:
table.insert(rowPart, '\r\n|style="text-align: left;"|'..type)
table.insert(rowPart, '\r\n|style="text-align: left;"|'..type)


table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="'..qty..'"|'..minqty)
table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="'..qty..'"|'..Shared.formatnum(minqty))
if qty ~= minqty then table.insert(rowPart, ' - '..qty) end
if qty ~= minqty then table.insert(rowPart, ' - '..Shared.formatnum(qty)) end
local chance = Shared.round(weight / totalWeight * 100, 2, 2)
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'
chance = 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
Line 630: Line 685:
table.insert(rowPart, '\r\n|colspan="2" ')
table.insert(rowPart, '\r\n|colspan="2" ')
else
else
local fraction = Shared.fraction(weight, totalWeight)
local fraction = Shared.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
--(happens sometimes with the rare thieving items)
--(happens sometimes with the rare thieving items)
table.insert(rowPart, '\r\n|colspan="2" ')
table.insert(rowPart, '\r\n|colspan="2" ')
else
else
table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="' .. chance .. '"| ' .. Shared.fraction(weight, totalWeight) .. '\r\n|')
table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="' .. chance .. '"| ' .. Shared.fraction(weight, totalWeight) .. '\r\n|')
end
end
Line 658: Line 713:
--Only add bones if this monster has loot (ie appears outside a dungeon) and isn't a boss
--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
--... 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
if monster.bones == item.id and Monsters._getMonsterBones(monster) ~= nil then
qty = monster.boneQty ~= nil and monster.boneQty or 1
qty = monster.boneQty ~= nil and monster.boneQty or 1
minqty = qty
minqty = qty
Line 664: Line 719:
totalWt = 1
totalWt = 1
elseif monster.lootTable ~= nil then
elseif monster.lootTable ~= nil 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:
--  - 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
for j, loot in ipairs(monster.lootTable) do
totalWt = totalWt + loot[2]
totalWt = totalWt + loot[2]
if loot[1] == item.id then
if loot[1] == item.id and not Monsters._isDungeonOnlyMonster(monster) then
wt = loot[2]
wt = loot[2]
qty = loot[3]
qty = loot[3]
Line 675: Line 733:
local lootChance = monster.lootChance ~= nil and monster.bones ~= item.id and monster.lootChance or 100
local lootChance = monster.lootChance ~= nil and monster.bones ~= item.id and monster.lootChance or 100


if wt > 0 and lootChance > 0 then
if wt > 0 and lootChance > 0 then
if not Shared.contains(item.name, 'Shard') and Monsters._isDungeonOnlyMonster(monster) then
-- Item drops when the monster is killed
-- For dungeon exclusive monsters, loot is only rolled when they are the last
table.insert(dropRows, {source = Icons.Icon({monster.name, type='monster'}), type = '[[Monster]]', minqty = minqty, qty = qty, weight = wt * lootChance, totalWeight = totalWt * 100})
-- monster within that dungeon (unless it is a shard)
if monster.isBoss then
local areaList = Areas.getMonsterAreas(monster.id)
for k, area in ipairs(areaList) do
  if area.type == 'dungeon' and area.monsters[#area.monsters] == monster.id then
table.insert(dropRows, {source = Icons.Icon({area.name, type='dungeon'}), type = '[[Dungeon]]', minqty = minqty, qty = qty, weight = wt * lootChance, totalWeight = totalWt * 100})
end
end
end
else
-- Item is not an end of dungeon reward, and drops when the monster is killed
table.insert(dropRows, {source = Icons.Icon({monster.name, type='monster'}), type = '[[Monster]]', minqty = minqty, qty = qty, weight = wt * lootChance, totalWeight = totalWt * 100})
end
end
end
end
-- 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)
if dungeonList ~= nil then
for i, dungeon in ipairs(dungeonList) do
table.insert(dropRows, {source = Icons.Icon({dungeon.name, type='dungeon'}), type = '[[Dungeon]]', minqty = 1, qty = 1, weight = 1, totalWeight = 1})
end
end
end
-- Is the item dropped from a cycle of the Impending Darkness event?
-- Is the item dropped from a cycle of the Impending Darkness event?
Line 708: Line 760:


--Next: Can we find it by rummaging around in another item?
--Next: Can we find it by rummaging around in another item?
for i, item2 in pairs(ItemData.Items) do
for i, item2 in ipairs(ItemData.Items) do
if item2.dropTable ~= nil then
if item2.dropTable ~= nil then
local qty = 1
local qty = 1
Line 730: Line 782:
--Finally, let's try just stealing it
--Finally, let's try just stealing it
local thiefType = Icons.Icon({"Thieving", type='skill'})
local thiefType = Icons.Icon({"Thieving", type='skill'})
local thiefItems = GatheringSkills.getThievingSourcesForItem(item.id)
local thiefItems = Skills.getThievingSourcesForItem(item.id)
for i, thiefRow in pairs(thiefItems) do
for i, thiefRow in ipairs(thiefItems) do
local sourceTxt = ''
local sourceTxt = ''
if thiefRow.npc == 'all' then
if thiefRow.npc == 'all' then
Line 737: Line 789:
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})
table.insert(dropRows, {source = sourceTxt, type = thiefType, minqty = thiefRow.minQty, qty = thiefRow.maxQty, weight = thiefRow.wt, totalWeight = thiefRow.totalWt})
end
end


--Bonus overtime: Special Fishing table & mining gem table. Also Rags to Riches
--Bonus overtime: Special Fishing table & mining gem table. Also Rags to Riches
if Shared.contains(SkillData.Fishing.JunkItems, item.id) then
local fishSource = '[[Fishing#Junk|Junk]]'
local fishType = Icons.Icon({'Fishing', type='skill'})
local fishTotWeight = Shared.tableCount(SkillData.Fishing.JunkItems)
table.insert(dropRows, {source = fishSource, type = fishType, minqty = 1, qty = 1, weight = 1, totalWeight = fishTotWeight})
else
local fishTotWeight, fishItem = 0, nil
for i, specialItem in ipairs(SkillData.Fishing.SpecialItems) do
if specialItem[1] == item.id then
fishItem = specialItem
end
fishTotWeight = fishTotWeight + specialItem[2]
end
if fishItem ~= nil then
local fishSource = '[[Fishing#Special|Special]]'
local fishType = Icons.Icon({'Fishing', type='skill'})
table.insert(dropRows, {source = fishSource, type = fishType, minqty = fishItem[3], qty = fishItem[3], weight = fishItem[2], totalWeight = fishTotWeight})
end
end
--Jadestone is special and doesn't count
--Jadestone is special and doesn't count
if item.type == 'Gem' and item.name ~= 'Jadestone' then
if item.type == 'Gem' and item.name ~= 'Jadestone' then
Line 752: Line 823:
table.insert(dropRows, {source = '[[Mining#Gems|Gem]]', type = mineType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
table.insert(dropRows, {source = '[[Mining#Gems|Gem]]', type = mineType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
local magicType = Icons.Icon({'Magic', type = 'skill'})
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})
for i, altSpell in ipairs(MagicData.AltMagic) do
table.insert(dropRows, {source = Icons.Icon({"Rags to Riches II", type="spell"}), type = magicType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
if type(altSpell.produces) == 'number' and altSpell.produces == -3 then
end
table.insert(dropRows, {source = Icons.Icon({altSpell.name, type='spell'}), type = magicType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
 
end
if item.fishingCatchWeight ~= nil then
end
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
 
if item.type == 'Junk' then
local fishSource = '[[Fishing#Junk|Junk]]'
local fishType = Icons.Icon({'Fishing', type='skill'})
table.insert(dropRows, {source = fishSource, type = fishType, minqty = 1, qty = 1, weight = 1, totalWeight = Items.junkCount})
end
end


Line 772: Line 834:


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
              else
return (a.type == b.type and a.source < b.source) or a.type < b.type
                return a.weight / a.totalWeight > b.weight / b.totalWeight
else
              end
return a.minqty + a.qty > b.minqty + b.qty
            end)
end
else
return a.weight / a.totalWeight > b.weight / b.totalWeight
end
end)
for i, data in pairs(dropRows) do
for i, data in pairs(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.type, data.minqty, data.qty, data.weight, data.totalWeight))
Line 810: Line 876:
table.insert(materials, Icons.Icon({mat.name, type='item', qty=row[2]}))
table.insert(materials, Icons.Icon({mat.name, type='item', qty=row[2]}))
end
end
if item.trimmedGPCost ~= nil then
if type(item.trimmedGPCost) == 'number' and item.trimmedGPCost > 0 then
table.insert(materials, Icons.GP(item.trimmedGPCost))
table.insert(materials, Icons.GP(item.trimmedGPCost))
end
end
Line 835: Line 901:
function p._getItemSuperheatTable(item)
function p._getItemSuperheatTable(item)
--Manually build the Superheat Item table
--Manually build the Superheat Item table
local oreString = ''
-- Validate that the item can be superheated
local coalString = ''
local canSuperheat, smithRecipe = true, nil
for i, mat in pairs(item.smithReq) do
if type(item.masteryID) ~= 'table' then
canSuperheat = false
elseif item.masteryID[1] ~= SkillEnum.Smithing then
canSuperheat = false
else
smithRecipe = SkillData.Smithing.Recipes[item.masteryID[2] + 1]
if smithRecipe == nil or smithRecipe.category ~= 0 then
canSuperheat = false
end
end
if not canSuperheat then
return 'ERROR: The item "' .. item.name .. '" cannot be superheated[[Category:Pages with script errors]]'
end
 
local oreStringPart, coalString = {}, ''
for i, mat in ipairs(smithRecipe.itemCosts) do
local thisMat = Items.getItemByID(mat.id)
local thisMat = Items.getItemByID(mat.id)
if thisMat.name == 'Coal Ore' then
if thisMat.name == 'Coal Ore' then
coalString = ', '..Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty})
coalString = Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty})
else
else
if string.len(oreString) > 0 then oreString = oreString..', ' end
table.insert(oreStringPart, Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty}))
oreString =  oreString..Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty})
end
end
end
end
Line 858: Line 938:
for i, sName in pairs(spellNames) do
for i, sName in pairs(spellNames) do
local spell = Magic.getSpell(sName, 'AltMagic')
local spell = Magic.getSpell(sName, 'AltMagic')
local rowPart = {}
table.insert(superheatTable, '\r\n|-\r\n|'..Icons.Icon({spell.name, type='spell', notext=true, size=50}))
table.insert(rowPart, '\r\n|-\r\n|'..Icons.Icon({spell.name, type='spell', notext=true, size=50}))
table.insert(superheatTable, '||'..Icons.Icon({spell.name, type='spell', noicon=true})..'||style="text-align:right;"|'..smithRecipe.level)
table.insert(rowPart, '||'..Icons.Icon({spell.name, type='spell', noicon=true})..'||style="text-align:right;"|'..item.smithingLevel)
table.insert(superheatTable, '||style="text-align:right;"|'..spell.level..'||style="text-align:right;"|'..spell.baseExperience)
table.insert(rowPart, '||style="text-align:right;"|'..spell.level..'||style="text-align:right;"|'..spell.magicXP..'||style="text-align:right;"|'..spell.convertToQty)
table.insert(superheatTable, '||style="text-align:right;"|'..spell.productionRatio)
table.insert(rowPart, '||'..oreString)
table.insert(superheatTable, '|| '..table.concat(oreStringPart, ', '))
if spell.ignoreCoal ~= nil and not spell.ignoreCoal then table.insert(rowPart, coalString) end
if spell.consumes == 2 and coalString ~= '' then
table.insert(rowPart, '||style="text-align:center"|')
-- 2 = Superheat with coal, 3 = Superheat without coal
for i, req in pairs(spell.runesRequired) do
table.insert(superheatTable, (Shared.tableCount(oreStringPart) > 0 and ', ' or '') .. coalString)
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
end
table.insert(rowPart, "<br/>'''OR'''<br/>")
table.insert(superheatTable, '||style="text-align:center"| ' .. Magic._getSpellRunes(spell))
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
--Add the table end and add the table to the result string
Line 913: Line 984:
end
end


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


Line 955: Line 1,029:
return table.concat(resultPart)
return table.concat(resultPart)
end
end
function p._getItemMonsterSources(item)
local resultArray = {}
for i, monster in ipairs(MonsterData.Monsters) do
local chance = 0
local weight = 0
local minQty = 1
local maxQty = 1
if monster.bones == 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
chance = 1
weight = 1
if monster.boneQty ~= nil then
minQty = monster.boneQty
maxQty = monster.boneQty
end
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
weight = weight + loot[2]
if loot[1] == item.id and not Monsters._isDungeonOnlyMonster(monster) then
chance = loot[2]
maxQty = loot[3]
end
end
local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
chance = chance * lootChance
weight = weight * 100
chance, weight = Shared.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
--[==[
-- Uncomment this block and execute 'p.test()' within the debug console
-- to test after making changes
function p.test()
local checkItems = {
'Gold Bar',
'Raw Shrimp',
'Coal Ore',
'Rune Platebody',
'Arrow Shafts',
'Yew Longbow',
'Water Rune',
'Steam Rune',
'Controlled Heat Potion II',
'Wolf',
'Cyclops',
'Leprechaun',
'Redwood Logs',
'Carrot Cake',
'Carrot Cake (Perfect)',
'Mantalyme Herb',
'Carrot',
'Topaz',
'Rune Essence',
'Sanguine Blade',
'Ring of Power',
'Infernal Claw',
'Chapeau Noir',
'Stardust',
'Rope',
'Ancient Ring of Mastery',
'Mysterious Stone',
'Mastery Token (Cooking)',
'Gem Gloves',
"Thief's Moneysack"
}
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

Revision as of 16:23, 21 May 2022

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

local p = {}

local MonsterData = mw.loadData('Module:Monsters/data')
local ItemData = mw.loadData('Module:Items/data')
local SkillData = mw.loadData('Module:Skills/data')
local MagicData = mw.loadData('Module:Magic/data')

local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local Magic = require('Module:Magic')
local Areas = require('Module:CombatAreas')
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 SkillEnum = mw.loadData('Module:Constants/data').skill
-- Implements overrides for sources which cannot be obtained from game data
-- Currently only overrides for dungeon sources are implemented here
local sourceOverrides = {
	['Dungeon'] = {
		[950] = 'Volcanic Cave', -- A Tale of the Past, a future's prophecy
		[951] = 'Fire God Dungeon', -- The First Hero and an Unknown Evil
		[1116] = 'Into the Mist' -- Beginning of the End
	}
}

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

	local tables = {}
	--First figure out what skill is used to make this...
	if type(item.masteryID) == 'table' then
		local skillID, masteryID = item.masteryID[1], item.masteryID[2]
		skill = Constants.getSkillName(skillID)
		if skillID == SkillEnum.Fishing then
			-- Fishing
			local fish = SkillData.Fishing.Fish[masteryID + 1]
			if fish ~= nil and fish.itemID == item.id then
				lvl = fish.level
				xp = fish.baseXP
				qty = 1
				time = fish.baseMinInterval / 1000
				maxTime = fish.baseMaxInterval / 1000
				table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime))
			end
		elseif skillID == SkillEnum.Mining then
			-- Mining
			local rock = SkillData.Mining.Rocks[masteryID + 1]
			if rock ~= nil then
				lvl = rock.levelRequired
				xp = rock.baseExperience
				qty = rock.baseQuantity
				time = 3
				if item.name == 'Dragonite Ore' then
					specialReq = Icons.Icon({"Mastery", notext='true'})..' 271 total [[Mining]] [[Mastery]]'
				end
				table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, nil, specialReq))
			end
		elseif Shared.contains({SkillEnum.Smithing, SkillEnum.Fletching, SkillEnum.Crafting,
								SkillEnum.Runecrafting, SkillEnum.Herblore}, skillID) then
			-- Smithing, Fletching, Crafting, Runecrafting, Herblore
			-- All have somewhat consistent recipe data structures
			local recipeKey = (skillID == SkillEnum.Herblore and 'Potions') or 'Recipes'
			local recipe = SkillData[skill][recipeKey][masteryID + 1]
			if recipe ~= nil then
				local masteryReq = nil
				local itemMatch = (recipe.itemID == item.id)
				if skillID == SkillEnum.Herblore then
					-- For Herblore, we need to check a table of potion IDs & determine the mastery requirement
					for i, potionID in ipairs(recipe.potionIDs) do
						if potionID == item.id then
							itemMatch = true
							masteryReq = SkillData.Herblore.TierMasteryLevels[i]
							break
						end
					end
				end
				if itemMatch then
					local baseTime = {
						[SkillEnum.Smithing] = 2,
						[SkillEnum.Fletching] = 2,
						[SkillEnum.Crafting] = 3,
						[SkillEnum.Runecrafting] = 2,
						[SkillEnum.Herblore] = 2
					}
					local baseQty = recipe.baseQuantity or 1
					lvl = recipe.level
					xp = recipe.baseXP
					time = baseTime[skillID]
					if masteryReq ~= nil and masteryReq > 1 then
						specialReq = Icons._MasteryReq(item.name, masteryReq, false)
					end
					-- Some items (such as Arrow shafts) have multiple recipes
					if type(recipe.alternativeCosts) == 'table' then
						local reqPart, qtyPart = {}, {}
						for i, altCost in ipairs(recipe.alternativeCosts) do
							local reqSubPart = {}
							for j, itemCost in ipairs(altCost.itemCosts) do
								local reqItem = Items.getItemByID(itemCost.id)
								if reqItem == nil then
									table.insert(reqSubPart, itemCost.qty .. 'x ?????')
								else
									table.insert(reqSubPart, Icons.Icon({reqItem.name, type='item', qty=itemCost.qty}))
								end
							end
							if recipe.gpCost ~= nil and recipe.gpCost > 0 then
								table.insert(reqSubPart, Icons.GP(recipe.gpCost))
							end
							if recipe.scCost ~= nil and recipe.scCost > 0 then
								table.insert(reqSubPart, Icons.GP(recipe.scCost))
							end
							table.insert(reqPart, table.concat(reqSubPart, ', '))
							table.insert(qtyPart, Shared.formatnum(baseQty * altCost.quantityMultiplier))
						end
						local sep = "<br/>'''OR''' "
						req = table.concat(reqPart, sep)
						qty = table.concat(qtyPart, sep)
						table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime, specialReq))
					elseif type(recipe.itemCosts) == 'table' and Shared.tableCount(recipe.itemCosts) > 0 then
						req = recipe.itemCosts
						table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, baseQty, time, maxTime, specialReq, recipe.gpCost, recipe.scCost))
					end
				end
			end
		elseif skillID == SkillEnum.Summoning then
			-- Summoning
			local recipe = SkillData.Summoning.Marks[masteryID + 1]
			if recipe ~= nil and recipe.itemID == item.id then
				lvl = recipe.level
				xp = recipe.baseXP
				qty = recipe.baseQuantity
				time = 5
				-- Create item requirements text
				local ShardCostArray, OtherCostArray = {}, {}
				-- Shards
				for j, cost in ipairs(recipe.itemCosts) do
					local shard = Items.getItemByID(cost.id)
					if shard ~= nil then
						table.insert(ShardCostArray, Icons.Icon({shard.name, type='item', notext=true, qty=cost.qty}))
					end
				end
				-- Other costs
				local recipeGPCost = SkillData.Summoning.RecipeGPCost
				if recipe.gpCost > 0 then
					table.insert(OtherCostArray, Icons.GP(recipe.gpCost))
				end
				if recipe.scCost > 0 then
					table.insert(OtherCostArray, Icons.SC(recipe.scCost))
				end
				for j, nonShardID in ipairs(recipe.nonShardItemCosts) do
					local nonShard = Items.getItemByID(nonShardID)
					if nonShard ~= nil then
						local itemValue = math.max(nonShard.sellsFor, 20)
						local nonShardQty = math.max(1, math.floor(recipeGPCost / itemValue))
						table.insert(OtherCostArray, Icons.Icon({nonShard.name, type='item', notext=true, qty=nonShardQty}))
					end
				end
				req = table.concat(ShardCostArray, ', ')
				if #OtherCostArray > 0 then
					req = req .. '<br/>and one of the following:<br/>' .. table.concat(OtherCostArray, "<br/>'''OR''' ")
				end
				table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
			end
		end
	end

	-- Woodcutting
	if item.type == 'Logs' then
		-- Determine which tree (if any) the log is from
		for i, tree in ipairs(SkillData.Woodcutting.Trees) do
			if tree.logID == item.id then
				skill = 'Woodcutting'
				lvl = tree.levelRequired
				time = tree.baseInterval / 1000
				xp = tree.baseExperience
				table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
				break
			end
		end
	end
	-- Cooking
	if item.canEat then
		for i, recipe in ipairs(SkillData.Cooking.Recipes) do
			if recipe.itemID == item.id or recipe.perfectCookID == item.id then
				skill = 'Cooking'
				lvl = recipe.level
				xp = recipe.baseXP
				req = recipe.itemCosts
				qty = recipe.baseQuantity
				time = recipe.baseInterval / 1000
				table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
				break
			end
		end
	end
	-- Farming
	if item.type == 'Herb' or item.type == 'Logs' or (item.type == 'Food' and item.category ~= 'Cooking') then
		-- Herb means farming
		-- Logs/Food might mean farming or might not. Depends on the item
		for i, seed in ipairs(ItemData.Items) do
			if seed.grownItemID ~= nil and seed.grownItemID == item.id then
				skill = 'Farming'
				lvl = seed.farmingLevel
				xp = seed.farmingXP
				time = seed.timeToGrow
				if item.type == 'Logs' then
					qty = 35
				else
					qty = 15
				end
				req = {{id = seed.id, qty = (seed.seedsRequired ~= nil and seed.seedsRequired or 1)}}
				table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
				break
			end
		end
	end

	-- Alt. Magic, excludes Gems and Bars
	-- Bars are handled by getItemSuperheatTable()
	-- Gems are handled by _getItemLootSourceTable()
	for i, altSpell in ipairs(MagicData.AltMagic) do
		if type(altSpell.produces) == 'number' and altSpell.produces == item.id then
			table.insert(tables, p._buildAltMagicTable(altSpell))
		end
	end

	if Shared.tableCount(tables) == 0 then
		return ""
	else
		return table.concat(tables, '\r\n')
	end
end

function p.getAltMagicTable(frame)
	local spellName = frame.args ~= nil and frame.args[1] or frame
	local spell = Magic.getSpell(spellName, 'AltMagic')
	if spell == nil then
		return 'ERROR: Could not find Alt. Magic spell "' .. spellName .. '"[[Category:Pages with script errors]]'
	else
		return p._buildAltMagicTable(spell)
	end
end

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

	-- The produces property of Alt. Magic spells is as follows:
	-- -3 = A random gem, using the same weights as Mining
	-- -2 = A bar of the type being created (Superheat)
	-- -1 = GP (Alchemy)
	--  0 = Undefined
	-- >0 = Item ID of the item being produced
	-- The amount produced is determined by the productionRatio property

	-- The consumes property of Alt. Magic spells is as follows:
	-- 0 = Any item
	-- 1 = Junk item
	-- 2 = Superheat/ores with Coal
	-- 3 = Superheat/ores without Coal
	-- 4 = Nothing
	-- 5 = Coal ore
	-- Superheat (2, 3) is handled by _getItemSuperheatTable()
	if spell.consumes ~= nil then
		local consumeText = {
			'1 of any item',
			'1 of any [[Fishing#Junk|Junk]] item',
			'1 x required ores for the chosen bar',
			'1 x required ores (except ' .. Icons.Icon({'Coal Ore', type='item'}) .. ') for the chosen bar',
			nil,
			Icons.Icon({'Coal Ore', type='item', qty=1})
		}
		local consumeStr = consumeText[spell.consumes + 1]
		if consumeStr ~= nil then
			table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials')
			table.insert(resultPart, '\r\n| ' .. consumeStr)
		end
	end

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

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

function p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime, specialReq, gpCost, scCost)
	if qty == nil then qty = 1 end
	local resultPart = {}
	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
	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
		table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Materials\r\n|')
		if type(req) == 'table' then
			for i, mat in pairs(req) do
				if i > 1 then table.insert(resultPart, '<br/>') end
				local matItem = Items.getItemByID(mat.id)
				if matItem == nil then
					table.insert(resultPart, mat.qty..'x ?????')
				else
					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
			if scCost ~= nil and scCost > 0 then
				table.insert(resultPart, '<br/>')
				table.insert(resultPart, Icons.SC(scCost))
			end
		else
			table.insert(resultPart, req)
		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 and maxTime > time then table.insert(resultPart, ' - '..Shared.formatnum(Shared.round(maxTime, 2, 0))..'s') end
	table.insert(resultPart, '\r\n|}')

	return table.concat(resultPart)
end

function p.getCreationTable(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = Items.getItem(itemName)
	if item == nil then
		return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
	end

	return p._getCreationTable(item)
end

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

	--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(MonsterData.Monsters) do
		local isDrop = false
		if monster.bones == 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.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[1] == item.id and not Monsters._isDungeonOnlyMonster(monster) then
					isDrop = true
					break
				end
			end
		end
		if isDrop then
			-- Item drops when the monster is killed
			table.insert(killStrPart, Icons.Icon({monster.name, type='monster', notext=true}))
		end
	end
	-- Is the item dropped from any dungeon?
	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)
	if dungeonList ~= nil then
		for i, dungeon in ipairs(dungeonList) do
			table.insert(dungeonStrPart, Icons.Icon({dungeon.name, type='dungeon', notext=true}))
		end
	end
	-- Is the item dropped from a cycle of the Impending Darkness event?
	for i, eventItemID in ipairs(Areas.eventData.rewards) do
		if item.id == eventItemID then
			local dungPrefix = (i == Shared.tableCount(Areas.eventData.rewards) and '' or i .. ' ' .. (i == 1 and 'cycle' or 'cycles') .. ' of ')
			table.insert(dungeonStrPart, dungPrefix .. Icons.Icon({'Impending Darkness Event', type='dungeon', notext=true}))
			break
		end
	end
	-- Special exceptions for lore books
	if sourceOverrides['Dungeon'][item.id] ~= nil then
		table.insert(dungeonStrPart, Icons.Icon({sourceOverrides['Dungeon'][item.id], type='dungeon', notext=true}))
	end

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

	--Next: Can we find it in a box?
	--While we're here, check for upgrades, and growing
	local lootPart, upgradePart, growPart = {}, {}, {}
	for i, item2 in pairs(ItemData.Items) do
		if item2.dropTable ~= nil then
			for j, loot in ipairs(item2.dropTable) do
				if loot[1] == item.id then
					table.insert(lootPart, Icons.Icon({item2.name, type='item', notext=true}))
					break
				end
			end
		end
		if item2.trimmedItemID ~= nil and item2.trimmedItemID == item.id then
			table.insert(upgradePart, Icons.Icon({item2.name, type='item', notext=true}))
		end
		if item2.grownItemID == item.id then
			-- Farming
			table.insert(growPart, Icons.Icon({item2.name, type='item', notext=true}))
		end
	end
	if #lootPart > 0 then
		table.insert(lineArray, 'Opening: ' .. table.concat(lootPart, ','))
	end
	if #upgradePart > 0 then
		table.insert(categoryArray, '[[Category:Upgraded Items]]')
		table.insert(lineArray, 'Upgrading: ' .. table.concat(upgradePart, ','))
	end
	if #growPart > 0 then
		table.insert(categoryArray, '[[Category:Harvestable Items]]')
		table.insert(lineArray, 'Growing: ' .. table.concat(growPart, ','))
	end

	--Next: Can we take it from somebody else -without- killing them?
	local thiefItems = Skills.getThievingSourcesForItem(item.id)
	if type(thiefItems) == 'table' then
		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))
			else
				table.insert(thiefPart, Icons.Icon({thiefRow.npc, type='thieving', notext=true}))
			end
		end
		if #thiefPart > 0 then
			table.insert(lineArray, 'Pickpocketing: ' .. table.concat(thiefPart, ','))
		end
	end

	--Check if we can make it ourselves
	--AstrologyCheck (Just a brute force for now because only two items)
	if Shared.contains({'Stardust', 'Golden Stardust'}, item.name) then
		table.insert(lineArray, Icons.Icon({'Astrology', type='skill'}))
	end

	-- Sources discoverable through mastery IDs
	-- Does _not_ handle:
	-- Fishing: Junk, special items
	-- Cooking: perfect items
	if type(item.masteryID) == 'table' then
		local skillID, masteryID = item.masteryID[1], item.masteryID[2]
		local skill = Constants.getSkillName(skillID)
		local keyData = {
			[SkillEnum.Fishing] = { ["recipe"] = 'Fish' },
			[SkillEnum.Mining] = { ["recipe"] = 'Rocks', ["level"] = 'levelRequired', ["item"] = 'oreID' },
			[SkillEnum.Smithing] = {},
			[SkillEnum.Fletching] = {},
			[SkillEnum.Crafting] = {},
			[SkillEnum.Runecrafting] = {},
			[SkillEnum.Herblore] = { ["recipe"] = 'Potions', ["item"] = 'potionIDs', ["isItemList"] = true },
			[SkillEnum.Summoning] = { ["recipe"] = 'Marks' }
		}
		local keys = keyData[skillID]
		if type(keys) == 'table' then
			if keys.recipe == nil then
				keys.recipe = 'Recipes'
			end
			if keys.level == nil then
				keys.level = 'level'
			end
			if keys.item == nil then
				keys.item = 'itemID'
			end
			if keys.isItemList == nil then
				keys.isItemList = false
			end

			local recipe = SkillData[skill][keys.recipe][masteryID + 1]
			if recipe ~= nil and (
				(not keys.isItemList and recipe[keys.item] == item.id) or
				(keys.isItemList and Shared.contains(recipe[keys.item], item.id))) then
				local levelReq = recipe[keys.level]
				if levelReq ~= nil then
					table.insert(lineArray, Icons._SkillReq(skill, levelReq))
				end
			end
		end
	end

	-- Woodcutting
	for i, tree in ipairs(SkillData.Woodcutting.Trees) do
		if tree.logID == item.id then
			table.insert(lineArray, Icons._SkillReq('Woodcutting', tree.levelRequired))
			break
		end
	end

	-- Woodcutting: Nests
	if item.name == 'Bird Nest' then
		table.insert(lineArray, Icons._SkillReq('Woodcutting', 1))
	end

	-- Fishing: Junk & Specials
	if Shared.contains(SkillData.Fishing.JunkItems, item.id) then
		table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Junk|Junk]]')
	else
		for i, specialItem in ipairs(SkillData.Fishing.SpecialItems) do
			if specialItem[1] == item.id then
				table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Special|Special]]')
				break
			end
		end
	end

	-- Cooking
	if item.canEat then
		for i, recipe in ipairs(SkillData.Cooking.Recipes) do
			if recipe.itemID == item.id or recipe.perfectCookID == item.id then
				table.insert(lineArray, Icons._SkillReq('Cooking', recipe.level))
				break
			end
		end
	end

	-- Alt. Magic
	for i, altSpell in ipairs(MagicData.AltMagic) do
		if type(altSpell.produces) == 'number' and
			((altSpell.produces == -3 and item.type == 'Gem' and item.name ~= 'Jadestone')
			or (altSpell.produces == -2 and item.type == 'Bar')
		    or (altSpell.produces > 0 and altSpell.produces == item.id)) then
			table.insert(lineArray, Icons.Icon({"Alt. Magic", type='skill'}))
			break
		end
	end

	--Finally there are some weird exceptions:
	--Coal can be acquired via firemaking
	if item.name == "Coal Ore" then
		table.insert(lineArray, Icons._SkillReq("Firemaking", 1))
	end

	--Gems can be acquired from Mining
	if item.type == 'Gem' and item.name ~= 'Jadestone' then
		table.insert(lineArray, Icons.Icon({"Mining", type='skill', notext=true})..' [[Mining#Gems|Gem]]')
	end

	--Rhaelyx pieces are also special
	if Shared.contains({'Circlet of Rhaelyx', 'Jewel of Rhaelyx', 'Mysterious Stone'}, item.name) then
		local rhaSkills = {
			Circlet = {'Woodcutting', 'Fishing', 'Mining', 'Thieving', 'Farming', 'Agility', 'Astrology'},
			Jewel = {'Firemaking', 'Cooking', 'Smithing', 'Fletching', 'Crafting', 'Runecrafting', 'Herblore', 'Summoning'}
		}
		local rhaSkList, subText = nil, ''
		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, rhaSkills.Circlet}
			subText = '<br/>after finding ' .. Icons.Icon({'Crown of Rhaelyx', type='item'})
		end

		local rhaStrPart = {}
		for i, skillList in ipairs(rhaSkList) do
			for j, skillName in ipairs(skillList) do
				table.insert(rhaStrPart, Icons.Icon({skillName, type='skill', notext=true}))
			end
		end
		table.insert(lineArray, 'Any action in: ' .. table.concat(rhaStrPart, ', ') .. subText)
	end

	--Tokens are from the appropriate skill
	if item.isToken and item.skill ~= nil then
		table.insert(lineArray, Icons._SkillReq(Constants.getSkillName(item.skill), 1))
	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)
	local shopSources = Shop.getItemSourceArray(item.id)
	if Shared.tableCount(shopSources) > 0 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

	--Gold Topaz Ring drops from any action (when not wearing a Gold Topaz Ring)
	--Also handling Signet Ring things here
	if item.name == 'Gold Topaz Ring' then
		table.insert(lineArray, 'Any non-combat action if not worn (Instead of '..Icons.Icon({"Signet Ring Half (a)", type="item"})..')')
		table.insert(lineArray, 'Killing any monster if not worn (Instead of '..Icons.Icon({"Signet Ring Half (b)", type="item"})..')')
	elseif item.name == 'Signet Ring Half (a)' then
		table.insert(lineArray, 'Any non-combat action while wearing '..Icons.Icon({'Gold Topaz Ring', type='item'}))
	elseif item.name == 'Signet Ring Half (b)' then
		table.insert(lineArray, 'Killing any monster while wearing '..Icons.Icon({'Gold Topaz Ring', type='item'}))
	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 "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
	end

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

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

	--Set up function for adding rows
	local buildRow = function(source, type, minqty, qty, weight, totalWeight)
		if minqty == nil then minqty = 1 end
		local rowPart = {}
		table.insert(rowPart, '\r\n|-')
		table.insert(rowPart, '\r\n|style="text-align: left;"|'..source)
		table.insert(rowPart, '\r\n|style="text-align: left;"|'..type)

		table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="'..qty..'"|'..Shared.formatnum(minqty))
		if qty ~= minqty then table.insert(rowPart, ' - '..Shared.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'
		chance = string.format(fmt, chance)
		if weight >= totalWeight then
			-- Fraction would be 1/1, so only show the percentage
			chance = 100
			table.insert(rowPart, '\r\n|colspan="2" ')
		else
			local fraction = Shared.fraction(weight, totalWeight)
			if Shared.contains(fraction, '%.') then
				--If fraction contains decimals, something screwy happened so just show only percentage
				--(happens sometimes with the rare thieving items)
				table.insert(rowPart, '\r\n|colspan="2" ')
			else
				table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="' .. chance .. '"| ' .. Shared.fraction(weight, totalWeight) .. '\r\n|')
			end
		end
		if weight == -1 then
			--Weight of -1 means this is a weird row that has a variable percentage
			table.insert(rowPart, 'style="text-align: right;" data-sort-value="0"|Varies (see Thieving page)')
		else
			table.insert(rowPart, 'style="text-align: right;" data-sort-value="'.. chance .. '"|'..chance..'%')
		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, monster in ipairs(MonsterData.Monsters) do
		local minqty = 1
		local qty = 1
		local wt = 0
		local totalWt = 0
		--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
		local lootChance = monster.lootChance ~= nil and monster.bones ~= item.id and monster.lootChance or 100

		if wt > 0 and lootChance > 0 then
			-- Item drops when the monster is killed
			table.insert(dropRows, {source = Icons.Icon({monster.name, type='monster'}), type = '[[Monster]]', minqty = minqty, qty = qty, weight = wt * lootChance, totalWeight = totalWt * 100})
		end
	end
	-- 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)
	if dungeonList ~= nil then
		for i, dungeon in ipairs(dungeonList) do
			table.insert(dropRows, {source = Icons.Icon({dungeon.name, type='dungeon'}), type = '[[Dungeon]]', minqty = 1, qty = 1, weight = 1, totalWeight = 1})
		end
	end
	-- Is the item dropped from a cycle of the Impending Darkness event?
	for i, eventItemID in ipairs(Areas.eventData.rewards) do
		if item.id == eventItemID then
			sourceTxt = Icons.Icon({'Impending Darkness Event', type='dungeon'}) .. (i == Shared.tableCount(Areas.eventData.rewards) and '' or ', Cycle ' .. i)
			table.insert(dropRows, {source = sourceTxt, type = '[[Dungeon]]', minqty = 1, qty = 1, weight = 1, totalWeight = 1})
			break
		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

	--Next: Can we find it by rummaging around in another item?
	for i, item2 in ipairs(ItemData.Items) do
		if item2.dropTable ~= nil then
			local qty = 1
			local wt = 0
			local totalWt = 0
			for j, loot in pairs(item2.dropTable) do
				totalWt = totalWt + loot[2]
				if loot[1] == item.id then
					wt = loot[2]
					if item2.dropQty ~= nil then qty = item2.dropQty[j] end
				end
			end

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

	--Finally, let's try just stealing it
	local thiefType = Icons.Icon({"Thieving", type='skill'})
	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
		table.insert(dropRows, {source = sourceTxt, type = thiefType, minqty = thiefRow.minQty, qty = thiefRow.maxQty, weight = thiefRow.wt, totalWeight = thiefRow.totalWt})
	end

	--Bonus overtime: Special Fishing table & mining gem table. Also Rags to Riches
	if Shared.contains(SkillData.Fishing.JunkItems, item.id) then
		local fishSource = '[[Fishing#Junk|Junk]]'
		local fishType = Icons.Icon({'Fishing', type='skill'})
		local fishTotWeight = Shared.tableCount(SkillData.Fishing.JunkItems)
		table.insert(dropRows, {source = fishSource, type = fishType, minqty = 1, qty = 1, weight = 1, totalWeight = fishTotWeight})
	else
		local fishTotWeight, fishItem = 0, nil
		for i, specialItem in ipairs(SkillData.Fishing.SpecialItems) do
			if specialItem[1] == item.id then
				fishItem = specialItem
			end
			fishTotWeight = fishTotWeight + specialItem[2]
		end
		if fishItem ~= nil then
			local fishSource = '[[Fishing#Special|Special]]'
			local fishType = Icons.Icon({'Fishing', type='skill'})
			table.insert(dropRows, {source = fishSource, type = fishType, minqty = fishItem[3], qty = fishItem[3], weight = fishItem[2], totalWeight = fishTotWeight})
		end
	end
	--Jadestone is special and doesn't count
	if item.type == 'Gem' and item.name ~= 'Jadestone' then
		local mineType = Icons.Icon({'Mining', type='skill'})
		local thisGemChance = Items.GemTable[item.name].chance
		local totalGemChance = 0
		for i, gem in pairs(Items.GemTable) do
			totalGemChance = totalGemChance + gem.chance
		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'})
		for i, altSpell in ipairs(MagicData.AltMagic) do
			if type(altSpell.produces) == 'number' and altSpell.produces == -3 then
				table.insert(dropRows, {source = Icons.Icon({altSpell.name, type='spell'}), type = magicType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
			end
		end
	end

	--Make sure to return nothing if there are no drop sources
	if Shared.tableCount(dropRows) == 0 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.type == b.type and a.source < b.source) or a.type < b.type
								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 pairs(dropRows) do
		table.insert(resultPart, buildRow(data.source, data.type, data.minqty, data.qty, data.weight, data.totalWeight))
	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 "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
	end

	return p._getItemLootSourceTable(item)
end

function p._getItemUpgradeTable(item)
	local resultPart = {}
	if item.itemsRequired ~= nil then
		--First, get details on all the required materials
		local upgradeFrom = {}
		local materials = {}
		for i, row in pairs(item.itemsRequired) do
			local mat = Items.getItemByID(row[1])
			--Check to see if the source item can trigger the upgrade
			if mat.canUpgrade or (mat.type == 'Armour' and mat.canUpgrade == nil) then
				table.insert(upgradeFrom, Icons.Icon({mat.name, type='item'}))
			end
			table.insert(materials, Icons.Icon({mat.name, type='item', qty=row[2]}))
		end
		if type(item.trimmedGPCost) == 'number' and item.trimmedGPCost > 0 then
			table.insert(materials, Icons.GP(item.trimmedGPCost))
		end
		table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="2"|[[Upgrading Items|Item Upgrade]]')
		--[[result = result..'\r\n|-\r\n!style="text-align:right;"|Upgrades From\r\n|'
		result = result..table.concat(upgradeFrom, '<br/>')--]]
		table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials\r\n|')
		table.insert(resultPart, table.concat(materials, '<br/>'))
		table.insert(resultPart, '\r\n|}')
	end
	return table.concat(resultPart)
end

function p.getItemUpgradeTable(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = Items.getItem(itemName)
	if item == nil then
		return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
	end

	return p._getItemUpgradeTable(item)
end

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

	local oreStringPart, coalString = {}, ''
	for i, mat in ipairs(smithRecipe.itemCosts) do
		local thisMat = Items.getItemByID(mat.id)
		if thisMat.name == 'Coal Ore' then
			coalString = Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty})
		else
			table.insert(oreStringPart, Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty}))
		end
	end
	--Set up the header
	local superheatTable = {}
	table.insert(superheatTable, '{|class="wikitable"\r\n!colspan="2"|Spell')
	table.insert(superheatTable, '!!'..Icons.Icon({'Smithing', type='skill', notext='true'})..' Level')
	table.insert(superheatTable, '!!'..Icons.Icon({'Magic', type='skill', notext='true'})..' Level')
	table.insert(superheatTable, '!!'..Icons.Icon({'Magic', type='skill', notext='true'})..' XP')
	table.insert(superheatTable, '!!'..Icons.Icon({item.name, type='item', notext='true'})..' Bars')
	table.insert(superheatTable, '!!Ore!!Runes')
	 --Loop through all the variants
	local spellNames = {'Superheat I', 'Superheat II', 'Superheat III', 'Superheat IV'}
	for i, sName in pairs(spellNames) do
		local spell = Magic.getSpell(sName, 'AltMagic')
		table.insert(superheatTable, '\r\n|-\r\n|'..Icons.Icon({spell.name, type='spell', notext=true, size=50}))
		table.insert(superheatTable, '||'..Icons.Icon({spell.name, type='spell', noicon=true})..'||style="text-align:right;"|'..smithRecipe.level)
		table.insert(superheatTable, '||style="text-align:right;"|'..spell.level..'||style="text-align:right;"|'..spell.baseExperience)
		table.insert(superheatTable, '||style="text-align:right;"|'..spell.productionRatio)
		table.insert(superheatTable, '|| '..table.concat(oreStringPart, ', '))
		if spell.consumes == 2 and coalString ~= '' then
			-- 2 = Superheat with coal, 3 = Superheat without coal
			table.insert(superheatTable, (Shared.tableCount(oreStringPart) > 0 and ', ' or '') .. coalString)
		end
		table.insert(superheatTable, '||style="text-align:center"| ' .. Magic._getSpellRunes(spell))
	end
	 --Add the table end and add the table to the result string
	table.insert(superheatTable, '\r\n|}')
	return table.concat(superheatTable)
end

function p.getItemSuperheatTable(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = Items.getItem(itemName)
	if item == nil then
		return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
	end

	return p._getItemSuperheatTable(item)
end

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

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

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

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

	local lootTable = p._getItemLootSourceTable(item)
	if string.len(lootTable) > 0 then
		if #resultPart > 0 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 "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
	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 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
		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, '| '..item.description..'\r\n')
	end

	table.insert(resultPart, '|}')

	return table.concat(resultPart)
end

function p._getItemMonsterSources(item)
	local resultArray = {}
	for i, monster in ipairs(MonsterData.Monsters) do
		local chance = 0
		local weight = 0
		local minQty = 1
		local maxQty = 1
		if monster.bones == 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
			chance = 1
			weight = 1
			if monster.boneQty ~= nil then
				minQty = monster.boneQty
				maxQty = monster.boneQty
			end
		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
				weight = weight + loot[2]
				if loot[1] == item.id and not Monsters._isDungeonOnlyMonster(monster) then
					chance = loot[2]
					maxQty = loot[3]
				end
			end
			local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
			chance = chance * lootChance
			weight = weight * 100
			chance, weight = Shared.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

--[==[
-- Uncomment this block and execute 'p.test()' within the debug console
-- to test after making changes
function p.test()
	local checkItems = {
		'Gold Bar',
		'Raw Shrimp',
		'Coal Ore',
		'Rune Platebody',
		'Arrow Shafts',
		'Yew Longbow',
		'Water Rune',
		'Steam Rune',
		'Controlled Heat Potion II',
		'Wolf',
		'Cyclops',
		'Leprechaun',
		'Redwood Logs',
		'Carrot Cake',
		'Carrot Cake (Perfect)',
		'Mantalyme Herb',
		'Carrot',
		'Topaz',
		'Rune Essence',
		'Sanguine Blade',
		'Ring of Power',
		'Infernal Claw',
		'Chapeau Noir',
		'Stardust',
		'Rope',
		'Ancient Ring of Mastery',
		'Mysterious Stone',
		'Mastery Token (Cooking)',
		'Gem Gloves',
		"Thief's Moneysack"
	}
	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