hypeorcode
Network offOpen alone (new tab)

Run details

Model
opencode/longcat-2.5-preview-free
Route
opencode CLI
Reasoning
provider default
Limits
60 min · 131,072 output tokens
Date
2 Oct 2026
Time
5 min 44 s
Size
24 kB
Tokens
31,090 in, 15,994 out
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

24 kB 828 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Breakout</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body {
    height: 100%;
    background: #05060f;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: 'Courier New', monospace;
    overflow: hidden;
  }
  #wrap {
    position: relative;
    box-shadow: 0 0 60px rgba(77, 150, 255, 0.25), 0 0 120px rgba(199, 125, 255, 0.12);
    border-radius: 8px;
    overflow: hidden;
  }
  canvas {
    display: block;
    background: #05060f;
    cursor: none;
  }
</style>
</head>
<body>
<div id="wrap">
  <canvas id="game" width="800" height="600"></canvas>
</div>
<script>
'use strict';

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

// ---------------- Audio ----------------
let audioCtx = null;
let muted = false;
function ensureAudio() {
  if (!audioCtx) {
    try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { audioCtx = null; }
  }
  if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
}
function beep(freq, dur, type, vol, slide) {
  if (!audioCtx || muted) return;
  type = type || 'square'; vol = vol || 0.15; slide = slide || 0;
  const t = audioCtx.currentTime;
  const o = audioCtx.createOscillator();
  const g = audioCtx.createGain();
  o.type = type;
  o.frequency.setValueAtTime(freq, t);
  if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(30, freq + slide), t + dur);
  g.gain.setValueAtTime(vol, t);
  g.gain.exponentialRampToValueAtTime(0.001, t + dur);
  o.connect(g); g.connect(audioCtx.destination);
  o.start(t); o.stop(t + dur);
}
function sfx(name, opt) {
  opt = opt || {};
  switch (name) {
    case 'paddle': beep(200, 0.08, 'square', 0.18, 120); break;
    case 'wall':   beep(150, 0.05, 'square', 0.08, 40); break;
    case 'brick':  beep(320 + (opt.combo || 0) * 45, 0.07, 'square', 0.14, 220); break;
    case 'silver': beep(520, 0.06, 'triangle', 0.14, 100); break;
    case 'launch': beep(440, 0.1, 'square', 0.12, 220); break;
    case 'lose':
      beep(320, 0.35, 'sawtooth', 0.18, -240);
      setTimeout(function(){ beep(180, 0.4, 'sawtooth', 0.15, -120); }, 120);
      break;
    case 'power':
      beep(523, 0.09, 'square', 0.14);
      setTimeout(function(){ beep(659, 0.09, 'square', 0.14); }, 80);
      setTimeout(function(){ beep(784, 0.14, 'square', 0.14); }, 160);
      break;
    case 'level':
      beep(392, 0.12, 'square', 0.15);
      setTimeout(function(){ beep(523, 0.12, 'square', 0.15); }, 110);
      setTimeout(function(){ beep(659, 0.12, 'square', 0.15); }, 220);
      setTimeout(function(){ beep(784, 0.25, 'square', 0.16); }, 330);
      break;
    case 'over':
      beep(392, 0.25, 'sawtooth', 0.16, -100);
      setTimeout(function(){ beep(311, 0.25, 'sawtooth', 0.16, -80); }, 240);
      setTimeout(function(){ beep(233, 0.5, 'sawtooth', 0.16, -100); }, 480);
      break;
  }
}

// ---------------- Constants ----------------
const STATE = { MENU: 0, PLAYING: 1, LEVELCLEAR: 2, GAMEOVER: 3, WIN: 4, PAUSED: 5 };
const PADDLE_W = 110, PADDLE_H = 14, PADDLE_Y = 556;
const BALL_R = 7;
const BRICK_W = 74, BRICK_H = 22, BRICK_GAP = 4, BRICK_TOP = 80, BRICK_LEFT = 30;
const COLS = 10;

