Module:Skills/Gathering: Difference between revisions

From Melvor Idle
(Use Thieving drop chances from game data instead of hard-coded values)
(Move functions to Module:Skills; Move Farming functions from Module:Skills)
Line 9: Line 9:
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')
function p.getConstellationByID(constID)
local ItemSourceTables = require('Module:Items/SourceTables')
return SkillData.Astrology.Constellations[constID]
end
 
function p.getConstellation(constName)
for i, const in ipairs(SkillData.Astrology.Constellations) do
if const.name == constName then
return const
end
end
return nil
end
 
function p.getConstellations(checkFunc)
local result = {}
for i, const in ipairs(SkillData.Astrology.Constellations) do
if checkFunc(const) then
table.insert(result, const)
end
end
return result
end


function p.getAxeTable(frame)
function p.getAxeTable(frame)
Line 357: Line 336:
result = result..'\r\n|}'
result = result..'\r\n|}'
return result
return result
end
function p.getThievingNPC(npcName)
local result = nil
for i, npc in Shared.skpairs(SkillData.Thieving.NPCs) do
if npc.name == npcName then
result = Shared.clone(npc)
break
end
end
return result
end
function p.getThievingNPCArea(npc)
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
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)
local npcName = frame.args ~= nil and frame.args[1] or frame[1]
local statName = frame.args ~= nil and frame.args[2] or frame[2]
local npc = p.getThievingNPC(npcName)
if npc == nil then
return "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
end
return p._getThievingNPCStat(npc, statName)
end
end


Line 530: Line 449:
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'
Line 574: Line 493:
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 "ERROR: Invalid Thieving NPC "..npcName.."[[Category:Pages with script errors]]"
Line 593: Line 512:
result = result..'||'..Icons.Icon({npc.name, type='thieving', noicon=true})
result = result..'||'..Icons.Icon({npc.name, type='thieving', noicon=true})


local area = p.getThievingNPCArea(npc)
local area = Skills.getThievingNPCArea(npc)
result = result..'||'..area.name
result = result..'||'..area.name
result = result..'||'..Icons._SkillReq('Thieving', npc.level)
result = result..'||'..Icons._SkillReq('Thieving', npc.level)
Line 671: Line 590:
end
end


function p.getThievingSourcesForItem(itemID)
function p._getFarmingTable(category)
local resultArray = {}
local seedList = {}
if category == 'Allotment' or category == 'Herb' or category == 'Tree' then
seedList = Items.getItems(function(item) return item.tier == category end)
else
return 'ERROR: Invalid farming category. Please choose Allotment, Herb, or Tree'
end


local areaNPCs = {}
local result = '{|class="wikitable sortable stickyHeader"'
result = result..'\r\n|- class="headerRow-0"'
result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
result = result..'!!XP!!Growth Time!!Seed Value'
if category == 'Allotment' then
result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
elseif category == 'Herb' then
result = result..'!!colspan="2"|Herb!!Herb Value'
elseif category == 'Tree' then
result = result..'!!colspan="2"|Logs!!Log Value'
end
result = result..'!!Seed Sources'


--First check area unique drops
table.sort(seedList, function(a, b) return a.farmingLevel < b.farmingLevel end)
--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, seed in pairs(seedList) do
for i, area in pairs(SkillData.Thieving.Areas) do
result = result..'\r\n|-'
for j, drop in pairs(area.uniqueDrops) do
result = result..'\r\n|'..Icons.Icon({seed.name, type='item', size='50', notext=true})..'||[['..seed.name..']]'
if drop.itemID == itemID then
result = result..'||'..seed.farmingLevel..'||'..Shared.formatnum(seed.farmingXP)
for k, npcID in pairs(area.npcs) do
result = result..'||data-sort-value="'..seed.timeToGrow..'"|'..Shared.timeString(seed.timeToGrow, true)
areaNPCs[npcID] = drop.qty
result = result..'||data-sort-value="'..seed.sellsFor..'"|'..Icons.GP(seed.sellsFor)
end
 
