Module:Skills: Difference between revisions

(Shortened growth time string)
(Update for v1.3)
 
(88 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:
--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 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 Icons = require('Module:Icons')
local Icons = require('Module:Icons')


local MasteryCheckpoints = {.1, .25, .5, .95}
-- Given a skill ID, returns the key for that skill's recipe data.
-- If the skill has no recipes (e.g. is a combat skill) then the
-- return value is nil
function p.getSkillRecipeKey(skillID)
-- Convert skillID to local ID if not already
local ns, localSkillID = GameData.getLocalID(skillID)
local recipeIDs = {
["Woodcutting"] = 'trees',
["Fishing"] = 'fish',
["Firemaking"] = 'logs',
["Mining"] = 'rockData',
["Thieving"] = 'npcs',
["Agility"] = 'obstacles',
["Cooking"] = 'recipes',
["Smithing"] = 'recipes',
["Farming"] = 'recipes',
["Summoning"] = 'recipes',
["Fletching"] = 'recipes',
["Crafting"] = 'recipes',
["Runecrafting"] = 'recipes',
["Herblore"] = 'recipes',
["Astrology"] = 'recipes',
["Harvesting"] = 'veinData'
}
return recipeIDs[localSkillID]
end
 
-- Given a skill ID & recipe, returns the skill level requirement for
-- that recipe and a boolean value indicating whether the level if abyssal or not.
-- If the level could not be determined, then the return value is nil, nil
function p.getRecipeLevelRealm(skillID, recipe)
local level, isAbyssal = nil, nil
-- Convert skillID to local ID if not already
local ns, localSkillID = GameData.getLocalID(skillID)
local realmID = p.getRecipeRealm(recipe)
if localSkillID == 'Agility' then
local course = GameData.getEntityByProperty(SkillData.Agility.courses, 'realm', realmID)
isAbyssal = (realmID == 'melvorItA:Abyssal')
-- For Agility, level is derived from obstacle category
if recipe.category ~= nil then
-- Obstacle
level = course.obstacleSlots[recipe.category + 1].level
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._AbyssalSkillReq(skillName, recipe.abyssalLevel, false))
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, Shared.formatnum(recipe.totalMasteryRequired) .. ' ' .. Icons.Icon({skillName, type='skill', notext=true}) .. ' ' .. Icons.Icon({'Mastery'}))
end
local reqsData = {}
if type(recipe.requirements) == 'table' then
reqsData = Shared.shallowClone(recipe.requirements)
end
if recipe.shopItemPurchased ~= nil then
-- Mining requirements are stored differently than other skills like
-- Woodcutting, standardize here
table.insert(reqsData, {
["type"] = 'ShopPurchase',
["purchaseID"] = recipe.shopItemPurchased,
["count"] = 1
})
end
if not Shared.tableIsEmpty(reqsData) then
local reqs = Common.getRequirementString(reqsData)
if reqs ~= nil then
table.insert(reqText, reqs)
end
end
return table.concat(reqText, '<br/>')
end
 
function p.standardRecipeSort(skillID, recipeA, recipeB)
local levelA, isAbyssalA = p.getRecipeLevelRealm(skillID, recipeA)
local levelB, isAbyssalB = p.getRecipeLevelRealm(skillID, recipeB)
local isAbyssalNumA, isAbyssalNumB = (isAbyssalA and 1) or 0, (isAbyssalB and 1) or 0
 
return (isAbyssalA == isAbyssalB and levelA < levelB) or isAbyssalNumA < isAbyssalNumB
end
 
 
function p.getRecipeRealm(recipe)
return recipe.realm or 'melvorD:Melvor'
end
 
function p.getRealmFromName(realmName)
local realm = nil
if realmName == nil or realmName == '' then
-- Default realm
realm = GameData.getEntityByID('realms', 'melvorD:Melvor')
else
realm = GameData.getEntityByName('realms', realmName)
end
return realm
end
 
