Code viewer for World: Wrong Way Motorway
//James Reilly CA318 Repeat Practical


//WRONGWAY MOTORWAY ALL SOURCES

//Game music downloaded non copyrighted from https://pixabay.com/music/search/arcade/
//All 3D models came from non copyrighted source of FREE3D.com
//Styling came from source https://bobbyhadz.com/blog/javascript-set-position-of-element and https://www.w3schools.com/css/
//camera positioning (lookat, positioning z , x etc..) from geeks for geeeks



// Initializing the start menu UI element
// This code creates a dynamic start menu on the webpage

const startMenu = document.createElement('div');
startMenu.style.position = 'absolute';
startMenu.style.width = '100%';
startMenu.style.height = '100%';
startMenu.style.display = 'flex';
startMenu.style.flexDirection = 'column';
startMenu.style.justifyContent = 'center';
startMenu.style.alignItems = 'center';
startMenu.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
document.body.appendChild(startMenu);


// Adding a start button to the start menu
// This section generates a clickable button within the start menu

const startButton = document.createElement('button');
startButton.textContent = 'Press Enter To Play';
startButton.style.padding = '10px 20px';
startButton.style.fontSize = '20px';
startButton.style.background = 'none';
startButton.style.border = '2px solid white';
startButton.style.color = 'white';
startMenu.appendChild(startButton);


// Create a div for the game instructions for the start menu
// This block of code creates an informative instructions box

const instructionsBox = document.createElement('div');
instructionsBox.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
instructionsBox.style.padding = '20px';
instructionsBox.style.marginTop = '70px'; 
instructionsBox.style.border = '2px solid white';
instructionsBox.style.color = 'white';
instructionsBox.style.fontSize = '20px';
instructionsBox.style.fontWeight = 'bold';
instructionsBox.style.textAlign = 'center';
instructionsBox.innerHTML = `
  <p><strong>Instructions:</strong></p>
  <p><strong>You and your friends have ended up on the wrong side of the motorway!</strong></p>
  <p>The <span style="color: blue;">blue car</span> in the left lanes use the <span style="color: blue;">A</span> and <span style="color: blue;">D</span> keys to avoid oncoming cars</p>
  <p>The <span style="color: red;">red car</span> in the right lanes use the <span style="color: red;">Arrow Left</span> and <span style="color: red;">Arrow Right</span> keys to avoid oncoming cars</p>
  <p>As the game goes on, drivers get faster</p>
  <p>The first car to crash loses the game</p>`;
startMenu.appendChild(instructionsBox);



// Create a message element for game over

const gameOverMessage = document.createElement('div');
gameOverMessage.style.color = 'white';
gameOverMessage.style.fontSize = '24px';
gameOverMessage.style.marginTop = '10px';
startMenu.appendChild(gameOverMessage);


// Adding a mute button for sound control
// Easily noticable on the start menu

const muteButton = document.createElement('button');
muteButton.textContent = 'Mute Sound';
muteButton.style.position = 'absolute';
muteButton.style.top = '20px';
muteButton.style.left = '20px'
muteButton.style.fontSize = '20px';
muteButton.style.background = 'none';
muteButton.style.border = '2px solid white';
muteButton.style.color = 'white';
muteButton.style.zIndex = '100';
startMenu.appendChild(muteButton);


// Setting up a 3D scene using Three.js
// Setting positions and perspectives for the user by rendering the camera for different displays

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight,0.01, 1000); //https://www.tabnine.com/code/javascript/functions/three/WebGLRenderer/setSize
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
document.documentElement.style.overflow = 'hidden';
document.body.style.overflow = 'hidden';


// Adding in a background image for the night sky
// I made this image myself on my computer using the 3D paint app

const backgroundTexture = new THREE.TextureLoader().load('/uploads/james2001/sky.png'); //https://threejs.org/docs/#examples/en/loaders
const backgroundGeometry = new THREE.PlaneGeometry(900, 500); 
const backgroundMaterial = new THREE.MeshBasicMaterial({ map: backgroundTexture });
const backgroundMesh = new THREE.Mesh(backgroundGeometry, backgroundMaterial);
backgroundMesh.position.z = -300;
scene.add(backgroundMesh);


