Code viewer for World: Run Run Run!
// Creates a new Scene
var scene = new THREE.Scene();

// =============================================================================================
// =============================================================================================
//                                          AUDIO
// =============================================================================================
// =============================================================================================
const SPEED1 = "/uploads/duarte/swingx1.mp3";
const SPEED2 = "/uploads/duarte/swingx1.25.mp3";
const SPEED3 = "/uploads/duarte/swingx1.5.mp3";
const HONK = "/uploads/duarte/car-honk.mp3";
const SPEEDUP = "/uploads/duarte/speed-up.mp3";

var speed1 = new Audio(SPEED1);
var speed2 = new Audio(SPEED2);
var speed3 = new Audio(SPEED3);
var honk = new Audio(HONK);
var speedUp = new Audio(SPEEDUP);

var isMuted = false;

// Function to sets volume of each sound
function setVolume() {
    if (isMuted) { // Mutes all sounds
        speed1.volume = 0;
        speed2.volume = 0;
        speed3.volume = 0;
        honk.volume = 0;
        speedUp.volume = 0;
    } else { // Unmutes all sounds
        speed1.volume = 0.1;
        speed2.volume = 0.1;
        speed3.volume = 0.1;
        honk.volume = 0.1;
        speedUp.volume = 0.1;
    }
}

// Calls function to set volumes
setVolume();

// =============================================================================================
// =============================================================================================
//                                          VARIABLES
// =============================================================================================
// =============================================================================================

let password = "753";

// Scoreboard Variables
var score1 = 0;
var hscore1 = 0;
var score2 = 0;
var hscore2 = 0;

// Lanes variables
const laneTypes = ['car', 'forest'];
const vechicleColors = [0xFFFFFF, 0x000000, 0xFF0000, 0xFFFF00, 0x0000FF, 0x9400D3, 0xFFA500];
const laneSpeeds = [2, 3, 5, 6, 7];
const bushHeights = [15, 25, 35];
const rocksHeights = [5, 7, 12];
let lanes;
let currentLane;
let currentColumn;

// Movement Variable
const positionWidth = 42;
const columns = 17;
const boardWidth = positionWidth * columns;
const stepTime = 50;
const zoom = 2;
let previousTimestamp;
let startMoving;
let moves;
let stepStartTimestamp;


// Player settings
var player;
const playerSize = 15;

// Lights Settings
const initialDirLightPositionX = -100;
const initialDirLightPositionY = -100;
var d = 500;


// Websockets Variables
var host;
var playerJoined = false;
var playerLeft = false;
var lockControls = false;
var clock;

// Manages Sound Speed UPs
var onSpeedUp1 = true;
var onSpeedUp2 = true;

// =============================================================================================
// =============================================================================================
//                                          CAMERA
// =============================================================================================
// =============================================================================================
const distance = 500; // Distance for the camera

// Creates camera and assigns position
const camera = new THREE.OrthographicCamera(window.innerWidth / -2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / -2, 0.1, 10000);
camera.rotation.x = 50 * Math.PI / 180;
camera.rotation.y = 20 * Math.PI / 180;
camera.rotation.z = 10 * Math.PI / 180;

const initialCameraPositionY = -Math.tan(camera.rotation.x) * distance;
const initialCameraPositionX = Math.tan(camera.rotation.y) * Math.sqrt(distance ** 2 + initialCameraPositionY ** 2);
camera.position.y = initialCameraPositionY;
camera.position.x = initialCameraPositionX;
camera.position.z = distance;


// =============================================================================================
// =============================================================================================
//                                        INITIALISE VALUES
// =============================================================================================
// =============================================================================================
const initaliseValues = () => { // Function to initialise values

    player = new Player(); // Creates player
    scene.add(player);     // Adds the player to the scene  

    hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6); // Creates a new Hemisphere Light
    scene.add(hemiLight);                 // Adds the Hemisphere Light to the scene
    dirLight = new THREE.DirectionalLight(0xffffff, 0.6);      // Creates a new Directional Light
    dirLight.position.set(initialDirLightPositionX, initialDirLightPositionY, 200); // Sets the Directional Light position
    dirLight.target = player;          // Sets the Directional Light target
    scene.add(dirLight);             // Adds the Directional Light to the scene


    backLight = new THREE.DirectionalLight(0x000000, .4);   // Creates a new Directional Light
    backLight.position.set(200, 200, 50);   // Sets the Directional Light position
    scene.add(backLight);                 // Adds the Directional Light to the scene

    lanes = generateLanes(); // Generates the lanes

    currentLane = 0;     // Sets the current lane to 0
    currentColumn = Math.floor(columns / 2);    // Sets the current column to the middle of the board

    previousTimestamp = null;   // Sets the previous timestamp to null

    startMoving = false;    // Sets start moving to false
    moves = [];        // Sets moves to an empty array
    stepStartTimestamp;  // Sets the step start timestamp to null

    player.position.x = 0;  // Sets the player position to 0
    player.position.y = 0;  // Sets the player position to 0

    camera.position.y = initialCameraPositionY; // Sets the camera position to the initial camera position
    camera.position.x = initialCameraPositionX; // Sets the camera position to the initial camera position

    dirLight.position.x = initialDirLightPositionX; // Sets the Directional Light position to the initial Directional Light position
    dirLight.position.y = initialDirLightPositionY; // reset lighting to follow player

    requestAnimationFrame(animate); // Calls the animate function
    updateHtml(); // Calls the update html function

    speed1.play(); // Plays the speed 1 sound
}

