ok it’s fix ok

This interactive p5.js sketch creates a chaotic simulation where you tap an orange to spawn people who eat it, then defend themselves against zombies, vans, and helicopters using game state management and object-oriented programming. The sketch layers multiple autonomous agents (people, zombies, vans, police helicopters, and civilian helicopters) that interact dynamically, with houses that can break and game states that shift based on what's happening on screen.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the orange bigger — Lower the divisor in orangeSize to make the orange take up more of the canvas—easier target for people and more dramatic.
  2. Slow down zombies — Increase the zombie speed divisor to make them move slower—gives people more escape time.
  3. Make people eat faster — Lower the eating duration so people finish eating and return home more quickly.
  4. Add more houses — Increase numHouses to create a denser, more crowded cityscape with more shelter.
  5. Change zombie color — Alter the zombie's RGB color—try (255, 0, 0) for red zombies or (150, 0, 150) for purple.
  6. Faster vans — Lower the van speed divisor so vans catch people more easily.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch is a playable interactive simulation where tapping the orange spawns people who eat it, triggering a cascade of chaos. Zombies hunt people and break houses, vans abduct civilians (prompting police helicopters to chase), and civilian helicopters get intercepted by police cars. The visual appeal comes from dynamic agent behavior, state transitions, and emergent interactions—all powered by ES6 classes (Person, Zombie, Van, PoliceHelicopter, CivilianHelicopter, PoliceCar), game state constants, and array filtering to track multiple active entities.

The code is organized into a single draw loop managing four main arrays (people, zombies, vans, helicopters, civilianHelicopters, policeCars), each updated and rendered every frame. You will learn how to design independent agent classes with state machines, coordinate interactions between different entity types (vans triggering helicopter spawns, helicopters triggering police car spawns), manage multiple game states, and use p5.js buttons for responsive event handling. This is a masterclass in structuring complex interactive systems where many objects act concurrently.

⚙️ How It Works

  1. When the sketch loads, setup() creates a responsive canvas and four control buttons (Orange, Zombie, Van, Heli), then calls resetGameElements() to generate a random layout of 10 colorful houses and place the orange at a random position. The game starts in FIND_ORANGE state with only the Orange button visible.
  2. Tapping the orange or the Orange button calls callPeople(), which spawns a Person from a random unbroken house's roof and sets gameState to EAT_ORANGE, showing the Zombie, Van, and Heli buttons. Each tap spawns another person.
  3. In draw(), every frame updates all active people, zombies, vans, and helicopters—each moving toward their target, changing states when reaching destinations, and triggering actions (eating, hunting, breaking, grabbing, leaving).
  4. Pressing the Zombie button spawns a Zombie at a random canvas edge that hunts available people; if it reaches one, the person dies and the zombie hunts the next target or breaks an unbroken house. After breaking a house or killing all people, the zombie leaves off-screen.
  5. Pressing the Van button spawns a Van that hunts a person, grabs them (making them isGrabbed=true so they freeze), then leaves—triggering a PoliceHelicopter to spawn and chase it. When the helicopter shoots the van, the grabbed person is released.
  6. Pressing the Heli button spawns a CivilianHelicopter that hunts and grabs people, then leaves—triggering a PoliceCar to spawn and chase it. When the police car shoots the helicopter, the grabbed person dies.
  7. The game transitions through states (FIND_ORANGE → EAT_ORANGE → RETURN_HOME, or ZOMBIE_ATTACK if a zombie spawns). If all houses break, gameState becomes ALL_HOUSES_BROKEN and people spawn from canvas edges, creating a desperate endgame.

🎓 Concepts You'll Learn

Game state machineObject-oriented design with ES6 classesMultiple autonomous agentsState transitions and event handlingArray filtering and dynamic entity managementCollision detection and targetingResponsive canvas and window resizingButton event handling (mousePressed and touchStarted)

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas and buttons, then calls resetGameElements() to set up the initial game state. Notice how each button has both mousePressed and touchStarted handlers—this dual approach ensures the sketch works on desktop and mobile devices.

function setup() {
  // Create a canvas that fills the entire window
  createCanvas(windowWidth, windowHeight);
  
  // Create the "Orange" button
  orangeButton = createButton('Orange');
  orangeButton.position(width - 120, 10); // Position it in the top-right
  orangeButton.style('font-size', '16px');
  orangeButton.style('padding', '10px 15px');
  orangeButton.style('background-color', '#4CAF50');
  orangeButton.style('color', 'white');
  orangeButton.style('border', 'none');
  orangeButton.style('border-radius', '5px');
  orangeButton.style('cursor', 'pointer');
  orangeButton.mousePressed(callPeople); // Attach the function to mousePressed event
  orangeButton.touchStarted(function() { // Attach touchStarted handler directly
    callPeople();
    return false; // Prevent default browser behavior (like scrolling)
  });

  // Create the "Zombie" button
  zombieButton = createButton('Zombie');
  zombieButton.position(width - 200, 10); // Position it to the left of the Orange button
  zombieButton.style('font-size', '16px');
  zombieButton.style('padding', '10px 15px');
  zombieButton.style('background-color', '#FF0000'); // Red color for zombie button
  zombieButton.style('color', 'white');
  zombieButton.style('border', 'none');
  zombieButton.style('border-radius', '5px');
  zombieButton.style('cursor', 'pointer');
  zombieButton.mousePressed(spawnZombie); // Attach the function to mousePressed event
  zombieButton.touchStarted(function() { // Attach touchStarted handler directly
    spawnZombie();
    return false;
  });

  // NEW: Create the "Van" button
  vanButton = createButton('Van');
  vanButton.position(width - 280, 10); // Position it to the left of the Zombie button
  vanButton.style('font-size', '16px');
  vanButton.style('padding', '10px 15px');
  vanButton.style('background-color', '#007bff'); // Blue color for van button
  vanButton.style('color', 'white');
  vanButton.style('border', 'none');
  vanButton.style('border-radius', '5px');
  vanButton.style('cursor', 'pointer');
  vanButton.mousePressed(spawnVan); // Attach the function to mousePressed event
  vanButton.touchStarted(function() { // Attach touchStarted handler directly
    spawnVan();
    return false;
  });

  // NEW: Create the "Helicopter" button
  heliButton = createButton('Heli');
  heliButton.position(width - 360, 10); // Position it to the left of the Van button
  heliButton.style('font-size', '16px');
  heliButton.style('padding', '10px 15px');
  heliButton.style('background-color', '#6c757d'); // Gray color for heli button
  heliButton.style('color', 'white');
  heliButton.style('border', 'none');
  heliButton.style('border-radius', '5px');
  heliButton.style('cursor', 'pointer');
  heliButton.mousePressed(spawnCivilianHelicopter); // Attach the function to mousePressed event
  heliButton.touchStarted(function() { // Attach touchStarted handler directly
    spawnCivilianHelicopter();
    return false;
  });

  // Reset all game elements
  resetGameElements();
}
Line-by-line explanation (6 lines)
createCanvas(windowWidth, windowHeight);
Creates a full-screen canvas that matches the browser window size, allowing the sketch to be responsive.
orangeButton = createButton('Orange');
Creates a button labeled 'Orange' and stores it in the global orangeButton variable so it can be reused later.
orangeButton.position(width - 120, 10);
Positions the Orange button 120 pixels from the right edge and 10 pixels from the top, placing it in the top-right corner.
orangeButton.mousePressed(callPeople);
Attaches the callPeople() function to the button's mouse click event—when clicked, it spawns a new person.
orangeButton.touchStarted(function() { callPeople(); return false; });
Adds touch support so the button works on mobile devices; returning false prevents default browser scrolling behavior.
resetGameElements();
Calls the function that initializes all game elements (houses, orange position, game state) before the draw loop starts.

resetGameElements()

resetGameElements() is called when the sketch starts and whenever a round ends (all people return home or die). It regenerates the houses, repositions the orange, clears all active entities, and resets the game state to FIND_ORANGE. This is the 'reset button' that creates a fresh puzzle each time you play.

