Module:Skills: Difference between revisions

From Melvor Idle
(Move Farming functions to Module:Skills/Gathering)
(Adjusted the appearance of the Lesser Relics table)
 
(29 intermediate revisions by 3 users not shown)
Line 15: Line 15:


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


local Shared = require('Module:Shared')
local Shared = require('Module:Shared')
local Constants = require('Module:Constants')
local Constants = require('Module:Constants')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Items = require('Module:Items')
local Items = require('Module:Items')
local 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'
}
return recipeIDs[localSkillID]
end
 
-- Given a skill ID & recipe, returns the skill level requirement for
-- that recipe. If the level could not be determined, then the return
-- value is nil
function p.getRecipeLevel(skillID, recipe)
-- Convert skillID to local ID if not already
local ns, localSkillID = GameData.getLocalID(skillID)
if localSkillID == 'Agility' then
-- For Agility, level is derived from obstacle category
if recipe.category ~= nil then
-- Obstacle
return SkillData.Agility.obstacleUnlockLevels[recipe.category+1]
else
-- Pillar
local nsR, localRecipeID = GameData.getLocalID(recipe.id)
if localRecipeID ~= nil then
if string.find(localRecipeID, '^Pillar') ~= nil then
return 99
elseif string.find(localRecipeID, '^ElitePillar') ~= nil then
return 120
end
end
end
else
-- For all other skills, the recipe should have a level property
return recipe.level
end
end


-- Thieving
-- Thieving
function p.getThievingNPCByID(npcID)
return GameData.getEntityByID(SkillData.Thieving.npcs, npcID)
end
function p.getThievingNPC(npcName)
function p.getThievingNPC(npcName)
local result = nil
return GameData.getEntityByName(SkillData.Thieving.npcs, npcName)
for i, npc in Shared.skpairs(SkillData.Thieving.NPCs) do
if npc.name == npcName then
result = Shared.clone(npc)
break
end
end
return result
end
end


function p.getThievingNPCArea(npc)
function p.getThievingNPCArea(npc)
if type(npc) == 'string' then
for i, area in ipairs(SkillData.Thieving.areas) do
npc = p.getThievingNPC(npc)
for j, npcID in ipairs(area.npcIDs) do
end
 
local result = nil
for i, area in Shared.skpairs(SkillData.Thieving.Areas) do
for j, npcID in pairs(area.npcs) do
if npcID == npc.id then
if npcID == npc.id then
result = area
return area
break
end
end
end
end
end
end
return result
end
end


Line 81: Line 122:
local npc = p.getThievingNPC(npcName)
local npc = p.getThievingNPC(npcName)
if npc == nil then
if npc == nil then
return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
return Shared.printError('Invalid Thieving NPC ' .. npcName)
end
end


Line 89: Line 130:
function p.getThievingSourcesForItem(itemID)
function p.getThievingSourcesForItem(itemID)
local resultArray = {}
local resultArray = {}
local areaNPCs = {}
local areaNPCs = {}


--First check area unique drops
--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
--If an area drops the item, add all the NPC ids to the list so we can add them later
if not result then
for i, area in pairs(SkillData.Thieving.areas) do
for i, area in pairs(SkillData.Thieving.Areas) do
for j, drop in pairs(area.uniqueDrops) do
for j, drop in pairs(area.uniqueDrops) do
if drop.id == itemID then
if drop.itemID == itemID then
for k, npcID in ipairs(area.npcIDs) do
for k, npcID in pairs(area.npcs) do
areaNPCs[npcID] = { qty = drop.quantity, area = area }
areaNPCs[npcID] = drop.qty
end
break
end
end
break
end
end
end
end
Line 108: Line 146:


--Now go through and get drop chances on each NPC if needed
--Now go through and get drop chances on each NPC if needed
for i, npc in pairs(SkillData.Thieving.NPCs) do
for i, npc in pairs(SkillData.Thieving.npcs) do
local totalWt = 0
local totalWt = 0
local dropWt = 0
local dropWt = 0
local dropQty = 0
local dropQty = { min = 0, max = 0 }
for j, drop in pairs(npc.lootTable) do
for j, drop in ipairs(npc.lootTable) do
totalWt = totalWt + drop[2]
totalWt = totalWt + drop.weight
if drop[1] == itemID then
if drop.itemID == itemID then
dropWt = drop[2]
dropWt = drop.weight
dropQty = drop[3]
dropQty = { min = drop.minQuantity, max = drop.maxQuantity }
end
end
end
end
if dropWt > 0 then
if dropWt > 0 then
table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = dropQty, wt = dropWt * SkillData.Thieving.ItemChance, totalWt = totalWt * 100, level = npc.level})
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
end


