Module:Skills/Gathering: Difference between revisions

From Melvor Idle
(fix header for new columns in Fishing table)
No edit summary
 
(103 intermediate revisions by 6 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 Modifiers = require('Module:Modifiers')
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')
local Num = require("Module:Number")


local thievingNormalLootChance = 75
local function lootValueText(lootValue)
local thievingAreaLootChance = 0.2
local returnPart = {}
 
for _, currencyDefn in ipairs(GameData.rawData.currencies) do
function p.getAxeTable(frame)
-- Guarantee order by iterating through currency game data definition
local toolArray = {}
local currID = currencyDefn.id
for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
local val = Num.round(lootValue[currID], 2, 2)
if Shared.contains(upgrade.name, 'Axe') then
if val ~= nil then
table.insert(toolArray, upgrade)
table.insert(returnPart, Icons._Currency(currID, val))
end
end
end
end
return table.concat(returnPart, ', ')
end


local result = '{| class="wikitable"'
function p.getTreesTable(frame)
result = result..'\r\n!colspan="4"| !!colspan="2"|Cut Time Decrease'
local args = frame.args ~= nil and frame.args or frame
result = result..'\r\n|- class="headerRow-0"'
local realmName = args.realm
result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Woodcutting', type='skill', notext=true})..' Level'
local realm = Skills.getRealmFromName(realmName)
result = result..'!!Cost!!This Axe!!Total'
if realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
local skillID = 'Woodcutting'


local total = 0
local html = mw.html.create('table')
:addClass('wikitable sortable stickyHeader')
html:tag('tr')
:addClass("headerRow-0")
:tag('th'):attr('colspan', 2)
  :wikitext('Tree')
:tag('th'):attr('colspan', 2)
  :wikitext('Logs')
:tag('th'):wikitext('Requirements')
:tag('th'):wikitext('[[DLC]]')
:tag('th'):wikitext('XP')
:tag('th'):wikitext('Cut Time')
:tag('th'):wikitext('XP/s')
:tag('th'):wikitext('Price/s')


for i, tool in Shared.skpairs(toolArray) do
local trees = GameData.getEntities(SkillData.Woodcutting.trees,
result = result..'\r\n|-'
function(tree)
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
return Skills.getRecipeRealm(tree) == realm.id
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
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)
table.sort(trees, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
for i, tree in ipairs(trees) do
local level = Skills.getRecipeLevel(skillID, tree)
local baseXP = tree.baseAbyssalExperience or tree.baseExperience
local baseInt = tree.baseInterval
local reqText = Skills.getRecipeRequirementText(SkillData.Woodcutting.name, tree)
local log = Items.getItemByID(tree.productId)
local sellCurrency = log.sellsForCurrency or 'melvorD:GP'
local XPSec = baseXP / (baseInt / 1000)
local currSec = Num.round(log.sellsFor / (baseInt / 1000), 2, 2)


local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
html:tag('tr')
total = total + cutTime
:tag('td'):wikitext(Icons.Icon({log.name, img=tree.name, type='tree', notext=true}))
result = result..'||style="text-align:right"|-'..cutTime..'%'
          :addClass('table-img')
result = result..'||style="text-align:right"|-'..total..'%'
          :attr('data-sort-value', tree.name)
:tag('td'):wikitext(tree.name)
:tag('td'):wikitext(Icons.Icon({log.name, type='item', notext=true}))
  :addClass('table-img')
          :attr('data-sort-value', log.name)
:tag('td'):wikitext('[[' .. log.name .. ']]')
:tag('td'):wikitext(reqText)
          :attr('data-sort-value', level)
:tag('td'):wikitext(Icons.getDLCColumnIcon(tree.id))
  :attr('data-sort-value', Icons.getExpansionID(tree.id))
  :css('text-align', 'center')
:tag('td'):wikitext(Num.formatnum(baseXP))
  :css('text-align', 'right')
:tag('td'):wikitext(Shared.timeString(baseInt / 1000, true))
  :attr('data-sort-value', baseInt)
  :css('text-align', 'right')
:tag('td'):wikitext(Num.round(XPSec, 2, 2))
  :css('text-align', 'right')
:tag('td'):wikitext(Icons._Currency(sellCurrency, currSec))
  :attr('data-sort-value', currSec)
  :css('text-align', 'right')
end
end
 
result = result..'\r\n|}'
return tostring(html)
return result
end
end


function p.getPickaxeTable(frame)
function p.getSpecialFishingTable(frame)
local toolArray = {}
local args = frame.args ~= nil and frame.args or frame
for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
local realmName = args.realm
if Shared.contains(upgrade.name, 'Pickaxe') then
local realm = Skills.getRealmFromName(realmName)
table.insert(toolArray, upgrade)
if realm == nil then
end
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
end


local result = '{| class="wikitable"'
local totalWt, lootValue = 0, {}
result = result..'\r\n!colspan="4"| !!colspan="2"|Mine Time Decrease!!colspan="2"|2x Ore Chance'
local realmSpecials = GameData.getEntityByProperty(SkillData.Fishing.specialItems, 'realmID', realm.id)
result = result..'\r\n|- class="headerRow-0"'
if realmSpecials == nil then
result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Mining', type='skill', notext=true})..' Level'
return ''
result = result..'!!Cost!!This Pick!!Total!!This Pick!!Total'
end
local itemArray = realmSpecials.drops
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 total = 0
local html = mw.html.create('table')
local total2 = 0
html:addClass('wikitable sortable stickyHeader')


for i, tool in Shared.skpairs(toolArray) do
-- Add header row
result = result..'\r\n|-'
local headerRow = html:tag('tr'):addClass('headerRow-0')
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
headerRow:tag('th'):attr('colspan', '2'):wikitext('Item')
result = result..'||'..tool.name
headerRow:tag('th'):wikitext('Value')
local level = 1
headerRow:tag('th'):attr('colspan', '2'):wikitext('Chance')
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]
-- Add item rows
total = total + cutTime
for i, itemDef in ipairs(itemArray) do
local item = Items.getItemByID(itemDef.itemID)
if item ~= nil then
local dropChance = itemDef.weight / totalWt * 100
local currID = item.sellsForCurrency or 'melvorD:GP'
-- 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 row = html:tag('tr')


result = result..'||style="text-align:right"|-'..cutTime..'%'
row:tag('td'):addClass('table-img'):wikitext(Icons.Icon({item.name, type='item', notext=true}))
result = result..'||style="text-align:right"|-'..total..'%'
row:tag('td'):wikitext(Icons.Icon({item.name, type='item', noicon=true}))
row:tag('td')
:attr('data-sort-value', item.sellsFor)
:wikitext(Items.getValueText(item))
row:tag('td')
:css('text-align', 'right')
:attr('data-sort-value', itemDef.weight)
:wikitext(Num.fraction(itemDef.weight, totalWt))
row:tag('td')
:css('text-align', 'right')
:wikitext(string.format(fmt, dropChance) .. '%')


local OreDouble = tool.contains.modifiers.increasedChanceToDoubleOres
if lootValue[currID] == nil then
total2 = total2 + OreDouble
lootValue[currID] = 0
 
end
result = result..'||style="text-align:right"|+'..OreDouble..'%'
lootValue[currID] = lootValue[currID] + (dropChance / 100 * item.sellsFor)
result = result..'||style="text-align:right"|+'..total2..'%'
end
end
end


result = result..'\r\n|}'
local result = tostring(html)
return result
local averageValueText = 'The average value of a roll on the special fishing loot table is ' .. lootValueText(lootValue)
return result .. '\n' .. averageValueText
end
end


function p.getRodTable(frame)
function p.getFishingJunkTable(frame)
local toolArray = {}
local html = mw.html.create('table')
for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
html:addClass('wikitable sortable stickyHeader')
if Shared.contains(upgrade.name, 'Fishing Rod') then
table.insert(toolArray, upgrade)
end
end


local result = '{| class="wikitable"'
-- Add header row
result = result..'\r\n!colspan="4"| !!colspan="2"|Catch Time Decrease'
local headerRow = html:tag('tr'):addClass('headerRow-0')
result = result..'\r\n|- class="headerRow-0"'
headerRow:tag('th'):attr('colspan', '2'):wikitext('Item')
result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Fishing', type='skill', notext=true})..' Level'
headerRow:tag('th'):wikitext('Value')
result = result..'!!Cost!!This Rod!!Total'


local total = 0
local itemArray = {}
 
for i, itemID in ipairs(SkillData.Fishing.junkItemIDs) do
for i, tool in Shared.skpairs(toolArray) do
local item = Items.getItemByID(itemID)
result = result..'\r\n|-'
if item ~= nil then
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(itemArray, item)
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
end
result = result..'||style="text-align:right"|'..level
end
result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)
table.sort(itemArray, function(a, b) return a.name < b.name end)


local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
-- Add item rows
total = total + cutTime
for i, item in ipairs(itemArray) do
result = result..'||style="text-align:right"|-'..cutTime..'%'
local row = html:tag('tr')
result = result..'||style="text-align:right"|-'..total..'%'
row:tag('td'):addClass('table-img')
:wikitext(Icons.Icon({item.name, type='item', notext=true}))
row:tag('td'):wikitext(Icons.Icon({item.name, type='item', noicon=true}))
row:tag('td')
:attr('data-sort-value', item.sellsFor)
:wikitext(Items.getValueText(item))
end
end


result = result..'\r\n|}'
return tostring(html)
return result
end
end


function p.getTreesTable(frame)
function p.getMiningOresTable(frame)
local result = '{| class="wikitable sortable"'
local args = frame.args ~= nil and frame.args or frame
result = result..'\r\n|- class="headerRow-0"'
local realmName = args.realm
result = result..'\r\n!colspan="2"|Tree!!colspan="2"|Logs!!'..Icons.Icon({'Woodcutting', type='skill', notext=true})..' Level'
local realm = Skills.getRealmFromName(realmName)
result = result..'!!XP!!Cut Time!!XP/s!!GP/s'
if realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
local skillID = 'Mining'
local html = mw.html.create('table')
:addClass("wikitable sortable stickyHeader")
html:tag('tr'):addClass("headerRow-0")
:tag('th'):wikitext('Rock')
  :attr('colspan', 2)
:tag('th'):wikitext('Ore')
  :attr('colspan', 2)
:tag('th'):wikitext('Type')
:tag('th'):wikitext('Requirements')
:tag('th'):wikitext('[[DLC]]')
:tag('th'):wikitext('XP')
:tag('th'):wikitext('Respawn<br>Time')
:tag('th'):wikitext('Ore Value')


