<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ball Clicker 3D Game</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <!-- Updated HUD -->
    <div id="hud">
        <div>Score: <span id="score">0</span></div>
        <div>Time: <span id="time">60</span></div>
    </div>

    <!-- Game Overlays -->
    <div id="start-screen" class="overlay visible">
        <div class="overlay-content">
            <h2>Ball Clicker 3D</h2>
            <p>Click the balls inside the rotating shape before time runs out!</p>
            <p>Move mouse to rotate the shape. Drag background to rotate view.</p>
            <button id="start-button">Start Game</button>
        </div>
    </div>
    <div id="game-over-screen" class="overlay">
         <div class="overlay-content">
            <h2>Game Over!</h2>
             <p>Final Score: <span id="final-score">0</span></p>
            <button id="restart-button">Play Again</button>
        </div>
    </div>

    <div id="container"></div>

    <!-- Sound Effect (Optional) -->
    <audio id="click-sound" src="audio/click.wav" preload="auto"></audio> <!-- You need to provide this audio file -->

    <!-- Include Three.js Library -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <!-- Include OrbitControls Add-on -->
    <script src="https://unpkg.com/three@0.128.0/examples/js/controls/OrbitControls.js"></script>

    <!-- Your simulation script -->
    <script src="script.js"></script>
</body>
</html>
body {
    margin: 0;
    font-family: Arial, sans-serif;
    background-color: #111; /* Dark background */
    color: #eee;
    overflow: hidden; /* Prevent scrollbars from canvas */
}

#container {
    width: 100vw;
    height: 100vh;
    display: block;
    cursor: crosshair; /* Indicate clickable area */
}

#hud {
    position: absolute;
    top: 10px;
    left: 10px;
    padding: 10px 15px;
    background-color: rgba(0, 0, 0, 0.6);
    border-radius: 5px;
    z-index: 100; /* Ensure it's on top */
    color: #fff;
    display: flex;
    gap: 20px; /* Space between score and time */
    font-size: 1.1em;
    pointer-events: none; /* Allow clicks through HUD */
}
#hud span {
    font-weight: bold;
    color: #4CAF50; /* Green */
    min-width: 30px;
    display: inline-block;
}

/* Overlay Styles */
.overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.75);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 110; /* Above HUD */
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.4s ease, visibility 0s 0.4s linear;
}

.overlay.visible {
    opacity: 1;
    visibility: visible;
    transition-delay: 0s;
}

.overlay-content {
    background-color: #2a2a3a;
    color: #eee;
    padding: 30px 40px;
    border-radius: 8px;
    text-align: center;
    box-shadow: 0 5px 15px rgba(0,0,0,0.5);
    transform: scale(0.9);
    transition: transform 0.4s ease;
}

.overlay.visible .overlay-content {
     transform: scale(1);
}

.overlay h2 {
    color: #00ffff; /* Cyan */
    margin-bottom: 15px;
}

.overlay p {
    margin-bottom: 10px;
    line-height: 1.5;
}
.overlay span {
     font-weight: bold;
     color: #4CAF50; /* Green */
}

.overlay button {
    padding: 10px 25px;
    font-size: 1.1em;
    font-weight: 600;
    color: #333;
    background-color: #00ffff; /* Cyan */
    border: none;
    border-radius: 5px;
    cursor: pointer;
    margin-top: 15px;
    transition: background-color 0.2s, transform 0.2s;
}

