Module:Skills/Gathering: Difference between revisions

From Melvor Idle
(Added initial pass at grouping together different Astrology bonuses on the same modifier)
(Changed formatting for multiple bonuses on the same modifier for astrology)
Line 735: Line 735:
local outArray = {}
local outArray = {}
for i, group in ipairs(allMods) do
for i, group in ipairs(allMods) do
table.insert(outArray, table.concat(group, ' & '))
local groupTxt = '<span style="1px solid black;padding:3px">'..table.concat(group, ' &<br/> ')..'</span>'
table.insert(outArray, groupTxt)
end
end
return table.concat(outArray, '<br/>')
return table.concat(outArray, '<br/>')

Revision as of 19:02, 27 October 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 Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local Shop = require('Module:Shop')
local Skills = require('Module:Skills')
local ItemSourceTables = require('Module:Items/SourceTables')

-- TODO Move tool tables to Module:Shop
function p.getToolTable(toolName, searchString, modifiers, skillID)
	local skillName = Constants.getSkillName(skillID)
	local toolArray = Shop.getPurchases(
		function(purch)
			return purch.category == 'melvorD:SkillUpgrades' and string.find(purch.id, searchString) ~= nil
		end)

	if skillName == nil or Shared.tableIsEmpty(toolArray) then
		return ''
	end
	if modifiers == nil then
		modifiers = {}
	end

	local modTotal = {}
	for i, modDef in ipairs(modifiers) do
		modTotal[modDef.name] = 0
	end

	local headerRowSpan = (Shared.tableIsEmpty(toolArray) and 1) or 2
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable"')
	table.insert(resultPart, '\r\n!rowspan="' .. headerRowSpan .. '" colspan="2"| Name')
	table.insert(resultPart, '\r\n!rowspan="' .. headerRowSpan .. '"| ' .. Icons.Icon({skillName, type='skill', notext=true})..' Level')
	table.insert(resultPart, '\r\n!rowspan="' .. headerRowSpan .. '"| Cost')
	for i, modDef in ipairs(modifiers) do
		modTotal[modDef.name] = 0
		table.insert(resultPart, '\r\n!colspan="2"| ' .. modDef.header)
	end
	if headerRowSpan > 1 then
		table.insert(resultPart, '\r\n|-' .. string.rep('\r\n!This ' .. toolName .. '\r\n!Total', Shared.tableCount(modifiers)))
	end

	for i, tool in ipairs(toolArray) do
		local toolName = Shop._getPurchaseName(tool)
		local toolCost = Shop.getCostString(tool.cost, false)
		local toolCostSort = Shop._getPurchaseSortValue(tool)
		table.insert(resultPart, '\r\n|-')
		table.insert(resultPart, '\r\n|style="min-width:25px" data-sort-value="' .. toolName .. '"| ' .. Icons.Icon({toolName, type='upgrade', size='50', notext=true}))
		table.insert(resultPart, '\r\n| ' .. toolName)
		local level = 1
		if tool.purchaseRequirements ~= nil and not Shared.tableIsEmpty(tool.purchaseRequirements) then
			for i, purchReq in ipairs(tool.purchaseRequirements) do
				if purchReq.type == 'SkillLevel' and purchReq.skillID == skillID then
					level = purchReq.level
					break
				end
			end
		end
		table.insert(resultPart, '\r\n|style="text-align:right"| '..level)
		table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. toolCostSort .. '"| ' .. toolCost)

		local cellStart = '\r\n|style="text-align:right"| '
		if tool.contains ~= nil and tool.contains.modifiers ~= nil then
			for j, modDef in ipairs(modifiers) do
				local modName = modDef.name
				local modVal = tool.contains.modifiers[modName]
				if modVal ~= nil then
					if type(modVal) == 'table' and type(modVal[1]) == 'table' and modVal[1].skillID ~= nil then
						modVal = modVal[1].value
					end
					modTotal[modName] = modTotal[modName] + modVal
				else
					modVal = 0
				end
				table.insert(resultPart, cellStart .. (modVal == 0 and '' or modDef.sign) .. modVal .. modDef.suffix)
				table.insert(resultPart, cellStart .. (modTotal[modName] == 0 and '' or modDef.sign) .. modTotal[modName] .. modDef.suffix)
			end
		end
	end

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