for i, tree in Shared.skpairs(SkillData.Woodcutting.Trees) do
local mineData = GameData.getEntities(SkillData.Mining.rockData,
result = result..'\r\n|-'
function(obj)
local treeName = Shared.titleCase(tree.type..' tree')
return Skills.getRecipeRealm(obj) == realm.id
local logName = Shared.titleCase(tree.type..' logs')
end
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..'||'..treeName..''
table.sort(mineData, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
result = result..'||style="min-width:25px" data-sort-value="'..logName..'"|'..Icons.Icon({logName, type='item', notext=true, size=50})
for i, oreData in ipairs(mineData) do
result = result..'||'..Icons.Icon({logName, type='item', noicon=true})
local level = Skills.getRecipeLevel(skillID, oreData)
result = result..'||style="text-align:right"|'..tree.level
local baseXP = oreData.baseAbyssalExperience or oreData.baseExperience
result = result..'||style="text-align:right"|'..tree.xp
local reqText = Skills.getRecipeRequirementText(SkillData.Mining.name, oreData)
result = result..'||style="text-align:right" data-sort-value="'..tree.interval..'"|'..Shared.timeString(tree.interval/1000, true)
local ore = Items.getItemByID(oreData.productId)
local XPs = tree.xp / (tree.interval / 1000)
local respawnSort, respawnText = 0, 'N/A'
local Log = Items.getItemByID(i - 1)
if oreData.hasPassiveRegen then
local GPs = Log.sellsFor / (tree.interval / 1000)
respawnSort = oreData.baseRespawnInterval / 1000
result = result..'||style="text-align:right"|'..Shared.round(XPs, 2, 2)
respawnText = Shared.timeString(respawnSort, true)
result = result..'||style="text-align:right" data-sort-value="'..GPs..'"|'..Icons.GP(Shared.round(GPs, 2, 2))
end
local categoryName = ''
local category = GameData.getEntityByID(SkillData.Mining.categories, oreData.category)
if category ~= nil and category.name ~= nil then
categoryName = category.name
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 ''
local row = html:tag('tr')
row :tag('td'):wikitext(Icons.Icon({oreData.name, type='rock', notext=true, nolink=true}))
  :addClass('table-img')
  :attr('data-sort-value', rockName)
:tag('td'):wikitext(rockName)
:tag('td'):wikitext(Icons.Icon({ore.name, type='item', notext=true}))
  :addClass('table-img')
  :attr('data-sort-value', ore.name)
:tag('td'):wikitext(qtyText .. '[[' .. ore.name .. ']]')
:tag('td'):wikitext(categoryName)
:tag('td'):wikitext(reqText)
  :attr('data-sort-value', level)
:tag('td'):wikitext(Icons.getDLCColumnIcon(oreData.id))
  :attr('data-sort-value', Icons.getExpansionID(oreData.id))
  :css('text-align', 'center')
:tag('td'):wikitext(Num.formatnum(baseXP))
  :css('text-align', 'right')
local respawn =  
row:tag('td'):wikitext(respawnText)
:attr('data-sort-value', respawnSort)
row:tag('td'):wikitext(Items.getValueText(ore))
:attr('data-sort-value', ore.sellsFor)
 
if respawnText == 'N/A' then
respawn:addClass('table-na')
else
respawn:css('text-align', 'right')
end
end
end


result = result..'\r\n|}'
return tostring(html)
return result
end
end


function p.getSpecialFishingTable(frame)
function p._getMiningGemsTable(gemType)
local lootValue = 0
if type(gemType) ~= 'string' then
local totalWt = Items.specialFishWt
gemType = 'Normal'
end
local validTypes = {
["Normal"] = 'randomGems',
["Superior"] = 'randomSuperiorGems',
["Abyssal"] = 'randomAbyssalGems'
}
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 html = mw.html.create('table')
:addClass('wikitable sortable')


local result = ''
-- Add header row
result = result..'\r\n{|class="wikitable sortable stickyHeader"'
local headerRow = html:tag('tr'):addClass('headerRow-0')
result = result..'\r\n|- class="headerRow-0"'
:tag('th'):wikitext('Gem')
result = result..'\r\n!Item'
  :attr('colspan', '2')
result = result..'!!Price!!colspan="2"|Chance'
:tag('th'):wikitext('[[DLC]]')
:tag('th'):wikitext('Gem Chance')
:tag('th'):wikitext('Gem Price')


--Sort the loot table by weight in descending order
-- Add gem rows
table.sort(Items.specialFishLoot, function(a, b) return a[2] > b[2] end)
for i, gem in ipairs(gemData) do
for i, row in pairs(Items.specialFishLoot) do
local gemItem = Items.getItemByID(gem.itemID)
local thisItem = Items.getItemByID(row[1])
local gemPct = gem.weight / totalWeight * 100
result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
local row = html:tag('tr')
result = result..'||style="text-align:left" data-sort-value="'..thisItem.sellsFor..'"'
result = result..'|'..Icons.GP(thisItem.sellsFor)


local dropChance = (row[2] / totalWt) * 100
row:tag('td'):addClass('table-img')
result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
:wikitext(Icons.Icon({gemItem.name, type='item', notext=true}))
result = result..'|'..Shared.fraction(row[2], totalWt)
row:tag('td'):attr('data-sort-value', gemItem.name)
result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
:wikitext('[[' .. gemItem.name ..']]')
lootValue = lootValue + (dropChance * 0.01 * thisItem.sellsFor)
row:tag('td'):wikitext(Icons.getDLCColumnIcon(gemItem.id))
:attr('data-sort-value', Icons.getExpansionID(gemItem.id))
:css('text-align', 'center')
row:tag('td'):css('text-align', 'right')
:attr('data-sort-value', gemPct)
:wikitext(string.format("%.1f%%", gemPct))
row:tag('td'):attr('data-sort-value', gemItem.sellsFor)
    :wikitext(Items.getValueText(gemItem))
end
end
result = result..'\r\n|}'
result = result..'\r\nThe average value of a roll on the special fishing loot table is '..Icons.GP(Shared.round(lootValue, 2, 0))


return result
return tostring(html)
end
end


function p.getFishingJunkTable(frame)
function p.getMiningGemsTable(frame)
local result = '{| class="wikitable sortable stickyHeader"'
local gemType = frame.args ~= nil and frame.args[1] or frame
result = result..'\r\n|- class="headerRow-0"'
return p._getMiningGemsTable(gemType)
result = result..'\r\n!colspan="2"|Item!!Value'
end
 
local itemArray = Items.getItems(function(item) return item.type == "Junk" end)
function p.getFishTable(frame)
local args = frame.args ~= nil and frame.args or frame
local realmName = args.realm
local realm = Skills.getRealmFromName(realmName)
if realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
local skillID = 'Fishing'


table.sort(itemArray, function(a, b) return a.name < b.name end)
local recipeList = GameData.getEntities(SkillData.Fishing.fish, function(obj)
return Skills.getRecipeRealm(obj) == realm.id
end)
table.sort(recipeList, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)


for i, item in Shared.skpairs(itemArray) do
-- Determine cooking levels for all fish
result = result..'\r\n|-'
local cookRecipes = {}
result = result..'\r\n|style="min-width:25px"|'..Icons.Icon({item.name, type='item', notext=true, size='50'})
for i, recipe in ipairs(SkillData.Cooking.recipes) do
result = result..'||'..Icons.Icon({item.name, type='item', noicon=true})
-- This assumes that each raw fish only appears in a single recipe, which is a bit rubbish
result = result..'||style="text-align:right;" data-sort-value="'..item.sellsFor..'"|'..Icons.GP(item.sellsFor)
-- but currently holds
for j, mat in ipairs(recipe.itemCosts) do
if cookRecipes[mat.id] == nil then
cookRecipes[mat.id] = recipe
end
end
end
end


result = result..'\r\n|}'
local html = mw.html.create('table')
 
html:addClass('wikitable sortable stickyHeader')
return result
end


function p.getMiningOresTable(frame)
-- Add header row
local result = '{|class="wikitable sortable stickyHeader"'
local headerRow0 = html:tag('tr'):addClass('headerRow-0')
result = result..'\r\n|- class="headerRow-0"'
headerRow0:tag('th'):attr('colspan', '2'):attr('rowspan', '2'):wikitext('Fish')
result = result..'\r\n!colspan=2|Ore!!'..Icons.Icon({'Mining', type='skill', notext=true})..' Level'
headerRow0:tag('th'):attr('rowspan', '2'):wikitext(Icons._SkillRealmIcon('Fishing', realm.id) .. '<br>Level')
result = result..'!!XP!!Respawn Time!!Ore Value'
headerRow0:tag('th'):attr('rowspan', '2'):wikitext('[[DLC]]')
headerRow0:tag('th'):attr('colspan', '3'):wikitext('Catch Time')
headerRow0:tag('th'):attr('rowspan', '2'):wikitext('XP')
headerRow0:tag('th'):attr('rowspan', '2'):wikitext('XP/s')
headerRow0:tag('th'):attr('rowspan', '2'):wikitext('Value')
headerRow0:tag('th'):attr('rowspan', '2'):wikitext('GP/s')


local mineData = Shared.clone(SkillData.Mining.Rocks)
local headerRow1 = html:tag('tr'):addClass('headerRow-1')
headerRow1:tag('th'):wikitext('Min')
headerRow1:tag('th'):wikitext('Max')
headerRow1:tag('th'):wikitext('Avg')


table.sort(mineData, function(a, b) return a.level < b.level end)
-- Add fish rows
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 level = Skills.getRecipeLevel(skillID, recipe)
local reqText = Skills.getRecipeRequirementText(skillID, recipe)
local baseXP = recipe.baseAbyssalExperience or recipe.baseExperience
local XPSec = baseXP / timeSortVal
local sellCurrency = fish.sellsForCurrency or 'melvorD:GP'
local GPSec = fish.sellsFor / timeSortVal


for i, oreData in Shared.skpairs(mineData) do
local row = html:tag('tr')
local ore = Items.getItemByID(oreData.ore)
row:tag('td'):wikitext(Icons.Icon({fish.name, type='item', notext=true}))
result = result..'\r\n|-\r\n|style="min-width:25px"|'..Icons.Icon({ore.name, type='item', size='50', notext=true})
:addClass('table-img')
result = result..'||'..Icons.Icon({ore.name, type='item', noicon=true})
row:tag('td'):wikitext('[[' .. fish.name .. ']]')
result = result..'||style="text-align:right"|'..oreData.level..'||style="text-align:right"|'..ore.miningXP
:attr('data-sort-value', fish.name)
result = result..'||style="text-align:right" data-sort-value="'..oreData.respawnInterval..'"|'
row:tag('td'):wikitext(level)
result = result..Shared.timeString(oreData.respawnInterval / 1000, true)
:css('text-align', 'center')
result = result..'||data-sort-value="'..ore.sellsFor..'"|'..Icons.GP(ore.sellsFor)
:attr('data-sort-value', level)
row:tag('td'):wikitext(Icons.getDLCColumnIcon(fish.id))
:css('text-align', 'center')
:attr('data-sort-value', Icons.getExpansionID(fish.id))
row:tag('td'):wikitext(string.format("%.1fs", timeMin))
:css('text-align', 'right')
:attr('data-sort-value', timeMin)
row:tag('td'):wikitext(string.format("%.1fs", timeMax))
:css('text-align', 'right')
:attr('data-sort-value', timeMax)
row:tag('td'):wikitext(string.format("%.1fs", timeAvg))
:css('text-align', 'right')
:attr('data-sort-value', timeAvg)
row:tag('td'):wikitext(Num.formatnum(baseXP))
:css('text-align', 'right')
:attr('data-sort-value', baseXP)
row:tag('td'):wikitext(Num.round(XPSec, 2, 2))
:css('text-align', 'right')
row:tag('td'):wikitext(Items.getValueText(fish))
:attr('data-sort-value', fish.sellsFor)
:css('text-align', 'right')
row:tag('td'):wikitext(Icons._Currency(sellCurrency, Num.round(GPSec, 2, 2)))
:attr('data-sort-value', GPSec)
:css('text-align', 'right')
end
end
end


result = result..'\r\n|}'
return tostring(html)
return result
end
end


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'


-- Sort gems by ID order
function p.getFishingAreasTable(frame)
for i, gemData in Shared.spairs(Items.GemTable, function(t,a,b) return t[a].id < t[b].id end) do
local args = frame.args ~= nil and frame.args or frame
local gem = Items.getItemByID(gemData.id)
local realmName = args.realm
result = result..'\r\n|-\r\n|style="min-width:25px"|'
local realm = Skills.getRealmFromName(realmName)
result = result..Icons.Icon({gem.name, type='item', size='50', notext=true})
if realm == nil then
result = result..'||'..Icons.Icon({gem.name, type='item', noicon=true})
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
result = result..'||style="text-align:right"|'..string.format("%.1f%%", gemData.chance)
result = result..'||data-sort-value="'..gem.sellsFor..'"|'..Icons.GP(gem.sellsFor)
end
end
local skillID = 'Fishing'


result = result..'\r\n|}'
local html = mw.html.create('table')
return result
html:addClass('wikitable sortable stickyHeader')
end


function p.getFishTable(frame)
-- Add header row
local data = Items.getItems(function(item) return item.fishingID ~= nil end)
local headerRow = html:tag('tr'):addClass('headerRow-0')
headerRow:tag('th'):wikitext('Name')
headerRow:tag('th'):wikitext('Fish')
headerRow:tag('th'):wikitext('[[DLC]]')
headerRow:tag('th'):wikitext('Fish Chance')
headerRow:tag('th'):wikitext('Junk Chance')
headerRow:tag('th'):wikitext('Special Chance')


table.sort(data, function(a, b) return a.fishingID < b.fishingID end)
-- Get fishing areas
local fishAreas = GameData.getEntities(SkillData.Fishing.areas, function(obj)
return Skills.getRecipeRealm(obj) == realm.id
end)


local result = '{| class="wikitable sortable stickyHeader"'
-- Add rows for each fishing area
result = result..'\r\n|- class="headerRow-0"'
for i, area in ipairs(fishAreas) do
result = result..'\r\n!Fish\r\n!Name\r\n!'..Icons.Icon({'Fishing', type='skill', notext=true})..' Level\r\n!Catch Time'
local row = html:tag('tr')
result = result..'\r\n!Experience\r\n!Fish Price\r\n!XP/s\r\n!GP/s\r\n!'
row:tag('td')
result = result..Icons.Icon({'Cooking', type='skill', notext=true})..' Level'
:css('text-align', 'left')
:wikitext(area.name)


for i, fish in Shared.skpairs(data) do
local fishArray = {}
result = result..'\r\n|-'
for j, fishID in ipairs(area.fishIDs) do
result = result..'\r\n| style="text-align: left;" | '..Icons.Icon({fish.name, type='item', size='50', notext=true})
local fishItem = Items.getItemByID(fishID)
result = result..'\r\n| style="text-align: left;" | '..Icons.Icon({fish.name, type='item', noicon=true})
if fishItem ~= nil then
result = result..'\r\n| style="text-align:right"|'..fish.fishingLevel
table.insert(fishArray, Icons.Icon({fishItem.name, type='item'}))
 
end
local timeSortVal = (fish.minFishingInterval + fish.maxFishingInterval) / 2
local timeStr = string.format("%.1fs-%.1fs", (fish.minFishingInterval/1000), (fish.maxFishingInterval/1000))
result = result..'\r\n| style="text-align:right" data-sort-value="'..timeSortVal..'"|'..timeStr
result = result..'\r\n| style="text-align:right"|'..fish.fishingXP
result = result..'\r\n| style="text-align:right"|'..fish.sellsFor
local XPs = fish.fishingXP / (timeSortVal / 1000)
local GPs = fish.sellsFor / (timeSortVal / 1000)
result = result..'\r\n| style="text-align:right"|'..Shared.round(XPs, 2, 2)
result = result..'\r\n| style="text-align:right" data-sort-value="'..GPs..'"|'..Icons.GP(Shared.round(GPs, 2, 2))
 
local cookStr = "N/A"
if fish.cookingLevel ~= nil then
cookStr = fish.cookingLevel
end
end
result = result..'\r\n| style="text-align:right"|'..cookStr
row:tag('td')
:wikitext(table.concat(fishArray, '<br/>'))
row:tag('td')
:css('text-align', 'center')
:wikitext(Icons.getDLCColumnIcon(area.id))
:attr('data-sort-value', Icons.getExpansionID(area.id))
row:tag('td')
:css('text-align', 'right')
:wikitext(area.fishChance .. '%')
row:tag('td')
:css('text-align', 'right')
:wikitext(area.junkChance .. '%')
row:tag('td')
:css('text-align', 'right')
:wikitext(area.specialChance .. '%')
end
end


result = result..'\r\n|}'
return tostring(html)
return result
end
end


function p.getFishingAreasTable(frame)


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


for i, area in Shared.skpairs(SkillData.Fishing.Areas) do
function p._getThievingGeneralRareTable(npc)
result = result..'\r\n|-'
local npcRealm = nil
result = result..'\r\n| style ="text-align: left;" |'..area.name
if npc ~= nil then
npcRealm = Skills.getRecipeRealm(npc)
end


local fishArray = {}
local html = mw.html.create('table')
for j, fish in Shared.skpairs(area.fish) do
html:addClass('wikitable sortable')
local fishTable = Items.getItems(function(item) return item.fishingID == fish end)
local fishItem = fishTable[0] or fishTable[1]
table.insert(fishArray, Icons.Icon({fishItem.name, type='item'}))
end
result = result..'\r\n|'..table.concat(fishArray, '<br />')


result = result..'\r\n| style="text-align:right"|'..area.fishChance..'%'
-- Add header row
result = result..'\r\n| style="text-align:right"|'..area.junkChance..'%'
local headerRow = html:tag('tr')
result = result..'\r\n| style="text-align:right"|'..area.specialChance..'%'
headerRow:tag('th'):wikitext('Item')
end
headerRow:tag('th'):wikitext('[[DLC]]')
headerRow:tag('th'):wikitext('Qty')
headerRow:tag('th'):wikitext('Price')
headerRow:tag('th'):attr('colspan', '2'):wikitext('Chance')


result = result..'\r\n|}'
-- Add rows for each rare item
return result
for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
end
-- 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
local npcMatch = (npc == nil or drop.npcs == nil or Shared.contains(drop.npcs, npc.id))
local realmMatch = (npcRealm == nil or drop.realms == nil or Shared.contains(drop.realms, npcRealm))


function p.getThievingNPC(npcName)
if npcMatch and realmMatch then
local result = nil
local thisItem = Items.getItemByID(drop.itemID)
for i, npc in Shared.skpairs(SkillData.Thieving.NPCs) do
local odds = drop.chance
if npc.name == npcName then
result = Shared.clone(npc)
break
end
end
return result
end


function p.getThievingNPCArea(npc)
local row = html:tag('tr')
if type(npc) == 'string' then
row:tag('td')
npc = p.getThievingNPC(npc)
:attr('data-sort-value', thisItem.name)
end
:wikitext(Icons.Icon({thisItem.name, type='item'}))
row:tag('td')
local result = nil
:wikitext(Icons.getDLCColumnIcon(thisItem.id))
for i, area in Shared.skpairs(SkillData.Thieving.Areas) do
:attr('data-sort-value', Icons.getExpansionID(thisItem.id))
for j, npcID in pairs(area.npcs) do
:css('text-align', 'center')
if npcID == npc.id then
row:tag('td')
result = area
:wikitext('1')
break
row:tag('td')
end
:attr('data-sort-value', thisItem.sellsFor)
:wikitext(Items.getValueText(thisItem))
row:tag('td')
:css('text-align', 'right')
:attr('data-sort-value', odds)
:wikitext(Num.fraction(1, Num.round2(1/(odds/100), 0)))
row:tag('td')
:css('text-align', 'right')
:attr('data-sort-value', odds)
:wikitext(Num.round(odds, 4, 4) .. '%')
end
end
end
end
return result
end


function p._getThievingNPCStat(npc, statName)
return tostring(html)
local result = nil
if statName == 'level' then
result = Icons._SkillReq('Thieving', npc.level)
elseif statName == 'maxHit' then
result = npc.maxHit * 10
elseif statName == 'area' then
local area = p.getThievingNPCArea(npc)
result = area.name
else
result = npc[statName]
end
if result == nil then
result = ''
end
return result
end
end


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


function p.getThievingGeneralRareTable(frame)
function p._getThievingNPCCurrencyText(npc)
local rareTxt = '{|class="wikitable sortable"'
local currTextPart = {}
rareTxt = rareTxt..'\r\n!Item!!Qty'
for _, currencyDrop in ipairs(npc.currencyDrops) do
rareTxt = rareTxt..'!!Price!!colspan="2"|Chance'
table.insert(currTextPart, Icons._Currency(currencyDrop.id, 1, currencyDrop.quantity))
for i, drop in pairs(SkillData.Thieving.RareItems) 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
end
rareTxt = rareTxt..'\r\n|}'
return table.concat(currTextPart, ', ')
return rareTxt
end
end


Line 403: Line 564:
local result = ''
local result = ''
local sectionTxt = {}
local sectionTxt = {}
 
--Five sections here: GP, normal loot, area loot, rare loot, and unique item
--Five sections here: Currency, normal loot, area loot, rare loot, and unique item
--First up, GP:
--First up, currency:
local gpTxt = 'Successfully pickpocketing the '..npc.name..' will always give '..Icons.GP(1, npc.maxGP)
table.insert(sectionTxt, 'Successfully pickpocketing the ' .. npc.name .. ' will always give '.. p._getThievingNPCCurrencyText(npc))
table.insert(sectionTxt, gpTxt)
 
--Next up, normal loot:
--Next up, normal loot:
--(Skip if no loot)
--(Skip if no loot)
if npc.lootTable ~= nil and Shared.tableCount(npc.lootTable) > 0 then
if npc.lootTable ~= nil and Shared.tableCount(npc.lootTable) > 0 then
local normalTxt = '===Possible Common Drops:===\r\nUp to one of these will be received on a successful pickpocket:'
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 totalWt = 0
local lootChance = thievingNormalLootChance
local lootChance = SkillData.Thieving.itemChance
local lootValue = 0
local lootValue = {}
 
--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
 
normalTxt = normalTxt..'\r\n{|class="wikitable sortable"'
table.insert(normalTxt, '\r\n{|class="wikitable sortable"')
normalTxt = normalTxt..'\r\n!Item!!Qty'
table.insert(normalTxt, '\r\n!Item!!Qty')
normalTxt = normalTxt..'!!Price!!colspan="2"|Chance'
table.insert(normalTxt, '!!Price!!colspan="2"|Chance')
 
local lootTable = Shared.shallowClone(npc.lootTable)
--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.itemID)
local maxQty = row[3]
if thisItem ~= nil then
if thisItem ~= nil then
normalTxt = normalTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
table.insert(normalTxt, '\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'}))
else
else
normalTxt = 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
normalTxt = 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.minQuantity ~= loot.maxQuantity then
normalTxt = normalTxt.. '1 - '
table.insert(normalTxt, Num.formatnum(loot.minQuantity) .. ' - ' .. Num.formatnum(loot.maxQuantity))
else
table.insert(normalTxt, Num.formatnum(loot.maxQuantity))
end
end
normalTxt = normalTxt..Shared.formatnum(row[3])
 
