Module:Township: Difference between revisions

From Melvor Idle
No edit summary
(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)
 
(45 intermediate revisions by 5 users not shown)
Line 1: Line 1:
local Shared = require('Module:Shared')
local Shared = require('Module:Shared')
local Icons = require('Module:Icons')
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 GameData = require('Module:GameData')
local Constants = require('Module:Constants')
local Modifiers = require('Module:Modifiers')
 
local Num = require('Module:Number')


local p = {}
local p = {}
Line 10: Line 13:
p.Township = 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


-- Returns the recipe for the item of a desired skill.
-- Gets a Township building by name, e.g. Hunters Cabin
-- Unfortunately Module:Items/SourceTables.getItemSources does not provide parseable data so we instead use this quick function
function p._getBuildingByName(name)
function p._FindItemRecipes(itemid, skill)
-- Check for the special statue case
if name == 'Statues' then
-- No skill? No recipes
name = 'Statue of Worship'
if skill == nil then
end
return {}
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
end
return nil
-- the key name for each skill in the json file
end
local skill_recipe_keys = {
['melvorD:Woodcutting'] = {recipes='trees', productID='productId'}, -- lowercase "d"
['melvorD:Fishing'] = {recipes='fish', productID='productId'}, -- lowercase "d"
['melvorD:Cooking'] = {recipes='recipes', productID='productID'},
['melvorD:Mining'] = {recipes='rockData', productID='productId'}, -- lowercase "d"
['melvorD:Smithing'] = {recipes='recipes', productID='productID'},
['melvorD:Farming'] = {recipes='recipes', productID='productId'}, -- lowercase "d"
['melvorD:Summoning'] = {recipes='recipes', productID='productID'},
['melvorD:Fletching'] = {recipes='recipes', productID='productID'},
['melvorD:Crafting'] = {recipes='recipes', productID='productID'},
['melvorD:Runecrafting'] = {recipes='recipes', productID='productID'},
['melvorD:Herblore'] = {recipes='recipes', productID='potionIDs'} -- Special case potions I-IV
--[[ Excluded skills:
Attack, Strength, Defence, Magic, Ranged, Prayer, Slayer
Thieving, Agility, Astrology, Firemaking, Township (not items)]]
}


local results = {}
-- 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
local SkillData = GameData.getSkillData(skill)
-- cannot be built within that biome.
local recipes = skill_recipe_keys[skill].recipes
function p._getBuildingCostText(building, biomeID, delimiter)
local productID = skill_recipe_keys[skill].productID
-- Basic validation of inputs
if type(building) == 'table' and building.cost ~= nil and biomeID ~= nil then
if SkillData[recipes] ~= nil then
local delim = delimiter
for _, recipe in ipairs(SkillData[recipes]) do
if delim == nil then
-- Special case for Herblore
delim = ', '
if skill == 'melvorD:Herblore' then
end
-- Iterate over the 4 potion tiers
for i, costDef in ipairs(building.cost) do
for _, potionid in ipairs(recipe[productID]) do
if costDef.biomeID == biomeID then
if itemid == potionid then
local resultPart = {}
table.insert(results, Shared.clone(recipe))
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
end
end
-- Base case
return table.concat(resultPart, delim)
else
if itemid == recipe[productID] then
table.insert(results, Shared.clone(recipe))
end
end
end
end
end
end
end
return results
end
end


-- Returns a list of all the Township resources
-- Given a building, groups biomes for which that building has a common cost
function p._ResourcesData()
function p._getBuildingGroupedCosts(building)
local biomeGroups = {}
-- Get a sorted list of all the resources
for i, biomeID in ipairs(building.biomes) do
local resources = GameData.sortByOrderTable(Township.resources, Township.resourceDisplayOrder)
local currentBiomeCost = p._getBuildingCostText(building, biomeID)
resources = Shared.clone(resources)
local found = false
for j, biomeGroup in ipairs(biomeGroups) do
return resources
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
end


-- Returns a sorted list of all Township buildings
-- Given a building, returns a text string repesenting the building costs for all biomes
function p._SortedBuildings()
function p._getBuildingGroupedCostText(building)
return GameData.sortByOrderTable(Township.buildings, Township.buildingDisplayOrder)
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
end


-- Returns a list of all the Township resources along with the Trader's trade ratios
-- Given a building and biome ID, returns a string displaying the building's benefits,
function p._TraderData()
-- or nil if no benefits
-- Get the list of resources. We get a copy instead of directly using p.resources because we are going to modify the table
function p._getBuildingBenefitText(building, biomeID, includeModifiers, delimiter)
local resources = p._ResourcesData()
-- Basic validation of inputs
if type(building) == 'table' and building.provides ~= nil and biomeID ~= nil then
-- Get the list of tradeable items
local delim = delimiter
-- See township.js -> TownshipResource.buildResourceItemConversions for the calculation of valid items
if delim == nil then
local function matchNone(item)
delim = ', '
return false
end
end
local includeMods = includeModifiers
local function matchFood(item)
if includeMods == nil then
return item.type == 'Food' and (not string.match(item.id, '_Perfect')) and item.category ~= 'Farming' and (not item.ignoreCompletion)
includeMods = false
end
end
local function matchLogs(item)
return item.type == 'Logs'
end
local function matchOre(item)
return item.type == 'Ore' and item.id ~= 'melvorTotH:Meteorite_Ore'
end
local function matchCoal(item)
return item.id == 'melvorD:Coal_Ore'
end
local function matchBar(item)
return item.type == 'Bar' and item.id ~= 'melvorTotH:Meteorite_Bar'
end
local function matchHerb(item)
return item.type == 'Herb'
end
local function matchEssence(item)
return item.id == 'melvorD:Rune_Essence' or item.id == 'melvorTotH:Pure_Essence'
end
local function matchLeather(item)
return item.id == 'melvorD:Leather'
end
local function matchPotion(item)
return item.type == 'Potion' and string.match(item.id, '_IV')
end
local function matchClothing(item)
return item.id == 'melvorD:Green_Dragonhide' or item.id == 'melvorD:Blue_Dragonhide' or item.id == 'melvorD:Red_Dragonhide' or item.id == 'melvorD:Black_Dragonhide' or item.id == 'melvorF:Elder_Dragonhide'
end


