Module:BTD6 stats: Difference between revisions

From Blooncyclopedia, the independent Bloons knowledge base
Jump to navigation Jump to search
mNo edit summary
big refactor, might break a few things
Tag: Reverted
Line 1: Line 1:
-- 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:
--[[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 give appropriate credit
-- * You are not using this material for commercial purposes
* You are not using this material for commercial purposes
-- * You are releasing it under the same license
* 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.
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
Further information: https://www.bloonswiki.com/Blooncyclopedia:Copyrights
-- By editing this code, you agree to release your changes under the same terms.
By editing this code, you agree to release your changes under the same terms.]]


local p = {}
local p = {}
local my_data = {}
local prev_data = {}


-- adds the "this data was last updated for" header
function last_updated(version)
return string.format("<div class='hatnote'>This data was last updated for: [[Bloons TD 6 v%s|version %s]]</div>", version, version)
end
-- table of template names for each type of object
local template_names = {
local template_names = {
attacks ="BTD6 attack",
attacks ="BTD6 attack",
Line 21: Line 27:
}
}


function parse_paragon_table(data)
function paragon_table(data)
local ret = {"\n{|class='wikitable mw-collapsible mw-collapsed'\n|+style='white-space:nowrap'|Degree-dependent stats"}
local ret = {"\n{|class='wikitable mw-collapsible mw-collapsed'\n|+style='white-space:nowrap'|Degree-dependent stats"}
Line 30: Line 36:
local msqrt = math.sqrt
local msqrt = math.sqrt
local mfloor = math.floor
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
}
-- -- -- -- -- --
-- -- -- -- -- --
Line 127: Line 121:
if data["abilities"] ~= nil then heading_abilities(data["abilities"]) end
if data["abilities"] ~= nil then heading_abilities(data["abilities"]) end
tinsert(ret, "!rowspan=2|Degree\n!rowspan=2|Power\n!rowspan=2|Boss multiplier")
tinsert(ret, "!rowspan=2|Power\n!rowspan=2|Boss multiplier")
for i, v in ipairs(heading_upper) do
for i, v in ipairs(heading_upper) do
Line 224: Line 218:
while d <= 100 do
while d <= 100 do
-- new row
-- new row
tinsert(ret, sformat("|-\n!%i\n|%i", d, degree_requirements[d]))
tinsert(ret, sformat("|-\n!%i", d))
-- boss damage mult
-- boss damage mult
Line 256: Line 250:
local ssub = string.sub
local ssub = string.sub
-- search through tables
-- search through variables
for k, v in pairs(data) do
for k, v in pairs(data) do
-- subtables (attacks, abilities, etc)
if type(v) == "table" and ssub(k, 1, 1) ~= "_" then
if type(v) == "table" and ssub(k, 1, 1) ~= "_" then
template[k] = {}
local subtables = {}
-- iterate through attacks/abilities/subtowers
for i, subtable_name in ipairs(v["_order"]) do
for _, i in ipairs(v["_order"]) do
-- insert header for subtowers
if k == "subtowers" then tinsert(template[k], sformat(header, i)) end
if k == "subtowers" then tinsert(subtables, sformat(header, subtable_name)) end
-- parse subtable of table
local stats = parse_stats_base(frame, subtable_name, v)
-- parse sub-table of table
-- append stats subtable to output
local stats = parse_stats_base(frame, i, v[i], template_names[k])
tinsert(subtables, frame:expandTemplate{title = template_names[k], args = stats})
if k == "subtowers" and paragon then stats = stats .. parse_paragon_table(v[i]) end
if k == "subtowers" and paragon then tinsert(subtables, paragon_table(v[subtable_name])) end
tinsert(template[k], stats)
end
end
template[k] = tconcat(template[k])
template[k] = tconcat(subtables)
-- other variables (rate, pierce, etc)
elseif type(v) ~= "table" then
elseif type(v) ~= "table" then
template[k] = data[k]
template[k] = data[k]
Line 278: Line 276:
end
end
if paragon then template["paragon stats"] = parse_paragon_table(data) end
-- append paragon table to output
if paragon then template["paragon stats"] = paragon_table(data) end
return frame:expandTemplate{title = "BTD6 tower", args = template}
return frame:expandTemplate{title = "BTD6 tower", args = template}
end
end


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