🔬 This loop creates numHouses with random widths. What happens if you change random(width / 30, width / 15) to random(width / 20, width / 10)? Will houses be wider or narrower?

  for (let i = 0; i < numHouses; i++) {
    let houseWidth = random(width / 30, width / 15);
function resetGameElements() {
  // Set the background to a grassy green color
  background(100, 150, 50); 

  // Reset orange tap count and position
  orangeTapCount = 0; // Reset tap count for new game
  orangeSize = min(width, height) / 75; // Recalculate orange size (made it bigger!)
  orangePos = createVector(random(orangeSize, width - orangeSize), random(orangeSize, height - orangeSize));

  // Clear existing houses, people, zombies, vans, and helicopters
  houses = [];
  people = [];
  zombies = []; // Clear zombies array
  vans = []; // NEW: Clear vans array
  helicopters = []; // NEW: Clear police helicopters array
  civilianHelicopters = []; // NEW: Clear civilian helicopters array
  policeCars = []; // NEW: Clear police cars array

  // Generate data for houses
  for (let i = 0; i < numHouses; i++) {
    let houseWidth = random(width / 30, width / 15);
    let houseHeight = random(height / 20, height / 10);
    let houseX = random(houseWidth / 2, width - houseWidth / 2);
    let houseY = random(houseHeight / 2, height - houseHeight / 2);
    let houseColor = color(random(100, 200), random(100, 200), random(100, 200)); 
    let roofColor = color(random(150, 250), random(50, 150), random(50, 150)); 

    houses.push({
      x: houseX,
      y: houseY,
      width: houseWidth,
      height: houseHeight,
      color: houseColor,
      roofColor: roofColor,
      homePos: createVector(houseX, houseY), // Store house center as home position for people
      isBroken: false // Initialize house as not broken
    });
  }

  // Set the game state back to finding the orange
  gameState = FIND_ORANGE;
  orangeButton.show(); // Show the button when in FIND_ORANGE state
  zombieButton.hide(); // Zombie button should be hidden initially
  vanButton.hide(); // NEW: Van button should be hidden initially
  heliButton.hide(); // NEW: Heli button should be hidden initially
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop House Generation Loop for (let i = 0; i < numHouses; i++) {

Loops through numHouses (10) times to generate random house objects with unique positions, sizes, and colors.

calculation Array Initialization houses = []; people = []; zombies = [];

Empties all entity arrays to reset the game state for a fresh round.

background(100, 150, 50);
Sets the background to a grassy green color—this is the sky/ground the sketch draws on.
orangeSize = min(width, height) / 75;
Calculates the orange's diameter as 1/75th of the smaller canvas dimension, ensuring it scales responsively with window size.
orangePos = createVector(random(orangeSize, width - orangeSize), random(orangeSize, height - orangeSize));
Places the orange at a random position within the canvas, ensuring it doesn't spawn partially off-screen.
for (let i = 0; i < numHouses; i++) {
Loops 10 times (numHouses) to create 10 house objects with unique properties.
let houseColor = color(random(100, 200), random(100, 200), random(100, 200));
Generates a random RGB color for the house's walls by picking three random values between 100 and 200.
houses.push({...});
Adds the completed house object to the houses array so it can be drawn and interacted with in the game.
gameState = FIND_ORANGE;
Sets the game state back to the starting state, waiting for the player to tap the orange.

draw()

draw() is the heartbeat of the sketch—it runs 60 times per second. Each frame, it clears the canvas, redraws all static objects (houses, orange), updates and displays all dynamic entities (people, zombies, vans, helicopters), and manages game state transitions. The backward loop pattern (for (let i = array.length - 1; i >= 0; i--)) is crucial when removing elements mid-loop to avoid skipping items.

🔬 This loop updates and draws every zombie. Why do you think it counts backward (i--) instead of forward (i++)? Hint: what happens when you call zombies.splice(i, 1)?

  for (let i = zombies.length - 1; i >= 0; i--) {
    let z = zombies[i];
    z.update();
    z.display();

🔬 This line removes dead people in one go using .filter(). What happens if you comment it out (add // before it)? Dead people might linger—where would you see them?

  // Filter out dead people before updating/displaying
  people = people.filter(person => person.state !== PERSON_DEAD);
function draw() {
  // Redraw the background and houses every frame
  background(100, 150, 50); 
  noStroke(); 

  for (let house of houses) {
    if (house.isBroken) {
      fill(150, 100, 50); // Broken house color (e.g., brown)
      rectMode(CENTER); 
      rect(house.x, house.y, house.width * 0.9, house.height * 0.9); // Slightly smaller
      fill(100, 70, 30); // Broken roof color
      triangle(
        house.x - house.width / 2 * 0.9, house.y - house.height / 2 * 0.9, 
        house.x + house.width / 2 * 0.9, house.y - house.height / 2 * 0.9, 
        house.x, house.y - house.height / 2 * 0.9 - house.height / 3 * 0.9 
      );
      // Optional: add some "damage" lines
      stroke(50);
      line(house.x - house.width/4, house.y - house.height/4, house.x + house.width/4, house.y + house.height/4);
      line(house.x + house.width/4, house.y - house.height/4, house.x - house.width/4, house.y + house.height/4);
      noStroke();
    } else {
      fill(house.color);
      rectMode(CENTER); 
      rect(house.x, house.y, house.width, house.height);

      fill(house.roofColor);
      triangle(
        house.x - house.width / 2, house.y - house.height / 2, 
        house.x + house.width / 2, house.y - house.height / 2, 
        house.x, house.y - house.height / 2 - house.height / 3 
      );
    }
  }

  // Always draw the orange
  fill(255, 165, 0); // Orange color
  noStroke(); 
  ellipse(orangePos.x, orangePos.y, orangeSize, orangeSize); 
  

  // Filter out dead people before updating/displaying
  people = people.filter(person => person.state !== PERSON_DEAD);

  // Handle people movement based on game state
  if (gameState === EAT_ORANGE || gameState === RETURN_HOME || gameState === ZOMBIE_ATTACK || gameState === ALL_HOUSES_BROKEN) { // Allow people to update in ALL_HOUSES_BROKEN state
    for (let person of people) {
      person.update(); // Update person's position and state
      person.display(); // Draw the person
    }
  }

  // Update and display multiple zombies
  for (let i = zombies.length - 1; i >= 0; i--) {
    let z = zombies[i];
    z.update();
    z.display();
    
    // If zombie is in leaving state and has moved off-screen, remove it
    if (z.state === ZOMBIE_LEAVING) {
      let d = dist(z.pos.x, z.pos.y, z.target.x, z.target.y);
      if (d < z.speed) { // Has reached off-screen target
        zombies.splice(i, 1); // Remove this zombie from the array
      }
    }
  }

  // NEW: Update and display multiple vans
  for (let i = vans.length - 1; i >= 0; i--) {
    let v = vans[i];
    v.update();
    v.display();
    
    // Splicing logic for vans. They now leave.
    if (v.isDestroyed || (v.state === Van.VAN_LEAVING && dist(v.pos.x, v.pos.y, v.target.x, v.target.y) < v.speed)) {
      vans.splice(i, 1); // Remove this van from the array
    }
  }

  // NEW: Update and display multiple helicopters
  for (let i = helicopters.length - 1; i >= 0; i--) {
    let h = helicopters[i];
    h.update();
    h.display();
    
    // Splicing logic for police helicopters. They now leave.
    if (h.state === PoliceHelicopter.HELICOPTER_LEAVING && dist(h.pos.x, h.pos.y, h.target.x, h.target.y) < h.speed) {
      helicopters.splice(i, 1); // Remove this helicopter from the array
    }
  }

  // NEW: Update and display multiple civilian helicopters
  for (let i = civilianHelicopters.length - 1; i >= 0; i--) {
    let ch = civilianHelicopters[i];
    ch.update();
    ch.display();
    
    // Splicing logic for civilian helicopters. They now leave.
    if (ch.isDestroyed || (ch.state === CivilianHelicopter.HELICOPTER_LEAVING && dist(ch.pos.x, ch.pos.y, ch.target.x, ch.target.y) < ch.speed)) {
      civilianHelicopters.splice(i, 1); // Remove this helicopter from the array
    }
  }

  // NEW: Update and display multiple police cars
  for (let i = policeCars.length - 1; i >= 0; i--) {
    let pc = policeCars[i];
    pc.update();
    pc.display();
    
    // Splicing logic for police cars. They now leave.
    if (pc.state === PoliceCar.POLICECAR_LEAVING && dist(pc.pos.x, pc.pos.y, pc.target.x, pc.target.y) < pc.speed) {
      policeCars.splice(i, 1); // Remove this police car from the array
    }
  }


  // NEW GAME LOGIC: Check if all houses are broken
  let allHousesBroken = houses.every(house => house.isBroken);

  if (allHousesBroken) {
    // If all houses are broken, transition to ALL_HOUSES_BROKEN state
    if (gameState !== ALL_HOUSES_BROKEN) {
      gameState = ALL_HOUSES_BROKEN;
      // Buttons remain visible as per user request
    }
    // People who are currently out will continue their path and get stuck if their house is broken.
    // The game will not reset automatically in this state.
  } else {
    // Original reset logic, but only if not in ALL_HOUSES_BROKEN state
    if (people.length === 0 && zombies.length === 0 && vans.length === 0 && helicopters.length === 0 && civilianHelicopters.length === 0 && policeCars.length === 0 && gameState !== FIND_ORANGE && gameState !== ALL_HOUSES_BROKEN) { // NEW: Include civilianHelicopters and policeCars
      resetGameElements();
    }
  }

  // If zombies are gone but people are still returning, transition game state
  // Only transition if not in ALL_HOUSES_BROKEN state
  if (gameState === ZOMBIE_ATTACK && zombies.length === 0 && !allHousesBroken) {
    gameState = RETURN_HOME;
  }
  
  // Display instructions or game state
  fill(255);
  textSize(18);
  textAlign(CENTER, TOP);
  if (gameState === FIND_ORANGE) {
    text("Tap orange/Orange button to call people! (Press Van/Zombie/Heli button to activate)", width / 2, 10); // NEW: Update text
    orangeButton.show(); // Ensure orange button is shown
    zombieButton.hide(); // Zombie button should be hidden initially
    vanButton.hide(); // NEW: Van button should be hidden initially
    heliButton.hide(); // NEW: Heli button should be hidden initially
  } else if (gameState === EAT_ORANGE) {
    text(people.length + " people eating! Press Zombie/Van/Heli to attack/abduct!", width / 2, 10); // NEW: Update text
    orangeButton.show(); // Ensure orange button is shown
    zombieButton.show(); // Show Zombie button when people are eating
    vanButton.show(); // Show Van button when people are eating
    heliButton.show(); // NEW: Show Heli button when people are eating
  } else if (gameState === RETURN_HOME) {
    text(people.length + " people returning home. New game starting soon!", width / 2, 10);
    orangeButton.show(); // Show orange button even when people are returning
    zombieButton.show(); // Show Zombie button even when people are returning
    vanButton.show(); // Show Van button even when people are returning
    heliButton.show(); // NEW: Show Heli button even when people are returning
  } else if (gameState === ZOMBIE_ATTACK) {
    let zombieTargetText = "";
    if (zombies.length > 0) {
      let firstZombie = zombies[0]; // Just use the first zombie for instructional text
      if (firstZombie.state === ZOMBIE_HUNTING_PERSON) {
        zombieTargetText = "Zombie hunting a person!";
      } else if (firstZombie.state === ZOMBIE_HUNTING_HOUSE || firstZombie.state === ZOMBIE_BREAKING_HOUSE) {
        zombieTargetText = "Zombie breaking a house!";
      } else {
        zombieTargetText = "Zombies attacking!";
      }
    } else {
      zombieTargetText = "Zombies attacking!"; // Default if zombies array somehow empty
    }
    text(zombieTargetText + " People returning home. " + zombies.length + " zombies active. Van/Helicopter active: " + (vans.length + helicopters.length + civilianHelicopters.length + policeCars.length) , width / 2, 10); // NEW: Update text
    orangeButton.show(); // Ensure orange button is shown
    zombieButton.show(); // Keep Zombie button visible during an active attack to spam more
    vanButton.show(); // Keep Van button visible during an active attack to spam more
    heliButton.show(); // NEW: Keep Heli button visible during an active attack to spam more
  } else if (gameState === ALL_HOUSES_BROKEN) { // Text for all houses broken state
    text("All houses are broken! No place to hide. People remain outside. Van/Zombie/Heli/Car active: " + (vans.length + zombies.length + helicopters.length + civilianHelicopters.length + policeCars.length), width / 2, 10); // NEW: Update text
    orangeButton.show(); // Ensure orange button is visible
    zombieButton.show(); // Ensure zombie button is visible
    vanButton.show(); // Ensure van button is visible
    heliButton.show(); // NEW: Ensure heli button is visible
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop House Rendering Loop for (let house of houses) {

Iterates through all houses and draws them as colored rectangles with roofs, or darker broken versions if isBroken is true.

calculation Dead Person Removal people = people.filter(person => person.state !== PERSON_DEAD);

Removes people with state PERSON_DEAD from the people array so dead bodies don't linger or cause errors.

for-loop Person Update and Display for (let person of people) { person.update(); person.display(); }

Updates each person's position/state and draws them on screen if the game state allows movement.

for-loop Zombie Update and Removal for (let i = zombies.length - 1; i >= 0; i--) {

Loops backward through zombies, updates them, and removes zombies that have left the screen (to clean up the array).

for-loop Van Update and Removal for (let i = vans.length - 1; i >= 0; i--) {

Loops backward through vans, updates them, and removes destroyed or off-screen vans.

for-loop Police Helicopter Update and Removal for (let i = helicopters.length - 1; i >= 0; i--) {

Loops backward through police helicopters, updates them, and removes helicopters that have left.

for-loop Civilian Helicopter Update and Removal for (let i = civilianHelicopters.length - 1; i >= 0; i--) {

Loops backward through civilian helicopters, updates them, and removes destroyed or off-screen helicopters.

for-loop Police Car Update and Removal for (let i = policeCars.length - 1; i >= 0; i--) {

Loops backward through police cars, updates them, and removes cars that have left.

conditional All Houses Broken Logic let allHousesBroken = houses.every(house => house.isBroken);

Checks if every house has isBroken=true; if so, transitions to ALL_HOUSES_BROKEN state and prevents auto-reset.

conditional Auto-Reset Condition if (people.length === 0 && zombies.length === 0 && ...) { resetGameElements(); }

Automatically starts a new game when all entities are gone and the game isn't in FIND_ORANGE or ALL_HOUSES_BROKEN state.

conditional Zombie Attack Recovery if (gameState === ZOMBIE_ATTACK && zombies.length === 0 && !allHousesBroken) { gameState = RETURN_HOME; }

Transitions from ZOMBIE_ATTACK back to RETURN_HOME once all zombies are defeated and not all houses are broken.

conditional Game State Text and Button Visibility if (gameState === FIND_ORANGE) { ... } else if (gameState === EAT_ORANGE) { ... }

Displays different instructions and shows/hides buttons based on the current game state.

background(100, 150, 50);
Clears the canvas with a grassy green color every frame, ensuring smooth animation without trails.
for (let house of houses) {
Iterates through every house object to render them—unbroken houses are colorful, broken houses are brown with damage lines.
ellipse(orangePos.x, orangePos.y, orangeSize, orangeSize);
Draws the orange as a circle at its current position; this is always visible and the target for people to eat.
people = people.filter(person => person.state !== PERSON_DEAD);
Removes any dead people from the array so they don't update or display anymore—keeps the array clean.
if (gameState === EAT_ORANGE || gameState === RETURN_HOME || gameState === ZOMBIE_ATTACK || gameState === ALL_HOUSES_BROKEN) {
Only updates and displays people if the game state permits movement (not in FIND_ORANGE when people are idle inside houses).
for (let i = zombies.length - 1; i >= 0; i--) {
Loops backward through the zombies array so removing an element (splice) doesn't skip the next zombie.
if (z.state === ZOMBIE_LEAVING) { let d = dist(z.pos.x, z.pos.y, z.target.x, z.target.y); if (d < z.speed) { zombies.splice(i, 1); } }
Checks if the zombie has reached its off-screen target; if so, removes it from the array to clean up memory.
let allHousesBroken = houses.every(house => house.isBroken);
Uses the array method .every() to return true only if ALL houses are broken; a compact way to check a condition across all items.
if (people.length === 0 && zombies.length === 0 && ... && gameState !== FIND_ORANGE && gameState !== ALL_HOUSES_BROKEN) { resetGameElements(); }
Automatically resets the game when all active entities are gone and not already in a starting or endgame state.

callPeople()

callPeople() is the entry point for spawning a person—it's called by the Orange button, canvas orange taps, and any touch event on the button. It handles two scenarios: normal play (spawn from unbroken houses) and endgame (spawn from canvas edges when all houses are broken). Notice how it filters arrays (unbrokenHouses) and transitions game state—both key patterns in this sketch.

🔬 This code filters for unbroken houses then picks one. What happens if you change it to spawn from ANY house (broken or not)? Remove the filter and see if people spawn from broken houses too.

    let unbrokenHouses = houses.filter(h => !h.isBroken);
    if (unbrokenHouses.length > 0) {
      let randomHouse = random(unbrokenHouses);
function callPeople() {
  if (gameState === ALL_HOUSES_BROKEN) {
    // If all houses are broken, spawn person from a random edge, immediately vulnerable
    let spawnPos;
    let edge = floor(random(4)); // 0: top, 1: right, 2: bottom, 3: left
    if (edge === 0) spawnPos = createVector(random(width), -orangeSize);
    else if (edge === 1) spawnPos = createVector(width + orangeSize, random(height));
    else if (edge === 2) spawnPos = createVector(random(width), height + orangeSize);
    else spawnPos = createVector(-orangeSize, random(height));

    // Homeless person - start them going to the orange
    // They will go to the orange, eat, then try to return "home" but get stuck at their spawn point
    // (which acts as their outsideHomePos) since there's no house to enter.
    people.push(new Person(null, spawnPos)); // Pass null for homeHouse, spawnPos for outsideHomePos
  } else {
    // Original logic: spawn from an unbroken house
    let unbrokenHouses = houses.filter(h => !h.isBroken);
    if (unbrokenHouses.length > 0) {
      let randomHouse = random(unbrokenHouses); 
      let outsideHomePos = createVector(randomHouse.x, randomHouse.y - randomHouse.height / 2 - randomHouse.height / 3 * 0.5);
      people.push(new Person(randomHouse, outsideHomePos)); // Pass the house object
    } else {
      console.warn("No unbroken houses left to call people from.");
    }
  }

  // Change game state once the first person is spawned
  if (gameState === FIND_ORANGE) {
    gameState = EAT_ORANGE; 
    zombieButton.show(); // Show the Zombie button when people are eating
    vanButton.show(); // NEW: Show the Van button when people are eating
    heliButton.show(); // NEW: Show the Heli button when people are eating
  }
  
  // Increment tap count (which now represents people spawned)
  orangeTapCount++; 
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional All Houses Broken Branch if (gameState === ALL_HOUSES_BROKEN) { let spawnPos; let edge = floor(random(4));

Spawns a homeless person from a random canvas edge when all houses are destroyed.

conditional Normal House Spawn let unbrokenHouses = houses.filter(h => !h.isBroken);

Finds all unbroken houses and picks one at random to spawn a person from its roof.

conditional Game State Transition if (gameState === FIND_ORANGE) { gameState = EAT_ORANGE;

Switches from FIND_ORANGE to EAT_ORANGE and shows the attack buttons when the first person is spawned.

if (gameState === ALL_HOUSES_BROKEN) {
Checks if all houses are destroyed; if so, people spawn from canvas edges as homeless persons.
let edge = floor(random(4)); // 0: top, 1: right, 2: bottom, 3: left
Randomly picks a canvas edge (0-3) where the person will spawn—creates the illusion they're arriving from outside.
if (edge === 0) spawnPos = createVector(random(width), -orangeSize);
If top edge is chosen, the person spawns above the canvas at a random x position and a y position just off-screen.
let unbrokenHouses = houses.filter(h => !h.isBroken);
Filters the houses array to keep only unbroken houses—these are the safe places to spawn people from.
let randomHouse = random(unbrokenHouses);
Picks a random unbroken house from the filtered array—people always start inside safe houses.
let outsideHomePos = createVector(randomHouse.x, randomHouse.y - randomHouse.height / 2 - randomHouse.height / 3 * 0.5);
Calculates the position at the top of the house's roof (where people exit to start their journey).
people.push(new Person(randomHouse, outsideHomePos));
Creates a new Person object, passes the house they belong to and their roof-exit position, and adds them to the people array.
if (gameState === FIND_ORANGE) { gameState = EAT_ORANGE;
When the first person is spawned, transition from waiting (FIND_ORANGE) to eating (EAT_ORANGE) and reveal attack buttons.

spawnZombie()

spawnZombie() is called by the Zombie button. It validates the game state, filters available targets, and only spawns a zombie if a valid person is outside. This pattern—check state, filter targets, validate, then spawn—is repeated in spawnVan() and spawnCivilianHelicopter(). Understanding this function teaches you how to gate spawning events behind multiple conditions to prevent errors and invalid game situations.

function spawnZombie() {
  // Zombies can spawn in EAT_ORANGE, RETURN_HOME, ZOMBIE_ATTACK, or ALL_HOUSES_BROKEN states
  if (gameState === EAT_ORANGE || gameState === RETURN_HOME || gameState === ZOMBIE_ATTACK || gameState === ALL_HOUSES_BROKEN) {
    // Filter for available people who are not dead, not grabbed by a van, and are outside a house.
    let availablePeople = people.filter(p => !p.isDead && !p.isGrabbed && (p.state === PERSON_COMING_OUT || p.state === PERSON_GOING_TO_ORANGE || p.state === PERSON_EATING || p.state === PERSON_GOING_HOME));

    let targetPerson = null;
    if (availablePeople.length > 0) {
      targetPerson = random(availablePeople);
    }

    // A zombie will ONLY spawn if there is an available person to hunt.
    // If no person is available, the zombie does not spawn at all, even if there are unbroken houses.
    if (targetPerson) {
      // Spawn zombie at a random edge
      let spawnPos;
      let edge = floor(random(4)); // 0: top, 1: right, 2: bottom, 3: left
      if (edge === 0) spawnPos = createVector(random(width), -orangeSize);
      else if (edge === 1) spawnPos = createVector(width + orangeSize, random(height));
      else if (edge === 2) spawnPos = createVector(random(width), height + orangeSize);
      else spawnPos = createVector(-orangeSize, random(height));

      let newZombie = new Zombie(spawnPos);
      newZombie.setTargetPerson(targetPerson); // Strictly target a person on spawn
      zombies.push(newZombie); // Add new zombie to the array
      gameState = ZOMBIE_ATTACK; // Change game state to reflect zombie attack
      // zombieButton.hide(); // Removed: Button remains visible to spam more zombies
    } else {
      console.warn("No people outside houses to hunt. Zombie not spawned.");
      // Optional: Provide visual feedback to the user that no zombie spawned
      fill(255, 0, 0);
      textSize(20);
      textAlign(CENTER, CENTER);
      text("No people out! Zombies won't spawn.", width / 2, height / 2);
    }
  } else {
    // Optionally provide feedback if zombie cannot be spawned
    if (gameState === FIND_ORANGE) {
      console.warn("Call some people first!");
      fill(255, 0, 0);
      textSize(20);
      textAlign(CENTER, CENTER);
      text("Call people first!", width / 2, height / 2);
    }
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Valid Game State Check if (gameState === EAT_ORANGE || gameState === RETURN_HOME || gameState === ZOMBIE_ATTACK || gameState === ALL_HOUSES_BROKEN) {

Only allows zombie spawning during active gameplay (not during FIND_ORANGE when people are hiding).

calculation Available People Filter let availablePeople = people.filter(p => !p.isDead && !p.isGrabbed && (p.state === PERSON_COMING_OUT || ...));

Filters people to find only those outside houses, alive, and not grabbed—valid zombie targets.

conditional Target Person Selection let targetPerson = null; if (availablePeople.length > 0) { targetPerson = random(availablePeople); }

Randomly picks a valid target from available people; remains null if none available.

conditional Zombie Spawn Validation if (targetPerson) { ... } else { ... }

Only spawns the zombie if a valid target exists; displays error feedback if no people are available.

if (gameState === EAT_ORANGE || gameState === RETURN_HOME || gameState === ZOMBIE_ATTACK || gameState === ALL_HOUSES_BROKEN) {
Checks if the game state allows zombie spawning (people must be outside to hunt); zombies cannot spawn during FIND_ORANGE.
let availablePeople = people.filter(p => !p.isDead && !p.isGrabbed && (p.state === PERSON_COMING_OUT || p.state === PERSON_GOING_TO_ORANGE || p.state === PERSON_EATING || p.state === PERSON_GOING_HOME));
Filters the people array to find only those who are outside (not in PERSON_IDLE), alive, and not grabbed—valid targets for zombies.
if (targetPerson) { let newZombie = new Zombie(spawnPos); newZombie.setTargetPerson(targetPerson); zombies.push(newZombie); gameState = ZOMBIE_ATTACK;
If a valid target exists, create a new Zombie, assign it to hunt that person, add it to the zombies array, and change game state to ZOMBIE_ATTACK.
} else { console.warn("No people outside houses to hunt. Zombie not spawned."); fill(255, 0, 0); text("No people out! Zombies won't spawn.", width / 2, height / 2); }
If no valid target exists, log a warning and display red text telling the player why the zombie didn't spawn—helpful feedback.

spawnVan()

spawnVan() mirrors spawnZombie() but spawns a Van instead. The key difference: vans don't change the game state—they're 'side stories' that happen in parallel. When a van abducts a person, it automatically triggers a police helicopter spawn via spawnHelicopter(). This cascade of spawning events creates emergent complexity.

function spawnVan() {
  // Vans can spawn in EAT_ORANGE, RETURN_HOME, ZOMBIE_ATTACK, or ALL_HOUSES_BROKEN states
  if (gameState === EAT_ORANGE || gameState === RETURN_HOME || gameState === ZOMBIE_ATTACK || gameState === ALL_HOUSES_BROKEN) {
    // Filter for available people who are not dead, not grabbed by a van, and are outside a house.
    let availablePeople = people.filter(p => !p.isDead && !p.isGrabbed && (p.state === PERSON_COMING_OUT || p.state === PERSON_GOING_TO_ORANGE || p.state === PERSON_EATING || p.state === PERSON_GOING_HOME));

    let targetPerson = null;
    if (availablePeople.length > 0) {
      targetPerson = random(availablePeople);
    }

    // A van will ONLY spawn if there is an available person to target.
    if (targetPerson) {
      // Spawn van at a random edge
      let spawnPos;
      let edge = floor(random(4)); // 0: top, 1: right, 2: bottom, 3: left
      if (edge === 0) spawnPos = createVector(random(width), -orangeSize);
      else if (edge === 1) spawnPos = createVector(width + orangeSize, random(height));
      else if (edge === 2) spawnPos = createVector(random(width), height + orangeSize);
      else spawnPos = createVector(-orangeSize, random(height));

      let newVan = new Van(spawnPos);
      newVan.setTargetPerson(targetPerson); // Strictly target a person on spawn
      vans.push(newVan); // Add new van to the array
      // gameState does not change for van events, they are side stories
    } else {
      console.warn("No people outside houses to abduct. Van won't spawn.");
      fill(0, 0, 255);
      textSize(20);
      textAlign(CENTER, CENTER);
      text("No people out! Van won't spawn.", width / 2, height / 2);
    }
  } else {
    // Optionally provide feedback if van cannot be spawned
    if (gameState === FIND_ORANGE) {
      console.warn("Call some people first!");
      fill(0, 0, 255);
      textSize(20);
      textAlign(CENTER, CENTER);
      text("Call people first!", width / 2, height / 2);
    }
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Game State Check if (gameState === EAT_ORANGE || gameState === RETURN_HOME || gameState === ZOMBIE_ATTACK || gameState === ALL_HOUSES_BROKEN) {

Validates that vans can only spawn during active gameplay.

calculation Available People Filter let availablePeople = people.filter(p => !p.isDead && !p.isGrabbed && ...);

Finds people outside and not already grabbed.

if (gameState === EAT_ORANGE || gameState === RETURN_HOME || gameState === ZOMBIE_ATTACK || gameState === ALL_HOUSES_BROKEN) {
Only allow van spawning when people are outside (active gameplay states).
let availablePeople = people.filter(p => !p.isDead && !p.isGrabbed && (p.state === PERSON_COMING_OUT || p.state === PERSON_GOING_TO_ORANGE || p.state === PERSON_EATING || p.state === PERSON_GOING_HOME));
Filter for people who are outside, not dead, and not already grabbed by another van or helicopter.
let newVan = new Van(spawnPos); newVan.setTargetPerson(targetPerson); vans.push(newVan);
Create a van, assign its target, and add it to the vans array for update/display in draw().

spawnCivilianHelicopter()

spawnCivilianHelicopter() is nearly identical to spawnVan()—same state checks, same target filtering. The main difference: helicopters spawn farther off-screen (-height/2 instead of -orangeSize) for visual drama. When a civilian helicopter abducts a person, it triggers a police car spawn, creating another cascade. This demonstrates reusable patterns in game design.

function spawnCivilianHelicopter() {
  // Civilian helicopters can spawn in EAT_ORANGE, RETURN_HOME, ZOMBIE_ATTACK, or ALL_HOUSES_BROKEN states
  if (gameState === EAT_ORANGE || gameState === RETURN_HOME || gameState === ZOMBIE_ATTACK || gameState === ALL_HOUSES_BROKEN) {
    // Filter for available people who are not dead, not grabbed by a van, and are outside a house.
    let availablePeople = people.filter(p => !p.isDead && !p.isGrabbed && (p.state === PERSON_COMING_OUT || p.state === PERSON_GOING_TO_ORANGE || p.state === PERSON_EATING || p.state === PERSON_GOING_HOME));

    let targetPerson = null;
    if (availablePeople.length > 0) {
      targetPerson = random(availablePeople);
    }

    // A helicopter will ONLY spawn if there is an available person to target.
    if (targetPerson) {
      // Spawn helicopter at a random edge (further off-screen for a more dramatic entrance)
      let spawnPos;
      let edge = floor(random(4)); // 0: top, 1: right, 2: bottom, 3: left
      if (edge === 0) spawnPos = createVector(random(width), -height/2);
      else if (edge === 1) spawnPos = createVector(width + width/2, random(height));
      else if (edge === 2) spawnPos = createVector(random(width), height + height/2);
      else spawnPos = createVector(-width/2, random(height));

      let newCivilianHelicopter = new CivilianHelicopter(spawnPos);
      newCivilianHelicopter.setTargetPerson(targetPerson); // Strictly target a person on spawn
      civilianHelicopters.push(newCivilianHelicopter); // Add new helicopter to the array
      // gameState does not change for helicopter events, they are side stories
    } else {
      console.warn("No people outside houses to abduct. Helicopter won't spawn.");
      fill(108, 117, 125); // Gray color for heli message
      textSize(20);
      textAlign(CENTER, CENTER);
      text("No people out! Helicopter won't spawn.", width / 2, height / 2);
    }
  } else {
    // Optionally provide feedback if helicopter cannot be spawned
    if (gameState === FIND_ORANGE) {
      console.warn("Call some people first!");
      fill(108, 117, 125);
      textSize(20);
      textAlign(CENTER, CENTER);
      text("Call people first!", width / 2, height / 2);
    }
  }
}
Line-by-line explanation (2 lines)
if (edge === 0) spawnPos = createVector(random(width), -height/2);
When spawning from the top, the helicopter appears far above the canvas (-height/2) for a dramatic entrance.
let newCivilianHelicopter = new CivilianHelicopter(spawnPos); newCivilianHelicopter.setTargetPerson(targetPerson); civilianHelicopters.push(newCivilianHelicopter);
Create a civilian helicopter, set its target person, and add it to the civilianHelicopters array.

class Person

The Person class is the foundation of the sketch's mechanics. Each person is a state machine moving through states (COMING_OUT → GOING_TO_ORANGE → EATING → GOING_HOME → IDLE). The update() method checks the current state and changes behavior accordingly; display() draws the person unless they're hidden (IDLE or DEAD). This state pattern is repeated in Zombie, Van, and Helicopter classes, making the codebase consistent and maintainable.

class Person {
  constructor(homeHouse, outsideHomePos) { // homeHouse can be a house object or null for homeless people
    this.homeHouse = homeHouse;
    this.outsideHomePos = outsideHomePos.copy(); // Store the roof edge point

    if (this.homeHouse) {
      this.pos = this.homeHouse.homePos.copy(); // Start at house center (inside)
      this.homePos = this.homeHouse.homePos.copy(); // House center as home
      this.target = this.outsideHomePos.copy(); // Initial target is outside roof
      this.state = PERSON_COMING_OUT; // Initial state
    } else {
      // Homeless person - spawn at outsideHomePos, immediately try to go to orange
      this.pos = this.outsideHomePos.copy(); // Start at the spawn point (which is already outside)
      this.homePos = null; // No home position
      this.target = orangePos.copy(); // Go to orange immediately
      this.state = PERSON_GOING_TO_ORANGE; // Immediately go to orange
    }

    this.normalSpeed = min(width, height) / 100; // Normal speed
    this.walkingSpeed = min(width, height) / 200; // Walking speed
    this.speed = this.normalSpeed; // Start at normal speed
    this.size = orangeSize * 1.5; // Person size relative to orange
    this.color = color(255, 200, 100); // Skin-like color
    this.eatingTimer = 0; // Timer for eating duration
    this.eatingDuration = 90; // How many frames to "eat" (3 seconds at 30 fps)
    this.isDead = false; // Flag for dead person
    this.isGrabbed = false; // NEW: Flag for grabbed person
  }

  update() {
    if (this.state === PERSON_IDLE || this.isDead || this.isGrabbed) return; // Don't update if idle, dead, or grabbed

    // Adjust speed based on game state
    this.speed = (gameState === ZOMBIE_ATTACK) ? this.walkingSpeed : this.normalSpeed;

    let d = dist(this.pos.x, this.pos.y, this.target.x, this.target.y);
    let reachedTarget = d < this.speed; // Check if target is reached

    if (!reachedTarget) {
      // Move towards the target
      let direction = p5.Vector.sub(this.target, this.pos);
      direction.setMag(this.speed);
      this.pos.add(direction);
    } else {
      // Logic for when target is reached
      if (this.state === PERSON_COMING_OUT) {
        // Once out of the house (at outsideHomePos), go to the orange
        this.target = orangePos.copy();
        this.state = PERSON_GOING_TO_ORANGE;
      } else if (this.state === PERSON_GOING_TO_ORANGE) {
        // Once at the orange, start eating
        this.state = PERSON_EATING;
        this.eatingTimer = 0; // Reset eating timer
      } else if (this.state === PERSON_EATING) {
        // Eating simulation
        this.eatingTimer++;
        if (this.eatingTimer > this.eatingDuration) {
          // Done eating, go home (to outsideHomePos)
          this.target = this.outsideHomePos.copy();
          this.state = PERSON_GOING_HOME;
          // Only change game state to RETURN_HOME if not already in ZOMBIE_ATTACK or ALL_HOUSES_BROKEN
          if (gameState !== ZOMBIE_ATTACK && gameState !== ALL_HOUSES_BROKEN) { 
            gameState = RETURN_HOME; 
          }
        }
      } else if (this.state === PERSON_GOING_HOME) {
        // Once at outsideHomePos (home edge)
        // Check if they have a house and if it's broken
        // NEW: Add check for any van or helicopter activity
        if (this.homeHouse && this.homeHouse.isBroken) { // Check homeHouse reference
          // House is broken, they cannot enter. Stay outside at outsideHomePos.
          // No state change to PERSON_IDLE.
          // They remain in PERSON_GOING_HOME state, but are effectively stuck.
        } else if (this.homeHouse && !this.homeHouse.isBroken && vans.length === 0 && helicopters.length === 0 && civilianHelicopters.length === 0 && policeCars.length === 0) { // NEW: Include civilianHelicopters and policeCars
          // House is not broken, and no van/helicopter activity, they can enter.
          this.state = PERSON_IDLE;
        } else {
          // No homeHouse (homeless person), they are stuck outside at outsideHomePos
          // They remain in PERSON_GOING_HOME state, but are effectively stuck.
        }
      }
    }
  }

  display() {
    if (this.state === PERSON_IDLE || this.isDead) return; // Don't draw if idle (inside house) or dead -- REMOVED || this.isGrabbed
    
    fill(this.color);
    ellipse(this.pos.x, this.pos.y, this.size, this.size);
    
    // Optional: Draw a small indicator if they are stuck at home edge due to zombies, broken house, or being homeless
    // NEW: Add check for any van or helicopter activity
    if (this.state === PERSON_GOING_HOME && dist(this.pos.x, this.pos.y, this.outsideHomePos.x, this.outsideHomePos.y) < this.speed && (gameState === ZOMBIE_ATTACK || (this.homeHouse && this.homeHouse.isBroken) || !this.homeHouse || vans.length > 0 || helicopters.length > 0 || civilianHelicopters.length > 0 || policeCars.length > 0)) { // NEW: Include civilianHelicopters and policeCars
      fill(255, 0, 0, 150); // Red semi-transparent dot
      ellipse(this.pos.x, this.pos.y - this.size / 2 - 5, 10, 10);
    }
  }
}
Line-by-line explanation (9 lines)
constructor(homeHouse, outsideHomePos) {
Takes a house object (or null for homeless persons) and the roof exit position; initializes all person properties.
if (this.homeHouse) { this.pos = this.homeHouse.homePos.copy(); ... this.state = PERSON_COMING_OUT; } else { ... this.state = PERSON_GOING_TO_ORANGE; }
Normal persons start inside their house heading out; homeless persons start at their spawn edge heading directly to the orange.
this.normalSpeed = min(width, height) / 100;
Normal speed scales with canvas size—ensures people move consistently on any screen size.
this.speed = (gameState === ZOMBIE_ATTACK) ? this.walkingSpeed : this.normalSpeed;
People move slower when a zombie is active—slows them to give zombie a chance to catch them.
let d = dist(this.pos.x, this.pos.y, this.target.x, this.target.y); let reachedTarget = d < this.speed;
Calculates distance to target; if less than one frame's movement, the person has 'reached' it.
let direction = p5.Vector.sub(this.target, this.pos); direction.setMag(this.speed); this.pos.add(direction);
Classic velocity pattern: calculate direction vector, set its magnitude to speed, add to position—smooth normalized movement.
if (this.state === PERSON_COMING_OUT) { this.target = orangePos.copy(); this.state = PERSON_GOING_TO_ORANGE; }
First state change: once outside, person immediately heads to the orange.
} else if (this.state === PERSON_EATING) { this.eatingTimer++; if (this.eatingTimer > this.eatingDuration) { this.target = this.outsideHomePos.copy(); this.state = PERSON_GOING_HOME; }
Eating is a timed action—increment timer each frame, and when it exceeds eatingDuration, person heads home.
} else if (this.state === PERSON_GOING_HOME) { if (this.homeHouse && this.homeHouse.isBroken) { ... } else if (this.homeHouse && !this.homeHouse.isBroken && vans.length === 0 && ...) { this.state = PERSON_IDLE; }
When arriving home, check if house is broken or if vans/helicopters are active; only enter (become IDLE) if safe and unbroken.

class Zombie

The Zombie class demonstrates a state machine with hunting, house-breaking, and escape logic. The findNextTarget() method is clever: it prioritizes people (more dangerous than breaking houses), then houses, then exits. When a target becomes invalid (person dies or is grabbed), the zombie doesn't crash—it searches for the next target. This defensive coding prevents null pointer errors.

class Zombie {
  constructor(startPos) {
    this.pos = startPos.copy();
    this.speed = min(width, height) / 150; // Zombie speed, slightly faster than people?
    this.size = orangeSize * 2; // Zombie size, slightly larger than people
    this.color = color(50, 150, 50); // Green zombie color
    this.target = null;
    this.state = ZOMBIE_IDLE;
    this.targetPerson = null;
    this.targetHouse = null;
    this.breakingTimer = 0;
    this.breakingDuration = 90; // How many frames to "break" a house (3 seconds)
  }

  setTargetPerson(person) {
    this.targetPerson = person;
    this.target = person.pos; // Zombie targets person's current position
    this.state = ZOMBIE_HUNTING_PERSON;
  }

  setTargetHouse(house) {
    this.targetHouse = house;
    this.target = house.homePos; // Zombie targets house's center
    this.state = ZOMBIE_HUNTING_HOUSE;
  }

  // New method to find the zombie's next target, prioritizing people
  findNextTarget() {
    // First, try to find another available person
    // Include PERSON_COMING_OUT as they are effectively "out" of the house.
    // Also ensure the person is NOT grabbed by a van, or a civilian helicopter.
    let availablePeople = people.filter(p => !p.isDead && !p.isGrabbed && (p.state === PERSON_COMING_OUT || p.state === PERSON_GOING_TO_ORANGE || p.state === PERSON_EATING || p.state === PERSON_GOING_HOME));

    if (availablePeople.length > 0) {
      let nextPerson = random(availablePeople);
      this.setTargetPerson(nextPerson);
      return; // Found a person, done retargeting
    }

    // If no people are available, try to find an unbroken house
    let unbrokenHouses = houses.filter(h => !h.isBroken);
    if (unbrokenHouses.length > 0) {
      let houseToBreak = random(unbrokenHouses);
      this.setTargetHouse(houseToBreak);
      return; // Found a house, done retargeting
    }

    // If no people and no unbroken houses, then leave
    this.state = ZOMBIE_LEAVING;
    this.target = generateOffscreenTarget(); 
    this.targetPerson = null;
    this.targetHouse = null;
  }

  update() {
    if (this.state === ZOMBIE_IDLE) {
      // Zombies only leave if explicitly told to, or if they have no targets.
      // If no targets, findNextTarget will set state to ZOMBIE_LEAVING.
      return; 
    }

    if (!this.target) {
      // If zombie has no target (e.g., its target person died or was grabbed), find a new one
      this.findNextTarget();
      return; // Try again next frame with the new target
    }

    // NEW: If target person is dead or grabbed, find a new target
    if (this.targetPerson && (this.targetPerson.isDead || this.targetPerson.isGrabbed)) {
      this.targetPerson = null; // Clear invalid person target
      this.findNextTarget();
      return; // Try again next frame with the new target
    }


    let d = dist(this.pos.x, this.pos.y, this.target.x, this.target.y);
    let reachedTarget = d < this.speed;

    if (!reachedTarget) {
      // Move towards the target
      let direction = p5.Vector.sub(this.target, this.pos);
      direction.setMag(this.speed);
      this.pos.add(direction);
    } else {
      // Logic for when target is reached
      if (this.state === ZOMBIE_HUNTING_PERSON) {
        if (this.targetPerson && !this.targetPerson.isDead && !this.targetPerson.isGrabbed) { // Also check isGrabbed
          this.targetPerson.isDead = true; // Kill the person
          this.targetPerson.state = PERSON_DEAD;
          this.targetPerson = null; // Clear person target
          this.findNextTarget(); // Immediately find the next target (prioritizing people)
        } else {
          // Person already dead, grabbed, or invalid target when zombie reached, find next target
          this.findNextTarget(); // Immediately find the next target (prioritizing people)
        }
      } else if (this.state === ZOMBIE_HUNTING_HOUSE) {
        if (this.targetHouse && !this.targetHouse.isBroken) {
          this.state = ZOMBIE_BREAKING_HOUSE;
          this.breakingTimer = 0;
        } else {
          // House already broken or invalid target, find next target
          this.findNextTarget();
        }
      } else if (this.state === ZOMBIE_BREAKING_HOUSE) {
        this.breakingTimer++;
        if (this.breakingTimer > this.breakingDuration) {
          if (this.targetHouse) {
            this.targetHouse.isBroken = true; // Mark house as broken
            // Instead of killing, make people inside come out and get stuck
            people.forEach(p => {
              if (p.homeHouse === this.targetHouse && p.state === PERSON_IDLE) { // Check homeHouse reference
                p.target = p.outsideHomePos.copy(); // Set target to outside edge
                p.state = PERSON_GOING_HOME; // Change state to going home (and get stuck)
              }
            });
            this.targetHouse = null; // Clear house target
          }
          this.state = ZOMBIE_LEAVING; // Zombie leaves after breaking house
          this.target = generateOffscreenTarget(); 
        }
      } else if (this.state === ZOMBIE_LEAVING) {
        // Zombie has reached its off-screen target and will be removed in draw()
        // No further action needed here.
      }
    }
  }

  display() {
    if (this.state === ZOMBIE_IDLE) return;

    fill(this.color);
    noStroke();
    ellipse(this.pos.x, this.pos.y, this.size, this.size);
    
    // Optional: add eyes or other features
    fill(0);
    ellipse(this.pos.x - this.size * 0.2, this.pos.y - this.size * 0.1, this.size * 0.2, this.size * 0.2);
    ellipse(this.pos.x + this.size * 0.2, this.pos.y - this.size * 0.1, this.size * 0.2, this.size * 0.2);
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Zombie Initialization constructor(startPos) { this.pos = startPos.copy(); this.speed = min(width, height) / 150; ... this.state = ZOMBIE_IDLE; }

Sets up zombie properties: position, speed, size, color, and initial idle state.

calculation Target Prioritization findNextTarget() { let availablePeople = ...; if (availablePeople.length > 0) { ... } else { let unbrokenHouses = ...; } }

Searches for next target: first available people, then unbroken houses, then leaves off-screen.

setTargetPerson(person) { this.targetPerson = person; this.target = person.pos; this.state = ZOMBIE_HUNTING_PERSON; }
Assigns a person to hunt; sets the zombie's target to that person's position and changes state to HUNTING_PERSON.
findNextTarget() { let availablePeople = people.filter(p => !p.isDead && !p.isGrabbed && ...)
Filters for people outside and not grabbed—the zombie's preferred targets. If none, searches for unbroken houses.
if (!this.target) { this.findNextTarget(); return; }
If zombie has no target (person died or was grabbed), immediately search for a new one without moving.
if (this.state === ZOMBIE_HUNTING_PERSON) { if (this.targetPerson && !this.targetPerson.isDead && !this.targetPerson.isGrabbed) { this.targetPerson.isDead = true; ... this.findNextTarget(); }
When reaching a person, kill them and immediately find the next target—the zombie relentlessly hunts.
} else if (this.state === ZOMBIE_BREAKING_HOUSE) { this.breakingTimer++; if (this.breakingTimer > this.breakingDuration) { this.targetHouse.isBroken = true; ... this.state = ZOMBIE_LEAVING; }
Breaking a house takes time—increment timer each frame. When done, mark house as broken, force any idle people out, and leave.

class Van

The Van class demonstrates a grab-and-flee mechanic. Vans hunt people, grab them (freezing them with isGrabbed=true), then escape off-screen while the grabbed person moves with them. The key innovation: calling spawnHelicopter(this) triggers a police response, creating a mini-story within the larger game. This is emergent gameplay—chain reactions of events that create narrative tension.

class Van {
  constructor(startPos) {
    this.pos = startPos.copy();
    this.speed = min(width, height) / 120; // Van speed
    this.sizeW = orangeSize * 4; // Van width
    this.sizeH = orangeSize * 3; // Van height
    this.color = color(100, 100, 255); // Blue van color
    this.target = null;
    this.state = Van.VAN_HUNTING_PERSON; // FIX: Access static property using class name
    this.targetPerson = null;
    this.grabbedPerson = null;
    this.grabbingTimer = 0;
    this.grabbingDuration = 30; // How many frames to "grab" (1 second)
    this.isDestroyed = false; // Flag for being destroyed by helicopter
  }

  // Define van states
  static VAN_IDLE = 0;
  static VAN_HUNTING_PERSON = 1;
  static VAN_GRABBING_PERSON = 2;
  static VAN_LEAVING = 3;
  // static VAN_PARKED = 4; // REMOVED: Vans will now drive off-screen

  setTargetPerson(person) {
    this.targetPerson = person;
    this.target = person.pos; // Van targets person's current position
    this.state = Van.VAN_HUNTING_PERSON;
  }

  update() {
    if (this.isDestroyed || this.state === Van.VAN_IDLE) return;

    // REMOVED: Handling for Van.VAN_PARKED state

    if (this.state === Van.VAN_LEAVING) {
      // Move towards off-screen target
      let d = dist(this.pos.x, this.pos.y, this.target.x, this.target.y);
      if (d < this.speed) {
        // Van has reached its off-screen target and will be removed in draw()
        // No further action needed here.
      } else {
        let direction = p5.Vector.sub(this.target, this.pos);
        direction.setMag(this.speed);
        this.pos.add(direction);
      }
      
      // Update grabbed person's position to follow the van
      if (this.grabbedPerson) {
        // Adjust Y position to center person inside van, and keep them centered in X
        this.grabbedPerson.pos.set(this.pos.x, this.pos.y + this.sizeH * 0.1); 
      }
      return; // No further logic for leaving state
    }

    if (!this.target) {
      // If van has no target (e.g., its target person died or was grabbed by another van/zombie), find a new one
      this.findNextTarget();
      return; // Try again next frame with the new target
    }

    // If target person is dead or grabbed, find a new target
    if (this.targetPerson && (this.targetPerson.isDead || this.targetPerson.isGrabbed)) {
      this.targetPerson = null; // Clear invalid person target
      this.findNextTarget();
      return; // Try again next frame with the new target
    }

    let d = dist(this.pos.x, this.pos.y, this.target.x, this.target.y);
    let reachedTarget = d < this.speed;

    if (!reachedTarget) {
      // Move towards the target
      let direction = p5.Vector.sub(this.target, this.pos);
      direction.setMag(this.speed);
      this.pos.add(direction);
    } else {
      // Logic for when target is reached
      if (this.state === Van.VAN_HUNTING_PERSON) {
        if (this.targetPerson && !this.targetPerson.isDead && !this.targetPerson.isGrabbed) {
          this.grabbedPerson = this.targetPerson; // Store the person
          this.grabbedPerson.isGrabbed = true; // Mark person as grabbed
          this.grabbedPerson.state = PERSON_IDLE; // Prevent person's own update logic
          this.targetPerson = null; // Clear target person for van
          this.state = Van.VAN_GRABBING_PERSON;
          this.grabbingTimer = 0; // Start grabbing timer
        } else {
          // Person already dead, grabbed, or invalid target when van reached, find next target
          this.findNextTarget();
        }
      } else if (this.state === Van.VAN_GRABBING_PERSON) {
        this.grabbingTimer++;
        if (this.grabbingTimer > this.grabbingDuration) {
          // Done grabbing, set off-screen target and leave
          this.target = generateOffscreenTarget(); // NEW: Off-screen target
          this.state = Van.VAN_LEAVING;
          spawnHelicopter(this); // Trigger helicopter to chase this van
        }
      }
    }
  }

  // Find next target person if current one is invalid or gone
  findNextTarget() {
    let availablePeople = people.filter(p => !p.isDead && !p.isGrabbed && (p.state === PERSON_COMING_OUT || p.state === PERSON_GOING_TO_ORANGE || p.state === PERSON_EATING || p.state === PERSON_GOING_HOME));
    if (availablePeople.length > 0) {
      this.setTargetPerson(random(availablePeople));
    } else {
      // If no people, the van just leaves
      this.state = Van.VAN_LEAVING;
      this.target = generateOffscreenTarget(); // NEW: Off-screen target
      this.grabbedPerson = null; // Release any potential grabbed person reference (shouldn't happen here)
    }
  }

  display() {
    if (this.state === Van.VAN_IDLE) return;

    fill(this.color);
    noStroke();
    rectMode(CENTER);
    rect(this.pos.x, this.pos.y, this.sizeW, this.sizeH);
    
    // Optional: Draw windows
    fill(200, 220, 255);
    rect(this.pos.x - this.sizeW * 0.25, this.pos.y - this.sizeH * 0.1, this.sizeW * 0.2, this.sizeH * 0.3);
    rect(this.pos.x + this.sizeW * 0.25, this.pos.y - this.sizeH * 0.1, this.sizeW * 0.2, this.sizeH * 0.3);
    
    // Optional: Draw wheels
    fill(50);
    ellipse(this.pos.x - this.sizeW * 0.3, this.pos.y + this.sizeH * 0.4, this.sizeW * 0.2, this.sizeW * 0.2);
    ellipse(this.pos.x + this.sizeW * 0.3, this.pos.y + this.sizeH * 0.4, this.sizeW * 0.2, this.sizeW * 0.2);

    // NEW: Draw grabbed person inside the van
    if (this.grabbedPerson) {
      fill(this.grabbedPerson.color);
      ellipse(this.pos.x, this.pos.y + this.sizeH * 0.1, this.grabbedPerson.size, this.grabbedPerson.size);
    }
  }
}
Line-by-line explanation (4 lines)
static VAN_HUNTING_PERSON = 1; static VAN_GRABBING_PERSON = 2; static VAN_LEAVING = 3;
Static properties define van states—enum-like constants shared by all van instances (no instance creation needed).
this.state = Van.VAN_HUNTING_PERSON; // FIX: Access static property using class name
Access static properties via the class name (Van.VAN_HUNTING_PERSON), not this—a common JavaScript pattern.
if (this.state === Van.VAN_LEAVING) { ... if (this.grabbedPerson) { this.grabbedPerson.pos.set(this.pos.x, this.pos.y + this.sizeH * 0.1); }
While leaving, keep the grabbed person positioned inside the van—they follow along as visual feedback.
if (this.grabbingTimer > this.grabbingDuration) { this.target = generateOffscreenTarget(); this.state = Van.VAN_LEAVING; spawnHelicopter(this);
After grabbing a person, the van heads off-screen and triggers a police helicopter to spawn—cascade of events.

class PoliceHelicopter

The PoliceHelicopter class is a pure consequence of van actions—it only spawns when a van grabs a person, then hunts and destroys it. This demonstrates the game's layered complexity: players summon vans, vans trigger helicopters, helicopters trigger police cars (in civilian helicopter case). Each class knows how to find and interact with its specific target, making the system modular and extensible.

class PoliceHelicopter {
  constructor(startPos, vanToTarget) {
    this.pos = startPos.copy();
    this.speed = min(width, height) / 80; // Helicopter speed
    this.sizeW = orangeSize * 5; // Helicopter width
    this.sizeH = orangeSize * 3; // Helicopter height
    this.color = color(150, 0, 0); // Red police helicopter color
    this.target = vanToTarget.pos; // Target the van's current position
    this.state = PoliceHelicopter.HELICOPTER_HUNTING_VAN; // Initial state
    this.targetVan = vanToTarget;
    this.shootingTimer = 0;
    this.shootingDuration = 60; // How many frames to "shoot" (2 seconds)
    this.leavingTimer = 0;
    this.leavingDuration = 90; // How many frames to "leave" after shooting
  }

  // Define helicopter states
  static HELICOPTER_HUNTING_VAN = 1;
  static HELICOPTER_SHOOTING_VAN = 2;
  static HELICOPTER_LEAVING = 3;
  // static HELICOPTER_HOVERING = 4; // REMOVED: Police helicopters will now leave

  update() {
    // REMOVED: Handling for PoliceHelicopter.HELICOPTER_HOVERING state

    if (this.state === PoliceHelicopter.HELICOPTER_LEAVING) {
      // Move towards off-screen target
      let d = dist(this.pos.x, this.pos.y, this.target.x, this.target.y);
      if (d < this.speed) {
        // Helicopter has reached its off-screen target and will be removed in draw()
        // No further action needed here.
      } else {
        let direction = p5.Vector.sub(this.target, this.pos);
        direction.setMag(this.speed);
        this.pos.add(direction);
      }
      return;
    }

    if (!this.targetVan || this.targetVan.isDestroyed) {
      // Van is already destroyed or invalid, helicopter just leaves
      this.state = PoliceHelicopter.HELICOPTER_LEAVING;
      this.target = generateOffscreenTarget(); // NEW: Off-screen target
      return;
    }

    // Helicopter's target is the van's current position
    this.target = this.targetVan.pos.copy();

    let d = dist(this.pos.x, this.pos.y, this.target.x, this.target.y);
    let reachedTarget = d < this.speed;

    if (!reachedTarget) {
      // Move towards the target
      let direction = p5.Vector.sub(this.target, this.pos);
      direction.setMag(this.speed);
      this.pos.add(direction);
    } else {
      // Logic for when target is reached
      if (this.state === PoliceHelicopter.HELICOPTER_HUNTING_VAN) {
        this.state = PoliceHelicopter.HELICOPTER_SHOOTING_VAN;
        this.shootingTimer = 0; // Start shooting timer
      } else if (this.state === PoliceHelicopter.HELICOPTER_SHOOTING_VAN) {
        this.shootingTimer++;
        if (this.shootingTimer > this.shootingDuration) {
          // Done shooting, destroy the van and release the person
          this.targetVan.isDestroyed = true; // Mark van as destroyed
          if (this.targetVan.grabbedPerson) {
            let person = this.targetVan.grabbedPerson;
            person.isGrabbed = false; // Release the person
            // Set person to leave off-screen in a random direction
            person.target = generateOffscreenTarget(); 
            person.state = PERSON_GOING_HOME; // Use PERSON_GOING_HOME to signify leaving
            this.targetVan.grabbedPerson = null; // Clear reference
          }
          this.targetVan = null; // Clear van target for helicopter
          this.state = PoliceHelicopter.HELICOPTER_LEAVING;
          this.target = generateOffscreenTarget(); // NEW: Off-screen target
        }
      }
    }
  }

  display() {
    // REMOVED: Checking HELICOPTER_IDLE as it's no longer a valid state for PoliceHelicopter
    // This helicopter is always "active" once spawned until it leaves.

    fill(this.color);
    noStroke();
    rectMode(CENTER);
    
    // Body
    rect(this.pos.x, this.pos.y, this.sizeW, this.sizeH);
    
    // Tail
    rect(this.pos.x - this.sizeW * 0.4, this.pos.y - this.sizeH * 0.1, this.sizeW * 0.2, this.sizeH * 0.2);
    
    // Rotor (simple line)
    stroke(0);
    strokeWeight(2);
    line(this.pos.x - this.sizeW * 0.3, this.pos.y - this.sizeH * 0.6, this.pos.x + this.sizeW * 0.3, this.pos.y - this.sizeH * 0.6);
    noStroke();
    
    // Police text (optional)
    fill(255);
    textSize(this.sizeH * 0.4);
    textAlign(CENTER, CENTER);
    text("POLICE", this.pos.x, this.pos.y);
  }
}
Line-by-line explanation (5 lines)
constructor(startPos, vanToTarget) { this.pos = startPos.copy(); ... this.targetVan = vanToTarget;
Takes the van's reference at construction—stores it to track the van's position and status throughout the helicopter's lifecycle.
this.target = vanToTarget.pos; // Target the van's current position
Initially sets target to the van's current position—this will update each frame as the van moves.
if (!this.targetVan || this.targetVan.isDestroyed) { this.state = PoliceHelicopter.HELICOPTER_LEAVING; this.target = generateOffscreenTarget(); return; }
Safety check: if the van is already destroyed or null, helicopter leaves immediately without wasting time.
this.target = this.targetVan.pos.copy(); // Helicopter's target is the van's current position
Each update, recopy the van's position—the helicopter chases a moving target, not a fixed point.
if (this.targetVan.grabbedPerson) { let person = this.targetVan.grabbedPerson; person.isGrabbed = false; person.target = generateOffscreenTarget(); person.state = PERSON_GOING_HOME;
After destroying the van, release the grabbed person and set them to leave off-screen—helicopter rescue mission complete.

class CivilianHelicopter

CivilianHelicopter mirrors Van and PoliceHelicopter but with one twist: when it escapes with a grabbed person, it triggers spawnPoliceCar(). This completes the three-level cascade: Van → PoliceHelicopter → PoliceCar, with each level chasing the previous. It's a chain reaction of emergent gameplay triggered by a single player action.

class CivilianHelicopter {
  constructor(startPos) {
    this.pos = startPos.copy();
    this.speed = min(width, height) / 100; // Heli speed, faster than van
    this.sizeW = orangeSize * 4.5; // Heli width
    this.sizeH = orangeSize * 2.5; // Heli height
    this.color = color(200); // Light gray/silver color for civilian heli
    this.target = null;
    this.state = CivilianHelicopter.HELICOPTER_HUNTING_PERSON; // Initial state
    this.targetPerson = null;
    this.grabbedPerson = null;
    this.grabbingTimer = 0;
    this.grabbingDuration = 30; // How many frames to "grab" (1 second)
    this.isDestroyed = false; // NEW: Flag for being destroyed by police car
  }

  // Define helicopter states
  static HELICOPTER_IDLE = 0;
  static HELICOPTER_HUNTING_PERSON = 1;
  static HELICOPTER_GRABBING_PERSON = 2;
  static HELICOPTER_LEAVING = 3;
  // static HELICOPTER_HOVERING = 4; // REMOVED: Civilian helicopters will now leave

  setTargetPerson(person) {
    this.targetPerson = person;
    this.target = person.pos; // Heli targets person's current position
    this.state = CivilianHelicopter.HELICOPTER_HUNTING_PERSON;
  }

  update() {
    if (this.isDestroyed || this.state === CivilianHelicopter.HELICOPTER_IDLE) return; // NEW: Check isDestroyed

    // REMOVED: Handling for CivilianHelicopter.HELICOPTER_HOVERING state

    if (this.state === CivilianHelicopter.HELICOPTER_LEAVING) {
      // Move towards off-screen target
      let d = dist(this.pos.x, this.pos.y, this.target.x, this.target.y);
      if (d < this.speed) {
        // Helicopter has reached its off-screen target and will be removed in draw()
        // No further action needed here.
      } else {
        let direction = p5.Vector.sub(this.target, this.pos);
        direction.setMag(this.speed);
        this.pos.add(direction);
      }
      
      // Update grabbed person's position to follow the helicopter
      if (this.grabbedPerson) {
        // Adjust Y position to center person inside heli, and keep them centered in X
        this.grabbedPerson.pos.set(this.pos.x, this.pos.y + this.sizeH * 0.1); 
      }
      return; // No further logic for leaving state
    }

    if (!this.target) {
      // If heli has no target (e.g., its target person died or was grabbed), find a new one
      this.findNextTarget();
      return; // Try again next frame with the new target
    }

    // If target person is dead or grabbed, find a new target
    if (this.targetPerson && (this.targetPerson.isDead || this.targetPerson.isGrabbed)) {
      this.targetPerson = null; // Clear invalid person target
      this.findNextTarget();
      return; // Try again next frame with the new target
    }

    let d = dist(this.pos.x, this.pos.y, this.target.x, this.target.y);
    let reachedTarget = d < this.speed;

    if (!reachedTarget) {
      // Move towards the target
      let direction = p5.Vector.sub(this.target, this.pos);
      direction.setMag(this.speed);
      this.pos.add(direction);
    } else {
      // Logic for when target is reached
      if (this.state === CivilianHelicopter.HELICOPTER_HUNTING_PERSON) {
        if (this.targetPerson && !this.targetPerson.isDead && !this.targetPerson.isGrabbed) {
          this.grabbedPerson = this.targetPerson; // Store the person
          this.grabbedPerson.isGrabbed = true; // Mark person as grabbed
          this.grabbedPerson.state = PERSON_IDLE; // Prevent person's own update logic
          this.targetPerson = null; // Clear target person for heli
          this.state = CivilianHelicopter.HELICOPTER_GRABBING_PERSON;
          this.grabbingTimer = 0; // Start grabbing timer
        } else {
          // Person already dead, grabbed, or invalid target when heli reached, find next target
          this.findNextTarget();
        }
      } else if (this.state === CivilianHelicopter.HELICOPTER_GRABBING_PERSON) {
        this.grabbingTimer++;
        if (this.grabbingTimer > this.grabbingDuration) {
          // Done grabbing, set off-screen target and leave
          this.target = generateOffscreenTarget(); // NEW: Off-screen target
          this.state = CivilianHelicopter.HELICOPTER_LEAVING;
          // NEW: Trigger police car to chase this helicopter
          spawnPoliceCar(this);
        }
      }
    }
  }

  // Find next target person if current one is invalid or gone
  findNextTarget() {
    let availablePeople = people.filter(p => !p.isDead && !p.isGrabbed && (p.state === PERSON_COMING_OUT || p.state === PERSON_GOING_TO_ORANGE || p.state === PERSON_EATING || p.state === PERSON_GOING_HOME));
    if (availablePeople.length > 0) {
      this.setTargetPerson(random(availablePeople));
    } else {
      // If no people, the heli just leaves
      this.state = CivilianHelicopter.HELICOPTER_LEAVING;
      this.target = generateOffscreenTarget(); // NEW: Off-screen target
      this.grabbedPerson = null; // Release any potential grabbed person reference (shouldn't happen here)
    }
  }

  display() {
    if (this.isDestroyed || this.state === CivilianHelicopter.HELICOPTER_IDLE) return; // NEW: Check isDestroyed

    fill(this.color);
    noStroke();
    rectMode(CENTER);
    
    // Body
    rect(this.pos.x, this.pos.y, this.sizeW, this.sizeH);
    
    // Tail
    rect(this.pos.x - this.sizeW * 0.4, this.pos.y - this.sizeH * 0.1, this.sizeW * 0.2, this.sizeH * 0.2);
    
    // Rotor (simple line)
    stroke(100);
    strokeWeight(2);
    line(this.pos.x - this.sizeW * 0.3, this.pos.y - this.sizeH * 0.6, this.pos.x + this.sizeW * 0.3, this.pos.y - this.sizeH * 0.6);
    noStroke();
    
    // Optional: Add a small "C" for Civilian
    fill(0);
    textSize(this.sizeH * 0.4);
    textAlign(CENTER, CENTER);
    text("C", this.pos.x, this.pos.y);

    // NEW: Draw grabbed person inside the helicopter
    if (this.grabbedPerson) {
      fill(this.grabbedPerson.color);
      ellipse(this.pos.x, this.pos.y + this.sizeH * 0.1, this.grabbedPerson.size, this.grabbedPerson.size);
    }
  }
}
Line-by-line explanation (1 lines)
if (this.grabbingTimer > this.grabbingDuration) { this.target = generateOffscreenTarget(); this.state = CivilianHelicopter.HELICOPTER_LEAVING; spawnPoliceCar(this);
After grabbing a person, the helicopter flees and triggers a police car to spawn—starting another cascade.

class PoliceCar

PoliceCar is the final link in the cascade chain. Unlike the helicopter that rescues the person from the van, the police car's actions end in tragedy—the grabbed person dies. This asymmetry creates interesting dynamics: vans are bad (lose people to them), but getting grabbed by a civilian helicopter leads to death. The sketch explores consequence and stakes through gameplay mechanics.

class PoliceCar {
  constructor(startPos, heliToTarget) {
    this.pos = startPos.copy();
    this.speed = min(width, height) / 100; // Car speed, slightly slower than heli
    this.sizeW = orangeSize * 4; // Car width
    this.sizeH = orangeSize * 2.5; // Car height
    this.color = color(0, 0, 150); // Dark blue police car color
    this.target = heliToTarget.pos; // Target the heli's current position
    this.state = PoliceCar.POLICECAR_HUNTING_HELI; // Initial state
    this.targetHeli = heliToTarget;
    this.shootingTimer = 0;
    this.shootingDuration = 60; // How many frames to "shoot" (2 seconds)
    this.leavingTimer = 0;
    this.leavingDuration = 90; // How many frames to "leave" after shooting
  }

  // Define police car states
  static POLICECAR_HUNTING_HELI = 1;
  static POLICECAR_SHOOTING_HELI = 2;
  static POLICECAR_LEAVING = 3;
  // static POLICECAR_PARKED = 4; // REMOVED: Police cars will now leave

  update() {
    // REMOVED: Handling for PoliceCar.POLICECAR_PARKED state

    if (this.state === PoliceCar.POLICECAR_LEAVING) {
      // Move towards off-screen target
      let d = dist(this.pos.x, this.pos.y, this.target.x, this.target.y);
      if (d < this.speed) {
        // Car has reached its off-screen target and will be removed in draw()
        // No further action needed here.
      } else {
        let direction = p5.Vector.sub(this.target, this.pos);
        direction.setMag(this.speed);
        this.pos.add(direction);
      }
      return;
    }

    if (!this.targetHeli || this.targetHeli.isDestroyed) {
      // Heli is already destroyed or invalid, car just leaves
      this.state = PoliceCar.POLICECAR_LEAVING;
      this.target = generateOffscreenTarget(); // NEW: Off-screen target
      return;
    }

    // Police car's target is the heli's current position
    this.target = this.targetHeli.pos.copy();

    let d = dist(this.pos.x, this.pos.y, this.target.x, this.target.y);
    let reachedTarget = d < this.speed;

    if (!reachedTarget) {
      // Move towards the target
      let direction = p5.Vector.sub(this.target, this.pos);
      direction.setMag(this.speed);
      this.pos.add(direction);
    } else {
      // Logic for when target is reached
      if (this.state === PoliceCar.POLICECAR_HUNTING_HELI) {
        this.state = PoliceCar.POLICECAR_SHOOTING_HELI;
        this.shootingTimer = 0; // Start shooting timer
      } else if (this.state === PoliceCar.POLICECAR_SHOOTING_HELI) {
        this.shootingTimer++;
        if (this.shootingTimer > this.shootingDuration) {
          // Done shooting, destroy the heli and release the person (to die)
          this.targetHeli.isDestroyed = true; // Mark heli as destroyed
          if (this.targetHeli.grabbedPerson) {
            let person = this.targetHeli.grabbedPerson;
            person.isGrabbed = false; // Release the person
            person.isDead = true; // NEW: Person dies immediately
            person.state = PERSON_DEAD; // Ensure person's own update logic stops
            this.targetHeli.grabbedPerson = null; // Clear reference
          }
          this.targetHeli = null; // Clear heli target for car
          this.state = PoliceCar.POLICECAR_LEAVING;
          this.target = generateOffscreenTarget(); // NEW: Off-screen target
        }
      }
    }
  }

  display() {
    // REMOVED: Checking POLICECAR_IDLE as it's no longer a valid state for PoliceCar
    // This car is always "active" once spawned until it leaves.

    fill(this.color);
    noStroke();
    rectMode(CENTER);
    
    // Body
    rect(this.pos.x, this.pos.y, this.sizeW, this.sizeH);
    
    // Windows
    fill(200, 220, 255);
    rect(this.pos.x - this.sizeW * 0.25, this.pos.y - this.sizeH * 0.1, this.sizeW * 0.2, this.sizeH * 0.3);
    rect(this.pos.x + this.sizeW * 0.25, this.pos.y - this.sizeH * 0.1, this.sizeW * 0.2, this.sizeH * 0.3);
    
    // Wheels
    fill(50);
    ellipse(this.pos.x - this.sizeW * 0.3, this.pos.y + this.sizeH * 0.4, this.sizeW * 0.2, this.sizeW * 0.2);
    ellipse(this.pos.x + this.sizeW * 0.3, this.pos.y + this.sizeH * 0.4, this.sizeW * 0.2, this.sizeW * 0.2);

    // Police text or siren (optional)
    fill(255);
    textSize(this.sizeH * 0.4);
    textAlign(CENTER, CENTER);
    text("CAR", this.pos.x, this.pos.y);
  }
}
Line-by-line explanation (1 lines)
if (this.targetHeli.grabbedPerson) { let person = this.targetHeli.grabbedPerson; person.isGrabbed = false; person.isDead = true; person.state = PERSON_DEAD;
When the police car destroys the civilian helicopter, it kills the grabbed person—a tragic outcome showing the stakes of the chase.

📦 Key Variables

orangePos p5.Vector

Stores the x,y position of the orange on the canvas; people's target when eating, changes each game reset

let orangePos = createVector(width/2, height/2);
orangeSize number

Diameter of the orange circle in pixels; scales responsively with canvas size (1/75th of smallest dimension)

let orangeSize = min(width, height) / 75;
houses array of objects

Array of house objects, each with x,y,width,height,color,roofColor,homePos,isBroken; stores all shelter in the game

let houses = []; // Populated in resetGameElements()
people array of Person objects

Array of active Person instances; dynamically grows/shrinks as people are spawned and die

let people = [];
zombies array of Zombie objects

Array of active Zombie instances spawned by the Zombie button; each hunts or breaks houses

let zombies = [];
vans array of Van objects

Array of active Van instances; abduct people and trigger police helicopter spawns

let vans = [];
helicopters array of PoliceHelicopter objects

Array of police helicopters that spawn to chase and destroy vans

let helicopters = [];
civilianHelicopters array of CivilianHelicopter objects

Array of civilian helicopters spawned by Heli button; abduct people and trigger police cars

let civilianHelicopters = [];
policeCars array of PoliceCar objects

Array of police cars that spawn to chase and destroy civilian helicopters

let policeCars = [];
gameState number

Tracks current game phase; values: FIND_ORANGE=0, EAT_ORANGE=1, RETURN_HOME=2, ZOMBIE_ATTACK=3, ALL_HOUSES_BROKEN=4

let gameState = FIND_ORANGE;
orangeTapCount number

Counts how many people have been spawned in the current round

let orangeTapCount = 0;
numHouses number

Constant controlling how many houses generate at game start; set to 10

const numHouses = 10;
orangeButton p5.Renderer.button

p5.js button DOM element positioned top-right; clicking calls callPeople()

let orangeButton;
zombieButton p5.Renderer.button

p5.js button DOM element; clicking calls spawnZombie(); hidden until people start eating

let zombieButton;
vanButton p5.Renderer.button

p5.js button DOM element; clicking calls spawnVan(); hidden until people start eating

let vanButton;
heliButton p5.Renderer.button

p5.js button DOM element; clicking calls spawnCivilianHelicopter(); hidden until people start eating

let heliButton;

🔧 Potential Improvements (8)

Here are some ways this code could be enhanced:

BUG Person class, update() method

When a person at PERSON_GOING_HOME reaches their outsideHomePos but the house is broken, they remain in PERSON_GOING_HOME state indefinitely—no visual indicator besides the small red dot.

💡 Add a timeout mechanism (e.g., stuckTimer) that marks them dead or transitions them to a new state after 5-10 seconds of being stuck, preventing infinite stalling.

BUG draw() function, person update loop

People are filtered to remove PERSON_DEAD state early in draw(), but the filter runs BEFORE people.update(), so dead people don't process their final update or state transitions.

💡 Move the filter to AFTER the people update/display loop, or use a flag-based approach (remove dead only when needed) to ensure state machines complete cleanly.

PERFORMANCE draw() function, multiple for-loops

draw() loops through people, zombies, vans, helicopters, civilianHelicopters, and policeCars separately—six nested loops each frame. With many entities, this becomes expensive.

💡 Create a single array of all entities and loop once, calling update/display on each. Or use a game loop pattern with different update phases (movement, collisions, rendering) to reduce redundancy.

BUG draw() function, auto-reset logic

The condition for auto-reset checks if 6 arrays are empty AND not in FIND_ORANGE/ALL_HOUSES_BROKEN. If a person gets stuck outside a broken house indefinitely, the reset never triggers.

💡 Add a round timer (e.g., if no action for 30 seconds, reset) or ensure stuck people are cleaned up automatically to prevent soft-lock states.

STYLE spawnZombie, spawnVan, spawnCivilianHelicopter functions

All three spawn functions are nearly identical (state check, target filter, spawn logic). Code duplication makes maintenance harder—changes to one function must be replicated in others.

💡 Create a generic spawnEntity(EntityClass, targetType) helper function that handles state/target checking, reducing duplication and making the code more maintainable.

FEATURE Zombie class

Zombies only destroy unbroken houses if no people are available; they don't strategically avoid going to houses where multiple people are hiding.

💡 Add weighted targeting: let zombies prefer houses with more people inside (if you track that), creating a smarter adversary that requires players to spread out.

PERFORMANCE Person class, update() method

Every person checks gameState and all van/helicopter arrays each frame to decide if they can go home—repeated global lookups.

💡 Cache global state checks in the draw loop and pass them to person.canEnterHome(isUnderThreat) to reduce repeated array.length checks.

BUG findOrange function and touch handling

The sketch handles orange taps through both mousePressed() and touchStarted(), but both call findOrange(). On touch devices, both handlers might fire, spawning duplicate people on a single tap.

💡 Use event.preventDefault() or a debounce flag to ensure only one handler executes per user interaction, preventing double-spawns on mobile.

🔄 Code Flow

Code flow showing setup, resetgameelements, draw, callpeople, spawnzombie, spawnvan, spawncivilianheli, person, zombie, van, policehelicopter, civilianhelicopter, policecar

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

graph TD start[Start] --> setup[setup] setup --> resetgameelements[resetGameElements] resetgameelements --> draw[draw loop] draw --> house-generation[House Generation Loop] house-generation --> array-clearing[Array Initialization] array-clearing --> house-rendering[House Rendering Loop] house-rendering --> dead-person-filter[Dead Person Removal] dead-person-filter --> people-update-loop[Person Update and Display] people-update-loop --> zombie-update-loop[Zombie Update and Removal] zombie-update-loop --> van-update-loop[Van Update and Removal] van-update-loop --> police-heli-update-loop[Police Helicopter Update and Removal] police-heli-update-loop --> civilian-heli-update-loop[Civilian Helicopter Update and Removal] civilian-heli-update-loop --> police-car-update-loop[Police Car Update and Removal] police-car-update-loop --> all-houses-broken-check[All Houses Broken Logic] all-houses-broken-check --> auto-reset-logic[Auto-Reset Condition] auto-reset-logic --> zombie-attack-recovery[Zombie Attack Recovery] zombie-attack-recovery --> state-display-logic[Game State Text and Button Visibility] state-display-logic --> homeless-spawn[All Houses Broken Branch] state-display-logic --> normal-spawn[Normal House Spawn] state-display-logic --> state-transition[Game State Transition] state-display-logic --> game-state-check[Valid Game State Check] game-state-check --> available-people-filter[Available People Filter] available-people-filter --> target-selection[Target Person Selection] target-selection --> zombie-spawn-check[Zombie Spawn Validation] zombie-spawn-check --> state-check[Game State Check] state-check --> target-filter[Available People Filter] target-filter --> constructor[Zombie Initialization] constructor --> findnexttarget[Target Prioritization] click setup href "#fn-setup" click resetgameelements href "#fn-resetgameelements" click draw href "#fn-draw" click house-generation href "#sub-house-generation" click array-clearing href "#sub-array-clearing" click house-rendering href "#sub-house-rendering" click dead-person-filter href "#sub-dead-person-filter" click people-update-loop href "#sub-people-update-loop" click zombie-update-loop href "#sub-zombie-update-loop" click van-update-loop href "#sub-van-update-loop" click police-heli-update-loop href "#sub-police-heli-update-loop" click civilian-heli-update-loop href "#sub-civilian-heli-update-loop" click police-car-update-loop href "#sub-police-car-update-loop" click all-houses-broken-check href "#sub-all-houses-broken-check" click auto-reset-logic href "#sub-auto-reset-logic" click zombie-attack-recovery href "#sub-zombie-attack-recovery" click state-display-logic href "#sub-state-display-logic" click homeless-spawn href "#sub-homeless-spawn" click normal-spawn href "#sub-normal-spawn" click state-transition href "#sub-state-transition" click game-state-check href "#sub-game-state-check" click available-people-filter href "#sub-available-people-filter" click target-selection href "#sub-target-selection" click zombie-spawn-check href "#sub-zombie-spawn-check" click state-check href "#sub-state-check" click target-filter href "#sub-target-filter" click constructor href "#sub-constructor" click findnexttarget href "#sub-findnexttarget"

Preview

ok it’s fix ok - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of ok it’s fix ok - Code flow showing setup, resetgameelements, draw, callpeople, spawnzombie, spawnvan, spawncivilianheli, person, zombie, van, policehelicopter, civilianhelicopter, policecar
Code Flow Diagram