escape tsunamis for brainrots

This is a fast-paced evacuation game where you collect colorful 'brainrots' and escape advancing tsunami waves. You manage inventory, buy speed upgrades, and activate utility waves with coins earned from storing brainrots—all while staying ahead of the rising water.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make brainrots spawn at the top more often — Higher tier brainrots spawn at the top where they're closer to the spawn line; adjust getTierForY thresholds to make rare brainrots more common
  2. Disable carrying penalty to test speed — Carrying a brainrot normally reduces speed to 70%; remove this penalty to make collection feel instant and test how the game feels
  3. Spawn waves twice as fast — Increase difficulty by halving the wave spawn interval so waves arrive more frequently
  4. Double inventory size for easier selling — Expand the base inventory from 8 to 16 slots so players can store more brainrots before having to sell
  5. Make all waves move at lightning speed — Add a global wave speed multiplier to make even slow waves deadly
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates an arcade-style survival game where players race to collect creatures called 'brainrots' while outrunning tsunami waves. The visual appeal comes from colorful tier-based brainrot sprites, animated rainbow effects, smooth player movement controlled by the cursor or WASD keys, and the constant threat of rising waves that accelerate the gameplay. The core p5.js techniques powering this are the draw loop for 60fps animation, collision detection between the player and waves, click/touch interaction for purchasing, and transform effects like colorMode(HSB) for rainbow animations.

The code is organized into three main sections: setup and draw loops that orchestrate the game, drawing functions that render all sprites and UI, and game logic functions that handle movement, waves, inventory, and special effects. By reading this sketch you will learn how to build a complete interactive game with multiple game states, how to structure large sketches with many independent systems (waves, brainrots, shops, utilities), and how utility functions and helper objects make complex game logic manageable and modular.

⚙️ How It Works

  1. When the sketch loads, setup() initializes the canvas, player position, 70 randomly-spawned brainrots scattered across the screen, safe zones, shops, and schedules the first tsunami wave.
  2. Every frame, draw() renders the background, guides, safe areas, base, shops, all brainrots and waves, and the player. The gameState variable controls whether the game is PLAYING or GAMEOVER.
  3. During PLAYING, updatePlayer() smoothly moves the player toward the cursor (or via WASD keys), updateWaves() advances each wave downscreen and removes it when it passes the despawn line, and handleBrainrotCollection() picks up brainrots automatically when the player touches them.
  4. The player can only move below the spawn line (top of screen) and above the red despawn line. Carrying a brainrot reduces the player's speed by 30% but the brainrot is stored in inventory when the player reaches the base area below the red line.
  5. Brainrots are tier-ranked (common, rare, epic, legendary, mythic, god, celestial) based on their vertical position—higher-tier brainrots spawn closer to the top and are worth more coins. Every 5 minutes a special celestial brainrot appears. Coins earned passively from stored brainrots can buy speed upgrades in the Speed Shop or activate special utility waves (Chaos, Zen, Clear, Love, Admin Wave, etc.) that modify how incoming waves behave.
  6. If any wave reaches the base, the game ends. Pressing SPACE restarts. Safe zones let players briefly hide from waves without consequence.

🎓 Concepts You'll Learn

Game state managementCollision detectionInventory and item systemsTween/easing (lerp)Arrays and object manipulationRainbow color cycling with colorMode(HSB)UI layout and interactionWave spawning and physics

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It creates the canvas, initializes all game variables, and attaches event listeners. p5.js then enters the draw loop automatically.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noSmooth();
  textFont('monospace');

  initializeLayout();
  initializePlayer();
  populateBrainrots();
  nextCelestialAt = millis() + celestialInterval;
  scheduleNextWave(500);

  window.addEventListener('blur', releaseAllKeys);
  window.addEventListener('focus', releaseAllKeys);
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

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

Creates a full-screen canvas and disables antialiasing for pixel-perfect graphics

function-call Game initialization initializeLayout();

Sets up spawn line, safe areas, base position, and red despawn line based on screen size

function-call Focus/blur event handlers window.addEventListener('blur', releaseAllKeys);

Prevents stuck movement keys when the browser window loses focus

createCanvas(windowWidth, windowHeight);
Makes a canvas that fills the entire browser window; updateCanvas on resize will handle window changes
noSmooth();
Disables antialiasing so pixels stay crisp and blocky, fitting the retro arcade style
textFont('monospace');
Uses a fixed-width font for all text, making numbers and labels look uniform
initializeLayout();
Calls helper to calculate spawnLineY, redLineY, base position, and safe areas based on current window size
initializePlayer();
Creates the player object and positions them at the base ready to start
populateBrainrots();
Fills the screen with 70 randomly-positioned and tier-ranked brainrots
nextCelestialAt = millis() + celestialInterval;
Schedules the first celestial brainrot spawn for 5 minutes from now (5 * 60 * 1000 ms)
scheduleNextWave(500);
Schedules the first tsunami wave to spawn after a 500ms delay
window.addEventListener('blur', releaseAllKeys);
Clears all movement flags when the player clicks away, preventing stuck keys

draw()

draw() runs 60 times per second. It separates rendering (drawing) from logic (updating) so you can see what's happening before the game state changes. The gameState check lets you pause all updates when the player loses.

