Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Galactic Defender | Advanced Space Shooter</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/GLTFLoader.js"></script> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| background-color: #000; | |
| font-family: 'Orbitron', sans-serif; | |
| touch-action: none; | |
| } | |
| canvas { | |
| display: block; | |
| } | |
| #ui { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| } | |
| #score-display { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| color: #0ff; | |
| font-size: 24px; | |
| text-shadow: 0 0 10px #0ff; | |
| } | |
| #health-container { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| width: 200px; | |
| height: 30px; | |
| background: rgba(255,0,0,0.2); | |
| border: 2px solid #f00; | |
| border-radius: 15px; | |
| overflow: hidden; | |
| } | |
| #health-bar { | |
| height: 100%; | |
| width: 100%; | |
| background: linear-gradient(90deg, #f00, #ff0); | |
| transition: width 0.3s; | |
| } | |
| #health-text { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| text-align: center; | |
| line-height: 30px; | |
| color: white; | |
| font-size: 14px; | |
| text-shadow: 0 0 5px #000; | |
| } | |
| #game-over { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| color: #f00; | |
| font-size: 48px; | |
| text-align: center; | |
| display: none; | |
| text-shadow: 0 0 20px #f00; | |
| } | |
| #start-screen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0,0,0,0.8); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| color: white; | |
| z-index: 100; | |
| } | |
| #start-button { | |
| margin-top: 30px; | |
| padding: 15px 40px; | |
| background: linear-gradient(45deg, #0ff, #00f); | |
| border: none; | |
| border-radius: 50px; | |
| color: white; | |
| font-size: 24px; | |
| cursor: pointer; | |
| pointer-events: auto; | |
| box-shadow: 0 0 20px #0ff; | |
| transition: all 0.3s; | |
| } | |
| #start-button:hover { | |
| transform: scale(1.1); | |
| box-shadow: 0 0 30px #0ff; | |
| } | |
| .title { | |
| font-size: 72px; | |
| margin-bottom: 20px; | |
| background: linear-gradient(45deg, #0ff, #00f); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| text-shadow: 0 0 20px rgba(0, 255, 255, 0.5); | |
| } | |
| .instructions { | |
| max-width: 600px; | |
| text-align: center; | |
| margin-bottom: 30px; | |
| line-height: 1.6; | |
| color: #aaa; | |
| } | |
| .powerup-indicator { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| color: #ff0; | |
| font-size: 18px; | |
| text-shadow: 0 0 10px #ff0; | |
| display: none; | |
| background: rgba(0,0,0,0.5); | |
| padding: 10px 20px; | |
| border-radius: 20px; | |
| border: 1px solid #ff0; | |
| pointer-events: none; | |
| } | |
| .controls { | |
| position: absolute; | |
| bottom: 20px; | |
| width: 100%; | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| box-sizing: border-box; | |
| pointer-events: none; | |
| } | |
| .control-btn { | |
| width: 80px; | |
| height: 80px; | |
| background: rgba(255,255,255,0.2); | |
| border-radius: 50%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| color: white; | |
| font-size: 24px; | |
| pointer-events: auto; | |
| touch-action: manipulation; | |
| user-select: none; | |
| } | |
| #fire-btn { | |
| background: rgba(255,0,0,0.3); | |
| border: 2px solid #f00; | |
| } | |
| #joystick { | |
| position: absolute; | |
| bottom: 30px; | |
| left: 30px; | |
| width: 120px; | |
| height: 120px; | |
| pointer-events: none; | |
| } | |
| #joystick-base { | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 50%; | |
| position: relative; | |
| } | |
| #joystick-head { | |
| width: 60px; | |
| height: 60px; | |
| background: rgba(0,255,255,0.3); | |
| border-radius: 50%; | |
| position: absolute; | |
| top: 30px; | |
| left: 30px; | |
| transition: transform 0.1s; | |
| border: 2px solid #0ff; | |
| } | |
| @font-face { | |
| font-family: 'Orbitron'; | |
| src: url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap'); | |
| } | |
| .explosion { | |
| position: absolute; | |
| width: 100px; | |
| height: 100px; | |
| background: radial-gradient(circle, rgba(255,100,0,0.8) 0%, rgba(255,0,0,0) 70%); | |
| border-radius: 50%; | |
| transform: translate(-50%, -50%); | |
| pointer-events: none; | |
| animation: explode 0.5s forwards; | |
| } | |
| @keyframes explode { | |
| 0% { transform: translate(-50%, -50%) scale(0); opacity: 1; } | |
| 100% { transform: translate(-50%, -50%) scale(3); opacity: 0; } | |
| } | |
| #mobile-controls { | |
| display: none; | |
| } | |
| @media (max-width: 768px) { | |
| #mobile-controls { | |
| display: block; | |
| } | |
| .instructions { | |
| max-width: 90%; | |
| font-size: 14px; | |
| } | |
| .title { | |
| font-size: 48px; | |
| } | |
| } | |
| #debug-info { | |
| position: absolute; | |
| bottom: 10px; | |
| left: 10px; | |
| color: white; | |
| font-size: 12px; | |
| background: rgba(0,0,0,0.5); | |
| padding: 5px; | |
| border-radius: 3px; | |
| } | |
| #desktop-controls { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| color: #aaa; | |
| font-size: 14px; | |
| text-align: center; | |
| background: rgba(0,0,0,0.5); | |
| padding: 10px 20px; | |
| border-radius: 20px; | |
| pointer-events: none; | |
| } | |
| .control-hint { | |
| display: inline-block; | |
| margin: 0 5px; | |
| padding: 2px 8px; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 4px; | |
| border: 1px solid #0ff; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="ui"> | |
| <div id="score-display">SCORE: 0</div> | |
| <div id="health-container"> | |
| <div id="health-bar"></div> | |
| <div id="health-text">100%</div> | |
| </div> | |
| <div id="game-over">GAME OVER<br><span style="font-size: 24px;">Score: 0</span><br><button id="restart-button" style="margin-top: 20px; padding: 10px 20px; background: #f00; color: white; border: none; border-radius: 5px; cursor: pointer; pointer-events: auto;">PLAY AGAIN</button></div> | |
| <div class="powerup-indicator" id="powerup-indicator">DOUBLE FIRE ACTIVE!</div> | |
| <div id="desktop-controls"> | |
| <span class="control-hint">WASD</span> Move | | |
| <span class="control-hint">Mouse</span> Aim | | |
| <span class="control-hint">Space</span> Fire | | |
| <span class="control-hint">Shift</span> Boost | | |
| <span class="control-hint">Q/E</span> Roll | |
| </div> | |
| <div id="mobile-controls"> | |
| <div id="joystick"> | |
| <div id="joystick-base"> | |
| <div id="joystick-head"></div> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <div class="control-btn" id="fire-btn">FIRE</div> | |
| </div> | |
| </div> | |
| <div id="debug-info"></div> | |
| </div> | |
| <div id="start-screen"> | |
| <h1 class="title">GALACTIC DEFENDER</h1> | |
| <div class="instructions"> | |
| <p><strong>DESKTOP CONTROLS:</strong> WASD to move, Mouse to aim, SPACE to fire, SHIFT to boost, Q/E to roll</p> | |
| <p><strong>MOBILE CONTROLS:</strong> Virtual joystick to move, FIRE button to shoot</p> | |
| <p><strong>POWER-UPS:</strong> Green = Health, Yellow = Double Fire</p> | |
| <p>Destroy alien ships before they reach you! Full 3D movement enabled.</p> | |
| </div> | |
| <button id="start-button">START MISSION</button> | |
| </div> | |
| <script> | |
| // Game variables | |
| let score = 0; | |
| let health = 100; | |
| let gameOver = false; | |
| let doubleFireActive = false; | |
| let doubleFireTimeout; | |
| let explosions = []; | |
| let isMobile = false; | |
| let debugMode = false; | |
| // Three.js variables | |
| let scene, camera, renderer; | |
| let player, playerSpeed = 0.8; // Increased base speed | |
| let boostSpeed = 2.0; // Increased boost speed | |
| let currentSpeed = playerSpeed; | |
| let bullets = []; | |
| let enemies = []; | |
| let enemyBullets = []; | |
| let powerups = []; | |
| let lastEnemySpawnTime = 0; | |
| let enemySpawnInterval = 2000; // ms | |
| let lastEnemyShootTime = 0; | |
| let enemyShootInterval = 1000; // ms | |
| let lastPowerupSpawnTime = 0; | |
| let powerupSpawnInterval = 10000; // ms | |
| let clock = new THREE.Clock(); | |
| let starField; | |
| let controls; | |
| let playerDirection = new THREE.Vector3(0, 0, -1); | |
| let playerQuaternion = new THREE.Quaternion(); | |
| let playerEuler = new THREE.Euler(0, 0, 0, 'YXZ'); | |
| let playerVelocity = new THREE.Vector3(); | |
| let playerRotationSpeed = 0.08; // Increased rotation speed | |
| let playerRollSpeed = 0.05; // Increased roll speed | |
| let loader = new THREE.GLTFLoader(); | |
| // Mobile controls | |
| let joystickActive = false; | |
| let joystickStartX = 0; | |
| let joystickStartY = 0; | |
| let joystickHead = null; | |
| let joystickBase = null; | |
| let movementX = 0; | |
| let movementY = 0; | |
| // Key states | |
| const keys = { | |
| ArrowUp: false, | |
| ArrowDown: false, | |
| ArrowLeft: false, | |
| ArrowRight: false, | |
| w: false, | |
| a: false, | |
| s: false, | |
| d: false, | |
| ' ': false, | |
| q: false, | |
| e: false, | |
| Shift: false | |
| }; | |
| // Initialize the game | |
| function init() { | |
| // Check if mobile | |
| isMobile = /Mobi|Android/i.test(navigator.userAgent); | |
| // Create scene | |
| scene = new THREE.Scene(); | |
| scene.fog = new THREE.FogExp2(0x000000, 0.001); | |
| // Create camera | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(0, 0, 10); | |
| camera.lookAt(0, 0, -1); | |
| // Create renderer | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| document.body.appendChild(renderer.domElement); | |
| // Add lights | |
| const ambientLight = new THREE.AmbientLight(0x404040); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 1); | |
| directionalLight.position.set(1, 1, 1); | |
| directionalLight.castShadow = true; | |
| directionalLight.shadow.mapSize.width = 1024; | |
| directionalLight.shadow.mapSize.height = 1024; | |
| scene.add(directionalLight); | |
| // Create starfield background | |
| createStarfield(); | |
| // Create player ship | |
| createPlayer(); | |
| // Add event listeners | |
| window.addEventListener('resize', onWindowResize); | |
| window.addEventListener('keydown', onKeyDown); | |
| window.addEventListener('keyup', onKeyUp); | |
| window.addEventListener('mousemove', onMouseMove); | |
| window.addEventListener('click', () => { | |
| if (document.pointerLockElement !== renderer.domElement) { | |
| renderer.domElement.requestPointerLock(); | |
| } | |
| }); | |
| // Mobile controls | |
| if (isMobile) { | |
| setupMobileControls(); | |
| } | |
| // Start button event listener | |
| document.getElementById('start-button').addEventListener('click', startGame); | |
| document.getElementById('restart-button').addEventListener('click', restartGame); | |
| document.getElementById('fire-btn').addEventListener('touchstart', () => { | |
| keys[' '] = true; | |
| // Auto-fire when holding the button | |
| if (!gameOver) shoot(); | |
| }); | |
| document.getElementById('fire-btn').addEventListener('touchend', () => keys[' '] = false); | |
| // Start animation loop | |
| animate(); | |
| } | |
| function setupMobileControls() { | |
| joystickHead = document.getElementById('joystick-head'); | |
| joystickBase = document.getElementById('joystick-base'); | |
| joystickBase.addEventListener('touchstart', handleJoystickStart, { passive: false }); | |
| joystickBase.addEventListener('touchmove', handleJoystickMove, { passive: false }); | |
| joystickBase.addEventListener('touchend', handleJoystickEnd, { passive: false }); | |
| // Add touch event for the whole screen to control aiming | |
| renderer.domElement.addEventListener('touchmove', handleTouchAim, { passive: false }); | |
| } | |
| function handleTouchAim(e) { | |
| if (gameOver || !joystickActive) return; | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| const centerX = window.innerWidth / 2; | |
| const centerY = window.innerHeight / 2; | |
| // Calculate relative movement from center | |
| const movementX = (touch.clientX - centerX) / 100; | |
| const movementY = (touch.clientY - centerY) / 100; | |
| // Update player direction based on touch position | |
| playerEuler.y -= movementX * 0.002; | |
| playerEuler.x -= movementY * 0.002; | |
| // Limit vertical rotation to prevent flipping | |
| playerEuler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, playerEuler.x)); | |
| playerQuaternion.setFromEuler(playerEuler); | |
| playerDirection.set(0, 0, -1).applyQuaternion(playerQuaternion); | |
| } | |
| function handleJoystickStart(e) { | |
| e.preventDefault(); | |
| joystickActive = true; | |
| const rect = joystickBase.getBoundingClientRect(); | |
| joystickStartX = rect.left + rect.width / 2; | |
| joystickStartY = rect.top + rect.height / 2; | |
| handleJoystickMove(e); | |
| } | |
| function handleJoystickMove(e) { | |
| if (!joystickActive) return; | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| const touchX = touch.clientX; | |
| const touchY = touch.clientY; | |
| // Calculate distance from center | |
| const dx = touchX - joystickStartX; | |
| const dy = touchY - joystickStartY; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| const maxDistance = 60; | |
| // Limit to joystick radius | |
| const angle = Math.atan2(dy, dx); | |
| const limitedDistance = Math.min(distance, maxDistance); | |
| // Position joystick head | |
| const posX = limitedDistance * Math.cos(angle); | |
| const posY = limitedDistance * Math.sin(angle); | |
| joystickHead.style.transform = `translate(${posX}px, ${posY}px)`; | |
| // Calculate movement direction (invert Y for natural movement) | |
| movementX = dx / maxDistance; | |
| movementY = -dy / maxDistance; // Inverted Y axis | |
| // Normalize if outside circle | |
| if (distance > maxDistance) { | |
| movementX = dx / distance; | |
| movementY = -dy / distance; // Inverted Y axis | |
| } | |
| } | |
| function handleJoystickEnd() { | |
| joystickActive = false; | |
| joystickHead.style.transform = 'translate(0, 0)'; | |
| movementX = 0; | |
| movementY = 0; | |
| } | |
| function startGame() { | |
| document.getElementById('start-screen').style.display = 'none'; | |
| gameOver = false; | |
| score = 0; | |
| health = 100; | |
| updateUI(); | |
| // Request pointer lock for mouse controls | |
| if (!isMobile) { | |
| renderer.domElement.requestPointerLock(); | |
| } | |
| } | |
| function restartGame() { | |
| // Clear all game objects | |
| while (enemies.length > 0) { | |
| scene.remove(enemies[0].mesh); | |
| enemies.shift(); | |
| } | |
| while (bullets.length > 0) { | |
| scene.remove(bullets[0]); | |
| bullets.shift(); | |
| } | |
| while (enemyBullets.length > 0) { | |
| scene.remove(enemyBullets[0]); | |
| enemyBullets.shift(); | |
| } | |
| while (powerups.length > 0) { | |
| scene.remove(powerups[0].mesh); | |
| powerups.shift(); | |
| } | |
| // Reset player position and rotation | |
| player.position.set(0, 0, 0); | |
| player.rotation.set(0, 0, 0); | |
| playerVelocity.set(0, 0, 0); | |
| playerDirection.set(0, 0, -1); | |
| // Reset game state | |
| gameOver = false; | |
| score = 0; | |
| health = 100; | |
| doubleFireActive = false; | |
| document.getElementById('powerup-indicator').style.display = 'none'; | |
| document.getElementById('game-over').style.display = 'none'; | |
| updateUI(); | |
| // Request pointer lock again | |
| if (!isMobile) { | |
| renderer.domElement.requestPointerLock(); | |
| } | |
| } | |
| function createStarfield() { | |
| const starGeometry = new THREE.BufferGeometry(); | |
| const starMaterial = new THREE.PointsMaterial({ | |
| color: 0xffffff, | |
| size: 0.1, | |
| transparent: true, | |
| opacity: 0.8 | |
| }); | |
| const starVertices = []; | |
| for (let i = 0; i < 10000; i++) { | |
| const x = (Math.random() - 0.5) * 2000; | |
| const y = (Math.random() - 0.5) * 2000; | |
| const z = (Math.random() - 0.5) * 2000; | |
| starVertices.push(x, y, z); | |
| } | |
| starGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starVertices, 3)); | |
| starField = new THREE.Points(starGeometry, starMaterial); | |
| scene.add(starField); | |
| } | |
| function createPlayer() { | |
| // Create a more advanced player ship | |
| const geometry = new THREE.BoxGeometry(2, 1, 3); | |
| // Create ship body | |
| const bodyGeometry = new THREE.BoxGeometry(1.5, 0.5, 2); | |
| const bodyMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x00aaff, | |
| emissive: 0x0044aa, | |
| specular: 0xffffff, | |
| shininess: 30, | |
| flatShading: false | |
| }); | |
| // Create wings | |
| const wingGeometry = new THREE.BoxGeometry(0.5, 0.2, 1.5); | |
| const wingMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x0066cc, | |
| emissive: 0x003366, | |
| specular: 0xffffff, | |
| shininess: 30, | |
| flatShading: false | |
| }); | |
| // Create cockpit | |
| const cockpitGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.5, 16); | |
| cockpitGeometry.rotateX(Math.PI / 2); | |
| const cockpitMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x00ffff, | |
| emissive: 0x006666, | |
| specular: 0xffffff, | |
| shininess: 100, | |
| transparent: true, | |
| opacity: 0.7 | |
| }); | |
| // Create engine glow | |
| const engineGlowGeometry = new THREE.CylinderGeometry(0.3, 0.6, 1, 8); | |
| const engineGlowMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0x00ffff, | |
| transparent: true, | |
| opacity: 0.7, | |
| blending: THREE.AdditiveBlending | |
| }); | |
| // Assemble the ship | |
| const ship = new THREE.Group(); | |
| const body = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
| body.position.set(0, 0, 0); | |
| ship.add(body); | |
| const leftWing = new THREE.Mesh(wingGeometry, wingMaterial); | |
| leftWing.position.set(-1.2, 0, 0); | |
| ship.add(leftWing); | |
| const rightWing = new THREE.Mesh(wingGeometry, wingMaterial); | |
| rightWing.position.set(1.2, 0, 0); | |
| ship.add(rightWing); | |
| const cockpit = new THREE.Mesh(cockpitGeometry, cockpitMaterial); | |
| cockpit.position.set(0, 0.3, 0.5); | |
| ship.add(cockpit); | |
| const engineGlow = new THREE.Mesh(engineGlowGeometry, engineGlowMaterial); | |
| engineGlow.position.set(0, 0, -1.5); | |
| ship.add(engineGlow); | |
| ship.castShadow = true; | |
| ship.receiveShadow = true; | |
| player = ship; | |
| scene.add(player); | |
| } | |
| function createEnemy() { | |
| const type = Math.floor(Math.random() * 3); // 3 different enemy types | |
| let geometry, material, scale = 1; | |
| switch(type) { | |
| case 0: // Organic alien ship | |
| geometry = new THREE.SphereGeometry(1, 16, 16); | |
| material = new THREE.MeshPhongMaterial({ | |
| color: 0xcc00ff, | |
| emissive: 0x660099, | |
| specular: 0xffffff, | |
| shininess: 30, | |
| flatShading: false | |
| }); | |
| break; | |
| case 1: // Mechanical alien ship | |
| geometry = new THREE.BoxGeometry(1.5, 0.8, 1.5); | |
| material = new THREE.MeshPhongMaterial({ | |
| color: 0xff6600, | |
| emissive: 0x993300, | |
| specular: 0xffffff, | |
| shininess: 20, | |
| flatShading: false | |
| }); | |
| break; | |
| case 2: // Advanced alien mothership | |
| const mothership = new THREE.Group(); | |
| // Main body | |
| const mainBody = new THREE.Mesh( | |
| new THREE.SphereGeometry(1.2, 16, 16), | |
| new THREE.MeshPhongMaterial({ | |
| color: 0x00ff00, | |
| emissive: 0x006600, | |
| specular: 0xffffff, | |
| shininess: 50 | |
| }) | |
| ); | |
| // Rings | |
| const ring1 = new THREE.Mesh( | |
| new THREE.TorusGeometry(1.5, 0.2, 16, 32), | |
| new THREE.MeshPhongMaterial({ | |
| color: 0x00cc00, | |
| emissive: 0x004400, | |
| specular: 0xffffff, | |
| shininess: 30 | |
| }) | |
| ); | |
| ring1.rotation.x = Math.PI / 2; | |
| const ring2 = new THREE.Mesh( | |
| new THREE.TorusGeometry(1.8, 0.15, 16, 32), | |
| new THREE.MeshPhongMaterial({ | |
| color: 0x009900, | |
| emissive: 0x002200, | |
| specular: 0xffffff, | |
| shininess: 20 | |
| }) | |
| ); | |
| ring2.rotation.z = Math.PI / 2; | |
| mothership.add(mainBody); | |
| mothership.add(ring1); | |
| mothership.add(ring2); | |
| const enemyMesh = mothership; | |
| scale = 0.8; | |
| break; | |
| } | |
| const enemyMesh = type !== 2 ? new THREE.Mesh(geometry, material) : geometry; | |
| enemyMesh.scale.set(scale, scale, scale); | |
| enemyMesh.castShadow = true; | |
| enemyMesh.receiveShadow = true; | |
| // Position enemy randomly in 3D space around player | |
| const angle = Math.random() * Math.PI * 2; | |
| const radius = 30 + Math.random() * 50; | |
| const height = (Math.random() - 0.5) * 40; | |
| const x = Math.cos(angle) * radius; | |
| const y = height; | |
| const z = Math.sin(angle) * radius; | |
| enemyMesh.position.set(x, y, z); | |
| // Make enemy face player | |
| enemyMesh.lookAt(player.position); | |
| scene.add(enemyMesh); | |
| enemies.push({ | |
| mesh: enemyMesh, | |
| type: type, | |
| health: type === 2 ? 5 : (type === 1 ? 3 : 2), | |
| speed: 0.05 + Math.random() * 0.05, | |
| lastShot: 0, | |
| shootInterval: 1500 + Math.random() * 1000 | |
| }); | |
| } | |
| function createBullet(position, direction, isEnemy = false) { | |
| const geometry = new THREE.SphereGeometry(0.2, 8, 8); | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: isEnemy ? 0xff0000 : 0x00ffff, | |
| emissive: isEnemy ? 0x990000 : 0x006666, | |
| specular: 0xffffff, | |
| shininess: 100 | |
| }); | |
| const bullet = new THREE.Mesh(geometry, material); | |
| bullet.position.copy(position); | |
| bullet.userData = { | |
| direction: direction.clone().normalize(), | |
| speed: isEnemy ? 0.3 : 0.5, | |
| isEnemy: isEnemy | |
| }; | |
| scene.add(bullet); | |
| if (isEnemy) { | |
| enemyBullets.push(bullet); | |
| } else { | |
| bullets.push(bullet); | |
| } | |
| return bullet; | |
| } | |
| function createPowerup(position) { | |
| const type = Math.floor(Math.random() * 2); // 0 = health, 1 = double fire | |
| const geometry = new THREE.IcosahedronGeometry(0.8); | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: type === 0 ? 0x00ff00 : 0xffff00, | |
| emissive: type === 0 ? 0x006600 : 0x666600, | |
| specular: 0xffffff, | |
| shininess: 100, | |
| transparent: true, | |
| opacity: 0.9 | |
| }); | |
| const powerup = new THREE.Mesh(geometry, material); | |
| powerup.position.copy(position); | |
| powerup.userData = { | |
| type: type, | |
| speed: 0.05, | |
| rotationSpeed: 0.02 | |
| }; | |
| scene.add(powerup); | |
| powerups.push(powerup); | |
| return powerup; | |
| } | |
| function createExplosion(position, size = 1) { | |
| const explosion = document.createElement('div'); | |
| explosion.className = 'explosion'; | |
| explosion.style.left = `${position.x * (window.innerWidth / 2) + window.innerWidth / 2}px`; | |
| explosion.style.top = `${-position.y * (window.innerHeight / 2) + window.innerHeight / 2}px`; | |
| explosion.style.width = `${size * 50}px`; | |
| explosion.style.height = `${size * 50}px`; | |
| document.getElementById('ui').appendChild(explosion); | |
| explosions.push({ | |
| element: explosion, | |
| startTime: Date.now() | |
| }); | |
| // Remove after animation completes | |
| setTimeout(() => { | |
| if (explosion.parentNode) { | |
| explosion.parentNode.removeChild(explosion); | |
| } | |
| }, 500); | |
| } | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| function onKeyDown(event) { | |
| if (keys.hasOwnProperty(event.key)) { | |
| keys[event.key] = true; | |
| event.preventDefault(); | |
| } | |
| // Toggle debug mode | |
| if (event.key === '`') { | |
| debugMode = !debugMode; | |
| document.getElementById('debug-info').style.display = debugMode ? 'block' : 'none'; | |
| } | |
| // Auto-fire when space is held down | |
| if (event.key === ' ' && !gameOver) { | |
| shoot(); | |
| } | |
| } | |
| function onKeyUp(event) { | |
| if (keys.hasOwnProperty(event.key)) { | |
| keys[event.key] = false; | |
| event.preventDefault(); | |
| } | |
| } | |
| function onMouseMove(event) { | |
| if (document.pointerLockElement === renderer.domElement && !isMobile) { | |
| const movementX = event.movementX || 0; | |
| const movementY = event.movementY || 0; | |
| // Update player direction based on mouse movement | |
| playerEuler.y -= movementX * 0.002; | |
| playerEuler.x -= movementY * 0.002; | |
| // Limit vertical rotation to prevent flipping | |
| playerEuler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, playerEuler.x)); | |
| playerQuaternion.setFromEuler(playerEuler); | |
| playerDirection.set(0, 0, -1).applyQuaternion(playerQuaternion); | |
| } | |
| } | |
| function shoot() { | |
| if (gameOver) return; | |
| const bulletPosition = player.position.clone(); | |
| bulletPosition.add(playerDirection.clone().multiplyScalar(-2)); // Shoot from front of ship | |
| // Create main bullet | |
| createBullet(bulletPosition, playerDirection.clone().negate()); | |
| // If double fire is active, create two additional bullets at angles | |
| if (doubleFireActive) { | |
| const angle = 0.2; | |
| const leftDir = playerDirection.clone().negate(); | |
| leftDir.applyAxisAngle(new THREE.Vector3(0, 1, 0), angle); | |
| createBullet(bulletPosition, leftDir); | |
| const rightDir = playerDirection.clone().negate(); | |
| rightDir.applyAxisAngle(new THREE.Vector3(0, 1, 0), -angle); | |
| createBullet(bulletPosition, rightDir); | |
| } | |
| } | |
| function updatePlayer(delta) { | |
| if (gameOver) return; | |
| // Apply boost if shift is pressed | |
| currentSpeed = keys.Shift ? boostSpeed : playerSpeed; | |
| // Reset velocity | |
| playerVelocity.set(0, 0, 0); | |
| // Calculate movement based on key states or joystick | |
| if (isMobile && joystickActive) { | |
| // Mobile joystick controls | |
| const moveX = movementX; | |
| const moveY = movementY; | |
| // Apply movement relative to camera view | |
| const forward = new THREE.Vector3(0, 0, -1); | |
| const right = new THREE.Vector3(1, 0, 0); | |
| playerVelocity.add(right.multiplyScalar(moveX * currentSpeed * delta * 60)); | |
| playerVelocity.add(forward.multiplyScalar(moveY * currentSpeed * delta * 60)); | |
| } else { | |
| // Keyboard controls (now more responsive) | |
| if (keys.w || keys.ArrowUp) playerVelocity.z -= currentSpeed * delta * 60; | |
| if (keys.s || keys.ArrowDown) playerVelocity.z += currentSpeed * delta * 60; | |
| if (keys.a || keys.ArrowLeft) playerVelocity.x -= currentSpeed * delta * 60; | |
| if (keys.d || keys.ArrowRight) playerVelocity.x += currentSpeed * delta * 60; | |
| // Roll controls | |
| if (keys.q) player.rotation.z += playerRollSpeed * delta * 60; | |
| if (keys.e) player.rotation.z -= playerRollSpeed * delta * 60; | |
| } | |
| // Apply movement direction rotation | |
| playerVelocity.applyQuaternion(playerQuaternion); | |
| // Apply movement | |
| player.position.add(playerVelocity); | |
| // Update player rotation based on direction | |
| player.quaternion.copy(playerQuaternion); | |
| // Auto-fire when space is held down | |
| if (keys[' ']) { | |
| const now = Date.now(); | |
| if (now - (player.lastShot || 0) > 300) { // Shoot every 300ms | |
| shoot(); | |
| player.lastShot = now; | |
| } | |
| } | |
| // Update camera position to follow player | |
| camera.position.copy(player.position); | |
| camera.position.add(playerDirection.clone().multiplyScalar(10)); // 10 units behind player | |
| camera.lookAt(player.position); | |
| // Debug info | |
| if (debugMode) { | |
| document.getElementById('debug-info').innerHTML = ` | |
| Position: ${player.position.x.toFixed(1)}, ${player.position.y.toFixed(1)}, ${player.position.z.toFixed(1)}<br> | |
| Rotation: ${player.rotation.x.toFixed(2)}, ${player.rotation.y.toFixed(2)}, ${player.rotation.z.toFixed(2)}<br> | |
| Speed: ${currentSpeed.toFixed(1)}<br> | |
| Enemies: ${enemies.length} | |
| `; | |
| } | |
| } | |
| function updateEnemies(delta) { | |
| const now = Date.now(); | |
| // Spawn new enemies | |
| if (now - lastEnemySpawnTime > enemySpawnInterval && enemies.length < 15) { | |
| createEnemy(); | |
| lastEnemySpawnTime = now; | |
| // Make game harder over time by decreasing spawn interval | |
| enemySpawnInterval = Math.max(500, enemySpawnInterval - 10); | |
| } | |
| // Update existing enemies | |
| for (let i = enemies.length - 1; i >= 0; i--) { | |
| const enemy = enemies[i]; | |
| // Move toward player | |
| const direction = new THREE.Vector3().subVectors(player.position, enemy.mesh.position).normalize(); | |
| enemy.mesh.position.addScaledVector(direction, enemy.speed * delta * 60); | |
| // Make enemy face player | |
| enemy.mesh.lookAt(player.position); | |
| // Enemy shooting | |
| if (now - enemy.lastShot > enemy.shootInterval) { | |
| const bulletPos = enemy.mesh.position.clone(); | |
| const bulletDir = new THREE.Vector3().subVectors(player.position, bulletPos); | |
| createBullet(bulletPos, bulletDir, true); | |
| enemy.lastShot = now; | |
| } | |
| // Check distance to player for collision | |
| if (enemy.mesh.position.distanceTo(player.position) < 3) { | |
| health -= 20; | |
| scene.remove(enemy.mesh); | |
| enemies.splice(i, 1); | |
| createExplosion(enemy.mesh.position, 1.5); | |
| updateUI(); | |
| if (health <= 0) { | |
| gameOver = true; | |
| document.getElementById('game-over').style.display = 'block'; | |
| document.getElementById('game-over').querySelector('span').textContent = `Score: ${score}`; | |
| } | |
| continue; | |
| } | |
| // Remove if health <= 0 | |
| if (enemy.health <= 0) { | |
| score += enemy.type === 2 ? 50 : (enemy.type === 1 ? 30 : 20); | |
| createExplosion(enemy.mesh.position, enemy.type === 2 ? 2 : 1); | |
| updateUI(); | |
| scene.remove(enemy.mesh); | |
| enemies.splice(i, 1); | |
| } | |
| } | |
| } | |
| function updateBullets(delta) { | |
| // Player bullets | |
| for (let i = bullets.length - 1; i >= 0; i--) { | |
| const bullet = bullets[i]; | |
| bullet.position.addScaledVector(bullet.userData.direction, bullet.userData.speed * delta * 60); | |
| // Check collision with enemies | |
| for (let j = enemies.length - 1; j >= 0; j--) { | |
| const enemy = enemies[j]; | |
| if (bullet.position.distanceTo(enemy.mesh.position) < 1.5) { | |
| enemy.health--; | |
| scene.remove(bullet); | |
| bullets.splice(i, 1); | |
| break; | |
| } | |
| } | |
| // Remove if too far away | |
| if (bullet.position.distanceTo(player.position) > 100) { | |
| scene.remove(bullet); | |
| bullets.splice(i, 1); | |
| } | |
| } | |
| // Enemy bullets | |
| for (let i = enemyBullets.length - 1; i >= 0; i--) { | |
| const bullet = enemyBullets[i]; | |
| bullet.position.addScaledVector(bullet.userData.direction, bullet.userData.speed * delta * 60); | |
| // Check collision with player | |
| if (bullet.position.distanceTo(player.position) < 2) { | |
| health -= 10; | |
| scene.remove(bullet); | |
| enemyBullets.splice(i, 1); | |
| createExplosion(bullet.position, 0.5); | |
| updateUI(); | |
| if (health <= 0) { | |
| gameOver = true; | |
| document.getElementById('game-over').style.display = 'block'; | |
| document.getElementById('game-over').querySelector('span').textContent = `Score: ${score}`; | |
| } | |
| continue; | |
| } | |
| // Remove if too far away | |
| if (bullet.position.distanceTo(player.position) > 100) { | |
| scene.remove(bullet); | |
| enemyBullets.splice(i, 1); | |
| } | |
| } | |
| } | |
| function updatePowerups(delta) { | |
| const now = Date.now(); | |
| // Spawn new powerups | |
| if (now - lastPowerupSpawnTime > powerupSpawnInterval && powerups.length < 3) { | |
| // Position powerup randomly in 3D space around player | |
| const angle = Math.random() * Math.PI * 2; | |
| const radius = 20 + Math.random() * 30; | |
| const height = (Math.random() - 0.5) * 20; | |
| const x = Math.cos(angle) * radius; | |
| const y = height; | |
| const z = Math.sin(angle) * radius; | |
| createPowerup(new THREE.Vector3(x, y, z)); | |
| lastPowerupSpawnTime = now; | |
| } | |
| // Update existing powerups | |
| for (let i = powerups.length - 1; i >= 0; i--) { | |
| const powerup = powerups[i]; | |
| // Rotate | |
| powerup.rotation.x += powerup.userData.rotationSpeed; | |
| powerup.rotation.y += powerup.userData.rotationSpeed; | |
| // Check collision with player | |
| if (powerup.position.distanceTo(player.position) < 2) { | |
| if (powerup.userData.type === 0) { | |
| // Health powerup | |
| health = Math.min(100, health + 20); | |
| } else { | |
| // Double fire powerup | |
| doubleFireActive = true; | |
| document.getElementById('powerup-indicator').style.display = 'block'; | |
| // Clear any existing timeout | |
| if (doubleFireTimeout) { | |
| clearTimeout(doubleFireTimeout); | |
| } | |
| // Set timeout to deactivate double fire | |
| doubleFireTimeout = setTimeout(() => { | |
| doubleFireActive = false; | |
| document.getElementById('powerup-indicator').style.display = 'none'; | |
| }, 10000); | |
| } | |
| scene.remove(powerup); | |
| powerups.splice(i, 1); | |
| updateUI(); | |
| continue; | |
| } | |
| } | |
| } | |
| function updateUI() { | |
| document.getElementById('score-display').textContent = `SCORE: ${score}`; | |
| document.getElementById('health-bar').style.width = `${health}%`; | |
| document.getElementById('health-text').textContent = `${health}%`; | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const delta = clock.getDelta(); | |
| if (!gameOver) { | |
| updatePlayer(delta); | |
| updateEnemies(delta); | |
| updateBullets(delta); | |
| updatePowerups(delta); | |
| // Rotate starfield for parallax effect | |
| if (starField) { | |
| starField.rotation.z += 0.0005; | |
| } | |
| } | |
| renderer.render(scene, camera); | |
| } | |
| // Start the game | |
| init(); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=vikassabbi/vsspace" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |