Module:Skills/Gathering: Difference between revisions

From Melvor Idle
(_buildAstrologyConstellationTable: Remove time column, which has been relocated to Astrology/Training)
(Update for v1.1)
Line 1: Line 1:
--Splitting some functions into here to avoid bloating a single file
--Splitting some functions into here to avoid bloating a single file
local p = {}
local p = {}
local SkillData = mw.loadData('Module:Skills/data')
local ShopData = mw.loadData('Module:Shop/data')


local Constants = require('Module:Constants')
local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local Shared = require('Module:Shared')
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 Shop = require('Module:Shop')
local Skills = require('Module:Skills')
local Skills = require('Module:Skills')
local ItemSourceTables = require('Module:Items/SourceTables')
local ItemSourceTables = require('Module:Items/SourceTables')


-- TODO Move tool tables to Module:Shop
function p.getAxeTable(frame)
function p.getAxeTable(frame)
local toolArray = {}
local toolArray = Shop.getPurchases(
for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
function(purch)
if Shared.contains(upgrade.name, 'Axe') then
return purch.category == 'melvorD:SkillUpgrades' and string.find(purch.id, '_Axe$') ~= nil
table.insert(toolArray, upgrade)
end)
end
end


local result = '{| class="wikitable"'
local result = '{| class="wikitable"'
Line 28: Line 27:
local total = 0
local total = 0


for i, tool in Shared.skpairs(toolArray) do
for i, tool in ipairs(toolArray) do
result = result..'\r\n|-'
result = result..'\r\n|-'
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.customName..'"|'..Icons.Icon({tool.customName, type='upgrade', size='50', notext=true})
result = result..'||'..tool.name
result = result..'||'..tool.customName
local level = 1
local level = 1
if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil then
if tool.purchaseRequirements ~= nil and not Shared.tableIsEmpty(tool.purchaseRequirements) then
--Gonna be lazy and assume there's only the one skill level and it's the one we care about
--Gonna be lazy and assume there's only the one skill level and it's the one we care about
level = tool.unlockRequirements.skillLevel[1][2]
level = tool.unlockRequirements.skillLevel[1].level
end
end
result = result..'||style="text-align:right"|'..level
result = result..'||style="text-align:right"|'..level
result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)
result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)


local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
-- TODO TotH tools have other modifiers which should be included
local cutTime = 0
if tool.contains.modifiers.decreasedSkillIntervalPercent ~= nil then
cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1].value
end
total = total + cutTime
total = total + cutTime
result = result..'||style="text-align:right"|-'..cutTime..'%'
result = result..'||style="text-align:right"|-'..cutTime..'%'
Line 51: Line 54:


function p.getPickaxeTable(frame)
function p.getPickaxeTable(frame)
local toolArray = {}
local toolArray = Shop.getPurchases(
for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
function(purch)
if Shared.contains(upgrade.name, 'Pickaxe') then
return purch.category == 'melvorD:SkillUpgrades' and string.find(purch.id, '_Pickaxe$') ~= nil
table.insert(toolArray, upgrade)
end)
end
end


local result = '{| class="wikitable"'
local result = '{| class="wikitable"'
Line 67: Line 68:
local total2 = 0
local total2 = 0


for i, tool in Shared.skpairs(toolArray) do
for i, tool in ipairs(toolArray) do
result = result..'\r\n|-'
result = result..'\r\n|-'
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.customName..'"|'..Icons.Icon({tool.customName, type='upgrade', size='50', notext=true})
result = result..'||'..tool.name
result = result..'||'..tool.customName
local level = 1
local level = 1
if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil then
if tool.purchaseRequirements ~= nil and not Shared.tableIsEmpty(tool.purchaseRequirements) then
--Gonna be lazy and assume there's only the one skill level and it's the one we care about
--Gonna be lazy and assume there's only the one skill level and it's the one we care about
level = tool.unlockRequirements.skillLevel[1][2]
level = tool.unlockRequirements.skillLevel[1].level
end
end
result = result..'||style="text-align:right"|'..level
result = result..'||style="text-align:right"|'..level
result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)
result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)


local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
-- TODO TotH tools have other modifiers which should be included
local cutTime = 0
if tool.contains.modifiers.decreasedSkillIntervalPercent ~= nil then
cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1].value
end
total = total + cutTime
total = total + cutTime


Line 85: Line 90:
result = result..'||style="text-align:right"|-'..total..'%'
result = result..'||style="text-align:right"|-'..total..'%'


local OreDouble = tool.contains.modifiers.increasedChanceToDoubleOres
local OreDouble = (tool.contains.modifiers.increasedChanceToDoubleOres or 0)
total2 = total2 + OreDouble
total2 = total2 + OreDouble


Line 97: Line 102:


function p.getRodTable(frame)
function p.getRodTable(frame)
local toolArray = {}
local toolArray = Shop.getPurchases(
for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
function(purch)
if Shared.contains(upgrade.name, 'Fishing Rod') then
return purch.category == 'melvorD:SkillUpgrades' and string.find(purch.id, '_Fishing_Rod$') ~= nil
table.insert(toolArray, upgrade)
end)
end
end


local result = '{| class="wikitable"'
local result = '{| class="wikitable"'
Line 114: Line 117:
for i, tool in Shared.skpairs(toolArray) do
for i, tool in Shared.skpairs(toolArray) do
result = result..'\r\n|-'
result = result..'\r\n|-'
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.customName..'"|'..Icons.Icon({tool.customName, type='upgrade', size='50', notext=true})
result = result..'||'..tool.name
result = result..'||'..tool.customName
local level = 1
local level = 1
if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil then
if tool.purchaseRequirements ~= nil and not Shared.tableIsEmpty(tool.purchaseRequirements) then
--Gonna be lazy and assume there's only the one skill level and it's the one we care about
--Gonna be lazy and assume there's only the one skill level and it's the one we care about
level = tool.unlockRequirements.skillLevel[1][2]
level = tool.unlockRequirements.skillLevel[1].level
end
end
result = result..'||style="text-align:right"|'..level
result = result..'||style="text-align:right"|'..level
result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)
result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)


local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
-- TODO TotH tools have other modifiers which should be included
local cutTime = 0
if tool.contains.modifiers.decreasedSkillIntervalPercent ~= nil then
cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1].value
end
total = total + cutTime
total = total + cutTime
result = result..'||style="text-align:right"|-'..cutTime..'%'
result = result..'||style="text-align:right"|-'..cutTime..'%'
Line 140: Line 147:
result = result..'!!XP!!Cut Time!!XP/s!!GP/s'
result = result..'!!XP!!Cut Time!!XP/s!!GP/s'


