Module:Skills: Difference between revisions

From Melvor Idle
(_formatLootTable: Re-order columns for table output & include total row)
(Adjusted the appearance of the Lesser Relics table)
 
(49 intermediate revisions by 4 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}
-- Given a skill ID, returns the key for that skill's recipe data.
-- If the skill has no recipes (e.g. is a combat skill) then the
-- return value is nil
function p.getSkillRecipeKey(skillID)
-- Convert skillID to local ID if not already
local ns, localSkillID = GameData.getLocalID(skillID)
local recipeIDs = {
["Woodcutting"] = 'trees',
["Fishing"] = 'fish',
["Firemaking"] = 'logs',
["Mining"] = 'rockData',
["Thieving"] = 'npcs',
["Agility"] = 'obstacles',
["Cooking"] = 'recipes',
["Smithing"] = 'recipes',
["Farming"] = 'recipes',
["Summoning"] = 'recipes',
["Fletching"] = 'recipes',
["Crafting"] = 'recipes',
["Runecrafting"] = 'recipes',
["Herblore"] = 'recipes',
["Astrology"] = 'recipes'
}
return recipeIDs[localSkillID]
end


function p.getSkillID(skillName)
-- Given a skill ID & recipe, returns the skill level requirement for
  for skName, ID in Shared.skpairs(Constants.skill) do
-- that recipe. If the level could not be determined, then the return
    if skName == skillName then
-- value is nil
      return ID
function p.getRecipeLevel(skillID, recipe)
    end
-- Convert skillID to local ID if not already
  end
local ns, localSkillID = GameData.getLocalID(skillID)
  return nil
if localSkillID == 'Agility' then
-- For Agility, level is derived from obstacle category
if recipe.category ~= nil then
-- Obstacle
return SkillData.Agility.obstacleUnlockLevels[recipe.category+1]
else
-- Pillar
local nsR, localRecipeID = GameData.getLocalID(recipe.id)
if localRecipeID ~= nil then
if string.find(localRecipeID, '^Pillar') ~= nil then
return 99
elseif string.find(localRecipeID, '^ElitePillar') ~= nil then
return 120
end
end
end
else
-- For all other skills, the recipe should have a level property
return recipe.level
end
end
end


function p.getSkillName(skillID)
-- Thieving
  for skName, ID in Shared.skpairs(Constants.skill) do
function p.getThievingNPCByID(npcID)
    if ID == skillID then
return GameData.getEntityByID(SkillData.Thieving.npcs, npcID)
      return skName
    end
  end
  return nil
end
end


function p.getThievingNPCByID(ID)
function p.getThievingNPC(npcName)
  local result = Shared.clone(SkillData.Thieving[ID + 1])
return GameData.getEntityByName(SkillData.Thieving.npcs, npcName)
  if result ~= nil then
    result.id = ID
  end
  return result
end
end


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


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


function p._getThievingNPCStat(npc, stat)
function p.getThievingNPCStat(frame)
  local itemDropChance = 0.75
local npcName = frame.args ~= nil and frame.args[1] or frame[1]
  local result = npc[stat]
local statName = frame.args ~= nil and frame.args[2] or frame[2]
  -- Overrides below
local npc = p.getThievingNPC(npcName)
  if stat == 'maxHit' then
if npc == nil then
    result = result * 10
return Shared.printError('Invalid Thieving NPC ' .. npcName)
  elseif stat == 'lootList' then
end
    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 p._getThievingNPCStat(npc, statName)
end
end


function p._getLootTableValue(lootTable)
function p.getThievingSourcesForItem(itemID)
  -- Calculates the average GP value of a given loot table
local resultArray = {}
  -- Expects lootTableIn to be in format {{itemID_1, itemWeight_1}, ..., {itemID_n, itemWeight_n}}
local areaNPCs = {}
  if Shared.tableCount(lootTable) == 0 then
 
    return 0
--First check area unique drops
  end
--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] = { qty = drop.quantity, area = area }
end
break
end
end
end


  local totalWeight = 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
    totalWeight = totalWeight + drop[2]
local totalWt = 0
  end
local dropWt = 0
  if totalWeight == 0 then
local dropQty = { min = 0, max = 0 }
    return 0
for j, drop in ipairs(npc.lootTable) do
  end
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, type = 'npc'})
end


  local avgValue = 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
    local item = Items.getItemByID(drop[1])
table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.quantity, maxQty = npc.uniqueDrop.quantity, wt = -1, totalWt = -1, level = npc.level, npcID = npc.id, type = 'npcUnique'})
    if item ~= nil then