--Chance of -1 on unique drops is to indicate variable chance
--Chance of -1 on unique drops is to indicate variable chance
if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID == itemID then
if npc.uniqueDrop ~= nil and npc.uniqueDrop.id == itemID then
table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.qty, maxQty = npc.uniqueDrop.qty, wt = -1, totalWt = -1, level = npc.level})
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
end


if areaNPCs[npc.id] ~= nil then
local areaNPC = areaNPCs[npc.id]
table.insert(resultArray, {npc = npc.name, minQty = areaNPCs[npc.id], maxQty = areaNPCs[npc.id], wt = SkillData.Thieving.AreaUniqueChance, totalWt = 100, level = npc.level})
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
end
end


for i, drop in pairs(SkillData.Thieving.RareItems) do
for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
if drop.itemID == itemID then
if drop.itemID == itemID then
table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = 1})
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
end
end
Line 144: Line 192:
-- Astrology
-- Astrology
function p.getConstellationByID(constID)
function p.getConstellationByID(constID)
return SkillData.Astrology.Constellations[constID]
return GameData.getEntityByID(SkillData.Astrology.recipes, constID)
end
end


function p.getConstellation(constName)
function p.getConstellation(constName)
for i, const in ipairs(SkillData.Astrology.Constellations) do
return GameData.getEntityByName(SkillData.Astrology.recipes, constName)
if const.name == constName then
return const
end
end
return nil
end
end


function p.getConstellations(checkFunc)
function p.getConstellations(checkFunc)
local result = {}
return GameData.getEntities(SkillData.Astrology.recipes, checkFunc)
for i, const in ipairs(SkillData.Astrology.Constellations) do
if checkFunc(const) then
table.insert(result, const)
end
end
return result
end
end


Line 203: Line 240:
table.insert(modTypes, 'uniqueModifiers')
table.insert(modTypes, 'uniqueModifiers')
end
end
local masteryReq = {
['standardModifiers'] = { 1, 40, 80 },
['uniqueModifiers'] = { 20, 60, 99 }
}


local modArray = {}
local modArray = {}
local isSkillMod = {}
local isSkillMod = {}
--Adding a Group Number to hold together different bonuses from the same modifier [Falterfire 22/10/27]
local groupNum = 0
for _, modType in ipairs(modTypes) do
for _, modType in ipairs(modTypes) do
for i, skillMods in ipairs(cons[modType]) do
for i, modTypeData in ipairs(cons[modType]) do
local skillID = cons.skills[i]
groupNum = masteryReq[modType][i]
if skillID ~= nil then
local modVal = nil
for j, modName in ipairs(skillMods) do
if modValue ~= nil then
local modBaseName, modText, sign, isNegative, unsign, modBase = Constants.getModifierDetails(modName)
modVal = modValue
-- Check if modifier varies by skill, and amend the modifier value accordingly
else
local modVal = modValue
modVal = modTypeData.incrementValue * modTypeData.maxCount
if Shared.contains(modText, '{SV0}') then
end
isSkillMod[modName] = true
for j, modifier in ipairs(modTypeData.modifiers) do
modVal = {skillID, modValue}
local modEntry = (modifier.skill ~= nil and { skillID = modifier.skill, value = modVal }) or modVal
end
addToArray(modArray, {modifier.key, modEntry, group = groupNum})
addToArray(modArray, {modName, modVal})
end
end
end
end
end
Line 228: Line 270:
for i, modDefn in ipairs(modArray) do
for i, modDefn in ipairs(modArray) do
local modName, modVal = modDefn[1], modDefn[2]
local modName, modVal = modDefn[1], modDefn[2]
local isSkill = isSkillMod[modName]
local isSkill = type(modVal) == 'table' and modVal.skillID ~= nil
if modArrayKV[modName] == nil then
if modArrayKV[modName] == nil then
modArrayKV[modName] = (isSkill and { modVal } or modVal)
modArrayKV[modName] = (isSkill and { modVal } or modVal)
Line 248: Line 290:
local skillID = Constants.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)
-- Clone so that we can sort by level
local unlockTable = Shared.clone(SkillData[localSkillID].masteryLevelUnlocks)
if unlockTable == nil then
if unlockTable == nil then
return 'ERROR: Failed to find Mastery Unlock data for '..skillName
return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
end
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|}'
Line 269: Line 314:
local skillID = Constants.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


