Module:Skills: Difference between revisions

From Melvor Idle
(getFiremakingTable: Add XP with bonfire stats & refactor to avoid repetitive string concatenation)
(Add Archaeology recipe key)
 
(65 intermediate revisions by 5 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 Common = require('Module:Common')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Modifiers = require('Module:Modifiers')
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 Num = require('Module:Number')


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',
["Archaeology"] = 'digSites',
["Harvesting"] = 'veinData'
}
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 and a boolean value indicating whether the level if abyssal or not.
    if skName == skillName then
-- If the level could not be determined, then the return value is nil, nil
      return ID
function p.getRecipeLevelRealm(skillID, recipe)
    end
local level, isAbyssal = nil, nil
  end
-- Convert skillID to local ID if not already
  return nil
local ns, localSkillID = GameData.getLocalID(skillID)
local realmID = p.getRecipeRealm(recipe)
if localSkillID == 'Agility' then
local course = GameData.getEntityByProperty(SkillData.Agility.courses, 'realm', realmID)
isAbyssal = (realmID == 'melvorItA:Abyssal')
-- For Agility, level is derived from obstacle category
if recipe.category ~= nil then
-- Obstacle
local slot = course.obstacleSlots[recipe.category + 1]
if isAbyssal then
level = slot.abyssalLevel
else
level = slot.level
end
elseif recipe.slot ~= nil then
-- Pillar
level = course.pillarSlots[recipe.slot + 1].level
end
elseif recipe.abyssalLevel ~= nil then
level, isAbyssal = recipe.abyssalLevel, true
else
-- For all other skills, the recipe should have a level property
level, isAbyssal = recipe.level, false
end
return level, isAbyssal
end
end


function p.getSkillName(skillID)
function p.getRecipeLevel(skillID, recipe)
  for skName, ID in Shared.skpairs(Constants.skill) do
local level, isAbyssal = p.getRecipeLevelRealm(skillID, recipe)
    if ID == skillID then
return level
      return skName
    end
  end
  return nil
end
end


function p.getThievingNPCByID(ID)
function p.getRecipeRequirementText(skillName, recipe)
  local result = Shared.clone(SkillData.Thieving[ID + 1])
local reqText = {}
  if result ~= nil then
if recipe.abyssalLevel ~= nil and recipe.abyssalLevel > 0 then
    result.id = ID
table.insert(reqText, Icons._SkillReq(skillName, recipe.abyssalLevel, false, 'melvorItA:Abyssal'))
  end
elseif recipe.level ~= nil and recipe.level > 0 then
  return result
table.insert(reqText, Icons._SkillReq(skillName, recipe.level, false))
end
if recipe.totalMasteryRequired ~= nil then
table.insert(reqText, Num.formatnum(recipe.totalMasteryRequired) .. ' ' .. Icons.Icon({skillName, type='skill', notext=true}) .. ' ' .. Icons.Icon({'Mastery'}))
end
local reqsData = {}
if type(recipe.requirements) == 'table' then
reqsData = Shared.shallowClone(recipe.requirements)
end
if recipe.shopItemPurchased ~= nil then
-- Mining requirements are stored differently than other skills like
-- Woodcutting, standardize here
table.insert(reqsData, {
["type"] = 'ShopPurchase',
["purchaseID"] = recipe.shopItemPurchased,
["count"] = 1
})
end
if not Shared.tableIsEmpty(reqsData) then
local reqs = Common.getRequirementString(reqsData)
if reqs ~= nil then
table.insert(reqText, reqs)
end
end
return table.concat(reqText, '<br/>')
end
end


function p.getThievingNPC(name)
function p.standardRecipeSort(skillID, recipeA, recipeB)
  local result = nil
local levelA, isAbyssalA = p.getRecipeLevelRealm(skillID, recipeA)
  for i, npc in pairs(SkillData.Thieving) do
local levelB, isAbyssalB = p.getRecipeLevelRealm(skillID, recipeB)
    if name == npc.name then
local isAbyssalNumA, isAbyssalNumB = (isAbyssalA and 1) or 0, (isAbyssalB and 1) or 0
      result = Shared.clone(npc)
 
      result.id = i - 1
return (isAbyssalA == isAbyssalB and levelA < levelB) or isAbyssalNumA < isAbyssalNumB
      break
    end
  end
  return result
end
end


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


function p._getThievingNPCStat(npc, stat)
function p.getRealmFromName(realmName)
  local itemDropChance = 0.75
local realm = nil
  local result = npc[stat]
if realmName == nil or realmName == '' then
  -- Overrides below
-- Default realm
  if stat == 'maxHit' then
realm = GameData.getEntityByID('realms', 'melvorD:Melvor')
    result = result * 10
else
  elseif stat == 'lootList' then
realm = GameData.getEntityByName('realms', realmName)
    return p._formatLootTable(npc['lootTable'], itemDropChance, true)
end
  elseif stat == 'lootTable' then
return realm
    return p._formatLootTable(npc['lootTable'], itemDropChance, false)
end
  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
function p.getMasteryActionCount(skillID, realmID, levelLimit)
local actCount = 0
local skillNS, skillLocalID = Shared.getLocalID(skillID)
local skillData = SkillData[skillLocalID]
local recipeKey = p.getSkillRecipeKey(skillLocalID)
if recipeKey ~= nil then
local recipeData = skillData[recipeKey]
for i, recipe in ipairs(recipeData) do
if (
p.getRecipeRealm(recipe) == realmID
and (recipe.noMastery == nil or not recipe.noMastery)
and (levelLimit == nil or p.getRecipeLevel(skillLocalID, recipe) <= levelLimit)
) then
actCount = actCount + 1
end
end
end
return actCount
end
end


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


  local totalWeight = 0