const BRICK_TYPES = {
  'R': { color: '#ff4d6d', hp: 1, score: 50 },
  'O': { color: '#ff9f1c', hp: 1, score: 60 },
  'Y': { color: '#ffe74c', hp: 1, score: 70 },
  'G': { color: '#2ec4b6', hp: 1, score: 80 },
  'B': { color: '#4d96ff', hp: 1, score: 90 },
  'P': { color: '#c77dff', hp: 1, score: 100 },
  'S': { color: '#e8eef5', hp: 2, score: 150 }
};

const LEVELS = [
  [
    'RRRRRRRRRR',
    'OOOOOOOOOO',
    'YYYYYYYYYY',
    'GGGGGGGGGG',
    'BBBBBBBBBB'
  ],
  [
    'R.R.R.R.R.',
    '.O.O.O.O.O',
    'Y.Y.Y.Y.Y.',
    '.G.G.G.G.G',
    'B.B.B.B.B.',
    '.P.P.P.P.P'
  ],
  [
    '....YY....',
    '...YRRY...',
    '..YRRRRY..',
    '.YRRRRRRY.',
    'YRRRRRRRRY',
    '.YRRRRRRY.',
    '..YRRRRY..',
    '...YRRY...',
    '....YY....'
  ],
  [
    'SSSSSSSSSS',
    'S........S',
    'S.RRRRRR.S',
    'S.R....R.S',
    'S.R.YY.R.S',
    'S.R.YY.R.S',
    'S.R....R.S',
    'S.RRRRRR.S',
    '..SS..SS..'
  ],
  [
    'P..P..P..P',
    'PP..PP..PP',
    '..PP..PP..',
    'B..B..B..B',
    'BB..BB..BB',
    '..BB..BB..',
    'G..G..G..G',
    'GG..GG..GG'
  ],
  [
    'SSSSSSSSSS',
    'SYRYRYRYRS',
    'SSSSSSSSSS',
    '..........',
    'PPP..PPPPP',
    'P........P',
    'P.BBBBBB.P',
    'P.B.SS.B.P',
    'P.B.SS.B.P',
    'P.BBBBBB.P',
    'P........P',
    'PPPPPPPPPP'
  ]
];

const POWER_TYPES = {
  'E': { color: '#2ec4b6', label: 'E', name: 'EXPAND' },
  'M': { color: '#c77dff', label: 'M', name: 'MULTI' },
  'S': { color: '#ffe74c', label: 'S', name: 'SLOW' },
  'L': { color: '#ff4d6d', label: '+', name: '1UP' }
};

// ---------------- State ----------------
let state = STATE.MENU;
let score = 0, lives = 3, level = 0, combo = 0, bestCombo = 0;
let best = 0;
try { best = parseInt(localStorage.getItem('neonBreakoutBest') || '0', 10) || 0; } catch (e) {}
let shake = 0, flash = 0, hitstop = 0, levelClearTimer = 0, time = 0;
let slowTimer = 0, expandTimer = 0;
let paddle, balls, bricks, particles, powerups, popups, stars;
const keys = { left: false, right: false };
let mouseX = W / 2;

function newBall(stuck) {
  return {
    x: paddle.x, y: PADDLE_Y - BALL_R - 1,
    vx: 0, vy: 0, r: BALL_R,
    speed: Math.min(340 + level * 25, 520),
    stuck: stuck, trail: []
  };
}

function buildBricks(map) {
  const arr = [];
  for (let r = 0; r < map.length; r++) {
    for (let c = 0; c < COLS; c++) {
      const ch = map[r][c];
      if (ch === '.' || ch === ' ') continue;
      const t = BRICK_TYPES[ch];
      if (!t) continue;
      arr.push({
        x: BRICK_LEFT + c * (BRICK_W + BRICK_GAP),
        y: BRICK_TOP + r * (BRICK_H + BRICK_GAP),
        w: BRICK_W, h: BRICK_H,
        hp: t.hp, maxHp: t.hp,
        color: t.color, score: t.score,
        alive: true, wobble: 0
      });
    }
  }
  return arr;
}

