Module:Shop: Difference between revisions

From Melvor Idle
m (Fix typo)
m (Remove % suffix from sight/survey range)
 
(76 intermediate revisions by 8 users not shown)
Line 1: Line 1:
local p = {}
local p = {}
local ShopData = mw.loadData('Module:Shop/data')
local ConstantData = mw.loadData('Module:Constants/data')
-- Data instead of Module:CombatAreas to avoid loop when that module attempts to require Module:Shop
local AreaData = require('Module:CombatAreas/data')


local Shared = require('Module:Shared')
local Shared = require('Module:Shared')
local Constants = require('Module:Constants')
local GameData = require('Module:GameData')
local Common = require('Module:Common')
local Modifiers = require('Module:Modifiers')
local Items = require('Module:Items')
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')
local Constants = require('Module:Constants')
local Pets = require('Module:Pets')
 
local Num = require('Module:Number')
-- 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, incCost = true},
["Golbin Crate"] = { icon = {'Golbin Crate', 'upgrade'}, link = nil, incCost = true}
}


function p.getPurchase(purchaseName)
function p.getPurchase(purchaseName)
for categoryName, categoryData in pairs(ShopData.Shop) do
local purchList = p.getPurchases(function(purch) return Common.getPurchaseName(purch) == purchaseName end)
for i, purchase in ipairs(categoryData) do
if purchList ~= nil and not Shared.tableIsEmpty(purchList) then
if purchase.name == purchaseName then
return purchList[1]
return p.processPurchase(categoryName, i - 1)
end
end
end
end
end
end


function p.processPurchase(category, purchaseID)
function p.getPurchaseByID(id)
local purchase = Shared.clone(ShopData.Shop[category][purchaseID + 1])
return GameData.getEntityByID('shopPurchases', id)
purchase.id = purchaseID
end
purchase.category = category
 
return purchase
-- Accepts a function(purchase, name) and a category.
-- Prevents external modules from having to make GameData and Common calls.
function p.getCategoryPurchases(checkFunc, category)
local shopCat = GameData.getEntityByName('shopCategories', category)
if shopCat == nil then
return Shared.printError('Invalid category ' .. shopCat)
end
-- We make a nested func to resolve the item name first, if required.
local func =
function(purchase)
if purchase.category ~= shopCat.id then
return false
end
local name = Common.getPurchaseName(purchase)
return checkFunc(purchase, name)
end
return GameData.getEntities('shopPurchases', func)
end
 
function p.getPurchases(checkFunc)
return GameData.getEntities('shopPurchases', checkFunc)
end
end


Line 50: Line 54:
return p.getCostString(purchase.cost, displayInline)
return p.getCostString(purchase.cost, displayInline)
elseif stat == 'requirements' then
elseif stat == 'requirements' then
return p.getRequirementString(purchase.unlockRequirements)
return Common.getRequirementString(purchase.purchaseRequirements, 'None')
elseif stat == 'contents' then
elseif stat == 'contents' then
return p._getPurchaseContents(purchase, true)
return p._getPurchaseContents(purchase, true)
elseif stat == 'type' then
elseif stat == 'type' then
return p._getPurchaseType(purchase)
return Common.getPurchaseType(purchase)
elseif stat == 'buyLimit' then
elseif stat == 'buyLimit' then
return p._getPurchaseBuyLimit(purchase, not displayInline)
return p._getPurchaseBuyLimit(purchase, not displayInline)
elseif stat == 'buyLimitHardcore' then
return p._getPurchaseBuyLimitNumeric(purchase, 'melvorF:Hardcore')
elseif stat == 'description' then
return p._getPurchaseDescription(purchase)
elseif stat =='expansionicon' then
return p._getPurchaseExpansionIcon(purchase)
else
else
return purchase[stat]
return purchase[stat]
Line 64: Line 74:
function p.getPurchaseStat(frame)
function p.getPurchaseStat(frame)
local args = frame.args ~= nil and frame.args or frame
local args = frame.args ~= nil and frame.args or frame
local purchaseName = args[1]
local purchaseName = Shared.fixPagename(args[1])
local statName = args[2]
local statName = args[2]
local displayInline = (args['inline'] ~= nil and string.lower(args['inline']) == 'true' or false)
local displayInline = (args['inline'] ~= nil and string.lower(args['inline']) == 'true' or false)
Line 70: Line 80:
local purchaseList = {}
local purchaseList = {}
if statName == 'cost' then
if statName == 'cost' then
purchaseList = p.getPurchases(function(cat, purch) return purch.name == purchaseName end)
purchaseList = p.getPurchases(function(purch) return Common.getPurchaseName(purch) == purchaseName end)
else
else
purchaseList = {p.getPurchase(purchaseName)}
purchaseList = {p.getPurchase(purchaseName)}
end
end


if Shared.tableCount(purchaseList) == 0 then
if Shared.tableIsEmpty(purchaseList) then
return "ERROR: Couldn't find purchase with name '" .. purchaseName .. "'[[Category:Pages with script errors]]"
return Shared.printError("Couldn't find purchase with name '" .. purchaseName .. "'")
else
else
local resultPart = {}
local resultPart = {}
Line 86: Line 96:
end
end


function p.getCostString(cost, inline)
function p._getPurchaseExpansionIcon(purch)
local displayInline = (inline ~= nil and inline or false)
if purch.id ~= nil then
local costArray = {}
return Icons.getExpansionIcon(purch.id)
if cost.gp ~= nil and cost.gp > 0 then
elseif purch.contains ~= nil then
table.insert(costArray, Icons.GP(cost.gp))
local item = nil
end
if purch.contains.items ~= nil and not Shared.tableIsEmpty(purch.contains.items) then
if cost.slayerCoins ~= nil and cost.slayerCoins > 0 then
return Icons.getExpansionIcon(purch.contains.items[1].id)
table.insert(costArray, Icons.SC(cost.slayerCoins))
elseif purch.contains.itemCharges ~= nil and not Shared.tableIsEmpty(purch.contains.itemCharges) then
end
return Icons.getExpansionIcon(purch.contains.itemCharges.id)
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
end
 
if Shared.tableCount(itemArray) > 0 then
if purch.contains.petID ~= nil then
table.insert(costArray, table.concat(itemArray, ", "))
return Icons.getExpansionIcon(purch.contains.petID)
end
end
end
end
return ''
end


local sep, lastSep = '<br/>', '<br/>'
function p._getPurchaseDescription(purch)
if displayInline then
if purch.customDescription ~= nil then
sep = ', '
local templateData = p._getPurchaseTemplateData(purch)
lastSep = Shared.tableCount(costArray) > 2 and ', and ' or ' and '
return Shared.applyTemplateData(purch.customDescription, templateData)
elseif purch.contains ~= nil then
local item = nil
if purch.contains.modifiers ~= nil then
return Modifiers.getModifiersText(purch.contains.modifiers, false)
elseif purch.contains.petID ~= nil then
local pet = Pets.getPetByID(purch.contains.petID)
return Pets._getPetEffect(pet)
elseif purch.contains.items ~= nil and Shared.tableCount(purch.contains.items) == 1 then
item = Items.getItemByID(purch.contains.items[1].id)
elseif purch.contains.itemCharges ~= nil then
item = Items.getItemByID(purch.contains.itemCharges.id)
end
if item ~= nil then
if item.customDescription ~= nil then
return item.customDescription
elseif item.modifiers ~= nil then
return Modifiers.getModifiersText(item.modifiers, false)
end
end
end
end
return mw.text.listToText(costArray, sep, lastSep)
return ''
end
end


function p.getRequirementString(reqs)
function p.getCostString(cost, inline)
if reqs == nil or Shared.tableCount(reqs) == 0 then
local displayInline = (inline ~= nil and inline or false)
return "None"
local costArray = {}
end
if cost.currencies ~= nil then
 
for i, costAmt in ipairs(cost.currencies) do
local reqArray = {}
local costStr = p.getCurrencyCostString(costAmt)
if reqs.slayerTaskCompletion ~= nil then
if costStr ~= nil then
for i, taskReq in Shared.skpairs(reqs.slayerTaskCompletion) do
table.insert(costArray, costStr)
local tierName = Constants.getSlayerTierName(taskReq[1])
end
table.insert(reqArray, 'Complete '..taskReq[2]..' '..tierName..' Slayer Tasks')
end
end
end
end
 
if cost.items ~= nil and not Shared.tableIsEmpty(cost.items) then
if reqs.dungeonCompletion ~= nil then
local itemArray = {}
for i, dungReq in Shared.skpairs(reqs.dungeonCompletion) do
for i, itemCost in ipairs(cost.items) do
local dung = AreaData['dungeons'][dungReq[1] + 1]
local item = Items.getItemByID(itemCost.id)
local dungStr = 'Complete '..Icons.Icon({dung.name, type='dungeon'})
if item ~= nil then
if dungReq[2] > 1 then
table.insert(itemArray, Icons.Icon({item.name, type="item", notext=(not displayInline and true or nil), qty=itemCost.quantity}))
dungStr = dungStr..' '..dungReq[2]..' times'
end
end
table.insert(reqArray, dungStr)
end
if not Shared.tableIsEmpty(itemArray) then
table.insert(costArray, table.concat(itemArray, ', '))
end
end
end
end


if reqs.skillLevel ~= nil then
if not Shared.tableIsEmpty(costArray) then
for i, skillReq in Shared.skpairs(reqs.skillLevel) do
local sep, lastSep = '<br/>', '<br/>'
local skillName = Constants.getSkillName(skillReq[1])
if displayInline then
table.insert(reqArray, Icons._SkillReq(skillName, skillReq[2]))
sep = ', '
lastSep = Shared.tableCount(costArray) > 2 and ', and ' or ' and '
end
end
return mw.text.listToText(costArray, sep, lastSep)
end
end
return ''
end


if reqs.shopItemPurchased ~= nil then
-- Generates description template data. See: shop.js, getDescriptionTemplateData()
for i, shopReq in Shared.skpairs(reqs.shopItemPurchased) do
function p._getPurchaseTemplateData(purchase)
local purchase = ShopData.Shop[shopReq[1]][shopReq[2] + 1]
-- qty is a static value of 1 for Bank slots
local isUpgrade = purchase.contains.items == nil or Shared.tableCount(purchase.contains.items) == 0
local templateData = { qty = 1 }
table.insert(reqArray, Icons.Icon({purchase.name, type=(isUpgrade and 'upgrade' or 'item')})..' Purchased')
if purchase.contains ~= nil and purchase.contains.items ~= nil then
for i, itemDef in ipairs(purchase.contains.items) do
templateData['qty' .. i] = itemDef.quantity
end
end
end
end
 
return templateData
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
end