for i, tree in Shared.skpairs(SkillData.Woodcutting.Trees) do
for i, tree in ipairs(SkillData.Woodcutting.trees) do
local log = Items.getItemByID(tree.productId)
result = result..'\r\n|-'
result = result..'\r\n|-'
local treeName = Shared.titleCase(tree.type..' tree')
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tree.name..'"|'..Icons.Icon({log.name, img=tree.name, type='tree', notext=true, size=50})
local logName = Shared.titleCase(tree.type..' logs')
result = result..'||'..tree.name..''
result = result..'\r\n|style="min-width:25px" data-sort-value="'..treeName..'"|'..Icons.Icon({logName, img=treeName, type='tree', notext=true, size=50})
result = result..'||style="min-width:25px" data-sort-value="'..log.name..'"|'..Icons.Icon({log.name, type='item', notext=true, size=50})
result = result..'||'..treeName..''
result = result..'||'..Icons.Icon({log.name, type='item', noicon=true})
result = result..'||style="min-width:25px" data-sort-value="'..logName..'"|'..Icons.Icon({logName, type='item', notext=true, size=50})
result = result..'||style="text-align:right"|'..tree.level
result = result..'||'..Icons.Icon({logName, type='item', noicon=true})
result = result..'||style="text-align:right"|'..tree.levelRequired
result = result..'||style="text-align:right"|'..tree.baseExperience
result = result..'||style="text-align:right"|'..tree.baseExperience
result = result..'||style="text-align:right" data-sort-value="'..tree.baseInterval..'"|'..Shared.timeString(tree.baseInterval/1000, true)
result = result..'||style="text-align:right" data-sort-value="'..tree.baseInterval..'"|'..Shared.timeString(tree.baseInterval/1000, true)
local XPs = tree.baseExperience / (tree.baseInterval / 1000)
local XPs = tree.baseExperience / (tree.baseInterval / 1000)
local Log = Items.getItemByID(tree.logID)
local GPs = log.sellsFor / (tree.baseInterval / 1000)
local GPs = Log.sellsFor / (tree.baseInterval / 1000)
result = result..'||style="text-align:right"|'..Shared.round(XPs, 2, 2)
result = result..'||style="text-align:right"|'..Shared.round(XPs, 2, 2)
result = result..'||style="text-align:right" data-sort-value="'..GPs..'"|'..Icons.GP(Shared.round(GPs, 2, 2))
result = result..'||style="text-align:right" data-sort-value="'..GPs..'"|'..Icons.GP(Shared.round(GPs, 2, 2))
Line 164: Line 169:
function p.getSpecialFishingTable(frame)
function p.getSpecialFishingTable(frame)
local totalWt, lootValue = 0, 0
local totalWt, lootValue = 0, 0
local itemArray = Shared.clone(SkillData.Fishing.SpecialItems)
local itemArray = GameData.getEntities(SkillData.Fishing.specialItems, function(item) return true end)
for i, itemDef in ipairs(itemArray) do
for i, itemDef in ipairs(itemArray) do
totalWt = totalWt + itemDef[2]
totalWt = totalWt + itemDef.weight
end
end
-- Sort the loot table by weight in descending order
-- Sort the loot table by weight in descending order
Line 175: Line 180:
table.insert(resultPart, '\r\n|- class="headerRow-0"\r\n!colspan="2"| Item\r\n!Value\r\n!colspan="2"|Chance')
table.insert(resultPart, '\r\n|- class="headerRow-0"\r\n!colspan="2"| Item\r\n!Value\r\n!colspan="2"|Chance')
for i, itemDef in ipairs(itemArray) do
for i, itemDef in ipairs(itemArray) do
local item = Items.getItemByID(itemDef[1])
local item = Items.getItemByID(itemDef.itemID)
if item ~= nil then
if item ~= nil then
local dropChance = itemDef[2] / totalWt * 100
local dropChance = itemDef.weight / totalWt * 100
-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
local fmt = (dropChance < 0.10 and '%.2g') or '%.2f'
local fmt = (dropChance < 0.10 and '%.2g') or '%.2f'
Line 183: Line 188:
table.insert(resultPart, '\r\n| ' .. Icons.Icon({item.name, type='item', noicon=true}))
table.insert(resultPart, '\r\n| ' .. Icons.Icon({item.name, type='item', noicon=true}))
table.insert(resultPart, '\r\n|data-sort-value="' .. item.sellsFor .. '"| ' .. Icons.GP(math.floor(item.sellsFor)))
table.insert(resultPart, '\r\n|data-sort-value="' .. item.sellsFor .. '"| ' .. Icons.GP(math.floor(item.sellsFor)))
table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. itemDef[2] .. '"| ' .. Shared.fraction(itemDef[2], totalWt))
table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. itemDef.weight .. '"| ' .. Shared.fraction(itemDef.weight, totalWt))
table.insert(resultPart, '\r\n|style="text-align:right"| ' .. string.format(fmt, dropChance) .. '%')
table.insert(resultPart, '\r\n|style="text-align:right"| ' .. string.format(fmt, dropChance) .. '%')
lootValue = lootValue + (dropChance / 100 * item.sellsFor)
lootValue = lootValue + (dropChance / 100 * item.sellsFor)
Line 199: Line 204:


local itemArray = {}
local itemArray = {}
for i, itemID in ipairs(SkillData.Fishing.JunkItems) do
for i, itemID in ipairs(SkillData.Fishing.junkItemIDs) do
local item = Items.getItemByID(itemID)
local item = Items.getItemByID(itemID)
if item ~= nil then
if item ~= nil then
Line 223: Line 228:
result = result..'!!XP!!Respawn Time!!Ore Value'
result = result..'!!XP!!Respawn Time!!Ore Value'


local mineData = Shared.clone(SkillData.Mining.Rocks)
local mineData = GameData.getEntities(SkillData.Mining.rockData, function(rock) return true end)
 
table.sort(mineData, function(a, b) return a.level < b.level end)
table.sort(mineData, function(a, b) return a.levelRequired < b.levelRequired end)


for i, oreData in Shared.skpairs(mineData) do
for i, oreData in Shared.skpairs(mineData) do
local ore = Items.getItemByID(oreData.oreID)
local ore = Items.getItemByID(oreData.productID)
result = result..'\r\n|-\r\n|style="min-width:25px"|'..Icons.Icon({ore.name, type='item', size='50', notext=true})
result = result..'\r\n|-\r\n|style="min-width:25px"|'..Icons.Icon({ore.name, type='item', size='50', notext=true})
result = result..'||'..Icons.Icon({ore.name, type='item', noicon=true})
result = result..'||'..Icons.Icon({ore.name, type='item', noicon=true})
result = result..'||style="text-align:right"|'..oreData.levelRequired..'||style="text-align:right"|'..oreData.baseExperience
result = result..'||style="text-align:right"|'..oreData.level..'||style="text-align:right"|'..oreData.baseExperience
result = result..'||style="text-align:right" data-sort-value="'..oreData.baseRespawnInterval..'"|'
result = result..'||style="text-align:right" data-sort-value="'..oreData.baseRespawnInterval..'"|'
result = result..Shared.timeString(oreData.baseRespawnInterval / 1000, true)
result = result..Shared.timeString(oreData.baseRespawnInterval / 1000, true)
Line 241: Line 245:
end
end


-- TODO TotH has random superior gems also
function p.getMiningGemsTable(frame)
function p.getMiningGemsTable(frame)
local result = '{|class="wikitable sortable stickyHeader"'
local result = '{|class="wikitable sortable stickyHeader"'
Line 246: Line 251:
result = result..'\r\n!colspan=2|Gem!!Gem Chance!!Gem Price'
result = result..'\r\n!colspan=2|Gem!!Gem Chance!!Gem Price'


-- Sort gems by ID order
local totalWeight = 0
for i, gemData in Shared.spairs(Items.GemTable, function(t,a,b) return t[a].id < t[b].id end) do
for i, gemData in ipairs(GameData.rawData.randomGems) do
local gem = Items.getItemByID(gemData.id)
totalWeight = totalWeight + gemData.weight
end
 
for i, gemData in ipairs(GameData.rawData.randomGems) do
local gem = Items.getItemByID(gemData.itemID)
result = result..'\r\n|-\r\n|style="min-width:25px"|'
result = result..'\r\n|-\r\n|style="min-width:25px"|'
result = result..Icons.Icon({gem.name, type='item', size='50', notext=true})
result = result..Icons.Icon({gem.name, type='item', size='50', notext=true})
result = result..'||'..Icons.Icon({gem.name, type='item', noicon=true})
result = result..'||'..Icons.Icon({gem.name, type='item', noicon=true})
result = result..'||style="text-align:right"|'..string.format("%.1f%%", gemData.chance)
result = result..'||style="text-align:right"|'..string.format("%.1f%%", gemData.weight / totalWeight * 100)
result = result..'||data-sort-value="'..gem.sellsFor..'"|'..Icons.GP(gem.sellsFor)
result = result..'||data-sort-value="'..gem.sellsFor..'"|'..Icons.GP(gem.sellsFor)
end
end
Line 261: Line 270:


function p.getFishTable(frame)
function p.getFishTable(frame)
local recipeList = {}
local recipeList = GameData.getEntities(SkillData.Fishing.fish, function(fish) return true end)
for i, recipe in ipairs(SkillData.Fishing.Fish) do
table.sort(recipeList, function(a, b) return a.level < b.level end)
table.insert(recipeList, recipe)
end
table.sort(recipeList, function(a, b) return (a.level == b.level and a.masteryID < b.masteryID) or a.level < b.level end)


-- Determine cooking levels for all fish
-- Determine cooking levels for all fish
local cookReq = {}
local cookReq = {}
for i, recipe in ipairs(SkillData.Cooking.Recipes) do
for i, recipe in ipairs(SkillData.Cooking.recipes) do
-- This assumes that each raw fish only appears in a single recipe, which is a bit rubbish
-- This assumes that each raw fish only appears in a single recipe, which is a bit rubbish
-- but currently holds
-- but currently holds
Line 286: Line 292:
table.insert(resultPart, '\r\n!' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' Level')
table.insert(resultPart, '\r\n!' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' Level')
for i, recipe in ipairs(recipeList) do
for i, recipe in ipairs(recipeList) do
local fish = Items.getItemByID(recipe.itemID)
local fish = Items.getItemByID(recipe.productID)
if fish ~= nil then
if fish ~= nil then
local timeSortVal = (recipe.baseMinInterval + recipe.baseMaxInterval) / 2000
local timeSortVal = (recipe.baseMinInterval + recipe.baseMaxInterval) / 2000
local timeStr = string.format("%.1fs - %.1fs", recipe.baseMinInterval / 1000, recipe.baseMaxInterval / 1000)
local timeStr = string.format("%.1fs - %.1fs", recipe.baseMinInterval / 1000, recipe.baseMaxInterval / 1000)
local XPs = recipe.baseXP / timeSortVal
local XPs = recipe.baseExperience / timeSortVal
local GPs = fish.sellsFor / timeSortVal
local GPs = fish.sellsFor / timeSortVal
local cookSortVal = cookReq[recipe.itemID] or 0
local cookSortVal = cookReq[recipe.itemID] or 0
Line 316: Line 322:
result = result..'\r\n!Junk Chance\r\n!Special Chance'
result = result..'\r\n!Junk Chance\r\n!Special Chance'


