Run details
- Model
- z-ai/glm-5.3-flashx-legal
- Route
- OpenRouter API, one user message
- Served by
- Z.AI
- Effort
- max, OpenRouter's default for this model
- Limits
- 60 min · 131,072 output tokens (the model's limit)
- Date
- 2 Nov 2026
- Time
- 11 min 20 s
- Size
- 33 kB
- Tokens
- 88 in, 83,992 out (71,077 of them thinking)
- List price
- $0.11 at today's price ($0.37 in / $1.25 out per M)
- Paid
- $0.11
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 901 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">
<meta name="theme-color" content="#F3EDE0">
<title>MISPRINT · a breakout in ink & paper</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden}
body{background:#F3EDE0;user-select:none;-webkit-user-select:none}
#c{display:block;width:100vw;height:100vh;touch-action:none}
#grain{position:fixed;inset:0;pointer-events:none;opacity:.09;mix-blend-mode:multiply}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="grain" aria-hidden="true"></div>
<script>
'use strict';
/* ---------- roundRect fallback ---------- */
if(!CanvasRenderingContext2D.prototype.roundRect){
CanvasRenderingContext2D.prototype.roundRect=function(x,y,w,h,r){
r=Math.min(r,Math.abs(w)/2,Math.abs(h)/2);
this.moveTo(x+r,y);
this.arcTo(x+w,y,x+w,y+h,r);
this.arcTo(x+w,y+h,x,y+h,r);
this.arcTo(x,y+h,x,y,r);
this.arcTo(x,y,x+w,y,r);
this.closePath();
return this;
};
}
/* ---------- paper grain (generated, no image files) ---------- */
(function(){
const n=document.createElement('canvas'); n.width=n.height=140;
const g=n.getContext('2d'), id=g.createImageData(140,140);
for(let i=0;i<id.data.length;i+=4){
const v=185+(Math.random()*70|0);
id.data[i]=v; id.data[i+1]=v; id.data[i+2]=v; id.data[i+3]=255;
}
g.putImageData(id,0,0);
document.getElementById('grain').style.backgroundImage='url('+n.toDataURL()+')';
})();
/* ---------- constants ---------- */
const FW=540, FH=720, HUD=56;
const COLS=11, BGAP=5, BH=20, BWALL=12;
const BW=(FW-BWALL*2-(COLS-1)*BGAP)/COLS;
const BTOP=HUD+36;
const PADDLE_Y=FH-46;
const INK='#181410', PAPER='#F3EDE0', PAPER2='#F9F4E8';
const ACCENT='#E8481F', MUSTARD='#E9A820', TEAL='#1E7A6F', BLUE='#2E5EA8';
const ROWC=[ACCENT,MUSTARD,TEAL,BLUE];
const PAL=[ACCENT,MUSTARD,TEAL,BLUE,INK];
const DISP='Futura,"Avenir Next","Century Gothic","Trebuchet MS",sans-serif';
const MONO='Menlo,Consolas,"DejaVu Sans Mono","Courier New",monospace';
const POWC={wide:ACCENT,multi:TEAL,slow:BLUE,pts:MUSTARD,life:ACCENT};
const POWL={wide:'W',multi:'M',slow:'S',pts:'500',life:'+'};
const LBL={wide:'WIDE',multi:'TRIPLE',slow:'SLOW',pts:'+500',life:'+1 LIFE'};
const LCOL={wide:ACCENT,multi:TEAL,slow:BLUE,pts:'#B47E10',life:ACCENT};
const LEVELS=[
{name:'INDEX',rows:[
'aaaaaaaaaaa','bbbbbbbbbbb','ccccccccccc','ddddddddddd','bbbbbbbbbbb','aaaaaaaaaaa']},
{name:'FREQUENCY',rows:[
'a.a.a.a.a.a','.b.b.b.b.b.','aTa.a.a.aTa','.b.b.b.b.b.','c.c.c.c.c.c','.d.d.d.d.d.']},
{name:'SIGNAL',rows:[
'.....b.....','....bbb....','...bbbbb...','..bbTdTbb..','.bbbdbdbbb.',
'..bbTdTbb..','...bbbbb...','....bbb....','.....b.....']},
{name:'STATIC',rows:[
'a.b.T.d.c.b','.d.c.a.T.b.','b.d.b.a.d.c','.c.a.d.b.T.','d.b.c.a.c.a','.T.d.b.c.d.']},
{name:'MONOLITH',rows:[
'.....a.....','....aTa....','...aSSSa...','..aTSSSTa..','.aSSSSSSSa.','aTTTTTTTTTa']},
{name:'BARCODE',rows:[
'a.a.a.a.a.a','aaaaaaaaaaa','b.b.b.b.b.b','bbbbbbbbbbb','c.c.c.c.c.c','ccccccccccc']},
{name:'THE PRESS',rows:[
'TTTTTTTTTTT','.a.b.c.d.a.','T.T.T.T.T.T','.d.c.b.a.d.','S.S.S.S.S.S','.b.a.d.c.b.']},
];
/* ---------- state ---------- */
let W=0,H=0,dpr=1,scl=1,ox=0,oy=0;
let state='title',paused=false,muted=false;
let time=0,timescale=1,slowmoT=0,freezeT=0;
let trauma=0,flash=0,scorePunch=0;
let score=0,lives=3,level=0,combo=0,mult=1;
let hi=0,hiStart=0,hiBeaten=false;
try{ hi=parseInt(localStorage.getItem('misprint_hi'),10)||0; }catch(err){}
let bricks=[],balls=[],pows=[],parts=[],texts=[];
let remaining=0;
let mouseX=null,lastInput='key';
let introT=0,introHit=false,readyT=0;
let clearT=0,bonused=false,lastBonus=0;
let overT=0,overHit=false,winT=0,winHit=false,winAcc=0;
let wideUntil=0,slowUntil=0;
const keys={left:false,right:false};
const paddle={x:FW/2,w:92,wt:92,vx:0,sq:0,tilt:0};
const MUTE={x:FW-86,y:FH-30,w:76,h:20};
/* ---------- tiny helpers ---------- */
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rand=(a,b)=>a+Math.random()*(b-a);
const pick=a=>a[(Math.random()*a.length)|0];
const pad=n=>String(Math.max(0,n|0)).padStart(6,'0');
function rr(x,y,w,h,r){ ctx.beginPath(); ctx.roundRect(x,y,w,h,r); }
function line(x1,y1,x2,y2,w,col){
ctx.strokeStyle=col||INK; ctx.lineWidth=w||2;
ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke();
}
function text(str,x,y,size,font,col,align,ls,alpha,weight){
ctx.save();
ctx.globalAlpha=alpha==null?1:alpha;
ctx.fillStyle=col;
ctx.font=(weight||'700')+' '+size+'px '+font;
ctx.textAlign=align||'center';
ctx.textBaseline='middle';
try{ ctx.letterSpacing=(ls||0)+'px'; }catch(err){}
ctx.fillText(str,x,y);
ctx.restore();
}
function misPrint(str,x,y,size){ // misregistration: the whole identity
ctx.save();
ctx.font='800 '+size+'px '+DISP;
ctx.textAlign='center'; ctx.textBaseline='middle';
try{ ctx.letterSpacing='2px'; }catch(err){}
const o=2.4+Math.sin(time*2.1)*1.1;
ctx.fillStyle=TEAL; ctx.fillText(str,x+o,y-o*0.65);
ctx.fillStyle=ACCENT; ctx.fillText(str,x-o,y+o*0.65);
ctx.fillStyle=INK; ctx.fillText(str,x,y);
ctx.restore();
}
function vtext(str,x,y,size,col,dir){
ctx.save(); ctx.translate(x,y); ctx.rotate(dir<0?-Math.PI/2:Math.PI/2);
text(str,0,0,size,DISP,col,'center',3,1,'700');
ctx.restore();
}
function dim(a){ ctx.fillStyle='rgba(243,237,224,'+a+')'; ctx.fillRect(0,0,FW,FH); }
/* ---------- audio (all synthesized) ---------- */
let actx=null,master=null;
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.4; master.connect(actx.destination);
}catch(err){}
}
function beep(freq,dur,type,vol,slideTo,when){
if(!actx||muted) return;
const t=actx.currentTime+(when||0);
const o=actx.createOscillator(), g=actx.createGain();
o.type=type; o.frequency.setValueAtTime(freq,t);
if(slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(20,slideTo),t+dur);
g.gain.setValueAtTime(0.0001,t);
g.gain.exponentialRampToValueAtTime(vol,t+0.008);
g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
o.connect(g); g.connect(master); o.start(t); o.stop(t+dur+0.05);
}
function hiss(dur,vol,when){
if(!actx||muted) return;
const t=actx.currentTime+(when||0), len=Math.max(1,actx.sampleRate*dur|0);
const buf=actx.createBuffer(1,len,actx.sampleRate), d=buf.getChannelData(0);
for(let i=0;i<len;i++) d[i]=(Math.random()*2-1)*(1-i/len);
const s=actx.createBufferSource(); s.buffer=buf;
const f=actx.createBiquadFilter(); f.type='highpass'; f.frequency.value=900;
const g=actx.createGain(); g.gain.value=vol;
s.connect(f); f.connect(g); g.connect(master); s.start(t);
}
const sfx={
pop(c){ const f=280*Math.pow(2,Math.min(c,14)/12); beep(f,0.1,'square',0.32); hiss(0.05,0.1); },
thud(){ beep(150,0.07,'triangle',0.38,90); },
steel(){ beep(92,0.05,'square',0.22); beep(184,0.035,'square',0.12); },
paddle(){ beep(105,0.12,'sine',0.5,65); hiss(0.03,0.06); },
wall(){ beep(640,0.035,'triangle',0.1); },
pow(){ beep(523,0.07,'square',0.22); beep(784,0.1,'square',0.22,0,0.07); },
life(){ beep(659,0.09,'sine',0.28); beep(880,0.12,'sine',0.28,0,0.08); },
lose(){ beep(300,0.5,'sawtooth',0.3,55); },
launch(){ beep(180,0.16,'square',0.22,460); },
clear(){ [523,659,784,1046].forEach((f,i)=>beep(f,0.13,'square',0.26,0,i*0.09)); },
over(){ [392,330,262,196].forEach((f,i)=>beep(f,0.2,'triangle',0.28,0,i*0.15)); },
start(){ beep(392,0.09,'square',0.22); beep(523,0.11,'square',0.22,0,0.08); },
stamp(){ beep(70,0.16,'sine',0.6,38); hiss(0.05,0.18); },
};
function toggleMute(){ muted=!muted; if(!muted) beep(660,0.06,'square',0.18); }
/* ---------- entities & fx ---------- */
const makeBall=(x,y,vx,vy)=>({x,y,vx,vy,r:6,trail:[],sq:0,stuck:false});
function addText(x,y,txt,col,size,mono){
texts.push({x,y,txt,t:0,life:0.85,col,size,mono});
if(texts.length>28) texts.shift();
}
function addScore(n,x,y,col){
score+=n; scorePunch=1;
if(score>hi) hi=score;
if(x!=null) addText(x,y,'+'+n,col||INK,12,true);
}
function addPart(p){ parts.push(p); if(parts.length>380) parts.shift(); }
function fleckBurst(x,y,col,n,sp){
for(let i=0;i<n;i++){
const a=rand(0,Math.PI*2), s=rand(sp*0.4,sp);
addPart({type:'fleck',x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-60,t:0,life:rand(0.3,0.6),col,sz:rand(2.5,5),rot:rand(0,6)});
}
}
function burstBrick(k){
const cx=k.x+BW/2, cy=k.y+BH/2;
for(let i=0;i<9;i++){
const a=rand(0,Math.PI*2), s=rand(70,330);
addPart({type:'fleck',x:cx,y:cy,vx:Math.cos(a)*s,vy:Math.sin(a)*s-90,t:0,life:rand(0.35,0.75),col:Math.random()<0.7?k.col:INK,sz:rand(3,7),rot:rand(0,6)});
}
addPart({type:'ring',x:cx,y:cy,t:0,life:0.32,col:k.col,r0:8,r1:36});
}
function burstConf(x,y,n){
for(let i=0;i<n;i++){
const a=rand(0,Math.PI*2), s=rand(90,380);
addPart({type:'conf',x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-120,t:0,life:rand(1.2,2.2),col:pick(PAL),sz:rand(4,7),rot:rand(0,6),ph:rand(0,6)});
}
}
function spawnConfetti(n){
for(let i=0;i<n;i++)
addPart({type:'conf',x:rand(30,FW-30),y:-12,vx:rand(-40,40),vy:rand(70,190),t:0,life:rand(1.8,3),col:pick(PAL),sz:rand(4,7),rot:rand(0,6),ph:rand(0,6)});
}
/* ---------- game flow ---------- */
function saveHi(){ try{ localStorage.setItem('misprint_hi',String(hi)); }catch(err){} }
addEventListener('beforeunload',saveHi);
function loadLevel(i){
bricks=[]; remaining=0; pows=[]; texts=[]; parts=[];
const rows=LEVELS[i].rows;
for(let r=0;r<rows.length;r++){
for(let c=0;c<COLS;c++){
const ch=rows[r][c];
if(!ch||ch==='.') continue;
const b={x:BWALL+c*(BW+BGAP),y:BTOP+r*(BH+BGAP),hp:1,steel:false,tough:false,col:ROWC[0],dead:false};
if(ch==='S') b.steel=true;
else if(ch==='T'){ b.tough=true; b.hp=2; b.col=INK; }
else b.col=ROWC['abcd'.indexOf(ch)]||ROWC[0];
bricks.push(b);
if(!b.steel) remaining++;
}
}
balls=[makeBall(paddle.x,PADDLE_Y-13,0,0)];
balls[0].stuck=true;
paddle.wt=92; wideUntil=0; slowUntil=0;
combo=0; mult=1;
}
function startGame(){
score=0; lives=3; level=0; hiStart=hi; hiBeaten=false;
loadLevel(0);
introT=0; introHit=false; state='intro'; paused=false;
sfx.start();
}
function nextLevel(){
level++;
if(level>=LEVELS.length){
state='win'; winT=0; winHit=false; winAcc=0;
hiBeaten=score>hiStart;
hi=Math.max(hi,score); saveHi(); sfx.clear();
}else{
loadLevel(level);
introT=0; introHit=false; state='intro'; sfx.start();
}
}
function toTitle(){ paused=false; state='title'; initAttract(); }
function launch(){
let b=null;
for(const x of balls) if(x.stuck){ b=x; break; }
if(!b) return;
b.stuck=false;
const sp=320+level*14;
const a=clamp((paddle.vx||0)*0.0007,-0.5,0.5)+rand(-0.12,0.12);
b.vx=Math.sin(a)*sp; b.vy=-Math.cos(a)*sp;
paddle.sq=1; state='play'; sfx.launch();
}
function action(){
if(paused){ paused=false; return; }
if(state==='title') startGame();
else if(state==='intro') introT=2;
else if(state==='ready') launch();
else if(state==='clear'){ if(clearT>0.8) clearT=2.45; }
else if(state==='over'&&overT>0.6) startGame();
else if(state==='win'&&winT>0.8) toTitle();
}
function togglePause(){ if(state==='play'||state==='ready') paused=!paused; }
function loseLife(){
lives--; combo=0; mult=1;
pows.length=0; wideUntil=0; slowUntil=0; paddle.wt=92;
trauma=0.7; flash=0.45; slowmoT=0.28;
sfx.lose();
addText(paddle.x,PADDLE_Y-30,'BALL LOST',ACCENT,14,false);
if(lives<=0){
state='over'; overT=0; overHit=false;
hiBeaten=score>hiStart;
hi=Math.max(hi,score); saveHi();
sfx.over();
if(hiBeaten) burstConf(FW/2,FH/2,60);
}else{
balls=[makeBall(paddle.x,PADDLE_Y-13,0,0)];
balls[0].stuck=true;
state='ready'; readyT=0;
}
}
function onClear(){
slowmoT=0.55;
freezeT=Math.max(freezeT,0.1);
trauma=Math.min(1,trauma+0.45);
state='clear'; clearT=0; bonused=false;
}
/* ---------- gameplay ---------- */
function maybeDrop(k){
if(Math.random()>=0.14) return;
const r=Math.random();
const type=r<0.30?'wide':r<0.52?'multi':r<0.74?'slow':r<0.90?'pts':'life';
pows.push({x:k.x+BW/2,y:k.y+BH/2,type,t:0,ph:rand(0,6)});
}
function applyPow(type){
if(type==='wide') wideUntil=time+12;
else if(type==='slow') slowUntil=time+6;
else if(type==='life'){
if(lives<5) lives++;
else { score+=250; scorePunch=1; if(score>hi) hi=score; }
}
else if(type==='pts') addScore(500,paddle.x,PADDLE_Y-40,MUSTARD);
else if(type==='multi'){
const snap=balls.filter(b=>!b.stuck);
for(const b of snap){
const sp=Math.hypot(b.vx,b.vy)||300, a=Math.atan2(b.vy,b.vx);
for(const da of [-0.5,0.5]){
if(balls.length>=6) break;
balls.push(makeBall(b.x,b.y,Math.cos(a+da)*sp,Math.sin(a+da)*sp));
}
}
}
}
function hitBrick(k){
if(k.steel){
sfx.steel(); trauma=Math.min(1,trauma+0.05);
fleckBurst(k.x+BW/2,k.y+BH/2,'#7B7264',3,150);
return;
}
k.hp--;
if(k.hp>0){
sfx.thud(); trauma=Math.min(1,trauma+0.06);
freezeT=Math.min(0.05,freezeT+0.025);
fleckBurst(k.x+BW/2,k.y+BH/2,k.col,4,170);
return;
}
k.dead=true; remaining--;
combo++;
const nm=Math.min(8,1+((combo/4)|0));
if(nm>mult){ mult=nm; addText(k.x+BW/2,k.y-10,'×'+mult,ACCENT,15,false); }
addScore((k.tough?120:50)*mult,k.x+BW/2,k.y+BH/2);
burstBrick(k);
freezeT=Math.min(0.09,freezeT+0.04);
trauma=Math.min(1,trauma+0.11);
sfx.pop(combo);
maybeDrop(k);
if(remaining<=0) onClear();
}
function jitter(b){
const a=(Math.random()-0.5)*0.04, c=Math.cos(a), s=Math.sin(a);
const vx=b.vx; b.vx=b.vx*c-b.vy*s; b.vy=vx*s+b.vy*c;
}
function brickCollide(b,px,py){
for(const k of bricks){
if(k.dead) continue;
const nx=clamp(b.x,k.x,k.x+BW), ny=clamp(b.y,k.y,k.y+BH);
const dx=b.x-nx, dy=b.y-ny;
if(dx*dx+dy*dy>b.r*b.r) continue;
let h=false,v=false;
if(px<=k.x||px>=k.x+BW) h=true;
if(py<=k.y||py>=k.y+BH) v=true;
if(!h&&!v) v=true;
if(h) b.vx=-b.vx;
if(v) b.vy=-b.vy;
jitter(b);
b.x=px; b.y=py;
b.sq=1;
hitBrick(k);
return true;
}
return false;
}
function paddleHit(b){
b.y=PADDLE_Y-b.r-0.5;
const rel=clamp((b.x-paddle.x)/(paddle.w/2+b.r),-1,1);
const ang=rel*1.05;
const sp=Math.min(Math.hypot(b.vx,b.vy)+7,620);
b.vx=Math.sin(ang)*sp; b.vy=-Math.cos(ang)*sp;
b.sq=1; paddle.sq=1;
if(state!=='title'){
combo=0; mult=1;
sfx.paddle();
trauma=Math.min(1,trauma+0.05);
}
fleckBurst(b.x,PADDLE_Y,ACCENT,4,160);
}
function wallFx(x,y){
fleckBurst(x,y,INK,3,130);
sfx.wall();
trauma=Math.min(1,trauma+0.012);
}
function stickBalls(){
for(const b of balls) if(b.stuck){ b.x=paddle.x; b.y=PADDLE_Y-7-b.r; b.trail.length=0; }
}
/* ---------- updates ---------- */
function updatePaddle(dt){
const dir=(keys.right?1:0)-(keys.left?1:0);
if(lastInput==='mouse'&&mouseX!=null){
const t=1-Math.exp(-16*dt);
const nx=paddle.x+(mouseX-paddle.x)*t;
paddle.vx=dt>0?(nx-paddle.x)/dt:0;
paddle.x=nx;
}else{
if(dir) paddle.vx=clamp(paddle.vx+dir*2800*dt,-840,840);
else paddle.vx*=Math.exp(-9*dt);
paddle.x+=paddle.vx*dt;
}
const hw=paddle.w/2+4;
paddle.x=clamp(paddle.x,hw,FW-hw);
paddle.wt=time<wideUntil?150:92;
paddle.w+=(paddle.wt-paddle.w)*(1-Math.exp(-9*dt));
paddle.tilt=clamp(paddle.vx*0.00011,-0.085,0.085);
}
function updateBalls(dt){
const f=time<slowUntil?0.62: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-7-b.r; b.trail.length=0; continue; }
const sp=Math.hypot(b.vx,b.vy)||1;
const steps=Math.max(1,Math.ceil(sp*f*dt/(b.r*0.8)));
const sdt=dt*f/steps;
let lost=false;
for(let s=0;s<steps;s++){
const px=b.x, py=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); wallFx(b.x,b.y); b.sq=1; }
else if(b.x>FW-b.r){ b.x=FW-b.r; b.vx=-Math.abs(b.vx); wallFx(b.x,b.y); b.sq=1; }
if(b.y<b.r){ b.y=b.r; b.vy=Math.abs(b.vy); wallFx(b.x,b.y); b.sq=1; }
if(b.vy>0&&b.y+b.r>=PADDLE_Y&&b.y-b.r<=PADDLE_Y+14&&Math.abs(b.x-paddle.x)<=paddle.w/2+b.r) paddleHit(b);
brickCollide(b,px,py);
if(b.y-b.r>FH+10){ lost=true; break; }
}
const s2=Math.hypot(b.vx,b.vy);
if(s2>0&&Math.abs(b.vy)<s2*0.18){
const sign=b.vy<0?-1:(b.vy>0?1:-1);
b.vy=sign*s2*0.18;
b.vx=(b.vx<0?-1:1)*Math.sqrt(Math.max(0,s2*s2-b.vy*b.vy));
}
b.sq=Math.max(0,(b.sq||0)-dt*5);
b.trail.push({x:b.x,y:b.y});
if(b.trail.length>11) b.trail.shift();
if(lost){
balls.splice(i,1);
if(state==='play'&&balls.length===0) loseLife();
}
}
}
function updatePows(dt){
for(let i=pows.length-1;i>=0;i--){
const p=pows[i];
p.t+=dt; p.y+=130*dt; p.x+=Math.sin(p.t*3+p.ph)*14*dt;
if(p.y>FH+16){ pows.splice(i,1); continue; }
if(p.y+11>=PADDLE_Y-2&&p.y-11<=PADDLE_Y+16&&Math.abs(p.x-paddle.x)<=paddle.w/2+13){
applyPow(p.type);
addText(p.x,PADDLE_Y-22,LBL[p.type],LCOL[p.type],13,false);
addPart({type:'ring',x:paddle.x,y:PADDLE_Y+6,t:0,life:0.35,col:LCOL[p.type],r0:8,r1:34});
if(p.type==='life') sfx.life(); else sfx.pow();
pows.splice(i,1);
}
}
}
function updateParts(dt){
for(let i=parts.length-1;i>=0;i--){
const p=parts[i];
p.t+=dt;
if(p.t>=p.life){ parts.splice(i,1); continue; }
if(p.type==='fleck'){
p.vy+=850*dt; p.vx*=Math.exp(-1.6*dt);
p.x+=p.vx*dt; p.y+=p.vy*dt; p.rot+=6*dt;
}else if(p.type==='conf'){
p.vy+=55*dt;
p.x+=p.vx*dt+Math.sin((p.t+p.ph)*3)*22*dt;
p.y+=p.vy*dt; p.rot+=2.6*dt;
}
}
}
function updateTexts(dt){
for(let i=texts.length-1;i>=0;i--){
const t=texts[i]; t.t+=dt; t.y-=28*dt;
if(t.t>=t.life) texts.splice(i,1);
}
}
function initAttract(){
bricks=[]; pows=[]; parts=[]; texts=[]; balls=[];
paddle.x=FW/2; paddle.w=paddle.wt=92; paddle.vx=0;
for(let i=0;i<3;i++){
const a=rand(0,Math.PI*2), sp=rand(230,310);
balls.push({x:rand(80,FW-80),y:rand(90,320),vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,r:6,trail:[],sq:0,stuck:false});
}
}
function updateAttract(dt){
paddle.x=FW/2+Math.sin(time*0.8)*150;
paddle.w=92; paddle.wt=92;
updateBalls(dt);
let guard=0;
while(balls.length<3&&guard++<5){
const a=Math.PI/2+rand(-0.7,0.7), sp=rand(230,310);
balls.push({x:rand(70,FW-70),y:rand(70,150),vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,r:6,trail:[],sq:0,stuck:false});
}
}
function update(dt,rdt){
time+=dt;
switch(state){
case 'title': updateAttract(dt); break;
case 'intro':
updatePaddle(dt); stickBalls();
introT+=rdt;
if(!introHit&&introT>0.16){ introHit=true; sfx.stamp(); trauma=Math.min(1,trauma+0.3); }
if(introT>1.35){ state='ready'; readyT=0; }
break;
case 'ready':
updatePaddle(dt); stickBalls(); readyT+=rdt;
break;
case 'play':
updatePaddle(dt); updateBalls(dt); updatePows(dt);
break;
case 'clear':
updatePaddle(dt); updateBalls(dt); updatePows(dt);
clearT+=rdt;
if(!bonused&&clearT>0.55){
bonused=true;
lastBonus=400+level*200+lives*150;
score+=lastBonus; scorePunch=1;
if(score>hi) hi=score;
addText(FW/2,180,'BONUS +'+lastBonus,ACCENT,16,false);
sfx.clear(); burstConf(FW/2,FH/2,46); saveHi();
}
if(clearT>2.45) nextLevel();
break;
case 'over':
overT+=rdt;
if(!overHit&&overT>0.16){ overHit=true; sfx.stamp(); trauma=Math.min(1,trauma+0.35); }
if(hiBeaten&&overT<1.4&&Math.random()<0.18) spawnConfetti(1);
break;
case 'win':
winT+=rdt; updatePaddle(dt); updateBalls(dt);
if(!winHit&&winT>0.16){ winHit=true; sfx.stamp(); trauma=0.4; }
winAcc+=rdt;
while(winAcc>0.05){ winAcc-=0.05; spawnConfetti(1); }
break;
}
updateParts(dt); updateTexts(dt);
}
/* ---------- drawing ---------- */
function drawField(){
ctx.fillStyle=INK; rr(8,10,FW,FH,6); ctx.fill();
ctx.fillStyle=PAPER2; rr(0,0,FW,FH,6); ctx.fill();
ctx.lineWidth=3; ctx.strokeStyle=INK; rr(1.5,1.5,FW-3,FH-3,5); ctx.stroke();
ctx.lineWidth=1; ctx.strokeStyle='rgba(24,20,16,0.22)'; rr(6,6,FW-12,FH-12,4); ctx.stroke();
}
function drawBricks(){
ctx.fillStyle=INK;
for(const k of bricks) if(!k.dead){ rr(k.x+3,k.y+4,BW,BH,3); ctx.fill(); }
for(const k of bricks){
if(k.dead) continue;
if(k.steel){
ctx.fillStyle='#4E463C'; rr(k.x,k.y,BW,BH,3); ctx.fill();
ctx.save(); rr(k.x,k.y,BW,BH,3); ctx.clip();
ctx.strokeStyle='rgba(243,237,224,0.16)'; ctx.lineWidth=1.2;
ctx.beginPath();
for(let i=-BH;i<BW;i+=5){ ctx.moveTo(k.x+i,k.y+BH); ctx.lineTo(k.x+i+BH,k.y); }
ctx.stroke(); ctx.restore();
ctx.fillStyle='rgba(243,237,224,0.45)';
const rv=[[4,4],[BW-4,4],[4,BH-4],[BW-4,BH-4]];
for(const d of rv){ ctx.beginPath(); ctx.arc(k.x+d[0],k.y+d[1],1.4,0,Math.PI*2); ctx.fill(); }
}else{
ctx.fillStyle=k.col; rr(k.x,k.y,BW,BH,3); ctx.fill();
if(k.tough){
if(k.hp>=2){
ctx.fillStyle='rgba(243,237,224,0.55)';
ctx.fillRect(k.x+5,k.y+BH/2-1,BW-10,2);
}else{
ctx.strokeStyle='rgba(243,237,224,0.8)'; ctx.lineWidth=1.4;
ctx.beginPath();
ctx.moveTo(k.x+BW*0.28,k.y+2);
ctx.lineTo(k.x+BW*0.46,k.y+BH*0.42);
ctx.lineTo(k.x+BW*0.3,k.y+BH*0.68);
ctx.lineTo(k.x+BW*0.52,k.y+BH-2);
ctx.stroke();
}
}
}
}
}
function drawPows(){
for(const p of pows){
ctx.save();
ctx.translate(p.x,p.y);
ctx.rotate(Math.sin(p.t*2.2+p.ph)*0.12);
ctx.fillStyle=INK; rr(-8.5,-7.5,22,22,6); ctx.fill();
ctx.fillStyle=POWC[p.type]; rr(-11,-11,22,22,6); ctx.fill();
text(POWL[p.type],0,1,p.type==='pts'?8:12,DISP,p.type==='pts'?INK:PAPER2,'center',0,1,'800');
ctx.restore();
}
}
function drawBall(b){
const n=b.trail.length;
for(let i=0;i<n;i++){
const p=b.trail[i], a=(i+1)/n;
ctx.globalAlpha=a*0.16; ctx.fillStyle=INK;
ctx.beginPath(); ctx.arc(p.x,p.y,b.r*a,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha=1;
const r=b.r*(1+(b.sq||0)*0.35);
const off=1.4+Math.min(combo,10)*0.22; // misregistration grows with combo
ctx.fillStyle=ACCENT;
ctx.beginPath(); ctx.arc(b.x-off,b.y-off*0.7,r,0,Math.PI*2); ctx.fill();
ctx.fillStyle=INK;
ctx.beginPath(); ctx.arc(b.x,b.y,r,0,Math.PI*2); ctx.fill();
ctx.fillStyle=PAPER2;
ctx.beginPath(); ctx.arc(b.x-2,b.y-2,1.6,0,Math.PI*2); ctx.fill();
}
function drawPaddle(){
const w=paddle.w, h=14;
ctx.save();
ctx.translate(paddle.x,PADDLE_Y+h/2);
ctx.rotate(paddle.tilt||0);
const s=paddle.sq||0;
ctx.scale(1+s*0.28,1-s*0.38);
ctx.fillStyle=INK; rr(-w/2+3,-h/2+4,w,h,7); ctx.fill();
ctx.fillStyle=ACCENT; rr(-w/2-2,-h/2-1.5,w,h,7); ctx.fill();
ctx.fillStyle=INK; rr(-w/2,-h/2,w,h,7); ctx.fill();
ctx.fillStyle=ACCENT;
if(w>110) rr(-16,-h/2+3,32,h-6,3); else rr(-10,-h/2+3,20,h-6,3);
ctx.fill();
ctx.restore();
}
function drawParts(){
for(const p of parts){
const a=1-p.t/p.life;
if(p.type==='ring'){
const r=p.r0+(p.r1-p.r0)*(p.t/p.life);
ctx.save(); ctx.globalAlpha=a*0.9; ctx.strokeStyle=p.col; ctx.lineWidth=2.5;
ctx.beginPath(); ctx.arc(p.x,p.y,r,0,Math.PI*2); ctx.stroke(); ctx.restore();
}else{
ctx.save(); ctx.globalAlpha=a; ctx.translate(p.x,p.y); ctx.rotate(p.rot);
ctx.fillStyle=p.col; ctx.fillRect(-p.sz/2,-p.sz/2,p.sz,p.sz); ctx.restore();
}
}
}
function drawTexts(){
for(const t of texts){
const a=1-t.t/t.life;
text(t.txt,t.x,t.y,t.size,t.mono?MONO:DISP,t.col,'center',1,a,'800');
}
}
function drawHud(){
line(14,HUD,FW-14,HUD,2,INK);
ctx.save();
ctx.translate(16,18);
const k=1+scorePunch*0.16; ctx.scale(k,k);
text('SCORE',0,0,8,DISP,INK,'left',2,0.55,'700');
text(pad(score),0,24,23,MONO,INK,'left',1,1,'700');
if(mult>1) text('×'+mult,112,24,14,DISP,ACCENT,'left',0,1,'800');
ctx.restore();
text('PLATE '+String(level+1).padStart(2,'0')+' / '+String(LEVELS.length).padStart(2,'0'),FW/2,18,9,DISP,INK,'center',2,0.55,'700');
text(LEVELS[level].name,FW/2,36,13,DISP,INK,'center',4,1,'800');
text('HI '+pad(hi),FW-16,15,9,MONO,INK,'right',1,0.6,'700');
const lx0=FW-18;
for(let i=0;i<lives;i++){
const lx=lx0-i*26;
ctx.fillStyle=INK; rr(lx-18,30,18,6,3); ctx.fill();
ctx.fillStyle=ACCENT; rr(lx-9,31.5,6,3,1.5); ctx.fill();
}
text('LIVES',lx0-Math.max(lives,1)*26-10,33,8,DISP,INK,'right',1,0.5,'700');
}
function drawChip(){
MUTE.x=FW-86; MUTE.y=FH-30;
ctx.save(); ctx.globalAlpha=0.45;
ctx.strokeStyle=INK; ctx.lineWidth=1.5;
rr(MUTE.x,MUTE.y,76,20,10); ctx.stroke();
text(muted?'SOUND OFF':'SOUND ON',MUTE.x+38,MUTE.y+10.5,8,DISP,INK,'center',1,1,'700');
ctx.restore();
}
function drawStamp(main,sub,t,col){
const p=Math.min(1,t/0.16), e=1-Math.pow(1-p,3);
const sc=1+(1-e)*1.9, al=Math.min(1,t/0.08);
ctx.save();
ctx.translate(FW/2,FH/2-16); ctx.rotate(-0.055); ctx.scale(sc,sc);
ctx.globalAlpha=al;
const w=376, h=sub?126:92;
ctx.fillStyle='rgba(249,244,232,0.94)'; rr(-w/2,-h/2,w,h,10); ctx.fill();
ctx.lineWidth=5; ctx.strokeStyle=col; rr(-w/2,-h/2,w,h,10); ctx.stroke();
ctx.lineWidth=1.5; rr(-w/2+7,-h/2+7,w-14,h-14,6); ctx.stroke();
text(main,0,sub?-10:0,30,DISP,col,'center',2,1,'800');
if(sub) text(sub,0,26,12,MONO,INK,'center',1,0.85,'700');
ctx.restore();
}
function drawReady(){
const b=balls[0]; if(!b) return;
ctx.save();
ctx.strokeStyle='rgba(24,20,16,0.5)'; ctx.lineWidth=2;
ctx.setLineDash([5,7]); ctx.lineDashOffset=-time*46;
ctx.beginPath(); ctx.moveTo(b.x,b.y-12); ctx.lineTo(b.x,b.y-86); ctx.stroke();
ctx.restore();
if(Math.sin(time*5.2)>-0.15)
text('SPACE OR CLICK TO LAUNCH',FW/2,FH-118,13,DISP,INK,'center',2,0.95,'800');
if(level===0&&readyT<5)
text('MOVE WITH MOUSE OR ARROW KEYS',FW/2,FH-94,10,DISP,INK,'center',2,0.55,'700');
}
function drawTitle(){
const pw=400, ph=410, px=(FW-pw)/2, py=(FH-ph)/2;
ctx.fillStyle=INK; rr(px+7,py+9,pw,ph,8); ctx.fill();
ctx.fillStyle=PAPER2; rr(px,py,pw,ph,8); ctx.fill();
ctx.lineWidth=3; ctx.strokeStyle=INK; rr(px,py,pw,ph,8); ctx.stroke();
const cols=[ACCENT,MUSTARD,TEAL,BLUE];
for(let i=0;i<4;i++){
const sx=FW/2-62+i*32, sy=py+26;
ctx.save(); ctx.translate(sx+12,sy+7); ctx.rotate((i-1.5)*0.05);
ctx.fillStyle=INK; rr(-14,-10,24,14,3); ctx.fill();
ctx.fillStyle=cols[i]; rr(-12,-7,24,14,3); ctx.fill();
ctx.restore();
}
ctx.strokeStyle=INK; ctx.lineWidth=1.5;
rr(FW/2-96,py+56,192,22,11); ctx.stroke();
text('SEVEN-PLATE EDITION',FW/2,py+67.5,8,DISP,INK,'center',2.5,0.8,'700');
misPrint('MISPRINT',FW/2,py+148,54);
line(px+44,py+182,px+pw-44,py+182,2,INK);
text('A BREAKOUT IN INK & PAPER',FW/2,py+202,11,DISP,INK,'center',3,0.75,'700');
text('BEST '+pad(hi),FW/2,py+226,12,MONO,INK,'center',1,0.85,'700');
if(Math.sin(time*4.6)>-0.2)
text('CLICK OR PRESS SPACE',FW/2,py+260,14,DISP,ACCENT,'center',2,1,'800');
const rows=[['MOVE','MOUSE / ARROWS'],['LAUNCH','SPACE'],['PAUSE','P'],['SOUND','M']];
rows.forEach((r,i)=>{
const yy=py+294+i*21;
text(r[0],px+70,yy,9,MONO,'rgba(24,20,16,0.5)','left',1,1,'700');
text(r[1],px+150,yy,10,MONO,INK,'left',1,1,'700');
});
text('PRINTED LOCALLY — NO NETWORK REQUIRED',FW/2,py+ph-20,8,DISP,INK,'center',2,0.4,'700');
}
function drawOverlay(){
if(state==='title'){ drawTitle(); return; }
if(state==='intro')
drawStamp('PLATE '+String(level+1).padStart(2,'0'),LEVELS[level].name,introT,ACCENT);
else if(state==='ready') drawReady();
else if(state==='clear'&&clearT>0.5)
drawStamp('PLATE CLEAR','BONUS +'+lastBonus,clearT-0.5,ACCENT);
else if(state==='over'){
dim(0.78);
drawStamp('GAME OVER',hiBeaten?'NEW HI-SCORE — '+pad(score):'SCORE '+pad(score),overT,ACCENT);
if(overT>0.7&&Math.sin(time*5)>-0.1)
text('SPACE TO RETRY',FW/2,FH/2+118,13,DISP,INK,'center',2,0.9,'800');
}else if(state==='win'){
dim(0.55);
drawStamp('EDITION COMPLETE','ALL 7 PLATES PRINTED — '+pad(score),winT,ACCENT);
if(winT>0.8&&Math.sin(time*5)>-0.1)
text('SPACE FOR TITLE',FW/2,FH/2+118,13,DISP,INK,'center',2,0.9,'800');
}
if(paused){ dim(0.82); drawStamp('PAUSED','P TO RESUME · R TO QUIT',0.2,INK); }
}
function drawWorld(){
if(state!=='title'){
ctx.save();
ctx.font='900 200px '+DISP;
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.strokeStyle='rgba(24,20,16,0.055)'; ctx.lineWidth=2;
ctx.strokeText(String(level+1),FW/2,410);
ctx.restore();
drawBricks();
}
drawPows();
for(const b of balls) drawBall(b);
drawPaddle();
drawParts();
if(state!=='title') drawHud();
drawTexts();
drawChip();
drawOverlay();
}
function drawMarks(){
const fx=ox, fy=oy, fw=FW*scl, fh=FH*scl;
ctx.strokeStyle='rgba(24,20,16,0.6)'; ctx.lineWidth=1.5;
const cs=[[fx,fy,-1,-1],[fx+fw,fy,1,-1],[fx,fy+fh,-1,1],[fx+fw,fy+fh,1,1]];
for(let i=0;i<4;i++){
const cx=cs[i][0]+cs[i][2]*13, cy=cs[i][1]+cs[i][3]*13;
if(cx<10||cx>W-10||cy<10||cy>H-10) continue;
ctx.beginPath(); ctx.arc(cx,cy,5,0,Math.PI*2); ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx-9,cy); ctx.lineTo(cx+9,cy);
ctx.moveTo(cx,cy-9); ctx.lineTo(cx,cy+9);
ctx.stroke();
}
const mL=fx-8, mR=W-(fx+fw)-8, mB=H-(fy+fh)-6;
if(mL>76) vtext('MISPRINT — INK & PAPER BREAKOUT',fx-16,fy+fh/2,10,'rgba(24,20,16,0.5)',-1);
if(mR>76){
vtext('SEVEN-PLATE EDITION · HI '+pad(hi),fx+fw+16,fy+fh/2,10,'rgba(24,20,16,0.5)',1);
if(mR>100){
const n=LEVELS.length, bh=14, gap=6, tot=n*bh+(n-1)*gap;
const cxp=fx+fw+36;
let yy=fy+fh/2-tot/2;
for(let i=0;i<n;i++){
if(i<level){ ctx.fillStyle=INK; rr(cxp-5,yy,10,bh,3); ctx.fill(); }
else if(i===level){ ctx.fillStyle=ACCENT; rr(cxp-5,yy,10,bh,3); ctx.fill(); }
else { ctx.strokeStyle='rgba(24,20,16,0.4)'; ctx.lineWidth=1.5; rr(cxp-5,yy,10,bh,3); ctx.stroke(); }
yy+=bh+gap;
}
}
}
if(mB>30)
text('MOUSE OR ARROWS MOVE · SPACE LAUNCH · P PAUSE · M SOUND',W/2,fy+fh+16,9,MONO,'rgba(24,20,16,0.55)','center',1,1,'700');
}
function render(){
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.fillStyle=PAPER; ctx.fillRect(0,0,W,H);
drawMarks();
const sh=trauma*trauma;
const shx=(Math.random()*2-1)*sh*13, shy=(Math.random()*2-1)*sh*13;
const rot=(Math.random()*2-1)*sh*0.014;
ctx.save();
ctx.translate(ox+FW*scl/2,oy+FH*scl/2);
ctx.rotate(rot); ctx.scale(scl,scl);
ctx.translate(-FW/2+shx,-FH/2+shy);
drawField();
ctx.save(); rr(3,3,FW-6,FH-6,4); ctx.clip();
drawWorld();
ctx.restore();
ctx.restore();
if(flash>0){
ctx.globalAlpha=Math.min(0.5,flash);
ctx.fillStyle=ACCENT;
ctx.fillRect(ox,oy,FW*scl,FH*scl);
ctx.globalAlpha=1;
}
canvas.style.cursor=(!paused&&(state==='play'||state==='ready'))?'none':'default';
}
/* ---------- input ---------- */
const canvas=document.getElementById('c'), ctx=canvas.getContext('2d');
addEventListener('keydown',e=>{
if(e.code==='ArrowLeft'||e.code==='KeyA'){ keys.left=true; lastInput='key'; e.preventDefault(); }
else if(e.code==='ArrowRight'||e.code==='KeyD'){ keys.right=true; lastInput='key'; e.preventDefault(); }
else if(e.code==='Space'||e.code==='Enter'){ e.preventDefault(); if(!e.repeat){ initAudio(); action(); } }
else if(e.code==='KeyP'||e.code==='Escape') togglePause();
else if(e.code==='KeyM') toggleMute();
else if(e.code==='KeyR'){ if(state==='play'||state==='ready'||paused) toTitle(); }
});
addEventListener('keyup',e=>{
if(e.code==='ArrowLeft'||e.code==='KeyA') keys.left=false;
if(e.code==='ArrowRight'||e.code==='KeyD') keys.right=false;
});
addEventListener('pointermove',e=>{
mouseX=clamp((e.clientX-ox)/scl,-30,FW+30);
lastInput='mouse';
});
addEventListener('pointerdown',e=>{
initAudio();
const fx=(e.clientX-ox)/scl, fy=(e.clientY-oy)/scl;
if(fx>=MUTE.x&&fx<=MUTE.x+76&&fy>=MUTE.y&&fy<=MUTE.y+20){ toggleMute(); return; }
if(e.button===0) action();
});
addEventListener('blur',()=>{ if(state==='play'||state==='ready') paused=true; });
/* ---------- boot & loop ---------- */
function resize(){
dpr=Math.min(window.devicePixelRatio||1,2);
W=window.innerWidth; H=window.innerHeight;
canvas.width=Math.round(W*dpr); canvas.height=Math.round(H*dpr);
canvas.style.width=W+'px'; canvas.style.height=H+'px';
scl=Math.min((W-14)/FW,(H-14)/FH);
ox=(W-FW*scl)/2; oy=(H-FH*scl)/2;
}
resize();
addEventListener('resize',resize);
initAttract();
let last=performance.now();
function frame(now){
requestAnimationFrame(frame);
const rdt=Math.min(0.05,(now-last)/1000)||0.016; last=now;
if(slowmoT>0){ slowmoT-=rdt; timescale+=(0.18-timescale)*Math.min(1,rdt*14); }
else timescale+=(1-timescale)*Math.min(1,rdt*8);
let dt=rdt*timescale;
if(freezeT>0){ freezeT-=rdt; dt=0; } // hit-stop
trauma=Math.max(0,trauma-rdt*1.5);
flash=Math.max(0,flash-rdt*2.4);
scorePunch=Math.max(0,scorePunch-rdt*3.5);
paddle.sq=Math.max(0,paddle.sq-rdt*4.5);
if(!paused) update(dt,rdt);
render();
}
requestAnimationFrame(frame);
</script>
</body>
</html>

