Module:Skills/Gathering: Difference between revisions

From Melvor Idle
m (getFishTable fix Skill icons)
(Left align sources column)
 
(99 intermediate revisions by 7 users not shown)
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 Common = require('Module:Common')
local Common = require('Module:Common')
local Items = require('Module:Items')
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')
local Skills = require('Module:Skills')
local ItemSourceTables = require('Module:Items/SourceTables')
function p.getRecipeRequirements(skillName, recipe)
local reqText = {}
if recipe.level ~= nil then
table.insert(reqText, Icons._SkillReq(skillName, recipe.level, false))
end
if recipe.totalMasteryRequired ~= nil then
table.insert(reqText, Shared.formatnum(recipe.totalMasteryRequired) .. ' ' .. Icons.Icon({skillName, type='skill', notext=true}) .. ' ' .. Icons.Icon({'Mastery'}))
end
if type(recipe.requirements) == 'table' then
local reqs = Common.getRequirementString(recipe.requirements)
if reqs ~= nil then
table.insert(reqText, reqs)
end
end
return table.concat(reqText, '<br/>')
end


function p.getAxeTable(frame)
function p.getTreesTable(frame)
  local toolArray = {}
local resultPart = {}
  for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
table.insert(resultPart, '{| class="wikitable sortable"')
    if Shared.contains(upgrade.name, 'Axe') then
table.insert(resultPart, '\n|- class="headerRow-0"')
      table.insert(toolArray, upgrade)
table.insert(resultPart, '\n!colspan="2"|Tree!!colspan="2"|Logs!!Requirements')
    end
table.insert(resultPart, '!!XP!!Cut Time!!XP/s!!GP/s')
  end
 
local trees = Shared.shallowClone(SkillData.Woodcutting.trees)
table.sort(trees, function(a, b) return a.level < b.level end)
for i, tree in ipairs(trees) do
local log = Items.getItemByID(tree.productId)
table.insert(resultPart, '\n|-')
table.insert(resultPart, '\n|class="table-img" data-sort-value="'..tree.name..'"| '..Icons.Icon({log.name, img=tree.name, type='tree', notext=true, size=50}))
table.insert(resultPart, '\n|data-sort-value="'..tree.name..'"|'..Icons.getExpansionIcon(tree.id)..tree.name)
table.insert(resultPart, '\n|class="table-img" data-sort-value="'..log.name..'"| '..Icons.Icon({log.name, type='item', notext=true, size=50}))
table.insert(resultPart, '\n| '..Icons.Icon({log.name, type='item', noicon=true}))
table.insert(resultPart, '\n|data-sort-value="' .. tree.level .. '"| ' .. p.getRecipeRequirements(SkillData.Woodcutting.name, tree))
table.insert(resultPart, '\n|style="text-align:right"| '..tree.baseExperience)
table.insert(resultPart, '\n|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)
table.insert(resultPart, '\n|style="text-align:right"| '..Shared.round(XPs, 2, 2))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="'..GPs..'"| '..Icons.GP(Shared.round(GPs, 2, 2)))
end
 
table.insert(resultPart, '\n|}')
return table.concat(resultPart)
end


  local result = '{| class="wikitable"'
function p.getSpecialFishingTable(frame)
  result = result..'\r\n!colspan="4"| !!colspan="2"|Cut Time Decrease'
local totalWt, lootValue = 0, 0
  result = result..'\r\n|- class="headerRow-0"'
local itemArray = Shared.shallowClone(SkillData.Fishing.specialItems)
  result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Woodcutting', type='skill', notext=true})..' Level'
for i, itemDef in ipairs(itemArray) do
  result = result..'!!Cost!!This Axe!!Total'
totalWt = totalWt + itemDef.weight
end
-- Sort the loot table by weight in descending order
table.sort(itemArray, function(a, b) return a.weight > b.weight end)


  local total = 0
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|class="table-img"| ' .. 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


  for i, tool in Shared.skpairs(toolArray) do
function p.getFishingJunkTable(frame)
    result = result..'\r\n|-'
local resultPart = {}
    result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
    result = result..'||'..tool.name
table.insert(resultPart, '\r\n|- class="headerRow-0"')
    local level = 1
table.insert(resultPart, '\r\n!colspan="2"|Item!!Value')
    if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil 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][2]
    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)


    local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
local itemArray = {}
    total = total + cutTime
for i, itemID in ipairs(SkillData.Fishing.junkItemIDs) do
    result = result..'||style="text-align:right"|-'..cutTime..'%'
local item = Items.getItemByID(itemID)
    result = result..'||style="text-align:right"|-'..total..'%'
if item ~= nil then
  end
table.insert(itemArray, item)
end
end
table.sort(itemArray, function(a, b) return a.name < b.name end)


  result = result..'\r\n|}'
for i, item in ipairs(itemArray) do
  return result
table.insert(resultPart, '\r\n|-')
table.insert(resultPart, '\r\n|class="table-img"| ' .. 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
end


function p.getPickaxeTable(frame)
function p.getMiningOresTable(frame)
  local toolArray = {}
local resultPart = {}
  for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
    if Shared.contains(upgrade.name, 'Pickaxe') then
table.insert(resultPart, '\n|- class="headerRow-0"')
      table.insert(toolArray, upgrade)
table.insert(resultPart, '\n!colspan="2"|Rock!!colspan=2|Ore!!Type!!Requirements')
    end
table.insert(resultPart, '!!XP!!Respawn Time!!Ore Value')
  end


  local result = '{| class="wikitable"'
local mineData = Shared.shallowClone(SkillData.Mining.rockData)
  result = result..'\r\n!colspan="4"| !!colspan="2"|Mine Time Decrease!!colspan="2"|2x Ore Chance'
table.sort(mineData, function(a, b) return a.level < b.level end)
  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
for i, oreData in ipairs(mineData) do
  local total2 = 0
local ore = Items.getItemByID(oreData.productId)
local respawnStyle, respawnSort, respawnText = 'class="table-na"', 0, 'N/A'
if oreData.hasPassiveRegen then
respawnStyle = 'style="text-align:right;'
respawnSort = oreData.baseRespawnInterval / 1000
respawnText = Shared.timeString(respawnSort, true)
end
local rockName = Icons.Icon({oreData.name, type='rock', noicon=true, nolink=true})
local qtyText = (oreData.baseQuantity > 1 and '<b>' .. oreData.baseQuantity .. 'x</b> ') or ''
table.insert(resultPart, '\n|-\n|class="table-img" data-sort-value="' .. rockName .. '"| '..Icons.Icon({oreData.name, type='rock', size='50', notext=true, nolink=true}))
table.insert(resultPart, '\n| data-sort-value="' ..rockName.. '"|'..Icons.getExpansionIcon(oreData.id) .. rockName)
table.insert(resultPart, '\n|class="table-img" data-sort-value="' .. ore.name .. '"| '..Icons.Icon({ore.name, type='item', size='50', notext=true}))
table.insert(resultPart, '\n| ' .. qtyText .. Icons.Icon({ore.name, type='item', noicon=true}))
table.insert(resultPart, '\n| ' .. oreData.type)
table.insert(resultPart, '\n|data-sort-value="' .. oreData.level ..'"| '..p.getRecipeRequirements(SkillData.Mining.name, oreData))
table.insert(resultPart, '\n|style="text-align:right"| '..oreData.baseExperience)
table.insert(resultPart, '\n|' .. respawnStyle .. ' data-sort-value="'..respawnSort..'"| ' .. respawnText)
table.insert(resultPart, '\n|data-sort-value="'..ore.sellsFor..'"| '..Icons.GP(ore.sellsFor))
end


  for i, tool in Shared.skpairs(toolArray) do
table.insert(resultPart, '\n|}')
    result = result..'\r\n|-'
return table.concat(resultPart)
    result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
end
    result = result..'||'..tool.name
    local level = 1
    if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil 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][2]
    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)


    local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
