Module:Township: Difference between revisions

From Melvor Idle
(getWorshipTable: Add season modifiers)
(getBuildingInfoBox, getBuildingUpgradeTable, getTraderTable: Update for v1.1.2)
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 Shop = require('Module:Shop')
local Shop = require('Module:Shop')
local GameData = require('Module:GameData')
local GameData = require('Module:GameData')
Line 19: Line 20:
else
else
return GameData.getEntityByID(Township.buildings, id)
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
end
end
Line 25: Line 42:
function p._getResourceByID(id)
function p._getResourceByID(id)
return GameData.getEntityByID(Township.resources, 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
end


Line 58: Line 87:
end
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
end


-- Given a building and biome ID, returns a string displaying the building's benefits,
-- Given a building and biome ID, returns a string displaying the building's benefits,
-- or nil if no benefits
-- or nil if no benefits
function p._getBuildingBenefits(building, biomeID, includeModifiers, delimiter)
function p._getBuildingBenefitText(building, biomeID, includeModifiers, delimiter)
-- Basic validation of inputs
-- Basic validation of inputs
if type(building) == 'table' and building.provides ~= nil and biomeID ~= nil then
if type(building) == 'table' and building.provides ~= nil and biomeID ~= nil then
Line 81: Line 152:
end
end
end
end
 
if providesData ~= nil then
if providesData ~= nil then
local resultPart = {}
local resultPart = {}
Line 109: Line 180:
-- Other stats
-- Other stats
for key, stat in pairs(stats) do
for key, stat in pairs(stats) do
-- TODO Fix always using first biome
local quantity = providesData[key]
local quantity = providesData[key]
if quantity ~= nil and quantity ~= 0 then
if quantity ~= nil and quantity ~= 0 then
Line 121: Line 191:
end
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
end


Line 291: Line 409:
table.insert(resultPart, '\n| ' .. Icons.Icon({biome.name, type='biome', nolink=true}))
table.insert(resultPart, '\n| ' .. Icons.Icon({biome.name, type='biome', nolink=true}))
table.insert(resultPart, '\n| ' .. p._getBuildingCostText(building, biomeID))
table.insert(resultPart, '\n| ' .. p._getBuildingCostText(building, biomeID))
local providesText = p._getBuildingBenefits(building, biomeID)
local providesText = p._getBuildingBenefitText(building, biomeID)
if building.modifiers ~= nil then
if building.modifiers ~= nil then
local modText = Constants.getModifiersText(building.modifiers)
local modText = Constants.getModifiersText(building.modifiers)
Line 310: Line 428:
-- Builds the table of trader items
-- Builds the table of trader items
function p.getTraderTable(frame)
function p.getTraderTable(frame)
-- Get the resources data with associated trader data
local resultPart = {}


-- Build the text
-- Build table header
local ret = {}
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
for _, resource in ipairs(p.resources) do
table.insert(resultPart, '\n|- class="headerRow-0"')
if #resource.itemConversions ~= 0 then -- Skips GP
table.insert(resultPart, '\n!colspan="2"| Item\n!Description\n!style="min-width:60px"| Cost\n!Requirements')
local ret_resource = {}


-- Header
for i, tsResource in ipairs(Township.itemConversions.fromTownship) do
table.insert(ret_resource, '\r\n==='..resource.name..'===')
local res = GameData.getEntityByID(Township.resources, tsResource.resourceID)
table.insert(ret_resource, '\r\n{| class="wikitable sortable stickyHeader"')
for j, tradeDef in ipairs(tsResource.items) do
table.insert(ret_resource, '\r\n|- class="headerRow-0"')
local item = Items.getItemByID(tradeDef.itemID)
table.insert(ret_resource, '\r\n!Item')
local itemDesc = item.customDescription
table.insert(ret_resource, '\r\n!Name')
if itemDesc == nil then
table.insert(ret_resource, '\r\n!Level')
if item.modifiers ~= nil then
table.insert(ret_resource, '\r\n!Give To')
itemDesc = Constants.getModifiersText(item.modifiers, false, true)
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
-- Each item
for _, item in ipairs(resource.itemConversions) do
-- To indicate the skill level, we need to find the recipe of the item in the target skill
-- Unfortunately Module:Items/SourceTables.getItemSources does not provide parseable data
local required_level = nil
local recipes = nil
-- Get the skill based on the item.id or else use the resource's default skill
local skill_overrides = {
['melvorD:Raw_Magic_Fish'] = 'melvorD:Fishing',
['melvorF:Apple'] = 'melvorD:Farming',
}
local skill = skill_overrides[item.id] or p._GetResourceSkill(resource.id)
local skill_namespace, skill_localid = GameData.getLocalID(skill or '')
-- Check for upgraded Crafting items and downgrade them so we can display the crafting level for the base item
-- e.g. converts Black_Dhide_Body_U -> Black_Dhide_Body for the purposes of the lookup
local lookup_id = item.id
if string.match(item.id, '_U$') then
lookup_id = string.sub(item.id, 1, #item.id - 2)
end
-- Find the recipe's level
local recipes = p._FindItemRecipes(lookup_id, skill)
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
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)
itemDesc = ''
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
end
local resQty = math.max(item.sellsFor, 2)
table.insert(ret_resource, '\r\n|}')
local costSort = i * 10000 + resQty
 
table.insert(ret, table.concat(ret_resource))
table.insert(resultPart, '\n|-\n| ' .. Icons.Icon({item.name, type='item', size=50, notext=true}))
table.insert(resultPart, '\n| ' .. 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
end
end
return table.concat(ret)
table.insert(resultPart, '\n|}')
 
return table.concat(resultPart)
end
end


Line 479: Line 542:
end
end


-- TODO Check if functions below this line are still in use
-- Gets a building and prepares all the relevant stats for the building, presented as an infobox
 
function p.getBuildingInfoBox(frame)
-- Returns the recipe for the item of a desired skill.
local name = frame.args ~= nil and frame.args[1] or frame
-- Unfortunately Module:Items/SourceTables.getItemSources does not provide parseable data so we instead use this quick function
local building = p._getBuildingByName(name)
function p._FindItemRecipes(itemid, skill)
if building == nil then
return Shared.printError('No building named "' .. name .. '" exists in the data module')
-- No skill? No recipes
if skill == nil then
return {}
end
-- the key name for each skill in the json file
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 = {}
local SkillData = GameData.getSkillData(skill)
local recipes = skill_recipe_keys[skill].recipes
local productID = skill_recipe_keys[skill].productID
if SkillData[recipes] ~= nil then
for _, recipe in ipairs(SkillData[recipes]) do
-- Special case for Herblore
if skill == 'melvorD:Herblore' then
-- Iterate over the 4 potion tiers
for _, potionid in ipairs(recipe[productID]) do
if itemid == potionid then
table.insert(results, Shared.clone(recipe))
end
end
-- Base case
else
if itemid == recipe[productID] then
table.insert(results, Shared.clone(recipe))
end
end
end
end
end
return results
end
-- Returns a list of all the Township resources
function p._ResourcesData()
-- Get a sorted list of all the resources
local resources = GameData.sortByOrderTable(Township.resources, Township.resourceDisplayOrder)
resources = Shared.clone(resources)
return resources
end


-- Returns a list of all the Township resources along with the Trader's trade ratios
function p._TraderData()
-- Get the list of resources. We get a copy instead of directly using p.resources because we are going to modify the table
local resources = p._ResourcesData()
-- Get the list of tradeable items
-- See township.js -> TownshipResource.buildResourceItemConversions for the calculation of valid items
local function matchNone(item)
return false
end
local function matchFood(item)
return item.type == 'Food' and (not string.match(item.id, '_Perfect')) and item.category ~= 'Farming' and (not item.ignoreCompletion)
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 = {
['melvorF:GP'] = {traderMatches = matchNone},
['melvorF:Food'] = {traderMatches = matchFood},
['melvorF:Wood'] = {traderMatches = matchLogs},
['melvorF:Stone'] = {traderMatches = matchOre},
['melvorF:Ore'] = {traderMatches = matchOre},
['melvorF:Coal'] = {traderMatches = matchCoal},
['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
resource.itemConversions = Shared.clone(GameData.getEntities('items', traderMatchesList[resource.id].traderMatches))
end
-- Calculate the trader's conversion ratios
-- See township.js TownshipResource.getBaseConvertToTownshipRatio and TownshipResource.getBaseConvertFromTownshipRatio for the conversion prices
for _, resource in ipairs(resources) do
if resource.id == 'melvorF:Food' then
for _, item in ipairs(resource.itemConversions) do
item.toTownship = math.max(math.floor(1000/(item.healsFor*10)), 2)
item.fromTownship = item.healsFor*5*6*5
end
elseif resource.id == 'melvorF:Planks' then
for _, item in ipairs(resource.itemConversions) do
item.toTownship = math.max(math.floor(3000/math.max(item.sellsFor, 1)), 2)
item.fromTownship = math.max(math.ceil(item.sellsFor/2)*6, 1);
end
elseif resource.id == 'melvorF:Rune_Essence' then
for _, item in ipairs(resource.itemConversions) do
item.toTownship = 5
item.fromTownship = (item.sellsFor+1)*10*6
end
elseif resource.id == 'melvorF:Leather' then
for _, item in ipairs(resource.itemConversions) do
item.toTownship = 20
item.fromTownship = 20*6
end
else
for _, item in ipairs(resource.itemConversions) do
        item.toTownship = math.max(math.floor(1000/math.max(item.sellsFor, 1)), 2)
    item.fromTownship = math.max(item.sellsFor*6, 1)
end
end
end
return resources
end
p.resources = p._TraderData()
-- Gets the associated skill of a resource by id
local resource_skill = {
['melvorF:GP'] = {skill = nil},
['melvorF:Food'] = {skill = 'melvorD:Cooking'},
['melvorF:Wood'] = {skill = 'melvorD:Woodcutting'},
['melvorF:Stone'] = {skill = 'melvorD:Mining'},
['melvorF:Ore'] = {skill = 'melvorD:Mining'},
['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
-- 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 building and prepares all the relevant stats for the building
-- TODO Rename, getBuildingInfoBox or something of the sort
function p.GetBuildingTable(frame)
local name = frame.args ~= nil and frame.args[1] or frame
local building = Shared.clone(p._GetBuildingByName(name))
local ret = {}
local ret = {}
-- Header
-- Header
table.insert(ret, '\r\n{| class="wikitable infobox"')
table.insert(ret, '{| class="wikitable infobox"')
-- Name
-- Name
table.insert(ret, '\r\n|-\r\n!'..building.name)
table.insert(ret, '\n|-\n! ' .. building.name)
-- Icon
-- Icon
table.insert(ret, '\r\n|-\r\n|style="text-align:center"|'..Icons.Icon({building.name, type='building', size='250', notext=true}))
table.insert(ret, '\n|-\n|style="text-align:center"| ' .. Icons.Icon({building.name, type='building', size='250', notext=true}))
-- ID
-- ID
table.insert(ret, '\r\n|-\r\n| <b>Building ID:</b> '..building.id)
table.insert(ret, '\n|-\n| <b>Building ID:</b> ' .. building.id)
-- Type
-- Type
table.insert(ret, '\r\n|-\r\n| <b>Type:</b> '..building.type)
table.insert(ret, '\n|-\n| <b>Type:</b> ' .. building.type)
-- Tier
-- Tier
local tier = p._getTierText(building.tier)
local tier = p._getTierText(building.tier)
table.insert(ret, '\r\n|-\r\n| <b>Requirements:</b><br>'..tier)
table.insert(ret, '\n|-\n| <b>Requirements:</b><br/>' .. tier)


-- Upgrades From
-- Upgrades From
table.insert(ret, '\r\n|-\r\n| <b>Base Cost:</b>')
table.insert(ret, '\n|-\n| <b>Base Cost:</b>')
local upgradesFrom = p._getBuildingDowngrade(building)
local upgradesFrom = p._getBuildingDowngrade(building)
if upgradesFrom ~= nil then
if upgradesFrom ~= nil then
table.insert(ret, '<br>'..Icons.Icon({upgradesFrom.name, type='building'}))
table.insert(ret, '<br/>' .. Icons.Icon({upgradesFrom.name, type='building'}))
end
end
-- Cost
-- Cost
local cost = p._GetBuildingBaseCost(building)
--table.insert(ret, '<br/>' .. p._getBuildingGroupedCostText(building))
table.insert(ret, '<br>'..cost)
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
-- Upgrades To
local upgradesTo = p._GetBuildingIDUpgrade(building.id)
local upgradesTo = p._getBuildingUpgrade(building)
if upgradesTo ~= nil then
if upgradesTo ~= nil then
table.insert(ret, '\r\n|-\r\n| <b>Upgrades To:</b>')
table.insert(ret, '\n|-\n| <b>Upgrades To:</b>')
table.insert(ret, '<br>'..Icons.Icon({upgradesTo.name, type='building'}))
table.insert(ret, '<br/>' .. Icons.Icon({upgradesTo.name, type='building'}))
local upgrade_cost = p._GetBuildingBaseCost(upgradesTo)
table.insert(ret, '\n' .. getGroupedText(upgradesTo, p._getBuildingGroupedCosts))
table.insert(ret, '<br>'..upgrade_cost)
end
end


-- Fixed benefits
-- Benefits
local benefits = p._getBuildingBenefits(building)
local benefits = p._getBuildingGroupedBenefitText(building)
if benefits ~= nil then
if benefits ~= nil and benefits ~= '' then
table.insert(ret, '\r\n|-\r\n| <b>Provides:</b> '..benefits)
table.insert(ret, '\n|-\n| <b>Provides:</b><br/>' .. benefits)
end
 
-- Production
--local production = p._GetBuildingBaseProduction(building)
--if production ~= nil then
--table.insert(ret, '\r\n|-\r\n| <b>Base Production per '..Icons.Icon({'Workers', type='township', notext=true})..':</b><br>')
--table.insert(ret, production)
--end
 
-- Modifiers
if building.modifiers ~= nil and not Shared.tableIsEmpty(building.modifiers) then
table.insert(ret, '\r\n|-\r\n| <b>Modifiers:</b>\r\n'..Constants.getModifiersText(building.modifiers, true))
end
end


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


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


-- Gets a string displaying the base production of a building, or nil if no production
-- Returns an upgrade table of a building
function p._GetBuildingBaseProduction(building)
function p.getBuildingUpgradeTable(frame)
-- TODO Fix always using first biome
local buildingname = frame.args ~= nil and frame.args[1] or frame
local production = Shared.clone(building.provides[1].resources)
local building = p._getBuildingByName(buildingname)
if building == nil then
if #production == 0 then
return Shared.printError('No building named "' .. buildingname .. '" exists in the data module')
return nil
end
end
local retResources = {}
for _, resource in ipairs(production) do
local retProduction = {}
-- 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
end
return table.concat(retResources, '<br>')
end


-- Given a building id, find the next building upgrade
function p._GetBuildingIDUpgrade(buildingid)
local function checkFunc(entity)
return entity.upgradesFrom ~= nil and entity.upgradesFrom == buildingid
end
local upgradesTo = GameData.getEntities(Township.buildings, checkFunc)
if #upgradesTo > 0 then
return upgradesTo[1]
end
return nil
end
-- Given a building, find the base resource cost
function p._GetBuildingBaseCost(building, _join)
local join = _join ~= nil and _join or ', '
local cost = {}
-- TODO Cost can vary by biome, properly handle this rather than
-- always taking costs for the first biome
for _, resource in ipairs(building.cost[1].cost) do
local resource_data = p._getResourceByID(resource.id)
table.insert(cost, Icons.Icon({resource_data.name, type='resource', notext=true})..'&nbsp;'..resource.quantity)
end
return table.concat(cost, join)
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)
-- Let's find the base building
-- Let's find the base building
local baseBuilding = building
local baseBuilding = building
Line 815: Line 642:
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 822: Line 649:
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 830: Line 657:
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.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))
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)


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


-- Returns a row containing a task given a title and a task table
-- Returns a row containing a task given a title and a task table
function p._GetTaskRow(title, task)
function p._getTaskRow(title, task)
local ret = {}
local ret = {}
 
-- If has description, we will need to rowspan the title by 2, and insert a description with colspan 2
-- If has description, we will need to rowspan the title by 2, and insert a description with colspan 2
local hasDescription = false
local hasDescription = false
Line 891: Line 726:
end
end
local titlespan = hasDescription == true and 'rowspan="2"|' or ''
local titlespan = hasDescription == true and 'rowspan="2"|' or ''
 
-- Title
-- Title
table.insert(ret, '\r\n|-')
table.insert(ret, '\n|-')
table.insert(ret, '\r\n!'..titlespan..title)
table.insert(ret, '\n!'..titlespan..title)
-- Description
-- Description
if hasDescription then
if hasDescription then
table.insert(ret, '\r\n|colspan="2"|'..task.description)
table.insert(ret, '\n|colspan="2"|'..task.description)
table.insert(ret, '\r\n|-')
table.insert(ret, '\n|-')
end
end
-- Requirements
-- Requirements
table.insert(ret, '\r\n|')
table.insert(ret, '\n|')
local requirements = {}
local requirements = {}
for _, item in ipairs(task.goals.items) do
for _, item in ipairs(task.goals.items) do
Line 920: Line 755:
end
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)
-- 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>'))
table.insert(ret, table.concat(requirements, '<br/>'))
-- Rewards
-- Rewards
table.insert(ret, '\r\n|')
table.insert(ret, '\n|')
local rewards = {}
local rewards = {}
if task.rewards.gp ~= 0 then
if task.rewards.gp ~= 0 then
Line 942: Line 777:
table.insert(rewards, Shared.formatnum(townshipResource.quantity)..' '..Icons.Icon({resourcename, type='resource'}))
table.insert(rewards, Shared.formatnum(townshipResource.quantity)..' '..Icons.Icon({resourcename, type='resource'}))
end
end
table.insert(ret, table.concat(rewards, '<br>'))
table.insert(ret, table.concat(rewards, '<br/>'))
return table.concat(ret)
return table.concat(ret)
end
end


-- Returns all the tasks of a given category
-- Returns all the tasks of a given category
function p.GetTaskTable(frame)
-- TODO: Support casual tasks
function p.getTaskTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
local category = frame.args ~= nil and frame.args[1] or frame
local categoryData = GameData.getEntityByID(Township.taskCategories, category)
local categoryData = GameData.getEntityByID(Township.taskCategories, category)
Line 955: Line 791:
local categoryname = categoryData.name
local categoryname = categoryData.name
local taskcount = 0
local taskcount = 0
 
local ret = {}
local ret = {}
table.insert(ret, '\r\n{| class="wikitable lighttable" style="text-align:left"')
table.insert(ret, '{| class="wikitable lighttable" style="text-align:left"')
table.insert(ret, '\r\n!Task')
table.insert(ret, '\n!Task')
table.insert(ret, '\r\n!Requirements')
table.insert(ret, '\n!Requirements')
table.insert(ret, '\r\n!Rewards')
table.insert(ret, '\n!Rewards')
for _, task in ipairs(Township.tasks) do
for _, task in ipairs(Township.tasks) do
Line 967: Line 803:
taskcount = taskcount + 1
taskcount = taskcount + 1
local title = categoryname..' '..taskcount
local title = categoryname..' '..taskcount
table.insert(ret, p._GetTaskRow(title, task))
table.insert(ret, p._getTaskRow(title, task))
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
-- Returns a table containing all the tasks that reference an item or monster
-- e.g. p.GetTaskReferenceTable({'Chicken Coop', 'dungeon'})
-- e.g. p.getTaskReferenceTable({'Chicken Coop', 'dungeon'})
-- name = item or monster name
-- name = item or monster name
-- type = 'item' or 'monster' or 'dungeon'
-- type = 'item' or 'monster' or 'dungeon'
function p.GetTaskReferenceTable(frame)
function p.getTaskReferenceTable(frame)
-- Returns a set containing all the desired IDs
-- Returns a set containing all the desired IDs
local function GetReferenceIDs(referenceName, referenceType)
local function GetReferenceIDs(referenceName, referenceType)
Line 1,008: Line 844:
return referenceType == 'item' and searchItems or searchMonsters
return referenceType == 'item' and searchItems or searchMonsters
end
end
 
local args = frame.args ~= nil and frame.args or frame
local args = frame.args ~= nil and frame.args or frame
local referenceName = Shared.fixPagename(args[1])
local referenceName = Shared.fixPagename(args[1])
Line 1,035: Line 871:
return ''
return ''
end
end
 
-- Build the table
-- Build the table
local ret = {}
local ret = {}
table.insert(ret, '==Tasks==')
table.insert(ret, '==Tasks==')
table.insert(ret, '\r\n{| class="wikitable" style="text-align:left"')
table.insert(ret, '\n{| class="wikitable" style="text-align:left"')
table.insert(ret, '\r\n!Task')
table.insert(ret, '\n!Task')
table.insert(ret, '\r\n!Requirements')
table.insert(ret, '\n!Requirements')
table.insert(ret, '\r\n!Rewards')
table.insert(ret, '\n!Rewards')
for _, task in ipairs(tasks) do
for _, task in ipairs(tasks) do
local categoryname = GameData.getEntityByID(Township.taskCategories, task.category).name
local categoryname = GameData.getEntityByID(Township.taskCategories, task.category).name
local title = '[[Township/Tasks#'..categoryname..'|'..categoryname..']]'
local title = '[[Township/Tasks#'..categoryname..'|'..categoryname..']]'
table.insert(ret, p._GetTaskRow(title, task))
table.insert(ret, p._getTaskRow(title, task))
end
end
table.insert(ret, '\r\n|}')
table.insert(ret, '\n|}')
return table.concat(ret)
return table.concat(ret)
end
end
Line 1,057: Line 893:
function p.GetWorshipTable()
function p.GetWorshipTable()
return p.getWorshipTable()
return p.getWorshipTable()
end
function p.GetBuildingTable(frame)
return p.getBuildingInfoBox()
end
function p.GetBuildingUpgradeTable(frame)
return p.getBuildingUpgradeTable(frame)
end
function p.GetTaskTable(frame)
return p.getTaskTable(frame)
end
function p.GetTaskReferenceTable(frame)
return p.getTaskReferenceTable(frame)
end
end


return p
return p

Revision as of 00:52, 26 April 2023

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 Shop = require('Module:Shop')
local GameData = require('Module:GameData')
local Constants = require('Module:Constants')

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',
				worship = 'Worship'
			}
			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 .. '">' .. Shared.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

			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 and population requirements for a tier
