Anonymous

Module:Township: Difference between revisions

From Melvor Idle
Fix township building sticky header and add cell borders
No edit summary
(Fix township building sticky header and add cell borders)
 
(43 intermediate revisions by 3 users not shown)
Line 1: Line 1:
local Shared = require('Module:Shared')
local Shared = require('Module:Shared')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
local Monsters = require('Module:Monsters')
local Shop = require('Module:Shop')
local GameData = require('Module:GameData')
local GameData = require('Module:GameData')
local Constants = require('Module:Constants')
local Constants = require('Module:Constants')
 
local Num = require('Module:Number')


local p = {}
local p = {}
Line 10: Line 13:
p.Township = Township
p.Township = Township


-- Returns the namespace name (eventually we should use an icon)
-- Gets a Township building by ID, e.g. melvorF:Hunters_Cabin
function p.PLACEHOLDER_NAMESPACE_ICON(namespace)
function p._getBuildingByID(id)
local namespaces = {
-- Check for the special statue case
melvorD = 'Demo',
if id == 'melvorF:Statues' then
melvorF = 'Full',
local building = Shared.clone(GameData.getEntityByID(Township.buildings, id))
melvorTotH = 'TotH'
building.name = 'Statue of Worship'
}
return building
return namespaces[namespace]
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
end


-- Returns the recipe for the item of a desired skill.
-- Given a building, find the building's downgrade
-- Unfortunately Module:Items/SourceTables.getItemSources does not provide parseable data so we instead use this quick function
function p._getBuildingDowngrade(building)
function p._FindItemRecipes(itemid, skill)
if building.upgradesFrom ~= nil then
return p._getBuildingByID(building.upgradesFrom)
-- No skill? No recipes
if skill == nil then
return {}
end
end
return nil
-- the key name for each skill in the json file
end
local skill_recipe_keys = {
['melvorD:Woodcutting'] = {recipes='trees', productID='productId'}, -- lowercase "d"
['melvorD:Fishing'] = {recipes='fish', productID='productId'}, -- lowercase "d"
['melvorD:Cooking'] = {recipes='recipes', productID='productID'},
['melvorD:Mining'] = {recipes='rockData', productID='productId'}, -- lowercase "d"
['melvorD:Smithing'] = {recipes='recipes', productID='productID'},
['melvorD:Farming'] = {recipes='recipes', productID='productId'}, -- lowercase "d"
['melvorD:Summoning'] = {recipes='recipes', productID='productID'},
['melvorD:Fletching'] = {recipes='recipes', productID='productID'},
['melvorD:Crafting'] = {recipes='recipes', productID='productID'},
['melvorD:Runecrafting'] = {recipes='recipes', productID='productID'},
['melvorD:Herblore'] = {recipes='recipes', productID='potionIDs'} -- Special case potions I-IV
--[[ Excluded skills:
Attack, Strength, Defence, Magic, Ranged, Prayer, Slayer
Thieving, Agility, Astrology, Firemaking, Township (not items)]]
}


local results = {}
-- Given a building and biome ID, returns the cost of constructing the building
-- within that biome as a human readable text string. Returns nil if the building
local SkillData = GameData.getSkillData(skill)
-- cannot be built within that biome.
local recipes = skill_recipe_keys[skill].recipes
function p._getBuildingCostText(building, biomeID, delimiter)
local productID = skill_recipe_keys[skill].productID
-- Basic validation of inputs
if type(building) == 'table' and building.cost ~= nil and biomeID ~= nil then
if SkillData[recipes] ~= nil then
local delim = delimiter
for _, recipe in ipairs(SkillData[recipes]) do
if delim == nil then
-- Special case for Herblore
delim = ', '
if skill == 'melvorD:Herblore' then
end
-- Iterate over the 4 potion tiers
for i, costDef in ipairs(building.cost) do
for _, potionid in ipairs(recipe[productID]) do
if costDef.biomeID == biomeID then
if itemid == potionid then
local resultPart = {}
table.insert(results, Shared.clone(recipe))
for j, cost in ipairs(costDef.cost) do
local resData = p._getResourceByID(cost.id)
if resData ~= nil then
table.insert(resultPart, Icons.Icon({resData.name, type='resource', notext=true, nolink=true, qty=cost.quantity}))
end
end
end
end
-- Base case
return table.concat(resultPart, delim)
else
if itemid == recipe[productID] then
table.insert(results, Shared.clone(recipe))
end
end
end
end
end
end
end
return results
end
end