function p._getPurchaseType(purchase)
function p.getCurrencyCostString(cost)
if purchase.contains == nil then
if cost.type == 'BankSlot' then
return 'Unknown'
-- Unusual bit of code that basically evaluates wikitext '<math>C_b</math>*'
elseif purchase.contains.pet ~= nil then
return mw.getCurrentFrame():callParserFunction('#tag:math', {'C_b'}) .. '*'
return 'Pet'
elseif cost.type == 'Linear' and (cost.initial > 0 or cost.scaling > 0) then
elseif purchase.contains.modifiers ~= nil or purchase.contains.items == nil or Shared.tableCount(purchase.contains.items) == 0 then
return Icons._Currency(cost.currency, cost.initial) .. '<br/>+' .. Icons._Currency(cost.currency, cost.scaling) .. ' for each purchase'
return 'Upgrade'
elseif cost.type == 'Glove' or cost.type == 'Fixed' and cost.cost > 0 then
elseif purchase.contains.items ~= nil and Shared.tableCount(purchase.contains.items) > 1 then
-- Type Glove exists in game so the Merchant's Permit cost reduction can be applied,
return 'Item Bundle'
-- it makes no difference here
else
return Icons._Currency(cost.currency, cost.cost)
return 'Item'
end
end
end
end
Line 186: Line 205:
local containArray = {}
local containArray = {}
local GPTotal = 0
local GPTotal = 0
if purchase.contains.items ~= nil and Shared.tableCount(purchase.contains.items) > 0 then
local currency = 'melvorD:GP'
if not asList then
if purchase.contains ~= nil then
table.insert(containArray, '{| class="wikitable sortable stickyHeader"')
if purchase.contains.items ~= nil and not Shared.tableIsEmpty(purchase.contains.items) then
table.insert(containArray, '|- class="headerRow-0"')
if not asList then
table.insert(containArray, '! colspan="2" | Item !! Quantity !! Price')
table.insert(containArray, '{| class="wikitable sortable stickyHeader"')
table.insert(containArray, '|- class="headerRow-0"')
table.insert(containArray, '! colspan="2" | Item !! Quantity !! Price')
end
for i, itemLine in ipairs(purchase.contains.items) do
local item = Items.getItemByID(itemLine.id)
local itemQty = itemLine.quantity
if item.sellsForCurrency ~= nil then
currency = item.sellsForCurrency
end
if asList then
table.insert(containArray, Icons.Icon({item.name, type='item', qty=itemQty}))
else
local GPVal = item.sellsFor * itemQty
GPTotal = GPTotal + GPVal
table.insert(containArray, '|-\r\n| class="table-img"| ' .. Icons.Icon({item.name, type='item', notext=true}))
table.insert(containArray, '|data-sort-value="'..item.name..'"|'.. Icons.getExpansionIcon(item.id) .. Icons.Icon({item.name, type='item', noicon=true}) .. '\r\n| data-sort-value="' .. itemQty .. '" style="text-align:right" | ' .. Num.formatnum(itemQty))
table.insert(containArray, '| data-sort-value="' .. GPVal .. '"| ' .. Icons._Currency(currency, GPVal))
end
end
end
end
for i, itemLine in Shared.skpairs(purchase.contains.items) do
if purchase.contains.itemCharges ~= nil and purchase.contains.itemCharges.quantity > 0 then
local item = Items.getItemByID(itemLine[1])
local gloveItem = Items.getItemByID(purchase.contains.itemCharges.id)
if asList then
local chargeQty = purchase.contains.itemCharges.quantity
table.insert(containArray, Icons.Icon({item.name, type='item', qty=itemLine[2]}))
if gloveItem ~= nil then
else
if gloveItem.sellsForCurrency ~= nil then
local GPVal = item.sellsFor * itemLine[2]
currency = gloveItem.sellsForCurrency
GPTotal = GPTotal + GPVal
end
table.insert(containArray, '|-\r\n| style="min-width:25px"| ' .. Icons.Icon({item.name, type='item', notext=true, size='25'}))
if asList then
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]))
table.insert(containArray, ' +'..Num.formatnum(chargeQty)..' '..Icons.Icon({gloveItem.name, type='item'})..' Charges')
table.insert(containArray, '| data-sort-value="' .. GPVal .. '"| ' .. Icons.GP(GPVal))
else
table.insert(containArray, '|-\r\n| class="table-img"| ' .. Icons.Icon({gloveItem.name, type='item', notext=true}))
table.insert(containArray, '| ' .. Icons.Icon({gloveItem.name, type='item', noicon=true}) .. ' Charges\r\n| data-sort-value="' .. chargeQty .. '" style="text-align:right" | ' .. Num.formatnum(chargeQty))
table.insert(containArray, '| data-sort-value="0"| ' .. Icons._Currency(currency, 0))
end
end
end
end
end
end
end
if purchase.charges ~= nil and purchase.charges > 0 then
if not asList and not Shared.tableIsEmpty(containArray) then
if asList then
table.insert(containArray, '|- class="sortbottom"\r\n! colspan="3"| Total\r\n| ' .. Icons._Currency(currency, GPTotal) .. '\r\n|}')
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({purchase.name, type='item', noicon=true}) .. ' Charges\r\n| data-sort-value="' .. purchase.charges .. '" style="text-align:right" | ' .. Shared.formatnum(purchase.charges))
table.insert(containArray, '| data-sort-value="0"| ' .. Icons.GP(0))
end
end
if not asList and Shared.tableCount(containArray) > 0 then
table.insert(containArray, '|- class="sortbottom"\r\n! colspan="3"| Total\r\n| ' .. Icons.GP(GPTotal) .. '\r\n|}')
end
end


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


if purchase == nil then
if purchase == nil then
return "ERROR: Couldn't find purchase with name '" .. purchaseName .. "'[[Category:Pages with script errors]]"
return Shared.printError("Couldn't find purchase with name '" .. purchaseName .. "'")
else
else
return p._getPurchaseContents(purchase, asList)
return p._getPurchaseContents(purchase, asList)
end
end
end
function p._getPurchaseBuyLimitNumeric(purchase, gamemodeID)
local buyLimit = (purchase.defaultBuyLimit > 0 and purchase.defaultBuyLimit)
if not Shared.tableIsEmpty(purchase.buyLimitOverrides) then
local gamemodeLimit = GameData.getEntityByProperty(purchase.buyLimitOverrides, 'gamemodeID', gamemodeID)
if gamemodeLimit ~= nil and gamemodeLimit.maximum ~= nil then
buyLimit = gamemodeLimit.maximum
end
end
return buyLimit
end
end


function p._getPurchaseBuyLimit(purchase, asList)
function p._getPurchaseBuyLimit(purchase, asList)
if asList == nil then asList = true end
if asList == nil then asList = true end
if type(purchase.buyLimit) == 'table' then
local defaultLimit = (purchase.defaultBuyLimit == 0 and 'Unlimited') or Num.formatnum(purchase.defaultBuyLimit)
if purchase.buyLimitOverrides == nil or Shared.tableIsEmpty(purchase.buyLimitOverrides) then
-- Same limit for all game modes
return defaultLimit
else
-- The limit varies depending on game mode
local limitTable = {}
local limitTable = {}
local gamemodeHasIcon = { 1, 2 }
local gamemodeHasIcon = { 'melvorF:Hardcore', 'melvorF:Adventure' }
-- Populate limitTable for each game mode to be included
for i, buyLimit in ipairs(purchase.buyLimitOverrides) do
for id, modeName in pairs(ConstantData.gamemode) do
local gamemode = GameData.getEntityByID('gamemodes', buyLimit.gamemodeID)
if tonumber(id) ~= nil and string.upper(modeName) ~= 'CHAOS' then
if gamemode ~= nil then
local buyLimit = tostring(purchase.buyLimit[id + 1])
local gamemodeName = Shared.splitString(gamemode.name, ' ')[1]
if limitTable[buyLimit] == nil then
local gamemodeText = nil
limitTable[buyLimit] = {}
if Shared.contains(gamemodeHasIcon, gamemode.id) then
end
gamemodeText = Icons.Icon({gamemodeName, notext=(not asList or nil)})
local gamemodeText = '[[Game Mode#' .. modeName .. '|' .. modeName .. ']]'
else
if Shared.contains(gamemodeHasIcon, id) then
gamemodeText = '[[Game Mode#' .. gamemodeName .. '|' .. gamemodeName .. ']]'
gamemodeText = Icons.Icon({modeName, notext=(not asList or nil)})
end
end
table.insert(limitTable[buyLimit], gamemodeText)
local limitText = (buyLimit.maximum == 0 and 'Unlimited') or Num.formatnum(buyLimit.maximum)
end
table.insert(limitTable, limitText .. (asList and ' for ' or ' ') .. gamemodeText)
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
end
end
return table.concat(resultPart, (asList and ' or ' or '<br/>'))
table.insert(limitTable, defaultLimit .. (asList and ' for ' or ' ') .. 'All other game modes')
return table.concat(limitTable, (asList and ' or ' or '<br/>'))
end
end
end
end
Line 277: Line 315:
if purchase == nil then
if purchase == nil then
return "ERROR: Couldn't find purchase with name '" .. purchaseName .. "'[[Category:Pages with script errors]]"
return Shared.printError("Couldn't find purchase with name '" .. purchaseName .. "'")
else
else
return p._getPurchaseBuyLimit(purchase, asList)
return p._getPurchaseBuyLimit(purchase, asList)
end
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
end


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


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


function p._getPurchaseSortValue(purchase)
function p._getPurchaseSortValue(purchase)
local costCurrencies = {'gp', 'slayerCoins', 'raidCoins'}
if purchase.cost ~= nil and purchase.cost.currencies ~= nil then
for j, curr in ipairs(costCurrencies) do
for _, costAmt in ipairs(purchase.cost.currencies) do
local costAmt = purchase.cost[curr]
-- Find cost for the current currency, if it exists
if costAmt ~= nil and costAmt > 0 then
if costAmt.type == 'BankSlot' then
return costAmt
return -1
elseif costAmt.type == 'Linear' then
return costAmt.initial
elseif costAmt.type == 'Glove' or costAmt.type == 'Fixed' and costAmt.cost > 0 then
return costAmt.cost
end
end
end
end
end
Line 331: Line 355:
["Cost"] = 'style="min-width:100px"'
["Cost"] = 'style="min-width:100px"'
}
}
local usedColumns, purchHeader, sortOrder, headerProps = {}, 'Purchase', nil, {}
local usedColumns, purchHeader, sortOrder, headerProps, stickyHeader = {}, 'Purchase', nil, {}, true


-- Process options if specified
-- Process options if specified
Line 354: Line 378:
if options.headerProps ~= nil and type(options.headerProps) == 'table' then
if options.headerProps ~= nil and type(options.headerProps) == 'table' then
headerProps = options.headerProps
headerProps = options.headerProps
end
-- Sticky header class
if options.stickyHeader ~= nil then
if type(options.stickyHeader) == 'boolean' then
stickyHeader = options.stickyHeader
elseif type(options.stickyHeader) == 'string' and string.lower(options.stickyHeader) == 'false' then
stickyHeader = false
end
end
end
end
end
Line 367: Line 399:
local resultPart = {}
local resultPart = {}
-- Generate header
-- Generate header
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
table.insert(resultPart, '{| class="wikitable sortable' .. (stickyHeader and ' stickyHeader' or '') .. '"')
table.insert(resultPart, '|- class="headerRow-0"')
table.insert(resultPart, '|- class="headerRow-0"')
for i, column in ipairs(usedColumns) do
for i, column in ipairs(usedColumns) do
Line 374: Line 406:
end
end


local purchIterator = nil
if sortOrder == nil then
if sortOrder == nil then
purchIterator = Shared.skpairs
Purchases = GameData.sortByOrderTable(Purchases, GameData.rawData.shopDisplayOrder, true)
else
else
table.sort(Purchases, sortOrder)
table.sort(Purchases, sortOrder)
purchIterator = ipairs
end
end
for i, purchase in purchIterator(Purchases) do
for i, purchase in ipairs(Purchases) do
local purchOverride = nil
local purchName = Common.getPurchaseName(purchase)
if purchOverrides ~= nil then
local purchExpIcon = p._getPurchaseExpansionIcon(purchase)
purchOverride = purchOverrides[purchase.name]
local purchType = Common.getPurchaseType(purchase)
end
 
local purchType = p._getPurchaseType(purchase)
local iconNoLink = nil
local purchLink = ''
local costString = p.getCostString(purchase.cost, false)
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
if purchOverride.incCost then
costString = costString .. '<br/>+' .. costString .. ' for each purchase'
end
end
local purchName = purchase.name
if iconNoLink == nil or iconNoLink ~= true then purchName = '[[' .. purchLink .. purchName .. ']]' end


table.insert(resultPart, '|-')
table.insert(resultPart, '|-')
for j, column in ipairs(usedColumns) do
for j, column in ipairs(usedColumns) do
if column == 'Purchase' then
if column == 'Purchase' then
table.insert(resultPart, '|style="min-width:25px"|' .. p._getPurchaseIcon({purchase, notext=true, size='50'}))
table.insert(resultPart, '|class="table-img"|' .. Common.getPurchaseIcon({purchase, notext=true}))
--table.insert(resultPart, '|style="min-width:25px"|' .. Icons.Icon({iconName, type=iconType, notext=true, nolink=iconNoLink, size='50'}))
table.insert(resultPart, '| data-sort-value="'..purchName..'"|'..purchExpIcon .. Common.getPurchaseIcon({purchase, noicon=true}))
table.insert(resultPart, '| ' .. purchName)
elseif column == 'Type' then
elseif column == 'Type' then
table.insert(resultPart, '| ' .. purchType)
table.insert(resultPart, '| ' .. purchType)
elseif column == 'Description' then
elseif column == 'Description' then
table.insert(resultPart, '| ' .. purchase.description)
table.insert(resultPart, '| ' .. p._getPurchaseDescription(purchase))
elseif column == 'Cost' then
elseif column == 'Cost' then
local cellProp = '|style="text-align:right;"'
local cellProp = '|style="text-align:right;"'
Line 422: Line 432:
table.insert(resultPart, cellProp .. '| ' .. costString)
table.insert(resultPart, cellProp .. '| ' .. costString)
elseif column == 'Requirements' then
elseif column == 'Requirements' then
table.insert(resultPart, '| ' .. p.getRequirementString(purchase.unlockRequirements))
table.insert(resultPart, '| ' .. Common.getRequirementString(purchase.purchaseRequirements, 'None'))
elseif column == 'Buy Limit' then
elseif column == 'Buy Limit' then
local buyLimit = p._getPurchaseBuyLimit(purchase, false)
local buyLimit = p._getPurchaseBuyLimit(purchase, false)
Line 435: Line 445:
table.insert(resultPart, '|}')
table.insert(resultPart, '|}')


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