if SkillData.MasteryCheckpoints[skillID] == nil then
local _, localSkillID = GameData.getLocalID(skillID)
return 'ERROR: Failed to find Mastery Unlock data for '..skillName
local checkpoints = SkillData[localSkillID].masteryCheckpoints
if checkpoints == nil then
return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
end
end


local bonuses = SkillData.MasteryCheckpoints[skillID].bonuses
local totalPoolXP = SkillData[localSkillID].baseMasteryPoolCap
local totalPoolXP = SkillData.MasteryPoolXP[skillID + 1]
local checkpointPct = GameData.rawData.masteryCheckpoints
 
local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
for i, bonus in Shared.skpairs(bonuses) do
for i, checkpointDesc in ipairs(checkpoints) do
result = result..'\r\n|-'
result = result..'\r\n|-'
result = result..'\r\n|'..(MasteryCheckpoints[i] * 100)..'%||'
result = result..'\r\n|'..checkpointPct[i]..'%||'
result = result..Shared.formatnum(totalPoolXP * MasteryCheckpoints[i])..' xp||'..bonus
result = result..Shared.formatnum(math.floor(totalPoolXP * checkpointPct[i] / 100))..' xp||'..checkpointDesc
end
end
result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
Line 292: Line 338:


function p.getMasteryTokenTable()
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 baseTokenChance = 18500
local masterySkills = {}
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


-- Find all mastery tokens
-- Iterate over each skill with mastery, determining the number of
local masteryTokens = Items.getItems(function(item) return item.isToken ~= nil and item.skill ~= nil and item.isToken end)
-- mastery actions for each
for i, item in pairs(masteryTokens) do
for skillLocalID, skill in pairs(SkillData) do
local milestones = SkillData.Milestones[item.skill + 1]
if skill.masteryTokenID ~= nil then
if milestones ~= nil then
local actCount = { ["skill"] = skill }
table.insert(masterySkills, {tokenRef = i, skillID = item.skill, milestoneCount = milestones})
for i, levelDef in ipairs(skillLevels) do
actCount[levelDef.id] = 0
end
 
local recipeKey = p.getSkillRecipeKey(skillLocalID)
if recipeKey ~= nil then
local recipeData = skill[recipeKey]
for i, recipe in ipairs(recipeData) do
if recipe.noMastery == nil or not recipe.noMastery then
local skillLevel = p.getRecipeLevel(skillLocalID, recipe)
if skillLevel ~= nil then
for j, levelDef in ipairs(skillLevels) do
if skillLevel <= levelDef.level then
actCount[levelDef.id] = actCount[levelDef.id] + 1
end
end
end
end
end
end
table.insert(masteryActionCount, actCount)
end
end
end
end
table.sort(masterySkills, function(a, b)
if a['milestoneCount'] == b['milestoneCount'] then
return a['skillID'] < b['skillID']
else
return a['milestoneCount'] > b['milestoneCount']
end
end)


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
-- Generate output table
local resultPart = {}
local resultPart = {}
local CCI = Items.getItem('Clue Chasers Insignia')
local CCIIcon = Icons.Icon({CCI.name, type='item', notext=true})
local CCIIcon = Icons.Icon({'Clue Chasers Insignia', type='item', notext=true})
local columnPairs = Shared.tableCount(skillLevels)
if CCI == nil then return '' end


