Module:Township: Difference between revisions

From Melvor Idle
(Fix issue where task table would only show up for task rewards if the task requirements included items)
(Add Soul Storage into list of building modifiers to check for; Removed getSeasonTable (this is now a manual table); Reduced icon size from 50px to 25px)
 
(10 intermediate revisions by 4 users not shown)
Line 162: Line 162:
education = 'Education',
education = 'Education',
storage = 'Storage',
storage = 'Storage',
worship = 'Worship'
soulStorage = 'Soul Storage',
worship = 'Worship',
fortification = 'Fortification'
}
}
local resourceText = function(resName, resType, quantity)
local resourceText = function(resName, resType, quantity)
local elemClass = (quantity < 0 and 'text-negative') or 'text-positive'
local elemClass = (quantity < 0 and 'text-negative') or 'text-positive'
local resIcon = Icons.Icon({resName, type=resType, notext=true})
local resIcon = Icons.Icon({resName, type=resType, notext=true})
return resIcon .. '&nbsp;<span class="' .. elemClass .. '">' .. Shared.numStrWithSign(quantity) .. '</span>'
return resIcon .. '&nbsp;<span class="' .. elemClass .. '">' .. Num.numStrWithSign(quantity) .. '</span>'
end
end


Line 257: Line 259:
end
end


-- Gets the Township level and population requirements for a tier
-- Gets the Township level or abyssalLevel, population and fortification requirements for a tier
-- Returns {population=X, level=X}
-- Returns {population=X, level=X} for non-abyssal tiers
function p._getTierRequirements(tier)
-- Returns {population=X, abyssalLevel=X, fortification=X} for abyssal tiers
return Township.populationForTier[tier]
function p._getTierRequirements(tier, abyssalTier)
local tierData = Township.populationForTier[tier]
if abyssalTier ~= nil then
local abyssalTierData = Shared.clone(Township.abyssalTierRequirements[abyssalTier + 1])
abyssalTierData.population = tierData.population
return abyssalTierData
else
return tierData
end
end
end


-- Returns a string containing the Township level and population requirements for a tier
-- Returns a string containing the Township level and population requirements for a tier
function p._getTierText(tier)
function p._getTierText(tier, abyssalTier)
local tierData = p._getTierRequirements(tier)
local realmID = (abyssalTier ~= nil and 'melvorItA:Abyssal' or 'melvorD:Melvor')
local tierData = p._getTierRequirements(tier, abyssalTier)
if tierData ~= nil then
if tierData ~= nil then
local tierText = Icons._SkillReq('Township', tierData.level, false)
local tierText = Icons._SkillReq('Township', tierData.abyssalLevel or tierData.level, false, realmID)
if tierData.population > 0 then
if tierData.population ~= nil and tierData.population > 0 then
tierText = tierText .. '<br/>' .. Icons.Icon({'Population', type='township', notext=true}) .. '&nbsp;' .. Shared.formatnum(tierData.population)
tierText = tierText .. '<br/>' .. Icons.Icon({'Population', type='township', notext=true}) .. '&nbsp;' .. Num.formatnum(tierData.population)
end
if tierData.fortification ~= nil and tierData.fortification > 0 then
tierText = tierText .. '<br/>' .. Icons.Icon({'Fortification', type='township', notext=true}) .. '&nbsp;' .. Num.formatnum(tierData.fortification) .. '%'
end
end
return tierText
return tierText
Line 275: Line 289:
end
end


-- Generates a table of all seasons, their type/requirements, and modifiers
function p.getBuildings(checkFunc)
function p.getSeasonTable(frame)
local result = {}
-- Manual data specifying the worship requirement for those rare seasons
for i, building in pairs(p.Township.buildings) do
local seasonReqs = {
if checkFunc(building) then
["Nightfall"] = Icons.Icon({'Township%23Worship', 'Bane Worship', img='Statue of Bane', type='building'}),
table.insert(result, building)
["SolarEclipse"] = Icons.Icon({'Township%23Worship', 'The Herald Worship', img='Statue of The Herald', type='building'}),
end
["Lemon"] = Icons.Icon({'Ancient_Relics', 'Ancient Relics', img='Ancient Relics'}),
end
["EternalDarkness"] = Icons.Icon({'Township%23Worship', 'Xon Worship', img='Statue of Xon', type='building'}),
return result
}
end


local seasons = Shared.shallowClone(Township.seasons)
function p.getSeasons(checkFunc)
table.sort(seasons, function(a, b) return a.order < b.order end)
local result = {}
for i, season in pairs(p.Township.seasons) do
if checkFunc(season) then
table.insert(result, season)
end
end
return result
end


local resultPart = {}
function p.getWorships(checkFunc)
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
local result = {}
table.insert(resultPart, '\n|- class="headerRow-0"')
for i, worship in pairs(p.Township.worships) do
table.insert(resultPart, '\n!colspan="2" | Season\n!Type\n!Modifiers')
if checkFunc(worship) then
 
table.insert(result, worship)
for i, season in ipairs(seasons) do
local ns, localSeasonID = Shared.getLocalID(season.id)
local reqs = seasonReqs[localSeasonID]
table.insert(resultPart, '\n|-')
table.insert(resultPart, '\n|class="table-img"| ' .. Icons.Icon({season.name, type='township', size=50, nolink=true, notext=true}))
table.insert(resultPart, '\n| ' .. Icons.Icon({season.name, type='township', nolink=true, noicon=true}))
table.insert(resultPart, '\n| ' .. (season.order <= 3 and 'Regular' or 'Rare'))
if reqs ~= nil then
table.insert(resultPart, '<br/>Requires ' .. reqs)
end
end
table.insert(resultPart, '\n| ' .. Modifiers.getModifiersText(season.modifiers))
end
end
table.insert(resultPart, '\n|}')
return result
 
return table.concat(resultPart)
end
end


Line 315: Line 324:
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
table.insert(resultPart, '\n|- class="headerRow-0"')
table.insert(resultPart, '\n|- class="headerRow-0"')
table.insert(resultPart, '\n!rowspan="2" colspan="2"| Biome\n!colspan="2"| Requirements')
table.insert(resultPart, '\n!rowspan="2" colspan="2"| Biome\n!colspan="3"| Requirements')
table.insert(resultPart, '\n|- class="headerRow-1"')
table.insert(resultPart, '\n|- class="headerRow-1"')
table.insert(resultPart, '\n! ' .. Icons.Icon({'Township', 'Level', type='skill', nolink=true}))
table.insert(resultPart, '\n! ' .. Icons.Icon({'Township', 'Level', type='skill', nolink=true}))
table.insert(resultPart, '\n! ' .. Icons.Icon({'Township', 'Population', img='Population', type='township', section='Population' }))
table.insert(resultPart, '\n! ' .. Icons.Icon({'Township', 'Population', img='Population', type='township', section='Population' }))
table.insert(resultPart, '\n! ' .. Icons.Icon({'Township', 'Forification', img='Fortification', type='township', section='Fortification' }))


for i, biome in ipairs(Township.biomes) do
for i, biome in ipairs(Township.biomes) do
local reqs = p._getTierRequirements(biome.tier)
local reqs = p._getTierRequirements(biome.tier, biome.abyssalTier)
table.insert(resultPart, '\n|-\n|class="table-img"| ' .. Icons.Icon({biome.name, type='biome', size=50, nolink=true, notext=true}))
local fortification = reqs.fortification or 0
table.insert(resultPart, '\n|-\n|class="table-img"| ' .. Icons.Icon({biome.name, type='biome', nolink=true, notext=true}))
table.insert(resultPart, '\n| ' .. biome.name)
table.insert(resultPart, '\n| ' .. biome.name)
table.insert(resultPart, '\n|style="text-align:right"| ' .. reqs.level)
table.insert(resultPart, '\n|style="text-align:right"| ' .. (reqs.abyssalLevel or reqs.level))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. reqs.population .. '"| ' .. Shared.formatnum(reqs.population))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. reqs.population .. '"| ' .. Num.formatnum(reqs.population))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. fortification .. '"| ' .. Num.formatnum(fortification))
end
end
table.insert(resultPart, '\n|}')
table.insert(resultPart, '\n|}')
Line 342: Line 354:
local level = mw.html.create('tr'):addClass('sorttop')
local level = mw.html.create('tr'):addClass('sorttop')
local pop = mw.html.create('tr'):addClass('sorttop')
local pop = mw.html.create('tr'):addClass('sorttop')
local fort = mw.html.create('tr'):addClass('sorttop')