local traderMatchesList = {
local providesData = nil
['melvorF:GP'] = {traderMatches = matchNone},
for i, provides in ipairs(building.provides) do
['melvorF:Food'] = {traderMatches = matchFood},
if provides.biomeID == biomeID then
['melvorF:Wood'] = {traderMatches = matchLogs},
providesData = provides
['melvorF:Stone'] = {traderMatches = matchOre},
break
['melvorF:Ore'] = {traderMatches = matchOre},
end
['melvorF:Coal'] = {traderMatches = matchCoal},
end
['melvorF:Bar'] = {traderMatches = matchBar},
['melvorF:Herbs'] = {traderMatches = matchHerb},
['melvorF:Rune_Essence'] = {traderMatches = matchEssence},
['melvorF:Leather'] = {traderMatches = matchLeather},
['melvorF:Potions'] = {traderMatches = matchPotion},
['melvorF:Planks'] = {traderMatches = matchLogs},
['melvorF:Clothing'] = {traderMatches = matchClothing}
}


for _, resource in ipairs(resources) do
if providesData ~= nil then
resource.itemConversions = Shared.clone(GameData.getEntities('items', traderMatchesList[resource.id].traderMatches))
local resultPart = {}
end
local stats = {
population = 'Population',
-- Calculate the trader's conversion ratios
happiness = 'Happiness',
-- See township.js TownshipResource.getBaseConvertToTownshipRatio and TownshipResource.getBaseConvertFromTownshipRatio for the conversion prices
education = 'Education',
for _, resource in ipairs(resources) do
storage = 'Storage',
if resource.id == 'melvorF:Food' then
soulStorage = 'Soul Storage',
for _, item in ipairs(resource.itemConversions) do
worship = 'Worship',
item.toTownship = math.max(math.floor(1000/(item.healsFor*10)), 2)
fortification = 'Fortification'
item.fromTownship = item.healsFor*5*6*5
}
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
end
elseif resource.id == 'melvorF:Planks' then
 
for _, item in ipairs(resource.itemConversions) do
-- Resources
item.toTownship = math.max(math.floor(3000/math.max(item.sellsFor, 1)), 2)
if providesData.resources ~= nil then
item.fromTownship = math.max(math.ceil(item.sellsFor/2)*6, 1);
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
end
elseif resource.id == 'melvorF:Rune_Essence' then
 
for _, item in ipairs(resource.itemConversions) do
-- Other stats
item.toTownship = 5
for key, stat in pairs(stats) do
item.fromTownship = (item.sellsFor+1)*10*6
local quantity = providesData[key]
if quantity ~= nil and quantity ~= 0 then
table.insert(resultPart, resourceText(stat, 'township', quantity))
end
end
end
elseif resource.id == 'melvorF:Leather' then
 
for _, item in ipairs(resource.itemConversions) do
-- Modifiers
item.toTownship = 20
if includeMods and building.modifiers ~= nil then
item.fromTownship = 20*6
table.insert(resultPart, Modifiers.getModifiersText(building.modifiers))
end
end
else
 
for _, item in ipairs(resource.itemConversions) do
if not Shared.tableIsEmpty(resultPart) then
        item.toTownship = math.max(math.floor(1000/math.max(item.sellsFor, 1)), 2)
return table.concat(resultPart, delim)
    item.fromTownship = math.max(item.sellsFor*6, 1)
end
end
end
end
end
end
return resources
end
end
p.resources = p._TraderData()


-- Builds the table of trader items
-- Given a building, groups biomes for which that building has a common benefit/provides
function p.GetTraderTable(frame)
function p._getBuildingGroupedBenefits(building, includeModifiers)
-- Get the resources data with associated trader data
if includeModifiers == nil then
includeModifiers = true
-- Build the text
end
local ret = {}
local biomeGroups = {}
for _, resource in ipairs(p.resources) do
for i, biomeID in ipairs(building.biomes) do
if #resource.itemConversions ~= 0 then -- Skips GP
local currentBiomeBenefit = p._getBuildingBenefitText(building, biomeID, includeModifiers)
local ret_resource = {}
local found = false
for j, biomeGroup in ipairs(biomeGroups) do
-- Header
if biomeGroup.benefit == currentBiomeBenefit then
table.insert(ret_resource, '\r\n==='..resource.name..'===')
-- Another biome exists with this cost
table.insert(ret_resource, '\r\n{| class="wikitable sortable stickyHeader"')
table.insert(biomeGroup.biomeIDs, biomeID)
table.insert(ret_resource, '\r\n|- class="headerRow-0"')
found = true
table.insert(ret_resource, '\r\n!Item')
break
table.insert(ret_resource, '\r\n!Name')
table.insert(ret_resource, '\r\n!Level')
table.insert(ret_resource, '\r\n!Give To')
table.insert(ret_resource, '\r\n!Take From')
table.insert(ret_resource, '\r\n!Value')
table.insert(ret_resource, '\r\n!Value/Resource')
if resource.id =='melvorF:Food' then
table.insert(ret_resource, '\r\n!Heals')
table.insert(ret_resource, '\r\n!Heals/Resource')
end
end
end
-- Each item
if not found then
for _, item in ipairs(resource.itemConversions) do
table.insert(biomeGroups, { biomeIDs = { biomeID }, cost = currentBiomeBenefit})
-- To indicate the skill level, we need to find the recipe of the item in the target skill
end
-- Unfortunately Module:Items/SourceTables.getItemSources does not provide parseable data
end
local required_level = nil
return biomeGroups
local recipes = nil
end
 
