Module:Skills: Difference between revisions

9,177 bytes added ,  15 October 2024
Add Archaeology recipe key
(Added value per bar to the Smithing tables)
(Add Archaeology recipe key)
(85 intermediate revisions by 6 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 and Module:Prayer being the current two examples.
--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 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
 
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
end


function p.getSkillName(skillID)
function p.standardRecipeSort(skillID, recipeA, recipeB)
  for skName, ID in Shared.skpairs(Constants.skill) do
local levelA, isAbyssalA = p.getRecipeLevelRealm(skillID, recipeA)
    if ID == skillID then
local levelB, isAbyssalB = p.getRecipeLevelRealm(skillID, recipeB)
      return skName
local isAbyssalNumA, isAbyssalNumB = (isAbyssalA and 1) or 0, (isAbyssalB and 1) or 0
    end
 
  end
return (isAbyssalA == isAbyssalB and levelA < levelB) or isAbyssalNumA < isAbyssalNumB
  return nil
end
end


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


  local unlockTable = SkillData.MasteryUnlocks[skillID]
function p.getRecipeRealm(recipe)
  if unlockTable == nil then
return recipe.realm or 'melvorD:Melvor'
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
end
  end


  local result = '{|class="wikitable"\r\n!Level!!Unlock'
function p.getRealmFromName(realmName)
  for i, unlock in Shared.skpairs(unlockTable) do
local realm = nil
    result = result..'\r\n|-'
if realmName == nil or realmName == '' then
    result = result..'\r\n|'..unlock.level..'||'..unlock.unlock
-- Default realm
  end
realm = GameData.getEntityByID('realms', 'melvorD:Melvor')
  result = result..'\r\n|}'
else
  return result
realm = GameData.getEntityByName('realms', realmName)
end
return realm
end
end


function p.getMasteryCheckpointTable(frame)
function p.getMasteryActionCount(skillID, realmID, levelLimit)
  local skillName = frame.args ~= nil and frame.args[1] or frame
local actCount = 0
  local skillID = p.getSkillID(skillName)
local skillNS, skillLocalID = Shared.getLocalID(skillID)
  if skillID == nil then
local skillData = SkillData[skillLocalID]
    return "ERROR: Failed to find a skill ID for "..skillName
local recipeKey = p.getSkillRecipeKey(skillLocalID)
  end
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


  if SkillData.MasteryCheckpoints[skillID] == nil then
-- Thieving
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
function p.getThievingNPCByID(npcID)
  end
return GameData.getEntityByID(SkillData.Thieving.npcs, npcID)
end


  local bonuses = SkillData.MasteryCheckpoints[skillID].bonuses
function p.getThievingNPC(npcName)
  local totalPoolXP = SkillData.MasteryPoolXP[skillID + 1]
return GameData.getEntityByName(SkillData.Thieving.npcs, npcName)
end


  local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
function p.getThievingNPCArea(npc)
  for i, bonus in Shared.skpairs(bonuses) do
for i, area in ipairs(SkillData.Thieving.areas) do
    result = result..'\r\n|-'
for j, npcID in ipairs(area.npcIDs) do
    result = result..'\r\n|'..(MasteryCheckpoints[i] * 100)..'%||'
if npcID == npc.id then
    result = result..Shared.formatnum(totalPoolXP * MasteryCheckpoints[i])..' xp||'..bonus
return area
  end
end
  result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
end
  result = result..'\r\n|'..Shared.formatnum(totalPoolXP)
end
  result = result..'\r\n|}'
  return result
end
end


function p._getFarmingTable(category)
function p._getThievingNPCStat(npc, statName)
  local seedList = {}
local result = nil
  if category == 'Allotment' or category == 'Herb' or category == 'Tree' then
    seedList = Items.getItems(function(item) return item.tier == category end)
  else
    return 'ERROR: Invalid farming category. Please choose Allotment, Herb, or Tree'
  end


  local result = '{|class="wikitable sortable stickyHeader"'
if statName == 'level' then
  result = result..'\r\n|- class="headerRow-0"'
result = Icons._SkillReq('Thieving', npc.level)
  result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
elseif statName == 'maxHit' then
  result = result..'!!XP!!Growth Time!!Seed Value'
result = npc.maxHit * 10
  if category == 'Allotment' then
elseif statName == 'area' then
    result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
local area = p.getThievingNPCArea(npc)
  elseif category == 'Herb' then
result = area.name
    result = result..'!!colspan="2"|Herb!!Herb Value'
else
  elseif category == 'Tree' then
result = npc[statName]
    result = result..'!!colspan="2"|Logs!!Log Value'
end
  end
  result = result..'!!Seed Sources'
 
  table.sort(seedList, function(a, b) return a.farmingLevel < b.farmingLevel end)


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


    local crop = Items.getItemByID(seed.grownItemID)
function p.getThievingNPCStat(frame)
    result = result..'||'..Icons.Icon({crop.name, type='item', size='50', notext=true})..'||[['..crop.name..']]'
local npcName = frame.args ~= nil and frame.args[1] or frame[1]
    if category == 'Allotment' then
local statName = frame.args ~= nil and frame.args[2] or frame[2]
      result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..(crop.healsFor * 10)
local npc = p.getThievingNPC(npcName)
    end
if npc == nil then
    result = result..'||data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
return Shared.printError('Invalid Thieving NPC ' .. npcName)
    result = result..'||'..ItemSourceTables._getItemSources(seed)
end
  end


  result = result..'\r\n|}'
return p._getThievingNPCStat(npc, statName)
  return result
end
end


function p.getFarmingTable(frame)
function p.getThievingSourcesForItem(itemID)
  local category = frame.args ~= nil and frame.args[1] or frame
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 p._getFarmingTable(category)
return resultArray
end
end


function p.getMiningTable(frame)
-- Astrology
  local result = '{|class="wikitable sortable stickyHeader"'
function p.getConstellationByID(constID)
  result = result..'\r\n|- class="headerRow-0"'
return GameData.getEntityByID(SkillData.Astrology.recipes, constID)
  result = result..'\r\n!colspan=2|Ore!!'..Icons.Icon({'Mining', type='skill', notext=true})..' Level'
end
  result = result..'!!XP!!Respawn Time!!Ore Value'
  local mineData = Shared.clone(SkillData.Mining)
  table.sort(mineData, function(a, b) return a.level < b.level end)
  for i, oreData in Shared.skpairs(mineData) do
    local ore = Items.getItemByID(oreData.ore)
    result = result..'\r\n|-\r\n|'..Icons.Icon({ore.name, type='item', size='50', notext=true})..'||'..ore.name
    result = result..'||style="text-align:right"|'..oreData.level..'||style="text-align:right"|'..ore.miningXP
    result = result..'||style="text-align:right" data-sort-value="'..oreData.respawnInterval..'"|'
    result = result..Shared.timeString(oreData.respawnInterval / 1000, true)
    result = result..'||data-sort-value="'..ore.sellsFor..'"|'..Icons.GP(ore.sellsFor)
  end


  result = result..'\r\n|}'
function p.getConstellation(constName)
  return result
return GameData.getEntityByName(SkillData.Astrology.recipes, constName)
end
end


function p.getPotionNavbox(frame)
function p.getConstellations(checkFunc)
  --•
return GameData.getEntities(SkillData.Astrology.recipes, checkFunc)
  local result = '{| class="wikitable" style="margin:auto; clear:both; width: 100%"'
end
  result = result..'\r\n!colspan=2|'..Icons.Icon({'Herblore', 'Potions', type='skill'})


  local CombatPots = {}
-- Combines Astrology constellation modifiers into an object similar to other entities,
  local SkillPots = {}
-- and multiplies the values up to their maximum possible amount
  for i, potData in Shared.skpairs(SkillData.Herblore.ItemData) do
function p._getConstellationModifiers(cons)
    if potData.category == 0 then
local result = {}
      table.insert(CombatPots, Icons.Icon({potData.name, type='item', img=(potData.name..' I')}))
local modKeys = { 'standardModifiers', 'uniqueModifiers', 'abyssalModifiers' }
    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;"'
for _, keyID in ipairs(modKeys) do
  result = result..'|'..table.concat(CombatPots, ' ')
result[keyID] = {}
  result = result..'\r\n|-\r\n!Skill Potions\r\n|class="center" style="vertical-align:middle;"'
local mods = cons[keyID]
  result = result..'|'..table.concat(SkillPots, ' ')
if mods ~= nil then
  result = result..'\r\n|}'
for _, mod in ipairs(mods) do
  return result
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
end


function p.getSpecialFishingTable(frame)
-- Mastery
  local lootValue = 0
function p.getMasteryUnlockTable(frame)
  local totalWt = Items.specialFishWt
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 result = ''
local _, localSkillID = GameData.getLocalID(skillID)
  result = result..'\r\n{|class="wikitable sortable"'
-- Clone so that we can sort by level
  result = result..'\r\n!Item'
local unlockTable = Shared.shallowClone(SkillData[localSkillID].masteryLevelUnlocks)
  result = result..'!!Price!!colspan="2"|Chance'
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)


  --Sort the loot table by weight in descending order
