Run details
- Model
- qwen/qwen3.8-max-0902
- Route
- OpenRouter API, one user message
- Served by
- Alibaba
- Effort
- xhigh, OpenRouter's default for this model
- Limits
- 60 min · 131,072 output tokens (the model's limit)
- Date
- 24 Oct 2026
- Time
- 12 min 28 s
- Size
- 34 kB
- Tokens
- 138 in, 31,632 out (17,574 of them thinking)
- List price
- $0.19 at today's price ($2 in / $6 out per M)
- Paid
- $0.19
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
34 kB 755 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NOVA BREAKOUT</title>
<style>
:root{
--disp: 'Bahnschrift','Avenir Next Condensed','HelveticaNeue-CondensedBold','Arial Narrow',Impact,sans-serif;
--mono: ui-monospace,'Cascadia Mono','SF Mono',Menlo,Consolas,'Liberation Mono',monospace;
--gold:#ffd23f; --cyan:#4cc9f0; --coral:#ff4f69; --mint:#3ddc97; --ink:#eaf3ff;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
background:
radial-gradient(1100px 620px at 72% -12%, rgba(76,201,240,.13), transparent 60%),
radial-gradient(900px 540px at 10% 112%, rgba(255,79,105,.10), transparent 60%),
radial-gradient(760px 500px at 50% 55%, rgba(20,34,72,.55), transparent 72%),
#04060d;
color:var(--ink); font-family:var(--mono);
display:flex; align-items:center; justify-content:center;
overflow:hidden; user-select:none; -webkit-user-select:none;
animation:drift 26s ease-in-out infinite alternate;
}
@keyframes drift{to{background-position:0 -34px, 0 26px, 0 0, 0 0}}
#wrap{display:flex;flex-direction:column;align-items:center;gap:10px;padding:12px}
#stage{
position:relative; overflow:hidden; border-radius:14px; cursor:crosshair; touch-action:none;
border:1px solid #223458;
box-shadow:0 0 0 6px #0a0f1c, 0 0 0 8px #1c2a45, 0 30px 80px rgba(0,0,0,.65), 0 0 60px rgba(76,201,240,.12);
}
#game{position:absolute;inset:0;width:100%;height:100%;display:block}
.fx{position:absolute;inset:0;pointer-events:none;z-index:3;
background:
repeating-linear-gradient(0deg, rgba(255,255,255,.025) 0 1px, transparent 1px 3px),
radial-gradient(120% 92% at 50% 42%, transparent 55%, rgba(2,5,14,.55));
}
/* ---------- HUD ---------- */
#hud{position:absolute;top:0;left:0;right:0;display:flex;align-items:flex-start;
justify-content:space-between;padding:.75em 1.1em .4em;pointer-events:none;z-index:4;
background:linear-gradient(180deg, rgba(5,9,20,.88), rgba(5,9,20,0))}
.hudCol{min-width:8em}
.hudCol.mid{text-align:center}
.hudCol.right{text-align:right}
.lab{font-size:.6em;letter-spacing:.34em;color:#6f86ad;text-transform:uppercase}
.big{font-family:var(--mono);font-weight:700;font-size:1.5em;line-height:1.05;
color:#fff;text-shadow:0 0 14px rgba(76,201,240,.55)}
.big.gold{text-shadow:0 0 14px rgba(255,210,63,.55)}
#levelname{font-family:var(--disp);font-size:1.02em;letter-spacing:.18em;color:var(--gold);
text-shadow:0 0 12px rgba(255,210,63,.4);margin-top:.15em}
#levelsub{font-size:.62em;letter-spacing:.3em;color:#7d92b8}
#lives{margin-top:.35em;display:flex;gap:.3em;justify-content:flex-end}
#lives i{display:block;width:1.35em;height:.42em;border-radius:.25em;
background:linear-gradient(180deg,#8ff0ff,#2ea8d8);box-shadow:0 0 8px rgba(76,201,240,.7)}
#muteBtn{pointer-events:auto;margin-top:.5em;background:rgba(14,22,40,.85);color:#9fb4d8;
border:1px solid #2b3f66;border-radius:.45em;font-family:var(--mono);font-size:.68em;
letter-spacing:.15em;padding:.3em .6em;cursor:pointer;transition:.15s}
#muteBtn:hover{color:#fff;border-color:var(--cyan);box-shadow:0 0 10px rgba(76,201,240,.4)}
/* ---------- Overlays ---------- */
.ov{position:absolute;inset:0;z-index:6;display:flex;align-items:center;justify-content:center;
text-align:center;background:rgba(4,8,18,.62);
transition:opacity .35s ease, visibility .35s}
.ov.hidden{opacity:0;visibility:hidden;pointer-events:none}
.ovBox{animation:pop .5s cubic-bezier(.2,1.4,.3,1)}
@keyframes pop{from{transform:scale(.9) translateY(16px);opacity:0}}
.sysTag{font-size:.62em;letter-spacing:.5em;color:#5d7ba8;margin-bottom:1em}
.logo{font-family:var(--disp);font-weight:900;line-height:.92;letter-spacing:.02em}
.logo .l1{display:block;font-size:4.6em;color:#eafaff;
text-shadow:-3px 0 var(--coral),3px 0 var(--cyan),0 0 30px rgba(76,201,240,.5);
animation:floaty 3.4s ease-in-out infinite}
.logo .l2{display:block;font-size:2.7em;color:var(--gold);letter-spacing:.3em;margin-top:.12em;
text-shadow:0 4px 0 #6b4a00,0 0 26px rgba(255,210,63,.45)}
@keyframes floaty{50%{transform:translateY(-5px)}}
.tag{margin:1.1em 0 1.4em;font-size:.66em;letter-spacing:.28em;color:#8fa7cc}
.ctrls{display:flex;gap:1.4em;justify-content:center;flex-wrap:wrap;margin-bottom:1.6em}
.ctrls div{border:1px solid #24365c;background:rgba(10,17,34,.7);padding:.5em .8em;border-radius:.4em}
.ctrls b{display:block;font-size:.6em;letter-spacing:.25em;color:#6f86ad;margin-bottom:.3em}
.ctrls i{font-style:normal;font-size:.75em;color:#dce9ff}
.prompt{font-family:var(--disp);font-size:1.1em;letter-spacing:.3em;color:#fff;
text-shadow:0 0 16px rgba(255,210,63,.6)}
.blink{animation:blink 1.1s steps(2,jump-none) infinite}
@keyframes blink{50%{opacity:.15}}
.hiLine{margin-top:1.4em;font-size:.7em;letter-spacing:.3em;color:#7d92b8}
.hiLine span{color:var(--gold)}
.bigTitle{font-family:var(--disp);font-weight:900;font-size:3.4em;letter-spacing:.06em}
.bigTitle.red{color:var(--coral);text-shadow:-3px 0 rgba(76,201,240,.8),0 0 30px rgba(255,79,105,.6)}
.bigTitle.grn{color:var(--mint);text-shadow:0 0 30px rgba(61,220,151,.6)}
.ovScore{margin:.7em 0 .2em;font-size:.7em;letter-spacing:.3em;color:#8fa7cc}
.ovScore b{display:block;font-size:2.6em;color:#fff;letter-spacing:.05em;
text-shadow:0 0 18px rgba(76,201,240,.6)}
.newBest{display:none;font-size:.72em;letter-spacing:.35em;color:var(--gold);
text-shadow:0 0 14px rgba(255,210,63,.7);margin-top:.6em}
.newBest.on{display:block;animation:blink 1s steps(2,jump-none) infinite}
/* ---------- footer ---------- */
#foot{font-size:11px;letter-spacing:.22em;color:#4d6285;text-transform:uppercase}
#foot b{color:#7f9ac4;font-weight:400}
</style>
</head>
<body>
<div id="wrap">
<div id="stage">
<canvas id="game"></canvas>
<div class="fx"></div>
<div id="hud">
<div class="hudCol left">
<div class="lab">Score</div>
<div class="big gold" id="score">0</div>
<div id="lives"></div>
</div>
<div class="hudCol mid">
<div id="levelname">SECTOR 1</div>
<div id="levelsub">OPENING SALVO</div>
</div>
<div class="hudCol right">
<div class="lab">Best</div>
<div class="big" id="hiscore">0</div>
<button id="muteBtn" title="toggle sound">♪ ON</button>
</div>
</div>
<div class="ov" id="ovTitle">
<div class="ovBox">
<div class="sysTag">MK·II ARCADE SYSTEM</div>
<h1 class="logo"><span class="l1">NOVA</span><span class="l2">BREAKOUT</span></h1>
<p class="tag">SMASH THE WALL · CHASE THE COMBO · SURVIVE 7 SECTORS</p>
<div class="ctrls">
<div><b>MOVE</b><i>MOUSE / ← →</i></div>
<div><b>LAUNCH · FIRE</b><i>SPACE / CLICK</i></div>
<div><b>PAUSE</b><i>P</i></div>
<div><b>SOUND</b><i>M</i></div>
</div>
<div class="prompt blink">PRESS ENTER OR CLICK TO START</div>
<div class="hiLine">BEST RUN — <span id="hiTitle">0</span></div>
</div>
</div>
<div class="ov hidden" id="ovPause">
<div class="ovBox">
<div class="bigTitle" style="color:#dce9ff;text-shadow:0 0 24px rgba(76,201,240,.5)">PAUSED</div>
<p class="tag">PRESS P TO RESUME</p>
</div>
</div>
<div class="ov hidden" id="ovOver">
<div class="ovBox">
<div class="bigTitle red">GAME OVER</div>
<div class="ovScore">FINAL SCORE<b id="overScore">0</b></div>
<div class="newBest" id="overNew">★ NEW BEST ★</div>
<p class="prompt blink" style="margin-top:1.4em">ENTER — TRY AGAIN</p>
</div>
</div>
<div class="ov hidden" id="ovWin">
<div class="ovBox">
<div class="bigTitle grn">SYSTEM CLEARED</div>
<p class="tag">ALL 7 SECTORS DEMOLISHED</p>
<div class="ovScore">FINAL SCORE<b id="winScore">0</b></div>
<div class="newBest" id="winNew">★ NEW BEST ★</div>
<p class="prompt blink" style="margin-top:1.2em">ENTER — PLAY AGAIN</p>
</div>
</div>
</div>
<div id="foot"><b>MOUSE / ←→</b> move · <b>SPACE</b> launch & fire · <b>P</b> pause · <b>M</b> sound</div>
</div>
<script>
'use strict';
/* ============================ helpers ============================ */
const $=s=>document.querySelector(s);
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rand=(a,b)=>a+Math.random()*(b-a);
const TAU=Math.PI*2;
function shade(hex,p){ // p>0 lighten, p<0 darken
const n=parseInt(hex.slice(1),16); let r=n>>16&255,g=n>>8&255,b=n&255;
if(p>=0){r+=(255-r)*p;g+=(255-g)*p;b+=(255-b)*p;}else{r*=1+p;g*=1+p;b*=1+p;}
return `rgb(${r|0},${g|0},${b|0})`;
}
function rr(c,x,y,w,h,r){r=Math.min(r,w/2,h/2);c.beginPath();c.moveTo(x+r,y);
c.arcTo(x+w,y,x+w,y+h,r);c.arcTo(x+w,y+h,x,y+h,r);c.arcTo(x,y+h,x,y,r);c.arcTo(x,y,x+w,y,r);c.closePath();}
/* ============================ audio ============================ */
let AC=null,master=null,noiseBuf=null,muted=false;
function ensureAudio(){
if(!AC){try{
AC=new (window.AudioContext||window.webkitAudioContext)();
master=AC.createGain();master.gain.value=.4;master.connect(AC.destination);
noiseBuf=AC.createBuffer(1,AC.sampleRate,AC.sampleRate);
const d=noiseBuf.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1;
}catch(e){}}
if(AC&&AC.state==='suspended')AC.resume();
}
function tone(f0,f1,dur,type,vol,delay=0){
if(!AC||muted)return;const t=AC.currentTime+delay;
const o=AC.createOscillator(),g=AC.createGain();
o.type=type;o.frequency.setValueAtTime(Math.max(f0,1),t);
o.frequency.exponentialRampToValueAtTime(Math.max(f1,1),t+dur);
g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(.0001,t+dur);
o.connect(g);g.connect(master);o.start(t);o.stop(t+dur+.03);
}
function nz(dur,vol,f,delay=0){
if(!AC||muted)return;const t=AC.currentTime+delay;
const s=AC.createBufferSource();s.buffer=noiseBuf;s.loop=true;
const f1=AC.createBiquadFilter();f1.type='bandpass';f1.frequency.value=f;f1.Q.value=.9;
const g=AC.createGain();g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(.0001,t+dur);
s.connect(f1);f1.connect(g);g.connect(master);s.start(t);s.stop(t+dur+.03);
}
const sfx={
paddle:rel=>{tone(210+130*(1-Math.abs(rel)),400,.06,'square',.2);nz(.04,.08,900);},
brick:cb=>{const b=240*Math.pow(1.059,Math.min(cb,16));tone(b,b*1.5,.09,'square',.2);nz(.12,.18,1900);},
tick:()=>tone(660,520,.05,'triangle',.15),
steel:()=>{tone(1250,900,.05,'triangle',.14);nz(.05,.1,4200);},
launch:()=>tone(180,540,.12,'square',.2),
laser:()=>tone(950,220,.09,'square',.16),
spawn:()=>tone(500,560,.07,'sine',.1),
power:()=>{tone(660,660,.07,'triangle',.22);tone(990,990,.1,'triangle',.22,.07);},
life:()=>{tone(520,520,.08,'triangle',.22);tone(780,780,.08,'triangle',.22,.08);tone(1040,1040,.14,'triangle',.22,.16);},
hurt:()=>{tone(300,60,.5,'sawtooth',.26);nz(.3,.22,300);},
clear:()=>[523,659,784,1046,1318].forEach((f,i)=>tone(f,f,.12,'triangle',.22,i*.09)),
over:()=>tone(220,38,1.1,'sawtooth',.26),
win:()=>{[523,659,784,1046,1318,1568].forEach((f,i)=>tone(f,f,.16,'triangle',.24,i*.11));nz(.6,.12,2400,.3);}
};
/* ============================ constants ============================ */
const W=900,H=640,COLS=12,BW=64,BH=22,GAPX=6,GAPY=6,
OX=(W-(COLS*BW+(COLS-1)*GAPX))/2,OY=84,PY=H-46,PH=16;
const PAL=['#ffcf3f','#ff8a3d','#ff4f69','#b465da','#4cc9f0','#3ddc97'];
const C2='#9fd8ff',C3='#ffb703',CST='#7c8ba1';
const BASE={1:50,2:120,3:250};
const LEVELS=[
{name:'OPENING SALVO',speed:330,map:[
'111111111111','111111111111','111111111111','111111111111']},
{name:'CHECKMATE',speed:350,map:[
'1.1.1.1.1.1.','.1.1.1.1.1.1','2.2.2.2.2.2.','.1.1.1.1.1.1','1.1.1.1.1.1.']},
{name:'THE PYRAMID',speed:368,map:[
'.....11.....','....1111....','...112211...','..11222211..','.1122332211.','112233332211']},
{name:'THE VAULT',speed:384,map:[
'.SSSSSSSSSS.','S1111111111S','S1222222221S','S1111111111S','.SSSSSSSSSS.']},
{name:'THE INVADER',speed:400,map:[
'...2.....2..','....2...2...','...2222222..','..22.222.22.','.22223332222',
'.2.2222222.2','.2.2.....2.2','...22...22..']},
{name:'SKYLINE',speed:414,map:[
'...3...3....','.2.3.2.3..2.','.2.222.3.22.','121222122221','11.11.11.11.']},
{name:'FINAL FORTRESS',speed:430,map:[
'2S22333322S2','S3232332323S','3232S22S2323','S3232332323S','2S22333322S2']}
];
/* ============================ dom refs ============================ */
const stage=$('#stage'),cv=$('#game'),ctx=cv.getContext('2d');
const scoreEl=$('#score'),hiEl=$('#hiscore'),lvNameEl=$('#levelname'),lvSubEl=$('#levelsub'),
livesEl=$('#lives'),muteBtn=$('#muteBtn'),hiTitle=$('#hiTitle'),
ovTitle=$('#ovTitle'),ovPause=$('#ovPause'),ovOver=$('#ovOver'),ovWin=$('#ovWin'),
overScore=$('#overScore'),overNew=$('#overNew'),winScore=$('#winScore'),winNew=$('#winNew');
const showOv=o=>o.classList.remove('hidden'),hideOv=o=>o.classList.add('hidden');
/* ============================ state ============================ */
let state='title',score=0,hi=0,lives=3,level=0,levelSpeed=330,remaining=0;
let combo=0,comboPulse=0,timeScale=1,shake=0,flashA=0,flashC='#fff';
let bannerT=0,bannerMain='',bannerSub='',levelBonus=0,clearT=0,fwT=0,loseCd=0,fireCd=0;
let bricks=[],balls=[],caps=[],bolts=[],parts=[],pops=[];
const paddle={x:W/2-56,w:112,wt:112,flash:0,squash:0};
const effects={wide:0,laser:0,slow:0};
const keys={l:0,r:0};let mouseOn=false,mouseX=W/2;
try{hi=parseInt(localStorage.getItem('novaBreakoutHi'))||0;}catch(e){}
/* stars + nebulae */
const stars=[];for(let i=0;i<110;i++)stars.push({x:Math.random()*W,y:Math.random()*H,
z:.25+Math.random()*.75,ph:Math.random()*TAU,sp:rand(.6,2.2)});
const nebs=[{x:W*.25,y:H*.3,r:320,c:'rgba(76,201,240,.10)',ph:0},
{x:W*.75,y:H*.62,r:360,c:'rgba(255,79,105,.08)',ph:2},
{x:W*.55,y:H*.15,r:260,c:'rgba(255,210,63,.05)',ph:4}];
/* ============================ setup ============================ */
function buildBricks(i){
bricks=[];remaining=0;
const m=LEVELS[i].map;
for(let r=0;r<m.length;r++)for(let c=0;c<COLS;c++){
const ch=m[r][c];if(ch==='.')continue;
const x=OX+c*(BW+GAPX),y=OY+r*(BH+GAPY);
const b={x,y,w:BW,h:BH,cx:x+BW/2,cy:y+BH/2,alive:true,flash:0,type:'brick',hp:1,maxHp:1,row:r,color:PAL[r%PAL.length]};
if(ch==='S'){b.type='steel';b.color=CST;}else{b.hp=b.maxHp=ch|0;b.color=b.maxHp===1?PAL[r%PAL.length]:b.maxHp===2?C2:C3;remaining++;}
bricks.push(b);
}
}
function makeStuckBall(){balls=[{x:paddle.x+paddle.w/2,y:PY-8,vx:0,vy:0,r:7,speed:levelSpeed,trail:[],stuck:true}];}
function loadLevel(i){
level=i;buildBricks(i);levelSpeed=LEVELS[i].speed;
caps=[];bolts=[];paddle.x=W/2-paddle.w/2;paddle.wt=112;paddle.w=112;
effects.wide=effects.laser=effects.slow=0;combo=0;
makeStuckBall();
bannerMain='SECTOR '+(i+1);bannerSub=LEVELS[i].name;bannerT=2.3;
lvNameEl.textContent='SECTOR '+(i+1);lvSubEl.textContent=LEVELS[i].name;
}
function updateHUD(){
scoreEl.textContent=score.toLocaleString('en-US');
hiEl.textContent=hi.toLocaleString('en-US');
livesEl.innerHTML='<i></i>'.repeat(Math.max(0,lives));
}
function reset(){
score=0;lives=3;combo=0;parts=[];pops=[];loseCd=0;
loadLevel(0);updateHUD();
}
/* ============================ juice ============================ */
function addScore(n,x,y,big){
score+=n;if(score>hi){hi=score;try{localStorage.setItem('novaBreakoutHi',hi);}catch(e){}}
if(x!==undefined)pops.push({x,y,t:big?'+ '+n.toLocaleString('en-US'):'+'+n,life:big?1.4:.9,
max:big?1.4:.9,big:!!big,col:big?'#3ddc97':'#ffd23f'});
updateHUD();
}
function burst(x,y,col,n,pw){
for(let i=0;i<n;i++){const a=rand(0,TAU),s=rand(60,240)*(pw||1);
parts.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-60,g:520,life:rand(.35,.8),max:.8,
size:rand(2,5),col,type:'shard',rot:rand(0,TAU),vr:rand(-8,8)});
if(parts.length>430)parts.shift();}
}
function sparks(x,y,n){
for(let i=0;i<n;i++){const a=rand(0,TAU),s=rand(120,340);
parts.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,g:200,life:rand(.15,.35),max:.35,
size:rand(1,2.4),col:'#dfe9ff',type:'spark'});
if(parts.length>430)parts.shift();}
}
function firework(x,y){
const col=PAL[(Math.random()*PAL.length)|0];
for(let i=0;i<26;i++){const a=i/26*TAU,s=rand(90,260);
parts.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,g:160,life:rand(.5,1),max:1,
size:rand(1.6,3),col,type:'spark'});}
}
/* ============================ gameplay ============================ */
function spawnCap(x,y){
if(caps.length>=3||Math.random()>.14)return;
const r=Math.random(),type=r<.26?'W':r<.52?'L':r<.72?'T':r<.92?'S':'H';
caps.push({x,y,vy:135,type,wob:rand(0,TAU)});sfx.spawn();
}
function applyCap(cp){
if(cp.type==='H'){
if(lives<6){lives++;pops.push({x:cp.x,y:PY-24,t:'+1 SHIP',life:1.2,max:1.2,big:true,col:'#3ddc97'});}
else addScore(500,cp.x,PY-24,true);
sfx.life();
}else if(cp.type==='W'){effects.wide=12;paddle.wt=178;sfx.power();}
else if(cp.type==='L'){effects.laser=10;sfx.power();}
else if(cp.type==='S'){effects.slow=8;sfx.power();}
else if(cp.type==='T'){
const snap=balls.filter(b=>!b.stuck);
for(const b of snap){if(balls.length>=9)break;
const sp=b.speed,a=Math.atan2(b.vy,b.vx);
for(const da of[-.42,.42]){if(balls.length>=9)break;
balls.push({x:b.x,y:b.y,vx:Math.cos(a+da)*sp,vy:Math.sin(a+da)*sp,r:7,speed:sp,trail:[],stuck:false});}}
sfx.power();
}
updateHUD();
}
function damageBrick(b,dmg,px,py,opt){
opt=opt||{};
if(b.type==='steel'){b.flash=1;sparks(px,py,6);if(!opt.silent){sfx.steel();shake+=1.2;}return;}
b.hp-=dmg;b.flash=1;
if(b.hp<=0){
b.alive=false;remaining--;
burst(b.cx,b.cy,b.color,14+dmg*4,1);shake+=1.5+b.maxHp*.8;
if(opt.silent)return;
if(!opt.noCombo){combo++;comboPulse=1;}
const multi=Math.min(6,1+Math.floor(combo/4));
addScore(BASE[b.maxHp]*multi,b.cx,b.cy);
spawnCap(b.cx,b.cy);
opt.noCombo?sfx.tick():sfx.brick(combo);
if(remaining<=0)startClear();
}else{
sparks(px,py,5);
if(!opt.silent){addScore(20);sfx.tick();shake+=.6;}
}
}
function startClear(){
if(state!=='play')return;
state='clear';clearT=0;fwT=0;
levelBonus=300+level*200+lives*150;
addScore(levelBonus);sfx.clear();shake+=3;
}
function loseLife(){
lives--;updateHUD();combo=0;
shake=14;flashA=.55;flashC='rgba(255,60,70,';
stage.classList.add('hurt');setTimeout(()=>stage.classList.remove('hurt'),350);
sfx.hurt();
if(lives<=0){
state='over';overScore.textContent=score.toLocaleString('en-US');
overNew.classList.toggle('on',score>=hi&&score>0);showOv(ovOver);sfx.over();
}else{loseCd=.7;makeStuckBall();}
}
function launch(){
const b=balls[0];if(!b||!b.stuck)return;
b.stuck=false;const a=-Math.PI/2+rand(-.3,.3);
b.vx=Math.cos(a)*levelSpeed;b.vy=Math.sin(a)*levelSpeed;b.speed=levelSpeed;
state='play';sfx.launch();
}
function fireLaser(){
if(effects.laser<=0||fireCd>0)return;
fireCd=.16;sfx.laser();
bolts.push({x:paddle.x+8,y:PY-4},{x:paddle.x+paddle.w-8,y:PY-4});
sparks(paddle.x+8,PY-2,3);sparks(paddle.x+paddle.w-8,PY-2,3);
}
/* ============================ physics ============================ */
function movePaddle(dt){
if(keys.l||keys.r){
const d=((keys.r?1:0)-(keys.l?1:0))*640*dt;
paddle.x=clamp(paddle.x+d,-10,W-paddle.w+10);
}else if(mouseOn){
paddle.x=clamp(mouseX-paddle.w/2,-10,W-paddle.w+10);
}
paddle.w+=(paddle.wt-paddle.w)*Math.min(1,8*dt);
paddle.flash=Math.max(0,paddle.flash-dt*5);
paddle.squash=Math.max(0,paddle.squash-dt*4);
}
function paddleHit(b){
const rel=clamp((b.x-(paddle.x+paddle.w/2))/(paddle.w/2),-1,1);
b.speed=Math.min(b.speed*1.028+4,680);
const a=-Math.PI/2+rel*1.05;
b.vx=Math.cos(a)*b.speed;b.vy=Math.sin(a)*b.speed;
b.y=PY-b.r-.5;
combo=0;paddle.flash=1;paddle.squash=1;
sparks(b.x,PY,5);sfx.paddle(rel);
}
function stepWorld(dt,silent){
/* capsules */
for(let i=caps.length-1;i>=0;i--){const c=caps[i];
c.y+=c.vy*dt;c.wob+=dt*5;c.x+=Math.sin(c.wob)*12*dt;
if(!silent&&c.y+9>=PY&&c.y-9<=PY+PH&&c.x>=paddle.x-17&&c.x<=paddle.x+paddle.w+17){
applyCap(c);burst(c.x,PY,'#ffffff',8,.6);caps.splice(i,1);continue;}
if(c.y>H+30)caps.splice(i,1);
}
/* laser bolts */
for(let i=bolts.length-1;i>=0;i--){const L=bolts[i];L.y-=900*dt;
let hit=null;
for(const b of bricks){if(!b.alive)continue;
if(L.x>b.x-2&&L.x<b.x+b.w+2&&L.y>b.y-2&&L.y<b.y+b.h+2){hit=b;break;}}
if(hit){damageBrick(hit,1,L.x,L.y,{silent,noCombo:true});bolts.splice(i,1);continue;}
if(L.y<-20)bolts.splice(i,1);
}
/* balls */
for(let i=balls.length-1;i>=0;i--){const b=balls[i];
if(b.stuck){b.x=paddle.x+paddle.w/2;b.y=PY-b.r-2;continue;}
const n=3,sdt=dt/n;
for(let s=0;s<n;s++){
b.x+=b.vx*sdt;b.y+=b.vy*sdt;
if(b.x<b.r){b.x=b.r;b.vx=Math.abs(b.vx);}
if(b.x>W-b.r){b.x=W-b.r;b.vx=-Math.abs(b.vx);}
if(b.y<b.r){b.y=b.r;b.vy=Math.abs(b.vy);}
if(b.vy>0&&b.y+b.r>=PY&&b.y+b.r<=PY+PH+16&&b.x>=paddle.x-b.r&&b.x<=paddle.x+paddle.w+b.r)paddleHit(b);
for(const k of bricks){if(!k.alive)continue;
const dx=Math.abs(b.x-k.cx),dy=Math.abs(b.y-k.cy);
if(dx>k.w/2+b.r||dy>k.h/2+b.r)continue;
const ox=k.w/2+b.r-dx,oy=k.h/2+b.r-dy;
if(ox<oy){b.vx=(b.x>k.cx?1:-1)*Math.abs(b.vx);b.x+=b.x>k.cx?ox:-ox;}
else{b.vy=(b.y>k.cy?1:-1)*Math.abs(b.vy);b.y+=b.y>k.cy?oy:-oy;}
if(Math.abs(b.vy)<b.speed*.22)b.vy=(b.vy>=0?1:-1)*b.speed*.22;
const m=Math.hypot(b.vx,b.vy)||1;b.vx*=b.speed/m;b.vy*=b.speed/m;
damageBrick(k,1,b.x,b.y,{silent});break;
}
}
b.trail.push({x:b.x,y:b.y});if(b.trail.length>10)b.trail.shift();
if(b.y>H+40)balls.splice(i,1);
}
}
/* ============================ update ============================ */
function spawnAttractBall(){
const a=rand(.5,1.2)*(Math.random()<.5?1:-1);
balls=[{x:rand(120,W-120),y:130,vx:Math.cos(Math.PI/2+a)*320,vy:Math.abs(Math.sin(Math.PI/2+a))*320,
r:7,speed:320,trail:[],stuck:false}];
}
function ambient(dt){
shake=Math.max(0,shake-dt*26);
flashA=Math.max(0,flashA-dt*1.6);
comboPulse=Math.max(0,comboPulse-dt*3);
for(const s of stars){s.x-=s.z*7*dt;s.y+=s.z*13*dt;
if(s.y>H+2){s.y=-2;s.x=Math.random()*W}if(s.x<-2){s.x=W+2;s.y=Math.random()*H}}
}
function updateFX(dt){
for(let i=parts.length-1;i>=0;i--){const p=parts[i];p.life-=dt;
if(p.life<=0){parts.splice(i,1);continue;}
p.vy+=p.g*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;if(p.rot!==undefined)p.rot+=p.vr*dt;}
for(let i=pops.length-1;i>=0;i--){const p=pops[i];p.life-=dt;p.y-=36*dt;
if(p.life<=0)pops.splice(i,1);}
}
function update(dt,t){
ambient(dt);
const target=effects.slow>0?.55:1;
timeScale+=(target-timeScale)*Math.min(1,6*dt);
if(state==='title'){
movePaddle(dt);
const b=balls[0];
if(b&&!b.stuck)paddle.x+=((b.x-paddle.w/2)-paddle.x)*Math.min(1,4.5*dt);
stepWorld(dt,true);
if(!balls.length)spawnAttractBall();
if(remaining<10)buildBricks(0);
updateFX(dt);return;
}
if(state==='paused'){updateFX(dt);return;}
if(state==='serve'){
movePaddle(dt);stepWorld(dt,false);updateFX(dt);
if(loseCd>0)loseCd-=dt;return;
}
if(state==='play'){
fireCd-=dt;
effects.wide=Math.max(0,effects.wide-dt);
effects.laser=Math.max(0,effects.laser-dt);
effects.slow=Math.max(0,effects.slow-dt);
if(effects.wide<=0)paddle.wt=112;
movePaddle(dt);
const wdt=dt*timeScale;
stepWorld(wdt,false);
updateFX(dt);
if(!balls.length&&loseCd<=0)loseLife();
return;
}
if(state==='clear'){
movePaddle(dt);clearT+=dt;fwT-=dt;
if(fwT<=0){fwT=.28;firework(rand(100,W-100),rand(90,300));}
updateFX(dt);
if(clearT>2.6){
if(level+1>=LEVELS.length){
state='win';winScore.textContent=score.toLocaleString('en-US');
winNew.classList.toggle('on',score>=hi&&score>0);showOv(ovWin);sfx.win();
}else{loadLevel(level+1);state='serve';}
}
return;
}
updateFX(dt); /* over / win */
}
/* ============================ render ============================ */
function drawBrick(b){
const c=ctx;
c.fillStyle=b.color;rr(c,b.x,b.y,b.w,b.h,4);c.fill();
c.fillStyle='rgba(255,255,255,.28)';c.fillRect(b.x+3,b.y+2,b.w-6,2.5);
c.fillStyle='rgba(0,0,20,.3)';c.fillRect(b.x+3,b.y+b.h-4,b.w-6,2);
if(b.type==='steel'){
c.save();rr(c,b.x,b.y,b.w,b.h,4);c.clip();
c.strokeStyle='rgba(255,255,255,.12)';c.lineWidth=3;
for(let x=b.x-b.h;x<b.x+b.w;x+=10){c.beginPath();c.moveTo(x,b.y+b.h+2);c.lineTo(x+b.h+4,b.y-2);c.stroke();}
c.restore();
c.fillStyle='#39424f';
for(const p of[[5,5],[b.w-5,5],[5,b.h-5],[b.w-5,b.h-5]]){c.beginPath();c.arc(b.x+p[0],b.y+p[1],1.8,0,TAU);c.fill();}
}else{
c.strokeStyle='rgba(8,12,26,.4)';c.lineWidth=1;rr(c,b.x+.5,b.y+.5,b.w-1,b.h-1,4);c.stroke();
if(b.hp<b.maxHp){
c.strokeStyle='rgba(10,14,28,.6)';c.lineWidth=1.4;c.beginPath();
c.moveTo(b.x+b.w*.3,b.y+2);c.lineTo(b.x+b.w*.42,b.y+b.h*.5);c.lineTo(b.x+b.w*.28,b.y+b.h-2);
c.moveTo(b.x+b.w*.68,b.y+2);c.lineTo(b.x+b.w*.6,b.y+b.h*.55);c.lineTo(b.x+b.w*.76,b.y+b.h-2);
c.stroke();
}
if(b.maxHp>1){c.fillStyle='rgba(8,12,26,.55)';
for(let i=0;i<b.maxHp;i++){c.beginPath();c.arc(b.cx-(b.maxHp-1)*4+i*8,b.y+b.h-5,1.7,0,TAU);c.fill();}}
}
if(b.flash>0){c.fillStyle=`rgba(255,255,255,${b.flash*.75})`;rr(c,b.x,b.y,b.w,b.h,4);c.fill();b.flash=Math.max(0,b.flash-.08);}
}
function pill(x,y,label,col,frac){
ctx.font='700 10px '+'ui-monospace,Menlo,Consolas,monospace';
const w=Math.max(64,ctx.measureText(label).width+26);
ctx.fillStyle='rgba(9,14,28,.85)';rr(ctx,x,y,w,18,9);ctx.fill();
ctx.strokeStyle=col;ctx.lineWidth=1.2;rr(ctx,x,y,w,18,9);ctx.stroke();
if(frac!==undefined){ctx.fillStyle=col;ctx.globalAlpha=.4;rr(ctx,x+2,y+13,(w-4)*frac,3,1.5);ctx.fill();ctx.globalAlpha=1;}
ctx.fillStyle='#eaf3ff';ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText(label,x+w/2,y+9);
return w;
}
const DFONT="'Bahnschrift','Avenir Next Condensed','Arial Narrow',Impact,sans-serif";
function render(t){
const s=cv._sc||1;
ctx.setTransform(s,0,0,s,0,0);
/* backdrop */
const g=ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#0a1226');g.addColorStop(.55,'#070c1a');g.addColorStop(1,'#05080f');
ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
for(const n of nebs){
const nx=n.x+Math.sin(t*.13+n.ph)*40,ny=n.y+Math.cos(t*.1+n.ph)*26;
const rg=ctx.createRadialGradient(nx,ny,0,nx,ny,n.r);
rg.addColorStop(0,n.c);rg.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=rg;ctx.fillRect(nx-n.r,ny-n.r,n.r*2,n.r*2);
}
for(const st of stars){
ctx.globalAlpha=(.35+.65*Math.abs(Math.sin(t*st.sp+st.ph)))*st.z;
ctx.fillStyle='#cfe4ff';ctx.fillRect(st.x,st.y,st.z*1.8,st.z*1.8);
}
ctx.globalAlpha=1;
/* shake */
if(shake>0)ctx.translate(rand(-shake,shake)*.5,rand(-shake,shake)*.5);
/* bricks */
for(const b of bricks)if(b.alive)drawBrick(b);
/* capsules */
const capCol={W:'#ffd23f',L:'#ff4f69',T:'#3ddc97',S:'#4cc9f0',H:'#ff8ad0'};
const capLetter={W:'W',L:'L',T:'T',S:'S',H:'♥'};
for(const c of caps){
ctx.save();ctx.translate(c.x,c.y);ctx.rotate(Math.sin(c.wob)*.15);
ctx.shadowColor=capCol[c.type];ctx.shadowBlur=12;
ctx.fillStyle=capCol[c.type];rr(ctx,-17,-9,34,18,9);ctx.fill();
ctx.shadowBlur=0;ctx.fillStyle='rgba(255,255,255,.35)';rr(ctx,-14,-7,28,6,3);ctx.fill();
ctx.fillStyle='#0b1020';ctx.font="700 12px "+DFONT;ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText(capLetter[c.type],0,1);ctx.restore();
}
/* bolts */
ctx.fillStyle='#ff6b81';ctx.shadowColor='#ff4f69';ctx.shadowBlur=10;
for(const L of bolts){ctx.fillRect(L.x-1.5,L.y-12,3,13);}ctx.shadowBlur=0;
/* particles */
for(const p of parts){
const a=clamp(p.life/p.max,0,1);
if(p.type==='shard'){ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.rot);
ctx.globalAlpha=a;ctx.fillStyle=p.col;ctx.fillRect(-p.size/2,-p.size/2,p.size,p.size*.7);ctx.restore();}
else{ctx.globalAlpha=a;ctx.fillStyle=p.col;
ctx.beginPath();ctx.arc(p.x,p.y,p.size,0,TAU);ctx.fill();}
}
ctx.globalAlpha=1;
/* balls */
for(const b of balls){
for(let i=0;i<b.trail.length;i++){const tr=b.trail[i],f=(i+1)/b.trail.length;
ctx.globalAlpha=f*.3;ctx.fillStyle='#9be8ff';
ctx.beginPath();ctx.arc(tr.x,tr.y,b.r*f,0,TAU);ctx.fill();}
ctx.globalAlpha=1;ctx.shadowColor='#4cc9f0';ctx.shadowBlur=16;
ctx.fillStyle='#eafaff';ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,TAU);ctx.fill();
ctx.shadowBlur=0;ctx.fillStyle='rgba(255,255,255,.9)';
ctx.beginPath();ctx.arc(b.x-2,b.y-2,2.4,0,TAU);ctx.fill();
}
/* paddle */
{
const sq=1-paddle.squash*.35,ph=PH*sq,py=PY+(PH-ph);
ctx.shadowColor='#4cc9f0';ctx.shadowBlur=14+paddle.flash*22;
const pg=ctx.createLinearGradient(0,py,0,py+ph);
pg.addColorStop(0,'#a5f3ff');pg.addColorStop(.45,'#37b6e9');pg.addColorStop(1,'#155a86');
ctx.fillStyle=pg;rr(ctx,paddle.x,py,paddle.w,ph,ph/2);ctx.fill();
ctx.shadowBlur=0;
if(effects.laser>0){ctx.fillStyle='#ff4f69';ctx.shadowColor='#ff4f69';ctx.shadowBlur=8;
ctx.fillRect(paddle.x+5,py-5,6,5);ctx.fillRect(paddle.x+paddle.w-11,py-5,6,5);ctx.shadowBlur=0;}
if(paddle.flash>0){ctx.fillStyle=`rgba(255,255,255,${paddle.flash*.5})`;
rr(ctx,paddle.x,py,paddle.w,ph,ph/2);ctx.fill();}
}
/* popups */
ctx.textAlign='center';ctx.textBaseline='middle';
for(const p of pops){
ctx.globalAlpha=clamp(p.life/p.max*1.4,0,1);
ctx.font=(p.big?'700 17px ':'700 13px ')+DFONT;
ctx.fillStyle='#0b1020';ctx.fillText(p.t,p.x+1,p.y+1);
ctx.fillStyle=p.col;ctx.fillText(p.t,p.x,p.y);
}
ctx.globalAlpha=1;
/* combo */
if(combo>=2&&(state==='play'||state==='serve')){
const multi=Math.min(6,1+Math.floor(combo/4));
ctx.save();ctx.translate(44,72);ctx.scale(1+comboPulse*.35,1+comboPulse*.35);
ctx.font='900 16px '+DFONT;ctx.textAlign='left';
ctx.fillStyle='#0b1020';ctx.fillText(`COMBO ×${multi}`,2,2);
ctx.fillStyle='#ffd23f';ctx.fillText(`COMBO ×${multi}`,0,0);
ctx.font='700 10px ui-monospace,Menlo,monospace';ctx.fillStyle='#c9a227';
ctx.fillText(combo+' HITS',2,14);ctx.restore();
}
/* effect pills */
{
const defs=[['WIDE',effects.wide,12,'#ffd23f'],['LASER',effects.laser,10,'#ff4f69'],['SLOW-MO',effects.slow,8,'#4cc9f0']];
const act=defs.filter(d=>d[1]>0);
let totW=act.reduce((a,d)=>a+Math.max(64,ctx.measureText(d[0]).width+26)+10,0)-10;
let x=W/2-totW/2;
for(const d of act){const w=pill(x,60,`${d[0]} ${Math.ceil(d[1])}s`,d[3],d[1]/d[2]);x+=w+10;}
}
/* banners + hints */
if(bannerT>0&&state!=='title'){
const e=2.3-bannerT;let a=1;
if(e<.3)a=e/.3;else if(bannerT<.4)a=bannerT/.4;
const sc=1+Math.max(0,.3-e)*1.6;
ctx.save();ctx.globalAlpha=a;ctx.translate(W/2,215);ctx.scale(sc,sc);
ctx.textAlign='center';ctx.font='900 46px '+DFONT;
ctx.fillStyle='#0b1020';ctx.fillText(bannerMain,3,3);
ctx.fillStyle='#eaf3ff';ctx.fillText(bannerMain,0,0);
ctx.font='700 15px ui-monospace,Menlo,monospace';ctx.fillStyle='#ffd23f';
ctx.fillText('— '+bannerSub+' —',0,30);ctx.restore();
}
if(state==='clear'){
const a=clamp(clearT*3,0,1);
ctx.save();ctx.globalAlpha=a;ctx.textAlign='center';
ctx.font='900 44px '+DFONT;ctx.fillStyle='#0b1020';ctx.fillText('SECTOR CLEAR',W/2+3,223);
ctx.fillStyle='#3ddc97';ctx.fillText('SECTOR CLEAR',W/2,220);
ctx.font='700 16px ui-monospace,Menlo,monospace';ctx.fillStyle='#ffd23f';
ctx.fillText('BONUS +'+levelBonus.toLocaleString('en-US'),W/2,252);ctx.restore();
}
if(state==='serve'&&bannerT<=.4&&loseCd<=0&&Math.sin(t*6)>-.3){
ctx.font='700 12px ui-monospace,Menlo,monospace';ctx.textAlign='center';
ctx.fillStyle='#8fa7cc';ctx.fillText('SPACE / CLICK TO LAUNCH',W/2,PY-30);
}
ctx.setTransform(s,0,0,s,0,0);
if(flashA>0){ctx.fillStyle=flashC+flashA+')';ctx.fillRect(0,0,W,H);}
}
/* ============================ input ============================ */
function primary(){
ensureAudio();
if(state==='title'){hideOv(ovTitle);reset();state='serve';return;}
if(state==='over'||state==='win'){hideOv(ovOver);hideOv(ovWin);reset();state='serve';return;}
if(state==='serve'){launch();return;}
if(state==='play'){fireLaser();return;}
}
window.addEventListener('keydown',e=>{
const k=e.key.toLowerCase();ensureAudio();
if([' ','arrowleft','arrowright','arrowup','arrowdown'].includes(k))e.preventDefault();
if(k==='arrowleft'||k==='a')keys.l=1;
if(k==='arrowright'||k==='d')keys.r=1;
if(k===' '||k==='enter')primary();
if(k==='m')toggleMute();
if(k==='p'&&(state==='play'||state==='serve'||state==='paused')){
if(state==='paused'){state=balls[0]&&balls[0].stuck?'serve':'play';hideOv(ovPause);}
else{state='paused';showOv(ovPause);}
}
});
window.addEventListener('keyup',e=>{
const k=e.key.toLowerCase();
if(k==='arrowleft'||k==='a')keys.l=0;
if(k==='arrowright'||k==='d')keys.r=0;
});
stage.addEventListener('pointermove',e=>{
const r=cv.getBoundingClientRect();
mouseX=(e.clientX-r.left)/r.width*W;mouseOn=true;
});
stage.addEventListener('pointerdown',e=>{
const r=cv.getBoundingClientRect();
mouseX=(e.clientX-r.left)/r.width*W;mouseOn=true;primary();
});
muteBtn.addEventListener('pointerdown',e=>e.stopPropagation());
muteBtn.addEventListener('click',e=>{e.stopPropagation();ensureAudio();toggleMute();});
function toggleMute(){muted=!muted;muteBtn.textContent=muted?'♪ OFF':'♪ ON';}
/* ============================ boot ============================ */
let viewScale=1;
function fit(){
const sc=clamp(Math.min((innerWidth-28)/W,(innerHeight-92)/H),.35,1.5);
stage.style.width=(W*sc)+'px';stage.style.height=(H*sc)+'px';
stage.style.fontSize=(16*sc)+'px';
const dpr=Math.min(window.devicePixelRatio||1,2);
cv.width=Math.max(2,Math.round(W*sc*dpr));cv.height=Math.max(2,Math.round(H*sc*dpr));
cv._sc=sc*dpr;viewScale=sc*dpr;
}
addEventListener('resize',fit);
cv.style.cursor='crosshair';
reset();state='title';spawnAttractBall();
hiTitle.textContent=hi.toLocaleString('en-US');updateHUD();
fit();
let last=performance.now();
function frame(t){
const dt=Math.min((t-last)/1000,.033);last=t;
update(dt,t/1000);render(t/1000);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>