-- Get the skill based on the item.id or else use the resource's default skill
-- Given a building, returns a text string repesenting the building benefits for all biomes
local skill_overrides = {
function p._getBuildingGroupedBenefitText(building, includeModifiers)
['melvorD:Raw_Magic_Fish'] = 'melvorD:Fishing',
if includeModifiers == nil then
['melvorF:Apple'] = 'melvorD:Farming',
includeModifiers = true
}
end
local skill = skill_overrides[item.id] or p._GetResourceSkill(resource.id)
local resultPart = {}
local skill_namespace, skill_localid = GameData.getLocalID(skill or '')
local biomeGroups = p._getBuildingGroupedBenefits(building, includeModifiers)
if Shared.tableCount(biomeGroups) == 1 then
-- Check for upgraded Crafting items and downgrade them so we can display the crafting level for the base item
-- If only one entry then simply output the cost
-- e.g. converts Black_Dhide_Body_U -> Black_Dhide_Body for the purposes of the lookup
table.insert(resultPart, biomeGroups[1].cost)
local lookup_id = item.id
else
if string.match(item.id, '_U$') then
-- Otherwise, split by biome group
lookup_id = string.sub(item.id, 1, #item.id - 2)
for i, biomeGroup in ipairs(biomeGroups) do
end
local biomeText = {}
for j, biomeID in ipairs(biomeGroup.biomeIDs) do
-- Find the recipe's level
local biome = GameData.getEntityByID(Township.biomes, biomeID)
local recipes = p._FindItemRecipes(lookup_id, skill)
table.insert(biomeText, Icons.Icon({biome.name, type='biome', notext=true, nolink=true, alt=biome.name}))
if #recipes == 1 then
required_level = recipes[1].level
end
-- Alright, now that we've found the required recipe and level, we can draw the item's row entry
table.insert(ret_resource, '\r\n|-')
-- Icon
table.insert(ret_resource, '\r\n|style="text-align:center"|'..Icons.Icon({item.name, type='item', size='50', notext=true}))
-- Name
table.insert(ret_resource, '\r\n|style="text-align:left"|'..Icons.getExpansionIcon(item.id)..Icons.Icon({item.name, type='item', noicon=true}))
-- Level
if required_level == nil then
-- Recipe not found, or multiple recipes found
table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="0"|N/A')
else
table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. required_level .. '"|'..Icons.Icon({skill_localid, type="skill", notext=true})..' '..required_level)
end
-- Give To
table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. item.toTownship .. '"|'..Icons.Icon({item.name, type='item', notext=true})..' '..Shared.formatnum(item.toTownship))
-- Take From
table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. item.fromTownship .. '"|'..Icons.Icon({resource.name, type='resource', notext=true})..' '..Shared.formatnum(item.fromTownship))
-- Value
table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. item.sellsFor .. '"|'..Icons.GP(item.sellsFor))
-- Value/Resource
table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. item.sellsFor/item.fromTownship .. '"|'..Icons.GP(Shared.round(item.sellsFor/item.fromTownship, 2, 2)))
if resource.id =='melvorF:Food' then
-- Heals
table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. item.healsFor*10 .. '"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..Shared.formatnum(item.healsFor*10))
-- Heals/Resource
table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. item.healsFor*10/item.fromTownship .. '"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..Shared.round(item.healsFor*10/item.fromTownship, 2, 2))
end
end
end
table.insert(resultPart, table.concat(biomeText, ', ') .. ': ' .. biomeGroup.cost)
table.insert(ret_resource, '\r\n|}')
table.insert(ret, table.concat(ret_resource))
end
end
end
end
return table.concat(ret)
return table.concat(resultPart, '<br/>')
end
end


-- Gets the associated skill of a resource by id
-- Returns a sorted list of all Township buildings
local resource_skill = {
function p._sortedBuildings(keepUnsorted)
['melvorF:GP'] = {skill = nil},
local ku = true
['melvorF:Food'] = {skill = 'melvorD:Cooking'},
if keepUnsorted ~= nil then
['melvorF:Wood'] = {skill = 'melvorD:Woodcutting'},
ku = keepUnsorted
['melvorF:Stone'] = {skill = 'melvorD:Mining'},
end
['melvorF:Ore'] = {skill = 'melvorD:Mining'},
return GameData.sortByOrderTable(Township.buildings, Township.buildingDisplayOrder, ku)
['melvorF:Coal'] = {skill = 'melvorD:Mining'},
['melvorF:Bar'] = {skill = 'melvorD:Smithing'},
['melvorF:Herbs'] = {skill = 'melvorD:Farming'},
['melvorF:Rune_Essence'] = {skill = 'melvorD:Mining'},
['melvorF:Leather'] = {skill = nil},
['melvorF:Potions'] = {skill = 'melvorD:Herblore'},
['melvorF:Planks'] = {skill = 'melvorD:Woodcutting'},
['melvorF:Clothing'] = {skill = nil}
}
function p._GetResourceSkill(id)
return resource_skill[id].skill
end
end


-- Gets a Township building by ID, e.g. melvorF:Hunters_Cabin
-- Gets the Township level or abyssalLevel, population and fortification requirements for a tier
function p._GetBuildingByID(id)
-- Returns {population=X, level=X} for non-abyssal tiers
-- Check for the special statue case
-- Returns {population=X, abyssalLevel=X, fortification=X} for abyssal tiers
if id == 'melvorF:Statues' then
function p._getTierRequirements(tier, abyssalTier)
local building = Shared.clone(GameData.getEntityByID(Township.buildings, id))
local tierData = Township.populationForTier[tier]
building.name = 'Statue of Worship'
if abyssalTier ~= nil then
return building
local abyssalTierData = Shared.clone(Township.abyssalTierRequirements[abyssalTier + 1])
abyssalTierData.population = tierData.population
return abyssalTierData
else
else
return GameData.getEntityByID(Township.buildings, id)
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
end
end