// Loading and placing 3D car models
// This code utilizes the MTLLoader and OBJLoader to load and position car models
// Two different car models (redCar and blueCar) are loaded and scaled
// The models' materials are configured with the provided MTL files in my uploads
// Adjustments are made to the scale, position, and rotation of the car objects
// Finally, the cars are added to the scene for display and interaction


let redCar, blueCar;
const mtlLoader = new THREE.MTLLoader();
mtlLoader.load("/uploads/james2001/Car.mtl", function (materials) { //https://threejs.org/docs/#examples/en/loaders
  materials.preload();
  const objLoader = new THREE.OBJLoader();
  objLoader.setMaterials(materials);
  objLoader.load("/uploads/james2001/Car.obj", function (object) {
    object.scale.set(0.5, 0.5, 0.5);
    object.position.set(0, 0, 0);
    object.rotation.y = Math.PI;
    redCar = object; // Add the loaded .obj model to redCar
    scene.add(redCar);
  });
});

mtlLoader.load("/uploads/james2001/bluecar.mtl", function (materials) {
  materials.preload();
  const objLoader = new THREE.OBJLoader();
  objLoader.setMaterials(materials);
  objLoader.load("/uploads/james2001/Car.obj", function (object) {
    object.scale.set(0.5, 0.5, 0.5); 
    object.position.set(0, 0, 0); 
    object.rotation.y = Math.PI;
    blueCar = object; // Add the loaded .obj model to redCar
    scene.add(blueCar);
  });
});


// Setting up lighting in the scene
// Here both a directional light and an ambient light are created for the displahy of my loaded obj models
// The directional light provides to a specific direction
// The ambient light adds to the entire scene
// These lights are added to the scene to enhance visibility and visual appeal

const directionalLight = new THREE.DirectionalLight(0xffffff, 1);  //https://threejs.org/docs/#api/en/lights/DirectionalLight
directionalLight.position.set(1, 1, 1); // Setting the light's position
const ambientLight = new THREE.AmbientLight(0x404040); // white light
scene.add(directionalLight);
scene.add(ambientLight);


// Creating and positioning a road in the scene
// This code block generates a road using a plane geometry and basic material
// The road is rotated to lie flat (horizontal) by adjusting its rotation
// The road is added to the scene with the specified position
// It creates the illusion of a road surface for the cars to move on


const roadGeometry = new THREE.PlaneGeometry(17, 100000,1,1);
const roadMaterial = new THREE.MeshBasicMaterial({ color: 0xaaaaaa });
const road = new THREE.Mesh(roadGeometry, roadMaterial);   //https://stackoverflow.com/questions/9252764/how-to-create-a-custom-mesh-on-three-js
road.rotation.x = -Math.PI / 2;
const roadMesh = new THREE.Mesh(roadGeometry, roadMaterial);
roadMesh.position.set(0, -2, 0)
roadMesh.rotation.x = -Math.PI / 2; // Rotating the road to lie flat
scene.add(roadMesh);


// Loadthe grass image for the middle patch
// making the image repeat
// Applying loaded texture to the grass material and adding to the scene

const grassImage = new THREE.TextureLoader().load('uploads/james2001/grass.jpg');
grassImage.wrapS = THREE.RepeatWrapping; //enabling texture repition along the whole grass strip
grassImage.wrapT = THREE.RepeatWrapping; //https://stackoverflow.com/questions/14114030/how-to-write-right-repeat-texture-with-three-js
const numRepeats = 130; 
grassImage.repeat.set(20, numRepeats);
const laneWidth = 2;
const grass = new THREE.PlaneGeometry(laneWidth, 200000, 1, numRepeats);
const grassmaterial = new THREE.MeshBasicMaterial({ map: grassImage }); 
const stripOfGrass = new THREE.Mesh(grass, grassmaterial);
stripOfGrass.rotation.x = -Math.PI / 2;
scene.add(stripOfGrass);


