Run details
- Model
- google/gemini-4-hydrogen
- Route
- OpenRouter API, one user message
- Served by
- Effort
- medium, OpenRouter's default for this model
- Limits
- 60 min · 400,000 output tokens (our $3.00 stop per request at $7.50/M)
- Date
- 12 Nov 2026
- Time
- 1 min 36 s
- Size
- 37 kB
- Tokens
- 109 in, 15,340 out (496 of them thinking)
- List price
- $0.12 at today's price ($1.50 in / $7.50 out per M)
- Paid
- $0.12
Exact prompt
Make a Breakout game I can play right now with the keyboard or the mouse. Give it several levels, a score and some juice. It must be one complete, self-contained HTML file: inline all CSS and JavaScript, and make no network requests of any kind (no CDNs, web fonts or remote images). Reply with the file in a single ```html code block.
Source
37 kB 1,385 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>NEON BREAKER</title>
<style>
:root {
--bg: #080b14;
--panel: #0f1626;
--border: #1f2b47;
--text: #e6f0ff;
--muted: #7c8db5;
--accent: #00f0ff;
--accent2: #ff2a85;
--gold: #ffd166;
}
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at 50% 20%, #121a30 0%, var(--bg) 70%);
color: var(--text);
font-family: 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.game-shell {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 12px;
max-width: 100vw;
max-height: 100vh;
}
.hud {
width: 800px;
max-width: 96vw;
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(15, 22, 38, 0.85);
border: 1px solid var(--border);
border-radius: 12px;
padding: 10px 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
gap: 12px;
flex-wrap: wrap;
}
.hud-group {
display: flex;
align-items: center;
gap: 18px;
}
.stat {
display: flex;
flex-direction: column;
line-height: 1.1;
}
.stat-label {
font-size: 10px;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--muted);
font-weight: 700;
}
.stat-value {
font-size: 20px;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: var(--text);
}
.stat-value.accent { color: var(--accent); }
.stat-value.gold { color: var(--gold); }
.lives-dots {
display: flex;
gap: 6px;
margin-top: 4px;
}
.life-dot {
width: 14px;
height: 7px;
border-radius: 4px;
background: var(--accent2);
box-shadow: 0 0 8px var(--accent2);
transition: opacity 0.2s, transform 0.2s;
}
.life-dot.lost {
opacity: 0.18;
box-shadow: none;
transform: scale(0.85);
}
.controls-bar {
display: flex;
align-items: center;
gap: 8px;
}
button.hud-btn {
background: #17213a;
color: var(--text);
border: 1px solid #2b3c63;
border-radius: 8px;
padding: 6px 11px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.5px;
cursor: pointer;
transition: transform 0.1s, background 0.15s, border-color 0.15s;
}
button.hud-btn:hover {
background: #202e50;
border-color: var(--accent);
}
button.hud-btn:active {
transform: scale(0.95);
}
.stage-wrap {
position: relative;
width: 800px;
max-width: 96vw;
aspect-ratio: 4 / 3;
max-height: calc(100vh - 125px);
border-radius: 14px;
overflow: hidden;
border: 2px solid var(--border);
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.65), 0 0 35px rgba(0, 240, 255, 0.08);
background: #060911;
}
canvas {
width: 100%;
height: 100%;
display: block;
cursor: none;
touch-action: none;
}
.overlay {
position: absolute;
inset: 0;
background: rgba(6, 9, 17, 0.78);
backdrop-filter: blur(5px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 24px;
transition: opacity 0.25s ease;
cursor: default;
}
.overlay.hidden {
opacity: 0;
pointer-events: none;
}
.title {
font-size: clamp(30px, 5vw, 52px);
font-weight: 900;
letter-spacing: 4px;
margin: 0 0 8px 0;
background: linear-gradient(90deg, var(--accent), #7b61ff, var(--accent2));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 0 30px rgba(0, 240, 255, 0.25);
}
.subtitle {
color: var(--muted);
font-size: 15px;
max-width: 460px;
line-height: 1.5;
margin: 0 0 22px 0;
}
.cta-btn {
background: linear-gradient(135deg, var(--accent), #2e86ff);
color: #050912;
border: none;
border-radius: 10px;
padding: 13px 30px;
font-size: 16px;
font-weight: 900;
letter-spacing: 1.5px;
text-transform: uppercase;
cursor: pointer;
box-shadow: 0 6px 25px rgba(0, 240, 255, 0.35);
transition: transform 0.12s, box-shadow 0.15s;
}
.cta-btn:hover {
transform: translateY(-2px) scale(1.02);
box-shadow: 0 10px 30px rgba(0, 240, 255, 0.5);
}
.cta-btn:active {
transform: translateY(1px) scale(0.98);
}
.legend {
margin-top: 22px;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px 18px;
font-size: 12px;
color: #a9b8d9;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.pill {
display: inline-block;
padding: 2px 7px;
border-radius: 5px;
font-weight: 800;
font-size: 10px;
color: #060911;
}
.footer-hint {
font-size: 12px;
color: var(--muted);
text-align: center;
}
kbd {
background: #17213a;
border: 1px solid #2b3c63;
border-radius: 4px;
padding: 1px 6px;
font-size: 11px;
color: var(--text);
font-family: inherit;
}
</style>
</head>
<body>
<div class="game-shell">
<div class="hud">
<div class="hud-group">
<div class="stat">
<span class="stat-label">Score</span>
<span class="stat-value accent" id="scoreEl">0</span>
</div>
<div class="stat">
<span class="stat-label">Best</span>
<span class="stat-value gold" id="bestEl">0</span>
</div>
<div class="stat">
<span class="stat-label">Level</span>
<span class="stat-value" id="levelEl">1 / 5</span>
</div>
<div class="stat">
<span class="stat-label">Lives</span>
<div class="lives-dots" id="livesEl"></div>
</div>
</div>
<div class="controls-bar">
<button class="hud-btn" id="pauseBtn" title="Pause (P)">PAUSE</button>
<button class="hud-btn" id="muteBtn" title="Toggle Sound (M)">SOUND: ON</button>
<button class="hud-btn" id="restartBtn" title="Restart Game (R)">RESTART</button>
</div>
</div>
<div class="stage-wrap">
<canvas id="game" width="800" height="600"></canvas>
<div class="overlay" id="overlay">
<h1 class="title" id="overlayTitle">NEON BREAKER</h1>
<p class="subtitle" id="overlaySub">
Smash through 5 handcrafted neon stages. Build combos, catch power-ups, and keep the ball alive.
</p>
<button class="cta-btn" id="overlayBtn">PLAY NOW</button>
<div class="legend" id="overlayLegend">
<div class="legend-item"><span class="pill" style="background:#00f0ff">WIDE</span> Expand Paddle</div>
<div class="legend-item"><span class="pill" style="background:#ff2a85">MULTI</span> +2 Balls</div>
<div class="legend-item"><span class="pill" style="background:#ff9f1c">FIRE</span> Piercing Ball</div>
<div class="legend-item"><span class="pill" style="background:#70e000">1UP</span> Extra Life</div>
<div class="legend-item"><span class="pill" style="background:#b388ff">SLOW</span> Chill Speed</div>
</div>
</div>
</div>
<div class="footer-hint">
Move: <strong>Mouse</strong> / <strong>Touch</strong> or <kbd>←</kbd> <kbd>→</kbd> / <kbd>A</kbd> <kbd>D</kbd> •
Launch: <strong>Click</strong> or <kbd>Space</kbd> •
Pause: <kbd>P</kbd> •
Mute: <kbd>M</kbd>
</div>
</div>
<script>
(() => {
'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
// UI refs
const scoreEl = document.getElementById('scoreEl');
const bestEl = document.getElementById('bestEl');
const levelEl = document.getElementById('levelEl');
const livesEl = document.getElementById('livesEl');
const overlay = document.getElementById('overlay');
const overlayTitle = document.getElementById('overlayTitle');
const overlaySub = document.getElementById('overlaySub');
const overlayBtn = document.getElementById('overlayBtn');
const overlayLegend = document.getElementById('overlayLegend');
const pauseBtn = document.getElementById('pauseBtn');
const muteBtn = document.getElementById('muteBtn');
const restartBtn = document.getElementById('restartBtn');
// --- Audio Synthesizer (Web Audio API, zero external assets) ---
class SoundFX {
constructor() {
this.ctx = null;
this.muted = false;
}
ensure() {
if (this.muted) return null;
if (!this.ctx) {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
if (AudioCtx) this.ctx = new AudioCtx();
}
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume().catch(() => {});
}
return this.ctx;
}
tone(freq, dur, type = 'sine', vol = 0.12, slide = 0) {
const ac = this.ensure();
if (!ac) return;
const now = ac.currentTime;
const osc = ac.createOscillator();
const gain = ac.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, now);
if (slide) osc.frequency.exponentialRampToValueAtTime(Math.max(40, freq + slide), now + dur);
gain.gain.setValueAtTime(vol, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + dur);
osc.connect(gain);
gain.connect(ac.destination);
osc.start(now);
osc.stop(now + dur);
}
paddle() { this.tone(360, 0.08, 'triangle', 0.14, 140); }
wall() { this.tone(220, 0.05, 'sine', 0.08, -40); }
brick(combo) {
const pitch = Math.min(1100, 340 + combo * 42);
this.tone(pitch, 0.11, 'square', 0.075, 110);
}
armorHit() { this.tone(170, 0.07, 'sawtooth', 0.08, -50); }
explode() {
this.tone(130, 0.24, 'sawtooth', 0.14, -95);
this.tone(85, 0.28, 'triangle', 0.16, -50);
}
powerup() {
this.tone(520, 0.09, 'sine', 0.11, 180);
setTimeout(() => this.tone(780, 0.14, 'triangle', 0.11, 220), 65);
}
loseLife() {
this.tone(260, 0.18, 'sawtooth', 0.13, -120);
setTimeout(() => this.tone(150, 0.28, 'sawtooth', 0.13, -80), 140);
}
levelWin() {
[523, 659, 784, 1046].forEach((f, i) => {
setTimeout(() => this.tone(f, 0.16, 'triangle', 0.12, 30), i * 85);
});
}
}
const sfx = new SoundFX();
// --- Levels ---
// Legend:
// . = empty
// 1-5 = standard colorful bricks (1 HP)
// A = Armored brick (2 HP)
// H = Heavy armored brick (3 HP)
// X = Explosive bomb brick (clears neighbors!)
// M = Unbreakable metal block (doesn't need clearing)
const LEVELS = [
{
name: 'NEON GATE',
speed: 390,
rows: [
'..11111111..',
'.2222222222.',
'33333XX33333',
'.4444444444.',
'..55555555..',
]
},
{
name: 'PRISM FORTRESS',
speed: 420,
rows: [
'11.AAAAAA.11',
'22.22XX22.22',
'33.333333.33',
'44.AA44AA.44',
'55.555555.55',
'..MM....MM..'
]
},
{
name: 'CYBER MATRIX',
speed: 445,
rows: [
'H.1.2.3.2.1.H',
'1.A.3.X.3.A.1',
'2.3.4.5.4.3.2',
'3.A.5.X.5.A.3',
'4.5.M.M.M.5.4',
'5.5.5.5.5.5.5'
].map(r => r.replace(/\./g, '')) // normalize to 12 cols
},
{
name: 'REACTOR CORE',
speed: 470,
rows: [
'M..HHHHHH..M',
'11.AXAAXA.11',
'22.A4444A.22',
'33.AXAAXA.33',
'44.HHHHHH.44',
'5555.MM.5555'
]
},
{
name: 'OMEGA CITADEL',
speed: 500,
rows: [
'HHHHHHHHHHHH',
'11X22AA22X11',
'33344XX44333',
'MM.AAAAAA.MM',
'555.HXXH.555',
'123451234512'
]
}
];
// Fix level 3 rows cleanly to 12 chars
LEVELS[2].rows = [
'H1232112321H',
'1A3X3AA3X3A1',
'234542245432',
'3A5X5AA5X5A3',
'45MM4444MM54',
'555555555555'
];
const COLORS = {
'1': { fill: '#00f0ff', glow: '#00f0ff' },
'2': { fill: '#3a86ff', glow: '#3a86ff' },
'3': { fill: '#b5179e', glow: '#f72585' },
'4': { fill: '#ff2a85', glow: '#ff2a85' },
'5': { fill: '#ffd166', glow: '#ffd166' },
'A': { fill: '#94a3b8', glow: '#cbd5e1' },
'H': { fill: '#e2e8f0', glow: '#ffffff' },
'X': { fill: '#ff6b00', glow: '#ff9f1c' },
'M': { fill: '#334155', glow: '#475569' }
};
const POWERUPS = [
{ type: 'WIDE', label: 'WIDE', color: '#00f0ff' },
{ type: 'MULTI', label: 'MULTI', color: '#ff2a85' },
{ type: 'FIRE', label: 'FIRE', color: '#ff9f1c' },
{ type: 'SLOW', label: 'SLOW', color: '#b388ff' },
{ type: 'LIFE', label: '1UP', color: '#70e000' }
];
// --- Game State ---
let state = 'MENU'; // MENU, PLAYING, PAUSED, LEVEL_CLEAR, GAMEOVER, VICTORY
let score = 0;
let bestScore = 0;
try {
bestScore = parseInt(localStorage.getItem('neon_breaker_best') || '0', 10) || 0;
} catch (e) {}
let levelIdx = 0;
let lives = 3;
let combo = 0;
let shakeTime = 0;
let shakeMag = 0;
let flashAlpha = 0;
// Paddle
const paddle = {
w: 112,
baseW: 112,
h: 16,
x: W / 2 - 56,
y: H - 42,
speed: 640,
hitScale: 0,
wideTimer: 0
};
// Entities
let balls = [];
let bricks = [];
let particles = [];
let powerups = [];
let popups = [];
let bgStars = Array.from({ length: 55 }, () => ({
x: Math.random() * W,
y: Math.random() * H,
r: Math.random() * 1.6 + 0.4,
a: Math.random() * 0.5 + 0.15,
s: Math.random() * 18 + 6
}));
// Input
const keys = { left: false, right: false };
let pointerX = null;
// --- Helpers ---
function saveBest() {
if (score > bestScore) {
bestScore = score;
try { localStorage.setItem('neon_breaker_best', String(bestScore)); } catch (e) {}
}
}
function updateHUD() {
scoreEl.textContent = score.toLocaleString();
bestEl.textContent = bestScore.toLocaleString();
levelEl.textContent = `${levelIdx + 1} / ${LEVELS.length}`;
livesEl.innerHTML = '';
const maxDots = Math.max(3, lives);
for (let i = 0; i < maxDots; i++) {
const dot = document.createElement('div');
dot.className = 'life-dot' + (i >= lives ? ' lost' : '');
livesEl.appendChild(dot);
}
}
function addShake(mag, dur) {
shakeMag = Math.max(shakeMag, mag);
shakeTime = Math.max(shakeTime, dur);
}
function addPopup(x, y, text, color = '#ffffff') {
popups.push({ x, y, text, color, life: 0.85, maxLife: 0.85 });
}
function spawnParticles(x, y, color, count = 14, speed = 220) {
for (let i = 0; i < count; i++) {
const ang = Math.random() * Math.PI * 2;
const spd = (0.3 + Math.random() * 0.9) * speed;
particles.push({
x, y,
vx: Math.cos(ang) * spd,
vy: Math.sin(ang) * spd,
size: 2 + Math.random() * 3.5,
color,
life: 0.35 + Math.random() * 0.35,
maxLife: 0.7
});
}
}
function createBall(stuck = true, x = null, y = null, vx = 0, vy = 0) {
const baseSpeed = LEVELS[levelIdx].speed;
return {
x: x ?? (paddle.x + paddle.w / 2),
y: y ?? (paddle.y - 9),
vx,
vy,
r: 8,
speed: baseSpeed,
stuck,
fireTimer: 0,
trail: []
};
}
function launchStuckBalls() {
let launched = false;
for (const b of balls) {
if (b.stuck) {
b.stuck = false;
const spread = (Math.random() - 0.5) * 0.55;
b.vx = Math.sin(spread) * b.speed;
b.vy = -Math.cos(spread) * b.speed;
launched = true;
}
}
if (launched) sfx.paddle();
}
function buildLevel(idx) {
bricks = [];
powerups = [];
particles = [];
popups = [];
combo = 0;
paddle.wideTimer = 0;
paddle.w = paddle.baseW;
paddle.x = Math.min(W - paddle.w - 10, Math.max(10, paddle.x));
const lvl = LEVELS[idx];
const cols = 12;
const padX = 36;
const gap = 6;
const totalW = W - padX * 2;
const bw = (totalW - gap * (cols - 1)) / cols;
const bh = 24;
const startY = 74;
lvl.rows.forEach((rowStr, r) => {
for (let c = 0; c < cols; c++) {
const ch = rowStr[c];
if (!ch || ch === '.') continue;
let hp = 1;
let breakable = true;
if (ch === 'A') hp = 2;
else if (ch === 'H') hp = 3;
else if (ch === 'M') { hp = Infinity; breakable = false; }
bricks.push({
row: r,
col: c,
x: padX + c * (bw + gap),
y: startY + r * (bh + gap),
w: bw,
h: bh,
type: ch,
hp,
maxHp: hp,
breakable,
hitFlash: 0
});
}
});
balls = [createBall(true)];
updateHUD();
}
function startNewGame() {
score = 0;
lives = 3;
levelIdx = 0;
paddle.x = W / 2 - paddle.baseW / 2;
buildLevel(levelIdx);
state = 'PLAYING';
overlay.classList.add('hidden');
pauseBtn.textContent = 'PAUSE';
updateHUD();
}
function showOverlay(title, sub, btnText, showLegend = false) {
overlayTitle.textContent = title;
overlaySub.textContent = sub;
overlayBtn.textContent = btnText;
overlayLegend.style.display = showLegend ? 'flex' : 'none';
overlay.classList.remove('hidden');
}
function togglePause() {
if (state === 'PLAYING') {
state = 'PAUSED';
pauseBtn.textContent = 'RESUME';
showOverlay('PAUSED', 'Take a breather. Click Resume or press P to jump back in.', 'RESUME');
} else if (state === 'PAUSED') {
state = 'PLAYING';
pauseBtn.textContent = 'PAUSE';
overlay.classList.add('hidden');
}
}
function maybeDropPowerup(brick) {
const chance = brick.type === 'A' || brick.type === 'H' ? 0.34 : 0.2;
if (Math.random() > chance) return;
const def = POWERUPS[Math.floor(Math.random() * POWERUPS.length)];
powerups.push({
x: brick.x + brick.w / 2,
y: brick.y + brick.h / 2,
w: 44,
h: 18,
vy: 145,
...def
});
}
function destroyBrick(brick, isChain = false) {
if (!brick.breakable || brick.hp <= 0) return;
brick.hp = 0;
combo++;
const mult = Math.min(5, 1 + Math.floor(combo / 4));
const basePts = brick.type === 'H' ? 150 : brick.type === 'A' ? 100 : brick.type === 'X' ? 120 : 50;
const pts = basePts * mult;
score += pts;
saveBest();
const col = COLORS[brick.type].fill;
const cx = brick.x + brick.w / 2;
const cy = brick.y + brick.h / 2;
spawnParticles(cx, cy, col, brick.type === 'X' ? 26 : 14, brick.type === 'X' ? 300 : 210);
addPopup(cx, cy, `+${pts}${mult > 1 ? ` x${mult}` : ''}`, col);
if (!isChain) sfx.brick(combo);
addShake(brick.type === 'X' ? 8 : 2.5, brick.type === 'X' ? 0.22 : 0.08);
maybeDropPowerup(brick);
// Bomb brick chain explosion
if (brick.type === 'X') {
sfx.explode();
flashAlpha = 0.22;
for (const other of bricks) {
if (other !== brick && other.breakable && other.hp > 0) {
const dr = Math.abs(other.row - brick.row);
const dc = Math.abs(other.col - brick.col);
if (dr <= 1 && dc <= 1) {
destroyBrick(other, true);
}
}
}
}
}
function applyPowerup(p) {
sfx.powerup();
addPopup(p.x, p.y - 10, p.label + '!', p.color);
score += 75;
saveBest();
if (p.type === 'WIDE') {
paddle.wideTimer = 11;
paddle.w = Math.min(200, paddle.baseW * 1.55);
paddle.x = Math.min(W - paddle.w - 8, paddle.x);
} else if (p.type === 'MULTI') {
const ref = balls.find(b => !b.stuck) || balls[0];
if (ref) {
if (ref.stuck) launchStuckBalls();
for (let i = 0; i < 2; i++) {
const angle = (i === 0 ? -0.45 : 0.45) + (Math.random() - 0.5) * 0.2;
const spd = ref.speed;
const nb = createBall(false, ref.x, ref.y, Math.sin(angle) * spd, -Math.abs(Math.cos(angle) * spd));
nb.fireTimer = ref.fireTimer;
balls.push(nb);
}
}
} else if (p.type === 'FIRE') {
for (const b of balls) b.fireTimer = 7.5;
} else if (p.type === 'SLOW') {
for (const b of balls) {
b.speed = Math.max(290, b.speed * 0.8);
const mag = Math.hypot(b.vx, b.vy) || 1;
b.vx = (b.vx / mag) * b.speed;
b.vy = (b.vy / mag) * b.speed;
}
} else if (p.type === 'LIFE') {
lives = Math.min(5, lives + 1);
}
updateHUD();
}
// --- Physics & Update ---
function update(dt) {
// Background stars always drift subtly
for (const st of bgStars) {
st.y += st.s * dt;
if (st.y > H) { st.y = 0; st.x = Math.random() * W; }
}
if (state !== 'PLAYING') return;
// Shake & flash decay
if (shakeTime > 0) {
shakeTime = Math.max(0, shakeTime - dt);
if (shakeTime === 0) shakeMag = 0;
}
if (flashAlpha > 0) flashAlpha = Math.max(0, flashAlpha - dt * 1.2);
if (paddle.hitScale > 0) paddle.hitScale = Math.max(0, paddle.hitScale - dt * 5);
// Wide timer
if (paddle.wideTimer > 0) {
paddle.wideTimer -= dt;
if (paddle.wideTimer <= 0) {
const center = paddle.x + paddle.w / 2;
paddle.w = paddle.baseW;
paddle.x = Math.max(8, Math.min(W - paddle.w - 8, center - paddle.w / 2));
}
}
// Paddle movement (keyboard or mouse)
if (keys.left) {
paddle.x -= paddle.speed * dt;
pointerX = null;
}
if (keys.right) {
paddle.x += paddle.speed * dt;
pointerX = null;
}
if (pointerX !== null) {
const target = pointerX - paddle.w / 2;
paddle.x += (target - paddle.x) * Math.min(1, dt * 28);
}
paddle.x = Math.max(8, Math.min(W - paddle.w - 8, paddle.x));
// Brick flash decay
for (const br of bricks) {
if (br.hitFlash > 0) br.hitFlash = Math.max(0, br.hitFlash - dt * 5);
}
// Update balls with substeps to prevent tunneling
for (let bi = balls.length - 1; bi >= 0; bi--) {
const b = balls[bi];
if (b.fireTimer > 0) b.fireTimer = Math.max(0, b.fireTimer - dt);
if (b.stuck) {
b.x = paddle.x + paddle.w / 2;
b.y = paddle.y - b.r - 1;
b.trail = [];
continue;
}
// Trail
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 10) b.trail.shift();
const steps = Math.max(1, Math.ceil((b.speed * dt) / 6));
const stepDt = dt / steps;
for (let s = 0; s < steps; s++) {
b.x += b.vx * stepDt;
b.y += b.vy * stepDt;
// Wall collisions
if (b.x - b.r < 6) {
b.x = 6 + b.r;
b.vx = Math.abs(b.vx);
sfx.wall();
spawnParticles(b.x, b.y, '#00f0ff', 5, 110);
} else if (b.x + b.r > W - 6) {
b.x = W - 6 - b.r;
b.vx = -Math.abs(b.vx);
sfx.wall();
spawnParticles(b.x, b.y, '#00f0ff', 5, 110);
}
if (b.y - b.r < 6) {
b.y = 6 + b.r;
b.vy = Math.abs(b.vy);
sfx.wall();
spawnParticles(b.x, b.y, '#00f0ff', 5, 110);
}
// Paddle collision
if (
b.vy > 0 &&
b.y + b.r >= paddle.y &&
b.y - b.r <= paddle.y + paddle.h &&
b.x + b.r >= paddle.x &&
b.x - b.r <= paddle.x + paddle.w
) {
const hitPos = ((b.x - paddle.x) / paddle.w) * 2 - 1; // -1..1
const clamped = Math.max(-0.92, Math.min(0.92, hitPos));
const maxAngle = Math.PI * 0.37; // ~66 deg
const angle = clamped * maxAngle;
// Slightly ramp speed on paddle hit up to cap
b.speed = Math.min(680, b.speed + 4);
b.vx = Math.sin(angle) * b.speed;
b.vy = -Math.cos(angle) * b.speed;
b.y = paddle.y - b.r - 0.5;
combo = 0;
paddle.hitScale = 1;
sfx.paddle();
spawnParticles(b.x, paddle.y, '#00f0ff', 8, 150);
break;
}
// Brick collisions
for (const br of bricks) {
if (br.hp <= 0) continue;
const closestX = Math.max(br.x, Math.min(b.x, br.x + br.w));
const closestY = Math.max(br.y, Math.min(b.y, br.y + br.h));
const dx = b.x - closestX;
const dy = b.y - closestY;
if (dx * dx + dy * dy <= b.r * b.r) {
const isFire = b.fireTimer > 0 && br.breakable;
if (!isFire) {
// Determine reflection axis
const overlapLeft = (b.x + b.r) - br.x;
const overlapRight = (br.x + br.w) - (b.x - b.r);
const overlapTop = (b.y + b.r) - br.y;
const overlapBottom = (br.y + br.h) - (b.y - b.r);
const minOverlapX = Math.min(overlapLeft, overlapRight);
const minOverlapY = Math.min(overlapTop, overlapBottom);
if (minOverlapX < minOverlapY) {
b.vx = -b.vx;
b.x += b.vx > 0 ? minOverlapX : -minOverlapX;
} else {
b.vy = -b.vy;
b.y += b.vy > 0 ? minOverlapY : -minOverlapY;
}
// Prevent horizontal deadlock
if (Math.abs(b.vy) < b.speed * 0.22) {
b.vy = (b.vy >= 0 ? 1 : -1) * b.speed * 0.24;
const mag = Math.hypot(b.vx, b.vy);
b.vx = (b.vx / mag) * b.speed;
b.vy = (b.vy / mag) * b.speed;
}
}
if (br.breakable) {
if (isFire) {
destroyBrick(br);
} else {
br.hp -= 1;
br.hitFlash = 1;
if (br.hp <= 0) {
destroyBrick(br);
} else {
sfx.armorHit();
addShake(2, 0.06);
spawnParticles(closestX, closestY, COLORS[br.type].fill, 7, 130);
}
}
} else {
// Unbreakable metal block
br.hitFlash = 1;
sfx.armorHit();
spawnParticles(closestX, closestY, '#94a3b8', 6, 120);
}
updateHUD();
if (!isFire) break;
}
}
}
// Fell below bottom
if (b.y - b.r > H + 12) {
balls.splice(bi, 1);
}
}
// Powerups fall & collect
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.y += p.vy * dt;
if (
p.y + p.h / 2 >= paddle.y &&
p.y - p.h / 2 <= paddle.y + paddle.h &&
p.x + p.w / 2 >= paddle.x &&
p.x - p.w / 2 <= paddle.x + paddle.w
) {
applyPowerup(p);
spawnParticles(p.x, p.y, p.color, 14, 180);
powerups.splice(i, 1);
continue;
}
if (p.y - p.h > H) powerups.splice(i, 1);
}
// Particles
for (let i = particles.length - 1; i >= 0; i--) {
const pt = particles[i];
pt.x += pt.vx * dt;
pt.y += pt.vy * dt;
pt.vx *= 0.96;
pt.vy *= 0.96;
pt.life -= dt;
if (pt.life <= 0) particles.splice(i, 1);
}
// Popups
for (let i = popups.length - 1; i >= 0; i--) {
const pp = popups[i];
pp.y -= 36 * dt;
pp.life -= dt;
if (pp.life <= 0) popups.splice(i, 1);
}
// Lost all balls?
if (balls.length === 0) {
lives--;
combo = 0;
sfx.loseLife();
addShake(9, 0.3);
flashAlpha = 0.25;
updateHUD();
if (lives <= 0) {
state = 'GAMEOVER';
saveBest();
showOverlay(
'GAME OVER',
`Final Score: ${score.toLocaleString()} • Reached Level ${levelIdx + 1}`,
'TRY AGAIN'
);
} else {
paddle.wideTimer = 0;
paddle.w = paddle.baseW;
balls = [createBall(true)];
}
return;
}
// Check level clear (all breakable bricks cleared)
const remaining = bricks.some(b => b.breakable && b.hp > 0);
if (!remaining) {
sfx.levelWin();
score += 500 * (levelIdx + 1);
saveBest();
updateHUD();
if (levelIdx + 1 >= LEVELS.length) {
state = 'VICTORY';
showOverlay(
'YOU WIN!',
`All ${LEVELS.length} Neon Stages Cleared! Final Score: ${score.toLocaleString()}`,
'PLAY AGAIN'
);
} else {
state = 'LEVEL_CLEAR';
showOverlay(
`STAGE ${levelIdx + 1} CLEAR!`,
`Next up: ${LEVELS[levelIdx + 1].name} (+${500 * (levelIdx + 1)} Stage Bonus)`,
'NEXT STAGE'
);
}
}
}
// --- Rendering ---
function roundRect(x, y, w, h, r) {
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 render() {
ctx.save();
// Screen shake offset
if (shakeTime > 0) {
const sx = (Math.random() - 0.5) * 2 * shakeMag;
const sy = (Math.random() - 0.5) * 2 * shakeMag;
ctx.translate(sx, sy);
}
// Background
ctx.fillStyle = '#060911';
ctx.fillRect(0, 0, W, H);
// Subtle cyber grid
ctx.strokeStyle = 'rgba(31, 43, 71, 0.28)';
ctx.lineWidth = 1;
for (let x = 0; x <= W; x += 40) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y <= H; y += 40) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
// Stars
for (const st of bgStars) {
ctx.fillStyle = `rgba(180, 215, 255, ${st.a})`;
ctx.beginPath();
ctx.arc(st.x, st.y, st.r, 0, Math.PI * 2);
ctx.fill();
}
// Arena inner border glow
ctx.strokeStyle = 'rgba(0, 240, 255, 0.25)';
ctx.lineWidth = 3;
ctx.strokeRect(4, 4, W - 8, H + 10);
// Level name watermark
ctx.fillStyle = 'rgba(124, 141, 181, 0.14)';
ctx.font = '900 18px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`STAGE ${levelIdx + 1}: ${LEVELS[levelIdx].name}`, W / 2, 38);
// Bricks
for (const br of bricks) {
if (br.hp <= 0) continue;
const col = COLORS[br.type];
ctx.save();
ctx.shadowColor = col.glow;
ctx.shadowBlur = br.type === 'X' ? 14 : 8;
ctx.fillStyle = br.hitFlash > 0.2 ? '#ffffff' : col.fill;
roundRect(br.x, br.y, br.w, br.h, 5);
ctx.fill();
// Top highlight
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255,255,255,0.22)';
roundRect(br.x + 2, br.y + 2, br.w - 4, 5, 2);
ctx.fill();
// Armored cracks / HP pips
if (br.type === 'A' || br.type === 'H') {
ctx.fillStyle = 'rgba(6, 9, 17, 0.65)';
const pips = br.hp;
for (let p = 0; p < pips; p++) {
ctx.beginPath();
ctx.arc(br.x + br.w / 2 + (p - (pips - 1) / 2) * 9, br.y + br.h / 2 + 2, 2.5, 0, Math.PI * 2);
ctx.fill();
}
} else if (br.type === 'X') {
ctx.fillStyle = '#060911';
ctx.font = '900 12px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('★', br.x + br.w / 2, br.y + br.h / 2 + 1);
} else if (br.type === 'M') {
ctx.strokeStyle = '#64748b';
ctx.lineWidth = 1.5;
roundRect(br.x + 3, br.y + 3, br.w - 6, br.h - 6, 3);
ctx.stroke();
}
ctx.restore();
}
// Powerups
for (const p of powerups) {
ctx.save();
ctx.shadowColor = p.color;
ctx.shadowBlur = 12;
ctx.fillStyle = p.color;
roundRect(p.x - p.w / 2, p.y - p.h / 2, p.w, p.h, 9);
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = '#060911';
ctx.font = '900 10px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(p.label, p.x, p.y + 0.5);
ctx.restore();
}
// Paddle
ctx.save();
const stretch = paddle.hitScale * 6;
const px = paddle.x - stretch / 2;
const py = paddle.y + paddle.hitScale * 2;
const pw = paddle.w + stretch;
const ph = paddle.h - paddle.hitScale * 2;
ctx.shadowColor = paddle.wideTimer > 0 ? '#00f0ff' : '#3a86ff';
ctx.shadowBlur = 16;
const grad = ctx.createLinearGradient(px, py, px + pw, py);
grad.addColorStop(0, '#00f0ff');
grad.addColorStop(0.5, '#60a5fa');
grad.addColorStop(1, '#00f0ff');
ctx.fillStyle = paddle.hitScale > 0.35 ? '#ffffff' : grad;
roundRect(px, py, pw, ph, 8);
ctx.fill();
// Paddle core line
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255,255,255,0.65)';
roundRect(px + 10, py + 3, pw - 20, 3, 2);
ctx.fill();
ctx.restore();
// Balls + trails
for (const b of balls) {
const isFire = b.fireTimer > 0;
const ballCol = isFire ? '#ff9f1c' : '#ffffff';
const glowCol = isFire ? '#ff5400' : '#00f0ff';
// Trail
for (let i = 0; i < b.trail.length; i++) {
const t = b.trail[i];
const frac = (i + 1) / b.trail.length;
ctx.fillStyle = isFire
? `rgba(255, 130, 20, ${frac * 0.45})`
: `rgba(0, 240, 255, ${frac * 0.35})`;
ctx.beginPath();
ctx.arc(t.x, t.y, b.r * frac * 0.85, 0, Math.PI * 2);
ctx.fill();
}
ctx.save();
ctx.shadowColor = glowCol;
ctx.shadowBlur = isFire ? 20 : 14;
ctx.fillStyle = ballCol;
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Launch aim hint if stuck
if (b.stuck && state === 'PLAYING') {
ctx.save();
ctx.fillStyle = 'rgba(230, 240, 255, 0.65)';
ctx.font = '700 12px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('CLICK OR SPACE TO LAUNCH', W / 2, paddle.y - 26);
ctx.restore();
}
}
// Particles
for (const pt of particles) {
ctx.globalAlpha = Math.max(0, pt.life / pt.maxLife);
ctx.fillStyle = pt.color;
ctx.fillRect(pt.x - pt.size / 2, pt.y - pt.size / 2, pt.size, pt.size);
}
ctx.globalAlpha = 1;
// Popups
for (const pp of popups) {
ctx.save();
ctx.globalAlpha = Math.max(0, pp.life / pp.maxLife);
ctx.fillStyle = pp.color;
ctx.font = '900 14px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(pp.text, pp.x, pp.y);
ctx.restore();
}
// Active buff indicators
const buffs = [];
if (paddle.wideTimer > 0) buffs.push(`WIDE ${Math.ceil(paddle.wideTimer)}s`);
const maxFire = balls.reduce((m, b) => Math.max(m, b.fireTimer), 0);
if (maxFire > 0) buffs.push(`FIRE ${Math.ceil(maxFire)}s`);
if (combo >= 3) buffs.push(`COMBO x${Math.min(5, 1 + Math.floor(combo / 4))}`);
if (buffs.length > 0) {
ctx.fillStyle = 'rgba(0, 240, 255, 0.85)';
ctx.font = '800 12px system-ui, sans-serif';
ctx.textAlign = 'left';
ctx.fillText(buffs.join(' • '), 16, H - 14);
}
// Screen flash
if (flashAlpha > 0) {
ctx.fillStyle = `rgba(255, 255, 255, ${flashAlpha})`;
ctx.fillRect(0, 0, W, H);
}
ctx.restore();
}
// --- Loop ---
let lastTs = performance.now();
function loop(ts) {
const dt = Math.min(0.033, (ts - lastTs) / 1000);
lastTs = ts;
update(dt);
render();
requestAnimationFrame(loop);
}
// --- Event Listeners ---
function setPointerFromClient(clientX) {
const rect = canvas.getBoundingClientRect();
pointerX = ((clientX - rect.left) / rect.width) * W;
}
canvas.addEventListener('mousemove', (e) => {
setPointerFromClient(e.clientX);
});
canvas.addEventListener('mousedown', (e) => {
if (state === 'PLAYING') {
setPointerFromClient(e.clientX);
launchStuckBalls();
}
});
canvas.addEventListener('touchstart', (e) => {
if (e.touches[0]) {
setPointerFromClient(e.touches[0].clientX);
if (state === 'PLAYING') launchStuckBalls();
}
}, { passive: true });
canvas.addEventListener('touchmove', (e) => {
if (e.touches[0]) setPointerFromClient(e.touches[0].clientX);
}, { passive: true });
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
keys.left = true;
} else if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
keys.right = true;
} else if (e.key === ' ' || e.code === 'Space') {
e.preventDefault();
if (state === 'PLAYING') launchStuckBalls();
else if (state === 'MENU' || state === 'GAMEOVER' || state === 'VICTORY') startNewGame();
else if (state === 'LEVEL_CLEAR') {
levelIdx++;
buildLevel(levelIdx);
state = 'PLAYING';
overlay.classList.add('hidden');
}
} else if (e.key === 'p' || e.key === 'P') {
togglePause();
} else if (e.key === 'm' || e.key === 'M') {
muteBtn.click();
} else if (e.key === 'r' || e.key === 'R') {
startNewGame();
}
});
window.addEventListener('keyup', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') keys.left = false;
else if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') keys.right = false;
});
overlayBtn.addEventListener('click', () => {
sfx.ensure();
if (state === 'PAUSED') {
togglePause();
} else if (state === 'LEVEL_CLEAR') {
levelIdx++;
buildLevel(levelIdx);
state = 'PLAYING';
overlay.classList.add('hidden');
} else {
startNewGame();
}
});
pauseBtn.addEventListener('click', togglePause);
restartBtn.addEventListener('click', startNewGame);
muteBtn.addEventListener('click', () => {
sfx.muted = !sfx.muted;
muteBtn.textContent = sfx.muted ? 'SOUND: OFF' : 'SOUND: ON';
});
// Init
buildLevel(0);
updateHUD();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>

