
Tower defense
A strategy game in which you protect a point against waves of enemies advancing along a route; you barely control them or your defenses, and instead decide which towers to buy and where to put them on a limited budget.
Also known as: TD, tower defence
Defining mechanics
- Enemies that follow a fixed path, or a maze the player forms with their own towers.
- Towers that fire on their own at whatever enters their range; the player only chooses the type and the tile.
- A closed economy in which every enemy defeated pays coins for the next tower or upgrade.
- Waves that grow in number, health and speed, with breaks to build between them.
- Lives lost whenever an enemy reaches the end of the route.
Gameplay loop
Look at the incoming wave, spend money on towers or upgrades, watch how the defense holds, collect the bounties and fix the weak spots before the next one.
Our demo
Controls. Pick the tower with the buttons at the top and click or tap a green tile to build it. Keys 1 and 2 also pick a tower, and space brings the wave forward.
Open full screenA fixed path with several bends, two tower types and eight waves of ever more numerous, tougher and faster enemies. Just enough to feel the genre's two decisions, which are what to spend on and where to build.
Made with Kaplay, in about 198 lines of JavaScript.
How it's made
Setup
The game is created at a fixed size and the grid (tile size, rows and columns) and the match rules are defined, such as the number of waves, lives and starting money. Changing these constants is the quickest way to tune the difficulty.
kaplay({ width: 640, height: 360, letterbox: true, pixelDensity, background: [20, 26, 34], // Everything is played with the mouse or a finger; the keyboard only adds shortcuts. buttons: { start: { keyboard: ["space", "enter"] }, tower1: { keyboard: ["1"] }, tower2: { keyboard: ["2"] }, }, }); const TILE = 32; const TOP = 40; // height of the top bar const COLS = 20; const ROWS = 10; const WAVES = 8; const LIVES = 10; const STARTING_COINS = 100; const tileCenter = (col, row) => vec2(col * TILE + TILE / 2, TOP + row * TILE + TILE / 2);The path
The route is a list of corners. From it the code works out the path tiles, where you cannot build, and the route in pixels that enemies follow. A long, winding path gives towers more time and creates spots that are better than others.
// The path is fixed: a list of corners (column, row) that enemies walk // through in order. Path tiles cannot hold towers. const CORNERS = [[-1, 2], [4, 2], [4, 7], [9, 7], [9, 1], [14, 1], [14, 8], [20, 8]]; const ROUTE = CORNERS.map(([col, row]) => tileCenter(col, row)); const onPath = new Set(); for (let i = 0; i < CORNERS.length - 1; i++) { let [col, row] = CORNERS[i]; const [col2, row2] = CORNERS[i + 1]; for (;;) { onPath.add(`${col},${row}`); if (col === col2 && row === row2) break; col += Math.sign(col2 - col); row += Math.sign(row2 - row); } }Tower types
Each tower is a data object with cost, range, fire rate, damage and area. The two complement each other: the archer is cheap and fast against one enemy, and the cannon is expensive and slow but hits a group. If one were better at everything, there would be no decision.
// Two towers with different roles: a cheap, fast one against a single enemy, // and an expensive, slow one with area damage. Choosing which and where is the strategy. const TOWERS = [ { name: t({ es: "Arquero", en: "Archer" }), cost: 40, range: 90, fireRate: 0.45, damage: 1, splash: 0, color: [110, 190, 255] }, { name: t({ es: "Cañón", en: "Cannon" }), cost: 70, range: 70, fireRate: 1.3, damage: 3, splash: 40, color: [240, 150, 70] }, ];Economy and scoreboard
The top bar shows coins, lives and wave, and holds the buttons to pick a tower. Money is the resource that ties everything together, because killing enemies gives coins and coins buy towers that kill more enemies.
// Top bar: coins, lives, wave and the buttons to pick a tower. for (let row = 0; row < ROWS; row++) for (let col = 0; col < COLS; col++) { const path = onPath.has(`${col},${row}`); add([rect(TILE - 2, TILE - 2), pos(col * TILE + 1, TOP + row * TILE + 1), color(path ? rgb(150, 122, 84) : rgb(50, 84, 60))]); } add([rect(width(), TOP), color(12, 16, 22)]); const hud = add([text("", { size: 15, font: FONT }), pos(10, 12)]); const notice = add([text("", { size: 16, font: FONT }), pos(width() / 2, TOP + 14), anchor("center"), z(20)]); const updateHud = () => (hud.text = `${t({ es: "Monedas", en: "Coins" })}: ${coins} ${t({ es: "Vidas", en: "Lives" })}: ${lives} ${t({ es: "Oleada", en: "Wave" })}: ${wave}/${WAVES}`); const buttons = TOWERS.map((type, i) => { const b = add([rect(116, 28, { radius: 4 }), pos(384 + i * 126, 6), color(40, 48, 60), outline(2, rgb(...type.color)), area()]); b.add([text(`${i + 1}. ${type.name} (${type.cost})`, { size: 13, font: FONT }), pos(58, 14), anchor("center")]); b.onClick(() => select(i)); return b; }); function select(i) { selected = i; buttons.forEach((b, j) => (b.color = j === i ? rgb(84, 100, 130) : rgb(40, 48, 60))); } onButtonPress("tower1", () => select(0)); onButtonPress("tower2", () => select(1)); select(0); updateHud();Placing towers
On a click, the mouse position is turned into a tile and the code checks that it is free, not on the path and affordable. The range is drawn under the cursor in green or red, so the player can see before spending whether that spot covers the path well.
// Click a free tile: if you can afford it, the selected tower is built. const tileAt = (m) => [Math.floor(m.x / TILE), Math.floor((m.y - TOP) / TILE)]; const isFree = (col, row) => row >= 0 && row < ROWS && col >= 0 && col < COLS && !onPath.has(`${col},${row}`) && !occupied.has(`${col},${row}`); onMousePress(() => { const [col, row] = tileAt(mousePos()); const type = TOWERS[selected]; if (!isFree(col, row) || coins < type.cost) return; coins -= type.cost; occupied.add(`${col},${row}`); updateHud(); addTower(type, tileCenter(col, row)); }); // Range preview under the cursor: green if you can build there, red if not. onDraw(() => { const m = mousePos(); if (m.y < TOP) return; const [col, row] = tileAt(m); const type = TOWERS[selected]; const ok = isFree(col, row) && coins >= type.cost; drawCircle({ pos: tileCenter(col, row), radius: type.range, color: ok ? rgb(120, 255, 140) : rgb(255, 90, 90), opacity: 0.15 }); }); function addTower(type, p) { const tower = add([circle(11), pos(p), anchor("center"), color(...type.color), outline(2, rgb(20, 20, 20)), { cooldown: 0 }]); tower.onUpdate(() => { tower.cooldown -= dt(); if (tower.cooldown > 0) return; const target = pickTarget(p, type.range); if (!target) return; tower.cooldown = type.fireRate; shoot(type, p, target); }); }Aiming and shooting
Each tower looks, among the enemies in its range, for the one that has advanced the most, because it is closest to escaping. The bullet chases its target; the cannon also damages everyone near the impact. Each kill adds its bounty to the coins.
// Classic targeting rule: of the enemies in range, the one furthest along, // that is, the one closest to escaping. function pickTarget(p, range) { let best = null; for (const e of get("enemy")) if (e.pos.dist(p) <= range && (!best || e.progress > best.progress)) best = e; return best; } function shoot(type, from, target) { const bullet = add([circle(type.splash ? 5 : 3), pos(from), anchor("center"), color(255, 240, 180)]); bullet.onUpdate(() => { if (!target.exists()) return destroy(bullet); const dir = target.pos.sub(bullet.pos); if (dir.len() < 8) { destroy(bullet); hit(type, target); } else bullet.move(dir.unit().scale(340)); }); } function hit(type, target) { if (!type.splash) return hurt(target, type.damage); add([circle(type.splash), pos(target.pos), anchor("center"), color(255, 180, 90), opacity(0.4), lifespan(0.25, { fade: 0.2 })]); for (const e of get("enemy")) if (e.pos.dist(target.pos) <= type.splash) hurt(e, type.damage); } function hurt(e, damage) { if (!e.exists()) return; e.health -= damage; if (e.health > 0) return; destroy(e); coins += e.bounty; // every kill pays for the next tower updateHud(); }Enemies
Enemies move from corner to corner and accumulate the distance travelled, which towers use to choose a target. Their health, speed and bounty depend on the wave number. If one reaches the end, a life is lost.
// Each enemy walks from corner to corner and accumulates `progress` (distance // travelled). If it reaches the end, you lose a life. function addEnemy(n) { const maxHealth = 2 + n * 2; const e = add([rect(18, 18), pos(ROUTE[0]), anchor("center"), color(200, 70, 90), "enemy", { health: maxHealth, maxHealth, speed: 42 + n * 5, leg: 1, progress: 0, bounty: 3 + n }]); e.onUpdate(() => { const step = e.speed * dt(); const dir = ROUTE[e.leg].sub(e.pos); if (dir.len() <= step) e.pos = ROUTE[e.leg++].clone(); else e.pos = e.pos.add(dir.unit().scale(step)); e.progress += step; if (e.leg < ROUTE.length) return; destroy(e); lives--; updateHud(); shake(4); if (lives <= 0) go("end", false, wave); }); e.onDraw(() => drawRect({ pos: vec2(-10, -16), width: (20 * e.health) / e.maxHealth, height: 3, color: rgb(120, 230, 120) })); }The wave curve
Each wave brings more enemies, with more health and closer together, and there are a few seconds to build between waves. This curve, compared with what the player earns from kills, is what decides whether the game feels fair, easy or impossible.
// The difficulty curve: each wave brings more enemies, with more health, // faster and closer together. Between waves there is a break to build. function startWave() { wave++; toSpawn = 4 + wave * 2; updateHud(); loop(Math.max(0.35, 1 - wave * 0.08), () => { addEnemy(wave); toSpawn--; }, toSpawn); } onButtonPress("start", () => (breakTime = 0)); // call the next wave early onUpdate(() => { if (toSpawn > 0 || get("enemy").length > 0) return (notice.text = ""); if (wave === WAVES) return go("end", true, wave); breakTime -= dt(); notice.text = t({ es: `Oleada ${wave + 1} en ${Math.ceil(breakTime)} s (espacio: ya)`, en: `Wave ${wave + 1} in ${Math.ceil(breakTime)} s (space: now)` }); if (breakTime <= 0) { breakTime = 6; startWave(); } });Scenes
The game has three scenes, title, game and end, and go() jumps between them. Restarting the game scene resets money, lives and the map to the start.
function screen(title, subtitle, next) { add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 40)), anchor("center")]); add([ text(subtitle, { size: 16, font: FONT, align: "center", width: 540 }), pos(center().add(0, 30)), anchor("center"), color(200, 210, 255), ]); onButtonPress("start", next); onClick(next); } scene("title", () => screen( t({ es: "Defensa de torres", en: "Tower defense" }), t({ es: "Elige una torre arriba y haz clic (o toca) en una casilla verde para construirla.\nQue ningún enemigo llegue al final del camino. Sobrevive 8 oleadas.\nHaz clic o pulsa espacio para empezar.", en: "Pick a tower at the top and click (or tap) a green tile to build it.\nDon't let any enemy reach the end of the path. Survive 8 waves.\nClick or press space to start.", }), () => go("game"), ), ); scene("end", (won, wave) => screen( won ? t({ es: "¡Resististe!", en: "You held out!" }) : t({ es: "Cayó la defensa", en: "The defense fell" }), t({ es: `Llegaste a la oleada ${wave}. Haz clic para repetir.`, en: `You reached wave ${wave}. Click to play again.` }), () => go("game"), ), ); go("title");
What we simplified
- Towers cannot be upgraded or sold, and there is a single enemy type that gets stronger with each wave.
- Towers, enemies and bullets are circles and squares, with no animations or sound.
- A single map with a fixed path; there is no maze for the player to lengthen.
To modify it
- Add a third tower that deals no damage but slows enemies for a few seconds, and think about where it is best placed.
- Allow upgrading a tower by clicking on it (more damage or range in exchange for coins).
- Create a fast enemy with little health and a slow one with a lot, and mix them in the waves.
- Change the targeting rule to the enemy with most health or the closest one, and compare how many lives you lose.
Full code
// Tower defense demo
// Engine: Kaplay 3001 (https://kaplayjs.com). MIT licence.
import kaplay from "../../vendor/kaplay.mjs";
import { t, FONT, pixelDensity } from "../common.js";
// #region setup
kaplay({
width: 640,
height: 360,
letterbox: true, pixelDensity,
background: [20, 26, 34],
// Everything is played with the mouse or a finger; the keyboard only adds shortcuts.
buttons: {
start: { keyboard: ["space", "enter"] },
tower1: { keyboard: ["1"] },
tower2: { keyboard: ["2"] },
},
});
const TILE = 32;
const TOP = 40; // height of the top bar
const COLS = 20;
const ROWS = 10;
const WAVES = 8;
const LIVES = 10;
const STARTING_COINS = 100;
const tileCenter = (col, row) => vec2(col * TILE + TILE / 2, TOP + row * TILE + TILE / 2);
// #endregion
// #region path
// The path is fixed: a list of corners (column, row) that enemies walk
// through in order. Path tiles cannot hold towers.
const CORNERS = [[-1, 2], [4, 2], [4, 7], [9, 7], [9, 1], [14, 1], [14, 8], [20, 8]];
const ROUTE = CORNERS.map(([col, row]) => tileCenter(col, row));
const onPath = new Set();
for (let i = 0; i < CORNERS.length - 1; i++) {
let [col, row] = CORNERS[i];
const [col2, row2] = CORNERS[i + 1];
for (;;) {
onPath.add(`${col},${row}`);
if (col === col2 && row === row2) break;
col += Math.sign(col2 - col);
row += Math.sign(row2 - row);
}
}
// #endregion
// #region tower-types
// Two towers with different roles: a cheap, fast one against a single enemy,
// and an expensive, slow one with area damage. Choosing which and where is the strategy.
const TOWERS = [
{ name: t({ es: "Arquero", en: "Archer" }), cost: 40, range: 90, fireRate: 0.45, damage: 1, splash: 0, color: [110, 190, 255] },
{ name: t({ es: "Cañón", en: "Cannon" }), cost: 70, range: 70, fireRate: 1.3, damage: 3, splash: 40, color: [240, 150, 70] },
];
// #endregion
scene("game", () => {
let coins = STARTING_COINS;
let lives = LIVES;
let wave = 0;
let toSpawn = 0; // enemies of this wave still waiting to come out
let breakTime = 4; // seconds until the next wave
let selected = 0;
const occupied = new Set();
// #region economy
// Top bar: coins, lives, wave and the buttons to pick a tower.
for (let row = 0; row < ROWS; row++)
for (let col = 0; col < COLS; col++) {
const path = onPath.has(`${col},${row}`);
add([rect(TILE - 2, TILE - 2), pos(col * TILE + 1, TOP + row * TILE + 1), color(path ? rgb(150, 122, 84) : rgb(50, 84, 60))]);
}
add([rect(width(), TOP), color(12, 16, 22)]);
const hud = add([text("", { size: 15, font: FONT }), pos(10, 12)]);
const notice = add([text("", { size: 16, font: FONT }), pos(width() / 2, TOP + 14), anchor("center"), z(20)]);
const updateHud = () =>
(hud.text = `${t({ es: "Monedas", en: "Coins" })}: ${coins} ${t({ es: "Vidas", en: "Lives" })}: ${lives} ${t({ es: "Oleada", en: "Wave" })}: ${wave}/${WAVES}`);
const buttons = TOWERS.map((type, i) => {
const b = add([rect(116, 28, { radius: 4 }), pos(384 + i * 126, 6), color(40, 48, 60), outline(2, rgb(...type.color)), area()]);
b.add([text(`${i + 1}. ${type.name} (${type.cost})`, { size: 13, font: FONT }), pos(58, 14), anchor("center")]);
b.onClick(() => select(i));
return b;
});
function select(i) {
selected = i;
buttons.forEach((b, j) => (b.color = j === i ? rgb(84, 100, 130) : rgb(40, 48, 60)));
}
onButtonPress("tower1", () => select(0));
onButtonPress("tower2", () => select(1));
select(0);
updateHud();
// #endregion
// #region placing-towers
// Click a free tile: if you can afford it, the selected tower is built.
const tileAt = (m) => [Math.floor(m.x / TILE), Math.floor((m.y - TOP) / TILE)];
const isFree = (col, row) => row >= 0 && row < ROWS && col >= 0 && col < COLS && !onPath.has(`${col},${row}`) && !occupied.has(`${col},${row}`);
onMousePress(() => {
const [col, row] = tileAt(mousePos());
const type = TOWERS[selected];
if (!isFree(col, row) || coins < type.cost) return;
coins -= type.cost;
occupied.add(`${col},${row}`);
updateHud();
addTower(type, tileCenter(col, row));
});
// Range preview under the cursor: green if you can build there, red if not.
onDraw(() => {
const m = mousePos();
if (m.y < TOP) return;
const [col, row] = tileAt(m);
const type = TOWERS[selected];
const ok = isFree(col, row) && coins >= type.cost;
drawCircle({ pos: tileCenter(col, row), radius: type.range, color: ok ? rgb(120, 255, 140) : rgb(255, 90, 90), opacity: 0.15 });
});
function addTower(type, p) {
const tower = add([circle(11), pos(p), anchor("center"), color(...type.color), outline(2, rgb(20, 20, 20)), { cooldown: 0 }]);
tower.onUpdate(() => {
tower.cooldown -= dt();
if (tower.cooldown > 0) return;
const target = pickTarget(p, type.range);
if (!target) return;
tower.cooldown = type.fireRate;
shoot(type, p, target);
});
}
// #endregion
// #region shooting
// Classic targeting rule: of the enemies in range, the one furthest along,
// that is, the one closest to escaping.
function pickTarget(p, range) {
let best = null;
for (const e of get("enemy")) if (e.pos.dist(p) <= range && (!best || e.progress > best.progress)) best = e;
return best;
}
function shoot(type, from, target) {
const bullet = add([circle(type.splash ? 5 : 3), pos(from), anchor("center"), color(255, 240, 180)]);
bullet.onUpdate(() => {
if (!target.exists()) return destroy(bullet);
const dir = target.pos.sub(bullet.pos);
if (dir.len() < 8) {
destroy(bullet);
hit(type, target);
} else bullet.move(dir.unit().scale(340));
});
}
function hit(type, target) {
if (!type.splash) return hurt(target, type.damage);
add([circle(type.splash), pos(target.pos), anchor("center"), color(255, 180, 90), opacity(0.4), lifespan(0.25, { fade: 0.2 })]);
for (const e of get("enemy")) if (e.pos.dist(target.pos) <= type.splash) hurt(e, type.damage);
}
function hurt(e, damage) {
if (!e.exists()) return;
e.health -= damage;
if (e.health > 0) return;
destroy(e);
coins += e.bounty; // every kill pays for the next tower
updateHud();
}
// #endregion
// #region enemies
// Each enemy walks from corner to corner and accumulates `progress` (distance
// travelled). If it reaches the end, you lose a life.
function addEnemy(n) {
const maxHealth = 2 + n * 2;
const e = add([rect(18, 18), pos(ROUTE[0]), anchor("center"), color(200, 70, 90), "enemy",
{ health: maxHealth, maxHealth, speed: 42 + n * 5, leg: 1, progress: 0, bounty: 3 + n }]);
e.onUpdate(() => {
const step = e.speed * dt();
const dir = ROUTE[e.leg].sub(e.pos);
if (dir.len() <= step) e.pos = ROUTE[e.leg++].clone();
else e.pos = e.pos.add(dir.unit().scale(step));
e.progress += step;
if (e.leg < ROUTE.length) return;
destroy(e);
lives--;
updateHud();
shake(4);
if (lives <= 0) go("end", false, wave);
});
e.onDraw(() => drawRect({ pos: vec2(-10, -16), width: (20 * e.health) / e.maxHealth, height: 3, color: rgb(120, 230, 120) }));
}
// #endregion
// #region waves
// The difficulty curve: each wave brings more enemies, with more health,
// faster and closer together. Between waves there is a break to build.
function startWave() {
wave++;
toSpawn = 4 + wave * 2;
updateHud();
loop(Math.max(0.35, 1 - wave * 0.08), () => {
addEnemy(wave);
toSpawn--;
}, toSpawn);
}
onButtonPress("start", () => (breakTime = 0)); // call the next wave early
onUpdate(() => {
if (toSpawn > 0 || get("enemy").length > 0) return (notice.text = "");
if (wave === WAVES) return go("end", true, wave);
breakTime -= dt();
notice.text = t({ es: `Oleada ${wave + 1} en ${Math.ceil(breakTime)} s (espacio: ya)`, en: `Wave ${wave + 1} in ${Math.ceil(breakTime)} s (space: now)` });
if (breakTime <= 0) {
breakTime = 6;
startWave();
}
});
// #endregion
});
// #region scenes
function screen(title, subtitle, next) {
add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 40)), anchor("center")]);
add([
text(subtitle, { size: 16, font: FONT, align: "center", width: 540 }),
pos(center().add(0, 30)),
anchor("center"),
color(200, 210, 255),
]);
onButtonPress("start", next);
onClick(next);
}
scene("title", () =>
screen(
t({ es: "Defensa de torres", en: "Tower defense" }),
t({
es: "Elige una torre arriba y haz clic (o toca) en una casilla verde para construirla.\nQue ningún enemigo llegue al final del camino. Sobrevive 8 oleadas.\nHaz clic o pulsa espacio para empezar.",
en: "Pick a tower at the top and click (or tap) a green tile to build it.\nDon't let any enemy reach the end of the path. Survive 8 waves.\nClick or press space to start.",
}),
() => go("game"),
),
);
scene("end", (won, wave) =>
screen(
won ? t({ es: "¡Resististe!", en: "You held out!" }) : t({ es: "Cayó la defensa", en: "The defense fell" }),
t({ es: `Llegaste a la oleada ${wave}. Haz clic para repetir.`, en: `You reached wave ${wave}. Click to play again.` }),
() => go("game"),
),
);
go("title");
// #endregionDesign challenges
- Balancing the wave curve against the income curve, so the defense is always a little stretched but never hopeless.
- Designing towers with distinct roles (cheap and fast, expensive and area damage, slow but slowing) so no single option dominates.
- Making the map offer good and bad places to build, because placement is the core decision.
- Keeping what happens readable (ranges, enemy health, where they come from) even with dozens of objects on screen.
History
- 1990OriginRampart
Space Invaders (1978) and Missile Command (1980) already asked you to defend territory, but Rampart, which combines building walls, placing cannons and surviving the assault, is considered the prototype. The name and today's form came with fan-made maps for StarCraft and Warcraft III.
Tower defense (Wikipedia) - 1990Rampart
Rampart alternates phases of building, arming and defending, and sets the genre's template.
Rampart (Wikipedia) - 2000
Custom StarCraft maps such as Turret Defense, and later Warcraft III ones, turn tower defense into a genre of its own made by fans.
Tower defense (Wikipedia) - 2007Desktop Tower Defense
Desktop Tower Defense removes the fixed path, lets towers form the maze and popularises the genre in the browser.
Desktop Tower Defense (Wikipedia) - 2009Plants vs. Zombies
Plants vs. Zombies swaps the path for lanes on a grid and takes the genre to a mass audience.
Plants vs. Zombies (Wikipedia) - 2011Kingdom Rush
Kingdom Rush, from Uruguayan studio Ironhide, polishes the fixed build-spot formula with a controllable hero and sells over seventeen million copies.
Tower defense (Wikipedia) - 2011
Sanctum blends the genre with first-person shooting and popularises the hybrid in which the player also fights.
Tower defense (Wikipedia)
Reference games
1990
Rampart
You alternate timed phases in which you surround castles with walls made of Tetris-like pieces, place cannons inside and then fire at attacking ships. Whatever is left unenclosed is lost. It is considered the prototype of tower defense.
2007
Desktop Tower Defense
A Flash game set on a drawn desktop with no fixed path, because the towers you place form the maze the enemies walk through. It popularised the genre in the browser.
2009
Plants vs. Zombies
You plant flora on a lawn grid to stop zombies advancing down lanes; the sun you collect is the currency. It brought tower defense to a very wide audience.
2011
Kingdom Rush
Enemies follow fixed roads while you build and upgrade four tower types (archers, mages, artillery and barracks) on marked spots, and move a hero around. It was made by a Uruguayan studio and began as a free browser game.
2014
Elematter
A minimalist tower defense made for js13kGames 2014 in under 13 kB. You place towers of four elements (fire, water, earth and air) beside a fixed path and upgrade them. Its author published it playable and open source.
Exercise
In the demo, change the enemies' health formula or the bounty per kill and play two rounds; note at which wave the balance breaks. Then add a third tower that slows instead of damaging and check whether it changes where it pays to build.