Anonymous

Module:Skills: Difference between revisions

From Melvor Idle
1,164 bytes removed ,  15 October 2024
Add Archaeology recipe key
(Implement getSkillUnlockCostTable())
(Add Archaeology recipe key)
(62 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
      break
    end
  end
  return result
end


function p.getThievingNPCStat(frame)
return (isAbyssalA == isAbyssalB and levelA < levelB) or isAbyssalNumA < isAbyssalNumB
  local args = frame.args ~= nil and frame.args or frame
  local npcName = args[1]
  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)
  local itemDropChance = 0.75
  local result = npc[stat]
  -- Overrides below
  if stat == 'maxHit' then
    result = result * 10
  elseif stat == 'lootList' then
    return p._formatLootTable(npc['lootTable'], itemDropChance, true)
  elseif stat == 'lootTable' then
    return p._formatLootTable(npc['lootTable'], itemDropChance, false)
  elseif stat == 'requirements' then
    if npc['level'] ~= nil then
      result = Icons._SkillReq('Thieving', npc['level'], true)
    else
      result = 'None'
    end
  elseif (stat == 'lootValue' or stat == 'pickpocketValue') then
    if stat == 'pickpocketValue' then
      local itemBP = Items.getItem("Bobby's Pocket")
      result = (1 + npc['maxCoins']) / 2 + itemBP.sellsFor * (1 / 120)
    else
      result = 0
    end
    result = Shared.round(result + p._getLootTableValue(npc['lootTable']) * itemDropChance, 2, 2)
  elseif stat == 'pageName' then
    local linkOverrides = { ['Golbin'] = 'Golbin (thieving)' }
    result = (linkOverrides[npc['name']] ~= nil and linkOverrides[npc['name']]) or npc['name']
  end


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


function p._getLootTableValue(lootTable)
function p.getRealmFromName(realmName)
  -- Calculates the average GP value of a given loot table
local realm = nil
  -- Expects lootTableIn to be in format {{itemID_1, itemWeight_1}, ..., {itemID_n, itemWeight_n}}
if realmName == nil or realmName == '' then
  if Shared.tableCount(lootTable) == 0 then
-- Default realm
    return 0
realm = GameData.getEntityByID('realms', 'melvorD:Melvor')
  end
else
realm = GameData.getEntityByName('realms', realmName)
end
return realm
end


  local totalWeight = 0
function p.getMasteryActionCount(skillID, realmID, levelLimit)
  for i, drop in pairs(lootTable) do
local actCount = 0
    totalWeight = totalWeight + drop[2]
local skillNS, skillLocalID = Shared.getLocalID(skillID)
  end
local skillData = SkillData[skillLocalID]
  if totalWeight == 0 then
local recipeKey = p.getSkillRecipeKey(skillLocalID)
    return 0
if recipeKey ~= nil then
  end
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


  local avgValue = 0
-- Thieving
  for i, drop in pairs(lootTable) do
function p.getThievingNPCByID(npcID)
    local item = Items.getItemByID(drop[1])
return GameData.getEntityByID(SkillData.Thieving.npcs, npcID)
    if item ~= nil then
      avgValue = avgValue + item.sellsFor * (drop[2] / totalWeight)
    end
  end
 
  return avgValue
end
end


function p._formatLootTable(lootTableIn, chanceMultIn, asList)
function p.getThievingNPC(npcName)
  -- Expects lootTableIn to be in format {{itemID_1, itemWeight_1}, ..., {itemID_n, itemWeight_n}}
return GameData.getEntityByName(SkillData.Thieving.npcs, npcName)
  if Shared.tableCount(lootTableIn) == 0 then
end
    return ''
  end


  local chanceMult = (chanceMultIn or 1) * 100
function p.getThievingNPCArea(npc)
  local lootTable = Shared.clone(lootTableIn)
for i, area in ipairs(SkillData.Thieving.areas) do
  -- Sort table from most to least common drop
for j, npcID in ipairs(area.npcIDs) do
  table.sort(lootTable, function(a, b)
if npcID == npc.id then
                          if a[2] == b[2] then
return area
                            return a[1] < b[1]
end
                          else
end
                            return a[2] > b[2]
end
                          end
end
                        end)


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


  -- Get the length (in characters) of the largest drop chance so that they can be right aligned
if statName == 'level' then
  -- [4/16/21]: Adding info for no drop
result = Icons._SkillReq('Thieving', npc.level)
  local maxDropLen = math.max(string.len(Shared.round(100 - chanceMult, 2, 2)), string.len(Shared.round(lootTable[1][2] / totalWeight * chanceMult, 2, 2)))
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 returnPart = {}
if result == nil then
  -- Generate header