function p.getMasteryActionCount(skillID, realmID, levelLimit)
local actCount = 0
local skillNS, skillLocalID = Shared.getLocalID(skillID)
local skillData = SkillData[skillLocalID]
local recipeKey = p.getSkillRecipeKey(skillLocalID)
if recipeKey ~= nil then
local recipeData = skillData[recipeKey]
for i, recipe in ipairs(recipeData) do
if (
p.getRecipeRealm(recipe) == realmID
and (recipe.noMastery == nil or not recipe.noMastery)
and (levelLimit == nil or p.getRecipeLevel(skillLocalID, recipe) <= levelLimit)
) then
actCount = actCount + 1
end
end
end
return actCount
end
 
-- Thieving
function p.getThievingNPCByID(npcID)
return GameData.getEntityByID(SkillData.Thieving.npcs, npcID)
end
 
function p.getThievingNPC(npcName)
return GameData.getEntityByName(SkillData.Thieving.npcs, npcName)
end
 
function p.getThievingNPCArea(npc)
for i, area in ipairs(SkillData.Thieving.areas) do
for j, npcID in ipairs(area.npcIDs) do
if npcID == npc.id then
return area
end
end
end
end
 
function p._getThievingNPCStat(npc, statName)
local result = nil
 
if statName == 'level' then
result = Icons._SkillReq('Thieving', npc.level)
elseif statName == 'maxHit' then
result = npc.maxHit * 10
elseif statName == 'area' then
local area = p.getThievingNPCArea(npc)
result = area.name
else
result = npc[statName]
end
 
if result == nil then
result = ''
end
 
return result
end
 
function p.getThievingNPCStat(frame)
local npcName = frame.args ~= nil and frame.args[1] or frame[1]
local statName = frame.args ~= nil and frame.args[2] or frame[2]
local npc = p.getThievingNPC(npcName)
if npc == nil then
return Shared.printError('Invalid Thieving NPC ' .. npcName)
end
 
return p._getThievingNPCStat(npc, statName)
end
 
function p.getThievingSourcesForItem(itemID)
local resultArray = {}
local areaNPCs = {}
 
--First check area unique drops
--If an area drops the item, add all the NPC ids to the list so we can add them later
for i, area in pairs(SkillData.Thieving.areas) do
for j, drop in pairs(area.uniqueDrops) do
if drop.id == itemID then
for k, npcID in ipairs(area.npcIDs) do
areaNPCs[npcID] = { qty = drop.quantity, area = area }
end
break
end
end
end
 
--Now go through and get drop chances on each NPC if needed
for i, npc in pairs(SkillData.Thieving.npcs) do
local totalWt = 0
local dropWt = 0
local dropQty = { min = 0, max = 0 }
for j, drop in ipairs(npc.lootTable) do
totalWt = totalWt + drop.weight
if drop.itemID == itemID then
dropWt = drop.weight
dropQty = { min = drop.minQuantity, max = drop.maxQuantity }
end
end
if dropWt > 0 then
table.insert(resultArray, {npc = npc.name, minQty = dropQty.min, maxQty = dropQty.max, wt = dropWt * SkillData.Thieving.itemChance, totalWt = totalWt * 100, level = npc.level, npcID = npc.id, type = 'npc'})
end
 
--Chance of -1 on unique drops is to indicate variable chance
if npc.uniqueDrop ~= nil and npc.uniqueDrop.id == itemID then
table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.quantity, maxQty = npc.uniqueDrop.quantity, wt = -1, totalWt = -1, level = npc.level, npcID = npc.id, type = 'npcUnique'})
end
 
local areaNPC = areaNPCs[npc.id]
if areaNPC ~= nil then
table.insert(resultArray, {npc = npc.name, minQty = areaNPC.qty, maxQty = areaNPC.qty, wt = SkillData.Thieving.baseAreaUniqueChance, totalWt = 100, level = npc.level, npcID = npc.id, area = areaNPC.area, type = 'areaUnique'})
end
end
 
for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
if drop.itemID == itemID then
if drop.npcs == nil then
table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = 1, npcID = itemID, type = 'generalRare'})
else
for j, npcID in ipairs(drop.npcs) do
local npc = p.getThievingNPCByID(npcID)
if npc ~= nil then
table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = npc.level, npcID = npc.id, type = 'generalRare'})
end
end
end
end
end
 
return resultArray
end
 
-- Astrology
function p.getConstellationByID(constID)
return GameData.getEntityByID(SkillData.Astrology.recipes, constID)
end
 
function p.getConstellation(constName)
return GameData.getEntityByName(SkillData.Astrology.recipes, constName)
end


function p.getSkillID(skillName)
function p.getConstellations(checkFunc)
  for skName, ID in Shared.skpairs(Constants.skill) do
return GameData.getEntities(SkillData.Astrology.recipes, checkFunc)
    if skName == skillName then
      return ID
    end
  end
  return nil
end
end


-- Combines Astrology constellation modifiers into an object similar to other entities,
-- and multiplies the values up to their maximum possible amount
function p._getConstellationModifiers(cons)
local result = {}
local modKeys = { 'standardModifiers', 'uniqueModifiers', 'abyssalModifiers' }
for _, keyID in ipairs(modKeys) do
result[keyID] = {}
local mods = cons[keyID]
if mods ~= nil then
for _, mod in ipairs(mods) do
local 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
end
table.insert(result[keyID], modAdj)
end
end
end
end
return result
end
-- Mastery
function p.getMasteryUnlockTable(frame)
function p.getMasteryUnlockTable(frame)
  local skillName = frame.args ~= nil and frame.args[1] or frame
local skillName = frame.args ~= nil and frame.args[1] or frame
  local skillID = p.getSkillID(skillName)
local skillID = Constants.getSkillID(skillName)
  if skillID == nil then
if skillID == nil then
    return "ERROR: Failed to find a skill ID for "..skillName
return Shared.printError('Failed to find a skill ID for ' .. skillName)
  end
end


  local unlockTable = SkillData.MasteryUnlocks[skillID]
local _, localSkillID = GameData.getLocalID(skillID)
  if unlockTable == nil then
-- Clone so that we can sort by level
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
local unlockTable = Shared.shallowClone(SkillData[localSkillID].masteryLevelUnlocks)
  end
if unlockTable == nil then
return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
end
table.sort(unlockTable, function(a, b) return (a.level == b.level and a.descriptionID < b.descriptionID) or a.level < b.level end)


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


function p.getMasteryCheckpointTable(frame)
function p.getMasteryCheckpointTable(frame)
  local skillName = frame.args ~= nil and frame.args[1] or frame
local skillName = frame.args ~= nil and frame.args[1]
  local skillID = p.getSkillID(skillName)
local realmName = frame.args ~= nil and frame.args[2]
  if skillID == nil then
local skillID = Constants.getSkillID(skillName)
    return "ERROR: Failed to find a skill ID for "..skillName
local realm = nil
  end
if realmName ~= nil then
realm = GameData.getEntityByName('realms', realmName)
else
realm = GameData.getEntityByID('realms', 'melvorD:Melvor')
end
 
if skillID == nil then
return Shared.printError('Failed to find a skill ID for ' .. skillName)
elseif realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
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..Shared.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|'..Shared.formatnum(totalPoolXP)
result = result..'\r\n|}'
return result
end
 
function p.getMasteryTokenTable()
-- Defines which skill levels should be included within the output
local skillLevels = {
{
["id"] = 'Base',
["level"] = 99,
["description"] = '[[Full Version|Base Game]] (Level 99)'
}, {
["id"] = 'TotH',
["level"] = 120,
["description"] = Icons.TotH() .. ' [[Throne of the Herald Expansion|Throne of the Herald]] (Level 120)'
}
}
local baseTokenChance = 18500
local masteryActionCount = {}
local CCI_ID = 'melvorD:Clue_Chasers_Insignia'
local CCI = Items.getItemByID(CCI_ID)
if CCI == nil then
return Shared.printError('Failed to find item with ID ' .. CCI_ID)
end
 
local tokens = Items.getItems(function(item) return item.itemType == 'MasteryToken' end)
local tokenItems = {}
for _, item in ipairs(tokens) do
if item.realm == 'melvorD:Melvor' and item.skill ~= nil then
local skillNS, skillLocalID = Shared.getLocalID(item.skill)
tokenItems[skillLocalID] = item
end
end
 
-- Iterate over each skill with mastery, determining the number of
-- mastery actions for each
for skillLocalID, skill in pairs(SkillData) do
if skill.masteryPoolBonuses ~= nil then
local actCount = { ["skill"] = skill, ["token"] = tokenItems[skillLocalID] }
for i, levelDef in ipairs(skillLevels) do
actCount[levelDef.id] = p.getMasteryActionCount(skillLocalID, 'melvorD:Melvor', levelDef.level)
end
table.insert(masteryActionCount, actCount)
end
end
 
local firstID = skillLevels[1].id
table.sort(masteryActionCount,
function(a, b)
if a[firstID] == b[firstID] then
return a.skill.name < b.skill.name
else
return a[firstID] > b[firstID]
end
end)
-- Generate output table
local resultPart = {}
local CCIIcon = Icons.Icon({CCI.name, type='item', notext=true})
local columnPairs = Shared.tableCount(skillLevels)
 
-- Generate header
table.insert(resultPart, '{| class="wikitable sortable"')
table.insert(resultPart, '\n!rowspan="3"|Token!!rowspan="3"|Skill!!colspan="' .. columnPairs * 2 .. '"|Approximate Mastery Token Chance')
table.insert(resultPart, '\n|-')
for i, levelDef in ipairs(skillLevels) do
table.insert(resultPart, '\n!colspan="2"| ' .. levelDef.description)
end
table.insert(resultPart, '\n|-' .. string.rep('\n!Without ' .. CCIIcon .. '\n!With ' .. CCIIcon, columnPairs))
 
for i, rowData in ipairs(masteryActionCount) do
local token = rowData.token
table.insert(resultPart, '\n|-')
local tokenImg = (token == nil and '?') or Icons.Icon({token.name, type='item', size=50, notext=true})
table.insert(resultPart, '\n|style="text-align:center"|' .. tokenImg)
table.insert(resultPart, '\n|' .. Icons.Icon({rowData.skill.name, type='skill'}))
 
for j, levelDef in ipairs(skillLevels) do
local actCount = rowData[levelDef.id]
local denom, denomCCI = 0, 0
if actCount > 0 then
denom = math.floor(baseTokenChance / actCount)
denomCCI = Shared.round(baseTokenChance / (actCount * (1 + CCI.modifiers.offItemChance / 100)), 0, 0)
end
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Shared.formatnum(denom))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denomCCI .. '"|1/' .. Shared.formatnum(denomCCI))
end
end
table.insert(resultPart, '\n|}')
 
return table.concat(resultPart)
end


  if SkillData.MasteryCheckpoints[skillID] == nil then
-- Skill unlock costs for Adventure game mode
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
function p.getSkillUnlockCostTable()
  end
local advMode = GameData.getEntityByID('gamemodes', 'melvorF:Adventure')
if advMode ~= nil then
local unlockCount = Shared.tableCount(GameData.skillData) - Shared.tableCount(advMode.startingSkills)
local costLength = Shared.tableCount(advMode.skillUnlockCost)
local returnPart = {}
table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative Cost')


  local bonuses = SkillData.MasteryCheckpoints[skillID].bonuses
local accCost = 0
  local totalPoolXP = SkillData.MasteryPoolXP[skillID + 1]
for i = 1, unlockCount, 1 do
local cost = advMode.skillUnlockCost[math.min(i, costLength)]
accCost = accCost + cost
table.insert(returnPart, '|-')
table.insert(returnPart, '|' .. i .. '||' .. Icons.GP(cost) .. '||' .. Icons.GP(accCost))
end
table.insert(returnPart, '|}')


  local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
return table.concat(returnPart, '\r\n')
  for i, bonus in Shared.skpairs(bonuses) do
end
    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.getFiremakingTable(frame)
  local seedList = {}
local args = frame.args ~= nil and frame.args or frame
  if category == 'Allotment' or category == 'Herb' or category == 'Tree' then
local realmName = args.realm
    seedList = Items.getItems(function(item) return item.tier == category end)
local realm = p.getRealmFromName(realmName)
  else
if realm == nil then
    return 'ERROR: Invalid farming category. Please choose Allotment, Herb, or Tree'
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
  end