function p.getThievingNPC(npcName)
  for i, drop in pairs(lootTable) do
return GameData.getEntityByName(SkillData.Thieving.npcs, npcName)
    totalWeight = totalWeight + drop[2]
end
  end
  if totalWeight == 0 then
    return 0
  end


  local avgValue = 0
function p.getThievingNPCArea(npc)
  for i, drop in pairs(lootTable) do
for i, area in ipairs(SkillData.Thieving.areas) do
    local item = Items.getItemByID(drop[1])
for j, npcID in ipairs(area.npcIDs) do
    if item ~= nil then
if npcID == npc.id then
      avgValue = avgValue + item.sellsFor * (drop[2] / totalWeight)
return area
    end
end
  end
end
 
end
  return avgValue
end
end


function p._formatLootTable(lootTableIn, chanceMultIn, asList)
function p._getThievingNPCStat(npc, statName)
  -- Expects lootTableIn to be in format {{itemID_1, itemWeight_1}, ..., {itemID_n, itemWeight_n}}
local result = nil
  if Shared.tableCount(lootTableIn) == 0 then
    return ''
  end


  local chanceMult = (chanceMultIn or 1) * 100
if statName == 'level' then
  local lootTable = Shared.clone(lootTableIn)
result = Icons._SkillReq('Thieving', npc.level)
  -- Sort table from most to least common drop
elseif statName == 'maxHit' then
  table.sort(lootTable, function(a, b)
result = npc.maxHit * 10
                          if a[2] == b[2] then
elseif statName == 'area' then
                            return a[1] < b[1]
local area = p.getThievingNPCArea(npc)
                          else
result = area.name
                            return a[2] > b[2]
else
                          end
result = npc[statName]
                        end)
end


  local totalWeight = 0
if result == nil then
  for i, drop in pairs(lootTable) do
result = ''
    totalWeight = totalWeight + drop[2]
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
return result
  -- [4/16/21]: Adding info for no drop
end
  local maxDropLen = math.max(string.len(Shared.round(100 - chanceMult, 2, 2)), string.len(Shared.round(lootTable[1][2] / totalWeight * chanceMult, 2, 2)))


  local returnPart = {}
function p.getThievingNPCStat(frame)
  -- Generate header
local npcName = frame.args ~= nil and frame.args[1] or frame[1]
  if asList then
local statName = frame.args ~= nil and frame.args[2] or frame[2]
    if chanceMult < 100 then
local npc = p.getThievingNPC(npcName)
      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')
if npc == nil then
    end
return Shared.printError('Invalid Thieving NPC ' .. npcName)
  else
end
    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')
return p._getThievingNPCStat(npc, statName)
end
end


function p.getThievingNPCTable()
function p.getThievingSourcesForItem(itemID)
  local returnPart = {}
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


  -- Create table header
--Now go through and get drop chances on each NPC if needed
  table.insert(returnPart, '{| class="wikitable sortable stickyHeader"')
for i, npc in pairs(SkillData.Thieving.npcs) do
  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>')
local totalWt = 0
 
local dropWt = 0
  -- Create row for each NPC
local dropQty = { min = 0, max = 0 }
  for i, npc in Shared.skpairs(SkillData.Thieving) do
for j, drop in ipairs(npc.lootTable) do
    local linkText = (npc.name ~= p._getThievingNPCStat(npc, 'pageName') and p._getThievingNPCStat(npc, 'pageName') .. '|' .. npc.name) or npc.name
totalWt = totalWt + drop.weight
    table.insert(returnPart, '|-\r\n|style="text-align: left;" |' .. Icons.Icon({npc.name, type='thieving', size=50, notext=true}))
if drop.itemID == itemID then
    table.insert(returnPart, '|style="text-align: left;" |[[' .. linkText .. ']]')
dropWt = drop.weight
    table.insert(returnPart, '|style="text-align: right;" |' .. p._getThievingNPCStat(npc, 'level'))
dropQty = { min = drop.minQuantity, max = drop.maxQuantity }
    table.insert(returnPart, '|style="text-align: right;" |' .. p._getThievingNPCStat(npc, 'xp'))
end
    table.insert(returnPart, '|style="text-align: right;" |' .. p._getThievingNPCStat(npc, 'maxHit'))
end
    table.insert(returnPart, '|style="text-align: right;" data-sort-value="' .. p._getThievingNPCStat(npc, 'maxCoins') .. '" |' .. Icons.GP(p._getThievingNPCStat(npc, 'maxCoins')))
if dropWt > 0 then
    table.insert(returnPart, '|style="text-align: right;" data-sort-value="' .. p._getThievingNPCStat(npc, 'pickpocketValue') .. '" |' .. Icons.GP(p._getThievingNPCStat(npc, 'pickpocketValue')))
table.insert(resultArray, {npc = npc.name, minQty = dropQty.min, maxQty = dropQty.max, wt = dropWt * SkillData.Thieving.itemChance, totalWt = totalWt * 100, level = npc.level, abyssalLevel = npc.abyssalLevel, npcID = npc.id, type = 'npc'})
  end
end
  table.insert(returnPart, '|}')


  return table.concat(returnPart, '\r\n')
--Chance of -1 on unique drops is to indicate variable chance
end
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, abyssalLevel = npc.abyssalLevel, npcID = npc.id, type = 'npcUnique'})
end


function p.getThievingNavbox()
local areaNPC = areaNPCs[npc.id]
  local returnPart = {}
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, abyssalLevel = npc.abyssalLevel, npcID = npc.id, area = areaNPC.area, type = 'areaUnique'})
end
end


  -- Create table header