function parse_tower(frame, new_data, prev_data, header)
function parse_tower_changes(frame, base, changes, header)
local template = {}
local template = {}
Line 320: Line 319:
local ssub = string.sub
local ssub = string.sub
-- search through tables
-- search through variables
for k, v in pairs(new_data) do
for k, v in pairs(changes) do
-- subtables (attacks, abilities, etc)
if type(v) == "table" and ssub(k, 1, 1) ~= "_" then
if type(v) == "table" and ssub(k, 1, 1) ~= "_" then
template[k] = {}
local subtables = {}
if prev_data[k] == nil then prev_data[k] = {} end
if prev_data[k] == nil then prev_data[k] = {} end
-- iterate through attacks/abilities/subtowers
for i, subtable_name in ipairs(v["_order"]) do
for _, i in ipairs(v["_order"]) do
if prev_data[k][subtable_name] == nil then prev_data[k][subtable_name] = {} end
if k == "subtowers" then tinsert(template[k], sformat(header, i)) end
-- insert header for subtowers
if prev_data[k][i] == nil then prev_data[k][i] = {} end
if k == "subtowers" then tinsert(subtables, sformat(header, subtable_name)) end
-- parse sub-table of table
-- parse subtable of table
local stats = parse_stats(frame, i, v[i], prev_data[k][i], template_names[k])
-- if neither the base tower nor a previous stat change has this subtable, then treat it as new
tinsert(template[k], stats)
if (base[k] == nil or base[k][subtable_name] == nil) and next(prev_data[k][subtable_name]) == nil then
local stats = parse_stats_base(frame, subtable_name, v)
prev_data[k][subtable_name] = stats
tinsert(subtables, frame:expandTemplate{title = template_names[k], args = stats})
else
local stats = parse_stats_changes(frame, subtable_name, base[k][subtable_name], v[subtable_name], prev_data[k][subtable_name])
tinsert(subtables, frame:expandTemplate{title = template_names[k], args = stats})
end
end
end
template[k] = tconcat(template[k])
template[k] = tconcat(subtables)
-- other variables (rate, pierce, etc)
elseif type(v) ~= "table" then
elseif type(v) ~= "table" then
-- if previous value exists, make it a comparison
-- stat comparison
if prev_data[k] ~= nil then
-- if this stat changed before, use that, otherwise use the base tower's stat
template[k] = prev_data[k]
if prev_data[k] ~= nil then template[k] = prev_data[k]
template[k .. " after"] = v
else template[k] = base[k] end
else
template[k] = v
end
-- update previous value
template[k .. " after"] = v
prev_data[k] = v
prev_data[k] = v
end
end
Line 355: Line 360:
end
end


function parse_stats(frame, name, new_data, prev_data, template_name)
function parse_stats_changes(frame, p_name, base, changes, prev)
local template = {name = name}
local template = {name = p_name}
-- local functions to improve performance
-- local functions to improve performance
Line 363: Line 368:
-- search through tables
-- search through tables
for k, v in pairs(new_data) do
for k, v in pairs(changes) do
-- subtables (attacks, abilities, etc)
if type(v) == "table" then
if type(v) == "table" then
template[k] = {}
local subtables = {}
if prev[k] == nil then prev[k] = {} end
if prev_data[k] == nil then prev_data[k] = {} end
for i, subtable_name in ipairs(v["_order"]) do
if prev[k][subtable_name] == nil then prev[k][subtable_name] = {} end
-- iterate through attacks/abilities/subtowers
for _, i in ipairs(v["_order"]) do
-- parse subtable of table
if prev_data[k][i] == nil then prev_data[k][i] = {} end
-- if neither the base tower nor a previous stat change has this subtable, then treat it as new
-- parse sub-table of table
if (base == nil or base[k] == nil or base[k][subtable_name] == nil) and next(prev[k][subtable_name]) == nil then
tinsert(template[k], parse_stats(frame, i, v[i], prev_data[k][i], template_names[k]))
local stats = parse_stats_base(frame, subtable_name, v)
prev[k][subtable_name] = stats
tinsert(subtables, frame:expandTemplate{title = template_names[k], args = stats})
else
local stats = parse_stats_changes(frame, subtable_name, base[k][subtable_name], v[subtable_name], prev[k][subtable_name])
tinsert(subtables, frame:expandTemplate{title = template_names[k], args = stats})
end
end
end
template[k] = tconcat(template[k])
template[k] = tconcat(subtables)
-- other variables (rate, pierce, etc)
else
else
-- if previous value exists, make it a comparison
-- stat comparison
if prev_data[k] ~= nil then
-- if this stat changed before, use that, otherwise use the base tower's stat
template[k] = prev_data[k]
if prev[k] ~= nil then template[k] = prev[k]
template[k .. " after"] = v
else template[k] = base[k] end
else
template[k] = v
end
-- update previous value
template[k .. " after"] = v
prev_data[k] = v
prev[k] = v
end
end
end
end
return frame:expandTemplate{title = template_name, args = template}
return template
end
end


