Run details
- Model
- google/gemini-4-argon
- Route
- OpenRouter API, one user message
- Reasoning
- provider default
- Limits
- 60 min · 60,000 output tokens
- Date
- 21 Oct 2026
- Time
- 2 min 46 s
- Size
- 35 kB
- Tokens
- 109 in, 13,836 out
- List price
- $0.14 ($2 in / $10 out per M)
- Paid
- $0.14
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
35 kB 1,328 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Juicy Neon Breakout</title>
<style>
:root {
--bg: #07070c;
--panel: #11111e;
--accent: #00f3ff;
--accent2: #ff00a0;
--text: #ffffff;
}
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
}
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background-color: var(--bg);
color: var(--text);
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
#game-container {
position: relative;
width: 800px;
height: 600px;
max-width: 100vw;
max-height: 100vh;
aspect-ratio: 4 / 3;
box-shadow: 0 0 40px rgba(0, 243, 255, 0.15), inset 0 0 20px rgba(0, 243, 255, 0.1);
border: 2px solid #22223b;
border-radius: 8px;
overflow: hidden;
background: radial-gradient(circle at center, #101024 0%, #07070c 100%);
}
canvas {
display: block;
width: 100%;
height: 100%;
cursor: none;
}
/* CRT Scanline Overlay */
.scanlines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
rgba(255,255,255,0),
rgba(255,255,255,0) 50%,
rgba(0, 0, 0, 0.25) 50%,
rgba(0, 0, 0, 0.25)
);
background-size: 100% 4px;
pointer-events: none;
z-index: 10;
}
/* UI Overlay */
#ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 20;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.hud {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 25px;
font-size: 18px;
font-weight: 800;
letter-spacing: 1.5px;
text-transform: uppercase;
text-shadow: 0 0 10px rgba(0, 243, 255, 0.6);
}
.hud-group {
display: flex;
gap: 25px;
align-items: center;
}
.stat-box span {
color: var(--accent);
font-variant-numeric: tabular-nums;
}
#combo-display {
color: var(--accent2);
text-shadow: 0 0 10px var(--accent2);
opacity: 0;
transition: opacity 0.2s, transform 0.1s;
display: inline-block;
}
#mute-btn {
pointer-events: auto;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(0, 243, 255, 0.4);
color: var(--text);
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 12px;
letter-spacing: 1px;
transition: all 0.2s;
}
#mute-btn:hover {
background: var(--accent);
color: #000;
box-shadow: 0 0 12px var(--accent);
}
/* Menus */
#menu-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(7, 7, 12, 0.82);
backdrop-filter: blur(4px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
pointer-events: auto;
z-index: 30;
transition: opacity 0.3s ease;
}
#menu-overlay.hidden {
opacity: 0;
pointer-events: none;
}
h1 {
font-size: 56px;
margin: 0 0 10px 0;
letter-spacing: 4px;
text-transform: uppercase;
background: linear-gradient(90deg, #00f3ff, #ff00a0, #ffea00);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
filter: drop-shadow(0 0 15px rgba(255, 0, 160, 0.4));
text-align: center;
}
.subtitle {
color: #aaaadd;
font-size: 18px;
margin-bottom: 35px;
text-align: center;
max-width: 80%;
line-height: 1.5;
}
.btn {
background: transparent;
color: var(--accent);
border: 2px solid var(--accent);
padding: 14px 40px;
font-size: 20px;
font-weight: 900;
letter-spacing: 2px;
text-transform: uppercase;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 0 15px rgba(0, 243, 255, 0.3), inset 0 0 15px rgba(0, 243, 255, 0.2);
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.btn:hover {
background: var(--accent);
color: #05050a;
box-shadow: 0 0 30px var(--accent);
transform: scale(1.06);
}
.btn:active {
transform: scale(0.98);
}
.controls-hint {
margin-top: 40px;
display: flex;
gap: 30px;
color: #777799;
font-size: 13px;
letter-spacing: 1px;
text-transform: uppercase;
}
.controls-hint div {
display: flex;
align-items: center;
gap: 8px;
}
.key {
border: 1px solid #555577;
padding: 3px 8px;
border-radius: 4px;
background: rgba(255,255,255,0.05);
color: #bbb;
font-weight: bold;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="canvas" width="800" height="600"></canvas>
<div class="scanlines"></div>
<div id="ui-layer">
<div class="hud">
<div class="hud-group">
<div class="stat-box">SCORE: <span id="score-el">0</span></div>
<div class="stat-box">HIGH: <span id="high-el">0</span></div>
<div id="combo-display">2x COMBO!</div>
</div>
<div class="hud-group">
<div class="stat-box">LEVEL: <span id="level-el">1</span></div>
<div class="stat-box">LIVES: <span id="lives-el">3</span></div>
<button id="mute-btn">SOUND: ON</button>
</div>
</div>
</div>
<div id="menu-overlay">
<h1 id="menu-title">NEON BREAKOUT</h1>
<div class="subtitle" id="menu-subtitle">Smash bricks, chain combos, and collect powerups in overdrive.</div>
<button class="btn" id="action-btn">PLAY NOW</button>
<div class="controls-hint">
<div><span class="key">MOUSE</span> or <span class="key">←</span> <span class="key">→</span> Move</div>
<div><span class="key">CLICK</span> or <span class="key">SPACE</span> Launch / Lasers</div>
<div><span class="key">P</span> Pause</div>
</div>
</div>
</div>
<script>
(() => {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
// UI Elements
const scoreEl = document.getElementById('score-el');
const highEl = document.getElementById('high-el');
const levelEl = document.getElementById('level-el');
const livesEl = document.getElementById('lives-el');
const comboEl = document.getElementById('combo-display');
const muteBtn = document.getElementById('mute-btn');
const menuOverlay = document.getElementById('menu-overlay');
const menuTitle = document.getElementById('menu-title');
const menuSubtitle = document.getElementById('menu-subtitle');
const actionBtn = document.getElementById('action-btn');
// --- WEB AUDIO SYNTHESIZER ---
class SoundFX {
constructor() {
this.ctx = null;
this.muted = false;
}
init() {
if (!this.ctx) {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioCtx();
}
if (this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
toggleMute() {
this.muted = !this.muted;
muteBtn.textContent = `SOUND: ${this.muted ? 'OFF' : 'ON'}`;
}
tone(freq, type, duration, vol = 0.15, slideFreq = null) {
if (this.muted || !this.ctx) return;
const now = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, now);
if (slideFreq) {
osc.frequency.exponentialRampToValueAtTime(slideFreq, now + duration);
}
gain.gain.setValueAtTime(vol, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + duration);
}
noise(duration, vol = 0.2) {
if (this.muted || !this.ctx) return;
const now = this.ctx.currentTime;
const bufferSize = this.ctx.sampleRate * duration;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(800, now);
filter.frequency.linearRampToValueAtTime(120, now + duration);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(vol, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + duration);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start(now);
}
paddle() { this.tone(240, 'triangle', 0.12, 0.25, 340); }
wall() { this.tone(150, 'sine', 0.08, 0.2, 110); }
brick(combo, unbreakable = false) {
if (unbreakable) {
this.tone(110, 'square', 0.08, 0.15, 90);
return;
}
// Pentatonic scale pitch escalation
const scale = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25, 587.33, 659.25, 783.99, 880.00, 1046.50];
const idx = Math.min(combo, scale.length - 1);
const freq = scale[idx];
this.tone(freq, 'sawtooth', 0.16, 0.14, freq * 1.4);
this.noise(0.09, 0.12);
}
powerup() {
this.tone(330, 'sine', 0.09, 0.18, 660);
setTimeout(() => this.tone(550, 'sine', 0.16, 0.18, 990), 70);
}
laser() { this.tone(880, 'sawtooth', 0.12, 0.1, 180); }
loseLife() {
this.tone(280, 'sawtooth', 0.45, 0.25, 55);
this.noise(0.35, 0.25);
}
winLevel() {
[261.6, 329.6, 392.0, 523.3, 659.3].forEach((f, i) => {
setTimeout(() => this.tone(f, 'triangle', 0.22, 0.2), i * 85);
});
}
}
const sfx = new SoundFX();
muteBtn.addEventListener('click', () => {
sfx.toggleMute();
muteBtn.blur();
});
// --- COLORS & LEVELS ---
const BRICK_TYPES = {
1: { hp: 1, color: '#ff0055', glow: '#ff0055', score: 100 }, // Neon Red
2: { hp: 1, color: '#ff7700', glow: '#ff7700', score: 120 }, // Neon Orange
3: { hp: 1, color: '#ffdd00', glow: '#ffdd00', score: 140 }, // Neon Yellow
4: { hp: 1, color: '#00ff66', glow: '#00ff66', score: 160 }, // Neon Green
5: { hp: 1, color: '#00f3ff', glow: '#00f3ff', score: 180 }, // Neon Cyan
6: { hp: 1, color: '#bd00ff', glow: '#bd00ff', score: 200 }, // Neon Purple
7: { hp: 2, color: '#e2e8f0', glow: '#94a3b8', score: 350 }, // Silver (2 HP)
8: { hp: 3, color: '#fbbf24', glow: '#f59e0b', score: 500 }, // Gold (3 HP)
9: { hp: Infinity, color: '#334155', glow: '#475569', score: 0 } // Indestructible
};
const LEVELS = [
// Level 1: Classic Neon Rainbow
{
name: "NEON DAWN",
grid: [
"000000000000",
"111111111111",
"222222222222",
"333333333333",
"444444444444",
"555555555555"
]
},
// Level 2: Twin Pyramids & Silver Guard
{
name: "TWIN PEAKS",
grid: [
"001100001100",
"012210012210",
"123321123321",
"234432234432",
"345543345543",
"777777777777"
]
},
// Level 3: Cyber Invader
{
name: "CYBER INVADER",
grid: [
"000400004000",
"000040040000",
"000444444000",
"004414414400",
"044444444440",
"040444444040",
"040400004040",
"000077770000"
]
},
// Level 4: The Fortress (Bumpers & Gold Core)
{
name: "THE CITADEL",
grid: [
"777777777777",
"788888888887",
"786666666687",
"786111111687",
"786155551687",
"990055550099",
"000077770000"
]
},
// Level 5: Overdrive Matrix
{
name: "OVERDRIVE CORE",
grid: [
"808080808080",
"010203040506",
"707070707070",
"060504030201",
"990808080899",
"555555555555",
"777777777777"
]
}
];
const POWERUP_TYPES = [
{ type: 'MULTI', color: '#00f3ff', label: 'MULTI' },
{ type: 'WIDE', color: '#00ff66', label: 'WIDE' },
{ type: 'LASER', color: '#ff0055', label: 'LASER' },
{ type: 'FIRE', color: '#ff7700', label: 'FIRE' },
{ type: 'LIFE', color: '#ff00a0', label: '+1 UP' }
];
// --- GAME STATE ---
let state = 'MENU'; // MENU, PLAYING, PAUSED, LEVEL_CLEAR, GAMEOVER, VICTORY
let score = 0;
let highScore = parseInt(localStorage.getItem('neon_breakout_high') || '0', 10);
highEl.textContent = highScore;
let levelIdx = 0;
let lives = 3;
let combo = 0;
// Juice variables
let shakeMag = 0;
let bgPulse = 0;
let scoreScale = 1;
let paddleSquish = 1;
// Entities
const paddle = {
x: W / 2 - 55,
y: H - 40,
w: 110,
targetW: 110,
h: 14,
speed: 620,
vx: 0,
laserTimer: 0,
laserCooldown: 0
};
let balls = [];
let bricks = [];
let particles = [];
let powerups = [];
let lasers = [];
let floatingTexts = [];
let bgStars = [];
// Initialize background stars
for (let i = 0; i < 65; i++) {
bgStars.push({
x: Math.random() * W,
y: Math.random() * H,
r: Math.random() * 1.6 + 0.4,
speed: Math.random() * 25 + 10,
alpha: Math.random() * 0.6 + 0.2
});
}
// --- INPUT HANDLING ---
const keys = { ArrowLeft: false, ArrowRight: false, KeyA: false, KeyD: false };
window.addEventListener('keydown', (e) => {
if (keys.hasOwnProperty(e.code)) keys[e.code] = true;
if (e.code === 'Space') {
e.preventDefault();
handleActionTrigger();
}
if (e.code === 'KeyP' || e.code === 'Escape') {
if (state === 'PLAYING') pauseGame();
else if (state === 'PAUSED') resumeGame();
}
});
window.addEventListener('keyup', (e) => {
if (keys.hasOwnProperty(e.code)) keys[e.code] = false;
});
canvas.addEventListener('mousemove', (e) => {
if (state !== 'PLAYING') return;
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const mouseX = (e.clientX - rect.left) * scaleX;
const prevX = paddle.x;
paddle.x = Math.max(10, Math.min(W - paddle.w - 10, mouseX - paddle.w / 2));
paddle.vx = (paddle.x - prevX) * 30;
});
canvas.addEventListener('mousedown', (e) => {
if (e.button === 0) handleActionTrigger();
});
// Touch support
canvas.addEventListener('touchmove', (e) => {
if (state !== 'PLAYING') return;
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const touchX = (e.touches[0].clientX - rect.left) * scaleX;
paddle.x = Math.max(10, Math.min(W - paddle.w - 10, touchX - paddle.w / 2));
}, { passive: false });
canvas.addEventListener('touchstart', (e) => {
if (state === 'PLAYING') {
e.preventDefault();
handleActionTrigger();
}
}, { passive: false });
function handleActionTrigger() {
sfx.init();
if (state !== 'PLAYING') return;
let launched = false;
for (const b of balls) {
if (b.stuck) {
b.stuck = false;
const angle = -Math.PI / 2 + (Math.random() * 0.4 - 0.2);
b.vx = Math.cos(angle) * b.speed;
b.vy = Math.sin(angle) * b.speed;
launched = true;
sfx.paddle();
}
}
if (!launched && paddle.laserTimer > 0 && paddle.laserCooldown <= 0) {
shootLasers();
}
}
function shootLasers() {
lasers.push({ x: paddle.x + 10, y: paddle.y - 6, vy: -650 });
lasers.push({ x: paddle.x + paddle.w - 10, y: paddle.y - 6, vy: -650 });
paddle.laserCooldown = 0.22;
sfx.laser();
}
// --- GAME FLOW ---
actionBtn.addEventListener('click', () => {
sfx.init();
if (state === 'MENU' || state === 'GAMEOVER' || state === 'VICTORY') {
startNewGame();
} else if (state === 'PAUSED') {
resumeGame();
} else if (state === 'LEVEL_CLEAR') {
levelIdx++;
loadLevel(levelIdx);
setMenu(false);
state = 'PLAYING';
}
});
function setMenu(visible, title = '', subtitle = '', btnText = '') {
if (visible) {
menuTitle.textContent = title;
menuSubtitle.textContent = subtitle;
actionBtn.textContent = btnText;
menuOverlay.classList.remove('hidden');
} else {
menuOverlay.classList.add('hidden');
}
}
function pauseGame() {
state = 'PAUSED';
setMenu(true, 'GAME PAUSED', 'Take a breather.', 'RESUME');
}
function resumeGame() {
state = 'PLAYING';
setMenu(false);
}
function startNewGame() {
score = 0;
lives = 3;
levelIdx = 0;
combo = 0;
updateHUD();
loadLevel(levelIdx);
setMenu(false);
state = 'PLAYING';
}
function loadLevel(idx) {
bricks = [];
powerups = [];
lasers = [];
particles = [];
floatingTexts = [];
const lvl = LEVELS[idx % LEVELS.length];
const rows = lvl.grid.length;
const cols = 12;
const padX = 46;
const padY = 75;
const gap = 6;
const bw = (W - padX * 2 - gap * (cols - 1)) / cols;
const bh = 22;
for (let r = 0; r < rows; r++) {
const rowStr = lvl.grid[r];
for (let c = 0; c < cols; c++) {
const ch = parseInt(rowStr[c], 10);
if (!ch || !BRICK_TYPES[ch]) continue;
const bt = BRICK_TYPES[ch];
bricks.push({
x: padX + c * (bw + gap),
y: padY + r * (bh + gap),
w: bw,
h: bh,
type: ch,
hp: bt.hp,
maxHp: bt.hp,
color: bt.color,
glow: bt.glow,
score: bt.score,
hitScale: 0,
entryDelay: (r * cols + c) * 0.012
});
}
}
resetPaddleAndBall();
updateHUD();
spawnFloatingText(W / 2, H / 2 + 20, `STAGE ${idx + 1}: ${lvl.name}`, '#00f3ff', 24, 1.8);
}
function resetPaddleAndBall() {
paddle.targetW = 110;
paddle.w = 110;
paddle.x = W / 2 - paddle.w / 2;
paddle.laserTimer = 0;
combo = 0;
updateComboUI();
const baseSpeed = 420 + levelIdx * 28;
balls = [{
x: paddle.x + paddle.w / 2,
y: paddle.y - 9,
r: 7,
vx: 0,
vy: 0,
speed: baseSpeed,
stuck: true,
fireTimer: 0,
trail: []
}];
}
function updateHUD() {
scoreEl.textContent = score;
if (score > highScore) {
highScore = score;
highEl.textContent = highScore;
localStorage.setItem('neon_breakout_high', highScore);
}
levelEl.textContent = levelIdx + 1;
livesEl.textContent = lives;
}
function updateComboUI() {
if (combo >= 2) {
comboEl.textContent = `${combo}x COMBO!`;
comboEl.style.opacity = '1';
comboEl.style.transform = 'scale(1.25)';
setTimeout(() => { comboEl.style.transform = 'scale(1)'; }, 90);
} else {
comboEl.style.opacity = '0';
}
}
// --- JUICE & EFFECTS ---
function addShake(amount) {
shakeMag = Math.min(shakeMag + amount, 18);
}
function spawnParticles(x, y, color, count = 16, speedMult = 1) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const spd = (Math.random() * 190 + 50) * speedMult;
particles.push({
x,
y,
vx: Math.cos(angle) * spd,
vy: Math.sin(angle) * spd,
size: Math.random() * 5 + 3,
color,
alpha: 1,
life: Math.random() * 0.45 + 0.25,
maxLife: 0.7,
rot: Math.random() * Math.PI,
vRot: (Math.random() - 0.5) * 10
});
}
}
function spawnFloatingText(x, y, text, color = '#fff', size = 15, life = 0.85) {
floatingTexts.push({ x, y, text, color, size, life, maxLife: life, vy: -45 });
}
function maybeDropPowerup(x, y) {
if (Math.random() > 0.19) return;
const p = POWERUP_TYPES[Math.floor(Math.random() * POWERUP_TYPES.length)];
powerups.push({
x: x - 24,
y: y - 10,
w: 48,
h: 20,
vy: 145,
type: p.type,
color: p.color,
label: p.label
});
}
function activatePowerup(p) {
sfx.powerup();
score += 250;
updateHUD();
spawnFloatingText(paddle.x + paddle.w / 2, paddle.y - 15, p.label, p.color, 18);
spawnParticles(paddle.x + paddle.w / 2, paddle.y, p.color, 20, 0.9);
if (p.type === 'WIDE') {
paddle.targetW = Math.min(210, paddle.targetW + 45);
} else if (p.type === 'LASER') {
paddle.laserTimer = 10;
} else if (p.type === 'LIFE') {
lives++;
updateHUD();
} else if (p.type === 'FIRE') {
balls.forEach(b => b.fireTimer = 8);
} else if (p.type === 'MULTI') {
const source = balls[0];
if (source && balls.length < 9) {
for (const offset of [-0.3, 0.3]) {
const baseAngle = source.stuck ? -Math.PI / 2 : Math.atan2(source.vy, source.vx);
const newAngle = baseAngle + offset;
balls.push({
x: source.x,
y: source.y,
r: source.r,
vx: Math.cos(newAngle) * source.speed,
vy: Math.sin(newAngle) * source.speed,
speed: source.speed,
stuck: false,
fireTimer: source.fireTimer,
trail: []
});
}
}
}
}
function damageBrick(b, idx, isFireball = false) {
if (b.type === 9) {
if (!isFireball) {
b.hitScale = 1;
sfx.brick(combo, true);
addShake(3);
spawnParticles(b.x + b.w / 2, b.y + b.h / 2, '#94a3b8', 6, 0.6);
}
return;
}
b.hp = isFireball ? 0 : b.hp - 1;
b.hitScale = 1;
combo++;
updateComboUI();
sfx.brick(combo, false);
const cx = b.x + b.w / 2;
const cy = b.y + b.h / 2;
if (b.hp <= 0) {
bricks.splice(idx, 1);
const mult = Math.min(combo, 8);
const pts = b.score * mult;
score += pts;
updateHUD();
addShake(6.5);
bgPulse = 0.28;
spawnParticles(cx, cy, b.color, 22, 1.15);
spawnFloatingText(cx, cy, `+${pts}`, b.color, mult > 2 ? 17 : 14);
maybeDropPowerup(cx, cy);
checkLevelComplete();
} else {
addShake(3.5);
spawnParticles(cx, cy, b.color, 9, 0.7);
}
}
function checkLevelComplete() {
const remaining = bricks.some(b => b.type !== 9);
if (!remaining) {
sfx.winLevel();
if (levelIdx + 1 >= LEVELS.length) {
state = 'VICTORY';
setMenu(true, 'YOU CONQUERED NEON!', `Final Score: ${score}`, 'PLAY AGAIN');
} else {
state = 'LEVEL_CLEAR';
setMenu(true, 'STAGE CLEARED!', `Get ready for Stage ${levelIdx + 2}`, 'NEXT STAGE');
}
}
}
// --- UPDATE LOOP ---
function update(dt) {
// Background stars always drift
for (const s of bgStars) {
s.y += s.speed * dt;
if (s.y > H) {
s.y = 0;
s.x = Math.random() * W;
}
}
// Juice decay
if (shakeMag > 0) {
shakeMag *= Math.pow(0.001, dt);
if (shakeMag < 0.2) shakeMag = 0;
}
if (bgPulse > 0) bgPulse = Math.max(0, bgPulse - dt * 1.5);
paddleSquish += (1 - paddleSquish) * 14 * dt;
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 260 * dt; // subtle gravity
p.rot += p.vRot * dt;
p.life -= dt;
p.alpha = Math.max(0, p.life / p.maxLife);
if (p.life <= 0) particles.splice(i, 1);
}
// Update floating texts
for (let i = floatingTexts.length - 1; i >= 0; i--) {
const ft = floatingTexts[i];
ft.y += ft.vy * dt;
ft.life -= dt;
if (ft.life <= 0) floatingTexts.splice(i, 1);
}
if (state !== 'PLAYING') return;
// Brick animations
for (const b of bricks) {
if (b.entryDelay > 0) b.entryDelay = Math.max(0, b.entryDelay - dt);
if (b.hitScale > 0) b.hitScale = Math.max(0, b.hitScale - dt * 6);
}
// Paddle width lerp & timers
if (Math.abs(paddle.w - paddle.targetW) > 0.5) {
const diff = (paddle.targetW - paddle.w) * 10 * dt;
paddle.x -= diff / 2;
paddle.w += diff;
}
if (paddle.laserTimer > 0) paddle.laserTimer = Math.max(0, paddle.laserTimer - dt);
if (paddle.laserCooldown > 0) paddle.laserCooldown = Math.max(0, paddle.laserCooldown - dt);
// Keyboard movement
let moveDir = 0;
if (keys.ArrowLeft || keys.KeyA) moveDir -= 1;
if (keys.ArrowRight || keys.KeyD) moveDir += 1;
if (moveDir !== 0) {
paddle.vx = moveDir * paddle.speed;
paddle.x += paddle.vx * dt;
paddle.x = Math.max(10, Math.min(W - paddle.w - 10, paddle.x));
} else {
paddle.vx *= 0.85;
}
// Lasers
for (let i = lasers.length - 1; i >= 0; i--) {
const l = lasers[i];
l.y += l.vy * dt;
if (l.y < 0) {
lasers.splice(i, 1);
continue;
}
for (let j = bricks.length - 1; j >= 0; j--) {
const b = bricks[j];
if (l.x >= b.x && l.x <= b.x + b.w && l.y >= b.y && l.y <= b.y + b.h) {
damageBrick(b, j, false);
lasers.splice(i, 1);
break;
}
}
}
// Powerups
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.y += p.vy * dt;
if (
p.x + p.w >= paddle.x &&
p.x <= paddle.x + paddle.w &&
p.y + p.h >= paddle.y &&
p.y <= paddle.y + paddle.h
) {
activatePowerup(p);
powerups.splice(i, 1);
} else if (p.y > H) {
powerups.splice(i, 1);
}
}
// Balls
for (let i = balls.length - 1; i >= 0; i--) {
const b = balls[i];
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;
continue;
}
// Store trail
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 12) b.trail.shift();
// Sub-stepping for crisp collision
const steps = 3;
const sDt = dt / steps;
for (let s = 0; s < steps; s++) {
b.x += b.vx * sDt;
b.y += b.vy * sDt;
// Walls
if (b.x - b.r < 0) {
b.x = b.r;
b.vx = Math.abs(b.vx);
sfx.wall();
spawnParticles(b.x, b.y, '#00f3ff', 5, 0.5);
} else if (b.x + b.r > W) {
b.x = W - b.r;
b.vx = -Math.abs(b.vx);
sfx.wall();
spawnParticles(b.x, b.y, '#00f3ff', 5, 0.5);
}
if (b.y - b.r < 0) {
b.y = b.r;
b.vy = Math.abs(b.vy);
sfx.wall();
spawnParticles(b.x, b.y, '#00f3ff', 5, 0.5);
}
// 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)) / (paddle.w / 2);
const clampedHit = Math.max(-1, Math.min(1, hitPos));
const maxBounceAngle = Math.PI * 0.37; // ~66 deg
const bounceAngle = clampedHit * maxBounceAngle;
b.speed = Math.min(720, b.speed + 4);
b.vx = Math.sin(bounceAngle) * b.speed;
b.vy = -Math.cos(bounceAngle) * b.speed;
b.y = paddle.y - b.r - 0.5;
combo = 0;
updateComboUI();
paddleSquish = 0.65;
sfx.paddle();
spawnParticles(b.x, paddle.y, '#00f3ff', 8, 0.6);
break;
}
// Bricks collision
for (let j = bricks.length - 1; j >= 0; j--) {
const br = bricks[j];
if (br.entryDelay > 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.type !== 9;
if (!isFire) {
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;
} else {
b.vy = -b.vy;
}
// Prevent vertical infinite loops with indestructible blocks
if (Math.abs(b.vy) < 60) {
b.vy = (b.vy >= 0 ? 1 : -1) * 90;
}
}
damageBrick(br, j, isFire);
break;
}
}
}
// Bottom out
if (b.y - b.r > H) {
balls.splice(i, 1);
}
}
// Check life lost
if (balls.length === 0 && state === 'PLAYING') {
lives--;
updateHUD();
sfx.loseLife();
addShake(14);
if (lives <= 0) {
state = 'GAMEOVER';
setMenu(true, 'GAME OVER', `Final Score: ${score}`, 'TRY AGAIN');
} else {
resetPaddleAndBall();
}
}
}
// --- RENDERING ---
function drawRoundedRect(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
if (shakeMag > 0) {
const dx = (Math.random() - 0.5) * shakeMag * 2;
const dy = (Math.random() - 0.5) * shakeMag * 2;
ctx.translate(dx, dy);
}
// Clear Background
ctx.fillStyle = '#07070c';
ctx.fillRect(0, 0, W, H);
// Subtle Grid & Pulse
ctx.strokeStyle = `rgba(0, 243, 255, ${0.035 + bgPulse * 0.15})`;
ctx.lineWidth = 1;
const gridSize = 40;
for (let x = 0; x < W; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, H);
ctx.stroke();
}
for (let y = 0; y < H; y += gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
// Background Stars
for (const s of bgStars) {
ctx.fillStyle = `rgba(255, 255, 255, ${s.alpha})`;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
// Draw Bricks
for (const b of bricks) {
if (b.entryDelay > 0) continue;
const pop = b.hitScale * 4;
const bx = b.x - pop / 2;
const by = b.y - pop / 2;
const bw = b.w + pop;
const bh = b.h + pop;
ctx.save();
ctx.shadowColor = b.glow;
ctx.shadowBlur = 12;
ctx.fillStyle = b.hitScale > 0.3 ? '#ffffff' : b.color;
drawRoundedRect(bx, by, bw, bh, 4);
ctx.fill();
// Inner bevel highlight
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
drawRoundedRect(bx + 2, by + 2, bw - 4, bh * 0.38, 2);
ctx.fill();
// Multi-hit cracks/dots
if (b.maxHp > 1 && b.maxHp < Infinity) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';
for (let i = 0; i < b.hp; i++) {
ctx.beginPath();
ctx.arc(bx + bw / 2 + (i - (b.hp - 1) / 2) * 10, by + bh / 2 + 2, 2.5, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
// Draw Powerups
for (const p of powerups) {
ctx.save();
ctx.shadowColor = p.color;
ctx.shadowBlur = 12;
ctx.fillStyle = '#11111e';
ctx.strokeStyle = p.color;
ctx.lineWidth = 2;
drawRoundedRect(p.x, p.y, p.w, p.h, 8);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(p.label, p.x + p.w / 2, p.y + p.h / 2 + 1);
ctx.restore();
}
// Draw Lasers
ctx.save();
ctx.shadowColor = '#ff0055';
ctx.shadowBlur = 10;
ctx.fillStyle = '#ff0055';
for (const l of lasers) {
ctx.fillRect(l.x - 2, l.y, 4, 14);
}
ctx.restore();
// Draw Paddle
ctx.save();
const pColor = paddle.laserTimer > 0 ? '#ff0055' : '#00f3ff';
ctx.shadowColor = pColor;
ctx.shadowBlur = 16;
ctx.fillStyle = pColor;
const ph = paddle.h * paddleSquish;
const py = paddle.y + (paddle.h - ph);
drawRoundedRect(paddle.x, py, paddle.w, ph, 7);
ctx.fill();
// Paddle core
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffffff';
drawRoundedRect(paddle.x + 8, py + 3, paddle.w - 16, ph - 6, 4);
ctx.fill();
// Laser turrets if active
if (paddle.laserTimer > 0) {
ctx.fillStyle = '#ff0055';
ctx.fillRect(paddle.x + 6, py - 5, 6, 5);
ctx.fillRect(paddle.x + paddle.w - 12, py - 5, 6, 5);
}
ctx.restore();
// Draw Balls & Trails
for (const b of balls) {
const isFire = b.fireTimer > 0;
const bColor = isFire ? '#ff7700' : '#00f3ff';
// Trail
for (let i = 0; i < b.trail.length; i++) {
const t = b.trail[i];
const ratio = (i + 1) / b.trail.length;
ctx.fillStyle = isFire
? `rgba(255, 119, 0, ${ratio * 0.45})`
: `rgba(0, 243, 255, ${ratio * 0.4})`;
ctx.beginPath();
ctx.arc(t.x, t.y, b.r * ratio * 0.9, 0, Math.PI * 2);
ctx.fill();
}
// Ball body
ctx.save();
ctx.shadowColor = bColor;
ctx.shadowBlur = 15;
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Launch Prompt
if (b.stuck && state === 'PLAYING') {
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.font = 'bold 13px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('CLICK OR SPACE TO LAUNCH', W / 2, H - 80);
}
}
// Draw Particles
for (const p of particles) {
ctx.save();
ctx.globalAlpha = p.alpha;
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.shadowColor = p.color;
ctx.shadowBlur = 8;
ctx.fillStyle = p.color;
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
ctx.restore();
}
// Draw Floating Texts
for (const ft of floatingTexts) {
ctx.save();
ctx.globalAlpha = Math.max(0, ft.life / ft.maxLife);
ctx.font = `900 ${ft.size}px sans-serif`;
ctx.fillStyle = ft.color;
ctx.shadowColor = ft.color;
ctx.shadowBlur = 8;
ctx.textAlign = 'center';
ctx.fillText(ft.text, ft.x, ft.y);
ctx.restore();
}
ctx.restore();
}
// --- MAIN LOOP ---
let lastTime = performance.now();
function loop(now) {
const dt = Math.min(0.05, (now - lastTime) / 1000);
lastTime = now;
update(dt);
render();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>