-- Generate header
table.insert(resultPart, '{| class="wikitable sortable"')
table.insert(resultPart, '{| class="wikitable sortable"')
table.insert(resultPart, '\r\n!rowspan="2"|Token!!rowspan="2"|Skill!!colspan="2"|Approximate Mastery Token Chance')
table.insert(resultPart, '\n!rowspan="3"|Token!!rowspan="3"|Skill!!colspan="' .. columnPairs * 2 .. '"|Approximate Mastery Token Chance')
table.insert(resultPart, '\r\n|-\r\n!Without ' .. CCIIcon .. '!!With ' .. CCIIcon)
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, m in ipairs(masterySkills) do
for i, rowData in ipairs(masteryActionCount) do
local token = masteryTokens[m.tokenRef]
local token = Items.getItemByID(rowData.skill.masteryTokenID)
local denom = math.floor(baseTokenChance / m['milestoneCount'])
table.insert(resultPart, '\n|-')
local denomCCI = Shared.round(baseTokenChance / (m['milestoneCount'] * (1 + CCI.increasedItemChance / 100)), 0, 0)
table.insert(resultPart, '\n|style="text-align:center"|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
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|style="text-align:center"|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
local actCount = rowData[levelDef.id]
table.insert(resultPart, '\r\n|' .. Icons.Icon({Constants.getSkillName(m['skillID']), type='skill'}))
local denom, denomCCI = 0, 0
table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Shared.formatnum(denom))
if actCount > 0 then
table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. denomCCI .. '"|1/' .. Shared.formatnum(denomCCI))
denom = math.floor(baseTokenChance / actCount)
denomCCI = Shared.round(baseTokenChance / (actCount * (1 + CCI.modifiers.increasedOffItemChance / 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
end
table.insert(resultPart, '\r\n|}')
table.insert(resultPart, '\n|}')


return table.concat(resultPart)
return table.concat(resultPart)
Line 339: Line 435:
-- Skill unlock costs for Adventure game mode
-- Skill unlock costs for Adventure game mode
function p.getSkillUnlockCostTable()
function p.getSkillUnlockCostTable()
local returnPart = {}
local advMode = GameData.getEntityByID('gamemodes', 'melvorF:Adventure')
table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative Cost')
if advMode ~= nil then
 
local unlockCount = Shared.tableCount(GameData.skillData) - Shared.tableCount(advMode.startingSkills)
local accCost = 0
local costLength = Shared.tableCount(advMode.skillUnlockCost)
for i, cost in ipairs(SkillData.SkillUnlockCosts) do
local returnPart = {}
accCost = accCost + cost
table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative 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
 
-- Accepts 1 parameter, being either:
--  'Smelting', for which a table of all bars is generated, or
--  A bar or tier name, which if valid generates a table of all smithing recipes using that bar/tier
function p.getSmithingTable(frame)
local tableType = frame.args ~= nil and frame.args[1] or frame
tableType = Shared.splitString(tableType, ' ')[1]
-- Translates Smithing category names to Smithing recipe data categories
local categoryMap = {
['Smelting'] = 0,
['Bronze'] = 1,
['Iron'] = 2,
['Steel'] = 3,
['Mithril'] = 4,
['Adamant'] = 5,
['Adamantite'] = 5,
['Rune'] = 6,
['Runite'] = 6,
['Dragon'] = 7,
['Dragonite'] = 7
}
local categoryID = categoryMap[tableType]
if categoryID == nil then
return 'ERROR: Invalid Smithing category: "' .. tableType .. '"[[Category:Pages with script errors]]'
end


-- Build a list of recipes to be included, and a list of bars while we're at it
local accCost = 0
-- The bar list will be used later for value/bar calculations
for i = 1, unlockCount, 1 do
local recipeList, barIDList = {}, {}
local cost = advMode.skillUnlockCost[math.min(i, costLength)]
for i, recipe in ipairs(SkillData.Smithing.Recipes) do
accCost = accCost + cost
if recipe.category == categoryID then
table.insert(returnPart, '|-')
local recipeItem = Items.getItemByID(recipe.itemID)
table.insert(returnPart, '|' .. i .. '||' .. Icons.GP(cost) .. '||' .. Icons.GP(accCost))
if recipeItem ~= nil then
table.insert(recipeList, { id = i, level = recipe.level, itemName = recipeItem.name, itemValue = recipeItem.sellsFor })
end
elseif recipe.category == 0 then
barIDList[recipe.itemID] = true
end
end
end
table.insert(returnPart, '|}')


-- Generate output table
return table.concat(returnPart, '\r\n')
local resultPart = {}
table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
table.insert(resultPart, '\r\n|-class="headerRow-0"')
table.insert(resultPart, '\r\n!Item!!Name!!'..Icons.Icon({'Smithing', type='skill', notext=true})..' Level!!XP!!Value!!Ingredients')
--Adding value/bar for things other than smelting
if categoryID > 0 then
table.insert(resultPart, '!!Value/Bar')
end
end
table.sort(recipeList, function(a, b)
if a.level ~= b.level then
return a.level < b.level
else
return a.itemName < b.itemName
end
end)
for i, recipeDef in ipairs(recipeList) do
local recipe = SkillData.Smithing.Recipes[recipeDef.id]
local totalValue = recipe.baseQuantity * recipeDef.itemValue
-- Determine the bar quantity & build the recipe cost string
local barQty, costString = 0, {}
for j, itemCost in ipairs(recipe.itemCosts) do
local costItem = Items.getItemByID(itemCost.id)
if costItem ~= nil then
table.insert(costString, Icons.Icon({costItem.name, type='item', qty=itemCost.qty, notext=true}))
end
if barIDList[itemCost.id] then
barQty = barQty + itemCost.qty
end
end
table.insert(resultPart, '\r\n|-')
table.insert(resultPart, '\r\n| ' .. Icons.Icon({recipeDef.itemName, type='item', size=50, notext=true}))
table.insert(resultPart, '\r\n| ')
if recipe.baseQuantity > 1 then
table.insert(resultPart, recipe.baseQuantity .. 'x ')
end
table.insert(resultPart, Icons.Icon({recipeDef.itemName, type='item', noicon=true}))
table.insert(resultPart, '\r\n|data-sort-value="' .. recipe.level .. '"| ' .. Icons._SkillReq('Smithing', recipe.level))
table.insert(resultPart, '\r\n|data-sort-value="' .. recipe.baseXP .. '"| ' .. Shared.formatnum(recipe.baseXP))
table.insert(resultPart, '\r\n|data-sort-value="' .. totalValue .. '"| ' .. Icons.GP(recipeDef.itemValue))
if recipe.baseQuantity > 1 then
table.insert(resultPart, ' (x' .. recipe.baseQuantity .. ')')
end
table.insert(resultPart, '\r\n| ' .. table.concat(costString, ', '))
if categoryID > 0 then
local barVal, barValTxt = 0, 'N/A'
if barQty > 0 then
barVal = totalValue / barQty
barTxt = Icons.GP(Shared.round(barVal, 1, 1))
end
table.insert(resultPart, '\r\n|data-sort-value="' .. barVal .. '"| ' .. barTxt)
end
end
table.insert(resultPart, '\r\n|}')
return table.concat(resultPart)
end
end


Line 462: Line 464:
table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')
table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')


for i, logData in Shared.skpairs(SkillData.Firemaking) do
for i, logData in ipairs(SkillData.Firemaking.logs) do
local logs = Items.getItemByID(logData.logID)
local logs = Items.getItemByID(logData.logID)
local name = logs.name
local name = logs.name
local burnTime = logData.baseInterval / 1000
local burnTime = logData.baseInterval / 1000
local bonfireTime = logData.baseBonfireInterval / 1000
local bonfireTime = logData.baseBonfireInterval / 1000
local XPS = logData.baseXP / burnTime
local XPS = logData.baseExperience / burnTime
local XP_BF = logData.baseXP * (1 + logData.bonfireXPBonus / 100)
local XP_BF = logData.baseExperience * (1 + logData.bonfireXPBonus / 100)
local XPS_BF = XP_BF / burnTime
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|-')
table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
table.insert(resultPart, '||[['..name..']]')
table.insert(resultPart, '||'..Icons.getExpansionIcon(logs.id)..Icons.Icon({name, type='item', noicon=true}))
table.insert(resultPart, '||style ="text-align: right;"|'..logData.level)
table.insert(resultPart, '||style ="text-align: right;"|'..logData.level)
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true))
table.insert(resultPart, '||style ="text-align: right;"|'..logData.baseXP)
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. logData.baseExperience .. '"| ' .. Shared.formatnum(logData.baseExperience))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.round(XPS, 2, 2))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.formatnum(Shared.round(XPS, 2, 2)))
table.insert(resultPart, '||style ="text-align: right;"|'..Shared.round(XP_BF, 2, 0))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. XP_BF .. '"| ' .. Shared.formatnum(XP_BF))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.round(XPS_BF, 2, 2))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.formatnum(XPS_BF, 2, 2))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..logData.bonfireXPBonus..'"|'..logData.bonfireXPBonus..'%')
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..logData.bonfireXPBonus..'"|'..logData.bonfireXPBonus..'%')
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireTime..'"|'..Shared.timeString(bonfireTime, true))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireTime..'"|'..Shared.timeString(bonfireTime, true))
Line 486: Line 489:
table.insert(resultPart, '\r\n|}')
table.insert(resultPart, '\r\n|}')
return table.concat(resultPart)
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
end