-- Returns a list of all the Township resources
-- Given a building, groups biomes for which that building has a common cost
function p._ResourcesData()
function p._getBuildingGroupedCosts(building)
local biomeGroups = {}
-- Get a sorted list of all the resources
for i, biomeID in ipairs(building.biomes) do
local resources = GameData.sortByOrderTable(Township.resources, Township.resourceDisplayOrder[1].ids)
local currentBiomeCost = p._getBuildingCostText(building, biomeID)
resources = Shared.clone(resources)
local found = false
for j, biomeGroup in ipairs(biomeGroups) do
return resources
if biomeGroup.cost == currentBiomeCost then
-- Another biome exists with this cost
table.insert(biomeGroup.biomeIDs, biomeID)
found = true
break
end
end
if not found then
table.insert(biomeGroups, { biomeIDs = { biomeID }, cost = currentBiomeCost})
end
end
return biomeGroups
end
end


-- Returns a list of all the Township resources along with the Trader's trade ratios
-- Given a building, returns a text string repesenting the building costs for all biomes
function p._TraderData()
function p._getBuildingGroupedCostText(building)
-- Get the list of resources. We get a copy instead of directly using p.resources because we are going to modify the table
local resultPart = {}
local resources = p._ResourcesData()
local biomeGroups = p._getBuildingGroupedCosts(building)
if Shared.tableCount(biomeGroups) == 1 then
-- Get the list of tradeable items
-- If only one entry then simply output the cost
-- See township.js -> TownshipResource.buildResourceItemConversions for the calculation of valid items
table.insert(resultPart, biomeGroups[1].cost)
local function matchNone(item)
else
return false
-- Otherwise, split by biome group
end
for i, biomeGroup in ipairs(biomeGroups) do
local function matchFood(item)
local biomeText = {}
return item.type == 'Food' and (not string.match(item.id, '_Perfect')) and item.category ~= 'Farming' and (not item.ignoreCompletion)
for j, biomeID in ipairs(biomeGroup.biomeIDs) do
end
local biome = GameData.getEntityByID(Township.biomes, biomeID)
local function matchLogs(item)
table.insert(biomeText, Icons.Icon({biome.name, type='biome', notext=true, nolink=true, alt=biome.name}))
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)
local valid_tiers = {'Leather', 'Hard Leather', 'Dragonhide', 'Elderwood', 'Revenant', 'Carrion'}
for _, tier in ipairs(valid_tiers) do
if item.tier == tier then
return true
end
end
table.insert(resultPart, table.concat(biomeText, ', ') .. ': ' .. biomeGroup.cost)
end
end
return false
end
end
return table.concat(resultPart, '<br/>')
end


local traderMatchesList = {
-- Given a building and biome ID, returns a string displaying the building's benefits,
['melvorF:GP'] = {traderMatches = matchNone},
-- or nil if no benefits
['melvorF:Food'] = {traderMatches = matchFood},
function p._getBuildingBenefitText(building, biomeID, includeModifiers, delimiter)
['melvorF:Wood'] = {traderMatches = matchLogs},
-- Basic validation of inputs
['melvorF:Stone'] = {traderMatches = matchOre},
if type(building) == 'table' and building.provides ~= nil and biomeID ~= nil then
['melvorF:Ore'] = {traderMatches = matchOre},
local delim = delimiter
['melvorF:Coal'] = {traderMatches = matchCoal},
if delim == nil then
['melvorF:Bar'] = {traderMatches = matchBar},
delim = ', '
['melvorF:Herbs'] = {traderMatches = matchHerb},
end
['melvorF:Rune_Essence'] = {traderMatches = matchEssence},
local includeMods = includeModifiers
['melvorF:Leather'] = {traderMatches = matchLeather},
if includeMods == nil then
['melvorF:Potions'] = {traderMatches = matchPotion},
includeMods = false
['melvorF:Planks'] = {traderMatches = matchLogs},
end
['melvorF:Clothing'] = {traderMatches = matchClothing}
 
}
local providesData = nil
for i, provides in ipairs(building.provides) do
if provides.biomeID == biomeID then
providesData = provides
break
end
end


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


