Module:Shop

From Melvor Idle
Revision as of 07:35, 3 February 2022 by Auron956 (talk | contribs) (Add icon/link overrides for new Golbin Raid purchases)
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:Shop/doc

local p = {}

local ShopData = mw.loadData('Module:Shop/data')
local ConstantData = mw.loadData('Module:Constants/data')
-- Data instead of Module:CombatAreas to avoid loop whne that module attempts to require Module:Shop
local AreaData = require('Module:CombatAreas/data')

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

-- Overrides for various items, mostly relating to icon overrides
local purchOverrides = {
	["Extra Bank Slot"] = { icon = {'Bank Slot', 'upgrade'}, link = 'Bank Slot', cost = Icons.Icon({'Coins', size = 25, notext = true}) .. ' <span style="font-size:127%; font-family: MathJax_Math; font-style: italic;">C<sub>b</sub></span>*' },
	-- Golbin Raid items
	["Reduce Wave Skip Cost"] = { icon = {'Melvor Logo', nil}, link = nil },
	["Food Bonus"] = { icon = {'Melvor Logo', nil}, link = nil },
	["Ammo Gatherer"] = { icon = {'Melvor Logo', nil}, link = nil },
	["Rune Pouch"] = { icon = {'Melvor Logo', nil}, link = nil },
	["Increase Starting Prayer Points"] = { icon = {'Melvor Logo', nil}, link = nil },
	["Unlock Combat Passive Slot"] = { icon = {'Melvor Logo', nil}, link = nil },
	["Prayer"] = { icon = {'Prayer', 'skill'}, link = nil },
	["Increase Prayer Level"] = { icon = {'Prayer', 'skill'}, link = nil },
	["Increase Prayer Points gained per Wave Completion"] = { icon = {'Prayer', 'skill'}, link = nil },
	["Faster Golbin Spawns"] = { icon = {'Timer', nil}, link = nil},
	["Golbin Crate"] = { icon = {'Golbin Crate', 'upgrade'}, link = nil}
}

function p.getPurchase(purchaseName)
	for categoryName, categoryData in pairs(ShopData.Shop) do
		for i, purchase in ipairs(categoryData) do
			if purchase.name == purchaseName then
				return p.processPurchase(categoryName, i - 1)
			end
		end
	end
end

function p.processPurchase(category, purchaseID)
	local purchase = Shared.clone(ShopData.Shop[category][purchaseID + 1])
	purchase.id = purchaseID
	purchase.category = category
	return purchase
end

function p._getPurchaseStat(purchase, stat, inline)
	local displayInline = (inline ~= nil and inline or false)
	if stat == 'cost' then
		return p.getCostString(purchase.cost, displayInline)
	elseif stat == 'requirements' then
		return p.getRequirementString(purchase.unlockRequirements)
	elseif stat == 'contents' then
		return p._getPurchaseContents(purchase, true)
	elseif stat == 'type' then
		return p._getPurchaseType(purchase)
	else
		return purchase[stat]
	end
end

function p.getPurchaseStat(frame)
	local args = frame.args ~= nil and frame.args or frame
	local purchaseName = args[1]
	local statName = args[2]
	local displayInline = (args['inline'] ~= nil and string.lower(args['inline']) == 'true' or false)
	-- Hack for some purchases existing twice with varying costs (e.g. 'Extra Equipment Set')
	local purchaseList = {}
	if statName == 'cost' then
		purchaseList = p.getPurchases(function(cat, purch) return purch.name == purchaseName end)
	else
		purchaseList = {p.getPurchase(purchaseName)}
	end

	if Shared.tableCount(purchaseList) == 0 then
		return "ERROR: Couldn't find purchase with name '" .. purchaseName .. "'[[Category:Pages with script errors]]"
	else
		local resultPart = {}
		for i, purchase in ipairs(purchaseList) do
			table.insert(resultPart, p._getPurchaseStat(purchase, statName, displayInline))
		end
		return table.concat(resultPart, ' or ')
	end
