Module:Skills: Difference between revisions

From Melvor Idle
(Implement getSkillUnlockCostTable())
(Move Farming functions to Module:Skills/Gathering)
(14 intermediate revisions by 2 users not shown)
Line 1: Line 1:
--This module should avoid including skill specific functions which generate
--output for wiki pages, especially those which require() other modules. For
--these functions, consider using the appropriate module from the below list.
--Some skills have their own modules:
--Some skills have their own modules:
--Module:Magic for Magic
--Module:Magic for Magic
--Module:Prayer for Prayer
--Module:Prayer for Prayer
--Module:Agility for Agility
--Module:Skills/Agility for Agility
--Module:Skills/Summoning for Summoning
--Module:Skills/Gathering for Mining, Fishing, Woodcutting
--Module:Skills/Gathering for Mining, Fishing, Woodcutting
--Module:Skills/Artisan for Smithing, Cooking, Herblore, etc.
--Module:Skills/Artisan for Smithing, Cooking, Herblore, etc.
--Also be aware of:
--Module:Navboxes for navigation boxes appearing near the bottom of pages


local p = {}
local p = {}
Line 10: Line 18:
local ItemData = mw.loadData('Module:Items/data')
local ItemData = mw.loadData('Module:Items/data')
local SkillData = mw.loadData('Module:Skills/data')
local SkillData = mw.loadData('Module:Skills/data')
local Constants = mw.loadData('Module:Constants/data')


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


local MasteryCheckpoints = {.1, .25, .5, .95}
local MasteryCheckpoints = {.1, .25, .5, .95}


function p.getSkillID(skillName)
-- Thieving
  for skName, ID in Shared.skpairs(Constants.skill) do
function p.getThievingNPC(npcName)
    if skName == skillName then
local result = nil
      return ID
for i, npc in Shared.skpairs(SkillData.Thieving.NPCs) do
    end
if npc.name == npcName then
  end
result = Shared.clone(npc)
  return nil
break
end
end
return result
end
end


function p.getSkillName(skillID)
function p.getThievingNPCArea(npc)
  for skName, ID in Shared.skpairs(Constants.skill) do
if type(npc) == 'string' then
    if ID == skillID then
npc = p.getThievingNPC(npc)
      return skName
end
    end
 
  end
local result = nil
  return nil
for i, area in Shared.skpairs(SkillData.Thieving.Areas) do
for j, npcID in pairs(area.npcs) do
if npcID == npc.id then
result = area
break
end
end
end
return result
end
end


function p.getThievingNPCByID(ID)
function p._getThievingNPCStat(npc, statName)
  local result = Shared.clone(SkillData.Thieving[ID + 1])
local result = nil
  if result ~= nil then
 
    result.id = ID
if statName == 'level' then
  end
result = Icons._SkillReq('Thieving', npc.level)
  return result
elseif statName == 'maxHit' then
end
result = npc.maxHit * 10
elseif statName == 'area' then
local area = p.getThievingNPCArea(npc)
result = area.name
else
result = npc[statName]
end
 
if result == nil then
result = ''
end


function p.getThievingNPC(name)
return result
  local result = nil
  for i, npc in pairs(SkillData.Thieving) do
    if name == npc.name then
      result = Shared.clone(npc)
      result.id = i - 1
      break
    end
  end
  return result
end
end


function p.getThievingNPCStat(frame)
function p.getThievingNPCStat(frame)
  local args = frame.args ~= nil and frame.args or frame
local npcName = frame.args ~= nil and frame.args[1] or frame[1]
  local npcName = args[1]
local statName = frame.args ~= nil and frame.args[2] or frame[2]
  local statName = args[2]
local npc = p.getThievingNPC(npcName)
  local npc = p.getThievingNPC(npcName)
if npc == nil then
  if npc == nil then
return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
    return 'ERROR: Failed to find Thieving NPC with name ' .. name .. '[[Category:Pages with script errors]]'
end
  end
 
 
return p._getThievingNPCStat(npc, statName)
  return p._getThievingNPCStat(npc, statName)
end
end


function p._getThievingNPCStat(npc, stat)
function p.getThievingSourcesForItem(itemID)
  local itemDropChance = 0.75
local resultArray = {}
  local result = npc[stat]
  -- Overrides below
  if stat == 'maxHit' then
    result = result * 10
  elseif stat == 'lootList' then
    return p._formatLootTable(npc['lootTable'], itemDropChance, true)
  elseif stat == 'lootTable' then
    return p._formatLootTable(npc['lootTable'], itemDropChance, false)
  elseif stat == 'requirements' then
    if npc['level'] ~= nil then
      result = Icons._SkillReq('Thieving', npc['level'], true)
    else
      result = 'None'
    end
  elseif (stat == 'lootValue' or stat == 'pickpocketValue') then
    if stat == 'pickpocketValue' then
      local itemBP = Items.getItem("Bobby's Pocket")
      result = (1 + npc['maxCoins']) / 2 + itemBP.sellsFor * (1 / 120)
    else
      result = 0
    end
    result = Shared.round(result + p._getLootTableValue(npc['lootTable']) * itemDropChance, 2, 2)
  elseif stat == 'pageName' then
    local linkOverrides = { ['Golbin'] = 'Golbin (thieving)' }
    result = (linkOverrides[npc['name']] ~= nil and linkOverrides[npc['name']]) or npc['name']
  end
 
  return result
end


function p._getLootTableValue(lootTable)
local areaNPCs = {}
  -- Calculates the average GP value of a given loot table
  -- Expects lootTableIn to be in format {{itemID_1, itemWeight_1}, ..., {itemID_n, itemWeight_n}}
  if Shared.tableCount(lootTable) == 0 then
    return 0
  end


  local totalWeight = 0
--First check area unique drops
  for i, drop in pairs(lootTable) do
--If an area drops the item, add all the NPC ids to the list so we can add them later
    totalWeight = totalWeight + drop[2]
if not result then
  end
for i, area in pairs(SkillData.Thieving.Areas) do
  if totalWeight == 0 then
for j, drop in pairs(area.uniqueDrops) do
    return 0
if drop.itemID == itemID then
  end
for k, npcID in pairs(area.npcs) do
areaNPCs[npcID] = drop.qty
end
break
end
end
end
end


  local avgValue = 0
--Now go through and get drop chances on each NPC if needed
  for i, drop in pairs(lootTable) do
for i, npc in pairs(SkillData.Thieving.NPCs) do
    local item = Items.getItemByID(drop[1])
local totalWt = 0
    if item ~= nil then
local dropWt = 0
      avgValue = avgValue + item.sellsFor * (drop[2] / totalWeight)
local dropQty = 0
    end
for j, drop in pairs(npc.lootTable) do
  end
totalWt = totalWt + drop[2]
 
if drop[1] == itemID then
  return avgValue
dropWt = drop[2]
end
dropQty = drop[3]
end
end
if dropWt > 0 then
table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = dropQty, wt = dropWt * SkillData.Thieving.ItemChance, totalWt = totalWt * 100, level = npc.level})
end


function p._formatLootTable(lootTableIn, chanceMultIn, asList)
--Chance of -1 on unique drops is to indicate variable chance
  -- Expects lootTableIn to be in format {{itemID_1, itemWeight_1}, ..., {itemID_n, itemWeight_n}}
