<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <title>Fruit Fall 3D</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <style> body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f0f0; } #gameCanvas { display: block; } #ui-container { position: absolute; top: 10px; left: 10px; padding: 10px; background-color: rgba(0,0,0,0.5); color: white; border-radius: 5px; z-index: 100; } #score { font-size: 1.5em; margin-bottom: 5px; } #start-button, #reset-button { padding: 8px 15px; font-size: 1em; background-color: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer; margin-top: 5px; } #reset-button { background-color: #f44336; display: none;} #game-over-message { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; background-color: rgba(0,0,0,0.7); padding: 20px; border-radius: 10px; font-size: 2em; text-align: center; z-index: 101; display: none; /* Hidden by default */ } </style> </head> <body> <div id="ui-container"> <div id="score">Score: 0</div> <button id="start-button">Start Game</button> <button id="reset-button">Play Again</button> </div> <div id="game-over-message"> Game Over!<br> <span id="final-score-message"></span> </div> <canvas id="gameCanvas"></canvas> <script> let scene, camera, renderer; let fruits = []; let score = 0; let gameActive = false; let raycaster, mouse; // For click detection let audioContext; let gameTimer, timeLeft; const GAME_DURATION = 30; // seconds const scoreElement = document.getElementById('score'); const startButton = document.getElementById('start-button'); const resetButton = document.getElementById('reset-button'); const gameOverMessageDiv = document.getElementById('game-over-message'); const finalScoreMessageSpan = document.getElementById('final-score-message'); const fruitTypes = [ { name: 'Mango', color: 0xFFBF00, size: 0.5, points: 10 }, { name: 'Apple', color: 0xFF0000, size: 0.4, points: 10 }, { name: 'Banana', color: 0xFFFF00, size: 0.35, points: 15 }, // Represented as sphere { name: 'Grapes', color: 0x800080, size: 0.3, points: 20 }, { name: 'Watermelon', color: 0x00FF00, size: 0.7, points: 5 } ]; function initAudio() { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } } function playSound(type = 'click', freq = 440, duration = 0.05) { if (!audioContext) return; const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.type = (type === 'miss') ? 'sawtooth' : 'triangle'; oscillator.frequency.setValueAtTime(freq, audioContext.currentTime); gainNode.gain.setValueAtTime(0.15, audioContext.currentTime); // Lowered volume gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + duration); oscillator.start(); oscillator.stop(audioContext.currentTime + duration); } function init() { // Scene scene = new THREE.Scene(); scene.background = new THREE.Color(0x87CEEB); // Sky blue // Camera camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 2, 7); // Positioned to see the falling fruits well camera.lookAt(0, 0, 0); // Renderer renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('gameCanvas'), antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; // Lighting const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(5, 10, 7); directionalLight.castShadow = true; scene.add(directionalLight); // Ground (optional, for visual reference) const groundGeometry = new THREE.PlaneGeometry(20, 20); const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22, side: THREE.DoubleSide }); // Forest green const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; ground.position.y = -5; // Fruits fall past this ground.receiveShadow = true; scene.add(ground); // Raycaster for mouse clicks raycaster = new THREE.Raycaster(); mouse = new THREE.Vector2(); // Event Listeners window.addEventListener('resize', onWindowResize, false); renderer.domElement.addEventListener('click', onClick, false); // For desktop renderer.domElement.addEventListener('touchstart', onTouch, false); // For mobile startButton.addEventListener('click', startGame); resetButton.addEventListener('click', startGame); // Reset button also starts the game } function startGame() { initAudio(); score = 0; timeLeft = GAME_DURATION; updateScoreDisplay(); gameActive = true; fruits.forEach(fruit => scene.remove(fruit.mesh)); // Clear existing fruits fruits = []; startButton.style.display = 'none'; resetButton.style.display = 'none'; gameOverMessageDiv.style.display = 'none'; if (gameTimer) clearInterval(gameTimer); gameTimer = setInterval(() => { timeLeft--; // Optionally display timer: scoreElement.textContent = `Score: ${score} | Time: ${timeLeft}`; if (timeLeft <= 0) { endGame(); } }, 1000); spawnFruitLoop(); // Start spawning fruits if (!renderer.xr.isPresenting) animate(); // Ensure animate isn't called twice if in XR } function endGame() { gameActive = false; clearInterval(gameTimer); clearTimeout(fruitSpawnTimeout); // Stop spawning new fruits finalScoreMessageSpan.textContent = `Your Score: ${score}`; gameOverMessageDiv.style.display = 'flex'; resetButton.style.display = 'inline-block'; } let fruitSpawnTimeout; function spawnFruitLoop() { if (!gameActive) return; createFruit(); const spawnInterval = Math.random() * 1500 + 500; // 0.5 to 2 seconds fruitSpawnTimeout = setTimeout(spawnFruitLoop, spawnInterval); } function createFruit() { if (!gameActive) return; const type = fruitTypes[Math.floor(Math.random() * fruitTypes.length)]; const geometry = new THREE.SphereGeometry(type.size, 16, 12); const material = new THREE.MeshStandardMaterial({ color: type.color, roughness: 0.5, metalness: 0.1 }); const fruitMesh = new THREE.Mesh(geometry, material); fruitMesh.castShadow = true; // Random spawn position at the top fruitMesh.position.x = (Math.random() - 0.5) * 10; // Range: -5 to 5 fruitMesh.position.y = 7 + Math.random() * 3; // Start above camera view fruitMesh.position.z = (Math.random() - 0.5) * 4; // Some depth variation fruitMesh.userData = { type: type.name, points: type.points, fallSpeed: 0.02 + Math.random() * 0.03 }; scene.add(fruitMesh); fruits.push({ mesh: fruitMesh, data: fruitMesh.userData }); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } function handleInteraction(clientX, clientY) { if (!gameActive) return; mouse.x = (clientX / window.innerWidth) * 2 - 1; mouse.y = -(clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const fruitMeshes = fruits.map(f => f.mesh); const intersects = raycaster.intersectObjects(fruitMeshes); if (intersects.length > 0) { const clickedFruitMesh = intersects[0].object; const fruitIndex = fruits.findIndex(f => f.mesh === clickedFruitMesh); if (fruitIndex !== -1) { const fruitData = fruits[fruitIndex].data; score += fruitData.points; updateScoreDisplay(); playSound('click', 300 + Math.random()*200); // Simple "pop" effect: shrink and remove // More complex particle effects are possible but add overhead clickedFruitMesh.scale.set(0.1,0.1,0.1); // Visually shrink setTimeout(() => { // Remove after slight delay scene.remove(clickedFruitMesh); }, 50); fruits.splice(fruitIndex, 1); } } } function onClick(event) { handleInteraction(event.clientX, event.clientY); } function onTouch(event) { if (event.touches.length > 0) { handleInteraction(event.touches[0].clientX, event.touches[0].clientY); } } function updateScoreDisplay() { scoreElement.textContent = `Score: ${score} | Time: ${timeLeft}`; } function animate() { if (!gameActive && timeLeft <= 0) { // If game ended, stop animation loop return; } requestAnimationFrame(animate); if (gameActive) { // Fruit falling logic for (let i = fruits.length - 1; i >= 0; i--) { const fruitObj = fruits[i]; fruitObj.mesh.position.y -= fruitObj.data.fallSpeed; fruitObj.mesh.rotation.x += 0.01; fruitObj.mesh.rotation.y += 0.01; // Remove fruit if it falls below a certain point (miss) if (fruitObj.mesh.position.y < -6) { // Below the ground plane scene.remove(fruitObj.mesh); fruits.splice(i, 1); // playSound('miss', 150); // Optional miss sound // score -= 5; // Optional penalty updateScoreDisplay(); } } } renderer.render(scene, camera); } // --- Start --- init(); // startGame(); // Initial call to animate if not waiting for button </script> </body> </html>