-- Gets a Township building by name, e.g. Hunters Cabin
function p.getBuildings(checkFunc)
function p._GetBuildingByName(name)
local result = {}
-- Check for the special statue case
for i, building in pairs(p.Township.buildings) do
if name == 'STATUE_NAME' then
if checkFunc(building) then
name = 'Statue of Worship'
table.insert(result, building)
end
end
end
local STATUE_OF = 'Statue of '
return result
if string.sub(name, 1, string.len(STATUE_OF)) == STATUE_OF then
end
local building = Shared.clone(GameData.getEntityByName(Township.buildings, 'STATUE_NAME'))
 
building.name = name
function p.getSeasons(checkFunc)
return building
local result = {}
else
for i, season in pairs(p.Township.seasons) do
return GameData.getEntityByName(Township.buildings, name)
if checkFunc(season) then
table.insert(result, season)
end
end
end
return result
end
end


-- Gets the Township level and population requirements for a tier
function p.getWorships(checkFunc)
-- Returns {population=X, level=X}
local result = {}
function p._GetTierRequirements(tier)
for i, worship in pairs(p.Township.worships) do
return Township.populationForTier[tier]
if checkFunc(worship) then
table.insert(result, worship)
end
end
return result
end
end


-- Returns a string containing the Township level and population requirements for a tier
-- Generates a table listing all biomes and their associated requirements
function p._GetTierText(tierlevel)
function p.getBiomeTable(frame)
local tier = p._GetTierRequirements(tierlevel)
local resultPart = {}
return Icons._SkillReq('Township', tier.level, false)..'<br>'..Icons.Icon({'Population', type='township', notext=true})..'&nbsp;'..tier.population
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
end


-- Gets a building and prepares all the relevant stats for the building
-- Generates a table showing which buildings can be built in which biomes
function p.GetBuildingTable(frame)
-- Skips upgraded buildings
local name = frame.args ~= nil and frame.args[1] or frame
function p.getBuildingBiomeTable(frame)
local building = Shared.clone(p._GetBuildingByName(name))
local tbl = mw.html.create('table')
local ret = {}
:addClass('wikitable sortable stickyHeader')
:css('text-align', 'center')


-- Header
local header = mw.html.create('tr'):addClass('headerRow-0')
table.insert(ret, '\r\n{| class="wikitable infobox"')
local level = mw.html.create('tr'):addClass('sorttop')
-- Name
local pop = mw.html.create('tr'):addClass('sorttop')
table.insert(ret, '\r\n|-\r\n!'..building.name)
local fort = mw.html.create('tr'):addClass('sorttop')
-- Icon
table.insert(ret, '\r\n|-\r\n|style="text-align:center"|'..Icons.Icon({building.name, type='building', size='250', notext=true}))
-- ID
table.insert(ret, '\r\n|-\r\n| <b>Building ID:</b> '..building.id)
-- Type
table.insert(ret, '\r\n|-\r\n| <b>Type:</b> '..building.type)
-- Tier
local tier = p._GetTierText(building.tier)
table.insert(ret, '\r\n|-\r\n| <b>Requirements:</b><br>'..tier)


-- Upgrades From
header:tag('th')
table.insert(ret, '\r\n|-\r\n| <b>Base Cost:</b>')
:css('z-index', '2')
local upgradesFrom = p._GetBuildingDowngrade(building)
:wikitext('Building')
if upgradesFrom ~= nil then
level:tag('th')
table.insert(ret, '<br>'..Icons.Icon({upgradesFrom.name, type='building'}))
: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
end
-- Cost
local cost = p._GetBuildingBaseCost(building)
table.insert(ret, '<br>'..cost)
-- Upgrades To
tbl:node(header)
local upgradesTo = p._GetBuildingIDUpgrade(building.id)
tbl:node(level)
if upgradesTo ~= nil then
tbl:node(pop)
table.insert(ret, '\r\n|-\r\n| <b>Upgrades To:</b>')
tbl:node(fort)
table.insert(ret, '<br>'..Icons.Icon({upgradesTo.name, type='building'}))
 
local upgrade_cost = p._GetBuildingBaseCost(upgradesTo)
for _, _building in ipairs(p._sortedBuildings(false)) do
table.insert(ret, '<br>'..upgrade_cost)
-- 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
end


-- Fixed benefits
return tostring(tbl)
local benefits = p._GetBuildingBenefits(building)
end
if benefits ~= nil then
 
table.insert(ret, '\r\n|-\r\n| <b>Provides:</b> '..benefits)
-- 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
end


-- Production
-- Generate table header
local production = p._GetBuildingBaseProduction(building)
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
if production ~= nil then
table.insert(resultPart, '\n|- class="headerRow-0"')
table.insert(ret, '\r\n|-\r\n| <b>Base Production per '..Icons.Icon({'Workers', type='township', notext=true})..':</b><br>')
table.insert(resultPart, '\n!colspan="2"|Building\n!Requirements\n!Max Built')
table.insert(ret, production)
table.insert(resultPart, '\n!Biomes\n!Cost\n!Provides')
end
 
local buildings = p._sortedBuildings(false)