-- Builds the table of trader items
-- Given a building, groups biomes for which that building has a common benefit/provides
function p.GetTraderTable(frame)
function p._getBuildingGroupedBenefits(building, includeModifiers)
-- Get the resources data with associated trader data
if includeModifiers == nil then
includeModifiers = true
-- Build the text
end
local ret = {}
local biomeGroups = {}
for _, resource in ipairs(p.resources) do
for i, biomeID in ipairs(building.biomes) do
if #resource.itemConversions ~= 0 then -- Skips GP
local currentBiomeBenefit = p._getBuildingBenefitText(building, biomeID, includeModifiers)
local ret_resource = {}
local found = false
for j, biomeGroup in ipairs(biomeGroups) do
-- Header
if biomeGroup.benefit == currentBiomeBenefit then
table.insert(ret_resource, '\r\n==='..resource.name..'===')
-- Another biome exists with this cost
table.insert(ret_resource, '\r\n{| class="wikitable sortable stickyHeader"')
table.insert(biomeGroup.biomeIDs, biomeID)
table.insert(ret_resource, '\r\n|- class="headerRow-0"')
found = true
table.insert(ret_resource, '\r\n!Item')
break
table.insert(ret_resource, '\r\n!Name')
table.insert(ret_resource, '\r\n!DLC')
table.insert(ret_resource, '\r\n!Level')
table.insert(ret_resource, '\r\n!Give To')
table.insert(ret_resource, '\r\n!Take From')
table.insert(ret_resource, '\r\n!Value')
table.insert(ret_resource, '\r\n!Value/Resource')
if resource.id =='melvorF:Food' then
table.insert(ret_resource, '\r\n!Heals')
table.insert(ret_resource, '\r\n!Heals/Resource')
end
end
end
-- Each item
if not found then
for _, item in ipairs(resource.itemConversions) do
table.insert(biomeGroups, { biomeIDs = { biomeID }, cost = currentBiomeBenefit})
-- To indicate the skill level, we need to find the recipe of the item in the target skill
-- 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.Icon({item.name, type='item', noicon=true}))
-- DLC
local item_namespace, item_localid = GameData.getLocalID(item.id)
table.insert(ret_resource, '\r\n|style="text-align:center"|'..p.PLACEHOLDER_NAMESPACE_ICON(item_namespace))
-- 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
end
end
return table.concat(ret)
return biomeGroups
end
end


-- Gets the associated skill of a resource by id
-- Given a building, returns a text string repesenting the building benefits for all biomes
local resource_skill = {
function p._getBuildingGroupedBenefitText(building, includeModifiers)
['melvorF:GP'] = {skill = nil},
if includeModifiers == nil then
['melvorF:Food'] = {skill = 'melvorD:Cooking'},
includeModifiers = true
['melvorF:Wood'] = {skill = 'melvorD:Woodcutting'},
end
['melvorF:Stone'] = {skill = 'melvorD:Mining'},
local resultPart = {}
['melvorF:Ore'] = {skill = 'melvorD:Mining'},
local biomeGroups = p._getBuildingGroupedBenefits(building, includeModifiers)
['melvorF:Coal'] = {skill = 'melvorD:Mining'},
if Shared.tableCount(biomeGroups) == 1 then
['melvorF:Bar'] = {skill = 'melvorD:Smithing'},
-- If only one entry then simply output the cost
['melvorF:Herbs'] = {skill = 'melvorD:Farming'},
table.insert(resultPart, biomeGroups[1].cost)
['melvorF:Rune_Essence'] = {skill = 'melvorD:Mining'},
else
['melvorF:Leather'] = {skill = nil},
-- Otherwise, split by biome group
['melvorF:Potions'] = {skill = 'melvorD:Herblore'},
for i, biomeGroup in ipairs(biomeGroups) do
['melvorF:Planks'] = {skill = 'melvorD:Woodcutting'},
local biomeText = {}
['melvorF:Clothing'] = {skill = 'melvorD:Crafting'}
for j, biomeID in ipairs(biomeGroup.biomeIDs) do
}
local biome = GameData.getEntityByID(Township.biomes, biomeID)
function p._GetResourceSkill(id)
table.insert(biomeText, Icons.Icon({biome.name, type='biome', notext=true, nolink=true, alt=biome.name}))
return resource_skill[id].skill
end
table.insert(resultPart, table.concat(biomeText, ', ') .. ': ' .. biomeGroup.cost)
end
end
return table.concat(resultPart, '<br/>')
end
end


-- Gets a Township building by ID, e.g. melvorF:Hunters_Cabin
-- Returns a sorted list of all Township buildings
function p._GetBuildingByID(id)
function p._sortedBuildings(keepUnsorted)
return GameData.getEntityByID(Township.buildings, id)
local ku = true
end
if keepUnsorted ~= nil then
 
ku = keepUnsorted
-- Gets a Township building by name, e.g. Hunters Cabin
end
function p._GetBuildingByName(name)
return GameData.sortByOrderTable(Township.buildings, Township.buildingDisplayOrder, ku)
return GameData.getEntityByName(Township.buildings, name)
end
end


-- Gets the Township level and population requirements for a tier
-- Gets the Township level and population requirements for a tier
-- Returns {population=X, level=X}
-- Returns {population=X, level=X}
function p._GetTierRequirements(tier)
function p._getTierRequirements(tier)
return Township.populationForTier[tier]
return Township.populationForTier[tier]
end
end


