escape tsunamis for brainrots v2

A fast-paced arcade survival game where you collect colorful "brainrot" characters, deposit them in your base for passive income, and dodge tsunamis that spawn from above. You can buy speed upgrades, activate utility waves for strategic advantages, and manage an inventory—all while racing against rising water.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make brainrots spawn in a narrower zone — Change the generateBrainrot() x-range to spawn all brainrots in the center third of the screen, making them cluster rather than spread across the width.
  2. Make waves move twice as fast by default — Increase the base wave speeds by modifying the waveTypes array; all waves will spawn 50% faster.
  3. Give the Speed Shop a bigger discount on upgrades — Change the speedCost multiplier so each upgrade becomes cheaper relative to the previous; try 1.2 (20% increase) instead of 1.4 (40%).
  4. Make god-tier brainrots spawn in the top half instead of bottom — Swap the getTierForY() thresholds so high-ratio (top area) brainrots are god-tier, inverting the rarity distribution.
  5. Triple the passive income multiplier for all brainrots — Increase all tier values in tierInfo to earn coins 3× faster from stored brainrots.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a fast-paced survival arcade game built entirely in p5.js. You pilot a square character across the screen, collecting colorful "brainrot" characters of different rarity tiers and depositing them in your base to earn passive income. Tsunamis spawn from the top and scroll downward; dodge them by staying in safe zones or using special utility waves. The game combines collision detection, animation loops, inventory arrays, a shop system, and event-driven input handling into a complete, playable game.

The code is organized into distinct sections: setup and draw loops, drawing functions for all game elements (player, brainrots, waves, UI), game logic functions for physics and collision, input handlers for keyboard and mouse, and utility functions for layout calculation and data management. By studying it, you'll learn how to structure a complex interactive sketch with multiple game states, manage arrays of objects, implement a wave-spawning system with variable speeds and effects, and build an interactive UI with shops and inventories.

⚙️ How It Works

  1. When setup() runs, the sketch creates a canvas spanning the full window, initializes the player at the base, spawns 70 brainrot characters randomly distributed, and schedules the first tsunami wave to appear after 500ms.
  2. On every frame, draw() clears the background (color 14, very dark), draws all static elements (guides, safe areas, base, shops), then draws all moving objects (brainrots, player, waves). It runs game logic—player movement, brainrot collection, wave spawning—only when gameState is 'PLAYING'.
  3. The player follows the mouse cursor by default (or moves with WASD). When the player touches a brainrot, it's stored in the carriedBrainrot variable and slows the player by 30%. Pressing E near the base deposits it into an inventory slot or drops it if full.
  4. Waves spawn on a timer determined by getCurrentSpawnInterval(). Each wave has a random height, speed, and color that determines its difficulty. Waves scroll downward using the draw loop; if they touch the player outside a safe zone, the player respawns at the base (or ragdolls if the ragdoll effect is active).
  5. Every brainrot stored in inventory earns passive income per second based on its rarity tier. You spend coins at the Speed Shop (press E) to increase movement speed, or in the Utility Waves panel to activate special effects like pause, slow, clear, or spawn special waves.
  6. If any wave reaches the base, gameState becomes 'GAMEOVER' and the screen shows a game-over message. Pressing SPACE calls restartGame() to reset coins, speed, waves, inventory, and spawn fresh brainrots.

🎓 Concepts You'll Learn

Collision detectionGame state managementArray and object manipulationAnimation loopsEvent-driven inputPassive income calculationUI layout and interaction

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas, prepares game objects, and schedules future events. Notice how it calls multiple helper functions to organize initialization logic.

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:

calculation Canvas Setup createCanvas(windowWidth, windowHeight);

Creates a canvas that fills the entire window

initialization Window Event Listeners window.addEventListener('blur', releaseAllKeys);

Releases all held keys when the window loses focus, preventing stuck movement

