Module:Skills/Gathering: Difference between revisions

From Melvor Idle
(_getThievingNPCLootTables: Correct visibility of total row for common drops table)
(Update for v1.0.2)
Line 20: Line 20:
for i, const in ipairs(SkillData.Astrology.Constellations) do
for i, const in ipairs(SkillData.Astrology.Constellations) do
if const.name == constName then
if const.name == constName then
return const
return const
end
end
end
end
Line 172: Line 172:
result = result..'||style="min-width:25px" data-sort-value="'..logName..'"|'..Icons.Icon({logName, type='item', notext=true, size=50})
result = result..'||style="min-width:25px" data-sort-value="'..logName..'"|'..Icons.Icon({logName, type='item', notext=true, size=50})
result = result..'||'..Icons.Icon({logName, type='item', noicon=true})
result = result..'||'..Icons.Icon({logName, type='item', noicon=true})
result = result..'||style="text-align:right"|'..tree.level
result = result..'||style="text-align:right"|'..tree.levelRequired
result = result..'||style="text-align:right"|'..tree.xp
result = result..'||style="text-align:right"|'..tree.baseExperience
result = result..'||style="text-align:right" data-sort-value="'..tree.interval..'"|'..Shared.timeString(tree.interval/1000, true)
result = result..'||style="text-align:right" data-sort-value="'..tree.baseInterval..'"|'..Shared.timeString(tree.baseInterval/1000, true)
local XPs = tree.xp / (tree.interval / 1000)
local XPs = tree.baseExperience / (tree.baseInterval / 1000)
local Log = Items.getItemByID(i - 1)
local Log = Items.getItemByID(tree.logID)
local GPs = Log.sellsFor / (tree.interval / 1000)
local GPs = Log.sellsFor / (tree.baseInterval / 1000)
result = result..'||style="text-align:right"|'..Shared.round(XPs, 2, 2)
result = result..'||style="text-align:right"|'..Shared.round(XPs, 2, 2)
result = result..'||style="text-align:right" data-sort-value="'..GPs..'"|'..Icons.GP(Shared.round(GPs, 2, 2))
result = result..'||style="text-align:right" data-sort-value="'..GPs..'"|'..Icons.GP(Shared.round(GPs, 2, 2))
Line 220: Line 220:
result = result..'\r\n|- class="headerRow-0"'
result = result..'\r\n|- class="headerRow-0"'
result = result..'\r\n!colspan="2"|Item!!Value'
result = result..'\r\n!colspan="2"|Item!!Value'
 
local itemArray = Items.getItems(function(item) return item.type == "Junk" end)
local itemArray = Items.getItems(function(item) return item.type == "Junk" end)


Line 308: Line 308:


local cookStr = "N/A"
local cookStr = "N/A"
if fish.cookingLevel ~= nil then  
if fish.cookingLevel ~= nil then
cookStr = fish.cookingLevel
cookStr = fish.cookingLevel
end
end
Line 361: Line 361:
npc = p.getThievingNPC(npc)
npc = p.getThievingNPC(npc)
end
end
 
local result = nil
local result = nil
for i, area in Shared.skpairs(SkillData.Thieving.Areas) do
for i, area in Shared.skpairs(SkillData.Thieving.Areas) do
Line 376: Line 376:
function p._getThievingNPCStat(npc, statName)
function p._getThievingNPCStat(npc, statName)
local result = nil
local result = nil
 
if statName == 'level' then
if statName == 'level' then
result = Icons._SkillReq('Thieving', npc.level)
result = Icons._SkillReq('Thieving', npc.level)
Line 387: Line 387:
result = npc[statName]
result = npc[statName]
end
end
 
if result == nil then
if result == nil then
result = ''
result = ''
end
end
 
return result
return result
end
end
Line 402: Line 402:
return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
end
end
 
return p._getThievingNPCStat(npc, statName)
return p._getThievingNPCStat(npc, statName)
end
end
Line 413: Line 413:
local thisItem = Items.getItemByID(drop.itemID)
local thisItem = Items.getItemByID(drop.itemID)
local odds = drop.chance
local odds = drop.chance
 
rareTxt = rareTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
rareTxt = rareTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
rareTxt = rareTxt..'||1||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
rareTxt = rareTxt..'||1||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
Line 426: Line 426:
local result = ''
local result = ''
local sectionTxt = {}
local sectionTxt = {}
 
--Five sections here: GP, normal loot, area loot, rare loot, and unique item
--Five sections here: GP, normal loot, area loot, rare loot, and unique item
--First up, GP:
--First up, GP:
local gpTxt = 'Successfully pickpocketing the '..npc.name..' will always give '..Icons.GP(1, npc.maxGP)
local gpTxt = 'Successfully pickpocketing the '..npc.name..' will always give '..Icons.GP(1, npc.maxGP)
table.insert(sectionTxt, gpTxt)
table.insert(sectionTxt, gpTxt)
 
--Next up, normal loot:
--Next up, normal loot:
--(Skip if no loot)
--(Skip if no loot)
Line 439: Line 439:
local lootChance = thievingNormalLootChance
local lootChance = thievingNormalLootChance
local lootValue = 0
local lootValue = 0
 
--First loop through to get the total weight so we have it for later
--First loop through to get the total weight so we have it for later
for i, loot in pairs(npc.lootTable) do
for i, loot in pairs(npc.lootTable) do
totalWt = totalWt + loot[2]
totalWt = totalWt + loot[2]
end
end
 
normalTxt = normalTxt..'\r\n{|class="wikitable sortable"'
normalTxt = normalTxt..'\r\n{|class="wikitable sortable"'
normalTxt = normalTxt..'\r\n!Item!!Qty'
normalTxt = normalTxt..'\r\n!Item!!Qty'
normalTxt = normalTxt..'!!Price!!colspan="2"|Chance'
normalTxt = normalTxt..'!!Price!!colspan="2"|Chance'
 
--Then sort the loot table by weight
--Then sort the loot table by weight
table.sort(npc.lootTable, function(a, b) return a[2] > b[2] end)
table.sort(npc.lootTable, function(a, b) return a[2] > b[2] end)
Line 460: Line 460:
end
end
normalTxt = normalTxt..'||style="text-align:right" data-sort-value="'..maxQty..'"|'
normalTxt = normalTxt..'||style="text-align:right" data-sort-value="'..maxQty..'"|'
 
