Module:Shop: Difference between revisions

From Melvor Idle
(getPurchaseBuyLimit: Initial implementation; _getShopTable: Support "Buy Limit" column)
m (Remove % suffix from sight/survey range)
 
(88 intermediate revisions by 9 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 whne 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 }
}


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


function p._getPurchaseStat(purchase, stat, inline)
function p._getPurchaseStat(purchase, stat, inline)
  local displayInline = (inline ~= nil and inline or false)
local displayInline = (inline ~= nil and inline or false)
  if stat == 'cost' then
if stat == 'cost' then
    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)
  else
elseif stat == 'buyLimit' then
    return purchase[stat]
return p._getPurchaseBuyLimit(purchase, not displayInline)
  end
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
end


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)
  -- Hack for some purchases existing twice with varying costs (e.g. 'Extra Equipment Set')
-- Hack for some purchases existing twice with varying costs (e.g. 'Extra Equipment Set')
  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 = {}
    for i, purchase in ipairs(purchaseList) do
for i, purchase in ipairs(purchaseList) do
      table.insert(resultPart, p._getPurchaseStat(purchase, statName, displayInline))
table.insert(resultPart, p._getPurchaseStat(purchase, statName, displayInline))
    end
end
    return table.concat(resultPart, ' or ')
return table.concat(resultPart, ' or ')
  end
end
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
end
    table.insert(costArray, Icons.RC(cost.raidCoins))
  end
if purch.contains.petID ~= nil then
  local itemArray = {}
return Icons.getExpansionIcon(purch.contains.petID)
  if cost.items ~= nil then
end
    for i, itemCost in Shared.skpairs(cost.items) do
end
      local item = Items.getItemByID(itemCost[1])
return ''
      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
function p._getPurchaseDescription(purch)
      table.insert(costArray, table.concat(itemArray, ", "))
if purch.customDescription ~= nil then
    end
local templateData = p._getPurchaseTemplateData(purch)
  end
return Shared.applyTemplateData(purch.customDescription, templateData)
 
elseif purch.contains ~= nil then
  local sep, lastSep = '<br/>', nil
local item = nil
  if displayInline then
if purch.contains.modifiers ~= nil then
    sep = ', '
return Modifiers.getModifiersText(purch.contains.modifiers, false)
    lastSep = Shared.tableCount(costArray) > 2 and ', and ' or ' and '
elseif purch.contains.petID ~= nil then
  end
local pet = Pets.getPetByID(purch.contains.petID)
  return Shared.joinList(costArray, sep, lastSep)
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
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 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


  local reqArray = {}
if not Shared.tableIsEmpty(costArray) then
  if reqs.slayerTaskCompletion ~= nil then
local sep, lastSep = '<br/>', '<br/>'
    for i, taskReq in Shared.skpairs(reqs.slayerTaskCompletion) do
if displayInline then
      local tierName = Constants.getSlayerTierName(taskReq[1])
sep = ', '
      table.insert(reqArray, 'Complete '..taskReq[2]..' '..tierName..' Slayer Tasks')
lastSep = Shared.tableCount(costArray) > 2 and ', and ' or ' and '
    end
end
  end
return mw.text.listToText(costArray, sep, lastSep)
end
return ''
end


  if reqs.dungeonCompletion ~= nil then
-- Generates description template data. See: shop.js, getDescriptionTemplateData()
    for i, dungReq in Shared.skpairs(reqs.dungeonCompletion) do
function p._getPurchaseTemplateData(purchase)
      local dung = AreaData['dungeons'][dungReq[1] + 1]
-- qty is a static value of 1 for Bank slots
      local dungStr = 'Complete '..Icons.Icon({dung.name, type='dungeon'})
local templateData = { qty = 1 }
      if dungReq[2] > 1 then
if purchase.contains ~= nil and purchase.contains.items ~= nil then
        dungStr = dungStr..' '..dungReq[2]..' times'
