Foooooooood

This is a restaurant cooking game where players manage food orders by clicking menu items to cook them in microwaves and fridges, then transferring them to chopping boards to serve. The game tracks score, failed orders, and customer satisfaction through emoji reactions and animated arrivals/departures.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up order completion — Increase the score awarded for completing orders to reward faster play and make it easier to build a high score
  2. Make the customer stay longer — Increase how long customers show their happy/angry reaction before leaving, giving you more time to see the feedback
  3. Add more microwave slots — Increase the number of simultaneous microwave items, making it easier to prepare multiple hot foods at once
  4. Make orders harder — Allow customers to order up to 5 of each item instead of 3, requiring more items per order
  5. Change the customer's neutral face — Give the customer a different starting emoji to change their personality
  6. Make the game unforgiving — Reduce the number of failed orders allowed before game over, making the game harder
Prefer the full editor? Open it there →

📖 About This Sketch

Foooooooood is an interactive restaurant cooking game built entirely in p5.js. Players click food emoji in a menu bar to cook items in microwaves (for hot food) or fridges (for cold food), then transfer them through chopping boards to serve customer orders. The sketch demonstrates professional game architecture: a state machine managing MENU, PLAYING, and GAME_OVER modes; array-based inventory systems for tracking items in three cooking stations; click detection to map tap coordinates to interactive areas; and emoji-driven visual feedback through customer facial expressions and animated entrances/exits.

The code is organized into setup() which initializes the game and layout, draw() which renders all game elements and runs the main game loop, and specialized functions for game logic (updateGame), input handling (mousePressed), customer animation (drawCustomerAndOrder), and state management (generateNewOrder, checkOrderComplete). By studying this sketch, you will learn how to build a complete interactive game: designing responsive click-target areas, managing complex state with strings and timers, using arrays to simulate real-world inventory, and creating satisfying visual feedback through animation and emoji.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, initializes 21 food items (hot or cold), creates three p5.Oscillator sound effects for tap/ding/buzz/payment feedback, and calculates responsive layout dimensions that adapt to any screen size.
  2. The draw() function runs 60 times per second. It clears the background, draws five UI zones (menu bar at top showing score and failed orders, customer area with animated emoji and speech bubble, microwave section, chopping boards, and fridge), and renders items currently stored in each zone.
  3. When the player taps the screen during PLAYING state, mousePressed() determines which interactive area was touched. Tapping the menu bar adds a food item to either the microwave (hot foods) or fridge (cold foods). Tapping the microwave transfers a cooked item to an empty chopping board slot. Tapping a chopping board checks if that item matches the current order and increments the served count if it does.
  4. A new customer arrives by sliding in from the left edge. Their speech bubble displays the current order (e.g., '🥚 x2 🥓 x1'). Once the player taps the customer, the game transitions to PLAYING_ORDER state, allowing items to be served.
  5. updateGame() continuously checks if the order is complete by comparing servedCount to quantity for every item. When all items are served, the customer's face changes from 😐 to 😊 (happy), a ding sound plays, and 100 points plus a speed bonus are added to the score.
  6. The customer then displays '💰 PAID!' for 1.5 seconds, slides off-screen to the right, and a new customer slides in from the left. If the player fails to serve an order (currently only possible through future logic), failedOrders increments, and once it reaches MAX_FAILED_ORDERS (3), the game transitions to GAME_OVER.

🎓 Concepts You'll Learn

Game state machinesArray-based inventory systemsClick/tap detection and hit testingAnimation with millis() and lerp()Responsive canvas layoutSound synthesis with p5.OscillatorEmoji-driven UIObject properties and tracking

📝 Code Breakdown

setup()

setup() runs once when the sketch first loads. It's the ideal place to initialize all your game data, create objects, and prepare sound resources. Because sound requires user interaction on mobile devices, setup() calls userStartAudio() early to ensure audio works when the player first taps the screen.

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

  // Initialize menu foods with type property
  menuFoods = [
    { emoji: "🥚", name: "Egg", type: "hot" },
    { emoji: "🥓", name: "Bacon", type: "hot" },
    { emoji: "🌭", name: "Hotdog", type: "hot" },
    { emoji: "🧇", name: "Waffle", type: "hot" },
    { emoji: "🍦", name: "Ice Cream", type: "cold" },
    { emoji: "🥗", name: "Salad", type: "cold" },
    { emoji: "🫐", name: "Blueberry", type: "cold" },
    { emoji: "🍍", name: "Pineapple", type: "cold" },
    { emoji: "🥑", name: "Avocado", type: "cold" },
    { emoji: "🥐", name: "Croissant", type: "hot" },
    { emoji: "🧄", name: "Garlic", type: "hot" },
    { emoji: "🫚", name: "Ginger", type: "hot" },
    { emoji: "🥨", name: "Pretzel", type: "hot" },
    { emoji: "🥞", name: "Pancakes", type: "hot" },
    { emoji: "🦴", name: "Bone", type: "hot" },
    { emoji: "🫓", name: "Flatbread", type: "hot" },
    { emoji: "🌯", name: "Burrito", type: "hot" },
    { emoji: "🥫", name: "Canned Food", type: "hot" },
    { emoji: "🍛", name: "Curry", type: "hot" },
    { emoji: "🍤", name: "Shrimp", type: "hot" },
    { emoji: "🥠", name: "Fortune Cookie", type: "hot" }
  ];

  // Calculate initial layout parameters
  calculateLayout();

  textAlign(CENTER, CENTER);

  userStartAudio();

  // Create simple sound effects using p5.Oscillator
  tapSound = new p5.Oscillator();
  tapSound.setType('sine');
  tapSound.freq(440);
  tapSound.amp(0);
  tapSound.start();

  dingSound = new p5.Oscillator();
  dingSound.setType('triangle');
  dingSound.freq(880);
  dingSound.amp(0);
  dingSound.start();

  buzzSound = new p5.Oscillator();
  buzzSound.setType('square');
  buzzSound.freq(220);
  buzzSound.amp(0);
  buzzSound.start();

  paymentSound = new p5.Oscillator();
  paymentSound.setType('sine');
  paymentSound.freq(1200);
  paymentSound.amp(0);
  paymentSound.start();
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

initialization Canvas Setup createCanvas(windowWidth, windowHeight);

Creates a full-window canvas that fills the entire screen

data-structure Menu Foods Array menuFoods = [ { emoji: "🥚", name: "Egg", type: "hot" }, ... ];

Initializes array of 21 food objects, each with emoji, name, and hot/cold type for routing to correct cooking station

initialization Sound Effect Setup tapSound = new p5.Oscillator(); tapSound.setType('sine'); tapSound.freq(440); tapSound.amp(0); tapSound.start();

Creates four oscillators (tap, ding, buzz, payment) that will play sound feedback for user interactions

createCanvas(windowWidth, windowHeight);
Creates a canvas that matches the full browser window size, allowing the game to be responsive to any screen dimension
menuFoods = [{ emoji: "🥚", name: "Egg", type: "hot" }, ...];
Populates the menuFoods array with 21 food objects. Each object stores an emoji, human-readable name, and a type (either 'hot' or 'cold') that determines whether it goes to the microwave or fridge
calculateLayout();
Calls a function that calculates all the x, y, width, and height values for each game zone (menu, customer area, microwave, chopping, fridge) based on the current canvas size
textAlign(CENTER, CENTER);
Sets global text alignment so that all emojis and text are centered around their x, y coordinates—this is critical for emoji to appear visually correct
userStartAudio();
p5.sound library function that enables audio on touch devices by requesting user permission—required on mobile before any sound can play
tapSound = new p5.Oscillator(); tapSound.setType('sine'); tapSound.freq(440); tapSound.amp(0); tapSound.start();
Creates an oscillator with sine wave at 440Hz (musical note A4), sets its volume to 0 (silent), and starts it running. When the player taps, the code will ramp the amplitude up then down to create a short beep

draw()

draw() is the heart of every p5.js sketch. It runs 60 times per second and redraw the entire canvas each time. The order of operations matters: background() clears the old frame first, then all drawing functions render new content on top, then game logic at the end updates state for the next frame. Notice that draw() doesn't directly handle clicks—that happens in mousePressed()—draw() only reads the results of those clicks (like items in microwaveItems array) and displays them.