for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
  table.insert(returnPart, '{| class="wikitable" style="text-align:center; clear:both; margin:auto; margin-bottom:1em;"')
if drop.itemID == itemID then
  table.insert(returnPart, '|-\r\n!' .. Icons.Icon({'Thieving', type='skill', notext=true}) .. '[[Thieving|Thieving Targets]]')
if drop.npcs == nil then
  table.insert(returnPart, '|-\r\n|')
table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Num.round2(1/(drop.chance/100), 0), level = 1, npcID = itemID, type = 'generalRare'})
 
else
  local npcList = {}
for j, npcID in ipairs(drop.npcs) do
  -- Create row for each NPC
local npc = p.getThievingNPCByID(npcID)
  for i, npc in Shared.skpairs(SkillData.Thieving) do
if npc ~= nil then
    local linkText = (npc.name ~= p._getThievingNPCStat(npc, 'pageName') and p._getThievingNPCStat(npc, 'pageName') .. '|' .. npc.name) or npc.name
table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = 1, wt = 1, totalWt = Num.round2(1/(drop.chance/100), 0), level = npc.level, abyssalLevel = npc.abyssalLevel, npcID = npc.id, type = 'generalRare'})
    table.insert(npcList, Icons.Icon({npc.name, type='thieving', notext=true}) .. ' [[' .. linkText .. ']]')
end
  end
end
  table.insert(returnPart, table.concat(npcList, ' • '))
end
  table.insert(returnPart, '|}')
end
end


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


function p.getMasteryUnlockTable(frame)
-- Astrology
  local skillName = frame.args ~= nil and frame.args[1] or frame
function p.getConstellationByID(constID)
  local skillID = p.getSkillID(skillName)
return GameData.getEntityByID(SkillData.Astrology.recipes, constID)
  if skillID == nil then
end
    return "ERROR: Failed to find a skill ID for "..skillName
  end


  local unlockTable = SkillData.MasteryUnlocks[skillID]
function p.getConstellation(constName)
  if unlockTable == nil then
return GameData.getEntityByName(SkillData.Astrology.recipes, constName)
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
end
  end


  local result = '{|class="wikitable"\r\n!Level!!Unlock'
function p.getConstellations(checkFunc)
  for i, unlock in Shared.skpairs(unlockTable) do
return GameData.getEntities(SkillData.Astrology.recipes, checkFunc)
    result = result..'\r\n|-'
    result = result..'\r\n|'..unlock.level..'||'..unlock.unlock
  end
  result = result..'\r\n|}'
  return result
end
end


function p.getMasteryCheckpointTable(frame)
-- Combines Astrology constellation modifiers into an object similar to other entities,
  local skillName = frame.args ~= nil and frame.args[1] or frame
-- and multiplies the values up to their maximum possible amount
  local skillID = p.getSkillID(skillName)
function p._getConstellationModifiers(cons)
  if skillID == nil then
local result = {}
    return "ERROR: Failed to find a skill ID for "..skillName
local modKeys = { 'standardModifiers', 'uniqueModifiers', 'abyssalModifiers' }
  end
 
for _, keyID in ipairs(modKeys) do
result[keyID] = {}
local mods = cons[keyID]
if mods ~= nil then
for _, mod in ipairs(mods) do
local newModObj = {}
local multValue = mod.maxCount
local subKeys = { 'modifiers', 'enemyModifiers' }
for _, subKey in ipairs(subKeys) do
local modAdj = Shared.clone(mod[subKey])
if type(modAdj) == 'table' then
for modName, modValueDef in pairs(modAdj) do
if type(modValueDef) == 'table' then
if modValueDef[1] ~= nil then
-- Table of multiple values
for i, subValue in ipairs(modValueDef) do
if type(subValue) == 'table' and subValue.value ~= nil then
subValue.value = subValue.value * multValue
elseif type(subValue) == 'number' then
modValueDef[i] = subValue * multValue
end
end
elseif modValueDef.value ~= nil then
-- Table but with a single value
modValueDef.value = modValueDef.value * multValue
end
elseif type(modValueDef) == 'number' then
-- Single value
modAdj[modName] = modValueDef * multValue
end
end
newModObj[subKey] = modAdj
end
end
table.insert(result[keyID], newModObj)
end
end
end
return result
end


  if SkillData.MasteryCheckpoints[skillID] == nil then
