Module:Shop: Difference between revisions

From Melvor Idle
(getCookingUtilityTable: Add mastery XP modifier for cooking pot table)
(Move certain functions to Module:Common and remove purchase override data in favour of standard implementation in Module:Icons)
Line 4: Line 4:
local Constants = require('Module:Constants')
local Constants = require('Module:Constants')
local GameData = require('Module:GameData')
local GameData = require('Module:GameData')
local Common = require('Module:Common')
local Items = require('Module:Items')
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')
local Pets = require('Module:Pets')
local Pets = require('Module:Pets')
-- Overrides for various items, mostly relating to icon overrides
local purchOverrides = {
["Extra Bank Slot"] = { icon = {'Bank Slot', 'upgrade'}, link = 'Bank Slot' },
-- Golbin Raid items
["Reduce Wave Skip Cost"] = { icon = {'Melvor Logo', nil}, link = nil },
["Food Bonus"] = { icon = {'Melvor Logo', nil}, link = nil },
["Ammo Gatherer"] = { icon = {'Melvor Logo', nil}, link = nil },
["Rune Pouch"] = { icon = {'Melvor Logo', nil}, link = nil },
["Increase Starting Prayer Points"] = { icon = {'Melvor Logo', nil}, link = nil },
["Unlock Combat Passive Slot"] = { icon = {'Melvor Logo', nil}, link = nil },
["Prayer"] = { icon = {'Prayer', 'skill'}, link = nil },
["Increase Prayer Level"] = { icon = {'Prayer', 'skill'}, link = nil },
["Increase Prayer Points gained per Wave Completion"] = { icon = {'Prayer', 'skill'}, link = nil },
["Faster Golbin Spawns"] = { icon = {'Timer', nil}, link = nil },
["Golbin Crate"] = { icon = {'Golbin Crate', 'upgrade'}, link = nil }
}


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


Line 37: Line 21:


function p.getPurchases(checkFunc)
function p.getPurchases(checkFunc)
    return GameData.getEntities('shopPurchases', checkFunc)
return GameData.getEntities('shopPurchases', checkFunc)
end
end


Line 45: Line 29:
return p.getCostString(purchase.cost, displayInline)
return p.getCostString(purchase.cost, displayInline)
elseif stat == 'requirements' then
elseif stat == 'requirements' then
return p.getRequirementString(purchase.purchaseRequirements)
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)
Line 71: Line 55:
local purchaseList = {}
local purchaseList = {}
if statName == 'cost' then
if statName == 'cost' then
purchaseList = p.getPurchases(function(purch) return p._getPurchaseName(purch) == purchaseName end)
purchaseList = p.getPurchases(function(purch) return Common.getPurchaseName(purch) == purchaseName end)
else
else
purchaseList = {p.getPurchase(purchaseName)}
purchaseList = {p.getPurchase(purchaseName)}
Line 85: Line 69:
return table.concat(resultPart, ' or ')
return table.concat(resultPart, ' or ')
end
end
end
function p._getPurchaseName(purch)
    if purch.customName ~= nil then
        return purch.customName
    elseif purch.contains ~= nil then
    local item = nil
        if purch.contains.items ~= nil and not Shared.tableIsEmpty(purch.contains.items) then
            item = Items.getItemByID(purch.contains.items[1].id)
        elseif purch.contains.itemCharges ~= nil and not Shared.tableIsEmpty(purch.contains.itemCharges) then
        item = Items.getItemByID(purch.contains.itemCharges.id)
        end
        if item ~= nil then
            return item.name
        end
        if purch.contains.petID ~= nil then
            local pet = Pets.getPetByID(purch.contains.petID)
            if pet ~= nil then
                return pet.name
            end
        end
    end
    return ''
end
end


function p._getPurchaseExpansionIcon(purch)
function p._getPurchaseExpansionIcon(purch)
    if purch.id ~= nil then
if purch.id ~= nil then
        return Icons.getExpansionIcon(purch.id)
return Icons.getExpansionIcon(purch.id)
    elseif purch.contains ~= nil then
elseif purch.contains ~= nil then
    local item = nil
local item = nil
        if purch.contains.items ~= nil and not Shared.tableIsEmpty(purch.contains.items) then
if purch.contains.items ~= nil and not Shared.tableIsEmpty(purch.contains.items) then
            return Icons.getExpansionIcon(purch.contains.items[1].id)
return Icons.getExpansionIcon(purch.contains.items[1].id)
        elseif purch.contains.itemCharges ~= nil and not Shared.tableIsEmpty(purch.contains.itemCharges) then
elseif purch.contains.itemCharges ~= nil and not Shared.tableIsEmpty(purch.contains.itemCharges) then
        return Icons.getExpansionIcon(purch.contains.itemCharges.id)
return Icons.getExpansionIcon(purch.contains.itemCharges.id)
        end
end
       
        if purch.contains.petID ~= nil then
if purch.contains.petID ~= nil then
return Icons.getExpansionIcon(purch.contains.petID)
return Icons.getExpansionIcon(purch.contains.petID)
        end
end
    end
end
    return ''
return ''
end
end