--Adding price columns
--Adding price columns
local itemPrice = 0
local sellAmount, sellCurrency = nil, nil
if thisItem == nil then
if thisItem == nil then
normalTxt = normalTxt..'||data-sort-value="0"|???'
table.insert(normalTxt, '||data-sort-value="0"|???')
else
else
itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
sellAmount = thisItem.sellsFor or 0
if itemPrice == 0 or maxQty == 1 then
sellCurrency = thisItem.sellsForCurrency or 'melvorD:GP'
normalTxt = normalTxt..'||'..Icons.GP(itemPrice)
table.insert(normalTxt, '||' .. Items.getValueText(thisItem, loot.minQuantity, loot.maxQuantity))
else
normalTxt = normalTxt..'||'..Icons.GP(itemPrice, itemPrice * maxQty)
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
normalTxt = normalTxt..'||style="text-align:right" data-sort-value="'..row[2]..'"'
table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..loot.weight..'"')
normalTxt = normalTxt..'|'..Shared.fraction(row[2] * lootChance, totalWt * 100)
table.insert(normalTxt, '|'..Num.fraction(loot.weight * lootChance, totalWt * 100))
normalTxt = normalTxt..'||'
table.insert(normalTxt, '||')
else
else
normalTxt = normalTxt..'||colspan="2" data-sort-value="'..row[2]..'"'
table.insert(normalTxt, '||colspan="2" data-sort-value="'..loot.weight..'"')
end
end
normalTxt = normalTxt..'style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
table.insert(normalTxt, 'style="text-align:right"|'..Num.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))
if sellAmount ~= nil and sellCurrency ~= nil then
if lootValue[sellCurrency] == nil then
lootValue[sellCurrency] = 0
end
lootValue[sellCurrency] = lootValue[sellCurrency] + (dropChance * 0.01 * sellAmount * (loot.minQuantity + loot.maxQuantity) / 2)
end
end
end
if multiDrop then
if Shared.tableCount(npc.lootTable) > 1 then
normalTxt = normalTxt..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
table.insert(normalTxt, '\r\n|-class="sortbottom" \r\n!colspan="3"|Total:')
if lootChance < 100 then
if lootChance < 100 then
normalTxt = normalTxt..'\r\n|style="text-align:right"|'..Shared.fraction(lootChance, 100)..'||'
table.insert(normalTxt, '\r\n|style="text-align:right"|'..Num.fraction(lootChance, 100)..'||')
else
else
normalTxt = normalTxt..'\r\n|colspan="2" '
table.insert(normalTxt, '\r\n|colspan="2" ')
end
end
normalTxt = normalTxt..'style="text-align:right"|'..lootChance..'.00%'
table.insert(normalTxt, 'style="text-align:right"|'..Num.round(lootChance, 2, 2)..'%')
end
table.insert(normalTxt, '\r\n|}')
 
table.insert(normalTxt, '\r\nThe loot obtained from the average successful pickpocket is worth ' .. lootValueText(lootValue) .. ' if sold.')
 
-- Amend lootValue
for _, currencyDrop in ipairs(npc.currencyDrops) do
lootValue[currencyDrop.id] = lootValue[currencyDrop.id] + (1 + currencyDrop.quantity) / 2
end
end
normalTxt = normalTxt..'\r\n|}'
 
table.insert(sectionTxt, normalTxt)
table.insert(normalTxt, '\r\n\r\nIncluding currency, the average successful pickpocket is worth ' .. lootValueText(lootValue) .. '.')
table.insert(sectionTxt, table.concat(normalTxt))
end
end
 
--After normal drops, add in rare drops
--After normal drops, add in rare drops
local rareTxt = '===Possible Rare Drops:===\r\nAny of these can be received after a successful pickpocket'
local rareTxt = '===Possible Rare Drops:===\r\nAny of these can be received after a successful pickpocket:'
rareTxt = rareTxt..'\r\n'..p.getThievingGeneralRareTable()
rareTxt = rareTxt..'\r\n'..p._getThievingGeneralRareTable(npc)
table.insert(sectionTxt, rareTxt)
table.insert(sectionTxt, rareTxt)
 
local areaTxt = '===Possible Area Unique Drops==='
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\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..'\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.'
areaTxt = areaTxt..'The chance of receiving an Area Unique drop is tripled if the 95% Thieving Mastery Pool checkpoint is active.'
 