// =============================================================================================
// =============================================================================================
//                                             LOAD RESOURCES 
// =============================================================================================
// =============================================================================================
function loadResources() { // Function to load resources
    AB.socketStart(); // Calls the socket start function

    AB.removeSplash(); // Removes the splash screen
    if (playerJoined) { // If the player has joined
        initaliseValues(); // Calls the initialise values function
    } else { // If the player has not joined
        waitingForPlayers(); // Calls the waiting for players function
    }
}


// =============================================================================================
// =============================================================================================
//                                              GAME 
// =============================================================================================
// =============================================================================================  
const renderer = new THREE.WebGLRenderer({  // Creates renderer
    alpha: true,
    antialias: true
});
renderer.shadowMap.enabled = true; // Enables shadows
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Soft shadows
renderer.setSize(window.innerWidth, window.innerHeight); // Sets size of renderer
document.body.appendChild(renderer.domElement); // Adds renderer to the body of the document

// =============================================================================================
// =============================================================================================
//                                              SOCKETS 
// =============================================================================================
// =============================================================================================
// function to handle socket out to the other user
function socketOut(_name, _joined, _closed) {   
    AB.socketOut({  
        name: _name,
        data: {
            joined: _joined,
            closed: _closed,
            host: host,
            clock: clock,
            score2: score1,
            hscore2: hscore1,
        }
    });
}

// =============================================================================================
// =============================================================================================
//                                             ON USER LEAVE  
// =============================================================================================
// =============================================================================================
window.onbeforeunload = function () {
    socketOut("playerLeft", false, true); // Remove player from game when leaving the page
}


// =============================================================================================
// =============================================================================================
//                                              SOCKET USER LIST  
// =============================================================================================
// =============================================================================================
AB.socketUserlist = function (e) {  // Function to handle socket user list
    // console.log(e);                 // Logs the event
    if (e.length % 2 === 0 && !playerJoined && host === undefined) {    // If there are an even number of players and the player has not joined and the host is undefined
        // Join game
        host = Math.floor(Math.random() * 999999999);   // Generate random host number
        // console.log(host);                 // Set host
        playerJoined = true;    // Sets player joined to true

        initaliseValues();  // Calls the initialise values function
        AB.removeSplash();  //  Removes the splash screen
        clock = new Clock();    // Creates a new clock
        clock.start();  // Starts the clock

        socketOut("triggerJoin", true, false);  // Send socket to other player
        // console.log("Join game");
    } else if (e.length % 2 === 1 && !playerJoined && host === undefined) { // If there is an odd number of players and the player has not joined and the host is undefined
        // Create game
        host = AB.socket.id;     // Sets the host to the socket id
        // console.log("Create game"); // Creates a new
    }

    switch (e.length) { // Switch statement to handle the number of players
        default:    // Default case
            if (void 0 !== host) {  // If the host is not undefined
                socketOut("start", false, true);    // Calls the socket out function
            }   
    }

    AB.msg("<p>Current number of players = " + e.length + "\n</p>", 2);     // Display number of players
}


// =============================================================================================
// =============================================================================================
//                                              SOCKET IN  
// =============================================================================================
// =============================================================================================  
AB.socketIn = function (element) {
    // console.log("TRIGGER SOCKET IN ", element.name, element.data);

    if (element.name === "playerLeft" && element.data.host === host && playerJoined) {
        lockControls = true;    // Lock controls
        playerLeft = true;      // Player left
        AB.socket.destroy();    // Destroy socket
        endGame();              // End game
        resetPlayer();          // Reset player
    } else if (element.name === "update" && element.data.host === host && playerJoined) {
        score2 = element.data.score2; // Updates score
        hscore2 = element.data.hscore2; // Updates highscore
        updateHtml();   // update HTML side screen
    } else if (element.name === "triggerJoin" && element.data.joined === true && !playerJoined) {
        playerJoined = true;    // sets playerJoined to true
        clock = element.data.clock; // gets clock from host
        host = element.data.host;   // gets host id
        initaliseValues();      // starts game
        AB.removeSplash();      // removes splash screen
    } else if (element.name === "clockUpdate" && element.data.host === host && playerJoined) {
        clock = element.data.clock; // Update clock
        AB.msg("<p>Time left: <strong>" + clock.currTime + "</strong> seconds</p>", 5); // Update time left screen
    } else if (element.name === "endGame" && element.data.host === host && playerJoined) { // End game
        score2 = element.data.score2; // Update score
        hscore2 = element.data.hscore2; // Update highscore
        lockControls = true; // Lock controls
        AB.socket.destroy(); // Destroy socket
        endGame(); // End game
        resetPlayer(); // Reset player
    }
};