header:tag('th')
header:tag('th')
Line 350: Line 363:
pop:tag('th')
pop:tag('th')
:wikitext(Icons.Icon({'Township', 'Population', img='Population', type='township', section='Population' }))
:wikitext(Icons.Icon({'Township', 'Population', img='Population', type='township', section='Population' }))
fort:tag('th')
:wikitext(Icons.Icon({'Township', 'Fortification', img='Fortification', type='township', section='Fortification' }))
for _, biome in ipairs(Township.biomes) do
for _, biome in ipairs(Township.biomes) do
local reqs = p._getTierRequirements(biome.tier)
local reqs = p._getTierRequirements(biome.tier, biome.abyssalTier)
header:tag('th')
header:tag('th')
:wikitext(Icons.Icon({biome.name, type='biome', notext=true, nolink=true}).. '<br/>' .. biome.name)
:wikitext(Icons.Icon({biome.name, type='biome', notext=true, nolink=true}).. '<br/>' .. biome.name)
level:tag('td')
level:tag('td')
:wikitext(Num.formatnum(reqs.level))
:wikitext(Num.formatnum((reqs.abyssalLevel or reqs.level)))
pop:tag('td')
pop:tag('td')
:wikitext(Num.formatnum(reqs.population))
:wikitext(Num.formatnum(reqs.population))
fort:tag('td')
:wikitext(Num.formatnum((reqs.fortification or 0)))
end
end
Line 364: Line 381:
tbl:node(level)
tbl:node(level)
tbl:node(pop)
tbl:node(pop)
tbl:node(fort)


for _, _building in ipairs(p._sortedBuildings(false)) do
for _, _building in ipairs(p._sortedBuildings(false)) do
Line 427: Line 445:
if firstRow then
if firstRow then
table.insert(resultPart, '\n|-')
table.insert(resultPart, '\n|-')
table.insert(resultPart, '\n|class="table-img"' .. rowSpan .. '| ' .. Icons.Icon({buildingName, type='building', notext=true, size=50}))
table.insert(resultPart, '\n|class="table-img"' .. rowSpan .. '| ' .. Icons.Icon({buildingName, type='building', notext=true}))
table.insert(resultPart, '\n' .. rowSpanOnly .. '| ' .. Icons.getExpansionIcon(building.id) .. Icons.Icon({buildingName, type='building', noicon=true}))
table.insert(resultPart, '\n' .. rowSpanOnly .. '| ' .. Icons.getExpansionIcon(building.id) .. Icons.Icon({buildingName, type='building', noicon=true}))
table.insert(resultPart, '\n|' .. 'data-sort-value="' .. building.tier .. '"' .. rowSpan .. '| ' .. (p._getTierText(building.tier) or ''))
table.insert(resultPart, '\n|' .. 'data-sort-value="' .. building.tier .. '"' .. rowSpan .. '| ' .. (p._getTierText(building.tier, building.abyssalTier) or ''))
table.insert(resultPart, '\n|style="text-align:right"' .. rowSpan .. '| ' .. building.maxUpgrades)
table.insert(resultPart, '\n|style="text-align:right"' .. rowSpan .. '| ' .. building.maxUpgrades)
firstRow = false
firstRow = false
Line 479: Line 497:
local costSort = i * 10000 + resQty
local costSort = i * 10000 + resQty


table.insert(resultPart, '\n|-\n| ' .. Icons.Icon({item.name, type='item', size=50, notext=true}))
table.insert(resultPart, '\n|-\n| ' .. Icons.Icon({item.name, type='item', notext=true}))
table.insert(resultPart, '\n| ' .. Icons.getExpansionIcon(item.id) .. Icons.Icon({item.name, type='item', noicon=true}))
table.insert(resultPart, '\n| ' .. Icons.getExpansionIcon(item.id) .. Icons.Icon({item.name, type='item', noicon=true}))
table.insert(resultPart, '\n| ' .. itemDesc)
table.insert(resultPart, '\n| ' .. itemDesc)
Line 494: Line 512:
function p.getWorshipTable()
function p.getWorshipTable()
local function getCheckpointCell(checkpoint)
local function getCheckpointCell(checkpoint)
return '\n|-\n!' .. checkpoint .. '%<br/>' .. Shared.formatnum(checkpoint * Township.maxWorship / 100) .. '/' .. Shared.formatnum(Township.maxWorship)
return '\n|-\n!' .. checkpoint .. '%<br/>' .. Num.formatnum(checkpoint * Township.maxWorship / 100) .. '/' .. Num.formatnum(Township.maxWorship)
end
end


Line 592: Line 610:
table.insert(ret, '\n|-\n| <b>Building ID:</b> ' .. building.id)
table.insert(ret, '\n|-\n| <b>Building ID:</b> ' .. building.id)
-- Tier
-- Tier
local tier = p._getTierText(building.tier)
local tier = p._getTierText(building.tier, building.abyssalTier)
table.insert(ret, '\n|-\n| <b>Requirements:</b><br/>' .. tier)
table.insert(ret, '\n|-\n| <b>Requirements:</b><br/>' .. tier)


Line 638: Line 656:
-- Maximum built
-- Maximum built
local biomeCount = Shared.tableCount(building.biomes)
local biomeCount = Shared.tableCount(building.biomes)
local maxText = Shared.formatnum(building.maxUpgrades)
local maxText = Num.formatnum(building.maxUpgrades)
if biomeCount > 1 then
if biomeCount > 1 then
maxText = maxText .. ' per biome, ' .. Shared.formatnum(biomeCount * building.maxUpgrades) .. ' total'
maxText = maxText .. ' per biome, ' .. Num.formatnum(biomeCount * building.maxUpgrades) .. ' total'
end
end
table.insert(ret, '\n|-\n| <b>Maximum Built:</b><br/>' .. maxText)
table.insert(ret, '\n|-\n| <b>Maximum Built:</b><br/>' .. maxText)
Line 709: Line 727:
table.insert(ret, '\n|-\n!colspan="2"| Requirements')
table.insert(ret, '\n|-\n!colspan="2"| Requirements')
for _, building in ipairs(buildingList) do
for _, building in ipairs(buildingList) do
table.insert(ret, '\n|' .. p._getTierText(building.tier))
table.insert(ret, '\n|' .. p._getTierText(building.tier, building.abyssalTier))
end
end