function p.getAxeTable(frame)
	local modifiers = {
		{ name = 'decreasedSkillIntervalPercent', header = 'Cut Time Decrease', sign = '-', suffix = '%' },
		{ name = 'increasedChanceToDoubleItemsSkill', header = 'Double Items Chance', sign = '+', suffix = '%' },
		{ name = 'increasedBirdNestDropRate', header = Icons.Icon({'Bird Nest', 'Drop Chance', type='item', nolink=true}), sign = '+', suffix = '%' },
		{ name = 'increasedChanceForAshInWoodcutting', header = Icons.Icon({'Ash', 'Drop Chance', type='item', nolink=true}), sign = '+', suffix = '%' }
		}
	
	return p.getToolTable('Axe', '_Axe$', modifiers, 'melvorD:Woodcutting')
end

function p.getPickaxeTable(frame)
	local modifiers = {
		{ name = 'decreasedSkillIntervalPercent', header = 'Mining Time Decrease', sign = '-', suffix = '%' },
		{ name = 'increasedChanceToDoubleOres', header = '2x Ore Chance', sign = '+', suffix = '%' },
		{ name = 'increasedChanceForOneExtraOre', header = '+1 Ore Chance', sign = '+', suffix = '%' },
		{ name = 'increasedChanceForQualitySuperiorGem', header = 'Superior Gem Chance', sign = '+', suffix = '%' },
		{ name = 'increasedMeteoriteOre', header = 'Increased ' .. Icons.Icon({'Meteorite Ore', type='item', notext=true}), sign = '+', suffix = '' }
		}

	return p.getToolTable('Pickaxe', '_Pickaxe$', modifiers, 'melvorD:Mining')
end

function p.getRodTable(frame)
	local modifiers = {
		{ name = 'decreasedSkillIntervalPercent', header = 'Catch Time Decrease', sign = '-', suffix = '%' },
		{ name = 'increasedChanceForOneExtraFish', header = '+1 Fish Chance', sign = '+', suffix = '%' },
		{ name = 'increasedChanceToFindLostChest', header = Icons.Icon({'Lost Chest', type='item', notext=true}) .. ' Chance', sign = '+', suffix = '%' },
		{ name = 'increasedFishingCookedChance', header = 'Cooked Fish Chance', sign = '+', suffix = '%' }
		}

	return p.getToolTable('Rod', '_Rod$', modifiers, 'melvorD:Fishing')
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 ipairs(SkillData.Woodcutting.trees) do
		local log = Items.getItemByID(tree.productId)
		result = result..'\r\n|-'
		result = result..'\r\n|style="min-width:25px" data-sort-value="'..tree.name..'"|'..Icons.Icon({log.name, img=tree.name, type='tree', notext=true, size=50})
		result = result..'||'..tree.name..''
		result = result..'||style="min-width:25px" data-sort-value="'..log.name..'"|'..Icons.Icon({log.name, type='item', notext=true, size=50})
		result = result..'||'..Icons.Icon({log.name, type='item', noicon=true})
		result = result..'||style="text-align:right"|'..tree.level
		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 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 totalWt, lootValue = 0, 0
	local itemArray = GameData.getEntities(SkillData.Fishing.specialItems, function(item) return true end)
	for i, itemDef in ipairs(itemArray) do
		totalWt = totalWt + itemDef.weight
	end
	-- Sort the loot table by weight in descending order
	table.sort(itemArray, function(a, b) return a.weight > b.weight end)

	local resultPart = {}
	table.insert(resultPart, '\r\n{|class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"\r\n!colspan="2"| Item\r\n!Value\r\n!colspan="2"|Chance')
	for i, itemDef in ipairs(itemArray) do
		local item = Items.getItemByID(itemDef.itemID)
		if item ~= nil then
			local dropChance = itemDef.weight / totalWt * 100
			-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
			local fmt = (dropChance < 0.10 and '%.2g') or '%.2f'
			table.insert(resultPart, '\r\n|-\r\n|style="text-align:center"| ' .. Icons.Icon({item.name, type='item', notext=true}))
			table.insert(resultPart, '\r\n| ' .. Icons.Icon({item.name, type='item', noicon=true}))
			table.insert(resultPart, '\r\n|data-sort-value="' .. item.sellsFor .. '"| ' .. Icons.GP(math.floor(item.sellsFor)))
			table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. itemDef.weight .. '"| ' .. Shared.fraction(itemDef.weight, totalWt))
			table.insert(resultPart, '\r\n|style="text-align:right"| ' .. string.format(fmt, dropChance) .. '%')
			lootValue = lootValue + (dropChance / 100 * item.sellsFor)
		end
	end
	table.insert(resultPart, '\r\n|}\r\nThe average value of a roll on the special fishing loot table is ' .. Icons.GP(Shared.round(lootValue, 2, 0)))
	return table.concat(resultPart)
