hypeorcode
Network offOpen alone (new tab)

Run details

Model
google/gemini-4-argon
Route
OpenRouter API, one user message
Reasoning
provider default
Limits
60 min · 60,000 output tokens
Date
21 Oct 2026
Time
2 min 46 s
Size
35 kB
Tokens
109 in, 13,836 out
List price
$0.14 ($2 in / $10 out per M)
Paid
$0.14

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

35 kB 1,328 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Juicy Neon Breakout</title>
<style>
  :root {
    --bg: #07070c;
    --panel: #11111e;
    --accent: #00f3ff;
    --accent2: #ff00a0;
    --text: #ffffff;
  }

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

  body, html {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    background-color: var(--bg);
    color: var(--text);
    font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    overflow: hidden;
  }

  #game-container {
    position: relative;
    width: 800px;
    height: 600px;
    max-width: 100vw;
    max-height: 100vh;
    aspect-ratio: 4 / 3;
    box-shadow: 0 0 40px rgba(0, 243, 255, 0.15), inset 0 0 20px rgba(0, 243, 255, 0.1);
    border: 2px solid #22223b;
    border-radius: 8px;
    overflow: hidden;
    background: radial-gradient(circle at center, #101024 0%, #07070c 100%);
  }

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

  /* CRT Scanline Overlay */
  .scanlines {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: linear-gradient(
      to bottom,
      rgba(255,255,255,0),
      rgba(255,255,255,0) 50%,
      rgba(0, 0, 0, 0.25) 50%,
      rgba(0, 0, 0, 0.25)
    );
    background-size: 100% 4px;
    pointer-events: none;
    z-index: 10;
  }

  /* UI Overlay */
  #ui-layer {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    z-index: 20;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
  }

  .hud {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 25px;
    font-size: 18px;
    font-weight: 800;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    text-shadow: 0 0 10px rgba(0, 243, 255, 0.6);
  }

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

  .stat-box span {
    color: var(--accent);
    font-variant-numeric: tabular-nums;
  }

  #combo-display {
    color: var(--accent2);
    text-shadow: 0 0 10px var(--accent2);
    opacity: 0;
    transition: opacity 0.2s, transform 0.1s;
    display: inline-block;
  }

  #mute-btn {
    pointer-events: auto;
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(0, 243, 255, 0.4);
    color: var(--text);
    padding: 6px 12px;
    border-radius: 4px;
    cursor: pointer;
    font-weight: bold;
    font-size: 12px;
    letter-spacing: 1px;
    transition: all 0.2s;
  }

  #mute-btn:hover {
    background: var(--accent);
    color: #000;
    box-shadow: 0 0 12px var(--accent);
  }

  /* Menus */
  #menu-overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(7, 7, 12, 0.82);
    backdrop-filter: blur(4px);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    pointer-events: auto;
    z-index: 30;
    transition: opacity 0.3s ease;
  }

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

  h1 {
    font-size: 56px;
    margin: 0 0 10px 0;
    letter-spacing: 4px;
    text-transform: uppercase;
    background: linear-gradient(90deg, #00f3ff, #ff00a0, #ffea00);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    filter: drop-shadow(0 0 15px rgba(255, 0, 160, 0.4));
    text-align: center;
  }

  .subtitle {
    color: #aaaadd;
    font-size: 18px;
    margin-bottom: 35px;
    text-align: center;
    max-width: 80%;
    line-height: 1.5;
  }

  .btn {
    background: transparent;
    color: var(--accent);
    border: 2px solid var(--accent);
    padding: 14px 40px;
    font-size: 20px;
    font-weight: 900;
    letter-spacing: 2px;
    text-transform: uppercase;
    border-radius: 50px;
    cursor: pointer;
    box-shadow: 0 0 15px rgba(0, 243, 255, 0.3), inset 0 0 15px rgba(0, 243, 255, 0.2);
    transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  }

  .btn:hover {
    background: var(--accent);
    color: #05050a;
    box-shadow: 0 0 30px var(--accent);
    transform: scale(1.06);
  }

  .btn:active {
    transform: scale(0.98);
  }

  .controls-hint {
    margin-top: 40px;
    display: flex;
    gap: 30px;
    color: #777799;
    font-size: 13px;
    letter-spacing: 1px;
    text-transform: uppercase;
  }

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

  .key {
    border: 1px solid #555577;
    padding: 3px 8px;
    border-radius: 4px;
    background: rgba(255,255,255,0.05);
    color: #bbb;
    font-weight: bold;
  }
</style>
</head>
<body>

<div id="game-container">
  <canvas id="canvas" width="800" height="600"></canvas>
  <div class="scanlines"></div>

  <div id="ui-layer">
    <div class="hud">
      <div class="hud-group">
        <div class="stat-box">SCORE: <span id="score-el">0</span></div>
        <div class="stat-box">HIGH: <span id="high-el">0</span></div>
        <div id="combo-display">2x COMBO!</div>
      </div>
      <div class="hud-group">
        <div class="stat-box">LEVEL: <span id="level-el">1</span></div>
        <div class="stat-box">LIVES: <span id="lives-el">3</span></div>
        <button id="mute-btn">SOUND: ON</button>
      </div>
    </div>
  </div>

  <div id="menu-overlay">
    <h1 id="menu-title">NEON BREAKOUT</h1>
    <div class="subtitle" id="menu-subtitle">Smash bricks, chain combos, and collect powerups in overdrive.</div>
    <button class="btn" id="action-btn">PLAY NOW</button>
    <div class="controls-hint">
      <div><span class="key">MOUSE</span> or <span class="key">←</span> <span class="key">→</span> Move</div>
      <div><span class="key">CLICK</span> or <span class="key">SPACE</span> Launch / Lasers</div>
      <div><span class="key">P</span> Pause</div>
    </div>
  </div>
</div>

<script>
(() => {
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const W = canvas.width;
  const H = canvas.height;

  // UI Elements
  const scoreEl = document.getElementById('score-el');
  const highEl = document.getElementById('high-el');
  const levelEl = document.getElementById('level-el');
  const livesEl = document.getElementById('lives-el');
  const comboEl = document.getElementById('combo-display');
  const muteBtn = document.getElementById('mute-btn');
  const menuOverlay = document.getElementById('menu-overlay');
  const menuTitle = document.getElementById('menu-title');
  const menuSubtitle = document.getElementById('menu-subtitle');
  const actionBtn = document.getElementById('action-btn');

  // --- WEB AUDIO SYNTHESIZER ---
  class SoundFX {
    constructor() {
      this.ctx = null;
      this.muted = false;
    }

    init() {
      if (!this.ctx) {
        const AudioCtx = window.AudioContext || window.webkitAudioContext;
        this.ctx = new AudioCtx();
      }
      if (this.ctx.state === 'suspended') {
        this.ctx.resume();
      }
    }

    toggleMute() {
      this.muted = !this.muted;
      muteBtn.textContent = `SOUND: ${this.muted ? 'OFF' : 'ON'}`;
    }

    tone(freq, type, duration, vol = 0.15, slideFreq = null) {
      if (this.muted || !this.ctx) return;
      const now = this.ctx.currentTime;
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = type;
      osc.frequency.setValueAtTime(freq, now);
      if (slideFreq) {
        osc.frequency.exponentialRampToValueAtTime(slideFreq, now + duration);
      }
      gain.gain.setValueAtTime(vol, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + duration);
      osc.connect(gain);
      gain.connect(this.ctx.destination);
      osc.start(now);
      osc.stop(now + duration);
    }

    noise(duration, vol = 0.2) {
      if (this.muted || !this.ctx) return;
      const now = this.ctx.currentTime;
      const bufferSize = this.ctx.sampleRate * duration;
      const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
      const data = buffer.getChannelData(0);
      for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;

      const noise = this.ctx.createBufferSource();
      noise.buffer = buffer;

      const filter = this.ctx.createBiquadFilter();
      filter.type = 'bandpass';
      filter.frequency.setValueAtTime(800, now);
      filter.frequency.linearRampToValueAtTime(120, now + duration);

      const gain = this.ctx.createGain();
      gain.gain.setValueAtTime(vol, now);
      gain.gain.exponentialRampToValueAtTime(0.01, now + duration);

      noise.connect(filter);
      filter.connect(gain);
      gain.connect(this.ctx.destination);
      noise.start(now);
    }

    paddle() { this.tone(240, 'triangle', 0.12, 0.25, 340); }
    wall()   { this.tone(150, 'sine', 0.08, 0.2, 110); }
    brick(combo, unbreakable = false) {
      if (unbreakable) {
        this.tone(110, 'square', 0.08, 0.15, 90);
        return;
      }
      // Pentatonic scale pitch escalation
      const scale = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25, 587.33, 659.25, 783.99, 880.00, 1046.50];
      const idx = Math.min(combo, scale.length - 1);
      const freq = scale[idx];
      this.tone(freq, 'sawtooth', 0.16, 0.14, freq * 1.4);
      this.noise(0.09, 0.12);
    }
    powerup() {
      this.tone(330, 'sine', 0.09, 0.18, 660);
      setTimeout(() => this.tone(550, 'sine', 0.16, 0.18, 990), 70);
    }
    laser() { this.tone(880, 'sawtooth', 0.12, 0.1, 180); }
    loseLife() {
      this.tone(280, 'sawtooth', 0.45, 0.25, 55);
      this.noise(0.35, 0.25);
    }
    winLevel() {
      [261.6, 329.6, 392.0, 523.3, 659.3].forEach((f, i) => {
        setTimeout(() => this.tone(f, 'triangle', 0.22, 0.2), i * 85);
      });
    }
  }

  const sfx = new SoundFX();
  muteBtn.addEventListener('click', () => {
    sfx.toggleMute();
    muteBtn.blur();
  });

  // --- COLORS & LEVELS ---
  const BRICK_TYPES = {
    1: { hp: 1, color: '#ff0055', glow: '#ff0055', score: 100 }, // Neon Red
    2: { hp: 1, color: '#ff7700', glow: '#ff7700', score: 120 }, // Neon Orange
    3: { hp: 1, color: '#ffdd00', glow: '#ffdd00', score: 140 }, // Neon Yellow
    4: { hp: 1, color: '#00ff66', glow: '#00ff66', score: 160 }, // Neon Green
    5: { hp: 1, color: '#00f3ff', glow: '#00f3ff', score: 180 }, // Neon Cyan
    6: { hp: 1, color: '#bd00ff', glow: '#bd00ff', score: 200 }, // Neon Purple
    7: { hp: 2, color: '#e2e8f0', glow: '#94a3b8', score: 350 }, // Silver (2 HP)
    8: { hp: 3, color: '#fbbf24', glow: '#f59e0b', score: 500 }, // Gold (3 HP)
    9: { hp: Infinity, color: '#334155', glow: '#475569', score: 0 } // Indestructible
  };

  const LEVELS = [
    // Level 1: Classic Neon Rainbow
    {
      name: "NEON DAWN",
      grid: [
        "000000000000",
        "111111111111",
        "222222222222",
        "333333333333",
        "444444444444",
        "555555555555"
      ]
    },
    // Level 2: Twin Pyramids & Silver Guard
    {
      name: "TWIN PEAKS",
      grid: [
        "001100001100",
        "012210012210",
        "123321123321",
        "234432234432",
        "345543345543",
        "777777777777"
      ]
    },
    // Level 3: Cyber Invader
    {
      name: "CYBER INVADER",
      grid: [
        "000400004000",
        "000040040000",
        "000444444000",
        "004414414400",
        "044444444440",
        "040444444040",
        "040400004040",
        "000077770000"
      ]
    },
    // Level 4: The Fortress (Bumpers & Gold Core)
    {
      name: "THE CITADEL",
      grid: [
        "777777777777",
        "788888888887",
        "786666666687",
        "786111111687",
        "786155551687",
        "990055550099",
        "000077770000"
      ]
    },
    // Level 5: Overdrive Matrix
    {
      name: "OVERDRIVE CORE",
      grid: [
        "808080808080",
        "010203040506",
        "707070707070",
        "060504030201",
        "990808080899",
        "555555555555",
        "777777777777"
      ]
    }
  ];

  const POWERUP_TYPES = [
    { type: 'MULTI', color: '#00f3ff', label: 'MULTI' },
    { type: 'WIDE',  color: '#00ff66', label: 'WIDE'  },
    { type: 'LASER', color: '#ff0055', label: 'LASER' },
    { type: 'FIRE',  color: '#ff7700', label: 'FIRE'  },
    { type: 'LIFE',  color: '#ff00a0', label: '+1 UP' }
  ];

  // --- GAME STATE ---
  let state = 'MENU'; // MENU, PLAYING, PAUSED, LEVEL_CLEAR, GAMEOVER, VICTORY
  let score = 0;
  let highScore = parseInt(localStorage.getItem('neon_breakout_high') || '0', 10);
  highEl.textContent = highScore;

  let levelIdx = 0;
  let lives = 3;
  let combo = 0;

  // Juice variables
  let shakeMag = 0;
  let bgPulse = 0;
  let scoreScale = 1;
  let paddleSquish = 1;

  // Entities
  const paddle = {
    x: W / 2 - 55,
    y: H - 40,
    w: 110,
    targetW: 110,
    h: 14,
    speed: 620,
    vx: 0,
    laserTimer: 0,
    laserCooldown: 0
  };

  let balls = [];
  let bricks = [];
  let particles = [];
  let powerups = [];
  let lasers = [];
  let floatingTexts = [];
  let bgStars = [];

  // Initialize background stars
  for (let i = 0; i < 65; i++) {
    bgStars.push({
      x: Math.random() * W,
      y: Math.random() * H,
      r: Math.random() * 1.6 + 0.4,
      speed: Math.random() * 25 + 10,
      alpha: Math.random() * 0.6 + 0.2
    });
  }

  // --- INPUT HANDLING ---
  const keys = { ArrowLeft: false, ArrowRight: false, KeyA: false, KeyD: false };

  window.addEventListener('keydown', (e) => {
    if (keys.hasOwnProperty(e.code)) keys[e.code] = true;
    if (e.code === 'Space') {
      e.preventDefault();
      handleActionTrigger();
    }
    if (e.code === 'KeyP' || e.code === 'Escape') {
      if (state === 'PLAYING') pauseGame();
      else if (state === 'PAUSED') resumeGame();
    }
  });

  window.addEventListener('keyup', (e) => {
    if (keys.hasOwnProperty(e.code)) keys[e.code] = false;
  });

  canvas.addEventListener('mousemove', (e) => {
    if (state !== 'PLAYING') return;
    const rect = canvas.getBoundingClientRect();
    const scaleX = W / rect.width;
    const mouseX = (e.clientX - rect.left) * scaleX;
    const prevX = paddle.x;
    paddle.x = Math.max(10, Math.min(W - paddle.w - 10, mouseX - paddle.w / 2));
    paddle.vx = (paddle.x - prevX) * 30;
  });

  canvas.addEventListener('mousedown', (e) => {
    if (e.button === 0) handleActionTrigger();
  });

  // Touch support
  canvas.addEventListener('touchmove', (e) => {
    if (state !== 'PLAYING') return;
    e.preventDefault();
    const rect = canvas.getBoundingClientRect();
    const scaleX = W / rect.width;
    const touchX = (e.touches[0].clientX - rect.left) * scaleX;
    paddle.x = Math.max(10, Math.min(W - paddle.w - 10, touchX - paddle.w / 2));
  }, { passive: false });

  canvas.addEventListener('touchstart', (e) => {
    if (state === 'PLAYING') {
      e.preventDefault();
      handleActionTrigger();
    }
  }, { passive: false });

  function handleActionTrigger() {
    sfx.init();
    if (state !== 'PLAYING') return;

    let launched = false;
    for (const b of balls) {
      if (b.stuck) {
        b.stuck = false;
        const angle = -Math.PI / 2 + (Math.random() * 0.4 - 0.2);
        b.vx = Math.cos(angle) * b.speed;
        b.vy = Math.sin(angle) * b.speed;
        launched = true;
        sfx.paddle();
      }
    }

    if (!launched && paddle.laserTimer > 0 && paddle.laserCooldown <= 0) {
      shootLasers();
    }
  }

  function shootLasers() {
    lasers.push({ x: paddle.x + 10, y: paddle.y - 6, vy: -650 });
    lasers.push({ x: paddle.x + paddle.w - 10, y: paddle.y - 6, vy: -650 });
    paddle.laserCooldown = 0.22;
    sfx.laser();
  }

  // --- GAME FLOW ---
  actionBtn.addEventListener('click', () => {
    sfx.init();
    if (state === 'MENU' || state === 'GAMEOVER' || state === 'VICTORY') {
      startNewGame();
    } else if (state === 'PAUSED') {
      resumeGame();
    } else if (state === 'LEVEL_CLEAR') {
      levelIdx++;
      loadLevel(levelIdx);
      setMenu(false);
      state = 'PLAYING';
    }
  });

  function setMenu(visible, title = '', subtitle = '', btnText = '') {
    if (visible) {
      menuTitle.textContent = title;
      menuSubtitle.textContent = subtitle;
      actionBtn.textContent = btnText;
      menuOverlay.classList.remove('hidden');
    } else {
      menuOverlay.classList.add('hidden');
    }
  }

  function pauseGame() {
    state = 'PAUSED';
    setMenu(true, 'GAME PAUSED', 'Take a breather.', 'RESUME');
  }

  function resumeGame() {
    state = 'PLAYING';
    setMenu(false);
  }

  function startNewGame() {
    score = 0;
    lives = 3;
    levelIdx = 0;
    combo = 0;
    updateHUD();
    loadLevel(levelIdx);
    setMenu(false);
    state = 'PLAYING';
  }

  function loadLevel(idx) {
    bricks = [];
    powerups = [];
    lasers = [];
    particles = [];
    floatingTexts = [];

    const lvl = LEVELS[idx % LEVELS.length];
    const rows = lvl.grid.length;
    const cols = 12;
    const padX = 46;
    const padY = 75;
    const gap = 6;
    const bw = (W - padX * 2 - gap * (cols - 1)) / cols;
    const bh = 22;

    for (let r = 0; r < rows; r++) {
      const rowStr = lvl.grid[r];
      for (let c = 0; c < cols; c++) {
        const ch = parseInt(rowStr[c], 10);
        if (!ch || !BRICK_TYPES[ch]) continue;
        const bt = BRICK_TYPES[ch];
        bricks.push({
          x: padX + c * (bw + gap),
          y: padY + r * (bh + gap),
          w: bw,
          h: bh,
          type: ch,
          hp: bt.hp,
          maxHp: bt.hp,
          color: bt.color,
          glow: bt.glow,
          score: bt.score,
          hitScale: 0,
          entryDelay: (r * cols + c) * 0.012
        });
      }
    }

    resetPaddleAndBall();
    updateHUD();
    spawnFloatingText(W / 2, H / 2 + 20, `STAGE ${idx + 1}: ${lvl.name}`, '#00f3ff', 24, 1.8);
  }

  function resetPaddleAndBall() {
    paddle.targetW = 110;
    paddle.w = 110;
    paddle.x = W / 2 - paddle.w / 2;
    paddle.laserTimer = 0;
    combo = 0;
    updateComboUI();

    const baseSpeed = 420 + levelIdx * 28;
    balls = [{
      x: paddle.x + paddle.w / 2,
      y: paddle.y - 9,
      r: 7,
      vx: 0,
      vy: 0,
      speed: baseSpeed,
      stuck: true,
      fireTimer: 0,
      trail: []
    }];
  }

  function updateHUD() {
    scoreEl.textContent = score;
    if (score > highScore) {
      highScore = score;
      highEl.textContent = highScore;
      localStorage.setItem('neon_breakout_high', highScore);
    }
    levelEl.textContent = levelIdx + 1;
    livesEl.textContent = lives;
  }

  function updateComboUI() {
    if (combo >= 2) {
      comboEl.textContent = `${combo}x COMBO!`;
      comboEl.style.opacity = '1';
      comboEl.style.transform = 'scale(1.25)';
      setTimeout(() => { comboEl.style.transform = 'scale(1)'; }, 90);
    } else {
      comboEl.style.opacity = '0';
    }
  }

  // --- JUICE & EFFECTS ---
  function addShake(amount) {
    shakeMag = Math.min(shakeMag + amount, 18);
  }

  function spawnParticles(x, y, color, count = 16, speedMult = 1) {
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const spd = (Math.random() * 190 + 50) * speedMult;
      particles.push({
        x,
        y,
        vx: Math.cos(angle) * spd,
        vy: Math.sin(angle) * spd,
        size: Math.random() * 5 + 3,
        color,
        alpha: 1,
        life: Math.random() * 0.45 + 0.25,
        maxLife: 0.7,
        rot: Math.random() * Math.PI,
        vRot: (Math.random() - 0.5) * 10
      });
    }
  }

  function spawnFloatingText(x, y, text, color = '#fff', size = 15, life = 0.85) {
    floatingTexts.push({ x, y, text, color, size, life, maxLife: life, vy: -45 });
  }

  function maybeDropPowerup(x, y) {
    if (Math.random() > 0.19) return;
    const p = POWERUP_TYPES[Math.floor(Math.random() * POWERUP_TYPES.length)];
    powerups.push({
      x: x - 24,
      y: y - 10,
      w: 48,
      h: 20,
      vy: 145,
      type: p.type,
      color: p.color,
      label: p.label
    });
  }

  function activatePowerup(p) {
    sfx.powerup();
    score += 250;
    updateHUD();
    spawnFloatingText(paddle.x + paddle.w / 2, paddle.y - 15, p.label, p.color, 18);
    spawnParticles(paddle.x + paddle.w / 2, paddle.y, p.color, 20, 0.9);

    if (p.type === 'WIDE') {
      paddle.targetW = Math.min(210, paddle.targetW + 45);
    } else if (p.type === 'LASER') {
      paddle.laserTimer = 10;
    } else if (p.type === 'LIFE') {
      lives++;
      updateHUD();
    } else if (p.type === 'FIRE') {
      balls.forEach(b => b.fireTimer = 8);
    } else if (p.type === 'MULTI') {
      const source = balls[0];
      if (source && balls.length < 9) {
        for (const offset of [-0.3, 0.3]) {
          const baseAngle = source.stuck ? -Math.PI / 2 : Math.atan2(source.vy, source.vx);
          const newAngle = baseAngle + offset;
          balls.push({
            x: source.x,
            y: source.y,
            r: source.r,
            vx: Math.cos(newAngle) * source.speed,
            vy: Math.sin(newAngle) * source.speed,
            speed: source.speed,
            stuck: false,
            fireTimer: source.fireTimer,
            trail: []
          });
        }
      }
    }
  }

  function damageBrick(b, idx, isFireball = false) {
    if (b.type === 9) {
      if (!isFireball) {
        b.hitScale = 1;
        sfx.brick(combo, true);
        addShake(3);
        spawnParticles(b.x + b.w / 2, b.y + b.h / 2, '#94a3b8', 6, 0.6);
      }
      return;
    }

    b.hp = isFireball ? 0 : b.hp - 1;
    b.hitScale = 1;
    combo++;
    updateComboUI();
    sfx.brick(combo, false);

    const cx = b.x + b.w / 2;
    const cy = b.y + b.h / 2;

    if (b.hp <= 0) {
      bricks.splice(idx, 1);
      const mult = Math.min(combo, 8);
      const pts = b.score * mult;
      score += pts;
      updateHUD();

      addShake(6.5);
      bgPulse = 0.28;
      spawnParticles(cx, cy, b.color, 22, 1.15);
      spawnFloatingText(cx, cy, `+${pts}`, b.color, mult > 2 ? 17 : 14);
      maybeDropPowerup(cx, cy);
      checkLevelComplete();
    } else {
      addShake(3.5);
      spawnParticles(cx, cy, b.color, 9, 0.7);
    }
  }

  function checkLevelComplete() {
    const remaining = bricks.some(b => b.type !== 9);
    if (!remaining) {
      sfx.winLevel();
      if (levelIdx + 1 >= LEVELS.length) {
        state = 'VICTORY';
        setMenu(true, 'YOU CONQUERED NEON!', `Final Score: ${score}`, 'PLAY AGAIN');
      } else {
        state = 'LEVEL_CLEAR';
        setMenu(true, 'STAGE CLEARED!', `Get ready for Stage ${levelIdx + 2}`, 'NEXT STAGE');
      }
    }
  }

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

    // Juice decay
    if (shakeMag > 0) {
      shakeMag *= Math.pow(0.001, dt);
      if (shakeMag < 0.2) shakeMag = 0;
    }
    if (bgPulse > 0) bgPulse = Math.max(0, bgPulse - dt * 1.5);
    paddleSquish += (1 - paddleSquish) * 14 * dt;

    // Update particles
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.vy += 260 * dt; // subtle gravity
      p.rot += p.vRot * dt;
      p.life -= dt;
      p.alpha = Math.max(0, p.life / p.maxLife);
      if (p.life <= 0) particles.splice(i, 1);
    }

    // Update floating texts
    for (let i = floatingTexts.length - 1; i >= 0; i--) {
      const ft = floatingTexts[i];
      ft.y += ft.vy * dt;
      ft.life -= dt;
      if (ft.life <= 0) floatingTexts.splice(i, 1);
    }

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

    // Brick animations
    for (const b of bricks) {
      if (b.entryDelay > 0) b.entryDelay = Math.max(0, b.entryDelay - dt);
      if (b.hitScale > 0) b.hitScale = Math.max(0, b.hitScale - dt * 6);
    }

    // Paddle width lerp & timers
    if (Math.abs(paddle.w - paddle.targetW) > 0.5) {
      const diff = (paddle.targetW - paddle.w) * 10 * dt;
      paddle.x -= diff / 2;
      paddle.w += diff;
    }
    if (paddle.laserTimer > 0) paddle.laserTimer = Math.max(0, paddle.laserTimer - dt);
    if (paddle.laserCooldown > 0) paddle.laserCooldown = Math.max(0, paddle.laserCooldown - dt);

    // Keyboard movement
    let moveDir = 0;
    if (keys.ArrowLeft || keys.KeyA) moveDir -= 1;
    if (keys.ArrowRight || keys.KeyD) moveDir += 1;
    if (moveDir !== 0) {
      paddle.vx = moveDir * paddle.speed;
      paddle.x += paddle.vx * dt;
      paddle.x = Math.max(10, Math.min(W - paddle.w - 10, paddle.x));
    } else {
      paddle.vx *= 0.85;
    }

    // Lasers
    for (let i = lasers.length - 1; i >= 0; i--) {
      const l = lasers[i];
      l.y += l.vy * dt;
      if (l.y < 0) {
        lasers.splice(i, 1);
        continue;
      }
      for (let j = bricks.length - 1; j >= 0; j--) {
        const b = bricks[j];
        if (l.x >= b.x && l.x <= b.x + b.w && l.y >= b.y && l.y <= b.y + b.h) {
          damageBrick(b, j, false);
          lasers.splice(i, 1);
          break;
        }
      }
    }

    // Powerups
    for (let i = powerups.length - 1; i >= 0; i--) {
      const p = powerups[i];
      p.y += p.vy * dt;
      if (
        p.x + p.w >= paddle.x &&
        p.x <= paddle.x + paddle.w &&
        p.y + p.h >= paddle.y &&
        p.y <= paddle.y + paddle.h
      ) {
        activatePowerup(p);
        powerups.splice(i, 1);
      } else if (p.y > H) {
        powerups.splice(i, 1);
      }
    }

    // Balls
    for (let i = balls.length - 1; i >= 0; i--) {
      const b = balls[i];
      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;
        continue;
      }

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

      // Sub-stepping for crisp collision
      const steps = 3;
      const sDt = dt / steps;

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

        // Walls
        if (b.x - b.r < 0) {
          b.x = b.r;
          b.vx = Math.abs(b.vx);
          sfx.wall();
          spawnParticles(b.x, b.y, '#00f3ff', 5, 0.5);
        } else if (b.x + b.r > W) {
          b.x = W - b.r;
          b.vx = -Math.abs(b.vx);
          sfx.wall();
          spawnParticles(b.x, b.y, '#00f3ff', 5, 0.5);
        }

        if (b.y - b.r < 0) {
          b.y = b.r;
          b.vy = Math.abs(b.vy);
          sfx.wall();
          spawnParticles(b.x, b.y, '#00f3ff', 5, 0.5);
        }

        // 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)) / (paddle.w / 2);
          const clampedHit = Math.max(-1, Math.min(1, hitPos));
          const maxBounceAngle = Math.PI * 0.37; // ~66 deg
          const bounceAngle = clampedHit * maxBounceAngle;

          b.speed = Math.min(720, b.speed + 4);
          b.vx = Math.sin(bounceAngle) * b.speed;
          b.vy = -Math.cos(bounceAngle) * b.speed;
          b.y = paddle.y - b.r - 0.5;

          combo = 0;
          updateComboUI();
          paddleSquish = 0.65;
          sfx.paddle();
          spawnParticles(b.x, paddle.y, '#00f3ff', 8, 0.6);
          break;
        }

        // Bricks collision
        for (let j = bricks.length - 1; j >= 0; j--) {
          const br = bricks[j];
          if (br.entryDelay > 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.type !== 9;
            if (!isFire) {
              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;
              } else {
                b.vy = -b.vy;
              }

              // Prevent vertical infinite loops with indestructible blocks
              if (Math.abs(b.vy) < 60) {
                b.vy = (b.vy >= 0 ? 1 : -1) * 90;
              }
            }

            damageBrick(br, j, isFire);
            break;
          }
        }
      }

      // Bottom out
      if (b.y - b.r > H) {
        balls.splice(i, 1);
      }
    }

    // Check life lost
    if (balls.length === 0 && state === 'PLAYING') {
      lives--;
      updateHUD();
      sfx.loseLife();
      addShake(14);
      if (lives <= 0) {
        state = 'GAMEOVER';
        setMenu(true, 'GAME OVER', `Final Score: ${score}`, 'TRY AGAIN');
      } else {
        resetPaddleAndBall();
      }
    }
  }

  // --- RENDERING ---
  function drawRoundedRect(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
    if (shakeMag > 0) {
      const dx = (Math.random() - 0.5) * shakeMag * 2;
      const dy = (Math.random() - 0.5) * shakeMag * 2;
      ctx.translate(dx, dy);
    }

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

    // Subtle Grid & Pulse
    ctx.strokeStyle = `rgba(0, 243, 255, ${0.035 + bgPulse * 0.15})`;
    ctx.lineWidth = 1;
    const gridSize = 40;
    for (let x = 0; x < W; x += gridSize) {
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, H);
      ctx.stroke();
    }
    for (let y = 0; y < H; y += gridSize) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(W, y);
      ctx.stroke();
    }

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

    // Draw Bricks
    for (const b of bricks) {
      if (b.entryDelay > 0) continue;

      const pop = b.hitScale * 4;
      const bx = b.x - pop / 2;
      const by = b.y - pop / 2;
      const bw = b.w + pop;
      const bh = b.h + pop;

      ctx.save();
      ctx.shadowColor = b.glow;
      ctx.shadowBlur = 12;
      ctx.fillStyle = b.hitScale > 0.3 ? '#ffffff' : b.color;
      drawRoundedRect(bx, by, bw, bh, 4);
      ctx.fill();

      // Inner bevel highlight
      ctx.shadowBlur = 0;
      ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
      drawRoundedRect(bx + 2, by + 2, bw - 4, bh * 0.38, 2);
      ctx.fill();

      // Multi-hit cracks/dots
      if (b.maxHp > 1 && b.maxHp < Infinity) {
        ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';
        for (let i = 0; i < b.hp; i++) {
          ctx.beginPath();
          ctx.arc(bx + bw / 2 + (i - (b.hp - 1) / 2) * 10, by + bh / 2 + 2, 2.5, 0, Math.PI * 2);
          ctx.fill();
        }
      }
      ctx.restore();
    }

    // Draw Powerups
    for (const p of powerups) {
      ctx.save();
      ctx.shadowColor = p.color;
      ctx.shadowBlur = 12;
      ctx.fillStyle = '#11111e';
      ctx.strokeStyle = p.color;
      ctx.lineWidth = 2;
      drawRoundedRect(p.x, p.y, p.w, p.h, 8);
      ctx.fill();
      ctx.stroke();

      ctx.shadowBlur = 0;
      ctx.fillStyle = '#ffffff';
      ctx.font = 'bold 10px sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(p.label, p.x + p.w / 2, p.y + p.h / 2 + 1);
      ctx.restore();
    }

    // Draw Lasers
    ctx.save();
    ctx.shadowColor = '#ff0055';
    ctx.shadowBlur = 10;
    ctx.fillStyle = '#ff0055';
    for (const l of lasers) {
      ctx.fillRect(l.x - 2, l.y, 4, 14);
    }
    ctx.restore();

    // Draw Paddle
    ctx.save();
    const pColor = paddle.laserTimer > 0 ? '#ff0055' : '#00f3ff';
    ctx.shadowColor = pColor;
    ctx.shadowBlur = 16;
    ctx.fillStyle = pColor;

    const ph = paddle.h * paddleSquish;
    const py = paddle.y + (paddle.h - ph);
    drawRoundedRect(paddle.x, py, paddle.w, ph, 7);
    ctx.fill();

    // Paddle core
    ctx.shadowBlur = 0;
    ctx.fillStyle = '#ffffff';
    drawRoundedRect(paddle.x + 8, py + 3, paddle.w - 16, ph - 6, 4);
    ctx.fill();

    // Laser turrets if active
    if (paddle.laserTimer > 0) {
      ctx.fillStyle = '#ff0055';
      ctx.fillRect(paddle.x + 6, py - 5, 6, 5);
      ctx.fillRect(paddle.x + paddle.w - 12, py - 5, 6, 5);
    }
    ctx.restore();

    // Draw Balls & Trails
    for (const b of balls) {
      const isFire = b.fireTimer > 0;
      const bColor = isFire ? '#ff7700' : '#00f3ff';

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

      // Ball body
      ctx.save();
      ctx.shadowColor = bColor;
      ctx.shadowBlur = 15;
      ctx.fillStyle = '#ffffff';
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();

      // Launch Prompt
      if (b.stuck && state === 'PLAYING') {
        ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
        ctx.font = 'bold 13px sans-serif';
        ctx.textAlign = 'center';
        ctx.fillText('CLICK OR SPACE TO LAUNCH', W / 2, H - 80);
      }
    }

    // Draw Particles
    for (const p of particles) {
      ctx.save();
      ctx.globalAlpha = p.alpha;
      ctx.translate(p.x, p.y);
      ctx.rotate(p.rot);
      ctx.shadowColor = p.color;
      ctx.shadowBlur = 8;
      ctx.fillStyle = p.color;
      ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
      ctx.restore();
    }

    // Draw Floating Texts
    for (const ft of floatingTexts) {
      ctx.save();
      ctx.globalAlpha = Math.max(0, ft.life / ft.maxLife);
      ctx.font = `900 ${ft.size}px sans-serif`;
      ctx.fillStyle = ft.color;
      ctx.shadowColor = ft.color;
      ctx.shadowBlur = 8;
      ctx.textAlign = 'center';
      ctx.fillText(ft.text, ft.x, ft.y);
      ctx.restore();
    }

    ctx.restore();
  }

  // --- MAIN LOOP ---
  let lastTime = performance.now();
  function loop(now) {
    const dt = Math.min(0.05, (now - lastTime) / 1000);
    lastTime = now;

    update(dt);
    render();
    requestAnimationFrame(loop);
  }

  requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Compare with

Gemini 4 Argon: other attempts