Module:Skills: Difference between revisions

From Melvor Idle
m (getMasteryTokenTable: Amend CCI logic to match in-game)
(Use printError function)
(31 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 = {}
local ItemData = mw.loadData('Module:Items/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 GameData = require('Module:GameData')
local SkillData = GameData.skillData
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}
-- Thieving
 
function p.getThievingNPCByID(npcID)
function p.getSkillID(skillName)
return GameData.getEntityByID(SkillData.Thieving.npcs, npcID)
  for skName, ID in Shared.skpairs(Constants.skill) do
    if skName == skillName then
      return ID
    end
  end
  return nil
end
end


function p.getSkillName(skillID)
function p.getThievingNPC(npcName)
  for skName, ID in Shared.skpairs(Constants.skill) do
return GameData.getEntityByName(SkillData.Thieving.npcs, npcName)
    if ID == skillID then
      return skName
    end
  end
  return nil
end
end


function p.getThievingNPCByID(ID)
function p.getThievingNPCArea(npc)
  local result = Shared.clone(SkillData.Thieving[ID + 1])
for i, area in ipairs(SkillData.Thieving.areas) do
  if result ~= nil then
for j, npcID in ipairs(area.npcIDs) do
    result.id = ID
if npcID == npc.id then
  end
return area
  return result
end
end
end
end
end


function p.getThievingNPC(name)
function p._getThievingNPCStat(npc, statName)
  local result = nil
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


function p.getThievingNPCStat(frame)
if statName == 'level' then
  local args = frame.args ~= nil and frame.args or frame
result = Icons._SkillReq('Thieving', npc.level)
  local npcName = args[1]
elseif statName == 'maxHit' then
  local statName = args[2]
result = npc.maxHit * 10
  local npc = p.getThievingNPC(npcName)
elseif statName == 'area' then
  if npc == nil then
local area = p.getThievingNPCArea(npc)
    return 'ERROR: Failed to find Thieving NPC with name ' .. name .. '[[Category:Pages with script errors]]'
result = area.name
  end
else
 
result = npc[statName]
  return p._getThievingNPCStat(npc, statName)
end
end


function p._getThievingNPCStat(npc, stat)
if result == nil then
  local itemDropChance = 0.75
result = ''
  local result = npc[stat]
end
  -- 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
return result
end
end


function p._getLootTableValue(lootTable)
function p.getThievingNPCStat(frame)
  -- Calculates the average GP value of a given loot table
local npcName = frame.args ~= nil and frame.args[1] or frame[1]
  -- Expects lootTableIn to be in format {{itemID_1, itemWeight_1}, ..., {itemID_n, itemWeight_n}}
local statName = frame.args ~= nil and frame.args[2] or frame[2]
  if Shared.tableCount(lootTable) == 0 then
local npc = p.getThievingNPC(npcName)
    return 0
if npc == nil then
  end
return Shared.printError('Invalid Thieving NPC ' .. npcName)
end


  local totalWeight = 0
return p._getThievingNPCStat(npc, statName)
  for i, drop in pairs(lootTable) do
end
    totalWeight = totalWeight + drop[2]
  end
  if totalWeight == 0 then
    return 0
  end


  local avgValue = 0
function p.getThievingSourcesForItem(itemID)
  for i, drop in pairs(lootTable) do
local resultArray = {}
    local item = Items.getItemByID(drop[1])
local areaNPCs = {}
    if item ~= nil then
      avgValue = avgValue + item.sellsFor * (drop[2] / totalWeight)
    end
  end
 
  return avgValue
end


function p._formatLootTable(lootTableIn, chanceMultIn, asList)
--First check area unique drops
  -- Expects lootTableIn to be in format {{itemID_1, itemWeight_1}, ..., {itemID_n, itemWeight_n}}
--If an area drops the item, add all the NPC ids to the list so we can add them later
  if Shared.tableCount(lootTableIn) == 0 then
for i, area in pairs(SkillData.Thieving.areas) do
    return ''
for j, drop in pairs(area.uniqueDrops) do
  end
if drop.id == itemID then
for k, npcID in ipairs(area.npcIDs) do
areaNPCs[npcID] = drop.quantity
end
break
end
end
end


  local chanceMult = (chanceMultIn or 1) * 100
--Now go through and get drop chances on each NPC if needed
  local lootTable = Shared.clone(lootTableIn)
for i, npc in pairs(SkillData.Thieving.npcs) do
  -- Sort table from most to least common drop
local totalWt = 0
  table.sort(lootTable, function(a, b)
local dropWt = 0
                          if a[2] == b[2] then
local dropQty = { min = 0, max = 0 }
                            return a[1] < b[1]
for j, drop in ipairs(npc.lootTable) do
                          else
totalWt = totalWt + drop.weight
                            return a[2] > b[2]
if drop.itemID == itemID then
                          end
dropWt = drop.weight
                        end)
dropQty = { min = drop.minQuantity, max = drop.maxQuantity }
end
end
if dropWt > 0 then
table.insert(resultArray, {npc = npc.name, minQty = dropQty.min, maxQty = dropQty.max, wt = dropWt * SkillData.Thieving.itemChance, totalWt = totalWt * 100, level = npc.level, npcID = npc.id})
end


  local totalWeight = 0
--Chance of -1 on unique drops is to indicate variable chance
  for i, drop in pairs(lootTable) do
if npc.uniqueDrop ~= nil and npc.uniqueDrop.id == itemID then
    totalWeight = totalWeight + drop[2]
table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.quantity, maxQty = npc.uniqueDrop.quantity, wt = -1, totalWt = -1, level = npc.level, npcID = npc.id})
  end