-- Modifiers
for i, building in ipairs(buildings) do
if building.modifiers ~= nil and not Shared.tableIsEmpty(building.modifiers) then
-- Number of rows per building is dictated by number of biomes
table.insert(ret, '\r\n|-\r\n| <b>Modifiers:</b>\r\n'..Constants.getModifiersText(building.modifiers, true))
local buildingName = (building.id == 'melvorF:Statues' and 'Statue of Worship') or building.name
end
local firstRow = true
local rowCount = Shared.tableCount(building.biomes)
-- Biomes
local rowSpan = (rowCount > 1 and ' rowspan="' .. rowCount .. '"') or ''
table.insert(ret, '\r\n|-\r\n| <b>Biomes:</b>')
local rowSpanOnly = (rowCount > 1 and '|' .. rowSpan) or ''
for _, biomeid in ipairs(building.biomes) do
for j, biomeID in ipairs(building.biomes) do
local biomename = GameData.getEntityByID(Township.biomes, biomeid).name
local biome = biomesByID[biomeID]
-- Optional hidden bonus/penalty for building
if firstRow then
local modifier = nil
table.insert(resultPart, '\n|-')
if #building.biomeModifiers > 0 then
table.insert(resultPart, '\n|class="table-img"' .. rowSpan .. '| ' .. Icons.Icon({buildingName, type='building', notext=true}))
modifier = GameData.getEntityByProperty(building.biomeModifiers, 'biomeID', biomeid)
table.insert(resultPart, '\n' .. rowSpanOnly .. '| ' .. Icons.getExpansionIcon(building.id) .. Icons.Icon({buildingName, type='building', noicon=true}))
end
table.insert(resultPart, '\n|' .. 'data-sort-value="' .. building.tier .. '"' .. rowSpan .. '| ' .. (p._getTierText(building.tier, building.abyssalTier) or ''))
if modifier ~= nil then
table.insert(resultPart, '\n|style="text-align:right"' .. rowSpan .. '| ' .. building.maxUpgrades)
local color = modifier.value < 0 and 'red' or 'green'
firstRow = false
local modifier_value = Shared.numStrWithSign(modifier.value)
else
table.insert(ret, '<br>'..Icons.Icon({biomename, type='biome', notext=true, nolink=true})..' <span style="color:'..color..'"><b>'..biomename..' ('..modifier_value..'%)</b></span>')
table.insert(resultPart, '\n|-')
else
end
table.insert(ret, '<br>'..Icons.Icon({biomename, type='biome', notext=true, nolink=true})..' <span>'..biomename..'</span>')
-- 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
end
end
table.insert(resultPart, '\n|}')
-- End
 
table.insert(ret, '\r\n|}')
return table.concat(resultPart)
return table.concat(ret)
end
end


-- Given a resource id, return the job id
-- Builds the table of trader items
-- e.g. melvorF:Bar -> melvorF:Blacksmith
function p.getTraderTable(frame)
function p._GetJobFromResource(resource_id)
local resultPart = {}
local job = GameData.getEntityByProperty(Township.jobs, 'produces', resource_id)
 
return job.id
-- Build table header
end
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')


-- Gets a string displaying the base production of a building, or nil if no production
for i, tsResource in ipairs(Township.itemConversions.fromTownship) do
function p._GetBuildingBaseProduction(building)
local res = GameData.getEntityByID(Township.resources, tsResource.resourceID)
local production = Shared.clone(building.provides.resources)
for j, tradeDef in ipairs(tsResource.items) do
local item = Items.getItemByID(tradeDef.itemID)
if #production == 0 then
local itemDesc = item.customDescription
return nil
if itemDesc == nil then
end
if item.modifiers ~= nil then
itemDesc = Modifiers.getModifiersText(item.modifiers, false, true)
local retResources = {}
else
for _, resource in ipairs(production) do
itemDesc = ''
local retProduction = {}
end
local job = p._GetJobFromResource(resource.id)
local workers = GameData.getEntityByID(building.provides.workers, job).quantity
-- Sourced from township.js -> Township.computeTownResourceGain()
local production = resource.quantity*100*(Township.tickLength/10)
local color = production < 0 and 'red' or 'green'
local resource_data = p._GetResourceByID(resource.id)
table.insert(retProduction, '<span style="color:'..color..'">'..Icons.Icon({resource_data.name, type='resource', notext=true})..'&nbsp;'..Shared.numStrWithSign(production)..'</span>')
if resource_data.requires ~= nil and #resource_data.requires > 0 then
for _, required_resource in ipairs(resource_data.requires) do
local demand = production*required_resource.quantity*100
local required_resource_data = p._GetResourceByID(required_resource.id)
table.insert(retProduction, '<span style="color:red">'..Icons.Icon({required_resource_data.name, type='resource', notext=true})..'&nbsp;-'..demand..'</span>')
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
retProduction = table.concat(retProduction, ', ')..'/t ('..Icons.Icon({'Workers', type='township', notext=true})..'&nbsp;'..workers..')'
table.insert(retResources, retProduction)
end
end
return table.concat(retResources, '<br>')
table.insert(resultPart, '\n|}')
 
return table.concat(resultPart)
end
end


-- Gets a string displaying the building's benefits, or nil if no benefits
-- Generates a table showing all the worship options
function p._GetBuildingBenefits(building)
function p.getWorshipTable()
local benefits = {}
local function getCheckpointCell(checkpoint)
local stats = {
return '\n|-\n!' .. checkpoint .. '%<br/>' .. Num.formatnum(checkpoint * Township.maxWorship / 100) .. '/' .. Num.formatnum(Township.maxWorship)
population = 'Population',
end
happiness = 'Happiness',
 
education = 'Education',
local worships = GameData.getEntities(Township.worships, function(w) return not w.isHidden end)
storage = 'Storage',
local ret = {}
deadStorage = 'Dead Storage',
 
worship = 'Worship'
table.insert(ret, '{| class="wikitable stickyHeader"')
}
table.insert(ret, '\n!' .. Icons.Icon({'Worship', type='township', nolink=true}))
for key, stat in pairs(stats) do
-- Names
if building.provides[key] ~= nil and building.provides[key] ~= 0 then
for _, worship in ipairs(worships) do
local quantity = building.provides[key]
table.insert(ret, '\n!' .. Icons.Icon({worship.name, type='monster', size=50}) .. Icons.Icon({'Statue of ' .. worship.name, type='building', size=50, notext=true}))
if quantity < 0 then
end
quantity = '<span style="color:red">'..quantity..'</span>'
 