function resetGame() {
  score = 0; lives = 3; level = 0; combo = 0; bestCombo = 0;
  slowTimer = 0; expandTimer = 0;
  startLevel(0);
}

function startLevel(i) {
  level = i;
  paddle = { x: W / 2, w: PADDLE_W, h: PADDLE_H, squash: 0 };
  bricks = buildBricks(LEVELS[i]);
  balls = [newBall(true)];
  powerups = []; particles = []; popups = [];
  state = STATE.PLAYING;
}

function initStars() {
  stars = [];
  for (let i = 0; i < 70; i++) {
    stars.push({ x: Math.random() * W, y: Math.random() * H, r: Math.random() * 1.6 + 0.4, p: Math.random() * Math.PI * 2 });
  }
}
initStars();

// ---------------- Effects ----------------
function spawnParticles(x, y, color, n) {
  for (let i = 0; i < n; i++) {
    const a = Math.random() * Math.PI * 2;
    const sp = 60 + Math.random() * 200;
    particles.push({
      x: x, y: y,
      vx: Math.cos(a) * sp, vy: Math.sin(a) * sp - 60,
      life: 0.5 + Math.random() * 0.5, maxLife: 1,
      size: 2 + Math.random() * 3, color: color
    });
  }
}
function spawnPopup(x, y, txt, color) {
  popups.push({ x: x, y: y, txt: txt, life: 0.9, color: color });
}
function maybeDropPowerup(x, y) {
  if (Math.random() > 0.16) return;
  const roll = Math.random();
  let type = 'E';
  if (roll < 0.30) type = 'E';
  else if (roll < 0.55) type = 'M';
  else if (roll < 0.80) type = 'S';
  else type = (lives < 5) ? 'L' : 'E';
  powerups.push({ x: x, y: y, vy: 130, type: type, life: 8 });
}

