Anonymous

Module:Skills: Difference between revisions

From Melvor Idle
3,365 bytes added ,  27 December 2022
Use printError function
(Removed redundant references to getSkillID since that's now a Constants function)
(Use printError function)
(23 intermediate revisions by 2 users not shown)
Line 1: Line 1:
--This module should avoid including skill specific functions which generate
--output for wiki pages, especially those which require() other modules. For
--these functions, consider using the appropriate module from the below list.
--Some skills have their own modules:
--Some skills have their own modules:
--Module:Magic for Magic
--Module:Magic for Magic
Line 11: Line 15:


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


local Shared = require('Module:Shared')
local Shared = require('Module:Shared')
local Constants = require('Module:Constants')
local Constants = require('Module:Constants')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Items = require('Module:Items')
local Items = require('Module:Items')
local ItemSourceTables = require('Module:Items/SourceTables')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')


local MasteryCheckpoints = {.1, .25, .5, .95}
-- Thieving
function p.getThievingNPCByID(npcID)
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.getMasteryUnlockTable(frame)
function p._getThievingNPCStat(npc, statName)
  local skillName = frame.args ~= nil and frame.args[1] or frame
local result = nil
  local skillID = Constants.getSkillID(skillName)
 
  if skillID == nil then
if statName == 'level' then
    return "ERROR: Failed to find a skill ID for "..skillName
result = Icons._SkillReq('Thieving', npc.level)
  end
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


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


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


function p.getMasteryCheckpointTable(frame)
function p.getThievingNPCStat(frame)
  local skillName = frame.args ~= nil and frame.args[1] or frame
local npcName = frame.args ~= nil and frame.args[1] or frame[1]
  local skillID = Constants.getSkillID(skillName)
local statName = frame.args ~= nil and frame.args[2] or frame[2]
  if skillID == nil then
local npc = p.getThievingNPC(npcName)
    return "ERROR: Failed to find a skill ID for "..skillName
if npc == nil then
  end
return Shared.printError('Invalid Thieving NPC ' .. npcName)
end
 
return p._getThievingNPCStat(npc, statName)
end


  if SkillData.MasteryCheckpoints[skillID] == nil then
function p.getThievingSourcesForItem(itemID)
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
local resultArray = {}
  end
local areaNPCs = {}


  local bonuses = SkillData.MasteryCheckpoints[skillID].bonuses
--First check area unique drops
  local totalPoolXP = SkillData.MasteryPoolXP[skillID + 1]
--If an area drops the item, add all the NPC ids to the list so we can add them later
for i, area in pairs(SkillData.Thieving.areas) do
for j, drop in pairs(area.uniqueDrops) do
if drop.id == itemID then
for k, npcID in ipairs(area.npcIDs) do
areaNPCs[npcID] = drop.quantity
end
break
end
end
end


  local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
--Now go through and get drop chances on each NPC if needed
  for i, bonus in Shared.skpairs(bonuses) do
for i, npc in pairs(SkillData.Thieving.npcs) do
    result = result..'\r\n|-'
local totalWt = 0
    result = result..'\r\n|'..(MasteryCheckpoints[i] * 100)..'%||'
local dropWt = 0
    result = result..Shared.formatnum(totalPoolXP * MasteryCheckpoints[i])..' xp||'..bonus
local dropQty = { min = 0, max = 0 }
  end
for j, drop in ipairs(npc.lootTable) do
  result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
totalWt = totalWt + drop.weight
  result = result..'\r\n|'..Shared.formatnum(totalPoolXP)
if drop.itemID == itemID then
  result = result..'\r\n|}'
dropWt = drop.weight
  return result
dropQty = { min = drop.minQuantity, max = drop.maxQuantity }
end
end
end
if dropWt > 0 then
table.insert(resultArray, {npc = npc.name, minQty = dropQty.min, maxQty = dropQty.max, wt = dropWt * SkillData.Thieving.itemChance, totalWt = totalWt * 100, level = npc.level, npcID = npc.id})
end


function p._getFarmingTable(category)
--Chance of -1 on unique drops is to indicate variable chance
  local seedList = {}
if npc.uniqueDrop ~= nil and npc.uniqueDrop.id == itemID then
  if category == 'Allotment' or category == 'Herb' or category == 'Tree' then
table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.quantity, maxQty = npc.uniqueDrop.quantity, wt = -1, totalWt = -1, level = npc.level, npcID = npc.id})
    seedList = Items.getItems(function(item) return item.tier == category end)
