Module:Skills: Difference between revisions

Adjusted the appearance of the Lesser Relics table
No edit summary
(Adjusted the appearance of the Lesser Relics table)
 
(76 intermediate revisions by 5 users not shown)
Line 1: Line 1:
--This module should avoid including skill specific functions which generate
--output for wiki pages, especially those which require() other modules. For
--these functions, consider using the appropriate module from the below list.
--Some skills have their own modules:
--Some skills have their own modules:
--Module:Magic and Module:Prayer being the current two examples.
--Module:Magic for Magic
--Module:Prayer for Prayer
--Module:Skills/Agility for Agility
--Module:Skills/Summoning for Summoning
--Module:Skills/Gathering for Mining, Fishing, Woodcutting
--Module:Skills/Artisan for Smithing, Cooking, Herblore, etc.
 
--Also be aware of:
--Module:Navboxes for navigation boxes appearing near the bottom of pages


local p = {}
local p = {}
local 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 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
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


function p.getSkillID(skillName)
-- Astrology
  for skName, ID in Shared.skpairs(Constants.skill) do
function p.getConstellationByID(constID)
    if skName == skillName then
return GameData.getEntityByID(SkillData.Astrology.recipes, constID)
      return ID
    end
  end
  return nil
end
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)
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.clone(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] 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 _, 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


  if SkillData.MasteryCheckpoints[skillID] == nil then
function p.getMasteryTokenTable()
    return 'ERROR: Failed to find Mastery Unlock data for '..skillName
-- Defines which skill levels should be included within the output
  end
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 bonuses = SkillData.MasteryCheckpoints[skillID].bonuses
-- Iterate over each skill with mastery, determining the number of
  local totalPoolXP = SkillData.MasteryPoolXP[skillID + 1]
-- 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 result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
local recipeKey = p.getSkillRecipeKey(skillLocalID)
  for i, bonus in Shared.skpairs(bonuses) do
if recipeKey ~= nil then
    result = result..'\r\n|-'
local recipeData = skill[recipeKey]
    result = result..'\r\n|'..(MasteryCheckpoints[i] * 100)..'%||'
for i, recipe in ipairs(recipeData) do
    result = result..Shared.formatnum(totalPoolXP * MasteryCheckpoints[i])..' xp||'..bonus
if recipe.noMastery == nil or not recipe.noMastery then
  end
local skillLevel = p.getRecipeLevel(skillLocalID, recipe)
  result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
if skillLevel ~= nil then
  result = result..'\r\n|'..Shared.formatnum(totalPoolXP)
for j, levelDef in ipairs(skillLevels) do
  result = result..'\r\n|}'
if skillLevel <= levelDef.level then
  return result
actCount[levelDef.id] = actCount[levelDef.id] + 1
end
end
end
end
end
end
end
table.insert(masteryActionCount, actCount)
end
end


function p._getFarmingTable(category)
local firstID = skillLevels[1].id
  local seedList = {}
table.sort(masteryActionCount,
  if category == 'Allotment' or category == 'Herb' or category == 'Tree' then
function(a, b)
    seedList = Items.getItems(function(item) return item.tier == category end)
if a[firstID] == b[firstID] then
  else
return a.skill.name < b.skill.name
    return 'ERROR: Invalid farming category. Please choose Allotment, Herb, or Tree'
else
  end
return a[firstID] > b[firstID]
end
end)
-- Generate output table
local resultPart = {}
local CCIIcon = Icons.Icon({CCI.name, type='item', notext=true})
local columnPairs = Shared.tableCount(skillLevels)


  local result = '{|class="wikitable sortable stickyHeader"'
-- Generate header
  result = result..'\r\n|- class="headerRow-0"'
table.insert(resultPart, '{| class="wikitable sortable"')
  result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
table.insert(resultPart, '\n!rowspan="3"|Token!!rowspan="3"|Skill!!colspan="' .. columnPairs * 2 .. '"|Approximate Mastery Token Chance')
  result = result..'!!XP!!Growth Time!!Seed Value'
table.insert(resultPart, '\n|-')
  if category == 'Allotment' then
for i, levelDef in ipairs(skillLevels) do
    result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