end
  if totalWeight == 0 then
    return ''
  end


  -- Get the length (in characters) of the largest drop chance so that they can be right aligned
if areaNPCs[npc.id] ~= nil then
  -- [4/16/21]: Adding info for no drop
table.insert(resultArray, {npc = npc.name, minQty = areaNPCs[npc.id], maxQty = areaNPCs[npc.id], wt = SkillData.Thieving.baseAreaUniqueChance, totalWt = 100, level = npc.level, npcID = npc.id})
  local maxDropLen = math.max(string.len(Shared.round(100 - chanceMult, 2, 2)), string.len(Shared.round(lootTable[1][2] / totalWeight * chanceMult, 2, 2)))
end
end


  local returnPart = {}
for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
  -- Generate header
if drop.itemID == itemID then
  if asList then
if drop.npcs == nil then
    if chanceMult < 100 then
table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = 1, npcID = itemID})
      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')
else
    end
for j, npcID in ipairs(drop.npcs) do
  else
local npc = p.getThievingNPCByID(npcID)
    table.insert(returnPart, '{|class="wikitable sortable"\r\n!Item!!Price!!colspan="2"|Chance')
if npc ~= nil then
  end
table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = npc.level, npcID = npc.id})
  -- Generate row for each item
end
  for i, drop in pairs(lootTable) do
end
    local item, itemText, sellsFor, dropChance = Items.getItemByID(drop[1]), 'Unknown', 0, Shared.round(drop[2] / totalWeight * chanceMult, 2, 2)
end
    if item ~= nil then
end
      itemText, sellsFor = Icons.Icon({item.name, type='item'}), item.sellsFor
end
    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')
return resultArray
end
end


function p.getThievingNPCTable()
-- Astrology
  local returnPart = {}
function p.getConstellationByID(constID)
return GameData.getEntityByID(SkillData.Astrology.recipes, constID)
end


  -- Create table header
function p.getConstellation(constName)
  table.insert(returnPart, '{| class="wikitable sortable stickyHeader"')
return GameData.getEntityByName(SkillData.Astrology.recipes, constName)
  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>')
end
 
  -- Create row for each NPC
  for i, npc in Shared.skpairs(SkillData.Thieving) do
    local linkText = (npc.name ~= p._getThievingNPCStat(npc, 'pageName') and p._getThievingNPCStat(npc, 'pageName') .. '|' .. npc.name) or npc.name
    table.insert(returnPart, '|-\r\n|style="text-align: left;" |' .. Icons.Icon({npc.name, type='thieving', size=50, notext=true}))
    table.insert(returnPart, '|style="text-align: left;" |[[' .. linkText .. ']]')
    table.insert(returnPart, '|style="text-align: right;" |' .. p._getThievingNPCStat(npc, 'level'))
    table.insert(returnPart, '|style="text-align: right;" |' .. p._getThievingNPCStat(npc, 'xp'))
    table.insert(returnPart, '|style="text-align: right;" |' .. p._getThievingNPCStat(npc, 'maxHit'))
    table.insert(returnPart, '|style="text-align: right;" data-sort-value="' .. p._getThievingNPCStat(npc, 'maxCoins') .. '" |' .. Icons.GP(p._getThievingNPCStat(npc, 'maxCoins')))
    table.insert(returnPart, '|style="text-align: right;" data-sort-value="' .. p._getThievingNPCStat(npc, 'pickpocketValue') .. '" |' .. Icons.GP(p._getThievingNPCStat(npc, 'pickpocketValue')))
  end
  table.insert(returnPart, '|}')


  return table.concat(returnPart, '\r\n')
