Module:Skills/Gathering: Difference between revisions

From Melvor Idle
(_buildAstrologyConstellationTable: Remove time column, which has been relocated to Astrology/Training)
m (Replaced Thieving npc.name link with an icon link)
 
(68 intermediate revisions by 4 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 Skills = require('Module:Skills')
local ItemSourceTables = require('Module:Items/SourceTables')
local ItemSourceTables = require('Module:Items/SourceTables')
local Num = require("Module:Number")


function p.getAxeTable(frame)
local function lootValueText(lootValue)
local toolArray = {}
local returnPart = {}
for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
for _, currencyDefn in ipairs(GameData.rawData.currencies) do
if Shared.contains(upgrade.name, 'Axe') then
-- Guarantee order by iterating through currency game data definition
table.insert(toolArray, upgrade)
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
end
end
 
return table.concat(returnPart, ', ')
local result = '{| class="wikitable"'
result = result..'\r\n!colspan="4"| !!colspan="2"|Cut Time Decrease'
result = result..'\r\n|- class="headerRow-0"'
result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Woodcutting', type='skill', notext=true})..' Level'
result = result..'!!Cost!!This Axe!!Total'
 
local total = 0
 
for i, tool in Shared.skpairs(toolArray) do
result = result..'\r\n|-'
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
result = result..'||'..tool.name
local level = 1
if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil then
--Gonna be lazy and assume there's only the one skill level and it's the one we care about
level = tool.unlockRequirements.skillLevel[1][2]
end
result = result..'||style="text-align:right"|'..level
result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)
 
local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
total = total + cutTime
result = result..'||style="text-align:right"|-'..cutTime..'%'
result = result..'||style="text-align:right"|-'..total..'%'
end
 
result = result..'\r\n|}'
return result
end
end


function p.getPickaxeTable(frame)
function p.getTreesTable(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 skillID = 'Woodcutting'


local result = '{| class="wikitable"'
local html = mw.html.create('table')
result = result..'\r\n!colspan="4"| !!colspan="2"|Mine Time Decrease!!colspan="2"|2x Ore Chance'
:addClass('wikitable sortable stickyHeader')
result = result..'\r\n|- class="headerRow-0"'
result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Mining', type='skill', notext=true})..' Level'
html:tag('tr')
result = result..'!!Cost!!This Pick!!Total!!This Pick!!Total'
: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 total = 0
local trees = GameData.getEntities(SkillData.Woodcutting.trees,
local total2 = 0
function(tree)
 
return Skills.getRecipeRealm(tree) == realm.id
for i, tool in Shared.skpairs(toolArray) do
result = result..'\r\n|-'
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
result = result..'||'..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}))
 
          :addClass('table-img')
result = result..'||style="text-align:right"|-'..cutTime..'%'
          :attr('data-sort-value', tree.name)
result = result..'||style="text-align:right"|-'..total..'%'
:tag('td'):wikitext(tree.name)
 
:tag('td'):wikitext(Icons.Icon({log.name, type='item', notext=true}))
local OreDouble = tool.contains.modifiers.increasedChanceToDoubleOres
  :addClass('table-img')
total2 = total2 + OreDouble
          :attr('data-sort-value', log.name)
 
:tag('td'):wikitext('[[' .. log.name .. ']]')
result = result..'||style="text-align:right"|+'..OreDouble..'%'
:tag('td'):wikitext(reqText)
result = result..'||style="text-align:right"|+'..total2..'%'
          :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.getRodTable(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, 'Fishing Rod') 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
 
local result = '{| class="wikitable"'
result = result..'\r\n!colspan="4"| !!colspan="2"|Catch Time Decrease'
result = result..'\r\n|- class="headerRow-0"'
result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Fishing', type='skill', notext=true})..' Level'
result = result..'!!Cost!!This Rod!!Total'
 
local total = 0
 
for i, tool in Shared.skpairs(toolArray) do
result = result..'\r\n|-'
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
result = result..'||'..tool.name
local level = 1
if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil then
--Gonna be lazy and assume there's only the one skill level and it's the one we care about
level = tool.unlockRequirements.skillLevel[1][2]
end
result = result..'||style="text-align:right"|'..level
result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)
 
local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
total = total + cutTime
result = result..'||style="text-align:right"|-'..cutTime..'%'
result = result..'||style="text-align:right"|-'..total..'%'
end
end


result = result..'\r\n|}'
local totalWt, lootValue = 0, {}
return result
local realmSpecials = GameData.getEntityByProperty(SkillData.Fishing.specialItems, 'realmID', realm.id)
end
if realmSpecials == nil then
 
return ''
function p.getTreesTable(frame)
local result = '{| class="wikitable sortable"'
result = result..'\r\n|- class="headerRow-0"'
result = result..'\r\n!colspan="2"|Tree!!colspan="2"|Logs!!'..Icons.Icon({'Woodcutting', type='skill', notext=true})..' Level'
result = result..'!!XP!!Cut Time!!XP/s!!GP/s'
 
