gta 6 current state upd for hooda math

This is a GTA-style 3D sandbox game built with three.js where players drive a car, evade police, and survive escalating waves of cop cars and tanks. The game features a persistent chunk-based world, a star-wanted level system, collision detection, and the ability to exit the car and shoot NPCs on foot.

🧪 Try This!

Experiment with the code by making these changes:

  1. Slow down the cops — Reduce the cop car speed so they chase you more slowly and the game is easier
  2. Make your car HUGE — Scale up the car body geometry so you have a comically oversized vehicle
  3. Triple the building count — Increase buildings per chunk from 15 to 45 for a denser, more crowded city
  4. Make tanks appear earlier — Tanks normally spawn at 5 stars; change this to 2 stars so they appear much sooner
  5. Shorter collision grace period — Reduce the time you can touch cops before losing from 7 seconds to 2 seconds
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a fully playable GTA-style 3D game using three.js, p5.js for audio, and vanilla JavaScript. Players start in a car and must evade escalating waves of police cars while a star meter counts up the longer they survive. The game combines several advanced p5.js and three.js techniques: procedural chunk-based world generation, AI path-finding for enemy vehicles, raycasting for shooting mechanics, collision detection, and audio synthesis with p5.sound.

The code is organized into setup and initialization (three.js scene, camera, renderer), model creation (car, cop cars, tanks, player, gun), chunk system management (generating and unloading terrain dynamically), AI logic (cops chasing the player), and the main animation loop that ties everything together. By studying this sketch you will learn how to build a 3D game loop, manage complex state with multiple game modes (driving vs. on-foot), implement enemy AI that responds to player position, and use raycasting for interactive shooting.

⚙️ How It Works

  1. When the sketch loads, p5.js initializes and displays a main menu with Play and Credits buttons. The three.js scene, camera, and renderer are created but hidden until the player clicks Play.
  2. Once Play is clicked, startGame() initializes three.js, creates the player's car, sets up lighting, and generates the initial terrain chunks around the player's starting position.
  3. Every frame, the animate() function updates the game state: the player drives the car using WASD keys, the chunk system loads/unloads terrain as the player moves, and cop cars use AI to chase the player.
  4. As the player survives longer, the survival timer increments and triggers star milestones (1 star at 60 seconds, 2 stars at 120 seconds, etc.), spawning stronger enemies like SWAT trucks at 2+ stars and tanks at 5+ stars.
  5. Cop cars use directional vectors and angle calculations to navigate toward the player, while tanks actively shoot projectiles that end the game on collision. Players can press E to exit the car and switch to on-foot mode where they can shoot NPCs with a raycaster.
  6. The game ends immediately if the player collides with a cop car for 7 seconds or if a tank projectile hits them. The star meter, timer, and cop count update in real-time on screen.

🎓 Concepts You'll Learn

3D game loop animationChunk-based world generationAI pathfinding and chasingRaycasting for shootingCollision detectionState management (driving vs. on-foot)Procedural spawningAudio synthesis with p5.sound

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes p5.js, hides its canvas, and wires up the menu buttons. Notice that the actual three.js initialization happens later in startGame(), only after the player clicks Play.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noCanvas(); // Hide the p5.js canvas

  // Get the crosshair DOM element
  crosshairElement = document.getElementById('crosshair');
  crosshairElement.style.display = 'none'; // Initially hidden

  // Get UI elements
  starsElement = document.getElementById('starsDisplay');
  timerElement = document.getElementById('timerDisplay');
  copMeterElement = document.getElementById('copMeterDisplay');

  // Handle Main Menu Buttons
  document.getElementById('playBtn').addEventListener('click', startGame);
  document.getElementById('creditsBtn').addEventListener('click', () => {
    document.getElementById('mainMenuButtons').style.display = 'none';
    document.getElementById('creditsScreen').style.display = 'flex';
  });
  document.getElementById('backBtn').addEventListener('click', () => {
    document.getElementById('creditsScreen').style.display = 'none';
    document.getElementById('mainMenuButtons').style.display = 'flex';
  });

  // Initialize mouse coordinates for raycasting
  mouse.x = 0; // Center of screen
  mouse.y = 0; // Center of screen
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas (though it will be hidden). This is required to load p5.js properly.
noCanvas();
Hides the p5.js canvas so three.js can render directly to the screen instead
crosshairElement = document.getElementById('crosshair');
Stores a reference to the HTML crosshair element so we can show/hide it later
crosshairElement.style.display = 'none';
Hides the crosshair initially—it only appears when the player switches to on-foot mode
starsElement = document.getElementById('starsDisplay');
Gets the HTML element that displays the star meter so we can update it every frame
document.getElementById('playBtn').addEventListener('click', startGame);
When the player clicks the Play button, call startGame() to initialize three.js and begin the game

startGame()

startGame() is called when the player clicks Play on the menu. It tears down the menu UI, activates audio, and hands control to three.js by calling initThreeJS() and animate().

function startGame() {
  // Hide Menu, Show HUD
  document.getElementById('mainMenu').style.display = 'none';
  document.getElementById('uiContainer').style.display = 'flex';
  
  // Audio Context must be started after a user gesture!
  userStartAudio(); 
  
  gameStarted = true;

  // Initial UI display
  updateStarDisplay(); // Shows "Stars: 0"
  timerElement.innerHTML = `Time: ${nf(survivalTimer / 1000, 1, 0)}s`;
  copMeterElement.innerHTML = `Cops: ${copCars.length}`; 

  // --- three.js Initialization ---
  initThreeJS();
  animate(); // Start the three.js animation loop
}
Line-by-line explanation (6 lines)
document.getElementById('mainMenu').style.display = 'none';
Hides the main menu screen so the player can see the game
document.getElementById('uiContainer').style.display = 'flex';
Shows the HUD (stars, timer, cop count) at the top left of the screen
userStartAudio();
Activates p5.sound's audio context—required by web browsers before playing any sound
gameStarted = true;
Sets the flag so the game loop begins responding to keyboard input and updates
initThreeJS();
Creates the three.js scene, camera, renderer, lights, models, and generates the first chunks of terrain
animate();
Starts the main animation loop that runs every frame (60 times per second)

initThreeJS()

initThreeJS() is the master initialization function. It sets up the scene, camera, renderer, lighting, and all the reusable geometries and materials. Then it creates the car, player, and gun models, and generates the first chunks of terrain. Finally, it wires up keyboard and mouse listeners so the game can respond to player input. This function only runs once when the game starts.