if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID == itemID then
  if Shared.tableCount(lootTableIn) == 0 then
table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.qty, maxQty = npc.uniqueDrop.qty, wt = -1, totalWt = -1, level = npc.level})
    return ''
end
  end


  local chanceMult = (chanceMultIn or 1) * 100
if areaNPCs[npc.id] ~= nil then
  local lootTable = Shared.clone(lootTableIn)
table.insert(resultArray, {npc = npc.name, minQty = areaNPCs[npc.id], maxQty = areaNPCs[npc.id], wt = SkillData.Thieving.AreaUniqueChance, totalWt = 100, level = npc.level})
  -- Sort table from most to least common drop
end
  table.sort(lootTable, function(a, b)
end
                          if a[2] == b[2] then
                            return a[1] < b[1]
                          else
                            return a[2] > b[2]
                          end
                        end)


  local totalWeight = 0
for i, drop in pairs(SkillData.Thieving.RareItems) do
  for i, drop in pairs(lootTable) do
if drop.itemID == itemID then
    totalWeight = totalWeight + drop[2]
table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = 1})
  end
end
  if totalWeight == 0 then
end
    return ''
  end


  -- Get the length (in characters) of the largest drop chance so that they can be right aligned
return resultArray
  -- [4/16/21]: Adding info for no drop
end
  local maxDropLen = math.max(string.len(Shared.round(100 - chanceMult, 2, 2)), string.len(Shared.round(lootTable[1][2] / totalWeight * chanceMult, 2, 2)))


  local returnPart = {}
-- Astrology
  -- Generate header
function p.getConstellationByID(constID)
  if asList then
return SkillData.Astrology.Constellations[constID]
    if chanceMult < 100 then
end
      table.insert(returnPart, '* ' .. string.rep('&nbsp;', math.max(0, (maxDropLen - string.len(Shared.round(100 - chanceMult, 2, 2))) * 2)) .. Shared.round(100 - chanceMult, 2, 2) .. '% No Item')
    end
  else
    table.insert(returnPart, '{|class="wikitable sortable"\r\n!Item!!Price!!colspan="2"|Chance')
  end
  -- Generate row for each item
  for i, drop in pairs(lootTable) do
    local item, itemText, sellsFor, dropChance = Items.getItemByID(drop[1]), 'Unknown', 0, Shared.round(drop[2] / totalWeight * chanceMult, 2, 2)
    if item ~= nil then
      itemText, sellsFor = Icons.Icon({item.name, type='item'}), item.sellsFor
    end
    if asList then
      table.insert(returnPart, '* ' .. string.rep('&nbsp;', math.max(0, (maxDropLen - string.len(dropChance)) * 2)) .. dropChance .. '% ' .. itemText)
    else
      table.insert(returnPart, '|-\r\n|' .. itemText)
      table.insert(returnPart, '|style="text-align:right;" data-sort-value="' .. sellsFor .. '"|' .. Icons.GP(sellsFor))
      table.insert(returnPart, '|style="text-align:right;" data-sort-value="' .. dropChance .. '"|' .. Shared.fraction(drop[2] * chanceMult, totalWeight * 100))
      table.insert(returnPart, '|style="text-align:right;"|' .. dropChance .. '%')
    end
  end
  if not asList then
    table.insert(returnPart, '|-class="sortbottom" \r\n!colspan="2"|Total:')
    local textTotChance = ''
    if chanceMult < 100 then
      textTotChance = '|style="text-align:right"|' .. Shared.fraction(chanceMult, 100) .. '\r\n|'
    else
      textTotChance = '|colspan="2" '
    end
    textTotChance = textTotChance .. 'style="text-align:right;"|' .. Shared.round(chanceMult, 2, 2) .. '%' .. '\r\n|}'
    table.insert(returnPart, textTotChance)
  end


  return table.concat(returnPart, '\r\n')
function p.getConstellation(constName)
for i, const in ipairs(SkillData.Astrology.Constellations) do
if const.name == constName then
return const
end
end
return nil
end
end


function p.getThievingNPCTable()
function p.getConstellations(checkFunc)
  local returnPart = {}
local result = {}
for i, const in ipairs(SkillData.Astrology.Constellations) do
if checkFunc(const) then
table.insert(result, const)
end
end
return result
end


  -- Create table header
-- For a given constellation cons and modifier value modValue, generates and returns
  table.insert(returnPart, '{| class="wikitable sortable stickyHeader"')
-- a table of modifiers, much like any other item/object elsewhere in the game.
  table.insert(returnPart, '|- class="headerRow-0"\r\n!Target!!Name!!' .. Icons.Icon({'Thieving', type='skill', notext=true}).. ' Level!!Experience!!Max Hit!!Max Coins!!<abbr title="Assumes all loot is sold, and no GP boosts apply (such as those from Mastery & Gloves of Silence)">GP/Theft</abbr>')
-- includeStandard: true|false, determines whether standard modifiers are included
 
-- includeUnique: true|false, determines whether unique modifiers are included
  -- Create row for each NPC
-- isDistinct: true|false, if true, the returned list of modifiers is de-duplicated
  for i, npc in Shared.skpairs(SkillData.Thieving) do
-- asKeyValue: true|false, if true, returns key/value pairs like usual modifier objects
    local linkText = (npc.name ~= p._getThievingNPCStat(npc, 'pageName') and p._getThievingNPCStat(npc, 'pageName') .. '|' .. npc.name) or npc.name
function p._buildAstrologyModifierArray(cons, modValue, includeStandard, includeUnique, isDistinct, asKeyValue)
    table.insert(returnPart, '|-\r\n|style="text-align: left;" |' .. Icons.Icon({npc.name, type='thieving', size=50, notext=true}))
-- Temporary function to determine if the table already contains a given modifier
    table.insert(returnPart, '|style="text-align: left;" |[[' .. linkText .. ']]')
local containsMod = function(modList, modNew)
    table.insert(returnPart, '|style="text-align: right;" |' .. p._getThievingNPCStat(npc, 'level'))
for i, modItem in ipairs(modList) do
    table.insert(returnPart, '|style="text-align: right;" |' .. p._getThievingNPCStat(npc, 'xp'))
-- Check mod names & value data types both equal
    table.insert(returnPart, '|style="text-align: right;" |' .. p._getThievingNPCStat(npc, 'maxHit'))
if modItem[1] == modNew[1] and type(modItem[2]) == type(modNew[2]) then
    table.insert(returnPart, '|style="text-align: right;" data-sort-value="' .. p._getThievingNPCStat(npc, 'maxCoins') .. '" |' .. Icons.GP(p._getThievingNPCStat(npc, 'maxCoins')))
if type(modItem[2]) == 'table' then
    table.insert(returnPart, '|style="text-align: right;" data-sort-value="' .. p._getThievingNPCStat(npc, 'pickpocketValue') .. '" |' .. Icons.GP(p._getThievingNPCStat(npc, 'pickpocketValue')))
if Shared.tablesEqual(modItem[2], modNew[2]) then
  end
return true
  table.insert(returnPart, '|}')
