Astro bot

This sketch creates a full platformer game called Astro Bot where players navigate through 5 themed worlds, rescue bots, defeat enemies, and fight bosses. The game features a hub world for level selection, animated platforms, touch controls, particle effects, and a complete game loop with lives, scoring, and world progression.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change the jump height — Higher JUMP_FORCE makes the player jump higher and stay in the air longer—try -20 for a super-jump
  2. Make enemies move faster — Enemies patrol at their initial vx value—increase it to make them faster and more dangerous
  3. Add more bots to rescue — More bots per level means the player has to explore more and the level takes longer
  4. Make the boss weaker — Lower bossMaxHealth means the boss takes fewer hits to defeat
  5. Change the background color — The hub background is dark brown—try any RGB values like 20, 40, 80 for deep blue
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a complete platformer game in p5.js where Astro Bot must rescue stranded bots across five themed worlds and defeat bosses to progress. The game combines physics simulation (gravity, jumping, platform collision), visual effects (particle explosions, animated characters, starfield scrolling), touch controls with a virtual joystick, and enemy AI to create an engaging interactive experience. The code demonstrates professional game architecture with state management, level generation, and player interaction.

The sketch is organized into three major sections: a hub world for navigation, a level system with platforms and enemies, and a boss battle mode. A central updateGame() function handles physics and collision detection each frame, while specialized drawing functions render each game state. By studying this code you will learn how to structure complex interactive projects, manage game state, implement touch controls, detect collisions between moving objects, and create satisfying visual feedback through particles and screen shake.

⚙️ How It Works

  1. When the sketch loads, setup() creates the canvas, initializes a starfield, creates the player object, and sets up touch control buttons positioned at the bottom of the screen. The gameState starts as 'hub' so the player sees a desert world with animated ships representing each of the five themed worlds.
  2. The draw() function runs 60 times per second and uses a switch statement to branch on gameState: 'hub' displays the world selection screen, 'level' runs gameplay with platforms and enemies, 'boss' runs a special level with a boss fight, and 'win' shows the victory screen.
  3. In the 'level' gameState, updateGame() applies gravity to the player, moves the player based on joystick input, checks for collisions with platforms (to keep the player from falling), checks for collisions with enemies (causing damage or defeating them), and checks for collisions with bots to rescue them. The camera scrolls to follow the player as they move through a level wider than the screen.
  4. When all bots in a level are rescued, the gameState switches to 'boss' and initBoss() creates a boss enemy with its own AI. The boss moves in patterns and charges at the player, while the player defeats it by punching or spinning when invincible frames end.
  5. Drawing happens in drawLevel() which renders platforms with highlights, enemies with animated eyes, bots with glowing auras, the player as Astro Bot, and a camera-scrolled view. Touch controls and particles are drawn on top, and the HUD shows lives, score, and world name.
  6. When the boss is defeated, currentWorld increments and gameState returns to 'hub'. When all five worlds are complete, gameState becomes 'win' and the victory screen displays the total score and bots rescued.

🎓 Concepts You'll Learn

Game state management (hub, level, boss, win)Physics simulation with gravity and velocityCollision detection (platforms, enemies, bots, boss)Touch controls and virtual joystickParticle systems and visual effectsCamera scrolling and viewport managementEnemy AI and behavior patternsAnimation loops and frame-based animation

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch first loads. It is the perfect place to initialize arrays, objects, and the canvas. Everything that happens before the game loop starts goes here.