return p
return p

Latest revision as of 00:12, 26 March 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 GameData = require('Module:GameData')
local SkillData = GameData.skillData
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'
	}
	return recipeIDs[localSkillID]
end

-- Given a skill ID & recipe, returns the skill level requirement for
-- that recipe. If the level could not be determined, then the return
-- value is nil
function p.getRecipeLevel(skillID, recipe)
	-- Convert skillID to local ID if not already
	local ns, localSkillID = GameData.getLocalID(skillID)
	if localSkillID == 'Agility' then
		-- For Agility, level is derived from obstacle category
		if recipe.category ~= nil then
			-- Obstacle
			return SkillData.Agility.obstacleUnlockLevels[recipe.category+1]
		else
			-- Pillar
			local nsR, localRecipeID = GameData.getLocalID(recipe.id)
			if localRecipeID ~= nil then
				if string.find(localRecipeID, '^Pillar') ~= nil then
					return 99
				elseif string.find(localRecipeID, '^ElitePillar') ~= nil then
					return 120
				end
			end
		end
	else
		-- For all other skills, the recipe should have a level property
		return recipe.level
	end
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

-- For a given constellation cons and modifier value modValue, generates and returns
-- a table of modifiers, much like any other item/object elsewhere in the game.
-- includeStandard: true|false, determines whether standard modifiers are included
-- includeUnique: true|false, determines whether unique modifiers are included
-- isDistinct: true|false, if true, the returned list of modifiers is de-duplicated
-- asKeyValue: true|false, if true, returns key/value pairs like usual modifier objects
function p._buildAstrologyModifierArray(cons, modValue, includeStandard, includeUnique, isDistinct, asKeyValue)
	-- Temporary function to determine if the table already contains a given modifier
	local containsMod = function(modList, modNew)
			for i, modItem in ipairs(modList) do
				-- Check mod names & value data types both equal
				if modItem[1] == modNew[1] and type(modItem[2]) == type(modNew[2]) then
					if type(modItem[2]) == 'table' then
						if Shared.tablesEqual(modItem[2], modNew[2]) then
							return true
						end
					elseif modItem[2] == modNew[2] then
						return true
					end
				end
			end
			return false
		end

	local addToArray = function(modArray, modNew)
			if not isDistinct or (isDistinct and not containsMod(modArray, modNew)) then
				table.insert(modArray, modNew)
			end
		end

	local modTypes = {}
	if includeStandard then
		table.insert(modTypes, 'standardModifiers')
	end
	if includeUnique then
		table.insert(modTypes, 'uniqueModifiers')
	end
	local masteryReq = {
		['standardModifiers'] = { 1, 40, 80 },
		['uniqueModifiers'] = { 20, 60, 99 }
	}

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

	for _, modType in ipairs(modTypes) do
		for i, modTypeData in ipairs(cons[modType]) do
			groupNum = masteryReq[modType][i]
			local modVal = nil
			if modValue ~= nil then
				modVal = modValue
			else
				modVal = modTypeData.incrementValue * modTypeData.maxCount
			end
			for j, modifier in ipairs(modTypeData.modifiers) do
				local modEntry = (modifier.skill ~= nil and { skillID = modifier.skill, value = modVal }) or modVal
				addToArray(modArray, {modifier.key, modEntry, group = groupNum})
			end
		end
	end

	if asKeyValue then
		local modArrayKV = {}
		for i, modDefn in ipairs(modArray) do
			local modName, modVal = modDefn[1], modDefn[2]
			local isSkill = type(modVal) == 'table' and modVal.skillID ~= nil
			if modArrayKV[modName] == nil then
				modArrayKV[modName] = (isSkill and { modVal } or modVal)
			elseif isSkill then
				table.insert(modArrayKV[modName], modVal)
			else
				modArrayKV[modName] = modArrayKV[modName] + modVal
			end
		end
		return modArrayKV
	else
		return modArray
	end