end
elseif modItem[2] == modNew[2] then
return true
end
end
end
return false
end


  return table.concat(returnPart, '\r\n')
local addToArray = function(modArray, modNew)
end
if not isDistinct or (isDistinct and not containsMod(modArray, modNew)) then
table.insert(modArray, modNew)
end
end


function p.getThievingNavbox()
local modTypes = {}
  local returnPart = {}
if includeStandard then
table.insert(modTypes, 'standardModifiers')
end
if includeUnique then
table.insert(modTypes, 'uniqueModifiers')
end


  -- Create table header
local modArray = {}
  table.insert(returnPart, '{| class="wikitable" style="text-align:center; clear:both; margin:auto; margin-bottom:1em;"')
local isSkillMod = {}
  table.insert(returnPart, '|-\r\n!' .. Icons.Icon({'Thieving', type='skill', notext=true}) .. '[[Thieving|Thieving Targets]]')
for _, modType in ipairs(modTypes) do
  table.insert(returnPart, '|-\r\n|')
for i, skillMods in ipairs(cons[modType]) do
 
local skillID = cons.skills[i]
  local npcList = {}
if skillID ~= nil then
  -- Create row for each NPC
for j, modName in ipairs(skillMods) do
  for i, npc in Shared.skpairs(SkillData.Thieving) do
local modBaseName, modText, sign, isNegative, unsign, modBase = Constants.getModifierDetails(modName)
    local linkText = (npc.name ~= p._getThievingNPCStat(npc, 'pageName') and p._getThievingNPCStat(npc, 'pageName') .. '|' .. npc.name) or npc.name
-- Check if modifier varies by skill, and amend the modifier value accordingly
    table.insert(npcList, Icons.Icon({npc.name, type='thieving', notext=true}) .. ' [[' .. linkText .. ']]')
local modVal = modValue
  end
if Shared.contains(modText, '{SV0}') then
  table.insert(returnPart, table.concat(npcList, ' • '))
isSkillMod[modName] = true
  table.insert(returnPart, '|}')
modVal = {skillID, modValue}
end
addToArray(modArray, {modName, modVal})
end
end
end
end


  return table.concat(returnPart, '\r\n')
if asKeyValue then
local modArrayKV = {}
for i, modDefn in ipairs(modArray) do
local modName, modVal = modDefn[1], modDefn[2]
local isSkill = isSkillMod[modName]
if modArrayKV[modName] == nil then
modArrayKV[modName] = (isSkill and { modVal } or modVal)
elseif isSkill then
table.insert(modArrayKV[modName], modVal)
else
modArrayKV[modName] = modArrayKV[modName] + modVal
end
end
return modArrayKV
else
return modArray
end
end
end


-- Mastery
function p.getMasteryUnlockTable(frame)
function p.getMasteryUnlockTable(frame)
  local skillName = frame.args ~= nil and frame.args[1] or frame
local skillName = frame.args ~= nil and frame.args[1] or frame
  local skillID = p.getSkillID(skillName)
local skillID = Constants.getSkillID(skillName)
  if skillID == nil then
if skillID == nil then
    return "ERROR: Failed to find a skill ID for "..skillName
return "ERROR: Failed to find a skill ID for "..skillName
  end
end


  local unlockTable = SkillData.MasteryUnlocks[skillID]
local unlockTable = SkillData.MasteryUnlocks[skillID]
  if unlockTable == nil then
if unlockTable == nil then
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
return 'ERROR: Failed to find Mastery Unlock data for '..skillName
  end
end


  local result = '{|class="wikitable"\r\n!Level!!Unlock'
local result = '{|class="wikitable"\r\n!Level!!Unlock'
  for i, unlock in Shared.skpairs(unlockTable) do
for i, unlock in Shared.skpairs(unlockTable) do
    result = result..'\r\n|-'
result = result..'\r\n|-'
    result = result..'\r\n|'..unlock.level..'||'..unlock.unlock
result = result..'\r\n|'..unlock.level..'||'..unlock.unlock
  end
end
  result = result..'\r\n|}'
result = result..'\r\n|}'
  return result
return result
end
end


function p.getMasteryCheckpointTable(frame)
function p.getMasteryCheckpointTable(frame)
  local skillName = frame.args ~= nil and frame.args[1] or frame
local skillName = frame.args ~= nil and frame.args[1] or frame
  local skillID = p.getSkillID(skillName)
local skillID = Constants.getSkillID(skillName)
  if skillID == nil then
if skillID == nil then
    return "ERROR: Failed to find a skill ID for "..skillName
return "ERROR: Failed to find a skill ID for "..skillName
  end
end


  if SkillData.MasteryCheckpoints[skillID] == nil then
if SkillData.MasteryCheckpoints[skillID] == nil then
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
return 'ERROR: Failed to find Mastery Unlock data for '..skillName
  end
end


  local bonuses = SkillData.MasteryCheckpoints[skillID].bonuses
local bonuses = SkillData.MasteryCheckpoints[skillID].bonuses
  local totalPoolXP = SkillData.MasteryPoolXP[skillID + 1]
local totalPoolXP = SkillData.MasteryPoolXP[skillID + 1]


  local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
  for i, bonus in Shared.skpairs(bonuses) do
for i, bonus in Shared.skpairs(bonuses) do
    result = result..'\r\n|-'
result = result..'\r\n|-'
    result = result..'\r\n|'..(MasteryCheckpoints[i] * 100)..'%||'
result = result..'\r\n|'..(MasteryCheckpoints[i] * 100)..'%||'
    result = result..Shared.formatnum(totalPoolXP * MasteryCheckpoints[i])..' xp||'..bonus
result = result..Shared.formatnum(totalPoolXP * MasteryCheckpoints[i])..' xp||'..bonus
  end
end
  result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
  result = result..'\r\n|'..Shared.formatnum(totalPoolXP)
result = result..'\r\n|'..Shared.formatnum(totalPoolXP)
  result = result..'\r\n|}'
result = result..'\r\n|}'
  return result
return result
end
end


function p._getFarmingTable(category)
function p.getMasteryTokenTable()
  local seedList = {}
local baseTokenChance = 18500
  if category == 'Allotment' or category == 'Herb' or category == 'Tree' then
local masterySkills = {}
    seedList = Items.getItems(function(item) return item.tier == category end)
  else
    return 'ERROR: Invalid farming category. Please choose Allotment, Herb, or Tree'
  end


  local result = '{|class="wikitable sortable stickyHeader"'
-- Find all mastery tokens
  result = result..'\r\n|- class="headerRow-0"'
local masteryTokens = Items.getItems(function(item) return item.isToken ~= nil and item.skill ~= nil and item.isToken end)
  result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
for i, item in pairs(masteryTokens) do
  result = result..'!!XP!!Growth Time!!Seed Value'
local milestones = SkillData.Milestones[item.skill + 1]
  if category == 'Allotment' then
if milestones ~= nil then
    result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
table.insert(masterySkills, {tokenRef = i, skillID = item.skill, milestoneCount = milestones})
  elseif category == 'Herb' then
end
    result = result..'!!colspan="2"|Herb!!Herb Value'
end
  elseif category == 'Tree' then
