pull a brainrot

A colorful incremental clicker game where you collect bouncing 'brainrot' creatures of different rarities that appear in vertical zones. Drag them to your inventory, upgrade and sell them to earn passive income, and unlock rarer creatures by accumulating strength.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make creatures spawn twice as fast — Halving the spawnInterval values makes creatures appear more frequently in each zone—you'll collect them and build strength faster.
  2. Start with 10,000 coins instead of 100 — More starting money lets you buy early upgrades immediately without grinding—useful for testing the upgrade system.
  3. Make the player character huge — Increasing the diameter makes the player much more visible and easier to control with a mouse.
  4. Increase passive income by 50% — Multiplying the income by 1.5 makes creatures earn faster, speeding up progression and unlocking rarer creatures sooner.
  5. Add 50 more slots to inventory — Expanding the inventory from 8 to 58 slots lets you store far more creatures and experiment with larger collections.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates an interactive incremental game featuring bouncing creatures called 'brainrots' that spawn in nine difficulty zones based on your strength level. The game uses p5.js vectors for smooth physics-based movement, the draw loop to animate creatures and update income every frame, and real-time collision detection to let you drag creatures into your inventory. What makes it visually engaging is the vibrant color-coded zones and the smooth following behavior as you pull brainrots toward your base.

The code is organized into a Player class that handles keyboard movement, a Brainrot class that manages creature spawning and animation, a zones system that gates creature spawning behind strength requirements, and an inventory system with individual upgrade mechanics. By studying it you will learn how to build a complete game loop with passive income calculations, how to structure a catalog system for content, how to use object destructuring and array methods for inventory management, and how to implement a progression system where unlocking stronger creatures requires accumulating stats.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-screen canvas, initializes the player at the left, and calls createZones() to divide the right side into nine colored rarity zones, each with spawn timers and strength requirements.
  2. Every frame, the draw loop calls applyPassiveIncome() to add coins based on inventory creatures, clears the background, updates the player position from WASD/arrow keys, animates all bouncing brainrots using vectors, and filters out delivered ones.
  3. brainrots spawn randomly in their zone when the spawn timer reaches the rarity's interval (Common every 4 seconds, Legendary every 9 seconds, OG every 10 minutes) and only if active count stays below that rarity's maximum.
  4. Pressing E near a brainrot attaches it to the player and draws a line connecting them; the brainrot follows by using vector subtraction to calculate direction and lerping toward the player at reduced speed.
  5. Dragging a brainrot past the left edge (x < BASE_WIDTH) auto-stores it in the first empty inventory slot and displays its income per second; this passive income is calculated and applied every frame.
  6. Clicking an inventory slot opens a context menu where you can upgrade the creature's level (increasing its income multiplicatively) or sell it for coins to unlock stronger rarities.

🎓 Concepts You'll Learn

Game loop and animationVector math and physicsObject-oriented programming (classes)Inventory and state managementIncremental progression systemsCollision detection and mouse input

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch loads. Use it to initialize the canvas, create objects, and set up the world state.

function setup() {
  createCanvas(windowWidth, windowHeight);
  textFont('monospace');
  player = new Player(120, height / 2);
  createZones();
  lastOGSpawn = millis();
}
Line-by-line explanation (5 lines)
createCanvas(windowWidth, windowHeight);
Creates a full-screen canvas that fills the entire browser window—windowWidth and windowHeight are p5.js variables that auto-update on window resize.
textFont('monospace');
Sets all future text to monospace font, giving the game a retro coding aesthetic that matches the HUD display.
player = new Player(120, height / 2);
Creates the player character object positioned at x=120 (in the left zone) and vertically centered; stores it in the global player variable so other functions can access it.
createZones();
Calls the function that divides the canvas into nine colored rarity zones, setting up spawn points and requirements for each creature type.
lastOGSpawn = millis();
Records the current time in milliseconds so the code can track when OG creatures were last spawned and enforce their rare 10-minute spawn interval.

draw()

draw() is p5.js's main animation loop, called 60 times per second. Order matters: clear background first, update game state, then draw everything. This sketch demonstrates a complete game loop with physics, spawning, input, and UI rendering.

🔬 The first line animates all brainrots, and the second removes delivered ones. What happens if you comment out the filter line? What visual artifact appears?

  brainrots.forEach((b) => b.update());
  brainrots = brainrots.filter((b) => !b.delivered);