function p._getMiningGemsTable(gemType)
    total = total + cutTime
if type(gemType) ~= 'string' then
gemType = 'Normal'
end
local validTypes = {
["Normal"] = 'randomGems',
["Superior"] = 'randomSuperiorGems'
}
local gemDataKey = validTypes[gemType]
if gemDataKey == nil then
return Shared.printError('No such gem type "' .. gemType .. '"')
end
local gemData = GameData.rawData[gemDataKey]
local totalWeight = 0
for i, gem in ipairs(gemData) do
totalWeight = totalWeight + gem.weight
end
local resultPart = {}
table.insert(resultPart, '{|class="wikitable sortable"')
table.insert(resultPart, '\n|- class="headerRow-0"')
table.insert(resultPart, '\n!colspan=2|Gem!!Gem Chance!!Gem Price')


    result = result..'||style="text-align:right"|-'..cutTime..'%'
for i, gem in ipairs(gemData) do
    result = result..'||style="text-align:right"|-'..total..'%'
local gemItem = Items.getItemByID(gem.itemID)
local gemPct = gem.weight / totalWeight * 100
table.insert(resultPart, '\n|-\n|class="table-img"| ')
table.insert(resultPart, Icons.Icon({gemItem.name, type='item', size='50', notext=true}))
table.insert(resultPart, '\n| data-sort-value="'..gemItem.name..'"|'..Icons.getExpansionIcon(gemItem.id) .. Icons.Icon({gemItem.name, type='item', noicon=true}))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. gemPct .. '" | ' .. string.format("%.1f%%", gemPct))
table.insert(resultPart, '\n|data-sort-value="' .. gemItem.sellsFor .. '"| ' .. Icons.GP(gemItem.sellsFor))
end


    local OreDouble = tool.contains.modifiers.increasedChanceToDoubleOres
table.insert(resultPart, '\n|}')
    total2 = total2 + OreDouble
return table.concat(resultPart)
end


    result = result..'||style="text-align:right"|+'..OreDouble..'%'
function p.getMiningGemsTable(frame)
    result = result..'||style="text-align:right"|+'..total2..'%'
local gemType = frame.args ~= nil and frame.args[1] or frame
  end
return p._getMiningGemsTable(gemType)
end
 
function p.getFishTable(frame)
local recipeList = Shared.shallowClone(SkillData.Fishing.fish)
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, '\n|- class="headerRow-0"')
table.insert(resultPart, '\n!colspan="2" rowspan="2"|Fish\n!rowspan="2"|' .. Icons.Icon({'Fishing', type='skill', notext=true}) .. ' Level')
table.insert(resultPart, '\n!colspan="3"|Catch Time\n!rowspan="2"|XP\n!rowspan="2"|Value\n!rowspan="2"|XP/s\n!rowspan="2"|GP/s')
table.insert(resultPart, '\n!rowspan="2"|' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' Level')
table.insert(resultPart, '\n|- class="headerRow-1"\n!Min\n!Max\n!Avg')
for i, recipe in ipairs(recipeList) do
local fish = Items.getItemByID(recipe.productId)
if fish ~= nil then
local timeMin, timeMax = recipe.baseMinInterval / 1000, recipe.baseMaxInterval / 1000
local timeAvg = (timeMin + timeMax) / 2
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.productId] or 0
local cookStyle = (cookReq[recipe.productId] ~= nil and 'style="text-align:right" ' or 'class="table-na" ')
local cookStr = cookReq[recipe.productId] or 'N/A'
table.insert(resultPart, '\n|-')
table.insert(resultPart, '\n|class="table-img"| ' .. Icons.Icon({fish.name, type='item', size='50', notext=true}))
table.insert(resultPart, '\n|data-sort-value="'..fish.name..'"|'..Icons.getExpansionIcon(fish.id) .. Icons.Icon({fish.name, type='item', noicon=true}))
table.insert(resultPart, '\n|style="text-align:right"| ' .. recipe.level)
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. timeMin .. '"| ' .. string.format("%.1fs", timeMin))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. timeMax .. '"| ' .. string.format("%.1fs", timeMax))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. timeAvg .. '"| ' .. string.format("%.1fs", timeAvg))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. recipe.baseExperience .. '"| ' .. Shared.formatnum(recipe.baseExperience))
table.insert(resultPart, '\n|data-sort-value="' .. fish.sellsFor .. '"| ' .. Icons.GP(fish.sellsFor))
table.insert(resultPart, '\n|style="text-align:right"| ' .. Shared.round(XPs, 2, 2))
table.insert(resultPart, '\n|data-sort-value="' .. GPs .. '"|' .. Icons.GP(Shared.round(GPs, 2, 2)))
table.insert(resultPart, '\n|' .. cookStyle .. 'data-sort-value="' .. cookSortVal .. '"| ' .. cookStr)
end
end
table.insert(resultPart, '\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;" |'..Icons.getExpansionIcon(area.id)..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
 
function p.getThievingGeneralRareTable(frame)
return p._getThievingGeneralRareTable()
end


  result = result..'\r\n|}'
function p._getThievingGeneralRareTable(npcID)
  return result
local rareTxt = '{|class="wikitable sortable"'
rareTxt = rareTxt..'\r\n!Item!!Qty'
rareTxt = rareTxt..'!!Price!!colspan="2"|Chance'
for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
-- If an npcID has been passed and the item is NPC specific, only display
-- the item if it may be obtained while pickpocketing that NPC
if npcID == nil or drop.npcs == nil or Shared.contains(drop.npcs, npcID) then
local thisItem = Items.getItemByID(drop.itemID)
local odds = drop.chance
rareTxt = rareTxt..'\r\n|-\r\n|data-sort-value="'..thisItem.name..'"|'..Icons.getExpansionIcon(thisItem.id)..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
end
rareTxt = rareTxt..'\r\n|}'
return rareTxt
end
end


function p.getRodTable(frame)
function p._getThievingNPCLootTables(npc)
  local toolArray = {}
local result = ''
  for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
local sectionTxt = {}
    if Shared.contains(upgrade.name, 'Fishing Rod') then
 
      table.insert(toolArray, upgrade)
--Five sections here: GP, normal loot, area loot, rare loot, and unique item
    end
--First up, GP:
  end
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 = Shared.shallowClone(npc.lootTable)
--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.itemID)
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.minQuantity ~= loot.maxQuantity then
table.insert(normalTxt, Shared.formatnum(loot.minQuantity) .. ' - ' .. 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.maxQuantity then
table.insert(normalTxt, '||'..Icons.GP(itemPrice))
else
table.insert(normalTxt, '||'..Icons.GP(itemPrice * loot.minQuantity, itemPrice * loot.maxQuantity))
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.minQuantity + loot.maxQuantity) / 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 pickpocket is worth ' .. Icons.GP(Shared.round(lootValue, 2, 2)) .. ' if sold.')
table.insert(normalTxt, '\r\n\r\nIncluding GP, the average successful pickpocket is worth ' .. Icons.GP(Shared.round(lootValue + (1 + npc.maxGP) / 2, 2, 2)) .. '.')
table.insert(sectionTxt, table.concat(normalTxt))
end


  local result = '{| class="wikitable"'