// =============================================================================================
// =============================================================================================
//                                              CLOCK 
// =============================================================================================
// =============================================================================================
class Clock { // Clock class
    constructor() { // Constructor  
        this.currTime = 60  // Current time
    }

    start() {   // Start clock
        var clock = this;   // Set clock to this
        function _time() {  // Function to update time
            clock.currTime--;
            if (clock.currTime < 0) {
                socketOut("endGame", false, false); // Send end game to other player
                lockControls = true;    // Lock controls
                AB.socket.destroy();    // Destroy socket
                endGame();              // End game
                resetPlayer();          // Reset player
                return;                 // Stop clock
            }
            AB.msg("<p>Time left: <strong>" + clock.currTime + "</strong> seconds</p>", 5); // Update time

            socketOut("clockUpdate", false, false); // Send clock update to other player
        }
        setInterval(_time, 1000);   // Set interval to 1 second
    }
}


// =============================================================================================
// =============================================================================================
//                                              PLAYER 
// =============================================================================================
// =============================================================================================
function Player() { // Player class
    const player = new THREE.Group();   // Create a group to hold the player

    const body = new THREE.Mesh(    // Create the body  
        new THREE.BoxBufferGeometry((playerSize - 1) * zoom, (playerSize - 1) * zoom, (playerSize - 1) * zoom), 
        new THREE.MeshPhongMaterial({ color: 0xF0B8A0 })
    );
    body.position.z = 10 * zoom; // Move the body up
    body.position.z += 15;       // Move the body up
    player.add(body);            // Adds the body to the group

    const hair = new THREE.Mesh(    // Create the hair
        new THREE.BoxBufferGeometry((playerSize - 0.8) * zoom, (playerSize - 0.8) * zoom, 2 * zoom), 
        new THREE.MeshLambertMaterial({ color: 0x000000 })
    );
    hair.position.z = 21 * zoom;    // Move the hair up
    hair.position.z += 6;           // Move the hair up
    player.add(hair);               // Add the hair to the group

    const pants = new THREE.Mesh(    // Create the pants
        new THREE.BoxBufferGeometry(playerSize * zoom, playerSize * zoom, 20 * zoom),
        new THREE.MeshLambertMaterial({ color: 0x435F9A })
    );
    pants.position.z = 10 * zoom / 2;   // Move the pants up
    player.add(pants);                  // Add the pants to the group

    return player;    // Returns the group
}


// =============================================================================================
// =============================================================================================
//                                              CAR 
// =============================================================================================
// =============================================================================================
function Wheel() {  // Wheel
    const wheel = new THREE.Mesh(    // Creates a new mesh  for the wheel
        new THREE.BoxBufferGeometry(12 * zoom, 33 * zoom, 12 * zoom),   // Creates a new box geometry for the wheel
        new THREE.MeshLambertMaterial({ color: 0x333333 })  // Creates a new material for the wheel
    );
    wheel.position.z = 6 * zoom;    // Sets the position of the wheel
    return wheel;   // Returns the wheel
}

function Car() {
    const car = new THREE.Group();  // Creates a new group
    const carFrontTexture = new Texture(40, 80, [{ x: 0, y: 10, w: 30, h: 60 }]);   // Creates a new texture
    const carBackTexture = new Texture(40, 80, [{ x: 10, y: 10, w: 30, h: 60 }]);   // Creates a new texture
    const carRightSideTexture = new Texture(110, 40, [{ x: 10, y: 0, w: 50, h: 30 }, { x: 70, y: 0, w: 30, h: 30 }]);   // Creates a new texture
    const carLeftSideTexture = new Texture(110, 40, [{ x: 10, y: 10, w: 50, h: 30 }, { x: 70, y: 10, w: 30, h: 30 }]);  // Creates a new texture
    const color = vechicleColors[Math.floor(Math.random() * vechicleColors.length)];    // Randomly selects a color from the array

    const main = new THREE.Mesh(    // Creates a new mesh
        new THREE.BoxBufferGeometry(60 * zoom, 30 * zoom, 15 * zoom),   // Creates a new box buffer geometry
        new THREE.MeshPhongMaterial({ color })  // Creates a new material
    );  // Creates a new mesh

    main.position.z = 12 * zoom;    // Sets the position of the mesh
    car.add(main)   // Adds the mesh to the group

    const cabin = new THREE.Mesh(   // Creates a new mesh
        new THREE.BoxBufferGeometry(33 * zoom, 24 * zoom, 12 * zoom),   // Creates a new box
        [
            new THREE.MeshPhongMaterial({ color: 0xcccccc, map: carBackTexture }),  // Creates a new material
            new THREE.MeshPhongMaterial({ color: 0xcccccc, map: carFrontTexture }), // Creates a new material
            new THREE.MeshPhongMaterial({ color: 0xcccccc, map: carRightSideTexture }), // Creates a new material
            new THREE.MeshPhongMaterial({ color: 0xcccccc, map: carLeftSideTexture }),  // Creates a new material
            new THREE.MeshPhongMaterial({ color: 0xcccccc, }), // top
            new THREE.MeshPhongMaterial({ color: 0xcccccc, }) // bottom
        ]
    );
    cabin.position.x = 6 * zoom;    // Sets the position of the mesh
    cabin.position.z = 25.5 * zoom; // Sets the position of the mesh
    car.add(cabin);                 // Adds the mesh to the group

    const frontWheel = new Wheel();     // Creates a new wheel
    frontWheel.position.x = -18 * zoom; // Sets the position of the wheel
    car.add(frontWheel);                // Adds the wheel to the group

    const backWheel = new Wheel();      // Creates a new wheel
    backWheel.position.x = 18 * zoom;   // Sets the position of the wheel
    car.add(backWheel);                 // Adds the wheel to the group

    return car; // Returns the group
}