Line 421: Line 432:
prev_data[k]["_keys"][i] = true
prev_data[k]["_keys"][i] = true
end
end
-- parse sub-table of table
-- parse subtable of table
parse_stats_full(frame, i, v[i], prev_data[k][i], template_names[k])
parse_stats_full(frame, i, v[i], prev_data[k][i], template_names[k])
end
end
Line 477: Line 488:
prev_data[k]["_keys"][i] = true
prev_data[k]["_keys"][i] = true
end
end
-- parse sub-table of table
-- parse subtable of table
parse_stats_full(frame, i, v[i], prev_data[k][i], template_names[k])
parse_stats_full(frame, i, v[i], prev_data[k][i], template_names[k])
end
end
Line 494: Line 505:
for _, i in ipairs(v["_order"]) do
for _, i in ipairs(v["_order"]) do
if prev_data[k][i] == nil then prev_data[k][i] = {} end
if prev_data[k][i] == nil then prev_data[k][i] = {} end
-- parse sub-table of table
-- parse subtable of table
tinsert(template[k], parse_stats_full(frame, i, nil, v[i], template_names[k]))
tinsert(template[k], parse_stats_full(frame, i, nil, v[i], template_names[k]))
end
end
Line 509: Line 520:


-- stats of a single tower
-- stats of a single tower
function p.base_stats(frame)
function p.tower_base(frame)
local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
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_level = frame.args["header level"] ~= nil and frame.args["header level"] or "3"
Line 529: Line 540:
local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
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 last_updated(data["_last_updated"]) .. parse_tower_base(frame, data, "<h3>%s</h3>", false)
return string.format("<div class='hatnote'>This data was last updated for: [[Bloons TD 6 v%s|version %s]]</div>", data["_last_updated"], data["_last_updated"]) .. out
end
end


-- stat changes of a tower
-- stat changes of a tower
function p.tower_upgrade_stats(frame)
function p.tower_upgrade_changes(frame)
local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
Line 544: Line 553:
local sformat = string.format
local sformat = string.format
local ssub = string.sub
local ssub = string.sub
local base_data = data[frame.args[2]]
local upgrade_crosspaths = {
local upgrade_crosspaths = {
Line 566: Line 573:
}
}
tinsert(ret, sformat("<div class='hatnote'>This data was last updated for: [[Bloons TD 6 v%s|version %s]]</div>", data["_last_updated"], data["_last_updated"]))
-- data for this upgrade + crosspaths
local base_data = data[frame.args[2]]
tinsert(ret, last_updated(data["_last_updated"], data["_last_updated"]))
tinsert(ret, parse_tower_base(frame, base_data, "<h3>%s</h3>", false))
for i, v in ipairs(upgrade_crosspaths[frame.args[2]]) do
for i, v in ipairs(upgrade_crosspaths[frame.args[2]]) do
local result = parse_tower(frame, base_data, my_data, "<h3>%s</h3>", false)
my_data = {}
if i == 1 then tinsert(ret, result) end
for ii, vv in ipairs(v) do
for ii, vv in ipairs(v) do
if base_data[vv] ~= nil then
if base_data[vv] ~= nil then
tinsert(ret, sformat("<h3>%s-%s-%s</h3>", ssub(vv, 2, 2), ssub(vv, 3, 3), ssub(vv, 4, 4)))
tinsert(ret, sformat("<h3>%s-%s-%s</h3>", ssub(vv, 2, 2), ssub(vv, 3, 3), ssub(vv, 4, 4)))
tinsert(ret, parse_tower(frame, base_data[vv], my_data, "<h4>%s</h4>", false))
tinsert(ret, parse_tower_changes(frame, base_data, base_data[vv], "<h4>%s</h4>"))
end
end
end
end
Line 591: Line 601:
local tinsert = table.insert
local tinsert = table.insert
local sformat = string.format
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
for i = 2, 20 do
tinsert(ret, sformat("===Level %i===", i))
tinsert(ret, sformat("===Level %i===", i))
tinsert(ret, parse_tower(frame, data["_" .. i], my_data, "<h4>%s</h4>", false))
tinsert(ret, parse_tower_changes(frame, data, data["_" .. i], "<h4>%s</h4>"))
end
end
Line 603: Line 610:
end
end