.overlay button:hover {
    background-color: #00dddd;
    transform: translateY(-2px);
}
// Basic check for Three.js and OrbitControls
if (typeof THREE === 'undefined' || typeof THREE.OrbitControls === 'undefined') {
    alert('Error: Three.js or OrbitControls library not loaded!');
} else {
    // --- DOM Elements ---
    const scoreDisplay = document.getElementById('score');
    const timeDisplay = document.getElementById('time');
    const startScreen = document.getElementById('start-screen');
    const gameOverScreen = document.getElementById('game-over-screen');
    const startButton = document.getElementById('start-button');
    const restartButton = document.getElementById('restart-button');
    const finalScoreDisplay = document.getElementById('final-score');
    const clickSound = document.getElementById('click-sound'); // Audio element
    const container = document.getElementById('container');

    // --- Game Config ---
    const INITIAL_TIME = 60; // seconds
    const BALL_COUNT = 100;
    const TETRA_RADIUS = 10;
    const BALL_RADIUS = 0.4;
    const BOUNDS = TETRA_RADIUS * 0.8; // Approximate inner bounds

    // --- Game State ---
    let score = 0;
    let timeLeft = INITIAL_TIME;
    let gameActive = false;
    let timerInterval = null;
    let balls = []; // Holds ball mesh objects
    let ballsToRemove = []; // Buffer for balls clicked this frame

    // --- Scene Setup ---
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 25;

    // --- Renderer Setup ---
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    container.appendChild(renderer.domElement);

    // --- Orbit Controls ---
    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.screenSpacePanning = false;
    controls.minDistance = 5;
    controls.maxDistance = 100;

    // --- Lighting ---
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(ambientLight);
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(5, 10, 7.5);
    scene.add(directionalLight);

    // --- Tetrahedron Container ---
    const tetraGeometry = new THREE.TetrahedronGeometry(TETRA_RADIUS);
    const tetraMaterial = new THREE.MeshBasicMaterial({
        color: 0x00ffff,
        wireframe: true,
        transparent: true, // Make slightly transparent
        opacity: 0.5     // To see balls inside better
    });
    const tetrahedronMesh = new THREE.Mesh(tetraGeometry, tetraMaterial);
    scene.add(tetrahedronMesh);

    // --- Ball Creation Function ---
    function createBall() {
        const ballGeometry = new THREE.SphereGeometry(BALL_RADIUS, 12, 12); // Lower segments
        const ballMaterial = new THREE.MeshStandardMaterial({
            color: Math.random() * 0xffffff,
            roughness: 0.5,
            metalness: 0.2,
        });
        const ballMesh = new THREE.Mesh(ballGeometry, ballMaterial);

        ballMesh.position.set(
            (Math.random() - 0.5) * 2 * BOUNDS * 0.8, // Start closer to center
            (Math.random() - 0.5) * 2 * BOUNDS * 0.8,
            (Math.random() - 0.5) * 2 * BOUNDS * 0.8
        );

        ballMesh.userData.velocity = new THREE.Vector3(
            (Math.random() - 0.5) * 0.2,
            (Math.random() - 0.5) * 0.2,
            (Math.random() - 0.5) * 0.2
        );
        // Add a flag to prevent clicking the same ball multiple times per frame
        ballMesh.userData.isClicked = false;

        return ballMesh;
    }

    // --- Initialize Balls ---
    function initBalls() {
        // Remove existing balls first if any
        balls.forEach(ball => scene.remove(ball));
        balls = []; // Clear the array

        for (let i = 0; i < BALL_COUNT; i++) {
            const ballMesh = createBall();
            balls.push(ballMesh);
            scene.add(ballMesh);
        }
    }

    // --- Raycasting ---
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();

    function onMouseClick(event) {
        if (!gameActive) return; // Only register clicks if game is active

        // Calculate mouse position in normalized device coordinates (-1 to +1)
        mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

        // Update the picking ray with the camera and mouse position
        raycaster.setFromCamera(mouse, camera);

        // Calculate objects intersecting the picking ray
        // Check only against the balls array for efficiency
        const intersects = raycaster.intersectObjects(balls);

        if (intersects.length > 0) {
            const clickedBall = intersects[0].object; // Closest intersected object

            // Check if already clicked this frame/cycle
            if (!clickedBall.userData.isClicked) {
                 handleBallClick(clickedBall);
            }
        }
    }
    container.addEventListener('click', onMouseClick);


    // --- Tetrahedron Rotation Control ---
    let lastMouseX = null, lastMouseY = null;
    const tetraRotationSpeed = 0.008;

    function onMouseMove(event) {
        // Rotate tetrahedron only if game is active and mouse isn't dragging for OrbitControls
        if (!gameActive || controls.state !== -1) { // -1 is the idle state for OrbitControls
             lastMouseX = null; // Reset when dragging camera
             lastMouseY = null;
             return;
        }

        if(lastMouseX !== null && lastMouseY !== null) {
            const deltaX = event.clientX - lastMouseX;
            const deltaY = event.clientY - lastMouseY;

            // Apply rotation based on mouse movement delta
            // Rotate around Y axis based on horizontal movement
            tetrahedronMesh.rotation.y += deltaX * tetraRotationSpeed;
            // Rotate around X axis based on vertical movement
            tetrahedronMesh.rotation.x += deltaY * tetraRotationSpeed;
        }

        lastMouseX = event.clientX;
        lastMouseY = event.clientY;
    }
     container.addEventListener('mousemove', onMouseMove);
     // Reset lastMouse on mouse leave to prevent jump on re-entry
     container.addEventListener('mouseleave', () => {
         lastMouseX = null;
         lastMouseY = null;
     });


    // --- Game Logic Functions ---
    function updateHUD() {
        scoreDisplay.textContent = score;
        timeDisplay.textContent = Math.max(0, timeLeft); // Ensure time doesn't go below 0
    }

    function handleBallClick(ball) {
         // Mark as clicked to prevent multi-trigger
         ball.userData.isClicked = true;

         score++;
         updateHUD();
         playSound(clickSound);

         // Add to removal buffer instead of removing immediately
         ballsToRemove.push(ball);

         // Optional: Add visual feedback (e.g., scale up/down quickly)
         // new TWEEN.Tween(ball.scale)
         //    .to({ x: 1.5, y: 1.5, z: 1.5 }, 100)
         //    .yoyo(true).repeat(1) // Scale up then back down
         //    .start();
    }

    function processBallRemovals() {
         if (ballsToRemove.length === 0) return;

         ballsToRemove.forEach(ball => {
             scene.remove(ball);
             // Remove from the main balls array
             const index = balls.indexOf(ball);
             if (index > -1) {
                 balls.splice(index, 1);
             }
         });
         // Clear the buffer
         ballsToRemove = [];
    }


    function startGame() {
        score = 0;
        timeLeft = INITIAL_TIME;
        gameActive = true;
        startScreen.classList.remove('visible');
        gameOverScreen.classList.remove('visible');
        initBalls(); // Create fresh set of balls
        updateHUD();
        startTimer();
    }

    function endGame() {
        gameActive = false;
        stopTimer();
        finalScoreDisplay.textContent = score;
        gameOverScreen.classList.add('visible');
    }

    function restartGame() {
        gameOverScreen.classList.remove('visible');
        startGame();
    }

    // --- Timer ---
    function startTimer() {
        stopTimer(); // Clear existing timer
        timerInterval = setInterval(() => {
            timeLeft--;
            updateHUD();
            if (timeLeft <= 0) {
                endGame();
            }
        }, 1000);
    }

    function stopTimer() {
        clearInterval(timerInterval);
        timerInterval = null;
    }

    // --- Sound Utility ---
    function playSound(audioElement) {
        if (audioElement && typeof audioElement.play === 'function') {
            audioElement.currentTime = 0;
            audioElement.play().catch(e => console.warn("Audio play failed:", e.message)); // Catch potential errors
        }
    }

    // --- Event Listeners for Buttons ---
    startButton.addEventListener('click', startGame);
    restartButton.addEventListener('click', restartGame);

    // --- Window Resize Handling ---
    window.addEventListener('resize', () => {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    });

    // --- Animation Loop ---
    function animate() {
        requestAnimationFrame(animate); // Loop animation

        // Update TWEEN if using it for effects
        // TWEEN.update();

        // Process any pending ball removals
        processBallRemovals();

        // Animate remaining balls only if game is active
        if (gameActive) {
            balls.forEach(ball => {
                 // Mark as not clicked for the next frame/interaction
                 ball.userData.isClicked = false;

                // Move ball
                ball.position.add(ball.userData.velocity);

                // Boundary check and bounce
                if (Math.abs(ball.position.x) >= BOUNDS - BALL_RADIUS) {
                    ball.userData.velocity.x *= -1;
                    ball.position.x = Math.sign(ball.position.x) * (BOUNDS - BALL_RADIUS);
                }
                if (Math.abs(ball.position.y) >= BOUNDS - BALL_RADIUS) {
                    ball.userData.velocity.y *= -1;
                    ball.position.y = Math.sign(ball.position.y) * (BOUNDS - BALL_RADIUS);
                }
                if (Math.abs(ball.position.z) >= BOUNDS - BALL_RADIUS) {
                    ball.userData.velocity.z *= -1;
                    ball.position.z = Math.sign(ball.position.z) * (BOUNDS - BALL_RADIUS);
                }
            });
        }

        // Update OrbitControls
        controls.update();

        // Render the scene
        renderer.render(scene, camera);
    }

    // --- Initial Setup ---
    updateHUD(); // Show initial state
    // Don't initBalls() here, wait for Start button
    animate(); // Start rendering loop
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.