for i, area in ipairs(SkillData.Fishing.Areas) do
for i, area in ipairs(SkillData.Fishing.areas) do
result = result..'\r\n|-'
result = result..'\r\n|-'
result = result..'\r\n| style ="text-align: left;" |'..area.name
result = result..'\r\n| style ="text-align: left;" |'..area.name


local fishArray = {}
local fishArray = {}
for j, fish in ipairs(area.fish) do
for j, fishID in ipairs(area.fishIDs) do
local fishItem = Items.getItemByID(fish.itemID)
local fishItem = Items.getItemByID(fishID)
if fishItem ~= nil then
if fishItem ~= nil then
table.insert(fishArray, Icons.Icon({fishItem.name, type='item'}))
table.insert(fishArray, Icons.Icon({fishItem.name, type='item'}))
Line 338: Line 344:
end
end


-- TODO Expose list of NPCs somewhere when applicable (e.g. for Thieving Shorts)
function p.getThievingGeneralRareTable(frame)
function p.getThievingGeneralRareTable(frame)
local rareTxt = '{|class="wikitable sortable"'
local rareTxt = '{|class="wikitable sortable"'
rareTxt = rareTxt..'\r\n!Item!!Qty'
rareTxt = rareTxt..'\r\n!Item!!Qty'
rareTxt = rareTxt..'!!Price!!colspan="2"|Chance'
rareTxt = rareTxt..'!!Price!!colspan="2"|Chance'
for i, drop in pairs(SkillData.Thieving.RareItems) do
for i, drop in pairs(SkillData.Thieving.generalRareItems) do
local thisItem = Items.getItemByID(drop.itemID)
local thisItem = Items.getItemByID(drop.itemID)
local odds = drop.chance
local odds = drop.chance
Line 370: Line 377:
table.insert(normalTxt, '===Possible Common Drops:===\r\nUp to one of these will be received on a successful pickpocket:')
table.insert(normalTxt, '===Possible Common Drops:===\r\nUp to one of these will be received on a successful pickpocket:')
local totalWt = 0
local totalWt = 0
local lootChance = SkillData.Thieving.ItemChance
local lootChance = SkillData.Thieving.itemChance
local lootValue = 0
local lootValue = 0


--First loop through to get the total weight so we have it for later
--First loop through to get the total weight so we have it for later
for i, loot in pairs(npc.lootTable) do
for i, loot in pairs(npc.lootTable) do
totalWt = totalWt + loot[2]
totalWt = totalWt + loot.weight
end
end


Line 382: Line 389:
table.insert(normalTxt, '!!Price!!colspan="2"|Chance')
table.insert(normalTxt, '!!Price!!colspan="2"|Chance')


local lootTable = GameData.getEntities(npc.lootTable, function(loot) return true end)
--Then sort the loot table by weight
--Then sort the loot table by weight
table.sort(npc.lootTable, function(a, b) return a[2] > b[2] end)
table.sort(lootTable, function(a, b) return a.weight > b.weight end)
for i, row in Shared.skpairs(npc.lootTable) do
for i, loot in ipairs(lootTable) do
local thisItem = Items.getItemByID(row[1])
local thisItem = Items.getItemByID(loot.id)
local maxQty = row[3]
if thisItem ~= nil then
if thisItem ~= nil then
table.insert(normalTxt, '\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'}))
table.insert(normalTxt, '\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'}))
Line 392: Line 399:
table.insert(normalTxt, '\r\n|-\r\n|Unknown Item[[Category:Pages with script errors]]')
table.insert(normalTxt, '\r\n|-\r\n|Unknown Item[[Category:Pages with script errors]]')
end
end
table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..maxQty..'"|')
table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..(loot.minQuantity + loot.maxQuantity)..'"|')


if maxQty > 1 then
if loot.minQuantitiy ~= loot.maxQuantitiy then
table.insert(normalTxt, '1 - ')
table.insert(normalTxt, Shared.formatnum(loot.minQuantitiy) .. ' - ' .. Shared.formatnum(loot.maxQuantity))
else
table.insert(normalTxt, Shared.formatnum(loot.maxQuantity))
end
end
table.insert(normalTxt, Shared.formatnum(row[3]))


--Adding price columns
--Adding price columns
Line 405: Line 413:
else
else
itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
if itemPrice == 0 or maxQty == 1 then
if itemPrice == 0 or loot.minQuantity == loot.maxQuantitiy then
table.insert(normalTxt, '||'..Icons.GP(itemPrice))
table.insert(normalTxt, '||'..Icons.GP(itemPrice))
else
else
table.insert(normalTxt, '||'..Icons.GP(itemPrice, itemPrice * maxQty))
table.insert(normalTxt, '||'..Icons.GP(itemPrice * loot.minQuantitiy, itemPrice * loot.maxQuantitiy))
end
end
end
end


--Getting the drop chance
--Getting the drop chance
local dropChance = (row[2] / totalWt * lootChance)
local dropChance = (loot.weight / totalWt * lootChance)
if dropChance ~= 100 then
if dropChance < 100 then
--Show fraction as long as it isn't going to be 1/1
--Show fraction as long as it isn't going to be 1/1
table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..row[2]..'"')
table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..loot.weight..'"')
table.insert(normalTxt, '|'..Shared.fraction(row[2] * lootChance, totalWt * 100))
table.insert(normalTxt, '|'..Shared.fraction(loot.weight * lootChance, totalWt * 100))
table.insert(normalTxt, '||')
table.insert(normalTxt, '||')
else
else
table.insert(normalTxt, '||colspan="2" data-sort-value="'..row[2]..'"')
table.insert(normalTxt, '||colspan="2" data-sort-value="'..loot.weight..'"')
end
end
table.insert(normalTxt, 'style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%')
table.insert(normalTxt, 'style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%')


--Adding to the average loot value based on price & dropchance
--Adding to the average loot value based on price & dropchance
lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((1 + maxQty) / 2))
lootValue = lootValue + (dropChance * 0.01 * itemPrice * (loot.minQuantitiy + loot.maxQuantitiy) / 2)
end
end
if Shared.tableCount(npc.lootTable) > 1 then
if Shared.tableCount(npc.lootTable) > 1 then
Line 456: Line 464:
areaTxt = areaTxt..'\r\n!Item!!Qty'
areaTxt = areaTxt..'\r\n!Item!!Qty'
areaTxt = areaTxt..'!!Price!!colspan="2"|Chance'
areaTxt = areaTxt..'!!Price!!colspan="2"|Chance'
local dropCount = Shared.tableCount(area.uniqueDrops)
local dropLines = {}
local dropLines = {}
for i, drop in pairs(area.uniqueDrops) do
for i, drop in pairs(area.uniqueDrops) do
local thisItem = Items.getItemByID(drop.itemID)
local thisItem = Items.getItemByID(drop.id)
local lineTxt = ''
local lineTxt = ''
lineTxt = lineTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
lineTxt = lineTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
lineTxt = lineTxt..'||data-sort-value="'..drop.qty..'"| '..Shared.formatnum(drop.qty)..'||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
lineTxt = lineTxt..'||data-sort-value="'..drop.quantity..'"| '..Shared.formatnum(drop.quantity)..'||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
lineTxt = lineTxt..'||style="text-align:right"|'..Shared.fraction(1, 1/(SkillData.Thieving.AreaUniqueChance/100))
lineTxt = lineTxt..'||style="text-align:right"|'..Shared.fraction(1, 1/(SkillData.Thieving.baseAreaUniqueChance/100))
lineTxt = lineTxt..'||'..Shared.round(SkillData.Thieving.AreaUniqueChance, 2, 2)..'%'
lineTxt = lineTxt..'||'..Shared.round(SkillData.Thieving.AreaUniqueChance, 2, 2)..'%'
dropLines[thisItem.name] = lineTxt
dropLines[thisItem.name] = lineTxt
Line 471: Line 478:
end
end
areaTxt = areaTxt..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
areaTxt = areaTxt..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
areaTxt = areaTxt..'\r\n|style="text-align:right"|'..Shared.fraction(1, 1/(SkillData.Thieving.AreaUniqueChance/100))..'||'
areaTxt = areaTxt..'\r\n|style="text-align:right"|'..Shared.fraction(1, 1/(SkillData.Thieving.baseAreaUniqueChance/100))..'||'
areaTxt = areaTxt..'style="text-align:right"|'..Shared.round(SkillData.Thieving.AreaUniqueChance, 2, 2)..'%'
areaTxt = areaTxt..'style="text-align:right"|'..Shared.round(SkillData.Thieving.baseAreaUniqueChance, 2, 2)..'%'
areaTxt = areaTxt..'\r\n|}'
areaTxt = areaTxt..'\r\n|}'
table.insert(sectionTxt, areaTxt)
table.insert(sectionTxt, areaTxt)