end
      avgValue = avgValue + item.sellsFor * (drop[2] / totalWeight)
    end
  end
 
  return avgValue
end


function p._formatLootTable(lootTableIn, chanceMultIn, asList)
local areaNPC = areaNPCs[npc.id]
  -- Expects lootTableIn to be in format {{itemID_1, itemWeight_1}, ..., {itemID_n, itemWeight_n}}
if areaNPC ~= nil then
  if Shared.tableCount(lootTableIn) == 0 then
table.insert(resultArray, {npc = npc.name, minQty = areaNPC.qty, maxQty = areaNPC.qty, wt = SkillData.Thieving.baseAreaUniqueChance, totalWt = 100, level = npc.level, npcID = npc.id, area = areaNPC.area, type = 'areaUnique'})
    return ''
end
  end
end


  local chanceMult = (chanceMultIn or 1) * 100
for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
  local lootTable = Shared.clone(lootTableIn)
if drop.itemID == itemID then
  -- Sort table from most to least common drop
if drop.npcs == nil then
  table.sort(lootTable, function(a, b)
table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = 1, npcID = itemID, type = 'generalRare'})
                          if a[2] == b[2] then
else
                            return a[1] < b[1]
for j, npcID in ipairs(drop.npcs) do
                          else
local npc = p.getThievingNPCByID(npcID)
                            return a[2] > b[2]
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, type = 'generalRare'})
                        end)
end
end
end
end
end


  local totalWeight = 0
return resultArray
  for i, drop in pairs(lootTable) do
end
    totalWeight = totalWeight + drop[2]
  end
  if totalWeight == 0 then
    return ''
  end


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


  local returnPart = {}
function p.getConstellation(constName)
  -- Generate header
return GameData.getEntityByName(SkillData.Astrology.recipes, constName)
  if asList then
end
    if chanceMult < 100 then
      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.getConstellations(checkFunc)
return GameData.getEntities(SkillData.Astrology.recipes, checkFunc)
end
end


function p.getThievingNPCTable()
-- 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 sortable stickyHeader"')
if not isDistinct or (isDistinct and not containsMod(modArray, modNew)) then
  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>')
table.insert(modArray, modNew)
 
end
  -- Create row for each NPC
end
  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')
local modTypes = {}
end
if includeStandard then
table.insert(modTypes, 'standardModifiers')
end
if includeUnique then
table.insert(modTypes, 'uniqueModifiers')
end
local masteryReq = {
['standardModifiers'] = { 1, 40, 80 },
['uniqueModifiers'] = { 20, 60, 99 }
}


function p.getThievingNavbox()
local modArray = {}
  local returnPart = {}
local isSkillMod = {}
--Adding a Group Number to hold together different bonuses from the same modifier [Falterfire 22/10/27]
local groupNum = 0


  -- Create table header
for _, modType in ipairs(modTypes) do
  table.insert(returnPart, '{| class="wikitable" style="text-align:center; clear:both; margin:auto; margin-bottom:1em;"')
for i, modTypeData in ipairs(cons[modType]) do
  table.insert(returnPart, '|-\r\n!' .. Icons.Icon({'Thieving', type='skill', notext=true}) .. '[[Thieving|Thieving Targets]]')
groupNum = masteryReq[modType][i]
  table.insert(returnPart, '|-\r\n|')
local modVal = nil
 
if modValue ~= nil then
  local npcList = {}
modVal = modValue
  -- Create row for each NPC
else
  for i, npc in Shared.skpairs(SkillData.Thieving) do
modVal = modTypeData.incrementValue * modTypeData.maxCount
    local linkText = (npc.name ~= p._getThievingNPCStat(npc, 'pageName') and p._getThievingNPCStat(npc, 'pageName') .. '|' .. npc.name) or npc.name
end
    table.insert(npcList, Icons.Icon({npc.name, type='thieving', notext=true}) .. ' [[' .. linkText .. ']]')
for j, modifier in ipairs(modTypeData.modifiers) do
  end
local modEntry = (modifier.skill ~= nil and { skillID = modifier.skill, value = modVal }) or modVal
  table.insert(returnPart, table.concat(npcList, ' • '))
addToArray(modArray, {modifier.key, modEntry, group = groupNum})
  table.insert(returnPart, '|}')
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 = 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
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 Shared.printError('Failed to find a skill ID for ' .. skillName)
  end
end


  local unlockTable = SkillData.MasteryUnlocks[skillID]