--After normal drops, add in rare drops
  result = result..'\r\n!colspan="4"| !!colspan="2"|Catch Time Decrease'
local rareTxt = '===Possible Rare Drops:===\r\nAny of these can be received after a successful pickpocket:'
  result = result..'\r\n|- class="headerRow-0"'
rareTxt = rareTxt..'\r\n'..p._getThievingGeneralRareTable(npc.id)
  result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Fishing', type='skill', notext=true})..' Level'
table.insert(sectionTxt, rareTxt)
  result = result..'!!Cost!!This Rod!!Total'


  local total = 0
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.'


  for i, tool in Shared.skpairs(toolArray) do
local area = Skills.getThievingNPCArea(npc)
    result = result..'\r\n|-'
areaTxt = areaTxt..'\r\n{|class="wikitable sortable"'
    result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
areaTxt = areaTxt..'\r\n!Item!!Qty'
    result = result..'||'..tool.name
areaTxt = areaTxt..'!!Price!!colspan="2"|Chance'
    local level = 1
local dropLines = {}
    if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil then
for i, drop in ipairs(area.uniqueDrops) do
      --Gonna be lazy and assume there's only the one skill level and it's the one we care about
local thisItem = Items.getItemByID(drop.id)
      level = tool.unlockRequirements.skillLevel[1][2]
local lineTxt = ''
    end
lineTxt = lineTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
    result = result..'||style="text-align:right"|'..level
lineTxt = lineTxt..'||data-sort-value="'..drop.quantity..'"| '..Shared.formatnum(drop.quantity)..'||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
    result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)
lineTxt = lineTxt..'||style="text-align:right"|'..Shared.fraction(1, 1/(SkillData.Thieving.baseAreaUniqueChance/100))
lineTxt = lineTxt..'||'..Shared.round(SkillData.Thieving.baseAreaUniqueChance, 2, 2)..'%'
dropLines[thisItem.name] = lineTxt
end
for i, txt in pairs(dropLines) do
areaTxt = areaTxt..txt
end
areaTxt = areaTxt..'\r\n|}'
table.insert(sectionTxt, areaTxt)


    local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
if npc.uniqueDrop ~= nil and npc.uniqueDrop.id ~= nil then
    total = total + cutTime
local thisItem = Items.getItemByID(npc.uniqueDrop.id)
    result = result..'||style="text-align:right"|-'..cutTime..'%'
if thisItem ~= nil then
    result = result..'||style="text-align:right"|-'..total..'%'
local uniqueTxt = '===Possible NPC Unique Drop==='
  end
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


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


function p.getTreesTable(frame)
function p.getThievingNPCLootTables(frame)
  local result = '{| class="wikitable sortable"'
local npcName = frame.args ~= nil and frame.args[1] or frame
  result = result..'\r\n|- class="headerRow-0"'
local npc = Skills.getThievingNPC(npcName)
  result = result..'\r\n!colspan="2"|Tree!!colspan="2"|Logs!!'..Icons.Icon({'Woodcutting', type='skill', notext=true})..' Level'
if npc == nil then
  result = result..'!!XP!!Cut Time!!XP/s!!GP/s'
return Shared.printError('Invalid Thieving NPC "' .. npcName .. '"')
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 = Shared.shallowClone(SkillData.Thieving.npcs)
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..'||data-sort-value="'..npc.name..'"|'..Icons.getExpansionIcon(npc.id)..Icons.Icon({npc.name, type='thieving', noicon=true})


  for i, tree in Shared.skpairs(SkillData.Woodcutting.Trees) do
local area = Skills.getThievingNPCArea(npc)
    result = result..'\r\n|-'
result = result..'||'..area.name
    local treeName = Shared.titleCase(tree.type..' tree')
result = result..'||data-sort-value="' .. npc.level .. '"| '..Icons._SkillReq('Thieving', npc.level)
    local logName = Shared.titleCase(tree.type..' logs')
result = result..'||style="text-align:right"|'..npc.baseExperience
    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="text-align:right"|'..(npc.maxHit * 10)
    result = result..'||'..treeName..''
result = result..'||style="text-align:right" data-sort-value="' .. npc.perception .. '| '..Shared.formatnum(npc.perception)
    result = result..'||style="min-width:25px" data-sort-value="'..logName..'"|'..Icons.Icon({logName, type='item', notext=true, size=50})
result = result..'||data-sort-value="' .. npc.maxGP .. '"|'..Icons.GP(1, npc.maxGP)
    result = result..'||[['..logName..']]'
if npc.uniqueDrop ~= nil then
    result = result..'||style="text-align:right"|'..tree.level
local uniqueDrop = Items.getItemByID(npc.uniqueDrop.id)
    result = result..'||style="text-align:right"|'..tree.xp
if npc.uniqueDrop.quantity > 1 then
    result = result..'||style="text-align:right" data-sort-value="'..tree.interval..'"|'..Shared.timeString(tree.interval/1000, true)
result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item', qty = npc.uniqueDrop.quantity})
    local XPs = tree.xp / (tree.interval / 1000)
else
    local Log = Items.getItemByID(i - 1)
result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item'})
    local GPs = Log.sellsFor / (tree.interval / 1000)