Line 446: Line 456:
--  sortOrder:      A function determining the order in which table items appear
--  sortOrder:      A function determining the order in which table items appear
--  purchaseHeader: Specifies header text for the Purchase column if not 'Purchase'
--  purchaseHeader: Specifies header text for the Purchase column if not 'Purchase'
-- stickyHeader:  Specifies if the table will have a sticky header or not
function p.getShopTable(frame)
function p.getShopTable(frame)
local cat = frame.args ~= nil and frame.args[1] or frame
local cat = frame.args ~= nil and frame.args[1] or frame
Line 453: Line 464:
if frame.args.purchaseHeader ~= nil then options.purchaseHeader = frame.args.purchaseHeader 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.sortOrder ~= nil then options.sortOrder = frame.args.sortOrder end
if frame.args.stickyHeader ~= nil then options.stickyHeader = frame.args.stickyHeader end
if frame.args.columnProps ~= nil then
if frame.args.columnProps ~= nil then
local columnPropValues = Shared.splitString(frame.args.columnProps, ',')
local columnPropValues = Shared.splitString(frame.args.columnProps, ',')
Line 465: Line 477:
end
end
end
end
local shopCat = ShopData.Shop[cat]
local shopCat = GameData.getEntityByName('shopCategories', cat)
if shopCat == nil then
if shopCat == nil then
return 'ERROR: Invalid category '..cat..'[[Category:Pages with script errors]]'
return Shared.printError('Invalid category ' .. cat)
else
else
return p._getShopTable(shopCat, options)
local catPurchases = p.getPurchases(function(purch) return purch.category == shopCat.id end)
return p._getShopTable(catPurchases, options)
end
end
end
end
Line 475: Line 488:
function p.getItemCostArray(itemID)
function p.getItemCostArray(itemID)
local purchaseArray = {}
local purchaseArray = {}
 
for i, purchase in ipairs(GameData.rawData.shopPurchases) do
for catName, cat in Shared.skpairs(ShopData.Shop) do
if purchase.cost ~= nil and purchase.cost.items ~= nil then
for j, purchase in Shared.skpairs(cat) do
for j, itemCost in ipairs(purchase.cost.items) do
if purchase.cost.items ~= nil then
if itemCost.id == itemID then
for k, costLine in Shared.skpairs(purchase.cost.items) do
table.insert(purchaseArray, { ["purchase"] = purchase, ["qty"] = itemCost.quantity })
if costLine[1] == itemID then
break
local temp = p.processPurchase(catName, j - 1)
temp.qty = costLine[2]
table.insert(purchaseArray, temp)
break
end
end
end
end
end
end
end
end
end
return purchaseArray
return purchaseArray
end
end
Line 496: Line 503:
function p.getItemSourceArray(itemID)
function p.getItemSourceArray(itemID)
local purchaseArray = {}
local purchaseArray = {}
 
for i, purchase in ipairs(GameData.rawData.shopPurchases) do
for catName, cat in Shared.skpairs(ShopData.Shop) do
if purchase.contains ~= nil then
for j, purchase in Shared.skpairs(cat) do
if purchase.contains.items ~= nil then
if purchase.contains.items ~= nil and purchase.contains.items ~= nil then
for j, itemContains in ipairs(purchase.contains.items) do
for k, containsLine in Shared.skpairs(purchase.contains.items) do
if itemContains.id == itemID then
if containsLine [1] == itemID then
table.insert(purchaseArray, { ["purchase"] = purchase, ["qty"] = itemContains.quantity })
local temp = p.processPurchase(catName, j - 1)
temp.qty = containsLine[2]
table.insert(purchaseArray, temp)
break
break
end
end
end
end
end
if purchase.contains.itemCharges ~= nil and purchase.contains.itemCharges.id == itemID then
table.insert(purchaseArray, { ["purchase"] = purchase, ["qty"] = 1 })
end
end
end
end
end
end
return purchaseArray
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
end


Line 531: Line 525:
result = result..'\r\n!colspan="2"|'..Icons.Icon({'Shop'})..' Purchase'
result = result..'\r\n!colspan="2"|'..Icons.Icon({'Shop'})..' Purchase'
if purchase.contains.items ~= nil and Shared.tableCount(purchase.contains.items) > 1 then
if purchase.contains.items ~= nil and Shared.tableCount(purchase.contains.items) > 1 then
result = result..' - '..Icons.Icon({purchase.name, type='item'})
result = result..' - '..p._getPurchaseExpansionIcon(purchase) .. Common.getPurchaseIcon({purchase, type='item'})
end
end


Line 538: Line 532:


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


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


result = result..'\r\n|}'
result = result..'\r\n|}'
Line 551: Line 545:
local purchaseArray = p.getItemSourceArray(item.id)
local purchaseArray = p.getItemSourceArray(item.id)


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


Line 562: Line 556:
local item = Items.getItem(itemName)
local item = Items.getItem(itemName)
if item == nil then
if item == nil then
return "ERROR: No item named "..itemName.." exists in the data module"
return Shared.printError('No item named ' .. itemName .. ' exists in the data module')
end
end


Line 569: Line 563:


function p.getShopMiscUpgradeTable()
function p.getShopMiscUpgradeTable()
local purchList = p.getPurchases(function(cat, purch) return cat == 'General' and string.find(purch.name, '^Auto Eat') == nil end)
-- All purchases in the general category besides Auto Eat, which is conained within a separate table
local purchList = p.getPurchases(function(purch) return purch.category == 'melvorD:General' and string.find(purch.id, '^melvorD:Auto_Eat') == nil end)
 
return p._getShopTable(purchList, { columns = { 'Purchase', 'Description', 'Cost', 'Requirements' }, purchaseHeader = 'Upgrade' })
end
 
function p.getShopSkillUpgradeTable()
-- All purchaes in the SkillUpgrades category except tools and any upgrades displayed as
-- tools (e.g. ship upgrades)
local purchList = p.getPurchases(
function(purch)
return purch.category == 'melvorD:SkillUpgrades'
-- Exclude tools, handled by p.getToolTable()
and string.find(purch.id, '_Axe$') == nil
and string.find(purch.id, '_Axe_Coating$') == nil
and string.find(purch.id, '_Pickaxe$') == nil
and string.find(purch.id, '_Pickaxe_Coating$') == nil
and string.find(purch.id, '_Rod$') == nil
and string.find(purch.id, '_Rod_Coating$') == nil
and string.find(purch.id, '_Harvester$') == nil
and string.find(purch.id, 'Fire$') == nil
and string.find(purch.id, 'Furnace$') == nil
and string.find(purch.id, 'Pot$') == nil
and string.find(purch.id, 'Sieve$') == nil
and string.find(purch.id, 'Trowel$') == nil
and string.find(purch.id, 'Brush$') == nil
and string.find(purch.id, 'Shovel$') == nil
and string.find(purch.id, 'ShipUpgrade') == nil
-- Exclude God upgrades, handled by p.getGodUpgradeTable()
and p.getGodUpgradeDungeon(purch) == nil
end
)
 
return p._getShopTable(purchList, { columns = { 'Purchase', 'Description', 'Cost', 'Requirements' }, purchaseHeader = 'Upgrade' })
return p._getShopTable(purchList, { columns = { 'Purchase', 'Description', 'Cost', 'Requirements' }, purchaseHeader = 'Upgrade' })
end
end


function p.getShopSkillcapeTable()
function p.getPurchaseDescription(frame)
local capeList = p.getPurchases(function(cat, purch) return cat == 'Skillcapes' end)
local itemName = frame.args ~= nil and frame.args[1] or frame
local sortOrderFunc = function(a, b)
local purchase = p.getPurchase(itemName)
if a.cost.gp == b.cost.gp then
if purchase == nil then
return a.name < b.name
return ''
else
end
return a.cost.gp < b.cost.gp
end
return p._getPurchaseDescription(purchase)
end
 
function p.isSkillcapePurchase(purch, isSuperior, skillID)
-- Returns true or false depending on whether the purchase is a skillcape or not.
-- If isSuperior is true, then this checks for superior skillcapes, false checks
-- for regular skillcapes, and nil checks for both.
-- If skillID is specified, then the skillcape must also relate to that skill
local checkCategories = (isSuperior == nil and {'melvorTotH:SuperiorSkillcapes', 'melvorD:Skillcapes'}) or (isSuperior and {'melvorTotH:SuperiorSkillcapes'}) or {'melvorD:Skillcapes'}
-- Some skillcapes (such as Archaeology & Cartography) reside outside of the usual categories
local overrideIDs = {
['melvorTotH:SuperiorSkillcapes'] = {
'melvorAoD:Superior_Archaeology_Skillcape',
'melvorAoD:Superior_Cartography_Skillcape',
'melvorAoD:Cape_of_Completion_AoD'
},
['melvorD:Skillcapes'] = {
'melvorAoD:Archaeology_Skillcape',
'melvorAoD:Cartography_Skillcape',
'melvorItA:Cape_of_Completion_ItA'
}
}
 
for i, cat in ipairs(checkCategories) do
if purch.category == cat or Shared.contains(overrideIDs[cat], purch.id) then
if skillID == nil then
return true
else
-- Also validate purchase requirements for relevant SkillLevel requirement
local hasReq = false
if type(purch.purchaseRequirements) == 'table' then
for j, req in ipairs(purch.purchaseRequirements) do
if req.type == 'SkillLevel' then
if req.skillID == skillID then
hasReq = true
else
-- The presence of any other skill's requirement indicates
-- this is not a skillcape for skill with ID skillID
return false
end
end
end
end
end
return hasReq
end
end
end
return false
end
function p._getShopSkillcapeTable(showSuperior)
local capeList = p.getPurchases(function(purch) return p.isSkillcapePurchase(purch, showSuperior) end)
local sortOrderFunc =
function(a, b)
local costA, costB = p._getPurchaseSortValue(a), p._getPurchaseSortValue(b)
if costA == costB then
return Common.getPurchaseName(a) < Common.getPurchaseName(b)
else
return costA < costB
end
end
return p._getShopTable(capeList, {
return p._getShopTable(capeList, {
columns = { 'Purchase', 'Description', 'Cost' },
columns = { 'Purchase', 'Description', 'Cost' },
purchaseHeader = 'Cape',
purchaseHeader = 'Cape',
sortOrder = sortOrderFunc,
sortOrder = sortOrderFunc,
stickyHeader = false,
headerProps = {["Purchase"] = 'colspan="2" style="width:200px;"', ["Cost"] = 'style=width:120px;'}
headerProps = {["Purchase"] = 'colspan="2" style="width:200px;"', ["Cost"] = 'style=width:120px;'}
})
})
end
end


function p.getAutoEatTable()
function p.getShopSkillcapeTable(frame)
local resultPart = {}
local capeCategory = frame.args ~= nil and frame.args[1] or frame
local purchasesAE = p.getPurchases(function(cat, purch) return string.find(purch.name, '^Auto Eat') ~= nil end)
local showSuperior = string.lower(capeCategory) == 'superior'
 
return p._getShopSkillcapeTable(showSuperior)
end
 
function p.getSkillcapeTable(frame)
local skillName = frame.args ~= nil and frame.args[1] or frame
local skillID = Constants.getSkillID(skillName)
if skillID == nil then
return Shared.printError('No such skill "' .. (skillName or 'nil') .. '"')
end


-- Table header
local capeList = p.getPurchases(function(purch) return p.isSkillcapePurchase(purch, nil, skillID) end)
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
if Shared.tableIsEmpty(capeList) then
table.insert(resultPart, '|- class="headerRow-0"')
return ''
table.insert(resultPart, '!colspan="2"|Auto Eat Tier!!Minimum Threshold!!Efficiency!!Max Healing!!Cost')
else
-- Rows for each Auto Eat tier
capeList = GameData.sortByOrderTable(capeList, GameData.rawData.shopDisplayOrder, true)
local mods = {["increasedAutoEatEfficiency"] = 0, ["increasedAutoEatHPLimit"] = 0, ["increasedAutoEatThreshold"] = 0}
local resultPart = {}
for i, purchase in ipairs(purchasesAE) do
table.insert(resultPart, '{| class="wikitable"\n')
-- Modifiers must be accumulated as we go
table.insert(resultPart, '!Skillcape!!Name!!Requirements!!Effect')
for modName, modValue in pairs(mods) do
for i, cape in ipairs(capeList) do
if purchase.contains.modifiers[modName] ~= nil then
local capeItem = Items.getItemByID(cape.contains.items[1].id)
mods[modName] = mods[modName] + purchase.contains.modifiers[modName]
if capeItem ~= nil then
table.insert(resultPart, '\n|-\n| ' .. Icons.Icon({capeItem.name, type='item', notext=true}))
table.insert(resultPart, '\n| data-sort-value="'..capeItem.name..'"|'..Icons.getExpansionIcon(capeItem.id) .. Icons.Icon({capeItem.name, type='item', noicon=true}))
table.insert(resultPart, '\n| ' .. Common.getRequirementString(cape.purchaseRequirements, 'None'))
table.insert(resultPart, '\n| ' .. p._getPurchaseDescription(cape))
end
end
end
end
table.insert(resultPart, '\n|}')
return table.concat(resultPart)
end
end


