btd clone lol

This sketch recreates a tower-defense game where colorful balloons travel along a winding path while player-placed monkeys shoot them down. As rounds progress, balloons become tougher and faster, and players earn money to buy and upgrade their defenses before their lives run out.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make balloons spawn much faster — Lower spawnInterval values create balloons more frequently, increasing difficulty—great for testing your defenses
  2. Give Dart monkeys a huge range boost
  3. Start with more money — Higher starting money lets you build a stronger early-game defense without waiting for balloon kills
  4. Make balloons much tougher — Increase balloon health values to create a challenge—forces players to upgrade damage or use expensive towers
  5. Make all monkeys super powerful — Increase base damage so towers are more effective, turning the game into rapid-fire balloon popping
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a playable tower-defense game using p5.js. Balloons of different types spawn continuously and follow a fixed path across the canvas while you place monkey towers to intercept them. The game combines object-oriented programming with real-time collision detection, game state tracking, and interactive UI—three essential patterns in every game or interactive sketch you will build.

The code is organized into a Game class that manages balloons, monkeys, money, and lives; a Balloon class that moves along a path and tracks health; and a Monkey class that defends territory. By reading it you will learn how to structure a game with multiple entities, handle turn-based upgrades via the DOM, apply damage across objects, and detect when a player wins or loses. This is the skeleton of nearly every interactive game you could create.

⚙️ How It Works

  1. setup() creates an 800×600 canvas and a Game object that initializes empty arrays for balloons and monkeys, along with starting money, round, and lives values.
  2. On every frame, draw() clears the background, renders the path, updates all game state (spawning balloons, moving them, checking collisions), and displays the HUD showing money, round, and selected monkey type.
  3. When you press number keys 1–3, the selectedMonkeyType variable changes, letting you choose which monkey to place next.
  4. When you click the canvas, mousePressed() either upgrades an existing monkey if you clicked near one, or places a new monkey of the selected type if you have enough money.
  5. Balloons spawn at regular intervals, move step-by-step through the path array, take damage from nearby monkeys, and either die (giving you money) or escape (costing you a life).
  6. Every 600 frames, the round increments, making balloons spawn faster and move quicker; the game ends when you reach round 80 (win) or run out of lives (lose).

🎓 Concepts You'll Learn

Object-oriented programming with classesCollision detection with distance checkingPath following and vector movementGame state managementInteractive upgrades and UIFactory pattern for balloon creationArray iteration and splicingConditional logic for game rules

📝 Code Breakdown

setup()

setup() runs once at the start. Use it to initialize your canvas and create the Game object that runs everything else.

function setup() {
  createCanvas(800, 600);
  game = new Game();
}
Line-by-line explanation (2 lines)
createCanvas(800, 600);
Creates the game board—800 pixels wide and 600 pixels tall
game = new Game();
Creates a new Game object that holds all balloons, monkeys, money, and round state

draw()

draw() runs 60 times per second. It is where all animation happens: clear the canvas, update game logic, and redraw everything in its new state.