Line 811: Line 829:
for goalIdx, goalObj in ipairs(goalData) do
for goalIdx, goalObj in ipairs(goalData) do
if goalType == 'items' then
if goalType == 'items' then
goalText = Shared.formatnum(goalObj.quantity) .. ' ' .. getItemText(goalObj.id)
goalText = Num.formatnum(goalObj.quantity) .. ' ' .. getItemText(goalObj.id)
elseif goalType == 'monsters' then
elseif goalType == 'monsters' then
goalText = Shared.formatnum(goalObj.quantity) .. ' ' .. getMonsterText(goalObj.id)
goalText = Num.formatnum(goalObj.quantity) .. ' ' .. getMonsterText(goalObj.id)
elseif goalType == 'monsterWithItems' then
elseif goalType == 'monsterWithItems' then
local itemsText = {}
local itemsText = {}
Line 819: Line 837:
table.insert(itemsText, getItemText(itemID))
table.insert(itemsText, getItemText(itemID))
end
end
goalText = Shared.formatnum(goalObj.quantity) .. ' ' .. getMonsterText(goalObj.monsterID) .. ' with ' .. table.concat(itemsText, ', ') .. ' equipped'
goalText = Num.formatnum(goalObj.quantity) .. ' ' .. getMonsterText(goalObj.monsterID) .. ' with ' .. table.concat(itemsText, ', ') .. ' equipped'
elseif goalType == 'skillXP' then
elseif goalType == 'skillXP' then
local skillName = GameData.getSkillData(goalObj.id).name
local skillName = GameData.getSkillData(goalObj.id).name
goalText = Shared.formatnum(goalObj.quantity) .. ' ' .. Icons.Icon({skillName, type='skill'}) .. ' XP'
goalText = Num.formatnum(goalObj.quantity) .. ' ' .. Icons.Icon({skillName, type='skill'}) .. ' XP'
elseif goalType == 'buildings' then
elseif goalType == 'buildings' then
local buildingName = p._GetBuildingByID(goalObj.id).name
local buildingName = p._GetBuildingByID(goalObj.id).name
goalText = Shared.formatnum(goalObj.quantity) .. ' ' .. Icons.Icon({buildingName, type='building'})
goalText = Num.formatnum(goalObj.quantity) .. ' ' .. Icons.Icon({buildingName, type='building'})
elseif goalType == 'numPOIs' then
elseif goalType == 'numPOIs' then
local mapName = GameData.getEntityByID(GameData.skillData.Cartography.worldMaps, goalObj.worldMapID).name
local mapName = GameData.getEntityByID(GameData.skillData.Cartography.worldMaps, goalObj.worldMapID).name
goalText = 'Discover ' .. Shared.formatnum(goalObj.quantity) .. ' Points of Interest in ' .. Icons.Icon({'Cartography', type='skill'}) .. ' world map of ' .. mapName
goalText = 'Discover ' .. Num.formatnum(goalObj.quantity) .. ' Points of Interest in ' .. Icons.Icon({'Cartography', type='skill'}) .. ' world map of ' .. mapName
else
else
goalText = Shared.printError('Unknown goal type: ' .. (goalType or 'nil'))
goalText = Shared.printError('Unknown goal type: ' .. (goalType or 'nil'))
Line 841: Line 859:
-- Goal data is another value of some type
-- Goal data is another value of some type
if goalType == 'numRefinements' then
if goalType == 'numRefinements' then
goalText = 'Refine dig site maps in ' .. Icons.Icon({'Cartography', type='skill'}) .. ' ' .. Shared.formatnum(goalData) .. ' times'
goalText = 'Refine dig site maps in ' .. Icons.Icon({'Cartography', type='skill'}) .. ' ' .. Num.formatnum(goalData) .. ' times'
else
else
goalText = Shared.printError('Unknown goal type: ' .. (goalType or 'nil'))
goalText = Shared.printError('Unknown goal type: ' .. (goalType or 'nil'))
Line 873: Line 891:
local rewards = {}
local rewards = {}
local rewardsVariableQty = {}
local rewardsVariableQty = {}
if task.rewards.currency ~= nil then
if task.rewards.currencies ~= nil then
for _, currReward in ipairs(task.rewards.currency) do
for _, currReward in ipairs(task.rewards.currencies) do
if isDailyTask then
if isDailyTask and currReward.id ~= 'melvorD:GP' then
table.insert(rewardsVariableQty, Icons._Currency(currReward.id))
table.insert(rewardsVariableQty, Icons._Currency(currReward.id))
else
elseif not isDailyTask then
table.insert(rewards, Icons._Currency(currReward.id, currReward.qty))
table.insert(rewards, Icons._Currency(currReward.id, currReward.quantity))
end
end
end
end
Line 884: Line 902:
for _, item in ipairs(task.rewards.items) do
for _, item in ipairs(task.rewards.items) do
local itemname = GameData.getEntityByID('items', item.id).name
local itemname = GameData.getEntityByID('items', item.id).name
table.insert(rewards, Shared.formatnum(item.quantity)..' '..Icons.Icon({itemname, type='item'}))
table.insert(rewards, Num.formatnum(item.quantity)..' '..Icons.Icon({itemname, type='item'}))
end
end
for _, skill in ipairs(task.rewards.skillXP) do
for _, skill in ipairs(task.rewards.skillXP) do
if not (isDailyTask and skill.id == 'melvorD:Township') then
if not (isDailyTask and skill.id == 'melvorD:Township') then
local skillname = GameData.getSkillData(skill.id).name
local skillname = GameData.getSkillData(skill.id).name
table.insert(rewards, Shared.formatnum(skill.quantity)..' '..Icons.Icon({skillname, type='skill'})..' XP')
table.insert(rewards, Num.formatnum(skill.quantity)..' '..Icons.Icon({skillname, type='skill'})..' XP')
end
end
end
end
for _, townshipResource in ipairs(task.rewards.townshipResources) do
for _, townshipResource in ipairs(task.rewards.townshipResources) do
local resourcename = p._getResourceByID(townshipResource.id).name
local resourcename = p._getResourceByID(townshipResource.id).name
table.insert(rewards, Shared.formatnum(townshipResource.quantity)..' '..Icons.Icon({resourcename, type='resource'}))
table.insert(rewards, Num.formatnum(townshipResource.quantity)..' '..Icons.Icon({resourcename, type='resource'}))
end
end
if not Shared.tableIsEmpty(rewardsVariableQty) then
if not Shared.tableIsEmpty(rewardsVariableQty) then
Line 927: Line 945:
local taskcount = 0
local taskcount = 0
local ret = {}
local ret = {}
table.insert(ret, '{| class="wikitable lighttable stickyHeader" style="text-align:left"')
table.insert(ret, '{| class="wikitable lighttable stickyHeader mw-collapsible" style="text-align:left"')
table.insert(ret, '\n|- class="headerRow-0"')
table.insert(ret, '\n|- class="headerRow-0"')
table.insert(ret, '\n!Task')
table.insert(ret, '\n!Task')
Line 966: Line 984:
if referenceType == 'dungeon' then
if referenceType == 'dungeon' then
-- We get the tasks associated with all monsters in the dungeon
-- We get the tasks associated with all monsters in the dungeon
local monsters = GameData.getEntityByName('dungeons', referenceName).monsterIDs
local area = nil
local areaTypes = {'dungeons', 'abyssDepths', 'strongholds'}
for _, areaType in ipairs(areaTypes) do
area = GameData.getEntityByName(areaType, referenceName)
if area ~= nil then
break
end
end
local monsters = area.monsterIDs
for _, monster in ipairs(monsters) do
for _, monster in ipairs(monsters) do
IDs[monster] = true
IDs[monster] = true

Latest revision as of 05:33, 8 October 2024

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

local Shared = require('Module:Shared')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
local Monsters = require('Module:Monsters')
local Shop = require('Module:Shop')
local GameData = require('Module:GameData')
local Modifiers = require('Module:Modifiers')
local Num = require('Module:Number')

local p = {}

local Township = GameData.getSkillData('melvorD:Township')
p.Township = Township

-- Gets a Township building by ID, e.g. melvorF:Hunters_Cabin
function p._getBuildingByID(id)
	-- Check for the special statue case
	if id == 'melvorF:Statues' then
		local building = Shared.clone(GameData.getEntityByID(Township.buildings, id))
		building.name = 'Statue of Worship'
		return building
	else
		return GameData.getEntityByID(Township.buildings, id)
	end
end

-- Gets a Township building by name, e.g. Hunters Cabin
function p._getBuildingByName(name)
	-- Check for the special statue case
	if name == 'Statues' then
		name = 'Statue of Worship'
	end
	local STATUE_OF = 'Statue of '
	if string.sub(name, 1, string.len(STATUE_OF)) == STATUE_OF then
		local building = Shared.clone(GameData.getEntityByID(Township.buildings, 'melvorF:Statues'))
		building.name = name
		return building
	else
		return GameData.getEntityByName(Township.buildings, name)
	end
end

-- Gets a resource from id
function p._getResourceByID(id)
	return GameData.getEntityByID(Township.resources, id)
end

-- Given a building, find the next building upgrade
function p._getBuildingUpgrade(building)
	local function checkFunc(entity)
		return entity.upgradesFrom ~= nil and entity.upgradesFrom == building.id
	end
	local upgradesTo = GameData.getEntities(Township.buildings, checkFunc)
	if #upgradesTo > 0 then
		return upgradesTo[1]
	end
	return nil