end
  else
    return 'ERROR: Invalid farming category. Please choose Allotment, Herb, or Tree'
  end


  local result = '{|class="wikitable sortable stickyHeader"'
if areaNPCs[npc.id] ~= nil then
  result = result..'\r\n|- class="headerRow-0"'
table.insert(resultArray, {npc = npc.name, minQty = areaNPCs[npc.id], maxQty = areaNPCs[npc.id], wt = SkillData.Thieving.baseAreaUniqueChance, totalWt = 100, level = npc.level, npcID = npc.id})
  result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
end
  result = result..'!!XP!!Growth Time!!Seed Value'
end
  if category == 'Allotment' then
    result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
  elseif category == 'Herb' then
    result = result..'!!colspan="2"|Herb!!Herb Value'
  elseif category == 'Tree' then
    result = result..'!!colspan="2"|Logs!!Log Value'
  end
  result = result..'!!Seed Sources'
 
  table.sort(seedList, function(a, b) return a.farmingLevel < b.farmingLevel end)


  for i, seed in pairs(seedList) do
for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
    result = result..'\r\n|-'
if drop.itemID == itemID then
    result = result..'\r\n|'..Icons.Icon({seed.name, type='item', size='50', notext=true})..'||[['..seed.name..']]'
if drop.npcs == nil then
    result = result..'||'..seed.farmingLevel..'||'..Shared.formatnum(seed.farmingXP)
table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = 1, npcID = itemID})
    result = result..'||data-sort-value="'..seed.timeToGrow..'"|'..Shared.timeString(seed.timeToGrow, true)
else
    result = result..'||data-sort-value="'..seed.sellsFor..'"|'..Icons.GP(seed.sellsFor)
for j, npcID in ipairs(drop.npcs) do
local npc = p.getThievingNPCByID(npcID)
if npc ~= nil then
table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = npc.level, npcID = npc.id})
end
end
end
end
end


    local crop = Items.getItemByID(seed.grownItemID)
return resultArray
    result = result..'||'..Icons.Icon({crop.name, type='item', size='50', notext=true})..'||[['..crop.name..']]'
end
    if category == 'Allotment' then
      result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..(crop.healsFor * 10)
    end
    result = result..'||data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
    result = result..'||'..ItemSourceTables._getItemSources(seed)
  end


  result = result..'\r\n|}'
-- Astrology
  return result
function p.getConstellationByID(constID)
return GameData.getEntityByID(SkillData.Astrology.recipes, constID)
end
end


function p.getFarmingTable(frame)
function p.getConstellation(constName)
  local category = frame.args ~= nil and frame.args[1] or frame
return GameData.getEntityByName(SkillData.Astrology.recipes, constName)
end


  return p._getFarmingTable(category)
function p.getConstellations(checkFunc)
return GameData.getEntities(SkillData.Astrology.recipes, checkFunc)
end
end


function p.getFarmingFoodTable(frame)
-- For a given constellation cons and modifier value modValue, generates and returns
  local result = '{| class="wikitable sortable stickyHeader"'
-- a table of modifiers, much like any other item/object elsewhere in the game.
  result = result..'\r\n|- class="headerRow-0"'
-- includeStandard: true|false, determines whether standard modifiers are included
  result = result..'\r\n!colspan="2"|Crop!!'..Icons.Icon({"Farming", type="skill", notext=true})..' Level'
-- includeUnique: true|false, determines whether unique modifiers are included
  result = result..'!!Healing!!Value'
-- isDistinct: true|false, if true, the returned list of modifiers is de-duplicated
 
-- asKeyValue: true|false, if true, returns key/value pairs like usual modifier objects
  local itemArray = Items.getItems(function(item) return item.grownItemID ~= nil end)
function p._buildAstrologyModifierArray(cons, modValue, includeStandard, includeUnique, isDistinct, asKeyValue)
-- Temporary function to determine if the table already contains a given modifier
local containsMod = function(modList, modNew)
for i, modItem in ipairs(modList) do
-- Check mod names & value data types both equal
if modItem[1] == modNew[1] and type(modItem[2]) == type(modNew[2]) then
if type(modItem[2]) == 'table' then
if Shared.tablesEqual(modItem[2], modNew[2]) then
return true
end
elseif modItem[2] == modNew[2] then
return true
end
end
end
return false
end


  table.sort(itemArray, function(a, b) return a.farmingLevel < b.farmingLevel end)