local area = p.getThievingNPCArea(npc)
local area = Skills.getThievingNPCArea(npc)
areaTxt = areaTxt..'\r\n{|class="wikitable sortable"'
areaTxt = areaTxt..'\r\n{|class="wikitable sortable"'
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 ipairs(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..'||'..drop.qty..'||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
lineTxt = lineTxt..'||data-sort-value="'..drop.quantity..'"| '..Num.formatnum(drop.quantity)..'||data-sort-value="'..thisItem.sellsFor..'"|'..Items.getValueText(thisItem)
lineTxt = lineTxt..'||style="text-align:right"|'..Shared.fraction(1, 1/(thievingAreaLootChance/100))
lineTxt = lineTxt..'||style="text-align:right"|'..Num.fraction(1, 1/(SkillData.Thieving.baseAreaUniqueChance/100))
lineTxt = lineTxt..'||'..Shared.round(thievingAreaLootChance, 2, 2)..'%'
lineTxt = lineTxt..'||'..Num.round(SkillData.Thieving.baseAreaUniqueChance, 2, 2)..'%'
dropLines[thisItem.name] = lineTxt
dropLines[thisItem.name] = lineTxt
end
end
for i, txt in Shared.skpairs(dropLines) do
for i, txt in pairs(dropLines) do
areaTxt = areaTxt..txt
areaTxt = areaTxt..txt
end
end
areaTxt = areaTxt..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
areaTxt = areaTxt..'\r\n|style="text-align:right"|'..Shared.fraction(1, 1/(thievingAreaLootChance/100))..'||'
areaTxt = areaTxt..'style="text-align:right"|'..Shared.round(thievingAreaLootChance, 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
 
return table.concat(sectionTxt, '\r\n')
return table.concat(sectionTxt, '\r\n')
end
end
Line 538: Line 708:
function p.getThievingNPCLootTables(frame)
function p.getThievingNPCLootTables(frame)
local npcName = frame.args ~= nil and frame.args[1] or frame
local npcName = frame.args ~= nil and frame.args[1] or frame
local npc = p.getThievingNPC(npcName)
local npc = Skills.getThievingNPC(npcName)
if npc == nil then
if npc == nil then
return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
return Shared.printError('Invalid Thieving NPC "' .. npcName .. '"')
end
end
 
return p._getThievingNPCLootTables(npc)
return p._getThievingNPCLootTables(npc)
end
end


function p.getThievingNPCTable()
function p.getThievingNPCTable(frame)
local result = '{| class="wikitable sortable stickyHeader"'
    local args = frame.args or frame:getParent().args
result = result..'\r\n|- class="headerRow-0"'
    local realmName = args.realm
result = result..'\r\n!colspan="2"|Name!!Area!!'..Icons.Icon({'Thieving', type='skill', notext=true})..' Level!!Experience!!Max Hit!!Perception!!Unique Drop'
    local realm = Skills.getRealmFromName(realmName)
local npcArray = Shared.clone(SkillData.Thieving.NPCs)
   
table.sort(npcArray, function(a, b) return a.level < b.level end)
    if realm == nil then
for i, npc in Shared.skpairs(npcArray) do
        return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
result = result..'\r\n|-'
    end
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 skillID = 'Thieving'
    local root = mw.html.create('table')
        :addClass('wikitable sortable stickyHeader')
   
    -- Header row
    local headerRow = root:tag('tr')
        :addClass('headerRow-0')
        :tag('th'):attr('colspan', '2'):wikitext('Name')
        :tag('th'):wikitext('Area')
        :tag('th'):wikitext(Icons.Icon({'Thieving', type='skill', notext=true}) .. '<br>Level')
        :tag('th'):wikitext('[[DLC]]')
        :tag('th'):wikitext('Experience')
        :tag('th'):wikitext('Max Hit')
        :tag('th'):wikitext('Perception')
        :tag('th'):wikitext('Currency')
        :tag('th'):wikitext('Unique Drop')
   
    local npcArray = GameData.getEntities(SkillData.Thieving.npcs,
        function(obj)
            return Skills.getRecipeRealm(obj) == realm.id
        end
    )
    table.sort(npcArray, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
   
    for i, npc in ipairs(npcArray) do
        local level = Skills.getRecipeLevel(skillID, npc)
        local baseXP = npc.baseAbyssalExperience or npc.baseExperience
        local area = Skills.getThievingNPCArea(npc)
        local currSortAmt = npc.currencyDrops[1].quantity
       
        local row = root:tag('tr')
        row:tag('td'):wikitext(Icons.Icon({npc.name, type='thieving', notext=true}))
        row:tag('td'):attr('data-sort-value', npc.name)
            :wikitext('[[' .. npc.name .. ']]')
        row:tag('td'):wikitext(area.name)
        row:tag('td'):wikitext(level)
        :css('text-align', 'center')
        row:tag('td'):wikitext(Icons.getDLCColumnIcon(npc.id))
        :css('text-align', 'center')
        :attr('data-sort-value', Icons.getExpansionID(npc.id))
        row:tag('td'):css('text-align', 'right')
            :wikitext(Num.formatnum(baseXP))
        row:tag('td'):css('text-align', 'right')
            :wikitext(Num.formatnum(npc.maxHit * 10))
        row:tag('td'):css('text-align', 'right')
            :attr('data-sort-value', npc.perception)
            :wikitext(Num.formatnum(npc.perception))
        row:tag('td'):attr('data-sort-value', currSortAmt)
            :wikitext(p._getThievingNPCCurrencyText(npc))
       
        if npc.uniqueDrop ~= nil then
            local uniqueDrop = Items.getItemByID(npc.uniqueDrop.id)
            if npc.uniqueDrop.quantity > 1 then
                row:tag('td'):attr('data-sort-value', uniqueDrop.name)
                    :wikitext(Icons.Icon({uniqueDrop.name, type='item', qty=npc.uniqueDrop.quantity}))
            else
                row:tag('td'):attr('data-sort-value', uniqueDrop.name)
                    :wikitext(Icons.Icon({uniqueDrop.name, type='item'}))
            end
        else
            row:tag('td'):wikitext(' ')
        end
    end
   
    return tostring(root)
end
 
function p.getThievingAreaTable(frame)
    local args = frame.args or frame:getParent().args
    local realmName = args.realm
    local realm = Skills.getRealmFromName(realmName)
   
    if realm == nil then
        return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
    end
   
    local skillID = 'Thieving'
    local root = mw.html.create('table')
        :addClass('wikitable sortable stickyHeader')
   
    -- Header row
    local headerRow = root:tag('tr')
        :addClass('headerRow-0')
    headerRow:tag('th'):wikitext('Area')
    headerRow:tag('th'):wikitext(Icons.Icon({'Thieving', type='skill', notext=true}) .. '<br>Level')
    headerRow:tag('th'):wikitext('NPCs')
    headerRow:tag('th'):wikitext('Unique Drops')
   
    local areas = GameData.getEntities(SkillData.Thieving.areas,
        function(obj)
            return Skills.getRecipeRealm(obj) == realm.id
        end
    )
   
    for i, area in ipairs(areas) do
        local minLevel, npcList, areaItemList = nil, {}, {}
       
        -- Build NPC list & determine minimum Thieving level
        if area.npcIDs and not Shared.tableIsEmpty(area.npcIDs) then
            for j, npcID in ipairs(area.npcIDs) do
                local npc = Skills.getThievingNPCByID(npcID)
                local level = Skills.getRecipeLevel(skillID, npc)
                if not minLevel or level < minLevel then
                    minLevel = 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 and Shared.tableCount(area.uniqueDrops) > 0 then
            for k, drop in ipairs(area.uniqueDrops) do
                local areaItem = Items.getItemByID(drop.id)
                if areaItem then
                    local iconDef = {areaItem.name, type='item'}
                    if drop.quantity > 1 then
                        iconDef.qty = drop.quantity
                    end
                    table.insert(areaItemList, Icons.Icon(iconDef))
                else
                    table.insert(areaItemList, 'Unknown[[Category:Pages with script errors]]')
                end
            end
        else
            table.insert(areaItemList, '')
        end
       
        -- Generate table row
        local row = root:tag('tr')
        row:tag('td'):wikitext(area.name)
        row:tag('td'):wikitext(minLevel)
        :css('text-align', 'center')
        row:tag('td'):wikitext(table.concat(npcList, '<br/>'))
        row:tag('td'):wikitext(table.concat(areaItemList, '<br/>'))
    end
   
    return tostring(root)
end
 
 
function p._getFarmingTable(realmID, category)
local seedList = GameData.getEntities(SkillData.Farming.recipes,
function(recipe)
return recipe.categoryID == category.id and Skills.getRecipeRealm(recipe) == realmID
end)
if Shared.tableIsEmpty(seedList) then
return ''
end
 
local skillID = 'Farming'
 
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}) .. '<br>Level')
        :tag('th'):wikitext('[[DLC]]')
        :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("Produce")
:tag('th'):wikitext('Healing')
:tag('th'):wikitext('Produce<br>Value')
else
html:tag('th'):attr("colspan", 2):wikitext("Produce")
:tag('th'):wikitext('Produce<br>Value')
end
--html = html:tag('th'):wikitext('Seed Sources')
 
table.sort(seedList, function(a, b) return Skills.standardRecipeSort(skillID, a, b) 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
local level = Skills.getRecipeLevel(skillID, seed)
local baseXP = seed.baseAbyssalExperience or seed.baseExperience
local baseInt = seed.baseInterval
local reqText = Skills.getRecipeRequirementText(SkillData.Farming.name, seed)
 
html =
html:tag('tr')
:tag('td'):wikitext(Icons.Icon({seedItem.name, type='item', notext=true}))
:tag('td'):wikitext('[[' .. seedItem.name .. ']]')
:tag('td'):wikitext(level)
          :css('text-align', 'center')
:tag('td'):wikitext(Icons.getDLCColumnIcon(seedItem.id))
  :css('text-align', 'center')
  :attr('data-sort-value', Icons.getExpansionID(seedItem.id))
:tag('td'):wikitext(Num.formatnum(baseXP))
:tag('td'):attr('data-sort-value', (baseInt / 1000))
  :wikitext(Shared.timeString(baseInt / 1000, true))
:tag('td'):attr('data-sort-value', seedItem.sellsFor)
      :wikitext(Items.getValueText(seedItem))
:tag('td'):wikitext(Icons.Icon({productItem.name, type='item', notext=true}))
:tag('td'):wikitext('[[' .. productItem.name .. ']]')


local area = p.getThievingNPCArea(npc)
if category.id == 'melvorD:Allotment' then
result = result..'||'..area.name
html:tag('td'):wikitext(Icons.Icon({'Hitpoints', type='skill', notext=true}))
result = result..'||'..Icons._SkillReq('Thieving', npc.level)
  :wikitext(' ')
result = result..'||style="text-align:right"|'..npc.xp
  :wikitext(((productItem.healsFor or 0) * 10))
result = result..'||style="text-align:right"|'..(npc.maxHit * 10)
result = result..'||style="text-align:right"|'..npc.perception
if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID > -1 then
local uniqueDrop = Items.getItemByID(npc.uniqueDrop.itemID)
if npc.uniqueDrop.qty > 1 then
result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item', qty = npc.uniqueDrop.qty})
else
result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item'})
end
end
else
html =
result = result..'|| '
html:tag('td'):attr('data-sort-value', productItem.sellsFor)
  :wikitext(Items.getValueText(productItem))
--:tag('td'):wikitext(ItemSourceTables._getItemSources(seedItem))
--   :css('text-align', 'left')
:done()
end
end
end
end
result = result..'\r\n|}'
 
return tostring(tbl:done())
return result
end
end


function p.getThievingAreaTable(frame)
function p._getSlimFarmingTable(realmID, category)
local resultPart = {}
local seedList = GameData.getEntities(SkillData.Farming.recipes,
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
function(recipe)
table.insert(resultPart, '\r\n|- class="headerRow-0"')
return recipe.categoryID == category.id and Skills.getRecipeRealm(recipe) == realmID
table.insert(resultPart, '\r\n!Area!!'..Icons.Icon({'Thieving', type='skill', notext=true})..' Level!!NPCs!!Unique Drops')
end)
if Shared.tableIsEmpty(seedList) then
local areaArray = Shared.clone(SkillData.Thieving.Areas)
return ''
table.sort(areaArray, function(a, b) return a.id < b.id end)
end
for i, area in ipairs(areaArray) do
 
local minLevel, npcList, areaItemList = nil, {}, {}
local skillID = 'Farming'
-- Build NPC list & determine level for area, this is the minimum
 
-- Thieving level required for all NPCs within that area
local tbl = mw.html.create()
if area.npcs ~= nil and Shared.tableCount(area.npcs) > 0 then
local html = tbl:tag("table")
for j, npcID in ipairs(area.npcs) do
        :addClass("wikitable sortable stickyHeader")
-- Don't bother cloning the NPC below since we aren't modifying any part of it
    :tag('tr'):addClass("headerRow-0")
local npc = SkillData.Thieving.NPCs[npcID + 1]
    :tag('th'):wikitext(Icons.Icon({'Farming', type='skill', notext=true}) .. '<br>Level')
if minLevel == nil or npc.level < minLevel then
        :tag('th'):attr("colspan", 2):wikitext("Seeds")
minLevel = npc.level
:tag('th'):attr("colspan", 2):wikitext("Produce")
end
        :tag('th'):wikitext('[[DLC]]')
table.insert(npcList, Icons.Icon({npc.name, type='thieving'}))
 
end
table.sort(seedList, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
else
 
table.insert(npcList, '')
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
local level = Skills.getRecipeLevel(skillID, seed)
 
html =
html:tag('tr')
:tag('td'):wikitext(level)
      :css('text-align', 'center')
:tag('td'):wikitext(Icons.Icon({seedItem.name, type='item', notext=true}))
:tag('td'):wikitext('[[' .. seedItem.name .. ']]')
:tag('td'):wikitext(Icons.Icon({productItem.name, type='item', notext=true}))
:tag('td'):wikitext('[[' .. productItem.name .. ']]')
:tag('td'):wikitext(Icons.getDLCColumnIcon(seedItem.id))
  :css('text-align', 'center')
  :attr('data-sort-value', Icons.getExpansionID(seedItem.id))
:done()
end
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.itemID)
if areaItem == nil then
table.insert(areaItemList, 'Unknown[[Category:Pages with script errors]]')
else
local iconDef = {areaItem.name, type='item'}
if drop.qty > 1 then
iconDef.qty = drop.qty
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
end
table.insert(resultPart, '\r\n|}')
 
return tostring(tbl:done())
return table.concat(resultPart)
end
end


function p.getThievingSourcesForItem(itemID)
function p.getFarmingTable(frame)
local resultArray = {}
local args = frame.args ~= nil and frame.args or frame
local realmName = args.realm
local slim = args.slim == 'true' or args.slim == 'True'
local realm = Skills.getRealmFromName(realmName)
if realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
local categoryName = args[1]
local areaNPCs = {}
local category = GameData.getEntityByName(SkillData.Farming.categories, categoryName)
if category == nil then
--First check area unique drops
return Shared.printError('Invalid farming category: ' .. categoryName .. '. Please choose Allotments, Herbs, Trees or Special')
--If an area drops the item, add all the NPC ids to the list so we can add them later
end
if not result then
 
for  i, area in pairs(SkillData.Thieving.Areas) do
if slim == true then  
for j, drop in pairs(area.uniqueDrops) do
return p._getSlimFarmingTable(realm.id, category)
if drop.itemID == itemID then
else
for k, npcID in pairs(area.npcs) do
return p._getFarmingTable(realm.id, category)
areaNPCs[npcID] = drop.qty
end
break
end
end
end
end
end
end
--Now go through and get drop chances on each NPC if needed
 
for i, npc in pairs(SkillData.Thieving.NPCs) do
function p.getFarmingFoodTable(frame)
local totalWt = 0
local args = frame.args ~= nil and frame.args or frame
local dropWt = 0
local realmName = args.realm
local dropQty = 0
local realm = Skills.getRealmFromName(realmName)
for j, drop in pairs(npc.lootTable) do
if realm == nil then
totalWt = totalWt + drop[2]
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
if drop[1] == itemID then
dropWt = drop[2]
dropQty = drop[3]
end
end
if dropWt > 0 then
table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = dropQty, wt = dropWt * thievingNormalLootChance, totalWt = totalWt * 100, level = npc.level})
end
--Chance of -1 on unique drops is to indicate variable chance
if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID == itemID then
table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.qty, maxQty = npc.uniqueDrop.qty, wt = -1, totalWt = -1, level = npc.level})
end
if areaNPCs[npc.id] ~= nil then
table.insert(resultArray, {npc = npc.name, minQty = areaNPCs[npc.id], maxQty = areaNPCs[npc.id], wt = thievingAreaLootChance, totalWt = 100, level = npc.level})
end
end
end
local skillID = 'Farming'
for i, drop in pairs(SkillData.Thieving.RareItems) do
    local root = mw.html.create('table')
