Run details
- Model
- nex-agi/nex-n2.6
- Route
- OpenRouter API, one user message
- Served by
- Nex AGI
- Effort
- high, OpenRouter's default for this model
- Limits
- 60 min · 235,929 output tokens (the model's limit)
- Date
- 10 Nov 2026
- Time
- 6 min 41 s
- Size
- 33 kB
- Tokens
- 88 in, 44,821 out (33,085 of them thinking)
- List price
- $0.018 at today's price ($0.12 in / $0.40 out per M)
- Paid
- $0.018
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
33 kB 860 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="dark">
<title>BREAKPOINT — Brick Breaker</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{
background:#07090d;
display:flex;align-items:center;justify-content:center;
font-family:ui-monospace,"SF Mono","Cascadia Mono",Menlo,Consolas,"Liberation Mono",monospace;
color:#e8edf4;overflow:hidden;user-select:none;-webkit-user-select:none;
}
#stage{position:relative;border:1px solid rgba(255,255,255,.09);border-radius:10px;overflow:hidden;box-shadow:0 30px 80px rgba(0,0,0,.6)}
#stage::after{content:"";position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(255,255,255,.018) 0 1px,transparent 1px 3px)}
canvas{display:block;background:#0b0e13;touch-action:none}
#overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;text-align:center;background:rgba(9,11,16,.84);backdrop-filter:blur(3px);opacity:0;pointer-events:none;transition:opacity .25s ease}
#overlay.show{opacity:1}
#ov-inner{max-width:680px;padding:24px}
.ov-eyebrow{font-size:11px;letter-spacing:.35em;color:#7f8ca0;margin-bottom:16px}
.ov-logo{font-size:clamp(44px,9vw,74px);letter-spacing:.1em;color:#f4f7fb;font-weight:800;line-height:1;margin-bottom:12px}
.ov-logo span{color:#ffc247}
.ov-title{font-size:clamp(30px,6vw,52px);letter-spacing:.14em;color:#ffc247;font-weight:800;margin-bottom:12px}
.ov-score{font-size:clamp(40px,8vw,64px);color:#f4f7fb;font-weight:800;letter-spacing:.06em;margin:4px 0 10px}
.ov-sub{font-size:13px;color:#aab6c4;margin-bottom:18px;letter-spacing:.08em}
.ov-controls{display:inline-flex;flex-direction:column;gap:6px;margin:6px 0 18px;font-size:12px;color:#8b98a8;letter-spacing:.08em;text-align:left}
.ov-controls span{display:inline-block;min-width:170px;color:#ffc247;text-align:right;margin-right:16px;font-weight:700}
.ov-best{font-size:12px;color:#5fd68b;letter-spacing:.2em;margin-bottom:16px}
.ov-hint{font-size:13px;color:#f4f7fb;letter-spacing:.25em;animation:blink 1.1s steps(2,end) infinite;margin-top:4px}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.25}}
</style>
</head>
<body>
<div id="stage">
<canvas id="game"></canvas>
<div id="overlay"><div id="ov-inner"></div></div>
</div>
<script>
'use strict';
/* ================= setup ================= */
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
const W=900,H=640;
const DPR=Math.min(window.devicePixelRatio||1,2);
canvas.width=W*DPR;canvas.height=H*DPR;
const MONO='ui-monospace,"SF Mono","Cascadia Mono",Menlo,Consolas,monospace';
function resize(){
const pad=28;
let w=Math.min(window.innerWidth-pad*2,(window.innerHeight-pad*2)*(W/H));
w=Math.max(320,w);
canvas.style.width=w+'px';
canvas.style.height=(w*H/W)+'px';
}
window.addEventListener('resize',resize);resize();
const overlay=document.getElementById('overlay');
const ovInner=document.getElementById('ov-inner');
function showOverlay(html){ovInner.innerHTML=html;overlay.classList.add('show');}
function hideOverlay(){overlay.classList.remove('show');}
function clamp(v,a,b){return v<a?a:(v>b?b:v);}
/* ================= synthesized audio (no files) ================= */
const Sfx=(()=>{
let ac=null,master=null,muted=false;
function ensure(){
if(!ac){
const AC=window.AudioContext||window.webkitAudioContext;
if(!AC)return false;
ac=new AC();master=ac.createGain();master.gain.value=0.4;master.connect(ac.destination);
}
if(ac.state==='suspended')ac.resume();
return true;
}
function env(g,t,vol,dur){g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(0.0001,t+dur);}
function blip(freq,dur,type,vol,slide){
if(!ensure()||muted)return;
dur=dur||0.08;type=type||'square';vol=vol==null?0.5:vol;
const t=ac.currentTime,o=ac.createOscillator(),g=ac.createGain();
o.type=type;o.frequency.setValueAtTime(freq,t);
if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(30,freq+slide),t+dur);
env(g,t,vol,dur);o.connect(g);g.connect(master);o.start(t);o.stop(t+dur+0.02);
}
function noise(dur,vol,freq,type){
if(!ensure()||muted)return;
dur=dur||0.15;vol=vol==null?0.4:vol;type=type||'lowpass';
const t=ac.currentTime,n=Math.floor(ac.sampleRate*dur);
const buf=ac.createBuffer(1,n,ac.sampleRate),d=buf.getChannelData(0);
for(let i=0;i<n;i++)d[i]=Math.random()*2-1;
const src=ac.createBufferSource();src.buffer=buf;
const f=ac.createBiquadFilter();f.type=type;f.frequency.value=freq;
const g=ac.createGain();env(g,t,vol,dur);
src.connect(f);f.connect(g);g.connect(master);src.start(t);
}
function note(freq,when,dur,type,vol){
if(!ensure()||muted)return;
const t=ac.currentTime+when,o=ac.createOscillator(),g=ac.createGain();
o.type=type||'square';o.frequency.value=freq;
g.gain.setValueAtTime(0.0001,t);
g.gain.exponentialRampToValueAtTime(vol||0.32,t+0.01);
g.gain.exponentialRampToValueAtTime(0.0001,t+(dur||0.12));
o.connect(g);g.connect(master);o.start(t);o.stop(t+(dur||0.12)+0.02);
}
return{
unlock(){ensure();},
toggleMute(){muted=!muted;return muted;},
get muted(){return muted;},
paddle(){blip(190,0.06,'square',0.45);noise(0.03,0.12,2400,'highpass');},
wall(){blip(150,0.045,'triangle',0.3);},
brickHit(){blip(320,0.05,'square',0.4);},
brickBreak(c){blip(340*Math.pow(2,Math.min(c,14)/14),0.07,'square',0.45);noise(0.06,0.2,3200,'highpass');},
steel(){blip(1250,0.04,'sine',0.3);blip(1870,0.03,'sine',0.2);noise(0.03,0.12,5000,'highpass');},
explode(){noise(0.4,0.6,420,'lowpass');blip(75,0.35,'sine',0.6,-40);},
power(){[523,659,784,1047].forEach((f,i)=>note(f,i*0.06,0.09,'square',0.3));},
launch(){blip(420,0.09,'square',0.4,300);},
lifeLost(){blip(320,0.5,'sawtooth',0.45,-260);noise(0.3,0.2,600,'lowpass');},
clear(){[523,659,784,1047,1319].forEach((f,i)=>note(f,i*0.09,0.15,'square',0.32));},
over(){[392,330,262,196].forEach((f,i)=>note(f,i*0.22,0.3,'sawtooth',0.3));},
win(){[523,659,784,1047,784,1047,1319].forEach((f,i)=>note(f,i*0.12,0.18,'square',0.3));},
click(){blip(700,0.03,'square',0.25);}
};
})();
/* ================= levels ================= */
const LEVELS=[
{name:'FIRST CONTACT',speed:340,rows:[
'.............',
'rrrrrrrrrrrrr',
'ooooooooooooo',
'yyyyyyyyyyyyy',
'ggggggggggggg']},
{name:'THE VAULT',speed:365,rows:[
'.....rrr.....',
'...TTTTTTT...',
'..ooooooooo..',
'TTTTTTTTTTTTT',
'..yyyyyyyyy..',
'...TTTTTTT...',
'.....ggg.....']},
{name:'DIAMOND',speed:390,rows:[
'......b......',
'.....bTb.....',
'....bToTb....',
'...bToEoTb...',
'....bToTb....',
'.....bTb.....',
'......b......']},
{name:'FORTRESS',speed:415,rows:[
'S...........S',
'S.TTTTTTTTT.S',
'S.rrrrrrrrr.S',
'S.TTTTTTTTT.S',
'S.yyyyyyyyy.S',
'S.TTTTTTTTT.S',
'S...........S']},
{name:'CHECKMATE',speed:440,rows:[
'T.T.T.T.T.T.T',
'.o.o.o.o.o.o.',
'T.T.T.T.T.T.T',
'.o.o.oEo.o.o.',
'T.T.T.T.T.T.T',
'.o.o.o.o.o.o.']},
{name:'FINAL GAUNTLET',speed:470,rows:[
'T.E.T.T.T.E.T',
'.ppppppppppp.',
'STSTSTSTSTSTS',
'.bbbbbbbbbbb.',
'T.E.T.T.T.E.T',
'.ppppppppppp.']}
];
const COLS=13,BRICK_W=60,BRICK_H=24,BRICK_GAP=6,BRICK_TOP=92,SIDE_PAD=24;
const COLORS={r:'#ff5c5c',o:'#ff9f43',y:'#ffd45e',g:'#5fd68b',b:'#5aa9ff',p:'#b78bff'};
const COLOR_LIST=Object.values(COLORS);
const POWERS=[
{id:'wide', color:'#5fd68b',label:'W',name:'WIDE PADDLE'},
{id:'slow', color:'#5aa9ff',label:'S',name:'SLOW MOTION'},
{id:'multi',color:'#ff9f43',label:'M',name:'MULTIBALL'},
{id:'fire', color:'#ff5c5c',label:'F',name:'FIREBALL'},
{id:'life', color:'#ff7ad9',label:'+',name:'EXTRA LIFE'}
];
/* ================= state ================= */
let state='menu'; // menu | intro | playing | clearing | dying | paused | gameover | win
let level=0,score=0,lives=3,combo=0;
let best=0;try{best=parseInt(localStorage.getItem('breakpoint_best_v1'))||0;}catch(e){}
function saveBest(){try{localStorage.setItem('breakpoint_best_v1',String(best));}catch(e){}}
let bricks=[],balls=[],powerups=[],particles=[],floaters=[],pendingBlasts=[];
let shakeMag=0,hitStop=0,flashColor=null,flashAlpha=0;
let slowTimer=0,wideTimer=0,fireTimer=0;
let introTimer=0,dyingTimer=0,clearTimer=0,cleared=false;
let time=0;
const paddle={x:W/2,y:H-46,w:112,h:16,baseW:112,targetW:112,squash:0,vx:0,_px:W/2};
/* stars backdrop */
const stars=[];
for(let i=0;i<70;i++)stars.push({x:Math.random()*W,y:Math.random()*H,s:0.5+Math.random()*1.4,v:6+Math.random()*18,a:0.04+Math.random()*0.14});
/* ================= helpers ================= */
function setPaddleX(x){const hw=paddle.w/2;paddle.x=clamp(x,hw+10,W-hw-10);}
function addShake(m){shakeMag=Math.min(shakeMag+m,26);}
function flash(c,a){flashColor=c;flashAlpha=Math.max(flashAlpha,a);}
function addFloater(x,y,text,color,size){floaters.push({x,y,text,color,size,life:1.1,vy:-46});if(floaters.length>24)floaters.shift();}
function capParticles(){while(particles.length>350)particles.shift();}
function rrPath(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();
}
/* ================= level build ================= */
function buildLevel(idx){
bricks=[];
LEVELS[idx].rows.forEach((row,r)=>{
for(let c=0;c<COLS;c++){
const ch=row[c];
if(!ch||ch==='.')continue;
const b={x:SIDE_PAD+c*(BRICK_W+BRICK_GAP),y:BRICK_TOP+r*(BRICK_H+BRICK_GAP),
w:BRICK_W,h:BRICK_H,type:'normal',hits:1,color:'#fff',alive:true,flash:0,cx:0,cy:0};
b.cx=b.x+b.w/2;b.cy=b.y+b.h/2;
if(COLORS[ch])b.color=COLORS[ch];
else if(ch==='T'){b.type='tough';b.hits=2;b.color='#9db1bd';}
else if(ch==='S'){b.type='steel';b.hits=Infinity;b.color='#414a57';}
else if(ch==='E'){b.type='explosive';b.color='#2e3440';}
bricks.push(b);
}
});
}
/* ================= balls ================= */
function spawnBall(x,y,ang,speed){
speed=speed||LEVELS[level].speed;
const b={x,y,r:7,speed,vx:Math.sin(ang)*speed,vy:-Math.cos(ang)*speed,stuck:false,fire:0,trail:[]};
balls.push(b);return b;
}
function spawnStickyBall(){const b=spawnBall(paddle.x,paddle.y-14,0);b.stuck=true;}
function launchBalls(){
let did=false;
balls.forEach(b=>{
if(b.stuck){b.stuck=false;const a=Math.random()*0.6-0.3;b.vx=Math.sin(a)*b.speed;b.vy=-Math.cos(a)*b.speed;did=true;}
});
if(did)Sfx.launch();
}
function pushTrail(b){
b.trail.push({x:b.x,y:b.y});
const max=b.fire>0?16:10;
while(b.trail.length>max)b.trail.shift();
}
function onBounce(b){
const minVy=b.speed*0.12,minVx=b.speed*0.06;
let vx=b.vx,vy=b.vy;
if(Math.abs(vy)<minVy)vy=(vy<0?-1:1)*minVy;
if(Math.abs(vx)<minVx)vx=(vx<0?-1:1)*minVx;
const m=Math.hypot(vx,vy)||1;
b.vx=vx/m*b.speed;b.vy=vy/m*b.speed;
}
/* ================= collisions ================= */
function collideWalls(b){
if(b.x<b.r){b.x=b.r;b.vx=Math.abs(b.vx);onBounce(b);Sfx.wall();addShake(1.2);}
else if(b.x>W-b.r){b.x=W-b.r;b.vx=-Math.abs(b.vx);onBounce(b);Sfx.wall();addShake(1.2);}
if(b.y<b.r+66){b.y=b.r+66;b.vy=Math.abs(b.vy);onBounce(b);Sfx.wall();addShake(1.2);}
}
function collidePaddle(b){
const hw=paddle.w/2,hh=paddle.h/2;
if(b.vy>0&&b.y+b.r>=paddle.y-hh&&b.y-b.r<=paddle.y+hh&&b.x>=paddle.x-hw-b.r&&b.x<=paddle.x+hw+b.r){
b.y=paddle.y-hh-b.r;
b.speed=Math.min(b.speed+3,640);
const rel=clamp((b.x-paddle.x)/hw,-1,1);
const ang=rel*1.05;
b.vx=Math.sin(ang)*b.speed;b.vy=-Math.cos(ang)*b.speed;
b.vx+=clamp(paddle.vx*0.12,-60,60);
onBounce(b);
combo=0;paddle.squash=1;
Sfx.paddle();addShake(2);
spawnSparks(b.x,paddle.y-hh,'#ffffff',6);
}
}
function collideBricks(b){
for(const br of bricks){
if(!br.alive)continue;
const nx=clamp(b.x,br.x,br.x+br.w),ny=clamp(b.y,br.y,br.y+br.h);
const dx=b.x-nx,dy=b.y-ny;
if(dx*dx+dy*dy<=b.r*b.r){
if(b.fire>0&&br.type!=='steel'){destroyBrick(br);continue;} // fireball pierces
hitBrick(b,br);
const ox=(b.x<br.x+br.w/2)?(br.x-b.r-b.x):(br.x+br.w+b.r-b.x);
const oy=(b.y<br.y+br.h/2)?(br.y-b.r-b.y):(br.y+br.h+b.r-b.y);
if(Math.abs(ox)<Math.abs(oy)){b.x+=ox;b.vx=-b.vx;}else{b.y+=oy;b.vy=-b.vy;}
onBounce(b);
break;
}
}
}
function hitBrick(b,br){
br.flash=1;
if(br.type==='steel'){Sfx.steel();spawnSparks(b.x,b.y,'#9fb0c0',5);addShake(1.5);return;}
br.hits--;
if(br.hits<=0)destroyBrick(br);
else{Sfx.brickHit();spawnSparks(b.x,b.y,br.color,4);addShake(2);addScore(10,br.cx,br.cy,false);}
}
function destroyBrick(br){
if(!br.alive)return;
br.alive=false;
combo++;
const base=br.type==='explosive'?120:(br.type==='tough'?80:50);
addScore(base,br.cx,br.cy,true);
Sfx.brickBreak(combo);
spawnShards(br);
addShake(br.type==='explosive'?9:3.5);
if(br.type==='explosive'){hitStop=Math.max(hitStop,0.08);pendingBlasts.push({x:br.cx,y:br.cy,delay:0.03});}
else if(br.type==='tough'){hitStop=Math.max(hitStop,0.035);}
maybeDropPower(br);
checkClear();
}
function doBlast(x,y){
spawnExplosion(x,y);Sfx.explode();addShake(12);
for(const br of bricks){
if(!br.alive||br.type==='steel')continue;
if(Math.abs(br.cx-x)<=BRICK_W+BRICK_GAP&&Math.abs(br.cy-y)<=BRICK_H+BRICK_GAP){
br.hits--;br.flash=1;
if(br.hits<=0)destroyBrick(br);
else addScore(10,br.cx,br.cy,false);
}
}
}
/* ================= score / clear ================= */
function addScore(base,x,y,popup){
const pts=base+Math.max(0,combo-1)*10;
score+=pts;
if(score>best){best=score;saveBest();}
if(popup){
addFloater(x,y,'+'+pts,'#ffffff',13);
if(combo>=3)addFloater(x,y-18,'COMBO ×'+combo,'#ffc247',11);
}
}
function checkClear(){
if(cleared)return;
if(bricks.some(b=>b.alive&&b.type!=='steel'))return;
cleared=true;
state='clearing';clearTimer=1.5;
const bonus=250*(level+1)+lives*100;
addScore(bonus,W/2,H/2,false);
addFloater(W/2,H/2-30,'+'+bonus,'#ffffff',16);
addFloater(W/2,H/2-64,'LEVEL CLEAR','#ffd45e',26);
Sfx.clear();fountain();
}
/* ================= power-ups ================= */
function maybeDropPower(br){
if(Math.random()>0.13||powerups.length>=3)return;
const table=[['wide',26],['slow',22],['multi',20],['fire',20],['life',12]];
let r=Math.random()*100,pick='wide';
for(const[id,w]of table){if(r<w){pick=id;break;}r-=w;}
const def=POWERS.find(p=>p.id===pick);
powerups.push({x:br.cx,y:br.cy,vy:135,wobble:Math.random()*6.28,id:def.id,label:def.label,color:def.color,name:def.name});
}
function applyPower(p){
Sfx.power();
addFloater(paddle.x,paddle.y-26,p.name,p.color,15);
spawnSparks(p.x,p.y,p.color,14);
addShake(3);
switch(p.id){
case 'wide':wideTimer=12;paddle.targetW=paddle.baseW*1.55;break;
case 'slow':slowTimer=8;break;
case 'fire':fireTimer=8;balls.forEach(b=>b.fire=8);break;
case 'multi':{
const snap=balls.filter(b=>!b.stuck).map(b=>({x:b.x,y:b.y,vx:b.vx,vy:b.vy,speed:b.speed,fire:b.fire}));
for(const s of snap){
if(balls.length>=9)break;
for(const da of[-0.5,0.5]){
if(balls.length>=9)break;
const a=Math.atan2(s.vx,-s.vy)+da;
const nb={x:s.x,y:s.y,r:7,speed:s.speed,vx:Math.sin(a)*s.speed,vy:-Math.cos(a)*s.speed,stuck:false,fire:s.fire,trail:[]};
balls.push(nb);
}
}
break;
}
case 'life':lives=Math.min(9,lives+1);break;
}
}
/* ================= particles / fx ================= */
function spawnSparks(x,y,color,n){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2,sp=60+Math.random()*180;
particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-40,life:0.35+Math.random()*0.25,max:0.6,size:1.5+Math.random()*2,color,g:600,shape:0,rot:0,vr:0});
}
capParticles();
}
function spawnShards(br){
for(let i=0;i<14;i++){
const a=Math.random()*Math.PI*2,sp=80+Math.random()*220;
particles.push({x:br.cx+(Math.random()-0.5)*br.w*0.7,y:br.cy+(Math.random()-0.5)*br.h*0.7,
vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-90,life:0.5+Math.random()*0.4,max:0.9,
size:3+Math.random()*4,color:Math.random()<0.75?br.color:'#ffffff',g:900,
rot:Math.random()*6.28,vr:(Math.random()-0.5)*12,shape:1});
}
capParticles();
}
function spawnExplosion(x,y){
flash('#ffb340',0.15);
for(let i=0;i<30;i++){
const a=Math.random()*Math.PI*2,sp=100+Math.random()*320;
particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:0.4+Math.random()*0.5,max:0.9,
size:2+Math.random()*4,color:['#ffb340','#ff5c5c','#ffd45e','#ffffff'][i%4],g:500,rot:0,vr:0,shape:1});
}
particles.push({x,y,vx:0,vy:0,life:0.18,max:0.18,size:46,color:'#ffffff',g:0,rot:0,vr:0,shape:2});
capParticles();
}
function spawnFireSpark(b){
particles.push({x:b.x+(Math.random()-0.5)*6,y:b.y+(Math.random()-0.5)*6,
vx:(Math.random()-0.5)*40,vy:30+Math.random()*40,life:0.3,max:0.3,
size:2+Math.random()*2.5,color:Math.random()<0.5?'#ff8c3b':'#ffd45e',g:-60,rot:0,vr:0,shape:0});
capParticles();
}
function fountain(){
for(let i=0;i<60;i++){
particles.push({x:W/2+(Math.random()-0.5)*W*0.7,y:H*0.35,
vx:(Math.random()-0.5)*250,vy:-Math.random()*320-100,life:0.8+Math.random()*0.8,max:1.6,
size:2+Math.random()*3,color:COLOR_LIST[i%6],g:500,rot:0,vr:(Math.random()-0.5)*10,shape:1});
}
capParticles();
}
/* ================= updates ================= */
function updateStars(dt){for(const s of stars){s.y+=s.v*dt;if(s.y>H){s.y=-2;s.x=Math.random()*W;}}}
function updateFx(dt){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];p.life-=dt;
if(p.life<=0){particles.splice(i,1);continue;}
p.x+=p.vx*dt;p.y+=p.vy*dt;p.vy+=(p.g||0)*dt;
if(p.vr)p.rot+=p.vr*dt;
}
for(let i=floaters.length-1;i>=0;i--){
const f=floaters[i];f.life-=dt;f.y+=f.vy*dt;
if(f.life<=0)floaters.splice(i,1);
}
shakeMag*=Math.exp(-7*dt);if(shakeMag<0.05)shakeMag=0;
if(flashAlpha>0)flashAlpha=Math.max(0,flashAlpha-dt*1.2);
for(const b of bricks)if(b.flash>0)b.flash=Math.max(0,b.flash-dt*5);
}
function updatePaddle(dt){
const dir=(keys.right?1:0)-(keys.left?1:0);
if(dir!==0)setPaddleX(paddle.x+dir*640*dt);
paddle.w+=(paddle.targetW-paddle.w)*Math.min(1,dt*10);
paddle.squash=Math.max(0,paddle.squash-dt*5);
paddle.vx=(paddle.x-paddle._px)/Math.max(dt,0.001);paddle._px=paddle.x;
}
function updateBalls(dt){
const sdt=dt*(slowTimer>0?0.72:1);
for(let i=balls.length-1;i>=0;i--){
const b=balls[i];
if(b.fire>0)b.fire=Math.max(0,b.fire-sdt);
if(b.stuck){b.x=paddle.x;b.y=paddle.y-paddle.h/2-b.r-1;continue;}
const dist=b.speed*sdt;
const steps=Math.max(1,Math.ceil(dist/6)),step=dist/steps;
let dead=false;
for(let s=0;s<steps;s++){
b.x+=b.vx/b.speed*step;b.y+=b.vy/b.speed*step;
collideWalls(b);collidePaddle(b);collideBricks(b);
if(b.y>H+20){dead=true;break;}
}
pushTrail(b);
if(b.fire>0&&Math.random()<0.5)spawnFireSpark(b);
if(dead){
balls.splice(i,1);
if(balls.length===0&&state==='playing')loseLife();
}
}
}
function updatePowerups(dt){
for(let i=powerups.length-1;i>=0;i--){
const p=powerups[i];
p.y+=p.vy*dt;p.x+=Math.sin(time*4+p.wobble)*12*dt;
if(p.y>paddle.y-14&&p.y<paddle.y+16&&Math.abs(p.x-paddle.x)<paddle.w/2+16){
applyPower(p);powerups.splice(i,1);continue;
}
if(p.y>H+24)powerups.splice(i,1);
}
}
function updateEffects(dt){
if(wideTimer>0){wideTimer-=dt;if(wideTimer<=0){wideTimer=0;paddle.targetW=paddle.baseW;}}
if(slowTimer>0)slowTimer=Math.max(0,slowTimer-dt);
if(fireTimer>0){fireTimer=Math.max(0,fireTimer-dt);if(fireTimer===0)balls.forEach(b=>b.fire=0);}
}
function updateBlasts(dt){
for(let i=pendingBlasts.length-1;i>=0;i--){
const bl=pendingBlasts[i];bl.delay-=dt;
if(bl.delay<=0){pendingBlasts.splice(i,1);doBlast(bl.x,bl.y);}
}
}
function loseLife(){
lives--;combo=0;
Sfx.lifeLost();addShake(14);flash('#ff3b3b',0.22);
slowTimer=0;fireTimer=0;balls.forEach(b=>b.fire=0);
state='dying';dyingTimer=lives>0?0.9:1.0;
}
function afterDeath(){
if(lives<=0){gameOver();return;}
paddle.targetW=paddle.baseW;wideTimer=0;
spawnStickyBall();state='playing';
}
function update(dt){
updateStars(dt);updateFx(dt);updateBlasts(dt);
if(state==='playing'||state==='clearing'){
updatePaddle(dt);updateBalls(dt);updatePowerups(dt);updateEffects(dt);
if(state==='clearing'){
clearTimer-=dt;
if(clearTimer<=0){level++;if(level>=LEVELS.length)winGame();else startLevel();}
}
}else if(state==='dying'){
updatePaddle(dt);dyingTimer-=dt;
if(dyingTimer<=0)afterDeath();
}
}
/* ================= rendering ================= */
let vignette=null,lastCursor='';
function setCursor(){
const want=(state==='playing'||state==='intro'||state==='clearing'||state==='dying')?'none':'default';
if(want!==lastCursor){canvas.style.cursor=want;lastCursor=want;}
}
function drawBricks(){
for(const b of bricks){
if(!b.alive)continue;
if(b.type==='steel'){
ctx.fillStyle='#414a57';rrPath(b.x,b.y,b.w,b.h,3);ctx.fill();
ctx.strokeStyle='#5b6678';ctx.lineWidth=1.5;rrPath(b.x,b.y,b.w,b.h,3);ctx.stroke();
ctx.fillStyle='#78849b';
ctx.beginPath();ctx.arc(b.x+7,b.y+7,1.8,0,7);ctx.fill();
ctx.beginPath();ctx.arc(b.x+b.w-7,b.y+7,1.8,0,7);ctx.fill();
ctx.beginPath();ctx.arc(b.x+7,b.y+b.h-7,1.8,0,7);ctx.fill();
ctx.beginPath();ctx.arc(b.x+b.w-7,b.y+b.h-7,1.8,0,7);ctx.fill();
}else{
ctx.fillStyle=b.color;rrPath(b.x,b.y,b.w,b.h,4);ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.28)';ctx.fillRect(b.x+3,b.y+2,b.w-6,2);
ctx.fillStyle='rgba(0,0,0,0.25)';ctx.fillRect(b.x+3,b.y+b.h-4,b.w-6,2);
if(b.type==='explosive'){
const p=0.55+0.45*Math.sin(time*6+b.cx);
ctx.strokeStyle='rgba(255,179,64,'+(0.45+0.3*Math.sin(time*6+b.cx)).toFixed(3)+')';
ctx.lineWidth=1.5;rrPath(b.x+1,b.y+1,b.w-2,b.h-2,4);ctx.stroke();
ctx.fillStyle='rgba(255,179,64,'+p.toFixed(3)+')';
ctx.beginPath();ctx.arc(b.cx,b.cy,5,0,7);ctx.fill();
}
if(b.type==='tough'&&b.hits<2){
ctx.strokeStyle='rgba(15,20,26,0.65)';ctx.lineWidth=1.5;ctx.beginPath();
ctx.moveTo(b.x+b.w*0.3,b.y+2);ctx.lineTo(b.x+b.w*0.45,b.y+b.h*0.5);ctx.lineTo(b.x+b.w*0.3,b.y+b.h-2);
ctx.moveTo(b.x+b.w*0.45,b.y+b.h*0.5);ctx.lineTo(b.x+b.w*0.7,b.y+b.h-3);
ctx.stroke();
}
if(b.flash>0){
ctx.globalAlpha=b.flash*0.8;ctx.fillStyle='#ffffff';
rrPath(b.x,b.y,b.w,b.h,4);ctx.fill();ctx.globalAlpha=1;
}
}
}
}
function drawPaddle(){
const sq=paddle.squash;
const w=paddle.w*(1+sq*0.12),h=paddle.h*(1-sq*0.35);
const x=paddle.x,y=paddle.y;
ctx.fillStyle='rgba(0,0,0,0.35)';rrPath(x-w/2,y-h/2+3,w,h,h/2);ctx.fill();
ctx.fillStyle='#e8edf4';rrPath(x-w/2,y-h/2,w,h,h/2);ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.5)';ctx.fillRect(x-w/2+6,y-h/2+2,w-12,2);
ctx.fillStyle='#ffb340';
const sw=Math.min(34,w*0.3);rrPath(x-sw/2,y-h/2+2,sw,h-4,3);ctx.fill();
if(wideTimer>0){
ctx.strokeStyle='rgba(95,214,139,0.7)';ctx.lineWidth=2;
rrPath(x-w/2-3,y-h/2-3,w+6,h+6,h/2+3);ctx.stroke();
}
}
function drawBalls(){
for(const b of balls){
for(let i=0;i<b.trail.length;i++){
const t=b.trail[i],f=i/b.trail.length;
ctx.globalAlpha=f*0.25;
ctx.fillStyle=b.fire>0?'#ff8c3b':'#ffffff';
ctx.beginPath();ctx.arc(t.x,t.y,b.r*f*0.9,0,7);ctx.fill();
}
ctx.globalAlpha=1;
if(b.stuck){
ctx.strokeStyle='rgba(255,255,255,0.18)';ctx.setLineDash([3,5]);
ctx.beginPath();ctx.moveTo(b.x,b.y-12);ctx.lineTo(b.x,b.y-52);ctx.stroke();
ctx.setLineDash([]);
}
if(b.fire>0){ctx.shadowColor='#ff7b2e';ctx.shadowBlur=14;}
ctx.fillStyle=b.fire>0?'#ffb340':'#fff6e8';
ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,7);ctx.fill();
ctx.shadowBlur=0;
ctx.fillStyle='rgba(255,255,255,0.8)';
ctx.beginPath();ctx.arc(b.x-2,b.y-2,2.2,0,7);ctx.fill();
}
}
function drawPowerups(){
for(const p of powerups){
const x=p.x+Math.sin(time*5+p.wobble)*3,y=p.y;
ctx.save();ctx.translate(x,y);
ctx.fillStyle=p.color;rrPath(-17,-9,34,18,9);ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.35)';ctx.fillRect(-13,-7,26,2);
ctx.fillStyle='#0b0e13';ctx.font='bold 12px '+MONO;
ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText(p.label,0,1);
ctx.restore();
}
ctx.textBaseline='alphabetic';
}
function drawParticles(){
for(const p of particles){
const a=Math.max(0,p.life/p.max);
ctx.globalAlpha=a;
if(p.shape===2){
const r=p.size*(1-a)+6;
ctx.strokeStyle=p.color;ctx.lineWidth=Math.max(0.5,3*a);
ctx.beginPath();ctx.arc(p.x,p.y,r,0,7);ctx.stroke();
}else if(p.shape===1){
ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.rot||0);
ctx.fillStyle=p.color;ctx.fillRect(-p.size/2,-p.size/2,p.size,p.size*0.8);
ctx.restore();
}else{
ctx.fillStyle=p.color;
ctx.beginPath();ctx.arc(p.x,p.y,p.size,0,7);ctx.fill();
}
}
ctx.globalAlpha=1;
}
function drawFloaters(){
ctx.textAlign='center';
for(const f of floaters){
ctx.globalAlpha=Math.min(1,f.life*1.8);
ctx.font='bold '+f.size+'px '+MONO;
ctx.strokeStyle='rgba(0,0,0,0.6)';ctx.lineWidth=3;
ctx.strokeText(f.text,f.x,f.y);
ctx.fillStyle=f.color;ctx.fillText(f.text,f.x,f.y);
}
ctx.globalAlpha=1;
}
function drawEffectBars(){
const acts=[];
if(wideTimer>0)acts.push(['WIDE',wideTimer/12,'#5fd68b']);
if(slowTimer>0)acts.push(['SLOW',slowTimer/8,'#5aa9ff']);
if(fireTimer>0)acts.push(['FIRE',fireTimer/8,'#ff5c5c']);
if(!acts.length)return;
let x=W/2-(acts.length*86)/2+43;
for(const[label,frac,col]of acts){
ctx.fillStyle='rgba(255,255,255,0.08)';ctx.fillRect(x-35,56,70,5);
ctx.fillStyle=col;ctx.fillRect(x-35,56,70*clamp(frac,0,1),5);
ctx.font='8px '+MONO;ctx.textAlign='center';ctx.fillText(label,x,52);
x+=86;
}
}
function drawHUD(){
ctx.fillStyle='rgba(255,255,255,0.025)';ctx.fillRect(0,0,W,64);
ctx.strokeStyle='rgba(255,255,255,0.08)';ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(0,64.5);ctx.lineTo(W,64.5);ctx.stroke();
ctx.textBaseline='alphabetic';
// score
ctx.textAlign='left';ctx.letterSpacing='3px';
ctx.fillStyle='#7f8ca0';ctx.font='10px '+MONO;ctx.fillText('SCORE',24,24);
ctx.letterSpacing='0px';
ctx.fillStyle='#f4f7fb';ctx.font='bold 25px '+MONO;ctx.fillText(String(score).padStart(6,'0'),24,50);
if(combo>=2&&(state==='playing'||state==='clearing')){
ctx.fillStyle='#ffc247';ctx.globalAlpha=0.65+0.35*Math.sin(time*10);
ctx.font='bold 13px '+MONO;ctx.fillText('×'+combo,168,50);ctx.globalAlpha=1;
}
// level
ctx.textAlign='center';ctx.letterSpacing='3px';
ctx.fillStyle='#7f8ca0';ctx.font='10px '+MONO;ctx.fillText('LEVEL '+(level+1)+' / '+LEVELS.length,W/2,24);
ctx.letterSpacing='2px';
ctx.fillStyle='#ffc247';ctx.font='bold 15px '+MONO;ctx.fillText(LEVELS[level].name,W/2,45);
ctx.letterSpacing='0px';
drawEffectBars();
// best + lives
ctx.textAlign='right';ctx.letterSpacing='3px';
ctx.fillStyle='#7f8ca0';ctx.font='10px '+MONO;ctx.fillText('BEST',W-24,24);
ctx.letterSpacing='0px';
ctx.fillStyle='#f4f7fb';ctx.font='bold 16px '+MONO;ctx.fillText(String(best).padStart(6,'0'),W-24,44);
for(let i=0;i<lives;i++){
ctx.fillStyle='#f4f7fb';
ctx.beginPath();ctx.arc(W-28-i*17,54,4.5,0,7);ctx.fill();
}
if(Sfx.muted){
ctx.fillStyle='#ff5c5c';ctx.font='bold 9px '+MONO;ctx.letterSpacing='2px';
ctx.fillText('MUTED',W-110,57);ctx.letterSpacing='0px';
}
}
function render(){
ctx.setTransform(DPR,0,0,DPR,0,0);
ctx.fillStyle='#0b0e13';ctx.fillRect(0,0,W,H);
for(const s of stars){
ctx.globalAlpha=s.a;ctx.fillStyle='#cfe3ff';ctx.fillRect(s.x,s.y,s.s,s.s);
}
ctx.globalAlpha=1;
const sx=(Math.random()*2-1)*shakeMag,sy=(Math.random()*2-1)*shakeMag;
ctx.save();ctx.translate(sx,sy);
drawBricks();drawPowerups();drawParticles();drawPaddle();drawBalls();drawFloaters();
ctx.restore();
if(flashAlpha>0&&flashColor){
ctx.globalAlpha=flashAlpha;ctx.fillStyle=flashColor;
ctx.fillRect(0,0,W,H);ctx.globalAlpha=1;
}
if(!vignette){
vignette=ctx.createRadialGradient(W/2,H/2,H*0.35,W/2,H/2,H*0.78);
vignette.addColorStop(0,'rgba(0,0,0,0)');
vignette.addColorStop(1,'rgba(0,0,0,0.42)');
}
ctx.fillStyle=vignette;ctx.fillRect(0,0,W,H);
drawHUD();
}
/* ================= overlays / flow ================= */
function menuHTML(){
return '<div class="ov-eyebrow">ARCADE // BRICK-BREAKER</div>'+
'<h1 class="ov-logo">BREAK<span>POINT</span></h1>'+
'<div class="ov-sub">SIX LEVELS. ONE PADDLE. ZERO MERCY.</div>'+
'<div class="ov-controls">'+
'<div><span>← → / A D</span>move</div>'+
'<div><span>MOUSE</span>move</div>'+
'<div><span>SPACE / CLICK</span>launch ball</div>'+
'<div><span>P / ESC</span>pause</div>'+
'<div><span>M</span>mute</div>'+
'</div>'+
(best>0?'<div class="ov-best">BEST · '+String(best).padStart(6,'0')+'</div>':'')+
'<div class="ov-hint">PRESS SPACE OR CLICK TO START</div>';
}
function introHTML(){
return '<div class="ov-eyebrow">LEVEL '+(level+1)+' OF '+LEVELS.length+'</div>'+
'<div class="ov-title">'+LEVELS[level].name+'</div>'+
'<div class="ov-sub">'+(level===0?'Clear every breakable brick. Steel is forever.':'It only gets faster from here.')+'</div>'+
'<div class="ov-hint">LAUNCH WITH SPACE OR CLICK</div>';
}
function gameOver(){
state='gameover';saveBest();Sfx.over();
showOverlay('<div class="ov-eyebrow">GAME OVER</div>'+
'<div class="ov-score">'+score+'</div>'+
'<div class="ov-sub">REACHED LEVEL '+(level+1)+' — '+LEVELS[level].name+((score>=best&&score>0)?' · NEW BEST':'')+'</div>'+
'<div class="ov-best">BEST · '+String(best).padStart(6,'0')+'</div>'+
'<div class="ov-hint">SPACE OR CLICK TO RETRY</div>');
}
function winGame(){
state='win';saveBest();Sfx.win();fountain();
showOverlay('<div class="ov-eyebrow">ALL SIX LEVELS CLEARED</div>'+
'<div class="ov-title" style="color:#5fd68b">YOU WIN</div>'+
'<div class="ov-score">'+score+'</div>'+
'<div class="ov-best">BEST · '+String(best).padStart(6,'0')+'</div>'+
'<div class="ov-hint">SPACE OR CLICK TO PLAY AGAIN</div>');
}
function startGame(){
score=0;lives=3;level=0;combo=0;shakeMag=0;flashAlpha=0;
particles=[];floaters=[];
hideOverlay();startLevel();
}
function startLevel(){
buildLevel(level);
balls=[];powerups=[];pendingBlasts=[];
slowTimer=0;wideTimer=0;fireTimer=0;
paddle.w=paddle.baseW;paddle.targetW=paddle.baseW;paddle.x=W/2;
combo=0;cleared=false;flashAlpha=0;
spawnStickyBall();
state='intro';introTimer=1.7;
showOverlay(introHTML());
Sfx.click();
}
function togglePause(){
if(state==='playing'){state='paused';showOverlay('<div class="ov-title">PAUSED</div><div class="ov-hint">P / ESC / CLICK TO RESUME</div>');Sfx.click();}
else if(state==='paused'){state='playing';hideOverlay();Sfx.click();}
}
function action(){
switch(state){
case 'menu':case 'gameover':case 'win':startGame();break;
case 'playing':launchBalls();break;
case 'paused':togglePause();break;
}
}
/* ================= input ================= */
const keys={left:false,right:false};
window.addEventListener('keydown',e=>{
const k=e.key;
if(k==='ArrowLeft'||k==='a'||k==='A'){keys.left=true;e.preventDefault();}
if(k==='ArrowRight'||k==='d'||k==='D'){keys.right=true;e.preventDefault();}
if(k===' '||k==='Enter'){e.preventDefault();Sfx.unlock();action();}
if(k==='p'||k==='P'||k==='Escape')togglePause();
if(k==='m'||k==='M'){const m=Sfx.toggleMute();addFloater(W/2,140,m?'MUTED':'SOUND ON','#9fb0c0',14);}
});
window.addEventListener('keyup',e=>{
const k=e.key;
if(k==='ArrowLeft'||k==='a'||k==='A')keys.left=false;
if(k==='ArrowRight'||k==='d'||k==='D')keys.right=false;
});
window.addEventListener('pointermove',e=>{
const r=canvas.getBoundingClientRect();
if(r.width===0)return;
if(state==='playing'||state==='intro'||state==='clearing'||state==='menu'){
setPaddleX((e.clientX-r.left)/r.width*W);
}
});
window.addEventListener('pointerdown',()=>{Sfx.unlock();action();});
window.addEventListener('blur',()=>{if(state==='playing')togglePause();});
/* ================= main loop ================= */
let last=performance.now();
function loop(now){
requestAnimationFrame(loop);
const dt=Math.min((now-last)/1000,0.033);
last=now;time+=dt;
setCursor();
if(hitStop>0){hitStop-=dt;render();return;} // hit-stop freeze for punch
if(state==='paused'||state==='menu'||state==='gameover'||state==='win'){
updateStars(dt);updateFx(dt);render();return;
}
if(state==='intro'){
updateStars(dt);updateFx(dt);
introTimer-=dt;
if(introTimer<=0){hideOverlay();state='playing';}
render();return;
}
update(dt);render();
}
/* ================= boot ================= */
buildLevel(0); // backdrop behind the menu
spawnStickyBall();
showOverlay(menuHTML());
requestAnimationFrame(loop);
</script>
</body>
</html>