createCanvas(windowWidth, windowHeight);
Creates a p5 canvas that fills the entire browser window width and height
noSmooth();
Disables anti-aliasing for a crisp, pixelated look that fits the arcade aesthetic
textFont('monospace');
Sets the default font to monospace, giving the UI a retro computer feel
initializeLayout();
Calculates positions of safe areas, base, and spawn lines based on canvas size
initializePlayer();
Creates the player object at the base's starting position
populateBrainrots();
Populates the brainrots array with 70 randomly positioned and tiered characters
nextCelestialAt = millis() + celestialInterval;
Schedules the first celestial brainrot spawn to occur 5 minutes from now
scheduleNextWave(500);
Schedules the first tsunami wave to spawn after 500 milliseconds
window.addEventListener('blur', releaseAllKeys);
When the browser tab loses focus, release all movement keys to prevent stuck keys

draw()

draw() is the main game loop, called ~60 times per second. It has two phases: drawing (rendering all visuals) and logic (updating positions, checking collisions, spawning waves). The order of drawing functions matters—later calls appear on top. Separating drawing from logic makes the code easier to understand and debug.

🔬 The draw() function calls many drawing functions in a specific order. What happens if you move 'drawPlayer()' to the very beginning, before 'drawGuides()'? Will the player appear on top of or behind the guides?

function draw() {
  background(14);

  drawGuides();
  drawSafeAreas();
  drawBase();
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 (5 lines)

🔧 Subcomponents:

conditional Drawing Phase drawGuides(); drawSafeAreas(); drawBase(); drawShop(); drawMarket(); drawBrainrots(); drawPlayer(); drawWaves(); drawUtilityButton();

Renders all visual game elements to the canvas in order

conditional Game Logic Phase (PLAYING) if (gameState === 'PLAYING') { updatePlayer(); updateWaves(); handleBrainrotCollection(); handleCarryDeposit(); checkPlayerWaveCollision(); checkWaveCollision(); updateCelestialSpawner(); updatePassiveIncome(); }

Runs game updates only during active play; skips logic when the game is over

background(14);
Fills the entire canvas with a very dark gray (nearly black), erasing the previous frame
drawGuides(); drawSafeAreas(); drawBase();
Renders static elements: spawn/despawn lines, safe zone areas, and the player's base
drawBrainrots(); drawPlayer(); drawWaves();
Renders all moving game objects: collectible brainrot characters, the player, and tsunami waves
if (gameState === 'PLAYING') { ... } else { drawGameOver(); }
Branches logic based on game state: if playing, update positions and check collisions; if over, show game-over screen
if (confirmSaleIndex !== null) drawConfirmPrompt();
Draws the confirmation dialog for selling a brainrot if the player clicked on an inventory item

updatePlayer()

This function runs every frame and implements two movement modes toggled by pressing TAB. Cursor-following uses lerp for smooth easing, while WASD uses direct pixel increments. Notice how carrying a brainrot slows the player—this is a classic game design mechanic that adds challenge and forces strategic thinking.

🔬 In WASD mode, this code calculates moveX by subtracting left from right. What happens if you swap the order and write `moveX = (moveLeft ? 1 : 0) - (moveRight ? 1 : 0)`? Which directions reverse?

  } 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;
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 (7 lines)

🔧 Subcomponents:

conditional Cursor-Follow Mode 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); }

Smoothly moves the player toward the mouse cursor using lerp (linear interpolation), slowing easing when carrying a brainrot

conditional WASD Movement Mode } 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;

Implements keyboard-based movement: calculates direction from movement flags, applies carry penalty, and updates position

const targetX = constrain(mouseX, playerSize / 2, width - playerSize / 2);
Calculates where the player should move toward, clamped so the player stays within canvas bounds (accounting for player radius)
const ease = carriedBrainrot ? 0.15 : 0.2;
If carrying a brainrot, use slower easing (0.15) for sluggish movement; otherwise use 0.2 for snappier response
player.x = lerp(player.x, targetX, ease);
Smoothly interpolates the player's x position toward the target using lerp, creating fluid cursor-following motion
const moveX = (moveRight ? 1 : 0) - (moveLeft ? 1 : 0);
Calculates horizontal direction: +1 if D is pressed, -1 if A is pressed, 0 if neither or both pressed
const effectiveSpeed = playerSpeed * (carriedBrainrot ? carrySlowFactor : 1);
Multiplies playerSpeed by 0.7 if carrying a brainrot (30% slower), otherwise uses full speed
player.x += moveX * effectiveSpeed;
Moves the player by effectiveSpeed pixels in the calculated direction each frame
player.x = constrain(player.x, playerSize / 2, width - playerSize / 2);
Clamps the player's position to stay within the canvas bounds

