Module:Township

From Melvor Idle
Revision as of 01:14, 24 April 2023 by Auron956 (talk | contribs) (Various amends to update for v1.1.2, still WIP)

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

local Shared = require('Module:Shared')
local Icons = require('Module:Icons')
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 resource from id
function p._getResourceByID(id)
	return GameData.getEntityByID(Township.resources, id)
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 and biome ID, returns a string displaying the building's benefits,
-- or nil if no benefits
function p._getBuildingBenefits(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
				-- TODO Fix always using first biome
				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

-- 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._getBuildingBenefits(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)
	-- Get the resources data with associated trader data
	
	-- Build the text
	local ret = {}
	for _, resource in ipairs(p.resources) do
		if #resource.itemConversions ~= 0 then -- Skips GP
			local ret_resource = {}
			
			-- Header
			table.insert(ret_resource, '\r\n==='..resource.name..'===')
			table.insert(ret_resource, '\r\n{| class="wikitable sortable stickyHeader"')
			table.insert(ret_resource, '\r\n|- class="headerRow-0"')
			table.insert(ret_resource, '\r\n!Item')
			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
			
			-- 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
					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
			
			table.insert(ret_resource, '\r\n|}')
			
			table.insert(ret, table.concat(ret_resource))
		end
	end
	return table.concat(ret)
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
		table.insert(ret, '\n|style="text-align:center"| ' .. Shop.getRequirementString(worship.unlockRequirements))
	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

-- TODO Check if functions below this line are still in use

-- Returns the recipe for the item of a desired skill.
-- Unfortunately Module:Items/SourceTables.getItemSources does not provide parseable data so we instead use this quick function
function p._FindItemRecipes(itemid, skill)
	
	-- 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
	
	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 = {}

	-- Header
	table.insert(ret, '\r\n{| class="wikitable infobox"')
	-- Name
	table.insert(ret, '\r\n|-\r\n!'..building.name)
	-- 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
	table.insert(ret, '\r\n|-\r\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
	local cost = p._GetBuildingBaseCost(building)
	table.insert(ret, '<br>'..cost)
	
	-- Upgrades To
	local upgradesTo = p._GetBuildingIDUpgrade(building.id)
	if upgradesTo ~= nil then
		table.insert(ret, '\r\n|-\r\n| <b>Upgrades To:</b>')
		table.insert(ret, '<br>'..Icons.Icon({upgradesTo.name, type='building'}))
		local upgrade_cost = p._GetBuildingBaseCost(upgradesTo)
		table.insert(ret, '<br>'..upgrade_cost)
	end

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

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

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

-- Gets a string displaying the base production of a building, or nil if no production
function p._GetBuildingBaseProduction(building)
	-- TODO Fix always using first biome
	local production = Shared.clone(building.provides[1].resources)
	
	if #production == 0 then
		return nil
	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
	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._GetBuildingIDUpgrade(_curBuilding.id)
		if _curBuilding == nil then
			break
		end
	end
	if #buildingList == 1 then
		return ''
	end
	
	local ret = {}
	table.insert(ret, '\r\n== Upgrade Chart ==')
	table.insert(ret, '\r\n{| class="wikitable"')
	
	-- Name
	table.insert(ret, '\r\n|- style="text-align:center" \r\n! Name')
	for _, building in ipairs(buildingList) do
		table.insert(ret, '\r\n!'..Icons.Icon({building.name, type='building'}))
	end

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

	-- Cost
	table.insert(ret, '\r\n|-\r\n! Cost')
	for _, building in ipairs(buildingList) do
		local cost = p._GetBuildingBaseCost(building)
		table.insert(ret, '\r\n|'..cost)
	end

	-- Optional params
	
	-- Generate a row
	-- textFunc: returns nil if no data for a building, or else returns a string
	local function BuildOptionalRow(header, textFunc)
		local texts = {}
		local hasTexts = false
		for _, building in ipairs(buildingList) do
			local text = textFunc(building)
			hasTexts = hasTexts == true or text ~= nil
			texts = texts ~= nil and texts or ''
			table.insert(texts, text)
		end
		if hasTexts == true then
			texts = table.concat(texts, '\r\n|')
			table.insert(ret, header..texts)
		end
	end
	BuildOptionalRow('\r\n|-\r\n! Benefits\r\n|', p._getBuildingBenefits)

	-- End
	table.insert(ret, '\r\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, '\r\n|-')
	table.insert(ret, '\r\n!'..titlespan..title)
	-- Description
	if hasDescription then
		table.insert(ret, '\r\n|colspan="2"|'..task.description)
		table.insert(ret, '\r\n|-')
	end
	-- Requirements
	table.insert(ret, '\r\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, '\r\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
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, '\r\n{| class="wikitable lighttable" style="text-align:left"')
	table.insert(ret, '\r\n!Task')
	table.insert(ret, '\r\n!Requirements')
	table.insert(ret, '\r\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, '\r\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, '\r\n{| class="wikitable" style="text-align:left"')
	table.insert(ret, '\r\n!Task')
	table.insert(ret, '\r\n!Requirements')
	table.insert(ret, '\r\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, '\r\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

return p