function setup() {
  createCanvas(windowWidth, windowHeight);
  textAlign(CENTER, CENTER);
  initStars();
  initPlayer();
  initTouchControls();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

function-call Canvas setup createCanvas(windowWidth, windowHeight);

Creates a full-screen canvas that fills the entire browser window

function-call Text centering textAlign(CENTER, CENTER);

Makes all text draw centered on its x,y position rather than from the top-left

function-call System initialization initStars(); initPlayer(); initTouchControls();

Calls helper functions to set up the starfield, player object, and on-screen control buttons

createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas that stretches to fill the entire browser window, making the game responsive to screen size
textAlign(CENTER, CENTER);
Tells p5.js to draw all text centered horizontally and vertically on the coordinates you give it (instead of left-aligned)
initStars();
Calls the function that fills the stars array with 80 randomly positioned stars for the scrolling starfield background
initPlayer();
Calls the function that creates the player object with position, velocity, dimensions, and animation properties
initTouchControls();
Calls the function that positions the joystick and action buttons based on screen size

initStars()

This function populates the stars array with random star objects. Each star has position (x, y), size (s), and scroll speed (sp). The stars are used in drawStarfield() to create a parallax scrolling background effect that makes the game world feel deeper.

🔬 This loop creates all the stars. What happens if you change random(1, 3) to random(5, 10)? What about changing random(0.5, 2) to random(0.1, 0.3)?

  for (let i = 0; i < 80; i++) {
    stars.push({ x: random(width * 2), y: random(height), s: random(1, 3), sp: random(0.5, 2) });
  }
function initStars() {
  stars = [];
  for (let i = 0; i < 80; i++) {
    stars.push({ x: random(width * 2), y: random(height), s: random(1, 3), sp: random(0.5, 2) });
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

assignment Clear array stars = [];

Empties the stars array to start fresh (used both at startup and when window resizes)

for-loop Create 80 stars for (let i = 0; i < 80; i++) {

Loops 80 times, creating one random star object each iteration

calculation Star properties stars.push({ x: random(width * 2), y: random(height), s: random(1, 3), sp: random(0.5, 2) });

Creates a star object with x/y position, size (s), and scroll speed (sp), then adds it to the stars array

stars = [];
Resets the stars array to empty—this runs both at startup and when the window is resized
for (let i = 0; i < 80; i++) {
Loops 80 times with i counting from 0 to 79, creating one star per iteration
stars.push({ x: random(width * 2), y: random(height), s: random(1, 3), sp: random(0.5, 2) });
Creates a star object with four properties: x position (0 to twice screen width), y position (anywhere on screen), size s (1 to 3 pixels), and scroll speed sp (0.5 to 2 pixels per frame). The push() method adds it to the stars array.

initPlayer()

initPlayer() creates the player object with all the properties needed for movement, collision, animation, and action states. This single object holds everything about the player's status. Most of these properties are read and modified constantly in updateGame() and drawAstroBot().

function initPlayer() {
  player = {
    x: width / 2, y: height - 100,
    vx: 0, vy: 0,
    w: 30, h: 40,
    onGround: false,
    facing: 1,
    punching: 0,
    spinning: 0,
    invincible: 0,
    animFrame: 0
  };
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

assignment Starting position x: width / 2, y: height - 100,

Places the player in the center horizontally and 100 pixels up from the bottom of the canvas

assignment Starting velocity vx: 0, vy: 0,

Sets horizontal and vertical velocity to zero so the player starts stationary

assignment Player size w: 30, h: 40,

Width of 30 pixels and height of 40 pixels—used for collision detection and drawing

assignment Action states onGround: false, facing: 1, punching: 0, spinning: 0, invincible: 0, animFrame: 0

Tracks whether the player is on a platform, which direction they face (1 or -1), punch timer, spin timer, invincibility timer, and animation frame counter

player = {
Creates a new object called player that holds all the data about the player character
x: width / 2, y: height - 100,
Sets starting position to the center of the canvas horizontally (width / 2) and 100 pixels from the bottom
vx: 0, vy: 0,
Initializes velocity to zero—vx controls left/right speed, vy controls up/down speed (affected by gravity)
w: 30, h: 40,
Sets the player's collision box to 30 pixels wide and 40 pixels tall
onGround: false,
A boolean flag that tracks whether the player is touching a platform (needed to allow jumping)
facing: 1,
Stores the direction the player is facing: 1 for right, -1 for left (used to flip the character visually)
punching: 0,
A timer that counts down from 15 when the punch button is pressed—used to draw the punch animation and detect punch hits
spinning: 0,
A timer that counts down from 30 when the spin button is pressed—used to draw spinning animation and defeat enemies
invincible: 0,
A timer that counts down after taking damage—while above 0, the player cannot take more damage and flashes on screen
animFrame: 0
A counter that advances each frame to animate the player's legs walking—it cycles continuously

initTouchControls()

This function positions four interactive controls: a left-side joystick for movement and three right-side buttons for jump, punch, and spin. All positions are calculated as percentages of screen size, so they scale correctly on phones, tablets, and desktops. The joystick is larger and centered for analog input, while the buttons are smaller and stacked.

🔬 The right-side buttons are positioned using formulas with bSize multipliers. What happens if you change the y positions so the buttons move higher or lower? Try changing height - margin - bSize to height - margin * 3 - bSize for all three.

  joystick = { x: margin + bSize, y: height - margin - bSize, r: bSize, dx: 0, dy: 0, active: false };
  jumpBtn = { x: width - margin - bSize * 2.2, y: height - margin - bSize, r: bSize * 0.7, label: '↑' };
  punchBtn = { x: width - margin - bSize * 0.6, y: height - margin - bSize * 1.8, r: bSize * 0.7, label: '👊' };
  spinBtn = { x: width - margin - bSize * 0.6, y: height - margin - bSize * 0.3, r: bSize * 0.7, label: '🌀' };
function initTouchControls() {
  let bSize = min(width, height) * 0.12;
  let margin = bSize * 0.6;
  joystick = { x: margin + bSize, y: height - margin - bSize, r: bSize, dx: 0, dy: 0, active: false };
  jumpBtn = { x: width - margin - bSize * 2.2, y: height - margin - bSize, r: bSize * 0.7, label: '↑' };
  punchBtn = { x: width - margin - bSize * 0.6, y: height - margin - bSize * 1.8, r: bSize * 0.7, label: '👊' };
  spinBtn = { x: width - margin - bSize * 0.6, y: height - margin - bSize * 0.3, r: bSize * 0.7, label: '🌀' };
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Button sizing let bSize = min(width, height) * 0.12;

Calculates button size as 12% of the smaller screen dimension, making buttons responsive to screen size

calculation Edge spacing let margin = bSize * 0.6;

Calculates how far buttons sit from the screen edges (60% of button size)

assignment Joystick setup joystick = { x: margin + bSize, y: height - margin - bSize, r: bSize, dx: 0, dy: 0, active: false };

Creates the left-side joystick with position (x, y), radius (r), and input values (dx, dy)

assignment Action buttons jumpBtn = { x: width - margin - bSize * 2.2, y: height - margin - bSize, r: bSize * 0.7, label: '↑' };

Creates three circular buttons on the right side (jump, punch, spin) with their positions, sizes, and emoji labels

let bSize = min(width, height) * 0.12;
Calculates a responsive button size: 12% of whichever is smaller (width or height). This keeps buttons sized well on any device.
let margin = bSize * 0.6;
Calculates margin as 60% of button size, so buttons sit a consistent distance from screen edges
joystick = { x: margin + bSize, y: height - margin - bSize, r: bSize, dx: 0, dy: 0, active: false };
Creates the joystick object positioned in the bottom-left, with radius bSize, starting dx/dy at 0 (neutral), and active as false
jumpBtn = { x: width - margin - bSize * 2.2, y: height - margin - bSize, r: bSize * 0.7, label: '↑' };
Creates the jump button in the bottom-right with radius 70% of bSize and a up-arrow label
punchBtn = { x: width - margin - bSize * 0.6, y: height - margin - bSize * 1.8, r: bSize * 0.7, label: '👊' };
Creates the punch button above the jump button with a fist emoji label
spinBtn = { x: width - margin - bSize * 0.6, y: height - margin - bSize * 0.3, r: bSize * 0.7, label: '🌀' };
Creates the spin button above the punch button with a spinning emoji label

initLevel()

initLevel() prepares a brand-new level by clearing all arrays and creating random platforms, enemies, and bots. This function is called every time the player enters a level from the hub, and the randomization means the layout changes slightly each time. The level is three times the screen width, allowing the camera to scroll horizontally as the player explores.

🔬 This loop places 3 bots. What happens if you change the 3 to 6? What if you change random(200, width * 2) to random(100, width * 3) to spread bots further?

  // Place bots to rescue
  for (let i = 0; i < 3; i++) {
    bots.push({
      x: random(200, width * 2),
      y: random(100, height - 150),
      rescued: false,
      bobOffset: random(TWO_PI)
    });
  }
function initLevel() {
  platforms = [];
  enemies = [];
  bots = [];
  boss = null;
  player.x = 60;
  player.y = height - 120;
  player.vx = 0;
  player.vy = 0;

  // Ground
  platforms.push({ x: 0, y: height - 50, w: width * 3, h: 50 });

  // Generate platforms
  for (let i = 0; i < 12; i++) {
    platforms.push({
      x: random(50, width * 2.5),
      y: random(height * 0.3, height - 100),
      w: random(80, 180),
      h: 15
    });
  }

  // Place bots to rescue
  for (let i = 0; i < 3; i++) {
    bots.push({
      x: random(200, width * 2),
      y: random(100, height - 150),
      rescued: false,
      bobOffset: random(TWO_PI)
    });
  }

  // Place enemies
  for (let i = 0; i < 5; i++) {
    enemies.push({
      x: random(200, width * 2),
      y: height - 80,
      vx: random([-1.5, 1.5]),
      w: 25, h: 25,
      alive: true,
      type: floor(random(3))
    });
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

assignment Clear game arrays platforms = []; enemies = []; bots = []; boss = null;

Empties all level data so each new level starts fresh with no old enemies or platforms

assignment Reset player state player.x = 60; player.y = height - 120; player.vx = 0; player.vy = 0;

Places the player back at the starting position with zero velocity

function-call Create ground platforms.push({ x: 0, y: height - 50, w: width * 3, h: 50 });

Adds a long ground platform at the bottom that is three times the screen width (for camera scrolling)

for-loop Random platforms for (let i = 0; i < 12; i++) {

Loops 12 times to create randomly positioned and sized platforms at various heights

for-loop Place bots for (let i = 0; i < 3; i++) {

Loops 3 times to place three bots scattered across the level

for-loop Spawn enemies for (let i = 0; i < 5; i++) {

Loops 5 times to create five enemies with random starting velocities (left or right)

platforms = [];
Clears the platforms array so no platforms from the previous level remain
enemies = [];
Clears the enemies array to start with no enemies
bots = [];
Clears the bots array to start with no bots to rescue
boss = null;
Sets boss to null so the game does not try to draw or update a boss during level play
player.x = 60;
Places the player 60 pixels from the left edge of the screen
player.y = height - 120;
Places the player 120 pixels up from the bottom of the canvas
player.vx = 0; player.vy = 0;
Resets both velocity components to zero so the player doesn't enter the level already moving
platforms.push({ x: 0, y: height - 50, w: width * 3, h: 50 });
Creates a ground platform 50 pixels tall at the bottom of the screen, three times the screen width wide (allowing the camera to scroll and still have ground)
for (let i = 0; i < 12; i++) {
Loops 12 times, creating 12 randomly positioned platforms
x: random(50, width * 2.5),
Random x position between 50 and width * 2.5, spreading platforms across a horizontally scrollable level
y: random(height * 0.3, height - 100),
Random y position between 30% down the screen and 100 pixels from the bottom, placing platforms at various heights
w: random(80, 180),
Random width between 80 and 180 pixels, creating platforms of different lengths
for (let i = 0; i < 3; i++) {
Loops 3 times to place exactly three bots that need rescuing
bobOffset: random(TWO_PI)
Gives each bot a random phase offset for its bobbing animation so they don't all bob in sync
vx: random([-1.5, 1.5]),
Randomly selects from the array [-1.5, 1.5], making each enemy move either left or right at speed 1.5

initBoss()

initBoss() sets up a boss fight by creating the boss object (using data from the current world), resetting the player position, and redesigning the platforms into a simple arena with stepping stones. Each world has a different boss with a unique name and color. The boss starts with a timer that drives its movement patterns in updateBoss().

function initBoss() {
  let w = WORLDS[currentWorld];
  bossHealth = bossMaxHealth;
  boss = {
    x: width * 0.7, y: height * 0.4,
    w: 80, h: 80,
    vx: 2, vy: 0,
    phase: 0,
    timer: 0,
    hit: 0,
    name: w.boss,
    color: w.bossColor
  };
  platforms = [{ x: 0, y: height - 50, w: width, h: 50 }];
  for (let i = 0; i < 4; i++) {
    platforms.push({ x: 80 + i * (width / 4), y: height - 150 - i * 40, w: 120, h: 15 });
  }
  player.x = 60;
  player.y = height - 100;
  enemies = [];
  bots = [];
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

assignment Get world properties let w = WORLDS[currentWorld];

Retrieves the current world's data (name, color, boss name, boss color) from the WORLDS array

assignment Create boss boss = { x: width * 0.7, y: height * 0.4, w: 80, h: 80, vx: 2, vy: 0, phase: 0, timer: 0, hit: 0, name: w.boss, color: w.bossColor };

Creates the boss object with position, size, velocity, animation state, and appearance data from the world

assignment Boss arena platforms platforms = [{ x: 0, y: height - 50, w: width, h: 50 }];

Clears platforms and creates a simple arena with just the bottom ground platform

for-loop Add stepping stones for (let i = 0; i < 4; i++) {

Loops 4 times to add ascending platform steps for the player to climb during the boss fight

let w = WORLDS[currentWorld];
Gets the current world object from the WORLDS array, which contains the boss name and color
bossHealth = bossMaxHealth;
Resets the boss's health to full (100 points) at the start of the fight
boss = {
Creates a new boss object that holds all the data for the boss character
x: width * 0.7, y: height * 0.4,
Positions the boss at 70% across the screen width and 40% down from the top
w: 80, h: 80,
Makes the boss an 80x80 pixel square (much bigger than the player)
vx: 2, vy: 0,
Gives the boss initial horizontal velocity of 2 pixels per frame (moving right); vy starts at 0
phase: 0,
Sets the boss to phase 0, which is the starting movement pattern (pacing side to side)
timer: 0,
A counter that tracks frame progress in the current phase, used to trigger phase changes
hit: 0,
A timer that flashes the boss white when hit, making damage visually feedback
name: w.boss, color: w.bossColor
Copies the boss's name and color from the world data, so different worlds have different-looking bosses
platforms = [{ x: 0, y: height - 50, w: width, h: 50 }];
Clears all old platforms and creates a fresh arena with only the ground platform
for (let i = 0; i < 4; i++) {
Loops 4 times to add 4 step platforms for the player to climb
x: 80 + i * (width / 4), y: height - 150 - i * 40, w: 120, h: 15
Creates platforms spaced across the width (each one further right and higher than the last), forming a staircase for the player to climb toward the boss
player.x = 60; player.y = height - 100;
Resets the player to the start position at the left side of the arena
enemies = []; bots = [];
Clears enemies and bots so the boss fight is just the player versus the boss

draw()

draw() is the main game loop that runs 60 times per second. It first applies screen shake (if active), then uses a switch statement to branch on gameState and execute the appropriate functions. Touch controls and particles are drawn on top of everything. This function is the orchestrator of the entire game.

🔬 This switch routes to different functions based on gameState. Notice that 'level' and 'boss' both call updateGame(). Why do you think they share the same update? What would happen if you removed updateGame() from the 'boss' case?

  switch (gameState) {
    case 'hub': drawHub(); break;
    case 'level': updateGame(); drawLevel(); break;
    case 'boss': updateGame(); updateBoss(); drawLevel(); drawBossUI(); break;
    case 'win': drawWin(); break;
  }
function draw() {
  if (shakeAmount > 0) {
    translate(random(-shakeAmount, shakeAmount), random(-shakeAmount, shakeAmount));
    shakeAmount *= 0.9;
  }

  switch (gameState) {
    case 'hub': drawHub(); break;
    case 'level': updateGame(); drawLevel(); break;
    case 'boss': updateGame(); updateBoss(); drawLevel(); drawBossUI(); break;
    case 'win': drawWin(); break;
  }

  drawTouchControls();
  drawParticles();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Screen shake effect if (shakeAmount > 0) { translate(random(-shakeAmount, shakeAmount), random(-shakeAmount, shakeAmount)); shakeAmount *= 0.9; }

Creates a screen-shake effect that decays over time whenever an explosion or hit occurs

switch-case Game state routing switch (gameState) {

Branches to different drawing functions based on the current game state (hub, level, boss, or win)

if (shakeAmount > 0) {
Checks if shakeAmount is greater than zero (meaning the screen should be shaking)
translate(random(-shakeAmount, shakeAmount), random(-shakeAmount, shakeAmount));
Moves the entire canvas by a random amount within ±shakeAmount pixels in both x and y directions, creating the shake effect
shakeAmount *= 0.9;
Multiplies shakeAmount by 0.9, making it decay by 10% each frame so the shake gradually stops
switch (gameState) {
Evaluates the current gameState and branches to the correct code block for that state
case 'hub': drawHub(); break;
If gameState is 'hub', draw the world selection screen and stop processing other cases
case 'level': updateGame(); drawLevel(); break;
If gameState is 'level', update player physics and collision, then draw the level with platforms and enemies
case 'boss': updateGame(); updateBoss(); drawLevel(); drawBossUI(); break;
If gameState is 'boss', update the player, update the boss AI, draw the level, and draw the boss health bar
case 'win': drawWin(); break;
If gameState is 'win', draw the victory screen
drawTouchControls();
Draws the joystick and action buttons on top of all other elements (always drawn, regardless of game state)
drawParticles();
Updates and draws all active particles (explosions, sparks, etc.) on top of everything

drawHub()

drawHub() creates the main menu screen with a desert planet theme. It draws an animated starfield, procedurally generated sand dunes (using Perlin noise), world selection ships that bob up and down, and Astro Bot standing at the bottom. Locked worlds appear grayed out with a lock emoji. This is where the player starts each time they play.

🔬 The hubShipAngle controls the bobbing motion of the ships. What happens if you change 0.02 to 0.05? What if you change the 15 at the end to 30 or 5?

  hubShipAngle += 0.02;
  for (let i = 0; i < WORLDS.length; i++) {
    let wx = width * (i + 1) / (WORLDS.length + 1);
    let wy = height * 0.45 + sin(hubShipAngle + i) * 15;
function drawHub() {
  // Desert planet hub
  background(40, 25, 15);
  drawStarfield();

  // Sand dunes
  fill(180, 140, 80);
  noStroke();
  for (let i = 0; i < width; i += 2) {
    let h = noise(i * 0.005, frameCount * 0.002) * 80 + 40;
    rect(i, height - h, 2, h);
  }

  // Title
  fill(255, 220, 100);
  textSize(min(32, width / 14));
  textStyle(BOLD);
  text('ASTRO BOT', width / 2, 50);
  textStyle(NORMAL);
  textSize(min(14, width / 28));
  fill(200, 180, 140);
  text('Tap a world to begin your mission!', width / 2, 85);
  text(`Bots Rescued: ${botsRescued} | Lives: ${lives}`, width / 2, 110);

  // World selection - ships
  hubShipAngle += 0.02;
  for (let i = 0; i < WORLDS.length; i++) {
    let wx = width * (i + 1) / (WORLDS.length + 1);
    let wy = height * 0.45 + sin(hubShipAngle + i) * 15;
    let unlocked = i <= currentWorld;

    // Ship
    push();
    translate(wx, wy);
    if (unlocked) {
      fill(...WORLDS[i].color);
      stroke(255, 200);
      strokeWeight(2);
    } else {
      fill(80);
      stroke(100);
      strokeWeight(1);
    }
    // Ship body
    ellipse(0, 0, 60, 40);
    fill(unlocked ? 200 : 60);
    ellipse(0, -5, 30, 20);
    // Flames
    if (unlocked) {
      fill(255, 150, 0, 150 + sin(frameCount * 0.3) * 100);
      noStroke();
      triangle(-10, 20, 10, 20, 0, 35 + sin(frameCount * 0.5) * 5);
    }
    pop();

    // World label
    fill(unlocked ? 255 : 120);
    noStroke();
    textSize(min(11, width / 40));
    text(WORLDS[i].name, wx, wy + 45);
    if (!unlocked) {
      fill(255, 80, 80);
      textSize(10);
      text('🔒', wx, wy + 60);
    }
  }

  // Astro Bot in hub
  drawAstroBot(width / 2, height - 100, 1.5);
}
Line-by-line explanation (21 lines)

🔧 Subcomponents:

function-call Background and starfield background(40, 25, 15); drawStarfield();

Fills the canvas with a dark brown color and draws the animated starfield

for-loop Procedural dunes for (let i = 0; i < width; i += 2) { let h = noise(i * 0.005, frameCount * 0.002) * 80 + 40; rect(i, height - h, 2, h); }

Draws wavy sand dunes using Perlin noise that animate slightly each frame

calculation Game title and instructions fill(255, 220, 100); textSize(min(32, width / 14)); textStyle(BOLD); text('ASTRO BOT', width / 2, 50);

Draws the game title in large, bold, golden text at the top center

for-loop Draw world ships for (let i = 0; i < WORLDS.length; i++) {

Loops through all 5 worlds and draws a ship for each one, locked or unlocked based on progress

calculation Ship bobbing motion let wy = height * 0.45 + sin(hubShipAngle + i) * 15;

Makes each ship bob up and down smoothly using sine wave, with each ship slightly out of phase

background(40, 25, 15);
Fills the entire canvas with a dark brown color (RGB: 40, 25, 15)
drawStarfield();
Calls the function that updates and draws the animated background stars
for (let i = 0; i < width; i += 2) {
Loops from 0 to width in steps of 2, drawing vertical sand dune columns across the bottom
let h = noise(i * 0.005, frameCount * 0.002) * 80 + 40;
Uses Perlin noise to generate a wavy height value between 40 and 120 pixels; the noise gradually changes with time (frameCount * 0.002) to animate the dunes
rect(i, height - h, 2, h);
Draws a 2-pixel-wide rectangle from the bottom of the screen up to height h, creating the dune effect
fill(255, 220, 100);
Sets the fill color to golden yellow (RGB: 255, 220, 100)
textSize(min(32, width / 14));
Sets text size to either 32 pixels or width/14 (whichever is smaller), making the title scale on smaller screens
textStyle(BOLD);
Makes the title text bold
text('ASTRO BOT', width / 2, 50);
Draws the title 'ASTRO BOT' centered horizontally and 50 pixels from the top
hubShipAngle += 0.02;
Increments the ship bobbing angle by 0.02 radians each frame, creating continuous animation
for (let i = 0; i < WORLDS.length; i++) {
Loops through all 5 worlds in the WORLDS array
let wx = width * (i + 1) / (WORLDS.length + 1);
Calculates the x position of each world ship so they are evenly spaced across the screen
let wy = height * 0.45 + sin(hubShipAngle + i) * 15;
Positions each ship at 45% down the screen, plus a sine-wave offset of ±15 pixels for bobbing animation
let unlocked = i <= currentWorld;
A boolean that is true if this world is unlocked (player has completed all previous worlds)
if (unlocked) {
If the world is unlocked, draw it in color with a bright outline
fill(...WORLDS[i].color);
Uses the spread operator (...) to unpack the color array from WORLDS[i], setting the fill to that world's color
ellipse(0, 0, 60, 40);
Draws the ship body as an ellipse 60 pixels wide and 40 pixels tall
triangle(-10, 20, 10, 20, 0, 35 + sin(frameCount * 0.5) * 5);
Draws animated flames underneath the ship that flicker up and down
text(WORLDS[i].name, wx, wy + 45);
Draws the world's name below the ship
text('🔒', wx, wy + 60);
Draws a lock emoji below locked worlds
drawAstroBot(width / 2, height - 100, 1.5);
Draws Astro Bot in the center bottom of the hub, scaled to 1.5 times normal size

updateGame()

updateGame() is the heart of the game loop. It applies gravity to the player, checks for collisions with platforms (to keep them from falling), moves enemies and checks for collisions with them, detects bot rescues, handles timers for punch and spin actions, and checks for level completion or falling off screen. Every aspect of the player's physics and interactions happens here.

🔬 This loop moves enemies and bounces them off walls. What happens if you change the 50 to 100 or the width * 2.5 to width * 1.5? This controls the play area where enemies can move.

  // Enemy collision
  for (let e of enemies) {
    if (!e.alive) continue;
    e.x += e.vx;
    if (e.x < 50 || e.x > width * 2.5) e.vx *= -1;
function updateGame() {
  // Player movement
  player.vx = joystick.dx * MOVE_SPEED;
  if (joystick.dx !== 0) player.facing = joystick.dx > 0 ? 1 : -1;

  // Apply gravity
  player.vy += GRAVITY;
  player.x += player.vx;
  player.y += player.vy;

  // Platform collision
  player.onGround = false;
  for (let p of platforms) {
    if (player.x + player.w / 2 > p.x - player.x * 0 &&
        player.x - player.w / 2 < p.x + p.w &&
        player.y + player.h / 2 > p.y &&
        player.y + player.h / 2 < p.y + p.h + player.vy + 2 &&
        player.vy >= 0) {
      player.y = p.y - player.h / 2;
      player.vy = 0;
      player.onGround = true;
    }
  }

  // Camera scroll
  let camX = max(0, player.x - width * 0.35);

  // Timers
  if (player.punching > 0) player.punching--;
  if (player.spinning > 0) player.spinning--;
  if (player.invincible > 0) player.invincible--;
  player.animFrame += 0.15;

  // Enemy collision
  for (let e of enemies) {
    if (!e.alive) continue;
    e.x += e.vx;
    if (e.x < 50 || e.x > width * 2.5) e.vx *= -1;

    let dx = (player.x) - e.x;
    let dy = (player.y) - e.y;
    let dist = sqrt(dx * dx + dy * dy);

    if (dist < 40) {
      if (player.punching > 0 || player.spinning > 0) {
        e.alive = false;
        score += 100;
        shakeAmount = 5;
        spawnParticles(e.x, e.y, [255, 200, 0], 8);
      } else if (player.invincible <= 0) {
        lives--;
        player.invincible = 60;
        shakeAmount = 10;
        if (lives <= 0) {
          gameState = 'hub';
          lives = 3;
        }
      }
    }
  }

  // Bot rescue
  for (let b of bots) {
    if (b.rescued) continue;
    let dx = player.x - b.x;
    let dy = player.y - b.y;
    if (sqrt(dx * dx + dy * dy) < 40) {
      b.rescued = true;
      botsRescued++;
      score += 500;
      spawnParticles(b.x, b.y, [0, 200, 255], 12);
    }
  }

  // Check level complete
  if (gameState === 'level' && bots.every(b => b.rescued)) {
    gameState = 'boss';
    initBoss();
  }

  // Fall off screen
  if (player.y > height + 100) {
    lives--;
    player.x = 60;
    player.y = 0;
    player.vy = 0;
    if (lives <= 0) { gameState = 'hub'; lives = 3; }
  }
}
Line-by-line explanation (44 lines)

🔧 Subcomponents:

assignment Apply joystick input player.vx = joystick.dx * MOVE_SPEED; if (joystick.dx !== 0) player.facing = joystick.dx > 0 ? 1 : -1;

Converts joystick horizontal input into player velocity and updates the direction the player faces

assignment Gravity and position update player.vy += GRAVITY; player.x += player.vx; player.y += player.vy;

Applies gravity acceleration, then updates player position by adding velocity

for-loop Platform collision detection for (let p of platforms) {

Checks if the player is colliding with any platform and handles landing

for-loop Enemy movement and collision for (let e of enemies) {

Updates enemy position, checks if they hit the wall, and detects collisions with the player

for-loop Bot rescue detection for (let b of bots) {

Checks if the player is close enough to each bot to rescue it

conditional Level complete check if (gameState === 'level' && bots.every(b => b.rescued)) {

Transitions to boss fight when all bots are rescued

conditional Fall off screen if (player.y > height + 100) {

Detects if the player has fallen below the screen and respawns them

player.vx = joystick.dx * MOVE_SPEED;
Sets the player's horizontal velocity by multiplying the joystick's normalized x input (joystick.dx, -1 to 1) by MOVE_SPEED (4)
if (joystick.dx !== 0) player.facing = joystick.dx > 0 ? 1 : -1;
If the joystick is being moved, update player.facing to 1 (right) or -1 (left) based on joystick direction
player.vy += GRAVITY;
Adds GRAVITY (0.6) to vertical velocity each frame, making the player accelerate downward
player.x += player.vx;
Updates the player's x position by adding horizontal velocity
player.y += player.vy;
Updates the player's y position by adding vertical velocity (affected by gravity)
player.onGround = false;
Assumes the player is not on ground; collision detection below will set it to true if they are on a platform
for (let p of platforms) {
Loops through all platform objects
if (player.x + player.w / 2 > p.x - player.x * 0 &&
Checks if the right edge of the player is past the left edge of the platform (note: player.x * 0 is always 0, so this just checks if player.x + player.w / 2 > p.x)
player.x - player.w / 2 < p.x + p.w &&
Checks if the left edge of the player is before the right edge of the platform
player.y + player.h / 2 > p.y &&
Checks if the bottom edge of the player is below the top edge of the platform
player.y + player.h / 2 < p.y + p.h + player.vy + 2 &&
Checks if the player is approaching or on the platform (accounting for velocity and a small buffer)
player.vy >= 0) {
Checks if the player is moving downward or stationary (not jumping upward into the platform from below)
player.y = p.y - player.h / 2;
Snaps the player's position to just above the platform (top of player at platform top)
player.vy = 0;
Sets vertical velocity to zero so the player doesn't sink into the platform
player.onGround = true;
Sets the flag so the player can jump
if (player.punching > 0) player.punching--;
Decrements the punch timer each frame; when it reaches 0, the punch animation ends
if (player.spinning > 0) player.spinning--;
Decrements the spin timer each frame; when it reaches 0, the spin animation ends
if (player.invincible > 0) player.invincible--;
Decrements the invincibility timer each frame; when it reaches 0, the player can take damage again
player.animFrame += 0.15;
Increments the animation frame counter, used to animate the player's walking legs
e.x += e.vx;
Updates the enemy's x position by adding its velocity
if (e.x < 50 || e.x > width * 2.5) e.vx *= -1;
Reverses the enemy's direction if it has moved past the left or right boundary
let dist = sqrt(dx * dx + dy * dy);
Calculates the distance between the player and enemy using the Pythagorean theorem
if (dist < 40) {
If the distance is less than 40 pixels (collision), handle hit detection
if (player.punching > 0 || player.spinning > 0) {
If the player is punching or spinning (both have timers > 0), the player defeats the enemy
e.alive = false;
Marks the enemy as dead so it won't be drawn or updated
score += 100;
Awards 100 points for defeating an enemy
shakeAmount = 5;
Triggers a small screen shake (amount 5)
spawnParticles(e.x, e.y, [255, 200, 0], 8);
Creates 8 yellow/gold particles at the enemy's position for visual feedback
} else if (player.invincible <= 0) {
If the player is NOT punching/spinning and NOT invincible, the enemy hits the player
lives--;
Reduces lives by 1
player.invincible = 60;
Grants 60 frames (1 second) of invincibility after being hit
shakeAmount = 10;
Triggers a larger screen shake (amount 10)
for (let b of bots) {
Loops through all bots to rescue
if (sqrt(dx * dx + dy * dy) < 40) {
If the distance between the player and bot is less than 40 pixels, rescue the bot
b.rescued = true;
Marks the bot as rescued so it won't be drawn anymore
botsRescued++;
Increments the total count of rescued bots (displayed in HUD and at game end)
score += 500;
Awards 500 points for rescuing a bot (much more valuable than defeating an enemy)
spawnParticles(b.x, b.y, [0, 200, 255], 12);
Creates 12 cyan particles at the bot's position
if (gameState === 'level' && bots.every(b => b.rescued)) {
If in level mode AND every bot has been rescued (checked using every()), advance to boss fight
gameState = 'boss';
Switches game state to 'boss'
initBoss();
Calls initBoss() to set up the boss fight
if (player.y > height + 100) {
If the player falls 100 pixels below the bottom of the screen, they fell off
lives--;
Reduces lives by 1
player.x = 60; player.y = 0; player.vy = 0;
Respawns the player at the starting position with zero velocity

updateBoss()

updateBoss() handles the boss fight AI and collision detection. The boss has two phases: phase 0 is pacing side to side with sine-wave bobbing, and phase 1 is charging at the player. When the player hits the boss with punch or spin, damage is dealt and the boss flashes. When the boss's health reaches 0, the player advances to the next world or wins the game.

🔬 This is the boss's pacing phase. What happens if you change 120 to 60? What about changing 0.03 to 0.05 or the 60 at the end to 100? Try experimenting with these numbers to make the boss behave differently.

  if (boss.phase === 0) {
    boss.x += boss.vx;
    if (boss.x < 100 || boss.x > width - 100) boss.vx *= -1;
    boss.y = height * 0.35 + sin(boss.timer * 0.03) * 60;
    if (boss.timer % 120 === 0) boss.phase = 1;
function updateBoss() {
  if (!boss) return;
  boss.timer++;

  // Boss AI patterns
  if (boss.phase === 0) {
    boss.x += boss.vx;
    if (boss.x < 100 || boss.x > width - 100) boss.vx *= -1;
    boss.y = height * 0.35 + sin(boss.timer * 0.03) * 60;
    if (boss.timer % 120 === 0) boss.phase = 1;
  } else if (boss.phase === 1) {
    // Charge at player
    let dx = player.x - boss.x;
    boss.x += (dx > 0 ? 3 : -3);
    boss.y += 2;
    if (boss.y > height - 130) { boss.phase = 0; boss.timer = 0; }
  }

  // Boss hit detection
  let dx = player.x - boss.x;
  let dy = player.y - boss.y;
  let dist = sqrt(dx * dx + dy * dy);

  if (dist < 60) {
    if (player.punching > 0 || player.spinning > 0) {
      if (boss.hit <= 0) {
        bossHealth -= 15;
        boss.hit = 20;
        shakeAmount = 8;
        spawnParticles(boss.x, boss.y, boss.color, 10);
        if (bossHealth <= 0) {
          currentWorld++;
          if (currentWorld >= WORLDS.length) {
            gameState = 'win';
          } else {
            gameState = 'hub';
          }
          spawnParticles(boss.x, boss.y, [255, 255, 0], 30);
        }
      }
    } else if (player.invincible <= 0) {
      lives--;
      player.invincible = 60;
      shakeAmount = 10;
      if (lives <= 0) { gameState = 'hub'; lives = 3; }
    }
  }
  if (boss.hit > 0) boss.hit--;
}
Line-by-line explanation (30 lines)

🔧 Subcomponents:

conditional Boss existence check if (!boss) return;

Exits early if boss is null (no boss fight active)

conditional Boss pacing phase if (boss.phase === 0) {

Boss paces side to side while bobbing up and down; transitions to phase 1 after 2 seconds

conditional Boss charge phase } else if (boss.phase === 1) {

Boss charges at the player, moving toward them and descending; transitions back to phase 0

conditional Boss hit detection if (dist < 60) {

Checks if player and boss are colliding; if so, either damages the boss or damages the player

if (!boss) return;
If boss is null or undefined, exit the function immediately (no boss to update)
boss.timer++;
Increments the boss's timer each frame; this is used to track time in each phase
if (boss.phase === 0) {
Checks if the boss is in phase 0 (pacing)
boss.x += boss.vx;
Updates the boss's x position by adding its velocity
if (boss.x < 100 || boss.x > width - 100) boss.vx *= -1;
If the boss hits the left or right boundary, reverse its direction
boss.y = height * 0.35 + sin(boss.timer * 0.03) * 60;
Sets the boss's y position to a fixed height (35% down) plus a sine wave oscillation of ±60 pixels, making it bob smoothly
if (boss.timer % 120 === 0) boss.phase = 1;
Every 120 frames (2 seconds), switch to phase 1 (charge)
} else if (boss.phase === 1) {
Checks if the boss is in phase 1 (charging)
let dx = player.x - boss.x;
Calculates the horizontal distance from the boss to the player
boss.x += (dx > 0 ? 3 : -3);
Moves the boss 3 pixels toward the player (positive or negative based on whether player is to the right or left)
boss.y += 2;
Moves the boss downward 2 pixels per frame during the charge
if (boss.y > height - 130) { boss.phase = 0; boss.timer = 0; }
If the boss descends below height - 130, switch back to phase 0 and reset the timer
let dist = sqrt(dx * dx + dy * dy);
Calculates the distance between the player and boss
if (dist < 60) {
If they are colliding (distance < 60 pixels)
if (player.punching > 0 || player.spinning > 0) {
If the player is punching or spinning, the player can damage the boss
if (boss.hit <= 0) {
Only deal damage once per punch/spin (boss.hit is a cooldown timer)
bossHealth -= 15;
Deals 15 damage to the boss
boss.hit = 20;
Sets a 20-frame cooldown so the player can't deal damage again until it expires
shakeAmount = 8;
Triggers screen shake (amount 8)
spawnParticles(boss.x, boss.y, boss.color, 10);
Creates 10 particles in the boss's color
if (bossHealth <= 0) {
If the boss is defeated
currentWorld++;
Unlocks the next world
if (currentWorld >= WORLDS.length) {
If all 5 worlds are complete
gameState = 'win';
Switch to the win screen
} else {
Otherwise, there are more worlds to go
gameState = 'hub';
Return to the hub to select the next world
spawnParticles(boss.x, boss.y, [255, 255, 0], 30);
Creates 30 yellow particles as a victory explosion
} else if (player.invincible <= 0) {
If the player is not punching/spinning and not invincible, the boss hits the player
lives--;
Reduces lives by 1
if (boss.hit > 0) boss.hit--;
Decrements the boss's hit cooldown timer each frame

drawLevel()

drawLevel() renders the game world with camera scrolling. It uses push/translate/pop to shift all drawings based on the camera position (which follows the player). Platforms, enemies, bots, and the boss are all drawn inside the camera transform so they scroll with the world. The HUD is drawn after pop() so it stays fixed on screen. This is where the visual heart of the gameplay happens.

🔬 Bots bob up and down using a sine wave. What happens if you change 0.05 to 0.1 (faster bobbing)? What if you change the 8 to 15 (bigger bobbing amplitude)?

  // Bots to rescue
  for (let b of bots) {
    if (b.rescued) continue;
    let by = b.y + sin(frameCount * 0.05 + b.bobOffset) * 8;
function drawLevel() {
  let w = WORLDS[min(currentWorld, WORLDS.length - 1)];
  background(...w.bg);
  drawStarfield();

  let camX = max(0, player.x - width * 0.35);

  push();
  translate(-camX, 0);

  // Platforms
  for (let p of platforms) {
    fill(...w.color);
    stroke(0, 50);
    strokeWeight(1);
    rect(p.x, p.y, p.w, p.h, 4);
    // Platform top highlight
    fill(255, 30);
    noStroke();
    rect(p.x + 2, p.y, p.w - 4, 3, 2);
  }

  // Enemies
  for (let e of enemies) {
    if (!e.alive) continue;
    push();
    translate(e.x, e.y);
    fill(200, 50, 50);
    stroke(0);
    strokeWeight(1);
    ellipse(0, 0, e.w, e.h);
    fill(255);
    ellipse(-5, -4, 8, 8);
    ellipse(5, -4, 8, 8);
    fill(0);
    ellipse(-4, -4, 4, 4);
    ellipse(6, -4, 4, 4);
    pop();
  }

  // Bots to rescue
  for (let b of bots) {
    if (b.rescued) continue;
    let by = b.y + sin(frameCount * 0.05 + b.bobOffset) * 8;
    push();
    translate(b.x, by);
    // Glow
    fill(0, 200, 255, 40);
    noStroke();
    ellipse(0, 0, 50, 50);
    // Bot body
    fill(180, 220, 255);
    stroke(100, 180, 255);
    strokeWeight(1);
    rect(-12, -15, 24, 25, 5);
    // Eyes
    fill(0, 200, 255);
    noStroke();
    ellipse(-5, -7, 6, 6);
    ellipse(5, -7, 6, 6);
    // Antenna
    stroke(100, 180, 255);
    strokeWeight(2);
    line(0, -15, 0, -22);
    fill(0, 255, 200);
    noStroke();
    ellipse(0, -24, 6, 6);
    pop();
  }

  // Boss
  if (boss) {
    push();
    translate(boss.x, boss.y);
    let flash = boss.hit > 0 ? 255 : 0;
    fill(boss.color[0] + flash, boss.color[1] + flash, boss.color[2] + flash);
    stroke(0);
    strokeWeight(2);
    ellipse(0, 0, boss.w, boss.h);
    // Boss eyes
    fill(255, 50 + flash, 50);
    noStroke();
    ellipse(-15, -10, 20, 20);
    ellipse(15, -10, 20, 20);
    fill(0);
    ellipse(-15, -10, 10, 10);
    ellipse(15, -10, 10, 10);
    // Boss name
    fill(255);
    textSize(14);
    text(boss.name, 0, -55);
    pop();
  }

  // Player
  if (player.invincible <= 0 || frameCount % 4 < 2) {
    drawAstroBot(player.x, player.y, 1);
  }

  pop();

  // HUD
  drawHUD(w);
}
Line-by-line explanation (25 lines)

🔧 Subcomponents:

assignment Get world colors let w = WORLDS[min(currentWorld, WORLDS.length - 1)];

Retrieves the current world's color scheme and background color

assignment Camera scrolling let camX = max(0, player.x - width * 0.35); push(); translate(-camX, 0);

Calculates camera position to follow the player and translates all drawable content

for-loop Draw platforms for (let p of platforms) {

Loops through and draws all platforms with highlights

for-loop Draw enemies for (let e of enemies) {

Loops through and draws alive enemies with animated eyes

for-loop Draw bots for (let b of bots) {

Loops through and draws unrescued bots with bobbing animation

let w = WORLDS[min(currentWorld, WORLDS.length - 1)];
Gets the current world object; min() ensures we don't go out of bounds
background(...w.bg);
Sets the background color using the world's bg property (spread operator unpacks the RGB array)
drawStarfield();
Draws the animated starfield background
let camX = max(0, player.x - width * 0.35);
Calculates the camera's x position to keep the player at 35% from the left edge; max(0, ...) prevents scrolling past x=0
push();
Saves the current transformation matrix before applying camera translation
translate(-camX, 0);
Shifts all subsequent drawing so the camera appears centered on the player
fill(...w.color);
Sets the platform fill color to the world's color (spread operator unpacks the RGB array)
rect(p.x, p.y, p.w, p.h, 4);
Draws the platform as a rectangle with 4-pixel corner radius
fill(255, 30); noStroke(); rect(p.x + 2, p.y, p.w - 4, 3, 2);
Draws a bright highlight on the top of the platform for depth and shading
if (!e.alive) continue;
Skips drawing dead enemies (continue jumps to the next loop iteration)
ellipse(0, 0, e.w, e.h);
Draws the enemy body as a red circle
ellipse(-5, -4, 8, 8); ellipse(5, -4, 8, 8);
Draws two white eye circles
ellipse(-4, -4, 4, 4); ellipse(6, -4, 4, 4);
Draws black pupils in the center of each eye
let by = b.y + sin(frameCount * 0.05 + b.bobOffset) * 8;
Calculates the bot's y position with a bobbing animation offset; each bot has a different bobOffset so they don't all bob in sync
ellipse(0, 0, 50, 50);
Draws a transparent cyan glow around the bot
rect(-12, -15, 24, 25, 5);
Draws the bot's body as a rounded rectangle
ellipse(-5, -7, 6, 6); ellipse(5, -7, 6, 6);
Draws two cyan eye circles
line(0, -15, 0, -22); fill(0, 255, 200); noStroke(); ellipse(0, -24, 6, 6);
Draws the bot's antenna as a line with a glowing cyan ball on top
let flash = boss.hit > 0 ? 255 : 0;
If the boss was recently hit (boss.hit > 0), add 255 to its color channels to flash white
fill(boss.color[0] + flash, boss.color[1] + flash, boss.color[2] + flash);
Sets the boss fill color, adding flash value for the white hit animation
ellipse(0, 0, boss.w, boss.h);
Draws the boss as a colored circle
ellipse(-15, -10, 20, 20); ellipse(15, -10, 20, 20);
Draws two large red eye circles
if (player.invincible <= 0 || frameCount % 4 < 2) {
Draws the player normally unless invincible, then flashes every 4 frames (creates blinking effect)
pop();
Restores the transformation matrix, undoing the camera translation
drawHUD(w);
Draws the top HUD bar with lives, world name, and score (outside the camera transform so it stays on screen)

drawAstroBot(x, y, s)

drawAstroBot() draws the main character. It uses push/pop to apply transformations (position, scale, and facing direction flip). The character has a body, helmet with visor and eyes, animated legs, and optional arms (punch or spin). When the player jumps, the jetpack shows flickering flames. This function is called from drawHub() (for the large hub display) and drawLevel() (for the player in gameplay).

🔬 The legs animate using a sine wave. What happens if you change the 3 to 10 (faster animation)? What if you change 4 to 8 (bigger leg movement)? What if you remove the (player.onGround ? legAnim : 0) logic and always apply legAnim?

  // Legs
  fill(180, 200, 220);
  stroke(150, 170, 190);
  let legAnim = sin(player.animFrame * 3) * 4;
  rect(-10, 17, 8, 12 + (player.onGround ? legAnim : 0), 3);
  rect(2, 17, 8, 12 + (player.onGround ? -legAnim : 0), 3);
function drawAstroBot(x, y, s) {
  push();
  translate(x, y);
  scale(s);
  scale(player.facing, 1);

  // Body
  fill(200, 220, 240);
  stroke(150, 180, 210);
  strokeWeight(1);
  rect(-12, -5, 24, 22, 6);

  // Head/helmet
  fill(220, 235, 255);
  stroke(150, 180, 210);
  ellipse(0, -14, 28, 26);
  // Visor
  fill(40, 140, 220, 200);
  noStroke();
  arc(0, -12, 20, 16, -PI * 0.8, PI * 0.8, CHORD);
  // Eyes
  fill(255);
  ellipse(-4, -14, 6, 6);
  ellipse(4, -14, 6, 6);
  fill(0);
  ellipse(-3, -14, 3, 3);
  ellipse(5, -14, 3, 3);

  // Legs
  fill(180, 200, 220);
  stroke(150, 170, 190);
  let legAnim = sin(player.animFrame * 3) * 4;
  rect(-10, 17, 8, 12 + (player.onGround ? legAnim : 0), 3);
  rect(2, 17, 8, 12 + (player.onGround ? -legAnim : 0), 3);

  // Arms
  if (player.punching > 0) {
    fill(255, 200, 100);
    noStroke();
    ellipse(18, -2, 20, 16);
  } else if (player.spinning > 0) {
    let spinAngle = frameCount * 0.5;
    fill(100, 200, 255, 150);
    noStroke();
    for (let i = 0; i < 4; i++) {
      let a = spinAngle + i * HALF_PI;
      ellipse(cos(a) * 20, sin(a) * 20 - 5, 12, 12);
    }
  }

  // Jetpack
  fill(100, 110, 130);
  noStroke();
  rect(-8, 2, 4, 14, 2);
  rect(4, 2, 4, 14, 2);
  if (!player.onGround && player.vy < 0) {
    fill(255, 150, 0, 200);
    noStroke();
    triangle(-6, 16, -6 + 4, 16, -6 + 2, 24 + random(4));
    triangle(6, 16, 6 + 4, 16, 6 + 2, 24 + random(4));
  }

  pop();
}
Line-by-line explanation (23 lines)

🔧 Subcomponents:

assignment Position and scale push(); translate(x, y); scale(s); scale(player.facing, 1);

Saves the matrix, positions the character at (x, y), scales it by size s, and flips it horizontally based on facing direction

function-call Body and head rect(-12, -5, 24, 22, 6); ... ellipse(0, -14, 28, 26);

Draws the astronaut's body and helmet

assignment Walking animation let legAnim = sin(player.animFrame * 3) * 4;

Calculates leg offset using sine wave of animFrame to create walking motion

conditional Punch animation if (player.punching > 0) {

If punching, draws a large orange arm extending to the right

conditional Spin animation } else if (player.spinning > 0) {

If spinning, draws four blue circles rotating around the player

conditional Jetpack flames if (!player.onGround && player.vy < 0) {

If the player is airborne and moving upward, shows jetpack flames

push();
Saves the current transformation matrix
translate(x, y);
Moves the origin to the character's position
scale(s);
Scales the entire character by factor s (1 for normal, 1.5 for hub display)
scale(player.facing, 1);
Horizontally flips the character if facing is -1 (facing left); 1 means no flip (facing right)
fill(200, 220, 240); stroke(150, 180, 210); strokeWeight(1); rect(-12, -5, 24, 22, 6);
Draws the body as a light blue rounded rectangle 24 pixels wide and 22 pixels tall
ellipse(0, -14, 28, 26);
Draws the helmet as a light cyan ellipse
arc(0, -12, 20, 16, -PI * 0.8, PI * 0.8, CHORD);
Draws a blue visor arc across the bottom of the helmet
ellipse(-4, -14, 6, 6); ellipse(4, -14, 6, 6);
Draws two white eye circles
ellipse(-3, -14, 3, 3); ellipse(5, -14, 3, 3);
Draws two black pupils
let legAnim = sin(player.animFrame * 3) * 4;
Creates a sine-wave animation that oscillates ±4 pixels; multiplying by 3 makes the animation cycle faster
rect(-10, 17, 8, 12 + (player.onGround ? legAnim : 0), 3);
Draws the left leg; if on ground, adds legAnim for walking motion; if in air, leg stays still (at length 12)
rect(2, 17, 8, 12 + (player.onGround ? -legAnim : 0), 3);
Draws the right leg with opposite animation (-legAnim) so legs alternate when walking
if (player.punching > 0) {
If the punch timer is active (punching > 0)
ellipse(18, -2, 20, 16);
Draws an orange punch arm extending to the right
} else if (player.spinning > 0) {
If the spin timer is active and punch is not
let spinAngle = frameCount * 0.5;
Creates a rotating angle that increases 0.5 radians per frame
for (let i = 0; i < 4; i++) {
Loops 4 times to draw 4 blue circles in a rotating pattern
let a = spinAngle + i * HALF_PI;
Calculates angle for each circle (90 degrees apart)
ellipse(cos(a) * 20, sin(a) * 20 - 5, 12, 12);
Draws a circle at distance 20 from center, at angle a
fill(100, 110, 130); rect(-8, 2, 4, 14, 2); rect(4, 2, 4, 14, 2);
Draws two dark blue jetpack modules on the back
if (!player.onGround && player.vy < 0) {
If the player is in the air AND moving upward (vy < 0 means negative/upward velocity)
triangle(-6, 16, -6 + 4, 16, -6 + 2, 24 + random(4)); triangle(6, 16, 6 + 4, 16, 6 + 2, 24 + random(4));
Draws two flickering flame triangles with random height variation (random(4)) for a dynamic jetpack effect
pop();
Restores the transformation matrix, undoing all scaling and translation

drawParticles()

drawParticles() updates and renders all active particles. Each particle has a position, velocity, life (countdown), color, and size. Every frame, particles move according to their velocity, fall due to gravity, fade out as their life expires, and are removed from the array when dead. This function is called in draw() after the main game rendering, so particles appear on top of everything. The backwards loop is crucial for safe array removal.

🔬 Particles fall due to the 0.15 gravity value. What happens if you change 0.15 to 0.05 (less gravity, particles float longer)? What about 0.4 (more gravity, particles fall fast)?

  for (let i = particles.length - 1; i >= 0; i--) {
    let p = particles[i];
    p.x += p.vx;
    p.y += p.vy;
    p.vy += 0.15;
function drawParticles() {
  for (let i = particles.length - 1; i >= 0; i--) {
    let p = particles[i];
    p.x += p.vx;
    p.y += p.vy;
    p.vy += 0.15;
    p.life--;
    fill(p.color[0], p.color[1], p.color[2], (p.life / 30) * 255);
    noStroke();
    ellipse(p.x, p.y, p.size);
    if (p.life <= 0) particles.splice(i, 1);
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

for-loop Reverse iteration for (let i = particles.length - 1; i >= 0; i--) {

Loops backwards through the array so splicing items doesn't skip any (important for safe array removal)

assignment Particle physics p.x += p.vx; p.y += p.vy; p.vy += 0.15;

Updates particle position and applies gravity to make them fall naturally

assignment Fade and draw p.life--; fill(p.color[0], p.color[1], p.color[2], (p.life / 30) * 255);

Decrements life and sets alpha based on remaining life, creating a fade-out effect

conditional Remove dead particles if (p.life <= 0) particles.splice(i, 1);

Removes the particle from the array when its life expires

for (let i = particles.length - 1; i >= 0; i--) {
Loops backwards from the last index to 0; this is important because we may remove items with splice() inside the loop, and going backwards prevents skipping elements
let p = particles[i];
Gets the current particle object for easier access
p.x += p.vx;
Updates the particle's x position by adding its horizontal velocity
p.y += p.vy;
Updates the particle's y position by adding its vertical velocity
p.vy += 0.15;
Applies gravity acceleration to the vertical velocity (0.15 per frame), making particles fall
p.life--;
Decrements the particle's life by 1 each frame (started at 30)
fill(p.color[0], p.color[1], p.color[2], (p.life / 30) * 255);
Sets the fill color and alpha: divides remaining life by 30 (the original life) to get a 0-1 ratio, then multiplies by 255 for alpha (255=opaque, 0=transparent); as life decreases, alpha decreases and the particle fades out
noStroke();
Disables stroke so particles draw as solid filled circles
ellipse(p.x, p.y, p.size);
Draws the particle as a circle at its current position
if (p.life <= 0) particles.splice(i, 1);
When the particle's life reaches 0, removes it from the array using splice(i, 1) which removes 1 element at index i

spawnParticles(x, y, col, count)

spawnParticles() is a helper function called whenever something dramatic happens (enemy defeated, bot rescued, boss hit, etc.). It creates multiple particles at a given position with random velocities and sizes. The particles then live for 30 frames before fading out. This function is called from updateGame() and updateBoss() to create visual feedback for player actions.

function spawnParticles(x, y, col, count) {
  for (let i = 0; i < count; i++) {
    particles.push({
      x, y,
      vx: random(-4, 4),
      vy: random(-6, 2),
      life: 30,
      color: col,
      size: random(3, 8)
    });
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop Create particles for (let i = 0; i < count; i++) {

Loops 'count' times, creating one particle per iteration

assignment Particle properties particles.push({ x, y, vx: random(-4, 4), vy: random(-6, 2), life: 30, color: col, size: random(3, 8) });

Creates a new particle object with random velocity, life span, and size, then adds it to the particles array

function spawnParticles(x, y, col, count) {
Takes four parameters: x and y (spawn position), col (color array), and count (number of particles to create)
for (let i = 0; i < count; i++) {
Loops 'count' times, creating one particle per iteration
particles.push({
Creates a new object and adds it to the particles array
x, y,
Shorthand for x: x, y: y—sets the particle's starting position
vx: random(-4, 4),
Sets a random horizontal velocity between -4 and 4 pixels per frame, so particles spread in different directions
vy: random(-6, 2),
Sets a random vertical velocity between -6 and 2, with more negative values so particles trend upward initially
life: 30,
Sets life to 30 frames—drawParticles() counts down from 30 to 0
color: col,
Stores the color array passed as a parameter
size: random(3, 8)
Sets a random size between 3 and 8 pixels for visual variety

handleTouch(isStart)

handleTouch() processes both mouse and touch input for the game controls. It is called by touchStarted() and touchMoved(). The joystick updates continuously with touch movement, while button presses only trigger on touch start (isStart = true). The function handles three action buttons (jump, punch, spin) by checking if the touch point is within 1.3 times each button's radius.

function handleTouch(isStart) {
  let tx = mouseX || (touches.length > 0 ? touches[0].x : 0);
  let ty = mouseY || (touches.length > 0 ? touches[0].y : 0);

  if (gameState === 'hub' || gameState === 'win') {
    if (isStart) handleClick();
    return;
  }

  // Joystick
  if (dist(tx, ty, joystick.x, joystick.y) < joystick.r * 1.5) {
    joystick.active = true;
    joystick.dx = constrain((tx - joystick.x) / joystick.r, -1, 1);
    joystick.dy = constrain((ty - joystick.y) / joystick.r, -1, 1);
  }

  if (!isStart) return;

  // Jump
  if (dist(tx, ty, jumpBtn.x, jumpBtn.y) < jumpBtn.r * 1.3 && player.onGround) {
    player.vy = JUMP_FORCE;
    player.onGround = false;
  }

  // Punch
  if (dist(tx, ty, punchBtn.x, punchBtn.y) < punchBtn.r * 1.3) {
    player.punching = 15;
    spawnParticles(player.x + player.facing * 25, player.y, [255, 200, 50], 4);
  }

  // Spin
  if (dist(tx, ty, spinBtn.x, spinBtn.y) < spinBtn.r * 1.3) {
    player.spinning = 30;
    spawnParticles(player.x, player.y, [100, 200, 255], 6);
  }
}
Line-by-line explanation (18 lines)

🔧 Subcomponents:

assignment Get touch coordinates let tx = mouseX || (touches.length > 0 ? touches[0].x : 0); let ty = mouseY || (touches.length > 0 ? touches[0].y : 0);

Gets the touch/mouse coordinates, checking for both mouse input and touch input

conditional Hub/win screen handling if (gameState === 'hub' || gameState === 'win') {

If in hub or win screen, use click handling instead and exit

conditional Joystick input if (dist(tx, ty, joystick.x, joystick.y) < joystick.r * 1.5) {

If touch is on the joystick, update its dx and dy values

conditional Button taps if (dist(tx, ty, jumpBtn.x, jumpBtn.y) < jumpBtn.r * 1.3 && player.onGround) {

On touch start, check if any action buttons were tapped

let tx = mouseX || (touches.length > 0 ? touches[0].x : 0);
Uses mouseX if available (mouse input), otherwise gets x from the first touch point, or defaults to 0
let ty = mouseY || (touches.length > 0 ? touches[0].y : 0);
Uses mouseY if available (mouse input), otherwise gets y from the first touch point, or defaults to 0
if (gameState === 'hub' || gameState === 'win') {
If the game is in hub or win state
if (isStart) handleClick();
If this is a touch start event, call handleClick() which handles world selection or returning to hub
return;
Exit the function—don't process joystick or buttons in hub/win state
if (dist(tx, ty, joystick.x, joystick.y) < joystick.r * 1.5) {
If the touch is within 1.5 times the joystick radius from its center, the user is using the joystick
joystick.active = true;
Marks the joystick as active
joystick.dx = constrain((tx - joystick.x) / joystick.r, -1, 1);
Calculates normalized x input: (touch x - joystick center x) divided by radius, clamped to -1 to 1
joystick.dy = constrain((ty - joystick.y) / joystick.r, -1, 1);
Calculates normalized y input: (touch y - joystick center y) divided by radius, clamped to -1 to 1
if (!isStart) return;
If this is a touch move event (not start), exit—button presses only happen on touch start
if (dist(tx, ty, jumpBtn.x, jumpBtn.y) < jumpBtn.r * 1.3 && player.onGround) {
If touch is on jump button AND the player is on ground (can jump)
player.vy = JUMP_FORCE;
Sets vertical velocity to the jump force (-12), launching the player upward
player.onGround = false;
Sets onGround to false so the player can't jump again until landing
if (dist(tx, ty, punchBtn.x, punchBtn.y) < punchBtn.r * 1.3) {
If touch is on punch button
player.punching = 15;
Starts the punch animation (30-frame countdown in updateGame())
spawnParticles(player.x + player.facing * 25, player.y, [255, 200, 50], 4);
Creates 4 yellow/gold particles in front of the player (player.facing * 25 offsets the spawn position)
if (dist(tx, ty, spinBtn.x, spinBtn.y) < spinBtn.r * 1.3) {
If touch is on spin button
player.spinning = 30;
Starts the spin animation (60-frame countdown)

drawStarfield()

drawStarfield() creates an animated background of scrolling stars. Each star moves left at its own speed (parallax effect—slower stars appear closer), wraps around to the right edge, and twinkles with a sine-wave brightness variation. This function is called in both drawHub() and drawLevel() to create depth and atmosphere.

🔬 Stars scroll left at varying speeds. What happens if you change 0.3 to 0.1 (slower scrolling) or 0.5 (faster)? Try changing the wrap detection from -10 to -50.

  for (let s of stars) {
    s.x -= s.sp * 0.3;
    if (s.x < -10) { s.x = width + 10; s.y = random(height); }
function drawStarfield() {
  for (let s of stars) {
    s.x -= s.sp * 0.3;
    if (s.x < -10) { s.x = width + 10; s.y = random(height); }
    fill(255, 200 + sin(frameCount * 0.05 + s.x) * 55);
    noStroke();
    ellipse(s.x, s.y, s.s, s.s);
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Update and draw stars for (let s of stars) {

Loops through all star objects and updates their position, then draws them

assignment Parallax scrolling s.x -= s.sp * 0.3;

Moves each star left by its speed multiplied by 0.3, creating parallax depth

conditional Star wrapping if (s.x < -10) { s.x = width + 10; s.y = random(height); }

When a star scrolls off the left edge, teleports it to the right with a new y position

assignment Twinkling effect fill(255, 200 + sin(frameCount * 0.05 + s.x) * 55);

Sets star brightness to twinkle using a sine wave (brightness varies from 145 to 255)

for (let s of stars) {
Loops through each star object in the stars array
s.x -= s.sp * 0.3;
Moves the star left by (speed * 0.3); slower stars have lower sp values, creating parallax depth
if (s.x < -10) {
Checks if the star has scrolled off the left edge (x is less than -10)
s.x = width + 10;
Teleports the star to the right edge of the screen
s.y = random(height);
Gives the star a new random y position so stars don't reappear in predictable rows
fill(255, 200 + sin(frameCount * 0.05 + s.x) * 55);
Sets the fill color to white (255) with varying alpha: the sine wave oscillates from 145 to 255, making stars twinkle
ellipse(s.x, s.y, s.s, s.s);
Draws the star as a circle with size s.s at its current position

drawHUD(w)

drawHUD() renders the in-game HUD (heads-up display) at the top of the screen. It shows the player's remaining lives (left), the current world name (center), and score (right). The background is a semi-transparent black bar that stays fixed on screen even when the camera scrolls. This function is called at the end of drawLevel().

function drawHUD(w) {
  // Top bar
  fill(0, 0, 0, 150);
  noStroke();
  rect(0, 0, width, 40, 0, 0, 8, 8);

  fill(255);
  textSize(min(14, width / 30));
  textAlign(LEFT, CENTER);
  text(`♥ ${lives}`, 15, 20);
  textAlign(CENTER, CENTER);
  text(w.name, width / 2, 20);
  textAlign(RIGHT, CENTER);
  text(`Score: ${score}`, width - 15, 20);
  textAlign(CENTER, CENTER);
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

function-call HUD background fill(0, 0, 0, 150); rect(0, 0, width, 40, 0, 0, 8, 8);

Draws a semi-transparent black bar at the top with rounded bottom corners

function-call HUD text text(`♥ ${lives}`, 15, 20); text(w.name, width / 2, 20); text(`Score: ${score}`, width - 15, 20);

Displays three text elements: lives on left, world name in center, score on right

fill(0, 0, 0, 150);
Sets fill to black with 150 alpha (semi-transparent)
rect(0, 0, width, 40, 0, 0, 8, 8);
Draws a rectangle spanning the full width at the top, 40 pixels tall, with 0-pixel radius on top corners and 8-pixel radius on bottom corners
fill(255);
Sets text fill to white
textSize(min(14, width / 30));
Sets text size to either 14 or width/30 (whichever is smaller), scaling for different screens
textAlign(LEFT, CENTER);
Aligns text to the left horizontally and centered vertically
text(`♥ ${lives}`, 15, 20);
Draws the heart symbol and current lives count at x=15 (left side)
textAlign(CENTER, CENTER);
Aligns text to center horizontally and vertically
text(w.name, width / 2, 20);
Draws the world name at the center
textAlign(RIGHT, CENTER);
Aligns text to the right horizontally and centered vertically
text(`Score: ${score}`, width - 15, 20);
Draws the score on the right side at x = width - 15

drawBossUI()

drawBossUI() renders the boss health bar at the top of the screen during boss fights. It displays the boss's current health as a proportion of max health, with a dark red background and bright red fill. The boss's name appears above the bar. This function is only called in the 'boss' gameState from draw().

function drawBossUI() {
  if (!boss) return;
  // Boss health bar
  let barW = width * 0.5;
  let barX = (width - barW) / 2;
  fill(0, 0, 0, 150);
  rect(barX - 4, 48, barW + 8, 20, 6);
  fill(60, 0, 0);
  rect(barX, 52, barW, 12, 4);
  fill(220, 40, 40);
  rect(barX, 52, barW * (bossHealth / bossMaxHealth), 12, 4);
  fill(255);
  textSize(10);
  text(boss.name, width / 2, 58);
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

conditional Boss existence check if (!boss) return;

Exits early if there is no boss (not in boss fight)

assignment Health bar background fill(0, 0, 0, 150); rect(barX - 4, 48, barW + 8, 20, 6);

Draws a semi-transparent black background for the health bar

assignment Health bar fill fill(220, 40, 40); rect(barX, 52, barW * (bossHealth / bossMaxHealth), 12, 4);

Draws a red bar that shrinks as bossHealth decreases

if (!boss) return;
If boss is null or undefined, exit immediately (no boss to display health for)
let barW = width * 0.5;
Calculates the full health bar width as 50% of the canvas width
let barX = (width - barW) / 2;
Calculates the x position to center the bar horizontally
fill(0, 0, 0, 150);
Sets fill to semi-transparent black
rect(barX - 4, 48, barW + 8, 20, 6);
Draws the background border box for the health bar (extends 4 pixels beyond the bar on each side for padding)
fill(60, 0, 0);
Sets fill to dark red (background of the health bar)
rect(barX, 52, barW, 12, 4);
Draws a dark red full-width bar underneath
fill(220, 40, 40);
Sets fill to bright red (the actual health indicator)
rect(barX, 52, barW * (bossHealth / bossMaxHealth), 12, 4);
Draws a red bar that shrinks as health decreases: multiplies full width by (current health / max health) ratio
fill(255);
Sets text color to white
textSize(10);
Sets text size to 10 pixels (small, for the boss name)
text(boss.name, width / 2, 58);
Draws the boss's name centered above the health bar

drawWin()

drawWin() displays the victory screen after all five worlds are completed. It shows a large golden VICTORY title, the player's final score and bot count, Astro Bot standing triumphantly at double scale, and a prompt to return to the hub. The background uses the same dark blue and starfield as the hub for thematic continuity.

function drawWin() {
  background(10, 5, 30);
  drawStarfield();

  fill(255, 220, 50);
  textSize(min(36, width / 10));
  textStyle(BOLD);
  text('VICTORY!', width / 2, height * 0.3);

  textStyle(NORMAL);
  fill(200, 220, 255);
  textSize(min(18, width / 22));
  text('Astro Bot saved all the bots!', width / 2, height * 0.45);
  text(`Total Score: ${score}`, width / 2, height * 0.55);
  text(`Bots Rescued: ${botsRescued}`, width / 2, height * 0.62);

  fill(100, 200, 255);
  textSize(min(14, width / 28));
  text('Tap to return to hub', width / 2, height * 0.75);

  drawAstroBot(width / 2, height * 0.85, 2);
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

function-call Background and starfield background(10, 5, 30); drawStarfield();

Fills the canvas with dark blue and draws the animated starfield

function-call Victory title fill(255, 220, 50); text('VICTORY!', width / 2, height * 0.3);

Displays the large golden VICTORY text

function-call Game stats text('Astro Bot saved all the bots!', width / 2, height * 0.45); text(`Total Score: ${score}`, width / 2, height * 0.55); text(`Bots Rescued: ${botsRescued}`, width / 2, height * 0.62);

Displays victory message and final game statistics

function-call Celebratory character drawAstroBot(width / 2, height * 0.85, 2);

Draws Astro Bot at 2x scale at the bottom of the screen

background(10, 5, 30);
Fills the canvas with a dark space blue (RGB: 10, 5, 30)
drawStarfield();
Draws the animated starfield
fill(255, 220, 50);
Sets text fill to golden yellow
textSize(min(36, width / 10));
Sets title size to either 36 or width/10 (whichever is smaller)
textStyle(BOLD);
Makes the title bold
text('VICTORY!', width / 2, height * 0.3);
Draws the title at the top of the screen (30% down)
textStyle(NORMAL);
Returns text style to normal (not bold)
fill(200, 220, 255);
Sets text fill to light cyan
textSize(min(18, width / 22));
Sets text size for the secondary message
text('Astro Bot saved all the bots!', width / 2, height * 0.45);
Displays the victory message
text(`Total Score: ${score}`, width / 2, height * 0.55);
Displays the total score (accumulated across all levels)
text(`Bots Rescued: ${botsRescued}`, width / 2, height * 0.62);
Displays the total number of bots rescued
fill(100, 200, 255);
Sets text fill to cyan
textSize(min(14, width / 28));
Sets text size for the instruction
text('Tap to return to hub', width / 2, height * 0.75);
Displays the instruction to return to the hub
drawAstroBot(width / 2, height * 0.85, 2);
Draws Astro Bot at 2x scale near the bottom of the screen as a celebratory display

📦 Key Variables

gameState string

Tracks which screen/mode the game is in: 'hub' (world select), 'level' (gameplay), 'boss' (boss fight), or 'win' (victory screen)

let gameState = 'hub';
currentWorld number

Stores which world (0-4) the player is currently in or preparing to enter

let currentWorld = 0;
player object

An object containing all player data: position (x, y), velocity (vx, vy), size, animation state, and action timers (punching, spinning, invincible)

let player = { x: 200, y: 300, vx: 0, vy: 0, w: 30, h: 40, onGround: false, ... };
platforms array

An array of platform objects; each platform has x, y, width, and height for collision detection

let platforms = [ { x: 0, y: 450, w: 800, h: 50 }, ... ];
enemies array

An array of enemy objects; each enemy has x, y, velocity, size, alive status, and type

let enemies = [ { x: 300, y: 400, vx: 1.5, w: 25, h: 25, alive: true, type: 0 }, ... ];
bots array

An array of bot objects to rescue; each bot has x, y, rescued status, and animation offset

let bots = [ { x: 400, y: 200, rescued: false, bobOffset: 1.5 }, ... ];
boss object or null

The current boss object (if any); contains position, size, velocity, phase, health, and visual data

let boss = { x: 600, y: 300, w: 80, h: 80, vx: 2, phase: 0, ... };
score number

The player's accumulated score, increased by defeating enemies (100 points) and rescuing bots (500 points)

let score = 0;
lives number

The number of lives the player has remaining; starts at 3, decreases when hit or falling off screen

let lives = 3;
botsRescued number

Total count of bots rescued across all levels; displayed in the hub and win screen

let botsRescued = 0;
bossHealth number

The current health of the active boss (decreases when hit); when it reaches 0, the boss is defeated

let bossHealth = 0;
bossMaxHealth number

The maximum health of a boss (100 points); used to calculate the health bar ratio

let bossMaxHealth = 100;
particles array

An array of particle objects for visual effects (explosions, sparks); each particle has position, velocity, life, color, and size

let particles = [ { x: 200, y: 150, vx: -2, vy: -4, life: 30, color: [255, 200, 0], size: 5 }, ... ];
stars array

An array of star objects for the parallax background; each star has x, y, size (s), and scroll speed (sp)

let stars = [ { x: 100, y: 50, s: 2, sp: 1.2 }, ... ];
shakeAmount number

Controls screen shake intensity; decreases each frame to create a fading shake effect after impacts or explosions

let shakeAmount = 0;
joystick object

The on-screen joystick control; stores position (x, y), radius (r), and normalized input (dx, dy)

let joystick = { x: 100, y: 500, r: 50, dx: 0, dy: 0, active: false };
jumpBtn object

The jump button object; stores position and radius for hit detection

let jumpBtn = { x: 700, y: 500, r: 35, label: '↑' };
punchBtn object

The punch button object; stores position and radius for hit detection

let punchBtn = { x: 750, y: 430, r: 35, label: '👊' };
spinBtn object

The spin button object; stores position and radius for hit detection

let spinBtn = { x: 750, y: 500, r: 35, label: '🌀' };
WORLDS array of objects

A constant array defining the five worlds; each world has a name, color, boss name, boss color, and background color

const WORLDS = [ { name: 'Jungle Planet', color: [40, 180, 60], boss: 'Rilla', ... }, ... ];

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG updateGame() platform collision

The collision check uses player.x * 0 which is always 0—this looks like a typo. The line should be checking player.x + player.w / 2 > p.x.

💡 Change `player.x + player.w / 2 > p.x - player.x * 0` to `player.x + player.w / 2 > p.x` to clean up the logic.

PERFORMANCE drawLevel() camera calculation

Camera position is recalculated twice: once in drawLevel() and once commented in updateGame(), using slightly different logic

💡 Calculate camera position once in updateGame() and store it in a variable (like let globalCamX) so it can be reused in both functions.

STYLE All functions

No input validation—if player, platforms, or boss are undefined in unexpected ways, the code could crash silently

💡 Add guard clauses like `if (!player) return;` at the start of functions that assume these objects exist (like drawAstroBot).

FEATURE Game over screen

When the player loses all lives, the game returns to the hub without showing why or how many levels/bots were completed

💡 Create a 'gameOver' gameState that shows final stats, lives lost, and progress before returning to the hub.

FEATURE Boss AI (updateBoss)

All bosses use identical movement patterns—phase 0 (pacing) and phase 1 (charging) apply to every boss regardless of world

💡 Add boss.type or boss.pattern and implement unique AI behaviors for each world's boss (e.g., Rilla jumps, Serpentis coils, Johnny flies).

🔄 Code Flow

Code flow showing setup, initstars, initplayer, inittouchcontrols, initlevel, initboss, draw, drawhub, updategame, updateboss, drawlevel, drawastrobot, drawparticles, spawnparticles, handletouch, drawstarfield, drawhud, drawbossui, drawhub

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] click setup href "#fn-setup" click draw href "#fn-draw" setup --> canvas-creation[Canvas Setup] setup --> text-alignment[Text Centering] setup --> initialization-calls[System Initialization] click canvas-creation href "#sub-canvas-creation" click text-alignment href "#sub-text-alignment" click initialization-calls href "#sub-initialization-calls" initialization-calls --> initstars[initStars] initstars --> array-reset[Clear Stars Array] array-reset --> star-loop[Create 80 Stars] star-loop --> star-object[Star Properties] star-object --> initplayer[initPlayer] click initstars href "#fn-initstars" click array-reset href "#sub-array-reset" click star-loop href "#sub-star-loop" click star-object href "#sub-star-object" initplayer --> position-init[Starting Position] position-init --> velocity-init[Starting Velocity] velocity-init --> dimensions[Player Size] dimensions --> state-flags[Action States] click initplayer href "#fn-initplayer" click position-init href "#sub-position-init" click velocity-init href "#sub-velocity-init" click dimensions href "#sub-dimensions" click state-flags href "#sub-state-flags" setup --> inittouchcontrols[inittouchControls] inittouchcontrols --> size-calculation[Button Sizing] size-calculation --> margin-calculation[Edge Spacing] margin-calculation --> joystick-init[Joystick Setup] joystick-init --> button-positions[Action Buttons] click inittouchcontrols href "#fn-inittouchcontrols" click size-calculation href "#sub-size-calculation" click margin-calculation href "#sub-margin-calculation" click joystick-init href "#sub-joystick-init" click button-positions href "#sub-button-positions" setup --> initlevel[initLevel] initlevel --> array-reset[Clear Game Arrays] array-reset --> player-reset[Reset Player State] player-reset --> ground-platform[Create Ground] ground-platform --> platform-generation[Random Platforms] platform-generation --> bot-placement[Place Bots] bot-placement --> enemy-placement[Spawn Enemies] click initlevel href "#fn-initlevel" click player-reset href "#sub-player-reset" click ground-platform href "#sub-ground-platform" click platform-generation href "#sub-platform-generation" click bot-placement href "#sub-bot-placement" click enemy-placement href "#sub-enemy-placement" setup --> initboss[initBoss] initboss --> world-data[Get World Properties] world-data --> boss-object[Create Boss] boss-object --> boss-platforms[Boss Arena Platforms] boss-platforms --> arena-generation[Add Stepping Stones] click initboss href "#fn-initboss" click world-data href "#sub-world-data" click boss-object href "#sub-boss-object" click boss-platforms href "#sub-boss-platforms" click arena-generation href "#sub-arena-generation" draw --> screen-shake[Screen Shake Effect] draw --> state-switch[Game State Routing] click screen-shake href "#sub-screen-shake" click state-switch href "#sub-state-switch" state-switch --> drawhub[drawHub] drawhub --> background-setup[Background and Starfield] background-setup --> sand-dunes[Procedural Dunes] sand-dunes --> title-text[Game Title and Instructions] title-text --> world-loop[Draw World Ships] world-loop --> ship-animation[Ship Bobbing Motion] click drawhub href "#fn-drawhub" click background-setup href "#sub-background-setup" click sand-dunes href "#sub-sand-dunes" click title-text href "#sub-title-text" click world-loop href "#sub-world-loop" click ship-animation href "#sub-ship-animation" state-switch --> drawlevel[drawLevel] drawlevel --> camera-setup[Camera Scrolling] camera-setup --> platform-loop[Draw Platforms] platform-loop --> enemy-loop[Draw Enemies] enemy-loop --> bot-loop[Draw Bots] click drawlevel href "#fn-drawlevel" click camera-setup href "#sub-camera-setup" click platform-loop href "#sub-platform-loop" click enemy-loop href "#sub-enemy-loop" click bot-loop href "#sub-bot-loop" state-switch --> updategame[updateGame] updategame --> movement-input[Apply Joystick Input] movement-input --> gravity-application[Gravity and Position Update] gravity-application --> platform-collision[Platform Collision Detection] platform-collision --> enemy-movement[Enemy Movement and Collision] enemy-movement --> bot-rescue[Bot Rescue Detection] bot-rescue --> level-completion[Level Complete Check] level-completion --> fall-detection[Fall Off Screen] click updategame href "#fn-updategame" click movement-input href "#sub-movement-input" click gravity-application href "#sub-gravity-application" click platform-collision href "#sub-platform-collision" click enemy-movement href "#sub-enemy-movement" click bot-rescue href "#sub-bot-rescue" click level-completion href "#sub-level-completion" click fall-detection href "#sub-fall-detection" state-switch --> updateboss[updateBoss] updateboss --> boss-check[Boss Existence Check] boss-check --> phase-zero[Boss Pacing Phase] phase-zero --> phase-one[Boss Charge Phase] phase-one --> hit-detection[Boss Hit Detection] click updateboss href "#fn-updateboss" click boss-check href "#sub-boss-check" click phase-zero href "#sub-phase-zero" click phase-one href "#sub-phase-one" click hit-detection href "#sub-hit-detection" draw --> drawastrobot[drawAstroBot] drawastrobot --> setup-transforms[Position and Scale] setup-transforms --> body-drawing[Body and Head] body-drawing --> leg-animation[Walking Animation] leg-animation --> punch-condition[Punch Animation] punch-condition --> spin-condition[Spin Animation] spin-condition --> jetpack-firing[Jetpack Flames] click drawastrobot href "#fn-drawastrobot" click setup-transforms href "#sub-setup-transforms" click body-drawing href "#sub-body-drawing" click leg-animation href "#sub-leg-animation" click punch-condition href "#sub-punch-condition" click spin-condition href "#sub-spin-condition" click jetpack-firing href "#sub-jetpack-firing" draw --> drawparticles[drawParticles] drawparticles --> backwards-loop[Reverse Iteration] backwards-loop --> physics-update[Particle Physics] physics-update --> fade-out[Fade and Draw] fade-out --> cleanup[Remove Dead Particles] click drawparticles href "#fn-drawparticles" click backwards-loop href "#sub-backwards-loop" click physics-update href "#sub-physics-update" click fade-out href "#sub-fade-out" click cleanup href "#sub-cleanup" draw --> spawnparticles[spawnParticles] spawnparticles --> spawn-loop[Create Particles] spawn-loop --> particle-object[Particle Properties] click spawnparticles href "#fn-spawnparticles" click spawn-loop href "#sub-spawn-loop" click particle-object href "#sub-particle-object" draw --> handletouch[handleTouch] handletouch --> touch-position[Get Touch Coordinates] touch-position --> hub-branch[Hub/Win Screen Handling] hub-branch --> joystick-handling[Joystick Input] joystick-handling --> action-buttons[Button Taps] click handletouch href "#fn-handletouch" click touch-position href "#sub-touch-position" click hub-branch href "#sub-hub-branch" click joystick-handling href "#sub-joystick-handling" click action-buttons href "#sub-action-buttons" draw --> drawstarfield[drawStarfield] drawstarfield --> scroll-loop[Update and Draw Stars] scroll-loop --> scrolling[Parallax Scrolling] scrolling --> wrapping[Star Wrapping] wrapping --> twinkling[Twinkling Effect] click drawstarfield href "#fn-drawstarfield" click scroll-loop href "#sub-scroll-loop" click scrolling href "#sub-scrolling" click wrapping href "#sub-wrapping" click twinkling href "#sub-twinkling" draw --> drawhud[drawHUD] drawhud --> bar-background[HUD Background] bar-background --> text-displays[HUD Text] click drawhud href "#fn-drawhud" click bar-background href "#sub-bar-background" click text-displays href "#sub-text-displays" state-switch --> drawbossui[drawBossUI] drawbossui --> bar-background[Health Bar Background] bar-background --> health-bar[Health Bar] click drawbossui href "#fn-drawbossui" click health-bar href "#sub-health-bar" state-switch --> drawwin[drawWin] drawwin --> background-setup[Background and Starfield] background-setup --> title[Victory Title] title --> stats[Game Stats] stats --> astro-display[Celebatory Character] click drawwin href "#fn-drawwin" click astro-display href "#sub-astro-display"

❓ Frequently Asked Questions

What visual elements are created in the Astro bot sketch?

The Astro bot sketch features a dark gradient background with floating particles that animate gently across the canvas, creating an ethereal and dynamic visual experience.

How can users interact with the Astro bot sketch?

Users can swipe between panels to explore various features, including an AI chat for coding assistance and a live preview of the sketch.

What creative coding concepts are demonstrated in the Astro bot sketch?

The sketch showcases techniques such as particle animation, dynamic background rendering, and responsive design to enhance user interaction.

Preview

Astro bot - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Astro bot - Code flow showing setup, initstars, initplayer, inittouchcontrols, initlevel, initboss, draw, drawhub, updategame, updateboss, drawlevel, drawastrobot, drawparticles, spawnparticles, handletouch, drawstarfield, drawhud, drawbossui, drawhub
Code Flow Diagram