Module:Skills/Gathering: Difference between revisions

From Melvor Idle
(added rare items to getThievingSourcesForItem and canItemBeStolen)
No edit summary
 
(114 intermediate revisions by 6 users not shown)
Line 1: Line 1:
--Splitting some functions into here to avoid bloating a single file
--Splitting some functions into here to avoid bloating a single file
local p = {}
local p = {}
local SkillData = mw.loadData('Module:Skills/data')
local ShopData = mw.loadData('Module:Shop/data')


local Constants = require('Module:Constants')
local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local Shared = require('Module:Shared')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Common = require('Module:Common')
local Modifiers = require('Module:Modifiers')
local Items = require('Module:Items')
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')
local Skills = require('Module:Skills')
local ItemSourceTables = require('Module:Items/SourceTables')
local Num = require("Module:Number")


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


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


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


for i, tool in Shared.skpairs(toolArray) do
local trees = GameData.getEntities(SkillData.Woodcutting.trees,
result = result..'\r\n|-'
function(tree)
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
return Skills.getRecipeRealm(tree) == realm.id
result = result..'||'..tool.name
local level = 1
if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil then
--Gonna be lazy and assume there's only the one skill level and it's the one we care about
level = tool.unlockRequirements.skillLevel[1][2]
end
end
result = result..'||style="text-align:right"|'..level
)
result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)
table.sort(trees, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
for i, tree in ipairs(trees) do
local level = Skills.getRecipeLevel(skillID, tree)
local baseXP = tree.baseAbyssalExperience or tree.baseExperience
local baseInt = tree.baseInterval
local reqText = Skills.getRecipeRequirementText(SkillData.Woodcutting.name, tree)
local log = Items.getItemByID(tree.productId)
local sellCurrency = log.sellsForCurrency or 'melvorD:GP'
local XPSec = baseXP / (baseInt / 1000)
local currSec = Num.round(log.sellsFor / (baseInt / 1000), 2, 2)


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


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


local result = '{| class="wikitable"'
local totalWt, lootValue = 0, {}
result = result..'\r\n!colspan="4"| !!colspan="2"|Mine Time Decrease!!colspan="2"|2x Ore Chance'
local realmSpecials = GameData.getEntityByProperty(SkillData.Fishing.specialItems, 'realmID', realm.id)
result = result..'\r\n|- class="headerRow-0"'
if realmSpecials == nil then
result = result..'\r\n!colspan="2"|Name!!'..Icons.Icon({'Mining', type='skill', notext=true})..' Level'
return ''
result = result..'!!Cost!!This Pick!!Total!!This Pick!!Total'
end
local itemArray = realmSpecials.drops
for i, itemDef in ipairs(itemArray) do
totalWt = totalWt + itemDef.weight
end
-- Sort the loot table by weight in descending order
table.sort(itemArray, function(a, b) return a.weight > b.weight end)


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


for i, tool in Shared.skpairs(toolArray) do
-- Add header row
result = result..'\r\n|-'
local headerRow = html:tag('tr'):addClass('headerRow-0')
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
headerRow:tag('th'):attr('colspan', '2'):wikitext('Item')
result = result..'||'..tool.name
headerRow:tag('th'):wikitext('Value')
local level = 1
headerRow:tag('th'):attr('colspan', '2'):wikitext('Chance')
if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil then
--Gonna be lazy and assume there's only the one skill level and it's the one we care about
level = tool.unlockRequirements.skillLevel[1][2]
end
result = result..'||style="text-align:right"|'..level
result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)


local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
-- Add item rows
total = total + cutTime
for i, itemDef in ipairs(itemArray) do
local item = Items.getItemByID(itemDef.itemID)
if item ~= nil then
local dropChance = itemDef.weight / totalWt * 100
local currID = item.sellsForCurrency or 'melvorD:GP'
-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
local fmt = (dropChance < 0.10 and '%.2g') or '%.2f'
local row = html:tag('tr')


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


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


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


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


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


local total = 0
local itemArray = {}
 