for i, tree in Shared.skpairs(SkillData.Woodcutting.Trees) do
result = result..'\r\n|-'
local treeName = Shared.titleCase(tree.type..' tree')
local logName = Shared.titleCase(tree.type..' logs')
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..''
result = result..'||style="min-width:25px" data-sort-value="'..logName..'"|'..Icons.Icon({logName, type='item', notext=true, size=50})
result = result..'||'..Icons.Icon({logName, type='item', noicon=true})
result = result..'||style="text-align:right"|'..tree.levelRequired
result = result..'||style="text-align:right"|'..tree.baseExperience
result = result..'||style="text-align:right" data-sort-value="'..tree.baseInterval..'"|'..Shared.timeString(tree.baseInterval/1000, true)
local XPs = tree.baseExperience / (tree.baseInterval / 1000)
local Log = Items.getItemByID(tree.logID)
local GPs = Log.sellsFor / (tree.baseInterval / 1000)
result = result..'||style="text-align:right"|'..Shared.round(XPs, 2, 2)
result = result..'||style="text-align:right" data-sort-value="'..GPs..'"|'..Icons.GP(Shared.round(GPs, 2, 2))
end
end
 
local itemArray = realmSpecials.drops
result = result..'\r\n|}'
return result
end
 
function p.getSpecialFishingTable(frame)
local totalWt, lootValue = 0, 0
local itemArray = Shared.clone(SkillData.Fishing.SpecialItems)
for i, itemDef in ipairs(itemArray) do
for i, itemDef in ipairs(itemArray) do
totalWt = totalWt + itemDef[2]
totalWt = totalWt + itemDef.weight
end
end
-- Sort the loot table by weight in descending order
-- Sort the loot table by weight in descending order
table.sort(itemArray, function(a, b) return (a[2] == b[2] and a[1] < b[1]) or a[2] > b[2] end)
table.sort(itemArray, function(a, b) return a.weight > b.weight end)


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


function p.getFishingJunkTable(frame)
function p.getFishingJunkTable(frame)
local resultPart = {}
local html = mw.html.create('table')
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
html:addClass('wikitable sortable stickyHeader')
table.insert(resultPart, '\r\n|- class="headerRow-0"')
 
table.insert(resultPart, '\r\n!colspan="2"|Item!!Value')
-- 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 = {}
local itemArray = {}
for i, itemID in ipairs(SkillData.Fishing.JunkItems) do
for i, itemID in ipairs(SkillData.Fishing.junkItemIDs) do
local item = Items.getItemByID(itemID)
local item = Items.getItemByID(itemID)
if item ~= nil then
if item ~= nil then
Line 207: Line 181:
table.sort(itemArray, function(a, b) return a.name < b.name end)
table.sort(itemArray, function(a, b) return a.name < b.name end)


-- Add item rows
for i, item in ipairs(itemArray) do
for i, item in ipairs(itemArray) do
table.insert(resultPart, '\r\n|-')
local row = html:tag('tr')
table.insert(resultPart, '\r\n|style="min-width:25px"| ' .. Icons.Icon({item.name, type='item', notext=true, size=50}))
row:tag('td'):addClass('table-img')
table.insert(resultPart, '\r\n| ' .. Icons.Icon({item.name, type='item', noicon=true}))
:wikitext(Icons.Icon({item.name, type='item', notext=true}))
table.insert(resultPart, '\r\n|data-sort-value="' .. item.sellsFor .. '"| ' .. Icons.GP(math.floor(item.sellsFor)))
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
table.insert(resultPart, '\r\n|}')
 
return table.concat(resultPart)
return tostring(html)
end
end


function p.getMiningOresTable(frame)
function p.getMiningOresTable(frame)
local result = '{|class="wikitable sortable stickyHeader"'
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|Ore!!'..Icons.Icon({'Mining', type='skill', notext=true})..' Level'
local realm = Skills.getRealmFromName(realmName)
result = result..'!!XP!!Respawn Time!!Ore Value'
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 = Shared.clone(SkillData.Mining.Rocks)
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


table.sort(mineData, function(a, b) return a.levelRequired < b.levelRequired end)
return tostring(html)
end


for i, oreData in Shared.skpairs(mineData) do
function p._getMiningGemsTable(gemType)
local ore = Items.getItemByID(oreData.oreID)
if type(gemType) ~= 'string' then
result = result..'\r\n|-\r\n|style="min-width:25px"|'..Icons.Icon({ore.name, type='item', size='50', notext=true})
gemType = 'Normal'
result = result..'||'..Icons.Icon({ore.name, type='item', noicon=true})
result = result..'||style="text-align:right"|'..oreData.levelRequired..'||style="text-align:right"|'..oreData.baseExperience
result = result..'||style="text-align:right" data-sort-value="'..oreData.baseRespawnInterval..'"|'
result = result..Shared.timeString(oreData.baseRespawnInterval / 1000, true)
result = result..'||data-sort-value="'..ore.sellsFor..'"|'..Icons.GP(ore.sellsFor)
end
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')