result = ''
  if asList then
end
    if chanceMult < 100 then
      table.insert(returnPart, '* ' .. string.rep('&nbsp;', math.max(0, (maxDropLen - string.len(Shared.round(100 - chanceMult, 2, 2))) * 2)) .. Shared.round(100 - chanceMult, 2, 2) .. '% No Item')
    end
  else
    table.insert(returnPart, '{|class="wikitable sortable"\r\n!Item!!Price!!colspan="2"|Chance')
  end
  -- Generate row for each item
  for i, drop in pairs(lootTable) do
    local item, itemText, sellsFor, dropChance = Items.getItemByID(drop[1]), 'Unknown', 0, Shared.round(drop[2] / totalWeight * chanceMult, 2, 2)
    if item ~= nil then
      itemText, sellsFor = Icons.Icon({item.name, type='item'}), item.sellsFor
    end
    if asList then
      table.insert(returnPart, '* ' .. string.rep('&nbsp;', math.max(0, (maxDropLen - string.len(dropChance)) * 2)) .. dropChance .. '% ' .. itemText)
    else
      table.insert(returnPart, '|-\r\n|' .. itemText)
      table.insert(returnPart, '|style="text-align:right;" data-sort-value="' .. sellsFor .. '"|' .. Icons.GP(sellsFor))
      table.insert(returnPart, '|style="text-align:right;" data-sort-value="' .. dropChance .. '"|' .. Shared.fraction(drop[2] * chanceMult, totalWeight * 100))
      table.insert(returnPart, '|style="text-align:right;"|' .. dropChance .. '%')
    end
  end
  if not asList then
    table.insert(returnPart, '|-class="sortbottom" \r\n!colspan="2"|Total:')
    local textTotChance = ''
    if chanceMult < 100 then
      textTotChance = '|style="text-align:right"|' .. Shared.fraction(chanceMult, 100) .. '\r\n|'
    else
      textTotChance = '|colspan="2" '
    end
    textTotChance = textTotChance .. 'style="text-align:right;"|' .. Shared.round(chanceMult, 2, 2) .. '%' .. '\r\n|}'
    table.insert(returnPart, textTotChance)
  end


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


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


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


  return table.concat(returnPart, '\r\n')
function p.getThievingSourcesForItem(itemID)
end
local resultArray = {}
local areaNPCs = {}


function p.getThievingNavbox()
--First check area unique drops
  local returnPart = {}
--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" style="text-align:center; clear:both; margin:auto; margin-bottom:1em;"')
for i, npc in pairs(SkillData.Thieving.npcs) do
  table.insert(returnPart, '|-\r\n!' .. Icons.Icon({'Thieving', type='skill', notext=true}) .. '[[Thieving|Thieving Targets]]')
local totalWt = 0
  table.insert(returnPart, '|-\r\n|')
local dropWt = 0
 
local dropQty = { min = 0, max = 0 }
  local npcList = {}
for j, drop in ipairs(npc.lootTable) do
  -- Create row for each NPC
totalWt = totalWt + drop.weight
  for i, npc in Shared.skpairs(SkillData.Thieving) do
if drop.itemID == itemID then
    local linkText = (npc.name ~= p._getThievingNPCStat(npc, 'pageName') and p._getThievingNPCStat(npc, 'pageName') .. '|' .. npc.name) or npc.name
dropWt = drop.weight
    table.insert(npcList, Icons.Icon({npc.name, type='thieving', notext=true}) .. ' [[' .. linkText .. ']]')
dropQty = { min = drop.minQuantity, max = drop.maxQuantity }
  end
end
  table.insert(returnPart, table.concat(npcList, ' '))
end
  table.insert(returnPart, '|}')
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


  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.getMasteryUnlockTable(frame)
local areaNPC = areaNPCs[npc.id]
  local skillName = frame.args ~= nil and frame.args[1] or frame
if areaNPC ~= nil then
  local skillID = p.getSkillID(skillName)
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'})
  if skillID == nil then
end
    return "ERROR: Failed to find a skill ID for "..skillName
end
  end


  local unlockTable = SkillData.MasteryUnlocks[skillID]
for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
  if unlockTable == nil then
if drop.itemID == itemID then
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
if drop.npcs == nil then
  end
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


  local result = '{|class="wikitable"\r\n!Level!!Unlock'
return resultArray
  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)
-- 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


  if SkillData.MasteryCheckpoints[skillID] == nil then