for i, itemID in ipairs(SkillData.Fishing.junkItemIDs) do
for i, tool in Shared.skpairs(toolArray) do
local item = Items.getItemByID(itemID)
result = result..'\r\n|-'
if item ~= nil then
result = result..'\r\n|style="min-width:25px" data-sort-value="'..tool.name..'"|'..Icons.Icon({tool.name, type='upgrade', size='50', notext=true})
table.insert(itemArray, item)
result = result..'||'..tool.name
local level = 1
if tool.unlockRequirements ~= nil and tool.unlockRequirements.skillLevel ~= nil then
--Gonna be lazy and assume there's only the one skill level and it's the one we care about
level = tool.unlockRequirements.skillLevel[1][2]
end
end
result = result..'||style="text-align:right"|'..level
end
result = result..'||style="text-align:right" data-sort-value="'..tool.cost.gp..'"|'..Icons.GP(tool.cost.gp)
table.sort(itemArray, function(a, b) return a.name < b.name end)


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


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


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


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


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


function p.getSpecialFishingTable(frame)
function p._getMiningGemsTable(gemType)
local lootValue = 0
if type(gemType) ~= 'string' then
local totalWt = Items.specialFishWt
gemType = 'Normal'
end
local validTypes = {
["Normal"] = 'randomGems',
["Superior"] = 'randomSuperiorGems',
["Abyssal"] = 'randomAbyssalGems'
}
local gemDataKey = validTypes[gemType]
if gemDataKey == nil then
return Shared.printError('No such gem type "' .. gemType .. '"')
end
local gemData = GameData.rawData[gemDataKey]
local totalWeight = 0
for i, gem in ipairs(gemData) do
totalWeight = totalWeight + gem.weight
end
local html = mw.html.create('table')
:addClass('wikitable sortable')


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


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


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


return result
return tostring(html)
end
end


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


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


for i, item in Shared.skpairs(itemArray) do
-- Determine cooking levels for all fish
result = result..'\r\n|-'
local cookRecipes = {}
result = result..'\r\n|style="min-width:25px"|'..Icons.Icon({item.name, type='item', notext='true', size='50'})..'||[['..item.name..']]'
for i, recipe in ipairs(SkillData.Cooking.recipes) do
result = result..'||style="text-align:right;" data-sort-value="'..item.sellsFor..'"|'..Icons.GP(item.sellsFor)
-- 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
end


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


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


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


table.sort(mineData, function(a, b) return a.level < b.level end)
-- Add fish rows
for i, recipe in ipairs(recipeList) do
local fish = Items.getItemByID(recipe.productId)
if fish ~= nil then
local timeMin, timeMax = recipe.baseMinInterval / 1000, recipe.baseMaxInterval / 1000
local timeAvg = (timeMin + timeMax) / 2
local timeSortVal = (recipe.baseMinInterval + recipe.baseMaxInterval) / 2000
local level = Skills.getRecipeLevel(skillID, recipe)
local reqText = Skills.getRecipeRequirementText(skillID, recipe)
local baseXP = recipe.baseAbyssalExperience or recipe.baseExperience
local XPSec = baseXP / timeSortVal
local sellCurrency = fish.sellsForCurrency or 'melvorD:GP'
local GPSec = fish.sellsFor / timeSortVal


for i, oreData in Shared.skpairs(mineData) do
local row = html:tag('tr')
local ore = Items.getItemByID(oreData.ore)
row:tag('td'):wikitext(Icons.Icon({fish.name, type='item', notext=true}))
result = result..'\r\n|-\r\n|style="min-width:25px"|'..Icons.Icon({ore.name, type='item', size='50', notext=true})..'||[['..ore.name..']]'
:addClass('table-img')
result = result..'||style="text-align:right"|'..oreData.level..'||style="text-align:right"|'..ore.miningXP
row:tag('td'):wikitext('[[' .. fish.name .. ']]')
result = result..'||style="text-align:right" data-sort-value="'..oreData.respawnInterval..'"|'
:attr('data-sort-value', fish.name)
result = result..Shared.timeString(oreData.respawnInterval / 1000, true)
row:tag('td'):wikitext(level)
result = result..'||data-sort-value="'..ore.sellsFor..'"|'..Icons.GP(ore.sellsFor)
: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
end


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


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


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


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


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


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


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


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


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


function p.getFishingAreasTable(frame)


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


for i, area in Shared.skpairs(SkillData.Fishing.Areas) do
local html = mw.html.create('table')
result = result..'\r\n|-'
html:addClass('wikitable sortable')
result = result..'\r\n| style ="text-align: left;" |'..area.name


