//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 webpageconst 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 menuconst 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 boxconst 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;">ArrowRight</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 overconst 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 menuconst 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 displaysconst 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/setSizeconst 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 appconst backgroundTexture =new THREE.TextureLoader().load('/uploads/james2001/sky.png');//https://threejs.org/docs/#examples/en/loadersconst 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 appealconst directionalLight =new THREE.DirectionalLight(0xffffff,1);//https://threejs.org/docs/#api/en/lights/DirectionalLight
directionalLight.position.set(1,1,1);// Setting the light's positionconst 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 onconst 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 sceneconst 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-jsconst 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 lineconst 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 creationconstOncomingCars=[];// 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 OncomingCarfunction 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.comconst 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;}elseif(lane ===3|| lane ===4){// Placing OncomingCar in the same position as redCar 1
lanePosition =(redCarLane -2.09)* laneWidth;}OncomingCar.position.x = lanePosition;// Set lane positionOncomingCar.position.z =-100;// Set initial position
scene.add(OncomingCar);// Add OncomingCars to the sceneOncomingCars.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/SpriteMaterialconst 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 animationconst startTimestamp = performance.now();//https://developer.mozilla.org/en-US/docs/Web/API/Performance/nowfunction 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 lampconst 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 MTLconst lampSpacing =10;// Spacing between lamps on z-axisconst lampStartPosition =-100;// Starting position on z- axisconst lampEndPosition =100;// Ending position on z axisconst 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 lampconst 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-positionfunction 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 lampconst 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 roadconst 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 loopedconst gameMusic =newAudio('/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';}elseif(!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}elseif(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 overOncomingCars.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 startedif(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 startedif(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 spawningconst 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 loopfunction animate(){
requestAnimationFrame(animate);if(gameStarted &&!gameOver){if(timeElapsed %2===0){//speeding up OncomingCar speed as time goes onOncomingCarSpeed+=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 detectionOncomingCars.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 neededif(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 carsconstOncomingCar=OncomingCars[i];// Calculate distances between OncomingCar and carsconst distanceX1 =Math.abs(OncomingCar.position.x - redCar.position.x);//math.abs at W3schoolsconst 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 speedconst 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 positionOncomingCars.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 messageconst explosionSound =newAudio('/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 positionOncomingCars.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 messageconst explosionSound =newAudio('/uploads/james2001/explosionSound.mp3');!isMuted && explosionSound.play();}else{// Removing OncomingCars that have moved beyond the camera viewfor(let i =0; i <OncomingCars.length; i++){constOncomingCar=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 intervalif(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-itemif(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-itemif(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();