Sketch 2026-03-17 02:11

This chaotic sushi restaurant simulator lets players click to cook sushi and manage a workflow of customers eating and paying. When customers get sick, random events spawn—poop creatures, zombies, police cars, or UFOs—that remove them from the game in spectacular fashion.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make customers always get sick — Every customer will trigger a chaotic event after eating instead of going to the cashier.
  2. Slow down the sushi station — Increase how fast customers arrive and create a backlog—tests your clicking speed.
  3. Make zombies much faster — Zombies move 5x faster toward customers, making them more menacing.
  4. Earn $50 per customer instead of $10 — Each payment is worth 5x more money, making the game feel more rewarding.
  5. Change sick customer detection to red outline
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a hilarious interactive restaurant simulator where players click to cook sushi and watch customers flow through eating and payment stages. When customers finish eating, there's a 20% chance they get sick, triggering one of four chaotic events: a poop creature waddles in to grab them, a zombie shambles over for an attack, a police car arrives to arrest them, or a UFO beams them away. The code uses object-oriented design with separate classes for Customers, UFOs, Zombies, Poops, PoliceCars, and Airplanes, all with their own state machines and animation timers.

The sketch is organized around a central draw loop that spawns customers, updates their state machines, and renders all entities. By studying it, you will learn how to build complex interactive simulations with multiple entity types, state management, distance-based collision detection, and timed animations. The customer workflow demonstrates a practical state machine pattern that scales to any multi-step process—ordering, cooking, serving, or checkout sequences.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas and initializes empty arrays for sushi, customers, and event creatures (UFOs, zombies, poops, police cars, airplanes).
  2. Every frame, draw() clears the canvas and draws four game areas: sushi station, dining table, cashier, and jail. It displays the current money total and renders all sushi, customers, and entities in order.
  3. New customers spawn automatically every 3 seconds from the left side of the screen and enter the WAITING state. When cooked sushi is available, they transition to MOVING_TO_SUSHI and walk to grab it.
  4. After grabbing sushi, customers move to the dining table, place their sushi, and eat for 2 seconds. After eating, they have a 20% chance to get sick and trigger a random event (poop, zombie, police, or UFO). The other 80% proceed to the cashier to pay.
  5. Players click on the sushi station to cook sushi (which instantly adds it to the array) or click on a customer at the cashier to receive payment. Paid customers enter WAITING_FOR_AIRPLANE and wait for an airplane to pick them up.
  6. Event creatures (UFOs, zombies, poops, police cars) spawn with the sick customer as their target, move toward them over multiple frames, perform an action (beam, attack, grab, or arrest), and then leave the screen. Each has its own animation logic and timing.

🎓 Concepts You'll Learn

Object-oriented design with classesState machines and state transitionsAnimation timing and millis()Distance-based collision detectionEntity management with arrays and filteringInteractive UI with mousePressed()2D vector movement and atan2()

📝 Code Breakdown

setup()

setup() runs once when the sketch loads. It initializes the canvas and the spawn timer. The try-catch block prevents errors from crashing the game silently—if something goes wrong, the error message displays on screen.