Line 137: Line 98:
return Constants.getModifiersText(purch.contains.modifiers, false)
return Constants.getModifiersText(purch.contains.modifiers, false)
elseif purch.contains.petID ~= nil then
elseif purch.contains.petID ~= nil then
            local pet = Pets.getPetByID(purch.contains.petID)
local pet = Pets.getPetByID(purch.contains.petID)
            return Pets._getPetEffect(pet)
return Pets._getPetEffect(pet)
elseif purch.contains.items ~= nil and Shared.tableCount(purch.contains.items) == 1 then
elseif purch.contains.items ~= nil and Shared.tableCount(purch.contains.items) == 1 then
item = Items.getItemByID(purch.contains.items[1].id)
item = Items.getItemByID(purch.contains.items[1].id)
Line 158: Line 119:
local displayInline = (inline ~= nil and inline or false)
local displayInline = (inline ~= nil and inline or false)
local costArray = {}
local costArray = {}
    local currencies = {'gp', 'slayerCoins', 'raidCoins'}
local currencies = {'gp', 'slayerCoins', 'raidCoins'}
    for i, currency in ipairs(currencies) do
for i, currency in ipairs(currencies) do
        if cost[currency] ~= nil then
if cost[currency] ~= nil then
            local costStr = p.getCurrencyCostString(cost[currency], currency)
local costStr = p.getCurrencyCostString(cost[currency], currency)
            if costStr ~= nil then
if costStr ~= nil then
                table.insert(costArray, costStr)
table.insert(costArray, costStr)
            end
end
        end
end
    end
end
if cost.items ~= nil and not Shared.tableIsEmpty(cost.items) then
if cost.items ~= nil and not Shared.tableIsEmpty(cost.items) then
        local itemArray = {}
local itemArray = {}
for i, itemCost in ipairs(cost.items) do
for i, itemCost in ipairs(cost.items) do
local item = Items.getItemByID(itemCost.id)
local item = Items.getItemByID(itemCost.id)
            if item ~= nil then
if item ~= nil then
    table.insert(itemArray, Icons.Icon({item.name, type="item", notext=(not displayInline and true or nil), qty=itemCost.quantity}))
table.insert(itemArray, Icons.Icon({item.name, type="item", notext=(not displayInline and true or nil), qty=itemCost.quantity}))
            end
end
end
if not Shared.tableIsEmpty(itemArray) then
table.insert(costArray, table.concat(itemArray, ', '))
end
end
        if not Shared.tableIsEmpty(itemArray) then
            table.insert(costArray, table.concat(itemArray, ', '))
        end
end
end


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


Line 203: Line 164:


function p.getCurrencyCostString(cost, currency)
function p.getCurrencyCostString(cost, currency)
    local decoratorList = {
local decoratorList = {
        ["gp"] = Icons.GP,
["gp"] = Icons.GP,
        ["slayerCoins"] = Icons.SC,
["slayerCoins"] = Icons.SC,
        ["raidCoins"] = Icons.RC
["raidCoins"] = Icons.RC
    }
}
    local decorator = nil
local decorator = nil
    if currency ~= nil then
if currency ~= nil then
        decorator = decoratorList[currency]
decorator = decoratorList[currency]
    end
end
    if decorator == nil then
if decorator == nil then
        decorator = function(cost) return cost end
