Gooooooooooooood

This interactive sketch creates a darkly humorous emoji printer that spawns animated characters who wait for you to choose their fate—abduction by UFO, airplane crash, shark attack, or ambulance rescue. Each choice triggers a unique animated sequence, leaving behind graves with the departed's emoji and name.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change the water color to green — Make the water look more like a swamp by adjusting the fill color used for the water rectangle.
  2. Make people walk twice as fast — Double the range of random walk speeds so people sprint to the printer instead of stroll.
  3. Make UFOs red by default — Change the random 50/50 chance to always spawn red (killer) UFOs.
  4. Make graves permanent (no fade-out) — Disable the time-based removal of graves so they accumulate on-screen as a graveyard.
  5. Speed up airplane crashes — Make airplanes crash faster by reducing how long they fly with a person.
  6. Make the printer bigger — Increase the size of the printer body so it dominates the canvas.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch is a macabre interactive animation where you print emoji characters, watch them walk across the canvas, and then decide their destiny by clicking buttons that summon UFOs, airplanes, sharks, or ambulances. The visual result is a parade of absurd character deaths, each leaving behind a named grave. It combines array management, state machines, interactive buttons, collision detection, and multi-object animation—all working in concert to create a complex interactive narrative.

The code is organized into seven character classes (Paper, Person, UFO, Airplane, Shark, Ambulance, Grave), a global setup() and draw() loop, and helper functions that manage printing, transport dispatch, and button positioning. By studying it, you'll learn how to build a multi-layered interactive system where objects interact across different states, how to queue and manage user input, and how to choreograph elaborate animation sequences with proper cleanup and removal.

⚙️ How It Works

  1. When you load the sketch, setup() creates a responsive canvas with an emoji input field and a Print button at the top. A printer visual is drawn above the water level (which occupies the bottom 150 pixels of the canvas).
  2. Type an emoji and click Print (or press Enter). This triggers createPaperAction(), which spawns a Paper object that prints downward, lands at the water level, and summons a new Person who walks toward the printer.
  3. When the person reaches the paper, they grab it and enter a 'waitingForTransportSelection' state. Four transport buttons appear above them: Red UFO, Blue UFO, Airplane, and Sharks.
  4. Clicking any button calls dispatchTransport(), which either reuses an idle transport object or creates a new one, assigns it a target person, and changes their state accordingly. The person is removed from normal display and becomes 'captured' or 'waiting'.
  5. Each transport type animates differently: Red UFOs cause instant escape (grave + skull animation), Blue UFOs abduct the person (grave created on UFO exit), Airplanes pick people up, fly for a moment, then crash (grave + crash fire), and Sharks kill at the water level (grave + blood + ambulance spawned).
  6. Ambulances arrive and pick up graves created by shark kills, driving off-screen with them. Graves display the emoji and person's name and either fade away or get picked up. Throughout, the draw() loop continuously updates all active objects, removes completed ones, and repositions buttons for the next waiting person.

🎓 Concepts You'll Learn

Object-oriented programming with ES6 classesState machines and state transitionsArray management and iteration with safe removalInteractive buttons and callbacksAnimation loops and frame-based timingCollision detection and distance-based interactionMulti-object coordination and queueing

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the ideal place to initialize the canvas, create UI elements, and set starting values. All buttons are hidden and off-screen initially because they only make sense when a person is waiting.