local result = '{|class="wikitable"\r\n!Level!!Unlock'
  table.sort(Items.specialFishLoot, function(a, b) return a[2] > b[2] end)
for i, unlock in ipairs(unlockTable) do
  for i, row in pairs(Items.specialFishLoot) do
result = result..'\r\n|-'
    local thisItem = Items.getItemByID(row[1])
result = result..'\r\n|'..unlock.level..'||'..unlock.description
    result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
end
    result = result..'||style="text-align:left" data-sort-value="'..thisItem.sellsFor..'"'
result = result..'\r\n|}'
    result = result..'|'..Icons.GP(thisItem.sellsFor)
return result
end


    local dropChance = (row[2] / totalWt) * 100
function p.getMasteryCheckpointTable(frame)
    result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
local args = frame.args ~= nil and frame.args or frame
    result = result..'|'..Shared.fraction(row[2], totalWt)
local skillName = args[1]
    result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
local realmName = args.realm
    lootValue = lootValue + (dropChance * 0.01 * thisItem.sellsFor)
  end
  result = result..'\r\n|}'
  result = result..'\r\nThe average value of a roll on the special fishing loot table is '..Icons.GP(Shared.round(lootValue, 2, 0))


  return result
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
end


function p.getSmithingTable(frame)
function p.getMasteryTokenTable()
  local tableType = frame.args ~= nil and frame.args[1] or frame