local _, localSkillID = GameData.getLocalID(skillID)
  if unlockTable == nil then
-- Clone so that we can sort by level
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
local unlockTable = Shared.clone(SkillData[localSkillID].masteryLevelUnlocks)
  end
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'
local result = '{|class="wikitable"\r\n!Level!!Unlock'
  for i, unlock in Shared.skpairs(unlockTable) do
for i, unlock in ipairs(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.description
  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 Shared.printError('Failed to find a skill ID for ' .. skillName)
  end
end


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


  local bonuses = SkillData.MasteryCheckpoints[skillID].bonuses
local totalPoolXP = SkillData[localSkillID].baseMasteryPoolCap
  local totalPoolXP = SkillData.MasteryPoolXP[skillID + 1]
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


  local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
function p.getMasteryTokenTable()
  for i, bonus in Shared.skpairs(bonuses) do
-- Defines which skill levels should be included within the output
    result = result..'\r\n|-'
local skillLevels = {
    result = result..'\r\n|'..(MasteryCheckpoints[i] * 100)..'%||'
{
    result = result..Shared.formatnum(totalPoolXP * MasteryCheckpoints[i])..' xp||'..bonus
["id"] = 'Base',
  end
["level"] = 99,
  result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
["description"] = '[[Full Version|Base Game]] (Level 99)'
  result = result..'\r\n|'..Shared.formatnum(totalPoolXP)
}, {
  result = result..'\r\n|}'
["id"] = 'TotH',
  return result
["level"] = 120,
end
["description"] = Icons.TotH() .. ' [[Throne of the Herald Expansion|Throne of the Herald]] (Level 120)'
}
}
local baseTokenChance = 18500
local masteryActionCount = {}
local CCI_ID = 'melvorD:Clue_Chasers_Insignia'
local CCI = Items.getItemByID(CCI_ID)
if CCI == nil then
return Shared.printError('Failed to find item with ID ' .. CCI_ID)
end


function p._getFarmingTable(category)
-- Iterate over each skill with mastery, determining the number of
  local seedList = {}
-- mastery actions for each
  if category == 'Allotment' or category == 'Herb' or category == 'Tree' then
for skillLocalID, skill in pairs(SkillData) do
    seedList = Items.getItems(function(item) return item.tier == category end)
if skill.masteryTokenID ~= nil then
  else
local actCount = { ["skill"] = skill }
    return 'ERROR: Invalid farming category. Please choose Allotment, Herb, or Tree'
for i, levelDef in ipairs(skillLevels) do
  end
actCount[levelDef.id] = 0
end


  local result = '{|class="wikitable sortable stickyHeader"'
local recipeKey = p.getSkillRecipeKey(skillLocalID)
  result = result..'\r\n|- class="headerRow-0"'
if recipeKey ~= nil then
  result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
local recipeData = skill[recipeKey]
  result = result..'!!XP!!Growth Time!!Seed Value'
for i, recipe in ipairs(recipeData) do
  if category == 'Allotment' then
if recipe.noMastery == nil or not recipe.noMastery then
    result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
local skillLevel = p.getRecipeLevel(skillLocalID, recipe)
  elseif category == 'Herb' then
if skillLevel ~= nil then
    result = result..'!!colspan="2"|Herb!!Herb Value'
for j, levelDef in ipairs(skillLevels) do
  elseif category == 'Tree' then
if skillLevel <= levelDef.level then
    result = result..'!!colspan="2"|Logs!!Log Value'
actCount[levelDef.id] = actCount[levelDef.id] + 1
  end
end
  result = result..'!!Seed Sources'
end
 
end
  table.sort(seedList, function(a, b) return a.farmingLevel < b.farmingLevel end)
end
end
end
table.insert(masteryActionCount, actCount)
end
end


  for i, seed in pairs(seedList) do
local firstID = skillLevels[1].id
    result = result..'\r\n|-'
table.sort(masteryActionCount,
    result = result..'\r\n|'..Icons.Icon({seed.name, type='item', size='50', notext=true})..'||[['..seed.name..']]'
function(a, b)
    result = result..'||'..seed.farmingLevel..'||'..Shared.formatnum(seed.farmingXP)
if a[firstID] == b[firstID] then
    result = result..'||data-sort-value="'..seed.timeToGrow..'"|'..Shared.timeString(seed.timeToGrow, true)
return a.skill.name < b.skill.name
    result = result..'||data-sort-value="'..seed.sellsFor..'"|'..Icons.GP(seed.sellsFor)
else
return a[firstID] > b[firstID]
end
end)
-- Generate output table
local resultPart = {}
local CCIIcon = Icons.Icon({CCI.name, type='item', notext=true})
local columnPairs = Shared.tableCount(skillLevels)


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


  result = result..'\r\n|}'
for i, rowData in ipairs(masteryActionCount) do
  return result
local token = Items.getItemByID(rowData.skill.masteryTokenID)
end
table.insert(resultPart, '\n|-')
table.insert(resultPart, '\n|style="text-align:center"|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
table.insert(resultPart, '\n|' .. Icons.Icon({rowData.skill.name, type='skill'}))


function p.getFarmingTable(frame)
for j, levelDef in ipairs(skillLevels) do
  local category = frame.args ~= nil and frame.args[1] or frame
local actCount = rowData[levelDef.id]
local denom, denomCCI = 0, 0
if actCount > 0 then
denom = math.floor(baseTokenChance / actCount)
denomCCI = Shared.round(baseTokenChance / (actCount * (1 + CCI.modifiers.increasedOffItemChance / 100)), 0, 0)
end
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Shared.formatnum(denom))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denomCCI .. '"|1/' .. Shared.formatnum(denomCCI))
end
end
table.insert(resultPart, '\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 advMode = GameData.getEntityByID('gamemodes', 'melvorF:Adventure')
  result = result..'\r\n!colspan="2"|Crop!!'..Icons.Icon({"Farming", type="skill", notext=true})..' Level'
if advMode ~= nil then
  result = result..'!!Healing!!Value'
local unlockCount = Shared.tableCount(GameData.skillData) - Shared.tableCount(advMode.startingSkills)
 
local costLength = Shared.tableCount(advMode.skillUnlockCost)
  local itemArray = Items.getItems(function(item) return item.grownItemID ~= nil end)
local returnPart = {}
 
table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative Cost')
  table.sort(itemArray, function(a, b) return a.farmingLevel < b.farmingLevel end)


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


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


function p.getFarmingPlotTable(frame)
function p.getFiremakingTable(frame)
  local areaName = frame.args ~= nil and frame.args[1] or frame
local resultPart = {}
  local patches = nil
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
  for i, area in Shared.skpairs(SkillData.Farming.Patches) do
table.insert(resultPart, '\r\n|-class="headerRow-0"')
    if area.areaName == areaName then
table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level')
      patches = area.patches
table.insert(resultPart, '!!rowspan="2"|Burn Time!!colspan="2"|Without Bonfire!!colspan="2"|With Bonfire!!rowspan="2"|Bonfire Bonus!!rowspan="2"|Bonfire Time')
      break
table.insert(resultPart, '\r\n|-class="headerRow-1"')
    end
table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')
  end
  if patches == nil then
    return "ERROR: Invalid area name.[[Category:Pages with script errors"
  end


  local result = '{|class="wikitable"'
for i, logData in ipairs(SkillData.Firemaking.logs) do
  result = result..'\r\n!Plot!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level!!Cost'
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 = Shared.round(XP_BF / burnTime, 2, 2)
XP_BF = Shared.round(XP_BF, 2, 0)


  for i, patch in Shared.skpairs(patches) do
table.insert(resultPart, '\r\n|-')
    result = result..'\r\n|-\r\n|'..i
table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
    result = result..'||style="text-align:right;" data-sort-value="0"|'..patch.level
table.insert(resultPart, '||'..Icons.getExpansionIcon(logs.id)..Icons.Icon({name, type='item', noicon=true}))
    if patch.cost == 0 then
table.insert(resultPart, '||style ="text-align: right;"|'..logData.level)
      result = result..'||Free'
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true))
    else
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. logData.baseExperience .. '"| ' .. Shared.formatnum(logData.baseExperience))
      result = result..'||style="text-align:right;" data-sort-value="'..patch.cost..'"|'..Icons.GP(patch.cost)
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.formatnum(Shared.round(XPS, 2, 2)))
    end
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. XP_BF .. '"| ' .. Shared.formatnum(XP_BF))
  end
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.formatnum(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


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


function p.getPotionNavbox(frame)
function p.getAncientRelicsTable(frame)
  --•
local skillName = frame.args ~= nil and frame.args[1] or frame
  local result = '{| class="wikitable" style="margin:auto; clear:both; width: 100%"'
local skillID = nil
  result = result..'\r\n!colspan=2|'..Icons.Icon({'Herblore', 'Potions', type='skill'})
if skillName ~= nil and skillName ~= '' then
 
skillID = Constants.getSkillID(skillName)
  local CombatPots = {}
if skillID == nil then
  local SkillPots = {}
return Shared.printError('Failed to find a skill ID for ' .. skillName)
  for i, potData in Shared.skpairs(SkillData.Herblore.ItemData) do
end
    if potData.category == 0 then
end
      table.insert(CombatPots, Icons.Icon({potData.name, type='item', img=(potData.name..' I')}))
    else
      if potData.name == 'Bird Nests Potion' then
        table.insert(SkillPots, Icons.Icon({"Bird Nest Potion", type='item', img="Bird Nest Potion I"}))
      else
        table.insert(SkillPots, Icons.Icon({potData.name, type='item', img=(potData.name..' I')}))
      end
    end
  end


  result = result..'\r\n|-\r\n!Combat Potions\r\n|class="center" style="vertical-align:middle;"'
local resultPart = {}
  result = result..'|'..table.concat(CombatPots, ' • ')
table.insert(resultPart, '{| class="wikitable sortable stickyHeader lighttable"')
  result = result..'\r\n|-\r\n!Skill Potions\r\n|class="center" style="vertical-align:middle;"'
table.insert(resultPart, '\n|-class="headerRow-0"')
  result = result..'|'..table.concat(SkillPots, ' • ')
table.insert(resultPart, '\n|-\n!colspan="2"|Skill\n!Relic\n!Modifiers')
  result = result..'\r\n|}'
  return result
end


function p.getSmithingTable(frame)
local relics = GameData.getEntities('ancientRelics',
  local tableType = frame.args ~= nil and frame.args[1] or frame
function(relic)
  local bar = nil
return skillID == nil or relic.skillID == skillID
  if tableType ~= 'Smelting' then
end)
    bar = Items.getItem(tableType)
table.sort(relics,
    if bar == nil then
function (a, b)
      return 'ERROR: Could not find an item named '..tableType..' to build a smithing table with'
local skillNameA, skillNameB = Constants.getSkillName(a.skillID), Constants.getSkillName(b.skillID)
    elseif bar.type ~= 'Bar' then
if skillNameA == skillNameB then
      return 'ERROR: '..tableType.." is not a bar and thus can't be used for smithing"
-- Order by numbers at the end of relic IDs
    end
-- Relics have a 'number' property, but this appears to contain duplicates
  end
return string.sub(a.id, string.len(a.id)) < string.sub(b.id, string.len(b.id))
else
return skillNameA < skillNameB
end
end)


  local smithList = {}
local function appendSkillRows(resultTable, rowTable, relicCount, skillID)
  for i, item in pairs(ItemData.Items) do
local skillName = Constants.getSkillName(skillID)
    if item.smithingLevel ~= nil then
table.insert(resultTable, '\n|-\n|rowspan="' .. relicCount .. '"| ' .. Icons.Icon({skillName, type='skill', notext=true, size=50}))
      if tableType == 'Smelting' then
table.insert(resultTable, '\n|rowspan="' .. relicCount .. '"| ' .. Icons.Icon({skillName, type='skill', noicon=true}))
        if item.type == 'Bar' then
table.insert(resultTable, table.concat(rowTable))
          table.insert(smithList, item)
end
        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"'
local skillRelicCount, currentSkillID, tablePart = 0, nil, {}
  result = result..'\r\n|-class="headerRow-0"'
for i, relic in ipairs(relics) do
  result = result..'\r\n!Item!!Name!!'..Icons.Icon({'Smithing', type='skill', notext=true})..' Level!!XP!!Value!!Ingredients'
if currentSkillID == nil then
  --Adding value/bar for things other than smelting
currentSkillID = relic.skillID
  if bar ~= nil then result = result..'!!Value/Bar' end
elseif relic.skillID ~= currentSkillID then
appendSkillRows(resultPart, tablePart, skillRelicCount, currentSkillID)
tablePart = {}
currentSkillID = relic.skillID
skillRelicCount = 0
end


  table.sort(smithList, function(a, b)
skillRelicCount = skillRelicCount + 1
                          if a.smithingLevel ~= b.smithingLevel then
if skillRelicCount > 1 then
                            return a.smithingLevel < b.smithingLevel
table.insert(tablePart, '\n|-')
                          else
end
                            return a.name < b.name
table.insert(tablePart, '\n| ' .. skillRelicCount .. '\n| ' .. Constants.getModifiersText(relic.modifiers))
                          end end)
end
  for i, item in Shared.skpairs(smithList) do
appendSkillRows(resultPart, tablePart, skillRelicCount, currentSkillID)
    result = result..'\r\n|-'
table.insert(resultPart, '\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 table.concat(resultPart)
  return result
end
end


function p.getFiremakingTable(frame)
function p.getLesserRelicsTable(frame)
  local result = '{| class="wikitable sortable stickyHeader"'
local lesserRelics = {}
  result = result..'\r\n|-class="headerRow-0"'
-- Iterate over each skill with a global rare drop then check
  result = result..'\r\n!colspan="2"|Logs!!'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level'
-- if the skill has a Lesser Relic drop
  result = result..'!!XP!!Burn Time!!XP/s!!Bonfire Bonus!!Bonfire Time'
for skillLocalID, skill in pairs(SkillData) do
if skill.rareDrops ~= nil then
for i, drops in pairs(skill.rareDrops) do
if string.match(drops.itemID, '_Lesser_Relic') then
table.insert(lesserRelics, Items.getItemByID(drops.itemID))
end
end
end
end
table.sort(lesserRelics, function(a, b) return a.name < b.name end)


  for i, logData in Shared.skpairs(SkillData.Firemaking) do
-- Create the Table
    result = result..'\r\n|-'
local resultTable = mw.html.create('table')
    local name = Shared.titleCase(logData.type..' Logs')
resultTable:addClass('wikitable sortable')
    result = result..'\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true})
resultTable:tag('tr'):addClass('headerRow-0')
    result = result..'||[['..name..']]'
:tag('th'):wikitext('Icon')
    result = result..'||style ="text-align: right;"|'..logData.level
:tag('th'):wikitext('Lesser Relic')
    result = result..'||style ="text-align: right;"|'..logData.xp
:tag('th'):wikitext('Modifiers')
    local burnTime = logData.interval / 1000
    local XPS = logData.xp / burnTime
    result = result..'||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true)
    result = result..'||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.round(XPS, 2, 2)
    result = result..'||style ="text-align: right;" data-sort-value="'..logData.bonfireBonus..'"|'..logData.bonfireBonus..'%'
    result = result..'||style ="text-align: right;" data-sort-value="'..logData.bonfireInterval..'"|'..Shared.timeString(logData.bonfireInterval / 1000, true)
  end


  result = result..'\r\n|}'
for _, relic in ipairs(lesserRelics) do
  return result
local tr = mw.html.create('tr')
tr:tag('td'):wikitext(Icons.Icon({relic.name, type='item', size='50', notext=true}))
tr:tag('td'):wikitext(Icons.Icon({relic.name, type='item', noicon=true}))
tr:tag('td'):wikitext(Constants.getModifiersText(relic.modifiers))
resultTable:node(tr)
end
return resultTable
end
end


return p
return p

Latest revision as of 00:12, 26 March 2024

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')

-- Given a skill ID, returns the key for that skill's recipe data.
-- If the skill has no recipes (e.g. is a combat skill) then the
-- return value is nil
function p.getSkillRecipeKey(skillID)
	-- Convert skillID to local ID if not already
	local ns, localSkillID = GameData.getLocalID(skillID)
	local recipeIDs = {
		["Woodcutting"] = 'trees',
		["Fishing"] = 'fish',
		["Firemaking"] = 'logs',
		["Mining"] = 'rockData',
		["Thieving"] = 'npcs',
		["Agility"] = 'obstacles',
		["Cooking"] = 'recipes',
		["Smithing"] = 'recipes',
		["Farming"] = 'recipes',
		["Summoning"] = 'recipes',
		["Fletching"] = 'recipes',
		["Crafting"] = 'recipes',
		["Runecrafting"] = 'recipes',
		["Herblore"] = 'recipes',
		["Astrology"] = 'recipes'
	}
	return recipeIDs[localSkillID]
end

-- Given a skill ID & recipe, returns the skill level requirement for
-- that recipe. If the level could not be determined, then the return
-- value is nil
function p.getRecipeLevel(skillID, recipe)
	-- Convert skillID to local ID if not already
	local ns, localSkillID = GameData.getLocalID(skillID)
	if localSkillID == 'Agility' then
		-- For Agility, level is derived from obstacle category
		if recipe.category ~= nil then
			-- Obstacle
			return SkillData.Agility.obstacleUnlockLevels[recipe.category+1]
		else
			-- Pillar
			local nsR, localRecipeID = GameData.getLocalID(recipe.id)
			if localRecipeID ~= nil then
				if string.find(localRecipeID, '^Pillar') ~= nil then
					return 99
				elseif string.find(localRecipeID, '^ElitePillar') ~= nil then
					return 120
				end
			end
		end
	else
		-- For all other skills, the recipe should have a level property
		return recipe.level
	end
end

-- 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] = { qty = drop.quantity, area = area }
				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, type = 'npc'})
		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, type = 'npcUnique'})
		end

		local areaNPC = areaNPCs[npc.id]
		if areaNPC ~= nil then
			table.insert(resultArray, {npc = npc.name, minQty = areaNPC.qty, maxQty = areaNPC.qty, wt = SkillData.Thieving.baseAreaUniqueChance, totalWt = 100, level = npc.level, npcID = npc.id, area = areaNPC.area, type = 'areaUnique'})
		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, type = 'generalRare'})
			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, type = 'generalRare'})
					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 masteryReq = {
		['standardModifiers'] = { 1, 40, 80 },
		['uniqueModifiers'] = { 20, 60, 99 }
	}

	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 = masteryReq[modType][i]
			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()
	-- Defines which skill levels should be included within the output
	local skillLevels = {
		{
			["id"] = 'Base',
			["level"] = 99,
			["description"] = '[[Full Version|Base Game]] (Level 99)'
		}, {
			["id"] = 'TotH',
			["level"] = 120,
			["description"] = Icons.TotH() .. ' [[Throne of the Herald Expansion|Throne of the Herald]] (Level 120)'
		}
	}
	local baseTokenChance = 18500
	local masteryActionCount = {}
	local CCI_ID = 'melvorD:Clue_Chasers_Insignia'
	local CCI = Items.getItemByID(CCI_ID)
	if CCI == nil then
		return Shared.printError('Failed to find item with ID ' .. CCI_ID)	
	end

	-- Iterate over each skill with mastery, determining the number of
	-- mastery actions for each
	for skillLocalID, skill in pairs(SkillData) do
		if skill.masteryTokenID ~= nil then
			local actCount = { ["skill"] = skill }
			for i, levelDef in ipairs(skillLevels) do
				actCount[levelDef.id] = 0
			end

			local recipeKey = p.getSkillRecipeKey(skillLocalID)
			if recipeKey ~= nil then
				local recipeData = skill[recipeKey]
				for i, recipe in ipairs(recipeData) do
					if recipe.noMastery == nil or not recipe.noMastery then
						local skillLevel = p.getRecipeLevel(skillLocalID, recipe)
						if skillLevel ~= nil then
							for j, levelDef in ipairs(skillLevels) do
								if skillLevel <= levelDef.level then
									actCount[levelDef.id] = actCount[levelDef.id] + 1
								end
							end
						end
					end
				end
			end
			table.insert(masteryActionCount, actCount)
		end
	end

	local firstID = skillLevels[1].id
	table.sort(masteryActionCount,
		function(a, b)
			if a[firstID] == b[firstID] then
				return a.skill.name < b.skill.name
			else
				return a[firstID] > b[firstID]
			end
		end)
	
	-- Generate output table
	local resultPart = {}
	local CCIIcon = Icons.Icon({CCI.name, type='item', notext=true})
	local columnPairs = Shared.tableCount(skillLevels)

	-- Generate header
	table.insert(resultPart, '{| class="wikitable sortable"')
	table.insert(resultPart, '\n!rowspan="3"|Token!!rowspan="3"|Skill!!colspan="' .. columnPairs * 2 .. '"|Approximate Mastery Token Chance')
	table.insert(resultPart, '\n|-')
	for i, levelDef in ipairs(skillLevels) do
		table.insert(resultPart, '\n!colspan="2"| ' .. levelDef.description)
	end
	table.insert(resultPart, '\n|-' .. string.rep('\n!Without ' .. CCIIcon .. '\n!With ' .. CCIIcon, columnPairs))

	for i, rowData in ipairs(masteryActionCount) do
		local token = Items.getItemByID(rowData.skill.masteryTokenID)
		table.insert(resultPart, '\n|-')
		table.insert(resultPart, '\n|style="text-align:center"|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
		table.insert(resultPart, '\n|' .. Icons.Icon({rowData.skill.name, type='skill'}))

		for j, levelDef in ipairs(skillLevels) do
			local actCount = rowData[levelDef.id]
			local denom, denomCCI = 0, 0
			if actCount > 0 then
				denom = math.floor(baseTokenChance / actCount)
				denomCCI = Shared.round(baseTokenChance / (actCount * (1 + CCI.modifiers.increasedOffItemChance / 100)), 0, 0)
			end
			table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Shared.formatnum(denom))
			table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denomCCI .. '"|1/' .. Shared.formatnum(denomCCI))
		end
	end
	table.insert(resultPart, '\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

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 = Shared.round(XP_BF / burnTime, 2, 2)
		XP_BF = Shared.round(XP_BF, 2, 0)

		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;" data-sort-value="' .. logData.baseExperience .. '"| ' .. Shared.formatnum(logData.baseExperience))
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.formatnum(Shared.round(XPS, 2, 2)))
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. XP_BF .. '"| ' .. Shared.formatnum(XP_BF))
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.formatnum(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

function p.getAncientRelicsTable(frame)
	local skillName = frame.args ~= nil and frame.args[1] or frame
	local skillID = nil
	if skillName ~= nil and skillName ~= '' then
		skillID = Constants.getSkillID(skillName)
		if skillID == nil then
			return Shared.printError('Failed to find a skill ID for ' .. skillName)
		end
	end

	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader lighttable"')
	table.insert(resultPart, '\n|-class="headerRow-0"')
	table.insert(resultPart, '\n|-\n!colspan="2"|Skill\n!Relic\n!Modifiers')

	local relics = GameData.getEntities('ancientRelics',
		function(relic)
			return skillID == nil or relic.skillID == skillID
		end)
	table.sort(relics,
		function (a, b)
			local skillNameA, skillNameB = Constants.getSkillName(a.skillID), Constants.getSkillName(b.skillID)
			if skillNameA == skillNameB then
				-- Order by numbers at the end of relic IDs
				-- Relics have a 'number' property, but this appears to contain duplicates
				return string.sub(a.id, string.len(a.id)) < string.sub(b.id, string.len(b.id))
			else
				return skillNameA < skillNameB
			end
		end)

	local function appendSkillRows(resultTable, rowTable, relicCount, skillID)
		local skillName = Constants.getSkillName(skillID)
		table.insert(resultTable, '\n|-\n|rowspan="' .. relicCount .. '"| ' .. Icons.Icon({skillName, type='skill', notext=true, size=50}))
			table.insert(resultTable, '\n|rowspan="' .. relicCount .. '"| ' .. Icons.Icon({skillName, type='skill', noicon=true}))
			table.insert(resultTable, table.concat(rowTable))
	end

	local skillRelicCount, currentSkillID, tablePart = 0, nil, {}
	for i, relic in ipairs(relics) do
		if currentSkillID == nil then
			currentSkillID = relic.skillID
		elseif relic.skillID ~= currentSkillID then
			appendSkillRows(resultPart, tablePart, skillRelicCount, currentSkillID)		
			tablePart = {}
			currentSkillID = relic.skillID
			skillRelicCount = 0
		end

		skillRelicCount = skillRelicCount + 1
		if skillRelicCount > 1 then
			table.insert(tablePart, '\n|-')
		end
		table.insert(tablePart, '\n| ' .. skillRelicCount .. '\n| ' .. Constants.getModifiersText(relic.modifiers))
	end
	appendSkillRows(resultPart, tablePart, skillRelicCount, currentSkillID)
	table.insert(resultPart, '\n|}')

	return table.concat(resultPart)
end

function p.getLesserRelicsTable(frame)
	local lesserRelics = {}
	-- Iterate over each skill with a global rare drop then check
	-- if the skill has a Lesser Relic drop
	for skillLocalID, skill in pairs(SkillData) do
		if skill.rareDrops ~= nil then
			for i, drops in pairs(skill.rareDrops) do
				if string.match(drops.itemID, '_Lesser_Relic') then
					table.insert(lesserRelics, Items.getItemByID(drops.itemID))
				end
			end
		end
	end
	table.sort(lesserRelics, function(a, b) return a.name < b.name end)

	-- Create the Table
	local resultTable = mw.html.create('table')
	resultTable:addClass('wikitable sortable')
	resultTable:tag('tr'):addClass('headerRow-0')
		:tag('th'):wikitext('Icon')
		:tag('th'):wikitext('Lesser Relic')
		:tag('th'):wikitext('Modifiers')

	for _, relic in ipairs(lesserRelics) do
		local tr = mw.html.create('tr')
		tr:tag('td'):wikitext(Icons.Icon({relic.name, type='item', size='50', notext=true}))
		tr:tag('td'):wikitext(Icons.Icon({relic.name, type='item', noicon=true}))
		tr:tag('td'):wikitext(Constants.getModifiersText(relic.modifiers))
		resultTable:node(tr)
	end
	return resultTable
end

return p