-- stat changes of a hero
-- full stats of a hero for each level
function p.hero_level_stats(frame)
function p.hero_level_stats(frame)
local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
Line 624: Line 631:
end
end


-- get "last updated" header
function p.last_updated(frame)
function p.last_updated(frame)
local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
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: [[Bloons TD 6 v%s|version %s]]</div>", data["_last_updated"], data["_last_updated"])
return last_updated(data["_last_modified"])
end
end


return p
return p

Revision as of 09:51, 6 March 2025

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 prev_data = {}

-- adds the "this data was last updated for" header
function last_updated(version)
	return string.format("<div class='hatnote'>This data was last updated for: [[Bloons TD 6 v%s|version %s]]</div>", version, version)
end

-- table of template names for each type of object
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"
}

function 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
	
	-- -- -- -- -- --
	-- 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
	
	tinsert(ret, "!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("|%gs", amt / (1 + (0.01 * msqrt((d-1)*50)))))
	end
	
	-- damage
	local insert_damage = function(amt)
		if d == 100 then	tinsert(ret, sformat("|%g", amt * 2 + 10))
		else				tinsert(ret, sformat("|%g", 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("|%g", amt * 2 + 10))
		else				tinsert(ret, sformat("|%g", 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("| +%g", amt * 2 + 10))
		else				tinsert(ret, sformat("| +%g", 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_mod(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", 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
		
		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 variables
	for k, v in pairs(data) do
		-- subtables (attacks, abilities, etc)
		if type(v) == "table" and ssub(k, 1, 1) ~= "_" then
			local subtables = {}
			
			for i, subtable_name in ipairs(v["_order"]) do
				-- insert header for subtowers
				if k == "subtowers" then tinsert(subtables, sformat(header, subtable_name)) end
				
				-- parse subtable of table
				local stats = parse_stats_base(frame, subtable_name, v)
				
				-- append stats subtable to output
				tinsert(subtables, frame:expandTemplate{title = template_names[k], args = stats})
				if k == "subtowers" and paragon then tinsert(subtables, paragon_table(v[subtable_name])) end
			end
			
			template[k] = tconcat(subtables)
		
		-- other variables (rate, pierce, etc)
		elseif type(v) ~= "table" then
			template[k] = data[k]
		end
	end
	
	-- append paragon table to output
	if paragon then template["paragon stats"] = paragon_table(data) end
	
	return frame:expandTemplate{title = "BTD6 tower", args = template}
end

function parse_stats_base(frame, p_name, data)
	local template = {name = p_name}
	
	-- local functions to improve performance
	local tconcat = table.concat
	
	-- search through variables
	for k, v in pairs(data[p_name]) do
		-- subtables (attacks, abilities, etc)
		if type(v) == "table" then
			local subtables = {}
			
			for i, subtable_name in ipairs(v["_order"]) do
				-- parse subtable of table
				subtables[i] = frame:expandTemplate{title = template_names[k], args = parse_stats_base(frame, subtable_name, v)}
			end
			
			template[k] = tconcat(subtables)
		
		-- other variables (rate, pierce, etc)
		else
			template[k] = v
		end
	end
	
	return template
end

function parse_tower_changes(frame, base, changes, 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 variables
	for k, v in pairs(changes) do
		-- subtables (attacks, abilities, etc)
		if type(v) == "table" and ssub(k, 1, 1) ~= "_" then
			local subtables = {}
			if prev_data[k] == nil then prev_data[k] = {} end
			
			for i, subtable_name in ipairs(v["_order"]) do
				if prev_data[k][subtable_name] == nil then prev_data[k][subtable_name] = {} end
				-- insert header for subtowers
				if k == "subtowers" then tinsert(subtables, sformat(header, subtable_name)) end
				
				-- parse subtable of table
				-- if neither the base tower nor a previous stat change has this subtable, then treat it as new
				if (base[k] == nil or base[k][subtable_name] == nil) and next(prev_data[k][subtable_name]) == nil then
					local stats = parse_stats_base(frame, subtable_name, v)
					prev_data[k][subtable_name] = stats
					tinsert(subtables, frame:expandTemplate{title = template_names[k], args = stats})
				else
					local stats = parse_stats_changes(frame, subtable_name, base[k][subtable_name], v[subtable_name], prev_data[k][subtable_name])
					tinsert(subtables, frame:expandTemplate{title = template_names[k], args = stats})
				end
			end
			
			template[k] = tconcat(subtables)
		
		-- other variables (rate, pierce, etc)
		elseif type(v) ~= "table" then
			-- stat comparison
			-- if this stat changed before, use that, otherwise use the base tower's stat
			if prev_data[k] ~= nil then template[k] = prev_data[k]
			else						template[k] = base[k] end
			
			template[k .. " after"] = v
			prev_data[k] = v
		end
	end
	
	return frame:expandTemplate{title = "BTD6 tower", args = template}
end

function parse_stats_changes(frame, p_name, base, changes, prev)
	local template = {name = p_name}
	
	-- local functions to improve performance
	local tinsert = table.insert
	local tconcat = table.concat
	
	-- search through tables
	for k, v in pairs(changes) do
		-- subtables (attacks, abilities, etc)
		if type(v) == "table" then
			local subtables = {}
			if prev[k] == nil then prev[k] = {} end
			
			for i, subtable_name in ipairs(v["_order"]) do
				if prev[k][subtable_name] == nil then prev[k][subtable_name] = {} end
				
				-- parse subtable of table
				-- if neither the base tower nor a previous stat change has this subtable, then treat it as new
				if (base == nil or base[k] == nil or base[k][subtable_name] == nil) and next(prev[k][subtable_name]) == nil then
					local stats = parse_stats_base(frame, subtable_name, v)
					prev[k][subtable_name] = stats
					tinsert(subtables, frame:expandTemplate{title = template_names[k], args = stats})
				else
					local stats = parse_stats_changes(frame, subtable_name, base[k][subtable_name], v[subtable_name], prev[k][subtable_name])
					tinsert(subtables, frame:expandTemplate{title = template_names[k], args = stats})
				end
			end
			
			template[k] = tconcat(subtables)
		
		-- other variables (rate, pierce, etc)
		else
			-- stat comparison
			-- if this stat changed before, use that, otherwise use the base tower's stat
			if prev[k] ~= nil then	template[k] = prev[k]
			else					template[k] = base[k] end
			
			template[k .. " after"] = v
			prev[k] = v
		end
	end
	
	return 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 subtable 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 subtable 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 subtable 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.tower_base(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]))
	
	return last_updated(data["_last_updated"]) .. parse_tower_base(frame, data, "<h3>%s</h3>", false)
end

-- stat changes of a tower
function p.tower_upgrade_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
	local ssub = string.sub
	
	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"}}
	}
	
	-- data for this upgrade + crosspaths
	local base_data = data[frame.args[2]]
	
	tinsert(ret, last_updated(data["_last_updated"], data["_last_updated"]))
	tinsert(ret, parse_tower_base(frame, base_data, "<h3>%s</h3>", false))
	
	for i, v in ipairs(upgrade_crosspaths[frame.args[2]]) do
		my_data = {}
		for ii, vv in ipairs(v) do
			if base_data[vv] ~= nil then
				tinsert(ret, sformat("<h3>%s-%s-%s</h3>", ssub(vv, 2, 2), ssub(vv, 3, 3), ssub(vv, 4, 4)))
				tinsert(ret, parse_tower_changes(frame, base_data, base_data[vv], "<h4>%s</h4>"))
			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
	
	for i = 2, 20 do
		tinsert(ret, sformat("===Level %i===", i))
		tinsert(ret, parse_tower_changes(frame, data, data["_" .. i], "<h4>%s</h4>"))
	end
	
	return table.concat(ret, "\n")
end

-- full stats of a hero for each level
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

-- get "last updated" header
function p.last_updated(frame)
	local data = mw.loadJsonData(string.format("Module:BTD6 stats/%s/new", frame.args[1]))
	return last_updated(data["_last_modified"])
end

return p