end

-- 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.clone(SkillData[localSkillID].masteryLevelUnlocks)
	if unlockTable == nil then
		return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
	end
	table.sort(unlockTable, function(a, b) return (a.level == b.level and a.descriptionID < b.descriptionID) or a.level < b.level end)

	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] or frame
	local skillID = Constants.getSkillID(skillName)
	if skillID == nil then
		return Shared.printError('Failed to find a skill ID for ' .. skillName)
	end

	local _, localSkillID = GameData.getLocalID(skillID)
	local checkpoints = SkillData[localSkillID].masteryCheckpoints
	if checkpoints == nil then
		return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
	end

	local totalPoolXP = SkillData[localSkillID].baseMasteryPoolCap
	local checkpointPct = GameData.rawData.masteryCheckpoints
	local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
	for i, checkpointDesc in ipairs(checkpoints) do
		result = result..'\r\n|-'
		result = result..'\r\n|'..checkpointPct[i]..'%||'
		result = result..Shared.formatnum(math.floor(totalPoolXP * checkpointPct[i] / 100))..' xp||'..checkpointDesc
	end
	result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
	result = result..'\r\n|'..Shared.formatnum(totalPoolXP)
	result = result..'\r\n|}'
	return result
end

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

	-- Iterate over each skill with mastery, determining the number of
	-- mastery actions for each
	for skillLocalID, skill in pairs(SkillData) do
		if skill.masteryTokenID ~= nil then
			local actCount = { ["skill"] = skill }
			for i, levelDef in ipairs(skillLevels) do
				actCount[levelDef.id] = 0
			end

			local recipeKey = p.getSkillRecipeKey(skillLocalID)
			if recipeKey ~= nil then
				local recipeData = skill[recipeKey]
				for i, recipe in ipairs(recipeData) do
					if recipe.noMastery == nil or not recipe.noMastery then
						local skillLevel = p.getRecipeLevel(skillLocalID, recipe)
						if skillLevel ~= nil then
							for j, levelDef in ipairs(skillLevels) do
								if skillLevel <= levelDef.level then
									actCount[levelDef.id] = actCount[levelDef.id] + 1
								end
							end
						end
					end
				end
			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 = Items.getItemByID(rowData.skill.masteryTokenID)
		table.insert(resultPart, '\n|-')
		table.insert(resultPart, '\n|style="text-align:center"|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
		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.increasedOffItemChance / 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 resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|-class="headerRow-0"')
	table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level')
	table.insert(resultPart, '!!rowspan="2"|Burn Time!!colspan="2"|Without Bonfire!!colspan="2"|With Bonfire!!rowspan="2"|Bonfire Bonus!!rowspan="2"|Bonfire Time')
	table.insert(resultPart, '\r\n|-class="headerRow-1"')
	table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')

	for i, logData in ipairs(SkillData.Firemaking.logs) do
		local logs = Items.getItemByID(logData.logID)
		local name = logs.name
		local burnTime = logData.baseInterval / 1000
		local bonfireTime = logData.baseBonfireInterval / 1000
		local XPS = logData.baseExperience / burnTime
		local XP_BF = logData.baseExperience * (1 + logData.bonfireXPBonus / 100)
		local XPS_BF = 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;"|'..logData.level)
		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="' .. logData.baseExperience .. '"| ' .. Shared.formatnum(logData.baseExperience))
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.formatnum(Shared.round(XPS, 2, 2)))
		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="'..logData.bonfireXPBonus..'"|'..logData.bonfireXPBonus..'%')
		table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireTime..'"|'..Shared.timeString(bonfireTime, true))
	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