end
    result = result..'||style="text-align:right"|'..Shared.round(XPs, 2, 2)
else
    result = result..'||style="text-align:right" data-sort-value="'..GPs..'"|'..Icons.GP(Shared.round(GPs, 2, 2))
result = result..'|| '
  end
end
end
result = result..'\r\n|}'


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


function p.getSpecialFishingTable(frame)
function p.getThievingAreaTable(frame)
  local lootValue = 0
local resultPart = {}
  local totalWt = Items.specialFishWt
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')


  local result = ''
for i, area in ipairs(SkillData.Thieving.areas) do
  result = result..'\r\n{|class="wikitable sortable stickyHeader"'
local minLevel, npcList, areaItemList = nil, {}, {}
  result = result..'\r\n|- class="headerRow-0"'
-- Build NPC list & determine level for area, this is the minimum
  result = result..'\r\n!Item'
-- Thieving level required for all NPCs within that area
  result = result..'!!Price!!colspan="2"|Chance'
if area.npcIDs ~= nil and not Shared.tableIsEmpty(area.npcIDs) then
for j, npcID in ipairs(area.npcIDs) 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


  --Sort the loot table by weight in descending order
-- Build area unique item list
  table.sort(Items.specialFishLoot, function(a, b) return a[2] > b[2] end)
if area.uniqueDrops ~= nil and Shared.tableCount(area.uniqueDrops) > 0 then
  for i, row in pairs(Items.specialFishLoot) do
for k, drop in ipairs(area.uniqueDrops) do
    local thisItem = Items.getItemByID(row[1])
local areaItem = Items.getItemByID(drop.id)
    result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
if areaItem == nil then
    result = result..'||style="text-align:left" data-sort-value="'..thisItem.sellsFor..'"'
table.insert(areaItemList, 'Unknown[[Category:Pages with script errors]]')
    result = result..'|'..Icons.GP(thisItem.sellsFor)
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


    local dropChance = (row[2] / totalWt) * 100
-- Generate table row
    result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
table.insert(resultPart, '\r\n|-')
    result = result..'|'..Shared.fraction(row[2], totalWt)
table.insert(resultPart, '\r\n|' .. area.name)
    result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
table.insert(resultPart, '\r\n|' .. Icons._SkillReq('Thieving', minLevel))
    lootValue = lootValue + (dropChance * 0.01 * thisItem.sellsFor)
table.insert(resultPart, '\r\n|' .. table.concat(npcList, '<br/>'))
  end
table.insert(resultPart, '\r\n|' .. table.concat(areaItemList, '<br/>'))
  result = result..'\r\n|}'
end
  result = result..'\r\nThe average value of a roll on the special fishing loot table is '..Icons.GP(Shared.round(lootValue, 2, 0))
table.insert(resultPart, '\r\n|}')


  return result
return table.concat(resultPart)
end
end


function p.getFishingJunkTable(frame)
function p._getFarmingTable(categoryName)
  local result = '{| class="wikitable sortable stickyHeader"'
local category = GameData.getEntityByName(SkillData.Farming.categories, categoryName)
  result = result..'\r\n|- class="headerRow-0"'
if category == nil then
  result = result..'\r\n!colspan="2"|Item!!Value'
return Shared.printError('Invalid farming category. Please choose Allotments, Herbs, or Trees')
 
end
  local itemArray = Items.getItems(function(item) return item.type == "Junk" end)
local seedList = GameData.getEntities(SkillData.Farming.recipes,
function(recipe)
return recipe.categoryID == category.id
end)
if Shared.tableIsEmpty(seedList) then
return ''
end
 
local tbl = mw.html.create()
local html = tbl:tag("table")
        :addClass("wikitable sortable stickyHeader")
    :tag('tr'):addClass("headerRow-0")
        :tag('th'):attr("colspan", 2):wikitext("Seeds")
        :tag('th'):wikitext(Icons.Icon({'Farming', type='skill', notext=true}) .. " Level")
        :tag('th'):wikitext('XP')
        :tag('th'):wikitext('Growth Time')
        :tag('th'):wikitext('Seed Value')
 
if category.id == 'melvorD:Allotment' then
html:tag('th'):attr("colspan", 2):wikitext("Crop")
:tag('th'):wikitext('Crop Healing')
:tag('th'):wikitext('Crop Value')
elseif category.id == 'melvorD:Herb' then
html:tag('th'):attr("colspan", 2):wikitext("Herb")
:tag('th'):wikitext('Herb Value')
elseif category.id == 'melvorD:Tree' then
html:tag('th'):attr("colspan", 2):wikitext("Logs")
:tag('th'):wikitext('Log Value')
end
html = html:tag('th'):wikitext('Seed Sources')
 