function p.getConstellations(checkFunc)
return GameData.getEntities(SkillData.Astrology.recipes, checkFunc)
end
end


function p.getThievingNavbox()
-- For a given constellation cons and modifier value modValue, generates and returns
  local returnPart = {}
-- 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


  -- Create table header
local addToArray = function(modArray, modNew)
  table.insert(returnPart, '{| class="wikitable" style="text-align:center; clear:both; margin:auto; margin-bottom:1em;"')
if not isDistinct or (isDistinct and not containsMod(modArray, modNew)) then
  table.insert(returnPart, '|-\r\n!' .. Icons.Icon({'Thieving', type='skill', notext=true}) .. '[[Thieving|Thieving Targets]]')
table.insert(modArray, modNew)
  table.insert(returnPart, '|-\r\n|')
end
 
end
  local npcList = {}
  -- Create row for each NPC
  for i, npc in Shared.skpairs(SkillData.Thieving) do
    local linkText = (npc.name ~= p._getThievingNPCStat(npc, 'pageName') and p._getThievingNPCStat(npc, 'pageName') .. '|' .. npc.name) or npc.name
    table.insert(npcList, Icons.Icon({npc.name, type='thieving', notext=true}) .. ' [[' .. linkText .. ']]')
  end
  table.insert(returnPart, table.concat(npcList, ' • '))
  table.insert(returnPart, '|}')


  return table.concat(returnPart, '\r\n')
local modTypes = {}
end
if includeStandard then
table.insert(modTypes, 'standardModifiers')
end
if includeUnique then
table.insert(modTypes, 'uniqueModifiers')
end


function p.getMasteryUnlockTable(frame)
local modArray = {}
  local skillName = frame.args ~= nil and frame.args[1] or frame
local isSkillMod = {}
  local skillID = p.getSkillID(skillName)
--Adding a Group Number to hold together different bonuses from the same modifier [Falterfire 22/10/27]
  if skillID == nil then
local groupNum = 0
    return "ERROR: Failed to find a skill ID for "..skillName
  end


  local unlockTable = SkillData.MasteryUnlocks[skillID]
for _, modType in ipairs(modTypes) do
  if unlockTable == nil then
for i, modTypeData in ipairs(cons[modType]) do
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
groupNum = groupNum + 1
  end
local modVal = nil
if modValue ~= nil then
modVal = modValue
else
modVal = modTypeData.incrementValue * modTypeData.maxCount
end
for j, modifier in ipairs(modTypeData.modifiers) do
local modEntry = (modifier.skill ~= nil and { skillID = modifier.skill, value = modVal }) or modVal
addToArray(modArray, {modifier.key, modEntry, group = groupNum})
end
end
end


  local result = '{|class="wikitable"\r\n!Level!!Unlock'
if asKeyValue then
  for i, unlock in Shared.skpairs(unlockTable) do
local modArrayKV = {}
    result = result..'\r\n|-'
for i, modDefn in ipairs(modArray) do
    result = result..'\r\n|'..unlock.level..'||'..unlock.unlock
local modName, modVal = modDefn[1], modDefn[2]
  end
local isSkill = type(modVal) == 'table' and modVal.skillID ~= nil
  result = result..'\r\n|}'
if modArrayKV[modName] == nil then
  return result
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


function p.getMasteryCheckpointTable(frame)
-- Mastery
  local skillName = frame.args ~= nil and frame.args[1] or frame
function p.getMasteryUnlockTable(frame)
  local skillID = p.getSkillID(skillName)
local skillName = frame.args ~= nil and frame.args[1] or frame
  if skillID == nil then
local skillID = Constants.getSkillID(skillName)
    return "ERROR: Failed to find a skill ID for "..skillName
if skillID == nil then
  end