function setup() {
  createCanvas(windowWidth, windowHeight);

  // Create the emoji input field
  emojiInput = createInput('📄'); // Default emoji
  emojiInput.id('emojiInput');
  emojiInput.position(10, 10);

  // Create the "Print" button
  printButton = createButton('Print');
  printButton.id('printButton');
  printButton.position(emojiInput.x + emojiInput.width + 10, emojiInput.y);
  printButton.mousePressed(triggerPrint);
  printButton.style('z-index', '100'); // Ensure button is above canvas

  // Create the transport selection buttons (initially hidden and off-screen)
  spawnRedUFOButton = createButton('Red UFO');
  spawnRedUFOButton.id('spawnRedUFOButton');
  spawnRedUFOButton.position(-999, -999); // Off-screen initially
  spawnRedUFOButton.mousePressed(() => dispatchTransport('redUFO'));
  spawnRedUFOButton.hide();

  spawnBlueUFOButton = createButton('Blue UFO');
  spawnBlueUFOButton.id('spawnBlueUFOButton');
  spawnBlueUFOButton.position(-999, -999); // Off-screen initially
  spawnBlueUFOButton.mousePressed(() => dispatchTransport('blueUFO'));
  spawnBlueUFOButton.hide();

  spawnAirplaneButton = createButton('Airplane');
  spawnAirplaneButton.id('spawnAirplaneButton');
  spawnAirplaneButton.position(-999, -999); // Off-screen initially
  spawnAirplaneButton.mousePressed(() => dispatchTransport('airplane'));
  spawnAirplaneButton.hide();

  // NEW: Create the Sharks button (initially hidden and off-screen)
  spawnSharksButton = createButton('Sharks');
  spawnSharksButton.id('spawnSharksButton');
  spawnSharksButton.position(-999, -999); // Off-screen initially
  spawnSharksButton.mousePressed(() => dispatchTransport('sharks'));
  spawnSharksButton.hide();

  // Set printer position
  printerX = width / 2;
  printerY = height - WATER_LEVEL - 50; // Position printer above the water level
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Canvas Setup createCanvas(windowWidth, windowHeight);

Creates a full-screen, responsive canvas that fills the entire browser window

initialization Emoji Input Field emojiInput = createInput('📄');

Creates a text input where users type or paste emojis to print

initialization Print Button printButton.mousePressed(triggerPrint);

Attaches the triggerPrint callback so clicking the button prints an emoji

initialization Transport Selection Buttons spawnRedUFOButton.mousePressed(() => dispatchTransport('redUFO'));

Creates four hidden buttons that will appear when a person is waiting; each dispatches a different transport type

calculation Printer Position printerX = width / 2; printerY = height - WATER_LEVEL - 50;

Places the printer in the center horizontally and above the water level

createCanvas(windowWidth, windowHeight);
Creates a canvas that matches the full browser window size. Using window dimensions makes the sketch responsive.
emojiInput = createInput('📄');
Creates an input field with a default emoji (📄) that users can change by typing or pasting
emojiInput.position(10, 10);
Positions the input field at 10 pixels from the top-left, placing it in the corner
printButton.mousePressed(triggerPrint);
Attaches a callback function so that whenever the Print button is clicked, triggerPrint() runs
spawnRedUFOButton.position(-999, -999);
Places the button way off-screen so it's hidden but ready to reposition when needed
spawnRedUFOButton.hide();
Hides the button element entirely until a person is waiting for transport selection
printerX = width / 2;
Sets the printer's horizontal position to the canvas center
printerY = height - WATER_LEVEL - 50;
Positions the printer 50 pixels above the water level so papers have room to print without landing in water

draw()

draw() runs 60 times per second (by default). It's a continuous loop where you clear the canvas, redraw all dynamic objects, and call their update methods. The backward loop pattern (counting down instead of up) is essential: if you splice an array while iterating forward, you skip elements. By going backward, removing an element doesn't affect indices you haven't processed yet. This sketch choreographs six types of objects all in draw(), proving that a single loop can coordinate complex interactions.

🔬 This loop handles every paper. What happens if you comment out the line `if (!paper.grabbed) { paper.display(); }`? Where will the paper appear to disappear?

  // Update and display papers
  for (let i = papers.length - 1; i >= 0; i--) {
    let paper = papers[i];
    paper.update();
    // Only display paper if it's not being grabbed by a person or already captured by ufo
    if (!paper.grabbed) {
      paper.display();
    }
    // Remove papers that have been fully processed (grabbed and person abducted)
    // This condition might need more specific logic if papers could exist without a person
    // For now, if a person is removed, their paper will eventually be removed here.
    if (paper.grabbed && paper.x > width + 100) {
      papers.splice(i, 1);
    }
  }

🔬 Each person calls update() then display(). What happens if you remove the `person.display();` line? Will people still move and interact, or will they vanish from the screen while still affecting the logic?

  // Update and display people
  // Iterate backwards to safely remove people
  for (let i = people.length - 1; i >= 0; i--) {
    let person = people[i];
    person.update();
    person.display();
function draw() {
  background(220);

  // NEW: Draw water
  push();
  noStroke();
  fill(50, 150, 200, 150); // Semi-transparent blue
  rect(0, height - WATER_LEVEL, width, WATER_LEVEL);
  pop();

  // Display printer
  push();
  fill(100);
  noStroke();
  rectMode(CENTER);
  rect(printerX, printerY, 150, 100, 10); // Printer body
  fill(80);
  rect(printerX, printerY - 40, 120, 20, 5); // Paper tray
  pop();

  // Update and display papers
  for (let i = papers.length - 1; i >= 0; i--) {
    let paper = papers[i];
    paper.update();
    // Only display paper if it's not being grabbed by a person or already captured by ufo
    if (!paper.grabbed) {
      paper.display();
    }
    // Remove papers that have been fully processed (grabbed and person abducted)
    // This condition might need more specific logic if papers could exist without a person
    // For now, if a person is removed, their paper will eventually be removed here.
    if (paper.grabbed && paper.x > width + 100) {
      papers.splice(i, 1);
    }
  }

  // Update and display people
  // Iterate backwards to safely remove people
  for (let i = people.length - 1; i >= 0; i--) {
    let person = people[i];
    person.update();
    person.display();
    // Remove people who have been abducted or are no longer part of the scene
    if (person.state === 'removed') {
      people.splice(i, 1);
    }
  }

  // Update and display ufos
  // Iterate backwards to safely remove ufos
  for (let i = ufos.length - 1; i >= 0; i--) {
    let ufo = ufos[i];
    ufo.update();
    ufo.display();
    // Remove ufos that have flown off
    if (ufo.state === 'removed') {
      ufos.splice(i, 1);
    }
  }

  // Update and display airplanes
  for (let i = airplanes.length - 1; i >= 0; i--) {
    let airplane = airplanes[i];
    airplane.update();
    airplane.display();
    if (airplane.state === 'removed') {
      airplanes.splice(i, 1);
    }
  }

  // NEW: Update and display sharks
  for (let i = sharks.length - 1; i >= 0; i--) {
    let shark = sharks[i];
    shark.update();
    shark.display();
    if (shark.state === 'removed') {
      sharks.splice(i, 1);
    }
  }

  // NEW: Update and display ambulances
  for (let i = ambulances.length - 1; i >= 0; i--) {
    let ambulance = ambulances[i];
    ambulance.update();
    ambulance.display();
    if (ambulance.state === 'removed') {
      ambulances.splice(i, 1);
    }
  }

  // Update and display graves
  // Iterate backwards to safely remove graves
  for (let i = graves.length - 1; i >= 0; i--) {
    let grave = graves[i];
    grave.update();
    grave.display();
    if (grave.state === 'removed') {
      graves.splice(i, 1);
    }
  }

  // Always update transport selection buttons in draw loop to ensure they follow the first waiting person
  updateTransportSelectionButtons();
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Background Clear background(220);

Clears the canvas each frame with a light gray color, preventing trails

shape-drawing Water Level Rectangle rect(0, height - WATER_LEVEL, width, WATER_LEVEL);

Draws a semi-transparent blue rectangle at the bottom representing water

shape-drawing Printer Visual rect(printerX, printerY, 150, 100, 10);

Draws the printer body and paper tray as rounded rectangles

for-loop Papers Update and Display for (let i = papers.length - 1; i >= 0; i--) { ... papers.splice(i, 1); }

Iterates through all papers, updates their positions, displays them, and removes ones that have floated off-screen

for-loop People Update and Display for (let i = people.length - 1; i >= 0; i--) { ... people.splice(i, 1); }

Updates each person's state machine, displays them, and removes ones marked as 'removed'

for-loop UFOs Update and Display for (let i = ufos.length - 1; i >= 0; i--) { ... ufos.splice(i, 1); }

Manages all UFO animations, abduction visuals, and cleanup

for-loop Airplanes Update and Display for (let i = airplanes.length - 1; i >= 0; i--) { ... airplanes.splice(i, 1); }

Handles airplane pickup, flight, and crash sequences

for-loop Sharks Update and Display for (let i = sharks.length - 1; i >= 0; i--) { ... sharks.splice(i, 1); }

Manages shark movement, killing animations, and cleanup

for-loop Ambulances Update and Display for (let i = ambulances.length - 1; i >= 0; i--) { ... ambulances.splice(i, 1); }

Handles ambulance arrival, grave pickup, and departure

for-loop Graves Update and Display for (let i = graves.length - 1; i >= 0; i--) { ... graves.splice(i, 1); }

Updates grave state (fading or being picked up) and removes them when done

function-call Button Position Update updateTransportSelectionButtons();

Reposition buttons to follow the first waiting person, or hide them all if no one is waiting

background(220);
Clears the screen with light gray (220 is medium gray in 0–255 scale). Without this, you'd see trails of all previous frames.
fill(50, 150, 200, 150);
Sets the fill color to a semi-transparent blue (RGBA: 50 red, 150 green, 200 blue, 150 alpha). The last number makes it see-through.
rect(0, height - WATER_LEVEL, width, WATER_LEVEL);
Draws a rectangle starting at the bottom of the canvas (height - WATER_LEVEL) and stretching down (WATER_LEVEL pixels tall) and across the full width—this is the water.
for (let i = papers.length - 1; i >= 0; i--) {
Iterates through papers backwards (from last to first). Backwards iteration is crucial: when you remove an item, the indices of items you haven't looped to yet stay valid.
paper.update();
Calls the paper's update method, which moves it downward if it's still printing
if (!paper.grabbed) { paper.display(); }
Only draws the paper if it hasn't been grabbed by a person—once grabbed, the person's display method draws it instead
if (paper.grabbed && paper.x > width + 100) { papers.splice(i, 1); }
Removes the paper from the array once it's been grabbed and has drifted far off-screen to the right
if (person.state === 'removed') { people.splice(i, 1); }
Deletes people who are no longer needed (abducted, crashed, killed, etc.). This keeps memory clean.
updateTransportSelectionButtons();
Called every frame to ensure buttons stay positioned above the first waiting person, or hides all buttons if the queue is empty

Paper()

The Paper class is the simplest in this sketch: it moves downward (printing), stops at the water level (groundY), and displays itself as a white rectangle with a centered emoji. Once a person grabs it, the paper's display stops and the person's display method draws it instead. This handoff (from paper.display() to person.display()) is a common pattern for managing ownership of visual objects.

class Paper {
  constructor(x, y, emoji) {
    this.x = x;
    this.y = y;
    this.width = 100;
    this.height = 150;
    this.emoji = emoji;
    this.printing = true;
    this.grabbed = false;
    this.printSpeed = 5;
    this.groundY = height - WATER_LEVEL; // Paper lands at this Y position (water level)
  }

  update() {
    if (this.printing) {
      this.y += this.printSpeed;
      if (this.y >= this.groundY) {
        this.y = this.groundY;
        this.printing = false;
      }
    }
  }

  display() {
    push();
    rectMode(CENTER);
    fill(255);
    stroke(0);
    rect(this.x, this.y, this.width, this.height, 5);
    fill(0);
    textAlign(CENTER, CENTER);
    textSize(48);
    text(this.emoji, this.x, this.y);
    pop();
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

initialization Paper Constructor constructor(x, y, emoji) { this.x = x; this.y = y; ... }

Initializes a paper with position, dimensions, emoji, and state flags

conditional Printing Animation if (this.printing) { this.y += this.printSpeed; }

While printing is true, moves the paper downward each frame until it hits the ground

constructor(x, y, emoji) {
Defines the constructor that runs when a new Paper is created, taking three arguments: x position, y position, and the emoji string
this.x = x;
Stores the x position passed in; papers always start at the printer's x but will move with their owner later
this.printing = true;
A flag that tracks whether the paper is still falling from the printer. Once it lands, this becomes false.
this.grabbed = false;
A flag set to true when a person grabs the paper, preventing the paper from being drawn independently
this.groundY = height - WATER_LEVEL;
Calculates where the paper should stop falling—at the water level. This is stored so the paper knows its landing spot.
if (this.printing) { this.y += this.printSpeed; }
While printing, increases y (moves downward) by printSpeed pixels each frame
if (this.y >= this.groundY) { this.printing = false; }
Once the paper reaches the ground level (groundY), sets printing to false so it stops moving
rectMode(CENTER);
Makes the next rectangle (the paper) draw from its center point rather than top-left
text(this.emoji, this.x, this.y);
Draws the emoji centered on the paper at the paper's current x and y position

Person()

The Person class is the heart of the sketch's state machine. Each person has nine possible states, and switching between them drives the entire narrative: print→walk→grab→wait for selection→transport (or escape). The clever part is how captured states ('waitingToBeCaptured', 'waitingToBePickedUpByAirplane', 'waitingToBeKilledByShark') prevent the person from updating themselves; instead, their captor's update method controls their animation, creating a sense of loss of autonomy that's both mechanically elegant and darkly comic.

🔬 This code moves the person horizontally toward the printer. What happens if you change the condition from `abs(targetX - this.x) > this.walkSpeed` to just `true`? Will the person walk past the printer or keep walking forever?

      case 'movingToPaper':
        if (this.targetPaper) {
          let targetX = printerX; // Move towards the printer's X
          let targetY = height - WATER_LEVEL; // Stand on the ground (water level)

          let direction = (targetX > this.x) ? 1 : -1;
          if (abs(targetX - this.x) > this.walkSpeed) {
            this.x += this.walkSpeed * direction;
          }
class Person {
  constructor(id) {
    this.id = id;
    this.x = random(-200, -100); // Start off-screen left
    this.y = height - WATER_LEVEL; // Ground level (water level) for people
    this.width = 40;
    this.height = 80;
    this.color = color(random(100, 200), random(100, 200), random(100, 200));
    this.hasPaper = false;
    this.targetPaper = null;
    this.walkSpeed = random(2, 4); // Person's speed
    // Added 'waitingToBeCaptured', 'walkingOff', 'waitingToBePickedUpByAirplane', and 'waitingToBeKilledByShark' states
    this.state = 'idle'; // 'idle', 'movingToPaper', 'grabbing', 'waitingForTransportSelection', 'waitingToBeCaptured', 'waitingToBePickedUpByAirplane', 'waitingToBeKilledByShark', 'walkingOff', 'removed'
    this.grabCounter = 0;
    this.grabDuration = 30; // Frames to grab the paper
    this.name = random(names); // Assign a random name
  }

  update() {
    if (this.state === 'removed' || this.state === 'waitingToBeCaptured' || this.state === 'waitingToBePickedUpByAirplane' || this.state === 'waitingToBeKilledByShark') {
      return; // Do nothing if the person is removed, being abducted by UFO, picked up by airplane, or killed by shark
    }

    switch (this.state) {
      case 'idle':
        if (this.targetPaper) {
          this.state = 'movingToPaper';
        }
        break;
      case 'movingToPaper':
        if (this.targetPaper) {
          let targetX = printerX; // Move towards the printer's X
          let targetY = height - WATER_LEVEL; // Stand on the ground (water level)

          let direction = (targetX > this.x) ? 1 : -1;
          if (abs(targetX - this.x) > this.walkSpeed) {
            this.x += this.walkSpeed * direction;
          } else {
            // Close enough to printer's X position
            // Check if the paper has landed
            if (!this.targetPaper.printing) {
              this.state = 'grabbing';
              this.grabCounter = 0;
              this.targetPaper.grabbed = true; // Mark paper as grabbed
            }
            // If paper is still printing, person waits at this X position
          }
        } else {
          this.state = 'removed'; // Should ideally not happen
        }
        break;
      case 'grabbing':
        this.grabCounter++;
        if (this.grabCounter >= this.grabDuration) {
          this.hasPaper = true;
          this.targetPaper.x = this.x;
          this.targetPaper.y = this.y - this.height / 2;

          this.state = 'waitingForTransportSelection'; // New state: wait for user to choose transport
          waitingPeople.push(this); // Add this person to the queue
          this.grabCounter = 0; // Reset for potential future use
        }
        break;
      case 'waitingForTransportSelection':
        // Person stands still, holding paper, waiting for user input
        // Transport selection buttons are visible (managed by updateTransportSelectionButtons in draw)
        break;
      case 'walkingOff': // Reintroduce this state
        this.x += this.walkSpeed;
        if (this.hasPaper && this.targetPaper) {
          this.targetPaper.x = this.x; // Move paper with the person
        }
        if (this.x > width + 100) { // Walk completely off-screen
          this.state = 'removed';
        }
        break;
    }
  }

  display() {
    // DO NOT display if removed or waiting to be captured/picked up/killed (their respective visuals handle it)
    if (this.state === 'removed' || this.state === 'waitingToBeCaptured' || this.state === 'waitingToBePickedUpByAirplane' || this.state === 'waitingToBeKilledByShark') {
      return;
    }

    // Display person normally if idle, movingToPaper, grabbing, waitingForTransportSelection, or walkingOff
    push();
    rectMode(CENTER);
    fill(this.color);
    noStroke();
    rect(this.x, this.y, this.width, this.height, 5);
    fill(this.color);
    ellipse(this.x, this.y - this.height / 2, this.width, this.width);

    if (this.hasPaper && this.targetPaper) {
      this.targetPaper.display(); // Display paper if person is carrying it
    }
    pop();
  }

  reset() {
    // This function is now only called when the person is fully removed.
    this.state = 'removed';
  }

  acquirePaper(paper) {
    this.targetPaper = paper;
    this.state = 'movingToPaper';
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

initialization Person Constructor constructor(id) { this.x = random(-200, -100); ... }

Creates a person with a random starting position off-screen, a random color, and a random name from the names array

conditional Capture State Guard if (this.state === 'removed' || this.state === 'waitingToBeCaptured' ...) { return; }

Stops the person from updating if they're being abducted, picked up, killed, or already removed—their captor handles their animation instead

switch-case Person State Machine switch (this.state) { case 'idle': ... case 'movingToPaper': ... }

Drives all person behavior: waiting, walking, grabbing, and finally waiting for transport selection

conditional Movement Toward Printer let direction = (targetX > this.x) ? 1 : -1; if (abs(targetX - this.x) > this.walkSpeed) { this.x += this.walkSpeed * direction; }

Calculates which direction to walk (left or right) and increments x by walkSpeed until close to the printer

conditional Paper Carrying Display if (this.hasPaper && this.targetPaper) { this.targetPaper.display(); }

Once the person grabs the paper, they draw it at their position so it moves with them

this.x = random(-200, -100);
Spawns the person slightly off-screen to the left so they walk on from the edge
this.color = color(random(100, 200), random(100, 200), random(100, 200));
Assigns a random color (RGB values between 100–200) so each person looks slightly different
this.name = random(names);
Picks a random name from the global names array; this name appears on the person's grave if they die
if (this.state === 'removed' || this.state === 'waitingToBeCaptured' ...) { return; }
Early exit: if the person is already being handled by a transport (UFO, airplane, shark) or removed, skip the rest of update()
case 'movingToPaper':
The person is walking toward the printer to grab the paper
let direction = (targetX > this.x) ? 1 : -1;
Ternary operator: if printer is to the right, direction is 1 (move right); otherwise, direction is -1 (move left)
if (abs(targetX - this.x) > this.walkSpeed) { this.x += this.walkSpeed * direction; }
If the distance to the printer is more than walkSpeed, take a step toward it; otherwise, stop and wait for the paper to land
if (!this.targetPaper.printing) { this.state = 'grabbing'; }
Once the paper has finished printing and landed, transition to the grabbing state
this.grabCounter++;
Increments a counter each frame; after grabDuration frames (30 by default), the person picks up the paper
waitingPeople.push(this);
Adds this person to the queue of people waiting for a transport selection
case 'walkingOff':
Person walks off-screen to the right (used when red UFO selects them, causing escape)
if (this.state === 'removed' || this.state === 'waitingToBeCaptured' ...) { return; }
In display(), skip drawing the person if they're being abducted or killed—their captor's display handles showing them
if (this.hasPaper && this.targetPaper) { this.targetPaper.display(); }
Draws the paper at the person's position so it appears to move with them as they walk or wait

UFO()

The UFO class demonstrates two advanced p5.js techniques: vector-based movement using trigonometry (atan2, cos, sin) and state-dependent display (the beam and abduction animation only appear in 'grabbingPerson' state). Critically, the red/blue distinction creates a moral duality in the sketch's dark humor: red UFOs intend to kill but the person escapes, while blue UFOs succeed in abduction. The abductedPersonVisual object cleverly decouples the person's data from their state, allowing the UFO to display them even after they've been marked for removal.

class UFO {
  constructor() {
    this.x = -200; // Start off-screen left
    this.y = random(height / 4, height - WATER_LEVEL - 50); // Random height above water
    this.flightSpeed = 4;
    this.state = 'idle'; // 'idle', 'movingToPerson', 'grabbingPerson', 'flyingOff', 'removed'
    this.targetPerson = null;
    this.grabCounter = 0;
    this.grabDuration = 60; // Frames for abduction
    this.abductedPersonVisual = null; // To hold the visual info of the abducted person
    this.isKiller = random() < 0.5; // 50% chance to be a killer UFO (this will be overridden by user selection)
  }

  update() {
    switch (this.state) {
      case 'idle':
        // Wait for a target person
        break;
      case 'movingToPerson':
        if (this.targetPerson) {
          let targetX = this.targetPerson.x;
          let targetY = this.targetPerson.y - this.targetPerson.height / 2; // Target their head

          let angle = atan2(targetY - this.y, targetX - this.x);
          this.x += cos(angle) * this.flightSpeed;
          this.y += sin(angle) * this.flightSpeed;

          // Check if close enough to grab
          if (dist(this.x, this.y, targetX, targetY) < this.flightSpeed * 2) {
            this.state = 'grabbingPerson';
            this.grabCounter = 0;

            // Mark the target person as waiting to be captured by this UFO
            // This is crucial for blue UFOs. For red UFOs, the person will already be walking off.
            if (this.targetPerson) {
                this.targetPerson.state = 'waitingToBeCaptured'; // This stops the person from drawing themselves
            }

            // Determine what the UFO *intends* to do, and store visual info.
            // For red UFOs, it's a 'death' intent, but the person escapes.
            // For blue UFOs, it's a 'capture' intent.
            if (this.isKiller) {
              // Red UFO intent: 'kill' (even if person escapes)
              this.abductedPersonVisual = {
                color: this.targetPerson.color,
                emoji: this.targetPerson.hasPaper ? this.targetPerson.targetPaper.emoji : '',
                isDead: true, // Intent is death for red UFOs
                lastX: this.targetPerson.x, // Store last ground position for grave
                name: this.targetPerson.name // Store person's name for grave
              };
            } else {
              this.abductedPersonVisual = {
                color: this.targetPerson.color,
                emoji: this.targetPerson.hasPaper ? this.targetPerson.targetPaper.emoji : '',
                isDead: false, // Not dead for blue UFOs
                lastX: this.targetPerson.x, // Store last ground position
                name: this.targetPerson.name // Store person's name
              };
            }
          }
        } else {
          this.state = 'flyingOff'; // If target disappears, just fly off
        }
        break;
      case 'grabbingPerson':
        this.grabCounter++;
        if (this.grabCounter >= this.grabDuration) {
          this.state = 'flyingOff';
          // Do NOT set targetPerson to null here. We need it to set its state to 'removed' later.
        }
        break;
      case 'flyingOff':
        this.x += this.flightSpeed;
        if (this.x > width + 200) { // Fly completely off-screen
          // Create grave if this UFO was a red (killer) UFO
          // The grave represents the red UFO's malevolent presence, even if the person walked off.
          if (this.isKiller && this.abductedPersonVisual) {
            graves.push(new Grave(this.abductedPersonVisual.lastX, height - WATER_LEVEL, this.abductedPersonVisual.emoji, this.abductedPersonVisual.name));
          }

          // If there was a target person (meaning it was a blue UFO that abducted them), mark them as fully removed
          if (this.targetPerson) {
              this.targetPerson.state = 'removed';
          }

          this.reset();
        }
        break;
      case 'removed':
        // Do nothing, waiting to be spliced from array
        break;
    }
  }

  display() {
    if (this.state === 'idle' || this.state === 'removed') {
      return; // Don't display if idle or removed
    }

    push();
    rectMode(CENTER);
    // Set UFO color based on isKiller property
    if (this.isKiller) {
      fill(255, 100, 100, 200); // Semi-transparent red for killer UFOs
    } else {
      fill(150, 200, 255, 200); // Semi-transparent blue for nice UFOs
    }
    stroke(50);
    rect(this.x, this.y, 100, 40, 20, 20, 0, 0); // Bottom ellipse shape
    ellipse(this.x, this.y - 20, 80, 40); // Top dome

    // Instead of emoji, draw some "lights" or "windows"
    fill(255, 255, 0, 150); // Yellow lights
    noStroke();
    ellipse(this.x - 30, this.y - 20, 10, 10);
    ellipse(this.x, this.y - 20, 10, 10);
    ellipse(this.x + 30, this.y - 20, 10, 10);

    // Add a beam for grabbing (only if grabbing, and not a red UFO that failed to grab)
    // The visual of the beam and abducted person/skull will only appear if the UFO is actually
    // performing the grab, which means it must be a blue UFO, or a red UFO before the person walks off.
    // However, the person walks off instantly, so red UFOs will likely not show the beam with a person.
    // The abductedPersonVisual will still hold the intent.
    if (this.state === 'grabbingPerson' && this.abductedPersonVisual) {
      noStroke();
      fill(200, 255, 200, 100); // Semi-transparent green beam
      ellipse(this.x, this.y + 40, 60, 100); // Wide ellipse for the beam

      // Display the abducted person/paper under the UFO
      let abductedY = this.y + 40; // Position below UFO

      if (this.abductedPersonVisual.isDead) {
        // If intent is death, draw skull emoji fading out
        let alpha = map(this.grabCounter, 0, this.grabDuration, 255, 0);
        fill(0, alpha); // Black skull, fading alpha
        textAlign(CENTER, CENTER);
        textSize(48); // Large skull emoji
        text('💀', this.x, abductedY + 30);
      } else {
        // If not dead, draw person's head and body
        fill(this.abductedPersonVisual.color);
        noStroke();
        ellipse(this.x, abductedY + 10, 20, 20); // Head
        rect(this.x, abductedY + 30, 20, 40, 2); // Body

        // Draw their paper if they had one
        if (this.abductedPersonVisual.emoji) {
          push();
          rectMode(CENTER);
          fill(255);
          stroke(0);
          rect(this.x + 20, abductedY + 30, 50, 75, 2); // Smaller paper
          fill(0);
          textAlign(CENTER, CENTER);
          textSize(24);
          text(this.abductedPersonVisual.emoji, this.x + 20, abductedY + 30);
          pop();
        }
      }
    }
    pop();
  }

  reset() {
    this.x = -200;
    this.y = random(height / 4, height - WATER_LEVEL - 50); // Reset to higher random height
    this.state = 'removed'; // Mark for removal after flying off
    this.targetPerson = null; // Person is now fully removed
    this.grabCounter = 0;
    this.abductedPersonVisual = null;
    // Reset isKiller for next appearance (optional, but makes each new UFO random)
    // For user-selected UFOs, this will be overridden by dispatchTransport().
    this.isKiller = random() < 0.5;
  }

  acquireTarget(person) {
    this.targetPerson = person;
    this.state = 'movingToPerson';
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

initialization UFO Constructor this.isKiller = random() < 0.5;

Randomly determines if this UFO is a red (killer) or blue (abductor) UFO

calculation UFO Movement Using Angle let angle = atan2(targetY - this.y, targetX - this.x); this.x += cos(angle) * this.flightSpeed; this.y += sin(angle) * this.flightSpeed;

Calculates the angle from UFO to target and moves toward it using trigonometric functions (cos/sin)

conditional Distance-Based Grab Trigger if (dist(this.x, this.y, targetX, targetY) < this.flightSpeed * 2)

Checks if UFO is close enough to person to begin abduction

object-creation Abducted Person Visual Storage this.abductedPersonVisual = { color: this.targetPerson.color, ... }

Stores the person's appearance and death status so the grab animation can display them correctly

conditional Grab Duration Counter this.grabCounter++; if (this.grabCounter >= this.grabDuration) { this.state = 'flyingOff'; }

Counts frames during abduction; after grabDuration, UFO flies away

conditional Abduction Beam Visual if (this.state === 'grabbingPerson' && this.abductedPersonVisual) { ... }

Draws the tractor beam and shows either a skull (red UFO) or person body (blue UFO) inside

this.x = -200;
UFOs start off-screen to the left, ready to zoom in when summoned
this.isKiller = random() < 0.5;
50% chance of being red (killer/intent to death) or blue (abductor/intent to capture)
let angle = atan2(targetY - this.y, targetX - this.x);
atan2() calculates the angle from the UFO to the target person. This angle drives smooth diagonal movement.
this.x += cos(angle) * this.flightSpeed;
cos(angle) gives the x-component of the angle. Multiplied by flightSpeed, this moves the UFO toward the target horizontally.
this.y += sin(angle) * this.flightSpeed;
sin(angle) gives the y-component. Together with cos, these two lines create diagonal movement toward the target at a constant speed.
if (dist(this.x, this.y, targetX, targetY) < this.flightSpeed * 2) {
dist() calculates the distance between UFO and target. When close enough, begin the grab.
if (this.isKiller) { this.abductedPersonVisual.isDead = true; }
Red UFOs store isDead: true, so their grab animation will show a skull instead of the person
this.targetPerson.state = 'waitingToBeCaptured';
Stops the person from updating themselves; the UFO now controls their visual position
if (this.x > width + 200) {
Once the UFO has flown far off-screen to the right, it's safe to remove it from the array and create a grave
if (this.isKiller && this.abductedPersonVisual) { graves.push(new Grave(...)); }
Red UFOs always create a grave, representing their deadly intent even if the person escaped
fill(255, 100, 100, 200); // Semi-transparent red for killer UFOs
Red UFOs are drawn in red (255 red, 100 green, 100 blue) to signal danger
if (this.abductedPersonVisual.isDead) { text('💀', this.x, abductedY + 30); }
During grab, if isDead, draw a black skull that fades out as the grab progresses
let alpha = map(this.grabCounter, 0, this.grabDuration, 255, 0);
map() converts the grab counter (0 to grabDuration) to alpha transparency (255 to 0), creating a fade-out effect

Airplane()

The Airplane class is a multi-stage state machine with seven states that choreograph a complete narrative arc: approach, pickup, flight, crash, wait, and departure. The crash is the visual climax: rapid descent, fire animation with fading alpha, grave creation, and a lingering moment (flyingOffAfterCrash) before the airplane escapes. By locking the x position during pickup and crash, the airplane creates the illusion of a stationary disaster, making the sequence feel inevitable rather than chaotic.

🔬 This code makes the airplane plummet at 10 pixels per frame. What happens if you change `this.y += 10` to `this.y += 2`? Will the crash be more gradual or will it speed up the plane's descent?

      case 'crashing':
        // Lock X position
        this.x = this.pickupX;
        this.actionCounter++;
        // Simulate descent and crash
        this.y += 10; // Rapid descent
        if (this.y >= height - WATER_LEVEL) { // Hit the ground (water level)
class Airplane {
  constructor(targetPerson = null) {
    this.x = -200; // Start off-screen left
    this.y = random(50, height - WATER_LEVEL - 50); // Higher flight path for airplane (above water)
    this.width = 150;
    this.height = 50;
    this.color = color(200); // Light grey
    this.flightSpeed = 5; // Fixed flight speed
    this.state = 'idle'; // 'idle', 'movingToPerson', 'pickingUpPerson', 'flyingWithPerson', 'crashing', 'flyingOffAfterCrash', 'flyingOff', 'removed'
    this.targetPerson = targetPerson;
    this.actionCounter = 0;
    this.actionDuration = 60; // Frames for picking up/crashing
    this.flyingDuration = 60; // Frames for flying with person before crashing (1 second)
    this.pickupX = -1; // Store X position at pickup
    this.leavingDuration = 60; // 1 second (60 frames) after crash before flying off
  }

  update() {
    switch (this.state) {
      case 'idle':
        if (this.targetPerson) {
          this.state = 'movingToPerson';
        }
        break;

      case 'movingToPerson':
        if (this.targetPerson) {
          let targetX = this.targetPerson.x;
          let targetY = this.targetPerson.y - this.targetPerson.height / 2; // Target their head

          let angle = atan2(targetY - this.y, targetX - this.x);
          this.x += cos(angle) * this.flightSpeed;
          this.y += sin(angle) * this.flightSpeed;

          // Check if close enough to pick up
          if (dist(this.x, this.y, targetX, targetY) < this.flightSpeed * 2) {
            this.state = 'pickingUpPerson';
            this.actionCounter = 0;
            this.pickupX = this.x; // Store current X at pickup
            // Mark the target person as waiting to be picked up by this airplane
            if (this.targetPerson) {
              this.targetPerson.state = 'waitingToBePickedUpByAirplane';
            }
          }
        } else {
          this.state = 'flyingOff'; // If target disappears, just fly off
        }
        break;

      case 'pickingUpPerson':
        // Lock X position after pickup
        this.x = this.pickupX;
        this.actionCounter++;
        // Simulate person moving up into the plane
        // The display method will handle drawing the person inside
        if (this.actionCounter >= this.actionDuration) {
          this.state = 'flyingWithPerson'; // Transition to flying phase
          this.actionCounter = 0; // Reset counter for flying phase
        }
        break;

      case 'flyingWithPerson': // NEW state
        // Lock X position
        this.x = this.pickupX;
        this.actionCounter++;
        // After flying for a while, transition to crashing
        if (this.actionCounter >= this.flyingDuration) { // Check flyingDuration here
          this.state = 'crashing';
          this.actionCounter = 0;
        }
        break;

      case 'crashing':
        // Lock X position
        this.x = this.pickupX;
        this.actionCounter++;
        // Simulate descent and crash
        this.y += 10; // Rapid descent
        if (this.y >= height - WATER_LEVEL) { // Hit the ground (water level)
          this.y = height - WATER_LEVEL;
          // Create grave for the person
          if (this.targetPerson) {
            graves.push(new Grave(this.x, this.y, this.targetPerson.hasPaper ? this.targetPerson.targetPaper.emoji : '', this.targetPerson.name));
            this.targetPerson.state = 'removed'; // Person is now dead
          }

          // Transition to flying off after crash
          this.state = 'flyingOffAfterCrash';
          this.actionCounter = 0; // Reset counter for leaving duration
        }
        break;

      case 'flyingOffAfterCrash': // NEW: State for leaving after crash
        this.actionCounter++;
        if (this.actionCounter >= this.leavingDuration) {
          this.state = 'flyingOff'; // Start moving off-screen
        }
        // Airplane stays stationary at crash site during this state
        break;

      case 'flyingOff':
        this.x += this.flightSpeed;
        if (this.x > width + 200) {
          this.reset();
        }
        break;

      case 'removed':
        break;
    }
  }

  display() {
    if (this.state === 'removed' || this.state === 'crashing') {
      return; // Don't display if removed or during crash (crash animation is separate)
    }

    push();
    rectMode(CENTER);
    fill(this.color);
    noStroke();

    // Airplane body (simple rectangle with wings)
    rect(this.x, this.y, this.width, this.height, 10); // Body
    rect(this.x - this.width / 4, this.y + this.height / 2, this.width / 2, this.height / 4, 5); // Left wing
    rect(this.x + this.width / 4, this.y + this.height / 2, this.width / 2, this.height / 4, 5); // Right wing

    // Add some details (windows, propeller)
    fill(200);
    ellipse(this.x - 30, this.y - 10, 10, 10);
    ellipse(this.x, this.y - 10, 10, 10);
    ellipse(this.x + 30, this.y - 10, 10, 10);

    // Only draw propeller for the initial plane (which is now the only type)
    fill(100);
    rect(this.x + this.width / 2, this.y, 10, 20); // Propeller shaft
    rect(this.x + this.width / 2 + 5, this.y, 5, 30); // Propeller blade

    // NEW: Draw engine fire if crashing
    if (this.state === 'crashing') {
      let fireX = this.x - this.width / 2 - 10; // Back of the plane
      let fireY = this.y;
      let alpha = map(this.actionCounter, 0, this.actionDuration, 255, 0);

      push();
      noStroke();
      fill(255, 100, 0, alpha); // Orange
      ellipse(fireX - 5, fireY, 20, 20);
      fill(255, 200, 0, alpha); // Yellow
      ellipse(fireX + 5, fireY, 15, 15);
      fill(255, 0, 0, alpha); // Red
      triangle(fireX, fireY - 10, fireX - 15, fireY + 10, fireX + 15, fireY + 10);
      pop();
    }

    // Display abducted person during 'pickingUpPerson' and 'flyingWithPerson' states
    if ((this.state === 'pickingUpPerson' || this.state === 'flyingWithPerson') && this.targetPerson) {
      let abductedY = this.y + 20; // Position person inside plane
      fill(this.targetPerson.color);
      noStroke();
      ellipse(this.x, abductedY - 10, 20, 20); // Head
      rect(this.x, abductedY + 10, 20, 40, 2); // Body
      if (this.targetPerson.hasPaper && this.targetPerson.targetPaper) {
        push();
        rectMode(CENTER);
        fill(255);
        stroke(0);
        rect(this.x + 20, abductedY + 10, 50, 75, 2);
        fill(0);
        textAlign(CENTER, CENTER);
        textSize(24);
        text(this.targetPerson.targetPaper.emoji, this.x + 20, abductedY + 10);
        pop();
      }
    }
    pop();
  }

  reset() {
    this.x = -200;
    this.y = random(50, height - WATER_LEVEL - 50);
    this.state = 'removed';
    this.targetPerson = null;
    this.actionCounter = 0;
    this.pickupX = -1; // Reset pickupX
  }

  acquireTarget(person) {
    this.targetPerson = person;
    this.state = 'movingToPerson';
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

initialization Airplane Constructor this.state = 'idle';

Initializes the airplane with eight possible states: idle, movingToPerson, pickingUpPerson, flyingWithPerson, crashing, flyingOffAfterCrash, flyingOff, removed

calculation Pickup Position Lock this.x = this.pickupX;

Locks the airplane's x position once it reaches the person, preventing it from drifting during the pickup animation

conditional Crash Descent Logic this.y += 10;

During the crash state, rapidly descends the airplane to the ground

conditional Crash Fire Visual if (this.state === 'crashing') { ... fill(255, 100, 0, alpha); ... }

Draws flames at the back of the airplane during the crash, fading out as the crash completes

conditional Person Inside Airplane if ((this.state === 'pickingUpPerson' || this.state === 'flyingWithPerson') && this.targetPerson)

Displays the person's head and body inside the airplane during pickup and flight, along with their paper

this.flightSpeed = 5;
Airplanes move slightly faster than UFOs (5 vs 4), making them more aggressive pickups
this.flyingDuration = 60;
The airplane flies with the person for exactly 60 frames (1 second at 60 FPS) before triggering a crash
this.pickupX = this.x;
Stores the x position at pickup so the airplane can lock itself there during pickup and crash sequences
case 'pickingUpPerson':
Airplane has reached the person and is now picking them up (animating the person entering the plane)
this.x = this.pickupX;
Locks the airplane's x position so it doesn't drift side-to-side during pickup
if (this.actionCounter >= this.actionDuration) { this.state = 'flyingWithPerson'; }
After 60 frames of pickup animation, transition to the flying state
case 'flyingWithPerson':
Airplane is flying with the person inside, waiting before the inevitable crash
if (this.actionCounter >= this.flyingDuration) { this.state = 'crashing'; }
After 60 frames of flying, begin the crash sequence
case 'crashing':
The airplane is descending rapidly toward the ground
this.y += 10;
Rapid vertical descent—10 pixels per frame is much faster than normal flight
if (this.y >= height - WATER_LEVEL) {
When the airplane reaches the water level (ground), the crash is complete; create a grave and transition to leaving
case 'flyingOffAfterCrash':
Airplane sits at the crash site for leavingDuration frames (1 second) before flying off
let alpha = map(this.actionCounter, 0, this.actionDuration, 255, 0);
Fire animation alpha fades from opaque (255) to transparent (0) during the crash
if ((this.state === 'pickingUpPerson' || this.state === 'flyingWithPerson') && this.targetPerson) {
Only display the person inside during pickup and flying states; not during or after the crash

Shark()

The Shark class is unique because it spawns an Ambulance when it kills a person—creating a chain reaction across three object types. Visually, the shark uses beginShape/endShape to draw a custom polygon body with fins, more detailed than the UFO's ellipses or the Airplane's rectangles. Mechanically, it demonstrates how killing a person can trigger secondary effects (ambulance creation), adding depth to the simulation's dark narrative.

class Shark {
  constructor(targetPerson = null) {
    this.x = -200; // Start off-screen left, in the water
    this.y = random(height - WATER_LEVEL + 20, height - 20);
    this.width = 100;
    this.height = 40;
    this.color = color(100);
    this.flightSpeed = 4;
    this.state = 'idle'; // 'idle', 'movingToPerson', 'killingPerson', 'leaving', 'removed'
    this.targetPerson = targetPerson;
    this.actionCounter = 0;
    this.actionDuration = 60; // Frames for killing
  }

  update() {
    switch (this.state) {
      case 'idle':
        if (this.targetPerson) {
          this.state = 'movingToPerson';
        }
        break;

      case 'movingToPerson':
        if (this.targetPerson) {
          let targetX = this.targetPerson.x;
          let targetY = this.targetPerson.y; // Target the person directly at water level

          let angle = atan2(targetY - this.y, targetX - this.x);
          this.x += cos(angle) * this.flightSpeed;
          this.y += sin(angle) * this.flightSpeed;

          // Check if close enough to kill
          if (dist(this.x, this.y, targetX, targetY) < this.flightSpeed * 2) {
            this.state = 'killingPerson';
            this.actionCounter = 0;
            // Mark the target person as waiting to be killed by this shark
            if (this.targetPerson) {
              this.targetPerson.state = 'waitingToBeKilledByShark';
            }
          }
        } else {
          this.state = 'leaving'; // If target disappears, just leave
        }
        break;

      case 'killingPerson':
        this.actionCounter++;
        if (this.actionCounter >= this.actionDuration) {
          // Create grave for the person
          if (this.targetPerson) {
            graves.push(new Grave(this.targetPerson.x, this.targetPerson.y, this.targetPerson.hasPaper ? this.targetPerson.targetPaper.emoji : '', this.targetPerson.name));
            this.targetPerson.state = 'removed'; // Person is now dead
          }

          // Spawn an ambulance at the grave location
          let grave = graves.find(g => g.x === this.targetPerson.x && g.y === this.targetPerson.y);
          if (grave) {
            ambulances.push(new Ambulance(grave.x, grave.y, grave));
          }

          this.state = 'leaving'; // Shark leaves after killing
        }
        break;

      case 'leaving':
        this.x += this.flightSpeed; // Swim off to the right
        if (this.x > width + 200) {
          this.reset();
        }
        break;

      case 'removed':
        break;
    }
  }

  display() {
    if (this.state === 'removed' || this.state === 'idle') {
      return;
    }

    push();
    rectMode(CENTER);
    fill(this.color);
    noStroke();

    // Body of the shark (more streamlined)
    beginShape();
    vertex(this.x - this.width / 2, this.y - this.height / 4); // Snout top
    vertex(this.x - this.width / 4, this.y - this.height / 2); // Back top
    vertex(this.x + this.width / 2, this.y - this.height / 4); // Tail base top
    vertex(this.x + this.width / 2 + 20, this.y - this.height / 2); // Tail tip top
    vertex(this.x + this.width / 2 + 10, this.y); // Tail middle
    vertex(this.x + this.width / 2 + 20, this.y + this.height / 2); // Tail tip bottom
    vertex(this.x + this.width / 2, this.y + this.height / 4); // Tail base bottom
    vertex(this.x - this.width / 4, this.y + this.height / 2); // Back bottom
    vertex(this.x - this.width / 2, this.y + this.height / 4); // Snout bottom
    endShape(CLOSE);

    // Dorsal Fin
    fill(this.color);
    triangle(
      this.x, this.y - this.height / 2,
      this.x - this.width / 4, this.y - this.height / 4,
      this.x + this.width / 4, this.y - this.height / 4
    );

    // Pectoral Fin (bottom fin)
    triangle(
      this.x - this.width / 4, this.y + this.height / 4,
      this.x - this.width / 8, this.y + this.height / 2,
      this.x + this.width / 8, this.y + this.height / 4
    );

    // Eye
    fill(0);
    ellipse(this.x - this.width / 4 + 10, this.y - this.height / 4, 5, 5);

    // Display killing animation (e.g., blood) if killing
    if (this.state === 'killingPerson' && this.targetPerson) {
      let bloodX = this.targetPerson.x;
      let bloodY = this.targetPerson.y;
      let alpha = map(this.actionCounter, 0, this.actionDuration, 255, 0);

      push();
      noStroke();
      fill(255, 0, 0, alpha); // Red blood
      ellipse(bloodX, bloodY, 30, 30);
      ellipse(bloodX + 10, bloodY - 10, 20, 20);
      ellipse(bloodX - 10, bloodY + 10, 15, 15);
      pop();
    }

    pop();
  }

  reset() {
    this.x = -200;
    this.y = random(height - WATER_LEVEL + 20, height - 20);
    this.state = 'removed';
    this.targetPerson = null;
    this.actionCounter = 0;
  }

  acquireTarget(person) {
    this.targetPerson = person;
    this.state = 'movingToPerson';
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

initialization Shark Constructor this.y = random(height - WATER_LEVEL + 20, height - 20);

Spawns the shark in the water (between water level and the bottom of the canvas)

calculation Shark Movement Using Angle let angle = atan2(targetY - this.y, targetX - this.x); this.x += cos(angle) * this.flightSpeed; this.y += sin(angle) * this.flightSpeed;

Uses the same vector-based movement as UFOs, allowing sharks to swim diagonally toward victims

conditional Killing and Ambulance Spawn if (this.actionCounter >= this.actionDuration) { graves.push(...); ambulances.push(...); }

After the killing animation, creates both a grave and spawns an ambulance to retrieve the body

conditional Blood Animation if (this.state === 'killingPerson' && this.targetPerson) { ... fill(255, 0, 0, alpha); ... }

Displays fading red blood circles around the victim during the killing animation

shape-drawing Shark Body Using beginShape beginShape(); vertex(...); ... endShape(CLOSE);

Draws a custom polygonal shark shape using beginShape/endShape instead of simple rectangles

this.y = random(height - WATER_LEVEL + 20, height - 20);
Spawns the shark submerged in the water, between slightly below the water level and the canvas bottom
let angle = atan2(targetY - this.y, targetX - this.x);
Calculates the angle from shark to victim using atan2, same technique as UFOs for diagonal movement
let targetY = this.targetPerson.y;
Unlike UFOs (which aim for the person's head), sharks target the person's full body at water level
beginShape(); vertex(...); ... endShape(CLOSE);
Draws a custom multi-vertex polygon instead of simple rectangles, creating a more detailed shark silhouette
let grave = graves.find(g => g.x === this.targetPerson.x && g.y === this.targetPerson.y);
Searches the graves array for the freshly-created grave so an ambulance can be spawned to retrieve it
ambulances.push(new Ambulance(grave.x, grave.y, grave));
Spawns an ambulance at the grave's position with a reference to the grave object it should pick up
fill(255, 0, 0, alpha);
Red blood circles fade out as the killing animation completes

Ambulance()

The Ambulance class is the final piece of the dark comedy: it responds to shark kills by driving in from off-screen, picking up the grave, and driving away. Unlike other transports, the ambulance drives horizontally rather than using vector-based diagonal movement. It demonstrates object ownership handoff: the ambulance takes control of the grave's visual position while moving, then releases it by marking its state as 'removed'. This creates a chain of cause and effect: person → shark → grave → ambulance.

class Ambulance {
  constructor(graveX, graveY, grave) {
    this.x = width + 200; // Start off-screen right
    this.y = height - 100; // Drives on the ground
    this.width = 120;
    this.height = 60;
    this.color = color(255); // White
    this.flightSpeed = 4; // Drives on ground, so less "flightSpeed"
    this.state = 'idle'; // 'idle', 'movingToGrave', 'pickingUpGrave', 'leaving', 'removed'
    this.graveX = graveX;
    this.graveY = graveY;
    this.grave = grave;
    this.actionCounter = 0;
    this.actionDuration = 90; // Frames for picking up grave
  }

  update() {
    switch (this.state) {
      case 'idle':
        if (this.graveX !== -1) {
          this.state = 'movingToGrave';
        }
        break;

      case 'movingToGrave':
        let targetX = this.graveX; // Target the X position of the grave
        if (abs(targetX - this.x) > this.flightSpeed) {
          this.x -= this.flightSpeed; // Drive left
        } else {
          this.x = targetX; // Snap to grave's X
          this.state = 'pickingUpGrave';
          this.actionCounter = 0;
        }
        break;

      case 'pickingUpGrave':
        this.actionCounter++;
        // Move the grave with the ambulance
        if (this.grave) {
          this.grave.x = this.x;
          this.grave.y = this.y - this.height / 2; // Position grave on top of ambulance
        }
        if (this.actionCounter >= this.actionDuration) {
          if (this.grave) {
            this.grave.state = 'removed'; // Remove grave from the graves array
          }
          this.state = 'leaving'; // Ambulance leaves after picking up
        }
        break;

      case 'leaving':
        this.x -= this.flightSpeed; // Drive off to the left
        if (this.grave) {
          // Continue moving the grave with the ambulance as it leaves
          this.grave.x = this.x;
          this.grave.y = this.y - this.height / 2;
        }
        if (this.x < -200) {
          this.reset();
        }
        break;

      case 'removed':
        break;
    }
  }

  display() {
    if (this.state === 'removed' || this.state === 'idle') {
      return;
    }

    push();
    rectMode(CENTER);
    fill(this.color);
    stroke(0);
    strokeWeight(2);

    // Ambulance body
    rect(this.x, this.y - this.height / 4, this.width, this.height / 2, 5);
    // Cabin
    rect(this.x + this.width / 4, this.y - this.height / 4 - this.height / 8, this.width / 2, this.height / 4, 3);

    // Wheels
    fill(50);
    ellipse(this.x - this.width / 4, this.y + this.height / 4, this.height / 2, this.height / 2);
    ellipse(this.x + this.width / 4, this.y + this.height / 4, this.height / 2, this.height / 2);

    // Red cross
    fill(255, 0, 0);
    noStroke();
    rect(this.x - this.width / 4, this.y - this.height / 4, this.height / 4, this.height / 8);
    rect(this.x - this.width / 4, this.y - this.height / 4, this.height / 8, this.height / 4);

    // Display grave on top of ambulance if picking up or leaving
    if (this.grave && (this.state === 'pickingUpGrave' || this.state === 'leaving')) {
      // The grave's display method will draw it at its current x,y
      this.grave.display();
    }

    pop();
  }

  reset() {
    this.x = width + 200;
    this.y = height - 100;
    this.state = 'removed';
    this.graveX = -1;
    this.graveY = -1;
    this.grave = null;
    this.actionCounter = 0;
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

initialization Ambulance Constructor this.x = width + 200;

Spawns the ambulance off-screen to the right, ready to drive left toward the grave

calculation Grave Movement with Ambulance this.grave.x = this.x; this.grave.y = this.y - this.height / 2;

Continuously updates the grave's position to sit on top of the ambulance as it drives

calculation Ambulance Horizontal Movement if (abs(targetX - this.x) > this.flightSpeed) { this.x -= this.flightSpeed; }

Drives the ambulance left (negative x) toward the grave's location

this.x = width + 200;
Spawns off-screen to the right, ready to drive in from the edge
this.y = height - 100;
Drives on the ground, positioned above the water level for visibility
this.grave = grave;
Stores a reference to the specific grave object it was created to retrieve
case 'movingToGrave':
The ambulance is driving left toward the grave's location
this.x -= this.flightSpeed;
Drives left (subtracts from x) at flightSpeed pixels per frame
case 'pickingUpGrave':
The ambulance has reached the grave and is now loading it
this.grave.x = this.x; this.grave.y = this.y - this.height / 2;
Positions the grave on top of the ambulance so it moves with it visually
if (this.actionCounter >= this.actionDuration) { this.grave.state = 'removed'; }
After holding the grave for actionDuration frames, marks it as removed from the graves array
case 'leaving':
The ambulance drives off-screen to the left with the grave on top
if (this.grave && (this.state === 'pickingUpGrave' || this.state === 'leaving')) { this.grave.display(); }
Displays the grave only while the ambulance is holding it, creating the visual of transport

Grave()

The Grave class represents the final resting place of a character. It embodies both narrative closure (the character's name and emoji are immortalized) and temporal rhythm (graves stick around for 2 seconds before walking off). Using millis() to measure elapsed time in a sketch (rather than frame counting) is important when you need absolute time measurements. The grave's three states—static, movingOff, removed—mirror the pattern used by all other classes, creating a consistent lifecycle across the entire sketch.

class Grave {
  constructor(x, y, emoji, name) {
    this.x = x;
    this.y = y;
    this.width = 60;
    this.height = 80;
    this.emoji = emoji;
    this.name = name;
    this.state = 'static'; // 'static', 'movingOff', 'removed'
    this.birthTime = millis();
    this.lifetime = 2000;
    this.walkSpeed = random(1, 2);
  }

  update() {
    if (this.state === 'static') {
      if (millis() - this.birthTime > this.lifetime) {
        this.state = 'movingOff';
      }
    } else if (this.state === 'movingOff') {
      this.x += this.walkSpeed;
      if (this.x > width + 100) {
        this.state = 'removed';
      }
    }
  }

  display() {
    if (this.state === 'removed') return; // Don't display if removed

    push();
    rectMode(CENTER);
    fill(150, 100, 50);
    stroke(50);
    rect(this.x, this.y - this.height / 2, this.width, this.height, 5);
    arc(this.x, this.y - this.height, this.width, this.height / 2, PI, TWO_PI);

    fill(0);
    textAlign(CENTER, CENTER);
    textSize(24);
    text(this.emoji, this.x, this.y - this.height * 0.6);

    textSize(12);
    text(this.name, this.x, this.y - this.height * 0.2);
    pop();
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Grave Lifetime Counter if (millis() - this.birthTime > this.lifetime) { this.state = 'movingOff'; }

Tracks how long the grave has existed (in milliseconds) and transitions to movingOff after 2 seconds

conditional Grave Walking Off Animation this.x += this.walkSpeed; if (this.x > width + 100) { this.state = 'removed'; }

After the lifetime expires, walks the grave off-screen to the right

shape-drawing Grave Tombstone Shape rect(...); arc(...);

Draws a rounded rectangle body and a semi-circular arc top to create a tombstone silhouette

this.birthTime = millis();
Captures the current time (in milliseconds since the sketch started) when the grave is created
this.lifetime = 2000;
The grave will stay static for 2000 milliseconds (2 seconds) before beginning to walk off
if (millis() - this.birthTime > this.lifetime) {
Checks if the current time minus birth time exceeds the lifetime; if so, the grave has aged out
this.x += this.walkSpeed;
Moves the grave to the right at a random walk speed (1–2 pixels per frame)
rect(this.x, this.y - this.height / 2, this.width, this.height, 5);
Draws the main tombstone body as a rounded rectangle in brown
arc(this.x, this.y - this.height, this.width, this.height / 2, PI, TWO_PI);
Draws a semi-circular arc (from PI to TWO_PI, which is the top half) to cap the tombstone with a rounded top
text(this.emoji, this.x, this.y - this.height * 0.6);
Displays the person's emoji (what was on their paper) in the upper part of the tombstone
text(this.name, this.x, this.y - this.height * 0.2);
Displays the person's random name below the emoji as an epitaph

triggerPrint()

triggerPrint() is called whenever the user clicks the Print button or presses Enter. It's a thin wrapper that validates input and delegates to createPaperAction(). This separation of concerns (input handling vs. action creation) keeps the code clean and testable.

function triggerPrint() {
  let currentEmoji = emojiInput.value();
  if (currentEmoji.length > 0) {
    createPaperAction(currentEmoji);
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

calculation Emoji Input Value Retrieval let currentEmoji = emojiInput.value();

Gets the current text from the emoji input field

conditional Non-Empty Validation if (currentEmoji.length > 0) {

Prevents printing empty strings; only creates a paper if the input has content

let currentEmoji = emojiInput.value();
Retrieves whatever text the user has typed in the emoji input field
if (currentEmoji.length > 0) {
Checks if the string is non-empty. If it's empty, nothing happens (preventing blank papers)
createPaperAction(currentEmoji);
Calls the helper function to create a paper, spawn a person, and start their journey

createPaperAction()

createPaperAction() is the engine of the sketch's narrative. Every time you print, this function creates a paper-person pair, links them, and launches the person into motion. This is a clean pattern: separate the user input (triggerPrint) from the action logic (createPaperAction), making it easy to trigger the same action from different sources (button click, keyboard press, etc.).

function createPaperAction(emoji) {
  let newPaper = new Paper(printerX, printerY - 20, emoji);
  papers.push(newPaper);

  let brandNewPerson = new Person(people.length);
  people.push(brandNewPerson);
  brandNewPerson.acquirePaper(newPaper);
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

object-creation Paper Object Creation let newPaper = new Paper(printerX, printerY - 20, emoji);

Creates a new Paper object at the printer position with the given emoji

object-creation Person Object Creation let brandNewPerson = new Person(people.length);

Creates a new Person with an ID equal to the current number of people (for uniqueness)

let newPaper = new Paper(printerX, printerY - 20, emoji);
Creates a new Paper object positioned at the printer's x and slightly above the printer's y (20 pixels up)
papers.push(newPaper);
Adds the paper to the global papers array so draw() can update and display it
let brandNewPerson = new Person(people.length);
Creates a new person with an ID based on the current length of the people array (first person gets ID 0, second gets 1, etc.)
people.push(brandNewPerson);
Adds the person to the global people array
brandNewPerson.acquirePaper(newPaper);
Calls the person's acquirePaper method, which sets their targetPaper and changes their state to 'movingToPaper'

updateTransportSelectionButtons()

updateTransportSelectionButtons() is called every frame from draw(). It dynamically repositions buttons to always hover above the first waiting person, creating a responsive UI that follows the character. This is an example of imperative UI management: each frame, buttons are explicitly positioned and shown/hidden based on game state. This approach is simple but works well for small, interactive sketches.

function updateTransportSelectionButtons() {
  if (waitingPeople.length > 0) {
    let person = waitingPeople[0]; // Get the first person in the queue
    let buttonY = person.y - 50; // Position buttons above the person

    // Adjust positions for 4 buttons
    spawnRedUFOButton.position(person.x - 180, buttonY);
    spawnRedUFOButton.show();

    spawnBlueUFOButton.position(person.x - 90, buttonY);
    spawnBlueUFOButton.show();

    spawnAirplaneButton.position(person.x, buttonY);
    spawnAirplaneButton.show();

    spawnSharksButton.position(person.x + 90, buttonY); // Position for new Sharks button
    spawnSharksButton.show();

  } else {
    // Hide all buttons if no one is waiting
    spawnRedUFOButton.hide();
    spawnBlueUFOButton.hide();
    spawnAirplaneButton.hide();
    spawnSharksButton.hide(); // Hide sharks button too
    spawnRedUFOButton.position(-999, -999);
    spawnBlueUFOButton.position(-999, -999);
    spawnAirplaneButton.position(-999, -999);
    spawnSharksButton.position(-999, -999); // Reset position
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Queue Existence Check if (waitingPeople.length > 0) {

Only displays buttons if there are people waiting for transport selection

calculation Four-Button Layout spawnRedUFOButton.position(person.x - 180, buttonY);

Positions four buttons horizontally spaced above the waiting person

function-call Button Show/Hide Management spawnRedUFOButton.show();

Shows buttons when someone is waiting, hides them when the queue is empty

if (waitingPeople.length > 0) {
Only positions and shows buttons if the waitingPeople queue has at least one person
let person = waitingPeople[0];
Gets the first person in the queue (the one who is currently waiting)
let buttonY = person.y - 50;
Positions buttons 50 pixels above the person's y position, so they hover above their head
spawnRedUFOButton.position(person.x - 180, buttonY);
Places Red UFO button 180 pixels to the left of the person's x position
spawnBlueUFOButton.position(person.x - 90, buttonY);
Places Blue UFO button 90 pixels to the left
spawnAirplaneButton.position(person.x, buttonY);
Places Airplane button directly above the person
spawnSharksButton.position(person.x + 90, buttonY);
Places Sharks button 90 pixels to the right
spawnRedUFOButton.show();
Makes the button visible (called for all four buttons)
} else { spawnRedUFOButton.hide(); ... }
If the queue is empty, hides all buttons and moves them off-screen so they don't interfere

dispatchTransport()

dispatchTransport() is the core of user interaction. Each transport type has its own dispatch logic, but all follow the same pattern: check for an idle instance, reuse or create, assign target, update person state. This object pooling approach (reusing idle instances instead of always creating new ones) is efficient and prevents memory bloat. The different person states ('walkingOff' for red UFOs vs 'waitingToBeCaptured' for blue UFOs) encode the unique narrative of each transport type.

function dispatchTransport(transportType) {
  if (waitingPeople.length === 0) return;

  let person = waitingPeople.shift(); // Get the first person from the queue

  if (transportType === 'redUFO' || transportType === 'blueUFO') {
    let idleUFO = ufos.find(ufo => ufo.state === 'idle');
    let dispatchedUFO;
    if (idleUFO) {
      dispatchedUFO = idleUFO;
    } else {
      dispatchedUFO = new UFO();
      ufos.push(dispatchedUFO);
    }
    dispatchedUFO.isKiller = (transportType === 'redUFO');
    dispatchedUFO.acquireTarget(person);
    if (transportType === 'redUFO') {
      person.state = 'walkingOff';
    } else {
      person.state = 'waitingToBeCaptured';
    }
  } else if (transportType === 'airplane') {
    let idleAirplane = airplanes.find(plane => plane.state === 'idle');
    let dispatchedAirplane;
    if (idleAirplane) {
      dispatchedAirplane = idleAirplane;
    } else {
      dispatchedAirplane = new Airplane();
      airplanes.push(dispatchedAirplane);
    }
    dispatchedAirplane.acquireTarget(person);
    person.state = 'waitingToBePickedUpByAirplane';
  } else if (transportType === 'sharks') { // NEW: Handle shark dispatch
    let idleShark = sharks.find(shark => shark.state === 'idle');
    let dispatchedShark;
    if (idleShark) {
      dispatchedShark = idleShark;
    } else {
      dispatchedShark = new Shark();
      sharks.push(dispatchedShark);
    }
    dispatchedShark.acquireTarget(person);
    person.state = 'waitingToBeKilledByShark'; // Set person state for shark
  }

  updateTransportSelectionButtons(); // Update buttons for the next person or hide
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Person Dequeuing let person = waitingPeople.shift();

Removes and returns the first person from the waiting queue

conditional UFO Dispatch Logic if (transportType === 'redUFO' || transportType === 'blueUFO') { ... }

Reuses idle UFO or creates new one, sets isKiller flag, and assigns target

conditional Airplane Dispatch Logic else if (transportType === 'airplane') { ... }

Reuses idle airplane or creates new one and assigns target

conditional Shark Dispatch Logic else if (transportType === 'sharks') { ... }

Reuses idle shark or creates new one and assigns target

if (waitingPeople.length === 0) return;
Early exit: if no one is waiting, do nothing (prevents errors)
let person = waitingPeople.shift();
shift() removes and returns the first element from an array, effectively dequeuing the person
let idleUFO = ufos.find(ufo => ufo.state === 'idle');
Searches the ufos array for a UFO in idle state. find() returns the first match or undefined if none exist
if (idleUFO) { dispatchedUFO = idleUFO; } else { dispatchedUFO = new UFO(); ufos.push(dispatchedUFO); }
Object pooling: reuse an idle UFO if one exists, otherwise create a new one and add it to the array
dispatchedUFO.isKiller = (transportType === 'redUFO');
Sets the UFO's isKiller property to true for red UFOs, false for blue UFOs
dispatchedUFO.acquireTarget(person);
Calls the UFO's acquireTarget method, setting its targetPerson and moving to 'movingToPerson' state
if (transportType === 'redUFO') { person.state = 'walkingOff'; }
Red UFOs cause the person to immediately enter 'walkingOff' state, so they escape before the UFO arrives
updateTransportSelectionButtons();
Updates the button UI for the next person in the queue, or hides buttons if the queue is now empty

windowResized()

windowResized() is a p5.js built-in function that's called whenever the browser window is resized. This sketch uses a responsive canvas (windowWidth, windowHeight), so windowResized() must recalibrate all positions to maintain the illusion of a stable scene. Notice the conditional in the person loop: people being captured are not updated, because their captor (UFO, airplane, shark) is controlling their position. This is a subtle but important detail that prevents conflicting position updates.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  emojiInput.position(10, 10);
  printButton.position(emojiInput.x + emojiInput.width + 10, emojiInput.y);
  printerY = height - WATER_LEVEL - 50; // Adjust printer Y based on new water level
  for (let paper of papers) {
    paper.groundY = height - WATER_LEVEL; // Adjust paper landing Y
  }
  for (let person of people) {
    // Only update Y for people not yet captured or removed
    if (person.state !== 'waitingToBeCaptured' && person.state !== 'waitingToBePickedUpByAirplane' && person.state !== 'waitingToBeKilledByShark' && person.state !== 'removed') {
      person.y = height - WATER_LEVEL; // Adjust person ground Y
    }
  }
  for (let grave of graves) {
    // Graves are placed at the ground Y (water level)
    grave.y = height - WATER_LEVEL;
  }
  updateTransportSelectionButtons();
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

function-call Canvas Resize resizeCanvas(windowWidth, windowHeight);

Resizes the p5.js canvas to match the new window dimensions

calculation UI Element Repositioning emojiInput.position(10, 10); printButton.position(...);

Reposition the input and button elements to stay in the top-left corner

for-loop Game Object Y-Coordinate Updates for (let paper of papers) { ... }

Updates all papers, people, and graves to maintain proper ground level positions relative to the new canvas size

resizeCanvas(windowWidth, windowHeight);
p5.js function that resizes the canvas to match current window width and height
emojiInput.position(10, 10);
Reposition input field to the top-left to keep it visible after resize
printerY = height - WATER_LEVEL - 50;
Recalculates printer's y position based on the new canvas height
for (let paper of papers) { paper.groundY = height - WATER_LEVEL; }
Updates each paper's landing height (groundY) so papers still land at the water level after resize
if (person.state !== 'waitingToBeCaptured' && ...) { person.y = height - WATER_LEVEL; }
Updates standing people's y position, but skips those currently being captured (they're controlled by their captor)
for (let grave of graves) { grave.y = height - WATER_LEVEL; }
Updates grave positions to stay at the water level ground
updateTransportSelectionButtons();
Recalculates button positions in case the waiting person's screen position has changed

keyPressed()

keyPressed() is a p5.js built-in function called whenever any key is pressed. By checking keyCode against p5.js constants (like ENTER, LEFT_ARROW, etc.), you can trigger sketch behavior from the keyboard. This sketch allows both button clicks and Enter key presses to print, giving users flexibility in how they interact.

function keyPressed() {
  if (keyCode === ENTER) {
    triggerPrint();
  }
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional Enter Key Detection if (keyCode === ENTER) {

Checks if the key pressed was the Enter/Return key

if (keyCode === ENTER) {
ENTER is a p5.js constant representing the Return/Enter key. keyCode is the numerical code of the key pressed.
triggerPrint();
Calls triggerPrint(), treating the Enter key the same as clicking the Print button

📦 Key Variables

emojiInput p5.Renderer (HTML input element)

Stores the HTML input element for typing emojis. Users type here to choose what emoji to print.

let emojiInput = createInput('📄');
printButton p5.Renderer (HTML button element)

Stores the Print button element. Clicking it calls triggerPrint().

let printButton = createButton('Print');
papers array of Paper objects

Holds all active Paper objects. Each paper is created when the user prints an emoji.

let papers = [];
people array of Person objects

Holds all active Person objects. Each person walks toward a paper, grabs it, waits for transport selection, then is transported or escapes.

let people = [];
ufos array of UFO objects

Holds all active UFO objects (both red killer UFOs and blue abductor UFOs).

let ufos = [];
airplanes array of Airplane objects

Holds all active Airplane objects. Airplanes pick up people and crash.

let airplanes = [];
sharks array of Shark objects

Holds all active Shark objects. Sharks kill people in the water.

let sharks = [];
ambulances array of Ambulance objects

Holds all active Ambulance objects. Ambulances are spawned after shark kills and retrieve graves.

let ambulances = [];
graves array of Grave objects

Holds all active Grave objects. Graves are created when people die and display the person's emoji and name.

let graves = [];
printerX number

The horizontal position of the printer on the canvas (canvas center).

let printerX = width / 2;
printerY number

The vertical position of the printer on the canvas (above water level).

let printerY = height - WATER_LEVEL - 50;
spawnRedUFOButton p5.Renderer (HTML button element)

The Red UFO transport selection button. Initially hidden; shown above waiting people.

let spawnRedUFOButton = createButton('Red UFO');
spawnBlueUFOButton p5.Renderer (HTML button element)

The Blue UFO transport selection button. Initially hidden; shown above waiting people.

let spawnBlueUFOButton = createButton('Blue UFO');
spawnAirplaneButton p5.Renderer (HTML button element)

The Airplane transport selection button. Initially hidden; shown above waiting people.

let spawnAirplaneButton = createButton('Airplane');
spawnSharksButton p5.Renderer (HTML button element)

The Sharks transport selection button. Initially hidden; shown above waiting people.

let spawnSharksButton = createButton('Sharks');
waitingPeople array of Person objects

A queue of people waiting for the user to choose their transport. The first person in this array gets the visible buttons.

let waitingPeople = [];
names array of strings

A list of 26 random names (Alice, Bob, Charlie, etc.). Each newly spawned person gets a random name from this array.

const names = ['Alice', 'Bob', 'Charlie', ...];
WATER_LEVEL number (constant)

Height of the water from the bottom of the canvas in pixels. All ground-level objects (people, graves) position themselves relative to this.

const WATER_LEVEL = 150;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG Airplane display()

The condition `if (this.state === 'removed' || this.state === 'crashing')` prevents the crash fire animation from displaying because display() returns before drawing it. The fire is drawn but only briefly during the transition.

💡 Change the condition to only skip removed state: `if (this.state === 'removed') { return; }`. The crash fire needs to be visible, so don't exclude the 'crashing' state from display().

PERFORMANCE dispatchTransport()

Object pooling searches the entire ufos/airplanes/sharks array every frame looking for idle instances. With many objects, this is O(n) per dispatch.

💡 Maintain separate idle queues for each transport type (e.g., `idleUFOQueue = []`). Reuse becomes O(1). When an object finishes its task, push it into the idle queue instead of just setting state to 'idle'.

STYLE Person update()

The condition checking captured states is repeated five times across the code: in update() guard, display() guard, and dispatchTransport(). This violates DRY (Don't Repeat Yourself).

💡 Create a helper function like `isPersonCaptured(person)` that returns true if state is one of the capture states. Use it everywhere instead of repeating the full condition.

FEATURE sketch.js (global)

There is no visual feedback when the user clicks a transport button—no sound, no pause, no flash. The buttons silently disappear and something happens off-screen.

💡 Add a brief visual effect when a transport is dispatched: flash the screen, shake the canvas, or animate the person up/down. This would make the interaction feel more responsive and game-like.

BUG Shark update(), grave spawning

After a shark kills a person and spawns an ambulance, the shark immediately transitions to 'leaving'. If another person is waiting (waitingPeople.length > 0), they will not be assigned to this shark because it's no longer idle.

💡 After spawning the ambulance, recycle the shark by resetting it to 'idle' state so it can be dispatched again, or ensure new sharks are always created when needed. Currently, each shark is single-use.

STYLE Paper and Person display()

The paper is drawn inside person.display() by calling `this.targetPaper.display()`, but the paper's display() also checks for grabbed status. If paper.grabbed is true but person is not displaying it, the paper won't render anywhere.

💡 Simplify: always draw the paper in person.display() if hasPaper is true, without relying on the paper's grabbed flag. The flag should only control whether paper.display() is called from draw(), not from within person.

🔄 Code Flow

Code flow showing setup, draw, paper, person, ufo, airplane, shark, ambulance, grave, triggerpint, createpaperction, updatetransportselectbuttons, dispatchtransport, windowresized, keypressed

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] click setup href "#fn-setup" click draw href "#fn-draw" setup --> canvas-creation[Canvas Setup] setup --> input-field[Emoji Input Field] setup --> print-button[Print Button] setup --> transport-buttons[Transport Selection Buttons] setup --> printer-position[Printer Position] click canvas-creation href "#sub-canvas-creation" click input-field href "#sub-input-field" click print-button href "#sub-print-button" click transport-buttons href "#sub-transport-buttons" click printer-position href "#sub-printer-position" draw --> background-clear[Background Clear] draw --> water-draw[Water Level Rectangle] draw --> printer-draw[Printer Visual] draw --> papers-loop[Papers Update and Display] draw --> people-loop[People Update and Display] draw --> ufos-loop[UFOs Update and Display] draw --> airplanes-loop[Airplanes Update and Display] draw --> sharks-loop[Sharks Update and Display] draw --> ambulances-loop[Ambulances Update and Display] draw --> graves-loop[Graves Update and Display] draw --> button-update[Button Position Update] click background-clear href "#sub-background-clear" click water-draw href "#sub-water-draw" click printer-draw href "#sub-printer-draw" click papers-loop href "#sub-papers-loop" click people-loop href "#sub-people-loop" click ufos-loop href "#sub-ufos-loop" click airplanes-loop href "#sub-airplanes-loop" click sharks-loop href "#sub-sharks-loop" click ambulances-loop href "#sub-ambulances-loop" click graves-loop href "#sub-graves-loop" click button-update href "#sub-button-update" papers-loop --> paper-constructor[Paper Constructor] papers-loop --> paper-printing-animation[Printing Animation] click paper-constructor href "#sub-paper-constructor" click paper-printing-animation href "#sub-paper-printing-animation" people-loop --> person-constructor[Person Constructor] people-loop --> state-check[Capture State Guard] people-loop --> state-machine[Person State Machine] people-loop --> movement-logic[Movement Toward Printer] people-loop --> paper-carrying[Paper Carrying Display] click person-constructor href "#sub-person-constructor" click state-check href "#sub-state-check" click state-machine href "#sub-state-machine" click movement-logic href "#sub-movement-logic" click paper-carrying href "#sub-paper-carrying" ufos-loop --> ufo-constructor[UFO Constructor] ufos-loop --> ufo-movement[UFO Movement Using Angle] ufos-loop --> distance-check[Distance-Based Grab Trigger] ufos-loop --> visual-storage[Abducted Person Visual Storage] ufos-loop --> grab-counter[Grab Duration Counter] ufos-loop --> beam-display[Abduction Beam Visual] click ufo-constructor href "#sub-ufo-constructor" click ufo-movement href "#sub-ufo-movement" click distance-check href "#sub-distance-check" click visual-storage href "#sub-visual-storage" click grab-counter href "#sub-grab-counter" click beam-display href "#sub-beam-display" airplanes-loop --> airplane-constructor[Airplane Constructor] airplanes-loop --> pickup-lock[Pickup Position Lock] airplanes-loop --> crash-descent[Crash Descent Logic] airplanes-loop --> fire-animation[Crash Fire Visual] airplanes-loop --> person-inside-display[Person Inside Airplane] click airplane-constructor href "#sub-airplane-constructor" click pickup-lock href "#sub-pickup-lock" click crash-descent href "#sub-crash-descent" click fire-animation href "#sub-fire-animation" click person-inside-display href "#sub-person-inside-display" sharks-loop --> shark-constructor[Shark Constructor] sharks-loop --> shark-movement[Shark Movement Using Angle] sharks-loop --> killing-sequence[Killing and Ambulance Spawn] sharks-loop --> blood-animation[Blood Animation] sharks-loop --> shark-body-shape[Shark Body Using beginShape] click shark-constructor href "#sub-shark-constructor" click shark-movement href "#sub-shark-movement" click killing-sequence href "#sub-killing-sequence" click blood-animation href "#sub-blood-animation" click shark-body-shape href "#sub-shark-body-shape" ambulances-loop --> ambulance-constructor[Ambulance Constructor] ambulances-loop --> grave-movement[Grave Movement with Ambulance] ambulances-loop --> ambulance-driving[Ambulance Horizontal Movement] click ambulance-constructor href "#sub-ambulance-constructor" click grave-movement href "#sub-grave-movement" click ambulance-driving href "#sub-ambulance-driving" graves-loop --> grave-lifetime[Grave Lifetime Counter] graves-loop --> grave-animation[Grave Walking Off Animation] graves-loop --> grave-visual[Grave Tombstone Shape] click grave-lifetime href "#sub-grave-lifetime" click grave-animation href "#sub-grave-animation" click grave-visual href "#sub-grave-visual" draw --> updatetransportselectbuttons[updateTransportSelectionButtons] click updatetransportselectbuttons href "#fn-updatetransportselectbuttons" triggerpint --> emoji-retrieval[Emoji Input Value Retrieval] triggerpint --> length-check[Non-Empty Validation] triggerpint --> createpaperction[createPaperAction] click triggerpint href "#fn-triggerpint" click createpaperction href "#fn-createpaperction" click emoji-retrieval href "#sub-emoji-retrieval" click length-check href "#sub-length-check" createpaperction --> paper-creation[Paper Object Creation] createpaperction --> person-creation[Person Object Creation] createpaperction --> link-establishment[Paper-Person Link Establishment] click paper-creation href "#sub-paper-creation" click person-creation href "#sub-person-creation" click link-establishment href "#sub-link-establishment" dispatchtransport --> queue-check[Queue Existence Check] dispatchtransport --> queue-removal[Person Dequeuing] dispatchtransport --> ufo-dispatch[UFO Dispatch Logic] dispatchtransport --> airplane-dispatch[Airplane Dispatch Logic] dispatchtransport --> shark-dispatch[Shark Dispatch Logic] click dispatchtransport href "#fn-dispatchtransport" click queue-check href "#sub-queue-check" click queue-removal href "#sub-queue-removal" click ufo-dispatch href "#sub-ufo-dispatch" click airplane-dispatch href "#sub-airplane-dispatch" click shark-dispatch href "#sub-shark-dispatch" windowresized --> canvas-resize[Canvas Resize] windowresized --> ui-reposition[UI Element Repositioning] windowresized --> game-object-update[Game Object Y-Coordinate Updates] click windowresized href "#fn-windowresized" click canvas-resize href "#sub-canvas-resize" click ui-reposition href "#sub-ui-reposition" click game-object-update href "#sub-game-object-update" keypressed --> enter-key-check[Enter Key Detection] click keypressed href "#fn-keypressed" click enter-key-check href "#sub-enter-key-check"

❓ Frequently Asked Questions

What visual elements can users expect to see in the Gooooooooooooood p5.js sketch?

The sketch features various animated objects including UFOs, airplanes, sharks, and people, all interacting within a colorful canvas.

How can users engage with the Gooooooooooooood sketch for an interactive experience?

Users can input emojis and click buttons to spawn different transport options like UFOs and airplanes, adding a playful element to the sketch.

What creative coding concepts are showcased in the Gooooooooooooood sketch?

This sketch demonstrates object-oriented programming by using arrays to manage different types of animated entities and their interactions on the canvas.

Preview

Gooooooooooooood - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Gooooooooooooood - Code flow showing setup, draw, paper, person, ufo, airplane, shark, ambulance, grave, triggerpint, createpaperction, updatetransportselectbuttons, dispatchtransport, windowresized, keypressed
Code Flow Diagram