diff --git a/frontend/css/player3d1.css b/frontend/css/player3d1.css new file mode 100644 index 0000000..1f670e6 --- /dev/null +++ b/frontend/css/player3d1.css @@ -0,0 +1,56 @@ +.container { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + +} + +/* Styled game canvas */ +#gameCanvas { + width: 640px; + height: 360px; + border: 5px solid #333; /* Dark border */ + border-radius: 10px; /* Rounded corners */ + background-color: #1a1a1a; /* Dark gray background */ +} + +/* Styled scoreboard */ +#scoreboard { + text-align: center; + font-family: 'Segoe UI', 'Helvetica', 'Ubuntu', sans-serif; + color: #fff; /* White text color */ + margin-top: 20px; /* Spacing above the scoreboard */ +} + +/* Styled scores */ +#scores { + font-size: 4em; /* Larger font size */ + padding: 10px; /* Increased padding */ + margin: 0; + color: #ffc107; /* Yellow text color */ +} + +/* Styled title */ +#title { + background-color: #ffc107; /* Yellow background */ + color: #333; /* Dark text color */ + padding: 10px; /* Padding around the title */ + font-size: 1.5em; /* Larger font size */ + margin-bottom: 20px; /* Spacing below the title */ + border-radius: 5px; /* Rounded corners */ +} + +/* Instructions */ +#instructions { + text-align: center; + color: #fff; /* White text color */ + font-family: 'Segoe UI', 'Helvetica', 'Ubuntu', sans-serif; + margin-top: 20px; /* Spacing above the instructions */ +} + +/* Unique styling for instructions */ +#instructions h3 { + font-size: 1em; /* Font size for instructions */ +} \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index 4468b4a..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - Pong42 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Pong42