local fishArray = {}
-- Add header row
for j, fish in Shared.skpairs(area.fish) do
local headerRow = html:tag('tr')
local fishTable = Items.getItems(function(item) return item.fishingID == fish end)
headerRow:tag('th'):wikitext('Item')
local fishItem = fishTable[0] or fishTable[1]
headerRow:tag('th'):wikitext('[[DLC]]')
table.insert(fishArray, Icons.Icon({fishItem.name, type='item'}))
headerRow:tag('th'):wikitext('Qty')
end
headerRow:tag('th'):wikitext('Price')
result = result..'\r\n|'..table.concat(fishArray, '<br />')
headerRow:tag('th'):attr('colspan', '2'):wikitext('Chance')


result = result..'\r\n| style="text-align:right"|'..area.fishChance..'%'
-- Add rows for each rare item
result = result..'\r\n| style="text-align:right"|'..area.junkChance..'%'
for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
result = result..'\r\n| style="text-align:right"|'..area.specialChance..'%'
-- 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
end
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))


result = result..'\r\n|}'
if npcMatch and realmMatch then
return result
local thisItem = Items.getItemByID(drop.itemID)
end
local odds = drop.chance


function p.getThievingNPC(npcName)
local row = html:tag('tr')
local result = nil
row:tag('td')
for i, npc in Shared.skpairs(SkillData.Thieving.NPCs) do
:attr('data-sort-value', thisItem.name)
if npc.name == npcName then
:wikitext(Icons.Icon({thisItem.name, type='item'}))
result = Shared.clone(npc)
row:tag('td')
break
: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
end
end
return result
end


function p.getThievingNPCArea(npc)
return tostring(html)
if type(npc) == 'string' then
npc = p.getThievingNPC(npc)
end
local result = nil
for i, area in Shared.skpairs(SkillData.Thieving.Areas) do
for j, npcID in pairs(area.npcs) do
if npcID == npc.id then
result = area
break
end
end
end
return result
end
end


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


function p.getThievingNPCStat(frame)
function p._getThievingNPCCurrencyText(npc)
local npcName = frame.args ~= nil and frame.args[1] or frame[1]
local currTextPart = {}
local statName = frame.args ~= nil and frame.args[2] or frame[2]
for _, currencyDrop in ipairs(npc.currencyDrops) do
local npc = p.getThievingNPC(npcName)
table.insert(currTextPart, Icons._Currency(currencyDrop.id, 1, currencyDrop.quantity))
if npc == nil then
return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
end
end
return table.concat(currTextPart, ', ')
return p._getThievingNPCStat(npc, statName)
end
end


Line 378: Line 564:
local result = ''
local result = ''
local sectionTxt = {}
local sectionTxt = {}
 
--Five sections here: GP, normal loot, area loot, rare loot, and unique item
--Five sections here: Currency, normal loot, area loot, rare loot, and unique item
--First up, GP:
--First up, currency:
local gpTxt = 'Successfully pickpocketing the '..npc.name..' will always give '..Icons.GP(1, npc.maxGP)
table.insert(sectionTxt, 'Successfully pickpocketing the ' .. npc.name .. ' will always give '.. p._getThievingNPCCurrencyText(npc))
table.insert(sectionTxt, gpTxt)
 
--Next up, normal loot:
--Next up, normal loot:
--(Skip if no loot)
--(Skip if no loot)
if npc.lootTable ~= nil and Shared.tableCount(npc.lootTable) > 0 then
if npc.lootTable ~= nil and Shared.tableCount(npc.lootTable) > 0 then
local normalTxt = '===Possible Common Drops:===\r\nUp to one of these will be received on a successful pickpocket:'
local normalTxt = {}
table.insert(normalTxt, '===Possible Common Drops:===\r\nUp to one of these will be received on a successful pickpocket:')
local totalWt = 0
local totalWt = 0
local lootChance = thievingNormalLootChance
local lootChance = SkillData.Thieving.itemChance
local lootValue = 0
local lootValue = {}
 
