Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Cosmic Sound Invaders</title> | |
| <!-- Production Tailwind CSS via CDN --> | |
| <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script> | |
| <style> | |
| #gameCanvas { | |
| background: linear-gradient(to bottom, #0f172a, #1e293b); | |
| border-radius: 12px; | |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); | |
| } | |
| .music-note { | |
| position: absolute; | |
| font-size: 24px; | |
| animation: float-up 1s ease-out forwards; | |
| opacity: 0.8; | |
| } | |
| @keyframes float-up { | |
| 0% { transform: translateY(0); opacity: 0.8; } | |
| 100% { transform: translateY(-50px); opacity: 0; } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-slate-900 text-white min-h-screen flex flex-col items-center justify-center p-4"> | |
| <div class="max-w-4xl w-full text-center mb-8"> | |
| <h1 class="text-5xl font-bold mb-2 bg-clip-text text-transparent bg-gradient-to-r from-purple-400 via-pink-500 to-indigo-500"> | |
| Cosmic Sound Invaders | |
| </h1> | |
| <p class="text-xl text-slate-300 mb-6"> | |
| Shoot aliens to create an intergalactic symphony! | |
| </p> | |
| <div class="flex justify-center gap-4 mb-8"> | |
| <button id="startBtn" class="px-6 py-3 bg-green-600 hover:bg-green-700 rounded-lg font-medium transition-all shadow-lg hover:shadow-green-500/30 flex items-center gap-2"> | |
| <i data-feather="play"></i> Start Game | |
| </button> | |
| <button id="muteBtn" class="px-6 py-3 bg-slate-700 hover:bg-slate-600 rounded-lg font-medium transition-all flex items-center gap-2"> | |
| <i data-feather="volume-2"></i> Sound On | |
| </button> | |
| </div> | |
| </div> | |
| <div class="relative"> | |
| <canvas id="gameCanvas" width="800" height="500" class="w-full max-w-full"></canvas> | |
| <div id="noteEffects" class="pointer-events-none"></div> | |
| </div> | |
| <div class="mt-8 w-full max-w-4xl"> | |
| <div class="bg-slate-800 bg-opacity-50 rounded-xl p-6 backdrop-blur-sm"> | |
| <h2 class="text-2xl font-semibold mb-4 text-indigo-300">Your Cosmic Composition</h2> | |
| <div id="activeInstruments" class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"></div> | |
| <div class="h-24 bg-slate-900 rounded-lg p-4 flex items-center justify-center"> | |
| <p id="currentTempo" class="text-lg">Tempo: 120 BPM</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-12 text-slate-400 text-sm flex items-center gap-2"> | |
| <i data-feather="info"></i> | |
| <span>Shoot aliens to add notes to your song! Higher aliens = higher pitch</span> | |
| </div> | |
| <script> | |
| // Game variables | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let gameRunning = false; | |
| let isMuted = false; | |
| let score = 0; | |
| let lives = 3; | |
| let tempo = 120; | |
| // Initialize audio | |
| let synth, fmSynth, amSynth, pluckSynth; | |
| async function initAudio() { | |
| await Tone.start(); | |
| synth = new Tone.PolySynth(Tone.Synth).toDestination(); | |
| // Game audio initialization | |
| let instruments = []; | |
| const instrumentNames = ["Space Lead", "FM Waves", "AM Modulation", "Pluck Strings"]; | |
| const instrumentColors = ["text-purple-400", "text-blue-400", "text-pink-400", "text-green-400"]; | |
| // Game variables | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let gameRunning = false; | |
| let isMuted = false; | |
| let score = 0; | |
| let lives = 3; | |
| let tempo = 120; | |
| // Player | |
| const player = { | |
| x: canvas.width / 2, | |
| y: canvas.height - 30, | |
| width: 60, | |
| height: 20, | |
| speed: 8, | |
| color: '#ec4899' | |
| }; | |
| // Bullets | |
| const bullets = []; | |
| const bulletSpeed = 8; | |
| // Aliens | |
| const aliens = []; | |
| const alienWidth = 40; | |
| const alienHeight = 30; | |
| const alienPadding = 20; | |
| const alienOffsetTop = 60; | |
| const alienOffsetLeft = 60; | |
| const alienRows = 4; | |
| const alienCols = 8; | |
| // Active notes (sounding notes) | |
| const activeNotes = []; | |
| // Initialize game | |
| function initGame() { | |
| bullets.length = 0; | |
| aliens.length = 0; | |
| activeNotes.length = 0; | |
| document.getElementById('activeInstruments').innerHTML = ''; | |
| score = 0; | |
| lives = 3; | |
| tempo = 120; | |
| document.getElementById('currentTempo').textContent = `Tempo: ${tempo} BPM`; | |
| // Create aliens | |
| for (let r = 0; r < alienRows; r++) { | |
| for (let c = 0; c < alienCols; c++) { | |
| aliens.push({ | |
| x: alienOffsetLeft + c * (alienWidth + alienPadding), | |
| y: alienOffsetTop + r * (alienHeight + alienPadding), | |
| width: alienWidth, | |
| height: alienHeight, | |
| alive: true, | |
| row: r, | |
| col: c | |
| }); | |
| } | |
| } | |
| gameRunning = true; | |
| animate(); | |
| } | |
| // Draw player | |
| function drawPlayer() { | |
| ctx.fillStyle = player.color; | |
| ctx.fillRect(player.x - player.width / 2, player.y - player.height / 2, player.width, player.height); | |
| // Draw player ship details | |
| ctx.fillStyle = '#f97316'; | |
| ctx.beginPath(); | |
| ctx.moveTo(player.x, player.y - player.height / 2); | |
| ctx.lineTo(player.x + 10, player.y - player.height / 2 - 15); | |
| ctx.lineTo(player.x - 10, player.y - player.height / 2 - 15); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } | |
| // Draw bullets | |
| function drawBullets() { | |
| ctx.fillStyle = '#facc15'; | |
| bullets.forEach(bullet => { | |
| ctx.beginPath(); | |
| ctx.arc(bullet.x, bullet.y, 5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| }); | |
| } | |
| // Draw aliens | |
| function drawAliens() { | |
| aliens.forEach(alien => { | |
| if (alien.alive) { | |
| // Alien body | |
| ctx.fillStyle = r % 2 === 0 ? '#4ade80' : '#60a5fa'; | |
| ctx.fillRect(alien.x, alien.y, alien.width, alien.height); | |
| // Alien details | |
| ctx.fillStyle = '#1e293b'; | |
| ctx.beginPath(); | |
| ctx.arc(alien.x + 10, alien.y + 10, 4, 0, Math.PI * 2); | |
| ctx.arc(alien.x + alien.width - 10, alien.y + 10, 4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillRect(alien.x + alien.width / 2 - 5, alien.y + 15, 10, 5); | |
| } | |
| }); | |
| } | |
| // Update game state | |
| function update() { | |
| // Move bullets | |
| for (let i = bullets.length - 1; i >= 0; i--) { | |
| bullets[i].y -= bulletSpeed; | |
| // Remove bullets that are off screen | |
| if (bullets[i].y < 0) { | |
| bullets.splice(i, 1); | |
| continue; | |
| } | |
| // Check for collisions with aliens | |
| for (let j = 0; j < aliens.length; j++) { | |
| if (aliens[j].alive && | |
| bullets[i].x > aliens[j].x && | |
| bullets[i].x < aliens[j].x + aliens[j].width && | |
| bullets[i].y > aliens[j].y && | |
| bullets[i].y < aliens[j].y + aliens[j].height) { | |
| // Alien hit! | |
| aliens[j].alive = false; | |
| bullets.splice(i, 1); | |
| score += 10; | |
| // Create a musical note based on alien position | |
| createMusicNote(aliens[j].x, aliens[j].y, aliens[j].row, aliens[j].col); | |
| break; | |
| } | |
| } | |
| } | |
| // Move aliens (simple left-right movement) | |
| // In a real game this would be more sophisticated | |
| const alienSpeed = 1 + score / 500; | |
| // Check if any aliens have reached the bottom | |
| aliens.forEach(alien => { | |
| if (alien.alive && alien.y + alien.height > canvas.height - 20) { | |
| lives--; | |
| if (lives <= 0) { | |
| gameRunning = false; | |
| alert(`Game Over! Your cosmic score: ${score}`); | |
| } | |
| } | |
| }); | |
| // Increase tempo based on score | |
| tempo = Math.min(200, 120 + Math.floor(score / 100)); | |
| document.getElementById('currentTempo').textContent = `Tempo: ${tempo} BPM`; | |
| Tone.Transport.bpm.value = tempo; | |
| } | |
| // Create a musical note when alien is hit | |
| function createMusicNote(x, y, row, col) { | |
| if (isMuted) return; | |
| // Determine pitch based on vertical position (row) | |
| const notes = ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5']; | |
| const noteIndex = row % notes.length; | |
| const note = notes[noteIndex]; | |
| // Choose instrument based on column | |
| const instrumentIndex = col % instruments.length; | |
| const instrument = instruments[instrumentIndex]; | |
| // Play the note | |
| const now = Tone.now(); | |
| instrument.triggerAttackRelease(note, "8n", now); | |
| // Add visual note effect | |
| const noteEffect = document.createElement('div'); | |
| noteEffect.className = `music-note ${instrumentColors[instrumentIndex]}`; | |
| noteEffect.style.left = `${x}px`; | |
| noteEffect.style.top = `${y}px`; | |
| noteEffect.textContent = note; | |
| document.getElementById('noteEffects').appendChild(noteEffect); | |
| // Remove after animation | |
| setTimeout(() => { | |
| noteEffect.remove(); | |
| }, 1000); | |
| // Add to active instruments display if not already there | |
| if (!activeNotes.includes(instrumentIndex)) { | |
| activeNotes.push(instrumentIndex); | |
| const instrumentCard = document.createElement('div'); | |
| instrumentCard.className = `bg-slate-700 bg-opacity-70 rounded-lg p-3 flex items-center gap-3 ${instrumentColors[instrumentIndex]}`; | |
| instrumentCard.innerHTML = ` | |
| <i data-feather="music"></i> | |
| <div> | |
| <div class="font-medium">${instrumentNames[instrumentIndex]}</div> | |
| <div class="text-xs">Playing: ${note}</div> | |
| </div> | |
| `; | |
| document.getElementById('activeInstruments').appendChild(instrumentCard); | |
| feather.replace(); | |
| } | |
| } | |
| // Main game loop | |
| function animate() { | |
| if (!gameRunning) return; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| update(); | |
| drawPlayer(); | |
| drawBullets(); | |
| drawAliens(); | |
| requestAnimationFrame(animate); | |
| } | |
| // Event listeners | |
| document.addEventListener('keydown', (e) => { | |
| if (!gameRunning) return; | |
| switch(e.key) { | |
| case 'ArrowLeft': | |
| player.x = Math.max(player.width / 2, player.x - player.speed); | |
| break; | |
| case 'ArrowRight': | |
| player.x = Math.min(canvas.width - player.width / 2, player.x + player.speed); | |
| break; | |
| case ' ': | |
| bullets.push({ | |
| x: player.x, | |
| y: player.y - player.height / 2 - 15 | |
| }); | |
| break; | |
| } | |
| }); | |
| // Initialize audio context on user interaction | |
| let audioInitialized = false; | |
| async function initAudioContext() { | |
| if (!audioInitialized) { | |
| await Tone.start(); | |
| synth = new Tone.PolySynth(Tone.Synth).toDestination(); | |
| fmSynth = new Tone.PolySynth(Tone.FMSynth).toDestination(); | |
| amSynth = new Tone.PolySynth(Tone.AMSynth).toDestination(); | |
| pluckSynth = new Tone.PolySynth(Tone.PluckSynth).toDestination(); | |
| audioInitialized = true; | |
| Tone.Transport.start(); | |
| isMuted = false; | |
| document.getElementById('muteBtn').innerHTML = '<i data-feather="volume-2"></i> Sound On'; | |
| feather.replace(); | |
| } | |
| return audioInitialized; | |
| } | |
| document.getElementById('startBtn').addEventListener('click', async () => { | |
| if (!gameRunning) { | |
| try { | |
| await initAudioContext(); | |
| initGame(); | |
| } catch (e) { | |
| console.error("Audio initialization failed:", e); | |
| alert("Please click the page first to enable audio, then click Start Game again"); | |
| } | |
| } | |
| }); | |
| // Enable audio context on any user interaction | |
| document.addEventListener('click', async function init() { | |
| try { | |
| await Tone.start(); | |
| audioInitialized = true; | |
| document.removeEventListener('click', init); | |
| } catch (e) { | |
| console.error("Initial audio setup failed:", e); | |
| } | |
| }, { once: true }); | |
| document.getElementById('muteBtn').addEventListener('click', () => { | |
| isMuted = !isMuted; | |
| const btn = document.getElementById('muteBtn'); | |
| if (isMuted) { | |
| btn.innerHTML = '<i data-feather="volume-x"></i> Sound Off'; | |
| Tone.Transport.stop(); | |
| } else { | |
| btn.innerHTML = '<i data-feather="volume-2"></i> Sound On'; | |
| Tone.Transport.start(); | |
| } | |
| feather.replace(); | |
| }); | |
| // Initialize UI | |
| isMuted = true; | |
| document.getElementById('muteBtn').innerHTML = '<i data-feather="volume-x"></i> Sound Off'; | |
| feather.replace(); | |
| </script> | |
| </body> | |
| </html> | |