end

-- Given a building, find the building's downgrade
function p._getBuildingDowngrade(building)
	if building.upgradesFrom ~= nil then
		return p._getBuildingByID(building.upgradesFrom)
	end
	return nil
end

-- Given a building and biome ID, returns the cost of constructing the building
-- within that biome as a human readable text string. Returns nil if the building
-- cannot be built within that biome.
function p._getBuildingCostText(building, biomeID, delimiter)
	-- Basic validation of inputs
	if type(building) == 'table' and building.cost ~= nil and biomeID ~= nil then
		local delim = delimiter
		if delim == nil then
			delim = ', '
		end
		for i, costDef in ipairs(building.cost) do
			if costDef.biomeID == biomeID then
				local resultPart = {}
				for j, cost in ipairs(costDef.cost) do
					local resData = p._getResourceByID(cost.id)
					if resData ~= nil then
						table.insert(resultPart, Icons.Icon({resData.name, type='resource', notext=true, nolink=true, qty=cost.quantity}))
					end
				end
				return table.concat(resultPart, delim)
			end
		end
	end
end

-- Given a building, groups biomes for which that building has a common cost
function p._getBuildingGroupedCosts(building)
	local biomeGroups = {}
	for i, biomeID in ipairs(building.biomes) do
		local currentBiomeCost = p._getBuildingCostText(building, biomeID)
		local found = false
		for j, biomeGroup in ipairs(biomeGroups) do
			if biomeGroup.cost == currentBiomeCost then
				-- Another biome exists with this cost
				table.insert(biomeGroup.biomeIDs, biomeID)
				found = true
				break
			end
		end
		if not found then
			table.insert(biomeGroups, { biomeIDs = { biomeID }, cost = currentBiomeCost})
		end
	end
	return biomeGroups
end

-- Given a building, returns a text string repesenting the building costs for all biomes
function p._getBuildingGroupedCostText(building)
	local resultPart = {}
	local biomeGroups = p._getBuildingGroupedCosts(building)
	if Shared.tableCount(biomeGroups) == 1 then
		-- If only one entry then simply output the cost
		table.insert(resultPart, biomeGroups[1].cost)
	else
		-- Otherwise, split by biome group
		for i, biomeGroup in ipairs(biomeGroups) do
			local biomeText = {}
			for j, biomeID in ipairs(biomeGroup.biomeIDs) do
				local biome = GameData.getEntityByID(Township.biomes, biomeID)
				table.insert(biomeText, Icons.Icon({biome.name, type='biome', notext=true, nolink=true, alt=biome.name}))
			end
			table.insert(resultPart, table.concat(biomeText, ', ') .. ': ' .. biomeGroup.cost)
		end
	end
	return table.concat(resultPart, '<br/>')
end

-- Given a building and biome ID, returns a string displaying the building's benefits,
-- or nil if no benefits
function p._getBuildingBenefitText(building, biomeID, includeModifiers, delimiter)
	-- Basic validation of inputs
	if type(building) == 'table' and building.provides ~= nil and biomeID ~= nil then
		local delim = delimiter
		if delim == nil then
			delim = ', '
		end
		local includeMods = includeModifiers
		if includeMods == nil then
			includeMods = false
		end

		local providesData = nil
		for i, provides in ipairs(building.provides) do
			if provides.biomeID == biomeID then
				providesData = provides
				break
			end
		end

		if providesData ~= nil then
			local resultPart = {}
			local stats = {
				population = 'Population',
				happiness = 'Happiness',
				education = 'Education',
				storage = 'Storage',
				soulStorage = 'Soul Storage',
				worship = 'Worship',
 				fortification = 'Fortification'
			}
			local resourceText = function(resName, resType, quantity)
				local elemClass = (quantity < 0 and 'text-negative') or 'text-positive'
				local resIcon = Icons.Icon({resName, type=resType, notext=true})
				return resIcon .. '&nbsp;<span class="' .. elemClass .. '">' .. Num.numStrWithSign(quantity) .. '</span>'
			end

			-- Resources
			if providesData.resources ~= nil then
				for i, resource in ipairs(providesData.resources) do
					local resData = p._getResourceByID(resource.id)
					if resData ~= nil and resource.quantity ~= 0 then
						table.insert(resultPart, resourceText(resData.name, 'resource', resource.quantity))
					end
				end
			end

			-- Other stats
			for key, stat in pairs(stats) do
				local quantity = providesData[key]
				if quantity ~= nil and quantity ~= 0 then
					table.insert(resultPart, resourceText(stat, 'township', quantity))
				end
			end

			-- Modifiers
			if includeMods and building.modifiers ~= nil then
				table.insert(resultPart, Modifiers.getModifiersText(building.modifiers))
			end

			if not Shared.tableIsEmpty(resultPart) then
				return table.concat(resultPart, delim)
			end
		end
	end
end

-- Given a building, groups biomes for which that building has a common benefit/provides
function p._getBuildingGroupedBenefits(building, includeModifiers)
	if includeModifiers == nil then
		includeModifiers = true
	end
	local biomeGroups = {}
	for i, biomeID in ipairs(building.biomes) do
		local currentBiomeBenefit = p._getBuildingBenefitText(building, biomeID, includeModifiers)
		local found = false
		for j, biomeGroup in ipairs(biomeGroups) do
			if biomeGroup.benefit == currentBiomeBenefit then
				-- Another biome exists with this cost
				table.insert(biomeGroup.biomeIDs, biomeID)
				found = true
				break
			end
		end
		if not found then
			table.insert(biomeGroups, { biomeIDs = { biomeID }, cost = currentBiomeBenefit})
		end
	end
	return biomeGroups
end

-- Given a building, returns a text string repesenting the building benefits for all biomes
function p._getBuildingGroupedBenefitText(building, includeModifiers)
	if includeModifiers == nil then
		includeModifiers = true
	end
	local resultPart = {}
	local biomeGroups = p._getBuildingGroupedBenefits(building, includeModifiers)
	if Shared.tableCount(biomeGroups) == 1 then
		-- If only one entry then simply output the cost
		table.insert(resultPart, biomeGroups[1].cost)
	else
		-- Otherwise, split by biome group
		for i, biomeGroup in ipairs(biomeGroups) do
			local biomeText = {}
			for j, biomeID in ipairs(biomeGroup.biomeIDs) do
				local biome = GameData.getEntityByID(Township.biomes, biomeID)
				table.insert(biomeText, Icons.Icon({biome.name, type='biome', notext=true, nolink=true, alt=biome.name}))
			end
			table.insert(resultPart, table.concat(biomeText, ', ') .. ': ' .. biomeGroup.cost)
		end
	end
	return table.concat(resultPart, '<br/>')
end

-- Returns a sorted list of all Township buildings
function p._sortedBuildings(keepUnsorted)
	local ku = true
	if keepUnsorted ~= nil then
		ku = keepUnsorted
	end
	return GameData.sortByOrderTable(Township.buildings, Township.buildingDisplayOrder, ku)
end

-- Gets the Township level or abyssalLevel, population and fortification requirements for a tier
-- Returns {population=X, level=X} for non-abyssal tiers
-- Returns {population=X, abyssalLevel=X, fortification=X} for abyssal tiers
function p._getTierRequirements(tier, abyssalTier)
	local tierData = Township.populationForTier[tier]
	if abyssalTier ~= nil then
		local abyssalTierData = Shared.clone(Township.abyssalTierRequirements[abyssalTier + 1])
		abyssalTierData.population = tierData.population
		return abyssalTierData
	else
		return tierData
	end
end

-- Returns a string containing the Township level and population requirements for a tier
function p._getTierText(tier, abyssalTier)
	local realmID = (abyssalTier ~= nil and 'melvorItA:Abyssal' or 'melvorD:Melvor')
	local tierData = p._getTierRequirements(tier, abyssalTier)
	if tierData ~= nil then
		local tierText = Icons._SkillReq('Township', tierData.abyssalLevel or tierData.level, false, realmID)
		if tierData.population ~= nil and tierData.population > 0 then
			tierText = tierText .. '<br/>' .. Icons.Icon({'Population', type='township', notext=true}) .. '&nbsp;' .. Num.formatnum(tierData.population)
		end
		if tierData.fortification ~= nil and tierData.fortification > 0 then
			tierText = tierText .. '<br/>' .. Icons.Icon({'Fortification', type='township', notext=true}) .. '&nbsp;' .. Num.formatnum(tierData.fortification) .. '%'
		end
		return tierText
	end
