escape tsunamis for brainrots v2 (Remix)

This is a fast-paced arcade game where players collect colorful "brainrot" creatures and deposit them in a base while evading incoming tsunami waves. The game combines resource collection, inventory management, shop upgrades, and special wave power-ups to create a chaotic, score-driven experience.

🧪 Try This!

Experiment with the code by making these changes:

  1. Double the number of starting brainrots — More creatures on screen makes collecting busier and more chaotic—the game becomes visually packed.
  2. Make the player much faster — Increasing base speed makes the game feel snappier and more responsive to controls.
  3. Spawn waves twice as often — Lowering wave spawn rate creates faster, more intense waves that arrive more frequently.
  4. Make carrying much slower — Reducing the carry speed factor makes brainrots feel heavier, adding more penalty to carrying them.
  5. Turn the background bright white — Changing background darkness completely inverts the visual tone—from dark arcade to bright minimalist.
  6. Make the player character bright blue
Prefer the full editor? Open it there →

📖 About This Sketch

Escape Tsunamis for Brainrots is a fast-paced arcade game that challenges players to collect floating creatures while staying ahead of incoming waves. The visuals blend minimalist geometric shapes with rainbow animations and multiple game zones (safe areas, shops, markets, base). It teaches several advanced p5.js techniques: the draw loop for real-time game state, collision detection for player-wave and player-brainrot interactions, UI layout and interaction, inventory systems, and state management across play and game-over screens.

The code is organized into three major sections: initialization (setup, layout, spawning), drawing functions (one for each game element), and game logic (physics, collisions, input handling, shop mechanics). By studying it, you'll learn how to structure a complete game in p5.js—how to manage multiple game objects, respond to user input, track state over time, and coordinate dozens of visual and logical systems into a cohesive whole.

⚙️ How It Works

  1. When the sketch loads, setup() initializes the canvas, creates the base in the lower center, spawns 70 brainrot creatures scattered between the spawn and despawn lines, and schedules the first tsunami wave.
  2. Every frame, draw() clears the background and renders all game elements: guides (spawn and despawn lines), safe areas, the base with inventory slots, shop and market UI, all brainrots, the player, waves, and utility buttons.
  3. The updatePlayer() function moves the player either by following the cursor (smoothly lerped) or via WASD keys, constraining movement within canvas bounds and slowing speed if carrying a brainrot.
  4. updateWaves() spawns new tsunamis at intervals and advances them downward, applying speed modifiers based on active utility effects (pause, zen slow, love gentle, wacky fast). When waves reach the despawn line or hit the base, the game ends or the wave clears.
  5. handleBrainrotCollection() picks up brainrots when the player touches them, and handleCarryDeposit() stores them in inventory slots once the player reaches the base area; if inventory is full, the brainrot is dropped.
  6. The market, shop, and utility panel let players sell brainrots for coins, buy speed upgrades, or activate special wave effects (chaos, pause, zen, ragdoll, clear, infinity, love, wacky, admin). Passive income is earned each frame based on the value of brainrots in storage.

🎓 Concepts You'll Learn

Game state managementReal-time collision detectionInventory and resource managementUI layout and interactionObject animation and interpolationArray filtering and updatesEvent input handlingPassive income calculation

📝 Code Breakdown

setup()

setup() runs once when the p5.js sketch starts. Use it to initialize your canvas, set up global variables, load resources, and prepare event listeners. In this game, it's where all the initial state is locked into place before the main game loop begins.

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)
createCanvas(windowWidth, windowHeight);
Creates a fullscreen canvas that fills the entire browser window, making the game responsive to window size.
noSmooth();
Disables anti-aliasing for a crisp, pixel-art-like appearance that fits the arcade aesthetic.
textFont('monospace');
Sets all text to monospace font, giving the UI a retro, code-like feel.
initializeLayout();
Calculates and stores positions of the base, safe areas, spawn line, and despawn line based on window dimensions.
initializePlayer();
Places the player character at the starting position (just below the base).
populateBrainrots();
Generates and scatters 70 brainrot creatures across the play area at random positions.
nextCelestialAt = millis() + celestialInterval;
Schedules the first special celestial brainrot to spawn after 5 minutes of play.
scheduleNextWave(500);
Schedules the first tsunami wave to spawn after 500 milliseconds, giving the player a brief moment to orient.
window.addEventListener('blur', releaseAllKeys);
Releases all movement keys when the window loses focus, preventing stuck movement if the player alt-tabs away.

draw()

draw() is the heart of every p5.js sketch—it runs 60 times per second. This function follows a classic game loop pattern: clear the screen, render everything, then update game logic. By separating drawing from updating, you keep code organized and bugs easy to spot.

🔬 These lines draw everything on screen in order. What happens if you rearrange them—for example, moving drawPlayer() before drawBrainrots()? How does the z-ordering (what appears on top) change?

  drawGuides();
  drawSafeAreas();
  drawBase();
  drawShop();
  drawMarket();
  drawBrainrots();
  drawPlayer();
  drawWaves();
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 (11 lines)

🔧 Subcomponents:

sequential-calls Rendering phase drawGuides(); drawSafeAreas(); drawBase(); drawShop(); drawMarket(); drawBrainrots(); drawPlayer(); drawWaves(); drawUtilityButton();

Draws all game elements in order so layering is correct (background, zones, then entities)

conditional Update and logic phase if (gameState === 'PLAYING') { updatePlayer(); updateWaves(); handleBrainrotCollection(); ... }

Only runs game logic when actively playing—pauses all updates if game is over

background(14);
Clears the canvas with a very dark gray (#0E0E0E), creating a dark arcade aesthetic and preventing motion trails.
drawGuides();
Renders the spawn line (orange) and despawn line (red) to show the player safe collection zones.
drawSafeAreas();
Draws two blue safe zones where the player cannot be hit by waves.
drawBase();
Renders the blue base structure and its inventory grid in the lower center.
drawShop();
Renders the speed upgrade shop in the lower right, highlighting when the player is inside.
drawMarket();
Renders the brainrot sale market in the lower left showing sellable items from inventory.
drawBrainrots();
Draws all loose brainrot creatures on the play area, showing their names when the player is near.
drawPlayer();
Renders the player character (white square with eyes) and any carried brainrot above it.
drawWaves();
Renders all active tsunami waves as colored horizontal bars with labels.
if (gameState === 'PLAYING') { ... }
Only updates game logic (positions, collisions, spawning) when the game is actively being played, not when game-over.
if (confirmSaleIndex !== null) drawConfirmPrompt();
If a sale is pending confirmation, draws a modal dialog with Sell and Cancel buttons.

updatePlayer()

This function shows two movement paradigms: smooth cursor-following (using lerp for easing) and discrete WASD movement (velocity-based). Both are common in games, and this sketch teaches you how to toggle between them. The carry penalty teaches a key game design principle: tangible consequences make mechanics feel real.

🔬 These lines calculate movement direction and then apply a speed penalty if carrying. What happens if you remove the carriedBrainrot check and always multiply by carrySlowFactor? How does the game feel?

    const moveX = (moveRight ? 1 : 0) - (moveLeft ? 1 : 0);
    const moveY = (moveDown ? 1 : 0) - (moveUp ? 1 : 0);
    const effectiveSpeed = playerSpeed * (carriedBrainrot ? carrySlowFactor : 1);
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 interpolates player position toward the mouse cursor using lerp

conditional WASD keyboard mode } else { ... }

Moves player by discrete velocity steps based on WASD key states