table.sort(masterySkills, function(a, b)
    result = result..'!!colspan="2"|Logs!!Log Value'
if a['milestoneCount'] == b['milestoneCount'] then
  end
return a['skillID'] < b['skillID']
  result = result..'!!Seed Sources'
else
 
return a['milestoneCount'] > b['milestoneCount']
  table.sort(seedList, function(a, b) return a.farmingLevel < b.farmingLevel end)
end
end)


  for i, seed in pairs(seedList) do
-- Generate output table
    result = result..'\r\n|-'
local resultPart = {}
    result = result..'\r\n|'..Icons.Icon({seed.name, type='item', size='50', notext=true})..'||[['..seed.name..']]'
local CCI = Items.getItem('Clue Chasers Insignia')
    result = result..'||'..seed.farmingLevel..'||'..Shared.formatnum(seed.farmingXP)
local CCIIcon = Icons.Icon({'Clue Chasers Insignia', type='item', notext=true})
    result = result..'||data-sort-value="'..seed.timeToGrow..'"|'..Shared.timeString(seed.timeToGrow, true)
if CCI == nil then return '' end
    result = result..'||data-sort-value="'..seed.sellsFor..'"|'..Icons.GP(seed.sellsFor)


    local crop = Items.getItemByID(seed.grownItemID)
table.insert(resultPart, '{| class="wikitable sortable"')
    result = result..'||'..Icons.Icon({crop.name, type='item', size='50', notext=true})..'||[['..crop.name..']]'
table.insert(resultPart, '\r\n!rowspan="2"|Token!!rowspan="2"|Skill!!colspan="2"|Approximate Mastery Token Chance')
    if category == 'Allotment' then
table.insert(resultPart, '\r\n|-\r\n!Without ' .. CCIIcon .. '!!With ' .. CCIIcon)
      result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..(crop.healsFor * 10)
    end
    result = result..'||data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
    result = result..'||'..ItemSourceTables._getItemSources(seed)
  end


  result = result..'\r\n|}'
for i, m in ipairs(masterySkills) do
  return result
local token = masteryTokens[m.tokenRef]
end
local denom = math.floor(baseTokenChance / m['milestoneCount'])
local denomCCI = Shared.round(baseTokenChance / (m['milestoneCount'] * (1 + CCI.increasedItemChance / 100)), 0, 0)


function p.getFarmingTable(frame)
table.insert(resultPart, '\r\n|-')
  local category = frame.args ~= nil and frame.args[1] or frame
table.insert(resultPart, '\r\n|style="text-align:center"|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
table.insert(resultPart, '\r\n|' .. Icons.Icon({Constants.getSkillName(m['skillID']), type='skill'}))
table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Shared.formatnum(denom))
table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. denomCCI .. '"|1/' .. Shared.formatnum(denomCCI))
end
table.insert(resultPart, '\r\n|}')


  return p._getFarmingTable(category)
return table.concat(resultPart)
end
end


function p.getFarmingFoodTable(frame)
-- Skill unlock costs for Adventure game mode
  local result = '{| class="wikitable sortable stickyHeader"'
function p.getSkillUnlockCostTable()
  result = result..'\r\n|- class="headerRow-0"'
local returnPart = {}
  result = result..'\r\n!colspan="2"|Crop!!'..Icons.Icon({"Farming", type="skill", notext=true})..' Level'
table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative Cost')
  result = result..'!!Healing!!Value'
 
  local itemArray = Items.getItems(function(item) return item.grownItemID ~= nil end)


  table.sort(itemArray, function(a, b) return a.farmingLevel < b.farmingLevel end)
local accCost = 0
for i, cost in ipairs(SkillData.SkillUnlockCosts) do
accCost = accCost + cost
table.insert(returnPart, '|-')
table.insert(returnPart, '|' .. i .. '||' .. Icons.GP(cost) .. '||' .. Icons.GP(accCost))
end
table.insert(returnPart, '|}')


  for i, item in Shared.skpairs(itemArray) do
return table.concat(returnPart, '\r\n')
    local crop = Items.getItemByID(item.grownItemID)
    if crop.healsFor ~= nil and crop.healsFor > 0 then
      result = result..'\r\n|-'
      result = result..'\r\n|'..Icons.Icon({crop.name, type='item', notext='true', size='50'})..'||[['..crop.name..']]'
      result = result..'||style="text-align:right;"|'..item.farmingLevel
      result = result..'||style="text-align:right" data-sort-value="'..crop.healsFor..'"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..(crop.healsFor * 10)
      result = result..'||style="text-align:right" data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
    end
  end
 
  result = result..'\r\n|}'
 
  return result
end
end


function p.getFarmingPlotTable(frame)
-- Accepts 1 parameter, being either:
  local areaName = frame.args ~= nil and frame.args[1] or frame
--  'Smelting', for which a table of all bars is generated, or
  local patches = nil
--  A bar or tier name, which if valid generates a table of all smithing recipes using that bar/tier
  for i, area in Shared.skpairs(SkillData.Farming.Patches) do
function p.getSmithingTable(frame)
    if area.areaName == areaName then
local tableType = frame.args ~= nil and frame.args[1] or frame
      patches = area.patches
tableType = Shared.splitString(tableType, ' ')[1]
      break
-- Translates Smithing category names to Smithing recipe data categories
    end
local categoryMap = {
  end
['Smelting'] = 0,
  if patches == nil then
['Bronze'] = 1,
    return "ERROR: Invalid area name.[[Category:Pages with script errors"
['Iron'] = 2,
  end
['Steel'] = 3,
['Mithril'] = 4,
['Adamant'] = 5,
['Adamantite'] = 5,
['Rune'] = 6,
['Runite'] = 6,
['Dragon'] = 7,
['Dragonite'] = 7
}
local categoryID = categoryMap[tableType]
if categoryID == nil then
return 'ERROR: Invalid Smithing category: "' .. tableType .. '"[[Category:Pages with script errors]]'
end


  local result = '{|class="wikitable"'
-- Build a list of recipes to be included, and a list of bars while we're at it
  result = result..'\r\n!Plot!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level!!Cost'
-- The bar list will be used later for value/bar calculations
local recipeList, barIDList = {}, {}
for i, recipe in ipairs(SkillData.Smithing.Recipes) do
if recipe.category == categoryID then
local recipeItem = Items.getItemByID(recipe.itemID)
if recipeItem ~= nil then
table.insert(recipeList, { id = i, level = recipe.level, itemName = recipeItem.name, itemValue = recipeItem.sellsFor })
end
elseif recipe.category == 0 then
barIDList[recipe.itemID] = true
end
end


  for i, patch in Shared.skpairs(patches) do
-- Generate output table
    result = result..'\r\n|-\r\n|'..i
local resultPart = {}
    result = result..'||style="text-align:right;" data-sort-value="0"|'..patch.level
table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
    if patch.cost == 0 then
table.insert(resultPart, '\r\n|-class="headerRow-0"')
      result = result..'||Free'
table.insert(resultPart, '\r\n!Item!!Name!!'..Icons.Icon({'Smithing', type='skill', notext=true})..' Level!!XP!!Value!!Ingredients')
    else
--Adding value/bar for things other than smelting
      result = result..'||style="text-align:right;" data-sort-value="'..patch.cost..'"|'..Icons.GP(patch.cost)