-- Returns a string containing the Township level and population requirements for a tier
-- Returns a string containing the Township level and population requirements for a tier
function p._GetTierText(tierlevel)
function p._getTierText(tier)
local tier = p._GetTierRequirements(tierlevel)
local tierData = p._getTierRequirements(tier)
return Icons._SkillReq('Township', tier.level, false)..'<br>'..Icons.Icon({'Population', type='township', notext=true})..'&nbsp;'..tier.population
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
end


-- Gets a building and prepares all the relevant stats for the building
-- Generates a table of all seasons, their type/requirements, and modifiers
function p.GetBuildingTable(frame)
function p.getSeasonTable(frame)
local name = frame.args ~= nil and frame.args[1] or frame
-- Manual data specifying the worship requirement for those rare seasons
local building = Shared.clone(p._GetBuildingByName(name))
local seasonReqs = {
local ret = {}
["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'}),
["Lemon"] = Icons.Icon({'Ancient_Relics', 'Ancient Relics', img='Ancient Relics'})
}
 
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


-- Header
-- Generates a table listing all biomes and their associated requirements
table.insert(ret, '\r\n{| class="wikitable infobox"')
function p.getBiomeTable(frame)
-- Name
local resultPart = {}
table.insert(ret, '\r\n|-\r\n!'..building.name)
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
-- Icon
table.insert(resultPart, '\n|- class="headerRow-0"')
table.insert(ret, '\r\n|-\r\n|style="text-align:center"|'..Icons.Icon({building.name, type='building', size='250', notext=true}))
table.insert(resultPart, '\n!rowspan="2" colspan="2"| Biome\n!colspan="2"| Requirements')
-- ID
table.insert(resultPart, '\n|- class="headerRow-1"')
table.insert(ret, '\r\n|-\r\n| <b>Building ID:</b> '..building.id)
table.insert(resultPart, '\n! ' .. Icons.Icon({'Township', 'Level', type='skill', nolink=true}))
-- Type
table.insert(resultPart, '\n! ' .. Icons.Icon({'Township', 'Population', img='Population', type='township', section='Population' }))
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
for i, biome in ipairs(Township.biomes) do
table.insert(ret, '\r\n|-\r\n| <b>Base Cost:</b>')
local reqs = p._getTierRequirements(biome.tier)
local upgradesFrom = p._GetBuildingDowngrade(building)
table.insert(resultPart, '\n|-\n|class="table-img"| ' .. Icons.Icon({biome.name, type='biome', size=50, nolink=true, notext=true}))
if upgradesFrom ~= nil then
table.insert(resultPart, '\n| ' .. biome.name)
table.insert(ret, '<br>'..Icons.Icon({upgradesFrom.name, type='building'}))
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
end
-- Cost
table.insert(resultPart, '\n|}')
local cost = p._GetBuildingBaseCost(building)
 
table.insert(ret, '<br>'..cost)
return table.concat(resultPart)
end
 
-- Generates a table showing which buildings can be built in which biomes
-- Skips upgraded buildings
function p.getBuildingBiomeTable(frame)
local tbl = mw.html.create('table')
:addClass('wikitable sortable stickyHeader')
:css('text-align', 'center')
 
local header = mw.html.create('tr'):addClass('headerRow-0')
local level = mw.html.create('tr'):addClass('sorttop')
local pop = mw.html.create('tr'):addClass('sorttop')
 
header:tag('th')
:css('z-index', '2')
:wikitext('Building')
level:tag('th')
:wikitext(Icons.Icon({'Township', 'Level', type='skill', nolink=true}))
pop:tag('th')
:wikitext(Icons.Icon({'Township', 'Population', img='Population', type='township', section='Population' }))
-- Upgrades To
for _, biome in ipairs(Township.biomes) do
local upgradesTo = p._GetBuildingIDUpgrade(building.id)
local reqs = p._getTierRequirements(biome.tier)
if upgradesTo ~= nil then
header:tag('th')
table.insert(ret, '\r\n|-\r\n| <b>Upgrades To:</b>')
:wikitext(Icons.Icon({biome.name, type='biome', notext=true, nolink=true}).. '<br/>' .. biome.name)
table.insert(ret, '<br>'..Icons.Icon({upgradesTo.name, type='building'}))
level:tag('td')
local upgrade_cost = p._GetBuildingBaseCost(upgradesTo)
:wikitext(Num.formatnum(reqs.level))
table.insert(ret, '<br>'..upgrade_cost)
pop:tag('td')
:wikitext(Num.formatnum(reqs.population))
end
end
tbl:node(header)
tbl:node(level)
tbl:node(pop)
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


-- Fixed benefits
local trow = tbl:tag('tr')
local benefits = p._GetBuildingBenefits(building)
trow:tag('th')
if benefits ~= nil then
:css('text-align', 'left')
table.insert(ret, '\r\n|-\r\n| <b>Provides:</b> '..benefits)
:attr('data-sort-value', building.name)
:wikitext(Icons.Icon({building.name, type='building'}))
 
for _, biome in ipairs(Township.biomes) do
if buildingBiomes[biome.id] then
trow:tag('td')
:addClass('table-positive')
:wikitext('✓')
else
trow:tag('td')
end
end
end
end
end


-- Production
return tostring(tbl)
local production = p._GetBuildingBaseProduction(building)
end
if production ~= nil then
 
table.insert(ret, '\r\n|-\r\n| <b>Base Production per '..Icons.Icon({'Workers', type='township', notext=true})..':</b><br>')
-- Generates a table contaning each building plus their relevant information
table.insert(ret, production)
function p.getBuildingTable(frame)
local resultPart = {}
 
-- Change structure of biomes data for ease of use later
local biomesByID = {}
for i, biome in ipairs(Township.biomes) do
biomesByID[biome.id] = biome
end
end


-- Modifiers
-- Generate table header
if building.modifiers ~= nil and not Shared.tableIsEmpty(building.modifiers) then
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
table.insert(ret, '\r\n|-\r\n| <b>Modifiers:</b>\r\n'..Constants.getModifiersText(building.modifiers, true))
table.insert(resultPart, '\n|- class="headerRow-0"')
end
table.insert(resultPart, '\n!colspan="2"|Building\n!Requirements\n!Max Built')
table.insert(resultPart, '\n!Biomes\n!Cost\n!Provides')
-- Biomes
 
table.insert(ret, '\r\n|-\r\n| <b>Biomes:</b><br><ul>')
local buildings = p._sortedBuildings(false)
for _, biomeid in ipairs(building.biomes) do
 
local biomename = GameData.getEntityByID(Township.biomes, biomeid).name
for i, building in ipairs(buildings) do
-- Optional hidden bonus/penalty for building
-- Number of rows per building is dictated by number of biomes
local modifier = nil
local buildingName = (building.id == 'melvorF:Statues' and 'Statue of Worship') or building.name
if #building.biomeModifiers > 0 then
local firstRow = true
modifier = GameData.getEntityByProperty(building.biomeModifiers, 'biomeID', biomeid)
local rowCount = Shared.tableCount(building.biomes)
end
local rowSpan = (rowCount > 1 and ' rowspan="' .. rowCount .. '"') or ''
if modifier ~= nil then
local rowSpanOnly = (rowCount > 1 and '|' .. rowSpan) or ''
local color = modifier.value < 0 and 'red' or 'green'
for j, biomeID in ipairs(building.biomes) do
local modifier_value = Shared.numStrWithSign(modifier.value)
local biome = biomesByID[biomeID]
table.insert(ret, '<li style="color:'..color..'"><b>'..biomename..' ('..modifier_value..'%)</b></li>')
if firstRow then
else
table.insert(resultPart, '\n|-')
table.insert(ret, '<li>'..biomename..'</li>')
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|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
end
end
table.insert(resultPart, '\n|}')
-- End
 
table.insert(ret, '\r\n|}')
return table.concat(resultPart)
return table.concat(ret)
end
end


-- Given a resource id, return the job id
-- Builds the table of trader items
-- e.g. melvorF:Bar -> melvorF:Blacksmith
function p.getTraderTable(frame)
function p._GetJobFromResource(resource_id)
local resultPart = {}
local job = GameData.getEntityByProperty(Township.jobs, 'produces', resource_id)
 
return job.id
-- Build table header
end
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
table.insert(resultPart, '\n|- class="headerRow-0"')
table.insert(resultPart, '\n!colspan="2"| Item\n!Description\n!style="min-width:60px"| Cost\n!Requirements')


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


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


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


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


-- Given a building, find the base resource cost
local ret = {}
function p._GetBuildingBaseCost(building, _join)
-- Header
local join = _join ~= nil and _join or ', '
table.insert(ret, '{| class="wikitable infobox"')
local cost = {}
-- Name
for _, resource in ipairs(building.cost) do
table.insert(ret, '\n|-\n! ' .. Icons.getExpansionIcon(building.id) .. building.name)
local resource_data = p._GetResourceByID(resource.id)
-- Icon
table.insert(cost, Icons.Icon({resource_data.name, type='resource', notext=true})..'&nbsp;'..resource.quantity)
table.insert(ret, '\n|-\n|style="text-align:center"| ' .. Icons.Icon({building.name, type='building', size='250', notext=true}))
-- ID
table.insert(ret, '\n|-\n| <b>Building ID:</b> ' .. building.id)
-- Tier
local tier = p._getTierText(building.tier)
table.insert(ret, '\n|-\n| <b>Requirements:</b><br/>' .. tier)
 
-- Upgrades From
table.insert(ret, '\n|-\n| <b>Base Cost:</b>')
local upgradesFrom = p._getBuildingDowngrade(building)
if upgradesFrom ~= nil then
table.insert(ret, '<br/>' .. Icons.Icon({upgradesFrom.name, type='building'}))
end
end
return table.concat(cost, join)
-- Cost
end
--table.insert(ret, '<br/>' .. p._getBuildingGroupedCostText(building))
local function getGroupedText(building, groupFunc)
local biomeGroups = groupFunc(building)
if Shared.tableCount(biomeGroups) == 1 then
-- If only one entry then simply output the cost
return biomeGroups[1].cost
else
-- Otherwise, split by biome group
local resultPart = {}
table.insert(resultPart, '{| class="wikitable" style="text-align:center; margin: 0.25em 0 0 0"')
for i, biomeGroup in ipairs(biomeGroups) do
local biomeText = {}
for j, biomeID in ipairs(biomeGroup.biomeIDs) do
local biome = GameData.getEntityByID(Township.biomes, biomeID)
table.insert(biomeText, Icons.Icon({biome.name, type='biome', notext=true, nolink=true, alt=biome.name}))
end
table.insert(resultPart, '\n|-\n| ' .. table.concat(biomeText, '<br/>'))
table.insert(resultPart, '\n| ' .. biomeGroup.cost)
end
table.insert(resultPart, '\n|}')
return table.concat(resultPart)
end
end
 
table.insert(ret, '\n' .. getGroupedText(building, p._getBuildingGroupedCosts))


-- Gets a resource from id
-- Upgrades To
function p._GetResourceByID(id)
local upgradesTo = p._getBuildingUpgrade(building)
return GameData.getEntityByID(p.resources, id)
if upgradesTo ~= nil then
end
table.insert(ret, '\n|-\n| <b>Upgrades To:</b>')
table.insert(ret, '<br/>' .. Icons.Icon({upgradesTo.name, type='building'}))
table.insert(ret, '\n' .. getGroupedText(upgradesTo, p._getBuildingGroupedCosts))
end


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


-- Returns an upgrade table of a building
-- Returns an upgrade table of a building
function p.GetBuildingUpgradeTable(frame)
function p.getBuildingUpgradeTable(frame)
local buildingname = frame.args ~= nil and frame.args[1] or frame
local buildingname = frame.args ~= nil and frame.args[1] or frame
local building = p._GetBuildingByName(buildingname)
local building = p._getBuildingByName(buildingname)
if building == nil then
return Shared.printError('No building named "' .. buildingname .. '" exists in the data module')
end
 
-- Let's find the base building
-- Let's find the base building
local baseBuilding = building
local baseBuilding = building
while true do
while true do
local previousBuilding = p._GetBuildingDowngrade(baseBuilding)
local previousBuilding = p._getBuildingDowngrade(baseBuilding)
if previousBuilding ~= nil then
if previousBuilding ~= nil then
baseBuilding = previousBuilding
baseBuilding = previousBuilding
Line 534: Line 676:
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 541: Line 683:
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 549: Line 691:
return ''
return ''
end
end
 
local ret = {}
local ret = {}
table.insert(ret, '\r\n== Upgrade Chart ==')
table.insert(ret, '\n== Upgrade Chart ==')
table.insert(ret, '\r\n{| class="wikitable"')
table.insert(ret, '\n{| class="wikitable" style="text-align:center"')
 
-- Name
-- Name
table.insert(ret, '\r\n|- style="text-align:center" \r\n! Name')
table.insert(ret, '\n|-\n!colspan="2"| Name')
for _, building in ipairs(buildingList) do
for _, building in ipairs(buildingList) do
table.insert(ret, '\r\n!'..Icons.Icon({building.name, type='building'}))
table.insert(ret, '\n!' .. Icons.getExpansionIcon(building.id) .. Icons.Icon({building.name, type='building'}))
end
end


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


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


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


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


local FREE_LAND = Township.sectionSize
-- Returns a row containing a task given a title and a task table
-- Gets the cost of the current price of land
function p._getTaskRow(title, task, isDailyTask)
-- Taken from township.js -> Township.getNextSectionCost
local ret = {}
function p._GetLandCost(frame)
 
local nthland = frame.args ~= nil and frame.args[1] or frame
-- If has description, we will need to rowspan the title by 2, and insert a description with colspan 2
return p._GetLandCost(nthland)
local hasDescription = false
if task.description ~= nil then
hasDescription = true
end
local titlespan = hasDescription == true and 'rowspan="2"|' or ''
 
-- Title
table.insert(ret, '\n|-')
table.insert(ret, '\n!' .. titlespan .. title)
-- Description
if hasDescription then
table.insert(ret, '\n|colspan="2"|' .. task.description)
table.insert(ret, '\n|-')
end
-- Requirements
table.insert(ret, '\n|')
-- Determines order of requirements output
local reqOrder = {
["items"] = 10,
["monsters"] = 20,
["monsterWithItems"] = 30,
["skillXP"] = 40,
["buildings"] = 50,
["numPOIs"] = 60,
["numRefinements"] = 70
}
local reqTextPart = {}
 
local function getItemText(itemID)
local item = Items.getItemByID(itemID)
if item == nil then
return Shared.printError('Unknown item: ' .. (itemID or 'nil'))
else
return Icons.Icon({item.name, type='item'})
end
end
local function getMonsterText(monsterID)
local monster = Monsters.getMonsterByID(monsterID)
if monster == nil then
return Shared.printError('Unknown monster: ' .. (monsterID or 'nil'))
else
return Icons.Icon({Monsters.getMonsterName(monster), type='monster'})
end
end
 
for goalType, goalData in pairs(task.goals) do
local typeOrder = reqOrder[goalType] or 0
local goalText = nil
if type(goalData) == 'table' then
-- Goal data is a table
for goalIdx, goalObj in ipairs(goalData) do
if goalType == 'items' then
goalText = Shared.formatnum(goalObj.quantity) .. ' ' .. getItemText(goalObj.id)
elseif goalType == 'monsters' then
goalText = Shared.formatnum(goalObj.quantity) .. ' ' .. getMonsterText(goalObj.id)
elseif goalType == 'monsterWithItems' then
local itemsText = {}
for i, itemID in ipairs(goalObj.itemIDs) do
table.insert(itemsText, getItemText(itemID))
end
goalText = Shared.formatnum(goalObj.quantity) .. ' ' .. getMonsterText(goalObj.monsterID) .. ' with ' .. table.concat(itemsText, ', ') .. ' equipped'
elseif goalType == 'skillXP' then
local skillName = GameData.getSkillData(goalObj.id).name
goalText = Shared.formatnum(goalObj.quantity) .. ' ' .. Icons.Icon({skillName, type='skill'}) .. ' XP'
elseif goalType == 'buildings' then
local buildingName = p._GetBuildingByID(goalObj.id).name
goalText = Shared.formatnum(goalObj.quantity) .. ' ' .. Icons.Icon({buildingName, type='building'})
elseif goalType == 'numPOIs' then
local mapName = GameData.getEntityByID(GameData.skillData.Cartography.worldMaps, goalObj.worldMapID).name
goalText = 'Discover ' .. Shared.formatnum(goalObj.quantity) .. ' Points of Interest in ' .. Icons.Icon({'Cartography', type='skill'}) .. ' world map of ' .. mapName
else
goalText = Shared.printError('Unknown goal type: ' .. (goalType or 'nil'))
end
table.insert(reqTextPart, {
["goalOrder"] = typeOrder,
["subOrder"] = goalIdx,
["text"] = goalText
})
end
else
-- Goal data is another value of some type
if goalType == 'numRefinements' then
goalText = 'Refine dig site maps in ' .. Icons.Icon({'Cartography', type='skill'}) .. ' ' .. Shared.formatnum(goalData) .. ' times'
else
goalText = Shared.printError('Unknown goal type: ' .. (goalType or 'nil'))
end
table.insert(reqTextPart, {
["goalOrder"] = typeOrder,
["subOrder"] = 0,
["text"] = goalText
})
end
end
 
table.sort(reqTextPart,
function(a, b)
if a.goalOrder == b.goalOrder then
return a.subOrder < b.subOrder
else
return a.goalOrder < b.goalOrder
end
end
)
 
local requirements = {}
for i, req in ipairs(reqTextPart) do
table.insert(requirements, req.text)
end
-- We don't check tasks.requirements (so far it's only used to enumerate the Tutorial tasks so you only see 1 at a time)
table.insert(ret, table.concat(requirements, '<br/>'))
-- Rewards
table.insert(ret, '\n|')
local rewards = {}
local rewardsVariableQty = {}
if task.rewards.gp > 0 and not isDailyTask then
table.insert(rewards, Icons.GP(task.rewards.gp))
end
if task.rewards.slayerCoins > 0 then
if isDailyTask then
table.insert(rewardsVariableQty, Icons.SC())
else
table.insert(rewards, Icons.SC(task.rewards.slayerCoins))
end
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
if not (isDailyTask and skill.id == 'melvorD:Township') then
local skillname = GameData.getSkillData(skill.id).name
table.insert(rewards, Shared.formatnum(skill.quantity)..' '..Icons.Icon({skillname, type='skill'})..' XP')
end
end
for _, townshipResource in ipairs(task.rewards.townshipResources) do
local resourcename = p._getResourceByID(townshipResource.id).name
table.insert(rewards, Shared.formatnum(townshipResource.quantity)..' '..Icons.Icon({resourcename, type='resource'}))
end
if not Shared.tableIsEmpty(rewardsVariableQty) then
table.insert(ret, '[[Township#Casual Tasks|Variable]] ' .. table.concat(rewardsVariableQty, ', ') .. '<br/>')
end
table.insert(ret, table.concat(rewards, '<br/>'))
 