// ---------------- Update ----------------
function update(dt) {
  time += dt;
  if (shake > 0) shake = Math.max(0, shake - dt * 30);
  if (flash > 0) flash = Math.max(0, flash - dt * 2.5);

  if (state === STATE.LEVELCLEAR) {
    levelClearTimer -= dt;
    updateParticles(dt);
    if (levelClearTimer <= 0) {
      if (level + 1 >= LEVELS.length) {
        state = STATE.WIN;
        if (score > best) { best = score; try { localStorage.setItem('neonBreakoutBest', String(best)); } catch (e) {} }
      } else {
        startLevel(level + 1);
      }
    }
    return;
  }
  if (state !== STATE.PLAYING) { updateParticles(dt); return; }

  if (hitstop > 0) { hitstop -= dt; return; }

  if (slowTimer > 0) slowTimer -= dt;
  if (expandTimer > 0) expandTimer -= dt;
  paddle.w = PADDLE_W * (expandTimer > 0 ? 1.5 : 1);
  paddle.squash = Math.max(0, paddle.squash - dt * 6);

  // Paddle movement
  const kb = 560;
  if (keys.left) mouseX -= kb * dt;
  if (keys.right) mouseX += kb * dt;
  paddle.x += (mouseX - paddle.x) * Math.min(1, dt * 18);
  paddle.x = Math.max(paddle.w / 2 + 4, Math.min(W - paddle.w / 2 - 4, paddle.x));

  // Balls
  for (let i = balls.length - 1; i >= 0; i--) {
    const b = balls[i];
    if (b.stuck) {
      b.x = paddle.x;
      b.y = PADDLE_Y - b.r - 1;
      continue;
    }
    b.x += b.vx * dt;
    b.y += b.vy * dt;

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

    // Walls
    if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx); sfx('wall'); }
    if (b.x + b.r > W) { b.x = W - b.r; b.vx = -Math.abs(b.vx); sfx('wall'); }
    if (b.y - b.r < 0) { b.y = b.r; b.vy = Math.abs(b.vy); sfx('wall'); }

    // Paddle
    if (b.vy > 0 &&
        b.y + b.r >= PADDLE_Y && b.y - b.r <= PADDLE_Y + PADDLE_H &&
        b.x >= paddle.x - paddle.w / 2 - b.r && b.x <= paddle.x + paddle.w / 2 + b.r) {
      const rel = Math.max(-1, Math.min(1, (b.x - paddle.x) / (paddle.w / 2)));
      const ang = -Math.PI / 2 + rel * (Math.PI / 3);
      b.vx = b.speed * Math.cos(ang);
      b.vy = b.speed * Math.sin(ang);
      b.y = PADDLE_Y - b.r - 0.5;
      combo = 0;
      paddle.squash = 1;
      spawnParticles(b.x, PADDLE_Y, '#4d96ff', 5);
      sfx('paddle');
    }

    // Bricks
    for (let j = 0; j < bricks.length; j++) {
      const k = bricks[j];
      if (!k.alive) continue;
      const nx = Math.max(k.x, Math.min(b.x, k.x + k.w));
      const ny = Math.max(k.y, Math.min(b.y, k.y + k.h));
      const dx = b.x - nx, dy = b.y - ny;
      if (dx * dx + dy * dy > b.r * b.r) continue;

      // Resolve bounce by smallest penetration
      const overlapL = (b.x + b.r) - k.x;
      const overlapR = (k.x + k.w) - (b.x - b.r);
      const overlapT = (b.y + b.r) - k.y;
      const overlapB = (k.y + k.h) - (b.y - b.r);
      const minX = Math.min(overlapL, overlapR);
      const minY = Math.min(overlapT, overlapB);
      if (minX < minY) {
        b.vx = (overlapL < overlapR) ? -Math.abs(b.vx) : Math.abs(b.vx);
      } else {
        b.vy = (overlapT < overlapB) ? -Math.abs(b.vy) : Math.abs(b.vy);
      }

      k.hp--;
      k.wobble = 1;
      if (k.hp <= 0) {
        k.alive = false;
        combo++;
        bestCombo = Math.max(bestCombo, combo);
        const mult = Math.min(1 + (combo - 1) * 0.25, 5);
        const pts = Math.round(k.score * mult);
        score += pts;
        spawnParticles(k.x + k.w / 2, k.y + k.h / 2, k.color, 16);
        spawnPopup(k.x + k.w / 2, k.y, '+' + pts, k.color);
        shake = Math.min(shake + 3.5, 9);
        hitstop = 0.03;
        maybeDropPowerup(k.x + k.w / 2, k.y + k.h / 2);
        sfx('brick', { combo: combo });
        b.speed = Math.min(b.speed + 2, 560);
        const sp = Math.hypot(b.vx, b.vy) || 1;
        b.vx = b.vx / sp * b.speed; b.vy = b.vy / sp * b.speed;
      } else {
        spawnParticles(nx, ny, '#e8eef5', 6);
        sfx('silver');
      }
      break;
    }

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

  if (balls.length === 0) {
    lives--;
    combo = 0;
    shake = 14;
    flash = 1;
    sfx('lose');
    if (lives <= 0) {
      state = STATE.GAMEOVER;
      if (score > best) { best = score; try { localStorage.setItem('neonBreakoutBest', String(best)); } catch (e) {} }
      sfx('over');
    } else {
      balls = [newBall(true)];
    }
  }

  // Level clear?
  let remaining = 0;
  for (let i = 0; i < bricks.length; i++) if (bricks[i].alive) remaining++;
  if (remaining === 0 && balls.length > 0) {
    score += 500;
    state = STATE.LEVELCLEAR;
    levelClearTimer = 1.8;
    sfx('level');
  }

  // Powerups
  for (let i = powerups.length - 1; i >= 0; i--) {
    const p = powerups[i];
    p.y += p.vy * dt;
    p.life -= dt;
    if (p.y > PADDLE_Y - 6 && p.y < PADDLE_Y + PADDLE_H + 6 &&
        p.x > paddle.x - paddle.w / 2 - 12 && p.x < paddle.x + paddle.w / 2 + 12) {
      applyPowerup(p.type, p.x, p.y);
      powerups.splice(i, 1);
    } else if (p.y > H + 20 || p.life <= 0) {
      powerups.splice(i, 1);
    }
  }

  updateParticles(dt);
}