if categoryID > 0 then
    end
table.insert(resultPart, '!!Value/Bar')
  end
end


  result = result..'\r\n|}'
table.sort(recipeList, function(a, b)
  return result
if a.level ~= b.level then
end
return a.level < b.level
else
return a.itemName < b.itemName
end
end)


function p.getSmithingTable(frame)
for i, recipeDef in ipairs(recipeList) do
  local tableType = frame.args ~= nil and frame.args[1] or frame
local recipe = SkillData.Smithing.Recipes[recipeDef.id]
  local bar = nil
local totalValue = recipe.baseQuantity * recipeDef.itemValue
  if tableType ~= 'Smelting' then
-- Determine the bar quantity & build the recipe cost string
    bar = Items.getItem(tableType)
local barQty, costString = 0, {}
    if bar == nil then
for j, itemCost in ipairs(recipe.itemCosts) do
      return 'ERROR: Could not find an item named '..tableType..' to build a smithing table with'
local costItem = Items.getItemByID(itemCost.id)
    elseif bar.type ~= 'Bar' then
if costItem ~= nil then
      return 'ERROR: '..tableType.." is not a bar and thus can't be used for smithing"
table.insert(costString, Icons.Icon({costItem.name, type='item', qty=itemCost.qty, notext=true}))
    end
end
  end
if barIDList[itemCost.id] then
barQty = barQty + itemCost.qty
end
end


  local smithList = {}
table.insert(resultPart, '\r\n|-')
  for i, item in pairs(ItemData.Items) do
table.insert(resultPart, '\r\n| ' .. Icons.Icon({recipeDef.itemName, type='item', size=50, notext=true}))
    if item.smithingLevel ~= nil then
table.insert(resultPart, '\r\n| ')
      if tableType == 'Smelting' then
if recipe.baseQuantity > 1 then
        if item.type == 'Bar' then
table.insert(resultPart, recipe.baseQuantity .. 'x ')
          table.insert(smithList, item)
end
        end
table.insert(resultPart, Icons.Icon({recipeDef.itemName, type='item', noicon=true}))
      else
table.insert(resultPart, '\r\n|data-sort-value="' .. recipe.level .. '"| ' .. Icons._SkillReq('Smithing', recipe.level))
        for j, req in pairs(item.smithReq) do
table.insert(resultPart, '\r\n|data-sort-value="' .. recipe.baseXP .. '"| ' .. Shared.formatnum(recipe.baseXP))
          if req.id == bar.id then
table.insert(resultPart, '\r\n|data-sort-value="' .. totalValue .. '"| ' .. Icons.GP(recipeDef.itemValue))
            table.insert(smithList, item)
if recipe.baseQuantity > 1 then
          end
table.insert(resultPart, ' (x' .. recipe.baseQuantity .. ')')
        end
end
      end
table.insert(resultPart, '\r\n| ' .. table.concat(costString, ', '))
    end
if categoryID > 0 then
  end
local barVal, barValTxt = 0, 'N/A'
if barQty > 0 then
barVal = totalValue / barQty
barTxt = Icons.GP(Shared.round(barVal, 1, 1))
end
table.insert(resultPart, '\r\n|data-sort-value="' .. barVal .. '"| ' .. barTxt)
end
end
table.insert(resultPart, '\r\n|}')


  local result = '{|class="wikitable sortable stickyHeader"'
return table.concat(resultPart)
  result = result..'\r\n|-class="headerRow-0"'
  result = result..'\r\n!Item!!Name!!'..Icons.Icon({'Smithing', type='skill', notext=true})..' Level!!XP!!Value!!Ingredients'
  --Adding value/bar for things other than smelting
  if bar ~= nil then result = result..'!!Value/Bar' end
 
  table.sort(smithList, function(a, b)
                          if a.smithingLevel ~= b.smithingLevel then
                            return a.smithingLevel < b.smithingLevel
                          else
                            return a.name < b.name
                          end end)
  for i, item in Shared.skpairs(smithList) do
    result = result..'\r\n|-'
    result = result..'\r\n|'..Icons.Icon({item.name, type='item', size='50', notext=true})..'||'
    local qty = item.smithingQty ~= nil and item.smithingQty or 1
    if qty > 1 then
      result = result..item.smithingQty..'x '
    end
    result = result..'[['..item.name..']]'
    result = result..'||data-sort-value="'..item.smithingLevel..'"|'..Icons._SkillReq('Smithing', item.smithingLevel)
    result = result..'||'..item.smithingXP
    local totalValue = item.sellsFor * qty
    result = result..'||data-sort-value="'..totalValue..'"|'..Icons.GP(item.sellsFor)
    if qty > 1 then
      result = result..' (x'..qty..')'
    end
    result = result..'||'
    local barQty = 0
    for i, mat in Shared.skpairs(item.smithReq) do
      matItem = Items.getItemByID(mat.id)
      if i > 1 then result = result..', ' end
      result = result..Icons.Icon({matItem.name, type='item', qty=mat.qty, notext=true})
      if bar ~= nil and mat.id == bar.id then
        barQty = mat.qty
      end
    end
    --Add the data for the value per bar
    if bar ~= nil then
      if barQty == 0 then
        result = result..'||data-sort-value="0"|N/A'
      else
        local barVal = totalValue / barQty
        result = result..'||data-sort-value="'..barVal..'"|'..Icons.GP(Shared.round(barVal, 1, 1))
      end
    end
  end
 
  result = result..'\r\n|}'
  return result
end
end


function p.getFiremakingTable(frame)
function p.getFiremakingTable(frame)
  local resultPart = {}
local resultPart = {}
  table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
  table.insert(resultPart, '\r\n|-class="headerRow-0"')
table.insert(resultPart, '\r\n|-class="headerRow-0"')
  table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level')
table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level')
  table.insert(resultPart, '!!rowspan="2"|Burn Time!!colspan="2"|Without Bonfire!!colspan="2"|With Bonfire!!rowspan="2"|Bonfire Bonus!!rowspan="2"|Bonfire Time')
table.insert(resultPart, '!!rowspan="2"|Burn Time!!colspan="2"|Without Bonfire!!colspan="2"|With Bonfire!!rowspan="2"|Bonfire Bonus!!rowspan="2"|Bonfire Time')
  table.insert(resultPart, '\r\n|-class="headerRow-1"')
table.insert(resultPart, '\r\n|-class="headerRow-1"')
  table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')
table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')
 
  for i, logData in Shared.skpairs(SkillData.Firemaking) do
    local name = Shared.titleCase(logData.type..' Logs')
    local burnTime = logData.interval / 1000
    local XPS = logData.xp / burnTime
    local XP_BF = logData.xp * (1 + logData.bonfireBonus / 100)
    local XPS_BF = XP_BF / burnTime
 
    table.insert(resultPart, '\r\n|-')
    table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
    table.insert(resultPart, '||[['..name..']]')
    table.insert(resultPart, '||style ="text-align: right;"|'..logData.level)
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true))
    table.insert(resultPart, '||style ="text-align: right;"|'..logData.xp)
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.round(XPS, 2, 2))
    table.insert(resultPart, '||style ="text-align: right;"|'..Shared.round(XP_BF, 2, 0))
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.round(XPS_BF, 2, 2))
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..logData.bonfireBonus..'"|'..logData.bonfireBonus..'%')
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..logData.bonfireInterval..'"|'..Shared.timeString(logData.bonfireInterval / 1000, true))
  end


  table.insert(resultPart, '\r\n|}')