--First loop through to get the total weight so we have it for later
--First loop through to get the total weight so we have it for later
for i, loot in pairs(npc.lootTable) do
for i, loot in pairs(npc.lootTable) do
totalWt = totalWt + loot[2]
totalWt = totalWt + loot.weight
end
end
 
normalTxt = normalTxt..'\r\n{|class="wikitable sortable"'
table.insert(normalTxt, '\r\n{|class="wikitable sortable"')
normalTxt = normalTxt..'\r\n!Item!!Qty'
table.insert(normalTxt, '\r\n!Item!!Qty')
normalTxt = normalTxt..'!!Price!!colspan="2"|Chance'
table.insert(normalTxt, '!!Price!!colspan="2"|Chance')
 
local lootTable = Shared.shallowClone(npc.lootTable)
--Then sort the loot table by weight
--Then sort the loot table by weight
table.sort(npc.lootTable, function(a, b) return a[2] > b[2] end)
table.sort(lootTable, function(a, b) return a.weight > b.weight end)
for i, row in Shared.skpairs(npc.lootTable) do
for i, loot in ipairs(lootTable) do
local thisItem = Items.getItemByID(row[1])
local thisItem = Items.getItemByID(loot.itemID)
local maxQty = row[3]
if thisItem ~= nil then
if thisItem ~= nil then
normalTxt = normalTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
table.insert(normalTxt, '\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'}))
else
else
normalTxt = normalTxt..'\r\n|-\r\n|Unknown Item[[Category:Pages with script errors]]'
table.insert(normalTxt, '\r\n|-\r\n|Unknown Item[[Category:Pages with script errors]]')
end
end
normalTxt = normalTxt..'||style="text-align:right" data-sort-value="'..maxQty..'"|'
table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..(loot.minQuantity + loot.maxQuantity)..'"|')
 
if maxQty > 1 then
if loot.minQuantity ~= loot.maxQuantity then
normalTxt = normalTxt.. '1 - '
table.insert(normalTxt, Num.formatnum(loot.minQuantity) .. ' - ' .. Num.formatnum(loot.maxQuantity))
else
table.insert(normalTxt, Num.formatnum(loot.maxQuantity))
end
end
normalTxt = normalTxt..Shared.formatnum(row[3])
 
--Adding price columns
--Adding price columns
local itemPrice = 0
local sellAmount, sellCurrency = nil, nil
if thisItem == nil then
if thisItem == nil then
normalTxt = normalTxt..'||data-sort-value="0"|???'
table.insert(normalTxt, '||data-sort-value="0"|???')
else
else
itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
sellAmount = thisItem.sellsFor or 0
if itemPrice == 0 or maxQty == 1 then
sellCurrency = thisItem.sellsForCurrency or 'melvorD:GP'
normalTxt = normalTxt..'||'..Icons.GP(itemPrice)
table.insert(normalTxt, '||' .. Items.getValueText(thisItem, loot.minQuantity, loot.maxQuantity))
else
normalTxt = normalTxt..'||'..Icons.GP(itemPrice, itemPrice * maxQty)
end
end
end
 
--Getting the drop chance
--Getting the drop chance
local dropChance = (row[2] / totalWt * lootChance)
local dropChance = (loot.weight / totalWt * lootChance)
if dropChance ~= 100 then
if dropChance < 100 then
--Show fraction as long as it isn't going to be 1/1
--Show fraction as long as it isn't going to be 1/1
normalTxt = normalTxt..'||style="text-align:right" data-sort-value="'..row[2]..'"'
table.insert(normalTxt, '||style="text-align:right" data-sort-value="'..loot.weight..'"')
normalTxt = normalTxt..'|'..Shared.fraction(row[2] * lootChance, totalWt * 100)
table.insert(normalTxt, '|'..Num.fraction(loot.weight * lootChance, totalWt * 100))
normalTxt = normalTxt..'||'
table.insert(normalTxt, '||')
else
else
normalTxt = normalTxt..'||colspan="2" data-sort-value="'..row[2]..'"'
table.insert(normalTxt, '||colspan="2" data-sort-value="'..loot.weight..'"')
end
end
normalTxt = normalTxt..'style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
table.insert(normalTxt, 'style="text-align:right"|'..Num.round(dropChance, 2, 2)..'%')
 