-- Unlock requirements, daily task specific
if isDailyTask then
table.insert(ret, '\n|' .. Shop.getRequirementString(task.requirements))
end
return table.concat(ret)
end
end


function p._GetLandCost(nthland)
-- Returns all the tasks of a given category
-- First FREE_LAND plots of land are free
-- TODO: Support casual tasks
if nthland <= FREE_LAND then
function p.getTaskTable(frame)
return 0
local category = frame.args ~= nil and frame.args[1] or frame
local categoryData = GameData.getEntityByID(Township.taskCategories, category)
local taskData, categoryName, isDailyTask = nil, nil, false
if category == 'Daily' then
isDailyTask = true
taskData = Township.casualTasks
categoryName = 'Casual'
elseif categoryData ~= nil then
taskData = Township.tasks
categoryName = categoryData.name
else
return Shared.printError('Invalid task category specified: ' .. (tostring(category) or 'nil'))
end
end
return math.floor(15^(0.0100661358978*(nthland/32) + (nthland/32)^0.42))
end


-- Gets the cost to buy land until you have X amount of available land
local taskcount = 0
-- Currently the max is 2048 land
local ret = {}
function p.GetCumulativeLandCost(frame)
table.insert(ret, '{| class="wikitable lighttable stickyHeader" style="text-align:left"')
local totalLand = frame.args ~= nil and frame.args[1] or frame
table.insert(ret, '\n|- class="headerRow-0"')
return p._GetCumulativeLandCost(totalLand)
table.insert(ret, '\n!Task')
table.insert(ret, '\n!Requirements')
table.insert(ret, '\n!Rewards')
if isDailyTask then
table.insert(ret, '<br/>(In addition to [[Township#Casual Tasks|Variable]] ' .. Icons.GP() .. ' & ' .. Icons.Icon({'Township', type='skill', notext=true}) .. ' XP)')
end
if isDailyTask then
table.insert(ret, '\n!Unlock Requirements')
end
for _, task in ipairs(taskData) do
-- Filter out other categories
if task.category == category then
taskcount = taskcount + 1
local title = categoryName .. ' ' .. taskcount
table.insert(ret, p._getTaskRow(title, task, isDailyTask))
end
end
table.insert(ret, '\n|}')
return table.concat(ret)
end
end