for i, itemDef in ipairs(purchase.contains.items) do
      end
templateData['qty' .. i] = itemDef.quantity
      table.insert(reqArray, dungStr)
end
    end
end
  end
return templateData
 
  if reqs.skillLevel ~= nil then
    for i, skillReq in Shared.skpairs(reqs.skillLevel) do
      local skillName = Constants.getSkillName(skillReq[1])
      table.insert(reqArray, Icons._SkillReq(skillName, skillReq[2]))
    end
  end
 
  if reqs.shopItemPurchased ~= nil then
    for i, shopReq in Shared.skpairs(reqs.shopItemPurchased) do
      local purchase = ShopData.Shop[shopReq[1]][shopReq[2] + 1]
      local isUpgrade = purchase.contains.items == nil or Shared.tableCount(purchase.contains.items) == 0
      table.insert(reqArray, Icons.Icon({purchase.name, type=(isUpgrade and 'upgrade' or 'item')})..' Purchased')
    end
  end
 
  if reqs.completionPercentage ~= nil then
    table.insert(reqArray, tostring(reqs.completionPercentage) .. '% Completion Log')
  end
 
  if reqs.text ~= nil then
    table.insert(reqArray, reqs.text)
  end
 
  return table.concat(reqArray, '<br/>')
end
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


function p._getPurchaseContents(purchase, asList)
function p._getPurchaseContents(purchase, asList)
  if asList == nil then asList = true end
if asList == nil then asList = true end
  local containArray = {}
local containArray = {}
  if purchase.contains.items ~= nil and Shared.tableCount(purchase.contains.items) > 0 then
local GPTotal = 0
    if not asList then
local currency = 'melvorD:GP'
      table.insert(containArray, '{| class="wikitable sortable stickyHeader"')
if purchase.contains ~= nil then
      table.insert(containArray, '|- class="headerRow-0"')
if purchase.contains.items ~= nil and not Shared.tableIsEmpty(purchase.contains.items) then
      table.insert(containArray, '! colspan="2" | Item !! Quantity')
if not asList then
    end
table.insert(containArray, '{| class="wikitable sortable stickyHeader"')
    for i, itemLine in Shared.skpairs(purchase.contains.items) do
table.insert(containArray, '|- class="headerRow-0"')
      local item = Items.getItemByID(itemLine[1])
table.insert(containArray, '! colspan="2" | Item !! Quantity !! Price')
      if asList then
end
        table.insert(containArray, Icons.Icon({item.name, type='item', qty=itemLine[2]}))
for i, itemLine in ipairs(purchase.contains.items) do
      else
local item = Items.getItemByID(itemLine.id)
        table.insert(containArray, '|-\r\n| style="min-width:25px"| ' .. Icons.Icon({item.name, type='item', notext=true, size='25'}))
local itemQty = itemLine.quantity
        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]))
if item.sellsForCurrency ~= nil then
      end
currency = item.sellsForCurrency
    end
end
  end
if asList then
  if purchase.charges ~= nil and purchase.charges > 0 then
table.insert(containArray, Icons.Icon({item.name, type='item', qty=itemQty}))
    if asList then
else
      table.insert(containArray, '+'..purchase.charges..' '..Icons.Icon({purchase.name, type='item'})..' Charges')
local GPVal = item.sellsFor * itemQty
    else
GPTotal = GPTotal + GPVal
      table.insert(containArray, '|-\r\n| style="min-width:25px"| ' .. Icons.Icon({purchase.name, type='item', notext=true, size='25'}))
table.insert(containArray, '|-\r\n| class="table-img"| ' .. Icons.Icon({item.name, type='item', notext=true}))
      table.insert(containArray, '| ' .. Icons.Icon({item.name, type='item', noicon=true}) .. ' Charges\r\n| data-sort-value="' .. purchase.charges .. '" style="text-align:right" | ' .. Shared.formatnum(purchase.charges))
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))
    end
