gta 6 current state 4 .5

This sketch creates a playable GTA-style game with a procedurally generated city, vehicle physics, and AI cops that chase the player. Features include a main menu, two game modes (Free Play and Survival), car and pedestrian controls, and dynamic collision physics.

🧪 Try This!

Experiment with the code by making these changes:

  1. Disable free play by default — Change the starting mode to Survival so cops spawn from the start—the game is harder
  2. Make cops spawn twice as fast — Lower the spawn interval from 600 frames (10 seconds) to 300 frames (5 seconds)—increases difficulty
  3. Double the car's max speed — Change the car's max speed from 18 to 36 pixels per frame—the vehicle becomes twice as fast
  4. Make buildings denser — Lower the random threshold from 0.3 to 0.6 so 60% of grid cells have buildings instead of 30%—the city becomes crowded
  5. Change the title — Customize the game's title on the main menu
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a fully playable top-down GTA-style game inside p5.js. The player controls a character on foot or in a car, navigates a procedurally generated city with hundreds of buildings, and either survives against spawning cop cars and officers (Survival Mode) or explores freely without threats (Free Play Mode). The sketch combines multiple advanced p5.js techniques: the draw loop for continuous animation, vector math for physics simulation, collision detection against buildings and enemies, state machines to switch between menus and gameplay, and object-oriented design with four custom classes.

The code is organized into a main game loop (draw), four entity classes (Person, Car, CopCar, CopPerson), and a comprehensive UI system that manages menus, buttons, and in-game HUD elements. By studying it you will learn how professional game sketches structure state management, how to implement basic 2D physics with velocity and acceleration, how procedural generation creates massive worlds efficiently, and how to build responsive controls for both keyboard and touch input.

⚙️ How It Works

  1. On startup, setup() creates a window-sized canvas, loads a background image for menus, and procedurally generates a grid of 500+ buildings across a 10,000×10,000 pixel world—using random() to skip 70% of grid positions so the city feels organic rather than uniform.
  2. The game starts in MENU state, displaying a background image and three buttons. When the player clicks PLAY, gameState becomes PLAYING, resetGame() spawns the player character and car at the origin, and the draw loop begins rendering the actual game world.
  3. Every frame in PLAYING state, the draw loop updates all entities: the player moves via WASD or arrow keys (or mouse click for mobile), the car accelerates and steers, and cop cars chase the player's position while spawning cop officers on foot if the player leaves their vehicle.
  4. A camera system (camX, camY) tracks either the player or the car and translates the entire world so the player stays centered on screen—this makes the 10,000×10,000 world feel seamless despite only drawing 2× the canvas width/height.
  5. Collision detection works by checking if the player's or car's next position overlaps any building's bounding box; if it does, the move is blocked and (for the car) health is reduced proportional to impact speed.
  6. When the car's health reaches zero or the player touches a cop, gameState switches to DESTROYED or BUSTED. If Instant Respawn is enabled, the game resets immediately; otherwise, a game-over screen displays the survival time before returning to the menu.

🎓 Concepts You'll Learn

Game state machinesProcedural generationVector-based physicsCollision detectionCamera follow systemObject-oriented designTouch and keyboard inputParticle effects

📝 Code Breakdown

setup()

setup() runs once at startup. Here it creates the canvas, loads assets, and generates the entire world. Procedural generation (using random placement rules) lets you create vast worlds—this 10,000×10,000 city with 500+ buildings would be tedious to hardcode, but a loop generates it in milliseconds.

