<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Atari Breakout - RALPH Edition</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
body {
background: #000000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
min-height: 100dvh;
font-family: 'Courier New', Courier, monospace;
color: #ffffff;
touch-action: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
#game-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
max-width: 100vw;
max-height: 100vh;
max-height: 100dvh;
}
#gameCanvas {
display: block;
background: #0a0a1a;
border: 3px solid #333333;
box-shadow:
0 0 30px rgba(0, 150, 255, 0.3),
inset 0 0 50px rgba(0, 0, 0, 0.5);
image-rendering: pixelated;
image-rendering: crisp-edges;
max-width: 100vw;
max-height: calc(100vh - 80px);
max-height: calc(100dvh - 80px);
}
#touch-controls {
display: none;
width: 100%;
max-width: 800px;
padding: 8px 12px;
gap: 12px;
justify-content: center;
align-items: center;
background: linear-gradient(to top, #0a0a0a, #111111);
border-top: 2px solid #333333;
z-index: 20;
}
#touch-controls.visible {
display: flex;
}
.touch-btn {
flex: 1;
max-width: 130px;
height: 54px;
background: linear-gradient(180deg, #2a2a4a 0%, #1a1a2a 100%);
border: 2px solid #4488ff;
border-radius: 10px;
color: #88ccff;
font-family: 'Courier New', Courier, monospace;
font-size: 16px;
font-weight: bold;
cursor: pointer;
touch-action: manipulation;
user-select: none;
-webkit-user-select: none;
text-shadow: 0 0 5px #4488ff;
transition: background 0.08s, transform 0.08s, box-shadow 0.08s;
box-shadow: 0 0 8px rgba(68, 136, 255, 0.25);
}
.touch-btn:active,
.touch-btn.active {
background: linear-gradient(180deg, #4488ff 0%, #2266cc 100%);
color: #ffffff;
transform: scale(0.95);
box-shadow: 0 0 18px rgba(68, 136, 255, 0.65);
}
.touch-btn.fire {
border-color: #ff4444;
color: #ff8888;
text-shadow: 0 0 5px #ff4444;
box-shadow: 0 0 8px rgba(255, 68, 68, 0.25);
max-width: 140px;
}
.touch-btn.fire:active,
.touch-btn.fire.active {
background: linear-gradient(180deg, #ff4444 0%, #cc2222 100%);
color: #ffffff;
box-shadow: 0 0 18px rgba(255, 68, 68, 0.65);
}
#audio-controls {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 8px;
align-items: center;
z-index: 30;
background: rgba(0, 0, 0, 0.55);
padding: 4px 8px;
border-radius: 6px;
border: 1px solid #333;
}
#mute-btn {
background: #1a1a2a;
border: 1px solid #4488ff;
color: #88ccff;
width: 36px;
height: 36px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
font-family: inherit;
}
#mute-btn:hover {
background: #2a2a4a;
}
#vol-slider {
width: 80px;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: #333333;
border-radius: 3px;
outline: none;
cursor: pointer;
}
#vol-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: #4488ff;
border-radius: 50%;
cursor: pointer;
}
#vol-slider::-moz-range-thumb {
width: 14px;
height: 14px;
background: #4488ff;
border-radius: 50%;
cursor: pointer;
border: none;
}
@media (max-width: 850px) {
#gameCanvas {
border-width: 2px;
}
#touch-controls {
display: flex;
}
#vol-slider {
width: 60px;
}
}
@media (max-width: 600px) {
#gameCanvas {
max-height: calc(100vh - 100px);
max-height: calc(100dvh - 100px);
border-width: 1px;
}
.touch-btn {
height: 48px;
font-size: 14px;
max-width: 110px;
}
#audio-controls {
top: 4px;
right: 4px;
padding: 2px 6px;
}
#mute-btn {
width: 30px;
height: 30px;
font-size: 14px;
}
#vol-slider {
width: 50px;
}
}
@media (orientation: landscape) and (max-height: 500px) {
#gameCanvas {
max-height: calc(100vh - 64px);
max-height: calc(100dvh - 64px);
}
#touch-controls {
padding: 4px 10px;
gap: 8px;
}
.touch-btn {
height: 40px;
font-size: 13px;
max-width: 100px;
border-radius: 6px;
}
#audio-controls {
top: 2px;
right: 2px;
}
}
@media (orientation: landscape) and (max-height: 360px) {
#gameCanvas {
max-height: calc(100vh - 50px);
max-height: calc(100dvh - 50px);
}
.touch-btn {
height: 34px;
font-size: 12px;
}
}
</style>
</head>
<body>
<div id="game-container">
<div id="audio-controls">
<button id="mute-btn" title="Mute/Unmute">🔊</button>
<input type="range" id="vol-slider" min="0" max="100" value="50" title="Volume">
</div>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div id="touch-controls">
<button class="touch-btn" id="btn-left" aria-label="Move Left">◀ LEFT</button>
<button class="touch-btn fire" id="btn-fire" aria-label="Fire">🔥 FIRE</button>
<button class="touch-btn" id="btn-right" aria-label="Move Right">RIGHT ▶</button>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const BASE_W = 800, BASE_H = 600;
const STATE = { MENU: 0, PLAYING: 1, PAUSED: 2, GAME_OVER: 3, LEVEL_COMPLETE: 4 };
let gameState = STATE.MENU;
let score = 0;
let lives = 3;
let level = 1;
let highScore = 0;
let scoreMilestone = 2500;
let nextLifeAt = 2500;
let frameCount = 0;
let lastTime = 0;
let deltaTime = 0;
const keys = {};
let mouseX = BASE_W / 2;
let mouseActive = false;
let touchLeft = false, touchRight = false, touchFire = false;
let isMobile = false;
let audioCtx = null;
let masterGain = null;
let musicGain = null;
let sfxGain = null;
let muted = false;
let volume = 0.5;
let musicPlaying = false;
let musicNodes = [];
let musicTimeout = null;
const paddle = {
x: BASE_W / 2 - 50,
y: BASE_H - 40,
width: 100,
baseWidth: 100,
height: 14,
speed: 8,
color: '#4488ff',
animating: false,
animT: 0,
rotY: 0,
scaleX: 1,
scaleY: 1,
glow: 0,
wings: false
};
let balls = [];
const BALL_MIN_SPEED = 3;
const BALL_MAX_SPEED = 8;
const BALL_RADIUS = 7;
function createBall(x, y, vx, vy) {
return {
x: x || BASE_W / 2,
y: y || BASE_H - 60,
vx: vx || (Math.random() > 0.5 ? 4 : -4),
vy: vy || -5,
radius: BALL_RADIUS,
color: '#ffffff',
active: true,
lava: false,
lavaTimer: 0,
bomb: false,
bombHits: 0,
bombMax: 5,
grown: false,
trail: []
};
}
const BRICK_COLS = 11;
const BRICK_ROWS = 5;
const BRICK_W = 68;
const BRICK_H = 22;
const BRICK_PAD = 4;
const BRICK_OFFSET_TOP = 60;
const BRICK_OFFSET_LEFT = 12;
const HP_COLORS = {
1: '#ffee44', 2: '#44cc44', 3: '#4488ff', 4: '#ff8844', 5: '#ff4444' };
let bricks = [];
let bricksRemaining = 0;
const POWERUP_TYPES = {
PADDLE: { id: 'paddle', color: '#44ff88', label: 'WIDE', score: 100 },
SPEED: { id: 'speed', color: '#44aaff', label: 'SLOW', score: 150 },
PROJECTILE: { id: 'projectile', color: '#ffaa00', label: 'MISS', score: 200 },
LAVA: { id: 'lava', color: '#ff4400', label: 'LAVA', score: 250 },
BOMB: { id: 'bomb', color: '#222222', label: 'BOMB', score: 300 },
GROWTH: { id: 'growth', color: '#ffffff', label: 'GROW', score: 200 },
MUSHROOM: { id: 'mushroom', color: '#ff2266', label: '1UP!', score: 500 },
MULTIBALL: { id: 'multiball', color: '#aa44ff', label: 'MULTI', score: 400 }
};
let powerups = [];
let activePowerups = {
paddleExtend: false,
paddleTimer: 0,
speedBoost: false,
speedTimer: 0,
projectile: false,
projectileShots: 0,
lava: false,
lavaTimer: 0,
bomb: false,
growth: false
};
let projectiles = [];
const PROJ_SPEED = 10;
const PROJ_W = 4;
const PROJ_H = 12;
let particles = [];
let floatingTexts = [];
const PATTERNS = [
'classic', 'pyramid', 'diamond', 'checkerboard', 'stripe',
'wave', 'spiral', 'hourglass', 'cross', 'rings',
'random', 'walls', 'zigzag', 'fortress', 'scatter'
];
function seededRandom(seed) {
let s = seed;
return function() {
s = (s * 16807 + 0) % 2147483647;
return (s - 1) / 2147483646;
};
}
function initAudio() {
if (audioCtx) return;
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = audioCtx.createGain();
masterGain.gain.value = volume;
masterGain.connect(audioCtx.destination);
musicGain = audioCtx.createGain();
musicGain.gain.value = 0.28;
musicGain.connect(masterGain);
sfxGain = audioCtx.createGain();
sfxGain.gain.value = 0.55;
sfxGain.connect(masterGain);
} catch(e) { console.warn('Audio init failed', e); }
}
function setVolume(v) {
volume = v;
if (masterGain) masterGain.gain.value = muted ? 0 : volume;
}
function toggleMute() {
muted = !muted;
if (masterGain) masterGain.gain.value = muted ? 0 : volume;
document.getElementById('mute-btn').textContent = muted ? '🔇' : '🔊';
}
function playTone(freq, duration, type, gainVal, dest, slideTo) {
if (!audioCtx || muted) return;
try {
if (audioCtx.state === 'suspended') audioCtx.resume();
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
osc.type = type || 'square';
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
if (slideTo) {
osc.frequency.exponentialRampToValueAtTime(slideTo, audioCtx.currentTime + duration * 0.8);
}
const now = audioCtx.currentTime;
g.gain.setValueAtTime(0, now);
g.gain.linearRampToValueAtTime(gainVal || 0.15, now + 0.005);
g.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(g);
g.connect(dest || sfxGain);
osc.start(now);
osc.stop(now + duration + 0.01);
} catch(e) {}
}
function playDualTone(freq1, freq2, duration, type, gainVal) {
playTone(freq1, duration, type || 'square', (gainVal || 0.12) * 0.7);
playTone(freq2, duration * 0.85, type || 'square', (gainVal || 0.12) * 0.5);
}
function sfxBrick() {
const base = 880 + Math.random() * 440;
playTone(base, 0.07, 'square', 0.14);
playTone(base * 1.5, 0.045, 'square', 0.08);
}
function sfxPaddle() {
playTone(180, 0.15, 'triangle', 0.22);
playTone(120, 0.2, 'square', 0.1, null, 80);
}
function sfxWall() {
playTone(90, 0.05, 'square', 0.12);
playTone(140, 0.03, 'square', 0.06);
}
function sfxPowerup() {
const notes = [392, 523, 659, 784];
notes.forEach((f, i) => {
setTimeout(() => playTone(f, 0.1, 'square', 0.14), i * 70);
});
}
function sfxLife() {
[523, 659, 784, 1047].forEach((f, i) => {
setTimeout(() => playDualTone(f, f * 1.5, 0.18, 'square', 0.16), i * 110);
});
}
function sfxLevelComplete() {
const fanfare = [
[262, 0.2], [330, 0.2], [392, 0.2], [523, 0.35],
[392, 0.15], [523, 0.15], [659, 0.2], [784, 0.4]
];
let t = 0;
fanfare.forEach(([f, d]) => {
setTimeout(() => {
playTone(f, d, 'square', 0.14);
playTone(f / 2, d, 'triangle', 0.08); }, t * 1000);
t += d * 0.85;
});
}
function sfxGameOver() {
[440, 392, 349, 294, 247, 196, 147].forEach((f, i) => {
setTimeout(() => playTone(f, 0.28, 'sawtooth', 0.1), i * 180);
});
}
function sfxShoot() {
playTone(700, 0.04, 'square', 0.1, null, 300);
playTone(450, 0.06, 'triangle', 0.07);
}
function sfx1UP() {
const notes = [523, 659, 784, 1047, 784, 1047, 1319];
notes.forEach((f, i) => {
setTimeout(() => playDualTone(f, f * 2, 0.12, 'square', 0.18), i * 75);
});
}
function sfxMultiball() {
for (let i = 0; i < 6; i++) {
setTimeout(() => playTone(300 + i * 100, 0.08, 'square', 0.1), i * 40);
}
}
function sfxBomb() {
playTone(80, 0.3, 'sawtooth', 0.2, null, 40);
playTone(150, 0.15, 'square', 0.12);
setTimeout(() => playTone(60, 0.25, 'triangle', 0.15), 50);
}
const MELODY = [
[262, 180], [294, 180], [330, 180], [349, 180],
[392, 270], [349, 90], [330, 180], [294, 180],
[262, 180], [294, 180], [330, 180], [392, 360],
[330, 180], [262, 360],
[294, 180], [330, 180], [349, 180], [392, 180],
[440, 270], [392, 90], [349, 180], [330, 180],
[294, 180], [330, 180], [349, 180], [440, 360],
[349, 180], [294, 360],
[330, 135], [349, 135], [392, 135], [440, 135],
[494, 270], [440, 90], [392, 180], [349, 180],
[330, 180], [294, 180], [262, 540],
[392, 135], [440, 135], [494, 135], [523, 360],
[494, 135], [440, 135], [392, 360],
[330, 180], [262, 540],
[294, 180], [330, 180], [392, 180], [330, 180],
[349, 180], [392, 180], [440, 360],
[392, 180], [330, 180], [262, 720]
];
const BASS = [
[131, 360], [131, 360], [165, 360], [165, 360],
[175, 360], [175, 360], [131, 360], [131, 360],
[147, 360], [147, 360], [175, 360], [175, 360],
[196, 360], [196, 360], [147, 360], [147, 360],
[165, 360], [175, 360], [196, 360], [220, 360],
[247, 360], [220, 360], [196, 360], [165, 360],
[131, 720],
[147, 360], [165, 360], [196, 360], [165, 360],
[175, 360], [196, 360], [220, 360],
[196, 360], [165, 360], [131, 720]
];
const ARPEGGIO = [
[523, 90], [0, 90], [659, 90], [0, 90],
[784, 90], [0, 90], [659, 90], [0, 90],
[523, 90], [0, 90], [659, 90], [0, 90],
[784, 180], [0, 180],
[0, 720], [587, 90], [0, 90], [698, 90], [0, 90],
[880, 90], [0, 90], [698, 90], [0, 90],
[587, 90], [0, 90], [698, 90], [0, 90],
[880, 180], [0, 180],
[0, 1440]
];
let melodyIdx = 0, bassIdx = 0, arpIdx = 0;
let musicTimeout = null;
let bassTimeout = null;
let arpTimeout = null;
let lastBassTime = 0;
function startMusic() {
if (!audioCtx || musicPlaying) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
musicPlaying = true;
melodyIdx = 0; bassIdx = 0; arpIdx = 0;
scheduleMelody();
scheduleBass();
scheduleArpeggio();
}
function scheduleMelody() {
if (!musicPlaying || !audioCtx) return;
if (melodyIdx >= MELODY.length) {
melodyIdx = 0; }
const [freq, dur] = MELODY[melodyIdx];
melodyIdx++;
try {
if (!muted && freq > 0) {
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
osc.type = 'square';
osc.frequency.value = freq;
g.gain.setValueAtTime(0, now);
g.gain.linearRampToValueAtTime(0.07, now + 0.01);
g.gain.setValueAtTime(0.07, now + dur / 1000 * 0.6);
g.gain.exponentialRampToValueAtTime(0.001, now + dur / 1000 * 0.95);
osc.connect(g);
g.connect(musicGain);
osc.start(now);
osc.stop(now + dur / 1000);
musicNodes.push(osc);
}
} catch(e) {}
musicTimeout = setTimeout(scheduleMelody, dur);
}
function scheduleBass() {
if (!musicPlaying || !audioCtx) return;
if (bassIdx >= BASS.length) {
bassIdx = 0;
}
const [freq, dur] = BASS[bassIdx];
bassIdx++;
try {
if (!muted && freq > 0) {
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.value = freq;
g.gain.setValueAtTime(0, now);
g.gain.linearRampToValueAtTime(0.09, now + 0.02);
g.gain.setValueAtTime(0.09, now + dur / 1000 * 0.5);
g.gain.exponentialRampToValueAtTime(0.001, now + dur / 1000 * 0.9);
osc.connect(g);
g.connect(musicGain);
osc.start(now);
osc.stop(now + dur / 1000);
}
} catch(e) {}
bassTimeout = setTimeout(scheduleBass, dur);
}
function scheduleArpeggio() {
if (!musicPlaying || !audioCtx) return;
if (arpIdx >= ARPEGGIO.length) {
arpIdx = 0;
}
const [freq, dur] = ARPEGGIO[arpIdx];
arpIdx++;
try {
if (!muted && freq > 0) {
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
osc.type = 'square';
osc.frequency.value = freq;
g.gain.setValueAtTime(0, now);
g.gain.linearRampToValueAtTime(0.03, now + 0.005);
g.gain.exponentialRampToValueAtTime(0.001, now + dur / 1000 * 0.8);
osc.connect(g);
g.connect(musicGain);
osc.start(now);
osc.stop(now + dur / 1000);
}
} catch(e) {}
arpTimeout = setTimeout(scheduleArpeggio, dur);
}
function stopMusic() {
musicPlaying = false;
if (musicTimeout) { clearTimeout(musicTimeout); musicTimeout = null; }
if (bassTimeout) { clearTimeout(bassTimeout); bassTimeout = null; }
if (arpTimeout) { clearTimeout(arpTimeout); arpTimeout = null; }
musicNodes.forEach(n => { try { n.stop(); } catch(e) {} });
musicNodes = [];
}
function pauseMusic() {
musicPlaying = false;
if (musicTimeout) { clearTimeout(musicTimeout); musicTimeout = null; }
if (bassTimeout) { clearTimeout(bassTimeout); bassTimeout = null; }
if (arpTimeout) { clearTimeout(arpTimeout); arpTimeout = null; }
}
function resumeMusic() {
if (!musicPlaying && gameState === STATE.PLAYING) {
musicPlaying = true;
scheduleMelody();
scheduleBass();
scheduleArpeggio();
}
}
function resizeCanvas() {
const touchEl = document.getElementById('touch-controls');
const touchVisible = !!(touchEl && (touchEl.classList.contains('visible') || window.innerWidth <= 850 || isMobile));
const touchH = touchVisible ? (touchEl.offsetHeight || 70) : 0;
const maxW = window.innerWidth - 4;
const maxH = window.innerHeight - touchH - 8;
let scale = Math.min(maxW / BASE_W, maxH / BASE_H, 1);
if (scale < 0.25) scale = 0.25; canvas.style.width = Math.floor(BASE_W * scale) + 'px';
canvas.style.height = Math.floor(BASE_H * scale) + 'px';
if (canvas.width !== BASE_W) canvas.width = BASE_W;
if (canvas.height !== BASE_H) canvas.height = BASE_H;
}
function setupInput() {
window.addEventListener('keydown', e => {
keys[e.key] = true;
keys[e.code] = true;
if (e.key === ' ' || e.code === 'Space') {
e.preventDefault();
handleSpace();
}
if (e.key === 'p' || e.key === 'P' || e.key === 'Escape') {
if (gameState === STATE.PLAYING) {
gameState = STATE.PAUSED;
pauseMusic();
} else if (gameState === STATE.PAUSED) {
gameState = STATE.PLAYING;
resumeMusic();
}
}
if ((e.key === 'Enter' || e.key === ' ') && (gameState === STATE.MENU || gameState === STATE.GAME_OVER)) {
startGame();
}
if (e.key === 'Enter' && gameState === STATE.LEVEL_COMPLETE) {
nextLevel();
}
});
window.addEventListener('keyup', e => {
keys[e.key] = false;
keys[e.code] = false;
});
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = BASE_W / rect.width;
mouseX = (e.clientX - rect.left) * scaleX;
mouseActive = true;
});
canvas.addEventListener('mouseleave', () => { mouseActive = false; });
canvas.addEventListener('click', () => {
if (gameState === STATE.MENU || gameState === STATE.GAME_OVER) startGame();
else if (gameState === STATE.LEVEL_COMPLETE) nextLevel();
else if (gameState === STATE.PLAYING) {
initAudio();
if (activePowerups.projectile && activePowerups.projectileShots > 0) fireProjectile();
}
});
canvas.addEventListener('touchstart', handleTouch, { passive: false });
canvas.addEventListener('touchmove', handleTouch, { passive: false });
canvas.addEventListener('touchend', e => { e.preventDefault(); });
const btnL = document.getElementById('btn-left');
const btnR = document.getElementById('btn-right');
const btnF = document.getElementById('btn-fire');
const bindTouch = (el, flag) => {
el.addEventListener('touchstart', e => { e.preventDefault(); window[flag] = true; el.classList.add('active'); }, { passive: false });
el.addEventListener('touchend', e => { e.preventDefault(); window[flag] = false; el.classList.remove('active'); });
el.addEventListener('mousedown', e => { window[flag] = true; el.classList.add('active'); });
el.addEventListener('mouseup', e => { window[flag] = false; el.classList.remove('active'); });
el.addEventListener('mouseleave', e => { window[flag] = false; el.classList.remove('active'); });
};
btnL.addEventListener('touchstart', e => { e.preventDefault(); touchLeft = true; btnL.classList.add('active'); }, { passive: false });
btnL.addEventListener('touchend', e => { e.preventDefault(); touchLeft = false; btnL.classList.remove('active'); });
btnL.addEventListener('mousedown', () => { touchLeft = true; btnL.classList.add('active'); });
btnL.addEventListener('mouseup', () => { touchLeft = false; btnL.classList.remove('active'); });
btnL.addEventListener('mouseleave', () => { touchLeft = false; btnL.classList.remove('active'); });
btnR.addEventListener('touchstart', e => { e.preventDefault(); touchRight = true; btnR.classList.add('active'); }, { passive: false });
btnR.addEventListener('touchend', e => { e.preventDefault(); touchRight = false; btnR.classList.remove('active'); });
btnR.addEventListener('mousedown', () => { touchRight = true; btnR.classList.add('active'); });
btnR.addEventListener('mouseup', () => { touchRight = false; btnR.classList.remove('active'); });
btnR.addEventListener('mouseleave', () => { touchRight = false; btnR.classList.remove('active'); });
btnF.addEventListener('touchstart', e => { e.preventDefault(); touchFire = true; btnF.classList.add('active'); handleSpace(); }, { passive: false });
btnF.addEventListener('touchend', e => { e.preventDefault(); touchFire = false; btnF.classList.remove('active'); });
btnF.addEventListener('mousedown', () => { touchFire = true; btnF.classList.add('active'); handleSpace(); });
btnF.addEventListener('mouseup', () => { touchFire = false; btnF.classList.remove('active'); });
isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent) || window.innerWidth <= 850;
if (isMobile) document.getElementById('touch-controls').classList.add('visible');
document.getElementById('mute-btn').addEventListener('click', () => { initAudio(); toggleMute(); });
document.getElementById('vol-slider').addEventListener('input', e => {
initAudio();
setVolume(parseInt(e.target.value) / 100);
});
window.addEventListener('resize', () => {
isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent) || ('ontouchstart' in window) || window.innerWidth <= 850;
document.getElementById('touch-controls').classList.toggle('visible', isMobile || window.innerWidth <= 850);
resizeCanvas();
});
window.addEventListener('orientationchange', () => {
setTimeout(() => {
isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent) || ('ontouchstart' in window) || window.innerWidth <= 850;
document.getElementById('touch-controls').classList.toggle('visible', isMobile || window.innerWidth <= 850);
resizeCanvas();
}, 150);
});
}
function handleTouch(e) {
e.preventDefault();
if (e.touches.length > 0) {
const rect = canvas.getBoundingClientRect();
const scaleX = BASE_W / rect.width;
mouseX = (e.touches[0].clientX - rect.left) * scaleX;
mouseActive = true;
}
if (gameState === STATE.MENU || gameState === STATE.GAME_OVER) startGame();
else if (gameState === STATE.LEVEL_COMPLETE) nextLevel();
}
function handleSpace() {
initAudio();
if (gameState === STATE.MENU || gameState === STATE.GAME_OVER) {
startGame();
} else if (gameState === STATE.LEVEL_COMPLETE) {
nextLevel();
} else if (gameState === STATE.PLAYING) {
if (activePowerups.projectile && activePowerups.projectileShots > 0) {
fireProjectile();
}
} else if (gameState === STATE.PAUSED) {
gameState = STATE.PLAYING;
resumeMusic();
}
}
function getMaxHPForLevel(lvl) {
return Math.min(5, 1 + Math.floor((lvl - 1) / 2));
}
function getPatternForLevel(lvl) {
if (lvl === 1) return 'classic';
return PATTERNS[(lvl - 1) % PATTERNS.length];
}
function createBricks(lvl) {
bricks = [];
bricksRemaining = 0;
const rng = seededRandom(lvl * 12345 + 67890);
const pattern = getPatternForLevel(lvl);
const maxHP = getMaxHPForLevel(lvl);
const totalW = BRICK_COLS * (BRICK_W + BRICK_PAD) - BRICK_PAD;
const startX = (BASE_W - totalW) / 2;
for (let row = 0; row < BRICK_ROWS; row++) {
for (let col = 0; col < BRICK_COLS; col++) {
let active = true;
let hp = 1;
switch (pattern) {
case 'classic':
active = true;
break;
case 'pyramid':
active = col >= row && col < BRICK_COLS - row;
break;
case 'diamond': {
const midR = Math.floor(BRICK_ROWS / 2);
const midC = Math.floor(BRICK_COLS / 2);
const dist = Math.abs(row - midR) + Math.abs(col - midC);
active = dist <= midC - 1;
break;
}
case 'checkerboard':
active = (row + col) % 2 === 0;
break;
case 'stripe':
active = col % 2 === 0;
break;
case 'wave':
active = Math.abs(col - Math.floor(BRICK_COLS / 2) - Math.sin(row * 1.2) * 3) < 4;
break;
case 'spiral': {
const midR = BRICK_ROWS / 2, midC = BRICK_COLS / 2;
const angle = Math.atan2(row - midR, col - midC);
const dist = Math.sqrt((row - midR) ** 2 + (col - midC) ** 2);
active = Math.sin(angle * 3 + dist * 2) > 0;
break;
}
case 'hourglass':
active = col >= row && col < BRICK_COLS - row ||
col >= BRICK_ROWS - 1 - row && col < BRICK_COLS - (BRICK_ROWS - 1 - row);
break;
case 'cross': {
const midC = Math.floor(BRICK_COLS / 2);
const midR = Math.floor(BRICK_ROWS / 2);
active = col === midC || col === midC - 1 || col === midC + 1 ||
row === midR || row === midR - 1;
break;
}
case 'rings': {
const midR = BRICK_ROWS / 2, midC = BRICK_COLS / 2;
const dist = Math.sqrt((row - midR) ** 2 + (col - midC) ** 2);
active = Math.abs(dist - 1.5) < 0.8 || Math.abs(dist - 3.5) < 0.8 || dist < 0.5;
break;
}
case 'walls':
active = col === 0 || col === BRICK_COLS - 1 || row === 0 ||
(row === BRICK_ROWS - 1 && col > 2 && col < BRICK_COLS - 3);
break;
case 'zigzag':
active = (row % 2 === 0) ? col % 3 !== 2 : col % 3 !== 0;
break;
case 'fortress':
active = row === 0 || row === BRICK_ROWS - 1 ||
col === 0 || col === BRICK_COLS - 1 ||
(row === 2 && col > 2 && col < BRICK_COLS - 3);
break;
case 'scatter':
active = rng() > 0.35;
break;
case 'random':
default:
active = rng() > 0.25;
break;
}
if (!active) continue;
const rowFactor = (BRICK_ROWS - row) / BRICK_ROWS; const baseHP = Math.ceil(rowFactor * maxHP);
hp = Math.max(1, Math.min(5, baseHP + (rng() > 0.7 ? 1 : 0)));
if (lvl > 5 && rng() < 0.2) hp = Math.min(5, hp + 1);
const brick = {
x: startX + col * (BRICK_W + BRICK_PAD),
y: BRICK_OFFSET_TOP + row * (BRICK_H + BRICK_PAD),
w: BRICK_W,
h: BRICK_H,
hp: hp,
maxHP: hp,
col: col,
row: row,
alive: true,
hasPowerup: rng() < Math.max(0.08, 0.22 - lvl * 0.01)
};
bricks.push(brick);
bricksRemaining++;
}
}
if (bricksRemaining < 5) {
for (let i = 0; i < 8; i++) {
const col = i % BRICK_COLS;
const row = Math.floor(i / BRICK_COLS);
const startX2 = (BASE_W - (BRICK_COLS * (BRICK_W + BRICK_PAD) - BRICK_PAD)) / 2;
bricks.push({
x: startX2 + col * (BRICK_W + BRICK_PAD),
y: BRICK_OFFSET_TOP + row * (BRICK_H + BRICK_PAD),
w: BRICK_W, h: BRICK_H,
hp: 1, maxHP: 1, col, row, alive: true, hasPowerup: false
});
bricksRemaining++;
}
}
}
function spawnPowerup(x, y) {
const types = Object.values(POWERUP_TYPES);
const weights = [25, 20, 15, 10, 8, 8, 5, 9]; let total = weights.reduce((a, b) => a + b, 0);
let r = Math.random() * total;
let idx = 0;
for (let i = 0; i < weights.length; i++) {
r -= weights[i];
if (r <= 0) { idx = i; break; }
}
const type = types[idx];
powerups.push({
x: x,
y: y,
w: 28,
h: 14,
vy: 2 + Math.random(),
type: type,
active: true,
bob: Math.random() * Math.PI * 2
});
}
function applyPowerup(type) {
sfxPowerup();
addScore(type.score);
spawnFloatingText(paddle.x + paddle.width / 2, paddle.y - 20, type.label, type.color);
switch (type.id) {
case 'paddle':
activePowerups.paddleExtend = true;
activePowerups.paddleTimer = 30 * 60; paddle.width = paddle.baseWidth * 1.5;
break;
case 'speed':
activePowerups.speedBoost = true;
activePowerups.speedTimer = 30 * 60;
balls.forEach(b => {
b.vx *= 0.5;
b.vy *= 0.5;
clampBallSpeed(b);
});
break;
case 'projectile':
activePowerups.projectile = true;
activePowerups.projectileShots = 100;
paddle.wings = true;
break;
case 'lava':
activePowerups.lava = true;
activePowerups.lavaTimer = 30 * 60;
balls.forEach(b => { b.lava = true; b.lavaTimer = 30 * 60; b.color = '#ff6600'; });
break;
case 'bomb':
activePowerups.bomb = true;
balls.forEach(b => { b.bomb = true; b.bombHits = 0; b.color = '#333333'; });
break;
case 'growth':
activePowerups.growth = true;
balls.forEach(b => { b.grown = true; b.radius = BALL_RADIUS * 1.5; });
break;
case 'mushroom':
lives++;
sfx1UP();
spawnFloatingText(BASE_W / 2, BASE_H / 2, '1UP!', '#ff2266', 2);
spawnParticles(paddle.x + paddle.width / 2, paddle.y, '#ff2266', 20, 'burst');
break;
case 'multiball':
spawnMultiball();
break;
}
}
function spawnMultiball() {
sfxMultiball();
const baseX = paddle.x + paddle.width / 2;
const baseY = paddle.y - 20;
const existingSpeed = balls.length > 0 ?
Math.sqrt(balls[0].vx ** 2 + balls[0].vy ** 2) : 5;
const newSpeed = existingSpeed * 0.5;
for (let i = 0; i < 10; i++) {
const angle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI * 0.8;
const ball = createBall(
baseX + (Math.random() - 0.5) * 40,
baseY,
Math.cos(angle) * newSpeed,
Math.sin(angle) * newSpeed
);
if (activePowerups.lava) { ball.lava = true; ball.lavaTimer = activePowerups.lavaTimer; ball.color = '#ff6600'; }
if (activePowerups.bomb) { ball.bomb = true; ball.color = '#333333'; }
if (activePowerups.growth) { ball.grown = true; ball.radius = BALL_RADIUS * 1.5; }
balls.push(ball);
}
spawnParticles(baseX, baseY, '#aa44ff', 30, 'burst');
}
function updatePowerups() {
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
if (!p.active) { powerups.splice(i, 1); continue; }
p.y += p.vy;
p.bob += 0.1;
p.x += Math.sin(p.bob) * 0.5;
if (p.y + p.h > paddle.y && p.y < paddle.y + paddle.height &&
p.x + p.w > paddle.x && p.x < paddle.x + paddle.width) {
p.active = false;
applyPowerup(p.type);
powerups.splice(i, 1);
continue;
}
if (p.y > BASE_H) {
powerups.splice(i, 1);
}
}
if (activePowerups.paddleExtend) {
activePowerups.paddleTimer--;
if (activePowerups.paddleTimer <= 0) {
activePowerups.paddleExtend = false;
paddle.width = paddle.baseWidth;
if (paddle.x + paddle.width > BASE_W) paddle.x = BASE_W - paddle.width;
}
}
if (activePowerups.speedBoost) {
activePowerups.speedTimer--;
if (activePowerups.speedTimer <= 0) {
activePowerups.speedBoost = false;
balls.forEach(b => {
b.vx *= 2;
b.vy *= 2;
clampBallSpeed(b);
});
}
}
if (activePowerups.lava) {
activePowerups.lavaTimer--;
balls.forEach(b => {
if (b.lava) {
b.lavaTimer--;
if (b.lavaTimer <= 0) {
b.lava = false;
b.color = '#ffffff';
}
}
});
if (activePowerups.lavaTimer <= 0) {
activePowerups.lava = false;
balls.forEach(b => { b.lava = false; b.color = b.bomb ? '#333333' : '#ffffff'; });
}
}
if (activePowerups.projectile && activePowerups.projectileShots <= 0) {
activePowerups.projectile = false;
paddle.wings = false;
}
}
function fireProjectile() {
if (!activePowerups.projectile || activePowerups.projectileShots <= 0) return;
activePowerups.projectileShots--;
sfxShoot();
const leftX = paddle.x + 10;
const rightX = paddle.x + paddle.width - 10;
const y = paddle.y;
projectiles.push({
x: leftX - PROJ_W / 2, y: y, w: PROJ_W, h: PROJ_H,
vy: -PROJ_SPEED, active: true, trail: []
});
projectiles.push({
x: rightX - PROJ_W / 2, y: y, w: PROJ_W, h: PROJ_H,
vy: -PROJ_SPEED, active: true, trail: []
});
}
function updateProjectiles() {
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
if (!p.active) { projectiles.splice(i, 1); continue; }
p.trail.push({ x: p.x + p.w / 2, y: p.y + p.h, life: 15 });
if (p.trail.length > 12) p.trail.shift();
p.trail.forEach(t => t.life--);
p.y += p.vy;
if (p.y + p.h < 0) {
projectiles.splice(i, 1);
continue;
}
for (const brick of bricks) {
if (!brick.alive) continue;
if (p.x < brick.x + brick.w && p.x + p.w > brick.x &&
p.y < brick.y + brick.h && p.y + p.h > brick.y) {
hitBrick(brick, 1);
break;
}
}
}
}
function spawnParticles(x, y, color, count, type) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = type === 'jet' ? 2 + Math.random() * 4 : 1 + Math.random() * 5;
particles.push({
x, y,
vx: type === 'jet' ? (Math.random() - 0.5) * 3 : Math.cos(angle) * speed,
vy: type === 'jet' ? -speed : Math.sin(angle) * speed,
life: type === 'jet' ? 30 + Math.random() * 20 : 20 + Math.random() * 25,
maxLife: 40,
color: color,
size: 2 + Math.random() * 3,
gravity: type === 'jet' ? 0.05 : 0.1,
type: type || 'fire'
});
}
}
function spawnFloatingText(x, y, text, color, scale) {
floatingTexts.push({
x, y, text, color: color || '#fff',
life: 60,
maxLife: 60,
vy: -1.5,
scale: scale || 1
});
}
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += p.gravity;
p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
for (let i = floatingTexts.length - 1; i >= 0; i--) {
const t = floatingTexts[i];
t.y += t.vy;
t.life--;
if (t.life <= 0) floatingTexts.splice(i, 1);
}
}
function clampBallSpeed(ball) {
const spd = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
if (spd < BALL_MIN_SPEED) {
const s = BALL_MIN_SPEED / (spd || 1);
ball.vx *= s;
ball.vy *= s;
} else if (spd > BALL_MAX_SPEED) {
const s = BALL_MAX_SPEED / spd;
ball.vx *= s;
ball.vy *= s;
}
if (Math.abs(ball.vy) < 1.5) {
ball.vy = ball.vy < 0 ? -1.5 : 1.5;
}
}
function updateBalls() {
let activeCount = 0;
for (let i = balls.length - 1; i >= 0; i--) {
const ball = balls[i];
if (!ball.active) { balls.splice(i, 1); continue; }
activeCount++;
if (ball.lava) {
ball.trail.push({ x: ball.x, y: ball.y, life: 10 });
if (ball.trail.length > 8) ball.trail.shift();
ball.trail.forEach(t => t.life--);
if (frameCount % 2 === 0) {
spawnParticles(ball.x, ball.y, Math.random() > 0.5 ? '#ff4400' : '#ffaa00', 1, 'fire');
}
}
ball.x += ball.vx;
ball.y += ball.vy;
if (ball.x - ball.radius < 0) {
ball.x = ball.radius;
ball.vx = Math.abs(ball.vx);
sfxWall();
}
if (ball.x + ball.radius > BASE_W) {
ball.x = BASE_W - ball.radius;
ball.vx = -Math.abs(ball.vx);
sfxWall();
}
if (ball.y - ball.radius < 0) {
ball.y = ball.radius;
ball.vy = Math.abs(ball.vy);
sfxWall();
}
if (ball.y - ball.radius > BASE_H) {
ball.active = false;
balls.splice(i, 1);
continue;
}
if (ball.vy > 0 &&
ball.y + ball.radius > paddle.y &&
ball.y - ball.radius < paddle.y + paddle.height &&
ball.x + ball.radius > paddle.x &&
ball.x - ball.radius < paddle.x + paddle.width) {
const hitPos = (ball.x - paddle.x) / paddle.width; const angle = (hitPos - 0.5) * Math.PI * 0.7; const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
ball.vx = Math.sin(angle) * speed;
ball.vy = -Math.abs(Math.cos(angle) * speed);
ball.y = paddle.y - ball.radius;
clampBallSpeed(ball);
sfxPaddle();
spawnParticles(ball.x, paddle.y, '#4488ff', 5, 'burst');
}
checkBallBrickCollision(ball);
}
if (activeCount === 0 && balls.length === 0 && gameState === STATE.PLAYING) {
lives--;
if (lives <= 0) {
gameState = STATE.GAME_OVER;
stopMusic();
sfxGameOver();
if (score > highScore) highScore = score;
} else {
balls = [createBall()];
activePowerups.bomb = false;
activePowerups.growth = false;
balls[0].color = '#ffffff';
}
}
}
function checkBallBrickCollision(ball) {
for (const brick of bricks) {
if (!brick.alive) continue;
const closestX = Math.max(brick.x, Math.min(ball.x, brick.x + brick.w));
const closestY = Math.max(brick.y, Math.min(ball.y, brick.y + brick.h));
const dx = ball.x - closestX;
const dy = ball.y - closestY;
const distSq = dx * dx + dy * dy;
if (distSq < ball.radius * ball.radius) {
const overlapLeft = (ball.x + ball.radius) - brick.x;
const overlapRight = (brick.x + brick.w) - (ball.x - ball.radius);
const overlapTop = (ball.y + ball.radius) - brick.y;
const overlapBottom = (brick.y + brick.h) - (ball.y - ball.radius);
const minOverlapX = Math.min(overlapLeft, overlapRight);
const minOverlapY = Math.min(overlapTop, overlapBottom);
if (!ball.lava) {
if (minOverlapX < minOverlapY) {
ball.vx = -ball.vx;
if (overlapLeft < overlapRight) ball.x = brick.x - ball.radius;
else ball.x = brick.x + brick.w + ball.radius;
} else {
ball.vy = -ball.vy;
if (overlapTop < overlapBottom) ball.y = brick.y - ball.radius;
else ball.y = brick.y + brick.h + ball.radius;
}
}
let damage = 1;
if (ball.lava) damage = brick.hp; if (ball.bomb) {
ball.bombHits++;
damage = brick.hp; if (ball.bombHits >= ball.bombMax) {
ball.bomb = false;
ball.color = ball.lava ? '#ff6600' : '#ffffff';
sfxBomb();
spawnParticles(ball.x, ball.y, '#ff4400', 25, 'burst');
spawnParticles(ball.x, ball.y, '#ffaa00', 15, 'burst');
}
}
hitBrick(brick, damage);
sfxBrick();
clampBallSpeed(ball);
if (!ball.lava) return; }
}
}
function hitBrick(brick, damage) {
if (!brick.alive) return;
brick.hp -= damage;
addScore(10 * brick.maxHP);
if (brick.hp <= 0) {
brick.alive = false;
brick.hp = 0;
bricksRemaining--;
const col = HP_COLORS[brick.maxHP] || '#fff';
spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, col, 12, 'fire');
if (brick.hasPowerup) {
spawnPowerup(brick.x + brick.w / 2 - 14, brick.y + brick.h / 2);
}
if (bricksRemaining <= 0) {
onLevelComplete();
}
} else {
spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2,
HP_COLORS[brick.hp] || '#fff', 4, 'fire');
}
}
function addScore(pts) {
score += pts;
while (score >= nextLifeAt) {
lives++;
nextLifeAt += scoreMilestone;
sfx1UP();
spawnFloatingText(BASE_W / 2, BASE_H / 2 - 40, '1UP!', '#ff2266', 2.5);
spawnParticles(BASE_W / 2, BASE_H / 2, '#ff2266', 30, 'burst');
}
}
function startGame() {
initAudio();
score = 0;
lives = 3;
level = 1;
nextLifeAt = scoreMilestone;
resetPowerups();
createBricks(level);
balls = [createBall()];
powerups = [];
projectiles = [];
particles = [];
floatingTexts = [];
paddle.x = BASE_W / 2 - paddle.width / 2;
paddle.width = paddle.baseWidth;
paddle.animating = false;
gameState = STATE.PLAYING;
startMusic();
}
function resetPowerups() {
activePowerups = {
paddleExtend: false, paddleTimer: 0,
speedBoost: false, speedTimer: 0,
projectile: false, projectileShots: 0,
lava: false, lavaTimer: 0,
bomb: false, growth: false
};
paddle.width = paddle.baseWidth;
paddle.wings = false;
}
function onLevelComplete() {
gameState = STATE.LEVEL_COMPLETE;
pauseMusic();
sfxLevelComplete();
lives++; spawnFloatingText(BASE_W / 2, BASE_H / 2 - 60, 'LEVEL CLEAR!', '#44ff88', 2);
spawnFloatingText(BASE_W / 2, BASE_H / 2 - 20, '+1 LIFE', '#ff2266', 1.5);
paddle.animating = true;
paddle.animT = 0;
paddle.rotY = 0;
paddle.scaleX = 1;
paddle.scaleY = 1;
paddle.glow = 0;
for (let i = 0; i < 5; i++) {
setTimeout(() => {
spawnParticles(paddle.x + 10, paddle.y + paddle.height, '#4488ff', 8, 'jet');
spawnParticles(paddle.x + paddle.width - 10, paddle.y + paddle.height, '#4488ff', 8, 'jet');
}, i * 200);
}
}
function nextLevel() {
level++;
resetPowerups();
createBricks(level);
balls = [createBall()];
const speedBoost = Math.min(1.5, 1 + (level - 1) * 0.05);
balls[0].vx *= speedBoost;
balls[0].vy *= speedBoost;
clampBallSpeed(balls[0]);
powerups = [];
projectiles = [];
paddle.x = BASE_W / 2 - paddle.width / 2;
paddle.width = paddle.baseWidth;
paddle.animating = false;
paddle.glow = 0;
gameState = STATE.PLAYING;
startMusic();
}
function updatePaddle() {
if (paddle.animating) {
paddle.animT += 1 / 60;
const t = Math.min(paddle.animT / 3, 1);
paddle.rotY = t * Math.PI * 8; paddle.y = (BASE_H - 40) - t * 200;
paddle.scaleX = 1 + Math.sin(t * Math.PI * 8) * 0.3;
paddle.scaleY = 1 - Math.sin(t * Math.PI * 8) * 0.2;
paddle.glow = t;
if (frameCount % 3 === 0) {
spawnParticles(paddle.x + paddle.width * 0.2, paddle.y + paddle.height, '#66aaff', 3, 'jet');
spawnParticles(paddle.x + paddle.width * 0.8, paddle.y + paddle.height, '#66aaff', 3, 'jet');
}
if (t >= 1) {
paddle.animating = false;
paddle.y = BASE_H - 40;
paddle.scaleX = 1;
paddle.scaleY = 1;
}
return;
}
if (keys['ArrowLeft'] || keys['a'] || keys['A'] || keys['KeyA'] || touchLeft) {
paddle.x -= paddle.speed;
mouseActive = false;
}
if (keys['ArrowRight'] || keys['d'] || keys['D'] || keys['KeyD'] || touchRight) {
paddle.x += paddle.speed;
mouseActive = false;
}
if (mouseActive) {
paddle.x = mouseX - paddle.width / 2;
}
if (paddle.x < 0) paddle.x = 0;
if (paddle.x + paddle.width > BASE_W) paddle.x = BASE_W - paddle.width;
}
function drawBackground() {
const grad = ctx.createLinearGradient(0, 0, 0, BASE_H);
grad.addColorStop(0, '#0a0a1a');
grad.addColorStop(0.5, '#0d0d20');
grad.addColorStop(1, '#080812');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, BASE_W, BASE_H);
ctx.fillStyle = 'rgba(0,0,0,0.15)';
for (let y = 0; y < BASE_H; y += 3) {
ctx.fillRect(0, y, BASE_W, 1);
}
ctx.strokeStyle = 'rgba(68, 136, 255, 0.2)';
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, BASE_W - 2, BASE_H - 2);
}
function drawBricks() {
for (const brick of bricks) {
if (!brick.alive) continue;
const color = HP_COLORS[brick.hp] || '#fff';
ctx.fillStyle = color;
ctx.fillRect(brick.x, brick.y, brick.w, brick.h);
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.fillRect(brick.x, brick.y, brick.w, 3);
ctx.fillRect(brick.x, brick.y, 3, brick.h);
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.fillRect(brick.x, brick.y + brick.h - 3, brick.w, 3);
ctx.fillRect(brick.x + brick.w - 3, brick.y, 3, brick.h);
if (brick.hp > 1 || brick.maxHP > 1) {
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.font = 'bold 12px Courier New';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(String(brick.hp), brick.x + brick.w / 2, brick.y + brick.h / 2 + 1);
}
}
}
function drawPaddle() {
ctx.save();
const cx = paddle.x + paddle.width / 2;
const cy = paddle.y + paddle.height / 2;
if (paddle.animating) {
ctx.translate(cx, cy);
const perspective = Math.abs(Math.cos(paddle.rotY));
ctx.scale(paddle.scaleX * (0.3 + 0.7 * perspective), paddle.scaleY);
ctx.translate(-cx, -cy);
ctx.shadowColor = '#4488ff';
ctx.shadowBlur = 20 + paddle.glow * 30;
}
const pGrad = ctx.createLinearGradient(paddle.x, paddle.y, paddle.x, paddle.y + paddle.height);
if (paddle.animating) {
pGrad.addColorStop(0, '#88ccff');
pGrad.addColorStop(0.5, '#4488ff');
pGrad.addColorStop(1, '#2266cc');
} else {
pGrad.addColorStop(0, '#66aaff');
pGrad.addColorStop(0.5, '#4488ff');
pGrad.addColorStop(1, '#2255aa');
}
ctx.fillStyle = pGrad;
roundRect(paddle.x, paddle.y, paddle.width, paddle.height, 4);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.4)';
roundRect(paddle.x + 2, paddle.y + 1, paddle.width - 4, 4, 2);
ctx.fill();
if (paddle.wings || activePowerups.projectile) {
ctx.fillStyle = '#ffaa00';
ctx.fillRect(paddle.x + 4, paddle.y - 6, 6, 8);
ctx.fillRect(paddle.x + 5, paddle.y - 10, 4, 5);
ctx.fillRect(paddle.x + paddle.width - 10, paddle.y - 6, 6, 8);
ctx.fillRect(paddle.x + paddle.width - 9, paddle.y - 10, 4, 5);
ctx.fillStyle = 'rgba(255, 170, 0, 0.4)';
ctx.beginPath();
ctx.arc(paddle.x + 7, paddle.y - 10, 4, 0, Math.PI * 2);
ctx.arc(paddle.x + paddle.width - 7, paddle.y - 10, 4, 0, Math.PI * 2);
ctx.fill();
}
if (activePowerups.paddleExtend) {
ctx.strokeStyle = 'rgba(68, 255, 136, 0.6)';
ctx.lineWidth = 2;
roundRect(paddle.x - 2, paddle.y - 2, paddle.width + 4, paddle.height + 4, 5);
ctx.stroke();
}
ctx.restore();
}
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function drawBalls() {
for (const ball of balls) {
if (!ball.active) continue;
if (ball.trail && ball.trail.length > 0) {
for (const t of ball.trail) {
if (t.life <= 0) continue;
const alpha = t.life / 10;
ctx.fillStyle = ball.lava ?
`rgba(255, ${100 + t.life * 10}, 0, ${alpha * 0.5})` :
`rgba(255,255,255,${alpha * 0.3})`;
ctx.beginPath();
ctx.arc(t.x, t.y, ball.radius * alpha, 0, Math.PI * 2);
ctx.fill();
}
}
if (ball.lava) {
const g = ctx.createRadialGradient(ball.x - 2, ball.y - 2, 1, ball.x, ball.y, ball.radius);
g.addColorStop(0, '#ffff44');
g.addColorStop(0.4, '#ff6600');
g.addColorStop(1, '#cc2200');
ctx.fillStyle = g;
ctx.shadowColor = '#ff4400';
ctx.shadowBlur = 12;
} else if (ball.bomb) {
ctx.fillStyle = '#222';
ctx.shadowColor = '#ff0000';
ctx.shadowBlur = 8;
} else {
const g = ctx.createRadialGradient(ball.x - 2, ball.y - 2, 1, ball.x, ball.y, ball.radius);
g.addColorStop(0, '#ffffff');
g.addColorStop(0.7, '#ccddee');
g.addColorStop(1, '#8899aa');
ctx.fillStyle = g;
}
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
if (ball.grown) {
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius + 1, 0, Math.PI * 2);
ctx.stroke();
}
if (ball.bomb) {
ctx.fillStyle = '#ff4444';
ctx.font = 'bold 10px Courier New';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(String(ball.bombMax - ball.bombHits), ball.x, ball.y);
}
}
}
function drawPowerups() {
for (const p of powerups) {
if (!p.active) continue;
ctx.shadowColor = p.type.color;
ctx.shadowBlur = 10;
ctx.fillStyle = p.type.color;
roundRect(p.x, p.y, p.w, p.h, 3);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
ctx.lineWidth = 1;
roundRect(p.x, p.y, p.w, p.h, 3);
ctx.stroke();
ctx.fillStyle = p.type.id === 'bomb' ? '#fff' : '#000';
ctx.font = 'bold 8px Courier New';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(p.type.label, p.x + p.w / 2, p.y + p.h / 2);
}
}
function drawProjectiles() {
for (const p of projectiles) {
if (!p.active) continue;
for (const t of p.trail) {
if (t.life <= 0) continue;
const alpha = t.life / 15;
ctx.fillStyle = `rgba(150, 150, 150, ${alpha * 0.4})`;
ctx.beginPath();
ctx.arc(t.x, t.y, 3 * alpha, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = '#ffaa00';
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.fillStyle = '#ff4400';
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(p.x + p.w / 2, p.y - 4);
ctx.lineTo(p.x + p.w, p.y);
ctx.fill();
ctx.fillStyle = 'rgba(255, 170, 0, 0.3)';
ctx.fillRect(p.x - 1, p.y, p.w + 2, p.h);
}
}
function drawParticles() {
for (const p of particles) {
const alpha = p.life / p.maxLife;
let color = p.color;
if (p.type === 'fire' || p.type === 'burst') {
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2);
ctx.fill();
} else if (p.type === 'jet') {
ctx.globalAlpha = alpha * 0.7;
ctx.fillStyle = color;
ctx.fillRect(p.x - 1, p.y, 2, p.size * 2);
}
}
ctx.globalAlpha = 1;
for (const t of floatingTexts) {
const alpha = t.life / t.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = t.color;
ctx.font = `bold ${Math.floor(16 * t.scale)}px Courier New`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = t.color;
ctx.shadowBlur = 8;
ctx.fillText(t.text, t.x, t.y);
ctx.shadowBlur = 0;
}
ctx.globalAlpha = 1;
}
function drawHUD() {
ctx.fillStyle = '#88ccff';
ctx.font = 'bold 16px Courier New';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText(`SCORE: ${score}`, 12, 10);
ctx.font = '12px Courier New';
ctx.fillStyle = '#6688aa';
ctx.fillText(`HI: ${highScore}`, 12, 30);
ctx.fillStyle = '#88ccff';
ctx.font = 'bold 16px Courier New';
ctx.textAlign = 'center';
ctx.fillText(`LEVEL ${level}`, BASE_W / 2, 10);
ctx.font = '10px Courier New';
ctx.fillStyle = '#556677';
ctx.fillText(getPatternForLevel(level).toUpperCase(), BASE_W / 2, 30);
ctx.textAlign = 'right';
ctx.fillStyle = '#88ccff';
ctx.font = 'bold 16px Courier New';
ctx.fillText(`LIVES: ${lives}`, BASE_W - 12, 10);
for (let i = 0; i < Math.min(lives, 5); i++) {
ctx.fillStyle = '#ff4466';
ctx.beginPath();
ctx.arc(BASE_W - 20 - i * 18, 38, 6, 0, Math.PI * 2);
ctx.fill();
}
let statusX = 12;
const statusY = BASE_H - 18;
ctx.font = 'bold 11px Courier New';
ctx.textAlign = 'left';
if (activePowerups.paddleExtend) {
ctx.fillStyle = '#44ff88';
const secs = Math.ceil(activePowerups.paddleTimer / 60);
ctx.fillText(`WIDE:${secs}s`, statusX, statusY);
statusX += 70;
}
if (activePowerups.speedBoost) {
ctx.fillStyle = '#44aaff';
const secs = Math.ceil(activePowerups.speedTimer / 60);
ctx.fillText(`SLOW:${secs}s`, statusX, statusY);
statusX += 70;
}
if (activePowerups.projectile) {
ctx.fillStyle = '#ffaa00';
ctx.fillText(`MISS:${activePowerups.projectileShots}`, statusX, statusY);
statusX += 75;
}
if (activePowerups.lava) {
ctx.fillStyle = '#ff4400';
const secs = Math.ceil(activePowerups.lavaTimer / 60);
ctx.fillText(`LAVA:${secs}s`, statusX, statusY);
statusX += 70;
}
if (activePowerups.bomb) {
ctx.fillStyle = '#ff6688';
ctx.fillText('BOMB', statusX, statusY);
statusX += 50;
}
if (activePowerups.growth) {
ctx.fillStyle = '#ffffff';
ctx.fillText('GROW', statusX, statusY);
}
}
function drawMenu() {
drawBackground();
ctx.fillStyle = '#4488ff';
ctx.font = 'bold 56px Courier New';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = '#2266cc';
ctx.shadowBlur = 20;
ctx.fillText('BREAKOUT', BASE_W / 2, BASE_H / 2 - 100);
ctx.shadowBlur = 0;
ctx.fillStyle = '#88aacc';
ctx.font = '16px Courier New';
ctx.fillText('ATARI-STYLE ARCADE', BASE_W / 2, BASE_H / 2 - 50);
if (Math.floor(frameCount / 30) % 2 === 0) {
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 20px Courier New';
ctx.fillText('PRESS ENTER / SPACE / TAP TO START', BASE_W / 2, BASE_H / 2 + 20);
}
ctx.fillStyle = '#556677';
ctx.font = '13px Courier New';
ctx.fillText('← → / A D / Mouse / Touch to move', BASE_W / 2, BASE_H / 2 + 70);
ctx.fillText('SPACE / FIRE to shoot missiles | P to pause', BASE_W / 2, BASE_H / 2 + 92);
ctx.fillText('Destroy all bricks • Collect powerups • Survive!', BASE_W / 2, BASE_H / 2 + 120);
const demoColors = ['#ffee44', '#44cc44', '#4488ff', '#ff8844', '#ff4444'];
for (let i = 0; i < 5; i++) {
ctx.fillStyle = demoColors[i];
ctx.fillRect(BASE_W / 2 - 180 + i * 75, BASE_H / 2 + 160, 65, 18);
}
}
function drawPaused() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(0, 0, BASE_W, BASE_H);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 40px Courier New';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('PAUSED', BASE_W / 2, BASE_H / 2 - 20);
ctx.font = '16px Courier New';
ctx.fillStyle = '#88aacc';
ctx.fillText('Press P or SPACE to resume', BASE_W / 2, BASE_H / 2 + 30);
}
function drawGameOver() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, 0, BASE_W, BASE_H);
ctx.fillStyle = '#ff4444';
ctx.font = 'bold 48px Courier New';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = '#aa0000';
ctx.shadowBlur = 15;
ctx.fillText('GAME OVER', BASE_W / 2, BASE_H / 2 - 60);
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 24px Courier New';
ctx.fillText(`SCORE: ${score}`, BASE_W / 2, BASE_H / 2);
ctx.fillStyle = '#88aacc';
ctx.font = '16px Courier New';
ctx.fillText(`Level Reached: ${level}`, BASE_W / 2, BASE_H / 2 + 35);
if (score >= highScore && score > 0) {
ctx.fillStyle = '#ffee44';
ctx.font = 'bold 18px Courier New';
ctx.fillText('NEW HIGH SCORE!', BASE_W / 2, BASE_H / 2 + 65);
}
if (Math.floor(frameCount / 30) % 2 === 0) {
ctx.fillStyle = '#ffffff';
ctx.font = '16px Courier New';
ctx.fillText('PRESS ENTER / SPACE / TAP TO RESTART', BASE_W / 2, BASE_H / 2 + 110);
}
}
function drawLevelComplete() {
ctx.fillStyle = 'rgba(0, 0, 30, 0.4)';
ctx.fillRect(0, 0, BASE_W, BASE_H);
ctx.fillStyle = '#44ff88';
ctx.font = 'bold 40px Courier New';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = '#22aa44';
ctx.shadowBlur = 15;
ctx.fillText('LEVEL COMPLETE!', BASE_W / 2, BASE_H / 2 - 80);
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffffff';
ctx.font = '20px Courier New';
ctx.fillText(`Level ${level} Cleared!`, BASE_W / 2, BASE_H / 2 - 30);
ctx.fillStyle = '#ff2266';
ctx.fillText('+1 LIFE', BASE_W / 2, BASE_H / 2 + 5);
ctx.fillStyle = '#88aacc';
ctx.font = '16px Courier New';
ctx.fillText(`Score: ${score}`, BASE_W / 2, BASE_H / 2 + 40);
if (Math.floor(frameCount / 30) % 2 === 0) {
ctx.fillStyle = '#ffffff';
ctx.font = '16px Courier New';
ctx.fillText('PRESS ENTER / TAP FOR NEXT LEVEL', BASE_W / 2, BASE_H / 2 + 90);
}
}
function update(dt) {
if (gameState === STATE.PLAYING) {
updatePaddle();
updateBalls();
updatePowerups();
updateProjectiles();
updateParticles();
} else if (gameState === STATE.LEVEL_COMPLETE) {
updatePaddle(); updateParticles();
}
}
function render() {
drawBackground();
if (gameState === STATE.MENU) {
drawMenu();
return;
}
drawBricks();
drawPowerups();
drawProjectiles();
drawBalls();
drawPaddle();
drawParticles();
drawHUD();
if (gameState === STATE.PAUSED) {
drawPaused();
} else if (gameState === STATE.GAME_OVER) {
drawGameOver();
} else if (gameState === STATE.LEVEL_COMPLETE) {
drawLevelComplete();
}
}
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
deltaTime = Math.min((timestamp - lastTime) / 1000, 0.05); lastTime = timestamp;
frameCount++;
update(deltaTime);
render();
requestAnimationFrame(gameLoop);
}
function init() {
setupInput();
resizeCanvas();
try {
highScore = parseInt(localStorage.getItem('breakout_highscore') || '0');
} catch(e) {}
setInterval(() => {
try {
if (score > highScore) {
highScore = score;
localStorage.setItem('breakout_highscore', String(highScore));
}
} catch(e) {}
}, 5000);
requestAnimationFrame(gameLoop);
}
init();
</script>
</body>
</html>