Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Neon Void: Hyper Space Shooter</title> | |
| <!-- Importing a futuristic font --> | |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet"> | |
| <!-- Importing FontAwesome for UI Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #00f3ff; | |
| --secondary-color: #bc13fe; | |
| --danger-color: #ff2a6d; | |
| --bg-color: #050505; | |
| --glass-bg: rgba(255, 255, 255, 0.05); | |
| --glass-border: rgba(255, 255, 255, 0.1); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| user-select: none; | |
| } | |
| body { | |
| overflow: hidden; | |
| background-color: var(--bg-color); | |
| font-family: 'Orbitron', sans-serif; | |
| color: white; | |
| height: 100vh; | |
| width: 100vw; | |
| } | |
| /* --- Canvas Layer --- */ | |
| #gameCanvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 1; | |
| } | |
| /* --- UI Layer --- */ | |
| #ui-layer { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 10; | |
| pointer-events: none; /* Let clicks pass through to canvas when playing */ | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| padding: 20px; | |
| } | |
| /* --- Header / HUD --- */ | |
| .hud-top { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| } | |
| .brand-link { | |
| pointer-events: auto; | |
| color: var(--primary-color); | |
| text-decoration: none; | |
| font-size: 0.8rem; | |
| opacity: 0.7; | |
| transition: opacity 0.3s; | |
| text-shadow: 0 0 10px var(--primary-color); | |
| } | |
| .brand-link:hover { | |
| opacity: 1; | |
| text-shadow: 0 0 20px var(--primary-color); | |
| } | |
| .score-board { | |
| text-align: right; | |
| text-shadow: 0 0 10px rgba(255,255,255,0.5); | |
| } | |
| .score-label { | |
| font-size: 0.8rem; | |
| color: #aaa; | |
| letter-spacing: 2px; | |
| } | |
| .score-value { | |
| font-size: 2.5rem; | |
| font-weight: 900; | |
| color: white; | |
| } | |
| /* --- Health Bar --- */ | |
| .health-container { | |
| position: absolute; | |
| bottom: 30px; | |
| left: 30px; | |
| width: 300px; | |
| pointer-events: auto; | |
| } | |
| .health-label { | |
| margin-bottom: 5px; | |
| font-size: 0.9rem; | |
| color: var(--danger-color); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .health-bar-bg { | |
| width: 100%; | |
| height: 10px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 5px; | |
| overflow: hidden; | |
| border: 1px solid var(--glass-border); | |
| } | |
| .health-bar-fill { | |
| height: 100%; | |
| width: 100%; | |
| background: linear-gradient(90deg, var(--danger-color), #ff8c00); | |
| box-shadow: 0 0 15px var(--danger-color); | |
| transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| /* --- Screens (Start / Game Over) --- */ | |
| .screen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| background: rgba(5, 5, 5, 0.6); | |
| backdrop-filter: blur(10px); | |
| z-index: 20; | |
| pointer-events: auto; | |
| transition: opacity 0.5s ease; | |
| } | |
| .hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| h1 { | |
| font-size: 4rem; | |
| text-transform: uppercase; | |
| background: linear-gradient(to bottom, #fff, var(--primary-color)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 10px; | |
| text-shadow: 0 0 30px rgba(0, 243, 255, 0.5); | |
| text-align: center; | |
| } | |
| h2 { | |
| font-size: 2.5rem; | |
| color: var(--danger-color); | |
| margin-bottom: 20px; | |
| text-shadow: 0 0 20px var(--danger-color); | |
| } | |
| p.instructions { | |
| margin-bottom: 40px; | |
| color: #ddd; | |
| font-size: 1.1rem; | |
| line-height: 1.6; | |
| text-align: center; | |
| max-width: 600px; | |
| } | |
| .key { | |
| display: inline-block; | |
| padding: 5px 10px; | |
| border: 1px solid var(--primary-color); | |
| border-radius: 4px; | |
| color: var(--primary-color); | |
| font-size: 0.8rem; | |
| margin: 0 5px; | |
| box-shadow: 0 0 5px var(--primary-color); | |
| } | |
| /* --- Buttons --- */ | |
| .btn { | |
| background: transparent; | |
| color: white; | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 1.2rem; | |
| font-weight: bold; | |
| padding: 15px 50px; | |
| border: 2px solid var(--primary-color); | |
| border-radius: 50px; | |
| cursor: pointer; | |
| position: relative; | |
| overflow: hidden; | |
| transition: all 0.3s ease; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| box-shadow: 0 0 15px rgba(0, 243, 255, 0.2); | |
| } | |
| .btn:hover { | |
| background: var(--primary-color); | |
| color: black; | |
| box-shadow: 0 0 40px rgba(0, 243, 255, 0.8); | |
| transform: scale(1.05); | |
| } | |
| .btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); | |
| transition: 0.5s; | |
| } | |
| .btn:hover::before { | |
| left: 100%; | |
| } | |
| /* --- Mobile Controls --- */ | |
| .mobile-controls { | |
| display: none; /* Shown via JS on touch devices */ | |
| position: absolute; | |
| bottom: 20px; | |
| width: 100%; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| pointer-events: none; | |
| } | |
| .control-zone { | |
| width: 120px; | |
| height: 120px; | |
| border: 2px solid rgba(255,255,255,0.2); | |
| border-radius: 50%; | |
| pointer-events: auto; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| color: rgba(255,255,255,0.5); | |
| font-size: 1.5rem; | |
| backdrop-filter: blur(4px); | |
| } | |
| .control-zone:active { | |
| background: rgba(255,255,255,0.1); | |
| border-color: var(--primary-color); | |
| color: var(--primary-color); | |
| } | |
| @media (max-width: 768px) { | |
| h1 { font-size: 2.5rem; } | |
| .score-value { font-size: 1.8rem; } | |
| .health-container { width: 200px; bottom: 160px; } | |
| .mobile-controls { display: flex; } | |
| .instructions { font-size: 0.9rem; padding: 0 20px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Canvas for 3D Rendering --> | |
| <canvas id="gameCanvas"></canvas> | |
| <!-- UI Overlay --> | |
| <div id="ui-layer"> | |
| <div class="hud-top"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="brand-link"> | |
| Built with anycoder <i class="fas fa-external-link-alt"></i> | |
| </a> | |
| <div class="score-board"> | |
| <div class="score-label">SCORE</div> | |
| <div class="score-value" id="scoreDisplay">0</div> | |
| </div> | |
| </div> | |
| <div class="health-container"> | |
| <div class="health-label">Shield Integrity</div> | |
| <div class="health-bar-bg"> | |
| <div class="health-bar-fill" id="healthFill"></div> | |
| </div> | |
| </div> | |
| <div class="mobile-controls"> | |
| <div class="control-zone" id="btnLeft"><i class="fas fa-arrow-left"></i></div> | |
| <div class="control-zone" id="btnRight"><i class="fas fa-arrow-right"></i></div> | |
| </div> | |
| </div> | |
| <!-- Start Screen --> | |
| <div id="startScreen" class="screen"> | |
| <h1>Neon Void</h1> | |
| <p class="instructions"> | |
| Pilot your ship through the hyper-speed corridor.<br> | |
| Avoid asteroids and destroy enemy drones.<br><br> | |
| <span class="key">←</span> <span class="key">→</span> to Move<br> | |
| <span class="key">SPACE</span> or <span class="key">CLICK</span> to Shoot | |
| </p> | |
| <button class="btn" id="startBtn">Initiate Launch</button> | |
| </div> | |
| <!-- Game Over Screen --> | |
| <div id="gameOverScreen" class="screen hidden"> | |
| <h2>CRITICAL FAILURE</h2> | |
| <p class="instructions">Final Score: <span id="finalScore" style="color:var(--primary-color); font-weight:bold;">0</span></p> | |
| <button class="btn" id="restartBtn">Reboot System</button> | |
| </div> | |
| <script> | |
| /** | |
| * 3D Space Shooter Logic | |
| * Uses HTML5 Canvas for pseudo-3D rendering. | |
| */ | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d', { alpha: false }); // Optimize for no transparency on bg | |
| // UI Elements | |
| const scoreEl = document.getElementById('scoreDisplay'); | |
| const finalScoreEl = document.getElementById('finalScore'); | |
| const healthFillEl = document.getElementById('healthFill'); | |
| const startScreen = document.getElementById('startScreen'); | |
| const gameOverScreen = document.getElementById('gameOverScreen'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const restartBtn = document.getElementById('restartBtn'); | |
| const btnLeft = document.getElementById('btnLeft'); | |
| const btnRight = document.getElementById('btnRight'); | |
| // Game State | |
| let isPlaying = false; | |
| let score = 0; | |
| let health = 100; | |
| let frameCount = 0; | |
| let speed = 15; // Base speed | |
| let speedMultiplier = 1; | |
| // Input State | |
| const keys = { ArrowLeft: false, ArrowRight: false, Space: false }; | |
| let touchInput = 0; // -1 left, 1 right, 0 none | |
| // Dimensions | |
| let width, height, cx, cy; | |
| function resize() { | |
| width = canvas.width = window.innerWidth; | |
| height = canvas.height = window.innerHeight; | |
| cx = width / 2; | |
| cy = height / 2; | |
| } | |
| window.addEventListener('resize', resize); | |
| resize(); | |
| // --- Classes --- | |
| // 1. Starfield (Speed Effect) | |
| class Star { | |
| constructor() { | |
| this.init(); | |
| } | |
| init() { | |
| this.x = (Math.random() - 0.5) * width * 2; // Spread wide | |
| this.y = (Math.random() - 0.5) * height * 2; | |
| this.z = Math.random() * 2000 + 500; // Depth | |
| this.pz = this.z; // Previous z for trail effect | |
| } | |
| update(currentSpeed) { | |
| this.z -= currentSpeed; | |
| if (this.z <= 1) { | |
| this.init(); | |
| this.z = 2000; | |
| this.pz = 2000; | |
| } | |
| } | |
| draw() { | |
| // Perspective projection | |
| const sx = (this.x / this.z) * width + cx; | |
| const sy = (this.y / this.z) * height + cy; | |
| // Previous position for trail (Speed Lines) | |
| const px = (this.x / this.pz) * width + cx; | |
| const py = (this.y / this.pz) * height + cy; | |
| this.pz = this.z; | |
| // Size based on proximity | |
| const size = (1 - this.z / 2000) * 3; | |
| const alpha = (1 - this.z / 2000); | |
| ctx.beginPath(); | |
| ctx.moveTo(px, py); | |
| ctx.lineTo(sx, sy); | |
| ctx.strokeStyle = `rgba(200, 240, 255, ${alpha})`; | |
| ctx.lineWidth = size; | |
| ctx.stroke(); | |
| } | |
| } | |
| // 2. Player Ship | |
| class Player { | |
| constructor() { | |
| this.x = 0; // Horizontal position (-1 to 1 range) | |
| this.y = 0.8; // Vertical fixed (near bottom) | |
| this.width = 0.15; // Relative width | |
| this.color = '#00f3ff'; | |
| this.bullets = []; | |
| this.cooldown = 0; | |
| } | |
| update() { | |
| // Movement smoothing | |
| if (keys.ArrowLeft || touchInput === -1) this.x -= 0.04; | |
| if (keys.ArrowRight || touchInput === 1) this.x += 0.04; | |
| // Clamp | |
| if (this.x < -1) this.x = -1; | |
| if (this.x > 1) this.x = 1; | |
| // Shooting | |
| if (this.cooldown > 0) this.cooldown--; | |
| if ((keys.Space || touchInput === 2) && this.cooldown <= 0) { | |
| this.shoot(); | |
| this.cooldown = 10; // Fire rate | |
| } | |
| // Update Bullets | |
| for (let i = this.bullets.length - 1; i >= 0; i--) { | |
| let b = this.bullets[i]; | |
| b.z -= speed * 2.5; // Bullets faster than ship | |
| if (b.z < 0) this.bullets.splice(i, 1); | |
| } | |
| } | |
| shoot() { | |
| // Dual guns | |
| this.bullets.push({ x: this.x - 0.05, y: this.y, z: 100 }); | |
| this.bullets.push({ x: this.x + 0.05, y: this.y, z: 100 }); | |
| } | |
| draw() { | |
| // Draw Bullets | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = this.color; | |
| ctx.fillStyle = '#fff'; | |
| this.bullets.forEach(b => { | |
| const sx = (b.x / b.z) * width + cx; | |
| const sy = (b.y / b.z) * height + cy; // Note: y is inverted in 3D usually, but simplified here | |
| // Adjust perspective y to be relative to center | |
| const screenY = height - (b.y * height * 0.5); // Rough approximation for gameplay feel | |
| // Better projection for gameplay objects | |
| const projX = (b.x * width * 0.8) + cx; | |
| const projY = height * 0.8; // Shoot from bottom plane | |
| // Scale bullet size | |
| const scale = 500 / b.z; | |
| if(scale > 0) { | |
| ctx.beginPath(); | |
| ctx.arc(projX, projY - (b.z * 0.5), 3 * scale, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| }); | |
| // Draw Ship (Simple 3D wireframe-ish look) | |
| const shipX = this.x * width * 0.4 + cx; | |
| const shipY = height * 0.85; | |
| const size = 40; | |
| ctx.shadowBlur = 20; | |
| ctx.shadowColor = this.color; | |
| ctx.strokeStyle = this.color; | |
| ctx.lineWidth = 2; | |
| // Main body triangle | |
| ctx.beginPath(); | |
| ctx.moveTo(shipX, shipY - size); | |
| ctx.lineTo(shipX - size, shipY + size); | |
| ctx.lineTo(shipX, shipY + size * 0.7); // Engine indent | |
| ctx.lineTo(shipX + size, shipY + size); | |
| ctx.closePath(); | |
| ctx.stroke(); | |
| // Engine glow | |
| ctx.fillStyle = `rgba(0, 243, 255, ${Math.random() * 0.5 + 0.5})`; | |
| ctx.beginPath(); | |
| ctx.moveTo(shipX - size * 0.5, shipY + size * 0.8); | |
| ctx.lineTo(shipX, shipY + size * 1.5 + Math.random() * 20); | |
| ctx.lineTo(shipX + size * 0.5, shipY + size * 0.8); | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; | |
| } | |
| } | |
| // 3. Enemies & Obstacles | |
| class Object3D { | |
| constructor(type) { | |
| this.type = type; // 'asteroid' or 'enemy' | |
| this.x = (Math.random() - 0.5) * 2.5; // Random X | |
| this.y = (Math.random() - 0.5) * 1; // Random Y spread | |
| this.z = 2000; | |
| this.active = true; | |
| this.rotation = Math.random() * Math.PI; | |
| this.rotSpeed = (Math.random() - 0.5) * 0.1; | |
| this.size = Math.random() * 0.5 + 0.5; | |
| } | |
| update() { | |
| this.z -= speed; | |
| this.rotation += this.rotSpeed; | |
| if (this.z < 10) { | |
| this.active = false; | |
| // If it passes player, maybe damage? | |
| if (Math.abs(this.x - player.x) < 0.3 && this.type === 'asteroid') { | |
| takeDamage(10); | |
| createExplosion(0, 0, 0, 'red'); // Screen shake effect essentially | |
| } | |
| } | |
| } | |
| draw() { | |
| const scale = 500 / this.z; | |
| const screenX = (this.x * width * 0.8) + cx; | |
| const screenY = cy + (this.y * height * 0.5); // Center projection | |
| ctx.save(); | |
| ctx.translate(screenX, screenY); | |
| ctx.scale(scale * this.size, scale * this.size); | |
| ctx.rotate(this.rotation); | |
| if (this.type === 'asteroid') { | |
| ctx.strokeStyle = '#888'; | |
| ctx.fillStyle = '#111'; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| // Draw a rough polygon | |
| for(let i=0; i<6; i++) { | |
| const angle = (i / 6) * Math.PI * 2; | |
| const r = 30 + Math.sin(i * 3) * 10; | |
| const px = Math.cos(angle) * r; | |
| const py = Math.sin(angle) * r; | |
| if(i===0) ctx.moveTo(px, py); | |
| else ctx.lineTo(px, py); | |
| } | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.stroke(); | |
| } else if (this.type === 'enemy') { | |
| ctx.shadowBlur = 15; | |
| ctx.shadowColor = '#bc13fe'; | |
| ctx.fillStyle = '#bc13fe'; | |
| // Drone shape | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -30); | |
| ctx.lineTo(20, 10); | |
| ctx.lineTo(0, 0); | |
| ctx.lineTo(-20, 10); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // Core | |
| ctx.fillStyle = '#fff'; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 5, 0, Math.PI*2); | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; | |
| } | |
| ctx.restore(); | |
| } | |
| } | |
| // 4. Particles (Explosions) | |
| class Particle { | |
| constructor(x, y, color) { | |
| this.x = x; | |
| this.y = y; | |
| this.vx = (Math.random() - 0.5) * 10; | |
| this.vy = (Math.random() - 0.5) * 10; | |
| this.life = 1.0; | |
| this.color = color; | |
| this.size = Math.random() * 5 + 2; | |
| } | |
| update() { | |
| this.x += this.vx; | |
| this.y += this.vy; | |
| this.life -= 0.03; | |
| this.size *= 0.95; | |
| } | |
| draw() { | |
| ctx.globalAlpha = this.life; | |
| ctx.fillStyle = this.color; | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.globalAlpha = 1.0; | |
| } | |
| } | |
| // --- Game Logic Setup --- | |
| const stars = Array.from({ length: 300 }, () => new Star()); | |
| let player = new Player(); | |
| let objects = []; | |
| let particles = []; | |
| let animationId; | |
| function createExplosion(x, y, color) { | |
| for(let i=0; i<15; i++) { | |
| particles.push(new Particle(x, y, color)); | |
| } | |
| } | |
| function takeDamage(amount) { | |
| health -= amount; | |
| healthFillEl.style.width = `${health}%`; | |
| // Screen shake effect | |
| canvas.style.transform = `translate(${Math.random()*20-10}px, ${Math.random()*20-10}px)`; | |
| setTimeout(() => canvas.style.transform = 'none', 50); | |
| if (health <= 0) { | |
| endGame(); | |
| } | |
| } | |
| function spawnObject() { | |
| // Difficulty curve | |
| const spawnRate = Math.max(20, 60 - Math.floor(score / 500)); | |
| if (frameCount % spawnRate === 0) { | |
| const type = Math.random() > 0.7 ? 'enemy' : 'asteroid'; | |
| objects.push(new Object3D(type)); | |
| } | |
| } | |
| function checkCollisions() { | |
| objects.forEach(obj => { | |
| if (!obj.active) return; | |
| // Bullet Collision | |
| player.bullets.forEach((b, bIndex) => { | |
| if (b.z < obj.z + 50 && b.z > obj.z - 50) { | |
| // Check X distance roughly | |
| // Map bullet relative X (-1 to 1) to Object X range | |
| // Simplified 3D distance check | |
| const dx = b.x - obj.x; | |
| const dy = (player.y - 0.5) - obj.y; // Adjust for player plane | |
| if (Math.sqrt(dx*dx + dy*dy) < 0.3) { | |
| // Hit | |
| obj.active = false; | |
| player.bullets.splice(bIndex, 1); | |
| // Visuals | |
| const sx = (obj.x * width * 0.8) + cx; | |
| const sy = cy + (obj.y * height * 0.5); | |
| createExplosion(sx, sy, obj.type === 'enemy' ? '#bc13fe' : '#aaa'); | |
| // Score | |
| if (obj.type === 'enemy') { | |
| score += 200; | |
| scoreEl.innerText = score; | |
| } else { | |
| score += 50; | |
| scoreEl.innerText = score; | |
| } | |
| } | |
| } | |
| }); | |
| // Player Collision (Direct hit) | |
| if (obj.z < 150 && obj.z > 50) { | |
| const dx = player.x - obj.x; | |
| if (Math.abs(dx) < 0.2) { | |
| obj.active = false; | |
| createExplosion(cx, height * 0.8, '#ff2a6d'); | |
| takeDamage(20); | |
| } | |
| } | |
| }); | |
| } | |
| function loop() { | |
| if (!isPlaying) return; | |
| // Clear Canvas | |
| ctx.fillStyle = 'rgba(5, 5, 5, 0.4)'; // Trail effect for motion blur | |
| ctx.fillRect(0, 0, width, height); | |
| frameCount++; | |
| // Increase speed gradually | |
| speed = 15 + (score / 1000); | |
| // Update Stars | |
| stars.forEach(star => { | |
| star.update(speed); | |
| star.draw(); | |
| }); | |
| // Update Player | |
| player.update(); | |
| player.draw(); | |
| // Update Objects | |
| objects = objects.filter(obj => obj.active); | |
| objects.forEach(obj => { | |
| obj.update(); | |
| obj.draw(); | |
| }); | |
| // Update Particles | |
| particles = particles.filter(p => p.life > 0); | |
| particles.forEach(p => { | |
| p.update(); | |
| p.draw(); | |
| }); | |
| spawnObject(); | |
| checkCollisions(); | |
| animationId = requestAnimationFrame(loop); | |
| } | |
| function startGame() { | |
| isPlaying = true; | |
| score = 0; | |
| health = 100; | |
| speed = 15; | |
| objects = []; | |
| particles = []; | |
| player.bullets = []; | |
| player.x = 0; | |
| scoreEl.innerText = '0'; | |
| healthFillEl.style.width = '100%'; | |
| startScreen.classList.add('hidden'); | |
| gameOverScreen.classList.add('hidden'); | |
| loop(); | |
| } | |
| function endGame() { | |
| isPlaying = false; | |
| cancelAnimationFrame(animationId); | |
| finalScoreEl.innerText = score; | |
| gameOverScreen.classList.remove('hidden'); | |
| } | |
| // --- Event Listeners --- | |
| window.addEventListener('keydown', e => { | |
| if (e.code === 'ArrowLeft') keys.ArrowLeft = true; | |
| if (e.code === 'ArrowRight') keys.ArrowRight = true; | |
| if (e.code === 'Space') keys.Space = true; | |
| }); | |
| window.addEventListener('keyup', e => { | |
| if (e.code === 'ArrowLeft') keys.ArrowLeft = false; | |
| if (e.code === 'ArrowRight') keys.ArrowRight = false; | |
| if (e.code === 'Space') keys.Space = false; | |
| }); | |
| // Mouse/Touch Drag for movement | |
| window.addEventListener('mousedown', () => keys.Space = true); | |
| window.addEventListener('mouseup', () => keys.Space = false); | |
| // Touch Logic for Mobile | |
| btnLeft.addEventListener('touchstart', (e) => { e.preventDefault(); touchInput = -1; }); | |
| btnLeft.addEventListener('touchend', (e) => { e.preventDefault(); touchInput = 0; }); | |
| btnRight.addEventListener('touchstart', (e) => { e.preventDefault(); touchInput = 1; }); | |
| btnRight.addEventListener('touchend', (e) => { e.preventDefault(); touchInput = 0; }); | |
| // Tap center to shoot on mobile (simplified) | |
| canvas.addEventListener('touchstart', (e) => { | |
| if(e.touches.length > 0) { | |
| const t = e.touches[0]; | |
| if(t.clientX > width * 0.3 && t.clientX < width * 0.7) { | |
| touchInput = 2; // Shoot code | |
| } | |
| } | |
| }); | |
| canvas.addEventListener('touchend', (e) => { | |
| touchInput = 0; | |
| }); | |
| startBtn.addEventListener('click', startGame); | |
| restartBtn.addEventListener('click', startGame); | |
| // Initial render for background | |
| ctx.fillStyle = '#050505'; | |
| ctx.fillRect(0, 0, width, height); | |
| stars.forEach(star => star.draw()); | |
| </script> | |
| </body> | |
| </html> |