for i, logData in Shared.skpairs(SkillData.Firemaking) do
  return table.concat(resultPart)
local logs = Items.getItemByID(logData.logID)
end
local name = logs.name
 
local burnTime = logData.baseInterval / 1000
function p.getMasteryTokenTable()
local bonfireTime = logData.baseBonfireInterval / 1000
  local baseTokenChance = 18500
local XPS = logData.baseXP / burnTime
  local masterySkills = {}
local XP_BF = logData.baseXP * (1 + logData.bonfireXPBonus / 100)
 
local XPS_BF = XP_BF / burnTime
  -- Find all mastery tokens
  local masteryTokens = Items.getItems(function(item) return item.isToken ~= nil and item.skill ~= nil and item.isToken end)
  for i, item in pairs(masteryTokens) do
    local milestones = SkillData.Milestones[item.skill + 1]
    if milestones ~= nil then
      table.insert(masterySkills, {tokenRef = i, skillID = item.skill, milestoneCount = milestones})
    end
  end
  table.sort(masterySkills, function(a, b)
                              if a['milestoneCount'] == b['milestoneCount'] then
                                return a['skillID'] < b['skillID']
                              else
                                return a['milestoneCount'] > b['milestoneCount']
                              end
                            end)
 
  -- Generate output table
  local resultPart = {}
  local CCI = Items.getItem('Clue Chasers Insignia')
  local CCIIcon = Icons.Icon({'Clue Chasers Insignia', type='item', notext=true})
  if CCI == nil then return '' end
 
  table.insert(resultPart, '{| class="wikitable sortable"')
  table.insert(resultPart, '\r\n!rowspan="2"|Token!!rowspan="2"|Skill!!colspan="2"|Approximate Mastery Token Chance')
  table.insert(resultPart, '\r\n|-\r\n!Without ' .. CCIIcon .. '!!With ' .. CCIIcon)
 
  for i, m in ipairs(masterySkills) do
    local token = masteryTokens[m.tokenRef]
    local denom = math.floor(baseTokenChance / m['milestoneCount'])
    local denomCCI = math.floor(baseTokenChance / m['milestoneCount'] * (1 - CCI.increasedItemChance / 100))
 
    table.insert(resultPart, '\r\n|-')
    table.insert(resultPart, '\r\n|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
    table.insert(resultPart, '\r\n|' .. Icons.Icon({p.getSkillName(m['skillID']), type='skill'}))
    table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Shared.formatnum(denom))
    table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. denomCCI .. '"|1/' .. Shared.formatnum(denomCCI))
  end
  table.insert(resultPart, '\r\n|}')
 
  return table.concat(resultPart)
end
 
function p.getSkillUnlockCostTable()
  local returnPart = {}
  table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative Cost')


  local accCost = 0
table.insert(resultPart, '\r\n|-')
  for i, cost in ipairs(SkillData.SkillUnlockCosts) do
table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
    accCost = accCost + cost
table.insert(resultPart, '||[['..name..']]')
    table.insert(returnPart, '|-')
table.insert(resultPart, '||style ="text-align: right;"|'..logData.level)
    table.insert(returnPart, '|' .. i .. '||' .. Icons.GP(cost) .. '||' .. Icons.GP(accCost))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true))
  end
table.insert(resultPart, '||style ="text-align: right;"|'..logData.baseXP)
  table.insert(returnPart, '|}')
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.round(XPS, 2, 2))
table.insert(resultPart, '||style ="text-align: right;"|'..Shared.round(XP_BF, 2, 0))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.round(XPS_BF, 2, 2))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..logData.bonfireXPBonus..'"|'..logData.bonfireXPBonus..'%')
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireTime..'"|'..Shared.timeString(bonfireTime, true))
end


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


return p
return p

Revision as of 14:29, 18 April 2022

Data pulled from Module:Skills/data


--This module should avoid including skill specific functions which generate
--output for wiki pages, especially those which require() other modules. For
--these functions, consider using the appropriate module from the below list.

--Some skills have their own modules:
--Module:Magic for Magic
--Module:Prayer for Prayer
--Module:Skills/Agility for Agility
--Module:Skills/Summoning for Summoning
--Module:Skills/Gathering for Mining, Fishing, Woodcutting
--Module:Skills/Artisan for Smithing, Cooking, Herblore, etc.

--Also be aware of:
--Module:Navboxes for navigation boxes appearing near the bottom of pages

local p = {}

local ItemData = mw.loadData('Module:Items/data')
local SkillData = mw.loadData('Module:Skills/data')

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

local MasteryCheckpoints = {.1, .25, .5, .95}

-- Thieving
function p.getThievingNPC(npcName)
	local result = nil
	for i, npc in Shared.skpairs(SkillData.Thieving.NPCs) do
		if npc.name == npcName then
			result = Shared.clone(npc)
			break
		end
	end
	return result
end

function p.getThievingNPCArea(npc)
	if type(npc) == 'string' then
		npc = p.getThievingNPC(npc)
	end

	local result = nil
	for i, area in Shared.skpairs(SkillData.Thieving.Areas) do
		for j, npcID in pairs(area.npcs) do
			if npcID == npc.id then
				result = area
				break
			end
		end
	end
	return result
end

function p._getThievingNPCStat(npc, statName)
	local result = nil

	if statName == 'level' then
		result = Icons._SkillReq('Thieving', npc.level)
	elseif statName == 'maxHit' then
		result = npc.maxHit * 10
	elseif statName == 'area' then
		local area = p.getThievingNPCArea(npc)
		result = area.name
	else
		result = npc[statName]
	end

	if result == nil then
		result = ''
	end

	return result
end

function p.getThievingNPCStat(frame)
	local npcName = frame.args ~= nil and frame.args[1] or frame[1]
	local statName = frame.args ~= nil and frame.args[2] or frame[2]
	local npc = p.getThievingNPC(npcName)
	if npc == nil then
		return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
	end

	return p._getThievingNPCStat(npc, statName)
end

function p.getThievingSourcesForItem(itemID)
	local resultArray = {}

	local areaNPCs = {}

	--First check area unique drops
	--If an area drops the item, add all the NPC ids to the list so we can add them later
	if not result then
		for i, area in pairs(SkillData.Thieving.Areas) do
			for j, drop in pairs(area.uniqueDrops) do
				if drop.itemID == itemID then
					for k, npcID in pairs(area.npcs) do
						areaNPCs[npcID] = drop.qty
					end
					break
				end
			end
		end
	end

	--Now go through and get drop chances on each NPC if needed
	for i, npc in pairs(SkillData.Thieving.NPCs) do
		local totalWt = 0
		local dropWt = 0
		local dropQty = 0
		for j, drop in pairs(npc.lootTable) do
			totalWt = totalWt + drop[2]
			if drop[1] == itemID then
				dropWt = drop[2]
				dropQty = drop[3]
			end
		end
		if dropWt > 0 then
			table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = dropQty, wt = dropWt * SkillData.Thieving.ItemChance, totalWt = totalWt * 100, level = npc.level})
		end

		--Chance of -1 on unique drops is to indicate variable chance
		if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID == itemID then
			table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.qty, maxQty = npc.uniqueDrop.qty, wt = -1, totalWt = -1, level = npc.level})
		end

		if areaNPCs[npc.id] ~= nil then
			table.insert(resultArray, {npc = npc.name, minQty = areaNPCs[npc.id], maxQty = areaNPCs[npc.id], wt = SkillData.Thieving.AreaUniqueChance, totalWt = 100, level = npc.level})
		end
	end

	for i, drop in pairs(SkillData.Thieving.RareItems) do
		if drop.itemID == itemID then
			table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = 1})
		end
	end

	return resultArray
