Module:Skills
From Melvor Idle
Data pulled from Module:GameData
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
--Some skills have their own modules:
--Module:Magic for Magic
--Module:Prayer for Prayer
--Module:Agility for Agility
--Module:Skills/Gathering for Mining, Fishing, Woodcutting
--Module:Skills/Artisan for Smithing, Cooking, Herblore, etc.
local p = {}
local ItemData = mw.loadData('Module:Items/data')
local SkillData = mw.loadData('Module:Skills/data')
local Constants = mw.loadData('Module:Constants/data')
local Shared = require('Module:Shared')
local Items = require('Module:Items')
local ItemSourceTables = require('Module:Items/SourceTables')
local Icons = require('Module:Icons')
local MasteryCheckpoints = {.1, .25, .5, .95}
function p.getSkillID(skillName)
for skName, ID in Shared.skpairs(Constants.skill) do
if skName == skillName then
return ID
end
end
return nil
end
function p.getSkillName(skillID)
for skName, ID in Shared.skpairs(Constants.skill) do
if ID == skillID then
return skName
end
end
return nil
end
function p.getThievingNPCByID(ID)
local result = Shared.clone(SkillData.Thieving[ID + 1])
if result ~= nil then
result.id = ID
end
return result
end
function p.getThievingNPC(name)
local result = nil
for i, npc in pairs(SkillData.Thieving) do
if name == npc.name then
result = Shared.clone(npc)
result.id = i - 1
break
end
end
return result
end
function p.getThievingNPCStat(frame)
local args = frame.args ~= nil and frame.args or frame
local npcName = args[1]
local statName = args[2]
local npc = p.getThievingNPC(npcName)
if npc == nil then
return 'ERROR: Failed to find Thieving NPC with name ' .. name .. '[[Category:Pages with script errors]]'
end
return p._getThievingNPCStat(npc, statName)
end
function p._getThievingNPCStat(npc, stat)
local itemDropChance = 0.75
local result = npc[stat]
-- Overrides below
if stat == 'maxHit' then
result = result * 10
elseif stat == 'lootList' then
return p._formatLootTable(npc['lootTable'], itemDropChance, true)
elseif stat == 'lootTable' then
return p._formatLootTable(npc['lootTable'], itemDropChance, false)
elseif stat == 'requirements' then
if npc['level'] ~= nil then
result = Icons._SkillReq('Thieving', npc['level'], true)
else
result = 'None'
end
elseif (stat == 'lootValue' or stat == 'pickpocketValue') then
if stat == 'pickpocketValue' then
local itemBP = Items.getItem("Bobby's Pocket")
result = (1 + npc['maxCoins']) / 2 + itemBP.sellsFor * (1 / 120)
else
result = 0
end
result = Shared.round(result + p._getLootTableValue(npc['lootTable']) * itemDropChance, 2, 2)
elseif stat == 'pageName' then
local linkOverrides = { ['Golbin'] = 'Golbin (thieving)' }
result = (linkOverrides[npc['name']] ~= nil and linkOverrides[npc['name']]) or npc['name']
end
return result
end
function p._getLootTableValue(lootTable)
-- Calculates the average GP value of a given loot table
-- Expects lootTableIn to be in format {{itemID_1, itemWeight_1}, ..., {itemID_n, itemWeight_n}}
if Shared.tableCount(lootTable) == 0 then
return 0
end
local totalWeight = 0
for i, drop in pairs(lootTable) do
totalWeight = totalWeight + drop[2]
end
if totalWeight == 0 then
return 0
end
local avgValue = 0
for i, drop in pairs(lootTable) do
local item = Items.getItemByID(drop[1])
if item ~= nil then
avgValue = avgValue + item.sellsFor * (drop[2] / totalWeight)
end
end
return avgValue
end
function p._formatLootTable(lootTableIn, chanceMultIn, asList)
-- Expects lootTableIn to be in format {{itemID_1, itemWeight_1}, ..., {itemID_n, itemWeight_n}}
if Shared.tableCount(lootTableIn) == 0 then
return ''
end
local chanceMult = (chanceMultIn or 1) * 100
local lootTable = Shared.clone(lootTableIn)
-- Sort table from most to least common drop
table.sort(lootTable, function(a, b)
if a[2] == b[2] then
return a[1] < b[1]
else
return a[2] > b[2]
end
end)
local totalWeight = 0
for i, drop in pairs(lootTable) do
totalWeight = totalWeight + drop[2]
end
if totalWeight == 0 then
return ''
end
-- Get the length (in characters) of the largest drop chance so that they can be right aligned
-- [4/16/21]: Adding info for no drop
local maxDropLen = math.max(string.len(Shared.round(100 - chanceMult, 2, 2)), string.len(Shared.round(lootTable[1][2] / totalWeight * chanceMult, 2, 2)))
local returnPart = {}
-- Generate header
if asList then
if chanceMult < 100 then
table.insert(returnPart, '* ' .. string.rep(' ', math.max(0, (maxDropLen - string.len(Shared.round(100 - chanceMult, 2, 2))) * 2)) .. Shared.round(100 - chanceMult, 2, 2) .. '% No Item')
end
else
table.insert(returnPart, '{|class="wikitable sortable"\r\n!Item!!Price!!colspan="2"|Chance')
end
-- Generate row for each item
for i, drop in pairs(lootTable) do
local item, itemText, sellsFor, dropChance = Items.getItemByID(drop[1]), 'Unknown', 0, Shared.round(drop[2] / totalWeight * chanceMult, 2, 2)
if item ~= nil then
itemText, sellsFor = Icons.Icon({item.name, type='item'}), item.sellsFor
end
if asList then
table.insert(returnPart, '* ' .. string.rep(' ', math.max(0, (maxDropLen - string.len(dropChance)) * 2)) .. dropChance .. '% ' .. itemText)
else
table.insert(returnPart, '|-\r\n|' .. itemText)
table.insert(returnPart, '|style="text-align:right;" data-sort-value="' .. sellsFor .. '"|' .. Icons.GP(sellsFor))
table.insert(returnPart, '|style="text-align:right;" data-sort-value="' .. dropChance .. '"|' .. Shared.fraction(drop[2] * chanceMult, totalWeight * 100))
table.insert(returnPart, '|style="text-align:right;"|' .. dropChance .. '%')
end
end
if not asList then
table.insert(returnPart, '|-class="sortbottom" \r\n!colspan="2"|Total:')
local textTotChance = ''
if chanceMult < 100 then
textTotChance = '|style="text-align:right"|' .. Shared.fraction(chanceMult, 100) .. '\r\n|'
else
textTotChance = '|colspan="2" '
end
textTotChance = textTotChance .. 'style="text-align:right;"|' .. Shared.round(chanceMult, 2, 2) .. '%' .. '\r\n|}'
table.insert(returnPart, textTotChance)
end
return table.concat(returnPart, '\r\n')
end
function p.getThievingNPCTable()
local returnPart = {}
-- Create table header
table.insert(returnPart, '{| class="wikitable sortable stickyHeader"')
table.insert(returnPart, '|- class="headerRow-0"\r\n!Target!!Name!!' .. Icons.Icon({'Thieving', type='skill', notext=true}).. ' Level!!Experience!!Max Hit!!Max Coins!!<abbr title="Assumes all loot is sold, and no GP boosts apply (such as those from Mastery & Gloves of Silence)">GP/Theft</abbr>')
-- Create row for each NPC
for i, npc in Shared.skpairs(SkillData.Thieving) do
local linkText = (npc.name ~= p._getThievingNPCStat(npc, 'pageName') and p._getThievingNPCStat(npc, 'pageName') .. '|' .. npc.name) or npc.name
table.insert(returnPart, '|-\r\n|style="text-align: left;" |' .. Icons.Icon({npc.name, type='thieving', size=50, notext=true}))
table.insert(returnPart, '|style="text-align: left;" |[[' .. linkText .. ']]')
table.insert(returnPart, '|style="text-align: right;" |' .. p._getThievingNPCStat(npc, 'level'))
table.insert(returnPart, '|style="text-align: right;" |' .. p._getThievingNPCStat(npc, 'xp'))
table.insert(returnPart, '|style="text-align: right;" |' .. p._getThievingNPCStat(npc, 'maxHit'))
table.insert(returnPart, '|style="text-align: right;" data-sort-value="' .. p._getThievingNPCStat(npc, 'maxCoins') .. '" |' .. Icons.GP(p._getThievingNPCStat(npc, 'maxCoins')))
table.insert(returnPart, '|style="text-align: right;" data-sort-value="' .. p._getThievingNPCStat(npc, 'pickpocketValue') .. '" |' .. Icons.GP(p._getThievingNPCStat(npc, 'pickpocketValue')))
end
table.insert(returnPart, '|}')
return table.concat(returnPart, '\r\n')
end
function p.getThievingNavbox()
local returnPart = {}
-- Create table header
table.insert(returnPart, '{| class="wikitable" style="text-align:center; clear:both; margin:auto; margin-bottom:1em;"')
table.insert(returnPart, '|-\r\n!' .. Icons.Icon({'Thieving', type='skill', notext=true}) .. '[[Thieving|Thieving Targets]]')
table.insert(returnPart, '|-\r\n|')
local npcList = {}
-- Create row for each NPC
for i, npc in Shared.skpairs(SkillData.Thieving) do
local linkText = (npc.name ~= p._getThievingNPCStat(npc, 'pageName') and p._getThievingNPCStat(npc, 'pageName') .. '|' .. npc.name) or npc.name
table.insert(npcList, Icons.Icon({npc.name, type='thieving', notext=true}) .. ' [[' .. linkText .. ']]')
end
table.insert(returnPart, table.concat(npcList, ' • '))
table.insert(returnPart, '|}')
return table.concat(returnPart, '\r\n')
end
function p.getMasteryUnlockTable(frame)
local skillName = frame.args ~= nil and frame.args[1] or frame
local skillID = p.getSkillID(skillName)
if skillID == nil then
return "ERROR: Failed to find a skill ID for "..skillName
end
local unlockTable = SkillData.MasteryUnlocks[skillID]
if unlockTable == nil then
return 'ERROR: Failed to find Mastery Unlock data for '..skillName
end
local result = '{|class="wikitable"\r\n!Level!!Unlock'
for i, unlock in Shared.skpairs(unlockTable) do
result = result..'\r\n|-'
result = result..'\r\n|'..unlock.level..'||'..unlock.unlock
end
result = result..'\r\n|}'
return result
end
function p.getMasteryCheckpointTable(frame)
local skillName = frame.args ~= nil and frame.args[1] or frame
local skillID = p.getSkillID(skillName)
if skillID == nil then
return "ERROR: Failed to find a skill ID for "..skillName
end
if SkillData.MasteryCheckpoints[skillID] == nil then
return 'ERROR: Failed to find Mastery Unlock data for '..skillName
end
local bonuses = SkillData.MasteryCheckpoints[skillID].bonuses
local totalPoolXP = SkillData.MasteryPoolXP[skillID + 1]
local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
for i, bonus in Shared.skpairs(bonuses) do
result = result..'\r\n|-'
result = result..'\r\n|'..(MasteryCheckpoints[i] * 100)..'%||'
result = result..Shared.formatnum(totalPoolXP * MasteryCheckpoints[i])..' xp||'..bonus
end
result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
result = result..'\r\n|'..Shared.formatnum(totalPoolXP)
result = result..'\r\n|}'
return result
end
function p._getFarmingTable(category)
local seedList = {}
if category == 'Allotment' or category == 'Herb' or category == 'Tree' then
seedList = Items.getItems(function(item) return item.tier == category end)
else
return 'ERROR: Invalid farming category. Please choose Allotment, Herb, or Tree'
end
local result = '{|class="wikitable sortable stickyHeader"'
result = result..'\r\n|- class="headerRow-0"'
result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
result = result..'!!XP!!Growth Time!!Seed Value'
if category == 'Allotment' then
result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
elseif category == 'Herb' then
result = result..'!!colspan="2"|Herb!!Herb Value'
elseif category == 'Tree' then
result = result..'!!colspan="2"|Logs!!Log Value'
end
result = result..'!!Seed Sources'
table.sort(seedList, function(a, b) return a.farmingLevel < b.farmingLevel end)
for i, seed in pairs(seedList) do
result = result..'\r\n|-'
result = result..'\r\n|'..Icons.Icon({seed.name, type='item', size='50', notext=true})..'||[['..seed.name..']]'
result = result..'||'..seed.farmingLevel..'||'..Shared.formatnum(seed.farmingXP)
result = result..'||data-sort-value="'..seed.timeToGrow..'"|'..Shared.timeString(seed.timeToGrow, true)
result = result..'||data-sort-value="'..seed.sellsFor..'"|'..Icons.GP(seed.sellsFor)
local crop = Items.getItemByID(seed.grownItemID)
result = result..'||'..Icons.Icon({crop.name, type='item', size='50', notext=true})..'||[['..crop.name..']]'
if category == 'Allotment' then
result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..(crop.healsFor * 10)
end
result = result..'||data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
result = result..'||'..ItemSourceTables._getItemSources(seed)
end
result = result..'\r\n|}'
return result
end
function p.getFarmingTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
return p._getFarmingTable(category)
end
function p.getFarmingFoodTable(frame)
local result = '{| class="wikitable sortable stickyHeader"'
result = result..'\r\n|- class="headerRow-0"'
result = result..'\r\n!colspan="2"|Crop!!'..Icons.Icon({"Farming", type="skill", notext=true})..' Level'
result = result..'!!Healing!!Value'
local itemArray = Items.getItems(function(item) return item.grownItemID ~= nil end)
table.sort(itemArray, function(a, b) return a.farmingLevel < b.farmingLevel end)
for i, item in Shared.skpairs(itemArray) do
local crop = Items.getItemByID(item.grownItemID)
if crop.healsFor ~= nil and crop.healsFor > 0 then
result = result..'\r\n|-'
result = result..'\r\n|'..Icons.Icon({crop.name, type='item', notext='true', size='50'})..'||[['..crop.name..']]'
result = result..'||style="text-align:right;"|'..item.farmingLevel
result = result..'||style="text-align:right" data-sort-value="'..crop.healsFor..'"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..(crop.healsFor * 10)
result = result..'||style="text-align:right" data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
end
end
result = result..'\r\n|}'
return result
end
function p.getFarmingPlotTable(frame)
local areaName = frame.args ~= nil and frame.args[1] or frame
local patches = nil
for i, area in Shared.skpairs(SkillData.Farming.Patches) do
if area.areaName == areaName then
patches = area.patches
break
end
end
if patches == nil then
return "ERROR: Invalid area name.[[Category:Pages with script errors"
end
local result = '{|class="wikitable"'
result = result..'\r\n!Plot!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level!!Cost'
for i, patch in Shared.skpairs(patches) do
result = result..'\r\n|-\r\n|'..i
result = result..'||style="text-align:right;" data-sort-value="0"|'..patch.level
if patch.cost == 0 then
result = result..'||Free'
else
result = result..'||style="text-align:right;" data-sort-value="'..patch.cost..'"|'..Icons.GP(patch.cost)
end
end
result = result..'\r\n|}'
return result
end
function p.getPotionNavbox(frame)
--•
local result = '{| class="wikitable" style="margin:auto; clear:both; width: 100%"'
result = result..'\r\n!colspan=2|'..Icons.Icon({'Herblore', 'Potions', type='skill'})
local CombatPots = {}
local SkillPots = {}
for i, potData in Shared.skpairs(SkillData.Herblore.ItemData) do
if potData.category == 0 then
table.insert(CombatPots, Icons.Icon({potData.name, type='item', img=(potData.name..' I')}))
else
if potData.name == 'Bird Nests Potion' then
table.insert(SkillPots, Icons.Icon({"Bird Nest Potion", type='item', img="Bird Nest Potion I"}))
else
table.insert(SkillPots, Icons.Icon({potData.name, type='item', img=(potData.name..' I')}))
end
end
end
result = result..'\r\n|-\r\n!Combat Potions\r\n|class="center" style="vertical-align:middle;"'
result = result..'|'..table.concat(CombatPots, ' • ')
result = result..'\r\n|-\r\n!Skill Potions\r\n|class="center" style="vertical-align:middle;"'
result = result..'|'..table.concat(SkillPots, ' • ')
result = result..'\r\n|}'
return result
end
function p.getSmithingTable(frame)
local tableType = frame.args ~= nil and frame.args[1] or frame
local bar = nil
if tableType ~= 'Smelting' then
bar = Items.getItem(tableType)
if bar == nil then
return 'ERROR: Could not find an item named '..tableType..' to build a smithing table with'
elseif bar.type ~= 'Bar' then
return 'ERROR: '..tableType.." is not a bar and thus can't be used for smithing"
end
end
local smithList = {}
for i, item in pairs(ItemData.Items) do
if item.smithingLevel ~= nil then
if tableType == 'Smelting' then
if item.type == 'Bar' then
table.insert(smithList, item)
end
else
for j, req in pairs(item.smithReq) do
if req.id == bar.id then
table.insert(smithList, item)
end
end
end
end
end
local result = '{|class="wikitable sortable stickyHeader"'
result = result..'\r\n|-class="headerRow-0"'
result = result..'\r\n!Item!!Name!!'..Icons.Icon({'Smithing', type='skill', notext=true})..' Level!!XP!!Value!!Ingredients'
--Adding value/bar for things other than smelting
if bar ~= nil then result = result..'!!Value/Bar' end
table.sort(smithList, function(a, b)
if a.smithingLevel ~= b.smithingLevel then
return a.smithingLevel < b.smithingLevel
else
return a.name < b.name
end end)
for i, item in Shared.skpairs(smithList) do
result = result..'\r\n|-'
result = result..'\r\n|'..Icons.Icon({item.name, type='item', size='50', notext=true})..'||'
local qty = item.smithingQty ~= nil and item.smithingQty or 1
if qty > 1 then
result = result..item.smithingQty..'x '
end
result = result..'[['..item.name..']]'
result = result..'||data-sort-value="'..item.smithingLevel..'"|'..Icons._SkillReq('Smithing', item.smithingLevel)
result = result..'||'..item.smithingXP
local totalValue = item.sellsFor * qty
result = result..'||data-sort-value="'..totalValue..'"|'..Icons.GP(item.sellsFor)
if qty > 1 then
result = result..' (x'..qty..')'
end
result = result..'||'
local barQty = 0
for i, mat in Shared.skpairs(item.smithReq) do
matItem = Items.getItemByID(mat.id)
if i > 1 then result = result..', ' end
result = result..Icons.Icon({matItem.name, type='item', qty=mat.qty, notext=true})
if bar ~= nil and mat.id == bar.id then
barQty = mat.qty
end
end
--Add the data for the value per bar
if bar ~= nil then
if barQty == 0 then
result = result..'||data-sort-value="0"|N/A'
else
local barVal = totalValue / barQty
result = result..'||data-sort-value="'..barVal..'"|'..Icons.GP(Shared.round(barVal, 1, 1))
end
end
end
result = result..'\r\n|}'
return result
end
function p.getFiremakingTable(frame)
local result = '{| class="wikitable sortable stickyHeader"'
result = result..'\r\n|-class="headerRow-0"'
result = result..'\r\n!colspan="2"|Logs!!'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level'
result = result..'!!XP!!Burn Time!!XP/s!!Bonfire Bonus!!Bonfire Time'
for i, logData in Shared.skpairs(SkillData.Firemaking) do
result = result..'\r\n|-'
local name = Shared.titleCase(logData.type..' Logs')
result = result..'\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true})
result = result..'||[['..name..']]'
result = result..'||style ="text-align: right;"|'..logData.level
result = result..'||style ="text-align: right;"|'..logData.xp
local burnTime = logData.interval / 1000
local XPS = logData.xp / burnTime
result = result..'||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true)
result = result..'||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.round(XPS, 2, 2)
result = result..'||style ="text-align: right;" data-sort-value="'..logData.bonfireBonus..'"|'..logData.bonfireBonus..'%'
result = result..'||style ="text-align: right;" data-sort-value="'..logData.bonfireInterval..'"|'..Shared.timeString(logData.bonfireInterval / 1000, true)
end
result = result..'\r\n|}'
return result
end
return p