🔬 This nested loop creates a grid of buildings. What happens if you change the outer loop's step from 400 to 800—do you expect twice as many or half as many buildings?

  for(let x = -5000; x <= 5000; x += 400) {
    for(let y = -5000; y <= 5000; y += 400) {
      if(random() > 0.3) {
        if(abs(x) < 200 && abs(y) < 200) continue;
function setup() {
  createCanvas(windowWidth, windowHeight);
  
  // Safely load the background image.
  loadImage(bgImageUrl, 
    (img) => { bgImage = img; }, 
    (err) => { console.warn("Background image failed to load (possibly blocked by CORS)."); }
  );
  
  // Generate Procedural City once
  for(let x = -5000; x <= 5000; x += 400) {
    for(let y = -5000; y <= 5000; y += 400) {
      if(random() > 0.3) {
        if(abs(x) < 200 && abs(y) < 200) continue; 
        buildings.push({
          x: x + random(-15, 15), 
          y: y + random(-15, 15), 
          w: random(180, 260), 
          h: random(180, 260),
          col: color(random(50, 100)),
          roof: color(random(30, 50))
        });
      }
    }
  }
  
  resetGame();
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

function-call Canvas creation createCanvas(windowWidth, windowHeight);

Creates a fullscreen canvas that adapts to the window size

function-call Async image loading loadImage(bgImageUrl, (img) => { bgImage = img; }, (err) => { console.warn(...); });

Loads a background image with error handling so the game doesn't break if the image is blocked

for-loop Procedural city generation for(let x = -5000; x <= 5000; x += 400) { for(let y = -5000; y <= 5000; y += 400) { ... } }

Creates a 50×50 grid of possible building locations and randomly populates them to generate a diverse city

createCanvas(windowWidth, windowHeight);
Creates the p5.js canvas at full window size so the game fills the screen on any device
loadImage(bgImageUrl, (img) => { bgImage = img; }, (err) => { console.warn(...); });
Asynchronously loads the background image from a URL; if it fails (CORS block), a warning is logged but the game still works
for(let x = -5000; x <= 5000; x += 400) {
Outer loop that steps through x-coordinates from -5000 to +5000 in increments of 400 pixels (the grid size)
for(let y = -5000; y <= 5000; y += 400) {
Inner loop that steps through y-coordinates, creating a 2D grid of potential building spots
if(random() > 0.3) {
70% of grid cells will create a building (since random() returns 0–1, and 0.7–1.0 is > 0.3); this makes the city feel organic with gaps
if(abs(x) < 200 && abs(y) < 200) continue;
Skips any building generation near the origin (within 200 pixels), leaving open space for the player to start
buildings.push({ x: x + random(-15, 15), y: y + random(-15, 15), w: random(180, 260), h: random(180, 260), col: color(random(50, 100)), roof: color(random(30, 50)) });
Creates a building object with position (with ±15 pixel jitter to avoid a rigid grid), random width/height, and random gray colors for the walls and roof
resetGame();
Initializes the player, car, cops, and other game state once the city is fully generated

resetGame()

resetGame() is called when the player clicks PLAY and whenever they respawn. It wipes the world clean and readies all variables for a fresh game. This separation of setup (world generation) and resetGame (game state) is a best practice because setup happens once at startup, but resetGame happens many times as the player replays.

function resetGame() {
  person = new Person(0, 50);
  car = new Car(0, 0);
  cops = [];
  copPeople = [];
  explosionParticles = [];
  survivalFrames = 0;
  
  if (!isFreePlay) {
    spawnCopCar();
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

function-call Entity initialization person = new Person(0, 50); car = new Car(0, 0);

Creates a fresh player character and car at the starting positions

calculation Clear enemy arrays cops = []; copPeople = [];

Empties the cop car and cop pedestrian arrays so no enemies remain from the previous game

conditional First cop spawn in survival mode if (!isFreePlay) { spawnCopCar(); }

In Survival Mode, spawns the first cop car; in Free Play, skips this so no cops ever spawn

person = new Person(0, 50);
Creates a new player character object at coordinates (0, 50)—just slightly below the center of the world
car = new Car(0, 0);
Creates a new car object at the origin (0, 0), positioned next to where the player spawns
cops = [];
Clears the cops array by replacing it with an empty array, removing all cop cars from the previous game
copPeople = [];
Clears the copPeople array, removing all cop pedestrians on foot
explosionParticles = [];
Clears any leftover explosion particles from a previous crash
survivalFrames = 0;
Resets the survival timer to 0 so the timer on the HUD starts fresh
if (!isFreePlay) {
Checks if Free Play mode is OFF (meaning Survival Mode is ON)
spawnCopCar();
In Survival Mode, immediately spawns the first cop car to begin the challenge

spawnCopCar()

spawnCopCar() demonstrates intelligent spawning: cops don't appear inside buildings (ruining the illusion) but spawn off-screen at unpredictable angles. The 50-attempt retry loop is a common game dev pattern for handling randomness constraints. If a random location fails a rule check, try again—but set a limit to avoid hangs.

// SPATIAL AWARENESS SPAWNING
function spawnCopCar() {
  let tX, tY;
  let validSpawn = false;
  let attempts = 0;
  
  // Try to find a spawn location that isn't inside a building (up to 50 tries)
  while (!validSpawn && attempts < 50) {
    let angle = random(TWO_PI);
    let spawnDist = max(width, height);
    
    // Target active camera position so they spawn out of view
    tX = camX + cos(angle) * spawnDist;
    tY = camY + sin(angle) * spawnDist;
    
    validSpawn = true;
    for (let b of buildings) {
      // Check if the spawned coordinate overlaps the building (with 40px extra padding)
      if (abs(tX - b.x) < b.w/2 + 40 && abs(tY - b.y) < b.h/2 + 40) {
        validSpawn = false;
        break; // Stop checking buildings, this spot is invalid
      }
    }
    attempts++;
  }
  
  cops.push(new CopCar(tX, tY));
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

while-loop Valid spawn location search while (!validSpawn && attempts < 50) { ... }

Repeatedly tries random spawn points until finding one that doesn't overlap a building, with a max of 50 attempts to avoid infinite loops

calculation Spawn on circle around camera tX = camX + cos(angle) * spawnDist; tY = camY + sin(angle) * spawnDist;

Places the cop car on a circle around the camera so it spawns off-screen but visible if the player turns

for-loop Building overlap detection for (let b of buildings) { if (abs(tX - b.x) < b.w/2 + 40 && abs(tY - b.y) < b.h/2 + 40) { validSpawn = false; break; } }

Checks every building to ensure the spawn point doesn't overlap any of them

let tX, tY;
Declares temporary variables to hold the candidate spawn coordinates
let validSpawn = false;
A flag that tracks whether the current spawn location is valid (not inside a building); starts false so the loop runs at least once
let attempts = 0;
A counter that tracks how many spawn attempts have been made, capping out at 50 to prevent infinite loops
while (!validSpawn && attempts < 50) {
Loops until either a valid spawn is found OR 50 attempts have been made
let angle = random(TWO_PI);
Picks a random angle between 0 and 2π radians (a full circle)
let spawnDist = max(width, height);
Sets spawn distance to the larger of canvas width or height, guaranteeing the cop spawns beyond the visible screen
tX = camX + cos(angle) * spawnDist;
Calculates the cop's x-coordinate by moving spawnDist pixels from the camera in the chosen angle direction
tY = camY + sin(angle) * spawnDist;
Calculates the cop's y-coordinate using sine for the perpendicular component
validSpawn = true;
Optimistically assumes this spawn is valid; will be set to false if any building overlaps
for (let b of buildings) {
Loops through every building object to check for collisions
if (abs(tX - b.x) < b.w/2 + 40 && abs(tY - b.y) < b.h/2 + 40) {
Uses rectangle collision: checks if the spawn point's distance to the building center is less than half the building's width/height plus a 40-pixel safety margin
validSpawn = false; break;
If a collision is found, marks this spawn as invalid and breaks out of the building loop to try a new angle
attempts++;
Increments the attempt counter so the while loop eventually exits if no valid spot is found after 50 tries
cops.push(new CopCar(tX, tY));
Creates a new CopCar object at the validated spawn coordinates and adds it to the cops array

createExplosion(x, y)

createExplosion() builds a particle system—the foundation of many visual effects in games. Each particle is a simple object with position, velocity, and lifecycle. By creating many particles with varied properties, you get an emergent, organic-looking effect.

// UPGRADED EXPLOSION MECHANIC
function createExplosion(x, y) {
  for (let i = 0; i < 60; i++) {
    explosionParticles.push({
      x: x, y: y,
      vx: random(-8, 8), vy: random(-8, 8),
      size: random(20, 80),
      life: 255,
      isSmoke: random() > 0.5 // Mix of fire and smoke
    });
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Particle creation loop for (let i = 0; i < 60; i++) { explosionParticles.push({ ... }); }

Creates 60 particle objects, each with random velocity and size, and pushes them into the explosionParticles array

for (let i = 0; i < 60; i++) {
Loops 60 times, creating 60 individual particle objects for the explosion effect
explosionParticles.push({
Pushes a new particle object onto the explosionParticles array
x: x, y: y,
Sets the particle's starting position to the explosion center (passed as function arguments)
vx: random(-8, 8), vy: random(-8, 8),
Gives each particle a random velocity in both x and y directions (between -8 and +8 pixels/frame), making them shoot outward in all directions
size: random(20, 80),
Each particle starts with a random diameter between 20 and 80 pixels, creating visual variety
life: 255,
Sets the particle's alpha (transparency) to 255 (fully opaque); it will fade to 0 over time
isSmoke: random() > 0.5 // Mix of fire and smoke
Randomly decides if this particle is smoke (darker gray) or fire (orange/red); roughly 50% of each

drawExplosion()

drawExplosion() runs every frame when the car is destroyed. It updates and renders all particles, creating a dynamic, physics-based visual effect. The backward loop (i--) is crucial: if you loop forward and remove items, you'll skip particles. The color mapping is clever—using map() to animate a color channel based on life creates smooth transitions.

🔬 These lines move the particle and apply friction. What happens if you remove the friction lines (p.vx *= 0.92; p.vy *= 0.92;)? Will particles move faster or keep moving forever?

    p.x += p.vx; p.y += p.vy;
    
    // Add friction so particles slow down as they expand
    p.vx *= 0.92;
    p.vy *= 0.92;
function drawExplosion() {
  noStroke();
  for (let i = explosionParticles.length - 1; i >= 0; i--) {
    let p = explosionParticles[i];
    p.x += p.vx; p.y += p.vy;
    
    // Add friction so particles slow down as they expand
    p.vx *= 0.92;
    p.vy *= 0.92;
    
    p.life -= 3;
    p.size += 0.8;
    
    if (p.isSmoke) {
      fill(50, 50, 50, p.life);
    } else {
      // Fire fades from yellow to orange to red
      fill(255, map(p.life, 255, 0, 200, 0), 0, p.life);
    }
    
    circle(p.x, p.y, p.size);
    if (p.life <= 0) explosionParticles.splice(i, 1);
  }
  
  // Draw burnt car chassis
  push(); 
  translate(car.pos.x, car.pos.y); 
  rotate(car.angle);
  rectMode(CENTER); 
  fill(30); 
  rect(0, 0, 40, 20, 5);
  fill(10);
  rect(2, 0, 20, 16, 3);
  pop();
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

for-loop Particle animation loop for (let i = explosionParticles.length - 1; i >= 0; i--) { ... }

Updates each particle's position, velocity, life, and size, then draws it; removes dead particles

calculation Velocity and friction p.x += p.vx; p.y += p.vy; p.vx *= 0.92; p.vy *= 0.92;

Moves each particle and applies friction so it slows down over time

conditional Fire/smoke color transition if (p.isSmoke) { fill(50, 50, 50, p.life); } else { fill(255, map(p.life, 255, 0, 200, 0), 0, p.life); }

Draws smoke as dark gray and fire as red-to-yellow, both fading with alpha

function-call Burnt car chassis push(); translate(car.pos.x, car.pos.y); rotate(car.angle); ... rect(...); pop();

Draws the car's burnt frame at its position and angle

noStroke();
Disables outlines so particles appear as solid circles
for (let i = explosionParticles.length - 1; i >= 0; i--) {
Loops backward through the particles array (from last to first); looping backward is safe when removing items with splice()
let p = explosionParticles[i];
Stores a reference to the current particle for easier access
p.x += p.vx; p.y += p.vy;
Updates the particle's position by adding its velocity—this creates smooth motion
p.vx *= 0.92; p.vy *= 0.92;
Multiplies velocity by 0.92 each frame, reducing it by 8% each step—this is friction, making particles slow down as if air resistance is present
p.life -= 3;
Decreases the particle's alpha by 3 each frame, making it fade out over ~85 frames
p.size += 0.8;
Grows the particle's size slightly each frame, making the explosion appear to expand
if (p.isSmoke) { fill(50, 50, 50, p.life); } else { fill(255, map(p.life, 255, 0, 200, 0), 0, p.life); }
Sets the fill color: smoke is dark gray with fading alpha; fire is bright red (255, 0, 0) but the green channel (second parameter) animates from 200 down to 0, creating a yellow-to-red fade as it ages
circle(p.x, p.y, p.size);
Draws the particle as a colored circle at its current position and size
if (p.life <= 0) explosionParticles.splice(i, 1);
Once a particle's life reaches 0 (fully transparent), removes it from the array so it's no longer updated or drawn
push(); translate(car.pos.x, car.pos.y); rotate(car.angle);
Saves the current transform state, then moves and rotates the coordinate system to the car's position and angle
fill(30); rect(0, 0, 40, 20, 5);
Draws a dark gray rectangle (the burnt chassis) centered at the car's position
fill(10); rect(2, 0, 20, 16, 3);
Draws a darker rectangle inside (a burnt window frame)
pop();
Restores the previous transform state so later draws aren't affected by the translation/rotation

draw()

draw() is the heart of the game. It runs 60 times per second and orchestrates everything: rendering, game logic, collision, entity updates, and UI. The camera system (translate) is a key concept—instead of moving every game object each frame, you move the canvas, creating the illusion of a camera following the player.

🔬 These lines draw three rectangles per building (sidewalk, base, roof). What happens if you change the roofing line from (b.w - 20, b.h - 20) to (b.w, b.h)—will the roof get smaller or bigger, and will it still be visible?

  // --- DRAW BUILDINGS ---
  for(let b of buildings) {
    if (abs(b.x - camX) > width/2 + 300 || abs(b.y - camY) > height/2 + 300) continue;
    rectMode(CENTER);
    fill(90); rect(b.x, b.y, b.w + 30, b.h + 30, 8); // Sidewalk
    fill(b.col); rect(b.x, b.y, b.w, b.h, 4);        // Base
    fill(b.roof); rect(b.x, b.y, b.w - 20, b.h - 20, 2); // Roof
function draw() {
  // If we are in menu states, show the background image instead of the city
  if (gameState === "MENU" || gameState === "SETTINGS" || gameState === "CREDITS") {
    background(20);
    if (bgImage) {
      imageMode(CORNER);
      image(bgImage, 0, 0, width, height);
    }
    drawUI();
    return; // Skip rendering the 3D world to save performance
  }

  // --- RENDERING THE GAME WORLD ---
  background(50, 120, 60); // Grass
  
  camX = person.inCar ? car.pos.x : person.pos.x;
  camY = person.inCar ? car.pos.y : person.pos.y;
  
  push();
  translate(width/2 - camX, height/2 - camY);
  
  // --- DRAW ROADS ---
  let startX = floor((camX - width) / 400) * 400;
  let endX = startX + width * 2;
  let startY = floor((camY - height) / 400) * 400;
  let endY = startY + height * 2;
  
  stroke(60); // Asphalt
  strokeWeight(100);
  strokeCap(SQUARE);
  for(let i = startX; i <= endX; i += 400) line(i, startY - 400, i, endY + 400);
  for(let i = startY; i <= endY; i += 400) line(startX - 400, i, endX + 400, i);
  
  stroke(255, 200, 0, 150); // Yellow lines
  strokeWeight(3);
  drawingContext.setLineDash([20, 20]);
  for(let i = startX; i <= endX; i += 400) line(i, startY - 400, i, endY + 400);
  for(let i = startY; i <= endY; i += 400) line(startX - 400, i, endX + 400, i);
  drawingContext.setLineDash([]); 
  noStroke();

  // --- DRAW BUILDINGS ---
  for(let b of buildings) {
    if (abs(b.x - camX) > width/2 + 300 || abs(b.y - camY) > height/2 + 300) continue;
    rectMode(CENTER);
    fill(90); rect(b.x, b.y, b.w + 30, b.h + 30, 8); // Sidewalk
    fill(b.col); rect(b.x, b.y, b.w, b.h, 4);        // Base
    fill(b.roof); rect(b.x, b.y, b.w - 20, b.h - 20, 2); // Roof
  }
  
  // --- GAME LOGIC ---
  if (gameState === "PLAYING") {
    survivalFrames++;
    
    // Spawn 1 new cop every 10 seconds (only if Free Play is off)
    if (survivalFrames % 600 === 0 && !isFreePlay) spawnCopCar();
    
    // Update player
    car.update();
    person.update();
    
    // Update Cops
    for (let i = cops.length - 1; i >= 0; i--) {
      cops[i].update({x: camX, y: camY});
      if (dist(camX, camY, cops[i].pos.x, cops[i].pos.y) > max(width, height) * 2.5) {
        cops.splice(i, 1);
      }
    }
    
    // Update Cop People (On Foot)
    for (let i = copPeople.length - 1; i >= 0; i--) {
      copPeople[i].update();
      if (dist(camX, camY, copPeople[i].pos.x, copPeople[i].pos.y) > max(width, height) * 2) {
        copPeople.splice(i, 1);
      }
    }
    
    // Handle Instant Respawn smoothly
    if (needsRespawn) {
      resetGame();
      needsRespawn = false;
    }
  }

  // --- DRAW ENTITIES ---
  for (let c of cops) c.draw();
  for (let cp of copPeople) cp.draw();
  
  if (gameState === "DESTROYED") {
    drawExplosion();
  } else {
    car.draw();
  }
  
  if (gameState !== "BUSTED") person.draw();
  
  pop();
  
  // --- UI OVERLAY ---
  drawUI();
}
Line-by-line explanation (29 lines)

🔧 Subcomponents:

calculation Camera follow camX = person.inCar ? car.pos.x : person.pos.x; camY = person.inCar ? car.pos.y : person.pos.y; push(); translate(width/2 - camX, height/2 - camY);

Tracks the player or car position and translates the world so the player stays centered on screen

for-loop Road rendering for(let i = startX; i <= endX; i += 400) line(...); for(let i = startY; i <= endY; i += 400) line(...);

Draws a grid of roads that align with the building grid

for-loop Building rendering with culling for(let b of buildings) { if (abs(b.x - camX) > width/2 + 300 || abs(b.y - camY) > height/2 + 300) continue; ... }

Only draws buildings near the camera to save performance (culling)

conditional Game state updates if (gameState === "PLAYING") { survivalFrames++; ... }

Updates all game entities (player, car, cops) and handles respawning

for-loop Entity drawing for (let c of cops) c.draw(); for (let cp of copPeople) cp.draw(); ... car.draw(); ... person.draw();

Renders all cops, cop pedestrians, the car, and the player

if (gameState === "MENU" || gameState === "SETTINGS" || gameState === "CREDITS") {
Checks if the game is in a menu state instead of active gameplay
background(20);
Fills the screen with dark color as a base
if (bgImage) { imageMode(CORNER); image(bgImage, 0, 0, width, height); }
If the background image loaded successfully, draws it stretched to fill the entire canvas
drawUI();
Renders menu buttons and text
return;
Exits the draw function early, skipping all game world rendering to improve performance when menus are shown
background(50, 120, 60);
Clears the canvas with a grass-green color for gameplay screens
camX = person.inCar ? car.pos.x : person.pos.x;
Sets the camera's x position to the car's x if the player is in the car, otherwise the player's x (ternary operator)
push(); translate(width/2 - camX, height/2 - camY);
Saves the current transform state, then translates so the camera position (camX, camY) moves to the center of the screen (width/2, height/2)
let startX = floor((camX - width) / 400) * 400;
Calculates the leftmost grid line x-coordinate to draw, aligned to the 400-pixel building grid and slightly beyond the camera view
stroke(60); strokeWeight(100); strokeCap(SQUARE);
Sets up the stroke style for roads: dark gray, thick (100px), with square caps at the ends
for(let i = startX; i <= endX; i += 400) line(i, startY - 400, i, endY + 400);
Draws vertical road lines (north-south roads) spaced 400 pixels apart
for(let i = startY; i <= endY; i += 400) line(startX - 400, i, endX + 400, i);
Draws horizontal road lines (east-west roads) spaced 400 pixels apart
stroke(255, 200, 0, 150); strokeWeight(3); drawingContext.setLineDash([20, 20]);
Changes to yellow color, thin stroke, and enables a dashed line pattern (20px dash, 20px gap)
drawingContext.setLineDash([]);
Disables the dashed line pattern by clearing the dash array
for(let b of buildings) { if (abs(b.x - camX) > width/2 + 300 || abs(b.y - camY) > height/2 + 300) continue;
Loops through every building; skips (culls) any that are more than half the screen width/height plus 300 pixels away from the camera
fill(90); rect(b.x, b.y, b.w + 30, b.h + 30, 8);
Draws a slightly larger gray rectangle behind each building (the sidewalk)
fill(b.col); rect(b.x, b.y, b.w, b.h, 4);
Draws the main building body with its random gray color
fill(b.roof); rect(b.x, b.y, b.w - 20, b.h - 20, 2);
Draws a slightly smaller, darker rectangle on top (the roof)
if (gameState === "PLAYING") { survivalFrames++;
If actively playing, increments the survival frame counter (used for the timer and cop spawn scheduling)
if (survivalFrames % 600 === 0 && !isFreePlay) spawnCopCar();
Every 600 frames (~10 seconds at 60 fps), if Survival Mode is on (not Free Play), spawns a new cop car
car.update(); person.update();
Calls the update methods for the car and player to process their movement and collisions
for (let i = cops.length - 1; i >= 0; i--) { cops[i].update({x: camX, y: camY}); if (dist(camX, camY, cops[i].pos.x, cops[i].pos.y) > max(width, height) * 2.5) { cops.splice(i, 1); } }
Loops backward through all cop cars, updates each with the camera position as the target, and removes any that are far from the camera
for (let i = copPeople.length - 1; i >= 0; i--) { copPeople[i].update(); if (dist(camX, camY, copPeople[i].pos.x, copPeople[i].pos.y) > max(width, height) * 2) { copPeople.splice(i, 1); } }
Same as cops, but for cop pedestrians (slightly closer culling distance)
if (needsRespawn) { resetGame(); needsRespawn = false; }
If a respawn flag was set (e.g., by instant respawn mode), resets the game and clears the flag
for (let c of cops) c.draw(); for (let cp of copPeople) cp.draw();
Draws all cop cars and cop pedestrians by calling their draw methods
if (gameState === "DESTROYED") { drawExplosion(); } else { car.draw(); }
If the car was destroyed, shows the explosion effect; otherwise draws the car normally
if (gameState !== "BUSTED") person.draw();
Draws the player character unless they were busted (caught by a cop)
pop();
Restores the previous transform state (undoing the camera translation)
drawUI();
Renders the HUD and any interactive UI elements on top of the game world

drawUI()

drawUI() handles all menu and HUD rendering. It's a good example of state-driven UI—different content displays based on gameState. The hover detection for buttons demonstrates interactive UI: checking if the cursor overlaps a rect and changing appearance accordingly. In production games, this would be refactored into a UI class, but for a small sketch it's practical to keep it centralized.

🔬 This if-else block shows different HUD text depending on whether Free Play is on. What happens if you swap the contents of the two branches so Free Play shows the survival timer instead?

    if (isFreePlay) {
      text("Mode: FREE PLAY", 15, 15);
      fill(100, 255, 100);
      text("No cops spawning.", 15, 45);
    } else {
      text("Surviving: " + seconds + " seconds", 15, 15);
      fill(255, 100, 100);
      text("Cops chasing: " + (cops.length + copPeople.length), 15, 45);
    }
// --- UI FUNCTION ---
function drawUI() {
  activeButtons = []; // Clear buttons every frame
  
  if (gameState === "MENU") {
    drawOverlay(160);
    drawTitle("GTA VI: DEMASTERED");
    drawButton("PLAY", height/2 - 20, () => { resetGame(); gameState = "PLAYING"; });
    drawButton("SETTINGS", height/2 + 50, () => { gameState = "SETTINGS"; });
    drawButton("CREDITS", height/2 + 120, () => { gameState = "CREDITS"; });
  } 
  else if (gameState === "SETTINGS") {
    drawOverlay(200);
    drawTitle("SETTINGS");
    
    // Settings Toggles
    drawButton("Free Play (No Cops): " + (isFreePlay ? "ON" : "OFF"), height/2 - 40, () => { isFreePlay = !isFreePlay; });
    drawButton("Instant Respawn: " + (isInstantRespawn ? "ON" : "OFF"), height/2 + 30, () => { isInstantRespawn = !isInstantRespawn; });
    
    drawButton("BACK", height/2 + 120, () => { gameState = "MENU"; });
  }
  else if (gameState === "CREDITS") {
    drawOverlay(220);
    drawTitle("CREDITS");
    fill(255); textSize(24); textAlign(CENTER, CENTER); textStyle(BOLD);
    text("Made by Corban machen (Corbun)", width/2, height/2 - 20);
    textSize(18); fill(220); textStyle(NORMAL);
    text("Credits to Google for home background image", width/2, height/2 + 20);
    
    drawButton("BACK", height/2 + 120, () => { gameState = "MENU"; });
  }
  else if (gameState === "PLAYING") {
    // HUD - Top Left
    fill(0, 150); rect(0, 0, 260, 90);
    fill(255); textSize(20); textAlign(LEFT, TOP);
    let seconds = floor(survivalFrames / 60);
    
    if (isFreePlay) {
      text("Mode: FREE PLAY", 15, 15);
      fill(100, 255, 100);
      text("No cops spawning.", 15, 45);
    } else {
      text("Surviving: " + seconds + " seconds", 15, 15);
      fill(255, 100, 100);
      text("Cops chasing: " + (cops.length + copPeople.length), 15, 45);
    }
    
    // HUD - Top Right Health Bar (shifted left to make room for X button)
    if (person.inCar) {
      fill(0, 150); rect(width - 280, 0, 220, 70);
      fill(255); textAlign(RIGHT, TOP);
      text("Car Condition", width - 80, 15);
      
      noStroke(); fill(100); rect(width - 260, 45, 180, 15);
      let hpW = map(max(car.health, 0), 0, 100, 0, 180);
      fill(car.health > 30 ? color(50, 255, 50) : color(255, 50, 50));
      rect(width - 260, 45, hpW, 15);
    } else {
      let d = dist(person.pos.x, person.pos.y, car.pos.x, car.pos.y);
      if (d < 60) {
        fill(50, 255, 50); textAlign(CENTER, TOP);
        text("Press ENTER to enter car!", width/2, 30);
      }
    }

    // X Button (Quit to Menu) in the top-right corner
    let quitBx = width - 50; let quitBy = 15; let quitBw = 35; let quitBh = 35;
    activeButtons.push({x: quitBx, y: quitBy, w: quitBw, h: quitBh, action: () => { gameState = "MENU"; }});
    
    let ixClick = mouseX; let iyClick = mouseY;
    if (touches.length > 0) { ixClick = touches[0].x; iyClick = touches[0].y; }
    let isQuitHover = (ixClick > quitBx && ixClick < quitBx + quitBw && iyClick > quitBy && iyClick < quitBy + quitBh);
    
    fill(isQuitHover ? color(255, 100, 100) : color(220, 50, 50));
    rectMode(CORNER);
    rect(quitBx, quitBy, quitBw, quitBh, 5);
    
    fill(255); textSize(22); textAlign(CENTER, CENTER); textStyle(BOLD);
    text("X", quitBx + quitBw/2, quitBy + quitBh/2 + 2);
    textStyle(NORMAL);

    // Interact Button (Enter/Exit vehicle on mobile)
    let btnX = width - 60, btnY = height - 60;
    fill(255, 50); stroke(255, 100); strokeWeight(2); circle(btnX, btnY, 80);
    noStroke(); fill(255); textAlign(CENTER, CENTER); textSize(16);
    text(person.inCar ? "EXIT" : "ENTER", btnX, btnY);
  }
  else if (gameState === "BUSTED" || gameState === "DESTROYED") {
    // Made the overlay lighter (140) so the explosion is visible
    fill(0, 140); rect(0, 0, width, height);
    fill(gameState === "BUSTED" ? color(50, 100, 255) : color(255, 50, 50));
    textAlign(CENTER, CENTER);
    textSize(60); textStyle(BOLD);
    text(gameState, width/2, height/2 - 40);
    textSize(24); fill(255); textStyle(NORMAL);
    
    if (!isFreePlay) {
      text("You survived " + floor(survivalFrames/60) + " seconds.", width/2, height/2 + 20);
    }
    
    drawButton("MAIN MENU", height/2 + 100, () => { gameState = "MENU"; });
  }
}
Line-by-line explanation (23 lines)

🔧 Subcomponents:

conditional Settings menu rendering else if (gameState === "SETTINGS") { ... }

Shows toggle buttons for Free Play and Instant Respawn modes

conditional Credits screen rendering else if (gameState === "CREDITS") { ... }

Displays credit text and a Back button

conditional In-game HUD rendering else if (gameState === "PLAYING") { ... }

Shows survival timer, cop count, car health bar, and mobile buttons (Enter/Exit, Quit)

conditional Game over screen rendering else if (gameState === "BUSTED" || gameState === "DESTROYED") { ... }

Displays the game-over state, survival time, and a return-to-menu button

activeButtons = [];
Clears the list of clickable buttons every frame so old button regions don't stay active
if (gameState === "MENU") {
Checks if the game is currently on the main menu screen
drawOverlay(160);
Draws a semi-transparent black rectangle to dim the background (opacity 160 out of 255)
drawTitle("GTA VI: DEMASTERED");
Renders the large title text using a helper function
drawButton("PLAY", height/2 - 20, () => { resetGame(); gameState = "PLAYING"; });
Draws a PLAY button; when clicked, resets the game and switches to PLAYING state
drawButton("SETTINGS", height/2 + 50, () => { gameState = "SETTINGS"; });
Draws a SETTINGS button that switches to the SETTINGS menu state
drawButton("CREDITS", height/2 + 120, () => { gameState = "CREDITS"; });
Draws a CREDITS button that switches to the CREDITS menu state
drawButton("Free Play (No Cops): " + (isFreePlay ? "ON" : "OFF"), height/2 - 40, () => { isFreePlay = !isFreePlay; });
Draws a toggle button showing the current Free Play status and flipping it on click
fill(255); textSize(24); textAlign(CENTER, CENTER); textStyle(BOLD);
Sets text styling: white color, 24pt size, centered alignment, bold
text("Made by Corban machen (Corbun)", width/2, height/2 - 20);
Draws the creator's name centered horizontally
fill(0, 150); rect(0, 0, 260, 90);
Draws a semi-transparent black rectangle in the top-left corner as a background for the HUD text
let seconds = floor(survivalFrames / 60);
Converts the frame count to seconds by dividing by 60 (the assumed frame rate) and rounding down
if (isFreePlay) { text("Mode: FREE PLAY", 15, 15); fill(100, 255, 100); text("No cops spawning.", 15, 45); }
If Free Play is on, displays that mode with green text; otherwise shows survival time and red cop count
if (person.inCar) { fill(0, 150); rect(width - 280, 0, 220, 70); ... }
If the player is in a car, displays a health bar in the top-right showing car condition
let hpW = map(max(car.health, 0), 0, 100, 0, 180);
Maps the car's health (0–100) to a bar width (0–180 pixels), clamping health to at least 0
fill(car.health > 30 ? color(50, 255, 50) : color(255, 50, 50));
Colors the health bar green if above 30% health, red otherwise
let d = dist(person.pos.x, person.pos.y, car.pos.x, car.pos.y); if (d < 60) { text("Press ENTER to enter car!", width/2, 30); }
If the player is near the car (within 60 pixels), displays a prompt to enter the vehicle
let ixClick = mouseX; let iyClick = mouseY; if (touches.length > 0) { ixClick = touches[0].x; iyClick = touches[0].y; }
Gets the click position from either the mouse or the first touch point (mobile support)
let isQuitHover = (ixClick > quitBx && ixClick < quitBx + quitBw && iyClick > quitBy && iyClick < quitBy + quitBh);
Checks if the cursor is inside the quit button's bounding box
fill(isQuitHover ? color(255, 100, 100) : color(220, 50, 50));
Colors the quit button brighter red on hover, darker red normally (visual feedback)
text(person.inCar ? "EXIT" : "ENTER", btnX, btnY);
Labels the mobile button as EXIT if in the car, ENTER if outside
fill(gameState === "BUSTED" ? color(50, 100, 255) : color(255, 50, 50));
Colors the game-over text blue for BUSTED (caught) or red for DESTROYED (crashed)
text("You survived " + floor(survivalFrames/60) + " seconds.", width/2, height/2 + 20);
Displays the final survival time in seconds (not shown in Free Play mode)

drawOverlay(opacity)

drawOverlay() is a simple helper function that dims the screen by drawing a semi-transparent black rectangle. It's used to make menu text more readable on top of the background image. Extracting helper functions like this keeps code organized and reusable.

function drawOverlay(opacity) {
  fill(0, opacity); rect(0, 0, width, height);
}
Line-by-line explanation (2 lines)
fill(0, opacity);
Sets the fill color to black with the specified opacity (0–255); higher values are more opaque
rect(0, 0, width, height);
Draws a rectangle covering the entire canvas, creating a darkening overlay

drawTitle(t)

drawTitle() is another helper that renders a large, responsive title. The textSize calculation (min(60, width/10)) adapts to different screen sizes: on small phones it scales down, on large monitors it stays at 60pt. This is a simple form of responsive design.

function drawTitle(t) {
  fill(255); textAlign(CENTER, CENTER);
  textSize(min(60, width/10)); textStyle(BOLD);
  text(t, width/2, height/5);
  textStyle(NORMAL);
}
Line-by-line explanation (6 lines)
fill(255);
Sets the text color to white
textAlign(CENTER, CENTER);
Centers text both horizontally and vertically around the specified coordinates
textSize(min(60, width/10));
Sets text size to either 60pt or 1/10 of the canvas width, whichever is smaller (responsive design)
textStyle(BOLD);
Makes the text bold
text(t, width/2, height/5);
Draws the title text centered horizontally, 1/5 of the way down the screen
textStyle(NORMAL);
Resets text style to normal for subsequent text draws

drawButton(label, y, onClick)

drawButton() is a reusable UI component that draws a clickable button with hover feedback. It separates rendering (drawButton) from logic (checkMenuClicks), which is good design. Every button registers itself in activeButtons so the click handler can find it—this indirect approach is more flexible than hardcoding button positions.

function drawButton(label, y, onClick) {
  let bw = 350; let bh = 50;
  let bx = width/2 - bw/2;
  
  activeButtons.push({x: bx, y: y, w: bw, h: bh, action: onClick});
  
  let iX = mouseX; let iY = mouseY;
  if (touches.length > 0) { iX = touches[0].x; iY = touches[0].y; }
  let isHover = (iX > bx && iX < bx + bw && iY > y && iY < y + bh);
  
  fill(isHover ? 200 : 255);
  rectMode(CORNER);
  rect(bx, y, bw, bh, 8);
  
  fill(0); textSize(20); textAlign(CENTER, CENTER); textStyle(BOLD);
  text(label, width/2, y + bh/2);
  textStyle(NORMAL);
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Button hit box registration activeButtons.push({x: bx, y: y, w: bw, h: bh, action: onClick});

Records this button's position and action so clicks can be detected later

calculation Hover state detection let iX = mouseX; let iY = mouseY; if (touches.length > 0) { iX = touches[0].x; iY = touches[0].y; } let isHover = (iX > bx && iX < bx + bw && iY > y && iY < y + bh);

Checks if the cursor (or touch) is over the button to enable hover feedback

let bw = 350; let bh = 50;
Sets the button width to 350 pixels and height to 50 pixels
let bx = width/2 - bw/2;
Calculates the left edge x-coordinate to center the button horizontally on screen
activeButtons.push({x: bx, y: y, w: bw, h: bh, action: onClick});
Registers this button in the activeButtons array so checkMenuClicks() can detect clicks on it
let iX = mouseX; let iY = mouseY;
Gets the current mouse position
if (touches.length > 0) { iX = touches[0].x; iY = touches[0].y; }
If touch input is available (mobile), overrides with the first touch position for cross-platform support
let isHover = (iX > bx && iX < bx + bw && iY > y && iY < y + bh);
Checks if the cursor/touch is inside the button's bounding box
fill(isHover ? 200 : 255);
Sets fill color to darker gray (200) on hover, white (255) normally—visual feedback
rect(bx, y, bw, bh, 8);
Draws the button rectangle with 8-pixel rounded corners
fill(0); textSize(20); textAlign(CENTER, CENTER); textStyle(BOLD);
Sets text properties: black color, 20pt size, centered, bold
text(label, width/2, y + bh/2);
Draws the button label text centered both horizontally and vertically on the button

checkMenuClicks(x, y)

checkMenuClicks() implements hit detection for all registered buttons. It's called from mousePressed() and touchStarted(). The loop returns early on the first match because buttons are typically drawn in order and the topmost button should be clickable.

// --- INPUTS ---
function checkMenuClicks(x, y) {
  for (let b of activeButtons) {
    if (x > b.x && x < b.x + b.w && y > b.y && y < b.y + b.h) {
      b.action();
      return true;
    }
  }
  return false;
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Button collision check for (let b of activeButtons) { if (x > b.x && x < b.x + b.w && y > b.y && y < b.y + b.h) { b.action(); return true; } }

Iterates through all registered buttons and triggers the first one that contains the click point

for (let b of activeButtons) {
Loops through every button that was registered in the current frame
if (x > b.x && x < b.x + b.w && y > b.y && y < b.y + b.h) {
Checks if the click position (x, y) falls inside this button's bounding box
b.action();
If the click is inside the button, calls the button's action function (passed when drawButton was called)
return true;
Returns true to indicate a click was handled, so other code knows not to process it further
return false;
If no button was clicked, returns false

mousePressed()

mousePressed() is a p5.js callback that fires whenever the mouse is clicked. It handles both menu clicks and the in-game mobile button.

function mousePressed() {
  if (checkMenuClicks(mouseX, mouseY)) return;
  if (gameState === "PLAYING" && dist(mouseX, mouseY, width - 60, height - 60) < 40) toggleVehicle();
}
Line-by-line explanation (2 lines)
if (checkMenuClicks(mouseX, mouseY)) return;
First checks if the click landed on a menu button; if so, handles it and exits early
if (gameState === "PLAYING" && dist(mouseX, mouseY, width - 60, height - 60) < 40) toggleVehicle();
If in gameplay and the click is within 40 pixels of the bottom-right corner (the mobile Enter/Exit button), toggles the vehicle state

touchStarted()

touchStarted() is a p5.js callback for touch input (mobile). It mirrors mousePressed() but uses the touches array. Returning false prevents default touch gestures from interfering with the game.

function touchStarted() {
  let tx = touches.length > 0 ? touches[0].x : mouseX;
  let ty = touches.length > 0 ? touches[0].y : mouseY;
  if (checkMenuClicks(tx, ty)) return false; 
  
  if (gameState === "PLAYING" && dist(tx, ty, width - 60, height - 60) < 40) toggleVehicle();
  return false; 
}
Line-by-line explanation (5 lines)
let tx = touches.length > 0 ? touches[0].x : mouseX;
Gets the touch x-coordinate if available, otherwise falls back to the mouse position (for testing on desktop)
let ty = touches.length > 0 ? touches[0].y : mouseY;
Gets the touch y-coordinate with the same fallback
if (checkMenuClicks(tx, ty)) return false;
Checks for menu button clicks and returns false to prevent default touch behavior
if (gameState === "PLAYING" && dist(tx, ty, width - 60, height - 60) < 40) toggleVehicle();
If in gameplay and the touch is on the Enter/Exit button, toggles the vehicle
return false;
Returns false to prevent the browser's default touch behavior (like scroll)

keyPressed()

keyPressed() is a p5.js callback for keyboard input. It allows the player to press ENTER to enter/exit the car, complementing the mobile button and mouse click.

function keyPressed() { 
  if (gameState === "PLAYING" && (keyCode === ENTER || key === 'Enter')) toggleVehicle(); 
}
Line-by-line explanation (1 lines)
if (gameState === "PLAYING" && (keyCode === ENTER || key === 'Enter')) toggleVehicle();
If in gameplay and the ENTER key is pressed, toggles between being in and out of the car

toggleVehicle()

toggleVehicle() handles the core vehicle entry/exit logic. When exiting, it uses trigonometry to position the player to the side of the car (offset perpendicular to the car's facing angle). This prevents the player from exiting directly into a building or behind the car.

function toggleVehicle() {
  if (person.inCar) {
    person.inCar = false; 
    person.pos.x = car.pos.x + 30 * cos(car.angle - HALF_PI); 
    person.pos.y = car.pos.y + 30 * sin(car.angle - HALF_PI);
  } else if (dist(person.pos.x, person.pos.y, car.pos.x, car.pos.y) < 60 && car.health > 0) {
    person.inCar = true;
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Exit car logic if (person.inCar) { person.inCar = false; person.pos.x = car.pos.x + 30 * cos(car.angle - HALF_PI); person.pos.y = car.pos.y + 30 * sin(car.angle - HALF_PI); }

Moves the player outside the car in the direction the car is facing

conditional Enter car logic else if (dist(person.pos.x, person.pos.y, car.pos.x, car.pos.y) < 60 && car.health > 0) { person.inCar = true; }

Allows entering the car only if the player is near it and the car is not destroyed

if (person.inCar) {
Checks if the player is currently in the car
person.inCar = false;
Sets the flag to false, marking the player as outside the car
person.pos.x = car.pos.x + 30 * cos(car.angle - HALF_PI);
Positions the player 30 pixels perpendicular to the car's facing direction (offset to the side)
person.pos.y = car.pos.y + 30 * sin(car.angle - HALF_PI);
Calculates the y-position using sine for the perpendicular offset
} else if (dist(person.pos.x, person.pos.y, car.pos.x, car.pos.y) < 60 && car.health > 0) {
If the player is not in the car but is within 60 pixels of it AND the car is not destroyed, allows entering
person.inCar = true;
Sets the flag to true, marking the player as inside the car

Person class

The Person class handles player movement on foot. It supports three input methods: WASD keys, arrow keys, and mouse/touch clicking. The normalization step is crucial: without it, diagonal movement would be faster than cardinal (moving right = 1px, but right+down = 1.41px). Collision detection prevents clipping into buildings.

🔬 This block moves the player and checks collisions. What happens if you remove the collision loop and always update position (set col to false permanently)? Can you walk through buildings?

    if (dx !== 0 || dy !== 0) {
      let moveVec = createVector(dx, dy).normalize().mult(this.speed);
      let nextX = this.pos.x + moveVec.x; let nextY = this.pos.y + moveVec.y;
      let col = false;
      for (let b of buildings) {
        if (abs(nextX - b.x) < b.w/2 + 10 && abs(nextY - b.y) < b.h/2 + 10) { col = true; break; }
      }
      if (!col) { this.pos.x = nextX; this.pos.y = nextY; }
class Person {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.speed = 4.5;
    this.angle = 0;
    this.inCar = false;
  }
  update() {
    if (this.inCar) { this.pos.x = car.pos.x; this.pos.y = car.pos.y; return; }
    
    let dx = 0, dy = 0;
    
    // WASD & Arrow Key Movement
    let up = keyIsDown(87) || keyIsDown(UP_ARROW);
    let down = keyIsDown(83) || keyIsDown(DOWN_ARROW);
    let left = keyIsDown(65) || keyIsDown(LEFT_ARROW);
    let right = keyIsDown(68) || keyIsDown(RIGHT_ARROW);

    if (up) dy -= 1;
    if (down) dy += 1;
    if (left) dx -= 1;
    if (right) dx += 1;
    
    // Mouse override only if keys aren't pressed
    if (dx === 0 && dy === 0) {
      if (mouseIsPressed || touches.length > 0) {
        let iX = touches.length > 0 ? touches[0].x : mouseX;
        let iY = touches.length > 0 ? touches[0].y : mouseY;
        if (dist(iX, iY, width - 60, height - 60) > 40) {
          let tX = iX - width/2 + this.pos.x;
          let tY = iY - height/2 + this.pos.y;
          if (dist(this.pos.x, this.pos.y, tX, tY) > 10) {
            let a = atan2(tY - this.pos.y, tX - this.pos.x);
            dx = cos(a); dy = sin(a);
          }
        }
      }
    }
    
    if (dx !== 0 || dy !== 0) {
      let moveVec = createVector(dx, dy).normalize().mult(this.speed);
      let nextX = this.pos.x + moveVec.x; let nextY = this.pos.y + moveVec.y;
      let col = false;
      for (let b of buildings) {
        if (abs(nextX - b.x) < b.w/2 + 10 && abs(nextY - b.y) < b.h/2 + 10) { col = true; break; }
      }
      if (!col) { this.pos.x = nextX; this.pos.y = nextY; }
      this.angle = atan2(dy, dx);
    }
  }
  draw() {
    if (this.inCar) return;
    push(); translate(this.pos.x, this.pos.y); rotate(this.angle); rectMode(CENTER);
    fill(50, 100, 200); rect(0, 0, 14, 24, 5); fill(255, 200, 150); circle(2, 0, 14); pop();
  }
}
Line-by-line explanation (24 lines)

🔧 Subcomponents:

function-call Person initialization constructor(x, y) { this.pos = createVector(x, y); this.speed = 4.5; this.angle = 0; this.inCar = false; }

Creates a person object with position, speed, facing angle, and in-car status

conditional In-car position lock if (this.inCar) { this.pos.x = car.pos.x; this.pos.y = car.pos.y; return; }

If in the car, snaps the player position to the car's position and skips all other movement logic

calculation WASD and arrow key input let up = keyIsDown(87) || keyIsDown(UP_ARROW); ... if (up) dy -= 1; ...

Detects which direction keys are pressed and updates movement vector

conditional Touch/mouse movement fallback if (dx === 0 && dy === 0) { if (mouseIsPressed || touches.length > 0) { ... } }

If no keys are pressed, allows clicking/touching to move the player (mobile support)

calculation Vector-based movement let moveVec = createVector(dx, dy).normalize().mult(this.speed); ... this.pos.x = nextX; this.pos.y = nextY;

Normalizes the movement direction and applies it at constant speed

for-loop Building collision detection for (let b of buildings) { if (abs(nextX - b.x) < b.w/2 + 10 && abs(nextY - b.y) < b.h/2 + 10) { col = true; break; } }

Prevents the player from walking through buildings

constructor(x, y) {
Defines the constructor that runs when a new Person is created
this.pos = createVector(x, y);
Creates a p5.Vector to store the person's 2D position
this.speed = 4.5;
Sets the person's walking speed in pixels per frame
this.angle = 0;
Initializes the facing angle to 0 (pointing right)
this.inCar = false;
Starts the person on foot, not in the car
if (this.inCar) { this.pos.x = car.pos.x; this.pos.y = car.pos.y; return; }
If in the car, locks the player to the car's position (passenger) and skips all other logic
let dx = 0, dy = 0;
Initializes movement direction variables; 0 means no input
let up = keyIsDown(87) || keyIsDown(UP_ARROW);
Checks if the W key (87) or up arrow is currently held down
if (up) dy -= 1;
If up is pressed, reduces dy (negative y = upward movement in p5.js)
if (dx === 0 && dy === 0) { if (mouseIsPressed || touches.length > 0) { ... } }
If no keys are pressed and the mouse/touch is active, use the cursor position to determine movement direction
let iX = touches.length > 0 ? touches[0].x : mouseX;
Gets the input position from touch or mouse, supporting both input methods
if (dist(iX, iY, width - 60, height - 60) > 40) {
Ignores clicks on the mobile button (bottom-right corner)
let tX = iX - width/2 + this.pos.x;
Converts screen-space click coordinates to world-space coordinates (accounting for camera)
if (dist(this.pos.x, this.pos.y, tX, tY) > 10) {
Only moves if the target is at least 10 pixels away (prevents jitter from small clicks)
let a = atan2(tY - this.pos.y, tX - this.pos.x);
Calculates the angle from the player toward the clicked location
dx = cos(a); dy = sin(a);
Converts the angle to a direction vector (cos for x, sin for y)
let moveVec = createVector(dx, dy).normalize().mult(this.speed);
Creates a movement vector: normalizes it to unit length, then scales by speed so diagonal movement isn't faster than cardinal
let nextX = this.pos.x + moveVec.x; let nextY = this.pos.y + moveVec.y;
Calculates where the player would be after moving
for (let b of buildings) { if (abs(nextX - b.x) < b.w/2 + 10 && abs(nextY - b.y) < b.h/2 + 10) { col = true; break; } }
Loops through buildings to check if the next position would be inside any of them
if (!col) { this.pos.x = nextX; this.pos.y = nextY; }
Only updates position if no collision was detected
this.angle = atan2(dy, dx);
Updates the facing angle to point in the direction of movement
push(); translate(this.pos.x, this.pos.y); rotate(this.angle); rectMode(CENTER);
Saves the transform state, moves to the player's position, rotates by their angle, and sets rect drawing mode to CENTER
fill(50, 100, 200); rect(0, 0, 14, 24, 5);
Draws the player's body as a blue rounded rectangle
fill(255, 200, 150); circle(2, 0, 14);
Draws the player's head as a peach-colored circle, offset forward

CopPerson class

CopPerson is an AI enemy that pursues and tackles the player on foot. The tackle mechanic is a mini state machine: normally chasing (continuous angle update), then briefly lunging (fixed angle, fast speed). After 12 frames the lunge ends. This creates a dynamic threat without complex pathfinding.

🔬 This block triggers a tackle when the player is within 80 pixels. What happens if you change 80 to 200—do cops tackle from farther away, making the game harder?

    // TACKLE MECHANIC: Trigger tackle if close enough and not already tackling
    if (!person.inCar && d < 80 && !this.tackling) {
      this.tackling = true;
      this.tackleFrames = 0;
      // Lock onto current angle for the dive
      this.angle = atan2(person.pos.y - this.pos.y, person.pos.x - this.pos.x); 
    }
class CopPerson {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.speed = 4.2;
    this.angle = 0;
    this.tackling = false;
    this.tackleFrames = 0;
  }
  update() {
    let d = dist(this.pos.x, this.pos.y, person.pos.x, person.pos.y);
    
    // TACKLE MECHANIC: Trigger tackle if close enough and not already tackling
    if (!person.inCar && d < 80 && !this.tackling) {
      this.tackling = true;
      this.tackleFrames = 0;
      // Lock onto current angle for the dive
      this.angle = atan2(person.pos.y - this.pos.y, person.pos.x - this.pos.x); 
    }
    
    if (this.tackling) {
      this.tackleFrames++;
      this.speed = 10; // Fast lunge speed!
      
      if (this.tackleFrames > 12) {
        this.tackling = false; // Finished lunge
        this.speed = 4.2;
      }
    } else {
      // Track player continuously if NOT tackling
      this.angle = atan2(person.pos.y - this.pos.y, person.pos.x - this.pos.x);
    }
    
    let nextX = this.pos.x + cos(this.angle) * this.speed;
    let nextY = this.pos.y + sin(this.angle) * this.speed;
    
    let col = false;
    for (let b of buildings) {
      if (abs(nextX - b.x) < b.w/2 + 10 && abs(nextY - b.y) < b.h/2 + 10) { col = true; break; }
    }
    if (!col) { this.pos.x = nextX; this.pos.y = nextY; }
    
    // Check if touched player (Busted)
    if (!person.inCar && d < 25) {
      if (isInstantRespawn) needsRespawn = true;
      else gameState = "BUSTED";
    }
  }
  draw() {
    push(); translate(this.pos.x, this.pos.y); rotate(this.angle); rectMode(CENTER);
    
    if (this.tackling) {
      // Diving / Tackling Animation
      fill(20, 20, 50); rect(5, 0, 26, 14, 5); // Stretched body leaning forward
      fill(255, 200, 150); circle(16, 0, 14); // Head forward
      
      // Arms reaching out
      fill(255, 200, 150);
      circle(20, -10, 8);
      circle(20, 10, 8);
    } else {
      // Normal running animation
      fill(20, 20, 50); rect(0, 0, 14, 24, 5); 
      fill(255, 200, 150); circle(2, 0, 14); 
    }
    pop();
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Tackle initiation if (!person.inCar && d < 80 && !this.tackling) { this.tackling = true; this.tackleFrames = 0; this.angle = atan2(person.pos.y - this.pos.y, person.pos.x - this.pos.x); }

Starts a tackle dive when the cop is within 80 pixels of the player and not already diving

conditional Tackle lunge duration if (this.tackling) { this.tackleFrames++; this.speed = 10; if (this.tackleFrames > 12) { this.tackling = false; this.speed = 4.2; } }

Runs a 12-frame lunge, then returns to normal speed

conditional Continuous angle tracking or locked lunge } else { this.angle = atan2(person.pos.y - this.pos.y, person.pos.x - this.pos.x); }

Normally faces the player; during a tackle, keeps the locked angle for a committed dive

let d = dist(this.pos.x, this.pos.y, person.pos.x, person.pos.y);
Calculates the distance from the cop to the player
if (!person.inCar && d < 80 && !this.tackling) {
If the player is on foot, within 80 pixels, and the cop isn't already tackling, start a tackle
this.tackling = true;
Sets the tackling flag to true
this.tackleFrames = 0;
Resets the frame counter for the tackle duration
this.angle = atan2(person.pos.y - this.pos.y, person.pos.x - this.pos.x);
Locks onto the player's current position so the dive is committed (doesn't redirect mid-lunge)
if (this.tackling) { this.tackleFrames++; this.speed = 10;
While tackling, increment the frame counter and set speed to 10 (fast lunge)
if (this.tackleFrames > 12) { this.tackling = false; this.speed = 4.2; }
After 12 frames of lunging, end the tackle and return to normal speed
} else { this.angle = atan2(person.pos.y - this.pos.y, person.pos.x - this.pos.x); }
When not tackling, continuously update angle to face the player (intelligent tracking)
let nextX = this.pos.x + cos(this.angle) * this.speed;
Calculates the next position using the angle and speed (polar to Cartesian conversion)
if (!person.inCar && d < 25) { if (isInstantRespawn) needsRespawn = true; else gameState = "BUSTED"; }
If the cop touches the player (within 25 pixels), the player is busted or instantly respawned depending on settings

Car class

The Car class handles physics, input, collision, and damage. It uses speed clamping and friction for realistic deceleration. Mouse steering uses angle interpolation (atan2 of trigonometric functions) to smoothly turn toward the target. The health system is simple: take damage proportional to impact speed, and destroy the car at zero health. Smoke puffs on low health add visual feedback.

🔬 This block steers the car; note the Math.sign() which makes reverse steering opposite. What happens if you remove Math.sign() so ts is always positive—does the car steer the same way in reverse, or opposite?

      if (left || right) {
        if (abs(this.speed) > 0.5) {
          let ts = 0.06 * Math.sign(this.speed);
          if (left) this.angle -= ts;
          if (right) this.angle += ts;
        }
      }
class Car {
  constructor(x, y) {
    this.pos = createVector(x, y); this.vel = createVector(0, 0); 
    this.angle = 0; this.speed = 0; this.health = 100;
  }
  update() {
    if (this.health <= 0) return;
    
    let acc = false;
    if (person.inCar) {
      let up = keyIsDown(87) || keyIsDown(UP_ARROW);
      let down = keyIsDown(83) || keyIsDown(DOWN_ARROW);
      let left = keyIsDown(65) || keyIsDown(LEFT_ARROW);
      let right = keyIsDown(68) || keyIsDown(RIGHT_ARROW);

      if (up) { this.speed += 0.3; acc = true; }
      if (down) { this.speed -= 0.3; acc = true; }
      
      let usingMouseSteer = false;
      let iX = touches.length > 0 ? touches[0].x : mouseX;
      let iY = touches.length > 0 ? touches[0].y : mouseY;
      if ((mouseIsPressed || touches.length > 0) && dist(iX, iY, width - 60, height - 60) > 40) {
        usingMouseSteer = true;
      }
      
      if (left || right) {
        if (abs(this.speed) > 0.5) {
          let ts = 0.06 * Math.sign(this.speed);
          if (left) this.angle -= ts;
          if (right) this.angle += ts;
        }
      } else if (usingMouseSteer) {
        let tAngle = atan2(iY - height/2 + this.pos.y - this.pos.y, iX - width/2 + this.pos.x - this.pos.x);
        let aDiff = atan2(sin(tAngle - this.angle), cos(tAngle - this.angle));
        this.angle += aDiff * 0.08; this.speed += 0.3; acc = true;
      }
    }
    
    if (!acc) this.speed *= 0.92;
    this.speed = constrain(this.speed, -5, 18);
    this.vel.x = cos(this.angle) * this.speed; this.vel.y = sin(this.angle) * this.speed;
    this.pos.add(this.vel);
    
    for(let b of buildings) {
      if(abs(this.pos.x - b.x) < b.w/2 + 15 && abs(this.pos.y - b.y) < b.h/2 + 15) {
        if(abs(this.speed) > 5) this.health -= abs(this.speed); 
        this.pos.sub(this.vel); this.speed *= -0.5;
      }
    }
    
    if (this.health <= 0 && gameState !== "DESTROYED") {
      createExplosion(this.pos.x, this.pos.y);
      if (isInstantRespawn) needsRespawn = true;
      else gameState = "DESTROYED";
    }
  }
  draw() {
    push(); translate(this.pos.x, this.pos.y); rotate(this.angle); rectMode(CENTER);
    fill(0, 50); rect(5, 5, 40, 20, 5); 
    
    let cRed = map(this.health, 0, 100, 50, 220);
    fill(cRed, 30, 30); rect(0, 0, 40, 20, 5); 
    fill(15); rect(2, 0, 20, 16, 3);
    fill(255, 255, 200); rect(18, -7, 4, 4); rect(18, 7, 4, 4);
    fill(255, 0, 0); rect(-18, -7, 3, 4); rect(-18, 7, 3, 4);
    
    if (this.health < 40 && random() > 0.5) {
      fill(100, 100); noStroke(); circle(random(-10, 10), random(-10, 10), random(10, 20));
    }
    pop();
  }
}
Line-by-line explanation (20 lines)

🔧 Subcomponents:

conditional Throttle and reverse input if (person.inCar) { ... if (up) { this.speed += 0.3; acc = true; } if (down) { this.speed -= 0.3; acc = true; } ... }

Only applies throttle when the player is in the car and holding up/down keys

conditional Left/right steering or mouse aiming if (left || right) { ... } else if (usingMouseSteer) { ... }

Allows steering with arrow keys or mouse/touch aiming

calculation Friction and speed clamping if (!acc) this.speed *= 0.92; this.speed = constrain(this.speed, -5, 18);

Slows the car when not accelerating and caps max speed

for-loop Collision with buildings and damage for(let b of buildings) { if(abs(this.pos.x - b.x) < b.w/2 + 15 && abs(this.pos.y - b.y) < b.h/2 + 15) { ... this.health -= abs(this.speed); ... } }

Detects collisions and damages the car based on impact speed

conditional Car destruction trigger if (this.health <= 0 && gameState !== "DESTROYED") { createExplosion(this.pos.x, this.pos.y); ... gameState = "DESTROYED"; }

When health reaches zero, triggers an explosion and the game-over state

constructor(x, y) { this.pos = createVector(x, y); this.vel = createVector(0, 0); this.angle = 0; this.speed = 0; this.health = 100; }
Initializes the car at (x, y) with zero velocity, angle, and speed, and 100 health points
if (this.health <= 0) return;
If the car is destroyed, skips all update logic (dead car doesn't move or respond to input)
let up = keyIsDown(87) || keyIsDown(UP_ARROW);
Checks if W (87) or the up arrow is held down (W = 87 in ASCII)
if (up) { this.speed += 0.3; acc = true; }
If forward is pressed, accelerates by 0.3 per frame and marks that acceleration happened
let iX = touches.length > 0 ? touches[0].x : mouseX;
Gets steering input from mouse or first touch point
if ((mouseIsPressed || touches.length > 0) && dist(iX, iY, width - 60, height - 60) > 40) { usingMouseSteer = true; }
If the mouse/touch is active and NOT on the mobile button, enables mouse steering
if (left || right) { if (abs(this.speed) > 0.5) { let ts = 0.06 * Math.sign(this.speed); if (left) this.angle -= ts; if (right) this.angle += ts; } }
If left/right keys are pressed and the car is moving fast enough, turns the car; ts is scaled by speed direction so reversing turns the opposite way
let tAngle = atan2(iY - height/2 + this.pos.y - this.pos.y, iX - width/2 + this.pos.x - this.pos.x);
Calculates the angle from the car toward the mouse/touch position (note: the pos.y - pos.y term is redundant but harmless)
let aDiff = atan2(sin(tAngle - this.angle), cos(tAngle - this.angle));
Calculates the shortest angular difference using atan2 (a standard technique for angle interpolation)
this.angle += aDiff * 0.08;
Smoothly rotates toward the target angle by adding 8% of the difference each frame
if (!acc) this.speed *= 0.92;
If the player isn't pressing accelerate, reduces speed by multiplying by 0.92 (natural deceleration/friction)
this.speed = constrain(this.speed, -5, 18);
Clamps speed to a range: -5 (max reverse) to 18 (max forward) so it doesn't accelerate forever
this.vel.x = cos(this.angle) * this.speed; this.vel.y = sin(this.angle) * this.speed;
Converts the angle and speed to a velocity vector using trigonometry
this.pos.add(this.vel);
Updates the car's position by adding the velocity (movement)
if(abs(this.pos.x - b.x) < b.w/2 + 15 && abs(this.pos.y - b.y) < b.h/2 + 15) {
Checks if the car overlaps a building's bounding box
if(abs(this.speed) > 5) this.health -= abs(this.speed);
Damages the car by the impact speed (only if speeding above 5); slower crashes do no damage
this.pos.sub(this.vel); this.speed *= -0.5;
Pushes the car back out of the building and reverses/reduces its speed (bounce effect)
let cRed = map(this.health, 0, 100, 50, 220);
Maps the car's health (0–100) to a red channel value (50–220), making damaged cars appear more red
fill(cRed, 30, 30); rect(0, 0, 40, 20, 5);
Draws the car's body with a red tint proportional to damage
if (this.health < 40 && random() > 0.5) { fill(100, 100); noStroke(); circle(random(-10, 10), random(-10, 10), random(10, 20)); }
When damaged below 40% health, randomly draws smoke puffs on the car for visual feedback

CopCar class

CopCar is the primary cop AI. It pursues the player's last known position and can eject a CopPerson if the player abandons the car. The car-to-car collision is sophisticated: it calculates impact damage and transfers momentum, creating dynamic chases. The blinking lights (frameCount-based) add visual polish without extra code.

class CopCar {
  constructor(x, y) {
    this.pos = createVector(x, y); this.vel = createVector(0, 0); this.angle = 0; this.speed = 0;
    this.maxSpeed = random(16, 20);
    this.hasCop = true;
  }
  update(targetPos) {
    if (!this.hasCop) {
      this.speed *= 0.9;
      this.pos.add(cos(this.angle) * this.speed, sin(this.angle) * this.speed);
      return;
    }
    
    if (!person.inCar) {
      let d = dist(this.pos.x, this.pos.y, person.pos.x, person.pos.y);
      if (d < 150) {
        if (abs(this.speed) > 2) {
          this.speed *= 0.85;
        } else {
          this.hasCop = false;
          copPeople.push(new CopPerson(this.pos.x, this.pos.y));
          return;
        }
      }
    }

    let tAngle = atan2(targetPos.y - this.pos.y, targetPos.x - this.pos.x);
    let aDiff = atan2(sin(tAngle - this.angle), cos(tAngle - this.angle));
    this.angle += aDiff * 0.06; this.speed += 0.4;
    this.speed = constrain(this.speed, -5, this.maxSpeed);
    this.vel.x = cos(this.angle) * this.speed; this.vel.y = sin(this.angle) * this.speed;
    this.pos.add(this.vel);
    
    if (person.inCar && dist(this.pos.x, this.pos.y, car.pos.x, car.pos.y) < 40) {
      let impact = abs(this.speed) + abs(car.speed);
      car.health -= impact * 0.4; 
      car.speed += this.speed * 0.5; 
      this.speed *= -0.5; 
      this.pos.sub(this.vel);
    }
    
    if (!person.inCar && dist(this.pos.x, this.pos.y, person.pos.x, person.pos.y) < 25) {
      if (isInstantRespawn) needsRespawn = true;
      else gameState = "BUSTED";
    }

    for(let b of buildings) {
      if(abs(this.pos.x - b.x) < b.w/2 + 15 && abs(this.pos.y - b.y) < b.h/2 + 15) {
        this.pos.sub(this.vel); this.speed *= -0.5; 
      }
    }
  }
  draw() {
    push(); translate(this.pos.x, this.pos.y); rotate(this.angle); rectMode(CENTER);
    fill(0, 50); rect(5, 5, 40, 20, 5); 
    fill(255); rect(0, 0, 40, 20, 5); fill(20); rect(0, 0, 20, 20); fill(15); rect(2, 0, 20, 16, 3);
    if (this.hasCop && frameCount % 16 < 8) {
      fill(255, 0, 0); rect(-2, -5, 6, 4); fill(50, 50, 100); rect(-2, 5, 6, 4);
    } else if (this.hasCop) {
      fill(100, 50, 50); rect(-2, -5, 6, 4); fill(0, 0, 255); rect(-2, 5, 6, 4);
    }
    pop();
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

conditional Cop abandoning car if (!this.hasCop) { this.speed *= 0.9; this.pos.add(cos(this.angle) * this.speed, sin(this.angle) * this.speed); return; }

When the cop has exited, the car drifts slowly to a stop

conditional Cop exits car if player is close on foot if (!person.inCar) { let d = dist(...); if (d < 150) { if (abs(this.speed) > 2) { ... } else { this.hasCop = false; copPeople.push(...); return; } } }

When the player is on foot and within 150 pixels, the cop slows down and eventually exits to chase on foot

conditional Collision with player car if (person.inCar && dist(this.pos.x, this.pos.y, car.pos.x, car.pos.y) < 40) { let impact = ...; car.health -= impact * 0.4; ... }

When hitting the player's car, damages it and exchanges momentum

conditional Police light blinking if (this.hasCop && frameCount % 16 < 8) { fill(255, 0, 0); ... } else if (this.hasCop) { fill(100, 50, 50); ... }

Makes the cop car's lights blink red and blue

constructor(x, y) { this.pos = createVector(x, y); this.vel = createVector(0, 0); this.angle = 0; this.speed = 0; this.maxSpeed = random(16, 20); this.hasCop = true; }
Initializes a cop car at (x, y) with random max speed between 16 and 20, and marks that it has a cop inside
if (!this.hasCop) { this.speed *= 0.9; this.pos.add(cos(this.angle) * this.speed, sin(this.angle) * this.speed); return; }
If the cop has exited the car (pursuing on foot), the car drifts at 90% of its current speed per frame until it stops
if (!person.inCar) { let d = dist(this.pos.x, this.pos.y, person.pos.x, person.pos.y);
If the player is on foot, checks the distance from the cop car to the player
if (d < 150) { if (abs(this.speed) > 2) { this.speed *= 0.85; } else { this.hasCop = false; copPeople.push(new CopPerson(this.pos.x, this.pos.y)); return; } }
If within 150 pixels: if moving, slow down (85% of speed per frame); if nearly stopped, eject the cop to chase on foot and exit early
let tAngle = atan2(targetPos.y - this.pos.y, targetPos.x - this.pos.x);
Calculates the angle toward the target position (the camera/player)
let aDiff = atan2(sin(tAngle - this.angle), cos(tAngle - this.angle));
Calculates the shortest angular difference for smooth turning
this.angle += aDiff * 0.06; this.speed += 0.4;
Turns toward the target (6% of angle difference per frame) and accelerates (0.4 per frame)
this.speed = constrain(this.speed, -5, this.maxSpeed);
Clamps speed to the car's random max speed (16–20)
if (person.inCar && dist(this.pos.x, this.pos.y, car.pos.x, car.pos.y) < 40) { let impact = abs(this.speed) + abs(car.speed); car.health -= impact * 0.4; car.speed += this.speed * 0.5; this.speed *= -0.5; this.pos.sub(this.vel); }
On collision with the player's car: damages the player's car by 40% of combined impact speed, transfers momentum to the player, and bounces the cop car backward
if (!person.inCar && dist(this.pos.x, this.pos.y, person.pos.x, person.pos.y) < 25) { if (isInstantRespawn) needsRespawn = true; else gameState = "BUSTED"; }
If the cop car hits the player on foot, triggers a bust or respawn
if(abs(this.pos.x - b.x) < b.w/2 + 15 && abs(this.pos.y - b.y) < b.h/2 + 15) { this.pos.sub(this.vel); this.speed *= -0.5; }
Bounces off buildings by undoing movement and reversing/reducing speed
if (this.hasCop && frameCount % 16 < 8) { fill(255, 0, 0); rect(-2, -5, 6, 4); fill(50, 50, 100); rect(-2, 5, 6, 4); }
When the cop is present: every 16 frames, the first 8 show red and blue lights (bright colors); the next 8 show them dimmed

windowResized()

windowResized() is a p5.js callback that fires whenever the browser window is resized. It keeps the canvas fullscreen and clears stale button data.

function windowResized() { activeButtons = []; resizeCanvas(windowWidth, windowHeight); }
Line-by-line explanation (2 lines)
activeButtons = [];
Clears the active buttons array so old button positions don't carry over after resize
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new window dimensions

📦 Key Variables

person object (Person instance)

Stores the player character; has position, speed, angle, and inCar status

let person = new Person(0, 50);
car object (Car instance)

Stores the player's vehicle; has position, velocity, angle, speed, and health

let car = new Car(0, 0);
cops array of CopCar objects

Stores all active cop cars pursuing the player

let cops = [];
copPeople array of CopPerson objects

Stores all cop pedestrians on foot (ejected from cop cars)

let copPeople = [];
buildings array of objects

Stores all building data (position, size, colors) for the procedurally generated city

let buildings = [];
isFreePlay boolean

When true, no cops spawn; when false, survival mode is active with spawning cops

let isFreePlay = true;
isInstantRespawn boolean

When true, the game resets immediately on busting/destruction; when false, shows game-over screen

let isInstantRespawn = false;
needsRespawn boolean

Flag set to true when respawn should happen; checked in the draw loop to trigger resetGame()

let needsRespawn = false;
camX number

The x-coordinate of the camera (either person or car position)

let camX = 0;
camY number

The y-coordinate of the camera (either person or car position)

let camY = 0;
gameState string

Tracks the current game state: MENU, PLAYING, SETTINGS, CREDITS, BUSTED, or DESTROYED

let gameState = "MENU";
survivalFrames number

Counter that increments every frame during gameplay; used to calculate survival time and spawn cops

let survivalFrames = 0;
explosionParticles array of objects

Stores all active explosion particles (fire and smoke) when the car is destroyed

let explosionParticles = [];
activeButtons array of objects

Stores clickable button regions (x, y, width, height, action) for hit detection; cleared every frame

let activeButtons = [];
bgImage p5.Image or undefined

Stores the loaded background image for menu screens

let bgImage; // loaded asynchronously in setup()

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG Car.update() line: let tAngle = atan2(iY - height/2 + this.pos.y - this.pos.y, ...)

The y-coordinate calculation has a redundant (this.pos.y - this.pos.y) which equals 0, making mouse steering unreliable. This looks like a typo.

💡 Change to: let tAngle = atan2(iY - height/2 + this.pos.y, iX - width/2 + this.pos.x); to properly convert screen-space cursor to world-space angle

PERFORMANCE draw() function

Every frame, the code checks all 500+ buildings for collision with the player and car, even buildings far off-screen. This is wasteful.

💡 Before collision checks, skip buildings outside a certain radius of the player using dist() or a spatial grid, as done for rendering culling

STYLE checkMenuClicks() and drawButton()

Button hit detection logic is duplicated: drawButton() calculates bounds and drawButton also does it. This makes button changes fragile.

💡 Create a dedicated Button class that handles both rendering and hit detection, eliminating duplication and improving maintainability

FEATURE Game overall

There is no scoring or leaderboard; players have no persistent metric of success beyond survival time

💡 Add a score variable that increments for surviving, defeating cops, or reaching specific locations; display it on the HUD and save high scores to localStorage

BUG CopPerson.update() and Person.update()

Both use the same collision box size (+10 pixels) for buildings, but there is no guarantee the player and cops have the same collision footprint—one could clip through buildings the other can't

💡 Define collision margins as class properties or constants so different entity types can have different collision sizes

🔄 Code Flow

Code flow showing setup, resetgame, spawnccocar, createexplosion, drawexplosion, draw, drawui, drawoverlay, drawtitle, drawbutton, checkmenuc licks, mousepressed, touchstarted, keypressed, togglevehicle, person, copperson, car, copcar, windowresized

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

graph TD start[Start] --> setup[setup] setup --> canvas-setup[canvas-setup] setup --> image-load[image-load] setup --> city-grid-loop[city-grid-loop] setup --> entity-init[entity-init] setup --> initial-cop-spawn[initial-cop-spawn] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-setup href "#sub-canvas-setup" click image-load href "#sub-image-load" click city-grid-loop href "#sub-city-grid-loop" click entity-init href "#sub-entity-init" click initial-cop-spawn href "#sub-initial-cop-spawn" draw --> menu-check[menu-check] draw --> camera-system[camera-system] draw --> road-grid[road-grid] draw --> building-culling[building-culling] draw --> game-logic[game-logic] draw --> entity-rendering[entity-rendering] draw --> drawui[drawui] draw --> drawoverlay[drawoverlay] draw --> drawtitle[drawtitle] draw --> drawbutton[drawbutton] click draw href "#fn-draw" click menu-check href "#sub-menu-check" click camera-system href "#sub-camera-system" click road-grid href "#sub-road-grid" click building-culling href "#sub-building-culling" click game-logic href "#sub-game-logic" click entity-rendering href "#sub-entity-rendering" click drawui href "#fn-drawui" click drawoverlay href "#fn-drawoverlay" click drawtitle href "#fn-drawtitle" click drawbutton href "#fn-drawbutton" game-logic --> array-clear[array-clear] game-logic --> resetgame[resetgame] game-logic --> spawnccocar[spawnccocar] click resetgame href "#fn-resetgame" click spawnccocar href "#fn-spawnccocar" spawnccocar --> spawn-angle-loop[spawn-angle-loop] spawnccocar --> circle-spawn[circle-spawn] spawnccocar --> building-collision-check[building-collision-check] click spawn-angle-loop href "#sub-spawn-angle-loop" click circle-spawn href "#sub-circle-spawn" click building-collision-check href "#sub-building-collision-check" createexplosion[createexplosion] --> particle-spawn-loop[particle-spawn-loop] click createexplosion href "#fn-createexplosion" particle-spawn-loop --> particle-update-loop[particle-update-loop] click particle-spawn-loop href "#sub-particle-spawn-loop" particle-update-loop --> particle-physics[particle-physics] particle-update-loop --> color-animation[color-animation] click particle-update-loop href "#sub-particle-update-loop" click particle-physics href "#sub-particle-physics" click color-animation href "#sub-color-animation" mousepressed[mousepressed] --> checkmenuclicks[checkmenuclicks] mousepressed --> exit-car[exit-car] mousepressed --> enter-car[enter-car] click mousepressed href "#fn-mousepressed" click checkmenuclicks href "#fn-checkmenuclicks" click exit-car href "#sub-exit-car" click enter-car href "#sub-enter-car" touchstarted[touchstarted] --> checkmenuclicks click touchstarted href "#fn-touchstarted" keypressed[keypressed] --> togglevehicle[togglevehicle] click keypressed href "#fn-keypressed" click togglevehicle href "#fn-togglevehicle" person[person] --> constructor[constructor] click person href "#fn-person" click constructor href "#sub-constructor" copperson[copperson] --> tackle-trigger[tackle-trigger] copperson --> tackle-duration[tackle-duration] copperson --> angle-tracking[angle-tracking] click copperson href "#fn-copperson" click tackle-trigger href "#sub-tackle-trigger" click tackle-duration href "#sub-tackle-duration" click angle-tracking href "#sub-angle-tracking" car[car] --> acceleration-input[acceleration-input] car --> steering[steering] car --> friction[friction] car --> building-collision[building-collision] car --> destruction-check[destruction-check] car --> lights-animation[lights-animation] click car href "#fn-car" click acceleration-input href "#sub-acceleration-input" click steering href "#sub-steering" click friction href "#sub-friction" click building-collision href "#sub-building-collision" click destruction-check href "#sub-destruction-check" click lights-animation href "#sub-lights-animation" copcar[copcar] --> player-pursuit-decision[player-pursuit-decision] copcar --> cop-exit[cop-exit] copcar --> car-ramming[car-ramming] click copcar href "#fn-copcar" click player-pursuit-decision href "#sub-player-pursuit-decision" click cop-exit href "#sub-cop-exit" click car-ramming href "#sub-car-ramming" windowresized[windowresized] --> canvas-setup click windowresized href "#fn-windowresized"

Preview

gta 6 current state 4 .5 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of gta 6 current state 4 .5 - Code flow showing setup, resetgame, spawnccocar, createexplosion, drawexplosion, draw, drawui, drawoverlay, drawtitle, drawbutton, checkmenuc licks, mousepressed, touchstarted, keypressed, togglevehicle, person, copperson, car, copcar, windowresized
Code Flow Diagram