end

-- Astrology
function p.getConstellationByID(constID)
	return SkillData.Astrology.Constellations[constID]
end

function p.getConstellation(constName)
	for i, const in ipairs(SkillData.Astrology.Constellations) do
		if const.name == constName then
			return const
		end
	end
	return nil
end

function p.getConstellations(checkFunc)
	local result = {}
	for i, const in ipairs(SkillData.Astrology.Constellations) do
		if checkFunc(const) then
			table.insert(result, const)
		end
	end
	return result
end

-- For a given constellation cons and modifier value modValue, generates and returns
-- a table of modifiers, much like any other item/object elsewhere in the game.
-- includeStandard: true|false, determines whether standard modifiers are included
-- includeUnique: true|false, determines whether unique modifiers are included
-- isDistinct: true|false, if true, the returned list of modifiers is de-duplicated
-- asKeyValue: true|false, if true, returns key/value pairs like usual modifier objects
function p._buildAstrologyModifierArray(cons, modValue, includeStandard, includeUnique, isDistinct, asKeyValue)
	-- Temporary function to determine if the table already contains a given modifier
	local containsMod = function(modList, modNew)
			for i, modItem in ipairs(modList) do
				-- Check mod names & value data types both equal
				if modItem[1] == modNew[1] and type(modItem[2]) == type(modNew[2]) then
					if type(modItem[2]) == 'table' then
						if Shared.tablesEqual(modItem[2], modNew[2]) then
							return true
						end
					elseif modItem[2] == modNew[2] then
						return true
					end
				end
			end
			return false
		end

	local addToArray = function(modArray, modNew)
			if not isDistinct or (isDistinct and not containsMod(modArray, modNew)) then
				table.insert(modArray, modNew)
			end
		end

	local modTypes = {}
	if includeStandard then
		table.insert(modTypes, 'standardModifiers')
	end
	if includeUnique then
		table.insert(modTypes, 'uniqueModifiers')
	end

	local modArray = {}
	local isSkillMod = {}
	for _, modType in ipairs(modTypes) do
		for i, skillMods in ipairs(cons[modType]) do
			local skillID = cons.skills[i]
			if skillID ~= nil then
				for j, modName in ipairs(skillMods) do
					local modBaseName, modText, sign, isNegative, unsign, modBase = Constants.getModifierDetails(modName)
					-- Check if modifier varies by skill, and amend the modifier value accordingly
					local modVal = modValue
					if Shared.contains(modText, '{SV0}') then
						isSkillMod[modName] = true
						modVal = {skillID, modValue}
					end
					addToArray(modArray, {modName, modVal})
				end
			end
		end
	end

	if asKeyValue then
		local modArrayKV = {}
		for i, modDefn in ipairs(modArray) do
			local modName, modVal = modDefn[1], modDefn[2]
			local isSkill = isSkillMod[modName]
			if modArrayKV[modName] == nil then
				modArrayKV[modName] = (isSkill and { modVal } or modVal)
			elseif isSkill then
				table.insert(modArrayKV[modName], modVal)
			else
				modArrayKV[modName] = modArrayKV[modName] + modVal
			end
		end
		return modArrayKV
	else
		return modArray
	end
end

-- Mastery
function p.getMasteryUnlockTable(frame)
	local skillName = frame.args ~= nil and frame.args[1] or frame
	local skillID = Constants.getSkillID(skillName)
	if skillID == nil then
		return "ERROR: Failed to find a skill ID for "..skillName
	end

	local unlockTable = SkillData.MasteryUnlocks[skillID]
	if unlockTable == nil then
		return 'ERROR: Failed to find Mastery Unlock data for '..skillName
	end

	local result = '{|class="wikitable"\r\n!Level!!Unlock'
	for i, unlock in Shared.skpairs(unlockTable) do
		result = result..'\r\n|-'
		result = result..'\r\n|'..unlock.level..'||'..unlock.unlock
	end
	result = result..'\r\n|}'
	return result
end

function p.getMasteryCheckpointTable(frame)
	local skillName = frame.args ~= nil and frame.args[1] or frame
	local skillID = Constants.getSkillID(skillName)
	if skillID == nil then
		return "ERROR: Failed to find a skill ID for "..skillName
	end

	if SkillData.MasteryCheckpoints[skillID] == nil then
		return 'ERROR: Failed to find Mastery Unlock data for '..skillName
	end

	local bonuses = SkillData.MasteryCheckpoints[skillID].bonuses
	local totalPoolXP = SkillData.MasteryPoolXP[skillID + 1]

	local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
	for i, bonus in Shared.skpairs(bonuses) do
		result = result..'\r\n|-'
		result = result..'\r\n|'..(MasteryCheckpoints[i] * 100)..'%||'
		result = result..Shared.formatnum(totalPoolXP * MasteryCheckpoints[i])..' xp||'..bonus
	end
	result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
	result = result..'\r\n|'..Shared.formatnum(totalPoolXP)
	result = result..'\r\n|}'
	return result
end

function p.getMasteryTokenTable()
	local baseTokenChance = 18500
	local masterySkills = {}

	-- Find all mastery tokens
	local masteryTokens = Items.getItems(function(item) return item.isToken ~= nil and item.skill ~= nil and item.isToken end)
	for i, item in pairs(masteryTokens) do
		local milestones = SkillData.Milestones[item.skill + 1]
		if milestones ~= nil then
			table.insert(masterySkills, {tokenRef = i, skillID = item.skill, milestoneCount = milestones})
		end
	end
	table.sort(masterySkills, function(a, b)
									if a['milestoneCount'] == b['milestoneCount'] then
										return a['skillID'] < b['skillID']
									else
										return a['milestoneCount'] > b['milestoneCount']
									end
								end)

	-- Generate output table
	local resultPart = {}
	local CCI = Items.getItem('Clue Chasers Insignia')
	local CCIIcon = Icons.Icon({'Clue Chasers Insignia', type='item', notext=true})
	if CCI == nil then return '' end

	table.insert(resultPart, '{| class="wikitable sortable"')
	table.insert(resultPart, '\r\n!rowspan="2"|Token!!rowspan="2"|Skill!!colspan="2"|Approximate Mastery Token Chance')
	table.insert(resultPart, '\r\n|-\r\n!Without ' .. CCIIcon .. '!!With ' .. CCIIcon)

	for i, m in ipairs(masterySkills) do
		local token = masteryTokens[m.tokenRef]
		local denom = math.floor(baseTokenChance / m['milestoneCount'])
		local denomCCI = Shared.round(baseTokenChance / (m['milestoneCount'] * (1 + CCI.increasedItemChance / 100)), 0, 0)

		table.insert(resultPart, '\r\n|-')
		table.insert(resultPart, '\r\n|style="text-align:center"|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
		table.insert(resultPart, '\r\n|' .. Icons.Icon({Constants.getSkillName(m['skillID']), type='skill'}))
		table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Shared.formatnum(denom))
		table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. denomCCI .. '"|1/' .. Shared.formatnum(denomCCI))
	end
	table.insert(resultPart, '\r\n|}')

	return table.concat(resultPart)
