Run details
- Model
- opencode/mimo-v2.6-flash-free
- Route
- opencode CLI
- Reasoning
- provider default
- Limits
- 60 min · 32,000 output tokens
- Date
- 2 Oct 2026
- Time
- 23 min 55 s
- Size
- 44 kB
- Tokens
- 108,619 in, 73,950 out
- List price
- $0.036 ($0.14 in / $0.28 out per M)
- Paid
- Not billed per run (CLI plan)
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). Save it as index.html in the current directory, then stop. Do not ask questions.
Source
44 kB 1,299 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>BREAKOUT // Neon Blocks</title>
<style>
:root{
--cyan:#38f5ff;
--magenta:#ff3ea5;
--lime:#9dff4f;
--gold:#ffd23e;
--bg:#04050b;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
background:
radial-gradient(900px 600px at 20% 10%, rgba(56,245,255,.10), transparent 60%),
radial-gradient(800px 700px at 85% 90%, rgba(255,62,165,.10), transparent 60%),
#04050b;
color:#dfe7ff;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
display:flex; align-items:center; justify-content:center;
overflow:hidden;
-webkit-font-smoothing:antialiased;
user-select:none;
}
#stage{
position:relative;
width:min(97vw, calc(96vh * 1.5));
aspect-ratio:3/2;
border-radius:14px;
overflow:hidden;
box-shadow:
0 0 0 1px rgba(56,245,255,.35),
0 0 34px rgba(56,245,255,.22),
0 0 90px rgba(255,62,165,.16),
0 24px 70px rgba(0,0,0,.7);
background:#04050b;
}
canvas{display:block;width:100%;height:100%;cursor:crosshair;touch-action:none}
#stage::after{
content:"";position:absolute;inset:0;pointer-events:none;
background:repeating-linear-gradient(to bottom, rgba(255,255,255,.035) 0 1px, transparent 1px 3px);
mix-blend-mode:overlay;opacity:.5;
}
#vign{position:absolute;inset:0;pointer-events:none;
background:radial-gradient(120% 100% at 50% 50%, transparent 55%, rgba(0,0,0,.55) 100%);}
/* banner */
.banner{
position:absolute;left:0;right:0;top:38%;
text-align:center;pointer-events:none;opacity:0;
transform:scale(.7);
}
.banner.show{animation:bannerIn 2.3s cubic-bezier(.2,1.4,.3,1) forwards}
.banner .big{
font-size:clamp(28px,6vw,62px);font-weight:800;letter-spacing:.14em;
color:#fff;text-shadow:0 0 18px var(--cyan),0 0 46px rgba(56,245,255,.6);
}
.banner .sub{
margin-top:.5em;font-size:clamp(11px,1.9vw,18px);letter-spacing:.42em;
color:var(--gold);text-shadow:0 0 14px rgba(255,210,62,.7);
}
@keyframes bannerIn{
0%{opacity:0;transform:scale(.6) translateY(18px)}
16%{opacity:1;transform:scale(1.04) translateY(0)}
24%{transform:scale(1)}
74%{opacity:1;transform:scale(1)}
100%{opacity:0;transform:scale(1.12) translateY(-14px)}
}
/* overlays */
.ov{
position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
background:rgba(4,6,14,.72);backdrop-filter:blur(6px);
transition:opacity .18s ease;
}
.ov.hidden{opacity:0;pointer-events:none}
.panel{
width:min(84%,560px);text-align:center;padding:26px 26px 24px;
border:1px solid rgba(56,245,255,.35);border-radius:14px;
background:linear-gradient(180deg, rgba(10,16,34,.94), rgba(6,8,18,.96));
box-shadow:0 0 0 1px rgba(255,255,255,.04) inset, 0 0 40px rgba(56,245,255,.16);
animation:pop .28s cubic-bezier(.2,1.5,.4,1);
}
@keyframes pop{from{opacity:0;transform:translateY(14px) scale(.96)}to{opacity:1;transform:none}}
.panel.hidden{display:none}
h1{
font-size:clamp(30px,7vw,58px);font-weight:800;letter-spacing:.16em;color:#fff;
text-shadow:0 0 16px var(--cyan), 0 0 52px rgba(56,245,255,.55), 0 4px 0 rgba(255,62,165,.45);
}
h1 em{font-style:normal;color:var(--magenta);text-shadow:0 0 18px var(--magenta)}
.sub{margin-top:2px;font-size:clamp(10px,1.8vw,14px);letter-spacing:.5em;color:var(--gold)}
.rules{
margin:16px auto 0;display:grid;grid-template-columns:1fr 1fr;gap:6px 14px;
font-size:clamp(10px,1.6vw,13px);color:#9fb2d8;text-align:left;
}
.rules b{color:#e8f0ff;font-weight:700}
.key{
display:inline-block;padding:0 5px;border:1px solid rgba(159,178,216,.45);
border-radius:4px;color:#cfe0ff;background:rgba(255,255,255,.05);
}
button{
margin-top:18px;font:inherit;font-weight:800;letter-spacing:.22em;font-size:16px;
padding:12px 34px;border-radius:999px;cursor:pointer;color:#04121a;
background:linear-gradient(180deg,#8ffbff,var(--cyan));
border:1px solid rgba(255,255,255,.6);
box-shadow:0 0 22px rgba(56,245,255,.6), 0 6px 0 #0a6d78;
transition:transform .08s ease, box-shadow .12s ease, filter .12s ease;
}
button:hover{filter:brightness(1.08);box-shadow:0 0 34px rgba(56,245,255,.85),0 6px 0 #0a6d78}
button:active{transform:translateY(4px);box-shadow:0 0 22px rgba(56,245,255,.6),0 2px 0 #0a6d78}
.hint{margin-top:14px;font-size:clamp(9px,1.5vw,12px);color:#7b8cb4;letter-spacing:.06em;line-height:1.7}
.big-score{
margin-top:14px;font-size:clamp(26px,5vw,44px);font-weight:800;color:#fff;
text-shadow:0 0 18px var(--magenta),0 0 44px rgba(255,62,165,.6);
}
.stat{display:flex;justify-content:center;gap:26px;margin-top:10px;font-size:12px;color:#9fb2d8}
.stat span b{display:block;font-size:18px;color:var(--gold);margin-top:3px}
.newhi{margin-top:8px;font-size:13px;color:var(--lime);letter-spacing:.3em;
text-shadow:0 0 14px rgba(157,255,79,.8);animation:blink .7s steps(2) infinite}
@keyframes blink{50%{opacity:.25}}
.title-wrap{position:relative}
</style>
</head>
<body>
<div id="stage">
<canvas id="cv" width="960" height="640"></canvas>
<div id="vign"></div>
<div class="banner" id="banner"><div class="big" id="bannerBig"></div><div class="sub" id="bannerSub"></div></div>
<div class="ov" id="ov">
<div class="panel" id="pMenu">
<div class="title-wrap"><h1>BREAK<em>OUT</em></h1></div>
<div class="sub">NEON BLOCKS</div>
<div class="rules">
<div><b>MOVE</b> <span class="key">←</span> <span class="key">→</span> / <span class="key">A</span> <span class="key">D</span> / mouse</div>
<div><b>LAUNCH</b> <span class="key">SPACE</span> / click</div>
<div><b>SHOOT</b> <span class="key">SPACE</span> (laser)</div>
<div><b>PAUSE</b> <span class="key">P</span> · <b>MUTE</b> <span class="key">M</span></div>
<div><b>WIDE</b> wider paddle</div>
<div><b>MULTI</b> split the ball</div>
<div><b>SLOW</b> ease time</div>
<div><b>+1UP / +500</b> goodies</div>
</div>
<button id="btnPlay">PLAY</button>
<div class="hint">8 levels · combos · screen shake · no mercy<br>break every soft brick to advance</div>
</div>
<div class="panel hidden" id="pPause">
<h1 style="font-size:clamp(24px,5vw,42px)">PAUSED</h1>
<div class="sub">TAKE A BREATH</div>
<button id="btnResume">RESUME</button>
<div class="hint"><span class="key">P</span> or <span class="key">ESC</span> to resume</div>
</div>
<div class="panel hidden" id="pEnd">
<h1 id="endTitle" style="font-size:clamp(26px,5.6vw,48px)">GAME OVER</h1>
<div class="sub" id="endSub">THE BLOCKS WIN</div>
<div class="big-score" id="endScore">0</div>
<div class="stat">
<span>LEVEL<b id="endLevel">1</b></span>
<span>BEST<b id="endHi">0</b></span>
<span>TOP COMBO<b id="endCombo">0</b></span>
</div>
<div class="newhi hidden" id="endNewHi" style="display:none">★ NEW HIGH SCORE ★</div>
<button id="btnRetry">PLAY AGAIN</button>
<div class="hint"><span class="key">SPACE</span> to restart</div>
</div>
</div>
</div>
<script>
'use strict';
/* ============================== SETUP ============================== */
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = 960, H = 640;
const COLS = 14;
const FIELD = { x: 44, w: W - 88 };
const bw = FIELD.w / COLS;
const bh = 26;
const BRICK_TOP = 104;
const clamp = (v,a,b) => v < a ? a : (v > b ? b : v);
const rand = (a,b) => a + Math.random() * (b-a);
const pick = a => a[(Math.random()*a.length)|0];
const easeOutBack = p => { const c = 1.70158 * 1.525; return 1 + (c+1) * Math.pow(p-1,3) + c * Math.pow(p-1,2); };
const isMobile = matchMedia('(pointer:coarse)').matches;
/* ============================== AUDIO ============================== */
let AC = null, muted = false, noiseBuf = null;
function audio(){
if (muted) return null;
if (!AC){
try { AC = new (window.AudioContext || window.webkitAudioContext)(); } catch(e){ return null; }
}
if (AC.state === 'suspended') AC.resume();
return AC;
}
function tone(freq, dur, type, vol, slideTo, delay){
const ac = audio(); if (!ac) return;
type = type || 'square'; vol = vol == null ? 0.05 : vol;
const t0 = ac.currentTime + (delay || 0);
const o = ac.createOscillator(), g = ac.createGain();
o.type = type;
o.frequency.setValueAtTime(Math.max(20,freq), t0);
if (slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(20,slideTo), t0 + dur);
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(vol, t0 + 0.012);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
o.connect(g); g.connect(ac.destination);
o.start(t0); o.stop(t0 + dur + 0.03);
}
function noise(dur, vol, freq, q, delay){
const ac = audio(); if (!ac) return;
if (!noiseBuf){
const n = ac.sampleRate * 0.5;
noiseBuf = ac.createBuffer(1, n, ac.sampleRate);
const d = noiseBuf.getChannelData(0);
for (let i=0;i<n;i++) d[i] = Math.random()*2-1;
}
const t0 = ac.currentTime + (delay||0);
const src = ac.createBufferSource(); src.buffer = noiseBuf; src.loop = true;
const f = ac.createBiquadFilter(); f.type = 'bandpass';
f.frequency.setValueAtTime(freq||900, t0);
if (freq) f.frequency.exponentialRampToValueAtTime(Math.max(80,(freq||900)*0.45), t0+dur);
f.Q.value = q || 1;
const g = ac.createGain();
g.gain.setValueAtTime(vol||0.08, t0);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
src.connect(f); f.connect(g); g.connect(ac.destination);
src.start(t0); src.stop(t0 + dur + 0.02);
}
const sfx = {
paddle: () => { tone(320, .07, 'square', .045, 240); noise(.05,.03,1400,1); },
wall: () => tone(620, .045, 'triangle', .035, 520),
tick: (c) => tone(420 + c*24, .06, 'square', .04, 340 + c*20),
crack: (c) => { tone(300 + c*40, .09, 'sawtooth', .045, 150); noise(.12,.07,1200 + c*60, 1.2); },
brk: (c) => { tone(560 + Math.min(c,10)*46, .1, 'square', .05, 260); noise(.16,.09,1800, .9); },
steel: () => { tone(150, .07, 'triangle', .05, 110); noise(.05,.04,600,2); },
power: () => { tone(660,.07,'square',.05); tone(880,.08,'square',.05,null,.06); tone(1180,.12,'square',.05,null,.12); },
life: () => { [523,659,784,1046].forEach((f,i)=>tone(f,.14,'triangle',.05,null,i*.07)); },
zap: () => tone(980, .08, 'sawtooth', .035, 240),
lose: () => { tone(340,.55,'sawtooth',.07,70); noise(.4,.06,700,.6); },
launch: () => { tone(180,.1,'square',.05,340); noise(.07,.04,900,1); },
clear: () => { [523,659,784,1046,1318].forEach((f,i)=>tone(f,.2,'square',.05,null,i*.09)); },
over: () => { [440,392,330,262,196].forEach((f,i)=>tone(f,.3,'sawtooth',.06,null,i*.13)); },
win: () => { [523,659,784,1046,784,1046,1318,1568].forEach((f,i)=>tone(f,.22,'square',.05,null,i*.11)); }
};
/* ============================== LEVELS ============================== */
const LEVELS = [
[ '11111111111111',
'11111111111111',
'11111111111111',
'.111111111111.',
'.111111111111.' ],
[ '10101010101010',
'01010101010101',
'10101010101010',
'01010101010101',
'11111111111111',
'11o11o11o11o11' ],
[ '......11......',
'.....1111.....',
'....111111....',
'...11111111...',
'..1122222211..',
'...11222211...',
'....112211....',
'.....1111.....' ],
[ '22222222222222',
'X..11111111..X',
'X..11o1o111..X',
'X..11111111..X',
'22222222222222',
'.111111111111.' ],
[ '33333333333333',
'2.....22.....2',
'2.111.22.111.2',
'2.1X1.22.1X1.2',
'2.111.22.111.2',
'2.....22.....2',
'33333333333333' ],
[ '....111111....',
'..1111111111..',
'11111111111111',
'11222222222211',
'1111o111o11111',
'11222222222211',
'11111111111111',
'..1111111111..' ],
[ '22222222222222',
'1X1X1X1X1X1X1X',
'1X1o1X1X1o1X1X',
'11111111111111',
'11o11o11o11o11',
'11111111111111',
'3X3X3X3X3X3X3X' ],
[ '33333333333333',
'3X3X3X3X3X3X3X',
'33333333333333',
'2o2o2o2o2o2o2o',
'22222222222222',
'11111111111111',
'o111o111o111o1',
'11111111111111' ]
];
const LEVEL_NAMES = ['WARM UP','CHECKER','DIAMOND','THE VAULT','FORTRESS','EYE OF STORM','STEEL MAZE','FINAL GAUNTLET'];
const TYPE = {
'1': { hp:1, pts:50, kind:'normal' },
'2': { hp:2, pts:80, kind:'hard' },
'3': { hp:3, pts:120, kind:'hard' },
'o': { hp:1, pts:200, kind:'bonus' },
'X': { hp:1e9, pts:0, kind:'steel' }
};
const PUPS = {
E:{ label:'W', color:'#4dff88', name:'WIDE PADDLE' },
M:{ label:'M', color:'#38f5ff', name:'MULTIBALL' },
S:{ label:'S', color:'#b06bff', name:'SLOW MOTION' },
L:{ label:'1', color:'#ff5f7e', name:'+1 LIFE' },
Z:{ label:'Z', color:'#ffd23e', name:'LASER' },
P:{ label:'$', color:'#ff9f3e', name:'+500' }
};
const PUP_POOL = ['E','E','E','M','M','M','S','S','Z','Z','P','P','P','L'];
/* ============================== STATE ============================== */
let state = 'menu';
let stateTimer = 0, hitStop = 0, time = 0;
let level = 0, score = 0, lives = 3, combo = 0, topCombo = 0, hi = 0;
let shake = 0, flash = 0, flashColor = '255,255,255';
let balls = [], bricks = [], parts = [], texts = [], pups = [], bolts = [];
let slowT = 0, levelBurstDone = false;
const paddle = {
x: W/2, vx: 0, w: 132, baseW: 132, h: 16, y: H - 58,
squish: 0, wide: 0, laser: 0, cool: 0
};
let stars = [];
for (let i=0;i<90;i++) stars.push({ x: Math.random()*W, y: Math.random()*H, z: Math.random(), s: Math.random()*1.7+.5 });
try { hi = parseInt(localStorage.getItem('breakout_hi')||'0',10) || 0; } catch(e){ hi = 0; }
function saveHi(){
if (score > hi){
hi = score;
try { localStorage.setItem('breakout_hi', String(hi)); } catch(e){}
return true;
}
return false;
}
const keys = { left:false, right:false };
let mouseActive = isMobile ? true : false, mouseX = W/2, mouseDown = false;
let actionQueued = false;
function setState(s){ state = s; stateTimer = 0; }
const softAlive = () => bricks.some(b => b.alive && b.kind !== 'steel');
/* ============================== LEVEL LOADING ============================== */
function loadLevel(i){
level = i;
bricks = [];
const rows = LEVELS[i];
for (let r=0;r<rows.length;r++){
const line = rows[r].padEnd(COLS,'.').slice(0,COLS);
for (let c=0;c<COLS;c++){
const ch = line[c];
if (ch === '.' || ch === '0') continue;
const def = TYPE[ch];
const hue = (i*47 + r*23) % 360;
bricks.push({
x: FIELD.x + c*bw + 2,
y: BRICK_TOP + r*bh + 2,
w: bw - 4, h: bh - 6,
hp: def.hp, maxHp: def.hp, pts: def.pts,
kind: def.kind, row: r, col: c, ch: ch,
hue: ch === 'o' ? 46 : (ch === 'X' ? 0 : hue),
alive: true, solid: false, t: 0,
delay: r*0.055 + c*0.022 + i*0.01,
scale: 0, flash: 0
});
}
}
}
function spawnBall(stuck){
balls = [{
x: paddle.x, y: paddle.y - 16, r: 8,
vx: 0, vy: 0, stuck: !!stuck, trail: [], spin: Math.random()*6.28
}];
}
function baseSpeed(){ return 400 + level*26; }
function resetPowerups(){
paddle.wide = 0; paddle.laser = 0; slowT = 0; paddle.w = paddle.baseW;
}
function startGame(){
score = 0; lives = 3; combo = 0; topCombo = 0;
parts = []; texts = []; pups = []; bolts = [];
resetPowerups();
loadLevel(0);
spawnBall(true);
hideOverlays();
setState('ready');
showBanner('LEVEL 1', LEVEL_NAMES[0]);
sfx.launch();
}
function nextLevel(){
if (level + 1 >= LEVELS.length){ endGame(true); return; }
resetPowerups();
bolts = []; pups = [];
loadLevel(level + 1);
spawnBall(true);
setState('ready');
showBanner('LEVEL ' + (level+1), LEVEL_NAMES[level]);
sfx.launch();
}
function endGame(win){
const beatHi = score > 0 && score > hi;
saveHi();
setState(win ? 'win' : 'over');
document.getElementById('endTitle').textContent = win ? 'YOU WIN' : 'GAME OVER';
document.getElementById('endSub').textContent = win ? 'ALL 8 LEVELS CLEARED' : 'THE BLOCKS WIN';
document.getElementById('endScore').textContent = score.toLocaleString();
document.getElementById('endLevel').textContent = (level+1) + '/' + LEVELS.length;
document.getElementById('endHi').textContent = hi.toLocaleString();
document.getElementById('endCombo').textContent = 'x' + topCombo;
const nh = document.getElementById('endNewHi');
nh.style.display = beatHi ? 'block' : 'none';
document.getElementById('pMenu').classList.add('hidden');
document.getElementById('pPause').classList.add('hidden');
document.getElementById('pEnd').classList.remove('hidden');
document.getElementById('ov').classList.remove('hidden');
if (win) sfx.win(); else sfx.over();
}
/* ============================== OVERLAY ============================== */
function hideOverlays(){ document.getElementById('ov').classList.add('hidden'); }
function showPause(){
document.getElementById('pMenu').classList.add('hidden');
document.getElementById('pEnd').classList.add('hidden');
document.getElementById('pPause').classList.remove('hidden');
document.getElementById('ov').classList.remove('hidden');
}
function showBanner(big, sub){
const el = document.getElementById('banner');
document.getElementById('bannerBig').textContent = big;
document.getElementById('bannerSub').textContent = sub || '';
el.classList.remove('show');
void el.offsetWidth;
el.classList.add('show');
}
document.getElementById('btnPlay').addEventListener('click', () => { audio(); startGame(); });
document.getElementById('btnRetry').addEventListener('click', () => { audio(); startGame(); });
document.getElementById('btnResume').addEventListener('click', () => { audio(); setState(prevState); hideOverlays(); });
/* ============================== INPUT ============================== */
window.addEventListener('keydown', e => {
const c = e.code;
if (c === 'ArrowLeft' || c === 'KeyA') { keys.left = true; mouseActive = false; e.preventDefault(); }
if (c === 'ArrowRight' || c === 'KeyD'){ keys.right = true; mouseActive = false; e.preventDefault(); }
if (c === 'Space'){ if (!e.repeat){ actionQueued = true; audio(); } e.preventDefault(); }
if (c === 'Enter'){ if (!e.repeat){ actionQueued = true; audio(); } e.preventDefault(); }
if (c === 'KeyP' || c === 'Escape'){ togglePause(); e.preventDefault(); }
if (c === 'KeyM'){ muted = !muted; if (!muted) audio(); toast(muted ? 'MUTED' : 'SOUND ON'); }
});
window.addEventListener('keyup', e => {
const c = e.code;
if (c === 'ArrowLeft' || c === 'KeyA') keys.left = false;
if (c === 'ArrowRight' || c === 'KeyD') keys.right = false;
});
function pos(e){
const r = cv.getBoundingClientRect();
return (e.clientX - r.left) / r.width * W;
}
window.addEventListener('mousemove', e => {
mouseX = clamp(pos(e), 0, W);
if (e.target === cv) mouseActive = true;
});
cv.addEventListener('mousedown', e => { mouseDown = true; mouseActive = true; actionQueued = true; audio(); e.preventDefault(); });
window.addEventListener('mouseup', () => { mouseDown = false; });
cv.addEventListener('touchstart', e => { mouseActive = true; mouseX = clamp(pos(e.touches[0]),0,W); actionQueued = true; audio(); e.preventDefault(); }, {passive:false});
cv.addEventListener('touchmove', e => { mouseActive = true; mouseX = clamp(pos(e.touches[0]),0,W); e.preventDefault(); }, {passive:false});
cv.addEventListener('contextmenu', e => e.preventDefault());
window.addEventListener('blur', () => { if (state === 'play' || state === 'ready') togglePause(); });
let prevState = 'play';
function togglePause(){
if (state === 'play' || state === 'ready'){
prevState = state;
setState('paused');
showPause();
} else if (state === 'paused'){
setState(prevState);
hideOverlays();
}
}
function toast(msg){
showBanner(msg, '');
}
function handleAction(){
if (state === 'menu') { startGame(); }
else if (state === 'ready') { launchBall(); }
else if (state === 'play') { if (paddle.laser > 0) fireLaser(); }
else if (state === 'paused'){ setState(prevState); hideOverlays(); }
else if (state === 'over' || state === 'win') { startGame(); }
}
function launchBall(){
const b = balls[0]; if (!b) return;
const a = rand(-0.3, 0.3), s = baseSpeed();
b.vx = Math.sin(a)*s; b.vy = -Math.cos(a)*s;
b.stuck = false;
setState('play');
sfx.launch();
burst(b.x, b.y, 10, '#38f5ff', {spd0:60, spd1:240, grav:400});
}
function fireLaser(){
if (paddle.cool > 0) return;
paddle.cool = 0.17;
const hw = paddle.w/2;
bolts.push({ x: paddle.x - hw + 9, y: paddle.y - 8, vy: -780 });
bolts.push({ x: paddle.x + hw - 9, y: paddle.y - 8, vy: -780 });
sfx.zap();
paddle.squish = Math.max(paddle.squish, .45);
}
/* ============================== FX ============================== */
function burst(x, y, n, color, o){
o = o || {};
const n0 = o.spd0 == null ? 70 : o.spd0;
const n1 = o.spd1 == null ? 330 : o.spd1;
for (let i=0;i<n;i++){
const a = (o.angle != null) ? o.angle + rand(-1,1)*(o.spread == null ? Math.PI : o.spread) : rand(0, Math.PI*2);
const sp = rand(n0, n1);
parts.push({
x, y,
vx: Math.cos(a)*sp + (o.vx||0),
vy: Math.sin(a)*sp + (o.vy||0),
life: 0, max: rand(0.3, 0.85),
size: rand(2, 5.5), color,
rot: rand(0,6.28), vr: rand(-10,10),
grav: o.grav == null ? 950 : o.grav,
shape: o.shape || 'rect'
});
if (parts.length > 600) parts.shift();
}
}
function floatText(x, y, str, color, size){
let xx = x + rand(-9,9), yy = y + rand(-5,5), guard = 0;
while (guard++ < 8 && texts.some(t => Math.abs(t.x - xx) < 78 && Math.abs(t.y - yy) < 19)) yy -= 20;
texts.push({ x: xx, y: yy, str, color: color || '#fff', size: size || 15, life: 0, max: 0.95, vy: -46 });
if (texts.length > 40) texts.shift();
}
function doShake(a){ shake = Math.min(26, shake + a); }
function doFlash(alpha, color){ flash = Math.max(flash, alpha); flashColor = color || '255,255,255'; }
/* ============================== GAMEPLAY ============================== */
function movePaddle(dt){
const half = paddle.w/2;
let nx = paddle.x;
if (mouseActive){
nx = paddle.x + (clamp(mouseX, half, W - half) - paddle.x) * Math.min(1, dt*22);
} else {
const dir = (keys.right?1:0) - (keys.left?1:0);
nx = paddle.x + dir * 780 * dt;
}
nx = clamp(nx, half, W - half);
paddle.vx = (nx - paddle.x) / Math.max(dt, 0.0001);
paddle.x = nx;
if (balls.length && balls[0].stuck){
balls[0].x = paddle.x;
balls[0].y = paddle.y - balls[0].r - 3;
}
}
function stickBalls(){
for (const b of balls){
if (b.stuck){ b.x = paddle.x; b.y = paddle.y - b.r - 3; b.trail.length = 0; }
}
}
function resolveSpeed(b){
const s = Math.hypot(b.vx, b.vy) || 1;
if (Math.abs(b.vy) < s*0.24){
const sign = b.vy >= 0 ? 1 : -1;
b.vy = sign * s * 0.24;
const rest = Math.sqrt(Math.max(0, s*s - b.vy*b.vy));
b.vx = (b.vx >= 0 ? 1 : -1) * rest;
}
}
function boost(b, amt){
const s = Math.hypot(b.vx, b.vy); if (s < 1) return;
const ns = Math.min(940, s + amt);
b.vx *= ns/s; b.vy *= ns/s;
}
function updateBalls(dt){
const total = dt * (slowT > 0 ? 0.72 : 1);
let maxD = 0;
for (const b of balls){ if (b.stuck) continue; maxD = Math.max(maxD, Math.hypot(b.vx, b.vy) * total); }
const steps = clamp(Math.ceil(maxD / 7), 1, 8);
const sub = total / steps;
for (let s = 0; s < steps; s++) stepBalls(sub, s === steps - 1);
}
function stepBalls(step, pushTrail){
for (let i = balls.length-1; i>=0; i--){
const b = balls[i];
if (b.stuck) continue;
b.x += b.vx * step;
b.y += b.vy * step;
b.spin += step * 6;
if (pushTrail){
b.trail.push({x:b.x, y:b.y});
if (b.trail.length > 13) b.trail.shift();
}
// walls
if (b.x - b.r < 0){ b.x = b.r; b.vx = Math.abs(b.vx); sfx.wall(); doShake(2); }
else if (b.x + b.r > W){ b.x = W - b.r; b.vx = -Math.abs(b.vx); sfx.wall(); doShake(2); }
if (b.y - b.r < 0){ b.y = b.r; b.vy = Math.abs(b.vy); sfx.wall(); doShake(2); }
// paddle
if (b.vy > 0 &&
b.y + b.r >= paddle.y && b.y - b.r <= paddle.y + paddle.h &&
b.x >= paddle.x - paddle.w/2 - b.r && b.x <= paddle.x + paddle.w/2 + b.r){
const rel = clamp((b.x - paddle.x) / (paddle.w/2), -1, 1);
const ang = rel * 1.05;
const s = Math.min(940, Math.max(baseSpeed(), Math.hypot(b.vx,b.vy)));
b.vx = Math.sin(ang) * s + paddle.vx * 0.16;
b.vy = -Math.abs(Math.cos(ang) * s);
b.y = paddle.y - b.r - 1;
combo = 0;
paddle.squish = 1;
sfx.paddle();
burst(b.x, paddle.y, 6, '#38f5ff', {spd0:50, spd1:190, grav:700, angle:-Math.PI/2, spread:1.1});
}
// bricks
for (const br of bricks){
if (!br.alive || !br.solid) continue;
const cx = clamp(b.x, br.x, br.x + br.w);
const 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) continue;
const ovx = (b.r + br.w/2) - Math.abs(b.x - (br.x + br.w/2));
const ovy = (b.r + br.h/2) - Math.abs(b.y - (br.y + br.h/2));
if (ovx < ovy){
b.vx = b.x < br.x + br.w/2 ? -Math.abs(b.vx) : Math.abs(b.vx);
b.x += b.x < br.x + br.w/2 ? -ovx : ovx;
} else {
b.vy = b.y < br.y + br.h/2 ? -Math.abs(b.vy) : Math.abs(b.vy);
b.y += b.y < br.y + br.h/2 ? -ovy : ovy;
}
hitBrick(br, b);
break;
}
if (b.y - b.r > H + 20) balls.splice(i, 1);
else resolveSpeed(b);
}
}
function hitBrick(br, b){
if (br.kind === 'steel'){
br.flash = 1;
doShake(3);
sfx.steel();
burst(b.x, b.y, 6, '#cdd6e6', {spd0:60, spd1:220, grav:800});
return;
}
br.hp -= 1;
br.flash = 1;
if (br.hp <= 0){
destroyBrick(br, b);
} else {
doShake(4);
sfx.crack(br.maxHp - br.hp);
burst(br.x + br.w/2, br.y + br.h/2, 6, brickColor(br), {spd0:50, spd1:200, grav:700});
combo++;
topCombo = Math.max(topCombo, combo);
if (b) boost(b, 5);
}
}
function brickColor(br){
if (br.kind === 'steel') return '#9aa6bc';
if (br.kind === 'bonus') return '#ffd23e';
const l = br.kind === 'hard' ? (br.maxHp >= 3 ? 42 : 50) : 58;
return 'hsl(' + br.hue + ',88%,' + l + '%)';
}
function destroyBrick(br, b){
br.alive = false;
combo++;
topCombo = Math.max(topCombo, combo);
const mult = comboMult();
const gain = br.pts * mult;
score += gain;
const cx = br.x + br.w/2, cy = br.y + br.h/2;
burst(cx, cy, 13, brickColor(br), {spd0:90, spd1:380, grav:1000, shape:'shard'});
burst(cx, cy, 5, '#ffffff', {spd0:120, spd1:300, grav:600, shape:'dot'});
doShake(br.kind === 'bonus' ? 9 : 6);
doFlash(0.13, '255,255,255');
hitStop = 0.028;
sfx.brk(combo);
if (mult > 1) floatText(cx, cy, '+' + gain, '#ffd23e', 15);
if (b) boost(b, 6);
if (br.kind === 'bonus' || Math.random() < 0.15) dropPup(br.x + br.w/2, br.y + br.h/2, br.kind === 'bonus');
if (!softAlive()){
hitStop = 0.07;
setState('clear');
stateTimer = 0;
doFlash(0.4, '56,245,255');
doShake(20);
sfx.clear();
for (let i=0;i<70;i++){
const px = rand(FIELD.x, FIELD.x + FIELD.w), py = rand(BRICK_TOP, BRICK_TOP + LEVELS[level].length*bh);
burst(px, py, 2, 'hsl(' + ((level*47+i*9)%360) + ',90%,60%)', {spd0:60, spd1:340, grav:700});
}
showBanner('LEVEL CLEAR', '+' + score.toLocaleString() + ' PTS');
}
}
function comboMult(){ return Math.min(6, 1 + Math.floor(combo/5)); }
function dropPup(x, y){
pups.push({ x, y, vy: 175, type: pick(PUP_POOL), bob: Math.random()*6.28 });
}
function collectPup(p){
const t = p.type;
sfx.power();
doFlash(0.16, '157,255,79');
burst(p.x, p.y, 22, PUPS[t].color, {spd0:90, spd1:360, grav:500});
floatText(p.x, p.y - 10, PUPS[t].name, PUPS[t].color, 14);
if (t === 'E'){ paddle.wide = 14; paddle.w = 200; }
else if (t === 'Z'){ paddle.laser = 12; }
else if (t === 'S'){ slowT = 10; }
else if (t === 'L'){ lives = Math.min(9, lives + 1); sfx.life(); }
else if (t === 'P'){ score += 500; floatText(p.x, p.y - 30, '+500', '#ff9f3e', 18); }
else if (t === 'M'){
const cur = balls.slice(0, 6);
for (const b of cur){
if (balls.length >= 12) break;
const s = Math.hypot(b.vx, b.vy) || baseSpeed();
const a0 = Math.atan2(b.vy, b.vx);
for (let k=0;k<2 && balls.length < 12;k++){
const a = a0 + (k === 0 ? 0.42 : -0.42);
balls.push({ x:b.x, y:b.y, r:8, vx:Math.cos(a)*s, vy:Math.sin(a)*s, stuck:false, trail:[], spin:0 });
}
}
if (balls.length === 1 && balls[0].stuck) launchBall();
}
}
function loseLife(){
lives--;
combo = 0;
resetPowerups();
bolts = [];
doFlash(0.55, '255,60,90');
doShake(24);
hitStop = 0.09;
sfx.lose();
if (lives <= 0){
endGame(false);
} else {
spawnBall(true);
setState('dying');
}
}
function updatePowerTimers(dt){
if (paddle.wide > 0){
paddle.wide -= dt;
if (paddle.wide <= 0){ paddle.wide = 0; paddle.w = paddle.baseW; }
}
if (paddle.laser > 0){
paddle.laser -= dt;
if (paddle.laser <= 0) paddle.laser = 0;
}
if (slowT > 0){ slowT -= dt; if (slowT <= 0) slowT = 0; }
if (paddle.cool > 0) paddle.cool -= dt;
}
function updatePups(dt){
for (let i=pups.length-1;i>=0;i--){
const p = pups[i];
p.y += p.vy * dt;
p.bob += dt*6;
if (p.y > H + 30){ pups.splice(i,1); continue; }
if (p.y + 10 >= paddle.y && p.y - 10 <= paddle.y + paddle.h &&
Math.abs(p.x - paddle.x) <= paddle.w/2 + 16){
collectPup(p);
pups.splice(i,1);
}
}
}
function updateBolts(dt){
const total = dt * (slowT > 0 ? 0.72 : 1);
for (let i=bolts.length-1;i>=0;i--){
const o = bolts[i];
let rem = o.vy * total, dead = false, guard = 0;
while (rem !== 0 && guard++ < 40){
const s = clamp(rem, -7, 7);
o.y += s;
rem -= s;
if (o.y < -20){ dead = true; break; }
for (const br of bricks){
if (!br.alive || !br.solid) continue;
if (o.x >= br.x && o.x <= br.x+br.w && o.y >= br.y && o.y <= br.y+br.h){
hitBrick(br, null);
dead = true;
break;
}
}
if (dead) break;
}
if (dead) bolts.splice(i,1);
}
}
/* ============================== UPDATE ============================== */
function updateBricksAnim(dt){
for (const b of bricks){
if (b.flash > 0) b.flash = Math.max(0, b.flash - dt*4.5);
if (!b.alive) continue;
b.t += dt;
if (b.t <= b.delay){ b.scale = 0; b.solid = false; }
else {
const p = Math.min(1, (b.t - b.delay) / 0.3);
b.scale = easeOutBack(p);
b.solid = p > 0.62;
}
}
}
function updateFx(dt){
for (let i=parts.length-1;i>=0;i--){
const p = parts[i];
p.life += dt;
if (p.life >= p.max){ parts.splice(i,1); continue; }
p.vy += p.grav * dt;
p.vx *= (1 - 1.6*dt);
p.x += p.vx * dt;
p.y += p.vy * dt;
p.rot += p.vr * dt;
}
for (let i=texts.length-1;i>=0;i--){
const t = texts[i];
t.life += dt;
if (t.life >= t.max){ texts.splice(i,1); continue; }
t.y += t.vy * dt;
t.vy *= (1 - 1.8*dt);
}
for (const s of stars){
s.y += (8 + s.z*46) * dt;
if (s.y > H){ s.y = -4; s.x = Math.random()*W; }
}
paddle.squish = Math.max(0, paddle.squish - dt*5.2);
}
function update(dt){
time += dt;
shake = Math.max(0, shake - dt*44);
flash = Math.max(0, flash - dt*2.6);
updateBricksAnim(dt);
updateFx(dt);
if (hitStop > 0){ hitStop -= dt; return; }
if (actionQueued){ actionQueued = false; handleAction(); }
stateTimer += dt;
switch (state){
case 'menu':
break;
case 'ready':
movePaddle(dt);
stickBalls();
break;
case 'play':
movePaddle(dt);
if (mouseDown && paddle.laser > 0) fireLaser();
updateBalls(dt);
updateBolts(dt);
updatePups(dt);
updatePowerTimers(dt);
if (balls.length === 0) loseLife();
break;
case 'dying':
movePaddle(dt);
if (stateTimer > 0.95){ setState('ready'); }
break;
case 'clear':
movePaddle(dt);
updateBalls(dt);
updateBolts(dt);
updatePups(dt);
updatePowerTimers(dt);
if (stateTimer > 2.35) nextLevel();
break;
default:
break;
}
}
/* ============================== RENDER ============================== */
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();
}
function txt(s, x, y, size, color, align, weight, glow){
ctx.font = (weight||'700') + ' ' + size + 'px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';
ctx.textAlign = align || 'left';
ctx.textBaseline = 'alphabetic';
if (glow){ ctx.shadowColor = color; ctx.shadowBlur = glow; }
ctx.fillStyle = color;
ctx.fillText(s, x, y);
ctx.shadowBlur = 0;
}
function drawBG(){
const g = ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0, '#0a1024');
g.addColorStop(0.55, '#070b18');
g.addColorStop(1, '#04050b');
ctx.fillStyle = g;
ctx.fillRect(0,0,W,H);
ctx.globalCompositeOperation = 'lighter';
const o1x = W*0.28 + Math.sin(time*0.4)*70, o1y = H*0.30 + Math.cos(time*0.31)*50;
const o2x = W*0.76 + Math.cos(time*0.33)*80, o2y = H*0.72 + Math.sin(time*0.27)*46;
let rg = ctx.createRadialGradient(o1x,o1y,0,o1x,o1y,320);
rg.addColorStop(0,'rgba(56,245,255,0.13)'); rg.addColorStop(1,'rgba(56,245,255,0)');
ctx.fillStyle = rg; ctx.fillRect(0,0,W,H);
rg = ctx.createRadialGradient(o2x,o2y,0,o2x,o2y,340);
rg.addColorStop(0,'rgba(255,62,165,0.12)'); rg.addColorStop(1,'rgba(255,62,165,0)');
ctx.fillStyle = rg; ctx.fillRect(0,0,W,H);
ctx.globalCompositeOperation = 'source-over';
ctx.strokeStyle = 'rgba(96,150,255,0.055)';
ctx.lineWidth = 1;
const off = (time*20) % 48;
ctx.beginPath();
for (let x = -48 + off; x <= W; x += 48){ ctx.moveTo(x+.5, 0); ctx.lineTo(x+.5, H); }
for (let y = -48 + off; y <= H; y += 48){ ctx.moveTo(0, y+.5); ctx.lineTo(W, y+.5); }
ctx.stroke();
ctx.fillStyle = '#cfe6ff';
for (const s of stars){
ctx.globalAlpha = 0.14 + s.z*0.5;
ctx.fillRect(s.x, s.y, s.s, s.s);
}
ctx.globalAlpha = 1;
ctx.strokeStyle = 'rgba(56,245,255,0.18)';
ctx.lineWidth = 1;
ctx.strokeRect(FIELD.x - 8.5, 84.5, FIELD.w + 17, H - 84.5 - 16);
}
function drawBricks(){
for (const br of bricks){
if (!br.alive || br.scale <= 0.01) continue;
const cx = br.x + br.w/2, cy = br.y + br.h/2;
ctx.save();
ctx.translate(cx, cy);
ctx.scale(br.scale, br.scale);
ctx.translate(-br.w/2, -br.h/2);
let base;
if (br.kind === 'steel'){
base = ctx.createLinearGradient(0,0,0,br.h);
base.addColorStop(0,'#93a0b6'); base.addColorStop(0.5,'#6a748a'); base.addColorStop(1,'#4b5468');
} else if (br.kind === 'bonus'){
base = ctx.createLinearGradient(0,0,0,br.h);
base.addColorStop(0,'#ffe98a'); base.addColorStop(0.5,'#ffd23e'); base.addColorStop(1,'#e08c14');
} else {
const l = br.kind === 'hard' ? (br.maxHp >= 3 ? 40 : 48) : 56;
base = ctx.createLinearGradient(0,0,0,br.h);
base.addColorStop(0, 'hsl('+br.hue+',92%,'+(l+16)+'%)');
base.addColorStop(0.55, 'hsl('+br.hue+',88%,'+l+'%)');
base.addColorStop(1, 'hsl('+br.hue+',85%,'+(l-14)+'%)');
}
ctx.fillStyle = base;
rr(0, 0, br.w, br.h, 5);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.30)';
rr(3, 2.5, br.w-6, 3.5, 2);
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.34)';
ctx.lineWidth = 1;
rr(0.5, 0.5, br.w-1, br.h-1, 5);
ctx.stroke();
if (br.kind === 'steel'){
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.beginPath(); ctx.arc(5,5,1.8,0,6.29); ctx.arc(br.w-5,5,1.8,0,6.29);
ctx.arc(5,br.h-5,1.8,0,6.29); ctx.arc(br.w-5,br.h-5,1.8,0,6.29); ctx.fill();
ctx.strokeStyle = 'rgba(20,26,40,0.5)';
ctx.beginPath(); ctx.moveTo(6, br.h-6); ctx.lineTo(br.w-6, 6); ctx.stroke();
}
if (br.kind === 'bonus'){
const pulse = 0.5 + 0.5*Math.sin(time*7 + br.col);
ctx.fillStyle = 'rgba(255,255,255,' + (0.35 + pulse*0.5) + ')';
ctx.font = '800 12px ui-monospace, Menlo, monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('$', br.w/2, br.h/2 + 0.5);
}
if (br.kind === 'hard' && br.hp < br.maxHp){
ctx.strokeStyle = 'rgba(10,12,24,0.75)';
ctx.lineWidth = 1.6;
const dmg = br.maxHp - br.hp;
ctx.beginPath();
ctx.moveTo(br.w*0.3, 2); ctx.lineTo(br.w*0.45, br.h*0.55); ctx.lineTo(br.w*0.32, br.h-2);
if (dmg > 1){ ctx.moveTo(br.w*0.68, 2); ctx.lineTo(br.w*0.55, br.h*0.5); ctx.lineTo(br.w*0.7, br.h-2); }
ctx.stroke();
}
if (br.flash > 0){
ctx.globalAlpha = br.flash * 0.85;
ctx.fillStyle = '#fff';
rr(0,0,br.w,br.h,5); ctx.fill();
ctx.globalAlpha = 1;
}
ctx.restore();
}
}
function drawPaddle(){
const w = paddle.w, h = paddle.h;
const sx = 1 + paddle.squish*0.16, sy = 1 - paddle.squish*0.14;
ctx.save();
ctx.translate(paddle.x, paddle.y + h/2);
ctx.scale(sx, sy);
ctx.translate(-w/2, -h/2);
ctx.shadowColor = 'rgba(56,245,255,0.9)';
ctx.shadowBlur = 18;
const g = ctx.createLinearGradient(0,0,0,h);
g.addColorStop(0,'#eafcff'); g.addColorStop(0.4,'#5df3ff'); g.addColorStop(1,'#0aa6c6');
ctx.fillStyle = g;
rr(0,0,w,h,8); ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255,255,255,0.65)';
rr(4, 2.5, w-8, 3, 2); ctx.fill();
ctx.fillStyle = 'rgba(4,20,30,0.5)';
rr(w/2-16, h-4.5, 32, 3, 2); ctx.fill();
if (paddle.laser > 0){
const pulse = 0.5 + 0.5*Math.sin(time*12);
ctx.fillStyle = '#ffd23e';
ctx.shadowColor = '#ffd23e'; ctx.shadowBlur = 10 + pulse*8;
rr(2, -7, 12, 10, 3); ctx.fill();
rr(w-14, -7, 12, 10, 3); ctx.fill();
ctx.shadowBlur = 0;
}
ctx.restore();
if (paddle.wide > 0){
ctx.strokeStyle = 'rgba(77,255,136,' + (0.25 + 0.3*Math.sin(time*8)) + ')';
ctx.lineWidth = 2;
rr(paddle.x - w/2 - 4, paddle.y - 5, w + 8, h + 10, 11);
ctx.stroke();
}
}
function drawBalls(){
for (const b of balls){
ctx.globalCompositeOperation = 'lighter';
for (let i=0;i<b.trail.length;i++){
const p = b.trail[i], a = (i+1)/b.trail.length;
ctx.globalAlpha = a * 0.3;
ctx.fillStyle = slowT > 0 ? '#b06bff' : '#38f5ff';
ctx.beginPath();
ctx.arc(p.x, p.y, b.r * a * 0.9, 0, 6.29);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
ctx.save();
ctx.translate(b.x, b.y);
ctx.shadowColor = slowT > 0 ? '#b06bff' : '#38f5ff';
ctx.shadowBlur = 16;
const g = ctx.createRadialGradient(-2.5,-3,1, 0,0,b.r+1);
g.addColorStop(0,'#ffffff'); g.addColorStop(0.45,'#dffcff');
g.addColorStop(1, slowT > 0 ? '#7b3dff' : '#12b7e8');
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(0,0,b.r,0,6.29); ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.beginPath(); ctx.arc(-2.6,-3,1.9,0,6.29); ctx.fill();
ctx.restore();
}
}
function drawPups(){
for (const p of pups){
const cfg = PUPS[p.type];
const w = 40, h = 20;
const x = p.x - w/2, y = p.y - h/2 + Math.sin(p.bob)*2;
ctx.save();
ctx.shadowColor = cfg.color; ctx.shadowBlur = 16;
const g = ctx.createLinearGradient(0,y,0,y+h);
g.addColorStop(0, '#ffffff'); g.addColorStop(0.35, cfg.color); g.addColorStop(1, 'rgba(0,0,0,0.55)');
ctx.fillStyle = g;
rr(x,y,w,h,10); ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = 'rgba(255,255,255,0.75)'; ctx.lineWidth = 1;
rr(x+.5,y+.5,w-1,h-1,10); ctx.stroke();
ctx.fillStyle = '#06101c';
ctx.font = '800 13px ui-monospace, Menlo, monospace';
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(cfg.label, p.x, y + h/2 + 1);
ctx.restore();
}
}
function drawBolts(){
ctx.globalCompositeOperation = 'lighter';
for (const o of bolts){
ctx.fillStyle = '#ffd23e';
ctx.shadowColor = '#ffd23e'; ctx.shadowBlur = 12;
ctx.fillRect(o.x - 2, o.y, 4, 16);
ctx.fillStyle = '#fff';
ctx.fillRect(o.x - 1, o.y + 2, 2, 12);
}
ctx.shadowBlur = 0;
ctx.globalCompositeOperation = 'source-over';
}
function drawFx(){
ctx.globalCompositeOperation = 'lighter';
for (const p of parts){
const a = 1 - p.life/p.max;
ctx.globalAlpha = Math.max(0, a);
ctx.fillStyle = p.color;
if (p.shape === 'dot'){
ctx.beginPath(); ctx.arc(p.x, p.y, p.size*0.55, 0, 6.29); ctx.fill();
} else {
const s = p.size * (0.4 + a*0.7);
ctx.save();
ctx.translate(p.x, p.y); ctx.rotate(p.rot);
ctx.fillRect(-s/2, -s/2, s, s*(p.shape==='shard'?1.5:1));
ctx.restore();
}
}
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
for (const t of texts){
const a = 1 - t.life/t.max;
ctx.globalAlpha = Math.max(0, Math.min(1, a*1.6));
txt(t.str, t.x, t.y, t.size, t.color, 'center', '800', 12);
}
ctx.globalAlpha = 1;
}
function drawHUD(){
const pad = 44;
txt('SCORE', pad, 30, 11, '#7f92bd', 'left', '700');
txt(String(score).padStart(7,'0'), pad, 54, 24, '#ffffff', 'left', '800', 8);
txt('BEST ' + String(hi).padStart(7,'0'), pad, 74, 11, '#6f82ad', 'left', '700');
txt('LEVEL ' + (level+1) + '/' + LEVELS.length, W - pad, 30, 13, '#9fb2d8', 'right', '800');
txt(LEVEL_NAMES[level] || '', W - pad, 47, 10, '#5f7098', 'right', '700');
// lives as mini paddles
const lx = W - pad;
for (let i=0;i<lives;i++){
const x = lx - 34 - i*40;
ctx.fillStyle = '#38f5ff';
ctx.shadowColor = '#38f5ff'; ctx.shadowBlur = 8;
rr(x - 15, 60, 30, 8, 4); ctx.fill();
ctx.shadowBlur = 0;
}
const mult = comboMult();
if (combo >= 5 && (state === 'play' || state === 'clear')){
const pulse = 1 + Math.sin(time*10)*0.06;
ctx.save();
ctx.translate(W/2, 44); ctx.scale(pulse, pulse);
txt('COMBO x' + mult, 0, 0, 22, '#ffd23e', 'center', '800', 18);
ctx.restore();
txt(combo + ' in a row', W/2, 62, 10, '#c9a63a', 'center', '700');
}
// powerup timers
let bx = pad;
const by = H - 26;
const bars = [];
if (paddle.wide > 0) bars.push(['W', paddle.wide, 14, PUPS.E.color]);
if (paddle.laser > 0) bars.push(['Z', paddle.laser, 12, PUPS.Z.color]);
if (slowT > 0) bars.push(['S', slowT, 10, PUPS.S.color]);
for (const b of bars){
txt(b[0], bx, by + 8, 12, b[3], 'left', '800', 8);
const w = 62, p = clamp(b[1]/b[2], 0, 1);
ctx.fillStyle = 'rgba(255,255,255,0.14)';
rr(bx + 14, by, w, 9, 4); ctx.fill();
ctx.fillStyle = b[3];
ctx.shadowColor = b[3]; ctx.shadowBlur = 8;
rr(bx + 14, by, Math.max(4, w*p), 9, 4); ctx.fill();
ctx.shadowBlur = 0;
bx += 92;
}
if (slowT > 0){
txt('SLOW', W - pad, H - 18, 12, '#b06bff', 'right', '800', 10);
}
if (state === 'ready'){
const blink = 0.55 + 0.45*Math.sin(time*5);
ctx.globalAlpha = blink;
txt(isMobile ? 'TAP TO LAUNCH' : 'SPACE / CLICK TO LAUNCH', W/2, H - 78, 15, '#9ff7ff', 'center', '800', 14);
ctx.globalAlpha = 1;
}
}
function render(){
ctx.setTransform(1,0,0,1,0,0);
drawBG();
const sx = shake > 0 ? rand(-shake, shake) : 0;
const sy = shake > 0 ? rand(-shake, shake) : 0;
ctx.save();
ctx.translate(sx, sy);
drawBricks();
drawPups();
drawBolts();
drawBalls();
drawPaddle();
drawFx();
ctx.restore();
drawHUD();
if (flash > 0){
ctx.fillStyle = 'rgba(' + flashColor + ',' + (flash * 0.7) + ')';
ctx.fillRect(0,0,W,H);
}
if (state === 'paused'){
ctx.fillStyle = 'rgba(4,6,14,0.35)';
ctx.fillRect(0,0,W,H);
}
}
/* ============================== LOOP ============================== */
let last = performance.now();
function frame(t){
let dt = (t - last) / 1000;
last = t;
if (dt > 0.05) dt = 0.05;
if (dt < 0) dt = 0;
update(dt);
render();
requestAnimationFrame(frame);
}
// boot
loadLevel(0);
spawnBall(true);
setState('menu');
document.getElementById('ov').classList.remove('hidden');
requestAnimationFrame(frame);
</script>
</body>
</html>Compare with
MiMo v2.6 Flash: other attempts
- MiMo v2.6 Flash, Watch: No file after 2 runs, used all 32,000 output tokens


- About · 144 words