function initThreeJS() {
  // 1. Scene
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x87ceeb); // Sky blue background

  // 2. Camera
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(0, 10, 20); // Initial camera position

  // 3. Renderer
  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(window.devicePixelRatio); // Handle retina displays
  renderer.domElement.id = 'threejs-canvas'; 
  document.body.appendChild(renderer.domElement);

  // 5. Lighting
  const ambientLight = new THREE.AmbientLight(0x404040); // Soft white light
  scene.add(ambientLight);

  const directionalLight = new THREE.DirectionalLight(0xffffff, 1); // White directional light
  directionalLight.position.set(5, 10, 7);
  directionalLight.castShadow = true; // Enable shadows
  directionalLight.shadow.mapSize.width = 1024;
  directionalLight.shadow.mapSize.height = 1024;
  directionalLight.shadow.camera.near = 0.5;
  directionalLight.shadow.camera.far = chunkSize * visibleRadius * 2;
  directionalLight.shadow.camera.left = -chunkSize * visibleRadius;
  directionalLight.shadow.camera.right = chunkSize * visibleRadius;
  directionalLight.shadow.camera.top = chunkSize * visibleRadius;
  directionalLight.shadow.camera.bottom = -chunkSize * visibleRadius;
  scene.add(directionalLight);

  // --- Create Reusable Geometries and Materials Here (ONE TIME!) ---
  groundGeometry = new THREE.PlaneGeometry(chunkSize, chunkSize);
  roadGeometry = new THREE.PlaneGeometry(10, chunkSize);
  sirenLightGeometry = new THREE.CylinderGeometry(0.2, 0.2, 0.5, 8);

  groundMaterial = new THREE.MeshLambertMaterial({ color: 0x33aa33 });
  roadMaterial = new THREE.MeshLambertMaterial({ color: 0x555555 });
  buildingMaterial = new THREE.MeshLambertMaterial({ color: 0xaaaaaa });
  npcMaterial = new THREE.MeshLambertMaterial({ color: 0x00ff00 });
  redSirenMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 });
  blueSirenMaterial = new THREE.MeshLambertMaterial({ color: 0x0000ff });

  // Car parts
  carBodyGeometry = new THREE.BoxGeometry(3, 1, 1.5);
  carBodyMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 });
  carCabinGeometry = new THREE.BoxGeometry(2, 0.8, 1.2);
  carCabinMaterial = new THREE.MeshLambertMaterial({ color: 0x3333ff });
  carWheelGeometry = new THREE.CylinderGeometry(0.5, 0.5, 0.4, 16);
  carWheelMaterial = new THREE.MeshLambertMaterial({ color: 0x000000 });

  // Cop Car parts
  copCarBodyGeometry = new THREE.BoxGeometry(3, 1, 1.5); 
  copCarBodyMaterial = new THREE.MeshLambertMaterial({ color: 0x0000ff }); 
  copCarCabinGeometry = new THREE.BoxGeometry(2, 0.8, 1.2); 
  copCarCabinMaterial = new THREE.MeshLambertMaterial({ color: 0x3333ff }); 
  copCarWheelGeometry = new THREE.CylinderGeometry(0.5, 0.5, 0.4, 16); 
  copCarWheelMaterial = new THREE.MeshLambertMaterial({ color: 0x000000 }); 

  // Player parts
  playerBodyGeometry = new THREE.CylinderGeometry(0.5, 0.5, 1.8, 16);
  playerBodyMaterial = new THREE.MeshLambertMaterial({ color: 0x00ff00 });

  // Gun parts
  gunGeometry = new THREE.CylinderGeometry(0.1, 0.2, 0.8, 8);
  gunMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });

  // Tank parts
  tankBodyGeometry = new THREE.BoxGeometry(4, 2, 2.5);
  tankBodyMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 });
  turretGeometry = new THREE.CylinderGeometry(0.8, 1.2, 1, 16);
  turretMaterial = new THREE.MeshLambertMaterial({ color: 0x666666 });
  cannonGeometry = new THREE.CylinderGeometry(0.3, 0.4, 2, 8);
  cannonMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
  tankWheelGeometry = new THREE.BoxGeometry(0.8, 1.5, 0.5);

  // SWAT Truck parts
  swatTruckBodyGeometry = new THREE.BoxGeometry(4, 1.5, 2); 
  swatTruckBodyMaterial = new THREE.MeshLambertMaterial({ color: 0x222222 }); 
  swatTruckCabinGeometry = new THREE.BoxGeometry(3, 1.2, 1.8); 
  swatTruckCabinMaterial = new THREE.MeshLambertMaterial({ color: 0x111111 }); 
  swatTruckWheelGeometry = new THREE.CylinderGeometry(0.7, 0.7, 0.6, 16); 
  swatTruckWheelMaterial = new THREE.MeshLambertMaterial({ color: 0x000000 });

  // 6. Create Car
  createCar();

  // 7. Create On-Foot Player and Gun
  createPlayer();
  createGun(); 

  // 8. Position player at car's initial position
  player.position.copy(car.position);
  player.visible = false; 

  // 9. Initial chunk generation
  updateChunks();

  // 10. Add Keyboard and Mouse Event Listeners for Car & Player Control
  document.addEventListener('keydown', onKeyDown);
  document.addEventListener('keyup', onKeyUp);
  document.addEventListener('mousedown', onMouseDown);
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

initialization Scene, Camera, and Renderer Setup scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); renderer = new THREE.WebGLRenderer({ antialias: true });

Creates the three.js scene (the container for all objects), a camera (the player's viewpoint), and a WebGL renderer (the engine that draws to the screen)

initialization Ambient and Directional Lighting const ambientLight = new THREE.AmbientLight(0x404040); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 1);

Adds soft ambient light (so dark areas are still visible) and a bright directional light (from the sun) that casts shadows

initialization Reusable Geometries and Materials groundGeometry = new THREE.PlaneGeometry(chunkSize, chunkSize); groundMaterial = new THREE.MeshLambertMaterial({ color: 0x33aa33 });

Creates one copy of each geometry and material that will be reused across many chunks—this saves memory instead of creating new geometries for every building or road

scene = new THREE.Scene();
Creates a new three.js scene, which is the container that holds all 3D objects, lights, and the camera
scene.background = new THREE.Color(0x87ceeb);
Sets the background color to sky blue (hex 0x87ceeb) so the sky isn't transparent
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
Creates a perspective camera with a 75-degree field of view, aspect ratio matching the window, and a view distance from 0.1 to 1000 units
renderer = new THREE.WebGLRenderer({ antialias: true });
Creates a WebGL renderer that will draw the 3D scene. Antialias smooths jagged edges.
renderer.setPixelRatio(window.devicePixelRatio);
On Retina/high-DPI displays, sets the pixel ratio so graphics are crisp instead of blurry
document.body.appendChild(renderer.domElement);
Adds the renderer's canvas to the HTML page so the user can see the rendered scene
directionalLight.castShadow = true;
Enables shadow casting so objects block light and create realistic shadows on the ground
groundGeometry = new THREE.PlaneGeometry(chunkSize, chunkSize);
Creates a flat square plane geometry for the ground. By creating it once and reusing it, we save memory.
groundMaterial = new THREE.MeshLambertMaterial({ color: 0x33aa33 });
Creates a Lambert material (a realistic matte finish) in green. All ground planes will share this material.
createCar();
Builds the player's red car and adds it to the scene
updateChunks();
Generates the terrain chunks around the player's starting position

createChunkContent(chunkX, chunkZ)

createChunkContent() is called every time a new chunk needs to be generated. It creates the ground, three roads, 15 random buildings, 8 NPCs, and spawns cop cars and SWAT trucks only in chunks visible to the player. By returning a THREE.Group, the chunk can later be added to or removed from the scene as a single unit, making it easy to implement the chunk system.