end

function p.getBuildings(checkFunc)
	local result = {}
	for i, building in pairs(p.Township.buildings) do
		if checkFunc(building) then
			table.insert(result, building)
		end
	end
	return result
end

function p.getSeasons(checkFunc)
	local result = {}
	for i, season in pairs(p.Township.seasons) do
		if checkFunc(season) then
			table.insert(result, season)
		end
	end
	return result
end

function p.getWorships(checkFunc)
	local result = {}
	for i, worship in pairs(p.Township.worships) do
		if checkFunc(worship) then
			table.insert(result, worship)
		end
	end
	return result
end

-- Generates a table listing all biomes and their associated requirements
function p.getBiomeTable(frame)
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\n|- class="headerRow-0"')
	table.insert(resultPart, '\n!rowspan="2" colspan="2"| Biome\n!colspan="3"| Requirements')
	table.insert(resultPart, '\n|- class="headerRow-1"')
	table.insert(resultPart, '\n! ' .. Icons.Icon({'Township', 'Level', type='skill', nolink=true}))
	table.insert(resultPart, '\n! ' .. Icons.Icon({'Township', 'Population', img='Population', type='township', section='Population' }))
	table.insert(resultPart, '\n! ' .. Icons.Icon({'Township', 'Forification', img='Fortification', type='township', section='Fortification' }))

	for i, biome in ipairs(Township.biomes) do
		local reqs = p._getTierRequirements(biome.tier, biome.abyssalTier)
		local fortification = reqs.fortification or 0
		table.insert(resultPart, '\n|-\n|class="table-img"| ' .. Icons.Icon({biome.name, type='biome', nolink=true, notext=true}))
		table.insert(resultPart, '\n| ' .. biome.name)
		table.insert(resultPart, '\n|style="text-align:right"| ' .. (reqs.abyssalLevel or reqs.level))
		table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. reqs.population .. '"| ' .. Num.formatnum(reqs.population))
		table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. fortification .. '"| ' .. Num.formatnum(fortification))
	end
	table.insert(resultPart, '\n|}')

	return table.concat(resultPart)
end

-- Generates a table showing which buildings can be built in which biomes
-- Skips upgraded buildings
function p.getBuildingBiomeTable(frame)
	local tbl = mw.html.create('table')
		:addClass('wikitable sortable stickyHeader')
		:css('text-align', 'center')

	local header =	mw.html.create('tr'):addClass('headerRow-0')
	local level =	mw.html.create('tr'):addClass('sorttop')
	local pop = 	mw.html.create('tr'):addClass('sorttop')
	local fort = 	mw.html.create('tr'):addClass('sorttop')

	header:tag('th')
		:css('z-index', '2')
		:wikitext('Building')
	level:tag('th')
		:wikitext(Icons.Icon({'Township', 'Level', type='skill', nolink=true}))
	pop:tag('th')
		:wikitext(Icons.Icon({'Township', 'Population', img='Population', type='township', section='Population' }))
	fort:tag('th')
		:wikitext(Icons.Icon({'Township', 'Fortification', img='Fortification', type='township', section='Fortification' }))
	
	for _, biome in ipairs(Township.biomes) do
		local reqs = p._getTierRequirements(biome.tier, biome.abyssalTier)
		header:tag('th')
			:wikitext(Icons.Icon({biome.name, type='biome', notext=true, nolink=true}).. '<br/>' .. biome.name)
		level:tag('td')
			:wikitext(Num.formatnum((reqs.abyssalLevel or reqs.level)))
		pop:tag('td')
			:wikitext(Num.formatnum(reqs.population))
		fort:tag('td')
			:wikitext(Num.formatnum((reqs.fortification or 0)))
	end
	
	tbl:node(header)
	tbl:node(level)
	tbl:node(pop)
	tbl:node(fort)

	for _, _building in ipairs(p._sortedBuildings(false)) do
		-- Fix melvorF:Statues
		local building = p._getBuildingByID(_building.id)
		-- Skip upgraded buildings
		if p._getBuildingDowngrade(building) == nil then
			-- Populate the biome habitability data
			local buildingBiomes = {}
			-- Set all valid biomes to true
			for _, biomeid in ipairs(building.biomes) do
				buildingBiomes[biomeid] = true
			end

			local trow = tbl:tag('tr')
			trow:tag('th')
				:css('text-align', 'left')
				:attr('data-sort-value', building.name)
				:wikitext(Icons.Icon({building.name, type='building'}))

			for _, biome in ipairs(Township.biomes) do
				if buildingBiomes[biome.id] then
					trow:tag('td')
						:addClass('table-positive')
						:wikitext('✓')
				else
					trow:tag('td')
				end
			end
		end
	end

	return tostring(tbl)
end

-- Generates a table contaning each building plus their relevant information
function p.getBuildingTable(frame)
	local resultPart = {}

	-- Change structure of biomes data for ease of use later
	local biomesByID = {}
	for i, biome in ipairs(Township.biomes) do
		biomesByID[biome.id] = biome
	end

	-- Generate table header
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\n|- class="headerRow-0"')
	table.insert(resultPart, '\n!colspan="2"|Building\n!Requirements\n!Max Built')
	table.insert(resultPart, '\n!Biomes\n!Cost\n!Provides')

	local buildings = p._sortedBuildings(false)

	for i, building in ipairs(buildings) do
		-- Number of rows per building is dictated by number of biomes
		local buildingName = (building.id == 'melvorF:Statues' and 'Statue of Worship') or building.name
		local firstRow = true
		local rowCount = Shared.tableCount(building.biomes)
		local rowSpan = (rowCount > 1 and ' rowspan="' .. rowCount .. '"') or ''
		local rowSpanOnly = (rowCount > 1 and '|' .. rowSpan) or ''
		for j, biomeID in ipairs(building.biomes) do
			local biome = biomesByID[biomeID]
			if firstRow then
				table.insert(resultPart, '\n|-')
				table.insert(resultPart, '\n|class="table-img"' .. rowSpan .. '| ' .. Icons.Icon({buildingName, type='building', notext=true}))
				table.insert(resultPart, '\n' .. rowSpanOnly .. '| ' .. Icons.getExpansionIcon(building.id) .. Icons.Icon({buildingName, type='building', noicon=true}))
				table.insert(resultPart, '\n|' .. 'data-sort-value="' .. building.tier .. '"' .. rowSpan .. '| ' .. (p._getTierText(building.tier, building.abyssalTier) or ''))
				table.insert(resultPart, '\n|style="text-align:right"' .. rowSpan .. '| ' .. building.maxUpgrades)
				firstRow = false
			else
				table.insert(resultPart, '\n|-')
			end
			-- This section generates by biome rows
			table.insert(resultPart, '\n| ' .. Icons.Icon({biome.name, type='biome', nolink=true}))
			table.insert(resultPart, '\n| ' .. p._getBuildingCostText(building, biomeID))
			local providesText = p._getBuildingBenefitText(building, biomeID)
			if building.modifiers ~= nil then
				local modText = Modifiers.getModifiersText(building.modifiers)
				if providesText == nil then
					providesText = modText
				else
					providesText = providesText .. '<br/>' .. modText
				end
			end
			table.insert(resultPart, '\n| ' .. (providesText or ''))
		end
	end
	table.insert(resultPart, '\n|}')

	return table.concat(resultPart)
end