return Shared.printError('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 _, localSkillID = GameData.getLocalID(skillID)
  local totalPoolXP = SkillData.MasteryPoolXP[skillID + 1]
-- Clone so that we can sort by level
local unlockTable = Shared.clone(SkillData[localSkillID].masteryLevelUnlocks)
if unlockTable == nil then
return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
end
table.sort(unlockTable, function(a, b) return (a.level == b.level and a.descriptionID < b.descriptionID) or a.level < b.level end)


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


function p._getFarmingTable(category)
function p.getMasteryCheckpointTable(frame)
  local seedList = {}
local skillName = frame.args ~= nil and frame.args[1] or frame
  if category == 'Allotment' or category == 'Herb' or category == 'Tree' then
local skillID = Constants.getSkillID(skillName)
    seedList = Items.getItems(function(item) return item.tier == category end)
if skillID == nil then
  else
return Shared.printError('Failed to find a skill ID for ' .. skillName)
    return 'ERROR: Invalid farming category. Please choose Allotment, Herb, or Tree'
end
  end


  local result = '{|class="wikitable sortable stickyHeader"'
local _, localSkillID = GameData.getLocalID(skillID)
  result = result..'\r\n|- class="headerRow-0"'
local checkpoints = SkillData[localSkillID].masteryCheckpoints
  result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
if checkpoints == nil then
  result = result..'!!XP!!Growth Time!!Seed Value'
return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
  if category == 'Allotment' then
end
    result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
  elseif category == 'Herb' then
    result = result..'!!colspan="2"|Herb!!Herb Value'
  elseif category == 'Tree' then
    result = result..'!!colspan="2"|Logs!!Log Value'
  end
  result = result..'!!Seed Sources'
 
  table.sort(seedList, function(a, b) return a.farmingLevel < b.farmingLevel end)


  for i, seed in pairs(seedList) do
local totalPoolXP = SkillData[localSkillID].baseMasteryPoolCap
    result = result..'\r\n|-'
local checkpointPct = GameData.rawData.masteryCheckpoints
    result = result..'\r\n|'..Icons.Icon({seed.name, type='item', size='50', notext=true})..'||[['..seed.name..']]'
local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
    result = result..'||'..seed.farmingLevel..'||'..Shared.formatnum(seed.farmingXP)
for i, checkpointDesc in ipairs(checkpoints) do
    result = result..'||data-sort-value="'..seed.timeToGrow..'"|'..Shared.timeString(seed.timeToGrow, true)
result = result..'\r\n|-'
    result = result..'||data-sort-value="'..seed.sellsFor..'"|'..Icons.GP(seed.sellsFor)
result = result..'\r\n|'..checkpointPct[i]..'%||'
 
result = result..Shared.formatnum(math.floor(totalPoolXP * checkpointPct[i] / 100))..' xp||'..checkpointDesc
    local crop = Items.getItemByID(seed.grownItemID)
end
    result = result..'||'..Icons.Icon({crop.name, type='item', size='50', notext=true})..'||[['..crop.name..']]'
result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
    if category == 'Allotment' then
result = result..'\r\n|'..Shared.formatnum(totalPoolXP)
      result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..(crop.healsFor * 10)
result = result..'\r\n|}'
    end
return result
    result = result..'||data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
    result = result..'||'..ItemSourceTables._getItemSources(seed)
  end
 
  result = result..'\r\n|}'
  return result
end
end


function p.getFarmingTable(frame)
function p.getMasteryTokenTable()
  local category = frame.args ~= nil and frame.args[1] or frame
local baseTokenChance = 18500
local masterySkills = {}
local CCI = Items.getItemByID('melvorD:Clue_Chasers_Insignia')
if CCI == nil then return '' end


  return p._getFarmingTable(category)
-- Build table of mastery skills
end
for skillLocalID, skill in pairs(SkillData) do
if skill.masteryTokenID ~= nil then
table.insert(masterySkills, skill)
end
end
table.sort(masterySkills,
function(a, b)
if a.milestoneCount == b.milestoneCount then
return a.name < b.name
else
return a.milestoneCount > b.milestoneCount
end
end)


function p.getFarmingFoodTable(frame)
-- Generate output table
  local result = '{| class="wikitable sortable stickyHeader"'
local resultPart = {}
  result = result..'\r\n|- class="headerRow-0"'
local CCIIcon = Icons.Icon({CCI.name, type='item', notext=true})
  result = result..'\r\n!colspan="2"|Crop!!'..Icons.Icon({"Farming", type="skill", notext=true})..' Level'
  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)
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, item in Shared.skpairs(itemArray) do
for i, skill in ipairs(masterySkills) do
    local crop = Items.getItemByID(item.grownItemID)
local token = Items.getItemByID(skill.masteryTokenID)
    if crop.healsFor ~= nil and crop.healsFor > 0 then
local denom = math.floor(baseTokenChance / skill.milestoneCount)
      result = result..'\r\n|-'