else
-- Requirements
quantity = Shared.numStrWithSign(quantity)
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(benefits, Icons.Icon({stat, type='township', notext=true})..'&nbsp;'..quantity)
end
end
table.insert(ret, '\n|' .. cellStyle .. '| ' .. table.concat(bonusPart, '<br/>'))
end
end
if #benefits > 0 then
 
return table.concat(benefits, ', ')
-- Base modifiers
table.insert(ret, getCheckpointCell(0))
for _, worship in ipairs(worships) do
table.insert(ret, '\n| ' .. Modifiers.getModifiersText(worship.modifiers))
end
end
return nil
end


-- Given a building id, find the next building upgrade
-- Checkpoint modifiers
function p._GetBuildingIDUpgrade(buildingid)
for i, checkpoint in ipairs(Township.worshipCheckpoints) do
local function checkFunc(entity)
table.insert(ret, getCheckpointCell(checkpoint))
return entity.upgradesFrom ~= nil and entity.upgradesFrom == buildingid
for _, worship in ipairs(worships) do
table.insert(ret, '\n| ' .. Modifiers.getModifiersText(worship.checkpoints[i]))
end
end
end
local upgradesTo = GameData.getEntities(Township.buildings, checkFunc)
 
if #upgradesTo > 0 then
-- Total sum
return upgradesTo[1]
-- 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
end
return nil
--]==]
table.insert(ret, '\n|}')
 
return table.concat(ret)
end
end


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


-- Given a building, find the base resource cost
local ret = {}
function p._GetBuildingBaseCost(building, _join)
-- Header
local join = _join ~= nil and _join or ', '
table.insert(ret, '{| class="wikitable infobox"')
local cost = {}
-- Name
for _, resource in ipairs(building.cost) do
table.insert(ret, '\n|-\n! ' .. Icons.getExpansionIcon(building.id) .. building.name)
local resource_data = p._GetResourceByID(resource.id)
-- Icon
table.insert(cost, Icons.Icon({resource_data.name, type='resource', notext=true})..'&nbsp;'..resource.quantity)
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
end
return table.concat(cost, join)
-- Cost
end
--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))


-- Gets a resource from id
-- Upgrades To
function p._GetResourceByID(id)
local upgradesTo = p._getBuildingUpgrade(building)
return GameData.getEntityByID(p.resources, id)
if upgradesTo ~= nil then
end
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


-- Gets text for only the biomes that have a modifier for a building
-- Maximum built
function p._GetBiomeModifiers(building)
local biomeCount = Shared.tableCount(building.biomes)
local biomeRet = {}
local maxText = Num.formatnum(building.maxUpgrades)
for _, biome in ipairs(building.biomeModifiers) do
if biomeCount > 1 then
local biomename = GameData.getEntityByID(Township.biomes, biome.biomeID).name
maxText = maxText .. ' per biome, ' .. Num.formatnum(biomeCount * building.maxUpgrades) .. ' total'
local color = biome.value < 0 and 'red' or 'green'
local biome_value = Shared.numStrWithSign(biome.value)
table.insert(biomeRet, Icons.Icon({biomename, type='biome', notext=true, nolink=true})..' <span style="color:'..color..'">'..biomename..' ('..biome_value..'%)</span>')
end
end
if #biomeRet == 0 then
table.insert(ret, '\n|-\n| <b>Maximum Built:</b><br/>' .. maxText)
return nil
-- Benefits
local benefits = p._getBuildingGroupedBenefitText(building)
if benefits ~= nil and benefits ~= '' then
table.insert(ret, '\n|-\n| <b>Provides:</b><br/>' .. benefits)
end
end
return table.concat(biomeRet, '<br>')
 
-- 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
end


-- Returns an upgrade table of a building
-- Returns an upgrade table of a building
function p.GetBuildingUpgradeTable(frame)
function p.getBuildingUpgradeTable(frame)
local buildingname = frame.args ~= nil and frame.args[1] or frame
local buildingname = frame.args ~= nil and frame.args[1] or frame
local building = p._GetBuildingByName(buildingname)
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
-- Let's find the base building
local baseBuilding = building
local baseBuilding = building
while true do
while true do
local previousBuilding = p._GetBuildingDowngrade(baseBuilding)
local previousBuilding = p._getBuildingDowngrade(baseBuilding)
if previousBuilding ~= nil then
if previousBuilding ~= nil then
baseBuilding = previousBuilding
baseBuilding = previousBuilding
Line 540: Line 698:
end
end
end
end
 
-- Let's make a list of all the buildings
-- 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)
-- Return empty string if there is only 1 building in the upgrade chain (i.e. no upgrades/downgrades)
Line 547: Line 705:
while true do
while true do
table.insert(buildingList, _curBuilding)
table.insert(buildingList, _curBuilding)
_curBuilding = p._GetBuildingIDUpgrade(_curBuilding.id)
_curBuilding = p._getBuildingUpgrade(_curBuilding)
if _curBuilding == nil then
if _curBuilding == nil then
break
break
Line 555: Line 713:
return ''
return ''
end
end
 
local ret = {}
local ret = {}
table.insert(ret, '\r\n== Upgrade Chart ==')
table.insert(ret, '\n== Upgrade Chart ==')
table.insert(ret, '\r\n{| class="wikitable"')
table.insert(ret, '\n{| class="wikitable" style="text-align:center"')
 
-- Name
-- Name
table.insert(ret, '\r\n|- style="text-align:center" \r\n! Name')
table.insert(ret, '\n|-\n!colspan="2"| Name')
for _, building in ipairs(buildingList) do
for _, building in ipairs(buildingList) do
table.insert(ret, '\r\n!'..Icons.Icon({building.name, type='building'}))
table.insert(ret, '\n!' .. Icons.getExpansionIcon(building.id) .. Icons.Icon({building.name, type='building'}))
end
end


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


