Good boy yyyy

This is an absurdly chaotic sushi restaurant simulator where customers arrive, order sushi, and eat—but then UFOs abduct them, zombies attack, sharks bite, helicopters shoot, and dozens of other ridiculous events unfold. The sketch combines dozens of event systems, combat mechanics, state management, and visual chaos into a single unhinged game.

🧪 Try This!

Experiment with the code by making these changes:

  1. Increase customer arrival speed — More customers arrive faster, creating chaos and more event triggers—watch the sickness and attacks multiply.
  2. Disable sick customers (no attacks) — Set sickness chance to 0 so no customers get sick, which prevents all the chaotic events (zombies, UFOs, sharks, etc.) from spawning.
  3. Make customers much faster — Customers move twice as fast between sushi station, table, and cashier, completing their meal cycle much quicker.
  4. Zombies move much faster — Change a zombie's initial speed from 0.3 to 2 pixels per frame—they'll chase customers much more aggressively.
  5. Sharks stay longer — After biting a customer, sharks walk on shore for longer (10 seconds instead of 5) before leaving.
  6. Start with no customers — Change the initial spawn count from 3 to 0, so the restaurant starts completely empty until you click the sushi station.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a sushi restaurant simulator that spirals into complete madness. Customers arrive hungry and docile, but the moment they eat, the chaos begins: UFOs beam them up, zombies spawn and fight them hand-to-hand with knives, sharks emerge from the water, police helicopters arrest them, tanks roll in, airplanes crash, bees sting them to death, and custom image uploads can turn anyone into an attacker. It teaches advanced p5.js techniques including state machines (each character has multiple states), object-oriented design with inheritance, array management for dozens of simultaneous entities, and event probability systems.

The code is organized into a setup() that initializes the canvas and three initial customers, and a draw() loop that spawns new customers, updates all game entities, handles collisions and payments, and renders everything to screen. By studying it you will learn how to manage complex game state across dozens of classes, how to use state machines to control entity behavior, how to spawn entities probabilistically based on game events, how to implement simple combat systems, and how to chain events together (when a customer gets sick, dozens of different attackers might spawn depending on random rolls).

⚙️ How It Works

  1. When the sketch starts, setup() creates a full-screen canvas, spawns three initial customers off-screen left, generates 100 background stars, and creates UI elements for image upload and a 'People Call' button.
  2. Every frame, draw() clears the background, draws all game areas (sushi station, table, cashier, jail, toilet, water), displays the money counter, and spawns new customers automatically at CUSTOMER_SPAWN_INTERVAL (1500ms).
  3. Customers move through states: WAITING → MOVING_TO_SUSHI → GRABBING_SUSHI → MOVING_TO_TABLE → PLACING_SUSHI → EATING_SUSHI. During eating, they have a 20% chance to get sick.
  4. When sick, a customer rolls the dice: they might end up in the water (triggering sharks or rescue dogs), or on land (triggering UFOs, poop entities, zombies, police cars, tanks, helicopters, airdrop soldiers, airplanes that crash, TV death, bees, wanted levels, blood helicopters, or plane crashes).
  5. If a zombie event triggers and the zombie wins a 1v1 knife fight against the customer, the zombie proceeds to the cashier to pay like a normal customer. Customers who pay transition to WAITING_FOR_AIRPLANE and are picked up by a school bus (20%), playground airplane (15%), or standard airplane (65%).
  6. Throughout all this, custom image persons can be spawned and will attack customers, spawning poop helicopters to carry away their bodies. Every entity (UFO, shark, helicopter, etc.) has its own update() and display() logic, collision detection, and removal when off-screen.

🎓 Concepts You'll Learn

State machinesObject-oriented design with classes and inheritanceArray and entity managementProbabilistic event spawningDistance-based collision detectionCombat simulation with health and damageAnimation timing with millis() and elapsed timeScreen wrapping and entity removalEvent chaining and cascading effects

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's where you initialize the canvas, set up global variables, and create entities that exist from the beginning. The try-catch block catches errors so a broken setup doesn't crash silently—instead, an error message prints to the console.

