Run details
- Model
- stepfun/step-5-preview
- Route
- OpenRouter API, one user message
- Reasoning
- provider default, 15,621 reasoning tokens
- Limits
- 60 min · 64,000 output tokens
- Date
- 21 Oct 2026
- Time
- 1 min 55 s
- Size
- 26 kB
- Tokens
- 87 in, 25,139 out
- List price
- $0.068 ($1 in / $2.7 out per M)
- Paid
- $0.068
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
26 kB 1,008 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Breakout</title>
<style>
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: #03030c; }
body {
font-family: "Courier New", Courier, monospace;
color: #dff6fa;
min-height: 100vh;
display: flex;
justify-content: center;
background:
radial-gradient(circle at 20% 0%, rgba(0,232,255,.12), transparent 55%),
radial-gradient(circle at 85% 100%, rgba(255,43,214,.12), transparent 50%),
#03030c;
user-select: none;
}
#wrap { width: 100%; max-width: 860px; padding: 14px; }
h1 {
margin: 4px 0 12px;
text-align: center;
font-size: clamp(20px, 4vw, 34px);
letter-spacing: 5px;
color: #00e8ff;
text-shadow: 0 0 10px #00e8ff, 0 0 26px rgba(0,232,255,.45);
}
#hud {
display: flex;
gap: 8px;
margin-bottom: 10px;
font-weight: bold;
text-align: center;
}
#hud div {
flex: 1;
border: 1px solid rgba(0,232,255,.28);
background: rgba(10,10,28,.7);
border-radius: 8px;
padding: 8px 4px;
font-size: clamp(11px, 2vw, 15px);
letter-spacing: 1px;
text-shadow: 0 0 8px currentColor;
box-shadow: inset 0 0 12px rgba(0,232,255,.08);
}
#canvasBox { position: relative; width: 100%; }
canvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 4 / 3;
background: #02020a;
border: 2px solid rgba(0,232,255,.35);
border-radius: 10px;
box-shadow: 0 0 32px rgba(0,232,255,.16), inset 0 0 28px rgba(0,0,0,.7);
}
#overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 18px;
background: rgba(2,2,14,.84);
border-radius: 10px;
transition: opacity .22s ease;
z-index: 2;
}
#overlay.hidden { opacity: 0; pointer-events: none; }
#overlay h2 {
margin: 0 0 12px;
font-size: clamp(24px, 5vw, 44px);
letter-spacing: 3px;
color: #ff2bd6;
text-shadow: 0 0 12px #ff2bd6, 0 0 28px rgba(255,43,214,.45);
}
#overlay p {
margin: 0 0 20px;
max-width: 560px;
line-height: 1.6;
color: #bfe4ec;
font-size: clamp(12px, 2.2vw, 15px);
}
button {
font-family: inherit;
font-weight: bold;
letter-spacing: 2px;
font-size: 18px;
color: #001018;
padding: 12px 26px;
border: none;
border-radius: 8px;
cursor: pointer;
background: linear-gradient(180deg, #00e8ff, #00a2b8);
box-shadow: 0 0 18px rgba(0,232,255,.55);
transition: transform .12s ease, box-shadow .12s ease;
}
button:hover { transform: translateY(-2px); box-shadow: 0 0 28px rgba(0,232,255,.85); }
button:active { transform: translateY(1px); }
.controls {
text-align: center;
color: #7f9aa3;
margin: 12px 0 0;
font-size: 12px;
line-height: 1.5;
}
.key {
display: inline-block;
border: 1px solid rgba(0,232,255,.35);
border-radius: 4px;
padding: 1px 6px;
margin: 0 2px;
color: #c8f6ff;
box-shadow: inset 0 0 8px rgba(0,232,255,.12);
}
</style>
</head>
<body>
<div id="wrap">
<h1>NEON BREAKOUT</h1>
<div id="hud">
<div>SCORE <span id="scoreEl">0</span></div>
<div>LIVES <span id="livesEl">3</span></div>
<div>LEVEL <span id="levelEl">1</span></div>
<div>COMBO <span id="comboEl">x1</span></div>
</div>
<div id="canvasBox">
<canvas id="game" width="800" height="600"></canvas>
<div id="overlay">
<h2 id="ovTitle">READY PLAYER ONE</h2>
<p id="ovText">Break every bright brick. Catch power-ups. Chain bricks for combo score.<br>Use the mouse or arrow keys. Click or press SPACE to launch.</p>
<button id="startBtn">START</button>
</div>
</div>
<p class="controls">
<span class="key">←</span><span class="key">→</span> or <span class="key">A</span><span class="key">D</span> move •
Mouse Move also moves • <span class="key">Click</span>/<span class="key">SPACE</span> launch •
<span class="key">P</span> pause • <span class="key">M</span> mute
</p>
</div>
<script>
(() => {
'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
const scoreEl = document.getElementById('scoreEl');
const livesEl = document.getElementById('livesEl');
const levelEl = document.getElementById('levelEl');
const comboEl = document.getElementById('comboEl');
const overlay = document.getElementById('overlay');
const ovTitle = document.getElementById('ovTitle');
const ovText = document.getElementById('ovText');
const startBtn = document.getElementById('startBtn');
const BALL_RADIUS = 7;
const PADDLE_BASE = 108;
const PADDLE_H = 16;
const PADDLE_Y = H - 56;
const BRICK_TOP = 82;
const BRICK_ROWS_PAD = 32;
const BRICK_GAP_X = 4;
const BRICK_GAP_Y = 6;
const COLS = 12;
const BRICK_W = (W - BRICK_ROWS_PAD * 2 - (COLS - 1) * BRICK_GAP_X) / COLS;
const BRICK_H = 22;
const LEVELS = [
[
"111111111111",
"111111111111",
"111111111111"
],
[
"111111111111",
"101111111101",
"100111111001",
"100111111001",
"101111111101",
"111111111111"
],
[
"222222222222",
"111111111111",
"333333333333"
],
[
"000011000000",
"000111100000",
"001111110000",
"011111111000",
"111111111111",
"011111111000",
"001111110000",
"000111100000",
"000011000000"
],
[
"333333333333",
"300000000003",
"300444444003",
"300000000003",
"222222222222"
],
[
"121212121212",
"212121212121",
"131313131313",
"313131313131"
],
[
"111111100000",
"000001111110",
"000000011111",
"111110000000",
"001111111111",
"110000000000",
"111111111000",
"000000111111"
],
[
"444444444444",
"441111111144",
"442222222244",
"442000002244",
"442233223344",
"442233223344",
"442000002244",
"442222222244",
"441111111144"
]
];
const BRICK_STYLE = {
1: { fill: "#00e8ff", stroke: "#008a99", value: 100 },
2: { fill: "#ff2bd6", stroke: "#8b0075", value: 200 },
3: { fill: "#ffe234", stroke: "#8c7800", value: 300 },
4: { fill: "#6b7a95", stroke: "#2f3a4a", value: 0 }
};
let state = "menu"; // menu | serve | playing | paused | gameover | victory
let prevState = "playing";
let bricks = [];
let balls = [];
let powerups = [];
let particles = [];
let floaters = [];
let stars = [];
let score = 0;
let lives = 3;
let levelIndex = 0;
let combo = 1;
let mute = false;
let useMouse = false;
let mouseX = W / 2;
let shake = 0;
let flashRed = 0;
const keys = { left: false, right: false };
const effects = { expand: 0, slow: 0 };
const paddle = { x: W / 2, y: PADDLE_Y, w: PADDLE_BASE, h: PADDLE_H, flash: 0 };
/* ------------------------- audio ------------------------- */
let audioCtx = null;
function initAudio() {
if (audioCtx) {
if (audioCtx.state === "suspended") audioCtx.resume();
return;
}
try {
const AC = window.AudioContext || window.webkitAudioContext;
if (AC) audioCtx = new AC();
} catch (e) { audioCtx = null; }
}
function playTone(freq, dur = 0.07, type = "square", vol = 0.08, slideTo = null) {
if (!audioCtx || mute) return;
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, t);
if (slideTo) osc.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), t + dur);
gain.gain.setValueAtTime(vol, t);
gain.gain.exponentialRampToValueAtTime(0.0001, t + dur);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(t);
osc.stop(t + dur + 0.02);
}
const sfx = {
paddle: () => playTone(210, 0.07, "square", 0.09, 150),
wall: () => playTone(120, 0.05, "square", 0.05),
brick: (hp) => playTone(280 + hp * 90, 0.07, "square", 0.08, 340 + hp * 90),
unbreakable: () => playTone(90, 0.08, "triangle", 0.06, 60),
power: () => playTone(520, 0.16, "triangle", 0.1, 900),
lose: () => playTone(380, 0.45, "sawtooth", 0.12, 70),
level: () => playTone(660, 0.18, "triangle", 0.1, 990)
};
/* ------------------------- helpers ------------------------- */
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function rand(a, b) { return a + Math.random() * (b - a); }
function roundRect(x, y, w, h, r) {
r = Math.min(r, w / 2, h / 2);
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 updateHUD() {
scoreEl.textContent = score;
livesEl.textContent = Math.max(0, lives);
levelEl.textContent = levelIndex + 1;
comboEl.textContent = "x" + combo;
}
function showOverlay(title, html, btnLabel) {
ovTitle.textContent = title;
ovText.innerHTML = html;
startBtn.textContent = btnLabel;
overlay.classList.remove("hidden");
}
function hideOverlay() { overlay.classList.add("hidden"); }
/* ------------------------- game setup ------------------------- */
function loadLevel(i) {
bricks = [];
const rows = LEVELS[i];
for (let r = 0; r < rows.length; r++) {
let row = rows[r];
while (row.length < COLS) row += "0";
for (let c = 0; c < COLS; c++) {
const ch = row[c];
if (ch === "0") continue;
const type = parseInt(ch, 10);
const style = BRICK_STYLE[type] || BRICK_STYLE[1];
bricks.push({
x: BRICK_ROWS_PAD + c * (BRICK_W + BRICK_GAP_X),
y: BRICK_TOP + r * (BRICK_H + BRICK_GAP_Y),
w: BRICK_W,
h: BRICK_H,
type,
maxHp: type === 4 ? 999 : type,
hp: type === 4 ? 999 : type,
flash: 0,
dead: false
});
}
}
}
function makeBall() {
return {
x: paddle.x,
y: paddle.y - BALL_RADIUS - 2,
vx: 0,
vy: 0,
r: BALL_RADIUS,
speed: 340,
stuck: true,
trail: []
};
}
function newServe() {
powerups = [];
effects.expand = 0;
effects.slow = 0;
combo = 1;
paddle.w = PADDLE_BASE;
paddle.x = W / 2;
balls = [makeBall()];
state = "serve";
hideOverlay();
updateHUD();
}
function startGame() {
initAudio();
score = 0;
lives = 3;
levelIndex = 0;
particles = [];
floaters = [];
shake = 0;
flashRed = 0;
loadLevel(0);
updateHUD();
newServe();
}
function nextLevel() {
const bonus = 250 + levelIndex * 120 + Math.max(0, lives) * 60;
score += bonus;
addFloater(W / 2, H / 2, "LEVEL CLEAR +" + bonus, "#00e8ff", 1.6);
sfx.level();
levelIndex++;
updateHUD();
if (levelIndex >= LEVELS.length) {
state = "victory";
showOverlay("YOU WIN!", "Final Score: " + score + "<br>Press SPACE or click START to play again.", "PLAY AGAIN");
} else {
loadLevel(levelIndex);
newServe();
}
}
function launchBall(b) {
if (!b.stuck) return;
b.stuck = false;
b.speed = 340 + Math.min(levelIndex * 16, 90);
b.vx = rand(-150, 150);
b.vy = -Math.sqrt(Math.max(50, b.speed * b.speed - b.vx * b.vx));
playTone(420, 0.08, "square", 0.09, 640);
}
function launchAll() {
let launched = false;
for (const b of balls) {
if (b.stuck) { launchBall(b); launched = true; }
}
if (launched) state = "playing";
}
function togglePause() {
if (state === "playing" || state === "serve") {
prevState = state;
state = "paused";
showOverlay("PAUSED", "Take a breath. The bricks will wait.<br>Press P, SPACE, or click RESUME.", "RESUME");
} else if (state === "paused") {
state = prevState;
hideOverlay();
}
}
/* ------------------------- fx ------------------------- */
function spawnParticles(x, y, color, count = 10, power = 160) {
for (let i = 0; i < count; i++) {
const a = Math.random() * Math.PI * 2;
const s = rand(power * 0.25, power);
particles.push({
x, y,
vx: Math.cos(a) * s,
vy: Math.sin(a) * s,
life: rand(0.3, 0.7),
maxLife: 0.7,
size: rand(2, 4),
color
});
}
}
function addFloater(x, y, text, color = "#fff", scale = 1) {
floaters.push({ x, y, text, color, scale, life: 1.0, vy: -34 });
}
function spawnPowerup(x, y) {
if (Math.random() > 0.15) return;
const types = ["EXP", "MULT", "SLOW", "LIFE"];
const t = types[Math.floor(Math.random() * types.length)];
powerups.push({ x: x - 9, y, w: 18, h: 12, type: t, vy: 165, flash: 0 });
}
function activatePower(p) {
sfx.power();
if (p.type === "EXP") {
effects.expand = 14;
addFloater(paddle.x, paddle.y - 30, "WIDE PADDLE", "#00e8ff");
} else if (p.type === "SLOW") {
effects.slow = 12;
addFloater(paddle.x, paddle.y - 30, "SLOW BALL", "#ffe234");
} else if (p.type === "LIFE") {
lives = Math.min(9, lives + 1);
addFloater(paddle.x, paddle.y - 30, "1UP", "#ff2bd6");
} else if (p.type === "MULT") {
const src = balls[Math.floor(Math.random() * balls.length)];
if (src) {
const count = Math.max(0, 4 - balls.length);
for (let i = 0; i < count; i++) {
const nb = {
x: src.x, y: src.y, r: BALL_RADIUS,
speed: src.speed, stuck: false, trail: []
};
const ang = rand(-Math.PI / 3, -Math.PI / 6);
nb.vx = Math.sin(ang) * src.speed * (i % 2 ? 1 : -1) + src.vx * 0.2;
nb.vy = Math.cos(ang) * src.speed;
balls.push(nb);
}
spawnParticles(src.x, src.y, "#ff2bd6", 18, 220);
}
addFloater(paddle.x, paddle.y - 30, "MULTI BALL", "#ff2bd6");
}
updateHUD();
}
/* ------------------------- collisions ------------------------- */
function paddleBounce(b) {
const rel = clamp((b.x - paddle.x) / (paddle.w / 2), -1, 1);
const angle = rel * (Math.PI / 3);
const speed = Math.min(b.speed + 6, 680);
b.speed = speed;
b.vx = Math.sin(angle) * speed;
b.vy = -Math.cos(angle) * speed;
b.y = paddle.y - b.r - 0.1;
paddle.flash = 1;
combo = 1;
shake = Math.min(shake + 3, 12);
sfx.paddle();
spawnParticles(b.x, paddle.y, "#00e8ff", 6, 120);
}
function damageBrick(brick) {
brick.flash = 1;
const style = BRICK_STYLE[brick.type] || BRICK_STYLE[1];
if (brick.type === 4) {
sfx.unbreakable();
spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, "#7f8fa8", 7, 120);
shake = Math.min(shake + 2, 10);
return;
}
brick.hp--;
sfx.brick(brick.hp);
shake = Math.min(shake + 2.2, 14);
if (brick.hp <= 0) {
brick.dead = true;
combo = Math.min(combo + 1, 99);
const points = style.value * combo;
score += points;
updateHUD();
spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, style.fill, 16, 230);
addFloater(brick.x + brick.w / 2, brick.y + brick.h / 2, "+" + points, style.fill, 0.95);
spawnPowerup(brick.x + brick.w / 2, brick.y + brick.h / 2);
flashRed = Math.min(flashRed + 0.08, 0.35);
} else {
spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, style.fill, 5, 110);
}
}
function collideBricks(b) {
for (const brick of bricks) {
if (brick.dead) continue;
const nx = clamp(b.x, brick.x, brick.x + brick.w);
const ny = clamp(b.y, brick.y, brick.y + brick.h);
const dx = b.x - nx;
const dy = b.y - ny;
if (dx * dx + dy * dy > b.r * b.r) continue;
const fromLeft = b.x + b.r - brick.x;
const fromRight = brick.x + brick.w - (b.x - b.r);
const fromTop = b.y + b.r - brick.y;
const fromBottom = brick.y + brick.h - (b.y - b.r);
const min = Math.min(fromLeft, fromRight, fromTop, fromBottom);
if (min === fromLeft) {
b.x = brick.x - b.r - 0.01;
b.vx = -Math.abs(b.vx);
} else if (min === fromRight) {
b.x = brick.x + brick.w + b.r + 0.01;
b.vx = Math.abs(b.vx);
} else if (min === fromTop) {
b.y = brick.y - b.r - 0.01;
b.vy = -Math.abs(b.vy);
} else {
b.y = brick.y + brick.h + b.r + 0.01;
b.vy = Math.abs(b.vy);
}
damageBrick(brick);
return true;
}
return false;
}
/* ------------------------- update ------------------------- */
function updateStars(dt) {
for (const s of stars) {
s.y += (8 + s.z * 34) * dt;
if (s.y > H) { s.y = -2; s.x = Math.random() * W; }
}
}
function updateFx(dt) {
for (const p of particles) {
p.life -= dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 320 * dt;
p.vx *= 0.99;
}
particles = particles.filter(p => p.life > 0);
for (const f of floaters) {
f.life -= dt * 0.85;
f.y += f.vy * dt;
}
floaters = floaters.filter(f => f.life > 0);
paddle.flash = Math.max(0, paddle.flash - dt * 4);
for (const b of bricks) b.flash = Math.max(0, b.flash - dt * 4.5);
}
function moveBall(b, dt) {
const bdt = dt * (effects.slow > 0 ? 0.62 : 1);
const dx = b.vx * bdt;
const dy = b.vy * bdt;
const dist = Math.hypot(dx, dy);
const steps = Math.max(1, Math.ceil(dist / (BALL_RADIUS * 0.65)));
const sx = dx / steps;
const sy = dy / steps;
for (let i = 0; i < steps && !b.dead; i++) {
b.x += sx;
b.y += sy;
if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx); sfx.wall(); }
else if (b.x + b.r > W) { b.x = W - b.r; b.vx = -Math.abs(b.vx); sfx.wall(); }
if (b.y - b.r < 0) { b.y = b.r; b.vy = Math.abs(b.vy); sfx.wall(); }
if (
b.vy > 0 &&
b.y + b.r >= paddle.y &&
b.y - b.r <= paddle.y + paddle.h &&
b.x >= paddle.x - paddle.w / 2 - b.r &&
b.x <= paddle.x + paddle.w / 2 + b.r
) {
paddleBounce(b);
}
collideBricks(b);
if (b.y - b.r > H) { b.dead = true; }
}
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 13) b.trail.shift();
}
function update(dt) {
if (state === "paused") return;
updateStars(dt);
updateFx(dt);
shake = Math.max(0, shake - dt * 34);
flashRed = Math.max(0, flashRed - dt * 1.1);
if (state !== "playing" && state !== "serve") return;
effects.expand = Math.max(0, effects.expand - dt);
effects.slow = Math.max(0, effects.slow - dt);
const targetW = PADDLE_BASE + (effects.expand > 0 ? 52 : 0);
paddle.w += (targetW - paddle.w) * Math.min(1, dt * 9);
const dir = (keys.right ? 1 : 0) - (keys.left ? 1 : 0);
if (useMouse) {
paddle.x += (mouseX - paddle.x) * Math.min(1, dt * 18);
} else if (dir !== 0) {
paddle.x += dir * 820 * dt;
}
paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);
for (const b of balls) {
if (b.stuck) {
b.x = paddle.x;
b.y = paddle.y - b.r - 2;
b.trail.length = 0;
} else {
moveBall(b, dt);
}
}
balls = balls.filter(b => !b.dead);
if (balls.length === 0) {
lives--;
updateHUD();
shake = 30;
flashRed = 0.7;
sfx.lose();
if (lives < 0) {
state = "gameover";
showOverlay("GAME OVER", "Final Score: " + score + "<br>Press SPACE or click START to try again.", "RETRY");
} else {
newServe();
}
return;
}
for (const p of powerups) {
p.y += p.vy * dt;
p.flash += dt * 6;
}
const caught = [];
for (const p of powerups) {
if (
p.x + p.w >= paddle.x - paddle.w / 2 &&
p.x <= paddle.x + paddle.w / 2 &&
p.y + p.h >= paddle.y &&
p.y <= paddle.y + paddle.h
) {
caught.push(p);
activatePower(p);
}
}
if (caught.length) powerups = powerups.filter(p => !caught.includes(p));
powerups = powerups.filter(p => p.y < H + 30);
if (bricks.length && !bricks.some(b => b.maxHp > 0 && !b.dead)) {
nextLevel();
}
}
/* ------------------------- draw ------------------------- */
function drawBackground() {
ctx.fillStyle = "#02020a";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "rgba(0,232,255,0.045)";
for (let x = 0; x <= W; x += 40) ctx.fillRect(x, 0, 1, H);
for (let y = 0; y <= H; y += 40) ctx.fillRect(0, y, W, 1);
for (const s of stars) {
ctx.globalAlpha = 0.18 + s.z * 0.6;
ctx.fillStyle = "#ffffff";
ctx.fillRect(s.x, s.y, 1 + s.z * 2, 1 + s.z * 2);
}
ctx.globalAlpha = 1;
}
function drawBricks() {
for (const b of bricks) {
if (b.dead) continue;
const style = BRICK_STYLE[b.type] || BRICK_STYLE[1];
ctx.fillStyle = style.fill;
roundRect(b.x, b.y, b.w, b.h, 4);
ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.16)";
ctx.fillRect(b.x + 3, b.y + 3, b.w - 6, 3);
if (b.flash > 0) {
ctx.fillStyle = "rgba(255,255,255," + (b.flash * 0.75) + ")";
roundRect(b.x, b.y, b.w, b.h, 4);
ctx.fill();
}
ctx.lineWidth = 2;
ctx.strokeStyle = style.stroke;
roundRect(b.x, b.y, b.w, b.h, 4);
ctx.stroke();
if (b.type === 4) {
ctx.save();
ctx.setLineDash([3, 4]);
ctx.strokeStyle = "rgba(255,255,255,0.35)";
ctx.lineWidth = 1;
roundRect(b.x + 3, b.y + 3, b.w - 6, b.h - 6, 3);
ctx.stroke();
ctx.restore();
} else if (b.maxHp > 1) {
ctx.fillStyle = "rgba(0,0,0,0.45)";
ctx.font = "bold 12px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(String(Math.max(1, b.hp)), b.x + b.w / 2, b.y + b.h / 2 + 1);
}
}
}
function drawPowerups() {
const colors = {
EXP: "#00e8ff", MULT: "#ff2bd6", SLOW: "#ffe234", LIFE: "#8cff6b"
};
for (const p of powerups) {
ctx.save();
ctx.shadowColor = colors[p.type] || "#fff";
ctx.shadowBlur = 14;
ctx.fillStyle = "rgba(0,0,0,0.55)";
roundRect(p.x, p.y, p.w, p.h, 4);
ctx.fill();
ctx.strokeStyle = colors[p.type] || "#fff";
ctx.lineWidth = 2;
roundRect(p.x, p.y, p.w, p.h, 4);
ctx.stroke();
ctx.restore();
ctx.fillStyle = colors[p.type] || "#fff";
ctx.font = "bold 10px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(p.type === "MULT" ? "M" : p.type[0], p.x + p.w / 2, p.y + p.h / 2 + 1);
}
}
function drawPaddle() {
const w = paddle.w;
const x = paddle.x - w / 2;
ctx.save();
ctx.shadowColor = "#00e8ff";
ctx.shadowBlur = 16 + paddle.flash * 24;
ctx.fillStyle = paddle.flash > 0.3 ? "#ffffff" : "#00e8ff";
roundRect(x, paddle.y, w, paddle.h, 7);
ctx.fill();
ctx.strokeStyle = "rgba(255,255,255,0.85)";
ctx.lineWidth = 2;
roundRect(x + 2, paddle.y + 2, w - 4, paddle.h - 4, 5);
ctx.stroke();
ctx.restore();
}
function drawBalls() {
for (const b of balls) {
for (let i = 0; i < b.trail.length; i++) {
const t = b.trail[i];
const a = (i + 1) / b.trail.length;
ctx.fillStyle = "rgba(0,232,255," + (a * 0.17).toFixed(3) + ")";
ctx.beginPath();
ctx.arc(t.x, t.y, b.r * a * 0.92, 0, Math.PI * 2);
ctx.fill();
}
ctx.save();
ctx.shadowColor = "#eafcff";
ctx.shadowBlur = 20;
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
function drawParticles() {
for (const p of particles) {
ctx.globalAlpha = Math.max(0, p.life / p.maxLife);
ctx.fillStyle = p.color;
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
}
ctx.globalAlpha = 1;
}
function drawFloaters() {
ctx.textAlign = "center";
ctx.textBaseline = "middle";
for (const f of floaters) {
ctx.globalAlpha = Math.max(0, f.life);
ctx.fillStyle = f.color;
ctx.font = "bold " + Math.round(15 * f.scale + 2) + "px monospace";
ctx.fillText(f.text, f.x, f.y);
}
ctx.globalAlpha = 1;
}
function drawOverlayFx() {
if (flashRed > 0) {
ctx.fillStyle = "rgba(255,40,80," + (flashRed * 0.45).toFixed(3) + ")";
ctx.fillRect(0, 0, W, H);
}
ctx.fillStyle = "rgba(0,0,0,0.14)";
for (let y = 0; y < H; y += 3) ctx.fillRect(0, y, W, 1);
const grad = ctx.createRadialGradient(W / 2, H / 2, H / 3.2, W / 2, H / 2, H / 1.1);
grad.addColorStop(0, "rgba(0,0,0,0)");
grad.addColorStop(1, "rgba(0,0,0,0.75)");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
function drawHint() {
if (state !== "serve") return;
ctx.save();
ctx.textAlign = "center";
ctx.font = "bold 18px monospace";
ctx.shadowColor = "#00e8ff";
ctx.shadowBlur = 16;
ctx.fillStyle = "rgba(255,255,255," + (0.6 + 0.35 * Math.sin(performance.now() / 190)).toFixed(3) + ")";
ctx.fillText("CLICK OR PRESS SPACE TO LAUNCH", W / 2, H - 110);
ctx.restore();
}
function draw() {
drawBackground();
ctx.save();
if (shake > 0.2) {
ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);
}
drawBricks();
drawPowerups();
drawPaddle();
drawBalls();
drawParticles();
drawFloaters();
ctx.restore();
drawOverlayFx();
drawHint();
}
/* ------------------------- input ------------------------- */
window.addEventListener("keydown", (e) => {
if (["Space", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.code)) e.preventDefault();
initAudio();
if (e.code === "ArrowLeft" || e.code === "KeyA") { keys.left = true; useMouse = false; }
if (e.code === "ArrowRight" || e.code === "KeyD") { keys.right = true; useMouse = false; }
if (e.code === "Space") {
if (state === "menu" || state === "gameover" || state === "victory") startGame();
else if (state === "paused") togglePause();
else if (state === "serve") launchAll();
}
if (e.code === "Enter") {
if (state === "menu" || state === "gameover" || state === "victory") startGame();
else if (state === "paused") togglePause();
}
if (e.code === "KeyP") {
if (state === "playing" || state === "serve" || state === "paused") togglePause();
}
if (e.code === "KeyM") {
mute = !mute;
addFloater(paddle.x, paddle.y - 34, mute ? "SOUND OFF" : "SOUND ON", "#ffffff", 0.8);
}
});
window.addEventListener("keyup", (e) => {
if (e.code === "ArrowLeft" || e.code === "KeyA") keys.left = false;
if (e.code === "ArrowRight" || e.code === "KeyD") keys.right = false;
});
canvas.addEventListener("mousemove", (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = ((e.clientX - rect.left) / rect.width) * W;
useMouse = true;
});
canvas.addEventListener("mousedown", (e) => {
e.preventDefault();
initAudio();
const rect = canvas.getBoundingClientRect();
mouseX = ((e.clientX - rect.left) / rect.width) * W;
useMouse = true;
if (state === "serve") launchAll();
else if (state === "paused") togglePause();
else if (state === "menu" || state === "gameover" || state === "victory") startGame();
});
startBtn.addEventListener("click", (e) => {
e.preventDefault();
initAudio();
if (state === "paused") togglePause();
else startGame();
});
/* ------------------------- main ------------------------- */
for (let i = 0; i < 90; i++) {
stars.push({ x: Math.random() * W, y: Math.random() * H, z: Math.random() * 0.8 + 0.2 });
}
loadLevel(0);
updateHUD();
let last = performance.now();
function loop(t) {
const dt = Math.min(0.05, (t - last) / 1000);
last = t;
update(dt);
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>

