Run details
- Model
- deepseek/deepseek-v4.1-flash-1017
- Route
- OpenRouter API, one user message
- Reasoning
- provider default, 14,992 reasoning tokens
- Limits
- 60 min · 393,216 output tokens
- Date
- 21 Oct 2026
- Time
- 69 s
- Size
- 27 kB
- Tokens
- 106 in, 25,220 out
- List price
- $0.015 ($0.15 in / $0.6 out per M)
- Paid
- $0.030
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). Reply with the file in a single ```html code block.
Source
27 kB 973 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Neon Breakout</title>
<style>
html, body {
margin: 0; padding: 0; height: 100%; overflow: hidden;
background: #05070d;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
#wrap {
position: fixed; inset: 0;
display: flex; align-items: center; justify-content: center;
background: radial-gradient(circle at 50% 35%, #101a2e 0%, #05070d 72%);
}
canvas {
width: min(100vw, calc(100vh * 900 / 620));
height: min(100vh, calc(100vw * 620 / 900));
display: block;
border-radius: 14px;
box-shadow: 0 0 70px rgba(80,180,255,.16), 0 0 0 1px rgba(120,180,255,.12);
touch-action: none;
cursor: crosshair;
}
</style>
</head>
<body>
<div id="wrap"><canvas id="c"></canvas></div>
<script>
(() => {
'use strict';
// ---------------------------------------------------------------- setup
const W = 900, H = 620;
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
function resize() {
const dpr = Math.min(2, window.devicePixelRatio || 1);
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resize);
resize();
// ---------------------------------------------------------------- helpers
const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);
const rand = (a, b) => a + Math.random() * (b - a);
function roundRect(c, x, y, w, h, r) {
r = Math.min(r, w / 2, h / 2);
c.beginPath();
c.moveTo(x + r, y);
c.lineTo(x + w - r, y);
c.quadraticCurveTo(x + w, y, x + w, y + r);
c.lineTo(x + w, y + h - r);
c.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
c.lineTo(x + r, y + h);
c.quadraticCurveTo(x, y + h, x, y + h - r);
c.lineTo(x, y + r);
c.quadraticCurveTo(x, y, x + r, y);
c.closePath();
}
function circleRect(cx, cy, r, rx, ry, rw, rh) {
const nx = clamp(cx, rx, rx + rw);
const ny = clamp(cy, ry, ry + rh);
const dx = cx - nx, dy = cy - ny;
return dx * dx + dy * dy <= r * r;
}
// ---------------------------------------------------------------- audio
let actx = null;
function initAudio() {
if (!actx) {
try { actx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { actx = null; }
}
if (actx && actx.state === 'suspended') actx.resume();
}
function tone(freq, dur, type, vol, slideTo) {
if (!actx || game.muted) return;
const t0 = actx.currentTime;
const o = actx.createOscillator();
const g = actx.createGain();
o.type = type || 'square';
o.frequency.setValueAtTime(freq, t0);
if (slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), t0 + dur);
g.gain.setValueAtTime(0.0001, t0);
g.gain.linearRampToValueAtTime(vol || 0.05, t0 + 0.006);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
o.connect(g); g.connect(actx.destination);
o.start(t0); o.stop(t0 + dur + 0.03);
}
function chord(freqs, dur, vol) {
freqs.forEach((f, i) => setTimeout(() => tone(f, dur, 'triangle', vol || 0.05), i * 55));
}
// ---------------------------------------------------------------- levels
const COLORS = {
A: '#ff4d6d', B: '#ff8c42', C: '#ffd93d', D: '#4ade80',
E: '#38bdf8', F: '#a78bfa', G: '#f472b6'
};
const LEVELS = [
[ // 1 - warmup
"cccccccccccc",
"bbbbbbbbbbbb",
"aaaaaaaaaaaa"
],
[ // 2 - pyramid
"....dddd....",
"...dddddd...",
"..dddddddd..",
".dddddddddd.",
"dddddddddddd"
],
[ // 3 - checker
"a.a.a.a.a.a.",
"b.b.b.b.b.b.",
"c.c.c.c.c.c.",
"d.d.d.d.d.d.",
"e.e.e.e.e.e."
],
[ // 4 - diamond
".....ee.....",
"....dddd....",
"...cccccc...",
"..bbbbbbbb..",
"...cccccc...",
"....dddd...."
],
[ // 5 - fortress (uppercase = armored, 2 hits)
"gggggggggggg",
"gFggggggggFg",
"gggggggggggg",
"ggFFggggFFgg",
"gggggggggggg"
]
];
// ---------------------------------------------------------------- state
const game = {
state: 'menu',
level: 0,
score: 0,
lives: 3,
best: 0,
combo: 0,
shake: 0,
flash: 0,
stateTimer: 0,
time: 0,
muted: false
};
const paddle = {
x: W / 2, y: H - 56, w: 130, baseW: 130, h: 16,
targetX: W / 2, squash: 0, wideTimer: 0
};
let balls = [];
let bricks = [];
let particles = [];
let popups = [];
let powerups = [];
let speedFactor = 1;
const keys = { left: false, right: false };
const PU_TYPES = [
{ type: 'wide', color: '#38bdf8', letter: 'W', weight: 30 },
{ type: 'multi', color: '#f472b6', letter: 'M', weight: 30 },
{ type: 'slow', color: '#a78bfa', letter: 'S', weight: 25 },
{ type: 'life', color: '#4ade80', letter: '+', weight: 15 }
];
// ---------------------------------------------------------------- level build
const MARGIN_X = 40, TOP_Y = 96, CELL_H = 28;
function buildLevel(idx) {
bricks = [];
const rows = LEVELS[idx];
const cols = rows[0].length;
const cellW = (W - MARGIN_X * 2) / cols;
for (let r = 0; r < rows.length; r++) {
for (let c = 0; c < cols; c++) {
const ch = rows[r][c];
if (ch === '.') continue;
const strong = ch === ch.toUpperCase();
const color = COLORS[ch.toUpperCase()] || '#94a3b8';
bricks.push({
x: MARGIN_X + c * cellW + 3,
y: TOP_Y + r * CELL_H + 3,
w: cellW - 6,
h: CELL_H - 6,
color,
hp: strong ? 2 : 1,
maxHp: strong ? 2 : 1,
points: strong ? 100 : 50,
alive: true,
flash: 0
});
}
}
}
// ---------------------------------------------------------------- ball
function makeBall(x, y, stuck) {
return { x, y, r: 8, vx: 0, vy: 0, stuck: !!stuck, trail: [], dead: false };
}
function baseSpeed() { return 330 + game.level * 24; }
function resetBall() {
balls = [makeBall(paddle.x, paddle.y - paddle.h / 2 - 12, true)];
speedFactor = 1;
}
function setSpeedFactor(f) {
const ratio = f / speedFactor;
for (const b of balls) { b.vx *= ratio; b.vy *= ratio; }
speedFactor = f;
}
function launch() {
let did = false;
for (const b of balls) {
if (b.stuck) {
b.stuck = false;
const a = -Math.PI / 2 + rand(-0.28, 0.28);
const sp = baseSpeed() * speedFactor;
b.vx = Math.cos(a) * sp;
b.vy = Math.sin(a) * sp;
did = true;
}
}
if (did) tone(660, 0.09, 'square', 0.05, 1100);
return did;
}
function fixStuck(b) {
const sp = Math.hypot(b.vx, b.vy);
if (sp < 1) return;
const minVy = sp * 0.24;
if (Math.abs(b.vy) < minVy) {
b.vy = (b.vy < 0 ? -1 : 1) * minVy;
b.vx = (b.vx < 0 ? -1 : 1) * Math.sqrt(Math.max(0, sp * sp - b.vy * b.vy));
}
}
// ---------------------------------------------------------------- fx
function shakeAdd(v) { game.shake = Math.min(26, game.shake + v); }
function spawnParticles(x, y, color, n, power) {
if (particles.length > 500) return;
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2;
const sp = rand(40, 250) * power;
const life = rand(0.35, 0.9);
particles.push({
x, y,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp - 40,
s: rand(2, 6) * (0.6 + power * 0.6),
color,
rot: Math.random() * Math.PI,
vr: rand(-10, 10),
life, max: life
});
}
}
function popup(x, y, text, color) {
popups.push({ x, y, text, color, life: 0.85, max: 0.85 });
}
// ---------------------------------------------------------------- score
function comboMult() { return Math.min(5, 1 + Math.floor(game.combo / 4)); }
// ---------------------------------------------------------------- bricks
function damageBrick(br) {
br.hp--;
br.flash = 1;
const cx = br.x + br.w / 2, cy = br.y + br.h / 2;
if (br.hp <= 0) {
br.alive = false;
game.combo++;
const mult = comboMult();
const pts = br.points * mult;
game.score += pts;
spawnParticles(cx, cy, br.color, 14, 1);
popup(cx, cy, '+' + pts, mult > 1 ? '#ffd93d' : '#ffffff');
shakeAdd(3.5);
tone(420 + Math.random() * 90 + game.combo * 12, 0.07, 'square', 0.045);
maybeDropPowerup(br);
} else {
spawnParticles(cx, cy, br.color, 5, 0.5);
tone(190, 0.05, 'sawtooth', 0.03);
}
}
function maybeDropPowerup(br) {
if (Math.random() > 0.16) return;
let total = 0;
for (const p of PU_TYPES) total += p.weight;
let roll = Math.random() * total;
let pick = PU_TYPES[0];
for (const p of PU_TYPES) { roll -= p.weight; if (roll <= 0) { pick = p; break; } }
powerups.push({
x: br.x + br.w / 2, y: br.y + br.h / 2,
w: 30, h: 18, vy: 135,
type: pick.type, color: pick.color, letter: pick.letter, spin: 0
});
}
// ---------------------------------------------------------------- collision
function hitPaddle(b) {
const rel = clamp((b.x - paddle.x) / (paddle.w / 2), -1, 1);
let speed = Math.hypot(b.vx, b.vy);
speed = Math.max(speed, baseSpeed() * 0.85);
const angle = rel * 1.02;
b.vx = Math.sin(angle) * speed;
b.vy = -Math.cos(angle) * speed;
b.y = paddle.y - paddle.h / 2 - b.r - 0.5;
paddle.squash = 1;
shakeAdd(1.6);
tone(210 + rel * 70, 0.055, 'square', 0.045);
spawnParticles(b.x, b.y, '#8be9fd', 5, 0.45);
}
function stepBall(b, dt) {
b.x += b.vx * dt;
b.y += b.vy * dt;
if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx); tone(300, 0.03, 'sine', 0.025); }
else if (b.x + b.r > W) { b.x = W - b.r; b.vx = -Math.abs(b.vx); tone(300, 0.03, 'sine', 0.025); }
if (b.y - b.r < 0) { b.y = b.r; b.vy = Math.abs(b.vy); tone(300, 0.03, 'sine', 0.025); }
if (b.y - b.r > H) { b.dead = true; return; }
// paddle
const px = paddle.x - paddle.w / 2;
const py = paddle.y - paddle.h / 2;
if (b.vy > 0 && circleRect(b.x, b.y, b.r, px, py, paddle.w, paddle.h)) {
hitPaddle(b);
}
// bricks
for (let i = 0; i < bricks.length; i++) {
const br = bricks[i];
if (!br.alive) continue;
if (circleRect(b.x, b.y, b.r, br.x, br.y, br.w, br.h)) {
const bcx = br.x + br.w / 2, bcy = br.y + br.h / 2;
const ox = (b.r + br.w / 2) - Math.abs(b.x - bcx);
const oy = (b.r + br.h / 2) - Math.abs(b.y - bcy);
const sx = b.x < bcx ? -1 : 1;
const sy = b.y < bcy ? -1 : 1;
if (ox < oy) { b.vx = sx * Math.abs(b.vx); b.x += sx * ox; }
else { b.vy = sy * Math.abs(b.vy); b.y += sy * oy; }
damageBrick(br);
break;
}
}
fixStuck(b);
}
function updateBalls(dt) {
for (let i = balls.length - 1; i >= 0; i--) {
const b = balls[i];
if (b.stuck) {
b.x = paddle.x;
b.y = paddle.y - paddle.h / 2 - b.r - 2;
b.trail.length = 0;
continue;
}
const speed = Math.hypot(b.vx, b.vy);
const steps = Math.max(1, Math.ceil((speed * dt) / 5));
for (let s = 0; s < steps; s++) {
stepBall(b, dt / steps);
if (b.dead) break;
}
if (b.dead) { balls.splice(i, 1); continue; }
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 10) b.trail.shift();
}
if (balls.length === 0) loseLife();
}
// ---------------------------------------------------------------- life / level flow
function loseLife() {
game.lives--;
game.combo = 0;
shakeAdd(16);
game.flash = 0.55;
tone(180, 0.35, 'sawtooth', 0.08, 55);
setPaddleWidth(paddle.baseW);
paddle.wideTimer = 0;
speedFactor = 1;
if (game.lives <= 0) {
game.state = 'gameover';
game.stateTimer = 0.9;
game.best = Math.max(game.best, game.score);
chord([330, 262, 196], 0.25, 0.05);
} else {
resetBall();
}
}
function levelClear() {
game.state = 'levelclear';
game.stateTimer = 1.9;
game.flash = 1;
game.score += 250;
shakeAdd(10);
chord([523, 659, 784, 1046], 0.16, 0.05);
for (const br of bricks) {
if (br.alive) spawnParticles(br.x + br.w / 2, br.y + br.h / 2, br.color, 6, 1);
}
}
function nextLevel() {
if (game.level + 1 >= LEVELS.length) {
game.state = 'win';
game.stateTimer = 0.9;
game.best = Math.max(game.best, game.score);
chord([523, 659, 784, 1046, 1318], 0.2, 0.06);
} else {
loadLevel(game.level + 1);
}
}
function loadLevel(i) {
game.level = i;
buildLevel(i);
powerups = [];
game.combo = 0;
setPaddleWidth(paddle.baseW);
paddle.wideTimer = 0;
resetBall();
game.state = 'playing';
game.stateTimer = 0;
}
function startGame() {
game.score = 0;
game.lives = 3;
game.combo = 0;
game.flash = 0;
game.shake = 0;
particles = [];
popups = [];
powerups = [];
loadLevel(0);
}
// ---------------------------------------------------------------- paddle
function setPaddleWidth(w) {
paddle.w = w;
paddle.targetX = clamp(paddle.targetX, w / 2, W - w / 2);
paddle.x = clamp(paddle.x, w / 2, W - w / 2);
}
function updatePaddle(dt) {
const sp = 680;
if (keys.left) paddle.targetX -= sp * dt;
if (keys.right) paddle.targetX += sp * dt;
paddle.targetX = clamp(paddle.targetX, paddle.w / 2, W - paddle.w / 2);
paddle.x += (paddle.targetX - paddle.x) * Math.min(1, dt * 30);
paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);
}
// ---------------------------------------------------------------- powerups
function updatePowerups(dt) {
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.y += p.vy * dt;
p.spin += dt * 4;
if (p.y - p.h / 2 > H) { powerups.splice(i, 1); continue; }
const px = paddle.x - paddle.w / 2;
const py = paddle.y - paddle.h / 2;
if (p.y + p.h / 2 > py && p.y - p.h / 2 < py + paddle.h &&
p.x + p.w / 2 > px && p.x - p.w / 2 < px + paddle.w) {
applyPowerup(p);
powerups.splice(i, 1);
}
}
}
function applyPowerup(p) {
spawnParticles(p.x, p.y, p.color, 16, 1);
shakeAdd(5);
tone(880, 0.12, 'triangle', 0.06, 1400);
popup(p.x, p.y - 10, p.type.toUpperCase(), p.color);
if (p.type === 'wide') {
setPaddleWidth(paddle.baseW * 1.55);
paddle.wideTimer = 10;
} else if (p.type === 'slow') {
setSpeedFactor(0.68);
paddle.slowTimer = 8;
} else if (p.type === 'multi') {
const src = balls.filter(b => !b.stuck);
const use = src.length ? src : balls;
const candidates = use.slice(0, 2);
for (const b of candidates) {
if (balls.length >= 7) break;
for (const dir of [-0.45, 0.45]) {
if (balls.length >= 7) break;
const sp = Math.hypot(b.vx, b.vy) || baseSpeed();
const ang = Math.atan2(b.vy, b.vx) + dir;
const nb = makeBall(b.x, b.y, false);
nb.vx = Math.cos(ang) * sp;
nb.vy = Math.sin(ang) * sp;
balls.push(nb);
}
}
} else if (p.type === 'life') {
game.lives = Math.min(9, game.lives + 1);
}
}
function updatePowerTimers(dt) {
if (paddle.wideTimer > 0) {
paddle.wideTimer -= dt;
if (paddle.wideTimer <= 0) { setPaddleWidth(paddle.baseW); paddle.wideTimer = 0; }
}
if (paddle.slowTimer > 0) {
paddle.slowTimer -= dt;
if (paddle.slowTimer <= 0) { setSpeedFactor(1); paddle.slowTimer = 0; }
}
}
// ---------------------------------------------------------------- particles & popups
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.life <= 0) { particles.splice(i, 1); continue; }
p.vy += 720 * dt;
p.vx *= (1 - 1.7 * dt);
p.x += p.vx * dt;
p.y += p.vy * dt;
p.rot += p.vr * dt;
}
}
function updatePopups(dt) {
for (let i = popups.length - 1; i >= 0; i--) {
const p = popups[i];
p.life -= dt;
p.y -= 42 * dt;
if (p.life <= 0) popups.splice(i, 1);
}
}
// ---------------------------------------------------------------- update
function update(dt) {
game.time += dt;
game.shake = Math.max(0, game.shake - dt * 42);
game.flash = Math.max(0, game.flash - dt * 3.2);
paddle.squash = Math.max(0, paddle.squash - dt * 5);
for (const br of bricks) if (br.flash > 0) br.flash = Math.max(0, br.flash - dt * 6);
updateParticles(dt);
updatePopups(dt);
if (game.state === 'playing') {
updatePaddle(dt);
updateBalls(dt);
updatePowerups(dt);
updatePowerTimers(dt);
if (!bricks.some(b => b.alive)) levelClear();
} else if (game.state === 'levelclear') {
game.stateTimer -= dt;
if (game.stateTimer <= 0) nextLevel();
} else if (game.state === 'gameover' || game.state === 'win') {
game.stateTimer -= dt;
} else if (game.state === 'paused') {
// frozen
}
}
// ---------------------------------------------------------------- drawing
function drawBackground() {
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, '#0a1020');
g.addColorStop(1, '#05070d');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = 'rgba(80,140,255,0.055)';
ctx.lineWidth = 1;
const gs = 45;
const off = (game.time * 12) % gs;
for (let x = -gs + off; x < W + gs; x += gs) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y < H; y += gs) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
const rg = ctx.createRadialGradient(W / 2, 0, 0, W / 2, 0, 520);
rg.addColorStop(0, 'rgba(60,130,255,0.13)');
rg.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = rg;
ctx.fillRect(0, 0, W, H);
}
function drawBricks() {
for (const br of bricks) {
if (!br.alive) continue;
ctx.fillStyle = br.color;
roundRect(ctx, br.x, br.y, br.w, br.h, 5);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.26)';
roundRect(ctx, br.x + 2.5, br.y + 2, br.w - 5, br.h * 0.34, 3);
ctx.fill();
ctx.fillStyle = 'rgba(0,0,0,0.18)';
roundRect(ctx, br.x + 2.5, br.y + br.h - 5.5, br.w - 5, 3.5, 2);
ctx.fill();
if (br.maxHp > 1) {
ctx.strokeStyle = br.hp > 1 ? 'rgba(0,0,0,0.45)' : 'rgba(255,255,255,0.55)';
ctx.lineWidth = 2.5;
roundRect(ctx, br.x + 4, br.y + 4, br.w - 8, br.h - 8, 3);
ctx.stroke();
}
if (br.flash > 0) {
ctx.fillStyle = 'rgba(255,255,255,' + (br.flash * 0.85) + ')';
roundRect(ctx, br.x, br.y, br.w, br.h, 5);
ctx.fill();
}
}
}
function drawPaddle() {
const sq = paddle.squash;
const h = paddle.h * (1 - sq * 0.35);
const y = paddle.y - h / 2 + sq * 2;
const x = paddle.x - paddle.w / 2;
const wide = paddle.wideTimer > 0;
ctx.save();
ctx.shadowColor = wide ? '#38bdf8' : '#7dd3fc';
ctx.shadowBlur = 22 + sq * 26;
const g = ctx.createLinearGradient(0, y, 0, y + h);
g.addColorStop(0, wide ? '#bae6fd' : '#e0f2fe');
g.addColorStop(0.55, wide ? '#38bdf8' : '#7dd3fc');
g.addColorStop(1, wide ? '#0369a1' : '#0284c7');
ctx.fillStyle = g;
roundRect(ctx, x, y, paddle.w, h, h / 2);
ctx.fill();
ctx.restore();
ctx.fillStyle = 'rgba(255,255,255,0.55)';
roundRect(ctx, x + 8, y + 2.5, paddle.w - 16, h * 0.3, h * 0.15);
ctx.fill();
}
function drawBalls() {
for (const b of balls) {
for (let i = 0; i < b.trail.length; i++) {
const t = b.trail[i];
const a = (i / b.trail.length) * 0.32;
ctx.fillStyle = 'rgba(140,225,255,' + a + ')';
ctx.beginPath();
ctx.arc(t.x, t.y, b.r * (0.35 + 0.65 * (i / b.trail.length)), 0, Math.PI * 2);
ctx.fill();
}
ctx.save();
ctx.shadowColor = '#6ee7ff';
ctx.shadowBlur = 20;
const rg = ctx.createRadialGradient(b.x - b.r * 0.3, b.y - b.r * 0.3, 1, b.x, b.y, b.r);
rg.addColorStop(0, '#ffffff');
rg.addColorStop(1, '#7fd8ff');
ctx.fillStyle = rg;
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
function drawPowerups() {
for (const p of powerups) {
ctx.save();
ctx.translate(p.x, p.y);
const sc = 1 + Math.sin(p.spin) * 0.08;
ctx.scale(sc, sc);
ctx.shadowColor = p.color;
ctx.shadowBlur = 16;
ctx.fillStyle = p.color;
roundRect(ctx, -p.w / 2, -p.h / 2, p.w, p.h, 6);
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(0,0,0,0.62)';
ctx.font = 'bold 14px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(p.letter, 0, 1);
ctx.restore();
}
ctx.textBaseline = 'alphabetic';
}
function drawParticles() {
for (const p of particles) {
const a = clamp(p.life / p.max, 0, 1);
ctx.globalAlpha = a;
ctx.fillStyle = p.color;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.fillRect(-p.s / 2, -p.s / 2, p.s, p.s);
ctx.restore();
}
ctx.globalAlpha = 1;
}
function drawPopups() {
ctx.textAlign = 'center';
ctx.font = 'bold 17px ui-monospace, monospace';
for (const p of popups) {
ctx.globalAlpha = clamp(p.life / p.max, 0, 1);
ctx.fillStyle = p.color;
ctx.fillText(p.text, p.x, p.y);
}
ctx.globalAlpha = 1;
}
function drawHUD() {
ctx.textBaseline = 'alphabetic';
ctx.font = 'bold 20px ui-monospace, monospace';
ctx.textAlign = 'left';
ctx.fillStyle = '#cbd5e1';
ctx.fillText('SCORE', 26, 34);
ctx.fillStyle = '#7dd3fc';
ctx.fillText(String(game.score).padStart(5, '0'), 106, 34);
ctx.textAlign = 'center';
ctx.fillStyle = '#94a3b8';
ctx.font = 'bold 18px ui-monospace, monospace';
ctx.fillText('LEVEL ' + (game.level + 1) + ' / ' + LEVELS.length, W / 2, 34);
// lives
ctx.textAlign = 'right';
ctx.fillStyle = '#94a3b8';
ctx.font = 'bold 16px ui-monospace, monospace';
ctx.fillText('LIVES', W - 26 - game.lives * 26, 32);
for (let i = 0; i < game.lives; i++) {
const x = W - 26 - i * 26;
ctx.fillStyle = '#7dd3fc';
ctx.shadowColor = '#38bdf8';
ctx.shadowBlur = 10;
roundRect(ctx, x - 18, 22, 18, 7, 3.5);
ctx.fill();
ctx.shadowBlur = 0;
}
// combo
if (game.combo >= 4 && (game.state === 'playing' || game.state === 'paused')) {
const mult = comboMult();
const pulse = 1 + Math.sin(game.time * 10) * 0.06;
ctx.save();
ctx.translate(W / 2, 62);
ctx.scale(pulse, pulse);
ctx.textAlign = 'center';
ctx.font = 'bold 16px ui-monospace, monospace';
ctx.fillStyle = mult >= 4 ? '#ffd93d' : '#f472b6';
ctx.fillText('COMBO x' + mult, 0, 0);
ctx.restore();
}
if (game.muted) {
ctx.textAlign = 'left';
ctx.font = 'bold 13px ui-monospace, monospace';
ctx.fillStyle = '#64748b';
ctx.fillText('MUTED', 26, 58);
}
}
function centerText(text, y, size, color, glow) {
ctx.textAlign = 'center';
ctx.font = 'bold ' + size + 'px ui-monospace, monospace';
if (glow) { ctx.shadowColor = glow; ctx.shadowBlur = 24; }
ctx.fillStyle = color;
ctx.fillText(text, W / 2, y);
ctx.shadowBlur = 0;
}
function drawOverlays() {
if (game.state === 'playing') return;
ctx.fillStyle = 'rgba(4,8,16,0.74)';
ctx.fillRect(0, 0, W, H);
if (game.state === 'menu') {
const p = 0.86 + Math.sin(game.time * 3) * 0.14;
ctx.save();
ctx.shadowColor = '#38bdf8';
ctx.shadowBlur = 34 * p;
centerText('NEON', 208, 80, '#7dd3fc');
ctx.shadowColor = '#f472b6';
centerText('BREAKOUT', 288, 80, '#f472b6');
ctx.restore();
centerText('CLICK or PRESS SPACE TO START', 400, 22,
'rgba(226,232,240,' + (0.6 + Math.sin(game.time * 4) * 0.4) + ')');
ctx.font = 'bold 16px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.fillStyle = '#64748b';
ctx.fillText('MOVE: mouse / \u2190 \u2192 or A D', W / 2, 470);
ctx.fillText('LAUNCH: space or click PAUSE: P MUTE: M', W / 2, 496);
ctx.fillStyle = '#475569';
ctx.fillText('Break every brick. Grab the falling power-ups. Chain bricks for combo multipliers.', W / 2, 540);
return;
}
if (game.state === 'paused') {
centerText('PAUSED', 290, 60, '#7dd3fc', '#38bdf8');
centerText('press P to resume', 340, 20, '#94a3b8');
return;
}
if (game.state === 'levelclear') {
centerText('LEVEL CLEAR', 270, 58, '#4ade80', '#22c55e');
centerText('+250 BONUS', 320, 26, '#ffd93d');
return;
}
if (game.state === 'gameover') {
centerText('GAME OVER', 262, 66, '#ff4d6d', '#ff4d6d');
centerText('SCORE ' + game.score, 330, 30, '#e2e8f0');
centerText('BEST ' + game.best, 368, 22, '#94a3b8');
if (game.stateTimer <= 0) {
centerText('press SPACE or click to play again', 470, 20,
'rgba(226,232,240,' + (0.55 + Math.sin(game.time * 4) * 0.45) + ')');
}
return;
}
if (game.state === 'win') {
const p = 0.9 + Math.sin(game.time * 5) * 0.1;
ctx.save();
ctx.shadowColor = '#ffd93d';
ctx.shadowBlur = 30 * p;
centerText('YOU WIN!', 260, 72, '#ffd93d');
ctx.restore();
centerText('ALL ' + LEVELS.length + ' LEVELS CLEARED', 320, 26, '#4ade80');
centerText('SCORE ' + game.score, 380, 30, '#e2e8f0');
if (game.stateTimer <= 0) {
centerText('press SPACE or click to play again', 470, 20,
'rgba(226,232,240,' + (0.55 + Math.sin(game.time * 4) * 0.45) + ')');
}
return;
}
}
function render() {
ctx.save();
if (game.shake > 0.15) {
ctx.translate((Math.random() - 0.5) * game.shake, (Math.random() - 0.5) * game.shake);
}
drawBackground();
drawBricks();
drawPowerups();
drawPaddle();
drawBalls();
drawParticles();
drawPopups();
ctx.restore();
if (game.flash > 0.01) {
ctx.fillStyle = 'rgba(255,255,255,' + (game.flash * 0.35) + ')';
ctx.fillRect(0, 0, W, H);
}
drawHUD();
drawOverlays();
}
// ---------------------------------------------------------------- input
function primaryAction() {
initAudio();
if (game.state === 'menu') {
startGame();
} else if (game.state === 'playing') {
launch();
} else if (game.state === 'paused') {
game.state = 'playing';
} else if ((game.state === 'gameover' || game.state === 'win') && game.stateTimer <= 0) {
startGame();
}
}
function togglePause() {
if (game.state === 'playing') game.state = 'paused';
else if (game.state === 'paused') game.state = 'playing';
}
window.addEventListener('keydown', (e) => {
const k = e.key;
if (k === 'ArrowLeft' || k === 'a' || k === 'A') { keys.left = true; e.preventDefault(); }
else if (k === 'ArrowRight' || k === 'd' || k === 'D') { keys.right = true; e.preventDefault(); }
else if (k === ' ' || k === 'Spacebar') { e.preventDefault(); primaryAction(); }
else if (k === 'p' || k === 'P' || k === 'Escape') { e.preventDefault(); togglePause(); }
else if (k === 'm' || k === 'M') { game.muted = !game.muted; }
initAudio();
});
window.addEventListener('keyup', (e) => {
const k = e.key;
if (k === 'ArrowLeft' || k === 'a' || k === 'A') keys.left = false;
if (k === 'ArrowRight' || k === 'd' || k === 'D') keys.right = false;
});
function pointerX(clientX) {
const r = canvas.getBoundingClientRect();
return clamp(((clientX - r.left) / r.width) * W, 0, W);
}
canvas.addEventListener('mousemove', (e) => {
paddle.targetX = clamp(pointerX(e.clientX), paddle.w / 2, W - paddle.w / 2);
});
canvas.addEventListener('mousedown', (e) => {
e.preventDefault();
paddle.targetX = clamp(pointerX(e.clientX), paddle.w / 2, W - paddle.w / 2);
primaryAction();
});
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
initAudio();
const t = e.touches[0];
if (t) paddle.targetX = clamp(pointerX(t.clientX), paddle.w / 2, W - paddle.w / 2);
primaryAction();
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const t = e.touches[0];
if (t) paddle.targetX = clamp(pointerX(t.clientX), paddle.w / 2, W - paddle.w / 2);
}, { passive: false });
// ---------------------------------------------------------------- loop
let last = performance.now();
function frame(now) {
let dt = (now - last) / 1000;
last = now;
if (dt > 1 / 25) dt = 1 / 25;
if (dt < 0) dt = 0;
update(dt);
render();
requestAnimationFrame(frame);
}
// initial idle scene
buildLevel(0);
resetBall();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