if maxQty > 1 then
if maxQty > 1 then
normalTxt = normalTxt.. '1 - '
normalTxt = normalTxt.. '1 - '
end
end
normalTxt = normalTxt..Shared.formatnum(row[3])
normalTxt = normalTxt..Shared.formatnum(row[3])
 
--Adding price columns
--Adding price columns
local itemPrice = 0
local itemPrice = 0
Line 478: Line 478:
end
end
end
end
 
--Getting the drop chance
--Getting the drop chance
local dropChance = (row[2] / totalWt * lootChance)
local dropChance = (row[2] / totalWt * lootChance)
Line 490: Line 490:
end
end
normalTxt = normalTxt..'style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
normalTxt = normalTxt..'style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
 
--Adding to the average loot value based on price & dropchance
--Adding to the average loot value based on price & dropchance
lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((1 + maxQty) / 2))
lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((1 + maxQty) / 2))
Line 506: Line 506:
table.insert(sectionTxt, normalTxt)
table.insert(sectionTxt, normalTxt)
end
end
 
--After normal drops, add in rare drops
--After normal drops, add in rare drops
local rareTxt = '===Possible Rare Drops:===\r\nAny of these can be received after a successful pickpocket'
local rareTxt = '===Possible Rare Drops:===\r\nAny of these can be received after a successful pickpocket'
rareTxt = rareTxt..'\r\n'..p.getThievingGeneralRareTable()
rareTxt = rareTxt..'\r\n'..p.getThievingGeneralRareTable()
table.insert(sectionTxt, rareTxt)
table.insert(sectionTxt, rareTxt)
 
local areaTxt = '===Possible Area Unique Drops==='
local areaTxt = '===Possible Area Unique Drops==='
areaTxt = areaTxt..'\r\nAny Area Unique Drop is equally likely to be obtained after a successful pickpocket. '
areaTxt = areaTxt..'\r\nAny Area Unique Drop is equally likely to be obtained after a successful pickpocket. '
areaTxt = areaTxt..'\r\nEach Area Unique Drop is rolled for separately, so it is possible to receive multiple Area Unique Drops from a single action. '
areaTxt = areaTxt..'\r\nEach Area Unique Drop is rolled for separately, so it is possible to receive multiple Area Unique Drops from a single action. '
areaTxt = areaTxt..'The chance of receiving an Area Unique drop is tripled if the 95% Thieving Mastery Pool checkpoint is active.'
areaTxt = areaTxt..'The chance of receiving an Area Unique drop is tripled if the 95% Thieving Mastery Pool checkpoint is active.'
 
local area = p.getThievingNPCArea(npc)
local area = p.getThievingNPCArea(npc)
areaTxt = areaTxt..'\r\n{|class="wikitable sortable"'
areaTxt = areaTxt..'\r\n{|class="wikitable sortable"'
Line 540: Line 540:
areaTxt = areaTxt..'\r\n|}'
areaTxt = areaTxt..'\r\n|}'
table.insert(sectionTxt, areaTxt)
table.insert(sectionTxt, areaTxt)
 
if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID > -1 then
if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID > -1 then
local uniqueTxt = '===Possible NPC Unique Drop==='
local uniqueTxt = '===Possible NPC Unique Drop==='
Line 552: Line 552:
uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item'})
uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item'})
end
end
 
table.insert(sectionTxt, uniqueTxt)
table.insert(sectionTxt, uniqueTxt)
end
end
 
return table.concat(sectionTxt, '\r\n')
return table.concat(sectionTxt, '\r\n')
end
end
Line 565: Line 565:
return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
end
end
 
return p._getThievingNPCLootTables(npc)
return p._getThievingNPCLootTables(npc)
end
end
Line 599: Line 599:
end
end
result = result..'\r\n|}'
result = result..'\r\n|}'
 
return result
return result
end
end
Line 608: Line 608:
table.insert(resultPart, '\r\n|- class="headerRow-0"')
table.insert(resultPart, '\r\n|- class="headerRow-0"')
table.insert(resultPart, '\r\n!Area!!'..Icons.Icon({'Thieving', type='skill', notext=true})..' Level!!NPCs!!Unique Drops')
table.insert(resultPart, '\r\n!Area!!'..Icons.Icon({'Thieving', type='skill', notext=true})..' Level!!NPCs!!Unique Drops')
 
local areaArray = Shared.clone(SkillData.Thieving.Areas)
local areaArray = Shared.clone(SkillData.Thieving.Areas)
table.sort(areaArray, function(a, b) return a.id < b.id end)
table.sort(areaArray, function(a, b) return a.id < b.id end)
Line 627: Line 627:
table.insert(npcList, '')
table.insert(npcList, '')
end
end
 
-- Build area unique item list
-- Build area unique item list
if area.uniqueDrops ~= nil and Shared.tableCount(area.uniqueDrops) > 0 then
if area.uniqueDrops ~= nil and Shared.tableCount(area.uniqueDrops) > 0 then
Line 645: Line 645:
table.insert(areaItemList, '')
table.insert(areaItemList, '')
end
end
 
-- Generate table row
-- Generate table row
table.insert(resultPart, '\r\n|-')
table.insert(resultPart, '\r\n|-')
Line 654: Line 654:
end
end
table.insert(resultPart, '\r\n|}')
table.insert(resultPart, '\r\n|}')
 
return table.concat(resultPart)
return table.concat(resultPart)
end
end
Line 660: Line 660:
function p.getThievingSourcesForItem(itemID)
function p.getThievingSourcesForItem(itemID)
local resultArray = {}
local resultArray = {}
 
local areaNPCs = {}
local areaNPCs = {}
 
--First check area unique drops
--First check area unique drops
--If an area drops the item, add all the NPC ids to the list so we can add them later
--If an area drops the item, add all the NPC ids to the list so we can add them later
Line 677: Line 677:
end
end
end
end
 
--Now go through and get drop chances on each NPC if needed
--Now go through and get drop chances on each NPC if needed
for i, npc in pairs(SkillData.Thieving.NPCs) do
for i, npc in pairs(SkillData.Thieving.NPCs) do
Line 693: Line 693:
table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = dropQty, wt = dropWt * thievingNormalLootChance, totalWt = totalWt * 100, level = npc.level})
table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = dropQty, wt = dropWt * thievingNormalLootChance, totalWt = totalWt * 100, level = npc.level})
end
end
 
