🎮ArcadeLab

Bouncing Balls – Score System

by PixelRider35
424 lines15.9 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Bouncing Balls – Score System</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            background: #000;
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            overflow: hidden;
        }
        canvas {
            display: block;
            background: #000;
            width: 100vw;
            height: 100vh;
        }
        #score-display {
            position: fixed;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            color: #fff;
            font: 28px/1.4 'Courier New', monospace;
            background: rgba(0, 0, 0, 0.75);
            padding: 10px 30px;
            border-radius: 30px;
            border: 2px solid #4fc3f7;
            box-shadow: 0 0 30px rgba(79, 195, 247, 0.15);
            pointer-events: none;
            user-select: none;
            z-index: 10;
            text-align: center;
            letter-spacing: 1px;
        }
        #score-display .score-value {
            color: #4fc3f7;
            font-weight: bold;
            font-size: 32px;
        }
        #score-display .score-label {
            color: #888;
            font-size: 16px;
            margin-right: 8px;
        }
        #controls-hint {
            position: fixed;
            bottom: 20px;
            right: 20px;
            color: #555;
            font: 14px/1.4 monospace;
            background: rgba(0, 0, 0, 0.6);
            padding: 6px 12px;
            border-radius: 16px;
            border: 1px solid #333;
            pointer-events: none;
            user-select: none;
            z-index: 10;
        }
        .score-popup {
            position: fixed;
            font: bold 32px/1 'Courier New', monospace;
            pointer-events: none;
            z-index: 20;
            animation: floatUp 1s ease-out forwards;
            text-shadow: 0 0 20px rgba(255,255,255,0.3);
        }
        @keyframes floatUp {
            0% {
                opacity: 1;
                transform: translateY(0) scale(1);
            }
            100% {
                opacity: 0;
                transform: translateY(-80px) scale(1.2);
            }
        }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>

    <div id="score-display">
        <span class="score-label"> SCORE</span>
        <span class="score-value" id="scoreValue">0</span>
    </div>

    

    <script>
        (function() {
            const canvas = document.getElementById('canvas');
            const ctx = canvas.getContext('2d');
            const scoreSpan = document.getElementById('scoreValue');

            // ----- SCORE -----
            let score = 0;

            function updateScoreDisplay() {
                scoreSpan.textContent = score;
            }

            function showScorePopup(x, y, text, color = '#4fc3f7') {
                const popup = document.createElement('div');
                popup.className = 'score-popup';
                popup.textContent = text;
                popup.style.left = x + 'px';
                popup.style.top = y + 'px';
                popup.style.color = color;
                document.body.appendChild(popup);
                setTimeout(() => popup.remove(), 1000);
            }

            // ----- resize canvas -----
            function resizeCanvas() {
                canvas.width = window.innerWidth;
                canvas.height = window.innerHeight;
            }
            window.addEventListener('resize', resizeCanvas);
            resizeCanvas();

            // ----- PADDLE -----
            const paddle = {
                width: 120,
                height: 16,
                x: 0,
                y: 0,
                speed: 8,
                color: '#4fc3f7'
            };

            function resetPaddle() {
                paddle.y = canvas.height - 40;
                paddle.x = (canvas.width - paddle.width) / 2;
            }
            resetPaddle();

            // ----- keyboard state -----
            const keys = { left: false, right: false };

            document.addEventListener('keydown', (e) => {
                if (e.key === 'ArrowLeft') { keys.left = true;
                    e.preventDefault(); }
                else if (e.key === 'ArrowRight') { keys.right = true;
                    e.preventDefault(); }
            });

            document.addEventListener('keyup', (e) => {
                if (e.key === 'ArrowLeft') { keys.left = false;
                    e.preventDefault(); }
                else if (e.key === 'ArrowRight') { keys.right = false;
                    e.preventDefault(); }
            });

            // ----- ball array -----
            let balls = [];

            // ----- helpers -----
            const rand = (min, max) => Math.random() * (max - min) + min;
            const randInt = (min, max) => Math.floor(rand(min, max + 1));

            // ----- create a ball -----
            function createBall() {
                const radius = rand(8, 35);
                const hue = randInt(0, 360);
                const sat = randInt(70, 100);
                const light = randInt(50, 80);
                const color = `hsl(${hue}, ${sat}%, ${light}%)`;

                const margin = radius + 2;
                const x = rand(margin, canvas.width - margin);
                const y = rand(margin, canvas.height - margin);

                const angle = rand(0, Math.PI * 2);
                const speed = rand(4.5, 8.0);
                const vx = Math.cos(angle) * speed;
                const vy = Math.sin(angle) * speed;

                return { x, y, vx, vy, radius, color, scored: false };
            }

            // ----- add a new ball every 3 seconds -----
            function addBall() {
                if (balls.length < 500) {
                    const newBall = createBall();
                    balls.push(newBall);
                }
            }
            setInterval(addBall, 3000);

            // ----- seed with 3 balls -----
            for (let i = 0; i < 3; i++) {
                balls.push(createBall());
            }
            updateScoreDisplay();

            // ----- collision: elastic bounce between balls -----
            function resolveBallCollisions() {
                const count = balls.length;
                for (let i = 0; i < count; i++) {
                    for (let j = i + 1; j < count; j++) {
                        const a = balls[i];
                        const b = balls[j];

                        const dx = b.x - a.x;
                        const dy = b.y - a.y;
                        const dist = Math.hypot(dx, dy);
                        const minDist = a.radius + b.radius;

                        if (dist < minDist && dist > 0.001) {
                            const nx = dx / dist;
                            const ny = dy / dist;

                            const overlap = (minDist - dist) * 0.5;
                            a.x -= nx * overlap;
                            a.y -= ny * overlap;
                            b.x += nx * overlap;
                            b.y += ny * overlap;

                            const dvx = a.vx - b.vx;
                            const dvy = a.vy - b.vy;
                            const dvn = dvx * nx + dvy * ny;

                            if (dvn > 0) {
                                const massA = a.radius * a.radius;
                                const massB = b.radius * b.radius;
                                const totalMass = massA + massB;
                                const impulse = (2 * dvn) / totalMass;

                                a.vx -= impulse * massB * nx;
                                a.vy -= impulse * massB * ny;
                                b.vx += impulse * massA * nx;
                                b.vy += impulse * massA * ny;
                            }
                        }
                    }
                }
            }

            // ----- update: bounce on top, left, right – wrap bottom, paddle collision -----
            function updateBalls() {
                const w = canvas.width;
                const h = canvas.height;

                // move paddle
                if (keys.left) paddle.x -= paddle.speed;
                if (keys.right) paddle.x += paddle.speed;
                if (paddle.x < 0) paddle.x = 0;
                if (paddle.x + paddle.width > w) paddle.x = w - paddle.width;

                for (const ball of balls) {
                    ball.x += ball.vx;
                    ball.y += ball.vy;

                    const r = ball.radius;

                    // LEFT wall bounce
                    if (ball.x - r < 0) {
                        ball.x = r;
                        ball.vx = -ball.vx;
                    }
                    // RIGHT wall bounce
                    else if (ball.x + r > w) {
                        ball.x = w - r;
                        ball.vx = -ball.vx;
                    }

                    // TOP wall bounce
                    if (ball.y - r < 0) {
                        ball.y = r;
                        ball.vy = -ball.vy;
                    }

                    // PADDLE collision (only if ball is moving downward)
                    if (ball.vy > 0) {
                        const ballRight = ball.x + r;
                        const ballLeft = ball.x - r;
                        const paddleRight = paddle.x + paddle.width;
                        const paddleLeft = paddle.x;

                        if (ballRight > paddleLeft && ballLeft < paddleRight) {
                            const ballBottom = ball.y + r;
                            const paddleTop = paddle.y;
                            const paddleBottom = paddle.y + paddle.height;

                            if (ballBottom >= paddleTop && ball.y - r <= paddleBottom) {
                                // Ball hit paddle - SCORE +10
                                if (!ball.scored) {
                                    ball.scored = true;
                                    score += 10;
                                    updateScoreDisplay();

                                    // Show popup
                                    const rect = canvas.getBoundingClientRect();
                                    const scaleX = canvas.width / rect.width;
                                    const scaleY = canvas.height / rect.height;
                                    const popupX = (ball.x / scaleX) + rect.left - 20;
                                    const popupY = (ball.y / scaleY) + rect.top - 40;
                                    showScorePopup(popupX, popupY, '+10', '#4fc3f7');
                                }

                                ball.y = paddleTop - r;
                                ball.vy = -Math.abs(ball.vy);

                                const hitPos = (ball.x - paddle.x) / paddle.width;
                                const angle = (hitPos - 0.5) * 1.2;
                                const speed = Math.hypot(ball.vx, ball.vy);
                                ball.vx = Math.sin(angle) * speed;
                                ball.vy = -Math.cos(angle) * speed;
                            }
                        }
                    }

                    // BOTTOM: ball escapes - LOSE 1 POINT
                    if (ball.y - r > h) {
                        if (!ball.scored) {
                            // Ball escaped without hitting paddle
                            score -= 1;
                            updateScoreDisplay();

                            // Show popup
                            const rect = canvas.getBoundingClientRect();
                            const scaleX = canvas.width / rect.width;
                            const scaleY = canvas.height / rect.height;
                            const popupX = (ball.x / scaleX) + rect.left - 20;
                            const popupY = (ball.y / scaleY) + rect.top - 20;
                            showScorePopup(popupX, popupY, '-1', '#ff6b6b');
                        }

                        // Reset ball to top
                        ball.y = -r;
                        ball.x = rand(r, w - r);
                        ball.scored = false; // Reset scored flag for next round
                    }
                }

                resolveBallCollisions();
            }

            // ----- draw everything -----
            function drawBalls() {
                ctx.clearRect(0, 0, canvas.width, canvas.height);

                // draw paddle
                ctx.shadowColor = 'rgba(79, 195, 247, 0.3)';
                ctx.shadowBlur = 20;
                ctx.fillStyle = paddle.color;
                ctx.beginPath();
                ctx.roundRect(paddle.x, paddle.y, paddle.width, paddle.height, 8);
                ctx.fill();
                ctx.shadowBlur = 0;

                // draw balls
                for (const ball of balls) {
                    ctx.beginPath();
                    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
                    ctx.fillStyle = ball.color;
                    ctx.fill();
                    ctx.shadowColor = 'rgba(255,255,255,0.12)';
                    ctx.shadowBlur = 8;
                    ctx.fill();
                    ctx.shadowBlur = 0;
                }
            }

            // ----- roundRect polyfill -----
            if (!CanvasRenderingContext2D.prototype.roundRect) {
                CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, radii) {
                    const r = typeof radii === 'number' ? radii : (radii || 0);
                    this.moveTo(x + r, y);
                    this.lineTo(x + w - r, y);
                    this.quadraticCurveTo(x + w, y, x + w, y + r);
                    this.lineTo(x + w, y + h - r);
                    this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
                    this.lineTo(x + r, y + h);
                    this.quadraticCurveTo(x, y + h, x, y + h - r);
                    this.lineTo(x, y + r);
                    this.quadraticCurveTo(x, y, x + r, y);
                    return this;
                };
            }

            // ----- animation loop -----
            function animate() {
                updateBalls();
                drawBalls();
                requestAnimationFrame(animate);
            }
            animate();

            // ----- handle resize -----
            window.addEventListener('resize', () => {
                resizeCanvas();
                resetPaddle();
            });

            // ----- click to add a ball -----
            canvas.addEventListener('click', (e) => {
                const rect = canvas.getBoundingClientRect();
                const scaleX = canvas.width / rect.width;
                const scaleY = canvas.height / rect.height;
                const x = (e.clientX - rect.left) * scaleX;
                const y = (e.clientY - rect.top) * scaleY;

                const newBall = createBall();
                newBall.x = Math.min(Math.max(x, newBall.radius + 2), canvas.width - newBall.radius - 2);
                newBall.y = Math.min(Math.max(y, newBall.radius + 2), canvas.height - newBall.radius - 2);
                balls.push(newBall);
            });

            console.log('?? Score system active! +10 for paddle hits, -1 for escapes.');
        })();
    </script>
</body>
</html>

Game Source: Bouncing Balls – Score System

Creator: PixelRider35

Libraries: none

Complexity: complex (424 lines, 15.9 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: bouncing-balls-score-system-pixelrider35" to link back to the original. Then publish at arcadelab.ai/publish.