function p._GetCumulativeLandCost(totalLand)
-- Returns a table containing all the tasks that reference an item or monster
local cost = 0
-- e.g. p.getTaskReferenceTable({'Chicken Coop', 'dungeon'})
while totalLand > FREE_LAND do
-- name = item or monster name
cost = cost + p.GetLandCost(totalLand)
-- type = 'item' or 'monster' or 'dungeon'
totalLand = totalLand - 1
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[Monsters.getMonster(referenceName).id] = true
end
return IDs
end
end
return cost
-- For a task, returns where to search for the desired IDs, given the type
end
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


-- Returns a table showing the land cost of a town
-- Build the table
function p.GetLandCostTable()
local ret = {}
local ret = {}
table.insert(ret, '\r\n{| class="wikitable"')
table.insert(ret, '==Tasks==')
table.insert(ret, '\r\n|- style="text-align:center" \r\n! Total Land \r\n! Single Land Cost \r\n! Total Cost')
table.insert(ret, '\n{| class="wikitable" style="text-align:left"')
for i=FREE_LAND,Township.maxTownSize,FREE_LAND do
table.insert(ret, '\n!Task')
table.insert(ret, '\r\n|-\r\n|'..i..'\r\n|'..Icons.GP(p._GetLandCost(i))..'\r\n|'..Icons.GP(p._GetCumulativeLandCost(i)))
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, false))
end
end
table.insert(ret, '\r\n|}')
table.insert(ret, '\n|}')
return table.concat(ret)
return table.concat(ret)
end
end


return p
return p
1,048

edits