local denomCCI = Shared.round(baseTokenChance / (skill.milestoneCount * (1 + CCI.modifiers.increasedOffItemChance / 100)), 0, 0)
      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|}'
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({skill.name, 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 result
return table.concat(resultPart)
end
end


function p.getFarmingPlotTable(frame)
-- Skill unlock costs for Adventure game mode
  local areaName = frame.args ~= nil and frame.args[1] or frame
function p.getSkillUnlockCostTable()
  local patches = nil
local advMode = GameData.getEntityByID('gamemodes', 'melvorF:Adventure')
  for i, area in Shared.skpairs(SkillData.Farming.Patches) do
if advMode ~= nil then
    if area.areaName == areaName then
local unlockCount = Shared.tableCount(GameData.skillData) - Shared.tableCount(advMode.startingSkills)
      patches = area.patches
local costLength = Shared.tableCount(advMode.skillUnlockCost)
      break
local returnPart = {}
    end
table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative Cost')
  end
  if patches == nil then
    return "ERROR: Invalid area name.[[Category:Pages with script errors"
  end
 
  local result = '{|class="wikitable"'
  result = result..'\r\n!Plot!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level!!Cost'


  for i, patch in Shared.skpairs(patches) do
local accCost = 0
    result = result..'\r\n|-\r\n|'..i
for i = 1, unlockCount, 1 do
    result = result..'||style="text-align:right;" data-sort-value="0"|'..patch.level
local cost = advMode.skillUnlockCost[math.min(i, costLength)]
    if patch.cost == 0 then
accCost = accCost + cost
      result = result..'||Free'
table.insert(returnPart, '|-')
    else
table.insert(returnPart, '|' .. i .. '||' .. Icons.GP(cost) .. '||' .. Icons.GP(accCost))
      result = result..'||style="text-align:right;" data-sort-value="'..patch.cost..'"|'..Icons.GP(patch.cost)
end
    end
table.insert(returnPart, '|}')
  end


  result = result..'\r\n|}'
return table.concat(returnPart, '\r\n')
  return result
end
end
end


-- Accepts 1 parameter, being either:
--  'Bars', 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)
function p.getSmithingTable(frame)
  local tableType = frame.args ~= nil and frame.args[1] or frame
local tableType = frame.args ~= nil and frame.args[1] or frame
  local bar = nil
  if tableType ~= 'Smelting' then
    bar = Items.getItem(tableType)
    if bar == nil then
      return 'ERROR: Could not find an item named '..tableType..' to build a smithing table with'
    elseif bar.type ~= 'Bar' then
      return 'ERROR: '..tableType.." is not a bar and thus can't be used for smithing"
    end
  end


  local smithList = {}
-- Has a valid category been passed (by name)?
  for i, item in pairs(ItemData.Items) do
local category = GameData.getEntityByName(SkillData.Smithing.categories, tableType)
    if item.smithingLevel ~= nil then
if category == nil then
      if tableType == 'Smelting' then
return Shared.printError('Invalid Smithing category: "' .. tableType .. '"')
        if item.type == 'Bar' then
end
          table.insert(smithList, item)
        end
      else
        for j, req in pairs(item.smithReq) do
          if req.id == bar.id then
            table.insert(smithList, item)
          end
        end
      end
    end
  end


  local result = '{|class="wikitable sortable stickyHeader"'
-- Build a list of recipes to be included, and a list of bars while we're at it
  result = result..'\r\n|-class="headerRow-0"'
-- The bar list will be used later for value/bar calculations
  result = result..'\r\n!Item!!Name!!'..Icons.Icon({'Smithing', type='skill', notext=true})..' Level!!XP!!Value!!Ingredients'
local recipeList, barIDList = {}, {}
  --Adding value/bar for things other than smelting
for i, recipe in ipairs(SkillData.Smithing.recipes) do
  if bar ~= nil then result = result..'!!Value/Bar' end
if recipe.categoryID == category.id then
local recipeItem = Items.getItemByID(recipe.productID)
if recipeItem ~= nil then
table.insert(recipeList, { id = i, level = recipe.level, itemName = recipeItem.name, itemValue = recipeItem.sellsFor, expIcon = Icons.getExpansionIcon(recipeItem.id) })
end
elseif recipe.categoryID == 'melvorD:Bars' then
barIDList[recipe.productID] = true
end
end


  table.sort(smithList, function(a, b)
-- Generate output table
                          if a.smithingLevel ~= b.smithingLevel then
local resultPart = {}
                            return a.smithingLevel < b.smithingLevel
table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
                          else
table.insert(resultPart, '\r\n|-class="headerRow-0"')
                            return a.name < b.name
table.insert(resultPart, '\r\n!Item!!Name!!'..Icons.Icon({'Smithing', type='skill', notext=true})..' Level!!XP!!Value!!Ingredients')
                          end end)
--Adding value/bar for things other than smelting
  for i, item in Shared.skpairs(smithList) do
if category.id ~= 'melvorD:Bars' then
    result = result..'\r\n|-'
table.insert(resultPart, '!!Value/Bar')
    result = result..'\r\n|'..Icons.Icon({item.name, type='item', size='50', notext=true})..'||'
end
    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|}'
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.getFiremakingTable(frame)
for i, recipeDef in ipairs(recipeList) do
  local resultPart = {}