result = result..'\r\n|}'
-- Add header row
return result
local headerRow = html:tag('tr'):addClass('headerRow-0')
end
:tag('th'):wikitext('Gem')
  :attr('colspan', '2')
:tag('th'):wikitext('[[DLC]]')
:tag('th'):wikitext('Gem Chance')
:tag('th'):wikitext('Gem Price')


function p.getMiningGemsTable(frame)
-- Add gem rows
local result = '{|class="wikitable sortable stickyHeader"'
for i, gem in ipairs(gemData) do
result = result..'\r\n|- class="headerRow-0"'
local gemItem = Items.getItemByID(gem.itemID)
result = result..'\r\n!colspan=2|Gem!!Gem Chance!!Gem Price'
local gemPct = gem.weight / totalWeight * 100
local row = html:tag('tr')


-- Sort gems by ID order
row:tag('td'):addClass('table-img')
for i, gemData in Shared.spairs(Items.GemTable, function(t,a,b) return t[a].id < t[b].id end) do
:wikitext(Icons.Icon({gemItem.name, type='item', notext=true}))
local gem = Items.getItemByID(gemData.id)
row:tag('td'):attr('data-sort-value', gemItem.name)
result = result..'\r\n|-\r\n|style="min-width:25px"|'
:wikitext('[[' .. gemItem.name ..']]')
result = result..Icons.Icon({gem.name, type='item', size='50', notext=true})
row:tag('td'):wikitext(Icons.getDLCColumnIcon(gemItem.id))
result = result..'||'..Icons.Icon({gem.name, type='item', noicon=true})
:attr('data-sort-value', Icons.getExpansionID(gemItem.id))
result = result..'||style="text-align:right"|'..string.format("%.1f%%", gemData.chance)
:css('text-align', 'center')
result = result..'||data-sort-value="'..gem.sellsFor..'"|'..Icons.GP(gem.sellsFor)
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|}'
return tostring(html)
return result
end
 
function p.getMiningGemsTable(frame)
local gemType = frame.args ~= nil and frame.args[1] or frame
return p._getMiningGemsTable(gemType)
end
end


function p.getFishTable(frame)
function p.getFishTable(frame)
local recipeList = {}
local args = frame.args ~= nil and frame.args or frame
for i, recipe in ipairs(SkillData.Fishing.Fish) do
local realmName = args.realm
table.insert(recipeList, recipe)
local realm = Skills.getRealmFromName(realmName)
if realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
end
table.sort(recipeList, function(a, b) return (a.level == b.level and a.masteryID < b.masteryID) or a.level < b.level 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
-- Determine cooking levels for all fish
local cookReq = {}
local cookRecipes = {}
for i, recipe in ipairs(SkillData.Cooking.Recipes) do
for i, recipe in ipairs(SkillData.Cooking.recipes) do
-- This assumes that each raw fish only appears in a single recipe, which is a bit rubbish
-- This assumes that each raw fish only appears in a single recipe, which is a bit rubbish
-- but currently holds
-- but currently holds
for j, mat in ipairs(recipe.itemCosts) do
for j, mat in ipairs(recipe.itemCosts) do
if cookReq[mat.id] == nil then
if cookRecipes[mat.id] == nil then
cookReq[mat.id] = recipe.level
cookRecipes[mat.id] = recipe
end
end
end
end
end
end


local resultPart = {}
local html = mw.html.create('table')
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
html:addClass('wikitable sortable stickyHeader')
table.insert(resultPart, '\r\n|- class="headerRow-0"')
 
table.insert(resultPart, '\r\n!Fish\r\n!Name\r\n!' .. Icons.Icon({'Fishing', type='skill', notext=true}) .. ' Level\r\n!Catch Time')
-- Add header row
table.insert(resultPart, '\r\n!XP\r\n!Value\r\n!XP/s\r\n!GP/s')
local headerRow0 = html:tag('tr'):addClass('headerRow-0')
table.insert(resultPart, '\r\n!' .. Icons.Icon({'Cooking', type='skill', notext=true}) .. ' Level')
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
for i, recipe in ipairs(recipeList) do
local fish = Items.getItemByID(recipe.itemID)
local fish = Items.getItemByID(recipe.productId)
if fish ~= nil then
if fish ~= nil then
local timeMin, timeMax = recipe.baseMinInterval / 1000, recipe.baseMaxInterval / 1000
local timeAvg = (timeMin + timeMax) / 2
local timeSortVal = (recipe.baseMinInterval + recipe.baseMaxInterval) / 2000
local timeSortVal = (recipe.baseMinInterval + recipe.baseMaxInterval) / 2000
local timeStr = string.format("%.1fs - %.1fs", recipe.baseMinInterval / 1000, recipe.baseMaxInterval / 1000)
local level = Skills.getRecipeLevel(skillID, recipe)
local XPs = recipe.baseXP / timeSortVal
local reqText = Skills.getRecipeRequirementText(skillID, recipe)
local GPs = fish.sellsFor / timeSortVal
local baseXP = recipe.baseAbyssalExperience or recipe.baseExperience
local cookSortVal = cookReq[recipe.itemID] or 0
local XPSec = baseXP / timeSortVal
local cookStr = cookReq[recipe.itemID] or 'N/A'
local sellCurrency = fish.sellsForCurrency or 'melvorD:GP'
table.insert(resultPart, '\r\n|-')
local GPSec = fish.sellsFor / timeSortVal
table.insert(resultPart, '\r\n|style="text-align:center"| ' .. Icons.Icon({fish.name, type='item', size='50', notext=true}))
 
table.insert(resultPart, '\r\n| ' .. Icons.Icon({fish.name, type='item', noicon=true}))
local row = html:tag('tr')
table.insert(resultPart, '\r\n|style="text-align:right"| ' .. recipe.level)
row:tag('td'):wikitext(Icons.Icon({fish.name, type='item', notext=true}))
table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. timeSortVal .. '"| ' .. timeStr)
:addClass('table-img')
table.insert(resultPart, '\r\n|style="text-align:right"| ' .. recipe.baseXP)
row:tag('td'):wikitext('[[' .. fish.name .. ']]')
table.insert(resultPart, '\r\n|data-sort-value="' .. fish.sellsFor .. '"| ' .. Icons.GP(fish.sellsFor))
:attr('data-sort-value', fish.name)
table.insert(resultPart, '\r\n|style="text-align:right"| ' .. Shared.round(XPs, 2, 2))
row:tag('td'):wikitext(level)
table.insert(resultPart, '\r\n|data-sort-value="' .. GPs .. '"|' .. Icons.GP(Shared.round(GPs, 2, 2)))
:css('text-align', 'center')
table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="' .. cookSortVal .. '"| ' .. cookStr)
: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
end
table.insert(resultPart, '\r\n|}')
 
