Run details
- Model
- mistralai/mistral-large-4-0
- Route
- OpenRouter API, one user message
- Reasoning
- provider default, 60,105 reasoning tokens
- Limits
- 60 min · 143,540 output tokens
- Date
- 21 Oct 2026
- Time
- 15 min 19 s
- Size
- 38 kB
- Tokens
- 81 in, 74,311 out
- List price
- $0.31 ($1.36 in / $4.18 out per M)
- Paid
- $0.31
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
38 kB 1,125 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>BREAKOUT — Neon Brick Breaker</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden}
body{
background:#020308;
display:flex;align-items:center;justify-content:center;
font-family:'Trebuchet MS','Segoe UI',Verdana,sans-serif;
color:#e2e8f0;
user-select:none;-webkit-user-select:none;
touch-action:none;
}
#stage{position:relative;line-height:0}
#game{
display:block;
width:min(97vw,calc(97vh * 1.6));
aspect-ratio:960/600;
border-radius:12px;
border:1px solid rgba(110,140,255,.28);
box-shadow:0 0 0 1px rgba(0,0,0,.7),0 0 50px rgba(34,211,238,.10),0 30px 90px rgba(0,0,0,.85);
background:#070812;
cursor:none;
touch-action:none;
}
/* ---------- HUD ---------- */
#hud{
position:absolute;top:0;left:0;right:0;
display:flex;align-items:flex-start;justify-content:space-between;
padding:12px 18px;pointer-events:none;
font-family:ui-monospace,'Cascadia Mono','SF Mono',Menlo,Consolas,monospace;
transition:opacity .3s;
}
#stage.is-menu #hud{opacity:0}
.hud-group{display:flex;gap:22px}
.hud-right{display:flex;align-items:flex-end;gap:14px}
.hud-item{display:flex;flex-direction:column;gap:3px}
.hud-item .lbl{font-size:9px;font-weight:700;letter-spacing:.3em;color:#64748b}
.hud-item .val{font-size:19px;font-weight:700;color:#e8fbff;font-variant-numeric:tabular-nums;line-height:1}
#score{color:#a5f3fc;text-shadow:0 0 14px rgba(34,211,238,.5);display:inline-block}
#level{color:#fbbf24;text-shadow:0 0 14px rgba(251,191,36,.45)}
#best{color:#64748b;font-size:15px;text-shadow:none}
#lives{display:flex;gap:5px;height:19px;align-items:center}
#lives .dot{width:11px;height:11px;border-radius:50%;background:#f0feff;box-shadow:0 0 10px rgba(103,232,249,.8)}
#score.pop{animation:pop .22s ease-out}
@keyframes pop{0%{transform:scale(1)}35%{transform:scale(1.3)}100%{transform:scale(1)}}
#muteBtn{
pointer-events:auto;margin-bottom:1px;
background:none;border:1px solid rgba(148,163,184,.3);border-radius:6px;
color:#64748b;font:700 9px ui-monospace,Menlo,monospace;letter-spacing:.18em;
padding:5px 9px;cursor:pointer;transition:.15s;
}
#muteBtn:hover{color:#cbd5e1;border-color:#94a3b8}
#pups{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;padding-top:2px}
.chip{
display:flex;align-items:center;gap:7px;
background:rgba(8,12,26,.85);border:1px solid rgba(148,163,184,.25);
border-radius:999px;padding:4px 10px 4px 8px;
font:700 10px ui-monospace,Menlo,monospace;letter-spacing:.12em;
}
.chip .sw{width:9px;height:9px;border-radius:3px;background:currentColor;box-shadow:0 0 8px currentColor}
.chip .bar{width:36px;height:3px;border-radius:2px;background:rgba(255,255,255,.14);overflow:hidden}
.chip .bar i{display:block;height:100%;background:currentColor}
#combo{
position:absolute;top:48px;left:50%;transform:translateX(-50%) scale(.9);
font:800 17px ui-monospace,Menlo,monospace;letter-spacing:.18em;
color:#fbbf24;text-shadow:0 0 18px rgba(251,191,36,.6);
opacity:0;transition:opacity .15s;pointer-events:none;white-space:nowrap;
}
#combo.show{opacity:1;transform:translateX(-50%) scale(1)}
#combo.pop{animation:cpop .18s ease-out}
@keyframes cpop{0%{transform:translateX(-50%) scale(1)}40%{transform:translateX(-50%) scale(1.25)}100%{transform:translateX(-50%) scale(1)}}
/* ---------- level banner ---------- */
#banner{
position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:10px;pointer-events:none;opacity:0;transition:opacity .18s;
}
#banner.show{opacity:1}
#banner .b1{font:800 clamp(26px,5vw,46px) ui-monospace,Menlo,monospace;letter-spacing:.16em;text-indent:.16em;color:#ecfeff;text-shadow:0 0 28px rgba(34,211,238,.6)}
#banner .b2{font:700 12px ui-monospace,Menlo,monospace;letter-spacing:.34em;text-indent:.34em;color:#94a3b8}
#banner .b2.good{color:#4ade80;text-shadow:0 0 16px rgba(74,222,128,.55)}
/* ---------- overlays ---------- */
.ov{
position:absolute;inset:0;border-radius:12px;
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;
background:rgba(4,5,14,.72);
backdrop-filter:blur(7px);-webkit-backdrop-filter:blur(7px);
opacity:0;visibility:hidden;pointer-events:none;
transition:opacity .25s,visibility .25s;
text-align:center;padding:24px;line-height:1.5;
}
.ov.show{opacity:1;visibility:visible;pointer-events:auto}
.logo{display:flex;gap:7px;margin-bottom:2px}
.logo i{width:22px;height:13px;border-radius:3px;display:block;animation:bob 1.5s ease-in-out infinite}
.logo i:nth-child(1){background:#f472b6;box-shadow:0 0 14px rgba(244,114,182,.55)}
.logo i:nth-child(2){background:#a78bfa;box-shadow:0 0 14px rgba(167,139,250,.55);animation-delay:.12s}
.logo i:nth-child(3){background:#fbbf24;box-shadow:0 0 14px rgba(251,191,36,.55);animation-delay:.24s}
.logo i:nth-child(4){background:#22d3ee;box-shadow:0 0 14px rgba(34,211,238,.55);animation-delay:.36s}
.logo i:nth-child(5){background:#4ade80;box-shadow:0 0 14px rgba(74,222,128,.55);animation-delay:.48s}
@keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
.title{
font-size:clamp(30px,6vw,54px);font-weight:800;
letter-spacing:.28em;text-indent:.28em;color:#ecfeff;
text-shadow:0 0 20px rgba(34,211,238,.5),0 0 70px rgba(34,211,238,.22);
}
.tagline{font:700 10px ui-monospace,Menlo,monospace;letter-spacing:.44em;text-indent:.44em;color:#64748b}
.btn{
font-family:inherit;font-size:14px;font-weight:700;
letter-spacing:.28em;text-indent:.28em;text-transform:uppercase;
color:#a5f3fc;background:rgba(34,211,238,.06);
border:1px solid rgba(34,211,238,.55);border-radius:8px;
padding:13px 36px;cursor:pointer;transition:.15s;margin-top:4px;
}
.btn:hover{background:rgba(34,211,238,.16);box-shadow:0 0 26px rgba(34,211,238,.35);transform:translateY(-1px)}
.controls{display:flex;flex-direction:column;gap:8px;font:500 11px ui-monospace,Menlo,monospace;color:#94a3b8;letter-spacing:.05em}
.controls kbd{
border:1px solid rgba(255,255,255,.18);border-bottom-width:2px;border-radius:4px;
padding:1px 7px;font-family:ui-monospace,Menlo,monospace;font-size:10px;color:#e2e8f0;
background:rgba(255,255,255,.05);
}
.small{font:600 10px ui-monospace,Menlo,monospace;letter-spacing:.22em;color:#475569}
.ov h2{font-size:clamp(26px,5vw,42px);font-weight:800;letter-spacing:.2em;text-indent:.2em;color:#ecfeff;text-shadow:0 0 24px rgba(34,211,238,.4)}
.ov h2.bad{color:#fda4af;text-shadow:0 0 26px rgba(251,113,133,.5)}
.ov h2.good{color:#86efac;text-shadow:0 0 26px rgba(74,222,128,.5)}
.bigscore{font:800 36px ui-monospace,Menlo,monospace;color:#a5f3fc;text-shadow:0 0 26px rgba(34,211,238,.5);font-variant-numeric:tabular-nums}
.sub{font:600 11px ui-monospace,Menlo,monospace;letter-spacing:.18em;color:#94a3b8}
@media (max-width:640px){
.hud-group{gap:12px}
.hud-item .val{font-size:15px}
#best-wrap{display:none}
.controls{font-size:10px}
}
</style>
</head>
<body>
<div id="stage" class="is-menu">
<canvas id="game"></canvas>
<div id="hud">
<div class="hud-group">
<div class="hud-item"><span class="lbl">SCORE</span><span class="val" id="score">0</span></div>
<div class="hud-item"><span class="lbl">LEVEL</span><span class="val" id="level">1</span></div>
<div class="hud-item" id="best-wrap"><span class="lbl">BEST</span><span class="val" id="best">0</span></div>
</div>
<div id="pups"></div>
<div class="hud-right">
<div class="hud-item"><span class="lbl">BALLS</span><span id="lives"></span></div>
<button id="muteBtn" type="button">SOUND ON</button>
</div>
</div>
<div id="combo"></div>
<div id="banner"><div class="b1"></div><div class="b2"></div></div>
<div class="ov show" id="ov-menu">
<div class="logo"><i></i><i></i><i></i><i></i><i></i></div>
<h1 class="title">BREAKOUT</h1>
<div class="tagline">NEON BRICK BREAKER</div>
<button class="btn" id="btn-play" type="button">PLAY</button>
<div class="controls">
<div><kbd>MOUSE</kbd> or <kbd>←</kbd> <kbd>→</kbd> — move the paddle</div>
<div><kbd>SPACE</kbd> / <kbd>CLICK</kbd> — launch & fire lasers</div>
<div><kbd>P</kbd> pause · <kbd>M</kbd> mute · <kbd>R</kbd> restart</div>
</div>
<div class="small" id="menu-best"></div>
</div>
<div class="ov" id="ov-pause">
<h2>PAUSED</h2>
<button class="btn" id="btn-resume" type="button">RESUME</button>
<div class="sub">PRESS P TO CONTINUE</div>
</div>
<div class="ov" id="ov-over">
<h2 class="bad">GAME OVER</h2>
<div class="bigscore" id="over-score">0</div>
<div class="sub" id="over-best"></div>
<button class="btn" id="btn-retry" type="button">RETRY</button>
</div>
<div class="ov" id="ov-win">
<h2 class="good">ALL CLEAR!</h2>
<div class="bigscore" id="win-score">0</div>
<div class="sub">YOU BROKE EVERY BRICK</div>
<button class="btn" id="btn-again" type="button">PLAY AGAIN</button>
</div>
</div>
<script>
(() => {
'use strict';
const $ = id => document.getElementById(id);
const canvas = $('game'), ctx = canvas.getContext('2d');
const W = 960, H = 600;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = W * dpr; canvas.height = H * dpr;
ctx.scale(dpr, dpr);
const scoreEl=$('score'), levelEl=$('level'), bestEl=$('best'), livesEl=$('lives'),
pupsEl=$('pups'), comboEl=$('combo'), bannerEl=$('banner'),
bannerB1=bannerEl.querySelector('.b1'), bannerB2=bannerEl.querySelector('.b2'),
muteBtn=$('muteBtn'), stage=$('stage'), menuBest=$('menu-best');
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const rnd=(a,b)=>a+Math.random()*(b-a);
const TAU=Math.PI*2;
const fmt=n=>n.toLocaleString('en-US');
const easeOutBack=p=>{const c1=1.70158,c3=c1+1;return 1+c3*Math.pow(p-1,3)+c1*Math.pow(p-1,2);};
/* ---------- persisted settings ---------- */
const LS_BEST='neon-breakout-best', LS_MUTE='neon-breakout-mute';
let best=0, muted=false;
try{
best=+(localStorage.getItem(LS_BEST)||0)||0;
muted=localStorage.getItem(LS_MUTE)==='1';
}catch(e){}
/* ---------- audio: everything synthesized locally with WebAudio ---------- */
let actx=null;
function audio(){
if(!actx){
const AC=window.AudioContext||window.webkitAudioContext;
if(AC) actx=new AC();
}
if(actx&&actx.state==='suspended') actx.resume();
return actx;
}
function tone(o){
if(muted) return;
const ac=audio(); if(!ac) return;
const t0=ac.currentTime+(o.delay||0);
const osc=ac.createOscillator(), g=ac.createGain();
osc.type=o.type||'sine';
osc.frequency.setValueAtTime(o.f,t0);
if(o.f2) osc.frequency.exponentialRampToValueAtTime(o.f2,t0+o.d);
g.gain.setValueAtTime(.0001,t0);
g.gain.exponentialRampToValueAtTime(o.g??.1,t0+.006);
g.gain.exponentialRampToValueAtTime(.0001,t0+o.d);
osc.connect(g); g.connect(ac.destination);
osc.start(t0); osc.stop(t0+o.d+.03);
}
const S={
wall: ()=>tone({f:180+Math.random()*60,f2:110,d:.05,type:'square',g:.05}),
paddle:()=>tone({f:640,f2:980,d:.07,type:'sine',g:.12}),
brick:hp=>tone({f:280+hp*150+rnd(-20,20),f2:110,d:.08,type:'square',g:.08}),
hard: ()=>tone({f:1100,d:.05,type:'square',g:.07}),
break: ()=>{tone({f:880,f2:140,d:.16,type:'sawtooth',g:.1});tone({f:1400,d:.05,type:'square',g:.06});},
power: ()=>{[660,990,1320].forEach((f,i)=>tone({f,d:.12,type:'sine',g:.09,delay:i*.07}));},
life: ()=>{[880,1320].forEach((f,i)=>tone({f,d:.14,type:'sine',g:.1,delay:i*.08}));},
lose: ()=>tone({f:480,f2:70,d:.5,type:'sawtooth',g:.16}),
launch:()=>tone({f:320,f2:940,d:.13,type:'sine',g:.1}),
laser: ()=>tone({f:1500,f2:420,d:.07,type:'square',g:.05}),
level: ()=>{[523,659,784,1047].forEach((f,i)=>tone({f,d:.16,type:'sine',g:.09,delay:i*.09}));},
explode:()=>{tone({f:140,f2:40,d:.3,type:'sawtooth',g:.15});tone({f:70,d:.28,type:'sine',g:.18});},
menu: ()=>tone({f:520,f2:800,d:.1,type:'sine',g:.08}),
};
/* ---------- levels (12 columns; 1/2/3 = hit points, H = hard, E = explosive) ---------- */
const LEVELS=[
{name:'FIRST CONTACT', rows:[
'............',
'.1111111111.',
'.2222222222.',
'.1111111111.',
'............']},
{name:'PYRAMID', rows:[
'............',
'....2222....',
'...333333...',
'..33333333..',
'...333333...',
'....2222....',
'............']},
{name:'CHECKERBOARD', rows:[
'............',
'.H1.H1.H1.H.',
'.2..2..2..2.',
'.H1.H1.H1.H.',
'............']},
{name:'FORTRESS', rows:[
'HHHHHHHHHHHH',
'H111111111H',
'H122222221H',
'H122222221H',
'H111111111H',
'HHHHHHHHHHHH']},
{name:'DIAMOND', rows:[
'.....2......',
'....3333....',
'...333333...',
'..33333333..',
'...333333...',
'....3333....',
'.....2......',
'............']},
{name:'GAUNTLET', rows:[
'E2E2E2E2E2E2',
'.H3.H3.H3.H.',
'E2E2E2E2E2E2',
'.1.1.1.1.1.1',
'.H2.H2.H2.H.',
'..33333333..']},
];
/* ---------- game state ---------- */
let state='menu'; // menu | playing | paused | transition | gameover | victory
let score=0, level=1, lives=3, combo=0;
let t=0, freezeT=0, flashR=0, flashW=0, shakeA=0;
let transitionT=0, afterTransition=null, pendingServe=0;
let destructible=0, speedBase=390, speedCap=820;
const paddle={x:W/2, y:H-52, w:112, baseW:112, h:14, squash:1, laserCd:0};
const fx={expandT:0, slowT:0, laserT:0, fireT:0};
let balls=[], bricks=[], falls=[], lasers=[], parts=[], texts=[];
const PU_TYPES=[
{id:'expand', label:'WIDE', color:'#22d3ee', w:.24},
{id:'multiball',label:'×3', color:'#f472b6', w:.18},
{id:'slow', label:'SLOW', color:'#4ade80', w:.16},
{id:'laser', label:'LASER', color:'#fb7185', w:.16},
{id:'fire', label:'FIRE', color:'#fb923c', w:.14},
{id:'life', label:'+1', color:'#e8fbff', w:.12},
];
const CHIP_DEFS={
expand:{label:'WIDE', color:'#22d3ee', get:()=>fx.expandT, max:14},
slow: {label:'SLOW', color:'#4ade80', get:()=>fx.slowT, max:10},
laser: {label:'LASER',color:'#fb7185', get:()=>fx.laserT, max:10},
fire: {label:'FIRE', color:'#fb923c', get:()=>fx.fireT, max:9},
};
const chipEls={};
/* ---------- input ---------- */
const keys={};
let mouseX=W/2, mouseActive=false;
window.addEventListener('keydown', e=>{
keys[e.code]=true;
if(e.repeat) return;
if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Space'].includes(e.code)) e.preventDefault();
if(e.code==='Space'||e.code==='Enter') action();
else if(e.code==='KeyP'||e.code==='Escape') togglePause();
else if(e.code==='KeyM') toggleMute();
else if(e.code==='KeyR'){ if(state!=='menu') restart(); }
});
window.addEventListener('keyup', e=>{ keys[e.code]=false; });
window.addEventListener('blur', ()=>{ for(const k in keys) keys[k]=false; });
function mapPointer(clientX){
const r=canvas.getBoundingClientRect();
return clamp((clientX-r.left)*(W/r.width),0,W);
}
canvas.addEventListener('mousemove', e=>{ mouseX=mapPointer(e.clientX); mouseActive=true; });
canvas.addEventListener('mousedown', ()=>action());
canvas.addEventListener('touchstart', e=>{
e.preventDefault();
if(e.touches[0]){ mouseX=mapPointer(e.touches[0].clientX); mouseActive=true; }
action();
},{passive:false});
canvas.addEventListener('touchmove', e=>{
e.preventDefault();
if(e.touches[0]){ mouseX=mapPointer(e.touches[0].clientX); mouseActive=true; }
},{passive:false});
function action(){
if(state==='menu'){ startGame(); return; }
if(state==='gameover'||state==='victory'){ restart(); return; }
if(state==='paused'){ resume(); return; }
if(state==='playing'||state==='transition'){
const sb=balls.find(b=>b.stuck);
if(sb) launch(sb);
}
}
function togglePause(){
if(state==='playing'){ state='paused'; showOv('pause'); }
else if(state==='paused') resume();
}
function resume(){ hideOvs(); state='playing'; }
function toggleMute(){
muted=!muted;
muteBtn.textContent=muted?'SOUND OFF':'SOUND ON';
try{ localStorage.setItem(LS_MUTE,muted?'1':'0'); }catch(e){}
if(!muted) S.menu();
}
muteBtn.addEventListener('click', e=>{ e.stopPropagation(); toggleMute(); });
/* ---------- overlays ---------- */
const ovs={menu:$('ov-menu'), pause:$('ov-pause'), over:$('ov-over'), win:$('ov-win')};
function showOv(k){ for(const o in ovs) ovs[o].classList.toggle('show',o===k); syncChrome(); }
function hideOvs(){ for(const o in ovs) ovs[o].classList.remove('show'); syncChrome(); }
function syncChrome(){ stage.classList.toggle('is-menu',state==='menu'); }
Object.values(ovs).forEach(ov=>ov.addEventListener('click',e=>{ if(e.target===ov) action(); }));
document.querySelectorAll('.btn').forEach(b=>b.addEventListener('click',()=>b.blur()));
$('btn-play').addEventListener('click',()=>startGame());
$('btn-resume').addEventListener('click',()=>resume());
$('btn-retry').addEventListener('click',()=>restart());
$('btn-again').addEventListener('click',()=>restart());
document.addEventListener('visibilitychange',()=>{
if(document.hidden&&state==='playing'){ state='paused'; showOv('pause'); }
});
/* ---------- flow ---------- */
function startGame(){
if(state!=='menu') return;
audio();
score=0; lives=3; combo=0;
updateHUD();
startLevel(1);
hideOvs();
S.menu();
}
function restart(){
if(state==='menu') return;
audio();
score=0; lives=3; combo=0;
updateHUD();
startLevel(1);
hideOvs();
}
function startLevel(n){
level=n;
buildLevel(n);
paddle.x=W/2; paddle.w=paddle.baseW; paddle.laserCd=0;
fx.expandT=fx.slowT=fx.laserT=fx.fireT=0;
balls=[]; falls=[]; lasers=[]; texts=[];
combo=0; pendingServe=0; updateCombo();
spawnStuckBall();
updateHUD();
banner('LEVEL '+level, LEVELS[level-1].name, false);
state='transition';
transitionT=1.35;
afterTransition=()=>{ hideBanner(); state='playing'; };
S.level();
syncChrome();
}
function buildLevel(n){
const rows=LEVELS[n-1].rows;
bricks=[]; destructible=0;
const cols=12, gap=5, margin=34, top=86;
const bw=(W-margin*2-gap*(cols-1))/cols, bh=22;
rows.forEach((row,r)=>{
for(let c=0;c<cols;c++){
const ch=row[c]||'.';
if(ch==='.') continue;
let type='normal', hp=1;
if(ch==='H') type='hard';
else if(ch==='E'){ type='explosive'; hp=1; }
else hp=+ch;
bricks.push({
x:margin+c*(bw+gap), y:top+r*(bh+gap), w:bw, h:bh,
type, hp, maxHp:hp, alive:true, flash:0,
appear:rnd(.05,.15)+(r*cols+c)*.012, scale:0, pulse:rnd(0,TAU)
});
if(type!=='hard') destructible++;
}
});
speedBase=390+(n-1)*28;
}
function spawnStuckBall(){
balls.push({x:paddle.x, y:paddle.y-paddle.h/2-8, vx:0, vy:0, r:7,
speed:speedBase, stuck:true, trail:[], squashT:0, fire:false});
}
function launch(b){
b.stuck=false;
b.fire=fx.fireT>0;
const ang=-Math.PI/2+rnd(-.32,.32);
const s=b.speed*(fx.slowT>0?.72:1);
b.vx=Math.cos(ang)*s; b.vy=Math.sin(ang)*s;
b.trail.length=0;
S.launch();
burst(b.x,b.y-4,'#a5f3fc',8,{speed:.5});
}
function loseLife(){
lives--;
updateHUD();
S.lose();
addShake(9);
freezeT=Math.max(freezeT,.22);
flashR=.5;
burst(W/2,H-90,'#ef4444',16,{speed:1.2});
if(lives>0){
pendingServe=.9;
floatText(W/2,H/2,'BALL LOST','#f87171',true);
}else{
gameOver();
}
}
function gameOver(){
state='gameover';
$('over-score').textContent=fmt(score);
saveBest();
$('over-best').textContent='BEST '+fmt(best);
showOv('over');
}
function victory(){
state='victory';
$('win-score').textContent=fmt(score);
saveBest();
showOv('win');
S.level();
const cols=['#f472b6','#a78bfa','#fbbf24','#22d3ee','#4ade80'];
for(let i=0;i<90;i++){
parts.push({x:rnd(0,W),y:rnd(-120,H*.3),vx:rnd(-170,170),vy:rnd(140,320),
life:rnd(.9,1.8),max:1.8,size:rnd(2,4.5),color:cols[i%cols.length],
shape:Math.random()<.5?'rect':'circle',g:320});
}
}
function levelClear(){
const bonus=250*level+100*lives;
addScore(bonus);
state='transition';
transitionT=2.2;
banner('LEVEL '+level+' CLEAR','+'+bonus+' BONUS',true);
afterTransition=()=>{
hideBanner();
if(level<LEVELS.length) startLevel(level+1);
else victory();
};
S.level();
addShake(8);
freezeT=.3; flashW=.45;
floatText(W/2,H/2-36,'LEVEL CLEAR!','#a5f3fc',true);
}
function saveBest(){
if(score>best){
best=score;
try{ localStorage.setItem(LS_BEST,String(best)); }catch(e){}
}
bestEl.textContent=fmt(best);
menuBest.textContent='BEST '+fmt(best);
}
function addScore(n){
score+=n;
scoreEl.textContent=fmt(score);
scoreEl.classList.remove('pop'); void scoreEl.offsetWidth; scoreEl.classList.add('pop');
}
/* ---------- HUD ---------- */
function updateHUD(){
scoreEl.textContent=fmt(score);
levelEl.textContent=level;
bestEl.textContent=fmt(best);
livesEl.innerHTML='';
for(let i=0;i<lives;i++){
const d=document.createElement('i');
d.className='dot';
livesEl.appendChild(d);
}
}
function updateCombo(){
if(combo>=2){
const mult=1+(combo-1)*.25;
comboEl.textContent='COMBO ×'+mult.toFixed(2);
comboEl.classList.add('show');
comboEl.classList.remove('pop'); void comboEl.offsetWidth; comboEl.classList.add('pop');
}else{
comboEl.classList.remove('show');
}
}
function updateChips(){
for(const id in CHIP_DEFS){
const d=CHIP_DEFS[id], v=d.get();
let el=chipEls[id];
if(v>0){
if(!el){
el=document.createElement('div');
el.className='chip';
el.style.color=d.color;
el.innerHTML='<span class="sw"></span><span class="cl">'+d.label+'</span><span class="bar"><i></i></span>';
pupsEl.appendChild(el);
chipEls[id]=el;
}
el.querySelector('i').style.width=(v/d.max*100).toFixed(1)+'%';
}else if(el){
el.remove();
delete chipEls[id];
}
}
}
function banner(b1,b2,good){
bannerB1.textContent=b1;
bannerB2.textContent=b2;
bannerB2.classList.toggle('good',!!good);
bannerEl.classList.add('show');
}
function hideBanner(){ bannerEl.classList.remove('show'); }
/* ---------- power-ups ---------- */
function pickPU(){
let r=Math.random(), acc=0;
for(const p of PU_TYPES){ acc+=p.w; if(r<=acc) return p; }
return PU_TYPES[0];
}
function maybeDrop(x,y){
if(Math.random()<.18){
const p=pickPU();
falls.push({x,y,vy:135,type:p.id,label:p.label,color:p.color,
ph:rnd(0,TAU),w:16+p.label.length*7.5,h:22});
}
}
function applyPU(id){
if(id==='expand'){ fx.expandT=14; }
else if(id==='multiball'){
const add=[];
for(const b of balls){
if(b.stuck) continue;
for(const da of [-.5,.5]){
if(balls.length+add.length>=9) break;
const ang=Math.atan2(b.vy,b.vx)+da;
add.push({x:b.x,y:b.y,vx:Math.cos(ang)*b.speed,vy:Math.sin(ang)*b.speed,
r:7,speed:b.speed,stuck:false,trail:[],squashT:0,fire:b.fire});
}
}
balls.push(...add);
burst(paddle.x,paddle.y,'#f472b6',16);
}
else if(id==='slow'){ fx.slowT=10; }
else if(id==='laser'){ fx.laserT=10; }
else if(id==='fire'){ fx.fireT=9; for(const b of balls) b.fire=true; }
else if(id==='life'){ lives=Math.min(6,lives+1); S.life(); updateHUD(); }
S.power();
}
function shootLaser(){
const y0=paddle.y-4;
lasers.push({x:paddle.x-paddle.w*.3,y:y0,vy:-620});
lasers.push({x:paddle.x+paddle.w*.3,y:y0,vy:-620});
S.laser();
addShake(.8);
burst(paddle.x-paddle.w*.3,y0-6,'#fb7185',3,{speed:.3,up:60});
burst(paddle.x+paddle.w*.3,y0-6,'#fb7185',3,{speed:.3,up:60});
}
/* ---------- effects helpers ---------- */
function addShake(a){ shakeA=Math.min(14,Math.max(shakeA,a)); }
function burst(x,y,color,n=10,o={}){
for(let i=0;i<n;i++){
const a=rnd(0,TAU), sp=rnd(30,300)*(o.speed||1);
parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-(o.up||0),
life:rnd(.3,.75),max:.75,size:rnd(1.5,3.5),color,
shape:Math.random()<.25?'rect':'circle',g:260});
}
}
function ring(x,y,color,r0=24){
parts.push({ring:true,x,y,r0,maxR:r0*2.4,life:.45,max:.45,color,vx:0,vy:0,g:0,size:0,shape:'ring'});
}
function spark(x,y,color){ burst(x,y,color||'#94a3b8',3,{speed:.35}); }
function floatText(x,y,str,color,big){
texts.push({x,y,str,color,life:.9,max:.9,vy:big?-48:-40,size:big?20:13});
}
function brickColors(b){
if(b.type==='hard') return {fill:'#3b4657',hi:'#64748b',lo:'#1e293b'};
if(b.type==='explosive')return {fill:'#ef4444',hi:'#fca5a5',lo:'#7f1d1d'};
if(b.hp>=3) return {fill:'#fbbf24',hi:'#fde68a',lo:'#b45309'};
if(b.hp===2) return {fill:'#a78bfa',hi:'#ddd6fe',lo:'#6d28d9'};
return {fill:'#f472b6',hi:'#fbcfe8',lo:'#be185d'};
}
/* ---------- bricks ---------- */
function damageBrick(b,dmg){
if(!b.alive) return;
if(b.type==='hard'){
b.flash=1; S.hard();
burst(b.x+b.w/2,b.y+b.h/2,'#94a3b8',5,{speed:.5});
addShake(1.2);
return;
}
b.hp-=dmg;
b.flash=1;
const cx=b.x+b.w/2, cy=b.y+b.h/2;
if(b.hp<=0){
b.alive=false;
destructible--;
const mult=1+Math.max(0,combo-1)*.25;
const pts=Math.round(b.maxHp*10*mult);
addScore(pts);
const col=brickColors(b).fill;
burst(cx,cy,col,12);
ring(cx,cy,col,20);
S.break();
addShake(2.2);
if(freezeT<.04) freezeT=.04;
floatText(cx,cy-8,'+'+pts,col);
if(combo>1) floatText(cx,cy+10,'×'+mult.toFixed(2),'#e2e8f0');
if(b.type==='explosive') explode(b);
maybeDrop(cx,cy);
if(destructible<=0) levelClear();
}else{
S.brick(b.maxHp);
burst(cx,cy,brickColors(b).hi,4,{speed:.5});
}
}
function explode(b){
const cx=b.x+b.w/2, cy=b.y+b.h/2;
S.explode();
addShake(7);
if(freezeT<.07) freezeT=.07;
burst(cx,cy,'#ef4444',24,{speed:1.5,up:40});
ring(cx,cy,'#ef4444',34);
for(const o of bricks){
if(!o.alive||o===b) continue;
if(Math.hypot(o.x+o.w/2-cx,o.y+o.h/2-cy)<b.w+o.w) damageBrick(o,1);
}
}
function hitBricks(ball){
for(const b of bricks){
if(!b.alive) continue;
if(ball.x+ball.r<b.x||ball.x-ball.r>b.x+b.w||
ball.y+ball.r<b.y||ball.y-ball.r>b.y+b.h) continue;
const cx=clamp(ball.x,b.x,b.x+b.w), cy=clamp(ball.y,b.y,b.y+b.h);
const dx=ball.x-cx, dy=ball.y-cy;
if(dx*dx+dy*dy>ball.r*ball.r) continue;
if(b.type!=='hard'){ combo++; updateCombo(); }
if(ball.fire&&b.type!=='hard'){ damageBrick(b,1); return true; }
const px=ball.prevX??ball.x, py=ball.prevY??ball.y;
let nx=0, ny=0;
if(px+ball.r<=b.x) nx=-1;
else if(px-ball.r>=b.x+b.w) nx=1;
else if(py+ball.r<=b.y) ny=-1;
else if(py-ball.r>=b.y+b.h) ny=1;
else{
const ox=(ball.r+b.w/2)-Math.abs(ball.x-(b.x+b.w/2));
const oy=(ball.r+b.h/2)-Math.abs(ball.y-(b.y+b.h/2));
if(ox<oy) nx=ball.x<b.x+b.w/2?-1:1;
else ny=ball.y<b.y+b.h/2?-1:1;
}
if(nx){ ball.vx=nx*Math.abs(ball.vx); ball.x=nx<0?b.x-ball.r-.1:b.x+b.w+ball.r+.1; }
else { ball.vy=ny*Math.abs(ball.vy); ball.y=ny<0?b.y-ball.r-.1:b.y+b.h+ball.r+.1; }
ball.squashT=.1;
addShake(1);
damageBrick(b,1);
return true;
}
return false;
}
/* ---------- update ---------- */
function updatePlay(dt){
fx.expandT=Math.max(0,fx.expandT-dt);
fx.slowT=Math.max(0,fx.slowT-dt);
fx.laserT=Math.max(0,fx.laserT-dt);
fx.fireT=Math.max(0,fx.fireT-dt);
if(fx.fireT<=0) for(const b of balls) b.fire=false;
paddle.w+=((fx.expandT>0?paddle.baseW*1.7:paddle.baseW)-paddle.w)*Math.min(1,dt*9);
paddle.squash+=(1-paddle.squash)*Math.min(1,dt*14);
let dir=0;
if(keys['ArrowLeft']||keys['KeyA']) dir-=1;
if(keys['ArrowRight']||keys['KeyD']) dir+=1;
if(dir) paddle.x+=dir*840*dt;
else if(mouseActive) paddle.x=lerp(paddle.x,mouseX,Math.min(1,dt*24));
paddle.x=clamp(paddle.x,paddle.w/2+4,W-paddle.w/2-4);
if(fx.laserT>0){
paddle.laserCd-=dt;
if(paddle.laserCd<=0){ shootLaser(); paddle.laserCd=.3; }
}
for(const b of bricks){
if(b.appear>0) b.appear-=dt;
else if(b.scale<1) b.scale=Math.min(1,b.scale+dt*3.5);
if(b.flash>0) b.flash-=dt*3.5;
}
for(let i=lasers.length-1;i>=0;i--){
const L=lasers[i];
L.y+=L.vy*dt;
let gone=false;
if(L.y>24){
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&&L.y<b.y+b.h){
damageBrick(b,1);
burst(L.x,L.y,'#fb7185',4,{speed:.4});
gone=true;
break;
}
}
}
if(gone||L.y<10) lasers.splice(i,1);
}
for(let i=falls.length-1;i>=0;i--){
const f=falls[i];
f.y+=f.vy*dt;
f.ph+=dt*3;
if(f.y>paddle.y-16&&f.y<paddle.y+20&&Math.abs(f.x-paddle.x)<paddle.w/2+16){
applyPU(f.type);
burst(f.x,paddle.y-8,f.color,10,{speed:.7,up:30});
floatText(f.x,paddle.y-24,f.label,f.color,true);
falls.splice(i,1);
updateHUD();
}else if(f.y>H+30) falls.splice(i,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-paddle.h/2-b.r-1;
b.trail.length=0;
continue;
}
const eff=b.speed*(fx.slowT>0?.72:1);
const sp=Math.hypot(b.vx,b.vy)||eff;
b.vx*=eff/sp; b.vy*=eff/sp;
if(b.squashT>0) b.squashT-=dt;
const steps=clamp(Math.ceil(eff*dt/(b.r*1.4)),1,4);
let dead=false;
for(let s=0;s<steps;s++){
const sdt=dt/steps;
b.prevX=b.x; b.prevY=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); S.wall(); spark(b.x,b.y); }
else if(b.x>W-b.r){ b.x=W-b.r; b.vx=-Math.abs(b.vx); S.wall(); spark(b.x,b.y); }
if(b.y<b.r){ b.y=b.r; b.vy=Math.abs(b.vy); S.wall(); spark(b.x,b.y); }
if(b.y>H+20){ dead=true; break; }
if(b.vy>0&&b.y+b.r>=paddle.y-1&&b.y+b.r<=paddle.y+paddle.h+14&&
Math.abs(b.x-paddle.x)<=paddle.w/2+b.r*.6){
const rel=clamp((b.x-paddle.x)/(paddle.w/2),-1,1);
let ang=rel*1.05;
if(Math.abs(ang)<.22) ang=.22*(rel<0?-1:1);
b.speed=Math.min(b.speed*1.018,speedCap);
const ns=b.speed*(fx.slowT>0?.72:1);
b.vx=Math.cos(ang)*ns; b.vy=Math.sin(ang)*ns;
b.y=paddle.y-b.r-1;
b.squashT=.12; paddle.squash=.74;
combo=0; updateCombo();
S.paddle();
burst(b.x,paddle.y-4,'#22d3ee',6,{speed:.5,up:50});
addShake(1.4);
}
hitBricks(b);
}
b.trail.push({x:b.x,y:b.y});
if(b.trail.length>9) b.trail.shift();
if(b.fire&&Math.random()<.6){
parts.push({x:b.x+rnd(-3,3),y:b.y+rnd(-3,3),vx:rnd(-25,25),vy:rnd(40,110),
life:.35,max:.35,size:rnd(1.5,3),color:Math.random()<.5?'#fb923c':'#fbbf24',
shape:'circle',g:-60});
}
if(dead) balls.splice(i,1);
}
if(balls.length===0&&pendingServe<=0) loseLife();
else if(pendingServe>0){
pendingServe-=dt;
if(pendingServe<=0) spawnStuckBall();
}
}
function update(dt){
if(freezeT>0){ freezeT-=dt; return; } // hit-stop: world pauses for a few ms
t+=dt;
if(flashR>0) flashR=Math.max(0,flashR-dt*1.4);
if(flashW>0) flashW=Math.max(0,flashW-dt*1.6);
shakeA=Math.max(0,shakeA-dt*10);
if(shakeA<.05) shakeA=0;
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||0)*dt;
p.x+=p.vx*dt; p.y+=p.vy*dt;
}
for(let i=texts.length-1;i>=0;i--){
const f=texts[i];
f.life-=dt; f.y+=f.vy*dt;
if(f.life<=0) texts.splice(i,1);
}
updateChips();
if(state==='playing'||state==='transition'){
updatePlay(dt);
if(state==='transition'){
transitionT-=dt;
if(transitionT<=0&&afterTransition){
const fn=afterTransition; afterTransition=null; fn();
}
}
}
}
/* ---------- render ---------- */
function roundRect(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();
}
const stars=Array.from({length:80},()=>({x:Math.random()*W,y:Math.random()*H,
z:rnd(.25,1),tw:rnd(0,TAU),sp:rnd(.4,1.8),vy:rnd(3,12)}));
function drawStars(dt){
ctx.fillStyle='#bfe9ff';
for(const s of stars){
s.tw+=dt*s.sp; s.y+=dt*s.vy*s.z;
if(s.y>H+4){ s.y=-4; s.x=Math.random()*W; }
ctx.globalAlpha=(.15+.55*(.5+.5*Math.sin(s.tw)))*s.z;
ctx.beginPath(); ctx.arc(s.x,s.y,s.z*1.3,0,TAU); ctx.fill();
}
ctx.globalAlpha=1;
}
function drawBricks(){
for(const b of bricks){
if(b.scale<=.01) continue;
const sc=easeOutBack(b.scale);
ctx.save();
const cx=b.x+b.w/2, cy=b.y+b.h/2;
ctx.translate(cx,cy); ctx.scale(sc,sc); ctx.translate(-cx,-cy);
const col=brickColors(b);
ctx.shadowColor=col.fill; ctx.shadowBlur=10*b.scale;
roundRect(b.x,b.y,b.w,b.h,5);
ctx.fillStyle=col.fill; ctx.fill();
ctx.shadowBlur=0;
ctx.strokeStyle='rgba(0,0,0,.35)'; ctx.lineWidth=1; ctx.stroke();
ctx.fillStyle=col.hi;
roundRect(b.x+2.5,b.y+2.5,b.w-5,5,2.5); ctx.fill();
ctx.fillStyle=col.lo;
ctx.fillRect(b.x+2.5,b.y+b.h-6,b.w-5,3.5);
if(b.type==='hard'){
ctx.fillStyle='rgba(15,23,42,.55)';
ctx.beginPath(); ctx.arc(b.x+7,b.y+b.h/2,2.2,0,TAU); ctx.fill();
ctx.beginPath(); ctx.arc(b.x+b.w-7,b.y+b.h/2,2.2,0,TAU); ctx.fill();
}
if(b.type==='explosive'){
const k=.5+.5*Math.sin(t*7+b.pulse);
ctx.fillStyle='rgba(255,255,255,'+(.35+.5*k)+')';
ctx.beginPath(); ctx.arc(cx,cy,4.5,0,TAU); ctx.fill();
}
if(b.flash>0){
ctx.fillStyle='rgba(255,255,255,'+Math.min(1,b.flash*1.6)+')';
roundRect(b.x,b.y,b.w,b.h,5); ctx.fill();
}
ctx.restore();
}
}
function drawFalls(){
for(const f of falls){
const y=f.y+Math.sin(f.ph)*3;
ctx.save();
ctx.translate(f.x,y);
ctx.shadowColor=f.color; ctx.shadowBlur=12;
roundRect(-f.w/2,-f.h/2,f.w,f.h,6);
ctx.fillStyle='rgba(10,14,30,.92)'; ctx.fill();
ctx.strokeStyle=f.color; ctx.lineWidth=1.5; ctx.stroke();
ctx.shadowBlur=0;
ctx.fillStyle='rgba(255,255,255,.25)';
ctx.fillRect(-f.w/2+3,-f.h/2+2.5,f.w-6,3);
ctx.fillStyle=f.color;
ctx.font='700 11px ui-monospace, Menlo, monospace';
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(f.label,0,1);
ctx.restore();
}
}
function drawLasers(){
ctx.save();
ctx.shadowColor='#fb7185'; ctx.shadowBlur=8;
ctx.fillStyle='#fecdd3';
for(const L of lasers){ roundRect(L.x-2,L.y-14,4,16,2); ctx.fill(); }
ctx.restore();
}
function drawPaddle(){
ctx.save();
const w=paddle.w, h=paddle.h, x=paddle.x-w/2, y=paddle.y;
ctx.translate(paddle.x,y+h/2);
ctx.scale(1,paddle.squash);
ctx.translate(-paddle.x,-(y+h/2));
ctx.shadowColor='rgba(34,211,238,.75)'; ctx.shadowBlur=16;
ctx.fillStyle='#22d3ee';
roundRect(x,y,w,h,h/2); ctx.fill();
ctx.shadowBlur=0;
ctx.fillStyle='rgba(255,255,255,.75)';
ctx.fillRect(x+4,y+2,w-8,3);
ctx.fillStyle='rgba(8,20,30,.35)';
for(let gx=x+14;gx<x+w-10;gx+=14) ctx.fillRect(gx,y+h-5,6,3);
if(fx.laserT>0){
ctx.fillStyle='#fb7185';
ctx.shadowColor='#fb7185'; ctx.shadowBlur=10;
ctx.fillRect(x+5,y-6,8,8);
ctx.fillRect(x+w-13,y-6,8,8);
ctx.shadowBlur=0;
}
ctx.restore();
ctx.strokeStyle='rgba(34,211,238,.25)'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(x+6,y+h+3); ctx.lineTo(x+w-6,y+h+3); ctx.stroke();
}
function drawBalls(){
for(const b of balls){
for(let i=0;i<b.trail.length;i++){
const p=b.trail[i], k=(i+1)/b.trail.length;
ctx.globalAlpha=.08+.3*k;
ctx.fillStyle=b.fire?'#fb923c':'#67e8f9';
ctx.beginPath(); ctx.arc(p.x,p.y,b.r*(.25+.65*k),0,TAU); ctx.fill();
}
ctx.globalAlpha=1;
const sp=Math.hypot(b.vx,b.vy);
let stretch=Math.min(.45,sp/2600);
if(b.squashT>0) stretch+=.3*(b.squashT/.12);
const pr=b.stuck?1+Math.sin(t*5)*.1:1;
ctx.save();
ctx.translate(b.x,b.y);
ctx.rotate(Math.atan2(b.vy,b.vx));
ctx.shadowColor=b.fire?'rgba(251,146,60,.9)':'rgba(103,232,249,.9)';
ctx.shadowBlur=14;
ctx.fillStyle=b.fire?'#ffedd5':'#f0feff';
ctx.beginPath();
ctx.ellipse(0,0,b.r*(1+stretch)*pr,b.r*(1-stretch*.55)*pr,0,0,TAU);
ctx.fill();
ctx.shadowBlur=0;
ctx.fillStyle=b.fire?'#fb923c':'#ffffff';
ctx.beginPath(); ctx.arc(0,0,b.r*.45,0,TAU); ctx.fill();
ctx.restore();
}
}
function drawParticles(){
for(const p of parts){
const k=p.life/p.max;
if(p.ring){
ctx.globalAlpha=k*.8;
ctx.strokeStyle=p.color;
ctx.lineWidth=2.5*(1-k)+.5;
ctx.beginPath();
ctx.arc(p.x,p.y,lerp(p.r0,p.maxR,1-k),0,TAU);
ctx.stroke();
}else{
ctx.globalAlpha=k;
ctx.fillStyle=p.color;
if(p.shape==='rect') ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);
else{
ctx.beginPath();
ctx.arc(p.x,p.y,p.size*(.5+.5*k),0,TAU);
ctx.fill();
}
}
}
ctx.globalAlpha=1;
}
function drawTexts(){
ctx.textAlign='center'; ctx.textBaseline='middle';
for(const f of texts){
ctx.globalAlpha=f.life/f.max;
ctx.shadowColor=f.color; ctx.shadowBlur=10;
ctx.fillStyle=f.color;
ctx.font='800 '+f.size+'px ui-monospace, Menlo, monospace';
ctx.fillText(f.str,f.x,f.y);
ctx.shadowBlur=0;
}
ctx.globalAlpha=1;
}
function drawHint(){
ctx.save();
ctx.globalAlpha=.55+.45*Math.sin(t*4);
ctx.fillStyle='#a5f3fc';
ctx.shadowColor='rgba(34,211,238,.8)'; ctx.shadowBlur=10;
ctx.font='700 13px ui-monospace, Menlo, monospace';
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText('SPACE / CLICK TO LAUNCH',W/2,paddle.y-34);
ctx.restore();
}
function drawVignette(){
const g=ctx.createRadialGradient(W/2,H*.42,H*.35,W/2,H*.55,H*1.05);
g.addColorStop(0,'rgba(0,0,0,0)');
g.addColorStop(1,'rgba(2,3,10,.5)');
ctx.fillStyle=g;
ctx.fillRect(0,0,W,H);
}
function render(dt){
ctx.fillStyle='#070812';
ctx.fillRect(0,0,W,H);
drawStars(dt);
ctx.save();
if(shakeA>.05) ctx.translate(rnd(-shakeA,shakeA),rnd(-shakeA,shakeA));
drawBricks();
drawFalls();
drawLasers();
drawPaddle();
drawBalls();
drawParticles();
drawTexts();
if((state==='playing'||state==='transition')&&balls.some(b=>b.stuck)) drawHint();
ctx.restore();
drawVignette();
if(flashR>0){ ctx.fillStyle='rgba(239,68,68,'+flashR+')'; ctx.fillRect(0,0,W,H); }
if(flashW>0){ ctx.fillStyle='rgba(224,255,255,'+flashW+')'; ctx.fillRect(0,0,W,H); }
}
/* ---------- main loop ---------- */
let last=performance.now();
requestAnimationFrame(function frame(now){
let dt=(now-last)/1000;
last=now;
dt=Math.min(dt,.033);
update(dt);
render(dt);
requestAnimationFrame(frame);
});
/* ---------- init ---------- */
bestEl.textContent=fmt(best);
menuBest.textContent='BEST '+fmt(best);
muteBtn.textContent=muted?'SOUND OFF':'SOUND ON';
updateHUD();
showOv('menu');
})();
</script>
</body>
</html>