function draw() {
  background(220);

  // Draw Menu Bar
  fill(50);
  noStroke();
  rect(menuX, menuY, menuW, menuH);

  // Draw game UI elements within the menu bar (score, timer, failed orders)
  drawGameUI();

  // Display menu food emojis (below the UI elements in the menu bar)
  textSize(menuEmojiSize);
  fill(255);
  textAlign(CENTER, CENTER);
  for (let i = 0; i < menuFoods.length; i++) {
    const food = menuFoods[i];
    const x = menuX + (i + 0.5) * (menuW / menuFoods.length);
    text(food.emoji, x, menuY + menuH / 2 + menuH / 4);
  }

  // Draw Customer and Order Bubble
  if (gameState === 'PLAYING') {
    drawCustomerAndOrder();
  }

  // Draw Microwave Area
  fill(200);
  stroke(0);
  strokeWeight(2);
  rect(microwaveX, microwaveY, microwaveW, microwaveH);

  fill(0);
  noStroke();
  textSize(menuEmojiSize * 0.4);
  textAlign(CENTER, CENTER);
  text('Microwaves', microwaveX + microwaveW / 2, microwaveY + microwaveH / 4);

  // Draw individual microwave slots and display items
  const slotWidth = microwaveW / NUM_MICROWAVES;
  for (let i = 0; i < NUM_MICROWAVES; i++) {
    const slotX = microwaveX + i * slotWidth;
    fill(180);
    stroke(0);
    strokeWeight(1);
    rect(slotX, microwaveY, slotWidth, microwaveH);

    if (microwaveItems[i]) {
      const item = microwaveItems[i];
      textSize(microwaveEmojiSize);
      textAlign(CENTER, CENTER);
      text(item.emoji, slotX + slotWidth / 2, microwaveY + microwaveH / 2);
    }
  }

  // Draw Chopping Boards Area
  fill(139, 69, 19);
  noStroke();
  rect(choppingAreaX, choppingAreaY, choppingAreaW, choppingAreaH);

  fill(255);
  textSize(menuEmojiSize * 0.4);
  textAlign(CENTER, CENTER);
  text('Chopping Boards', choppingAreaX + choppingAreaW / 2, choppingAreaY + choppingAreaH / 4);

  // Draw individual chopping board slots and display items
  const boardWidth = choppingAreaW / NUM_CHOPPING_BOARDS;
  for (let i = 0; i < NUM_CHOPPING_BOARDS; i++) {
    const boardX = choppingAreaX + i * boardWidth;
    fill(160, 82, 45);
    stroke(0);
    strokeWeight(1);
    rect(boardX, choppingAreaY, boardWidth, choppingAreaH);

    if (choppingBoards[i]) {
      const item = choppingBoards[i];
      textSize(choppedEmojiSize);
      textAlign(CENTER, CENTER);
      text(item.emoji, boardX + boardWidth / 2, choppingAreaY + choppingAreaH / 2);
    }
  }

  // Draw Fridge Area
  fill(100, 100, 200);
  stroke(0);
  strokeWeight(2);
  rect(fridgeX, fridgeY, fridgeW, fridgeH);

  fill(255);
  noStroke();
  textSize(menuEmojiSize * 0.4);
  textAlign(CENTER, CENTER);
  text('Fridge', fridgeX + fridgeW / 2, fridgeY + fridgeH / 4);

  // Draw individual fridge slots and display items
  const fridgeSlotWidth = fridgeW / NUM_FRIDGE_SLOTS;
  for (let i = 0; i < NUM_FRIDGE_SLOTS; i++) {
    const fridgeSlotX = fridgeX + i * fridgeSlotWidth;
    fill(120, 120, 220);
    stroke(0);
    strokeWeight(1);
    rect(fridgeSlotX, fridgeY, fridgeSlotWidth, fridgeH);

    if (fridgeItems[i]) {
      const item = fridgeItems[i];
      textSize(microwaveEmojiSize);
      textAlign(CENTER, CENTER);
      text(item.emoji, fridgeSlotX + fridgeSlotWidth / 2, fridgeY + fridgeH / 2);
    }
  }

  // --- Update Game Logic based on State ---
  if (gameState === 'PLAYING') {
    updateGame();
  } else if (gameState === 'MENU') {
    drawMenuScreen();
  } else if (gameState === 'GAME_OVER') {
    drawGameOverScreen();
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Food Emoji Display Loop for (let i = 0; i < menuFoods.length; i++) { const x = menuX + (i + 0.5) * (menuW / menuFoods.length); text(food.emoji, x, menuY + menuH / 2 + menuH / 4); }

Loops through all 21 food items and draws each emoji evenly distributed across the menu bar width

for-loop Microwave Slot Rendering for (let i = 0; i < NUM_MICROWAVES; i++) { const slotX = microwaveX + i * slotWidth; fill(180); stroke(0); strokeWeight(1); rect(slotX, microwaveY, slotWidth, microwaveH); if (microwaveItems[i]) { const item = microwaveItems[i]; textSize(microwaveEmojiSize); textAlign(CENTER, CENTER); text(item.emoji, slotX + slotWidth / 2, microwaveY + microwaveH / 2); } }

Draws 5 separate microwave slot rectangles; if an item exists in that slot, renders its emoji centered

for-loop Chopping Board Slot Rendering for (let i = 0; i < NUM_CHOPPING_BOARDS; i++) { const boardX = choppingAreaX + i * boardWidth; fill(160, 82, 45); stroke(0); strokeWeight(1); rect(boardX, choppingAreaY, boardWidth, choppingAreaH); if (choppingBoards[i]) { const item = choppingBoards[i]; textSize(choppedEmojiSize); textAlign(CENTER, CENTER); text(item.emoji, boardX + boardWidth / 2, choppingAreaY + choppingAreaH / 2); } }

Draws 2 chopping board slot rectangles; renders emoji for any item present in each slot

for-loop Fridge Slot Rendering for (let i = 0; i < NUM_FRIDGE_SLOTS; i++) { const fridgeSlotX = fridgeX + i * fridgeSlotWidth; fill(120, 120, 220); stroke(0); strokeWeight(1); rect(fridgeSlotX, fridgeY, fridgeSlotWidth, fridgeH); if (fridgeItems[i]) { const item = fridgeItems[i]; textSize(microwaveEmojiSize); textAlign(CENTER, CENTER); text(item.emoji, fridgeSlotX + fridgeSlotWidth / 2, fridgeY + fridgeH / 2); } }

Draws 4 fridge slot rectangles; renders emoji for any cold food item currently stored

conditional Game State Routing if (gameState === 'PLAYING') { updateGame(); } else if (gameState === 'MENU') { drawMenuScreen(); } else if (gameState === 'GAME_OVER') { drawGameOverScreen(); }

Routes program logic based on current game state: PLAYING runs game updates, MENU/GAME_OVER overlay their screens

background(220);
Clears the entire canvas with light gray (value 220). This must happen every frame to erase the previous frame and prevent motion trails
fill(50); noStroke(); rect(menuX, menuY, menuW, menuH);
Draws the dark gray (50) menu bar rectangle at the top. noStroke() removes the black outline so it looks solid
const x = menuX + (i + 0.5) * (menuW / menuFoods.length);
Calculates the center x position of the i-th food item's slot. The (i + 0.5) centers it within its slot, and dividing by menuFoods.length (21) distributes all items evenly across the menu width
text(food.emoji, x, menuY + menuH / 2 + menuH / 4);
Draws the food emoji at the calculated position. The y position is offset down by menuH / 4 to leave space for the score/timer text above
if (gameState === 'PLAYING') { drawCustomerAndOrder(); }
Only draws the customer and speech bubble when the game is actively playing. During MENU or GAME_OVER states, no customer appears
const slotWidth = microwaveW / NUM_MICROWAVES;
Divides the total microwave area width by 5 to get the width of each individual slot, ensuring slots fit evenly
if (microwaveItems[i]) { const item = microwaveItems[i]; textSize(microwaveEmojiSize); text(item.emoji, slotX + slotWidth / 2, microwaveY + microwaveH / 2); }
Checks if the slot contains an item. If it does, draws the emoji at the slot's center position. If null, the slot appears empty

drawGameUI()

drawGameUI() is a helper function called from draw() that handles just the UI elements in the menu bar. Breaking code into focused helper functions makes draw() easier to read and changes easier to manage. Notice how the failed orders counter changes color based on how close you are to losing—this is a subtle but effective way to communicate game state to the player without using words.

function drawGameUI() {
  fill(255);
  textSize(menuEmojiSize * 0.4);

  // Display Score (left in menu bar)
  textAlign(LEFT, CENTER);
  text(`Score: ${score}`, menuX + menuW * 0.05, menuY + menuH / 4);

  // Display Failed Orders (right in menu bar)
  textAlign(RIGHT, CENTER);
  fill(255);
  if (failedOrders >= MAX_FAILED_ORDERS - 1) fill(255, 100, 0);
  if (failedOrders >= MAX_FAILED_ORDERS) fill(255, 0, 0);
  text(`Failed: ${failedOrders}/${MAX_FAILED_ORDERS}`, menuX + menuW * 0.95, menuY + menuH / 4);

  // Display Order Timer (center in menu bar)
  textAlign(CENTER, CENTER);
  if (gameState === 'PLAYING') {
    fill(255);
    textSize(menuEmojiSize * 0.4);
    text(`Time: --`, menuX + menuW * 0.5, menuY + menuH / 4);
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

text-render Score Text textAlign(LEFT, CENTER); text(`Score: ${score}`, menuX + menuW * 0.05, menuY + menuH / 4);

Displays current score aligned to the left 5% of the menu bar

conditional-text Failed Orders Counter with Color textAlign(RIGHT, CENTER); fill(255); if (failedOrders >= MAX_FAILED_ORDERS - 1) fill(255, 100, 0); if (failedOrders >= MAX_FAILED_ORDERS) fill(255, 0, 0); text(`Failed: ${failedOrders}/${MAX_FAILED_ORDERS}`, menuX + menuW * 0.95, menuY + menuH / 4);

Displays failed orders count on the right side; changes color to orange when 1 away from game over, then red when game over

conditional-text Order Timer textAlign(CENTER, CENTER); if (gameState === 'PLAYING') { fill(255); textSize(menuEmojiSize * 0.4); text(`Time: --`, menuX + menuW * 0.5, menuY + menuH / 4); }

Displays '--' in the center of the menu bar (no actual time limit currently in effect)

fill(255);
Sets the color to white for all text that follows
textSize(menuEmojiSize * 0.4);
Makes the UI text 40% the size of the menu emojis, creating visual hierarchy
textAlign(LEFT, CENTER);
Aligns text to the left horizontally and vertically centered—good for the left-side score
text(`Score: ${score}`, menuX + menuW * 0.05, menuY + menuH / 4);
Draws the score text at 5% from the left edge of the menu bar. Template literals (backticks and ${}) insert the current score value into the string
if (failedOrders >= MAX_FAILED_ORDERS - 1) fill(255, 100, 0);
If failed orders is at 2 (one away from 3), change the fill color to orange (255, 100, 0) to warn the player
if (failedOrders >= MAX_FAILED_ORDERS) fill(255, 0, 0);
If failed orders reaches 3 (game over threshold), change the fill to red (255, 0, 0) to signal danger
text(`Time: --`, menuX + menuW * 0.5, menuY + menuH / 4);
Displays '--' instead of a countdown since there is no actual time limit (orderTimeLimit is Infinity)

drawCustomerAndOrder()

drawCustomerAndOrder() handles all the visual aspects of the customer: their emoji face, the speech bubble showing the order, and the payment confirmation. The key insight is that the customer's mood (expressed through different emoji) is driven entirely by the customerState variable. Every frame, draw() calls this function, which looks up customerState and renders accordingly. This separation of state (a string) from rendering (what emoji to show) is a professional pattern you'll use in all your games.

function drawCustomerAndOrder() {
  // Draw Customer Emoji
  let customerFace = '😐';
  if (customerState === 'HAPPY') customerFace = '😊';
  if (customerState === 'ANGRY') customerFace = '😡';

  textSize(customerSize);
  fill(0);
  textAlign(CENTER, CENTER);
  text(customerFace, customerAreaX + customerAreaW * 0.1, customerAreaY + customerAreaH / 2 + 2);
  fill(255);
  text(customerFace, customerAreaX + customerAreaW * 0.1, customerAreaY + customerAreaH / 2);

  // Draw Speech Bubble and Order Text only if customer is neutral, happy, angry, or playing order
  if (customerState === 'NEUTRAL' || customerState === 'HAPPY' || customerState === 'ANGRY' || customerState === 'PLAYING_ORDER') {
    const orderText = currentOrder.map(item => `${item.emoji} x${item.quantity}`).join(' ');
    const orderTextWidth = textWidth(orderText);
    const textHeight = textSize();

    const bubbleW = min(width * 0.7, orderTextWidth + bubblePadding * 2);
    const bubbleH = textHeight + bubblePadding * 2;
    const bubbleX = customerAreaX + customerAreaW * 0.2;
    const bubbleY = customerAreaY + customerAreaH / 2 - bubbleH / 2;

    fill(255);
    noStroke();
    rect(bubbleX, bubbleY, bubbleW, bubbleH, 10);

    // Draw speech bubble tail pointing to the customer
    triangle(
      bubbleX + 20, bubbleY + bubbleH,
      bubbleX + 40, bubbleY + bubbleH,
      customerAreaX + customerAreaW * 0.1, customerAreaY + customerAreaH / 2 + customerSize / 2
    );

    // Draw Order Text inside bubble
    fill(0);
    textSize(menuEmojiSize * 0.5);
    textAlign(CENTER, CENTER);
    text(orderText, bubbleX + bubbleW / 2, bubbleY + bubbleH / 2);
  }

  // Draw "PAID!" text if customer is in PAID_DISPLAY state
  if (customerState === 'PAID_DISPLAY') {
    fill(0, 180, 0);
    textSize(menuEmojiSize * 0.6);
    textAlign(CENTER, CENTER);
    text('💰 PAID!', customerAreaX + customerAreaW * 0.5, customerAreaY + customerAreaH / 2);
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Customer Face Selection let customerFace = '😐'; if (customerState === 'HAPPY') customerFace = '😊'; if (customerState === 'ANGRY') customerFace = '😡';

Selects which emoji to display based on customer state: neutral face, happy smile, or angry face

drawing Shadow and Face Rendering fill(0); text(customerFace, customerAreaX + customerAreaW * 0.1, customerAreaY + customerAreaH / 2 + 2); fill(255); text(customerFace, customerAreaX + customerAreaW * 0.1, customerAreaY + customerAreaH / 2);

Draws the emoji twice: first in black offset by 2 pixels (shadow), then in white on top (main)

conditional Speech Bubble Display Logic if (customerState === 'NEUTRAL' || customerState === 'HAPPY' || customerState === 'ANGRY' || customerState === 'PLAYING_ORDER') { ... }

Only renders the speech bubble and order text during these states; hidden when PAID_DISPLAY or LEAVING

calculation Order Text Build const orderText = currentOrder.map(item => `${item.emoji} x${item.quantity}`).join(' ');

Uses map() to create an array of strings like '🥚 x2', then joins them with spaces into a single display string

calculation Speech Bubble Size const bubbleW = min(width * 0.7, orderTextWidth + bubblePadding * 2); const bubbleH = textHeight + bubblePadding * 2;

Calculates bubble width to fit the text with padding, but caps it at 70% of screen width

drawing Speech Bubble Triangle Tail triangle( bubbleX + 20, bubbleY + bubbleH, bubbleX + 40, bubbleY + bubbleH, customerAreaX + customerAreaW * 0.1, customerAreaY + customerAreaH / 2 + customerSize / 2 );

Draws a triangle pointing from the bottom of the bubble to the customer emoji

conditional Payment Confirmation if (customerState === 'PAID_DISPLAY') { fill(0, 180, 0); textSize(menuEmojiSize * 0.6); textAlign(CENTER, CENTER); text('💰 PAID!', customerAreaX + customerAreaW * 0.5, customerAreaY + customerAreaH / 2); }

Replaces the order bubble with green '💰 PAID!' text when order is complete

let customerFace = '😐';
Initializes customerFace to the neutral emoji (😐). This variable will be reassigned based on customerState
if (customerState === 'HAPPY') customerFace = '😊';
When customer is happy, change the emoji to a smiling face to show satisfaction
if (customerState === 'ANGRY') customerFace = '😡';
When customer is angry, change the emoji to a red angry face to show frustration
fill(0); text(customerFace, customerAreaX + customerAreaW * 0.1, customerAreaY + customerAreaH / 2 + 2);
Draws the emoji in black, shifted down by 2 pixels. This shadow effect makes the emoji appear to have depth
fill(255); text(customerFace, customerAreaX + customerAreaW * 0.1, customerAreaY + customerAreaH / 2);
Draws the same emoji again in white at the normal position, on top of the black shadow
const orderText = currentOrder.map(item => `${item.emoji} x${item.quantity}`).join(' ');
The map() function transforms each order item into a string like '🥚 x2'. Then join(' ') combines all strings with spaces between them, creating '🥚 x2 🥓 x1'
const orderTextWidth = textWidth(orderText);
Calculates how many pixels wide the order text will be when drawn, needed to size the bubble correctly
const bubbleW = min(width * 0.7, orderTextWidth + bubblePadding * 2);
Sets bubble width to either 70% of screen (if text is very long) or just enough to fit the text with padding on both sides (whichever is smaller)
rect(bubbleX, bubbleY, bubbleW, bubbleH, 10);
Draws a white rounded rectangle with 10-pixel corner radius for the speech bubble
triangle( bubbleX + 20, bubbleY + bubbleH, bubbleX + 40, bubbleY + bubbleH, customerAreaX + customerAreaW * 0.1, customerAreaY + customerAreaH / 2 + customerSize / 2 );
Draws a triangle with two points at the bottom of the bubble and one point at the customer's mouth area, creating the classic speech bubble pointer
text(orderText, bubbleX + bubbleW / 2, bubbleY + bubbleH / 2);
Draws the order text (e.g., '🥚 x2 🥓 x1') centered inside the bubble

updateGame()

updateGame() is the core game logic engine. Every frame, it checks what state the customer is in and performs appropriate transitions: HAPPY → PAID_DISPLAY → LEAVING → NEUTRAL (new customer). It also checks if the player has completed the order and awards points. Notice how timing is critical: each state transition waits for a certain duration (3 seconds for reactions, 1.5 seconds for payment) before moving to the next. This uses millis() to track absolute time rather than counting frames, making animations smooth regardless of frame rate. This state machine pattern (a series of states with timed transitions) is used in almost every game you'll ever build.

function updateGame() {
  // Timer update (always runs)
  const elapsed = millis() - orderStartTime;

  // --- State Transitions for Customer ---

  // 1. If customer is happy, paid, or angry, and their reaction/display duration is over, transition to leaving
  if ((customerState === 'HAPPY' || customerState === 'ANGRY') && millis() - customerStateTimer > CUSTOMER_STATE_DURATION) {
      if (customerState === 'HAPPY') {
          customerState = 'PAID_DISPLAY';
          customerPaidTimer = millis();
          paymentSound.amp(0.5, 0.1);
          paymentSound.amp(0, 0.5);
      } else { // customerState === 'ANGRY'
          // If angry and max failed orders reached, it's game over
          if (failedOrders >= MAX_FAILED_ORDERS) {
              gameState = 'GAME_OVER';
          } else {
              customerState = 'LEAVING';
              customerLeavingTimer = millis();
              // Customer leaves angry, so clear the order and counter
              clearFoodItems();
          }
      }
  }

  // 2. If customer is displaying "PAID!", and payment display duration is over, transition to leaving
  if (customerState === 'PAID_DISPLAY' && millis() - customerPaidTimer > PAYMENT_DISPLAY_DURATION) {
      customerState = 'LEAVING';
      customerLeavingTimer = millis();
      // Order was completed and paid, clear the counter
      clearFoodItems();
  }

  // 3. If customer is leaving, animate their departure
  if (customerState === 'LEAVING') {
      const leaveProgress = (millis() - customerLeavingTimer) / CUSTOMER_LEAVING_DURATION;
      // Animate customerAreaX from normal position to off-screen right
      customerAreaX = lerp(0, width * 1.2, leaveProgress);

      if (leaveProgress >= 1) {
          // Customer has left, reset for new arrival
          customerState = 'NEUTRAL';
          customerAreaX = width * -0.2; // Position off-screen left for new arrival animation
          generateNewOrder(); // Generate a new order for the next customer
      }
  }

  // 4. Time's up logic is removed since orderTimeLimit is Infinity.
  //    Orders will only fail if MAX_FAILED_ORDERS is reached through other means (e.g., future penalties).

  // 5. If current order is complete and customer is not already reacting, paid, or leaving, it's a happy completion
  if (checkOrderComplete() && (customerState === 'PLAYING_ORDER' || customerState === 'NEUTRAL')) {
      console.log("Order complete!");
      dingSound.amp(0.5, 0.1);
      dingSound.amp(0, 0.5);
      score += 100 + (orderTimeLimit - elapsed) / 100;
      customerState = 'HAPPY';
      customerStateTimer = millis();
  }

  // 6. Animate customer arrival (only if neutral and off-screen left)
  if (customerState === 'NEUTRAL' && customerAreaX < 0) {
      const arrivalProgress = (millis() - orderStartTime) / CUSTOMER_ARRIVAL_DURATION;
      // Animate customerAreaX from off-screen left to normal position
      customerAreaX = lerp(width * -0.2, 0, arrivalProgress);
      if (arrivalProgress >= 1) {
          customerAreaX = 0;
      }
  }
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

conditional Happy/Angry State Transition if ((customerState === 'HAPPY' || customerState === 'ANGRY') && millis() - customerStateTimer > CUSTOMER_STATE_DURATION) { if (customerState === 'HAPPY') { customerState = 'PAID_DISPLAY'; customerPaidTimer = millis(); paymentSound.amp(0.5, 0.1); paymentSound.amp(0, 0.5); } else { if (failedOrders >= MAX_FAILED_ORDERS) { gameState = 'GAME_OVER'; } else { customerState = 'LEAVING'; customerLeavingTimer = millis(); clearFoodItems(); } } }

After 3 seconds in HAPPY or ANGRY state, transitions to PAID_DISPLAY (if happy), GAME_OVER (if angry and max fails reached), or LEAVING (if angry but game continues)

conditional Payment Display Transition if (customerState === 'PAID_DISPLAY' && millis() - customerPaidTimer > PAYMENT_DISPLAY_DURATION) { customerState = 'LEAVING'; customerLeavingTimer = millis(); clearFoodItems(); }

After 1.5 seconds of showing '💰 PAID!', transitions to LEAVING and clears all food items from stations

animation Customer Departure Animation if (customerState === 'LEAVING') { const leaveProgress = (millis() - customerLeavingTimer) / CUSTOMER_LEAVING_DURATION; customerAreaX = lerp(0, width * 1.2, leaveProgress); if (leaveProgress >= 1) { customerState = 'NEUTRAL'; customerAreaX = width * -0.2; generateNewOrder(); } }

Smoothly slides customer off the right side of the screen over 1 second, then resets for next customer

conditional Order Complete Victory if (checkOrderComplete() && (customerState === 'PLAYING_ORDER' || customerState === 'NEUTRAL')) { console.log("Order complete!"); dingSound.amp(0.5, 0.1); dingSound.amp(0, 0.5); score += 100 + (orderTimeLimit - elapsed) / 100; customerState = 'HAPPY'; customerStateTimer = millis(); }

When all order items are served, plays a ding sound, adds 100+ points to score, and transitions customer to HAPPY state

animation Customer Arrival Animation if (customerState === 'NEUTRAL' && customerAreaX < 0) { const arrivalProgress = (millis() - orderStartTime) / CUSTOMER_ARRIVAL_DURATION; customerAreaX = lerp(width * -0.2, 0, arrivalProgress); if (arrivalProgress >= 1) { customerAreaX = 0; } }

Smoothly slides customer in from the left side of the screen over 1 second at the start of each order

const elapsed = millis() - orderStartTime;
Calculates how many milliseconds have passed since the order started by comparing current time (millis()) to when orderStartTime was set
if ((customerState === 'HAPPY' || customerState === 'ANGRY') && millis() - customerStateTimer > CUSTOMER_STATE_DURATION) {
Checks two things: (1) customer is in HAPPY or ANGRY state, AND (2) 3000ms (3 seconds) have passed since they entered that state. If both are true, it's time to transition
if (customerState === 'HAPPY') {
Nested condition: if the customer is happy specifically (not angry), proceed with happy transition
customerState = 'PAID_DISPLAY';
Changes state to PAID_DISPLAY, which will trigger the '💰 PAID!' text to appear in drawCustomerAndOrder()
paymentSound.amp(0.5, 0.1); paymentSound.amp(0, 0.5);
Ramps the payment sound up to amplitude 0.5 over 0.1 seconds, then ramps back down to 0 over 0.5 seconds—creates a quick beep
if (failedOrders >= MAX_FAILED_ORDERS) { gameState = 'GAME_OVER'; }
If the player has failed 3 orders and the customer is angry, immediately end the game. Otherwise, let the angry customer leave normally
clearFoodItems();
Empties all microwave, chopping board, and fridge slots to clean up for the next customer
if (customerState === 'PAID_DISPLAY' && millis() - customerPaidTimer > PAYMENT_DISPLAY_DURATION) {
Checks if customer is showing '💰 PAID!' AND 1500ms have passed since it started showing. If so, it's time to leave
const leaveProgress = (millis() - customerLeavingTimer) / CUSTOMER_LEAVING_DURATION;
Calculates a progress value from 0 to 1+ by dividing elapsed time by total leaving duration (1000ms). At 0, animation hasn't started; at 1, animation is complete
customerAreaX = lerp(0, width * 1.2, leaveProgress);
Smoothly interpolates customerAreaX from position 0 (on-screen) to width * 1.2 (off-screen right) based on leaveProgress. lerp() blends two values
if (leaveProgress >= 1) { customerState = 'NEUTRAL'; customerAreaX = width * -0.2; generateNewOrder(); }
When animation completes, reset customer to NEUTRAL, position off-screen left for next arrival, and generate a fresh order for the new customer
if (checkOrderComplete() && (customerState === 'PLAYING_ORDER' || customerState === 'NEUTRAL')) {
Checks if all order items have been served (checkOrderComplete returns true) AND the customer is in a state where they can receive the order (PLAYING_ORDER or NEUTRAL, not already happy/angry/leaving)
score += 100 + (orderTimeLimit - elapsed) / 100;
Awards 100 base points plus a bonus. Since orderTimeLimit is Infinity, the bonus calculation still applies mathematically (though it approaches a ceiling)
customerState = 'HAPPY'; customerStateTimer = millis();
Transitions customer to HAPPY state and records the exact moment this happened (millis()) so the 3-second HAPPY duration timer can start counting
const arrivalProgress = (millis() - orderStartTime) / CUSTOMER_ARRIVAL_DURATION;
Calculates progress from 0 to 1+ over 1000ms. Uses the same timer as the order start, so arrival animation begins immediately
customerAreaX = lerp(width * -0.2, 0, arrivalProgress);
Slides customer from off-screen left (width * -0.2) to on-screen center (0) as arrivalProgress goes from 0 to 1

mousePressed()

mousePressed() is p5.js's built-in function that runs whenever the player clicks or taps. This function is the game's input handler—it routes clicks to different parts of the game based on where the click happened. The key technique is hit testing: checking if (mouseX, mouseY) falls within specific rectangular regions. Notice how the code uses early returns to prevent multiple handlers from running on the same click—once the menu/restart click is handled, it returns, so no other code runs. This prevents bugs where a click could simultaneously affect both the menu AND the game.

function mousePressed() {
  // Play a tap sound for any interaction
  tapSound.amp(0.2, 0.1);
  tapSound.amp(0, 0.2);

  if (gameState === 'MENU' || gameState === 'GAME_OVER') {
    gameState = 'PLAYING';
    score = 0;
    failedOrders = 0;
    customerState = 'NEUTRAL';
    customerAreaX = width * -0.2;
    clearFoodItems();
    generateNewOrder();
    return;
  }

  if (gameState === 'PLAYING') {
    // Check if the customer area (speech bubble) was clicked to start the order
    if (customerState === 'NEUTRAL' &&
        mouseX > customerAreaX && mouseX < customerAreaX + customerAreaW &&
        mouseY > customerAreaY && mouseY < customerAreaY + customerAreaH) {
      startCurrentOrder();
      return;
    }

    // Check if the click/tap is within the menu bar (food items area)
    if (mouseY > menuY + menuH / 2 && mouseY < menuY + menuH) {
      const foodWidth = menuW / menuFoods.length;
      let selectedFood = null;

      for (let i = 0; i < menuFoods.length; i++) {
        if (mouseX > menuX + i * foodWidth && mouseX < menuX + (i + 1) * foodWidth) {
          selectedFood = menuFoods[i];
          break;
        }
      }

      if (selectedFood) {
        if (selectedFood.type === 'hot') {
          const emptySlotIndex = microwaveItems.indexOf(null);
          if (emptySlotIndex !== -1) {
            const slotWidth = microwaveW / NUM_MICROWAVES;
            const slotX = microwaveX + emptySlotIndex * slotWidth;
            microwaveItems[emptySlotIndex] = {
              emoji: selectedFood.emoji,
              x: slotX + slotWidth / 2,
              y: microwaveY + microwaveH / 2,
              slotIndex: emptySlotIndex
            };
            console.log(`Placed ${microwaveItems[emptySlotIndex].emoji} in microwave slot ${emptySlotIndex} (instantly ready)`);
          } else {
            console.log("All microwaves are currently busy! Please chop an item first.");
          }
        } else { // selectedFood.type === 'cold'
          const emptySlotIndex = fridgeItems.indexOf(null);
          if (emptySlotIndex !== -1) {
            const slotWidth = fridgeW / NUM_FRIDGE_SLOTS;
            const slotX = fridgeX + emptySlotIndex * slotWidth;
            fridgeItems[emptySlotIndex] = {
              emoji: selectedFood.emoji,
              x: slotX + slotWidth / 2,
              y: fridgeY + fridgeH / 2,
              slotIndex: emptySlotIndex
            };
            console.log(`Placed cold ${fridgeItems[emptySlotIndex].emoji} in fridge slot ${emptySlotIndex}`);
          } else {
            console.log("Fridge is currently full! Please serve a cold item first.");
          }
        }
      }
    }
    // Check if the click/tap is within the microwave area
    else if (mouseY > microwaveY && mouseY < microwaveY + microwaveH) {
      const slotWidth = microwaveW / NUM_MICROWAVES;
      const tappedSlotIndex = floor((mouseX - microwaveX) / slotWidth);

      if (tappedSlotIndex >= 0 && tappedSlotIndex < NUM_MICROWAVES && microwaveItems[tappedSlotIndex]) {
        const itemInMicrowave = microwaveItems[tappedSlotIndex];
        const emptyBoardIndex = choppingBoards.indexOf(null);
        if (emptyBoardIndex !== -1) {
          const boardWidth = choppingAreaW / NUM_CHOPPING_BOARDS;
          const boardX = choppingAreaX + emptyBoardIndex * boardWidth;
          choppingBoards[emptyBoardIndex] = {
            emoji: itemInMicrowave.emoji,
            x: boardX + boardWidth / 2,
            y: choppingAreaY + choppingAreaH / 2,
            slotIndex: emptyBoardIndex
          };
          microwaveItems[tappedSlotIndex] = null;
          console.log(`Transferred ${itemInMicrowave.emoji} to chopping board slot ${emptyBoardIndex} (instantly ready)`);
        } else {
          console.log("All chopping boards are currently busy! Please serve an item first.");
        }
      }
    }
    // Check if the click/tap is within the chopping area
    else if (mouseY > choppingAreaY && mouseY < choppingAreaY + choppingAreaH) {
      const boardWidth = choppingAreaW / NUM_CHOPPING_BOARDS;
      const tappedBoardIndex = floor((mouseX - choppingAreaX) / boardWidth);

      if (tappedBoardIndex >= 0 && tappedBoardIndex < NUM_CHOPPING_BOARDS && choppingBoards[tappedBoardIndex]) {
        const itemOnBoard = choppingBoards[tappedBoardIndex];
        let foundInOrder = false;
        for (let j = 0; j < currentOrder.length; j++) {
            const orderItem = currentOrder[j];
            if (orderItem.emoji === itemOnBoard.emoji && orderItem.servedCount < orderItem.quantity) {
                orderItem.servedCount++;
                choppingBoards[tappedBoardIndex] = null;
                foundInOrder = true;
                console.log(`Served ${itemOnBoard.emoji}. Order progress: ${orderItem.servedCount}/${orderItem.quantity}`);
                break;
            }
        }

        if (!foundInOrder) {
            console.log(`Served ${itemOnBoard.emoji} but it's not needed for the current order.`);
            choppingBoards[tappedBoardIndex] = null;
        }
      }
    }
    // Check if the click/tap is within the fridge area
    else if (mouseY > fridgeY && mouseY < fridgeY + fridgeH) {
      const fridgeSlotWidth = fridgeW / NUM_FRIDGE_SLOTS;
      const tappedSlotIndex = floor((mouseX - fridgeX) / fridgeSlotWidth);

      if (tappedSlotIndex >= 0 && tappedSlotIndex < NUM_FRIDGE_SLOTS && fridgeItems[tappedSlotIndex]) {
        const itemInFridge = fridgeItems[tappedSlotIndex];

        let foundInOrder = false;
        for (let j = 0; j < currentOrder.length; j++) {
            const orderItem = currentOrder[j];
            if (orderItem.emoji === itemInFridge.emoji && orderItem.servedCount < orderItem.quantity) {
                orderItem.servedCount++;
                fridgeItems[tappedSlotIndex] = null;
                foundInOrder = true;
                console.log(`Served cold ${itemInFridge.emoji}. Order progress: ${orderItem.servedCount}/${orderItem.quantity}`);
                break;
            }
        }

        if (!foundInOrder) {
            console.log(`Served cold ${itemInFridge.emoji} but it's not needed for the current order.`);
            fridgeItems[tappedSlotIndex] = null;
        }
      }
    }
  }
}
Line-by-line explanation (18 lines)

🔧 Subcomponents:

hit-test Customer Click Detection if (customerState === 'NEUTRAL' && mouseX > customerAreaX && mouseX < customerAreaX + customerAreaW && mouseY > customerAreaY && mouseY < customerAreaY + customerAreaH) { startCurrentOrder(); return; }

Checks if click is within the customer area rectangle and customer is neutral, then starts order processing

logic Microwave Flow (Menu → Microwave) const emptySlotIndex = microwaveItems.indexOf(null); if (emptySlotIndex !== -1) { const slotWidth = microwaveW / NUM_MICROWAVES; const slotX = microwaveX + emptySlotIndex * slotWidth; microwaveItems[emptySlotIndex] = { emoji: selectedFood.emoji, x: slotX + slotWidth / 2, y: microwaveY + microwaveH / 2, slotIndex: emptySlotIndex }; } else { console.log("All microwaves are currently busy!"); }

Finds first empty microwave slot; if one exists, creates item object and stores it; if all full, logs message

logic Fridge Flow (Menu → Fridge) const emptySlotIndex = fridgeItems.indexOf(null); if (emptySlotIndex !== -1) { const slotWidth = fridgeW / NUM_FRIDGE_SLOTS; const slotX = fridgeX + emptySlotIndex * slotWidth; fridgeItems[emptySlotIndex] = { emoji: selectedFood.emoji, x: slotX + slotWidth / 2, y: fridgeY + fridgeH / 2, slotIndex: emptySlotIndex }; } else { console.log("Fridge is currently full!"); }

Finds first empty fridge slot; if one exists, creates item object and stores it; if all full, logs message

hit-test-and-transfer Microwave Item Transfer else if (mouseY > microwaveY && mouseY < microwaveY + microwaveH) { const slotWidth = microwaveW / NUM_MICROWAVES; const tappedSlotIndex = floor((mouseX - microwaveX) / slotWidth); if (tappedSlotIndex >= 0 && tappedSlotIndex < NUM_MICROWAVES && microwaveItems[tappedSlotIndex]) { const itemInMicrowave = microwaveItems[tappedSlotIndex]; const emptyBoardIndex = choppingBoards.indexOf(null); if (emptyBoardIndex !== -1) { ... transfer to chopping board ... } else { console.log("All chopping boards are currently busy!"); } } }

Detects tap in microwave area, calculates which slot, moves item to first available chopping board

hit-test-and-serve Chopping Board Serve else if (mouseY > choppingAreaY && mouseY < choppingAreaY + choppingAreaH) { const boardWidth = choppingAreaW / NUM_CHOPPING_BOARDS; const tappedBoardIndex = floor((mouseX - choppingAreaX) / boardWidth); if (tappedBoardIndex >= 0 && tappedBoardIndex < NUM_CHOPPING_BOARDS && choppingBoards[tappedBoardIndex]) { const itemOnBoard = choppingBoards[tappedBoardIndex]; let foundInOrder = false; for (let j = 0; j < currentOrder.length; j++) { const orderItem = currentOrder[j]; if (orderItem.emoji === itemOnBoard.emoji && orderItem.servedCount < orderItem.quantity) { orderItem.servedCount++; choppingBoards[tappedBoardIndex] = null; foundInOrder = true; break; } } if (!foundInOrder) { choppingBoards[tappedBoardIndex] = null; } } }

Detects tap in chopping area, checks if item matches any item in current order, increments servedCount if match found

hit-test-and-serve Fridge Cold Item Serve else if (mouseY > fridgeY && mouseY < fridgeY + fridgeH) { const fridgeSlotWidth = fridgeW / NUM_FRIDGE_SLOTS; const tappedSlotIndex = floor((mouseX - fridgeX) / fridgeSlotWidth); if (tappedSlotIndex >= 0 && tappedSlotIndex < NUM_FRIDGE_SLOTS && fridgeItems[tappedSlotIndex]) { const itemInFridge = fridgeItems[tappedSlotIndex]; let foundInOrder = false; for (let j = 0; j < currentOrder.length; j++) { const orderItem = currentOrder[j]; if (orderItem.emoji === itemInFridge.emoji && orderItem.servedCount < orderItem.quantity) { orderItem.servedCount++; fridgeItems[tappedSlotIndex] = null; foundInOrder = true; break; } } if (!foundInOrder) { fridgeItems[tappedSlotIndex] = null; } } }

Detects tap in fridge area, checks if cold item matches order, increments servedCount if match found

tapSound.amp(0.2, 0.1); tapSound.amp(0, 0.2);
Ramps the tap sound up to amplitude 0.2 over 0.1 seconds, then down to 0 over 0.2 seconds—provides audio feedback for every click
if (gameState === 'MENU' || gameState === 'GAME_OVER') {
Checks if the player is on the start menu or game over screen. If so, treat this click as a request to start/restart the game
gameState = 'PLAYING'; score = 0; failedOrders = 0; customerState = 'NEUTRAL'; customerAreaX = width * -0.2; clearFoodItems(); generateNewOrder();
Resets all game variables to initial state and generates the first order, effectively starting a fresh game
return;
Exits the mousePressed() function early so that clicks on MENU or GAME_OVER don't also try to interact with game elements
if (customerState === 'NEUTRAL' && mouseX > customerAreaX && mouseX < customerAreaX + customerAreaW && mouseY > customerAreaY && mouseY < customerAreaY + customerAreaH) {
Four-part condition: customer must be NEUTRAL (not happy/angry/paying/leaving) AND click must be inside the customerAreaX/Y/W/H rectangle. All must be true to trigger
startCurrentOrder();
Calls the helper function that transitions customerState from NEUTRAL to PLAYING_ORDER, allowing the player to start serving items
if (mouseY > menuY + menuH / 2 && mouseY < menuY + menuH) {
Checks if click is in the bottom half of the menu bar (where food emojis are). Top half has score/timer text
const foodWidth = menuW / menuFoods.length;
Calculates the width of each food item's clickable zone by dividing total menu width by 21 foods
for (let i = 0; i < menuFoods.length; i++) { if (mouseX > menuX + i * foodWidth && mouseX < menuX + (i + 1) * foodWidth) { selectedFood = menuFoods[i]; break; } }
Loops through all 21 foods, checking if the click falls within each one's x-range. When found, stores that food object and breaks out of loop
if (selectedFood.type === 'hot') { const emptySlotIndex = microwaveItems.indexOf(null);
Checks if the selected food is type 'hot'. If so, uses indexOf(null) to find the first empty microwave slot (indexOf returns -1 if not found)
if (emptySlotIndex !== -1) { ... create item object and add to array ... } else { console.log("All microwaves are currently busy!"); }
If an empty slot was found (emptySlotIndex is NOT -1), create and store the item. Otherwise, log a message that all microwaves are full
const tappedSlotIndex = floor((mouseX - microwaveX) / slotWidth);
Calculates which microwave slot was clicked: subtract the microwave's left edge from the click position, divide by slot width, and floor to get integer slot number
if (tappedSlotIndex >= 0 && tappedSlotIndex < NUM_MICROWAVES && microwaveItems[tappedSlotIndex]) {
Three-part safety check: slot must be in range (0-4) AND that slot must actually contain an item (not null)
const emptyBoardIndex = choppingBoards.indexOf(null);
Finds the first empty chopping board slot using indexOf(null)
choppingBoards[emptyBoardIndex] = { emoji: itemInMicrowave.emoji, x: ..., y: ..., slotIndex: emptyBoardIndex };
Creates a new item object with the same emoji and calculates its position in the destination slot, then stores it in the array
microwaveItems[tappedSlotIndex] = null;
Clears the microwave slot that just transferred its item, making that space available for a new item
for (let j = 0; j < currentOrder.length; j++) { const orderItem = currentOrder[j]; if (orderItem.emoji === itemOnBoard.emoji && orderItem.servedCount < orderItem.quantity) { orderItem.servedCount++; break; } }
Loops through all items in the current order. If the emoji matches AND more are needed (servedCount < quantity), increment servedCount and stop. This marks the item as served
if (!foundInOrder) { choppingBoards[tappedBoardIndex] = null; }
If the served item wasn't needed for the order, still remove it from the chopping board to prevent waste buildup

calculateLayout()

calculateLayout() is called once during setup() and again whenever the window is resized. It computes all the X/Y positions and widths/heights for every game zone, making the layout fully responsive. The key insight is that all positions are calculated relative to width and height (not hardcoded numbers), so the game automatically adjusts to any screen size. This is crucial for modern web games that need to work on phones, tablets, and desktops.

function calculateLayout() {
  // Define heights for each section
  menuHeight = height * 0.15;
  customerAreaHeight = height * 0.15;
  microwaveHeight = height * 0.20;
  choppingAreaHeight = height * 0.20;
  fridgeHeight = height - menuHeight - customerAreaHeight - microwaveHeight - choppingAreaHeight;

  // Define layout coordinates and dimensions for each area
  menuX = 0;
  menuY = 0;
  menuW = width;
  menuH = menuHeight;

  customerAreaX = width * -0.2;
  customerAreaY = menuHeight;
  customerAreaW = width;
  customerAreaH = customerAreaHeight;

  microwaveX = width * 0.1;
  microwaveY = customerAreaY + customerAreaH + height * 0.02;
  microwaveW = width * 0.8;
  microwaveH = microwaveHeight - height * 0.04;

  choppingAreaX = 0;
  choppingAreaY = menuHeight + customerAreaHeight + microwaveHeight;
  choppingAreaW = width;
  choppingAreaH = choppingAreaHeight;

  fridgeX = width * 0.1;
  fridgeY = menuHeight + customerAreaHeight + microwaveHeight + choppingAreaHeight + height * 0.02;
  fridgeW = width * 0.8;
  fridgeH = fridgeHeight - height * 0.04;

  // Define emoji sizes for each area
  menuEmojiSize = min(width, height) * 0.1;
  microwaveEmojiSize = min(width, height) * 0.15;
  choppedEmojiSize = min(width, height) * 0.08;
  customerSize = min(width, height) * 0.1;
  bubblePadding = menuEmojiSize * 0.2;
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

calculation Vertical Section Heights menuHeight = height * 0.15; customerAreaHeight = height * 0.15; microwaveHeight = height * 0.20; choppingAreaHeight = height * 0.20; fridgeHeight = height - menuHeight - customerAreaHeight - microwaveHeight - choppingAreaHeight;

Divides canvas height into 5 sections: 15% menu, 15% customer, 20% microwave, 20% chopping, remaining % fridge

Customer Area Layout customerAreaX = width * -0.2; customerAreaY = menuHeight; customerAreaW = width; customerAreaH = customerAreaHeight;

Positions customer area below menu, starts off-screen left for arrival animation

positioning Microwave Area Layout microwaveX = width * 0.1; microwaveY = customerAreaY + customerAreaH + height * 0.02; microwaveW = width * 0.8; microwaveH = microwaveHeight - height * 0.04;

Positions microwave with 10% left margin, small gap below customer area, 80% width, with padding deductions

positioning Chopping Area Layout choppingAreaX = 0; choppingAreaY = menuHeight + customerAreaHeight + microwaveHeight; choppingAreaW = width; choppingAreaH = choppingAreaHeight;

Positions chopping boards full width below microwave area

positioning Fridge Area Layout fridgeX = width * 0.1; fridgeY = menuHeight + customerAreaHeight + microwaveHeight + choppingAreaHeight + height * 0.02; fridgeW = width * 0.8; fridgeH = fridgeHeight - height * 0.04;

Positions fridge with 10% left margin, small gap below chopping, 80% width

calculation Emoji Size Scaling menuEmojiSize = min(width, height) * 0.1; microwaveEmojiSize = min(width, height) * 0.15; choppedEmojiSize = min(width, height) * 0.08; customerSize = min(width, height) * 0.1; bubblePadding = menuEmojiSize * 0.2;

Scales all emoji and text sizes based on screen dimensions so game looks good on any device

menuHeight = height * 0.15;
Allocates 15% of the canvas height to the menu bar, so on a 800px tall screen, the menu is 120px tall
customerAreaHeight = height * 0.15;
Allocates 15% of canvas height to the customer area, giving them equal space to the menu
microwaveHeight = height * 0.20;
Allocates 20% of canvas height to the microwave section
choppingAreaHeight = height * 0.20;
Allocates 20% of canvas height to the chopping boards section
fridgeHeight = height - menuHeight - customerAreaHeight - microwaveHeight - choppingAreaHeight;
Fridge gets whatever height is left after the other four sections—this ensures the layout always fills the entire screen without gaps
menuX = 0; menuY = 0; menuW = width; menuH = menuHeight;
Menu starts at top-left (0, 0), stretches full width, and has the menuHeight we calculated
customerAreaX = width * -0.2;
Positions customer off-screen to the left (negative x). When the game starts, updateGame() will animate this to 0 (on-screen)
customerAreaY = menuHeight;
Customer area starts immediately below the menu bar
customerAreaW = width; customerAreaH = customerAreaHeight;
Customer area spans full width and uses the customerAreaHeight we allocated
microwaveX = width * 0.1;
Microwave area has 10% margin on the left, so it doesn't touch the edge
microwaveY = customerAreaY + customerAreaH + height * 0.02;
Microwave starts below the customer area, plus a small 2% gap for visual breathing room
microwaveW = width * 0.8;
Microwave uses 80% of screen width (20% margin: 10% left + 10% right)
microwaveH = microwaveHeight - height * 0.04;
Microwave height is slightly less than the allocated microwaveHeight to account for label text
choppingAreaY = menuHeight + customerAreaHeight + microwaveHeight;
Chopping area Y position is the sum of all sections above it—this ensures perfect stacking
fridgeY = menuHeight + customerAreaHeight + microwaveHeight + choppingAreaHeight + height * 0.02;
Fridge starts after chopping area, plus a small gap
menuEmojiSize = min(width, height) * 0.1;
Emoji size is 10% of the smaller dimension (width or height), ensuring they scale proportionally for any aspect ratio

startCurrentOrder()

startCurrentOrder() is a simple but important function. It's called when the player taps the customer, and it transitions the game into a state where orders can be served. Notice how it only changes one variable (customerState) rather than doing a complex operation—this simplicity makes it easy to understand and modify. The console.log() is a professional habit: always log state changes for debugging.

function startCurrentOrder() {
  // Update customer state to reflect order is being prepared
  customerState = 'PLAYING_ORDER';
  console.log("Order preparation started by tapping customer!");
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

assignment State Change customerState = 'PLAYING_ORDER';

Transitions customer from NEUTRAL to PLAYING_ORDER, allowing items to be served

customerState = 'PLAYING_ORDER';
Changes the customer's state. In PLAYING_ORDER, the order bubble remains visible and updateGame() will accept served items
console.log("Order preparation started by tapping customer!");
Prints a message to the browser console for debugging—helps you confirm that the tap was registered

generateNewOrder()

generateNewOrder() uses randomness (random(), floor()) and array operations (splice) to create varied, unpredictable customer orders. The key technique is using splice() to remove items as they're picked—this ensures no duplicates in a single order. Notice how it records orderStartTime (millis()) the moment an order is created; this moment becomes the anchor point for all animations (customer arrival, timing checks, etc.). By calling generateNewOrder() at the start of a new game and again when a customer leaves, the game always has a fresh order ready.

function generateNewOrder() {
  currentOrder = [];
  const numItems = floor(random(1, 3)); // Order 1 or 2 different items
  let availableFoods = [...menuFoods]; // Copy menu foods to pick without duplicates

  for (let i = 0; i < numItems; i++) {
    if (availableFoods.length === 0) break;
    const randomIndex = floor(random(availableFoods.length));
    const food = availableFoods[randomIndex];
    const quantity = floor(random(1, 4)); // 1 to 3 of each item
    // Use servedCount for all items now
    currentOrder.push({ emoji: food.emoji, quantity: quantity, servedCount: 0 });
    availableFoods.splice(randomIndex, 1); // Remove from available to prevent duplicates in one order
  }

  orderStartTime = millis(); // Reset timer for the new order (though it's infinite now)
  orderTimer = orderTimeLimit; // Set to Infinity
  console.log("New Order:", currentOrder.map(item => `${item.emoji} x${item.quantity}`).join(', '));
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

initialization Order Array Reset currentOrder = [];

Clears any previous order and starts with an empty array for the new order

random-generation Random Item Count const numItems = floor(random(1, 3));

Randomly decides if the order will have 1 or 2 different items (never 0, never 3+)

array-operation Food List Copy let availableFoods = [...menuFoods];

Creates a shallow copy of menuFoods array using spread operator (...) so we can remove items without affecting the original menu

for-loop Order Item Loop for (let i = 0; i < numItems; i++) { if (availableFoods.length === 0) break; const randomIndex = floor(random(availableFoods.length)); const food = availableFoods[randomIndex]; const quantity = floor(random(1, 4)); currentOrder.push({ emoji: food.emoji, quantity: quantity, servedCount: 0 }); availableFoods.splice(randomIndex, 1); }

Loops numItems times, each iteration picks a random food from availableFoods, assigns a random quantity (1-3), adds it to order, and removes it from available list to prevent duplicates

timing Order Timer Start orderStartTime = millis(); orderTimer = orderTimeLimit;

Records the exact moment this order started (for animation timing) and sets the order timer to Infinity

debugging Order Logging console.log("New Order:", currentOrder.map(item => `${item.emoji} x${item.quantity}`).join(', '));

Prints the new order to the console in human-readable format (e.g., '🥚 x2, 🥓 x1')

currentOrder = [];
Empties the currentOrder array so we can build a fresh order from scratch
const numItems = floor(random(1, 3));
random(1, 3) generates a decimal between 1.0 (inclusive) and 3.0 (exclusive), floor() converts it to integer 1 or 2
let availableFoods = [...menuFoods];
Spread operator (...) copies all elements from menuFoods into a new array. This prevents the loop from accidentally modifying the original menu
for (let i = 0; i < numItems; i++) {
Loops 1 or 2 times (based on numItems). Each iteration adds one item to the order
if (availableFoods.length === 0) break;
Safety check: if we've run out of foods to pick from (unlikely but possible), exit the loop early
const randomIndex = floor(random(availableFoods.length));
Generates a random integer from 0 to length-1, which is a valid index for the availableFoods array
const food = availableFoods[randomIndex];
Retrieves the food object at that random index
const quantity = floor(random(1, 4));
Randomly decides how many of this food item to order: 1, 2, or 3
currentOrder.push({ emoji: food.emoji, quantity: quantity, servedCount: 0 });
Creates a new order item object with emoji, quantity, and servedCount starting at 0, then adds it to currentOrder array
availableFoods.splice(randomIndex, 1);
Removes the just-picked food from availableFoods so the next iteration can't pick the same food again in this order
orderStartTime = millis();
Records the current time (milliseconds since sketch started) so we can calculate elapsed time and drive animations
console.log("New Order:", currentOrder.map(item => `${item.emoji} x${item.quantity}`).join(', '));
Transforms currentOrder into a readable string (e.g., '🥚 x2, 🥓 x1') and prints it to the browser console for debugging

checkOrderComplete()

checkOrderComplete() is a simple but critical function. It returns a boolean (true/false) that answers the question: 'Is the order done?' updateGame() calls this every frame; when it returns true, the customer becomes happy, score increases, and the next customer arrives. This is an example of a helper function that encapsulates a check into a descriptive name, making the main game logic (updateGame) cleaner to read.

function checkOrderComplete() {
  for (let i = 0; i < currentOrder.length; i++) {
    const orderItem = currentOrder[i];
    // Check servedCount for all items
    if (orderItem.servedCount < orderItem.quantity) {
      return false; // Not all items in the order have been served
    }
  }
  return true; // All items in the order have been served
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Item Quantity Check Loop for (let i = 0; i < currentOrder.length; i++) { const orderItem = currentOrder[i]; if (orderItem.servedCount < orderItem.quantity) { return false; } }

Loops through each item in the order; if any item's servedCount is less than its required quantity, immediately returns false (order incomplete)

for (let i = 0; i < currentOrder.length; i++) {
Loops through every item in currentOrder. For an order with 2 items, this runs twice
const orderItem = currentOrder[i];
Gets the current order item object (which has emoji, quantity, and servedCount properties)
if (orderItem.servedCount < orderItem.quantity) {
Checks if we've served fewer of this item than the customer requested. If so, the order is not yet complete
return false;
Immediately exits the function and returns false, signaling that the order is incomplete. No need to check other items
return true;
If the loop completes without hitting return false, all items have been fully served, so return true

clearFoodItems()

clearFoodItems() is called in two situations: (1) when a customer's angry reaction ends and they leave without their order being served, and (2) when a customer's order is completed and paid. In both cases, it's important to clean the kitchen—remove all in-progress items so the next customer starts with a fresh slate. This prevents confusion and ensures each order is independent.

function clearFoodItems() {
  microwaveItems = Array(NUM_MICROWAVES).fill(null);
  choppingBoards = Array(NUM_CHOPPING_BOARDS).fill(null);
  fridgeItems = Array(NUM_FRIDGE_SLOTS).fill(null);
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

array-reset Microwave Array Reset microwaveItems = Array(NUM_MICROWAVES).fill(null);

Creates a new array of length NUM_MICROWAVES (5) filled entirely with null, replacing the old microwaveItems

array-reset Chopping Board Array Reset choppingBoards = Array(NUM_CHOPPING_BOARDS).fill(null);

Creates a new array of length NUM_CHOPPING_BOARDS (2) filled entirely with null, replacing the old choppingBoards

array-reset Fridge Array Reset fridgeItems = Array(NUM_FRIDGE_SLOTS).fill(null);

Creates a new array of length NUM_FRIDGE_SLOTS (4) filled entirely with null, replacing the old fridgeItems

microwaveItems = Array(NUM_MICROWAVES).fill(null);
Array(5) creates an array with 5 empty slots. fill(null) sets every slot to null (empty). This line completely empties all microwaves
choppingBoards = Array(NUM_CHOPPING_BOARDS).fill(null);
Same pattern for chopping boards: creates a fresh array of 2 empty slots
fridgeItems = Array(NUM_FRIDGE_SLOTS).fill(null);
Same pattern for fridge: creates a fresh array of 4 empty slots

drawMenuScreen()

drawMenuScreen() is called from draw() when gameState === 'MENU'. It draws a darkened overlay on top of the game (creating a modal effect) with the title and instructions. This is a common UI pattern: whenever the game isn't actively playing, show a menu overlay that explains what to do. The alpha transparency (150) allows the game underneath to faintly show through, hinting at what's coming.

function drawMenuScreen() {
  fill(0, 150);
  rect(0, 0, width, height);
  fill(255);
  textSize(min(width, height) * 0.1);
  textAlign(CENTER, CENTER);
  text('Restaurant Cooking Game', width / 2, height / 3);
  textSize(min(width, height) * 0.05);
  text('Tap anywhere to start!', width / 2, height / 2);
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

drawing Semi-Transparent Overlay fill(0, 150); rect(0, 0, width, height);

Draws a black rectangle covering the entire screen with 150 alpha (semi-transparent), dimming the game behind it

text-render Menu Title fill(255); textSize(min(width, height) * 0.1); textAlign(CENTER, CENTER); text('Restaurant Cooking Game', width / 2, height / 3);

Displays large white title text centered at 1/3 down the screen

text-render Start Instruction textSize(min(width, height) * 0.05); text('Tap anywhere to start!', width / 2, height / 2);

Displays smaller white instruction text centered at midscreen

fill(0, 150);
Sets fill color to black (0, 0, 0) with alpha 150. Alpha ranges from 0 (invisible) to 255 (opaque), so 150 is half-transparent
rect(0, 0, width, height);
Draws a rectangle covering the entire canvas, creating a darkened overlay
fill(255);
Changes fill to white (255, 255, 255) for the text
textSize(min(width, height) * 0.1);
Sets text size to 10% of the smaller screen dimension (width or height), ensuring the title is large but readable on all devices
text('Restaurant Cooking Game', width / 2, height / 3);
Draws the title centered horizontally (width / 2) and 1/3 down vertically (height / 3)
textSize(min(width, height) * 0.05);
Makes instruction text smaller (5% of screen dimension) than the title
text('Tap anywhere to start!', width / 2, height / 2);
Draws instruction text centered at the middle of the screen

drawGameOverScreen()

drawGameOverScreen() displays when gameState === 'GAME_OVER'. It shows the final score and asks the player to restart. Notice the color difference: 'GAME OVER!' is red (danger) while the menu title was white (neutral). This use of color signals to the player that something important has changed. Like drawMenuScreen(), it uses an overlay to prevent interaction with the game underneath.

function drawGameOverScreen() {
  fill(0, 150);
  rect(0, 0, width, height);
  fill(255, 0, 0);
  textSize(min(width, height) * 0.12);
  textAlign(CENTER, CENTER);
  text('GAME OVER!', width / 2, height / 3);
  fill(255);
  textSize(min(width, height) * 0.06);
  text(`Final Score: ${score}`, width / 2, height / 2);
  textSize(min(width, height) * 0.04);
  text('Tap anywhere to restart!', width / 2, height * 2 / 3);
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

drawing Semi-Transparent Overlay fill(0, 150); rect(0, 0, width, height);

Draws a black semi-transparent rectangle covering the entire screen

text-render Game Over Title fill(255, 0, 0); textSize(min(width, height) * 0.12); textAlign(CENTER, CENTER); text('GAME OVER!', width / 2, height / 3);

Displays large red 'GAME OVER!' text at 1/3 down the screen

text-render Final Score Display fill(255); textSize(min(width, height) * 0.06); text(`Final Score: ${score}`, width / 2, height / 2);

Displays the final score in white text at midscreen, including the actual score value

text-render Restart Instruction textSize(min(width, height) * 0.04); text('Tap anywhere to restart!', width / 2, height * 2 / 3);

Displays smaller white instruction text at 2/3 down the screen

fill(0, 150);
Black with alpha 150 for the dimming overlay
rect(0, 0, width, height);
Covers the entire screen with the dark overlay
fill(255, 0, 0);
Changes fill color to pure red (255, 0, 0), signaling that the game has ended
textSize(min(width, height) * 0.12);
Makes the 'GAME OVER!' text even larger than the menu title (12% vs 10%)
text('GAME OVER!', width / 2, height / 3);
Draws red 'GAME OVER!' text centered and 1/3 down
fill(255);
Changes fill back to white for the score and instruction text
textSize(min(width, height) * 0.06);
Score text is slightly smaller than the game over title
text(`Final Score: ${score}`, width / 2, height / 2);
Inserts the current score value into the text string using template literals
textSize(min(width, height) * 0.04);
Makes instruction text smallest (4% of screen)
text('Tap anywhere to restart!', width / 2, height * 2 / 3);
Draws instruction at 2/3 down the screen (lower than menu screen)

windowResized()

windowResized() is a special p5.js function that's called automatically whenever the browser window is resized. It's critical for responsive games: without it, the layout would stay fixed to the original window size and break if the player resizes their browser or rotates their phone. Notice how it calls calculateLayout() first (to update all the position variables), then repositions existing items to match the new layout. This ensures the game always looks correct, no matter the screen size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  calculateLayout();

  // Reposition microwave items if they exist
  const slotWidth = microwaveW / NUM_MICROWAVES;
  for (let i = 0; i < NUM_MICROWAVES; i++) {
    if (microwaveItems[i]) {
      const slotX = microwaveX + i * slotWidth;
      microwaveItems[i].x = slotX + slotWidth / 2;
      microwaveItems[i].y = microwaveY + microwaveH / 2;
    }
  }

  // Reposition chopping board items if they exist
  const boardWidth = choppingAreaW / NUM_CHOPPING_BOARDS;
  for (let i = 0; i < NUM_CHOPPING_BOARDS; i++) {
    if (choppingBoards[i]) {
      const boardX = choppingAreaX + i * boardWidth;
      choppingBoards[i].x = boardX + boardWidth / 2;
      choppingBoards[i].y = choppingAreaY + choppingAreaH / 2;
    }
  }

  // Reposition fridge items if they exist
  const fridgeSlotWidth = fridgeW / NUM_FRIDGE_SLOTS;
  for (let i = 0; i < NUM_FRIDGE_SLOTS; i++) {
    if (fridgeItems[i]) {
      const fridgeSlotX = fridgeX + i * fridgeSlotWidth;
      fridgeItems[i].x = fridgeSlotX + fridgeSlotWidth / 2;
      fridgeItems[i].y = fridgeY + fridgeH / 2;
    }
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

initialization Canvas Resize resizeCanvas(windowWidth, windowHeight);

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

calculation Layout Recalculation calculateLayout();

Recalculates all layout variables (menu bar height, emoji sizes, area positions) based on new canvas size

for-loop Microwave Item Repositioning for (let i = 0; i < NUM_MICROWAVES; i++) { if (microwaveItems[i]) { const slotX = microwaveX + i * slotWidth; microwaveItems[i].x = slotX + slotWidth / 2; microwaveItems[i].y = microwaveY + microwaveH / 2; } }

Updates the x, y positions of any items currently in the microwave to match the new layout

for-loop Chopping Board Item Repositioning for (let i = 0; i < NUM_CHOPPING_BOARDS; i++) { if (choppingBoards[i]) { const boardX = choppingAreaX + i * boardWidth; choppingBoards[i].x = boardX + boardWidth / 2; choppingBoards[i].y = choppingAreaY + choppingAreaH / 2; } }

Updates the x, y positions of any items on chopping boards to match the new layout

for-loop Fridge Item Repositioning for (let i = 0; i < NUM_FRIDGE_SLOTS; i++) { if (fridgeItems[i]) { const fridgeSlotX = fridgeX + i * fridgeSlotWidth; fridgeItems[i].x = fridgeSlotX + fridgeSlotWidth / 2; fridgeItems[i].y = fridgeY + fridgeH / 2; } }

Updates the x, y positions of any items in the fridge to match the new layout

resizeCanvas(windowWidth, windowHeight);
p5.js built-in function that changes the canvas size. windowWidth and windowHeight are p5.js variables that update when the browser window is resized
calculateLayout();
Calls the layout function to recalculate all layout variables (menuHeight, emoji sizes, etc.) for the new canvas dimensions
const slotWidth = microwaveW / NUM_MICROWAVES;
Recalculates slot width based on the new microwaveW (which was updated by calculateLayout)
if (microwaveItems[i]) {
Only reposition items that actually exist (not null)
const slotX = microwaveX + i * slotWidth;
Recalculates the slot's x position using the new microwaveX and slotWidth
microwaveItems[i].x = slotX + slotWidth / 2; microwaveItems[i].y = microwaveY + microwaveH / 2;
Updates the item's stored x and y position to the center of its slot, using the new layout values

📦 Key Variables

gameState string

Tracks the current game mode: 'MENU' (start screen), 'PLAYING' (active gameplay), or 'GAME_OVER' (end screen). Controls which logic and drawing functions run each frame.

let gameState = 'MENU';
score number

Tracks the player's total points. Incremented by 100+ points each time an order is completed. Displayed in the menu bar and on the game over screen.

let score = 0;
failedOrders number

Counts how many orders the player has failed. Incremented when a customer leaves angry. Game ends when this reaches MAX_FAILED_ORDERS (3).

let failedOrders = 0;
currentOrder array

Array of objects representing the current customer's order. Each object has { emoji, quantity, servedCount }. Used to check if order is complete and display in speech bubble.

let currentOrder = [{ emoji: '🥚', quantity: 2, servedCount: 0 }];
menuFoods array

Array of 21 food item objects, each with { emoji, name, type }. Type is 'hot' (microwave) or 'cold' (fridge). Used to populate the menu bar and generate random orders.

let menuFoods = [{ emoji: '🥚', name: 'Egg', type: 'hot' }, ...];
microwaveItems array

Array of 5 slots that can each hold a food item object or null. Represents the microwave cooking area. Items move from here to chopping boards.

let microwaveItems = Array(5).fill(null);
choppingBoards array

Array of 2 slots that can each hold a food item object or null. Represents the chopping board processing area. Items move from microwave to here, then are served from here.

let choppingBoards = Array(2).fill(null);
fridgeItems array

Array of 4 slots that can each hold a cold food item or null. Represents the refrigerator. Cold items start here instead of microwave.

let fridgeItems = Array(4).fill(null);
customerState string

Tracks the current customer's emotional/action state: 'NEUTRAL' (waiting), 'PLAYING_ORDER' (order being served), 'HAPPY' (order complete), 'ANGRY' (order failed), 'PAID_DISPLAY' (payment showing), 'LEAVING' (sliding out). Controls customer emoji face and animations.

let customerState = 'NEUTRAL';
customerAreaX number

The x position of the customer area. Animated from off-screen left (width * -0.2) to on-screen (0) during arrival, then to off-screen right (width * 1.2) during departure.

let customerAreaX = width * -0.2;
orderStartTime number

Stores millis() (time in milliseconds since sketch started) when the current order began. Used to calculate elapsed time and drive customer arrival animation.

let orderStartTime;
customerStateTimer number

Stores millis() when the customer entered the HAPPY or ANGRY state. Used to count down 3 seconds before transitioning to PAID_DISPLAY or LEAVING.

let customerStateTimer = 0;
customerPaidTimer number

Stores millis() when the PAID_DISPLAY state began. Used to count down 1.5 seconds before transitioning to LEAVING.

let customerPaidTimer = 0;
customerLeavingTimer number

Stores millis() when the LEAVING state began. Used to animate the customer sliding off-screen over 1 second.

let customerLeavingTimer = 0;
menuHeight number

Height of the menu bar in pixels. Calculated as 15% of canvas height in calculateLayout().

let menuHeight;
menuX, menuY, menuW, menuH number

Position (x, y) and dimensions (width, height) of the menu bar rectangle.

let menuX = 0; let menuY = 0; let menuW = width; let menuH = menuHeight;
microwaveX, microwaveY, microwaveW, microwaveH number

Position and dimensions of the microwave area rectangle.

let microwaveX = width * 0.1;
choppingAreaX, choppingAreaY, choppingAreaW, choppingAreaH number

Position and dimensions of the chopping boards area rectangle.

let choppingAreaX = 0;
fridgeX, fridgeY, fridgeW, fridgeH number

Position and dimensions of the fridge area rectangle.

let fridgeX = width * 0.1;
customerAreaX, customerAreaY, customerAreaW, customerAreaH number

Position and dimensions of the customer display area (where the emoji and speech bubble appear).

let customerAreaX = width * -0.2;
menuEmojiSize number

Font size for emojis displayed in the menu bar. Calculated as 10% of the smaller of width or height.

let menuEmojiSize = min(width, height) * 0.1;
microwaveEmojiSize number

Font size for emojis in the microwave and fridge areas. Calculated as 15% of the smaller of width or height.

let microwaveEmojiSize = min(width, height) * 0.15;
choppedEmojiSize number

Font size for emojis on the chopping boards. Calculated as 8% of the smaller of width or height.

let choppedEmojiSize = min(width, height) * 0.08;
customerSize number

Font size for the customer emoji face. Calculated as 10% of the smaller of width or height.

let customerSize = min(width, height) * 0.1;
bubblePadding number

Padding (empty space) inside the customer's speech bubble. Calculated as 20% of menuEmojiSize.

let bubblePadding = menuEmojiSize * 0.2;
tapSound, dingSound, buzzSound, paymentSound p5.Oscillator

p5.sound oscillators that produce audio feedback. tapSound plays for all clicks, dingSound for completed orders, buzzSound for errors, paymentSound for payment.

let tapSound = new p5.Oscillator();

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

FEATURE updateGame()

Orders never fail. With orderTimeLimit = Infinity, customers never get angry from timeout. The only way to lose is through future logic that never gets triggered.

💡 Implement a failure condition: either (1) set a real orderTimeLimit value instead of Infinity, or (2) penalize players for serving wrong items. For example: 'if (servedWrongItem) { failedOrders++; customerState = "ANGRY"; }'

FEATURE choppingBoards and fridgeItems

Cold items can be served directly from the fridge without processing on a chopping board first, creating two different serving paths that are inconsistent.

💡 Consider requiring all items (hot and cold) to pass through a chopping board before serving, for gameplay consistency. Or visually hint that cold items bypass chopping by styling them differently.

PERFORMANCE draw()

Three similar for-loops render microwave, chopping board, and fridge slots. This is repetitive code that could introduce bugs if one loop changes.

💡 Extract slot rendering into a helper function: 'drawSlots(x, y, w, h, items, numSlots, emojiSize)' that works for all three stations. This reduces code duplication and makes maintenance easier.

STYLE choppingAreaY variable name

Variable is named 'choppingAreaAreaY' (with duplicate 'Area') instead of 'choppingAreaY', which is inconsistent with naming conventions for other areas.

💡 Rename 'choppingAreaAreaY' to 'choppingAreaY' throughout the code (in calculateLayout, windowResized, mousePressed, and draw) for consistency.

FEATURE generateNewOrder()

Orders are pure random and can be weighted unfairly (an emoji could dominate multiple orders in a row). There's no difficulty scaling—a new player faces same randomness as an expert.

💡 Add weighted randomness: increase the chance of easier items early, harder items later. Or implement difficulty levels (easy/normal/hard) that control order complexity.

BUG mousePressed() menu bar food selection

If a food spans a slot boundary, clicking on the boundary could select one food or the other unpredictably due to floating-point math.

💡 Add a small tolerance: 'if (mouseX >= menuX + i * foodWidth && mouseX < menuX + (i + 1) * foodWidth)' uses >= on the left to ensure no gaps.

🔄 Code Flow

Code flow showing setup, draw, drawgameui, drawcustomerandorder, updategame, mousepressed, calculatelayout, startcurrentorder, generateNeworder, checkordercomplete, clearfooditems, drawmenuscreen, drawgameoverscreen, windowresized

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

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Canvas Setup] setup --> food-array-init[Menu Foods Array] setup --> sound-oscillators[Sound Effect Setup] setup --> calculatelayout[calculateLayout] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click food-array-init href "#sub-food-array-init" click sound-oscillators href "#sub-sound-oscillators" click calculatelayout href "#fn-calculatelayout" draw --> menu-bar-render[Menu Bar Background] draw --> gamestate-dispatch[Game State Routing] draw --> drawgameui[drawGameUI] draw --> drawcustomerandorder[drawCustomerAndOrder] draw --> updategame[updateGame] click draw href "#fn-draw" click menu-bar-render href "#sub-menu-bar-render" click gamestate-dispatch href "#sub-gamestate-dispatch" click drawgameui href "#fn-drawgameui" click drawcustomerandorder href "#fn-drawcustomerandorder" click updategame href "#fn-updategame" gamestate-dispatch -->|PLAYING| score-display[Score Text] gamestate-dispatch -->|PLAYING| failed-orders-display[Failed Orders Counter with Color] gamestate-dispatch -->|PLAYING| timer-display[Order Timer] gamestate-dispatch -->|MENU| drawmenuscreen[drawMenuScreen] gamestate-dispatch -->|GAME_OVER| drawgameoverscreen[drawGameOverScreen] click score-display href "#sub-score-display" click failed-orders-display href "#sub-failed-orders-display" click timer-display href "#sub-timer-display" click drawmenuscreen href "#fn-drawmenuscreen" click drawgameoverscreen href "#fn-drawgameoverscreen" drawgameui --> menu-positioning[Menu Bar Layout] drawgameui --> score-display drawgameui --> failed-orders-display drawgameui --> timer-display click menu-positioning href "#sub-menu-positioning" drawcustomerandorder --> face-selection[Customer Face Selection] drawcustomerandorder --> face-shadow-render[Shadow and Face Rendering] drawcustomerandorder --> speech-bubble-conditional[Speech Bubble Display Logic] drawcustomerandorder --> order-text-formatting[Order Text Build] drawcustomerandorder --> bubble-dimensions[Speech Bubble Size] drawcustomerandorder --> bubble-tail[Speech Bubble Triangle Tail] drawcustomerandorder --> paid-display[Payment Confirmation] click face-selection href "#sub-face-selection" click face-shadow-render href "#sub-face-shadow-render" click speech-bubble-conditional href "#sub-speech-bubble-conditional" click order-text-formatting href "#sub-order-text-formatting" click bubble-dimensions href "#sub-bubble-dimensions" click bubble-tail href "#sub-bubble-tail" click paid-display href "#sub-paid-display" updategame --> checkordercomplete[Order Complete Victory] updategame --> happy-angry-transition[Happy/Angry State Transition] updategame --> paid-transition[Payment Display Transition] updategame --> leaving-animation[Customer Departure Animation] click checkordercomplete href "#fn-checkordercomplete" click happy-angry-transition href "#sub-happy-angry-transition" click paid-transition href "#sub-paid-transition" click leaving-animation href "#sub-leaving-animation" mousepressed[mousePressed] --> customer-tap[Customer Click Detection] mousepressed --> menu-food-selection[Food Menu Selection] click mousepressed href "#fn-mousepressed" click customer-tap href "#sub-customer-tap" click menu-food-selection href "#sub-menu-food-selection" customer-tap --> state-transition[State Change] customer-tap --> startcurrentorder[startCurrentOrder] click state-transition href "#sub-state-transition" click startcurrentorder href "#fn-startcurrentorder" menu-food-selection --> microwave-food-flow[Microwave Flow] menu-food-selection --> fridge-food-flow[Fridge Flow] click microwave-food-flow href "#sub-microwave-food-flow" click fridge-food-flow href "#sub-fridge-food-flow" microwave-food-flow --> microwave-clear[Microwave Array Reset] microwave-food-flow --> item-count-random[Random Item Count] microwave-food-flow --> food-copy[Food List Copy] microwave-food-flow --> order-generation-loop[Order Item Loop] click microwave-clear href "#sub-microwave-clear" click item-count-random href "#sub-item-count-random" click food-copy href "#sub-food-copy" click order-generation-loop href "#sub-order-generation-loop" order-generation-loop --> timer-reset[Order Timer Start] order-generation-loop --> console-log[Order Logging] click timer-reset href "#sub-timer-reset" click console-log href "#sub-console-log" checkordercomplete --> loop-check[Item Quantity Check Loop] click loop-check href "#sub-loop-check" clearfooditems[clearFoodItems] --> microwave-clear clearfooditems --> chopping-clear[Chopping Board Array Reset] clearfooditems --> fridge-clear[Fridge Array Reset] click chopping-clear href "#sub-chopping-clear" click fridge-clear href "#sub-fridge-clear" windowresized[windowResized] --> canvas-resize[Canvas Resize] windowresized --> layout-recalc[Layout Recalculation] click windowresized href "#fn-windowresized" click canvas-resize href "#sub-canvas-resize" click layout-recalc href "#sub-layout-recalc" layout-recalc --> microwave-reposition[Microwave Item Repositioning] layout-recalc --> chopping-reposition[Chopping Board Item Repositioning] layout-recalc --> fridge-reposition[Fridge Item Repositioning] click microwave-reposition href "#sub-microwave-reposition" click chopping-reposition href "#sub-chopping-reposition" click fridge-reposition href "#sub-fridge-reposition"

❓ Frequently Asked Questions

What visual elements does the Foooooooood sketch create?

The Foooooooood sketch visually represents a cooking simulation with various food emojis, menu items, and designated areas for microwaving, chopping, and storing food.

How can users interact with the Foooooooood sketch?

Users can interact with the sketch by managing food orders, utilizing microwaves and chopping boards to prepare dishes, and responding to customer demands.

What creative coding concepts does the Foooooooood sketch demonstrate?

This sketch demonstrates game state management, array manipulation for food items, and user interaction design within a cooking simulation context.

Preview

Foooooooood - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Foooooooood - Code flow showing setup, draw, drawgameui, drawcustomerandorder, updategame, mousepressed, calculatelayout, startcurrentorder, generateNeworder, checkordercomplete, clearfooditems, drawmenuscreen, drawgameoverscreen, windowresized
Code Flow Diagram