end

function p.getCostString(cost, inline)
	local displayInline = (inline ~= nil and inline or false)
	local costArray = {}
	if cost.gp ~= nil and cost.gp > 0 then
		table.insert(costArray, Icons.GP(cost.gp))
	end
	if cost.slayerCoins ~= nil and cost.slayerCoins > 0 then
		table.insert(costArray, Icons.SC(cost.slayerCoins))
	end
	if cost.raidCoins ~= nil and cost.raidCoins > 0 then
		table.insert(costArray, Icons.RC(cost.raidCoins))
	end
	local itemArray = {}
	if cost.items ~= nil then
		for i, itemCost in Shared.skpairs(cost.items) do
			local item = Items.getItemByID(itemCost[1])
			table.insert(itemArray, Icons.Icon({item.name, type="item", notext=(not displayInline and true or nil), qty=itemCost[2]}))
		end

		if Shared.tableCount(itemArray) > 0 then
			table.insert(costArray, table.concat(itemArray, ", "))
		end
	end

	local sep, lastSep = '<br/>', '<br/>'
	if displayInline then
		sep = ', '
		lastSep = Shared.tableCount(costArray) > 2 and ', and ' or ' and '
	end
	return mw.text.listToText(costArray, sep, lastSep)
end

function p.getRequirementString(reqs)
	if reqs == nil or Shared.tableCount(reqs) == 0 then
		return "None"
	end

	local reqArray = {}
	if reqs.slayerTaskCompletion ~= nil then
		for i, taskReq in Shared.skpairs(reqs.slayerTaskCompletion) do
			local tierName = Constants.getSlayerTierName(taskReq[1])
			table.insert(reqArray, 'Complete '..taskReq[2]..' '..tierName..' Slayer Tasks')
		end
	end

	if reqs.dungeonCompletion ~= nil then
		for i, dungReq in Shared.skpairs(reqs.dungeonCompletion) do
			local dung = AreaData['dungeons'][dungReq[1] + 1]
			local dungStr = 'Complete '..Icons.Icon({dung.name, type='dungeon'})
			if dungReq[2] > 1 then
				dungStr = dungStr..' '..dungReq[2]..' times'
			end
			table.insert(reqArray, dungStr)
		end
	end

	if reqs.skillLevel ~= nil then
		for i, skillReq in Shared.skpairs(reqs.skillLevel) do
			local skillName = Constants.getSkillName(skillReq[1])
			table.insert(reqArray, Icons._SkillReq(skillName, skillReq[2]))
		end
	end

	if reqs.shopItemPurchased ~= nil then
		for i, shopReq in Shared.skpairs(reqs.shopItemPurchased) do
			local purchase = ShopData.Shop[shopReq[1]][shopReq[2] + 1]
			local isUpgrade = purchase.contains.items == nil or Shared.tableCount(purchase.contains.items) == 0
			table.insert(reqArray, Icons.Icon({purchase.name, type=(isUpgrade and 'upgrade' or 'item')})..' Purchased')
		end
	end

	if reqs.completionPercentage ~= nil then
		table.insert(reqArray, tostring(reqs.completionPercentage) .. '% Completion Log')
	end

	if reqs.text ~= nil then
		table.insert(reqArray, reqs.text)
	end

	return table.concat(reqArray, '<br/>')
end

function p._getPurchaseType(purchase)
	if purchase.contains == nil then
		return 'Unknown'
	elseif purchase.contains.pet ~= nil then
		return 'Pet'
	elseif purchase.contains.modifiers ~= nil or purchase.contains.items == nil or Shared.tableCount(purchase.contains.items) == 0 then
		return 'Upgrade'
	elseif purchase.contains.items ~= nil and Shared.tableCount(purchase.contains.items) > 1 then
		return 'Item Bundle'
	else
		return 'Item'
	end
end