function draw() {
  applyPassiveIncome();
  background('#181A27');
  drawZones();
  player.update();
  brainrots.forEach((b) => b.update());
  brainrots = brainrots.filter((b) => !b.delivered);
  handleDelivery();
  maybeSpawnBrainrots();
  drawUI();
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

function-call Apply passive income from inventory applyPassiveIncome();

Adds coins each frame based on how many brainrots are stored in inventory and their income levels

for-loop Update all brainrot positions brainrots.forEach((b) => b.update());

Calls the update method on every brainrot creature, moving them and redrawing them at their new position

filter Remove delivered brainrots brainrots = brainrots.filter((b) => !b.delivered);

Removes creatures from the active list once they've been stored in inventory so they stop animating

function-call Check if attached brainrot reached inventory handleDelivery();

Checks if the currently attached brainrot has reached the left edge and stores it in an inventory slot

applyPassiveIncome();
Calculates and adds passive income earned from inventory creatures every frame—this is called 60 times per second so income flows smoothly rather than in chunks.
background('#181A27');
Clears the canvas by drawing a dark navy-blue rectangle over everything; without this, creatures would leave trails instead of disappearing.
drawZones();
Renders the nine colored rarity zones as tall rectangles from left to right, and draws text labels showing each zone's name and strength requirement.
player.update();
Updates the player position based on which keys are pressed (WASD or arrow keys) and redraws the player as a yellow circle.
brainrots.forEach((b) => b.update());
Loops through every active brainrot creature, calls its update method to move it and handle following behavior if attached, and draws it on canvas.
brainrots = brainrots.filter((b) => !b.delivered);
Creates a new array containing only brainrots that are NOT delivered; creatures that reached inventory are removed from the active animation list.
handleDelivery();
Checks if the currently attached brainrot is being dragged past the left edge (into the inventory base), and if so, stores it in the inventory array.
maybeSpawnBrainrots();
Checks each zone's spawn timer and randomly spawns new creatures if enough time has passed, the player's strength is high enough, and there's room in that zone.
drawUI();
Renders all on-screen UI: the HUD panel with coins/strength/income stats, the inventory panel with slots, slot context menus, and the status message.

createZones()

This function divides the canvas into equally-sized zones and calculates the safe spawn area within each. The spread operator (...cfg) merges configuration data with position data, a common ES6 pattern for building complex objects.

function createZones() {
  const zoneWidth = (width - BASE_WIDTH) / rarityConfigs.length;
  const padding = 16;

  zones = rarityConfigs.map((cfg, idx) => {
    const x = BASE_WIDTH + idx * zoneWidth;
    const innerX = x + padding;
    const innerW = max(40, zoneWidth - padding * 2);

    return {
      ...cfg,
      x,
      w: zoneWidth,
      spawnMinX: innerX,
      spawnMaxX: innerX + innerW,
      spawnTimer: 0
    };
  });
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Calculate width per zone const zoneWidth = (width - BASE_WIDTH) / rarityConfigs.length;

Divides the remaining canvas width (after inventory) evenly among 9 rarity zones

for-loop Map rarity configs to zone objects zones = rarityConfigs.map((cfg, idx) => {

Transforms the rarity config array into zone objects with position, spawn area, and timer properties

const zoneWidth = (width - BASE_WIDTH) / rarityConfigs.length;
Calculates how wide each zone should be: total canvas width minus the left inventory panel, divided by 9 (the number of rarity tiers).
const padding = 16;
Sets the gap between the zone edge and the actual spawn area—creatures spawn in the inner rectangle to avoid spawning outside visible bounds.
zones = rarityConfigs.map((cfg, idx) => {
Uses array.map() to transform each rarity config into a full zone object; idx is the index (0-8) used to position zones left-to-right.
const x = BASE_WIDTH + idx * zoneWidth;
Calculates each zone's left edge: starts after the inventory panel (BASE_WIDTH) and adds idx * zoneWidth to space them horizontally.
const innerX = x + padding; const innerW = max(40, zoneWidth - padding * 2);
Creates the inner spawn rectangle by moving inward by 'padding' pixels and reducing width; max(40, ...) ensures zones always have at least 40 pixels width.
return { ...cfg, x, w: zoneWidth, spawnMinX: innerX, spawnMaxX: innerX + innerW, spawnTimer: 0 };
Spreads all config properties (color, label, spawn interval, etc.) into the zone object, then adds position/size fields and initializes the spawn timer to 0.

Player()

The Player class demonstrates ES6 class syntax with a constructor and methods. Using p5.Vector for position enables vector math (add, setMag, dist) that makes physics intuitive. The speed property is upgraded globally but affects this instance, showing how state can be modified after construction.

🔬 This code normalizes the direction before adding it. What happens if you remove the setMag() line and just do this.pos.add(dir.mult(this.speed))? Hint: think about diagonal movement.

    if (dir.mag() > 0) {
      dir.setMag(this.speed);
      this.pos.add(dir);
    }
class Player {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.speed = 3;
  }
  update() {
    const dir = createVector(
      keyIsDown(68) || keyIsDown(RIGHT_ARROW) ? 1 : keyIsDown(65) || keyIsDown(LEFT_ARROW) ? -1 : 0,
      keyIsDown(83) || keyIsDown(DOWN_ARROW) ? 1 : keyIsDown(87) || keyIsDown(UP_ARROW) ? -1 : 0
    );
    if (dir.mag() > 0) {
      dir.setMag(this.speed);
      this.pos.add(dir);
    }
    this.pos.x = constrain(this.pos.x, 0, width);
    this.pos.y = constrain(this.pos.y, 0, height);
    push();
    fill('#ffe66d');
    stroke('#000');
    strokeWeight(2);
    circle(this.pos.x, this.pos.y, 26);
    pop();
  }
  bumpSpeed(amount) {
    this.speed += amount;
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

conditional Read keyboard input into direction vector const dir = createVector( keyIsDown(68) || keyIsDown(RIGHT_ARROW) ? 1 : keyIsDown(65) || keyIsDown(LEFT_ARROW) ? -1 : 0, keyIsDown(83) || keyIsDown(DOWN_ARROW) ? 1 : keyIsDown(87) || keyIsDown(UP_ARROW) ? -1 : 0 );

Checks which keys are pressed and builds a direction vector; D/Right Arrow = +x, A/Left = -x, S/Down = +y, W/Up = -y

conditional Apply movement if a key is pressed if (dir.mag() > 0) { dir.setMag(this.speed); this.pos.add(dir); }

Moves the player only if a direction was input; setMag ensures consistent speed regardless of diagonal movement

conditional Keep player on canvas this.pos.x = constrain(this.pos.x, 0, width); this.pos.y = constrain(this.pos.y, 0, height);

Prevents player from moving past the canvas edges using constrain (clamps value between min and max)

constructor(x, y) {
The constructor initializes a new Player object with a starting position and default speed; called once per game by new Player(120, height/2).
this.pos = createVector(x, y);
Stores the player's position as a p5.Vector, which has x/y properties and math methods like add(), dist(), etc.
this.speed = 3;
Sets the player's movement speed in pixels per frame; upgraded later when player buys speed upgrades with coins.
const dir = createVector(...);
Reads WASD and arrow keys using keyIsDown() and builds a 2D direction vector where each component is -1, 0, or 1 based on input.
keyIsDown(68) || keyIsDown(RIGHT_ARROW) ? 1 : keyIsDown(65) || keyIsDown(LEFT_ARROW) ? -1 : 0,
Checks keys D (code 68) or Right Arrow for +x direction, A (code 65) or Left Arrow for -x, else 0 (no horizontal input); the ? : is a ternary operator.
if (dir.mag() > 0) {
Only move if a key was pressed; mag() returns the vector's magnitude (length), which is > 0 only if direction isn't (0, 0).
dir.setMag(this.speed);
Normalizes the direction vector to a fixed length equal to this.speed; this ensures diagonal movement isn't faster than single-direction movement.
this.pos.add(dir);
Adds the direction vector to the position, moving the player by 'speed' pixels in the input direction.
this.pos.x = constrain(this.pos.x, 0, width);
Clamps the x position between 0 and canvas width, preventing the player from leaving the left or right edge.
this.pos.y = constrain(this.pos.y, 0, height);
Clamps the y position between 0 and canvas height, preventing the player from leaving the top or bottom edge.
circle(this.pos.x, this.pos.y, 26);
Draws a circle at the player's current position with diameter 26 pixels; fill() and stroke() set its yellow color and black outline.
bumpSpeed(amount) {
A helper method that increases player.speed by 'amount' when speed upgrades are purchased.

Brainrot()

The Brainrot class demonstrates how to package game entity data (position, stats, rarity) and behavior (following, drawing) together. The following logic uses vector math to create smooth pursuit: subtracting positions gives direction, setMag limits speed, and a fractional multiplier applies damping for realistic movement.

🔬 This code makes the creature follow the player with a damping factor of 0.35. What happens if you change 0.35 to 1.0? What if you change it to 0.1? Why does the damping matter?

    if (this.attached) {
      const desired = p5.Vector.sub(player.pos, this.pos);
      if (desired.mag() > 1) {
        desired.setMag(min(desired.mag(), 6));
        this.pos.add(desired.mult(0.35));
      }
    }
class Brainrot {
  constructor(zone, template) {
    this.zoneKey = zone.key;
    this.rarityLabel = zone.label;
    this.color = zone.color;
    this.requiredStrength = zone.requiredStrength;
    this.pos = createVector(
      random(zone.spawnMinX, zone.spawnMaxX),
      random(70, height - 70)
    );
    this.size = 34;
    this.name = template.name;
    this.price = template.price;
    this.income = template.income;
    this.level = floor(random(1, 101)); // NEW: random upgrade level
    this.attached = false;
    this.delivered = false;
  }

  update() {
    if (this.attached) {
      const desired = p5.Vector.sub(player.pos, this.pos);
      if (desired.mag() > 1) {
        desired.setMag(min(desired.mag(), 6));
        this.pos.add(desired.mult(0.35));
      }
    }
    this.draw();
  }

  draw() {
    push();
    noStroke();
    fill(this.color);
    circle(this.pos.x, this.pos.y, this.size);
    fill('#fff');
    textAlign(CENTER, CENTER);
    textSize(10);
    text(this.rarityLabel[0], this.pos.x, this.pos.y - 10);
    textSize(9);
    text(`${this.requiredStrength}`, this.pos.x, this.pos.y + 4);
    pop();
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Random spawn position in zone this.pos = createVector( random(zone.spawnMinX, zone.spawnMaxX), random(70, height - 70) );

Places the creature at a random x within the zone's safe spawn area and a random y with padding from top/bottom

conditional Follow player if attached if (this.attached) { const desired = p5.Vector.sub(player.pos, this.pos); if (desired.mag() > 1) { desired.setMag(min(desired.mag(), 6)); this.pos.add(desired.mult(0.35)); } }

When attached, calculates direction to player and smoothly moves toward them at a damped speed

this.zoneKey = zone.key;
Stores the zone identifier (e.g., 'common', 'rare', 'legendary') so the creature remembers its rarity tier.
this.rarityLabel = zone.label;
Stores the human-readable rarity name (e.g., 'Common', 'Rare') for display on the creature and in inventory.
this.color = zone.color;
Stores the zone's hex color (e.g., '#3d1f5c' for common) so the creature draws in its rarity's distinct color.
this.requiredStrength = zone.requiredStrength;
Stores the strength requirement; the player can only attach this creature if their strength >= requiredStrength.
this.pos = createVector( random(zone.spawnMinX, zone.spawnMaxX), random(70, height - 70) );
Creates the creature at a random position within the zone's safe spawn rectangle (avoiding edges).
this.level = floor(random(1, 101));
Spawns each creature with a random level from 1 to 100; higher level creatures earn more income when stored in inventory.
const desired = p5.Vector.sub(player.pos, this.pos);
Calculates the vector from creature to player by subtracting positions; this points in the direction the creature should move.
if (desired.mag() > 1) {
Only updates position if the creature is more than 1 pixel away from player, avoiding jittery movement when very close.
desired.setMag(min(desired.mag(), 6));
Limits the desired vector's length to a maximum of 6 pixels; this caps speed so creatures don't snap instantly to player.
this.pos.add(desired.mult(0.35));
Moves the creature 35% of the way toward the player each frame; 0.35 is a damping factor that creates smooth following, not snappy chasing.
text(this.rarityLabel[0], this.pos.x, this.pos.y - 10);
Draws the first letter of the rarity label (C, U, R, E, L, M, G, S, O) above the creature as a visual indicator.
text(`${this.requiredStrength}`, this.pos.x, this.pos.y + 4);
Draws the creature's strength requirement below it, so player knows immediately what's needed to catch it.

parseAmount()

parseAmount() converts strings like '1.5k' and '250M' into numeric values for cost and income calculations. This pattern is common in web games: store human-readable prices in config, parse them at runtime. The function uses regex matching, type checking, and a multiplier lookup table.

function parseAmount(value) {
  if (typeof value === 'number') return value;
  const str = value.toString().trim().toLowerCase().replace(/[, ]/g, '');
  const match = str.match(/([\.d.]+)([kmb]?)/);
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Return number if already numeric if (typeof value === 'number') return value;

Short-circuits parsing if input is already a number

calculation Extract number and suffix from string const match = str.match(/([\.d.]+)([kmb]?)/);

Uses regex to split '1.5k' into number '1.5' and suffix 'k'

if (typeof value === 'number') return value;
If the input is already a number (not a string), return it unchanged—no parsing needed.
const str = value.toString().trim().toLowerCase().replace(/[, ]/g, '');
Converts value to string, removes leading/trailing whitespace, converts to lowercase (so '1K' and '1k' both work), and removes commas and spaces.
const match = str.match(/([\.d.]+)([kmb]?)/);
Uses a regex to match the string: ([\d.]+) captures digits and decimal points (the number part), ([kmb]?) optionally captures k/m/b (the multiplier suffix).
const multipliers = { k: 1e3, m: 1e6, b: 1e9 };
Defines a lookup table: 'k' = 1000, 'm' = 1000000, 'b' = 1000000000; other suffixes default to 1 (no multiplier).
return num * (multipliers[suffix] || 1);
Multiplies the parsed number by the suffix's multiplier, or by 1 if no suffix; '1.5k' becomes 1.5 * 1000 = 1500.

attemptAttach()

attemptAttach() demonstrates proximity detection (finding nearest object within range), state toggling (press E to attach, press E again to detach), and permission checking (verify strength requirement). These patterns appear in every interactive game.

🔬 This loop finds the closest unattached creature. What happens if you remove the 'd < 80' check? Would you be able to attach creatures from anywhere on the map? Try it and see the gameplay change.

  brainrots.forEach((b) => {
    if (!b.attached) {
      const d = p5.Vector.dist(player.pos, b.pos);
      if (d < 80 && d < closestDist) {
        closest = b;
        closestDist = d;
      }
    }
  });
function attemptAttach() {
  if (attachedBrainrot) {
    attachedBrainrot.attached = false;
    attachedBrainrot = null;
    info('Detached brainrot.');
    return;
  }
  let closest = null;
  let closestDist = Infinity;
  brainrots.forEach((b) => {
    if (!b.attached) {
      const d = p5.Vector.dist(player.pos, b.pos);
      if (d < 80 && d < closestDist) {
        closest = b;
        closestDist = d;
      }
    }
  });
  if (!closest) return info('No brainrot nearby.');
  if (strength < closest.requiredStrength) return info(`Need ${closest.requiredStrength} strength.`);
  closest.attached = true;
  attachedBrainrot = closest;
  info(`Attached ${closest.name} (Lv${closest.level} ${closest.rarityLabel}).`);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Detach if creature already attached if (attachedBrainrot) { attachedBrainrot.attached = false; attachedBrainrot = null; info('Detached brainrot.'); return; }

If player presses E while carrying a creature, releases it immediately

conditional Verify player has enough strength if (strength < closest.requiredStrength) return info(`Need ${closest.requiredStrength} strength.`);

Prevents attaching rarer creatures unless player's strength stat is high enough

if (attachedBrainrot) {
Checks if player is already carrying a creature; if so, toggle it off (detach) and exit early with return.
let closest = null; let closestDist = Infinity;
Initializes variables to track the nearest brainrot; closestDist starts at Infinity so any real distance is smaller.
const d = p5.Vector.dist(player.pos, b.pos);
Calculates the Euclidean distance between player and this brainrot using p5.Vector.dist().
if (d < 80 && d < closestDist) {
Only considers brainrots within 80 pixels AND closer than the previous closest; this finds the single closest nearby creature.
if (!closest) return info('No brainrot nearby.');
If no creature was found in range, display a message and exit; without this check, attaching null would crash.
if (strength < closest.requiredStrength) return info(`Need ${closest.requiredStrength} strength.`);
Checks if player's global strength stat is at least the creature's requirement; if not, shows message and exits without attaching.
closest.attached = true; attachedBrainrot = closest;
Attaches the creature by setting its attached flag to true and storing a reference in the global attachedBrainrot variable.
info(`Attached ${closest.name} (Lv${closest.level} ${closest.rarityLabel}).`);
Shows a status message naming the creature, its level, and rarity; info() displays this in the HUD for 150 frames.

handleDelivery()

handleDelivery() transfers an attached creature from the game world into inventory storage. It demonstrates: conditional boundary checking, array search with findIndex(), creating object literals for data, and state transitions (attached → delivered → stored). The income message gives immediate feedback so player knows their action was rewarded.

function handleDelivery() {
  if (attachedBrainrot && player.pos.x < BASE_WIDTH - 10) {
    const slotIndex = inventory.findIndex((slot) => slot === null);
    if (slotIndex === -1) {
      info('Inventory full! Click a slot to upgrade or sell.');
      return;
    }
    inventory[slotIndex] = {
      name: attachedBrainrot.name,
      rarity: attachedBrainrot.rarityLabel,
      color: attachedBrainrot.color,
      baseIncome: attachedBrainrot.income,
      basePrice: attachedBrainrot.price,
      level: attachedBrainrot.level
    };
    attachedBrainrot.delivered = true;
    attachedBrainrot.attached = false;
    info(`Stored Lv${attachedBrainrot.level} ${attachedBrainrot.name} · ${formatNumber(getSlotIncome(inventory[slotIndex]))}/s`);
    attachedBrainrot = null;
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Check if creature reached inventory zone if (attachedBrainrot && player.pos.x < BASE_WIDTH - 10) {

Triggers only when player drags an attached creature past the left boundary into the base zone

calculation Find first empty inventory slot const slotIndex = inventory.findIndex((slot) => slot === null);

Uses findIndex() to locate the first null (empty) slot, returning -1 if inventory is full

calculation Create stored creature object inventory[slotIndex] = { name: attachedBrainrot.name, rarity: attachedBrainrot.rarityLabel, color: attachedBrainrot.color, baseIncome: attachedBrainrot.income, basePrice: attachedBrainrot.price, level: attachedBrainrot.level };

Creates a new inventory item object copying creature data and initializes it at level from spawn

if (attachedBrainrot && player.pos.x < BASE_WIDTH - 10) {
Checks two conditions: a creature is currently attached AND the player has crossed into the inventory zone (left of BASE_WIDTH minus 10-pixel buffer).
const slotIndex = inventory.findIndex((slot) => slot === null);
Searches the inventory array for the first empty (null) slot; findIndex returns 0-7 for a valid slot or -1 if all slots are full.
if (slotIndex === -1) { info('Inventory full! Click a slot to upgrade or sell.'); return; }
If no empty slot exists (inventory is full), display a message and exit without storing the creature—it stays attached to player.
inventory[slotIndex] = { name: attachedBrainrot.name, ... };
Creates a new object and stores it in the empty slot; this object contains all data needed for income calculation, display, and upgrades.
attachedBrainrot.delivered = true;
Marks the creature as delivered so the draw loop's filter will remove it from active animation on the next frame.
attachedBrainrot = null;
Clears the global attachedBrainrot reference so player is no longer carrying anything; E can now attach a new creature.
info(`Stored Lv${attachedBrainrot.level} ${attachedBrainrot.name} · ${formatNumber(getSlotIncome(inventory[slotIndex]))}/s`);
Shows a confirmation message displaying the stored creature's level, name, and passive income per second so player sees immediate reward.

applyPassiveIncome()

applyPassiveIncome() runs every frame (60 times per second) to add income smoothly rather than in discrete chunks. Using deltaTime (milliseconds elapsed since last frame) ensures the same total income per real-time second regardless of frame rate. This is a best practice in game dev: frame-time-agnostic updates. The reduce() method is perfect for summing an array into a single value.

function applyPassiveIncome() {
  const incomePerSecond = inventory.reduce((sum, slot) => (slot ? sum + getSlotIncome(slot) : sum), 0);
  money += (incomePerSecond * deltaTime) / 1000;
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

calculation Sum income from all stored creatures const incomePerSecond = inventory.reduce((sum, slot) => (slot ? sum + getSlotIncome(slot) : sum), 0);

Uses reduce to fold the inventory array into a single total income value by adding each creature's per-second earnings

calculation Award fractional income based on frame time money += (incomePerSecond * deltaTime) / 1000;

Multiplies per-second rate by elapsed frame time (in milliseconds, divided by 1000 to convert to seconds) so income accrues smoothly per frame

const incomePerSecond = inventory.reduce((sum, slot) => (slot ? sum + getSlotIncome(slot) : sum), 0);
Uses reduce() to sum up income: for each inventory slot, if it's not null, add getSlotIncome(slot) to the running sum; start with 0. This gives total passive income per second.
money += (incomePerSecond * deltaTime) / 1000;
Adds a fraction of the per-second income to the money counter; deltaTime (provided by p5.js) is milliseconds since last frame, divided by 1000 to get seconds. Ensures smooth income that doesn't stutter on frame rate changes.

maybeSpawnBrainrots()

maybeSpawnBrainrots() implements spawn gating: each zone has its own timer that accumulates, and creatures only appear when timer exceeds the interval AND active count is below the cap. OG creatures have a second global timer to enforce their ultra-rare spawn rate. This pattern is universal in games: spawn loops, cooldown tracking, and population caps.

🔬 This logic spawns OG creatures only if BOTH timers have elapsed. What happens if you remove the 'millis() - lastOGSpawn >= OG_INTERVAL' check? Would OG creatures spawn more frequently? Try it to see the difference.

    const canSpawn =
      zone.key === 'og'
        ? millis() - lastOGSpawn >= OG_INTERVAL && zone.spawnTimer >= interval
        : zone.spawnTimer >= interval;
function maybeSpawnBrainrots() {
  zones.forEach((zone) => {
    zone.spawnTimer += deltaTime;
    const active = getActiveCount(zone.key);
    const interval = zone.key === 'og' ? OG_INTERVAL : zone.spawnInterval;

    const canSpawn =
      zone.key === 'og'
        ? millis() - lastOGSpawn >= OG_INTERVAL && zone.spawnTimer >= interval
        : zone.spawnTimer >= interval;

    if (canSpawn && active < zone.maxActive && brainrotCatalog[zone.key]?.length) {
      spawnBrainrot(zone);
      zone.spawnTimer = 0;
      if (zone.key === 'og') lastOGSpawn = millis();
    }
  });
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Accumulate spawn timer each frame zone.spawnTimer += deltaTime;

Increases the zone's spawn counter by elapsed frame time in milliseconds

conditional Check all spawn prerequisites const canSpawn = zone.key === 'og' ? millis() - lastOGSpawn >= OG_INTERVAL && zone.spawnTimer >= interval : zone.spawnTimer >= interval;

For OG creatures, both the global timer AND zone timer must pass; for others, only the zone timer. Prevents OG from spawning faster than intended.

conditional Final spawn gates if (canSpawn && active < zone.maxActive && brainrotCatalog[zone.key]?.length) {

Ensures timer passed, active count is below max, and creature catalog exists before spawning

zone.spawnTimer += deltaTime;
Increments the zone's internal spawn timer by deltaTime (milliseconds since last frame); this accumulates until it reaches the spawn interval.
const active = getActiveCount(zone.key);
Calls getActiveCount() to count how many undelivered creatures of this rarity are currently active in the game world.
const interval = zone.key === 'og' ? OG_INTERVAL : zone.spawnInterval;
Selects the spawn interval: for 'og' rarity use the global OG_INTERVAL (10 minutes), for all others use the zone's spawnInterval (4-20 seconds).
const canSpawn = zone.key === 'og' ? millis() - lastOGSpawn >= OG_INTERVAL && zone.spawnTimer >= interval : zone.spawnTimer >= interval;
Determines if spawn conditions are met: OG creatures require BOTH global time elapsed AND zone timer elapsed; all other rarities only need zone timer elapsed. This double-gate prevents OG spawning too fast even if triggered multiple times.
if (canSpawn && active < zone.maxActive && brainrotCatalog[zone.key]?.length) {
Final gates: timer passed AND active count is below the zone's max AND the catalog for this rarity has creatures. The ?. operator checks if the array exists before checking length.
spawnBrainrot(zone); zone.spawnTimer = 0; if (zone.key === 'og') lastOGSpawn = millis();
Spawns a creature, resets the zone timer to 0, and updates lastOGSpawn time if this was an OG zone (for the global 10-minute gate).

drawUI()

drawUI() is a coordinator function that calls sub-drawing functions in order. The white tether line provides crucial visual feedback—players immediately understand their creature is attached. Separating UI rendering into multiple functions (drawHud, drawInventoryPanel) keeps code organized and each function focused on one task.

function drawUI() {
  drawHud();
  drawInventoryPanel();

  if (attachedBrainrot) {
    stroke('#fff');
    line(player.pos.x, player.pos.y, attachedBrainrot.pos.x, attachedBrainrot.pos.y);
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

function-call Draw stats panel drawHud();

Renders the bottom-left panel showing coins, strength, speed, and passive income

function-call Draw inventory panel and slots drawInventoryPanel();

Renders the top-right inventory panel with 8 creature slots and interaction menus

conditional Draw line when carrying creature if (attachedBrainrot) { stroke('#fff'); line(player.pos.x, player.pos.y, attachedBrainrot.pos.x, attachedBrainrot.pos.y); }

Draws a white line from player to attached creature for visual feedback that they're linked

drawHud();
Calls the function that renders the HUD panel with stats (coins, strength, speed level, passive income) and help text.
drawInventoryPanel();
Calls the function that renders the inventory panel (8 slots) and any open context menu for upgrading/selling.
if (attachedBrainrot) {
Only draws the line if player is currently carrying a creature (attachedBrainrot is not null).
stroke('#fff'); line(player.pos.x, player.pos.y, attachedBrainrot.pos.x, attachedBrainrot.pos.y);
Sets stroke color to white and draws a line from the player's center to the attached creature's center, creating a visual tether effect.

drawHud()

drawHud() demonstrates text layout with precise positioning and alpha transparency for overlays. The infoTimer countdown pattern is a classic game dev technique: show messages temporarily and auto-dismiss them. Using formatNumber() keeps large numbers readable; 1234567 displays as '1.23M' instead of cluttering the screen.

function drawHud() {
  const panelHeight = 175;
  push();
  fill(0, 180);
  rect(0, height - panelHeight, width, panelHeight);
  fill('#fff');
  textAlign(LEFT, TOP);
  textSize(18);
  text(`Coins: ${formatNumber(money)}`, 20, height - panelHeight + 10);
  text(`Strength: ${formatNumber(strength)}`, 20, height - panelHeight + 40);
  text(`Speed Lv ${speedLevel} (${player.speed.toFixed(2)})`, 20, height - panelHeight + 70);
  const passive = inventory.reduce((sum, slot) => (slot ? sum + getSlotIncome(slot) : sum), 0);
  text(`Passive Income: ${formatNumber(passive)}/s`, 20, height - panelHeight + 100);
  textSize(14);
  text(
    'Upgrades → STR: 1(100) · 10(500) · 100(1k) · 1k(10k) · 10k(100k) · 100k(1M) | Speed=I | Attach=E',
    20,
    height - panelHeight + 125
  );
  text('Click an inventory slot to upgrade (max Lv100) or sell for more.', 20, height - panelHeight + 145);

  if (infoTimer > 0) {
    infoTimer--;
    textAlign(RIGHT, TOP);
    text(infoMessage, width - 20, height - panelHeight + 10);
  }
  pop();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Draw semi-transparent background fill(0, 180); rect(0, height - panelHeight, width, panelHeight);

Creates a dark semi-transparent rectangle covering the bottom of the screen as the HUD background

calculation Render four stat lines text(`Coins: ${formatNumber(money)}`, 20, height - panelHeight + 10); text(`Strength: ${formatNumber(strength)}`, 20, height - panelHeight + 40); text(`Speed Lv ${speedLevel} (${player.speed.toFixed(2)})`, 20, height - panelHeight + 70); const passive = inventory.reduce((sum, slot) => (slot ? sum + getSlotIncome(slot) : sum), 0); text(`Passive Income: ${formatNumber(passive)}/s`, 20, height - panelHeight + 100);

Displays player's current coins, strength, speed level with actual speed value, and passive income per second

conditional Display temporary status message if (infoTimer > 0) { infoTimer--; textAlign(RIGHT, TOP); text(infoMessage, width - 20, height - panelHeight + 10); }

Shows temporary action feedback (e.g., 'Attached creature' or 'Inventory full') for 150 frames then fades

const panelHeight = 175;
Defines the height of the HUD panel in pixels; used to position the background rectangle and text lines.
fill(0, 180); rect(0, height - panelHeight, width, panelHeight);
Draws a dark semi-transparent rectangle (fill(0, 180) = black with 180/255 alpha) covering the bottom 175 pixels of the canvas.
text(`Coins: ${formatNumber(money)}`, 20, height - panelHeight + 10);
Displays the current coin count using template literals and formatNumber() to abbreviate large numbers (e.g., 1234567 → '1.23M').
const passive = inventory.reduce((sum, slot) => (slot ? sum + getSlotIncome(slot) : sum), 0);
Calculates total passive income per second by summing getSlotIncome() for every creature in inventory (same logic as applyPassiveIncome).
if (infoTimer > 0) { infoTimer--; text(infoMessage, width - 20, height - panelHeight + 10); }
If a temporary message is active (infoTimer > 0), displays it at top-right and decrements the timer; once timer reaches 0, message stops showing.

drawInventoryPanel()

drawInventoryPanel() demonstrates list UI rendering: a loop draws 8 identical-sized items in a column, calculating positions from the loop index. It also builds an interaction geometry array (inventorySlotsBounds) in parallel, which is used by mousePressed() to convert screen coordinates into logical slot indices. This pattern appears everywhere: menu systems, lists, grids.

function drawInventoryPanel() {
  const panelWidth = 300;
  const panelHeight = 305;
  const x = width - panelWidth - 20;
  const y = 20;
  push();
  fill(0, 180);
  rect(x, y, panelWidth, panelHeight, 8);
  fill('#fff');
  textAlign(CENTER, TOP);
  textSize(16);
  text('Inventory (8 slots)', x + panelWidth / 2, y + 10);
  pop();

  inventorySlotsBounds = [];
  const slotHeight = 30;
  for (let i = 0; i < INVENTORY_SLOTS; i++) {
    const slotX = x + 15;
    const slotY = y + 45 + i * (slotHeight + 5);
    const slotW = panelWidth - 30;
    push();
    const slot = inventory[i];
    fill(slot ? slot.color || '#444' : 'rgba(255,255,255,0.2)');
    rect(slotX, slotY, slotW, slotHeight, 5);
    fill('#fff');
    textAlign(LEFT, CENTER);
    textSize(11);
    const label = slot
      ? `Lv${slot.level} ${slot.rarity} · ${slot.name} · ${formatNumber(getSlotIncome(slot))}/s`
      : `Slot ${i + 1} (empty)`;
    text(label, slotX + 8, slotY + slotHeight / 2);
    pop();
    inventorySlotsBounds.push({ x: slotX, y: slotY, w: slotW, h: slotHeight, index: i });
  }

  drawSlotMenu();
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Draw inventory panel background and title fill(0, 180); rect(x, y, panelWidth, panelHeight, 8); fill('#fff'); textAlign(CENTER, TOP); textSize(16); text('Inventory (8 slots)', x + panelWidth / 2, y + 10);

Renders the semi-transparent panel box with rounded corners and centered title text

for-loop Draw all 8 inventory slots for (let i = 0; i < INVENTORY_SLOTS; i++) { const slotX = x + 15; const slotY = y + 45 + i * (slotHeight + 5); const slotW = panelWidth - 30; push(); const slot = inventory[i]; fill(slot ? slot.color || '#444' : 'rgba(255,255,255,0.2)'); rect(slotX, slotY, slotW, slotHeight, 5); fill('#fff'); textAlign(LEFT, CENTER); textSize(11); const label = slot ? `Lv${slot.level} ${slot.rarity} · ${slot.name} · ${formatNumber(getSlotIncome(slot))}/s` : `Slot ${i + 1} (empty)`; text(label, slotX + 8, slotY + slotHeight / 2); pop(); inventorySlotsBounds.push({ x: slotX, y: slotY, w: slotW, h: slotHeight, index: i }); }

Loops 8 times, drawing each inventory slot with its creature data (if occupied) or empty placeholder, and stores clickable bounds for input handling

const panelWidth = 300; const panelHeight = 305; const x = width - panelWidth - 20; const y = 20;
Defines the inventory panel size (300x305) and positions it at top-right with 20-pixel margins from the edge.
rect(x, y, panelWidth, panelHeight, 8);
Draws the panel background rectangle; the last argument (8) rounds the corners to 8-pixel radius for a modern look.
inventorySlotsBounds = [];
Clears and reinitializes the bounds array; will be filled with clickable rectangles for each slot so mousePressed() knows where to check for clicks.
const slotY = y + 45 + i * (slotHeight + 5);
Positions each slot: starts at y+45 (below title), then each slot is slotHeight (30 pixels) plus 5-pixel gap, spaced vertically.
fill(slot ? slot.color || '#444' : 'rgba(255,255,255,0.2)');
Sets slot color: if occupied, use the creature's rarity color; if empty, use a semi-transparent white for visual contrast.
const label = slot ? `Lv${slot.level} ${slot.rarity} · ${slot.name} · ${formatNumber(getSlotIncome(slot))}/s` : `Slot ${i + 1} (empty)`;
Builds the slot label: if creature, show level/rarity/name/income; if empty, show placeholder text with slot number.
inventorySlotsBounds.push({ x: slotX, y: slotY, w: slotW, h: slotHeight, index: i });
Stores the slot's position and size as an object; mousePressed() uses this array to detect which slot was clicked.

mousePressed()

mousePressed() implements hierarchical input handling: prioritize open menus, then fallback to selecting inventory slots. It demonstrates point-in-rectangle collision detection (used throughout games) and state toggling (open/close menu). The pattern is: check open UI first, then check world state, update state, and exit early to avoid double-processing.

function mousePressed() {
  if (slotMenu) {
    const bounds = inventorySlotsBounds[slotMenu.index];
    const slot = inventory[slotMenu.index];
    const layout = bounds && slot ? getSlotMenuLayout(bounds) : null;

    if (layout && pointInRect(mouseX, mouseY, { x: layout.x, y: layout.y, w: layout.w, h: layout.h })) {
      if (pointInRect(mouseX, mouseY, layout.upgradeBtn)) {
        attemptSlotUpgrade(slotMenu.index);
      } else if (pointInRect(mouseX, mouseY, layout.sellBtn)) {
        sellSlot(slotMenu.index);
      }
      return;
    }
  }

  slotMenu = null;

  for (const box of inventorySlotsBounds) {
    if (pointInRect(mouseX, mouseY, box)) {
      if (inventory[box.index]) slotMenu = { index: box.index };
      return;
    }
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Handle clicks on open menu if (slotMenu) { const bounds = inventorySlotsBounds[slotMenu.index]; const slot = inventory[slotMenu.index]; const layout = bounds && slot ? getSlotMenuLayout(bounds) : null; if (layout && pointInRect(mouseX, mouseY, { x: layout.x, y: layout.y, w: layout.w, h: layout.h })) { if (pointInRect(mouseX, mouseY, layout.upgradeBtn)) { attemptSlotUpgrade(slotMenu.index); } else if (pointInRect(mouseX, mouseY, layout.sellBtn)) { sellSlot(slotMenu.index); } return; } }

If a slot menu is open, check if click was inside it; if so, route to upgrade or sell and exit early

for-loop Find which inventory slot was clicked for (const box of inventorySlotsBounds) { if (pointInRect(mouseX, mouseY, box)) { if (inventory[box.index]) slotMenu = { index: box.index }; return; } }

Loops through all slot bounds and opens menu if click hits an occupied slot

if (slotMenu) {
Checks if a slot context menu is currently open; if so, prioritize handling clicks on that menu before checking other slots.
const layout = bounds && slot ? getSlotMenuLayout(bounds) : null;
Calculates the menu's layout (position and button bounds) only if both bounds and slot exist; null if either is missing (safety check).
if (layout && pointInRect(mouseX, mouseY, { x: layout.x, y: layout.y, w: layout.w, h: layout.h })) {
Checks if the click was inside the menu's bounding box; if outside, the menu closes (handled below) and slot selection proceeds.
if (pointInRect(mouseX, mouseY, layout.upgradeBtn)) { attemptSlotUpgrade(slotMenu.index); } else if (pointInRect(mouseX, mouseY, layout.sellBtn)) { sellSlot(slotMenu.index); }
Checks which button within the menu was clicked (upgrade or sell) and calls the appropriate function; return exits early so no other clicks are processed.
slotMenu = null;
Closes any open menu before checking for new slot clicks; clicking outside a menu closes it and allows opening another one.
for (const box of inventorySlotsBounds) { if (pointInRect(mouseX, mouseY, box)) {
Loops through all 8 inventory slot bounds and checks if the click was inside any of them using the pointInRect helper.
if (inventory[box.index]) slotMenu = { index: box.index };
Only opens a menu if the slot is occupied (inventory[box.index] is not null); clicking empty slots closes the current menu but opens nothing.

keyPressed()

keyPressed() is called by p5.js whenever a key is released. It uses simple if-else chains to map keys to game actions. The strength upgrade keys demonstrate gating: higher batch sizes (1000, 10000) are locked until the player reaches that strength level, creating progression gates. This pattern guides players toward intended upgrade paths.

function keyPressed() {
  if (key === 'e' || key === 'E') {
    attemptAttach();
  } else if (key === '1') {
    upgradeStrength(1);
  } else if (key === '2') {
    upgradeStrength(10);
  } else if (key === '3') {
    upgradeStrength(100);
  } else if (key === '4' && strength >= 1000) {
    upgradeStrength(1000);
  } else if (key === '5' && strength >= 10000) {
    upgradeStrength(10000);
  } else if (key === '6' && strength >= 100000) {
    upgradeStrength(100000);
  } else if (key === 'i' || key === 'I') {
    upgradeSpeed();
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Handle E key for attach/detach if (key === 'e' || key === 'E') { attemptAttach(); }

Pressing E toggles attachment of nearby creatures

conditional Handle number keys for strength upgrades } else if (key === '1') { upgradeStrength(1); } else if (key === '2') { upgradeStrength(10); } else if (key === '3') { upgradeStrength(100); } else if (key === '4' && strength >= 1000) { upgradeStrength(1000); } else if (key === '5' && strength >= 10000) { upgradeStrength(10000); } else if (key === '6' && strength >= 100000) { upgradeStrength(100000); }

Pressing 1-6 buys strength upgrades in batches; higher number keys check strength requirement to prevent early access

if (key === 'e' || key === 'E') {
Checks if the E key was pressed (case-insensitive); this is the main interaction button for attaching/detaching creatures.
} else if (key === '1') { upgradeStrength(1);
Pressing '1' buys a single strength upgrade; always available, costs 100 coins (fixed cost for batch size 1).
} else if (key === '4' && strength >= 1000) { upgradeStrength(1000);
Pressing '4' buys 1000 strength, but only if player's current strength is already >= 1000 (gates the feature by progression).
} else if (key === 'i' || key === 'I') { upgradeSpeed();
Pressing I buys a speed upgrade; each upgrade increases player.speed and costs more (exponential scaling via getSpeedUpgradeCost()).

upgradeStrength()

upgradeStrength() is a simple purchase function: check affordability, deduct cost, apply benefit, show feedback. The pattern is universal in games with currency. Using info() provides immediate feedback so the player knows their action succeeded.

function upgradeStrength(amount) {
  const cost = getStrengthUpgradeCost(amount);
  if (money < cost) return info(`Need ${formatNumber(cost)} coins.`);
  money -= cost;
  strength += amount;
  info(`Strength +${amount}! Now ${strength}.`);
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Get cost for this upgrade batch const cost = getStrengthUpgradeCost(amount);

Calculates the total cost to buy 'amount' strength points based on batch size and current strength

conditional Verify player has enough coins if (money < cost) return info(`Need ${formatNumber(cost)} coins.`);

Prevents purchase if insufficient coins and shows required amount

const cost = getStrengthUpgradeCost(amount);
Calls the helper function to calculate the cost; amount can be 1, 10, 100, 1000, 10000, or 100000 for predefined batches.
if (money < cost) return info(`Need ${formatNumber(cost)} coins.`);
Checks if player has enough coins; if not, shows a message and returns early without making the purchase.
money -= cost;
Deducts the cost from the player's money; now player has less coins.
strength += amount;
Adds the upgrade amount to the player's total strength; this increases the strength stat and may unlock rarer creature zones.
info(`Strength +${amount}! Now ${strength}.`);
Shows a congratulations message with the amount gained and new total strength; displays for 150 frames in the HUD.

getStrengthUpgradeCost()

getStrengthUpgradeCost() demonstrates cost scaling in incremental games. Fixed costs for standard batches are convenient and avoid rounding artifacts. The fallback formula shows exponential scaling: each strength level multiplies the cost by 1.12, so late-game upgrades become prohibitively expensive. This encourages diversifying upgrade strategies (buy speed, upgrade creatures, etc.) rather than hoarding one resource.

function getStrengthUpgradeCost(amount) {
  if (amount === 1) return 100;
  if (amount === 10) return 500;
  if (amount === 100) return 1000;
  if (amount === 1000) return 10000;
  if (amount === 10000) return 100000;
  if (amount === 100000) return 1000000;
  // fallback for any other batch size
  let total = 0;
  for (let i = 0; i < amount; i++) {
    const level = strength + i;
    total += Math.round(STRENGTH_BASE_COST * Math.pow(1.12, level));
  }
  return total;
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Return fixed costs for standard batch sizes if (amount === 1) return 100; if (amount === 10) return 500; if (amount === 100) return 1000; if (amount === 1000) return 10000; if (amount === 10000) return 100000; if (amount === 100000) return 1000000;

Hard-coded costs for the 6 standard upgrade buttons (1, 10, 100, 1000, 10000, 100000)

for-loop Fallback: calculate cost for arbitrary amounts let total = 0; for (let i = 0; i < amount; i++) { const level = strength + i; total += Math.round(STRENGTH_BASE_COST * Math.pow(1.12, level)); } return total;

For any other batch size, sums individual upgrade costs using exponential scaling (each level costs more)

if (amount === 1) return 100;
Strength upgrade batch of 1 costs a fixed 100 coins; hard-coded for simplicity and to avoid rounding noise.
if (amount === 10) return 500;
Batch of 10 costs 500 coins (not 10x the single cost, providing a small discount for bulk buying).
const level = strength + i;
In the fallback loop, calculates what the player's strength would be after i upgrades; used in the exponential formula.
total += Math.round(STRENGTH_BASE_COST * Math.pow(1.12, level));
Adds the cost for one upgrade at that level using exponential scaling: base cost (25) × 1.12^level. Higher levels cost exponentially more.

upgradeSpeed()

upgradeSpeed() is similar to upgradeStrength() but demonstrates coupling: it affects both a global stat (speedLevel) and a player property (player.speed). Calling player.bumpSpeed() instead of directly modifying player.speed encapsulates the change, making code easier to refactor later.

function upgradeSpeed() {
  const cost = getSpeedUpgradeCost();
  if (money < cost) return info(`Need ${formatNumber(cost)} coins.`);
  money -= cost;
  speedLevel++;
  player.bumpSpeed(SPEED_INCREMENT);
  info(`Speed upgraded! Level ${speedLevel}.`);
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Calculate cost for next speed level const cost = getSpeedUpgradeCost();

Gets the cost based on current speedLevel using exponential scaling

conditional Check funds, deduct cost, apply upgrade if (money < cost) return info(`Need ${formatNumber(cost)} coins.`); money -= cost; speedLevel++; player.bumpSpeed(SPEED_INCREMENT);

Standard purchase pattern: verify affordability, deduct coins, increment level, apply physical change to player

const cost = getSpeedUpgradeCost();
Calls getSpeedUpgradeCost() to compute the cost for the next speed upgrade based on speedLevel; costs increase exponentially.
if (money < cost) return info(`Need ${formatNumber(cost)} coins.`);
Checks if player has enough coins; if not, shows the required amount and exits early without purchasing.
speedLevel++;
Increments the global speedLevel counter; this is used for display and to calculate the next upgrade's cost.
player.bumpSpeed(SPEED_INCREMENT);
Calls the player's bumpSpeed() method, which adds SPEED_INCREMENT (0.35) to player.speed, making the character move faster.

attemptSlotUpgrade()

attemptSlotUpgrade() is a purchase function focused on inventory management. It demonstrates level capping (max 100), progressive cost scaling (cost × level), and immediate impact (level → income). The pattern is standard for item upgrades in games.

function attemptSlotUpgrade(index) {
  const slot = inventory[index];
  if (!slot) return;
  if (slot.level >= 100) return info('This brainrot is already Lv100.');
  const cost = getSlotUpgradeCost(slot);
  if (money < cost) return info(`Need ${formatNumber(cost)} coins to upgrade.`);
  money -= cost;
  slot.level++;
  info(`${slot.name} upgraded to Lv${slot.level}!`);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Verify slot is occupied if (!slot) return;

Prevents upgrade on empty slots

conditional Check max level (100) if (slot.level >= 100) return info('This brainrot is already Lv100.');

Prevents upgrading creatures past level 100

const slot = inventory[index];
Retrieves the creature object at the given inventory slot index.
if (!slot) return;
Safety check: if the slot is empty (null), exit early; prevents errors from attempting upgrades on nothing.
if (slot.level >= 100) return info('This brainrot is already Lv100.');
Checks if creature is already max level; if so, display message and prevent further upgrades (hard cap at level 100).
const cost = getSlotUpgradeCost(slot);
Calls getSlotUpgradeCost() which returns basePrice × level; cost scales with current level, making late-stage upgrades very expensive.
if (money < cost) return info(`Need ${formatNumber(cost)} coins to upgrade.`);
Checks affordability; if not enough coins, shows cost and exits without upgrading.
slot.level++;
Increments the creature's level by 1; this immediately increases its passive income (getSlotIncome = baseIncome × level) and future upgrade costs.

sellSlot()

sellSlot() removes an inventory item and awards coins. It demonstrates the inverse of delivery: clearing inventory space and providing a fallback income source. Selling valuable creatures is a strategic choice in incremental games: immediate cash vs. long-term passive income.

function sellSlot(index) {
  const slot = inventory[index];
  if (!slot) return;
  const payout = getSlotSellValue(slot);
  money += payout;
  inventory[index] = null;
  slotMenu = null;
  info(`Sold ${slot.name} for ${formatNumber(payout)} coins.`);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Delete creature from inventory inventory[index] = null;

Frees the slot so new creatures can be stored

const slot = inventory[index];
Retrieves the creature object at the slot index.
if (!slot) return;
Safety check: prevents selling from empty slots.
const payout = getSlotSellValue(slot);
Calls getSlotSellValue() which returns basePrice × level; higher level creatures sell for more.
money += payout;
Awards the payout to the player's money; selling a high-level creature is a significant coin source.
inventory[index] = null;
Clears the slot so it becomes available for new creatures; the global inventory array is modified, affecting passive income immediately.
slotMenu = null;
Closes the context menu; after selling, the menu is no longer valid and should disappear.

formatNumber()

formatNumber() is a utility that makes large numbers human-readable on screen. Common in incremental games, it prevents UI clutter (1234567890 → '1.23B'). The function uses toFixed(2) for 2 decimal places and checks tiers in descending order so the largest applicable suffix is used.

function formatNumber(value) {
  if (value >= 1e12) return (value / 1e12).toFixed(2) + 'T';
  if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B';
  if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M';
  if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
  return Math.floor(value).toString();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Select appropriate abbreviation tier if (value >= 1e12) return (value / 1e12).toFixed(2) + 'T'; if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B'; if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M'; if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';

Scales number down by dividing by the appropriate power of 10 and appends suffix (T, B, M, k)

if (value >= 1e12) return (value / 1e12).toFixed(2) + 'T';
If value is 1 trillion or more, divide by 1 trillion and append 'T'; 1.5e12 becomes '1.50T'.
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B';
If value is 1 billion or more, divide by 1 billion and append 'B'; 1.5e9 becomes '1.50B'.
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M';
If value is 1 million or more, divide by 1 million and append 'M'; 1.5e6 becomes '1.50M'.
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
If value is 1 thousand or more, divide by 1 thousand and append 'k'; 1500 becomes '1.50k'.
return Math.floor(value).toString();
For values under 1000, return as an integer with no decimals; 123 stays '123'.

📦 Key Variables

zones array

Stores 9 zone objects, each representing a rarity tier with spawn area, timer, and requirements; updated every frame to manage creature spawning.

let zones = [];
brainrots array

Stores all currently active (not yet delivered) creature objects; filtered each frame to remove delivered ones.

let brainrots = [];
player object

The Player instance; stores position, speed, and provides movement/animation methods.

let player;
attachedBrainrot object (Brainrot) or null

Stores a reference to the creature currently being carried by player, or null if none; used to draw the tether line and handle delivery.

let attachedBrainrot = null;
money number

Player's total coins; updated each frame by passive income and spent on upgrades.

let money = 100;
strength number

Player's total strength stat; determines which creature rarities can be attached; increased by buying strength upgrades.

let strength = 0;
speedLevel number

Player's speed upgrade level; tracks how many speed upgrades have been purchased and displayed in HUD.

let speedLevel = 1;
inventory array (length 8)

Stores up to 8 creatures in inventory; each element is a creature object (with name, level, income, etc.) or null if empty.

const inventory = Array(INVENTORY_SLOTS).fill(null);
inventorySlotsBounds array of objects

Stores clickable bounding boxes for each inventory slot; rebuilt every frame for input handling in mousePressed().

let inventorySlotsBounds = [];
slotMenu object {index} or null

Tracks which inventory slot has an open context menu (upgrade/sell); null if no menu is open.

let slotMenu = null;
lastOGSpawn number

Timestamp (in milliseconds via millis()) when an OG creature was last spawned; used to enforce the 10-minute spawn interval.

let lastOGSpawn = 0;
infoTimer number

Countdown timer (in frames) for displaying temporary status messages in the HUD; decremented each frame until it reaches 0.

let infoTimer = 0;
infoMessage string

The current temporary message to display (e.g., 'Attached creature' or 'Inventory full').

let infoMessage = '';

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

PERFORMANCE draw()

inventorySlotsBounds is rebuilt every frame even when inventory hasn't changed, causing unnecessary array allocation and GC pressure.

💡 Track a 'dirty' flag and only rebuild inventorySlotsBounds when inventory changes (on delivery or sale), not every frame.

BUG attemptAttach()

Creatures can be grabbed from any distance if there are no other unattached creatures nearby (the 80-pixel range check passes 'closest = null').

💡 Add a second check: 'if (!closest || closestDist >= 80) return info(...)' to enforce the range requirement independently.

STYLE brainrotCatalog initialization

Large hardcoded data structure (9 rarity tiers × 3-5 creatures) clutters sketch.js; difficult to extend or balance.

💡 Move rawCatalog and rarity configs to a separate data.js file or an external JSON file loaded at runtime for easier maintenance.

FEATURE Brainrot class

No visual variety—all creatures are simple circles with only color and text differentiating them.

💡 Add properties like 'emoji', 'size', or 'animationStyle' to the catalog so different creatures have unique visual identities beyond just color.

BUG maybeSpawnBrainrots()

If the brainrotCatalog[zone.key] array is empty, spawnBrainrot() is called but returns silently, wasting a spawn opportunity.

💡 Move the catalog check earlier: skip the whole spawn attempt if the catalog is empty, so the zone timer resets only on valid spawns.

PERFORMANCE mousePressed()

pointInRect is called many times each click; for 8 slots + 2 menu buttons, that's 10+ function calls and comparisons.

💡 Inline the rect collision logic or use a spatial hash if inventory grows to 50+ slots (as in the tryThis example).

🔄 Code Flow

Code flow showing setup, draw, createzones, player, brainrot, parseamount, attemptattach, handledelivery, applypassiveincome, maybeSpawnBrainrots, drawui, drawhud, drawinventorypanel, mousepressed, keypressed, upgradestrength, getstrengthupgradecost, upgradespeed, attemptslotupgrade, sellslot, formatnumber

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> passive-income[Apply passive income] draw --> animation-loop[Update all brainrot positions] draw --> delivery-cleanup[Remove delivered brainrots] draw --> delivery-check[Check if attached brainrot reached inventory] draw --> drawui[drawUI] drawui --> hud-render[Draw stats panel] drawui --> inventory-render[Draw inventory panel and slots] drawui --> carry-line[Draw line when carrying creature] passive-income --> income-calc[Sum income from all stored creatures] passive-income --> deltatime-income[Award fractional income based on frame time] animation-loop --> following-logic[Follow player if attached] animation-loop --> movement-apply[Apply movement if a key is pressed] animation-loop --> boundary-clamp[Keep player on canvas] delivery-cleanup --> delivery-check[Check if creature reached inventory zone] delivery-check --> slot-find[Find first empty inventory slot] delivery-check --> inventory-write[Create stored creature object] delivery-check --> removal[Delete creature from inventory] delivery-check --> info-message[Display temporary status message] click setup href "#fn-setup" click draw href "#fn-draw" click passive-income href "#sub-passive-income" click animation-loop href "#sub-animation-loop" click delivery-cleanup href "#sub-delivery-cleanup" click delivery-check href "#sub-delivery-check" click drawui href "#fn-drawui" click hud-render href "#sub-hud-render" click inventory-render href "#sub-inventory-render" click carry-line href "#sub-carry-line" click income-calc href "#sub-income-calc" click deltatime-income href "#sub-deltatime-income" click following-logic href "#sub-following-logic" click movement-apply href "#sub-movement-apply" click boundary-clamp href "#sub-boundary-clamp" click delivery-check href "#sub-delivery-check" click slot-find href "#sub-slot-find" click inventory-write href "#sub-inventory-write" click removal href "#sub-removal" click info-message href "#sub-info-message"

❓ Frequently Asked Questions

What visual elements can users expect to see in the 'pull a brainrot' sketch?

Users will see colorful, bouncing 'brainrot' creatures of various rarities, animated within vibrant zones, creating a lively and engaging visual experience.

How can players interact with the 'pull a brainrot' sketch?

Players can collect brainrots as they appear, drag and attach them to their inventory, and strategically click to upgrade and unlock rarer creatures over time.

What creative coding techniques are demonstrated in the 'pull a brainrot' sketch?

The sketch showcases concepts like object management, animation, and event-driven interactions, allowing for a dynamic and interactive gameplay experience.

Preview

pull a brainrot - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of pull a brainrot - Code flow showing setup, draw, createzones, player, brainrot, parseamount, attemptattach, handledelivery, applypassiveincome, maybeSpawnBrainrots, drawui, drawhud, drawinventorypanel, mousepressed, keypressed, upgradestrength, getstrengthupgradecost, upgradespeed, attemptslotupgrade, sellslot, formatnumber
Code Flow Diagram