function draw() {
  background(200, 230, 255);
  drawPath();
  game.update();
  game.show();
  drawControls();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Clear canvas with sky-blue background background(200, 230, 255);

Erases everything from the previous frame so balloons and monkeys draw in their new positions

function-call Draw the balloon path drawPath();

Renders the brown winding line that balloons follow

function-call Update game state game.update();

Spawns balloons, moves them, checks collisions, damages balloons, awards money, and increments rounds

function-call Draw all balloons and monkeys game.show();

Calls the show() method on every balloon and monkey to draw them on screen

function-call Display controls and stats drawControls();

Shows the HUD with money, round, lives, and keyboard instructions

background(200, 230, 255);
Fills the entire canvas with a light blue color, clearing the previous frame
drawPath();
Calls the drawPath() function to render the brown path that balloons follow
game.update();
Calls the Game's update() method, which spawns balloons, moves them, checks if they hit monkeys or escape, and advances rounds
game.show();
Calls the Game's show() method, which tells each balloon and monkey to draw itself on screen
drawControls();
Renders text showing money, round, lives, and the instructions for picking and placing monkeys

keyPressed()

keyPressed() is called whenever the player presses a key. Use it to detect input and update game state like selecting which tower to place next.

🔬 These three lines let you pick which monkey to place by pressing 1, 2, or 3. What happens if you add a 4th key to select a new monkey type you create (like a cheap fast-shooting tower)?

  if (key === "1") selectedMonkeyType = "Dart";
  if (key === "2") selectedMonkeyType = "Sniper";
  if (key === "3") selectedMonkeyType = "Super";
function keyPressed() {
  if (key === "1") selectedMonkeyType = "Dart";
  if (key === "2") selectedMonkeyType = "Sniper";
  if (key === "3") selectedMonkeyType = "Super";
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Select Dart Monkey if (key === "1") selectedMonkeyType = "Dart";

When player presses 1, sets selectedMonkeyType to Dart so the next click places a Dart monkey

conditional Select Sniper Monkey if (key === "2") selectedMonkeyType = "Sniper";

When player presses 2, switches to Sniper type with infinite range

conditional Select Super Monkey if (key === "3") selectedMonkeyType = "Super";

When player presses 3, switches to Super type with high damage

if (key === "1") selectedMonkeyType = "Dart";
Checks if the player pressed the 1 key and changes selectedMonkeyType to Dart
if (key === "2") selectedMonkeyType = "Sniper";
Checks if the player pressed the 2 key and changes selectedMonkeyType to Sniper
if (key === "3") selectedMonkeyType = "Super";
Checks if the player pressed the 3 key and changes selectedMonkeyType to Super

mousePressed()

mousePressed() runs when the player clicks. It decides whether to upgrade an existing tower or place a new one.

function mousePressed() {
  if (!game.tryUpgradeMonkey(mouseX, mouseY)) {
    game.placeMonkey(mouseX, mouseY, selectedMonkeyType);
  }
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional Try to upgrade existing monkey if (!game.tryUpgradeMonkey(mouseX, mouseY)) {

Checks if the click is on an existing monkey; if yes, upgrade it; if no, place a new one

if (!game.tryUpgradeMonkey(mouseX, mouseY)) {
Calls tryUpgradeMonkey() with the mouse position. The ! means 'if this returns false' (no monkey was clicked)
game.placeMonkey(mouseX, mouseY, selectedMonkeyType);
If no existing monkey was clicked, place a new monkey at the mouse location of the currently selected type

drawPath()

drawPath() renders the winding brown path that balloons follow. It uses beginShape() and vertex() to connect all the waypoints in the path array into a smooth curve.

🔬 This entire block draws the path. What happens if you change strokeWeight(20) to strokeWeight(5)? How about strokeWeight(50)?

  stroke(180, 150, 100);
  strokeWeight(20);
  noFill();
  beginShape();
  for (let p of path) vertex(p.x, p.y);
  endShape();
function drawPath() {
  stroke(180, 150, 100);
  strokeWeight(20);
  noFill();
  beginShape();
  for (let p of path) vertex(p.x, p.y);
  endShape();
  noStroke();
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Set stroke color and thickness stroke(180, 150, 100); strokeWeight(20);

Makes the path a brown color that is 20 pixels thick

for-loop Draw path vertices for (let p of path) vertex(p.x, p.y);

Loops through each waypoint in the path array and draws a line connecting them all

stroke(180, 150, 100);
Sets the line color to a brownish tone (RGB: 180, 150, 100)
strokeWeight(20);
Makes the path line 20 pixels wide so it is clearly visible
noFill();
Tells p5.js not to fill any shapes—we only want the outline
beginShape();
Starts defining a shape made of connected vertices
for (let p of path) vertex(p.x, p.y);
Loops through each point in the path array and adds it as a vertex, creating a connected line
endShape();
Finishes drawing the shape; p5.js now renders all the vertices as a continuous line
noStroke();
Removes stroke from future shapes so the balloons and monkeys don't have outlines

drawControls()

drawControls() renders the on-screen HUD displaying game stats and instructions. It reads live values like game.money and selectedMonkeyType to show the current state.

function drawControls() {
  fill(0);
  textSize(14);
  let x = width - 220;
  let y = 20;
  text("Controls:", x, y);
  text("1 = Dart Monkey", x, y+20);
  text("2 = Sniper Monkey", x, y+40);
  text("3 = Super Monkey", x, y+60);
  text("Click monkey to upgrade ($50)", x, y+80);
  text("Money: $" + game.money, x, y+100);
  text("Round: " + game.round, x, y+120);
  text("Lives: " + game.lives, x, y+140);
  text("Selected: " + selectedMonkeyType, x, y+160);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Calculate HUD position let x = width - 220; let y = 20;

Places the HUD text in the upper-right corner of the canvas

calculation Draw instruction text text("Controls:", x, y);

Displays static labels explaining the keyboard controls

calculation Draw game stats text("Money: $" + game.money, x, y+100);

Shows live game state (money, round, lives, selected monkey) that changes every frame

fill(0);
Sets the text color to black
textSize(14);
Sets the font size to 14 pixels
let x = width - 220;
Positions the text 220 pixels from the right edge, placing the HUD on the right side of the canvas
let y = 20;
Positions the text 20 pixels from the top of the canvas
text("Money: $" + game.money, x, y+100);
Displays the current money amount, concatenating the string with game.money to show a live number
text("Round: " + game.round, x, y+120);
Displays the current round number
text("Lives: " + game.lives, x, y+140);
Displays the remaining lives
text("Selected: " + selectedMonkeyType, x, y+160);
Shows which monkey type is currently selected (Dart, Sniper, or Super)

Game() constructor

The constructor runs when new Game() is called in setup(). It initializes all the variables that track the game state: arrays for entities and numbers for money, round, and difficulty.

  constructor() {
    this.balloons = [];
    this.monkeys = [];
    this.money = 100;
    this.round = 1;
    this.spawnInterval = 60;
    this.spawnCounter = 0;
    this.lives = 10;
  }
Line-by-line explanation (7 lines)
this.balloons = [];
Creates an empty array to hold all active Balloon objects
this.monkeys = [];
Creates an empty array to hold all placed Monkey objects
this.money = 100;
Gives the player 100 starting money to buy towers
this.round = 1;
Sets the game to round 1 when it starts
this.spawnInterval = 60;
Balloons spawn every 60 frames—lower numbers mean faster spawning
this.spawnCounter = 0;
A counter that increments each frame; when it reaches spawnInterval, a balloon spawns and the counter resets
this.lives = 10;
The player starts with 10 lives; each balloon that escapes costs one life

Game.update()

update() is where all game logic happens: balloons spawn, move, take damage from monkeys, and escape. It also checks win/lose conditions and advances rounds. This is the heart of the game.

🔬 This loop handles damage and death. What happens if you remove the 'break;' statement at the end? (Hint: multiple monkeys could damage one balloon per frame.)

      for (let m of this.monkeys) {
        let inRange = m.type === "Sniper" ? true : dist(this.balloons[i].pos.x, this.balloons[i].pos.y, m.x, m.y) < m.range;
        if (inRange) {
          this.balloons[i].health -= m.damage;
          if (this.balloons[i].health <= 0) {
            this.money += this.balloons[i].value;
            this.balloons.splice(i, 1);
          }
          break;
        }
      }
  update() {
    this.spawnCounter++;
    if (this.spawnCounter >= this.spawnInterval) {
      this.balloons.push(BalloonFactory(this.round));
      this.spawnCounter = 0;
    }

    for (let i = this.balloons.length - 1; i >= 0; i--) {
      this.balloons[i].move();

      for (let m of this.monkeys) {
        let inRange = m.type === "Sniper" ? true : dist(this.balloons[i].pos.x, this.balloons[i].pos.y, m.x, m.y) < m.range;
        if (inRange) {
          this.balloons[i].health -= m.damage;
          if (this.balloons[i].health <= 0) {
            this.money += this.balloons[i].value;
            this.balloons.splice(i, 1);
          }
          break;
        }
      }

      if (i < this.balloons.length && this.balloons[i].index >= path.length - 1) {
        this.lives--;
        this.balloons.splice(i, 1);
      }
    }

    if (frameCount % 600 === 0) {
      this.round++;
      this.spawnInterval = max(10, this.spawnInterval - 2);
    }

    if (this.round > 80) {
      noLoop();
      fill(0, 200, 0);
      textSize(50);
      text("You Win!", width/2 - 100, height/2);
    } else if (this.lives <= 0) {
      noLoop();
      fill(200, 0, 0);
      textSize(50);
      text("Game Over", width/2 - 130, height/2);
    }
  }
Line-by-line explanation (16 lines)

🔧 Subcomponents:

conditional Spawn balloons on interval if (this.spawnCounter >= this.spawnInterval) { this.balloons.push(BalloonFactory(this.round)); this.spawnCounter = 0; }

Every spawnInterval frames, creates a new balloon via the factory and resets the counter

for-loop Update and check each balloon for (let i = this.balloons.length - 1; i >= 0; i--) {

Loops backward through balloons so we can safely remove dead ones without skipping any

nested-for-loop Check monkey collision with balloons for (let m of this.monkeys) { let inRange = m.type === "Sniper" ? true : dist(this.balloons[i].pos.x, this.balloons[i].pos.y, m.x, m.y) < m.range; if (inRange) { this.balloons[i].health -= m.damage; if (this.balloons[i].health <= 0) { this.money += this.balloons[i].value; this.balloons.splice(i, 1); } break; } }

Checks if any monkey is in range; if so, damages the balloon and removes it if its health reaches zero

conditional Check if balloon escaped if (i < this.balloons.length && this.balloons[i].index >= path.length - 1) { this.lives--; this.balloons.splice(i, 1); }

If the balloon reached the end of the path, subtract a life and remove it

conditional Increment round every 600 frames if (frameCount % 600 === 0) { this.round++; this.spawnInterval = max(10, this.spawnInterval - 2); }

Every 10 seconds, advances the round and makes balloons spawn faster

conditional Check win condition if (this.round > 80) {

If you reach round 81, the game stops and displays 'You Win!'

conditional Check lose condition } else if (this.lives <= 0) {

If lives drop to zero, the game stops and displays 'Game Over'

this.spawnCounter++;
Increments the spawn counter by 1 each frame
if (this.spawnCounter >= this.spawnInterval) {
Checks if enough frames have passed to spawn a new balloon
this.balloons.push(BalloonFactory(this.round));
Creates a new balloon via the factory function (which picks a random type) and adds it to the balloons array
this.spawnCounter = 0;
Resets the counter to 0 so it can count up to spawnInterval again
for (let i = this.balloons.length - 1; i >= 0; i--) {
Loops through the balloons array backward (from last to first) so we can safely splice out dead ones
this.balloons[i].move();
Calls each balloon's move() method, advancing it one step along the path
let inRange = m.type === "Sniper" ? true : dist(this.balloons[i].pos.x, this.balloons[i].pos.y, m.x, m.y) < m.range;
Checks if the monkey is in range: Snipers always hit, others only hit if distance is less than their range
this.balloons[i].health -= m.damage;
Subtracts the monkey's damage from the balloon's health
if (this.balloons[i].health <= 0) {
If the balloon's health reaches zero or below, it dies
this.money += this.balloons[i].value;
Awards the player money equal to the balloon's value
this.balloons.splice(i, 1);
Removes the dead balloon from the array
break;
Stops checking other monkeys for this balloon since it already took damage
if (i < this.balloons.length && this.balloons[i].index >= path.length - 1) {
Checks if the balloon reached the end of the path (only if it still exists after the collision loop)
this.lives--;
Subtracts one life when a balloon escapes
if (frameCount % 600 === 0) {
Every 600 frames (10 seconds at 60 FPS), advance the round
this.spawnInterval = max(10, this.spawnInterval - 2);
Makes balloons spawn faster each round by reducing the interval, but never below 10

Game.show()

show() iterates through all active balloons and monkeys and tells each to draw itself. This is a common pattern: the main object asks all its children to render.

  show() {
    for (let b of this.balloons) b.show();
    for (let m of this.monkeys) m.show();
  }
Line-by-line explanation (2 lines)
for (let b of this.balloons) b.show();
Loops through all balloons and calls their show() method to draw each one at its current position
for (let m of this.monkeys) m.show();
Loops through all monkeys and calls their show() method to draw each one with its range indicator

Game.placeMonkey()

placeMonkey() is called when the player clicks on an empty spot. It checks if the player has enough money, creates a new Monkey object, and deducts the cost.

  placeMonkey(x, y, type) {
    let cost = 50;
    if (this.money >= cost) {
      this.monkeys.push(new Monkey(x, y, type));
      this.money -= cost;
    }
  }
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Check if player can afford the tower if (this.money >= cost) {

Only places a monkey if the player has enough money

let cost = 50;
Sets the cost of placing a monkey to 50 money
if (this.money >= cost) {
Checks if the player has at least 50 money
this.monkeys.push(new Monkey(x, y, type));
Creates a new Monkey at the clicked position with the selected type and adds it to the monkeys array
this.money -= cost;
Subtracts 50 money from the player's total

Game.tryUpgradeMonkey()

tryUpgradeMonkey() checks if the player clicked on an existing monkey. If they did and they can afford the upgrade, it opens the upgrade menu and charges them. It returns true/false to signal whether a monkey was clicked.

  tryUpgradeMonkey(x, y) {
    for (let m of this.monkeys) {
      if (dist(x, y, m.x, m.y) < m.range) {
        if (this.money >= upgradeCost) {
          m.upgradeMenu();
          this.money -= upgradeCost;
        }
        return true;
      }
    }
    return false;
  }
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Check each monkey for click for (let m of this.monkeys) {

Loops through all monkeys to see if the click is on any of them

conditional Check if click is within monkey's range if (dist(x, y, m.x, m.y) < m.range) {

Uses distance to determine if the click is on this monkey

conditional Check if player can afford upgrade if (this.money >= upgradeCost) {

Only upgrades if the player has enough money

for (let m of this.monkeys) {
Loops through each monkey in the monkeys array
if (dist(x, y, m.x, m.y) < m.range) {
Checks if the click position is within the monkey's range using distance calculation
if (this.money >= upgradeCost) {
Checks if the player has enough money to afford the upgrade cost
m.upgradeMenu();
Calls the monkey's upgradeMenu() method to let the player choose what to upgrade
this.money -= upgradeCost;
Deducts the upgrade cost from the player's money
return true;
Returns true to signal that a monkey was found and clicked
return false;
If no monkey was clicked, returns false so mousePressed() will place a new one instead

Balloon() constructor

The Balloon constructor runs when a new balloon is created. It initializes all the properties: position, speed, health, value, and color based on the balloon's type and current round.

  constructor(type, round) {
    this.type = type;
    this.index = 0;
    this.pos = createVector(path[0].x, path[0].y);
    this.speed = 1.5 + round*0.05;
    this.health = this.setHealth(type, round);
    this.value = this.setValue(type);
    this.color = this.setColor(type);
  }
Line-by-line explanation (7 lines)
this.type = type;
Stores the balloon's type (Red, Blue, Green, Yellow, Lead, or Black)
this.index = 0;
Sets the starting position to the first waypoint in the path
this.pos = createVector(path[0].x, path[0].y);
Creates a p5.Vector at the first path waypoint, which will be updated as the balloon moves
this.speed = 1.5 + round*0.05;
Sets speed based on the round number; later rounds have faster balloons
this.health = this.setHealth(type, round);
Calls setHealth() to determine how many hits the balloon can take
this.value = this.setValue(type);
Calls setValue() to determine how much money the player gets for popping this balloon
this.color = this.setColor(type);
Calls setColor() to get the correct color for this balloon type

Balloon.setHealth()

setHealth() uses a switch statement to map each balloon type to a health value. Different balloon types have different health values, making them progressively harder to pop.

  setHealth(type, round){
    switch(type){
      case "Red": return 1;
      case "Blue": return 2;
      case "Green": return 3;
      case "Yellow": return 4;
      case "Lead": return 8;
      case "Black": return 5;
    }
  }
Line-by-line explanation (7 lines)

🔧 Subcomponents:

switch-case Map balloon type to health value switch(type){ ... }

Returns the correct health value for each balloon type

switch(type){
Starts a switch statement that checks the balloon type
case "Red": return 1;
Red balloons have 1 health and die in one hit
case "Blue": return 2;
Blue balloons have 2 health and need 2 hits to pop
case "Green": return 3;
Green balloons have 3 health
case "Yellow": return 4;
Yellow balloons have 4 health
case "Lead": return 8;
Lead balloons are the toughest with 8 health
case "Black": return 5;
Black balloons have 5 health

Balloon.setValue()

setValue() returns how much money the player earns for popping each balloon type. Tougher balloons are worth more money, encouraging risky plays.

  setValue(type){
    switch(type){
      case "Red": return 5;
      case "Blue": return 6;
      case "Green": return 7;
      case "Yellow": return 10;
      case "Lead": return 15;
      case "Black": return 12;
    }
  }
Line-by-line explanation (4 lines)

🔧 Subcomponents:

switch-case Map balloon type to money value switch(type){ ... }

Returns the correct money reward for each balloon type

case "Red": return 5;
Popping a Red balloon earns you 5 money
case "Blue": return 6;
Blue balloons give 6 money
case "Yellow": return 10;
Yellow balloons are worth 10 money
case "Lead": return 15;
Lead balloons are the most valuable at 15 money—rewarding you for popping tough balloons

Balloon.setColor()

setColor() returns the p5.js color object for each balloon type using RGB values. Different colors make it easy for the player to see which balloons are tougher at a glance.

  setColor(type){
    switch(type){
      case "Red": return color(255,0,0);
      case "Blue": return color(0,0,255);
      case "Green": return color(0,255,0);
      case "Yellow": return color(255,255,0);
      case "Lead": return color(100);
      case "Black": return color(0);
    }
  }
Line-by-line explanation (3 lines)

🔧 Subcomponents:

switch-case Map balloon type to color switch(type){ ... }

Returns the correct fill color for each balloon type

case "Red": return color(255,0,0);
Creates a pure red color (full red, no green, no blue)
case "Blue": return color(0,0,255);
Creates a pure blue color
case "Lead": return color(100);
Creates a dark gray color (100 is the same for R, G, and B)

Balloon.move()

move() uses vector math to smoothly glide the balloon from waypoint to waypoint. It calculates the direction to the next waypoint, scales it by speed, and updates the position each frame. When close enough, it advances to the next waypoint.

🔬 This entire function moves the balloon step-by-step along the path. What happens if you remove the normalize() call? Hint: normalize() makes the direction vector length 1; without it, longer path segments would move faster.

  move(){
    if (this.index >= path.length-1) return;
    let target = createVector(path[this.index+1].x, path[this.index+1].y);
    let dir = p5.Vector.sub(target, this.pos);
    dir.normalize();
    dir.mult(this.speed);
    this.pos.add(dir);
    if (p5.Vector.dist(this.pos, target) < this.speed) this.index++;
  }
  move(){
    if (this.index >= path.length-1) return;
    let target = createVector(path[this.index+1].x, path[this.index+1].y);
    let dir = p5.Vector.sub(target, this.pos);
    dir.normalize();
    dir.mult(this.speed);
    this.pos.add(dir);
    if (p5.Vector.dist(this.pos, target) < this.speed) this.index++;
  }
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Check if balloon reached the end if (this.index >= path.length-1) return;

Stops moving if the balloon has reached the final waypoint

calculation Calculate direction vector let dir = p5.Vector.sub(target, this.pos); dir.normalize(); dir.mult(this.speed);

Finds the direction from the balloon to the next waypoint and scales it by speed

calculation Move toward next waypoint this.pos.add(dir);

Updates the balloon's position by moving it in the direction of the target

conditional Check if reached next waypoint if (p5.Vector.dist(this.pos, target) < this.speed) this.index++;

Advances to the next waypoint when the balloon gets close enough

if (this.index >= path.length-1) return;
If the balloon has reached the last waypoint, stop moving (return exits the function)
let target = createVector(path[this.index+1].x, path[this.index+1].y);
Gets the coordinates of the next waypoint as a vector
let dir = p5.Vector.sub(target, this.pos);
Creates a direction vector by subtracting the balloon's current position from the target
dir.normalize();
Scales the direction vector to length 1, so it only shows direction, not distance
dir.mult(this.speed);
Multiplies the direction by speed to move the balloon the right amount each frame
this.pos.add(dir);
Adds the scaled direction to the balloon's position, moving it one step forward
if (p5.Vector.dist(this.pos, target) < this.speed) this.index++;
If the balloon is now very close to the target (within speed pixels), advance to the next waypoint

Balloon.show()

show() draws the balloon as an ellipse (oval) at its current position with the appropriate color. It runs every frame as part of game.show().

  show(){
    fill(this.color);
    ellipse(this.pos.x, this.pos.y, 30, 40);
  }
Line-by-line explanation (2 lines)
fill(this.color);
Sets the fill color to the balloon's color (set in the constructor)
ellipse(this.pos.x, this.pos.y, 30, 40);
Draws an oval that is 30 pixels wide and 40 pixels tall at the balloon's current position

Monkey() constructor

The Monkey constructor runs when a new monkey is placed. It stores the position, sets initial damage and range based on type, and initializes an upgrades tracker.

  constructor(x, y, type){
    this.x = x;
    this.y = y;
    this.range = type === "Sniper" ? width*2 : 50;
    this.damage = type==="Super"? 5 : 1;
    this.type = type; // Dart, Sniper, Super
    this.upgrades = [0,0,0];
  }
Line-by-line explanation (6 lines)
this.x = x;
Stores the X position where the monkey is placed
this.y = y;
Stores the Y position where the monkey is placed
this.range = type === "Sniper" ? width*2 : 50;
Sniper monkeys have infinite range (twice the canvas width), others have range 50
this.damage = type==="Super"? 5 : 1;
Super monkeys deal 5 damage per hit, all others deal 1 damage
this.type = type;
Stores the monkey type (Dart, Sniper, or Super)
this.upgrades = [0,0,0];
Creates an array to track how many times each upgrade path has been chosen [damage, range, type]

Monkey.show()

show() draws the monkey as a colored circle with eyes and a range indicator. The color changes based on type, and the range circle gets larger as you upgrade.

  show(){
    if(this.type==="Dart") fill(150,75,0);
    if(this.type==="Sniper") fill(0,150,150);
    if(this.type==="Super") fill(255,150,0);
    ellipse(this.x,this.y,40,40);

    // eyes
    fill(0);
    ellipse(this.x,this.y,10,10);

    // range
    noFill();
    stroke(0,100);
    ellipse(this.x,this.y,this.range*2);
    noStroke();
  }
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Set body color based on type if(this.type==="Dart") fill(150,75,0);

Different monkey types get different colors so the player can tell them apart

calculation Draw monkey body ellipse(this.x,this.y,40,40);

Draws the main body as a circle

calculation Draw eyes ellipse(this.x,this.y,10,10);

Draws a small black circle for eyes

calculation Draw range indicator ellipse(this.x,this.y,this.range*2);

Draws an outline circle showing the monkey's attack range

if(this.type==="Dart") fill(150,75,0);
If this is a Dart monkey, set the fill color to brown
if(this.type==="Sniper") fill(0,150,150);
If this is a Sniper monkey, set the fill color to cyan
if(this.type==="Super") fill(255,150,0);
If this is a Super monkey, set the fill color to orange
ellipse(this.x,this.y,40,40);
Draws a circle 40 pixels in diameter at the monkey's position as the body
fill(0);
Changes fill color to black for the eyes
ellipse(this.x,this.y,10,10);
Draws a small black circle (the eyes) on top of the body
noFill();
Removes fill so the next shape is just an outline
stroke(0,100);
Sets the outline color to semi-transparent black
ellipse(this.x,this.y,this.range*2);
Draws an outline circle showing the monkey's attack range—larger for upgraded monkeys

Monkey.upgradeMenu()

upgradeMenu() opens a dialog asking the player to choose an upgrade path (damage, range, or type). It validates the input and then calls applyUpgrade() to apply the chosen upgrade.

  upgradeMenu(){
    let pathChoice = prompt("Choose upgrade path 1=Damage,2=Range,3=Type");
    if(pathChoice && ["1","2","3"].includes(pathChoice)){
      let p = int(pathChoice)-1;
      this.upgrades[p]++;
      this.applyUpgrade(p);
    }
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Show upgrade choice dialog let pathChoice = prompt("Choose upgrade path 1=Damage,2=Range,3=Type");

Opens a dialog asking the player which upgrade they want

conditional Validate player input if(pathChoice && ["1","2","3"].includes(pathChoice)){

Checks that the player entered 1, 2, or 3 (not an invalid input)

calculation Apply the upgrade this.applyUpgrade(p);

Calls applyUpgrade() to actually modify the monkey's stats

let pathChoice = prompt("Choose upgrade path 1=Damage,2=Range,3=Type");
Shows a dialog box asking the player to enter 1, 2, or 3 for their choice
if(pathChoice && ["1","2","3"].includes(pathChoice)){
Checks two things: 1) pathChoice is not null (user didn't cancel), and 2) it contains 1, 2, or 3
let p = int(pathChoice)-1;
Converts the string choice to a number and subtracts 1, giving 0, 1, or 2 for array indexing
this.upgrades[p]++;
Increments the count for this upgrade path in the upgrades array
this.applyUpgrade(p);
Calls applyUpgrade() to actually change the monkey's damage, range, or type

Monkey.applyUpgrade()

applyUpgrade() applies the chosen upgrade by modifying the monkey's damage, range, or type. The type upgrade cycles through all three monkey types, and Snipers get special infinite range when promoted.

🔬 The damage upgrade adds 1, range adds 20. What happens if you change case 1 to add 50 range instead of 20?

  applyUpgrade(path){
    switch(path){
      case 0: this.damage += 1; break;
      case 1: this.range += 20; break;
      case 2:
        if(this.type==="Dart") this.type="Sniper";
        else if(this.type==="Sniper") this.type="Super";
        else this.type="Dart";
        if(this.type==="Sniper") this.range = width*2;
        break;
    }
  }
  applyUpgrade(path){
    switch(path){
      case 0: this.damage += 1; break;
      case 1: this.range += 20; break;
      case 2:
        if(this.type==="Dart") this.type="Sniper";
        else if(this.type==="Sniper") this.type="Super";
        else this.type="Dart";
        if(this.type==="Sniper") this.range = width*2;
        break;
    }
  }
Line-by-line explanation (7 lines)

🔧 Subcomponents:

switch-case Increase damage by 1 case 0: this.damage += 1; break;

Upgrade path 1: adds 1 damage to every hit

switch-case Increase range by 20 case 1: this.range += 20; break;

Upgrade path 2: expands the attack range by 20 pixels

switch-case Cycle through monkey types case 2: if(this.type==="Dart") this.type="Sniper"; ...

Upgrade path 3: evolves the monkey through Dart → Sniper → Super → Dart

switch(path){
Starts a switch statement checking which upgrade path (0, 1, or 2) was chosen
case 0: this.damage += 1; break;
If path 0 (damage), add 1 to the monkey's damage value
case 1: this.range += 20; break;
If path 1 (range), add 20 to the monkey's range
if(this.type==="Dart") this.type="Sniper";
If the monkey is currently Dart type, change it to Sniper
else if(this.type==="Sniper") this.type="Super";
If it's Sniper, change it to Super
else this.type="Dart";
If it's Super, change it back to Dart (cycling through all types)
if(this.type==="Sniper") this.range = width*2;
Special rule: if the type becomes Sniper after upgrade, set its range to infinite (twice the canvas width)

BalloonFactory()

BalloonFactory() uses weighted random selection to spawn different balloon types with different probabilities. Red is common, tough balloons are rare. This pattern is great for games where you want to control variety.

🔬 This factory controls the probability of each balloon type spawning. What happens if you change the first condition to if (typeChance < 0.8) instead of 0.5? Hint: more Red balloons will spawn.

function BalloonFactory(round) {
  let typeChance = random(1);
  if (typeChance < 0.5) return new Balloon("Red", round);
  else if (typeChance < 0.7) return new Balloon("Blue", round);
  else if (typeChance < 0.85) return new Balloon("Green", round);
  else if (typeChance < 0.95) return new Balloon("Yellow", round);
  else return new Balloon(random(["Lead", "Black"]), round);
}
function BalloonFactory(round) {
  let typeChance = random(1);
  if (typeChance < 0.5) return new Balloon("Red", round);
  else if (typeChance < 0.7) return new Balloon("Blue", round);
  else if (typeChance < 0.85) return new Balloon("Green", round);
  else if (typeChance < 0.95) return new Balloon("Yellow", round);
  else return new Balloon(random(["Lead", "Black"]), round);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Generate random value let typeChance = random(1);

Gets a random number between 0 and 1 for determining balloon type

conditional Select balloon type based on probability if (typeChance < 0.5) return new Balloon("Red", round);

Red is most common (50% chance), others progressively rarer

let typeChance = random(1);
Generates a random number between 0 and 1
if (typeChance < 0.5) return new Balloon("Red", round);
50% of the time (when random is less than 0.5), create a Red balloon
else if (typeChance < 0.7) return new Balloon("Blue", round);
20% of the time (0.5 to 0.7), create a Blue balloon
else if (typeChance < 0.85) return new Balloon("Green", round);
15% of the time (0.7 to 0.85), create a Green balloon
else if (typeChance < 0.95) return new Balloon("Yellow", round);
10% of the time (0.85 to 0.95), create a Yellow balloon
else return new Balloon(random(["Lead", "Black"]), round);
5% of the time (0.95 to 1.0), create either a Lead or Black balloon randomly

📦 Key Variables

game object

Holds the Game instance that manages all balloons, monkeys, money, and game state

let game;
path array

Array of waypoint objects that define the winding path balloons follow

let path = [{x: 0, y: 300}, {x: 100, y: 300}, ...]
selectedMonkeyType string

Tracks which monkey type (Dart, Sniper, Super) the player has selected to place next

let selectedMonkeyType = "Dart";
upgradeCost number

How much money it costs to upgrade a monkey's damage, range, or type

let upgradeCost = 50;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG Game.update() - collision detection with splicing

When a balloon is spliced during the collision loop (line removing it), the outer loop's index becomes invalid. If a balloon dies, we break, but if we remove it via splice, subsequent monkeys might iterate past array bounds.

💡 The current code is safe because it checks `if (i < this.balloons.length)` after the monkey loop before checking escape. However, if a balloon is removed by a monkey, the break statement exits the monkey loop, preventing further damage checks on that frame. This is intentional—consider documenting it.

BUG Balloon.move() - waypoint advancement logic

If balloon speed is very high or waypoints are very close, the balloon might skip waypoints entirely instead of hitting them in order

💡 Add a check to ensure the balloon hits the exact waypoint before moving to the next: use a clamped distance or check if the balloon has passed the target point

STYLE Game.update() - win/lose conditions

The win/lose text drawing happens inside the update() method, which mixes game logic with rendering—violates separation of concerns

💡 Move the text rendering to draw() or a separate drawGameEnd() function; update() should only handle logic, not display

FEATURE Game class

There is no way to pause or restart the game—once started, you cannot reset without reloading the page

💡 Add a pause boolean, a restart button, or keyboard shortcut (e.g., press R to restart) to improve user experience

PERFORMANCE Game.update() - nested loop collision detection

Checking every balloon against every monkey is O(n*m), which becomes slow with many entities. Once a monkey hits a balloon, it should not be checked by other monkeys

💡 The break statement already exits the monkey loop, so this is optimized. However, if you add many towers, consider using spatial partitioning or a quadtree for better scaling

STYLE Monkey.upgradeMenu()

Using prompt() is crude and breaks the visual cohesion of the game—it opens a browser dialog that disrupts immersion

💡 Implement an on-canvas UI menu using p5.js buttons or clickable regions to keep the player in the game world

🔄 Code Flow

Code flow showing setup, draw, keypressed, mousepressed, drawpath, drawcontrols, gameconstructor, gameupdate, gameshow, gameplaceMonkey, gametryupgrademonkey, balloonconstractor, balloonsethealth, balloonsetvalue, balloonsetcolor, balloonmove, balloonshow, monkeyconstractor, monkeyshow, monkeyupgrademenu, monkeyapplyupgrade, balloonfactory

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> gameconstructor[gameconstructor] gameconstructor --> draw[draw loop] draw --> clearcanvas[clear-canvas] clearcanvas --> renderpath[render-path] renderpath --> pathstyle[path-style] pathstyle --> pathloop[path-loop] renderpath --> gamelogic[game-logic] gamelogic --> spawnlogic[spawn-logic] spawnlogic --> balloonfactory[balloonfactory] gamelogic --> balloonloop[balloon-loop] balloonloop --> collisioncheck[collision-check] collisioncheck --> escapecheck[escape-check] escapecheck --> roundincrement[round-increment] roundincrement --> wincondition[win-condition] wincondition --> losecondition[lose-condition] gamelogic --> renderentities[render-entities] renderentities --> gameshow[gameshow] gameshow --> monkeyloop[monkey-loop] monkeyloop --> clickdetect[click-detect] renderentities --> renderhud[render-hud] renderhud --> hudposition[hud-position] hudposition --> statictext[static-text] statictext --> dynamicstats[dynamic-stats] draw --> keypressed[keypressed] keypressed --> dartselect[dart-select] dartselect --> sniperselect[sniper-select] sniperselect --> superselect[super-select] draw --> mousepressed[mousepressed] mousepressed --> upgradecheck[upgrade-check] upgradecheck --> gametryupgrademonkey[gamelogic] gametryupgrademonkey --> gameplaceMonkey[gameplaceMonkey] gameplaceMonkey --> affordcheck[afford-check] affordcheck --> monkeyconstractor[monkeyconstractor] monkeyconstractor --> monkeyshow[monkeyshow] monkeyshow --> monkeyupgrademenu[monkeyupgrademenu] monkeyupgrademenu --> inputprompt[input-prompt] inputprompt --> validation[validation] validation --> applyupgrade[apply-upgrade] applyupgrade --> damageupgrade[damage-upgrade] applyupgrade --> rangeupgrade[range-upgrade] applyupgrade --> typeupgrade[type-upgrade] balloonfactory --> balloonconstractor[balloonconstractor] balloonconstractor --> balloonsethealth[balloonsethealth] balloonsethealth --> healthswitch[health-switch] balloonconstractor --> balloonsetvalue[balloonsetvalue] balloonsetvalue --> valueswitch[value-switch] balloonconstractor --> balloonsetcolor[balloonsetcolor] balloonsetcolor --> colorswitch[color-switch] balloonconstractor --> balloonmove[balloonmove] balloonmove --> directioncalc[direction-calc] directioncalc --> movement[movement] movement --> waypointcheck[waypoint-check] balloonmove --> pathcompletecheck[path-complete-check] balloonshow --> balloonshow[balloonshow] balloonshow --> balloonloop[balloon-loop] click setup href "#fn-setup" click gameconstructor href "#fn-gameconstructor" click draw href "#fn-draw" click clearcanvas href "#sub-clear-canvas" click renderpath href "#sub-render-path" click pathstyle href "#sub-path-style" click pathloop href "#sub-path-loop" click gamelogic href "#sub-game-logic" click spawnlogic href "#sub-spawn-logic" click balloonfactory href "#fn-balloonfactory" click balloonloop href "#sub-balloon-loop" click collisioncheck href "#sub-collision-check" click escapecheck href "#sub-escape-check" click roundincrement href "#sub-round-increment" click wincondition href "#sub-win-condition" click losecondition href "#sub-lose-condition" click renderentities href "#sub-render-entities" click gameshow href "#fn-gameshow" click monkeyloop href "#sub-monkey-loop" click clickdetect href "#sub-click-detect" click renderhud href "#sub-render-hud" click hudposition href "#sub-hud-position" click statictext href "#sub-static-text" click dynamicstats href "#sub-dynamic-stats" click keypressed href "#fn-keypressed" click dartselect href "#sub-dart-select" click sniperselect href "#sub-sniper-select" click superselect href "#sub-super-select" click mousepressed href "#fn-mousepressed" click upgradecheck href "#sub-upgrade-check" click gametryupgrademonkey href "#fn-gametryupgrademonkey" click gameplaceMonkey href "#fn-gameplaceMonkey" click affordcheck href "#sub-afford-check" click monkeyconstractor href "#fn-monkeyconstractor" click monkeyshow href "#fn-monkeyshow" click monkeyupgrademenu href "#fn-monkeyupgrademenu" click inputprompt href "#sub-input-prompt" click validation href "#sub-validation" click applyupgrade href "#sub-apply-upgrade" click damageupgrade href "#sub-damage-upgrade" click rangeupgrade href "#sub-range-upgrade" click typeupgrade href "#sub-type-upgrade" click balloonconstractor href "#fn-balloonconstractor" click balloonsethealth href "#fn-balloonsethealth" click healthswitch href "#sub-health-switch" click balloonsetvalue href "#fn-balloonsetvalue" click valueswitch href "#sub-value-switch" click balloonsetcolor href "#fn-balloonsetcolor" click colorswitch href "#sub-color-switch" click balloonmove href "#fn-balloonmove" click directioncalc href "#sub-direction-calc" click movement href "#sub-movement" click waypointcheck href "#sub-waypoint-check" click pathcompletecheck href "#sub-path-complete-check" click balloonshow href "#fn-balloonshow"

❓ Frequently Asked Questions

What visual experience does the btd clone lol sketch provide?

This sketch creates a vibrant tower-defense scene featuring colorful balloons traveling along a winding path, while various monkey types attempt to pop them.

How can players interact with the btd clone lol tower-defense game?

Players can interact by using number keys to switch between monkey types and clicking to place or upgrade monkeys as they defend against waves of balloons.

What creative coding concepts are showcased in the btd clone lol sketch?

The sketch demonstrates concepts such as object-oriented programming with the Game class, animation through the draw loop, and user input handling for dynamic gameplay.

Preview

btd clone lol - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of btd clone lol - Code flow showing setup, draw, keypressed, mousepressed, drawpath, drawcontrols, gameconstructor, gameupdate, gameshow, gameplaceMonkey, gametryupgrademonkey, balloonconstractor, balloonsethealth, balloonsetvalue, balloonsetcolor, balloonmove, balloonshow, monkeyconstractor, monkeyshow, monkeyupgrademenu, monkeyapplyupgrade, balloonfactory
Code Flow Diagram