table.sort(seedList, function(a, b) return a.level < b.level 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
html =
html:tag('tr')
:tag('td'):wikitext(Icons.Icon({seedItem.name, type='item', size='50', notext=true}))
:tag('td'):wikitext(Icons.getExpansionIcon(seedItem.id) .. Icons.Icon({seedItem.name, type='item', noicon=true}))
:tag('td'):wikitext(seed.level)
:tag('td'):wikitext(Shared.formatnum(seed.baseExperience))
:tag('td'):attr('data-sort-value', (seed.baseInterval / 1000))
  :wikitext(Shared.timeString(seed.baseInterval / 1000, true))
:tag('td'):attr('data-sort-value', seedItem.sellsFor)
      :wikitext(Icons.GP(seedItem.sellsFor))
:tag('td'):wikitext(Icons.Icon({productItem.name, type='item', size='50', notext=true}))
:tag('td'):wikitext(Icons.getExpansionIcon(productItem.id) .. Icons.Icon({productItem.name, type='item', noicon=true}))


  table.sort(itemArray, function(a, b) return a.name < b.name end)
if category.id == 'melvorD:Allotment' then
html:tag('td'):wikitext(Icons.Icon({'Hitpoints', type='skill', notext=true}))
  :wikitext(' ')
  :wikitext(((productItem.healsFor or 0) * 10))
end
html =
html:tag('td'):attr('data-sort-value', productItem.sellsFor)
  :wikitext(Icons.GP(productItem.sellsFor))
:tag('td'):wikitext(ItemSourceTables._getItemSources(seedItem))
  :css('text-align', 'left')
:done()
end
end


  for i, item in Shared.skpairs(itemArray) do
return tostring(tbl:done())
    result = result..'\r\n|-'
end
    result = result..'\r\n|style="min-width:25px"|'..Icons.Icon({item.name, type='item', notext='true', size='50'})..'||[['..item.name..']]'
    result = result..'||style="text-align:right;" data-sort-value="'..item.sellsFor..'"|'..Icons.GP(item.sellsFor)
  end


  result = result..'\r\n|}'
function p.getFarmingTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame


  return result
return p._getFarmingTable(category)
end
end


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


  local mineData = Shared.clone(SkillData.Mining.Rocks)
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)


  table.sort(mineData, 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'})
result = result..'|| ' .. Icons.getExpansionIcon(product.id) .. Icons.Icon({product.name, type='item', noicon=true})
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


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


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


function p.getFishTable(frame)
function p.getFarmingPlotTable(frame)
  local data = Items.getItems(function(item) return item.fishingID ~= nil end)
local areaName = frame.args ~= nil and frame.args[1] or frame
local category = GameData.getEntityByName(SkillData.Farming.categories, areaName)
if category == nil then
return Shared.printError('Invalid farming category. Please choose Allotments, Herbs, or Trees')
end
local patches = GameData.getEntities(SkillData.Farming.plots,
function(plot)
return plot.categoryID == category.id
end)
table.sort(patches,
function(a, b)
if a.level == b.level then
return a.id < b.id
else
return a.level < b.level
end
end)
if Shared.tableIsEmpty(patches) then
return ''
end
 
local result = '{|class="wikitable sortable stickyHeader"'
result = result..'\r\n!Plot!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level!!Cost'
 
for i, patch in ipairs(patches) do
result = result..'\r\n|-\r\n|'..i
result = result..'||style="text-align:right;" data-sort-value="' .. patch.level .. '"|'..patch.level
local costText = Common.getCostString({ items = patch.itemCosts, gp = patch.gpCost }, 'Free')
result = result..'||style="text-align:right;" data-sort-value="'..patch.gpCost..'"|'..costText
end
 
result = result..'\r\n|}'
return result
end


  table.sort(data, function(a, b) return a.fishingID < b.fishingID end)
function p._buildAstrologyConstellationTable()
local resultPart = {}
table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
table.insert(resultPart, '\n|- class="headerRow-0"')
table.insert(resultPart, '\n!rowspan="2" colspan="2"|Constellation!!rowspan="2"| ' .. Icons.Icon({"Astrology", type='skill', notext='true'}) .. ' Level')
table.insert(resultPart, '!!rowspan="2"| XP!!rowspan="2"| Skills!!colspan="2"| Standard Stars!!colspan="2"| Unique Stars')
table.insert(resultPart, '\n|- class="headerRow-1"')
table.insert(resultPart, string.rep('\n! ' .. Icons.Icon({'Mastery', notext=true}) .. 'Level\n! Modifiers', 2))


  local result = '{| class="wikitable sortable stickyHeader"'
local recipes = Shared.shallowClone(SkillData.Astrology.recipes)
  result = result..'\r\n|- class="headerRow-0"'
table.sort(recipes, function(a, b) return a.level < b.level end)
  result = result..'\r\n!Fish\r\n!Name\r\n!'..Icons.Icon({'Fishing', type='skill', notext=true})..' Level\r\n!Catch Time'
for i, cons in ipairs(recipes) do
  result = result..'\r\n!Experience\r\n!Fish Price\r\n!'..Icons.Icon({'Cooking', type='skill', notext=true})' Level'
-- Generate the list of modifiers first for the purpose of determining the
-- the number of rows required to display all stars
local modTypes = { 'standard', 'unique' }
local modsRaw = {
["standard"] = Skills._buildAstrologyModifierArray(cons, nil, true, false, false, false),
["unique"] = Skills._buildAstrologyModifierArray(cons, nil, false, true, false, false)
}
-- Building the list of standard & unique modifiers
local mods = {}
local modLevels = {}
local maxRows = 1
for j, modType in ipairs(modTypes) do
mods[modType] = {}
modLevels[modType] = {}
for k, modifier in ipairs(modsRaw[modType]) do
local masteryLevel = modifier.group
if mods[modType][masteryLevel] == nil then
mods[modType][masteryLevel] = {}
table.insert(modLevels[modType], masteryLevel)
end
local modMagnitude = type(modifier[2]) == 'table' and {modifier[2]} or modifier[2]
table.insert(mods[modType][masteryLevel], Constants._getModifierText(modifier[1], modMagnitude, false))
end
table.sort(modLevels[modType])
local typeCount = Shared.tableCount(modLevels[modType])
if typeCount > maxRows then
maxRows = typeCount
end
end
local name = cons.name
local rowSpan = (maxRows > 1 and 'rowspan="' .. maxRows .. '"') or ''
table.insert(resultPart, '\n|-')
table.insert(resultPart, '\n|' .. rowSpan .. ' data-sort-value="' .. name .. '" id="'..name..'"| ')
table.insert(resultPart, Icons.Icon({name, type='constellation', size='50', notext=true}))
table.insert(resultPart, '\n|' .. rowSpan .. '| ' .. Icons.getExpansionIcon(cons.id) .. name)
table.insert(resultPart, '\n|' .. rowSpan .. ' style="text-align:right"| ' .. cons.level)
table.insert(resultPart, '\n|' .. rowSpan .. ' style="text-align:right"| ' .. cons.baseExperience)


  for i, fish in Shared.skpairs(data) do
local skillIconArray = {}
    result = result..'\r\n|-'
for j, skillID in ipairs(cons.skillIDs) do
    result = result..'\r\n| style="text-align: left;" | '..Icons.Icon({fish.name, type='item', size='50', notext=true})
table.insert(skillIconArray, Icons.Icon({Constants.getSkillName(skillID), type='skill'}))
    result = result..'\r\n| style ="text-align: left;" |[['..fish.name..']]'
end
    result = result..'\r\n| style="text-align:right"|'..fish.fishingLevel
table.insert(resultPart, '\n|' .. rowSpan .. '| ' .. table.concat(skillIconArray, '<br/>'))


    local timeSortVal = (fish.minFishingInterval + fish.maxFishingInterval) / 2
-- Generate table text for standard & unique modifiers
    local timeStr = string.format("%.1fs-%.1fs", (fish.minFishingInterval/1000), (fish.maxFishingInterval/1000))
for row = 1, maxRows, 1 do
    result = result..'\r\n| style="text-align:right" data-sort-value="'..timeSortVal..'"|'..timeStr
for j, modType in ipairs(modTypes) do
    result = result..'\r\n| style="text-align:right"|'..fish.fishingXP
local masteryLevel = (modLevels[modType] ~= nil and modLevels[modType][row]) or nil
    result = result..'\r\n| style="text-align:right"|'..fish.sellsFor
if masteryLevel ~= nil then
table.insert(resultPart, '\n|style="text-align:right"| ' .. masteryLevel)
table.insert(resultPart, '\n| ' .. table.concat(mods[modType][masteryLevel], '<br/>'))
else
table.insert(resultPart, '\n|colspan="2" class="table-na"| ')
end
end
if row < maxRows then
table.insert(resultPart, '\n|-')
end
end
end
table.insert(resultPart, '\n|}')


    local cookStr = "N/A"
return table.concat(resultPart)
    if fish.cookingLevel ~= nil then
end
      cookStr = fish.cookingLevel
    end
    result = result..'\r\n| style="text-align:right"|'..cookStr
  end


  result = result..'\r\n|}'
function p.buildAstrologyConstellationTable(frame)
  return result
return p._buildAstrologyConstellationTable()
end
end


return p
return p

Latest revision as of 17:32, 26 March 2024

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

function p.getRecipeRequirements(skillName, recipe)
	local reqText = {}
	if recipe.level ~= nil then
		table.insert(reqText, Icons._SkillReq(skillName, recipe.level, false))
	end
	if recipe.totalMasteryRequired ~= nil then
		table.insert(reqText, Shared.formatnum(recipe.totalMasteryRequired) .. ' ' .. Icons.Icon({skillName, type='skill', notext=true}) .. ' ' .. Icons.Icon({'Mastery'}))
	end
	if type(recipe.requirements) == 'table' then
		local reqs = Common.getRequirementString(recipe.requirements)
		if reqs ~= nil then
			table.insert(reqText, reqs)
		end
	end
	return table.concat(reqText, '<br/>')
end

function p.getTreesTable(frame)
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable"')
	table.insert(resultPart, '\n|- class="headerRow-0"')
	table.insert(resultPart, '\n!colspan="2"|Tree!!colspan="2"|Logs!!Requirements')
	table.insert(resultPart, '!!XP!!Cut Time!!XP/s!!GP/s')

	local trees = Shared.shallowClone(SkillData.Woodcutting.trees)
	table.sort(trees, function(a, b) return a.level < b.level end)
	for i, tree in ipairs(trees) do
		local log = Items.getItemByID(tree.productId)
		table.insert(resultPart, '\n|-')
		table.insert(resultPart, '\n|class="table-img" data-sort-value="'..tree.name..'"| '..Icons.Icon({log.name, img=tree.name, type='tree', notext=true, size=50}))
		table.insert(resultPart, '\n|data-sort-value="'..tree.name..'"|'..Icons.getExpansionIcon(tree.id)..tree.name)
		table.insert(resultPart, '\n|class="table-img" data-sort-value="'..log.name..'"| '..Icons.Icon({log.name, type='item', notext=true, size=50}))
		table.insert(resultPart, '\n| '..Icons.Icon({log.name, type='item', noicon=true}))
		table.insert(resultPart, '\n|data-sort-value="' .. tree.level .. '"| ' .. p.getRecipeRequirements(SkillData.Woodcutting.name, tree))
		table.insert(resultPart, '\n|style="text-align:right"| '..tree.baseExperience)
		table.insert(resultPart, '\n|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)
		table.insert(resultPart, '\n|style="text-align:right"| '..Shared.round(XPs, 2, 2))
		table.insert(resultPart, '\n|style="text-align:right" data-sort-value="'..GPs..'"| '..Icons.GP(Shared.round(GPs, 2, 2)))
	end

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

function p.getSpecialFishingTable(frame)
	local totalWt, lootValue = 0, 0
	local itemArray = Shared.shallowClone(SkillData.Fishing.specialItems)
	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.weight > b.weight 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|class="table-img"| ' .. 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|class="table-img"| ' .. 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 resultPart = {}
	table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\n|- class="headerRow-0"')
	table.insert(resultPart, '\n!colspan="2"|Rock!!colspan=2|Ore!!Type!!Requirements')
	table.insert(resultPart, '!!XP!!Respawn Time!!Ore Value')

	local mineData = Shared.shallowClone(SkillData.Mining.rockData)
	table.sort(mineData, function(a, b) return a.level < b.level end)

	for i, oreData in ipairs(mineData) do
		local ore = Items.getItemByID(oreData.productId)
		local respawnStyle, respawnSort, respawnText = 'class="table-na"', 0, 'N/A'
		if oreData.hasPassiveRegen then
			respawnStyle = 'style="text-align:right;'
			respawnSort = oreData.baseRespawnInterval / 1000
			respawnText = Shared.timeString(respawnSort, true)
		end
		local rockName = Icons.Icon({oreData.name, type='rock', noicon=true, nolink=true})
		local qtyText = (oreData.baseQuantity > 1 and '<b>' .. oreData.baseQuantity .. 'x</b> ') or ''
		
		table.insert(resultPart, '\n|-\n|class="table-img" data-sort-value="' .. rockName .. '"| '..Icons.Icon({oreData.name, type='rock', size='50', notext=true, nolink=true}))
		table.insert(resultPart, '\n| data-sort-value="' ..rockName.. '"|'..Icons.getExpansionIcon(oreData.id) .. rockName)
		table.insert(resultPart, '\n|class="table-img" data-sort-value="' .. ore.name .. '"| '..Icons.Icon({ore.name, type='item', size='50', notext=true}))
		table.insert(resultPart, '\n| ' .. qtyText .. Icons.Icon({ore.name, type='item', noicon=true}))
		table.insert(resultPart, '\n| ' .. oreData.type)
		table.insert(resultPart, '\n|data-sort-value="' .. oreData.level ..'"| '..p.getRecipeRequirements(SkillData.Mining.name, oreData))
		table.insert(resultPart, '\n|style="text-align:right"| '..oreData.baseExperience)
		table.insert(resultPart, '\n|' .. respawnStyle .. ' data-sort-value="'..respawnSort..'"| ' .. respawnText)
		table.insert(resultPart, '\n|data-sort-value="'..ore.sellsFor..'"| '..Icons.GP(ore.sellsFor))
	end

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

function p._getMiningGemsTable(gemType)
	if type(gemType) ~= 'string' then
		gemType = 'Normal'
	end
	local validTypes = {
		["Normal"] = 'randomGems',
		["Superior"] = 'randomSuperiorGems'
	}
	local gemDataKey = validTypes[gemType]
	if gemDataKey == nil then
		return Shared.printError('No such gem type "' .. gemType .. '"')
	end
	
	local gemData = GameData.rawData[gemDataKey]
	local totalWeight = 0
	for i, gem in ipairs(gemData) do
		totalWeight = totalWeight + gem.weight
	end
	
	local resultPart = {}
	table.insert(resultPart, '{|class="wikitable sortable"')
	table.insert(resultPart, '\n|- class="headerRow-0"')
	table.insert(resultPart, '\n!colspan=2|Gem!!Gem Chance!!Gem Price')

	for i, gem in ipairs(gemData) do
		local gemItem = Items.getItemByID(gem.itemID)
		local gemPct = gem.weight / totalWeight * 100
		table.insert(resultPart, '\n|-\n|class="table-img"| ')
		table.insert(resultPart, Icons.Icon({gemItem.name, type='item', size='50', notext=true}))
		table.insert(resultPart, '\n| data-sort-value="'..gemItem.name..'"|'..Icons.getExpansionIcon(gemItem.id) .. Icons.Icon({gemItem.name, type='item', noicon=true}))
		table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. gemPct .. '" | ' .. string.format("%.1f%%", gemPct))
		table.insert(resultPart, '\n|data-sort-value="' .. gemItem.sellsFor .. '"| ' .. Icons.GP(gemItem.sellsFor))
	end

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

function p.getMiningGemsTable(frame)
	local gemType = frame.args ~= nil and frame.args[1] or frame
	return p._getMiningGemsTable(gemType)
end

function p.getFishTable(frame)
	local recipeList = Shared.shallowClone(SkillData.Fishing.fish)
	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, '\n|- class="headerRow-0"')
	table.insert(resultPart, '\n!colspan="2" rowspan="2"|Fish\n!rowspan="2"|' .. Icons.Icon({'Fishing', type='skill', notext=true}) .. ' Level')
	table.insert(resultPart, '\n!colspan="3"|Catch Time\n!rowspan="2"|XP\n!rowspan="2"|Value\n!rowspan="2"|XP/s\n!rowspan="2"|GP/s')
	table.insert(resultPart, '\n!rowspan="2"|' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' Level')
	table.insert(resultPart, '\n|- class="headerRow-1"\n!Min\n!Max\n!Avg')
	for i, recipe in ipairs(recipeList) do
		local fish = Items.getItemByID(recipe.productId)
		if fish ~= nil then
			local timeMin, timeMax = recipe.baseMinInterval / 1000, recipe.baseMaxInterval / 1000
			local timeAvg = (timeMin + timeMax) / 2
			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.productId] or 0
			local cookStyle = (cookReq[recipe.productId] ~= nil and 'style="text-align:right" ' or 'class="table-na" ')
			local cookStr = cookReq[recipe.productId] or 'N/A'
			table.insert(resultPart, '\n|-')
			table.insert(resultPart, '\n|class="table-img"| ' .. Icons.Icon({fish.name, type='item', size='50', notext=true}))
			table.insert(resultPart, '\n|data-sort-value="'..fish.name..'"|'..Icons.getExpansionIcon(fish.id) .. Icons.Icon({fish.name, type='item', noicon=true}))
			table.insert(resultPart, '\n|style="text-align:right"| ' .. recipe.level)
			table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. timeMin .. '"| ' .. string.format("%.1fs", timeMin))
			table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. timeMax .. '"| ' .. string.format("%.1fs", timeMax))
			table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. timeAvg .. '"| ' .. string.format("%.1fs", timeAvg))
			table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. recipe.baseExperience .. '"| ' .. Shared.formatnum(recipe.baseExperience))
			table.insert(resultPart, '\n|data-sort-value="' .. fish.sellsFor .. '"| ' .. Icons.GP(fish.sellsFor))
			table.insert(resultPart, '\n|style="text-align:right"| ' .. Shared.round(XPs, 2, 2))
			table.insert(resultPart, '\n|data-sort-value="' .. GPs .. '"|' .. Icons.GP(Shared.round(GPs, 2, 2)))
			table.insert(resultPart, '\n|' .. cookStyle .. 'data-sort-value="' .. cookSortVal .. '"| ' .. cookStr)
		end
	end
	table.insert(resultPart, '\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;" |'..Icons.getExpansionIcon(area.id)..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

function p.getThievingGeneralRareTable(frame)
	return p._getThievingGeneralRareTable()
end

function p._getThievingGeneralRareTable(npcID)
	local rareTxt = '{|class="wikitable sortable"'
	rareTxt = rareTxt..'\r\n!Item!!Qty'
	rareTxt = rareTxt..'!!Price!!colspan="2"|Chance'
	for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
		-- If an npcID has been passed and the item is NPC specific, only display
		-- the item if it may be obtained while pickpocketing that NPC
		if npcID == nil or drop.npcs == nil or Shared.contains(drop.npcs, npcID) then
			local thisItem = Items.getItemByID(drop.itemID)
			local odds = drop.chance
	
			rareTxt = rareTxt..'\r\n|-\r\n|data-sort-value="'..thisItem.name..'"|'..Icons.getExpansionIcon(thisItem.id)..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
	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 = Shared.shallowClone(npc.lootTable)
		--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.itemID)
			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.minQuantity ~= loot.maxQuantity then
				table.insert(normalTxt, Shared.formatnum(loot.minQuantity) .. ' - ' .. 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.maxQuantity then
					table.insert(normalTxt, '||'..Icons.GP(itemPrice))
				else
					table.insert(normalTxt, '||'..Icons.GP(itemPrice * loot.minQuantity, itemPrice * loot.maxQuantity))
				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.minQuantity + loot.maxQuantity) / 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 pickpocket is worth ' .. Icons.GP(Shared.round(lootValue, 2, 2)) .. ' if sold.')
		table.insert(normalTxt, '\r\n\r\nIncluding GP, the average successful pickpocket 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(npc.id)
	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 ipairs(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.baseAreaUniqueChance, 2, 2)..'%'
		dropLines[thisItem.name] = lineTxt
	end
	for i, txt in pairs(dropLines) do
		areaTxt = areaTxt..txt
	end
	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 Shared.printError('Invalid Thieving NPC "' .. npcName .. '"')
	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 = Shared.shallowClone(SkillData.Thieving.npcs)
	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..'||data-sort-value="'..npc.name..'"|'..Icons.getExpansionIcon(npc.id)..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" data-sort-value="' .. npc.perception .. '| '..Shared.formatnum(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.npcIDs ~= nil and not Shared.tableIsEmpty(area.npcIDs) then
			for j, npcID in ipairs(area.npcIDs) 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 Shared.printError('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 tbl = mw.html.create()
	local html = tbl:tag("table")
        :addClass("wikitable sortable stickyHeader")
    	:tag('tr'):addClass("headerRow-0")
        	:tag('th'):attr("colspan", 2):wikitext("Seeds")
        	:tag('th'):wikitext(Icons.Icon({'Farming', type='skill', notext=true}) .. " Level")
        	:tag('th'):wikitext('XP')
        	:tag('th'):wikitext('Growth Time')
        	:tag('th'):wikitext('Seed Value')

	if category.id == 'melvorD:Allotment' then
		html:tag('th'):attr("colspan", 2):wikitext("Crop")
			:tag('th'):wikitext('Crop Healing')
			:tag('th'):wikitext('Crop Value')
	elseif category.id == 'melvorD:Herb' then
		html:tag('th'):attr("colspan", 2):wikitext("Herb")
			:tag('th'):wikitext('Herb Value')
	elseif category.id == 'melvorD:Tree' then
		html:tag('th'):attr("colspan", 2):wikitext("Logs")
			:tag('th'):wikitext('Log Value')
	end
	html = html:tag('th'):wikitext('Seed Sources')

	table.sort(seedList, function(a, b) return a.level < b.level 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
			html = 
			html:tag('tr')
				:tag('td'):wikitext(Icons.Icon({seedItem.name, type='item', size='50', notext=true}))
				:tag('td'):wikitext(Icons.getExpansionIcon(seedItem.id) .. Icons.Icon({seedItem.name, type='item', noicon=true}))
				:tag('td'):wikitext(seed.level)
				:tag('td'):wikitext(Shared.formatnum(seed.baseExperience))
				:tag('td'):attr('data-sort-value', (seed.baseInterval / 1000))
						  :wikitext(Shared.timeString(seed.baseInterval / 1000, true))
				:tag('td'):attr('data-sort-value', seedItem.sellsFor)
					      :wikitext(Icons.GP(seedItem.sellsFor))
				:tag('td'):wikitext(Icons.Icon({productItem.name, type='item', size='50', notext=true}))
				:tag('td'):wikitext(Icons.getExpansionIcon(productItem.id) .. Icons.Icon({productItem.name, type='item', noicon=true}))

			if category.id == 'melvorD:Allotment' then
				html:tag('td'):wikitext(Icons.Icon({'Hitpoints', type='skill', notext=true}))
							  :wikitext(' ')
							  :wikitext(((productItem.healsFor or 0) * 10))
			end
			html =
			html:tag('td'):attr('data-sort-value', productItem.sellsFor)
						  :wikitext(Icons.GP(productItem.sellsFor))
				:tag('td'):wikitext(ItemSourceTables._getItemSources(seedItem))
						  :css('text-align', 'left')
				:done()
		end
	end

	return tostring(tbl:done())
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'})
			result = result..'|| ' .. Icons.getExpansionIcon(product.id) .. Icons.Icon({product.name, type='item', noicon=true})
			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 category = GameData.getEntityByName(SkillData.Farming.categories, areaName)
	if category == nil then
		return Shared.printError('Invalid farming category. Please choose Allotments, Herbs, or Trees')
	end
	local patches = GameData.getEntities(SkillData.Farming.plots,
		function(plot)
			return plot.categoryID == category.id
		end)
	table.sort(patches,
		function(a, b)
			if a.level == b.level then
				return a.id < b.id
			else
				return a.level < b.level
			end
		end)
	if Shared.tableIsEmpty(patches) then
		return ''
	end

	local result = '{|class="wikitable sortable stickyHeader"'
	result = result..'\r\n!Plot!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level!!Cost'

	for i, patch in ipairs(patches) do
		result = result..'\r\n|-\r\n|'..i
		result = result..'||style="text-align:right;" data-sort-value="' .. patch.level .. '"|'..patch.level
		local costText = Common.getCostString({ items = patch.itemCosts, gp = patch.gpCost }, 'Free')
		result = result..'||style="text-align:right;" data-sort-value="'..patch.gpCost..'"|'..costText
	end

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

function p._buildAstrologyConstellationTable()
	local resultPart = {}
	table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\n|- class="headerRow-0"')
	table.insert(resultPart, '\n!rowspan="2" colspan="2"|Constellation!!rowspan="2"| ' .. Icons.Icon({"Astrology", type='skill', notext='true'}) .. ' Level')
	table.insert(resultPart, '!!rowspan="2"| XP!!rowspan="2"| Skills!!colspan="2"| Standard Stars!!colspan="2"| Unique Stars')
	table.insert(resultPart, '\n|- class="headerRow-1"')
	table.insert(resultPart, string.rep('\n! ' .. Icons.Icon({'Mastery', notext=true}) .. 'Level\n! Modifiers', 2))

	local recipes = Shared.shallowClone(SkillData.Astrology.recipes)
	table.sort(recipes, function(a, b) return a.level < b.level end)
	for i, cons in ipairs(recipes) do
		-- Generate the list of modifiers first for the purpose of determining the
		-- the number of rows required to display all stars
		local modTypes = { 'standard', 'unique' }
		local modsRaw = {
			["standard"] = Skills._buildAstrologyModifierArray(cons, nil, true, false, false, false),
			["unique"] = Skills._buildAstrologyModifierArray(cons, nil, false, true, false, false)
		}
		-- Building the list of standard & unique modifiers
		local mods = {}
		local modLevels = {}
		local maxRows = 1
		for j, modType in ipairs(modTypes) do
			mods[modType] = {}
			modLevels[modType] = {}
			for k, modifier in ipairs(modsRaw[modType]) do
				local masteryLevel = modifier.group
				if mods[modType][masteryLevel] == nil then
					mods[modType][masteryLevel] = {}
					table.insert(modLevels[modType], masteryLevel)
				end
				local modMagnitude = type(modifier[2]) == 'table' and {modifier[2]} or modifier[2]
				table.insert(mods[modType][masteryLevel], Constants._getModifierText(modifier[1], modMagnitude, false))
			end
			table.sort(modLevels[modType])
			local typeCount = Shared.tableCount(modLevels[modType])
			if typeCount > maxRows then
				maxRows = typeCount
			end
		end
		
		local name = cons.name
		local rowSpan = (maxRows > 1 and 'rowspan="' .. maxRows .. '"') or ''
		table.insert(resultPart, '\n|-')
		table.insert(resultPart, '\n|' .. rowSpan .. ' data-sort-value="' .. name .. '" id="'..name..'"| ')
		table.insert(resultPart, Icons.Icon({name, type='constellation', size='50', notext=true}))
		table.insert(resultPart, '\n|' .. rowSpan .. '| ' .. Icons.getExpansionIcon(cons.id) .. name)
		table.insert(resultPart, '\n|' .. rowSpan .. ' style="text-align:right"| ' .. cons.level)
		table.insert(resultPart, '\n|' .. rowSpan .. ' style="text-align:right"| ' .. cons.baseExperience)

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

		-- Generate table text for standard & unique modifiers
		for row = 1, maxRows, 1 do
			for j, modType in ipairs(modTypes) do
				local masteryLevel = (modLevels[modType] ~= nil and modLevels[modType][row]) or nil
				if masteryLevel ~= nil then
					table.insert(resultPart, '\n|style="text-align:right"| ' .. masteryLevel)
					table.insert(resultPart, '\n| ' .. table.concat(mods[modType][masteryLevel], '<br/>'))
				else
					table.insert(resultPart, '\n|colspan="2" class="table-na"| ')
				end
			end
			if row < maxRows then
				table.insert(resultPart, '\n|-')
			end
		end
	end
	table.insert(resultPart, '\n|}')

	return table.concat(resultPart)
end

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

return p