decorator = function(cost) return cost end
    end
 
    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 decorator(cost.initial) .. '<br/>+' .. decorator(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 Merchan's Permit cost reduction can be applied,
        -- it makes no difference here
        return decorator(cost.cost)
    end
end
 
function p.getRequirementString(reqs)
if reqs == nil or Shared.tableIsEmpty(reqs) then
return 'None'
end
end


local reqArray = {}
if cost.type == 'BankSlot' then
    for i, req in ipairs(reqs) do
-- Unusual bit of code that basically evaluates wikitext '<math>C_b</math>*'
        if req.type == 'SkillLevel' then
return mw.getCurrentFrame():callParserFunction('#tag:math', {'C_b'}) .. '*'
            local skillName = Constants.getSkillName(req.skillID)
elseif cost.type == 'Linear' and (cost.initial > 0 or cost.scaling > 0) then
            if skillName ~= nil then
return decorator(cost.initial) .. '<br/>+' .. decorator(cost.scaling) .. ' for each purchase'
                table.insert(reqArray, Icons._SkillReq(skillName, req.level))
elseif cost.type == 'Glove' or cost.type == 'Fixed' and cost.cost > 0 then
            end
-- Type Glove exists in game so the Merchan's Permit cost reduction can be applied,
        elseif req.type == 'DungeonCompletion' then
-- it makes no difference here
            local dung = GameData.getEntityByID('dungeons', req.dungeonID)
return decorator(cost.cost)
            if dung ~= nil then
                local dungStr = 'Complete ' .. Icons.Icon({dung.name, type='dungeon'})
                if req.count > 1 then
                    dungStr = dungStr .. ' ' .. Shared.formatnum(req.count) .. ' times'
                end
                table.insert(reqArray, dungStr)
            end
        elseif req.type == 'SlayerTask' then
            table.insert(reqArray, 'Complete ' .. Shared.formatnum(req.count) .. ' ' .. req.tier .. ' Slayer Tasks')
        elseif req.type == 'TownshipTask' then
            table.insert(reqArray, 'Complete ' .. Shared.formatnum(req.count) .. ' Township Tasks')
        elseif req.type == 'TownshipBuilding' then
            local tsData = GameData.getSkillData('melvorD:Township')
            if tsData ~= nil and tsData.buildings ~= nil then
                local building = GameData.getEntityByID(tsData.buildings, req.buildingID)
                if building ~= nil then
                    table.insert(reqArray, 'Have ' .. Shared.formatnum(req.count) .. ' ' .. building.name .. ' actively built in Township')
                end
            end
        elseif req.type == 'ShopPurchase' then
        local shopPurch = p.getPurchaseByID(req.purchaseID)
        if shopPurch ~= nil then
        table.insert(reqArray, p._getPurchaseIcon({shopPurch}) .. ' Purchased')
        end
        elseif req.type == 'Completion' then
            local ns = GameData.getEntityByName('namespaces', req.namespace)
            if ns ~= nil then
                table.insert(reqArray, req.percent .. '% ' .. ns.displayName .. ' Completion')
            end
        elseif req.type == 'AllSkillLevels' then
            local reqText = 'Level ' .. req.level .. ' in all skills'
            if req.exceptions ~= nil and not Shared.tableIsEmpty(req.exceptions) then
                local exceptSkills = {}
                for i, skillID in ipairs(req.exceptions) do
                    local skillName = Constants.getSkillName(skillID)
                    if skillName ~= nil then
                        table.insert(exceptSkills, Icons.Icon({skillName, type='skill'}))
                    end
                end
                reqText = reqText .. ' except for ' .. table.concat(exceptSkills, ', ')
            end
            table.insert(reqArray, reqText)
        elseif req.type == 'ArchaeologyItemsDonated' then
        table.insert(reqArray, 'Donate ' .. Shared.formatnum(req.count) .. ' Artefacts to the Museum in ' .. Icons.Icon({'Archaeology', type='skill'}))
        else
            table.insert(reqArray, Shared.printError('Unknown requirement: ' .. (req.type or 'nil')))
        end
    end
 
    if Shared.tableIsEmpty(reqArray) then
        return 'None'
    else
    return table.concat(reqArray, '<br/>')
    end
end
 
function p._getPurchaseType(purchase)
if purchase.contains == nil then
return 'Unknown'
elseif purchase.contains.petID ~= nil then
return 'Pet'
elseif purchase.contains.itemCharges ~= nil then
return 'Item'
elseif purchase.contains.modifiers ~= nil or purchase.contains.items == nil or Shared.tableCount(purchase.contains.items) == 0 then
return 'Upgrade'
elseif purchase.contains.items ~= nil and Shared.tableCount(purchase.contains.items) > 1 then
return 'Item Bundle'
else
return 'Item'
end
end
end
end
Line 327: Line 202:
for i, itemLine in ipairs(purchase.contains.items) do
for i, itemLine in ipairs(purchase.contains.items) do
local item = Items.getItemByID(itemLine.id)
local item = Items.getItemByID(itemLine.id)
            local itemQty = itemLine.quantity
local itemQty = itemLine.quantity
if asList then
if asList then
table.insert(containArray, Icons.Icon({item.name, type='item', qty=itemQty}))
table.insert(containArray, Icons.Icon({item.name, type='item', qty=itemQty}))
Line 340: Line 215:
end
end
if purchase.contains.itemCharges ~= nil and purchase.contains.itemCharges.quantity > 0 then
if purchase.contains.itemCharges ~= nil and purchase.contains.itemCharges.quantity > 0 then
        local gloveItem = Items.getItemByID(purchase.contains.itemCharges.id)
local gloveItem = Items.getItemByID(purchase.contains.itemCharges.id)
        local chargeQty = purchase.contains.itemCharges.quantity
local chargeQty = purchase.contains.itemCharges.quantity
        if gloveItem ~= nil then
if gloveItem ~= nil then
            if asList then
if asList then
                table.insert(containArray, '+'..Shared.formatnum(chargeQty)..' '..Icons.Icon({gloveItem.name, type='item'})..' Charges')
table.insert(containArray, '+'..Shared.formatnum(chargeQty)..' '..Icons.Icon({gloveItem.name, type='item'})..' Charges')
            else
else
                table.insert(containArray, '|-\r\n| class="table-img"| ' .. Icons.Icon({gloveItem.name, type='item', notext=true, size='25'}))
table.insert(containArray, '|-\r\n| class="table-img"| ' .. Icons.Icon({gloveItem.name, type='item', notext=true, size='25'}))
                table.insert(containArray, '| ' .. Icons.Icon({gloveItem.name, type='item', noicon=true}) .. ' Charges\r\n| data-sort-value="' .. chargeQty .. '" style="text-align:right" | ' .. Shared.formatnum(chargeQty))
table.insert(containArray, '| ' .. Icons.Icon({gloveItem.name, type='item', noicon=true}) .. ' Charges\r\n| data-sort-value="' .. chargeQty .. '" style="text-align:right" | ' .. Shared.formatnum(chargeQty))
                table.insert(containArray, '| data-sort-value="0"| ' .. Icons.GP(0))
table.insert(containArray, '| data-sort-value="0"| ' .. Icons.GP(0))
            end
end
        end
end
end
end
end
end
Line 387: Line 262:
function p._getPurchaseBuyLimit(purchase, asList)
function p._getPurchaseBuyLimit(purchase, asList)
if asList == nil then asList = true end
if asList == nil then asList = true end
    local defaultLimit = (purchase.defaultBuyLimit == 0 and 'Unlimited') or Shared.formatnum(purchase.defaultBuyLimit)
local defaultLimit = (purchase.defaultBuyLimit == 0 and 'Unlimited') or Shared.formatnum(purchase.defaultBuyLimit)
    if purchase.buyLimitOverrides == nil or Shared.tableIsEmpty(purchase.buyLimitOverrides) then
if purchase.buyLimitOverrides == nil or Shared.tableIsEmpty(purchase.buyLimitOverrides) then
        -- Same limit for all game modes
-- Same limit for all game modes
        return defaultLimit
return defaultLimit
    else
else
        -- The limit varies depending on game mode
-- The limit varies depending on game mode
        local limitTable = {}
local limitTable = {}
        local gamemodeHasIcon = { 'melvorF:Hardcore', 'melvorF:Adventure' }
local gamemodeHasIcon = { 'melvorF:Hardcore', 'melvorF:Adventure' }
        for i, buyLimit in ipairs(purchase.buyLimitOverrides) do
for i, buyLimit in ipairs(purchase.buyLimitOverrides) do
            local gamemode = GameData.getEntityByID('gamemodes', buyLimit.gamemodeID)
local gamemode = GameData.getEntityByID('gamemodes', buyLimit.gamemodeID)
            if gamemode ~= nil then
if gamemode ~= nil then
local gamemodeName = Shared.splitString(gamemode.name, ' ')[1]
local gamemodeName = Shared.splitString(gamemode.name, ' ')[1]
                local gamemodeText = nil
local gamemodeText = nil
                if Shared.contains(gamemodeHasIcon, gamemode.id) then
if Shared.contains(gamemodeHasIcon, gamemode.id) then
                    gamemodeText = Icons.Icon({gamemodeName, notext=(not asList or nil)})
gamemodeText = Icons.Icon({gamemodeName, notext=(not asList or nil)})
                else
else
                    gamemodeText = '[[Game Mode#' .. gamemodeName .. '|' .. gamemodeName .. ']]'
gamemodeText = '[[Game Mode#' .. gamemodeName .. '|' .. gamemodeName .. ']]'
                end
end
                local limitText = (buyLimit.maximum == 0 and 'Unlimited') or Shared.formatnum(buyLimit.maximum)
local limitText = (buyLimit.maximum == 0 and 'Unlimited') or Shared.formatnum(buyLimit.maximum)
                table.insert(limitTable, limitText .. (asList and ' for ' or ' ') .. gamemodeText)
table.insert(limitTable, limitText .. (asList and ' for ' or ' ') .. gamemodeText)
            end
end
        end
end
        table.insert(limitTable, defaultLimit .. (asList and ' for ' or ' ') .. 'All other game modes')
table.insert(limitTable, defaultLimit .. (asList and ' for ' or ' ') .. 'All other game modes')
        return table.concat(limitTable, (asList and ' or ' or '<br/>'))
return table.concat(limitTable, (asList and ' or ' or '<br/>'))
    end
end
end
end


Line 425: Line 300:
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 purchaseName = p._getPurchaseName(purchase)
local override = purchOverrides[purchaseName]
local purchType = p._getPurchaseType(purchase)
-- Amend iconArgs before passing to Icons.Icon()
iconArgs[1] = ((override ~= nil and override.icon[1]) or purchaseName)
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


Line 456: Line 311:
else
else
args[1] = purchase
args[1] = purchase
return p._getPurchaseIcon(args)
return Common.getPurchaseIcon(args)
end
end
end
end
Line 464: Line 319:
for j, curr in ipairs(costCurrencies) do
for j, curr in ipairs(costCurrencies) do
local costAmt = purchase.cost[curr]
local costAmt = purchase.cost[curr]
        if costAmt.type == 'BankSlot' then
if costAmt.type == 'BankSlot' then
            return -1
return -1
        elseif costAmt.type == 'Linear' then
elseif costAmt.type == 'Linear' then
            return costAmt.initial
return costAmt.initial
        elseif costAmt.type == 'Glove' or costAmt.type == 'Fixed' and costAmt.cost > 0 then
elseif costAmt.type == 'Glove' or costAmt.type == 'Fixed' and costAmt.cost > 0 then
            return costAmt.cost
return costAmt.cost
        end
end
    end
end
end
end


Line 537: Line 392:
end
end
for i, purchase in ipairs(Purchases) do
for i, purchase in ipairs(Purchases) do
        local purchName = p._getPurchaseName(purchase)
local purchName = Common.getPurchaseName(purchase)
        local purchExpIcon = p._getPurchaseExpansionIcon(purchase)
local purchExpIcon = p._getPurchaseExpansionIcon(purchase)
local purchOverride = nil
local purchType = Common.getPurchaseType(purchase)
if purchOverrides ~= nil then
purchOverride = purchOverrides[purchName]
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
end
        local purchSortName = purchName
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, '|class="table-img"|' .. p._getPurchaseIcon({purchase, notext=true, size='50'}))
table.insert(resultPart, '|class="table-img"|' .. Common.getPurchaseIcon({purchase, notext=true, size='50'}))
--table.insert(resultPart, '|class="table-img"|' .. 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, '| data-sort-value="'..purchSortName..'"|'..purchExpIcon .. purchName)
elseif column == 'Type' then
elseif column == 'Type' then
table.insert(resultPart, '| ' .. purchType)
table.insert(resultPart, '| ' .. purchType)
Line 575: Line 412:
table.insert(resultPart, cellProp .. '| ' .. costString)
table.insert(resultPart, cellProp .. '| ' .. costString)
elseif column == 'Requirements' then
elseif column == 'Requirements' then
table.insert(resultPart, '| ' .. p.getRequirementString(purchase.purchaseRequirements))
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 620: Line 457:
end
end
end
end
    local shopCat = GameData.getEntityByName('shopCategories', cat)