// Setting up roadlines for the road
// Defining the number of roadlines to be generated
// Looping through to create and position each road line


const numberOfLines = 4;
const lineGeometry = new THREE.PlaneGeometry(0.07, 270000); //
const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
for (let i = 0; i < numberOfLines; i++) {
  const line = new THREE.Mesh(lineGeometry, lineMaterial);
  const lineX = (i - (numberOfLines - 1) / 2) * laneWidth;
  line.position.set(lineX, 0.02, 0);
  line.rotation.x = -Math.PI / 2;
  scene.add(line);
}



let lastOncomingCarLane = -1; // Tracking the last lane used for OncomingCar creation
const OncomingCars = []; // Storing OncomingCars in an array
let numOfOncomingCars = 10; 
let OncomingCarSpeed = 0.20; // Initialising OncomingCar speed
let timeElapsed = 0; // Tracking time elapsed
let gameStarted = false; // Flagging to indicate if the game has started
let gameOver = false; // Flagging to indicate if the game has started



// Function to create an OncomingCar
function createOncomingCar(z, lane) {
    const mtlFiles = [
    "/uploads/james2001/pinkcar.mtl",
    "/uploads/james2001/orange.mtl",
    "/uploads/james2001/limegreen.mtl",
    "/uploads/james2001/hotpink.mtl",
    "/uploads/james2001/darkpurple.mtl",
    "/uploads/james2001/darkgreen.mtl",
    "/uploads/james2001/brown.mtl",
    "/uploads/james2001/white.mtl",
    "/uploads/james2001/yellow.mtl"
    

    // All my mtl files in my uploads section
  ];
  const randomIndex = Math.floor(Math.random() * mtlFiles.length); //all my knowledge on math floor and math random came from w3schools.com
  const randomMtlFile = mtlFiles[randomIndex];
  
  const mtlLoader = new THREE.MTLLoader();
  mtlLoader.load(randomMtlFile, function (materials) {
  materials.preload();
  const objLoader = new THREE.OBJLoader();
  objLoader.setMaterials(materials);
  objLoader.load("/uploads/james2001/Car.obj", function (object) {
  OncomingCar = object; 
  object.scale.set(0.5, 0.5, 0.5);
 
  let lanePosition;

            if (lane === 0 || lane === 1) {
                // Placing OncomingCar in the same position as redCar 2
                lanePosition = (blueCarLane - 1.95) * laneWidth;
            } else if (lane === 3 || lane === 4) {
                // Placing OncomingCar in the same position as redCar 1
                lanePosition = (redCarLane - 2.09) * laneWidth;
            }
  OncomingCar.position.x = lanePosition; // Set lane position
  OncomingCar.position.z = -100; // Set initial position
  scene.add(OncomingCar); // Add OncomingCars to the scene
  OncomingCars.push(OncomingCar);
});
});
}


const explosionTexture = new THREE.TextureLoader().load('uploads/james2001/explosion.png');

function createExplosion(position, lane) {
  const explosionMaterial = new THREE.SpriteMaterial({ map: explosionTexture}); //SPRITE : https://threejs.org/docs/#api/en/materials/SpriteMaterial
  const explosion = new THREE.Sprite(explosionMaterial);
  explosion.position.copy(position);
  explosion.position.y = 2.2; // Setring explosion position to the lane
  explosion.scale.set(8, 8, 8);
  scene.add(explosion);

  // Start the explosion animation
  const startTimestamp = performance.now(); //https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
  function animateExplosion(timestamp) {
    const elapsed = (timestamp - startTimestamp) / 1000;
    const progress = Math.min(elapsed / 1.0, 1.0);

    explosion.scale.set(progress, progress, progress);
    explosion.material.opacity = 1.0 - progress;

    if (progress < 1.0) {
      requestAnimationFrame(animateExplosion); //https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
    } else {
      scene.remove(explosion);
    }
  }
  requestAnimationFrame(animateExplosion);
}
// Create a point light for the street lamp
const streetLampLight = new THREE.PointLight(0xffffff, 1, 20); // White light 
streetLampLight.position.set(200, 290, 200); // Adjusting position to be at the top of the street lamp
scene.add(streetLampLight);