--Chance of -1 on unique drops is to indicate variable chance
--Chance of -1 on unique drops is to indicate variable chance
if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID == itemID then
if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID == itemID then
table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.qty, maxQty = npc.uniqueDrop.qty, wt = -1, totalWt = -1, level = npc.level})
table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.qty, maxQty = npc.uniqueDrop.qty, wt = -1, totalWt = -1, level = npc.level})
end
end
 
if areaNPCs[npc.id] ~= nil then
if areaNPCs[npc.id] ~= nil then
table.insert(resultArray, {npc = npc.name, minQty = areaNPCs[npc.id], maxQty = areaNPCs[npc.id], wt = thievingAreaLootChance, totalWt = 100, level = npc.level})
table.insert(resultArray, {npc = npc.name, minQty = areaNPCs[npc.id], maxQty = areaNPCs[npc.id], wt = thievingAreaLootChance, totalWt = 100, level = npc.level})
end
end
end
end
 
for i, drop in pairs(SkillData.Thieving.RareItems) do
for i, drop in pairs(SkillData.Thieving.RareItems) do
if drop.itemID == itemID then
if drop.itemID == itemID then
Line 709: Line 709:
end
end
end
end
 
return resultArray
return resultArray
end
end
Line 736: Line 736:
return false
return false
end
end
 
local addToArray = function(modArray, modNew)
local addToArray = function(modArray, modNew)
if not isDistinct or (isDistinct and not containsMod(modArray, modNew)) then
if not isDistinct or (isDistinct and not containsMod(modArray, modNew)) then
Line 742: Line 742:
end
end
end
end
 
local modArray = {}
local modArray = {}
local isSkillMod = {}
local isSkillMod = {}
Line 769: Line 769:
table.insert(skillArray, SkillData.Skills[skillID + 1])
table.insert(skillArray, SkillData.Skills[skillID + 1])
end
end
 
for i, modName in ipairs(cons.uniqueModifiers) do
for i, modName in ipairs(cons.uniqueModifiers) do
-- The most important thing we're getting here is the modText and modBase
-- The most important thing we're getting here is the modText and modBase
Line 788: Line 788:
end
end
end
end
 
if asKeyValue then
if asKeyValue then
local modArrayKV = {}
local modArrayKV = {}
Line 814: Line 814:
result = result..'\r\n!colspan="2"|Constellation!!'..Icons.Icon({"Astrology", type='skill', notext='true'})..' Level'
result = result..'\r\n!colspan="2"|Constellation!!'..Icons.Icon({"Astrology", type='skill', notext='true'})..' Level'
result = result..'!!XP!!Skills!!Standard Modifiers!!Unique Modifiers'
result = result..'!!XP!!Skills!!Standard Modifiers!!Unique Modifiers'
 
for i, cons in Shared.skpairs(SkillData.Astrology.Constellations) do
for i, cons in Shared.skpairs(SkillData.Astrology.Constellations) do
local name = cons.name
local name = cons.name
Line 820: Line 820:
result = result..'\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='constellation', size='50', notext=true})..'||'..name
result = result..'\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='constellation', size='50', notext=true})..'||'..name
result = result..'||'..cons.level..'||'..cons.provides.xp
result = result..'||'..cons.level..'||'..cons.provides.xp
 
local skillIconArray = {}
local skillIconArray = {}
for j, skillID in ipairs(cons.skills) do
for j, skillID in ipairs(cons.skills) do
Line 826: Line 826:
end
end
result = result..'||'..table.concat(skillIconArray, '<br/>')
result = result..'||'..table.concat(skillIconArray, '<br/>')
 
local standModsRaw = p._buildAstrologyModifierArray(cons, maxModifier, true, false, false, false)
local standModsRaw = p._buildAstrologyModifierArray(cons, maxModifier, true, false, false, false)
local standMods = {}
local standMods = {}
Line 834: Line 834:
end
end
result = result..'|| '..table.concat(standMods, '<br/>')
result = result..'|| '..table.concat(standMods, '<br/>')
 
--Building the list of all Unique Modifiers
--Building the list of all Unique Modifiers
local uModsRaw = p._buildAstrologyModifierArray(cons, maxModifier, false, true, false, false)
local uModsRaw = p._buildAstrologyModifierArray(cons, maxModifier, false, true, false, false)
Line 844: Line 844:
end
end
result = result..'\r\n|}'
result = result..'\r\n|}'
 
return result
return result
end
end

Revision as of 07:14, 3 February 2022

Documentation for this module may be created at Module:Skills/Gathering/doc

--Splitting some functions into here to avoid bloating a single file
local p = {}

local SkillData = mw.loadData('Module:Skills/data')
local ShopData = mw.loadData('Module:Shop/data')

local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local Items = require('Module:Items')
local Icons = require('Module:Icons')

local thievingNormalLootChance = 75
local thievingAreaLootChance = 0.2

function p.getConstellationByID(constID)
	return SkillData.Astrology.Constellations[constID]
end

function p.getConstellation(constName)
	for i, const in ipairs(SkillData.Astrology.Constellations) do
		if const.name == constName then
			return const
		end
	end
	return nil
end

function p.getConstellations(checkFunc)
	local result = {}
	for i, const in ipairs(SkillData.Astrology.Constellations) do
		if checkFunc(const) then
			table.insert(result, const)
		end
	end
	return result
end

function p.getAxeTable(frame)
	local toolArray = {}
	for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
		if Shared.contains(upgrade.name, 'Axe') then
			table.insert(toolArray, upgrade)
		end
	end

	local result = '{| class="wikitable"'
	result = result..'\r\n!colspan="4"| !!colspan="2"|Cut Time Decrease'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Woodcutting', type='skill', notext=true})..' Level'
	result = result..'!!Cost!!This Axe!!Total'

	local total = 0

	for i, tool in Shared.skpairs(toolArray) do
		result = result..'\r\n|-'
		result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
		result = result..'||'..tool.name
		local level = 1
		if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil then
			--Gonna be lazy and assume there's only the one skill level and it's the one we care about
			level = tool.unlockRequirements.skillLevel[1][2]
		end
		result = result..'||style="text-align:right"|'..level
		result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)

		local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
		total = total + cutTime
		result = result..'||style="text-align:right"|-'..cutTime..'%'
		result = result..'||style="text-align:right"|-'..total..'%'
	end

	result = result..'\r\n|}'
	return result
end