end
local skillID = 'Firemaking'


  local result = '{|class="wikitable sortable stickyHeader"'
local resultPart = {}
  result = result..'\r\n|- class=headerRow-0"'
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
  result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
table.insert(resultPart, '\r\n|-class="headerRow-0"')
  result = result..'!!XP!!Growth Time!!Seed Value'
table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|Requirements')
  if category == 'Allotment' then
table.insert(resultPart, '!!rowspan="2"|Burn Time!!colspan="2"|Without Bonfire!!colspan="2"|With Bonfire!!rowspan="2"|Bonfire Bonus!!rowspan="2"|Bonfire Time')
    result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
table.insert(resultPart, '\r\n|-class="headerRow-1"')
  elseif category == 'Herb' then
table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')
    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
local logsData = GameData.getEntities(SkillData.Firemaking.logs,
    result = result..'\r\n|-'
function(obj)
    result = result..'\r\n|'..Icons.Icon({seed.name, type='item', size='50', notext=true})..'||[['..seed.name..']]'
return p.getRecipeRealm(obj) == realm.id
    result = result..'||'..seed.farmingLevel..'||'..seed.farmingXP..'||'..Shared.timeString(seed.timeToGrow, true)
end
    result = result..'||'..Icons.GP(seed.sellsFor)
)
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 = Shared.round(XP_BF / burnTime, 2, 2)
XP_BF = Shared.round(XP_BF, 2, 0)


    local crop = Items.getItemByID(seed.grownItemID)
table.insert(resultPart, '\r\n|-')
    result = result..'||'..Icons.Icon({crop.name, type='item', size='50', notext=true})..'||[['..crop.name..']]'
table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
    if category == 'Allotment' then
table.insert(resultPart, '||'..Icons.getExpansionIcon(logs.id)..Icons.Icon({name, type='item', noicon=true}))
      result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..crop.healsFor
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. level .. '"|'..reqText)
    end
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true))
    result = result..'||'..Icons.GP(crop.sellsFor)
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. baseXP .. '"| ' .. Shared.formatnum(baseXP))
    result = result..'||'..Items._getItemSources(seed)
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.formatnum(Shared.round(XPS, 2, 2)))
  end
if bonfireBonus == 0 then
table.insert(resultPart, '||colspan="4" class="table-na"| N/A ')
else
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. XP_BF .. '"| ' .. Shared.formatnum(XP_BF))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.formatnum(XPS_BF, 2, 2))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireBonus..'"|'..bonfireBonus..'%')
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireTime..'"|'..Shared.timeString(bonfireTime, true))
end
end


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


function p.getFarmingTable(frame)
function p.getAncientRelicsTable(frame)
  local category = frame.args ~= nil and frame.args[1] or frame
local skillName = frame.args ~= nil and frame.args[1] or frame
local skillID = nil
if skillName ~= nil and skillName ~= '' then
skillID = Constants.getSkillID(skillName)
if skillID == nil then
return Shared.printError('Failed to find a skill ID for ' .. skillName)
end
end
 
local resultPart = {}
table.insert(resultPart, '{| class="wikitable sortable stickyHeader lighttable"')
table.insert(resultPart, '\n|-class="headerRow-0"')
table.insert(resultPart, '\n|-\n!colspan="2"|Skill\n!Relic\n!Modifiers')
 
local relics = GameData.getEntities('ancientRelics',
function(relic)
return skillID == nil or relic.skillID == skillID
end)
table.sort(relics,
function (a, b)
local skillNameA, skillNameB = Constants.getSkillName(a.skillID), Constants.getSkillName(b.skillID)
if skillNameA == skillNameB then
-- Order by numbers at the end of relic IDs
-- Relics have a 'number' property, but this appears to contain duplicates
return string.sub(a.id, string.len(a.id)) < string.sub(b.id, string.len(b.id))
else
return skillNameA < skillNameB
end
end)
 
local function appendSkillRows(resultTable, rowTable, relicCount, skillID)
local skillName = Constants.getSkillName(skillID)
table.insert(resultTable, '\n|-\n|rowspan="' .. relicCount .. '"| ' .. Icons.Icon({skillName, type='skill', notext=true, size=50}))
table.insert(resultTable, '\n|rowspan="' .. relicCount .. '"| ' .. Icons.Icon({skillName, type='skill', noicon=true}))
table.insert(resultTable, table.concat(rowTable))
end
 