🔬 This loop spawns customers off-screen left. What happens if you change random(100, height - 100) to random(height / 2, height) so all initial customers start in the bottom half?

    // Spawn a few initial customers to prevent an empty screen at the start
    for (let i = 0; i < 3; i++) {
      customers.push(new Customer(-CUSTOMER_SIZE * (i + 1), random(100, height - 100)));
function setup() {
  try {
    createCanvas(windowWidth, windowHeight);
    lastCustomerSpawnTime = millis();

    // Spawn a few initial customers to prevent an empty screen at the start
    for (let i = 0; i < 3; i++) {
      customers.push(new Customer(-CUSTOMER_SIZE * (i + 1), random(100, height - 100))); // Spawn off-screen left at random height
    }
    
    // NEW: Generate 100 background stars (RESTORED)
    for (let i = 0; i < 100; i++) {
      backgroundStars.push({
        x: random(width),
        y: random(height / 2), // Top half of the screen
        size1: random(5, 10),
        size2: random(10, 20),
        points: floor(random(5, 8)) // 5, 6, or 7 points
      });
    }

    // NEW: Create Image Upload Input and People Call Button (RESTORED)
    imageInput = createInput('', 'file');
    imageInput.position(20, height - 80);
    imageInput.size(150, 20);
    imageInput.attribute('accept', 'image/*'); // Only accept image files
    imageInput.elt.addEventListener('change', handleImageUpload);

    peopleCallCustomButton = createButton('People Call Image!');
    peopleCallCustomButton.position(20, height - 50);
    peopleCallCustomButton.size(150, 30);
    peopleCallCustomButton.mousePressed(callCustomImagePerson);


  } catch (error) {
    console.error("Error in setup():", error);
    // Optionally display an error message on the screen if canvas fails
    textSize(24);
    textAlign(CENTER, CENTER);
    text("Error in Setup!", width/2, height/2);
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

function-call Canvas creation and timing createCanvas(windowWidth, windowHeight); lastCustomerSpawnTime = millis();

Sets up a full-screen canvas and initializes the spawn timer so customers start arriving immediately

for-loop Spawn initial customers for (let i = 0; i < 3; i++) { customers.push(new Customer(-CUSTOMER_SIZE * (i + 1), random(100, height - 100))); }

Creates three customers off-screen left at staggered x positions so they don't all arrive at once

for-loop Generate background stars for (let i = 0; i < 100; i++) { backgroundStars.push({ x: random(width), y: random(height / 2), size1: random(5, 10), size2: random(10, 20), points: floor(random(5, 8)) }); }

Populates the backgroundStars array with 100 star objects containing position and size properties for visual decoration

function-call Create image upload UI imageInput = createInput('', 'file'); imageInput.position(20, height - 80); imageInput.size(150, 20); imageInput.attribute('accept', 'image/*'); imageInput.elt.addEventListener('change', handleImageUpload);

Creates a file input element at the bottom left that accepts image files and triggers handleImageUpload when a file is selected

function-call Create People Call button peopleCallCustomButton = createButton('People Call Image!'); peopleCallCustomButton.position(20, height - 50); peopleCallCustomButton.size(150, 30); peopleCallCustomButton.mousePressed(callCustomImagePerson);

Creates a button that spawns custom image attackers, toggling spam mode when clicked

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire window. windowWidth and windowHeight are p5.js variables that adjust if the window resizes.
lastCustomerSpawnTime = millis();
Records the current milliseconds so the draw loop can track when to spawn the next customer (via CUSTOMER_SPAWN_INTERVAL).
customers.push(new Customer(-CUSTOMER_SIZE * (i + 1), random(100, height - 100)));
Adds three new Customer objects to the customers array. They spawn off-screen left (negative x) at random heights so the restaurant doesn't start empty.
backgroundStars.push({ x: random(width), y: random(height / 2), size1: random(5, 10), size2: random(10, 20), points: floor(random(5, 8)) });
Adds a star object with random position (top half only), two size values for the star's inner and outer radius, and a random point count (5–7) so stars look different.
imageInput.attribute('accept', 'image/*');
Restricts the file picker to image files only, preventing accidental uploads of text or videos.
imageInput.elt.addEventListener('change', handleImageUpload);
Attaches a listener so when the user picks an image, handleImageUpload() fires immediately to load the image into the customImage variable.
peopleCallCustomButton.mousePressed(callCustomImagePerson);
Binds the button so clicking it calls callCustomImagePerson(), which toggles image spam mode and spawns the first attacker.

draw()

draw() runs 60 times per second (by default). It's where all animation happens—every frame, you clear the background, update game state (move entities, check timers, remove off-screen objects), and redraw everything. The for-loops here show a common pattern: for each entity type (customers, zombies, helicopters), call update() and display(). This separates logic (update) from rendering (display), which is very clean. The backward loop (for (let i = sushi.length - 1; i >= 0; i--)) is crucial because if you delete an item from an array while looping forward, you'll skip the next item.

🔬 This loop updates and displays every customer. What happens if you comment out customer.update() so customers are drawn but never move or change state?

    for (let customer of customers) {
      customer.update();
      customer.display();
    }
function draw() {
  try {
    background(220);

    // NEW: Draw background stars (RESTORED)
    fill(255, 200, 0); // Yellow for stars
    noStroke();
    for (let star of backgroundStars) {
      drawStar(star.x, star.y, star.size1, star.size2, star.points);
    }

    // --- Draw Game Areas ---
    stroke(0);
    fill(200);
    rectMode(CORNER); // Reset rectMode to CORNER for these areas
    rect(SUSHI_STATION_X - SUSHI_STATION_SIZE / 2, SUSHI_STATION_Y - SUSHI_STATION_SIZE / 2, SUSHI_STATION_SIZE, SUSHI_STATION_SIZE, 10);
    fill(0);
    textSize(12);
    textAlign(CENTER, CENTER);

    fill(150, 100, 50); // Brown for table
    rect(TABLE_X, TABLE_Y, TABLE_WIDTH, TABLE_HEIGHT, 10);

    fill(180);
    rect(CASHIER_X, CASHIER_Y, CASHIER_WIDTH, CASHIER_HEIGHT, 10);

    // New: Draw Jail Area
    fill(100); // Gray for jail
    rect(JAIL_X, JAIL_Y, JAIL_WIDTH, JAIL_HEIGHT, 10);

    // New: Draw Toilet Area
    fill(150, 200, 255); // Light blue for toilet
    rect(TOILET_X, TOILET_Y, TOILET_WIDTH, TOILET_HEIGHT, 10);

    // --- Draw Water if sharks are present or dogs are present --- (RESTORED DOGS)
    if (sharks.length > 0 || rescueDogs.length > 0) {
      fill(100, 150, 200, 180); // Semi-transparent blue
      noStroke();
      rect(0, WATER_Y, width, WATER_HEIGHT);

      // Add a simple wave effect at the top of the water
      stroke(100, 150, 200);
      strokeWeight(2);
      noFill();
      beginShape();
      for (let x = 0; x <= width; x += 20) {
        let y = WATER_Y + sin(x * 0.05 + frameCount * 0.05) * 5;
        vertex(x, y);
      }
      endShape();
    }

    // --- Draw Money ---
    fill(0);
    textSize(24);
    textAlign(LEFT, TOP);
    text(`Money: $${totalMoney}`, 20, 20);

    // --- Draw Poop Land --- (Draw BEFORE customers)
    for (let customer of customers) {
      if (customer.sick && customer.poopLand) {
        push();
        translate(customer.x, customer.y + CUSTOMER_SIZE / 2);
        noStroke();
        fill(102, 51, 0); // Brown color for poop land

        // Create an irregular blob shape using noise
        beginShape();
        let radius = CUSTOMER_SIZE * 0.8;
        let numPoints = 20;
        let noiseScale = 0.05;
        for (let i = 0; i < numPoints; i++) {
          let angle = map(i, 0, numPoints, 0, TWO_PI);
          let xOffset = cos(angle) * radius;
          let yOffset = sin(angle) * radius;
          let n = noise(xOffset * noiseScale, yOffset * noiseScale, frameCount * 0.01);
          let r = radius * (0.8 + 0.4 * n);
          vertex(cos(angle) * r, sin(angle) * r);
        }
        endShape(CLOSE);
        pop();
      }
    }

    // --- Manage Sushi ---
    for (let i = sushi.length - 1; i >= 0; i--) {
      sushi[i].display();
      if (sushi[i].eaten) {
        sushi.splice(i, 1);
      }
    }

    // --- Manage Customers ---
    // Spawn new customer (automatic, no limit on count)
    if (millis() - lastCustomerSpawnTime > CUSTOMER_SPAWN_INTERVAL) {
      customers.push(new Customer(-CUSTOMER_SIZE, random(100, height - 100)));
      lastCustomerSpawnTime = millis();
    }

    // Update and display customers
    for (let customer of customers) {
      customer.update();
      customer.display();
    }

    // NEW: Update and display CustomImagePersons (RESTORED)
    for (let i = customImagePersons.length - 1; i >= 0; i--) {
      let imagePerson = customImagePersons[i];
      imagePerson.update();
      imagePerson.display();

      // If CustomImagePerson is dead and waiting, spawn a helicopter for it
      if (imagePerson.state === 'dead_waiting_helicopter') {
        let existingHelicopter = customImageBodyHelicopters.find(h => h.targetBody === imagePerson);
        if (!existingHelicopter) {
          let newHeli = new CustomImageBodyHelicopter(-150, random(50, height - 50), imagePerson);
          customImageBodyHelicopters.push(newHeli);
        }
      }
      // If imagePerson has left the screen, remove it from the array
      if (imagePerson.state === 'leaving' && imagePerson.x > width + CUSTOMER_SIZE) {
        customImagePersons.splice(i, 1);
      }
    }

    // NEW: Update and display CustomImageBodyHelicopters (RESTORED)
    for (let i = customImageBodyHelicopters.length - 1; i >= 0; i--) {
      let heli = customImageBodyHelicopters[i];
      heli.update();
      heli.display();
      if (heli.state === 'leaving_screen' && heli.x > width + 150) {
        customImageBodyHelicopters.splice(i, 1);
      }
    }

    // NEW: Spamming logic (RESTORED)
    if (isSpammingImages && millis() - lastImageSpawnTime > IMAGE_SPAM_INTERVAL) {
      spawnCustomImagePerson();
      lastImageSpawnTime = millis();
    }

    // --- Manage UFOs ---
    for (let ufo of ufos) {
      ufo.update();
      ufo.display();
    }

    // --- Manage Poops ---
    for (let poop of poops) {
      poop.update();
      poop.display();
    }
    
    // --- Manage Zombies ---
    for (let zombie of zombies) {
      zombie.update();
      zombie.display();
    }

    // --- Manage Airplanes (Airplanes now drop airdrops) ---
    for (let airplane of airplanes) {
      airplane.update();
      airplane.display();
    }

    // --- Manage Airplane Crashes (New!) --- (RESTORED)
    for (let i = airplaneCrashes.length - 1; i >= 0; i--) {
      airplaneCrashes[i].update();
      airplaneCrashes[i].display();
      if (airplaneCrashes[i].state === 'finished') {
        airplaneCrashes.splice(i, 1);
      }
    }

    // --- Manage Police Cars (New!) ---
    for (let policeCar of policeCars) {
      policeCar.update();
      policeCar.display();
    }

    // --- Manage SWAT Cars (NEW!) --- (RESTORED)
    for (let swatCar of swatCars) {
      swatCar.update();
      swatCar.display();
    }

    // --- Manage Tanks (New!) ---
    for (let tank of tanks) {
      tank.update();
      tank.display();
    }

    // --- Manage Helicopters (New!) ---
    for (let helicopter of helicopters) {
      helicopter.update();
      helicopter.display();
    }

    // --- Manage Police Helicopters (New!) --- (RESTORED)
    for (let policeHelicopter of policeHelicopters) {
      policeHelicopter.update();
      policeHelicopter.display();
    }

    // --- Manage Airdrops (New!) ---
    for (let airdrop of airdrops) {
      airdrop.update();
      airdrop.display();
    }

    // --- Manage Airdrop Persons (New!) ---
    for (let airdropPerson of airdropPersons) {
      airdropPerson.update();
      airdropPerson.display();
    }

    // --- Manage Spiders (NEW!) ---
    for (let i = spiders.length - 1; i >= 0; i--) {
      spiders[i].update();
      spiders[i].display();
      if (spiders[i].state === 'leaving' && spiders[i].targetCustomer === null) {
        spiders.splice(i, 1);
      }
    }

    // --- Manage Sharks (NEW!) ---
    for (let shark of sharks) {
      shark.update();
      shark.display();
    }

    // --- Manage School Buses (NEW!) --- (RESTORED)
    for (let schoolBus of schoolBuses) {
      schoolBus.update();
      schoolBus.display();
    }

    // --- Manage Playground Airplanes (NEW!) --- (RESTORED - FIX FOR ERROR)
    for (let playgroundAirplane of playgroundAirplanes) {
      playgroundAirplane.update();
      playgroundAirplane.display();
    }

    // --- Manage TVs (NEW!) ---
    for (let tv of tvs) {
      tv.update();
      tv.display();
      if (customers.filter(c => c === tv.targetCustomer).length === 0) {
        tvs = tvs.filter(t => t !== tv);
      }
    }

    // --- Manage Poop Helicopters (NEW!) ---
    for (let poopHelicopter of poopHelicopters) {
      poopHelicopter.update();
      poopHelicopter.display();
    }

    // --- Manage Bees (NEW!) ---
    for (let bee of bees) {
      bee.update();
      bee.display();
    }

    // --- Manage Blood Helicopters (NEW!) --- (RESTORED)
    for (let bloodHelicopter of bloodHelicopters) {
      bloodHelicopter.update();
      bloodHelicopter.display();
    }

    // --- Manage Rescue Dogs (NEW!) --- (RESTORED)
    for (let rescueDog of rescueDogs) {
      rescueDog.update();
      rescueDog.display();
    }

    // --- Display "Empty Restaurant" message if no customers --- (RESTORED customImagePersons check)
    if (customers.length === 0 && customImagePersons.length === 0) {
      fill(255, 0, 0);
      textSize(32);
      textAlign(CENTER, CENTER);
      text("EMPTY RESTAURANT!", width / 2, height / 2);
    }

  } catch (error) {
    console.error("Error in draw():", error);
    fill(255, 0, 0);
    textSize(24);
    textAlign(CENTER, CENTER);
    text("Error: " + error.message, width/2, height/2);
    noLoop();
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

function-call Clear canvas background(220);

Repaints the entire canvas light gray, erasing the previous frame so motion isn't a blur trail

for-loop Draw background stars for (let star of backgroundStars) { drawStar(star.x, star.y, star.size1, star.size2, star.points); }

Iterates through all 100 background stars and calls drawStar() for each to render them as yellow star shapes

conditional Draw water and waves if (sharks.length > 0 || rescueDogs.length > 0) { ... }

Only draws the water area (and animates waves) if there are sharks or rescue dogs on screen, saving performance otherwise

for-loop Draw poop land for (let customer of customers) { if (customer.sick && customer.poopLand) { ... } }

Loops through all customers and draws a brown blob beneath any sick customer who has poopLand true

for-loop Update and display sushi for (let i = sushi.length - 1; i >= 0; i--) { sushi[i].display(); if (sushi[i].eaten) { sushi.splice(i, 1); } }

Loops backward through sushi array to safely remove eaten pieces while displaying the rest

conditional Spawn new customer if (millis() - lastCustomerSpawnTime > CUSTOMER_SPAWN_INTERVAL) { customers.push(new Customer(-CUSTOMER_SIZE, random(100, height - 100))); lastCustomerSpawnTime = millis(); }

Checks elapsed time and spawns a new customer off-screen left if enough time has passed

for-loop Update and display all entities for (let customer of customers) { ... } for (let ufo of ufos) { ... } for (let shark of sharks) { ... }

20+ entity loops that call update() and display() on every zombie, helicopter, shark, etc., keeping all game objects animated and rendered

conditional Empty restaurant message if (customers.length === 0 && customImagePersons.length === 0) { fill(255, 0, 0); textSize(32); textAlign(CENTER, CENTER); text("EMPTY RESTAURANT!", width / 2, height / 2); }

Displays a red 'EMPTY RESTAURANT!' message when both customers and custom image attackers are gone from the screen

background(220);
Fills the canvas with light gray (220 out of 255), erasing everything from the last frame before drawing new content.
for (let star of backgroundStars) { drawStar(star.x, star.y, star.size1, star.size2, star.points); }
Loops through each star object and calls drawStar() to render it at its position with its unique size and point count.
if (sharks.length > 0 || rescueDogs.length > 0) {
Only draws the water area and waves if there are actually sharks or dogs on screen—if not, water is skipped to save computation.
if (millis() - lastCustomerSpawnTime > CUSTOMER_SPAWN_INTERVAL) {
Checks if enough milliseconds have elapsed since the last customer spawn. If true, creates a new customer and resets the timer.
customers.push(new Customer(-CUSTOMER_SIZE, random(100, height - 100)));
Constructs a brand new Customer object off-screen left (negative x) at a random height, then adds it to the customers array.
for (let customer of customers) { customer.update(); customer.display(); }
Loops through every customer, calling update() to run their state machine and move them, then display() to draw them on screen.
for (let i = sushi.length - 1; i >= 0; i--) {
Loops backward through the sushi array (from end to start) so removing items doesn't skip any—essential when deleting during iteration.
if (sushi[i].eaten) { sushi.splice(i, 1); }
If a sushi piece is marked as eaten, removes it from the array using splice(), freeing memory.
if (customers.length === 0 && customImagePersons.length === 0) {
Only shows the 'EMPTY RESTAURANT!' message when there are truly no customers or custom image attackers left on screen.

Customer class

The Customer class is the heart of the game. Every customer has a state (from CUSTOMER_STATES), a position (x, y), a target (targetX, targetY), and a move() method that handles smooth pathfinding. The update() method contains a giant switch statement with a case for every state—each case checks timers or proximity and transitions to the next state. When a customer gets sick after eating, the update() method rolls the dice on dozens of different attackers. This is a classic state machine: input (current state) → logic (what to do in this state) → output (new state). Understanding this pattern is foundational for game design.

class Customer {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.targetX = x;
    this.targetY = y;
    this.state = CUSTOMER_STATES.WAITING;
    this.hasSushi = null;
    this.eatingTimer = 0;
    this.toiletTimer = 0;
    this.paymentReceived = false;
    this.leavingOffset = 0;
    this.color = color(random(150, 255), random(150, 255), random(150, 255));
    this.sick = false;
    this.poopLand = false;
    this.tvWatchTimer = 0;
    this.stingTimer = 0;
    this.wantedLevel = 0;
    this.health = CUSTOMER_HEALTH;
    this.damage = CUSTOMER_DAMAGE;
    this.attackCooldown = ATTACK_COOLDOWN;
    this.lastAttackTime = 0;
    this.opponent = null;
  }

  display() {
    push();
    translate(this.x, this.y);
    // Body, eyes, mouth rendering
    fill(this.color);
    noStroke();
    ellipse(0, 0, CUSTOMER_SIZE);
    fill(0);
    ellipse(-CUSTOMER_SIZE * 0.15, -CUSTOMER_SIZE * 0.15, CUSTOMER_SIZE * 0.1);
    ellipse(CUSTOMER_SIZE * 0.15, -CUSTOMER_SIZE * 0.15, CUSTOMER_SIZE * 0.1);
    stroke(0);
    line(-CUSTOMER_SIZE * 0.1, CUSTOMER_SIZE * 0.1, CUSTOMER_SIZE * 0.1, CUSTOMER_SIZE * 0.1);
    // (... many state-specific display blocks ...)
    pop();
  }

  move() {
    let d = dist(this.x, this.y, this.targetX, this.targetY);
    if (d > CUSTOMER_SPEED) {
      let angle = atan2(this.targetY - this.y, this.targetX - this.x);
      this.x += cos(angle) * CUSTOMER_SPEED;
      this.y += sin(angle) * CUSTOMER_SPEED;
    } else {
      this.x = this.targetX;
      this.y = this.targetY;
    }
  }

  setState(newState, targetX, targetY) {
    this.state = newState;
    this.targetX = targetX;
    this.targetY = targetY;
  }

  takeDamage(amount) {
    this.health -= amount;
    if (this.health <= 0) {
      this.state = CUSTOMER_STATES.DEAD_FROM_TV;
      this.toiletTimer = millis();
      if (this.opponent && this.opponent.state === 'fighting_customer') {
        this.opponent.setState('moving_to_cashier', CASHIER_X + CASHIER_WIDTH / 2, CASHIER_Y + CUSTOMER_SIZE);
        this.opponent.opponent = null;
      }
    }
  }

  update() {
    // (... 40+ lines of state machine logic ...)
  }

  contains(px, py) {
    let d = dist(px, py, this.x, this.y);
    return d < CUSTOMER_SIZE / 2;
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

function Customer constructor constructor(x, y) { this.x = x; this.y = y; ... }

Initializes a new customer at position (x, y) with random color, zero health timers, WAITING state, and combat properties

function Customer display display() { ... fill(this.color); ellipse(0, 0, CUSTOMER_SIZE); ... }

Draws the customer as a colored circle with eyes and mouth, plus state-specific bubbles (sick, toilet, airplane, wanted stars, health bar if fighting)

function Customer move move() { let d = dist(...); if (d > CUSTOMER_SPEED) { ... } }

Moves the customer toward their target position at CUSTOMER_SPEED pixels per frame using angle math

function Customer setState setState(newState, targetX, targetY) { this.state = newState; this.targetX = targetX; this.targetY = targetY; }

Cleanly transitions a customer to a new state and sets their movement target in one call

function Customer takeDamage takeDamage(amount) { this.health -= amount; if (this.health <= 0) { ... } }

Reduces customer health by the given amount and triggers death logic if health reaches 0 (for knife fights with zombies)

function Customer update update() { ... switch(this.state) { ... } ... }

Runs the customer's state machine every frame, handling transitions between waiting, moving, eating, sick, fighting, and leaving

function Customer contains contains(px, py) { let d = dist(px, py, this.x, this.y); return d < CUSTOMER_SIZE / 2; }

Tests if a point (px, py) is inside the customer's circular body—used in mousePressed() for payment detection

this.color = color(random(150, 255), random(150, 255), random(150, 255));
Assigns a random RGB color to the customer (values 150–255 ensure bright, visible colors rather than dark ones).
this.state = CUSTOMER_STATES.WAITING;
Sets the initial state to WAITING, meaning the customer will look for available sushi to grab.
let d = dist(this.x, this.y, this.targetX, this.targetY);
Calculates the distance from the customer's current position to their target using the dist() function.
if (d > CUSTOMER_SPEED) {
If the distance is greater than one step, the customer hasn't reached the target yet, so move them closer.
let angle = atan2(this.targetY - this.y, this.targetX - this.x);
Calculates the angle from the customer to the target using atan2(), which returns radians pointing toward (targetX, targetY).
this.x += cos(angle) * CUSTOMER_SPEED; this.y += sin(angle) * CUSTOMER_SPEED;
Moves the customer one step in the direction of that angle: cos gets the x-component, sin gets the y-component.
if (this.health <= 0) {
Checks if the customer has been defeated in a knife fight. If so, they die and drop out of the FIGHTING_ZOMBIE state.

mousePressed()

mousePressed() is called once whenever the mouse button is clicked. It's how the player interacts with the game: clicking the sushi station cooks sushi, and clicking a customer at the cashier collects payment. The function uses dist() to test spatial proximity—a classic collision detection pattern in games. Notice the payment logic spawns a random departure vehicle using a probability roll: this is how you weight different outcomes (school bus is most common at 20%, but three distinct choices add variety).

function mousePressed() {
  // 1. Cook Sushi
  let d = dist(mouseX, mouseY, SUSHI_STATION_X, SUSHI_STATION_Y);
  if (d < SUSHI_STATION_SIZE / 2) {
    let newSushi = new Sushi(SUSHI_STATION_X, SUSHI_STATION_Y);
    newSushi.cooked = true;
    sushi.push(newSushi);
    customers.push(new Customer(-CUSTOMER_SIZE, random(100, height - 100)));
    lastCustomerSpawnTime = millis();
  }

  // 2. Receive Payment from Customer (Click on customers at the cashier!)
  for (let customer of customers) {
    if (customer.state === CUSTOMER_STATES.PAYING && !customer.paymentReceived) {
      if (customer.contains(mouseX, mouseY)) {
        totalMoney += SUSHI_VALUE;
        customer.paymentReceived = true;
        customer.setState(CUSTOMER_STATES.WAITING_FOR_AIRPLANE, customer.x, customer.y);

        let departureRoll = random();
        if (departureRoll < SCHOOL_BUS_SPAWN_CHANCE) {
          let schoolBus = new SchoolBus(-150, customer.y, customer);
          schoolBuses.push(schoolBus);
        } else if (departureRoll < SCHOOL_BUS_SPAWN_CHANCE + PLAYGROUND_AIRPLANE_SPAWN_CHANCE) {
          let playgroundAirplane = new PlaygroundAirplane(-180, random(50, height - 50), customer);
          playgroundAirplanes.push(playgroundAirplane);
        } else {
          let departureAirplane = new Airplane(-180, random(50, height - 50), customer);
          if (random() < AIRPLANE_FIRE_CHANCE) {
            departureAirplane.fire = true;
            departureAirplane.state = 'crashing';
          }
          airplanes.push(departureAirplane);
        }
      }
    }
  }

  // NEW: Receive Payment from Zombie (Click on winning zombies at the cashier!)
  for (let zombie of zombies) {
    if (zombie.state === 'paying' && !zombie.paymentReceived) {
      let d = dist(mouseX, mouseY, zombie.x, zombie.y);
      if (d < CUSTOMER_SIZE / 2) {
        totalMoney += SUSHI_VALUE;
        zombie.paymentReceived = true;
        zombie.setState('leaving', zombie.x, zombie.y);
      }
    }
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Cook sushi interaction let d = dist(mouseX, mouseY, SUSHI_STATION_X, SUSHI_STATION_Y); if (d < SUSHI_STATION_SIZE / 2) { ... }

Tests if the mouse click was near the sushi station and, if so, creates a new cooked sushi and spawns a customer

for-loop Customer payment handling for (let customer of customers) { if (customer.state === CUSTOMER_STATES.PAYING && !customer.paymentReceived) { if (customer.contains(mouseX, mouseY)) { ... } } }

Loops through all customers looking for those in PAYING state, then checks if the click hit them and processes payment

conditional Departure method probability roll let departureRoll = random(); if (departureRoll < SCHOOL_BUS_SPAWN_CHANCE) { ... } else if (departureRoll < SCHOOL_BUS_SPAWN_CHANCE + PLAYGROUND_AIRPLANE_SPAWN_CHANCE) { ... } else { ... }

Uses random() to pick which vehicle picks up the customer: school bus (20%), playground airplane (15%), or standard airplane (65%)

for-loop Zombie payment handling for (let zombie of zombies) { if (zombie.state === 'paying' && !zombie.paymentReceived) { let d = dist(mouseX, mouseY, zombie.x, zombie.y); if (d < CUSTOMER_SIZE / 2) { ... } } }

Loops through all zombies looking for winners in PAYING state and processes their payment (just like a regular customer)

let d = dist(mouseX, mouseY, SUSHI_STATION_X, SUSHI_STATION_Y);
Calculates the distance from the mouse click to the sushi station center.
if (d < SUSHI_STATION_SIZE / 2) {
If the click is within the radius of the sushi station, the player clicked it and cooking should happen.
let newSushi = new Sushi(SUSHI_STATION_X, SUSHI_STATION_Y); newSushi.cooked = true; sushi.push(newSushi);
Creates a new Sushi object, marks it as cooked (so customers can grab it), and adds it to the global sushi array.
if (customer.state === CUSTOMER_STATES.PAYING && !customer.paymentReceived) {
Only processes payment if the customer is in PAYING state and hasn't already been paid (paymentReceived is false).
if (customer.contains(mouseX, mouseY)) {
Calls the customer's contains() method to check if the mouse click landed inside their circular body.
totalMoney += SUSHI_VALUE;
Adds SUSHI_VALUE (10) to the player's total money pool.
let departureRoll = random();
Generates a random number 0–1 for the probability roll that determines departure vehicle.
if (departureRoll < SCHOOL_BUS_SPAWN_CHANCE) {
If the roll is less than 0.2 (20%), spawn a school bus to pick up the customer.

drawStar()

drawStar() is a helper function that draws a star shape at a given position with a given number of points and two radii (inner and outer). It uses trigonometry (cos and sin) to place vertices in a circle. The key insight is that a star is just a polygon where you alternate between far vertices (outer radius) and close vertices (inner radius) as you go around the circle. This pattern—use trig to place points in a circle—is useful for drawing all sorts of radial shapes: flowers, gears, spirographs, etc.

🔬 This function draws a star by alternating between outer points (radius2) and inner points (radius1). What happens if you swap radius1 and radius2 (so inners are bigger than outers)? The star will invert into a concave blob shape.

  let angle = TWO_PI / npoints;
  let halfAngle = angle / 2.0;
  beginShape();
  for (let a = 0; a < TWO_PI; a += angle) {
    let sx = x + cos(a) * radius2;
    let sy = y + sin(a) * radius2;
    vertex(sx, sy);
    sx = x + cos(a + halfAngle) * radius1;
    sy = y + sin(a + halfAngle) * radius1;
    vertex(sx, sy);
  }
function drawStar(x, y, radius1, radius2, npoints) {
  let angle = TWO_PI / npoints;
  let halfAngle = angle / 2.0;
  beginShape();
  for (let a = 0; a < TWO_PI; a += angle) {
    let sx = x + cos(a) * radius2;
    let sy = y + sin(a) * radius2;
    vertex(sx, sy);
    sx = x + cos(a + halfAngle) * radius1;
    sy = y + sin(a + halfAngle) * radius1;
    vertex(sx, sy);
  }
  endShape(CLOSE);
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Calculate angle per point let angle = TWO_PI / npoints; let halfAngle = angle / 2.0;

Divides a full circle (TWO_PI radians) into npoints equal sections, with halfAngle for inner points

for-loop Draw star vertices for (let a = 0; a < TWO_PI; a += angle) { let sx = x + cos(a) * radius2; let sy = y + sin(a) * radius2; vertex(sx, sy); sx = x + cos(a + halfAngle) * radius1; sy = y + sin(a + halfAngle) * radius1; vertex(sx, sy); }

Loops around the circle, placing outer vertices at radius2 and inner vertices at radius1 to create the star spike pattern

let angle = TWO_PI / npoints;
Calculates the angle (in radians) between consecutive outer points of the star. For a 5-pointed star, angle = TWO_PI / 5 ≈ 1.257 radians.
let halfAngle = angle / 2.0;
Calculates the angle to the inner points, which sit halfway between outer points.
beginShape();
Starts recording vertices for a shape that will be drawn as a connected polygon.
for (let a = 0; a < TWO_PI; a += angle) {
Loops around a full circle (0 to TWO_PI radians) in steps of angle, hitting each outer point of the star.
let sx = x + cos(a) * radius2;
Calculates the x-coordinate of an outer point using the cosine of angle a, scaled by radius2.
let sy = y + sin(a) * radius2;
Calculates the y-coordinate of an outer point using the sine of angle a, scaled by radius2.
vertex(sx, sy);
Adds the outer point to the shape.
sx = x + cos(a + halfAngle) * radius1;
Calculates the x-coordinate of an inner point (halfway between outer points) using radius1.
vertex(sx, sy);
Adds the inner point to the shape, creating the dip between spikes.
endShape(CLOSE);
Finishes the shape and closes it by connecting the last vertex back to the first.

Zombie class

The Zombie class teaches combat systems. Zombies can either instantly kill their target (the 'attacking' state) or engage in a 1v1 knife fight where both combatants take turns damaging each other until one dies. If a zombie wins a 1v1, it becomes a paying customer—a darkly humorous reminder that this game cares little for customer service. The takeDamage() method shows how to handle death: when health <= 0, the entity transitions to a 'leaving' state. This is reused by the Customer class for the same combat mechanic.

🔬 This code makes the zombie approach its target customer. What happens if you change this.targetX to this.targetCustomer.x (right now it uses a stale targetX set at spawn, so it doesn't follow the customer if they move)? This will make zombies track moving customers.

    if (this.targetCustomer) {
      let d = dist(this.x, this.y, this.targetCustomer.x, this.targetCustomer.y);
      if (d > this.speed) {
        let angle = atan2(this.targetCustomer.y - this.y, this.targetX - this.x);
        this.x += cos(angle) * this.speed;
        this.y += sin(angle) * this.speed;
class Zombie {
  constructor(x, y, targetCustomer) {
    this.x = x;
    this.y = y;
    this.speed = 0.3;
    this.size = CUSTOMER_SIZE * 0.9;
    this.targetCustomer = targetCustomer;
    this.targetX = targetCustomer.x;
    this.targetY = targetCustomer.y;
    this.state = 'moving';
    this.attackTimer = 0;
    this.attackDuration = 500;
    this.health = ZOMBIE_HEALTH;
    this.damage = ZOMBIE_DAMAGE;
    this.attackCooldown = ATTACK_COOLDOWN;
    this.lastAttackTime = 0;
    this.opponent = null;
    this.paymentReceived = false;
  }

  display() {
    push();
    translate(this.x, this.y);
    fill(100, 150, 100);
    noStroke();
    beginShape();
    let radius = this.size / 2;
    let numPoints = 20;
    let noiseScale = 0.1;
    for (let i = 0; i < numPoints; i++) {
      let angle = map(i, 0, numPoints, 0, TWO_PI);
      let xOffset = cos(angle) * radius;
      let yOffset = sin(angle) * radius;
      let n = noise(xOffset * noiseScale, yOffset * noiseScale, frameCount * 0.02);
      let r = radius * (0.8 + 0.4 * n);
      vertex(cos(angle) * r, sin(angle) * r);
    }
    endShape(CLOSE);
    fill(255, 0, 0);
    ellipse(-this.size * 0.15, -this.size * 0.15, this.size * 0.1);
    ellipse(this.size * 0.15, -this.size * 0.15, this.size * 0.1);
    fill(50);
    beginShape();
    vertex(-this.size * 0.1, this.size * 0.1);
    vertex(0, this.size * 0.15);
    vertex(this.size * 0.1, this.size * 0.1);
    vertex(this.size * 0.05, this.size * 0.05);
    vertex(-this.size * 0.05, this.size * 0.05);
    endShape(CLOSE);
    
    if (this.state === 'attacking' || this.state === 'fighting_customer') {
      let elapsed = millis() - this.attackTimer;
      let alpha = map(elapsed, 0, this.attackDuration, 200, 0);
      let grow = map(elapsed, 0, this.attackDuration, 0, this.size * 0.5);
      noFill();
      stroke(255, 0, 0, alpha);
      strokeWeight(5);
      ellipse(0, 0, this.size + grow);
      push();
      translate(-this.size * 0.3, this.size * 0.2);
      rotate(PI / 6 + sin(millis() * 0.01) * PI / 12);
      rectMode(CORNER);
      fill(150);
      noStroke();
      rect(0, 0, 15, 3);
      fill(50);
      rect(-3, -2, 5, 7);
      pop();
    }

    if (this.state === 'fighting_customer' && this.opponent) {
      push();
      translate(0, -this.size - 15);
      fill(255);
      stroke(0);
      rectMode(CENTER);
      rect(0, 0, this.size, 10, 3);
      fill(0, 200, 0);
      rectMode(CORNER);
      let healthWidth = map(this.health, 0, ZOMBIE_HEALTH, 0, this.size);
      rect(-this.size / 2, -5, healthWidth, 10, 3);
      pop();
    }

    if (this.state === 'paying' && !this.paymentReceived) {
      push();
      translate(0, -this.size);
      fill(255);
      stroke(0);
      rectMode(CENTER);
      rect(0, 0, 60, 40, 5);
      fill(0);
      noStroke();
      textSize(14);
      textAlign(CENTER, CENTER);
      text('PAY!', 0, 0);
      pop();
    }
    pop();
  }

  move() {
    let d = dist(this.x, this.y, this.targetX, this.targetY);
    if (d > this.speed) {
      let angle = atan2(this.targetY - this.y, this.targetX - this.x);
      this.x += cos(angle) * this.speed;
      this.y += sin(angle) * this.speed;
    } else {
      this.x = this.targetX;
      this.y = this.targetY;
    }
  }

  setState(newState, targetX, targetY) {
    this.state = newState;
    this.targetX = targetX;
    this.targetY = targetY;
  }

  takeDamage(amount) {
    this.health -= amount;
    if (this.health <= 0) {
      this.state = 'leaving';
      if (this.opponent && this.opponent.state === CUSTOMER_STATES.FIGHTING_ZOMBIE) {
        this.opponent.setState(CUSTOMER_STATES.MOVING_TO_CASHIER, CASHIER_X + CASHIER_WIDTH / 2, CASHIER_Y + CUSTOMER_SIZE);
        this.opponent.opponent = null;
      }
    }
  }

  update() {
    if (this.state === 'leaving') {
      this.x += this.speed * 2;
      if (this.x > width + this.size) {
        zombies = zombies.filter(z => z !== this);
      }
      return;
    }

    if (this.targetCustomer === null && this.state === 'moving') {
      this.state = 'leaving';
      this.targetX = width + this.size * 2;
      this.targetY = this.y;
      return;
    }

    if (this.state === 'moving_to_customer_1v1' && this.opponent) {
      let d = dist(this.x, this.y, this.opponent.x, this.opponent.y);
      if (d > this.speed) {
        let angle = atan2(this.opponent.y - this.y, this.opponent.x - this.x);
        this.x += cos(angle) * this.speed;
        this.y += sin(angle) * this.speed;
      } else {
        this.x = this.opponent.x;
        this.y = this.opponent.y;
        this.state = 'fighting_customer';
        this.lastAttackTime = millis();
      }
      return;
    }

    if (this.state === 'fighting_customer' && this.opponent) {
      if (millis() - this.lastAttackTime > this.attackCooldown) {
        this.opponent.takeDamage(this.damage);
        this.lastAttackTime = millis();
      }
      return;
    }

    if (this.state === 'moving_to_cashier') {
      let d = dist(this.x, this.y, this.targetX, this.targetY);
      if (d > this.speed) {
        let angle = atan2(this.targetY - this.y, this.targetX - this.x);
        this.x += cos(angle) * this.speed;
        this.y += sin(angle) * this.speed;
      } else {
        this.x = this.targetX;
        this.y = this.targetY;
        this.state = 'paying';
      }
      return;
    }

    if (this.state === 'paying') {
      return;
    }

    if (this.targetCustomer) {
      let d = dist(this.x, this.y, this.targetCustomer.x, this.targetCustomer.y);
      if (d > this.speed) {
        let angle = atan2(this.targetCustomer.y - this.y, this.targetX - this.x);
        this.x += cos(angle) * this.speed;
        this.y += sin(angle) * this.speed;
      } else {
        this.state = 'attacking';
        this.attackTimer = millis();
      }
    } else if (this.state === 'attacking') {
      let elapsed = millis() - this.attackTimer;
      if (elapsed > this.attackDuration) {
        this.state = 'leaving';
      }
    }
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

function Zombie constructor constructor(x, y, targetCustomer) { this.x = x; this.y = y; this.targetCustomer = targetCustomer; ... }

Initializes a zombie at position (x, y) that targets the given customer, with slow speed (0.3), combat stats, and a 'moving' state

conditional 1v1 fight initiation if (this.state === 'moving_to_customer_1v1' && this.opponent) { ... }

If in 1v1 mode, moves toward opponent until close enough to fight

conditional Fighting logic if (this.state === 'fighting_customer' && this.opponent) { if (millis() - this.lastAttackTime > this.attackCooldown) { this.opponent.takeDamage(this.damage); this.lastAttackTime = millis(); } }

While fighting, attacks the opponent at intervals (attackCooldown), dealing damage each time

conditional Winner goes to cashier if (this.state === 'moving_to_cashier') { ... this.state = 'paying'; ... }

Winning zombies proceed to the cashier just like regular customers to collect payment

this.speed = 0.3;
Zombies move very slowly (0.3 pixels per frame), making them creepy and deliberate—they're slow but relentless.
this.health = ZOMBIE_HEALTH;
Zombies start with ZOMBIE_HEALTH (150 points), higher than customers (100), making them tougher in 1v1 fights.
if (this.state === 'moving_to_customer_1v1' && this.opponent) {
If this zombie was set up for a 1v1 fight, approach the opponent customer until close enough to fight.
if (millis() - this.lastAttackTime > this.attackCooldown) {
Checks if enough time has passed since the last attack. If so, attack again and reset the timer.
this.opponent.takeDamage(this.damage);
Calls the opponent's takeDamage() method, reducing their health by ZOMBIE_DAMAGE (40 points).

Shark class

The Shark class demonstrates a multi-state entity that changes appearance and behavior based on state: swimming with a realistic body, then becoming a cartoonish land shark with legs. The walking animation uses sin(frameCount) to create a smooth oscillation for leg movement—a common trick for procedural animation. The constrain() function keeps the shark within bounds, and the direction variable (±1) is flipped at edges to create bouncing behavior.

class Shark {
  constructor(x, y, targetCustomer) {
    this.x = x;
    this.y = y;
    this.targetCustomer = targetCustomer;
    this.targetX = targetCustomer.x;
    this.targetY = targetCustomer.y;
    this.state = 'moving';
    this.speed = 4;
    this.size = CUSTOMER_SIZE * 1.5;
    this.biteTimer = 0;
    this.biteDuration = 1200;
    this.mouthAngle = 0;
    this.mouthOpen = false;
    this.direction = 1;
    this.legOffset = 0;
    this.walkingTimer = 0;
    this.walkingDuration = 5000;
  }

  display() {
    push();
    translate(this.x, this.y);
    if (this.state === 'walking') {
      fill(100, 150, 200);
      noStroke();
      ellipse(0, 0, this.size * 1.5, this.size * 0.8);
      triangle(-this.size * 0.3, -this.size * 0.4, -this.size * 0.1, -this.size * 0.7, -this.size * 0.5, -this.size * 0.4);
      fill(255);
      stroke(50);
      strokeWeight(1);
      arc(-this.size * 0.5, this.size * 0.1, this.size * 0.6, this.size * 0.3, 0, PI);
      fill(0);
      ellipse(-this.size * 0.6, -this.size * 0.1, this.size * 0.1);
      fill(150, 200, 250);
      noStroke();
      rectMode(CENTER);
      rect(-this.size * 0.4, this.size * 0.4 + this.legOffset, this.size * 0.2, this.size * 0.4);
      ellipse(-this.size * 0.4, this.size * 0.6 + this.legOffset, this.size * 0.3, this.size * 0.2);
      rect(this.size * 0.1, this.size * 0.4 - this.legOffset, this.size * 0.2, this.size * 0.4);
      ellipse(this.size * 0.1, this.size * 0.6 - this.legOffset, this.size * 0.3, this.size * 0.2);
    } else {
      fill(100, 150, 200);
      noStroke();
      ellipse(0, 0, this.size * 2, this.size);
      triangle(this.size, 0, this.size * 1.5, -this.size * 0.4, this.size * 1.5, this.size * 0.4);
      triangle(-this.size * 0.5, 0, -this.size * 0.2, -this.size * 0.8, -this.size * 0.8, 0);
      triangle(0, this.size * 0.4, -this.size * 0.3, this.size, this.size * 0.1, this.size * 0.8);
      fill(0);
      ellipse(-this.size * 0.7, -this.size * 0.15, this.size * 0.15);
      fill(255);
      stroke(50);
      strokeWeight(1);
      push();
      translate(-this.size * 0.9, this.size * 0.2);
      rotate(this.mouthAngle);
      arc(0, 0, this.size * 0.5, this.size * 0.2, PI, TWO_PI);
      rotate(-2 * this.mouthAngle);
      arc(0, 0, this.size * 0.5, this.size * 0.2, 0, PI);
      pop();
    }
    pop();
  }

  update() {
    switch (this.state) {
      case 'moving':
        let d = dist(this.x, this.y, this.targetX, this.targetY);
        if (d > this.speed) {
          let angle = atan2(this.targetY - this.y, this.targetX - this.x);
          this.x += cos(angle) * this.speed;
          this.y += sin(angle) * this.speed;
          this.direction = (this.x < this.targetX) ? 1 : -1;
        } else {
          this.x = this.targetX;
          this.y = this.targetY;
          this.state = 'biting';
          this.biteTimer = millis();
          this.direction = (random() > 0.5) ? 1 : -1;
        }
        break;

      case 'biting':
        let elapsed = millis() - this.biteTimer;
        if (elapsed < this.biteDuration / 3) {
          this.mouthAngle = map(elapsed, 0, this.biteDuration / 3, 0, PI / 6);
        } else if (elapsed < this.biteDuration * 2 / 3) {
          this.mouthAngle = map(elapsed, this.biteDuration / 3, this.biteDuration * 2 / 3, PI / 6, 0);
        } else {
          this.mouthAngle = 0;
        }

        if (elapsed > this.biteDuration) {
          if (this.targetCustomer) {
            customers = customers.filter(c => c !== this.targetCustomer);
            this.targetCustomer = null;
          }
          this.state = 'walking';
          this.y = WATER_Y - this.size * 0.4;
          this.x = constrain(this.x, this.size / 2, width - (this.size * 1.5) / 2);
          this.walkingTimer = millis();
        }
        break;

      case 'walking':
        this.x += this.speed * this.direction * 0.5;
        this.legOffset = sin(frameCount * 0.1) * this.size * 0.05;

        if (this.x > width - (this.size * 1.5) / 2 || this.x < (this.size * 1.5) / 2) {
          this.direction *= -1;
        }

        if (millis() - this.walkingTimer > this.walkingDuration) {
          this.state = 'leaving_screen';
          this.targetX = width + this.size * 2;
          this.targetY = this.y;
        }
        break;

      case 'leaving_screen':
        this.x += this.speed * 0.75;
        if (this.x > width + this.size * 2) {
          sharks = sharks.filter(s => s !== this);
        }
        break;
    }
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

function Shark constructor constructor(x, y, targetCustomer) { ... this.state = 'moving'; ... }

Creates a shark at position (x, y) targeting a customer, starting in 'moving' state with slow walking and a biting animation

conditional Walking shark rendering if (this.state === 'walking') { ... ellipse(0, 0, this.size * 1.5, this.size * 0.8); ... rect(..., this.size * 0.4 + this.legOffset, ...); }

When walking, draws a cartoonish shark with legs and animated leg movement for comedic effect

conditional Swimming shark rendering } else { ... ellipse(0, 0, this.size * 2, this.size); ... triangle(this.size, 0, ...); }

When moving and biting, draws a realistic shark with tail, fins, and opening/closing mouth

conditional Moving toward target case 'moving': ... let d = dist(this.x, this.y, this.targetX, this.targetY); ...

Shark swims toward the customer at speed 4; once reached, transitions to biting

conditional Bite animation timing case 'biting': ... this.mouthAngle = map(elapsed, 0, this.biteDuration / 3, 0, PI / 6); ...

Animates the shark's mouth opening and closing over 1.2 seconds using elapsed time and map()

conditional Walking on shore case 'walking': ... this.x += this.speed * this.direction * 0.5; ... this.legOffset = sin(frameCount * 0.1) * this.size * 0.05;

After killing the customer, the shark walks along the shore in one direction, bouncing off edges, for 5 seconds before leaving

this.speed = 4;
Sharks move at 4 pixels per frame while swimming—twice as fast as customers, making them swift attackers.
this.size = CUSTOMER_SIZE * 1.5;
Sharks are 1.5x the size of customers, making them visually imposing.
this.walkingDuration = 5000;
After biting, the shark walks on shore for 5 seconds before leaving—plenty of time to see the chaos it caused.
if (this.state === 'walking') {
If the shark is in walking state, draw the cartoonish land shark with legs and silly gait.
this.y = WATER_Y - this.size * 0.4;
Positions the shark at the water line (WATER_Y) minus its height, placing it on the shore to walk.
this.x = constrain(this.x, this.size / 2, width - (this.size * 1.5) / 2);
Clamps the shark's x position to stay within canvas bounds so it doesn't walk off-screen.
this.legOffset = sin(frameCount * 0.1) * this.size * 0.05;
Uses sin(frameCount * 0.1) to create a smooth leg-walking animation where legs move up and down continuously.
if (this.x > width - (this.size * 1.5) / 2 || this.x < (this.size * 1.5) / 2) {
Checks if the shark hit the left or right edge; if so, reverse direction so it bounces back.

spawnCustomImagePerson()

spawnCustomImagePerson() demonstrates priority-based target selection using a cascade of find() calls. This is cleaner than a long if-else chain: you try the best target first, and if it fails, try the next best. The priority itself (waiting for airplane > waiting > eating > ...) reflects the game's design: it's funnier to interrupt someone about to leave than someone just arriving. Notice the console.log() calls—they're for debugging, helping the coder understand what's happening.

🔬 This code prioritizes targets: first waiting-for-airplane, then waiting, then eating. What if you reverse the order so eating customers are the priority? Image persons will target customers mid-meal.

  let targetCustomer = customers.find(c => c.state === CUSTOMER_STATES.WAITING_FOR_AIRPLANE);
  if (!targetCustomer) targetCustomer = customers.find(c => c.state === CUSTOMER_STATES.WAITING);
  if (!targetCustomer) targetCustomer = customers.find(c => c.state === CUSTOMER_STATES.EATING_SUSHI);
function spawnCustomImagePerson() {
  if (!customImage) {
    console.log("Please upload an image first!");
    return;
  }

  let targetCustomer = customers.find(c => c.state === CUSTOMER_STATES.WAITING_FOR_AIRPLANE);
  if (!targetCustomer) targetCustomer = customers.find(c => c.state === CUSTOMER_STATES.WAITING);
  if (!targetCustomer) targetCustomer = customers.find(c => c.state === CUSTOMER_STATES.EATING_SUSHI);
  if (!targetCustomer) targetCustomer = customers.find(c => c.state === CUSTOMER_STATES.MOVING_TO_TABLE);
  if (!targetCustomer) targetCustomer = customers.find(c => c.state === CUSTOMER_STATES.MOVING_TO_SUSHI);
  if (!targetCustomer) targetCustomer = customers.find(c => c.state === CUSTOMER_STATES.PAYING);

  let newImagePerson = new CustomImagePerson(-CUSTOMER_SIZE * 2, random(100, height - 100), customImage);
  if (targetCustomer) {
    newImagePerson.setTargetCustomer(targetCustomer);
    console.log("CustomImagePerson spawned, targeting a customer!");
  } else {
    newImagePerson.state = 'leaving';
    console.log("CustomImagePerson spawned, but no target customer found, so it's leaving.");
  }
  customImagePersons.push(newImagePerson);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Image uploaded check if (!customImage) { console.log("Please upload an image first!"); return; }

Validates that an image has been uploaded before spawning a person—prevents errors from trying to use a null image

conditional Find priority target let targetCustomer = customers.find(c => c.state === CUSTOMER_STATES.WAITING_FOR_AIRPLANE); if (!targetCustomer) targetCustomer = customers.find(c => c.state === CUSTOMER_STATES.WAITING); // ... (5 more priority levels)

Uses a priority chain to find the best target customer: waiting for departure first, then waiting, then eating, etc.

function-call Create custom image person let newImagePerson = new CustomImagePerson(-CUSTOMER_SIZE * 2, random(100, height - 100), customImage); if (targetCustomer) { newImagePerson.setTargetCustomer(targetCustomer);

Spawns a new CustomImagePerson off-screen left, and if a target exists, sets them as the target and adds to array

if (!customImage) {
Checks if customImage is null (falsy). If no image has been uploaded, warn the player and exit early with return.
let targetCustomer = customers.find(c => c.state === CUSTOMER_STATES.WAITING_FOR_AIRPLANE);
Uses Array.find() to search for the first customer in WAITING_FOR_AIRPLANE state (highest priority target).
if (!targetCustomer) targetCustomer = customers.find(c => c.state === CUSTOMER_STATES.WAITING);
If no high-priority target was found, searches for a WAITING customer (second priority), and so on down the chain.
let newImagePerson = new CustomImagePerson(-CUSTOMER_SIZE * 2, random(100, height - 100), customImage);
Creates a new CustomImagePerson off-screen left (negative x) at a random height, carrying the uploaded image.
newImagePerson.setTargetCustomer(targetCustomer);
If a target was found, sets the image person's target and state to 'moving_to_customer' so it approaches them.
customImagePersons.push(newImagePerson);
Adds the new image person to the global customImagePersons array so draw() will update and display it.

📦 Key Variables

customers array

Stores all Customer objects currently in the game. Updated in draw() loop.

let customers = [];
sushi array

Stores all Sushi objects (cooked and placed) waiting to be eaten.

let sushi = [];
ufos array

Stores all UFO objects that are currently beaming up or leaving.

let ufos = [];
zombies array

Stores all Zombie objects spawned by sick customers.

let zombies = [];
sharks array

Stores all Shark objects swimming in the water area.

let sharks = [];
helicopters array

Stores all gun-wielding Helicopter objects that shoot customers.

let helicopters = [];
totalMoney number

The player's accumulated money from customer payments (SUSHI_VALUE per customer).

let totalMoney = 0;
lastCustomerSpawnTime number

Milliseconds timestamp of when the last customer was spawned (used to time the next spawn).

let lastCustomerSpawnTime = 0;
CUSTOMER_SPAWN_INTERVAL number

Milliseconds between automatic customer arrivals (1500 = one customer every 1.5 seconds).

const CUSTOMER_SPAWN_INTERVAL = 1500;
CUSTOMER_SIZE number

Diameter of a customer circle in pixels (50) and base size for all entities.

const CUSTOMER_SIZE = 50;
SICKNESS_CHANCE number

Probability (0–1) that a customer gets sick after eating and triggers an attack event.

const SICKNESS_CHANCE = 0.2;
customImage p5.Image or null

Stores the image uploaded by the player via the file input, or null if no image has been uploaded.

let customImage;
isSpammingImages boolean

Toggle flag—true if the player is spawning custom image persons at rapid intervals.

let isSpammingImages = false;
backgroundStars array of objects

Array of 100 star objects with x, y, size1, size2, and points properties for decorative background rendering.

let backgroundStars = [];

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG Zombie.update() line 'this.x += cos(angle) * this.speed; this.y += sin(angle) * this.speed;'

A zombie moving to its targetCustomer uses this.targetX instead of this.targetCustomer.x, so it doesn't track a moving customer—it aims for where the customer was when the zombie spawned.

💡 Change `this.targetX - this.x` to `this.targetCustomer.x - this.x` so zombies dynamically track moving targets.

PERFORMANCE draw() function

The draw() function contains 20+ entity loops that all call update() and display() each frame, creating significant overhead when hundreds of entities exist simultaneously.

💡 Consider spatial partitioning (divide canvas into quadrants) or object pooling (reuse dead entities) to reduce per-frame computations when entity count grows very large.

BUG Customer.update() during 'eating' state

When a customer gets sick and a spider spawns at the table, the customer is immediately set to BITTEN_BY_SPIDER state, but the spider itself is spawned in a 'waiting' state (for the toilet). The spider will never reach 'moving' or 'biting' state because the customer isn't moving to the toilet.

💡 In the Spider.update(), check if targetCustomer.state === BITTEN_BY_SPIDER and immediately transition to 'biting' state, or skip the 'waiting' state altogether when the spider is spawned at a customer's current location.

STYLE Zombie class and Customer class

Both classes contain nearly identical combat code (takeDamage, health, health bars, knife drawing). This violates DRY (Don't Repeat Yourself) principle.

💡 Create a Combatant base class or mixin with shared combat logic, then have both Zombie and Customer inherit from it or include it.

FEATURE Airplane class

Airplanes can catch fire and crash, but there's no visual/audio feedback before it happens—the crash is sudden.

💡 Add a smoke trail before the crash, or a warning animation (shaking, sparks) so players see the danger coming.

BUG spawnWantedEntities() function

If a customer has a wanted level but no police cars/helicopters/SWAT cars spawn (unlikely but possible), the customer is stuck in WANTED state indefinitely.

💡 Add a timeout: after 10 seconds in WANTED state with no pursuers, transition the customer to LEAVING or remove them.

🔄 Code Flow

Code flow showing setup, draw, customer, mousePressed, drawstar, zombie, shark, spawncustomimage

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

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Canvas Creation] setup --> initial-customers-loop[Initial Customers Loop] setup --> background-stars-loop[Background Stars Loop] setup --> image-input-setup[Image Input Setup] setup --> button-setup[Button Setup] setup --> draw[draw loop] draw --> background-clear[Background Clear] draw --> customer-spawn[Customer Spawn] draw --> entity-loops[Entity Loops] draw --> empty-message[Empty Message] draw --> water-conditional[Water Conditional] draw --> poop-land-loop[Poop Land Loop] draw --> sushi-management[Sushi Management] draw --> star-drawing-loop[Star Drawing Loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click initial-customers-loop href "#sub-initial-customers-loop" click background-stars-loop href "#sub-background-stars-loop" click image-input-setup href "#sub-image-input-setup" click button-setup href "#sub-button-setup" click draw href "#fn-draw" click background-clear href "#sub-background-clear" click customer-spawn href "#sub-customer-spawn" click entity-loops href "#sub-entity-loops" click empty-message href "#sub-empty-message" click water-conditional href "#sub-water-conditional" click poop-land-loop href "#sub-poop-land-loop" click sushi-management href "#sub-sushi-management" click star-drawing-loop href "#sub-star-drawing-loop" entity-loops --> customer[Customer] entity-loops --> zombie[Zombie] entity-loops --> shark[Shark] entity-loops --> spawncustomimage[Spawn Custom Image] customer --> update-method[Update Method] customer --> display-method[Display Method] customer --> move-method[Move Method] customer --> setstate-method[Set State Method] customer --> takedamage-method[Take Damage Method] customer --> contains-method[Contains Method] click update-method href "#sub-update-method" click display-method href "#sub-display-method" click move-method href "#sub-move-method" click setstate-method href "#sub-setstate-method" click takedamage-method href "#sub-takedamage-method" click contains-method href "#sub-contains-method" zombie --> zombie-constructor[Zombie Constructor] zombie --> zombie-combat-check[Zombie Combat Check] zombie --> zombie-fighting-state[Zombie Fighting State] zombie --> zombie-winning-cashier[Zombie Winning Cashier] click zombie-constructor href "#sub-zombie-constructor" click zombie-combat-check href "#sub-zombie-combat-check" click zombie-fighting-state href "#sub-zombie-fighting-state" click zombie-winning-cashier href "#sub-zombie-winning-cashier" shark --> shark-constructor[Shark Constructor] shark --> shark-walking-display[Shark Walking Display] shark --> shark-swimming-display[Shark Swimming Display] shark --> shark-moving-state[Shark Moving State] shark --> shark-biting-animation[Shark Biting Animation] shark --> shark-walking-state[Shark Walking State] click shark-constructor href "#sub-shark-constructor" click shark-walking-display href "#sub-shark-walking-display" click shark-swimming-display href "#sub-shark-swimming-display" click shark-moving-state href "#sub-shark-moving-state" click shark-biting-animation href "#sub-shark-biting-animation" click shark-walking-state href "#sub-shark-walking-state" spawncustomimage --> image-check[Image Check] spawncustomimage --> target-priority[Target Priority] spawncustomimage --> spawn-person[Spawn Person] click image-check href "#sub-image-check" click target-priority href "#sub-target-priority" click spawn-person href "#sub-spawn-person"

❓ Frequently Asked Questions

What visual elements can I expect to see in the Good Boy yyyy p5.js sketch?

The sketch features colorful graphics including sushi stations, customers, and various interactive objects like a jail, toilet, and playground airplane.

How can I interact with the Good Boy yyyy creative coding sketch?

Users can interact with the sketch by controlling the movement of customers and influencing their outcomes, such as whether they get sick or encounter various events.

What creative coding concepts does the Good Boy yyyy sketch illustrate?

This sketch demonstrates concepts like randomness in event generation, collision detection for interactive objects, and the integration of multiple game mechanics.

Preview

Good boy yyyy - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Good boy yyyy - Code flow showing setup, draw, customer, mousePressed, drawstar, zombie, shark, spawncustomimage
Code Flow Diagram