table.insert(containArray, '| data-sort-value="' .. GPVal .. '"| ' .. Icons._Currency(currency, GPVal))
  end
end
  if not asList and Shared.tableCount(containArray) > 0 then table.insert(containArray, '|}') 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')
local delim = (asList and '<br/>' or '\r\n')
  return table.concat(containArray, delim)
return table.concat(containArray, delim)
end
end


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
return Shared.printError("Couldn't find purchase with name '" .. purchaseName .. "'")
else
return p._getPurchaseContents(purchase, asList)
end
end


  if purchase == nil then
function p._getPurchaseBuyLimitNumeric(purchase, gamemodeID)
    return "ERROR: Couldn't find purchase with name '" .. purchaseName .. "'[[Category:Pages with script errors]]"
local buyLimit = (purchase.defaultBuyLimit > 0 and purchase.defaultBuyLimit)
  else
if not Shared.tableIsEmpty(purchase.buyLimitOverrides) then
    return p._getPurchaseContents(purchase, asList)
local gamemodeLimit = GameData.getEntityByProperty(purchase.buyLimitOverrides, 'gamemodeID', gamemodeID)
  end
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 266: 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
    end
elseif costAmt.type == 'Linear' then
  end
return costAmt.initial
elseif costAmt.type == 'Glove' or costAmt.type == 'Fixed' and costAmt.cost > 0 then
return costAmt.cost
end
end
end
end
end


function p._getShopTable(Purchases, options)
function p._getShopTable(Purchases, options)
  local availableColumns = { 'Purchase', 'Type', 'Description', 'Cost', 'Requirements', 'Buy Limit' }
