hypeorcode
Network offOpen alone (new tab)

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)

Raw files: output · meta.json

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">&larr;</span> <span class="key">&rarr;</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> &middot; <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 &middot; combos &middot; screen shake &middot; 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">&#9733; NEW HIGH SCORE &#9733;</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