-- Builds the table of trader items
function p.getTraderTable(frame)
	local resultPart = {}

	-- Build table header
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\n|- class="headerRow-0"')
	table.insert(resultPart, '\n!colspan="2"| Item\n!Description\n!style="min-width:60px"| Cost\n!Requirements')

	for i, tsResource in ipairs(Township.itemConversions.fromTownship) do
		local res = GameData.getEntityByID(Township.resources, tsResource.resourceID)
		for j, tradeDef in ipairs(tsResource.items) do
			local item = Items.getItemByID(tradeDef.itemID)
			local itemDesc = item.customDescription
			if itemDesc == nil then
				if item.modifiers ~= nil then
					itemDesc = Modifiers.getModifiersText(item.modifiers, false, true)
				else
					itemDesc = ''
				end
			end
			local resQty = math.max(item.sellsFor, 2)
			local costSort = i * 10000 + resQty

			table.insert(resultPart, '\n|-\n| ' .. Icons.Icon({item.name, type='item', notext=true}))
			table.insert(resultPart, '\n| ' .. Icons.getExpansionIcon(item.id) .. Icons.Icon({item.name, type='item', noicon=true}))
			table.insert(resultPart, '\n| ' .. itemDesc)
			table.insert(resultPart, '\n|data-sort-value="' .. costSort ..'" style="text-align:right"| ' .. Icons.Icon({res.name, type='resource', qty=resQty, notext=true}))
			table.insert(resultPart, '\n| ' .. Shop.getRequirementString(tradeDef.unlockRequirements))
		end
	end
	table.insert(resultPart, '\n|}')

	return table.concat(resultPart)
end

-- Generates a table showing all the worship options
function p.getWorshipTable()
	local function getCheckpointCell(checkpoint)
		return '\n|-\n!' .. checkpoint .. '%<br/>' .. Num.formatnum(checkpoint * Township.maxWorship / 100) .. '/' .. Num.formatnum(Township.maxWorship)
	end

	local worships = GameData.getEntities(Township.worships, function(w) return not w.isHidden end)
	local ret = {}

	table.insert(ret, '{| class="wikitable stickyHeader"')
	table.insert(ret, '\n!' .. Icons.Icon({'Worship', type='township', nolink=true}))
	-- Names
	for _, worship in ipairs(worships) do
		table.insert(ret, '\n!' .. Icons.Icon({worship.name, type='monster', size=50}) .. Icons.Icon({'Statue of ' .. worship.name, type='building', size=50, notext=true}))
	end

	-- Requirements
	table.insert(ret, '\n|-\n!Requirements')
	for _, worship in ipairs(worships) do
		local cellStyle = (Shared.tableIsEmpty(worship.unlockRequirements) and 'class="table-na"') or 'style="text-align:center"'
		table.insert(ret, '\n|' .. cellStyle ..'| ' .. Shop.getRequirementString(worship.unlockRequirements))
	end

	-- Season multipliers
	table.insert(ret, '\n|-\n!Bonus Seasons')
	for _, worship in ipairs(worships) do
		local bonusPart = {}
		local cellStyle = 'style="text-align:center"'
		if Shared.tableIsEmpty(worship.seasonMultiplier) then
			bonusPart, cellStyle = {'None'}, 'class="table-na"'
		end
		for i, seasonMult in ipairs(worship.seasonMultiplier) do
			local season = GameData.getEntityByID(Township.seasons, seasonMult.seasonID)
			if season ~= nil then
				table.insert(bonusPart, Icons.Icon({season.name, type='township', nolink=true}) .. ' (' .. seasonMult.multiplier .. 'x)')
			end
		end
		table.insert(ret, '\n|' .. cellStyle .. '| ' .. table.concat(bonusPart, '<br/>'))
	end

	-- Base modifiers
	table.insert(ret, getCheckpointCell(0))
	for _, worship in ipairs(worships) do
		table.insert(ret, '\n| ' .. Modifiers.getModifiersText(worship.modifiers))
	end

	-- Checkpoint modifiers
	for i, checkpoint in ipairs(Township.worshipCheckpoints) do
		table.insert(ret, getCheckpointCell(checkpoint))
		for _, worship in ipairs(worships) do
			table.insert(ret, '\n| ' .. Modifiers.getModifiersText(worship.checkpoints[i]))
		end
	end

	-- Total sum
	-- TODO Needs fixing, no function currently for aggregating modifiers
	--[==[ 
	table.insert(ret, '\n|-\n!Total')
	for _, worship in ipairs(worships) do
		local modifiers = Shared.clone(worship.modifiers)
		for _, checkpoint in ipairs(worship.checkpoints) do
			for modifier, magnitude in pairs(checkpoint) do
				local swappedModifier = string.sub(modifier, 1, string.len('increased')) == 'increased' and string.gsub(modifier, 'increased', 'decreased') or string.gsub(modifier, 'decreased', 'increased')
				-- The modifier already exists, so we add the two modifiers together
				if modifiers[modifier] ~= nil then
					modifiers[modifier] = modifiers[modifier] + magnitude
				-- The inverse modifier already exists, so we subtract the negative value of the new modifier
				elseif modifiers[swappedModifier] ~= nil then
					modifiers[swappedModifier] = modifiers[swappedModifier] - magnitude
				-- The modifier does not exist, so create the modifier
				else
					modifiers[modifier] = magnitude
				end
			end
		end
		table.insert(ret, '\n|' .. Modifiers.getModifiersText(modifiers))
	end
	--]==]
	table.insert(ret, '\n|}')

	return table.concat(ret)
end

-- Gets a building and prepares all the relevant stats for the building, presented as an infobox
function p.getBuildingInfoBox(frame)
	local name = frame.args ~= nil and frame.args[1] or frame
	local building = p._getBuildingByName(name)
	if building == nil then
		return Shared.printError('No building named "' .. name .. '" exists in the data module')
	end

	local ret = {}
	-- Header
	table.insert(ret, '{| class="wikitable infobox"')
	-- Name
	table.insert(ret, '\n|-\n! ' .. Icons.getExpansionIcon(building.id) .. building.name)
	-- Icon
	table.insert(ret, '\n|-\n|style="text-align:center"| ' .. Icons.Icon({building.name, type='building', size='250', notext=true}))
	-- ID
	table.insert(ret, '\n|-\n| <b>Building ID:</b> ' .. building.id)
	-- Tier
	local tier = p._getTierText(building.tier, building.abyssalTier)
	table.insert(ret, '\n|-\n| <b>Requirements:</b><br/>' .. tier)

	-- Upgrades From
	table.insert(ret, '\n|-\n| <b>Base Cost:</b>')
	local upgradesFrom = p._getBuildingDowngrade(building)
	if upgradesFrom ~= nil then
		table.insert(ret, '<br/>' .. Icons.Icon({upgradesFrom.name, type='building'}))
	end
	-- Cost
	--table.insert(ret, '<br/>' .. p._getBuildingGroupedCostText(building))
	local function getGroupedText(building, groupFunc)
		local biomeGroups = groupFunc(building)
		if Shared.tableCount(biomeGroups) == 1 then
			-- If only one entry then simply output the cost
			return biomeGroups[1].cost
		else
			-- Otherwise, split by biome group
			local resultPart = {}
			table.insert(resultPart, '{| class="wikitable" style="text-align:center; margin: 0.25em 0 0 0"')
			for i, biomeGroup in ipairs(biomeGroups) do
				local biomeText = {}
				for j, biomeID in ipairs(biomeGroup.biomeIDs) do
					local biome = GameData.getEntityByID(Township.biomes, biomeID)
					table.insert(biomeText, Icons.Icon({biome.name, type='biome', notext=true, nolink=true, alt=biome.name}))
				end
				table.insert(resultPart, '\n|-\n| ' .. table.concat(biomeText, '<br/>'))
				table.insert(resultPart, '\n| ' .. biomeGroup.cost)
			end
			table.insert(resultPart, '\n|}')
			return table.concat(resultPart)
		end
	end

	table.insert(ret, '\n' .. getGroupedText(building, p._getBuildingGroupedCosts))

	-- Upgrades To
	local upgradesTo = p._getBuildingUpgrade(building)
	if upgradesTo ~= nil then
		table.insert(ret, '\n|-\n| <b>Upgrades To:</b>')
		table.insert(ret, '<br/>' .. Icons.Icon({upgradesTo.name, type='building'}))
		table.insert(ret, '\n' .. getGroupedText(upgradesTo, p._getBuildingGroupedCosts))
	end

	-- Maximum built
	local biomeCount = Shared.tableCount(building.biomes)
	local maxText = Num.formatnum(building.maxUpgrades)
	if biomeCount > 1 then
		maxText = maxText .. ' per biome, ' .. Num.formatnum(biomeCount * building.maxUpgrades) .. ' total'
	end
	table.insert(ret, '\n|-\n| <b>Maximum Built:</b><br/>' .. maxText)
	
	-- Benefits
	local benefits = p._getBuildingGroupedBenefitText(building)
	if benefits ~= nil and benefits ~= '' then
		table.insert(ret, '\n|-\n| <b>Provides:</b><br/>' .. benefits)
	end

	-- Biomes
	table.insert(ret, '\n|-\n| <b>Biomes:</b>')
	for _, biomeid in ipairs(building.biomes) do
		local biome = GameData.getEntityByID(Township.biomes, biomeid)
		table.insert(ret, '<br/>' .. Icons.Icon({biome.name, type='biome', nolink=true}))
	end

	-- End
	table.insert(ret, '\n|}')
	return table.concat(ret)