local addToArray = function(modArray, modNew)
if not isDistinct or (isDistinct and not containsMod(modArray, modNew)) then
table.insert(modArray, modNew)
end
end


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


  result = result..'\r\n|}'
local modArray = {}
local isSkillMod = {}
--Adding a Group Number to hold together different bonuses from the same modifier [Falterfire 22/10/27]
local groupNum = 0


  return result
for _, modType in ipairs(modTypes) do
for i, modTypeData in ipairs(cons[modType]) do
groupNum = groupNum + 1
local modVal = nil
if modValue ~= nil then
modVal = modValue
else
modVal = modTypeData.incrementValue * modTypeData.maxCount
end
for j, modifier in ipairs(modTypeData.modifiers) do
local modEntry = (modifier.skill ~= nil and { skillID = modifier.skill, value = modVal }) or modVal
addToArray(modArray, {modifier.key, modEntry, group = groupNum})
end
end
end
 
if asKeyValue then
local modArrayKV = {}
for i, modDefn in ipairs(modArray) do
local modName, modVal = modDefn[1], modDefn[2]
local isSkill = type(modVal) == 'table' and modVal.skillID ~= nil
if modArrayKV[modName] == nil then
modArrayKV[modName] = (isSkill and { modVal } or modVal)
elseif isSkill then
table.insert(modArrayKV[modName], modVal)
else
modArrayKV[modName] = modArrayKV[modName] + modVal
end
end
return modArrayKV
else
return modArray
end
end
end


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


  local result = '{|class="wikitable"'
local _, localSkillID = GameData.getLocalID(skillID)
  result = result..'\r\n!Plot!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level!!Cost'
-- Clone so that we can sort by level
local unlockTable = Shared.clone(SkillData[localSkillID].masteryLevelUnlocks)
if unlockTable == nil then
return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
end
table.sort(unlockTable, function(a, b) return (a.level == b.level and a.descriptionID < b.descriptionID) or a.level < b.level end)


  for i, patch in Shared.skpairs(patches) do
local result = '{|class="wikitable"\r\n!Level!!Unlock'
    result = result..'\r\n|-\r\n|'..i
for i, unlock in ipairs(unlockTable) do
    result = result..'||style="text-align:right;" data-sort-value="0"|'..patch.level
result = result..'\r\n|-'
    if patch.cost == 0 then
result = result..'\r\n|'..unlock.level..'||'..unlock.description
      result = result..'||Free'
end
    else
result = result..'\r\n|}'
      result = result..'||style="text-align:right;" data-sort-value="'..patch.cost..'"|'..Icons.GP(patch.cost)
return result
    end
end
  end


  result = result..'\r\n|}'
function p.getMasteryCheckpointTable(frame)
  return result
local skillName = frame.args ~= nil and frame.args[1] or frame
local skillID = Constants.getSkillID(skillName)
if skillID == nil then
return Shared.printError('Failed to find a skill ID for ' .. skillName)
end
 
local _, localSkillID = GameData.getLocalID(skillID)
local checkpoints = SkillData[localSkillID].masteryCheckpoints
if checkpoints == nil then
return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
end
 
local totalPoolXP = SkillData[localSkillID].baseMasteryPoolCap
local checkpointPct = GameData.rawData.masteryCheckpoints
local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
for i, checkpointDesc in ipairs(checkpoints) do
result = result..'\r\n|-'
result = result..'\r\n|'..checkpointPct[i]..'%||'
result = result..Shared.formatnum(math.floor(totalPoolXP * checkpointPct[i] / 100))..' xp||'..checkpointDesc
end
result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
result = result..'\r\n|'..Shared.formatnum(totalPoolXP)
result = result..'\r\n|}'
return result
end
end


function p.getSmithingTable(frame)
function p.getMasteryTokenTable()
  local tableType = frame.args ~= nil and frame.args[1] or frame
local baseTokenChance = 18500
  local bar = nil
local masterySkills = {}
  if tableType ~= 'Smelting' then
