// 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);