-- Mastery
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
function p.getMasteryUnlockTable(frame)
  end
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 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.shallowClone(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 args = frame.args ~= nil and frame.args or frame
  if category == 'Allotment' or category == 'Herb' or category == 'Tree' then
local skillName = args[1]
    seedList = Items.getItems(function(item) return item.tier == category end)
local realmName = args.realm
  else
    return 'ERROR: Invalid farming category. Please choose Allotment, Herb, or Tree'
  end


  local result = '{|class="wikitable sortable stickyHeader"'
local realm = p.getRealmFromName(realmName)
  result = result..'\r\n|- class="headerRow-0"'
if realm == nil then
  result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
  result = result..'!!XP!!Growth Time!!Seed Value'
end
  if category == 'Allotment' then
local skillID = Constants.getSkillID(skillName)
    result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
if skillID == nil then
  elseif category == 'Herb' then
return Shared.printError('Failed to find a skill ID for ' .. skillName)
    result = result..'!!colspan="2"|Herb!!Herb Value'
end
  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 _, localSkillID = GameData.getLocalID(skillID)
    result = result..'\r\n|-'
local checkpoints = SkillData[localSkillID].masteryPoolBonuses
    result = result..'\r\n|'..Icons.Icon({seed.name, type='item', size='50', notext=true})..'||[['..seed.name..']]'
if checkpoints == nil then
    result = result..'||'..seed.farmingLevel..'||'..Shared.formatnum(seed.farmingXP)
return Shared.printError('Failed to find Mastery checkpoint data for ' .. skillName)
    result = result..'||data-sort-value="'..seed.timeToGrow..'"|'..Shared.timeString(seed.timeToGrow, true)
end
    result = result..'||data-sort-value="'..seed.sellsFor..'"|'..Icons.GP(seed.sellsFor)


    local crop = Items.getItemByID(seed.grownItemID)
local totalPoolXP = p.getMasteryActionCount(localSkillID, realm.id) * 500000
    result = result..'||'..Icons.Icon({crop.name, type='item', size='50', notext=true})..'||[['..crop.name..']]'
local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
    if category == 'Allotment' then
for i, checkpointData in ipairs(checkpoints) do
      result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..(crop.healsFor * 10)
if checkpointData.realm == realm.id then
    end
local chkDesc = Modifiers.getModifiersText(checkpointData.modifiers, false, false)
    result = result..'||data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
local chkPercent = checkpointData.percent
    result = result..'||'..ItemSourceTables._getItemSources(seed)
result = result..'\r\n|-'
  end
result = result..'\r\n|'..chkPercent..'%||'
result = result..Num.formatnum(math.floor(totalPoolXP * chkPercent / 100))..' xp||'..chkDesc
end
end
result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
result = result..'\r\n|'..Num.formatnum(totalPoolXP)
result = result..'\r\n|}'
return result
end


  result = result..'\r\n|}'
function p.getMasteryTokenTable()
  return result
-- Defines which skill levels should be included within the output
end
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


function p.getFarmingTable(frame)
local tokens = Items.getItems(function(item) return item.itemType == 'MasteryToken' end)
  local category = frame.args ~= nil and frame.args[1] or frame
local tokenItems = {}
for _, item in ipairs(tokens) do
if item.realm == 'melvorD:Melvor' and item.skill ~= nil then
local skillNS, skillLocalID = Shared.getLocalID(item.skill)
tokenItems[skillLocalID] = item
end
end


  return p._getFarmingTable(category)
-- Iterate over each skill with mastery, determining the number of
end
-- mastery actions for each
for skillLocalID, skill in pairs(SkillData) do
if skill.masteryPoolBonuses ~= nil then
local actCount = { ["skill"] = skill, ["token"] = tokenItems[skillLocalID] }
for i, levelDef in ipairs(skillLevels) do
actCount[levelDef.id] = p.getMasteryActionCount(skillLocalID, 'melvorD:Melvor', levelDef.level)
end
table.insert(masteryActionCount, actCount)
end
end


function p.getFarmingFoodTable(frame)
local firstID = skillLevels[1].id
  local result = '{| class="wikitable sortable stickyHeader"'
table.sort(masteryActionCount,
  result = result..'\r\n|- class="headerRow-0"'
function(a, b)
  result = result..'\r\n!colspan="2"|Crop!!'..Icons.Icon({"Farming", type="skill", notext=true})..' Level'
if a[firstID] == b[firstID] then
  result = result..'!!Healing!!Value'
return a.skill.name < b.skill.name
 
else
  local itemArray = Items.getItems(function(item) return item.grownItemID ~= nil end)
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)


  table.sort(itemArray, function(a, b) return a.farmingLevel < b.farmingLevel end)
-- 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, item in Shared.skpairs(itemArray) do
for i, rowData in ipairs(masteryActionCount) do
    local crop = Items.getItemByID(item.grownItemID)
local token = rowData.token
    if crop.healsFor ~= nil and crop.healsFor > 0 then
table.insert(resultPart, '\n|-')
      result = result..'\r\n|-'
local tokenImg = (token == nil and '?') or Icons.Icon({token.name, type='item', notext=true})
      result = result..'\r\n|'..Icons.Icon({crop.name, type='item', notext='true', size='50'})..'||[['..crop.name..']]'
table.insert(resultPart, '\n|style="text-align:center"|' .. tokenImg)
      result = result..'||style="text-align:right;"|'..item.farmingLevel
table.insert(resultPart, '\n|' .. Icons.Icon({rowData.skill.name, type='skill'}))
      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|}'
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 = Num.round(baseTokenChance / (actCount * (1 + CCI.modifiers.offItemChance / 100)), 0, 0)
end
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Num.formatnum(denom))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denomCCI .. '"|1/' .. Num.formatnum(denomCCI))
end
end
table.insert(resultPart, '\n|}')


  return result
return table.concat(resultPart)
end
end