local recipe = SkillData.Smithing.recipes[recipeDef.id]
  table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
local totalValue = recipe.baseQuantity * recipeDef.itemValue
  table.insert(resultPart, '\r\n|-class="headerRow-0"')
-- Determine the bar quantity & build the recipe cost string
  table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level')
local barQty, costString = 0, {}
  table.insert(resultPart, '!!rowspan="2"|Burn Time!!colspan="2"|Without Bonfire!!colspan="2"|With Bonfire!!rowspan="2"|Bonfire Bonus!!rowspan="2"|Bonfire Time')
for j, itemCost in ipairs(recipe.itemCosts) do
  table.insert(resultPart, '\r\n|-class="headerRow-1"')
local costItem = Items.getItemByID(itemCost.id)
  table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')
if costItem ~= nil then
table.insert(costString, Icons.Icon({costItem.name, type='item', qty=itemCost.quantity, notext=true}))
end
if barIDList[itemCost.id] then
barQty = barQty + itemCost.quantity
end
end


  for i, logData in Shared.skpairs(SkillData.Firemaking) do
table.insert(resultPart, '\r\n|-')
    local name = Shared.titleCase(logData.type..' Logs')
table.insert(resultPart, '\r\n| ' .. Icons.Icon({recipeDef.itemName, type='item', size=50, notext=true}))
    local burnTime = logData.interval / 1000
table.insert(resultPart, '\r\n| ')
    local XPS = logData.xp / burnTime
table.insert(resultPart, recipeDef.expIcon)
    local XP_BF = logData.xp * (1 + logData.bonfireBonus / 100)
if recipe.baseQuantity > 1 then
    local XPS_BF = XP_BF / burnTime
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.baseExperience .. '"| ' .. Shared.formatnum(recipe.baseExperience))
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 category.id ~= 'melvorD:Bars' then
local barVal, barValTxt = 0, 'N/A'
if barQty > 0 then
barVal = totalValue / barQty
barValTxt = Icons.GP(Shared.round(barVal, 1, 1))
end
table.insert(resultPart, '\r\n|data-sort-value="' .. barVal .. '"| ' .. barValTxt)
end
end
table.insert(resultPart, '\r\n|}')


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


function p.getMasteryTokenTable()
function p.getFiremakingTable(frame)
  local baseTokenChance = 18500
local resultPart = {}
  local masterySkills = {}
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
 
table.insert(resultPart, '\r\n|-class="headerRow-0"')
  -- Find all mastery tokens
table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level')
  local masteryTokens = Items.getItems(function(item) return item.isToken ~= nil and item.skill ~= nil and item.isToken end)
table.insert(resultPart, '!!rowspan="2"|Burn Time!!colspan="2"|Without Bonfire!!colspan="2"|With Bonfire!!rowspan="2"|Bonfire Bonus!!rowspan="2"|Bonfire Time')
  for i, item in pairs(masteryTokens) do
table.insert(resultPart, '\r\n|-class="headerRow-1"')
    local milestones = SkillData.Milestones[item.skill + 1]
table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')
    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"')
for i, logData in ipairs(SkillData.Firemaking.logs) do
  table.insert(resultPart, '\r\n!rowspan="2"|Token!!rowspan="2"|Skill!!colspan="2"|Approximate Mastery Token Chance')
local logs = Items.getItemByID(logData.logID)
  table.insert(resultPart, '\r\n|-\r\n!Without ' .. CCIIcon .. '!!With ' .. CCIIcon)
local name = logs.name
local burnTime = logData.baseInterval / 1000
local bonfireTime = logData.baseBonfireInterval / 1000
local XPS = logData.baseExperience / burnTime
local XP_BF = logData.baseExperience * (1 + logData.bonfireXPBonus / 100)
local XPS_BF = XP_BF / burnTime


  for i, m in pairs(masterySkills) do