if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID > -1 then
if npc.uniqueDrop ~= nil and npc.uniqueDrop.id ~= nil then
local uniqueTxt = '===Possible NPC Unique Drop==='
local thisItem = Items.getItemByID(npc.uniqueDrop.id)
uniqueTxt = uniqueTxt..'\r\nThe chance of receiving the unique drop for an NPC is based on a combination of several factors.'
if thisItem ~= nil then
uniqueTxt = uniqueTxt..' The unique drop chance for an NPC is included in the tooltip for your Stealth against that NPC.'
local uniqueTxt = '===Possible NPC Unique Drop==='
local thisItem = Items.getItemByID(npc.uniqueDrop.itemID)
uniqueTxt = uniqueTxt..'\r\nThe chance of receiving the unique drop for an NPC is based on a combination of several factors.'
uniqueTxt = uniqueTxt..'\r\nThe unique drop for the '..npc.name..' is '
uniqueTxt = uniqueTxt..' The unique drop chance for an NPC is included in the tooltip for your Stealth against that NPC.'
if npc.uniqueDrop.qty > 1 then
uniqueTxt = uniqueTxt..'\r\nThe unique drop for the '..npc.name..' is '
uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item', qty=npc.uniqueDrop.qty})
if npc.uniqueDrop.quantity > 1 then
else
uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item', qty=npc.uniqueDrop.quantity})
uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item'})
else
uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item'})
end
table.insert(sectionTxt, uniqueTxt)
end
end
table.insert(sectionTxt, uniqueTxt)
end
end


Line 508: Line 516:
result = result..'\r\n|- class="headerRow-0"'
result = result..'\r\n|- class="headerRow-0"'
result = result..'\r\n!colspan="2"|Name!!Area!!'..Icons.Icon({'Thieving', type='skill', notext=true})..' Level!!Experience!!Max Hit!!Perception!!GP!!Unique Drop'
result = result..'\r\n!colspan="2"|Name!!Area!!'..Icons.Icon({'Thieving', type='skill', notext=true})..' Level!!Experience!!Max Hit!!Perception!!GP!!Unique Drop'
local npcArray = Shared.clone(SkillData.Thieving.NPCs)
local npcArray = GameData.getEntities(SkillData.Thieving.npcs, function(npc) return true end)
table.sort(npcArray, function(a, b) return a.level < b.level end)
table.sort(npcArray, function(a, b) return a.level < b.level end)
for i, npc in Shared.skpairs(npcArray) do
for i, npc in ipairs(npcArray) do
result = result..'\r\n|-'
result = result..'\r\n|-'
result = result..'\r\n|'..Icons.Icon({npc.name, type='thieving', size='50', notext=true})
result = result..'\r\n|'..Icons.Icon({npc.name, type='thieving', size='50', notext=true})
Line 518: Line 526:
result = result..'||'..area.name
result = result..'||'..area.name
result = result..'||data-sort-value="' .. npc.level .. '"| '..Icons._SkillReq('Thieving', npc.level)
result = result..'||data-sort-value="' .. npc.level .. '"| '..Icons._SkillReq('Thieving', npc.level)
result = result..'||style="text-align:right"|'..npc.xp
result = result..'||style="text-align:right"|'..npc.baseExperience
result = result..'||style="text-align:right"|'..(npc.maxHit * 10)
result = result..'||style="text-align:right"|'..(npc.maxHit * 10)
result = result..'||style="text-align:right"|'..npc.perception
result = result..'||style="text-align:right"|'..npc.perception
result = result..'||data-sort-value="' .. npc.maxGP .. '"|'..Icons.GP(1, npc.maxGP)
result = result..'||data-sort-value="' .. npc.maxGP .. '"|'..Icons.GP(1, npc.maxGP)
if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID > -1 then
if npc.uniqueDrop ~= nil then
local uniqueDrop = Items.getItemByID(npc.uniqueDrop.itemID)
local uniqueDrop = Items.getItemByID(npc.uniqueDrop.id)
if npc.uniqueDrop.qty > 1 then
if npc.uniqueDrop.quantity > 1 then
result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item', qty = npc.uniqueDrop.qty})
result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item', qty = npc.uniqueDrop.quantity})
else
else
result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item'})
result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item'})
Line 544: Line 552:
table.insert(resultPart, '\r\n!Area!!'..Icons.Icon({'Thieving', type='skill', notext=true})..' Level!!NPCs!!Unique Drops')
table.insert(resultPart, '\r\n!Area!!'..Icons.Icon({'Thieving', type='skill', notext=true})..' Level!!NPCs!!Unique Drops')


local areaArray = Shared.clone(SkillData.Thieving.Areas)
for i, area in ipairs(SkillData.Thieving.areas) do
table.sort(areaArray, function(a, b) return a.id < b.id end)
for i, area in ipairs(areaArray) do
local minLevel, npcList, areaItemList = nil, {}, {}
local minLevel, npcList, areaItemList = nil, {}, {}
-- Build NPC list & determine level for area, this is the minimum
-- Build NPC list & determine level for area, this is the minimum
-- Thieving level required for all NPCs within that area
-- Thieving level required for all NPCs within that area
if area.npcs ~= nil and Shared.tableCount(area.npcs) > 0 then
if area.npcs ~= nil and not Shared.tableIsEmpty(area.npcs) then
for j, npcID in ipairs(area.npcs) do
for j, npcID in ipairs(area.npcs) do
-- Don't bother cloning the NPC below since we aren't modifying any part of it
local npc = Skills.getThievingNPCByID(npcID)
local npc = SkillData.Thieving.NPCs[npcID + 1]
if minLevel == nil or npc.level < minLevel then
if minLevel == nil or npc.level < minLevel then
minLevel = npc.level
minLevel = npc.level
Line 566: Line 571:
if area.uniqueDrops ~= nil and Shared.tableCount(area.uniqueDrops) > 0 then
if area.uniqueDrops ~= nil and Shared.tableCount(area.uniqueDrops) > 0 then
for k, drop in ipairs(area.uniqueDrops) do
for k, drop in ipairs(area.uniqueDrops) do
local areaItem = Items.getItemByID(drop.itemID)
local areaItem = Items.getItemByID(drop.id)
if areaItem == nil then
if areaItem == nil then
table.insert(areaItemList, 'Unknown[[Category:Pages with script errors]]')
table.insert(areaItemList, 'Unknown[[Category:Pages with script errors]]')
else
else
local iconDef = {areaItem.name, type='item'}
local iconDef = {areaItem.name, type='item'}
if drop.qty > 1 then
if drop.quantity > 1 then
iconDef.qty = drop.qty
iconDef.qty = drop.quantity
end
end
table.insert(areaItemList, Icons.Icon(iconDef))
table.insert(areaItemList, Icons.Icon(iconDef))
Line 593: Line 598:
end
end


function p._getFarmingTable(category)
function p._getFarmingTable(categoryName)
local seedList = {}
local category = GameData.getEntityByName(SkillData.Farming.categories, categoryName)
if category == 'Allotment' or category == 'Herb' or category == 'Tree' then
if category == nil then
seedList = Items.getItems(function(item) return item.tier == category end)
return 'ERROR: Invalid farming category. Please choose Allotments, Herbs, or Trees'
else
end
return 'ERROR: Invalid farming category. Please choose Allotment, Herb, or Tree'
local seedList = GameData.getEntities(SkillData.Farming.recipes,
function(recipe)
return recipe.categoryID == category.id
end)
if Shared.tableIsEmpty(seedList) then
return ''
end
end


Line 605: Line 615:
result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
result = result..'!!XP!!Growth Time!!Seed Value'
result = result..'!!XP!!Growth Time!!Seed Value'
if category == 'Allotment' then
if category.id == 'melvorD:Allotment' then
result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
elseif category == 'Herb' then
elseif category.id == 'melvorD:Herb' then
result = result..'!!colspan="2"|Herb!!Herb Value'
result = result..'!!colspan="2"|Herb!!Herb Value'
elseif category == 'Tree' then
elseif category.id == 'melvorD:Tree' then
result = result..'!!colspan="2"|Logs!!Log Value'
result = result..'!!colspan="2"|Logs!!Log Value'
end
end
Line 616: Line 626:
table.sort(seedList, function(a, b) return a.farmingLevel < b.farmingLevel end)
table.sort(seedList, function(a, b) return a.farmingLevel < b.farmingLevel end)


for i, seed in pairs(seedList) do
for i, seed in ipairs(seedList) do
result = result..'\r\n|-'
local seedItem = Items.getItemByID(seed.seedCost.id)
result = result..'\r\n|'..Icons.Icon({seed.name, type='item', size='50', notext=true})..'||[['..seed.name..']]'
local productItem = Items.getItemByID(seed.productId)
result = result..'||'..seed.farmingLevel..'||'..Shared.formatnum(seed.farmingXP)
if seedItem ~= nil and productItem ~= nil then
result = result..'||data-sort-value="'..seed.timeToGrow..'"|'..Shared.timeString(seed.timeToGrow, true)
result = result..'\r\n|-'
result = result..'||data-sort-value="'..seed.sellsFor..'"|'..Icons.GP(seed.sellsFor)
result = result..'\r\n|'..Icons.Icon({seedItem.name, type='item', size='50', notext=true})..'||[['..seedItem.name..']]'
 
