Run details
- Model
- z-ai/glm-5.3-flash
- Route
- OpenRouter API, one user message
- Served by
- OpenInference
- Effort
- max, OpenRouter's default for this model
- Limits
- 60 min · 131,072 output tokens (the model's limit)
- Date
- 31 Oct 2026
- Time
- 13 min 57 s
- Size
- 29 kB
- Tokens
- 103 in, 64,984 out (53,387 of them thinking)
- List price
- $0.010 at today's price ($0.016 in / $0.148 out per M)
- Paid
- $0.002
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
29 kB 882 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BRICK RIOT</title>
<style>
html,body{margin:0;height:100%;background:#0a0b10;display:flex;align-items:center;justify-content:center;overflow:hidden;user-select:none;-webkit-user-select:none}
canvas{display:block;border:1px solid #262b38;border-radius:10px;box-shadow:0 24px 70px rgba(0,0,0,.6);cursor:none;touch-action:none}
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
'use strict';
/* =========================================================
BRICK RIOT — a self-contained arcade breakout
========================================================= */
/* ---------- constants & palette ---------- */
const W=560, H=760, WALL=12, HUD_H=64, TOP=HUD_H+WALL;
const BRICK_X0=18, BRICK_Y0=150, BW=44, BH=20, BSTEP=48, BSTEPY=24;
const COL={
bg:'#12141b', wall:'#1b1e28',
bone:'#efe9db', dim:'#8b8676',
b1:'#ffd44d', b2:'#ff9d3b', b3:'#ff5a4e',
steel:'#454b59', steelEdge:'#626a7c',
coral:'#ff5a4e', amber:'#ffb03a', mint:'#3fd6a4', ice:'#7fd7e4',
};
const POWCOL={W:'#3fd6a4',M:'#ffb03a',S:'#7fd7e4',L:'#ff5a4e'};
const POWLET={W:'W',M:'M',S:'S',L:'+'};
/* ---------- canvas & scaling ---------- */
const cv=document.getElementById('game');
const ctx=cv.getContext('2d');
const DPR=Math.min(2, window.devicePixelRatio||1);
cv.width=W*DPR; cv.height=H*DPR;
function resize(){
const s=Math.min((window.innerWidth-28)/W,(window.innerHeight-28)/H);
cv.style.width=(W*s)+'px'; cv.style.height=(H*s)+'px';
}
window.addEventListener('resize',resize); resize();
/* ---------- helpers ---------- */
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const easeOut=t=>1-Math.pow(1-t,3);
const pad2=n=>String(n).padStart(2,'0');
const pad6=n=>String(clamp(Math.floor(n),0,999999)).padStart(6,'0');
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();
}
/* ---------- hand-built 5x7 dot-matrix font ---------- */
const FONT={
'0':[14,17,19,21,25,17,14],'1':[4,12,4,4,4,4,14],'2':[14,17,1,2,4,8,31],
'3':[31,2,4,2,1,17,14],'4':[2,6,10,18,31,2,2],'5':[31,16,30,1,1,17,14],
'6':[6,8,16,30,17,17,14],'7':[31,1,2,4,8,8,8],'8':[14,17,17,14,17,17,14],
'9':[14,17,17,15,1,2,12],
'A':[14,17,17,31,17,17,17],'B':[30,17,17,30,17,17,30],'C':[14,17,16,16,16,17,14],
'D':[28,18,17,17,17,18,28],'E':[31,16,16,30,16,16,31],'F':[31,16,16,30,16,16,16],
'G':[14,17,16,23,17,17,15],'H':[17,17,17,31,17,17,17],'I':[14,4,4,4,4,4,14],
'J':[7,2,2,2,2,18,12],'K':[17,18,20,24,20,18,17],'L':[16,16,16,16,16,16,31],
'M':[17,27,21,21,17,17,17],'N':[17,17,25,21,19,17,17],'O':[14,17,17,17,17,17,14],
'P':[30,17,17,30,16,16,16],'Q':[14,17,17,17,21,18,13],'R':[30,17,17,30,20,18,17],
'S':[15,16,16,14,1,1,30],'T':[31,4,4,4,4,4,4],'U':[17,17,17,17,17,17,14],
'V':[17,17,17,17,17,10,4],'W':[17,17,17,21,21,27,17],'X':[17,17,10,4,10,17,17],
'Y':[17,17,10,4,4,4,4],'Z':[31,1,2,4,8,16,31],
' ':[0,0,0,0,0,0,0],'.':[0,0,0,0,0,12,12],',':[0,0,0,0,12,4,8],
'!':[4,4,4,4,4,0,4],'?':[14,17,1,2,4,0,4],':':[0,12,12,0,12,12,0],
'-':[0,0,0,14,0,0,0],'+':[0,4,4,31,4,4,0],'×':[0,17,10,4,10,17,0],
'·':[0,0,0,12,12,0,0],'>':[16,24,28,30,28,24,16],
};
function textW(str,p){ return str.length*6*p-p; }
function drawText(str,x,y,p,color,align){
str=String(str).toUpperCase();
const w=textW(str,p);
if(align==='center') x-=w/2; else if(align==='right') x-=w;
ctx.fillStyle=color;
const g=p>=3?Math.max(0.6,p*0.18):0;
for(let i=0;i<str.length;i++){
const gl=FONT[str[i]]; if(!gl) continue;
const cx=x+i*6*p;
for(let r=0;r<7;r++){
const row=gl[r]; if(!row) continue;
for(let c=0;c<5;c++){
if(row&(16>>c)) ctx.fillRect(cx+c*p,y+r*p,p-g,p-g);
}
}
}
}
/* ---------- procedural audio (Web Audio, no assets) ---------- */
let actx=null, master=null, noiseBuf=null, muted=false;
function initAudio(){
if(actx){ if(actx.state==='suspended') actx.resume(); return; }
try{
actx=new (window.AudioContext||window.webkitAudioContext)();
master=actx.createGain(); master.gain.value=0.42; master.connect(actx.destination);
noiseBuf=actx.createBuffer(1,actx.sampleRate*0.5,actx.sampleRate);
const d=noiseBuf.getChannelData(0);
for(let i=0;i<d.length;i++) d[i]=Math.random()*2-1;
}catch(e){}
}
function tone(f,dur,type,vol,f2,delay){
if(!actx||muted) return;
const t=actx.currentTime+(delay||0);
const o=actx.createOscillator(), g=actx.createGain();
o.type=type||'square';
o.frequency.setValueAtTime(f,t);
if(f2) o.frequency.exponentialRampToValueAtTime(Math.max(20,f2),t+dur);
g.gain.setValueAtTime(vol,t);
g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
o.connect(g); g.connect(master);
o.start(t); o.stop(t+dur+0.03);
}
function noise(dur,vol,freq,q,delay){
if(!actx||muted||!noiseBuf) return;
const t=actx.currentTime+(delay||0);
const src=actx.createBufferSource(); src.buffer=noiseBuf;
const bp=actx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=freq; bp.Q.value=q||1;
const g=actx.createGain(); g.gain.setValueAtTime(vol,t);
g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
src.connect(bp); bp.connect(g); g.connect(master);
src.start(t); src.stop(t+dur+0.03);
}
const sfx={
brick(hp,row,combo){
const f=(300+row*36)*(1+combo*0.02);
tone(f,0.09,'square',0.16,f*0.7);
noise(0.07,0.12,2200+combo*60,1.2);
},
chip(hp){ tone(180+hp*60,0.05,'square',0.09); },
steel(){ tone(1700,0.05,'triangle',0.10,1400); tone(2400,0.04,'triangle',0.06,2000,0.012); },
paddle(){ tone(200,0.09,'triangle',0.26,130); noise(0.04,0.05,700,1); },
wall(){ tone(660,0.035,'square',0.06); },
launch(){ noise(0.12,0.10,900,0.8); tone(480,0.10,'triangle',0.14,760); },
combo(){ tone(880+Math.min(8,combo)*60,0.06,'square',0.10); },
power(){ tone(523,0.07,'triangle',0.16); tone(659,0.07,'triangle',0.16,null,0.07); tone(880,0.10,'triangle',0.16,null,0.14); },
oneUp(){ tone(660,0.08,'triangle',0.16); tone(880,0.08,'triangle',0.16,null,0.09); tone(1046,0.14,'triangle',0.18,null,0.18); },
death(){ tone(300,0.5,'sawtooth',0.24,60); noise(0.3,0.12,300,0.7); },
clear(){ [523,659,784,1046].forEach((f,i)=>tone(f,0.12,'triangle',0.17,null,i*0.09)); },
over(){ [392,311,233].forEach((f,i)=>tone(f,0.3,'sawtooth',0.16,null,i*0.22)); },
};
/* ---------- levels ---------- */
const LEVELS=[
{name:'WARM-UP',rows:[
'...........',
'.111111111.',
'.111111111.',
'.222222222.',
'.111111111.',
'.111111111.',
]},
{name:'THE DIAMOND',rows:[
'.....2.....',
'....222....',
'...23332...',
'..2333332..',
'...23332...',
'....222....',
'.....2.....',
]},
{name:'PILLARS',rows:[
'S.1.1.1.1.S',
'S.2.2.2.2.S',
'S.3.3.3.3.S',
'S.2.2.2.2.S',
'S.1.1.1.1.S',
]},
{name:'CHECKMATE',rows:[
'2.2.2.2.2.2',
'.3.3.3.3.3.',
'2.2.2.2.2.2',
'.3.3.3.3.3.',
'2.2.2.2.2.2',
'.3.3.3.3.3.',
]},
{name:'THE VAULT',rows:[
'33333333333',
'32222222223',
'32S11111S23',
'32222222223',
'33333333333',
]},
];
/* ---------- state ---------- */
let state='menu', paused=false;
let score=0, lives=3, levelIdx=0, loop=0, totalLevel=1, combo=0;
let hi=0; try{ hi=parseInt(localStorage.getItem('brick-riot-hi'))||0; }catch(e){}
let newBest=false;
let shake=0, shakeX=0, shakeY=0, freezeT=0, redFlash=0;
let slowT=0, slowCur=1, wideT=0;
let balls=[], bricks=[], breakable=0, powerups=[], particles=[], popups=[], toasts=[];
let brickAnimT=0, introDur=1.5;
let serveT=0, deadT=0, clearT=0, clearBonus=0, overT=0;
let demoT=0.9, demoLaunched=false, demoRebuildT=1.4;
let tGlobal=0;
const paddle={x:W/2, y:H-60, w:92, h:14, vx:0, squash:0};
const mouse={x:W/2};
const keys={};
/* ---------- level building ---------- */
function buildBricks(idx){
bricks=[]; breakable=0;
const rows=LEVELS[idx].rows;
let maxDelay=0;
for(let r=0;r<rows.length;r++){
for(let c=0;c<11;c++){
const ch=rows[r][c];
if(ch==='.') continue;
const steel=ch==='S';
const hp=steel?0:Math.min(3,parseInt(ch,10)+loop);
const b={col:c,row:r,x:BRICK_X0+c*BSTEP,y:BRICK_Y0+r*BSTEPY,w:BW,h:BH,
hp,maxHp:hp,steel,alive:true,flash:0,delay:r*0.05+c*0.014,cracks:[]};
maxDelay=Math.max(maxDelay,b.delay);
if(!steel){
breakable++;
let s=(r*73+c*137)%97+1;
const rnd=()=>{s=(s*16807)%2147483647;return s/2147483647;};
for(let i=0;i<3;i++){
const x1=4+rnd()*(b.w-8), y1=2+rnd()*(b.h-4);
const x2=clamp(x1+(rnd()-0.5)*22,3,b.w-3), y2=clamp(y1+(rnd()-0.5)*10,1,b.h-1);
b.cracks.push([x1,y1,x2,y2]);
}
}
bricks.push(b);
}
}
introDur=maxDelay+0.75;
brickAnimT=0;
}
/* ---------- game flow ---------- */
function baseSpeed(){ return Math.min(480,330+(totalLevel-1)*13); }
function newBall(){ return {x:paddle.x,y:paddle.y-8,dx:0,dy:-1,speed:baseSpeed(),r:6,trail:[]}; }
function serve(){ balls=[newBall()]; state='serve'; serveT=0; }
function launch(){
const b=balls[0]; if(!b) return;
const a=0.95*Math.sin(serveT*1.9);
b.dx=Math.sin(a); b.dy=-Math.cos(a);
b.speed=baseSpeed();
state='play'; serveT=0;
sfx.launch(); ring(b.x,b.y,COL.bone);
}
function startLevel(i){
levelIdx=i; totalLevel=loop*LEVELS.length+i+1;
buildBricks(i);
balls=[]; powerups=[]; combo=0;
freezeT=0; slowT=0; wideT=0; slowCur=1;
state='intro';
if(loop>0&&i===0) toast('LOOP '+(loop+1)+' SPEED UP');
}
function nextLevel(){
levelIdx++;
if(levelIdx>=LEVELS.length){ levelIdx=0; loop++; }
startLevel(levelIdx);
}
function startGame(){
score=0; lives=3; loop=0; newBest=false;
startLevel(0);
}
function levelCleared(){
state='clear'; clearT=0;
clearBonus=400+lives*200;
score+=clearBonus;
if(score>hi){ hi=score; newBest=true; }
for(const b of balls){ ring(b.x,b.y,COL.bone); sparks(b.x,b.y,COL.bone,6); }
balls.length=0; powerups.length=0;
sfx.clear(); shake=10;
}
function loseLife(){
lives--; redFlash=1; shake=13; combo=0;
sfx.death();
if(lives<0) gameOver();
else { deadT=0.7; state='dead'; }
}
function gameOver(){
state='over'; overT=0;
try{ localStorage.setItem('brick-riot-hi',String(hi)); }catch(e){}
sfx.over();
}
/* ---------- effects ---------- */
function trimParticles(){ if(particles.length>320) particles.splice(0,particles.length-320); }
function burst(br){
const col=COL['b'+br.maxHp]||COL.bone;
const cx=br.x+br.w/2, cy=br.y+br.h/2;
for(let i=0;i<12;i++){
const a=Math.random()*Math.PI*2, sp=60+Math.random()*260;
particles.push({type:'shard',x:cx,y:cy,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-90,g:1000,
rot:Math.random()*Math.PI,vr:(Math.random()-0.5)*12,size:3+Math.random()*5,
color:col,life:0.45+Math.random()*0.45,max:0.9});
}
for(let i=0;i<5;i++){
const a=Math.random()*Math.PI*2, sp=120+Math.random()*260;
particles.push({type:'spark',x:cx,y:cy,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,g:300,
color:COL.bone,life:0.18+Math.random()*0.14,max:0.32});
}
trimParticles();
}
function sparks(x,y,color,n){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2, sp=70+Math.random()*220;
particles.push({type:'spark',x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,g:500,
color,life:0.15+Math.random()*0.12,max:0.27});
}
trimParticles();
}
function ring(x,y,color){ particles.push({type:'ring',x,y,r:5,life:0.28,max:0.28,color}); }
function popup(x,y,text,color){ popups.push({x,y,text,color,life:0.85,max:0.85}); }
function toast(text){ toasts.push({text,life:1.5,max:1.5}); }
/* ---------- power-ups ---------- */
function spawnPowerup(br){
const r=Math.random();
let t='W';
if(r<0.35) t='W'; else if(r<0.65) t='M'; else if(r<0.88) t='S'; else t='L';
powerups.push({x:br.x+br.w/2,y:br.y+br.h/2,type:t,seed:Math.random()*6.28});
}
function multiball(){
const cur=balls.slice();
for(const b of cur){
for(const da of [-0.45,0.45]){
if(balls.length>=6) return;
const a=Math.atan2(b.dy,b.dx)+da;
balls.push({x:b.x,y:b.y,dx:Math.cos(a),dy:Math.sin(a),speed:b.speed,r:6,trail:[]});
}
}
}
function applyPower(t){
if(t==='W'){ wideT=12; popup(paddle.x,paddle.y-30,'WIDE',COL.mint); sfx.power(); }
else if(t==='M'){ multiball(); popup(paddle.x,paddle.y-30,'MULTI ×3',COL.amber); sfx.power(); }
else if(t==='S'){ slowT=8; popup(paddle.x,paddle.y-30,'SLOW-MO',COL.ice); sfx.power(); }
else if(t==='L'){ lives=Math.min(6,lives+1); popup(paddle.x,paddle.y-30,'1UP',COL.coral); sfx.oneUp(); }
}
function updatePowerups(dt){
for(let i=powerups.length-1;i>=0;i--){
const p=powerups[i];
p.y+=150*dt;
if(p.y>H+24){ powerups.splice(i,1); continue; }
if(p.y+9>=paddle.y && p.y-9<=paddle.y+paddle.h+8 && Math.abs(p.x-paddle.x)<paddle.w/2+16){
applyPower(p.type);
ring(p.x,paddle.y-6,POWCOL[p.type]);
powerups.splice(i,1);
}
}
}
/* ---------- physics ---------- */
function hitBrick(br,demo){
if(br.steel){
if(!demo) sfx.steel();
sparks(br.x+br.w/2,br.y+br.h/2,'#a9b0bd',5);
shake=Math.min(16,shake+0.8);
return;
}
br.hp--; br.flash=1;
if(br.hp<=0){
br.alive=false; breakable--;
if(!demo){
combo++;
const pts=(br.maxHp===1?50:br.maxHp===2?80:120)*(1+loop)+combo*10;
score+=pts;
if(score>hi){ hi=score; newBest=true; }
popups.push({x:br.x+br.w/2,y:br.y+br.h/2,text:'+'+pts,color:COL.bone,life:0.7,max:0.7});
if(combo>=3) popups.push({x:br.x+br.w/2,y:br.y+br.h/2-18,text:'×'+combo,color:COL.coral,life:0.9,max:0.9});
if(combo%5===0) sfx.combo();
if(Math.random()<0.14) spawnPowerup(br);
}
burst(br);
shake=Math.min(16,shake+2.4+combo*0.25);
freezeT=Math.min(0.06,0.035+(combo>=5?0.015:0));
if(!demo){
sfx.brick(br.maxHp,br.row,combo);
if(breakable===0) levelCleared();
}
} else {
if(!demo) sfx.chip(br.maxHp);
sparks(br.x+br.w/2,br.y+br.h/2,COL['b'+br.hp]||COL.bone,3);
shake=Math.min(16,shake+0.6);
}
}
function collideBricks(b,demo){
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) continue;
if(dx===0&&dy===0){ b.dy=-b.dy; }
else if(Math.abs(dx)>Math.abs(dy)){
b.dx=Math.abs(b.dx)*(dx<0?-1:1);
b.x=nx+(dx<0?-b.r:b.r);
} else {
b.dy=Math.abs(b.dy)*(dy<0?-1:1);
b.y=ny+(dy<0?-b.r:b.r);
}
b.dx+=(Math.random()-0.5)*0.02; b.dy+=(Math.random()-0.5)*0.008;
const m=Math.hypot(b.dx,b.dy); b.dx/=m; b.dy/=m;
hitBrick(br,demo);
return;
}
}
function collidePaddle(b,demo){
if(b.dy<=0) return;
if(b.y+b.r<paddle.y||b.y-b.r>paddle.y+paddle.h+8) return;
if(Math.abs(b.x-paddle.x)>paddle.w/2+b.r*0.85) return;
b.y=paddle.y-b.r;
const rel=clamp((b.x-paddle.x)/(paddle.w/2),-1,1);
const ang=rel*1.05;
let vx=Math.sin(ang)*b.speed+clamp(paddle.vx,-700,700)*0.22;
let vy=-Math.cos(ang)*b.speed;
let m=Math.hypot(vx,vy);
b.dx=vx/m; b.dy=vy/m;
if(Math.abs(b.dy)<0.32){
b.dy=-0.32; const s=b.dx<0?-1:1; b.dx=s*Math.sqrt(1-0.1024);
m=Math.hypot(b.dx,b.dy); b.dx/=m; b.dy/=m;
}
b.speed=Math.min(560,b.speed*1.015+1.5);
paddle.squash=1; combo=0;
sparks(b.x,paddle.y,COL.bone,5);
if(!demo){ sfx.paddle(); shake=Math.min(14,shake+1.1); }
}
function physicsStep(dt,demo){
if(freezeT>0){ freezeT-=dt; return; }
const spdMul=slowCur;
for(let i=balls.length-1;i>=0;i--){
const b=balls[i];
const spd=b.speed*spdMul;
const steps=Math.max(1,Math.ceil(spd*dt/5));
const sdt=dt/steps;
for(let s=0;s<steps;s++){
b.x+=b.dx*spd*sdt; b.y+=b.dy*spd*sdt;
if(b.x-b.r<WALL){ b.x=WALL+b.r; b.dx=Math.abs(b.dx); if(!demo) sfx.wall(); }
if(b.x+b.r>W-WALL){ b.x=W-WALL-b.r; b.dx=-Math.abs(b.dx); if(!demo) sfx.wall(); }
if(b.y-b.r<TOP){ b.y=TOP+b.r; b.dy=Math.abs(b.dy); if(!demo) sfx.wall(); }
if(Math.abs(b.dy)<0.06){
b.dy=0.06*(b.dy<0?-1:1);
const m=Math.hypot(b.dx,b.dy); b.dx/=m; b.dy/=m;
}
collidePaddle(b,demo);
collideBricks(b,demo);
if(state==='clear'||state==='dead'||state==='over') return;
if(b.y-b.r>H+10){
balls.splice(i,1);
if(demo) demoT=0.6;
else if(balls.length===0) loseLife();
return;
}
}
b.trail.push({x:b.x,y:b.y});
if(b.trail.length>11) b.trail.shift();
}
}
/* ---------- paddle ---------- */
function movePaddle(dt){
const kb=(keys['ArrowLeft']||keys['KeyA']?-1:0)+(keys['ArrowRight']||keys['KeyD']?1:0);
const px=paddle.x;
if(kb!==0){ paddle.x+=kb*640*dt; mouse.x=paddle.x; }
else { paddle.x+=(mouse.x-paddle.x)*Math.min(1,dt*16); }
paddle.x=clamp(paddle.x,WALL+paddle.w/2+2,W-WALL-paddle.w/2-2);
paddle.vx=clamp((paddle.x-px)/Math.max(dt,1e-4),-700,700);
}
/* ---------- update ---------- */
function updateParticles(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; }
if(p.type==='ring'){ p.r+=140*dt; continue; }
p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=p.g*dt; p.rot+=p.vr*dt;
}
}
function updatePopups(dt){
for(let i=popups.length-1;i>=0;i--){
const p=popups[i];
p.y-=48*dt; p.life-=dt;
if(p.life<=0) popups.splice(i,1);
}
}
function updateToasts(dt){
for(let i=toasts.length-1;i>=0;i--){
toasts[i].life-=dt;
if(toasts[i].life<=0) toasts.splice(i,1);
}
}
function updateMenu(dt){
if(balls.length===0){
demoT-=dt;
if(demoT<=0){ balls=[newBall()]; demoLaunched=false; demoT=0.55; }
}
const b0=balls[0];
if(b0&&!demoLaunched){
b0.x=paddle.x; b0.y=paddle.y-b0.r-2;
demoT-=dt;
if(demoT<=0){
const a=Math.random()*1.4-0.7;
b0.dx=Math.sin(a); b0.dy=-Math.cos(a); b0.speed=340;
demoLaunched=true;
}
} else if(b0){
const target=clamp(b0.x+b0.dx*30, WALL+paddle.w/2+2, W-WALL-paddle.w/2-2);
const px=paddle.x, mv=540*dt;
paddle.x+=clamp(target-paddle.x,-mv,mv);
paddle.vx=clamp((paddle.x-px)/Math.max(dt,1e-4),-700,700);
}
physicsStep(dt,true);
if(breakable===0){
demoRebuildT-=dt;
if(demoRebuildT<=0){ buildBricks(0); demoRebuildT=1.4; }
}
}
function update(dt){
shake*=Math.exp(-8*dt); if(shake<0.04) shake=0;
shakeX=(Math.random()*2-1)*shake; shakeY=(Math.random()*2-1)*shake;
redFlash=Math.max(0,redFlash-1.6*dt);
brickAnimT+=dt;
for(const br of bricks) if(br.flash>0) br.flash=Math.max(0,br.flash-dt*6);
updateParticles(dt); updatePopups(dt); updateToasts(dt);
if(wideT>0) wideT-=dt;
if(slowT>0) slowT-=dt;
slowCur+=((slowT>0?0.6:1)-slowCur)*Math.min(1,dt*7);
paddle.w+=((wideT>0?150:92)-paddle.w)*Math.min(1,dt*9);
paddle.squash=Math.max(0,paddle.squash-dt*6.5);
switch(state){
case 'menu': updateMenu(dt); break;
case 'intro':
movePaddle(dt);
if(brickAnimT>=introDur) serve();
break;
case 'serve':
movePaddle(dt); serveT+=dt;
if(balls[0]){ balls[0].x=paddle.x; balls[0].y=paddle.y-balls[0].r-2; }
break;
case 'play':
movePaddle(dt); physicsStep(dt,false); updatePowerups(dt);
break;
case 'dead': movePaddle(dt); deadT-=dt; if(deadT<=0) serve(); break;
case 'clear': movePaddle(dt); clearT+=dt; break;
case 'over': overT+=dt; break;
}
}
/* ---------- rendering ---------- */
function drawGrid(){
ctx.fillStyle='rgba(240,235,220,0.05)';
for(let x=WALL+17;x<W-WALL;x+=34)
for(let y=TOP+17;y<H;y+=34)
ctx.fillRect(x-1,y-1,2,2);
}
function drawWalls(){
ctx.fillStyle=COL.wall;
ctx.fillRect(0,HUD_H,WALL,H-HUD_H);
ctx.fillRect(W-WALL,HUD_H,WALL,H-HUD_H);
ctx.fillRect(0,HUD_H,W,WALL);
ctx.fillStyle='rgba(240,235,220,0.10)';
ctx.fillRect(WALL,TOP,1.5,H-TOP);
ctx.fillRect(W-WALL-1.5,TOP,1.5,H-TOP);
ctx.fillRect(WALL,TOP,W-2*WALL,1.5);
}
function drawBricks(){
for(const b of bricks){
if(!b.alive) continue;
let y=b.y, e=1;
const ap=clamp((brickAnimT-b.delay)/0.5,0,1);
if(ap<1){ e=easeOut(ap); y=b.y-(1-e)*240; }
ctx.globalAlpha=e;
const color=b.steel?COL.steel:COL['b'+Math.min(3,b.hp)];
rr(b.x,y,b.w,b.h,4); ctx.fillStyle=color; ctx.fill();
if(b.steel){
ctx.strokeStyle=COL.steelEdge; ctx.lineWidth=1.5;
rr(b.x+1.5,y+1.5,b.w-3,b.h-3,3); ctx.stroke();
ctx.fillStyle='rgba(240,235,220,0.28)';
ctx.fillRect(b.x+4,y+b.h/2-1,2,2);
ctx.fillRect(b.x+b.w-6,y+b.h/2-1,2,2);
} else {
ctx.fillStyle='rgba(255,255,255,0.22)';
rr(b.x+3,y+2.5,b.w-6,2.5,1.2); ctx.fill();
ctx.fillStyle='rgba(0,0,0,0.18)';
rr(b.x+3,y+b.h-4.5,b.w-6,2.5,1.2); ctx.fill();
if(b.hp<b.maxHp){
ctx.strokeStyle='rgba(10,11,15,0.5)'; ctx.lineWidth=1.4;
ctx.beginPath();
for(const seg of b.cracks){
ctx.moveTo(b.x+seg[0],y+seg[1]);
ctx.lineTo(b.x+seg[2],y+seg[3]);
}
ctx.stroke();
}
}
if(b.flash>0){
ctx.fillStyle='rgba(255,255,255,'+(b.flash*0.85)+')';
rr(b.x-1,y-1,b.w+2,b.h+2,5); ctx.fill();
}
ctx.globalAlpha=1;
}
}
function drawPowerups(){
for(const p of powerups){
ctx.save();
ctx.translate(p.x,p.y);
ctx.rotate(Math.sin(tGlobal*3+p.seed)*0.12);
rr(-18,-9,36,18,9); ctx.fillStyle=POWCOL[p.type]; ctx.fill();
rr(-15,-6,30,3,1.5); ctx.fillStyle='rgba(255,255,255,0.3)'; ctx.fill();
drawText(POWLET[p.type],0,-6.6,1.9,'#15161c','center');
ctx.restore();
}
}
function drawParticles(){
for(const p of particles){
const a=clamp(p.life/p.max,0,1);
if(p.type==='ring'){
ctx.globalAlpha=a*0.7; ctx.strokeStyle=p.color; ctx.lineWidth=2.5;
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,7); ctx.stroke();
} else if(p.type==='spark'){
ctx.globalAlpha=a; ctx.strokeStyle=p.color; ctx.lineWidth=1.8;
ctx.beginPath();
ctx.moveTo(p.x,p.y);
ctx.lineTo(p.x-p.vx*0.035,p.y-p.vy*0.035);
ctx.stroke();
} else {
ctx.globalAlpha=a;
ctx.save();
ctx.translate(p.x,p.y); ctx.rotate(p.rot);
const s=p.size*(0.4+0.6*a);
ctx.fillStyle=p.color;
ctx.fillRect(-s/2,-s/2,s,s*0.7);
ctx.restore();
}
}
ctx.globalAlpha=1;
}
function drawBall(b,moving){
for(let i=0;i<b.trail.length;i++){
const t=b.trail[i], k=(i+1)/b.trail.length;
ctx.globalAlpha=k*0.14;
ctx.fillStyle=slowCur<0.95?'#78e1c3':'#f0ebdc';
ctx.beginPath(); ctx.arc(t.x,t.y,b.r*k*0.85,0,7); ctx.fill();
}
ctx.globalAlpha=1;
ctx.save();
ctx.translate(b.x,b.y);
if(moving){
ctx.rotate(Math.atan2(b.dy,b.dx));
const st=1+Math.min(0.3,b.speed/2400);
ctx.scale(st,1/st);
} else {
const pu=1+0.06*Math.sin(tGlobal*7);
ctx.scale(pu,pu);
}
ctx.fillStyle=COL.bone;
ctx.beginPath(); ctx.arc(0,0,b.r,0,7); ctx.fill();
ctx.fillStyle='rgba(10,11,15,0.14)';
ctx.beginPath(); ctx.arc(b.r*0.32,b.r*0.32,b.r*0.5,0,7); ctx.fill();
ctx.restore();
}
function drawAim(b){
const a=0.95*Math.sin(serveT*1.9);
const dx=Math.sin(a), dy=-Math.cos(a);
ctx.fillStyle=COL.bone;
for(let i=1;i<=8;i++){
const d=16+i*13;
const s=Math.max(1.2,3.4-i*0.28);
ctx.globalAlpha=clamp(0.55-i*0.05+0.15*Math.sin(tGlobal*6+i),0,1);
ctx.fillRect(b.x+dx*d-s/2,b.y+dy*d-s/2,s,s);
}
ctx.globalAlpha=1;
}
function drawBalls(){
const moving=state==='play'||state==='menu';
for(const b of balls) drawBall(b,moving);
if(state==='serve'&&balls[0]) drawAim(balls[0]);
}
function drawPaddle(){
const k=paddle.squash;
const w=paddle.w*(1+0.16*k), h=paddle.h*(1-0.28*k);
const x=paddle.x-w/2, y=paddle.y+(paddle.h-h);
rr(x,y,w,h,7); ctx.fillStyle=COL.bone; ctx.fill();
ctx.fillStyle='rgba(0,0,0,0.22)';
rr(x+2,y+h-3,w-4,2.2,1.1); ctx.fill();
const tipCol=wideT>0?COL.mint:COL.amber;
const tipA=(wideT>0&&wideT<1.8)?0.45+0.55*Math.abs(Math.sin(tGlobal*9)):1;
ctx.globalAlpha=tipA; ctx.fillStyle=tipCol;
rr(x+3,y+2.5,7,h-5,2.5); ctx.fill();
rr(x+w-10,y+2.5,7,h-5,2.5); ctx.fill();
ctx.globalAlpha=1;
ctx.fillStyle=COL.coral;
rr(paddle.x-2,y+2,4,4,1.5); ctx.fill();
}
function drawPopups(){
for(const p of popups){
ctx.globalAlpha=clamp(p.life/p.max,0,1);
drawText(p.text,p.x,p.y,1.7,p.color,'center');
}
ctx.globalAlpha=1;
}
function drawHUD(){
ctx.fillStyle='rgba(240,235,220,0.06)';
ctx.fillRect(0,HUD_H-1,W,1);
drawText('SCORE',18,12,1.4,COL.dim,'left');
drawText(pad6(score),18,26,2.6,COL.bone,'left');
drawText('LEVEL',W/2,12,1.4,COL.dim,'center');
drawText(pad2(totalLevel),W/2,26,2.6,COL.bone,'center');
drawText('HI',W-18,12,1.4,COL.dim,'right');
drawText(pad6(hi),W-18,26,2.6,COL.bone,'right');
const n=Math.max(0,lives);
if(n>0){
const iw=16,g=6,tw=n*iw+(n-1)*g;
let lx=W/2-tw/2;
ctx.fillStyle=COL.bone;
for(let i=0;i<n;i++){ rr(lx+i*(iw+g),54,iw,4,2); ctx.fill(); }
}
if(combo>=3){
ctx.globalAlpha=0.7+0.3*Math.sin(tGlobal*8);
drawText('COMBO ×'+combo,18,50,1.6,COL.coral,'left');
ctx.globalAlpha=1;
}
if(muted) drawText('MUTED',W-18,52,1.4,COL.dim,'right');
if(state==='serve'||state==='play'){
ctx.globalAlpha=0.4;
drawText('MOUSE MOVE · SPACE LAUNCH · P PAUSE · M SOUND',W/2,H-16,1.35,COL.dim,'center');
ctx.globalAlpha=1;
}
}
function drawToasts(){
for(const t of toasts){
ctx.globalAlpha=Math.min(1,t.life/0.35);
drawText(t.text,W/2,112-(t.max-t.life)*14,1.8,COL.amber,'center');
}
ctx.globalAlpha=1;
}
function drawOverlays(){
const blink=0.4+0.6*Math.max(0,Math.sin(tGlobal*4));
if(paused){
ctx.fillStyle='rgba(10,11,15,0.66)'; ctx.fillRect(0,0,W,H);
drawText('PAUSED',W/2,320,4,COL.bone,'center');
ctx.globalAlpha=blink;
drawText('P OR ESC TO RESUME',W/2,392,1.7,COL.dim,'center');
ctx.globalAlpha=1;
return;
}
if(state==='menu'){
ctx.fillStyle='rgba(11,12,17,0.62)'; ctx.fillRect(0,0,W,H);
if(hi>0) drawText('HI SCORE '+pad6(hi),W/2,116,1.6,COL.dim,'center');
drawText('BRICK',W/2,170,12.5,COL.bone,'center');
drawText('RIOT',W/2,272,12.5,COL.coral,'center');
ctx.fillStyle=COL.coral;
ctx.fillRect(W/2-64,370,128,2.5);
drawText('MOUSE OR ARROW KEYS TO MOVE',W/2,428,1.7,COL.dim,'center');
drawText('SPACE OR CLICK TO LAUNCH',W/2,456,1.7,COL.dim,'center');
drawText('P PAUSE · M SOUND',W/2,484,1.7,COL.dim,'center');
ctx.globalAlpha=blink;
drawText('PRESS SPACE TO START',W/2,546,2.4,COL.coral,'center');
ctx.globalAlpha=1;
return;
}
if(state==='intro'){
const aIn=clamp(brickAnimT/0.25,0,1);
const aOut=clamp((introDur-brickAnimT)/0.35,0,1);
ctx.globalAlpha=aIn*aOut;
drawText('LEVEL '+pad2(totalLevel),W/2,296,3.2,COL.bone,'center');
drawText(LEVELS[levelIdx].name,W/2,346,2.2,COL.coral,'center');
ctx.globalAlpha=1;
}
if(state==='serve'){
ctx.globalAlpha=blink;
drawText('SPACE OR CLICK TO LAUNCH',W/2,636,1.7,COL.dim,'center');
ctx.globalAlpha=1;
}
if(state==='clear'){
ctx.fillStyle='rgba(10,11,15,0.55)'; ctx.fillRect(0,0,W,H);
drawText('LEVEL '+pad2(totalLevel)+' CLEAR',W/2,286,3,COL.bone,'center');
drawText('BONUS +'+clearBonus,W/2,344,2.2,COL.amber,'center');
drawText('SCORE '+pad6(score),W/2,388,1.8,COL.dim,'center');
if(clearT>0.55){
ctx.globalAlpha=blink;
drawText('SPACE TO CONTINUE',W/2,452,2,COL.bone,'center');
ctx.globalAlpha=1;
}
}
if(state==='over'){
ctx.fillStyle='rgba(10,11,15,0.7)'; ctx.fillRect(0,0,W,H);
drawText('GAME OVER',W/2,270,4.2,COL.coral,'center');
drawText('SCORE '+pad6(score),W/2,348,2.2,COL.bone,'center');
if(newBest){
ctx.globalAlpha=blink;
drawText('NEW BEST!',W/2,392,2,COL.amber,'center');
ctx.globalAlpha=1;
} else {
drawText('BEST '+pad6(hi),W/2,392,1.8,COL.dim,'center');
}
drawText('REACHED LEVEL '+pad2(totalLevel),W/2,428,1.6,COL.dim,'center');
if(overT>0.7){
ctx.globalAlpha=blink;
drawText('SPACE TO RETRY',W/2,492,2,COL.bone,'center');
ctx.globalAlpha=1;
}
}
}
function render(){
ctx.setTransform(DPR,0,0,DPR,0,0);
ctx.fillStyle=COL.bg; ctx.fillRect(0,0,W,H);
ctx.save();
ctx.translate(shakeX,shakeY);
drawGrid();
drawWalls();
drawBricks();
drawPowerups();
drawParticles();
drawBalls();
drawPaddle();
drawPopups();
if(state!=='menu') drawHUD();
ctx.restore();
drawOverlays();
drawToasts();
if(redFlash>0){
ctx.fillStyle='rgba(255,70,60,'+(redFlash*0.22)+')';
ctx.fillRect(0,H-70,W,70);
ctx.fillStyle='rgba(255,70,60,'+(redFlash*0.55)+')';
ctx.fillRect(0,H-3,W,3);
}
}
/* ---------- input ---------- */
function press(){
if(paused){ paused=false; return; }
switch(state){
case 'menu': startGame(); break;
case 'intro': brickAnimT=Math.max(brickAnimT,introDur); serve(); break;
case 'serve': launch(); break;
case 'clear': if(clearT>0.55) nextLevel(); break;
case 'over': if(overT>0.7) startGame(); break;
}
}
window.addEventListener('keydown',e=>{
if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Space'].includes(e.code)) e.preventDefault();
initAudio();
if(e.repeat) return;
keys[e.code]=true;
if(e.code==='KeyM'){ muted=!muted; toast(muted?'SOUND OFF':'SOUND ON'); }
if(e.code==='KeyP'||e.code==='Escape'){
if(state==='play'||state==='serve'||state==='dead') paused=!paused;
}
if(e.code==='Space'||e.code==='Enter') press();
});
window.addEventListener('keyup',e=>{ keys[e.code]=false; });
window.addEventListener('pointermove',e=>{
const r=cv.getBoundingClientRect();
mouse.x=(e.clientX-r.left)*W/r.width;
});
cv.addEventListener('pointerdown',e=>{ e.preventDefault(); initAudio(); press(); });
window.addEventListener('blur',()=>{
if(state==='play'||state==='serve'||state==='dead') paused=true;
});
/* ---------- boot ---------- */
buildBricks(0);
let last=performance.now();
function frame(now){
const dt=Math.min(0.033,(now-last)/1000);
last=now;
tGlobal+=dt;
if(!paused) update(dt);
render();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>