local CCI = Items.getItemByID('melvorD:Clue_Chasers_Insignia')
    bar = Items.getItem(tableType)
if CCI == nil then return '' end
    if bar == nil then
 
      return 'ERROR: Could not find an item named '..tableType..' to build a smithing table with'
-- Build table of mastery skills
    elseif bar.type ~= 'Bar' then
for skillLocalID, skill in pairs(SkillData) do
      return 'ERROR: '..tableType.." is not a bar and thus can't be used for smithing"
if skill.masteryTokenID ~= nil then
    end
table.insert(masterySkills, skill)
  end
end
end
table.sort(masterySkills,
function(a, b)
if a.milestoneCount == b.milestoneCount then
return a.name < b.name
else
return a.milestoneCount > b.milestoneCount
end
end)
 
-- Generate output table
local resultPart = {}
local CCIIcon = Icons.Icon({CCI.name, type='item', notext=true})
 
table.insert(resultPart, '{| class="wikitable sortable"')
table.insert(resultPart, '\r\n!rowspan="2"|Token!!rowspan="2"|Skill!!colspan="2"|Approximate Mastery Token Chance')
table.insert(resultPart, '\r\n|-\r\n!Without ' .. CCIIcon .. '!!With ' .. CCIIcon)
 
for i, skill in ipairs(masterySkills) do
local token = Items.getItemByID(skill.masteryTokenID)
local denom = math.floor(baseTokenChance / skill.milestoneCount)
local denomCCI = Shared.round(baseTokenChance / (skill.milestoneCount * (1 + CCI.modifiers.increasedOffItemChance / 100)), 0, 0)
 
table.insert(resultPart, '\r\n|-')
table.insert(resultPart, '\r\n|style="text-align:center"|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
table.insert(resultPart, '\r\n|' .. Icons.Icon({skill.name, type='skill'}))
table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Shared.formatnum(denom))
table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. denomCCI .. '"|1/' .. Shared.formatnum(denomCCI))
end
table.insert(resultPart, '\r\n|}')


  local smithList = {}
return table.concat(resultPart)
  for i, item in pairs(ItemData.Items) do
end
    if item.smithingLevel ~= nil then
      if tableType == 'Smelting' then
        if item.type == 'Bar' then
          table.insert(smithList, item)
        end
      else
        for j, req in pairs(item.smithReq) do
          if req.id == bar.id then
            table.insert(smithList, item)
          end
        end
      end
    end
  end


  local result = '{|class="wikitable sortable stickyHeader"'
-- Skill unlock costs for Adventure game mode
  result = result..'\r\n|-class="headerRow-0"'
function p.getSkillUnlockCostTable()
  result = result..'\r\n!Item!!Name!!'..Icons.Icon({'Smithing', type='skill', notext=true})..' Level!!XP!!Value!!Ingredients'
local advMode = GameData.getEntityByID('gamemodes', 'melvorF:Adventure')
  --Adding value/bar for things other than smelting
if advMode ~= nil then
  if bar ~= nil then result = result..'!!Value/Bar' end
local unlockCount = Shared.tableCount(GameData.skillData) - Shared.tableCount(advMode.startingSkills)
local costLength = Shared.tableCount(advMode.skillUnlockCost)
local returnPart = {}
table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative Cost')


  table.sort(smithList, function(a, b)
local accCost = 0
                          if a.smithingLevel ~= b.smithingLevel then
for i = 1, unlockCount, 1 do
                            return a.smithingLevel < b.smithingLevel
local cost = advMode.skillUnlockCost[math.min(i, costLength)]
                          else
accCost = accCost + cost
                            return a.name < b.name
table.insert(returnPart, '|-')
                          end end)
table.insert(returnPart, '|' .. i .. '||' .. Icons.GP(cost) .. '||' .. Icons.GP(accCost))
  for i, item in Shared.skpairs(smithList) do
end
    result = result..'\r\n|-'