result = result..'||'..seed.level..'||'..Shared.formatnum(seed.baseExperience)
local crop = Items.getItemByID(seed.grownItemID)
result = result..'||data-sort-value="'..(seed.baseInterval / 1000)..'"|'..Shared.timeString(seed.baseInterval / 1000, true)
result = result..'||'..Icons.Icon({crop.name, type='item', size='50', notext=true})..'||[['..crop.name..']]'
result = result..'||data-sort-value="'..seedItem.sellsFor..'"|'..Icons.GP(seedItem.sellsFor)
if category == 'Allotment' then
result = result..'||'..Icons.Icon({productItem.name, type='item', size='50', notext=true})..'||[['..productItem.name..']]'
result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..(crop.healsFor * 10)
if category.id == 'melvorD:Allotment' then
result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..((productItem.healsFor or 0) * 10)
end
result = result..'||data-sort-value="'..productItem.sellsFor..'"|'..Icons.GP(productItem.sellsFor)
result = result..'||'..ItemSourceTables._getItemSources(seedItem.id)
end
end
result = result..'||data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
result = result..'||'..ItemSourceTables._getItemSources(seed)
end
end


Line 648: Line 660:
result = result..'!!Healing!!Value'
result = result..'!!Healing!!Value'


local itemArray = Items.getItems(function(item) return item.grownItemID ~= nil end)
local recipes = GameData.getEntities(SkillData.Farming.recipes,
 
function(recipe)
table.sort(itemArray, function(a, b) return a.farmingLevel < b.farmingLevel end)
local product = Items.getItemByID(recipe.productID)
return product ~= nil and product.healsFor ~= nil and product.healsFor > 0
end)
table.sort(recipes, function(a, b) return a.level < b.level end)


for i, item in Shared.skpairs(itemArray) do
for i, recipe in ipairs(recipes) do
local crop = Items.getItemByID(item.grownItemID)
local product = Items.getItemByID(recipe.productID)
if crop.healsFor ~= nil and crop.healsFor > 0 then
if product ~= nil and product.healsFor ~= nil and product.healsFor > 0 then
result = result..'\r\n|-'
result = result..'\r\n|-'
result = result..'\r\n|'..Icons.Icon({crop.name, type='item', notext='true', size='50'})..'||[['..crop.name..']]'
result = result..'\r\n|'..Icons.Icon({product.name, type='item', notext='true', size='50'})..'||[['..product.name..']]'
result = result..'||style="text-align:right;"|'..item.farmingLevel
result = result..'||style="text-align:right;"|'..recipe.level
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="'..product.healsFor..'"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..(product.healsFor * 10)
result = result..'||style="text-align:right" data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
result = result..'||style="text-align:right" data-sort-value="'..product.sellsFor..'"|'..Icons.GP(product.sellsFor)
end
end
end
end

Revision as of 15:52, 22 October 2022

Documentation for this module may be created at Module:Skills/Gathering/doc

--Splitting some functions into here to avoid bloating a single file
local p = {}

local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local Shop = require('Module:Shop')
local Skills = require('Module:Skills')
local ItemSourceTables = require('Module:Items/SourceTables')

-- TODO Move tool tables to Module:Shop
function p.getAxeTable(frame)
	local toolArray = Shop.getPurchases(
		function(purch)
			return purch.category == 'melvorD:SkillUpgrades' and string.find(purch.id, '_Axe$') ~= nil
		end)

	local result = '{| class="wikitable"'
	result = result..'\r\n!colspan="4"| !!colspan="2"|Cut Time Decrease'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Woodcutting', type='skill', notext=true})..' Level'
	result = result..'!!Cost!!This Axe!!Total'

	local total = 0

	for i, tool in ipairs(toolArray) do
		result = result..'\r\n|-'
		result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.customName..'"|'..Icons.Icon({tool.customName, type='upgrade', size='50', notext=true})
		result = result..'||'..tool.customName
		local level = 1
		if tool.purchaseRequirements ~= nil and not Shared.tableIsEmpty(tool.purchaseRequirements) then
			--Gonna be lazy and assume there's only the one skill level and it's the one we care about
			level = tool.unlockRequirements.skillLevel[1].level
		end
		result = result..'||style="text-align:right"|'..level
		result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)

		-- TODO TotH tools have other modifiers which should be included
		local cutTime = 0
		if tool.contains.modifiers.decreasedSkillIntervalPercent ~= nil then
			cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1].value
		end
		total = total + cutTime
		result = result..'||style="text-align:right"|-'..cutTime..'%'
		result = result..'||style="text-align:right"|-'..total..'%'
	end

	result = result..'\r\n|}'
	return result
end

function p.getPickaxeTable(frame)
	local toolArray = Shop.getPurchases(
		function(purch)
			return purch.category == 'melvorD:SkillUpgrades' and string.find(purch.id, '_Pickaxe$') ~= nil
		end)

	local result = '{| class="wikitable"'
	result = result..'\r\n!colspan="4"| !!colspan="2"|Mine Time Decrease!!colspan="2"|2x Ore Chance'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Mining', type='skill', notext=true})..' Level'
	result = result..'!!Cost!!This Pick!!Total!!This Pick!!Total'

	local total = 0
	local total2 = 0

	for i, tool in ipairs(toolArray) do
		result = result..'\r\n|-'
		result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.customName..'"|'..Icons.Icon({tool.customName, type='upgrade', size='50', notext=true})
		result = result..'||'..tool.customName
		local level = 1
		if tool.purchaseRequirements ~= nil and not Shared.tableIsEmpty(tool.purchaseRequirements) then
			--Gonna be lazy and assume there's only the one skill level and it's the one we care about
			level = tool.unlockRequirements.skillLevel[1].level
		end
		result = result..'||style="text-align:right"|'..level
		result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)

		-- TODO TotH tools have other modifiers which should be included
		local cutTime = 0
		if tool.contains.modifiers.decreasedSkillIntervalPercent ~= nil then
			cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1].value
		end
		total = total + cutTime

		result = result..'||style="text-align:right"|-'..cutTime..'%'
		result = result..'||style="text-align:right"|-'..total..'%'

		local OreDouble = (tool.contains.modifiers.increasedChanceToDoubleOres or 0)
		total2 = total2 + OreDouble

		result = result..'||style="text-align:right"|+'..OreDouble..'%'
		result = result..'||style="text-align:right"|+'..total2..'%'
	end

	result = result..'\r\n|}'
	return result
end

function p.getRodTable(frame)
	local toolArray = Shop.getPurchases(
		function(purch)
			return purch.category == 'melvorD:SkillUpgrades' and string.find(purch.id, '_Fishing_Rod$') ~= nil
		end)

	local result = '{| class="wikitable"'
	result = result..'\r\n!colspan="4"| !!colspan="2"|Catch Time Decrease'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Fishing', type='skill', notext=true})..' Level'
	result = result..'!!Cost!!This Rod!!Total'

	local total = 0

	for i, tool in Shared.skpairs(toolArray) do
		result = result..'\r\n|-'
		result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.customName..'"|'..Icons.Icon({tool.customName, type='upgrade', size='50', notext=true})
		result = result..'||'..tool.customName
		local level = 1
		if tool.purchaseRequirements ~= nil and not Shared.tableIsEmpty(tool.purchaseRequirements) then
			--Gonna be lazy and assume there's only the one skill level and it's the one we care about
			level = tool.unlockRequirements.skillLevel[1].level
		end
		result = result..'||style="text-align:right"|'..level
		result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)

		-- TODO TotH tools have other modifiers which should be included
		local cutTime = 0
		if tool.contains.modifiers.decreasedSkillIntervalPercent ~= nil then
			cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1].value
		end
		total = total + cutTime
		result = result..'||style="text-align:right"|-'..cutTime..'%'
		result = result..'||style="text-align:right"|-'..total..'%'
	end

	result = result..'\r\n|}'
	return result
end

function p.getTreesTable(frame)
	local result = '{| class="wikitable sortable"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Tree!!colspan="2"|Logs!!'..Icons.Icon({'Woodcutting', type='skill', notext=true})..' Level'
	result = result..'!!XP!!Cut Time!!XP/s!!GP/s'

	for i, tree in ipairs(SkillData.Woodcutting.trees) do
		local log = Items.getItemByID(tree.productId)
		result = result..'\r\n|-'
		result = result..'\r\n|style="min-width:25px" data-sort-value="'..tree.name..'"|'..Icons.Icon({log.name, img=tree.name, type='tree', notext=true, size=50})
		result = result..'||'..tree.name..''
		result = result..'||style="min-width:25px" data-sort-value="'..log.name..'"|'..Icons.Icon({log.name, type='item', notext=true, size=50})
		result = result..'||'..Icons.Icon({log.name, type='item', noicon=true})
		result = result..'||style="text-align:right"|'..tree.level
		result = result..'||style="text-align:right"|'..tree.baseExperience
		result = result..'||style="text-align:right" data-sort-value="'..tree.baseInterval..'"|'..Shared.timeString(tree.baseInterval/1000, true)
		local XPs = tree.baseExperience / (tree.baseInterval / 1000)
		local GPs = log.sellsFor / (tree.baseInterval / 1000)
		result = result..'||style="text-align:right"|'..Shared.round(XPs, 2, 2)
		result = result..'||style="text-align:right" data-sort-value="'..GPs..'"|'..Icons.GP(Shared.round(GPs, 2, 2))
	end

	result = result..'\r\n|}'
	return result
