Run details
- Model
- google/gemini-3.8-flash
- Route
- Command Code CLI
- Reasoning
- provider default
- Limits
- 60 min
- Date
- 2 Oct 2026
- Time
- 86 s
- Size
- 32 kB
- Tokens
- 104,731 in, 13,067 out
- List price
- $0.14 ($0.825 in / $4.13 out per M)
- Paid
- Not billed per run (CLI plan)
Exact prompt
Make a Breakout game I can play right now with the keyboard or the mouse. Give it several levels, a score and some juice. It must be one complete, self-contained HTML file: inline all CSS and JavaScript, and make no network requests of any kind (no CDNs, web fonts or remote images). Save it as index.html in the current directory, then stop. Do not ask questions.
Source
32 kB 1,152 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout Deluxe</title>
<style>
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
margin: 0;
padding: 0;
}
body {
background: #090a12;
color: #e0e6ed;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
overflow: hidden;
}
#game-container {
position: relative;
box-shadow: 0 0 50px rgba(0, 150, 255, 0.25), 0 20px 40px rgba(0, 0, 0, 0.8);
border-radius: 12px;
overflow: hidden;
border: 2px solid #1f273d;
background: #0d111d;
}
canvas {
display: block;
cursor: none;
}
.info-bar {
margin-top: 14px;
font-size: 13px;
color: #7b8ba4;
letter-spacing: 0.5px;
display: flex;
gap: 18px;
}
.info-bar span kbd {
background: #192135;
border: 1px solid #2d3b59;
border-radius: 4px;
padding: 2px 6px;
color: #a7bce0;
font-size: 11px;
font-family: inherit;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="game" width="800" height="600"></canvas>
</div>
<div class="info-bar">
<span><kbd>Mouse</kbd> or <kbd>←</kbd> <kbd>→</kbd> / <kbd>A</kbd> <kbd>D</kbd> Move</span>
<span><kbd>Space</kbd> or <kbd>Click</kbd> Launch</span>
<span><kbd>P</kbd> Pause</span>
<span><kbd>M</kbd> Mute</span>
</div>
<script>
(() => {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
// Web Audio Context for synthesized sound fx
let audioCtx = null;
let muted = false;
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playSound(type) {
if (muted || !audioCtx) return;
try {
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
if (type === 'hit-brick') {
osc.type = 'triangle';
osc.frequency.setValueAtTime(360 + Math.random() * 80, now);
osc.frequency.exponentialRampToValueAtTime(140, now + 0.08);
gain.gain.setValueAtTime(0.25, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
osc.start(now);
osc.stop(now + 0.08);
} else if (type === 'hit-paddle') {
osc.type = 'sine';
osc.frequency.setValueAtTime(220, now);
osc.frequency.exponentialRampToValueAtTime(440, now + 0.09);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.09);
osc.start(now);
osc.stop(now + 0.09);
} else if (type === 'hit-wall') {
osc.type = 'sine';
osc.frequency.setValueAtTime(180, now);
osc.frequency.exponentialRampToValueAtTime(90, now + 0.05);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
osc.start(now);
osc.stop(now + 0.05);
} else if (type === 'powerup') {
osc.type = 'sine';
osc.frequency.setValueAtTime(320, now);
osc.frequency.exponentialRampToValueAtTime(880, now + 0.2);
gain.gain.setValueAtTime(0.25, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
} else if (type === 'lose-life') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(260, now);
osc.frequency.exponentialRampToValueAtTime(60, now + 0.35);
gain.gain.setValueAtTime(0.35, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
osc.start(now);
osc.stop(now + 0.35);
} else if (type === 'level-clear') {
const chord = [440, 554.37, 659.25, 880];
chord.forEach((freq, idx) => {
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.connect(g);
g.connect(audioCtx.destination);
o.type = 'triangle';
o.frequency.setValueAtTime(freq, now + idx * 0.07);
g.gain.setValueAtTime(0, now);
g.gain.setValueAtTime(0.2, now + idx * 0.07);
g.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.07 + 0.35);
o.start(now + idx * 0.07);
o.stop(now + idx * 0.07 + 0.35);
});
} else if (type === 'game-over') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, now);
osc.frequency.linearRampToValueAtTime(50, now + 0.6);
gain.gain.setValueAtTime(0.4, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.6);
osc.start(now);
osc.stop(now + 0.6);
} else if (type === 'laser') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(900, now);
osc.frequency.exponentialRampToValueAtTime(200, now + 0.12);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
osc.start(now);
osc.stop(now + 0.12);
}
} catch (e) {
// Ignore audio errors
}
}
// Level layouts (0: empty, 1: red, 2: orange, 3: yellow, 4: green, 5: cyan, 6: purple, 9: hardened)
const LEVELS = [
// Level 1: Classic Rainbow warmup
{
name: "The Gateway",
rows: [
"111111111111",
"222222222222",
"333333333333",
"444444444444",
"555555555555"
]
},
// Level 2: Space Invader / Castle shape
{
name: "Fortress",
rows: [
"001111111100",
"019222222910",
"190333333091",
"114400004411",
"005555555500",
"000966669000"
]
},
// Level 3: Checkerboard / Diamonds
{
name: "Prism Maze",
rows: [
"101010101010",
"020202020202",
"309030309030",
"040404040404",
"505050505050",
"060906609060",
"101010101010"
]
},
// Level 4: The Citadel with armored blocks
{
name: "Titan's Keep",
rows: [
"999999999999",
"111001100111",
"229229922922",
"333333333333",
"044094490440",
"555555555555",
"006660066600"
]
},
// Level 5: Heart of the Arcade
{
name: "Core Meltdown",
rows: [
"011000000110",
"122109901221",
"233296692332",
"344396693443",
"044009900440",
"005500005500",
"000660066000",
"000099990000"
]
}
];
const COLOR_PALETTE = {
1: { top: '#ff3366', bot: '#b3003b', glow: '#ff3366', points: 100 }, // Red
2: { top: '#ff9933', bot: '#cc5500', glow: '#ff9933', points: 80 }, // Orange
3: { top: '#ffea00', bot: '#c7a700', glow: '#ffea00', points: 60 }, // Yellow
4: { top: '#00e676', bot: '#00963f', glow: '#00e676', points: 40 }, // Green
5: { top: '#00d4ff', bot: '#007fa3', glow: '#00d4ff', points: 30 }, // Cyan
6: { top: '#b347ff', bot: '#6c00b8', glow: '#b347ff', points: 50 }, // Purple
9: { top: '#8a99ad', bot: '#424f63', glow: '#9bb1cc', points: 150, hp: 2 } // Steel / Armored
};
const POWERUP_TYPES = [
{ type: 'expand', label: 'WIDE', color: '#00e676', icon: '↔' },
{ type: 'multiball', label: 'MULTI', color: '#00d4ff', icon: '••' },
{ type: 'laser', label: 'LASER', color: '#ff3366', icon: '▲' },
{ type: 'fireball', label: 'FIRE', color: '#ff9933', icon: '🔥' },
{ type: 'life', label: '+LIFE', color: '#ffea00', icon: '♥' }
];
// Game state
let state = 'title'; // 'title', 'playing', 'paused', 'levelclear', 'gameover', 'victory'
let score = 0;
let highScore = parseInt(localStorage.getItem('breakout_high') || '0', 10);
let lives = 3;
let currentLevelIdx = 0;
let combo = 0;
let comboTimer = 0;
let screenShake = 0;
let messageTimer = 0;
let levelBannerText = '';
// Entities
let paddle = {
x: W / 2 - 55,
y: H - 42,
w: 110,
baseW: 110,
targetW: 110,
h: 14,
speed: 9,
dx: 0,
laserTimer: 0
};
let balls = [];
let bricks = [];
let particles = [];
let floatingTexts = [];
let powerups = [];
let lasers = [];
// Controls
const keys = { left: false, right: false, space: false };
let mouseActive = false;
let mouseTargetX = W / 2;
function resetPaddle() {
paddle.w = paddle.baseW;
paddle.targetW = paddle.baseW;
paddle.x = W / 2 - paddle.w / 2;
paddle.laserTimer = 0;
}
function spawnBall(stickToPaddle = true) {
const ballSpeed = 6.5 + currentLevelIdx * 0.4;
return {
x: paddle.x + paddle.w / 2,
y: paddle.y - 10,
vx: (Math.random() > 0.5 ? 1 : -1) * (ballSpeed * 0.6),
vy: -ballSpeed * 0.8,
speed: ballSpeed,
radius: 7,
stuck: stickToPaddle,
fireball: 0,
tail: []
};
}
function loadLevel(idx) {
currentLevelIdx = idx % LEVELS.length;
const lvl = LEVELS[currentLevelIdx];
bricks = [];
powerups = [];
lasers = [];
particles = [];
const numCols = 12;
const padding = 8;
const offsetTop = 75;
const offsetLeft = 40;
const availableW = W - offsetLeft * 2;
const brickW = (availableW - padding * (numCols - 1)) / numCols;
const brickH = 22;
lvl.rows.forEach((row, r) => {
for (let c = 0; c < row.length; c++) {
const char = row[c];
if (char !== '0') {
const type = parseInt(char, 10);
const meta = COLOR_PALETTE[type];
bricks.push({
x: offsetLeft + c * (brickW + padding),
y: offsetTop + r * (brickH + padding),
w: brickW,
h: brickH,
type: type,
color: meta,
hp: meta.hp || 1,
maxHp: meta.hp || 1,
scale: 1,
wobble: 0
});
}
}
});
resetPaddle();
balls = [spawnBall(true)];
levelBannerText = `Level ${currentLevelIdx + 1}: ${lvl.name}`;
messageTimer = 100;
}
function addScore(pts, x, y) {
const multiplier = 1 + Math.floor(combo / 3) * 0.25;
const finalPts = Math.round(pts * multiplier);
score += finalPts;
if (score > highScore) {
highScore = score;
localStorage.setItem('breakout_high', highScore.toString());
}
combo++;
comboTimer = 110;
floatingTexts.push({
text: `+${finalPts}` + (multiplier > 1 ? ` (x${multiplier.toFixed(2)})` : ''),
x: x || W / 2,
y: y || H / 2,
vy: -1.4,
alpha: 1,
color: multiplier > 1 ? '#ffea00' : '#ffffff',
size: multiplier > 1 ? 16 : 14
});
}
function createExplosion(x, y, color, count = 16, speed = 4) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const spd = (0.5 + Math.random()) * speed;
particles.push({
x: x,
y: y,
vx: Math.cos(angle) * spd,
vy: Math.sin(angle) * spd,
size: 2 + Math.random() * 4,
alpha: 1,
color: color,
life: 25 + Math.random() * 20
});
}
}
function tryDropPowerup(x, y) {
// 22% chance of dropping powerup
if (Math.random() < 0.22) {
const p = POWERUP_TYPES[Math.floor(Math.random() * POWERUP_TYPES.length)];
powerups.push({
x: x,
y: y,
w: 32,
h: 18,
vy: 2.2,
type: p.type,
label: p.label,
color: p.color,
icon: p.icon,
angle: 0
});
}
}
function applyPowerup(p) {
playSound('powerup');
screenShake = Math.max(screenShake, 6);
floatingTexts.push({
text: p.label + '!',
x: paddle.x + paddle.w / 2,
y: paddle.y - 20,
vy: -2,
alpha: 1,
color: p.color,
size: 20
});
if (p.type === 'expand') {
paddle.targetW = Math.min(220, paddle.w + 45);
} else if (p.type === 'multiball') {
const currentCount = balls.length;
for (let i = 0; i < currentCount; i++) {
const b = balls[i];
if (!b.stuck) {
for (let k = -1; k <= 1; k += 2) {
balls.push({
x: b.x,
y: b.y,
vx: b.vx * Math.cos(k * 0.4) - b.vy * Math.sin(k * 0.4),
vy: b.vx * Math.sin(k * 0.4) + b.vy * Math.cos(k * 0.4),
speed: b.speed,
radius: b.radius,
stuck: false,
fireball: b.fireball,
tail: []
});
}
}
}
} else if (p.type === 'laser') {
paddle.laserTimer = 400; // ~6.5 seconds of firing
} else if (p.type === 'fireball') {
balls.forEach(b => {
b.fireball = 400;
});
} else if (p.type === 'life') {
lives++;
createExplosion(paddle.x + paddle.w / 2, paddle.y, '#ffea00', 25, 5);
}
}
function launchBall() {
initAudio();
let launchedAny = false;
balls.forEach(b => {
if (b.stuck) {
b.stuck = false;
// Direction based on paddle center offset
const offset = (b.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2);
b.vx = offset * b.speed * 0.8;
b.vy = -Math.sqrt(Math.max(16, b.speed * b.speed - b.vx * b.vx));
launchedAny = true;
}
});
if (launchedAny) {
playSound('hit-paddle');
}
}
function startGame() {
initAudio();
score = 0;
lives = 3;
combo = 0;
comboTimer = 0;
currentLevelIdx = 0;
loadLevel(0);
state = 'playing';
}
// Input listeners
window.addEventListener('keydown', (e) => {
initAudio();
if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
if (e.code === 'Space') {
keys.space = true;
if (state === 'title' || state === 'gameover' || state === 'victory') {
startGame();
} else if (state === 'playing') {
launchBall();
} else if (state === 'levelclear') {
loadLevel(currentLevelIdx + 1);
state = 'playing';
}
}
if (e.code === 'KeyP') {
if (state === 'playing') state = 'paused';
else if (state === 'paused') state = 'playing';
}
if (e.code === 'KeyM') {
muted = !muted;
}
});
window.addEventListener('keyup', (e) => {
if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
if (e.code === 'Space') keys.space = false;
});
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
mouseTargetX = (e.clientX - rect.left) * scaleX;
mouseActive = true;
});
canvas.addEventListener('mousedown', (e) => {
initAudio();
if (state === 'title' || state === 'gameover' || state === 'victory') {
startGame();
} else if (state === 'playing') {
launchBall();
} else if (state === 'levelclear') {
loadLevel(currentLevelIdx + 1);
state = 'playing';
}
});
// Main game update loop
let lastTime = performance.now();
function update() {
// Screen shake decay
if (screenShake > 0) {
screenShake *= 0.88;
if (screenShake < 0.2) screenShake = 0;
}
if (state !== 'playing') {
// Update particles even during victory / clear
updateParticles();
return;
}
// Paddle width lerp
paddle.w += (paddle.targetW - paddle.w) * 0.15;
// Paddle movement
if (mouseActive) {
const prevX = paddle.x;
paddle.x = mouseTargetX - paddle.w / 2;
paddle.dx = paddle.x - prevX;
mouseActive = false; // until next mousemove
} else {
if (keys.left) {
paddle.dx = -paddle.speed;
paddle.x += paddle.dx;
} else if (keys.right) {
paddle.dx = paddle.speed;
paddle.x += paddle.dx;
} else {
paddle.dx *= 0.7;
}
}
// Clamp paddle
if (paddle.x < 12) paddle.x = 12;
if (paddle.x + paddle.w > W - 12) paddle.x = W - 12 - paddle.w;
// Laser weapon firing
if (paddle.laserTimer > 0) {
paddle.laserTimer--;
if (paddle.laserTimer % 18 === 0) {
playSound('laser');
lasers.push({ x: paddle.x + 8, y: paddle.y - 4, vy: -12, w: 4, h: 14 });
lasers.push({ x: paddle.x + paddle.w - 12, y: paddle.y - 4, vy: -12, w: 4, h: 14 });
}
}
// Lasers update
for (let i = lasers.length - 1; i >= 0; i--) {
const l = lasers[i];
l.y += l.vy;
let hit = false;
for (let j = bricks.length - 1; j >= 0; j--) {
const b = bricks[j];
if (l.x + l.w > b.x && l.x < b.x + b.w && l.y < b.y + b.h && l.y + l.h > b.y) {
hit = true;
b.hp--;
b.scale = 1.15;
createExplosion(l.x, l.y, '#ff3366', 8, 3);
playSound('hit-brick');
if (b.hp <= 0) {
addScore(b.color.points, b.x + b.w / 2, b.y + b.h / 2);
createExplosion(b.x + b.w / 2, b.y + b.h / 2, b.color.top, 18, 4);
tryDropPowerup(b.x + b.w / 2, b.y + b.h / 2);
bricks.splice(j, 1);
}
break;
}
}
if (hit || l.y < 0) {
lasers.splice(i, 1);
}
}
// Update powerups
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.y += p.vy;
p.angle += 0.04;
// Check paddle collision
if (
p.x + p.w / 2 > paddle.x &&
p.x - p.w / 2 < paddle.x + paddle.w &&
p.y + p.h / 2 > paddle.y &&
p.y - p.h / 2 < paddle.y + paddle.h
) {
applyPowerup(p);
powerups.splice(i, 1);
continue;
}
if (p.y > H + 20) {
powerups.splice(i, 1);
}
}
// Combo timer
if (comboTimer > 0) {
comboTimer--;
if (comboTimer === 0) combo = 0;
}
// Message banner timer
if (messageTimer > 0) messageTimer--;
// Update balls
let allLost = true;
for (let i = balls.length - 1; i >= 0; i--) {
const b = balls[i];
if (b.fireball > 0) b.fireball--;
if (b.stuck) {
b.x = paddle.x + paddle.w / 2;
b.y = paddle.y - b.radius - 2;
allLost = false;
continue;
}
// Trail recording
b.tail.unshift({ x: b.x, y: b.y, alpha: 0.6, fireball: b.fireball > 0 });
if (b.tail.length > 8) b.tail.pop();
// Ball movement
b.x += b.vx;
b.y += b.vy;
// Wall collisions
if (b.x - b.radius < 12) {
b.x = 12 + b.radius;
b.vx = Math.abs(b.vx);
playSound('hit-wall');
createExplosion(12, b.y, '#5577aa', 5, 2);
} else if (b.x + b.radius > W - 12) {
b.x = W - 12 - b.radius;
b.vx = -Math.abs(b.vx);
playSound('hit-wall');
createExplosion(W - 12, b.y, '#5577aa', 5, 2);
}
if (b.y - b.radius < 12) {
b.y = 12 + b.radius;
b.vy = Math.abs(b.vy);
playSound('hit-wall');
createExplosion(b.x, 12, '#5577aa', 5, 2);
}
// Paddle collision
if (
b.y + b.radius >= paddle.y &&
b.y - b.radius <= paddle.y + paddle.h &&
b.x + b.radius >= paddle.x &&
b.x - b.radius <= paddle.x + paddle.w &&
b.vy > 0
) {
playSound('hit-paddle');
screenShake = Math.max(screenShake, 3);
// Angle influence based on impact point on paddle
const hitPoint = (b.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2);
const maxAngle = Math.PI * 0.42; // ~75 deg
const angle = hitPoint * maxAngle;
const currentSpeed = Math.min(12, b.speed * 1.02); // tiny acceleration
b.speed = currentSpeed;
b.vx = currentSpeed * Math.sin(angle) + paddle.dx * 0.15;
b.vy = -currentSpeed * Math.cos(angle);
b.y = paddle.y - b.radius;
createExplosion(b.x, paddle.y, '#00d4ff', 10, 3);
}
// Brick collisions
for (let j = bricks.length - 1; j >= 0; j--) {
const brk = bricks[j];
if (
b.x + b.radius > brk.x &&
b.x - b.radius < brk.x + brk.w &&
b.y + b.radius > brk.y &&
b.y - b.radius < brk.y + brk.h
) {
playSound('hit-brick');
screenShake = Math.max(screenShake, b.fireball > 0 ? 8 : 4);
if (b.fireball === 0) {
// Determine collision edge
const prevX = b.x - b.vx;
const prevY = b.y - b.vy;
let hitFromSide = false;
if (prevX + b.radius <= brk.x || prevX - b.radius >= brk.x + brk.w) {
b.vx = -b.vx;
hitFromSide = true;
}
if (prevY + b.radius <= brk.y || prevY - b.radius >= brk.y + brk.h) {
b.vy = -b.vy;
} else if (!hitFromSide) {
b.vy = -b.vy;
}
}
brk.hp -= (b.fireball > 0 ? 2 : 1);
brk.scale = 1.25;
if (brk.hp <= 0) {
addScore(brk.color.points, brk.x + brk.w / 2, brk.y + brk.h / 2);
createExplosion(brk.x + brk.w / 2, brk.y + brk.h / 2, brk.color.top, 22, 5);
tryDropPowerup(brk.x + brk.w / 2, brk.y + brk.h / 2);
bricks.splice(j, 1);
} else {
createExplosion(b.x, b.y, brk.color.top, 8, 3);
}
break; // collide with one brick per ball frame
}
}
// Ball below bottom
if (b.y > H + 20) {
balls.splice(i, 1);
} else {
allLost = false;
}
}
// Check level clear
if (bricks.length === 0) {
playSound('level-clear');
if (currentLevelIdx + 1 < LEVELS.length) {
state = 'levelclear';
} else {
state = 'victory';
}
return;
}
// Check lost life
if (allLost) {
lives--;
playSound('lose-life');
screenShake = 12;
combo = 0;
if (lives <= 0) {
playSound('game-over');
state = 'gameover';
} else {
resetPaddle();
balls = [spawnBall(true)];
}
}
updateParticles();
}
function updateParticles() {
// Particle physics
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vx *= 0.96;
p.vy *= 0.96;
p.life--;
p.alpha = Math.max(0, p.life / 40);
if (p.life <= 0) particles.splice(i, 1);
}
// Floating texts
for (let i = floatingTexts.length - 1; i >= 0; i--) {
const t = floatingTexts[i];
t.y += t.vy;
t.alpha -= 0.02;
if (t.alpha <= 0) floatingTexts.splice(i, 1);
}
// Brick scale animations
bricks.forEach(b => {
if (b.scale > 1) {
b.scale += (1 - b.scale) * 0.2;
}
});
}
// Render loop
function draw() {
ctx.save();
// Screen shake transform
if (screenShake > 0) {
const sx = (Math.random() - 0.5) * screenShake * 2;
const sy = (Math.random() - 0.5) * screenShake * 2;
ctx.translate(sx, sy);
}
// Background gradient & subtle grid
const bgGrad = ctx.createRadialGradient(W / 2, H / 2, 80, W / 2, H / 2, 500);
bgGrad.addColorStop(0, '#101626');
bgGrad.addColorStop(1, '#080a11');
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, W, H);
// Subtle arena boundaries
ctx.strokeStyle = '#1e2942';
ctx.lineWidth = 4;
ctx.strokeRect(10, 10, W - 20, H - 20);
// Border neon glow
ctx.strokeStyle = 'rgba(0, 170, 255, 0.15)';
ctx.lineWidth = 1;
ctx.strokeRect(12, 12, W - 24, H - 24);
// Draw Bricks
bricks.forEach(b => {
ctx.save();
const cx = b.x + b.w / 2;
const cy = b.y + b.h / 2;
ctx.translate(cx, cy);
ctx.scale(b.scale, b.scale);
const halfW = b.w / 2;
const halfH = b.h / 2;
// Glow for bricks
ctx.shadowColor = b.color.glow;
ctx.shadowBlur = b.scale > 1.05 ? 14 : 4;
const grad = ctx.createLinearGradient(-halfW, -halfH, -halfW, halfH);
grad.addColorStop(0, b.color.top);
grad.addColorStop(1, b.color.bot);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.roundRect(-halfW, -halfH, b.w, b.h, 4);
ctx.fill();
// Top highlight
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255, 255, 255, 0.35)';
ctx.beginPath();
ctx.roundRect(-halfW + 2, -halfH + 2, b.w - 4, (b.h - 4) * 0.35, 2);
ctx.fill();
// Armored crack indicator
if (b.type === 9 && b.hp === 1) {
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(-halfW + 6, -halfH + 4);
ctx.lineTo(0, 0);
ctx.lineTo(halfW - 8, halfH - 4);
ctx.stroke();
}
ctx.restore();
});
// Draw Lasers
lasers.forEach(l => {
ctx.save();
ctx.fillStyle = '#ff3366';
ctx.shadowColor = '#ff3366';
ctx.shadowBlur = 10;
ctx.fillRect(l.x, l.y, l.w, l.h);
ctx.restore();
});
// Draw Powerups
powerups.forEach(p => {
ctx.save();
ctx.translate(p.x, p.y);
ctx.shadowColor = p.color;
ctx.shadowBlur = 10;
ctx.fillStyle = '#101524';
ctx.strokeStyle = p.color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(-p.w / 2, -p.h / 2, p.w, p.h, 6);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
ctx.fillStyle = p.color;
ctx.font = 'bold 10px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(p.label, 0, 1);
ctx.restore();
});
// Draw Ball Trails & Balls
balls.forEach(b => {
// Trail
for (let i = 0; i < b.tail.length; i++) {
const pt = b.tail[i];
ctx.beginPath();
ctx.arc(pt.x, pt.y, b.radius * (1 - i / b.tail.length * 0.6), 0, Math.PI * 2);
ctx.fillStyle = pt.fireball
? `rgba(255, 120, 20, ${0.45 * (1 - i / b.tail.length)})`
: `rgba(0, 212, 255, ${0.35 * (1 - i / b.tail.length)})`;
ctx.fill();
}
// Ball body
ctx.save();
const isFire = b.fireball > 0;
ctx.shadowColor = isFire ? '#ff6600' : '#00d4ff';
ctx.shadowBlur = isFire ? 18 : 12;
const ballGrad = ctx.createRadialGradient(
b.x - b.radius * 0.3,
b.y - b.radius * 0.3,
b.radius * 0.1,
b.x,
b.y,
b.radius
);
if (isFire) {
ballGrad.addColorStop(0, '#ffffff');
ballGrad.addColorStop(0.4, '#ffdd33');
ballGrad.addColorStop(1, '#ff3300');
} else {
ballGrad.addColorStop(0, '#ffffff');
ballGrad.addColorStop(0.3, '#7fe9ff');
ballGrad.addColorStop(1, '#0099cc');
}
ctx.fillStyle = ballGrad;
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
// Draw Paddle
ctx.save();
ctx.shadowColor = paddle.laserTimer > 0 ? '#ff3366' : '#00e5ff';
ctx.shadowBlur = 14;
const padGrad = ctx.createLinearGradient(paddle.x, paddle.y, paddle.x, paddle.y + paddle.h);
if (paddle.laserTimer > 0) {
padGrad.addColorStop(0, '#ff6688');
padGrad.addColorStop(0.5, '#cc0033');
padGrad.addColorStop(1, '#66001a');
} else {
padGrad.addColorStop(0, '#66ffff');
padGrad.addColorStop(0.5, '#0099dd');
padGrad.addColorStop(1, '#004488');
}
ctx.fillStyle = padGrad;
ctx.beginPath();
ctx.roundRect(paddle.x, paddle.y, paddle.w, paddle.h, 7);
ctx.fill();
// Paddle gloss top
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255, 255, 255, 0.45)';
ctx.beginPath();
ctx.roundRect(paddle.x + 3, paddle.y + 2, paddle.w - 6, 3, 2);
ctx.fill();
// Laser cannons indicator
if (paddle.laserTimer > 0) {
ctx.fillStyle = '#ffea00';
ctx.fillRect(paddle.x + 6, paddle.y - 4, 4, 6);
ctx.fillRect(paddle.x + paddle.w - 10, paddle.y - 4, 4, 6);
}
ctx.restore();
// Draw Particles
particles.forEach(p => {
ctx.save();
ctx.globalAlpha = p.alpha;
ctx.fillStyle = p.color;
ctx.shadowColor = p.color;
ctx.shadowBlur = 6;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
// Draw Floating Text
floatingTexts.forEach(t => {
ctx.save();
ctx.globalAlpha = Math.max(0, t.alpha);
ctx.fillStyle = t.color;
ctx.font = `bold ${t.size}px monospace`;
ctx.textAlign = 'center';
ctx.shadowColor = t.color;
ctx.shadowBlur = 8;
ctx.fillText(t.text, t.x, t.y);
ctx.restore();
});
// Top HUD
ctx.save();
ctx.font = '600 15px -apple-system, BlinkMacSystemFont, sans-serif';
ctx.fillStyle = '#94a3b8';
// Score
ctx.fillText('SCORE', 24, 32);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 20px monospace';
ctx.fillText(score.toString().padStart(6, '0'), 24, 54);
// High score
ctx.font = '600 13px -apple-system, BlinkMacSystemFont, sans-serif';
ctx.fillStyle = '#64748b';
ctx.fillText(`HIGH: ${highScore}`, 130, 52);
// Level
ctx.font = '600 15px -apple-system, BlinkMacSystemFont, sans-serif';
ctx.fillStyle = '#94a3b8';
ctx.textAlign = 'center';
ctx.fillText('LEVEL', W / 2, 32);
ctx.fillStyle = '#00e5ff';
ctx.font = 'bold 18px monospace';
ctx.fillText(`${currentLevelIdx + 1} / ${LEVELS.length}`, W / 2, 54);
// Lives
ctx.textAlign = 'right';
ctx.font = '600 15px -apple-system, BlinkMacSystemFont, sans-serif';
ctx.fillStyle = '#94a3b8';
ctx.fillText('LIVES', W - 24, 32);
ctx.fillStyle = '#ff3366';
let heartStr = '';
for (let l = 0; l < Math.min(lives, 5); l++) heartStr += '♥ ';
if (lives > 5) heartStr += `+${lives - 5}`;
ctx.font = 'bold 18px sans-serif';
ctx.fillText(heartStr, W - 24, 54);
// Combo indicator
if (combo > 2) {
ctx.textAlign = 'center';
ctx.font = 'bold 14px monospace';
ctx.fillStyle = '#ffea00';
ctx.fillText(`COMBO x${combo}!`, W / 2, 74);
}
ctx.restore();
// Banner message during game
if (state === 'playing' && messageTimer > 0) {
ctx.save();
const alpha = Math.min(1, messageTimer / 30);
ctx.globalAlpha = alpha;
ctx.fillStyle = '#00d4ff';
ctx.font = 'bold 22px monospace';
ctx.textAlign = 'center';
ctx.shadowColor = '#00d4ff';
ctx.shadowBlur = 12;
ctx.fillText(levelBannerText, W / 2, H / 2 - 20);
ctx.restore();
}
// Overlays for states
if (state === 'title') {
renderOverlay('BREAKOUT DELUXE', 'Press SPACE or Click to Play', '#00d4ff');
} else if (state === 'paused') {
renderOverlay('PAUSED', 'Press P to Resume', '#ffea00');
} else if (state === 'levelclear') {
renderOverlay('LEVEL COMPLETED!', 'Press SPACE or Click for Next Level', '#00e676');
} else if (state === 'gameover') {
renderOverlay('GAME OVER', `Final Score: ${score} • Press SPACE to Restart`, '#ff3366');
} else if (state === 'victory') {
renderOverlay('VICTORY!', `You Beat All Levels! Score: ${score} • SPACE to Play Again`, '#ffea00');
}
ctx.restore();
}
function renderOverlay(title, subtitle, color) {
ctx.save();
ctx.fillStyle = 'rgba(8, 10, 18, 0.82)';
ctx.fillRect(0, 0, W, H);
ctx.shadowColor = color;
ctx.shadowBlur = 25;
ctx.fillStyle = color;
ctx.font = '900 44px -apple-system, BlinkMacSystemFont, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(title, W / 2, H / 2 - 15);
ctx.shadowBlur = 0;
ctx.fillStyle = '#e2e8f0';
ctx.font = '500 16px -apple-system, BlinkMacSystemFont, sans-serif';
ctx.fillText(subtitle, W / 2, H / 2 + 35);
ctx.restore();
}
function loop(time) {
update();
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