function p.getPickaxeTable(frame)
	local toolArray = {}
	for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
		if Shared.contains(upgrade.name, 'Pickaxe') then
			table.insert(toolArray, upgrade)
		end
	end

	local result = '{| class="wikitable"'
	result = result..'\r\n!colspan="4"| !!colspan="2"|Mine Time Decrease!!colspan="2"|2x Ore Chance'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Mining', type='skill', notext=true})..' Level'
	result = result..'!!Cost!!This Pick!!Total!!This Pick!!Total'

	local total = 0
	local total2 = 0

	for i, tool in Shared.skpairs(toolArray) do
		result = result..'\r\n|-'
		result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
		result = result..'||'..tool.name
		local level = 1
		if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil then
			--Gonna be lazy and assume there's only the one skill level and it's the one we care about
			level = tool.unlockRequirements.skillLevel[1][2]
		end
		result = result..'||style="text-align:right"|'..level
		result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)

		local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
		total = total + cutTime

		result = result..'||style="text-align:right"|-'..cutTime..'%'
		result = result..'||style="text-align:right"|-'..total..'%'

		local OreDouble = tool.contains.modifiers.increasedChanceToDoubleOres
		total2 = total2 + OreDouble

		result = result..'||style="text-align:right"|+'..OreDouble..'%'
		result = result..'||style="text-align:right"|+'..total2..'%'
	end

	result = result..'\r\n|}'
	return result
end

function p.getRodTable(frame)
	local toolArray = {}
	for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
		if Shared.contains(upgrade.name, 'Fishing Rod') then
			table.insert(toolArray, upgrade)
		end
	end

	local result = '{| class="wikitable"'
	result = result..'\r\n!colspan="4"| !!colspan="2"|Catch Time Decrease'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Fishing', type='skill', notext=true})..' Level'
	result = result..'!!Cost!!This Rod!!Total'

	local total = 0

	for i, tool in Shared.skpairs(toolArray) do
		result = result..'\r\n|-'
		result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
		result = result..'||'..tool.name
		local level = 1
		if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil then
			--Gonna be lazy and assume there's only the one skill level and it's the one we care about
			level = tool.unlockRequirements.skillLevel[1][2]
		end
		result = result..'||style="text-align:right"|'..level
		result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)

		local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
		total = total + cutTime
		result = result..'||style="text-align:right"|-'..cutTime..'%'
		result = result..'||style="text-align:right"|-'..total..'%'
	end

	result = result..'\r\n|}'
	return result
end

function p.getTreesTable(frame)
	local result = '{| class="wikitable sortable"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Tree!!colspan="2"|Logs!!'..Icons.Icon({'Woodcutting', type='skill', notext=true})..' Level'
	result = result..'!!XP!!Cut Time!!XP/s!!GP/s'

	for i, tree in Shared.skpairs(SkillData.Woodcutting.Trees) do
		result = result..'\r\n|-'
		local treeName = Shared.titleCase(tree.type..' tree')
		local logName = Shared.titleCase(tree.type..' logs')
		result = result..'\r\n|style="min-width:25px" data-sort-value="'..treeName..'"|'..Icons.Icon({logName, img=treeName, type='tree', notext=true, size=50})
		result = result..'||'..treeName..''
		result = result..'||style="min-width:25px" data-sort-value="'..logName..'"|'..Icons.Icon({logName, type='item', notext=true, size=50})
		result = result..'||'..Icons.Icon({logName, type='item', noicon=true})
		result = result..'||style="text-align:right"|'..tree.levelRequired
		result = result..'||style="text-align:right"|'..tree.baseExperience
		result = result..'||style="text-align:right" data-sort-value="'..tree.baseInterval..'"|'..Shared.timeString(tree.baseInterval/1000, true)
		local XPs = tree.baseExperience / (tree.baseInterval / 1000)
		local Log = Items.getItemByID(tree.logID)
		local GPs = Log.sellsFor / (tree.baseInterval / 1000)
		result = result..'||style="text-align:right"|'..Shared.round(XPs, 2, 2)
		result = result..'||style="text-align:right" data-sort-value="'..GPs..'"|'..Icons.GP(Shared.round(GPs, 2, 2))
	end

	result = result..'\r\n|}'
	return result
end

function p.getSpecialFishingTable(frame)
	local lootValue = 0
	local totalWt = Items.specialFishWt

	local result = ''
	result = result..'\r\n{|class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!Item'
	result = result..'!!Price!!colspan="2"|Chance'

	--Sort the loot table by weight in descending order
	table.sort(Items.specialFishLoot, function(a, b) return a[2] > b[2] end)
	for i, row in pairs(Items.specialFishLoot) do
		local thisItem = Items.getItemByID(row[1])
		result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
		result = result..'||style="text-align:left" data-sort-value="'..thisItem.sellsFor..'"'
		result = result..'|'..Icons.GP(thisItem.sellsFor)

		local dropChance = (row[2] / totalWt) * 100
		result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
		result = result..'|'..Shared.fraction(row[2], totalWt)
		result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
		lootValue = lootValue + (dropChance * 0.01 * thisItem.sellsFor)
	end
	result = result..'\r\n|}'
	result = result..'\r\nThe average value of a roll on the special fishing loot table is '..Icons.GP(Shared.round(lootValue, 2, 0))

	return result
end

function p.getFishingJunkTable(frame)
	local result = '{| class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Item!!Value'

	local itemArray = Items.getItems(function(item) return item.type == "Junk" end)

	table.sort(itemArray, function(a, b) return a.name < b.name end)

	for i, item in Shared.skpairs(itemArray) do
		result = result..'\r\n|-'
		result = result..'\r\n|style="min-width:25px"|'..Icons.Icon({item.name, type='item', notext=true, size='50'})
		result = result..'||'..Icons.Icon({item.name, type='item', noicon=true})
		result = result..'||style="text-align:right;" data-sort-value="'..item.sellsFor..'"|'..Icons.GP(item.sellsFor)
	end

	result = result..'\r\n|}'

	return result
end

