Run details
- Model
- poolside/laguna-s-2.1-free
- Route
- Command Code CLI
- Reasoning
- provider default
- Limits
- 60 min
- Date
- 2 Oct 2026
- Time
- 30 min 5 s
- Size
- 28 kB
- Tokens
- 2,738,780 in, 93,207 out
- List price
- $0.26 ($0.09 in / $0.18 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
28 kB 958 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout</title>
<style>
html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#000;font-family:monospace;font-weight:bold}
canvas{display:block;width:100vw;height:100vh}
</style>
</head>
<body>
<canvas id="g"></canvas>
<script>
(() => {
'use strict';
const canvas = document.getElementById('g');
const ctx = canvas.getContext('2d');
let W, H;
const dpr = window.devicePixelRatio || 1;
function resize(){
W = window.innerWidth; H = window.innerHeight;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
updateConfig();
generateStars();
}
window.addEventListener('resize', resize);
resize();
const CONFIG = {};
function updateConfig(){
CONFIG.paddleW = Math.max(80, W * 0.13);
CONFIG.paddleH = Math.max(12, H * 0.02);
CONFIG.paddleY = H - 70;
CONFIG.paddleSpeed = W * 9;
CONFIG.ballR = Math.max(8, W * 0.011);
CONFIG.ballSpeed = W * 3.2;
CONFIG.ballSpeedCap = W * 6;
CONFIG.ballSpeedInc = 1.06;
CONFIG.brickGap = W * 0.013;
CONFIG.maxTrail = 12;
CONFIG.lives = 3;
CONFIG.maxParticles = 400;
CONFIG.maxTexts = 120;
CONFIG.comboTime = 2000;
}
const rand = (a, b) => a + Math.random() * (b - a);
const lerp = (a, b, t) => a + (b - a) * t;
function hexToRgba(h, a){
if (h.startsWith('#')){
if (h.length === 4) h = '#' + h[1]+h[1] + h[2]+h[2] + h[3]+h[3];
const r = parseInt(h.slice(1,3),16), g = parseInt(h.slice(3,5),16), b = parseInt(h.slice(5,7),16);
return `rgba(${r},${g},${b},${a})`;
}
return h;
}
function adjustBrightness(hex, p){
if (!hex || !hex.startsWith('#')) return hex;
if (hex.length === 4) hex = '#' + hex[1]+hex[1] + hex[2]+hex[2] + hex[3]+hex[3];
const r = Math.max(0, Math.min(255, parseInt(hex.slice(1,3),16) + p));
const g = Math.max(0, Math.min(255, parseInt(hex.slice(3,5),16) + p));
const b = Math.max(0, Math.min(255, parseInt(hex.slice(5,7),16) + p));
return `rgb(${r},${g},${b})`;
}
function roundedRect(x, y, w, h, r, fill, stroke){
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
if (fill) ctx.fill();
if (stroke) ctx.stroke();
}
// ===== AUDIO =====
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let audioCtx = null;
function ensureAudio(){
if (!audioCtx && AudioCtx) audioCtx = new AudioCtx();
}
function playSound(freq, dur, type='square', vol=0.1, decay=true, slide=null){
ensureAudio();
if (!audioCtx) return;
const t = audioCtx;
const osc = t.createOscillator();
const gain = t.createGain();
osc.connect(gain); gain.connect(t.destination);
osc.type = type;
if (slide){
osc.frequency.setValueAtTime(slide[0], t.currentTime);
osc.frequency.exponentialRampToValueAtTime(slide[1], t.currentTime + dur);
} else {
osc.frequency.setValueAtTime(freq, t.currentTime);
}
gain.gain.setValueAtTime(vol, t.currentTime);
if (decay){
gain.gain.exponentialRampToValueAtTime(0.001, t.currentTime + dur);
} else {
gain.gain.setValueAtTime(vol * 0.3, t.currentTime + dur * 0.7);
gain.gain.exponentialRampToValueAtTime(0.001, t.currentTime + dur);
}
osc.start(t.currentTime);
osc.stop(t.currentTime + dur);
}
function playChord(freqs, dur, type='sine', vol=0.1){
freqs.forEach(f => playSound(f, dur, type, vol * 0.7));
}
function playNoise(dur, vol=0.15){
ensureAudio();
if (!audioCtx) return;
const t = audioCtx;
const buffer = t.createBuffer(1, t.sampleRate * dur, t.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i++){
data[i] = (Math.random() * 2 - 1) * (1 - i / data.length);
}
const src = t.createBufferSource();
const gain = t.createGain();
src.buffer = buffer; src.connect(gain); gain.connect(t.destination);
gain.gain.setValueAtTime(vol, t.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, t.currentTime + dur);
src.start(t.currentTime);
src.stop(t.currentTime + dur);
}
// ===== INPUT =====
let mouseX=0, mouseY=0;
let mouseMoved=false;
let lastMouseTime=0;
const keys = {};
canvas.addEventListener('mousemove', e=>{
const r = canvas.getBoundingClientRect();
mouseX = (e.clientX - r.left) * (canvas.width / r.width);
mouseY = (e.clientY - r.top) * (canvas.height / r.height);
mouseMoved = true;
lastMouseTime = Date.now();
});
canvas.addEventListener('mousedown', ()=>{
lastMouseTime = Date.now();
if (gameState === 'playing' && !ball.launched){
launchBall();
} else {
handleInteraction();
}
});
canvas.addEventListener('touchstart', e=>{
e.preventDefault();
lastMouseTime = Date.now();
if (gameState === 'playing' && !ball.launched){
launchBall();
} else {
handleInteraction();
}
});
window.addEventListener('keydown', e=>{
if (e.code.startsWith('Key') || e.code.startsWith('Arrow')) keys[e.code] = true;
if (e.code === 'Space'){
e.preventDefault();
if (gameState === 'playing' && !ball.launched){
launchBall();
} else {
handleInteraction();
}
}
});
window.addEventListener('keyup', e=>{ keys[e.code] = false; });
window.addEventListener('blur', ()=>{ keys = {}; });
// ===== STARS =====
let stars = [];
class Star {
constructor(){ this.reset(); }
reset(){
this.x = Math.random() * W;
this.y = Math.random() * H;
this.s = Math.random() * 1.5 + 0.5;
this.b = 0.3 + Math.random() * 0.7;
this.t = Math.random() * Math.PI * 2;
}
update(dt){ this.t += dt * 2; }
draw(ctx){
const a = this.b * (0.7 + 0.3 * Math.sin(this.t));
ctx.fillStyle = hexToRgba('#fff', a);
ctx.fillRect(this.x, this.y, this.s, this.s);
}
}
function generateStars(){
stars = [];
const n = Math.floor(W * H / 6000);
for (let i = 0; i < n; i++) stars.push(new Star());
}
// ===== PARTICLES =====
let particles = [];
class Particle{
constructor(x, y, c){
this.x = x; this.y = y;
this.vx = (Math.random() - 0.5) * W * 0.02;
this.vy = (Math.random() - 0.5) * W * 0.02;
this.life = 0.4 + Math.random() * 0.4;
this.max = this.life;
this.c = c;
this.s = Math.random() * 4 + 2;
this.a = Math.random() * Math.PI * 2;
}
update(dt){
this.life -= dt;
if (this.life <= 0) return false;
this.vy += H * 0.7 * dt;
this.vx *= (0.97 - dt * 30);
this.x += this.vx; this.y += this.vy;
this.s *= 0.97;
this.a += dt * 3;
return true;
}
draw(ctx){
const a = this.life / this.max;
ctx.fillStyle = hexToRgba(this.c, a * 0.8);
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.a);
ctx.fillRect(-this.s / 2, -this.s / 2, this.s, this.s);
ctx.restore();
}
}
function spawnBreakParticles(x, y, c){
for (let i = 0; i < 18; i++) particles.push(new Particle(x, y, c));
playSound(rand(200, 300), 0.15, 'square', 0.08);
}
function spawnHitParticles(x, y, c){
for (let i = 0; i < 8; i++) particles.push(new Particle(x, y, c));
}
function spawnPaddleParticles(x, y){
for (let i = 0; i < 10; i++) particles.push(new Particle(x, y, '#4fc3f7'));
playSound(180, 0.12, 'triangle', 0.08);
}
// ===== FLOATING TEXT =====
let texts = [];
class FloatingText{
constructor(text, x, y, c, sz=16){
this.text = text; this.x = x; this.y = y;
this.vx = (Math.random() - 0.5) * 80;
this.vy = -200 - Math.random() * 100;
this.life = 0.8 + Math.random() * 0.4;
this.c = c;
this.sz = sz + Math.random() * 6;
this.scale = 0.8 + Math.random() * 0.4;
}
update(dt){
this.life -= dt;
if (this.life <= 0) return false;
this.vy += H * 0.3 * dt;
this.x += this.vx * dt;
this.y += this.vy * dt;
this.scale *= 0.97;
return true;
}
draw(ctx){
const a = Math.max(0, this.life / 0.8);
ctx.fillStyle = hexToRgba(this.c, a);
ctx.font = `${this.sz * this.scale}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.text, this.x, this.y);
}
}
function floatText(text, x, y, c, sz){
texts.push(new FloatingText(text, x, y, c, sz));
}
// ===== PADDLE =====
class Paddle{
constructor(){ this.reset(); }
reset(){
this.x = W / 2; this.y = CONFIG.paddleY;
this.w = CONFIG.paddleW; this.h = CONFIG.paddleH;
this.color = '#4fc3f7';
this.scaleX = 1; this.scaleY = 1;
this.glow = 0;
}
update(dt){
this.w = CONFIG.paddleW; this.h = CONFIG.paddleH; this.y = CONFIG.paddleY;
const mouseActive = (Date.now() - lastMouseTime) < 100;
let dir = 0;
if (keys['ArrowLeft']||keys['KeyA']) dir -= 1;
if (keys['ArrowRight']||keys['KeyD']) dir += 1;
if (mouseActive && mouseMoved){
const targetX = mouseX - this.w / 2;
this.x = lerp(this.x, targetX, 0.25);
} else if (dir){
this.x += dir * CONFIG.paddleSpeed * dt;
}
this.x = Math.max(5, Math.min(W - this.w - 5, this.x));
this.scaleX += (1 - this.scaleX) * 0.25;
this.scaleY += (1 - this.scaleY) * 0.25;
if (this.glow > 0) this.glow -= dt * 8;
}
hit(){
this.scaleX = 1.25; this.scaleY = 0.65;
this.glow = 1;
}
draw(ctx){
ctx.save();
ctx.translate(this.x + this.w / 2, this.y + this.h / 2);
ctx.scale(this.scaleX, this.scaleY);
const grad = ctx.createRadialGradient(0, 0, this.w * 0.2, 0, 0, this.w * 0.5);
if (this.glow > 0){
grad.addColorStop(0, '#81f0fc');
grad.addColorStop(0.5, '#4fc3f7');
grad.addColorStop(1, '#1e88e5');
} else {
grad.addColorStop(0, adjustBrightness(this.color, -10));
grad.addColorStop(1, this.color);
}
roundedRect(-this.w/2, -this.h/2, this.w, this.h, Math.max(3, this.h*0.3), true, false);
ctx.fillStyle = grad;
ctx.fill();
if (this.glow > 0){
ctx.shadowColor = '#81f0fc';
ctx.shadowBlur = 20 * this.glow;
ctx.fill();
ctx.shadowBlur = 0;
}
ctx.restore();
}
}
// ===== BALL =====
class Ball{
constructor(){ this.reset(); }
reset(){
this.r = CONFIG.ballR;
this.x = paddle.x + paddle.w / 2;
this.y = CONFIG.paddleY - this.r - 2;
this.vx = 0; this.vy = 0;
this.launched = false;
this.trail = [];
}
launch(){
if (this.launched) return;
ensureAudio();
const angle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI / 3;
this.vx = Math.cos(angle) * CONFIG.ballSpeed;
this.vy = Math.sin(angle) * CONFIG.ballSpeed;
this.launched = true;
this.trail = [];
playSound(400, 0.2, 'sine', 0.15, false);
playSound(600, 0.2, 'sine', 0.15, false);
}
update(dt){
this.r = CONFIG.ballR;
if (!this.launched){
this.x = paddle.x + paddle.w / 2;
this.y = CONFIG.paddleY - this.r - Math.sin(performance.now() * 0.008) * 3;
this.trail = [];
return;
}
this.x += this.vx * dt;
this.y += this.vy * dt;
// trail
this.trail.push({x: this.x, y: this.y, t: Date.now()});
if (this.trail.length > CONFIG.maxTrail) this.trail.shift();
// walls
if (this.x - this.r < 0){
this.x = this.r; this.vx = Math.abs(this.vx);
playSound(220, 0.1, 'square', 0.06);
spawnHitParticles(this.x, this.y, '#4fc3f7');
}
if (this.x + this.r > W){
this.x = W - this.r; this.vx = -Math.abs(this.vx);
playSound(220, 0.1, 'square', 0.06);
spawnHitParticles(this.x, this.y, '#4fc3f7');
}
if (this.y - this.r < 0){
this.y = this.r; this.vy = Math.abs(this.vy);
playSound(330, 0.1, 'sawtooth', 0.06);
spawnHitParticles(this.x, this.y, '#4fc3f7');
}
// paddle
if (this.vy > 0 &&
this.x + this.r > paddle.x &&
this.x - this.r < paddle.x + paddle.w &&
this.y + this.r > paddle.y &&
this.y - this.r < paddle.y + paddle.h){
const rel = (this.x - paddle.x - paddle.w / 2) / (paddle.w / 2);
const maxBounce = Math.PI / 3.2;
const angle = rel * maxBounce;
const speed = Math.min(CONFIG.ballSpeedCap,
Math.sqrt(this.vx * this.vx + this.vy * this.vy) * CONFIG.ballSpeedInc);
this.vx = Math.sin(angle) * speed;
this.vy = -Math.cos(angle) * speed;
this.y = paddle.y - this.r;
spawnPaddleParticles(this.x, this.y);
paddle.hit();
this.trail = [];
this.trail.push({x: this.x, y: this.y, t: Date.now()});
}
// bricks
for (let i = bricks.length - 1; i >= 0; i--){
const b = bricks[i];
if (b.destroyed) continue;
const closestX = Math.max(b.x, Math.min(this.x, b.x + b.w));
const closestY = Math.max(b.y, Math.min(this.y, b.y + b.h));
const dx = this.x - closestX;
const dy = this.y - closestY;
const distSq = dx * dx + dy * dy;
if (distSq < this.r * this.r){
const dist = Math.sqrt(distSq);
if (dist > 0){
const nx = dx / dist, ny = dy / dist;
this.x += nx * (this.r - dist + 0.5);
this.y += ny * (this.r - dist + 0.5);
const dot = this.vx * nx + this.vy * ny;
if (dot < 0){
this.vx -= 2 * dot * nx;
this.vy -= 2 * dot * ny;
}
} else {
this.vy = -Math.abs(this.vy);
}
b.hit();
spawnBreakParticles(b.x + b.w / 2, b.y + b.h / 2, b.color);
if (b.destroyed){
addScore(b.points, this.x, this.y, b.color);
addToCombo(this.x, this.y);
bricks.splice(i, 1);
playSound(880, 0.1, 'square', 0.1, true, [880, 220]);
} else {
playSound(440, 0.08, 'square', 0.06, true, [440, 220]);
}
break;
}
}
// bottom (lose life)
if (this.y - this.r > H + 50){
this.launched = false;
ballLost = true;
}
}
draw(ctx){
const now = Date.now();
// trail
for (let i = 0; i < this.trail.length; i++){
const p = this.trail[i];
const age = (now - p.t) / 1000;
if (age > 0.3) continue;
const a = (1 - age / 0.3) * 0.5;
const s = this.r * (i / this.trail.length) * 0.7;
ctx.beginPath();
ctx.arc(p.x, p.y, s, 0, Math.PI * 2);
ctx.fillStyle = hexToRgba('#fff', a);
ctx.fill();
}
// ball
const grad = ctx.createRadialGradient(
this.x - this.r / 3, this.y - this.r / 3, 1,
this.x, this.y, this.r
);
grad.addColorStop(0, '#fff');
grad.addColorStop(0.4, '#81f0fc');
grad.addColorStop(1, '#1e88e5');
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
// highlight
ctx.beginPath();
ctx.arc(this.x - this.r / 4, this.y - this.r / 4, this.r * 0.3, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.fill();
}
}
// ===== BRICK =====
const BRICK_DATA = {
r: {hp:1, color:'#e74c3c', points:10},
o: {hp:2, color:'#f39c12', points:20},
y: {hp:3, color:'#f1c40f', points:30},
g: {hp:4, color:'#2ecc71', points:40},
};
class Brick{
constructor(x, y, w, h, type){
this.x=x; this.y=y; this.w=w; this.h=h;
this.type=type;
const d = BRICK_DATA[type];
this.maxHp = d.hp;
this.hp = d.hp;
this.color = d.color;
this.points = d.points * d.hp;
this.destroyed = false;
this.glow = 0;
}
hit(){
this.hp--;
this.glow = 0.8;
if (this.hp <= 0) this.destroyed = true;
}
update(dt){
if (this.glow > 0) this.glow -= dt * 10;
}
draw(ctx){
if (this.destroyed) return;
ctx.save();
if (this.glow > 0){
ctx.shadowColor = hexToRgba(this.color, 0.8);
ctx.shadowBlur = 30 * this.glow;
}
const damage = (this.maxHp - this.hp) / this.maxHp;
const c = adjustBrightness(this.color, -damage * 30);
const grad = ctx.createLinearGradient(this.x, this.y, this.x, this.y + this.h);
grad.addColorStop(0, adjustBrightness(c, 40));
grad.addColorStop(1, c);
const r = Math.min(6, this.h * 0.25);
roundedRect(this.x, this.y, this.w, this.h, r, true, false);
ctx.fillStyle = grad;
ctx.fill();
// cracks for damaged bricks
if (damage > 0){
ctx.strokeStyle = hexToRgba('#000', 0.4 + damage * 0.3);
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(this.x + this.w * 0.15, this.y + this.h * 0.15);
ctx.lineTo(this.x + this.w * 0.85, this.y + this.h * 0.85);
ctx.stroke();
}
// hp indicator
if (this.maxHp > 1){
ctx.fillStyle = '#fff';
ctx.font = `${Math.min(this.h * 0.45, 14)}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.hp, this.x + this.w / 2, this.y + this.h / 2);
}
ctx.restore();
}
}
// ===== LEVELS =====
const LEVELS = [
{
name: "First Strike",
color: '#e74c3c',
layout: [
"rrrrrrrr",
"rrrrrrrr",
"rrrrrrrr",
"rrrrrrrr",
"rrrrrrrr",
]
},
{
name: "Layered Defense",
color: '#f39c12',
layout: [
"oooooooo",
"oooooooo",
"oooooooo",
"rrrrrrrr",
"rrrrrrrr",
"rrrrrrrr",
]
},
{
name: "The Summit",
color: '#f1c40f',
layout: [
"yyyyyyyyyy",
"oooooooooo",
"oooooooooo",
"rrrrrrrrrr",
"rrrrrrrrrr",
]
}
];
// ===== GAME STATE =====
let gameState = 'menu';
let score = 0;
let lives = CONFIG.lives;
let levelIndex = 0;
let combo = 0;
let comboTimer = 0;
let ballLost = false;
let screenShake = 0;
let flashAlpha = 0;
let time = 0;
let bricks = [];
let paddle, ball;
let highScore = 0;
// ===== GAME FUNCTIONS =====
function resetLevel(){
bricks = [];
const level = LEVELS[levelIndex];
const layout = level.layout;
const rows = layout.length;
const cols = layout[0].length;
const padding = W * 0.04;
const totalW = W - padding * 2 - (cols - 1) * CONFIG.brickGap;
const brickW = totalW / cols;
const brickH = Math.max(20, H * 0.035);
const offsetY = H * 0.12;
for (let row = 0; row < rows; row++){
for (let col = 0; col < cols; col++){
const t = layout[row][col];
if (!t || t === ' ') continue;
const x = padding + col * (brickW + CONFIG.brickGap);
const y = offsetY + row * (brickH + CONFIG.brickGap);
bricks.push(new Brick(x, y, brickW, brickH, t));
}
}
}
function launchBall(){
if (gameState !== 'playing' || ball.launched) return;
ball.reset();
ball.launch();
spawnLaunchEffect();
}
function spawnLaunchEffect(){
for (let i = 0; i < 20; i++){
const angle = (i / 20) * Math.PI * 2;
const p = new Particle(paddle.x + paddle.w / 2, paddle.y, '#fff');
p.vx = Math.cos(angle) * W * 0.03;
p.vy = Math.sin(angle) * W * 0.03;
p.s = 3 + Math.random() * 3;
p.life = 0.5 + Math.random() * 0.3;
particles.push(p);
}
}
function addScore(points, x, y, color){
const bonus = combo >= 3 ? combo * 10 : 0;
score += points + bonus;
if (combo >= 3){
floatText(`+${points + bonus} COMBO x${combo}!`, x, y, '#ffeb3b', 18);
} else {
floatText(`+${points}`, x, y, color || '#fff', 18);
}
}
function addToCombo(x, y){
combo++;
comboTimer = CONFIG.comboTime;
if (combo >= 3){
floatText(`COMBO x${combo}!`, x, y - 30, '#ffeb3b', 22);
playChord([261, 329, 392, 523].slice(0, Math.min(combo, 4)), 0.2, 'sawtooth', 0.1);
}
}
function startGame(){
score = 0;
lives = CONFIG.lives;
levelIndex = 0;
combo = 0;
comboTimer = 0;
gameState = 'playing';
paddle = new Paddle();
ball = new Ball();
ballLost = false;
screenShake = 0;
flashAlpha = 0;
resetLevel();
setTimeout(() => launchBall(), 300);
}
function nextLevel(){
if (levelIndex < LEVELS.length - 1){
levelIndex++;
combo = 0;
comboTimer = 0;
resetLevel();
gameState = 'playing';
ball.reset();
setTimeout(() => launchBall(), 500);
} else {
gameState = 'victory';
saveHighScore();
}
}
function saveHighScore(){
try {
const prev = parseInt(localStorage.getItem('breakout-hs') || '0');
if (score > prev) localStorage.setItem('breakout-hs', score.toString());
} catch(e){}
}
function loadHighScore(){
try {
highScore = parseInt(localStorage.getItem('breakout-hs') || '0');
} catch(e){}
}
function gameOver(){
gameState = 'gameOver';
saveHighScore();
}
function applyScreenShake(intensity){
screenShake = intensity;
flashAlpha = 0.3;
}
function handleInteraction(){
ensureAudio();
if (gameState === 'menu'){
startGame();
} else if (gameState === 'levelComplete'){
nextLevel();
} else if (gameState === 'gameOver' || gameState === 'victory'){
startGame();
}
}
// ===== UPDATE =====
function update(dt){
time += dt;
// combo timer
if (comboTimer > 0){
comboTimer -= dt * 1000;
if (comboTimer <= 0) combo = 0;
}
// screen shake
if (screenShake > 0) screenShake -= dt * 20;
// flash
flashAlpha *= (1 - dt * 3);
// stars
for (const s of stars) s.update(dt);
// particles
for (let i = particles.length - 1; i >= 0; i--){
if (!particles[i].update(dt)) particles.splice(i, 1);
}
if (particles.length > CONFIG.maxParticles){
particles.splice(0, particles.length - CONFIG.maxParticles);
}
// texts
for (let i = texts.length - 1; i >= 0; i--){
if (!texts[i].update(dt)) texts.splice(i, 1);
}
// bricks
for (const b of bricks) b.update(dt);
if (gameState === 'playing') updatePlaying(dt);
}
function updatePlaying(dt){
if (ballLost){
ballLost = false;
lives--;
if (lives <= 0){
gameOver();
playChord([523, 392, 330, 261], 1, 'sine', 0.15);
playNoise(0.5, 0.2);
return;
}
ball.reset();
setTimeout(() => launchBall(), 300);
}
paddle.update(dt);
ball.update(dt);
if (bricks.length === 0){
gameState = 'levelComplete';
applyScreenShake(15);
playChord([329, 392, 523, 659], 0.5, 'sine', 0.15);
playNoise(0.3, 0.2);
addScore(1000, W/2, H/2, '#fff');
}
}
// ===== DRAW =====
function draw(){
// Background
const bgGrad = ctx.createLinearGradient(0, 0, 0, H);
bgGrad.addColorStop(0, '#0a0f20');
bgGrad.addColorStop(0.5, '#080c18');
bgGrad.addColorStop(1, '#050814');
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, W, H);
// Stars
for (const s of stars) s.draw(ctx);
// Vignette
const vg = ctx.createRadialGradient(W/2, H/2, 0, W/2, H/2, W);
vg.addColorStop(0, 'rgba(0,0,0,0)');
vg.addColorStop(1, 'rgba(0,0,0,0.5)');
ctx.fillStyle = vg;
ctx.fillRect(0, 0, W, H);
// Screen shake
if (screenShake > 0){
ctx.save();
ctx.translate((Math.random() - 0.5) * screenShake, (Math.random() - 0.5) * screenShake);
}
// Game objects (shaken)
for (const b of bricks) b.draw(ctx);
for (const p of particles) p.draw(ctx);
paddle.draw(ctx);
ball.draw(ctx);
for (const t of texts) t.draw(ctx);
if (screenShake > 0) ctx.restore();
// Flash overlay (not shaken)
if (flashAlpha > 0.01){
ctx.fillStyle = hexToRgba('#fff', flashAlpha);
ctx.fillRect(0, 0, W, H);
}
// UI
drawUI();
// State screens
if (gameState === 'menu') drawMenu();
else if (gameState === 'levelComplete') drawLevelComplete();
else if (gameState === 'gameOver') drawGameOver();
else if (gameState === 'victory') drawVictory();
// Reset shadow/text
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
}
function drawUI(){
ctx.fillStyle = '#fff';
ctx.font = `bold ${Math.min(W * 0.024, 22)}px monospace`;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText(`SCORE: ${score}`, 20, 15);
if (highScore > 0 && gameState !== 'menu'){
ctx.font = `bold ${Math.min(W * 0.016, 16)}px monospace`;
ctx.fillStyle = '#ffeb3b';
ctx.fillText(`BEST: ${highScore}`, 20, 40);
}
// Lives
ctx.fillStyle = '#fff';
ctx.font = `bold ${Math.min(W * 0.018, 18)}px monospace`;
ctx.textAlign = 'left';
ctx.fillText('LIVES:', 20, H - 30);
for (let i = 0; i < lives; i++){
ctx.fillStyle = '#e74c3c';
ctx.beginPath();
ctx.arc(80 + i * 22, H - 24, 8, 0, Math.PI * 2);
ctx.fill();
}
// Level
ctx.fillStyle = '#fff';
ctx.font = `bold ${Math.min(W * 0.022, 20)}px monospace`;
ctx.textAlign = 'right';
ctx.fillText(`LEVEL ${levelIndex + 1} / ${LEVELS.length}`, W - 20, 15);
ctx.fillStyle = LEVELS[levelIndex].color;
ctx.fillText(LEVELS[levelIndex].name.toUpperCase(), W - 20, 40);
// Combo
if (combo >= 3){
ctx.fillStyle = hexToRgba('#ffeb3b', 0.9);
ctx.font = `bold ${Math.min(W * 0.06, 64)}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const pulse = Math.sin(time * 8) * 4;
ctx.shadowColor = '#ffeb3b';
ctx.shadowBlur = 25 + pulse;
ctx.fillText(`x${combo}`, W / 2, H / 2 - 60);
ctx.shadowBlur = 0;
ctx.font = `bold ${Math.min(W * 0.025, 24)}px monospace`;
ctx.fillText(`COMBO`, W / 2, H / 2 - 30);
}
// Ready prompt
if (gameState === 'playing' && !ball.launched){
ctx.fillStyle = hexToRgba('#4fc3f7', 0.8 + Math.sin(time * 5) * 0.2);
ctx.font = `bold ${Math.min(W * 0.028, 26)}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('READY', W / 2, CONFIG.paddleY - 60);
}
}
function drawCenteredText(text, y, size, color='#fff', subText=null, subSize=null, subColor='#aaa'){
ctx.fillStyle = color;
ctx.font = `bold ${size}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = color;
ctx.shadowBlur = size / 8;
ctx.fillText(text, W / 2, y);
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
if (subText){
ctx.fillStyle = subColor;
ctx.font = `bold ${subSize || size * 0.5}px monospace`;
ctx.fillText(subText, W / 2, y + size * 0.9);
}
}
function drawMenu(){
ctx.fillStyle = 'rgba(0,0,0,0.85)';
ctx.fillRect(0, 0, W, H);
drawCenteredText('BREAKOUT', H * 0.35, Math.min(W * 0.1, 100), '#4fc3f7');
drawCenteredText('mouse or arrow keys / A-D', H * 0.48, Math.min(W * 0.025, 22), '#888');
drawCenteredText('click or SPACE to launch', H * 0.53, Math.min(W * 0.022, 20), '#aaa');
drawCenteredText('click or SPACE to start', H * 0.59, Math.min(W * 0.022, 20), '#aaa');
if (highScore > 0){
ctx.fillStyle = '#ffeb3b';
ctx.font = `bold ${Math.min(W * 0.018, 18)}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`HIGH SCORE: ${highScore}`, W / 2, H * 0.72);
}
// Demo ball on paddle
if (ball && paddle){
paddle.w = CONFIG.paddleW; paddle.h = CONFIG.paddleH; paddle.y = CONFIG.paddleY;
paddle.x = W / 2;
ball.r = CONFIG.ballR;
ball.x = paddle.x + paddle.w / 2;
ball.y = CONFIG.paddleY - ball.r - Math.sin(time * 4) * 3;
paddle.draw(ctx);
ball.draw(ctx);
}
}
function drawLevelComplete(){
ctx.fillStyle = 'rgba(0,0,0,0.85)';
ctx.fillRect(0, 0, W, H);
drawCenteredText('LEVEL CLEAR!', H * 0.35, Math.min(W * 0.09, 90), '#2ecc71',
`SCORE: ${score}`, Math.min(W * 0.025, 22), '#fff');
drawCenteredText(`LEVEL ${levelIndex + 1} COMPLETE`, H * 0.48, Math.min(W * 0.022, 20), '#aaa');
drawCenteredText('click or SPACE', H * 0.58, Math.min(W * 0.02, 18), '#aaa');
}
function drawGameOver(){
ctx.fillStyle = 'rgba(0,0,0,0.85)';
ctx.fillRect(0, 0, W, H);
drawCenteredText('GAME OVER', H * 0.35, Math.min(W * 0.1, 100), '#e74c3c',
`FINAL SCORE: ${score}`, Math.min(W * 0.025, 22), '#fff');
drawCenteredText('click or SPACE to restart', H * 0.52, Math.min(W * 0.022, 20), '#aaa');
}
function drawVictory(){
ctx.fillStyle = 'rgba(0,0,0,0.85)';
ctx.fillRect(0, 0, W, H);
drawCenteredText('VICTORY!', H * 0.3, Math.min(W * 0.11, 110), '#2ecc71',
`FINAL SCORE: ${score}`, Math.min(W * 0.025, 22), '#fff');
drawCenteredText('all levels cleared', H * 0.45, Math.min(W * 0.02, 18), '#aaa');
drawCenteredText('click or SPACE to play again', H * 0.55, Math.min(W * 0.022, 20), '#aaa');
}
// ===== LOOP =====
let lastTime = 0;
function loop(timestamp){
if (!lastTime) lastTime = timestamp;
let dt = (timestamp - lastTime) / 1000;
if (dt > 0.1) dt = 0.1;
lastTime = timestamp;
update(dt);
draw();
requestAnimationFrame(loop);
}
// Init
loadHighScore();
paddle = new Paddle();
ball = new Ball();
resetLevel();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>