-- Cost
-- Cost
table.insert(ret, '\r\n|-\r\n! Cost')
local biomeCount = Shared.tableCount(baseBuilding.biomes)
for _, building in ipairs(buildingList) do
table.insert(ret, '\n|-\n!rowspan="' .. biomeCount .. '"| Cost')
local cost = p._GetBuildingBaseCost(building)
local firstBiome = true
table.insert(ret, '\r\n|'..cost)
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
end


-- Optional params
-- Benefits
local benefitText = {}
-- Generate a row
table.insert(benefitText, '\n|-\n!rowspan="' .. biomeCount .. '"| Benefits')
-- textFunc: returns nil if no data for a building, or else returns a string
firstBiome = true
local function BuildOptionalRow(header, textFunc)
local hasText = false
local texts = {}
for _, biomeID in ipairs(baseBuilding.biomes) do
local hasTexts = false
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
for _, building in ipairs(buildingList) do
local text = textFunc(building)
local benefit = p._getBuildingBenefitText(building, biomeID, true) or ''
hasTexts = hasTexts == true or text ~= nil
if not hasText and benefit ~= '' then
texts = texts ~= nil and texts or ''
hasText = true
table.insert(texts, text)
end
end
table.insert(benefitText, '\n| ' .. benefit)
if hasTexts == true then
texts = table.concat(texts, '\r\n|')
table.insert(ret, header..texts)
end
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
BuildOptionalRow('\r\n|-\r\n! Benefits\r\n|', p._GetBuildingBenefits)
BuildOptionalRow('\r\n|-\r\n! Base Production per '..Icons.Icon({'Workers', type='township', notext=true})..'\r\n|', p._GetBuildingBaseProduction)
BuildOptionalRow('\r\n|-\r\n! Biome Production Modifiers\r\n|', p._GetBiomeModifiers)


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


local FREE_LAND = Township.sectionSize
-- Returns a row containing a task given a title and a task table
-- Gets the cost of the current price of land
function p._getTaskRow(title, task, isDailyTask)
-- Taken from township.js -> Township.getNextSectionCost
local ret = {}
function p.GetLandCost(frame)
local nthland = tonumber(frame.args ~= nil and frame.args[1] or frame)
return p._GetLandCost(nthland)
end


function p._GetLandCost(nthland)
-- If has description, we will need to rowspan the title by 2, and insert a description with colspan 2
-- First FREE_LAND plots of land are free
local hasDescription = false
if nthland <= FREE_LAND then
if task.description ~= nil then
return 0
hasDescription = true
end
end
return math.floor(15^(0.0100661358978*(nthland/32) + (nthland/32)^0.42))
local titlespan = hasDescription == true and 'rowspan="2"|' or ''
end


-- Gets the cost to buy land until you have X amount of available land
-- Title
-- Currently the max is 2048 land
table.insert(ret, '\n|-')
function p.GetCumulativeLandCost(frame)
table.insert(ret, '\n!' .. titlespan .. title)
local totalLand = tonumber(frame.args ~= nil and frame.args[1] or frame)
-- Description
return p._GetCumulativeLandCost(totalLand)
if hasDescription then
end
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 = {}


function p._GetCumulativeLandCost(totalLand)
local function getItemText(itemID)
local cost = 0
local item = Items.getItemByID(itemID)
while totalLand > FREE_LAND do
if item == nil then
cost = cost + p._GetLandCost(totalLand)
return Shared.printError('Unknown item: ' .. (itemID or 'nil'))
totalLand = totalLand - 1
else
return Icons.Icon({item.name, type='item'})
end
end
end
return cost
local function getMonsterText(monsterID)
end
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/>'))


-- Returns a table showing the land cost of a town
-- Unlock requirements, daily task specific
function p.GetLandCostTable()
if isDailyTask then
local ret = {}
table.insert(ret, '\n|' .. Shop.getRequirementString(task.requirements))
table.insert(ret, '\r\n{| class="wikitable"')
table.insert(ret, '\r\n|- style="text-align:center" \r\n! Total Land \r\n! Single Land Cost \r\n! Total Cost')
for i=FREE_LAND,Township.maxTownSize,FREE_LAND do
table.insert(ret, '\r\n|-\r\n|'..i..'\r\n|'..Icons.GP(p._GetLandCost(i))..'\r\n|'..Icons.GP(p._GetCumulativeLandCost(i)))
end
end
table.insert(ret, '\r\n|}')
return table.concat(ret)
return table.concat(ret)
end
end


