Module:Items/SourceTables

From Melvor Idle
< Module:Items
Revision as of 19:11, 8 December 2021 by Falterfire (talk | contribs) (Updated indenting)
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

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 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 GatheringSkills = require('Module:Skills/Gathering')

-- Implements overrides for sources which cannot be obtained from game data
-- Currently only overrides for dungeon sources are implemented here
local sourceOverrides = {
	['Dungeon'] = {
		[361] = 'Volcanic Cave', -- Fire Cape
		[941] = 'Infernal Stronghold', -- Infernal Cape
		[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 item.smithingLevel ~= nil then
		skill = 'Smithing'
		lvl = item.smithingLevel
		xp = item.smithingXP
		req = item.smithReq
		qty = item.smithingQty
		time = 2
		table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
	end
	if item.craftingLevel ~= nil then
		skill = 'Crafting'
		lvl = item.craftingLevel
		xp = item.craftingXP
		req = item.craftReq
		qty = item.craftQty
		time = 3
		table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, nil, nil, item.craftGPCost))
	end
	if item.runecraftingLevel ~= nil then
		skill = 'Runecrafting'
		lvl = item.runecraftingLevel
		xp = item.runecraftingXP
		req = item.runecraftReq
		qty = item.runecraftQty
		time = 2
		table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
	end
	if item.fletchingLevel ~= nil then
		skill = 'Fletching'
		lvl = item.fletchingLevel
		xp = item.fletchingXP
		req = item.fletchReq
		qty = item.fletchQty
		time = 2
		if item.name == 'Arrow Shafts' then
			--Arrow Shafts get special (weird) treatment
			req = '1 of any [[Log]]'
			qty = '15 - 135'
		end
		table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
	end
	if item.cookingLevel ~= nil and item.recipeRequirements ~= nil then
		for i, reqSet in pairs(item.recipeRequirements) do
			skill = 'Cooking'
			lvl = item.cookingLevel
			xp = item.cookingXP
			req = reqSet
			qty = item.cookingQty
			time = item.cookingInterval / 1000
			table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
		end
	end
	if item.herbloreReq ~= nil then
		skill = 'Herblore'
		req = item.herbloreReq
		--Currently using 'masteryID' as shorthand to find details, could be a better method
		local potionID = item.masteryID[2]
		local potionData = SkillData.Herblore.ItemData[potionID + 1]
		lvl = potionData.level
		xp = potionData.herbloreXP
		time = 2
		table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
	end
	if item.masteryID ~= nil and item.masteryID[1] == 4 then
		skill = 'Mining'
		lvl = SkillData.Mining.Rocks[item.masteryID[2] + 1].levelRequired
		time = 3
		xp = item.miningXP
		--Rune Essence has double quantity, but that's a hard-coded thing in the game so it's hard-coded here
		if item.name == 'Rune Essence' then qty = 2 else qty = 1 end

		if item.name == 'Dragonite Ore' then
			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
	if item.type == "Logs" then
		--Well this feels like cheating, but for as long as logs are the first items by ID it works
		local treeData = SkillData.Woodcutting.Trees[item.id + 1]
		skill = 'Woodcutting'
		lvl = treeData.level
		time = treeData.interval / 1000
		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'
				lvl = item2.farmingLevel
				xp = item2.farmingXP
				time = item2.timeToGrow
				if item.type == 'Logs' then
					qty = 35
				else
					qty = 15
				end
				req = {{id = i - 1, qty = (item2.seedsRequired ~= nil and item2.seedsRequired or 1)}}
				table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
				break
			end

			--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
	if item.summoningLevel ~= nil then
		skill = 'Summoning'
		lvl = item.summoningLevel
		--Summoning uses a formula to calculate XP for creation instead of referring to the item's XP data directly
		xp = (5 + 2 * math.floor(item.summoningLevel / 5))
		local ShardCostArray = {}
		for j, cost in Shared.skpairs(item.summoningReq[1]) do
			if cost.id >= 0 then
				local item = Items.getItemByID(cost.id)
				if item.type == 'Shard' then
					table.insert(ShardCostArray, Icons.Icon({item.name,  type='item', notext=true, qty=cost.qty}))
				end
			end
		end
		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

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

function p.buildAltMagicTable(spellName)
	local spell = Magic.getSpell(spellName, 'AltMagic')
	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))
	-- 1 means select any item. 0 would mean Superheat, but that's handled elsewhere
	-- -1 means no item is needed, so hide this section
	if spell.selectItem == 1 then
		table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials')
		table.insert(resultPart, '\r\n|1 of any item')
	end
	--Add runes
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Runes\r\n|')
	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)
	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 XP\r\n|'..spell.magicXP)
	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)
	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
		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 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 = {}
	local dungeonStrPart = {}
	for i, monster in ipairs(MonsterData.Monsters) do
		local isDrop, isBones = false, 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
			isBones = true
			elseif monster.lootTable ~= nil then
				-- If the monster has a loot table, check if the item we are looking for is in there
					for j, loot in ipairs(monster.lootTable) do
					    if loot[1] == item.id then
					  	isDrop = true
					  	break
					  end
				end
			end
		if isDrop then
			if not isBones and Monsters._isDungeonOnlyMonster(monster) then
				-- For dungeon exclusive monsters, loot is only rolled when they are the last
				-- 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(dungeonStrPart, Icons.Icon({area.name, type='dungeon', notext=true}))
						end
					end
				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
	-- 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 Fire/Infernal Cape and first two lore books
	if sourceOverrides['Dungeon'][item.id] ~= nil then
		table.insert(dungeonStrPart, Icons.Icon({sourceOverrides['Dungeon'][item.id], type='dungeon', notext=true}))
	end

	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, originals (for perfect items), and growing
	local lootStr = ''
	local upgradeStr = ''
	local cookStr = ''
	local growStr = ''
	local count1 = 0
	local count2 = 0
	for i, item2 in pairs(ItemData.Items) do
		if item2.dropTable ~= nil then
			for j, loot in pairs(item2.dropTable) do
				if loot[1] == item.id then
					count1 = count1 + 1
					if string.len(lootStr) > 0 then
					  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
		if item2.trimmedItemID == item.id then
					count2 = count2 + 1
				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
		if item2.grownItemID == item.id then
				if string.len(growStr) > 0 then
					growStr = growStr..','..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
	if string.len(lootStr) > 0 then table.insert(lineArray, lootStr) end
	if string.len(upgradeStr) > 0 then table.insert(lineArray, upgradeStr) end
	if string.len(cookStr) > 0 then table.insert(lineArray, cookStr) end
	if string.len(growStr) > 0 then table.insert(lineArray, growStr) end

	--Next: Can we take it from somebody else -without- killing them?
	local thiefItems = GatheringSkills.getThievingSourcesForItem(item.id)
	local thiefStr = ''
	if Shared.tableCount(thiefItems) > 0 then
		thiefStr = 'Pickpocketing: '
		for i, thiefRow in pairs(thiefItems) do
			if thiefRow.npc == 'all' then
				--if 'any' is the npc, this is a rare item so just say 'Thieving level 1'
				thiefStr = Icons._SkillReq('Thieving', 1)
			else
				if i > 1 then thiefStr = thiefStr..', ' end
				thiefStr = thiefStr..Icons.Icon({thiefRow.npc, type='thieving', notext='true'})
			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
	--AstrologyCheck
	--(Just a brute force for now because only two items and I'm lazy)
	if item.name == 'Stardust' or item.name == 'Golden Stardust' then
		table.insert(lineArray, Icons.Icon({"Astrology", type="skill"}))
	end

	--SmithCheck:
	if item.smithingLevel ~= nil then
		table.insert(lineArray, Icons._SkillReq("Smithing", item.smithingLevel))
	end

	--CraftCheck:
	if item.craftingLevel ~= nil then
		table.insert(lineArray, Icons._SkillReq("Crafting", item.craftingLevel))
	end

	--FletchCheck:
	if item.fletchingLevel ~= nil then
		table.insert(lineArray, Icons._SkillReq("Fletching", item.fletchingLevel))
	end

	--RunecraftCheck:
	if item.runecraftingLevel ~= nil then
		table.insert(lineArray, Icons._SkillReq("Runecrafting", item.runecraftingLevel))
	end

	--CookCheck
	if item.cookingLevel ~= nil and item.recipeRequirements ~= nil then
		table.insert(lineArray, Icons._SkillReq('Cooking', item.cookingLevel))
	end

	--MineCheck:
	if item.masteryID ~= nil and item.masteryID[1] == 4 then
		table.insert(lineArray, Icons._SkillReq("Mining", SkillData.Mining.Rocks[item.masteryID[2] + 1].levelRequired))
	end

	--FishCheck:
	if (item.category == "Fishing" and (item.type == "Junk" or item.type == "Special")) then
		table.insert(lineArray, Icons.Icon({"Fishing", type='skill', notext=true})..' [[Fishing#'..item.type..'|'..item.type..']]')
	elseif item.fishingLevel ~= nil then
		table.insert(lineArray, Icons._SkillReq("Fishing", item.fishingLevel))
	end

	--HerbCheck:
	if item.masteryID ~= nil and item.masteryID[1] == 19 then
		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

	--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, fishing, and alt. magic
	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({"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

	--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 = {}
		if item.name == 'Circlet of Rhaelyx' then
			rhaSkList = rhaSkills.Circlet
		elseif item.name == 'Jewel of Rhaelyx' then
			rhaSkList = rhaSkills.Jewel
		elseif item.name == 'Mysterious Stone' then
			rhaSkList = rhaSkills.Jewel
			for i, v in ipairs(rhaSkills.Circlet) do
				table.insert(rhaSkList, v)
			end
		end

		local rhaStrPart = {}
		for i, skillName in ipairs(rhaSkList) do
			table.insert(rhaStrPart, Icons.Icon({skillName, type='skill', notext=true}))
		end
		local rhaStr = 'Any action in: ' .. table.concat(rhaStrPart, ', ')
		if item.name == 'Mysterious Stone' then rhaStr = rhaStr .. '<br/>after finding ' .. Icons.Icon({'Crown of Rhaelyx', type='item'}) end
		table.insert(lineArray, rhaStr)
	end

	--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

	--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..'"|'..minqty)
		if qty ~= minqty then table.insert(rowPart, ' - '..qty) end
		local chance = Shared.round(weight / totalWeight * 100, 2, 2)
		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
			for j, loot in ipairs(monster.lootTable) do
				totalWt = totalWt + loot[2]
				if loot[1] == item.id 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
		if not Shared.contains(item.name, 'Shard') and Monsters._isDungeonOnlyMonster(monster) then
		-- For dungeon exclusive monsters, loot is only rolled when they are the last
			-- 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
	-- 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 pairs(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 = GatheringSkills.getThievingSourcesForItem(item.id)
	for i, thiefRow in pairs(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
	--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'})
		table.insert(dropRows, {source = Icons.Icon({"Rags to Riches I", type="spell"}), type = magicType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
		table.insert(dropRows, {source = Icons.Icon({"Rags to Riches II", type="spell"}), type = magicType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
	end

	if item.fishingCatchWeight ~= nil then
		local fishSource = '[[Fishing#Special|Special]]'
		local fishType = Icons.Icon({'Fishing', type='skill'})
		table.insert(dropRows, {source = fishSource, type = fishType, minqty = 1, qty = 1, weight = item.fishingCatchWeight, totalWeight = Items.specialFishWt})
	end

	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

	--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
					                 return a.minqty + a.qty > b.minqty + b.qty
					               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 item.trimmedGPCost ~= nil 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
	local oreString = ''
	local coalString = ''
	for i, mat in pairs(item.smithReq) 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
			if string.len(oreString) > 0 then oreString = oreString..', ' end
			oreString =  oreString..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')
		local rowPart = {}
		table.insert(rowPart, '\r\n|-\r\n|'..Icons.Icon({spell.name, type='spell', notext=true, size=50}))
		table.insert(rowPart, '||'..Icons.Icon({spell.name, type='spell', noicon=true})..'||style="text-align:right;"|'..item.smithingLevel)
		table.insert(rowPart, '||style="text-align:right;"|'..spell.level..'||style="text-align:right;"|'..spell.magicXP..'||style="text-align:right;"|'..spell.convertToQty)
		table.insert(rowPart, '||'..oreString)
		if spell.ignoreCoal ~= nil and not spell.ignoreCoal then table.insert(rowPart, coalString) end
		table.insert(rowPart, '||style="text-align:center"|')
		for i, req in pairs(spell.runesRequired) 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(rowPart, "<br/>'''OR'''<br/>")
		for i, req in pairs(spell.runesRequiredAlt) do
			local rune = Items.getItemByID(req.id)
			if i > 1 then table.insert(rowPart, ', ') end
			table.insert(rowPart, Icons.Icon({rune.name, type='item', notext=true, qty=req.qty}))
		end
		table.insert(superheatTable, table.concat(rowPart))
	end
	 --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 item.type == 'Bar' then
		table.insert(resultPart, '\r\n==='..Icons.Icon({'Alt Magic', type='skill'})..'===\r\n'..p._getItemSuperheatTable(item))
	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

return p