hypeorcode
Network offOpen alone (new tab)

Run details

Model
mistralai/mistral-large-4-0
Route
OpenRouter API, one user message
Reasoning
provider default, 60,105 reasoning tokens
Limits
60 min · 143,540 output tokens
Date
21 Oct 2026
Time
15 min 19 s
Size
38 kB
Tokens
81 in, 74,311 out
List price
$0.31 ($1.36 in / $4.18 out per M)
Paid
$0.31

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

38 kB 1,125 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>BREAKOUT — Neon Brick Breaker</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden}
body{
  background:#020308;
  display:flex;align-items:center;justify-content:center;
  font-family:'Trebuchet MS','Segoe UI',Verdana,sans-serif;
  color:#e2e8f0;
  user-select:none;-webkit-user-select:none;
  touch-action:none;
}
#stage{position:relative;line-height:0}
#game{
  display:block;
  width:min(97vw,calc(97vh * 1.6));
  aspect-ratio:960/600;
  border-radius:12px;
  border:1px solid rgba(110,140,255,.28);
  box-shadow:0 0 0 1px rgba(0,0,0,.7),0 0 50px rgba(34,211,238,.10),0 30px 90px rgba(0,0,0,.85);
  background:#070812;
  cursor:none;
  touch-action:none;
}
/* ---------- HUD ---------- */
#hud{
  position:absolute;top:0;left:0;right:0;
  display:flex;align-items:flex-start;justify-content:space-between;
  padding:12px 18px;pointer-events:none;
  font-family:ui-monospace,'Cascadia Mono','SF Mono',Menlo,Consolas,monospace;
  transition:opacity .3s;
}
#stage.is-menu #hud{opacity:0}
.hud-group{display:flex;gap:22px}
.hud-right{display:flex;align-items:flex-end;gap:14px}
.hud-item{display:flex;flex-direction:column;gap:3px}
.hud-item .lbl{font-size:9px;font-weight:700;letter-spacing:.3em;color:#64748b}
.hud-item .val{font-size:19px;font-weight:700;color:#e8fbff;font-variant-numeric:tabular-nums;line-height:1}
#score{color:#a5f3fc;text-shadow:0 0 14px rgba(34,211,238,.5);display:inline-block}
#level{color:#fbbf24;text-shadow:0 0 14px rgba(251,191,36,.45)}
#best{color:#64748b;font-size:15px;text-shadow:none}
#lives{display:flex;gap:5px;height:19px;align-items:center}
#lives .dot{width:11px;height:11px;border-radius:50%;background:#f0feff;box-shadow:0 0 10px rgba(103,232,249,.8)}
#score.pop{animation:pop .22s ease-out}
@keyframes pop{0%{transform:scale(1)}35%{transform:scale(1.3)}100%{transform:scale(1)}}
#muteBtn{
  pointer-events:auto;margin-bottom:1px;
  background:none;border:1px solid rgba(148,163,184,.3);border-radius:6px;
  color:#64748b;font:700 9px ui-monospace,Menlo,monospace;letter-spacing:.18em;
  padding:5px 9px;cursor:pointer;transition:.15s;
}
#muteBtn:hover{color:#cbd5e1;border-color:#94a3b8}
#pups{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;padding-top:2px}
.chip{
  display:flex;align-items:center;gap:7px;
  background:rgba(8,12,26,.85);border:1px solid rgba(148,163,184,.25);
  border-radius:999px;padding:4px 10px 4px 8px;
  font:700 10px ui-monospace,Menlo,monospace;letter-spacing:.12em;
}
.chip .sw{width:9px;height:9px;border-radius:3px;background:currentColor;box-shadow:0 0 8px currentColor}
.chip .bar{width:36px;height:3px;border-radius:2px;background:rgba(255,255,255,.14);overflow:hidden}
.chip .bar i{display:block;height:100%;background:currentColor}
#combo{
  position:absolute;top:48px;left:50%;transform:translateX(-50%) scale(.9);
  font:800 17px ui-monospace,Menlo,monospace;letter-spacing:.18em;
  color:#fbbf24;text-shadow:0 0 18px rgba(251,191,36,.6);
  opacity:0;transition:opacity .15s;pointer-events:none;white-space:nowrap;
}
#combo.show{opacity:1;transform:translateX(-50%) scale(1)}
#combo.pop{animation:cpop .18s ease-out}
@keyframes cpop{0%{transform:translateX(-50%) scale(1)}40%{transform:translateX(-50%) scale(1.25)}100%{transform:translateX(-50%) scale(1)}}
/* ---------- level banner ---------- */
#banner{
  position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:10px;pointer-events:none;opacity:0;transition:opacity .18s;
}
#banner.show{opacity:1}
#banner .b1{font:800 clamp(26px,5vw,46px) ui-monospace,Menlo,monospace;letter-spacing:.16em;text-indent:.16em;color:#ecfeff;text-shadow:0 0 28px rgba(34,211,238,.6)}
#banner .b2{font:700 12px ui-monospace,Menlo,monospace;letter-spacing:.34em;text-indent:.34em;color:#94a3b8}
#banner .b2.good{color:#4ade80;text-shadow:0 0 16px rgba(74,222,128,.55)}
/* ---------- overlays ---------- */
.ov{
  position:absolute;inset:0;border-radius:12px;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;
  background:rgba(4,5,14,.72);
  backdrop-filter:blur(7px);-webkit-backdrop-filter:blur(7px);
  opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity .25s,visibility .25s;
  text-align:center;padding:24px;line-height:1.5;
}
.ov.show{opacity:1;visibility:visible;pointer-events:auto}
.logo{display:flex;gap:7px;margin-bottom:2px}
.logo i{width:22px;height:13px;border-radius:3px;display:block;animation:bob 1.5s ease-in-out infinite}
.logo i:nth-child(1){background:#f472b6;box-shadow:0 0 14px rgba(244,114,182,.55)}
.logo i:nth-child(2){background:#a78bfa;box-shadow:0 0 14px rgba(167,139,250,.55);animation-delay:.12s}
.logo i:nth-child(3){background:#fbbf24;box-shadow:0 0 14px rgba(251,191,36,.55);animation-delay:.24s}
.logo i:nth-child(4){background:#22d3ee;box-shadow:0 0 14px rgba(34,211,238,.55);animation-delay:.36s}
.logo i:nth-child(5){background:#4ade80;box-shadow:0 0 14px rgba(74,222,128,.55);animation-delay:.48s}
@keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
.title{
  font-size:clamp(30px,6vw,54px);font-weight:800;
  letter-spacing:.28em;text-indent:.28em;color:#ecfeff;
  text-shadow:0 0 20px rgba(34,211,238,.5),0 0 70px rgba(34,211,238,.22);
}
.tagline{font:700 10px ui-monospace,Menlo,monospace;letter-spacing:.44em;text-indent:.44em;color:#64748b}
.btn{
  font-family:inherit;font-size:14px;font-weight:700;
  letter-spacing:.28em;text-indent:.28em;text-transform:uppercase;
  color:#a5f3fc;background:rgba(34,211,238,.06);
  border:1px solid rgba(34,211,238,.55);border-radius:8px;
  padding:13px 36px;cursor:pointer;transition:.15s;margin-top:4px;
}
.btn:hover{background:rgba(34,211,238,.16);box-shadow:0 0 26px rgba(34,211,238,.35);transform:translateY(-1px)}
.controls{display:flex;flex-direction:column;gap:8px;font:500 11px ui-monospace,Menlo,monospace;color:#94a3b8;letter-spacing:.05em}
.controls kbd{
  border:1px solid rgba(255,255,255,.18);border-bottom-width:2px;border-radius:4px;
  padding:1px 7px;font-family:ui-monospace,Menlo,monospace;font-size:10px;color:#e2e8f0;
  background:rgba(255,255,255,.05);
}
.small{font:600 10px ui-monospace,Menlo,monospace;letter-spacing:.22em;color:#475569}
.ov h2{font-size:clamp(26px,5vw,42px);font-weight:800;letter-spacing:.2em;text-indent:.2em;color:#ecfeff;text-shadow:0 0 24px rgba(34,211,238,.4)}
.ov h2.bad{color:#fda4af;text-shadow:0 0 26px rgba(251,113,133,.5)}
.ov h2.good{color:#86efac;text-shadow:0 0 26px rgba(74,222,128,.5)}
.bigscore{font:800 36px ui-monospace,Menlo,monospace;color:#a5f3fc;text-shadow:0 0 26px rgba(34,211,238,.5);font-variant-numeric:tabular-nums}
.sub{font:600 11px ui-monospace,Menlo,monospace;letter-spacing:.18em;color:#94a3b8}
@media (max-width:640px){
  .hud-group{gap:12px}
  .hud-item .val{font-size:15px}
  #best-wrap{display:none}
  .controls{font-size:10px}
}
</style>
</head>
<body>
<div id="stage" class="is-menu">
  <canvas id="game"></canvas>

  <div id="hud">
    <div class="hud-group">
      <div class="hud-item"><span class="lbl">SCORE</span><span class="val" id="score">0</span></div>
      <div class="hud-item"><span class="lbl">LEVEL</span><span class="val" id="level">1</span></div>
      <div class="hud-item" id="best-wrap"><span class="lbl">BEST</span><span class="val" id="best">0</span></div>
    </div>
    <div id="pups"></div>
    <div class="hud-right">
      <div class="hud-item"><span class="lbl">BALLS</span><span id="lives"></span></div>
      <button id="muteBtn" type="button">SOUND ON</button>
    </div>
  </div>

  <div id="combo"></div>
  <div id="banner"><div class="b1"></div><div class="b2"></div></div>

  <div class="ov show" id="ov-menu">
    <div class="logo"><i></i><i></i><i></i><i></i><i></i></div>
    <h1 class="title">BREAKOUT</h1>
    <div class="tagline">NEON BRICK BREAKER</div>
    <button class="btn" id="btn-play" type="button">PLAY</button>
    <div class="controls">
      <div><kbd>MOUSE</kbd> or <kbd>←</kbd> <kbd>→</kbd> — move the paddle</div>
      <div><kbd>SPACE</kbd> / <kbd>CLICK</kbd> — launch &amp; fire lasers</div>
      <div><kbd>P</kbd> pause &nbsp;·&nbsp; <kbd>M</kbd> mute &nbsp;·&nbsp; <kbd>R</kbd> restart</div>
    </div>
    <div class="small" id="menu-best"></div>
  </div>

  <div class="ov" id="ov-pause">
    <h2>PAUSED</h2>
    <button class="btn" id="btn-resume" type="button">RESUME</button>
    <div class="sub">PRESS P TO CONTINUE</div>
  </div>

  <div class="ov" id="ov-over">
    <h2 class="bad">GAME OVER</h2>
    <div class="bigscore" id="over-score">0</div>
    <div class="sub" id="over-best"></div>
    <button class="btn" id="btn-retry" type="button">RETRY</button>
  </div>

  <div class="ov" id="ov-win">
    <h2 class="good">ALL CLEAR!</h2>
    <div class="bigscore" id="win-score">0</div>
    <div class="sub">YOU BROKE EVERY BRICK</div>
    <button class="btn" id="btn-again" type="button">PLAY AGAIN</button>
  </div>
</div>

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

const scoreEl=$('score'), levelEl=$('level'), bestEl=$('best'), livesEl=$('lives'),
      pupsEl=$('pups'), comboEl=$('combo'), bannerEl=$('banner'),
      bannerB1=bannerEl.querySelector('.b1'), bannerB2=bannerEl.querySelector('.b2'),
      muteBtn=$('muteBtn'), stage=$('stage'), menuBest=$('menu-best');

const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const rnd=(a,b)=>a+Math.random()*(b-a);
const TAU=Math.PI*2;
const fmt=n=>n.toLocaleString('en-US');
const easeOutBack=p=>{const c1=1.70158,c3=c1+1;return 1+c3*Math.pow(p-1,3)+c1*Math.pow(p-1,2);};

/* ---------- persisted settings ---------- */
const LS_BEST='neon-breakout-best', LS_MUTE='neon-breakout-mute';
let best=0, muted=false;
try{
  best=+(localStorage.getItem(LS_BEST)||0)||0;
  muted=localStorage.getItem(LS_MUTE)==='1';
}catch(e){}

/* ---------- audio: everything synthesized locally with WebAudio ---------- */
let actx=null;
function audio(){
  if(!actx){
    const AC=window.AudioContext||window.webkitAudioContext;
    if(AC) actx=new AC();
  }
  if(actx&&actx.state==='suspended') actx.resume();
  return actx;
}
function tone(o){
  if(muted) return;
  const ac=audio(); if(!ac) return;
  const t0=ac.currentTime+(o.delay||0);
  const osc=ac.createOscillator(), g=ac.createGain();
  osc.type=o.type||'sine';
  osc.frequency.setValueAtTime(o.f,t0);
  if(o.f2) osc.frequency.exponentialRampToValueAtTime(o.f2,t0+o.d);
  g.gain.setValueAtTime(.0001,t0);
  g.gain.exponentialRampToValueAtTime(o.g??.1,t0+.006);
  g.gain.exponentialRampToValueAtTime(.0001,t0+o.d);
  osc.connect(g); g.connect(ac.destination);
  osc.start(t0); osc.stop(t0+o.d+.03);
}
const S={
  wall:  ()=>tone({f:180+Math.random()*60,f2:110,d:.05,type:'square',g:.05}),
  paddle:()=>tone({f:640,f2:980,d:.07,type:'sine',g:.12}),
  brick:hp=>tone({f:280+hp*150+rnd(-20,20),f2:110,d:.08,type:'square',g:.08}),
  hard:  ()=>tone({f:1100,d:.05,type:'square',g:.07}),
  break: ()=>{tone({f:880,f2:140,d:.16,type:'sawtooth',g:.1});tone({f:1400,d:.05,type:'square',g:.06});},
  power: ()=>{[660,990,1320].forEach((f,i)=>tone({f,d:.12,type:'sine',g:.09,delay:i*.07}));},
  life:  ()=>{[880,1320].forEach((f,i)=>tone({f,d:.14,type:'sine',g:.1,delay:i*.08}));},
  lose:  ()=>tone({f:480,f2:70,d:.5,type:'sawtooth',g:.16}),
  launch:()=>tone({f:320,f2:940,d:.13,type:'sine',g:.1}),
  laser: ()=>tone({f:1500,f2:420,d:.07,type:'square',g:.05}),
  level: ()=>{[523,659,784,1047].forEach((f,i)=>tone({f,d:.16,type:'sine',g:.09,delay:i*.09}));},
  explode:()=>{tone({f:140,f2:40,d:.3,type:'sawtooth',g:.15});tone({f:70,d:.28,type:'sine',g:.18});},
  menu:  ()=>tone({f:520,f2:800,d:.1,type:'sine',g:.08}),
};

/* ---------- levels (12 columns; 1/2/3 = hit points, H = hard, E = explosive) ---------- */
const LEVELS=[
  {name:'FIRST CONTACT', rows:[
    '............',
    '.1111111111.',
    '.2222222222.',
    '.1111111111.',
    '............']},
  {name:'PYRAMID', rows:[
    '............',
    '....2222....',
    '...333333...',
    '..33333333..',
    '...333333...',
    '....2222....',
    '............']},
  {name:'CHECKERBOARD', rows:[
    '............',
    '.H1.H1.H1.H.',
    '.2..2..2..2.',
    '.H1.H1.H1.H.',
    '............']},
  {name:'FORTRESS', rows:[
    'HHHHHHHHHHHH',
    'H111111111H',
    'H122222221H',
    'H122222221H',
    'H111111111H',
    'HHHHHHHHHHHH']},
  {name:'DIAMOND', rows:[
    '.....2......',
    '....3333....',
    '...333333...',
    '..33333333..',
    '...333333...',
    '....3333....',
    '.....2......',
    '............']},
  {name:'GAUNTLET', rows:[
    'E2E2E2E2E2E2',
    '.H3.H3.H3.H.',
    'E2E2E2E2E2E2',
    '.1.1.1.1.1.1',
    '.H2.H2.H2.H.',
    '..33333333..']},
];

/* ---------- game state ---------- */
let state='menu';            // menu | playing | paused | transition | gameover | victory
let score=0, level=1, lives=3, combo=0;
let t=0, freezeT=0, flashR=0, flashW=0, shakeA=0;
let transitionT=0, afterTransition=null, pendingServe=0;
let destructible=0, speedBase=390, speedCap=820;

const paddle={x:W/2, y:H-52, w:112, baseW:112, h:14, squash:1, laserCd:0};
const fx={expandT:0, slowT:0, laserT:0, fireT:0};
let balls=[], bricks=[], falls=[], lasers=[], parts=[], texts=[];

const PU_TYPES=[
  {id:'expand',   label:'WIDE',  color:'#22d3ee', w:.24},
  {id:'multiball',label:'×3',    color:'#f472b6', w:.18},
  {id:'slow',     label:'SLOW',  color:'#4ade80', w:.16},
  {id:'laser',    label:'LASER', color:'#fb7185', w:.16},
  {id:'fire',     label:'FIRE',  color:'#fb923c', w:.14},
  {id:'life',     label:'+1',    color:'#e8fbff', w:.12},
];
const CHIP_DEFS={
  expand:{label:'WIDE', color:'#22d3ee', get:()=>fx.expandT, max:14},
  slow:  {label:'SLOW', color:'#4ade80', get:()=>fx.slowT,   max:10},
  laser: {label:'LASER',color:'#fb7185', get:()=>fx.laserT,  max:10},
  fire:  {label:'FIRE', color:'#fb923c', get:()=>fx.fireT,   max:9},
};
const chipEls={};

/* ---------- input ---------- */
const keys={};
let mouseX=W/2, mouseActive=false;

window.addEventListener('keydown', e=>{
  keys[e.code]=true;
  if(e.repeat) return;
  if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Space'].includes(e.code)) e.preventDefault();
  if(e.code==='Space'||e.code==='Enter') action();
  else if(e.code==='KeyP'||e.code==='Escape') togglePause();
  else if(e.code==='KeyM') toggleMute();
  else if(e.code==='KeyR'){ if(state!=='menu') restart(); }
});
window.addEventListener('keyup', e=>{ keys[e.code]=false; });
window.addEventListener('blur', ()=>{ for(const k in keys) keys[k]=false; });

function mapPointer(clientX){
  const r=canvas.getBoundingClientRect();
  return clamp((clientX-r.left)*(W/r.width),0,W);
}
canvas.addEventListener('mousemove', e=>{ mouseX=mapPointer(e.clientX); mouseActive=true; });
canvas.addEventListener('mousedown', ()=>action());
canvas.addEventListener('touchstart', e=>{
  e.preventDefault();
  if(e.touches[0]){ mouseX=mapPointer(e.touches[0].clientX); mouseActive=true; }
  action();
},{passive:false});
canvas.addEventListener('touchmove', e=>{
  e.preventDefault();
  if(e.touches[0]){ mouseX=mapPointer(e.touches[0].clientX); mouseActive=true; }
},{passive:false});

function action(){
  if(state==='menu'){ startGame(); return; }
  if(state==='gameover'||state==='victory'){ restart(); return; }
  if(state==='paused'){ resume(); return; }
  if(state==='playing'||state==='transition'){
    const sb=balls.find(b=>b.stuck);
    if(sb) launch(sb);
  }
}
function togglePause(){
  if(state==='playing'){ state='paused'; showOv('pause'); }
  else if(state==='paused') resume();
}
function resume(){ hideOvs(); state='playing'; }
function toggleMute(){
  muted=!muted;
  muteBtn.textContent=muted?'SOUND OFF':'SOUND ON';
  try{ localStorage.setItem(LS_MUTE,muted?'1':'0'); }catch(e){}
  if(!muted) S.menu();
}
muteBtn.addEventListener('click', e=>{ e.stopPropagation(); toggleMute(); });

/* ---------- overlays ---------- */
const ovs={menu:$('ov-menu'), pause:$('ov-pause'), over:$('ov-over'), win:$('ov-win')};
function showOv(k){ for(const o in ovs) ovs[o].classList.toggle('show',o===k); syncChrome(); }
function hideOvs(){ for(const o in ovs) ovs[o].classList.remove('show'); syncChrome(); }
function syncChrome(){ stage.classList.toggle('is-menu',state==='menu'); }
Object.values(ovs).forEach(ov=>ov.addEventListener('click',e=>{ if(e.target===ov) action(); }));
document.querySelectorAll('.btn').forEach(b=>b.addEventListener('click',()=>b.blur()));
$('btn-play').addEventListener('click',()=>startGame());
$('btn-resume').addEventListener('click',()=>resume());
$('btn-retry').addEventListener('click',()=>restart());
$('btn-again').addEventListener('click',()=>restart());
document.addEventListener('visibilitychange',()=>{
  if(document.hidden&&state==='playing'){ state='paused'; showOv('pause'); }
});

/* ---------- flow ---------- */
function startGame(){
  if(state!=='menu') return;
  audio();
  score=0; lives=3; combo=0;
  updateHUD();
  startLevel(1);
  hideOvs();
  S.menu();
}
function restart(){
  if(state==='menu') return;
  audio();
  score=0; lives=3; combo=0;
  updateHUD();
  startLevel(1);
  hideOvs();
}
function startLevel(n){
  level=n;
  buildLevel(n);
  paddle.x=W/2; paddle.w=paddle.baseW; paddle.laserCd=0;
  fx.expandT=fx.slowT=fx.laserT=fx.fireT=0;
  balls=[]; falls=[]; lasers=[]; texts=[];
  combo=0; pendingServe=0; updateCombo();
  spawnStuckBall();
  updateHUD();
  banner('LEVEL '+level, LEVELS[level-1].name, false);
  state='transition';
  transitionT=1.35;
  afterTransition=()=>{ hideBanner(); state='playing'; };
  S.level();
  syncChrome();
}
function buildLevel(n){
  const rows=LEVELS[n-1].rows;
  bricks=[]; destructible=0;
  const cols=12, gap=5, margin=34, top=86;
  const bw=(W-margin*2-gap*(cols-1))/cols, bh=22;
  rows.forEach((row,r)=>{
    for(let c=0;c<cols;c++){
      const ch=row[c]||'.';
      if(ch==='.') continue;
      let type='normal', hp=1;
      if(ch==='H') type='hard';
      else if(ch==='E'){ type='explosive'; hp=1; }
      else hp=+ch;
      bricks.push({
        x:margin+c*(bw+gap), y:top+r*(bh+gap), w:bw, h:bh,
        type, hp, maxHp:hp, alive:true, flash:0,
        appear:rnd(.05,.15)+(r*cols+c)*.012, scale:0, pulse:rnd(0,TAU)
      });
      if(type!=='hard') destructible++;
    }
  });
  speedBase=390+(n-1)*28;
}
function spawnStuckBall(){
  balls.push({x:paddle.x, y:paddle.y-paddle.h/2-8, vx:0, vy:0, r:7,
              speed:speedBase, stuck:true, trail:[], squashT:0, fire:false});
}
function launch(b){
  b.stuck=false;
  b.fire=fx.fireT>0;
  const ang=-Math.PI/2+rnd(-.32,.32);
  const s=b.speed*(fx.slowT>0?.72:1);
  b.vx=Math.cos(ang)*s; b.vy=Math.sin(ang)*s;
  b.trail.length=0;
  S.launch();
  burst(b.x,b.y-4,'#a5f3fc',8,{speed:.5});
}
function loseLife(){
  lives--;
  updateHUD();
  S.lose();
  addShake(9);
  freezeT=Math.max(freezeT,.22);
  flashR=.5;
  burst(W/2,H-90,'#ef4444',16,{speed:1.2});
  if(lives>0){
    pendingServe=.9;
    floatText(W/2,H/2,'BALL LOST','#f87171',true);
  }else{
    gameOver();
  }
}
function gameOver(){
  state='gameover';
  $('over-score').textContent=fmt(score);
  saveBest();
  $('over-best').textContent='BEST '+fmt(best);
  showOv('over');
}
function victory(){
  state='victory';
  $('win-score').textContent=fmt(score);
  saveBest();
  showOv('win');
  S.level();
  const cols=['#f472b6','#a78bfa','#fbbf24','#22d3ee','#4ade80'];
  for(let i=0;i<90;i++){
    parts.push({x:rnd(0,W),y:rnd(-120,H*.3),vx:rnd(-170,170),vy:rnd(140,320),
      life:rnd(.9,1.8),max:1.8,size:rnd(2,4.5),color:cols[i%cols.length],
      shape:Math.random()<.5?'rect':'circle',g:320});
  }
}
function levelClear(){
  const bonus=250*level+100*lives;
  addScore(bonus);
  state='transition';
  transitionT=2.2;
  banner('LEVEL '+level+' CLEAR','+'+bonus+' BONUS',true);
  afterTransition=()=>{
    hideBanner();
    if(level<LEVELS.length) startLevel(level+1);
    else victory();
  };
  S.level();
  addShake(8);
  freezeT=.3; flashW=.45;
  floatText(W/2,H/2-36,'LEVEL CLEAR!','#a5f3fc',true);
}
function saveBest(){
  if(score>best){
    best=score;
    try{ localStorage.setItem(LS_BEST,String(best)); }catch(e){}
  }
  bestEl.textContent=fmt(best);
  menuBest.textContent='BEST '+fmt(best);
}
function addScore(n){
  score+=n;
  scoreEl.textContent=fmt(score);
  scoreEl.classList.remove('pop'); void scoreEl.offsetWidth; scoreEl.classList.add('pop');
}

/* ---------- HUD ---------- */
function updateHUD(){
  scoreEl.textContent=fmt(score);
  levelEl.textContent=level;
  bestEl.textContent=fmt(best);
  livesEl.innerHTML='';
  for(let i=0;i<lives;i++){
    const d=document.createElement('i');
    d.className='dot';
    livesEl.appendChild(d);
  }
}
function updateCombo(){
  if(combo>=2){
    const mult=1+(combo-1)*.25;
    comboEl.textContent='COMBO ×'+mult.toFixed(2);
    comboEl.classList.add('show');
    comboEl.classList.remove('pop'); void comboEl.offsetWidth; comboEl.classList.add('pop');
  }else{
    comboEl.classList.remove('show');
  }
}
function updateChips(){
  for(const id in CHIP_DEFS){
    const d=CHIP_DEFS[id], v=d.get();
    let el=chipEls[id];
    if(v>0){
      if(!el){
        el=document.createElement('div');
        el.className='chip';
        el.style.color=d.color;
        el.innerHTML='<span class="sw"></span><span class="cl">'+d.label+'</span><span class="bar"><i></i></span>';
        pupsEl.appendChild(el);
        chipEls[id]=el;
      }
      el.querySelector('i').style.width=(v/d.max*100).toFixed(1)+'%';
    }else if(el){
      el.remove();
      delete chipEls[id];
    }
  }
}
function banner(b1,b2,good){
  bannerB1.textContent=b1;
  bannerB2.textContent=b2;
  bannerB2.classList.toggle('good',!!good);
  bannerEl.classList.add('show');
}
function hideBanner(){ bannerEl.classList.remove('show'); }

/* ---------- power-ups ---------- */
function pickPU(){
  let r=Math.random(), acc=0;
  for(const p of PU_TYPES){ acc+=p.w; if(r<=acc) return p; }
  return PU_TYPES[0];
}
function maybeDrop(x,y){
  if(Math.random()<.18){
    const p=pickPU();
    falls.push({x,y,vy:135,type:p.id,label:p.label,color:p.color,
                ph:rnd(0,TAU),w:16+p.label.length*7.5,h:22});
  }
}
function applyPU(id){
  if(id==='expand'){ fx.expandT=14; }
  else if(id==='multiball'){
    const add=[];
    for(const b of balls){
      if(b.stuck) continue;
      for(const da of [-.5,.5]){
        if(balls.length+add.length>=9) break;
        const ang=Math.atan2(b.vy,b.vx)+da;
        add.push({x:b.x,y:b.y,vx:Math.cos(ang)*b.speed,vy:Math.sin(ang)*b.speed,
                  r:7,speed:b.speed,stuck:false,trail:[],squashT:0,fire:b.fire});
      }
    }
    balls.push(...add);
    burst(paddle.x,paddle.y,'#f472b6',16);
  }
  else if(id==='slow'){ fx.slowT=10; }
  else if(id==='laser'){ fx.laserT=10; }
  else if(id==='fire'){ fx.fireT=9; for(const b of balls) b.fire=true; }
  else if(id==='life'){ lives=Math.min(6,lives+1); S.life(); updateHUD(); }
  S.power();
}
function shootLaser(){
  const y0=paddle.y-4;
  lasers.push({x:paddle.x-paddle.w*.3,y:y0,vy:-620});
  lasers.push({x:paddle.x+paddle.w*.3,y:y0,vy:-620});
  S.laser();
  addShake(.8);
  burst(paddle.x-paddle.w*.3,y0-6,'#fb7185',3,{speed:.3,up:60});
  burst(paddle.x+paddle.w*.3,y0-6,'#fb7185',3,{speed:.3,up:60});
}

/* ---------- effects helpers ---------- */
function addShake(a){ shakeA=Math.min(14,Math.max(shakeA,a)); }
function burst(x,y,color,n=10,o={}){
  for(let i=0;i<n;i++){
    const a=rnd(0,TAU), sp=rnd(30,300)*(o.speed||1);
    parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-(o.up||0),
      life:rnd(.3,.75),max:.75,size:rnd(1.5,3.5),color,
      shape:Math.random()<.25?'rect':'circle',g:260});
  }
}
function ring(x,y,color,r0=24){
  parts.push({ring:true,x,y,r0,maxR:r0*2.4,life:.45,max:.45,color,vx:0,vy:0,g:0,size:0,shape:'ring'});
}
function spark(x,y,color){ burst(x,y,color||'#94a3b8',3,{speed:.35}); }
function floatText(x,y,str,color,big){
  texts.push({x,y,str,color,life:.9,max:.9,vy:big?-48:-40,size:big?20:13});
}
function brickColors(b){
  if(b.type==='hard')     return {fill:'#3b4657',hi:'#64748b',lo:'#1e293b'};
  if(b.type==='explosive')return {fill:'#ef4444',hi:'#fca5a5',lo:'#7f1d1d'};
  if(b.hp>=3)             return {fill:'#fbbf24',hi:'#fde68a',lo:'#b45309'};
  if(b.hp===2)            return {fill:'#a78bfa',hi:'#ddd6fe',lo:'#6d28d9'};
  return {fill:'#f472b6',hi:'#fbcfe8',lo:'#be185d'};
}

/* ---------- bricks ---------- */
function damageBrick(b,dmg){
  if(!b.alive) return;
  if(b.type==='hard'){
    b.flash=1; S.hard();
    burst(b.x+b.w/2,b.y+b.h/2,'#94a3b8',5,{speed:.5});
    addShake(1.2);
    return;
  }
  b.hp-=dmg;
  b.flash=1;
  const cx=b.x+b.w/2, cy=b.y+b.h/2;
  if(b.hp<=0){
    b.alive=false;
    destructible--;
    const mult=1+Math.max(0,combo-1)*.25;
    const pts=Math.round(b.maxHp*10*mult);
    addScore(pts);
    const col=brickColors(b).fill;
    burst(cx,cy,col,12);
    ring(cx,cy,col,20);
    S.break();
    addShake(2.2);
    if(freezeT<.04) freezeT=.04;
    floatText(cx,cy-8,'+'+pts,col);
    if(combo>1) floatText(cx,cy+10,'×'+mult.toFixed(2),'#e2e8f0');
    if(b.type==='explosive') explode(b);
    maybeDrop(cx,cy);
    if(destructible<=0) levelClear();
  }else{
    S.brick(b.maxHp);
    burst(cx,cy,brickColors(b).hi,4,{speed:.5});
  }
}
function explode(b){
  const cx=b.x+b.w/2, cy=b.y+b.h/2;
  S.explode();
  addShake(7);
  if(freezeT<.07) freezeT=.07;
  burst(cx,cy,'#ef4444',24,{speed:1.5,up:40});
  ring(cx,cy,'#ef4444',34);
  for(const o of bricks){
    if(!o.alive||o===b) continue;
    if(Math.hypot(o.x+o.w/2-cx,o.y+o.h/2-cy)<b.w+o.w) damageBrick(o,1);
  }
}
function hitBricks(ball){
  for(const b of bricks){
    if(!b.alive) continue;
    if(ball.x+ball.r<b.x||ball.x-ball.r>b.x+b.w||
       ball.y+ball.r<b.y||ball.y-ball.r>b.y+b.h) continue;
    const cx=clamp(ball.x,b.x,b.x+b.w), cy=clamp(ball.y,b.y,b.y+b.h);
    const dx=ball.x-cx, dy=ball.y-cy;
    if(dx*dx+dy*dy>ball.r*ball.r) continue;

    if(b.type!=='hard'){ combo++; updateCombo(); }
    if(ball.fire&&b.type!=='hard'){ damageBrick(b,1); return true; }

    const px=ball.prevX??ball.x, py=ball.prevY??ball.y;
    let nx=0, ny=0;
    if(px+ball.r<=b.x) nx=-1;
    else if(px-ball.r>=b.x+b.w) nx=1;
    else if(py+ball.r<=b.y) ny=-1;
    else if(py-ball.r>=b.y+b.h) ny=1;
    else{
      const ox=(ball.r+b.w/2)-Math.abs(ball.x-(b.x+b.w/2));
      const oy=(ball.r+b.h/2)-Math.abs(ball.y-(b.y+b.h/2));
      if(ox<oy) nx=ball.x<b.x+b.w/2?-1:1;
      else ny=ball.y<b.y+b.h/2?-1:1;
    }
    if(nx){ ball.vx=nx*Math.abs(ball.vx); ball.x=nx<0?b.x-ball.r-.1:b.x+b.w+ball.r+.1; }
    else  { ball.vy=ny*Math.abs(ball.vy); ball.y=ny<0?b.y-ball.r-.1:b.y+b.h+ball.r+.1; }
    ball.squashT=.1;
    addShake(1);
    damageBrick(b,1);
    return true;
  }
  return false;
}

/* ---------- update ---------- */
function updatePlay(dt){
  fx.expandT=Math.max(0,fx.expandT-dt);
  fx.slowT=Math.max(0,fx.slowT-dt);
  fx.laserT=Math.max(0,fx.laserT-dt);
  fx.fireT=Math.max(0,fx.fireT-dt);
  if(fx.fireT<=0) for(const b of balls) b.fire=false;

  paddle.w+=((fx.expandT>0?paddle.baseW*1.7:paddle.baseW)-paddle.w)*Math.min(1,dt*9);
  paddle.squash+=(1-paddle.squash)*Math.min(1,dt*14);

  let dir=0;
  if(keys['ArrowLeft']||keys['KeyA']) dir-=1;
  if(keys['ArrowRight']||keys['KeyD']) dir+=1;
  if(dir) paddle.x+=dir*840*dt;
  else if(mouseActive) paddle.x=lerp(paddle.x,mouseX,Math.min(1,dt*24));
  paddle.x=clamp(paddle.x,paddle.w/2+4,W-paddle.w/2-4);

  if(fx.laserT>0){
    paddle.laserCd-=dt;
    if(paddle.laserCd<=0){ shootLaser(); paddle.laserCd=.3; }
  }

  for(const b of bricks){
    if(b.appear>0) b.appear-=dt;
    else if(b.scale<1) b.scale=Math.min(1,b.scale+dt*3.5);
    if(b.flash>0) b.flash-=dt*3.5;
  }

  for(let i=lasers.length-1;i>=0;i--){
    const L=lasers[i];
    L.y+=L.vy*dt;
    let gone=false;
    if(L.y>24){
      for(const b of bricks){
        if(!b.alive) continue;
        if(L.x>b.x-2&&L.x<b.x+b.w+2&&L.y>b.y&&L.y<b.y+b.h){
          damageBrick(b,1);
          burst(L.x,L.y,'#fb7185',4,{speed:.4});
          gone=true;
          break;
        }
      }
    }
    if(gone||L.y<10) lasers.splice(i,1);
  }

  for(let i=falls.length-1;i>=0;i--){
    const f=falls[i];
    f.y+=f.vy*dt;
    f.ph+=dt*3;
    if(f.y>paddle.y-16&&f.y<paddle.y+20&&Math.abs(f.x-paddle.x)<paddle.w/2+16){
      applyPU(f.type);
      burst(f.x,paddle.y-8,f.color,10,{speed:.7,up:30});
      floatText(f.x,paddle.y-24,f.label,f.color,true);
      falls.splice(i,1);
      updateHUD();
    }else if(f.y>H+30) falls.splice(i,1);
  }

  for(let i=balls.length-1;i>=0;i--){
    const b=balls[i];
    if(b.stuck){
      b.x=paddle.x; b.y=paddle.y-paddle.h/2-b.r-1;
      b.trail.length=0;
      continue;
    }
    const eff=b.speed*(fx.slowT>0?.72:1);
    const sp=Math.hypot(b.vx,b.vy)||eff;
    b.vx*=eff/sp; b.vy*=eff/sp;
    if(b.squashT>0) b.squashT-=dt;

    const steps=clamp(Math.ceil(eff*dt/(b.r*1.4)),1,4);
    let dead=false;
    for(let s=0;s<steps;s++){
      const sdt=dt/steps;
      b.prevX=b.x; b.prevY=b.y;
      b.x+=b.vx*sdt; b.y+=b.vy*sdt;

      if(b.x<b.r){ b.x=b.r; b.vx=Math.abs(b.vx); S.wall(); spark(b.x,b.y); }
      else if(b.x>W-b.r){ b.x=W-b.r; b.vx=-Math.abs(b.vx); S.wall(); spark(b.x,b.y); }
      if(b.y<b.r){ b.y=b.r; b.vy=Math.abs(b.vy); S.wall(); spark(b.x,b.y); }
      if(b.y>H+20){ dead=true; break; }

      if(b.vy>0&&b.y+b.r>=paddle.y-1&&b.y+b.r<=paddle.y+paddle.h+14&&
         Math.abs(b.x-paddle.x)<=paddle.w/2+b.r*.6){
        const rel=clamp((b.x-paddle.x)/(paddle.w/2),-1,1);
        let ang=rel*1.05;
        if(Math.abs(ang)<.22) ang=.22*(rel<0?-1:1);
        b.speed=Math.min(b.speed*1.018,speedCap);
        const ns=b.speed*(fx.slowT>0?.72:1);
        b.vx=Math.cos(ang)*ns; b.vy=Math.sin(ang)*ns;
        b.y=paddle.y-b.r-1;
        b.squashT=.12; paddle.squash=.74;
        combo=0; updateCombo();
        S.paddle();
        burst(b.x,paddle.y-4,'#22d3ee',6,{speed:.5,up:50});
        addShake(1.4);
      }
      hitBricks(b);
    }

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

    if(b.fire&&Math.random()<.6){
      parts.push({x:b.x+rnd(-3,3),y:b.y+rnd(-3,3),vx:rnd(-25,25),vy:rnd(40,110),
        life:.35,max:.35,size:rnd(1.5,3),color:Math.random()<.5?'#fb923c':'#fbbf24',
        shape:'circle',g:-60});
    }
    if(dead) balls.splice(i,1);
  }

  if(balls.length===0&&pendingServe<=0) loseLife();
  else if(pendingServe>0){
    pendingServe-=dt;
    if(pendingServe<=0) spawnStuckBall();
  }
}

function update(dt){
  if(freezeT>0){ freezeT-=dt; return; }   // hit-stop: world pauses for a few ms
  t+=dt;
  if(flashR>0) flashR=Math.max(0,flashR-dt*1.4);
  if(flashW>0) flashW=Math.max(0,flashW-dt*1.6);
  shakeA=Math.max(0,shakeA-dt*10);
  if(shakeA<.05) shakeA=0;

  for(let i=parts.length-1;i>=0;i--){
    const p=parts[i];
    p.life-=dt;
    if(p.life<=0){ parts.splice(i,1); continue; }
    p.vy+=(p.g||0)*dt;
    p.x+=p.vx*dt; p.y+=p.vy*dt;
  }
  for(let i=texts.length-1;i>=0;i--){
    const f=texts[i];
    f.life-=dt; f.y+=f.vy*dt;
    if(f.life<=0) texts.splice(i,1);
  }
  updateChips();

  if(state==='playing'||state==='transition'){
    updatePlay(dt);
    if(state==='transition'){
      transitionT-=dt;
      if(transitionT<=0&&afterTransition){
        const fn=afterTransition; afterTransition=null; fn();
      }
    }
  }
}

/* ---------- render ---------- */
function roundRect(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();
}
const stars=Array.from({length:80},()=>({x:Math.random()*W,y:Math.random()*H,
  z:rnd(.25,1),tw:rnd(0,TAU),sp:rnd(.4,1.8),vy:rnd(3,12)}));
function drawStars(dt){
  ctx.fillStyle='#bfe9ff';
  for(const s of stars){
    s.tw+=dt*s.sp; s.y+=dt*s.vy*s.z;
    if(s.y>H+4){ s.y=-4; s.x=Math.random()*W; }
    ctx.globalAlpha=(.15+.55*(.5+.5*Math.sin(s.tw)))*s.z;
    ctx.beginPath(); ctx.arc(s.x,s.y,s.z*1.3,0,TAU); ctx.fill();
  }
  ctx.globalAlpha=1;
}
function drawBricks(){
  for(const b of bricks){
    if(b.scale<=.01) continue;
    const sc=easeOutBack(b.scale);
    ctx.save();
    const cx=b.x+b.w/2, cy=b.y+b.h/2;
    ctx.translate(cx,cy); ctx.scale(sc,sc); ctx.translate(-cx,-cy);
    const col=brickColors(b);
    ctx.shadowColor=col.fill; ctx.shadowBlur=10*b.scale;
    roundRect(b.x,b.y,b.w,b.h,5);
    ctx.fillStyle=col.fill; ctx.fill();
    ctx.shadowBlur=0;
    ctx.strokeStyle='rgba(0,0,0,.35)'; ctx.lineWidth=1; ctx.stroke();
    ctx.fillStyle=col.hi;
    roundRect(b.x+2.5,b.y+2.5,b.w-5,5,2.5); ctx.fill();
    ctx.fillStyle=col.lo;
    ctx.fillRect(b.x+2.5,b.y+b.h-6,b.w-5,3.5);
    if(b.type==='hard'){
      ctx.fillStyle='rgba(15,23,42,.55)';
      ctx.beginPath(); ctx.arc(b.x+7,b.y+b.h/2,2.2,0,TAU); ctx.fill();
      ctx.beginPath(); ctx.arc(b.x+b.w-7,b.y+b.h/2,2.2,0,TAU); ctx.fill();
    }
    if(b.type==='explosive'){
      const k=.5+.5*Math.sin(t*7+b.pulse);
      ctx.fillStyle='rgba(255,255,255,'+(.35+.5*k)+')';
      ctx.beginPath(); ctx.arc(cx,cy,4.5,0,TAU); ctx.fill();
    }
    if(b.flash>0){
      ctx.fillStyle='rgba(255,255,255,'+Math.min(1,b.flash*1.6)+')';
      roundRect(b.x,b.y,b.w,b.h,5); ctx.fill();
    }
    ctx.restore();
  }
}
function drawFalls(){
  for(const f of falls){
    const y=f.y+Math.sin(f.ph)*3;
    ctx.save();
    ctx.translate(f.x,y);
    ctx.shadowColor=f.color; ctx.shadowBlur=12;
    roundRect(-f.w/2,-f.h/2,f.w,f.h,6);
    ctx.fillStyle='rgba(10,14,30,.92)'; ctx.fill();
    ctx.strokeStyle=f.color; ctx.lineWidth=1.5; ctx.stroke();
    ctx.shadowBlur=0;
    ctx.fillStyle='rgba(255,255,255,.25)';
    ctx.fillRect(-f.w/2+3,-f.h/2+2.5,f.w-6,3);
    ctx.fillStyle=f.color;
    ctx.font='700 11px ui-monospace, Menlo, monospace';
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.fillText(f.label,0,1);
    ctx.restore();
  }
}
function drawLasers(){
  ctx.save();
  ctx.shadowColor='#fb7185'; ctx.shadowBlur=8;
  ctx.fillStyle='#fecdd3';
  for(const L of lasers){ roundRect(L.x-2,L.y-14,4,16,2); ctx.fill(); }
  ctx.restore();
}
function drawPaddle(){
  ctx.save();
  const w=paddle.w, h=paddle.h, x=paddle.x-w/2, y=paddle.y;
  ctx.translate(paddle.x,y+h/2);
  ctx.scale(1,paddle.squash);
  ctx.translate(-paddle.x,-(y+h/2));
  ctx.shadowColor='rgba(34,211,238,.75)'; ctx.shadowBlur=16;
  ctx.fillStyle='#22d3ee';
  roundRect(x,y,w,h,h/2); ctx.fill();
  ctx.shadowBlur=0;
  ctx.fillStyle='rgba(255,255,255,.75)';
  ctx.fillRect(x+4,y+2,w-8,3);
  ctx.fillStyle='rgba(8,20,30,.35)';
  for(let gx=x+14;gx<x+w-10;gx+=14) ctx.fillRect(gx,y+h-5,6,3);
  if(fx.laserT>0){
    ctx.fillStyle='#fb7185';
    ctx.shadowColor='#fb7185'; ctx.shadowBlur=10;
    ctx.fillRect(x+5,y-6,8,8);
    ctx.fillRect(x+w-13,y-6,8,8);
    ctx.shadowBlur=0;
  }
  ctx.restore();
  ctx.strokeStyle='rgba(34,211,238,.25)'; ctx.lineWidth=2;
  ctx.beginPath(); ctx.moveTo(x+6,y+h+3); ctx.lineTo(x+w-6,y+h+3); ctx.stroke();
}
function drawBalls(){
  for(const b of balls){
    for(let i=0;i<b.trail.length;i++){
      const p=b.trail[i], k=(i+1)/b.trail.length;
      ctx.globalAlpha=.08+.3*k;
      ctx.fillStyle=b.fire?'#fb923c':'#67e8f9';
      ctx.beginPath(); ctx.arc(p.x,p.y,b.r*(.25+.65*k),0,TAU); ctx.fill();
    }
    ctx.globalAlpha=1;
    const sp=Math.hypot(b.vx,b.vy);
    let stretch=Math.min(.45,sp/2600);
    if(b.squashT>0) stretch+=.3*(b.squashT/.12);
    const pr=b.stuck?1+Math.sin(t*5)*.1:1;
    ctx.save();
    ctx.translate(b.x,b.y);
    ctx.rotate(Math.atan2(b.vy,b.vx));
    ctx.shadowColor=b.fire?'rgba(251,146,60,.9)':'rgba(103,232,249,.9)';
    ctx.shadowBlur=14;
    ctx.fillStyle=b.fire?'#ffedd5':'#f0feff';
    ctx.beginPath();
    ctx.ellipse(0,0,b.r*(1+stretch)*pr,b.r*(1-stretch*.55)*pr,0,0,TAU);
    ctx.fill();
    ctx.shadowBlur=0;
    ctx.fillStyle=b.fire?'#fb923c':'#ffffff';
    ctx.beginPath(); ctx.arc(0,0,b.r*.45,0,TAU); ctx.fill();
    ctx.restore();
  }
}
function drawParticles(){
  for(const p of parts){
    const k=p.life/p.max;
    if(p.ring){
      ctx.globalAlpha=k*.8;
      ctx.strokeStyle=p.color;
      ctx.lineWidth=2.5*(1-k)+.5;
      ctx.beginPath();
      ctx.arc(p.x,p.y,lerp(p.r0,p.maxR,1-k),0,TAU);
      ctx.stroke();
    }else{
      ctx.globalAlpha=k;
      ctx.fillStyle=p.color;
      if(p.shape==='rect') ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);
      else{
        ctx.beginPath();
        ctx.arc(p.x,p.y,p.size*(.5+.5*k),0,TAU);
        ctx.fill();
      }
    }
  }
  ctx.globalAlpha=1;
}
function drawTexts(){
  ctx.textAlign='center'; ctx.textBaseline='middle';
  for(const f of texts){
    ctx.globalAlpha=f.life/f.max;
    ctx.shadowColor=f.color; ctx.shadowBlur=10;
    ctx.fillStyle=f.color;
    ctx.font='800 '+f.size+'px ui-monospace, Menlo, monospace';
    ctx.fillText(f.str,f.x,f.y);
    ctx.shadowBlur=0;
  }
  ctx.globalAlpha=1;
}
function drawHint(){
  ctx.save();
  ctx.globalAlpha=.55+.45*Math.sin(t*4);
  ctx.fillStyle='#a5f3fc';
  ctx.shadowColor='rgba(34,211,238,.8)'; ctx.shadowBlur=10;
  ctx.font='700 13px ui-monospace, Menlo, monospace';
  ctx.textAlign='center'; ctx.textBaseline='middle';
  ctx.fillText('SPACE / CLICK TO LAUNCH',W/2,paddle.y-34);
  ctx.restore();
}
function drawVignette(){
  const g=ctx.createRadialGradient(W/2,H*.42,H*.35,W/2,H*.55,H*1.05);
  g.addColorStop(0,'rgba(0,0,0,0)');
  g.addColorStop(1,'rgba(2,3,10,.5)');
  ctx.fillStyle=g;
  ctx.fillRect(0,0,W,H);
}
function render(dt){
  ctx.fillStyle='#070812';
  ctx.fillRect(0,0,W,H);
  drawStars(dt);

  ctx.save();
  if(shakeA>.05) ctx.translate(rnd(-shakeA,shakeA),rnd(-shakeA,shakeA));
  drawBricks();
  drawFalls();
  drawLasers();
  drawPaddle();
  drawBalls();
  drawParticles();
  drawTexts();
  if((state==='playing'||state==='transition')&&balls.some(b=>b.stuck)) drawHint();
  ctx.restore();

  drawVignette();
  if(flashR>0){ ctx.fillStyle='rgba(239,68,68,'+flashR+')'; ctx.fillRect(0,0,W,H); }
  if(flashW>0){ ctx.fillStyle='rgba(224,255,255,'+flashW+')'; ctx.fillRect(0,0,W,H); }
}

/* ---------- main loop ---------- */
let last=performance.now();
requestAnimationFrame(function frame(now){
  let dt=(now-last)/1000;
  last=now;
  dt=Math.min(dt,.033);
  update(dt);
  render(dt);
  requestAnimationFrame(frame);
});

/* ---------- init ---------- */
bestEl.textContent=fmt(best);
menuBest.textContent='BEST '+fmt(best);
muteBtn.textContent=muted?'SOUND OFF':'SOUND ON';
updateHUD();
showOv('menu');
})();
</script>
</body>
</html>

Compare with

Mistral Large 4: other attempts