// Load street lamp OBJ and MTL

const lampSpacing = 10;  // Spacing between lamps on z-axis
const lampStartPosition = -100;       // Starting position on z- axis
const lampEndPosition = 100;        // Ending position on z axis

const streetLamps = [];


// Load street lamp OBJ and MTL same way as 3d car models
// creating a loop for the streetlamps to appear
//loading and setting up to the materials to be passed on and used in create street lamp


const streetLampMtlLoader = new THREE.MTLLoader();
streetLampMtlLoader.load("uploads/james2001/Street_Lamp.mtl", function (materials) {
materials.preload();
const streetLampObjLoader = new THREE.OBJLoader();
streetLampObjLoader.setMaterials(materials);

  for (let z = lampStartPosition; z <= lampEndPosition; z += lampSpacing) { //sets lamps up along the side of the road
    createStreetLampAtZ(streetLampObjLoader, z);
  }
});



// Function to create and position street lamps at a given z-position
function createStreetLampAtZ(loader, zPosition) {
  // Loading the 3D street lamp model
  loader.load("uploads/james2001/StreetLamp.obj", function (object) {

    // Scaling down the street lamp model for appropriate sizing
    object.scale.set(0.003, 0.003, 0.003);
    
    // Creating a new instance of the street lamp
    const streetLamp = object.clone();
    
    // Positioning the first street lamp instance on the left side of the road
    streetLamp.position.set(-3.7 * laneWidth, 0, zPosition);
    
    // Adding the first street lamp to the scene
    scene.add(streetLamp);
    
    // Attaching a cloned point light to the first street lamp
    streetLamp.add(streetLampLight.clone());
    
    // Storing the first street lamp instance in the streetLamps array
    streetLamps.push(streetLamp);

    // Creating another instance of the street lamp for the right side of the road
    const streetLampRightLane = object.clone();
    
    // Positioning the second street lamp instance on the right side of the road
    streetLampRightLane.position.set(3.7 * laneWidth, 0, zPosition);
    
    // Addding the second street lamp to the scene
    scene.add(streetLampRightLane);
    
    // Attaching a cloned point light to the second street lamp
    streetLampRightLane.add(streetLampLight.clone());
    
    // Rotating the second street lamp to face the opposite direction
    streetLampRightLane.rotation.y = Math.PI;
    
    // Storing the second street lamp instance in the streetLamps array
    streetLamps.push(streetLampRightLane);
  });
}

// Variables to track the lanes and positions of the red and blue cars

let blueCarLane = 1;
let redCarLane = 3;
let redCarPosition = 1;
let redCarVelocityY = 0;
let blueCarPosition = 1;
let blueCarVelocityY = 0;




//Game music downloaded freely and non copyrighted from https://pixabay.com/music/search/arcade/
//game music should constantly be looped

const gameMusic = new Audio('/uploads/james2001/gamemusic.mp3');
gameMusic.loop = true;
let isMuted = false; // Added flag for mute state

//if the game is not muted , mute sound button appears so user can mute
//otherwise unmute sound appears for user to unmute

muteButton.addEventListener('click', (e) => {  //addEventListener - https://coderpad.io/blog/development/addeventlistener-javascript/
  if (isMuted && e.pointerType === 'mouse') {
    isMuted = false;
    gameMusic.play();
    muteButton.innerText = 'Mute Sound';
     
  } 
  else if (!isMuted && e.pointerType === 'mouse'){
    isMuted = true;
    gameMusic.pause();
    muteButton.textContent = 'Unmute Sound';
  }
  
});



//If enter is pressed game starts
//instructions menu gets hidden