local shopCat = GameData.getEntityByName('shopCategories', cat)
if shopCat == nil then
if shopCat == nil then
return Shared.printError('Invalid category ' .. cat)
return Shared.printError('Invalid category ' .. cat)
else
else
        local catPurchases = p.getPurchases(function(purch) return purch.category == shopCat.id end)
local catPurchases = p.getPurchases(function(purch) return purch.category == shopCat.id end)
return p._getShopTable(catPurchases, options)
return p._getShopTable(catPurchases, options)
end
end
Line 630: Line 467:


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


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


Line 668: Line 505:
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..' - '..p._getPurchaseExpansionIcon(purchase)..Icons.Icon({p._getPurchaseName(purchase), type='item'})
result = result..' - '..p._getPurchaseExpansionIcon(purchase)..Icons.Icon({Common.getPurchaseName(purchase), type='item'})
end
end


Line 675: Line 512:


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.purchaseRequirements)
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'
Line 718: Line 555:
local costA, costB = p._getPurchaseSortValue(a), p._getPurchaseSortValue(b)
local costA, costB = p._getPurchaseSortValue(a), p._getPurchaseSortValue(b)
if costA == costB then
if costA == costB then
return p._getPurchaseName(a) < p._getPurchaseName(b)
return Common.getPurchaseName(a) < Common.getPurchaseName(b)
else
else
return costA < costB
return costA < costB
Line 744: Line 581:
local capeList = {}
local capeList = {}
if skillName == 'HP' or skillName == 'Hitpoints' then
if skillName == 'HP' or skillName == 'Hitpoints' then
capeList = p.getPurchases(function(purch) return Shared.contains({'melvorD:Skillcapes', 'melvorTotH:SuperiorSkillcapes'}, purch.category) and (p._getPurchaseName(purch) == 'HP Skillcape' or p._getPurchaseName(purch) == 'Superior Hitpoints Skillcape') end)
capeList = p.getPurchases(function(purch) return Shared.contains({'melvorD:Skillcapes', 'melvorTotH:SuperiorSkillcapes'}, purch.category) and (Common.getPurchaseName(purch) == 'HP Skillcape' or Common.getPurchaseName(purch) == 'Superior Hitpoints Skillcape') end)
else
else
capeList = p.getPurchases(function(purch) return Shared.contains({'melvorD:Skillcapes', 'melvorTotH:SuperiorSkillcapes'}, purch.category) and string.find(p._getPurchaseName(purch), skillName) end)
capeList = p.getPurchases(function(purch) return Shared.contains({'melvorD:Skillcapes', 'melvorTotH:SuperiorSkillcapes'}, purch.category) and string.find(Common.getPurchaseName(purch), skillName) end)
end
end
if Shared.tableIsEmpty(capeList) then
if Shared.tableIsEmpty(capeList) then
Line 759: Line 596:
table.insert(resultPart, '\r\n|-\r\n| ' .. Icons.Icon({capeItem.name, type='item', size='60', notext=true}))
table.insert(resultPart, '\r\n|-\r\n| ' .. Icons.Icon({capeItem.name, type='item', size='60', notext=true}))
table.insert(resultPart, '\r\n| data-sort-value="'..capeItem.name..'"|'..Icons.getExpansionIcon(capeItem.id) .. Icons.Icon({capeItem.name, type='item', noicon=true}))
table.insert(resultPart, '\r\n| data-sort-value="'..capeItem.name..'"|'..Icons.getExpansionIcon(capeItem.id) .. Icons.Icon({capeItem.name, type='item', noicon=true}))
table.insert(resultPart, '\r\n| ' .. p.getRequirementString(cape.purchaseRequirements))
table.insert(resultPart, '\r\n| ' .. Common.getRequirementString(cape.purchaseRequirements, 'None'))
table.insert(resultPart, '\r\n| ' .. p._getPurchaseDescription(cape))
table.insert(resultPart, '\r\n| ' .. p._getPurchaseDescription(cape))
end
end
Line 780: Line 617:
local mods = {["increasedAutoEatEfficiency"] = 0, ["increasedAutoEatHPLimit"] = 0, ["increasedAutoEatThreshold"] = 0}
local mods = {["increasedAutoEatEfficiency"] = 0, ["increasedAutoEatHPLimit"] = 0, ["increasedAutoEatThreshold"] = 0}
for i, purchase in ipairs(purchasesAE) do
for i, purchase in ipairs(purchasesAE) do
        local purchaseName = p._getPurchaseName(purchase)