-- Returns {population=X, level=X}
function p._getTierRequirements(tier)
	return Township.populationForTier[tier]
end

-- Returns a string containing the Township level and population requirements for a tier
function p._getTierText(tier)
	local tierData = p._getTierRequirements(tier)
	if tierData ~= nil then
		local tierText = Icons._SkillReq('Township', tierData.level, false)
		if tierData.population > 0 then
			tierText = tierText .. '<br/>' .. Icons.Icon({'Population', type='township', notext=true}) .. '&nbsp;' .. Shared.formatnum(tierData.population)
		end
		return tierText
	end
end

-- Generates a table of all seasons, their type/requirements, and modifiers
function p.getSeasonTable(frame)
	-- Manual data specifying the worship requirement for those rare seasons
	local seasonReqs = {
		["Nightfall"] = Icons.Icon({'Township%23Worship', 'Bane Worship', img='Statue of Bane', type='building'}),
		["SolarEclipse"] = Icons.Icon({'Township%23Worship', 'The Herald Worship', img='Statue of The Herald', type='building'})
	}

	local seasons = Shared.shallowClone(Township.seasons)
	table.sort(seasons, function(a, b) return a.order < b.order end)

	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\n|- class="headerRow-0"')
	table.insert(resultPart, '\n!colspan="2" | Season\n!Type\n!Modifiers')

	for i, season in ipairs(seasons) do
		local ns, localSeasonID = Shared.getLocalID(season.id)
		local reqs = seasonReqs[localSeasonID]
		table.insert(resultPart, '\n|-')
		table.insert(resultPart, '\n|class="table-img"| ' .. Icons.Icon({season.name, type='township', size=50, nolink=true, notext=true}))
		table.insert(resultPart, '\n| ' .. Icons.Icon({season.name, type='township', nolink=true, noicon=true}))
		table.insert(resultPart, '\n| ' .. (season.order <= 3 and 'Regular' or 'Rare'))
		if reqs ~= nil then
			table.insert(resultPart, '<br/>Requires ' .. reqs)
		end
		table.insert(resultPart, '\n| ' .. Constants.getModifiersText(season.modifiers))
	end
	table.insert(resultPart, '\n|}')

	return table.concat(resultPart)
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="2"| 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%23Population', 'Population', img='Population', type='township'}))

	for i, biome in ipairs(Township.biomes) do
		local reqs = p._getTierRequirements(biome.tier)
		table.insert(resultPart, '\n|-\n|class="table-img"| ' .. Icons.Icon({biome.name, type='biome', size=50, nolink=true, notext=true}))
		table.insert(resultPart, '\n| ' .. biome.name)
		table.insert(resultPart, '\n|style="text-align:right"| ' .. reqs.level)
		table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. reqs.population .. '"| ' .. Shared.formatnum(reqs.population))
	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)
	-- Setup the table
	local ret = {}
	table.insert(ret, '{| class="wikitable sortable stickyHeader" style="text-align:center"')
	table.insert(ret, '\n|- class="headerRow-0"')
	table.insert(ret, '\n! Building')

	-- Generate the table header, one column per biome
	for _, biome in ipairs(Township.biomes) do
		table.insert(ret, '\n! ' .. Icons.Icon({biome.name, type='biome', notext=true, nolink=true}) .. '<br/>' .. biome.name)
	end

	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

			-- Build the row
			table.insert(ret, '\n|-')
			table.insert(ret, '\n!data-sort-value="' .. building.name .. '" style="text-align:left"| ' .. Icons.Icon({building.name, type='building'}))
			for _, biome in ipairs(Township.biomes) do
				if buildingBiomes[biome.id] then
					-- Buildable
					table.insert(ret, '\n|class="table-positive"| ✓')
				else
					-- Invalid biome
					table.insert(ret, '\n|style="border:0px"|')
				end
			end
		end
	end
	table.insert(ret, '\n|}')

	return table.concat(ret)
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!Type\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, size=50}))
				table.insert(resultPart, '\n' .. rowSpanOnly .. '| ' .. Icons.getExpansionIcon(building.id) .. Icons.Icon({buildingName, type='building', noicon=true}))
				table.insert(resultPart, '\n|' .. 'data-sort-value="' .. building.tier .. '"' .. rowSpan .. '| ' .. (p._getTierText(building.tier) or ''))
				table.insert(resultPart, '\n' .. rowSpanOnly .. '| ' .. building.type)
				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 = Constants.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 = Constants.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', size=50, notext=true}))
			table.insert(resultPart, '\n| ' .. 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/>' .. Shared.formatnum(checkpoint * Township.maxWorship / 100) .. '/' .. Shared.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| ' .. Constants.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| ' .. Constants.getModifiersText(worship.checkpoints[i]))
		end
	end

	-- Total sum
	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|' .. Constants.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! ' .. 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)
	-- Type
	table.insert(ret, '\n|-\n| <b>Type:</b> ' .. building.type)
	-- Tier
	local tier = p._getTierText(building.tier)
	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

	-- 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.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))
	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)
	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|')
	local requirements = {}
	for _, item in ipairs(task.goals.items) do
		local itemname = GameData.getEntityByID('items', item.id).name
		table.insert(requirements, Shared.formatnum(item.quantity)..' '..Icons.Icon({itemname, type='item'}))
	end
	for _, monster in ipairs(task.goals.monsters) do
		local monstername = GameData.getEntityByID('monsters', monster.id).name
		table.insert(requirements, Shared.formatnum(monster.quantity)..' '..Icons.Icon({monstername, type='monster'}))
	end
	for _, skill in ipairs(task.goals.skillXP) do
		local skillname = GameData.getSkillData(skill.id).name
		table.insert(requirements, Shared.formatnum(skill.quantity)..' '..Icons.Icon({skillname, type='skill'})..' XP')
	end
	for _, building in ipairs(task.goals.buildings) do
		local buildingname = p._GetBuildingByID(building.id).name
		table.insert(requirements, Shared.formatnum(building.quantity)..' '..Icons.Icon({buildingname, type='building'}))
	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 = {}
	if task.rewards.gp ~= 0 then
		table.insert(rewards, Icons.GP(task.rewards.gp))
	end
	if task.rewards.slayerCoins ~= 0 then
		table.insert(rewards, Icons.SC(task.rewards.slayerCoins))
	end
	for _, item in ipairs(task.rewards.items) do
		local itemname = GameData.getEntityByID('items', item.id).name
		table.insert(rewards, Shared.formatnum(item.quantity)..' '..Icons.Icon({itemname, type='item'}))
	end
	for _, skill in ipairs(task.rewards.skillXP) do
		local skillname = GameData.getSkillData(skill.id).name
		table.insert(rewards, Shared.formatnum(skill.quantity)..' '..Icons.Icon({skillname, type='skill'})..' XP')
	end
	for _, townshipResource in ipairs(task.rewards.townshipResources) do
		local resourcename = p._getResourceByID(townshipResource.id).name
		table.insert(rewards, Shared.formatnum(townshipResource.quantity)..' '..Icons.Icon({resourcename, type='resource'}))
	end
	table.insert(ret, table.concat(rewards, '<br/>'))
	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.getEntityByID(Township.taskCategories, category)
	if categoryData == nil then
		return Shared.printError('Invalid task category specified: ' .. (tostring(category) or 'nil'))
	end
	local categoryname = categoryData.name
	local taskcount = 0

	local ret = {}
	table.insert(ret, '{| class="wikitable lighttable" style="text-align:left"')
	table.insert(ret, '\n!Task')
	table.insert(ret, '\n!Requirements')
	table.insert(ret, '\n!Rewards')
	
	for _, task in ipairs(Township.tasks) do
		-- Filter out other categories
		if task.category == category then
			taskcount = taskcount + 1
			local title = categoryname..' '..taskcount
			table.insert(ret, p._getTaskRow(title, task))
		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 monsters = GameData.getEntityByName('dungeons', referenceName).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[GameData.getEntityByName('monsters', 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 ipairs(GetSearchTables(task)) do
			-- 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
		local categoryname = GameData.getEntityByID(Township.taskCategories, task.category).name
		local title = '[[Township/Tasks#'..categoryname..'|'..categoryname..']]'
		table.insert(ret, p._getTaskRow(title, task))
	end
	table.insert(ret, '\n|}')
	return table.concat(ret)
end

-- TODO Temporary functions, these exist to enable renaming of functions
-- above without causing templates to break until they are migrated to these
-- newly-named functions
function p.GetWorshipTable()
	return p.getWorshipTable()
end

function p.GetBuildingTable(frame)
	return p.getBuildingInfoBox()
end

function p.GetBuildingUpgradeTable(frame)
	return p.getBuildingUpgradeTable(frame)
end

function p.GetTaskTable(frame)
	return p.getTaskTable(frame)
end

function p.GetTaskReferenceTable(frame)
	return p.getTaskReferenceTable(frame)
end

return p