return table.concat(resultPart)
return tostring(html)
end
end


function p.getFishingAreasTable(frame)
function p.getFishingAreasTable(frame)
local result = '{| class="wikitable sortable stickyHeader"'
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!Name\r\n!Fish\r\n!Fish Chance'
local realm = Skills.getRealmFromName(realmName)
result = result..'\r\n!Junk Chance\r\n!Special Chance'
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')


for i, area in ipairs(SkillData.Fishing.Areas) do
-- Get fishing areas
result = result..'\r\n|-'
local fishAreas = GameData.getEntities(SkillData.Fishing.areas, function(obj)
result = result..'\r\n| style ="text-align: left;" |'..area.name
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')
:attr('id', area.name)
:wikitext(area.name)


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


result = result..'\r\n| style="text-align:right"|'..area.fishChance..'%'
return tostring(html)
result = result..'\r\n| style="text-align:right"|'..area.junkChance..'%'
end
result = result..'\r\n| style="text-align:right"|'..area.specialChance..'%'
 
 
function p.getThievingGeneralRareTable(frame)
return p._getThievingGeneralRareTable()
end
 
function p._getThievingGeneralRareTable(npc)
local npcRealm = nil
if npc ~= nil then
npcRealm = Skills.getRecipeRealm(npc)
end
end


result = result..'\r\n|}'
local html = mw.html.create('table')
return result
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
end


function p.getThievingGeneralRareTable(frame)
local rareTxt = '{|class="wikitable sortable"'
rareTxt = rareTxt..'\r\n!Item!!Qty'
rareTxt = rareTxt..'!!Price!!colspan="2"|Chance'
for i, drop in pairs(SkillData.Thieving.RareItems) do
local thisItem = Items.getItemByID(drop.itemID)
local odds = drop.chance


rareTxt = rareTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
function p._getThievingNPCCurrencyText(npc)
rareTxt = rareTxt..'||1||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
local currTextPart = {}
rareTxt = rareTxt..'||style="text-align:right" data-sort-value="'..odds..'"|'..Shared.fraction(1, Shared.round2(1/(odds/100), 0))
for _, currencyDrop in ipairs(npc.currencyDrops) do
rareTxt = rareTxt..'||style="text-align:right" data-sort-value="'..odds..'"|'..Shared.round(odds, 4, 4)..'%'
table.insert(currTextPart, Icons._Currency(currencyDrop.id, 1, currencyDrop.quantity))
end
end
rareTxt = rareTxt..'\r\n|}'
return table.concat(currTextPart, ', ')
return rareTxt
end
end


Line 359: Line 566:
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:
Line 370: Line 576:
table.insert(normalTxt, '===Possible Common Drops:===\r\nUp to one of these will be received on a successful pickpocket:')
table.insert(normalTxt, '===Possible Common Drops:===\r\nUp to one of these will be received on a successful pickpocket:')
local totalWt = 0
local totalWt = 0
local lootChance = SkillData.Thieving.ItemChance
local lootChance = SkillData.Thieving.itemChance
local lootValue = 0
local lootValue = {}


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


Line 382: Line 588:
table.insert(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
table.insert(normalTxt, '\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'}))
table.insert(normalTxt, '\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'}))
Line 392: Line 598:
table.insert(normalTxt, '\r\n|-\r\n|Unknown Item[[Category:Pages with script errors]]')
table.insert(normalTxt, '\r\n|-\r\n|Unknown Item[[Category:Pages with script errors]]')
end
end
table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..maxQty..'"|')
table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..(loot.minQuantity + loot.maxQuantity)..'"|')