local purchaseName = Common.getPurchaseName(purchase)
-- Modifiers must be accumulated as we go
-- Modifiers must be accumulated as we go
for modName, modValue in pairs(mods) do
for modName, modValue in pairs(mods) do
Line 806: Line 643:
local getGodDungeon =
local getGodDungeon =
function(reqs)
function(reqs)
            for i, req in ipairs(reqs) do
for i, req in ipairs(reqs) do
                if req.type == 'DungeonCompletion' and string.find(req.dungeonID, 'God_Dungeon$') ~= nil then
if req.type == 'DungeonCompletion' and string.find(req.dungeonID, 'God_Dungeon$') ~= nil then
                    return GameData.getEntityByID('dungeons', req.dungeonID)
return GameData.getEntityByID('dungeons', req.dungeonID)
                end
end
            end
end
        end
end


local upgradeList = p.getPurchases(
local upgradeList = p.getPurchases(
Line 821: Line 658:
end)
end)
if Shared.tableIsEmpty(upgradeList) then
if Shared.tableIsEmpty(upgradeList) then
        return ''
return ''
    end
end


-- Table header
-- Table header
Line 831: Line 668:
-- 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 upgradeName = p._getPurchaseName(upgrade)
local upgradeName = Common.getPurchaseName(upgrade)
local dung = getGodDungeon(upgrade.purchaseRequirements)
local dung = getGodDungeon(upgrade.purchaseRequirements)
local costSortValue = p._getPurchaseSortValue(upgrade)
local costSortValue = p._getPurchaseSortValue(upgrade)
Line 886: Line 723:
local modsUnused = {}
local modsUnused = {}
for i, tool in ipairs(toolArray) do
for i, tool in ipairs(toolArray) do
local toolName = p._getPurchaseName(tool)
local toolName = Common.getPurchaseName(tool)
local toolCost = p.getCostString(tool.cost, false)
local toolCost = p.getCostString(tool.cost, false)
local toolCostSort = p._getPurchaseSortValue(tool) or 0
local toolCostSort = p._getPurchaseSortValue(tool) or 0
Line 903: Line 740:
if skillID == nil then
if skillID == nil then
-- Return all requirements
-- Return all requirements
level = p.getRequirementString(tool.purchaseRequirements)
level = Common.getRequirementString(tool.purchaseRequirements, 'None')
if level ~= 'None' then
if level ~= 'None' then
levelStyle = ''
levelStyle = ''
Line 1,027: Line 864:


