Run details
- Model
- tencent/hy4-preview
- Route
- Command Code CLI
- Reasoning
- provider default
- Limits
- 60 min
- Date
- 2 Oct 2026
- Time
- 15 min 56 s
- Size
- 38 kB
- Tokens
- 476,637 in, 20,227 out
- List price
- $0.45 ($0.834 in / $2.5 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
38 kB 1,168 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>NEON BREAKOUT</title>
<style>
:root{
--bg:#05060d;
--ink:#e8f0ff;
--accent:#4ff0d0;
--accent2:#ff4f9a;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
background:
radial-gradient(1200px 700px at 50% -10%, #14203a 0%, #05060d 60%),
var(--bg);
color:var(--ink);
font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
display:flex;align-items:center;justify-content:center;
overflow:hidden;
-webkit-user-select:none;user-select:none;
touch-action:none;
}
#wrap{
position:relative;
width:min(96vw, 920px);
aspect-ratio: 4 / 3;
max-height:94vh;
}
canvas{
width:100%;height:100%;display:block;
border-radius:14px;
background:#070a14;
box-shadow:
0 0 0 1px rgba(120,180,255,.18),
0 0 40px rgba(70,180,255,.12),
0 30px 80px rgba(0,0,0,.7);
cursor:none;
}
#hud{
position:absolute;inset:0;
pointer-events:none;
border-radius:14px;
overflow:hidden;
}
.bar{
position:absolute;left:0;right:0;
display:flex;align-items:center;gap:14px;
padding:10px 16px;
font-size:clamp(10px,1.9vw,15px);
letter-spacing:.14em;
text-transform:uppercase;
}
#top{top:0;
background:linear-gradient(#000a,#0000);
}
.chip{
display:flex;align-items:center;gap:6px;
text-shadow:0 0 12px rgba(80,200,255,.5);
}
.chip b{font-weight:700;color:var(--accent)}
.chip.warn b{color:var(--accent2)}
.spacer{flex:1}
#overlay{
position:absolute;inset:0;
display:flex;flex-direction:column;
align-items:center;justify-content:center;
gap:18px;text-align:center;
background:radial-gradient(60% 60% at 50% 50%, rgba(5,8,18,.72), rgba(5,8,18,.93));
backdrop-filter:blur(3px);
pointer-events:auto;
}
#overlay.hidden{display:none}
h1{
font-size:clamp(28px,7vw,64px);
letter-spacing:.16em;
font-weight:800;
background:linear-gradient(180deg,#fff,var(--accent));
-webkit-background-clip:text;background-clip:text;color:transparent;
filter:drop-shadow(0 0 24px rgba(80,240,220,.45));
}
h2{
font-size:clamp(16px,3.2vw,26px);
letter-spacing:.2em;color:var(--accent2);
text-shadow:0 0 18px rgba(255,79,154,.5);
}
p.lead{
max-width:44ch;line-height:1.7;
font-size:clamp(11px,2.1vw,14px);
color:#9fb2d4;letter-spacing:.06em;
}
kbd{
display:inline-block;
padding:2px 7px;margin:0 2px;
border:1px solid #33507f;border-bottom-width:2px;
border-radius:5px;background:#0d1426;color:#cfe2ff;
font:inherit;font-size:.9em;
}
button{
pointer-events:auto;
font:inherit;letter-spacing:.18em;text-transform:uppercase;
padding:12px 26px;border-radius:10px;
color:#04121a;font-weight:700;
background:linear-gradient(180deg,#7dffe6,#22c9a8);
border:0;cursor:pointer;
box-shadow:0 6px 0 #0d6f5d, 0 0 34px rgba(60,240,210,.45);
transition:transform .08s ease, box-shadow .08s ease, filter .15s ease;
}
button:hover{filter:brightness(1.08)}
button:active{transform:translateY(4px);box-shadow:0 2px 0 #0d6f5d,0 0 22px rgba(60,240,210,.35)}
.hint{font-size:11px;color:#64789c;letter-spacing:.14em}
#flash{
position:absolute;inset:0;pointer-events:none;
background:#fff;opacity:0;mix-blend-mode:screen;
}
</style>
</head>
<body>
<div id="wrap">
<canvas id="game" width="960" height="720"></canvas>
<div id="hud">
<div class="bar" id="top">
<span class="chip">Score <b id="score">0</b></span>
<span class="chip">Best <b id="best">0</b></span>
<span class="spacer"></span>
<span class="chip">Level <b id="level">1</b></span>
<span class="chip warn">Lives <b id="lives">3</b></span>
</div>
<div id="flash"></div>
</div>
<div id="overlay">
<h1 id="title">Neon Breakout</h1>
<h2 id="subtitle">Smash every brick</h2>
<p class="lead" id="lead">
Move with <kbd>←</kbd><kbd>→</kbd> or <kbd>A</kbd><kbd>D</kbd> — or just move your mouse.<br>
Launch with <kbd>Space</kbd> or click. Wall-crawl with <kbd>Shift</kbd>.<br>
Grab the falling power-ups. Don't drop the ball.
</p>
<div id="stats" class="hint"></div>
<button id="start">Start Game</button>
<div class="hint"><kbd>P</kbd> pause · <kbd>M</kbd> mute · <kbd>R</kbd> restart</div>
</div>
</div>
<script>
(() => {
"use strict";
// ---------- Setup ----------
const W = 960, H = 720;
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d", { alpha: false });
const overlay = document.getElementById("overlay");
const el = {
title: document.getElementById("title"),
subtitle: document.getElementById("subtitle"),
lead: document.getElementById("lead"),
stats: document.getElementById("stats"),
start: document.getElementById("start"),
score: document.getElementById("score"),
best: document.getElementById("best"),
level: document.getElementById("level"),
lives: document.getElementById("lives"),
flash: document.getElementById("flash"),
};
function fitDPR() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.imageSmoothingEnabled = true;
}
fitDPR();
addEventListener("resize", fitDPR);
// ---------- Tiny synths (WebAudio, no assets) ----------
const Sound = (() => {
let ac = null, master = null, muted = false;
function ensure() {
if (ac) return ac;
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return null;
ac = new AC();
master = ac.createGain();
master.gain.value = 0.22;
master.connect(ac.destination);
return ac;
}
function blip({ freq = 440, to = freq, type = "square", dur = 0.09, gain = 1, delay = 0 }) {
if (muted) return;
const c = ensure(); if (!c) return;
if (c.state === "suspended") c.resume();
const t = c.currentTime + delay;
const o = c.createOscillator(), g = c.createGain();
o.type = type;
o.frequency.setValueAtTime(freq, t);
if (to !== freq) o.frequency.exponentialRampToValueAtTime(Math.max(30, to), t + dur);
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(gain, t + 0.008);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.connect(g); g.connect(master);
o.start(t); o.stop(t + dur + 0.02);
}
function noise({ dur = 0.16, gain = 0.5, lp = 1600 }) {
if (muted) return;
const c = ensure(); if (!c) return;
const n = Math.floor(c.sampleRate * dur);
const buf = c.createBuffer(1, n, c.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < n; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / n);
const src = c.createBufferSource(); src.buffer = buf;
const f = c.createBiquadFilter(); f.type = "lowpass"; f.frequency.value = lp;
const g = c.createGain(); g.gain.value = gain;
src.connect(f); f.connect(g); g.connect(master);
src.start();
}
return {
unlock() { const c = ensure(); if (c && c.state === "suspended") c.resume(); },
toggle() { muted = !muted; if (!muted) blip({ freq: 660, dur: 0.07 }); return muted; },
get muted() { return muted; },
paddle() { blip({ freq: 220, to: 320, type: "square", dur: 0.07, gain: 0.5 }); },
wall() { blip({ freq: 300, to: 220, type: "square", dur: 0.05, gain: 0.32 }); },
brick(hp, max) {
const base = 380 + (max - hp) * 90;
blip({ freq: base, to: base * 1.6, type: "square", dur: 0.07, gain: 0.45 });
},
smash() { noise({ dur: 0.22, gain: 0.5, lp: 2200 }); },
power() { [523, 659, 880].forEach((f, i) => blip({ freq: f, type: "triangle", dur: 0.1, gain: 0.4, delay: i * 0.055 })); },
launc() { blip({ freq: 300, to: 700, type: "sawtooth", dur: 0.12, gain: 0.35 }); },
die() { [330, 262, 196, 131].forEach((f, i) => blip({ freq: f, to: f * 0.7, type: "sawtooth", dur: 0.2, gain: 0.4, delay: i * 0.09 })); noise({ dur: 0.4, gain: 0.4, lp: 900 }); },
win() { [523, 659, 784, 1046, 1318].forEach((f, i) => blip({ freq: f, type: "triangle", dur: 0.22, gain: 0.4, delay: i * 0.1 })); },
level() { [392, 523, 659].forEach((f, i) => blip({ freq: f, type: "square", dur: 0.14, gain: 0.35, delay: i * 0.08 })); },
};
})();
// ---------- Utility ----------
const rnd = (a, b) => a + Math.random() * (b - a);
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
const lerp = (a, b, t) => a + (b - a) * t;
const TAU = Math.PI * 2;
function screenShake(mag) { shake = Math.max(shake, mag); }
// ---------- Game state ----------
const STATE = { MENU: "menu", SERVE: "serve", PLAY: "play", PAUSED: "paused", DEAD: "dead", WIN: "win", OVER: "over" };
const P = {
paddleW: 120, paddleH: 16, paddleY: H - 54,
ballR: 8, ballSpeed: 430,
paddleSpeed: 760,
};
let state = STATE.MENU;
let score = 0, best = 0, lives = 3, levelIndex = 0, combo = 0, comboTimer = 0;
let paddle, balls, bricks, particles, powerups, floaters, levelName;
let shake = 0, flashAmt = 0, timeSlow = 0, tSec = 0;
let message = "", messageT = 0;
let keyLeft = false, keyRight = false, keyStick = false;
let mouseX = W / 2, usingMouse = false;
try { best = parseInt(localStorage.getItem("breakout.best") || "0", 10) || 0; } catch (e) { best = 0; }
el.best.textContent = best;
// ---------- Levels ----------
const LEVELS = [
{ name: "Ignition", rows: 5, cols: 11, hpMap: (r, c) => 1, gap: 6, top: 96 },
{ name: "Grid Lock", rows: 6, cols: 12, hpMap: (r, c) => (r < 2 ? 2 : 1), gap: 6, top: 92 },
{ name: "Fortress", rows: 7, cols: 13, hpMap: (r, c) => {
const edge = (c === 0 || c === 12 || r === 0);
const core = (r >= 2 && r <= 4 && c >= 4 && c <= 8);
return core ? 3 : edge ? 2 : 1;
}, gap: 5, top: 88 },
{ name: "Cathedral", rows: 8, cols: 14, hpMap: (r, c) => {
if (r === 0 || c === 0 || c === 13) return 2;
const arch = Math.abs(c - 6.5) < 1.2 - r * 0.12;
return arch ? 3 : 1;
}, gap: 5, top: 84 },
{ name: "Overload", rows: 9, cols: 15, hpMap: (r, c) => {
const d = Math.hypot(c - 7, (r - 4) * 1.4);
if (d < 2) return 4;
if (d < 4) return 3;
if (d < 6) return 2;
return 1;
}, gap: 4, top: 80 },
];
function brickPalette(hp) {
switch (hp) {
case 1: return { a: "#7ef7ff", b: "#2b9bd8", glow: "#7ef7ff" };
case 2: return { a: "#b98cff", b: "#6a3bd6", glow: "#b98cff" };
case 3: return { a: "#ffd166", b: "#e0851f", glow: "#ffd166" };
case 4: return { a: "#ff6ba8", b: "#c31e68", glow: "#ff6ba8" };
default: return { a: "#ff3b3b", b: "#8a0f0f", glow: "#ff6b6b" };
}
}
// ---------- Entities ----------
function resetPaddle() {
paddle = {
x: W / 2 - P.paddleW / 2, y: P.paddleY,
w: P.paddleW, h: P.paddleH,
vx: 0, target: W / 2, sticky: false, laser: 0, glow: 0,
};
}
function makeBall(x, y, ang, speed, opts = {}) {
return {
x, y, r: P.ballR,
vx: Math.cos(ang) * speed, vy: Math.sin(ang) * speed,
speed, stuck: false, stickOff: 0,
trail: [],
fire: opts.fire || false,
lastHit: 0,
};
}
function serveBall() {
balls = [makeBall(paddle.x + paddle.w / 2, paddle.y - P.ballR - 1, -Math.PI / 2 - rnd(-0.35, 0.35), P.ballSpeed)];
const b = balls[0];
b.stuck = true; b.stickOff = 0;
}
function loadLevel(i) {
const L = LEVELS[Math.min(i, LEVELS.length - 1)];
levelName = L.name;
const extraRows = Math.floor(i / LEVELS.length);
const rows = Math.min(L.rows + extraRows, 12);
const cols = L.cols;
const sideMargin = 48;
const bw = (W - sideMargin * 2 - L.gap * (cols - 1)) / cols;
const bh = 26;
bricks = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const hp = Math.min(L.hpMap(r, c) + (extraRows ? 1 : 0), 5);
if (hp <= 0) continue;
bricks.push({
x: sideMargin + c * (bw + L.gap),
y: L.top + r * (bh + L.gap),
w: bw, h: bh, hp, maxHp: hp,
row: r, col: c,
hit: 0, dead: false, drop: null,
seed: Math.random() * TAU,
});
}
}
// Assign powerup drops
const alive = bricks.slice();
const dropCount = Math.max(2, Math.round(alive.length * 0.09));
const kinds = ["expand", "multi", "slow", "laser", "life", "sticky"];
for (let i2 = 0; i2 < dropCount; i2++) {
const b = alive.splice(Math.floor(Math.random() * alive.length), 1)[0];
if (!b) break;
b.drop = kinds[Math.floor(Math.random() * (b.maxHp >= 3 ? kinds.length : 4))];
}
combo = 0; comboTimer = 0;
powerups = [];
resetPaddle();
serveBall();
el.level.textContent = i + 1;
showMessage("LEVEL " + (i + 1) + " — " + levelName.toUpperCase(), 1.6);
}
// ---------- Effects ----------
function spawnParticles(x, y, color, n, power = 1) {
for (let i = 0; i < n; i++) {
const a = rnd(0, TAU), s = rnd(40, 320) * power;
particles.push({
x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s - 40,
life: rnd(0.25, 0.75), max: 0.75, size: rnd(1.6, 4.2), color,
});
}
}
function addScore(n, x, y, color) {
score += n;
if (score > best) { best = score; try { localStorage.setItem("breakout.best", String(best)); } catch (e) {} }
el.best.textContent = best;
floaters.push({ x, y, text: "+" + n, life: 0.85, max: 0.85, color: color || "#e8f0ff", vy: -46, size: 15 });
}
function showMessage(text, dur = 1.4) { message = text; messageT = dur; }
function flash(a = 0.5) { flashAmt = Math.max(flashAmt, a); }
// ---------- Power-ups ----------
const POWER_STYLE = {
expand: { color: "#4ff0d0", label: "WIDE" },
multi: { color: "#ffd166", label: "MULTI" },
slow: { color: "#7ef7ff", label: "SLOW" },
laser: { color: "#ff6ba8", label: "LASER" },
life: { color: "#7dffa0", label: "LIFE" },
sticky: { color: "#b98cff", label: "CATCH" },
};
function applyPower(kind) {
Sound.power();
flash(0.28);
switch (kind) {
case "expand":
paddle.target += 0; paddle.w = Math.min(paddle.w + 42, 260);
break;
case "multi": {
const src = balls.filter(b => !b.stuck);
const add = Math.min(3 - balls.length, 2);
for (let i = 0; i < add; i++) {
const b = src[0] || { vx: 0, vy: -1 };
const ang = Math.atan2(b.vy, b.vx) + rnd(-0.7, 0.7);
const nb = makeBall(b.x || W / 2, b.y || H / 2, ang, b.speed || P.ballSpeed);
nb.fire = b.fire;
balls.push(nb);
}
break;
}
case "slow": timeSlow = 6; break;
case "laser": paddle.laser = 8; break;
case "life": lives++; updateHud(); break;
case "sticky": paddle.sticky = true; paddle.target = paddle.x + paddle.w / 2; break;
}
if (balls[0] && balls[0].stuck) balls[0].stuck = true;
}
// ---------- Input ----------
function pointerToCanvas(e) {
const r = canvas.getBoundingClientRect();
const cx = (e.touches ? e.touches[0].clientX : e.clientX);
return (cx - r.left) / r.width * W;
}
canvas.addEventListener("mousemove", e => {
mouseX = pointerToCanvas(e);
usingMouse = true;
});
canvas.addEventListener("touchmove", e => { e.preventDefault(); mouseX = pointerToCanvas(e); usingMouse = true; }, { passive: false });
function primaryAction() {
Sound.unlock();
if (state === STATE.MENU || state === STATE.OVER || state === STATE.WIN) { newGame(); return; }
if (state === STATE.PAUSED) { state = STATE.PLAY; overlay.classList.add("hidden"); return; }
if (state === STATE.SERVE) {
const b = balls[0];
if (b && b.stuck) {
b.stuck = false;
b.stickOff = 0.18;
Sound.launc();
spawnParticles(b.x, b.y, "#7ef7ff", 12, 0.6);
}
state = STATE.PLAY;
return;
}
if (state === STATE.PLAY && paddle.laser > 0) fireLasers();
}
canvas.addEventListener("mousedown", e => { e.preventDefault(); primaryAction(); });
canvas.addEventListener("touchstart", e => { e.preventDefault(); primaryAction(); }, { passive: false });
el.start.addEventListener("click", e => { e.stopPropagation(); Sound.unlock(); primaryAction(); });
const keys = new Set();
addEventListener("keydown", e => {
const k = e.key.toLowerCase();
keys.add(k);
if (["arrowleft", "arrowright", " ", "arrowup", "arrowdown"].includes(k)) e.preventDefault();
if (k === "arrowleft" || k === "a") { keyLeft = true; usingMouse = false; }
if (k === "arrowright" || k === "d") { keyRight = true; usingMouse = false; }
if (k === "shift") keyStick = true;
if (k === " ") primaryAction();
if (k === "arrowup" && paddle.laser > 0) fireLasers();
if (k === "p") togglePause();
if (k === "m") { const m = Sound.toggle(); showMessage(m ? "MUTED" : "SOUND ON", 0.9); }
if (k === "r") if (state !== STATE.MENU) newGame();
});
addEventListener("keyup", e => {
const k = e.key.toLowerCase();
keys.delete(k);
if (k === "arrowleft" || k === "a") keyLeft = false;
if (k === "arrowright" || k === "d") keyRight = false;
if (k === "shift") keyStick = false;
});
addEventListener("blur", () => { keyLeft = keyRight = false; if (state === STATE.PLAY) togglePause(); });
function togglePause() {
if (state === STATE.PLAY) {
state = STATE.PAUSED;
showOverlay("Paused", "Breathe. Then smash.", "", "Resume");
} else if (state === STATE.PAUSED) {
state = STATE.PLAY;
overlay.classList.add("hidden");
}
}
// ---------- Lasers ----------
let lasers = [];
function fireLasers() {
const y = paddle.y - 6;
lasers.push({ x: paddle.x + 10, y, vy: -900, life: 1.2 });
lasers.push({ x: paddle.x + paddle.w - 10, y, vy: -900, life: 1.2 });
Sound.launc();
flash(0.12);
}
// ---------- Flow ----------
function showOverlay(title, subtitle, lead, btn) {
el.title.textContent = title;
el.subtitle.textContent = subtitle;
el.lead.innerHTML = lead;
el.stats.innerHTML = "";
el.start.textContent = btn;
overlay.classList.remove("hidden");
}
function newGame() {
score = 0; lives = 3; levelIndex = 0;
particles = []; floaters = []; lasers = [];
timeSlow = 0;
updateHud();
overlay.classList.add("hidden");
loadLevel(0);
state = STATE.SERVE;
Sound.level();
}
function updateHud() {
el.score.textContent = score;
el.lives.textContent = lives;
el.best.textContent = best;
}
function gameOver() {
state = STATE.OVER;
Sound.die();
flash(0.5); screenShake(18);
showOverlay("Game Over", "The wall wins this round",
`Final score <b style="color:#4ff0d0">${score}</b><br>Best of all time <b style="color:#ffd166">${best}</b>`,
"Play Again");
}
function winGame() {
state = STATE.WIN;
Sound.win();
showOverlay("You Win", "Every level cleared",
`Final score <b style="color:#4ff0d0">${score}</b><br>Restart to chase a better run.`,
"Play Again");
}
function levelClear() {
score += 500 + lives * 100;
updateHud();
lives = Math.min(lives + 1, 6);
Sound.level();
flash(0.4);
levelIndex++;
loadLevel(levelIndex);
state = STATE.SERVE;
}
function loseLife() {
lives--;
updateHud();
combo = 0;
if (lives <= 0) {
particles.length = 0;
setTimeout(gameOver, 350);
state = STATE.DEAD;
balls = [];
return;
}
Sound.die();
flash(0.35); screenShake(14);
resetPaddle();
serveBall();
state = STATE.SERVE;
}
// ---------- Update ----------
function update(dt) {
tSec += dt;
if (timeSlow > 0) timeSlow -= dt;
const slow = timeSlow > 0 ? 0.55 : 1;
// paddle
const pcx = paddle.x + paddle.w / 2;
if (!usingMouse && (keyLeft || keyRight)) {
const dir = (keyRight ? 1 : 0) - (keyLeft ? 1 : 0);
paddle.x += dir * P.paddleSpeed * dt;
} else if (usingMouse) {
const targetWanted = clamp(mouseX, paddle.w / 2, W - paddle.w / 2);
paddle.x = lerp(paddle.x, targetWanted - paddle.w / 2, 1 - Math.pow(0.0015, dt));
}
paddle.x = clamp(paddle.x, 6, W - paddle.w - 6);
paddle.vx = (paddle.x + paddle.w / 2 - pcx) / Math.max(dt, 1e-4);
if (paddle.laser > 0) paddle.laser -= dt;
paddle.glow = Math.max(0, paddle.glow - dt * 3);
for (const b of balls) {
if (b.stuck) {
b.x = paddle.x + paddle.w / 2;
b.y = paddle.y - b.r - 2;
if (keyStick) { b.stuck = false; b.stickOff = 0.15; Sound.launc(); }
}
b.stickOff = Math.max(0, b.stickOff - dt);
if (!b.stuck) {
const sp = Math.hypot(b.vx, b.vy);
const target = (P.ballSpeed + levelIndex * 12) * (b.fire ? 1.5 : 1);
const cur = Math.max(sp, 1);
const newSpeed = lerp(cur, Math.min(target, 900), 1 - Math.pow(0.4, dt));
b.vx = b.vx / cur * newSpeed;
b.vy = b.vy / cur * newSpeed;
const steps = 3;
const sdt = dt * slow / steps;
for (let s = 0; s < steps; s++) {
moveBall(b, sdt);
}
}
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 12) b.trail.shift();
}
// lasers
for (const l of lasers) {
l.y += l.vy * dt * slow;
l.life -= dt;
for (const br of bricks) {
if (br.dead) continue;
if (l.x > br.x && l.x < br.x + br.w && l.y < br.y + br.h && l.y > br.y) {
hitBrick(br, 1);
l.life = 0;
spawnParticles(l.x, l.y, "#ff6ba8", 8, 0.7);
break;
}
}
}
lasers = lasers.filter(l => l.life > 0 && l.y > -20);
// powerups falling
for (const p of powerups) {
p.y += p.vy * dt * slow;
p.spin += dt * 3;
if (p.y > paddle.y - 10 && p.y < paddle.y + paddle.h + 10 &&
p.x > paddle.x - p.w / 2 && p.x < paddle.x + paddle.w + p.w / 2) {
p.dead = true;
applyPower(p.kind);
spawnParticles(p.x, p.y, POWER_STYLE[p.kind].color, 18, 1);
floaters.push({ x: p.x, y: p.y - 10, text: POWER_STYLE[p.kind].label, life: 0.9, max: 0.9, color: POWER_STYLE[p.kind].color, vy: -50, size: 17 });
}
if (p.y > H + 30) p.dead = true;
}
powerups = powerups.filter(p => !p.dead);
// combo decay
if (combo > 0) { comboTimer -= dt; if (comboTimer <= 0) combo = 0; }
// particles
for (const p of particles) {
p.life -= dt;
p.vy += 420 * dt;
p.vx *= 0.985;
p.x += p.vx * dt; p.y += p.vy * dt;
}
particles = particles.filter(p => p.life > 0);
for (const f of floaters) { f.life -= dt; f.y += f.vy * dt; f.vy *= 0.94; }
floaters = floaters.filter(f => f.life > 0);
for (const br of bricks) if (br.hit > 0) br.hit -= dt * 4;
// effects decay
shake *= Math.pow(0.001, dt);
flashAmt *= Math.pow(0.0025, dt);
if (messageT > 0) messageT -= dt;
// cleared?
if (state === STATE.PLAY || state === STATE.SERVE) {
if (bricks.every(b2 => b2.dead)) levelClear();
}
}
function moveBall(b, dt) {
b.x += b.vx * dt;
b.y += b.vy * dt;
// walls
if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx); Sound.wall(); }
if (b.x + b.r > W) { b.x = W - b.r; b.vx = -Math.abs(b.vx); Sound.wall(); }
if (b.y - b.r < 0) { b.y = b.r; b.vy = Math.abs(b.vy); Sound.wall(); }
// paddle
if (b.stickOff <= 0 && b.vy > 0 &&
b.y + b.r >= paddle.y && b.y - b.r <= paddle.y + paddle.h &&
b.x + b.r >= paddle.x && b.x - b.r <= paddle.x + paddle.w) {
b.y = paddle.y - b.r;
const rel = clamp((b.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2), -1, 1);
const ang = -Math.PI / 2 + rel * 1.05;
const sp = Math.hypot(b.vx, b.vy);
const boost = Math.min(sp * 1 + 6, 900);
b.vx = Math.cos(ang) * boost + paddle.vx * 0.18;
b.vy = Math.sin(ang) * boost;
const ns = Math.hypot(b.vx, b.vy);
b.vx = b.vx / ns * boost; b.vy = b.vy / ns * boost;
Sound.paddle();
paddle.glow = 1;
spawnParticles(b.x, paddle.y, "#4ff0d0", 8, 0.5);
if (paddle.sticky) { b.stuck = true; paddle.sticky = false; b.stickOff = 0.2; }
}
// bricks
let bestHit = null, bestDist = Infinity;
for (const br of bricks) {
if (br.dead) continue;
if (b.x + b.r > br.x && b.x - b.r < br.x + br.w &&
b.y + b.r > br.y && b.y - b.r < br.y + br.h) {
// penetration resolution
const overlapL = (b.x + b.r) - br.x;
const overlapR = (br.x + br.w) - (b.x - b.r);
const overlapT = (b.y + b.r) - br.y;
const overlapB = (br.y + br.h) - (b.y - b.r);
const minX = Math.min(overlapL, overlapR);
const minY = Math.min(overlapT, overlapB);
const d = Math.min(minX, minY);
if (d < bestDist) { bestDist = d; bestHit = { br, minX, minY }; }
}
}
if (bestHit) {
const { br, minX, minY } = bestHit;
if (minX < minY) {
if ((b.vx > 0 && minX === (b.x + b.r) - br.x) || (b.vx < 0 && minX === (br.x + br.w) - (b.x - b.r))) {
b.vx = -b.vx;
b.x += b.vx > 0 ? minX : -minX;
} else { b.vy = -b.vy; }
} else {
if ((b.vy > 0 && minY === (b.y + b.r) - br.y) || (b.vy < 0 && minY === (br.y + br.h) - (b.y - b.r))) {
b.vy = -b.vy;
b.y += b.vy > 0 ? minY : -minY;
} else { b.vx = -b.vx; }
}
hitBrick(br, b.fire ? 2 : 1);
}
// bottom
if (b.y - b.r > H + 20) b.dead = true;
}
function hitBrick(br, dmg) {
if (br.dead) return;
br.hp -= dmg;
br.hit = 1;
const pal = brickPalette(br.maxHp);
if (br.hp > 0) {
Sound.brick(br.hp, br.maxHp);
spawnParticles(br.x + br.w / 2, br.y + br.h / 2, pal.glow, 6, 0.5);
screenShake(3);
combo = 0;
} else {
br.dead = true;
combo = Math.min(combo + 1, 12);
comboTimer = 1.6;
const pts = Math.round((10 * br.maxHp + br.maxHp * 5) * (1 + combo * 0.15));
addScore(pts, br.x + br.w / 2, br.y + br.h / 2, pal.glow);
Sound.brick(0, br.maxHp);
Sound.smash();
spawnParticles(br.x + br.w / 2, br.y + br.h / 2, pal.glow, 18 + br.maxHp * 4, 1);
spawnParticles(br.x + br.w / 2, br.y + br.h / 2, "#ffffff", 6, 0.8);
screenShake(4 + br.maxHp);
if (combo >= 3) floaters.push({ x: br.x + br.w / 2, y: br.y, text: combo + "x COMBO", life: 0.8, max: 0.8, color: "#ffd166", vy: -60, size: 13 });
if (br.drop) {
powerups.push({
x: br.x + br.w / 2, y: br.y + br.h / 2, kind: br.drop,
vy: 165, w: 26, h: 26, spin: 0, dead: false,
});
}
}
updateHud();
}
// remove dead balls
function pruneBalls() {
const before = balls.length;
balls = balls.filter(b => !b.dead);
if (balls.length === 0 && before > 0 && state !== STATE.OVER && state !== STATE.WIN && state !== STATE.DEAD) {
loseLife();
}
}
// ---------- Render ----------
function render() {
ctx.save();
if (shake > 0.2) {
ctx.translate(rnd(-shake, shake), rnd(-shake, shake));
}
// background
drawBackground();
drawPlayfieldBorder();
drawBricks();
drawPowerups();
drawPaddle();
drawLasers();
drawBalls();
drawParticles();
drawFloaters();
if (combo >= 3 && comboTimer > 0) drawCombo();
ctx.restore();
drawVignette();
drawMessage();
drawControlsHint();
if (state === STATE.SERVE) drawServeArrow();
}
function drawBackground() {
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, "#0b1226");
g.addColorStop(0.55, "#070a16");
g.addColorStop(1, "#04050c");
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
// grid
ctx.save();
ctx.globalAlpha = 0.12;
ctx.strokeStyle = "#3a6ea8";
ctx.lineWidth = 1;
const cell = 48;
const off = (tSec * 12) % cell;
ctx.beginPath();
for (let x = -cell; x < W + cell; x += cell) { ctx.moveTo(x + off, 0); ctx.lineTo(x + off, H); }
for (let y = 0; y < H; y += cell) { ctx.moveTo(0, y + off * 0.4); ctx.lineTo(W, y + off * 0.4); }
ctx.stroke();
ctx.restore();
// horizon glow
const rg = ctx.createRadialGradient(W / 2, H + 40, 10, W / 2, H + 40, H * 0.9);
rg.addColorStop(0, "rgba(80,220,255,.10)");
rg.addColorStop(1, "rgba(0,0,0,0)");
ctx.fillStyle = rg;
ctx.fillRect(0, 0, W, H);
}
function drawPlayfieldBorder() {
ctx.save();
ctx.strokeStyle = "rgba(120,190,255,.28)";
ctx.lineWidth = 2;
ctx.shadowColor = "rgba(90,190,255,.6)";
ctx.shadowBlur = 18;
roundRect(ctx, 1, 1, W - 2, H - 2, 12);
ctx.stroke();
ctx.restore();
}
function drawBricks() {
for (const br of bricks) {
if (br.dead) continue;
const pal = brickPalette(br.hp);
const x = br.x, y = br.y, w = br.w, h = br.h;
const pulse = br.hit > 0 ? br.hit : 0;
const g = ctx.createLinearGradient(x, y, x, y + h);
g.addColorStop(0, pal.a);
g.addColorStop(1, pal.b);
ctx.save();
ctx.shadowColor = pal.glow;
ctx.shadowBlur = 10 + pulse * 18;
ctx.fillStyle = g;
roundRect(ctx, x, y, w, h, 5);
ctx.fill();
ctx.restore();
// top highlight
ctx.fillStyle = "rgba(255,255,255,.28)";
roundRect(ctx, x + 2, y + 2, w - 4, h * 0.32, 4);
ctx.fill();
// inner vein
ctx.save();
ctx.globalAlpha = 0.25;
ctx.strokeStyle = "#000";
ctx.lineWidth = 1;
roundRect(ctx, x + .5, y + .5, w - 1, h - 1, 5);
ctx.stroke();
ctx.restore();
// crack marks for damaged bricks
if (br.hp < br.maxHp) {
ctx.save();
ctx.globalAlpha = 0.35;
ctx.strokeStyle = "#000";
ctx.lineWidth = 1.4;
ctx.beginPath();
const n = br.maxHp - br.hp;
for (let i = 0; i <= n; i++) {
const t = (i + 1) / (n + 2);
ctx.moveTo(x + w * t - 6, y + h * 0.15);
ctx.lineTo(x + w * t + 4, y + h * 0.55);
ctx.lineTo(x + w * t - 3, y + h * 0.9);
}
ctx.stroke();
ctx.restore();
}
// drop indicator
if (br.drop) {
const c = POWER_STYLE[br.drop].color;
ctx.save();
ctx.globalAlpha = 0.85;
ctx.fillStyle = c;
ctx.shadowColor = c; ctx.shadowBlur = 10;
const px = x + w / 2, py = y + h / 2;
ctx.beginPath();
ctx.moveTo(px, py - 5); ctx.lineTo(px + 5, py); ctx.lineTo(px, py + 5); ctx.lineTo(px - 5, py);
ctx.closePath(); ctx.fill();
ctx.restore();
}
if (pulse > 0) {
ctx.save();
ctx.globalAlpha = pulse * 0.7;
ctx.fillStyle = "#fff";
roundRect(ctx, x, y, w, h, 5);
ctx.fill();
ctx.restore();
}
}
}
function drawPaddle() {
const x = paddle.x, y = paddle.y, w = paddle.w, h = paddle.h;
ctx.save();
const g = ctx.createLinearGradient(x, y, x, y + h);
g.addColorStop(0, "#eafffb");
g.addColorStop(0.5, "#4ff0d0");
g.addColorStop(1, "#178c78");
ctx.shadowColor = "rgba(79,240,208,.85)";
ctx.shadowBlur = 16 + paddle.glow * 26;
ctx.fillStyle = g;
roundRect(ctx, x, y, w, h, 8);
ctx.fill();
if (paddle.sticky) {
ctx.globalAlpha = 0.8;
ctx.fillStyle = "#b98cff";
roundRect(ctx, x + 4, y + h - 4, w - 8, 3, 2);
ctx.fill();
ctx.globalAlpha = 1;
}
if (paddle.laser > 0) {
ctx.fillStyle = "#ff6ba8";
ctx.shadowColor = "#ff6ba8"; ctx.shadowBlur = 14;
ctx.fillRect(x + 6, y - 4, 4, 5);
ctx.fillRect(x + w - 10, y - 4, 4, 5);
}
ctx.restore();
// reflection under paddle
ctx.save();
ctx.globalAlpha = 0.18;
const rg = ctx.createLinearGradient(x, y + h, x, y + h + 26);
rg.addColorStop(0, "rgba(79,240,208,.6)");
rg.addColorStop(1, "rgba(79,240,208,0)");
ctx.fillStyle = rg;
ctx.fillRect(x, y + h, w, 26);
ctx.restore();
}
function drawBalls() {
for (const b of balls) {
// trail
ctx.save();
for (let i = 0; i < b.trail.length; i++) {
const t = i / b.trail.length;
ctx.globalAlpha = t * 0.35;
ctx.fillStyle = b.fire ? "#ff9c5b" : "#7ef7ff";
ctx.beginPath();
ctx.arc(b.trail[i].x, b.trail[i].y, b.r * t * 0.9, 0, TAU);
ctx.fill();
}
ctx.restore();
ctx.save();
const g = ctx.createRadialGradient(b.x - 2, b.y - 2, 1, b.x, b.y, b.r * 1.6);
g.addColorStop(0, "#ffffff");
g.addColorStop(0.5, b.fire ? "#ffd166" : "#9ffcff");
g.addColorStop(1, b.fire ? "#ff5722" : "#2b9bd8");
ctx.shadowColor = b.fire ? "rgba(255,140,60,.9)" : "rgba(126,247,255,.9)";
ctx.shadowBlur = 22;
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, TAU); ctx.fill();
ctx.restore();
}
}
function drawLasers() {
ctx.save();
ctx.shadowColor = "#ff6ba8"; ctx.shadowBlur = 14;
ctx.strokeStyle = "#ffd3e6"; ctx.lineWidth = 3;
ctx.beginPath();
for (const l of lasers) { ctx.moveTo(l.x, l.y); ctx.lineTo(l.x, l.y + 18); }
ctx.stroke();
ctx.restore();
}
function drawPowerups() {
for (const p of powerups) {
const st = POWER_STYLE[p.kind];
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(Math.sin(p.spin) * 0.35);
ctx.shadowColor = st.color; ctx.shadowBlur = 16;
ctx.fillStyle = "rgba(8,14,26,.9)";
roundRect(ctx, -p.w / 2, -p.h / 2, p.w, p.h, 6); ctx.fill();
ctx.strokeStyle = st.color; ctx.lineWidth = 2;
roundRect(ctx, -p.w / 2, -p.h / 2, p.w, p.h, 6); ctx.stroke();
ctx.shadowBlur = 0;
ctx.fillStyle = st.color;
ctx.font = "bold 9px ui-monospace, monospace";
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.fillText(st.label[0], 0, 0.5);
ctx.restore();
}
}
function drawParticles() {
ctx.save();
for (const p of particles) {
ctx.globalAlpha = clamp(p.life / p.max, 0, 1);
ctx.fillStyle = p.color;
ctx.shadowColor = p.color; ctx.shadowBlur = 8;
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
}
ctx.restore();
}
function drawFloaters() {
ctx.save();
ctx.textAlign = "center"; ctx.textBaseline = "middle";
for (const f of floaters) {
const t = clamp(f.life / f.max, 0, 1);
ctx.globalAlpha = t;
ctx.font = `bold ${f.size}px ui-monospace, monospace`;
ctx.fillStyle = f.color;
ctx.shadowColor = f.color; ctx.shadowBlur = 10;
ctx.fillText(f.text, f.x, f.y);
}
ctx.restore();
}
function drawCombo() {
ctx.save();
ctx.globalAlpha = clamp(comboTimer / 1.6, 0, 1) * 0.9;
ctx.textAlign = "right";
ctx.font = "bold 22px ui-monospace, monospace";
ctx.fillStyle = "#ffd166";
ctx.shadowColor = "#ffd166"; ctx.shadowBlur = 16;
ctx.fillText(`${combo}x`, W - 24, H - 96);
ctx.font = "10px ui-monospace, monospace";
ctx.fillText("COMBO", W - 24, H - 80);
ctx.restore();
}
function drawMessage() {
if (messageT <= 0 || !message) return;
const t = clamp(messageT, 0, 1);
ctx.save();
ctx.globalAlpha = Math.min(1, t * 1.6) * 0.95;
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.font = "bold 34px ui-monospace, monospace";
ctx.fillStyle = "#e8f0ff";
ctx.shadowColor = "rgba(80,240,220,.8)"; ctx.shadowBlur = 22;
ctx.fillText(message, W / 2, H * 0.42);
ctx.restore();
}
function drawServeArrow() {
if (!balls[0]) return;
const b = balls[0];
const bob = Math.sin(tSec * 5) * 6;
ctx.save();
ctx.globalAlpha = 0.6 + Math.sin(tSec * 5) * 0.25;
ctx.strokeStyle = "#4ff0d0"; ctx.lineWidth = 3;
ctx.shadowColor = "#4ff0d0"; ctx.shadowBlur = 12;
ctx.beginPath();
ctx.moveTo(b.x, b.y - 26 + bob);
ctx.lineTo(b.x - 8, b.y - 12 + bob);
ctx.lineTo(b.x + 8, b.y - 12 + bob);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
function drawControlsHint() {
if (state !== STATE.SERVE && state !== STATE.PLAY) return;
ctx.save();
ctx.globalAlpha = 0.35;
ctx.textAlign = "center";
ctx.font = "11px ui-monospace, monospace";
ctx.fillStyle = "#7f93b8";
const txt = usingMouse ? "MOUSE — CLICK TO LAUNCH" : "ARROWS / A-D — SPACE TO LAUNCH";
ctx.fillText(txt, W / 2, H - 18);
ctx.restore();
}
function drawVignette() {
const v = ctx.createRadialGradient(W / 2, H / 2, H * 0.35, W / 2, H / 2, H * 0.85);
v.addColorStop(0, "rgba(0,0,0,0)");
v.addColorStop(1, "rgba(0,0,0,.55)");
ctx.fillStyle = v;
ctx.fillRect(0, 0, W, H);
}
function roundRect(c, x, y, w, h, r) {
const rr = Math.min(r, w / 2, h / 2);
c.beginPath();
c.moveTo(x + rr, y);
c.arcTo(x + w, y, x + w, y + h, rr);
c.arcTo(x + w, y + h, x, y + h, rr);
c.arcTo(x, y + h, x, y, rr);
c.arcTo(x, y, x + w, y, rr);
c.closePath();
}
// ---------- Loop ----------
let last = performance.now();
function frame(now) {
let dt = (now - last) / 1000;
last = now;
if (dt > 0.05) dt = 0.05;
if (state === STATE.PLAY || state === STATE.SERVE) {
update(dt);
pruneBalls();
} else if (state === STATE.DEAD) {
// still animate particles on death
tSec += dt;
for (const p of particles) { p.life -= dt; p.vy += 420 * dt; p.x += p.vx * dt; p.y += p.vy * dt; }
particles = particles.filter(p => p.life > 0);
shake *= Math.pow(0.001, dt);
flashAmt *= Math.pow(0.0025, dt);
if (messageT > 0) messageT -= dt;
for (const b of bricks) if (b.hit > 0) b.hit -= dt * 4;
}
render();
requestAnimationFrame(frame);
}
// ---------- Boot ----------
particles = []; floaters = []; powerups = []; lasers = []; balls = [];
bricks = [];
resetPaddle();
serveBall();
updateHud();
requestAnimationFrame(frame);
// flash hookup
setInterval(() => { el.flash.style.opacity = String(clamp(flashAmt, 0, 1)); }, 16);
})();
</script>
</body>
</html>