function p.getFarmingPlotTable(frame)
function p.getFiremakingTable(frame)
  local areaName = frame.args ~= nil and frame.args[1] or frame
    local args = frame.args ~= nil and frame.args or frame
  local patches = nil
    local realmName = args.realm
  for i, area in Shared.skpairs(SkillData.Farming.Patches) do
    local realm = p.getRealmFromName(realmName)
     if area.areaName == areaName then
     if realm == nil then
      patches = area.patches
        return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
      break
     end
     end
  end
    local skillID = 'Firemaking'
  if patches == nil then
    return "ERROR: Invalid area name.[[Category:Pages with script errors"
  end


  local result = '{|class="wikitable"'
    local tableHtml = mw.html.create('table')
  result = result..'\r\n!Plot!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level!!Cost'
        :addClass('wikitable sortable stickyHeader')
   
    local headerRow0 = tableHtml:tag('tr'):addClass('headerRow-0')
    headerRow0:tag('th'):attr('colspan', '2')
                        :attr('rowspan', '2')
                        :wikitext('Logs')
    headerRow0:tag('th'):attr('rowspan', '2')
                        :wikitext(Icons._SkillRealmIcon('Firemaking', realm.id) .. '<br>Level')
    headerRow0:tag('th'):attr('rowspan', '2')
                        :wikitext('[[DLC]]')
    headerRow0:tag('th'):attr('rowspan', '2')
                        :wikitext('Burn<br>Time')
    headerRow0:tag('th'):attr('colspan', '2')
                        :wikitext('Without Bonfire')
    headerRow0:tag('th'):attr('colspan', '2')
                        :wikitext('With Bonfire')
    headerRow0:tag('th'):attr('rowspan', '2')
                        :wikitext('Bonfire<br>Bonus')
    headerRow0:tag('th'):attr('rowspan', '2')
                        :wikitext('Bonfire<br>Time')


  for i, patch in Shared.skpairs(patches) do
    local headerRow1 = tableHtml:tag('tr'):addClass('headerRow-1')
    result = result..'\r\n|-\r\n|'..i
     headerRow1:tag('th'):wikitext('XP')
     result = result..'||style="text-align:right;" data-sort-value="0"|'..patch.level
     headerRow1:tag('th'):wikitext('XP/s')
     if patch.cost == 0 then
     headerRow1:tag('th'):wikitext('XP')
      result = result..'||Free'
    headerRow1:tag('th'):wikitext('XP/s')
     else
      result = result..'||style="text-align:right;" data-sort-value="'..patch.cost..'"|'..Icons.GP(patch.cost)
    end
  end


  result = result..'\r\n|}'
    local logsData = GameData.getEntities(SkillData.Firemaking.logs, function(obj)
  return result
        return p.getRecipeRealm(obj) == realm.id
end
    end)
    table.sort(logsData, function(a, b) return p.standardRecipeSort(skillID, a, b) end)
   
    for i, logData in ipairs(logsData) do
        local logs = Items.getItemByID(logData.logID)
        local name = logs.name
        local level = p.getRecipeLevel(skillID, logData)
        local baseXP = logData.baseAbyssalExperience or logData.baseExperience
        local reqText = p.getRecipeRequirementText(SkillData.Firemaking.name, logData)
        local bonfireBonus = logData.bonfireAXPBonus or logData.bonfireXPBonus
        local burnTime = logData.baseInterval / 1000
        local bonfireTime = logData.baseBonfireInterval / 1000
        local XPS = baseXP / burnTime
        local XP_BF = baseXP * (1 + bonfireBonus / 100)
        local XPS_BF = Num.round(XP_BF / burnTime, 2, 2)
        XP_BF = Num.round(XP_BF, 2, 0)


function p.getSmithingTable(frame)
        local row = tableHtml:tag('tr')
  local tableType = frame.args ~= nil and frame.args[1] or frame
        row:tag('td'):attr('data-sort-value', name)
  local bar = nil
                    :wikitext(Icons.Icon({name, type='item', notext=true}))
  if tableType ~= 'Smelting' then
        row:tag('td'):wikitext('[[' .. name .. ']]')
    bar = Items.getItem(tableType)
        row:tag('td'):css('text-align', 'center')
    if bar == nil then
                    :wikitext(level)
      return 'ERROR: Could not find an item named '..tableType..' to build a smithing table with'
        row:tag('td'):css('text-align', 'center')
    elseif bar.type ~= 'Bar' then
            :attr('data-sort-value', Icons.getExpansionID(logData.logID))
      return 'ERROR: '..tableType.." is not a bar and thus can't be used for smithing"
            :wikitext(Icons.getDLCColumnIcon(logData.logID))
    end
        row:tag('td'):css('text-align', 'right')
  end
                    :attr('data-sort-value', burnTime)
 
                    :wikitext(Shared.timeString(burnTime, true))
  local smithList = {}
        row:tag('td'):css('text-align', 'right')
  for i, item in pairs(ItemData.Items) do
                    :attr('data-sort-value', baseXP)
    if item.smithingLevel ~= nil then
                    :wikitext(Num.formatnum(baseXP))
      if tableType == 'Smelting' then
        row:tag('td'):css('text-align', 'right')
         if item.type == 'Bar' then
                    :attr('data-sort-value', XPS)
          table.insert(smithList, item)
                    :wikitext(Num.formatnum(Num.round(XPS, 2, 2)))
       
        if bonfireBonus == 0 then
            row:tag('td'):attr('colspan', '4')
                        :addClass('table-na')
                        :wikitext('N/A')
         else
            row:tag('td'):css('text-align', 'right')
                        :attr('data-sort-value', XP_BF)
                        :wikitext(Num.formatnum(XP_BF))
            row:tag('td'):css('text-align', 'right')
                        :attr('data-sort-value', XPS_BF)
                        :wikitext(Num.formatnum(XPS_BF, 2, 2))
            row:tag('td'):css('text-align', 'right')
                        :attr('data-sort-value', bonfireBonus)
                        :wikitext(bonfireBonus .. '%')
            row:tag('td'):css('text-align', 'right')
                        :attr('data-sort-value', bonfireTime)
                        :wikitext(Shared.timeString(bonfireTime, true))
         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"'
  result = result..'\r\n|-class="headerRow-0"'
  result = result..'\r\n!Item!!Name!!'..Icons.Icon({'Smithing', type='skill', notext=true})..' Level!!XP!!Value!!Ingredients'
  --Adding value/bar for things other than smelting
  if bar ~= nil then result = result..'!!Value/Bar' end
  table.sort(smithList, function(a, b)
                          if a.smithingLevel ~= b.smithingLevel then
                            return a.smithingLevel < b.smithingLevel
                          else
                            return a.name < b.name
                          end end)
  for i, item in Shared.skpairs(smithList) do
    result = result..'\r\n|-'
    result = result..'\r\n|'..Icons.Icon({item.name, type='item', size='50', notext=true})..'||'
    local qty = item.smithingQty ~= nil and item.smithingQty or 1
    if qty > 1 then
      result = result..item.smithingQty..'x '
     end
     end
    result = result..'[['..item.name..']]'
    result = result..'||data-sort-value="'..item.smithingLevel..'"|'..Icons._SkillReq('Smithing', item.smithingLevel)
    result = result..'||'..item.smithingXP
    local totalValue = item.sellsFor * qty
    result = result..'||data-sort-value="'..totalValue..'"|'..Icons.GP(item.sellsFor)
    if qty > 1 then
      result = result..' (x'..qty..')'
    end
    result = result..'||'
    local barQty = 0
    for i, mat in Shared.skpairs(item.smithReq) do
      matItem = Items.getItemByID(mat.id)
      if i > 1 then result = result..', ' end
      result = result..Icons.Icon({matItem.name, type='item', qty=mat.qty, notext=true})
      if bar ~= nil and mat.id == bar.id then
        barQty = mat.qty
      end
    end
    --Add the data for the value per bar
    if bar ~= nil then
      if barQty == 0 then
        result = result..'||data-sort-value="0"|N/A'
      else
        local barVal = totalValue / barQty
        result = result..'||data-sort-value="'..barVal..'"|'..Icons.GP(Shared.round(barVal, 1, 1))
      end
    end
  end
  result = result..'\r\n|}'
  return result
