Spaces:
Running
Running
| // Game variables | |
| let scene, camera, renderer, controls; | |
| let player, walls = [], enemies = [], coins = [], emojis = [], powerups = []; | |
| let score = 0, timeLeft = 60, gameActive = false, gameOver = false; | |
| let playerSpeed = 5, invincible = false, doublePoints = false; | |
| let clock = new THREE.Clock(); | |
| let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false; | |
| // Audio elements | |
| const bgMusic = document.getElementById('bg-music'); | |
| const coinSound = document.getElementById('coin-sound'); | |
| const enemySound = document.getElementById('enemy-sound'); | |
| const winSound = document.getElementById('win-sound'); | |
| const loseSound = document.getElementById('lose-sound'); | |
| // UI elements | |
| const scoreDisplay = document.getElementById('score-display'); | |
| const timerDisplay = document.getElementById('timer'); | |
| // Initialize the game | |
| function init() { | |
| // Create scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x87CEEB); | |
| // Create camera | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(0, 1.6, 0); | |
| // Create renderer | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| document.body.appendChild(renderer.domElement); | |
| // Add pointer lock controls | |
| controls = new THREE.PointerLockControls(camera, renderer.domElement); | |
| // Create player | |
| createPlayer(); | |
| // Generate maze | |
| generateMaze(); | |
| // Add lights | |
| addLights(); | |
| // Add event listeners | |
| setupEventListeners(); | |
| // Start render loop | |
| animate(); | |
| // Setup restart button | |
| document.getElementById('restart-btn').addEventListener('click', restartGame); | |
| } | |
| // Create player | |
| function createPlayer() { | |
| // Create player with better 3D model | |
| const group = new THREE.Group(); | |
| // Body | |
| const body = new THREE.Mesh( | |
| new THREE.BoxGeometry(0.5, 1, 0.3), | |
| new THREE.MeshStandardMaterial({ color: 0x4ECDC4 }) | |
| ); | |
| body.position.y = 0.5; | |
| // Head | |
| const head = new THREE.Mesh( | |
| new THREE.SphereGeometry(0.3, 16, 16), | |
| new THREE.MeshStandardMaterial({ color: 0xFFD166 }) | |
| ); | |
| head.position.y = 1.15; | |
| // Eyes | |
| const eyeGeometry = new THREE.SphereGeometry(0.05, 8, 8); | |
| const leftEye = new THREE.Mesh(eyeGeometry, new THREE.MeshStandardMaterial({ color: 0x000000 })); | |
| const rightEye = new THREE.Mesh(eyeGeometry, new THREE.MeshStandardMaterial({ color: 0x000000 })); | |
| leftEye.position.set(0.1, 1.15, 0.25); | |
| rightEye.position.set(-0.1, 1.15, 0.25); | |
| group.add(body); | |
| group.add(head); | |
| group.add(leftEye); | |
| group.add(rightEye); | |
| player = group; | |
| player.castShadow = true; | |
| player.position.y = 0.85; | |
| scene.add(player); | |
| } | |
| // Generate maze | |
| function generateMaze() { | |
| const size = 30; | |
| const gridSize = 15; | |
| // Add skybox | |
| const skyGeometry = new THREE.BoxGeometry(100, 100, 100); | |
| const skyMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0x87CEEB, | |
| side: THREE.BackSide | |
| }); | |
| const skybox = new THREE.Mesh(skyGeometry, skyMaterial); | |
| scene.add(skybox); | |
| const maze = []; | |
| // Create grid | |
| for (let i = 0; i < gridSize; i++) { | |
| maze[i] = []; | |
| for (let j = 0; j < gridSize; j++) { | |
| maze[i][j] = Math.random() > 0.7 ? 1 : 0; | |
| } | |
| } | |
| // Ensure exit path | |
| maze[0][0] = 0; | |
| maze[gridSize-1][gridSize-1] = 0; | |
| // Create walls | |
| for (let i = 0; i < gridSize; i++) { | |
| for (let j = 0; j < gridSize; j++) { | |
| if (maze[i][j] === 1) { | |
| const wall = new THREE.Mesh( | |
| new THREE.BoxGeometry(size/gridSize, 2, size/gridSize), | |
| new THREE.MeshStandardMaterial({ color: 0xFFD166 }) | |
| ); | |
| wall.position.x = (i - gridSize/2) * (size/gridSize); | |
| wall.position.z = (j - gridSize/2) * (size/gridSize); | |
| wall.position.y = 1; | |
| wall.castShadow = true; | |
| wall.receiveShadow = true; | |
| scene.add(wall); | |
| walls.push(wall); | |
| } | |
| } | |
| } | |
| // Create coins | |
| for (let i = 0; i < 30; i++) { | |
| createCoin(); | |
| } | |
| // Create enemies | |
| for (let i = 0; i < 5; i++) { | |
| createEnemy(); | |
| } | |
| // Create powerups | |
| for (let i = 0; i < 3; i++) { | |
| createPowerup(); | |
| } | |
| // Create exit | |
| const exit = new THREE.Mesh( | |
| new THREE.BoxGeometry(size/gridSize, 2, size/gridSize), | |
| new THREE.MeshStandardMaterial({ color: 0x06D6A0, transparent: true, opacity: 0.5 }) | |
| ); | |
| exit.position.x = (gridSize/2 - 0.5) * (size/gridSize); | |
| exit.position.z = (gridSize/2 - 0.5) * (size/gridSize); | |
| exit.position.y = 1; | |
| scene.add(exit); | |
| exit.isExit = true; | |
| walls.push(exit); | |
| } | |
| function createCoin() { | |
| const coin = new THREE.Mesh( | |
| new THREE.CylinderGeometry(0.3, 0.3, 0.1, 32), | |
| new THREE.MeshStandardMaterial({ | |
| color: 0xFFD700, | |
| metalness: 0.9, | |
| roughness: 0.1 | |
| }) | |
| ); | |
| coin.castShadow = true; | |
| coin.rotation.x = Math.PI / 2; | |
| coin.position.set( | |
| (Math.random() - 0.5) * 18, | |
| 0.5, | |
| (Math.random() - 0.5) * 18 | |
| ); | |
| coin.castShadow = true; | |
| coin.isCoin = true; | |
| scene.add(coin); | |
| coins.push(coin); | |
| } | |
| function createEnemy() { | |
| const enemy = new THREE.Mesh( | |
| new THREE.ConeGeometry(0.4, 1.2, 4), | |
| new THREE.MeshStandardMaterial({ | |
| color: 0xFF6B6B, | |
| emissive: 0x880000, | |
| emissiveIntensity: 0.2 | |
| }) | |
| ); | |
| enemy.position.set( | |
| (Math.random() - 0.5) * 18, | |
| 0.5, | |
| (Math.random() - 0.5) * 18 | |
| ); | |
| enemy.castShadow = true; | |
| enemy.speed = 0.02 + Math.random() * 0.03; | |
| enemy.direction = new THREE.Vector3(Math.random() - 0.5, 0, Math.random() - 0.5).normalize(); | |
| scene.add(enemy); | |
| enemies.push(enemy); | |
| // Create emoji above enemy | |
| const emoji = document.createElement('div'); | |
| emoji.className = 'enemy-emote'; | |
| emoji.textContent = ['😈', '👹', '👻', '🤡', '👾'][Math.floor(Math.random() * 5)]; | |
| document.getElementById('ui').appendChild(emo); | |
| emojis.push({ element: emoji, enemy: enemy }); | |
| } | |
| function addLights() { | |
| // Ambient light | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); | |
| scene.add(ambientLight); | |
| // Directional light (sun) | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(10, 20, 10); | |
| directionalLight.castShadow = true; | |
| directionalLight.shadow.mapSize.width = 2048; | |
| directionalLight.shadow.mapSize.height = 2048; | |
| directionalLight.shadow.camera.near = 0.5; | |
| directionalLight.shadow.camera.far = 50; | |
| directionalLight.shadow.camera.left = -20; | |
| directionalLight.shadow.camera.right = 20; | |
| directionalLight.shadow.camera.top = 20; | |
| directionalLight.shadow.camera.bottom = -20; | |
| scene.add(directionalLight); | |
| // Hemisphere light for more natural outdoor lighting | |
| const hemisphereLight = new THREE.HemisphereLight(0x87CEEB, 0x006400, 0.3); | |
| scene.add(hemisphereLight); | |
| // Add some point lights for better 3D effect | |
| const pointLight1 = new THREE.PointLight(0xFFD166, 0.5, 10); | |
| pointLight1.position.set(5, 3, 5); | |
| scene.add(pointLight1); | |
| const pointLight2 = new THREE.PointLight(0x4ECDC4, 0.5, 10); | |
| pointLight2.position.set(-5, 3, -5); | |
| scene.add(pointLight2); | |
| } | |
| function setupEventListeners() { | |
| // Movement controls | |
| document.addEventListener('keydown', (event) => { | |
| switch (event.code) { | |
| case 'KeyW': | |
| case 'ArrowUp': moveForward = true; break; | |
| case 'KeyA': | |
| case 'ArrowLeft': moveLeft = true; break; | |
| case 'KeyS': | |
| case 'ArrowDown': moveBackward = true; break; | |
| case 'KeyD': | |
| case 'ArrowRight': moveRight = true; break; | |
| } | |
| }); | |
| document.addEventListener('keyup', (event) => { | |
| switch (event.code) { | |
| case 'KeyW': | |
| case 'ArrowUp': moveForward = false; break; | |
| case 'KeyA': | |
| case 'ArrowLeft': moveLeft = false; break; | |
| case 'KeyS': | |
| case 'ArrowDown': moveBackward = false; break; | |
| case 'KeyD': | |
| case 'ArrowRight': moveRight = false; break; | |
| } | |
| }); | |
| // Touch controls | |
| const upBtn = document.getElementById('up'); | |
| const leftBtn = document.getElementById('left'); | |
| const downBtn = document.getElementById('down'); | |
| const rightBtn = document.getElementById('right'); | |
| const lookBtn = document.getElementById('look'); | |
| // Touch start events | |
| upBtn.addEventListener('touchstart', (e) => { e.preventDefault(); moveForward = true; }); | |
| leftBtn.addEventListener('touchstart', (e) => { e.preventDefault(); moveLeft = true; }); | |
| downBtn.addEventListener('touchstart', (e) => { e.preventDefault(); moveBackward = true; }); | |
| rightBtn.addEventListener('touchstart', (e) => { e.preventDefault(); moveRight = true; }); | |
| lookBtn.addEventListener('touchstart', (e) => { | |
| e.preventDefault(); | |
| if (!controls.isLocked) { | |
| controls.lock(); | |
| } | |
| }); | |
| // Touch end events | |
| upBtn.addEventListener('touchend', (e) => { e.preventDefault(); moveForward = false; }); | |
| leftBtn.addEventListener('touchend', (e) => { e.preventDefault(); moveLeft = false; }); | |
| downBtn.addEventListener('touchend', (e) => { e.preventDefault(); moveBackward = false; }); | |
| rightBtn.addEventListener('touchend', (e) => { e.preventDefault(); moveRight = false; }); | |
| // Mouse events for desktop | |
| upBtn.addEventListener('mousedown', () => moveForward = true); | |
| leftBtn.addEventListener('mousedown', () => moveLeft = true); | |
| downBtn.addEventListener('mousedown', () => moveBackward = true); | |
| rightBtn.addEventListener('mousedown', () => moveRight = true); | |
| lookBtn.addEventListener('mousedown', () => { | |
| if (!controls.isLocked) { | |
| controls.lock(); | |
| } | |
| }); | |
| upBtn.addEventListener('mouseup', () => moveForward = false); | |
| leftBtn.addEventListener('mouseup', () => moveLeft = false); | |
| downBtn.addEventListener('mouseup', () => moveBackward = false); | |
| rightBtn.addEventListener('mouseup', () => moveRight = false); | |
| // Pointer lock controls | |
| const lockChangeAlert = () => { | |
| if (document.pointerLockElement === renderer.domElement) { | |
| controls.isLocked = true; | |
| } else { | |
| controls.isLocked = false; | |
| } | |
| }; | |
| document.addEventListener('pointerlockchange', lockChangeAlert, false); | |
| // Window resize | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| } | |
| function startGame() { | |
| // This function is now empty since we start the game automatically | |
| } | |
| function restartGame() { | |
| // Reset all game state | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x87CEEB); | |
| // Clear all arrays | |
| walls = []; | |
| enemies = []; | |
| coins = []; | |
| powerups = []; | |
| emojis.forEach(emoji => emoji.element.remove()); | |
| emojis = []; | |
| // Reset variables | |
| score = 0; | |
| timeLeft = 60; | |
| gameActive = false; | |
| gameOver = false; | |
| playerSpeed = 5; | |
| invincible = false; | |
| doublePoints = false; | |
| // Update UI | |
| scoreDisplay.textContent = `Coins: ${score}`; | |
| timerDisplay.textContent = `Time: ${timeLeft}`; | |
| // Hide end screen | |
| document.getElementById('end-screen').style.display = 'none'; | |
| document.getElementById('start-screen').style.display = 'flex'; | |
| // Rebuild game elements | |
| createPlayer(); | |
| generateMaze(); | |
| addLights(); | |
| // Reattach camera | |
| controls.getObject().position.set(0, 1.6, 0); | |
| camera.position.set(0, 1.6, 0); | |
| camera.lookAt(0, 1.6, -1); | |
| } | |
| function endGame(win) { | |
| gameOver = true; | |
| gameActive = false; | |
| if (controls.isLocked) { | |
| controls.unlock(); | |
| } | |
| const endScreen = document.getElementById('end-screen'); | |
| const endMessage = document.getElementById('end-message'); | |
| endScreen.style.display = 'flex'; | |
| if (win) { | |
| endMessage.textContent = `You Escaped!\nFinal Score: ${score}`; | |
| endMessage.style.animation = 'rainbow 2s infinite'; | |
| winSound.play(); | |
| } else { | |
| endMessage.textContent = `Time's Up!\nFinal Score: ${score}`; | |
| endMessage.style.color = '#FF6B6B'; | |
| loseSound.play(); | |
| } | |
| } | |
| function createPowerup() { | |
| const types = ['speed', 'invincible', 'double']; | |
| const type = types[Math.floor(Math.random() * types.length)]; | |
| let color, shape; | |
| switch(type) { | |
| case 'speed': | |
| color = 0x00FF00; | |
| shape = new THREE.SphereGeometry(0.4); | |
| break; | |
| case 'invincible': | |
| color = 0x0000FF; | |
| shape = new THREE.TetrahedronGeometry(0.5); | |
| break; | |
| case 'double': | |
| color = 0xFF00FF; | |
| shape = new THREE.OctahedronGeometry(0.4); | |
| break; | |
| } | |
| const powerup = new THREE.Mesh( | |
| shape, | |
| new THREE.MeshStandardMaterial({ | |
| color: color, | |
| emissive: color, | |
| emissiveIntensity: 0.3 | |
| }) | |
| ); | |
| powerup.position.set( | |
| (Math.random() - 0.5) * 28, | |
| 0.5, | |
| (Math.random() - 0.5) * 28 | |
| ); | |
| powerup.castShadow = true; | |
| powerup.type = type; | |
| scene.add(powerup); | |
| powerups.push(powerup); | |
| } | |
| function checkCollisions() { | |
| // Check coin collisions | |
| for (let i = coins.length - 1; i >= 0; i--) { | |
| const coin = coins[i]; | |
| if (player.position.distanceTo(coin.position) < 1) { | |
| scene.remove(coin); | |
| coins.splice(i, 1); | |
| score += doublePoints ? 20 : 10; | |
| scoreDisplay.textContent = `Coins: ${score}`; | |
| coinSound.play(); | |
| createCoin(); | |
| } | |
| } | |
| // Check powerup collisions | |
| for (let i = powerups.length - 1; i >= 0; i--) { | |
| const powerup = powerups[i]; | |
| if (player.position.distanceTo(powerup.position) < 1) { | |
| scene.remove(powerup); | |
| powerups.splice(i, 1); | |
| switch(powerup.type) { | |
| case 'speed': | |
| playerSpeed = 8; | |
| setTimeout(() => playerSpeed = 5, 10000); | |
| break; | |
| case 'invincible': | |
| invincible = true; | |
| setTimeout(() => invincible = false, 15000); | |
| break; | |
| case 'double': | |
| doublePoints = true; | |
| setTimeout(() => doublePoints = false, 20000); | |
| break; | |
| } | |
| coinSound.play(); | |
| } | |
| } | |
| // Check wall collisions | |
| const playerBox = new THREE.Box3().setFromObject(player); | |
| for (const wall of walls) { | |
| const wallBox = new THREE.Box3().setFromObject(wall); | |
| if (playerBox.intersectsBox(wallBox)) { | |
| if (wall.isExit) { | |
| endGame(true); | |
| } else { | |
| // Simple collision response | |
| const direction = new THREE.Vector3().subVectors(player.position, wall.position).normalize(); | |
| player.position.add(direction.multiplyScalar(0.1)); | |
| } | |
| } | |
| } | |
| // Check enemy collisions | |
| for (let i = 0; i < enemies.length; i++) { | |
| const enemy = enemies[i]; | |
| if (player.position.distanceTo(enemy.position) < 1 && !invincible) { | |
| score = Math.max(0, score - 5); | |
| scoreDisplay.textContent = `Coins: ${score}`; | |
| enemySound.play(); | |
| // Push player away | |
| const direction = new THREE.Vector3().subVectors(player.position, enemy.position).normalize(); | |
| player.position.add(direction.multiplyScalar(1.5)); | |
| } | |
| } | |
| } | |
| function updateEnemies() { | |
| for (let i = 0; i < enemies.length; i++) { | |
| const enemy = enemies[i]; | |
| // Simple AI - change direction randomly | |
| if (Math.random() < 0.01) { | |
| enemy.direction = new THREE.Vector3(Math.random() - 0.5, 0, Math.random() - 0.5).normalize(); | |
| } | |
| // Move enemy | |
| enemy.position.add(enemy.direction.clone().multiplyScalar(enemy.speed)); | |
| // Boundary check | |
| if (enemy.position.x > 9 || enemy.position.x < -9 || enemy.position.z > 9 || enemy.position.z < -9) { | |
| enemy.direction.negate(); | |
| } | |
| // Wall collision | |
| const enemyBox = new THREE.Box3().setFromObject(enemy); | |
| for (const wall of walls) { | |
| const wallBox = new THREE.Box3().setFromObject(wall); | |
| if (enemyBox.intersectsBox(wallBox) && !wall.isExit) { | |
| enemy.direction.negate(); | |
| break; | |
| } | |
| } | |
| // Update emoji position | |
| if (emojis[i]) { | |
| const emoji = emojis[i].element; | |
| const screenPos = enemy.position.clone().project(camera); | |
| emoji.style.left = `${(screenPos.x * 0.5 + 0.5) * window.innerWidth}px`; | |
| emoji.style.top = `${(-(screenPos.y * 0.5) + 0.5) * window.innerHeight - 50}px`; | |
| // Show emoji when enemy is close | |
| const distance = player.position.distanceTo(enemy.position); | |
| emoji.style.opacity = distance < 5 ? '1' : '0'; | |
| } | |
| } | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const delta = clock.getDelta(); | |
| if (gameActive && controls.isLocked) { | |
| // Player movement | |
| const speed = playerSpeed * delta; | |
| if (moveForward) { | |
| controls.moveForward(speed); | |
| } | |
| if (moveBackward) { | |
| controls.moveForward(-speed); | |
| } | |
| if (moveLeft) { | |
| controls.moveRight(-speed); | |
| } | |
| if (moveRight) { | |
| controls.moveRight(speed); | |
| } | |
| // Update player position | |
| player.position.copy(controls.getObject().position); | |
| player.position.y = 0.85; | |
| // Check collisions | |
| checkCollisions(); | |
| // Update enemies | |
| updateEnemies(); | |
| } | |
| renderer.render(scene, camera); | |
| } | |
| // Start the game | |
| init(); |