-- Defines which skill levels should be included within the output
  local bar = nil
local skillLevels = {
  if tableType ~= 'Smelting' then
{
    bar = Items.getItem(tableType)
["id"] = 'Base',
    if bar == nil then
["level"] = 99,
      return 'ERROR: Could not find an item named '..tableType..' to build a smithing table with'
["description"] = '[[Full Version|Base Game]] (Level 99)'
    elseif bar.type ~= 'Bar' then
}, {
      return 'ERROR: '..tableType.." is not a bar and thus can't be used for smithing"
["id"] = 'TotH',
    end
["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
 
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)


  local smithList = {}
-- Generate header
  for i, item in pairs(ItemData.Items) do
table.insert(resultPart, '{| class="wikitable sortable"')
    if item.smithingLevel ~= nil then
table.insert(resultPart, '\n!rowspan="3"|Token!!rowspan="3"|Skill!!colspan="' .. columnPairs * 2 .. '"|Approximate Mastery Token Chance')
      if tableType == 'Smelting' then
table.insert(resultPart, '\n|-')
        if item.type == 'Bar' then
for i, levelDef in ipairs(skillLevels) do
          table.insert(smithList, item)
table.insert(resultPart, '\n!colspan="2"| ' .. levelDef.description)
        end
end
      else
table.insert(resultPart, '\n|-' .. string.rep('\n!Without ' .. CCIIcon .. '\n!With ' .. CCIIcon, columnPairs))
        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"'
for i, rowData in ipairs(masteryActionCount) do
  result = result..'\r\n|-class="headerRow-0"'
local token = rowData.token
  result = result..'\r\n!Item!!Name!!'..Icons.Icon({'Smithing', type='skill', notext=true})..' Level!!XP!!Value!!Ingredients'
table.insert(resultPart, '\n|-')
  --Adding value/bar for things other than smelting
local tokenImg = (token == nil and '?') or Icons.Icon({token.name, type='item', notext=true})
  if bar ~= nil then result = result..'!!Value/Bar' end
table.insert(resultPart, '\n|style="text-align:center"|' .. tokenImg)
table.insert(resultPart, '\n|' .. Icons.Icon({rowData.skill.name, type='skill'}))


  table.sort(smithList, function(a, b)
for j, levelDef in ipairs(skillLevels) do
                          if a.smithingLevel ~= b.smithingLevel then
local actCount = rowData[levelDef.id]
                            return a.smithingLevel < b.smithingLevel
local denom, denomCCI = 0, 0
                          else
if actCount > 0 then
                            return a.name < b.name
denom = math.floor(baseTokenChance / actCount)
                          end end)
denomCCI = Num.round(baseTokenChance / (actCount * (1 + CCI.modifiers.offItemChance / 100)), 0, 0)
  for i, item in Shared.skpairs(smithList) do
end
    result = result..'\r\n|-'
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Num.formatnum(denom))
    result = result..'\r\n|'..Icons.Icon({item.name, type='item', size='50', notext=true})..'||'
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denomCCI .. '"|1/' .. Num.formatnum(denomCCI))
    local qty = item.smithingQty ~= nil and item.smithingQty or 1
end
    if qty > 1 then
end
      result = result..item.smithingQty..'x '
table.insert(resultPart, '\n|}')
    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, 2, 2))
      end
    end
  end


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


function p.getFiremakingTable(frame)
function p.getFiremakingTable(frame)
  local result = '{| class="wikitable sortable stickyHeader"'
    local args = frame.args ~= nil and frame.args or frame
  result = result..'\r\n|-class="headerRow-0"'
    local realmName = args.realm
  result = result..'\r\n!colspan="2"|Logs!!'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level'
    local realm = p.getRealmFromName(realmName)
  result = result..'!!XP!!Burn Time!!XP/s!!Bonfire Bonus!!Bonfire Time'
    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)


  for i, logData in Shared.skpairs(SkillData.Firemaking) do
        local row = tableHtml:tag('tr')
    result = result..'\r\n|-'
        row:tag('td'):attr('data-sort-value', name)
    local name = Shared.titleCase(logData.type..' Logs')
                    :wikitext(Icons.Icon({name, type='item', notext=true}))
    result = result..'\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true})
        row:tag('td'):wikitext('[[' .. name .. ']]')
    result = result..'||'..name
        row:tag('td'):css('text-align', 'center')
    result = result..'||style ="text-align: right;"|'..logData.level
                    :wikitext(level)
    result = result..'||style ="text-align: right;"|'..logData.xp
        row:tag('td'):css('text-align', 'center')
    local burnTime = logData.interval / 1000
            :attr('data-sort-value', Icons.getExpansionID(logData.logID))
    local XPS = logData.xp / burnTime
            :wikitext(Icons.getDLCColumnIcon(logData.logID))
    result = result..'||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true)
        row:tag('td'):css('text-align', 'right')
    result = result..'||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.round(XPS, 2, 2)
                    :attr('data-sort-value', burnTime)
    result = result..'||style ="text-align: right;" data-sort-value="'..logData.bonfireBonus..'"|'..logData.bonfireBonus..'%'
                    :wikitext(Shared.timeString(burnTime, true))
    result = result..'||style ="text-align: right;" data-sort-value="'..logData.bonfireInterval..'"|'..Shared.timeString(logData.bonfireInterval / 1000, true)
        row:tag('td'):css('text-align', 'right')
  end
                    :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


  result = result..'\r\n|}'
    return tostring(tableHtml)
  return result
end
end


return p
return p