Run details
- Model
- fireworks/ember-1
- Route
- OpenRouter API, one user message
- Served by
- Fireworks
- Effort
- max, OpenRouter's default for this model
- Limits
- 60 min · 40,000 output tokens (our $0.60 limit per run at $15/M)
- Date
- 23 Oct 2026
- Time
- 6 min 30 s
- Size
- 22 kB
- Tokens
- 165 in, 30,989 out (22,367 of them thinking)
- List price
- $0.47 at today's price ($3 in / $15 out per M)
- Paid
- $0.47
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
22 kB 325 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Neon Breakout</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{background:#05070d;display:flex;align-items:center;justify-content:center;min-height:100vh;overflow:hidden;font-family:system-ui,sans-serif}
canvas{width:min(96vw,calc(96vh*4/3));aspect-ratio:4/3;display:block;border-radius:12px;background:#0b0e1a;cursor:pointer;touch-action:none;
box-shadow:0 0 60px rgba(56,182,255,.15),0 0 120px rgba(255,77,109,.08)}
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
(()=>{ 'use strict';
const canvas=document.getElementById('game'),ctx=canvas.getContext('2d');
const W=800,H=600,clamp=(v,a,b)=>v<a?a:v>b?b:v;
function fit(){const d=Math.min(devicePixelRatio||1,2);canvas.width=W*d;canvas.height=H*d;ctx.setTransform(d,0,0,d,0,0);}
fit();addEventListener('resize',fit);
const store={get(k,d){try{return localStorage.getItem(k)??d}catch(e){return d}},set(k,v){try{localStorage.setItem(k,v)}catch(e){}}};
/* ---------- audio (all synthesized, no files) ---------- */
let AC=null,master=null,nbuf=null;
function initAudio(){if(AC)return;try{AC=new(window.AudioContext||window.webkitAudioContext)();master=AC.createGain();master.gain.value=.5;master.connect(AC.destination);}catch(e){}}
function tone(f0,f1,dur,type='square',vol=.12,delay=0){if(!AC||G.muted)return;const t=AC.currentTime+delay,o=AC.createOscillator(),g=AC.createGain();
o.type=type;o.frequency.setValueAtTime(Math.max(f0,1),t);if(f1&&f1!==f0)o.frequency.exponentialRampToValueAtTime(Math.max(f1,1),t+dur);
g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(.0001,t+dur);o.connect(g);g.connect(master);o.start(t);o.stop(t+dur+.05);}
function noise(dur=.15,vol=.12,freq=1200,delay=0){if(!AC||G.muted)return;if(!nbuf){nbuf=AC.createBuffer(1,AC.sampleRate*.5,AC.sampleRate);const d=nbuf.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1;}
const t=AC.currentTime+delay,s=AC.createBufferSource();s.buffer=nbuf;s.loop=true;const f=AC.createBiquadFilter();f.type='bandpass';f.frequency.value=freq;f.Q.value=.8;
const g=AC.createGain();g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(.0001,t+dur);s.connect(f);f.connect(g);g.connect(master);s.start(t);s.stop(t+dur+.05);}
const sfx={
paddle(){tone(200,150,.08,'square',.14);noise(.06,.05,800);},
wall(){tone(140,110,.05,'square',.08);},
brick(c){tone(260+Math.min(c,16)*28,220+c*18,.07,'square',.12);noise(.09,.07,1600);},
hard(){tone(160,120,.08,'square',.1);noise(.06,.08,500);},
launch(){tone(280,520,.12,'sine',.14);},
power(){tone(440,880,.14,'sine',.15);tone(660,1320,.14,'sine',.1,.07);},
life(){tone(520,1040,.2,'sine',.16);tone(780,1560,.2,'sine',.1,.1);},
lose(){tone(320,60,.55,'sawtooth',.18);noise(.4,.1,300);},
clear(){[523,659,784,1046].forEach((f,i)=>tone(f,f,.12,'square',.12,i*.09));},
over(){[392,311,262,196].forEach((f,i)=>tone(f,f*.9,.22,'sawtooth',.14,i*.18));},
win(){[523,659,784,1046,1319,1568].forEach((f,i)=>tone(f,f,.14,'square',.12,i*.1));},
fire(){noise(.25,.12,600);tone(150,400,.2,'sawtooth',.08);}
};
/* ---------- constants & levels ---------- */
const PADDLE_Y=H-48,PADDLE_H=12,BASE_PW=92,BALL_R=7;
const COLS=12,MARGIN=34,GAP=5,BRICK_TOP=84,BRICK_H=22;
const BRICK_W=(W-MARGIN*2-GAP*(COLS-1))/COLS;
const COLORS={
r:{c:'#ff4d6d',hp:1,p:50},o:{c:'#ff9f43',hp:1,p:60},y:{c:'#ffd93b',hp:1,p:70},
g:{c:'#3ddc84',hp:1,p:80},b:{c:'#38b6ff',hp:1,p:90},p:{c:'#b06bff',hp:1,p:100},
s:{c:'#dfe6ee',hp:2,p:150},m:{c:'#8d99ae',hp:3,p:250},X:{c:'#2e3445',hp:1/0,p:0}};
const LEVELS=[
{name:'WARM UP',rows:['............','rrrrrrrrrrrr','oooooooooooo','yyyyyyyyyyyy','gggggggggggg','bbbbbbbbbbbb']},
{name:'PYRAMID',rows:['....pppp....','...pbbbbp...','..pbggggbp..','.pbgyrrygbp.','pbgyroorygbp']},
{name:'CHECKERS',rows:['r.r.r.r.r.r.','.o.o.o.o.o.o','y.y.y.y.y.y.','.g.g.g.g.g.g','b.b.b.b.b.b.','.p.p.p.p.p.p']},
{name:'BUNKER',rows:['XXXXXXXXXXXX','XssssssssssX','Xs........sX','Xs.pppppp.sX','Xs.pmmmm.p.X','Xs.pmmmm.p.X','Xs.pppppp.sX','Xs........sX','XssssssssssX']},
{name:'RAINBOW',rows:['ssssssssssss','pppppppppppp','bbbbbbbbbbbb','gggggggggggg','yyyyyyyyyyyy','oooooooooooo','rrrrrrrrrrrr']},
{name:'GAUNTLET',rows:['sXppppppppXs','bXbbbbbbbbXb','gXggggggggXg','yXyyyyyyyyXy','oXooooooooXo','rXrrrrrrrrXr','sXssssssssXs']}];
const POWT={E:{c:'#3ddc84',n:'EXPAND'},M:{c:'#38b6ff',n:'MULTIBALL'},S:{c:'#ffd93b',n:'SLOW-MO'},F:{c:'#ff6b35',n:'FIREBALL'},L:{c:'#ff4d6d',n:'+1 LIFE'}};
/* ---------- state ---------- */
const G={state:'menu',time:0,stateT:0,score:0,lives:3,level:0,cycle:0,bonus:0,combo:0,bestCombo:0,
best:+store.get('nb_high',0)||0,shake:0,flashA:0,flashC:'#fff',
balls:[],bricks:[],powerups:[],particles:[],texts:[],rings:[],
effects:{expand:0,slow:0,fire:0},breakable:0,destroyed:0,
muted:store.get('nb_mute','0')==='1',pausedFrom:'play'};
const P={x:W/2,tx:W/2,w:BASE_PW,targetW:BASE_PW,vel:0,tilt:0,flash:0,prevX:W/2};
const keys={};
const stars=Array.from({length:70},()=>({x:Math.random()*W,y:Math.random()*H,z:.3+Math.random()*.7}));
/* ---------- helpers ---------- */
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 text(str,x,y,size,color,align='center',glow=0){ctx.font='700 '+size+'px "Segoe UI",system-ui,-apple-system,sans-serif';ctx.textAlign=align;ctx.textBaseline='middle';if(glow){ctx.shadowColor=color;ctx.shadowBlur=glow;}ctx.fillStyle=color;ctx.fillText(str,x,y);ctx.shadowBlur=0;}
function dim(a){ctx.fillStyle='rgba(4,6,14,'+a+')';ctx.fillRect(0,0,W,H);}
function shake(a){G.shake=Math.min(G.shake+a,16);}
function addText(x,y,str,color,size=16){G.texts.push({x,y,str,color,size,life:1});if(G.texts.length>40)G.texts.shift();}
function ring(x,y,color){G.rings.push({x,y,r:6,vr:320,life:.28,color});}
function explode(br){for(let i=0;i<12;i++){const a=Math.random()*Math.PI*2,sp=60+Math.random()*220;
G.particles.push({x:br.x+br.w/2,y:br.y+br.h/2,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-60,w:3+Math.random()*6,h:2+Math.random()*4,rot:Math.random()*6,vr:(Math.random()-.5)*12,color:br.color,life:.5+Math.random()*.5,grav:500});}}
function confetti(n){const cols=['#ff4d6d','#ff9f43','#ffd93b','#3ddc84','#38b6ff','#b06bff'];
for(let i=0;i<n;i++)G.particles.push({x:W/2+(Math.random()-.5)*300,y:H/2-80,vx:(Math.random()-.5)*420,vy:-Math.random()*380-60,w:4+Math.random()*4,h:3+Math.random()*3,rot:Math.random()*6,vr:(Math.random()-.5)*14,color:cols[(Math.random()*cols.length)|0],life:1+Math.random()*.6,grav:520});}
/* ---------- game logic ---------- */
function speed(){return Math.min(360+G.level*22+G.cycle*45+G.bonus,640)*(G.effects.slow>0?.62:1);}
function normAll(){const sp=speed();for(const b of G.balls){const m=Math.hypot(b.vx,b.vy)||1;b.vx=b.vx/m*sp;b.vy=b.vy/m*sp;}}
function serveBall(){return{x:P.x,y:PADDLE_Y-BALL_R-2,vx:0,vy:0,r:BALL_R,trail:[]};}
function buildLevel(i){G.bricks=[];G.breakable=0;G.destroyed=0;
LEVELS[i].rows.forEach((row,ri)=>{for(let ci=0;ci<COLS;ci++){const ch=row[ci];if(ch==='.')continue;const d=COLORS[ch];
G.bricks.push({x:MARGIN+ci*(BRICK_W+GAP),y:BRICK_TOP+ri*(BRICK_H+GAP),w:BRICK_W,h:BRICK_H,hp:d.hp,maxHp:d.hp,color:d.c,pts:d.p,unb:ch==='X',flash:0,alive:true});
if(ch!=='X')G.breakable++;}});}
function startGame(){G.score=0;G.lives=3;G.level=0;G.cycle=0;G.bonus=0;G.combo=0;G.bestCombo=0;
G.effects.expand=G.effects.slow=G.effects.fire=0;P.targetW=BASE_PW;P.w=BASE_PW;
G.powerups.length=G.particles.length=G.texts.length=G.rings.length=0;
buildLevel(0);G.balls=[serveBall()];G.state='intro';G.stateT=0;}
function launch(){if(!G.balls.length)G.balls.push(serveBall());
const sp=speed(),a=-Math.PI/2+(Math.random()*.4-.2),b=G.balls[0];
b.vx=Math.cos(a)*sp;b.vy=Math.sin(a)*sp;G.state='play';sfx.launch();}
function loseLife(){G.lives--;G.combo=0;G.effects.expand=G.effects.slow=G.effects.fire=0;P.targetW=BASE_PW;G.powerups.length=0;
sfx.lose();shake(14);G.flashA=.4;G.flashC='#ff3355';
if(G.lives<0){G.state='gameover';G.stateT=0;store.set('nb_high',G.best);sfx.over();}
else{G.state='serve';G.balls=[serveBall()];}}
function levelClear(){G.state='levelclear';G.stateT=0;G.score+=250;
if(G.score>G.best){G.best=G.score;store.set('nb_high',G.best);}
addText(W/2,H/2-30,'LEVEL BONUS +250','#ffd93b',26);sfx.clear();confetti(90);G.flashA=.25;G.flashC='#ffffff';}
function nextLevel(){G.level++;G.bonus=0;G.effects.expand=G.effects.slow=G.effects.fire=0;P.targetW=BASE_PW;G.powerups.length=0;
if(G.level>=LEVELS.length){G.state='win';G.stateT=0;sfx.win();confetti(160);store.set('nb_high',G.best);return;}
buildLevel(G.level);G.balls=[serveBall()];G.state='intro';G.stateT=0;}
function continueNG(){G.cycle++;G.level=0;G.bonus=0;buildLevel(0);G.balls=[serveBall()];G.state='intro';G.stateT=0;}
function damageBrick(br,dmg){br.flash=.12;
if(br.unb){sfx.hard();shake(2);return;}
br.hp-=dmg;
if(br.hp>0){sfx.hard();shake(1.5);
for(let i=0;i<4;i++)G.particles.push({x:br.x+br.w/2,y:br.y+br.h/2,vx:(Math.random()-.5)*160,vy:(Math.random()-.5)*160,w:3,h:2,rot:0,vr:0,color:br.color,life:.3,grav:300});return;}
br.alive=false;G.destroyed++;G.combo++;G.bestCombo=Math.max(G.bestCombo,G.combo);
const gain=Math.round(br.pts*(1+Math.min(G.combo,20)*.1));G.score+=gain;
if(G.score>G.best){G.best=G.score;store.set('nb_high',G.best);}
addText(br.x+br.w/2,br.y+br.h/2,'+'+gain,br.color,15);
explode(br);ring(br.x+br.w/2,br.y+br.h/2,br.color);sfx.brick(G.combo);shake(G.effects.fire>0?5:3);
G.bonus=Math.min(G.bonus+1.5,140);normAll();
if(Math.random()<.16){const r=Math.random(),type=r<.28?'E':r<.52?'M':r<.72?'S':r<.88?'F':'L';
G.powerups.push({x:br.x+br.w/2,y:br.y+br.h/2,vy:150,t:Math.random()*6,type});}
if(G.destroyed>=G.breakable)levelClear();}
function multiball(){const sp=speed(),src=G.balls.slice();
for(const b of src){const a=Math.atan2(b.vy,b.vx);
for(const d of[-.45,.45]){if(G.balls.length>=8)return;
G.balls.push({x:b.x,y:b.y,vx:Math.cos(a+d)*sp,vy:Math.sin(a+d)*sp,r:BALL_R,trail:[]});}}}
function collect(pu){addText(P.x,PADDLE_Y-24,POWT[pu.type].n,POWT[pu.type].c,18);shake(2);
if(pu.type==='E'){G.effects.expand=12;P.targetW=150;sfx.power();}
else if(pu.type==='M'){multiball();sfx.power();}
else if(pu.type==='S'){G.effects.slow=8;normAll();sfx.power();}
else if(pu.type==='F'){G.effects.fire=6;sfx.fire();}
else{G.lives=Math.min(5,G.lives+1);sfx.life();}}
/* ---------- update ---------- */
function updatePaddle(dt){
if(keys['arrowleft']||keys['a'])P.tx-=640*dt;
if(keys['arrowright']||keys['d'])P.tx+=640*dt;
P.tx=clamp(P.tx,P.w/2+6,W-P.w/2-6);
P.prevX=P.x;P.x+=(P.tx-P.x)*Math.min(1,dt*22);
const raw=(P.x-P.prevX)/Math.max(dt,1e-4);P.vel+=(raw-P.vel)*.35;
P.tilt=clamp(P.vel*.00045,-.16,.16);
P.w+=(P.targetW-P.w)*Math.min(1,dt*8);
P.flash=Math.max(0,P.flash-dt);}
function updateBalls(dt){const sp=speed();
for(const b of G.balls){
if(G.effects.fire>0&&Math.random()<.5)G.particles.push({x:b.x,y:b.y,vx:(Math.random()-.5)*40,vy:(Math.random()-.5)*40,w:3,h:3,rot:0,vr:0,color:'#ff9f43',life:.3,grav:0});
const steps=Math.max(1,Math.ceil(Math.hypot(b.vx,b.vy)*dt/8)),sdt=dt/steps;
for(let s=0;s<steps;s++){
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);sfx.wall();}
else if(b.x>W-b.r){b.x=W-b.r;b.vx=-Math.abs(b.vx);sfx.wall();}
if(b.y<b.r){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&&b.x>=P.x-P.w/2-b.r&&b.x<=P.x+P.w/2+b.r){
b.y=PADDLE_Y-b.r-.5;
const hit=clamp((b.x-P.x)/(P.w/2),-1,1),ang=hit*1.15;
b.vx=Math.sin(ang)*sp+P.vel*.15;b.vy=-Math.cos(ang)*sp;
const m=Math.hypot(b.vx,b.vy);b.vx=b.vx/m*sp;b.vy=b.vy/m*sp;
P.flash=.12;G.combo=0;sfx.paddle();shake(1.5);
for(let i=0;i<6;i++)G.particles.push({x:b.x,y:PADDLE_Y,vx:(Math.random()-.5)*200,vy:-Math.random()*160,w:3,h:2,rot:0,vr:0,color:'#7ee7ff',life:.35,grav:600});}
for(const br of G.bricks){
if(!br.alive)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)continue;
const fire=G.effects.fire>0;
if(!(fire&&!br.unb)){
const dx=b.x-(br.x+br.w/2),dy=b.y-(br.y+br.h/2);
const px=br.w/2+b.r-Math.abs(dx),py=br.h/2+b.r-Math.abs(dy);
if(px<py){b.vx=dx>0?Math.abs(b.vx):-Math.abs(b.vx);b.x+=dx>0?px:-px;}
else{b.vy=dy>0?Math.abs(b.vy):-Math.abs(b.vy);b.y+=dy>0?py:-py;}}
damageBrick(br,fire&&!br.unb?br.hp:1);break;}}
b.trail.push({x:b.x,y:b.y});if(b.trail.length>13)b.trail.shift();}
const had=G.balls.length;
G.balls=G.balls.filter(b=>b.y-b.r<H+40);
if(had>0&&G.balls.length===0&&G.state==='play')loseLife();}
function updatePowerups(dt){
for(const pu of G.powerups){pu.t+=dt;pu.y+=pu.vy*dt;pu.x+=Math.sin(pu.t*4)*.4;
if(pu.y+8>=PADDLE_Y&&pu.y-8<=PADDLE_Y+PADDLE_H+6&&Math.abs(pu.x-P.x)<P.w/2+14){pu.dead=true;collect(pu);}
if(pu.y>H+20)pu.dead=true;}
G.powerups=G.powerups.filter(p=>!p.dead);}
function updateFx(dt){
G.shake*=Math.exp(-5.5*dt);if(G.shake<.05)G.shake=0;
G.flashA=Math.max(0,G.flashA-dt*1.2);
for(const s of stars){s.y+=s.z*24*dt;if(s.y>H){s.y=-2;s.x=Math.random()*W;}}
for(const p of G.particles){p.x+=p.vx*dt;p.y+=p.vy*dt;p.vy+=(p.grav||0)*dt;p.rot+=(p.vr||0)*dt;p.life-=dt;}
G.particles=G.particles.filter(p=>p.life>0);
if(G.particles.length>400)G.particles.splice(0,G.particles.length-400);
for(const r of G.rings){r.r+=r.vr*dt;r.life-=dt;}
G.rings=G.rings.filter(r=>r.life>0);
for(const t of G.texts){t.y-=36*dt;t.life-=dt;}
G.texts=G.texts.filter(t=>t.life>0);
for(const b of G.bricks)if(b.flash>0)b.flash-=dt;}
function update(dt){G.time+=dt;
if(G.state==='paused')return;
updateFx(dt);
if(G.state==='menu'||G.state==='gameover'||G.state==='win')return;
updatePaddle(dt);
if(G.state==='intro'){G.stateT+=dt;if(G.stateT>1.5){G.state='serve';G.stateT=0;}return;}
if(G.state==='serve'){const b=G.balls[0];if(b){b.x=P.x;b.y=PADDLE_Y-BALL_R-2;}return;}
if(G.state==='levelclear'){G.stateT+=dt;if(G.stateT>2.2)nextLevel();return;}
if(G.state==='play'){
for(const k of['expand','slow','fire'])if(G.effects[k]>0){G.effects[k]-=dt;
if(G.effects[k]<=0){G.effects[k]=0;if(k==='expand')P.targetW=BASE_PW;if(k==='slow')normAll();}}
updateBalls(dt);updatePowerups(dt);}}
/* ---------- render ---------- */
function drawBrick(b){const{x,y,w,h}=b;
ctx.fillStyle=b.color;rr(x,y,w,h,4);ctx.fill();
ctx.fillStyle='rgba(255,255,255,.22)';ctx.fillRect(x+2,y+1.5,w-4,3);
if(b.unb){ctx.strokeStyle='rgba(255,255,255,.12)';ctx.lineWidth=1;ctx.strokeRect(x+3.5,y+3.5,w-7,h-7);}
else if(b.hp<b.maxHp){ctx.fillStyle='rgba(0,0,0,.35)';rr(x,y,w,h,4);ctx.fill();}
if(b.flash>0){ctx.fillStyle='rgba(255,255,255,'+(b.flash/.12*.7)+')';rr(x,y,w,h,4);ctx.fill();}}
function drawPaddle(){ctx.save();ctx.translate(P.x,PADDLE_Y+PADDLE_H/2);ctx.rotate(P.tilt);
const g=ctx.createLinearGradient(0,-8,0,8);g.addColorStop(0,'#aef3ff');g.addColorStop(.5,'#38b6ff');g.addColorStop(1,'#1d6fd1');
ctx.shadowColor='#38b6ff';ctx.shadowBlur=16;ctx.fillStyle=g;rr(-P.w/2,-PADDLE_H/2,P.w,PADDLE_H,6);ctx.fill();ctx.shadowBlur=0;
if(P.flash>0){ctx.fillStyle='rgba(255,255,255,'+(P.flash/.12*.6)+')';rr(-P.w/2,-PADDLE_H/2,P.w,PADDLE_H,6);ctx.fill();}
ctx.restore();}
function drawBalls(){const fire=G.effects.fire>0;
for(const b of G.balls){
for(let i=0;i<b.trail.length;i++){const t=b.trail[i];
ctx.globalAlpha=(i/b.trail.length)*.3;ctx.fillStyle=fire?'#ff9f43':'#7ee7ff';
ctx.beginPath();ctx.arc(t.x,t.y,b.r*(.4+.6*i/b.trail.length),0,7);ctx.fill();}
ctx.globalAlpha=1;ctx.shadowColor=fire?'#ff6b35':'#7ee7ff';ctx.shadowBlur=18;
ctx.fillStyle=fire?'#ffd0a8':'#eafcff';ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,7);ctx.fill();ctx.shadowBlur=0;}}
function drawPowerups(){for(const pu of G.powerups){const d=POWT[pu.type];
ctx.save();ctx.translate(pu.x,pu.y);ctx.shadowColor=d.c;ctx.shadowBlur=12;
ctx.fillStyle=d.c;rr(-14,-9,28,18,9);ctx.fill();ctx.shadowBlur=0;
text(pu.type==='L'?'+1':pu.type,0,1,12,'#0b0e1a');ctx.restore();}}
function drawParticles(){for(const p of G.particles){ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.rot);
ctx.globalAlpha=clamp(p.life*2,0,1);ctx.fillStyle=p.color;ctx.fillRect(-p.w/2,-p.h/2,p.w,p.h);ctx.restore();}ctx.globalAlpha=1;}
function drawRings(){for(const r of G.rings){ctx.globalAlpha=clamp(r.life/.28*.6,0,1);
ctx.strokeStyle=r.color;ctx.lineWidth=2;ctx.beginPath();ctx.arc(r.x,r.y,r.r,0,7);ctx.stroke();}ctx.globalAlpha=1;}
function drawTexts(){for(const t of G.texts){ctx.globalAlpha=clamp(t.life,0,1);text(t.str,t.x,t.y,t.size,t.color,'center',8);}ctx.globalAlpha=1;}
function drawHUD(){ctx.fillStyle='rgba(255,255,255,.06)';ctx.fillRect(0,0,W,44);
text('SCORE '+G.score,18,15,17,'#e8f1ff','left');
text('BEST '+G.best,18,33,11,'#8fa3c8','left');
const li=Math.min(G.level,LEVELS.length-1);
if(G.state!=='menu')text('LEVEL '+(li+1)+' · '+LEVELS[li].name,W/2,22,14,'#8fa3c8');
for(let i=0;i<Math.max(0,G.lives);i++)text('♥',W-24-i*24,22,20,'#ff4d6d','center',8);
text(G.muted?'M · MUTED':'M · SOUND',W-16,H-14,10,'#5a6b8c','right');}
function drawEffects(){const list=[['expand','E','#3ddc84',12],['slow','S','#ffd93b',8],['fire','F','#ff6b35',6]];let y=H-18;
for(const[k,l,c,m]of list){const v=G.effects[k];if(v<=0)continue;
text(l,22,y,13,c,'left');ctx.fillStyle='rgba(255,255,255,.15)';ctx.fillRect(38,y-4,70,8);
ctx.fillStyle=c;ctx.fillRect(38,y-4,70*(v/m),8);y-=18;}
if(G.combo>=4&&G.state==='play'){const s=1+.12*Math.sin(G.time*10);
ctx.save();ctx.translate(W/2,H-120);ctx.scale(s,s);
text('COMBO ×'+G.combo,0,0,24,G.combo>=10?'#ff6b35':'#ffd93b','center',16);ctx.restore();}}
function render(){
const bg=ctx.createLinearGradient(0,0,0,H);bg.addColorStop(0,'#0d1122');bg.addColorStop(1,'#070a14');
ctx.fillStyle=bg;ctx.fillRect(0,0,W,H);
ctx.save();
if(G.shake>.1)ctx.translate((Math.random()-.5)*G.shake,(Math.random()-.5)*G.shake);
for(const s of stars){ctx.fillStyle='rgba(255,255,255,'+(s.z*.35)+')';ctx.fillRect(s.x,s.y,s.z*2,s.z*2);}
ctx.strokeStyle='rgba(126,231,255,.12)';ctx.lineWidth=2;rr(8,52,W-16,H-60,10);ctx.stroke();
for(const b of G.bricks)if(b.alive)drawBrick(b);
drawPowerups();drawPaddle();drawBalls();drawParticles();drawRings();drawTexts();
ctx.restore();
drawHUD();drawEffects();
if(G.flashA>.01){ctx.globalAlpha=G.flashA;ctx.fillStyle=G.flashC;ctx.fillRect(0,0,W,H);ctx.globalAlpha=1;}
const blink=Math.floor(G.time*1.8)%2===0;
if(G.state==='menu'){dim(.62);const p=.7+.3*Math.sin(G.time*2.2);
text('NEON',W/2,150,66,'#7ee7ff','center',30*p);
text('BREAKOUT',W/2,214,66,'#ff4d6d','center',30*p);
text('SIX LEVELS · COMBOS · POWER-UPS',W/2,266,15,'#8fa3c8');
text('← → / A D / MOUSE — MOVE',W/2,326,16,'#c9d6ee');
text('SPACE / CLICK — LAUNCH P — PAUSE',W/2,352,16,'#c9d6ee');
text('E EXPAND · M MULTI · S SLOW · F FIRE · +1 LIFE',W/2,392,13,'#8fa3c8');
if(blink)text('PRESS SPACE OR CLICK TO START',W/2,448,20,'#ffd93b','center',14);
if(G.best>0)text('BEST '+G.best,W/2,496,15,'#8fa3c8');}
else if(G.state==='intro'){dim(.45);
text('LEVEL '+(G.level+1),W/2,H/2-40,44,'#7ee7ff','center',24);
text(LEVELS[G.level].name,W/2,H/2+6,22,'#ffd93b','center',14);
if(G.cycle>0)text('NEW GAME+'+G.cycle,W/2,H/2+38,16,'#ff9f43','center',10);}
else if(G.state==='serve'){if(blink)text('SPACE / CLICK TO LAUNCH',W/2,PADDLE_Y-64,16,'#c9d6ee');}
else if(G.state==='levelclear'){dim(.35);text('LEVEL CLEAR!',W/2,H/2+34,40,'#3ddc84','center',22);}
else if(G.state==='gameover'){dim(.72);
text('GAME OVER',W/2,200,54,'#ff4d6d','center',30);
text('SCORE '+G.score,W/2,268,24,'#e8f1ff');
text('BEST '+G.best+' BEST COMBO ×'+G.bestCombo,W/2,304,15,'#8fa3c8');
if(blink)text('R / CLICK — TRY AGAIN',W/2,368,18,'#ffd93b','center',12);}
else if(G.state==='win'){dim(.6);
text('YOU WIN!',W/2,190,56,'#ffd93b','center',32);
text('SCORE '+G.score,W/2,262,24,'#e8f1ff');
text('BEST '+G.best+' BEST COMBO ×'+G.bestCombo,W/2,298,15,'#8fa3c8');
if(blink)text('SPACE — NEW GAME+ (FASTER)',W/2,362,18,'#3ddc84','center',12);}
else if(G.state==='paused'){dim(.6);text('PAUSED',W/2,H/2-20,44,'#7ee7ff','center',24);text('P — RESUME',W/2,H/2+30,16,'#c9d6ee');}
const v=ctx.createRadialGradient(W/2,H/2,H*.45,W/2,H/2,H*.85);
v.addColorStop(0,'rgba(0,0,0,0)');v.addColorStop(1,'rgba(0,0,0,.35)');
ctx.fillStyle=v;ctx.fillRect(0,0,W,H);}
/* ---------- input ---------- */
function primaryAction(){initAudio();if(AC&&AC.state==='suspended')AC.resume();
if(G.state==='menu')startGame();
else if(G.state==='serve')launch();
else if(G.state==='gameover')startGame();
else if(G.state==='win')continueNG();
else if(G.state==='paused')G.state=G.pausedFrom;}
function togglePause(){
if(G.state==='play'||G.state==='serve'){G.pausedFrom=G.state;G.state='paused';}
else if(G.state==='paused')G.state=G.pausedFrom;}
addEventListener('keydown',e=>{const k=e.key.toLowerCase();
if(['arrowleft','arrowright','arrowup','arrowdown',' '].includes(k))e.preventDefault();
keys[k]=true;
if(k===' '||k==='enter'||k==='arrowup')primaryAction();
else if(k==='p'||k==='escape')togglePause();
else if(k==='m'){initAudio();G.muted=!G.muted;store.set('nb_mute',G.muted?'1':'0');}
else if(k==='r'&&G.state==='gameover')startGame();});
addEventListener('keyup',e=>keys[e.key.toLowerCase()]=false);
canvas.addEventListener('pointermove',e=>{const r=canvas.getBoundingClientRect();
P.tx=clamp((e.clientX-r.left)*W/r.width,0,W);});
canvas.addEventListener('pointerdown',e=>{e.preventDefault();primaryAction();});
document.addEventListener('visibilitychange',()=>{if(document.hidden&&(G.state==='play'||G.state==='serve')){G.pausedFrom=G.state;G.state='paused';}});
/* ---------- main loop ---------- */
let last=performance.now();
(function frame(t){requestAnimationFrame(frame);
const dt=Math.min((t-last)/1000,1/30);last=t;update(dt);render();})(last);
})();
</script>
</body>
</html>

