People button to is fix spawn

This is a factory builder game where players place furniture items on a grid, spawn customers to buy them, and manage zombie invasions. The game combines building mechanics, NPC pathfinding, and tower-defense elements with money rewards, level progression, and weapon acquisition.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make customers move faster — Increase PERSON_SPEED so customers reach items and leave much quicker, making the game feel more frantic.
  2. Make zombies extremely slow after gun grab — Decrease RED_ZOMBIE_SPEED to 0.01 so red zombies barely crawl after the gun is picked up, making the defense phase much easier.
  3. Speed up automatic customer spawning — Lower PERSON_SPPAWN_INTERVAL so new customers arrive every 2 seconds instead of 5, creating a busier game world.
  4. Make grabbing items instant — Reduce GRAB_TIME so customers grab items instantly instead of taking 1 second, speeding up the flow of gameplay.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates an interactive factory-building game where players tap to place items on a grid, watch AI-controlled customers arrive to purchase them, and defend against zombie waves. The game uses object-oriented programming with three main classes—Item, Person, and Zombie—each with their own state machines, target-finding logic, and animation routines. It combines p5.js fundamentals like the canvas, shapes, and the draw loop with advanced concepts like collision detection, pathfinding algorithms, and complex game state management.

The code is organized into a setup() function that initializes the UI and game grid, a draw() function that updates all game entities every frame, and several helper functions that handle input, game logic, and UI management. By studying it, you will learn how to structure a multi-entity game in p5.js, manage arrays of objects with different behaviors, implement state machines for character AI, and create responsive touch/mouse controls that work seamlessly on both desktop and mobile devices.

⚙️ How It Works

  1. When the sketch loads, setup() creates a canvas sized to the window, initializes the UI buttons for building and spawning, and creates a 3×2 grid of build spots where players can place items.
  2. The draw() loop runs 60 times per second. Each frame, it clears the background, updates the positions and states of all items, people, and zombies, and redraws them at their new positions. The loop also auto-spawns new customers on a timer and checks for level-ups based on total money.
  3. Players tap an item type button to select it, then tap a build spot to place it there. The game deducts the cost from infinite money and adds the item to the items array.
  4. Customers (Person objects) spawn off-screen at the top and pathfind to the nearest item or empty build spot. When they reach an item, they 'grab' it over several frames, collect its monetary value, and leave.
  5. Zombies spawn when players select them or are created by graves. Red zombies (spawned from graves) start slow and hunt customers. If a customer picks up a gun item, all red zombies slow dramatically and a new red zombie spawns.
  6. Dead zombies and graves trigger an auto-grave spawn and consuming animation. The grave then sinks the zombie and leaves the screen. This completes the game loop: build → customers arrive → zombies threaten → guns acquired → zombies defeated.

🎓 Concepts You'll Learn

Object-oriented programming with ES6 classesState machines for character AIPathfinding and target selection algorithmsArray management and filteringCollision detection and distance calculationsGame loops and frame-based animationTouch and mouse input handlingUI management with p5.ElementsResponsive canvas sizing

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas, UI elements, and the grid of build spots that structure the game world. Notice how it uses responsive sizing (windowWidth/windowHeight) so the game works on phones, tablets, and desktops.