local costAmt = p._getPurchaseSortValue(purchase)
function p.getGodUpgradeDungeon(purch)
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}))
-- Identifies skill upgrades which have a dungeon completion requirement for an area
table.insert(resultPart, '| ' .. Icons.Icon({purchase.name, type='upgrade', noicon=true}))
-- whose name ends with 'God Dungeon'. Returns the ID of the dungeon which must be
table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. mods.increasedAutoEatThreshold .. '" | ' .. Shared.formatnum(Shared.round(mods.increasedAutoEatThreshold, 0, 0)) .. '%')
-- completed before the purchase may be bought if the purchase is a god upgrade
table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. mods.increasedAutoEatEfficiency .. '" | ' .. Shared.formatnum(Shared.round(mods.increasedAutoEatEfficiency, 0, 0)) .. '%')
if purch.category == 'melvorD:SkillUpgrades' and type(purch.purchaseRequirements) == 'table' then
table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. mods.increasedAutoEatHPLimit .. '" | ' .. Shared.formatnum(Shared.round(mods.increasedAutoEatHPLimit, 0, 0)) .. '%')
for i, req in ipairs(purch.purchaseRequirements) do
table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. costAmt .. '" | ' .. Icons.GP(costAmt))
if req.type == 'DungeonCompletion' and string.find(req.dungeonID, 'God_Dungeon$') ~= nil then
return req.dungeonID
end
end
end
end
table.insert(resultPart, '|}')
return table.concat(resultPart, '\r\n')
end
end


function p.getGodUpgradeTable()
function p.getGodUpgradeTable()
local resultPart = {}
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(
local upgradeList = p.getPurchases(
function(cat, purch)
function(purch)
if cat == 'SkillUpgrades' and purch.unlockRequirements ~= nil then
return p.getGodUpgradeDungeon(purch) ~= nil
return getGodDungeon(purch.unlockRequirements) ~= nil
end
return false
end)
end)
if Shared.tableCount(upgradeList) == 0 then return '' end
if Shared.tableIsEmpty(upgradeList) then
return ''
end


-- Table header
-- Table header
Line 651: Line 743:
-- Rows for each God upgrade
-- Rows for each God upgrade
for i, upgrade in ipairs(upgradeList) do
for i, upgrade in ipairs(upgradeList) do
local dung = getGodDungeon(upgrade.unlockRequirements)
local upgradeName = Common.getPurchaseName(upgrade)
local dung = GameData.getEntityByID('dungeons', p.getGodUpgradeDungeon(upgrade))
local costSortValue = p._getPurchaseSortValue(upgrade)
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, '|-\r\n|class="table-img" data-sort-value="' .. upgradeName .. '"| ' ..p._getPurchaseExpansionIcon(upgrade).. Icons.Icon({upgradeName, type='upgrade', notext=true}))
table.insert(resultPart, '| ' .. Icons.Icon({upgrade.name, type='upgrade', noicon=true}))
table.insert(resultPart, '| ' .. Icons.Icon({upgradeName, type='upgrade', noicon=true}))
table.insert(resultPart, '| ' .. upgrade.description)
table.insert(resultPart, '| ' .. p._getPurchaseDescription(upgrade))
table.insert(resultPart, '| data-sort-value="' .. dung.name .. '"| ' .. Icons.Icon({dung.name, type='dungeon'}))
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))
table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. costSortValue .. '"| ' .. p.getCostString(upgrade.cost, false))
Line 664: Line 757:
end
end


function p.getCookingUtilityTable(frame)
function p.getAoDTable(frame)
local category = nil
-- All purchases in the Atlas of Discovery category except for skillcapes, which are handled
if frame ~= nil then category = frame.args ~= nil and frame.args[1] or frame end
-- by p.getShopSkillcapeTable()
local validCategories = {'Cooking Fire', 'Furnace', 'Pot'}
local purchList = p.getPurchases(
if category == nil or not Shared.contains({'Cooking Fire', 'Furnace', 'Pot'}, category) then
function(purch)
return 'ERROR: Invalid category specified. Must be one of the following: ' .. mw.text.listToText(validCategories, ', ', ' or ')
return purch.category == 'melvorAoD:AtlasOfDiscovery' and not p.isSkillcapePurchase(purch)
end
)
 
return p._getShopTable(purchList, { columns = { 'Purchase', 'Description', 'Cost', 'Requirements' } })
end
 
function p.getItATable(frame)
-- As above for AoD, but for Into the Abyss instead
local purchList = p.getPurchases(
function(purch)
return purch.category == 'melvorItA:IntoTheAbyss' and not p.isSkillcapePurchase(purch)
end
)
 
return p._getShopTable(purchList, { columns = { 'Purchase', 'Description', 'Cost', 'Requirements' } })
end
 
function p.getToolTable(toolName, searchString, modifiers, skillID)
local skillName = nil
if type(skillID) == 'string' then
skillName = Constants.getSkillName(skillID)
end
local toolArray = p.getPurchases(
function(purch)
return purch.category == 'melvorD:SkillUpgrades' and string.find(purch.id, searchString) ~= nil
end)
 
if Shared.tableIsEmpty(toolArray) then
return ''
end
if modifiers == nil then
modifiers = {}
end
end
 
local categoryShort = string.match(category, '[^%s]+$')
-- Determine match criteria for modifier matches later & initialize
local bonusSkillID = Constants.getSkillID('Cooking')
-- accumulators for modifier magnitudes
local bonusColMod, bonusColName = nil, nil
local modTotal, modMatchCriteria = {}, {}
if category == 'Cooking Fire' then
for i, modDef in ipairs(modifiers) do
bonusColMod = 'increasedSkillXP'
modTotal[i] = 0
bonusColName = 'Bonus ' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' XP'
modMatchCriteria[i] = Modifiers.getMatchCriteriaFromIDs({ modDef.matchRule })
else
bonusColMod = 'increasedChanceToDoubleItemsSkill'
bonusColName = 'Double Items Chance'
end
end
local modsPerfectChance = {'increasedChancePerfectCookFire', 'increasedChancePerfectCookFurnace',
 
'increasedChancePerfectCookPot', 'increasedChancePerfectCookGlobal'}
local headerRowSpan = (Shared.tableIsEmpty(toolArray) and 1) or 2
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 = {}
local resultPart = {}
-- Table header
table.insert(resultPart, '{| class="wikitable stickyHeader"')
table.insert(resultPart, '{| class="wikitable stickyHeader"')
table.insert(resultPart, '|- class="headerRow-0"')
table.insert(resultPart, '\n|- class="headerRow-0"')
table.insert(resultPart, '!colspan="4"| !!colspan="2"|' .. bonusColName .. '!!colspan="2"|Bonus Perfect Chance')
table.insert(resultPart, '\n!rowspan="' .. headerRowSpan .. '" colspan="2"| Name')
table.insert(resultPart, '|- class="headerRow-1"')
table.insert(resultPart, '\n!rowspan="' .. headerRowSpan .. '"| ' .. (skillName == nil and 'Requirements' or Icons.Icon({skillName, type='skill', notext=true}) .. ' Level'))
table.insert(resultPart, '!colspan="2"|Name!!Level!!Cost' .. string.rep('!!This ' .. categoryShort .. '!!Total', 2))
table.insert(resultPart, '\n!rowspan="' .. headerRowSpan .. '"| Cost')
for i, modDef in ipairs(modifiers) do
table.insert(resultPart, '\n!colspan="2"| ' .. modDef.header)
end
if headerRowSpan > 1 then
table.insert(resultPart, '\n|- class="headerRow-1"' .. string.rep('\n!This ' .. toolName .. '\n!Total', Shared.tableCount(modifiers)))
end


-- Row for each upgrade
for i, tool in ipairs(toolArray) do
for i, utility in ipairs(utilityList) do
local toolName = Common.getPurchaseName(tool)
-- First determine bonus XP/doubling chance and perfect chance
local toolCost = p.getCostString(tool.cost, false)
local bonusVal, perfectChance = 0, 0
local toolCostSort = p._getPurchaseSortValue(tool) or 0
if type(utility.contains) == 'table' then
table.insert(resultPart, '\n|-')
if type(utility.contains.modifiers) == 'table' then
table.insert(resultPart, '\n|class="table-img" data-sort-value="' .. toolName .. '"| ' .. Icons.Icon({toolName, type='upgrade', notext=true}))
for modName, modVal in pairs(utility.contains.modifiers) do
table.insert(resultPart, '\n| data-sort-value="' .. toolName.. '"|' .. Icons.getExpansionIcon(tool.id) .. toolName)
if modName == bonusColMod and type(modVal) == 'table' then
local level, levelStyle = nil, nil
-- Bonus XP/doubling
if skillID == nil then
for skID, skVal in pairs(modVal) do
level = 'None'
if skVal[1] == bonusSkillID then bonusVal = bonusVal + skVal[2] end
levelStyle = '|class="table-na"'
end
else
elseif Shared.contains(modsPerfectChance, modName) then
level = 1
-- Perfect chance
levelStyle = '|style="text-align:right"'
perfectChance = perfectChance + modVal
end
if tool.purchaseRequirements ~= nil and not Shared.tableIsEmpty(tool.purchaseRequirements) then
if skillID == nil then
-- Return all requirements
level = Common.getRequirementString(tool.purchaseRequirements, 'None')
if level ~= 'None' then
levelStyle = ''
end
else
-- Return level requirement for just the specified skill
for i, purchReq in ipairs(tool.purchaseRequirements) do
if (purchReq.type == 'SkillLevel' or purchReq.type == 'AbyssalLevel') and purchReq.skillID == skillID then
level = purchReq.level
break
end
end
end
end
end
end
end
end
totalBonusVal = totalBonusVal + bonusVal
table.insert(resultPart, '\n' .. levelStyle .. '| '..level)
totalPerfectChance = totalPerfectChance + perfectChance
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. toolCostSort .. '"| ' .. toolCost)