--Adding to the average loot value based on price & dropchance
--Adding to the average loot value based on price & dropchance
lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((1 + maxQty) / 2))
if sellAmount ~= nil and sellCurrency ~= nil then
if lootValue[sellCurrency] == nil then
lootValue[sellCurrency] = 0
end
lootValue[sellCurrency] = lootValue[sellCurrency] + (dropChance * 0.01 * sellAmount * (loot.minQuantity + loot.maxQuantity) / 2)
end
end
end
if multiDrop then
if Shared.tableCount(npc.lootTable) > 1 then
normalTxt = normalTxt..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
table.insert(normalTxt, '\r\n|-class="sortbottom" \r\n!colspan="3"|Total:')
if lootChance < 100 then
if lootChance < 100 then
normalTxt = normalTxt..'\r\n|style="text-align:right"|'..Shared.fraction(lootChance, 100)..'||'
table.insert(normalTxt, '\r\n|style="text-align:right"|'..Num.fraction(lootChance, 100)..'||')
else
else
normalTxt = normalTxt..'\r\n|colspan="2" '
table.insert(normalTxt, '\r\n|colspan="2" ')
end
end
normalTxt = normalTxt..'style="text-align:right"|'..lootChance..'.00%'
table.insert(normalTxt, 'style="text-align:right"|'..Num.round(lootChance, 2, 2)..'%')
end
end
normalTxt = normalTxt..'\r\n|}'
table.insert(normalTxt, '\r\n|}')
table.insert(sectionTxt, normalTxt)
 
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
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{|class="wikitable sortable"'
rareTxt = rareTxt..'\r\n'..p._getThievingGeneralRareTable(npc)
rareTxt = rareTxt..'\r\n!Item!!Qty'
rareTxt = rareTxt..'!!Price!!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'})
rareTxt = rareTxt..'||1||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
rareTxt = rareTxt..'||style="text-align:right" data-sort-value="'..odds..'"|'..Shared.round(odds, 4, 4)..'%'
end
rareTxt = rareTxt..'\r\n|}'
table.insert(sectionTxt, rareTxt)
table.insert(sectionTxt, rareTxt)
 
local areaTxt = '===Possible Area Unique Drops==='
local areaTxt = '===Possible Area Unique Drops==='
areaTxt = areaTxt..'\r\nAny Area Unique Drop is equally likely to be obtained after a successful pickpocket. '
areaTxt = areaTxt..'\r\nAny Area Unique Drop is equally likely to be obtained after a successful pickpocket. '
areaTxt = areaTxt..'\r\nEach Area Unique Drop is rolled for separately, so it is possible to receive multiple Area Unique Drops from a single action. '
areaTxt = areaTxt..'\r\nEach Area Unique Drop is rolled for separately, so it is possible to receive multiple Area Unique Drops from a single action. '
areaTxt = areaTxt..'The chance of receiving an Area Unique drop is tripled if the 95% Thieving Mastery Pool checkpoint is active.'
areaTxt = areaTxt..'The chance of receiving an Area Unique drop is tripled if the 95% Thieving Mastery Pool checkpoint is active.'
 