if drop.itemID == itemID then
        :addClass('wikitable sortable stickyHeader')
table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = 1})
   
end
    -- Header row
    local headerRow = root:tag('tr')
        :addClass('headerRow-0')
    headerRow:tag('th'):attr('colspan', '2'):wikitext('Crop')
    headerRow:tag('th'):wikitext(Icons.Icon({"Farming", type="skill", notext=true}) .. '<br>Level')
    headerRow:tag('th'):wikitext('[[DLC]]')
    headerRow:tag('th'):wikitext('Healing')
    headerRow:tag('th'):wikitext('Value')
   
    local recipes = GameData.getEntities(SkillData[skillID].recipes,
        function(recipe)
        if Skills.getRecipeRealm(recipe) ~= realm.id then
        return false
        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 Skills.standardRecipeSort(skillID, a, b) end)
   
    for i, recipe in ipairs(recipes) do
        local product = Items.getItemByID(recipe.productId)
        if product and product.healsFor and product.healsFor > 0 then
            local row = root:tag('tr')
            row:tag('td'):wikitext(Icons.Icon({product.name, type='item', notext='true'}))
            row:tag('td'):wikitext('[[' .. product.name .. ']]')
            row:tag('td'):css('text-align', 'center')
            :wikitext(Skills.getRecipeLevel(skillID, recipe))
            row:tag('td'):css('text-align', 'center')
                    :attr('data-sort-value', Icons.getExpansionID(product.id)) 
                :wikitext(Icons.getDLCColumnIcon(product.id))       
            row:tag('td'):css('text-align', 'right')
                :attr('data-sort-value', product.healsFor)
                :wikitext(Icons.Icon({"Hitpoints", type="skill", notext=true}) .. ' ' .. (product.healsFor * 10))
            row:tag('td'):css('text-align', 'right')
                :attr('data-sort-value', product.sellsFor)
                :wikitext(Items.getValueText(product))
        end
    end
   
    return tostring(root)
end
 
 
function p.getFarmingPlotTable(frame)
    local skillID = 'Farming'
    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, Trees or Special')
    end
   
    local patches = GameData.getEntities(SkillData.Farming.plots,
        function(plot)
            return plot.categoryID == category.id
        end
    )
   
    table.sort(patches,
        function(a, b)
            local abyssA, abyssB = a.abyssalLevel or 0, b.abyssalLevel or 0
            if abyssA == abyssB then
                if a.level == b.level then
                    return a.id < b.id
                else
                    return a.level < b.level
                end
            else
                return abyssA < abyssB
            end
        end
    )
   
    if Shared.tableIsEmpty(patches) then
        return ''
    end
   
    local root = mw.html.create('table')
        :addClass('wikitable sortable stickyHeader')
   
    -- Header row
    local headerRow = root:tag('tr')
    headerRow:tag('th'):wikitext('Plot')
    headerRow:tag('th'):wikitext('Requirements')
    headerRow:tag('th'):wikitext('Cost')
   
    for i, patch in ipairs(patches) do
        local level = Skills.getRecipeLevel(skillID, patch)
        local reqText = Skills.getRecipeRequirementText(skillID, patch)
        local costText = Common.getCostString({ items = patch.itemCosts, currencies = patch.currencyCosts }, 'Free')
        local costVal = (patch.currencyCosts and patch.currencyCosts[1] and patch.currencyCosts[1].quantity) or 0
       
        local row = root:tag('tr')
        row:tag('td'):wikitext(i)
        row:tag('td'):css('text-align', 'right')
            :attr('data-sort-value', level)
            :wikitext(reqText)
        row:tag('td'):css('text-align', 'right')
            :attr('data-sort-value', costVal)
            :wikitext(costText)
    end
   
    return tostring(root)
end
 
function p._buildAstrologyConstellationTable(realmID)
    local modTypes = {
        {
            name = 'Standard',
            modKey = 'standardModifiers',
            levels = SkillData.Astrology.standardModifierLevels,
            inUse = false
        },
        {
            name = 'Unique',
            modKey = 'uniqueModifiers',
            levels = SkillData.Astrology.uniqueModifierLevels,
            inUse = false
        },
        {
            name = 'Abyssal',
            modKey = 'abyssalModifiers',
            levels = SkillData.Astrology.abyssalModifierLevels,
            inUse = false
        },
    }
 
    local recipes = GameData.getEntities(SkillData.Astrology.recipes,
        function(cons)
            return Skills.getRecipeRealm(cons) == realmID
        end
    )
    table.sort(recipes,
        function(a, b)
            return Skills.getRecipeLevel('Astrology', a) < Skills.getRecipeLevel('Astrology', b)
        end
    )
 
    -- Determine which mod types are in use
    for _, recipe in ipairs(recipes) do
        for _, modType in ipairs(modTypes) do
            if not modType.inUse then
                local recipeMods = recipe[modType.modKey]
                if recipeMods ~= nil and not Shared.tableIsEmpty(recipeMods) then
                    modType.inUse = true
                end
            end
        end
    end
 
    local root = mw.html.create()
    local tableRoot = root:tag('table')
    tableRoot:addClass('wikitable stickyHeader')
 
    -- Header rows
    local headerRow1 = tableRoot:tag('tr'):addClass('headerRow-0')
    headerRow1:tag('th'):attr('rowspan', 2)
    :attr('colspan', 2)
    :wikitext('Constellation')
    headerRow1:tag('th'):attr('rowspan', 2)
    :wikitext(Icons.Icon({ "Astrology", type='skill', notext='true' }) .. '<br>Level')
    headerRow1:tag('th'):attr('rowspan', 2)
    :wikitext('[[DLC]]')
    headerRow1:tag('th'):attr('rowspan', 2)
    :wikitext('XP')
    headerRow1:tag('th'):attr('rowspan', 2)
    :wikitext('Skills')
 
    local headerRow2 = tableRoot:tag('tr'):addClass('headerRow-1')
    for _, modType in ipairs(modTypes) do
        if modType.inUse then
        local spanCount = modType.name == 'Abyssal' and 3 or 2
            headerRow1:tag('th'):attr('colspan', spanCount):wikitext(modType.name .. ' Stars')
            headerRow2:tag('th'):wikitext(Icons.Icon({'Mastery', notext=true}) .. '<br>Level')
            if modType.name == "Abyssal" then
            headerRow2:tag('th'):wikitext('Level')
            end
            headerRow2:tag('th'):wikitext('Modifiers')
        end
    end
 
    -- Data rows
    for _, cons in ipairs(recipes) do
        local modData = Skills._getConstellationModifiers(cons)
        local name = cons.name
        local skillIconArray = {}
        for _, skillID in ipairs(cons.skillIDs) do
            table.insert(skillIconArray, Icons.Icon({Constants.getSkillName(skillID), type='skill'}))
        end
 
        -- Calculate maximum rows needed
        local maxRows = 1
        for _, modTypeData in pairs(modData) do
            maxRows = math.max(maxRows, Shared.tableCount(modTypeData))
        end
 
        -- Iterate through rows
        for rowIdx = 1, maxRows do
            local row = tableRoot:tag('tr')
            if rowIdx == 1 then
                row:tag('td'):attr('rowspan', maxRows)
                :wikitext(Icons.Icon({name, type='constellation', size=50, notext=true}))
                :css('text-align', 'center')
                row:tag('td'):attr('rowspan', maxRows)
                :wikitext(name)
                row:tag('td'):attr('rowspan', maxRows)
                :wikitext((cons.abyssalLevel or cons.level))
                :css('text-align', 'center')
            row:tag('td'):css('text-align', 'center')
                    :attr('data-sort-value', Icons.getExpansionID(cons.id)) 
                    :attr('rowspan', maxRows)
                :wikitext(Icons.getDLCColumnIcon(cons.id)) 
                row:tag('td'):attr('rowspan', maxRows)
                    :wikitext(Num.formatnum(cons.baseAbyssalExperience or cons.baseExperience))
                    :css('text-align', 'right')
                row:tag('td'):attr('rowspan', maxRows)
                :wikitext(table.concat(skillIconArray, '<br/>'))
                :css('text-wrap', 'nowrap')
            end
 
            -- Modifiers data
            for _, modType in ipairs(modTypes) do
                if modType.inUse then
                    local masteryLevel = modType.levels[rowIdx]
                    local rowModData = modData[modType.modKey][rowIdx]
                    local rowConsData = cons[modType.modKey][rowIdx]
                    local cell1 = row:tag('td')
                    if modType.name == "Abyssal" then
                    local starUnlockReq = nil
                    local cell2 = row:tag('td')
                    if rowConsData ~= nil and rowConsData.unlockRequirements ~= nil and Shared.tableCount(rowConsData.unlockRequirements) == 2 then
                    starUnlockReq = Common.getRequirementString({cons[modType.modKey][rowIdx].unlockRequirements[2]}, nil)
                    end
                    if starUnlockReq ~= nil then
                    cell2:wikitext(starUnlockReq)
                    :css('text-align', 'right')
                    else
                    cell2:wikitext('N/A')
                    :addClass('table-na')
                    end
                    end
                    local cell3 = row:tag('td')
                    if masteryLevel ~= nil and rowModData ~= nil then
local modText = {}
                        for _, modKey in ipairs({'modifiers', 'enemyModifiers'}) do
                        local mods = rowModData[modKey]
                        if mods ~= nil then
                        if modKey == 'enemyModifiers' then
                        table.insert(modText, 'Gives the enemy:')
                        end
                        table.insert(modText, Modifiers.getModifiersText(mods, false, false, 10))
                        end
                        end
                        cell1:wikitext(masteryLevel)
                        :css('text-align', 'right')
                        cell3:wikitext(table.concat(modText, '<br>'))
                    else
                        cell1:attr('colspan', 2)
                        :wikitext('N/A')
                        :addClass('table-na')
                    end
                end
            end
        end
    end
 
return tostring(root)
end
 
function p.buildAstrologyConstellationTable(frame)
local args = frame.args ~= nil and frame.args or frame
local realmName = args.realm
local realm = Skills.getRealmFromName(realmName)
if realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
end
 
return resultArray
return p._buildAstrologyConstellationTable(realm.id)
end
end


return p
return p

Latest revision as of 13:05, 7 July 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 Modifiers = require('Module:Modifiers')
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local Skills = require('Module:Skills')
local ItemSourceTables = require('Module:Items/SourceTables')
local Num = require("Module:Number")

local function lootValueText(lootValue)
	local returnPart = {}
	for _, currencyDefn in ipairs(GameData.rawData.currencies) do
		-- Guarantee order by iterating through currency game data definition
		local currID = currencyDefn.id
		local val = Num.round(lootValue[currID], 2, 2)
		if val ~= nil then
			table.insert(returnPart, Icons._Currency(currID, val))
		end
	end
	return table.concat(returnPart, ', ')
end