// =============================================================================================
// =============================================================================================
//                                           BUSH
// =============================================================================================
// =============================================================================================
function Bush() {   // Creates a new bush object
    const bush = new THREE.Group(); // Creates a new group

    height = bushHeights[Math.floor(Math.random() * bushHeights.length)]; // Randomly selects a height for the bush

    const leaf = new THREE.Mesh(    // Creates a new mesh for the leaves
        new THREE.BoxBufferGeometry(30 * zoom, 30 * zoom, height * zoom),   // Creates a box with the size of the bush
        new THREE.MeshLambertMaterial({ color: 0x37AE0F, }) // Creates a material for the leaves
    );
    leaf.position.z = (height / 2) * zoom;  // Sets the position of the leaves
    bush.add(leaf); // Adds the leaves to the bush

    return bush;    // Returns the bush
}

// =============================================================================================
// =============================================================================================
//                                            ROCK
// =============================================================================================
// =============================================================================================
function Rock() { // Create a new rock object
    const rocks = new THREE.Group(); // Create a new group for the rock

    height = rocksHeights[Math.floor(Math.random() * bushHeights.length)];  // Get a random height for the rock

    const rock = new THREE.Mesh(    // Create the rock
        new THREE.BoxBufferGeometry(12 * zoom, 12 * zoom, height * zoom),   // Create a box with the random height
        new THREE.MeshLambertMaterial({ color: 0x666a6c, })   // Set the color of the rock
    );
    rock.position.z = (height / 2) * zoom;  // Set the position of the rock
    rocks.add(rock);    // Add the rock to the group

    return rocks;       // Return the group
}



// =============================================================================================
// =============================================================================================
//                                            ROAD
// =============================================================================================
// =============================================================================================
function Road() { // Creates a new road object    
    const road = new THREE.Group(); // Creates a new group for the road

    const createSection = color => new THREE.Mesh(  // Creates a new section of the road
        new THREE.PlaneBufferGeometry(boardWidth * zoom, positionWidth * zoom), // Creates a new plane with the width of the board and the height of the position
        new THREE.MeshPhongMaterial({ color })  // Creates a new material with the color of the road
    );  

    const middle = createSection(0x454A59); // Creates the middle section of the road
    road.add(middle);   // Adds the middle section to the road

    const left = createSection(0x393D49);   // Creates a new section
    left.position.x = - boardWidth * zoom;  // Moves the section to the left
    road.add(left); // Adds the section to the road

    const right = createSection(0x393D49);   // Creates a new section
    right.position.x = boardWidth * zoom;   // Moves the section to the right
    road.add(right);                    // Adds the section to the road

    return road;    // Returns the road
}

// =============================================================================================
// =============================================================================================
//                                         GRASS
// =============================================================================================
// =============================================================================================
function Grass() { // Creates a new grass object
    const grass = new THREE.Group(); // Creates a new group

    const createSection = color => new THREE.Mesh( // Creates a new section
        new THREE.BoxBufferGeometry(boardWidth * zoom, positionWidth * zoom, 3 * zoom), // Creates a new box geometry
        new THREE.MeshPhongMaterial({ color })  // Creates a new material
    );  // Creates a new section

    const middle = createSection(0xbaf455); // Creates a new section
    middle.receiveShadow = true;    // Sets the shadow to true
    grass.add(middle);        // Adds the section to the grass group

    const left = createSection(0x99C846);   // Creates a new section
    left.position.x = - boardWidth * zoom;  // Sets the position of the section
    grass.add(left);        // Adds the section to the grass group

    const right = createSection(0x99C846);  // Creates a new section
    right.position.x = boardWidth * zoom;   // Sets the position of the section
    grass.add(right);       // Adds the section to the grass group

    grass.position.z = 1.5 * zoom;  // Moves the grass up a bit
    return grass; // Returns the grass object
}