end

-- Skill unlock costs for Adventure game mode
function p.getSkillUnlockCostTable()
	local returnPart = {}
	table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative Cost')

	local accCost = 0
	for i, cost in ipairs(SkillData.SkillUnlockCosts) do
		accCost = accCost + cost
		table.insert(returnPart, '|-')
		table.insert(returnPart, '|' .. i .. '||' .. Icons.GP(cost) .. '||' .. Icons.GP(accCost))
	end
	table.insert(returnPart, '|}')

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

-- Accepts 1 parameter, being either:
--  'Smelting', for which a table of all bars is generated, or
--  A bar or tier name, which if valid generates a table of all smithing recipes using that bar/tier
function p.getSmithingTable(frame)
	local tableType = frame.args ~= nil and frame.args[1] or frame
	tableType = Shared.splitString(tableType, ' ')[1]
	-- Translates Smithing category names to Smithing recipe data categories
	local categoryMap = {
		['Smelting'] = 0,
		['Bronze'] = 1,
		['Iron'] = 2,
		['Steel'] = 3,
		['Mithril'] = 4,
		['Adamant'] = 5,
		['Adamantite'] = 5,
		['Rune'] = 6,
		['Runite'] = 6,
		['Dragon'] = 7,
		['Dragonite'] = 7
	}
	local categoryID = categoryMap[tableType]
	if categoryID == nil then
		return 'ERROR: Invalid Smithing category: "' .. tableType .. '"[[Category:Pages with script errors]]'
	end

	-- Build a list of recipes to be included, and a list of bars while we're at it
	-- The bar list will be used later for value/bar calculations
	local recipeList, barIDList = {}, {}
	for i, recipe in ipairs(SkillData.Smithing.Recipes) do
		if recipe.category == categoryID then
			local recipeItem = Items.getItemByID(recipe.itemID)
			if recipeItem ~= nil then
				table.insert(recipeList, { id = i, level = recipe.level, itemName = recipeItem.name, itemValue = recipeItem.sellsFor })
			end
		elseif recipe.category == 0 then
			barIDList[recipe.itemID] = true
		end
	end

	-- Generate output table
	local resultPart = {}
	table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|-class="headerRow-0"')
	table.insert(resultPart, '\r\n!Item!!Name!!'..Icons.Icon({'Smithing', type='skill', notext=true})..' Level!!XP!!Value!!Ingredients')
	--Adding value/bar for things other than smelting
	if categoryID > 0 then
		table.insert(resultPart, '!!Value/Bar')
	end

	table.sort(recipeList, function(a, b)
			if a.level ~= b.level then
				return a.level < b.level
			else
				return a.itemName < b.itemName
			end
		end)

	for i, recipeDef in ipairs(recipeList) do
		local recipe = SkillData.Smithing.Recipes[recipeDef.id]
		local totalValue = recipe.baseQuantity * recipeDef.itemValue
		-- Determine the bar quantity & build the recipe cost string
		local barQty, costString = 0, {}
		for j, itemCost in ipairs(recipe.itemCosts) do
			local costItem = Items.getItemByID(itemCost.id)
			if costItem ~= nil then
				table.insert(costString, Icons.Icon({costItem.name, type='item', qty=itemCost.qty, notext=true}))
			end
			if barIDList[itemCost.id] then
				barQty = barQty + itemCost.qty
			end
		end

		table.insert(resultPart, '\r\n|-')
		table.insert(resultPart, '\r\n| ' .. Icons.Icon({recipeDef.itemName, type='item', size=50, notext=true}))
		table.insert(resultPart, '\r\n| ')
		if recipe.baseQuantity > 1 then
			table.insert(resultPart, recipe.baseQuantity .. 'x ')
		end
		table.insert(resultPart, Icons.Icon({recipeDef.itemName, type='item', noicon=true}))
		table.insert(resultPart, '\r\n|data-sort-value="' .. recipe.level .. '"| ' .. Icons._SkillReq('Smithing', recipe.level))
		table.insert(resultPart, '\r\n|data-sort-value="' .. recipe.baseXP .. '"| ' .. Shared.formatnum(recipe.baseXP))
		table.insert(resultPart, '\r\n|data-sort-value="' .. totalValue .. '"| ' .. Icons.GP(recipeDef.itemValue))
		if recipe.baseQuantity > 1 then
			table.insert(resultPart, ' (x' .. recipe.baseQuantity .. ')')
		end
		table.insert(resultPart, '\r\n| ' .. table.concat(costString, ', '))
		if categoryID > 0 then
			local barVal, barValTxt = 0, 'N/A'
			if barQty > 0 then
				barVal = totalValue / barQty
				barTxt = Icons.GP(Shared.round(barVal, 1, 1))
			end
			table.insert(resultPart, '\r\n|data-sort-value="' .. barVal .. '"| ' .. barTxt)
		end
	end
	table.insert(resultPart, '\r\n|}')

	return table.concat(resultPart)
end

function p.getFiremakingTable(frame)
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|-class="headerRow-0"')
	table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level')
	table.insert(resultPart, '!!rowspan="2"|Burn Time!!colspan="2"|Without Bonfire!!colspan="2"|With Bonfire!!rowspan="2"|Bonfire Bonus!!rowspan="2"|Bonfire Time')
	table.insert(resultPart, '\r\n|-class="headerRow-1"')
	table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')

	for i, logData in Shared.skpairs(SkillData.Firemaking) do
		local logs = Items.getItemByID(logData.logID)
		local name = logs.name
		local burnTime = logData.baseInterval / 1000
		local bonfireTime = logData.baseBonfireInterval / 1000
		local XPS = logData.baseXP / burnTime
		local XP_BF = logData.baseXP * (1 + logData.bonfireXPBonus / 100)
		local XPS_BF = XP_BF / burnTime

		table.insert(resultPart, '\r\n|-')
		table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
		table.insert(resultPart, '||[['..name..']]')
		table.insert(resultPart, '||style ="text-align: right;"|'..logData.level)
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true))
		table.insert(resultPart, '||style ="text-align: right;"|'..logData.baseXP)
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.round(XPS, 2, 2))
		table.insert(resultPart, '||style ="text-align: right;"|'..Shared.round(XP_BF, 2, 0))
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.round(XPS_BF, 2, 2))
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..logData.bonfireXPBonus..'"|'..logData.bonfireXPBonus..'%')
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireTime..'"|'..Shared.timeString(bonfireTime, true))
	end

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

return p