let instructionsDisplayed = false;
document.addEventListener('keydown', (event) => {
  if (event.key === 'Enter') {
    if (!gameStarted) {
        
        if (!instructionsDisplayed) {
        // Display instructions for the first time
        instructionsBox.style.display = 'none'; // Hiding the instructions
        instructionsDisplayed = true; // Setting the flag to true
        }
        gameStarted = true;
        startMenu.style.display = 'none'; // Hiding the start menu
        !isMuted && gameMusic.play();
        animate(); // Starting the game loop
    } 
    else if (gameOver) {
      // Restarting the game and resetting values
      gameOver = false;
      startMenu.style.display = 'none';
      numOfOncomingCars = 2;
      OncomingCars.forEach((OncomingCar) => scene.remove(OncomingCar)); //resetting different variables once the game is over
      OncomingCars.length = 0;
      OncomingCarSpeed = 0.20;
      createOncomingCar(-100, lane);
      redCar.position.set(0, 0, 0);
      animate();
    }
  }

  // Moving the redCar left or right only if the game has started
  if (gameStarted && !gameOver) {
    if (event.key === 'ArrowLeft') {
      redCarLane = Math.max(redCarLane - 1, 3);
    }
    if (event.key === 'ArrowRight') {
      redCarLane = Math.min(redCarLane + 1, 4);
    }
  }

  // Moving the blueCar left or right only if the game has started
  if (gameStarted && !gameOver) {
    if (event.key === 'a' || event.key === 'A') { // 'A' or 'a' key for moving left
      blueCarLane = Math.max(blueCarLane - 1, 0);
    }
    if (event.key === 'd' || event.key === 'D') { // 'D' or 'd' key for moving right
      blueCarLane = Math.min(blueCarLane + 1, 1);
    }
  }
});


// this is used to count the time OncomingCars spawning

const baseSpawnInterval = 0.7;

// The minimum time interval between OncomingCar spawns.
// Initially set to the base spawn interval, but changed in game

let minSpawnInterval = baseSpawnInterval;