if (followCursor) {
Checks if the player has cursor-follow mode enabled (default), or WASD mode.
const targetX = constrain(mouseX, playerSize / 2, width - playerSize / 2);
Calculates where the player should move toward (mouse X), but keeps it within canvas bounds so the player doesn't go off-screen.
const ease = carriedBrainrot ? 0.15 : 0.2;
If carrying a brainrot, use slower easing (0.15) for sluggish movement; otherwise use 0.2 for responsive movement. This teaches player feedback through movement.
player.x = lerp(player.x, targetX, ease);
Smoothly blends the player's current X position toward the target X using linear interpolation (lerp)—the smaller the ease value, the more smooth and floaty the motion.
const moveX = (moveRight ? 1 : 0) - (moveLeft ? 1 : 0);
Converts left/right key states to a velocity: -1 (left), 0 (still), or +1 (right).
const effectiveSpeed = playerSpeed * (carriedBrainrot ? carrySlowFactor : 1);
Multiplies base speed by 0.7 if carrying a brainrot, representing weight and burden—a core game mechanic.
player.x += moveX * effectiveSpeed;
Updates X position by adding (moveX * speed), moving the player instantly in WASD mode instead of smoothly.
player.x = constrain(player.x, playerSize / 2, width - playerSize / 2);
Clamps the player's position to stay within the canvas, preventing them from escaping off the edges.

updateWaves()

This function demonstrates layered state management: multiple timers (pauseUntil, zenUntil, etc.) each track independent effects, and their multipliers are combined into a single speedMultiplier. This is how complex game mechanics stay organized—each effect is independent, but their results compose together.

🔬 These lines stack multiple effects (zen, love, wacky) by multiplying together. What happens if two effects are active at once? For example, if both zen (0.35) and wacky (1.2) are active, what is the final multiplier? Try adding console.log(speedMultiplier) after each line to see the stacking in action.

  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 trigger if (waves.length === 0 && now >= nextWaveAt) { spawnWave(); }

Spawns a new wave only if no waves exist and enough time has passed

calculation Speed multiplier stacking let speedMultiplier = globalWaveScalar; if (now < waveEffects.zenUntil) speedMultiplier *= 0.35; ...

Combines all active speed effects (pause, zen, love, wacky) into a single multiplier

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

Advances each wave downward and removes waves that have passed the despawn line

const now = millis();
Stores the current time in milliseconds, used for all time-based checks this frame.
if (waves.length === 0 && now >= nextWaveAt) {
Spawns a new wave only if the screen is currently clear (no waves) AND enough time has passed since the last wave was cleared.
const paused = now < waveEffects.pauseUntil;
Checks if the pause utility effect is still active (its timer hasn't expired).
let speedMultiplier = globalWaveScalar;
Starts with a base multiplier (usually 1) to which all active effects will be applied.
if (now < waveEffects.zenUntil) speedMultiplier *= 0.35;
If zen mode is active, multiply speed by 0.35—making waves move much slower for 7 seconds.
if (now < loveWaveTimer) speedMultiplier *= 0.8;
If a love wave is active, reduce speed to 80% of normal—a gentle difficulty ease.
if (now < waveEffects.wackyUntil) speedMultiplier *= 1.2;
If wacky mode is active, increase speed to 120%—creating chaotic, faster waves.
if (!paused) wave.y += wave.speed * speedMultiplier;
If not paused, advances the wave's Y position downward by its base speed multiplied by all stacked effects.
if (wave.y + wave.h >= redLineY) {
Checks if the wave has reached or passed the despawn line (bottom boundary).
if (wave.special !== 'infinity') { waves.splice(i, 1); scheduleNextWave(); }
Removes the wave from the array and schedules the next one—unless it's an infinity wave which persists.

checkPlayerWaveCollision()

This function teaches 1D axis-aligned bounding box (AABB) collision: you only need to check one axis (Y in this case) because waves span the entire width. Safe areas introduce game design depth by giving players refuge zones. The ragdoll mode adds dynamism—sometimes you bounce instead of reset, keeping the game surprising.

🔬 This collision test checks vertical overlap (Y bounds). Currently it only checks if the player hits ANY part of a wave. What if you also added an X bounds check? Would waves need to fill the entire width to hit the player, or could they be narrower? Try adding: && player.x + playerSize / 2 >= wave.x && player.x - playerSize / 2 <= wave.x + wave.w

    if (player.y + playerSize / 2 >= wave.y && player.y - playerSize / 2 <= wave.y + wave.h) {
      if (millis() < waveEffects.ragdollUntil) {
        ragdollPlayer();
      } else {
        respawnPlayer();
      }
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;

Early exit if player is in a safe zone—waves cannot hurt them here

for-loop Wave collision detection for (const wave of waves) { if (player.y + playerSize / 2 >= wave.y && player.y - playerSize / 2 <= wave.y + wave.h) { ... } }

Tests if the player's vertical bounds overlap with any wave's vertical bounds

if (isInSafeArea(player.x, player.y)) return;
If the player is inside a safe zone, exit early—no collision possible in safe areas.
for (const wave of waves) {
Loop through all active waves on screen.
if (player.y + playerSize / 2 >= wave.y && player.y - playerSize / 2 <= wave.y + wave.h) {
Tests 1D collision: if the player's bottom is at or below the wave's top AND the player's top is at or above the wave's bottom, they overlap vertically.
if (millis() < waveEffects.ragdollUntil) { ragdollPlayer(); } else { respawnPlayer(); }
If ragdoll mode is active, tosses the player randomly; otherwise, teleports them back to the base.
break;
Exits the loop immediately after processing the collision—don't check multiple waves, only one per frame.

handleBrainrotCollection()

This function teaches two key patterns: circular collision detection (using dist and radius sum) and safe array removal (looping backward before splicing). The one-carry limit is a classic game design constraint that forces decision-making: which brainrot matters most?

🔬 This loop uses backward iteration (i-- from the end). What would happen if you changed it to forward iteration (i++ from 0)? Try it and see if the pickup still works or if it skips brainrots.

  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:

conditional Carry capacity check if (carriedBrainrot) return;

Exit early if already carrying a brainrot—prevents picking up a second one

for-loop Brainrot proximity loop for (let i = brainrots.length - 1; i >= 0; i--) { ... }

Iterates backward through brainrots to safely remove one during the loop

if (carriedBrainrot) return;
Player can only carry one brainrot at a time; if carrying one, exit early.
for (let i = brainrots.length - 1; i >= 0; i--) {
Loop backward through the brainrots array (from last to first). This is crucial: if you loop forward and splice, you skip elements. Looping backward avoids this bug.
const d = dist(player.x, player.y, b.x, b.y);
Calculates the Euclidean distance between the player and this brainrot—p5.js's dist() function computes √((x2-x1)² + (y2-y1)²).
if (d < (playerSize + b.size) / 2) {
Checks if distance is less than the sum of their radii—a simple collision test for two circles.
carriedBrainrot = { ...b };
Copies the brainrot object using spread syntax so carrying it doesn't modify the original.
brainrots.splice(i, 1);
Removes the brainrot from the loose array—it's now carried, not floating anymore.
break;
Exits the loop immediately—only pick up one brainrot per frame.

handleCarryDeposit()

This function models the deposit lifecycle: picking up removes a brainrot, storing it saves it, and spawning a replacement maintains game balance. By replacing collected brainrots, the game stays dynamic and never runs out of things to collect. The inventory full drop-off teaches edge-case handling.

🔬 These lines store the brainrot, clear the carry state, and spawn a replacement. What happens if you comment out spawnSingleBrainrot()? How does the game change? Is collecting rewarding anymore?

  inventory[slotIndex] = { ...carriedBrainrot };
  lastCollected = carriedBrainrot;
  carriedBrainrot = null;
  spawnSingleBrainrot();
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 Base zone detection if (player.y + playerSize / 2 < redLineY) return;

Only allows deposit when player is below the red despawn line (in the base safe zone)

conditional Inventory capacity const slotIndex = inventory.findIndex((slot) => slot === null); if (slotIndex === -1) { ... }

Checks if there's an empty slot; if not, drops the brainrot nearby instead

if (!carriedBrainrot) return;
If not carrying anything, exit early—nothing to deposit.
if (player.y + playerSize / 2 < redLineY) return;
If player is above the red line (despawn boundary), exit—you must be in the base zone to deposit.
const slotIndex = inventory.findIndex((slot) => slot === null);
Searches for the first empty inventory slot (null value). Returns -1 if no empty slots exist.
if (slotIndex === -1) { dropCarriedBrainrot(...); return; }
If inventory is full, drop the brainrot on the ground near the base instead of storing it.
inventory[slotIndex] = { ...carriedBrainrot };
Copies the brainrot into the empty inventory slot—it's now stored and safe.
lastCollected = carriedBrainrot;
Remembers this brainrot so the UI can display 'Last stored: [name]'.
carriedBrainrot = null;
Releases the carried brainrot—the player is no longer holding it.
spawnSingleBrainrot();
Spawns a new loose brainrot to replace the one stored, keeping the total count constant and maintaining game flow.

activateUtilityWave(id)

This function is the game's "power-up" system: each utility wave modifies game state in different ways (timers, arrays, counters). The switch statement routes nine different effects cleanly. Using max() to extend timers teaches an important pattern: effects can overlap and stack without overwriting each other.

🔬 Chaos doubles wave speed with waves.forEach(...). What if you changed it to multiply by 3 instead of 2? Or what if you added: waves = []; to clear waves after doubling? Would chaos become more or less chaotic?

    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);
      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 Chaos Wave (speed boost) case 'chaos': waves.forEach((wave) => (wave.speed *= 2));

Doubles the speed of all current waves, making the game harder for a moment

switch-case Pause Wave effect case 'pause': waveEffects.pauseUntil = max(...)

Sets a pause timer that prevents waves from moving in updateWaves()

switch-case Infinity Wave (special) case 'infinity': waves = [{ ... special: 'infinity' }];

Spawns a massive persistent wave that won't clear at the despawn line

const now = millis();
Captures the current time so all effects are scheduled from the same moment.
switch (id) {
Routes different utility wave types to their own behavior based on the id string.
waves.forEach((wave) => (wave.speed *= 2));
For chaos wave: iterates through all active waves and doubles their speed—a temporary difficulty spike.
waveEffects.pauseUntil = max(waveEffects.pauseUntil, now + 5000);
Sets the pause timer to 5 seconds from now. The max() ensures it extends the pause if one is already active.
waves = [];
Clears the waves array—removes all current waves from the screen instantly.
waves = [{ y: spawnLineY, h: height * 0.6, speed: random(2.4, 3.0), color: '#ffffff', label: 'Infinity Wave', special: 'infinity' }];
Creates a single massive white wave that fills 60% of screen height. The special: 'infinity' flag prevents it from being auto-cleared at the despawn line.
coins += 25;
Grants bonus coins when love wave activates—a reward for buying a nice utility.
utilityMessage = '...'; utilityMessageTimer = now + 4000;
Sets a message to display on screen and schedules it to disappear after 4000ms.

drawPlayer()

This function combines basic shape drawing with conditional rendering and animation. Drawing the carried brainrot above the player provides critical visual feedback—the player instantly knows they're holding something. Ragdoll aura adds polish by warning the player of a special state.

🔬 These two lines draw the left and right eyes. What if you added a third eye in the middle, or moved them lower? Try changing the positions (player.y - 2) to (player.y - 5) to move them up, or add rect(player.x, player.y, 6, 6) for a third eye.

  fill(0);
  rect(player.x - 4, player.y - 2, 6, 6);
  rect(player.x + 4, player.y - 2, 6, 6);
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 (11 lines)

🔧 Subcomponents:

drawing Player body and eyes rectMode(CENTER); fill(255); rect(player.x, player.y, playerSize, playerSize); ... rect(player.x - 4, player.y - 2, 6, 6);

Draws the main player square and two black eyes for personality

conditional Carried brainrot visual if (carriedBrainrot) { ... }

If holding a brainrot, renders it above the player with animated rainbow if applicable

conditional Ragdoll visual feedback if (ragdollVisualUntil > millis()) { circle(...) }

Shows an orange glow when ragdoll mode is active to warn the player

rectMode(CENTER);
Changes rect() to interpret coordinates as the center point instead of top-left, making positioning cleaner.
fill(255);
Sets the fill color to white for the player body.
rect(player.x, player.y, playerSize, playerSize);
Draws a 22x22 white square at the player's position.
fill(0);
Switches fill to black for the eyes.
rect(player.x - 4, player.y - 2, 6, 6);
Draws left eye 4 pixels to the left and 2 pixels up from the player's center.
if (carriedBrainrot) {
Only render the carried item if the player is holding one.
const carryY = player.y - playerSize;
Positions the carried brainrot directly above the player's head by subtracting the player's size.
colorMode(HSB, 360, 100, 100);
Switches to HSB (Hue-Saturation-Brightness) color mode for rainbow animation—HSB is better for rotating through hues.
fill((frameCount * 3) % 360, 90, 100);
Animates the hue by incrementing frameCount each frame, cycling through 0-360 continuously for a rainbow effect.
if (ragdollVisualUntil > millis()) {
Checks if the ragdoll visual timer is still active (hasn't expired yet).
circle(player.x, player.y, playerSize + 10);
Draws an orange glow circle around the player slightly larger than their body—a warning aura.

generateBrainrot()

This function encapsulates brainrot creation logic: position, rarity, visuals, and name are all determined together. By tying tier to Y position, the game creates a skill reward—venturing deeper (toward the red line) yields rarer, higher-value brainrots but more danger from approaching waves.

🔬 These lines pick a random Y, then determine tier from that Y. This means brainrots at the bottom are always rarer. What if you randomly chose the tier first, then positioned based on tier? Try: const tierIndex = floor(random(6)); const y = map(tierIndex, 0, 5, spawnLineY + 30, redLineY - 30);

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

🔧 Subcomponents:

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

Chooses a random position within valid play area boundaries

calculation Tier assignment by Y position const tierIndex = getTierForY(y); const tierData = tierInfo[tierIndex];

Determines rarity based on vertical position—higher = rarer

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

Picks a name from the tier's pool, or generates a generic one if pool is empty

const y = random(spawnLineY + 30, redLineY - 30);
Randomly positions the brainrot vertically between the spawn and despawn lines, with 30-pixel margins for safety.
const x = random(30, width - 30);
Randomly positions horizontally across the width, keeping 30 pixels from each edge.
const tierIndex = getTierForY(y);
Calls getTierForY() to determine rarity tier based on Y position—high Y (near bottom) = rarer, higher value.
const tierData = tierInfo[tierIndex];
Looks up the tier definition (color, value, name pool) using the calculated tier index.
const pool = tierData.pool;
Grabs the array of possible names for this tier.
const name = pool.length ? random(pool) : `${tierData.label} Brainrot`;
If the pool has names, picks one randomly. Otherwise, generates a generic name like 'Rare Brainrot'.
const size = 16 + tierIndex * 3;
Calculates size based on tier—higher tier brainrots are visually bigger (more pixels), making them easier to spot and more rewarding.
return { x, y, tierIndex, name, abbr, color, rainbow, size, rShift };
Returns an object with all properties needed to draw and track this brainrot.

getTierForY(y)

This function uses a single normalized value (ratio) to map spatial position to game value. It's a clean example of level design: by changing threshold values, you adjust the difficulty curve and reward distribution across the map.

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 position ratio const ratio = (redLineY - y) / (redLineY - spawnLineY);

Converts absolute Y position into a 0-1 ratio (0 = spawn line, 1 = despawn line)

conditional Tier threshold checks if (ratio < 0.18) return 0; if (ratio < 0.32) return 1; ...

Maps ratio ranges to tier indices, creating difficulty zones

const ratio = (redLineY - y) / (redLineY - spawnLineY);
Calculates a normalized ratio: (distance from y to redLine) divided by (total distance from spawn to despawn). Result is 0.0 at spawn, 1.0 at despawn.
if (ratio < 0.18) return 0; // Common
If brainrot is in the top 18% of play area, it's Common (tier 0, easiest, worth 5 coins).
if (ratio < 0.32) return 1; // Rare
Between 18-32%, it's Rare (tier 1, worth 8 coins).
if (ratio < 0.55) return 2; // Epic
Between 32-55%, it's Epic (tier 2, worth 15 coins).
if (ratio < 0.75) return 3; // Legendary
Between 55-75%, it's Legendary (tier 3, worth 25 coins).
if (ratio < 0.9) return 4; // Mythic
Between 75-90%, it's Mythic (tier 4, worth 40 coins)—very rare and valuable.
return 5; // Brainrot God
Below 90% (near the bottom), it's a Brainrot God (tier 5, worth 65 coins)—rarest and most dangerous to collect.

keyPressed()

This function demonstrates input handling patterns: keyCode for special keys (TAB), key for character input (a, d, w, s), and context-aware actions (E means different things in different states). By setting flags instead of moving directly, updatePlayer() decouples input from motion, making the code cleaner and more flexible.

🔬 These four lines set movement flags for WASD keys. What if you added diagonal movement by combining keys? Currently, pressing W and D separately moves up, then right. What if you added a new key like Space to trigger a dash, or changed S to reverse?

  if (k === 'a') moveLeft = true;
  if (k === 'd') moveRight = true;
  if (k === 'w') moveUp = true;
  if (k === 's') moveDown = true;
function keyPressed() {
  if (keyCode === TAB) {
    followCursor = !followCursor;
    return false;
  }

  if (gameState === 'GAMEOVER' && key === ' ') {
    restartGame();
    return;
  }
  const k = key.toLowerCase();
  if (k === 'a') moveLeft = true;
  if (k === 'd') moveRight = true;
  if (k === 'w') moveUp = true;
  if (k === 's') moveDown = true;
  if (k === 'e') {
    if (carriedBrainrot) {
      dropCarriedBrainrot(player.x, player.y - playerSize / 2);
    } else {
      attemptSpeedPurchase();
    }
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Tab mode toggle if (keyCode === TAB) { followCursor = !followCursor; return false; }

Toggles between cursor-follow and WASD modes; return false prevents browser default TAB behavior

conditional Game-over restart if (gameState === 'GAMEOVER' && key === ' ') { restartGame(); return; }

Allows pressing SPACE to restart when game is over

conditional WASD movement flags if (k === 'a') moveLeft = true; if (k === 'd') moveRight = true; ...

Sets movement boolean flags when keys are pressed (read by updatePlayer in WASD mode)

conditional E key drop/purchase if (k === 'e') { if (carriedBrainrot) { ... } else { attemptSpeedPurchase(); } }

E drops carried brainrot or buys a speed upgrade depending on current state

if (keyCode === TAB) {
Checks the key code (not the character) for TAB, which toggles movement modes.
followCursor = !followCursor;
Flips the boolean: if true becomes false, false becomes true.
return false;
Returns false to prevent the browser's default TAB behavior (switching focus), keeping the sketch in focus.
if (gameState === 'GAMEOVER' && key === ' ') {
If game is over AND space is pressed, handle restart.
const k = key.toLowerCase();
Converts the key character to lowercase so both 'A' and 'a' register as the same input.
if (k === 'a') moveLeft = true;
Sets moveLeft flag to true when A is pressed. updatePlayer() will read this flag later.
if (k === 'e') {
E key is context-sensitive: it drops a carried brainrot or buys a speed upgrade.
if (carriedBrainrot) { dropCarriedBrainrot(...); } else { attemptSpeedPurchase(); }
If holding a brainrot, drop it. Otherwise, try to buy a speed upgrade at the shop.

mousePressed()

This function orchestrates the entire UI interaction layer: confirmations, panel toggles, purchases, and market transactions. It uses layered conditional checks (return early if a dialog is open, then check the button, then check the panel, etc.) to avoid conflicting interactions.

🔬 This loop finds which utility option was clicked and deducts coins. What happens if you remove the break statement at the end of the loop? Would multiple options activate from a single click, or would it cause bugs?

    for (const optRect of utilityOptionRects) {
      if (pointInRect(mouseX, mouseY, optRect)) {
        clickedOption = true;
        const option = utilityOptions.find((o) => o.id === optRect.id);
        if (option && coins >= option.cost) {
          coins -= option.cost;
          activateUtilityWave(option.id);
        }
function mousePressed() {
  if (confirmSaleIndex !== null) {
    if (pointInRect(mouseX, mouseY, confirmButtons.sell)) {
      sellBrainrot(confirmSaleIndex);
      confirmSaleIndex = null;
    } else if (pointInRect(mouseX, mouseY, confirmButtons.cancel)) {
      confirmSaleIndex = null;
    }
    return;
  }

  const btnRect = getUtilityButtonRect();
  if (pointInRect(mouseX, mouseY, btnRect)) {
    utilityPanelOpen = !utilityPanelOpen;
    if (utilityPanelOpen) utilityScroll = 0;
    return;
  }

  if (utilityPanelOpen) {
    let clickedOption = false;
    for (const optRect of utilityOptionRects) {
      if (pointInRect(mouseX, mouseY, optRect)) {
        clickedOption = true;
        const option = utilityOptions.find((o) => o.id === optRect.id);
        if (option && coins >= option.cost) {
          coins -= option.cost;
          activateUtilityWave(option.id);
        } else if (option) {
          utilityMessage = 'Not enough coins!';
          utilityMessageTimer = millis() + 2000;
        }
        break;
      }
    }
    if (!clickedOption && !pointInRect(mouseX, mouseY, getUtilityPanelRect())) {
      utilityPanelOpen = false;
    }
  }

  if (!pointInRect(player.x, player.y, getMarketArea())) return;

  for (const slot of marketSlotRects) {
    if (
      mouseX >= slot.x &&
      mouseX <= slot.x + slot.w &&
      mouseY >= slot.y &&
      mouseY <= slot.y + slot.h &&
      inventory[slot.index]
    ) {
      confirmSaleIndex = slot.index;
      break;
    }
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Sale confirmation handler if (confirmSaleIndex !== null) { if (pointInRect(..., confirmButtons.sell)) { ... } else if (pointInRect(..., confirmButtons.cancel)) { ... } }

Processes clicks on Sell or Cancel buttons in the confirmation modal

conditional Utility panel toggle const btnRect = getUtilityButtonRect(); if (pointInRect(...)) { utilityPanelOpen = !utilityPanelOpen; }

Toggles the utility waves panel open/closed

conditional Utility option purchase if (utilityPanelOpen) { for (const optRect of utilityOptionRects) { ... } }

Processes clicks on utility wave options and deducts coins if affordable

conditional Market slot selection if (!pointInRect(player.x, player.y, getMarketArea())) return; for (const slot of marketSlotRects) { ... confirmSaleIndex = slot.index; }

Initiates sale confirmation when a player clicks an inventory slot in the market

if (confirmSaleIndex !== null) {
If a sale confirmation dialog is open, prioritize handling its buttons first.
if (pointInRect(mouseX, mouseY, confirmButtons.sell)) {
Checks if the click landed on the Sell button rectangle.
sellBrainrot(confirmSaleIndex); confirmSaleIndex = null;
Calls sellBrainrot() to complete the transaction and closes the confirmation dialog.
const btnRect = getUtilityButtonRect();
Gets the rectangle bounds of the utility button at the bottom center.
if (pointInRect(mouseX, mouseY, btnRect)) { utilityPanelOpen = !utilityPanelOpen; }
If clicked, toggles the utility panel open or closed.
for (const optRect of utilityOptionRects) {
Loops through all visible utility option rectangles to detect which one was clicked.
if (option && coins >= option.cost) { coins -= option.cost; activateUtilityWave(option.id); }
If the player has enough coins, deduct the cost and activate the selected utility wave.
if (!pointInRect(player.x, player.y, getMarketArea())) return;
Only allow market clicks if the player is inside the market area—prevents accidental sales from afar.
for (const slot of marketSlotRects) { ... confirmSaleIndex = slot.index; }
Checks each inventory slot rectangle; if clicked and contains an item, opens the sale confirmation dialog.

📦 Key Variables

player object

Stores the player's x and y position as a p5.Vector, updated each frame to track player location.

let player = createVector(0, 0);
playerSize number

The width and height in pixels of the player's character square (22 pixels).

const playerSize = 22;
playerSpeed number

Current movement speed in pixels per frame, increased by buying speed upgrades.

let playerSpeed = basePlayerSpeed;
speedLevel number

The number of speed upgrades purchased; each increases playerSpeed and the cost of the next upgrade.

let speedLevel = 0;
carriedBrainrot object

Stores the brainrot object currently being held by the player, or null if not carrying.

let carriedBrainrot = null;
inventory array

An array of 8 slots (each null or a brainrot object) representing the player's storage at the base.

let inventory = new Array(8).fill(null);
base object

Stores the base's x, y position and size (diameter)—the player's home zone.

let base = { x: 0, y: 0, size: 180 };
brainrots array

Array of all loose brainrot objects currently on the play area, updated as they're collected or spawned.

let brainrots = [];
waves array

Array of active tsunami wave objects, usually containing 0 or 1 wave at a time.

let waves = [];
coins number

Current coin count earned by selling brainrots or passive income.

let coins = 0;
gameState string

Tracks if game is 'PLAYING' or 'GAMEOVER', controlling which logic runs each frame.

let gameState = 'PLAYING';
waveEffects object

Stores millisecond timestamps for when utility effects (pause, zen, ragdoll, wacky) should expire.

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

Tracks whether the utility waves purchase panel is currently visible.

let utilityPanelOpen = false;
spawnLineY number

The Y coordinate of the orange line marking the top boundary of the play area.

let spawnLineY = 40;
redLineY number

The Y coordinate of the red line marking the bottom boundary where waves despawn.

let redLineY = 0;
moveLeft boolean

Flag set to true when 'A' key is held down, used for WASD movement mode.

let moveLeft = false;
followCursor boolean

When true, player follows mouse smoothly; when false, uses WASD keys for movement.

let followCursor = true;
ragdollVisualUntil number

Millisecond timestamp until which the orange ragdoll aura should be drawn around the player.

let ragdollVisualUntil = 0;
passiveAccumulator number

Fractional coins earned from passive income, accumulated each frame and converted to whole coins.

let passiveAccumulator = 0;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG handleBrainrotCollection() and brainrots array

If multiple brainrots occupy the same space, only the last one in the array is ever collected since the loop breaks after the first collision.

💡 Store the closest brainrot before breaking, or refactor to prioritize by distance: let closest = null; for (const b of brainrots) { const d = dist(...); if (d < ...closest distance...) closest = b; } Then collect closest.

PERFORMANCE drawUtilityPanel() clipping region

The drawingContext clipping path is created every frame, which is wasteful. The clip region never changes during the panel draw.

💡 Calculate the clip path once in setup() or cache it, or use a simpler approach like checking if each option is in bounds before drawing.

STYLE utilityOptions array and activateUtilityWave() switch

The utility options are defined as a const array, but activateUtilityWave() uses a switch statement that must match each id. Adding a new utility requires updates in two places.

💡 Combine them: define a utilityWaves map where each entry is {id, label, description, cost, handler} and iterate through it. Call handler() instead of switch(id).

FEATURE waves and collision detection

Waves always span the entire screen width. Players in safe zones are invulnerable, but there's no mechanic to partially block or deflect waves.

💡 Add wave widths or x-bounds so waves are narrower and only cover part of the screen—players could outmaneuver them. This would increase skill expression.

BUG updatePassiveIncome()

Passive income uses deltaTime, which can vary wildly between frames. If the frame rate drops (e.g., to 30 FPS), deltaTime doubles, causing income to spike.

💡 Cap deltaTime: const dt = min(deltaTime / 1000, 1/30) to ensure stable income even with frame drops.

STYLE getTierForY() threshold values

The tier thresholds (0.18, 0.32, 0.55, etc.) are hardcoded magic numbers with no comments explaining the intent.

💡 Define them as a named array: const tierThresholds = [0.18, 0.32, 0.55, 0.75, 0.9] and comment why each exists (e.g., 'aim for ~30% common, ~20% rare, etc.').

🔄 Code Flow

Code flow showing setup, draw, updateplayer, updatewaves, checkplayerwavecollision, handlebrainrotcollection, handlecarrydeposit, activateutilitywave, drawplayer, generatebrainrot, gettierfory, keyPressed, mousepressed

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> update-phase[update-phase] draw --> drawing-phase[drawing-phase] update-phase --> wave-spawn-check[wave-spawn-check] wave-spawn-check -->|if no waves and time passed| updatewaves[updatewaves] updatewaves --> speed-multiplier-calc[speed-multiplier-calc] speed-multiplier-calc --> wave-update-loop[wave-update-loop] wave-update-loop --> safe-area-check[safe-area-check] safe-area-check -->|if not in safe zone| collision-loop[collision-loop] collision-loop --> checkplayerwavecollision[checkplayerwavecollision] update-phase --> updateplayer[updateplayer] updateplayer --> cursor-follow-mode[cursor-follow-mode] updateplayer --> wasd-mode[wasd-mode] wasd-mode --> movement-keys[movement-keys] updateplayer --> carry-check[carry-check] carry-check -->|if not carrying| handlebrainrotcollection[handlebrainrotcollection] handlebrainrotcollection --> collection-loop[collection-loop] handlebrainrotcollection --> handlecarrydeposit[handlecarrydeposit] handlecarrydeposit --> deposit-zone-check[deposit-zone-check] deposit-zone-check -->|if in base zone| inventory-full-check[inventory-full-check] inventory-full-check -->|if full| drop-off[drop-off] drop-off --> handlecarrydeposit update-phase --> activateutilitywave[activateutilitywave] activateutilitywave --> chaos-case[chaos-case] activateutilitywave --> pause-case[pause-case] activateutilitywave --> infinity-case[infinity-case] drawing-phase --> player-body[player-body] drawing-phase --> carried-item[carried-item] drawing-phase --> ragdoll-aura[ragdoll-aura] drawing-phase --> drawplayer[drawplayer] drawplayer -->|draws player| player-body drawplayer -->|draws carried brainrot| carried-item drawplayer -->|draws ragdoll aura| ragdoll-aura draw -->|loop back| draw click setup href "#fn-setup" click draw href "#fn-draw" click updateplayer href "#fn-updateplayer" click updatewaves href "#fn-updatewaves" click checkplayerwavecollision href "#fn-checkplayerwavecollision" click handlebrainrotcollection href "#fn-handlebrainrotcollection" click handlecarrydeposit href "#fn-handlecarrydeposit" click activateutilitywave href "#fn-activateutilitywave" click drawplayer href "#fn-drawplayer" click wave-spawn-check href "#sub-wave-spawn-check" click speed-multiplier-calc href "#sub-speed-multiplier-calc" click wave-update-loop href "#sub-wave-update-loop" click safe-area-check href "#sub-safe-area-check" click collision-loop href "#sub-collision-loop" click carry-check href "#sub-carry-check" click collection-loop href "#sub-collection-loop" click deposit-zone-check href "#sub-deposit-zone-check" click inventory-full-check href "#sub-inventory-full-check" click chaos-case href "#sub-chaos-case" click pause-case href "#sub-pause-case" click infinity-case href "#sub-infinity-case" click player-body href "#sub-player-body" click carried-item href "#sub-carried-item" click ragdoll-aura href "#sub-ragdoll-aura"

❓ Frequently Asked Questions

What visual experience does the escape tsunamis for brainrots v2 sketch offer?

This sketch creates a dynamic visual landscape filled with colorful tsunami waves and playful characters, simulating a frantic race to escape incoming dangers.

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

Users can control the player's movement using the WASD keys and manage inventory items while navigating through waves and collecting brainrots.

What creative coding concepts are showcased in the escape tsunamis for brainrots v2 sketch?

The sketch demonstrates concepts like object-oriented programming for player and wave behaviors, as well as real-time game mechanics with speed variations and interactive elements.

Preview

escape tsunamis for brainrots v2 (Remix) - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of escape tsunamis for brainrots v2 (Remix) - Code flow showing setup, draw, updateplayer, updatewaves, checkplayerwavecollision, handlebrainrotcollection, handlecarrydeposit, activateutilitywave, drawplayer, generatebrainrot, gettierfory, keyPressed, mousepressed
Code Flow Diagram