local availableColumns = { 'Purchase', 'Type', 'Description', 'Cost', 'Requirements', 'Buy Limit' }
  local headerPropsDefault = {
local headerPropsDefault = {
    ["Purchase"] = 'colspan="2"',
["Purchase"] = 'colspan="2"',
    ["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
  if options ~= nil and type(options) == 'table' then
if options ~= nil and type(options) == 'table' then
    -- Custom columns
-- Custom columns
    if options.columns ~= nil and type(options.columns) == 'table' then
if options.columns ~= nil and type(options.columns) == 'table' then
      for i, column in ipairs(options.columns) do
for i, column in ipairs(options.columns) do
        if Shared.contains(availableColumns, column) then
if Shared.contains(availableColumns, column) then
          table.insert(usedColumns, column)
table.insert(usedColumns, column)
        end
end
      end
end
    end
end
    -- Purchase column header text
-- Purchase column header text
    if options.purchaseHeader ~= nil and type(options.purchaseHeader) == 'string' then
if options.purchaseHeader ~= nil and type(options.purchaseHeader) == 'string' then
      purchHeader = options.purchaseHeader
purchHeader = options.purchaseHeader
    end
end
    -- Custom sort order
-- Custom sort order
    if options.sortOrder ~= nil and type(options.sortOrder) == 'function' then
if options.sortOrder ~= nil and type(options.sortOrder) == 'function' then
      sortOrder = options.sortOrder
sortOrder = options.sortOrder
    end
end
    -- Header properties
-- Header properties
    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
end
  end
-- Sticky header class
  -- Use default columns if no custom columns specified
if options.stickyHeader ~= nil then
  if Shared.tableCount(usedColumns) == 0 then
if type(options.stickyHeader) == 'boolean' then
    usedColumns = availableColumns
stickyHeader = options.stickyHeader
  end
elseif type(options.stickyHeader) == 'string' and string.lower(options.stickyHeader) == 'false' then
  if Shared.tableCount(headerProps) == 0 then
stickyHeader = false
    headerProps = headerPropsDefault
end
  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
-- Begin output generation
  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
    local prop = headerProps[column]
local prop = headerProps[column]
    table.insert(resultPart, '!' .. (prop and prop .. '| ' or ' ') .. (column == 'Purchase' and purchHeader or column))
table.insert(resultPart, '!' .. (prop and prop .. '| ' or ' ') .. (column == 'Purchase' and purchHeader or column))
  end
end


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


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


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


Line 432: 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
  local options = {}
local options = {}
  if frame.args ~= nil then
if frame.args ~= nil then
    if frame.args.columns ~= nil then options.columns = Shared.splitString(frame.args.columns, ',') end
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.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.columnProps ~= nil then
if frame.args.stickyHeader ~= nil then options.stickyHeader = frame.args.stickyHeader end
      local columnPropValues = Shared.splitString(frame.args.columnProps, ',')
if frame.args.columnProps ~= nil then
      local columnProps = {}
local columnPropValues = Shared.splitString(frame.args.columnProps, ',')
      for i, prop in pairs(columnPropValues) do
local columnProps = {}
        local propName, propValue = string.match(prop, '^([^:]+):(.*)$')
for i, prop in pairs(columnPropValues) do
        if propName ~= nil then
local propName, propValue = string.match(prop, '^([^:]+):(.*)$')
          columnProps[propName] = propValue
if propName ~= nil then
        end
columnProps[propName] = propValue
      end
end
      if Shared.tableCount(columnProps) > 0 then options.headerProps = columnProps end
end
    end
if Shared.tableCount(columnProps) > 0 then options.headerProps = columnProps end
  end
end
  local shopCat = ShopData.Shop[cat]
end
  if shopCat == nil then
local shopCat = GameData.getEntityByName('shopCategories', cat)
    return 'ERROR: Invalid category '..cat..'[[Category:Pages with script errors]]'
if shopCat == nil then
  else
return Shared.printError('Invalid category ' .. cat)
    return p._getShopTable(shopCat, options)
else
  end
local catPurchases = p.getPurchases(function(purch) return purch.category == shopCat.id end)
return p._getShopTable(catPurchases, options)
end
end
end


function p.getItemCostArray(itemID)
function p.getItemCostArray(itemID)
  local purchaseArray = {}
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


  for catName, cat in Shared.skpairs(ShopData.Shop) do
function p.getItemSourceArray(itemID)
    for j, purchase in Shared.skpairs(cat) do
local purchaseArray = {}
      if purchase.cost.items ~= nil then
for i, purchase in ipairs(GameData.rawData.shopPurchases) do
        for k, costLine in Shared.skpairs(purchase.cost.items) do
if purchase.contains ~= nil then
          if costLine[1] == itemID then
if purchase.contains.items ~= nil then
            local temp = p.processPurchase(catName, j - 1)
for j, itemContains in ipairs(purchase.contains.items) do
            temp.qty = costLine[2]
if itemContains.id == itemID then
            table.insert(purchaseArray, temp)
table.insert(purchaseArray, { ["purchase"] = purchase, ["qty"] = itemContains.quantity })
            break
break
          end
end
        end
end
      end
end
    end
if purchase.contains.itemCharges ~= nil and purchase.contains.itemCharges.id == itemID then
  end
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 purchaseArray
return p._getShopTable(purchList, { columns = { 'Purchase', 'Description', 'Cost', 'Requirements' }, purchaseHeader = 'Upgrade' })
end
end


function p.getItemSourceArray(itemID)
function p.getPurchaseDescription(frame)
  local purchaseArray = {}
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


  for catName, cat in Shared.skpairs(ShopData.Shop) do
function p.isSkillcapePurchase(purch, isSuperior, skillID)
    for j, purchase in Shared.skpairs(cat) do
-- Returns true or false depending on whether the purchase is a skillcape or not.
      if purchase.contains.items ~= nil and purchase.contains.items ~= nil then
-- If isSuperior is true, then this checks for superior skillcapes, false checks
        for k, containsLine in Shared.skpairs(purchase.contains.items) do
-- for regular skillcapes, and nil checks for both.
          if containsLine [1] == itemID then
-- If skillID is specified, then the skillcape must also relate to that skill
            local temp = p.processPurchase(catName, j - 1)
local checkCategories = (isSuperior == nil and {'melvorTotH:SuperiorSkillcapes', 'melvorD:Skillcapes'}) or (isSuperior and {'melvorTotH:SuperiorSkillcapes'}) or {'melvorD:Skillcapes'}
            temp.qty = containsLine[2]
-- Some skillcapes (such as Archaeology & Cartography) reside outside of the usual categories
            table.insert(purchaseArray, temp)
local overrideIDs = {
            break
['melvorTotH:SuperiorSkillcapes'] = {
          end
'melvorAoD:Superior_Archaeology_Skillcape',
        end
'melvorAoD:Superior_Cartography_Skillcape',
      end
'melvorAoD:Cape_of_Completion_AoD'
    end
},
  end
['melvorD:Skillcapes'] = {
'melvorAoD:Archaeology_Skillcape',
'melvorAoD:Cartography_Skillcape',
'melvorItA:Cape_of_Completion_ItA'
}
}


  return purchaseArray
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
end


function p.getPurchases(checkFunc)
function p._getShopSkillcapeTable(showSuperior)
  local purchaseList = {}
local capeList = p.getPurchases(function(purch) return p.isSkillcapePurchase(purch, showSuperior) end)
  for category, purchaseArray in Shared.skpairs(ShopData.Shop) do
local sortOrderFunc =
    for i, purchase in Shared.skpairs(purchaseArray) do
function(a, b)
      if checkFunc(category, purchase) then
local costA, costB = p._getPurchaseSortValue(a), p._getPurchaseSortValue(b)
        table.insert(purchaseList, p.processPurchase(category, i - 1))
if costA == costB then
      end
return Common.getPurchaseName(a) < Common.getPurchaseName(b)
    end
else
  end
return costA < costB
  return purchaseList
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
end


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


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


  result = result..'\r\n|-\r\n!style="text-align:right;"|Requirements'
function p.getSkillcapeTable(frame)
  result = result..'\r\n|'..p.getRequirementString(purchase.unlockRequirements)
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


  result = result..'\r\n|-\r\n!style="text-align:right;"|Contains'
local capeList = p.getPurchases(function(purch) return p.isSkillcapePurchase(purch, nil, skillID) end)
  result = result..'\r\n|style="text-align:right;"|'..p._getPurchaseContents(purchase, true)
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


  result = result..'\r\n|}'
function p.getGodUpgradeDungeon(purch)
  return result
-- 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
end


function p._getItemShopTable(item)
function p.getGodUpgradeTable()
  local tableArray = {}
local resultPart = {}
  local purchaseArray = p.getItemSourceArray(item.id)
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')


  for i, purchase in Shared.skpairs(purchaseArray) do
-- Rows for each God upgrade
    table.insert(tableArray, p._getPurchaseTable(purchase))
for i, upgrade in ipairs(upgradeList) do
  end
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(tableArray, '\r\n\r\n')
return table.concat(resultPart, '\r\n')
end
end


function p.getItemShopTable(frame)
function p.getAoDTable(frame)
  local itemName = frame.args ~= nil and frame.args[1] or frame
-- All purchases in the Atlas of Discovery category except for skillcapes, which are handled
  local item = Items.getItem(itemName)
-- by p.getShopSkillcapeTable()
  if item == nil then
local purchList = p.getPurchases(
    return "ERROR: No item named "..itemName.." exists in the data module"
function(purch)
  end
return purch.category == 'melvorAoD:AtlasOfDiscovery' and not p.isSkillcapePurchase(purch)
end
)


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


function p.getShopMiscUpgradeTable()
function p.getItATable(frame)
  local purchList = p.getPurchases(function(cat, purch) return cat == 'General' and string.find(purch.name, '^Auto Eat') == nil end)
-- As above for AoD, but for Into the Abyss instead
  return p._getShopTable(purchList, { columns = { 'Purchase', 'Description', 'Cost', 'Requirements' }, purchaseHeader = 'Upgrade' })
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
end


function p.getShopSkillcapeTable()
function p.getToolTable(toolName, searchString, modifiers, skillID)
  local capeList = p.getPurchases(function(cat, purch) return cat == 'Skillcapes' end)
local skillName = nil
  local sortOrderFunc = function(a, b)
if type(skillID) == 'string' then
                      if a.cost.gp == b.cost.gp then
skillName = Constants.getSkillName(skillID)
                        return a.name < b.name
end
                      else
local toolArray = p.getPurchases(
                        return a.cost.gp < b.cost.gp
function(purch)
                      end
return purch.category == 'melvorD:SkillUpgrades' and string.find(purch.id, searchString) ~= nil
                    end
end)
  return p._getShopTable(capeList,
 
    { columns = { 'Purchase', 'Description', 'Cost' },
if Shared.tableIsEmpty(toolArray) then
      purchaseHeader = 'Cape',
return ''
      sortOrder = sortOrderFunc,
end
      headerProps = {["Purchase"] = 'colspan="2" style="width:200px;"', ["Cost"] = 'style=width:120px;'}
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
end


function p.getAutoEatTable()
function p.getAxeTable(frame)
  local resultPart = {}
local modifiers = {
  local purchasesAE = p.getPurchases(function(cat, purch) return string.find(purch.name, '^Auto Eat') ~= nil end)
{
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


  -- Table header
function p.getAxeCoatingTable(frame)
  table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
local modifiers = {
  table.insert(resultPart, '|- class="headerRow-0"')
{
  table.insert(resultPart, '!colspan="2"|Auto Eat Tier!!Minimum Threshold!!Efficiency!!Max Healing!!Cost')
header = 'AXP Increase',
  -- Rows for each Auto Eat tier
sign = '+',
  local mods = {["increasedAutoEatEfficiency"] = 0, ["increasedAutoEatHPLimit"] = 0, ["increasedAutoEatThreshold"] = 0}
suffix = '%',
  for i, purchase in ipairs(purchasesAE) do
matchRule = {
    -- Modifiers must be accumulated as we go
["id"] = 'abyssalSkillXP',
    for modName, modValue in pairs(mods) do
["type"] = 'id',
      if purchase.contains.modifiers[modName] ~= nil then
["props"] = { ["skillID"] = 'melvorD:Woodcutting' }
        mods[modName] = mods[modName] + purchase.contains.modifiers[modName]
}
      end
}, {
    end
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


    local costAmt = p._getPurchaseSortValue(purchase)
function p.getPickaxeTable(frame)
    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}))
local modifiers = {
    table.insert(resultPart, '| ' .. Icons.Icon({purchase.name, type='upgrade', noicon=true}))
{
    table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. mods.increasedAutoEatThreshold .. '" | ' .. Shared.formatnum(Shared.round(mods.increasedAutoEatThreshold, 0, 0)) .. '%')
header = 'Mining Time Decrease',
    table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. mods.increasedAutoEatEfficiency .. '" | ' .. Shared.formatnum(Shared.round(mods.increasedAutoEatEfficiency, 0, 0)) .. '%')
sign = '',
    table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. mods.increasedAutoEatHPLimit .. '" | ' .. Shared.formatnum(Shared.round(mods.increasedAutoEatHPLimit, 0, 0)) .. '%')
suffix = '%',
    table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. costAmt .. '" | ' .. Icons.GP(costAmt))
matchRule = {
  end
["id"] = 'skillInterval',
  table.insert(resultPart, '|}')
["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 table.concat(resultPart, '\r\n')
return p.getToolTable('Pickaxe', '_Pickaxe$', modifiers, 'melvorD:Mining')
end
end


function p.getGodUpgradeTable()
function p.getPickaxeCoatingTable(frame)
  local resultPart = {}
local modifiers = {
  -- Obtain list of God upgrades: look for skill upgrades which have a dungeon completion
{
  --  requirement for an area whose name ends with 'God Dungeon'
header = 'AXP Increase',
  local getGodDungeon =
sign = '+',
    function(reqs)
suffix = '%',
      if reqs.dungeonCompletion ~= nil then
matchRule = {
        for i, areaReq in ipairs(reqs.dungeonCompletion) do
["id"] = 'abyssalSkillXP',
          local dung = AreaData['dungeons'][areaReq[1] + 1]
["type"] = 'id',
          if string.find(dung.name, 'God Dungeon$') ~= nil then return dung end
["props"] = { ["skillID"] = 'melvorD:Mining' }
        end
}
      end
}, {
    end
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


  local upgradeList = p.getPurchases(
function p.getRodTable(frame)
    function(cat, purch)
local modifiers = {
      if cat == 'SkillUpgrades' and purch.unlockRequirements ~= nil then
{
        return getGodDungeon(purch.unlockRequirements) ~= nil
header = 'Catch Time Decrease',
      end
sign = '',
      return false
suffix = '%',
    end)
matchRule = {
  if Shared.tableCount(upgradeList) == 0 then return '' end
["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'
}
}
}


  -- Table header
return p.getToolTable('Rod', '_Rod$', modifiers, 'melvorD:Fishing')
  table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
end
  table.insert(resultPart, '|- class="headerRow-0"')
  table.insert(resultPart, '!colspan="2"|God Upgrade!!Effect!!Dungeon!!Cost')


  -- Rows for each God upgrade
function p.getRodCoatingTable(frame)
  for i, upgrade in ipairs(upgradeList) do
local modifiers = {
    local dung = getGodDungeon(upgrade.unlockRequirements)
{
    local costSortValue = p._getPurchaseSortValue(upgrade)
header = 'AXP Increase',
    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}))
sign = '+',
    table.insert(resultPart, '| ' .. Icons.Icon({upgrade.name, type='upgrade', noicon=true}))
suffix = '%',
    table.insert(resultPart, '| ' .. upgrade.description)
matchRule = {
    table.insert(resultPart, '| data-sort-value="' .. dung.name .. '"| ' .. Icons.Icon({dung.name, type='dungeon'}))
["id"] = 'abyssalSkillXP',
    table.insert(resultPart, '| style="text-align:right;" data-sort-value="' .. costSortValue .. '"| ' .. p.getCostString(upgrade.cost, false))
["type"] = 'id',
  end
["props"] = { ["skillID"] = 'melvorD:Fishing' }
  table.insert(resultPart, '|}')
}
}, {
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


  return table.concat(resultPart, '\r\n')
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
end


Line 655: Line 1,171:
local validCategories = {'Cooking Fire', 'Furnace', 'Pot'}
local validCategories = {'Cooking Fire', 'Furnace', 'Pot'}
if category == nil or not Shared.contains({'Cooking Fire', 'Furnace', 'Pot'}, category) then
if category == nil or not Shared.contains({'Cooking Fire', 'Furnace', 'Pot'}, category) then
return 'ERROR: Invalid category specified. Must be one of the following: ' .. mw.text.listToText(validCategories, ', ', ' or ')
return Shared.printError('Invalid category specified. Must be one of the following: ' .. mw.text.listToText(validCategories, ', ', ' or '))
end
end
 
local categoryShort = string.match(category, '[^%s]+$')
local categoryShort = string.match(category, '[^%s]+$')
local bonusSkillID = Constants.getSkillID('Cooking')
local modifiers = {
local bonusColMod, bonusColName = nil, nil
['Cooking Fire'] = {
if category == 'Cooking Fire' then
{
bonusColMod = 'increasedSkillXP'
header = 'Bonus ' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' XP',
bonusColName = 'Bonus ' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' XP'
sign = '+',
else
suffix = '%',
bonusColMod = 'increasedChanceToDoubleItemsSkill'
matchRule = {
bonusColName = 'Double Items Chance'
["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
end
local modsPerfectChance = {'increasedChancePerfectCookFire', 'increasedChancePerfectCookFurnace',
 
                      'increasedChancePerfectCookPot', 'increasedChancePerfectCookGlobal'}
local modifiers = {
local totalBonusVal, totalPerfectChance = 0, 0
{
local utilityList = p.getPurchases(function(cat, purch) return cat == 'SkillUpgrades' and string.find(purch.name, category .. '$') ~= nil end)
header = 'Increased ' .. category .. 'Tool Level',
local resultPart = {}
sign = '+',
suffix = '',
    -- Table header
matchRule = {
    table.insert(resultPart, '{| class="wikitable stickyHeader"')
["id"] = string.lower(category) .. 'ToolLevel',
    table.insert(resultPart, '|- class="headerRow-0"')
["type"] = 'id'
    table.insert(resultPart, '!colspan="4"| !!colspan="2"|' .. bonusColName .. '!!colspan="2"|Bonus Perfect Chance')
}
    table.insert(resultPart, '|- class="headerRow-1"')
}
    table.insert(resultPart, '!colspan="2"|Name!!Level!!Cost' .. string.rep('!!This ' .. categoryShort .. '!!Total', 2))
}
   
return p.getToolTable(category, category .. '$', modifiers, 'melvorAoD:Archaeology')
    -- Row for each upgrade
end
    for i, utility in ipairs(utilityList) do
 
    -- First determine bonus XP/doubling chance and perfect chance
-- Below functions included for backwards compatibility
    local bonusVal, perfectChance = 0, 0
-- TODO: Remove dependency on these functions in all other modules
    if type(utility.contains) == 'table' then
function p._getPurchaseName(purchase)
    if type(utility.contains.modifiers) == 'table' then
return Common.getPurchaseName(purchase)
    for modName, modVal in pairs(utility.contains.modifiers) do
end
    if modName == bonusColMod and type(modVal) == 'table' then
function p._getPurchaseType(purchase)
    -- Bonus XP/doubling
return Common.getPurchaseType(purchase)
    for skID, skVal in pairs(modVal) do
end
    if skVal[1] == bonusSkillID then bonusVal = bonusVal + skVal[2] end
function p._getPurchaseIcon(iconArgs)
    end
return Common.getPurchaseIcon(iconArgs)
    elseif Shared.contains(modsPerfectChance, modName) then
end
    -- Perfect chance
function p.getRequirementString(reqs)
    perfectChance = perfectChance + modVal
return Common.getRequirementString(reqs, 'None')
    end
    end
    end
    end
    totalBonusVal = totalBonusVal + bonusVal
    totalPerfectChance = totalPerfectChance + perfectChance
   
    -- Mangle unlockRequirements so that it only includes skillLevels
    local unlockReqs = {}
    if type(utility.unlockRequirements) == 'table' then
    unlockReqs['skillLevel'] = utility.unlockRequirements.skillLevel
    end
   
    table.insert(resultPart, '|-')
    table.insert(resultPart, '|style="min-width:25px"|' .. Icons.Icon({utility.name, type='upgrade', size='50', notext=true}))
    table.insert(resultPart, '|' .. utility.name)
    table.insert(resultPart, '|style="text-align:right"|' .. p.getRequirementString(unlockReqs))
    table.insert(resultPart, '|style="text-align:right"|' .. p.getCostString(utility.cost, false))
    table.insert(resultPart, '|style="text-align:right"|' .. '+' .. bonusVal .. '%')
    table.insert(resultPart, '|style="text-align:right"|' .. '+' .. totalBonusVal .. '%')
    table.insert(resultPart, '|style="text-align:right"|' .. '+' .. perfectChance .. '%')
    table.insert(resultPart, '|style="text-align:right"|' .. '+' .. totalPerfectChance .. '%')
    end
    table.insert(resultPart, '|}')
return table.concat(resultPart, '\r\n')
end
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