- - - - - - -
- -
-
- - - - -
-
-
- - - - - diff --git a/frontend/js/player3d1.js b/frontend/js/player3d1.js index a3dc00e..dd7a8f7 100644 --- a/frontend/js/player3d1.js +++ b/frontend/js/player3d1.js @@ -1,75 +1,637 @@ function showPlayer3d1Page(){ console.log("pong 3d1 page"); - let container; - let renderer, scene, camera; - let ball, paddle1, paddle2, mainLight; - let WIDTH = 700, HEIGHT = 500; - let FIELD_WIDTH = 1200, FIELD_LENGTH = 3000; - - function init() { - container = document.getElementById('container'); - scene = new THREE.Scene(); - setupRenderer(); - setupCamera(); - addLights(); - addObjects(); - window.addEventListener('mousemove', handleMouseMove); - render(); - } + +// --------------------------------------------- // +// ------- 3D PONG built with Three.JS --------- // +// -------- Created by Nikhil Suresh ----------- // +// -------- Three.JS is by Mr. doob ----------- // +// --------------------------------------------- // - function setupRenderer() { - renderer = new THREE.WebGLRenderer({ antialias: true }); - renderer.setSize(WIDTH, HEIGHT); - renderer.setClearColor(0x9999bb, 1); - container.appendChild(renderer.domElement); - } +// ------------------------------------- // +// ------- GLOBAL VARIABLES ------------ // +// ------------------------------------- // - function setupCamera() { - camera = new THREE.PerspectiveCamera(45, WIDTH / HEIGHT, 0.1, 10000); - camera.position.set(0, 100, FIELD_LENGTH / 2 + 500); - camera.lookAt(new THREE.Vector3(0, 0, 0)); - } +// scene object variables +var renderer, scene, camera, pointLight, spotLight; - function addLights() { - mainLight = new THREE.HemisphereLight(0xffffff, 0x003300); - scene.add(mainLight); - } +// field variables +var fieldWidth = 400, fieldHeight = 200; - function addObjects() { - const fieldGeometry = new THREE.BoxGeometry(FIELD_WIDTH, 5, FIELD_LENGTH); - const fieldMaterial = new THREE.MeshLambertMaterial({ color: 0x003300 }); - const field = new THREE.Mesh(fieldGeometry, fieldMaterial); - field.position.set(0, -50, 0); - scene.add(field); +// paddle variables +var paddleWidth, paddleHeight, paddleDepth, paddleQuality; +var paddle1DirY = 0, paddle2DirY = 0, paddleSpeed = 3; - paddle1 = addPaddle(FIELD_LENGTH / 2); - paddle2 = addPaddle(-FIELD_LENGTH / 2); +// ball variables +var ball, paddle1, paddle2; +var ballDirX = 1, ballDirY = 1, ballSpeed = 2; - const ballGeometry = new THREE.SphereGeometry(20, 16, 16); - const ballMaterial = new THREE.MeshLambertMaterial({ color: 0xcc0000 }); - ball = new THREE.Mesh(ballGeometry, ballMaterial); - scene.add(ball); - } +// game-related variables +var score1 = 0, score2 = 0; +// you can change this to any positive whole number +var maxScore = 7; - function addPaddle(zPosition) { - const paddleGeometry = new THREE.BoxGeometry(200, 30, 10); - const paddleMaterial = new THREE.MeshLambertMaterial({ color: 0xcccccc }); - const paddle = new THREE.Mesh(paddleGeometry, paddleMaterial); - paddle.position.z = zPosition; - scene.add(paddle); - return paddle; - } +// set opponent reflexes (0 - easiest, 1 - hardest) +var difficulty = 0.2; + +// ------------------------------------- // +// ------- GAME FUNCTIONS -------------- // +// ------------------------------------- // + +function setup() +{ + // update the board to reflect the max score for match win + document.getElementById("winnerBoard").innerHTML = "   Who gets " + maxScore + " points first wins!"; + + // now reset player and opponent scores + score1 = 0; + score2 = 0; + + // set up all the 3D objects in the scene + createScene(); + + // and let's get cracking! + draw(); +} + +function createScene() { + // set the scene size + var WIDTH = 640, + HEIGHT = 360; + + // set some camera attributes + var VIEW_ANGLE = 50, + ASPECT = WIDTH / HEIGHT, + NEAR = 0.1, + FAR = 10000; + + var c = document.getElementById("gameCanvas"); + + // create a WebGL renderer, camera + // and a scene + renderer = new THREE.WebGLRenderer(); + camera = + new THREE.PerspectiveCamera( + VIEW_ANGLE, + ASPECT, + NEAR, + FAR); + + scene = new THREE.Scene(); + + // add the camera to the scene + scene.add(camera); + + // set a default position for the camera + // not doing this somehow messes up shadow rendering + camera.position.z = 320; + + // start the renderer + renderer.setSize(WIDTH, HEIGHT); + + // attach the render-supplied DOM element + c.appendChild(renderer.domElement); + + // set up the playing surface plane + var planeWidth = fieldWidth, + planeHeight = fieldHeight, + planeQuality = 10; + + // create the paddle1's material + var paddle1Material = + new THREE.MeshLambertMaterial({ + color: 0x1B32C0 + }); + // create the paddle2's material + var paddle2Material = + new THREE.MeshLambertMaterial({ + color: 0xFF4045 + }); + // create the plane's material + var planeMaterial = + new THREE.MeshLambertMaterial({ + color: 0x4BD121 + }); + // create the table's material + var tableMaterial = + new THREE.MeshLambertMaterial({ + color: 0x111111 + }); + // create the pillar's material + var pillarMaterial = + new THREE.MeshLambertMaterial({ + color: 0x534d0d + }); + // create the ground's material + var groundMaterial = + new THREE.MeshLambertMaterial({ + color: 0x888888 + }); + + // create the playing surface plane + var plane = new THREE.Mesh( + + new THREE.PlaneGeometry( + planeWidth * 0.95, // 95% of table width, since we want to show where the ball goes out-of-bounds + planeHeight, + planeQuality, + planeQuality), + + planeMaterial); + + scene.add(plane); + plane.receiveShadow = true; + + var table = new THREE.Mesh( + + new THREE.BoxGeometry( + planeWidth * 1.05, // this creates the feel of a billiards table, with a lining + planeHeight * 1.03, + 100, // an arbitrary depth, the camera can't see much of it anyway + planeQuality, + planeQuality, + 1), + + tableMaterial); + table.position.z = -51; // we sink the table into the ground by 50 units. The extra 1 is so the plane can be seen + scene.add(table); + table.receiveShadow = true; + + // // set up the sphere vars + // lower 'segment' and 'ring' values will increase performance + var radius = 5, + segments = 6, + rings = 6; + + // // create the sphere's material + var sphereMaterial = + new THREE.MeshLambertMaterial({ + color: 0xD43001 + }); + + // Create a ball with sphere geometry + ball = new THREE.Mesh( + + new THREE.SphereGeometry( + radius, + segments, + rings), + + sphereMaterial); + + // // add the sphere to the scene + scene.add(ball); + + ball.position.x = 0; + ball.position.y = 0; + // set ball above the table surface + ball.position.z = radius; + ball.receiveShadow = true; + ball.castShadow = true; + + // // set up the paddle vars + paddleWidth = 10; + paddleHeight = 30; + paddleDepth = 10; + paddleQuality = 1; + + paddle1 = new THREE.Mesh( - function handleMouseMove(event) { - var mouseX = (event.clientX / window.innerWidth) * 2 - 1; - paddle1.position.x = THREE.MathUtils.lerp(-FIELD_WIDTH / 2, FIELD_WIDTH / 2, mouseX + 0.5); + new THREE.BoxGeometry( + paddleWidth, + paddleHeight, + paddleDepth, + paddleQuality, + paddleQuality, + paddleQuality), + + paddle1Material); + + // // add the sphere to the scene + scene.add(paddle1); + paddle1.receiveShadow = true; + paddle1.castShadow = true; + + paddle2 = new THREE.Mesh( + + new THREE.BoxGeometry( + paddleWidth, + paddleHeight, + paddleDepth, + paddleQuality, + paddleQuality, + paddleQuality), + + paddle2Material); + + // // add the sphere to the scene + scene.add(paddle2); + paddle2.receiveShadow = true; + paddle2.castShadow = true; + + // set paddles on each side of the table + paddle1.position.x = -fieldWidth / 2 + paddleWidth; + paddle2.position.x = fieldWidth / 2 - paddleWidth; + + // lift paddles over playing surface + paddle1.position.z = paddleDepth; + paddle2.position.z = paddleDepth; + + // we iterate 10x (5x each side) to create pillars to show off shadows + // this is for the pillars on the left + for (var i = 0; i < 5; i++) { + var backdrop = new THREE.Mesh( + + new THREE.BoxGeometry( + 30, + 30, + 300, + 1, + 1, + 1), + + pillarMaterial); + + backdrop.position.x = -50 + i * 100; + backdrop.position.y = 230; + backdrop.position.z = -30; + backdrop.castShadow = true; + backdrop.receiveShadow = true; + scene.add(backdrop); } + // we iterate 10x (5x each side) to create pillars to show off shadows + // this is for the pillars on the right + for (var i = 0; i < 5; i++) { + var backdrop = new THREE.Mesh( + + new THREE.BoxGeometry( + 30, + 30, + 300, + 1, + 1, + 1), + + pillarMaterial); - function render() { - requestAnimationFrame(render); - // Spiellogik-Updates hier einfügen - renderer.render(scene, camera); + backdrop.position.x = -50 + i * 100; + backdrop.position.y = -230; + backdrop.position.z = -30; + backdrop.castShadow = true; + backdrop.receiveShadow = true; + scene.add(backdrop); } - init(); + // finally we finish by adding a ground plane + // to show off pretty shadows + var ground = new THREE.Mesh( + + new THREE.BoxGeometry( + 1000, + 1000, + 3, + 1, + 1, + 1), + + groundMaterial); + // set ground to arbitrary z position to best show off shadowing + ground.position.z = -132; + ground.receiveShadow = true; + scene.add(ground); + + // // create a point light + pointLight = + new THREE.PointLight(0xF8D898); + + // set its position + pointLight.position.x = -1000; + pointLight.position.y = 0; + pointLight.position.z = 1000; + pointLight.intensity = 2.9; + pointLight.distance = 10000; + // add to the scene + scene.add(pointLight); + + // add a spot light + // this is important for casting shadows + spotLight = new THREE.SpotLight(0xF8D898); + spotLight.position.set(0, 0, 460); + spotLight.intensity = 1.5; + spotLight.castShadow = true; + scene.add(spotLight); + + renderer.shadowMap.enabled = true; +} + + +function draw() +{ + // draw THREE.JS scene + renderer.render(scene, camera); + // loop draw function call + requestAnimationFrame(draw); + + ballPhysics(); + paddlePhysics(); + cameraPhysics(); + playerPaddleMovement(); + opponentPaddleMovement(); +} + +function ballPhysics() +{ + // if ball goes off the 'left' side (Player's side) + if (ball.position.x <= -fieldWidth/2) + { + // CPU scores + score2++; + // update scoreboard HTML + document.getElementById("scores").innerHTML = score1 + "-" + score2; + // reset ball to center + resetBall(2); + matchScoreCheck(); + } + + // if ball goes off the 'right' side (CPU's side) + if (ball.position.x >= fieldWidth/2) + { + // Player scores + score1++; + // update scoreboard HTML + document.getElementById("scores").innerHTML = score1 + "-" + score2; + // reset ball to center + resetBall(1); + matchScoreCheck(); + } + + // if ball goes off the top side (side of table) + if (ball.position.y <= -fieldHeight/2) + { + ballDirY = -ballDirY; + } + // if ball goes off the bottom side (side of table) + if (ball.position.y >= fieldHeight/2) + { + ballDirY = -ballDirY; + } + + // update ball position over time + ball.position.x += ballDirX * ballSpeed; + ball.position.y += ballDirY * ballSpeed; + + // limit ball's y-speed to 2x the x-speed + // this is so the ball doesn't speed from left to right super fast + // keeps game playable for humans + if (ballDirY > ballSpeed * 2) + { + ballDirY = ballSpeed * 2; + } + else if (ballDirY < -ballSpeed * 2) + { + ballDirY = -ballSpeed * 2; + } +} + +// Handles CPU paddle movement and logic +function opponentPaddleMovement() +{ + // Lerp towards the ball on the y plane + paddle2DirY = (ball.position.y - paddle2.position.y) * difficulty; + + // in case the Lerp function produces a value above max paddle speed, we clamp it + if (Math.abs(paddle2DirY) <= paddleSpeed) + { + paddle2.position.y += paddle2DirY; + } + // if the lerp value is too high, we have to limit speed to paddleSpeed + else + { + // if paddle is lerping in +ve direction + if (paddle2DirY > paddleSpeed) + { + paddle2.position.y += paddleSpeed; + } + // if paddle is lerping in -ve direction + else if (paddle2DirY < -paddleSpeed) + { + paddle2.position.y -= paddleSpeed; + } + } + // We lerp the scale back to 1 + // this is done because we stretch the paddle at some points + // stretching is done when paddle touches side of table and when paddle hits ball + // by doing this here, we ensure paddle always comes back to default size + paddle2.scale.y += (1 - paddle2.scale.y) * 0.2; +} + + +// Handles player's paddle movement +function playerPaddleMovement() +{ + // move left + if (Key.isDown(Key.A)) + { + // if paddle is not touching the side of table + // we move + if (paddle1.position.y < fieldHeight * 0.45) + { + paddle1DirY = paddleSpeed * 0.5; + } + // else we don't move and stretch the paddle + // to indicate we can't move + else + { + paddle1DirY = 0; + paddle1.scale.z += (10 - paddle1.scale.z) * 0.2; + } + } + // move right + else if (Key.isDown(Key.D)) + { + // if paddle is not touching the side of table + // we move + if (paddle1.position.y > -fieldHeight * 0.45) + { + paddle1DirY = -paddleSpeed * 0.5; + } + // else we don't move and stretch the paddle + // to indicate we can't move + else + { + paddle1DirY = 0; + paddle1.scale.z += (10 - paddle1.scale.z) * 0.2; + } + } + // else don't move paddle + else + { + // stop the paddle + paddle1DirY = 0; + } + + paddle1.scale.y += (1 - paddle1.scale.y) * 0.2; + paddle1.scale.z += (1 - paddle1.scale.z) * 0.2; + paddle1.position.y += paddle1DirY; +} + +// Handles camera and lighting logic +function cameraPhysics() +{ + // we can easily notice shadows if we dynamically move lights during the game + spotLight.position.x = ball.position.x * 2; + spotLight.position.y = ball.position.y * 2; + + // move to behind the player's paddle + camera.position.x = paddle1.position.x - 100; + camera.position.y += (paddle1.position.y - camera.position.y) * 0.05; + camera.position.z = paddle1.position.z + 100 + 0.04 * (-ball.position.x + paddle1.position.x); + + // rotate to face towards the opponent + camera.rotation.x = -0.01 * (ball.position.y) * Math.PI/180; + camera.rotation.y = -60 * Math.PI/180; + camera.rotation.z = -90 * Math.PI/180; +} + +// Handles paddle collision logic +function paddlePhysics() +{ + // PLAYER PADDLE LOGIC + + // if ball is aligned with paddle1 on x plane + // remember the position is the CENTER of the object + // we only check between the front and the middle of the paddle (one-way collision) + if (ball.position.x <= paddle1.position.x + paddleWidth + && ball.position.x >= paddle1.position.x) + { + // and if ball is aligned with paddle1 on y plane + if (ball.position.y <= paddle1.position.y + paddleHeight/2 + && ball.position.y >= paddle1.position.y - paddleHeight/2) + { + // and if ball is travelling towards player (-ve direction) + if (ballDirX < 0) + { + // stretch the paddle to indicate a hit + paddle1.scale.y = 15; + // switch direction of ball travel to create bounce + ballDirX = -ballDirX; + // we impact ball angle when hitting it + // this is not realistic physics, just spices up the gameplay + // allows you to 'slice' the ball to beat the opponent + ballDirY -= paddle1DirY * 0.7; + } + } + } + + // OPPONENT PADDLE LOGIC + + // if ball is aligned with paddle2 on x plane + // remember the position is the CENTER of the object + // we only check between the front and the middle of the paddle (one-way collision) + if (ball.position.x <= paddle2.position.x + paddleWidth + && ball.position.x >= paddle2.position.x) + { + // and if ball is aligned with paddle2 on y plane + if (ball.position.y <= paddle2.position.y + paddleHeight/2 + && ball.position.y >= paddle2.position.y - paddleHeight/2) + { + // and if ball is travelling towards opponent (+ve direction) + if (ballDirX > 0) + { + // stretch the paddle to indicate a hit + paddle2.scale.y = 15; + // switch direction of ball travel to create bounce + ballDirX = -ballDirX; + // we impact ball angle when hitting it + // this is not realistic physics, just spices up the gameplay + // allows you to 'slice' the ball to beat the opponent + ballDirY -= paddle2DirY * 0.7; + } + } + } +} + +function resetBall(loser) +{ + // position the ball in the center of the table + ball.position.x = 0; + ball.position.y = 0; + + // if player lost the last point, we send the ball to opponent + if (loser == 1) + { + ballDirX = -1; + } + // else if opponent lost, we send ball to player + else + { + ballDirX = 1; + } + + // set the ball to move +ve in y plane (towards left from the camera) + ballDirY = 1; +} + +var bounceTime = 0; +// checks if either player or opponent has reached 7 points +function matchScoreCheck() +{ + // if player has 7 points + if (score1 >= maxScore) + { + // stop the ball + ballSpeed = 0; + // write to the banner + document.getElementById("scores").innerHTML = "Player wins!"; + document.getElementById("winnerBoard").innerHTML = "Refresh to play again"; + // make paddle bounce up and down + bounceTime++; + paddle1.position.z = Math.sin(bounceTime * 0.1) * 10; + // enlarge and squish paddle to emulate joy + paddle1.scale.z = 2 + Math.abs(Math.sin(bounceTime * 0.1)) * 10; + paddle1.scale.y = 2 + Math.abs(Math.sin(bounceTime * 0.05)) * 10; + } + // else if opponent has 7 points + else if (score2 >= maxScore) + { + // stop the ball + ballSpeed = 0; + // write to the banner + document.getElementById("scores").innerHTML = "CPU wins!"; + document.getElementById("winnerBoard").innerHTML = "Refresh to play again"; + // make paddle bounce up and down + bounceTime++; + paddle2.position.z = Math.sin(bounceTime * 0.1) * 10; + // enlarge and squish paddle to emulate joy + paddle2.scale.z = 2 + Math.abs(Math.sin(bounceTime * 0.1)) * 10; + paddle2.scale.y = 2 + Math.abs(Math.sin(bounceTime * 0.05)) * 10; + } +} + +window.addEventListener('keyup', function(event) { Key.onKeyup(event); }, false); +window.addEventListener('keydown', function(event) { Key.onKeydown(event); }, false); + +var Key = { + _pressed: {}, + + A: 65, + W: 87, + D: 68, + S: 83, + SPACE: 32, + + isDown: function(keyCode) { + return this._pressed[keyCode]; + }, + + onKeydown: function(event) { + this._pressed[event.keyCode] = true; + }, + + onKeyup: function(event) { + delete this._pressed[event.keyCode]; + } +}; + + setup(); + }; \ No newline at end of file diff --git a/frontend/views/player3d1.html b/frontend/views/player3d1.html index eb4de33..6ff46fd 100644 --- a/frontend/views/player3d1.html +++ b/frontend/views/player3d1.html @@ -1 +1,16 @@ -
\ No newline at end of file + +
+
+

A - Move Left

+

D - Move Right

+
+ +
+
+

0-0

+

3D PONG

+

  Who gets 7 points first wins!

+
+ +
+