// Main game loop
function animate() {
  requestAnimationFrame(animate);

  if (gameStarted && !gameOver) {
    
      if (timeElapsed % 2 === 0) { //speeding up OncomingCar speed as time goes on
      OncomingCarSpeed +=0.015;
      
   }
    //this code here makes to the effect of the cars falling down onto the  road as the game starts
    redCarVelocityY -= 0.01;
    redCarPosition += redCarVelocityY;
    if (redCarPosition <= 0) { // making sure it doesnt go  below the road level
      redCarPosition = 0;
      redCarVelocityY = 0;
    }
    // Updating red car's vertical position
    redCar.position.y = redCarPosition;
    
    // Updating red car's horizontal position based on lane
    redCar.position.x = (redCarLane - 2.09) * laneWidth;

    //this code here makes to the effect of the cars falling down onto the  road as the game starts
    blueCarVelocityY -= 0.01;
    blueCarPosition += blueCarVelocityY;
    if (blueCarPosition <= 0) {// making sure it doesnt go  below the road level
      blueCarPosition = 0;
      blueCarVelocityY = 0;
    }
    // Updating blue car's vertical position
    blueCar.position.y = blueCarPosition;
    
    // Updating blue car's horizontal position based on lane
    blueCar.position.x = (blueCarLane - 1.95) * laneWidth;

    // Updating OncomingCars and collision detection
    OncomingCars.forEach((OncomingCar) => {
     OncomingCar.position.z += OncomingCarSpeed;
      if (OncomingCar.position.z > 5) {
        scene.remove(OncomingCar);
      }
});
    
    
    // Flags to track collision status for each car
    let collidedredCar = false;
    let collidedblueCar = false;
    
    for (const lamp of streetLamps) {
        lamp.position.z += 0.15; // Adjusting the speed as needed
        if (lamp.position.z > lampEndPosition) {
        lamp.position.z = lampStartPosition;} //when the array of oncoming lamps finsihes start it again at the original starting point
    
  }

    for (let i = 0; i < OncomingCars.length; i++) { //looping through OncomingCars and check for collisions with cars
      const OncomingCar = OncomingCars[i];
      
      // Calculate distances between OncomingCar and cars
      const distanceX1 = Math.abs(OncomingCar.position.x - redCar.position.x); //math.abs at W3schools
      const distanceZ1 = Math.abs(OncomingCar.position.z - redCar.position.z);
      const distanceX2 = Math.abs(OncomingCar.position.x - blueCar.position.x);
      const distanceZ2 = Math.abs(OncomingCar.position.z - blueCar.position.z);
    
      // Adjusting collision thresholds based on OncomingCar speed
      const collisionThresholdX = 0.5;
      const collisionThresholdZ = 1.0 + (OncomingCarSpeed * 0.1);

    
    if (distanceX1 < collisionThresholdX && distanceZ1 < collisionThresholdZ) { 
        
        //if the collision detection threshold is greater then the distances between OncomingCars and the red car, red car has crashed 
        collidedredCar = true;
        // console.log("Collision detected with redCar 1");
        createExplosion(OncomingCar.position, redCar.position.x); // creating explosion at colllision position
        OncomingCars.splice(i, 1); // Removing the OncomingCar from the array
        scene.remove(OncomingCar);
        i--;
        gameOver = true;
        gameOverMessage.textContent = 'Blue Wins!';  //Creating the game over message
        gameOverMessage.style.color = 'blue';
        gameOverMessage.style.fontSize = '50px'; 
        gameOverMessage.style.fontWeight = 'bold';
        startMenu.style.display = 'flex'; // Showing the start menu with game over message
        const explosionSound = new Audio('/uploads/james2001/explosionSound.mp3');
       !isMuted && explosionSound.play(); 
    } 
    if (distanceX2 < collisionThresholdX && distanceZ2 < collisionThresholdZ) {
        
         //if the collision detection threshold is greater then the distances between OncomingCars and the blue car, blue car has crashed 
        collidedblueCar = true;
        // console.log("Collision detected with redCar 2");
        createExplosion(OncomingCar.position, blueCar.position.x);  // creating explosion at colllision position
        OncomingCars.splice(i, 1); // Removing the OncomingCar from the array
        scene.remove(OncomingCar);
        i--; 
        gameOver = true;
        gameOverMessage.textContent = 'Red Wins!';  //Creating the game over message
        gameOverMessage.style.color = 'red';
        gameOverMessage.style.fontSize = '50px'; // Adjusting font size as needed
        gameOverMessage.style.fontWeight = 'bold';
        startMenu.style.display = 'flex'; // Showing the start menu with game over message
        const explosionSound = new Audio('/uploads/james2001/explosionSound.mp3');
        !isMuted && explosionSound.play();
    } else {
        // Removing OncomingCars that have moved beyond the camera view
      for (let i = 0; i < OncomingCars.length; i++) {
        const OncomingCar = OncomingCars[i];
        if (OncomingCar.position.z > 5 && OncomingCar.position.z < 10) {
          
          scene.remove(OncomingCar);
        }
      }
    }
}
    // Updating time elapsed and calculating minimum spawn interval
    timeElapsed += 1 / 60; // Incrementing timeElapsed by the fraction of a second
    minSpawnInterval = baseSpawnInterval / OncomingCarSpeed; // Calculating the adjusted minimum spawn interval
    
    if (timeElapsed >= minSpawnInterval) { // Checking if enough time has passed for OncomingCar spawning
      timeElapsed = 0; // Reset the timeElapsed counter



      let laneIndex;
      if (Math.random() < 0.5) {
        // Random index between 0 and 1
        laneIndex = Math.floor(Math.random() * 2); //https://www.programiz.com/javascript/examples/get-random-item
        if (laneIndex === 0) {
          // Adding OncomingCar in lane 0
          createOncomingCar(-100, 0);
        } else {
          // Adding OncomingCar in lane 1
          createOncomingCar(-100, 1);
        }
      } else {
        // Random index between 3 and 4
        laneIndex = Math.floor(Math.random() * 2) + 3; //https://www.programiz.com/javascript/examples/get-random-item
        if (laneIndex === 3) {
          // Adding OncomingCar in lane 3
          createOncomingCar(-100, 3);
        } else {
          // Adding OncomingCar in lane 4
          createOncomingCar(-100, 4);
        }
  }
}
    //continuously outputs scene to user
    renderer.render(scene, camera);
  }
}

// Setting camera position along y and z axis

camera.position.y = 3;
camera.position.z = 7;

animate();