function p.getConstellation(constName)
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
return GameData.getEntityByName(SkillData.Astrology.recipes, constName)
  end
 
  local bonuses = SkillData.MasteryCheckpoints[skillID].bonuses
  local totalPoolXP = SkillData.MasteryPoolXP[skillID + 1]
 
  local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
  for i, bonus in Shared.skpairs(bonuses) do
    result = result..'\r\n|-'
    result = result..'\r\n|'..(MasteryCheckpoints[i] * 100)..'%||'
    result = result..Shared.formatnum(totalPoolXP * MasteryCheckpoints[i])..' xp||'..bonus
  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._getFarmingTable(category)
function p.getConstellations(checkFunc)
  local seedList = {}
return GameData.getEntities(SkillData.Astrology.recipes, checkFunc)
  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"'
  result = result..'\r\n|- class="headerRow-0"'
  result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
  result = result..'!!XP!!Growth Time!!Seed Value'
  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
    result = result..'\r\n|-'
    result = result..'\r\n|'..Icons.Icon({seed.name, type='item', size='50', notext=true})..'||[['..seed.name..']]'
    result = result..'||'..seed.farmingLevel..'||'..Shared.formatnum(seed.farmingXP)
    result = result..'||data-sort-value="'..seed.timeToGrow..'"|'..Shared.timeString(seed.timeToGrow, true)
    result = result..'||data-sort-value="'..seed.sellsFor..'"|'..Icons.GP(seed.sellsFor)
 
    local crop = Items.getItemByID(seed.grownItemID)
    result = result..'||'..Icons.Icon({crop.name, type='item', size='50', notext=true})..'||[['..crop.name..']]'
    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|}'
  return result
end
end


function p.getFarmingTable(frame)
-- Combines Astrology constellation modifiers into an object similar to other entities,
  local category = frame.args ~= nil and frame.args[1] or frame
-- and multiplies the values up to their maximum possible amount
function p._getConstellationModifiers(cons)
local result = {}
local modKeys = { 'standardModifiers', 'uniqueModifiers', 'abyssalModifiers' }


  return p._getFarmingTable(category)
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
end


function p.getFarmingFoodTable(frame)
-- Mastery
  local result = '{| class="wikitable sortable stickyHeader"'
function p.getMasteryUnlockTable(frame)
  result = result..'\r\n|- class="headerRow-0"'
local skillName = frame.args ~= nil and frame.args[1] or frame
  result = result..'\r\n!colspan="2"|Crop!!'..Icons.Icon({"Farming", type="skill", notext=true})..' Level'
local skillID = Constants.getSkillID(skillName)
  result = result..'!!Healing!!Value'
if skillID == nil then
 
return Shared.printError('Failed to find a skill ID for ' .. skillName)
  local itemArray = Items.getItems(function(item) return item.grownItemID ~= nil end)
end
 
  table.sort(itemArray, function(a, b) return a.farmingLevel < b.farmingLevel end)
 
  for i, item in Shared.skpairs(itemArray) do
    local crop = Items.getItemByID(item.grownItemID)
    if crop.healsFor ~= nil and crop.healsFor > 0 then
      result = result..'\r\n|-'
      result = result..'\r\n|'..Icons.Icon({crop.name, type='item', notext='true', size='50'})..'||[['..crop.name..']]'
      result = result..'||style="text-align:right;"|'..item.farmingLevel
      result = result..'||style="text-align:right" data-sort-value="'..crop.healsFor..'"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..(crop.healsFor * 10)
      result = result..'||style="text-align:right" data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
    end
  end


  result = result..'\r\n|}'
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)


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