break
local crop = Items.getItemByID(seed.grownItemID)
end
result = result..'||'..Icons.Icon({crop.name, type='item', size='50', notext=true})..'||[['..crop.name..']]'
end
if category == 'Allotment' then
result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..(crop.healsFor * 10)
end
end
result = result..'||data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
result = result..'||'..ItemSourceTables._getItemSources(seed)
end
end


--Now go through and get drop chances on each NPC if needed
result = result..'\r\n|}'
for i, npc in pairs(SkillData.Thieving.NPCs) do
return result
local totalWt = 0
end
local dropWt = 0
 
local dropQty = 0
function p.getFarmingTable(frame)
for j, drop in pairs(npc.lootTable) do
local category = frame.args ~= nil and frame.args[1] or frame
totalWt = totalWt + drop[2]
 
if drop[1] == itemID then
return p._getFarmingTable(category)
dropWt = drop[2]
end
dropQty = drop[3]
 
end
function p.getFarmingFoodTable(frame)
end
local result = '{| class="wikitable sortable stickyHeader"'
if dropWt > 0 then
result = result..'\r\n|- class="headerRow-0"'
table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = dropQty, wt = dropWt * SkillData.Thieving.ItemChance, totalWt = totalWt * 100, level = npc.level})
result = result..'\r\n!colspan="2"|Crop!!'..Icons.Icon({"Farming", type="skill", notext=true})..' Level'
end
result = result..'!!Healing!!Value'
 
local itemArray = Items.getItems(function(item) return item.grownItemID ~= nil end)


--Chance of -1 on unique drops is to indicate variable chance
table.sort(itemArray, function(a, b) return a.farmingLevel < b.farmingLevel end)
if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID == itemID then
table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.qty, maxQty = npc.uniqueDrop.qty, wt = -1, totalWt = -1, level = npc.level})
end


if areaNPCs[npc.id] ~= nil then
for i, item in Shared.skpairs(itemArray) do
table.insert(resultArray, {npc = npc.name, minQty = areaNPCs[npc.id], maxQty = areaNPCs[npc.id], wt = SkillData.Thieving.AreaUniqueChance, totalWt = 100, level = npc.level})
local crop = Items.getItemByID(item.grownItemID)
if crop.healsFor ~= nil and crop.healsFor > 0 then
result = result..'\r\n|-'
result = result..'\r\n|'..Icons.Icon({crop.name, type='item', notext='true', size='50'})..'||[['..crop.name..']]'
result = result..'||style="text-align:right;"|'..item.farmingLevel
result = result..'||style="text-align:right" data-sort-value="'..crop.healsFor..'"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..(crop.healsFor * 10)
result = result..'||style="text-align:right" data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
end
end
end
end


for i, drop in pairs(SkillData.Thieving.RareItems) do
result = result..'\r\n|}'
if drop.itemID == itemID then
table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = 1})
end
end


return resultArray
return result
end
end


-- For a given constellation cons and modifier value modValue, generates and returns
function p.getFarmingPlotTable(frame)
-- a table of modifiers, much like any other item/object elsewhere in the game.
local areaName = frame.args ~= nil and frame.args[1] or frame
-- includeStandard: true|false, determines whether standard modifiers are included
local patches = nil
-- includeUnique: true|false, determines whether unique modifiers are included
for i, area in Shared.skpairs(SkillData.Farming.Patches) do
-- isDistinct: true|false, if true, the returned list of modifiers is de-duplicated
if area.areaName == areaName then
-- asKeyValue: true|false, if true, returns key/value pairs like usual modifier objects
patches = area.patches
function p._buildAstrologyModifierArray(cons, modValue, includeStandard, includeUnique, isDistinct, asKeyValue)
break
-- Temporary function to determine if the table already contains a given modifier
local containsMod = function(modList, modNew)
for i, modItem in ipairs(modList) do
-- Check mod names & value data types both equal
if modItem[1] == modNew[1] and type(modItem[2]) == type(modNew[2]) then
if type(modItem[2]) == 'table' then
if Shared.tablesEqual(modItem[2], modNew[2]) then
return true
end
elseif modItem[2] == modNew[2] then
return true
end
end
end
return false
end
end
local addToArray = function(modArray, modNew)
if not isDistinct or (isDistinct and not containsMod(modArray, modNew)) then
table.insert(modArray, modNew)
end
end
local modTypes = {}
if includeStandard then
table.insert(modTypes, 'standardModifiers')
end
end
if includeUnique then
if patches == nil then
table.insert(modTypes, 'uniqueModifiers')
return "ERROR: Invalid area name.[[Category:Pages with script errors]]"
end
end