function p.getMiningOresTable(frame)
	local result = '{|class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan=2|Ore!!'..Icons.Icon({'Mining', type='skill', notext=true})..' Level'
	result = result..'!!XP!!Respawn Time!!Ore Value'

	local mineData = Shared.clone(SkillData.Mining.Rocks)

	table.sort(mineData, function(a, b) return a.levelRequired < b.levelRequired end)

	for i, oreData in Shared.skpairs(mineData) do
		local ore = Items.getItemByID(oreData.oreID)
		result = result..'\r\n|-\r\n|style="min-width:25px"|'..Icons.Icon({ore.name, type='item', size='50', notext=true})
		result = result..'||'..Icons.Icon({ore.name, type='item', noicon=true})
		result = result..'||style="text-align:right"|'..oreData.levelRequired..'||style="text-align:right"|'..ore.miningXP
		result = result..'||style="text-align:right" data-sort-value="'..oreData.baseRespawnInterval..'"|'
		result = result..Shared.timeString(oreData.baseRespawnInterval / 1000, true)
		result = result..'||data-sort-value="'..ore.sellsFor..'"|'..Icons.GP(ore.sellsFor)
	end

	result = result..'\r\n|}'
	return result
end

function p.getMiningGemsTable(frame)
	local result = '{|class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan=2|Gem!!Gem Chance!!Gem Price'

	-- Sort gems by ID order
	for i, gemData in Shared.spairs(Items.GemTable, function(t,a,b) return t[a].id < t[b].id end) do
		local gem = Items.getItemByID(gemData.id)
		result = result..'\r\n|-\r\n|style="min-width:25px"|'
		result = result..Icons.Icon({gem.name, type='item', size='50', notext=true})
		result = result..'||'..Icons.Icon({gem.name, type='item', noicon=true})
		result = result..'||style="text-align:right"|'..string.format("%.1f%%", gemData.chance)
		result = result..'||data-sort-value="'..gem.sellsFor..'"|'..Icons.GP(gem.sellsFor)
	end

	result = result..'\r\n|}'
	return result
end

function p.getFishTable(frame)
	local data = Items.getItems(function(item) return item.fishingID ~= nil end)

	table.sort(data, function(a, b) return a.fishingID < b.fishingID end)

	local result = '{| class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!Fish\r\n!Name\r\n!'..Icons.Icon({'Fishing', type='skill', notext=true})..' Level\r\n!Catch Time'
	result = result..'\r\n!Experience\r\n!Fish Price\r\n!XP/s\r\n!GP/s\r\n!'
	result = result..Icons.Icon({'Cooking', type='skill', notext=true})..' Level'

	for i, fish in Shared.skpairs(data) do
		result = result..'\r\n|-'
		result = result..'\r\n| style="text-align: left;" | '..Icons.Icon({fish.name, type='item', size='50', notext=true})
		result = result..'\r\n| style="text-align: left;" | '..Icons.Icon({fish.name, type='item', noicon=true})
		result = result..'\r\n| style="text-align:right"|'..fish.fishingLevel

		local timeSortVal = (fish.minFishingInterval + fish.maxFishingInterval) / 2
		local timeStr = string.format("%.1fs-%.1fs", (fish.minFishingInterval/1000), (fish.maxFishingInterval/1000))
		result = result..'\r\n| style="text-align:right" data-sort-value="'..timeSortVal..'"|'..timeStr
		result = result..'\r\n| style="text-align:right"|'..fish.fishingXP
		result = result..'\r\n| style="text-align:right"|'..fish.sellsFor
		local XPs = fish.fishingXP / (timeSortVal / 1000)
		local GPs = fish.sellsFor / (timeSortVal / 1000)
		result = result..'\r\n| style="text-align:right"|'..Shared.round(XPs, 2, 2)
		result = result..'\r\n| style="text-align:right" data-sort-value="'..GPs..'"|'..Icons.GP(Shared.round(GPs, 2, 2))

		local cookStr = "N/A"
		if fish.cookingLevel ~= nil then
			cookStr = fish.cookingLevel
		end
		result = result..'\r\n| style="text-align:right"|'..cookStr
	end

	result = result..'\r\n|}'
	return result
end

function p.getFishingAreasTable(frame)

	local result = '{| class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!Name\r\n!Fish\r\n!Fish Chance'
	result = result..'\r\n!Junk Chance\r\n!Special Chance'

	for i, area in Shared.skpairs(SkillData.Fishing.Areas) do
		result = result..'\r\n|-'
		result = result..'\r\n| style ="text-align: left;" |'..area.name

		local fishArray = {}
		for j, fish in Shared.skpairs(area.fish) do
			local fishTable = Items.getItems(function(item) return item.fishingID == fish end)
			local fishItem = fishTable[0] or fishTable[1]
			table.insert(fishArray, Icons.Icon({fishItem.name, type='item'}))
		end
		result = result..'\r\n|'..table.concat(fishArray, '<br />')

		result = result..'\r\n| style="text-align:right"|'..area.fishChance..'%'
		result = result..'\r\n| style="text-align:right"|'..area.junkChance..'%'
		result = result..'\r\n| style="text-align:right"|'..area.specialChance..'%'
	end

	result = result..'\r\n|}'
	return result
end

function p.getThievingNPC(npcName)
	local result = nil
	for i, npc in Shared.skpairs(SkillData.Thieving.NPCs) do
		if npc.name == npcName then
			result = Shared.clone(npc)
			break
		end
	end
	return result
end

function p.getThievingNPCArea(npc)
	if type(npc) == 'string' then
		npc = p.getThievingNPC(npc)
	end

	local result = nil
	for i, area in Shared.skpairs(SkillData.Thieving.Areas) do
		for j, npcID in pairs(area.npcs) do
			if npcID == npc.id then
				result = area
				break
			end
		end
	end
	return result
end

function p._getThievingNPCStat(npc, statName)
	local result = nil

	if statName == 'level' then
		result = Icons._SkillReq('Thieving', npc.level)
	elseif statName == 'maxHit' then
		result = npc.maxHit * 10
	elseif statName == 'area' then
		local area = p.getThievingNPCArea(npc)
		result = area.name
	else
		result = npc[statName]
	end

	if result == nil then
		result = ''
	end

	return result
end

function p.getThievingNPCStat(frame)
	local npcName = frame.args ~= nil and frame.args[1] or frame[1]
	local statName = frame.args ~= nil and frame.args[2] or frame[2]
	local npc = p.getThievingNPC(npcName)
	if npc == nil then
		return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
	end

	return p._getThievingNPCStat(npc, statName)
end