function p._getPurchaseContents(purchase, asList)
	if asList == nil then asList = true end
	local containArray = {}
	if purchase.contains.items ~= nil and Shared.tableCount(purchase.contains.items) > 0 then
		if not asList then
			table.insert(containArray, '{| class="wikitable sortable stickyHeader"')
			table.insert(containArray, '|- class="headerRow-0"')
			table.insert(containArray, '! colspan="2" | Item !! Quantity')
		end
		for i, itemLine in Shared.skpairs(purchase.contains.items) do
			local item = Items.getItemByID(itemLine[1])
			if asList then
				table.insert(containArray, Icons.Icon({item.name, type='item', qty=itemLine[2]}))
			else
				table.insert(containArray, '|-\r\n| style="min-width:25px"| ' .. Icons.Icon({item.name, type='item', notext=true, size='25'}))
				table.insert(containArray, '| ' .. Icons.Icon({item.name, type='item', noicon=true}) .. '\r\n| data-sort-value="' .. itemLine[2] .. '" style="text-align:right" | ' .. Shared.formatnum(itemLine[2]))
			end
		end
	end
	if purchase.charges ~= nil and purchase.charges > 0 then
		if asList then
			table.insert(containArray, '+'..purchase.charges..' '..Icons.Icon({purchase.name, type='item'})..' Charges')
		else
			table.insert(containArray, '|-\r\n| style="min-width:25px"| ' .. Icons.Icon({purchase.name, type='item', notext=true, size='25'}))
			table.insert(containArray, '| ' .. Icons.Icon({item.name, type='item', noicon=true}) .. ' Charges\r\n| data-sort-value="' .. purchase.charges .. '" style="text-align:right" | ' .. Shared.formatnum(purchase.charges))
		end
	end
	if not asList and Shared.tableCount(containArray) > 0 then table.insert(containArray, '|}') end

	local delim = (asList and '<br/>' or '\r\n')
	return table.concat(containArray, delim)
end

function p.getPurchaseContents(frame)
	local args = frame.args ~= nil and frame.args or frame
	local purchaseName = args[1]
	local asList = (args[2] ~= nil and string.upper(args[2]) == 'TRUE')
	local purchase = p.getPurchase(purchaseName)

	if purchase == nil then
		return "ERROR: Couldn't find purchase with name '" .. purchaseName .. "'[[Category:Pages with script errors]]"
	else
		return p._getPurchaseContents(purchase, asList)
	end
end

function p._getPurchaseBuyLimit(purchase, asList)
	if asList == nil then asList = true end
	if type(purchase.buyLimit) == 'table' then
		local limitTable = {}
		local gamemodeHasIcon = { 1, 2 }
		-- Populate limitTable for each game mode to be included
		for id, modeName in pairs(ConstantData.gamemode) do
			if tonumber(id) ~= nil and string.upper(modeName) ~= 'CHAOS' then
				local buyLimit = tostring(purchase.buyLimit[id + 1])
				if limitTable[buyLimit] == nil then
					limitTable[buyLimit] = {}
				end
				local gamemodeText = '[[Game Mode#' .. modeName .. '|' .. modeName .. ']]'
				if Shared.contains(gamemodeHasIcon, id) then
					gamemodeText = Icons.Icon({modeName, notext=(not asList or nil)})
				end
				table.insert(limitTable[buyLimit], gamemodeText)
			end
		end
		
		local numLimits = Shared.tableCount(limitTable)
		local resultPart = {}
		for buyLimit, gameModes in Shared.skpairs(limitTable, true) do
			local limitText = (buyLimit == '0' and 'Unlimited' or tostring(buyLimit))
			if numLimits == 1 then
				-- Buy limit is the same for all game modes
				return limitText
			else
				table.insert(resultPart, limitText .. (asList and ' for ' or ' ') .. mw.text.listToText(gameModes, ', ', (asList and ' and ' or ', ')))
			end
		end
		return table.concat(resultPart, (asList and ' or ' or '<br/>'))
	end
end