local area = p.getThievingNPCArea(npc)
local area = Skills.getThievingNPCArea(npc)
areaTxt = areaTxt..'\r\n{|class="wikitable sortable"'
areaTxt = areaTxt..'\r\n{|class="wikitable sortable"'
areaTxt = areaTxt..'\r\n!Item!!Qty'
areaTxt = areaTxt..'\r\n!Item!!Qty'
areaTxt = areaTxt..'!!Price!!colspan="2"|Chance'
areaTxt = areaTxt..'!!Price!!colspan="2"|Chance'
local dropCount = Shared.tableCount(area.uniqueDrops)
local dropLines = {}
local dropLines = {}
for i, drop in pairs(area.uniqueDrops) do
for i, drop in ipairs(area.uniqueDrops) do
local thisItem = Items.getItemByID(drop.itemID)
local thisItem = Items.getItemByID(drop.id)
local lineTxt = ''
local lineTxt = ''
lineTxt = lineTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
lineTxt = lineTxt..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
lineTxt = lineTxt..'||'..drop.qty..'||data-sort-value="'..thisItem.sellsFor..'"|'..Icons.GP(thisItem.sellsFor)
lineTxt = lineTxt..'||data-sort-value="'..drop.quantity..'"| '..Num.formatnum(drop.quantity)..'||data-sort-value="'..thisItem.sellsFor..'"|'..Items.getValueText(thisItem)
lineTxt = lineTxt..'||style="text-align:right"|'..thievingAreaLootChance
lineTxt = lineTxt..'||style="text-align:right"|'..Num.fraction(1, 1/(SkillData.Thieving.baseAreaUniqueChance/100))
lineTxt = lineTxt..'||'..Shared.round(thievingAreaLootChance, 2, 2)..'%'
lineTxt = lineTxt..'||'..Num.round(SkillData.Thieving.baseAreaUniqueChance, 2, 2)..'%'
dropLines[thisItem.name] = lineTxt
dropLines[thisItem.name] = lineTxt
end
end
for i, txt in Shared.skpairs(dropLines) do
for i, txt in pairs(dropLines) do
areaTxt = areaTxt..txt
areaTxt = areaTxt..txt
end
end
areaTxt = areaTxt..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
areaTxt = areaTxt..'\r\n|style="text-align:right"|'..Shared.fraction(thievingAreaLootChance, 100)..'||'
areaTxt = areaTxt..'style="text-align:right"|'..Shared.round(thievingAreaLootChance, 2, 2)..'%'
areaTxt = areaTxt..'\r\n|}'
areaTxt = areaTxt..'\r\n|}'
table.insert(sectionTxt, areaTxt)
table.insert(sectionTxt, areaTxt)
 
if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID > -1 then
if npc.uniqueDrop ~= nil and npc.uniqueDrop.id ~= nil then
local uniqueTxt = '===Possible NPC Unique Drop==='
local thisItem = Items.getItemByID(npc.uniqueDrop.id)
uniqueTxt = uniqueTxt..'\r\nThe chance of receiving the unique drop for an NPC is based on a combination of several factors.'
if thisItem ~= nil then
uniqueTxt = uniqueTxt..' The unique drop chance for an NPC is included in the tooltip for your Stealth against that NPC.'
local uniqueTxt = '===Possible NPC Unique Drop==='
local thisItem = Items.getItemByID(npc.uniqueDrop.itemID)
uniqueTxt = uniqueTxt..'\r\nThe chance of receiving the unique drop for an NPC is based on a combination of several factors.'
uniqueTxt = uniqueTxt..'\r\nThe unique drop for the '..npc.name..' is '
uniqueTxt = uniqueTxt..' The unique drop chance for an NPC is included in the tooltip for your Stealth against that NPC.'
if npc.uniqueDrop.qty > 1 then
uniqueTxt = uniqueTxt..'\r\nThe unique drop for the '..npc.name..' is '
uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item', qty=npc.uniqueDrop.qty})
if npc.uniqueDrop.quantity > 1 then
else
uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item', qty=npc.uniqueDrop.quantity}) .. '.'
uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item'})
else
uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item'}) .. '.'
end
table.insert(sectionTxt, uniqueTxt)
end
end
table.insert(sectionTxt, uniqueTxt)
end
end
 
return table.concat(sectionTxt, '\r\n')
return table.concat(sectionTxt, '\r\n')
end
end
Line 524: Line 708:
function p.getThievingNPCLootTables(frame)
function p.getThievingNPCLootTables(frame)
local npcName = frame.args ~= nil and frame.args[1] or frame
local npcName = frame.args ~= nil and frame.args[1] or frame
local npc = p.getThievingNPC(npcName)
local npc = Skills.getThievingNPC(npcName)
if npc == nil then
if npc == nil then
return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
return Shared.printError('Invalid Thieving NPC "' .. npcName .. '"')
end
end
 
return p._getThievingNPCLootTables(npc)
return p._getThievingNPCLootTables(npc)
end
end


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