local modArray = {}
local result = '{|class="wikitable"'
local isSkillMod = {}
result = result..'\r\n!Plot!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level!!Cost'
for _, modType in ipairs(modTypes) do
 
for i, skillMods in ipairs(cons[modType]) do
for i, patch in Shared.skpairs(patches) do
local skillID = cons.skills[i]
result = result..'\r\n|-\r\n|'..i
if skillID ~= nil then
result = result..'||style="text-align:right;" data-sort-value="' .. patch.level .. '"|'..patch.level
for j, modName in ipairs(skillMods) do
if patch.cost == 0 then
local modBaseName, modText, sign, isNegative, unsign, modBase = Constants.getModifierDetails(modName)
result = result..'||Free'
-- Check if modifier varies by skill, and amend the modifier value accordingly
else
local modVal = modValue
result = result..'||style="text-align:right;" data-sort-value="'..patch.cost..'"|'..Icons.GP(patch.cost)
if Shared.contains(modText, '{SV0}') then
isSkillMod[modName] = true
modVal = {skillID, modValue}
end
addToArray(modArray, {modName, modVal})
end
end
end
end
end
end


if asKeyValue then
result = result..'\r\n|}'
local modArrayKV = {}
return result
for i, modDefn in ipairs(modArray) do
local modName, modVal = modDefn[1], modDefn[2]
local isSkill = isSkillMod[modName]
if modArrayKV[modName] == nil then
modArrayKV[modName] = (isSkill and { modVal } or modVal)
elseif isSkill then
table.insert(modArrayKV[modName], modVal)
else
modArrayKV[modName] = modArrayKV[modName] + modVal
end
end
return modArrayKV
else
return modArray
end
end
end


Line 822: Line 714:
result = result..'||'..table.concat(skillIconArray, '<br/>')
result = result..'||'..table.concat(skillIconArray, '<br/>')


local standModsRaw = p._buildAstrologyModifierArray(cons, maxModifier, true, false, false, false)
local standModsRaw = Skills._buildAstrologyModifierArray(cons, maxModifier, true, false, false, false)
local standMods = {}
local standMods = {}
--Building the list of Standard modifiers:
--Building the list of Standard modifiers:
Line 831: Line 723:


--Building the list of all Unique Modifiers
--Building the list of all Unique Modifiers
local uModsRaw = p._buildAstrologyModifierArray(cons, maxModifier, false, true, false, false)
local uModsRaw = Skills._buildAstrologyModifierArray(cons, maxModifier, false, true, false, false)
local uMods = {}
local uMods = {}
for j, modifier in ipairs(uModsRaw) do
for j, modifier in ipairs(uModsRaw) do

Revision as of 14:29, 18 April 2022

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

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

local SkillData = mw.loadData('Module:Skills/data')
local ShopData = mw.loadData('Module:Shop/data')

local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local Skills = require('Module:Skills')
local ItemSourceTables = require('Module:Items/SourceTables')

function p.getAxeTable(frame)
	local toolArray = {}
	for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
		if Shared.contains(upgrade.name, 'Axe') then
			table.insert(toolArray, upgrade)
		end
	end

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

	local total = 0

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

		local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
		total = total + cutTime
		result = result..'||style="text-align:right"|-'..cutTime..'%'
		result = result..'||style="text-align:right"|-'..total..'%'
	end

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

function p.getPickaxeTable(frame)
	local toolArray = {}
	for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
		if Shared.contains(upgrade.name, 'Pickaxe') then
			table.insert(toolArray, upgrade)
		end
	end

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

	local total = 0
	local total2 = 0

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

		local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
		total = total + cutTime

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

		local OreDouble = tool.contains.modifiers.increasedChanceToDoubleOres
		total2 = total2 + OreDouble

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

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