function p.getPurchaseBuyLimit(frame)
	local args = frame.args ~= nil and frame.args or frame
	local purchaseName = args[1]
	local asList = (args[2] ~= nil and string.upper(args[2]) == 'TRUE')
	local purchase = p.getPurchase(purchaseName)
	
	if purchase == nil then
		return "ERROR: Couldn't find purchase with name '" .. purchaseName .. "'[[Category:Pages with script errors]]"
	else
		return p._getPurchaseBuyLimit(purchase, asList)
	end
end

-- Accept similar arguments to Icons.Icon
function p._getPurchaseIcon(iconArgs)
	local purchase = iconArgs[1]
	local override = purchOverrides[purchase.name]
	local purchType = p._getPurchaseType(purchase)
	-- Amend iconArgs before passing to Icons.Icon()
	iconArgs[1] = ((override ~= nil and override.icon[1]) or purchase.name)
	if override ~= nil then
		iconArgs['type'] = override.icon[2]
		if override.link == nil then
			iconArgs['nolink'] = true
		end
	else
		iconArgs['type'] = (purchType == 'Item Bundle' and 'item') or string.lower(purchType)
	end

	return Icons.Icon(iconArgs)
end

function p.getPurchaseIcon(frame)
	local args = frame.args ~= nil and frame.args or frame
	local purchaseName = args[1]
	local purchase = p.getPurchase(purchaseName)

	if purchase == nil then
		return "ERROR: Couldn't find purchase with name '" .. purchaseName .. "'[[Category:Pages with script errors]]"
	else
		args[1] = purchase
		return p._getPurchaseIcon(args)
	end
end

function p._getPurchaseSortValue(purchase)
	local costCurrencies = {'gp', 'slayerCoins', 'raidCoins'}
	for j, curr in ipairs(costCurrencies) do
		local costAmt = purchase.cost[curr]
		if costAmt ~= nil and costAmt > 0 then
			return costAmt
		end
	end
end