end
function p.getFiremakingTable(frame)
  local resultPart = {}
  table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
  table.insert(resultPart, '\r\n|-class="headerRow-0"')
  table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level')
  table.insert(resultPart, '!!rowspan="2"|Burn Time!!colspan="2"|Without Bonfire!!colspan="2"|With Bonfire!!rowspan="2"|Bonfire Bonus!!rowspan="2"|Bonfire Time')
  table.insert(resultPart, '\r\n|-class="headerRow-1"')
  table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')
  for i, logData in Shared.skpairs(SkillData.Firemaking) do
    local name = Shared.titleCase(logData.type..' Logs')
    local burnTime = logData.interval / 1000
    local XPS = logData.xp / burnTime
    local XP_BF = logData.xp * (1 + logData.bonfireBonus / 100)
    local XPS_BF = XP_BF / burnTime
    table.insert(resultPart, '\r\n|-')
    table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
    table.insert(resultPart, '||[['..name..']]')
    table.insert(resultPart, '||style ="text-align: right;"|'..logData.level)
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true))
    table.insert(resultPart, '||style ="text-align: right;"|'..logData.xp)
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.round(XPS, 2, 2))
    table.insert(resultPart, '||style ="text-align: right;"|'..Shared.round(XP_BF, 2, 0))
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.round(XPS_BF, 2, 2))
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..logData.bonfireBonus..'"|'..logData.bonfireBonus..'%')
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..logData.bonfireInterval..'"|'..Shared.timeString(logData.bonfireInterval / 1000, true))
  end


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


return p
return p

Latest revision as of 22:57, 15 October 2024

Data pulled from Module:GameData

Some skills have their own modules:

Also be aware of:


--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 Common = require('Module:Common')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Modifiers = require('Module:Modifiers')
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local Num = require('Module:Number')

-- 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',
		["Archaeology"] = 'digSites',
		["Harvesting"] = 'veinData'
	}
	return recipeIDs[localSkillID]
end

-- Given a skill ID & recipe, returns the skill level requirement for
-- that recipe and a boolean value indicating whether the level if abyssal or not.
-- If the level could not be determined, then the return value is nil, nil
function p.getRecipeLevelRealm(skillID, recipe)
	local level, isAbyssal = nil, nil
	-- Convert skillID to local ID if not already
	local ns, localSkillID = GameData.getLocalID(skillID)
	local realmID = p.getRecipeRealm(recipe)
	if localSkillID == 'Agility' then
		local course = GameData.getEntityByProperty(SkillData.Agility.courses, 'realm', realmID)
		isAbyssal = (realmID == 'melvorItA:Abyssal')
		-- For Agility, level is derived from obstacle category
		if recipe.category ~= nil then
			-- Obstacle
			local slot = course.obstacleSlots[recipe.category + 1]
			if isAbyssal then
				level = slot.abyssalLevel
			else
				level = slot.level
			end
		elseif recipe.slot ~= nil then
			-- Pillar
			level = course.pillarSlots[recipe.slot + 1].level
		end
	elseif recipe.abyssalLevel ~= nil then
		level, isAbyssal = recipe.abyssalLevel, true
	else
		-- For all other skills, the recipe should have a level property
		level, isAbyssal = recipe.level, false
	end
	return level, isAbyssal
end

function p.getRecipeLevel(skillID, recipe)
	local level, isAbyssal = p.getRecipeLevelRealm(skillID, recipe)
	return level
end

function p.getRecipeRequirementText(skillName, recipe)
	local reqText = {}
	if recipe.abyssalLevel ~= nil and recipe.abyssalLevel > 0 then
		table.insert(reqText, Icons._SkillReq(skillName, recipe.abyssalLevel, false, 'melvorItA:Abyssal'))
	elseif recipe.level ~= nil and recipe.level > 0 then
		table.insert(reqText, Icons._SkillReq(skillName, recipe.level, false))
	end
	if recipe.totalMasteryRequired ~= nil then
		table.insert(reqText, Num.formatnum(recipe.totalMasteryRequired) .. ' ' .. Icons.Icon({skillName, type='skill', notext=true}) .. ' ' .. Icons.Icon({'Mastery'}))
	end
	local reqsData = {}
	if type(recipe.requirements) == 'table' then
		reqsData = Shared.shallowClone(recipe.requirements)
	end
	if recipe.shopItemPurchased ~= nil then
		-- Mining requirements are stored differently than other skills like
		-- Woodcutting, standardize here
		table.insert(reqsData, {
			["type"] = 'ShopPurchase',
			["purchaseID"] = recipe.shopItemPurchased,
			["count"] = 1
		})
	end
	if not Shared.tableIsEmpty(reqsData) then
		local reqs = Common.getRequirementString(reqsData)
		if reqs ~= nil then
			table.insert(reqText, reqs)
		end
	end
	return table.concat(reqText, '<br/>')
