• Updated 12 Nov, 18:36 UTC
  • Next run 13 Nov, 05:17 UTCNext 13 Nov 05:17
  • Last 7 days: 9 new models · 67 runs7 d: 9 models · 67 runs
Playable Breakout

Gemini 4 HydrogenGoogle

Network offOpen alone (new tab)

Run details

Model
google/gemini-4-hydrogen
Route
OpenRouter API, one user message
Served by
Google
Effort
medium, OpenRouter's default for this model
Limits
60 min · 400,000 output tokens (our $3.00 stop per request at $7.50/M)
Date
12 Nov 2026
Time
1 min 36 s
Size
37 kB
Tokens
109 in, 15,340 out (496 of them thinking)
List price
$0.12 at today's price ($1.50 in / $7.50 out per M)
Paid
$0.12

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). Reply with the file in a single ```html code block.

Source

37 kB 1,385 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>NEON BREAKER</title>
<style>
  :root {
    --bg: #080b14;
    --panel: #0f1626;
    --border: #1f2b47;
    --text: #e6f0ff;
    --muted: #7c8db5;
    --accent: #00f0ff;
    --accent2: #ff2a85;
    --gold: #ffd166;
  }

  * {
    box-sizing: border-box;
    user-select: none;
    -webkit-user-select: none;
  }

  html, body {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    background: radial-gradient(circle at 50% 20%, #121a30 0%, var(--bg) 70%);
    color: var(--text);
    font-family: 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }

  .game-shell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: 12px;
    max-width: 100vw;
    max-height: 100vh;
  }

  .hud {
    width: 800px;
    max-width: 96vw;
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: rgba(15, 22, 38, 0.85);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 10px 16px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
    gap: 12px;
    flex-wrap: wrap;
  }

  .hud-group {
    display: flex;
    align-items: center;
    gap: 18px;
  }

  .stat {
    display: flex;
    flex-direction: column;
    line-height: 1.1;
  }

  .stat-label {
    font-size: 10px;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    color: var(--muted);
    font-weight: 700;
  }

  .stat-value {
    font-size: 20px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: var(--text);
  }

  .stat-value.accent { color: var(--accent); }
  .stat-value.gold { color: var(--gold); }

  .lives-dots {
    display: flex;
    gap: 6px;
    margin-top: 4px;
  }

  .life-dot {
    width: 14px;
    height: 7px;
    border-radius: 4px;
    background: var(--accent2);
    box-shadow: 0 0 8px var(--accent2);
    transition: opacity 0.2s, transform 0.2s;
  }

  .life-dot.lost {
    opacity: 0.18;
    box-shadow: none;
    transform: scale(0.85);
  }

  .controls-bar {
    display: flex;
    align-items: center;
    gap: 8px;
  }

  button.hud-btn {
    background: #17213a;
    color: var(--text);
    border: 1px solid #2b3c63;
    border-radius: 8px;
    padding: 6px 11px;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.5px;
    cursor: pointer;
    transition: transform 0.1s, background 0.15s, border-color 0.15s;
  }

  button.hud-btn:hover {
    background: #202e50;
    border-color: var(--accent);
  }

  button.hud-btn:active {
    transform: scale(0.95);
  }

  .stage-wrap {
    position: relative;
    width: 800px;
    max-width: 96vw;
    aspect-ratio: 4 / 3;
    max-height: calc(100vh - 125px);
    border-radius: 14px;
    overflow: hidden;
    border: 2px solid var(--border);
    box-shadow: 0 18px 50px rgba(0, 0, 0, 0.65), 0 0 35px rgba(0, 240, 255, 0.08);
    background: #060911;
  }

  canvas {
    width: 100%;
    height: 100%;
    display: block;
    cursor: none;
    touch-action: none;
  }

  .overlay {
    position: absolute;
    inset: 0;
    background: rgba(6, 9, 17, 0.78);
    backdrop-filter: blur(5px);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 24px;
    transition: opacity 0.25s ease;
    cursor: default;
  }

  .overlay.hidden {
    opacity: 0;
    pointer-events: none;
  }

  .title {
    font-size: clamp(30px, 5vw, 52px);
    font-weight: 900;
    letter-spacing: 4px;
    margin: 0 0 8px 0;
    background: linear-gradient(90deg, var(--accent), #7b61ff, var(--accent2));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    text-shadow: 0 0 30px rgba(0, 240, 255, 0.25);
  }

  .subtitle {
    color: var(--muted);
    font-size: 15px;
    max-width: 460px;
    line-height: 1.5;
    margin: 0 0 22px 0;
  }

  .cta-btn {
    background: linear-gradient(135deg, var(--accent), #2e86ff);
    color: #050912;
    border: none;
    border-radius: 10px;
    padding: 13px 30px;
    font-size: 16px;
    font-weight: 900;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    cursor: pointer;
    box-shadow: 0 6px 25px rgba(0, 240, 255, 0.35);
    transition: transform 0.12s, box-shadow 0.15s;
  }

  .cta-btn:hover {
    transform: translateY(-2px) scale(1.02);
    box-shadow: 0 10px 30px rgba(0, 240, 255, 0.5);
  }

  .cta-btn:active {
    transform: translateY(1px) scale(0.98);
  }

  .legend {
    margin-top: 22px;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 12px 18px;
    font-size: 12px;
    color: #a9b8d9;
  }

  .legend-item {
    display: flex;
    align-items: center;
    gap: 6px;
  }

  .pill {
    display: inline-block;
    padding: 2px 7px;
    border-radius: 5px;
    font-weight: 800;
    font-size: 10px;
    color: #060911;
  }

  .footer-hint {
    font-size: 12px;
    color: var(--muted);
    text-align: center;
  }

  kbd {
    background: #17213a;
    border: 1px solid #2b3c63;
    border-radius: 4px;
    padding: 1px 6px;
    font-size: 11px;
    color: var(--text);
    font-family: inherit;
  }
</style>
</head>
<body>

<div class="game-shell">
  <div class="hud">
    <div class="hud-group">
      <div class="stat">
        <span class="stat-label">Score</span>
        <span class="stat-value accent" id="scoreEl">0</span>
      </div>
      <div class="stat">
        <span class="stat-label">Best</span>
        <span class="stat-value gold" id="bestEl">0</span>
      </div>
      <div class="stat">
        <span class="stat-label">Level</span>
        <span class="stat-value" id="levelEl">1 / 5</span>
      </div>
      <div class="stat">
        <span class="stat-label">Lives</span>
        <div class="lives-dots" id="livesEl"></div>
      </div>
    </div>

    <div class="controls-bar">
      <button class="hud-btn" id="pauseBtn" title="Pause (P)">PAUSE</button>
      <button class="hud-btn" id="muteBtn" title="Toggle Sound (M)">SOUND: ON</button>
      <button class="hud-btn" id="restartBtn" title="Restart Game (R)">RESTART</button>
    </div>
  </div>

  <div class="stage-wrap">
    <canvas id="game" width="800" height="600"></canvas>

    <div class="overlay" id="overlay">
      <h1 class="title" id="overlayTitle">NEON BREAKER</h1>
      <p class="subtitle" id="overlaySub">
        Smash through 5 handcrafted neon stages. Build combos, catch power-ups, and keep the ball alive.
      </p>
      <button class="cta-btn" id="overlayBtn">PLAY NOW</button>
      <div class="legend" id="overlayLegend">
        <div class="legend-item"><span class="pill" style="background:#00f0ff">WIDE</span> Expand Paddle</div>
        <div class="legend-item"><span class="pill" style="background:#ff2a85">MULTI</span> +2 Balls</div>
        <div class="legend-item"><span class="pill" style="background:#ff9f1c">FIRE</span> Piercing Ball</div>
        <div class="legend-item"><span class="pill" style="background:#70e000">1UP</span> Extra Life</div>
        <div class="legend-item"><span class="pill" style="background:#b388ff">SLOW</span> Chill Speed</div>
      </div>
    </div>
  </div>

  <div class="footer-hint">
    Move: <strong>Mouse</strong> / <strong>Touch</strong> or <kbd>←</kbd> <kbd>→</kbd> / <kbd>A</kbd> <kbd>D</kbd> &nbsp;•&nbsp;
    Launch: <strong>Click</strong> or <kbd>Space</kbd> &nbsp;•&nbsp;
    Pause: <kbd>P</kbd> &nbsp;•&nbsp;
    Mute: <kbd>M</kbd>
  </div>
</div>

<script>
(() => {
  'use strict';

  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const W = canvas.width;
  const H = canvas.height;

  // UI refs
  const scoreEl = document.getElementById('scoreEl');
  const bestEl = document.getElementById('bestEl');
  const levelEl = document.getElementById('levelEl');
  const livesEl = document.getElementById('livesEl');
  const overlay = document.getElementById('overlay');
  const overlayTitle = document.getElementById('overlayTitle');
  const overlaySub = document.getElementById('overlaySub');
  const overlayBtn = document.getElementById('overlayBtn');
  const overlayLegend = document.getElementById('overlayLegend');
  const pauseBtn = document.getElementById('pauseBtn');
  const muteBtn = document.getElementById('muteBtn');
  const restartBtn = document.getElementById('restartBtn');

  // --- Audio Synthesizer (Web Audio API, zero external assets) ---
  class SoundFX {
    constructor() {
      this.ctx = null;
      this.muted = false;
    }
    ensure() {
      if (this.muted) return null;
      if (!this.ctx) {
        const AudioCtx = window.AudioContext || window.webkitAudioContext;
        if (AudioCtx) this.ctx = new AudioCtx();
      }
      if (this.ctx && this.ctx.state === 'suspended') {
        this.ctx.resume().catch(() => {});
      }
      return this.ctx;
    }
    tone(freq, dur, type = 'sine', vol = 0.12, slide = 0) {
      const ac = this.ensure();
      if (!ac) return;
      const now = ac.currentTime;
      const osc = ac.createOscillator();
      const gain = ac.createGain();
      osc.type = type;
      osc.frequency.setValueAtTime(freq, now);
      if (slide) osc.frequency.exponentialRampToValueAtTime(Math.max(40, freq + slide), now + dur);
      gain.gain.setValueAtTime(vol, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + dur);
      osc.connect(gain);
      gain.connect(ac.destination);
      osc.start(now);
      osc.stop(now + dur);
    }
    paddle() { this.tone(360, 0.08, 'triangle', 0.14, 140); }
    wall() { this.tone(220, 0.05, 'sine', 0.08, -40); }
    brick(combo) {
      const pitch = Math.min(1100, 340 + combo * 42);
      this.tone(pitch, 0.11, 'square', 0.075, 110);
    }
    armorHit() { this.tone(170, 0.07, 'sawtooth', 0.08, -50); }
    explode() {
      this.tone(130, 0.24, 'sawtooth', 0.14, -95);
      this.tone(85, 0.28, 'triangle', 0.16, -50);
    }
    powerup() {
      this.tone(520, 0.09, 'sine', 0.11, 180);
      setTimeout(() => this.tone(780, 0.14, 'triangle', 0.11, 220), 65);
    }
    loseLife() {
      this.tone(260, 0.18, 'sawtooth', 0.13, -120);
      setTimeout(() => this.tone(150, 0.28, 'sawtooth', 0.13, -80), 140);
    }
    levelWin() {
      [523, 659, 784, 1046].forEach((f, i) => {
        setTimeout(() => this.tone(f, 0.16, 'triangle', 0.12, 30), i * 85);
      });
    }
  }
  const sfx = new SoundFX();

  // --- Levels ---
  // Legend:
  // . = empty
  // 1-5 = standard colorful bricks (1 HP)
  // A = Armored brick (2 HP)
  // H = Heavy armored brick (3 HP)
  // X = Explosive bomb brick (clears neighbors!)
  // M = Unbreakable metal block (doesn't need clearing)
  const LEVELS = [
    {
      name: 'NEON GATE',
      speed: 390,
      rows: [
        '..11111111..',
        '.2222222222.',
        '33333XX33333',
        '.4444444444.',
        '..55555555..',
      ]
    },
    {
      name: 'PRISM FORTRESS',
      speed: 420,
      rows: [
        '11.AAAAAA.11',
        '22.22XX22.22',
        '33.333333.33',
        '44.AA44AA.44',
        '55.555555.55',
        '..MM....MM..'
      ]
    },
    {
      name: 'CYBER MATRIX',
      speed: 445,
      rows: [
        'H.1.2.3.2.1.H',
        '1.A.3.X.3.A.1',
        '2.3.4.5.4.3.2',
        '3.A.5.X.5.A.3',
        '4.5.M.M.M.5.4',
        '5.5.5.5.5.5.5'
      ].map(r => r.replace(/\./g, '')) // normalize to 12 cols
    },
    {
      name: 'REACTOR CORE',
      speed: 470,
      rows: [
        'M..HHHHHH..M',
        '11.AXAAXA.11',
        '22.A4444A.22',
        '33.AXAAXA.33',
        '44.HHHHHH.44',
        '5555.MM.5555'
      ]
    },
    {
      name: 'OMEGA CITADEL',
      speed: 500,
      rows: [
        'HHHHHHHHHHHH',
        '11X22AA22X11',
        '33344XX44333',
        'MM.AAAAAA.MM',
        '555.HXXH.555',
        '123451234512'
      ]
    }
  ];

  // Fix level 3 rows cleanly to 12 chars
  LEVELS[2].rows = [
    'H1232112321H',
    '1A3X3AA3X3A1',
    '234542245432',
    '3A5X5AA5X5A3',
    '45MM4444MM54',
    '555555555555'
  ];

  const COLORS = {
    '1': { fill: '#00f0ff', glow: '#00f0ff' },
    '2': { fill: '#3a86ff', glow: '#3a86ff' },
    '3': { fill: '#b5179e', glow: '#f72585' },
    '4': { fill: '#ff2a85', glow: '#ff2a85' },
    '5': { fill: '#ffd166', glow: '#ffd166' },
    'A': { fill: '#94a3b8', glow: '#cbd5e1' },
    'H': { fill: '#e2e8f0', glow: '#ffffff' },
    'X': { fill: '#ff6b00', glow: '#ff9f1c' },
    'M': { fill: '#334155', glow: '#475569' }
  };

  const POWERUPS = [
    { type: 'WIDE', label: 'WIDE', color: '#00f0ff' },
    { type: 'MULTI', label: 'MULTI', color: '#ff2a85' },
    { type: 'FIRE', label: 'FIRE', color: '#ff9f1c' },
    { type: 'SLOW', label: 'SLOW', color: '#b388ff' },
    { type: 'LIFE', label: '1UP', color: '#70e000' }
  ];

  // --- Game State ---
  let state = 'MENU'; // MENU, PLAYING, PAUSED, LEVEL_CLEAR, GAMEOVER, VICTORY
  let score = 0;
  let bestScore = 0;
  try {
    bestScore = parseInt(localStorage.getItem('neon_breaker_best') || '0', 10) || 0;
  } catch (e) {}
  let levelIdx = 0;
  let lives = 3;
  let combo = 0;
  let shakeTime = 0;
  let shakeMag = 0;
  let flashAlpha = 0;

  // Paddle
  const paddle = {
    w: 112,
    baseW: 112,
    h: 16,
    x: W / 2 - 56,
    y: H - 42,
    speed: 640,
    hitScale: 0,
    wideTimer: 0
  };

  // Entities
  let balls = [];
  let bricks = [];
  let particles = [];
  let powerups = [];
  let popups = [];
  let bgStars = Array.from({ length: 55 }, () => ({
    x: Math.random() * W,
    y: Math.random() * H,
    r: Math.random() * 1.6 + 0.4,
    a: Math.random() * 0.5 + 0.15,
    s: Math.random() * 18 + 6
  }));

  // Input
  const keys = { left: false, right: false };
  let pointerX = null;

  // --- Helpers ---
  function saveBest() {
    if (score > bestScore) {
      bestScore = score;
      try { localStorage.setItem('neon_breaker_best', String(bestScore)); } catch (e) {}
    }
  }

  function updateHUD() {
    scoreEl.textContent = score.toLocaleString();
    bestEl.textContent = bestScore.toLocaleString();
    levelEl.textContent = `${levelIdx + 1} / ${LEVELS.length}`;
    livesEl.innerHTML = '';
    const maxDots = Math.max(3, lives);
    for (let i = 0; i < maxDots; i++) {
      const dot = document.createElement('div');
      dot.className = 'life-dot' + (i >= lives ? ' lost' : '');
      livesEl.appendChild(dot);
    }
  }

  function addShake(mag, dur) {
    shakeMag = Math.max(shakeMag, mag);
    shakeTime = Math.max(shakeTime, dur);
  }

  function addPopup(x, y, text, color = '#ffffff') {
    popups.push({ x, y, text, color, life: 0.85, maxLife: 0.85 });
  }

  function spawnParticles(x, y, color, count = 14, speed = 220) {
    for (let i = 0; i < count; i++) {
      const ang = Math.random() * Math.PI * 2;
      const spd = (0.3 + Math.random() * 0.9) * speed;
      particles.push({
        x, y,
        vx: Math.cos(ang) * spd,
        vy: Math.sin(ang) * spd,
        size: 2 + Math.random() * 3.5,
        color,
        life: 0.35 + Math.random() * 0.35,
        maxLife: 0.7
      });
    }
  }

  function createBall(stuck = true, x = null, y = null, vx = 0, vy = 0) {
    const baseSpeed = LEVELS[levelIdx].speed;
    return {
      x: x ?? (paddle.x + paddle.w / 2),
      y: y ?? (paddle.y - 9),
      vx,
      vy,
      r: 8,
      speed: baseSpeed,
      stuck,
      fireTimer: 0,
      trail: []
    };
  }

  function launchStuckBalls() {
    let launched = false;
    for (const b of balls) {
      if (b.stuck) {
        b.stuck = false;
        const spread = (Math.random() - 0.5) * 0.55;
        b.vx = Math.sin(spread) * b.speed;
        b.vy = -Math.cos(spread) * b.speed;
        launched = true;
      }
    }
    if (launched) sfx.paddle();
  }

  function buildLevel(idx) {
    bricks = [];
    powerups = [];
    particles = [];
    popups = [];
    combo = 0;
    paddle.wideTimer = 0;
    paddle.w = paddle.baseW;
    paddle.x = Math.min(W - paddle.w - 10, Math.max(10, paddle.x));

    const lvl = LEVELS[idx];
    const cols = 12;
    const padX = 36;
    const gap = 6;
    const totalW = W - padX * 2;
    const bw = (totalW - gap * (cols - 1)) / cols;
    const bh = 24;
    const startY = 74;

    lvl.rows.forEach((rowStr, r) => {
      for (let c = 0; c < cols; c++) {
        const ch = rowStr[c];
        if (!ch || ch === '.') continue;
        let hp = 1;
        let breakable = true;
        if (ch === 'A') hp = 2;
        else if (ch === 'H') hp = 3;
        else if (ch === 'M') { hp = Infinity; breakable = false; }

        bricks.push({
          row: r,
          col: c,
          x: padX + c * (bw + gap),
          y: startY + r * (bh + gap),
          w: bw,
          h: bh,
          type: ch,
          hp,
          maxHp: hp,
          breakable,
          hitFlash: 0
        });
      }
    });

    balls = [createBall(true)];
    updateHUD();
  }

  function startNewGame() {
    score = 0;
    lives = 3;
    levelIdx = 0;
    paddle.x = W / 2 - paddle.baseW / 2;
    buildLevel(levelIdx);
    state = 'PLAYING';
    overlay.classList.add('hidden');
    pauseBtn.textContent = 'PAUSE';
    updateHUD();
  }

  function showOverlay(title, sub, btnText, showLegend = false) {
    overlayTitle.textContent = title;
    overlaySub.textContent = sub;
    overlayBtn.textContent = btnText;
    overlayLegend.style.display = showLegend ? 'flex' : 'none';
    overlay.classList.remove('hidden');
  }

  function togglePause() {
    if (state === 'PLAYING') {
      state = 'PAUSED';
      pauseBtn.textContent = 'RESUME';
      showOverlay('PAUSED', 'Take a breather. Click Resume or press P to jump back in.', 'RESUME');
    } else if (state === 'PAUSED') {
      state = 'PLAYING';
      pauseBtn.textContent = 'PAUSE';
      overlay.classList.add('hidden');
    }
  }

  function maybeDropPowerup(brick) {
    const chance = brick.type === 'A' || brick.type === 'H' ? 0.34 : 0.2;
    if (Math.random() > chance) return;
    const def = POWERUPS[Math.floor(Math.random() * POWERUPS.length)];
    powerups.push({
      x: brick.x + brick.w / 2,
      y: brick.y + brick.h / 2,
      w: 44,
      h: 18,
      vy: 145,
      ...def
    });
  }

  function destroyBrick(brick, isChain = false) {
    if (!brick.breakable || brick.hp <= 0) return;
    brick.hp = 0;
    combo++;
    const mult = Math.min(5, 1 + Math.floor(combo / 4));
    const basePts = brick.type === 'H' ? 150 : brick.type === 'A' ? 100 : brick.type === 'X' ? 120 : 50;
    const pts = basePts * mult;
    score += pts;
    saveBest();

    const col = COLORS[brick.type].fill;
    const cx = brick.x + brick.w / 2;
    const cy = brick.y + brick.h / 2;
    spawnParticles(cx, cy, col, brick.type === 'X' ? 26 : 14, brick.type === 'X' ? 300 : 210);
    addPopup(cx, cy, `+${pts}${mult > 1 ? ` x${mult}` : ''}`, col);

    if (!isChain) sfx.brick(combo);
    addShake(brick.type === 'X' ? 8 : 2.5, brick.type === 'X' ? 0.22 : 0.08);
    maybeDropPowerup(brick);

    // Bomb brick chain explosion
    if (brick.type === 'X') {
      sfx.explode();
      flashAlpha = 0.22;
      for (const other of bricks) {
        if (other !== brick && other.breakable && other.hp > 0) {
          const dr = Math.abs(other.row - brick.row);
          const dc = Math.abs(other.col - brick.col);
          if (dr <= 1 && dc <= 1) {
            destroyBrick(other, true);
          }
        }
      }
    }
  }

  function applyPowerup(p) {
    sfx.powerup();
    addPopup(p.x, p.y - 10, p.label + '!', p.color);
    score += 75;
    saveBest();

    if (p.type === 'WIDE') {
      paddle.wideTimer = 11;
      paddle.w = Math.min(200, paddle.baseW * 1.55);
      paddle.x = Math.min(W - paddle.w - 8, paddle.x);
    } else if (p.type === 'MULTI') {
      const ref = balls.find(b => !b.stuck) || balls[0];
      if (ref) {
        if (ref.stuck) launchStuckBalls();
        for (let i = 0; i < 2; i++) {
          const angle = (i === 0 ? -0.45 : 0.45) + (Math.random() - 0.5) * 0.2;
          const spd = ref.speed;
          const nb = createBall(false, ref.x, ref.y, Math.sin(angle) * spd, -Math.abs(Math.cos(angle) * spd));
          nb.fireTimer = ref.fireTimer;
          balls.push(nb);
        }
      }
    } else if (p.type === 'FIRE') {
      for (const b of balls) b.fireTimer = 7.5;
    } else if (p.type === 'SLOW') {
      for (const b of balls) {
        b.speed = Math.max(290, b.speed * 0.8);
        const mag = Math.hypot(b.vx, b.vy) || 1;
        b.vx = (b.vx / mag) * b.speed;
        b.vy = (b.vy / mag) * b.speed;
      }
    } else if (p.type === 'LIFE') {
      lives = Math.min(5, lives + 1);
    }
    updateHUD();
  }

  // --- Physics & Update ---
  function update(dt) {
    // Background stars always drift subtly
    for (const st of bgStars) {
      st.y += st.s * dt;
      if (st.y > H) { st.y = 0; st.x = Math.random() * W; }
    }

    if (state !== 'PLAYING') return;

    // Shake & flash decay
    if (shakeTime > 0) {
      shakeTime = Math.max(0, shakeTime - dt);
      if (shakeTime === 0) shakeMag = 0;
    }
    if (flashAlpha > 0) flashAlpha = Math.max(0, flashAlpha - dt * 1.2);
    if (paddle.hitScale > 0) paddle.hitScale = Math.max(0, paddle.hitScale - dt * 5);

    // Wide timer
    if (paddle.wideTimer > 0) {
      paddle.wideTimer -= dt;
      if (paddle.wideTimer <= 0) {
        const center = paddle.x + paddle.w / 2;
        paddle.w = paddle.baseW;
        paddle.x = Math.max(8, Math.min(W - paddle.w - 8, center - paddle.w / 2));
      }
    }

    // Paddle movement (keyboard or mouse)
    if (keys.left) {
      paddle.x -= paddle.speed * dt;
      pointerX = null;
    }
    if (keys.right) {
      paddle.x += paddle.speed * dt;
      pointerX = null;
    }
    if (pointerX !== null) {
      const target = pointerX - paddle.w / 2;
      paddle.x += (target - paddle.x) * Math.min(1, dt * 28);
    }
    paddle.x = Math.max(8, Math.min(W - paddle.w - 8, paddle.x));

    // Brick flash decay
    for (const br of bricks) {
      if (br.hitFlash > 0) br.hitFlash = Math.max(0, br.hitFlash - dt * 5);
    }

    // Update balls with substeps to prevent tunneling
    for (let bi = balls.length - 1; bi >= 0; bi--) {
      const b = balls[bi];
      if (b.fireTimer > 0) b.fireTimer = Math.max(0, b.fireTimer - dt);

      if (b.stuck) {
        b.x = paddle.x + paddle.w / 2;
        b.y = paddle.y - b.r - 1;
        b.trail = [];
        continue;
      }

      // Trail
      b.trail.push({ x: b.x, y: b.y });
      if (b.trail.length > 10) b.trail.shift();

      const steps = Math.max(1, Math.ceil((b.speed * dt) / 6));
      const stepDt = dt / steps;

      for (let s = 0; s < steps; s++) {
        b.x += b.vx * stepDt;
        b.y += b.vy * stepDt;

        // Wall collisions
        if (b.x - b.r < 6) {
          b.x = 6 + b.r;
          b.vx = Math.abs(b.vx);
          sfx.wall();
          spawnParticles(b.x, b.y, '#00f0ff', 5, 110);
        } else if (b.x + b.r > W - 6) {
          b.x = W - 6 - b.r;
          b.vx = -Math.abs(b.vx);
          sfx.wall();
          spawnParticles(b.x, b.y, '#00f0ff', 5, 110);
        }
        if (b.y - b.r < 6) {
          b.y = 6 + b.r;
          b.vy = Math.abs(b.vy);
          sfx.wall();
          spawnParticles(b.x, b.y, '#00f0ff', 5, 110);
        }

        // Paddle collision
        if (
          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
        ) {
          const hitPos = ((b.x - paddle.x) / paddle.w) * 2 - 1; // -1..1
          const clamped = Math.max(-0.92, Math.min(0.92, hitPos));
          const maxAngle = Math.PI * 0.37; // ~66 deg
          const angle = clamped * maxAngle;

          // Slightly ramp speed on paddle hit up to cap
          b.speed = Math.min(680, b.speed + 4);
          b.vx = Math.sin(angle) * b.speed;
          b.vy = -Math.cos(angle) * b.speed;
          b.y = paddle.y - b.r - 0.5;

          combo = 0;
          paddle.hitScale = 1;
          sfx.paddle();
          spawnParticles(b.x, paddle.y, '#00f0ff', 8, 150);
          break;
        }

        // Brick collisions
        for (const br of bricks) {
          if (br.hp <= 0) continue;
          const closestX = Math.max(br.x, Math.min(b.x, br.x + br.w));
          const closestY = Math.max(br.y, Math.min(b.y, br.y + br.h));
          const dx = b.x - closestX;
          const dy = b.y - closestY;

          if (dx * dx + dy * dy <= b.r * b.r) {
            const isFire = b.fireTimer > 0 && br.breakable;

            if (!isFire) {
              // Determine reflection axis
              const overlapLeft = (b.x + b.r) - br.x;
              const overlapRight = (br.x + br.w) - (b.x - b.r);
              const overlapTop = (b.y + b.r) - br.y;
              const overlapBottom = (br.y + br.h) - (b.y - b.r);
              const minOverlapX = Math.min(overlapLeft, overlapRight);
              const minOverlapY = Math.min(overlapTop, overlapBottom);

              if (minOverlapX < minOverlapY) {
                b.vx = -b.vx;
                b.x += b.vx > 0 ? minOverlapX : -minOverlapX;
              } else {
                b.vy = -b.vy;
                b.y += b.vy > 0 ? minOverlapY : -minOverlapY;
              }

              // Prevent horizontal deadlock
              if (Math.abs(b.vy) < b.speed * 0.22) {
                b.vy = (b.vy >= 0 ? 1 : -1) * b.speed * 0.24;
                const mag = Math.hypot(b.vx, b.vy);
                b.vx = (b.vx / mag) * b.speed;
                b.vy = (b.vy / mag) * b.speed;
              }
            }

            if (br.breakable) {
              if (isFire) {
                destroyBrick(br);
              } else {
                br.hp -= 1;
                br.hitFlash = 1;
                if (br.hp <= 0) {
                  destroyBrick(br);
                } else {
                  sfx.armorHit();
                  addShake(2, 0.06);
                  spawnParticles(closestX, closestY, COLORS[br.type].fill, 7, 130);
                }
              }
            } else {
              // Unbreakable metal block
              br.hitFlash = 1;
              sfx.armorHit();
              spawnParticles(closestX, closestY, '#94a3b8', 6, 120);
            }

            updateHUD();
            if (!isFire) break;
          }
        }
      }

      // Fell below bottom
      if (b.y - b.r > H + 12) {
        balls.splice(bi, 1);
      }
    }

    // Powerups fall & collect
    for (let i = powerups.length - 1; i >= 0; i--) {
      const p = powerups[i];
      p.y += p.vy * dt;
      if (
        p.y + p.h / 2 >= paddle.y &&
        p.y - p.h / 2 <= paddle.y + paddle.h &&
        p.x + p.w / 2 >= paddle.x &&
        p.x - p.w / 2 <= paddle.x + paddle.w
      ) {
        applyPowerup(p);
        spawnParticles(p.x, p.y, p.color, 14, 180);
        powerups.splice(i, 1);
        continue;
      }
      if (p.y - p.h > H) powerups.splice(i, 1);
    }

    // Particles
    for (let i = particles.length - 1; i >= 0; i--) {
      const pt = particles[i];
      pt.x += pt.vx * dt;
      pt.y += pt.vy * dt;
      pt.vx *= 0.96;
      pt.vy *= 0.96;
      pt.life -= dt;
      if (pt.life <= 0) particles.splice(i, 1);
    }

    // Popups
    for (let i = popups.length - 1; i >= 0; i--) {
      const pp = popups[i];
      pp.y -= 36 * dt;
      pp.life -= dt;
      if (pp.life <= 0) popups.splice(i, 1);
    }

    // Lost all balls?
    if (balls.length === 0) {
      lives--;
      combo = 0;
      sfx.loseLife();
      addShake(9, 0.3);
      flashAlpha = 0.25;
      updateHUD();

      if (lives <= 0) {
        state = 'GAMEOVER';
        saveBest();
        showOverlay(
          'GAME OVER',
          `Final Score: ${score.toLocaleString()} • Reached Level ${levelIdx + 1}`,
          'TRY AGAIN'
        );
      } else {
        paddle.wideTimer = 0;
        paddle.w = paddle.baseW;
        balls = [createBall(true)];
      }
      return;
    }

    // Check level clear (all breakable bricks cleared)
    const remaining = bricks.some(b => b.breakable && b.hp > 0);
    if (!remaining) {
      sfx.levelWin();
      score += 500 * (levelIdx + 1);
      saveBest();
      updateHUD();

      if (levelIdx + 1 >= LEVELS.length) {
        state = 'VICTORY';
        showOverlay(
          'YOU WIN!',
          `All ${LEVELS.length} Neon Stages Cleared! Final Score: ${score.toLocaleString()}`,
          'PLAY AGAIN'
        );
      } else {
        state = 'LEVEL_CLEAR';
        showOverlay(
          `STAGE ${levelIdx + 1} CLEAR!`,
          `Next up: ${LEVELS[levelIdx + 1].name} (+${500 * (levelIdx + 1)} Stage Bonus)`,
          'NEXT STAGE'
        );
      }
    }
  }

  // --- Rendering ---
  function roundRect(x, y, w, h, r) {
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.arcTo(x + w, y, x + w, y + h, r);
    ctx.arcTo(x + w, y + h, x, y + h, r);
    ctx.arcTo(x, y + h, x, y, r);
    ctx.arcTo(x, y, x + w, y, r);
    ctx.closePath();
  }

  function render() {
    ctx.save();

    // Screen shake offset
    if (shakeTime > 0) {
      const sx = (Math.random() - 0.5) * 2 * shakeMag;
      const sy = (Math.random() - 0.5) * 2 * shakeMag;
      ctx.translate(sx, sy);
    }

    // Background
    ctx.fillStyle = '#060911';
    ctx.fillRect(0, 0, W, H);

    // Subtle cyber grid
    ctx.strokeStyle = 'rgba(31, 43, 71, 0.28)';
    ctx.lineWidth = 1;
    for (let x = 0; x <= W; x += 40) {
      ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
    }
    for (let y = 0; y <= H; y += 40) {
      ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
    }

    // Stars
    for (const st of bgStars) {
      ctx.fillStyle = `rgba(180, 215, 255, ${st.a})`;
      ctx.beginPath();
      ctx.arc(st.x, st.y, st.r, 0, Math.PI * 2);
      ctx.fill();
    }

    // Arena inner border glow
    ctx.strokeStyle = 'rgba(0, 240, 255, 0.25)';
    ctx.lineWidth = 3;
    ctx.strokeRect(4, 4, W - 8, H + 10);

    // Level name watermark
    ctx.fillStyle = 'rgba(124, 141, 181, 0.14)';
    ctx.font = '900 18px system-ui, sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(`STAGE ${levelIdx + 1}: ${LEVELS[levelIdx].name}`, W / 2, 38);

    // Bricks
    for (const br of bricks) {
      if (br.hp <= 0) continue;
      const col = COLORS[br.type];
      ctx.save();
      ctx.shadowColor = col.glow;
      ctx.shadowBlur = br.type === 'X' ? 14 : 8;

      ctx.fillStyle = br.hitFlash > 0.2 ? '#ffffff' : col.fill;
      roundRect(br.x, br.y, br.w, br.h, 5);
      ctx.fill();

      // Top highlight
      ctx.shadowBlur = 0;
      ctx.fillStyle = 'rgba(255,255,255,0.22)';
      roundRect(br.x + 2, br.y + 2, br.w - 4, 5, 2);
      ctx.fill();

      // Armored cracks / HP pips
      if (br.type === 'A' || br.type === 'H') {
        ctx.fillStyle = 'rgba(6, 9, 17, 0.65)';
        const pips = br.hp;
        for (let p = 0; p < pips; p++) {
          ctx.beginPath();
          ctx.arc(br.x + br.w / 2 + (p - (pips - 1) / 2) * 9, br.y + br.h / 2 + 2, 2.5, 0, Math.PI * 2);
          ctx.fill();
        }
      } else if (br.type === 'X') {
        ctx.fillStyle = '#060911';
        ctx.font = '900 12px system-ui, sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText('★', br.x + br.w / 2, br.y + br.h / 2 + 1);
      } else if (br.type === 'M') {
        ctx.strokeStyle = '#64748b';
        ctx.lineWidth = 1.5;
        roundRect(br.x + 3, br.y + 3, br.w - 6, br.h - 6, 3);
        ctx.stroke();
      }
      ctx.restore();
    }

    // Powerups
    for (const p of powerups) {
      ctx.save();
      ctx.shadowColor = p.color;
      ctx.shadowBlur = 12;
      ctx.fillStyle = p.color;
      roundRect(p.x - p.w / 2, p.y - p.h / 2, p.w, p.h, 9);
      ctx.fill();
      ctx.shadowBlur = 0;
      ctx.fillStyle = '#060911';
      ctx.font = '900 10px system-ui, sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(p.label, p.x, p.y + 0.5);
      ctx.restore();
    }

    // Paddle
    ctx.save();
    const stretch = paddle.hitScale * 6;
    const px = paddle.x - stretch / 2;
    const py = paddle.y + paddle.hitScale * 2;
    const pw = paddle.w + stretch;
    const ph = paddle.h - paddle.hitScale * 2;

    ctx.shadowColor = paddle.wideTimer > 0 ? '#00f0ff' : '#3a86ff';
    ctx.shadowBlur = 16;
    const grad = ctx.createLinearGradient(px, py, px + pw, py);
    grad.addColorStop(0, '#00f0ff');
    grad.addColorStop(0.5, '#60a5fa');
    grad.addColorStop(1, '#00f0ff');
    ctx.fillStyle = paddle.hitScale > 0.35 ? '#ffffff' : grad;
    roundRect(px, py, pw, ph, 8);
    ctx.fill();

    // Paddle core line
    ctx.shadowBlur = 0;
    ctx.fillStyle = 'rgba(255,255,255,0.65)';
    roundRect(px + 10, py + 3, pw - 20, 3, 2);
    ctx.fill();
    ctx.restore();

    // Balls + trails
    for (const b of balls) {
      const isFire = b.fireTimer > 0;
      const ballCol = isFire ? '#ff9f1c' : '#ffffff';
      const glowCol = isFire ? '#ff5400' : '#00f0ff';

      // Trail
      for (let i = 0; i < b.trail.length; i++) {
        const t = b.trail[i];
        const frac = (i + 1) / b.trail.length;
        ctx.fillStyle = isFire
          ? `rgba(255, 130, 20, ${frac * 0.45})`
          : `rgba(0, 240, 255, ${frac * 0.35})`;
        ctx.beginPath();
        ctx.arc(t.x, t.y, b.r * frac * 0.85, 0, Math.PI * 2);
        ctx.fill();
      }

      ctx.save();
      ctx.shadowColor = glowCol;
      ctx.shadowBlur = isFire ? 20 : 14;
      ctx.fillStyle = ballCol;
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();

      // Launch aim hint if stuck
      if (b.stuck && state === 'PLAYING') {
        ctx.save();
        ctx.fillStyle = 'rgba(230, 240, 255, 0.65)';
        ctx.font = '700 12px system-ui, sans-serif';
        ctx.textAlign = 'center';
        ctx.fillText('CLICK OR SPACE TO LAUNCH', W / 2, paddle.y - 26);
        ctx.restore();
      }
    }

    // Particles
    for (const pt of particles) {
      ctx.globalAlpha = Math.max(0, pt.life / pt.maxLife);
      ctx.fillStyle = pt.color;
      ctx.fillRect(pt.x - pt.size / 2, pt.y - pt.size / 2, pt.size, pt.size);
    }
    ctx.globalAlpha = 1;

    // Popups
    for (const pp of popups) {
      ctx.save();
      ctx.globalAlpha = Math.max(0, pp.life / pp.maxLife);
      ctx.fillStyle = pp.color;
      ctx.font = '900 14px system-ui, sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText(pp.text, pp.x, pp.y);
      ctx.restore();
    }

    // Active buff indicators
    const buffs = [];
    if (paddle.wideTimer > 0) buffs.push(`WIDE ${Math.ceil(paddle.wideTimer)}s`);
    const maxFire = balls.reduce((m, b) => Math.max(m, b.fireTimer), 0);
    if (maxFire > 0) buffs.push(`FIRE ${Math.ceil(maxFire)}s`);
    if (combo >= 3) buffs.push(`COMBO x${Math.min(5, 1 + Math.floor(combo / 4))}`);

    if (buffs.length > 0) {
      ctx.fillStyle = 'rgba(0, 240, 255, 0.85)';
      ctx.font = '800 12px system-ui, sans-serif';
      ctx.textAlign = 'left';
      ctx.fillText(buffs.join('  •  '), 16, H - 14);
    }

    // Screen flash
    if (flashAlpha > 0) {
      ctx.fillStyle = `rgba(255, 255, 255, ${flashAlpha})`;
      ctx.fillRect(0, 0, W, H);
    }

    ctx.restore();
  }

  // --- Loop ---
  let lastTs = performance.now();
  function loop(ts) {
    const dt = Math.min(0.033, (ts - lastTs) / 1000);
    lastTs = ts;
    update(dt);
    render();
    requestAnimationFrame(loop);
  }

  // --- Event Listeners ---
  function setPointerFromClient(clientX) {
    const rect = canvas.getBoundingClientRect();
    pointerX = ((clientX - rect.left) / rect.width) * W;
  }

  canvas.addEventListener('mousemove', (e) => {
    setPointerFromClient(e.clientX);
  });

  canvas.addEventListener('mousedown', (e) => {
    if (state === 'PLAYING') {
      setPointerFromClient(e.clientX);
      launchStuckBalls();
    }
  });

  canvas.addEventListener('touchstart', (e) => {
    if (e.touches[0]) {
      setPointerFromClient(e.touches[0].clientX);
      if (state === 'PLAYING') launchStuckBalls();
    }
  }, { passive: true });

  canvas.addEventListener('touchmove', (e) => {
    if (e.touches[0]) setPointerFromClient(e.touches[0].clientX);
  }, { passive: true });

  window.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
      keys.left = true;
    } else if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
      keys.right = true;
    } else if (e.key === ' ' || e.code === 'Space') {
      e.preventDefault();
      if (state === 'PLAYING') launchStuckBalls();
      else if (state === 'MENU' || state === 'GAMEOVER' || state === 'VICTORY') startNewGame();
      else if (state === 'LEVEL_CLEAR') {
        levelIdx++;
        buildLevel(levelIdx);
        state = 'PLAYING';
        overlay.classList.add('hidden');
      }
    } else if (e.key === 'p' || e.key === 'P') {
      togglePause();
    } else if (e.key === 'm' || e.key === 'M') {
      muteBtn.click();
    } else if (e.key === 'r' || e.key === 'R') {
      startNewGame();
    }
  });

  window.addEventListener('keyup', (e) => {
    if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') keys.left = false;
    else if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') keys.right = false;
  });

  overlayBtn.addEventListener('click', () => {
    sfx.ensure();
    if (state === 'PAUSED') {
      togglePause();
    } else if (state === 'LEVEL_CLEAR') {
      levelIdx++;
      buildLevel(levelIdx);
      state = 'PLAYING';
      overlay.classList.add('hidden');
    } else {
      startNewGame();
    }
  });

  pauseBtn.addEventListener('click', togglePause);
  restartBtn.addEventListener('click', startNewGame);
  muteBtn.addEventListener('click', () => {
    sfx.muted = !sfx.muted;
    muteBtn.textContent = sfx.muted ? 'SOUND: OFF' : 'SOUND: ON';
  });

  // Init
  buildLevel(0);
  updateHUD();
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Compare with

Gemini 4 Hydrogen: other attempts