end

function p.getSpecialFishingTable(frame)
	local totalWt, lootValue = 0, 0
	local itemArray = GameData.getEntities(SkillData.Fishing.specialItems, function(item) return true end)
	for i, itemDef in ipairs(itemArray) do
		totalWt = totalWt + itemDef.weight
	end
	-- Sort the loot table by weight in descending order
	table.sort(itemArray, function(a, b) return (a[2] == b[2] and a[1] < b[1]) or a[2] > b[2] end)

	local resultPart = {}
	table.insert(resultPart, '\r\n{|class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"\r\n!colspan="2"| Item\r\n!Value\r\n!colspan="2"|Chance')
	for i, itemDef in ipairs(itemArray) do
		local item = Items.getItemByID(itemDef.itemID)
		if item ~= nil then
			local dropChance = itemDef.weight / totalWt * 100
			-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
			local fmt = (dropChance < 0.10 and '%.2g') or '%.2f'
			table.insert(resultPart, '\r\n|-\r\n|style="text-align:center"| ' .. Icons.Icon({item.name, type='item', notext=true}))
			table.insert(resultPart, '\r\n| ' .. Icons.Icon({item.name, type='item', noicon=true}))
			table.insert(resultPart, '\r\n|data-sort-value="' .. item.sellsFor .. '"| ' .. Icons.GP(math.floor(item.sellsFor)))
			table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. itemDef.weight .. '"| ' .. Shared.fraction(itemDef.weight, totalWt))
			table.insert(resultPart, '\r\n|style="text-align:right"| ' .. string.format(fmt, dropChance) .. '%')
			lootValue = lootValue + (dropChance / 100 * item.sellsFor)
		end
	end
	table.insert(resultPart, '\r\n|}\r\nThe average value of a roll on the special fishing loot table is ' .. Icons.GP(Shared.round(lootValue, 2, 0)))
	return table.concat(resultPart)
end

function p.getFishingJunkTable(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"|Item!!Value')

	local itemArray = {}
	for i, itemID in ipairs(SkillData.Fishing.junkItemIDs) do
		local item = Items.getItemByID(itemID)
		if item ~= nil then
			table.insert(itemArray, item)
		end
	end
	table.sort(itemArray, function(a, b) return a.name < b.name end)

	for i, item in ipairs(itemArray) do
		table.insert(resultPart, '\r\n|-')
		table.insert(resultPart, '\r\n|style="min-width:25px"| ' .. Icons.Icon({item.name, type='item', notext=true, size=50}))
		table.insert(resultPart, '\r\n| ' .. Icons.Icon({item.name, type='item', noicon=true}))
		table.insert(resultPart, '\r\n|data-sort-value="' .. item.sellsFor .. '"| ' .. Icons.GP(math.floor(item.sellsFor)))
	end
	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

function p.getMiningOresTable(frame)
	local result = '{|class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan=2|Ore!!'..Icons.Icon({'Mining', type='skill', notext=true})..' Level'
	result = result..'!!XP!!Respawn Time!!Ore Value'

	local mineData = GameData.getEntities(SkillData.Mining.rockData, function(rock) return true end)
	table.sort(mineData, function(a, b) return a.level < b.level end)

	for i, oreData in Shared.skpairs(mineData) do
		local ore = Items.getItemByID(oreData.productID)
		result = result..'\r\n|-\r\n|style="min-width:25px"|'..Icons.Icon({ore.name, type='item', size='50', notext=true})
		result = result..'||'..Icons.Icon({ore.name, type='item', noicon=true})
		result = result..'||style="text-align:right"|'..oreData.level..'||style="text-align:right"|'..oreData.baseExperience
		result = result..'||style="text-align:right" data-sort-value="'..oreData.baseRespawnInterval..'"|'
		result = result..Shared.timeString(oreData.baseRespawnInterval / 1000, true)
		result = result..'||data-sort-value="'..ore.sellsFor..'"|'..Icons.GP(ore.sellsFor)
	end

	result = result..'\r\n|}'
	return result
end

-- TODO TotH has random superior gems also
function p.getMiningGemsTable(frame)
	local result = '{|class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan=2|Gem!!Gem Chance!!Gem Price'

	local totalWeight = 0
	for i, gemData in ipairs(GameData.rawData.randomGems) do
		totalWeight = totalWeight + gemData.weight
	end

	for i, gemData in ipairs(GameData.rawData.randomGems) do
		local gem = Items.getItemByID(gemData.itemID)
		result = result..'\r\n|-\r\n|style="min-width:25px"|'
		result = result..Icons.Icon({gem.name, type='item', size='50', notext=true})
		result = result..'||'..Icons.Icon({gem.name, type='item', noicon=true})
		result = result..'||style="text-align:right"|'..string.format("%.1f%%", gemData.weight / totalWeight * 100)
		result = result..'||data-sort-value="'..gem.sellsFor..'"|'..Icons.GP(gem.sellsFor)
	end

	result = result..'\r\n|}'
	return result
end

function p.getFishTable(frame)
	local recipeList = GameData.getEntities(SkillData.Fishing.fish, function(fish) return true end)
	table.sort(recipeList, function(a, b) return a.level < b.level end)

	-- Determine cooking levels for all fish
	local cookReq = {}
	for i, recipe in ipairs(SkillData.Cooking.recipes) do
		-- This assumes that each raw fish only appears in a single recipe, which is a bit rubbish
		-- but currently holds
		for j, mat in ipairs(recipe.itemCosts) do
			if cookReq[mat.id] == nil then
				cookReq[mat.id] = recipe.level
			end
		end
	end

	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"')
	table.insert(resultPart, '\r\n!Fish\r\n!Name\r\n!' .. Icons.Icon({'Fishing', type='skill', notext=true}) .. ' Level\r\n!Catch Time')
	table.insert(resultPart, '\r\n!XP\r\n!Value\r\n!XP/s\r\n!GP/s')
	table.insert(resultPart, '\r\n!' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' Level')
	for i, recipe in ipairs(recipeList) do
		local fish = Items.getItemByID(recipe.productID)
		if fish ~= nil then
			local timeSortVal = (recipe.baseMinInterval + recipe.baseMaxInterval) / 2000
			local timeStr = string.format("%.1fs - %.1fs", recipe.baseMinInterval / 1000, recipe.baseMaxInterval / 1000)
			local XPs = recipe.baseExperience / timeSortVal
			local GPs = fish.sellsFor / timeSortVal
			local cookSortVal = cookReq[recipe.itemID] or 0
			local cookStr = cookReq[recipe.itemID] or 'N/A'
			table.insert(resultPart, '\r\n|-')
			table.insert(resultPart, '\r\n|style="text-align:center"| ' .. Icons.Icon({fish.name, type='item', size='50', notext=true}))
			table.insert(resultPart, '\r\n| ' .. Icons.Icon({fish.name, type='item', noicon=true}))
			table.insert(resultPart, '\r\n|style="text-align:right"| ' .. recipe.level)
			table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. timeSortVal .. '"| ' .. timeStr)
			table.insert(resultPart, '\r\n|style="text-align:right"| ' .. recipe.baseXP)
			table.insert(resultPart, '\r\n|data-sort-value="' .. fish.sellsFor .. '"| ' .. Icons.GP(fish.sellsFor))
			table.insert(resultPart, '\r\n|style="text-align:right"| ' .. Shared.round(XPs, 2, 2))
			table.insert(resultPart, '\r\n|data-sort-value="' .. GPs .. '"|' .. Icons.GP(Shared.round(GPs, 2, 2)))
			table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. cookSortVal .. '"| ' .. cookStr)
		end
	end
	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

function p.getFishingAreasTable(frame)
	local result = '{| class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!Name\r\n!Fish\r\n!Fish Chance'
	result = result..'\r\n!Junk Chance\r\n!Special Chance'

	for i, area in ipairs(SkillData.Fishing.areas) do
		result = result..'\r\n|-'
		result = result..'\r\n| style ="text-align: left;" |'..area.name

		local fishArray = {}
		for j, fishID in ipairs(area.fishIDs) do
			local fishItem = Items.getItemByID(fishID)
			if fishItem ~= nil then
				table.insert(fishArray, Icons.Icon({fishItem.name, type='item'}))
			end
		end
		result = result..'\r\n|'..table.concat(fishArray, '<br/>')

		result = result..'\r\n| style="text-align:right"|'..area.fishChance..'%'
		result = result..'\r\n| style="text-align:right"|'..area.junkChance..'%'
		result = result..'\r\n| style="text-align:right"|'..area.specialChance..'%'
	end

	result = result..'\r\n|}'
	return result
