🎮ArcadeLab

ring clicker · miss penalty

by PixelRider35
348 lines12.3 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ring clicker · miss penalty</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            user-select: none;
        }
        body {
            background: black;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            font-family: 'Segoe UI', system-ui, sans-serif;
        }
        canvas {
            display: block;
            background: black;
            width: 100vw;
            height: 100vh;
            object-fit: contain;
            cursor: crosshair;
        }
        #scoreboard {
            position: fixed;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            color: white;
            font-size: 32px;
            font-weight: 700;
            letter-spacing: 1px;
            background: rgba(0, 0, 0, 0.65);
            padding: 10px 28px;
            border-radius: 40px;
            border: 1px solid rgba(255, 255, 255, 0.15);
            backdrop-filter: blur(4px);
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
            z-index: 10;
            pointer-events: none;
            text-shadow: 0 0 12px rgba(255, 255, 255, 0.2);
        }
        #scoreboard span {
            color: #ffd966;
        }
        #missIndicator {
            position: fixed;
            top: 80px;
            left: 50%;
            transform: translateX(-50%);
            color: #ff6b6b;
            font-size: 20px;
            font-weight: 600;
            opacity: 0;
            transition: opacity 0.15s ease;
            pointer-events: none;
            z-index: 10;
            text-shadow: 0 0 20px rgba(255, 0, 0, 0.4);
        }
        #missIndicator.show {
            opacity: 1;
        }
    </style>
