Download script.js from Gregniuki/Pingpong: direct link, hf CLI and curl.
- Browser
- Download file 20 kB
-
https://huggingface.co/spaces/Gregniuki/Pingpong/resolve/main/script.js
- Command line
-
hf download hf://spaces/Gregniuki/Pingpong/script.js
-
curl -L -o script.js https://huggingface.co/spaces/Gregniuki/Pingpong/resolve/main/script.js
20 kB
| // --- Get Elements --- | |
| const gameArea = document.getElementById('gameArea'); | |
| const paddleLeft = document.getElementById('paddleLeft'); | |
| const paddleRight = document.getElementById('paddleRight'); | |
| const ball = document.getElementById('ball'); | |
| const powerUpElement = document.getElementById('powerUp'); | |
| const playerScoreDisplay = document.getElementById('playerScore'); | |
| const botScoreDisplay = document.getElementById('botScore'); | |
| const chargeIndicator = document.getElementById('chargeIndicator'); | |
| const chargeIndicatorFill = document.getElementById('chargeIndicatorFill'); | |
| // --- Constants --- | |
| const BASE_PADDLE_HEIGHT = 80; | |
| const PADDLE_WIDTH = 10; | |
| const BALL_SIZE = 15; | |
| const MAX_ANGLE_DEG = 60; // Increased max angle for more variation | |
| const MAX_ANGLE_RAD = MAX_ANGLE_DEG * (Math.PI / 180); | |
| const INITIAL_BALL_SPEED = 4; | |
| const SPEED_INCREASE_FACTOR = 1.08; // Slightly lower increase | |
| const MAX_BALL_SPEED = 18; | |
| const BOT_PADDLE_SPEED = 5; // Base speed | |
| const BOT_PREDICTION_FACTOR = 0.85; // 0 = No prediction, 1 = Perfect prediction (adjust for difficulty) | |
| const SPIN_FACTOR = 0.08; // How much paddle speed translates to spin | |
| const MAX_SPIN = 1.5; // Max spin value | |
| const SPIN_DRAG = 0.995; // Spin decreases slightly over time | |
| const MAGNUS_EFFECT_STRENGTH = 0.003; // How much spin affects trajectory | |
| const CHARGE_RATE = 150; // Lower value = faster charge (ms per 100% charge) | |
| const MAX_CHARGE_BOOST_FACTOR = 2.0; // Max speed multiplier for fully charged shot | |
| const POWERUP_SPAWN_CHANCE = 0.002; // Chance per frame to spawn | |
| const POWERUP_DURATION = 7000; // ms | |
| const PADDLE_SIZE_INCREASE = 40; // Pixels to add to height | |
| // --- Game State Variables --- | |
| let paddleLeftY = window.innerHeight / 2 - BASE_PADDLE_HEIGHT / 2; | |
| let paddleRightY = window.innerHeight / 2 - BASE_PADDLE_HEIGHT / 2; | |
| let paddleLeftHeight = BASE_PADDLE_HEIGHT; | |
| let paddleRightHeight = BASE_PADDLE_HEIGHT; | |
| let ballX = window.innerWidth / 2 - BALL_SIZE / 2; | |
| let ballY = window.innerHeight / 2 - BALL_SIZE / 2; | |
| let ballSpeedX = INITIAL_BALL_SPEED; | |
| let ballSpeedY = 0; | |
| let ballSpin = 0; // Positive = Clockwise (Topspin from left player), Negative = Counter-Clockwise (Backspin from left player) | |
| let playerScore = 0; | |
| let botScore = 0; | |
| let isGamePaused = true; | |
| let lastPlayerPaddleY = paddleLeftY; // For calculating spin | |
| let isCharging = false; | |
| let chargeStartTime = 0; | |
| let currentChargeBoost = 1.0; // Speed multiplier from charge | |
| let activePowerUp = null; // { type: 'PaddleSizeUp', side: 'left'/'right'/'both', timeoutId: null } | |
| let powerUpData = { x: 0, y: 0, active: false }; | |
| // --- Initial Setup --- | |
| function initializeGame() { | |
| paddleLeft.style.height = paddleLeftHeight + 'px'; | |
| paddleRight.style.height = paddleRightHeight + 'px'; | |
| paddleLeft.style.top = paddleLeftY + 'px'; | |
| paddleRight.style.top = paddleRightY + 'px'; | |
| ball.style.left = ballX + 'px'; | |
| ball.style.top = ballY + 'px'; | |
| playerScoreDisplay.textContent = playerScore; | |
| botScoreDisplay.textContent = botScore; | |
| // Place paddles correctly at start/resize | |
| paddleRight.style.right = '5px'; // Ensure right paddle is positioned correctly | |
| paddleLeft.style.left = '5px'; | |
| } | |
| // --- Input Handling (Touch & Mouse for Player) --- | |
| let interactionStartY = null; // Track touch start position | |
| function handleInteractionStart(clientY, clientX) { | |
| if (clientX < window.innerWidth / 2) { // Only charge if interacting on the left side | |
| isCharging = true; | |
| chargeStartTime = performance.now(); | |
| chargeIndicator.style.display = 'block'; | |
| updateChargeIndicator(); // Update visual immediately | |
| interactionStartY = clientY; // Store initial Y for potential movement during charge | |
| if (isGamePaused) { | |
| isGamePaused = false; | |
| // Determine starting direction if needed (optional) | |
| // ballSpeedX = Math.abs(ballSpeedX) || INITIAL_BALL_SPEED; | |
| } | |
| } | |
| } | |
| function handleInteractionMove(clientY, clientX) { | |
| if (clientX < window.innerWidth / 2) { // Only control left paddle | |
| // Move paddle based on current position, not start position | |
| paddleLeftY = clientY - paddleLeftHeight / 2; | |
| paddleLeftY = Math.max(0, Math.min(paddleLeftY, window.innerHeight - paddleLeftHeight)); | |
| paddleLeft.style.top = paddleLeftY + 'px'; | |
| } | |
| } | |
| function handleInteractionEnd() { | |
| if (isCharging) { | |
| isCharging = false; | |
| const chargeDuration = performance.now() - chargeStartTime; | |
| const chargeLevel = Math.min(chargeDuration / CHARGE_RATE, 1.0); // Cap at 100% | |
| currentChargeBoost = 1.0 + chargeLevel * (MAX_CHARGE_BOOST_FACTOR - 1.0); | |
| // console.log("Charge Released! Boost:", currentChargeBoost.toFixed(2)); // Debug | |
| chargeIndicator.style.display = 'none'; | |
| chargeIndicatorFill.style.width = '0%'; | |
| // Note: Boost is applied in handlePaddleCollision if released just before hit | |
| } | |
| interactionStartY = null; | |
| } | |
| gameArea.addEventListener('touchstart', (e) => { | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| handleInteractionStart(touch.clientY, touch.clientX); | |
| }, { passive: false }); | |
| gameArea.addEventListener('touchmove', (e) => { | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| handleInteractionMove(touch.clientY, touch.clientX); | |
| }, { passive: false }); | |
| gameArea.addEventListener('touchend', (e) => { | |
| handleInteractionEnd(); | |
| }); | |
| gameArea.addEventListener('touchcancel', (e) => { | |
| handleInteractionEnd(); // Treat cancel like end | |
| }); | |
| // Mouse fallback for Desktop | |
| gameArea.addEventListener('mousedown', (e) => handleInteractionStart(e.clientY, e.clientX)); | |
| gameArea.addEventListener('mousemove', (e) => { | |
| // Only move paddle if mouse button is potentially down OR touch isn't active | |
| // A better approach might track mouseDown state explicitly | |
| if (interactionStartY !== null || e.buttons === 1) { // Move if charging or mouse down | |
| handleInteractionMove(e.clientY, e.clientX); | |
| } | |
| }); | |
| gameArea.addEventListener('mouseup', (e) => handleInteractionEnd()); | |
| gameArea.addEventListener('mouseleave', (e) => handleInteractionEnd()); // Stop charging if mouse leaves | |
| function updateChargeIndicator() { | |
| if (isCharging) { | |
| const chargeDuration = performance.now() - chargeStartTime; | |
| const chargeLevel = Math.min(chargeDuration / CHARGE_RATE, 1.0); | |
| chargeIndicatorFill.style.width = `${chargeLevel * 100}%`; | |
| requestAnimationFrame(updateChargeIndicator); // Keep updating while charging | |
| } | |
| } | |
| // --- Power-Up Logic --- | |
| function spawnPowerUp() { | |
| if (!powerUpData.active && Math.random() < POWERUP_SPAWN_CHANCE) { | |
| // console.log("Spawning PowerUp!"); // Debug | |
| powerUpData.x = Math.random() * (window.innerWidth * 0.6) + window.innerWidth * 0.2; // Spawn near middle horizontally | |
| powerUpData.y = Math.random() * (window.innerHeight * 0.8) + window.innerHeight * 0.1; // Avoid edges vertically | |
| powerUpData.active = true; | |
| powerUpElement.style.left = powerUpData.x + 'px'; | |
| powerUpElement.style.top = powerUpData.y + 'px'; | |
| powerUpElement.style.display = 'block'; | |
| // playSound('powerUpSpawn'); | |
| } | |
| } | |
| function checkPowerUpCollision() { | |
| if (!powerUpData.active) return; | |
| const ballRect = ball.getBoundingClientRect(); | |
| const powerUpRect = powerUpElement.getBoundingClientRect(); | |
| if (ballRect.left < powerUpRect.right && | |
| ballRect.right > powerUpRect.left && | |
| ballRect.top < powerUpRect.bottom && | |
| ballRect.bottom > powerUpRect.top) | |
| { | |
| // console.log("PowerUp Hit!"); // Debug | |
| powerUpData.active = false; | |
| powerUpElement.style.display = 'none'; | |
| activatePowerUp('PaddleSizeUp'); // Hardcoded type for now | |
| // playSound('powerUpCollect'); | |
| } | |
| } | |
| function activatePowerUp(type) { | |
| clearTimeout(activePowerUp?.timeoutId); // Clear existing timer if any | |
| const side = (ballSpeedX > 0) ? 'left' : 'right'; // Give power-up to the player who last hit | |
| // console.log(`Activating ${type} for ${side}`); // Debug | |
| if (type === 'PaddleSizeUp') { | |
| if (side === 'left' || side === 'both') { | |
| paddleLeftHeight = BASE_PADDLE_HEIGHT + PADDLE_SIZE_INCREASE; | |
| paddleLeft.style.height = paddleLeftHeight + 'px'; | |
| } | |
| if (side === 'right' || side === 'both') { | |
| paddleRightHeight = BASE_PADDLE_HEIGHT + PADDLE_SIZE_INCREASE; | |
| paddleRight.style.height = paddleRightHeight + 'px'; | |
| } | |
| // Set timer to deactivate | |
| const timeoutId = setTimeout(() => deactivatePowerUp(type, side), POWERUP_DURATION); | |
| activePowerUp = { type, side, timeoutId }; | |
| } | |
| // Add other power-up types here (MultiBall, SpeedChange, etc.) | |
| } | |
| function deactivatePowerUp(type, side) { | |
| // console.log(`Deactivating ${type} for ${side}`); // Debug | |
| if (type === 'PaddleSizeUp') { | |
| if (side === 'left' || side === 'both') { | |
| paddleLeftHeight = BASE_PADDLE_HEIGHT; | |
| paddleLeft.style.height = paddleLeftHeight + 'px'; | |
| } | |
| if (side === 'right' || side === 'both') { | |
| paddleRightHeight = BASE_PADDLE_HEIGHT; | |
| paddleRight.style.height = paddleRightHeight + 'px'; | |
| } | |
| } | |
| // Add deactivation for other types | |
| if (activePowerUp?.timeoutId) { // Clean up state | |
| activePowerUp = null; | |
| } | |
| } | |
| // --- Collision Handling --- | |
| function handlePaddleCollision(paddleY, paddleHeight, paddleX, isLeftPaddle) { | |
| const ballRadius = BALL_SIZE / 2; | |
| const ballCenterX = ballX + ballRadius; | |
| const ballCenterY = ballY + ballRadius; | |
| const paddleTop = paddleY; | |
| const paddleBottom = paddleY + paddleHeight; | |
| const paddleLeftEdge = paddleX; | |
| const paddleRightEdge = paddleX + PADDLE_WIDTH; | |
| // Find closest point on paddle to ball center | |
| let closestX = Math.max(paddleLeftEdge, Math.min(ballCenterX, paddleRightEdge)); | |
| let closestY = Math.max(paddleTop, Math.min(ballCenterY, paddleBottom)); | |
| const dx = ballCenterX - closestX; | |
| const dy = ballCenterY - closestY; | |
| const distanceSquared = (dx * dx) + (dy * dy); | |
| if (distanceSquared < (ballRadius * ballRadius)) { // Collision detected | |
| // playSound('paddleHit'); | |
| showImpactEffect(isLeftPaddle ? paddleLeft : paddleRight); | |
| // --- Calculate Spin based on paddle movement --- | |
| let paddleVelocityY = 0; | |
| if (isLeftPaddle) { | |
| paddleVelocityY = paddleLeftY - lastPlayerPaddleY; // How much paddle moved since last frame | |
| } else { | |
| // Basic AI doesn't have velocity tracking here, could add if needed | |
| // For now, AI imparts less spin or random spin | |
| paddleVelocityY = (Math.random() - 0.5) * 2; // Small random spin | |
| } | |
| // Add spin based on paddle speed, clamp it | |
| const addedSpin = -paddleVelocityY * SPIN_FACTOR; // Negative because Y increases downwards | |
| ballSpin += addedSpin; | |
| ballSpin = Math.max(-MAX_SPIN, Math.min(MAX_SPIN, ballSpin)); | |
| // --- Calculate Bounce Angle --- | |
| const paddleCenterY = paddleTop + paddleHeight / 2; | |
| let hitPosition = (ballCenterY - paddleCenterY) / (paddleHeight / 2); | |
| hitPosition = Math.max(-1, Math.min(1, hitPosition)); // Clamp -1 to 1 | |
| // Angle based on hit position AND incoming spin | |
| let bounceAngle = hitPosition * MAX_ANGLE_RAD; | |
| // Spin affects bounce angle slightly (e.g., topspin makes it bounce lower) | |
| bounceAngle -= ballSpin * 0.1; // Adjust multiplier as needed | |
| // --- Calculate Speed --- | |
| const currentSpeed = Math.sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY); | |
| let chargeBoostToApply = 1.0; | |
| if (isLeftPaddle && currentChargeBoost > 1.0) { | |
| chargeBoostToApply = currentChargeBoost; | |
| currentChargeBoost = 1.0; // Reset boost after applying | |
| // console.log("Applying Charge Boost!", chargeBoostToApply.toFixed(2)); // Debug | |
| // playSound('chargeHit'); | |
| showImpactEffect(ball, true); // Extra effect for charged hit | |
| } | |
| let newSpeed = currentSpeed * SPEED_INCREASE_FACTOR * chargeBoostToApply; | |
| newSpeed = Math.min(newSpeed, MAX_BALL_SPEED); | |
| // --- Calculate New Velocities --- | |
| const directionX = isLeftPaddle ? 1 : -1; | |
| ballSpeedX = directionX * newSpeed * Math.cos(bounceAngle); | |
| // Ball speed Y influenced by bounce angle and slightly by existing spin reversal | |
| ballSpeedY = newSpeed * Math.sin(bounceAngle) - ballSpin * 0.5; // Spin influences vertical speed | |
| // Reverse spin slightly on paddle impact (simulating friction) | |
| ballSpin *= -0.6; // Dampen and reverse spin slightly | |
| // --- Reposition Ball --- | |
| // Simple horizontal push out based on direction | |
| const overlap = ballRadius - Math.sqrt(distanceSquared); | |
| ballX += directionX * (overlap + 1); // Push out slightly more | |
| // Ensure it's fully out (optional safety) | |
| if (isLeftPaddle && ballX < paddleRightEdge) ballX = paddleRightEdge; | |
| else if (!isLeftPaddle && ballX + BALL_SIZE > paddleLeftEdge) ballX = paddleLeftEdge - BALL_SIZE; | |
| return true; // Indicate collision happened | |
| } | |
| return false; // No collision | |
| } | |
| // --- Visual Effects --- | |
| function showImpactEffect(element, strong = false) { | |
| element.classList.add('impact'); | |
| if (strong) { | |
| element.style.boxShadow = '0 0 25px 10px red'; // Stronger effect | |
| } | |
| setTimeout(() => { | |
| element.classList.remove('impact'); | |
| element.style.boxShadow = ''; // Reset specific strong effect | |
| }, 100); // Duration of the flash | |
| } | |
| function updateBallTrail() { | |
| const speed = Math.sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY); | |
| if (speed > INITIAL_BALL_SPEED * 1.5) { // Only show trail above certain speed | |
| ball.classList.add('moving'); | |
| // Optional: Adjust trail length/opacity based on speed here if desired | |
| } else { | |
| ball.classList.remove('moving'); | |
| } | |
| // Reset the animation trick for CSS trail | |
| void ball.offsetWidth; // Trigger reflow to restart CSS transition | |
| } | |
| // --- AI Logic --- | |
| function updateBotAI() { | |
| // Predictive AI: Estimate where the ball will cross the bot's paddle line | |
| let predictedY = ballY; | |
| if (ballSpeedX > 0) { // Only predict if ball is moving towards bot | |
| const timeToReachPaddle = (window.innerWidth - PADDLE_WIDTH - ballX) / ballSpeedX; | |
| // Simple prediction (doesn't account for wall bounces during flight yet) | |
| predictedY = ballY + ballSpeedY * timeToReachPaddle; | |
| // Add prediction based on spin (Magnus effect over time) | |
| // Simplified: Average spin effect over the predicted time | |
| predictedY += ballSpin * MAGNUS_EFFECT_STRENGTH * timeToReachPaddle * timeToReachPaddle * ballSpeedX / 2; // Rough estimate | |
| // Add inaccuracy based on prediction factor | |
| const targetError = (paddleRightHeight / 2) * (1 - BOT_PREDICTION_FACTOR) * (Math.random() - 0.5); | |
| predictedY += targetError; | |
| // Clamp prediction to bounds (prevent predicting outside court) | |
| predictedY = Math.max(BALL_SIZE / 2, Math.min(predictedY, window.innerHeight - BALL_SIZE / 2)); | |
| } else { | |
| // If ball moving away, slowly center the paddle | |
| predictedY = window.innerHeight / 2; | |
| } | |
| // Move Paddle towards predicted Y | |
| const botPaddleCenterTarget = predictedY - paddleRightHeight / 2; // Target top position for paddle | |
| const currentBotCenter = paddleRightY + paddleRightHeight / 2; | |
| if (paddleRightY + paddleRightHeight / 2 < predictedY - 5) { // Move down | |
| paddleRightY += BOT_PADDLE_SPEED; | |
| } else if (paddleRightY + paddleRightHeight / 2 > predictedY + 5) { // Move up | |
| paddleRightY -= BOT_PADDLE_SPEED; | |
| } | |
| // Clamp bot paddle position | |
| paddleRightY = Math.max(0, Math.min(paddleRightY, window.innerHeight - paddleRightHeight)); | |
| paddleRight.style.top = paddleRightY + 'px'; | |
| } | |
| // --- Game Update Loop --- | |
| function update() { | |
| if (!isGamePaused) { | |
| // --- Ball Movement --- | |
| ballX += ballSpeedX; | |
| // Apply Magnus effect (curve due to spin) | |
| ballY += ballSpeedY + (ballSpin * MAGNUS_EFFECT_STRENGTH * Math.abs(ballSpeedX)); // Effect stronger at higher horizontal speed | |
| ballSpin *= SPIN_DRAG; // Spin decays slowly | |
| // --- Wall Collisions --- | |
| if (ballY <= 0) { | |
| ballY = 0; | |
| ballSpeedY *= -1; | |
| ballSpin *= 0.8; // Spin dampens on wall hit | |
| // playSound('wallHit'); | |
| showImpactEffect(ball); | |
| } else if (ballY >= window.innerHeight - BALL_SIZE) { | |
| ballY = window.innerHeight - BALL_SIZE; | |
| ballSpeedY *= -1; | |
| ballSpin *= 0.8; | |
| // playSound('wallHit'); | |
| showImpactEffect(ball); | |
| } | |
| // --- Paddle Collisions --- | |
| let collisionOccurred = false; | |
| if (ballSpeedX < 0) { // Moving left | |
| collisionOccurred = handlePaddleCollision(paddleLeftY, paddleLeftHeight, 5, true); // Left paddle at x=5 | |
| } else { // Moving right | |
| collisionOccurred = handlePaddleCollision(paddleRightY, paddleRightHeight, window.innerWidth - PADDLE_WIDTH - 5, false); // Right paddle position | |
| } | |
| // --- Power-Up Logic --- | |
| spawnPowerUp(); | |
| checkPowerUpCollision(); | |
| // --- Scoring --- | |
| if (ballX + BALL_SIZE <= 0) { | |
| botScore++; | |
| botScoreDisplay.textContent = botScore; | |
| resetBall('right'); // Bot serves | |
| // playSound('score'); | |
| } else if (ballX >= window.innerWidth) { | |
| playerScore++; | |
| playerScoreDisplay.textContent = playerScore; | |
| resetBall('left'); // Player serves | |
| // playSound('score'); | |
| } | |
| // --- AI Update --- | |
| updateBotAI(); | |
| // --- Update Visuals --- | |
| ball.style.left = ballX + 'px'; | |
| ball.style.top = ballY + 'px'; | |
| updateBallTrail(); | |
| // Track player paddle's last position AFTER potential collision handling | |
| lastPlayerPaddleY = paddleLeftY; | |
| } // End if(!isGamePaused) | |
| // Loop | |
| requestAnimationFrame(update); | |
| } | |
| // --- Reset Ball Function --- | |
| function resetBall(scoringSide) { | |
| isGamePaused = true; // Pause until next interaction | |
| // Deactivate any active powerups immediately on score | |
| if (activePowerUp) { | |
| clearTimeout(activePowerUp.timeoutId); | |
| deactivatePowerUp(activePowerUp.type, activePowerUp.side); // Visually reset paddles | |
| } | |
| if (powerUpData.active) { | |
| powerUpData.active = false; | |
| powerUpElement.style.display = 'none'; | |
| } | |
| ballY = window.innerHeight / 2 - BALL_SIZE / 2; | |
| ballSpeedY = 0; | |
| ballSpin = 0; | |
| currentChargeBoost = 1.0; // Reset charge boost | |
| isCharging = false; // Ensure charging stops | |
| chargeIndicator.style.display = 'none'; | |
| chargeIndicatorFill.style.width = '0%'; | |
| if (scoringSide === 'left') { // Player scored, player serves | |
| ballX = PADDLE_WIDTH + 30; | |
| ballSpeedX = INITIAL_BALL_SPEED; | |
| } else { // Bot scored, bot serves | |
| ballX = window.innerWidth - PADDLE_WIDTH - BALL_SIZE - 30; | |
| ballSpeedX = -INITIAL_BALL_SPEED; | |
| } | |
| // Position ball visually for restart | |
| ball.style.left = ballX + 'px'; | |
| ball.style.top = ballY + 'px'; | |
| ball.classList.remove('moving'); // Stop trail | |
| // Could add a "Tap to serve" message here | |
| } | |
| // --- Utility for Sound (Placeholder) --- | |
| // function playSound(soundName) { | |
| // // console.log("Playing sound:", soundName); // Placeholder | |
| // // In a real implementation: | |
| // // const audio = new Audio(`sounds/${soundName}.wav`); | |
| // // audio.play(); | |
| // } | |
| // --- Start Game --- | |
| initializeGame(); | |
| requestAnimationFrame(update); // Start the loop | |