end

-- TODO Expose list of NPCs somewhere when applicable (e.g. for Thieving Shorts)
function p.getThievingGeneralRareTable(frame)
	local rareTxt = '{|class="wikitable sortable"'
	rareTxt = rareTxt..'\r\n!Item!!Qty'
	rareTxt = rareTxt..'!!Price!!colspan="2"|Chance'
	for i, drop in pairs(SkillData.Thieving.generalRareItems) do
		local thisItem = Items.getItemByID(drop.itemID)
		local odds = drop.chance

		rareTxt = rareTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
		rareTxt = rareTxt..'||1||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
		rareTxt = rareTxt..'||style="text-align:right" data-sort-value="'..odds..'"|'..Shared.fraction(1, Shared.round2(1/(odds/100), 0))
		rareTxt = rareTxt..'||style="text-align:right" data-sort-value="'..odds..'"|'..Shared.round(odds, 4, 4)..'%'
	end
	rareTxt = rareTxt..'\r\n|}'
	return rareTxt
end

function p._getThievingNPCLootTables(npc)
	local result = ''
	local sectionTxt = {}

	--Five sections here: GP, normal loot, area loot, rare loot, and unique item
	--First up, GP:
	local gpTxt = 'Successfully pickpocketing the '..npc.name..' will always give '..Icons.GP(1, npc.maxGP)
	table.insert(sectionTxt, gpTxt)

	--Next up, normal loot:
	--(Skip if no loot)
	if npc.lootTable ~= nil and Shared.tableCount(npc.lootTable) > 0 then
		local normalTxt = {}
		table.insert(normalTxt, '===Possible Common Drops:===\r\nUp to one of these will be received on a successful pickpocket:')
		local totalWt = 0
		local lootChance = SkillData.Thieving.itemChance
		local lootValue = 0

		--First loop through to get the total weight so we have it for later
		for i, loot in pairs(npc.lootTable) do
			totalWt = totalWt + loot.weight
		end

		table.insert(normalTxt, '\r\n{|class="wikitable sortable"')
		table.insert(normalTxt, '\r\n!Item!!Qty')
		table.insert(normalTxt, '!!Price!!colspan="2"|Chance')

		local lootTable = GameData.getEntities(npc.lootTable, function(loot) return true end)
		--Then sort the loot table by weight
		table.sort(lootTable, function(a, b) return a.weight > b.weight end)
		for i, loot in ipairs(lootTable) do
			local thisItem = Items.getItemByID(loot.id)
			if thisItem ~= nil then
				table.insert(normalTxt, '\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'}))
			else
				table.insert(normalTxt, '\r\n|-\r\n|Unknown Item[[Category:Pages with script errors]]')
			end
			table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..(loot.minQuantity + loot.maxQuantity)..'"|')

			if loot.minQuantitiy ~= loot.maxQuantitiy then
				table.insert(normalTxt, Shared.formatnum(loot.minQuantitiy) .. ' - ' .. Shared.formatnum(loot.maxQuantity))
			else
				table.insert(normalTxt, Shared.formatnum(loot.maxQuantity))
			end

			--Adding price columns
			local itemPrice = 0
			if thisItem == nil then
				table.insert(normalTxt, '||data-sort-value="0"|???')
			else
				itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
				if itemPrice == 0 or loot.minQuantity == loot.maxQuantitiy then
					table.insert(normalTxt, '||'..Icons.GP(itemPrice))
				else
					table.insert(normalTxt, '||'..Icons.GP(itemPrice * loot.minQuantitiy, itemPrice * loot.maxQuantitiy))
				end
			end

			--Getting the drop chance
			local dropChance = (loot.weight / totalWt * lootChance)
			if dropChance < 100 then
				--Show fraction as long as it isn't going to be 1/1
				table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..loot.weight..'"')
				table.insert(normalTxt, '|'..Shared.fraction(loot.weight * lootChance, totalWt * 100))
				table.insert(normalTxt, '||')
			else
				table.insert(normalTxt, '||colspan="2" data-sort-value="'..loot.weight..'"')
			end
			table.insert(normalTxt, 'style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%')

			--Adding to the average loot value based on price & dropchance
			lootValue = lootValue + (dropChance * 0.01 * itemPrice * (loot.minQuantitiy + loot.maxQuantitiy) / 2)
		end
		if Shared.tableCount(npc.lootTable) > 1 then
			table.insert(normalTxt, '\r\n|-class="sortbottom" \r\n!colspan="3"|Total:')
			if lootChance < 100 then
				table.insert(normalTxt, '\r\n|style="text-align:right"|'..Shared.fraction(lootChance, 100)..'||')
			else
				table.insert(normalTxt, '\r\n|colspan="2" ')
			end
			table.insert(normalTxt, 'style="text-align:right"|'..Shared.round(lootChance, 2, 2)..'%')
		end
		table.insert(normalTxt, '\r\n|}')
		table.insert(normalTxt, '\r\nThe loot obtained from the average successful pickocket is worth ' .. Icons.GP(Shared.round(lootValue, 2, 2)) .. ' if sold.')
		table.insert(normalTxt, '\r\n\r\nIncluding GP, the average successful pickocket is worth ' .. Icons.GP(Shared.round(lootValue + (1 + npc.maxGP) / 2, 2, 2)) .. '.')
		table.insert(sectionTxt, table.concat(normalTxt))
	end

	--After normal drops, add in rare drops
	local rareTxt = '===Possible Rare Drops:===\r\nAny of these can be received after a successful pickpocket'
	rareTxt = rareTxt..'\r\n'..p.getThievingGeneralRareTable()
	table.insert(sectionTxt, rareTxt)

	local areaTxt = '===Possible Area Unique Drops==='
	areaTxt = areaTxt..'\r\nAny Area Unique Drop is equally likely to be obtained after a successful pickpocket. '
	areaTxt = areaTxt..'\r\nEach Area Unique Drop is rolled for separately, so it is possible to receive multiple Area Unique Drops from a single action. '
	areaTxt = areaTxt..'The chance of receiving an Area Unique drop is tripled if the 95% Thieving Mastery Pool checkpoint is active.'

	local area = Skills.getThievingNPCArea(npc)
	areaTxt = areaTxt..'\r\n{|class="wikitable sortable"'
	areaTxt = areaTxt..'\r\n!Item!!Qty'
	areaTxt = areaTxt..'!!Price!!colspan="2"|Chance'
	local dropLines = {}
	for i, drop in pairs(area.uniqueDrops) do
		local thisItem = Items.getItemByID(drop.id)
		local lineTxt = ''
		lineTxt = lineTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
		lineTxt = lineTxt..'||data-sort-value="'..drop.quantity..'"| '..Shared.formatnum(drop.quantity)..'||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
		lineTxt = lineTxt..'||style="text-align:right"|'..Shared.fraction(1, 1/(SkillData.Thieving.baseAreaUniqueChance/100))
		lineTxt = lineTxt..'||'..Shared.round(SkillData.Thieving.AreaUniqueChance, 2, 2)..'%'
		dropLines[thisItem.name] = lineTxt
	end
	for i, txt in Shared.skpairs(dropLines) do
		areaTxt = areaTxt..txt
	end
	areaTxt = areaTxt..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
	areaTxt = areaTxt..'\r\n|style="text-align:right"|'..Shared.fraction(1, 1/(SkillData.Thieving.baseAreaUniqueChance/100))..'||'
	areaTxt = areaTxt..'style="text-align:right"|'..Shared.round(SkillData.Thieving.baseAreaUniqueChance, 2, 2)..'%'
	areaTxt = areaTxt..'\r\n|}'
	table.insert(sectionTxt, areaTxt)

	if npc.uniqueDrop ~= nil and npc.uniqueDrop.id ~= nil then
		local thisItem = Items.getItemByID(npc.uniqueDrop.id)
		if thisItem ~= nil then
			local uniqueTxt = '===Possible NPC Unique Drop==='
			uniqueTxt = uniqueTxt..'\r\nThe chance of receiving the unique drop for an NPC is based on a combination of several factors.'
			uniqueTxt = uniqueTxt..' The unique drop chance for an NPC is included in the tooltip for your Stealth against that NPC.'
			uniqueTxt = uniqueTxt..'\r\nThe unique drop for the '..npc.name..' is '
			if npc.uniqueDrop.quantity > 1 then
				uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item', qty=npc.uniqueDrop.quantity})
			else
				uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item'})
			end
			table.insert(sectionTxt, uniqueTxt)
		end
	end

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

function p.getThievingNPCLootTables(frame)
	local npcName = frame.args ~= nil and frame.args[1] or frame
	local npc = Skills.getThievingNPC(npcName)
	if npc == nil then
		return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
	end

	return p._getThievingNPCLootTables(npc)