function applyPowerup(type, x, y) {
  const t = POWER_TYPES[type];
  spawnPopup(x, y - 10, t.name, t.color);
  spawnParticles(x, y, t.color, 14);
  sfx('power');
  if (type === 'E') {
    expandTimer = 12;
  } else if (type === 'S') {
    slowTimer = 8;
    for (const b of balls) { b.vx *= 0.7; b.vy *= 0.7; }
  } else if (type === 'M') {
    const additions = [];
    const src = balls.filter(function(b) { return !b.stuck; });
    for (const b of src) {
      if (balls.length + additions.length >= 6) break;
      const base = Math.atan2(b.vy, b.vx);
      const offs = src.length === 1 ? [-0.5, 0.5] : [0.45];
      for (const da of offs) {
        if (balls.length + additions.length >= 6) break;
        additions.push({
          x: b.x, y: b.y, r: b.r, speed: b.speed, stuck: false, trail: [],
          vx: b.speed * Math.cos(base + da), vy: b.speed * Math.sin(base + da)
        });
      }
    }
    balls = balls.concat(additions);
  } else if (type === 'L') {
    lives = Math.min(lives + 1, 5);
  }
}

function updateParticles(dt) {
  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 += 320 * dt;
    p.life -= dt;
    if (p.life <= 0) particles.splice(i, 1);
  }
  for (let i = popups.length - 1; i >= 0; i--) {
    const p = popups[i];
    p.y -= 45 * dt;
    p.life -= dt;
    if (p.life <= 0) popups.splice(i, 1);
  }
  for (const k of bricks || []) {
    if (k.wobble > 0) k.wobble = Math.max(0, k.wobble - dt * 5);
  }
}

// ---------------- Render ----------------
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 drawBackground() {
  const g = ctx.createLinearGradient(0, 0, 0, H);
  g.addColorStop(0, '#0a0d1f');
  g.addColorStop(1, '#05060f');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, W, H);
  for (const s of stars) {
    const a = 0.25 + 0.35 * (0.5 + 0.5 * Math.sin(time * 1.5 + s.p));
    ctx.globalAlpha = a;
    ctx.fillStyle = '#9db4ff';
    ctx.beginPath();
    ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.globalAlpha = 1;
}

function drawBricks() {
  for (const k of bricks) {
    if (!k.alive) continue;
    const wob = k.wobble > 0 ? Math.sin(k.wobble * 20) * k.wobble * 2 : 0;
    ctx.save();
    ctx.translate(k.x + k.w / 2 + wob, k.y + k.h / 2);
    ctx.shadowColor = k.color;
    ctx.shadowBlur = 10;
    ctx.fillStyle = k.hp < k.maxHp ? '#8d99ae' : k.color;
    roundRect(-k.w / 2, -k.h / 2, k.w, k.h, 5);
    ctx.fill();
    ctx.shadowBlur = 0;
    ctx.globalAlpha = 0.35;
    ctx.fillStyle = '#ffffff';
    roundRect(-k.w / 2 + 3, -k.h / 2 + 2, k.w - 6, 6, 3);
    ctx.fill();
    ctx.globalAlpha = 1;
    ctx.restore();
  }
}