function p.getThievingGeneralRareTable(frame)
	local rareTxt = '{|class="wikitable sortable"'
	rareTxt = rareTxt..'\r\n!Item!!Qty'
	rareTxt = rareTxt..'!!Price!!colspan="2"|Chance'
	for i, drop in pairs(SkillData.Thieving.RareItems) do
		local thisItem = Items.getItemByID(drop.itemID)
		local odds = drop.chance

		rareTxt = rareTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
		rareTxt = rareTxt..'||1||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
		rareTxt = rareTxt..'||style="text-align:right" data-sort-value="'..odds..'"|'..Shared.fraction(1, Shared.round2(1/(odds/100), 0))
		rareTxt = rareTxt..'||style="text-align:right" data-sort-value="'..odds..'"|'..Shared.round(odds, 4, 4)..'%'
	end
	rareTxt = rareTxt..'\r\n|}'
	return rareTxt
end

function p._getThievingNPCLootTables(npc)
	local result = ''
	local sectionTxt = {}

	--Five sections here: GP, normal loot, area loot, rare loot, and unique item
	--First up, GP:
	local gpTxt = 'Successfully pickpocketing the '..npc.name..' will always give '..Icons.GP(1, npc.maxGP)
	table.insert(sectionTxt, gpTxt)

	--Next up, normal loot:
	--(Skip if no loot)
	if npc.lootTable ~= nil and Shared.tableCount(npc.lootTable) > 0 then
		local normalTxt = '===Possible Common Drops:===\r\nUp to one of these will be received on a successful pickpocket:'
		local totalWt = 0
		local lootChance = thievingNormalLootChance
		local lootValue = 0

		--First loop through to get the total weight so we have it for later
		for i, loot in pairs(npc.lootTable) do
			totalWt = totalWt + loot[2]
		end

		normalTxt = normalTxt..'\r\n{|class="wikitable sortable"'
		normalTxt = normalTxt..'\r\n!Item!!Qty'
		normalTxt = normalTxt..'!!Price!!colspan="2"|Chance'

		--Then sort the loot table by weight
		table.sort(npc.lootTable, function(a, b) return a[2] > b[2] end)
		for i, row in Shared.skpairs(npc.lootTable) do
			local thisItem = Items.getItemByID(row[1])
			local maxQty = row[3]
			if thisItem ~= nil then
				normalTxt = normalTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
			else
				normalTxt = normalTxt..'\r\n|-\r\n|Unknown Item[[Category:Pages with script errors]]'
			end
			normalTxt = normalTxt..'||style="text-align:right" data-sort-value="'..maxQty..'"|'

			if maxQty > 1 then
				normalTxt = normalTxt.. '1 - '
			end
			normalTxt = normalTxt..Shared.formatnum(row[3])

			--Adding price columns
			local itemPrice = 0
			if thisItem == nil then
				normalTxt = normalTxt..'||data-sort-value="0"|???'
			else
				itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
				if itemPrice == 0 or maxQty == 1 then
					normalTxt = normalTxt..'||'..Icons.GP(itemPrice)
				else
					normalTxt = normalTxt..'||'..Icons.GP(itemPrice, itemPrice * maxQty)
				end
			end

			--Getting the drop chance
			local dropChance = (row[2] / totalWt * lootChance)
			if dropChance ~= 100 then
				--Show fraction as long as it isn't going to be 1/1
				normalTxt = normalTxt..'||style="text-align:right" data-sort-value="'..row[2]..'"'
				normalTxt = normalTxt..'|'..Shared.fraction(row[2] * lootChance, totalWt * 100)
				normalTxt = normalTxt..'||'
			else
				normalTxt = normalTxt..'||colspan="2" data-sort-value="'..row[2]..'"'
			end
			normalTxt = normalTxt..'style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'

			--Adding to the average loot value based on price & dropchance
			lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((1 + maxQty) / 2))
		end
		if Shared.tableCount(npc.lootTable) > 1 then
			normalTxt = normalTxt..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
			if lootChance < 100 then
				normalTxt = normalTxt..'\r\n|style="text-align:right"|'..Shared.fraction(lootChance, 100)..'||'
			else
				normalTxt = normalTxt..'\r\n|colspan="2" '
			end
			normalTxt = normalTxt..'style="text-align:right"|'..Shared.round(lootChance, 2, 2)..'%'
		end
		normalTxt = normalTxt..'\r\n|}'
		table.insert(sectionTxt, normalTxt)
	end

	--After normal drops, add in rare drops
	local rareTxt = '===Possible Rare Drops:===\r\nAny of these can be received after a successful pickpocket'
	rareTxt = rareTxt..'\r\n'..p.getThievingGeneralRareTable()
	table.insert(sectionTxt, rareTxt)

	local areaTxt = '===Possible Area Unique Drops==='
	areaTxt = areaTxt..'\r\nAny Area Unique Drop is equally likely to be obtained after a successful pickpocket. '
	areaTxt = areaTxt..'\r\nEach Area Unique Drop is rolled for separately, so it is possible to receive multiple Area Unique Drops from a single action. '
	areaTxt = areaTxt..'The chance of receiving an Area Unique drop is tripled if the 95% Thieving Mastery Pool checkpoint is active.'

	local area = p.getThievingNPCArea(npc)
	areaTxt = areaTxt..'\r\n{|class="wikitable sortable"'
	areaTxt = areaTxt..'\r\n!Item!!Qty'
	areaTxt = areaTxt..'!!Price!!colspan="2"|Chance'
	local dropCount = Shared.tableCount(area.uniqueDrops)
	local dropLines = {}
	for i, drop in pairs(area.uniqueDrops) do
		local thisItem = Items.getItemByID(drop.itemID)
		local lineTxt = ''
		lineTxt = lineTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
		lineTxt = lineTxt..'||'..drop.qty..'||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
		lineTxt = lineTxt..'||style="text-align:right"|'..Shared.fraction(1, 1/(thievingAreaLootChance/100))
		lineTxt = lineTxt..'||'..Shared.round(thievingAreaLootChance, 2, 2)..'%'
		dropLines[thisItem.name] = lineTxt
	end
	for i, txt in Shared.skpairs(dropLines) do
		areaTxt = areaTxt..txt
	end
	areaTxt = areaTxt..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
	areaTxt = areaTxt..'\r\n|style="text-align:right"|'..Shared.fraction(1, 1/(thievingAreaLootChance/100))..'||'
	areaTxt = areaTxt..'style="text-align:right"|'..Shared.round(thievingAreaLootChance, 2, 2)..'%'
	areaTxt = areaTxt..'\r\n|}'
	table.insert(sectionTxt, areaTxt)

	if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID > -1 then
		local uniqueTxt = '===Possible NPC Unique Drop==='
		uniqueTxt = uniqueTxt..'\r\nThe chance of receiving the unique drop for an NPC is based on a combination of several factors.'
		uniqueTxt = uniqueTxt..' The unique drop chance for an NPC is included in the tooltip for your Stealth against that NPC.'
		local thisItem = Items.getItemByID(npc.uniqueDrop.itemID)
		uniqueTxt = uniqueTxt..'\r\nThe unique drop for the '..npc.name..' is '
		if npc.uniqueDrop.qty > 1 then
			uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item', qty=npc.uniqueDrop.qty})
		else
			uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item'})
		end

		table.insert(sectionTxt, uniqueTxt)
	end

	return table.concat(sectionTxt, '\r\n')