function p.getFarmingPlotTable(frame)
function p.getMasteryCheckpointTable(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 skillName = args[1]
  for i, area in Shared.skpairs(SkillData.Farming.Patches) do
local realmName = args.realm
    if area.areaName == areaName then
      patches = area.patches
      break
    end
  end
  if patches == nil then
    return "ERROR: Invalid area name.[[Category:Pages with script errors"
  end


  local result = '{|class="wikitable"'
local realm = p.getRealmFromName(realmName)
  result = result..'\r\n!Plot!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level!!Cost'
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


  for i, patch in Shared.skpairs(patches) do
local _, localSkillID = GameData.getLocalID(skillID)
    result = result..'\r\n|-\r\n|'..i
local checkpoints = SkillData[localSkillID].masteryPoolBonuses
    result = result..'||style="text-align:right;" data-sort-value="0"|'..patch.level
if checkpoints == nil then
    if patch.cost == 0 then
return Shared.printError('Failed to find Mastery checkpoint data for ' .. skillName)
      result = result..'||Free'
end
    else
      result = result..'||style="text-align:right;" data-sort-value="'..patch.cost..'"|'..Icons.GP(patch.cost)
    end
  end


  result = result..'\r\n|}'
local totalPoolXP = p.getMasteryActionCount(localSkillID, realm.id) * 500000
  return result
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 smithList = {}
local tokens = Items.getItems(function(item) return item.itemType == 'MasteryToken' end)
  for i, item in pairs(ItemData.Items) do
local tokenItems = {}
    if item.smithingLevel ~= nil then
for _, item in ipairs(tokens) do
      if tableType == 'Smelting' then
if item.realm == 'melvorD:Melvor' and item.skill ~= nil then
        if item.type == 'Bar' then
local skillNS, skillLocalID = Shared.getLocalID(item.skill)
          table.insert(smithList, item)
tokenItems[skillLocalID] = item
        end
end
      else
end
        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"'
-- Iterate over each skill with mastery, determining the number of
  result = result..'\r\n|-class="headerRow-0"'
-- mastery actions for each
  result = result..'\r\n!Item!!Name!!'..Icons.Icon({'Smithing', type='skill', notext=true})..' Level!!XP!!Value!!Ingredients'
for skillLocalID, skill in pairs(SkillData) do
  --Adding value/bar for things other than smelting
if skill.masteryPoolBonuses ~= nil then
  if bar ~= nil then result = result..'!!Value/Bar' end
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


  table.sort(smithList, function(a, b)
local firstID = skillLevels[1].id
                          if a.smithingLevel ~= b.smithingLevel then
table.sort(masteryActionCount,
                            return a.smithingLevel < b.smithingLevel
function(a, b)
                          else
if a[firstID] == b[firstID] then
                            return a.name < b.name
return a.skill.name < b.skill.name
                          end end)
else
  for i, item in Shared.skpairs(smithList) do
return a[firstID] > b[firstID]
    result = result..'\r\n|-'
end
    result = result..'\r\n|'..Icons.Icon({item.name, type='item', size='50', notext=true})..'||'
end)
    local qty = item.smithingQty ~= nil and item.smithingQty or 1
    if qty > 1 then
-- Generate output table
      result = result..item.smithingQty..'x '
local resultPart = {}
    end
local CCIIcon = Icons.Icon({CCI.name, type='item', notext=true})
    result = result..'[['..item.name..']]'
local columnPairs = Shared.tableCount(skillLevels)
    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)
-- Generate header
  local resultPart = {}
table.insert(resultPart, '{| class="wikitable sortable"')
  table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
table.insert(resultPart, '\n!rowspan="3"|Token!!rowspan="3"|Skill!!colspan="' .. columnPairs * 2 .. '"|Approximate Mastery Token Chance')
  table.insert(resultPart, '\r\n|-class="headerRow-0"')
table.insert(resultPart, '\n|-')
  table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level')
for i, levelDef in ipairs(skillLevels) do
  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, '\n!colspan="2"| ' .. levelDef.description)
  table.insert(resultPart, '\r\n|-class="headerRow-1"')
end
  table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')
table.insert(resultPart, '\n|-' .. string.rep('\n!Without ' .. CCIIcon .. '\n!With ' .. CCIIcon, columnPairs))


  for i, logData in Shared.skpairs(SkillData.Firemaking) do
for i, rowData in ipairs(masteryActionCount) do
    local name = Shared.titleCase(logData.type..' Logs')
local token = rowData.token
    local burnTime = logData.interval / 1000
table.insert(resultPart, '\n|-')
    local XPS = logData.xp / burnTime
local tokenImg = (token == nil and '?') or Icons.Icon({token.name, type='item', notext=true})
    local XP_BF = logData.xp * (1 + logData.bonfireBonus / 100)
table.insert(resultPart, '\n|style="text-align:center"|' .. tokenImg)
    local XPS_BF = XP_BF / burnTime
table.insert(resultPart, '\n|' .. Icons.Icon({rowData.skill.name, type='skill'}))


    table.insert(resultPart, '\r\n|-')
for j, levelDef in ipairs(skillLevels) do
    table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
local actCount = rowData[levelDef.id]
    table.insert(resultPart, '||[['..name..']]')
local denom, denomCCI = 0, 0
    table.insert(resultPart, '||style ="text-align: right;"|'..logData.level)
if actCount > 0 then
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true))
denom = math.floor(baseTokenChance / actCount)
    table.insert(resultPart, '||style ="text-align: right;"|'..logData.xp)