return p.getToolTable(categoryShort, categoryShort .. '$', modifiers[category], nil)
return p.getToolTable(categoryShort, categoryShort .. '$', modifiers[category], nil)
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

Revision as of 23:18, 9 September 2023

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 Items = require('Module:Items')
local Icons = require('Module:Icons')
local Pets = require('Module:Pets')

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

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 Constants.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 Constants.getModifiersText(item.modifiers, false)
			end
		end
	end
	return ''
end

function p.getCostString(cost, inline)
	local displayInline = (inline ~= nil and inline or false)
	local costArray = {}
	local currencies = {'gp', 'slayerCoins', 'raidCoins'}
	for i, currency in ipairs(currencies) do
		if cost[currency] ~= nil then
			local costStr = p.getCurrencyCostString(cost[currency], currency)
			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
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, currency)
	local decoratorList = {
		["gp"] = Icons.GP,
		["slayerCoins"] = Icons.SC,
		["raidCoins"] = Icons.RC
	}
	local decorator = nil
	if currency ~= nil then
		decorator = decoratorList[currency]
	end
	if decorator == nil then
		decorator = function(cost) return cost end
	end

	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 decorator(cost.initial) .. '<br/>+' .. decorator(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 Merchan's Permit cost reduction can be applied,
		-- it makes no difference here
		return decorator(cost.cost)
	end
end

function p._getPurchaseContents(purchase, asList)
	if asList == nil then asList = true end
	local containArray = {}
	local GPTotal = 0
	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 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, size='25'}))
					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" | ' .. Shared.formatnum(itemQty))
					table.insert(containArray, '| data-sort-value="' .. GPVal .. '"| ' .. Icons.GP(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 asList then
					table.insert(containArray, '+'..Shared.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, size='25'}))
					table.insert(containArray, '| ' .. Icons.Icon({gloveItem.name, type='item', noicon=true}) .. ' Charges\r\n| data-sort-value="' .. chargeQty .. '" style="text-align:right" | ' .. Shared.formatnum(chargeQty))
					table.insert(containArray, '| data-sort-value="0"| ' .. Icons.GP(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.GP(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 = 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 Shared.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 Shared.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)
	local costCurrencies = {'gp', 'slayerCoins', 'raidCoins'}
	for j, curr in ipairs(costCurrencies) do
		local costAmt = purchase.cost[curr]
		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

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, size='50'}))
				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, '\r\n')