table.insert(resultPart, '\r\n|-')
    local token = masteryTokens[m.tokenRef]
table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
    local denom = math.floor(baseTokenChance / m['milestoneCount'])
table.insert(resultPart, '||'..Icons.getExpansionIcon(logs.id)..Icons.Icon({name, type='item', noicon=true}))
    local denomCCI = math.floor(baseTokenChance / m['milestoneCount'] * (1 - CCI.increasedItemChance / 100))
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.baseExperience)
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|-')
table.insert(resultPart, '\r\n|}')
    table.insert(resultPart, '\r\n|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
return table.concat(resultPart)
    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
end


return p
return p

Revision as of 12:44, 27 December 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 Shared = require('Module:Shared')
local Constants = require('Module:Constants')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Items = require('Module:Items')
local Icons = require('Module:Icons')

-- Thieving
function p.getThievingNPCByID(npcID)
	return GameData.getEntityByID(SkillData.Thieving.npcs, npcID)
end

function p.getThievingNPC(npcName)
	return GameData.getEntityByName(SkillData.Thieving.npcs, npcName)
end

function p.getThievingNPCArea(npc)
	for i, area in ipairs(SkillData.Thieving.areas) do
		for j, npcID in ipairs(area.npcIDs) do
			if npcID == npc.id then
				return area
			end
		end
	end
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 Shared.printError('Invalid Thieving NPC ' .. npcName)
	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
	for i, area in pairs(SkillData.Thieving.areas) do
		for j, drop in pairs(area.uniqueDrops) do
			if drop.id == itemID then
				for k, npcID in ipairs(area.npcIDs) do
					areaNPCs[npcID] = drop.quantity
				end
				break
			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 = { min = 0, max = 0 }
		for j, drop in ipairs(npc.lootTable) do
			totalWt = totalWt + drop.weight
			if drop.itemID == itemID then
				dropWt = drop.weight
				dropQty = { min = drop.minQuantity, max = drop.maxQuantity }
			end
		end
		if dropWt > 0 then
			table.insert(resultArray, {npc = npc.name, minQty = dropQty.min, maxQty = dropQty.max, wt = dropWt * SkillData.Thieving.itemChance, totalWt = totalWt * 100, level = npc.level, npcID = npc.id})
		end

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

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

	for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
		if drop.itemID == itemID then
			if drop.npcs == nil then
				table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = 1, npcID = itemID})
			else
				for j, npcID in ipairs(drop.npcs) do
					local npc = p.getThievingNPCByID(npcID)
					if npc ~= nil then
						table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = npc.level, npcID = npc.id})
					end
				end
			end
		end
	end

	return resultArray
end

-- Astrology
function p.getConstellationByID(constID)
	return GameData.getEntityByID(SkillData.Astrology.recipes, constID)
end

function p.getConstellation(constName)
	return GameData.getEntityByName(SkillData.Astrology.recipes, constName)
end