-- Mangle unlockRequirements so that it only includes skillLevels
local resultPrefix = ''
local unlockReqs = {}
local cellStart = '\n|style="text-align:right"'
if type(utility.unlockRequirements) == 'table' then
if tool.contains ~= nil and tool.contains.modifiers ~= nil then
unlockReqs['skillLevel'] = utility.unlockRequirements.skillLevel
local toolMods = tool.contains.modifiers
for j, modDef in ipairs(modifiers) do
local matchedMods = Modifiers.getMatchingModifiers(tool.contains.modifiers, modMatchCriteria[j])
local modVal = Modifiers.getModifierValue(matchedMods.matched) or 0
modTotal[j] = modTotal[j] + modVal
local cellStartVal = cellStart .. ((modVal == 0 and ' class="table-na"') or '')
local cellStartTot = cellStart .. ((modTotal[j] == 0 and ' class="table-na"') or '')
table.insert(resultPart, cellStartVal .. '| ' .. (modVal == 0 and '' or modDef.sign) .. modVal .. modDef.suffix)
table.insert(resultPart, cellStartTot .. '| ' .. (modTotal[j] == 0 and '' or modDef.sign) .. modTotal[j] .. modDef.suffix)
end
end
end
end
table.insert(resultPart, '\n|}')
return table.concat(resultPart)
end
function p.getAxeTable(frame)
local modifiers = {
{
header = 'Cut Time Decrease',
sign = '',
suffix = '%',
matchRule = {
["id"] = 'skillInterval',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Woodcutting' }
}
}, {
header = 'Double Items Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'skillItemDoublingChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Woodcutting' }
}
}, {
header = Icons.Icon({'Bird Nest', 'Drop Chance', type='item', nolink=true}),
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'randomProductChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Woodcutting', ["itemID"] = 'melvorD:Bird_Nest' }
}
}, {
header = Icons.Icon({'Ash', 'Drop Chance', type='item', nolink=true}),
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'additionalRandomSkillItemChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Woodcutting', ["itemID"] = 'melvorF:Ash' }
}
}
}
return p.getToolTable('Axe', '_Axe$', modifiers, 'melvorD:Woodcutting')
end
function p.getAxeCoatingTable(frame)
local modifiers = {
{
header = 'AXP Increase',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'abyssalSkillXP',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Woodcutting' }
}
}, {
header = 'Log Quantity Increase',
sign = '+',
suffix = '',
matchRule = {
["id"] = 'flatAdditionalPrimaryProductQuantity',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Woodcutting' }
}
}, {
header = Icons.Icon({'Shadow Raven Nest', 'Drop Chance', type='item', nolink=true}),
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'randomProductChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Woodcutting', ["itemID"] = 'melvorItA:Shadow_Raven_Nest' }
}
}, {
header = Icons.Icon({'Shadow Drake Nest', 'Drop Chance', type='item', nolink=true}),
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'randomProductChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Woodcutting', ["itemID"] = 'melvorItA:Shadow_Drake_Nest' }
}
}
}
return p.getToolTable('Coating', '_Axe_Coating$', modifiers, 'melvorD:Woodcutting')
end
function p.getPickaxeTable(frame)
local modifiers = {
{
header = 'Mining Time Decrease',
sign = '',
suffix = '%',
matchRule = {
["id"] = 'skillInterval',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Mining' }
}
}, {
header = '2x Ore Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'skillItemDoublingChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Mining' }
}
}, {
header = '+1 Ore Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'additionalPrimaryProductChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Mining', ["categoryID"] = 'melvorD:Ore' }
}
}, {
header = 'Superior Gem Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'qualitySuperiorGemChance',
["type"] = 'id'
}
}, {
header = 'Increased ' .. Icons.Icon({'Meteorite Ore', type='item', notext=true}),
sign = '+',
suffix = '',
matchRule = {
["id"] = 'flatBasePrimaryProductQuantity',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Mining', ["actionID"] = 'melvorTotH:Meteorite_Ore' }
}
}
}
return p.getToolTable('Pickaxe', '_Pickaxe$', modifiers, 'melvorD:Mining')
end
function p.getPickaxeCoatingTable(frame)
local modifiers = {
{
header = 'AXP Increase',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'abyssalSkillXP',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Mining' }
}
}, {
header = 'Rock Quantity Increase',
sign = '+',
suffix = '',
matchRule = {
["id"] = 'flatAdditionalPrimaryProductQuantity',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Mining' }
}
}, {
header = 'Abyssal Gem Vein Location Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'abyssalGemVeinChanceIncrease',
["type"] = 'id'
}
}, {
header = '+1 Abyssal Gem Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'additionalAbyssalGemChance',
["type"] = 'id'
}
}
}
return p.getToolTable('Coating', '_Pickaxe_Coating$', modifiers, 'melvorD:Mining')
end
function p.getRodTable(frame)
local modifiers = {
{
header = 'Catch Time Decrease',
sign = '',
suffix = '%',
matchRule = {
["id"] = 'skillInterval',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Fishing' }
}
}, {
header = '+1 Fish Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'additionalPrimaryProductChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Fishing' }
}
}, {
header = Icons.Icon({'Lost Chest', type='item', notext=true}) .. ' Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'additionalRandomSkillItemChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Fishing', ["itemID"] = 'melvorTotH:Lost_Chest' }
}
}, {
header = 'Cooked Fish Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'fishingCookedChance',
["type"] = 'id'
}
}
}
return p.getToolTable('Rod', '_Rod$', modifiers, 'melvorD:Fishing')
end
function p.getRodCoatingTable(frame)
local modifiers = {
{
header = 'AXP Increase',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'abyssalSkillXP',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Fishing' }
}
}, {
header = 'Fish Quantity Increase',
sign = '+',
suffix = '',
matchRule = {
["id"] = 'flatAdditionalPrimaryProductQuantity',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Fishing' }
}
}, {
header = 'Cooked Fish Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'fishingCookedChance',
["type"] = 'id'
}
}
}
return p.getToolTable('Coating', '_Rod_Coating$', modifiers, 'melvorD:Fishing')
end
function p.getHarvesterTable(frame)
local modifiers = {
{
header = 'Minimum Harvesting Intensity',
sign = '+',
suffix = '',
matchRule = {
["id"] = 'minimumHarvestingIntensity',
["type"] = 'id'
}
}, {
header = '2x Intensity Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'doubleHarvestingIntensityChance',
["type"] = 'id'
}
}, {
header = '2x Item Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'flatAdditionalPrimaryProductQuantity',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorItA:Harvesting' }
}
}
}
return p.getToolTable('Harvester', '_Harvester$', modifiers, 'melvorItA:Harvesting')
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 Shared.printError('Invalid category specified. Must be one of the following: ' .. mw.text.listToText(validCategories, ', ', ' or '))
end
local categoryShort = string.match(category, '[^%s]+$')
local modifiers = {
['Cooking Fire'] = {
{
header = 'Bonus ' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' XP',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'skillXP',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Cooking' }
}
}, {
header = Icons.Icon({'Normal Cooking Fire', type='upgrade', notext=true, nolink=true}) .. ' Perfect Cook Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'perfectCookChance',
["type"] = 'id',
["props"] = { ["categoryID"] = 'melvorD:Fire' }
}
}, {
header = 'Passive Cook Time Decrease',
sign = '',
suffix = '%',
matchRule = {
["id"] = 'passiveCookingInterval',
["type"] = 'id'
}
}, {
header = '2x Items Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'skillItemDoublingChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Cooking' }
}
}, {
header = 'Active Cook Time Decrease',
sign = '',
suffix = '%',
matchRule = {
["id"] = 'skillInterval',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Cooking' }
}
}
},
['Furnace'] = {
{
header = Icons.Icon({'Basic Furnace', type='upgrade', notext=true, nolink=true}) .. ' Perfect Cook Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'perfectCookChance',
["type"] = 'id',
["props"] = { ["categoryID"] = 'melvorD:Furnace' }
}
}, {
header = '2x Items Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'skillItemDoublingChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Cooking' }
}
}, {
header = 'Passive Cook Time Decrease',
sign = '',
suffix = '%',
matchRule = {
["id"] = 'passiveCookingInterval',
["type"] = 'id'
}
}, {
header = 'Active Cook Time Decrease',
sign = '',
suffix = '%',
matchRule = {
["id"] = 'skillInterval',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Cooking' }
}
}, {
header = '+1 Item Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'additionalPrimaryProductChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Cooking' }
}
}
},
['Pot'] = {
{
header = Icons.Icon({'Basic Pot', type='upgrade', notext=true, nolink=true}) .. ' Perfect Cook Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'perfectCookChance',
["type"] = 'id',
["props"] = { ["categoryID"] = 'melvorD:Pot' }
}
}, {
header = '2x Items Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'skillItemDoublingChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Cooking' }
}
}, {
header = 'Passive Cook Time Decrease',
sign = '',
suffix = '%',
matchRule = {
["id"] = 'passiveCookingInterval',
["type"] = 'id'
}
}, {
header = 'Active Cook Time Decrease',
sign = '',
suffix = '%',
matchRule = {
["id"] = 'skillInterval',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Cooking' }
}
}, {
header = '+1 Item Chance',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'additionalPrimaryProductChance',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Cooking' }
}
}, {
header = 'Increased Cooking ' .. Icons.Icon({'Mastery', nolink=true}) .. ' XP',
sign = '+',
suffix = '%',
matchRule = {
["id"] = 'masteryXP',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorD:Cooking' }
}
}
}
}
return p.getToolTable(categoryShort, categoryShort .. '$', modifiers[category], nil)
end
--Adding table for Ship upgrades for Cartography
function p.getShipTable(frame)
local modifiers = {
{
header = 'Cartography Interval',
sign = '',
suffix = '%',
matchRule = {
["id"] = 'skillInterval',
["type"] = 'id',
["props"] = { ["skillID"] = 'melvorAoD:Cartography' }
}
}, {
header = 'Increased Sight Range',
sign = '+',
suffix = '',
matchRule = {
["id"] = 'cartographySightRange',
["type"] = 'id'
}
}, {
header = 'Increased Survey Range',
sign = '+',
suffix = '',
matchRule = {
["id"] = 'cartographySurveyRange',
["type"] = 'id'
}
}
}


table.insert(resultPart, '|-')
return p.getToolTable('Ship', 'Ship', modifiers, 'melvorAoD:Cartography')
table.insert(resultPart, '|style="min-width:25px"|' .. Icons.Icon({utility.name, type='upgrade', size='50', notext=true}))
end
table.insert(resultPart, '|' .. utility.name)
 
table.insert(resultPart, '|style="text-align:right"|' .. p.getRequirementString(unlockReqs))
function p.getArchToolTable(frame)
table.insert(resultPart, '|style="text-align:right"|' .. p.getCostString(utility.cost, false))
local category = nil
table.insert(resultPart, '|style="text-align:right"|' .. '+' .. bonusVal .. '%')
if frame ~= nil then category = frame.args ~= nil and frame.args[1] or frame end
table.insert(resultPart, '|style="text-align:right"|' .. '+' .. totalBonusVal .. '%')
local validCategories = {'Sieve', 'Trowel', 'Brush', 'Shovel'}
table.insert(resultPart, '|style="text-align:right"|' .. '+' .. perfectChance .. '%')
if category == nil or not Shared.contains(validCategories, category) then
table.insert(resultPart, '|style="text-align:right"|' .. '+' .. totalPerfectChance .. '%')
return Shared.printError('Invalid category specified. Must be one of the following: ' .. mw.text.listToText(validCategories, ', ', ' or '))
end
end
table.insert(resultPart, '|}')


return table.concat(resultPart, '\r\n')
local modifiers = {
{
header = 'Increased ' .. category .. 'Tool Level',
sign = '+',
suffix = '',
matchRule = {
["id"] = string.lower(category) .. 'ToolLevel',
["type"] = 'id'
}
}
}
return p.getToolTable(category, category .. '$', modifiers, 'melvorAoD:Archaeology')
end
 
-- Below functions included for backwards compatibility
-- TODO: Remove dependency on these functions in all other modules
function p._getPurchaseName(purchase)
return Common.getPurchaseName(purchase)
end
function p._getPurchaseType(purchase)
return Common.getPurchaseType(purchase)
end
function p._getPurchaseIcon(iconArgs)
return Common.getPurchaseIcon(iconArgs)
end
function p.getRequirementString(reqs)
return Common.getRequirementString(reqs, 'None')
end
end


return p
return p

Latest revision as of 15:50, 17 August 2024

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

local p = {}

local Shared = require('Module:Shared')
local Constants = require('Module:Constants')
local GameData = require('Module:GameData')
local Common = require('Module:Common')
local Modifiers = require('Module:Modifiers')
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local Pets = require('Module:Pets')
local Num = require('Module:Number')

function p.getPurchase(purchaseName)
	local purchList = p.getPurchases(function(purch) return Common.getPurchaseName(purch) == purchaseName end)
	if purchList ~= nil and not Shared.tableIsEmpty(purchList) then
		return purchList[1]
	end
end

function p.getPurchaseByID(id)
	return GameData.getEntityByID('shopPurchases', id)
end

-- Accepts a function(purchase, name) and a category.
-- Prevents external modules from having to make GameData and Common calls.
function p.getCategoryPurchases(checkFunc, category)
	local shopCat = GameData.getEntityByName('shopCategories', category)
	
	if shopCat == nil then
		return Shared.printError('Invalid category ' .. shopCat)
	end
	
	-- We make a nested func to resolve the item name first, if required.
	local func = 
		function(purchase)
			if purchase.category ~= shopCat.id then 
				return false
			end
			
			local name = Common.getPurchaseName(purchase)
			return checkFunc(purchase, name)
		end
		
	return GameData.getEntities('shopPurchases', func)
end	

function p.getPurchases(checkFunc)
	return GameData.getEntities('shopPurchases', checkFunc)
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 Common.getRequirementString(purchase.purchaseRequirements, 'None')
	elseif stat == 'contents' then
		return p._getPurchaseContents(purchase, true)
	elseif stat == 'type' then
		return Common.getPurchaseType(purchase)
	elseif stat == 'buyLimit' then
		return p._getPurchaseBuyLimit(purchase, not displayInline)
	elseif stat == 'buyLimitHardcore' then
		return p._getPurchaseBuyLimitNumeric(purchase, 'melvorF:Hardcore')
	elseif stat == 'description' then
		return p._getPurchaseDescription(purchase)
	elseif stat =='expansionicon' then
		return p._getPurchaseExpansionIcon(purchase)
	else
		return purchase[stat]
	end
end

function p.getPurchaseStat(frame)
	local args = frame.args ~= nil and frame.args or frame
	local purchaseName = Shared.fixPagename(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(purch) return Common.getPurchaseName(purch) == purchaseName end)
	else
		purchaseList = {p.getPurchase(purchaseName)}
	end

	if Shared.tableIsEmpty(purchaseList) then
		return Shared.printError("Couldn't find purchase with name '" .. purchaseName .. "'")
	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._getPurchaseExpansionIcon(purch)
	if purch.id ~= nil then
		return Icons.getExpansionIcon(purch.id)
	elseif purch.contains ~= nil then
		local item = nil
		if purch.contains.items ~= nil and not Shared.tableIsEmpty(purch.contains.items) then
			return Icons.getExpansionIcon(purch.contains.items[1].id)
		elseif purch.contains.itemCharges ~= nil and not Shared.tableIsEmpty(purch.contains.itemCharges) then
			return Icons.getExpansionIcon(purch.contains.itemCharges.id)
		end
		
		if purch.contains.petID ~= nil then
			return Icons.getExpansionIcon(purch.contains.petID)
		end
	end
	return ''