function setup() {
  try {
    createCanvas(windowWidth, windowHeight);
    lastCustomerSpawnTime = millis();
    console.log("Setup completed successfully.");
  } catch (error) {
    console.error("Error in setup():", error);
    textSize(24);
    textAlign(CENTER, CENTER);
    text("Error in Setup!", width/2, height/2);
  }
}
Line-by-line explanation (2 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, allowing the game to scale to any screen size.
lastCustomerSpawnTime = millis();
Records the current time in milliseconds so the spawn timer has a starting point. millis() returns elapsed time since the sketch started.

draw()

draw() is the heartbeat of the sketch—it runs 60 times per second by default. Every frame, it clears the screen, updates all game logic, and redraws everything. The order matters: game areas render first (as backgrounds), then sushi and customers (as foreground entities). The spawn timer uses millis() to track elapsed time, a common pattern for spawning, animations, and timed events. Notice how the function wraps everything in a try-catch block—if any entity's update() throws an error, it won't crash the entire game.

function draw() {
  try {
    background(220);

    // --- Draw Game Areas ---
    stroke(0);
    fill(200);
    rectMode(CORNER);
    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);
    rect(TABLE_X, TABLE_Y, TABLE_WIDTH, TABLE_HEIGHT, 10);
    fill(0);

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

    fill(100);
    rect(JAIL_X, JAIL_Y, JAIL_WIDTH, JAIL_HEIGHT, 10);
    fill(0);

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

    // --- Draw Poop Land ---
    for (let customer of customers) {
      if (customer.sick && customer.poopLand) {
        push();
        translate(customer.x, customer.y + CUSTOMER_SIZE / 2);
        noStroke();
        fill(102, 51, 0);
        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 ---
    if (millis() - lastCustomerSpawnTime > CUSTOMER_SPAWN_INTERVAL) {
      customers.push(new Customer(-CUSTOMER_SIZE, height / 2));
      lastCustomerSpawnTime = millis();
    }

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

    // --- 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 ---
    for (let airplane of airplanes) {
      airplane.update();
      airplane.display();
    }

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

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

🔧 Subcomponents:

conditional Customer Spawn Timer if (millis() - lastCustomerSpawnTime > CUSTOMER_SPAWN_INTERVAL) {

Checks if enough time has passed to spawn a new customer, preventing spawning every single frame

for-loop Sushi Cleanup for (let i = sushi.length - 1; i >= 0; i--) {

Loops backward through sushi to safely remove eaten pieces without skipping elements

for-loop Entity Update Loop for (let customer of customers) {

Updates each customer's state machine and redraws them every frame

background(220);
Clears the canvas with light gray every frame, removing previous drawings and preventing visual trails.
rectMode(CORNER);
Sets rectangle drawing mode so the top-left corner is the origin—required for the game area layouts to position correctly.
text(`Money: $${totalMoney}`, 20, 20);
Displays the current total money earned in the top-left corner using template literal syntax to insert the variable value.
if (millis() - lastCustomerSpawnTime > CUSTOMER_SPAWN_INTERVAL) {
Compares elapsed time since the last spawn to the spawn interval. When enough time passes, a new customer spawns.
customers.push(new Customer(-CUSTOMER_SIZE, height / 2));
Creates a new Customer object off-screen on the left (negative x) and adds it to the customers array. The game immediately starts managing it.
for (let customer of customers) {
Uses a for-of loop to iterate through every customer. This cleaner syntax is perfect for when you don't need the index.
customer.update();
Calls the customer's state machine to transition states and move toward targets—the core of the customer workflow.
customer.display();
Draws the customer as a circle with eyes and mouth, plus any held sushi or status bubbles (SICK, PAYING, etc).

mousePressed()

mousePressed() is p5.js's built-in callback that fires once each time the user clicks. It receives the global mouseX and mouseY variables automatically. This function demonstrates two interaction patterns: circular hit testing with dist() for the sushi station, and object method testing with customer.contains() for precise customer clicks. By separating the payment line into its own loop, the code allows clicking any paying customer (not just the first one found), making the game feel more responsive and fair.

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

  // 2. Receive Payment from Customer
  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);
        airplanes.push(new Airplane(-180, random(50, height - 50), customer));
      }
    }
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Sushi Cooking Check if (d < SUSHI_STATION_SIZE / 2) {

Tests if the click is inside the circular sushi station—if so, cook a sushi

for-loop Payment Collection for (let customer of customers) {

Searches for a customer in the PAYING state at the clicked position to collect money

let d = dist(mouseX, mouseY, SUSHI_STATION_X, SUSHI_STATION_Y);
Calculates the distance from the click position to the sushi station center using the dist() function—needed to test if the click is inside the station.
if (d < SUSHI_STATION_SIZE / 2) {
Checks if the distance is less than the station's radius. If so, the click hit the station and sushi should be cooked.
let newSushi = new Sushi(SUSHI_STATION_X, SUSHI_STATION_Y);
Creates a new Sushi object at the station's position. The sushi is uncooked by default.
newSushi.cooked = true;
Sets the cooked flag to true so the Sushi.display() method knows to render it as a finished sushi (blue and orange).
sushi.push(newSushi);
Adds the new sushi to the global sushi array. The draw() loop will now update and display it every frame.
customers.push(new Customer(-CUSTOMER_SIZE, random(100, height - 100)));
Also spawns a random new customer when sushi is cooked—keeping supply and demand balanced and rewarding players with more action.
if (customer.state === CUSTOMER_STATES.PAYING && !customer.paymentReceived) {
Checks two conditions: the customer is waiting to pay AND hasn't been paid yet. Prevents paying the same customer twice.
if (customer.contains(mouseX, mouseY)) {
Calls the customer's contains() method to test if the click position is inside their body. If true, the click is on this specific customer.
totalMoney += SUSHI_VALUE;
Adds SUSHI_VALUE (10 dollars) to the player's total. This is the only source of income in the game.
customer.setState(CUSTOMER_STATES.WAITING_FOR_AIRPLANE, customer.x, customer.y);
Transitions the customer to WAITING_FOR_AIRPLANE state so they stop trying to leave and wait for pickup.
airplanes.push(new Airplane(-180, random(50, height - 50), customer));
Spawns a new airplane off-screen left to pick up the paid customer. The airplane targets this specific customer.

Customer()

The Customer class is the main actor in this game. Its update() method contains a state machine—a switch statement that changes the customer's behavior based on their current state. Each state transitions to the next when a condition is met (like arriving at the target or eating long enough). The move() function decouples movement logic from state transitions, making it easy to pause movement for sick customers. The sickness roll at the end of eating demonstrates weighted randomness: each event type gets a probability slice, and they're mutually exclusive by construction (poop 30%, zombie 30%, police 20%, UFO 20%).

🔬 This is the vector movement logic. The 'if (d > CUSTOMER_SPEED)' check snaps the customer exactly to the target when they get really close. What happens if you remove that check and let them overshoot the target repeatedly?

    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;
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.paymentReceived = false;
    this.leavingOffset = 0;
    this.color = color(random(150, 255), random(150, 255), random(150, 255));
    this.sick = false;
    this.poopLand = false;
  }

  display() {
    push();
    translate(this.x, this.y);

    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);

    if (this.hasSushi && !this.hasSushi.placed && !this.hasSushi.eaten) {
      push();
      translate(CUSTOMER_SIZE / 2, -CUSTOMER_SIZE / 4);
      noStroke();
      fill(100, 100, 200);
      ellipse(0, 0, SUSHI_SIZE * 0.7);
      fill(255, 200, 150);
      rectMode(CENTER);
      rect(0, 0, SUSHI_SIZE * 0.5, SUSHI_SIZE * 0.3);
      pop();
    }

    if (this.state === CUSTOMER_STATES.PAYING && !this.paymentReceived) {
      push();
      translate(0, -CUSTOMER_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();
    }

    if (this.state === CUSTOMER_STATES.SICK) {
      push();
      translate(0, -CUSTOMER_SIZE);
      fill(255, 50, 50);
      stroke(0);
      rectMode(CENTER);
      rect(0, 0, 60, 40, 5);
      fill(255);
      noStroke();
      textSize(14);
      textAlign(CENTER, CENTER);
      text('SICK!', 0, 0);
      pop();
    }
    
    if (this.state === CUSTOMER_STATES.WAITING_FOR_AIRPLANE) {
      push();
      translate(0, -CUSTOMER_SIZE);
      fill(255, 255, 0);
      stroke(0);
      rectMode(CENTER);
      rect(0, 0, 80, 40, 5);
      fill(0);
      noStroke();
      textSize(14);
      textAlign(CENTER, CENTER);
      text('AIRPLANE!', 0, 0);
      pop();
    }

    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;
  }

  update() {
    if (this.state === CUSTOMER_STATES.SICK || this.state === CUSTOMER_STATES.WAITING_FOR_AIRPLANE) {
      return;
    }

    this.move();

    let d;
    let availableSushi;

    switch (this.state) {
      case CUSTOMER_STATES.WAITING:
        availableSushi = sushi.find(s => s.cooked && !s.grabbed);
        if (availableSushi) {
          this.setState(CUSTOMER_STATES.MOVING_TO_SUSHI, SUSHI_STATION_X, SUSHI_STATION_Y + CUSTOMER_SIZE);
        }
        break;

      case CUSTOMER_STATES.MOVING_TO_SUSHI:
        d = dist(this.x, this.y, this.targetX, this.targetY);
        if (d < 5) {
          this.setState(CUSTOMER_STATES.GRABBING_SUSHI, this.x, this.y);
        }
        break;

      case CUSTOMER_STATES.GRABBING_SUSHI:
        availableSushi = sushi.find(s => s.cooked && !s.grabbed);
        if (availableSushi) {
          availableSushi.grabbed = true;
          availableSushi.owner = this;
          this.hasSushi = availableSushi;
          this.setState(CUSTOMER_STATES.MOVING_TO_TABLE, TABLE_X + TABLE_WIDTH / 2, TABLE_Y + TABLE_HEIGHT / 2 + CUSTOMER_SIZE / 2);
        } else {
          this.setState(CUSTOMER_STATES.WAITING, this.x, this.y);
        }
        break;

      case CUSTOMER_STATES.MOVING_TO_TABLE:
        d = dist(this.x, this.y, this.targetX, this.targetY);
        if (d < 5) {
          this.setState(CUSTOMER_STATES.PLACING_SUSHI, this.x, this.y);
        }
        break;

      case CUSTOMER_STATES.PLACING_SUSHI:
        if (this.hasSushi) {
          this.hasSushi.placed = true;
          this.hasSushi.x = this.x;
          this.hasSushi.y = this.y - CUSTOMER_SIZE / 2;
          this.setState(CUSTOMER_STATES.EATING_SUSHI, this.x, this.y);
          this.eatingTimer = millis();
        }
        break;

      case CUSTOMER_STATES.EATING_SUSHI:
        if (millis() - this.eatingTimer > 2000) {
          if (this.hasSushi) {
            this.hasSushi.eaten = true;
            this.hasSushi = null;
          }
          if (random() < SICKNESS_CHANCE) {
            this.sick = true;
            this.poopLand = true;
            this.setState(CUSTOMER_STATES.SICK, this.x, this.y);
            let spawnRoll = random();
            if (spawnRoll < POOP_SPAWN_CHANCE) {
              poops.push(new Poop(-100, random(100, height - 100), this));
            } else if (spawnRoll < POOP_SPAWN_CHANCE + ZOMBIE_SPAWN_CHANCE) {
              zombies.push(new Zombie(-100, random(100, height - 100), this));
            } else if (spawnRoll < POOP_SPAWN_CHANCE + ZOMBIE_SPAWN_CHANCE + POLICE_CAR_SPAWN_CHANCE) {
              policeCars.push(new PoliceCar(-100, random(100, height - 100), this));
            } else {
              ufos.push(new UFO(-100, random(100, height - 100), this));
            }
          } else {
            this.setState(CUSTOMER_STATES.MOVING_TO_CASHIER, CASHIER_X + CASHIER_WIDTH / 2, CASHIER_Y + CUSTOMER_SIZE);
          }
        }
        break;

      case CUSTOMER_STATES.MOVING_TO_CASHIER:
        d = dist(this.x, this.y, this.targetX, this.targetY);
        if (d < 5) {
          this.setState(CUSTOMER_STATES.PAYING, this.x, this.y);
        }
        break;

      case CUSTOMER_STATES.PAYING:
        break;

      case CUSTOMER_STATES.WAITING_FOR_AIRPLANE:
        break;

      case CUSTOMER_STATES.LEAVING:
        this.leavingOffset += CUSTOMER_SPEED;
        this.x = this.targetX + this.leavingOffset;
        if (this.x > width + CUSTOMER_SIZE) {
          customers = customers.filter(c => c !== this);
        }
        break;
    }
  }

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

🔧 Subcomponents:

function Constructor constructor(x, y)

Initializes a new customer with position, state, and visual properties

function display() display() { ... }

Draws the customer as a colored circle with eyes, mouth, and status bubbles

function move() move() { ... }

Updates position each frame to move toward the target using vector math

switch-case State Machine switch (this.state)

Central logic that transitions customers through the workflow (WAITING → GRABBING → EATING → PAYING → LEAVING)

conditional Sickness Event Trigger if (random() < SICKNESS_CHANCE)

After eating, decides whether the customer gets sick (20% chance) and spawns an appropriate event creature

this.color = color(random(150, 255), random(150, 255), random(150, 255));
Generates a random pastel color for this customer by picking random R, G, B values between 150-255. Each customer gets a unique color for visual distinction.
this.move();
Called at the start of update() to move the customer one frame's distance toward their target. This separation keeps movement logic reusable.
let d = dist(this.x, this.y, this.targetX, this.targetY);
Calculates the distance to the current target. Used to test whether the customer has arrived (d < 5 means close enough).
let angle = atan2(this.targetY - this.y, this.targetX - this.x);
Calculates the angle toward the target using atan2(), which returns an angle in radians pointing from current position to target.
this.x += cos(angle) * CUSTOMER_SPEED;
Moves the x position by multiplying the cosine of the angle by the speed. cos gives the horizontal component of movement.
this.y += sin(angle) * CUSTOMER_SPEED;
Moves the y position using sine, which gives the vertical component. Together, cos/sin move diagonally at any angle.
if (this.state === CUSTOMER_STATES.SICK || this.state === CUSTOMER_STATES.WAITING_FOR_AIRPLANE) {
Sick customers and those waiting for airplanes skip movement—they stay still until an event creature or airplane removes them.
availableSushi = sushi.find(s => s.cooked && !s.grabbed);
Searches the global sushi array for the first sushi that is cooked AND not already grabbed. Returns null if none exists.
if (millis() - this.eatingTimer > 2000) {
Checks if 2000 milliseconds (2 seconds) have elapsed since eating started. When true, the customer finishes eating and moves on.
let spawnRoll = random();
Generates a random number 0-1 that will be used to decide which event creature to spawn if the customer gets sick.
if (spawnRoll < POOP_SPAWN_CHANCE) {
If the roll is less than 0.3 (30%), spawn a poop. Otherwise, check other conditions for zombie, police, or UFO.

Sushi()

The Sushi class is simple but clever. Each sushi tracks its state (cooked, grabbed, placed, eaten) and displays itself differently depending on that state. The cooked flag is set by mousePressed(), the grabbed and placed flags are managed by the Customer state machine, and eaten is used by draw() to remove sushi from the array. The contains() method mirrors the one in Customer, allowing precise click detection if you ever wanted to let players interact with individual sushi.

class Sushi {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.cooked = false;
    this.grabbed = false;
    this.placed = false;
    this.eaten = false;
    this.eatingTimer = 0;
    this.owner = null;
  }

  display() {
    push();
    translate(this.x, this.y);
    noStroke();
    
    if (this.grabbed) {
      // If grabbed, display relative to owner
    } else if (this.placed) {
      fill(100, 100, 200, 150);
      ellipse(0, 0, SUSHI_SIZE * 0.8);
      fill(255, 200, 150, 150);
      rectMode(CENTER);
      rect(0, 0, SUSHI_SIZE * 0.6, SUSHI_SIZE * 0.4);
    } else {
      if (this.cooked) {
        fill(100, 100, 200);
        ellipse(0, 0, SUSHI_SIZE);
        fill(255, 200, 150);
        rectMode(CENTER);
        rect(0, 0, SUSHI_SIZE * 0.8, SUSHI_SIZE * 0.4);
      } else {
        fill(150);
        ellipse(0, 0, SUSHI_SIZE);
      }
    }
    
    pop();
  }

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

🔧 Subcomponents:

conditional Sushi Display States if (this.placed) { ... } else if (this.cooked) { ... }

Changes the sushi's appearance based on its state: placed on table (semi-transparent), cooked at station (bright), or uncooked (gray)

this.cooked = false;
Sushi starts uncooked. The mousePressed() function sets cooked = true when the player clicks the sushi station.
this.grabbed = false;
Tracks whether a customer has claimed this sushi. When true, it becomes part of the customer's workflow.
this.owner = null;
Stores a reference to the customer who owns this sushi. Helps track which sushi belongs to which customer.
if (this.grabbed) { }
When grabbed, sushi rendering is delegated to the customer's display() method (rendered next to the customer).
fill(100, 100, 200, 150);
Light blue with 150 alpha (semi-transparent). The 4th number is transparency, making placed sushi stand out as different.
else if (this.cooked) {
At the station, cooked sushi displays at full opacity with bright blue and orange colors.
fill(150);
Uncooked sushi is just a gray circle—simple and visually distinct from the flashy blue-orange cooked sushi.

UFO()

The UFO class demonstrates three-phase animation: moving toward a target, executing an action (beaming) with timed animation, and leaving. The beamHeight uses map() to smoothly interpolate between values over time—a fundamental technique for all animations. The map() function takes an input range (elapsed time) and maps it to an output range (beam height), making animations independent of framerate. The filter() pattern for removing the customer is elegant: filter() keeps only customers where the condition is true, so c !== this.targetCustomer removes the one we're abducting.

class UFO {
  constructor(x, y, targetCustomer) {
    this.x = x;
    this.y = y;
    this.targetCustomer = targetCustomer;
    this.targetX = targetCustomer.x;
    this.targetY = targetCustomer.y - CUSTOMER_SIZE;
    this.state = 'moving';
    this.speed = 3;
    this.beamTimer = 0;
    this.beamDuration = 1500;
    this.beamHeight = 0;
    this.beamMaxHeight = CUSTOMER_SIZE * 2;
  }

  display() {
    push();
    translate(this.x, this.y);

    fill(120, 120, 120);
    stroke(50);
    strokeWeight(2);
    ellipse(0, 0, 80, 40);
    arc(0, 0, 60, 30, PI, TWO_PI);
    fill(180, 220, 255);
    stroke(50);
    ellipse(0, -10, 20, 10);

    fill(255, 200, 50);
    noStroke();
    ellipse(-25, 5, 8);
    ellipse(0, 5, 8);
    ellipse(25, 5, 8);

    if (this.state === 'beaming') {
      noStroke();
      fill(100, 255, 255, 100);
      rectMode(CENTER);
      rect(0, this.beamHeight / 2, 40, this.beamHeight);

      fill(100, 255, 255, 50);
      arc(0, 0, 60, this.beamMaxHeight * 2, 0, PI);
    }

    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;
        } else {
          this.x = this.targetX;
          this.y = this.targetY;
          this.state = 'beaming';
          this.beamTimer = millis();
        }
        break;

      case 'beaming':
        let elapsed = millis() - this.beamTimer;
        if (elapsed < this.beamDuration / 2) {
          this.beamHeight = map(elapsed, 0, this.beamDuration / 2, 0, this.beamMaxHeight);
        } else if (elapsed < this.beamDuration) {
          this.beamHeight = map(elapsed, this.beamDuration / 2, this.beamDuration, this.beamMaxHeight, 0);
        } else {
          if (this.targetCustomer) {
            customers = customers.filter(c => c !== this.targetCustomer);
            this.targetCustomer = null;
          }
          this.state = 'leaving';
          this.targetX = width + 100;
          this.targetY = this.y;
        }
        break;

      case 'leaving':
        this.x += this.speed;
        if (this.x > width + 100) {
          ufos = ufos.filter(u => u !== this);
        }
        break;
    }
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

switch-case Moving State case 'moving':

UFO travels toward the sick customer; when close enough, transitions to beaming

switch-case Beaming State case 'beaming':

Animates the beam growing and shrinking over 1.5 seconds, then removes the customer and leaves

calculation Beam Height Mapping this.beamHeight = map(elapsed, 0, this.beamDuration / 2, 0, this.beamMaxHeight);

Uses map() to grow the beam smoothly from 0 to max height over the first half of the beam duration

this.targetCustomer = targetCustomer;
Stores a reference to the sick customer this UFO is hunting. The UFO targets this specific customer, not a random one.
this.targetY = targetCustomer.y - CUSTOMER_SIZE;
UFO hovers slightly above the customer (one customer size up). This makes the beam animation feel more natural.
let angle = atan2(this.targetY - this.y, this.targetX - this.x);
Calculates the angle from the UFO to its target using atan2, just like customers do. This is the standard way to aim at something.
this.x += cos(angle) * this.speed;
Moves the UFO horizontally using cosine. The speed (3 pixels/frame) controls how fast it flies.
this.beamTimer = millis();
Records the time the UFO arrived, starting the timer for the beam animation.
let elapsed = millis() - this.beamTimer;
Calculates how much time has passed since beaming started. Used to animate the beam height.
this.beamHeight = map(elapsed, 0, this.beamDuration / 2, 0, this.beamMaxHeight);
Maps elapsed time (0 to 750ms) to beam height (0 to CUSTOMER_SIZE * 2). At halfway through the animation, the beam is at full height.
customers = customers.filter(c => c !== this.targetCustomer);
Removes the customer from the array using filter(). This is the only line that actually deletes them—the UFO abduction!
this.targetX = width + 100;
After picking up the customer, the UFO flies off-screen to the right. This position is far enough that it won't appear again.

Zombie()

The Zombie class is nearly identical in structure to the UFO, but with key differences: slower speed for scarier pursuit, an irregular blob body using Perlin noise, red eyes for threat signaling, and a direct attack animation instead of a beam. The targeting logic is tight: zombies are born with a specific sick customer target and hunt them relentlessly. The attack state is a brief 500ms animation before the customer is removed—short enough to feel snappy, long enough to be satisfying. The expanding red ring using map() to control both alpha and size is a reusable animation pattern you'll see throughout the code.

🔬 Zombies move at this.speed (1.5 pixels/frame) toward their target. What happens if you change this.speed to 0.5? To 5? How does the zombie's menace change?

    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.targetCustomer.x - 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 = 1.5;
    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;
  }

  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') {
      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);
    }

    pop();
  }

  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.state === 'attacking') {
      let elapsed = millis() - this.attackTimer;
      if (elapsed > this.attackDuration) {
        if (this.targetCustomer) {
          customers = customers.filter(c => c !== this.targetCustomer);
          this.targetCustomer = null;
        }
        this.state = 'leaving';
      }
      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.targetCustomer.x - this.x);
        this.x += cos(angle) * this.speed;
        this.y += sin(angle) * this.speed;
      } else {
        this.state = 'attacking';
        this.attackTimer = millis();
      }
    } else {
      this.state = 'leaving';
    }
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Irregular Body Shape beginShape(); ... endShape(CLOSE);

Uses Perlin noise to create a jagged, organic blob shape that looks zombie-like

conditional Target Following Logic if (this.targetCustomer) {

Zombie always pursues its assigned sick customer; moves toward them using distance and angle math

animation Attack Ring Animation if (this.state === 'attacking') {

Draws an expanding red ring that fades out during the 500ms attack, creating an impact effect

this.speed = 1.5;
Zombies move slower than customers (2) and UFOs (3), making them seem lumbering and menacing.
let n = noise(xOffset * noiseScale, yOffset * noiseScale, frameCount * 0.02);
Perlin noise creates natural-looking randomness. The frameCount * 0.02 makes the noise evolve over time, creating a subtle pulsating effect.
fill(255, 0, 0);
Red eyes make the zombie instantly recognizable as a threat—color coding for a chaotic game.
if (this.targetCustomer) {
Zombie always has a target (set in the constructor), so it always pursues. This check handles edge cases if the target somehow disappears.
if (d > this.speed) {
Only moves if the distance is greater than the speed. When d <= speed, the zombie snaps to the target and attacks.
let alpha = map(elapsed, 0, this.attackDuration, 200, 0);
Maps time (0 to 500ms) to transparency (200 to 0). At the start of the attack, the ring is opaque; by the end, it's invisible.
let grow = map(elapsed, 0, this.attackDuration, 0, this.size * 0.5);
Maps time to size growth, so the attack ring expands and fades simultaneously, creating a dynamic impact effect.
this.x += this.speed * 2;
When leaving, zombies speed up (2x) so they exit the screen faster. Prevents slow zombie escapes.

PoliceCar()

The PoliceCar class is the most complex entity because it has a five-state workflow: drive to criminal, arrest (animation), drive to jail, announce completion, and leave. The flashing lights are a delightful detail—the ternary operator makes the color toggle concise. The arrested customer display while in transit adds narrative clarity: you can see the criminal in the back of the police car being hauled off to jail. The jail location is specified by JAIL_X and JAIL_Y constants, which draw() renders as a gray rectangle. Unlike the UFO or zombie, the police car doesn't delete the customer immediately—it transports them first, giving the arrest a complete story arc.

🔬 This moves the police car toward the customer. The state machine has five states: moving_to_customer, arresting, moving_to_jail, leaving_jail, and leaving_screen. What happens if you change this.state = 'arresting' to this.state = 'moving_to_jail' to skip the arrest animation?

      case 'moving_to_customer':
        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;
class PoliceCar {
  constructor(x, y, targetCustomer) {
    this.x = x;
    this.y = y;
    this.targetCustomer = targetCustomer;
    this.targetX = targetCustomer.x;
    this.targetY = targetCustomer.y;
    this.jailTargetX = JAIL_X + JAIL_WIDTH / 2;
    this.jailTargetY = JAIL_Y + JAIL_HEIGHT / 2;
    this.state = 'moving_to_customer';
    this.speed = 4;
    this.arrestTimer = 0;
    this.arrestDuration = 1000;
    this.arrestedCustomer = null;
    this.lightFlashTimer = 0;
    this.lightFlashInterval = 200;
    this.isRedLight = true;
  }

  display() {
    push();
    translate(this.x, this.y);

    fill(50, 50, 200);
    stroke(0);
    strokeWeight(2);
    rectMode(CENTER);
    rect(0, 0, 100, 40, 5);

    fill(180, 220, 255);
    rect(-15, 0, 60, 30, 3);

    fill(0);
    ellipse(-35, 20, 20, 20);
    ellipse(35, 20, 20, 20);

    if (this.state === 'moving_to_customer' || this.state === 'moving_to_jail') {
      if (millis() - this.lightFlashTimer > this.lightFlashInterval) {
        this.isRedLight = !this.isRedLight;
        this.lightFlashTimer = millis();
      }
      fill(this.isRedLight ? 255 : 0, 0, this.isRedLight ? 0 : 255);
      noStroke();
      rect(-20, -25, 15, 8);
      rect(20, -25, 15, 8);
    } else {
      fill(0, 0, 255);
      noStroke();
      rect(-20, -25, 15, 8);
      rect(20, -25, 15, 8);
    }

    fill(255);
    noStroke();
    textSize(10);
    textAlign(CENTER, CENTER);
    text('POLICE', 0, 0);

    if (this.arrestedCustomer) {
      push();
      translate(-20, 0);
      fill(this.arrestedCustomer.color);
      noStroke();
      ellipse(0, 0, CUSTOMER_SIZE * 0.7);
      pop();
    }

    if (this.state === 'arresting') {
      let elapsed = millis() - this.arrestTimer;
      let alpha = map(elapsed, 0, this.arrestDuration, 200, 0);
      let grow = map(elapsed, 0, this.arrestDuration, 0, CUSTOMER_SIZE * 0.5);
      noFill();
      stroke(255, 0, 0, alpha);
      strokeWeight(5);
      ellipse(0, 0, CUSTOMER_SIZE + grow);
    }

    pop();
  }

  update() {
    switch (this.state) {
      case 'moving_to_customer':
        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 = 'arresting';
          this.arrestTimer = millis();
        }
        break;

      case 'arresting':
        let elapsed = millis() - this.arrestTimer;
        if (elapsed > this.arrestDuration) {
          if (this.targetCustomer) {
            customers = customers.filter(c => c !== this.targetCustomer);
            this.arrestedCustomer = this.targetCustomer;
            this.targetCustomer = null;
          }
          this.state = 'moving_to_jail';
          this.targetX = this.jailTargetX;
          this.targetY = this.jailTargetY;
        }
        break;

      case 'moving_to_jail':
        let djail = dist(this.x, this.y, this.jailTargetX, this.jailTargetY);
        if (djail > this.speed) {
          let angle = atan2(this.jailTargetY - this.y, this.jailTargetX - this.x);
          this.x += cos(angle) * this.speed;
          this.y += sin(angle) * this.speed;
        } else {
          this.x = this.jailTargetX;
          this.y = this.jailTargetY;
          this.state = 'leaving_jail';
          this.arrestedCustomer = null;
        }
        break;

      case 'leaving_jail':
        this.targetX = width + 100;
        this.targetY = this.y;
        this.state = 'leaving_screen';
        break;

      case 'leaving_screen':
        this.x += this.speed;
        if (this.x > width + 100) {
          policeCars = policeCars.filter(pc => pc !== this);
        }
        break;
    }
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Flashing Police Lights if (millis() - this.lightFlashTimer > this.lightFlashInterval)

Toggles between red and blue lights every 200ms when the police car is in motion

switch-case Multi-State Movement switch (this.state)

Police car transitions through four states: moving to customer, arresting (animation), moving to jail, leaving the scene

conditional Arrested Customer Display if (this.arrestedCustomer) {

Displays the arrested customer as a small circle in the police car window during transport to jail

this.speed = 4;
Police cars are the fastest entity (UFO 3, customers 2, zombies 1.5, poop 2.5)—they need to respond quickly!
this.jailTargetX = JAIL_X + JAIL_WIDTH / 2;
Pre-calculates the center of the jail. The police car will drive here after arresting the customer.
if (millis() - this.lightFlashTimer > this.lightFlashInterval) {
Every 200ms, toggles the light color. This timer pattern is reusable for any flashing effect (beacon, heartbeat, etc).
fill(this.isRedLight ? 255 : 0, 0, this.isRedLight ? 0 : 255);
Ternary operator: if isRedLight is true, R=255,G=0,B=0 (red); if false, R=0,G=0,B=255 (blue). Compact and elegant.
if (this.arrestedCustomer) {
While transporting a customer to jail, render them as a small circle in the car window. A nice visual detail!
fill(this.arrestedCustomer.color);
Uses the customer's original color so you can recognize who got arrested. Adds visual continuity.
this.arrestedCustomer = this.targetCustomer;
Stores the arrested customer so the car can display them while driving to jail. targetCustomer is set to null after arrest.
this.arrestedCustomer = null;
When the car reaches jail, it clears the reference—the customer has been processed and booked!

Poop()

The Poop class is the simplest of the event creatures but the most visceral. It uses Perlin noise for an irregular blob body and sin() for smooth pulsation—these are fundamental animation techniques. The pulsation is a visual loop: elapsed time feeds into sin(), which oscillates smoothly, creating a breathing effect. Unlike the zombie or police car, poop has only three states (moving, grabbing, leaving), making it straightforward. The grabbed customer is removed after just 1 second, faster than other creatures, giving poop a sense of urgency and humor. The brown color (#663300) and simple animations make it instantly recognizable as comedic chaos.

class Poop {
  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 = 2.5;
    this.grabTimer = 0;
    this.grabDuration = 1000;
    this.sizeOffset = 0;
  }

  display() {
    push();
    translate(this.x, this.y);

    noStroke();
    fill(102, 51, 0);

    beginShape();
    let radius = CUSTOMER_SIZE * 0.9 + this.sizeOffset;
    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);

    fill(255);
    ellipse(-CUSTOMER_SIZE * 0.2, -CUSTOMER_SIZE * 0.2, CUSTOMER_SIZE * 0.15);
    ellipse(CUSTOMER_SIZE * 0.2, -CUSTOMER_SIZE * 0.2, CUSTOMER_SIZE * 0.15);
    fill(0);
    ellipse(-CUSTOMER_SIZE * 0.2, -CUSTOMER_SIZE * 0.2, CUSTOMER_SIZE * 0.05);
    ellipse(CUSTOMER_SIZE * 0.2, -CUSTOMER_SIZE * 0.2, CUSTOMER_SIZE * 0.05);

    arc(0, CUSTOMER_SIZE * 0.1, CUSTOMER_SIZE * 0.3, CUSTOMER_SIZE * 0.2, 0, PI);

    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;
        } else {
          this.x = this.targetX;
          this.y = this.targetY;
          this.state = 'grabbing';
          this.grabTimer = millis();
        }
        break;

      case 'grabbing':
        let elapsed = millis() - this.grabTimer;
        this.sizeOffset = sin(elapsed * 0.01) * CUSTOMER_SIZE * 0.1;

        if (elapsed > this.grabDuration) {
          if (this.targetCustomer) {
            customers = customers.filter(c => c !== this.targetCustomer);
            this.targetCustomer = null;
          }
          this.state = 'leaving';
          this.targetX = width + 100;
          this.targetY = this.y;
          this.sizeOffset = 0;
        }
        break;

      case 'leaving':
        this.x += this.speed;
        if (this.x > width + 100) {
          poops = poops.filter(p => p !== this);
        }
        break;
    }
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Blob Body with Noise let n = noise(xOffset * noiseScale, yOffset * noiseScale, frameCount * 0.01);

Uses Perlin noise to create an irregular, lumpy poop creature shape

animation Pulsating Size Animation this.sizeOffset = sin(elapsed * 0.01) * CUSTOMER_SIZE * 0.1;

Makes the poop creature pulse/breathe while grabbing the customer

this.speed = 2.5;
Poop moves at 2.5 pixels/frame, slightly faster than customers but slower than UFOs and police cars.
this.sizeOffset = sin(elapsed * 0.01) * CUSTOMER_SIZE * 0.1;
Uses sin() to create a smooth, repeating pulsation. sin() oscillates between -1 and 1, perfect for breathing/pulsing effects. The 0.01 controls frequency.
let radius = CUSTOMER_SIZE * 0.9 + this.sizeOffset;
Base radius plus the pulsating sizeOffset creates a creature that grows and shrinks smoothly while grabbing.
let n = noise(xOffset * noiseScale, yOffset * noiseScale, frameCount * 0.01);
Perlin noise creates organic blob irregularities. The frameCount * 0.01 makes the blob's shape slightly change over time.
if (elapsed > this.grabDuration) {
After 1000ms (1 second) of grabbing, the customer is removed. Shorter than UFO or zombie, making poop feel quick and aggressive.

Airplane()

The Airplane class brings closure to the game loop: paid customers get picked up by airplanes and fly away. The animated propeller is a joy—it rotates using a simple angle increment, demonstrating that you don't need complex trig, just incremental rotation each frame. Airplanes have three states like poop (moving, picking up, leaving), but the pickup phase is twice as long (2 seconds) to suggest the plane is boarding passengers. The translate(-90, 0) then rotate() pattern is essential p5.js knowledge: you must translate to the rotation point BEFORE rotating, otherwise the rotation happens around the origin. The speed of 3.5 ensures airplanes arrive faster than other event creatures, rewarding patient players who let their customers survive the chaos.

class Airplane {
  constructor(x, y, targetCustomer) {
    this.x = x;
    this.y = y;
    this.targetCustomer = targetCustomer;
    this.targetX = targetCustomer.x;
    this.targetY = targetCustomer.y - CUSTOMER_SIZE;
    this.state = 'moving';
    this.speed = 3.5;
    this.pickupTimer = 0;
    this.pickupDuration = 2000;
    this.propellerAngle = 0;
  }

  display() {
    push();
    translate(this.x, this.y);

    fill(200, 200, 255);
    stroke(50);
    strokeWeight(2);
    ellipse(0, 0, 180, 54);

    rectMode(CENTER);
    rect(0, -36, 144, 18);
    rect(0, 36, 144, 18);

    triangle(90, -27, 108, -27, 108, -9);

    push();
    translate(-90, 0);
    rotate(this.propellerAngle);
    fill(100);
    noStroke();
    rect(0, -9, 18, 18);
    rect(0, 9, 18, 18);
    pop();

    pop();
  }

  update() {
    this.propellerAngle += 0.5;

    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;
        } else {
          this.x = this.targetX;
          this.y = this.targetY;
          this.state = 'picking_up';
          this.pickupTimer = millis();
        }
        break;

      case 'picking_up':
        let elapsed = millis() - this.pickupTimer;
        if (elapsed > this.pickupDuration) {
          if (this.targetCustomer) {
            customers = customers.filter(c => c !== this.targetCustomer);
            this.targetCustomer = null;
          }
          this.state = 'leaving';
          this.targetX = width + 180;
          this.targetY = this.y;
        }
        break;

      case 'leaving':
        this.x += this.speed;
        if (this.x > width + 180) {
          airplanes = airplanes.filter(a => a !== this);
        }
        break;
    }
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

animation Rotating Propeller this.propellerAngle += 0.5;

Increments the rotation angle each frame, creating a spinning propeller effect

this.speed = 3.5;
Airplanes are the fastest entity, even faster than police cars (4), because they need to arrive quickly to pick up customers waiting to leave.
this.propellerAngle += 0.5;
Increments the angle by 0.5 radians each frame. The rotate() function later uses this to spin the propeller blades around their center.
translate(-90, 0);
Moves the origin to the front of the plane where the propeller is. Now rotating will spin the propeller around that point, not the plane's center.
rotate(this.propellerAngle);
Rotates the coordinate system by propellerAngle radians. All shapes drawn after this (the propeller blades) will be rotated.
this.pickupDuration = 2000;
Airplanes take 2 seconds to pick up a customer (longer than other creatures). This gives the animation time to feel complete.

📦 Key Variables

SUSHI_STATION_X, SUSHI_STATION_Y number

Defines the x, y position of the sushi cooking station on the canvas

const SUSHI_STATION_X = 150;
const SUSHI_STATION_Y = 150;
SUSHI_VALUE number

How many dollars the player earns when a customer pays at the cashier

const SUSHI_VALUE = 10;
SICKNESS_CHANCE number

Probability (0-1) that a customer gets sick after eating. 0.2 = 20% chance

const SICKNESS_CHANCE = 0.2;
CUSTOMER_SPEED number

Pixels per frame that customers move toward their target

const CUSTOMER_SPEED = 2;
CUSTOMER_SPAWN_INTERVAL number

Milliseconds to wait between spawning new customers (automatic, not click-based)

const CUSTOMER_SPAWN_INTERVAL = 3000;
sushi array

Global array storing all Sushi objects currently on the canvas

let sushi = [];
customers array

Global array storing all Customer objects in the game

let customers = [];
ufos array

Global array storing all UFO objects that are abducting sick customers

let ufos = [];
zombies array

Global array storing all Zombie objects that are attacking sick customers

let zombies = [];
poops array

Global array storing all Poop objects that are grabbing sick customers

let poops = [];
policeCars array

Global array storing all PoliceCar objects that are arresting sick customers

let policeCars = [];
airplanes array

Global array storing all Airplane objects picking up paid customers

let airplanes = [];
totalMoney number

Running total of dollars earned from customers paying at the cashier

let totalMoney = 0;
lastCustomerSpawnTime number

Timestamp (in milliseconds) of when the last customer was automatically spawned

let lastCustomerSpawnTime = 0;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG Customer.update() state machine

When a customer is GRABBING_SUSHI but no sushi is available, they transition back to WAITING at the same position. This can cause them to loop endlessly if sushi keeps disappearing before they grab it.

💡 Consider moving the customer back to the left edge of the screen or to a waiting area instead of having them wait at the sushi station. This prevents blocking other customers.

PERFORMANCE draw() function - entities render every frame

Sick customers and customers waiting for airplanes don't move but still call move() in update(). More importantly, every frame iterates through all arrays (sushi, customers, UFOs, zombies, poops, police cars, airplanes) even if they're empty.

💡 The move() early-return already optimizes sick customers. For larger games, consider spatial partitioning or quadtrees to cull off-screen entities. Current structure is fine for this game's scope.

FEATURE Customer workflow

Customers have no timeout for waiting at the cashier. A player could ignore paying customers forever, and they'd stay on screen indefinitely.

💡 Add a patience timer to customers in PAYING state. After 10-15 seconds, they get angry and leave without paying, or transition to a different state. This adds urgency and realism.

STYLE Global variables and constants

Some constants are grouped by type (e.g., SUSHI_STATION_X/Y, TABLE_X/Y) but CUSTOMER_STATES is mixed in. The file is 700+ lines with classes, global state, and p5.js functions all at the same level.

💡 Consider organizing the file as: (1) Constants, (2) Classes (Sushi, Customer, UFO, etc), (3) Global state, (4) p5.js functions. Add comments like '=== Event Creatures ===' above Poop, Zombie, UFO, PoliceCar, Airplane to group them visually.

BUG PoliceCar.update() - arrest animation

If a sick customer is removed by another event (e.g., UFO) before the police car arrives, the police car will crash when trying to filter the customers array because the customer reference becomes stale.

💡 Add a check before filtering: `if (this.targetCustomer && customers.includes(this.targetCustomer))` to ensure the customer still exists before trying to remove them.

FEATURE Game difficulty

Chaotic event creatures (UFO, zombie, poop, police) spawn immediately when a customer gets sick. A new player has no warning and no chance to react.

💡 Add a short delay (e.g., 1-2 seconds) after a customer becomes sick before spawning the event creature. Display the SICK bubble for longer so players see it coming. Or spawn the creature off-screen and give it time to enter the scene, making the approach more dramatic.

🔄 Code Flow

Code flow showing setup, draw, mousepressed, customer, sushi, ufo, zombie, policecar, poop, airplane

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> spawn-customers[Customer Spawn Timer] draw --> remove-eaten-sushi[Sushi Cleanup] draw --> update-and-display[Entity Update Loop] spawn-customers -->|conditional| draw remove-eaten-sushi -->|for-loop| draw update-and-display -->|for-loop| draw update-and-display --> customer.update-state-machine[update-state-machine] update-and-display --> customer.display[display] update-and-display --> customer.move[move] draw --> mousepressed[mousePressed] mousepressed --> cook-sushi[cook-sushi] mousepressed --> receive-payment[receive-payment] cook-sushi -->|conditional| draw receive-payment -->|for-loop| draw customer --> constructor[constructor] customer --> update-state-machine[update-state-machine] customer --> sickness-roll[sickness-roll] sushi --> sushi-display[sushi-display] ufo --> ufo-moving[ufo-moving] ufo --> ufo-beaming[ufo-beaming] ufo-beaming --> beam-animation[beam-animation] zombie --> zombie-body[zombie-body] zombie --> zombie-targeting[zombie-targeting] zombie --> attack-effect[attack-effect] policecar --> light-flash[light-flash] policecar --> arrest-transport[arrest-transport] policecar --> arrested-display[arrested-display] poop --> poop-body[poop-body] poop --> pulsate-effect[pulsate-effect] airplane --> propeller-animation[propeller-animation] click setup href "#fn-setup" click draw href "#fn-draw" click spawn-customers href "#sub-customer-spawn-timer" click remove-eaten-sushi href "#sub-sushi-cleanup" click update-and-display href "#sub-entity-update-loop" click cook-sushi href "#sub-sushi-cooking-check" click receive-payment href "#sub-receive-payment" click constructor href "#sub-constructor" click display href "#sub-display" click move href "#sub-move" click update-state-machine href "#sub-state-machine" click sickness-roll href "#sub-sickness-event-trigger" click sushi-display href "#sub-sushi-display-states" click ufo-moving href "#sub-ufo-moving-state" click ufo-beaming href "#sub-ufo-beaming-state" click beam-animation href "#sub-beam-height-mapping" click zombie-body href "#sub-zombie-body" click zombie-targeting href "#sub-zombie-targeting-logic" click attack-effect href "#sub-attack-ring-animation" click light-flash href "#sub-flashing-police-lights" click arrest-transport href "#sub-multi-state-movement" click arrested-display href "#sub-arrested-customer-display" click poop-body href "#sub-blob-body-with-noise" click pulsate-effect href "#sub-pulsating-size-animation" click propeller-animation href "#sub-rotating-propeller"

❓ Frequently Asked Questions

What visual elements are featured in the p5.js sushi restaurant sketch?

The sketch visually depicts a sushi restaurant environment with sushi stations, tables, customers, and various dynamic elements like zombies, police cars, and UFOs.

How can users interact with the sushi restaurant sketch created in p5.js?

Users can interact with the sketch by managing the flow of customers, ensuring they eat sushi and navigate through the restaurant while dealing with unexpected events like sickness and arrests.

What creative coding concepts are showcased in this p5.js sketch?

This sketch demonstrates concepts such as state management for customer behaviors, random event generation, and the interaction of multiple entities within a simulated environment.

Preview

Sketch 2026-03-17 02:11 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-03-17 02:11 - Code flow showing setup, draw, mousepressed, customer, sushi, ufo, zombie, policecar, poop, airplane
Code Flow Diagram