Module:Shop: Difference between revisions

From Melvor Idle
(getGodUpgradeTable: Initial implementation)
(getShopTable: Implement columnProps parameter & document other parameters)
Line 235: Line 235:
end
end


-- getShopTable parameter definition:
--  columns:        Comma separated values indicating which columns are to be included & the order
--                  in which they are displayed.
--                  Values can be any of: Purchase, Type, Description, Cost, Requirements
--  columnProps:    Comma separated values indicating formatting to be applied to each column. Each
--                  value must be in the format column:property, e.g. Purchase:colspan="2"
--  sortOrder:      A function determining the order in which table items appear
--  purchaseHeader: Specifies header text for the Purchase column if not 'Purchase'
function p.getShopTable(frame)
function p.getShopTable(frame)
   local cat = frame.args ~= nil and frame.args[1] or frame
   local cat = frame.args ~= nil and frame.args[1] or frame
Line 242: Line 250:
     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
      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
   end
   local shopCat = ShopData.Shop[cat]
   local shopCat = ShopData.Shop[cat]

Revision as of 17:08, 1 August 2021

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

local p = {}

local ShopData = mw.loadData('Module:Shop/data')

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

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

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

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

  return table.concat(costArray, "<br/>")
end

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

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

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

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

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

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

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

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

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

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

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

  -- Various overrides for certain shop items
  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 }
  }
  -- Begin output generation
  local resultPart = {}
  -- Generate header
  table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
  table.insert(resultPart, '|- class="headerRow-0"')
  for i, column in ipairs(usedColumns) do
    local prop = headerProps[column]
    table.insert(resultPart, '!' .. (prop and prop .. '| ' or ' ') .. (column == 'Purchase' and purchHeader or column))
  end

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

    local purchType = 'Item'
    if purchase.contains.pet ~= nil then
      purchType = 'Pet'
    elseif purchase.contains.modifiers ~= nil or purchase.contains.items == nil or Shared.tableCount(purchase.contains.items) == 0 then
      purchType = 'Upgrade'
    elseif purchase.contains.items ~= nil and Shared.tableCount(purchase.contains.items) > 1 then
      purchType = 'Item Bundle'
    end

    local iconName = purchase.name
    local iconType = (purchType == 'Item Bundle' and 'item' or string.lower(purchType))
    local iconNoLink = nil
    local purchLink = ''
    local costString = p.getCostString(purchase.cost)
    if purchOverride ~= nil then
      if purchOverride.icon ~= nil then
        iconName = purchOverride.icon[1]
        iconType = purchOverride.icon[2]
      end
      if purchOverride.link == nil then
        iconNoLink = true
      else
        purchLink = purchOverride.link .. '|'
      end
      if purchOverride.cost ~= nil then costString = purchOverride.cost end
    end

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

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

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

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

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

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

  return purchaseArray
end

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

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

  return purchaseArray
end

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

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

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

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

  result = result..'\r\n|-\r\n!style="text-align:right;"|Contains'
  local containArray = {}
  if purchase.contains.items ~= nil then
    for i, itemLine in Shared.skpairs(purchase.contains.items) do
      local item = Items.getItemByID(itemLine[1])
      table.insert(containArray, Icons.Icon({item.name, type='item', qty=itemLine[2]}))
    end
  end
  if purchase.charges ~= nil and purchase.charges > 0 then
    table.insert(containArray, '+'..purchase.charges..' '..Icons.Icon({purchase.name, type='item'})..' Charges')
  end
  result = result..'\r\n|style="text-align:right;"|'..table.concat(containArray, '<br/>')

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

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

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

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

function p.getItemShopTable(frame)
  local itemName = frame.args ~= nil and frame.args[1] or frame
  local item = Items.getItem(itemName)
  if item == nil then
    return "ERROR: No item named "..itemName.." exists in the data module"
  end

  return p._getItemShopTable(item)
end

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

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

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

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

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

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

function p.getGodUpgradeTable()
  local resultPart = {}
  -- Obtain list of God upgrades: look for skill upgrades which have a dungeon completion
  --   requirement for an area whose name ends with 'God Dungeon'
  local getGodDungeon =
    function(reqs)
      if reqs.dungeonCompletion ~= nil then
        for i, areaReq in ipairs(reqs.dungeonCompletion) do
          local dung = Areas.getAreaByID('dungeon', areaReq[1])
          if string.find(dung.name, 'God Dungeon$') ~= nil then return dung end
        end
      end
    end
                          
  local upgradeList = p.getPurchases(
    function(cat, purch)
      if cat == 'SkillUpgrades' and purch.unlockRequirements ~= nil then
        return getGodDungeon(purch.unlockRequirements) ~= nil
      end
      return false
    end)
  if Shared.tableCount(upgradeList) == 0 then return '' end

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

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

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

return p