function p.getTreesTable(frame)
	local args = frame.args ~= nil and frame.args or frame
	local realmName = args.realm
	local realm = Skills.getRealmFromName(realmName)
	if realm == nil then
		return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
	end
	local skillID = 'Woodcutting'

	local html = mw.html.create('table')
		:addClass('wikitable sortable stickyHeader')
		
	html:tag('tr')
		:addClass("headerRow-0")
			:tag('th'):attr('colspan', 2)
					  :wikitext('Tree')
			:tag('th'):attr('colspan', 2)
					  :wikitext('Logs')
			:tag('th'):wikitext('Requirements')
			:tag('th'):wikitext('[[DLC]]')
			:tag('th'):wikitext('XP')
			:tag('th'):wikitext('Cut Time')
			:tag('th'):wikitext('XP/s')
			:tag('th'):wikitext('Price/s')

	local trees = GameData.getEntities(SkillData.Woodcutting.trees,
		function(tree)
			return Skills.getRecipeRealm(tree) == realm.id
		end
	)
	
	table.sort(trees, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
	for i, tree in ipairs(trees) do
		local level = Skills.getRecipeLevel(skillID, tree)
		local baseXP = tree.baseAbyssalExperience or tree.baseExperience
		local baseInt = tree.baseInterval
		local reqText = Skills.getRecipeRequirementText(SkillData.Woodcutting.name, tree)
		local log = Items.getItemByID(tree.productId)
		local sellCurrency = log.sellsForCurrency or 'melvorD:GP'
		local XPSec = baseXP / (baseInt / 1000)
		local currSec = Num.round(log.sellsFor / (baseInt / 1000), 2, 2)
	

		html:tag('tr')
				:tag('td'):wikitext(Icons.Icon({log.name, img=tree.name, type='tree', notext=true}))
				          :addClass('table-img')
				          :attr('data-sort-value', tree.name)
				:tag('td'):wikitext(tree.name)	
				:tag('td'):wikitext(Icons.Icon({log.name, type='item', notext=true}))	
						  :addClass('table-img')
				          :attr('data-sort-value', log.name)		
				:tag('td'):wikitext('[[' .. log.name .. ']]')	
				:tag('td'):wikitext(reqText)	
				          :attr('data-sort-value', level)	
				:tag('td'):wikitext(Icons.getDLCColumnIcon(tree.id))
						  :attr('data-sort-value', Icons.getExpansionID(tree.id))	
						  :css('text-align', 'center')
				:tag('td'):wikitext(Num.formatnum(baseXP))	
						  :css('text-align', 'right')
				:tag('td'):wikitext(Shared.timeString(baseInt / 1000, true))
						  :attr('data-sort-value', baseInt)	
						  :css('text-align', 'right')
				:tag('td'):wikitext(Num.round(XPSec, 2, 2))
						  :css('text-align', 'right')
				:tag('td'):wikitext(Icons._Currency(sellCurrency, currSec))
						  :attr('data-sort-value', currSec)	
						  :css('text-align', 'right')
	end
	
	return tostring(html)
end

function p.getSpecialFishingTable(frame)
	local args = frame.args ~= nil and frame.args or frame
	local realmName = args.realm
	local realm = Skills.getRealmFromName(realmName)
	if realm == nil then
		return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
	end

	local totalWt, lootValue = 0, {}
	local realmSpecials = GameData.getEntityByProperty(SkillData.Fishing.specialItems, 'realmID', realm.id)
	if realmSpecials == nil then
		return ''
	end
	local itemArray = realmSpecials.drops
	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 html = mw.html.create('table')
	html:addClass('wikitable sortable stickyHeader')

	-- Add header row
	local headerRow = html:tag('tr'):addClass('headerRow-0')
	headerRow:tag('th'):attr('colspan', '2'):wikitext('Item')
	headerRow:tag('th'):wikitext('Value')
	headerRow:tag('th'):attr('colspan', '2'):wikitext('Chance')

	-- Add item rows
	for i, itemDef in ipairs(itemArray) do
		local item = Items.getItemByID(itemDef.itemID)
		if item ~= nil then
			local dropChance = itemDef.weight / totalWt * 100
			local currID = item.sellsForCurrency or 'melvorD:GP'
			-- 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 row = html:tag('tr')

			row:tag('td'):addClass('table-img'):wikitext(Icons.Icon({item.name, type='item', notext=true}))
			row:tag('td'):wikitext(Icons.Icon({item.name, type='item', noicon=true}))
			row:tag('td')
				:attr('data-sort-value', item.sellsFor)
				:wikitext(Items.getValueText(item))
			row:tag('td')
				:css('text-align', 'right')
				:attr('data-sort-value', itemDef.weight)
				:wikitext(Num.fraction(itemDef.weight, totalWt))
			row:tag('td')
				:css('text-align', 'right')
				:wikitext(string.format(fmt, dropChance) .. '%')

			if lootValue[currID] == nil then
				lootValue[currID] = 0
			end
			lootValue[currID] = lootValue[currID] + (dropChance / 100 * item.sellsFor)
		end
	end

	local result = tostring(html)
	local averageValueText = 'The average value of a roll on the special fishing loot table is ' .. lootValueText(lootValue)
	return result .. '\n' .. averageValueText
end

function p.getFishingJunkTable(frame)
	local html = mw.html.create('table')
	html:addClass('wikitable sortable stickyHeader')

	-- Add header row
	local headerRow = html:tag('tr'):addClass('headerRow-0')
	headerRow:tag('th'):attr('colspan', '2'):wikitext('Item')
	headerRow:tag('th'):wikitext('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)

	-- Add item rows
	for i, item in ipairs(itemArray) do
		local row = html:tag('tr')
		row:tag('td'):addClass('table-img')
					 :wikitext(Icons.Icon({item.name, type='item', notext=true}))
		row:tag('td'):wikitext(Icons.Icon({item.name, type='item', noicon=true}))
		row:tag('td')
			:attr('data-sort-value', item.sellsFor)
			:wikitext(Items.getValueText(item))
	end

	return tostring(html)
end

function p.getMiningOresTable(frame)
	local args = frame.args ~= nil and frame.args or frame
	local realmName = args.realm
	local realm = Skills.getRealmFromName(realmName)
	if realm == nil then
		return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
	end
	local skillID = 'Mining'
	
	local html = mw.html.create('table')
		:addClass("wikitable sortable stickyHeader")
		
	html:tag('tr'):addClass("headerRow-0")
			:tag('th'):wikitext('Rock')
					  :attr('colspan', 2)
			:tag('th'):wikitext('Ore')
					  :attr('colspan', 2)
			:tag('th'):wikitext('Type')
			:tag('th'):wikitext('Requirements')
			:tag('th'):wikitext('[[DLC]]')
			:tag('th'):wikitext('XP')
			:tag('th'):wikitext('Respawn<br>Time')
			:tag('th'):wikitext('Ore Value')

	local mineData = GameData.getEntities(SkillData.Mining.rockData,
		function(obj)
			return Skills.getRecipeRealm(obj) == realm.id
		end
	)
	table.sort(mineData, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
	for i, oreData in ipairs(mineData) do
		local level = Skills.getRecipeLevel(skillID, oreData)
		local baseXP = oreData.baseAbyssalExperience or oreData.baseExperience
		local reqText = Skills.getRecipeRequirementText(SkillData.Mining.name, oreData)
		local ore = Items.getItemByID(oreData.productId)
		local respawnSort, respawnText = 0, 'N/A'
		if oreData.hasPassiveRegen then
			respawnSort = oreData.baseRespawnInterval / 1000
			respawnText = Shared.timeString(respawnSort, true)
		end
		local categoryName = ''
		local category = GameData.getEntityByID(SkillData.Mining.categories, oreData.category)
		if category ~= nil and category.name ~= nil then
			categoryName = category.name
		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 ''
		
		local row = html:tag('tr')
		row :tag('td'):wikitext(Icons.Icon({oreData.name, type='rock', notext=true, nolink=true}))
					  :addClass('table-img')
					  :attr('data-sort-value', rockName)
			:tag('td'):wikitext(rockName)
			:tag('td'):wikitext(Icons.Icon({ore.name, type='item', notext=true}))
					  :addClass('table-img')
					  :attr('data-sort-value', ore.name)
			:tag('td'):wikitext(qtyText .. '[[' .. ore.name .. ']]')
			:tag('td'):wikitext(categoryName)
			:tag('td'):wikitext(reqText)
					  :attr('data-sort-value', level)
			:tag('td'):wikitext(Icons.getDLCColumnIcon(oreData.id))
					  :attr('data-sort-value', Icons.getExpansionID(oreData.id))
					  :css('text-align', 'center')
			:tag('td'):wikitext(Num.formatnum(baseXP))
					  :css('text-align', 'right')
		local respawn = 
		row:tag('td'):wikitext(respawnText)
					 :attr('data-sort-value', respawnSort)
		row:tag('td'):wikitext(Items.getValueText(ore))
					 :attr('data-sort-value', ore.sellsFor)
					  
		if respawnText == 'N/A' then
			respawn:addClass('table-na')
		else
			respawn:css('text-align', 'right')
		end
	end

	return tostring(html)
end

function p._getMiningGemsTable(gemType)
	if type(gemType) ~= 'string' then
		gemType = 'Normal'
	end
	local validTypes = {
		["Normal"] = 'randomGems',
		["Superior"] = 'randomSuperiorGems',
		["Abyssal"] = 'randomAbyssalGems'
	}
	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 html = mw.html.create('table')
		:addClass('wikitable sortable')

	-- Add header row
	local headerRow = html:tag('tr'):addClass('headerRow-0')
		:tag('th'):wikitext('Gem')
				  :attr('colspan', '2')
		:tag('th'):wikitext('[[DLC]]')
		:tag('th'):wikitext('Gem Chance')
		:tag('th'):wikitext('Gem Price')

	-- Add gem rows
	for i, gem in ipairs(gemData) do
		local gemItem = Items.getItemByID(gem.itemID)
		local gemPct = gem.weight / totalWeight * 100
		local row = html:tag('tr')

		row:tag('td'):addClass('table-img')
					 :wikitext(Icons.Icon({gemItem.name, type='item', notext=true}))
		row:tag('td'):attr('data-sort-value', gemItem.name)
					 :wikitext('[[' .. gemItem.name ..']]')
		row:tag('td'):wikitext(Icons.getDLCColumnIcon(gemItem.id))
					 :attr('data-sort-value', Icons.getExpansionID(gemItem.id))
					 :css('text-align', 'center')
		row:tag('td'):css('text-align', 'right')
					 :attr('data-sort-value', gemPct)
					 :wikitext(string.format("%.1f%%", gemPct))
		row:tag('td'):attr('data-sort-value', gemItem.sellsFor)
				     :wikitext(Items.getValueText(gemItem))
	end

	return tostring(html)
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 args = frame.args ~= nil and frame.args or frame
	local realmName = args.realm
	local realm = Skills.getRealmFromName(realmName)
	if realm == nil then
		return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
	end
	local skillID = 'Fishing'

	local recipeList = GameData.getEntities(SkillData.Fishing.fish, function(obj)
		return Skills.getRecipeRealm(obj) == realm.id
	end)
	table.sort(recipeList, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)

	-- Determine cooking levels for all fish
	local cookRecipes = {}
	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 cookRecipes[mat.id] == nil then
				cookRecipes[mat.id] = recipe
			end
		end
	end

	local html = mw.html.create('table')
	html:addClass('wikitable sortable stickyHeader')

	-- Add header row
	local headerRow0 = html:tag('tr'):addClass('headerRow-0')
	headerRow0:tag('th'):attr('colspan', '2'):attr('rowspan', '2'):wikitext('Fish')
	headerRow0:tag('th'):attr('rowspan', '2'):wikitext(Icons._SkillRealmIcon('Fishing', realm.id) .. '<br>Level')
	headerRow0:tag('th'):attr('rowspan', '2'):wikitext('[[DLC]]')
	headerRow0:tag('th'):attr('colspan', '3'):wikitext('Catch Time')
	headerRow0:tag('th'):attr('rowspan', '2'):wikitext('XP')
	headerRow0:tag('th'):attr('rowspan', '2'):wikitext('XP/s')
	headerRow0:tag('th'):attr('rowspan', '2'):wikitext('Value')
	headerRow0:tag('th'):attr('rowspan', '2'):wikitext('GP/s')

	local headerRow1 = html:tag('tr'):addClass('headerRow-1')
	headerRow1:tag('th'):wikitext('Min')
	headerRow1:tag('th'):wikitext('Max')
	headerRow1:tag('th'):wikitext('Avg')

	-- Add fish rows
	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 level = Skills.getRecipeLevel(skillID, recipe)
			local reqText = Skills.getRecipeRequirementText(skillID, recipe)
			local baseXP = recipe.baseAbyssalExperience or recipe.baseExperience
			local XPSec = baseXP / timeSortVal
			local sellCurrency = fish.sellsForCurrency or 'melvorD:GP'
			local GPSec = fish.sellsFor / timeSortVal

			local row = html:tag('tr')
			row:tag('td'):wikitext(Icons.Icon({fish.name, type='item', notext=true}))
						 :addClass('table-img')
			row:tag('td'):wikitext('[[' .. fish.name .. ']]')
						 :attr('data-sort-value', fish.name)
			row:tag('td'):wikitext(level)
						 :css('text-align', 'center')
						 :attr('data-sort-value', level)
			row:tag('td'):wikitext(Icons.getDLCColumnIcon(fish.id))
						 :css('text-align', 'center')
						 :attr('data-sort-value', Icons.getExpansionID(fish.id))
			row:tag('td'):wikitext(string.format("%.1fs", timeMin))
						 :css('text-align', 'right')
						 :attr('data-sort-value', timeMin)
			row:tag('td'):wikitext(string.format("%.1fs", timeMax))
						 :css('text-align', 'right')
						 :attr('data-sort-value', timeMax)
			row:tag('td'):wikitext(string.format("%.1fs", timeAvg))
						 :css('text-align', 'right')
						 :attr('data-sort-value', timeAvg)
			row:tag('td'):wikitext(Num.formatnum(baseXP))
						 :css('text-align', 'right')
						 :attr('data-sort-value', baseXP)
			row:tag('td'):wikitext(Num.round(XPSec, 2, 2))
						 :css('text-align', 'right')
			row:tag('td'):wikitext(Items.getValueText(fish))
						 :attr('data-sort-value', fish.sellsFor)
						 :css('text-align', 'right')
			row:tag('td'):wikitext(Icons._Currency(sellCurrency, Num.round(GPSec, 2, 2)))
						 :attr('data-sort-value', GPSec)
						 :css('text-align', 'right')
				
		end
	end

	return tostring(html)
end


function p.getFishingAreasTable(frame)
	local args = frame.args ~= nil and frame.args or frame
	local realmName = args.realm
	local realm = Skills.getRealmFromName(realmName)
	if realm == nil then
		return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
	end
	local skillID = 'Fishing'

	local html = mw.html.create('table')
	html:addClass('wikitable sortable stickyHeader')

	-- Add header row
	local headerRow = html:tag('tr'):addClass('headerRow-0')
	headerRow:tag('th'):wikitext('Name')
	headerRow:tag('th'):wikitext('Fish')
	headerRow:tag('th'):wikitext('[[DLC]]')
	headerRow:tag('th'):wikitext('Fish Chance')
	headerRow:tag('th'):wikitext('Junk Chance')
	headerRow:tag('th'):wikitext('Special Chance')

	-- Get fishing areas
	local fishAreas = GameData.getEntities(SkillData.Fishing.areas, function(obj)
		return Skills.getRecipeRealm(obj) == realm.id
	end)

	-- Add rows for each fishing area
	for i, area in ipairs(fishAreas) do
		local row = html:tag('tr')
		row:tag('td')
			:css('text-align', 'left')
			:wikitext(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
		row:tag('td')
			:wikitext(table.concat(fishArray, '<br/>'))
		row:tag('td')
			:css('text-align', 'center')
			:wikitext(Icons.getDLCColumnIcon(area.id))
			:attr('data-sort-value', Icons.getExpansionID(area.id))
		row:tag('td')
			:css('text-align', 'right')
			:wikitext(area.fishChance .. '%')
		row:tag('td')
			:css('text-align', 'right')
			:wikitext(area.junkChance .. '%')
		row:tag('td')
			:css('text-align', 'right')
			:wikitext(area.specialChance .. '%')
	end

	return tostring(html)
end


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

function p._getThievingGeneralRareTable(npc)
	local npcRealm = nil
	if npc ~= nil then
		npcRealm = Skills.getRecipeRealm(npc)
	end

	local html = mw.html.create('table')
	html:addClass('wikitable sortable')

	-- Add header row
	local headerRow = html:tag('tr')
	headerRow:tag('th'):wikitext('Item')
	headerRow:tag('th'):wikitext('[[DLC]]')
	headerRow:tag('th'):wikitext('Qty')
	headerRow:tag('th'):wikitext('Price')
	headerRow:tag('th'):attr('colspan', '2'):wikitext('Chance')

	-- Add rows for each rare item
	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
		local npcMatch = (npc == nil or drop.npcs == nil or Shared.contains(drop.npcs, npc.id))
		local realmMatch = (npcRealm == nil or drop.realms == nil or Shared.contains(drop.realms, npcRealm))

		if npcMatch and realmMatch then
			local thisItem = Items.getItemByID(drop.itemID)
			local odds = drop.chance

			local row = html:tag('tr')
			row:tag('td')
				:attr('data-sort-value', thisItem.name)
				:wikitext(Icons.Icon({thisItem.name, type='item'}))
			row:tag('td')
				:wikitext(Icons.getDLCColumnIcon(thisItem.id))
				:attr('data-sort-value', Icons.getExpansionID(thisItem.id))
				:css('text-align', 'center')
			row:tag('td')
				:wikitext('1')
			row:tag('td')
				:attr('data-sort-value', thisItem.sellsFor)
				:wikitext(Items.getValueText(thisItem))
			row:tag('td')
				:css('text-align', 'right')
				:attr('data-sort-value', odds)
				:wikitext(Num.fraction(1, Num.round2(1/(odds/100), 0)))
			row:tag('td')
				:css('text-align', 'right')
				:attr('data-sort-value', odds)
				:wikitext(Num.round(odds, 4, 4) .. '%')
		end
	end

	return tostring(html)
end


function p._getThievingNPCCurrencyText(npc)
	local currTextPart = {}
	for _, currencyDrop in ipairs(npc.currencyDrops) do
		table.insert(currTextPart, Icons._Currency(currencyDrop.id, 1, currencyDrop.quantity))
	end
	return table.concat(currTextPart, ', ')
end

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

	--Five sections here: Currency, normal loot, area loot, rare loot, and unique item
	--First up, currency:
	table.insert(sectionTxt, 'Successfully pickpocketing the ' .. npc.name .. ' will always give '.. p._getThievingNPCCurrencyText(npc))

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

		--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, Num.formatnum(loot.minQuantity) .. ' - ' .. Num.formatnum(loot.maxQuantity))
			else
				table.insert(normalTxt, Num.formatnum(loot.maxQuantity))
			end

			--Adding price columns
			local sellAmount, sellCurrency = nil, nil
			if thisItem == nil then
				table.insert(normalTxt, '||data-sort-value="0"|???')
			else
				sellAmount = thisItem.sellsFor or 0
				sellCurrency = thisItem.sellsForCurrency or 'melvorD:GP'
				table.insert(normalTxt, '||' .. Items.getValueText(thisItem, loot.minQuantity, loot.maxQuantity))
			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, '|'..Num.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"|'..Num.round(dropChance, 2, 2)..'%')

			--Adding to the average loot value based on price & dropchance
			if sellAmount ~= nil and sellCurrency ~= nil then
				if lootValue[sellCurrency] == nil then
					lootValue[sellCurrency] = 0
				end
				lootValue[sellCurrency] = lootValue[sellCurrency] + (dropChance * 0.01 * sellAmount * (loot.minQuantity + loot.maxQuantity) / 2)
			end
		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"|'..Num.fraction(lootChance, 100)..'||')
			else
				table.insert(normalTxt, '\r\n|colspan="2" ')
			end
			table.insert(normalTxt, 'style="text-align:right"|'..Num.round(lootChance, 2, 2)..'%')
		end
		table.insert(normalTxt, '\r\n|}')

		table.insert(normalTxt, '\r\nThe loot obtained from the average successful pickpocket is worth ' .. lootValueText(lootValue) .. ' if sold.')

		-- Amend lootValue
		for _, currencyDrop in ipairs(npc.currencyDrops) do
			lootValue[currencyDrop.id] = lootValue[currencyDrop.id] + (1 + currencyDrop.quantity) / 2
		end

		table.insert(normalTxt, '\r\n\r\nIncluding currency, the average successful pickpocket is worth ' .. lootValueText(lootValue) .. '.')
		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)
	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..'"| '..Num.formatnum(drop.quantity)..'||data-sort-value="'..thisItem.sellsFor..'"|'..Items.getValueText(thisItem)
		lineTxt = lineTxt..'||style="text-align:right"|'..Num.fraction(1, 1/(SkillData.Thieving.baseAreaUniqueChance/100))
		lineTxt = lineTxt..'||'..Num.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(frame)
    local args = frame.args or frame:getParent().args
    local realmName = args.realm
    local realm = Skills.getRealmFromName(realmName)
    
    if realm == nil then
        return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
    end
    
    local skillID = 'Thieving'
    local root = mw.html.create('table')
        :addClass('wikitable sortable stickyHeader')
    
    -- Header row
    local headerRow = root:tag('tr')
        :addClass('headerRow-0')
        :tag('th'):attr('colspan', '2'):wikitext('Name')
        :tag('th'):wikitext('Area')
        :tag('th'):wikitext(Icons.Icon({'Thieving', type='skill', notext=true}) .. '<br>Level')
        :tag('th'):wikitext('[[DLC]]')
        :tag('th'):wikitext('Experience')
        :tag('th'):wikitext('Max Hit')
        :tag('th'):wikitext('Perception')
        :tag('th'):wikitext('Currency')
        :tag('th'):wikitext('Unique Drop')
    
    local npcArray = GameData.getEntities(SkillData.Thieving.npcs,
        function(obj)
            return Skills.getRecipeRealm(obj) == realm.id
        end
    )
    table.sort(npcArray, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
    
    for i, npc in ipairs(npcArray) do
        local level = Skills.getRecipeLevel(skillID, npc)
        local baseXP = npc.baseAbyssalExperience or npc.baseExperience
        local area = Skills.getThievingNPCArea(npc)
        local currSortAmt = npc.currencyDrops[1].quantity
        
        local row = root:tag('tr')
        row:tag('td'):wikitext(Icons.Icon({npc.name, type='thieving', notext=true}))
        row:tag('td'):attr('data-sort-value', npc.name)
            		 :wikitext('[[' .. npc.name .. ']]')
        row:tag('td'):wikitext(area.name)
        row:tag('td'):wikitext(level)
        			 :css('text-align', 'center')
        row:tag('td'):wikitext(Icons.getDLCColumnIcon(npc.id))
        			 :css('text-align', 'center')
        			 :attr('data-sort-value', Icons.getExpansionID(npc.id))
        row:tag('td'):css('text-align', 'right')
            		 :wikitext(Num.formatnum(baseXP))
        row:tag('td'):css('text-align', 'right')
            		 :wikitext(Num.formatnum(npc.maxHit * 10))
        row:tag('td'):css('text-align', 'right')
            		 :attr('data-sort-value', npc.perception)
            		 :wikitext(Num.formatnum(npc.perception))
        row:tag('td'):attr('data-sort-value', currSortAmt)
            		 :wikitext(p._getThievingNPCCurrencyText(npc))
        
        if npc.uniqueDrop ~= nil then
            local uniqueDrop = Items.getItemByID(npc.uniqueDrop.id)
            if npc.uniqueDrop.quantity > 1 then
                row:tag('td'):attr('data-sort-value', uniqueDrop.name)
                    :wikitext(Icons.Icon({uniqueDrop.name, type='item', qty=npc.uniqueDrop.quantity}))
            else
                row:tag('td'):attr('data-sort-value', uniqueDrop.name)
                    :wikitext(Icons.Icon({uniqueDrop.name, type='item'}))
            end
        else
            row:tag('td'):wikitext(' ')
        end
    end
    
    return tostring(root)
end

function p.getThievingAreaTable(frame)
    local args = frame.args or frame:getParent().args
    local realmName = args.realm
    local realm = Skills.getRealmFromName(realmName)
    
    if realm == nil then
        return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
    end
    
    local skillID = 'Thieving'
    local root = mw.html.create('table')
        :addClass('wikitable sortable stickyHeader')
    
    -- Header row
    local headerRow = root:tag('tr')
        :addClass('headerRow-0')
    headerRow:tag('th'):wikitext('Area')
    headerRow:tag('th'):wikitext(Icons.Icon({'Thieving', type='skill', notext=true}) .. '<br>Level')
    headerRow:tag('th'):wikitext('NPCs')
    headerRow:tag('th'):wikitext('Unique Drops')
    
    local areas = GameData.getEntities(SkillData.Thieving.areas,
        function(obj)
            return Skills.getRecipeRealm(obj) == realm.id
        end
    )
    
    for i, area in ipairs(areas) do
        local minLevel, npcList, areaItemList = nil, {}, {}
        
        -- Build NPC list & determine minimum Thieving level
        if area.npcIDs and not Shared.tableIsEmpty(area.npcIDs) then
            for j, npcID in ipairs(area.npcIDs) do
                local npc = Skills.getThievingNPCByID(npcID)
                local level = Skills.getRecipeLevel(skillID, npc)
                if not minLevel or level < minLevel then
                    minLevel = 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 and Shared.tableCount(area.uniqueDrops) > 0 then
            for k, drop in ipairs(area.uniqueDrops) do
                local areaItem = Items.getItemByID(drop.id)
                if areaItem then
                    local iconDef = {areaItem.name, type='item'}
                    if drop.quantity > 1 then
                        iconDef.qty = drop.quantity
                    end
                    table.insert(areaItemList, Icons.Icon(iconDef))
                else
                    table.insert(areaItemList, 'Unknown[[Category:Pages with script errors]]')
                end
            end
        else
            table.insert(areaItemList, '')
        end
        
        -- Generate table row
        local row = root:tag('tr')
        row:tag('td'):wikitext(area.name)
        row:tag('td'):wikitext(minLevel)
        			 :css('text-align', 'center')
        row:tag('td'):wikitext(table.concat(npcList, '<br/>'))
        row:tag('td'):wikitext(table.concat(areaItemList, '<br/>'))
    end
    
    return tostring(root)
end


function p._getFarmingTable(realmID, category)
	local seedList = GameData.getEntities(SkillData.Farming.recipes,
		function(recipe)
			return recipe.categoryID == category.id and Skills.getRecipeRealm(recipe) == realmID
		end)
	if Shared.tableIsEmpty(seedList) then
		return ''
	end

	local skillID = 'Farming'

	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}) .. '<br>Level')
        	:tag('th'):wikitext('[[DLC]]')
        	: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("Produce")
			:tag('th'):wikitext('Healing')
			:tag('th'):wikitext('Produce<br>Value')
	else 
		html:tag('th'):attr("colspan", 2):wikitext("Produce")
			:tag('th'):wikitext('Produce<br>Value')
	end
	--html = html:tag('th'):wikitext('Seed Sources')

	table.sort(seedList, function(a, b) return Skills.standardRecipeSort(skillID, a, b) 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
			local level = Skills.getRecipeLevel(skillID, seed)
			local baseXP = seed.baseAbyssalExperience or seed.baseExperience
			local baseInt = seed.baseInterval
			local reqText = Skills.getRecipeRequirementText(SkillData.Farming.name, seed)

			html = 
			html:tag('tr')
				:tag('td'):wikitext(Icons.Icon({seedItem.name, type='item', notext=true}))
				:tag('td'):wikitext('[[' .. seedItem.name .. ']]')
				:tag('td'):wikitext(level)
				          :css('text-align', 'center')
				:tag('td'):wikitext(Icons.getDLCColumnIcon(seedItem.id))
						  :css('text-align', 'center')
						  :attr('data-sort-value', Icons.getExpansionID(seedItem.id))
				:tag('td'):wikitext(Num.formatnum(baseXP))
				:tag('td'):attr('data-sort-value', (baseInt / 1000))
						  :wikitext(Shared.timeString(baseInt / 1000, true))
				:tag('td'):attr('data-sort-value', seedItem.sellsFor)
					      :wikitext(Items.getValueText(seedItem))
				:tag('td'):wikitext(Icons.Icon({productItem.name, type='item', notext=true}))
				:tag('td'):wikitext('[[' .. productItem.name .. ']]')

			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(Items.getValueText(productItem))
				--:tag('td'):wikitext(ItemSourceTables._getItemSources(seedItem))
				--		  :css('text-align', 'left')
				:done()
		end
	end

	return tostring(tbl:done())
end

function p._getSlimFarmingTable(realmID, category)
	local seedList = GameData.getEntities(SkillData.Farming.recipes,
		function(recipe)
			return recipe.categoryID == category.id and Skills.getRecipeRealm(recipe) == realmID
		end)
	if Shared.tableIsEmpty(seedList) then
		return ''
	end

	local skillID = 'Farming'

	local tbl = mw.html.create()
	local html = tbl:tag("table")
        :addClass("wikitable sortable stickyHeader")
    	:tag('tr'):addClass("headerRow-0")
    		:tag('th'):wikitext(Icons.Icon({'Farming', type='skill', notext=true}) .. '<br>Level')
        	:tag('th'):attr("colspan", 2):wikitext("Seeds")
			:tag('th'):attr("colspan", 2):wikitext("Produce")
        	:tag('th'):wikitext('[[DLC]]')

	table.sort(seedList, function(a, b) return Skills.standardRecipeSort(skillID, a, b) 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
			local level = Skills.getRecipeLevel(skillID, seed)

			html = 
			html:tag('tr')
				:tag('td'):wikitext(level)
					      :css('text-align', 'center')
				:tag('td'):wikitext(Icons.Icon({seedItem.name, type='item', notext=true}))
				:tag('td'):wikitext('[[' .. seedItem.name .. ']]')
				:tag('td'):wikitext(Icons.Icon({productItem.name, type='item', notext=true}))
				:tag('td'):wikitext('[[' .. productItem.name .. ']]')
				:tag('td'):wikitext(Icons.getDLCColumnIcon(seedItem.id))
						  :css('text-align', 'center')
						  :attr('data-sort-value', Icons.getExpansionID(seedItem.id))
				:done()
		end
	end

	return tostring(tbl:done())
end

function p.getFarmingTable(frame)
	local args = frame.args ~= nil and frame.args or frame
	local realmName = args.realm
	local slim = args.slim == 'true' or args.slim == 'True'
	local realm = Skills.getRealmFromName(realmName)
	if realm == nil then
		return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
	end
	local categoryName = args[1]
	
	local category = GameData.getEntityByName(SkillData.Farming.categories, categoryName)
	if category == nil then
		return Shared.printError('Invalid farming category: ' .. categoryName .. '. Please choose Allotments, Herbs, Trees or Special')
	end

	if slim == true then 
		return p._getSlimFarmingTable(realm.id, category)
	else
		return p._getFarmingTable(realm.id, category)
	end
end

function p.getFarmingFoodTable(frame)
	local args = frame.args ~= nil and frame.args or frame
	local realmName = args.realm
	local realm = Skills.getRealmFromName(realmName)
	if realm == nil then
		return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
	end
	local skillID = 'Farming'
	
    local root = mw.html.create('table')
        :addClass('wikitable sortable stickyHeader')
    
    -- Header row
    local headerRow = root:tag('tr')
        :addClass('headerRow-0')
    headerRow:tag('th'):attr('colspan', '2'):wikitext('Crop')
    headerRow:tag('th'):wikitext(Icons.Icon({"Farming", type="skill", notext=true}) .. '<br>Level')
    headerRow:tag('th'):wikitext('[[DLC]]')
    headerRow:tag('th'):wikitext('Healing')
    headerRow:tag('th'):wikitext('Value')
    
    local recipes = GameData.getEntities(SkillData[skillID].recipes,
        function(recipe)
        	if Skills.getRecipeRealm(recipe) ~= realm.id then
        		return false
        	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 Skills.standardRecipeSort(skillID, a, b) end)
    
    for i, recipe in ipairs(recipes) do
        local product = Items.getItemByID(recipe.productId)
        if product and product.healsFor and product.healsFor > 0 then
            local row = root:tag('tr')
            row:tag('td'):wikitext(Icons.Icon({product.name, type='item', notext='true'}))
            row:tag('td'):wikitext('[[' .. product.name .. ']]')
            row:tag('td'):css('text-align', 'center')
            			 :wikitext(Skills.getRecipeLevel(skillID, recipe))
            row:tag('td'):css('text-align', 'center')
                    	 :attr('data-sort-value', Icons.getExpansionID(product.id))   
                		 :wikitext(Icons.getDLCColumnIcon(product.id))        			
            row:tag('td'):css('text-align', 'right')
                		 :attr('data-sort-value', product.healsFor)
                		 :wikitext(Icons.Icon({"Hitpoints", type="skill", notext=true}) .. ' ' .. (product.healsFor * 10))
            row:tag('td'):css('text-align', 'right')
                		 :attr('data-sort-value', product.sellsFor)
                		 :wikitext(Items.getValueText(product))
        end
    end
    
    return tostring(root)
end


function p.getFarmingPlotTable(frame)
    local skillID = 'Farming'
    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, Trees or Special')
    end
    
    local patches = GameData.getEntities(SkillData.Farming.plots,
        function(plot)
            return plot.categoryID == category.id
        end
    )
    
    table.sort(patches,
        function(a, b)
            local abyssA, abyssB = a.abyssalLevel or 0, b.abyssalLevel or 0
            if abyssA == abyssB then
                if a.level == b.level then
                    return a.id < b.id
                else
                    return a.level < b.level
                end
            else
                return abyssA < abyssB
            end
        end
    )
    
    if Shared.tableIsEmpty(patches) then
        return ''
    end
    
    local root = mw.html.create('table')
        :addClass('wikitable sortable stickyHeader')
    
    -- Header row
    local headerRow = root:tag('tr')
    headerRow:tag('th'):wikitext('Plot')
    headerRow:tag('th'):wikitext('Requirements')
    headerRow:tag('th'):wikitext('Cost')
    
    for i, patch in ipairs(patches) do
        local level = Skills.getRecipeLevel(skillID, patch)
        local reqText = Skills.getRecipeRequirementText(skillID, patch)
        local costText = Common.getCostString({ items = patch.itemCosts, currencies = patch.currencyCosts }, 'Free')
        local costVal = (patch.currencyCosts and patch.currencyCosts[1] and patch.currencyCosts[1].quantity) or 0
        
        local row = root:tag('tr')
        row:tag('td'):wikitext(i)
        row:tag('td'):css('text-align', 'right')
            :attr('data-sort-value', level)
            :wikitext(reqText)
        row:tag('td'):css('text-align', 'right')
            :attr('data-sort-value', costVal)
            :wikitext(costText)
    end
    
    return tostring(root)
end

function p._buildAstrologyConstellationTable(realmID)
    local modTypes = {
        {
            name = 'Standard',
            modKey = 'standardModifiers',
            levels = SkillData.Astrology.standardModifierLevels,
            inUse = false
        },
        {
            name = 'Unique',
            modKey = 'uniqueModifiers',
            levels = SkillData.Astrology.uniqueModifierLevels,
            inUse = false
        },
        {
            name = 'Abyssal',
            modKey = 'abyssalModifiers',
            levels = SkillData.Astrology.abyssalModifierLevels,
            inUse = false
        },
    }

    local recipes = GameData.getEntities(SkillData.Astrology.recipes,
        function(cons)
            return Skills.getRecipeRealm(cons) == realmID
        end
    )
    table.sort(recipes,
        function(a, b)
            return Skills.getRecipeLevel('Astrology', a) < Skills.getRecipeLevel('Astrology', b)
        end
    )

    -- Determine which mod types are in use
    for _, recipe in ipairs(recipes) do
        for _, modType in ipairs(modTypes) do
            if not modType.inUse then
                local recipeMods = recipe[modType.modKey]
                if recipeMods ~= nil and not Shared.tableIsEmpty(recipeMods) then
                    modType.inUse = true
                end
            end
        end
    end

    local root = mw.html.create()
    local tableRoot = root:tag('table')
    tableRoot:addClass('wikitable stickyHeader')

    -- Header rows
    local headerRow1 = tableRoot:tag('tr'):addClass('headerRow-0')
    headerRow1:tag('th'):attr('rowspan', 2)
    					:attr('colspan', 2)
    					:wikitext('Constellation')
    headerRow1:tag('th'):attr('rowspan', 2)
    					:wikitext(Icons.Icon({ "Astrology", type='skill', notext='true' }) .. '<br>Level')
    headerRow1:tag('th'):attr('rowspan', 2)
    					:wikitext('[[DLC]]')
    headerRow1:tag('th'):attr('rowspan', 2)
    					:wikitext('XP')
    headerRow1:tag('th'):attr('rowspan', 2)
    					:wikitext('Skills')

    local headerRow2 = tableRoot:tag('tr'):addClass('headerRow-1')
    for _, modType in ipairs(modTypes) do
        if modType.inUse then
        	local spanCount = modType.name == 'Abyssal' and 3 or 2
            headerRow1:tag('th'):attr('colspan', spanCount):wikitext(modType.name .. ' Stars')
            headerRow2:tag('th'):wikitext(Icons.Icon({'Mastery', notext=true}) .. '<br>Level')
            if modType.name == "Abyssal" then
            	headerRow2:tag('th'):wikitext('Level')
            end
            headerRow2:tag('th'):wikitext('Modifiers')
        end
    end

    -- Data rows
    for _, cons in ipairs(recipes) do
        local modData = Skills._getConstellationModifiers(cons)
        local name = cons.name
        local skillIconArray = {}
        for _, skillID in ipairs(cons.skillIDs) do
            table.insert(skillIconArray, Icons.Icon({Constants.getSkillName(skillID), type='skill'}))
        end

        -- Calculate maximum rows needed
        local maxRows = 1
        for _, modTypeData in pairs(modData) do
            maxRows = math.max(maxRows, Shared.tableCount(modTypeData))
        end

        -- Iterate through rows
        for rowIdx = 1, maxRows do
            local row = tableRoot:tag('tr')
            if rowIdx == 1 then
                row:tag('td'):attr('rowspan', maxRows)
                			 :wikitext(Icons.Icon({name, type='constellation', size=50, notext=true}))
                			 :css('text-align', 'center')
                row:tag('td'):attr('rowspan', maxRows)
                			 :wikitext(name)
                row:tag('td'):attr('rowspan', maxRows)
                			 :wikitext((cons.abyssalLevel or cons.level))
                			 :css('text-align', 'center')
            	row:tag('td'):css('text-align', 'center')
                    		 :attr('data-sort-value', Icons.getExpansionID(cons.id))   
                    		 :attr('rowspan', maxRows)
                			 :wikitext(Icons.getDLCColumnIcon(cons.id))   
                row:tag('td'):attr('rowspan', maxRows)
                		     :wikitext(Num.formatnum(cons.baseAbyssalExperience or cons.baseExperience))
                		     :css('text-align', 'right')
                row:tag('td'):attr('rowspan', maxRows)
                			 :wikitext(table.concat(skillIconArray, '<br/>'))
                			 :css('text-wrap', 'nowrap')
            end

            -- Modifiers data
            for _, modType in ipairs(modTypes) do
                if modType.inUse then
                    local masteryLevel = modType.levels[rowIdx]
                    local rowModData = modData[modType.modKey][rowIdx]
                    local rowConsData = cons[modType.modKey][rowIdx]
                    local cell1 = row:tag('td')
                    if modType.name == "Abyssal" then
                    	local starUnlockReq = nil
                    	local cell2 = row:tag('td')
                    	if rowConsData ~= nil and rowConsData.unlockRequirements ~= nil and Shared.tableCount(rowConsData.unlockRequirements) == 2 then
                    		starUnlockReq = Common.getRequirementString({cons[modType.modKey][rowIdx].unlockRequirements[2]}, nil)
                    	end
                    	if starUnlockReq ~= nil then
                    		cell2:wikitext(starUnlockReq)
                    			 :css('text-align', 'right')
                    	else
                    		cell2:wikitext('N/A')
                    			 :addClass('table-na')
                    	end
                    end
                    local cell3 = row:tag('td')
                    if masteryLevel ~= nil and rowModData ~= nil then
						local modText = {}
                        for _, modKey in ipairs({'modifiers', 'enemyModifiers'}) do
                        	local mods = rowModData[modKey] 
                        	if mods ~= nil then
                        		if modKey == 'enemyModifiers' then
                        			table.insert(modText, 'Gives the enemy:')
                        		end
                        		table.insert(modText, Modifiers.getModifiersText(mods, false, false, 10))
                        	end
                        end
                        cell1:wikitext(masteryLevel)
                        	 :css('text-align', 'right')
                        cell3:wikitext(table.concat(modText, '<br>'))
                    else
                        cell1:attr('colspan', 2)
                        	 :wikitext('N/A')
                        	 :addClass('table-na')
                    end
                end
            end
        end
    end

	return tostring(root)
end

function p.buildAstrologyConstellationTable(frame)
	local args = frame.args ~= nil and frame.args or frame
	local realmName = args.realm
	local realm = Skills.getRealmFromName(realmName)
	if realm == nil then
		return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
	end

	return p._buildAstrologyConstellationTable(realm.id)
end

return p