function p.getConstellations(checkFunc)
	return GameData.getEntities(SkillData.Astrology.recipes, checkFunc)
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 = {}
	--Adding a Group Number to hold together different bonuses from the same modifier [Falterfire 22/10/27]
	local groupNum = 0

	for _, modType in ipairs(modTypes) do
		for i, modTypeData in ipairs(cons[modType]) do
			groupNum = groupNum + 1
			local modVal = nil
			if modValue ~= nil then
				modVal = modValue
			else
				modVal = modTypeData.incrementValue * modTypeData.maxCount
			end
			for j, modifier in ipairs(modTypeData.modifiers) do
				local modEntry = (modifier.skill ~= nil and { skillID = modifier.skill, value = modVal }) or modVal
				addToArray(modArray, {modifier.key, modEntry, group = groupNum})
			end
		end
	end

	if asKeyValue then
		local modArrayKV = {}
		for i, modDefn in ipairs(modArray) do
			local modName, modVal = modDefn[1], modDefn[2]
			local isSkill = type(modVal) == 'table' and modVal.skillID ~= nil
			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 Shared.printError('Failed to find a skill ID for ' .. skillName)
	end

	local _, localSkillID = GameData.getLocalID(skillID)
	-- Clone so that we can sort by level
	local unlockTable = Shared.clone(SkillData[localSkillID].masteryLevelUnlocks)
	if unlockTable == nil then
		return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
	end
	table.sort(unlockTable, function(a, b) return (a.level == b.level and a.descriptionID < b.descriptionID) or a.level < b.level end)

	local result = '{|class="wikitable"\r\n!Level!!Unlock'
	for i, unlock in ipairs(unlockTable) do
		result = result..'\r\n|-'
		result = result..'\r\n|'..unlock.level..'||'..unlock.description
	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 Shared.printError('Failed to find a skill ID for ' .. skillName)
	end

	local _, localSkillID = GameData.getLocalID(skillID)
	local checkpoints = SkillData[localSkillID].masteryCheckpoints
	if checkpoints == nil then
		return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
	end

	local totalPoolXP = SkillData[localSkillID].baseMasteryPoolCap
	local checkpointPct = GameData.rawData.masteryCheckpoints
	local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
	for i, checkpointDesc in ipairs(checkpoints) do
		result = result..'\r\n|-'
		result = result..'\r\n|'..checkpointPct[i]..'%||'
		result = result..Shared.formatnum(math.floor(totalPoolXP * checkpointPct[i] / 100))..' xp||'..checkpointDesc
	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 = {}
	local CCI = Items.getItemByID('melvorD:Clue_Chasers_Insignia')
	if CCI == nil then return '' end

	-- Build table of mastery skills
	for skillLocalID, skill in pairs(SkillData) do
		if skill.masteryTokenID ~= nil then
			table.insert(masterySkills, skill)
		end
	end
	table.sort(masterySkills,
		function(a, b)
			if a.milestoneCount == b.milestoneCount then
				return a.name < b.name
			else
				return a.milestoneCount > b.milestoneCount
			end
		end)

	-- Generate output table
	local resultPart = {}
	local CCIIcon = Icons.Icon({CCI.name, type='item', notext=true})

	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, skill in ipairs(masterySkills) do
		local token = Items.getItemByID(skill.masteryTokenID)
		local denom = math.floor(baseTokenChance / skill.milestoneCount)
		local denomCCI = Shared.round(baseTokenChance / (skill.milestoneCount * (1 + CCI.modifiers.increasedOffItemChance / 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({skill.name, 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 advMode = GameData.getEntityByID('gamemodes', 'melvorF:Adventure')
	if advMode ~= nil then
		local unlockCount = Shared.tableCount(GameData.skillData) - Shared.tableCount(advMode.startingSkills)
		local costLength = Shared.tableCount(advMode.skillUnlockCost)
		local returnPart = {}
		table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative Cost')

		local accCost = 0
		for i = 1, unlockCount, 1 do
		local cost = advMode.skillUnlockCost[math.min(i, costLength)]
			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
end

-- Accepts 1 parameter, being either:
--  'Bars', 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

	-- Has a valid category been passed (by name)?
	local category = GameData.getEntityByName(SkillData.Smithing.categories, tableType)
	if category == nil then
		return Shared.printError('Invalid Smithing category: "' .. tableType .. '"')
	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.categoryID == category.id then
			local recipeItem = Items.getItemByID(recipe.productID)
			if recipeItem ~= nil then
				table.insert(recipeList, { id = i, level = recipe.level, itemName = recipeItem.name, itemValue = recipeItem.sellsFor, expIcon = Icons.getExpansionIcon(recipeItem.id) })
			end
		elseif recipe.categoryID == 'melvorD:Bars' then
			barIDList[recipe.productID] = 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 category.id ~= 'melvorD:Bars' 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.quantity, notext=true}))
			end
			if barIDList[itemCost.id] then
				barQty = barQty + itemCost.quantity
			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| ')
		table.insert(resultPart, recipeDef.expIcon)
		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.baseExperience .. '"| ' .. Shared.formatnum(recipe.baseExperience))
		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 category.id ~= 'melvorD:Bars' then
			local barVal, barValTxt = 0, 'N/A'
			if barQty > 0 then
				barVal = totalValue / barQty
				barValTxt = Icons.GP(Shared.round(barVal, 1, 1))
			end
			table.insert(resultPart, '\r\n|data-sort-value="' .. barVal .. '"| ' .. barValTxt)
		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 ipairs(SkillData.Firemaking.logs) do
		local logs = Items.getItemByID(logData.logID)
		local name = logs.name
		local burnTime = logData.baseInterval / 1000
		local bonfireTime = logData.baseBonfireInterval / 1000
		local XPS = logData.baseExperience / burnTime
		local XP_BF = logData.baseExperience * (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, '||'..Icons.getExpansionIcon(logs.id)..Icons.Icon({name, type='item', noicon=true}))
		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.baseExperience)
		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