function drawPaddle() {
  const sq = 1 - paddle.squash * 0.3;
  ctx.save();
  ctx.translate(paddle.x, PADDLE_Y + PADDLE_H / 2);
  ctx.scale(1 + paddle.squash * 0.15, sq);
  ctx.shadowColor = '#4d96ff';
  ctx.shadowBlur = 18;
  const g = ctx.createLinearGradient(0, -PADDLE_H / 2, 0, PADDLE_H / 2);
  g.addColorStop(0, '#7db4ff');
  g.addColorStop(1, '#2b6cb0');
  ctx.fillStyle = g;
  roundRect(-paddle.w / 2, -PADDLE_H / 2, paddle.w, PADDLE_H, 7);
  ctx.fill();
  ctx.restore();
}

function drawBalls() {
  for (const b of balls) {
    for (let i = 0; i < b.trail.length; i++) {
      const t = b.trail[i];
      const a = (i / b.trail.length) * 0.3;
      ctx.globalAlpha = a;
      ctx.fillStyle = '#7df9ff';
      ctx.beginPath();
      ctx.arc(t.x, t.y, b.r * (i / b.trail.length), 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
    ctx.shadowColor = '#7df9ff';
    ctx.shadowBlur = 16;
    ctx.fillStyle = '#eafcff';
    ctx.beginPath();
    ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
    ctx.fill();
    ctx.shadowBlur = 0;
  }
}

function drawPowerups() {
  for (const p of powerups) {
    const t = POWER_TYPES[p.type];
    const pulse = 1 + 0.12 * Math.sin(time * 8);
    ctx.save();
    ctx.translate(p.x, p.y);
    ctx.scale(pulse, pulse);
    ctx.shadowColor = t.color;
    ctx.shadowBlur = 12;
    ctx.fillStyle = t.color;
    roundRect(-13, -8, 26, 16, 8);
    ctx.fill();
    ctx.shadowBlur = 0;
    ctx.fillStyle = '#05060f';
    ctx.font = 'bold 11px Courier New';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(t.label, 0, 1);
    ctx.restore();
  }
}

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

function drawPopups() {
  ctx.font = 'bold 14px Courier New';
  ctx.textAlign = 'center';
  for (const p of popups) {
    ctx.globalAlpha = Math.min(1, p.life * 2);
    ctx.fillStyle = p.color;
    ctx.fillText(p.txt, p.x, p.y);
  }
  ctx.globalAlpha = 1;
}

function drawHUD() {
  ctx.font = 'bold 16px Courier New';
  ctx.textAlign = 'left';
  ctx.fillStyle = '#eaf2ff';
  ctx.fillText('SCORE ' + String(score).padStart(6, '0'), 16, 28);
  ctx.textAlign = 'center';
  ctx.fillStyle = '#9db4ff';
  ctx.fillText('LEVEL ' + (level + 1) + ' / ' + LEVELS.length, W / 2, 28);
  ctx.textAlign = 'right';
  ctx.fillText('BEST ' + String(best).padStart(6, '0'), W - 16, 28);
  for (let i = 0; i < lives; i++) {
    ctx.fillStyle = '#ff4d6d';
    ctx.shadowColor = '#ff4d6d';
    ctx.shadowBlur = 8;
    ctx.beginPath();
    ctx.arc(W - 20 - i * 20, 44, 6, 0, Math.PI * 2);
    ctx.fill();
    ctx.shadowBlur = 0;
  }
  if (combo >= 2 && state === STATE.PLAYING) {
    const pulse = 1 + 0.15 * Math.sin(time * 10);
    ctx.save();
    ctx.translate(W / 2, 52);
    ctx.scale(pulse, pulse);
    ctx.font = 'bold 18px Courier New';
    ctx.textAlign = 'center';
    ctx.fillStyle = '#ffe74c';
    ctx.shadowColor = '#ffe74c';
    ctx.shadowBlur = 10;
    ctx.fillText('COMBO x' + combo, 0, 0);
    ctx.restore();
  }
  if (slowTimer > 0) {
    ctx.font = '12px Courier New';
    ctx.textAlign = 'left';
    ctx.fillStyle = '#ffe74c';
    ctx.fillText('SLOW ' + slowTimer.toFixed(1), 16, 48);
  }
  if (expandTimer > 0) {
    ctx.font = '12px Courier New';
    ctx.textAlign = 'left';
    ctx.fillStyle = '#2ec4b6';
    ctx.fillText('WIDE ' + expandTimer.toFixed(1), 16, 64);
  }
}

function drawCenterText(lines) {
  ctx.textAlign = 'center';
  let y = H / 2 - (lines.length - 1) * 26;
  for (const ln of lines) {
    ctx.font = ln.font;
    if (ln.glow) { ctx.shadowColor = ln.color; ctx.shadowBlur = ln.glow; }
    ctx.fillStyle = ln.color;
    ctx.fillText(ln.text, W / 2, y);
    ctx.shadowBlur = 0;
    y += ln.gap || 44;
  }
}

function render() {
  drawBackground();
  ctx.save();
  if (shake > 0) {
    ctx.translate((Math.random() - 0.5) * shake * 2, (Math.random() - 0.5) * shake * 2);
  }
  if (state !== STATE.MENU) {
    drawBricks();
    drawPowerups();
    drawPaddle();
    drawBalls();
  }
  drawParticles();
  drawPopups();
  ctx.restore();

  if (flash > 0) {
    ctx.globalAlpha = flash * 0.25;
    ctx.fillStyle = '#ff4d6d';
    ctx.fillRect(0, 0, W, H);
    ctx.globalAlpha = 1;
  }

  if (state !== STATE.MENU) drawHUD();

  if (state === STATE.MENU) {
    drawCenterText([
      { text: 'NEON BREAKOUT', font: 'bold 52px Courier New', color: '#4d96ff', glow: 24, gap: 60 },
      { text: 'Mouse or Arrow Keys to move', font: '16px Courier New', color: '#9db4ff', gap: 30 },
      { text: 'Click or Space to launch', font: '16px Courier New', color: '#9db4ff', gap: 30 },
      { text: 'P pause   M mute', font: '14px Courier New', color: '#5a6b8c', gap: 44 }
    ]);
    if (Math.sin(time * 4) > -0.2) {
      ctx.font = 'bold 20px Courier New';
      ctx.textAlign = 'center';
      ctx.fillStyle = '#ffe74c';
      ctx.shadowColor = '#ffe74c';
      ctx.shadowBlur = 12;
      ctx.fillText('CLICK OR PRESS SPACE TO START', W / 2, H / 2 + 118);
      ctx.shadowBlur = 0;
    }
    if (best > 0) {
      ctx.font = '14px Courier New';
      ctx.textAlign = 'center';
      ctx.fillStyle = '#5a6b8c';
      ctx.fillText('BEST ' + best, W / 2, H - 30);
    }
  } else if (state === STATE.LEVELCLEAR) {
    drawCenterText([
      { text: 'LEVEL CLEAR!', font: 'bold 44px Courier New', color: '#2ec4b6', glow: 20, gap: 56 },
      { text: 'BONUS +500', font: 'bold 20px Courier New', color: '#ffe74c', glow: 10, gap: 40 },
      { text: 'SCORE ' + score, font: '18px Courier New', color: '#eaf2ff', gap: 0 }
    ]);
  } else if (state === STATE.GAMEOVER) {
    drawCenterText([
      { text: 'GAME OVER', font: 'bold 52px Courier New', color: '#ff4d6d', glow: 22, gap: 60 },
      { text: 'SCORE ' + score, font: 'bold 22px Courier New', color: '#eaf2ff', gap: 36 },
      { text: 'BEST COMBO x' + bestCombo + '   BEST ' + best, font: '15px Courier New', color: '#9db4ff', gap: 48 },
      { text: 'CLICK OR PRESS SPACE TO RETRY', font: 'bold 17px Courier New', color: '#ffe74c', glow: 10, gap: 0 }
    ]);
  } else if (state === STATE.WIN) {
    drawCenterText([
      { text: 'YOU WIN!', font: 'bold 56px Courier New', color: '#ffe74c', glow: 26, gap: 62 },
      { text: 'ALL ' + LEVELS.length + ' LEVELS CLEARED', font: '18px Courier New', color: '#2ec4b6', gap: 38 },
      { text: 'FINAL SCORE ' + score, font: 'bold 22px Courier New', color: '#eaf2ff', gap: 38 },
      { text: 'BEST COMBO x' + bestCombo, font: '15px Courier New', color: '#9db4ff', gap: 48 },
      { text: 'CLICK OR PRESS SPACE TO PLAY AGAIN', font: 'bold 17px Courier New', color: '#c77dff', glow: 10, gap: 0 }
    ]);
  } else if (state === STATE.PAUSED) {
    drawCenterText([
      { text: 'PAUSED', font: 'bold 44px Courier New', color: '#9db4ff', glow: 16, gap: 52 },
      { text: 'PRESS P TO RESUME', font: '16px Courier New', color: '#eaf2ff', gap: 0 }
    ]);
  }
}

// ---------------- Input ----------------
function canvasMouseX(clientX) {
  const rect = canvas.getBoundingClientRect();
  return (clientX - rect.left) * (W / rect.width);
}
canvas.addEventListener('mousemove', function(e) {
  mouseX = canvasMouseX(e.clientX);
});
canvas.addEventListener('mousedown', function(e) {
  e.preventDefault();
  ensureAudio();
  handleAction();
});
canvas.addEventListener('touchstart', function(e) {
  e.preventDefault();
  ensureAudio();
  if (e.touches.length > 0) mouseX = canvasMouseX(e.touches[0].clientX);
  handleAction();
}, { passive: false });
canvas.addEventListener('touchmove', function(e) {
  e.preventDefault();
  if (e.touches.length > 0) mouseX = canvasMouseX(e.touches[0].clientX);
}, { passive: false });

function handleAction() {
  if (state === STATE.MENU) { resetGame(); return; }
  if (state === STATE.GAMEOVER || state === STATE.WIN) { state = STATE.MENU; return; }
  if (state === STATE.LEVELCLEAR) { levelClearTimer = 0; return; }
  if (state !== STATE.PLAYING) return;
  let launched = false;
  for (const b of balls) {
    if (b.stuck) {
      b.stuck = false;
      const ang = -Math.PI / 2 + (Math.random() * 0.3 - 0.15);
      b.vx = b.speed * Math.cos(ang);
      b.vy = b.speed * Math.sin(ang);
      launched = true;
    }
  }
  if (launched) sfx('launch');
}

window.addEventListener('keydown', function(e) {
  if (e.code === 'ArrowLeft' || e.code === 'KeyA') { keys.left = true; e.preventDefault(); }
  if (e.code === 'ArrowRight' || e.code === 'KeyD') { keys.right = true; e.preventDefault(); }
  if (e.code === 'Space') { ensureAudio(); handleAction(); e.preventDefault(); }
  if (e.code === 'KeyP') {
    if (state === STATE.PLAYING) state = STATE.PAUSED;
    else if (state === STATE.PAUSED) state = STATE.PLAYING;
  }
  if (e.code === 'KeyM') muted = !muted;
});
window.addEventListener('keyup', function(e) {
  if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
  if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
});

// ---------------- Main loop ----------------
let last = performance.now();
function loop(now) {
  let dt = (now - last) / 1000;
  last = now;
  if (dt > 0.033) dt = 0.033;
  update(dt);
  render();
  requestAnimationFrame(loop);
}
paddle = { x: W / 2, w: PADDLE_W, h: PADDLE_H, squash: 0 };
balls = [newBall(true)];
bricks = buildBricks(LEVELS[0]);
particles = []; powerups = []; popups = [];
requestAnimationFrame(loop);
</script>
</body>
</html>

LongCat 2.5 Preview: other attempts

  • LongCat 2.5 Preview, Pelican: Not run: no OpenRouter route
  • About · 144 words