denomCCI = Num.round(baseTokenChance / (actCount * (1 + CCI.modifiers.offItemChance / 100)), 0, 0)
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.round(XPS, 2, 2))
end
    table.insert(resultPart, '||style ="text-align: right;"|'..Shared.round(XP_BF, 2, 0))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Num.formatnum(denom))
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.round(XPS_BF, 2, 2))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denomCCI .. '"|1/' .. Num.formatnum(denomCCI))
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..logData.bonfireBonus..'"|'..logData.bonfireBonus..'%')
end
    table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..logData.bonfireInterval..'"|'..Shared.timeString(logData.bonfireInterval / 1000, true))
end
  end
table.insert(resultPart, '\n|}')


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


function p.getMasteryTokenTable()
function p.getFiremakingTable(frame)
  local baseTokenChance = 18500
    local args = frame.args ~= nil and frame.args or frame
  local masterySkills = {}
    local realmName = args.realm
 
     local realm = p.getRealmFromName(realmName)
  -- Find all mastery tokens
     if realm == nil then
  local masteryTokens = Items.getItems(function(item) return item.isToken ~= nil and item.skill ~= nil and item.isToken end)
        return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
  for i, item in pairs(masteryTokens) do
     local milestones = SkillData.Milestones[item.skill + 1]
     if milestones ~= nil then
      table.insert(masterySkills, {tokenRef = i, skillID = item.skill, milestoneCount = milestones})
     end
     end
  end
    local skillID = 'Firemaking'
  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"')
    local tableHtml = mw.html.create('table')
  table.insert(resultPart, '\r\n!rowspan="2"|Token!!rowspan="2"|Skill!!colspan="2"|Approximate Mastery Token Chance')
        :addClass('wikitable sortable stickyHeader')
  table.insert(resultPart, '\r\n|-\r\n!Without ' .. CCIIcon .. '!!With ' .. CCIIcon)
   
    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, m in ipairs(masterySkills) do
    local headerRow1 = tableHtml:tag('tr'):addClass('headerRow-1')
     local token = masteryTokens[m.tokenRef]
     headerRow1:tag('th'):wikitext('XP')
     local denom = math.floor(baseTokenChance / m['milestoneCount'])
     headerRow1:tag('th'):wikitext('XP/s')
     local denomCCI = math.floor(baseTokenChance / m['milestoneCount'] * (1 - CCI.increasedItemChance / 100))
    headerRow1:tag('th'):wikitext('XP')
     headerRow1:tag('th'):wikitext('XP/s')


     table.insert(resultPart, '\r\n|-')
     local logsData = GameData.getEntities(SkillData.Firemaking.logs, function(obj)
    table.insert(resultPart, '\r\n|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
        return p.getRecipeRealm(obj) == realm.id
     table.insert(resultPart, '\r\n|' .. Icons.Icon({p.getSkillName(m['skillID']), type='skill'}))
    end)
     table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Shared.formatnum(denom))
     table.sort(logsData, function(a, b) return p.standardRecipeSort(skillID, a, b) end)
    table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. denomCCI .. '"|1/' .. Shared.formatnum(denomCCI))
      
  end
    for i, logData in ipairs(logsData) do
  table.insert(resultPart, '\r\n|}')
        local logs = Items.getItemByID(logData.logID)
 
        local name = logs.name
  return table.concat(resultPart)
        local level = p.getRecipeLevel(skillID, logData)
end
        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.getSkillUnlockCostTable()
        local row = tableHtml:tag('tr')
  local returnPart = {}
        row:tag('td'):attr('data-sort-value', name)
  table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative Cost')
                    :wikitext(Icons.Icon({name, type='item', notext=true}))
 
        row:tag('td'):wikitext('[[' .. name .. ']]')
  local accCost = 0
        row:tag('td'):css('text-align', 'center')
  for i, cost in ipairs(SkillData.SkillUnlockCosts) do
                    :wikitext(level)
    accCost = accCost + cost
        row:tag('td'):css('text-align', 'center')
    table.insert(returnPart, '|-')
            :attr('data-sort-value', Icons.getExpansionID(logData.logID))
    table.insert(returnPart, '|' .. i .. '||' .. Icons.GP(cost) .. '||' .. Icons.GP(accCost))
            :wikitext(Icons.getDLCColumnIcon(logData.logID))
  end
        row:tag('td'):css('text-align', 'right')
  table.insert(returnPart, '|}')
                    :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 table.concat(returnPart, '\r\n')
    return tostring(tableHtml)
end
end


return p
return p