, meta viewport (width=device-width, initial-scale=1, user-scalable=no) - style - header: h1 BREAKOUT, tagline - .cabinet > canvas#game - footer: hints + sound button - script JS: ```js const W=400,H=600; const COLS=8, ROWS=4, BW=50, BH=20; const PADDLE_W=60,PADDLE_H=10,PADDLE_Y=580; const BALL_R=3; let canvas, ctx, dpr; const state = { ... } // 'ready','play','paused','over','win' ``` Bricks: array of {x,y,w,h,alive, x=col*50, y=row*20}. Wait, top-aligned — y = row*20 starting 0? Bricks touch the top edge. Spec: "top-aligned" "each brick is at row*20 + col*50." So yes, y=row*20, x=col*50. Top edge of brick row 0 is y=0. But HUD score is top-left — will overlap bricks! Score at top-left of brick field... bricks fill 400×80 at the top. HUD at top-left would cover bricks. Hmm. Options: draw HUD above bricks with semi-transparent backing (score legible, bricks slightly hidden behind). Or offset bricks below HUD? Spec explicitly says top-aligned y=row*20 and score at top-left. So draw score text over the bricks with a subtle dark backing for readability. That's per spec — fine. Actually let me reconsider: score at top-left (8,16) would sit on row 0 of bricks. Draw a semi-transparent black rounded rect behind score. Acceptable and spec-compliant. Alternatively add a top margin... no, follow spec exactly: bricks at row*20. Game object: ```js function resetGame(){ bricks = buildBricks(); score=0; lives=3; resetBall(); state='ready'; particles.length=0; popups... } function resetBall(){ ball={x:paddle.x+PADDLE_W/2, y:570, dx:2, dy:-2.8, r:3, trail:[]} } ``` Ball ready: follows paddle x each frame until launched. On launch: state='play', ball at (paddle.x+30, 570), dx=2, dy=-2.8. Update (dt factor f): - Paddle: keys (left/right or A/D) → paddle.x ±= 7*f, clamp [0,340]. Mouse target lerp. - Ball (if play): - x += dx*f; y += dy*f (or split substep if f>1? dt clamp ≤ 3 frames; tunneling risk low at 3.5px/step; max step ~10px, brick 20px tall — no tunneling through bricks (min dimension 20); wall: reflect at boundary anyway; paddle 10 tall — ball 3.4*3=10.3 could skip paddle if dt spike... dt clamp to 33ms → f≤2 → step ≤7px < paddle 10+radius. OK, clamp dt to 33ms.) - Walls - Paddle (only if dy>0) - Bricks: find nearest alive brick with circle-AABB overlap; if found: brick.alive=false, score+=10, particles, popup, reflect by min penetration axis, bricksLeft-- - Bottom → lose life - Update particles, popups, trail. Circle-AABB overlap: ```js function hitBrick(b){ const cx = clamp(ball.x, b.x, b.x+b.w); const cy = clamp(ball.y, b.y, b.y+b.h); const dx = ball.x-cx, dy = ball.y-cy; return dx*dx+dy*dy <= ball.r*ball.r; } ``` Nearest: min squared distance between ball center and rect (0 if inside). Process first, break. Reflect: ```js const overlapX = (b.w/2 + ball.r) - Math.abs(ball.x - (b.x+b.w/2)); const overlapY = (b.h/2 + ball.r) - Math.abs(ball.y - (b.y+b.h/2)); if (overlapX < overlapY) { ball.dx = ball.x < b.x+b.w/2 ? -Math.abs(ball.dx) : Math.abs(ball.dx); } else { ball.dy = ball.y < b.y+b.h/2 ? -Math.abs(ball.dy) : Math.abs(ball.dy); } ``` Wait: if ball center is left of brick center → hit left side → dx should be negative (moving away). Yes as above. Edge: ball exactly inside center — either fine. But there's a subtlety: min-overlap axis works for circles too. Good. Paddle collision: ```js if (ball.dy > 0 && ball.y + ball.r >= PADDLE_Y && ball.y - ball.r <= PADDLE_Y+PADDLE_H && ball.x >= paddle.x - ball.r && ball.x <= paddle.x + PADDLE_W + ball.r && ball.y < PADDLE_Y + PADDLE_H) ``` Simplify: ball bottom reaches paddle top while descending: ball.y + r >= PADDLE_Y && prevBallY + r <= PADDLE_Y? Use prev position to avoid double-hit when inside. Since we set dy<0 on hit, and require dy>0, double hit within same frame can't happen. When ball is overlapping paddle from side (edge hit) — spec: "paddle boundary: ball can bounce off edges (paddle extends full width)" — meaning the ball can bounce off the side edge of the paddle. Hmm: "paddle extends full width" is confusing. I think it means the ball can hit paddle edges (corners) and bounce properly, not pass through. My AABB with radius handles corners: if ball is below paddle top but x slightly outside → hits side → flip dx. Let me handle: compute overlap; if ball center is below paddle top and hits from side → flip dx; from above → upward angle bounce. Implementation: ```js function collidePaddle(){ if (ball.x < paddle.x - r || ball.x > paddle.x + PADDLE_W + r) return; if (ball.y + r < PADDLE_Y || ball.y - r > PADDLE_Y + PADDLE_H) return; // circle vs rect closest point const cx = clamp(ball.x, paddle.x, paddle.x+PADDLE_W); const cy = clamp(ball.y, PADDLE_Y, PADDLE_Y+PADDLE_H); if ((ball.x-cx)**2+(ball.y-cy)**2 <= r*r){ // determine face const fromTop = ball.y < PADDLE_Y + PADDLE_H/2; if (ball.dy > 0 && fromTop){ // normal bounce with angle const rel = clamp((ball.x - (paddle.x+PADDLE_W/2)) / (PADDLE_W/2), -1, 1); const sp = Math.hypot(ball.dx, ball.dy); const ang = rel * (Math.PI*0.35); // max 63° ball.dx = sp*Math.sin(ang); ball.dy = -sp*Math.cos(ang); ball.y = PADDLE_Y - r - 0.1? // push out } else if (ball.dy < 0 && !fromTop) { /* hitting from below? rare */ ball.dy = Math.abs(ball.dy); } else