-- Generates a table showing which buildings can be built in which biomes
-- Returns all the tasks of a given category
-- Skips upgraded buildings
-- TODO: Support casual tasks
function p.GetBuildingBiomeTable()
function p.getTaskTable(frame)
-- Setup the table
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 = {}
local ret = {}
table.insert(ret, '\r\n{| class="wikitable sortable" style="text-align:center"')
table.insert(ret, '{| class="wikitable lighttable stickyHeader mw-collapsible" style="text-align:left"')
table.insert(ret, '\r\n!Building')
table.insert(ret, '\n|- class="headerRow-0"')
table.insert(ret, '\n!Task')
-- Make a biomeModifiers table that will keep track of the bonus of each building
table.insert(ret, '\n!Requirements')
-- At the same time, make the output table header
table.insert(ret, '\n!Rewards')
local biomeModifiersMaster = {}
if isDailyTask then
for _, biome in ipairs(Township.biomes) do
table.insert(ret, '<br/>(In addition to [[Township#Casual Tasks|Variable]] ' .. Icons._Currency('melvorD:GP') .. ' & ' .. Icons.Icon({'Township', type='skill', notext=true}) .. ' XP)')
table.insert(ret, '\r\n!'..Icons.Icon({biome.name, type='biome', notext=true, nolink=true})..'<br>'..biome.name)
end
biomeModifiersMaster[biome.id] = false
if isDailyTask then
table.insert(ret, '\n!Unlock Requirements')
end
end
for _, _building in ipairs(p._SortedBuildings()) do
for _, task in ipairs(taskData) do
-- Fix melvorF:Statues
-- Filter out other categories
local building = p._GetBuildingByID(_building.id)
local categoryID, categoryNS, categoryLocalID = '', '', ''
-- Skip upgraded buildings
if categoryData ~= nil then
local downgrade = p._GetBuildingDowngrade(building)
categoryID = categoryData.id
if downgrade == nil then
categoryNS, categoryLocalID = Shared.getLocalID(categoryID)
-- Let's populate the biome habitability data
end
local biomeModifiers = Shared.clone(biomeModifiersMaster)
if isDailyTask or task.category == categoryID or task.category == categoryLocalID then
-- Set all valid biomes to 0
taskcount = taskcount + 1
for _, biomeid in ipairs(building.biomes) do
local title = categoryName .. ' ' .. taskcount
biomeModifiers[biomeid] = 0
table.insert(ret, p._getTaskRow(title, task, isDailyTask))
end
-- Then add the biome modifier values
for _, biomeModifier in ipairs(building.biomeModifiers) do
biomeModifiers[biomeModifier.biomeID] = biomeModifier.value
end
-- Let's build the row
table.insert(ret, '\r\n|-')
table.insert(ret, '\r\n!data-sort-value="'..building.name..'" style="text-align:left"|'..Icons.Icon({building.name, type='building'}))
for _, biome in ipairs(Township.biomes) do
local modifier = biomeModifiers[biome.id]
if modifier then
if modifier == 0 then
-- Buildable but no bonuses
table.insert(ret, '\r\n|class="table-na"|+0%')
else
-- Bonus or penalty
local class = modifier < 0 and 'table-negative' or 'table-positive'
local modifier_value = Shared.numStrWithSign(modifier)
table.insert(ret, '\r\n|class="'..class..'"|<b>'..modifier_value..'%</b>')
end
else
-- Invalid biome
table.insert(ret, '\r\n|style="border:0px"|')
end
end
end
end
end
end
table.insert(ret, '\r\n|}')
table.insert(ret, '\n|}')
return table.concat(ret)
return table.concat(ret)
end
end


 
-- Returns a table containing all the tasks that reference an item or monster
-- Generates a table showing all the maps and the number of biomes
-- e.g. p.getTaskReferenceTable({'Chicken Coop', 'dungeon'})
-- Skips upgraded buildings
-- name = item or monster name
function p.GetMapTable()
-- type = 'item' or 'monster' or 'dungeon'
-- Setup the table
function p.getTaskReferenceTable(frame)
local ret = {}
-- Returns a set containing all the desired IDs
table.insert(ret, '\r\n{| class="wikitable sortable" style="text-align:center"')
local function GetReferenceIDs(referenceName, referenceType)
table.insert(ret, '\r\n!Map')
local IDs = {}
if referenceType == 'dungeon' then
-- Make two table that will keep track of the max/min amount of land for each biome
-- We get the tasks associated with all monsters in the dungeon
-- At the same time, make the output table header
local area = nil
local biomeMax = {}
local areaTypes = {'dungeons', 'abyssDepths', 'strongholds'}
local biomeMin = {}
for _, areaType in ipairs(areaTypes) do
for _, biome in ipairs(Township.biomes) do
area = GameData.getEntityByName(areaType, referenceName)
table.insert(ret, '\r\n!'..Icons.Icon({biome.name, type='biome', notext=true, nolink=true})..'<br>'..biome.name)
if area ~= nil then
biomeMax[biome.id] = -1
break
biomeMin[biome.id] = Township.maxTownSize + 1
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
end
-- For a task, returns where to search for the desired IDs, given the type
-- Find the min and max amount for each biome
local function GetGetSearchTables(referenceType)
for _, map in ipairs(Township.maps) do
local function searchItems(task)
for _, biome in ipairs(map.biomes) do
return {task.goals.items, task.rewards.items}
biomeMax[biome.biomeID] = math.max(biomeMax[biome.biomeID], biome.count)
end
biomeMin[biome.biomeID] = math.min(biomeMin[biome.biomeID], biome.count)
local function searchMonsters(task)
return {task.goals.monsters}
end
end
-- item -> searchItems; monster or dungeon -> searchMonsters
return referenceType == 'item' and searchItems or searchMonsters
end
end
 
-- Draw all the map rows
local args = frame.args ~= nil and frame.args or frame
for _, map in ipairs(Township.maps) do
local referenceName = Shared.fixPagename(args[1])
table.insert(ret, '\r\n|-')
local referenceType = args[2]
table.insert(ret, '\r\n!style="text-align:left"|'..map.name)
local referenceIDs = GetReferenceIDs(referenceName, referenceType)
-- GetSearchTables = function searchItems/Monsters(task)
for _, biome in ipairs(map.biomes) do
local GetSearchTables = GetGetSearchTables(referenceType)
-- Color the cell if min or max value
 
local max = biomeMax[biome.biomeID]
local function checkTask(task)
local min = biomeMin[biome.biomeID]
local function checkID(entry)
local count = biome.count
return referenceIDs[entry.id] ~= nil
local class = count == max and 'table-positive' or count == min and 'table-negative' or ''
end
-- Insert cell
for _, searchTable in pairs(GetSearchTables(task)) do -- ipairs won't work if first table is nil
table.insert(ret, '\r\n|class="'..class..'"|'..count)
-- 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
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
end
table.insert(ret, '\r\n|}')
table.insert(ret, '\n|}')
return table.concat(ret)
return table.concat(ret)
end
end


return p
return p

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