local skillRelicCount, currentSkillID, tablePart = 0, nil, {}
for i, relic in ipairs(relics) do
if currentSkillID == nil then
currentSkillID = relic.skillID
elseif relic.skillID ~= currentSkillID then
appendSkillRows(resultPart, tablePart, skillRelicCount, currentSkillID)
tablePart = {}
currentSkillID = relic.skillID
skillRelicCount = 0
end
 
skillRelicCount = skillRelicCount + 1
if skillRelicCount > 1 then
table.insert(tablePart, '\n|-')
end
table.insert(tablePart, '\n| ' .. skillRelicCount .. '\n| ' .. Constants.getModifiersText(relic.modifiers))
end
appendSkillRows(resultPart, tablePart, skillRelicCount, currentSkillID)
table.insert(resultPart, '\n|}')
 
return table.concat(resultPart)
end
 
function p.getLesserRelicsTable(frame)
local lesserRelics = {}
-- Iterate over each skill with a global rare drop then check
-- if the skill has a Lesser Relic drop
for skillLocalID, skill in pairs(SkillData) do
if skill.rareDrops ~= nil then
for i, drops in pairs(skill.rareDrops) do
if string.match(drops.itemID, '_Lesser_Relic') then
table.insert(lesserRelics, Items.getItemByID(drops.itemID))
end
end
end
end
table.sort(lesserRelics, function(a, b) return a.name < b.name end)
 
-- Create the Table
local resultTable = mw.html.create('table')
resultTable:addClass('wikitable sortable')
resultTable:tag('tr'):addClass('headerRow-0')
:tag('th'):wikitext('Icon')
:tag('th'):wikitext('Lesser Relic')
:tag('th'):wikitext('Modifiers')


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


return p
return p

Latest revision as of 17:55, 18 June 2024

Data pulled from Module:Skills/data


--This module should avoid including skill specific functions which generate
--output for wiki pages, especially those which require() other modules. For
--these functions, consider using the appropriate module from the below list.

--Some skills have their own modules:
--Module:Magic for Magic
--Module:Prayer for Prayer
--Module:Skills/Agility for Agility
--Module:Skills/Summoning for Summoning
--Module:Skills/Gathering for Mining, Fishing, Woodcutting
--Module:Skills/Artisan for Smithing, Cooking, Herblore, etc.

--Also be aware of:
--Module:Navboxes for navigation boxes appearing near the bottom of pages

local p = {}

local Shared = require('Module:Shared')
local Constants = require('Module:Constants')
local Common = require('Module:Common')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Modifiers = require('Module:Modifiers')
local Items = require('Module:Items')
local Icons = require('Module:Icons')

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

-- Given a skill ID & recipe, returns the skill level requirement for
-- that recipe and a boolean value indicating whether the level if abyssal or not.
-- If the level could not be determined, then the return value is nil, nil
function p.getRecipeLevelRealm(skillID, recipe)
	local level, isAbyssal = nil, nil
	-- Convert skillID to local ID if not already
	local ns, localSkillID = GameData.getLocalID(skillID)
	local realmID = p.getRecipeRealm(recipe)
	if localSkillID == 'Agility' then
		local course = GameData.getEntityByProperty(SkillData.Agility.courses, 'realm', realmID)
		isAbyssal = (realmID == 'melvorItA:Abyssal')
		-- For Agility, level is derived from obstacle category
		if recipe.category ~= nil then
			-- Obstacle
			level = course.obstacleSlots[recipe.category + 1].level
		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._AbyssalSkillReq(skillName, recipe.abyssalLevel, false))
	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, Shared.formatnum(recipe.totalMasteryRequired) .. ' ' .. Icons.Icon({skillName, type='skill', notext=true}) .. ' ' .. Icons.Icon({'Mastery'}))
	end
	local reqsData = {}
	if type(recipe.requirements) == 'table' then
		reqsData = Shared.shallowClone(recipe.requirements)
	end
	if recipe.shopItemPurchased ~= nil then
		-- Mining requirements are stored differently than other skills like
		-- Woodcutting, standardize here
		table.insert(reqsData, {
			["type"] = 'ShopPurchase',
			["purchaseID"] = recipe.shopItemPurchased,
			["count"] = 1
		})
	end
	if not Shared.tableIsEmpty(reqsData) then
		local reqs = Common.getRequirementString(reqsData)
		if reqs ~= nil then
			table.insert(reqText, reqs)
		end
	end
	return table.concat(reqText, '<br/>')
