• Updated 11 Nov, 12:43 UTC
  • Next run 12 Nov, 05:17 UTCNext 12 Nov 05:17
  • Last 7 days: 10 new models · 61 runs7 d: 10 models · 61 runs
Playable Breakout

Muse EmberMeta

Network offOpen alone (new tab)

Run details

Model
meta/muse-ember
Route
OpenRouter API, one user message
Served by
Meta
Effort
max, OpenRouter's default for this model
Limits
60 min · 943,718 output tokens (the model's limit)
Date
9 Nov 2026
Time
61 s
Size
40 kB
Tokens
85 in, 39,247 out (25,314 of them thinking)
List price
$0.020 at today's price ($0.10 in / $0.50 out per M)
Paid
$0.020

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

40 kB 956 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="data:,">
<title>Neon Breakout</title>
<style>
  *{box-sizing:border-box}
  html,body{margin:0;padding:0}
  body{
    font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    background:
      radial-gradient(1200px 700px at 20% -10%, rgba(124,58,237,.35), transparent 60%),
      radial-gradient(1000px 600px at 90% 0%, rgba(34,211,238,.22), transparent 55%),
      #050816;
    color:#e2e8f0;
    min-height:100vh;
  }
  .wrap{max-width:920px;margin:0 auto;padding:20px 16px 34px}
  header{display:flex;justify-content:space-between;align-items:flex-end;gap:16px;margin-bottom:14px;flex-wrap:wrap}
  h1{
    margin:0;font-size:clamp(28px,5vw,44px);letter-spacing:.06em;font-weight:900;
    background:linear-gradient(90deg,#67e8f9,#f472b6,#a78bfa);
    -webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
  }
  .tag{margin:4px 0 0;color:#94a3b8}
  .hi{
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);
    padding:10px 14px;border-radius:12px;color:#94a3b8;font-size:12px;letter-spacing:.12em;
  }
  .hi span{display:block;color:#fde68a;font-size:20px;font-weight:800;letter-spacing:0;margin-top:2px}
  .hud{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:10px}
  .hud-card{
    background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.09);
    border-radius:14px;padding:10px 14px;display:flex;flex-direction:column;gap:2px;backdrop-filter:blur(6px);
  }
  .hud-card.hot{border-color:rgba(250,204,21,.55);box-shadow:0 0 18px rgba(250,204,21,.18)}
  .hud-card.hot strong{color:#fde68a}
  .label{font-size:11px;letter-spacing:.18em;color:#94a3b8;font-weight:700}
  .hud-card strong{font-size:22px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.1}
  .hud-card small{color:#64748b;font-size:12px}
  .hearts{color:#fb7185;letter-spacing:2px}
  .powers{display:flex;gap:8px;align-items:center;min-height:30px;margin-bottom:10px;flex-wrap:wrap}
  .chip{font-size:12px;font-weight:800;letter-spacing:.08em;padding:6px 10px;border-radius:999px;border:1px solid;display:inline-flex;gap:6px;align-items:center}
  .chip.timer{display:none}
  .chip-expand{color:#67e8f9;border-color:rgba(34,211,238,.5);background:rgba(34,211,238,.12)}
  .chip-fire{color:#fdba74;border-color:rgba(251,146,60,.5);background:rgba(251,146,60,.12)}
  .chip-slow{color:#93c5fd;border-color:rgba(96,165,250,.5);background:rgba(96,165,250,.12)}
  .chip.info{color:#94a3b8;border-color:rgba(148,163,184,.25);background:rgba(148,163,184,.08);font-weight:600;letter-spacing:0}
  .game-shell{
    position:relative;border-radius:16px;overflow:hidden;
    border:2px solid rgba(34,211,238,.4);
    box-shadow:0 0 42px rgba(34,211,238,.22), 0 24px 60px rgba(0,0,0,.5);
    background:#050816;
  }
  canvas{display:block;width:100%;height:auto;touch-action:none;user-select:none;-webkit-user-select:none;cursor:crosshair}
  .scanlines{position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(255,255,255,.025) 0 1px,transparent 1px 3px)}
  .overlay{
    position:absolute;inset:0;display:flex;align-items:center;justify-content:center;text-align:center;
    background:rgba(5,8,22,.74);backdrop-filter:blur(6px);padding:24px;z-index:5;
    transition:opacity .25s ease;
  }
  .overlay.hidden{opacity:0;pointer-events:none}
  .panel{max-width:560px;animation:pop .35s cubic-bezier(.2,1.6,.4,1)}
  @keyframes pop{from{transform:scale(.92) translateY(10px);opacity:0}to{transform:scale(1) translateY(0);opacity:1}}
  .badge{display:inline-block;padding:6px 12px;border-radius:999px;background:rgba(244,114,182,.15);color:#f9a8d4;border:1px solid rgba(244,114,182,.4);font-size:12px;font-weight:800;letter-spacing:.18em}
  .overlay h2{
    font-size:clamp(30px,6vw,52px);margin:14px 0 10px;letter-spacing:.04em;
    background:linear-gradient(90deg,#67e8f9,#f472b6,#a78bfa);
    -webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
  }
  .overlay p{color:#cbd5e1;line-height:1.55;margin:0 0 18px}
  .btn{
    font:inherit;font-weight:900;letter-spacing:.08em;cursor:pointer;border:none;color:#fff;
    padding:14px 28px;border-radius:999px;
    background:linear-gradient(90deg,#ec4899,#06b6d4);
    box-shadow:0 8px 26px rgba(236,72,153,.35),0 0 0 1px rgba(255,255,255,.15) inset;
    transition:transform .15s ease, box-shadow .15s ease;
  }
  .btn:hover{transform:translateY(-2px) scale(1.02);box-shadow:0 12px 32px rgba(6,182,212,.45)}
  .btn:active{transform:translateY(0)}
  .btn:focus-visible,.ctl:focus-visible{outline:3px solid #fde68a;outline-offset:2px}
  .tiny{font-size:12px;color:#64748b;margin-top:14px}
  .controls{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-top:14px}
  .ctl{
    font:inherit;font-weight:700;cursor:pointer;color:#e2e8f0;
    background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.14);
    padding:9px 14px;border-radius:999px;transition:border-color .15s,background .15s,transform .15s;
  }
  .ctl:hover{border-color:rgba(34,211,238,.7);background:rgba(34,211,238,.1);transform:translateY(-1px)}
  .spacer{flex:1}
  .keys{color:#64748b;font-size:13px}
  kbd{background:#1e293b;border:1px solid #334155;border-bottom-width:3px;border-radius:6px;padding:2px 6px;font-family:inherit;font-size:11px;color:#e2e8f0}
  .legend{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:16px}
  .legend article{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:12px 14px}
  .legend h3{margin:0 0 6px;font-size:14px;letter-spacing:.06em}
  .legend p{margin:0;color:#94a3b8;font-size:13px;line-height:1.55}
  .dot{display:inline-block;width:11px;height:11px;border-radius:3px;background:var(--c);margin-right:6px;box-shadow:0 0 10px var(--c);vertical-align:-1px}
  @media (max-width:700px){
    .hud{grid-template-columns:repeat(2,1fr)}
    .legend{grid-template-columns:1fr}
    .chip.info{display:none}
    .keys{flex-basis:100%}
  }
</style>
</head>
<body>
<div class="wrap">
  <header>
    <div>
      <h1>NEON BREAKOUT</h1>
      <p class="tag">Smash bricks. Chain combos. Don't drop the ball.</p>
    </div>
    <div class="hi">HI-SCORE<span id="hiScore">000000</span></div>
  </header>

  <section class="hud" aria-label="Score">
    <div class="hud-card"><span class="label">SCORE</span><strong id="score">000000</strong><small>points</small></div>
    <div class="hud-card"><span class="label">LEVEL</span><strong id="level">1 / 7</strong><small id="levelName">First Contact</small></div>
    <div class="hud-card"><span class="label">LIVES</span><strong id="lives" class="hearts">♥♥♥</strong><small>don't let it fall</small></div>
    <div class="hud-card"><span class="label">COMBO</span><strong id="multiplier">×1</strong><small id="comboText">hit bricks consecutively</small></div>
  </section>

  <div class="powers" aria-live="polite">
    <span class="chip timer chip-expand" id="chipExpand">EXPAND <b>0s</b></span>
    <span class="chip timer chip-fire" id="chipFire">FIREBALL <b>0s</b></span>
    <span class="chip timer chip-slow" id="chipSlow">SLOW-MO <b>0s</b></span>
    <span class="chip info">Catch falling capsules for power-ups!</span>
  </div>

  <main class="game-shell" id="shell">
    <canvas id="game" width="800" height="600" aria-label="Breakout game canvas"></canvas>
    <div class="scanlines"></div>
    <div class="overlay" id="overlay">
      <div class="panel">
        <div class="badge" id="overlayBadge">★ ARCADE CLASSIC</div>
        <h2 id="overlayTitle">NEON BREAKOUT</h2>
        <p id="overlayText">Break every brick, chain combos, catch power-ups and clear all 7 levels.</p>
        <button id="primaryBtn" class="btn">▶ START GAME</button>
        <p class="tiny" id="overlayHint">Mouse or ← → / A D to move • SPACE to launch • P pause • M mute</p>
      </div>
    </div>
  </main>

  <nav class="controls">
    <button id="pauseBtn" class="ctl">⏸ Pause</button>
    <button id="soundBtn" class="ctl">🔊 Sound on</button>
    <button id="restartBtn" class="ctl">⟳ Restart game</button>
    <span class="spacer"></span>
    <span class="keys"><kbd>←</kbd><kbd>→</kbd> or mouse / drag &nbsp; <kbd>SPACE</kbd> launch &nbsp; <kbd>P</kbd> pause &nbsp; <kbd>M</kbd> mute</span>
  </nav>

  <section class="legend">
    <article>
      <h3>Bricks</h3>
      <p><span class="dot" style="--c:#ff4d6d"></span>1 hit &nbsp;<span class="dot" style="--c:#22d3ee"></span>2 hits &nbsp;<span class="dot" style="--c:#a78bfa"></span>3 hits<br><span class="dot" style="--c:#fb923c"></span>Explosive — blows up neighbours &nbsp;<span class="dot" style="--c:#94a3b8"></span>Unbreakable</p>
    </article>
    <article>
      <h3>Power-ups</h3>
      <p><b style="color:#67e8f9">E</b> Expand paddle &nbsp;<b style="color:#f472b6">M</b> Multi-ball<br><b style="color:#93c5fd">S</b> Slow-mo &nbsp;<b style="color:#fdba74">F</b> Fireball &nbsp;<b style="color:#fb7185">+</b> Extra life</p>
    </article>
    <article>
      <h3>Juice</h3>
      <p>Particles, ball trails, screen shake, hit-flash, floating score popups and a combo multiplier — every 8 bricks in a row without touching the paddle raises it.</p>
    </article>
  </section>
</div>

<script>
(() => {
'use strict';
const W = 800, H = 600;
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = W * dpr;
canvas.height = H * dpr;

const $ = id => document.getElementById(id);
const scoreEl = $('score'), levelEl = $('level'), levelNameEl = $('levelName'),
      livesEl = $('lives'), multiEl = $('multiplier'), comboEl = $('comboText'), hiEl = $('hiScore');
const overlay = $('overlay'), ovBadge = $('overlayBadge'), ovTitle = $('overlayTitle'),
      ovText = $('overlayText'), ovHint = $('overlayHint'), primaryBtn = $('primaryBtn');
const chipExpand = $('chipExpand'), chipFire = $('chipFire'), chipSlow = $('chipSlow');
const pauseBtn = $('pauseBtn'), soundBtn = $('soundBtn'), restartBtn = $('restartBtn');

const LEVELS = [
  { name:'First Contact', rows:[
    '1111111111','1111111111','2222222222','2222222222','1111111111','1111111111']},
  { name:'Pyramid', rows:[
    '....11....','...1221...','..123321..','.12333321.','1233333321','1111111111']},
  { name:'Fortress', rows:[
    '1111111111','X22222222X','X23333332X','X22222222X','1111111111','X..1111..X']},
  { name:'Diamond', rows:[
    '....11....','...1221...','..123321..','.12333321.','..123321..','...1221...','....11....']},
  { name:'Columns', rows:[
    '1.1.1.1.1.','.2.2.2.2.2','3.3.3.3.3.','.2.2.2.2.2','1.1.1.1.1.','.3.3.3.3.3']},
  { name:'Blast Zone', rows:[
    '1111111111','1E222222E1','12E3333E21','1E222222E1','1111111111','.E.1111.E.']},
  { name:'Final Chaos', rows:[
    '3X3X3X3X33','1111111111','2E222222E2','2222222222','X1X1X1X1X1','1111111111']}
];

const BRICK_TYPES = {
  '1': { hp:1, color:'#ff4d6d', points:50 },
  '2': { hp:2, color:'#22d3ee', points:100 },
  '3': { hp:3, color:'#a78bfa', points:150 },
  'E': { hp:1, color:'#fb923c', points:120 },
  'X': { hp:Infinity, color:'#64748b', points:0 }
};

const POWER_INFO = {
  expand:{ label:'E', name:'EXPAND', color:'#22d3ee' },
  multi: { label:'M', name:'MULTI-BALL', color:'#f472b6' },
  slow:  { label:'S', name:'SLOW-MO', color:'#60a5fa' },
  fire:  { label:'F', name:'FIREBALL', color:'#fb923c' },
  life:  { label:'+', name:'EXTRA LIFE', color:'#f43f5e' }
};

let state = 'menu';
let score = 0, hiScore = 0, lives = 3, levelIndex = 0;
try { hiScore = parseInt(localStorage.getItem('neonBreakoutHi') || '0', 10) || 0; } catch(e) {}
let combo = 0, multiplier = 1;
let bricks = [], balls = [], particles = [], floaters = [], powerups = [], stars = [];
let gameTime = 0, animTime = 0;
let expandUntil = 0, fireUntil = 0, slowUntil = 0, wasSlow = false;
let shake = 0, flash = 0, flashColor = '255,255,255';
let muted = false;
const paddle = { x:W/2, y:H-28, w:110, baseW:110, h:14, targetX:W/2, tilt:0, squash:0, vel:0 };
const keys = {};
let usingMouse = false;

function clamp(v, a, b){ return v < a ? a : (v > b ? b : v); }

/* ---------- Audio (WebAudio, no files) ---------- */
let audioCtx = null;
function ensureAudio(){
  if(!audioCtx){ try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {} }
  if(audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
}
function tone(freq, dur, type, vol, slideTo){
  if(muted || !audioCtx) return;
  dur = dur || 0.08; type = type || 'square'; vol = vol || 0.12;
  const t = audioCtx.currentTime;
  const o = audioCtx.createOscillator(), g = audioCtx.createGain();
  o.type = type; o.frequency.setValueAtTime(freq, t);
  if(slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), 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 noiseBurst(dur, vol){
  if(muted || !audioCtx) return;
  dur = dur || 0.25; vol = vol || 0.28;
  const n = Math.floor(audioCtx.sampleRate * dur);
  const buf = audioCtx.createBuffer(1, n, audioCtx.sampleRate);
  const d = buf.getChannelData(0);
  for(let i = 0; i < n; i++) d[i] = (Math.random()*2 - 1) * (1 - i/n);
  const src = audioCtx.createBufferSource(); src.buffer = buf;
  const f = audioCtx.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 900;
  const g = audioCtx.createGain(); g.gain.value = vol;
  src.connect(f); f.connect(g); g.connect(audioCtx.destination); src.start();
}
const sfx = {
  paddle(){ tone(200, .06, 'square', .12, 320); },
  wall(){ tone(160, .05, 'square', .08); },
  clank(){ tone(120, .05, 'square', .10, 90); },
  hit(){ tone(340, .05, 'square', .08, 260); },
  breakBrick(c){ const b = 420 + ((c||0) % 10) * 35; tone(b, .08, 'square', .13, b*1.8); },
  explosion(){ noiseBurst(.3, .28); tone(90, .3, 'sawtooth', .18, 40); },
  power(){ [523,659,784,1046].forEach((f,i)=>setTimeout(()=>tone(f,.09,'square',.12), i*55)); },
  launch(){ tone(300, .15, 'square', .12, 700); },
  loseBall(){ tone(400, .25, 'sawtooth', .14, 80); },
  combo(){ tone(880, .1, 'square', .12, 1320); },
  levelClear(){ [523,659,784,1046,1318].forEach((f,i)=>setTimeout(()=>tone(f,.12,'square',.13), i*70)); },
  gameOver(){ [392,330,262,196].forEach((f,i)=>setTimeout(()=>tone(f,.18,'sawtooth',.12), i*130)); }
};

/* ---------- HUD & overlay ---------- */
function saveHi(){ try { localStorage.setItem('neonBreakoutHi', String(hiScore)); } catch(e) {} }
function updateHUD(){
  scoreEl.textContent = String(score).padStart(6, '0');
  hiEl.textContent = String(hiScore).padStart(6, '0');
  levelEl.textContent = (levelIndex + 1) + ' / ' + LEVELS.length;
  levelNameEl.textContent = LEVELS[levelIndex].name;
  livesEl.textContent = '♥'.repeat(Math.max(0, lives)) || '—';
  multiEl.textContent = '×' + multiplier;
  comboEl.textContent = combo > 1 ? (combo + ' brick combo') : 'hit bricks consecutively';
  const card = multiEl.closest('.hud-card');
  if(card) card.classList.toggle('hot', multiplier > 1);
}
function showOverlay(kind, data){
  data = data || {};
  overlay.classList.remove('hidden');
  if(kind === 'menu'){
    ovBadge.textContent = '★ ARCADE CLASSIC';
    ovTitle.textContent = 'NEON BREAKOUT';
    ovText.textContent = 'Break every brick, chain combos, catch power-ups and clear all 7 levels. You have 3 lives — angle the ball off the paddle edge to aim where it goes!';
    ovHint.textContent = 'Mouse / touch drag or ← → / A D to move • SPACE to launch • P pause • M mute';
    primaryBtn.textContent = '▶ START GAME';
  } else if(kind === 'paused'){
    ovBadge.textContent = '⏸ PAUSED';
    ovTitle.textContent = 'PAUSED';
    ovText.textContent = 'The balls are frozen in mid-air. Take your time — your combo and power-ups are safe.';
    ovHint.textContent = 'P or ESC to resume • M to mute';
    primaryBtn.textContent = '▶ RESUME';
  } else if(kind === 'levelComplete'){
    ovBadge.textContent = '★ LEVEL CLEAR';
    ovTitle.textContent = 'LEVEL CLEAR!';
    ovText.textContent = 'Nice smashing! Bonus +' + data.bonus + ' points. Next up: ' + data.nextName + '. New patterns, faster ball.';
    ovHint.textContent = 'PRESS SPACE or click below for the next level';
    primaryBtn.textContent = '▶ NEXT LEVEL';
  } else if(kind === 'gameOver'){
    ovBadge.textContent = '✕ GAME OVER';
    ovTitle.textContent = 'OUT OF LIVES';
    ovText.textContent = 'You scored ' + score.toLocaleString() + ' points. Hi-score: ' + hiScore.toLocaleString() + '.';
    ovHint.textContent = 'PRESS SPACE to try again';
    primaryBtn.textContent = '↻ PLAY AGAIN';
  } else if(kind === 'victory'){
    ovBadge.textContent = '🏆 CHAMPION';
    ovTitle.textContent = 'YOU WIN!';
    ovText.textContent = 'All 7 levels cleared! Final score: ' + score.toLocaleString() + ' points. Hi-score: ' + hiScore.toLocaleString() + '. Neon legend status: achieved.';
    ovHint.textContent = 'PRESS SPACE to play again';
    primaryBtn.textContent = '↻ PLAY AGAIN';
  }
}
function hideOverlay(){ overlay.classList.add('hidden'); }

/* ---------- Game flow ---------- */
function currentBallSpeed(){ return 400 + levelIndex * 18; }
function resetBallToPaddle(){
  balls = [{ x:paddle.x, y:paddle.y - 9, vx:0, vy:0, r:7, trail:[], stuck:true }];
}
function loadLevel(idx){
  levelIndex = idx;
  const rows = LEVELS[idx].rows;
  const cols = Math.max.apply(null, rows.map(r => r.length));
  const gap = 4, top = 72, margin = 30;
  const bw = (W - margin*2 - gap*(cols-1)) / cols, bh = 22;
  bricks = [];
  rows.forEach((row, r) => {
    for(let c = 0; c < row.length; c++){
      const ch = row[c];
      if(ch === '.' || !BRICK_TYPES[ch]) continue;
      const cfg = BRICK_TYPES[ch];
      bricks.push({
        x: margin + c*(bw+gap), y: top + r*(bh+gap), w: bw, h: bh,
        hp: cfg.hp, maxHp: cfg.hp, type: ch, color: cfg.color, points: cfg.points,
        flash: 0, wobble: 0, dead: false
      });
    }
  });
  powerups = []; combo = 0; multiplier = 1;
  expandUntil = 0; fireUntil = 0; slowUntil = 0; wasSlow = false;
  paddle.w = paddle.baseW; paddle.x = W/2; paddle.targetX = W/2; paddle.tilt = 0; paddle.squash = 0;
  resetBallToPaddle();
  state = 'ready';
  hideOverlay();
  pauseBtn.textContent = '⏸ Pause';
  updateHUD();
}
function startGame(){
  score = 0; lives = 3; combo = 0; multiplier = 1;
  loadLevel(0);
}
function nextLevel(){
  if(levelIndex + 1 < LEVELS.length) loadLevel(levelIndex + 1);
}
function launchBall(){
  if(state !== 'ready') return;
  state = 'playing';
  hideOverlay();
  const speed = currentBallSpeed();
  const ang = Math.random()*0.6 - 0.3;
  balls.forEach(b => { b.stuck = false; b.vx = Math.sin(ang)*speed; b.vy = -Math.cos(ang)*speed; });
  sfx.launch();
  floater(paddle.x, paddle.y - 32, 'GO!', '#ffffff', 22);
}
function primaryAction(){
  ensureAudio();
  if(state === 'menu' || state === 'gameOver' || state === 'victory') startGame();
  else if(state === 'levelComplete') nextLevel();
  else if(state === 'ready') launchBall();
  else if(state === 'paused') togglePause();
}
function togglePause(){
  if(state === 'playing'){ state = 'paused'; showOverlay('paused'); pauseBtn.textContent = '▶ Resume'; }
  else if(state === 'paused'){ state = 'playing'; hideOverlay(); pauseBtn.textContent = '⏸ Pause'; }
}
function toggleMute(){
  muted = !muted;
  soundBtn.textContent = muted ? '🔇 Sound off' : '🔊 Sound on';
  if(!muted) ensureAudio();
}
function gameOver(){
  state = 'gameOver';
  saveHi(); updateHUD();
  sfx.gameOver();
  shake = 16; flash = .4; flashColor = '244,63,94';
  showOverlay('gameOver');
}
function onAllBallsLost(){
  lives--; combo = 0; multiplier = 1; updateHUD();
  if(lives <= 0){ gameOver(); return; }
  state = 'ready';
  expandUntil = 0; fireUntil = 0; slowUntil = 0; wasSlow = false;
  paddle.w = paddle.baseW;
  resetBallToPaddle();
  floater(W/2, H/2, 'BALL LOST', '#fda4af', 28);
}
function checkLevelComplete(){
  if(state !== 'playing') return;
  const remaining = bricks.some(b => !b.dead && b.type !== 'X');
  if(remaining) return;
  const bonus = 500 + Math.max(0, lives - 1) * 100;
  score += bonus;
  if(score > hiScore) hiScore = score;
  spawnConfetti();
  sfx.levelClear();
  if(levelIndex + 1 >= LEVELS.length){
    state = 'victory';
    saveHi(); updateHUD();
    showOverlay('victory');
  } else {
    state = 'levelComplete';
    saveHi(); updateHUD();
    showOverlay('levelComplete', { bonus: bonus, nextName: LEVELS[levelIndex+1].name });
  }
}

/* ---------- Particles & popups ---------- */
function spawnParticles(x, y, color, n, speed, gravity){
  n = n || 12; speed = speed || 200; gravity = (gravity === undefined) ? 520 : gravity;
  for(let i = 0; i < n; i++){
    const ang = Math.random()*Math.PI*2;
    const sp = speed * (0.3 + Math.random()*0.7);
    particles.push({
      x:x, y:y, vx:Math.cos(ang)*sp, vy:Math.sin(ang)*sp - 40,
      life:0, maxLife:0.45 + Math.random()*0.5,
      size:1.5 + Math.random()*3.5, color:color, gravity:gravity,
      rot:Math.random()*Math.PI, vr:(Math.random()-0.5)*10,
      shape: Math.random() < 0.3 ? 'rect' : 'circle'
    });
  }
}
function spawnConfetti(){
  const colors = ['#ff4d6d','#22d3ee','#a78bfa','#facc15','#4ade80','#fb923c'];
  for(let i = 0; i < 140; i++){
    particles.push({
      x:Math.random()*W, y:-20 - Math.random()*200,
      vx:(Math.random()-0.5)*80, vy:80 + Math.random()*160,
      life:0, maxLife:2 + Math.random()*1.5,
      size:4 + Math.random()*5, color:colors[i % colors.length],
      gravity:60, rot:Math.random()*Math.PI, vr:(Math.random()-0.5)*12, shape:'rect'
    });
  }
}
function updateParticles(dt){
  for(let i = particles.length-1; i >= 0; i--){
    const p = particles[i];
    p.life += dt;
    if(p.life >= p.maxLife){ particles.splice(i,1); continue; }
    p.vy += p.gravity*dt; p.x += p.vx*dt; p.y += p.vy*dt; p.rot += p.vr*dt;
  }
  for(let i = floaters.length-1; i >= 0; i--){
    const f = floaters[i];
    f.life += dt; f.y -= 42*dt;
    if(f.life > 1.1) floaters.splice(i,1);
  }
}
function floater(x, y, text, color, size){
  floaters.push({ x:clamp(x,60,W-60), y:y, text:text, color:color||'#ffffff', size:size||16, life:0 });
}
function addScore(points, x, y, color, size){
  score += points;
  if(score > hiScore) hiScore = score;
  if(x !== undefined) floater(x, y, '+' + points, color || '#ffffff', size || 14);
  updateHUD();
}

/* ---------- Bricks, power-ups ---------- */
function damageBrick(br, dmg){
  if(br.dead) return;
  if(br.type === 'X'){
    br.flash = 1; br.wobble = 1;
    spawnParticles(br.x + br.w/2, br.y + br.h/2, '#94a3b8', 4, 100);
    sfx.clank();
    return;
  }
  br.hp -= dmg; br.flash = 1; br.wobble = 1;
  const cx = br.x + br.w/2, cy = br.y + br.h/2;
  if(br.hp > 0){
    addScore(10, cx, cy, '#e2e8f0', 12);
    spawnParticles(cx, cy, br.color, 6, 140);
    sfx.hit();
    return;
  }
  br.dead = true;
  combo++;
  const oldMult = multiplier;
  multiplier = 1 + Math.floor(combo / 8);
  if(multiplier > oldMult){
    floater(W/2, H/2 - 40, 'COMBO ×' + multiplier, '#fde68a', 30);
    sfx.combo();
  }
  addScore(br.points * multiplier, cx, cy, br.color, 16);
  spawnParticles(cx, cy, br.color, br.type === 'E' ? 26 : 16, br.type === 'E' ? 320 : 220);
  shake = Math.min(14, shake + (br.type === 'E' ? 7 : 2.5));
  sfx.breakBrick(combo);
  maybeDropPowerup(cx, cy);
  if(br.type === 'E') explodeBrick(br);
  updateHUD();
}
function explodeBrick(br){
  flash = .25; flashColor = '251,146,60';
  sfx.explosion();
  spawnParticles(br.x + br.w/2, br.y + br.h/2, '#f97316', 30, 380);
  const cx = br.x + br.w/2, cy = br.y + br.h/2;
  const neighbors = bricks.filter(o => o !== br && !o.dead &&
    Math.abs((o.x + o.w/2) - cx) < br.w * 1.6 &&
    Math.abs((o.y + o.h/2) - cy) < br.h * 2.2);
  neighbors.forEach(n => damageBrick(n, 1));
}
function maybeDropPowerup(x, y){
  if(powerups.length > 3) return;
  if(Math.random() > 0.16) return;
  const roll = Math.random();
  let type = 'expand';
  if(roll < 0.30) type = 'expand';
  else if(roll < 0.55) type = 'multi';
  else if(roll < 0.73) type = 'slow';
  else if(roll < 0.93) type = 'fire';
  else type = 'life';
  powerups.push({ x:x, y:y, vy:155, type:type, phase:Math.random()*Math.PI*2 });
}
function applyPowerup(type, x, y){
  const info = POWER_INFO[type];
  floater(x, y - 14, info.name + '!', info.color, 20);
  spawnParticles(x, y, info.color, 18, 200);
  sfx.power();
  if(type === 'expand') expandUntil = gameTime + 10;
  else if(type === 'fire') fireUntil = gameTime + 7;
  else if(type === 'slow'){
    if(gameTime >= slowUntil){
      balls.forEach(b => { if(!b.stuck){ b.vx *= 0.65; b.vy *= 0.65; } });
    }
    slowUntil = gameTime + 8;
  } else if(type === 'multi'){
    const clones = [];
    for(const b of balls){
      if(b.stuck) continue;
      for(let k = 0; k < 2 && balls.length + clones.length < 6; k++){
        const ang = Math.atan2(b.vy, b.vx) + (k === 0 ? -0.5 : 0.5);
        const sp = Math.hypot(b.vx, b.vy);
        clones.push({ x:b.x, y:b.y, vx:Math.cos(ang)*sp, vy:Math.sin(ang)*sp, r:b.r, trail:[], stuck:false });
      }
    }
    balls = balls.concat(clones);
  } else if(type === 'life'){
    lives = Math.min(5, lives + 1);
  }
  updateHUD();
}
function updatePowerups(dt){
  for(let i = powerups.length-1; i >= 0; i--){
    const p = powerups[i];
    p.y += p.vy*dt; p.phase += dt*6; p.x += Math.sin(p.phase)*0.3;
    if(Math.abs(p.x - paddle.x) <= paddle.w/2 + 14 && Math.abs(p.y - paddle.y) <= 20){
      applyPowerup(p.type, p.x, p.y);
      powerups.splice(i,1); continue;
    }
    if(p.y > H + 20) powerups.splice(i,1);
  }
}
function handleSlowExpiry(){
  const isSlow = gameTime < slowUntil;
  if(wasSlow && !isSlow){
    balls.forEach(b => {
      if(b.stuck) return;
      const s = Math.hypot(b.vx, b.vy);
      const ns = Math.min(620, s / 0.65);
      if(s > 0){ b.vx = b.vx/s*ns; b.vy = b.vy/s*ns; }
    });
  }
  wasSlow = isSlow;
}

/* ---------- Update ---------- */
function updatePaddle(dt){
  const left = keys['ArrowLeft'] || keys['KeyA'];
  const right = keys['ArrowRight'] || keys['KeyD'];
  if(left){ paddle.targetX -= 620*dt; usingMouse = false; }
  if(right){ paddle.targetX += 620*dt; usingMouse = false; }
  paddle.targetX = clamp(paddle.targetX, paddle.w/2, W - paddle.w/2);
  const prev = paddle.x;
  paddle.x += (paddle.targetX - paddle.x) * Math.min(1, dt*30);
  paddle.x = clamp(paddle.x, paddle.w/2, W - paddle.w/2);
  paddle.vel = dt > 0 ? (paddle.x - prev)/dt : 0;
  paddle.tilt = clamp(paddle.vel/900, -1, 1);
  const targetW = (gameTime < expandUntil) ? 172 : paddle.baseW;
  paddle.w += (targetW - paddle.w) * Math.min(1, dt*10);
  paddle.squash = Math.max(0, paddle.squash - dt*4);
  if(state === 'ready' && balls[0]){
    balls[0].x = paddle.x;
    balls[0].y = paddle.y - balls[0].r - 2;
  }
}
function increaseBallSpeed(b, amt){
  const s = Math.hypot(b.vx, b.vy);
  if(s === 0 || s >= 620) return;
  const ns = Math.min(620, s + amt);
  b.vx = b.vx/s*ns; b.vy = b.vy/s*ns;
}
function updateBalls(dt){
  for(let i = balls.length-1; i >= 0; i--){
    const b = balls[i];
    if(b.stuck) continue;
    b.trail.push({ x:b.x, y:b.y });
    if(b.trail.length > 14) b.trail.shift();
    const prevX = b.x, prevY = b.y;
    b.x += b.vx*dt; b.y += b.vy*dt;

    if(b.x - b.r < 0){ b.x = b.r; b.vx = Math.abs(b.vx); sfx.wall(); }
    else 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(); }

    if(b.vy > 0 && b.y + b.r >= paddle.y && b.y - b.r <= paddle.y + paddle.h + 6 &&
       Math.abs(b.x - paddle.x) <= paddle.w/2 + b.r){
      const rel = clamp((b.x - paddle.x) / (paddle.w/2), -1, 1);
      const speed = Math.hypot(b.vx, b.vy) || currentBallSpeed();
      const ang = rel * 1.05;
      b.vx = Math.sin(ang)*speed;
      b.vy = -Math.cos(ang)*speed;
      b.y = paddle.y - b.r - 1;
      combo = 0; multiplier = 1; updateHUD();
      paddle.squash = 1;
      spawnParticles(b.x, paddle.y, '#67e8f9', 6, 120);
      sfx.paddle();
    }

    for(const br of bricks){
      if(br.dead) continue;
      if(b.x + b.r > br.x && b.x - b.r < br.x + br.w && b.y + b.r > br.y && b.y - b.r < br.y + br.h){
        if(gameTime < fireUntil && br.type !== 'X'){
          damageBrick(br, 999);
          spawnParticles(b.x, b.y, '#fb923c', 10, 200);
          break;
        }
        if(prevY + b.r <= br.y || prevY - b.r >= br.y + br.h) b.vy = -b.vy;
        else if(prevX + b.r <= br.x || prevX - b.r >= br.x + br.w) b.vx = -b.vx;
        else b.vy = -b.vy;
        damageBrick(br, 1);
        increaseBallSpeed(b, 4);
        break;
      }
    }

    if(b.y - b.r > H){
      balls.splice(i,1);
      spawnParticles(b.x, H - 10, '#f43f5e', 18, 240);
      shake = Math.min(18, shake + 8);
      flash = .35; flashColor = '244,63,94';
      sfx.loseBall();
      if(balls.length === 0){ onAllBallsLost(); return; }
    }
  }
  bricks = bricks.filter(br => !br.dead);
}
function setChip(el, remain){
  if(remain > 0){
    el.style.display = 'inline-flex';
    el.querySelector('b').textContent = Math.ceil(remain) + 's';
  } else {
    el.style.display = 'none';
  }
}
function update(dt){
  if(state !== 'paused') updateParticles(dt);
  if(state === 'playing' || state === 'ready'){
    handleSlowExpiry();
    updatePaddle(dt);
    if(state === 'playing'){
      updateBalls(dt);
      updatePowerups(dt);
      checkLevelComplete();
    }
  }
  shake = Math.max(0, shake - dt*28);
  flash = Math.max(0, flash - dt*1.8);
  for(const b of bricks){
    b.flash = Math.max(0, b.flash - dt*5);
    b.wobble = Math.max(0, b.wobble - dt*3);
  }
  setChip(chipExpand, expandUntil - gameTime);
  setChip(chipFire, fireUntil - gameTime);
  setChip(chipSlow, slowUntil - gameTime);
}

/* ---------- Draw ---------- */
let bgGrad = null;
function rr(x, y, w, h, r){
  r = Math.min(r, w/2, h/2);
  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 drawNebula(x, y, r, color){
  const g = ctx.createRadialGradient(x, y, 0, x, y, r);
  g.addColorStop(0, color); g.addColorStop(1, 'rgba(0,0,0,0)');
  ctx.fillStyle = g; ctx.fillRect(x-r, y-r, r*2, r*2);
}
function drawBackground(){
  if(!bgGrad){
    bgGrad = ctx.createLinearGradient(0, 0, 0, H);
    bgGrad.addColorStop(0, '#0f0c29'); bgGrad.addColorStop(.5, '#141433'); bgGrad.addColorStop(1, '#050816');
  }
  ctx.fillStyle = bgGrad; ctx.fillRect(0, 0, W, H);
  drawNebula(180 + Math.sin(animTime*0.2)*30, 160, 260, 'rgba(124,58,237,0.18)');
  drawNebula(620 + Math.cos(animTime*0.16)*30, 420, 280, 'rgba(34,211,238,0.13)');
  drawNebula(400, 520, 240, 'rgba(255,77,109,0.10)');
  for(const s of stars){
    const tw = 0.5 + 0.5*Math.sin(animTime*2 + s.tw);
    ctx.globalAlpha = 0.35 + 0.4*tw;
    ctx.fillStyle = '#e0e7ff';
    ctx.beginPath(); ctx.arc(s.x, (s.y + animTime*s.sp) % H, s.s, 0, Math.PI*2); ctx.fill();
  }
  ctx.globalAlpha = 1;
}
function drawBricks(){
  for(const b of bricks){
    if(b.dead) continue;
    const wob = b.wobble > 0 ? Math.sin(animTime*40 + b.x)*b.wobble*1.5 : 0;
    const y = b.y + wob;
    let color = b.color;
    if(b.type === '2' && b.hp === 1) color = '#67e8f9';
    if(b.type === '3' && b.hp === 2) color = '#c4b5fd';
    if(b.type === '3' && b.hp === 1) color = '#ddd6fe';
    ctx.save();
    ctx.shadowColor = color; ctx.shadowBlur = (b.type === 'X') ? 4 : 12;
    ctx.fillStyle = color; rr(b.x, y, b.w, b.h, 5); ctx.fill();
    ctx.shadowBlur = 0;
    const g = ctx.createLinearGradient(b.x, y, b.x, y+b.h);
    g.addColorStop(0, 'rgba(255,255,255,0.35)'); g.addColorStop(.5, 'rgba(255,255,255,0.05)'); g.addColorStop(1, 'rgba(0,0,0,0.22)');
    ctx.fillStyle = g; rr(b.x, y, b.w, b.h, 5); ctx.fill();
    if(b.type === 'X'){
      ctx.strokeStyle = 'rgba(226,232,240,0.7)'; ctx.lineWidth = 1.5;
      ctx.beginPath(); ctx.moveTo(b.x+6, y+b.h-6); ctx.lineTo(b.x+b.w-6, y+6); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(b.x+6, y+6); ctx.lineTo(b.x+b.w-6, y+b.h-6); ctx.stroke();
    } else if(b.type === 'E'){
      const pulse = 1 + Math.sin(animTime*8 + b.x)*0.18;
      ctx.fillStyle = '#fff7ed';
      ctx.beginPath(); ctx.arc(b.x+b.w/2, y+b.h/2, 5*pulse, 0, Math.PI*2); ctx.fill();
      ctx.fillStyle = '#7c2d12'; ctx.font = 'bold 7px sans-serif';
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText('!', b.x+b.w/2, y+b.h/2 + 0.5);
    } else if(b.maxHp > 1){
      ctx.fillStyle = 'rgba(255,255,255,0.85)';
      for(let i = 0; i < b.hp; i++){
        ctx.beginPath(); ctx.arc(b.x + b.w/2 + (i - (b.hp-1)/2)*9, y + b.h/2, 2.2, 0, Math.PI*2); ctx.fill();
      }
    }
    if(b.flash > 0){ ctx.fillStyle = 'rgba(255,255,255,' + (b.flash*0.8) + ')'; rr(b.x, y, b.w, b.h, 5); ctx.fill(); }
    ctx.strokeStyle = 'rgba(255,255,255,0.18)'; ctx.lineWidth = 1; rr(b.x, y, b.w, b.h, 5); ctx.stroke();
    ctx.restore();
  }
}
function drawPaddle(){
  const p = paddle, expanded = gameTime < expandUntil;
  ctx.save();
  ctx.translate(p.x, p.y);
  ctx.rotate(p.tilt * 0.06);
  ctx.scale(1 + p.squash*0.08, 1 - p.squash*0.25);
  ctx.shadowColor = expanded ? '#f0abfc' : '#22d3ee'; ctx.shadowBlur = 18;
  const g = ctx.createLinearGradient(0, -p.h/2, 0, p.h/2);
  g.addColorStop(0, '#a5f3fc');
  g.addColorStop(.45, expanded ? '#e879f9' : '#22d3ee');
  g.addColorStop(1, expanded ? '#a21caf' : '#0369a1');
  ctx.fillStyle = g; rr(-p.w/2, -p.h/2, p.w, p.h, 7); ctx.fill();
  ctx.shadowBlur = 0;
  ctx.fillStyle = 'rgba(255,255,255,0.85)'; rr(-p.w/2, -p.h/2, p.w, 3, 2); ctx.fill();
  ctx.fillStyle = 'rgba(255,255,255,0.9)'; rr(-10, -p.h/2 + 3, 20, 4, 2); ctx.fill();
  ctx.restore();
}
function drawBalls(){
  const fire = gameTime < fireUntil;
  for(const b of balls){
    for(let i = 0; i < b.trail.length; i++){
      const t = b.trail[i], a = (i / b.trail.length) * 0.25;
      ctx.fillStyle = fire ? 'rgba(251,146,60,' + a + ')' : 'rgba(165,243,252,' + a + ')';
      ctx.beginPath(); ctx.arc(t.x, t.y, b.r * (i / b.trail.length) * 0.8, 0, Math.PI*2); ctx.fill();
    }
    ctx.save();
    ctx.shadowColor = fire ? '#fb923c' : '#67e8f9'; ctx.shadowBlur = 16;
    const g = ctx.createRadialGradient(b.x-2, b.y-2, 1, b.x, b.y, b.r);
    if(fire){ g.addColorStop(0,'#fff7ed'); g.addColorStop(.5,'#fdba74'); g.addColorStop(1,'#ea580c'); }
    else { g.addColorStop(0,'#ffffff'); g.addColorStop(.5,'#a5f3fc'); g.addColorStop(1,'#0891b2'); }
    ctx.fillStyle = g;
    ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI*2); ctx.fill();
    ctx.restore();
  }
}
function drawPowerups(){
  for(const p of powerups){
    const info = POWER_INFO[p.type];
    ctx.save();
    ctx.translate(p.x, p.y + Math.sin(p.phase)*2);
    ctx.shadowColor = info.color; ctx.shadowBlur = 14;
    ctx.fillStyle = info.color; rr(-16, -10, 32, 20, 10); ctx.fill();
    ctx.shadowBlur = 0;
    ctx.strokeStyle = 'rgba(255,255,255,0.7)'; ctx.lineWidth = 1.5; rr(-16, -10, 32, 20, 10); ctx.stroke();
    ctx.fillStyle = 'rgba(0,0,0,0.75)'; ctx.font = 'bold 13px system-ui, sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText(info.label, 0, 1);
    ctx.restore();
  }
}
function drawParticles(){
  for(const p of particles){
    const a = 1 - p.life/p.maxLife;
    ctx.save();
    ctx.globalAlpha = Math.max(0, a);
    ctx.translate(p.x, p.y); ctx.rotate(p.rot);
    ctx.fillStyle = p.color;
    if(p.shape === 'rect') ctx.fillRect(-p.size/2, -p.size/3, p.size, p.size*0.66);
    else { ctx.beginPath(); ctx.arc(0, 0, p.size*0.6, 0, Math.PI*2); ctx.fill(); }
    ctx.restore();
  }
}
function drawFloaters(){
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  for(const f of floaters){
    const a = f.life < 0.8 ? 1 : 1 - (f.life - 0.8)/0.3;
    ctx.save();
    ctx.globalAlpha = Math.max(0, a);
    ctx.font = '900 ' + f.size + 'px system-ui, sans-serif';
    ctx.shadowColor = f.color; ctx.shadowBlur = 10;
    ctx.fillStyle = f.color;
    ctx.fillText(f.text, f.x, f.y);
    ctx.restore();
  }
}
function drawReadyHint(){
  const pulse = 0.7 + 0.3*Math.sin(animTime*5);
  ctx.save();
  ctx.globalAlpha = pulse; ctx.textAlign = 'center';
  ctx.font = '900 24px system-ui, sans-serif';
  ctx.shadowColor = '#22d3ee'; ctx.shadowBlur = 16; ctx.fillStyle = '#e0faff';
  ctx.fillText('CLICK or press SPACE to launch', W/2, H/2 + 30);
  ctx.font = '700 15px system-ui, sans-serif';
  ctx.fillStyle = '#94a3b8'; ctx.shadowBlur = 0;
  ctx.fillText('Move the paddle with your mouse or ← → keys', W/2, H/2 + 58);
  if(balls[0]){
    ctx.strokeStyle = '#e0faff'; ctx.lineWidth = 3;
    ctx.beginPath(); ctx.moveTo(balls[0].x, balls[0].y - 18); ctx.lineTo(balls[0].x, balls[0].y - 42); ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(balls[0].x - 7, balls[0].y - 34);
    ctx.lineTo(balls[0].x, balls[0].y - 46);
    ctx.lineTo(balls[0].x + 7, balls[0].y - 34);
    ctx.closePath(); ctx.fillStyle = '#e0faff'; ctx.fill();
  }
  ctx.restore();
}
function draw(){
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  if(shake > 0.2) ctx.translate((Math.random()-0.5)*shake, (Math.random()-0.5)*shake);
  drawBackground();
  drawBricks();
  drawPowerups();
  drawPaddle();
  drawBalls();
  drawParticles();
  drawFloaters();
  if(state === 'ready') drawReadyHint();
  if(flash > 0){ ctx.fillStyle = 'rgba(' + flashColor + ',' + flash + ')'; ctx.fillRect(-20, -20, W+40, H+40); }
  const vg = ctx.createRadialGradient(W/2, H/2, H*0.35, W/2, H/2, H*0.85);
  vg.addColorStop(0, 'rgba(0,0,0,0)'); vg.addColorStop(1, 'rgba(0,0,0,0.35)');
  ctx.fillStyle = vg; ctx.fillRect(0, 0, W, H);
}

/* ---------- Input ---------- */
window.addEventListener('keydown', e => {
  if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Space'].indexOf(e.code) >= 0) e.preventDefault();
  keys[e.code] = true;
  if(e.code === 'ArrowLeft' || e.code === 'ArrowRight' || e.code === 'KeyA' || e.code === 'KeyD') usingMouse = false;
  if(e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') primaryAction();
  if(e.code === 'KeyP' || e.code === 'Escape') togglePause();
  if(e.code === 'KeyM') toggleMute();
});
window.addEventListener('keyup', e => { keys[e.code] = false; });

function pointerX(e){
  const r = canvas.getBoundingClientRect();
  const clientX = e.touches ? e.touches[0].clientX : e.clientX;
  return (clientX - r.left) * (W / r.width);
}
canvas.addEventListener('mousemove', e => {
  paddle.targetX = pointerX(e); usingMouse = true;
});
canvas.addEventListener('click', () => {
  ensureAudio();
  if(state === 'ready') launchBall();
});
canvas.addEventListener('touchstart', e => {
  e.preventDefault(); ensureAudio();
  paddle.targetX = pointerX(e); usingMouse = true;
  if(state === 'ready') launchBall();
}, { passive:false });
canvas.addEventListener('touchmove', e => {
  e.preventDefault();
  paddle.targetX = pointerX(e); usingMouse = true;
}, { passive:false });
canvas.addEventListener('contextmenu', e => e.preventDefault());

primaryBtn.addEventListener('click', () => { ensureAudio(); primaryAction(); primaryBtn.blur(); });
pauseBtn.addEventListener('click', () => { ensureAudio(); togglePause(); pauseBtn.blur(); });
soundBtn.addEventListener('click', () => { toggleMute(); soundBtn.blur(); });
restartBtn.addEventListener('click', () => { ensureAudio(); startGame(); restartBtn.blur(); });
document.addEventListener('visibilitychange', () => { if(document.hidden && state === 'playing') togglePause(); });
window.addEventListener('blur', () => { if(state === 'playing') togglePause(); });

/* ---------- Init & main loop ---------- */
function initStars(){
  stars = [];
  for(let i = 0; i < 90; i++){
    stars.push({ x:Math.random()*W, y:Math.random()*H, s:Math.random()*1.8 + 0.3, sp:Math.random()*18 + 6, tw:Math.random()*Math.PI*2 });
  }
}
initStars();
loadLevel(0);
state = 'menu';
updateHUD();
showOverlay('menu');

let last = performance.now();
function frame(now){
  requestAnimationFrame(frame);
  let dt = (now - last) / 1000;
  last = now;
  if(dt > 0.033) dt = 0.033;
  animTime += dt;
  if(state === 'playing' || state === 'ready') gameTime += dt;
  update(dt);
  draw();
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

Compare with

Muse Ember: other attempts