// =============================================================================================
// =============================================================================================
//                                          LANE
// =============================================================================================
// =============================================================================================

// Generates inital lanes 
const generateLanes = () => [-9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((index) => {
    const lane = new Lane(index); // Creates a new Lane as an object 
    lane.mesh.position.y = index * positionWidth * zoom; // Edit lane y position
    scene.add(lane.mesh);  // Adds it to the scene
    return lane;
}).filter((lane) => lane.index >= 0);

const addLane = () => { // Adds a new lane to the scene
    const index = lanes.length; // Gets next Index to add lane
    const lane = new Lane(index); // Creates a new Lane as an object 
    lane.mesh.position.y = index * positionWidth * zoom; // Edit lane y position 
    scene.add(lane.mesh);  // Adds lane to the scene
    lanes.push(lane);   // Appends new Lane to current Lanes
}

function Lane(index) {
    this.index = index;     // Index of the lane
    this.type = index <= 0 ? 'field' : laneTypes[Math.floor(Math.random() * laneTypes.length)]; // randomly chooses to either create a field or a forest or a car lane

    switch (this.type) {
        case 'field': {  // Makes a new field lane
            this.type = 'field';
            this.mesh = new Grass(); // Makes a grass layer
            break;
        }
        case 'forest': {  // Makes a new forest lane
            this.mesh = new Grass(); // Makes a grass layer

            this.occupiedPositions = new Set(); // Saves the ocuppied spots 
            this.threes = [1, 2, 3, 4].map(() => {  // Creates 4 bushes or rocks at random positions
                var obstacle;   
                if (Math.random() > 0.7) { // Random to add either a bush or a rock
                    obstacle = new Bush(); // Creates a new bush
                } else {
                    obstacle = new Rock(); // Creates a new rock
                }

                let position;   // Saves the position of the obstacle
                do {
                    position = Math.floor(Math.random() * columns); // Randomly chooses a position
                } while (this.occupiedPositions.has(position))  // Checks if the position is already occupied
                this.occupiedPositions.add(position);   // Adds the position to the occupied positions
                obstacle.position.x = (position * positionWidth + positionWidth / 2) * zoom - boardWidth * zoom / 2;    // Sets the x position of the obstacle
                this.mesh.add(obstacle);    // Adds the obstacle to the mesh
                return obstacle; // Returns the new made obstacle
            }
            )
            break;
        }
        case 'car': {   // Makes a new car lane
            this.mesh = new Road(); // Makes a road layer
            this.direction = Math.random() >= 0.5;  // Randomly chooses the direction of the car lane

            const occupiedPositions = new Set(); // Saves the ocuppied spots
            this.vechicles = [1, 2, 3].map(() => {  // Creates 3 cars
                const vechicle = new Car(); // Creates a new car
                let position;   // Creates a new position
                do {
                    position = Math.floor(Math.random() * columns / 2); // Randomly chooses a position
                } while (occupiedPositions.has(position))   // Checks if the position is already taken
                occupiedPositions.add(position);    // Adds the position to the taken positions
                vechicle.position.x = (position * positionWidth * 2 + positionWidth / 2) * zoom - boardWidth * zoom / 2;    // Sets the position of the car
                if (!this.direction) vechicle.rotation.z = Math.PI; // Rotates the car if the direction is false
                this.mesh.add(vechicle);    // Adds the car to the scene
                return vechicle; // Returns the new made obstacle
            })

            this.speed = laneSpeeds[Math.floor(Math.random() * laneSpeeds.length)]; // Randomly chooses a speed for the lane
            if (score1 >= 30) {
                this.speed *= 1.7;  // Increases speed of cars if score is greater than 30
            } else if (score1 >= 10) {
                this.speed *= 1.3;  // Increases the speed of the cars
            }   

            break;
        }
    }
}

// =============================================================================================
// =============================================================================================
//                                          TEXTURE
// =============================================================================================
// =============================================================================================
function Texture(width, height, rects) { // Creates a new texture
    const canvas = document.createElement("canvas"); // Creates a new canvas
    canvas.width = width; canvas.height = height; // Sets the width and height of the canvas
    const context = canvas.getContext("2d"); // Gets the context of the canvas
    context.fillStyle = "#FFFFFF";  // Sets the fill color to white
    context.fillRect(0, 0, width, height);  // Fills the canvas with white
    context.fillStyle = "rgba(0,0,0,0.6)";  // Sets the fill color to black with 60% opacity
    rects.forEach(rect => { // Loops through all the rectangles
        context.fillRect(rect.x, rect.y, rect.w, rect.h); // Fills the rectangle with the black color
    }); 
    return new THREE.CanvasTexture(canvas); // Returns the canvas texture
}


// =============================================================================================
// =============================================================================================
//                                          MOVES
// =============================================================================================
// =============================================================================================
function move(direction) {
    const finalPositions = moves.reduce((position, move) => { // Gets the final position of the player
        if (move === 'forward') return { lane: position.lane + 1, column: position.column };  // If the move is forward, then the lane increases by 1
        if (move === 'backward') return { lane: position.lane - 1, column: position.column }; // If the move is backward, then the lane decreases by 1
        if (move === 'left') return { lane: position.lane, column: position.column - 1 };     // If the move is left, then the column decreases by 1
        if (move === 'right') return { lane: position.lane, column: position.column + 1 };    // If the move is right, then the column increases by 1
    }, { lane: currentLane, column: currentColumn }) // The initial position is the current position of the player  


    if (direction === 'forward') {
        // If the player is trying to move into a bush, then don't move
        if (lanes[finalPositions.lane + 1].type === 'forest' && lanes[finalPositions.lane + 1].occupiedPositions.has(finalPositions.column)) return;
        if (!stepStartTimestamp) startMoving = true;    
        addLane();
    }
    else if (direction === 'backward') {
        if (finalPositions.lane === 0) return;
        // If the player is trying to move into a bush, then don't move
        if (lanes[finalPositions.lane - 1].type === 'forest' && lanes[finalPositions.lane - 1].occupiedPositions.has(finalPositions.column)) return;
        if (!stepStartTimestamp) startMoving = true;
    }
    else if (direction === 'left') {
        if (finalPositions.column === 0) return; // If the player is at the left most column, then don't move 
        // If the player is trying to move into a bush, then don't move
        if (lanes[finalPositions.lane].type === 'forest' && lanes[finalPositions.lane].occupiedPositions.has(finalPositions.column - 1)) return; 
        if (!stepStartTimestamp) startMoving = true;
    }
    else if (direction === 'right') {
        if (finalPositions.column === columns - 1) return; // If the player is at the right most column, then don't move
        // If the player is trying to move into a bush, then don't move
        if (lanes[finalPositions.lane].type === 'forest' && lanes[finalPositions.lane].occupiedPositions.has(finalPositions.column + 1)) return;    
        if (!stepStartTimestamp) startMoving = true;        
    }
    moves.push(direction);  // Adds the direction to the moves array
}

// =============================================================================================
// =============================================================================================
//                                     ANIMATION
// =============================================================================================
// =============================================================================================
function animate(timestamp) {
    requestAnimationFrame(animate);

    if (!previousTimestamp) previousTimestamp = timestamp; // Checks timestamp
    const delta = timestamp - previousTimestamp;  // edits timestamp with the new delta
    previousTimestamp = timestamp; // sets previous timestamp to current timestamp

    // Animate cars moving on the lane
    lanes.forEach(lane => { // Loops through each lane and animates all cars
        if (lane.type === 'car') { // if lane is a car animate it
            const aBitBeforeTheBeginingOfLane = -boardWidth * zoom / 2 - positionWidth * 2 * zoom; // Sets the position of the car
            const aBitAfterTheEndOFLane = boardWidth * zoom / 2 + positionWidth * 2 * zoom; // Sets the end of the lane
            lane.vechicles.forEach(vechicle => { // Loops through each car and animates it
                if (lane.direction) {
                    vechicle.position.x = vechicle.position.x < aBitBeforeTheBeginingOfLane ? aBitAfterTheEndOFLane : vechicle.position.x -= lane.speed / 16 * delta;
                } else {
                    vechicle.position.x = vechicle.position.x > aBitAfterTheEndOFLane ? aBitBeforeTheBeginingOfLane : vechicle.position.x += lane.speed / 16 * delta;
                }
            });
        }
    });

    if (startMoving) { // If start moving is true
        stepStartTimestamp = timestamp;  // sets the timestamp to the start of the step
        startMoving = false; // sets start moving to false
    }

    if (stepStartTimestamp) { // If step start timestamp is true
        // Moves player
        const moveDeltaTime = timestamp - stepStartTimestamp;   // sets the move delta time to the timestamp - the start of the step
        const moveDeltaDistance = Math.min(moveDeltaTime / stepTime, 1) * positionWidth * zoom; // Sets the distance of the player
        switch (moves[0]) {
            case 'forward': { // If move is forward
                const positionY = currentLane * positionWidth * zoom + moveDeltaDistance;   // Sets the position of the player
                camera.position.y = initialCameraPositionY + positionY; // Sets the position of the camera
                dirLight.position.y = initialDirLightPositionY + positionY; // Sets the position of the light
                player.position.y = positionY;  // sets the position of the player
                break;
            }
            case 'backward': { // If move is backward
                positionY = currentLane * positionWidth * zoom - moveDeltaDistance;   // Sets the position of the player
                camera.position.y = initialCameraPositionY + positionY; // Sets camera position
                dirLight.position.y = initialDirLightPositionY + positionY; // Sets the position of the light
                player.position.y = positionY;  // Sets the position of the player
                break;
            }
            case 'left': {  // If move is left
                const positionX = (currentColumn * positionWidth + positionWidth / 2) * zoom - boardWidth * zoom / 2 - moveDeltaDistance;   // Sets the position of the player
                camera.position.x = initialCameraPositionX + positionX; // Sets camera position
                dirLight.position.x = initialDirLightPositionX + positionX; // Sets the position of the light
                player.position.x = positionX;  // Sets the position of the player
                break;
            }
            case 'right': {     // If move is right
                const positionX = (currentColumn * positionWidth + positionWidth / 2) * zoom - boardWidth * zoom / 2 + moveDeltaDistance; // Sets the position of the player
                camera.position.x = initialCameraPositionX + positionX; // Sets camera position
                dirLight.position.x = initialDirLightPositionX + positionX; // Sets the position of the light
                player.position.x = positionX; // Sets the position of the player
                break;
            }
        }

        // Once a step has ended 
        if (moveDeltaTime > stepTime) {
            switch (moves[0]) { // Switches through the moves
                case 'forward': { // If move is forward
                    if (lanes[currentLane].type === 'car') { // If the lane is a car
                        score1++; // Add to score
                    }
                    currentLane++; // Add to current lane
                    break;
                }
                case 'backward': {  // If move is backward
                    currentLane--; // Decreases the current lane
                    if (lanes[currentLane].type === 'car') {    // If the lane is a car
                        score1--; // Decreases the current
                    }
                    break;
                }
                case 'left': {  // If move is left
                    currentColumn--; // Decreases the current column
                    break;
                }
                case 'right': {     // If move is right
                    currentColumn++; // Increases the current column
                    break;
                }
            }
            if (hscore1 < score1) hscore1 = score1; // saves highscore

            if (score1 >= 30 && onSpeedUp2) { // Updates sounds speed to level 2
                speed2.pause();
                speedUp.play();
                onSpeedUp2 = false;
                speed3.play();
            } else if (score1 >= 10 && onSpeedUp1) { // Updates sounds speed to level 1
                speed1.pause();
                speedUp.play();
                speed2.play();
                onSpeedUp1 = false;
            }
            socketOut("update", false, false); // calls websockets
            updateHtml(); // updates HTML sidebar

            // removes the first element in moves
            moves.shift();

            // If more steps are to be taken then restart counter otherwise stop stepping
            stepStartTimestamp = moves.length === 0 ? null : timestamp;
        }
    }

    // Hit test for lanes
    if (lanes[currentLane].type === 'car') {

        const playerMinX = player.position.x - playerSize * zoom / 2; // sets player min x
        const playerMaxX = player.position.x + playerSize * zoom / 2; // sets player min y

        const vechicleLength = 60;

        lanes[currentLane].vechicles.forEach(vechicle => { // Loops through each vechicle in the lane
            const carMinX = vechicle.position.x - vechicleLength * zoom / 2; // sets car min x
            const carMaxX = vechicle.position.x + vechicleLength * zoom / 2; // sets car max x
            if (playerMaxX > carMinX && playerMinX < carMaxX) { // if player is in the same x as the car
                if (hscore1 < score1) hscore1 = score1; // saves highscore
                speed1.pause(); // pauses sound
                speed2.pause(); // pauses sound
                speed3.pause(); // pauses sound
                honk.play();  // plays honk sound
                onSpeedUp1 = true; // resets speed up
                onSpeedUp2 = true; // resets speed up
                score1 = 0; // resets score
                resetPlayer();  // resets player
                socketOut("update", false, false); // calls websockets
            }
        });
    }

    renderer.render(scene, camera); // Updates scene and camera
}

// =============================================================================================
// =============================================================================================
//                                     RESET PLAYERS
// =============================================================================================
// =============================================================================================
function resetPlayer() {
    currentLane = 0; // sets the current lane
    currentColumn = Math.floor(columns / 2); // sets the current collumn

    previousTimestamp = null; // reset timestamp

    startMoving = false; // reset startMoving
    moves = []; // reset moves
    stepStartTimestamp; // reset stepStartTimestamp

    player.position.x = 0; // reset players x position
    player.position.y = 0; // reset players y position

    camera.position.y = initialCameraPositionY;  // reset cameras y position
    camera.position.x = initialCameraPositionX;  // reset cameras y position

    dirLight.position.x = initialDirLightPositionX; // reset lighting to follow player
    dirLight.position.y = initialDirLightPositionY; // reset lighting to follow player

    updateHtml();  // update HTML
    speed1.play(); // play speed1 song
}

// =============================================================================================
// =============================================================================================
//                                  HANDLE KEY PRESSES
// =============================================================================================
// =============================================================================================
window.addEventListener("keyup", event => { // Handles key ups
    if (lockControls) return;
    if (event.keyCode == '38' || event.keyCode == '87') {
        // up arrow OR W
        move('forward');  // moves foward
    }
    else if (event.keyCode == '40' || event.keyCode == '83') {
        // down arrow OR S
        move('backward');  // moves backward
    }
    else if (event.keyCode == '37' || event.keyCode == '65') {
        // left arrow OR A
        move('left');  // moves to the left
    }
    else if (event.keyCode == '39' || event.keyCode == '68') {
        // right arrow OR D
        move('right');  // moves to the right
    }
    else if (event.keyCode == '77') {
        // Mute or Unmute
        isMuted = !isMuted; // changes to muted or unmuted
        setVolume(); // changes volume of sounds

    }
});

// =============================================================================================
// =============================================================================================
//                                        SIDE SCREEN HTML INFO 
// =============================================================================================
// =============================================================================================
function updateHtml() {
    // Creates HTML for the side screen
    AB.msg(`<h3> Welcome to our game!</h3>`, 1);
    AB.msg(`<p><strong>Player 1</strong> Score: <strong>` + score1 + `</strong> | Highest: <strong>` + hscore1 + `</strong> </p>`, 3);
    AB.msg(`<p><strong>Player 2</strong> Score: <strong>` + score2 + `</strong> | Highest: <strong>` + hscore2 + `</strong> </p>`, 4);
    AB.msg(`<p><strong>Controls</strong><br>
    <strong>Arrow Up/W</strong> - Move Up<br>
    <strong>Arrow Left/A</strong> - Move Left<br>
    <strong>Arrow Down/S</strong> - Move Down<br>
    <strong>Arrow Right/D</strong> - Move Right</p>`, 7);
    AB.msg(`<p>Press <strong>M</strong> to mute </p>`, 8);

}


// =============================================================================================
// =============================================================================================
//                                        WAITING FOR PLAYERS 
// =============================================================================================
// =============================================================================================
function endGame() {
    speed1.pause(); // Pauses music
    speed2.pause(); // Pauses music
    speed3.pause(); // Pauses music

    AB.removeSplash(); // Removes old splashed
    AB.newSplash(); // Creates new splash

    var txt;
    var other = "";
    if (playerLeft) {
        // Other player left
        txt = "Other Player has left.<br><br><span style='color: rgb(0, 255, 0);'>You Won!</span>";
    } else if (hscore1 > hscore2) {
        // You Win
        txt = "<span style='color: rgb(0, 255, 0);'>You Won!</span>";
        other = `Your opponent's highscore was: ` + hscore2 + ` <br>`;
    } else if (hscore2 > hscore1) {
        // Other Player Wins
        txt = "<span style='color: rgb(255, 0, 0);'>You Lost!</span>";
        other = `Your opponent's highscore was: ` + hscore2 + ` <br>`;
    } else {
        // Draw
        txt = "It's a draw!";
        other = `Your opponent's highscore was: ` + hscore2 + ` <br>`;
    }
    
    
    
    AB.socket.destroy();    // Destroy socket

    // Creates HTML for the splash screen  
    AB.splashHtml(` 
    <h1> GAME OVER... </h1>  
    <h3>` + txt + `<br></h3>
    Your highest score was: ` + hscore1 + `<br>
    `+ other + `<br>
    <button onclick='location.reload();'  class=ab-largenormbutton >Play Again</button>` );
}

// =============================================================================================
// =============================================================================================
//                                        WAITING FOR PLAYERS 
// =============================================================================================
// =============================================================================================
function waitingForPlayers() {
    AB.removeSplash(); // Removes old splashed
    AB.newSplash();    // Creates new splash                     

    // Creates HTML for the splash screen 
    AB.splashHtml(` 
    <h1> Waiting for opponent, please wait... </h1>
    <img src="/uploads/wuk/loading-buffering.gif">
    ` );
}

// =============================================================================================
// =============================================================================================
//                                              WELCOME 
// =============================================================================================
// =============================================================================================
function welcome() {
    AB.removeSplash(); // Removes old splashed
    AB.newSplash();    // Creates new splash                

    // Creates HTML for the splash screen 
    AB.splashHtml(` 
    <h1 style="font-family:cooper"> RUN RUN RUN! </h1>
    Run as far as you can in 60 seconds and beat your opponent's highscore to win!  <br>
    Avoid all the cars! If you get hit you will start again. <br>
    <br>
    <b>Controls:</b>  <br>
    Arrow Up/W - Move Up        <br>
    Arrow Left/A - Move Left    <br>
    Arrow Down/S - Move Down    <br>
    Arrow Right/D - Move Right  <br>
    <br>
    <b>GOOD LUCK! <br>
    <p>
    <button onclick='loadResources();'  class=ab-largenormbutton >Search for match</button>
    <p>  
    <div id=errordiv name=errordiv> </div> ` );
}

welcome();