end

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

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


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

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

function p.getMasteryActionCount(skillID, realmID, levelLimit)
	local actCount = 0
	local skillNS, skillLocalID = Shared.getLocalID(skillID)
	local skillData = SkillData[skillLocalID]
	local recipeKey = p.getSkillRecipeKey(skillLocalID)
	if recipeKey ~= nil then
		local recipeData = skillData[recipeKey]
		for i, recipe in ipairs(recipeData) do
			if (
				p.getRecipeRealm(recipe) == realmID
				and (recipe.noMastery == nil or not recipe.noMastery)
				and (levelLimit == nil or p.getRecipeLevel(skillLocalID, recipe) <= levelLimit)
			) then
				actCount = actCount + 1
			end
		end
	end
	return actCount
end

-- Thieving
function p.getThievingNPCByID(npcID)
	return GameData.getEntityByID(SkillData.Thieving.npcs, npcID)
end

function p.getThievingNPC(npcName)
	return GameData.getEntityByName(SkillData.Thieving.npcs, npcName)
end

function p.getThievingNPCArea(npc)
	for i, area in ipairs(SkillData.Thieving.areas) do
		for j, npcID in ipairs(area.npcIDs) do
			if npcID == npc.id then
				return area
			end
		end
	end
end

function p._getThievingNPCStat(npc, statName)
	local result = nil

	if statName == 'level' then
		result = Icons._SkillReq('Thieving', npc.level)
	elseif statName == 'maxHit' then
		result = npc.maxHit * 10
	elseif statName == 'area' then
		local area = p.getThievingNPCArea(npc)
		result = area.name
	else
		result = npc[statName]
	end

	if result == nil then
		result = ''
	end

	return result
end

function p.getThievingNPCStat(frame)
	local npcName = frame.args ~= nil and frame.args[1] or frame[1]
	local statName = frame.args ~= nil and frame.args[2] or frame[2]
	local npc = p.getThievingNPC(npcName)
	if npc == nil then
		return Shared.printError('Invalid Thieving NPC ' .. npcName)
	end

	return p._getThievingNPCStat(npc, statName)
end

function p.getThievingSourcesForItem(itemID)
	local resultArray = {}
	local areaNPCs = {}

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

	--Now go through and get drop chances on each NPC if needed
	for i, npc in pairs(SkillData.Thieving.npcs) do
		local totalWt = 0
		local dropWt = 0
		local dropQty = { min = 0, max = 0 }
		for j, drop in ipairs(npc.lootTable) do
			totalWt = totalWt + drop.weight
			if drop.itemID == itemID then
				dropWt = drop.weight
				dropQty = { min = drop.minQuantity, max = drop.maxQuantity }
			end
		end
		if dropWt > 0 then
			table.insert(resultArray, {npc = npc.name, minQty = dropQty.min, maxQty = dropQty.max, wt = dropWt * SkillData.Thieving.itemChance, totalWt = totalWt * 100, level = npc.level, npcID = npc.id, type = 'npc'})
		end

		--Chance of -1 on unique drops is to indicate variable chance
		if npc.uniqueDrop ~= nil and npc.uniqueDrop.id == itemID then
			table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.quantity, maxQty = npc.uniqueDrop.quantity, wt = -1, totalWt = -1, level = npc.level, npcID = npc.id, type = 'npcUnique'})
		end

		local areaNPC = areaNPCs[npc.id]
		if areaNPC ~= nil then
			table.insert(resultArray, {npc = npc.name, minQty = areaNPC.qty, maxQty = areaNPC.qty, wt = SkillData.Thieving.baseAreaUniqueChance, totalWt = 100, level = npc.level, npcID = npc.id, area = areaNPC.area, type = 'areaUnique'})
		end
	end

	for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
		if drop.itemID == itemID then
			if drop.npcs == nil then
				table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = 1, npcID = itemID, type = 'generalRare'})
			else
				for j, npcID in ipairs(drop.npcs) do
					local npc = p.getThievingNPCByID(npcID)
					if npc ~= nil then
						table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = npc.level, npcID = npc.id, type = 'generalRare'})
					end
				end
			end
		end
	end

	return resultArray
