Run details
- Model
- qwen/qwen3.8-max-prime
- 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
- 1 Nov 2026
- Time
- 8 min 8 s
- Size
- 33 kB
- Tokens
- 138 in, 42,625 out (29,250 of them thinking)
- List price
- $0.51 at today's price ($4.00 in / $12.00 out per M)
- Paid
- $0.51
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 671 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NEON BREAKER — Arcade Breakout</title>
<style>
:root{
--cy:#3fe0ff; --mg:#ff4fd8; --yl:#ffd23f; --or:#ff9f45; --rd:#ff4d6d;
--disp:Impact,'Haettenschweiler','Franklin Gothic Medium','Arial Black',sans-serif;
--mono:Consolas,'SF Mono',Menlo,'Courier New',monospace;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
min-height:100vh;display:flex;align-items:center;justify-content:center;padding:22px 12px;
font-family:'Segoe UI',system-ui,-apple-system,sans-serif;color:#cfe3ff;overflow-x:hidden;
background:
radial-gradient(1100px 560px at 50% -12%, #1c2347 0%, rgba(28,35,71,0) 62%),
radial-gradient(760px 480px at 88% 112%, rgba(255,79,216,.10), rgba(255,79,216,0) 62%),
radial-gradient(760px 480px at 10% 110%, rgba(63,224,255,.10), rgba(63,224,255,0) 62%),
#070a18;
}
body::before{content:'';position:fixed;inset:-40%;pointer-events:none;z-index:0;
background:
radial-gradient(620px 420px at 22% 30%, rgba(63,224,255,.06), transparent 70%),
radial-gradient(700px 520px at 78% 72%, rgba(255,79,216,.06), transparent 70%);
animation:drift 26s ease-in-out infinite alternate;}
@keyframes drift{to{transform:translate(4%,-3%) scale(1.07)}}
.cab{position:relative;z-index:1;width:min(1010px,97vw);
background:linear-gradient(180deg,#101536,#0b0f26);
border:1px solid #2c3a6e;border-radius:20px;padding:18px 20px 12px;
box-shadow:0 40px 90px rgba(0,0,0,.65), inset 0 1px 0 rgba(255,255,255,.04), 0 0 70px rgba(63,224,255,.06);}
header{display:flex;align-items:flex-end;gap:20px;flex-wrap:wrap;padding:2px 4px 14px}
.brand{font-family:var(--disp);font-size:32px;line-height:.95;letter-spacing:3px;color:#fff;
text-shadow:-2px 0 rgba(255,79,216,.75), 2px 0 rgba(63,224,255,.75), 0 0 22px rgba(63,224,255,.35);}
.brand em{font-style:normal;color:var(--cy)}
.brand small{display:block;font-family:var(--mono);font-size:10px;letter-spacing:5px;color:#6d7fc0;margin-top:6px;text-shadow:none}
.stats{margin-left:auto;display:flex;gap:26px;align-items:flex-end;flex-wrap:wrap}
.stat label{display:block;font-size:10px;letter-spacing:3px;color:#6d7fc0;margin-bottom:4px}
.stat b{display:inline-block;font:700 24px/1 var(--mono);color:#eaf6ff;text-shadow:0 0 12px rgba(63,224,255,.35)}
.stat b.bump{animation:bump .25s}
@keyframes bump{40%{transform:scale(1.22);color:var(--yl)}}
#combo.hot{color:var(--yl);text-shadow:0 0 14px rgba(255,210,63,.6);animation:hot .45s infinite alternate}
@keyframes hot{to{transform:scale(1.12)}}
#lvlname{display:block;font:600 10px var(--mono);letter-spacing:2px;color:#8fa3e8;margin-top:4px}
#lives{display:flex;gap:6px;padding:6px 0}
.dot{width:13px;height:13px;border-radius:50%;
background:radial-gradient(circle at 35% 30%, #fff, var(--cy) 55%, #0a6c86);
box-shadow:0 0 10px rgba(63,224,255,.8)}
.dot.off{background:#1c2447;box-shadow:none;border:1px solid #2c3a6e}
.stage{position:relative;border-radius:14px;overflow:hidden;border:1px solid #26305c;background:#05070f;
box-shadow:inset 0 0 60px rgba(0,0,0,.6);user-select:none;-webkit-user-select:none}
canvas{display:block;width:100%;height:auto;cursor:none}
.fx{position:absolute;inset:0;pointer-events:none}
.scan{background:repeating-linear-gradient(0deg, rgba(0,0,0,.13) 0 1px, transparent 1px 3px)}
.vig{background:radial-gradient(120% 92% at 50% 45%, transparent 56%, rgba(0,0,0,.42))}
#banner{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:8px;pointer-events:none;opacity:0;z-index:4}
#banner.show{animation:ban 2s ease forwards}
#banner b{font-family:var(--disp);font-weight:400;font-size:clamp(28px,5vw,46px);letter-spacing:8px;color:#fff;
text-shadow:-3px 0 rgba(255,79,216,.7), 3px 0 rgba(63,224,255,.7), 0 0 30px rgba(63,224,255,.5)}
#banner span{font:700 14px var(--mono);letter-spacing:4px;color:var(--yl);text-shadow:0 0 12px rgba(255,210,63,.5)}
@keyframes ban{0%{opacity:0;transform:translateY(22px) scale(.92)}14%{opacity:1;transform:none}72%{opacity:1}100%{opacity:0;transform:translateY(-14px)}}
#overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
background:rgba(6,9,22,.86);transition:opacity .25s;z-index:5}
#overlay.hidden{opacity:0;pointer-events:none}
.ov{max-width:580px;text-align:center;padding:20px;animation:pop .3s ease}
@keyframes pop{from{opacity:0;transform:scale(.94) translateY(10px)}}
.ov-kicker{font:700 11px var(--mono);letter-spacing:5px;color:#6d7fc0;margin-bottom:10px}
.ov h2{font-family:var(--disp);font-weight:400;font-size:clamp(34px,5vw,52px);letter-spacing:5px;color:#fff;margin:0 0 12px;
text-shadow:-3px 0 rgba(255,79,216,.7), 3px 0 rgba(63,224,255,.7), 0 0 34px rgba(63,224,255,.4)}
.ov p{color:#9fb2e8;margin:0 0 18px;font-size:14px;line-height:1.5}
.legend{display:flex;gap:16px;justify-content:center;flex-wrap:wrap;margin:0 0 18px}
.legend div{display:flex;align-items:center;gap:7px;font:600 10px var(--mono);letter-spacing:1px;color:#8fa3e8}
.pw{--c:var(--cy);display:inline-flex;align-items:center;justify-content:center;width:27px;height:17px;border-radius:9px;
background:var(--c);color:#081018;font:800 11px var(--mono);box-shadow:0 0 12px var(--c)}
.keysline{display:flex;gap:16px;justify-content:center;flex-wrap:wrap;margin-bottom:20px;font-size:11px;color:#8fa3e8}
kbd{font:700 10px var(--mono);background:#161d3f;border:1px solid #3a4780;border-bottom-width:3px;border-radius:5px;
padding:2px 6px;color:#cfe3ff}
.ovstats{display:flex;gap:34px;justify-content:center;margin:0 0 22px}
.ovstats div{font:700 22px var(--mono);color:#eaf6ff}
.ovstats span{display:block;font:400 10px var(--mono);letter-spacing:3px;color:#6d7fc0;margin-bottom:4px}
.btn{font:700 14px var(--mono);letter-spacing:3px;color:#bff3ff;background:rgba(63,224,255,.07);
border:2px solid var(--cy);border-radius:10px;padding:13px 26px;cursor:pointer;margin:4px 6px;transition:.15s;
text-shadow:0 0 10px rgba(63,224,255,.5)}
.btn:hover{background:rgba(63,224,255,.2);box-shadow:0 0 26px rgba(63,224,255,.45);transform:translateY(-2px)}
.btn.alt{border-color:var(--mg);color:#ffd7f2;background:rgba(255,79,216,.07);text-shadow:0 0 10px rgba(255,79,216,.5)}
.btn.alt:hover{background:rgba(255,79,216,.18);box-shadow:0 0 26px rgba(255,79,216,.4)}
footer{display:flex;gap:18px;align-items:center;flex-wrap:wrap;padding:12px 4px 2px;font-size:11px;color:#6d7fc0}
footer .sp{margin-left:auto}
#snd{font:700 10px var(--mono);letter-spacing:2px;color:#8fa3e8;background:#141a3a;border:1px solid #2c3a6e;
border-radius:7px;padding:5px 10px;cursor:pointer;transition:.15s}
#snd:hover{color:#cfe3ff;border-color:var(--cy)}
</style>
</head>
<body>
<div class="cab">
<header>
<div class="brand">NEON<em>BREAKER</em><small>ARCADE BLOCK PROTOCOL</small></div>
<div class="stats">
<div class="stat"><label>SCORE</label><b id="score">000000</b></div>
<div class="stat"><label>HI-SCORE</label><b id="hi">000000</b></div>
<div class="stat"><label>SECTOR</label><b id="level">01</b><span id="lvlname">—</span></div>
<div class="stat"><label>LIVES</label><div id="lives"></div></div>
<div class="stat"><label>COMBO</label><b id="combo">—</b></div>
</div>
</header>
<div class="stage" id="stage">
<canvas id="cv" width="960" height="620"></canvas>
<div class="fx scan"></div>
<div class="fx vig"></div>
<div id="banner"></div>
<div id="overlay"><div class="ov" id="ovContent"></div></div>
</div>
<footer>
<span><kbd>←</kbd><kbd>→</kbd> / <kbd>A</kbd><kbd>D</kbd> MOVE</span>
<span><kbd>SPACE</kbd> / <kbd>CLICK</kbd> LAUNCH · FIRE</span>
<span><kbd>P</kbd> PAUSE</span>
<span><kbd>M</kbd> SOUND</span>
<span class="sp"><button id="snd">SOUND: ON</button></span>
</footer>
</div>
<script>
(() => {
'use strict';
/* ============ setup ============ */
const W = 960, H = 620, WALL = 4;
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
const DPR = Math.min(2, window.devicePixelRatio || 1);
cv.width = W * DPR; cv.height = H * DPR; ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
const $ = id => document.getElementById(id);
const scoreEl=$('score'), hiEl=$('hi'), levelEl=$('level'), lvlnameEl=$('lvlname'),
livesEl=$('lives'), comboEl=$('combo'), overlay=$('overlay'), ovContent=$('ovContent'),
banner=$('banner'), sndBtn=$('snd');
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
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();}
/* ============ levels ============ */
const ROWCOL=['#ff4d6d','#ff9440','#ffd23f','#6bf178','#3fe0ff','#6f8bff','#c66bff'];
const LEVELS=[
{name:'WARM-UP GRID', map:[
'############','############','############','############']},
{name:'CHECKERBOARD', map:[
'#.#.#.#.#.#.','.#.#.#.#.#.#','#.#.#.#.#.#.','.#.#.#.#.#.#','#.#.#.#.#.#.']},
{name:'THE PYRAMID', map:[
'.....##.....','....####....','...######...','..########..','.##########.','############']},
{name:'THE VAULT', map:[
'222222222222','2..........2','2.########.2','2.33333333.2','2.########.2','2..........2','222222222222']},
{name:'BATTLEMENTS', map:[
'##.##.##.##.','##.##.##.##.','##.##.##.##.','############','############']},
{name:'ZIGZAG', map:[
'###.........','.###........','..###.......','...###......','....###.....','.....###....','......###...','.......###..']},
{name:'THE DIAMOND', map:[
'.....22.....','....#22#....','...#2##2#...','..#2#33#2#..','...#2##2#...','....#22#....','.....22.....']},
{name:'THE INVADER', map:[
'..#.....#...','...#...#....','..2222222...','.##.....##..','###########.','#.2222222.#.','#.#.....#.#.','...##.##....']},
];
const COLS=12, BW=70, GX=6, BH=24, GY=6, OX=27, OY=64;
/* ============ state ============ */
let state='title', paused=false;
let score=0, dispScore=0, lastShown=-1, hi=0, runStartHi=0, bestCombo=0;
let lives=3, levelIdx=0, endless=false, combo=0, left=0, totalBricks=1;
let bricks=[], balls=[], pows=[], bullets=[], parts=[], rings=[], texts=[];
let paddle={x:W/2, y:H-46, w:116, h:14, baseW:116, flash:0};
let eff={wide:0, laser:0, slow:0};
let fireCd=0, firing=false, clearTimer=0;
let shake=0, flashA=0, flashC='#ffffff', T=0, lastDt=0.016;
let keys={}, mouseX=W/2, useMouse=false;
try{ hi = +localStorage.getItem('nb.hi') || 0; }catch(e){}
function saveHi(){ try{ localStorage.setItem('nb.hi', hi); }catch(e){} }
/* ============ audio (all synthesized, no files) ============ */
let AC=null, master=null, muted=false;
function audio(){ if(!AC){ const A=window.AudioContext||window.webkitAudioContext; if(!A)return null;
AC=new A(); master=AC.createGain(); master.gain.value=.5; master.connect(AC.destination);}
if(AC.state==='suspended')AC.resume(); return AC; }
function tone(f0,f1,dur,type,vol,delay=0){ if(muted||!AC)return; try{
const t=AC.currentTime+delay, o=AC.createOscillator(), g=AC.createGain();
o.type=type; o.frequency.setValueAtTime(Math.max(30,f0),t);
o.frequency.exponentialRampToValueAtTime(Math.max(30,f1),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);}catch(e){} }
function arp(fs,step,type,vol){ fs.forEach((f,i)=>tone(f,f,.13,type,vol,i*step)); }
const S={
wall: ()=>tone(200,150,.05,'square',.10),
paddle:()=>tone(220,360,.07,'square',.18),
brick: (row,c)=>tone(460+row*34+Math.min(c,14)*16,(460+row*34)*1.6,.08,'square',.18),
hard: ()=>tone(950,640,.06,'triangle',.2),
launch:()=>tone(280,560,.12,'triangle',.2),
laser: ()=>tone(1400,320,.09,'sawtooth',.10),
pow: ()=>arp([520,660,880],.06,'triangle',.2),
multi: ()=>arp([440,554,659,880],.05,'square',.18),
life: ()=>arp([392,330,262],.09,'sawtooth',.2),
lose: ()=>arp([400,200,90],.14,'sawtooth',.24),
clear: ()=>arp([523,659,784,1047],.09,'square',.2),
over: ()=>arp([392,311,233,155],.16,'sawtooth',.22),
click: ()=>tone(700,920,.05,'square',.14),
};
function toggleMute(){ muted=!muted; sndBtn.textContent = muted?'SOUND: OFF':'SOUND: ON'; if(!muted){audio();S.click();} }
sndBtn.addEventListener('click',()=>{audio();toggleMute();});
/* ============ background ============ */
const stars=[]; for(let i=0;i<110;i++)stars.push({x:Math.random()*W,y:Math.random()*H,z:.3+Math.random()*.7,tw:1+Math.random()*3,ph:Math.random()*7});
const bgGrad=ctx.createLinearGradient(0,0,0,H);
bgGrad.addColorStop(0,'#0d1230'); bgGrad.addColorStop(.6,'#080b1e'); bgGrad.addColorStop(1,'#0a0e24');
function neb(x,y,r,c){const g=ctx.createRadialGradient(x,y,0,x,y,r);g.addColorStop(0,c);g.addColorStop(1,'rgba(0,0,0,0)');return g;}
const neb1=neb(W*.22,H*.3,430,'rgba(63,224,255,.055)'), neb2=neb(W*.8,H*.64,470,'rgba(255,79,216,.055)');
/* ============ level flow ============ */
function buildLevel(i){
const L=LEVELS[i%LEVELS.length]; bricks=[]; let r=0;
for(const row of L.map){
for(let c=0;c<COLS;c++){
const ch=row[c]||'.'; if(ch==='.')continue;
const hp=ch==='2'?2:ch==='3'?3:1;
bricks.push({x:OX+c*(BW+GX), y:OY+r*(BH+GY), w:BW, h:BH, hp, mhp:hp, row:r, col:c, flash:0,
color: hp===1?ROWCOL[r%ROWCOL.length] : hp===2?'#aebfd6':'#ffcf4d'});
} r++;
}
left=bricks.length; totalBricks=left;
}
function baseSpeed(){ return Math.min(620, 340*(1+0.05*levelIdx)); }
function makeBall(){ return {x:paddle.x, y:paddle.y-paddle.h/2-9, vx:0, vy:0, r:7, stuck:true, trail:[], kick:1}; }
function serve(){ balls=[makeBall()]; state='serve'; }
function launch(){
if(state!=='serve'||!balls[0])return;
const b=balls[0], th=Math.random()*.6-.3, sp=baseSpeed();
b.vx=Math.sin(th)*sp; b.vy=-Math.cos(th)*sp; b.stuck=false;
state='play'; S.launch(); rings.push({x:b.x,y:b.y,r:4,life:.4,ml:.4,color:'#3fe0ff'});
}
function showBanner(a,b){ banner.innerHTML='<b>'+a+'</b><span>'+b+'</span>';
banner.classList.remove('show'); void banner.offsetWidth; banner.classList.add('show'); }
function startLevel(i){
buildLevel(i); pows.length=0; bullets.length=0; texts.length=0;
eff={wide:0,laser:0,slow:0}; paddle.w=paddle.baseW; paddle.x=W/2; combo=0; setCombo();
const L=LEVELS[i%LEVELS.length];
levelEl.textContent=String(i+1).padStart(2,'0'); lvlnameEl.textContent=L.name;
serve(); showBanner('SECTOR '+String(i+1).padStart(2,'0'), L.name);
}
function winLevel(){
if(state!=='play')return;
state='clear'; clearTimer=2.0; balls.length=0; pows.length=0; bullets.length=0;
eff={wide:0,laser:0,slow:0}; paddle.w=paddle.baseW;
const bonus=100+lives*100; score+=bonus; if(score>hi)hi=score;
showBanner('SECTOR CLEAR','BONUS +'+bonus); S.clear();
flashA=.3; flashC='#ffffff'; shake=10; saveHi();
}
function nextLevel(){
levelIdx++;
if(levelIdx>=LEVELS.length && !endless){ state='win'; showOverlay('win'); return; }
startLevel(levelIdx);
}
function loseLife(){
lives--; combo=0; setCombo(); shake=18; flashA=.5; flashC='#ff2d55'; S.lose(); updateLives();
texts.push({x:W/2,y:H/2,txt:'BALL LOST',life:1.4,ml:1.4,color:'#ff4d6d',big:true});
if(lives<=0){ state='over'; saveHi(); showOverlay('over'); S.over(); }
else { serve(); saveHi(); }
}
function startGame(){
hideOverlay(); paused=false; score=0; dispScore=0; lives=3; combo=0; bestCombo=0;
endless=false; levelIdx=0; runStartHi=hi; updateLives(); startLevel(0);
}
function startEndless(){
hideOverlay(); paused=false; endless=true; lives=3; combo=0; runStartHi=hi;
updateLives(); startLevel(levelIdx);
}
/* ============ combat ============ */
function damageBrick(br,dmg,hx,hy){
br.hp-=dmg; br.flash=1;
if(br.hp<=0){
left--; combo++; if(combo>bestCombo)bestCombo=combo; setCombo();
const base=br.mhp===3?60:br.mhp===2?30:10, gain=base+combo*5;
score+=gain; if(score>hi)hi=score;
texts.push({x:br.x+br.w/2,y:br.y+6,txt:'+'+gain,life:.9,ml:.9,color:br.color});
burst(br.x+br.w/2,br.y+br.h/2,br.color,14,1);
rings.push({x:hx!==undefined?hx:br.x+br.w/2, y:hy!==undefined?hy:br.y+br.h/2, r:5, life:.4, ml:.4, color:br.color});
maybeDrop(br); shake=Math.min(16,shake+3.5); S.brick(br.row,combo);
if(left<=0)winLevel();
} else {
burst(hx!==undefined?hx:br.x+br.w/2, hy!==undefined?hy:br.y+br.h/2, '#ffffff', 4, .4);
shake=Math.min(16,shake+1); S.hard();
}
}
function maybeDrop(br){
if(pows.length>=3||Math.random()>=0.14)return;
const r=Math.random(), type=r<.24?'W':r<.46?'M':r<.66?'S':r<.88?'L':'+';
pows.push({x:br.x+br.w/2,y:br.y+br.h/2,type,ph:Math.random()*7});
}
function applyPow(p){
S.pow(); score+=25; if(score>hi)hi=score;
burst(p.x,p.y,'#ffffff',10,.7);
rings.push({x:paddle.x,y:paddle.y,r:8,life:.5,ml:.5,color:'#ffffff'});
const tx=(x,y,txt,color,big)=>texts.push({x,y,txt,life:1.1,ml:1.1,color,big:!!big});
switch(p.type){
case 'W': eff.wide=12; paddle.w=paddle.baseW*1.55; tx(paddle.x,paddle.y-42,'WIDE PADDLE','#3fe0ff'); break;
case 'M': multiball(); tx(W/2,H/2-50,'MULTIBALL!','#ff4fd8',true); S.multi(); break;
case 'S': eff.slow=8; tx(paddle.x,paddle.y-42,'SLOW-MO','#7cf26b'); break;
case 'L': eff.laser=10; tx(paddle.x,paddle.y-42,'LASERS ONLINE','#ff9f45'); break;
case '+': lives=Math.min(6,lives+1); updateLives(); tx(paddle.x,paddle.y-42,'+1 LIFE','#ff4d6d'); break;
}
}
function multiball(){
const src=balls.filter(b=>!b.stuck).slice();
for(const b of src){
if(balls.length>=9)break;
for(const da of [-0.5,0.5]){
if(balls.length>=9)break;
const ang=Math.atan2(b.vy,b.vx)+da, sp=Math.hypot(b.vx,b.vy);
balls.push({x:b.x,y:b.y,vx:Math.cos(ang)*sp,vy:Math.sin(ang)*sp,r:b.r,stuck:false,trail:[],kick:b.kick});
}
}
}
function fireLasers(){
bullets.push({x:paddle.x-paddle.w*.42,y:paddle.y-paddle.h/2-6,vy:-780},
{x:paddle.x+paddle.w*.42,y:paddle.y-paddle.h/2-6,vy:-780});
fireCd=0.22; S.laser(); shake=Math.min(16,shake+.8);
burst(paddle.x-paddle.w*.42,paddle.y-paddle.h/2,'#ffd9a0',3,.35);
burst(paddle.x+paddle.w*.42,paddle.y-paddle.h/2,'#ffd9a0',3,.35);
}
function burst(x,y,color,n,sp){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2, v=(60+Math.random()*260)*sp, l=.4+Math.random()*.5;
parts.push({x,y,vx:Math.cos(a)*v,vy:Math.sin(a)*v-60*sp,g:760,s:2+Math.random()*3.5,life:l,ml:l,color});
}
if(parts.length>420)parts.splice(0,parts.length-420);
}
function hitFx(b){ burst(b.x,b.y,'#9be9ff',3,.35); }
/* ============ update ============ */
function movePaddle(dt){
let dir=0; if(keys.ArrowLeft||keys.a)dir--; if(keys.ArrowRight||keys.d)dir++;
if(dir!==0){ paddle.x+=dir*660*dt; useMouse=false; }
else if(useMouse){ paddle.x+=(mouseX-paddle.x)*Math.min(1,dt*26); }
paddle.x=clamp(paddle.x,paddle.w/2+WALL+2,W-paddle.w/2-WALL-2);
}
function circleRect(b,rx,ry,rw,rh){
const cx=clamp(b.x,rx,rx+rw),cy=clamp(b.y,ry,ry+rh),dx=b.x-cx,dy=b.y-cy;
return dx*dx+dy*dy<b.r*b.r;
}
function updateBalls(dt){
for(let i=balls.length-1;i>=0;i--){
const b=balls[i];
if(b.stuck){ b.x=paddle.x; b.y=paddle.y-paddle.h/2-b.r-2; continue; }
const sf=eff.slow>0?0.62:1;
const sp0=Math.hypot(b.vx,b.vy);
const n=Math.max(1,Math.ceil(sp0*dt*sf/7)), h=dt*sf/n;
for(let s=0;s<n;s++){
b.x+=b.vx*h; b.y+=b.vy*h;
if(b.x<WALL+b.r){ b.x=WALL+b.r; b.vx=Math.abs(b.vx); hitFx(b); S.wall(); }
else if(b.x>W-WALL-b.r){ b.x=W-WALL-b.r; b.vx=-Math.abs(b.vx); hitFx(b); S.wall(); }
if(b.y<WALL+b.r){ b.y=WALL+b.r; b.vy=Math.abs(b.vy); hitFx(b); S.wall(); }
// paddle
if(b.vy>0 && circleRect(b,paddle.x-paddle.w/2,paddle.y-paddle.h/2,paddle.w,paddle.h)){
const rel=clamp((b.x-paddle.x)/(paddle.w/2),-.95,.95), th=rel*1.05;
const nsp=Math.min(660,Math.hypot(b.vx,b.vy)*1.015);
b.vx=Math.sin(th)*nsp; b.vy=-Math.cos(th)*nsp;
b.y=paddle.y-paddle.h/2-b.r-.5;
if(combo>0){ combo=0; setCombo(); }
paddle.flash=1; S.paddle();
rings.push({x:b.x,y:paddle.y-paddle.h/2,r:4,life:.35,ml:.35,color:'#3fe0ff'});
burst(b.x,paddle.y-paddle.h/2,'#9be9ff',5,.4);
}
// bricks
for(const br of bricks){
if(br.hp<=0)continue;
const cx=clamp(b.x,br.x,br.x+br.w), cy=clamp(b.y,br.y,br.y+br.h);
const dx=b.x-cx, dy=b.y-cy;
if(dx*dx+dy*dy < b.r*b.r){
const d=Math.sqrt(dx*dx+dy*dy)||.001, nx=dx/d, ny=dy/d;
b.x=cx+nx*(b.r+.2); b.y=cy+ny*(b.r+.2);
const dot=b.vx*nx+b.vy*ny; if(dot<0){ b.vx-=2*dot*nx; b.vy-=2*dot*ny; }
damageBrick(br,1,b.x,b.y);
break;
}
}
}
b.trail.push({x:b.x,y:b.y}); if(b.trail.length>9)b.trail.shift();
if(b.y>H+30){ balls.splice(i,1); if(balls.length===0)loseLife(); }
}
}
function updateBullets(dt){
for(let i=bullets.length-1;i>=0;i--){
const u=bullets[i]; u.y+=u.vy*dt; let hit=false;
for(const br of bricks){
if(br.hp<=0)continue;
if(u.x>br.x-2&&u.x<br.x+br.w+2&&u.y<br.y+br.h&&u.y>br.y-8){
damageBrick(br,1,u.x,br.y+br.h); burst(u.x,br.y+br.h,'#ffd9a0',4,.5); hit=true; break;
}
}
if(hit||u.y<-10)bullets.splice(i,1);
}
}
function updatePows(dt){
for(let i=pows.length-1;i>=0;i--){
const p=pows[i]; p.y+=150*dt; p.x+=Math.sin(T*2.5+p.ph)*24*dt;
if(p.y+10>paddle.y-paddle.h/2 && p.y-10<paddle.y+paddle.h && Math.abs(p.x-paddle.x)<paddle.w/2+16){
applyPow(p); pows.splice(i,1); continue;
}
if(p.y>H+20)pows.splice(i,1);
}
}
function update(dt){
T+=dt;
shake=Math.max(0,shake-dt*34); flashA=Math.max(0,flashA-dt*2.2);
paddle.flash=Math.max(0,paddle.flash-dt*5); fireCd-=dt;
for(const b of bricks)if(b.flash>0)b.flash=Math.max(0,b.flash-dt*5);
for(const k of ['wide','laser','slow']){
if(eff[k]>0){ eff[k]-=dt; if(eff[k]<=0){ eff[k]=0; if(k==='wide')paddle.w=paddle.baseW; } }
}
for(const s of stars){ s.y+=s.z*14*dt; if(s.y>H){s.y=-2;s.x=Math.random()*W;} }
movePaddle(dt);
if(state==='serve'){
const b=balls[0]; if(b){ b.x=paddle.x; b.y=paddle.y-paddle.h/2-b.r-2; }
} else if(state==='play'){
updateBalls(dt); updateBullets(dt); updatePows(dt);
if(eff.laser>0&&firing&&fireCd<=0)fireLasers();
} else if(state==='clear'){
clearTimer-=dt; if(clearTimer<=0)nextLevel();
}
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;}
for(let i=rings.length-1;i>=0;i--){const r=rings[i];r.life-=dt;r.r+=dt*260;
if(r.life<=0)rings.splice(i,1);}
for(let i=texts.length-1;i>=0;i--){const t=texts[i];t.life-=dt;t.y-=40*dt;
if(t.life<=0)texts.splice(i,1);}
}
/* ============ render ============ */
function drawBrick(b){
rr(b.x,b.y,b.w,b.h,5); ctx.fillStyle=b.color; ctx.fill();
ctx.fillStyle='rgba(255,255,255,.32)'; rr(b.x+2,b.y+2,b.w-4,9,3); ctx.fill();
ctx.fillStyle='rgba(0,0,20,.28)'; rr(b.x+2,b.y+b.h-7,b.w-4,5,3); ctx.fill();
if(b.mhp>1){ ctx.fillStyle='rgba(0,0,0,.4)';
const sx=b.x+b.w/2-(b.hp*7-3)/2;
for(let i=0;i<b.hp;i++)ctx.fillRect(sx+i*7,b.y+b.h/2-1.5,4,3); }
if(b.hp<b.mhp){ ctx.strokeStyle='rgba(0,0,0,.5)'; ctx.lineWidth=1; ctx.beginPath();
ctx.moveTo(b.x+b.w*.3,b.y+2); ctx.lineTo(b.x+b.w*.42,b.y+b.h-2);
ctx.moveTo(b.x+b.w*.7,b.y+b.h-2); ctx.lineTo(b.x+b.w*.62,b.y+4); ctx.stroke(); }
if(b.flash>0){ ctx.globalAlpha=b.flash*.85; ctx.fillStyle='#fff'; rr(b.x,b.y,b.w,b.h,5); ctx.fill(); ctx.globalAlpha=1; }
}
const POWC={W:'#3fe0ff',M:'#ff4fd8',S:'#7cf26b',L:'#ff9f45','+':'#ff4d6d'};
function drawPow(p){
const c=POWC[p.type];
ctx.save(); ctx.shadowColor=c; ctx.shadowBlur=14;
ctx.fillStyle=c; rr(p.x-17,p.y-10,34,20,10); ctx.fill();
ctx.fillStyle='rgba(255,255,255,.35)'; rr(p.x-14,p.y-8,28,7,4); ctx.fill();
ctx.restore();
ctx.fillStyle='#081018'; ctx.font='800 12px Consolas,monospace';
ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText(p.type,p.x,p.y+1);
ctx.textBaseline='alphabetic';
}
function drawPaddle(){
const px=paddle.x-paddle.w/2, py=paddle.y-paddle.h/2;
ctx.save();
ctx.shadowColor=eff.laser>0?'#ff9f45':'#3fe0ff'; ctx.shadowBlur=18;
const g=ctx.createLinearGradient(0,py,0,py+paddle.h);
g.addColorStop(0,'#f2ffff'); g.addColorStop(.45,'#54d8f5'); g.addColorStop(1,'#1587b0');
ctx.fillStyle=g; rr(px,py,paddle.w,paddle.h,7); ctx.fill();
if(eff.laser>0){ ctx.fillStyle='#ff9f45';
rr(px+paddle.w*.08-3,py-8,6,9,2); ctx.fill();
rr(px+paddle.w*.92-3,py-8,6,9,2); ctx.fill(); }
ctx.restore();
if(paddle.flash>0){ ctx.globalAlpha=paddle.flash; ctx.fillStyle='#fff'; rr(px,py,paddle.w,paddle.h,7); ctx.fill(); ctx.globalAlpha=1; }
}
function chips(){
const arr=[]; if(eff.wide>0)arr.push(['W',eff.wide,'#3fe0ff']);
if(eff.laser>0)arr.push(['L',eff.laser,'#ff9f45']); if(eff.slow>0)arr.push(['S',eff.slow,'#7cf26b']);
let y=14; ctx.font='700 12px Consolas,monospace'; ctx.textBaseline='middle';
for(const [c,t,col] of arr){
const txt=c+' '+Math.ceil(t)+'s', wpx=ctx.measureText(txt).width+18, x=W-30-wpx;
ctx.fillStyle='rgba(10,14,32,.75)'; rr(x,y,wpx,20,6); ctx.fill();
ctx.strokeStyle=col; ctx.lineWidth=1.2; rr(x,y,wpx,20,6); ctx.stroke();
ctx.fillStyle=col; ctx.textAlign='left'; ctx.fillText(txt,x+9,y+11); y+=26;
}
ctx.textBaseline='alphabetic';
}
function render(){
ctx.fillStyle=bgGrad; ctx.fillRect(0,0,W,H);
ctx.fillStyle=neb1; ctx.fillRect(0,0,W,H); ctx.fillStyle=neb2; ctx.fillRect(0,0,W,H);
for(const s of stars){
ctx.globalAlpha=(.25+.55*Math.abs(Math.sin(T*s.tw+s.ph)))*s.z;
ctx.fillStyle=s.z>.65?'#bfeaff':'#8fa3e8';
const d=s.z>.65?2:1; ctx.fillRect(s.x,s.y,d,d);
}
ctx.globalAlpha=1;
const sx=(Math.random()*2-1)*shake, sy=(Math.random()*2-1)*shake;
ctx.save(); ctx.translate(sx,sy);
// rails
ctx.fillStyle='rgba(63,224,255,.06)'; ctx.fillRect(0,0,WALL,H); ctx.fillRect(W-WALL,0,WALL,H); ctx.fillRect(0,0,W,WALL);
ctx.fillStyle='rgba(63,224,255,.5)';
ctx.fillRect(WALL-1.5,0,1.5,H); ctx.fillRect(W-WALL,0,1.5,H); ctx.fillRect(0,WALL-1.5,W,1.5);
// progress
const frac=(totalBricks-left)/totalBricks;
ctx.fillStyle='rgba(255,255,255,.08)'; rr(27,10,906,4,2); ctx.fill();
if(frac>0){ const g=ctx.createLinearGradient(27,0,933,0);
g.addColorStop(0,'#3fe0ff'); g.addColorStop(1,'#ff4fd8');
ctx.fillStyle=g; rr(27,10,906*frac,4,2); ctx.fill(); }
for(const b of bricks)if(b.hp>0)drawBrick(b);
for(const p of pows)drawPow(p);
ctx.save(); ctx.shadowColor='#ff9f45'; ctx.shadowBlur=10; ctx.fillStyle='#ffd9a0';
for(const u of bullets){ rr(u.x-2,u.y-9,4,14,2); ctx.fill(); }
ctx.restore();
for(const r of rings){ const f=r.life/r.ml; ctx.globalAlpha=f*.8; ctx.strokeStyle=r.color;
ctx.lineWidth=2.5*f+.5; ctx.beginPath(); ctx.arc(r.x,r.y,r.r,0,7); ctx.stroke(); }
ctx.globalAlpha=1;
for(const p of parts){ ctx.globalAlpha=Math.max(0,p.life/p.ml); ctx.fillStyle=p.color;
ctx.fillRect(p.x-p.s/2,p.y-p.s/2,p.s,p.s); }
ctx.globalAlpha=1;
for(const b of balls){
for(let i=0;i<b.trail.length;i++){ const t=b.trail[i], f=i/b.trail.length;
ctx.globalAlpha=f*.35; ctx.fillStyle=eff.slow>0?'#7cf26b':'#3fe0ff';
ctx.beginPath(); ctx.arc(t.x,t.y,b.r*f,0,7); ctx.fill(); }
ctx.globalAlpha=1;
ctx.save(); ctx.shadowColor='#3fe0ff'; ctx.shadowBlur=16;
const bg=ctx.createRadialGradient(b.x-2,b.y-3,1,b.x,b.y,b.r);
bg.addColorStop(0,'#fff'); bg.addColorStop(1,'#9be9ff');
ctx.fillStyle=bg; ctx.beginPath(); ctx.arc(b.x,b.y,b.r,0,7); ctx.fill(); ctx.restore();
}
drawPaddle();
ctx.textAlign='center';
for(const t of texts){
ctx.globalAlpha=Math.min(1,t.life/t.ml*1.8);
ctx.font=t.big?'400 26px Impact,"Arial Black",sans-serif':'700 15px Consolas,monospace';
ctx.lineWidth=4; ctx.strokeStyle='rgba(5,8,20,.85)'; ctx.strokeText(t.txt,t.x,t.y);
ctx.fillStyle=t.color; ctx.fillText(t.txt,t.x,t.y);
}
ctx.globalAlpha=1;
if(state==='serve'){
const a=.5+.5*Math.sin(T*4);
ctx.globalAlpha=.35+.5*a; ctx.fillStyle='#bfefff'; ctx.font='700 14px Consolas,monospace';
ctx.fillText('CLICK / SPACE TO LAUNCH', clamp(paddle.x,170,W-170), paddle.y-58);
ctx.globalAlpha=1;
}
chips();
ctx.restore();
if(flashA>0){ ctx.globalAlpha=flashA; ctx.fillStyle=flashC; ctx.fillRect(0,0,W,H); ctx.globalAlpha=1; }
}
/* ============ HUD ============ */
function fmt(n){ return String(Math.max(0,n|0)).padStart(6,'0'); }
function updateLives(){
livesEl.innerHTML=''; const n=Math.min(6,Math.max(3,lives));
for(let i=0;i<n;i++){ const d=document.createElement('span');
d.className='dot'+(i<lives?'':' off'); livesEl.appendChild(d); }
}
function setCombo(){
if(combo>=2){ comboEl.textContent='×'+combo; comboEl.classList.toggle('hot',combo>=6); }
else { comboEl.textContent='—'; comboEl.classList.remove('hot'); }
}
function hud(){
dispScore+=(score-dispScore)*Math.min(1,lastDt*10);
if(Math.abs(dispScore-score)<.6)dispScore=score;
const st=fmt(Math.round(dispScore));
if(st!==scoreEl.textContent)scoreEl.textContent=st;
if(score>lastShown&&lastShown>=0){ scoreEl.classList.remove('bump'); void scoreEl.offsetWidth; scoreEl.classList.add('bump'); }
lastShown=score;
const ht=fmt(hi); if(ht!==hiEl.textContent)hiEl.textContent=ht;
}
/* ============ overlays ============ */
function showOverlay(kind){
overlay.classList.remove('hidden'); let h='';
if(kind==='title'){
h='<div class="ov-kicker">// ARCADE BLOCK PROTOCOL //</div><h2>READY?</h2>'+
'<p>Smash every block across 8 sectors. Chain bricks for combo points, grab the falling capsules, and don\u2019t drop the ball.</p>'+
'<div class="legend">'+
'<div><span class="pw" style="--c:#3fe0ff">W</span>WIDE</div>'+
'<div><span class="pw" style="--c:#ff4fd8">M</span>MULTI</div>'+
'<div><span class="pw" style="--c:#7cf26b">S</span>SLOW</div>'+
'<div><span class="pw" style="--c:#ff9f45">L</span>LASER</div>'+
'<div><span class="pw" style="--c:#ff4d6d">+</span>LIFE</div></div>'+
'<div class="keysline"><span><kbd>\u2190</kbd><kbd>\u2192</kbd> / <kbd>A</kbd><kbd>D</kbd> or MOUSE \u2014 move</span>'+
'<span><kbd>SPACE</kbd> / <kbd>CLICK</kbd> \u2014 launch & fire</span></div>'+
'<button class="btn" data-act="start">INSERT COIN \u00b7 START</button>';
} else if(kind==='over'){
h='<div class="ov-kicker">// SIGNAL LOST //</div><h2>GAME OVER</h2>'+
'<div class="ovstats"><div><span>SCORE</span>'+fmt(score)+'</div>'+
'<div><span>BEST COMBO</span>\u00d7'+bestCombo+'</div>'+
'<div><span>HI-SCORE</span>'+fmt(hi)+'</div></div>'+
(score>runStartHi?'<p>NEW RECORD!</p>':'')+
'<button class="btn" data-act="restart">PLAY AGAIN</button>';
} else if(kind==='win'){
h='<div class="ov-kicker">// ALL 8 SECTORS CLEARED //</div><h2>EARTH IS SAFE</h2>'+
'<div class="ovstats"><div><span>SCORE</span>'+fmt(score)+'</div>'+
'<div><span>BEST COMBO</span>\u00d7'+bestCombo+'</div>'+
'<div><span>HI-SCORE</span>'+fmt(hi)+'</div></div>'+
'<button class="btn" data-act="endless">ENDLESS MODE</button>'+
'<button class="btn alt" data-act="restart">RESTART</button>';
} else if(kind==='pause'){
h='<h2>PAUSED</h2><p>Press <kbd>P</kbd> to resume the assault.</p>'+
'<button class="btn" data-act="resume">RESUME</button>';
}
ovContent.innerHTML=h;
}
function hideOverlay(){ overlay.classList.add('hidden'); }
ovContent.addEventListener('click',e=>{
const b=e.target.closest('[data-act]'); if(!b)return;
audio(); S.click();
const a=b.dataset.act;
if(a==='start'||a==='restart')startGame();
else if(a==='endless')startEndless();
else if(a==='resume')togglePause();
});
function togglePause(){
if(state!=='serve'&&state!=='play')return;
paused=!paused;
if(paused)showOverlay('pause'); else hideOverlay();
}
/* ============ input ============ */
window.addEventListener('keydown',e=>{
const k=e.key;
if([' ','ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(k))e.preventDefault();
audio();
keys[k.length===1?k.toLowerCase():k]=true;
if(k===' '){
if(state==='title'||state==='over')startGame();
else if(state==='win')startEndless();
else if(paused)togglePause();
else if(state==='serve')launch();
else firing=true;
}
else if(k==='Enter'){
if(state==='title'||state==='over')startGame();
else if(state==='win')startEndless();
}
else if(k==='p'||k==='P'||k==='Escape')togglePause();
else if(k==='m'||k==='M')toggleMute();
});
window.addEventListener('keyup',e=>{
const k=e.key; keys[k.length===1?k.toLowerCase():k]=false;
if(k===' ')firing=false;
});
function setMouse(cx){ const r=cv.getBoundingClientRect(); if(!r.width)return;
mouseX=(cx-r.left)*W/r.width; useMouse=true; }
window.addEventListener('mousemove',e=>setMouse(e.clientX));
cv.addEventListener('pointerdown',e=>{ audio(); setMouse(e.clientX);
if(state==='serve')launch(); else if(state==='play')firing=true; });
window.addEventListener('pointerup',()=>firing=false);
cv.addEventListener('touchstart',e=>{ e.preventDefault(); audio(); setMouse(e.touches[0].clientX);
if(state==='serve')launch(); else firing=true; },{passive:false});
cv.addEventListener('touchmove',e=>{ e.preventDefault(); setMouse(e.touches[0].clientX); },{passive:false});
window.addEventListener('touchend',()=>firing=false);
/* ============ boot & loop ============ */
updateLives(); hiEl.textContent=fmt(hi); showOverlay('title');
let tPrev=performance.now();
function frame(t){
requestAnimationFrame(frame);
let dt=(t-tPrev)/1000; tPrev=t; if(dt>0.05)dt=0.05; lastDt=dt;
if(!paused)update(dt);
render(); hud();
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

