Run details
- Model
- minimaxai/minimax-m3
- Route
- Command Code CLI
- Reasoning
- provider default
- Limits
- 60 min
- Date
- 2 Oct 2026
- Time
- 2 min 5 s
- Size
- 39 kB
- Tokens
- 39,814 in, 13,178 out
- List price
- $0.028 ($0.3 in / $1.2 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
39 kB 1,199 lines Raw
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Neon Breakout</title>
<style>
:root {
--bg-0: #05060c;
--bg-1: #0b0f1f;
--grid: rgba(80, 110, 200, 0.07);
--fg: #d8e1ff;
--muted: #6c7aaf;
--accent: #7df9ff;
--accent-2: #ff5fd0;
--warn: #ffd166;
--good: #5cffb1;
--danger: #ff5e7a;
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0; height: 100%;
background: radial-gradient(1200px 800px at 50% 30%, var(--bg-1), var(--bg-0) 70%);
color: var(--fg);
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
}
body {
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 10px;
background-image:
linear-gradient(var(--grid) 1px, transparent 1px),
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
background-size: 40px 40px, 40px 40px;
background-position: -1px -1px;
}
h1 {
margin: 0; font-size: 14px; letter-spacing: 0.4em; color: var(--muted);
text-transform: uppercase; font-weight: 600;
}
.hud {
width: min(900px, 96vw);
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 16px;
font-size: 13px;
color: var(--muted);
}
.hud .left, .hud .right { display: flex; gap: 18px; align-items: center; }
.hud .right { justify-content: flex-end; }
.hud .center { text-align: center; color: var(--fg); }
.pill {
display: inline-flex; align-items: center; gap: 8px;
padding: 6px 10px; border-radius: 999px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(125, 249, 255, 0.15);
color: var(--fg);
}
.pill .lbl { color: var(--muted); font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; }
.pill .val { font-weight: 600; }
.stage {
position: relative;
width: min(900px, 96vw);
aspect-ratio: 9 / 7;
border-radius: 14px;
overflow: hidden;
background:
radial-gradient(ellipse at 50% 0%, rgba(125, 249, 255, 0.08), transparent 60%),
linear-gradient(180deg, #060818 0%, #03040a 100%);
box-shadow:
0 0 0 1px rgba(125, 249, 255, 0.18),
0 30px 80px rgba(0,0,0,0.6),
inset 0 0 80px rgba(125, 249, 255, 0.04);
}
canvas {
display: block; width: 100%; height: 100%;
image-rendering: pixelated;
}
.controls {
width: min(900px, 96vw);
display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
gap: 8px 14px;
color: var(--muted);
font-size: 12px;
}
.controls .key {
display: inline-block;
padding: 2px 8px;
border-radius: 6px;
border: 1px solid rgba(125, 249, 255, 0.25);
background: rgba(125, 249, 255, 0.06);
color: var(--fg);
font-size: 11px;
}
.controls .sep { opacity: 0.4; }
.controls .btn {
cursor: pointer;
padding: 6px 10px;
border-radius: 6px;
border: 1px solid rgba(125, 249, 255, 0.25);
background: rgba(125, 249, 255, 0.06);
color: var(--fg);
font: inherit;
}
.controls .btn:hover { background: rgba(125, 249, 255, 0.12); }
.controls .btn:active { transform: translateY(1px); }
.overlay {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
background: radial-gradient(circle at 50% 40%, rgba(0,0,0,0.55), rgba(0,0,0,0.85) 70%);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
transition: opacity 220ms ease;
pointer-events: auto;
}
.overlay.hidden { opacity: 0; pointer-events: none; }
.panel {
width: min(440px, 86%);
text-align: center;
padding: 22px 24px;
border-radius: 14px;
background: linear-gradient(180deg, rgba(20,26,48,0.85), rgba(8,10,22,0.9));
border: 1px solid rgba(125, 249, 255, 0.25);
box-shadow: 0 30px 80px rgba(0,0,0,0.6), inset 0 0 30px rgba(125, 249, 255, 0.05);
}
.panel h2 {
margin: 0 0 6px 0;
font-size: 28px;
letter-spacing: 0.18em;
text-transform: uppercase;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.panel p { margin: 6px 0; color: var(--muted); font-size: 13px; }
.panel .row { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 12px; }
.panel .big {
font-size: 14px; color: var(--fg);
margin-top: 8px;
}
.panel .big span { color: var(--accent); }
.panel .btn {
cursor: pointer;
padding: 10px 16px;
border-radius: 10px;
border: 1px solid rgba(125, 249, 255, 0.35);
background: linear-gradient(180deg, rgba(125, 249, 255, 0.16), rgba(125, 249, 255, 0.06));
color: var(--fg);
font: inherit; font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase;
transition: transform 80ms ease, background 120ms ease;
}
.panel .btn:hover { background: linear-gradient(180deg, rgba(125, 249, 255, 0.28), rgba(125, 249, 255, 0.1)); }
.panel .btn:active { transform: translateY(1px); }
.panel .btn.secondary {
background: rgba(255,255,255,0.04);
border-color: rgba(255,255,255,0.12);
color: var(--muted);
}
.badge {
display: inline-block;
font-size: 10px;
letter-spacing: 0.18em;
padding: 2px 6px;
border-radius: 4px;
background: rgba(125, 249, 255, 0.1);
border: 1px solid rgba(125, 249, 255, 0.25);
color: var(--accent);
text-transform: uppercase;
margin-left: 6px;
}
/* Subtle CRT scanlines */
.stage::after {
content: "";
position: absolute; inset: 0;
pointer-events: none;
background: repeating-linear-gradient(0deg, rgba(255,255,255,0.02) 0 1px, transparent 1px 3px);
mix-blend-mode: overlay;
}
</style>
</head>
<body>
<h1>Neon Breakout</h1>
<div class="hud">
<div class="left">
<div class="pill"><span class="lbl">Score</span><span class="val" id="score">0</span></div>
<div class="pill"><span class="lbl">Best</span><span class="val" id="best">0</span></div>
<div class="pill"><span class="lbl">Combo</span><span class="val" id="combo">x1</span></div>
</div>
<div class="center" id="levelLabel">Level 1</div>
<div class="right">
<div class="pill"><span class="lbl">Lives</span><span class="val" id="lives">●●●</span></div>
<div class="pill"><span class="lbl">Ball</span><span class="val" id="balls">x1</span></div>
</div>
</div>
<div class="stage" id="stage">
<canvas id="game" width="900" height="700"></canvas>
<div class="overlay" id="overlay">
<div class="panel" id="panel">
<h2 id="title">Neon Breakout</h2>
<p id="subtitle">Smash every brick. Don't drop the ball.</p>
<div class="big" id="bigStats"></div>
<div class="row" id="panelButtons">
<button class="btn" id="startBtn">Start</button>
</div>
<p style="margin-top:14px; font-size:11px; opacity:0.8;">
<span class="key">◀</span> <span class="key">▶</span> or <span class="key">A</span> <span class="key">D</span> move paddle ·
<span class="key">Mouse</span> aim · <span class="key">Space</span> launch · <span class="key">P</span> pause · <span class="key">R</span> reset
</p>
</div>
</div>
</div>
<div class="controls">
<span><span class="key">◀</span> <span class="key">▶</span> / <span class="key">A</span> <span class="key">D</span> move</span>
<span class="sep">·</span>
<span><span class="key">Mouse</span> aim</span>
<span class="sep">·</span>
<span><span class="key">Space</span> launch</span>
<span class="sep">·</span>
<span><span class="key">P</span> pause</span>
<span class="sep">·</span>
<span><span class="key">R</span> reset</span>
<span class="sep">·</span>
<button class="btn" id="muteBtn" title="Mute / Unmute">Sound: On</button>
<span class="sep">·</span>
<button class="btn" id="qualityBtn" title="Toggle quality">Quality: High</button>
</div>
<script>
(() => {
'use strict';
// ---------- Canvas & DPR ----------
const stage = document.getElementById('stage');
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
// ---------- HUD ----------
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('best');
const comboEl = document.getElementById('combo');
const livesEl = document.getElementById('lives');
const ballsEl = document.getElementById('balls');
const levelLabel = document.getElementById('levelLabel');
// ---------- Overlay ----------
const overlay = document.getElementById('overlay');
const titleEl = document.getElementById('title');
const subEl = document.getElementById('subtitle');
const bigEl = document.getElementById('bigStats');
const btnsEl = document.getElementById('panelButtons');
const startBtn = document.getElementById('startBtn');
// ---------- Settings ----------
const muteBtn = document.getElementById('muteBtn');
const qualityBtn = document.getElementById('qualityBtn');
let soundOn = true;
let highQuality = true;
muteBtn.addEventListener('click', () => {
soundOn = !soundOn;
muteBtn.textContent = 'Sound: ' + (soundOn ? 'On' : 'Off');
});
qualityBtn.addEventListener('click', () => {
highQuality = !highQuality;
qualityBtn.textContent = 'Quality: ' + (highQuality ? 'High' : 'Low');
});
// ---------- Persistent best ----------
let best = 0;
try {
const saved = localStorage.getItem('neon-breakout-best');
if (saved) best = parseInt(saved, 10) || 0;
} catch (e) {}
bestEl.textContent = best;
// ---------- Audio (WebAudio synth) ----------
let actx = null;
function ensureAudio() {
if (!actx) {
try { actx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {}
}
if (actx && actx.state === 'suspended') actx.resume();
return actx;
}
function blip(freq, dur, type = 'square', gain = 0.06, sweep = 0) {
if (!soundOn) return;
const a = ensureAudio(); if (!a) return;
const o = a.createOscillator();
const g = a.createGain();
o.type = type;
o.frequency.setValueAtTime(freq, a.currentTime);
if (sweep) o.frequency.exponentialRampToValueAtTime(Math.max(20, freq + sweep), a.currentTime + dur);
g.gain.setValueAtTime(0, a.currentTime);
g.gain.linearRampToValueAtTime(gain, a.currentTime + 0.005);
g.gain.exponentialRampToValueAtTime(0.0001, a.currentTime + dur);
o.connect(g); g.connect(a.destination);
o.start();
o.stop(a.currentTime + dur + 0.02);
}
function noiseBurst(dur = 0.18, gain = 0.08, lowpass = 1200) {
if (!soundOn) return;
const a = ensureAudio(); if (!a) return;
const buf = a.createBuffer(1, Math.floor(a.sampleRate * dur), a.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / d.length);
const src = a.createBufferSource(); src.buffer = buf;
const lp = a.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = lowpass;
const g = a.createGain(); g.gain.value = gain;
src.connect(lp); lp.connect(g); g.connect(a.destination);
src.start();
}
// ---------- Game State ----------
const STATE = { MENU: 0, PLAYING: 1, PAUSED: 2, LEVEL_CLEAR: 3, GAME_OVER: 4, WIN: 5 };
let state = STATE.MENU;
let score = 0;
let lives = 3;
let balls = 1; // balls-in-play modifier (multi-ball pickups)
let level = 1;
let combo = 0; // chain count for consecutive brick hits
let comboTimer = 0; // ms remaining before combo resets
const COMBO_WINDOW = 2200;
let ballStuck = true;
let launchAim = 0; // aim angle when ball is stuck to paddle
// Particles
const particles = [];
const sparks = [];
const floats = []; // floating score numbers
// Screen shake
let shake = 0;
function addShake(amt) { shake = Math.min(28, shake + amt); }
// Paddle
const paddle = {
w: 130, h: 14,
x: W / 2 - 65, y: H - 50,
speed: 0, vx: 0,
targetX: null,
color: '#7df9ff'
};
// Ball
const ballProto = { x: 0, y: 0, vx: 0, vy: 0, r: 8, stuck: true, trail: [] };
let ballsArr = [];
// Bricks
let bricks = [];
const BRICK_ROWS = 6;
const BRICK_COLS_BASE = 11;
// Powerups
const powerups = []; // {x, y, vx, vy, type, age}
const activePowers = { wide: 0, slow: 0, multi: 0 }; // ms remaining
// ---------- Level Design ----------
// Each level: { rows, cols, pattern, ballSpeed, paddleW, palette }
// pattern uses string grid where '#' = brick, ' ' = empty
function makePattern(rows, cols, fn) {
const grid = [];
for (let r = 0; r < rows; r++) {
const row = '';
for (let c = 0; c < cols; c++) row += fn(r, c) ? '#' : ' ';
grid.push(row);
}
return grid;
}
const LEVELS = [
// Level 1 — gentle intro: solid rectangle
{
pattern: (r, c) => r < 4,
ballSpeed: 360, paddleW: 140, lives: 3, name: 'Warmup',
},
// Level 2 — checkerboard
{
pattern: (r, c) => (r + c) % 2 === 0 && r < 5,
ballSpeed: 380, paddleW: 130, lives: 3, name: 'Checkers',
},
// Level 3 — pyramid
{
pattern: (r, c, cols) => {
const half = Math.floor(cols / 2);
const spread = r + 1;
return Math.abs(c - half) < spread && r < 5;
},
ballSpeed: 400, paddleW: 120, lives: 3, name: 'Pyramid',
},
// Level 4 — heart-ish / diamond
{
pattern: (r, c, cols) => {
const half = (cols - 1) / 2;
const d = Math.abs(c - half);
return r + d * 0.7 < 4.4 && r < 5;
},
ballSpeed: 420, paddleW: 115, lives: 3, name: 'Diamond',
},
// Level 5 — fortress
{
pattern: (r, c, cols) => {
// border frame
if (r === 0 || r === 5) return c > 0 && c < cols - 1;
if (c === 0 || c === cols - 1) return r > 0 && r < 5;
// inner cross
if (r === 3 && c > 2 && c < cols - 3) return true;
if (c === Math.floor(cols / 2) && r > 1 && r < 4) return true;
return false;
},
ballSpeed: 440, paddleW: 110, lives: 2, name: 'Fortress',
},
// Level 6 — spiral
{
pattern: (r, c, cols) => {
const x = c / (cols - 1);
const y = r / 5;
const dx = x - 0.5, dy = y - 0.5;
const ang = Math.atan2(dy, dx);
const dist = Math.sqrt(dx * dx + dy * dy);
const spiral = Math.abs(dist - 0.18 * (1 + Math.sin(ang * 3 + r * 0.4))) < 0.07;
return spiral;
},
ballSpeed: 460, paddleW: 105, lives: 2, name: 'Spiral',
},
// Level 7 — final gauntlet
{
pattern: (r, c, cols) => {
// Big "B" with hollows
const cols2 = cols;
const u = c / (cols2 - 1);
const v = r / 5;
const left = u < 0.18;
const right = u > 0.82;
const topBot = v < 0.18 || v > 0.82;
const mid = v > 0.42 && v < 0.58;
return left || right || topBot || mid;
},
ballSpeed: 480, paddleW: 100, lives: 2, name: 'Gauntlet',
},
];
// Brick colors per row (gradient top->bottom)
const ROW_COLORS = [
['#ff5e7a', '#ff7e9a'], // pink/red
['#ff8a5c', '#ffae7c'], // orange
['#ffd166', '#ffe199'], // yellow
['#5cffb1', '#9affd0'], // green
['#7df9ff', '#aef9ff'], // cyan
['#9d7dff', '#bda4ff'], // purple
];
// ---------- Helpers ----------
function clamp(v, lo, hi) { return v < lo ? lo : v > hi ? hi : v; }
function rand(lo, hi) { return lo + Math.random() * (hi - lo); }
// ---------- Build level ----------
function buildLevel(idx) {
const def = LEVELS[Math.min(idx, LEVELS.length - 1)];
const cols = BRICK_COLS_BASE + Math.min(2, Math.floor(idx / 2)); // slightly wider as you progress
const rows = BRICK_ROWS;
const padX = 60, padY = 90;
const brickW = (W - padX * 2) / cols;
const brickH = 22;
bricks = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (def.pattern(r, c, cols)) {
// Some bricks in later levels are tougher (hp > 1)
const hp = (idx >= 2 && (r + c) % 7 === 0) ? 2 : 1;
bricks.push({
x: padX + c * brickW + 1,
y: padY + r * brickH + 1,
w: brickW - 2,
h: brickH - 2,
row: r,
hp,
maxHp: hp,
alive: true,
color: ROW_COLORS[r % ROW_COLORS.length],
flash: 0
});
}
}
}
// Set paddle + balls
paddle.w = def.paddleW;
paddle.x = W / 2 - paddle.w / 2;
paddle.y = H - 50;
paddle.speed = 700;
ballsArr = [];
spawnBallOnPaddle();
activePowers.wide = 0; activePowers.slow = 0; activePowers.multi = 0;
ballStuck = true;
level = idx + 1;
levelLabel.textContent = `Level ${level} · ${def.name}`;
}
function spawnBallOnPaddle() {
const b = {
x: paddle.x + paddle.w / 2,
y: paddle.y - 12,
vx: 0, vy: 0,
r: 8,
stuck: true,
trail: []
};
ballsArr.push(b);
}
// ---------- Input ----------
const keys = {};
const mouse = { x: W / 2, y: H / 2, down: false, over: false };
window.addEventListener('keydown', (e) => {
if (['ArrowLeft','ArrowRight','Space','KeyA','KeyD','KeyP','KeyR'].includes(e.code)) e.preventDefault();
keys[e.code] = true;
if (e.code === 'Space') {
if (state === STATE.MENU || state === STATE.GAME_OVER || state === STATE.WIN) {
startNewGame();
} else if (state === STATE.PLAYING && ballStuck) {
launchBall();
} else if (state === STATE.LEVEL_CLEAR) {
nextLevel();
}
}
if (e.code === 'KeyP') {
if (state === STATE.PLAYING) { state = STATE.PAUSED; showOverlay('Paused', 'Press P to resume', '', [{label:'Resume', primary:true, action:()=>{state=STATE.PLAYING; hideOverlay();}}]); }
else if (state === STATE.PAUSED) { state = STATE.PLAYING; hideOverlay(); }
}
if (e.code === 'KeyR') {
startNewGame();
}
});
window.addEventListener('keyup', (e) => { keys[e.code] = false; });
function canvasMouse(e) {
const r = canvas.getBoundingClientRect();
const sx = canvas.width / r.width;
const sy = canvas.height / r.height;
mouse.x = (e.clientX - r.left) * sx;
mouse.y = (e.clientY - r.top) * sy;
}
canvas.addEventListener('mousemove', canvasMouse);
canvas.addEventListener('mouseenter', () => { mouse.over = true; });
canvas.addEventListener('mouseleave', () => { mouse.over = false; });
canvas.addEventListener('mousedown', (e) => {
canvasMouse(e);
if (state === STATE.MENU || state === STATE.GAME_OVER || state === STATE.WIN) {
startNewGame();
return;
}
if (state === STATE.LEVEL_CLEAR) { nextLevel(); return; }
if (state === STATE.PLAYING && ballStuck) launchBall();
});
function getPaddleTargetX() {
if (mouse.over) {
return clamp(mouse.x - paddle.w / 2, 8, W - paddle.w - 8);
}
return null;
}
// ---------- Launch / aim ----------
function launchBall() {
const a = launchAim || (-Math.PI / 2 + rand(-0.25, 0.25));
const sp = LEVELS[Math.min(level-1, LEVELS.length-1)].ballSpeed;
for (const b of ballsArr) {
b.stuck = false;
b.vx = Math.cos(a) * sp;
b.vy = Math.sin(a) * sp;
b.x = paddle.x + paddle.w / 2;
b.y = paddle.y - 12;
}
ballStuck = false;
blip(880, 0.07, 'square', 0.05, 200);
}
function aimFromInput() {
if (ballStuck && mouse.over) {
const dx = mouse.x - (paddle.x + paddle.w / 2);
const dy = (paddle.y - 30) - (paddle.y - 12);
const a = Math.atan2(dy, dx);
// Keep aiming upward (vy < 0)
launchAim = clamp(a, -Math.PI + 0.15, -0.15);
}
}
// ---------- Powerups ----------
const POWER_TYPES = ['wide', 'slow', 'multi', 'life', 'shrink'];
function maybeDropPowerup(x, y) {
if (Math.random() > 0.18) return; // 18% drop chance
const t = POWER_TYPES[Math.floor(Math.random() * POWER_TYPES.length)];
powerups.push({ x, y, vx: 0, vy: 90, type: t, age: 0 });
}
function applyPowerup(t) {
if (t === 'wide') { activePowers.wide = 10000; paddle.w = (LEVELS[Math.min(level-1, LEVELS.length-1)].paddleW || 120) + 60; }
if (t === 'slow') { activePowers.slow = 8000; }
if (t === 'multi') {
// split each ball into 3
const newBalls = [];
for (const b of ballsArr) {
for (let i = 0; i < 2; i++) {
const a = Math.atan2(b.vy, b.vx) + rand(-0.5, 0.5);
const sp = Math.hypot(b.vx, b.vy);
newBalls.push({ x: b.x, y: b.y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp, r: b.r, stuck: false, trail: [] });
}
}
ballsArr.push(...newBalls);
addShake(4);
}
if (t === 'life') { lives = Math.min(5, lives + 1); blip(660, 0.2, 'sine', 0.08, 220); }
if (t === 'shrink') { paddle.w = Math.max(60, paddle.w * 0.65); }
}
// ---------- Particles ----------
function burst(x, y, color, n = 14, speed = 220, life = 600) {
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2;
const s = rand(speed * 0.3, speed);
particles.push({
x, y,
vx: Math.cos(a) * s,
vy: Math.sin(a) * s,
life: life * rand(0.6, 1.2),
age: 0,
size: rand(1.5, 3.5),
color
});
}
}
function spark(x, y, color) {
sparks.push({ x, y, life: 300, age: 0, color });
}
function floatText(x, y, text, color) {
floats.push({ x, y, text, color, life: 800, age: 0 });
}
// ---------- Collision ----------
function circleRect(cx, cy, cr, 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) <= cr * cr;
}
// ---------- Game Logic ----------
function startNewGame() {
score = 0; lives = 3; combo = 0; comboTimer = 0;
particles.length = 0; sparks.length = 0; floats.length = 0; powerups.length = 0;
buildLevel(0);
state = STATE.PLAYING;
hideOverlay();
updateHUD();
}
function nextLevel() {
if (level >= LEVELS.length) {
// Player completed all built-in levels — keep going with cycling patterns at higher speed
const idx = LEVELS.length - 1;
const def = LEVELS[idx];
def.ballSpeed += 30;
def.paddleW = Math.max(70, def.paddleW - 4);
def.lives = Math.max(1, (def.lives || 1) - 1);
buildLevel(idx);
state = STATE.PLAYING;
hideOverlay();
return;
}
buildLevel(level);
state = STATE.PLAYING;
hideOverlay();
}
function update(dt) {
if (state !== STATE.PLAYING) {
// still animate particles + floats subtly
stepParticles(dt);
shake = Math.max(0, shake - dt * 30);
return;
}
// ---- Paddle movement ----
let left = keys['ArrowLeft'] || keys['KeyA'];
let right = keys['ArrowRight'] || keys['KeyD'];
let dx = 0;
if (left) dx -= 1;
if (right) dx += 1;
const targetX = getPaddleTargetX();
if (targetX !== null) {
// smooth toward mouse
const desired = targetX;
const diff = desired - paddle.x;
paddle.x += clamp(diff, -paddle.speed * dt * 1.4, paddle.speed * dt * 1.4);
} else if (dx !== 0) {
paddle.x += dx * paddle.speed * dt;
}
paddle.x = clamp(paddle.x, 8, W - paddle.w - 8);
// Power timers
if (activePowers.wide > 0) {
activePowers.wide -= dt * 1000;
if (activePowers.wide <= 0) {
paddle.w = LEVELS[Math.min(level-1, LEVELS.length-1)].paddleW;
activePowers.wide = 0;
}
}
if (activePowers.slow > 0) {
activePowers.slow -= dt * 1000;
if (activePowers.slow <= 0) activePowers.slow = 0;
}
// ---- Ball aim when stuck ----
aimFromInput();
// ---- Balls ----
if (ballStuck) {
for (const b of ballsArr) {
b.x = paddle.x + paddle.w / 2;
b.y = paddle.y - 12;
}
} else {
for (let bi = ballsArr.length - 1; bi >= 0; bi--) {
const b = ballsArr[bi];
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 10) b.trail.shift();
// apply slow power
let spMul = 1;
if (activePowers.slow > 0) spMul = 0.78;
b.x += b.vx * dt * spMul;
b.y += b.vy * dt * spMul;
// walls
if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx); blip(420, 0.04, 'square', 0.04); spark(b.x, b.y, '#7df9ff'); }
if (b.x + b.r > W) { b.x = W - b.r; b.vx = -Math.abs(b.vx); blip(420, 0.04, 'square', 0.04); spark(b.x, b.y, '#7df9ff'); }
if (b.y - b.r < 0) { b.y = b.r; b.vy = Math.abs(b.vy); blip(520, 0.04, 'square', 0.04, 80); spark(b.x, b.y, '#7df9ff'); }
// paddle collision (only if moving down)
if (b.vy > 0 && circleRect(b.x, b.y, b.r, paddle.x, paddle.y, paddle.w, paddle.h)) {
b.y = paddle.y - b.r - 0.5;
// angle based on hit position
const hit = (b.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2);
const baseAngle = -Math.PI / 2; // straight up
const maxA = Math.PI / 3;
const ang = baseAngle + clamp(hit, -1, 1) * maxA;
const sp = Math.max(260, Math.hypot(b.vx, b.vy));
b.vx = Math.cos(ang) * sp;
b.vy = Math.sin(ang) * sp;
// tiny upward nudge to prevent stickiness
b.y -= 0.5;
blip(640, 0.05, 'triangle', 0.05, 200);
spark(b.x, b.y, '#7df9ff');
addShake(1.2);
}
// bricks collision
for (const br of bricks) {
if (!br.alive) continue;
if (circleRect(b.x, b.y, b.r, br.x, br.y, br.w, br.h)) {
// determine collision side for nicer reflection
const prevX = b.x - b.vx * dt * spMul;
const prevY = b.y - b.vy * dt * spMul;
const wasLeft = prevX + b.r <= br.x;
const wasRight = prevX - b.r >= br.x + br.w;
const wasAbove = prevY + b.r <= br.y;
const wasBelow = prevY - b.r >= br.y + br.h;
if (wasLeft || wasRight) b.vx = -b.vx;
else if (wasAbove || wasBelow) b.vy = -b.vy;
else { b.vx = -b.vx; b.vy = -b.vy; }
br.hp -= 1;
br.flash = 0.18;
if (br.hp <= 0) {
br.alive = false;
onBrickDestroyed(br);
} else {
// damaged brick
burst(b.x, b.y, br.color[0], 6, 120, 350);
blip(300, 0.05, 'square', 0.05, -120);
}
break;
}
}
// fell off bottom
if (b.y - b.r > H) {
ballsArr.splice(bi, 1);
}
}
if (ballsArr.length === 0) {
// lose a life
lives -= 1;
addShake(14);
noiseBurst(0.25, 0.1, 600);
if (lives <= 0) {
gameOver();
return;
} else {
// respawn a single ball stuck to paddle
spawnBallOnPaddle();
ballStuck = true;
}
}
}
// ---- Powerups ----
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.age += dt * 1000;
p.y += p.vy * dt;
if (p.y > H + 30) { powerups.splice(i, 1); continue; }
if (circleRect(p.x, p.y, 10, paddle.x, paddle.y, paddle.w, paddle.h)) {
applyPowerup(p.type);
floatText(p.x, p.y - 8, powerLabel(p.type), powerColor(p.type));
burst(p.x, p.y, powerColor(p.type), 18, 220, 500);
blip(900, 0.12, 'sine', 0.07, 200);
powerups.splice(i, 1);
}
}
// ---- Combo ----
if (comboTimer > 0) {
comboTimer -= dt * 1000;
if (comboTimer <= 0) { combo = 0; updateHUD(); }
}
stepParticles(dt);
shake = Math.max(0, shake - dt * 40);
// Level clear?
if (bricks.every(b => !b.alive)) {
onLevelClear();
}
}
function powerLabel(t) {
return { wide: 'WIDE!', slow: 'SLOW!', multi: 'MULTI!', life: '+1 LIFE', shrink: 'SHRINK!' }[t] || t.toUpperCase();
}
function powerColor(t) {
return { wide: '#7df9ff', slow: '#9d7dff', multi: '#ff5fd0', life: '#5cffb1', shrink: '#ff8a5c' }[t] || '#fff';
}
function onBrickDestroyed(br) {
combo += 1;
comboTimer = COMBO_WINDOW;
const base = 10 * br.row + 10; // higher rows worth more
const gained = base * combo;
score += gained;
floatText(br.x + br.w / 2, br.y + br.h / 2, '+' + gained, '#ffffff');
burst(br.x + br.w / 2, br.y + br.h / 2, br.color[0], 22, 280, 650);
burst(br.x + br.w / 2, br.y + br.h / 2, br.color[1], 12, 180, 500);
noiseBurst(0.06, 0.05, 2400);
addShake(2 + Math.min(4, combo * 0.4));
if (combo > 1) blip(740 + combo * 24, 0.08, 'square', 0.05, 80);
else blip(520, 0.06, 'square', 0.05, 80);
maybeDropPowerup(br.x + br.w / 2, br.y + br.h / 2);
updateHUD();
}
function onLevelClear() {
state = STATE.LEVEL_CLEAR;
score += level * 100; // level bonus
addShake(6);
noiseBurst(0.18, 0.07, 1800);
blip(660, 0.18, 'sine', 0.08, 300);
if (score > best) { best = score; try { localStorage.setItem('neon-breakout-best', String(best)); } catch (e) {} }
updateHUD();
const last = level >= LEVELS.length;
showOverlay(
'Level ' + level + ' Cleared!',
last ? 'Final stretch unlocked. Press Space to continue.' : 'Ready for the next one?',
`<span>Score: ${score}</span> · <span>Best: ${best}</span>`,
[{ label: last ? 'Continue' : 'Next Level', primary: true, action: () => nextLevel() },
{ label: 'Restart', primary: false, action: () => startNewGame() }]
);
}
function gameOver() {
state = STATE.GAME_OVER;
if (score > best) { best = score; try { localStorage.setItem('neon-breakout-best', String(best)); } catch (e) {} }
updateHUD();
addShake(20);
noiseBurst(0.4, 0.12, 400);
showOverlay(
'Game Over',
'The neon has dimmed.',
`<span>Final Score: ${score}</span> · <span>Best: ${best}</span> · <span>Reached Level ${level}</span>`,
[{ label: 'Play Again', primary: true, action: () => startNewGame() }]
);
}
function stepParticles(dt) {
const g = 360; // gravity for powerups
// powerups
for (let i = powerups.length - 1; i >= 0; i--) {
// handled in update; left here for safety
}
// particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.age += dt * 1000;
if (p.age >= p.life) { particles.splice(i, 1); continue; }
p.vy += g * 0.4 * dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vx *= 0.99;
}
// sparks
for (let i = sparks.length - 1; i >= 0; i--) {
const s = sparks[i];
s.age += dt * 1000;
if (s.age >= s.life) { sparks.splice(i, 1); continue; }
}
// floats
for (let i = floats.length - 1; i >= 0; i--) {
const f = floats[i];
f.age += dt * 1000;
f.y -= 30 * dt;
if (f.age >= f.life) floats.splice(i, 1);
}
}
// ---------- Render ----------
function drawBackground() {
// Subtle moving grid
if (highQuality) {
ctx.save();
ctx.globalAlpha = 0.35;
const off = (performance.now() * 0.02) % 40;
ctx.strokeStyle = 'rgba(125, 249, 255, 0.05)';
ctx.lineWidth = 1;
for (let x = -off; x < W; x += 40) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = -off; y < H; y += 40) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
ctx.restore();
}
}
function drawBricks() {
for (const br of bricks) {
if (!br.alive) continue;
const base = br.color[0];
const hi = br.color[1];
// brick body with vertical gradient
const grad = ctx.createLinearGradient(0, br.y, 0, br.y + br.h);
grad.addColorStop(0, hi);
grad.addColorStop(1, base);
ctx.fillStyle = grad;
const r = 4;
roundRect(ctx, br.x, br.y, br.w, br.h, r);
ctx.fill();
// inner glow border
ctx.strokeStyle = 'rgba(255,255,255,' + (0.15 + br.flash * 0.6) + ')';
ctx.lineWidth = 1;
roundRect(ctx, br.x + 0.5, br.y + 0.5, br.w - 1, br.h - 1, r);
ctx.stroke();
// top sheen
ctx.fillStyle = 'rgba(255,255,255,0.10)';
roundRect(ctx, br.x + 2, br.y + 1.5, br.w - 4, 3, 2);
ctx.fill();
// damaged overlay
if (br.hp < br.maxHp) {
ctx.fillStyle = 'rgba(0,0,0,0.35)';
roundRect(ctx, br.x, br.y, br.w, br.h, r);
ctx.fill();
}
// flash decay
if (br.flash > 0) br.flash = Math.max(0, br.flash - 0.016);
}
}
function drawPaddle() {
// glow
ctx.save();
ctx.shadowColor = paddle.color;
ctx.shadowBlur = activePowers.wide > 0 ? 28 : 16;
const grad = ctx.createLinearGradient(0, paddle.y, 0, paddle.y + paddle.h);
grad.addColorStop(0, '#bff8ff');
grad.addColorStop(1, '#7df9ff');
ctx.fillStyle = grad;
roundRect(ctx, paddle.x, paddle.y, paddle.w, paddle.h, 7);
ctx.fill();
ctx.restore();
// highlight
ctx.fillStyle = 'rgba(255,255,255,0.18)';
roundRect(ctx, paddle.x + 4, paddle.y + 1.5, paddle.w - 8, 3, 2);
ctx.fill();
}
function drawBalls() {
for (const b of ballsArr) {
// trail
ctx.lineWidth = b.r * 0.9;
for (let i = 0; i < b.trail.length; i++) {
const t = b.trail[i];
const a = (i + 1) / b.trail.length;
ctx.strokeStyle = 'rgba(125, 249, 255,' + (0.05 + a * 0.25) + ')';
ctx.beginPath();
ctx.arc(t.x, t.y, b.r * (0.3 + a * 0.7), 0, Math.PI * 2);
ctx.stroke();
}
ctx.save();
ctx.shadowColor = '#7df9ff';
ctx.shadowBlur = 18;
const grad = ctx.createRadialGradient(b.x - 2, b.y - 2, 1, b.x, b.y, b.r);
grad.addColorStop(0, '#ffffff');
grad.addColorStop(0.4, '#bff8ff');
grad.addColorStop(1, '#7df9ff');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// stuck: aim line
if (b.stuck && state === STATE.PLAYING) {
const a = launchAim;
const sp = LEVELS[Math.min(level-1, LEVELS.length-1)].ballSpeed;
const ex = b.x + Math.cos(a) * 60;
const ey = b.y + Math.sin(a) * 60;
ctx.strokeStyle = 'rgba(125, 249, 255, 0.7)';
ctx.setLineDash([4, 6]);
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(b.x, b.y);
ctx.lineTo(ex, ey);
ctx.stroke();
ctx.setLineDash([]);
}
}
}
function drawPowerups() {
for (const p of powerups) {
ctx.save();
ctx.translate(p.x, p.y);
const pulse = 1 + Math.sin(p.age * 0.01) * 0.08;
ctx.scale(pulse, pulse);
const col = powerColor(p.type);
ctx.shadowColor = col;
ctx.shadowBlur = 16;
const grad = ctx.createLinearGradient(0, -10, 0, 10);
grad.addColorStop(0, '#ffffff');
grad.addColorStop(1, col);
ctx.fillStyle = grad;
roundRect(ctx, -10, -8, 20, 16, 5);
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = '#000';
ctx.font = 'bold 10px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const letter = { wide: 'W', slow: 'S', multi: 'M', life: '+', shrink: '-' }[p.type] || '?';
ctx.fillText(letter, 0, 1);
ctx.restore();
}
}
function drawParticles() {
for (const p of particles) {
const a = 1 - p.age / p.life;
ctx.fillStyle = p.color;
ctx.globalAlpha = a;
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
}
ctx.globalAlpha = 1;
for (const s of sparks) {
const a = 1 - s.age / s.life;
ctx.fillStyle = s.color;
ctx.globalAlpha = a;
ctx.beginPath();
ctx.arc(s.x, s.y, 2 + a * 2, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
for (const f of floats) {
const a = 1 - f.age / f.life;
ctx.globalAlpha = a;
ctx.fillStyle = f.color;
ctx.font = 'bold 16px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.fillText(f.text, f.x, f.y);
}
ctx.globalAlpha = 1;
}
function roundRect(ctx, x, y, w, h, r) {
const rr = Math.min(r, w / 2, h / 2);
ctx.beginPath();
ctx.moveTo(x + rr, y);
ctx.arcTo(x + w, y, x + w, y + h, rr);
ctx.arcTo(x + w, y + h, x, y + h, rr);
ctx.arcTo(x, y + h, x, y, rr);
ctx.arcTo(x, y, x + w, y, rr);
ctx.closePath();
}
function drawAimHUD() {
if (state === STATE.PLAYING && ballStuck) {
// subtle "Click / Space to launch" hint
ctx.save();
ctx.globalAlpha = 0.6 + Math.sin(performance.now() * 0.005) * 0.2;
ctx.fillStyle = '#7df9ff';
ctx.font = '12px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.fillText('SPACE / CLICK to launch', W / 2, H - 14);
ctx.restore();
}
}
function render() {
// shake transform
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, W, H);
if (shake > 0.1) {
const sx = rand(-shake, shake);
const sy = rand(-shake, shake);
ctx.translate(sx, sy);
}
drawBackground();
drawBricks();
drawPowerups();
drawParticles();
drawPaddle();
drawBalls();
drawAimHUD();
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
// ---------- HUD ----------
function updateHUD() {
scoreEl.textContent = score;
bestEl.textContent = best;
comboEl.textContent = 'x' + Math.max(1, combo);
livesEl.textContent = '●'.repeat(Math.max(0, lives)) + '○'.repeat(Math.max(0, 3 - lives));
ballsEl.textContent = 'x' + Math.max(1, ballsArr.length);
}
// ---------- Overlay ----------
function showOverlay(title, sub, big, buttons) {
titleEl.textContent = title;
subEl.textContent = sub;
bigEl.innerHTML = big;
btnsEl.innerHTML = '';
for (const b of buttons) {
const el = document.createElement('button');
el.className = 'btn' + (b.primary ? '' : ' secondary');
el.textContent = b.label;
el.addEventListener('click', b.action);
btnsEl.appendChild(el);
}
overlay.classList.remove('hidden');
}
function hideOverlay() {
overlay.classList.add('hidden');
}
// ---------- Main loop ----------
let last = performance.now();
function loop(now) {
const dt = Math.min(0.033, (now - last) / 1000);
last = now;
update(dt);
render();
requestAnimationFrame(loop);
}
// ---------- Boot ----------
startBtn.addEventListener('click', () => startNewGame());
// Init first level so the background scene has bricks, even on menu
buildLevel(0);
state = STATE.MENU;
showOverlay(
'Neon Breakout',
'Smash every brick. Don\u2019t drop the ball.',
`<span>Best: ${best}</span>`,
[{ label: 'Start', primary: true, action: () => startNewGame() }]
);
// Start loop
requestAnimationFrame((t) => { last = t; loop(t); });
})();
</script>
</body>
</html>