end

function p.getThievingNPCLootTables(frame)
	local npcName = frame.args ~= nil and frame.args[1] or frame
	local npc = p.getThievingNPC(npcName)
	if npc == nil then
		return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
	end

	return p._getThievingNPCLootTables(npc)
end

function p.getThievingNPCTable()
	local result = '{| class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Name!!Area!!'..Icons.Icon({'Thieving', type='skill', notext=true})..' Level!!Experience!!Max Hit!!Perception!!GP!!Unique Drop'
	local npcArray = Shared.clone(SkillData.Thieving.NPCs)
	table.sort(npcArray, function(a, b) return a.level < b.level end)
	for i, npc in Shared.skpairs(npcArray) do
		result = result..'\r\n|-'
		result = result..'\r\n|'..Icons.Icon({npc.name, type='thieving', size='50', notext=true})
		result = result..'||'..Icons.Icon({npc.name, type='thieving', noicon=true})

		local area = p.getThievingNPCArea(npc)
		result = result..'||'..area.name
		result = result..'||'..Icons._SkillReq('Thieving', npc.level)
		result = result..'||style="text-align:right"|'..npc.xp
		result = result..'||style="text-align:right"|'..(npc.maxHit * 10)
		result = result..'||style="text-align:right"|'..npc.perception
		result = result..'||data-sort-value="' .. npc.maxGP .. '"|'..Icons.GP(1, npc.maxGP)
		if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID > -1 then
			local uniqueDrop = Items.getItemByID(npc.uniqueDrop.itemID)
			if npc.uniqueDrop.qty > 1 then
				result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item', qty = npc.uniqueDrop.qty})
			else
				result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item'})
			end
		else
			result = result..'|| '
		end
	end
	result = result..'\r\n|}'

	return result
end

function p.getThievingAreaTable(frame)
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"')
	table.insert(resultPart, '\r\n!Area!!'..Icons.Icon({'Thieving', type='skill', notext=true})..' Level!!NPCs!!Unique Drops')

	local areaArray = Shared.clone(SkillData.Thieving.Areas)
	table.sort(areaArray, function(a, b) return a.id < b.id end)
	for i, area in ipairs(areaArray) do
		local minLevel, npcList, areaItemList = nil, {}, {}
		-- Build NPC list & determine level for area, this is the minimum
		-- Thieving level required for all NPCs within that area
		if area.npcs ~= nil and Shared.tableCount(area.npcs) > 0 then
			for j, npcID in ipairs(area.npcs) do
				-- Don't bother cloning the NPC below since we aren't modifying any part of it
				local npc = SkillData.Thieving.NPCs[npcID + 1]
				if minLevel == nil or npc.level < minLevel then
					minLevel = npc.level
				end
				table.insert(npcList, Icons.Icon({npc.name, type='thieving'}))
			end
		else
			table.insert(npcList, '')
		end

		-- Build area unique item list
		if area.uniqueDrops ~= nil and Shared.tableCount(area.uniqueDrops) > 0 then
			for k, drop in ipairs(area.uniqueDrops) do
				local areaItem = Items.getItemByID(drop.itemID)
				if areaItem == nil then
					table.insert(areaItemList, 'Unknown[[Category:Pages with script errors]]')
				else
					local iconDef = {areaItem.name, type='item'}
					if drop.qty > 1 then
						iconDef.qty = drop.qty
					end
					table.insert(areaItemList, Icons.Icon(iconDef))
				end
			end
		else
			table.insert(areaItemList, '')
		end

		-- Generate table row
		table.insert(resultPart, '\r\n|-')
		table.insert(resultPart, '\r\n|' .. area.name)
		table.insert(resultPart, '\r\n|' .. Icons._SkillReq('Thieving', minLevel))
		table.insert(resultPart, '\r\n|' .. table.concat(npcList, '<br/>'))
		table.insert(resultPart, '\r\n|' .. table.concat(areaItemList, '<br/>'))
	end
	table.insert(resultPart, '\r\n|}')

	return table.concat(resultPart)
end

function p.getThievingSourcesForItem(itemID)
	local resultArray = {}

	local areaNPCs = {}

	--First check area unique drops
	--If an area drops the item, add all the NPC ids to the list so we can add them later
	if not result then
		for i, area in pairs(SkillData.Thieving.Areas) do
			for j, drop in pairs(area.uniqueDrops) do
				if drop.itemID == itemID then
					for k, npcID in pairs(area.npcs) do
						areaNPCs[npcID] = drop.qty
					end
					break
				end
			end
		end
	end

	--Now go through and get drop chances on each NPC if needed
	for i, npc in pairs(SkillData.Thieving.NPCs) do
		local totalWt = 0
		local dropWt = 0
		local dropQty = 0
		for j, drop in pairs(npc.lootTable) do
			totalWt = totalWt + drop[2]
			if drop[1] == itemID then
				dropWt = drop[2]
				dropQty = drop[3]
			end
		end
		if dropWt > 0 then
			table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = dropQty, wt = dropWt * thievingNormalLootChance, totalWt = totalWt * 100, level = npc.level})
		end

		--Chance of -1 on unique drops is to indicate variable chance
		if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID == itemID then
			table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.qty, maxQty = npc.uniqueDrop.qty, wt = -1, totalWt = -1, level = npc.level})
		end

		if areaNPCs[npc.id] ~= nil then
			table.insert(resultArray, {npc = npc.name, minQty = areaNPCs[npc.id], maxQty = areaNPCs[npc.id], wt = thievingAreaLootChance, totalWt = 100, level = npc.level})
		end
	end

	for i, drop in pairs(SkillData.Thieving.RareItems) do
		if drop.itemID == itemID then
			table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = 1})
		end
	end

	return resultArray