end

-- Returns an upgrade table of a building
function p.getBuildingUpgradeTable(frame)
	local buildingname = frame.args ~= nil and frame.args[1] or frame
	local building = p._getBuildingByName(buildingname)
	if building == nil then
		return Shared.printError('No building named "' .. buildingname .. '" exists in the data module')
	end

	-- Let's find the base building
	local baseBuilding = building
	while true do
		local previousBuilding = p._getBuildingDowngrade(baseBuilding)
		if previousBuilding ~= nil then
			baseBuilding = previousBuilding
		else
			break
		end
	end

	-- Let's make a list of all the buildings
	-- Return empty string if there is only 1 building in the upgrade chain (i.e. no upgrades/downgrades)
	local buildingList = {}
	local _curBuilding = baseBuilding
	while true do
		table.insert(buildingList, _curBuilding)
		_curBuilding = p._getBuildingUpgrade(_curBuilding)
		if _curBuilding == nil then
			break
		end
	end
	if #buildingList == 1 then
		return ''
	end

	local ret = {}
	table.insert(ret, '\n== Upgrade Chart ==')
	table.insert(ret, '\n{| class="wikitable" style="text-align:center"')

	-- Name
	table.insert(ret, '\n|-\n!colspan="2"| Name')
	for _, building in ipairs(buildingList) do
		table.insert(ret, '\n!' .. Icons.getExpansionIcon(building.id) .. Icons.Icon({building.name, type='building'}))
	end

	-- Tier
	table.insert(ret, '\n|-\n!colspan="2"| Requirements')
	for _, building in ipairs(buildingList) do
		table.insert(ret, '\n|' .. p._getTierText(building.tier, building.abyssalTier))
	end

	-- Cost
	local biomeCount = Shared.tableCount(baseBuilding.biomes)
	table.insert(ret, '\n|-\n!rowspan="' .. biomeCount .. '"| Cost')
	local firstBiome = true
	for _, biomeID in ipairs(baseBuilding.biomes) do
		local biome = GameData.getEntityByID(Township.biomes, biomeID)
		table.insert(ret, (firstBiome and '' or '\n|-') .. '\n! ' .. Icons.Icon({biome.name, type='biome', nolink=true}))
		for _, building in ipairs(buildingList) do
			local cost = p._getBuildingCostText(building, biomeID)
			table.insert(ret, '\n| ' .. cost)
		end
		firstBiome = false
	end

	-- Benefits
	local benefitText = {}
	table.insert(benefitText, '\n|-\n!rowspan="' .. biomeCount .. '"| Benefits')
	firstBiome = true
	local hasText = false
	for _, biomeID in ipairs(baseBuilding.biomes) do
		local biome = GameData.getEntityByID(Township.biomes, biomeID)
		table.insert(benefitText, (firstBiome and '' or '\n|-') .. '\n! ' .. Icons.Icon({biome.name, type='biome', nolink=true}))
		for _, building in ipairs(buildingList) do
			local benefit = p._getBuildingBenefitText(building, biomeID, true) or ''
			if not hasText and benefit ~= '' then
				hasText = true
			end
			table.insert(benefitText, '\n| ' .. benefit)
		end
		firstBiome = false
	end
	if hasText then
		-- Only add benefits rows if the building has benefits to display
		table.insert(ret, table.concat(benefitText))
	end

	-- End
	table.insert(ret, '\n|}')

	return table.concat(ret)
end

-- Returns a row containing a task given a title and a task table
function p._getTaskRow(title, task, isDailyTask)
	local ret = {}

	-- If has description, we will need to rowspan the title by 2, and insert a description with colspan 2
	local hasDescription = false
	if task.description ~= nil then
		hasDescription = true
	end
	local titlespan = hasDescription == true and 'rowspan="2"|' or ''

	-- Title
	table.insert(ret, '\n|-')
	table.insert(ret, '\n!' .. titlespan .. title)
	-- Description
	if hasDescription then
		table.insert(ret, '\n|colspan="2"|' .. task.description)
		table.insert(ret, '\n|-')
	end
	-- Requirements
	table.insert(ret, '\n|')
	-- Determines order of requirements output
	local reqOrder = {
		["items"] = 10,
		["monsters"] = 20,
		["monsterWithItems"] = 30,
		["skillXP"] = 40,
		["buildings"] = 50,
		["numPOIs"] = 60,
		["numRefinements"] = 70
	}
	local reqTextPart = {}

	local function getItemText(itemID)
		local item = Items.getItemByID(itemID)
		if item == nil then
			return Shared.printError('Unknown item: ' .. (itemID or 'nil'))
		else
			return Icons.Icon({item.name, type='item'})
		end
	end
	local function getMonsterText(monsterID)
		local monster = Monsters.getMonsterByID(monsterID)
		if monster == nil then
			return Shared.printError('Unknown monster: ' .. (monsterID or 'nil'))
		else
			return Icons.Icon({Monsters.getMonsterName(monster), type='monster'})
		end
	end

	for goalType, goalData in pairs(task.goals) do
		local typeOrder = reqOrder[goalType] or 0
		local goalText = nil
		if type(goalData) == 'table' then
			-- Goal data is a table
			for goalIdx, goalObj in ipairs(goalData) do
				if goalType == 'items' then
					goalText = Num.formatnum(goalObj.quantity) .. ' ' .. getItemText(goalObj.id)
				elseif goalType == 'monsters' then
					goalText = Num.formatnum(goalObj.quantity) .. ' ' .. getMonsterText(goalObj.id)
				elseif goalType == 'monsterWithItems' then
					local itemsText = {}
					for i, itemID in ipairs(goalObj.itemIDs) do
						table.insert(itemsText, getItemText(itemID))
					end
					goalText = Num.formatnum(goalObj.quantity) .. ' ' .. getMonsterText(goalObj.monsterID) .. ' with ' .. table.concat(itemsText, ', ') .. ' equipped'
				elseif goalType == 'skillXP' then
					local skillName = GameData.getSkillData(goalObj.id).name
					goalText = Num.formatnum(goalObj.quantity) .. ' ' .. Icons.Icon({skillName, type='skill'}) .. ' XP'
				elseif goalType == 'buildings' then
					local buildingName = p._GetBuildingByID(goalObj.id).name
					goalText = Num.formatnum(goalObj.quantity) .. ' ' .. Icons.Icon({buildingName, type='building'})
				elseif goalType == 'numPOIs' then
					local mapName = GameData.getEntityByID(GameData.skillData.Cartography.worldMaps, goalObj.worldMapID).name
					goalText = 'Discover ' .. Num.formatnum(goalObj.quantity) .. ' Points of Interest in ' .. Icons.Icon({'Cartography', type='skill'}) .. ' world map of ' .. mapName
				else
					goalText = Shared.printError('Unknown goal type: ' .. (goalType or 'nil'))
				end
				table.insert(reqTextPart, {
					["goalOrder"] = typeOrder,
					["subOrder"] = goalIdx,
					["text"] = goalText
				})
			end
		else
			-- Goal data is another value of some type
			if goalType == 'numRefinements' then
				goalText = 'Refine dig site maps in ' .. Icons.Icon({'Cartography', type='skill'}) .. ' ' .. Num.formatnum(goalData) .. ' times'
			else
				goalText = Shared.printError('Unknown goal type: ' .. (goalType or 'nil'))
			end
			table.insert(reqTextPart, {
				["goalOrder"] = typeOrder,
				["subOrder"] = 0,
				["text"] = goalText
			})
		end
	end

	table.sort(reqTextPart,
		function(a, b)
			if a.goalOrder == b.goalOrder then
				return a.subOrder < b.subOrder
			else
				return a.goalOrder < b.goalOrder
			end
		end
	)

	local requirements = {}
	for i, req in ipairs(reqTextPart) do
		table.insert(requirements, req.text)
	end
	-- We don't check tasks.requirements (so far it's only used to enumerate the Tutorial tasks so you only see 1 at a time)
	table.insert(ret, table.concat(requirements, '<br/>'))
	-- Rewards
	table.insert(ret, '\n|')
	local rewards = {}
	local rewardsVariableQty = {}
	if task.rewards.currencies ~= nil then
		for _, currReward in ipairs(task.rewards.currencies) do
			if isDailyTask and currReward.id ~= 'melvorD:GP' then
				table.insert(rewardsVariableQty, Icons._Currency(currReward.id))
			elseif not isDailyTask then
				table.insert(rewards, Icons._Currency(currReward.id, currReward.quantity))
			end
		end
	end
	for _, item in ipairs(task.rewards.items) do
		local itemname = GameData.getEntityByID('items', item.id).name
		table.insert(rewards, Num.formatnum(item.quantity)..' '..Icons.Icon({itemname, type='item'}))
	end
	for _, skill in ipairs(task.rewards.skillXP) do
		if not (isDailyTask and skill.id == 'melvorD:Township') then
			local skillname = GameData.getSkillData(skill.id).name
			table.insert(rewards, Num.formatnum(skill.quantity)..' '..Icons.Icon({skillname, type='skill'})..' XP')
		end
	end
	for _, townshipResource in ipairs(task.rewards.townshipResources) do
		local resourcename = p._getResourceByID(townshipResource.id).name
		table.insert(rewards, Num.formatnum(townshipResource.quantity)..' '..Icons.Icon({resourcename, type='resource'}))
	end
	if not Shared.tableIsEmpty(rewardsVariableQty) then
		table.insert(ret, '[[Township#Casual Tasks|Variable]] ' .. table.concat(rewardsVariableQty, ', ') .. '<br/>')
	end
	table.insert(ret, table.concat(rewards, '<br/>'))

	-- Unlock requirements, daily task specific
	if isDailyTask then
		table.insert(ret, '\n|' .. Shop.getRequirementString(task.requirements))
	end
	return table.concat(ret)
