hypeorcode
Network offOpen alone (new tab)

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)

Raw files: output · meta.json

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 &nbsp;·&nbsp;
        <span class="key">Mouse</span> aim &nbsp;·&nbsp; <span class="key">Space</span> launch &nbsp;·&nbsp; <span class="key">P</span> pause &nbsp;·&nbsp; <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> &nbsp;·&nbsp; <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> &nbsp;·&nbsp; <span>Best: ${best}</span> &nbsp;·&nbsp; <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>

Compare with

MiniMax M3: other attempts