Spaces:
Running
Running
| // Game variables | |
| let scene, camera, renderer; | |
| let sphere, playerPaddle, computerPaddle, ball; | |
| let playerScore = 0; | |
| let computerScore = 0; | |
| let gameActive = false; | |
| let clock = new THREE.Clock(); | |
| // Paddle properties | |
| const PADDLE_WIDTH = 0.5; | |
| const PADDLE_HEIGHT = 0.3; | |
| const PADDLE_DEPTH = 0.1; | |
| const PADDLE_SPEED = 0.08; | |
| // Ball properties | |
| const BALL_RADIUS = 0.05; | |
| let ballVelocity = new THREE.Vector3(0.03, 0.03, 0.03); | |
| // Sphere arena properties | |
| const SPHERE_RADIUS = 3; | |
| const SPHERE_SEGMENTS = 32; | |
| const SPHERE_RINGS = 32; | |
| // Rotation controls | |
| let sphereRotationX = 0; | |
| let sphereRotationY = 0; | |
| let sphereRotationSpeed = 0.02; | |
| let autoRotate = true; | |
| // DOM Elements | |
| const playerScoreElement = document.getElementById('playerScore'); | |
| const computerScoreElement = document.getElementById('computerScore'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| const playAgainBtn = document.getElementById('playAgainBtn'); | |
| const gameOverScreen = document.getElementById('gameOverScreen'); | |
| const winnerText = document.getElementById('winnerText'); | |
| // Keyboard state tracking | |
| const keys = { | |
| w: false, | |
| s: false, | |
| q: false, | |
| e: false | |
| }; | |
| // Initialize Three.js scene | |
| function init() { | |
| // Create scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x0c0c2d); | |
| // Create camera | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(0, 0, 7); | |
| // Create renderer | |
| renderer = new THREE.WebGLRenderer({ | |
| canvas: document.getElementById('gameCanvas'), | |
| antialias: true | |
| }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| // Create spherical arena | |
| createSphereArena(); | |
| // Create paddles | |
| createPaddles(); | |
| // Create ball | |
| createBall(); | |
| // Add lighting | |
| addLighting(); | |
| // Event listeners | |
| setupEventListeners(); | |
| // Start animation loop | |
| animate(); | |
| } | |
| // Create the spherical playing arena | |
| function createSphereArena() { | |
| const geometry = new THREE.SphereGeometry(SPHERE_RADIUS, SPHERE_SEGMENTS, SPHERE_RINGS); | |
| const material = new THREE.MeshBasicMaterial({ | |
| color: 0x1a1a40, | |
| wireframe: true, | |
| transparent: true, | |
| opacity: 0.5 | |
| }); | |
| sphere = new THREE.Mesh(geometry, material); | |
| scene.add(sphere); | |
| // Add center dividing planes | |
| const planeGeometry = new THREE.PlaneGeometry(SPHERE_RADIUS * 1.8, SPHERE_RADIUS * 1.8); | |
| const planeMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0x00ffff, | |
| wireframe: true, | |
| transparent: true, | |
| opacity: 0.2, | |
| side: THREE.DoubleSide | |
| }); | |
| // Vertical center plane | |
| const verticalPlane = new THREE.Mesh(planeGeometry, planeMaterial); | |
| verticalPlane.rotation.x = Math.PI / 2; | |
| scene.add(verticalPlane); | |
| // Horizontal center plane | |
| const horizontalPlane = new THREE.Mesh(planeGeometry, planeMaterial); | |
| scene.add(horizontalPlane); | |
| } | |
| // Create player and computer paddles as curved surfaces on the sphere | |
| function createPaddles() { | |
| // Player paddle (right side) | |
| const playerGeometry = new THREE.BoxGeometry(PADDLE_WIDTH, PADDLE_HEIGHT, PADDLE_DEPTH); | |
| const playerMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); | |
| playerPaddle = new THREE.Mesh(playerGeometry, playerMaterial); | |
| playerPaddle.position.x = SPHERE_RADIUS - 0.1; | |
| scene.add(playerPaddle); | |
| // Computer paddle (left side) | |
| const computerGeometry = new THREE.BoxGeometry(PADDLE_WIDTH, PADDLE_HEIGHT, PADDLE_DEPTH); | |
| const computerMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); | |
| computerPaddle = new THREE.Mesh(computerGeometry, computerMaterial); | |
| computerPaddle.position.x = -(SPHERE_RADIUS - 0.1); | |
| scene.add(computerPaddle); | |
| } | |
| // Create the ball | |
| function createBall() { | |
| const geometry = new THREE.SphereGeometry(BALL_RADIUS, 16, 16); | |
| const material = new THREE.MeshBasicMaterial({ color: 0xffff00 }); | |
| ball = new THREE.Mesh(geometry, material); | |
| resetBall(); | |
| scene.add(ball); | |
| } | |
| // Add lighting to the scene | |
| function addLighting() { | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(1, 1, 1); | |
| scene.add(directionalLight); | |
| const pointLight = new THREE.PointLight(0x4d79ff, 1, 20); | |
| pointLight.position.set(5, 5, 5); | |
| scene.add(pointLight); | |
| } | |
| // Reset ball to center | |
| function resetBall() { | |
| ball.position.set(0, 0, 0); | |
| // Random direction | |
| const angleXY = Math.random() * Math.PI * 2; | |
| const angleZ = (Math.random() - 0.5) * Math.PI; | |
| const speed = 0.03; | |
| ballVelocity.set( | |
| Math.cos(angleXY) * Math.cos(angleZ) * speed, | |
| Math.sin(angleXY) * Math.cos(angleZ) * speed, | |
| Math.sin(angleZ) * speed | |
| ); | |
| } | |
| // Update player paddle based on keyboard input | |
| function updatePlayerPaddle() { | |
| if (!gameActive) return; | |
| // Move paddle vertically (W/S keys) | |
| if (keys.w) { | |
| playerPaddle.position.y += PADDLE_SPEED; | |
| } | |
| if (keys.s) { | |
| playerPaddle.position.y -= PADDLE_SPEED; | |
| } | |
| // Constrain paddle to sphere surface | |
| constrainPaddleToSphere(playerPaddle); | |
| } | |
| // Update computer paddle AI | |
| function updateComputerPaddle() { | |
| if (!gameActive) return; | |
| // Simple AI - follow the ball with some delay | |
| const targetY = ball.position.y; | |
| const currentY = computerPaddle.position.y; | |
| if (currentY < targetY - 0.1) { | |
| computerPaddle.position.y += PADDLE_SPEED * 0.8; // Slightly slower than player | |
| } else if (currentY > targetY + 0.1) { | |
| computerPaddle.position.y -= PADDLE_SPEED * 0.8; | |
| } | |
| // Constrain paddle to sphere surface | |
| constrainPaddleToSphere(computerPaddle); | |
| } | |
| // Constrain paddle to sphere surface | |
| function constrainPaddleToSphere(paddle) { | |
| // Keep paddle on sphere surface | |
| const distanceFromCenter = Math.sqrt( | |
| paddle.position.x * paddle.position.x + | |
| paddle.position.y * paddle.position.y + | |
| paddle.position.z * paddle.position.z | |
| ); | |
| if (Math.abs(distanceFromCenter - SPHERE_RADIUS) > 0.1) { | |
| const scale = SPHERE_RADIUS / distanceFromCenter; | |
| paddle.position.x *= scale; | |
| paddle.position.y *= scale; | |
| paddle.position.z *= scale; | |
| } | |
| // Limit paddle movement to prevent it from going through the sphere | |
| const maxDistance = SPHERE_RADIUS - Math.max(PADDLE_WIDTH, PADDLE_HEIGHT) / 2; | |
| const currentDistance = Math.sqrt( | |
| paddle.position.x * paddle.position.x + | |
| paddle.position.y * paddle.position.y + | |
| paddle.position.z * paddle.position.z | |
| ); | |
| if (currentDistance > maxDistance) { | |
| const scale = maxDistance / currentDistance; | |
| paddle.position.x *= scale; | |
| paddle.position.y *= scale; | |
| paddle.position.z *= scale; | |
| } | |
| } | |
| // Update ball position and handle collisions | |
| function updateBall() { | |
| if (!gameActive) return; | |
| // Move ball | |
| ball.position.add(ballVelocity); | |
| // Check for collisions with paddles | |
| checkPaddleCollisions(); | |
| // Check for scoring | |
| checkScoring(); | |
| // Keep ball on sphere surface | |
| constrainBallToSphere(); | |
| } | |
| // Check for collisions with paddles | |
| function checkPaddleCollisions() { | |
| // Player paddle collision | |
| if ( | |
| Math.abs(ball.position.x - playerPaddle.position.x) < (PADDLE_WIDTH/2 + BALL_RADIUS) && | |
| Math.abs(ball.position.y - playerPaddle.position.y) < (PADDLE_HEIGHT/2 + BALL_RADIUS) && | |
| Math.abs(ball.position.z - playerPaddle.position.z) < (PADDLE_DEPTH/2 + BALL_RADIUS) | |
| ) { | |
| // Reflect ball and add some randomness | |
| ballVelocity.x = -Math.abs(ballVelocity.x); | |
| ballVelocity.y += (ball.position.y - playerPaddle.position.y) * 0.02; | |
| ballVelocity.z += (ball.position.z - playerPaddle.position.z) * 0.02; | |
| // Increase speed slightly | |
| ballVelocity.multiplyScalar(1.05); | |
| } | |
| // Computer paddle collision | |
| if ( | |
| Math.abs(ball.position.x - computerPaddle.position.x) < (PADDLE_WIDTH/2 + BALL_RADIUS) && | |
| Math.abs(ball.position.y - computerPaddle.position.y) < (PADDLE_HEIGHT/2 + BALL_RADIUS) && | |
| Math.abs(ball.position.z - computerPaddle.position.z) < (PADDLE_DEPTH/2 + BALL_RADIUS) | |
| ) { | |
| // Reflect ball and add some randomness | |
| ballVelocity.x = Math.abs(ballVelocity.x); | |
| ballVelocity.y += (ball.position.y - computerPaddle.position.y) * 0.02; | |
| ballVelocity.z += (ball.position.z - computerPaddle.position.z) * 0.02; | |
| // Increase speed slightly | |
| ballVelocity.multiplyScalar(1.05); | |
| } | |
| } | |
| // Check for scoring | |
| function checkScoring() { | |
| // Check if ball has passed the paddles (scoring) | |
| if (ball.position.x > SPHERE_RADIUS) { | |
| // Computer scores | |
| computerScore++; | |
| computerScoreElement.textContent = computerScore; | |
| resetBall(); | |
| checkGameOver(); | |
| } else if (ball.position.x < -SPHERE_RADIUS) { | |
| // Player scores | |
| playerScore++; | |
| playerScoreElement.textContent = playerScore; | |
| resetBall(); | |
| checkGameOver(); | |
| } | |
| } | |
| // Check if game is over | |
| function checkGameOver() { | |
| if (playerScore >= 5 || computerScore >= 5) { | |
| gameActive = false; | |
| winnerText.textContent = playerScore >= 5 ? "You Win!" : "Computer Wins!"; | |
| gameOverScreen.classList.remove('hidden'); | |
| } | |
| } | |
| // Keep ball constrained to sphere surface | |
| function constrainBallToSphere() { | |
| const distance = ball.position.length(); | |
| if (Math.abs(distance - SPHERE_RADIUS) > BALL_RADIUS) { | |
| // Normalize position vector and scale to sphere radius | |
| ball.position.normalize().multiplyScalar(SPHERE_RADIUS); | |
| // Reflect velocity vector | |
| const normal = ball.position.clone().normalize(); | |
| ballVelocity.reflect(normal); | |
| } | |
| } | |
| // Rotate sphere based on keyboard input | |
| function rotateSphere() { | |
| if (keys.q) { | |
| sphereRotationY += sphereRotationSpeed; | |
| } | |
| if (keys.e) { | |
| sphereRotationY -= sphereRotationSpeed; | |
| } | |
| sphere.rotation.y = sphereRotationY; | |
| sphere.rotation.x = sphereRotationX; | |
| } | |
| // Set up event listeners | |
| function setupEventListeners() { | |
| // Keyboard controls | |
| document.addEventListener('keydown', (event) => { | |
| if (event.key === 'w' || event.key === 'W') keys.w = true; | |
| if (event.key === 's' || event.key === 'S') keys.s = true; | |
| if (event.key === 'q' || event.key === 'Q') keys.q = true; | |
| if (event.key === 'e' || event.key === 'E') keys.e = true; | |
| }); | |
| document.addEventListener('keyup', (event) => { | |
| if (event.key === 'w' || event.key === 'W') keys.w = false; | |
| if (event.key === 's' || event.key === 'S') keys.s = false; | |
| if (event.key === 'q' || event.key === 'Q') keys.q = false; | |
| if (event.key === 'e' || event.key === 'E') keys.e = false; | |
| }); | |
| // Mouse controls for camera rotation | |
| let isDragging = false; | |
| let previousMousePosition = { | |
| x: 0, | |
| y: 0 | |
| }; | |
| document.addEventListener('mousedown', () => { | |
| isDragging = true; | |
| }); | |
| document.addEventListener('mouseup', () => { | |
| isDragging = false; | |
| }); | |
| document.addEventListener('mousemove', (event) => { | |
| if (!isDragging) return; | |
| const deltaMove = { | |
| x: event.offsetX - previousMousePosition.x, | |
| y: event.offsetY - previousMousePosition.y | |
| }; | |
| // Rotate camera based on mouse movement | |
| camera.rotation.y += deltaMove.x * 0.01; | |
| camera.rotation.x += deltaMove.y * 0.01; | |
| previousMousePosition = { | |
| x: event.offsetX, | |
| y: event.offsetY | |
| }; | |
| }); | |
| // Start button | |
| startBtn.addEventListener('click', () => { | |
| gameActive = !gameActive; | |
| startBtn.textContent = gameActive ? "Pause" : "Start Game"; | |
| if (playerScore >= 5 || computerScore >= 5) { | |
| resetGame(); | |
| } | |
| }); | |
| // Reset button | |
| resetBtn.addEventListener('click', resetGame); | |
| // Play again button | |
| playAgainBtn.addEventListener('click', () => { | |
| gameOverScreen.classList.add('hidden'); | |
| resetGame(); | |
| gameActive = true; | |
| startBtn.textContent = "Pause"; | |
| }); | |
| // Window resize | |
| window.addEventListener('resize', onWindowResize); | |
| } | |
| // Reset game state | |
| function resetGame() { | |
| playerScore = 0; | |
| computerScore = 0; | |
| playerScoreElement.textContent = "0"; | |
| computerScoreElement.textContent = "0"; | |
| resetBall(); | |
| gameActive = false; | |
| startBtn.textContent = "Start Game"; | |
| sphereRotationX = 0; | |
| sphereRotationY = 0; | |
| sphere.rotation.set(0, 0, 0); | |
| camera.rotation.set(0, 0, 0); | |
| } | |
| // Handle window resize | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| // Animation loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const delta = clock.getDelta(); | |
| // Update game objects | |
| updatePlayerPaddle(); | |
| updateComputerPaddle(); | |
| updateBall(); | |
| rotateSphere(); | |
| // Auto-rotate sphere slowly when not manually rotated | |
| if (autoRotate && !keys.q && !keys.e) { | |
| sphere.rotation.y += 0.002; | |
| } | |
| renderer.render(scene, camera); | |
| } | |
| // Initialize the game when page loads | |
| window.addEventListener('load', init); | |