Module:BTD6 stats

From Blooncyclopedia, the independent Bloons knowledge base
Revision as of 01:03, 24 April 2026 by Pymonkibot (talk | contribs) (Text replacement - "[[Bloons TD 6 v" to "[[Update history:Bloons TD 6/Version ")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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

-- This code is copyrighted and licensed under the Creative Commons Attribution-NonCommercial-ShareAlike International license, version 4.0. You may reuse and adapt this code for other purposes if:
-- * You give appropriate credit
-- * You are not using this material for commercial purposes
-- * You are releasing it under the same license
-- The subpages of this page are adapted from internal data in Bloons TD 6, which is copyrighted by Ninja Kiwi Limited. Its usage on Blooncyclopedia is believed to be fair use.
-- Further information: https://www.bloonswiki.com/Blooncyclopedia:Copyrights
-- By editing this code, you agree to release your changes under the same terms.

local p = {}
local my_data = {}

local template_names = {
	attacks		="BTD6 attack",
	projectiles	="BTD6 projectile",
	collectables="BTD6 collectable",
	abilities	="BTD6 ability",
	effects		="BTD6 effect",
	buffs		="BTD6 buff",
	zones		="BTD6 zone",
	subtowers	="BTD6 tower"
}

local function parse_beast_table(data, tiers)
	local ret = {"\n{|class='wikitable mw-collapsible mw-collapsed'\n|+style='white-space:nowrap'|Beast Power-dependent stats"}
	
	-- local functions to improve performance
	local tconcat = table.concat
	local sformat = string.format
	local msqrt = math.sqrt
	local mfloor = math.floor
	
	-- -- -- -- -- --
	-- CREATE HEADING
	-- -- -- -- -- --
	local headingUpper = {}
	local upperColspans = {}
	local headingLower = {}
	
	local headingEffects = function(effects)
		for i, v in ipairs(effects["_order"]) do
			local effect = effects[v]
			local amount = 0
	
			if v == "Knockback" and effect.lifespan	then headingLower[#headingLower+1] = "Lifespan" amount = amount + 1 end
			
			if amount > 0 then
				headingUpper[#headingUpper+1] = v
				upperColspans[#upperColspans+1] = amount
			end
		end
	end
	
	local headingProjectiles = function(projectiles)
		for i, v in ipairs(projectiles["_order"]) do
			local projectile = projectiles[v]
			local amount = 0
					
			if projectile["maxPierce"]<1 and projectile["pierce"]<99999	then headingLower[#headingLower+1] = "Pierce" amount = amount + 1 end
			if projectile["damage"]~=nil and projectile["damage"]>0		then headingLower[#headingLower+1] = "Damage" amount = amount + 1 end
			if projectile["damageModifierForStunned"]~=nil				then headingLower[#headingLower+1] = "Damage to stunned" amount = amount + 1 end
			
			if amount > 0 then
				headingUpper[#headingUpper+1] = v
				upperColspans[#upperColspans+1] = amount
			end
		end
	end
	
	local headingAttacks = function(attacks)
		for i, v in ipairs(attacks["_order"]) do
			local attack = attacks[v]
			local amount = 0
					
			if attack.rate and attack.rate < 9999 and not attack.rateMin	then headingLower[#headingLower+1] = "Cooldown" amount = amount + 1 end
			if attack.initialDamage and attack.initialDamage > 0			then headingLower[#headingLower+1] = "Scratch damage" amount = amount + 1 end
			if attack.grapplingDamage and attack.grapplingDamage > 0		then headingLower[#headingLower+1] = "Grappling damage" amount = amount + 1 end
			if attack.thrashingProjectileRate								then headingLower[#headingLower+1] = "Thrash cooldown" amount = amount + 1 end
			
			if amount > 0 then
				headingUpper[#headingUpper+1] = v
				upperColspans[#upperColspans+1] = amount
			end
			
			-- projectiles
			if attack.projectiles then headingProjectiles(attack.projectiles) end
		end
	end
	
	local headingAbilities = function(abils)
		for i, v in ipairs(abils["_order"]) do
			local abil = abils[v]
			local amount = 0
	
			if abil.cooldown	then headingLower[#headingLower+1] = "Cooldown" amount = amount + 1 end
			
			if amount > 0 then
				headingUpper[#headingUpper+1] = v
				upperColspans[#upperColspans+1] = amount
			end
			
			-- projectiles
			if abil.projectiles then headingProjectiles(abil.projectiles) end
		end
	end
	
	local minPower = {
		["_002"] = 3,
		["_003"] = 8,
		["_004"] = 16,
		["_005"] = 36,
		["_020"] = 3,
		["_030"] = 8,
		["_040"] = 16,
		["_050"] = 36,
		["_200"] = 3,
		["_300"] = 8,
		["_400"] = 16,
		["_500"] = 36
	}
	
	local maxPower = {
		["_200"] = 6,
		["_300"] = 24,
		["_400"] = 64,
		["_500"] = 132,
		["_020"] = 6,
		["_030"] = 24,
		["_040"] = 64,
		["_050"] = 132,
		["_002"] = 6,
		["_003"] = 24,
		["_004"] = 64,
		["_005"] = 132
	}
	
	-- insert headings
	if data.speedRangeGyrfalcon > 0 then
		headingLower[#headingLower+1] = "Flight speed"
		headingUpper[#headingUpper+1]= "Beast"
		upperColspans[#upperColspans+1] = 1
	end
	if data.attacks then headingAttacks(data.attacks) end
	if data.abilities then headingAbilities(data.abilities) end
	
	ret[#ret+1] = "!rowspan=2|Power!!rowspan=2|Scale"
	
	for i, v in ipairs(headingUpper) do
		ret[#ret+1] = sformat("!colspan=%i|%s", upperColspans[i], v)
	end
	ret[#ret+1] = "|-\n!" .. table.concat(headingLower, "!!")
	
	-- -- -- -- -- -- --
	-- CALCULATE STATS
	-- -- -- -- -- -- --
	
	-- current scale
	local scale
	
	-- attack cooldown
	local insertRate = function(amt)
		ret[#ret+1] = sformat("| %.4gs", amt - (data.cooldownScaleRange*scale))
	end
	
	-- thrash cooldown
	local insertThrashRate = function(amt, thrashRate)
		ret[#ret+1] = sformat("| %.4gs", (1 - data.cooldownScaleRange*scale/amt) * thrashRate)
	end
	
	-- ability cooldown
	local insertAbilityCooldown = function(amt, mainAttackRate)
		ret[#ret+1] = sformat("|%.4gs", amt * (1 - (data.cooldownScaleRange*scale/(mainAttackRate - (data.cooldownScaleRange*scale)))))
	end
	
	-- damage
	local insertDamage = function(amt)
		ret[#ret+1] = sformat("|%i", amt + mfloor(data.damageRange*scale))
	end
	
	-- dino stun damage
	local insertStunDamage = function(amt)
		ret[#ret+1] = sformat("|%i", amt + mfloor(data.damageRange*scale/data.stunBonusDivideMicroraptor))
	end
	
	-- pierce
	local insertPierce = function(amt)
		ret[#ret+1] = sformat("|%i", amt + mfloor(data.pierceRange*scale))
	end
	
	-- thrash knockback duration
	local insertKnockback = function(amt)
		ret[#ret+1] = sformat("|%.4gs", amt + (data.thrashKnockbackLifetimeRange*scale))
	end
	
	-- bird flight speed
	local insertGyrfalconSpeed = function()
		ret[#ret+1] = sformat("|%.4g units/s", data.speed + (data.speedRangeGyrfalcon*scale))
	end
	
	local parseEffects = function(effects)
		for i, v in ipairs(effects["_order"]) do
			local effect = effects[v]
			
			if v == "Knockback" and effect.duration	then insertKnockback(effect.lifespan) end
		end
	end
	
	local parseProjectiles = function(projectiles)
		for i, v in ipairs(projectiles["_order"]) do
			local projectile = projectiles[v]
					
			if projectile.maxPierce < 1 and projectile.pierce < 99999	then insertPierce(projectile.pierce) end
			if projectile.damage and projectile.damage > 0				then insertDamage(projectile.damage) end
			if projectile.damageModifierForStunned						then insertStunDamage(projectile.damageModifierForStunned) end
			
			if projectile.effects then parseEffects(projectile.effects) end
		end
	end
	
	local parseAttacks = function(attacks)
		for i, v in ipairs(attacks["_order"]) do
			local attack = attacks[v]
			
			if attack.rate and attack.rate < 9999 and not attack.rateMin	then insertRate(attack.rate) end
			if attack.initialDamage and attack.initialDamage > 0			then insertDamage(attack.initialDamage) end
			if attack.grapplingDamage and attack.grapplingDamage > 0		then insertDamage(attack.initialDamage) end
			if attack.thrashingProjectileRate								then insertThrashRate(attack.rate, attack.thrashingProjectileRate) end
			
			-- projectiles
			if attack.projectiles then parseProjectiles(attack.projectiles) end
		end
	end
	
	local parseAbilities = function(abils)
		for i, v in ipairs(abils["_order"]) do
			local abil = abils[v]
			
			local attackRate = data.attacks["Attack"].rate
			
			-- rate
			insertAbilityCooldown(abil.cooldown, attackRate)
			
			if abil.projectiles ~= nil then parseProjectiles(abil.projectiles) end
		end
	end

	for i = minPower[tiers], maxPower[tiers] do
		scale = ((i - minPower[tiers]) / (maxPower[tiers] - minPower[tiers]))
		ret[#ret+1] = sformat("|-\n!%i\n|%.4g%%", i, scale*100)
		
		-- calculations
		if data.speedRangeGyrfalcon > 0 then insertGyrfalconSpeed() end
		if data.attacks		then parseAttacks(data.attacks) end
		if data.abilities	then parseAbilities(data.abilities) end
	end
	
	ret[#ret+1] = "|}"
	
	return table.concat(ret, "\n")
end

function parse_paragon_table(data)
	local ret = {"\n{|class='wikitable mw-collapsible mw-collapsed'\n|+style='white-space:nowrap'|Degree-dependent stats"}
	
	-- local functions to improve performance
	local tinsert = table.insert
	local tconcat = table.concat
	local sformat = string.format
	local msqrt = math.sqrt
	local mfloor = math.floor
	local degree_requirements = {
		0,      2000,   2324,   2666,   3027,   3408,   3808,   4228,   4669,   5131,
		5615,   6121,   6650,   7203,   7779,   8379,   9004,   9654,   10330,  11032,
		11761,  12518,  13302,  14114,  14955,  15825,  16725,  17655,  18616,  19609,
		20633,  21689,  22778,  23900,  25056,  26246,  27471,  28732,  30028,  31360,
		32729,  34135,  35579,  37061,  38582,  40143,  41743,  43383,  45064,  46786,
		48550,  50356,  52205,  54098,  56034,  58014,  60039,  62109,  64225,  66387,
		68596,  70853,  73157,  75509,  77910,  80360,  82860,  85410,  88011,  90664,
		93368,  96124,  98933,  101795, 104711, 107681, 110706, 113787, 116923, 120115,
		123364, 126670, 130034, 133456, 136937, 140478, 144078, 147738, 151459, 155241,
		159085, 162991, 166960, 170993, 175089, 179249, 183474, 187764, 192120, 200000
	}
	
	-- -- -- -- -- --
	-- CREATE HEADING
	-- -- -- -- -- --
	local heading_upper = {}
	local upper_colspans = {}
	local heading_lower = {}
	
	local heading_effects = function(effects)
		for i, v in ipairs(effects["_order"]) do
			local effect = effects[v]
			local amount = 0
	
			if effect["damage"]~=nil					then tinsert(heading_lower, "Damage") amount = amount + 1 end
			if effect["damageModifierForBoss"]~=nil		then tinsert(heading_lower, "Damage to bosses") amount = amount + 1 end
			if effect["damageModifierForMoabs"]~=nil	then tinsert(heading_lower, "Damage to MOAB-Class") amount = amount + 1 end
			if effect["damageModifierForCeramic"]~=nil	then tinsert(heading_lower, "Damage to Ceramic") amount = amount + 1 end
			if effect["damageModifierForCamo"]~=nil		then tinsert(heading_lower, "Damage to Camo") amount = amount + 1 end
			if effect["damageModifierForStunned"]~=nil	then tinsert(heading_lower, "Damage to stunned") amount = amount + 1 end
			if effect["damageModifierForStickied"]~=nil	then tinsert(heading_lower, "Damage to stickied") amount = amount + 1 end
			
			if amount > 0 then
				tinsert(heading_upper, v)
				tinsert(upper_colspans, amount)
			end
		end
	end
	
	local heading_projectiles = function(projectiles)
		for i, v in ipairs(projectiles["_order"]) do
			local projectile = projectiles[v]
			local amount = 0
					
			if projectile["maxPierce"]<1 and projectile["pierce"]<99999	then tinsert(heading_lower, "Pierce") amount = amount + 1 end
			if projectile["damage"]~=nil and projectile["damage"]>0		then tinsert(heading_lower, "Damage") amount = amount + 1 end
			if projectile["damageModifierForBoss"]~=nil					then tinsert(heading_lower, "Damage to bosses") amount = amount + 1 end
			if projectile["damageModifierForMoabs"]~=nil				then tinsert(heading_lower, "Damage to MOAB-Class") amount = amount + 1 end
			if projectile["damageModifierForCeramic"]~=nil				then tinsert(heading_lower, "Damage to Ceramic") amount = amount + 1 end
			if projectile["damageModifierForCamo"]~=nil					then tinsert(heading_lower, "Damage to Camo") amount = amount + 1 end
			if projectile["damageModifierForStunned"]~=nil				then tinsert(heading_lower, "Damage to stunned") amount = amount + 1 end
			if projectile["damageModifierForStickied"]~=nil				then tinsert(heading_lower, "Damage to stickied") amount = amount + 1 end
			
			if amount > 0 then
				tinsert(heading_upper, v)
				tinsert(upper_colspans, amount)
			end
			if projectile["effects"] ~= nil then heading_effects(projectile["effects"]) end
		end
	end
	
	local heading_attacks = function(attacks)
		for i, v in ipairs(attacks["_order"]) do
			local attack = attacks[v]
			
			-- rate
			if attack["rate"] ~= nil and attack["rate"] < 9999 and attack["rateMin"] == nil then
				tinsert(heading_lower, "Cooldown")
				tinsert(heading_upper, v)
				tinsert(upper_colspans, 1)
			end
			
			-- projectiles
			if attack["projectiles"] ~= nil then heading_projectiles(attack["projectiles"]) end
		end
	end
	
	local heading_abilities = function(abils)
		for i, v in ipairs(abils["_order"]) do
			local abil = abils[v]
			
			-- rate
			tinsert(heading_lower, "Cooldown")
			tinsert(heading_upper, v)
			tinsert(upper_colspans, 1)
			
			if abil["projectiles"] ~= nil then heading_projectiles(abil["projectiles"]) end
			if abil["attacks"] ~= nil then heading_attacks(abil["attacks"]) end
		end
	end
	
	-- insert headings
	if data["projectiles"] ~= nil then heading_attacks(data["projectiles"]) end
	if data["attacks"] ~= nil then heading_attacks(data["attacks"]) end
	if data["abilities"] ~= nil then heading_abilities(data["abilities"]) end
	if data["zones"] ~= nil then heading_effects(data["zones"]) end
	
	tinsert(ret, "!rowspan=2|Degree\n!rowspan=2|Power\n!rowspan=2|Boss multiplier")
	
	for i, v in ipairs(heading_upper) do
		tinsert(ret, sformat("!colspan=%i|%s", upper_colspans[i], v))
	end
	tinsert(ret, "|-\n!" .. table.concat(heading_lower, "!!"))
	
	-- -- -- -- -- -- --
	-- CALCULATE STATS
	-- -- -- -- -- -- --
	
	-- current degree
	local d = 1
	
	-- attack cooldowns and ability cooldowns
	local insert_rate = function(amt)
		tinsert(ret, sformat("|%.4gs", amt / (1 + (0.01 * msqrt((d-1)*50)))))
	end
	
	-- damage
	local insert_damage = function(amt)
		if d == 100 then	tinsert(ret, sformat("|%.4g", amt * 2 + 10))
		else				tinsert(ret, sformat("|%.4g", amt * (1 + (d-1) * 0.01) + mfloor((d-1)/10))) end
	end
	
	-- pierce
	local insert_pierce = function(amt)
		if d == 100 then	tinsert(ret, sformat("|%.4g", amt * 2 + 10))
		else				tinsert(ret, sformat("|%.4g", mfloor(amt * (1 + (d-1) * 0.01)) + (d-1)/10)) end
	end
	
	-- damage modifiers
	local insert_damage_mod = function(amt)
		if d == 100 then	tinsert(ret, sformat("| +%.4g", amt * 2 + 10))
		else				tinsert(ret, sformat("| +%.4g", amt * (1 + (d-1) * 0.01))) end
	end
	
	local parse_effects = function(effects)
		for i, v in ipairs(effects["_order"]) do
			local effect = effects[v]
	
			if effect["damage"]~=nil					then insert_damage(effect["damage"]) end
			if effect["damageModifierForBoss"]~=nil		then insert_damage_mod(effect["damageModifierForBoss"]) end
			if effect["damageModifierForMoabs"]~=nil	then insert_damage_mod(effect["damageModifierForMoabs"]) end
			if effect["damageModifierForCeramic"]~=nil	then insert_damage_mod(effect["damageModifierForCeramic"]) end
			if effect["damageModifierForCamo"]~=nil		then insert_damage_mod(effect["damageModifierForCamo"]) end
			if effect["damageModifierForStunned"]~=nil	then insert_damage_mod(effect["damageModifierForStunned"]) end
			if effect["damageModifierForStickied"]~=nil	then insert_damage_mod(effect["damageModifierForStickied"]) end
		end
	end
	
	local parse_projectiles = function(projectiles)
		for i, v in ipairs(projectiles["_order"]) do
			local projectile = projectiles[v]
					
			if projectile["maxPierce"]<1 and projectile["pierce"]<99999	then insert_pierce(projectile["pierce"]) end
			if projectile["damage"]~=nil and projectile["damage"]>0		then insert_damage(projectile["damage"]) end
			if projectile["damageModifierForBoss"]~=nil					then insert_damage_mod(projectile["damageModifierForBoss"]) end
			if projectile["damageModifierForMoabs"]~=nil				then insert_damage_mod(projectile["damageModifierForMoabs"]) end
			if projectile["damageModifierForCeramic"]~=nil				then insert_damage_mod(projectile["damageModifierForCeramic"]) end
			if projectile["damageModifierForCamo"]~=nil					then insert_damage_mod(projectile["damageModifierForCamo"]) end
			if projectile["damageModifierForStunned"]~=nil				then insert_damage_mod(projectile["damageModifierForStunned"]) end
			if projectile["damageModifierForStickied"]~=nil				then insert_damage_mod(projectile["damageModifierForStickied"]) end
			
			if projectile["effects"] ~= nil then parse_effects(projectile["effects"]) end
		end
	end
	
	local parse_attacks = function(attacks)
		for i, v in ipairs(attacks["_order"]) do
			local attack = attacks[v]
			
			-- rate
			if attack["rate"] ~= nil and attack["rate"] < 9999 and attack["rateMin"] == nil then
				insert_rate(attack["rate"])
			end
			
			-- projectiles
			if attack["projectiles"] ~= nil then parse_projectiles(attack["projectiles"]) end
		end
	end
	
	local parse_abilities = function(abils)
		for i, v in ipairs(abils["_order"]) do
			local abil = abils[v]
			
			-- rate
			insert_rate(abil["cooldown"])
			
			if abil["projectiles"] ~= nil then parse_projectiles(abil["projectiles"]) end
			if abil["attacks"] ~= nil then parse_attacks(abil["attacks"]) end
		end
	end
	
	-- degree loop
	while d <= 100 do
		-- new row
		tinsert(ret, sformat("|-\n!%i\n|%i", d, degree_requirements[d]))
		
		-- boss damage mult
		if d < 20 then tinsert(ret, "|×1.0")
		elseif d < 40 then tinsert(ret, "|×1.25")
		elseif d < 60 then tinsert(ret, "|×1.5")
		elseif d < 80 then tinsert(ret, "|×1.75")
		elseif d ~= 100 then tinsert(ret, "|×2.0")
		else tinsert(ret, "|×2.25") end
		
		-- calculations
		if data["projectiles"] ~= nil then parse_attacks(data["projectiles"]) end
		if data["attacks"] ~= nil then parse_attacks(data["attacks"]) end
		if data["abilities"] ~= nil then parse_abilities(data["abilities"]) end
		if data["zones"] ~= nil then parse_effects(data["zones"]) end
		
		d = d + 1
	end
	
	tinsert(ret, "|}")
	
	return table.concat(ret, "\n")
end

function parse_tower_base(frame, data, header, paragon)
	local template = {}
	
	-- local functions to improve performance
	local tinsert = table.insert
	local tconcat = table.concat
	local sformat = string.format
	local ssub = string.sub
	
	-- search through tables
	for k, v in pairs(data) do
		if type(v) == "table" and ssub(k, 1, 1) ~= "_" then
			template[k] = {}
			
			-- iterate through attacks/abilities/subtowers
			for _, i in ipairs(v["_order"]) do
				if k == "subtowers" then tinsert(template[k], sformat(header, i)) end
				
				-- parse sub-table of table
				local stats = parse_stats_base(frame, i, v[i], template_names[k])
				if k == "subtowers" and paragon then stats = stats .. parse_paragon_table(v[i]) end
				tinsert(template[k], stats)
			end
			
			template[k] = tconcat(template[k])
		
		elseif type(v) ~= "table" then
			template[k] = data[k]
		end
	end
	
	if paragon then template["paragon stats"] = parse_paragon_table(data) end
	
	return frame:expandTemplate{title = "BTD6 tower", args = template}
end

function parse_stats_base(frame, name, data, template_name)
	local template = {name = name}
	
	-- local functions to improve performance
	local tinsert = table.insert
	local tconcat = table.concat
	
	-- search through tables
	for k, v in pairs(data) do
		if type(v) == "table" then
			template[k] = {}
			
			-- iterate through attacks/abilities/subtowers
			for _, i in ipairs(v["_order"]) do
				-- parse sub-table of table
				tinsert(template[k], parse_stats_base(frame, i, v[i], template_names[k]))
			end
			
			template[k] = tconcat(template[k])
		
		else
			template[k] = v
		end
	end
	
	return frame:expandTemplate{title = template_name, args = template}
end

function parse_tower(frame, new_data, prev_data, header)
	local template = {}
	
	-- local functions to improve performance
	local tinsert = table.insert
	local tconcat = table.concat
	local sformat = string.format
	local ssub = string.sub
	
	-- search through tables
	for k, v in pairs(new_data) do
		if type(v) == "table" and ssub(k, 1, 1) ~= "_" then
			template[k] = {}
			if prev_data[k] == nil then prev_data[k] = {} end
			
			-- iterate through attacks/abilities/subtowers
			for _, i in ipairs(v["_order"]) do
				if k == "subtowers" then tinsert(template[k], sformat(header, i)) end
				if prev_data[k][i] == nil then prev_data[k][i] = {} end
				
				-- parse sub-table of table
				local stats = parse_stats(frame, i, v[i], prev_data[k][i], template_names[k])
				tinsert(template[k], stats)
			end
			
			template[k] = tconcat(template[k])
		
		elseif type(v) ~= "table" then
			-- if previous value exists, make it a comparison
			if prev_data[k] ~= nil then
				template[k] = prev_data[k]
				template[k .. " after"] = v
			else
				template[k] = v
			end
			
			-- update previous value
			prev_data[k] = v
		end
	end
	
	return frame:expandTemplate{title = "BTD6 tower", args = template}
end

function parse_stats(frame, name, new_data, prev_data, template_name)
	local template = {name = name}
	
	-- local functions to improve performance
	local tinsert = table.insert
	local tconcat = table.concat
	
	-- search through tables
	for k, v in pairs(new_data) do
		if type(v) == "table" then
			template[k] = {}
			
			if prev_data[k] == nil then prev_data[k] = {} end
			
			-- iterate through attacks/abilities/subtowers
			for _, i in ipairs(v["_order"]) do
				if prev_data[k][i] == nil then prev_data[k][i] = {} end
				-- parse sub-table of table
				tinsert(template[k], parse_stats(frame, i, v[i], prev_data[k][i], template_names[k]))
			end
			
			template[k] = tconcat(template[k])
		
		else
			-- if previous value exists, make it a comparison
			if prev_data[k] ~= nil then
				template[k] = prev_data[k]
				template[k .. " after"] = v
			else
				template[k] = v
			end
			
			-- update previous value
			prev_data[k] = v
		end
	end
	
	return frame:expandTemplate{title = template_name, args = template}
end

function parse_tower_full(frame, new_data, prev_data, header)
	local template = {}
	
	-- local functions to improve performance
	local tinsert = table.insert
	local tconcat = table.concat
	local sformat = string.format
	local ssub = string.sub
	
	-- search through tables
	for k, v in pairs(new_data) do
		if type(v) == "table" and ssub(k, 1, 1) ~= "_" then
			if template[k] == nil then template[k] = {} end
			if prev_data[k] == nil then prev_data[k] = {} end
			if prev_data[k]["_order"] == nil then
				prev_data[k]["_order"] = {}
				prev_data[k]["_keys"] = {}
			end
			
			-- iterate through attacks/abilities/subtowers
			for _, i in ipairs(v["_order"]) do
				if prev_data[k][i] == nil then prev_data[k][i] = {} end
				if prev_data[k]["_keys"][i] == nil then
					tinsert(prev_data[k]["_order"], i)
					prev_data[k]["_keys"][i] = true
				end
				-- parse sub-table of table
				parse_stats_full(frame, i, v[i], prev_data[k][i], template_names[k])
			end
		
		elseif type(v) ~= "table" then
			-- update previous value
			prev_data[k] = v
		end
	end
	
	-- update prev_data first
	for k, v in pairs(prev_data) do
		if k == "attacks" or k == "abilities" or k == "subtowers" or k == "zones" then
			template[k] = {}
			
			-- iterate through attacks/abilities/subtowers
			for _, i in ipairs(v["_order"]) do
				if k == "subtowers" then tinsert(template[k], sformat(header, i)) end
				tinsert(template[k], parse_stats_full(frame, i, nil, v[i], template_names[k]))
			end
			
			template[k] = tconcat(template[k])
		
		elseif type(v) ~= "table" then
			-- update previous value
			template[k] = v
		end
	end
	
	return frame:expandTemplate{title = "BTD6 tower", args = template}
end

function parse_stats_full(frame, name, new_data, prev_data, template_name)
	local template = {name = name}
	
	-- local functions to improve performance
	local tinsert = table.insert
	local tconcat = table.concat
	
	-- update prev_data first
	if new_data ~= nil then
		for k, v in pairs(new_data) do
			if type(v) == "table" then
				if prev_data[k] == nil then prev_data[k] = {} end
			if prev_data[k]["_order"] == nil then
				prev_data[k]["_order"] = {}
				prev_data[k]["_keys"] = {}
				end
				
				-- iterate through attacks/abilities/subtowers
				for _, i in ipairs(v["_order"]) do
					if prev_data[k][i] == nil then prev_data[k][i] = {} end
					if prev_data[k]["_keys"][i] == nil then
						tinsert(prev_data[k]["_order"], i)
						prev_data[k]["_keys"][i] = true
					end	
					-- parse sub-table of table
					parse_stats_full(frame, i, v[i], prev_data[k][i], template_names[k])
				end
			
			else
				prev_data[k] = v
			end
		end
	end
	
	for k, v in pairs(prev_data) do
		if type(v) == "table" then
			template[k] = {}
			
			-- iterate through attacks/abilities/subtowers
			for _, i in ipairs(v["_order"]) do
				if prev_data[k][i] == nil then prev_data[k][i] = {} end
				-- parse sub-table of table
				tinsert(template[k], parse_stats_full(frame, i, nil, v[i], template_names[k]))
			end
			
			template[k] = tconcat(template[k])
		
		else
			template[k] = v
		end
	end
	
	return frame:expandTemplate{title = template_name, args = template}
end

-- stats of a single tower
function p.base_stats(frame)
	local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
	local header_level = frame.args["header level"] ~= nil and frame.args["header level"] or "3"
	local header = "<h" .. header_level .. ">%s</h" .. header_level .. ">"
	
	if frame.args[2] == nil then
		if data["_000"] ~= nil then
			return parse_tower_base(frame, data["_000"], header, false)
		else
			return parse_tower_base(frame, data, header, false)
		end
	else
		return parse_tower_base(frame, data["_" .. frame.args[2]], header, false)
	end
end

-- stats of a paragon
function p.paragon(frame)
	local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
	
	local out = parse_tower_base(frame, data, "<h3>%s</h3>", true)
	
	return string.format("<div class='hatnote'>This data was last updated for: [[Update history:Bloons TD 6/Version %s|version %s]]</div>", data["_last_updated"], data["_last_updated"]) .. out
end

-- stat changes of a tower
function p.tower_upgrade_stats(frame)
	local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
	
	local ret = {}
	
	-- local functions to improve performance
	local tinsert = table.insert
	local sformat = string.format
	local ssub = string.sub
	
	local base_data = data[frame.args[2]]
	
	local upgrade_crosspaths = {
		["_000"] = {{}},
		["_100"] = {{"_110"}, {"_101"}},
		["_200"] = {{"_210", "_220"}, {"_201", "_202"}},
		["_010"] = {{"_110"}, {"_011"}},
		["_020"] = {{"_120", "_220"}, {"_021", "_022"}},
		["_001"] = {{"_101"}, {"_011"}},
		["_002"] = {{"_102", "_202"}, {"_012", "_022"}},
		["_300"] = {{"_310", "_320"}, {"_301", "_302"}},
		["_400"] = {{"_410", "_420"}, {"_401", "_402"}},
		["_500"] = {{"_510", "_520"}, {"_501", "_502"}},
		["_030"] = {{"_130", "_230"}, {"_031", "_032"}},
		["_040"] = {{"_140", "_240"}, {"_041", "_042"}},
		["_050"] = {{"_150", "_250"}, {"_051", "_052"}},
		["_003"] = {{"_103", "_203"}, {"_013", "_023"}},
		["_004"] = {{"_104", "_204"}, {"_014", "_024"}},
		["_005"] = {{"_105", "_205"}, {"_015", "_025"}}
	}
	
	tinsert(ret, sformat("<div class='hatnote'>This data was last updated for: [[Update history:Bloons TD 6/Version %s|version %s]]</div>", data["_last_updated"], data["_last_updated"]))
	
	for i, v in ipairs(upgrade_crosspaths[frame.args[2]]) do
		my_data = {}
		local result = parse_tower(frame, base_data, my_data, "<h3>%s</h3>", false)
		
		-- inject beast handler stats table
		if frame.args[1] == "Beast Handler" and not (frame.args[2] == "_000" or frame.args[2] == "_100" or frame.args[2] == "_010" or frame.args[2] == "_001") then
			result = result .. parse_beast_table(base_data.subtowers.Beast, frame.args[2])
		end
		
		if i == 1 then tinsert(ret, result) end
		for ii, vv in ipairs(v) do
			if base_data[vv] ~= nil then
				local crosspath = parse_tower(frame, base_data[vv], my_data, "<h4>%s</h4>", false)
				if mw.text.trim(crosspath) ~= [[
]] then
					tinsert(ret, sformat("<h3>%s-%s-%s</h3>", ssub(vv, 2, 2), ssub(vv, 3, 3), ssub(vv, 4, 4)))
					tinsert(ret, crosspath)
				end
			end
		end
	end
	
	return table.concat(ret, "\n")
end

-- stat changes of a hero
function p.hero_level_changes(frame)
	local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
	
	local ret = {}
	
	-- local functions to improve performance
	local tinsert = table.insert
	local sformat = string.format
	
	-- calculate base tower stats for later comparison
	parse_tower(frame, data, my_data, "<h3>%s</h3>", false)
	
	for i = 2, 20 do
		tinsert(ret, sformat("===Level %i===", i))
		tinsert(ret, parse_tower(frame, data["_" .. i], my_data, "<h4>%s</h4>", false))
	end
	
	return table.concat(ret, "\n")
end

-- stat changes of a hero
function p.hero_level_stats(frame)
	local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
	
	local ret = {}
	
	-- local functions to improve performance
	local tinsert = table.insert
	local sformat = string.format
	
	-- calculate base tower stats for later comparison
	parse_tower(frame, data, my_data, "<h3>%s</h3>", false)
	
	for i = 2, 20 do
		tinsert(ret, sformat("===Level %i===", i))
		tinsert(ret, parse_tower_full(frame, data["_" .. i], my_data, "<h4>%s</h4>", false))
	end
	
	return table.concat(ret, "\n")
end

function p.last_updated(frame)
	local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
	return string.format("<div class='hatnote'>This data was last updated for: [[Update history:Bloons TD 6/Version %s|version %s]]</div>", data["_last_updated"], data["_last_updated"])
end

return p