function p._getShopTable(Purchases, options)
	local availableColumns = { 'Purchase', 'Type', 'Description', 'Cost', 'Requirements', 'Buy Limit' }
	local headerPropsDefault = {
		["Purchase"] = 'colspan="2"',
		["Cost"] = 'style="min-width:100px"'
	}
	local usedColumns, purchHeader, sortOrder, headerProps = {}, 'Purchase', nil, {}

	-- Process options if specified
	if options ~= nil and type(options) == 'table' then
		-- Custom columns
		if options.columns ~= nil and type(options.columns) == 'table' then
			for i, column in ipairs(options.columns) do
				if Shared.contains(availableColumns, column) then
					table.insert(usedColumns, column)
				end
			end
		end
		-- Purchase column header text
		if options.purchaseHeader ~= nil and type(options.purchaseHeader) == 'string' then
			purchHeader = options.purchaseHeader
		end
		-- Custom sort order
		if options.sortOrder ~= nil and type(options.sortOrder) == 'function' then
			sortOrder = options.sortOrder
		end
		-- Header properties
		if options.headerProps ~= nil and type(options.headerProps) == 'table' then
			headerProps = options.headerProps
		end
	end
	-- Use default columns if no custom columns specified
	if Shared.tableCount(usedColumns) == 0 then
		usedColumns = availableColumns
	end
	if Shared.tableCount(headerProps) == 0 then
		headerProps = headerPropsDefault
	end

	-- Begin output generation
	local resultPart = {}
	-- Generate header
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '|- class="headerRow-0"')
	for i, column in ipairs(usedColumns) do
		local prop = headerProps[column]
		table.insert(resultPart, '!' .. (prop and prop .. '| ' or ' ') .. (column == 'Purchase' and purchHeader or column))
	end

	local purchIterator = nil
	if sortOrder == nil then
		purchIterator = Shared.skpairs
	else
		table.sort(Purchases, sortOrder)
		purchIterator = ipairs
	end
	for i, purchase in purchIterator(Purchases) do
		local purchOverride = nil
		if purchOverrides ~= nil then
			purchOverride = purchOverrides[purchase.name]
		end

		local purchType = p._getPurchaseType(purchase)
		local iconNoLink = nil
		local purchLink = ''
		local costString = p.getCostString(purchase.cost, false)
		if purchOverride ~= nil then
			if purchOverride.link == nil then
				iconNoLink = true
			else
				purchLink = purchOverride.link .. '|'
			end
			if purchOverride.cost ~= nil then costString = purchOverride.cost end
		end

		local purchName = purchase.name
		if iconNoLink == nil or iconNoLink ~= true then purchName = '[[' .. purchLink .. purchName .. ']]' end

		table.insert(resultPart, '|-')
		for j, column in ipairs(usedColumns) do
			if column == 'Purchase' then
				table.insert(resultPart, '|style="min-width:25px"|' .. p._getPurchaseIcon({purchase, notext=true, size='50'}))
				--table.insert(resultPart, '|style="min-width:25px"|' .. Icons.Icon({iconName, type=iconType, notext=true, nolink=iconNoLink, size='50'}))
				table.insert(resultPart, '| ' .. purchName)
			elseif column == 'Type' then
				table.insert(resultPart, '| ' .. purchType)
			elseif column == 'Description' then
				table.insert(resultPart, '| ' .. purchase.description)
			elseif column == 'Cost' then
				local cellProp = '|style="text-align:right;"'
				local sortValue = p._getPurchaseSortValue(purchase)
				if sortValue ~= nil then cellProp = cellProp .. ' data-sort-value="' .. sortValue .. '"' end
				table.insert(resultPart, cellProp .. '| ' .. costString)
			elseif column == 'Requirements' then
				table.insert(resultPart, '| ' .. p.getRequirementString(purchase.unlockRequirements))
			elseif column == 'Buy Limit' then
				local buyLimit = p._getPurchaseBuyLimit(purchase, false)
				local sortValue = (tonumber(buyLimit) == nil and -1 or buyLimit)
				table.insert(resultPart, '| data-sort-value="' .. sortValue .. '"| ' .. buyLimit)
			else
				-- Shouldn't be reached, but will prevent the resulting table becoming horribly mis-aligned if it ever happens
				table.insert(resultPart, '| ')
			end
		end
	end
	table.insert(resultPart, '|}')

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

-- getShopTable parameter definition:
--   columns:        Comma separated values indicating which columns are to be included & the order
--                   in which they are displayed.
--                   Values can be any of: Purchase, Type, Description, Cost, Requirements
--   columnProps:    Comma separated values indicating formatting to be applied to each column. Each
--                   value must be in the format column:property, e.g. Purchase:colspan="2"
--   sortOrder:      A function determining the order in which table items appear
--   purchaseHeader: Specifies header text for the Purchase column if not 'Purchase'
function p.getShopTable(frame)
	local cat = frame.args ~= nil and frame.args[1] or frame
	local options = {}
	if frame.args ~= nil then
		if frame.args.columns ~= nil then options.columns = Shared.splitString(frame.args.columns, ',') end
		if frame.args.purchaseHeader ~= nil then options.purchaseHeader = frame.args.purchaseHeader end
		if frame.args.sortOrder ~= nil then options.sortOrder = frame.args.sortOrder end
		if frame.args.columnProps ~= nil then
			local columnPropValues = Shared.splitString(frame.args.columnProps, ',')
			local columnProps = {}
			for i, prop in pairs(columnPropValues) do
				local propName, propValue = string.match(prop, '^([^:]+):(.*)$')
				if propName ~= nil then
					columnProps[propName] = propValue
				end
			end
			if Shared.tableCount(columnProps) > 0 then options.headerProps = columnProps end
		end
	end
	local shopCat = ShopData.Shop[cat]
	if shopCat == nil then
		return 'ERROR: Invalid category '..cat..'[[Category:Pages with script errors]]'
	else
		return p._getShopTable(shopCat, options)
	end