function p.getRodTable(frame)
	local toolArray = {}
	for i, upgrade in Shared.skpairs(ShopData.Shop.SkillUpgrades) do
		if Shared.contains(upgrade.name, 'Fishing Rod') then
			table.insert(toolArray, upgrade)
		end
	end

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

	local total = 0

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

		local cutTime = tool.contains.modifiers.decreasedSkillIntervalPercent[1][2]
		total = total + cutTime
		result = result..'||style="text-align:right"|-'..cutTime..'%'
		result = result..'||style="text-align:right"|-'..total..'%'
	end

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

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

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

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

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

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

function p.getFishingJunkTable(frame)
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"')
	table.insert(resultPart, '\r\n!colspan="2"|Item!!Value')

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

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

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

	local mineData = Shared.clone(SkillData.Mining.Rocks)

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

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

	result = result..'\r\n|}'
	return result
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
	for i, gemData in Shared.spairs(Items.GemTable, function(t,a,b) return t[a].id < t[b].id end) do
		local gem = Items.getItemByID(gemData.id)
		result = result..'\r\n|-\r\n|style="min-width:25px"|'
		result = result..Icons.Icon({gem.name, type='item', size='50', notext=true})
		result = result..'||'..Icons.Icon({gem.name, type='item', noicon=true})
		result = result..'||style="text-align:right"|'..string.format("%.1f%%", gemData.chance)
		result = result..'||data-sort-value="'..gem.sellsFor..'"|'..Icons.GP(gem.sellsFor)
	end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

			if maxQty > 1 then
				normalTxt = normalTxt.. '1 - '
			end
			normalTxt = normalTxt..Shared.formatnum(row[3])

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

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

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

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

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

	local area = Skills.getThievingNPCArea(npc)
	areaTxt = areaTxt..'\r\n{|class="wikitable sortable"'
	areaTxt = areaTxt..'\r\n!Item!!Qty'
	areaTxt = areaTxt..'!!Price!!colspan="2"|Chance'
	local dropCount = Shared.tableCount(area.uniqueDrops)
	local dropLines = {}
	for i, drop in pairs(area.uniqueDrops) do
		local thisItem = Items.getItemByID(drop.itemID)
		local lineTxt = ''
		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..'||style="text-align:right"|'..Shared.fraction(1, 1/(SkillData.Thieving.AreaUniqueChance/100))
		lineTxt = lineTxt..'||'..Shared.round(SkillData.Thieving.AreaUniqueChance, 2, 2)..'%'
		dropLines[thisItem.name] = lineTxt
	end
	for i, txt in Shared.skpairs(dropLines) do
		areaTxt = areaTxt..txt
	end
	areaTxt = areaTxt..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
	areaTxt = areaTxt..'\r\n|style="text-align:right"|'..Shared.fraction(1, 1/(SkillData.Thieving.AreaUniqueChance/100))..'||'
	areaTxt = areaTxt..'style="text-align:right"|'..Shared.round(SkillData.Thieving.AreaUniqueChance, 2, 2)..'%'
	areaTxt = areaTxt..'\r\n|}'
	table.insert(sectionTxt, areaTxt)

	if npc.uniqueDrop ~= nil and npc.uniqueDrop.itemID > -1 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.'
		local thisItem = Items.getItemByID(npc.uniqueDrop.itemID)
		uniqueTxt = uniqueTxt..'\r\nThe unique drop for the '..npc.name..' is '
		if npc.uniqueDrop.qty > 1 then
			uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item', qty=npc.uniqueDrop.qty})
		else
			uniqueTxt = uniqueTxt..Icons.Icon({thisItem.name, type='item'})
		end

		table.insert(sectionTxt, uniqueTxt)
	end

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

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

	return p._getThievingNPCLootTables(npc)
end

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

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

	return result