end

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

	local itemArray = {}
	for i, itemID in ipairs(SkillData.Fishing.junkItemIDs) do
		local item = Items.getItemByID(itemID)
		if item ~= nil then
			table.insert(itemArray, item)
		end
	end
	table.sort(itemArray, function(a, b) return a.name < b.name end)

	for i, item in ipairs(itemArray) do
		table.insert(resultPart, '\r\n|-')
		table.insert(resultPart, '\r\n|style="min-width:25px"| ' .. Icons.Icon({item.name, type='item', notext=true, size=50}))
		table.insert(resultPart, '\r\n| ' .. Icons.Icon({item.name, type='item', noicon=true}))
		table.insert(resultPart, '\r\n|data-sort-value="' .. item.sellsFor .. '"| ' .. Icons.GP(math.floor(item.sellsFor)))
	end
	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
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!!Requirements'
	result = result..'!!XP!!Respawn Time!!Ore Value'

	local mineData = GameData.getEntities(SkillData.Mining.rockData, function(rock) return true end)
	table.sort(mineData, function(a, b) return a.level < b.level end)

	for i, oreData in ipairs(mineData) do
		local ore = Items.getItemByID(oreData.productId)
		local reqText = { Icons._SkillReq('Mining', oreData.level) }
		if oreData.shopItemPurchased ~= nil then
			local purchase = Shop.getPurchaseByID(oreData.shopItemPurchased)
			if purchase ~= nil then
				table.insert(reqText, Shop._getPurchaseIcon({purchase}) .. ' purchased')
			end
		end
		local respawnSort, respawnText = 0, 'N/A'
		if oreData.hasPassiveRegen then
			respawnSort = oreData.baseRespawnInterval / 1000
			respawnText = Shared.timeString(respawnSort, true)
		end
		
		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..'||data-sort-value="' .. oreData.level ..'"| '..table.concat(reqText, '<br/>')..'||style="text-align:right"|'..oreData.baseExperience
		result = result..'||style="text-align:right" data-sort-value="'..respawnSort..'"|'
		result = result..respawnText
		result = result..'||data-sort-value="'..ore.sellsFor..'"|'..Icons.GP(ore.sellsFor)
	end

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

-- TODO TotH has random superior gems also
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'

	local totalWeight = 0
	for i, gemData in ipairs(GameData.rawData.randomGems) do
		totalWeight = totalWeight + gemData.weight
	end

	for i, gemData in ipairs(GameData.rawData.randomGems) do
		local gem = Items.getItemByID(gemData.itemID)
		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.weight / totalWeight * 100)
		result = result..'||data-sort-value="'..gem.sellsFor..'"|'..Icons.GP(gem.sellsFor)
	end

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