table.insert(returnPart, '|}')
    result = result..'\r\n|'..Icons.Icon({item.name, type='item', size='50', notext=true})..'||'
    local qty = item.smithingQty ~= nil and item.smithingQty or 1
    if qty > 1 then
      result = result..item.smithingQty..'x '
    end
    result = result..'[['..item.name..']]'
    result = result..'||data-sort-value="'..item.smithingLevel..'"|'..Icons._SkillReq('Smithing', item.smithingLevel)
    result = result..'||'..item.smithingXP
    local totalValue = item.sellsFor * qty
    result = result..'||data-sort-value="'..totalValue..'"|'..Icons.GP(item.sellsFor)
    if qty > 1 then
      result = result..' (x'..qty..')'
    end
    result = result..'||'
    local barQty = 0
    for i, mat in Shared.skpairs(item.smithReq) do
      matItem = Items.getItemByID(mat.id)
      if i > 1 then result = result..', ' end
      result = result..Icons.Icon({matItem.name, type='item', qty=mat.qty, notext=true})
      if bar ~= nil and mat.id == bar.id then
        barQty = mat.qty
      end
    end
    --Add the data for the value per bar
    if bar ~= nil then
      if barQty == 0 then
        result = result..'||data-sort-value="0"|N/A'
      else
        local barVal = totalValue / barQty
        result = result..'||data-sort-value="'..barVal..'"|'..Icons.GP(Shared.round(barVal, 1, 1))
      end
    end
  end


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


function p.getFiremakingTable(frame)
-- Accepts 1 parameter, being either:
  local resultPart = {}
-- 'Bars', for which a table of all bars is generated, or
  table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
--  A bar or tier name, which if valid generates a table of all smithing recipes using that bar/tier
  table.insert(resultPart, '\r\n|-class="headerRow-0"')
function p.getSmithingTable(frame)
  table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level')
local tableType = frame.args ~= nil and frame.args[1] or frame
  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
-- Has a valid category been passed (by name)?
  local logs = Items.getItemByID(logData.logID)
local category = GameData.getEntityByName(SkillData.Smithing.categories, tableType)
    local name = logs.name
if category == nil then
    local burnTime = logData.baseInterval / 1000
return Shared.printError('Invalid Smithing category: "' .. tableType .. '"')
    local bonfireTime = logData.baseBonfireInterval / 1000
end
    local XPS = logData.baseXP / burnTime
    local XP_BF = logData.baseXP * (1 + logData.bonfireXPBonus / 100)
    local XPS_BF = XP_BF / burnTime


    table.insert(resultPart, '\r\n|-')
-- Build a list of recipes to be included, and a list of bars while we're at it
    table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
-- The bar list will be used later for value/bar calculations
    table.insert(resultPart, '||[['..name..']]')
local recipeList, barIDList = {}, {}
    table.insert(resultPart, '||style ="text-align: right;"|'..logData.levelRequired)
for i, recipe in ipairs(SkillData.Smithing.recipes) do
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true))
if recipe.categoryID == category.id then
    table.insert(resultPart, '||style ="text-align: right;"|'..logData.baseXP)
local recipeItem = Items.getItemByID(recipe.productID)
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.round(XPS, 2, 2))
if recipeItem ~= nil then
    table.insert(resultPart, '||style ="text-align: right;"|'..Shared.round(XP_BF, 2, 0))
table.insert(recipeList, { id = i, level = recipe.level, itemName = recipeItem.name, itemValue = recipeItem.sellsFor, expIcon = Icons.getExpansionIcon(recipeItem.id) })
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.round(XPS_BF, 2, 2))
end
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..logData.bonfireXPBonus..'"|'..logData.bonfireXPBonus..'%')
elseif recipe.categoryID == 'melvorD:Bars' then
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireTime..'"|'..Shared.timeString(bonfireTime, true))
barIDList[recipe.productID] = true
  end
end
end


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


function p.getMasteryTokenTable()
table.sort(recipeList, function(a, b)
  local baseTokenChance = 18500
if a.level ~= b.level then
  local masterySkills = {}
return a.level < b.level
 
else
  -- Find all mastery tokens
return a.itemName < b.itemName
  local masteryTokens = Items.getItems(function(item) return item.isToken ~= nil and item.skill ~= nil and item.isToken end)
end
  for i, item in pairs(masteryTokens) do
end)
    local milestones = SkillData.Milestones[item.skill + 1]
    if milestones ~= nil then
      table.insert(masterySkills, {tokenRef = i, skillID = item.skill, milestoneCount = milestones})
    end
  end
  table.sort(masterySkills, function(a, b)
                              if a['milestoneCount'] == b['milestoneCount'] then
                                return a['skillID'] < b['skillID']
                              else
                                return a['milestoneCount'] > b['milestoneCount']
                              end
                            end)
 
  -- Generate output table
  local resultPart = {}
  local CCI = Items.getItem('Clue Chasers Insignia')
  local CCIIcon = Icons.Icon({'Clue Chasers Insignia', type='item', notext=true})
  if CCI == nil then return '' end


  table.insert(resultPart, '{| class="wikitable sortable"')