end

-- Returns all the tasks of a given category
-- TODO: Support casual tasks
function p.getTaskTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	local categoryData = GameData.getEntityByName(Township.taskCategories, category)
	local taskData, categoryName, isDailyTask = nil, nil, false
	if category == 'Daily' then
		isDailyTask = true
		taskData = Township.casualTasks
		categoryName = 'Casual'
	elseif categoryData ~= nil then
		taskData = Township.tasks
		categoryName = categoryData.name
	else
		return Shared.printError('Invalid task category specified: ' .. (tostring(category) or 'nil'))
	end

	local taskcount = 0
	local ret = {}
	table.insert(ret, '{| class="wikitable lighttable stickyHeader mw-collapsible" style="text-align:left"')
	table.insert(ret, '\n|- class="headerRow-0"')
	table.insert(ret, '\n!Task')
	table.insert(ret, '\n!Requirements')
	table.insert(ret, '\n!Rewards')
	if isDailyTask then
		table.insert(ret, '<br/>(In addition to [[Township#Casual Tasks|Variable]] ' .. Icons._Currency('melvorD:GP') .. ' & ' .. Icons.Icon({'Township', type='skill', notext=true}) .. ' XP)')
	end
	if isDailyTask then
		table.insert(ret, '\n!Unlock Requirements')
	end
	
	for _, task in ipairs(taskData) do
		-- Filter out other categories
		local categoryID, categoryNS, categoryLocalID = '', '', ''
		if categoryData ~= nil then
			categoryID = categoryData.id
			categoryNS, categoryLocalID = Shared.getLocalID(categoryID)
		end
		if isDailyTask or task.category == categoryID or task.category == categoryLocalID then
			taskcount = taskcount + 1
			local title = categoryName .. ' ' .. taskcount
			table.insert(ret, p._getTaskRow(title, task, isDailyTask))
		end
	end
	table.insert(ret, '\n|}')
	return table.concat(ret)
end

-- Returns a table containing all the tasks that reference an item or monster
-- e.g. p.getTaskReferenceTable({'Chicken Coop', 'dungeon'})
-- name = item or monster name
-- type = 'item' or 'monster' or 'dungeon'
function p.getTaskReferenceTable(frame)
	-- Returns a set containing all the desired IDs
	local function GetReferenceIDs(referenceName, referenceType)
		local IDs = {}
		if referenceType == 'dungeon' then
			-- We get the tasks associated with all monsters in the dungeon
			local area = nil
			local areaTypes = {'dungeons', 'abyssDepths', 'strongholds'}
			for _, areaType in ipairs(areaTypes) do
				area = GameData.getEntityByName(areaType, referenceName)
				if area ~= nil then
					break
				end
			end
			local monsters = area.monsterIDs
			for _, monster in ipairs(monsters) do
				IDs[monster] = true
			end
		end
		if referenceType == 'item' then
			IDs[GameData.getEntityByName('items', referenceName).id] = true
		end
		if referenceType == 'monster' then
			IDs[Monsters.getMonster(referenceName).id] = true
		end
		return IDs
	end
	-- For a task, returns where to search for the desired IDs, given the type
	local function GetGetSearchTables(referenceType)
		local function searchItems(task)
			return {task.goals.items, task.rewards.items}
		end
		local function searchMonsters(task)
			return {task.goals.monsters}
		end
		-- item -> searchItems; monster or dungeon -> searchMonsters
		return referenceType == 'item' and searchItems or searchMonsters
	end

	local args = frame.args ~= nil and frame.args or frame
	local referenceName = Shared.fixPagename(args[1])
	local referenceType = args[2]
	local referenceIDs = GetReferenceIDs(referenceName, referenceType)
	-- GetSearchTables = function searchItems/Monsters(task)
	local GetSearchTables = GetGetSearchTables(referenceType)

	local function checkTask(task)
		local function checkID(entry)
			return referenceIDs[entry.id] ~= nil
		end
		for _, searchTable in pairs(GetSearchTables(task)) do -- ipairs won't work if first table is nil
			-- Check to see if the table contains any of the IDs in referenceIDs
			if searchTable[1] ~= nil then -- Make sure table is not empty
				if #GameData.getEntities(searchTable, checkID) ~= 0 then -- Make sure we have at least 1 match
					return true
				end
			end
		end
		return false
	end
	-- Find all tasks that contain the desired ids
	local tasks = GameData.getEntities(Township.tasks, checkTask)
	if #tasks == 0 then
		return ''
	end

	-- Build the table
	local ret = {}
	table.insert(ret, '==Tasks==')
	table.insert(ret, '\n{| class="wikitable" style="text-align:left"')
	table.insert(ret, '\n!Task')
	table.insert(ret, '\n!Requirements')
	table.insert(ret, '\n!Rewards')
	for _, task in ipairs(tasks) do
		-- Some categories have a local ID, resolve this before looking up the task category
		local taskNS, taskLocalID = Shared.getLocalID(task.id)
		local catID = Shared.getNamespacedID(taskNS,  task.category)
		local categoryname = GameData.getEntityByID(Township.taskCategories, catID).name
		local title = '[[Township/Tasks#'..categoryname..'|'..categoryname..']]'
		table.insert(ret, p._getTaskRow(title, task, false))
	end
	table.insert(ret, '\n|}')
	return table.concat(ret)
end

return p