function p.getFishTable(frame)
	local recipeList = GameData.getEntities(SkillData.Fishing.fish, function(fish) return true end)
	table.sort(recipeList, function(a, b) return a.level < b.level end)

	-- Determine cooking levels for all fish
	local cookReq = {}
	for i, recipe in ipairs(SkillData.Cooking.recipes) do
		-- This assumes that each raw fish only appears in a single recipe, which is a bit rubbish
		-- but currently holds
		for j, mat in ipairs(recipe.itemCosts) do
			if cookReq[mat.id] == nil then
				cookReq[mat.id] = recipe.level
			end
		end
	end

	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"')
	table.insert(resultPart, '\r\n!Fish\r\n!Name\r\n!' .. Icons.Icon({'Fishing', type='skill', notext=true}) .. ' Level\r\n!Catch Time')
	table.insert(resultPart, '\r\n!XP\r\n!Value\r\n!XP/s\r\n!GP/s')
	table.insert(resultPart, '\r\n!' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' Level')
	for i, recipe in ipairs(recipeList) do
		local fish = Items.getItemByID(recipe.productId)
		if fish ~= nil then
			local timeSortVal = (recipe.baseMinInterval + recipe.baseMaxInterval) / 2000
			local timeStr = string.format("%.1fs - %.1fs", recipe.baseMinInterval / 1000, recipe.baseMaxInterval / 1000)
			local XPs = recipe.baseExperience / timeSortVal
			local GPs = fish.sellsFor / timeSortVal
			local cookSortVal = cookReq[recipe.productId] or 0
			local cookStr = cookReq[recipe.productId] or 'N/A'
			table.insert(resultPart, '\r\n|-')
			table.insert(resultPart, '\r\n|style="text-align:center"| ' .. Icons.Icon({fish.name, type='item', size='50', notext=true}))
			table.insert(resultPart, '\r\n| ' .. Icons.Icon({fish.name, type='item', noicon=true}))
			table.insert(resultPart, '\r\n|style="text-align:right"| ' .. recipe.level)
			table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. timeSortVal .. '"| ' .. timeStr)
			table.insert(resultPart, '\r\n|style="text-align:right"| ' .. recipe.baseExperience)
			table.insert(resultPart, '\r\n|data-sort-value="' .. fish.sellsFor .. '"| ' .. Icons.GP(fish.sellsFor))
			table.insert(resultPart, '\r\n|style="text-align:right"| ' .. Shared.round(XPs, 2, 2))
			table.insert(resultPart, '\r\n|data-sort-value="' .. GPs .. '"|' .. Icons.GP(Shared.round(GPs, 2, 2)))
			table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. cookSortVal .. '"| ' .. cookStr)
		end
	end
	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
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 ipairs(SkillData.Fishing.areas) do
		result = result..'\r\n|-'
		result = result..'\r\n| style ="text-align: left;" |'..area.name

		local fishArray = {}
		for j, fishID in ipairs(area.fishIDs) do
			local fishItem = Items.getItemByID(fishID)
			if fishItem ~= nil then
				table.insert(fishArray, Icons.Icon({fishItem.name, type='item'}))
			end
		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.getThievingGeneralRareTable(frame)
	return p._getThievingGeneralRareTable()
end