end

function p._getPurchaseDescription(purch)
	if purch.customDescription ~= nil then
		local templateData = p._getPurchaseTemplateData(purch)
		return Shared.applyTemplateData(purch.customDescription, templateData)
	elseif purch.contains ~= nil then
		local item = nil
		if purch.contains.modifiers ~= nil then
			return Modifiers.getModifiersText(purch.contains.modifiers, false)
		elseif purch.contains.petID ~= nil then
			local pet = Pets.getPetByID(purch.contains.petID)
			return Pets._getPetEffect(pet)
		elseif purch.contains.items ~= nil and Shared.tableCount(purch.contains.items) == 1 then
			item = Items.getItemByID(purch.contains.items[1].id)
		elseif purch.contains.itemCharges ~= nil then
			item = Items.getItemByID(purch.contains.itemCharges.id)
		end
		if item ~= nil then
			if item.customDescription ~= nil then
				return item.customDescription
			elseif item.modifiers ~= nil then
				return Modifiers.getModifiersText(item.modifiers, false)
			end
		end
	end
	return ''
end

function p.getCostString(cost, inline)
	local displayInline = (inline ~= nil and inline or false)
	local costArray = {}
	if cost.currencies ~= nil then
		for i, costAmt in ipairs(cost.currencies) do
			local costStr = p.getCurrencyCostString(costAmt)
			if costStr ~= nil then
				table.insert(costArray, costStr)
			end
		end
	end
	if cost.items ~= nil and not Shared.tableIsEmpty(cost.items) then
		local itemArray = {}
		for i, itemCost in ipairs(cost.items) do
			local item = Items.getItemByID(itemCost.id)
			if item ~= nil then
				table.insert(itemArray, Icons.Icon({item.name, type="item", notext=(not displayInline and true or nil), qty=itemCost.quantity}))
			end
		end
		if not Shared.tableIsEmpty(itemArray) then
			table.insert(costArray, table.concat(itemArray, ', '))
		end
	end

	if not Shared.tableIsEmpty(costArray) then
		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
	return ''
end

-- Generates description template data. See: shop.js, getDescriptionTemplateData()
function p._getPurchaseTemplateData(purchase)
	-- qty is a static value of 1 for Bank slots
	local templateData = { qty = 1 }
	if purchase.contains ~= nil and purchase.contains.items ~= nil then
		for i, itemDef in ipairs(purchase.contains.items) do
			templateData['qty' .. i] = itemDef.quantity
		end
	end
	return templateData
end

function p.getCurrencyCostString(cost)
	if cost.type == 'BankSlot' then
		-- Unusual bit of code that basically evaluates wikitext '<math>C_b</math>*'
		return mw.getCurrentFrame():callParserFunction('#tag:math', {'C_b'}) .. '*'
	elseif cost.type == 'Linear' and (cost.initial > 0 or cost.scaling > 0) then
		return Icons._Currency(cost.currency, cost.initial) .. '<br/>+' .. Icons._Currency(cost.currency, cost.scaling) .. ' for each purchase'
	elseif cost.type == 'Glove' or cost.type == 'Fixed' and cost.cost > 0 then
		-- Type Glove exists in game so the Merchant's Permit cost reduction can be applied,
		-- it makes no difference here
		return Icons._Currency(cost.currency, cost.cost)
	end
end

function p._getPurchaseContents(purchase, asList)
	if asList == nil then asList = true end
	local containArray = {}
	local GPTotal = 0
	local currency = 'melvorD:GP'
	if purchase.contains ~= nil then
		if purchase.contains.items ~= nil and not Shared.tableIsEmpty(purchase.contains.items) 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 !! Price')
			end
			for i, itemLine in ipairs(purchase.contains.items) do
				local item = Items.getItemByID(itemLine.id)
				local itemQty = itemLine.quantity
				if item.sellsForCurrency ~= nil then
					currency = item.sellsForCurrency
				end
				if asList then
					table.insert(containArray, Icons.Icon({item.name, type='item', qty=itemQty}))
				else
					local GPVal = item.sellsFor * itemQty
					GPTotal = GPTotal + GPVal
					table.insert(containArray, '|-\r\n| class="table-img"| ' .. Icons.Icon({item.name, type='item', notext=true}))
					table.insert(containArray, '|data-sort-value="'..item.name..'"|'.. Icons.getExpansionIcon(item.id) .. Icons.Icon({item.name, type='item', noicon=true}) .. '\r\n| data-sort-value="' .. itemQty .. '" style="text-align:right" | ' .. Num.formatnum(itemQty))
					table.insert(containArray, '| data-sort-value="' .. GPVal .. '"| ' .. Icons._Currency(currency, GPVal))
				end
			end
		end
		if purchase.contains.itemCharges ~= nil and purchase.contains.itemCharges.quantity > 0 then
			local gloveItem = Items.getItemByID(purchase.contains.itemCharges.id)
			local chargeQty = purchase.contains.itemCharges.quantity
			if gloveItem ~= nil then
				if gloveItem.sellsForCurrency ~= nil then
					currency = gloveItem.sellsForCurrency
				end
				if asList then
					table.insert(containArray, ' +'..Num.formatnum(chargeQty)..' '..Icons.Icon({gloveItem.name, type='item'})..' Charges')
				else
					table.insert(containArray, '|-\r\n| class="table-img"| ' .. Icons.Icon({gloveItem.name, type='item', notext=true}))
					table.insert(containArray, '| ' .. Icons.Icon({gloveItem.name, type='item', noicon=true}) .. ' Charges\r\n| data-sort-value="' .. chargeQty .. '" style="text-align:right" | ' .. Num.formatnum(chargeQty))
					table.insert(containArray, '| data-sort-value="0"| ' .. Icons._Currency(currency, 0))
				end
			end
		end
	end
	if not asList and not Shared.tableIsEmpty(containArray) then
		table.insert(containArray, '|- class="sortbottom"\r\n! colspan="3"| Total\r\n| ' .. Icons._Currency(currency, GPTotal) .. '\r\n|}')
	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 = Shared.fixPagename(args[1])
	local asList = (args[2] ~= nil and string.upper(args[2]) == 'TRUE')
	local purchase = p.getPurchase(purchaseName)

	if purchase == nil then
		return Shared.printError("Couldn't find purchase with name '" .. purchaseName .. "'")
	else
		return p._getPurchaseContents(purchase, asList)
	end
end

function p._getPurchaseBuyLimitNumeric(purchase, gamemodeID)
	local buyLimit = (purchase.defaultBuyLimit > 0 and purchase.defaultBuyLimit)
	if not Shared.tableIsEmpty(purchase.buyLimitOverrides) then
		local gamemodeLimit = GameData.getEntityByProperty(purchase.buyLimitOverrides, 'gamemodeID', gamemodeID)
		if gamemodeLimit ~= nil and gamemodeLimit.maximum ~= nil then
			buyLimit = gamemodeLimit.maximum
		end
	end
	return buyLimit
end

function p._getPurchaseBuyLimit(purchase, asList)
	if asList == nil then asList = true end
	local defaultLimit = (purchase.defaultBuyLimit == 0 and 'Unlimited') or Num.formatnum(purchase.defaultBuyLimit)
	if purchase.buyLimitOverrides == nil or Shared.tableIsEmpty(purchase.buyLimitOverrides) then
		-- Same limit for all game modes
		return defaultLimit
	else
		-- The limit varies depending on game mode
		local limitTable = {}
		local gamemodeHasIcon = { 'melvorF:Hardcore', 'melvorF:Adventure' }
		for i, buyLimit in ipairs(purchase.buyLimitOverrides) do
			local gamemode = GameData.getEntityByID('gamemodes', buyLimit.gamemodeID)
			if gamemode ~= nil then
				local gamemodeName = Shared.splitString(gamemode.name, ' ')[1]
				local gamemodeText = nil
				if Shared.contains(gamemodeHasIcon, gamemode.id) then
					gamemodeText = Icons.Icon({gamemodeName, notext=(not asList or nil)})
				else
					gamemodeText = '[[Game Mode#' .. gamemodeName .. '|' .. gamemodeName .. ']]'
				end
				local limitText = (buyLimit.maximum == 0 and 'Unlimited') or Num.formatnum(buyLimit.maximum)
				table.insert(limitTable, limitText .. (asList and ' for ' or ' ') .. gamemodeText)
			end
		end
		table.insert(limitTable, defaultLimit .. (asList and ' for ' or ' ') .. 'All other game modes')
		return table.concat(limitTable, (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 Shared.printError("Couldn't find purchase with name '" .. purchaseName .. "'")
	else
		return p._getPurchaseBuyLimit(purchase, asList)
	end
end

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

	if purchase == nil then
		return Shared.printError("Couldn't find purchase with name '" .. tostring(purchaseName) .. "'")
	else
		args[1] = purchase
		return Common.getPurchaseIcon(args)
	end
end

function p._getPurchaseSortValue(purchase)
	if purchase.cost ~= nil and purchase.cost.currencies ~= nil then
		for _, costAmt in ipairs(purchase.cost.currencies) do
			-- Find cost for the current currency, if it exists
			if costAmt.type == 'BankSlot' then
				return -1
			elseif costAmt.type == 'Linear' then
				return costAmt.initial
			elseif costAmt.type == 'Glove' or costAmt.type == 'Fixed' and costAmt.cost > 0 then
				return costAmt.cost
			end
		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, stickyHeader = {}, 'Purchase', nil, {}, true

	-- 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
		-- Sticky header class
		if options.stickyHeader ~= nil then
			if type(options.stickyHeader) == 'boolean' then
				stickyHeader = options.stickyHeader
			elseif type(options.stickyHeader) == 'string' and string.lower(options.stickyHeader) == 'false' then
				stickyHeader = false
			end
		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 and ' stickyHeader' or '') .. '"')
	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

	if sortOrder == nil then
		Purchases = GameData.sortByOrderTable(Purchases, GameData.rawData.shopDisplayOrder, true)
	else
		table.sort(Purchases, sortOrder)
	end
	for i, purchase in ipairs(Purchases) do
		local purchName = Common.getPurchaseName(purchase)
		local purchExpIcon = p._getPurchaseExpansionIcon(purchase)
		local purchType = Common.getPurchaseType(purchase)
		local costString = p.getCostString(purchase.cost, false)

		table.insert(resultPart, '|-')
		for j, column in ipairs(usedColumns) do
			if column == 'Purchase' then
				table.insert(resultPart, '|class="table-img"|' .. Common.getPurchaseIcon({purchase, notext=true}))
				table.insert(resultPart, '| data-sort-value="'..purchName..'"|'..purchExpIcon .. Common.getPurchaseIcon({purchase, noicon=true}))
			elseif column == 'Type' then
				table.insert(resultPart, '| ' .. purchType)
			elseif column == 'Description' then
				table.insert(resultPart, '| ' .. p._getPurchaseDescription(purchase))
			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, '| ' .. Common.getRequirementString(purchase.purchaseRequirements, 'None'))
			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, '\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'
--	 stickyHeader:   Specifies if the table will have a sticky header or not
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.stickyHeader ~= nil then options.stickyHeader = frame.args.stickyHeader 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 = GameData.getEntityByName('shopCategories', cat)
	if shopCat == nil then
		return Shared.printError('Invalid category ' .. cat)
	else
		local catPurchases = p.getPurchases(function(purch) return purch.category == shopCat.id end)
		return p._getShopTable(catPurchases, options)
	end
end

function p.getItemCostArray(itemID)
	local purchaseArray = {}
	for i, purchase in ipairs(GameData.rawData.shopPurchases) do
		if purchase.cost ~= nil and purchase.cost.items ~= nil then
			for j, itemCost in ipairs(purchase.cost.items) do
				if itemCost.id == itemID then
					table.insert(purchaseArray, { ["purchase"] = purchase, ["qty"] = itemCost.quantity })
					break
				end
			end
		end
	end
	return purchaseArray
end