end

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

	local areaArray = Shared.clone(SkillData.Thieving.Areas)
	table.sort(areaArray, function(a, b) return a.id < b.id end)
	for i, area in ipairs(areaArray) do
		local minLevel, npcList, areaItemList = nil, {}, {}
		-- Build NPC list & determine level for area, this is the minimum
		-- Thieving level required for all NPCs within that area
		if area.npcs ~= nil and Shared.tableCount(area.npcs) > 0 then
			for j, npcID in ipairs(area.npcs) do
				-- Don't bother cloning the NPC below since we aren't modifying any part of it
				local npc = SkillData.Thieving.NPCs[npcID + 1]
				if minLevel == nil or npc.level < minLevel then
					minLevel = npc.level
				end
				table.insert(npcList, Icons.Icon({npc.name, type='thieving'}))
			end
		else
			table.insert(npcList, '')
		end

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

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

	return table.concat(resultPart)
end

function p._getFarmingTable(category)
	local seedList = {}
	if category == 'Allotment' or category == 'Herb' or category == 'Tree' then
		seedList = Items.getItems(function(item) return item.tier == category end)
	else
		return 'ERROR: Invalid farming category. Please choose Allotment, Herb, or Tree'
	end

	local result = '{|class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan=2|Seeds!!'..Icons.Icon({'Farming', type='skill', notext=true})..' Level'
	result = result..'!!XP!!Growth Time!!Seed Value'
	if category == 'Allotment' then
		result = result..'!!colspan="2"|Crop!!Crop Healing!!Crop Value'
	elseif category == 'Herb' then
		result = result..'!!colspan="2"|Herb!!Herb Value'
	elseif category == 'Tree' then
		result = result..'!!colspan="2"|Logs!!Log Value'
	end
	result = result..'!!Seed Sources'

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

	for i, seed in pairs(seedList) do
		result = result..'\r\n|-'
		result = result..'\r\n|'..Icons.Icon({seed.name, type='item', size='50', notext=true})..'||[['..seed.name..']]'
		result = result..'||'..seed.farmingLevel..'||'..Shared.formatnum(seed.farmingXP)
		result = result..'||data-sort-value="'..seed.timeToGrow..'"|'..Shared.timeString(seed.timeToGrow, true)
		result = result..'||data-sort-value="'..seed.sellsFor..'"|'..Icons.GP(seed.sellsFor)

		local crop = Items.getItemByID(seed.grownItemID)
		result = result..'||'..Icons.Icon({crop.name, type='item', size='50', notext=true})..'||[['..crop.name..']]'
		if category == 'Allotment' then
			result = result..'||'..Icons.Icon({'Hitpoints', type='skill', notext=true})..' '..(crop.healsFor * 10)
		end
		result = result..'||data-sort-value="'..crop.sellsFor..'"|'..Icons.GP(crop.sellsFor)
		result = result..'||'..ItemSourceTables._getItemSources(seed)
	end

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

function p.getFarmingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame

	return p._getFarmingTable(category)
end

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

	local itemArray = Items.getItems(function(item) return item.grownItemID ~= nil end)

	table.sort(itemArray, function(a, b) return a.farmingLevel < b.farmingLevel end)

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

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

	return result
end

function p.getFarmingPlotTable(frame)
	local areaName = frame.args ~= nil and frame.args[1] or frame
	local patches = nil
	for i, area in Shared.skpairs(SkillData.Farming.Patches) do
		if area.areaName == areaName then
			patches = area.patches
			break
		end
	end
	if patches == nil then
		return "ERROR: Invalid area name.[[Category:Pages with script errors]]"
	end

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

	for i, patch in Shared.skpairs(patches) do
		result = result..'\r\n|-\r\n|'..i
		result = result..'||style="text-align:right;" data-sort-value="' .. patch.level .. '"|'..patch.level
		if patch.cost == 0 then
			result = result..'||Free'
		else
			result = result..'||style="text-align:right;" data-sort-value="'..patch.cost..'"|'..Icons.GP(patch.cost)
		end
	end

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

function p._buildAstrologyConstellationTable()
	local maxModifier = 5
	local result = '{|class="wikitable sortable stickyHeader"'
	result = result..'\r\n|- class="headerRow-0"'
	result = result..'\r\n!colspan="2"|Constellation!!'..Icons.Icon({"Astrology", type='skill', notext='true'})..' Level'
	result = result..'!!XP!!Skills!!Standard Modifiers!!Unique Modifiers'

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

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

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

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

	return result
end

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

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

return p