-- TODO Expose list of NPCs somewhere when applicable (e.g. for Thieving Shorts)
function p._getThievingGeneralRareTable(npcID)
	local rareTxt = '{|class="wikitable sortable"'
	rareTxt = rareTxt..'\r\n!Item!!Qty'
	rareTxt = rareTxt..'!!Price!!colspan="2"|Chance'
	for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
		-- If an npcID has been passed and the item is NPC specific, only display
		-- the item if it may be obtained while pickpocketing that NPC
		if npcID == nil or drop.npcs == nil or Shared.contains(drop.npcs, npcID) then
			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
	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 = {}
		table.insert(normalTxt, '===Possible Common Drops:===\r\nUp to one of these will be received on a successful pickpocket:')
		local totalWt = 0
		local lootChance = SkillData.Thieving.itemChance
		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.weight
		end

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

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

			if loot.minQuantity ~= loot.maxQuantity then
				table.insert(normalTxt, Shared.formatnum(loot.minQuantity) .. ' - ' .. Shared.formatnum(loot.maxQuantity))
			else
				table.insert(normalTxt, Shared.formatnum(loot.maxQuantity))
			end

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

			--Getting the drop chance
			local dropChance = (loot.weight / totalWt * lootChance)
			if dropChance < 100 then
				--Show fraction as long as it isn't going to be 1/1
				table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..loot.weight..'"')
				table.insert(normalTxt, '|'..Shared.fraction(loot.weight * lootChance, totalWt * 100))
				table.insert(normalTxt, '||')
			else
				table.insert(normalTxt, '||colspan="2" data-sort-value="'..loot.weight..'"')
			end
			table.insert(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 * (loot.minQuantity + loot.maxQuantity) / 2)
		end
		if Shared.tableCount(npc.lootTable) > 1 then
			table.insert(normalTxt, '\r\n|-class="sortbottom" \r\n!colspan="3"|Total:')
			if lootChance < 100 then
				table.insert(normalTxt, '\r\n|style="text-align:right"|'..Shared.fraction(lootChance, 100)..'||')
			else
				table.insert(normalTxt, '\r\n|colspan="2" ')
			end
			table.insert(normalTxt, 'style="text-align:right"|'..Shared.round(lootChance, 2, 2)..'%')
		end
		table.insert(normalTxt, '\r\n|}')
		table.insert(normalTxt, '\r\nThe loot obtained from the average successful pickocket is worth ' .. Icons.GP(Shared.round(lootValue, 2, 2)) .. ' if sold.')
		table.insert(normalTxt, '\r\n\r\nIncluding GP, the average successful pickocket is worth ' .. Icons.GP(Shared.round(lootValue + (1 + npc.maxGP) / 2, 2, 2)) .. '.')
		table.insert(sectionTxt, table.concat(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(npc.id)
	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 = Skills.getThievingNPCArea(npc)
	areaTxt = areaTxt..'\r\n{|class="wikitable sortable"'
	areaTxt = areaTxt..'\r\n!Item!!Qty'
	areaTxt = areaTxt..'!!Price!!colspan="2"|Chance'
	local dropLines = {}
	for i, drop in ipairs(area.uniqueDrops) do
		local thisItem = Items.getItemByID(drop.id)
		local lineTxt = ''
		lineTxt = lineTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
		lineTxt = lineTxt..'||data-sort-value="'..drop.quantity..'"| '..Shared.formatnum(drop.quantity)..'||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
		lineTxt = lineTxt..'||style="text-align:right"|'..Shared.fraction(1, 1/(SkillData.Thieving.baseAreaUniqueChance/100))
		lineTxt = lineTxt..'||'..Shared.round(SkillData.Thieving.baseAreaUniqueChance, 2, 2)..'%'
		dropLines[thisItem.name] = lineTxt
	end
	for i, txt in pairs(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/(SkillData.Thieving.baseAreaUniqueChance/100))..'||'
	areaTxt = areaTxt..'style="text-align:right"|'..Shared.round(SkillData.Thieving.baseAreaUniqueChance, 2, 2)..'%'
	areaTxt = areaTxt..'\r\n|}'
	table.insert(sectionTxt, areaTxt)

	if npc.uniqueDrop ~= nil and npc.uniqueDrop.id ~= nil then
		local thisItem = Items.getItemByID(npc.uniqueDrop.id)
		if thisItem ~= nil 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.'
			uniqueTxt = uniqueTxt..'\r\nThe unique drop for the '..npc.name..' is '
			if npc.uniqueDrop.quantity > 1 then
				uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item', qty=npc.uniqueDrop.quantity})
			else
				uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item'})
			end
			table.insert(sectionTxt, uniqueTxt)
		end
	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 = Skills.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 = GameData.getEntities(SkillData.Thieving.npcs, function(npc) return true end)
	table.sort(npcArray, function(a, b) return a.level < b.level end)
	for i, npc in ipairs(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 = Skills.getThievingNPCArea(npc)
		result = result..'||'..area.name
		result = result..'||data-sort-value="' .. npc.level .. '"| '..Icons._SkillReq('Thieving', npc.level)
		result = result..'||style="text-align:right"|'..npc.baseExperience
		result = result..'||style="text-align:right"|'..(npc.maxHit * 10)
		result = result..'||style="text-align:right" data-sort-value="' .. npc.perception .. '| '..Shared.formatnum(npc.perception)
		result = result..'||data-sort-value="' .. npc.maxGP .. '"|'..Icons.GP(1, npc.maxGP)
		if npc.uniqueDrop ~= nil then
			local uniqueDrop = Items.getItemByID(npc.uniqueDrop.id)
			if npc.uniqueDrop.quantity > 1 then
				result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item', qty = npc.uniqueDrop.quantity})
			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')

	for i, area in ipairs(SkillData.Thieving.areas) 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.npcIDs ~= nil and not Shared.tableIsEmpty(area.npcIDs) then
			for j, npcID in ipairs(area.npcIDs) do
				local npc = Skills.getThievingNPCByID(npcID)
				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.id)
				if areaItem == nil then
					table.insert(areaItemList, 'Unknown[[Category:Pages with script errors]]')
				else
					local iconDef = {areaItem.name, type='item'}
					if drop.quantity > 1 then
						iconDef.qty = drop.quantity
					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._getFarmingTable(categoryName)
	local category = GameData.getEntityByName(SkillData.Farming.categories, categoryName)
	if category == nil then
		return 'ERROR: Invalid farming category. Please choose Allotments, Herbs, or Trees[[Category:Pages with script errors]]'
	end
	local seedList = GameData.getEntities(SkillData.Farming.recipes,
		function(recipe)
			return recipe.categoryID == category.id
		end)
	if Shared.tableIsEmpty(seedList) then
		return ''
	end

	local result = '{|class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
	result = result..'!!XP!!Growth Time!!Seed Value'
	if category.id == 'melvorD:Allotment' then
		result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
	elseif category.id == 'melvorD:Herb' then
		result = result..'!!colspan="2"|Herb!!Herb Value'
	elseif category.id == 'melvorD:Tree' then
		result = result..'!!colspan="2"|Logs!!Log Value'
	end
	result = result..'!!Seed Sources'

	table.sort(seedList, function(a, b) return a.level < b.level end)

	for i, seed in ipairs(seedList) do
		local seedItem = Items.getItemByID(seed.seedCost.id)
		local productItem = Items.getItemByID(seed.productId)
		if seedItem ~= nil and productItem ~= nil then
			result = result..'\r\n|-'
			result = result..'\r\n|'..Icons.Icon({seedItem.name, type='item', size='50', notext=true})..'||[['..seedItem.name..']]'
			result = result..'||'..seed.level..'||'..Shared.formatnum(seed.baseExperience)
			result = result..'||data-sort-value="'..(seed.baseInterval / 1000)..'"|'..Shared.timeString(seed.baseInterval / 1000, true)
			result = result..'||data-sort-value="'..seedItem.sellsFor..'"|'..Icons.GP(seedItem.sellsFor)
			result = result..'||'..Icons.Icon({productItem.name, type='item', size='50', notext=true})..'||[['..productItem.name..']]'
			if category.id == 'melvorD:Allotment' then
				result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..((productItem.healsFor or 0) * 10)
			end
			result = result..'||data-sort-value="'..productItem.sellsFor..'"|'..Icons.GP(productItem.sellsFor)
			result = result..'||'..ItemSourceTables._getItemSources(seedItem)
		end
	end

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

function p.getFarmingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame

	return p._getFarmingTable(category)
end

function p.getFarmingFoodTable(frame)
	local result = '{| class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Crop!!'..Icons.Icon({"Farming", type="skill", notext=true})..' Level'
	result = result..'!!Healing!!Value'

	local recipes = GameData.getEntities(SkillData.Farming.recipes,
		function(recipe)
			local product = Items.getItemByID(recipe.productId)
			return product ~= nil and product.healsFor ~= nil and product.healsFor > 0
		end)
	table.sort(recipes, function(a, b) return a.level < b.level end)

	for i, recipe in ipairs(recipes) do
		local product = Items.getItemByID(recipe.productId)
		if product ~= nil and product.healsFor ~= nil and product.healsFor > 0 then
			result = result..'\r\n|-'
			result = result..'\r\n|'..Icons.Icon({product.name, type='item', notext='true', size='50'})..'||[['..product.name..']]'
			result = result..'||style="text-align:right;"|'..recipe.level
			result = result..'||style="text-align:right" data-sort-value="'..product.healsFor..'"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..(product.healsFor * 10)
			result = result..'||style="text-align:right" data-sort-value="'..product.sellsFor..'"|'..Icons.GP(product.sellsFor)
		end
	end

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

	return result
end

function p.getFarmingPlotTable(frame)
	local areaName = frame.args ~= nil and frame.args[1] or frame
	local category = GameData.getEntityByName(SkillData.Farming.categories, areaName)
	if category == nil then
		return 'ERROR: Invalid farming category. Please choose Allotments, Herbs, or Trees[[Category:Pages with script errors]]'
	end
	local patches = GameData.getEntities(SkillData.Farming.plots,
		function(plot)
			return plot.categoryID == category.id
		end)
	if Shared.tableIsEmpty(patches) then
		return ''
	end

	local result = '{|class="wikitable sortable stickyHeader"'
	result = result..'\r\n!Plot!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level!!Cost'

	for i, patch in Shared.skpairs(patches) do
		result = result..'\r\n|-\r\n|'..i
		result = result..'||style="text-align:right;" data-sort-value="' .. patch.level .. '"|'..patch.level
		local costText = (patch.gpCost > 0 and Icons.GP(patch.gpCost)) or 'Free'
		result = result..'||style="text-align:right;" data-sort-value="'..patch.gpCost..'"|'..costText
	end

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

function p._buildAstrologyConstellationTable()
	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 ipairs(SkillData.Astrology.recipes) 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.baseExperience

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

		--Adding a function that converts an array of connected bonuses into text [Falterfire 22/10/27]
		local groupedModsToText = function(allMods)
				local outArray = {}
				for i, group in ipairs(allMods) do
					local groupTxt = '<span style="1px solid black;padding:3px">'..table.concat(group, ' &<br/> ')..'</span>'
					table.insert(outArray, groupTxt)
				end
				return table.concat(outArray, '<br/>')
			end

		local standModsRaw = Skills._buildAstrologyModifierArray(cons, nil, true, false, false, false)
		local standMods = {}
		--Building the list of Standard modifiers:
		
		for j, modifier in ipairs(standModsRaw) do
			local modMagnitude = type(modifier[2]) == 'table' and {modifier[2]} or modifier[2]
			local groupNum = modifier.group
			if standMods[groupNum] == nil then standMods[groupNum] = {} end
			table.insert(standMods[groupNum], Constants._getModifierText(modifier[1], modMagnitude, false))
		end
		result = result..'|| '..groupedModsToText(standMods)

		--Building the list of all Unique Modifiers
		local uModsRaw = Skills._buildAstrologyModifierArray(cons, nil, false, true, false, false)
		local uMods = {}
		for j, modifier in ipairs(uModsRaw) do
			local modMagnitude = type(modifier[2]) == 'table' and {modifier[2]} or modifier[2]
			local groupNum = modifier.group
			if uMods[groupNum] == nil then uMods[groupNum] = {} end
			table.insert(uMods[groupNum], Constants._getModifierText(modifier[1], modMagnitude, false))
		end
		result = result..'|| '..groupedModsToText(uMods)
	end
	result = result..'\r\n|}'

	return result
end

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

return p