</head>
<body>
    <div id="scoreboard"> <span id="scoreDisplay">0</span></div>
    <div id="missIndicator"></div>
    <canvas id="ringCanvas"></canvas>
    <script>
        (function() {
            const canvas = document.getElementById('ringCanvas');
            const ctx = canvas.getContext('2d');
            const scoreSpan = document.getElementById('scoreDisplay');
            const missIndicator = document.getElementById('missIndicator');

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

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

            // ----- ring storage -----
            let rings = [];

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

            // ----- generate a random bright color (HSL) -----
            function randomColor() {
                const h = randInt(0, 360);
                const s = randInt(70, 100);
                const l = randInt(55, 85);
                return `hsl(${h}, ${s}%, ${l}%)`;
            }

            // ----- collision detection (circle vs circle) -----
            function circlesOverlap(x1, y1, r1, x2, y2, r2) {
                const dx = x1 - x2;
                const dy = y1 - y2;
                const dist = Math.hypot(dx, dy);
                return dist < r1 + r2;
            }

            // ----- check if a new ring overlaps any existing ring -----
            function overlapsAny(newRing) {
                for (let r of rings) {
                    if (circlesOverlap(
                        newRing.x, newRing.y, newRing.radius,
                        r.x, r.y, r.radius
                    )) {
                        return true;
                    }
                }
                return false;
            }

            // ----- generate a non-overlapping ring with random color -----
            function generateNonOverlappingRing(maxAttempts = 800) {
                const w = canvas.width;
                const h = canvas.height;

                const minR = 12;
                const maxR = 100;

                for (let attempt = 0; attempt < maxAttempts; attempt++) {
                    const radius = rand(minR, maxR);
                    const margin = radius + 2;
                    const x = rand(margin, w - margin);
                    const y = rand(margin, h - margin);

                    const candidate = { x, y, radius };

                    if (!overlapsAny(candidate)) {
                        const minLife = 300;    // 
                        const maxLife = 2000;   // 
                        const lifeMs = rand(minLife, maxLife);
                        return {
                            x, y,
                            radius,
                            color: randomColor(),
                            birth: performance.now(),
                            lifeMs: lifeMs,
                            deathTime: performance.now() + lifeMs,
                            filled: false,
                            wasClicked: false,   // track if user clicked this ring
                            id: Math.random().toString(36).substring(2, 9)
                        };
                    }
                }
                return null;
            }

            // ----- spawn a new ring (if possible) -----
            function spawnRing() {
                const newRing = generateNonOverlappingRing();
                if (newRing) {
                    rings.push(newRing);
                }
            }

            // ----- remove expired rings & penalize if not clicked -----
            function removeExpiredRings(now) {
                const toRemove = [];
                for (let i = rings.length - 1; i >= 0; i--) {
                    const r = rings[i];
                    if (r.deathTime <= now) {
                        // Ring expired naturally
                        if (!r.wasClicked && !r.filled) {
                            // User missed this ring ? -1 point
                            score -= 1;
                            updateScore();
                            showMissIndicator();
                        }
                        toRemove.push(i);
                    }
                }
                // Remove from end to start
                for (let idx of toRemove) {
                    rings.splice(idx, 1);
                }
            }

            // ----- draw all rings with fade and color -----
            function drawRings(now) {
                ctx.clearRect(0, 0, canvas.width, canvas.height);

                for (let r of rings) {
                    const timeLeft = r.deathTime - now;
                    let alpha = 1.0;
                    if (timeLeft < 400 && timeLeft > 0) {
                        alpha = timeLeft / 400;
                    } else if (timeLeft <= 0) {
                        alpha = 0;
                    }

                    ctx.globalAlpha = alpha;

                    if (r.filled) {
                        // FILLED ring: solid color
                        ctx.beginPath();
                        ctx.arc(r.x, r.y, r.radius, 0, Math.PI * 2);
                        ctx.fillStyle = r.color;
                        ctx.fill();
                        ctx.strokeStyle = r.color;
                        ctx.lineWidth = 1.5;
                        ctx.stroke();
                    } else {
                        // NORMAL ring: only stroke
                        ctx.beginPath();
                        ctx.arc(r.x, r.y, r.radius, 0, Math.PI * 2);
                        ctx.strokeStyle = r.color;
                        ctx.lineWidth = 2.8;
                        ctx.stroke();
                    }
                }
                ctx.globalAlpha = 1.0;
            }

            // ----- update score display -----
            function updateScore() {
                scoreSpan.textContent = score;
            }

            // ----- show miss indicator briefly -----
            let missTimeout = null;
            function showMissIndicator() {
                missIndicator.classList.add('show');
                if (missTimeout) {
                    clearTimeout(missTimeout);
                }
                missTimeout = setTimeout(() => {
                    missIndicator.classList.remove('show');
                }, 300);
            }

            // 
            function handleCanvasClick(e) {
                const rect = canvas.getBoundingClientRect();
                const scaleX = canvas.width / rect.width;
                const scaleY = canvas.height / rect.height;

                const mouseX = (e.clientX - rect.left) * scaleX;
                const mouseY = (e.clientY - rect.top) * scaleY;

                let hitRing = false;
                const now = performance.now();

                // iterate rings (backwards so we can modify safely)
                for (let i = rings.length - 1; i >= 0; i--) {
                    const r = rings[i];
                    const timeLeft = r.deathTime - now;
                    if (timeLeft <= 0) continue;

                    const dx = mouseX - r.x;
                    const dy = mouseY - r.y;
                    const dist = Math.hypot(dx, dy);
                    if (dist < r.radius) {
                        // 
                        score += 5;
                        updateScore();

                        // Mark as clicked and fill it
                        r.wasClicked = true;
                        r.filled = true;

                        // Remove after a short delay so fill is visible
                        const ringId = r.id;
                        setTimeout(() => {
                            const index = rings.findIndex(rr => rr.id === ringId);
                            if (index !== -1) {
                                rings.splice(index, 1);
                            }
                        }, 150);

                        hitRing = true;
                        break; // only one ring per click
                    }
                }

                // if no ring was hit: -1 point
                if (!hitRing) {
                    score -= 1;
                    updateScore();
                    showMissIndicator();
                }
            }

            // ----- animation loop -----
            let lastSpawnTime = 0;
            let spawnInterval = rand(200, 600);

            function animate(timestamp) {
                const now = performance.now();

                // 1) remove expired rings & penalize misses
                removeExpiredRings(now);

                // 2) spawn new rings at a fast pace
                if (now - lastSpawnTime > spawnInterval) {
                    spawnInterval = rand(200, 600);
                    spawnRing();
                    lastSpawnTime = now;
                }

                // 3) draw everything
                drawRings(now);

                requestAnimationFrame(animate);
            }

            // ----- START WITH A BLACK SCREEN (no initial rings) -----
            // rings array starts empty ? canvas stays black until first spawn

            // set spawn timer
            lastSpawnTime = performance.now();

            // start animation
            requestAnimationFrame(animate);

            // ----- click listener -----
            canvas.addEventListener('click', handleCanvasClick);

            // ----- resize: keep rings, they'll naturally expire -----
            window.addEventListener('resize', () => {
                resizeCanvas();
            });

            // init score display
            updateScore();

            console.log(' Click inside a ring ? +5 points & ring fills!');
            console.log('? Miss a ring (click outside or let it expire) ? -1 point');
        })();
    </script>
</body>
</html>

Game Source: ring clicker · miss penalty

Creator: PixelRider35

Libraries: none

Complexity: complex (348 lines, 12.3 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: ring-clicker-miss-penalty-pixelrider35" to link back to the original. Then publish at arcadelab.ai/publish.