end

function p.getItemCostArray(itemID)
	local purchaseArray = {}

	for catName, cat in Shared.skpairs(ShopData.Shop) do
		for j, purchase in Shared.skpairs(cat) do
			if purchase.cost.items ~= nil then
				for k, costLine in Shared.skpairs(purchase.cost.items) do
					if costLine[1] == itemID then
						local temp = p.processPurchase(catName, j - 1)
						temp.qty = costLine[2]
						table.insert(purchaseArray, temp)
						break
					end
				end
			end
		end
	end

	return purchaseArray
end

function p.getItemSourceArray(itemID)
	local purchaseArray = {}

	for catName, cat in Shared.skpairs(ShopData.Shop) do
		for j, purchase in Shared.skpairs(cat) do
			if purchase.contains.items ~= nil and purchase.contains.items ~= nil then
				for k, containsLine in Shared.skpairs(purchase.contains.items) do
					if containsLine [1] == itemID then
						local temp = p.processPurchase(catName, j - 1)
						temp.qty = containsLine[2]
						table.insert(purchaseArray, temp)
						break
					end
				end
			end
		end
	end

	return purchaseArray
end

function p.getPurchases(checkFunc)
	local purchaseList = {}
	for category, purchaseArray in Shared.skpairs(ShopData.Shop) do
		for i, purchase in Shared.skpairs(purchaseArray) do
			if checkFunc(category, purchase) then
				table.insert(purchaseList, p.processPurchase(category, i - 1))
			end
		end
	end
	return purchaseList
end

function p._getPurchaseTable(purchase)
	local result = '{| class="wikitable"\r\n|-'
	result = result..'\r\n!colspan="2"|'..Icons.Icon({'Shop'})..' Purchase'
	if purchase.contains.items ~= nil and Shared.tableCount(purchase.contains.items) > 1 then
		result = result..' - '..Icons.Icon({purchase.name, type='item'})
	end

	result = result..'\r\n|-\r\n!style="text-align:right;"|Cost'
	result = result..'\r\n|'..p.getCostString(purchase.cost, false)

	result = result..'\r\n|-\r\n!style="text-align:right;"|Requirements'
	result = result..'\r\n|'..p.getRequirementString(purchase.unlockRequirements)

	result = result..'\r\n|-\r\n!style="text-align:right;"|Contains'
	result = result..'\r\n|style="text-align:right;"|'..p._getPurchaseContents(purchase, true)

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

function p._getItemShopTable(item)
	local tableArray = {}
	local purchaseArray = p.getItemSourceArray(item.id)

	for i, purchase in Shared.skpairs(purchaseArray) do
		table.insert(tableArray, p._getPurchaseTable(purchase))
	end

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