function setup() {
  createCanvas(windowWidth, windowHeight);
  // Tip: Use windowWidth/windowHeight for responsive canvas

  // Initialize UI elements
  // Check if money is Infinity, display "inf", otherwise display the number
  let moneyDisplay = (money === Infinity) ? 'inf' : `$${money}`;
  infoDiv = createDiv(`Money: ${moneyDisplay} | Level: ${level}`);
  infoDiv.position(10, 10);
  infoDiv.style('font-size', '20px');
  infoDiv.style('color', '#333');
  infoDiv.style('font-family', 'Arial, sans-serif');

  // Define build spots (adjust as needed for screen size)
  let spotWidth = width / 4;
  let spotHeight = height / 3;
  for (let i = 0; i < 6; i++) {
    let x = (i % 3) * spotWidth + spotWidth / 2;
    let y = floor(i / 3) * spotHeight + height / 3; // Start lower to leave space for UI
    buildSpots.push(createVector(x, y));
  }

  // Create all UI buttons
  createBuildUI();
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Responsive Canvas Setup createCanvas(windowWidth, windowHeight);

Creates a canvas that fills the entire browser window, making the game work on any screen size

calculation Money and Level Display let moneyDisplay = (money === Infinity) ? 'inf' : `$${money}`;

Formats the money display to show 'inf' when infinite, otherwise the numeric value

for-loop Build Spot Grid Creation for (let i = 0; i < 6; i++) { let x = (i % 3) * spotWidth + spotWidth / 2; let y = floor(i / 3) * spotHeight + height / 3; buildSpots.push(createVector(x, y)); }

Creates a 3×2 grid of p5.Vector positions where players can build items

createCanvas(windowWidth, windowHeight);
Creates a canvas that stretches to fill the entire browser window, so the game adapts to any screen size
let moneyDisplay = (money === Infinity) ? 'inf' : `$${money}`;
Uses a ternary operator to check if money is infinite and display 'inf', otherwise shows the dollar amount
infoDiv = createDiv(`Money: ${moneyDisplay} | Level: ${level}`);
Creates an HTML div element to display money and level; this sits on top of the canvas
infoDiv.position(10, 10);
Positions the info div 10 pixels from the top-left corner
let spotWidth = width / 4; let spotHeight = height / 3;
Calculates the spacing for a 3×2 grid by dividing the canvas width into 4 sections and height into 3
let x = (i % 3) * spotWidth + spotWidth / 2;
Calculates the x position for each grid cell using modulo operator to cycle through columns
let y = floor(i / 3) * spotHeight + height / 3;
Calculates the y position using integer division to move down to the next row; height / 3 leaves space for UI buttons
buildSpots.push(createVector(x, y));
Adds each grid position to the buildSpots array as a p5.Vector for later reference
createBuildUI();
Calls the helper function that creates all the UI buttons for item selection and spawning

draw()

draw() is the heart of the game loop. It runs 60 times per second, updating all game entities (items, people, zombies), checking for state changes, and redrawing everything. Notice how the three main loops all iterate backward when removing items—this prevents skipping elements when splicing from an array.

function draw() {
  background(220);

  // Update info div
  // Check if money is Infinity, display "inf", otherwise display the number
  let moneyDisplay = (money === Infinity) ? 'inf' : `$${money}`;
  infoDiv.html(`Money: ${moneyDisplay} | Level: ${level}`);

  // Check for level up
  checkLevelUp();

  // Draw build spots (including highlight for selected one)
  drawBuildSpots();

  // Update and display items
  for (let i = items.length - 1; i >= 0; i--) {
    items[i].update(); // Call update for grave logic
    items[i].display();
    // Remove empty graves (which includes graves that timed out and finished their leaving animation)
    if (items[i].isGrave && items[i].state === 'empty') {
      items.splice(i, 1);
    }
    // Remove gun item once grabbed (handled in Person.update)
    // No specific removal here, Person.update handles it
  }

  // Update and display people
  for (let i = people.length - 1; i >= 0; i--) {
    people[i].update();
    people[i].display();
    if (people[i].state === 'leaving' && people[i].pos.y > height + PERSON_SIZE) {
      people.splice(i, 1); // Remove person if they've left the screen
    }
  }

  // Update and display zombies
  for (let i = zombies.length - 1; i >= 0; i--) {
    zombies[i].update();
    zombies[i].display();
    // Remove zombies that are leaving (killed, or left screen) or consumed
    if (zombies[i].state === 'leaving' && zombies[i].pos.y > height + ZOMBIE_SIZE) {
      zombies.splice(i, 1);
    } else if (zombies[i].state === 'consumed') {
      zombies.splice(i, 1); // Should already be handled by grave consuming, but as fallback
    }
  }

  // Spawn new people (automatic)
  personSpawnTimer++;
  if (personSpawnTimer > PERSON_SPPAWN_INTERVAL) {
    people.push(new Person());
    personSpawnTimer = 0;
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Background Clearing background(220);

Clears the entire canvas with a light gray color each frame, erasing the previous frame's drawings

for-loop Update and Display Items for (let i = items.length - 1; i >= 0; i--) { items[i].update(); items[i].display(); if (items[i].isGrave && items[i].state === 'empty') { items.splice(i, 1); } }

Loops backward through the items array, updates each item's state, draws it, and removes empty graves

for-loop Update and Display People for (let i = people.length - 1; i >= 0; i--) { people[i].update(); people[i].display(); if (people[i].state === 'leaving' && people[i].pos.y > height + PERSON_SIZE) { people.splice(i, 1); } }

Updates each person's pathfinding and state, draws them, and removes those who have left the screen

for-loop Update and Display Zombies for (let i = zombies.length - 1; i >= 0; i--) { zombies[i].update(); zombies[i].display(); if (zombies[i].state === 'leaving' && zombies[i].pos.y > height + ZOMBIE_SIZE) { zombies.splice(i, 1); } }

Updates each zombie's AI and hunting, draws them, and removes those who have left the screen

calculation Automatic Customer Spawning personSpawnTimer++; if (personSpawnTimer > PERSON_SPPAWN_INTERVAL) { people.push(new Person()); personSpawnTimer = 0; }

Increments a timer each frame and spawns a new customer when the timer exceeds the spawn interval

background(220);
Clears the canvas with light gray (220 in grayscale), so old drawings from the previous frame disappear
let moneyDisplay = (money === Infinity) ? 'inf' : `$${money}`;
Re-formats the money display each frame so the UI always shows the current amount
infoDiv.html(`Money: ${moneyDisplay} | Level: ${level}`);
Updates the HTML text in the info div to reflect current money and level
checkLevelUp();
Checks if the player has earned enough money to reach the next level and unlock new items
drawBuildSpots();
Draws the grid of build spots on the canvas; the selected spot is highlighted in red
for (let i = items.length - 1; i >= 0; i--)
Loops backward through the items array (backward so removing items doesn't skip any)
items[i].update();
Calls the item's update() method, which handles grave animations and state transitions
items[i].display();
Draws the item to the canvas at its current position and state
if (items[i].isGrave && items[i].state === 'empty') { items.splice(i, 1); }
Removes empty graves from the array after they finish their animation and leave the screen
personSpawnTimer++;
Increments the timer by 1 each frame (so at 60 fps, it increases by 60 per second)
if (personSpawnTimer > PERSON_SPPAWN_INTERVAL) { people.push(new Person()); personSpawnTimer = 0; }
When the timer exceeds the interval (e.g., 300 frames ≈ 5 seconds), spawn a new customer and reset the timer

createBuildUI()

createBuildUI() handles all UI button creation and is called in both setup() and windowResized(). This function demonstrates dynamic button generation—only showing buttons for items unlocked at the current level. Notice how buttonX is incremented to position buttons horizontally in a row, and how callbacks bind each button to its action.

function createBuildUI() {
  // Clear existing buttons
  for (let type in buildTypeButtons) {
    buildTypeButtons[type].remove();
  }
  buildTypeButtons = {};
  if (buildButton) buildButton.remove();
  if (spawnPersonButton) spawnPersonButton.remove(); // Remove old Spawn Person button
  if (spawnZombieButton) spawnZombieButton.remove(); // Remove old Spawn Zombie button

  let buttonX = 10;
  let buttonY = 50;
  const buttonSpacing = 10;

  // Create item type selection buttons
  for (let type in itemData) {
    if (itemData[type].unlockLevel <= level) {
      let button = createButton(`${type}`); // Cost will be on the main Build button
      button.position(buttonX, buttonY);
      button.style('font-size', '16px');
      button.style('padding', '8px 12px');
      button.style('background-color', type === currentBuildType ? '#4CAF50' : '#f0f0f0');
      button.style('color', type === currentBuildType ? 'white' : '#333');
      button.style('border', 'none');
      button.style('border-radius', '5px');
      button.style('cursor', 'pointer');
      button.mousePressed(() => selectBuildType(type)); // This will now correctly trigger on touch
      buildTypeButtons[type] = button;

      buttonX += button.width + buttonSpacing;
    }
  }

  // Create the main "Build" button
  buildButton = createButton(currentBuildType ? `Build ${currentBuildType} ($${itemData[currentBuildType].cost})` : 'Select Item');
  buildButton.position(10, buttonY + 60); // Position below item type buttons
  buildButton.style('font-size', '18px');
  buildButton.style('padding', '10px 15px');
  // Set initial background based on whether an item is selected
  buildButton.style('background-color', currentBuildType ? '#007bff' : '#6c757d');
  buildButton.style('color', 'white');
  buildButton.style('border', 'none');
  buildButton.style('border-radius', '5px');
  buildButton.style('cursor', 'pointer');
  buildButton.mousePressed(confirmBuild); // This will now correctly trigger on touch

  // Create the "Spawn Person" button
  spawnPersonButton = createButton('Spawn Person');
  spawnPersonButton.position(10, buildButton.y + buildButton.height + 10); // Position below Build button
  spawnPersonButton.style('font-size', '18px');
  spawnPersonButton.style('padding', '10px 15px');
  spawnPersonButton.style('background-color', '#ffc107'); // Yellow/orange for distinct action
  spawnPersonButton.style('color', '#343a40');
  spawnPersonButton.style('border', 'none');
  spawnPersonButton.style('border-radius', '5px');
  spawnPersonButton.style('cursor', 'pointer');
  spawnPersonButton.mousePressed(spawnNewPerson); // Attach new handler

  // Create the "Spawn Zombie" button
  spawnZombieButton = createButton('Spawn Zombie');
  spawnZombieButton.position(10, spawnPersonButton.y + spawnPersonButton.height + 10); // Position below Spawn Person button
  spawnZombieButton.style('font-size', '18px');
  spawnZombieButton.style('padding', '10px 15px');
  spawnZombieButton.style('background-color', '#dc3545'); // Red for zombies
  spawnZombieButton.style('color', 'white');
  spawnZombieButton.style('border', 'none');
  spawnZombieButton.style('border-radius', '5px');
  spawnZombieButton.style('cursor', 'pointer');
  spawnZombieButton.mousePressed(spawnNewZombie); // Attach new handler
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Clear Previous Buttons for (let type in buildTypeButtons) { buildTypeButtons[type].remove(); }

Removes all old item type buttons from the DOM before creating new ones (necessary when levels unlock new items)

for-loop Create Item Type Buttons for (let type in itemData) { if (itemData[type].unlockLevel <= level) { ... } }

Loops through all item types in itemData and creates a button for each one unlocked at the current level

calculation Button Positioning Logic buttonX += button.width + buttonSpacing;

Calculates the x position for each new button by adding the previous button's width, creating a horizontal row

for (let type in buildTypeButtons) { buildTypeButtons[type].remove(); }
Loops through all stored buttons and calls remove() to delete them from the page; necessary when upgrading levels
buildTypeButtons = {};
Resets the buildTypeButtons object to empty so it can be repopulated with new buttons
if (itemData[type].unlockLevel <= level) {
Only creates a button if the item is unlocked at the current level—e.g., level 1 items show at setup, level 2 items appear after first upgrade
button.mousePressed(() => selectBuildType(type));
Attaches a callback so clicking the button calls selectBuildType(type) to select that item for building
buildTypeButtons[type] = button;
Stores the button in the object so it can be updated later (e.g., to highlight when selected)
buttonX += button.width + buttonSpacing;
Moves the x position to the right by the button width plus spacing, so the next button appears to the right
buildButton.position(10, buttonY + 60);
Positions the 'Build' confirmation button below all the item type buttons
spawnPersonButton.position(10, buildButton.y + buildButton.height + 10);
Positions the 'Spawn Person' button below the Build button, maintaining consistent spacing

selectBuildType(type)

selectBuildType() updates the game state and UI when a player taps an item type button. It highlights the selected button, updates the main Build button's text and color, and clears the previously selected build spot. This is a good example of UI state synchronization in a game.

function selectBuildType(type) {
  currentBuildType = type;
  selectedSpotIndex = -1; // Deselect any spot when a new item type is chosen
  // Update button styles
  for (let btnType in buildTypeButtons) {
    buildTypeButtons[btnType].style('background-color', btnType === currentBuildType ? '#4CAF50' : '#f0f0f0');
    buildTypeButtons[btnType].style('color', btnType === currentBuildType ? 'white' : '#333');
  }
  buildButton.html(`Build ${currentBuildType} ($${itemData[currentBuildType].cost})`);
  buildButton.style('background-color', '#007bff'); // Ensure build button is active color
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Highlight Selected Button for (let btnType in buildTypeButtons) { buildTypeButtons[btnType].style('background-color', btnType === currentBuildType ? '#4CAF50' : '#f0f0f0'); }

Changes the visual style of item type buttons to highlight the currently selected one in green

currentBuildType = type;
Stores the selected item type so confirmBuild() knows what to build
selectedSpotIndex = -1;
Clears any previously selected build spot, forcing the player to tap a new spot for this item
for (let btnType in buildTypeButtons) {
Loops through all item type buttons to update their visual styles
buildTypeButtons[btnType].style('background-color', btnType === currentBuildType ? '#4CAF50' : '#f0f0f0');
Uses a ternary operator to make the selected button green (#4CAF50) and all others light gray (#f0f0f0)
buildButton.html(`Build ${currentBuildType} ($${itemData[currentBuildType].cost})`);
Updates the main Build button's text to show the selected item and its cost
buildButton.style('background-color', '#007bff');
Changes the Build button color to blue to indicate it's ready to be pressed

confirmBuild()

confirmBuild() validates the player's build selection and adds the item to the world. It handles two distinct cases: zombies (which are mobile and don't block spots) and regular items/graves/guns (which occupy a spot). Notice how it filters out graves in certain states when checking occupancy—this allows graves to be replaced after they finish consuming and leaving.

function confirmBuild() {
  if (currentBuildType === null) {
    console.log("Please select an item type first.");
    return;
  }
  if (selectedSpotIndex === -1) {
    console.log("Please tap a build spot first.");
    return;
  }

  const spot = buildSpots[selectedSpotIndex];
  const itemDataForBuild = itemData[currentBuildType];

  // Logic for building a zombie
  if (currentBuildType === "zombie") {
    // Money deduction still happens, but with infinite money, it's not a concern
    money -= itemDataForBuild.cost;
    zombies.push(new Zombie(spot.x, spot.y)); // Create zombie at the build spot
    // Zombies are mobile and do not permanently occupy a build spot,
    // so the spot remains available for other items.
    currentBuildType = null;
    selectedSpotIndex = -1;
    buildButton.html('Select Item');
    buildButton.style('background-color', '#6c757d'); // Dim build button
    for (let btnType in buildTypeButtons) {
      buildTypeButtons[btnType].style('background-color', '#f0f0f0');
      buildTypeButtons[btnType].style('color', '#333');
    }
  }
  // Logic for building regular items, graves, or guns
  else {
    // Check if spot is empty (only for regular items, graves, and guns)
    let spotTaken = false;
    for (let item of items) {
      // Close enough to be the same spot, ignore graves that are consuming, empty, or leaving
      if (dist(item.pos.x, item.pos.y, spot.x, spot.y) < 1 && !(item.isGrave && (item.state === 'consuming' || item.state === 'empty' || item.state === 'leaving'))) {
        spotTaken = true;
        break;
      }
    }

    if (spotTaken) {
      console.log("This spot is already taken.");
      selectedSpotIndex = -1; // Deselect spot
      return;
    }

    // Money deduction still happens, but with infinite money, it's not a concern
    money -= itemDataForBuild.cost;
    // For graves, pass deadZombieRef and initialState
    if (currentBuildType === "grave") {
      items.push(new Item(spot.x, spot.y, currentBuildType, itemDataForBuild.value, itemDataForBuild.color, null, 'idle'));
    } else {
      items.push(new Item(spot.x, spot.y, currentBuildType, itemDataForBuild.value, itemDataForBuild.color));
    }
    currentBuildType = null; // Clear selected item type
    selectedSpotIndex = -1; // Clear selected spot
    buildButton.html('Select Item');
    buildButton.style('background-color', '#6c757d'); // Dim build button
    // Reset item type button styles
    for (let btnType in buildTypeButtons) {
      buildTypeButtons[btnType].style('background-color', '#f0f0f0');
      buildTypeButtons[btnType].style('color', '#333');
    }
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Validate Build Selection if (currentBuildType === null) { console.log("Please select an item type first."); return; } if (selectedSpotIndex === -1) { console.log("Please tap a build spot first."); return; }

Prevents building if the player hasn't selected an item type or a build spot yet

for-loop Spot Occupancy Check for (let item of items) { if (dist(item.pos.x, item.pos.y, spot.x, spot.y) < 1 && !(item.isGrave && (item.state === 'consuming' || item.state === 'empty' || item.state === 'leaving'))) { spotTaken = true; break; } }

Loops through all items to see if any are already on the selected spot; ignores empty/leaving graves

if (currentBuildType === null) { console.log("Please select an item type first."); return; }
Checks if an item has been selected; if not, logs a message and exits early
if (selectedSpotIndex === -1) { console.log("Please tap a build spot first."); return; }
Checks if a build spot has been selected; if not (index is -1), logs a message and exits early
const spot = buildSpots[selectedSpotIndex];
Retrieves the p5.Vector position of the selected build spot from the buildSpots array
const itemDataForBuild = itemData[currentBuildType];
Looks up the cost, value, and color of the selected item type from the itemData object
if (currentBuildType === "zombie") {
Checks if the player is building a zombie (which behaves differently—it moves and doesn't occupy a spot)
money -= itemDataForBuild.cost;
Deducts the item's cost from the player's money (even with infinite money, this maintains game logic)
zombies.push(new Zombie(spot.x, spot.y));
Creates a new Zombie object at the selected spot's position and adds it to the zombies array
if (dist(item.pos.x, item.pos.y, spot.x, spot.y) < 1 && !(item.isGrave && ...)) { spotTaken = true; break; }
Checks if an item is within 1 pixel of the selected spot AND is not an empty/leaving grave; marks spot as taken if true
items.push(new Item(spot.x, spot.y, currentBuildType, itemDataForBuild.value, itemDataForBuild.color));
Creates a new Item object and adds it to the items array at the selected spot
currentBuildType = null; selectedSpotIndex = -1;
Resets both the selected item type and build spot so the player must choose again

spawnNewPerson()

spawnNewPerson() is called when the player taps the 'Spawn Person' button. It's a simple helper that creates a new Person and resets the automatic spawn timer—preventing the manual spawn from being immediately followed by an auto-spawn.

function spawnNewPerson() {
  people.push(new Person());
  console.log("Manually spawned a new person!");
  // Reset the automatic spawn timer so it doesn't immediately spawn another person
  personSpawnTimer = 0;
}
Line-by-line explanation (3 lines)
people.push(new Person());
Creates a new Person object with default values (spawns at top, heading downward) and adds it to the people array
console.log("Manually spawned a new person!");
Logs a message to the console for debugging—helps confirm the button was pressed
personSpawnTimer = 0;
Resets the automatic spawn timer to 0, so the auto-spawner won't immediately create another person after this manual spawn

spawnNewZombie()

spawnNewZombie() creates a red zombie at a random build spot when the player taps the 'Spawn Zombie' button. Red zombies are initially faster than normal zombies but slow dramatically when a customer picks up a gun.

function spawnNewZombie() {
  let randomSpot = random(buildSpots); // Spawn at a random build spot
  zombies.push(new Zombie(randomSpot.x, randomSpot.y, true)); // Spawn a red zombie
  console.log("Manually spawned a new red zombie!");
}
Line-by-line explanation (3 lines)
let randomSpot = random(buildSpots);
Picks a random p5.Vector from the buildSpots array using p5's random() function
zombies.push(new Zombie(randomSpot.x, randomSpot.y, true));
Creates a new Zombie at the random spot's position with isRed = true (third parameter), making it a red/challenging zombie
console.log("Manually spawned a new red zombie!");
Logs a debug message confirming the zombie was created

checkLevelUp()

checkLevelUp() is called every frame and checks if the player has earned enough money to advance. When a level-up occurs, it increments the level variable and calls createBuildUI() to reveal newly unlocked items. With infinite money, the player reaches higher levels almost immediately, making this a quick progression system.

function checkLevelUp() {
  // With infinite money, you'll reach max level very quickly.
  // This logic remains the same, just triggers faster.
  if (level === 1 && money >= 100) {
    level = 2;
    createBuildUI(); // Unlock new items and recreate buttons
  } else if (level === 2 && money >= 250) {
    level = 3;
    createBuildUI(); // Unlock new items and recreate buttons
  }
  // Add more level conditions as needed
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Level 1 to 2 Upgrade if (level === 1 && money >= 100) { level = 2; createBuildUI(); }

Upgrades the player from level 1 to 2 when they accumulate $100, unlocking new items

conditional Level 2 to 3 Upgrade else if (level === 2 && money >= 250) { level = 3; createBuildUI(); }

Upgrades the player from level 2 to 3 when they accumulate $250, unlocking more items

if (level === 1 && money >= 100) {
Checks if the player is currently at level 1 AND has earned at least $100
level = 2;
Increments the level variable from 1 to 2
createBuildUI();
Recreates all UI buttons, which now shows the newly unlocked level 2 items like lamp and bookshelf

drawBuildSpots()

drawBuildSpots() visualizes the 3×2 grid of build positions. Each spot is drawn as an outlined rectangle, and the currently selected spot is highlighted in red with a thicker border. This function is called every frame in draw() so the highlight updates as the player taps new spots.

function drawBuildSpots() {
  noFill();
  stroke(150);
  strokeWeight(2);
  for (let i = 0; i < buildSpots.length; i++) {
    let spot = buildSpots[i];
    rectMode(CENTER);
    if (i === selectedSpotIndex) {
      stroke(255, 0, 0); // Highlight selected spot in red
      strokeWeight(4);
    } else {
      stroke(150);
      strokeWeight(2);
    }
    rect(spot.x, spot.y, ITEM_SIZE + 10, ITEM_SIZE + 10, 5); // Draw a frame
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Draw Each Build Spot for (let i = 0; i < buildSpots.length; i++) { let spot = buildSpots[i]; ... rect(spot.x, spot.y, ITEM_SIZE + 10, ITEM_SIZE + 10, 5); }

Loops through all build spots and draws each one as an outline; highlights the selected spot in red

noFill();
Disables filling, so only the outline of rectangles will be drawn
stroke(150);
Sets the outline color to medium gray (150 in RGB)
for (let i = 0; i < buildSpots.length; i++) {
Loops through all 6 build spots
if (i === selectedSpotIndex) { stroke(255, 0, 0); strokeWeight(4); }
If this is the selected spot, changes the stroke to red and makes it thicker (4 pixels) to highlight it
rect(spot.x, spot.y, ITEM_SIZE + 10, ITEM_SIZE + 10, 5);
Draws a rectangle at the spot's position with slight padding (ITEM_SIZE + 10) and rounded corners (5 pixel radius)

touchStarted()

touchStarted() handles all touch/mouse input and uses a priority system: (1) if building, select a spot; (2) if armed, target a zombie; (3) otherwise, pre-select a spot. Returning false prevents default behavior (like UI button handlers firing), while returning true allows them. This function is crucial for making the game playable on both desktop and mobile.

function touchStarted() {
  // Check if a build spot was touched OR if a zombie was tapped for shooting
  let touchX = touches.length > 0 ? touches[0].x : mouseX;
  let touchY = touches.length > 0 ? touches[0].y : mouseY;

  // 1. If an item type is selected, prioritize build spot selection
  if (currentBuildType !== null) {
    for (let i = 0; i < buildSpots.length; i++) {
      let spot = buildSpots[i];
      if (dist(touchX, touchY, spot.x, spot.y) < ITEM_SIZE / 2 + 5) {
        selectedSpotIndex = i;
        console.log("Selected spot for building:", i);
        return false; // Handled, prevent default
      }
    }
    console.log("Tap a build spot to confirm building.");
    return true; // Let p5.Element handlers fire if no spot hit
  }

  // 2. If no item type is selected, check for tapping a zombie to make a person shoot
  if (people.some(p => p.hasGun)) { // Only if at least one person has a gun
    let closestZombie = null;
    let minDistanceToZombie = Infinity;

    for (let zombie of zombies) {
      // Only target active zombies (no more idle red zombies)
      if (zombie.state === 'moving') {
        let d = dist(touchX, touchY, zombie.pos.x, zombie.pos.y);
        if (d < ZOMBIE_SIZE / 2) { // Tapped directly on zombie
          if (d < minDistanceToZombie) {
            closestZombie = zombie;
            minDistanceToZombie = d;
          }
        }
      }
    }

    if (closestZombie) {
      // Find the closest person with a gun and make them target this zombie
      let closestShooter = null;
      let minDistanceToPerson = Infinity;
      for (let person of people) {
        if (person.hasGun && person.state !== 'shooting' && person.state !== 'leaving') {
          let d = dist(closestZombie.pos.x, closestZombie.pos.y, person.pos.x, person.pos.y);
          if (d < minDistanceToPerson) {
            closestShooter = person;
            minDistanceToPerson = d;
          }
        }
      }

      if (closestShooter) {
        closestShooter.target = closestZombie;
        closestShooter.targetIsSpot = false; // Not a spot
        closestShooter.state = 'moving'; // Will move to shoot position and then shoot
        console.log("Person sent to shoot zombie!");
        return false; // Handled, prevent default
      }
    }
  }

  // 3. Otherwise, if no item type selected and no zombie tapped,
  //    check for tapping a build spot to select it for future building.
  for (let i = 0; i < buildSpots.length; i++) {
    let spot = buildSpots[i];
    if (dist(touchX, touchY, spot.x, spot.y) < ITEM_SIZE / 2 + 5) {
      selectedSpotIndex = i;
      console.log("Selected spot for future building:", i);
      return false; // Handled, prevent default
    }
  }

  // If no specific interaction was handled, return true to allow p5.Element mousePressed
  // handlers to fire (e.g., for UI buttons).
  return true;
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Get Touch Coordinates let touchX = touches.length > 0 ? touches[0].x : mouseX; let touchY = touches.length > 0 ? touches[0].y : mouseY;

Retrieves the first touch point if available, otherwise falls back to mouse coordinates (works on both touch and desktop)

for-loop Build Spot Selection (Priority 1) for (let i = 0; i < buildSpots.length; i++) { let spot = buildSpots[i]; if (dist(touchX, touchY, spot.x, spot.y) < ITEM_SIZE / 2 + 5) { selectedSpotIndex = i; ... } }

If an item is selected, prioritizes checking if a build spot was tapped and selects it

for-loop Zombie Targeting (Priority 2) for (let zombie of zombies) { if (zombie.state === 'moving') { let d = dist(touchX, touchY, zombie.pos.x, zombie.pos.y); if (d < ZOMBIE_SIZE / 2) { ... } } }

If a person has a gun and no build is pending, checks if a zombie was tapped and directs the closest armed person to shoot it

for-loop Build Spot Pre-Selection (Priority 3) for (let i = 0; i < buildSpots.length; i++) { let spot = buildSpots[i]; if (dist(touchX, touchY, spot.x, spot.y) < ITEM_SIZE / 2 + 5) { selectedSpotIndex = i; ... } }

If no build type is selected and no zombie tapped, allows pre-selecting a build spot for future building

let touchX = touches.length > 0 ? touches[0].x : mouseX;
Uses a ternary to check if a touch exists; if yes, use the first touch's x coordinate; otherwise, use the mouse x
if (currentBuildType !== null) {
Checks if an item type is currently selected—if so, prioritize build spot selection
if (dist(touchX, touchY, spot.x, spot.y) < ITEM_SIZE / 2 + 5) {
Uses dist() to check if the tap is within about 35 pixels of a build spot center
selectedSpotIndex = i;
Stores the index of the tapped build spot so confirmBuild() knows where to place the item
return false;
Prevents the default p5.js behavior, stopping button clicks from firing on the tapped spot
if (people.some(p => p.hasGun)) {
Uses array.some() to check if at least one person in the people array has a gun
if (zombie.state === 'moving') {
Only allows targeting active (moving) zombies, not dead or leaving ones
closestShooter.state = 'moving';
Sets the person's state to moving, which will cause them to move to a shooting position and start shooting
return true;
Returns true to allow p5.Element button handlers to fire (so clicking UI buttons works normally)

Item (class)

The Item class represents furniture, graves, and guns. Most items are static decorations; graves are special and animate through multiple states: idle (waiting to spawn), spawning (hosting a red zombie), consuming (sinking the dead zombie), and leaving (moving off-screen). The display() method uses a switch statement to draw each item type's unique appearance.

class Item {
  constructor(x, y, type, value, color, deadZombieRef = null, initialState = 'idle') {
    this.pos = createVector(x, y);
    this.type = type;
    this.value = value;
    this.color = color;
    this.isGrave = (type === "grave"); // Flag for graves
    this.isGun = (type === "gun"); // Flag for guns
    if (this.isGrave) {
      this.spawnTimer = 0;
      this.zombieSpawned = false;
      this.deadZombieRef = deadZombieRef; // To store reference to the dead zombie it spawned
      this.state = initialState; // 'idle', 'spawning', 'consuming', 'empty', 'leaving'
      this.consumeProgress = 0;
      this.leaveTimer = 0; // Timer for grave to leave if zombie not killed
      this.GRAVE_LEAVE_TIMEOUT = 1800; // Increased to 30 seconds (1800 frames at 60fps)
      this.GRAVE_LEAVE_DURATION = 120; // 2 seconds to animate leaving
      if (this.state === 'consuming') {
        // If an auto-grave, it immediately starts the consuming animation
        console.log("Auto-grave appeared and is consuming zombie.");
      }
    }
  }

  update() {
    if (this.isGrave) {
      if (this.state === 'idle') {
        this.spawnTimer++;
        if (this.spawnTimer > 180) { // Spawn zombie after 3 seconds
          this.spawnZombie();
          this.state = 'spawning';
          this.leaveTimer = 0; // Reset leave timer when zombie is spawned
        }
      } else if (this.state === 'spawning') {
        // Check if zombie died
        if (this.deadZombieRef && this.deadZombieRef.state === 'dead') {
          this.state = 'consuming';
          this.leaveTimer = 0; // Reset leave timer, now consuming
        } else {
          // If zombie is still alive, grave starts its leaving timeout
          this.leaveTimer++;
          if (this.leaveTimer > this.GRAVE_LEAVE_TIMEOUT) {
            console.log("Grave timed out, zombie not killed. Grave is leaving.");
            // If the zombie is still moving, it should also leave
            if (this.deadZombieRef && (this.deadZombieRef.state === 'moving' || this.deadZombieRef.state === 'idle')) { // Also check for idle state
              this.deadZombieRef.state = 'leaving';
              // If it's a red zombie and it didn't kill anyone, set its speed back to normal
              if (this.deadZombieRef.isRed) {
                this.deadZombieRef.speed = ZOMBIE_SPEED;
                console.log("Red zombie leaving at normal speed due to grave timeout.");
              }
            }
            this.deadZombieRef = null; // Grave no longer cares about this zombie
            this.state = 'leaving';
            this.leaveTimer = 0; // Reset for grave's leaving animation
          }
        }
      } else if (this.state === 'consuming') {
        if (this.deadZombieRef) {
          // Animate dead zombie sinking into grave
          let targetY = this.pos.y + ITEM_SIZE / 2;
          this.deadZombieRef.pos.y = lerp(this.deadZombieRef.pos.y, targetY, 0.05);
          if (this.deadZombieRef.pos.y >= targetY - 1) {
            // Zombie fully consumed, mark it for removal
            this.deadZombieRef.state = 'consumed';
            this.deadZombieRef = null;
            this.state = 'leaving'; // Grave leaves after consuming
            this.leaveTimer = 0; // Reset for grave's leaving animation
            console.log("Grave consumed zombie, grave is leaving.");
          }
        } else {
          // Should not happen, but as a fallback, grave leaves
          this.state = 'leaving';
          this.leaveTimer = 0;
        }
      } else if (this.state === 'leaving') {
        this.pos.y += PERSON_SPEED; // Move downwards like people/zombies
        if (this.pos.y > height + ITEM_SIZE) {
          this.state = 'empty'; // Mark for removal when completely off-screen
        }
      }
    }
  }

  spawnZombie() {
    if (!this.zombieSpawned) {
      // Spawn a red zombie at the grave's position
      let newZombie = new Zombie(this.pos.x, this.pos.y, true); // true for isRed
      zombies.push(newZombie);
      this.zombieSpawned = true;
      this.deadZombieRef = newZombie; // Link grave to the zombie it spawned
      console.log("Red zombie spawned from grave!");
      // Unlock gun for people on first red zombie spawn
      if (!people.some(p => p.hasGun)) {
        people.forEach(p => p.hasGun = true);
        console.log("People now have guns!");
      }
    }
  }

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

    if (this.isGrave) {
      if (this.state === 'empty') {
        // Grave disappears
      } else { // Draw grave normally for all other states including 'leaving'
        fill(this.color);
        stroke(50);
        strokeWeight(2);
        rect(0, 0, ITEM_SIZE * 0.8, ITEM_SIZE * 0.5, 5); // Grave stone
        fill(200);
        rect(0, ITEM_SIZE * 0.1, ITEM_SIZE * 0.3, ITEM_SIZE * 0.1); // Cross horizontally
        rect(0, ITEM_SIZE * 0.1, ITEM_SIZE * 0.1, ITEM_SIZE * 0.3); // Cross vertically
      }
    } else if (this.isGun) {
      fill(this.color); // Grey for gun
      stroke(50);
      strokeWeight(2);
      rect(-ITEM_SIZE / 4, 0, ITEM_SIZE / 2, ITEM_SIZE / 4); // Barrel
      rect(ITEM_SIZE / 4, 0, ITEM_SIZE / 4, ITEM_SIZE / 2); // Handle
    } else {
      // Existing item display logic
      fill(this.color);
      stroke(50);
      strokeWeight(2);
      switch (this.type) {
        case "chair":
          rect(-ITEM_SIZE / 4, 0, ITEM_SIZE / 2, ITEM_SIZE); // Backrest
          rect(ITEM_SIZE / 4, 0, ITEM_SIZE / 2, ITEM_SIZE / 2); // Seat
          break;
        case "table":
          rect(0, 0, ITEM_SIZE, ITEM_SIZE / 2); // Tabletop
          rectMode(CORNER);
          rect(-ITEM_SIZE / 2 + 5, ITEM_SIZE / 4, 10, ITEM_SIZE / 4); // Leg 1
          rect(ITEM_SIZE / 2 - 15, ITEM_SIZE / 4, 10, ITEM_SIZE / 4); // Leg 2
          break;
        case "tree":
          fill('#8B4513'); // Trunk color
          rect(0, ITEM_SIZE / 4, ITEM_SIZE / 4, ITEM_SIZE / 2); // Trunk
          fill(this.color); // Leaves color
          ellipse(0, -ITEM_SIZE / 4, ITEM_SIZE * 0.8, ITEM_SIZE * 0.8); // Leaves
          break;
        case "lamp":
          ellipse(0, -ITEM_SIZE / 4, ITEM_SIZE / 2); // Shade
          rect(0, 0, 5, ITEM_SIZE / 2); // Stand
          rect(0, ITEM_SIZE / 4, ITEM_SIZE / 4, 5); // Base
          break;
        case "sofa":
          rect(0, 0, ITEM_SIZE * 1.5, ITEM_SIZE / 2); // Seat
          rect(-ITEM_SIZE * 0.7, -ITEM_SIZE / 4, ITEM_SIZE * 0.2, ITEM_SIZE / 2); // Armrest 1
          rect(ITEM_SIZE * 0.7, -ITEM_SIZE / 4, ITEM_SIZE * 0.2, ITEM_SIZE / 2); // Armrest 2
          rect(0, -ITEM_SIZE / 2, ITEM_SIZE * 1.5, ITEM_SIZE / 4); // Backrest
          break;
        case "bookshelf":
          rect(0, 0, ITEM_SIZE, ITEM_SIZE); // Main frame
          fill(this.color + 'A0'); // Slightly lighter color for shelves
          rect(-ITEM_SIZE / 4, -ITEM_SIZE / 4, ITEM_SIZE / 2, 5); // Shelf 1
          rect(-ITEM_SIZE / 4, ITEM_SIZE / 4, ITEM_SIZE / 2, 5); // Shelf 2
          break; // Added break here
        case "food":
          ellipse(0, 0, ITEM_SIZE * 0.6, ITEM_SIZE * 0.6); // Generic food shape (e.g., a bun)
          fill('#FFA07A'); // A light orange/brown for "crust"
          ellipse(0, 0, ITEM_SIZE * 0.4, ITEM_SIZE * 0.4); // Inner detail
          break;
        case "car":
          rect(0, 0, ITEM_SIZE, ITEM_SIZE / 2); // Car body
          fill('#555'); // Wheel color
          ellipse(-ITEM_SIZE / 4, ITEM_SIZE / 4, ITEM_SIZE / 4); // Front wheel
          ellipse(ITEM_SIZE / 4, ITEM_SIZE / 4, ITEM_SIZE / 4); // Back wheel
          rect(-ITEM_SIZE / 4, -ITEM_SIZE / 4, ITEM_SIZE / 2, ITEM_SIZE / 4); // Window/roof
          break;
        default:
          rect(0, 0, ITEM_SIZE, ITEM_SIZE);
          break;
      }
    }
    pop();
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Grave Spawn Logic (update) if (this.state === 'idle') { this.spawnTimer++; if (this.spawnTimer > 180) { this.spawnZombie(); this.state = 'spawning'; } }

After 3 seconds (180 frames), a grave spawns a red zombie and transitions to spawning state

calculation Grave Consuming Animation this.deadZombieRef.pos.y = lerp(this.deadZombieRef.pos.y, targetY, 0.05);

Smoothly lerps the dead zombie's y position toward the grave using lerp() for a sinking animation

switch-case Item Display Switch Statement switch (this.type) { case "chair": ... case "table": ... }

Draws different item shapes based on the type—chair, table, tree, lamp, etc.

this.pos = createVector(x, y);
Stores the item's position as a p5.Vector so it can be moved and animated
this.isGrave = (type === "grave");
Sets a flag to true if this item is a grave, used later to determine special behavior
this.state = initialState;
Sets the initial state—'idle' for regular graves, or 'consuming' for auto-graves from dead zombies
if (this.spawnTimer > 180) { this.spawnZombie(); }
After 180 frames (3 seconds), calls spawnZombie() to create a red zombie at this grave's position
if (this.deadZombieRef && this.deadZombieRef.state === 'dead') { this.state = 'consuming'; }
Checks if the zombie spawned by this grave is now dead, then transitions to consuming state
let targetY = this.pos.y + ITEM_SIZE / 2; this.deadZombieRef.pos.y = lerp(this.deadZombieRef.pos.y, targetY, 0.05);
Calculates a target position below the grave and smoothly moves the zombie toward it using lerp()
people.forEach(p => p.hasGun = true);
Gives all people guns when the first red zombie spawns, enabling the gun mechanic
switch (this.type) { case "chair": ... }
Uses a switch statement to draw different shapes for each item type—chair, table, tree, lamp, sofa, bookshelf, food, car

Person (class)

The Person class is the most complex in the game. Each person is an autonomous agent with a state machine (moving, grabbing, waiting, shooting, leaving) and a multi-priority AI system for finding targets. The findTarget() method uses a smart 4-level priority system, and pathfinding uses p5.Vector math to move smoothly toward targets. People can pick up guns, which enables them to shoot zombies and triggers game state changes.

🔬 This code finds the closest moving zombie. What if you changed `zombie.state === 'moving'` to `zombie.state !== 'leaving'`? Would armed people target dead zombies or other states?

    // 1. If person already has a gun, prioritize targeting the closest active Zombie
    if (this.hasGun) {
      for (let zombie of zombies) {
        if (zombie.state === 'moving') { // Only target active zombies (red zombies now spawn moving)
          let d = dist(this.pos.x, this.pos.y, zombie.pos.x, zombie.pos.y);
          if (d < closestDistance) {
            this.target = zombie; // Store the actual Zombie object
            closestDistance = d;
          }
        }
      }
      if (this.target) {
        this.shootingZombieRef = this.target; // Link person to the zombie they are shooting
        return; // Found a zombie, no need to look for items/spots
      }
    }
class Person {
  constructor() {
    this.pos = createVector(random(width), -PERSON_SIZE); // Spawn off-screen top
    this.speed = PERSON_SPEED;
    this.target = null; // Can be an Item object, a p5.Vector (for empty spot), or a Zombie object
    this.targetIsSpot = false; // Flag to distinguish target type
    this.state = 'moving'; // 'moving', 'grabbing', 'waiting_at_empty_spot', 'shooting', 'leaving'
    this.grabTimer = 0;
    this.hasGun = false; // Initially no gun
    this.shootTimer = 0;
    this.shootCooldown = 30; // Frames between shots (0.5 seconds)
    this.shootingZombieRef = null; // To store the zombie being shot
  }

  findTarget() {
    this.target = null;
    this.targetIsSpot = false;
    let closestDistance = Infinity;

    // 1. If person already has a gun, prioritize targeting the closest active Zombie
    if (this.hasGun) {
      for (let zombie of zombies) {
        if (zombie.state === 'moving') { // Only target active zombies (red zombies now spawn moving)
          let d = dist(this.pos.x, this.pos.y, zombie.pos.x, zombie.pos.y);
          if (d < closestDistance) {
            this.target = zombie; // Store the actual Zombie object
            closestDistance = d;
          }
        }
      }
      if (this.target) {
        this.shootingZombieRef = this.target; // Link person to the zombie they are shooting
        return; // Found a zombie, no need to look for items/spots
      }
    }

    // 2. If person does NOT have a gun, prioritize targeting the closest 'gun' item
    closestDistance = Infinity;
    let closestGun = null;
    for (let item of items) {
      if (item.isGun) { // Check specifically for the 'gun' item
        let d = dist(this.pos.x, this.pos.y, item.pos.x, item.pos.y);
        if (d < closestDistance) {
          closestGun = item;
          closestDistance = d;
        }
      }
    }
    if (closestGun) {
      this.target = closestGun;
      this.targetIsSpot = false; // It's an item, not a spot
      return;
    }

    // 3. If no 'gun' item is found (or person already has a gun and no zombies),
    //    try to find the closest available OTHER Item (not a grave in special states)
    closestDistance = Infinity; // Reset closest distance again for other item search
    for (let item of items) {
      // Don't target graves that are consuming, empty, or leaving, AND don't target guns (already handled)
      if (!item.isGun && !(item.isGrave && (item.state === 'consuming' || item.state === 'empty' || item.state === 'leaving'))) {
        let d = dist(this.pos.x, this.pos.y, item.pos.x, item.pos.y);
        if (d < closestDistance) {
          this.target = item;
          closestDistance = d;
        }
      }
    }

    // 4. If no Items (gun or other) are found, try to find the closest empty build spot
    if (!this.target) {
      closestDistance = Infinity;
      let closestEmptySpot = null;

      for (let i = 0; i < buildSpots.length; i++) {
        let spot = buildSpots[i];
        let spotOccupied = false;
        for (let item of items) {
          if (dist(item.pos.x, item.pos.y, spot.x, spot.y) < 1 && !(item.isGrave && (item.state === 'consuming' || item.state === 'empty' || item.state === 'leaving'))) {
            spotOccupied = true;
            break;
          }
        }

        if (!spotOccupied) {
          let d = dist(this.pos.x, this.pos.y, spot.x, spot.y);
          if (d < closestDistance) {
            closestEmptySpot = spot;
            closestDistance = d;
          }
        }
      }

      if (closestEmptySpot) {
        this.target = closestEmptySpot;
        this.targetIsSpot = true;
      } else {
        this.target = null;
        this.targetIsSpot = false;
      }
    } else {
      this.targetIsSpot = false;
    }
  }

  update() {
    if (this.state === 'moving') {
      if (!this.target) {
        this.findTarget(); // Try to find a target (zombie, item, or empty spot)
      }

      if (this.target) {
        let targetPos;
        let finalMovePosX;
        let finalMovePosY;

        if (this.targetIsSpot) {
          targetPos = this.target; // It's a P5.Vector (empty spot)
          finalMovePosX = targetPos.x;
          finalMovePosY = targetPos.y;
        } else if (this.target instanceof Item) {
          targetPos = this.target.pos; // It's an Item object
          finalMovePosX = targetPos.x;
          finalMovePosY = targetPos.y;
        } else if (this.target instanceof Zombie) {
          targetPos = this.target.pos; // It's a Zombie object
          // Aim for a shooting position slightly to the left of the zombie
          finalMovePosX = targetPos.x - PERSON_SIZE;
          finalMovePosY = targetPos.y;
        }

        let direction = p5.Vector.sub(createVector(finalMovePosX, finalMovePosY), this.pos);
        direction.setMag(this.speed);
        this.pos.add(direction);

        // Check if close enough to transition to next state
        if (dist(this.pos.x, this.pos.y, finalMovePosX, finalMovePosY) < this.speed) {
          if (this.targetIsSpot) {
            this.state = 'waiting_at_empty_spot';
            this.pos.set(targetPos.x, targetPos.y); // Ensure exact spot
          } else if (this.target instanceof Item) {
            if (this.target.isGun) {
              // Person grabbed the gun
              this.hasGun = true;
              items = items.filter(item => item !== this.target); // Remove the gun item
              console.log("Person picked up a gun!");

              gunGrabbed = true; // Set the global flag!
              console.log("Gun grabbed! All red zombies will now slow down.");

              // Update speeds of all existing red zombies
              for (let zombie of zombies) {
                if (zombie.isRed && zombie.state === 'moving') {
                  zombie.speed = RED_ZOMBIE_SPEED;
                }
              }

              // Spawn a new red zombie at a random build spot
              let randomSpot = random(buildSpots);
              zombies.push(new Zombie(randomSpot.x, randomSpot.y, true)); // This new zombie will use the updated 'gunGrabbed' flag

              this.target = null; // Clear target to find the new zombie or another item
              this.targetIsSpot = false;
              this.state = 'moving'; // Continue looking for targets
            } else {
              this.state = 'grabbing';
              this.grabTimer = GRAB_TIME;
            }
          } else if (this.target instanceof Zombie) {
            this.state = 'shooting'; // Start shooting the zombie
            this.shootTimer = 0; // Reset shoot timer for first shot
            this.pos.set(finalMovePosX, finalMovePosY); // Ensure exact shooting position
          }
        }
      } else {
        // If no targets, just leave
        this.pos.y += 1;
        if (this.pos.y > height + PERSON_SIZE) {
          this.state = 'leaving'; // Mark for removal
        }
      }
    } else if (this.state === 'grabbing') {
      this.grabTimer--;
      if (this.grabTimer <= 0) {
        if (this.target && this.target instanceof Item) { // Ensure it's an Item
          // Only add money if it's not a grave, or if it's a grave that has already consumed a zombie
          // (meaning it's now 'empty' and ready to be removed for its 'value' — though grave value is 0)
          if (!this.target.isGrave) {
            money += this.target.value;
            items = items.filter(item => item !== this.target);
          } else if (this.target.isGrave && this.target.state === 'empty') {
            // Grave has finished its cycle and is now 'empty', remove it.
            // Its value is 0, so no money is added.
            items = items.filter(item => item !== this.target);
          }
          // If it's a grave that is 'idle' or 'spawning' or 'consuming', it should not be removed by grabbing.
        }
        this.target = null;
        this.targetIsSpot = false;
        this.state = 'leaving'; // After grabbing/interacting, they leave
      }
    } else if (this.state === 'waiting_at_empty_spot') {
      // Stay at the spot and continuously look for targets
      this.findTarget(); // Re-evaluate target
      if (this.target && !(this.targetIsSpot)) { // If an Item or Zombie appeared
        this.state = 'moving'; // Go grab/shoot it
      } else if (!this.target) {
        // If the spot they were waiting at is now occupied (e.g., by another item),
        // or all build spots are gone, or no items/empty spots are available
        // anywhere, they should eventually leave.
        this.state = 'leaving';
      }
      // If this.target is still an empty spot (and this.targetIsSpot is true),
      // they remain in this state and continue waiting.
    } else if (this.state === 'shooting') {
      if (this.shootingZombieRef && this.shootingZombieRef.state === 'moving') {
        // Calculate shooting position relative to the zombie
        let targetPos = this.shootingZombieRef.pos;
        let shootingPosX = targetPos.x - PERSON_SIZE; // Aim for a shooting position slightly to the left
        let shootingPosY = targetPos.y;

        // Move towards the shooting position if not already there
        if (dist(this.pos.x, this.pos.y, shootingPosX, shootingPosY) > this.speed) {
          let direction = p5.Vector.sub(createVector(shootingPosX, shootingPosY), this.pos);
          direction.setMag(this.speed);
          this.pos.add(direction);
        } else {
          // If close enough, ensure exact position and start shooting timer
          this.pos.set(shootingPosX, shootingPosY);
          this.shootTimer++;
          if (this.shootTimer >= this.shootCooldown) {
            this.shootingZombieRef.takeDamage();
            console.log(`Person shot zombie, health: ${this.shootingZombieRef.health}`);
            this.shootTimer = 0;
          }
        }
      } else {
        // Zombie is dead or gone, person leaves
        this.shootingZombieRef = null;
        this.target = null;
        this.targetIsSpot = false;
        this.state = 'leaving';
        console.log("Person finished shooting, leaving.");
      }
    } else if (this.state === 'leaving') {
      this.pos.y += this.speed;
    }
  }

  display() {
    push();
    translate(this.pos.x, this.pos.y);
    fill(100, 150, 200); // Blue color for people
    stroke(50);
    strokeWeight(1);
    ellipse(0, 0, PERSON_SIZE); // Body
    fill(255);
    ellipse(-PERSON_SIZE / 4, -PERSON_SIZE / 4, PERSON_SIZE / 8); // Eye 1
    ellipse(PERSON_SIZE / 4, -PERSON_SIZE / 4, PERSON_SIZE / 8); // Eye 2
    arc(0, PERSON_SIZE / 8, PERSON_SIZE / 4, PERSON_SIZE / 8, 0, PI); // Mouth

    if (this.state === 'grabbing' && this.target && this.target instanceof Item) {
      // Draw a "grab" animation
      let progress = map(this.grabTimer, GRAB_TIME, 0, 0, 1);
      fill(255, 100, 100);
      noStroke();
      ellipse(0, 0, PERSON_SIZE * progress * 1.2);
    } else if (this.state === 'waiting_at_empty_spot') {
      // Draw a "waiting" icon (yellow triangle)
      fill(255, 200, 0); // Yellow
      noStroke();
      triangle(0, -PERSON_SIZE / 2, -PERSON_SIZE / 4, -PERSON_SIZE / 4, PERSON_SIZE / 4, -PERSON_SIZE / 4);
    } else if (this.state === 'shooting' && this.hasGun) {
      // Draw a gun
      fill(100);
      rectMode(CORNER);
      rect(PERSON_SIZE / 4, -PERSON_SIZE / 8, PERSON_SIZE / 2, PERSON_SIZE / 8); // Barrel
      rect(PERSON_SIZE / 4, PERSON_SIZE / 8, PERSON_SIZE / 4, PERSON_SIZE / 4); // Handle
      rectMode(CENTER);

      // Draw a muzzle flash if firing
      if (this.shootTimer > this.shootCooldown - 5 && this.shootTimer < this.shootCooldown) {
        fill(255, 200, 0); // Yellow flash
        ellipse(PERSON_SIZE / 4 + PERSON_SIZE / 2 + 5, -PERSON_SIZE / 8 + 2, 8);
      }
    }
    pop();
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop Multi-Priority Target Finding if (this.hasGun) { for (let zombie of zombies) { if (zombie.state === 'moving') { ... } } } ... for (let item of items) { if (item.isGun) { ... } } ... for (let i = 0; i < buildSpots.length; i++) { ... }

Uses a 4-priority system to find targets: (1) active zombies if armed, (2) guns if not armed, (3) other items, (4) empty build spots

calculation Vector-Based Pathfinding let direction = p5.Vector.sub(createVector(finalMovePosX, finalMovePosY), this.pos); direction.setMag(this.speed); this.pos.add(direction);

Calculates a direction vector toward the target, normalizes it to the person's speed, and moves toward the target smoothly

conditional Person State Machine if (this.state === 'moving') { ... } else if (this.state === 'grabbing') { ... } else if (this.state === 'shooting') { ... } else if (this.state === 'leaving') { ... }

Manages the person's behavior through 5 states: moving, grabbing, waiting, shooting, leaving

this.pos = createVector(random(width), -PERSON_SIZE);
Spawns the person at a random x position off the top of the screen (negative y)
this.state = 'moving';
Sets the initial state to moving so the person immediately starts looking for targets
if (this.hasGun) { for (let zombie of zombies) { if (zombie.state === 'moving') { ... } } }
If this person has a gun, prioritizes finding the closest moving zombie to shoot
for (let item of items) { if (item.isGun) { ... } }
Searches for a gun item to pick up (second priority for unarmed people)
let direction = p5.Vector.sub(createVector(finalMovePosX, finalMovePosY), this.pos);
Calculates a vector from the person's position to the target using p5.Vector.sub()
direction.setMag(this.speed);
Normalizes the direction vector to the person's speed so they move at a consistent pace
this.pos.add(direction);
Adds the direction to the person's position, moving them one step toward the target
gunGrabbed = true;
Sets a global flag when the gun is picked up, used to slow red zombies
let progress = map(this.grabTimer, GRAB_TIME, 0, 0, 1);
Maps the remaining grab timer to a 0–1 range for animation (uses map() to convert ranges)

Zombie (class)

The Zombie class represents both normal zombies (built by the player) and red zombies (spawned from graves). Red zombies are more dangerous and can be slowed when a person picks up a gun. Zombies use pathfinding similar to people, moving toward the closest target person. When a red zombie dies, it triggers an auto-grave spawn that consumes the body.

class Zombie {
  constructor(x, y, isRed = false) {
    this.pos = createVector(x, y); // Spawn at the build spot
    this.isRed = isRed; // Set isRed flag first

    // Determine speed based on isRed and whether a gun has been grabbed yet
    if (this.isRed) {
      this.speed = gunGrabbed ? RED_ZOMBIE_SPEED : ZOMBIE_SPEED; // Slow if gun grabbed, normal otherwise
    } else {
      this.speed = ZOMBIE_SPEED; // Normal zombies always ZOMBIE_SPEED
    }

    this.targetPerson = null;
    this.state = 'moving'; // Red zombies now start moving immediately, others too
    this.health = 3; // Zombies take 3 hits to die
    this.deadTimer = 0;
    this.killDistance = this.isRed ? KILL_DISTANCE * 0.8 : KILL_DISTANCE; // Red zombies are more aggressive
  }

  findTargetPerson() {
    let closestPerson = null;
    let minDistance = Infinity;

    for (let person of people) {
      // Don't target people who are leaving
      // Red zombies should target anyone else, including those who are shooting them!
      if (person.state !== 'leaving') {
        let d = dist(this.pos.x, this.pos.y, person.pos.x, person.pos.y);
        if (d < minDistance) {
          closestPerson = person;
          minDistance = d;
        }
      }
    }
    this.targetPerson = closestPerson;
  }

  update() {
    // Red zombies no longer have an 'idle' state, they start moving immediately
    if (this.state === 'moving') {
      if (!this.targetPerson) {
        this.findTargetPerson();
      }

      if (this.targetPerson) {
        let targetPos = this.targetPerson.pos;
        let direction = p5.Vector.sub(targetPos, this.pos);
        direction.setMag(this.speed);
        this.pos.add(direction);

        if (dist(this.pos.x, this.pos.y, targetPos.x, targetPos.y) < this.killDistance) {
          // Kill the person
          let personIndex = people.indexOf(this.targetPerson);
          if (personIndex > -1) {
            people.splice(personIndex, 1);
            console.log("A person was killed!");
          }
          this.state = 'leaving'; // Zombie leaves after killing
          // IMPORTANT: If red zombie kills, it leaves at normal speed
          if (this.isRed) {
            this.speed = ZOMBIE_SPEED;
            console.log("Red zombie leaving at normal speed after kill.");
          }
        }
      } else {
        // If no people to target, just move down the screen to leave
        this.pos.y += 1;
        if (this.pos.y > height + ZOMBIE_SIZE) {
          this.state = 'leaving'; // Mark for removal
        }
      }
    } else if (this.state === 'dead') {
      // Zombie is dead, waiting to be consumed by grave (or leave if no grave)
      this.deadTimer++;
      // The grave's update() method will handle the consuming animation
    } else if (this.state === 'leaving' || this.state === 'consumed') {
      // Handled by grave for consumed, or just move off-screen for leaving
      this.pos.y += this.speed;
    }
  }

  takeDamage() {
    this.health--;
    if (this.health <= 0) {
      this.state = 'dead';
      // If it's a red zombie, create an auto-grave to consume it
      if (this.isRed) {
        let autoGrave = new Item(this.pos.x, this.pos.y, "grave", 0, itemData["grave"].color, this, 'consuming');
        items.push(autoGrave);
      }
      console.log("Zombie died!");
    }
  }

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

    if (this.state === 'dead' || this.state === 'consuming') {
      // Draw a falling/sinking animation for dead zombie
      rotate(PI / 2); // Lay flat
      fill(this.isRed ? '#FF0000' : itemData["zombie"].color); // Red or original color
      stroke(50);
      strokeWeight(1);
      rect(0, 0, ZOMBIE_SIZE, ZOMBIE_SIZE * 0.7); // Body
      fill(this.isRed ? '#FF0000' : itemData["zombie"].color); // Head
      ellipse(0, -ZOMBIE_SIZE * 0.4, ZOMBIE_SIZE * 0.8);
      fill(50); // Eyes
      ellipse(-ZOMBIE_SIZE * 0.2, -ZOMBIE_SIZE * 0.45, ZOMBIE_SIZE * 0.1);
      ellipse(ZOMBIE_SIZE * 0.2, -ZOMBIE_SIZE * 0.45, ZOMBIE_SIZE * 0.1);
    } else {
      // Normal zombie display
      fill(this.isRed ? '#FF0000' : itemData["zombie"].color); // Red or original color
      stroke(50);
      strokeWeight(1);
      rect(0, 0, ZOMBIE_SIZE * 0.7, ZOMBIE_SIZE); // Body
      fill(this.isRed ? '#FF0000' : itemData["zombie"].color); // Head
      ellipse(0, -ZOMBIE_SIZE * 0.4, ZOMBIE_SIZE * 0.8);
      fill(50); // Eyes
      ellipse(-ZOMBIE_SIZE * 0.2, -ZOMBIE_SIZE * 0.45, ZOMBIE_SIZE * 0.1);
      ellipse(ZOMBIE_SIZE * 0.2, -ZOMBIE_SIZE * 0.45, ZOMBIE_SIZE * 0.1);

      // Display health bar
      if (this.isRed) { // Only show health bar for red zombies
        noStroke();
        fill(100);
        rect(0, ZOMBIE_SIZE / 2 + 5, ZOMBIE_SIZE, 5); // Background
        fill(0, 200, 0);
        rectMode(CORNER);
        rect(-ZOMBIE_SIZE / 2, ZOMBIE_SIZE / 2 + 5, map(this.health, 0, 3, 0, ZOMBIE_SIZE), 5); // Health
        rectMode(CENTER);
      }
    }
    pop();
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Speed Determination Logic if (this.isRed) { this.speed = gunGrabbed ? RED_ZOMBIE_SPEED : ZOMBIE_SPEED; } else { this.speed = ZOMBIE_SPEED; }

Red zombies check the gunGrabbed flag to determine if they should move slowly; normal zombies always move at ZOMBIE_SPEED

calculation Health Bar Display fill(0, 200, 0); rectMode(CORNER); rect(-ZOMBIE_SIZE / 2, ZOMBIE_SIZE / 2 + 5, map(this.health, 0, 3, 0, ZOMBIE_SIZE), 5);

Uses map() to scale the health bar width based on remaining health (0–3), showing a visual indicator for red zombies

this.isRed = isRed;
Stores whether this is a red zombie (spawned from graves) or a normal one (built by player)
this.speed = gunGrabbed ? RED_ZOMBIE_SPEED : ZOMBIE_SPEED;
Uses a ternary to check the global gunGrabbed flag—red zombies move slowly if true, normally if false
this.state = 'moving';
All zombies (including red) start in the moving state and immediately begin hunting
this.killDistance = this.isRed ? KILL_DISTANCE * 0.8 : KILL_DISTANCE;
Red zombies have a slightly smaller kill distance (more aggressive), calculated with a ternary
if (person.state !== 'leaving') {
Only targets people who are not in the leaving state, including those who are shooting the zombie
if (dist(this.pos.x, this.pos.y, targetPos.x, targetPos.y) < this.killDistance) {
Checks if the zombie is close enough to its target person to execute a kill
people.splice(personIndex, 1);
Removes the killed person from the people array using splice()
this.health--;
Decrements health when the zombie takes damage from a person's gunshot
let autoGrave = new Item(this.pos.x, this.pos.y, "grave", 0, itemData["grave"].color, this, 'consuming');
When a red zombie dies, creates an auto-grave that immediately starts consuming animation

📦 Key Variables

money number

Stores the player's total money (set to Infinity for unlimited building)

let money = Infinity;
level number

Tracks the current game level, which unlocks new items to build

let level = 1;
items array

Stores all Item objects (furniture, graves, guns) currently on the canvas

let items = [];
people array

Stores all Person objects (customers) currently on the canvas

let people = [];
zombies array

Stores all Zombie objects currently on the canvas

let zombies = [];
currentBuildType string

Tracks which item type is currently selected for building (e.g., 'chair', 'table', or null if none)

let currentBuildType = null;
selectedSpotIndex number

Stores the index of the currently selected build spot (-1 if none selected)

let selectedSpotIndex = -1;
personSpawnTimer number

Counts frames until the next automatic customer spawn

let personSpawnTimer = 0;
gunGrabbed boolean

Global flag set to true when a person picks up a gun, which slows all red zombies

let gunGrabbed = false;
buildSpots array

Stores p5.Vector positions for the 3×2 grid of build locations

const buildSpots = [];

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG touchStarted()

Typo in constant name: 'PERSON_SPPAWN_INTERVAL' has double 'P' (should be 'PERSON_SPAWN_INTERVAL')

💡 Rename the constant throughout the file to use correct spelling. This is purely cosmetic but good practice for code quality.

PERFORMANCE findTarget() method in Person class

Nested loop through zombies, items, and build spots runs every frame—O(n³) complexity can become slow with many entities

💡 Cache results or use spatial partitioning (e.g., divide canvas into grid cells) to reduce distance checks each frame.

BUG confirmBuild() and draw() loops

Removing items via splice() while iterating forward in some contexts could skip items; backward iteration is correct but inconsistent in touchStarted()

💡 Consistently use backward iteration (i--) when removing from arrays during loops, or use filter() to create new arrays instead of splicing.

FEATURE Game logic overall

No score system or wave progression—game runs indefinitely with no win condition or escalating difficulty

💡 Add a score system tracking total money earned, implement wave-based zombie spawns, or create a score threshold for winning.

STYLE itemData object

Hardcoded costs and values make balancing difficult; color strings are inconsistent (mix of names and hex codes)

💡 Move all item data to a separate config file or use consistent hex color codes throughout for maintainability.

🔄 Code Flow

Code flow showing setup, draw, createbuildui, selectbuildtype, confirmbuild, spawnnewperson, spawnzombie, checklevelup, drawbuildspots, touchstarted, item, person, zombie

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> background-clear[Background Clearing] draw --> info-display[Money and Level Display] draw --> drawbuildspots[drawBuildSpots] draw --> item-loop[Update and Display Items] draw --> people-loop[Update and Display People] draw --> zombie-loop[Update and Display Zombies] draw --> spawn-timer[Automatic Customer Spawning] click setup href "#fn-setup" click draw href "#fn-draw" click background-clear href "#sub-background-clear" click info-display href "#sub-info-display" click drawbuildspots href "#fn-drawbuildspots" click item-loop href "#sub-item-loop" click people-loop href "#sub-people-loop" click zombie-loop href "#sub-zombie-loop" click spawn-timer href "#sub-spawn-timer" item-loop -->|for-loop| check-spot-occupied[check-spot-occupied] item-loop -->|for-loop| switch-display[switch-display] click check-spot-occupied href "#sub-check-spot-occupied" click switch-display href "#sub-switch-display" people-loop -->|for-loop| findtarget-priority[findtarget-priority] people-loop -->|for-loop| pathfinding-logic[pathfinding-logic] people-loop -->|for-loop| state-machine[state-machine] click findtarget-priority href "#sub-findtarget-priority" click pathfinding-logic href "#sub-pathfinding-logic" click state-machine href "#sub-state-machine" zombie-loop -->|for-loop| speed-logic[speed-logic] zombie-loop -->|for-loop| pathfinding-logic click speed-logic href "#sub-speed-logic" drawbuildspots -->|for-loop| draw-loop[draw-loop] click draw-loop href "#sub-draw-loop" setup --> canvas-creation[Responsive Canvas Setup] setup --> build-spot-grid[Build Spot Grid Creation] setup --> createbuildui[createBuildUI] click canvas-creation href "#sub-canvas-creation" click build-spot-grid href "#sub-build-spot-grid" click createbuildui href "#fn-createbuildui" createbuildui --> clear-old-buttons[Clear Previous Buttons] createbuildui --> create-item-buttons[Create Item Type Buttons] createbuildui --> position-buttons[Button Positioning Logic] createbuildui --> update-button-styles[Highlight Selected Button] click clear-old-buttons href "#sub-clear-old-buttons" click create-item-buttons href "#sub-create-item-buttons" click position-buttons href "#sub-position-buttons" click update-button-styles href "#sub-update-button-styles" draw --> checklevelup[checkLevelUp] checklevelup --> level-1-check[level-1-check] checklevelup --> level-2-check[level-2-check] click checklevelup href "#fn-checklevelup" click level-1-check href "#sub-level-1-check" click level-2-check href "#sub-level-2-check" touchstarted --> get-touch-coords[get-touch-coords] touchstarted --> build-spot-selection[build-spot-selection] touchstarted --> zombie-targeting[zombie-targeting] touchstarted --> spot-pre-selection[spot-pre-selection] click get-touch-coords href "#sub-get-touch-coords" click build-spot-selection href "#sub-build-spot-selection" click zombie-targeting href "#sub-zombie-targeting" click spot-pre-selection href "#sub-spot-pre-selection" confirmbuild --> validate-selection[validate-selection] confirmbuild --> grave-spawn-logic[grave-spawn-logic] confirmbuild --> grave-consuming-animation[grave-consuming-animation] click validate-selection href "#sub-validate-selection" click grave-spawn-logic href "#sub-grave-spawn-logic" click grave-consuming-animation href "#sub-grave-consuming-animation" spawnnewperson --> spawnnewperson[spawnNewPerson] click spawnnewperson href "#fn-spawnnewperson" spawnzombie --> spawnzombie[spawnNewZombie] click spawnzombie href "#fn-spawnzombie"

Preview

People button to is fix spawn - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of People button to is fix spawn - Code flow showing setup, draw, createbuildui, selectbuildtype, confirmbuild, spawnnewperson, spawnzombie, checklevelup, drawbuildspots, touchstarted, item, person, zombie
Code Flow Diagram