end

-- getShopTable parameter definition:
--   columns:        Comma separated values indicating which columns are to be included & the order
--                   in which they are displayed.
--                   Values can be any of: Purchase, Type, Description, Cost, Requirements
--   columnProps:    Comma separated values indicating formatting to be applied to each column. Each
--                   value must be in the format column:property, e.g. Purchase:colspan="2"
--   sortOrder:      A function determining the order in which table items appear
--   purchaseHeader: Specifies header text for the Purchase column if not 'Purchase'
--	 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)..Icons.Icon({Common.getPurchaseName(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|style="text-align:right;"|'..p._getPurchaseContents(purchase, true)

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

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

	for i, purchase in 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()
	local purchList = p.getPurchases(function(purch) return (purch.category == 'melvorD:General' and string.find(purch.id, '^melvorD:Auto_Eat') == nil) or Shared.contains({'melvorTotH:SignOfTheStars', 'melvorTotH:SummonersAltar'}, purch.id) end)

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

function p._getShopSkillcapeTable(showSuperior)
	local categoryID = (showSuperior and 'melvorTotH:SuperiorSkillcapes') or 'melvorD:Skillcapes'
	local capeList = p.getPurchases(function(purch) return purch.category == categoryID 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
	-- Exception for "HP Skillcape" + "Superior Hitpoints Skillcape"
	local capeList = {}
	if skillName == 'HP' or skillName == 'Hitpoints' then
		capeList = p.getPurchases(function(purch) return Shared.contains({'melvorD:Skillcapes', 'melvorTotH:SuperiorSkillcapes'}, purch.category) and (Common.getPurchaseName(purch) == 'HP Skillcape' or Common.getPurchaseName(purch) == 'Superior Hitpoints Skillcape') end)
	else
		capeList = p.getPurchases(function(purch) return Shared.contains({'melvorD:Skillcapes', 'melvorTotH:SuperiorSkillcapes'}, purch.category) and string.find(Common.getPurchaseName(purch), skillName) end)
	end
	if Shared.tableIsEmpty(capeList) then
		return ''
	else
		local resultPart = {}
		table.insert(resultPart, '{| class="wikitable"\r\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, '\r\n|-\r\n| ' .. Icons.Icon({capeItem.name, type='item', size='60', notext=true}))
				table.insert(resultPart, '\r\n| data-sort-value="'..capeItem.name..'"|'..Icons.getExpansionIcon(capeItem.id) .. Icons.Icon({capeItem.name, type='item', noicon=true}))
				table.insert(resultPart, '\r\n| ' .. Common.getRequirementString(cape.purchaseRequirements, 'None'))
				table.insert(resultPart, '\r\n| ' .. p._getPurchaseDescription(cape))
			end
		end
		
		table.insert(resultPart, '\r\n|}')
		return table.concat(resultPart)
	end
end

function p.getAutoEatTable()
	local resultPart = {}
	local purchasesAE = p.getPurchases(function(purch) return purch.category == 'melvorD:General' and string.find(purch.id, '^melvorD:Auto_Eat') ~= nil end)

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

		table.insert(resultPart, '|-\r\n|class="table-img" data-sort-value="' .. purchaseName .. '"| ' .. Icons.Icon({purchaseName, type='upgrade', size=50, notext=true}))
		table.insert(resultPart, '| ' .. Icons.Icon({purchaseName, type='upgrade', noicon=true}))
		table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. mods.increasedAutoEatThreshold .. '" | ' .. Shared.formatnum(Shared.round(mods.increasedAutoEatThreshold, 0, 0)) .. '%')
		table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. mods.increasedAutoEatEfficiency .. '" | ' .. Shared.formatnum(Shared.round(mods.increasedAutoEatEfficiency, 0, 0)) .. '%')
		table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. mods.increasedAutoEatHPLimit .. '" | ' .. Shared.formatnum(Shared.round(mods.increasedAutoEatHPLimit, 0, 0)) .. '%')
		table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. p._getPurchaseSortValue(purchase) .. '" | ' .. p.getCostString(purchase.cost, false))
	end
	table.insert(resultPart, '|}')

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

function p.getGodUpgradeTable()
	local resultPart = {}
	-- Obtain list of God upgrades: look for skill upgrades which have a dungeon completion
	--   requirement for an area whose name ends with 'God Dungeon'
	local getGodDungeon =
		function(reqs)
			for i, req in ipairs(reqs) do
				if req.type == 'DungeonCompletion' and string.find(req.dungeonID, 'God_Dungeon$') ~= nil then
					return GameData.getEntityByID('dungeons', req.dungeonID)
				end
			end
		end

	local upgradeList = p.getPurchases(
		function(purch)
			if purch.category == 'melvorD:SkillUpgrades' and purch.purchaseRequirements ~= nil then
				return getGodDungeon(purch.purchaseRequirements) ~= nil
			end
			return false
		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 = getGodDungeon(upgrade.purchaseRequirements)
		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', size=50, 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.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

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

	local headerRowSpan = (Shared.tableIsEmpty(toolArray) and 1) or 2
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable 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
		modTotal[modDef.name] = 0
		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

	-- Keep track of modifiers which are present on tools but not exposed within the table, so
	-- that an error may be printed if any are omitted
	local modsUnused = {}
	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', size='50', 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' 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
			for modName, modVal in pairs(tool.contains.modifiers) do
				if modTotal[modName] == nil and not Shared.contains(modsUnused, modName) then
					-- Mod with name modName is provided by the purchase, but not exposed within
					-- the output table
					table.insert(modsUnused, modName)
				end
			end

			for j, modDef in ipairs(modifiers) do
				local modName = modDef.name
				local modVal = tool.contains.modifiers[modName]
				if modVal ~= nil then
					if type(modVal) == 'table' and type(modVal[1]) == 'table' and modVal[1].skillID ~= nil and (modDef.skillID == nil or modDef.skillID == modVal[1].skillID) then
						modVal = modVal[1].value
					end
					modTotal[modName] = modTotal[modName] + modVal
				else
					modVal = 0
				end
				local cellStartVal = cellStart .. ((modVal == 0 and ' class="table-na"') or '')
				local cellStartTot = cellStart .. ((modTotal[modName] == 0 and ' class="table-na"') or '')
				table.insert(resultPart, cellStartVal .. '| ' .. (modVal == 0 and '' or modDef.sign) .. modVal .. modDef.suffix)
				table.insert(resultPart, cellStartTot .. '| ' .. (modTotal[modName] == 0 and '' or modDef.sign) .. modTotal[modName] .. modDef.suffix)
			end
		end
	end

	local resultPrefix = ''
	if not Shared.tableIsEmpty(modsUnused) then
		resultPrefix = Shared.printError('The following modifiers are not included within the table: ' .. table.concat(modsUnused, ', ')) .. '\n'
	end
	table.insert(resultPart, '\n|}')
	return resultPrefix .. table.concat(resultPart)
end

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

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

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

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

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

function p.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'] = {
			{ name = 'increasedSkillXP', skillID = 'melvorD:Cooking', header = 'Bonus ' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' XP', sign = '+', suffix = '%' },
			{ name = 'increasedChancePerfectCookFire', header = Icons.Icon({'Normal Cooking Fire', type='upgrade', notext=true, nolink=true}) .. ' Perfect Cook Chance', sign ='+', suffix = '%' },
			{ name = 'decreasedPassiveCookInterval', header = 'Passive Cook Time Decrease', sign = '-', suffix = '%' },
			{ name = 'increasedChanceToDoubleItemsSkill', skillID = 'melvorD:Cooking', header = '2x Items Chance', sign = '+', suffix = '%' },
			{ name = 'decreasedSkillIntervalPercent', skillID = 'melvorD:Cooking', header = 'Active Cook Time Decrease', sign = '-', suffix = '%' }
		},
		['Furnace'] = {
			{ name = 'increasedChancePerfectCookFurnace', header = Icons.Icon({'Basic Furnace', type='upgrade', notext=true, nolink=true}) .. ' Perfect Cook Chance', sign = '+', suffix = '%' },
			{ name = 'increasedChanceToDoubleItemsSkill', skillID = 'melvorD:Cooking', header = '2x Items Chance', sign = '+', suffix = '%' },
			{ name = 'decreasedPassiveCookInterval', header = 'Passive Cook Time Decrease', sign = '-', suffix = '%' },
			{ name = 'decreasedSkillIntervalPercent', skillID = 'melvorD:Cooking', header = 'Active Cook Time Decrease', sign = '-', suffix = '%' },
			{ name = 'increasedChanceAdditionalSkillResource', skillID = 'melvorD:Cooking', header = '+1 Item Chance', sign = '+', suffix = '%' }
		},
		['Pot'] = {
			{ name = 'increasedChancePerfectCookPot', header = Icons.Icon({'Basic Pot', type='upgrade', notext=true, nolink=true}) .. ' Perfect Cook Chance', sign = '+', suffix = '%' },
			{ name = 'increasedChanceToDoubleItemsSkill', skillID = 'melvorD:Cooking', header = '2x Items Chance', sign = '+', suffix = '%' },
			{ name = 'decreasedPassiveCookInterval', header = 'Passive Cook Time Decrease', sign = '-', suffix = '%' },
			{ name = 'decreasedSkillIntervalPercent', skillID = 'melvorD:Cooking', header = 'Active Cook Time Decrease', sign = '-', suffix = '%' },
			{ name = 'increasedChanceAdditionalSkillResource', skillID = 'melvorD:Cooking', header = '+1 Item Chance', sign = '+', suffix = '%' },
			{ name = 'increasedMasteryXP', skillID = 'melvorD:Cooking', header = 'Increased Cooking ' .. Icons.Icon({'Mastery', nolink=true}) .. ' XP', sign = '+', suffix = '%'}
		}
	}

	return p.getToolTable(categoryShort, categoryShort .. '$', modifiers[category], nil)
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