function p.getItemShopTable(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"
	end

	return p._getItemShopTable(item)
end

function p.getShopMiscUpgradeTable()
	local purchList = p.getPurchases(function(cat, purch) return cat == 'General' and string.find(purch.name, '^Auto Eat') == nil end)
	return p._getShopTable(purchList, { columns = { 'Purchase', 'Description', 'Cost', 'Requirements' }, purchaseHeader = 'Upgrade' })
end

function p.getShopSkillcapeTable()
	local capeList = p.getPurchases(function(cat, purch) return cat == 'Skillcapes' end)
	local sortOrderFunc = function(a, b)
								if a.cost.gp == b.cost.gp then
									return a.name < b.name
								else
									return a.cost.gp < b.cost.gp
								end
							end
	return p._getShopTable(capeList, {
			columns = { 'Purchase', 'Description', 'Cost' },
			purchaseHeader = 'Cape',
			sortOrder = sortOrderFunc,
			headerProps = {["Purchase"] = 'colspan="2" style="width:200px;"', ["Cost"] = 'style=width:120px;'}
		})
end

function p.getAutoEatTable()
	local resultPart = {}
	local purchasesAE = p.getPurchases(function(cat, purch) return string.find(purch.name, '^Auto Eat') ~= nil end)

	-- Table header
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '|- class="headerRow-0"')
	table.insert(resultPart, '!colspan="2"|Auto Eat Tier!!Minimum Threshold!!Efficiency!!Max Healing!!Cost')
	-- Rows for each Auto Eat tier
	local mods = {["increasedAutoEatEfficiency"] = 0, ["increasedAutoEatHPLimit"] = 0, ["increasedAutoEatThreshold"] = 0}
	for i, purchase in ipairs(purchasesAE) do
		-- Modifiers must be accumulated as we go
		for modName, modValue in pairs(mods) do
			if purchase.contains.modifiers[modName] ~= nil then
				mods[modName] = mods[modName] + purchase.contains.modifiers[modName]
			end
		end

		local costAmt = p._getPurchaseSortValue(purchase)
		table.insert(resultPart, '|-\r\n|style="min-width:25px; text-align:center;" data-sort-value="' .. purchase.name .. '"| ' .. Icons.Icon({purchase.name, type='upgrade', size=50, notext=true}))
		table.insert(resultPart, '| ' .. Icons.Icon({purchase.name, type='upgrade', noicon=true}))
		table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. mods.increasedAutoEatThreshold .. '" | ' .. Shared.formatnum(Shared.round(mods.increasedAutoEatThreshold, 0, 0)) .. '%')
		table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. mods.increasedAutoEatEfficiency .. '" | ' .. Shared.formatnum(Shared.round(mods.increasedAutoEatEfficiency, 0, 0)) .. '%')
		table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. mods.increasedAutoEatHPLimit .. '" | ' .. Shared.formatnum(Shared.round(mods.increasedAutoEatHPLimit, 0, 0)) .. '%')
		table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. costAmt .. '" | ' .. Icons.GP(costAmt))
	end
	table.insert(resultPart, '|}')

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

function p.getGodUpgradeTable()
	local resultPart = {}
	-- Obtain list of God upgrades: look for skill upgrades which have a dungeon completion
	--   requirement for an area whose name ends with 'God Dungeon'
	local getGodDungeon =
		function(reqs)
			if reqs.dungeonCompletion ~= nil then
				for i, areaReq in ipairs(reqs.dungeonCompletion) do
					local dung = AreaData['dungeons'][areaReq[1] + 1]
					if string.find(dung.name, 'God Dungeon$') ~= nil then return dung end
				end
			end
		end

	local upgradeList = p.getPurchases(
		function(cat, purch)
			if cat == 'SkillUpgrades' and purch.unlockRequirements ~= nil then
				return getGodDungeon(purch.unlockRequirements) ~= nil
			end
			return false
		end)
	if Shared.tableCount(upgradeList) == 0 then return '' end

	-- Table header
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '|- class="headerRow-0"')
	table.insert(resultPart, '!colspan="2"|God Upgrade!!Effect!!Dungeon!!Cost')

	-- Rows for each God upgrade
	for i, upgrade in ipairs(upgradeList) do
		local dung = getGodDungeon(upgrade.unlockRequirements)
		local costSortValue = p._getPurchaseSortValue(upgrade)
		table.insert(resultPart, '|-\r\n|style="min-width:25px; text-align:center;" data-sort-value="' .. upgrade.name .. '"| ' .. Icons.Icon({upgrade.name, type='upgrade', size=50, notext=true}))
		table.insert(resultPart, '| ' .. Icons.Icon({upgrade.name, type='upgrade', noicon=true}))
		table.insert(resultPart, '| ' .. upgrade.description)
		table.insert(resultPart, '| data-sort-value="' .. dung.name .. '"| ' .. Icons.Icon({dung.name, type='dungeon'}))
		table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. costSortValue .. '"| ' .. p.getCostString(upgrade.cost, false))
	end
	table.insert(resultPart, '|}')

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