🔬 This loop spawns random-height buildings. What happens if you change `Math.random() * 20 + 5` to `Math.random() * 40 + 10`? What will the buildings look like?

  for (let i = 0; i < numBuildingsPerChunk; i++) {
    const height = Math.random() * 20 + 5;
    const building = new THREE.Mesh(carBodyGeometry.clone(), buildingMaterial);
    building.geometry.applyMatrix4(new THREE.Matrix4().makeScale(1, height / 1, 1));
function createChunkContent(chunkX, chunkZ) {
  const chunkGroup = new THREE.Group();
  const offsetX = chunkX * chunkSize;
  const offsetZ = chunkZ * chunkSize;
  const halfChunk = chunkSize / 2;
  const spawnRadius = 2; // Spawn cop cars in chunks within 2 chunks of player

  // Ground Plane
  const ground = new THREE.Mesh(groundGeometry, groundMaterial);
  ground.rotation.x = -Math.PI / 2;
  ground.position.y = -0.5;
  ground.position.x = offsetX;
  ground.position.z = offsetZ;
  ground.receiveShadow = true;
  chunkGroup.add(ground);

  // Roads
  const road1 = new THREE.Mesh(roadGeometry, roadMaterial);
  road1.rotation.x = -Math.PI / 2;
  road1.position.y = -0.49;
  road1.position.x = offsetX;
  road1.position.z = offsetZ;
  road1.receiveShadow = true;
  chunkGroup.add(road1);

  const road2 = new THREE.Mesh(roadGeometry, roadMaterial);
  road2.rotation.x = -Math.PI / 2;
  road2.position.y = -0.49;
  road2.position.x = offsetX + 20; 
  road2.position.z = offsetZ;
  road2.receiveShadow = true;
  chunkGroup.add(road2);

  const road3 = new THREE.Mesh(roadGeometry, roadMaterial); 
  road3.rotation.x = -Math.PI / 2;
  road3.position.y = -0.49;
  road3.position.x = offsetX - 20;
  road3.position.z = offsetZ;
  road3.receiveShadow = true;
  chunkGroup.add(road3);

  // Simple Cube Buildings
  const numBuildingsPerChunk = 15;
  for (let i = 0; i < numBuildingsPerChunk; i++) {
    const height = Math.random() * 20 + 5;
    const building = new THREE.Mesh(carBodyGeometry.clone(), buildingMaterial);
    building.geometry.applyMatrix4(new THREE.Matrix4().makeScale(1, height / 1, 1));
    building.position.y = height / 2;

    let x, z;
    do {
      x = (Math.random() * chunkSize - halfChunk);
      z = (Math.random() * chunkSize - halfChunk);
    } while (x > -5 && x < 5); 

    building.position.x = offsetX + x;
    building.position.z = offsetZ + z;
    building.castShadow = true;
    building.receiveShadow = true;
    chunkGroup.add(building);
  }

  // NPCs (People) 
  const numNpcsPerChunk = 8;
  for (let i = 0; i < numNpcsPerChunk; i++) {
    const npcHeight = 1.8;
    const npc = new THREE.Mesh(playerBodyGeometry.clone(), npcMaterial);
    npc.geometry.applyMatrix4(new THREE.Matrix4().makeScale(1, npcHeight / 1.8, 1)); 
    npc.position.y = npcHeight / 2; 

    let x, z;
    do {
      x = (Math.random() * chunkSize - halfChunk);
      z = (Math.random() * chunkSize - halfChunk);
    } while (x > -10 && x < 10); 

    npc.position.x = offsetX + x;
    npc.position.z = offsetZ + z;
    npc.castShadow = true;
    npc.receiveShadow = true;
    chunkGroup.add(npc);
    npcs.push(npc);
    npc.chunkCoords = { x: chunkX, z: chunkZ };
  }

  // Create Regular Cop Cars
  if (stars < 5 &&
      Math.abs(chunkX - currentChunkX) <= spawnRadius &&
      Math.abs(chunkZ - currentChunkZ) <= spawnRadius) {
    for (let i = 0; i < numCopCarsPerChunk; i++) { 
      const copCar = createCopCarModel(); 
      let x, z;
      do {
        x = (Math.random() * chunkSize - halfChunk);
        z = (Math.random() * chunkSize - halfChunk);
      } while (x > -30 && x < 30); 

      copCar.position.set(offsetX + x, 0, offsetZ + z); 
      chunkGroup.add(copCar);
      copCars.push(copCar);
      copCar.chunkCoords = { x: chunkX, z: chunkZ };
    }
  }

  // Create SWAT Trucks
  if (stars >= 2 && stars < 5 &&
      Math.abs(chunkX - currentChunkX) <= spawnRadius &&
      Math.abs(chunkZ - currentChunkZ) <= spawnRadius) {
    for (let i = 0; i < numSwatTrucksPerChunk; i++) {
      const swatTruck = createSwatTruckModel();
      let x, z;
      do {
        x = (Math.random() * chunkSize - halfChunk);
        z = (Math.random() * chunkSize - halfChunk);
      } while (x > -30 && x < 30); 

      swatTruck.position.set(offsetX + x, 0, offsetZ + z);
      swatTruck.isSwatTruck = true;
      chunkGroup.add(swatTruck);
      copCars.push(swatTruck);
      swatTruck.chunkCoords = { x: chunkX, z: chunkZ }; 
    }
  }

  return chunkGroup;
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

initialization Ground and Road Creation const ground = new THREE.Mesh(groundGeometry, groundMaterial); chunkGroup.add(ground);

Creates one ground plane per chunk and three roads in different directions

for-loop Building Spawning Loop for (let i = 0; i < numBuildingsPerChunk; i++) {

Spawns 15 random buildings of varying heights in each chunk, avoiding the roads (x > -5 && x < 5)

for-loop NPC Spawning Loop for (let i = 0; i < numNpcsPerChunk; i++) {

Spawns 8 NPCs (green people) per chunk that the player can shoot when on foot

conditional Cop Car Spawn Condition if (stars < 5 && Math.abs(chunkX - currentChunkX) <= spawnRadius && Math.abs(chunkZ - currentChunkZ) <= spawnRadius) {

Only spawns regular cop cars in visible chunks (within 2 chunks of player) and only until 5-star level

conditional SWAT Truck Spawn Condition if (stars >= 2 && stars < 5 && Math.abs(chunkX - currentChunkX) <= spawnRadius && Math.abs(chunkZ - currentChunkZ) <= spawnRadius) {

Spawns SWAT trucks only at 2-4 star levels in visible chunks

const chunkGroup = new THREE.Group();
Creates a THREE.Group to hold all objects (ground, roads, buildings, NPCs, cops) for this chunk as one unit
const offsetX = chunkX * chunkSize;
Calculates the world X position of this chunk by multiplying the chunk index by chunk size (e.g., chunk 2 starts at x=400 if chunkSize is 200)
const halfChunk = chunkSize / 2;
Half the chunk size, used to center random spawning within the chunk boundaries
ground.rotation.x = -Math.PI / 2;
Rotates the plane 90 degrees so it lies flat on the ground (planes default to vertical)
ground.receiveShadow = true;
Allows shadows from other objects to be cast onto the ground plane, making it look realistic
const height = Math.random() * 20 + 5;
Generates a random building height between 5 and 25 units for visual variety
const building = new THREE.Mesh(carBodyGeometry.clone(), buildingMaterial);
Uses a clone of the car body geometry to create a building (reusing geometry saves memory)
building.geometry.applyMatrix4(new THREE.Matrix4().makeScale(1, height / 1, 1));
Scales the building's geometry vertically so it matches the random height we generated
} while (x > -5 && x < 5);
Ensures buildings don't spawn on the roads (which run through x=-5 to x=5)
if (stars < 5 &&
Regular cop cars only spawn until you reach 5 stars (then tanks take over)
Math.abs(chunkX - currentChunkX) <= spawnRadius &&
Only spawns cops in chunks within 2 chunks of the player (visible chunks), not in far-away chunks

createCar()

createCar() assembles the player's vehicle from basic shapes: a red box body, a blue cabin, and four black cylinder wheels. The car is created as a THREE.Group so we can move and rotate it as a single unit. This modular approach makes it easy to scale, position, and animate the whole car.

function createCar() {
  car = new THREE.Group(); 

  const carBody = new THREE.Mesh(carBodyGeometry, carBodyMaterial); 
  carBody.castShadow = true;
  carBody.receiveShadow = true;
  car.add(carBody);

  const carCabin = new THREE.Mesh(carCabinGeometry, carCabinMaterial); 
  carCabin.position.set(0, 0.9, 0);
  carCabin.castShadow = true;
  carCabin.receiveShadow = true;
  car.add(carCabin);

  const wheelPositions = [
    { x: -1, y: -0.5, z: 0.8 },
    { x: 1, y: -0.5, z: 0.8 },
    { x: -1, y: -0.5, z: -0.8 },
    { x: 1, y: -0.5, z: -0.8 },
  ];

  wheelPositions.forEach(pos => {
    const wheel = new THREE.Mesh(carWheelGeometry, carWheelMaterial); 
    wheel.position.set(pos.x, pos.y, pos.z);
    wheel.rotation.x = Math.PI / 2; 
    wheel.castShadow = true;
    wheel.receiveShadow = true;
    car.add(wheel);
  });

  scene.add(car);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

initialization Car Body and Cabin Assembly const carBody = new THREE.Mesh(carBodyGeometry, carBodyMaterial);

Creates the main body of the car by combining body and cabin meshes into one group

forEach-loop Wheel Positioning Loop wheelPositions.forEach(pos => {

Places four wheels at the corners of the car using a forEach loop over predefined positions

car = new THREE.Group();
Creates a THREE.Group to hold the car body, cabin, and wheels as one unit
const carBody = new THREE.Mesh(carBodyGeometry, carBodyMaterial);
Creates the main body of the car using the pre-created red box geometry and material
carBody.castShadow = true;
Allows the car body to cast shadows onto the ground and other objects
carBody.receiveShadow = true;
Allows shadows from other objects to fall on the car body
car.add(carBody);
Adds the body mesh to the car group
carCabin.position.set(0, 0.9, 0);
Positions the cabin (blue part) on top of the body (0.9 units up)
wheelPositions.forEach(pos => {
Loops through the four predefined wheel positions and creates a wheel at each one
wheel.rotation.x = Math.PI / 2;
Rotates the wheel 90 degrees so the cylinder lies sideways like a real wheel

animate()

animate() is the heart of the game—it runs 60 times per second and is responsible for everything: updating the survival timer and star meter, reading keyboard input and moving the player's car or player character, updating cop car AI to chase the player, checking for collisions, spawning and updating projectiles, and finally rendering the scene. The function is split into two major branches: one for driving mode and one for on-foot mode. Understanding this function is key to understanding how the whole game works.

🔬 This code handles acceleration (W) and braking (S). What happens if you change `carBrakeDeceleration` from 0.1 to 0.5? Test it and see if braking feels snappier or slower.

    if (keys['w']) {
      carSpeed += carAcceleration;
      carSpeed = Math.min(carSpeed, carMaxSpeed);
    } else if (keys['s']) {
      if (carSpeed > 0) {
        carSpeed -= carBrakeDeceleration;

🔬 This code makes cops chase you and wrap around the world edges. What if you remove the two if-statements that handle world wrapping? Cops would take the long way around instead of the shortcut. Try it and drive to the edge to see the difference!

      let direction = playerPos.clone().sub(copCarPos);
      direction.y = 0; 

      if (Math.abs(direction.x) > worldSize / 2) {
        direction.x -= Math.sign(direction.x) * worldSize;
function animate() {
  requestAnimationFrame(animate);

  if (gameOver) {
    renderer.render(scene, camera); 
    return; 
  }

  // --- Survival Timer and Star Meter Update ---
  if (!gameOver) {
      survivalTimer += deltaTime; 
      timerElement.innerHTML = `Time: ${nf(survivalTimer / 1000, 1, 0)}s`;

      for (let i = 0; i < survivalMilestones.length; i++) {
          if (survivalTimer >= survivalMilestones[i] && stars <= i) {
              stars = i + 1;
              updateStarDisplay(); 

              if (stars === 5) {
                  const spawnRadius = 2; 
                  const halfChunk = chunkSize / 2;
                  for (let j = 0; j < numTankCopCars; j++) {
                      let x, z;
                      do {
                          x = (Math.random() * chunkSize - halfChunk);
                          z = (Math.random() * chunkSize - halfChunk);
                      } while (x > -30 && x < 30); 

                      const tankCar = createTankCopCarModel(); 
                      tankCar.position.set(currentChunkX * chunkSize + x, 0, currentChunkZ * chunkSize + z);
                      tankCar.isTank = true; 
                      tankCar.shootingTimer = 0; 
                      scene.add(tankCar);
                      copCars.push(tankCar); 
                  }
              }
              break; 
          }
      }
  }

  copMeterElement.innerHTML = `Cops: ${copCars.length}`;

  if (playerState === 'driving') {
    // --- Car Controls ---
    if (keys['w']) {
      carSpeed += carAcceleration;
      carSpeed = Math.min(carSpeed, carMaxSpeed);
    } else if (keys['s']) {
      if (carSpeed > 0) {
        carSpeed -= carBrakeDeceleration;
        carSpeed = Math.max(carSpeed, 0);
      } else {
        carSpeed -= carAcceleration / 2; 
        carSpeed = Math.max(carSpeed, -carMaxSpeed / 2);
      }  
    } else { 
      if (carSpeed > 0) {
        carSpeed -= carDeceleration;
        carSpeed = Math.max(carSpeed, 0);
      } else if (carSpeed < 0) {
        carSpeed += carDeceleration;
        carSpeed = Math.min(carSpeed, 0);
      }
    }

    if (carSpeed !== 0) {
      if (keys['a']) { 
        car.rotation.y += carTurnSpeed * (carSpeed > 0 ? 1 : -1);
      }
      if (keys['d']) { 
        car.rotation.y -= carTurnSpeed * (carSpeed > 0 ? 1 : -1);
      }
    }

    const forwardVector = new THREE.Vector3(0, 0, -1); 
    forwardVector.applyQuaternion(car.quaternion); 
    car.position.addScaledVector(forwardVector, carSpeed);
    car.position.y = 0;
    wrapPosition(car.position, worldSize);

    const cameraOffset = new THREE.Vector3(0, 10, 15); 
    const rotatedOffset = cameraOffset.clone().applyQuaternion(car.quaternion); 
    camera.position.copy(car.position).add(rotatedOffset);
    camera.lookAt(car.position); 

    updateChunks();

    // --- Cop Car AI Chasing Logic ---
    for (const copCar of copCars) {
      let playerPos = car.position;
      let copCarPos = copCar.position;

      let currentCopSpeed = copCarSpeed;
      let currentCopTurnSpeed = copCarTurnSpeed;
      if (copCar.isSwatTruck) {
        currentCopSpeed = swatTruckSpeed;
        currentCopTurnSpeed = swatTruckTurnSpeed;
      } else if (copCar.isTank) {
        currentCopSpeed = tankCopCarSpeed;
        currentCopTurnSpeed = tankCopCarTurnSpeed;
      }

      let direction = playerPos.clone().sub(copCarPos);
      direction.y = 0; 

      if (Math.abs(direction.x) > worldSize / 2) {
        direction.x -= Math.sign(direction.x) * worldSize;
      }
      if (Math.abs(direction.z) > worldSize / 2) {
        direction.z -= Math.sign(direction.z) * worldSize;
      }
      direction.normalize();

      let desiredAngle = Math.atan2(direction.x, direction.z);
      let currentAngle = copCar.rotation.y;

      let angleDiff = desiredAngle - currentAngle;
      if (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
      if (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;

      copCar.rotation.y += Math.sign(angleDiff) * Math.min(Math.abs(angleDiff), currentCopTurnSpeed);
      copCar.position.addScaledVector(direction, currentCopSpeed);
      copCar.position.y = 0;

      wrapPosition(copCar.position, worldSize);

      // --- Tank Shooting Logic ---
      if (copCar.isTank) {
        const distanceToPlayer = copCar.position.distanceTo(car.position);
        if (distanceToPlayer < tankShootingRange && carSpeed !== 0) { 
            const tankTurret = copCar.children[1];
            const tankTurretPos = new THREE.Vector3();
            tankTurret.getWorldPosition(tankTurretPos);

            const targetPos = car.position.clone();
            targetPos.y += 0.5; 

            const tankDirection = targetPos.clone().sub(tankTurretPos).normalize();

            const desiredTurretAngle = Math.atan2(tankDirection.x, tankDirection.z);
            let currentTurretAngle = tankTurret.rotation.y;
            let turretAngleDiff = desiredTurretAngle - currentTurretAngle;
            if (turretAngleDiff > Math.PI) turretAngleDiff -= 2 * Math.PI;
            if (turretAngleDiff < -Math.PI) turretAngleDiff += 2 * Math.PI;
            tankTurret.rotation.y += Math.sign(turretAngleDiff) * 0.08; 

            const currentTime = Date.now();
            if (currentTime - copCar.shootingTimer > tankShootingCooldown) {
                copCar.shootingTimer = currentTime;
                const projectile = createProjectile(tankTurretPos, tankDirection);
                projectiles.push(projectile);
                scene.add(projectile);
            }
        }
      }
    }

    // --- Collision Detection ---
    inCollision = false; 
    for (const copCar of copCars) {
      if (car.position.distanceTo(copCar.position) < collisionDistance) {
        inCollision = true; 
        break; 
      }
    }

    if (inCollision) {
      if (collisionTimer === 0) {
        collisionTimer = Date.now(); 
      } else {
        const elapsed = Date.now() - collisionTimer;
        if (elapsed >= 7000) { 
          gameOver = true;
          carSpeed = 0; 
          scene.background = new THREE.Color(0x000000); 
          document.getElementById('gameOverImage').style.display = 'block'; 
          document.getElementById('crosshair').style.display = 'none'; 
          return; 
        }
      }
    } else {
      collisionTimer = 0; 
    }

    // --- Projectile Updates ---
    for (let i = projectiles.length - 1; i >= 0; i--) {
        const projectile = projectiles[i];
        projectile.position.addScaledVector(projectile.velocity, projectileSpeed);

        if (projectile.position.distanceTo(car.position) < (projectileRadius + collisionDistance / 2)) {
            gameOver = true;
            carSpeed = 0; 
            scene.background = new THREE.Color(0x000000); 
            document.getElementById('gameOverImage').style.display = 'block'; 
            document.getElementById('crosshair').style.display = 'none'; 
            scene.remove(projectile);
            projectile.geometry.dispose();
            projectile.material.dispose();
            projectiles.splice(i, 1);
            return; 
        }

        if (Math.abs(projectile.position.x) > worldSize / 2 || Math.abs(projectile.position.z) > worldSize / 2) {
            scene.remove(projectile);
            projectile.geometry.dispose();
            projectile.material.dispose();
            projectiles.splice(i, 1);
        }
    }

  } else if (playerState === 'onFoot') {
    // --- On-Foot Controls ---
    const playerForwardVector = new THREE.Vector3(0, 0, -1);
    playerForwardVector.applyQuaternion(player.quaternion);
    const playerRightVector = new THREE.Vector3(1, 0, 0);
    playerRightVector.applyQuaternion(player.quaternion);

    if (keys['w']) { 
      player.position.addScaledVector(playerForwardVector, playerSpeed);
    }
    if (keys['s']) { 
      player.position.addScaledVector(playerForwardVector, -playerSpeed);
    }
    if (keys['a']) { 
      player.rotation.y += playerTurnSpeed;
    }
    if (keys['d']) { 
      player.rotation.y -= playerTurnSpeed;
    }

    player.position.y = 0;
    wrapPosition(player.position, worldSize);

    const playerCameraOffset = new THREE.Vector3(0, 1.6, 0); 
    const rotatedPlayerOffset = playerCameraOffset.clone().applyQuaternion(player.quaternion);
    camera.position.copy(player.position).add(rotatedPlayerOffset);
    camera.lookAt(player.position.clone().add(playerForwardVector.multiplyScalar(10))); 
  }

  renderer.render(scene, camera);
}
Line-by-line explanation (34 lines)

🔧 Subcomponents:

calculation Survival Timer and Star Milestone Check survivalTimer += deltaTime;

Increments the survival timer every frame and checks if milestones (60s, 120s, etc.) have been reached to award new star levels

conditional Car Acceleration and Braking if (keys['w']) { carSpeed += carAcceleration; carSpeed = Math.min(carSpeed, carMaxSpeed);

Handles throttle (W key) to accelerate up to max speed, and brake (S key) to decelerate

conditional Car Steering Logic if (carSpeed !== 0) { if (keys['a']) { car.rotation.y += carTurnSpeed * (carSpeed > 0 ? 1 : -1); }

Turns the car left or right (A/D keys) only when moving, reversing turn direction when backing up

calculation Car Position Update const forwardVector = new THREE.Vector3(0, 0, -1); forwardVector.applyQuaternion(car.quaternion); car.position.addScaledVector(forwardVector, carSpeed);

Moves the car forward in the direction it's facing by applying its rotation to a forward vector

calculation Camera Following the Car const cameraOffset = new THREE.Vector3(0, 10, 15); const rotatedOffset = cameraOffset.clone().applyQuaternion(car.quaternion); camera.position.copy(car.position).add(rotatedOffset);

Positions the camera 10 units above and 15 units behind the car, rotating with it

for-loop Cop Car AI Chasing Loop for (const copCar of copCars) {

Updates each cop car's position and rotation to chase the player

calculation Angle-Based Navigation let desiredAngle = Math.atan2(direction.x, direction.z); let currentAngle = copCar.rotation.y; let angleDiff = desiredAngle - currentAngle;

Uses atan2 to calculate the angle from the cop car to the player, then smoothly rotates toward it

conditional Tank Shooting Mechanic if (copCar.isTank) { const distanceToPlayer = copCar.position.distanceTo(car.position);

If a cop car is a tank and within range, aim the turret and fire projectiles at the player

for-loop Collision Detection with Cop Cars for (const copCar of copCars) { if (car.position.distanceTo(copCar.position) < collisionDistance) {

Checks every cop car to see if it's touching the player car

conditional Collision Timer for Game Over if (elapsed >= 7000) { gameOver = true;

Ends the game if the player stays in collision with a cop car for 7 seconds

for-loop Projectile Movement and Cleanup for (let i = projectiles.length - 1; i >= 0; i--) {

Updates each projectile's position, checks collision with player car, and removes off-screen projectiles

conditional On-Foot Player Controls } else if (playerState === 'onFoot') { // --- On-Foot Controls ---

When the player is walking (not in the car), WASD moves and rotates the player, and the camera follows

requestAnimationFrame(animate);
Schedules this function to run again on the next frame (60 times per second), creating the animation loop
if (gameOver) { renderer.render(scene, camera); return;
If the game is over, render the scene once more and stop processing controls or updates
survivalTimer += deltaTime;
Adds the time elapsed since the last frame to the survival timer (deltaTime is p5.js's frame delta)
if (survivalTimer >= survivalMilestones[i] && stars <= i) {
Checks if the survival time has crossed a milestone (60s, 120s, etc.) and the star count hasn't been awarded yet
if (carSpeed > 0) { carSpeed -= carBrakeDeceleration;
If moving forward and braking (S key), decelerate faster than normal deceleration
if (carSpeed !== 0) { if (keys['a']) {
Only allow steering if the car is moving (carSpeed is not zero)
car.rotation.y += carTurnSpeed * (carSpeed > 0 ? 1 : -1);
Turn left or right depending on movement direction—if backing up, reverse the turn direction
const forwardVector = new THREE.Vector3(0, 0, -1);
Creates a vector pointing forward in car-space (before rotation)
forwardVector.applyQuaternion(car.quaternion);
Rotates the forward vector by the car's rotation so it points in the car's current facing direction
car.position.addScaledVector(forwardVector, carSpeed);
Moves the car by multiplying the forward direction by carSpeed and adding it to the position
const rotatedOffset = cameraOffset.clone().applyQuaternion(car.quaternion);
Rotates the camera offset (10 units up, 15 units back) by the car's rotation so the camera orbits the car as it turns
let direction = playerPos.clone().sub(copCarPos);
Calculates the vector from the cop car to the player (the direction to chase)
if (Math.abs(direction.x) > worldSize / 2) {
Handles world wrapping—if the player is far on one side, chase them via the wrapped-around side instead of across the whole map
direction.normalize();
Converts the direction vector to a unit vector (length 1) so speed is controlled separately
let desiredAngle = Math.atan2(direction.x, direction.z);
Calculates the angle the cop car should be facing using atan2 (gives angle from -π to π)
if (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
Normalizes the angle difference to be between -π and π so the cop turns the shortest way
copCar.rotation.y += Math.sign(angleDiff) * Math.min(Math.abs(angleDiff), currentCopTurnSpeed);
Turns the cop car toward the desired angle at its turn speed (or less if the angle difference is tiny)
const distanceToPlayer = copCar.position.distanceTo(car.position);
Calculates the straight-line distance from the tank to the player
const tankTurret = copCar.children[1];
Gets the turret (the second child of the tank) so we can aim it separately from the body
tankTurret.getWorldPosition(tankTurretPos);
Finds the turret's position in world space (accounting for the tank's position and rotation)
if (currentTime - copCar.shootingTimer > tankShootingCooldown) {
Only fires if enough time has passed since the last shot (cooldown system)
const projectile = createProjectile(tankTurretPos, tankDirection);
Creates a new projectile at the turret's position, moving in the direction the turret is aiming
if (car.position.distanceTo(copCar.position) < collisionDistance) {
Checks if the distance between the player car and this cop car is less than the collision threshold
if (collisionTimer === 0) { collisionTimer = Date.now();
Records the current time when a collision just started, so we can measure how long it lasts
if (elapsed >= 7000) {
If the player has been in collision for 7000 milliseconds (7 seconds), end the game
scene.background = new THREE.Color(0x000000);
Turns the background black when game over to indicate failure
for (let i = projectiles.length - 1; i >= 0; i--) {
Loops backward through projectiles so we can safely remove them with splice() without skipping any
projectile.position.addScaledVector(projectile.velocity, projectileSpeed);
Moves the projectile in its velocity direction at projectileSpeed pixels per frame
if (projectile.position.distanceTo(car.position) < (projectileRadius + collisionDistance / 2)) {
If the projectile gets close enough to the player car, treat it as a hit and end the game
projectiles.splice(i, 1);
Removes the projectile from the array after cleaning up its geometry and material
const playerForwardVector = new THREE.Vector3(0, 0, -1);
Creates a vector for the on-foot player's forward direction (same as the car logic)
player.position.addScaledVector(playerForwardVector, playerSpeed);
Moves the player forward at playerSpeed when W is pressed
camera.lookAt(player.position.clone().add(playerForwardVector.multiplyScalar(10)));
Makes the camera look 10 units ahead of the player so you can see where you're walking
renderer.render(scene, camera);
Draws the entire scene to the screen using the current camera viewpoint

onKeyDown(event)

onKeyDown() is called every time the player presses a key. It stores which keys are pressed in the keys object, which animate() reads every frame to know which direction to move. When the player presses E, this function toggles between driving and on-foot mode, showing or hiding the car and gun.

function onKeyDown(event) {
  if (gameOver || !gameStarted) return; 

  keys[event.key.toLowerCase()] = true;

  if (event.key.toLowerCase() === 'e') {
    if (playerState === 'driving') {
      playerState = 'onFoot';
      player.position.copy(car.position);
      player.rotation.copy(car.rotation); 
      car.visible = false; 
      gun.visible = true; 
      carSpeed = 0; 
      if (crosshairElement) crosshairElement.style.display = 'block'; 
    } else {
      playerState = 'driving';
      car.position.copy(player.position); 
      car.rotation.copy(player.rotation); 
      car.visible = true; 
      gun.visible = false; 
      if (crosshairElement) crosshairElement.style.display = 'none'; 
    }
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Key State Tracking keys[event.key.toLowerCase()] = true;

Records which key was pressed by storing it in the keys object

conditional Driving/On-Foot Mode Toggle if (event.key.toLowerCase() === 'e') {

When E is pressed, switches between car mode and on-foot mode

if (gameOver || !gameStarted) return;
Ignores all key presses if the game is over or hasn't started yet
keys[event.key.toLowerCase()] = true;
Stores the pressed key in the keys object (converted to lowercase so 'W' and 'w' are treated the same)
if (event.key.toLowerCase() === 'e') {
Checks specifically for the E key to toggle between driving and on-foot mode
playerState = 'onFoot';
Sets the state to on-foot mode, which changes how animate() interprets movement keys
player.position.copy(car.position);
Positions the player character at the same location as the car so there's no teleport
player.rotation.copy(car.rotation);
Copies the car's rotation to the player so they face the same direction
car.visible = false;
Hides the car from view
gun.visible = true;
Shows the gun in the player's hand (attached to the camera)
carSpeed = 0;
Stops the car's momentum so it doesn't keep moving after the player exits
if (crosshairElement) crosshairElement.style.display = 'block';
Shows the crosshair on screen so the player knows where their gun is aiming

onMouseDown(event)

onMouseDown() handles left-clicks when the player is on-foot. It implements a shooting cooldown by checking the time elapsed since the last shot—this prevents players from firing too rapidly. If enough time has passed, it calls shoot() to create a raycast and check for NPC hits.

function onMouseDown(event) {
  if (gameOver || !gameStarted) return; 

  if (playerState === 'onFoot' && event.button === 0) { 
    const currentTime = Date.now();
    if (currentTime - lastShotTime > shootingCooldown) {
      lastShotTime = currentTime;
      shoot();
    }
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Shooting Cooldown Check if (currentTime - lastShotTime > shootingCooldown) {

Prevents the player from spamming shots by checking if enough time has passed since the last shot

if (playerState === 'onFoot' && event.button === 0) {
Only allows shooting when the player is on-foot (not in the car) and clicking the left mouse button (button 0)
const currentTime = Date.now();
Gets the current time in milliseconds since the epoch
if (currentTime - lastShotTime > shootingCooldown) {
Checks if 300 milliseconds (shootingCooldown) have passed since the last shot
lastShotTime = currentTime;
Records this shot time so the cooldown counter resets
shoot();
Calls the shoot() function to fire a bullet and play a sound

shoot()

shoot() is called when the player clicks while in on-foot mode. It uses raycasting (firing an invisible ray from the camera through the crosshair) to detect if any NPCs are hit. If an NPC is hit, it's removed from the scene and a yellow impact effect appears for 100ms. The function also plays a gunshot sound and creates a brief orange muzzle flash effect to give visual feedback.

🔬 This code creates a yellow (0xffff00) sphere when you hit an NPC. What if you change the color to 0xff0000 (red) or make the sphere bigger by changing 0.1 to 0.5?

    const hitEffectGeometry = new THREE.SphereGeometry(0.1, 8, 8);
    const hitEffectMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
    const hitEffect = new THREE.Mesh(hitEffectGeometry, hitEffectMaterial);
function shoot() {
  raycaster.setFromCamera(mouse, camera);

  if (gunshotSound) {
    if (gunshotSound instanceof p5.Oscillator) {
      gunshotSound.amp(0, 0.01); 
      gunshotSound.amp(0.5, 0.05); 
      gunshotSound.amp(0.2, 0.05); 
      gunshotSound.amp(0, 0.2); 
    } else {
      gunshotSound.play();
    }
  }

  const intersects = raycaster.intersectObjects(npcs);

  if (intersects.length > 0) {
    const hitNpc = intersects[0].object;
    scene.remove(hitNpc);
    npcs = npcs.filter(npc => npc !== hitNpc);

    hitNpc.geometry.dispose(); 

    const hitEffectGeometry = new THREE.SphereGeometry(0.1, 8, 8);
    const hitEffectMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
    const hitEffect = new THREE.Mesh(hitEffectGeometry, hitEffectMaterial);
    hitEffect.position.copy(intersects[0].point);
    scene.add(hitEffect);

    setTimeout(() => {
      scene.remove(hitEffect);
      hitEffectGeometry.dispose(); 
      hitEffectMaterial.dispose(); 
    }, 100);
  }

  const gunFlashGeometry = new THREE.CylinderGeometry(0.1, 0.2, 0.2, 8);
  const gunFlashMaterial = new THREE.MeshBasicMaterial({ color: 0xffa500 }); 
  const gunFlash = new THREE.Mesh(gunFlashGeometry, gunFlashMaterial);
  gunFlash.position.copy(gun.position);
  gunFlash.position.z -= 0.5; 
  gunFlash.rotation.x = Math.PI / 2;
  camera.add(gunFlash); 

  setTimeout(() => {
    camera.remove(gunFlash);
    gunFlashGeometry.dispose();
    gunFlashMaterial.dispose();
  }, 50);
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

calculation Raycaster Initialization raycaster.setFromCamera(mouse, camera);

Creates a ray from the camera through the crosshair center into the scene

calculation Gunshot Sound Envelope gunshotSound.amp(0, 0.01); gunshotSound.amp(0.5, 0.05);

Creates an ADSR-like envelope: attack (quick ramp to 0.5), decay (to 0.2), release (to 0). This makes the gunshot sound crisp.

calculation NPC Hit Detection const intersects = raycaster.intersectObjects(npcs);

Checks which NPCs the ray passes through (usually just one, the closest hit)

conditional NPC Death and Hit Effect if (intersects.length > 0) {

If the ray hit an NPC, remove it from the scene and create a yellow impact effect

initialization Muzzle Flash Effect const gunFlash = new THREE.Mesh(gunFlashGeometry, gunFlashMaterial); gunFlash.position.copy(gun.position);

Creates a brief orange flash at the gun's tip to show the shot firing

raycaster.setFromCamera(mouse, camera);
Creates an invisible ray starting from the camera and pointing through the center of the screen (where the crosshair is)
gunshotSound.amp(0, 0.01);
Sets amplitude to 0 over 0.01 seconds (silence—start state)
gunshotSound.amp(0.5, 0.05);
Ramps amplitude to 0.5 over 0.05 seconds (quick attack—the loud part)
gunshotSound.amp(0.2, 0.05);
Reduces amplitude to 0.2 over 0.05 seconds (decay)
gunshotSound.amp(0, 0.2);
Fades amplitude to 0 over 0.2 seconds (release—the tail of the sound)
const intersects = raycaster.intersectObjects(npcs);
Finds all NPCs that the ray intersects (hits). Returns an array sorted by distance (closest first).
const hitNpc = intersects[0].object;
Gets the closest (first) NPC that was hit
scene.remove(hitNpc);
Removes the NPC from the scene so it disappears
npcs = npcs.filter(npc => npc !== hitNpc);
Removes the NPC from the npcs array so it won't be raycasted again in the future
hitNpc.geometry.dispose();
Frees the NPC's geometry from memory (important for performance)
const hitEffect = new THREE.Mesh(hitEffectGeometry, hitEffectMaterial);
Creates a small yellow sphere at the hit point to show visual feedback
hitEffect.position.copy(intersects[0].point);
Positions the hit effect at the exact spot where the ray hit the NPC
setTimeout(() => {
Schedules the hit effect to be removed after 100 milliseconds (a brief flash)
const gunFlash = new THREE.Mesh(gunFlashGeometry, gunFlashMaterial);
Creates an orange cylinder for the muzzle flash (shows the gun firing)
gunFlash.position.copy(gun.position); gunFlash.position.z -= 0.5;
Positions the flash slightly in front of the gun (z -= 0.5 moves it forward)
camera.add(gunFlash);
Adds the flash to the camera instead of the scene so it stays attached to the gun and doesn't drift away

updateChunks()

updateChunks() is called every frame while driving. It implements the chunk system: it calculates which chunk the player is in, loads any new chunks that became visible, and unloads chunks that are too far away. By only keeping a 3x3 (or larger) grid of chunks around the player, the game avoids loading the entire infinite world at once, keeping performance high. Shared geometries and materials are never disposed, only chunk-specific ones.

🔬 This nested loop loads chunks in a grid around the player. If visibleRadius is 1, it loads a 3x3 grid. What happens if you change visibleRadius to 2? The grid becomes 5x5, so you see more terrain at once.

    for (let cx = currentChunkX - visibleRadius; cx <= currentChunkX + visibleRadius; cx++) {
      for (let cz = currentChunkZ - visibleRadius; cz <= currentChunkZ + visibleRadius; cz++) {
function updateChunks() {
  const newChunkX = Math.floor(car.position.x / chunkSize);
  const newChunkZ = Math.floor(car.position.z / chunkSize);

  if (newChunkX !== currentChunkX || newChunkZ !== currentChunkZ) {
    currentChunkX = newChunkX;
    currentChunkZ = newChunkZ;

    const activeChunkKeys = new Set();

    for (let cx = currentChunkX - visibleRadius; cx <= currentChunkX + visibleRadius; cx++) {
      for (let cz = currentChunkZ - visibleRadius; cz <= currentChunkZ + visibleRadius; cz++) {
        const key = `${cx},${cz}`;
        activeChunkKeys.add(key);

        if (!chunks.has(key)) {
          const chunkGroup = createChunkContent(cx, cz);
          chunks.set(key, chunkGroup);
          scene.add(chunkGroup);
        }
      }
    }

    const sharedGeometries = new Set([
        groundGeometry, roadGeometry, sirenLightGeometry,
        carBodyGeometry, carCabinGeometry, carWheelGeometry,
        copCarBodyGeometry, copCarCabinGeometry, copCarWheelGeometry,
        playerBodyGeometry, gunGeometry,
        tankBodyGeometry, turretGeometry, cannonGeometry,
        swatTruckBodyGeometry, swatTruckCabinGeometry, swatTruckWheelGeometry
    ]);

    const sharedMaterials = new Set([
        groundMaterial, roadMaterial, buildingMaterial, npcMaterial,
        redSirenMaterial, blueSirenMaterial,
        carBodyMaterial, carCabinMaterial, carWheelMaterial,
        copCarBodyMaterial, copCarCabinMaterial, copCarWheelMaterial,
        playerBodyMaterial, gunMaterial,
        tankBodyMaterial, turretMaterial, cannonMaterial, tankWheelMaterial,
        swatTruckBodyMaterial, swatTruckCabinMaterial, swatTruckWheelMaterial
    ]);

    chunks.forEach((chunkGroup, key) => {
      if (!activeChunkKeys.has(key)) {
        scene.remove(chunkGroup);

        chunkGroup.children.forEach(object => {
          if (object.geometry && !sharedGeometries.has(object.geometry)) {
            object.geometry.dispose();
          }
          if (object.material && !sharedMaterials.has(object.material)) {
            object.material.dispose();
          }
        });

        npcs = npcs.filter(npc => npc.chunkCoords && (npc.chunkCoords.x !== currentChunkX || npc.chunkCoords.z !== currentChunkZ));
        copCars = copCars.filter(copCar => copCar.chunkCoords && (copCar.chunkCoords.x !== currentChunkX || copCar.chunkCoords.z !== currentChunkZ));

        chunks.delete(key);
      }
    });
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

calculation Current Chunk Index Calculation const newChunkX = Math.floor(car.position.x / chunkSize);

Calculates which chunk the player is currently in by dividing their position by chunk size

conditional Chunk Transition Detection if (newChunkX !== currentChunkX || newChunkZ !== currentChunkZ) {

Only updates visible chunks if the player has moved to a new chunk

for-loop Visible Chunks Generation for (let cx = currentChunkX - visibleRadius; cx <= currentChunkX + visibleRadius; cx++) {

Creates or loads all chunks within the visible radius (a 3x3 grid if visibleRadius is 1)

forEach-loop Far Chunk Cleanup chunks.forEach((chunkGroup, key) => {

Removes chunks that are no longer visible and frees their memory

const newChunkX = Math.floor(car.position.x / chunkSize);
Divides the car's X position by chunk size (200) and rounds down to get the chunk index (e.g., position 250 is in chunk 1)
if (newChunkX !== currentChunkX || newChunkZ !== currentChunkZ) {
Only runs the expensive chunk loading/unloading logic if the player has actually moved to a new chunk
const activeChunkKeys = new Set();
Creates a set to track which chunk keys should be visible this frame
for (let cx = currentChunkX - visibleRadius; cx <= currentChunkX + visibleRadius; cx++) {
Loops through all chunks in a radius around the player (e.g., from -1 to +1 if visibleRadius is 1)
const key = `${cx},${cz}`;
Creates a string key like '5,-3' to identify each chunk uniquely
activeChunkKeys.add(key);
Marks this chunk as 'should be visible'
if (!chunks.has(key)) {
Checks if this chunk has already been created
const chunkGroup = createChunkContent(cx, cz);
Generates a new chunk with terrain, buildings, NPCs, and cop cars
chunks.set(key, chunkGroup);
Stores the chunk in the chunks Map for fast lookup later
scene.add(chunkGroup);
Adds the new chunk to the three.js scene so it becomes visible
const sharedGeometries = new Set([
Creates a set of all geometries that are shared across chunks. We won't dispose() these.
chunks.forEach((chunkGroup, key) => {
Loops through all loaded chunks to find ones that should be unloaded
if (!activeChunkKeys.has(key)) {
If this chunk is NOT in the active set, it's too far away and should be unloaded
scene.remove(chunkGroup);
Removes the chunk from the scene so it's no longer rendered
if (object.geometry && !sharedGeometries.has(object.geometry)) {
Only dispose() of geometries that aren't shared (shared ones are still needed by other chunks)
npcs = npcs.filter(npc => npc.chunkCoords && (npc.chunkCoords.x !== currentChunkX || npc.chunkCoords.z !== currentChunkZ));
Removes NPCs from the unloaded chunk from the global npcs array so they can't be shot anymore
chunks.delete(key);
Removes the chunk from the chunks Map so the memory is fully freed

📦 Key Variables

scene THREE.Scene

The three.js scene container that holds all 3D objects, lights, and the camera

scene = new THREE.Scene();
camera THREE.PerspectiveCamera

The player's viewpoint into the 3D world; controls what part of the scene is rendered

camera = new THREE.PerspectiveCamera(75, ...);
renderer THREE.WebGLRenderer

The three.js rendering engine that draws the scene to the HTML canvas

renderer = new THREE.WebGLRenderer({ antialias: true });
car THREE.Group

The player's vehicle; a group containing the body, cabin, and wheels

car = new THREE.Group();
player THREE.Group

The player's on-foot character model; visible when not driving

player = new THREE.Group();
gun THREE.Mesh

The gun model attached to the camera; visible when on-foot and shooting

gun = new THREE.Mesh(gunGeometry, gunMaterial);
keys object

Tracks which keyboard keys are currently pressed (e.g., keys['w'] = true when W is held)

let keys = {};
carSpeed number

The car's current velocity; positive is forward, negative is reverse

let carSpeed = 0;
playerState string

Either 'driving' or 'onFoot'; controls which movement logic animate() uses

let playerState = 'driving';
npcs array

An array of all NPC (green people) meshes that can be shot

let npcs = [];
copCars array

An array of all cop car, SWAT truck, and tank meshes currently in the world

let copCars = [];
stars number

The current wanted level (0-5 stars); increases over time

let stars = 0;
survivalTimer number

Tracks elapsed time in milliseconds since the game started

let survivalTimer = 0;
gameOver boolean

True if the player has been caught or hit by a projectile; stops gameplay

let gameOver = false;
gameStarted boolean

True after the player clicks Play; prevents input before the game initializes

let gameStarted = false;
chunks Map

A map of chunk keys (e.g., '5,-3') to THREE.Group objects; stores all loaded terrain chunks

let chunks = new Map();
currentChunkX number

The chunk index of the player's current position (X axis)

let currentChunkX = 0;
currentChunkZ number

The chunk index of the player's current position (Z axis)

let currentChunkZ = 0;
projectiles array

An array of tank projectile meshes currently in flight

let projectiles = [];
lastShotTime number

Timestamp of the last time the player shot; used to enforce shooting cooldown

let lastShotTime = 0;
inCollision boolean

True if the car is currently touching a cop car

let inCollision = false;
collisionTimer number

Timestamp when collision started; used to end game after 7 seconds of continuous collision

let collisionTimer = 0;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG updateChunks() NPC filtering

NPCs are filtered incorrectly when chunks unload. The condition uses OR (||) instead of AND (&&), causing NPCs from visible chunks to be removed.

💡 Change `(npc.chunkCoords.x !== currentChunkX || npc.chunkCoords.z !== currentChunkZ)` to `(npc.chunkCoords.x !== currentChunkX && npc.chunkCoords.z !== currentChunkZ)` so only NPCs from completely unrelated chunks are removed.

PERFORMANCE shoot() function

Creating new SphereGeometry and MeshBasicMaterial for every hit creates memory churn. Disposing them 100ms later is good, but hit effects are created frequently.

💡 Reuse one hit effect geometry and material like the car parts do, or pool hit effects so you're not creating new objects constantly.

BUG animate() on-foot movement

On-foot player never updates chunks or unloads terrain chunks because updateChunks() is only called in the driving branch.

💡 Move `updateChunks()` outside the driving/on-foot conditional so chunks load/unload even when walking around on foot.

STYLE Global variables section

Many constants are defined at the top but scattered among different concerns (car speeds, tank speeds, cooldowns, etc.). They could be better organized.

💡 Group related constants into commented sections: '--- Car Constants ---', '--- Tank Constants ---', '--- Gameplay Constants ---' to improve readability.

FEATURE Collision system

Collisions with NPCs are never checked; the player can drive through them as if they don't exist.

💡 Add collision detection between the car and NPCs similar to cop cars so the world feels more solid and NPCs react to the player.

PERFORMANCE animate() cop car loop

Every frame, every cop car calculates distance to player and runs angle math. At high cop counts this is O(n) per frame.

💡 Consider spatial partitioning (divide the world into grid cells) so you only update cops within a certain distance, skipping far-away ones.

🔄 Code Flow

Code flow showing setup, startgame, initthreejs, createchunkcontent, createcar, animate, onkeydown, onmousedown, shoot, updatechunks

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> animate[animate] animate --> survival-timer-update[survival-timer-update] survival-timer-update --> car-acceleration-logic[car-acceleration-logic] car-acceleration-logic --> car-steering[car-steering] car-steering --> car-movement[car-movement] car-movement --> camera-follow[camera-follow] camera-follow --> cop-ai-loop[cop-ai-loop] cop-ai-loop --> angle-calculation[angle-calculation] angle-calculation --> tank-shooting-logic[tank-shooting-logic] cop-ai-loop --> collision-detection-loop[collision-detection-loop] collision-detection-loop --> collision-timer-logic[collision-timer-logic] animate --> onkeydown[onkeydown] onkeydown --> key-tracking[key-tracking] key-tracking --> mode-toggle[mode-toggle] animate --> onmousedown[onmousedown] onmousedown --> click-cooldown[click-cooldown] click-cooldown --> raycaster-setup[raycaster-setup] raycaster-setup --> raycast-intersection[raycast-intersection] raycast-intersection --> npc-removal[npc-removal] npc-removal --> muzzle-flash[muzzle-flash] animate --> updatechunks[updatechunks] updatechunks --> chunk-index-calculation[chunk-index-calculation] chunk-index-calculation --> chunk-changed-check[chunk-changed-check] chunk-changed-check --> visible-chunks-loop[visible-chunks-loop] visible-chunks-loop --> chunk-unloading[chunk-unloading] click setup href "#fn-setup" click draw href "#fn-draw" click animate href "#fn-animate" click survival-timer-update href "#sub-survival-timer-update" click car-acceleration-logic href "#sub-car-acceleration-logic" click car-steering href "#sub-car-steering" click car-movement href "#sub-car-movement" click camera-follow href "#sub-camera-follow" click cop-ai-loop href "#sub-cop-ai-loop" click angle-calculation href "#sub-angle-calculation" click tank-shooting-logic href "#sub-tank-shooting-logic" click collision-detection-loop href "#sub-collision-detection-loop" click collision-timer-logic href "#sub-collision-timer-logic" click onkeydown href "#fn-onkeydown" click key-tracking href "#sub-key-tracking" click mode-toggle href "#sub-mode-toggle" click onmousedown href "#fn-onmousedown" click click-cooldown href "#sub-click-cooldown" click raycaster-setup href "#sub-raycaster-setup" click raycast-intersection href "#sub-raycast-intersection" click npc-removal href "#sub-npc-removal" click muzzle-flash href "#sub-muzzle-flash" click updatechunks href "#fn-updatechunks" click chunk-index-calculation href "#sub-chunk-index-calculation" click chunk-changed-check href "#sub-chunk-changed-check" click visible-chunks-loop href "#sub-visible-chunks-loop" click chunk-unloading href "#sub-chunk-unloading"

Preview

gta 6 current state upd for hooda math - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of gta 6 current state upd for hooda math - Code flow showing setup, startgame, initthreejs, createchunkcontent, createcar, animate, onkeydown, onmousedown, shoot, updatechunks
Code Flow Diagram