end

-- For a given constellation cons and modifier value modValue, generates and returns
-- a table of modifiers, much like any other item/object elsewhere in the game.
-- includeStandard: true|false, determines whether standard modifiers are included
-- includeUnique: true|false, determines whether unique modifiers are included
-- isDistinct: true|false, if true, the returned list of modifiers is de-duplicated
-- asKeyValue: true|false, if true, returns key/value pairs like usual modifier objects
function p._buildAstrologyModifierArray(cons, modValue, includeStandard, includeUnique, isDistinct, asKeyValue)
	-- Temporary function to determine if the table already contains a given modifier
	local containsMod = function(modList, modNew)
			for i, modItem in ipairs(modList) do
				-- Check mod names & value data types both equal
				if modItem[1] == modNew[1] and type(modItem[2]) == type(modNew[2]) then
					if type(modItem[2]) == 'table' then
						if Shared.tablesEqual(modItem[2], modNew[2]) then
							return true
						end
					elseif modItem[2] == modNew[2] then
						return true
					end
				end
			end
			return false
		end

	local addToArray = function(modArray, modNew)
			if not isDistinct or (isDistinct and not containsMod(modArray, modNew)) then
				table.insert(modArray, modNew)
			end
		end

	local modArray = {}
	local isSkillMod = {}
	-- Standard modifiers
	if includeStandard then
		for i, skillMods in ipairs(cons.standardModifiers) do
			local skillID = cons.skills[i]
			if skillID ~= nil then
				for j, modName in ipairs(skillMods) do
					local modBaseName, modText, sign, isNegative, unsign, modBase = Constants.getModifierDetails(modName)
					-- Check if modifier varies by skill, and amend the modifier value accordingly
					local modVal = modValue
					if Shared.contains(modText, '{SV0}') then
						isSkillMod[modName] = true
						modVal = {skillID, modValue}
					end
					addToArray(modArray, {modName, modVal})
				end
			end
		end
	end
	-- Unique modifiers
	if includeUnique then
		local skillArray = {}
		for i, skillID in ipairs(cons.skills) do
			table.insert(skillArray, SkillData.Skills[skillID + 1])
		end

		for i, modName in ipairs(cons.uniqueModifiers) do
			-- The most important thing we're getting here is the modText and modBase
			-- modText lets us check if this is a per-skill modifier or not
			-- modBase lets us check .isSkill, which are modifiers that we only use for skills with Mastery
			local modBaseName, modText, sign, isNegative, unsign, modBase = Constants.getModifierDetails(modName)
			if Shared.contains(modText, '{SV0}') then
				isSkillMod[modName] = true
				-- Check which skills the current modifier can be used for
				for j, skillID in ipairs(cons.skills) do
					if not modBase.isSkill or (modBase.isSkill and skillArray[j].hasMastery) then
						addToArray(modArray, {modName, {skillID, modValue}})
					end
				end
			else
				addToArray(modArray, {modName, modValue})
			end
		end
	end

	if asKeyValue then
		local modArrayKV = {}
		for i, modDefn in ipairs(modArray) do
			local modName, modVal = modDefn[1], modDefn[2]
			local isSkill = isSkillMod[modName]
			if modArrayKV[modName] == nil then
				modArrayKV[modName] = (isSkill and { modVal } or modVal)
			elseif isSkill then
				table.insert(modArrayKV[modName], modVal)
			else
				modArrayKV[modName] = modArrayKV[modName] + modVal
			end
		end
		return modArrayKV
	else
		return modArray
	end
end

function p._buildAstrologyConstellationTable()
	local maxModifier = 5
	local result = '{|class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Constellation!!'..Icons.Icon({"Astrology", type='skill', notext='true'})..' Level'
	result = result..'!!XP!!Skills!!Standard Modifiers!!Unique Modifiers'

	for i, cons in Shared.skpairs(SkillData.Astrology.Constellations) do
		local name = cons.name
		result = result..'\r\n|-'
		result = result..'\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='constellation', size='50', notext=true})..'||'..name
		result = result..'||'..cons.level..'||'..cons.provides.xp

		local skillIconArray = {}
		for j, skillID in ipairs(cons.skills) do
			table.insert(skillIconArray, Icons.Icon({Constants.getSkillName(skillID), type='skill'}))
		end
		result = result..'||'..table.concat(skillIconArray, '<br/>')

		local standModsRaw = p._buildAstrologyModifierArray(cons, maxModifier, true, false, false, false)
		local standMods = {}
		--Building the list of Standard modifiers:
		for j, modifier in ipairs(standModsRaw) do
			table.insert(standMods, Constants._getModifierText(modifier[1], modifier[2], false))
		end
		result = result..'|| '..table.concat(standMods, '<br/>')

		--Building the list of all Unique Modifiers
		local uModsRaw = p._buildAstrologyModifierArray(cons, maxModifier, false, true, false, false)
		local uMods = {}
		for j, modifier in ipairs(uModsRaw) do
			table.insert(uMods, Constants._getModifierText(modifier[1], modifier[2], false))
		end
		result = result..'||'..table.concat(uMods, '<br/>')
	end
	result = result..'\r\n|}'

	return result
end

function p.buildAstrologyConstellationTable(frame)
	return p._buildAstrologyConstellationTable()
end

function p.buildAstrologyValueTable()
	local result = '{|class="wikitable sortable"'
	result = result..'\r\n!rowspan="2"| Value!!colspan="2"| Chance'
	result = result..'\r\n|-\r\n! This Value!! This Value or Greater'
	local lastChance = 0
	local cumulativeChance = 100
	for i, chance in Shared.skpairs(SkillData.Astrology.Defaults.valueWeight) do
		local thisChance = (i == 5 and chance) or chance - lastChance
		result = result..'\r\n|-'
		result = result..'\r\n|style="text-align:right"| '..i
		result = result..'\r\n|style="text-align:right"| ' .. thisChance .. '%'
		result = result..'\r\n|style="text-align:right"| ' .. cumulativeChance .. '%'
		cumulativeChance = cumulativeChance - thisChance
		lastChance = chance
	end
	result = result..'\r\n|}'
	return result
end

return p