function p.getCookingUtilityTable(frame)
	local category = nil
	if frame ~= nil then category = frame.args ~= nil and frame.args[1] or frame end
	local validCategories = {'Cooking Fire', 'Furnace', 'Pot'}
	if category == nil or not Shared.contains({'Cooking Fire', 'Furnace', 'Pot'}, category) then
		return 'ERROR: Invalid category specified. Must be one of the following: ' .. mw.text.listToText(validCategories, ', ', ' or ')
	end
	
	local categoryShort = string.match(category, '[^%s]+$')
	local bonusSkillID = Constants.getSkillID('Cooking')
	local bonusColMod, bonusColName = nil, nil
	if category == 'Cooking Fire' then
		bonusColMod = 'increasedSkillXP'
		bonusColName = 'Bonus ' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' XP'
	else
		bonusColMod = 'increasedChanceToDoubleItemsSkill'
		bonusColName = 'Double Items Chance'
	end
	local modsPerfectChance = {'increasedChancePerfectCookFire', 'increasedChancePerfectCookFurnace',
								'increasedChancePerfectCookPot', 'increasedChancePerfectCookGlobal'}
	local totalBonusVal, totalPerfectChance = 0, 0
	local utilityList = p.getPurchases(function(cat, purch) return cat == 'SkillUpgrades' and string.find(purch.name, category .. '$') ~= nil end)
	local resultPart = {}
	
	-- Table header
	table.insert(resultPart, '{| class="wikitable stickyHeader"')
	table.insert(resultPart, '|- class="headerRow-0"')
	table.insert(resultPart, '!colspan="4"| !!colspan="2"|' .. bonusColName .. '!!colspan="2"|Bonus Perfect Chance')
	table.insert(resultPart, '|- class="headerRow-1"')
	table.insert(resultPart, '!colspan="2"|Name!!Level!!Cost' .. string.rep('!!This ' .. categoryShort .. '!!Total', 2))

	-- Row for each upgrade
	for i, utility in ipairs(utilityList) do
		-- First determine bonus XP/doubling chance and perfect chance
		local bonusVal, perfectChance = 0, 0
		if type(utility.contains) == 'table' then
			if type(utility.contains.modifiers) == 'table' then
				for modName, modVal in pairs(utility.contains.modifiers) do
					if modName == bonusColMod and type(modVal) == 'table' then
						-- Bonus XP/doubling
						for skID, skVal in pairs(modVal) do
							if skVal[1] == bonusSkillID then bonusVal = bonusVal + skVal[2] end
						end
					elseif Shared.contains(modsPerfectChance, modName) then
						-- Perfect chance
						perfectChance = perfectChance + modVal
					end
				end
			end
		end
		totalBonusVal = totalBonusVal + bonusVal
		totalPerfectChance = totalPerfectChance + perfectChance

		-- Mangle unlockRequirements so that it only includes skillLevels
		local unlockReqs = {}
		if type(utility.unlockRequirements) == 'table' then
			unlockReqs['skillLevel'] = utility.unlockRequirements.skillLevel
		end

		table.insert(resultPart, '|-')
		table.insert(resultPart, '|style="min-width:25px"|' .. Icons.Icon({utility.name, type='upgrade', size='50', notext=true}))
		table.insert(resultPart, '|' .. utility.name)
		table.insert(resultPart, '|style="text-align:right"|' .. p.getRequirementString(unlockReqs))
		table.insert(resultPart, '|style="text-align:right"|' .. p.getCostString(utility.cost, false))
		table.insert(resultPart, '|style="text-align:right"|' .. '+' .. bonusVal .. '%')
		table.insert(resultPart, '|style="text-align:right"|' .. '+' .. totalBonusVal .. '%')
		table.insert(resultPart, '|style="text-align:right"|' .. '+' .. perfectChance .. '%')
		table.insert(resultPart, '|style="text-align:right"|' .. '+' .. totalPerfectChance .. '%')
	end
	table.insert(resultPart, '|}')

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

return p