Buckets:
| <think> | |
| Let me create a complete, self-contained HTML file implementing an Arcanoid/Breakout game with all the specified features: | |
| 1. **Neon Theme** - Dark background, animated grid, floating particles, glowing elements | |
| 2. **Game Mechanics** - Paddle control, ball physics, lives, scoring, multiple levels | |
| 3. **Web Audio API Sounds** - All synthesized sounds | |
| 4. **Power-up System** - 6 different power-ups with temporary effects | |
| 5. **Technical Requirements** - Single HTML file, Canvas rendering, responsive, 60fps | |
| Let me plan the architecture: | |
| **Game States:** | |
| - START_SCREEN | |
| - PLAYING | |
| - GAME_OVER | |
| - LEVEL_COMPLETE | |
| **Entities:** | |
| - Paddle | |
| - Ball(s) | |
| - Bricks | |
| - Power-ups | |
| - Particles (for effects) | |
| **Audio System:** | |
| - Oscillator-based sounds | |
| - Different frequencies for each event | |
| **Visual Effects:** | |
| - Neon glow using shadowBlur | |
| - Animated grid background | |
| - Floating particles | |
| - Color cycling | |
| Let me write this comprehensive implementation. | |
| </think> | |
| ```html | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=10.0"> | |
| <title>NEON ACRANOID</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| background: #000; | |
| overflow: hidden; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 100vh; | |
| font-family: 'Courier New', monospace; | |
| } | |
| canvas { | |
| display: block; | |
| image-rendering: pixelated; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="gameCanvas"></canvas> | |
| <script> | |
| // ============================================================ | |
| // NEON ACRANOID - Complete Breakout Game | |
| // ============================================================ | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // --- CONFIGURATION --- | |
| const GAME_WIDTH = 480; | |
| const GAME_HEIGHT = 720; | |
| const PADDLE_WIDTH = 80; | |
| const PADDLE_HEIGHT = 14; | |
| const BALL_RADIUS = 6; | |
| const BRICK_ROWS = 6; | |
| const BRICK_COLS = 8; | |
| const BRICK_HEIGHT = 24; | |
| const BRICK_PADDING = 4; | |
| const BRICK_TOP_OFFSET = 80; | |
| const MAX_LIVES = 3; | |
| const POWERUP_CHANCE = 0.20; | |
| // Neon color palette | |
| const NEON_COLORS = { | |
| cyan: '#00FFFF', | |
| magenta: '#FF00FF', | |
| lime: '#39FF14', | |
| electricBlue: '#1E90FF', | |
| hotPink: '#FF69B4', | |
| orange: '#FF6600', | |
| yellow: '#FFFF00', | |
| red: '#FF0044', | |
| white: '#FFFFFF' | |
| }; | |
| // Power-up types | |
| const POWERUP_TYPES = [ | |
| { name: 'WIDE', color: NEON_COLORS.cyan, symbol: 'W', duration: 10000 }, | |
| { name: 'MULTI', color: NEON_COLORS.magenta, symbol: 'M', duration: 0 }, // permanent-ish | |
| { name: 'SLOW', color: NEON_COLORS.electricBlue, symbol: 'S', duration: 8000 }, | |
| { name: 'STICKY', color: NEON_COLORS.hotPink, symbol: 'T', duration: 12000 }, | |
| { name: 'LASER', color: NEON_COLORS.orange, symbol: 'L', duration: 7000 }, | |
| { name: 'LIFE', color: NEON_COLORS.lime, symbol: '+', duration: 0 } | |
| ]; | |
| // ============================================================ | |
| // AUDIO SYSTEM (Web Audio API) | |
| // ============================================================ | |
| class AudioSystem { | |
| constructor() { | |
| this.ctx = null; | |
| this.masterGain = null; | |
| this.initialized = false; | |
| this.volume = 0.5; | |
| } | |
| init() { | |
| if (this.initialized) return; | |
| try { | |
| this.ctx = new (window.AudioContext || window.webkitAudioContext)(); | |
| this.masterGain = this.ctx.createGain(); | |
| this.masterGain.gain.value = this.volume; | |
| this.masterGain.connect(this.ctx.destination); | |
| this.initialized = true; | |
| } catch (e) { | |
| console.warn('Web Audio API not available'); | |
| } | |
| } | |
| setVolume(v) { | |
| this.volume = v; | |
| if (this.masterGain) this.masterGain.gain.value = v; | |
| } | |
| // Ball hitting wall | |
| playWallHit() { | |
| if (!this.initialized) return; | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = 'square'; | |
| osc.frequency.setValueAtTime(440, this.ctx.currentTime); | |
| osc.frequency.exponentialRampToValueAtTime(220, this.ctx.currentTime + 0.08); | |
| gain.gain.setValueAtTime(0.15, this.ctx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.08); | |
| osc.connect(gain); | |
| gain.connect(this.masterGain); | |
| osc.start(this.ctx.currentTime); | |
| osc.stop(this.ctx.currentTime + 0.08); | |
| } | |
| // Ball hitting paddle | |
| playPaddleHit() { | |
| if (!this.initialized) return; | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = 'sine'; | |
| osc.frequency.setValueAtTime(520, this.ctx.currentTime); | |
| osc.frequency.exponentialRampToValueAtTime(380, this.ctx.currentTime + 0.1); | |
| gain.gain.setValueAtTime(0.2, this.ctx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.1); | |
| osc.connect(gain); | |
| gain.connect(this.masterGain); | |
| osc.start(this.ctx.currentTime); | |
| osc.stop(this.ctx.currentTime + 0.1); | |
| } | |
| // Brick destroyed | |
| playBrickDestroy(brickLevel) { | |
| if (!this.initialized) return; | |
| const baseFreq = 600 + (brickLevel * 100); | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = 'sine'; | |
| osc.frequency.setValueAtTime(baseFreq, this.ctx.currentTime); | |
| osc.frequency.exponentialRampToValueAtTime(baseFreq * 2.5, this.ctx.currentTime + 0.15); | |
| gain.gain.setValueAtTime(0.25, this.ctx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.2); | |
| osc.connect(gain); | |
| gain.connect(this.masterGain); | |
| osc.start(this.ctx.currentTime); | |
| osc.stop(this.ctx.currentTime + 0.2); | |
| // Add sparkle | |
| const osc2 = this.ctx.createOscillator(); | |
| const gain2 = this.ctx.createGain(); | |
| osc2.type = 'triangle'; | |
| osc2.frequency.setValueAtTime(baseFreq * 1.5, this.ctx.currentTime + 0.02); | |
| osc2.frequency.exponentialRampToValueAtTime(baseFreq * 3, this.ctx.currentTime + 0.15); | |
| gain2.gain.setValueAtTime(0.1, this.ctx.currentTime + 0.02); | |
| gain2.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.15); | |
| osc2.connect(gain2); | |
| gain2.connect(this.masterGain); | |
| osc2.start(this.ctx.currentTime + 0.02); | |
| osc2.stop(this.ctx.currentTime + 0.17); | |
| } | |
| // Power-up collected | |
| playPowerUpCollect() { | |
| if (!this.initialized) return; | |
| const notes = [523, 659, 784, 1046]; | |
| notes.forEach((freq, i) => { | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = 'sine'; | |
| osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.06); | |
| gain.gain.setValueAtTime(0.15, this.ctx.currentTime + i * 0.06); | |
| gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + i * 0.06 + 0.15); | |
| osc.connect(gain); | |
| gain.connect(this.masterGain); | |
| osc.start(this.ctx.currentTime + i * 0.06); | |
| osc.stop(this.ctx.currentTime + i * 0.06 + 0.15); | |
| }); | |
| } | |
| // Life lost | |
| playLifeLost() { | |
| if (!this.initialized) return; | |
| const notes = [440, 392, 349, 330]; | |
| notes.forEach((freq, i) => { | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = 'sawtooth'; | |
| osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.15); | |
| gain.gain.setValueAtTime(0.12, this.ctx.currentTime + i * 0.15); | |
| gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + i * 0.15 + 0.2); | |
| osc.connect(gain); | |
| gain.connect(this.masterGain); | |
| osc.start(this.ctx.currentTime + i * 0.15); | |
| osc.stop(this.ctx.currentTime + i * 0.15 + 0.2); | |
| }); | |
| } | |
| // Game over | |
| playGameOver() { | |
| if (!this.initialized) return; | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| const filter = this.ctx.createBiquadFilter(); | |
| osc.type = 'sawtooth'; | |
| osc.frequency.setValueAtTime(300, this.ctx.currentTime); | |
| osc.frequency.exponentialRampToValueAtTime(40, this.ctx.currentTime + 0.8); | |
| filter.type = 'lowpass'; | |
| filter.frequency.setValueAtTime(2000, this.ctx.currentTime); | |
| filter.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + 0.8); | |
| gain.gain.setValueAtTime(0.25, this.ctx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.8); | |
| osc.connect(filter); | |
| filter.connect(gain); | |
| gain.connect(this.masterGain); | |
| osc.start(this.ctx.currentTime); | |
| osc.stop(this.ctx.currentTime + 0.8); | |
| } | |
| // Level complete | |
| playLevelComplete() { | |
| if (!this.initialized) return; | |
| const notes = [392, 440, 523, 587, 659, 784, 880, 1046]; | |
| notes.forEach((freq, i) => { | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = 'sine'; | |
| osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.08); | |
| gain.gain.setValueAtTime(0.18, this.ctx.currentTime + i * 0.08); | |
| gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + i * 0.08 + 0.25); | |
| osc.connect(gain); | |
| gain.connect(this.masterGain); | |
| osc.start(this.ctx.currentTime + i * 0.08); | |
| osc.stop(this.ctx.currentTime + i * 0.08 + 0.25); | |
| }); | |
| } | |
| // Laser shot | |
| playLaserShot() { | |
| if (!this.initialized) return; | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = 'square'; | |
| osc.frequency.setValueAtTime(800, this.ctx.currentTime); | |
| osc.frequency.exponentialRampToValueAtTime(1200, this.ctx.currentTime + 0.03); | |
| gain.gain.setValueAtTime(0.1, this.ctx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.05); | |
| osc.connect(gain); | |
| gain.connect(this.masterGain); | |
| osc.start(this.ctx.currentTime); | |
| osc.stop(this.ctx.currentTime + 0.05); | |
| } | |
| // Laser hit brick | |
| playLaserHit() { | |
| if (!this.initialized) return; | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = 'sawtooth'; | |
| osc.frequency.setValueAtTime(1000, this.ctx.currentTime); | |
| osc.frequency.exponentialRampToValueAtTime(200, this.ctx.currentTime + 0.06); | |
| gain.gain.setValueAtTime(0.12, this.ctx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.06); | |
| osc.connect(gain); | |
| gain.connect(this.masterGain); | |
| osc.start(this.ctx.currentTime); | |
| osc.stop(this.ctx.currentTime + 0.06); | |
| } | |
| } | |
| const audio = new AudioSystem(); | |
| // ============================================================ | |
| // PARTICLE SYSTEM | |
| // ============================================================ | |
| class Particle { | |
| constructor(x, y, color, vx, vy, life, size) { | |
| this.x = x; | |
| this.y = y; | |
| this.color = color; | |
| this.vx = vx; | |
| this.vy = vy; | |
| this.life = life; | |
| this.maxLife = life; | |
| this.size = size || 2; | |
| } | |
| update(dt) { | |
| this.x += this.vx * dt; | |
| this.y += this.vy * dt; | |
| this.life -= dt; | |
| this.vy += 50 * dt; // gravity | |
| } | |
| draw(ctx) { | |
| const alpha = Math.max(0, this.life / this.maxLife); | |
| ctx.save(); | |
| ctx.globalAlpha = alpha; | |
| ctx.shadowBlur = 8; | |
| ctx.shadowColor = this.color; | |
| ctx.fillStyle = this.color; | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.size * alpha, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| isDead() { | |
| return this.life <= 0; | |
| } | |
| } | |
| // ============================================================ | |
| // BACKGROUND RENDERER (Grid + Particles) | |
| // ============================================================ | |
| class BackgroundRenderer { | |
| constructor() { | |
| this.time = 0; | |
| this.bgParticles = []; | |
| // Create floating particles | |
| for (let i = 0; i < 60; i++) { | |
| this.bgParticles.push({ | |
| x: Math.random() * GAME_WIDTH, | |
| y: Math.random() * GAME_HEIGHT, | |
| size: Math.random() * 2 + 1, | |
| speed: Math.random() * 20 + 5, | |
| phase: Math.random() * Math.PI * 2, | |
| color: Object.values(NEON_COLORS)[Math.floor(Math.random() * 6)] | |
| }); | |
| } | |
| } | |
| update(dt) { | |
| this.time += dt; | |
| this.bgParticles.forEach(p => { | |
| p.y -= p.speed * dt; | |
| p.x += Math.sin(this.time + p.phase) * 10 * dt; | |
| if (p.y < -10) { | |
| p.y = GAME_HEIGHT + 10; | |
| p.x = Math.random() * GAME_WIDTH; | |
| } | |
| }); | |
| } | |
| draw(ctx) { | |
| // Dark background | |
| ctx.fillStyle = '#0a0a12'; | |
| ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); | |
| // Animated neon grid | |
| const gridSpacing = 40; | |
| const gridOffset = (this.time * 15) % gridSpacing; | |
| ctx.save(); | |
| ctx.globalAlpha = 0.15; | |
| ctx.strokeStyle = NEON_COLORS.cyan; | |
| ctx.shadowBlur = 4; | |
| ctx.shadowColor = NEON_COLORS.cyan; | |
| ctx.lineWidth = 1; | |
| // Vertical lines | |
| for (let x = 0; x <= GAME_WIDTH; x += gridSpacing) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x, 0); | |
| ctx.lineTo(x, GAME_HEIGHT); | |
| ctx.stroke(); | |
| } | |
| // Horizontal lines with animation | |
| for (let y = -gridSpacing + gridOffset; y <= GAME_HEIGHT; y += gridSpacing) { | |
| ctx.beginPath(); | |
| ctx.moveTo(0, y); | |
| ctx.lineTo(GAME_WIDTH, y); | |
| ctx.stroke(); | |
| } | |
| ctx.restore(); | |
| // Floating particles | |
| this.bgParticles.forEach(p => { | |
| const pulse = 0.5 + 0.5 * Math.sin(this.time * 2 + p.phase); | |
| ctx.save(); | |
| ctx.globalAlpha = 0.3 + pulse * 0.4; | |
| ctx.shadowBlur = 6; | |
| ctx.shadowColor = p.color; | |
| ctx.fillStyle = p.color; | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, p.size * (0.8 + pulse * 0.4), 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| }); | |
| // Subtle gradient overlay at top | |
| const grad = ctx.createLinearGradient(0, 0, 0, 120); | |
| grad.addColorStop(0, 'rgba(0, 255, 255, 0.08)'); | |
| grad.addColorStop(1, 'rgba(0, 0, 0, 0)'); | |
| ctx.fillStyle = grad; | |
| ctx.fillRect(0, 0, GAME_WIDTH, 120); | |
| } | |
| } | |
| // ============================================================ | |
| // BRICK CLASS | |
| // ============================================================ | |
| class Brick { | |
| constructor(x, y, width, height, hits, color) { | |
| this.x = x; | |
| this.y = y; | |
| this.width = width; | |
| this.height = height; | |
| this.hits = hits; | |
| this.maxHits = hits; | |
| this.color = color; | |
| this.alive = true; | |
| this.hasPowerUp = Math.random() < POWERUP_CHANCE; | |
| this.powerUpType = this.hasPowerUp ? POWERUP_TYPES[Math.floor(Math.random() * POWERUP_TYPES.length)] : null; | |
| } | |
| hit() { | |
| this.hits--; | |
| return this.hits <= 0; | |
| } | |
| draw(ctx) { | |
| if (!this.alive) return; | |
| const alpha = this.hits / this.maxHits; | |
| const glowIntensity = 8 + (alpha * 12); | |
| ctx.save(); | |
| ctx.shadowBlur = glowIntensity; | |
| ctx.shadowColor = this.color; | |
| // Main brick body | |
| ctx.fillStyle = this.color; | |
| ctx.globalAlpha = 0.3 + alpha * 0.5; | |
| ctx.fillRect(this.x + 1, this.y + 1, this.width - 2, this.height - 2); | |
| // Border | |
| ctx.globalAlpha = 0.7 + alpha * 0.3; | |
| ctx.strokeStyle = this.color; | |
| ctx.lineWidth = 1.5; | |
| ctx.strokeRect(this.x, this.y, this.width, this.height); | |
| // Crack effect for damaged bricks | |
| if (this.hits < this.maxHits) { | |
| ctx.globalAlpha = 0.6; | |
| ctx.strokeStyle = '#000'; | |
| ctx.lineWidth = 1; | |
| const cx = this.x + this.width / 2; | |
| const cy = this.y + this.height / 2; | |
| ctx.beginPath(); | |
| ctx.moveTo(cx - this.width * 0.3, cy - this.height * 0.3); | |
| ctx.lineTo(cx + this.width * 0.1, cy); | |
| ctx.lineTo(cx + this.width * 0.3, cy - this.height * 0.2); | |
| ctx.stroke(); | |
| ctx.beginPath(); | |
| ctx.moveTo(cx - this.width * 0.1, cy + this.height * 0.1); | |
| ctx.lineTo(cx + this.width * 0.2, cy + this.height * 0.3); | |
| ctx.stroke(); | |
| } | |
| // Hit indicator for multi-hit bricks | |
| if (this.maxHits > 1) { | |
| ctx.globalAlpha = 0.9; | |
| ctx.fillStyle = '#fff'; | |
| ctx.font = 'bold 12px Courier New'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.shadowBlur = 4; | |
| ctx.shadowColor = '#fff'; | |
| ctx.fillText(this.hits.toString(), this.x + this.width / 2, this.y + this.height / 2); | |
| } | |
| // Power-up indicator | |
| if (this.hasPowerUp && this.alive) { | |
| ctx.globalAlpha = 0.5 + 0.3 * Math.sin(Date.now() / 300); | |
| ctx.fillStyle = this.powerUpType.color; | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = this.powerUpType.color; | |
| ctx.beginPath(); | |
| ctx.arc(this.x + this.width / 2, this.y + this.height / 2, 3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| ctx.restore(); | |
| } | |
| } | |
| // ============================================================ | |
| // BALL CLASS | |
| // ============================================================ | |
| class Ball { | |
| constructor(x, y, speed) { | |
| this.x = x; | |
| this.y = y; | |
| this.radius = BALL_RADIUS; | |
| this.speed = speed || 280; | |
| this.vx = 0; | |
| this.vy = 0; | |
| this.stuck = false; | |
| this.trail = []; | |
| this.color = NEON_COLORS.white; | |
| } | |
| launch(angle) { | |
| const rad = angle || -Math.PI / 4; | |
| this.vx = Math.cos(rad) * this.speed; | |
| this.vy = Math.sin(rad) * this.speed; | |
| if (this.vy > -20) this.vy = -20; | |
| this.stuck = false; | |
| } | |
| update(dt, paddle, bricks) { | |
| if (this.stuck) { | |
| this.x = paddle.x + paddle.width / 2; | |
| this.y = paddle.y - this.radius - 1; | |
| return; | |
| } | |
| // Store trail | |
| this.trail.push({ x: this.x, y: this.y }); | |
| if (this.trail.length > 8) this.trail.shift(); | |
| this.x += this.vx * dt; | |
| this.y += this.vy * dt; | |
| // Wall collisions | |
| if (this.x - this.radius <= 0) { | |
| this.x = this.radius; | |
| this.vx = Math.abs(this.vx); | |
| audio.playWallHit(); | |
| } | |
| if (this.x + this.radius >= GAME_WIDTH) { | |
| this.x = GAME_WIDTH - this.radius; | |
| this.vx = -Math.abs(this.vx); | |
| audio.playWallHit(); | |
| } | |
| if (this.y - this.radius <= 0) { | |
| this.y = this.radius; | |
| this.vy = Math.abs(this.vy); | |
| audio.playWallHit(); | |
| } | |
| // Paddle collision | |
| if (this.vy > 0 && | |
| this.y + this.radius >= paddle.y && | |
| this.y + this.radius <= paddle.y + paddle.height + 4 && | |
| this.x >= paddle.x - this.radius && | |
| this.x <= paddle.x + paddle.width + this.radius) { | |
| // Calculate bounce angle based on where ball hits paddle | |
| const hitPos = (this.x - paddle.x) / paddle.width; // 0 to 1 | |
| const angle = (hitPos - 0.5) * Math.PI * 0.7; // -63 to +63 degrees | |
| const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy); | |
| this.vx = Math.sin(angle) * speed; | |
| this.vy = -Math.cos(angle * 0.8) * speed; | |
| this.y = paddle.y - this.radius - 1; | |
| // Sticky paddle: ball stays stuck | |
| if (paddle.sticky) { | |
| this.stuck = true; | |
| this.vx = 0; | |
| this.vy = 0; | |
| } | |
| audio.playPaddleHit(); | |
| } | |
| // Brick collisions | |
| for (let brick of bricks) { | |
| if (!brick.alive) continue; | |
| if (this.x + this.radius > brick.x && | |
| this.x - this.radius < brick.x + brick.width && | |
| this.y + this.radius > brick.y && | |
| this.y - this.radius < brick.y + brick.height) { | |
| // Determine collision side | |
| const overlapLeft = (this.x + this.radius) - brick.x; | |
| const overlapRight = (brick.x + brick.width) - (this.x - this.radius); | |
| const overlapTop = (this.y + this.radius - brick.y); | |
| const overlapBottom = (brick.y + brick.height) - (this.y - this.radius); | |
| const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom); | |
| if (minOverlap === overlapLeft || minOverlap === overlapRight) { | |
| this.vx = -this.vx; | |
| } else { | |
| this.vy = -this.vy; | |
| } | |
| const destroyed = brick.hit(); | |
| if (destroyed) { | |
| brick.alive = false; | |
| audio.playBrickDestroy(brick.maxHits); | |
| return { destroyed: true, brick: brick }; | |
| } | |
| break; // One brick per frame | |
| } | |
| } | |
| return null; | |
| } | |
| draw(ctx) { | |
| // Trail | |
| for (let i = 0; i < this.trail.length; i++) { | |
| const t = this.trail[i]; | |
| const alpha = (i / this.trail.length) * 0.4; | |
| const size = this.radius * (i / this.trail.length); | |
| ctx.save(); | |
| ctx.globalAlpha = alpha; | |
| ctx.shadowBlur = 6; | |
| ctx.shadowColor = this.color; | |
| ctx.fillStyle = this.color; | |
| ctx.beginPath(); | |
| ctx.arc(t.x, t.y, size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| // Ball | |
| ctx.save(); | |
| ctx.shadowBlur = 14; | |
| ctx.shadowColor = this.color; | |
| ctx.fillStyle = this.color; | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Inner highlight | |
| ctx.fillStyle = '#fff'; | |
| ctx.globalAlpha = 0.6; | |
| ctx.beginPath(); | |
| ctx.arc(this.x - 1, this.y - 1, this.radius * 0.4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| } | |
| // ============================================================ | |
| // LASER BULLET CLASS | |
| // ============================================================ | |
| class LaserBullet { | |
| constructor(x, y) { | |
| this.x = x; | |
| this.y = y; | |
| this.width = 3; | |
| this.height = 12; | |
| this.speed = 400; | |
| this.alive = true; | |
| } | |
| update(dt) { | |
| this.y -= this.speed * dt; | |
| if (this.y < -20) this.alive = false; | |
| } | |
| draw(ctx) { | |
| ctx.save(); | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = NEON_COLORS.orange; | |
| ctx.fillStyle = NEON_COLORS.orange; | |
| ctx.fillRect(this.x - this.width / 2, this.y, this.width, this.height); | |
| ctx.restore(); | |
| } | |
| } | |
| // ============================================================ | |
| // POWER-UP CAPSULE CLASS | |
| // ============================================================ | |
| class PowerUpCapsule { | |
| constructor(x, y, type) { | |
| this.x = x; | |
| this.y = y; | |
| this.width = 20; | |
| this.height = 20; | |
| this.type = type; | |
| this.speed = 80; | |
| this.alive = true; | |
| this.pulsePhase = 0; | |
| } | |
| update(dt) { | |
| this.y += this.speed * dt; | |
| this.pulsePhase += dt * 5; | |
| if (this.y > GAME_HEIGHT + 20) this.alive = false; | |
| } | |
| draw(ctx) { | |
| const pulse = 0.8 + 0.2 * Math.sin(this.pulsePhase); | |
| ctx.save(); | |
| ctx.shadowBlur = 12 * pulse; | |
| ctx.shadowColor = this.type.color; | |
| ctx.strokeStyle = this.type.color; | |
| ctx.lineWidth = 2; | |
| ctx.fillStyle = this.type.color; | |
| ctx.globalAlpha = 0.3 * pulse; | |
| // Capsule shape | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, 10, 0, Math.PI); | |
| ctx.arc(this.x, this.y + 10, 10, Math.PI, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.globalAlpha = 0.8; | |
| ctx.strokeStyle = this.type.color; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| // Symbol | |
| ctx.globalAlpha = 1; | |
| ctx.fillStyle = '#fff'; | |
| ctx.font = 'bold 12px Courier New'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.shadowBlur = 4; | |
| ctx.shadowColor = this.type.color; | |
| ctx.fillText(this.type.symbol, this.x, this.y + 5); | |
| ctx.restore(); | |
| } | |
| } | |
| // ============================================================ | |
| // PADDLE CLASS | |
| // ============================================================ | |
| class Paddle { | |
| constructor() { | |
| this.width = PADDLE_WIDTH; | |
| this.height = PADDLE_HEIGHT; | |
| this.x = GAME_WIDTH / 2 - this.width / 2; | |
| this.y = GAME_HEIGHT - 40; | |
| this.baseWidth = this.width; | |
| this.speed = 350; | |
| this.sticky = false; | |
| this.stickyTimer = 0; | |
| this.laserActive = false; | |
| this.laserTimer = 0; | |
| this.wideActive = false; | |
| this.wideTimer = 0; | |
| this.slowActive = false; | |
| this.slowTimer = 0; | |
| this.lastLaserShot = 0; | |
| } | |
| update(dt, mouseX, keys) { | |
| // Movement | |
| if (mouseX !== null) { | |
| this.x = mouseX - this.width / 2; | |
| } | |
| if (keys.left) this.x -= this.speed * dt; | |
| if (keys.right) this.x += this.speed * dt; | |
| // Clamp | |
| this.x = Math.max(0, Math.min(GAME_WIDTH - this.width, this.x)); | |
| // Timer updates | |
| if (this.sticky) { | |
| this.stickyTimer -= dt * 1000; | |
| if (this.stickyTimer <= 0) this.sticky = false; | |
| } | |
| if (this.laserActive) { | |
| this.laserTimer -= dt * 1000; | |
| if (this.laserTimer <= 0) this.laserActive = false; | |
| } | |
| if (this.wideActive) { | |
| this.wideTimer -= dt * 1000; | |
| if (this.wideTimer <= 0) { | |
| this.wideActive = false; | |
| this.width = this.baseWidth; | |
| } | |
| } | |
| if (this.slowActive) { | |
| this.slowTimer -= dt * 1000; | |
| if (this.slowTimer <= 0) this.slowActive = false; | |
| } | |
| // Laser auto-fire | |
| if (this.laserActive) { | |
| this.lastLaserShot += dt * 1000; | |
| if (this.lastLaserShot > 250) { | |
| this.lastLaserShot = 0; | |
| return { x: this.x + this.width * 0.25, y: this.y - 5 }; | |
| } | |
| if (this.lastLaserShot > 250) { | |
| this.lastLaserShot = 0; | |
| return { x: this.x + this.width * 0.75, y: this.y - 5 }; | |
| } | |
| } | |
| return null; | |
| } | |
| draw(ctx) { | |
| ctx.save(); | |
| // Glow color based on active power-ups | |
| let glowColor = NEON_COLORS.cyan; | |
| if (this.laserActive) glowColor = NEON_COLORS.orange; | |
| else if (this.sticky) glowColor = NEON_COLORS.hotPink; | |
| else if (this.wideActive) glowColor = NEON_COLORS.cyan; | |
| else if (this.slowActive) glowColor = NEON_COLORS.electricBlue; | |
| const glowIntensity = 15 + 5 * Math.sin(Date.now() / 200); | |
| ctx.shadowBlur = glowIntensity; | |
| ctx.shadowColor = glowColor; | |
| // Paddle body | |
| const grad = ctx.createLinearGradient(this.x, this.y, this.x + this.width, this.y); | |
| grad.addColorStop(0, glowColor); | |
| grad.addColorStop(0.5, '#fff'); | |
| grad.addColorStop(1, glowColor); | |
| ctx.fillStyle = grad; | |
| ctx.globalAlpha = 0.8; | |
| ctx.fillRect(this.x, this.y, this.width, this.height); | |
| // Border | |
| ctx.globalAlpha = 1; | |
| ctx.strokeStyle = glowColor; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(this.x, this.y, this.width, this.height); | |
| // Laser cannons | |
| if (this.laserActive) { | |
| ctx.fillStyle = NEON_COLORS.orange; | |
| ctx.shadowBlur = 8; | |
| ctx.shadowColor = NEON_COLORS.orange; | |
| ctx.fillRect(this.x + this.width * 0.2, this.y - 6, 4, 6); | |
| ctx.fillRect(this.x + this.width * 0.75, this.y - 6, 4, 6); | |
| } | |
| // Sticky indicator | |
| if (this.sticky) { | |
| ctx.globalAlpha = 0.5; | |
| ctx.strokeStyle = NEON_COLORS.hotPink; | |
| ctx.lineWidth = 1; | |
| ctx.setLineDash([3, 3]); | |
| ctx.strokeRect(this.x - 2, this.y - 2, this.width + 4, this.height + 4); | |
| ctx.setLineDash([]); | |
| } | |
| ctx.restore(); | |
| } | |
| applyPowerUp(type) { | |
| switch (type.name) { | |
| case 'WIDE': | |
| this.wideActive = true; | |
| this.wideTimer = type.duration; | |
| this.width = this.baseWidth * 1.6; | |
| break; | |
| case 'SLOW': | |
| this.slowActive = true; | |
| this.slowTimer = type.duration; | |
| break; | |
| case 'STICKY': | |
| this.sticky = true; | |
| this.stickyTimer = type.duration; | |
| break; | |
| case 'LASER': | |
| this.laserActive = true; | |
| this.laserTimer = type.duration; | |
| break; | |
| case 'LIFE': | |
| return 'LIFE'; // Special return for extra life | |
| } | |
| return null; | |
| } | |
| getActiveEffects() { | |
| const effects = []; | |
| if (this.wideActive) effects.push({ name: 'WIDE', timer: this.wideTimer, color: NEON_COLORS.cyan }); | |
| if (this.slowActive) effects.push({ name: 'SLOW', timer: this.slowTimer, color: NEON_COLORS.electricBlue }); | |
| if (this.sticky) effects.push({ name: 'STICKY', timer: this.stickyTimer, color: NEON_COLORS.hotPink }); | |
| if (this.laserActive) effects.push({ name: 'LASER', timer: this.laserTimer, color: NEON_COLORS.orange }); | |
| return effects; | |
| } | |
| } | |
| // ============================================================ | |
| // MAIN GAME CLASS | |
| // ============================================================ | |
| class Game { | |
| constructor() { | |
| this.state = 'START'; // START, PLAYING, GAME_OVER, LEVEL_COMPLETE | |
| this.score = 0; | |
| this.lives = MAX_LIVES; | |
| this.level = 1; | |
| this.balls = []; | |
| this.bricks = []; | |
| this.particles = []; | |
| this.powerUps = []; | |
| this.lasers = []; | |
| this.paddle = new Paddle(); | |
| this.bg = new BackgroundRenderer(); | |
| this.mouseX = null; | |
| this.keys = { left: false, right: false }; | |
| this.ballLaunchAngle = -Math.PI / 4; | |
| this.time = 0; | |
| this.levelCompleteTimer = 0; | |
| this.gameOverTimer = 0; | |
| this.stars = []; | |
| // Resize canvas | |
| this.resize(); | |
| window.addEventListener('resize', () => this.resize()); | |
| // Input | |
| canvas.addEventListener('mousemove', (e) => { | |
| const rect = canvas.getBoundingClientRect(); | |
| const scaleX = GAME_WIDTH / rect.width; | |
| this.mouseX = (e.clientX - rect.left) * scaleX; | |
| }); | |
| canvas.addEventListener('touchmove', (e) => { | |
| e.preventDefault(); | |
| const rect = canvas.getBoundingClientRect(); | |
| const scaleX = GAME_WIDTH / rect.width; | |
| this.mouseX = (e.touches[0].clientX - rect.left) * scaleX; | |
| }, { passive: false }); | |
| canvas.addEventListener('mousedown', () => this.handleClick()); | |
| canvas.addEventListener('touchstart', (e) => { | |
| e.preventDefault(); | |
| this.handleClick(); | |
| }); | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'ArrowLeft' || e.key === 'a') this.keys.left = true; | |
| if (e.key === 'ArrowRight' || e.key === 'd') this.keys.right = true; | |
| if (e.key === ' ' || e.key === 'Enter') { | |
| if (this.state === 'START' || this.state === 'GAME_OVER') { | |
| this.startGame(); | |
| } else if (this.state === 'LEVEL_COMPLETE') { | |
| this.nextLevel(); | |
| } | |
| } | |
| }); | |
| document.addEventListener('keyup', (e) => { | |
| if (e.key === 'ArrowLeft' || e.key === 'a') this.keys.left = false; | |
| if (e.key === 'ArrowRight' || e.key === 'd') this.keys.right = false; | |
| }); | |
| // Start game loop | |
| this.lastTime = performance.now(); | |
| requestAnimationFrame((t) => this.gameLoop(t)); | |
| } | |
| resize() { | |
| const aspect = GAME_WIDTH / GAME_HEIGHT; | |
| const windowAspect = window.innerWidth / window.innerHeight; | |
| let w, h; | |
| if (windowAspect > aspect) { | |
| h = window.innerHeight * 0.95; | |
| w = h * aspect; | |
| } else { | |
| w = window.innerWidth * 0.95; | |
| h = w / aspect; | |
| } | |
| canvas.style.width = w + 'px'; | |
| canvas.style.height = h + 'px'; | |
| canvas.width = GAME_WIDTH; | |
| canvas.height = GAME_HEIGHT; | |
| } | |
| startGame() { | |
| audio.init(); | |
| this.state = 'PLAYING'; | |
| this.score = 0; | |
| this.lives = MAX_LIVES; | |
| this.level = 1; | |
| this.initLevel(); | |
| } | |
| initLevel() { | |
| this.balls = []; | |
| this.bricks = []; | |
| this.particles = []; | |
| this.powerUps = []; | |
| this.lasers = []; | |
| this.paddle = new Paddle(); | |
| // Reset first ball | |
| const ball = new Ball(GAME_WIDTH / 2, GAME_HEIGHT - 60, 280); | |
| ball.stuck = true; | |
| this.balls.push(ball); | |
| // Generate bricks | |
| const brickWidth = (GAME_WIDTH - BRICK_PADDING * (BRICK_COLS + 1)) / BRICK_COLS; | |
| const rows = Math.min(BRICK_ROWS + Math.floor(this.level / 2), 10); | |
| for (let row = 0; row < rows; row++) { | |
| for (let col = 0; col < BRICK_COLS; col++) { | |
| const x = BRICK_PADDING + col * (brickWidth + BRICK_PADDING); | |
| const y = BRICK_TOP_OFFSET + row * (BRICK_HEIGHT + BRICK_PADDING); | |
| // Varying hit points based on row and level | |
| let hits = 1; | |
| if (this.level >= 2 && row < 2) hits = 2; | |
| if (this.level >= 4 && row === 0) hits = 3; | |
| const colorIndex = (row + col + this.level) % Object.keys(NEON_COLORS).length; | |
| const color = Object.values(NEON_COLORS)[colorIndex]; | |
| this.bricks.push(new Brick(x, y, brickWidth, BRICK_HEIGHT, hits, color)); | |
| } | |
| } | |
| } | |
| nextLevel() { | |
| this.level++; | |
| this.initLevel(); | |
| this.state = 'PLAYING'; | |
| } | |
| handleClick() { | |
| if (this.state === 'START' || this.state === 'GAME_OVER') { | |
| this.startGame(); | |
| return; | |
| } | |
| if (this.state === 'LEVEL_COMPLETE') { | |
| this.nextLevel(); | |
| return; | |
| } | |
| // Launch stuck balls | |
| this.balls.forEach(ball => { | |
| if (ball.stuck) { | |
| ball.launch(this.ballLaunchAngle); | |
| } | |
| }); | |
| } | |
| spawnParticles(x, y, color, count) { | |
| for (let i = 0; i < count; i++) { | |
| const angle = Math.random() * Math.PI * 2; | |
| const speed = Math.random() * 150 + 50; | |
| this.particles.push(new Particle( | |
| x, y, color, | |
| Math.cos(angle) * speed, | |
| Math.sin(angle) * speed, | |
| Math.random() * 0.5 + 0.2, | |
| Math.random() * 3 + 1 | |
| )); | |
| } | |
| } | |
| update(dt) { | |
| this.time += dt; | |
| // Update background | |
| this.bg.update(dt); | |
| // Update particles | |
| this.particles.forEach(p => p.update(dt)); | |
| this.particles = this.particles.filter(p => !p.isDead()); | |
| if (this.state === 'PLAYING') { | |
| // Update paddle | |
| const laserShot = this.paddle.update(dt, this.mouseX, this.keys); | |
| if (laserShot) { | |
| this.lasers.push(new LaserBullet(laserShot.x, laserShot.y)); | |
| audio.playLaserShot(); | |
| } | |
| // Update lasers | |
| this.lasers.forEach(l => l.update(dt)); | |
| this.lasers = this.lasers.filter(l => l.alive); | |
| // Laser-brick collision | |
| this.lasers.forEach(l => { | |
| this.bricks.forEach(brick => { | |
| if (!brick.alive) return; | |
| if (l.x > brick.x && l.x < brick.x + brick.width && | |
| l.y > brick.y && l.y < brick.y + brick.height) { | |
| l.alive = false; | |
| const destroyed = brick.hit(); | |
| if (destroyed) { | |
| brick.alive = false; | |
| this.score += 10 * brick.maxHits; | |
| audio.playLaserHit(); | |
| this.spawnParticles(brick.x + brick.width / 2, brick.y + brick.height / 2, brick.color, 8); | |
| if (brick.hasPowerUp) { | |
| this.powerUps.push(new PowerUpCapsule(brick.x + brick.width / 2, brick.y, brick.powerUpType)); | |
| } | |
| } | |
| } | |
| }); | |
| }); | |
| // Update balls | |
| const ballsToRemove = []; | |
| const powerUpDrops = []; | |
| this.balls.forEach((ball, bi) => { | |
| const result = ball.update(dt, this.paddle, this.bricks); | |
| if (result && result.destroyed) { | |
| const brick = result.brick; | |
| this.score += 10 * brick.maxHits; | |
| this.spawnParticles(brick.x + brick.width / 2, brick.y + brick.height / 2, brick.color, 12); | |
| if (brick.hasPowerUp) { | |
| powerUpDrops.push(new PowerUpCapsule(brick.x + brick.width / 2, brick.y, brick.powerUpType)); | |
| } | |
| } | |
| // Check if ball is lost | |
| if (ball.y > GAME_HEIGHT + 20) { | |
| ballsToRemove.push(bi); | |
| } | |
| }); | |
| // Apply power-up drops | |
| this.powerUps.push(...powerUpDrops); | |
| // Remove lost balls | |
| if (ballsToRemove.length > 0) { | |
| ballsToRemove.reverse().forEach(i => this.balls.splice(i, 1)); | |
| if (this.balls.length === 0) { | |
| this.lives--; | |
| audio.playLifeLost(); | |
| if (this.lives <= 0) { | |
| this.state = 'GAME_OVER'; | |
| this.gameOverTimer = 0; | |
| audio.playGameOver(); | |
| } else { | |
| // Reset with one ball | |
| const ball = new Ball(GAME_WIDTH / 2, GAME_HEIGHT - 60, 280); | |
| ball.stuck = true; | |
| this.balls.push(ball); | |
| } | |
| } | |
| } | |
| // Update power-ups | |
| this.powerUps.forEach(pu => pu.update(dt)); | |
| this.powerUps = this.powerUps.filter(pu => pu.alive); | |
| // Power-up collection | |
| this.powerUps.forEach(pu => { | |
| if (pu.x + pu.width / 2 > this.paddle.x && | |
| pu.x - pu.width / 2 < this.paddle.x + this.paddle.width && | |
| pu.y + pu.height / 2 > this.paddle.y && | |
| pu.y - pu.height / 2 < this.paddle.y + this.paddle.height) { | |
| pu.alive = false; | |
| audio.playPowerUpCollect(); | |
| this.spawnParticles(pu.x, pu.y, pu.type.color, 15); | |
| const extraLife = this.paddle.applyPowerUp(pu.type); | |
| if (extraLife === 'LIFE') { | |
| this.lives++; | |
| } | |
| // Multi-ball: split into 3 | |
| if (pu.type.name === 'SLOW' || pu.type.name === 'WIDE' || pu.type.name === 'STICKY' || pu.type.name === 'LASER') { | |
| // These are single-use, already handled by paddle | |
| } | |
| // Special multi-ball handling | |
| if (pu.type.name === 'MULTI' && this.balls.length < 6) { | |
| const existingBall = this.balls[0]; | |
| if (existingBall) { | |
| const b1 = new Ball(existingBall.x, existingBall.y, existingBall.speed); | |
| const b2 = new Ball(existingBall.x, existingBall.y, existingBall.speed); | |
| b1.launch(-Math.PI / 4); | |
| b2.launch(-3 * Math.PI / 4); | |
| this.balls.push(b1, b2); | |
| } | |
| } | |
| } | |
| }); | |
| // Check win condition | |
| if (this.bricks.every(b => !b.alive)) { | |
| this.state = 'LEVEL_COMPLETE'; | |
| this.levelCompleteTimer = 0; | |
| audio.playLevelComplete(); | |
| } | |
| } | |
| // Level complete timer | |
| if (this.state === 'LEVEL_COMPLETE') { | |
| this.levelCompleteTimer += dt; | |
| } | |
| // Game over timer | |
| if (this.state === 'GAME_OVER') { | |
| this.gameOverTimer += dt; | |
| } | |
| } | |
| draw() { | |
| // Clear and draw background | |
| this.bg.draw(ctx); | |
| // Draw bricks | |
| this.bricks.forEach(brick => brick.draw(ctx)); | |
| // Draw power-ups | |
| this.powerUps.forEach(pu => pu.draw(ctx)); | |
| // Draw lasers | |
| this.lasers.forEach(l => l.draw(ctx)); | |
| // Draw balls | |
| this.balls.forEach(ball => ball.draw(ctx)); | |
| // Draw paddle | |
| this.paddle.draw(ctx); | |
| // Draw particles | |
| this.particles.forEach(p => p.draw(ctx)); | |
| // Draw HUD | |
| this.drawHUD(); | |
| // Draw state screens | |
| if (this.state === 'START') this.drawStartScreen(); | |
| else if (this.state === 'GAME_OVER') this.drawGameOverScreen(); | |
| else if (this.state === 'LEVEL_COMPLETE') this.drawLevelCompleteScreen(); | |
| } | |
| drawHUD() { | |
| ctx.save(); | |
| // Score | |
| ctx.shadowBlur = 8; | |
| ctx.shadowColor = NEON_COLORS.cyan; | |
| ctx.fillStyle = NEON_COLORS.cyan; | |
| ctx.font = 'bold 18px Courier New'; | |
| ctx.textAlign = 'left'; | |
| ctx.textBaseline = 'top'; | |
| ctx.fillText(`SCORE: ${this.score}`, 10, 8); | |
| // Level | |
| ctx.fillStyle = NEON_COLORS.magenta; | |
| ctx.shadowColor = NEON_COLORS.magenta; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(`LEVEL ${this.level}`, GAME_WIDTH / 2, 8); | |
| // Lives | |
| ctx.fillStyle = NEON_COLORS.lime; | |
| ctx.shadowColor = NEON_COLORS.lime; | |
| ctx.textAlign = 'right'; | |
| let livesText = ''; | |
| for (let i = 0; i < this.lives; i++) livesText += '♥ '; | |
| ctx.fillText(livesText.trim(), GAME_WIDTH - 10, 8); | |
| // Active power-up indicators | |
| const effects = this.paddle.getActiveEffects(); | |
| if (effects.length > 0) { | |
| const startY = 30; | |
| effects.forEach((eff, i) => { | |
| const remaining = Math.max(0, eff.timer / 1000); | |
| ctx.fillStyle = eff.color; | |
| ctx.shadowColor = eff.color; | |
| ctx.font = '11px Courier New'; | |
| ctx.textAlign = 'left'; | |
| ctx.globalAlpha = 0.8; | |
| ctx.fillText(`${eff.name}: ${remaining.toFixed(1)}s`, 10, startY + i * 16); | |
| // Timer bar | |
| const barWidth = 60; | |
| const barHeight = 4; | |
| const maxTime = eff.name === 'MULTI' ? 999 : ( | |
| eff.name === 'WIDE' ? 10 : | |
| eff.name === 'SLOW' ? 8 : | |
| eff.name === 'STICKY' ? 12 : 7 | |
| ); | |
| const fill = Math.min(1, remaining / maxTime); | |
| ctx.fillStyle = eff.color; | |
| ctx.globalAlpha = 0.3; | |
| ctx.fillRect(10, startY + i * 16 + 8, barWidth, barHeight); | |
| ctx.globalAlpha = 0.8; | |
| ctx.fillRect(10, startY + i * 16 + 8, barWidth * fill, barHeight); | |
| }); | |
| } | |
| // Ball stuck indicator | |
| if (this.balls.some(b => b.stuck)) { | |
| ctx.globalAlpha = 0.5 + 0.3 * Math.sin(Date.now() / 300); | |
| ctx.fillStyle = NEON_COLORS.white; | |
| ctx.shadowColor = NEON_COLORS.white; | |
| ctx.font = '12px Courier New'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('CLICK TO LAUNCH', GAME_WIDTH / 2, GAME_HEIGHT - 80); | |
| } | |
| ctx.restore(); | |
| } | |
| drawStartScreen() { | |
| ctx.save(); | |
| // Darken overlay | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; | |
| ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); | |
| // Title | |
| const titlePulse = 0.8 + 0.2 * Math.sin(Date.now() / 400); | |
| ctx.shadowBlur = 20 * titlePulse; | |
| ctx.shadowColor = NEON_COLORS.cyan; | |
| ctx.fillStyle = NEON_COLORS.cyan; | |
| ctx.font = 'bold 48px Courier New'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.globalAlpha = titlePulse; | |
| ctx.fillText('NEON', GAME_WIDTH / 2, GAME_HEIGHT / 2 - 80); | |
| ctx.shadowColor = NEON_COLORS.magenta; | |
| ctx.fillStyle = NEON_COLORS.magenta; | |
| ctx.font = 'bold 56px Courier New'; | |
| ctx.fillText('ARCANOID', GAME_WIDTH / 2, GAME_HEIGHT / 2 - 25); | |
| // Subtitle | |
| ctx.shadowBlur = 6; | |
| ctx.shadowColor = NEON_COLORS.lime; | |
| ctx.fillStyle = NEON_COLORS.lime; | |
| ctx.font = '16px Courier New'; | |
| ctx.globalAlpha = 0.7 + 0.3 * Math.sin(Date.now() / 500); | |
| ctx.fillText('BREAK THE BRICKS. SURVIVE THE NEON.', GAME_WIDTH / 2, GAME_HEIGHT / 2 + 30); | |
| // Instructions | |
| ctx.shadowBlur = 4; | |
| ctx.shadowColor = NEON_COLORS.white; | |
| ctx.fillStyle = NEON_COLORS.white; | |
| ctx.font = '14px Courier New'; | |
| ctx.globalAlpha = 0.8; | |
| ctx.fillText('MOUSE / TOUCH / ARROWS to move', GAME_WIDTH / 2, GAME_HEIGHT / 2 + 80); | |
| ctx.fillText('CLICK / SPACE to launch ball', GAME_WIDTH / 2, GAME_HEIGHT / 2 + 105); | |
| // Start prompt | |
| const blink = Math.sin(Date.now() / 400) > 0; | |
| if (blink) { | |
| ctx.shadowBlur = 12; | |
| ctx.shadowColor = NEON_COLORS.hotPink; | |
| ctx.fillStyle = NEON_COLORS.hotPink; | |
| ctx.font = 'bold 20px Courier New'; | |
| ctx.globalAlpha = 1; | |
| ctx.fillText('CLICK OR PRESS SPACE TO START', GAME_WIDTH / 2, GAME_HEIGHT / 2 + 160); | |
| } | |
| ctx.restore(); | |
| } | |
| drawGameOverScreen() { | |
| ctx.save(); | |
| // Darken overlay | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.75)'; | |
| ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); | |
| // Game Over title | |
| const pulse = 0.7 + 0.3 * Math.sin(Date.now() / 300); | |
| ctx.shadowBlur = 20 * pulse; | |
| ctx.shadowColor = NEON_COLORS.red; | |
| ctx.fillStyle = NEON_COLORS.red; | |
| ctx.font = 'bold 44px Courier New'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.globalAlpha = pulse; | |
| ctx.fillText('GAME OVER', GAME_WIDTH / 2, GAME_HEIGHT / 2 - 60); | |
| // Final score | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = NEON_COLORS.cyan; | |
| ctx.fillStyle = NEON_COLORS.cyan; | |
| ctx.font = 'bold 24px Courier New'; | |
| ctx.globalAlpha = 1; | |
| ctx.fillText(`FINAL SCORE: ${this.score}`, GAME_WIDTH / 2, GAME_HEIGHT / 2); | |
| // Level reached | |
| ctx.shadowColor = NEON_COLORS.magenta; | |
| ctx.fillStyle = NEON_COLORS.magenta; | |
| ctx.font = '18px Courier New'; | |
| ctx.fillText(`LEVEL REACHED: ${this.level}`, GAME_WIDTH / 2, GAME_HEIGHT / 2 + 35); | |
| // Restart prompt | |
| const blink = Math.sin(Date.now() / 400) > 0; | |
| if (blink) { | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = NEON_COLORS.hotPink; | |
| ctx.fillStyle = NEON_COLORS.hotPink; | |
| ctx.font = 'bold 18px Courier New'; | |
| ctx.globalAlpha = 1; | |
| ctx.fillText('CLICK OR PRESS SPACE TO RESTART', GAME_WIDTH / 2, GAME_HEIGHT / 2 + 90); | |
| } | |
| ctx.restore(); | |
| } | |
| drawLevelCompleteScreen() { | |
| ctx.save(); | |
| // Darken overlay | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; | |
| ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); | |
| // Level complete title | |
| const pulse = 0.8 + 0.2 * Math.sin(Date.now() / 300); | |
| ctx.shadowBlur = 18 * pulse; | |
| ctx.shadowColor = NEON_COLORS.lime; | |
| ctx.fillStyle = NEON_COLORS.lime; | |
| ctx.font = 'bold 36px Courier New'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.globalAlpha = pulse; | |
| ctx.fillText(`LEVEL ${this.level} CLEAR!`, GAME_WIDTH / 2, GAME_HEIGHT / 2 - 50); | |
| // Score | |
| ctx.shadowBlur = 8; | |
| ctx.shadowColor = NEON_COLORS.cyan; | |
| ctx.fillStyle = NEON_COLORS.cyan; | |
| ctx.font = '20px Courier New'; | |
| ctx.globalAlpha = 1; | |
| ctx.fillText(`SCORE: ${this.score}`, GAME_WIDTH / 2, GAME_HEIGHT / 2); | |
| // Next level prompt | |
| const blink = Math.sin(Date.now() / 400) > 0; | |
| if (blink) { | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = NEON_COLORS.magenta; | |
| ctx.fillStyle = NEON_COLORS.magenta; | |
| ctx.font = 'bold 18px Courier New'; | |
| ctx.globalAlpha = 1; | |
| ctx.fillText('CLICK OR PRESS SPACE FOR NEXT LEVEL', GAME_WIDTH / 2, GAME_HEIGHT / 2 + 50); | |
| } | |
| ctx.restore(); | |
| } | |
| gameLoop(timestamp) { | |
| const dt = Math.min((timestamp - this.lastTime) / 1000, 0.05); // Cap at 50ms | |
| this.lastTime = timestamp; | |
| this.update(dt); | |
| this.draw(); | |
| requestAnimationFrame((t) => this.gameLoop(t)); | |
| } | |
| } | |
| // ============================================================ | |
| // INITIALIZE GAME | |
| // ============================================================ | |
| const game = new Game(); | |
| </script> | |
| </body> | |
| </html> | |
| ``` | |
| This is a complete, self-contained HTML file implementing the Neon Arcanoid game with all requested features: | |
| **Visual Features:** | |
| - Dark background with animated neon grid lines that slowly drift | |
| - 60+ floating neon particles that pulse and drift across the screen | |
| - All game elements rendered with neon glow effects using `shadowBlur` | |
| - Color-cycling palette with cyan, magenta, lime, electric blue, hot pink, and orange | |
| **Gameplay:** | |
| - Paddle controlled by mouse, touch, or arrow keys | |
| - Ball physics with proper wall/paddle/brick bouncing | |
| - 3 lives with ball reset on each loss | |
| - Multi-hit bricks (1-3 hits) shown with crack effects and hit counters | |
| - Progressive difficulty across levels (more rows, tougher bricks) | |
| - Score tracking displayed in the HUD | |
| **Audio (Web Audio API):** | |
| - Wall hits: short percussive blip | |
| - Paddle hits: sine wave blip | |
| - Brick destruction: ascending chime with sparkle | |
| - Power-up collection: rising arpeggio | |
| - Life lost: descending sad tones | |
| - Game over: dramatic low synth sweep | |
| - Level complete: ascending fanfare | |
| - Laser shots and hits | |
| **Power-Up System (6 types):** | |
| 1. **W** - Wide Paddle (10s) | |
| 2. **M** - Multi-Ball (splits into 3) | |
| 3. **S** - Slow Ball (8s) | |
| 4. **T** - Sticky Paddle (12s, click to launch) | |
| 5. **L** - Laser (7s, auto-fire) | |
| 6. **+** - Extra Life | |
| **Technical:** | |
| - Responsive canvas with black bars | |
| - 60fps game loop via `requestAnimationFrame` | |
| - All code in a single HTML file with no external dependencies | |
Xet Storage Details
- Size:
- 53.6 kB
- Xet hash:
- cfd48ea3cecf9604a1a00ef2264fa4dde27da8bf021d4cbcbdeeec37331bd215
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.