function p.canItemBeStolen(itemID)
function p._getSlimFarmingTable(realmID, category)
local result = false
local seedList = GameData.getEntities(SkillData.Farming.recipes,
--First, check if this is a rare item
function(recipe)
for i, drop in pairs(SkillData.Thieving.RareItems) do
return recipe.categoryID == category.id and Skills.getRecipeRealm(recipe) == realmID
if drop.itemID == itemID then
end)
result = true
if Shared.tableIsEmpty(seedList) then
break
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
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]
--Then check each NPC to see if they drop the item
local category = GameData.getEntityByName(SkillData.Farming.categories, categoryName)
if not result then
if category == nil then
for i, npc in pairs(SkillData.Thieving.NPCs) do
return Shared.printError('Invalid farming category: ' .. categoryName .. '. Please choose Allotments, Herbs, Trees or Special')
for j, drop in pairs(npc.lootTable) do
if drop[1] == itemID then
result = true
end
end
if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID == itemID then
result = true
end
if result  then
break
end
end
end
end
 
--Then check area unique drops
if slim == true then  
if not result then
return p._getSlimFarmingTable(realm.id, category)
for  i, area in pairs(SkillData.Thieving.Areas) do
else
for j, drop in pairs(area.uniqueDrops) do
return p._getFarmingTable(realm.id, category)
if drop.itemID == itemID then
result = true
break
end
end
if result then
break
end
end
end
end
return result
end
end


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


return p
return p

Latest revision as of 13:05, 7 July 2024

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

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

local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Common = require('Module:Common')
local Modifiers = require('Module:Modifiers')
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local Skills = require('Module:Skills')
local ItemSourceTables = require('Module:Items/SourceTables')
local Num = require("Module:Number")

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

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

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

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

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

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

	local totalWt, lootValue = 0, {}
	local realmSpecials = GameData.getEntityByProperty(SkillData.Fishing.specialItems, 'realmID', realm.id)
	if realmSpecials == nil then
		return ''
	end
	local itemArray = realmSpecials.drops
	for i, itemDef in ipairs(itemArray) do
		totalWt = totalWt + itemDef.weight
	end
	-- Sort the loot table by weight in descending order
	table.sort(itemArray, function(a, b) return a.weight > b.weight end)

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

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

	-- Add item rows
	for i, itemDef in ipairs(itemArray) do
		local item = Items.getItemByID(itemDef.itemID)
		if item ~= nil then
			local dropChance = itemDef.weight / totalWt * 100
			local currID = item.sellsForCurrency or 'melvorD:GP'
			-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
			local fmt = (dropChance < 0.10 and '%.2g') or '%.2f'
			local row = html:tag('tr')

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

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

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

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

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

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

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

	return tostring(html)
end

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

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

	return tostring(html)
end

function p._getMiningGemsTable(gemType)
	if type(gemType) ~= 'string' then
		gemType = 'Normal'
	end
	local validTypes = {
		["Normal"] = 'randomGems',
		["Superior"] = 'randomSuperiorGems',
		["Abyssal"] = 'randomAbyssalGems'
	}
	local gemDataKey = validTypes[gemType]
	if gemDataKey == nil then
		return Shared.printError('No such gem type "' .. gemType .. '"')
	end
	
	local gemData = GameData.rawData[gemDataKey]
	local totalWeight = 0
	for i, gem in ipairs(gemData) do
		totalWeight = totalWeight + gem.weight
	end
	
	local html = mw.html.create('table')
		:addClass('wikitable sortable')

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

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

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

	return tostring(html)
end

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

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

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

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

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

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

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

	-- Add fish rows
	for i, recipe in ipairs(recipeList) do
		local fish = Items.getItemByID(recipe.productId)
		if fish ~= nil then
			local timeMin, timeMax = recipe.baseMinInterval / 1000, recipe.baseMaxInterval / 1000
			local timeAvg = (timeMin + timeMax) / 2
			local timeSortVal = (recipe.baseMinInterval + recipe.baseMaxInterval) / 2000
			local level = Skills.getRecipeLevel(skillID, recipe)
			local reqText = Skills.getRecipeRequirementText(skillID, recipe)
			local baseXP = recipe.baseAbyssalExperience or recipe.baseExperience
			local XPSec = baseXP / timeSortVal
			local sellCurrency = fish.sellsForCurrency or 'melvorD:GP'
			local GPSec = fish.sellsFor / timeSortVal

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

	return tostring(html)
end


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

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

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

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

	-- Add rows for each fishing area
	for i, area in ipairs(fishAreas) do
		local row = html:tag('tr')
		row:tag('td')
			:css('text-align', 'left')
			:wikitext(area.name)

		local fishArray = {}
		for j, fishID in ipairs(area.fishIDs) do
			local fishItem = Items.getItemByID(fishID)
			if fishItem ~= nil then
				table.insert(fishArray, Icons.Icon({fishItem.name, type='item'}))
			end
		end
		row:tag('td')
			:wikitext(table.concat(fishArray, '<br/>'))
		row:tag('td')
			:css('text-align', 'center')
			:wikitext(Icons.getDLCColumnIcon(area.id))
			:attr('data-sort-value', Icons.getExpansionID(area.id))
		row:tag('td')
			:css('text-align', 'right')
			:wikitext(area.fishChance .. '%')
		row:tag('td')
			:css('text-align', 'right')
			:wikitext(area.junkChance .. '%')
		row:tag('td')
			:css('text-align', 'right')
			:wikitext(area.specialChance .. '%')
	end

	return tostring(html)
end


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

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

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

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

	-- Add rows for each rare item
	for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
		-- If an npcID has been passed and the item is NPC specific, only display the item if it may be obtained while pickpocketing that NPC
		local npcMatch = (npc == nil or drop.npcs == nil or Shared.contains(drop.npcs, npc.id))
		local realmMatch = (npcRealm == nil or drop.realms == nil or Shared.contains(drop.realms, npcRealm))

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

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

	return tostring(html)
end


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

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

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

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

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

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

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

			if loot.minQuantity ~= loot.maxQuantity then
				table.insert(normalTxt, Num.formatnum(loot.minQuantity) .. ' - ' .. Num.formatnum(loot.maxQuantity))
			else
				table.insert(normalTxt, Num.formatnum(loot.maxQuantity))
			end

			--Adding price columns
			local sellAmount, sellCurrency = nil, nil
			if thisItem == nil then
				table.insert(normalTxt, '||data-sort-value="0"|???')
			else
				sellAmount = thisItem.sellsFor or 0
				sellCurrency = thisItem.sellsForCurrency or 'melvorD:GP'
				table.insert(normalTxt, '||' .. Items.getValueText(thisItem, loot.minQuantity, loot.maxQuantity))
			end

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

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

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

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

		table.insert(normalTxt, '\r\n\r\nIncluding currency, the average successful pickpocket is worth ' .. lootValueText(lootValue) .. '.')
		table.insert(sectionTxt, table.concat(normalTxt))
	end

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

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

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

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

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

function p.getThievingNPCLootTables(frame)
	local npcName = frame.args ~= nil and frame.args[1] or frame
	local npc = Skills.getThievingNPC(npcName)
	if npc == nil then
		return Shared.printError('Invalid Thieving NPC "' .. npcName .. '"')
	end

	return p._getThievingNPCLootTables(npc)
end

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

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


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

	local skillID = 'Farming'

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

	if category.id == 'melvorD:Allotment' then
		html:tag('th'):attr("colspan", 2):wikitext("Produce")
			:tag('th'):wikitext('Healing')
			:tag('th'):wikitext('Produce<br>Value')
	else 
		html:tag('th'):attr("colspan", 2):wikitext("Produce")
			:tag('th'):wikitext('Produce<br>Value')
	end
	--html = html:tag('th'):wikitext('Seed Sources')

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

	for i, seed in ipairs(seedList) do
		local seedItem = Items.getItemByID(seed.seedCost.id)
		local productItem = Items.getItemByID(seed.productId)
		if seedItem ~= nil and productItem ~= nil then
			local level = Skills.getRecipeLevel(skillID, seed)
			local baseXP = seed.baseAbyssalExperience or seed.baseExperience
			local baseInt = seed.baseInterval
			local reqText = Skills.getRecipeRequirementText(SkillData.Farming.name, seed)

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

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

	return tostring(tbl:done())
end

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

	local skillID = 'Farming'

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

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

	for i, seed in ipairs(seedList) do
		local seedItem = Items.getItemByID(seed.seedCost.id)
		local productItem = Items.getItemByID(seed.productId)
		if seedItem ~= nil and productItem ~= nil then
			local level = Skills.getRecipeLevel(skillID, seed)

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

	return tostring(tbl:done())
end

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

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

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


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

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

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

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

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

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

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

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

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

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

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

	return tostring(root)
end

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

	return p._buildAstrologyConstellationTable(realm.id)
end

return p