function p.getItemSourceArray(itemID)
	local purchaseArray = {}
	for i, purchase in ipairs(GameData.rawData.shopPurchases) do
		if purchase.contains ~= nil then
			if purchase.contains.items ~= nil then
				for j, itemContains in ipairs(purchase.contains.items) do
					if itemContains.id == itemID then
						table.insert(purchaseArray, { ["purchase"] = purchase, ["qty"] = itemContains.quantity })
						break
					end
				end
			end
			if purchase.contains.itemCharges ~= nil and purchase.contains.itemCharges.id == itemID then
				table.insert(purchaseArray, { ["purchase"] = purchase, ["qty"] = 1 })
			end
		end
	end
	return purchaseArray
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..' - '..p._getPurchaseExpansionIcon(purchase) .. Common.getPurchaseIcon({purchase, 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|'..Common.getRequirementString(purchase.purchaseRequirements, 'None')

	result = result..'\r\n|-\r\n!style="text-align:right;"|Contains'
	result = result..'\r\n|'..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 ipairs(purchaseArray) do
		table.insert(tableArray, p._getPurchaseTable(purchase.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 Shared.printError('No item named ' .. itemName .. ' exists in the data module')
	end

	return p._getItemShopTable(item)
end

function p.getShopMiscUpgradeTable()
	-- All purchases in the general category besides Auto Eat, which is conained within a separate table
	local purchList = p.getPurchases(function(purch) return purch.category == 'melvorD:General' and string.find(purch.id, '^melvorD:Auto_Eat') == nil end)

	return p._getShopTable(purchList, { columns = { 'Purchase', 'Description', 'Cost', 'Requirements' }, purchaseHeader = 'Upgrade' })
end

function p.getShopSkillUpgradeTable()
	-- All purchaes in the SkillUpgrades category except tools and any upgrades displayed as
	-- tools (e.g. ship upgrades)
	local purchList = p.getPurchases(
		function(purch)
			return purch.category == 'melvorD:SkillUpgrades'
				-- Exclude tools, handled by p.getToolTable()
				and string.find(purch.id, '_Axe$') == nil
				and string.find(purch.id, '_Axe_Coating$') == nil
				and string.find(purch.id, '_Pickaxe$') == nil
				and string.find(purch.id, '_Pickaxe_Coating$') == nil
				and string.find(purch.id, '_Rod$') == nil
				and string.find(purch.id, '_Rod_Coating$') == nil
				and string.find(purch.id, '_Harvester$') == nil
				and string.find(purch.id, 'Fire$') == nil
				and string.find(purch.id, 'Furnace$') == nil
				and string.find(purch.id, 'Pot$') == nil
				and string.find(purch.id, 'Sieve$') == nil
				and string.find(purch.id, 'Trowel$') == nil
				and string.find(purch.id, 'Brush$') == nil
				and string.find(purch.id, 'Shovel$') == nil
				and string.find(purch.id, 'ShipUpgrade') == nil
				-- Exclude God upgrades, handled by p.getGodUpgradeTable()
				and p.getGodUpgradeDungeon(purch) == nil
		end
	)

	return p._getShopTable(purchList, { columns = { 'Purchase', 'Description', 'Cost', 'Requirements' }, purchaseHeader = 'Upgrade' })
end

function p.getPurchaseDescription(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local purchase = p.getPurchase(itemName)
	if purchase == nil then
		return ''
	end
	
	return p._getPurchaseDescription(purchase)
end

function p.isSkillcapePurchase(purch, isSuperior, skillID)
	-- Returns true or false depending on whether the purchase is a skillcape or not.
	-- If isSuperior is true, then this checks for superior skillcapes, false checks
	-- for regular skillcapes, and nil checks for both.
	-- If skillID is specified, then the skillcape must also relate to that skill
	local checkCategories = (isSuperior == nil and {'melvorTotH:SuperiorSkillcapes', 'melvorD:Skillcapes'}) or (isSuperior and {'melvorTotH:SuperiorSkillcapes'}) or {'melvorD:Skillcapes'}
	-- Some skillcapes (such as Archaeology & Cartography) reside outside of the usual categories
	local overrideIDs = {
		['melvorTotH:SuperiorSkillcapes'] = {
			'melvorAoD:Superior_Archaeology_Skillcape',
			'melvorAoD:Superior_Cartography_Skillcape',
			'melvorAoD:Cape_of_Completion_AoD'
		},
		['melvorD:Skillcapes'] = {
			'melvorAoD:Archaeology_Skillcape',
			'melvorAoD:Cartography_Skillcape',
			'melvorItA:Cape_of_Completion_ItA'
		}
	}

	for i, cat in ipairs(checkCategories) do
		if purch.category == cat or Shared.contains(overrideIDs[cat], purch.id) then
			if skillID == nil then
				return true
			else
				-- Also validate purchase requirements for relevant SkillLevel requirement
				local hasReq = false
				if type(purch.purchaseRequirements) == 'table' then
					for j, req in ipairs(purch.purchaseRequirements) do
						if req.type == 'SkillLevel' then
							if req.skillID == skillID then
								hasReq = true
							else
								-- The presence of any other skill's requirement indicates
								-- this is not a skillcape for skill with ID skillID
								return false
							end
						end
					end
				end
				return hasReq
			end
		end
	end
	return false
end

function p._getShopSkillcapeTable(showSuperior)
	local capeList = p.getPurchases(function(purch) return p.isSkillcapePurchase(purch, showSuperior) end)
	local sortOrderFunc =
		function(a, b)
			local costA, costB = p._getPurchaseSortValue(a), p._getPurchaseSortValue(b)
			if costA == costB then
				return Common.getPurchaseName(a) < Common.getPurchaseName(b)
			else
				return costA < costB
			end
		end
	return p._getShopTable(capeList, {
			columns = { 'Purchase', 'Description', 'Cost' },
			purchaseHeader = 'Cape',
			sortOrder = sortOrderFunc,
			stickyHeader = false,
			headerProps = {["Purchase"] = 'colspan="2" style="width:200px;"', ["Cost"] = 'style=width:120px;'}
		})
end

function p.getShopSkillcapeTable(frame)
	local capeCategory = frame.args ~= nil and frame.args[1] or frame
	local showSuperior = string.lower(capeCategory) == 'superior'

	return p._getShopSkillcapeTable(showSuperior)
end

function p.getSkillcapeTable(frame)
	local skillName = frame.args ~= nil and frame.args[1] or frame
	local skillID = Constants.getSkillID(skillName)
	if skillID == nil then
		return Shared.printError('No such skill "' .. (skillName or 'nil') .. '"')
	end

	local capeList = p.getPurchases(function(purch) return p.isSkillcapePurchase(purch, nil, skillID) end)
	if Shared.tableIsEmpty(capeList) then
		return ''
	else
		capeList = GameData.sortByOrderTable(capeList, GameData.rawData.shopDisplayOrder, true)
		local resultPart = {}
		table.insert(resultPart, '{| class="wikitable"\n')
		table.insert(resultPart, '!Skillcape!!Name!!Requirements!!Effect')
		for i, cape in ipairs(capeList) do
			local capeItem = Items.getItemByID(cape.contains.items[1].id)
			if capeItem ~= nil then
				table.insert(resultPart, '\n|-\n| ' .. Icons.Icon({capeItem.name, type='item', notext=true}))
				table.insert(resultPart, '\n| data-sort-value="'..capeItem.name..'"|'..Icons.getExpansionIcon(capeItem.id) .. Icons.Icon({capeItem.name, type='item', noicon=true}))
				table.insert(resultPart, '\n| ' .. Common.getRequirementString(cape.purchaseRequirements, 'None'))
				table.insert(resultPart, '\n| ' .. p._getPurchaseDescription(cape))
			end
		end
		
		table.insert(resultPart, '\n|}')
		return table.concat(resultPart)
	end
end

function p.getGodUpgradeDungeon(purch)
	-- Identifies skill upgrades which have a dungeon completion requirement for an area
	--	whose name ends with 'God Dungeon'. Returns the ID of the dungeon which must be
	--	completed before the purchase may be bought if the purchase is a god upgrade
	if purch.category == 'melvorD:SkillUpgrades' and type(purch.purchaseRequirements) == 'table' then
		for i, req in ipairs(purch.purchaseRequirements) do
			if req.type == 'DungeonCompletion' and string.find(req.dungeonID, 'God_Dungeon$') ~= nil then
				return req.dungeonID
			end
		end
	end
end

function p.getGodUpgradeTable()
	local resultPart = {}
	local upgradeList = p.getPurchases(
		function(purch)
			return p.getGodUpgradeDungeon(purch) ~= nil
		end)
	if Shared.tableIsEmpty(upgradeList) 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 upgradeName = Common.getPurchaseName(upgrade)
		local dung = GameData.getEntityByID('dungeons', p.getGodUpgradeDungeon(upgrade))
		local costSortValue = p._getPurchaseSortValue(upgrade)
		table.insert(resultPart, '|-\r\n|class="table-img" data-sort-value="' .. upgradeName .. '"| ' ..p._getPurchaseExpansionIcon(upgrade).. Icons.Icon({upgradeName, type='upgrade', notext=true}))
		table.insert(resultPart, '| ' .. Icons.Icon({upgradeName, type='upgrade', noicon=true}))
		table.insert(resultPart, '| ' .. p._getPurchaseDescription(upgrade))
		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.getAoDTable(frame)
	-- All purchases in the Atlas of Discovery category except for skillcapes, which are handled
	-- by p.getShopSkillcapeTable()
	local purchList = p.getPurchases(
		function(purch)
			return purch.category == 'melvorAoD:AtlasOfDiscovery' and not p.isSkillcapePurchase(purch)
		end
	)

	return p._getShopTable(purchList, { columns = { 'Purchase', 'Description', 'Cost', 'Requirements' }	})
end

function p.getItATable(frame)
	-- As above for AoD, but for Into the Abyss instead
	local purchList = p.getPurchases(
		function(purch)
			return purch.category == 'melvorItA:IntoTheAbyss' and not p.isSkillcapePurchase(purch)
		end
	)

	return p._getShopTable(purchList, { columns = { 'Purchase', 'Description', 'Cost', 'Requirements' }	})
end

function p.getToolTable(toolName, searchString, modifiers, skillID)
	local skillName = nil
	if type(skillID) == 'string' then
		skillName = Constants.getSkillName(skillID)
	end
	local toolArray = p.getPurchases(
		function(purch)
			return purch.category == 'melvorD:SkillUpgrades' and string.find(purch.id, searchString) ~= nil
		end)

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

	-- Determine match criteria for modifier matches later & initialize
	-- accumulators for modifier magnitudes
	local modTotal, modMatchCriteria = {}, {}
	for i, modDef in ipairs(modifiers) do
		modTotal[i] = 0
		modMatchCriteria[i] = Modifiers.getMatchCriteriaFromIDs({ modDef.matchRule })
	end

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

	for i, tool in ipairs(toolArray) do
		local toolName = Common.getPurchaseName(tool)
		local toolCost = p.getCostString(tool.cost, false)
		local toolCostSort = p._getPurchaseSortValue(tool) or 0
		table.insert(resultPart, '\n|-')
		table.insert(resultPart, '\n|class="table-img" data-sort-value="' .. toolName .. '"| ' .. Icons.Icon({toolName, type='upgrade', notext=true}))
		table.insert(resultPart, '\n| data-sort-value="' .. toolName.. '"|' .. Icons.getExpansionIcon(tool.id) .. toolName)
		local level, levelStyle = nil, nil
		if skillID == nil then
			level = 'None'
			levelStyle = '|class="table-na"'
		else
			level = 1
			levelStyle = '|style="text-align:right"'
		end
		if tool.purchaseRequirements ~= nil and not Shared.tableIsEmpty(tool.purchaseRequirements) then
			if skillID == nil then
				-- Return all requirements
				level = Common.getRequirementString(tool.purchaseRequirements, 'None')
				if level ~= 'None' then
					levelStyle = ''
				end
			else
				-- Return level requirement for just the specified skill
				for i, purchReq in ipairs(tool.purchaseRequirements) do
					if (purchReq.type == 'SkillLevel' or purchReq.type == 'AbyssalLevel') and purchReq.skillID == skillID then
						level = purchReq.level
						break
					end
				end
			end
		end
		table.insert(resultPart, '\n' .. levelStyle .. '| '..level)
		table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. toolCostSort .. '"| ' .. toolCost)

		local resultPrefix = ''
		local cellStart = '\n|style="text-align:right"'
		if tool.contains ~= nil and tool.contains.modifiers ~= nil then
			local toolMods = tool.contains.modifiers
			for j, modDef in ipairs(modifiers) do
				local matchedMods = Modifiers.getMatchingModifiers(tool.contains.modifiers, modMatchCriteria[j])
				local modVal = Modifiers.getModifierValue(matchedMods.matched) or 0
				modTotal[j] = modTotal[j] + modVal
				local cellStartVal = cellStart .. ((modVal == 0 and ' class="table-na"') or '')
				local cellStartTot = cellStart .. ((modTotal[j] == 0 and ' class="table-na"') or '')
				table.insert(resultPart, cellStartVal .. '| ' .. (modVal == 0 and '' or modDef.sign) .. modVal .. modDef.suffix)
				table.insert(resultPart, cellStartTot .. '| ' .. (modTotal[j] == 0 and '' or modDef.sign) .. modTotal[j] .. modDef.suffix)
			end
		end
	end

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

function p.getAxeTable(frame)
	local modifiers = {
		{ 
			header = 'Cut Time Decrease',
			sign = '',
			suffix = '%',
			matchRule = {
				["id"] = 'skillInterval',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Woodcutting' }
			}
		}, {
			header = 'Double Items Chance',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'skillItemDoublingChance',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Woodcutting' }
			}
		}, {
			header = Icons.Icon({'Bird Nest', 'Drop Chance', type='item', nolink=true}),
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'randomProductChance',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Woodcutting', ["itemID"] = 'melvorD:Bird_Nest' }
			}
		}, {
			header = Icons.Icon({'Ash', 'Drop Chance', type='item', nolink=true}),
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'additionalRandomSkillItemChance',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Woodcutting', ["itemID"] = 'melvorF:Ash' }
			}
		}
	}
	
	return p.getToolTable('Axe', '_Axe$', modifiers, 'melvorD:Woodcutting')
end

function p.getAxeCoatingTable(frame)
	local modifiers = {
		{ 
			header = 'AXP Increase',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'abyssalSkillXP',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Woodcutting' }
			}
		}, {
			header = 'Log Quantity Increase',
			sign = '+',
			suffix = '',
			matchRule = {
				["id"] = 'flatAdditionalPrimaryProductQuantity',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Woodcutting' }
			}
		}, {
			header = Icons.Icon({'Shadow Raven Nest', 'Drop Chance', type='item', nolink=true}),
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'randomProductChance',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Woodcutting', ["itemID"] = 'melvorItA:Shadow_Raven_Nest' }
			}
		}, {
			header = Icons.Icon({'Shadow Drake Nest', 'Drop Chance', type='item', nolink=true}),
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'randomProductChance',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Woodcutting', ["itemID"] = 'melvorItA:Shadow_Drake_Nest' }
			}
		}
	}
	
	return p.getToolTable('Coating', '_Axe_Coating$', modifiers, 'melvorD:Woodcutting')
