Run details
- Model
- anthropic/claude-haiku-5.5
- Route
- OpenRouter API, one user message
- Reasoning
- provider default, 22,871 reasoning tokens
- Limits
- 60 min · 128,000 output tokens
- Date
- 21 Oct 2026
- Time
- 2 min 21 s
- Size
- 23 kB
- Tokens
- 118 in, 35,658 out
- List price
- $0.018 ($0.1 in / $0.5 out per M)
- Paid
- $0.018
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
23 kB 735 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Breakout Deluxe</title>
<style>
html, body {
margin: 0; height: 100%;
background: #05061a;
color: #eee;
font-family: monospace;
overflow: hidden;
}
body { display: flex; align-items: center; justify-content: center; }
#wrap {
position: relative;
width: min(94vw, calc((100vh - 48px) * 4 / 3));
aspect-ratio: 4 / 3;
}
canvas {
width: 100%; height: 100%; display: block;
border-radius: 10px;
box-shadow: 0 0 40px #4a2cff66, 0 0 0 2px #2a2f6e;
cursor: none;
touch-action: none;
}
#help {
position: absolute; left: 0; right: 0; bottom: -30px;
text-align: center; font-size: 13px; opacity: .55;
}
</style>
</head>
<body>
<div id="wrap">
<canvas id="game"></canvas>
<div id="help">Mouse / touch or ← → / A D to move · Click / Space to launch · P / Esc to pause</div>
</div>
<script>
(() => {
"use strict";
// ---------- Setup ----------
const W = 800, H = 600;
const cv = document.getElementById("game");
const ctx = cv.getContext("2d");
cv.width = W; cv.height = H;
const COLS = 12, BW = 60, BH = 22, GAP = 6;
const GX = (W - (COLS * (BW + GAP) - GAP)) / 2;
const GY = 86;
const PY = H - 44, PH = 14, PSPEED = 760;
const BASE_SPEED = 380;
const TYPES = {
"1": { hp: 1, pts: 10 },
"2": { hp: 2, pts: 25 },
"3": { hp: 3, pts: 60 },
"X": { hp: Infinity, pts: 0 } // indestructible steel
};
const LEVELS = [
{ name: "Warm-Up", rows: [
"111111111111",
"111111111111",
"222222222222",
"222222222222",
"111111111111"
]},
{ name: "Diamonds", rows: [
"....33......",
"...2112.....",
"..213312....",
"..213312....",
"...2112.....",
"....33......"
]},
{ name: "Checkers", rows: [
"1.1.1.1.1.1.",
".2.2.2.2.2.2",
"1.1.1.1.1.1.",
".2.2.2.2.2.2",
"1X1X1X1X1X1X"
]},
{ name: "Fortress", rows: [
"XXXXXXXXXXXX",
"X2222222222X",
"X2111111112X",
"X2113333112X",
"X2113333112X",
"X2111111112X",
"X2222222222X",
"XXXXX..XXXXX"
]},
{ name: "Invaders", rows: [
"..2......2..",
"...222222...",
"..22X22X22..",
".22222222222",
"2.2.2.2.2.2.",
"1.1.1.1.1.1."
]}
];
const PCOL = { W: "#41f5a0", M: "#ff5fd2", S: "#ffd447", L: "#ff4d6d" };
const PNAME = { W: "WIDE", M: "MULTI", S: "SLOW", L: "1UP" };
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const rand = (a, b) => a + Math.random() * (b - a);
// ---------- State ----------
const game = {
state: "menu", level: 0, score: 0, lives: 3, combo: 0,
shake: 0, hitstop: 0, flash: 0, stateT: 0, high: 0
};
const paddle = { x: W / 2, target: W / 2, w: 104, squash: 0 };
let balls = [], bricks = [], powers = [], parts = [], texts = [];
let wideT = 0, slowT = 0;
const keys = {};
const stars = Array.from({ length: 80 }, () => ({ x: rand(0, W), y: rand(0, H), z: rand(0.2, 1) }));
try { game.high = +localStorage.getItem("breakout-deluxe-high") || 0; } catch (e) {}
// ---------- Audio (WebAudio, synthesized, no files) ----------
let actx = null;
function initAudio() {
if (!actx) {
try { actx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { return; }
}
if (actx.state === "suspended") actx.resume();
}
function tone(f, d = 0.08, type = "square", v = 0.05, slide = 1) {
if (!actx) return;
const t = actx.currentTime;
const o = actx.createOscillator(), g = actx.createGain();
o.type = type;
o.frequency.setValueAtTime(f, t);
if (slide !== 1) o.frequency.exponentialRampToValueAtTime(f * slide, t + d);
g.gain.setValueAtTime(v, t);
g.gain.exponentialRampToValueAtTime(0.0008, t + d);
o.connect(g); g.connect(actx.destination);
o.start(t); o.stop(t + d + 0.03);
}
function noise(d = 0.12, v = 0.05) {
if (!actx) return;
const n = Math.floor(actx.sampleRate * d);
const buf = actx.createBuffer(1, n, actx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < n; i++) data[i] = (Math.random() * 2 - 1) * (1 - i / n);
const s = actx.createBufferSource(), g = actx.createGain();
g.gain.value = v;
s.buffer = buf; s.connect(g); g.connect(actx.destination); s.start();
}
const sfxPaddle = () => tone(220, 0.07, "triangle", 0.09, 1.4);
const sfxWall = () => tone(170, 0.04, "sine", 0.04);
const sfxHit = (combo) => tone(440 * Math.pow(2, Math.min(combo, 24) / 12), 0.09, "square", 0.04);
const sfxBreak = (combo) => { tone(440 * Math.pow(2, Math.min(combo, 24) / 12), 0.14, "triangle", 0.09, 1.8); noise(0.1, 0.05); };
const sfxSteel = () => { tone(120, 0.08, "sawtooth", 0.05); noise(0.05, 0.03); };
const sfxLoss = () => tone(340, 0.6, "sawtooth", 0.08, 0.2);
const sfxPower = () => [523, 659, 784].forEach((f, i) => setTimeout(() => tone(f, 0.1, "triangle", 0.08), i * 70));
const sfxClear = () => [523, 659, 784, 1046].forEach((f, i) => setTimeout(() => tone(f, 0.18, "square", 0.06), i * 110));
// ---------- Helpers ----------
function makeBall(attached, x, y, vx = 0, vy = 0) {
return { x, y, vx, vy, r: 7, attached, trail: [], dead: false };
}
function ballSpeed() {
return (BASE_SPEED + game.level * 22) * (slowT > 0 ? 0.65 : 1);
}
function mult() { return Math.min(5, 1 + Math.floor(game.combo / 5)); }
function burst(x, y, color, n, speed = 260) {
for (let i = 0; i < n; i++) {
const a = rand(0, Math.PI * 2), s = rand(speed * 0.2, speed), life = rand(0.4, 0.9);
parts.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, life, max: life,
color, size: rand(2, 5), g: 300 });
}
}
function popup(x, y, text, color) {
texts.push({ x, y, text, life: 1, color });
}
function colorFor(ch, r) {
if (ch === "1") return `hsl(${200 + r * 18}, 85%, 58%)`;
if (ch === "2") return `hsl(${265 + r * 12}, 70%, 58%)`;
if (ch === "3") return "#ffd54a";
return "#525c70";
}
function loadLevel(i) {
const L = LEVELS[i];
bricks = [];
L.rows.forEach((row, r) => {
for (let c = 0; c < COLS; c++) {
const ch = row[c];
if (!ch || ch === ".") continue;
const def = TYPES[ch];
bricks.push({
x: GX + c * (BW + GAP), y: GY + r * (BH + GAP), w: BW, h: BH,
t: ch, hp: def.hp, max: def.hp, pts: def.pts,
color: colorFor(ch, r), flash: 0, alive: true
});
}
});
balls = [makeBall(true, paddle.x, PY - 8)];
powers = [];
wideT = slowT = 0;
game.combo = 0;
}
function startGame() {
game.level = 0; game.score = 0; game.lives = 3; game.combo = 0;
paddle.w = 104;
loadLevel(0);
game.state = "play"; game.stateT = 0;
}
function saveHigh() {
if (game.score > game.high) {
game.high = game.score;
try { localStorage.setItem("breakout-deluxe-high", String(game.high)); } catch (e) {}
}
}
function togglePause() {
if (game.state === "play") { game.state = "pause"; }
else if (game.state === "pause") { game.state = "play"; }
}
function launchBalls() {
let any = false;
for (const b of balls) {
if (!b.attached) continue;
b.attached = false;
const a = rand(-0.35, 0.35), sp = ballSpeed();
b.vx = Math.sin(a) * sp;
b.vy = -Math.cos(a) * sp;
any = true;
}
if (any) tone(523, 0.06, "square", 0.05);
}
function action() {
switch (game.state) {
case "menu": startGame(); break;
case "play": if (balls.some(b => b.attached)) launchBalls(); break;
case "pause": togglePause(); break;
case "over": case "win": if (game.stateT > 0.8) startGame(); break;
}
}
function loseLife() {
game.lives--;
game.combo = 0;
game.shake = 14; game.flash = 1;
sfxLoss();
burst(paddle.x, PY, "#ff5577", 30, 320);
if (game.lives <= 0) {
game.state = "over"; game.stateT = 0; saveHigh();
} else {
balls = [makeBall(true, paddle.x, PY - 8)];
powers = []; wideT = slowT = 0;
}
}
function spawnClones() {
const moving = balls.filter(b => !b.attached && !b.dead);
for (const b of moving) {
for (const ang of [-0.5, 0.5]) {
if (balls.length >= 8) return;
const c = Math.cos(ang), s = Math.sin(ang);
balls.push(makeBall(false, b.x, b.y, b.vx * c - b.vy * s, b.vx * s + b.vy * c));
}
}
}
function applyPower(type) {
const col = PCOL[type];
if (type === "W") { wideT = 12; popup(paddle.x, PY - 30, "WIDE!", col); }
if (type === "M") { spawnClones(); popup(paddle.x, PY - 30, "MULTI!", col); }
if (type === "S") { slowT = 8; popup(paddle.x, PY - 30, "SLOW", col); }
if (type === "L") { game.lives = Math.min(9, game.lives + 1); popup(paddle.x, PY - 30, "1UP!", col); }
sfxPower();
burst(paddle.x, PY - 10, col, 16, 200);
}
// ---------- Collision & brick hits ----------
function hitBrick(br, x, y) {
if (br.t === "X") {
sfxSteel();
burst(x, y, "#c0c8d8", 6, 180);
game.shake = Math.max(game.shake, 2);
br.flash = 0.2;
return;
}
br.hp--;
br.flash = 1;
game.combo++;
if (br.hp > 0) {
sfxHit(game.combo);
burst(x, y, br.color, 6, 200);
game.shake = Math.max(game.shake, 3);
return;
}
br.alive = false;
const cx = br.x + br.w / 2, cy = br.y + br.h / 2;
const pts = br.pts * mult();
game.score += pts;
burst(cx, cy, br.color, 26, 340);
popup(cx, cy, `+${pts}`, "#fff");
game.shake = Math.max(game.shake, 6);
game.hitstop = 0.035;
sfxBreak(game.combo);
if (Math.random() < 0.15) {
const types = ["W", "W", "M", "S", "L"];
powers.push({ x: cx, y: cy, vy: 150, t: 0, type: types[Math.floor(Math.random() * types.length)] });
}
if (!bricks.some(b => b.alive && b.t !== "X")) {
const bonus = 500 + game.lives * 200;
game.score += bonus;
game.state = "clear"; game.stateT = 0;
popup(W / 2, H / 2 - 40, `+${bonus} BONUS`, "#ffd54a");
sfxClear();
saveHigh();
}
}
function stepBall(b, h) {
b.x += b.vx * h; b.y += b.vy * h;
// walls
if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx); sfxWall(); }
if (b.x + b.r > W) { b.x = W - b.r; b.vx = -Math.abs(b.vx); sfxWall(); }
if (b.y - b.r < 0) { b.y = b.r; b.vy = Math.abs(b.vy); sfxWall(); }
// paddle
const pw = paddle.w / 2;
if (b.vy > 0 && b.y + b.r >= PY && b.y - b.r <= PY + PH &&
b.x > paddle.x - pw - b.r && b.x < paddle.x + pw + b.r) {
b.y = PY - b.r;
const t = clamp((b.x - paddle.x) / pw, -1, 1);
const ang = t * 1.05; // up to ~60 degrees off vertical
const sp = ballSpeed();
b.vx = sp * Math.sin(ang);
b.vy = -sp * Math.cos(ang);
game.combo = game.combo; // combo persists while rallying
paddle.squash = 1;
sfxPaddle();
burst(b.x, PY, "#bff6ff", 4, 120);
return;
}
// bricks
for (const br of bricks) {
if (!br.alive) continue;
const cx = clamp(b.x, br.x, br.x + br.w);
const cy = clamp(b.y, br.y, br.y + br.h);
const dx = b.x - cx, dy = b.y - cy;
if (dx * dx + dy * dy >= b.r * b.r) continue;
const penL = b.x + b.r - br.x;
const penR = br.x + br.w - (b.x - b.r);
const penT = b.y + b.r - br.y;
const penB = br.y + br.h - (b.y - b.r);
const m = Math.min(penL, penR, penT, penB);
if (m === penL) { b.vx = -Math.abs(b.vx); b.x = br.x - b.r; }
else if (m === penR) { b.vx = Math.abs(b.vx); b.x = br.x + br.w + b.r; }
else if (m === penT) { b.vy = -Math.abs(b.vy); b.y = br.y - b.r; }
else { b.vy = Math.abs(b.vy); b.y = br.y + br.h + b.r; }
hitBrick(br, cx, cy);
return;
}
}
// ---------- Update ----------
function update(dt) {
game.stateT += dt;
game.shake = Math.max(0, game.shake - dt * 30);
game.flash = Math.max(0, game.flash - dt * 2.5);
paddle.squash = Math.max(0, paddle.squash - dt * 6);
for (const s of stars) {
s.y += dt * (15 + s.z * 60);
if (s.y > H) { s.y = 0; s.x = rand(0, W); }
}
for (const br of bricks) br.flash = Math.max(0, br.flash - dt * 6);
for (const p of parts) { p.life -= dt; p.vy += p.g * dt; p.x += p.vx * dt; p.y += p.vy * dt; }
parts = parts.filter(p => p.life > 0);
for (const t of texts) { t.life -= dt; t.y -= dt * 40; }
texts = texts.filter(t => t.life > 0);
// paddle control (keyboard target + smoothed mouse target)
const dir = ((keys.ArrowLeft || keys.KeyA) ? -1 : 0) + ((keys.ArrowRight || keys.KeyD) ? 1 : 0);
if (dir) paddle.target += dir * PSPEED * dt;
paddle.target = clamp(paddle.target, 0, W);
paddle.x += (paddle.target - paddle.x) * (1 - Math.exp(-dt * 22));
paddle.w += ((wideT > 0 ? 170 : 104) - paddle.w) * Math.min(1, dt * 10);
paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);
if (game.state === "play") {
let pdt = dt;
if (game.hitstop > 0) { game.hitstop -= dt; pdt = 0; }
wideT = Math.max(0, wideT - dt);
slowT = Math.max(0, slowT - dt);
for (const b of balls) {
if (b.attached) { b.x = paddle.x; b.y = PY - b.r - 1; b.trail.length = 0; continue; }
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 12) b.trail.shift();
// keep speed near target and avoid near-horizontal stalls
const sp = ballSpeed();
const m = Math.hypot(b.vx, b.vy) || 1;
const nm = m + (sp - m) * Math.min(1, dt * 2);
b.vx *= nm / m; b.vy *= nm / m;
const minVy = sp * 0.25;
if (Math.abs(b.vy) < minVy) {
b.vy = (b.vy < 0 ? -1 : 1) * minVy;
b.vx = Math.sign(b.vx || 1) * Math.sqrt(Math.max(0, sp * sp - minVy * minVy));
}
const dist = Math.hypot(b.vx, b.vy) * pdt;
const n = Math.max(1, Math.ceil(dist / 3));
for (let i = 0; i < n && game.state === "play" && !b.dead; i++) stepBall(b, pdt / n);
if (b.y - b.r > H) b.dead = true;
}
balls = balls.filter(b => !b.dead);
if (balls.length === 0 && game.state === "play") loseLife();
for (const p of powers) {
p.t += dt;
p.y += p.vy * pdt;
if (p.y > PY + 30) p.dead = true;
else if (p.y + 12 >= PY && p.y - 12 <= PY + PH && Math.abs(p.x - paddle.x) < paddle.w / 2 + 12) {
p.dead = true;
applyPower(p.type);
}
}
powers = powers.filter(p => !p.dead);
} else {
for (const b of balls) if (b.attached) { b.x = paddle.x; b.y = PY - b.r - 1; }
for (const p of powers) p.t += dt;
}
if (game.state === "clear" && game.stateT > 2.8) {
if (game.level + 1 < LEVELS.length) {
game.level++;
loadLevel(game.level);
game.state = "play"; game.stateT = 0;
} else {
game.state = "win"; game.stateT = 0;
}
}
}
// ---------- Drawing ----------
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function drawBrick(b) {
if (!b.alive) return;
const { x, y, w, h } = b;
ctx.save();
if (b.t === "3") { ctx.shadowColor = "#ffd54a"; ctx.shadowBlur = 14; }
ctx.fillStyle = b.t === "X" ? "#525c70" : b.color;
ctx.fillRect(x, y, w, h);
ctx.shadowBlur = 0;
ctx.fillStyle = "rgba(255,255,255,0.35)"; ctx.fillRect(x, y, w, 3);
ctx.fillStyle = "rgba(0,0,0,0.35)"; ctx.fillRect(x, y + h - 4, w, 4);
if (b.t === "X") {
ctx.save();
ctx.beginPath(); ctx.rect(x, y, w, h); ctx.clip();
ctx.strokeStyle = "rgba(0,0,0,0.35)"; ctx.lineWidth = 2;
for (let i = -h; i < w; i += 8) {
ctx.beginPath(); ctx.moveTo(x + i, y + h); ctx.lineTo(x + i + h, y); ctx.stroke();
}
ctx.restore();
} else if (b.hp < b.max) {
ctx.strokeStyle = "rgba(0,0,0,0.55)"; ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(x + w * 0.3, y); ctx.lineTo(x + w * 0.45, y + h * 0.6); ctx.lineTo(x + w * 0.4, y + h);
ctx.stroke();
}
if (b.flash > 0) {
ctx.fillStyle = `rgba(255,255,255,${b.flash * 0.8})`;
ctx.fillRect(x, y, w, h);
}
ctx.restore();
}
function drawPower(p) {
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(Math.sin(p.t * 4) * 0.2);
ctx.shadowColor = PCOL[p.type]; ctx.shadowBlur = 14;
ctx.fillStyle = PCOL[p.type];
ctx.beginPath(); ctx.arc(0, 0, 12, 0, Math.PI * 2); ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = "#06081a";
ctx.font = "bold 13px monospace";
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.fillText(p.type, 0, 1);
ctx.restore();
}
function drawPaddle() {
const w = paddle.w, h = PH + paddle.squash * 4;
const x = paddle.x - w / 2, y = PY - (h - PH) / 2;
ctx.save();
ctx.shadowColor = "#4de1ff"; ctx.shadowBlur = 18;
const g = ctx.createLinearGradient(0, y, 0, y + h);
g.addColorStop(0, "#d8fbff"); g.addColorStop(1, "#2a7fff");
ctx.fillStyle = g;
roundRect(x, y, w, h, 7); ctx.fill();
ctx.restore();
}
function drawBall(b) {
const n = b.trail.length;
for (let i = 0; i < n; i++) {
const t = b.trail[i];
ctx.fillStyle = `rgba(180,240,255,${((i + 1) / n) * 0.3})`;
ctx.beginPath(); ctx.arc(t.x, t.y, b.r * (i + 1) / n, 0, Math.PI * 2); ctx.fill();
}
ctx.save();
ctx.shadowColor = "#8ff"; ctx.shadowBlur = 16;
ctx.fillStyle = "#fff";
ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill();
ctx.restore();
}
function drawHUD() {
ctx.save();
ctx.fillStyle = "rgba(0,0,0,0.4)";
ctx.fillRect(0, 0, W, 62);
ctx.textBaseline = "middle";
ctx.textAlign = "left";
ctx.font = "bold 20px monospace"; ctx.fillStyle = "#fff";
ctx.fillText("SCORE " + String(game.score).padStart(6, "0"), 14, 22);
ctx.font = "13px monospace"; ctx.fillStyle = "#9fb0ff";
ctx.fillText("HI " + String(game.high).padStart(6, "0"), 14, 46);
ctx.textAlign = "center";
ctx.font = "bold 18px monospace"; ctx.fillStyle = "#fff";
ctx.fillText(`LEVEL ${game.level + 1} ${LEVELS[game.level].name.toUpperCase()}`, W / 2, 22);
if (game.combo >= 2) {
ctx.fillStyle = `hsl(${(game.stateT * 300) % 360},100%,70%)`;
ctx.font = "bold 14px monospace";
ctx.fillText(`COMBO ${game.combo} x${mult()}`, W / 2, 46);
}
// lives as tiny balls
ctx.fillStyle = "#fff"; ctx.shadowColor = "#8ff"; ctx.shadowBlur = 8;
for (let i = 0; i < game.lives; i++) {
ctx.beginPath(); ctx.arc(W - 22 - i * 20, 22, 6, 0, Math.PI * 2); ctx.fill();
}
ctx.shadowBlur = 0;
// powerup timers
const parts2 = [];
if (wideT > 0) parts2.push({ t: `WIDE ${wideT.toFixed(1)}`, c: PCOL.W });
if (slowT > 0) parts2.push({ t: `SLOW ${slowT.toFixed(1)}`, c: PCOL.S });
ctx.textAlign = "right"; ctx.font = "13px monospace";
let xr = W - 14;
for (const p of parts2.reverse()) {
ctx.fillStyle = p.c;
ctx.fillText(p.t, xr, 46);
xr -= ctx.measureText(p.t).width + 16;
}
ctx.restore();
}
function bigText(text, y, size, color) {
ctx.font = `bold ${size}px monospace`;
ctx.fillStyle = color;
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.fillText(text, W / 2, y);
}
function drawOverlay() {
const st = game.state;
if (st === "menu") {
ctx.fillStyle = "rgba(5,6,20,0.7)"; ctx.fillRect(0, 0, W, H);
const hue = (game.stateT * 60) % 360;
bigText("BREAKOUT", H / 2 - 130, 64, `hsl(${hue},100%,65%)`);
bigText("D E L U X E", H / 2 - 80, 22, "#9fb0ff");
ctx.font = "16px monospace"; ctx.fillStyle = "#ddd";
ctx.textAlign = "center";
const lines = [
"Mouse / touch or ← → / A D : move paddle",
"Click / Space / Enter : launch ball",
"P or Esc : pause",
"",
"Yellow = 3 hits · Purple = 2 hits · Grey = unbreakable",
"Catch the falling capsules: W wide · M multi · S slow · L 1UP"
];
lines.forEach((l, i) => ctx.fillText(l, W / 2, H / 2 - 20 + i * 24));
if (Math.sin(game.stateT * 4) > -0.3) bigText("PRESS ENTER, SPACE OR CLICK", H / 2 + 180, 18, "#fff");
if (game.high > 0) bigText(`HIGH SCORE ${game.high}`, H / 2 + 215, 16, "#ffd54a");
} else if (st === "pause") {
ctx.fillStyle = "rgba(5,6,20,0.55)"; ctx.fillRect(0, 0, W, H);
bigText("PAUSED", H / 2, 56, "#fff");
bigText("P / Esc / Click to resume", H / 2 + 50, 18, "#9fb0ff");
} else if (st === "clear") {
ctx.fillStyle = "rgba(5,6,20,0.35)"; ctx.fillRect(0, 0, W, H);
const s = 1 + Math.max(0, 0.3 - game.stateT) * 2;
ctx.save(); ctx.translate(W / 2, H / 2); ctx.scale(s, s); ctx.translate(-W / 2, -H / 2);
bigText("LEVEL CLEAR!", H / 2, 52, "#7dffb0");
ctx.restore();
} else if (st === "over") {
ctx.fillStyle = "rgba(20,0,10,0.7)"; ctx.fillRect(0, 0, W, H);
bigText("GAME OVER", H / 2 - 40, 60, "#ff5577");
bigText(`SCORE ${game.score}`, H / 2 + 20, 26, "#fff");
bigText(`HIGH ${game.high}`, H / 2 + 56, 18, "#ffd54a");
if (game.stateT > 0.8) bigText("PRESS ENTER / SPACE / CLICK TO RETRY", H / 2 + 110, 16, "#ddd");
} else if (st === "win") {
ctx.fillStyle = "rgba(5,6,20,0.7)"; ctx.fillRect(0, 0, W, H);
bigText("YOU WIN!", H / 2 - 40, 64, `hsl(${(game.stateT * 120) % 360},100%,65%)`);
bigText(`FINAL SCORE ${game.score}`, H / 2 + 20, 26, "#fff");
bigText(`HIGH ${game.high}`, H / 2 + 56, 18, "#ffd54a");
if (game.stateT > 0.8) bigText("PRESS ENTER / SPACE / CLICK TO PLAY AGAIN", H / 2 + 110, 16, "#ddd");
}
if (st === "play" && balls.some(b => b.attached)) {
if (Math.sin(game.stateT * 5) > -0.2)
bigText("CLICK / SPACE TO LAUNCH", PY - 50, 16, "#cfe");
}
}
function draw() {
ctx.save();
if (game.shake > 0) ctx.translate(rand(-game.shake, game.shake), rand(-game.shake, game.shake));
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, "#0c0f2e");
g.addColorStop(1, "#1d0b2e");
ctx.fillStyle = g;
ctx.fillRect(-20, -20, W + 40, H + 40);
for (const s of stars) {
ctx.globalAlpha = 0.25 + s.z * 0.75;
ctx.fillStyle = "#fff";
ctx.fillRect(s.x, s.y, s.z * 2, s.z * 2);
}
ctx.globalAlpha = 1;
bricks.forEach(drawBrick);
powers.forEach(drawPower);
drawPaddle();
balls.forEach(drawBall);
for (const p of parts) {
ctx.globalAlpha = Math.max(0, p.life / p.max);
ctx.fillStyle = p.color;
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
}
ctx.globalAlpha = 1;
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.font = "bold 16px monospace";
for (const t of texts) {
ctx.globalAlpha = Math.min(1, t.life * 1.5);
ctx.fillStyle = t.color;
ctx.fillText(t.text, t.x, t.y);
}
ctx.globalAlpha = 1;
ctx.restore();
drawHUD();
if (game.flash > 0) {
ctx.fillStyle = `rgba(255,40,80,${game.flash * 0.35})`;
ctx.fillRect(0, 0, W, H);
}
if (game.lives === 1 && game.state === "play") {
const a = 0.12 + 0.08 * Math.sin(game.stateT * 6);
ctx.strokeStyle = `rgba(255,50,80,${a})`;
ctx.lineWidth = 10;
ctx.strokeRect(5, 5, W - 10, H - 10);
}
drawOverlay();
}
// ---------- Input ----------
const CODES = ["Space", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"];
window.addEventListener("keydown", e => {
if (CODES.includes(e.code)) e.preventDefault();
initAudio();
keys[e.code] = true;
if (e.repeat) return;
if (e.code === "Enter" || e.code === "Space") action();
else if (e.code === "KeyP" || e.code === "Escape") togglePause();
});
window.addEventListener("keyup", e => { keys[e.code] = false; });
window.addEventListener("blur", () => {
for (const k in keys) keys[k] = false;
if (game.state === "play") game.state = "pause";
});
function toCanvasX(e) {
const r = cv.getBoundingClientRect();
return (e.clientX - r.left) * W / r.width;
}
cv.addEventListener("pointermove", e => { paddle.target = toCanvasX(e); });
cv.addEventListener("pointerdown", e => {
initAudio();
paddle.target = toCanvasX(e);
action();
});
// ---------- Loop ----------
loadLevel(0);
let last = performance.now();
function frame(now) {
const dt = Math.min(0.05, (now - last) / 1000);
last = now;
update(dt);
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