end

function p.standardRecipeSort(skillID, recipeA, recipeB)
	local levelA, isAbyssalA = p.getRecipeLevelRealm(skillID, recipeA)
	local levelB, isAbyssalB = p.getRecipeLevelRealm(skillID, recipeB)
	local isAbyssalNumA, isAbyssalNumB = (isAbyssalA and 1) or 0, (isAbyssalB and 1) or 0

	return (isAbyssalA == isAbyssalB and levelA < levelB) or isAbyssalNumA < isAbyssalNumB
end


function p.getRecipeRealm(recipe)
	return recipe.realm or 'melvorD:Melvor'
end

function p.getRealmFromName(realmName)
	local realm = nil
	if realmName == nil or realmName == '' then
		-- Default realm
		realm = GameData.getEntityByID('realms', 'melvorD:Melvor')
	else
		realm = GameData.getEntityByName('realms', realmName)
	end
	return realm
end

function p.getMasteryActionCount(skillID, realmID, levelLimit)
	local actCount = 0
	local skillNS, skillLocalID = Shared.getLocalID(skillID)
	local skillData = SkillData[skillLocalID]
	local recipeKey = p.getSkillRecipeKey(skillLocalID)
	if recipeKey ~= nil then
		local recipeData = skillData[recipeKey]
		for i, recipe in ipairs(recipeData) do
			if (
				p.getRecipeRealm(recipe) == realmID
				and (recipe.noMastery == nil or not recipe.noMastery)
				and (levelLimit == nil or p.getRecipeLevel(skillLocalID, recipe) <= levelLimit)
			) then
				actCount = actCount + 1
			end
		end
	end
	return actCount
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, abyssalLevel = npc.abyssalLevel, 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, abyssalLevel = npc.abyssalLevel, 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, abyssalLevel = npc.abyssalLevel, 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 = Num.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 = Num.round2(1/(drop.chance/100), 0), level = npc.level, abyssalLevel = npc.abyssalLevel, 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

-- Combines Astrology constellation modifiers into an object similar to other entities,
-- and multiplies the values up to their maximum possible amount
function p._getConstellationModifiers(cons)
	local result = {}
	local modKeys = { 'standardModifiers', 'uniqueModifiers', 'abyssalModifiers' }

	for _, keyID in ipairs(modKeys) do
		result[keyID] = {}
		local mods = cons[keyID]
		if mods ~= nil then
			for _, mod in ipairs(mods) do
				local newModObj = {}
				local multValue = mod.maxCount
				local subKeys = { 'modifiers', 'enemyModifiers' }
				for _, subKey in ipairs(subKeys) do
					local modAdj = Shared.clone(mod[subKey])
					if type(modAdj) == 'table' then
						for modName, modValueDef in pairs(modAdj) do
							if type(modValueDef) == 'table' then
								if modValueDef[1] ~= nil then
									-- Table of multiple values
									for i, subValue in ipairs(modValueDef) do
										if type(subValue) == 'table' and subValue.value ~= nil then
											subValue.value = subValue.value * multValue
										elseif type(subValue) == 'number' then
											modValueDef[i] = subValue * multValue
										end
									end
								elseif modValueDef.value ~= nil then
									-- Table but with a single value
									modValueDef.value = modValueDef.value * multValue
								end
							elseif type(modValueDef) == 'number' then
								-- Single value
								modAdj[modName] = modValueDef * multValue
							end
						end
						newModObj[subKey] = modAdj
					end
				end
				table.insert(result[keyID], newModObj)
			end
		end
	end
	return result
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.shallowClone(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 args = frame.args ~= nil and frame.args or frame
	local skillName = args[1]
	local realmName = args.realm

	local realm = p.getRealmFromName(realmName)
	if realm == nil then
		return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
	end
	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].masteryPoolBonuses
	if checkpoints == nil then
		return Shared.printError('Failed to find Mastery checkpoint data for ' .. skillName)
	end

	local totalPoolXP = p.getMasteryActionCount(localSkillID, realm.id) * 500000
	local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
	for i, checkpointData in ipairs(checkpoints) do
		if checkpointData.realm == realm.id then
			local chkDesc = Modifiers.getModifiersText(checkpointData.modifiers, false, false)
			local chkPercent = checkpointData.percent
			result = result..'\r\n|-'
			result = result..'\r\n|'..chkPercent..'%||'
			result = result..Num.formatnum(math.floor(totalPoolXP * chkPercent / 100))..' xp||'..chkDesc
		end
	end
	result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
	result = result..'\r\n|'..Num.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

	local tokens = Items.getItems(function(item) return item.itemType == 'MasteryToken' end)
	local tokenItems = {}
	for _, item in ipairs(tokens) do
		if item.realm == 'melvorD:Melvor' and item.skill ~= nil then
			local skillNS, skillLocalID = Shared.getLocalID(item.skill)
			tokenItems[skillLocalID] = item
		end
	end

	-- Iterate over each skill with mastery, determining the number of
	-- mastery actions for each
	for skillLocalID, skill in pairs(SkillData) do
		if skill.masteryPoolBonuses ~= nil then
			local actCount = { ["skill"] = skill, ["token"] = tokenItems[skillLocalID] }
			for i, levelDef in ipairs(skillLevels) do
				actCount[levelDef.id] = p.getMasteryActionCount(skillLocalID, 'melvorD:Melvor', levelDef.level)
			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 = rowData.token
		table.insert(resultPart, '\n|-')
		local tokenImg = (token == nil and '?') or Icons.Icon({token.name, type='item', notext=true})
		table.insert(resultPart, '\n|style="text-align:center"|' .. tokenImg)
		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 = Num.round(baseTokenChance / (actCount * (1 + CCI.modifiers.offItemChance / 100)), 0, 0)
			end
			table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Num.formatnum(denom))
			table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denomCCI .. '"|1/' .. Num.formatnum(denomCCI))
		end
	end
	table.insert(resultPart, '\n|}')

	return table.concat(resultPart)