table.insert(resultPart, '\n!colspan="2"| ' .. levelDef.description)
  elseif category == 'Herb' then
end
    result = result..'!!colspan="2"|Herb!!Herb Value'
table.insert(resultPart, '\n|-' .. string.rep('\n!Without ' .. CCIIcon .. '\n!With ' .. CCIIcon, columnPairs))
  elseif category == 'Tree' then
    result = result..'!!colspan="2"|Logs!!Log Value'
  end
  result = result..'!!Seed Sources'
 
  table.sort(seedList, function(a, b) return a.farmingLevel < b.farmingLevel end)


  for i, seed in pairs(seedList) do
for i, rowData in ipairs(masteryActionCount) do
    result = result..'\r\n|-'
local token = Items.getItemByID(rowData.skill.masteryTokenID)
    result = result..'\r\n|'..Icons.Icon({seed.name, type='item', size='50', notext=true})..'||[['..seed.name..']]'
table.insert(resultPart, '\n|-')
    result = result..'||'..seed.farmingLevel..'||'..Shared.formatnum(seed.farmingXP)
table.insert(resultPart, '\n|style="text-align:center"|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
    result = result..'||data-sort-value="'..seed.timeToGrow..'"|'..Shared.timeString(seed.timeToGrow, true)
table.insert(resultPart, '\n|' .. Icons.Icon({rowData.skill.name, type='skill'}))
    result = result..'||data-sort-value="'..seed.sellsFor..'"|'..Icons.GP(seed.sellsFor)


    local crop = Items.getItemByID(seed.grownItemID)
for j, levelDef in ipairs(skillLevels) do
    result = result..'||'..Icons.Icon({crop.name, type='item', size='50', notext=true})..'||[['..crop.name..']]'
local actCount = rowData[levelDef.id]
    if category == 'Allotment' then
local denom, denomCCI = 0, 0
      result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..(crop.healsFor * 10)
if actCount > 0 then
    end
denom = math.floor(baseTokenChance / actCount)
    result = result..'||data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
denomCCI = Shared.round(baseTokenChance / (actCount * (1 + CCI.modifiers.increasedOffItemChance / 100)), 0, 0)
    result = result..'||'..Items._getItemSources(seed)
end
  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|}')


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


function p.getFarmingTable(frame)
-- Skill unlock costs for Adventure game mode
  local category = frame.args ~= nil and frame.args[1] or frame
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 p._getFarmingTable(category)
return table.concat(returnPart, '\r\n')
end
end
end


function p.getMiningTable(frame)
function p.getFiremakingTable(frame)
  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|Ore!!'..Icons.Icon({'Mining', type='skill', notext=true})..' Level'
table.insert(resultPart, '\r\n|-class="headerRow-0"')
  result = result..'!!XP!!Respawn Time!!Ore Value'
table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level')
  local mineData = Shared.clone(SkillData.Mining)
table.insert(resultPart, '!!rowspan="2"|Burn Time!!colspan="2"|Without Bonfire!!colspan="2"|With Bonfire!!rowspan="2"|Bonfire Bonus!!rowspan="2"|Bonfire Time')
  table.sort(mineData, function(a, b) return a.level < b.level end)
table.insert(resultPart, '\r\n|-class="headerRow-1"')
  for i, oreData in Shared.skpairs(mineData) do
table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')
    local ore = Items.getItemByID(oreData.ore)
 
    result = result..'\r\n|-\r\n|'..Icons.Icon({ore.name, type='item', size='50', notext=true})..'||'..ore.name
for i, logData in ipairs(SkillData.Firemaking.logs) do
    result = result..'||style="text-align:right"|'..oreData.level..'||style="text-align:right"|'..ore.miningXP
local logs = Items.getItemByID(logData.logID)
    result = result..'||style="text-align:right" data-sort-value="'..oreData.respawnInterval..'"|'
local name = logs.name
    result = result..Shared.timeString(oreData.respawnInterval / 1000, true)
local burnTime = logData.baseInterval / 1000
    result = result..'||data-sort-value="'..ore.sellsFor..'"|'..Icons.GP(ore.sellsFor)