for i, recipeDef in ipairs(recipeList) do
  table.insert(resultPart, '\r\n!rowspan="2"|Token!!rowspan="2"|Skill!!colspan="2"|Approximate Mastery Token Chance')
local recipe = SkillData.Smithing.recipes[recipeDef.id]
  table.insert(resultPart, '\r\n|-\r\n!Without ' .. CCIIcon .. '!!With ' .. CCIIcon)
local totalValue = recipe.baseQuantity * recipeDef.itemValue
-- Determine the bar quantity & build the recipe cost string
local barQty, costString = 0, {}
for j, itemCost in ipairs(recipe.itemCosts) do
local costItem = Items.getItemByID(itemCost.id)
if costItem ~= nil then
table.insert(costString, Icons.Icon({costItem.name, type='item', qty=itemCost.quantity, notext=true}))
end
if barIDList[itemCost.id] then
barQty = barQty + itemCost.quantity
end
end


  for i, m in ipairs(masterySkills) do
table.insert(resultPart, '\r\n|-')
    local token = masteryTokens[m.tokenRef]
table.insert(resultPart, '\r\n| ' .. Icons.Icon({recipeDef.itemName, type='item', size=50, notext=true}))
    local denom = math.floor(baseTokenChance / m['milestoneCount'])
table.insert(resultPart, '\r\n| ')
    local denomCCI = math.floor(baseTokenChance / m['milestoneCount'] * (1 - CCI.increasedItemChance / 100))
table.insert(resultPart, recipeDef.expIcon)
if recipe.baseQuantity > 1 then
table.insert(resultPart, recipe.baseQuantity .. 'x ')
end
table.insert(resultPart, Icons.Icon({recipeDef.itemName, type='item', noicon=true}))
table.insert(resultPart, '\r\n|data-sort-value="' .. recipe.level .. '"| ' .. Icons._SkillReq('Smithing', recipe.level))
table.insert(resultPart, '\r\n|data-sort-value="' .. recipe.baseExperience .. '"| ' .. Shared.formatnum(recipe.baseExperience))
table.insert(resultPart, '\r\n|data-sort-value="' .. totalValue .. '"| ' .. Icons.GP(recipeDef.itemValue))
if recipe.baseQuantity > 1 then
table.insert(resultPart, ' (x' .. recipe.baseQuantity .. ')')
end
table.insert(resultPart, '\r\n| ' .. table.concat(costString, ', '))
if category.id ~= 'melvorD:Bars' then
local barVal, barValTxt = 0, 'N/A'
if barQty > 0 then
barVal = totalValue / barQty
barValTxt = Icons.GP(Shared.round(barVal, 1, 1))
end
table.insert(resultPart, '\r\n|data-sort-value="' .. barVal .. '"| ' .. barValTxt)
end
end
table.insert(resultPart, '\r\n|}')


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


function p.getSkillUnlockCostTable()
function p.getFiremakingTable(frame)
  local returnPart = {}
local resultPart = {}
  table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative Cost')
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
table.insert(resultPart, '\r\n|-class="headerRow-0"')
table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level')
table.insert(resultPart, '!!rowspan="2"|Burn Time!!colspan="2"|Without Bonfire!!colspan="2"|With Bonfire!!rowspan="2"|Bonfire Bonus!!rowspan="2"|Bonfire Time')
table.insert(resultPart, '\r\n|-class="headerRow-1"')
table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')
 
for i, logData in ipairs(SkillData.Firemaking.logs) do
local logs = Items.getItemByID(logData.logID)
local name = logs.name
local burnTime = logData.baseInterval / 1000
local bonfireTime = logData.baseBonfireInterval / 1000
local XPS = logData.baseExperience / burnTime
local XP_BF = logData.baseExperience * (1 + logData.bonfireXPBonus / 100)
local XPS_BF = XP_BF / burnTime


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


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


return p
return p