if maxQty > 1 then
if loot.minQuantity ~= loot.maxQuantity then
table.insert(normalTxt, '1 - ')
table.insert(normalTxt, Num.formatnum(loot.minQuantity) .. ' - ' .. Num.formatnum(loot.maxQuantity))
else
table.insert(normalTxt, Num.formatnum(loot.maxQuantity))
end
end
table.insert(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
table.insert(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'
table.insert(normalTxt, '||'..Icons.GP(itemPrice))
table.insert(normalTxt, '||' .. Items.getValueText(thisItem, loot.minQuantity, loot.maxQuantity))
else
table.insert(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
table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..row[2]..'"')
table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..loot.weight..'"')
table.insert(normalTxt, '|'..Shared.fraction(row[2] * lootChance, totalWt * 100))
table.insert(normalTxt, '|'..Num.fraction(loot.weight * lootChance, totalWt * 100))
table.insert(normalTxt, '||')
table.insert(normalTxt, '||')
else
else
table.insert(normalTxt, '||colspan="2" data-sort-value="'..row[2]..'"')
table.insert(normalTxt, '||colspan="2" data-sort-value="'..loot.weight..'"')
end
end
table.insert(normalTxt, 'style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%')
table.insert(normalTxt, 'style="text-align:right"|'..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 Shared.tableCount(npc.lootTable) > 1 then
if Shared.tableCount(npc.lootTable) > 1 then
table.insert(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
table.insert(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
table.insert(normalTxt, '\r\n|colspan="2" ')
table.insert(normalTxt, '\r\n|colspan="2" ')
end
end
table.insert(normalTxt, 'style="text-align:right"|'..Shared.round(lootChance, 2, 2)..'%')
table.insert(normalTxt, 'style="text-align:right"|'..Num.round(lootChance, 2, 2)..'%')
end
end
table.insert(normalTxt, '\r\n|}')
table.insert(normalTxt, '\r\n|}')
table.insert(normalTxt, '\r\nThe loot obtained from the average successful pickocket is worth ' .. Icons.GP(Shared.round(lootValue, 2, 2)) .. ' if sold.')
 
table.insert(normalTxt, '\r\n\r\nIncluding GP, the average successful pickocket is worth ' .. Icons.GP(Shared.round(lootValue + (1 + npc.maxGP) / 2, 2, 2)) .. '.')
table.insert(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))
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)


Line 456: Line 672:
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..'||data-sort-value="'..drop.qty..'"| '..Shared.formatnum(drop.qty)..'||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
lineTxt = lineTxt..'||data-sort-value="'..drop.quantity..'"| '..Num.formatnum(drop.quantity)..'||data-sort-value="'..thisItem.sellsFor..'"|'..Items.getValueText(thisItem)
lineTxt = lineTxt..'||style="text-align:right"|'..Shared.fraction(1, 1/(SkillData.Thieving.AreaUniqueChance/100))
lineTxt = lineTxt..'||style="text-align:right"|'..Num.fraction(1, 1/(SkillData.Thieving.baseAreaUniqueChance/100))
lineTxt = lineTxt..'||'..Shared.round(SkillData.Thieving.AreaUniqueChance, 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/(SkillData.Thieving.AreaUniqueChance/100))..'||'
areaTxt = areaTxt..'style="text-align:right"|'..Shared.round(SkillData.Thieving.AreaUniqueChance, 2, 2)..'%'
areaTxt = areaTxt..'\r\n|}'
areaTxt = areaTxt..'\r\n|}'
table.insert(sectionTxt, areaTxt)
table.insert(sectionTxt, areaTxt)


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


Line 498: Line 711:
local npc = Skills.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


Line 504: Line 717:
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!!GP!!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(Icons.Icon({npc.name, type='thieving', noicon=true}))
        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)
        :attr('id', 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 area = Skills.getThievingNPCArea(npc)
local tbl = mw.html.create()
result = result..'||'..area.name
local html = tbl:tag("table")
result = result..'||data-sort-value="' .. npc.level .. '"| '..Icons._SkillReq('Thieving', npc.level)
        :addClass("wikitable sortable stickyHeader")
result = result..'||style="text-align:right"|'..npc.xp
    :tag('tr'):addClass("headerRow-0")
result = result..'||style="text-align:right"|'..(npc.maxHit * 10)
        :tag('th'):attr("colspan", 2):wikitext("Seeds")
result = result..'||style="text-align:right"|'..npc.perception
        :tag('th'):wikitext(Icons.Icon({'Farming', type='skill', notext=true}) .. '<br>Level')
result = result..'||data-sort-value="' .. npc.maxGP .. '"|'..Icons.GP(1, npc.maxGP)
        :tag('th'):wikitext('[[DLC]]')
if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID > -1 then
        :tag('th'):wikitext('XP')
local uniqueDrop = Items.getItemByID(npc.uniqueDrop.itemID)
        :tag('th'):wikitext('Growth Time')
if npc.uniqueDrop.qty > 1 then
        :tag('th'):wikitext('Seed Value')
result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item', qty = npc.uniqueDrop.qty})
 
else
if category.id == 'melvorD:Allotment' then
result = result..'||data-sort-value="'..uniqueDrop.name..'"|'..Icons.Icon({uniqueDrop.name, type='item'})
html:tag('th'):attr("colspan", 2):wikitext("Produce")
end
:tag('th'):wikitext('Healing')
else
:tag('th'):wikitext('Produce<br>Value')
result = result..'|| '
else
end
html:tag('th'):attr("colspan", 2):wikitext("Produce")
:tag('th'):wikitext('Produce<br>Value')
end
end
result = result..'\r\n|}'
--html = html:tag('th'):wikitext('Seed Sources')


return result
table.sort(seedList, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
end


function p.getThievingAreaTable(frame)
for i, seed in ipairs(seedList) do
local resultPart = {}
local seedItem = Items.getItemByID(seed.seedCost.id)
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
local productItem = Items.getItemByID(seed.productId)
table.insert(resultPart, '\r\n|- class="headerRow-0"')
if seedItem ~= nil and productItem ~= nil then
table.insert(resultPart, '\r\n!Area!!'..Icons.Icon({'Thieving', type='skill', notext=true})..' Level!!NPCs!!Unique Drops')
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)


local areaArray = Shared.clone(SkillData.Thieving.Areas)
html =  
table.sort(areaArray, function(a, b) return a.id < b.id end)
html:tag('tr')
for i, area in ipairs(areaArray) do
:tag('td'):wikitext(Icons.Icon({seedItem.name, type='item', notext=true}))
local minLevel, npcList, areaItemList = nil, {}, {}
:tag('td'):wikitext('[[' .. seedItem.name .. ']]')
-- Build NPC list & determine level for area, this is the minimum
:tag('td'):wikitext(level)
-- Thieving level required for all NPCs within that area
          :css('text-align', 'center')
if area.npcs ~= nil and Shared.tableCount(area.npcs) > 0 then
:tag('td'):wikitext(Icons.getDLCColumnIcon(seedItem.id))
for j, npcID in ipairs(area.npcs) do
  :css('text-align', 'center')
-- Don't bother cloning the NPC below since we aren't modifying any part of it
  :attr('data-sort-value', Icons.getExpansionID(seedItem.id))
local npc = SkillData.Thieving.NPCs[npcID + 1]
:tag('td'):wikitext(Num.formatnum(baseXP))
if minLevel == nil or npc.level < minLevel then
:tag('td'):attr('data-sort-value', (baseInt / 1000))
minLevel = npc.level
  :wikitext(Shared.timeString(baseInt / 1000, true))
end
:tag('td'):attr('data-sort-value', seedItem.sellsFor)
table.insert(npcList, Icons.Icon({npc.name, type='thieving'}))
      :wikitext(Items.getValueText(seedItem))
end
:tag('td'):wikitext(Icons.Icon({productItem.name, type='item', notext=true}))
else
:tag('td'):wikitext('[[' .. productItem.name .. ']]')
table.insert(npcList, '')
end


-- Build area unique item list
if category.id == 'melvorD:Allotment' then
if area.uniqueDrops ~= nil and Shared.tableCount(area.uniqueDrops) > 0 then
html:tag('td'):wikitext(Icons.Icon({'Hitpoints', type='skill', notext=true}))
for k, drop in ipairs(area.uniqueDrops) do
  :wikitext(' ')
local areaItem = Items.getItemByID(drop.itemID)
  :wikitext(((productItem.healsFor or 0) * 10))
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
end
else
html =
table.insert(areaItemList, '')
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
-- 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 table.concat(resultPart)
return tostring(tbl:done())
end
end


function p._getFarmingTable(category)
function p._getSlimFarmingTable(realmID, category)
local seedList = {}
local seedList = GameData.getEntities(SkillData.Farming.recipes,
if category == 'Allotment' or category == 'Herb' or category == 'Tree' then
function(recipe)
seedList = Items.getItems(function(item) return item.tier == category end)
return recipe.categoryID == category.id and Skills.getRecipeRealm(recipe) == realmID
else
end)
return 'ERROR: Invalid farming category. Please choose Allotment, Herb, or Tree'
if Shared.tableIsEmpty(seedList) then
return ''
end
end


local result = '{|class="wikitable sortable stickyHeader"'
local skillID = 'Farming'
result = result..'\r\n|- class="headerRow-0"'
 
result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
local tbl = mw.html.create()
result = result..'!!XP!!Growth Time!!Seed Value'
local html = tbl:tag("table")
if category == 'Allotment' then
        :addClass("wikitable sortable stickyHeader")
result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
    :tag('tr'):addClass("headerRow-0")
elseif category == 'Herb' then
    :tag('th'):wikitext(Icons.Icon({'Farming', type='skill', notext=true}) .. '<br>Level')
result = result..'!!colspan="2"|Herb!!Herb Value'
        :tag('th'):attr("colspan", 2):wikitext("Seeds")
elseif category == 'Tree' then
:tag('th'):attr("colspan", 2):wikitext("Produce")
result = result..'!!colspan="2"|Logs!!Log Value'
        :tag('th'):wikitext('[[DLC]]')
end
result = result..'!!Seed Sources'


table.sort(seedList, function(a, b) return a.farmingLevel < b.farmingLevel end)
table.sort(seedList, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)


for i, seed in pairs(seedList) do
for i, seed in ipairs(seedList) do
result = result..'\r\n|-'
local seedItem = Items.getItemByID(seed.seedCost.id)
result = result..'\r\n|'..Icons.Icon({seed.name, type='item', size='50', notext=true})..'||[['..seed.name..']]'
local productItem = Items.getItemByID(seed.productId)
result = result..'||'..seed.farmingLevel..'||'..Shared.formatnum(seed.farmingXP)
if seedItem ~= nil and productItem ~= nil then
result = result..'||data-sort-value="'..seed.timeToGrow..'"|'..Shared.timeString(seed.timeToGrow, true)
local level = Skills.getRecipeLevel(skillID, seed)
result = result..'||data-sort-value="'..seed.sellsFor..'"|'..Icons.GP(seed.sellsFor)


local crop = Items.getItemByID(seed.grownItemID)
html =  
result = result..'||'..Icons.Icon({crop.name, type='item', size='50', notext=true})..'||[['..crop.name..']]'
html:tag('tr')
if category == 'Allotment' then
:tag('td'):wikitext(level)
result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..(crop.healsFor * 10)
      :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
result = result..'||data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
result = result..'||'..ItemSourceTables._getItemSources(seed)
end
end


result = result..'\r\n|}'
return tostring(tbl:done())
return result
end
end


function p.getFarmingTable(frame)
function p.getFarmingTable(frame)
local category = frame.args ~= nil and frame.args[1] or 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


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


function p.getFarmingFoodTable(frame)
function p.getFarmingFoodTable(frame)
local result = '{| class="wikitable sortable stickyHeader"'
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"|Crop!!'..Icons.Icon({"Farming", type="skill", notext=true})..' Level'
local realm = Skills.getRealmFromName(realmName)
result = result..'!!Healing!!Value'
if realm == nil then
 
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
local itemArray = Items.getItems(function(item) return item.grownItemID ~= nil end)
 
table.sort(itemArray, function(a, b) return a.farmingLevel < b.farmingLevel end)
 
for i, item in Shared.skpairs(itemArray) do
local crop = Items.getItemByID(item.grownItemID)
if crop.healsFor ~= nil and crop.healsFor > 0 then
result = result..'\r\n|-'
result = result..'\r\n|'..Icons.Icon({crop.name, type='item', notext='true', size='50'})..'||[['..crop.name..']]'
result = result..'||style="text-align:right;"|'..item.farmingLevel
result = result..'||style="text-align:right" data-sort-value="'..crop.healsFor..'"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..(crop.healsFor * 10)
result = result..'||style="text-align:right" data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
end
end
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


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


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


function p.getFarmingPlotTable(frame)
function p._buildAstrologyConstellationTable(realmID)
local areaName = frame.args ~= nil and frame.args[1] or frame
    local modTypes = {
local patches = nil
        {
for i, area in Shared.skpairs(SkillData.Farming.Patches) do
            name = 'Standard',
if area.areaName == areaName then
            modKey = 'standardModifiers',
patches = area.patches
            levels = SkillData.Astrology.standardModifierLevels,
break
            inUse = false
end
        },
end
        {
if patches == nil then
            name = 'Unique',
return "ERROR: Invalid area name.[[Category:Pages with script errors]]"
            modKey = 'uniqueModifiers',
end
            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
    )


local result = '{|class="wikitable"'
    -- Determine which mod types are in use
result = result..'\r\n!Plot!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level!!Cost'
    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


for i, patch in Shared.skpairs(patches) do
    local root = mw.html.create()
result = result..'\r\n|-\r\n|'..i
    local tableRoot = root:tag('table')
result = result..'||style="text-align:right;" data-sort-value="' .. patch.level .. '"|'..patch.level
    tableRoot:addClass('wikitable stickyHeader')
if patch.cost == 0 then
result = result..'||Free'
else
result = result..'||style="text-align:right;" data-sort-value="'..patch.cost..'"|'..Icons.GP(patch.cost)
end
end


result = result..'\r\n|}'
    -- Header rows
return result
    local headerRow1 = tableRoot:tag('tr'):addClass('headerRow-0')
end
    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')


function p._buildAstrologyConstellationTable()
    local headerRow2 = tableRoot:tag('tr'):addClass('headerRow-1')
local maxModifier = 5
    for _, modType in ipairs(modTypes) do
local result = '{|class="wikitable sortable stickyHeader"'
        if modType.inUse then
result = result..'\r\n|- class="headerRow-0"'
        local spanCount = modType.name == 'Abyssal' and 3 or 2
result = result..'\r\n!colspan="2"|Constellation!!'..Icons.Icon({"Astrology", type='skill', notext='true'})..' Level'
            headerRow1:tag('th'):attr('colspan', spanCount):wikitext(modType.name .. ' Stars')
result = result..'!!XP!!Skills!!Standard Modifiers!!Unique Modifiers'
            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


for i, cons in ipairs(SkillData.Astrology.Constellations) do
    -- Data rows
local name = cons.name
    for _, cons in ipairs(recipes) do
result = result..'\r\n|-'
        local modData = Skills._getConstellationModifiers(cons)
result = result..'\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='constellation', size='50', notext=true})..'||'..name
        local name = cons.name
result = result..'||'..cons.level..'||'..cons.provides.xp
        local skillIconArray = {}
        for _, skillID in ipairs(cons.skillIDs) do
            table.insert(skillIconArray, Icons.Icon({Constants.getSkillName(skillID), type='skill'}))
        end


local skillIconArray = {}
        -- Calculate maximum rows needed
for j, skillID in ipairs(cons.skills) do
        local maxRows = 1
table.insert(skillIconArray, Icons.Icon({Constants.getSkillName(skillID), type='skill'}))
        for _, modTypeData in pairs(modData) do
end
            maxRows = math.max(maxRows, Shared.tableCount(modTypeData))
result = result..'||'..table.concat(skillIconArray, '<br/>')
        end


local standModsRaw = Skills._buildAstrologyModifierArray(cons, maxModifier, true, false, false, false)
        -- Iterate through rows
local standMods = {}
        for rowIdx = 1, maxRows do
--Building the list of Standard modifiers:
            local row = tableRoot:tag('tr')
for j, modifier in ipairs(standModsRaw) do
            if rowIdx == 1 then
table.insert(standMods, Constants._getModifierText(modifier[1], modifier[2], false))
                row:tag('td'):attr('rowspan', maxRows)
end
                :wikitext(Icons.Icon({name, type='constellation', size=50, notext=true}))
result = result..'|| '..table.concat(standMods, '<br/>')
                :css('text-align', 'center')
                row:tag('td'):attr('rowspan', maxRows)
                :wikitext(name)
                :attr('id', 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


--Building the list of all Unique Modifiers
            -- Modifiers data
local uModsRaw = Skills._buildAstrologyModifierArray(cons, maxModifier, false, true, false, false)
            for _, modType in ipairs(modTypes) do
local uMods = {}
                if modType.inUse then
for j, modifier in ipairs(uModsRaw) do
                    local masteryLevel = modType.levels[rowIdx]
table.insert(uMods, Constants._getModifierText(modifier[1], modifier[2], false))
                    local rowModData = modData[modType.modKey][rowIdx]
end
                    local rowConsData = cons[modType.modKey][rowIdx]
result = result..'||'..table.concat(uMods, '<br/>')
                    local cell1 = row:tag('td')
end
                    if modType.name == "Abyssal" then
result = result..'\r\n|}'
                    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 result
return tostring(root)
end
end


function p.buildAstrologyConstellationTable(frame)
function p.buildAstrologyConstellationTable(frame)
return p._buildAstrologyConstellationTable()
local args = frame.args ~= nil and frame.args or frame
end
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


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


return p
return p

Latest revision as of 04:39, 4 November 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')
			:attr('id', area.name)
			: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(Icons.Icon({npc.name, type='thieving', noicon=true}))
        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)
        			 :attr('id', 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)
                			 :attr('id', 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