local bonfireTime = logData.baseBonfireInterval / 1000
  end
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)


  result = result..'\r\n|}'
table.insert(resultPart, '\r\n|-')
  return result
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
end


function p.getPotionNavbox(frame)
function p.getAncientRelicsTable(frame)
  --•
local skillName = frame.args ~= nil and frame.args[1] or frame
  local result = '{| class="wikitable" style="margin:auto; clear:both; width: 100%"'
local skillID = nil
  result = result..'\r\n!colspan=2|'..Icons.Icon({'Herblore', 'Potions', type='skill'})
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 CombatPots = {}
local function appendSkillRows(resultTable, rowTable, relicCount, skillID)
  local SkillPots = {}
local skillName = Constants.getSkillName(skillID)
  for i, potData in Shared.skpairs(SkillData.Herblore.ItemData) do
table.insert(resultTable, '\n|-\n|rowspan="' .. relicCount .. '"| ' .. Icons.Icon({skillName, type='skill', notext=true, size=50}))
    if potData.category == 0 then
table.insert(resultTable, '\n|rowspan="' .. relicCount .. '"| ' .. Icons.Icon({skillName, type='skill', noicon=true}))
      table.insert(CombatPots, Icons.Icon({potData.name, type='item', img=(potData.name..' I')}))
table.insert(resultTable, table.concat(rowTable))
    else
end
      table.insert(SkillPots, Icons.Icon({potData.name, type='item', img=(potData.name..' I')}))
    end
  end


  result = result..'\r\n|-\r\n!Combat Potions\r\n|class="center" style="vertical-align:middle;"'
local skillRelicCount, currentSkillID, tablePart = 0, nil, {}
  result = result..'|'..table.concat(CombatPots, ' • ')
for i, relic in ipairs(relics) do
  result = result..'\r\n|-\r\n!Skill Potions\r\n|class="center" style="vertical-align:middle;"'
if currentSkillID == nil then
  result = result..'|'..table.concat(SkillPots, ' • ')
currentSkillID = relic.skillID
  result = result..'\r\n|}'
elseif relic.skillID ~= currentSkillID then
  return result
appendSkillRows(resultPart, tablePart, skillRelicCount, currentSkillID)
end
tablePart = {}
currentSkillID = relic.skillID
skillRelicCount = 0
end


function p.getSpecialFishingTable(frame)
skillRelicCount = skillRelicCount + 1
  local lootValue = 0
if skillRelicCount > 1 then
  local totalWt = Items.specialFishWt
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|}')


  local result = ''
return table.concat(resultPart)
  result = result..'\r\n{|class="wikitable sortable"'
end
  result = result..'\r\n!Item'
  result = result..'!!Price!!colspan="2"|Chance'


  --Sort the loot table by weight in descending order
function p.getLesserRelicsTable(frame)
  table.sort(Items.specialFishLoot, function(a, b) return a[2] > b[2] end)
local lesserRelics = {}
  for i, row in pairs(Items.specialFishLoot) do
-- Iterate over each skill with a global rare drop then check
    local thisItem = Items.getItemByID(row[1])
-- if the skill has a Lesser Relic drop
    result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
for skillLocalID, skill in pairs(SkillData) do
    result = result..'||style="text-align:left" data-sort-value="'..thisItem.sellsFor..'"'
if skill.rareDrops ~= nil then
    result = result..'|'..Icons.GP(thisItem.sellsFor)
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)


    local dropChance = (row[2] / totalWt) * 100
-- Create the Table
    result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
local resultTable = mw.html.create('table')
    result = result..'|'..Shared.fraction(row[2], totalWt)
resultTable:addClass('wikitable sortable')
    result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
resultTable:tag('tr'):addClass('headerRow-0')
    lootValue = lootValue + (dropChance * 0.01 * thisItem.sellsFor)
:tag('th'):wikitext('Icon')
  end
:tag('th'):wikitext('Lesser Relic')
  result = result..'\r\n|}'
:tag('th'):wikitext('Modifiers')
  result = result..'\r\nThe average value of a roll on the special fishing loot table is '..Icons.GP(Shared.round(lootValue, 2, 0))


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

edits