end

-- Astrology
function p.getConstellationByID(constID)
	return GameData.getEntityByID(SkillData.Astrology.recipes, constID)
end

function p.getConstellation(constName)
	return GameData.getEntityByName(SkillData.Astrology.recipes, constName)
end

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

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

	for _, keyID in ipairs(modKeys) do
		result[keyID] = {}
		local mods = cons[keyID]
		if mods ~= nil then
			for _, mod in ipairs(mods) do
				local 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
					end
					table.insert(result[keyID], modAdj)
				end
			end
		end
	end
	return result
end

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

	local _, localSkillID = GameData.getLocalID(skillID)
	-- Clone so that we can sort by level
	local unlockTable = Shared.shallowClone(SkillData[localSkillID].masteryLevelUnlocks)
	if unlockTable == nil then
		return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
	end
	table.sort(unlockTable, function(a, b) return (a.level == b.level and a.descriptionID < b.descriptionID) or a.level < b.level end)

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

function p.getMasteryCheckpointTable(frame)
	local skillName = frame.args ~= nil and frame.args[1]
	local realmName = frame.args ~= nil and frame.args[2]
	local skillID = Constants.getSkillID(skillName)
	local realm = nil
	if realmName ~= nil then
		realm = GameData.getEntityByName('realms', realmName)
	else
		realm = GameData.getEntityByID('realms', 'melvorD:Melvor')
	end

	if skillID == nil then
		return Shared.printError('Failed to find a skill ID for ' .. skillName)
	elseif realm == nil then
		return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
	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..Shared.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|'..Shared.formatnum(totalPoolXP)
	result = result..'\r\n|}'
	return result
end

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

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

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

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

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

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

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

	return table.concat(resultPart)
end

-- Skill unlock costs for Adventure game mode
function p.getSkillUnlockCostTable()
	local advMode = GameData.getEntityByID('gamemodes', 'melvorF:Adventure')
	if advMode ~= nil then
		local unlockCount = Shared.tableCount(GameData.skillData) - Shared.tableCount(advMode.startingSkills)
		local costLength = Shared.tableCount(advMode.skillUnlockCost)
		local returnPart = {}
		table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative Cost')

		local accCost = 0
		for i = 1, unlockCount, 1 do
		local cost = advMode.skillUnlockCost[math.min(i, costLength)]
			accCost = accCost + cost
			table.insert(returnPart, '|-')
			table.insert(returnPart, '|' .. i .. '||' .. Icons.GP(cost) .. '||' .. Icons.GP(accCost))
		end
		table.insert(returnPart, '|}')

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

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

	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|-class="headerRow-0"')
	table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|Requirements')
	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')

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

		table.insert(resultPart, '\r\n|-')
		table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
		table.insert(resultPart, '||'..Icons.getExpansionIcon(logs.id)..Icons.Icon({name, type='item', noicon=true}))
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. level .. '"|'..reqText)
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true))
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. baseXP .. '"| ' .. Shared.formatnum(baseXP))
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.formatnum(Shared.round(XPS, 2, 2)))
		if bonfireBonus == 0 then
			table.insert(resultPart, '||colspan="4" class="table-na"| N/A ')
		else
			table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. XP_BF .. '"| ' .. Shared.formatnum(XP_BF))
			table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.formatnum(XPS_BF, 2, 2))
			table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireBonus..'"|'..bonfireBonus..'%')
			table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireTime..'"|'..Shared.timeString(bonfireTime, true))
		end
	end

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

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

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

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

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

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

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

	return table.concat(resultPart)
end

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

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

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

return p