end

function p.getThievingNPCTable()
	local result = '{| class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Name!!Area!!'..Icons.Icon({'Thieving', type='skill', notext=true})..' Level!!Experience!!Max Hit!!Perception!!GP!!Unique Drop'
	local npcArray = GameData.getEntities(SkillData.Thieving.npcs, function(npc) return true end)
	table.sort(npcArray, function(a, b) return a.level < b.level end)
	for i, npc in ipairs(npcArray) do
		result = result..'\r\n|-'
		result = result..'\r\n|'..Icons.Icon({npc.name, type='thieving', size='50', notext=true})
		result = result..'||'..Icons.Icon({npc.name, type='thieving', noicon=true})

		local area = Skills.getThievingNPCArea(npc)
		result = result..'||'..area.name
		result = result..'||data-sort-value="' .. npc.level .. '"| '..Icons._SkillReq('Thieving', npc.level)
		result = result..'||style="text-align:right"|'..npc.baseExperience
		result = result..'||style="text-align:right"|'..(npc.maxHit * 10)
		result = result..'||style="text-align:right"|'..npc.perception
		result = result..'||data-sort-value="' .. npc.maxGP .. '"|'..Icons.GP(1, npc.maxGP)
		if npc.uniqueDrop ~= nil then
			local uniqueDrop = Items.getItemByID(npc.uniqueDrop.id)
			if npc.uniqueDrop.quantity > 1 then
				result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item', qty = npc.uniqueDrop.quantity})
			else
				result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item'})
			end
		else
			result = result..'|| '
		end
	end
	result = result..'\r\n|}'

	return result
end

function p.getThievingAreaTable(frame)
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"')
	table.insert(resultPart, '\r\n!Area!!'..Icons.Icon({'Thieving', type='skill', notext=true})..' Level!!NPCs!!Unique Drops')

	for i, area in ipairs(SkillData.Thieving.areas) do
		local minLevel, npcList, areaItemList = nil, {}, {}
		-- Build NPC list & determine level for area, this is the minimum
		-- Thieving level required for all NPCs within that area
		if area.npcs ~= nil and not Shared.tableIsEmpty(area.npcs) then
			for j, npcID in ipairs(area.npcs) do
				local npc = Skills.getThievingNPCByID(npcID)
				if minLevel == nil or npc.level < minLevel then
					minLevel = npc.level
				end
				table.insert(npcList, Icons.Icon({npc.name, type='thieving'}))
			end
		else
			table.insert(npcList, '')
		end

		-- Build area unique item list
		if area.uniqueDrops ~= nil and Shared.tableCount(area.uniqueDrops) > 0 then
			for k, drop in ipairs(area.uniqueDrops) do
				local areaItem = Items.getItemByID(drop.id)
				if areaItem == nil then
					table.insert(areaItemList, 'Unknown[[Category:Pages with script errors]]')
				else
					local iconDef = {areaItem.name, type='item'}
					if drop.quantity > 1 then
						iconDef.qty = drop.quantity
					end
					table.insert(areaItemList, Icons.Icon(iconDef))
				end
			end
		else
			table.insert(areaItemList, '')
		end

		-- Generate table row
		table.insert(resultPart, '\r\n|-')
		table.insert(resultPart, '\r\n|' .. area.name)
		table.insert(resultPart, '\r\n|' .. Icons._SkillReq('Thieving', minLevel))
		table.insert(resultPart, '\r\n|' .. table.concat(npcList, '<br/>'))
		table.insert(resultPart, '\r\n|' .. table.concat(areaItemList, '<br/>'))
	end
	table.insert(resultPart, '\r\n|}')

	return table.concat(resultPart)
end

function p._getFarmingTable(categoryName)
	local category = GameData.getEntityByName(SkillData.Farming.categories, categoryName)
	if category == nil then
		return 'ERROR: Invalid farming category. Please choose Allotments, Herbs, or Trees'
	end
	local seedList = GameData.getEntities(SkillData.Farming.recipes,
		function(recipe)
			return recipe.categoryID == category.id
		end)
	if Shared.tableIsEmpty(seedList) then
		return ''
	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.id == 'melvorD:Allotment' then
		result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
	elseif category.id == 'melvorD:Herb' then
		result = result..'!!colspan="2"|Herb!!Herb Value'
	elseif category.id == 'melvorD: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 ipairs(seedList) do
		local seedItem = Items.getItemByID(seed.seedCost.id)
		local productItem = Items.getItemByID(seed.productId)
		if seedItem ~= nil and productItem ~= nil then
			result = result..'\r\n|-'
			result = result..'\r\n|'..Icons.Icon({seedItem.name, type='item', size='50', notext=true})..'||[['..seedItem.name..']]'
			result = result..'||'..seed.level..'||'..Shared.formatnum(seed.baseExperience)
			result = result..'||data-sort-value="'..(seed.baseInterval / 1000)..'"|'..Shared.timeString(seed.baseInterval / 1000, true)
			result = result..'||data-sort-value="'..seedItem.sellsFor..'"|'..Icons.GP(seedItem.sellsFor)
			result = result..'||'..Icons.Icon({productItem.name, type='item', size='50', notext=true})..'||[['..productItem.name..']]'
			if category.id == 'melvorD:Allotment' then
				result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..((productItem.healsFor or 0) * 10)
			end
			result = result..'||data-sort-value="'..productItem.sellsFor..'"|'..Icons.GP(productItem.sellsFor)
			result = result..'||'..ItemSourceTables._getItemSources(seedItem.id)
		end
	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 recipes = GameData.getEntities(SkillData.Farming.recipes,
		function(recipe)
			local product = Items.getItemByID(recipe.productID)
			return product ~= nil and product.healsFor ~= nil and product.healsFor > 0
		end)
	table.sort(recipes, function(a, b) return a.level < b.level end)

	for i, recipe in ipairs(recipes) do
		local product = Items.getItemByID(recipe.productID)
		if product ~= nil and product.healsFor ~= nil and product.healsFor > 0 then
			result = result..'\r\n|-'
			result = result..'\r\n|'..Icons.Icon({product.name, type='item', notext='true', size='50'})..'||[['..product.name..']]'
			result = result..'||style="text-align:right;"|'..recipe.level
			result = result..'||style="text-align:right" data-sort-value="'..product.healsFor..'"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..(product.healsFor * 10)
			result = result..'||style="text-align:right" data-sort-value="'..product.sellsFor..'"|'..Icons.GP(product.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="' .. patch.level .. '"|'..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._buildAstrologyConstellationTable()
	local maxModifier = 5
	local result = '{|class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Constellation!!'..Icons.Icon({"Astrology", type='skill', notext='true'})..' Level'
	result = result..'!!XP!!Skills!!Standard Modifiers!!Unique Modifiers'

	for i, cons in ipairs(SkillData.Astrology.Constellations) do
		local name = cons.name
		result = result..'\r\n|-'
		result = result..'\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='constellation', size='50', notext=true})..'||'..name
		result = result..'||'..cons.level..'||'..cons.provides.xp

		local skillIconArray = {}
		for j, skillID in ipairs(cons.skills) do
			table.insert(skillIconArray, Icons.Icon({Constants.getSkillName(skillID), type='skill'}))
		end
		result = result..'||'..table.concat(skillIconArray, '<br/>')

		local standModsRaw = Skills._buildAstrologyModifierArray(cons, maxModifier, true, false, false, false)
		local standMods = {}
		--Building the list of Standard modifiers:
		for j, modifier in ipairs(standModsRaw) do
			table.insert(standMods, Constants._getModifierText(modifier[1], modifier[2], false))
		end
		result = result..'|| '..table.concat(standMods, '<br/>')

		--Building the list of all Unique Modifiers
		local uModsRaw = Skills._buildAstrologyModifierArray(cons, maxModifier, false, true, false, false)
		local uMods = {}
		for j, modifier in ipairs(uModsRaw) do
			table.insert(uMods, Constants._getModifierText(modifier[1], modifier[2], false))
		end
		result = result..'||'..table.concat(uMods, '<br/>')
	end
	result = result..'\r\n|}'

	return result
end

function p.buildAstrologyConstellationTable(frame)
	return p._buildAstrologyConstellationTable()
end

function p.buildAstrologyValueTable()
	local result = '{|class="wikitable sortable"'
	result = result..'\r\n!rowspan="2"| Value!!colspan="2"| Chance'
	result = result..'\r\n|-\r\n! This Value!! This Value or Greater'
	local cumulativeChance = 100
	for i, chance in ipairs(SkillData.Astrology.ModifierMagnitudeChances) do
		result = result..'\r\n|-'
		result = result..'\r\n|style="text-align:right"| ' .. i
		result = result..'\r\n|style="text-align:right"| ' .. chance .. '%'
		result = result..'\r\n|style="text-align:right"| ' .. cumulativeChance .. '%'
		cumulativeChance = cumulativeChance - chance
	end
	result = result..'\r\n|}'
	return result
end

return p