🔬 This block runs all the game logic only when gameState is 'PLAYING'. What happens if you comment out checkWaveCollision() so the wave never triggers game over?

  if (gameState === 'PLAYING') {
    updatePlayer();
    updateWaves();
    handleBrainrotCollection();
    handleCarryDeposit();
    checkPlayerWaveCollision();
    checkWaveCollision();
    updateCelestialSpawner();
    updatePassiveIncome();
  }
function draw() {
  background(14);

  drawGuides();
  drawSafeAreas();
  drawBase();
  drawShop();
  drawMarket();
  drawBrainrots();
  drawPlayer();
  drawWaves();
  drawUtilityButton();
  if (utilityPanelOpen) drawUtilityPanel();
  drawUI();

  if (gameState === 'PLAYING') {
    updatePlayer();
    updateWaves();
    handleBrainrotCollection();
    handleCarryDeposit();
    checkPlayerWaveCollision();
    checkWaveCollision();
    updateCelestialSpawner();
    updatePassiveIncome();
  } else {
    drawGameOver();
  }

  if (confirmSaleIndex !== null) drawConfirmPrompt();
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

function-call Rendering phase drawGuides();

Draws all visual elements (guides, base, brainrots, player, waves) before updating game state

conditional Game logic phase if (gameState === 'PLAYING') {

Updates movement, waves, collisions, and income only when actively playing, not when game is over

background(14);
Clears the screen with a very dark gray (almost black), erasing the previous frame
drawGuides();
Draws the orange spawn line and red despawn line so players can see the boundaries
drawBrainrots();
Draws all floating brainrots at their current positions with names visible when player is near
drawPlayer();
Draws the white player square, eyes, and any carried brainrot hovering above
drawWaves();
Draws each wave as a colored rectangle with its label in the center
if (gameState === 'PLAYING') {
Only update and check collisions if the game is still running, not if GAMEOVER
updatePlayer();
Moves the player toward the cursor (or via WASD) with easing, respecting screen boundaries
updateWaves();
Advances all waves downscreen by their speed, removes waves that pass the red line, and schedules new ones
handleBrainrotCollection();
Picks up any brainrot the player is touching and holds it above their head
checkPlayerWaveCollision();
Tests if the player is hit by a wave; if so, either ragdolls them or respawns them
checkWaveCollision();
Tests if any wave has reached the base; if so, sets gameState to GAMEOVER
} else {
When the game is over, only draw the game over screen; stop all game updates

updatePlayer()

updatePlayer() handles both movement modes: smooth cursor-follow (great for mobile/touchscreen) and direct WASD control (traditional keyboard). The carrying speed penalty adds strategic tension—you move slower but you're actively collecting loot.

🔬 This code lets players move diagonally by pressing two keys at once. What happens if you remove the carrySlowFactor and always use full playerSpeed, even while carrying?

    const moveX = (moveRight ? 1 : 0) - (moveLeft ? 1 : 0);
    const moveY = (moveDown ? 1 : 0) - (moveUp ? 1 : 0);
    const effectiveSpeed = playerSpeed * (carriedBrainrot ? carrySlowFactor : 1);

    player.x += moveX * effectiveSpeed;
    player.y += moveY * effectiveSpeed;
function updatePlayer() {
  if (followCursor) {
    const targetX = constrain(mouseX, playerSize / 2, width - playerSize / 2);
    const targetY = constrain(mouseY, spawnLineY + playerSize / 2, height - playerSize / 2);
    const ease = carriedBrainrot ? 0.15 : 0.2;
    player.x = lerp(player.x, targetX, ease);
    player.y = lerp(player.y, targetY, ease);
  } else {
    const moveX = (moveRight ? 1 : 0) - (moveLeft ? 1 : 0);
    const moveY = (moveDown ? 1 : 0) - (moveUp ? 1 : 0);
    const effectiveSpeed = playerSpeed * (carriedBrainrot ? carrySlowFactor : 1);

    player.x += moveX * effectiveSpeed;
    player.y += moveY * effectiveSpeed;

    player.x = constrain(player.x, playerSize / 2, width - playerSize / 2);
    player.y = constrain(player.y, spawnLineY + playerSize / 2, height - playerSize / 2);
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Cursor-follow mode if (followCursor) {

Smoothly eases player toward mouse position if cursor-follow is enabled

conditional WASD direct movement } else {

Moves player directly by WASD key state if cursor-follow is disabled

calculation Carrying speed reduction const effectiveSpeed = playerSpeed * (carriedBrainrot ? carrySlowFactor : 1);

Applies 30% speed penalty when player is holding a brainrot

if (followCursor) {
Checks if the player is in cursor-follow mode (TAB to toggle)
const targetX = constrain(mouseX, playerSize / 2, width - playerSize / 2);
Clamps the target X position to stay within screen bounds so the player never leaves the canvas
const ease = carriedBrainrot ? 0.15 : 0.2;
Uses a slower easing (0.15) when carrying so the player feels sluggish; faster (0.2) when empty
player.x = lerp(player.x, targetX, ease);
Smoothly interpolates toward the target using lerp—the ease value controls how quickly to arrive (lower = slower)
const moveX = (moveRight ? 1 : 0) - (moveLeft ? 1 : 0);
Calculates net horizontal movement: +1 if moving right, -1 if moving left, 0 if both or neither
const effectiveSpeed = playerSpeed * (carriedBrainrot ? carrySlowFactor : 1);
If carrying, multiply speed by 0.7; otherwise use full speed
player.x += moveX * effectiveSpeed;
Applies the net movement direction multiplied by effective speed to move the player instantly (no easing in WASD mode)
player.x = constrain(player.x, playerSize / 2, width - playerSize / 2);
Clamps the player to stay within screen bounds after movement

updateWaves()

updateWaves() is the heart of the difficulty system. It layers multiple effects (paused, zen, love, wacky) and stacks their multipliers. This design lets utility waves combine in interesting ways—for example, activating both Zen and Chaos at the same time creates a moderate-speed wave.

🔬 These multipliers can stack—if both Zen and Wacky are active, the final multiplier is 0.35 * 1.2. What happens if you add a new line that checks for Chaos Wave and doubles the speed?

  const paused = now < waveEffects.pauseUntil;
  let speedMultiplier = globalWaveScalar;
  if (now < waveEffects.zenUntil) speedMultiplier *= 0.35;
  if (now < loveWaveTimer) speedMultiplier *= 0.8;
  if (now < waveEffects.wackyUntil) speedMultiplier *= 1.2;
function updateWaves() {
  const now = millis();

  if (waves.length === 0 && now >= nextWaveAt) {
    spawnWave();
  }

  const paused = now < waveEffects.pauseUntil;
  let speedMultiplier = globalWaveScalar;
  if (now < waveEffects.zenUntil) speedMultiplier *= 0.35;
  if (now < loveWaveTimer) speedMultiplier *= 0.8;
  if (now < waveEffects.wackyUntil) speedMultiplier *= 1.2;

  for (let i = waves.length - 1; i >= 0; i--) {
    const wave = waves[i];
    if (!paused) wave.y += wave.speed * speedMultiplier;

    if (wave.y + wave.h >= redLineY) {
      if (wave.special !== 'infinity') {
        waves.splice(i, 1);
        scheduleNextWave();
      }
    }
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Wave spawn timing if (waves.length === 0 && now >= nextWaveAt) {

Spawns a new wave when the screen is clear and enough time has passed

calculation Speed multiplier effects let speedMultiplier = globalWaveScalar;

Stacks multiple effect modifiers (zen, love, wacky) to adjust wave speed dynamically

for-loop Wave position update for (let i = waves.length - 1; i >= 0; i--) {

Moves each wave downscreen and removes it when it passes the despawn line

const now = millis();
Gets the current time in milliseconds for all effect and spawn timing checks
if (waves.length === 0 && now >= nextWaveAt) {
Only spawn a new wave if the previous one is gone AND the scheduled time has arrived
const paused = now < waveEffects.pauseUntil;
Checks if the Pause Wave utility is still active; if so, waves will not move
let speedMultiplier = globalWaveScalar;
Starts with the global multiplier (1.0 by default), then applies effects on top
if (now < waveEffects.zenUntil) speedMultiplier *= 0.35;
If Zen Wave is active, reduce speed to 35% (slow waves to a crawl)
if (now < loveWaveTimer) speedMultiplier *= 0.8;
If Love Wave is active, reduce speed to 80% (gentle, almost no effect combined with other modifiers)
if (now < waveEffects.wackyUntil) speedMultiplier *= 1.2;
If Wacky Waves is active, increase speed to 120% (waves move 20% faster)
if (!paused) wave.y += wave.speed * speedMultiplier;
Only move the wave if not paused; apply both the wave's base speed and the multiplier
if (wave.y + wave.h >= redLineY) {
Checks if the wave's bottom edge has reached or passed the despawn line
if (wave.special !== 'infinity') {
If it's NOT the Infinity Wave, remove it and schedule the next one

checkPlayerWaveCollision()

This function demonstrates bounding-box collision detection: checking if two rectangles overlap by comparing their edges. Safe zones are a risk-vs-reward mechanic—hide to survive, but you can't collect brainrots while hiding.

function checkPlayerWaveCollision() {
  if (isInSafeArea(player.x, player.y)) return;

  for (const wave of waves) {
    if (player.y + playerSize / 2 >= wave.y && player.y - playerSize / 2 <= wave.y + wave.h) {
      if (millis() < waveEffects.ragdollUntil) {
        ragdollPlayer();
      } else {
        respawnPlayer();
      }
      break;
    }
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Safe zone immunity if (isInSafeArea(player.x, player.y)) return;

Skips all collision checks if the player is in a safe area, granting temporary immunity

conditional Vertical overlap detection if (player.y + playerSize / 2 >= wave.y && player.y - playerSize / 2 <= wave.y + wave.h) {

Tests whether the player's vertical range overlaps with the wave's vertical range

conditional Ragdoll effect check if (millis() < waveEffects.ragdollUntil) {

Chooses whether to ragdoll or respawn based on active utility effects

if (isInSafeArea(player.x, player.y)) return;
If the player is in a safe zone, exit immediately—no collision damage
for (const wave of waves) {
Loop through all active waves to check for collision with the player
if (player.y + playerSize / 2 >= wave.y && player.y - playerSize / 2 <= wave.y + wave.h) {
Tests if the player's bottom edge is at or below the wave's top AND the player's top edge is at or above the wave's bottom (vertical overlap)
if (millis() < waveEffects.ragdollUntil) {
If Ragdoll Wave is active, throw the player; otherwise reset to spawn
ragdollPlayer();
Tosses the player randomly in a new direction instead of respawning at the base
break;
Stop checking after the first collision—only one wave can hit per frame

handleBrainrotCollection()

This function uses circular collision detection (distance-based) instead of rectangular. It iterates backwards because we're modifying the array while looping—a common pattern in game loops to avoid skipping elements.

🔬 This loop picks up brainrots automatically via distance. What happens if you remove the break statement so the player can pick up MULTIPLE brainrots at once?

  for (let i = brainrots.length - 1; i >= 0; i--) {
    const b = brainrots[i];
    const d = dist(player.x, player.y, b.x, b.y);
    if (d < (playerSize + b.size) / 2) {
      carriedBrainrot = { ...b };
      brainrots.splice(i, 1);
      break;
    }
  }
function handleBrainrotCollection() {
  if (carriedBrainrot) return;

  for (let i = brainrots.length - 1; i >= 0; i--) {
    const b = brainrots[i];
    const d = dist(player.x, player.y, b.x, b.y);
    if (d < (playerSize + b.size) / 2) {
      carriedBrainrot = { ...b };
      brainrots.splice(i, 1);
      break;
    }
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Single-carry limit if (carriedBrainrot) return;

Prevents picking up another brainrot while already carrying one

for-loop Proximity detection loop for (let i = brainrots.length - 1; i >= 0; i--) {

Checks each brainrot to see if the player is close enough to grab it

calculation Circle collision test const d = dist(player.x, player.y, b.x, b.y);

Calculates distance between player center and brainrot center

if (carriedBrainrot) return;
If the player is already holding a brainrot, skip all pickup logic and exit
for (let i = brainrots.length - 1; i >= 0; i--) {
Loop backwards through the brainrots array so we can safely splice while iterating
const d = dist(player.x, player.y, b.x, b.y);
Calculates the distance in pixels between the player and this brainrot using p5.js dist() function
if (d < (playerSize + b.size) / 2) {
If the distance is less than the sum of their radii, they're overlapping—pick it up
carriedBrainrot = { ...b };
Copies the brainrot object to the carriedBrainrot variable using spread syntax
brainrots.splice(i, 1);
Removes the brainrot from the array so it no longer appears on screen
break;
Stop looping after picking up the first brainrot—only one per frame

activateUtilityWave()

This is a large switch statement that handles all 9 utility waves. Each case modifies game state differently—some change wave speed, others clear the screen, others grant bonuses or spawn new brainrots. The max() function in pause/zen/ragdoll prevents a shorter effect from overwriting a longer one already active.

function activateUtilityWave(id) {
  const now = millis();
  switch (id) {
    case 'chaos':
      waves.forEach((wave) => (wave.speed *= 2));
      utilityMessage = 'Chaos Wave unleashed! All waves double speed.';
      utilityMessageTimer = now + 4000;
      break;
    case 'pause':
      waveEffects.pauseUntil = max(waveEffects.pauseUntil, now + 5000);
      utilityMessage = 'Pause Wave: waves frozen.';
      utilityMessageTimer = now + 4000;
      break;
    case 'zen':
      waveEffects.zenUntil = max(waveEffects.zenUntil, now + 7000);
      utilityMessage = 'Zen Wave: soothing slows applied.';
      utilityMessageTimer = now + 4000;
      break;
    case 'ragdoll':
      waveEffects.ragdollUntil = max(waveEffects.ragdollUntil, now + 8000);
      utilityMessage = 'Ragdoll Wave: brace for bouncy hits.';
      utilityMessageTimer = now + 4000;
      break;
    case 'clear':
      waves = [];
      scheduleNextWave();
      utilityMessage = 'Clear Wave: tsunami wiped out.';
      utilityMessageTimer = now + 4000;
      break;
    case 'infinity':
      waves = [{
        y: spawnLineY,
        h: height * 0.6,
        speed: random(2.4, 3.0),
        color: '#ffffff',
        label: 'Infinity Wave',
        special: 'infinity',
      }];
      utilityMessage = 'Infinity Wave: an endless white wall approaches!';
      utilityMessageTimer = now + 4000;
      break;
    case 'love':
      waves = [];
      spawnWave({ label: 'Love Wave', color: '#ff7ab8', speed: [1.0, 1.3] });
      coins += 25;
      utilityMessage = 'Love Wave: gentle tides & bonus coins!';
      utilityMessageTimer = now + 4000;
      break;
    case 'wacky':
      waveEffects.wackyUntil = max(waveEffects.wackyUntil, now + 9000);
      waves = [];
      spawnWave();
      utilityMessage = 'Wacky Waves: surge of chaotic waters!';
      utilityMessageTimer = now + 4000;
      break;
    case 'admin':
      waves = [];
      waveEffects.pauseUntil = max(waveEffects.pauseUntil, now + 100000);
      spawnCelestialBrainrot();
      nextCelestialAt = millis() + celestialInterval;
      scheduleNextWave();
      utilityMessage = 'Admin Wave: Waves halted for 100s & a Celestial appeared!';
      utilityMessageTimer = now + 5000;
      break;
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

switch-case Chaos Wave case 'chaos':

Doubles the speed of all current waves immediately

switch-case Pause Wave case 'pause':

Freezes all waves for 5 seconds

switch-case Zen Wave case 'zen':

Slows all waves to 35% speed for 7 seconds

switch-case Infinity Wave case 'infinity':

Summons a massive white wave that never despawns

switch-case Admin Wave case 'admin':

Freezes all waves for 100 seconds and spawns a celestial brainrot

const now = millis();
Gets the current time so all effect durations are consistent within this function call
switch (id) {
Routes to different behavior based on which utility wave was purchased
case 'chaos':
Chaos Wave: modifies existing waves instead of creating new ones
waves.forEach((wave) => (wave.speed *= 2));
Iterates through all waves and multiplies each wave's speed by 2 instantly
waveEffects.pauseUntil = max(waveEffects.pauseUntil, now + 5000);
Sets pauseUntil to 5 seconds from now; max() prevents overwriting a longer pause if already active
case 'infinity':
Infinity Wave: clears all waves and replaces them with one massive white wave
h: height * 0.6,
Makes the Infinity Wave 60% of the screen height—much taller than normal waves
special: 'infinity',
Marks this wave as special so it won't be removed when it reaches the despawn line
case 'admin':
Admin Wave: the most powerful utility—pauses waves for 100 seconds AND spawns a celestial

drawPlayer()

drawPlayer() demonstrates several p5.js techniques: rectMode(CENTER) for centered drawing, push/pop for isolated colorMode changes, and frameCount for animation. The rainbow effect shows how HSB colorMode lets you cycle hue as a single number while keeping saturation and brightness constant.

function drawPlayer() {
  rectMode(CENTER);
  fill(255);
  rect(player.x, player.y, playerSize, playerSize);

  fill(0);
  rect(player.x - 4, player.y - 2, 6, 6);
  rect(player.x + 4, player.y - 2, 6, 6);

  if (carriedBrainrot) {
    const carryY = player.y - playerSize;
    if (carriedBrainrot.rainbow) {
      push();
      colorMode(HSB, 360, 100, 100);
      fill((frameCount * 3) % 360, 90, 100);
      rect(player.x, carryY, carriedBrainrot.size, carriedBrainrot.size, 3);
      pop();
    } else {
      fill(carriedBrainrot.color);
      rect(player.x, carryY, carriedBrainrot.size, carriedBrainrot.size, 3);
    }
    fill(255);
    textAlign(CENTER, BOTTOM);
    textSize(10);
    text(carriedBrainrot.name, player.x, carryY - carriedBrainrot.size / 2 - 4);
  }

  if (ragdollVisualUntil > millis()) {
    push();
    noFill();
    stroke(255, 180, 80);
    strokeWeight(2);
    circle(player.x, player.y, playerSize + 10);
    pop();
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

function-call Player square and eyes rect(player.x, player.y, playerSize, playerSize);

Draws the player as a white square with two black eyes

conditional Carried brainrot rendering if (carriedBrainrot) {

Draws the carried brainrot above the player with rainbow animation if applicable

calculation Rainbow hue cycling fill((frameCount * 3) % 360, 90, 100);

Cycles hue from 0–360 using frameCount, creating a rainbow color animation

conditional Ragdoll indicator if (ragdollVisualUntil > millis()) {

Draws an orange circle around the player when they've been ragdolled

rectMode(CENTER);
Sets rect drawing mode so coordinates are the center, not the top-left corner
fill(255);
Sets fill color to white for the player body
rect(player.x, player.y, playerSize, playerSize);
Draws the player as a white square (22x22 pixels) at their position
rect(player.x - 4, player.y - 2, 6, 6);
Draws the left eye at offset (-4, -2) relative to the player center
if (carriedBrainrot) {
Only draw the carried brainrot if one is being held
const carryY = player.y - playerSize;
Positions the carried brainrot directly above the player (one player size up)
if (carriedBrainrot.rainbow) {
Checks if the brainrot is a rainbow-tier god or celestial brainrot
colorMode(HSB, 360, 100, 100);
Switches to HSB color mode so we can cycle hue smoothly (0–360 degrees)
fill((frameCount * 3) % 360, 90, 100);
Every frame, hue increases by 3 modulo 360, creating a continuous rainbow cycle
if (ragdollVisualUntil > millis()) {
If the ragdoll effect is still active, draw an orange ring around the player

drawWaves()

drawWaves() is simple but effective: it draws full-width colored bars that expand and contract visually based on wave.h and wave.y. The label helps players identify which wave type is approaching.

function drawWaves() {
  textAlign(CENTER, CENTER);
  textSize(12);
  for (const wave of waves) {
    fill(wave.color);
    rect(0, wave.y, width, wave.h);
    fill(255);
    text(wave.label || 'Wave', width / 2, wave.y + wave.h / 2);
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Wave rendering loop for (const wave of waves) {

Iterates through all active waves and draws each as a colored rectangle

textAlign(CENTER, CENTER);
Centers text both horizontally and vertically within the wave
for (const wave of waves) {
Loops through the waves array; each wave is an object with y, h, color, label, and speed
fill(wave.color);
Sets the fill color to the wave's specific color (cyan for Slow, red for Fast, purple for Lightning, etc.)
rect(0, wave.y, width, wave.h);
Draws a rectangle spanning the full screen width at the wave's y position with height wave.h
fill(255);
Changes fill to white for the text label
text(wave.label || 'Wave', width / 2, wave.y + wave.h / 2);
Draws the wave's label (e.g., 'Slow', 'Fast') centered in the middle of the wave rectangle

populateBrainrots()

populateBrainrots() is a simple initialization function called once in setup(). It's also called when restarting the game. The loop count (brainrotTargetCount = 70) controls difficulty—higher numbers mean more brainrots to juggle.

function populateBrainrots() {
  brainrots = [];
  for (let i = 0; i < brainrotTargetCount; i++) {
    brainrots.push(generateBrainrot());
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

assignment Array reset brainrots = [];

Clears any existing brainrots to start fresh

for-loop Brainrot generation loop for (let i = 0; i < brainrotTargetCount; i++) {

Generates 70 brainrots with random positions and tier ranks

brainrots = [];
Empties the brainrots array completely
for (let i = 0; i < brainrotTargetCount; i++) {
Loops 70 times (brainrotTargetCount = 70) to spawn 70 brainrots
brainrots.push(generateBrainrot());
Calls generateBrainrot() to create one brainrot with random position and tier, then adds it to the array

generateBrainrot()

generateBrainrot() demonstrates object creation and conditional logic. The key insight is that tier (rarity) is determined by position—brainrots closer to the spawn line are rarer, creating a difficulty gradient. This makes the game easier as players learn and earn more coins, but harder when they first start.

🔬 Tier is determined by Y position—brainrots at the top are always rarer. What happens if you remove the tierIndex line and replace getTierForY(y) with a constant like getTierForY(height * 0.5) so all brainrots spawn at the same tier?

  const y = random(spawnLineY + 30, redLineY - 30);
  const x = random(30, width - 30);
  const tierIndex = getTierForY(y);
function generateBrainrot() {
  const y = random(spawnLineY + 30, redLineY - 30);
  const x = random(30, width - 30);
  const tierIndex = getTierForY(y);
  const tierData = tierInfo[tierIndex];
  const pool = tierData.pool;
  const name = pool.length ? random(pool) : `${tierData.label} Brainrot`;
  const size = 16 + tierIndex * 3;

  return {
    x,
    y,
    tierIndex,
    name,
    abbr: generateAbbreviation(name),
    color: tierData.color === 'RAINBOW' ? '#ffffff' : tierData.color,
    rainbow: tierData.color === 'RAINBOW',
    size,
    rShift: random(360),
  };
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Random position generation const y = random(spawnLineY + 30, redLineY - 30);

Chooses a random y position between the spawn and despawn lines

function-call Tier determination const tierIndex = getTierForY(y);

Determines brainrot rarity based on its vertical position

conditional Name pool selection const name = pool.length ? random(pool) : `${tierData.label} Brainrot`;

Picks a random name from the tier's pool or generates a generic fallback

calculation Brainrot object return {

Constructs and returns a brainrot object with all required properties

const y = random(spawnLineY + 30, redLineY - 30);
Picks a random y between the spawn line (top) and despawn line (bottom), with 30-pixel padding
const x = random(30, width - 30);
Picks a random x from left to right, staying 30 pixels from the edges
const tierIndex = getTierForY(y);
Calls getTierForY() to map the y position to a tier index (0=Common, 5=God, etc.)
const pool = tierData.pool;
Gets the array of names for this tier (e.g., ['Noobini Pizzanini', 'Tim Cheese', 'Pipi Kiwi'] for Common)
const name = pool.length ? random(pool) : `${tierData.label} Brainrot`;
If the pool has names, pick one randomly; otherwise generate a generic label
const size = 16 + tierIndex * 3;
Size increases with tier: Common=16px, Rare=19px, Epic=22px, ..., God=34px
color: tierData.color === 'RAINBOW' ? '#ffffff' : tierData.color,
Stores the color; if rainbow-tier, use white as a placeholder (rainbow is animated in draw)
rainbow: tierData.color === 'RAINBOW',
Flags whether this brainrot is rainbow-tier so the draw function animates its color
rShift: random(360),
Assigns a random hue offset so multiple rainbow brainrots don't all cycle in sync

getPassiveRate()

getPassiveRate() is a concise use of reduce() to sum values. It's called every frame in updatePassiveIncome() to calculate how many coins the player earns per second from stored brainrots. This creates a meta-game: store rarer brainrots to earn coins faster, which lets you buy upgrades or utility waves.

function getPassiveRate() {
  return inventory.reduce(
    (sum, slot) => (slot ? sum + getBrainrotValue(slot.tierIndex) : sum),
    0
  );
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

function-call Inventory value summation return inventory.reduce(

Sums the values of all brainrots in inventory to calculate passive income rate

return inventory.reduce(
Uses the reduce() array method to fold (aggregate) all inventory values into a single sum
(sum, slot) => (slot ? sum + getBrainrotValue(slot.tierIndex) : sum),
For each slot, if it contains a brainrot, add its value to the sum; otherwise keep sum unchanged
0
Starts the accumulation at 0 (no income if inventory is empty)

📦 Key Variables

player object (p5.Vector)

Stores the player's x and y position on the canvas

let player; // initialized in setup() as createVector(...)
playerSize number

Width and height of the player's hitbox in pixels (22)

const playerSize = 22;
playerSpeed number

Current movement speed; increases when buying Speed upgrades

let playerSpeed = basePlayerSpeed;
carriedBrainrot object or null

The brainrot currently being held by the player; null if not carrying

let carriedBrainrot = null;
inventory array

Array of 8 slots storing brainrots to be sold or earn passive income

let inventory = new Array(inventorySize).fill(null);
brainrots array

Array of all brainrot objects currently on screen waiting to be collected

let brainrots = [];
waves array

Array of active tsunami waves; usually contains 0–1 waves at a time

let waves = [];
coins number

Player's currency; earned by selling brainrots or passively from inventory

let coins = 0;
gameState string

Either 'PLAYING' or 'GAMEOVER'; controls whether updates run

let gameState = 'PLAYING';
waveEffects object

Stores timers for active wave modifiers (pauseUntil, zenUntil, ragdollUntil, wackyUntil)

let waveEffects = { pauseUntil: 0, zenUntil: 0, ragdollUntil: 0, wackyUntil: 0 };
followCursor boolean

If true, player moves toward cursor; if false, uses WASD keys

let followCursor = true;
spawnLineY number

Y-coordinate of the orange line where waves appear (top of play area)

let spawnLineY = 0; // set in initializeLayout()
redLineY number

Y-coordinate of the red line where waves despawn (bottom boundary)

let redLineY = 0; // set in initializeLayout()
base object

Stores base's x, y position and size; player must reach base to store brainrots

let base = { x: 0, y: 0, size: 180 };
safeAreas array of objects

Array of rectangular zones where the player is immune to wave damage

let safeAreas = []; // populated in initializeLayout()
nextWaveAt number

Timestamp (millis) when the next wave should spawn

let nextWaveAt = 0;
nextCelestialAt number

Timestamp when the next celestial brainrot should spawn (every 5 minutes)

let nextCelestialAt = 0;
speedLevel number

How many speed upgrades the player has purchased; affects playerSpeed and cost

let speedLevel = 0;
passiveAccumulator number

Fractional coins earned passively; converted to whole coins when >= 1

let passiveAccumulator = 0;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG handleCarryDeposit()

If inventory is full and player tries to deposit a brainrot, it drops at redLineY - 50. But the dropped brainrot may not be instantly re-pickable if the drop logic doesn't account for immediate re-collision.

💡 Add a temporary immunity flag to the dropped brainrot (e.g., dropTime: millis()) and ignore pickup for 200ms after dropping to prevent immediate re-grabbing.

BUG getTierForY()

Celestial and Secret tier brainrots (tierIndex 6, 7) are never naturally spawned by getTierForY() because it only returns 0–5. They only appear via special spawn functions.

💡 Clarify in comments that generateBrainrot() never creates Secret/Celestial tiers naturally. Or add a small chance in getTierForY() to return 7 (celestial) as an easter egg.

PERFORMANCE drawBrainrots()

Calculates dist() for every brainrot to determine name visibility on every frame, even though distance changes smoothly. For 70+ brainrots, this is unnecessary CPU.

💡 Cache proximity checks or only update visibility every 5–10 frames: if (frameCount % 10 === 0) { updateProximities(); }

STYLE activateUtilityWave()

Very long switch statement (11 cases) makes the function hard to read and modify. Each case has similar message-setting boilerplate.

💡 Extract a helper function: function setUtilityMessage(msg, duration) { utilityMessage = msg; utilityMessageTimer = millis() + duration; } to reduce repetition.

FEATURE Game state management

No pause state—game can only be PLAYING or GAMEOVER. Players can't pause mid-game.

💡 Add a 'PAUSED' state toggled by pressing P. Skip all updates when paused but continue rendering to let players see the screen.

FEATURE Wave spawning

Waves are always fullwidth and uniform height. No visual variation besides color and label.

💡 Add wave.width and wave.startX to allow waves to be narrower or offset, creating dodge/weave mechanics instead of just forward movement.

🔄 Code Flow

Code flow showing setup, draw, updateplayer, updatewaves, checkplayerwavecollision, handlebrinrotcollection, activateutilitywave, drawplayer, drawwaves, populatebrainrots, generatebrainrot, getpassiverate

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

graph TD start[Start] --> setup[setup] setup --> canvas-setup[Canvas Setup] setup --> game-init[Game Initialization] setup --> event-listeners[Event Listeners] setup --> populatebrainrots[populateBrainrots] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-setup href "#sub-canvas-setup" click game-init href "#sub-game-init" click event-listeners href "#sub-event-listeners" click populatebrainrots href "#fn-populatebrainrots" draw --> render-phase[Render Phase] draw --> logic-phase[Logic Phase] click draw href "#fn-draw" click render-phase href "#sub-render-phase" click logic-phase href "#sub-logic-phase" logic-phase --> updateplayer[updatePlayer] logic-phase --> updatewaves[updateWaves] logic-phase --> checkplayerwavecollision[checkPlayerWaveCollision] logic-phase --> handlebrinrotcollection[handleBrainrotCollection] logic-phase --> getpassiverate[getPassiveRate] click updateplayer href "#fn-updateplayer" click updatewaves href "#fn-updatewaves" click checkplayerwavecollision href "#fn-checkplayerwavecollision" click handlebrinrotcollection href "#fn-handlebrinrotcollection" click getpassiverate href "#fn-getpassiverate" updateplayer --> cursor-follow[Cursor-Follow Mode] updateplayer --> wasd-mode[WASD Direct Movement] updateplayer --> carry-penalty[Carrying Speed Reduction] click cursor-follow href "#sub-cursor-follow" click wasd-mode href "#sub-wasd-mode" click carry-penalty href "#sub-carry-penalty" updatewaves --> spawn-check[Wave Spawn Timing] updatewaves --> multiplier-calc[Speed Multiplier Effects] updatewaves --> wave-movement[Wave Position Update] click spawn-check href "#sub-spawn-check" click multiplier-calc href "#sub-multiplier-calc" click wave-movement href "#sub-wave-movement" checkplayerwavecollision --> safe-area-check[Safe Area Immunity] checkplayerwavecollision --> wave-overlap[Vertical Overlap Detection] click safe-area-check href "#sub-safe-area-check" click wave-overlap href "#sub-wave-overlap" handlebrinrotcollection --> already-carrying[Single-Carry Limit] handlebrinrotcollection --> distance-collision[Proximity Detection Loop] click already-carrying href "#sub-already-carrying" click distance-collision href "#sub-distance-collision" distance-collision --> radius-check[Circle Collision Test] click radius-check href "#sub-radius-check" updatewaves --> activateutilitywave[activateUtilityWave] activateutilitywave --> chaos-case[Chaos Wave] activateutilitywave --> pause-case[Pause Wave] activateutilitywave --> zen-case[Zen Wave] activateutilitywave --> infinity-case[Infinity Wave] activateutilitywave --> admin-case[Admin Wave] click activateutilitywave href "#fn-activateutilitywave" click chaos-case href "#sub-chaos-case" click pause-case href "#sub-pause-case" click zen-case href "#sub-zen-case" click infinity-case href "#sub-infinity-case" click admin-case href "#sub-admin-case" draw --> drawplayer[drawPlayer] draw --> drawwaves[drawWaves] click drawplayer href "#fn-drawplayer" click drawwaves href "#fn-drawwaves" drawplayer --> player-body[Player Body] drawplayer --> carried-display[Carried Brainrot Rendering] drawplayer --> rainbow-animation[Rainbow Animation] drawplayer --> ragdoll-visual[Ragdoll Indicator] click player-body href "#sub-player-body" click carried-display href "#sub-carried-display" click rainbow-animation href "#sub-rainbow-animation" click ragdoll-visual href "#sub-ragdoll-visual" drawwaves --> wave-loop[Wave Rendering Loop] click wave-loop href "#sub-wave-loop" populatebrainrots --> clear-array[Array Reset] populatebrainrots --> generation-loop[Brainrot Generation Loop] click clear-array href "#sub-clear-array" click generation-loop href "#sub-generation-loop" generation-loop --> position-calc[Random Position Generation] generation-loop --> tier-calc[Tier Determination] generation-loop --> name-selection[Name Pool Selection] generation-loop --> object-creation[Brainrot Object] click position-calc href "#sub-position-calc" click tier-calc href "#sub-tier-calc" click name-selection href "#sub-name-selection" click object-creation href "#sub-object-creation" getpassiverate --> reduce-fold[Inventory Value Summation] click reduce-fold href "#sub-reduce-fold"

❓ Frequently Asked Questions

What visual experience does the 'Escape Tsunami for Brainrots' sketch provide?

The sketch creates a dynamic and colorful visual experience featuring tsunami waves of varying speeds and effects, as well as a player character navigating through a chaotic environment.

How can users interact with the 'Escape Tsunami for Brainrots' sketch?

Users can control the player character's movement using the WASD keys or by following the cursor, while also managing an inventory of brainrots to escape incoming tsunami waves.

What creative coding techniques does the 'Escape Tsunami for Brainrots' sketch demonstrate?

The sketch showcases techniques such as dynamic object spawning, collision detection, and an economy system, all contributing to an engaging gameplay experience.

Preview

escape tsunamis for brainrots - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of escape tsunamis for brainrots - Code flow showing setup, draw, updateplayer, updatewaves, checkplayerwavecollision, handlebrinrotcollection, activateutilitywave, drawplayer, drawwaves, populatebrainrots, generatebrainrot, getpassiverate
Code Flow Diagram