end

function p.getPickaxeTable(frame)
	local modifiers = {
		{ 
			header = 'Mining Time Decrease',
			sign = '',
			suffix = '%',
			matchRule = {
				["id"] = 'skillInterval',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Mining' }
			}
		}, {
			header = '2x Ore Chance',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'skillItemDoublingChance',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Mining' }
			}
		}, {
			header = '+1 Ore Chance',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'additionalPrimaryProductChance',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Mining', ["categoryID"] = 'melvorD:Ore' }
			}
		}, {
			header = 'Superior Gem Chance',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'qualitySuperiorGemChance',
				["type"] = 'id'
			}
		}, {
			header = 'Increased ' .. Icons.Icon({'Meteorite Ore', type='item', notext=true}),
			sign = '+',
			suffix = '',
			matchRule = {
				["id"] = 'flatBasePrimaryProductQuantity',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Mining', ["actionID"] = 'melvorTotH:Meteorite_Ore' }
			}
		}
	}

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

function p.getPickaxeCoatingTable(frame)
	local modifiers = {
		{ 
			header = 'AXP Increase',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'abyssalSkillXP',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Mining' }
			}
		}, {
			header = 'Rock Quantity Increase',
			sign = '+',
			suffix = '',
			matchRule = {
				["id"] = 'flatAdditionalPrimaryProductQuantity',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Mining' }
			}
		}, {
			header = 'Abyssal Gem Vein Location Chance',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'abyssalGemVeinChanceIncrease',
				["type"] = 'id'
			}
		}, {
			header = '+1 Abyssal Gem Chance',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'additionalAbyssalGemChance',
				["type"] = 'id'
			}
		}
	}
	
	return p.getToolTable('Coating', '_Pickaxe_Coating$', modifiers, 'melvorD:Mining')
end

function p.getRodTable(frame)
	local modifiers = {
		{ 
			header = 'Catch Time Decrease',
			sign = '',
			suffix = '%',
			matchRule = {
				["id"] = 'skillInterval',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Fishing' }
			}
		}, {
			header = '+1 Fish Chance',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'additionalPrimaryProductChance',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Fishing' }
			}
		}, {
			header = Icons.Icon({'Lost Chest', type='item', notext=true}) .. ' Chance',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'additionalRandomSkillItemChance',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Fishing', ["itemID"] = 'melvorTotH:Lost_Chest' }
			}
		}, {
			header = 'Cooked Fish Chance',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'fishingCookedChance',
				["type"] = 'id'
			}
		}
	}

	return p.getToolTable('Rod', '_Rod$', modifiers, 'melvorD:Fishing')
end

function p.getRodCoatingTable(frame)
	local modifiers = {
		{ 
			header = 'AXP Increase',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'abyssalSkillXP',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Fishing' }
			}
		}, {
			header = 'Fish Quantity Increase',
			sign = '+',
			suffix = '',
			matchRule = {
				["id"] = 'flatAdditionalPrimaryProductQuantity',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorD:Fishing' }
			}
		}, {
			header = 'Cooked Fish Chance',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'fishingCookedChance',
				["type"] = 'id'
			}
		}
	}
	
	return p.getToolTable('Coating', '_Rod_Coating$', modifiers, 'melvorD:Fishing')
end

function p.getHarvesterTable(frame)
	local modifiers = {
		{ 
			header = 'Minimum Harvesting Intensity',
			sign = '+',
			suffix = '',
			matchRule = {
				["id"] = 'minimumHarvestingIntensity',
				["type"] = 'id'
			}
		}, {
			header = '2x Intensity Chance',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'doubleHarvestingIntensityChance',
				["type"] = 'id'
			}
		}, {
			header = '2x Item Chance',
			sign = '+',
			suffix = '%',
			matchRule = {
				["id"] = 'flatAdditionalPrimaryProductQuantity',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorItA:Harvesting' }
			}
		}
	}
	
	return p.getToolTable('Harvester', '_Harvester$', modifiers, 'melvorItA:Harvesting')
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 Shared.printError('Invalid category specified. Must be one of the following: ' .. mw.text.listToText(validCategories, ', ', ' or '))
	end

	local categoryShort = string.match(category, '[^%s]+$')
	local modifiers = {
		['Cooking Fire'] = {
			{ 
				header = 'Bonus ' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' XP',
				sign = '+',
				suffix = '%',
				matchRule = {
					["id"] = 'skillXP',
					["type"] = 'id',
					["props"] = { ["skillID"] = 'melvorD:Cooking' }
				}
			}, {
				header = Icons.Icon({'Normal Cooking Fire', type='upgrade', notext=true, nolink=true}) .. ' Perfect Cook Chance',
				sign = '+',
				suffix = '%',
				matchRule = {
					["id"] = 'perfectCookChance',
					["type"] = 'id',
					["props"] = { ["categoryID"] = 'melvorD:Fire' }
				}
			}, {
				header = 'Passive Cook Time Decrease',
				sign = '',
				suffix = '%',
				matchRule = {
					["id"] = 'passiveCookingInterval',
					["type"] = 'id'
				}
			}, {
				header = '2x Items Chance',
				sign = '+',
				suffix = '%',
				matchRule = {
					["id"] = 'skillItemDoublingChance',
					["type"] = 'id',
					["props"] = { ["skillID"] = 'melvorD:Cooking' }
				}
			}, {
				header = 'Active Cook Time Decrease',
				sign = '',
				suffix = '%',
				matchRule = {
					["id"] = 'skillInterval',
					["type"] = 'id',
					["props"] = { ["skillID"] = 'melvorD:Cooking' }
				}
			}
		},
		['Furnace'] = {
			{
				header = Icons.Icon({'Basic Furnace', type='upgrade', notext=true, nolink=true}) .. ' Perfect Cook Chance',
				sign = '+',
				suffix = '%',
				matchRule = {
					["id"] = 'perfectCookChance',
					["type"] = 'id',
					["props"] = { ["categoryID"] = 'melvorD:Furnace' }
				}
			}, {
				header = '2x Items Chance',
				sign = '+',
				suffix = '%',
				matchRule = {
					["id"] = 'skillItemDoublingChance',
					["type"] = 'id',
					["props"] = { ["skillID"] = 'melvorD:Cooking' }
				}
			}, {
				header = 'Passive Cook Time Decrease',
				sign = '',
				suffix = '%',
				matchRule = {
					["id"] = 'passiveCookingInterval',
					["type"] = 'id'
				}
			}, {
				header = 'Active Cook Time Decrease',
				sign = '',
				suffix = '%',
				matchRule = {
					["id"] = 'skillInterval',
					["type"] = 'id',
					["props"] = { ["skillID"] = 'melvorD:Cooking' }
				}
			}, {
				header = '+1 Item Chance',
				sign = '+',
				suffix = '%',
				matchRule = {
					["id"] = 'additionalPrimaryProductChance',
					["type"] = 'id',
					["props"] = { ["skillID"] = 'melvorD:Cooking' }
				}
			}
		},
		['Pot'] = {
			{
				header = Icons.Icon({'Basic Pot', type='upgrade', notext=true, nolink=true}) .. ' Perfect Cook Chance',
				sign = '+',
				suffix = '%',
				matchRule = {
					["id"] = 'perfectCookChance',
					["type"] = 'id',
					["props"] = { ["categoryID"] = 'melvorD:Pot' }
				}
			}, {
				header = '2x Items Chance',
				sign = '+',
				suffix = '%',
				matchRule = {
					["id"] = 'skillItemDoublingChance',
					["type"] = 'id',
					["props"] = { ["skillID"] = 'melvorD:Cooking' }
				}
			}, {
				header = 'Passive Cook Time Decrease',
				sign = '',
				suffix = '%',
				matchRule = {
					["id"] = 'passiveCookingInterval',
					["type"] = 'id'
				}
			}, {
				header = 'Active Cook Time Decrease',
				sign = '',
				suffix = '%',
				matchRule = {
					["id"] = 'skillInterval',
					["type"] = 'id',
					["props"] = { ["skillID"] = 'melvorD:Cooking' }
				}
			}, {
				header = '+1 Item Chance',
				sign = '+',
				suffix = '%',
				matchRule = {
					["id"] = 'additionalPrimaryProductChance',
					["type"] = 'id',
					["props"] = { ["skillID"] = 'melvorD:Cooking' }
				}
			}, {
				header = 'Increased Cooking ' .. Icons.Icon({'Mastery', nolink=true}) .. ' XP',
				sign = '+',
				suffix = '%',
				matchRule = {
					["id"] = 'masteryXP',
					["type"] = 'id',
					["props"] = { ["skillID"] = 'melvorD:Cooking' }
				}
			}
		}
	}

	return p.getToolTable(categoryShort, categoryShort .. '$', modifiers[category], nil)
end

--Adding table for Ship upgrades for Cartography
function p.getShipTable(frame)
	local modifiers = {
		{
			header = 'Cartography Interval',
			sign = '',
			suffix = '%',
			matchRule = {
				["id"] = 'skillInterval',
				["type"] = 'id',
				["props"] = { ["skillID"] = 'melvorAoD:Cartography' }
			}
		}, {
			header = 'Increased Sight Range',
			sign = '+',
			suffix = '',
			matchRule = {
				["id"] = 'cartographySightRange',
				["type"] = 'id'
			}
		}, {
			header = 'Increased Survey Range',
			sign = '+',
			suffix = '',
			matchRule = {
				["id"] = 'cartographySurveyRange',
				["type"] = 'id'
			}
		}
	}

	return p.getToolTable('Ship', 'Ship', modifiers, 'melvorAoD:Cartography')
end

function p.getArchToolTable(frame)
	local category = nil
	if frame ~= nil then category = frame.args ~= nil and frame.args[1] or frame end
	local validCategories = {'Sieve', 'Trowel', 'Brush', 'Shovel'}
	if category == nil or not Shared.contains(validCategories, category) then
		return Shared.printError('Invalid category specified. Must be one of the following: ' .. mw.text.listToText(validCategories, ', ', ' or '))
	end

	local modifiers = {
		{
			header = 'Increased ' .. category .. 'Tool Level',
			sign = '+',
			suffix = '',
			matchRule = {
				["id"] = string.lower(category) .. 'ToolLevel',
				["type"] = 'id'
			}
		}
	}
	return p.getToolTable(category, category .. '$', modifiers, 'melvorAoD:Archaeology')
end

-- Below functions included for backwards compatibility
-- TODO: Remove dependency on these functions in all other modules
function p._getPurchaseName(purchase)
	return Common.getPurchaseName(purchase)
end
function p._getPurchaseType(purchase)
	return Common.getPurchaseType(purchase)
end
function p._getPurchaseIcon(iconArgs)
	return Common.getPurchaseIcon(iconArgs)
end
function p.getRequirementString(reqs)
	return Common.getRequirementString(reqs, 'None')
end

return p