handleBrainrotCollection()

This function detects collision between the player and brainrots using distance-based collision. The pattern—check, create, remove—is a classic game mechanic. Note the backward loop: this is essential when removing items from an array during iteration, because removing an item shifts all subsequent indices.

🔬 This loop goes backward (i--) instead of forward. Why? What would happen if you changed it to `for (let i = 0; i < brainrots.length; i++)` and we remove an item? (Hint: the loop index would skip an element.)

  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) {
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:

for-loop Brainrot Collision Loop 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) {

Loops backward through all brainrots and detects when the player's radius touches a brainrot's radius

if (carriedBrainrot) return;
Exit early if the player is already carrying a brainrot—can only carry one at a time
for (let i = brainrots.length - 1; i >= 0; i--) {
Loop backward through the brainrots array so we can safely remove an item without skipping elements
const d = dist(player.x, player.y, b.x, b.y);
Calculate the distance between the player and this brainrot using p5's dist() function
if (d < (playerSize + b.size) / 2) {
If distance is less than the sum of their radii, the player and brainrot are overlapping (collision detected)
carriedBrainrot = { ...b };
Copy the brainrot object into carriedBrainrot using the spread operator (...)
brainrots.splice(i, 1);
Remove this brainrot from the array so it disappears from the game world
break;
Stop checking other brainrots (only one collection per frame)

handleCarryDeposit()

This function handles the core economy: converting collected brainrots into inventory items, which then generate passive income. The fullness check forces a choice—drop or find a slot—adding strategic depth. Spawning a replacement maintains game flow.

function handleCarryDeposit() {
  if (!carriedBrainrot) return;
  if (player.y + playerSize / 2 < redLineY) return;

  const slotIndex = inventory.findIndex((slot) => slot === null);
  if (slotIndex === -1) {
    dropCarriedBrainrot(base.x + random(-base.size / 3, base.size / 3), redLineY - 50);
    return;
  }

  inventory[slotIndex] = { ...carriedBrainrot };
  lastCollected = carriedBrainrot;
  carriedBrainrot = null;
  spawnSingleBrainrot();
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Deposit Zone Check if (player.y + playerSize / 2 < redLineY) return;

Only allows deposits when the player is in the base area (below the red despawn line)

if (!carriedBrainrot) return;
Exit early if the player is not carrying a brainrot
if (player.y + playerSize / 2 < redLineY) return;
Exit if the player is above the red line (not in the deposit zone); brainrots can only be deposited near the base
const slotIndex = inventory.findIndex((slot) => slot === null);
Find the first empty (null) slot in the inventory array, or return -1 if full
if (slotIndex === -1) { dropCarriedBrainrot(...); return; }
If no empty slot, drop the brainrot near the base instead of storing it
inventory[slotIndex] = { ...carriedBrainrot };
Copy the carried brainrot into the empty inventory slot
lastCollected = carriedBrainrot;
Remember which brainrot was just collected for display in the UI
carriedBrainrot = null;
Clear the carried brainrot variable so the player can collect another
spawnSingleBrainrot();
Spawn a replacement brainrot to maintain the target count

updateWaves()

This function is the heart of the wave system. It spawns waves on a timer, applies multiple simultaneous effects to wave speed (stacking multipliers), and removes waves when they leave the play area. The pause mechanic is implemented simply: skip the position update if paused. This design makes adding new effects easy—just add another speed multiplier.

🔬 These lines stack speed modifiers by multiplying them together. If zen is active (×0.35) AND wacky is active (×1.2), what's the combined multiplier? (Multiply: 0.35 × 1.2) Can you add a 'chaos' effect that doubles speed?

  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 (7 lines)

🔧 Subcomponents:

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

Spawns a new wave when the canvas is empty and the spawn timer has elapsed

calculation Speed Multiplier Calculation let speedMultiplier = globalWaveScalar; if (now < waveEffects.zenUntil) speedMultiplier *= 0.35; if (now < loveWaveTimer) speedMultiplier *= 0.8; if (now < waveEffects.wackyUntil) speedMultiplier *= 1.2;

Calculates combined speed modifiers from active utility wave effects

for-loop Wave Movement & Despawn 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) {

Moves waves downward each frame and removes them when they reach the despawn line

const now = millis();
Get the current time in milliseconds for timer comparisons
if (waves.length === 0 && now >= nextWaveAt) {
If no waves are currently active and the spawn timer has elapsed, spawn a new wave
const paused = now < waveEffects.pauseUntil;
Check if a pause effect is active; if so, waves won't move this frame
let speedMultiplier = globalWaveScalar;
Start with the base speed multiplier (usually 1, modified by utility waves)
if (now < waveEffects.zenUntil) speedMultiplier *= 0.35;
If the zen effect is active, multiply speed by 0.35 (slow waves to 35% speed)
if (!paused) wave.y += wave.speed * speedMultiplier;
Move the wave downward by its speed times the multiplier, but only if not paused
if (wave.y + wave.h >= redLineY) {
If the wave's bottom edge reaches the despawn line, remove it and schedule the next wave

checkPlayerWaveCollision()

This function combines safe zones with collision detection. Notice the elegant use of safe areas as a strategy mechanic—players must position themselves wisely. The ragdoll check adds an optional "fun" penalty that differs from the standard respawn.

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 (5 lines)

🔧 Subcomponents:

conditional Safe Area Immunity if (isInSafeArea(player.x, player.y)) return;

Exits early if the player is in a safe zone, preventing collision damage

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

Detects if the player's y-range overlaps with any wave's y-range

if (isInSafeArea(player.x, player.y)) return;
Exit early if the player is in a safe zone—safe zones are immune to waves
for (const wave of waves) {
Loop through all active waves
if (player.y + playerSize / 2 >= wave.y && player.y - playerSize / 2 <= wave.y + wave.h) {
Check if the player's bottom edge is at or below the wave's top AND the player's top is at or above the wave's bottom (vertical overlap)
if (millis() < waveEffects.ragdollUntil) { ragdollPlayer(); } else { respawnPlayer(); }
If the ragdoll effect is active, toss the player; otherwise, instantly teleport them back to the base
break;
Stop checking other waves (only one collision response per frame)

spawnCelestialBrainrot()

This function is called by the celestial spawner on a timer (~every 5 minutes). It demonstrates object creation, array insertion, and data-driven design using the tierInfo configuration. The message system creates transient UI feedback.

function spawnCelestialBrainrot() {
  const tierIndex = tierInfo.findIndex((tier) => tier.id === 'celestial');
  if (tierIndex === -1) return;

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

  brainrots.push({
    x,
    y,
    tierIndex,
    name,
    abbr: generateAbbreviation(name),
    color: tierData.color,
    rainbow: tierData.color === 'RAINBOW',
    size,
    rShift: random(360),
  });

  utilityMessage = 'Celestial Brainrot has spawned!';
  utilityMessageTimer = millis() + 4000;
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Tier Data Lookup const tierIndex = tierInfo.findIndex((tier) => tier.id === 'celestial');

Finds the tier data object for celestial brainrots using findIndex()

calculation Brainrot Object Creation brainrots.push({ x, y, tierIndex, name, abbr, color, rainbow, size, rShift });

Creates and adds a new brainrot object with all required properties

const tierIndex = tierInfo.findIndex((tier) => tier.id === 'celestial');
Search the tierInfo array for the tier with id='celestial' and get its index
if (tierIndex === -1) return;
If celestial tier not found, exit early
const x = random(40, width - 40);
Pick a random x position with 40-pixel margins from the edges
const y = random(spawnLineY + 30, redLineY - 30);
Pick a random y position within the playable zone (between spawn and despawn lines)
const name = tierData.pool.length ? random(tierData.pool) : `${tierData.label} Brainrot`;
Use a name from the tier's pool if available, otherwise generate a default name
const size = 16 + tierIndex * 3;
Calculate size based on tier: higher tiers are larger (tier 7 = 16 + 7*3 = 37 pixels)
brainrots.push({ x, y, tierIndex, name, ... });
Create a brainrot object and add it to the brainrots array
utilityMessage = 'Celestial Brainrot has spawned!';
Set a message to display in the UI for 4 seconds

activateUtilityWave(id)

This is a switch-case statement routing different utility purchases to different effects. Each case modifies game state (wave speeds, effect timers, coins) and displays a message. The max() function on timers is key: it ensures longer effects don't get overwritten by shorter ones if both are active. This function demonstrates how to create a flexible power-up system.

🔬 The chaos case uses forEach to double every wave's speed. How would you write a similar case for 'slowmo' that halves all wave speeds instead? (Change *= 2 to *= 0.5)

    case 'chaos':
      waves.forEach((wave) => (wave.speed *= 2));
      utilityMessage = 'Chaos Wave unleashed! All waves double speed.';
      utilityMessageTimer = now + 4000;
      break;
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); // 100s
      spawnCelestialBrainrot();
      nextCelestialAt = millis() + celestialInterval;
      scheduleNextWave();
      utilityMessage = 'Admin Wave: Waves halted for 100s & a Celestial appeared!';
      utilityMessageTimer = now + 5000;
      break;
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

switch-case Effect Switch switch (id) { case 'chaos': ... case 'pause': ... }

Routes each utility wave ID to its unique effect logic

const now = millis();
Get the current time for calculating when effects expire
case 'chaos': waves.forEach((wave) => (wave.speed *= 2));
Doubles the speed of all active waves immediately
case 'pause': waveEffects.pauseUntil = max(waveEffects.pauseUntil, now + 5000);
Sets pauseUntil to 5 seconds from now; max() ensures effects don't overwrite longer durations
case 'zen': waveEffects.zenUntil = max(waveEffects.zenUntil, now + 7000);
Activates zen slowdown for 7 seconds; updateWaves() will apply the 0.35× multiplier
case 'clear': waves = []; scheduleNextWave();
Clears all active waves and schedules the next spawn
case 'infinity': waves = [{ ... special: 'infinity' }];
Spawns a huge, slow white wave that doesn't despawn (special flag prevents removal)
case 'love': coins += 25;
The love wave grants bonus coins in addition to spawning a gentle pink wave
case 'admin': waveEffects.pauseUntil = max(waveEffects.pauseUntil, now + 100000);
Pauses waves for 100 seconds (100,000ms) and spawns a celestial brainrot

drawWaves()

This function is simple but effective: loop through waves and draw each as a colored rectangle. The label helps players identify wave types by color. The simplicity here allows updateWaves() to focus on logic.

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 (7 lines)

🔧 Subcomponents:

for-loop Wave Rectangle Loop for (const wave of waves) { fill(wave.color); rect(0, wave.y, width, wave.h);

Draws a colored rectangle for each active wave

textAlign(CENTER, CENTER);
Center-align text both horizontally and vertically
textSize(12);
Set font size to 12 pixels for wave labels
for (const wave of waves) {
Loop through all active waves
fill(wave.color);
Set the fill color using the wave's color property (e.g., '#ff3b3b' for red)
rect(0, wave.y, width, wave.h);
Draw a rectangle spanning the full width at the wave's y position with the wave's height
fill(255);
Change fill to white for the label text
text(wave.label || 'Wave', width / 2, wave.y + wave.h / 2);
Draw the wave's label centered within the wave rectangle

generateBrainrot()

This factory function creates a brainrot object with all properties initialized based on game rules. It uses getTierForY() to tie rarity to geography—lower brainrots are rarer. The pool system allows each tier to have its own set of funny names, making the game feel rich. The rainbow property demonstrates conditional rendering based on data.

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 Tier Assignment by Y Position const tierIndex = getTierForY(y);

Determines the brainrot's rarity tier based on its vertical position

calculation Brainrot Object Construction return { x, y, tierIndex, name, abbr, color, rainbow, size, rShift };

Creates and returns a complete brainrot object with all properties

const y = random(spawnLineY + 30, redLineY - 30);
Pick a random y position in the playable zone (with 30-pixel margins)
const x = random(30, width - 30);
Pick a random x position with 30-pixel margins from left and right edges
const tierIndex = getTierForY(y);
Call getTierForY() to determine the brainrot's tier (0-5) based on its y position
const pool = tierData.pool;
Get the name pool for this tier (e.g., ['Noobini Pizzanini', 'Tim Cheese', 'Pipi Kiwi'] for common)
const name = pool.length ? random(pool) : `${tierData.label} Brainrot`;
Pick a random name from the tier's pool, or generate a default name if the pool is empty
const size = 16 + tierIndex * 3;
Higher tiers are bigger: common (tier 0) = 16px, rare (tier 1) = 19px, ..., god (tier 5) = 31px
color: tierData.color === 'RAINBOW' ? '#ffffff' : tierData.color,
If the tier is rainbow, use white as the base color (rainbow animation is applied in drawBrainrots); otherwise use the tier color
rainbow: tierData.color === 'RAINBOW',
Set a flag indicating whether this brainrot should animate with HSB color cycling
rShift: random(360),
Pick a random HSB offset so multiple rainbow brainrots don't all animate in sync

getTierForY(y)

This function implements a classic game design idea: geography determines loot quality. By remapping y position to a 0-1 ratio and using thresholds, the code creates distinct rarity zones without hardcoding pixel values. This makes the system resilient to canvas size changes.

🔬 This function maps y position to rarity tiers. Currently, the bottom 10% spawns god brainrots. What if you wanted the bottom 30% to spawn god brainrots instead? Which threshold would you change from 0.9 to 0.7?

  const ratio = (redLineY - y) / (redLineY - spawnLineY);
  if (ratio < 0.18) return 0; // Common
  if (ratio < 0.32) return 1; // Rare
  if (ratio < 0.55) return 2; // Epic
function getTierForY(y) {
  const ratio = (redLineY - y) / (redLineY - spawnLineY);
  if (ratio < 0.18) return 0; // Common
  if (ratio < 0.32) return 1; // Rare
  if (ratio < 0.55) return 2; // Epic
  if (ratio < 0.75) return 3; // Legendary
  if (ratio < 0.9) return 4; // Mythic
  return 5; // Brainrot God
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Normalized Y Ratio const ratio = (redLineY - y) / (redLineY - spawnLineY);

Converts the y position into a 0-1 scale where 0 is spawn line and 1 is base line

conditional Tier Threshold Ladder if (ratio < 0.18) return 0; if (ratio < 0.32) return 1;

Maps ratio ranges to tier indices, creating different rarity zones

const ratio = (redLineY - y) / (redLineY - spawnLineY);
Normalize the y position to a 0-1 scale: 0 is at spawnLineY (top), 1 is at redLineY (base). Higher y = higher ratio.
if (ratio < 0.18) return 0; // Common
If ratio < 0.18 (bottom 18%), return tier 0 (common) — most brainrots near the top are common
if (ratio < 0.32) return 1; // Rare
If ratio < 0.32, return tier 1 (rare)
if (ratio < 0.55) return 2; // Epic
If ratio < 0.55 (middle area), return tier 2 (epic)
if (ratio < 0.75) return 3; // Legendary
If ratio < 0.75, return tier 3 (legendary)
if (ratio < 0.9) return 4; // Mythic
If ratio < 0.9, return tier 4 (mythic)
return 5; // Brainrot God
Otherwise (ratio >= 0.9, bottom 10%), return tier 5 (god) — rarest brainrots spawn near the base

📦 Key Variables

player object (p5.Vector)

Stores the player's x and y position; created in initializePlayer() and updated in updatePlayer()

let player = createVector(baseX, baseY);
carriedBrainrot object or null

Holds the brainrot object the player is currently carrying, or null if empty-handed

carriedBrainrot = { x: 100, y: 200, name: 'Tim Cheese', tier: 0 };
inventory array of objects or null

Array of 8 slots storing deposited brainrots; null entries represent empty slots

inventory[0] = { name: 'Tim Cheese', tierIndex: 0, color: '#94989f' };
brainrots array of objects

Array of all collectible brainrot objects currently on the canvas

brainrots = [{ x: 50, y: 100, name: 'Lirilì Larilà', tierIndex: 1, ... }, ...];
waves array of objects

Array of active tsunami wave objects; usually contains 0 or 1 wave

waves = [{ y: 100, h: 60, speed: 2.5, color: '#ff3b3b', label: 'Fast' }];
base object

Stores the base's x, y center position and size; where brainrots are deposited

{ x: 400, y: 600, size: 180 }
safeAreas array of objects

Array of rectangular safe zones where the player is immune to waves; two zones on left and right

safeAreas = [{ x: 40, y: 300, w: 126, h: 180 }, { x: 874, y: 300, w: 126, h: 180 }];
coins number

The player's current coin count; earned from passive income, depositing brainrots, or utility wave bonuses

coins = 150;
speedLevel number

The player's current speed upgrade level; incremented when buying speed at the shop

speedLevel = 3;
playerSpeed number

The player's current movement speed in pixels per frame; starts at basePlayerSpeed, increases with speedLevel

playerSpeed = 4 + 3 * 0.6 = 5.8;
waveEffects object with 4 numeric properties

Stores millisecond timestamps for when various utility wave effects expire (pauseUntil, zenUntil, ragdollUntil, wackyUntil)

waveEffects = { pauseUntil: 0, zenUntil: 5000, ragdollUntil: 0, wackyUntil: 0 };
gameState string ('PLAYING' or 'GAMEOVER')

Controls whether the game is actively running or showing a game-over screen

gameState = 'PLAYING';
nextWaveAt number (milliseconds)

Timestamp (from millis()) indicating when the next wave should spawn

nextWaveAt = 5000; // spawn a wave 5 seconds from now
nextCelestialAt number (milliseconds)

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

nextCelestialAt = 300000; // spawn celestial in 5 minutes
followCursor boolean

Toggled by pressing TAB; if true, player follows mouse; if false, player moves with WASD

followCursor = true;
moveLeft, moveRight, moveUp, moveDown boolean

Track which WASD keys are currently pressed (used in WASD movement mode)

moveLeft = true; moveRight = false;
spawnLineY number (pixels)

The y coordinate of the spawn line where waves originate

spawnLineY = 40;
redLineY number (pixels)

The y coordinate of the red despawn line; waves are removed when they pass this line

redLineY = 680;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG handleCarryDeposit()

If the inventory is full and the player tries to deposit a brainrot, it drops at a fixed y position (redLineY - 50), which could cause it to spawn inside or behind the base.

💡 Calculate the drop position relative to the player's current location to ensure it lands safely: `dropCarriedBrainrot(player.x + random(-40, 40), player.y - 20);`

BUG checkPlayerWaveCollision()

The collision detection only checks vertical overlap (y-axis) and ignores horizontal position. If waves don't span the full width, players could 'phase' through waves on the sides.

💡 Add horizontal bounds checking: `if (player.x + playerSize/2 >= 0 && player.x - playerSize/2 <= width)` to ensure the player is within the wave's horizontal range.

PERFORMANCE drawBrainrots() and drawSlots()

Every frame, the rainbow color is recalculated using `(frameCount * 3 + b.rShift) % 360` and HSB colorMode conversion. With 70+ brainrots, this creates unnecessary CPU overhead.

💡 Pre-calculate rainbow frames once per brainrot spawn (store in the object), or use a shared rainbow offset and update it once per frame outside the loop.

STYLE utilityOptions array and activateUtilityWave()

The utility wave effects are defined in two places: the label/description in utilityOptions and the switch-case logic in activateUtilityWave(). Adding a new wave requires edits in both locations, risking inconsistency.

💡 Refactor to store effect implementations as functions in each utilityOptions object: `{ id: 'pause', label: '...', cost: 70, effect: (now) => { ... } }` and call them in activateUtilityWave().

FEATURE game logic

There is no persistence—coins, upgrades, and inventory are lost on restart. Players cannot keep progress.

💡 Add localStorage integration: save and restore coins, speedLevel, and inventory state using `localStorage.setItem()` and `localStorage.getItem()`.

BUG updateWaves()

The 'infinity' wave has a special flag to prevent despawn, but if a player buys 'clear' while it's active, it gets removed along with all other waves, breaking the intended persistence.

💡 Modify the 'clear' case to check: `waves = waves.filter(w => w.special === 'infinity');` to preserve special waves.

🔄 Code Flow

Code flow showing setup, draw, updateplayer, handlebrinrotcollection, handlecarrydeposit, updatewaves, checkplayerwavecollision, spawncelestialbrainrot, activateutilitywave, drawwaves, generatebrainrot, gettierfory

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

graph TD start[Start] --> setup[setup] setup --> canvas-create[Canvas Setup] setup --> event-listeners[Window Event Listeners] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-create href "#sub-canvas-create" click event-listeners href "#sub-event-listeners" draw --> game-logic-phase[Game Logic Phase] draw --> drawing-phase[Drawing Phase] click draw href "#fn-draw" click game-logic-phase href "#sub-game-logic-phase" click drawing-phase href "#sub-drawing-phase" game-logic-phase --> updateplayer[updateplayer] game-logic-phase --> updatewaves[updatewaves] game-logic-phase --> handlebrinrotcollection[handlebrinrotcollection] game-logic-phase --> handlecarrydeposit[handlecarrydeposit] game-logic-phase --> checkplayerwavecollision[checkplayerwavecollision] click updateplayer href "#fn-updateplayer" click updatewaves href "#fn-updatewaves" click handlebrinrotcollection href "#fn-handlebrinrotcollection" click handlecarrydeposit href "#fn-handlecarrydeposit" click checkplayerwavecollision href "#fn-checkplayerwavecollision" updateplayer --> cursor-follow-mode[Cursor-Follow Mode] updateplayer --> wasd-mode[WASD Movement Mode] click cursor-follow-mode href "#sub-cursor-follow-mode" click wasd-mode href "#sub-wasd-mode" handlebrinrotcollection --> collision-loop[Brainrot Collision Loop] click collision-loop href "#sub-collision-loop" handlecarrydeposit --> deposit-zone-check[Deposit Zone Check] handlecarrydeposit --> empty-slot-search[Empty Slot Search] click deposit-zone-check href "#sub-deposit-zone-check" click empty-slot-search href "#sub-empty-slot-search" updatewaves --> wave-spawn-check[Wave Spawn Timer] updatewaves --> speed-multiplier[Speed Multiplier Calculation] updatewaves --> wave-movement-loop[Wave Movement & Despawn] click wave-spawn-check href "#sub-wave-spawn-check" click speed-multiplier href "#sub-speed-multiplier" click wave-movement-loop href "#sub-wave-movement-loop" checkplayerwavecollision --> safe-area-check[Safe Area Immunity] checkplayerwavecollision --> collision-detection[Vertical Overlap Detection] click safe-area-check href "#sub-safe-area-check" click collision-detection href "#sub-collision-detection" draw --> drawwaves[drawwaves] drawwaves --> wave-render-loop[Wave Rectangle Loop] click drawwaves href "#fn-drawwaves" click wave-render-loop href "#sub-wave-render-loop" spawncelestialbrainrot[spawncelestialbrainrot] --> tier-lookup[Tier Data Lookup] spawncelestialbrainrot --> object-creation[Brainrot Object Creation] click spawncelestialbrainrot href "#fn-spawncelestialbrainrot" click tier-lookup href "#sub-tier-lookup" click object-creation href "#sub-object-creation" activateutilitywave[activateutilitywave] --> switch-statement[Effect Switch] click activateutilitywave href "#fn-activateutilitywave" click switch-statement href "#sub-switch-statement" generatebrainrot[generatebrainrot] --> tier-assignment[Tier Assignment by Y Position] generatebrainrot --> object-return[Brainrot Object Construction] click generatebrainrot href "#fn-generatebrainrot" click tier-assignment href "#sub-tier-assignment" click object-return href "#sub-object-return" tier-assignment --> ratio-calculation[Normalized Y Ratio] tier-assignment --> tier-threshold-ladder[Tier Threshold Ladder] click ratio-calculation href "#sub-ratio-calculation" click tier-threshold-ladder href "#sub-tier-threshold-ladder"

❓ Frequently Asked Questions

What visual elements are featured in the escape tsunamis for brainrots v2 sketch?

This sketch showcases colorful tsunami waves in various speeds and types, along with a player character navigating through these waves while collecting brainrots.

How can users interact with the escape tsunamis for brainrots v2 sketch?

Users can control the player character's movement using the WASD keys, collect items, and manage an inventory while avoiding incoming tsunami waves.

What creative coding techniques are demonstrated in the escape tsunamis for brainrots v2 sketch?

The sketch utilizes procedural generation for wave types and spawning, as well as dynamic player movement and inventory management to enhance gameplay.

Preview

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