end

function p.getFiremakingTable(frame)
    local args = frame.args ~= nil and frame.args or frame
    local realmName = args.realm
    local realm = p.getRealmFromName(realmName)
    if realm == nil then
        return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
    end
    local skillID = 'Firemaking'

    local tableHtml = mw.html.create('table')
        :addClass('wikitable sortable stickyHeader')
    
    local headerRow0 = tableHtml:tag('tr'):addClass('headerRow-0')
    headerRow0:tag('th'):attr('colspan', '2')
                        :attr('rowspan', '2')
                        :wikitext('Logs')
    headerRow0:tag('th'):attr('rowspan', '2')
                        :wikitext(Icons._SkillRealmIcon('Firemaking', realm.id) .. '<br>Level')
    headerRow0:tag('th'):attr('rowspan', '2')
                        :wikitext('[[DLC]]')
    headerRow0:tag('th'):attr('rowspan', '2')
                        :wikitext('Burn<br>Time')
    headerRow0:tag('th'):attr('colspan', '2')
                        :wikitext('Without Bonfire')
    headerRow0:tag('th'):attr('colspan', '2')
                        :wikitext('With Bonfire')
    headerRow0:tag('th'):attr('rowspan', '2')
                        :wikitext('Bonfire<br>Bonus')
    headerRow0:tag('th'):attr('rowspan', '2')
                        :wikitext('Bonfire<br>Time')

    local headerRow1 = tableHtml:tag('tr'):addClass('headerRow-1')
    headerRow1:tag('th'):wikitext('XP')
    headerRow1:tag('th'):wikitext('XP/s')
    headerRow1:tag('th'):wikitext('XP')
    headerRow1:tag('th'):wikitext('XP/s')

    local logsData = GameData.getEntities(SkillData.Firemaking.logs, function(obj)
        return p.getRecipeRealm(obj) == realm.id
    end)
    table.sort(logsData, function(a, b) return p.standardRecipeSort(skillID, a, b) end)
    
    for i, logData in ipairs(logsData) do
        local logs = Items.getItemByID(logData.logID)
        local name = logs.name
        local level = p.getRecipeLevel(skillID, logData)
        local baseXP = logData.baseAbyssalExperience or logData.baseExperience
        local reqText = p.getRecipeRequirementText(SkillData.Firemaking.name, logData)
        local bonfireBonus = logData.bonfireAXPBonus or logData.bonfireXPBonus
        local burnTime = logData.baseInterval / 1000
        local bonfireTime = logData.baseBonfireInterval / 1000
        local XPS = baseXP / burnTime
        local XP_BF = baseXP * (1 + bonfireBonus / 100)
        local XPS_BF = Num.round(XP_BF / burnTime, 2, 2)
        XP_BF = Num.round(XP_BF, 2, 0)

        local row = tableHtml:tag('tr')
        row:tag('td'):attr('data-sort-value', name)
                     :wikitext(Icons.Icon({name, type='item', notext=true}))
        row:tag('td'):wikitext('[[' .. name .. ']]')
        row:tag('td'):css('text-align', 'center')
                     :wikitext(level)
        row:tag('td'):css('text-align', 'center')
        		     :attr('data-sort-value', Icons.getExpansionID(logData.logID))
        		     :wikitext(Icons.getDLCColumnIcon(logData.logID))
        row:tag('td'):css('text-align', 'right')
                     :attr('data-sort-value', burnTime)
                     :wikitext(Shared.timeString(burnTime, true))
        row:tag('td'):css('text-align', 'right')
                     :attr('data-sort-value', baseXP)
                     :wikitext(Num.formatnum(baseXP))
        row:tag('td'):css('text-align', 'right')
                     :attr('data-sort-value', XPS)
                     :wikitext(Num.formatnum(Num.round(XPS, 2, 2)))
        
        if bonfireBonus == 0 then
            row:tag('td'):attr('colspan', '4')
                         :addClass('table-na')
                         :wikitext('N/A')
        else
            row:tag('td'):css('text-align', 'right')
                         :attr('data-sort-value', XP_BF)
                         :wikitext(Num.formatnum(XP_BF))
            row:tag('td'):css('text-align', 'right')
                         :attr('data-sort-value', XPS_BF)
                         :wikitext(Num.formatnum(XPS_BF, 2, 2))
            row:tag('td'):css('text-align', 'right')
                         :attr('data-sort-value', bonfireBonus)
                         :wikitext(bonfireBonus .. '%')
            row:tag('td'):css('text-align', 'right')
                         :attr('data-sort-value', bonfireTime)
                         :wikitext(Shared.timeString(bonfireTime, true))
        end
    end

    return tostring(tableHtml)
end

return p